tools/audit_tracks.py и reports/tracks_audit.md — аудит всех дорожек техданных по data/raw с планом. Пункт 1 плана: harvest_core.codec_line_rows добавляет студии из строк «Аудио 2: AC3 … (MVO, LostFilm)», «Аудио Rus: … | TVShows» (studio_source=codec), которых нет среди строк релиза; тот же шаг в tools/relabel_studios.py (повторный запуск ничего не добавляет). labels.csv: +6 428 строк в 2 862 релизах (kinozal 3 444, nnm 1 609, rutracker 1 375), со студией 29 331 -> 35 759. tests/e2e_segmenter.py: модель через E2E_MODEL. Co-Authored-By: Claude Opus 5.5 <[email protected]>
137 lines
6.2 KiB
Python
137 lines
6.2 KiB
Python
"""Доразметка студий в data/labels.csv без пересборки страниц.
|
||
|
||
То же, что теперь делает сборщик (tools/harvest/harvest_core.py) при переиндексации,
|
||
но по готовому labels.csv и data/raw — минуты вместо полного разбора HTML:
|
||
|
||
* nnm, строки «Перевод:» (studio_source=tech) с «(не указана)»: студия, названная
|
||
в самой части строки («Дублированный (Невафильм)», «… - Jaskier») — nnm_part_studio;
|
||
* kinozal: студии из заголовка раздачи «ПМ (LostFilm, HDrezka Studio)», которых нет
|
||
среди строк релиза (studio_source=title) — kz_title_rows по data/raw/kinozal/<id>.json;
|
||
* все трекеры: студии из кодек-строк «Аудио 2: AC3 … (MVO, LostFilm)», «Аудио Rus: … | TVShows»,
|
||
которых нет среди строк релиза (studio_source=codec) — codec_line_rows по data/raw/<трекер>/<id>.json.
|
||
|
||
Ручные релизы (data/manual_ids.txt) не трогаются. Перед записью — копия
|
||
data/labels.bak_relabel_<время>.csv. Повторный запуск ничего не добавляет.
|
||
|
||
.venv\\Scripts\\python tools\\relabel_studios.py # применить
|
||
.venv\\Scripts\\python tools\\relabel_studios.py --dry-run # только статистика
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import csv
|
||
import json
|
||
import re
|
||
import shutil
|
||
import sys
|
||
import time
|
||
from collections import Counter, defaultdict
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
sys.path.insert(0, str(ROOT))
|
||
sys.path.insert(0, str(ROOT / "tools" / "harvest"))
|
||
|
||
import harvest_core as H # noqa: E402
|
||
|
||
LABELS = ROOT / "data" / "labels.csv"
|
||
RAW = ROOT / "data" / "raw"
|
||
RAW_KZ = RAW / "kinozal"
|
||
MANUAL = ROOT / "data" / "manual_ids.txt"
|
||
NO_STUDIO = "(не указана)"
|
||
|
||
|
||
def relabel(rows: list[dict], raw_kz: Path = RAW_KZ, manual: set[str] | None = None) -> tuple[list[dict], Counter]:
|
||
"""rows -> (новые rows, статистика). Исходные dict меняются на месте."""
|
||
manual = manual or set()
|
||
stats: Counter = Counter()
|
||
for r in rows:
|
||
if r["source"] == "nnm" and r.get("studio_source") == "tech" and r["studio"] == NO_STUDIO \
|
||
and r["release_id"] not in manual:
|
||
studio = H.nnm_part_studio(r.get("role_raw") or "")
|
||
if studio:
|
||
r["studio"] = studio
|
||
stats["nnm: студия из «Перевод:»"] += 1
|
||
by_rel: dict[str, list[dict]] = defaultdict(list)
|
||
for r in rows:
|
||
if r["source"] == "kinozal":
|
||
by_rel[r["release_id"]].append(r)
|
||
added: list[dict] = []
|
||
for rid, rel_rows in by_rel.items():
|
||
if rid in manual:
|
||
continue
|
||
path = raw_kz / f"{rid.removeprefix('kz-')}.json"
|
||
if not path.exists():
|
||
stats["kinozal: нет raw JSON"] += 1
|
||
continue
|
||
rec = json.loads(path.read_text(encoding="utf-8"))
|
||
rec["id"] = rid
|
||
text = "\n".join(x for x in (rec.get("release_tab") or "", rec.get("tech_tab") or "") if x)
|
||
new = H.kz_title_rows(rec, text, rel_rows)
|
||
if new:
|
||
stats["kinozal: релизов со студиями из заголовка"] += 1
|
||
stats["kinozal: студий из заголовка"] += len(new)
|
||
stats["kinozal: из них со строкой «Аудио»"] += sum(bool(x["audio_raw"]) for x in new)
|
||
added += new
|
||
rows = rows + added
|
||
by_rel = defaultdict(list)
|
||
for r in rows:
|
||
by_rel[(r["source"], r["release_id"])].append(r)
|
||
codec: list[dict] = []
|
||
for (src, rid), rel_rows in by_rel.items():
|
||
if rid in manual:
|
||
continue
|
||
text = raw_text(raw_kz.parent, src, rid)
|
||
if text is None:
|
||
continue
|
||
new = H.codec_line_rows({"id": rid, "source": src}, text, rel_rows)
|
||
if new:
|
||
stats[f"{src}: релизов со студиями из кодек-строк"] += 1
|
||
stats[f"{src}: студий из кодек-строк"] += len(new)
|
||
codec += new
|
||
return rows + codec, stats
|
||
|
||
|
||
def raw_text(raw_dir: Path, src: str, rid: str) -> str | None:
|
||
"""Текст страницы, как у сборщика: kinozal — «Релиз» + «Техданные», nnm/rutracker — пост."""
|
||
num = re.sub(r"^(kz|nnm|rt)-", "", rid)
|
||
for name in (f"{num}.json", f"{num}.legacy.json"):
|
||
path = raw_dir / src / name
|
||
if path.exists():
|
||
rec = json.loads(path.read_text(encoding="utf-8"))
|
||
if src == "kinozal":
|
||
return "\n".join(x for x in (rec.get("release_tab") or "", rec.get("tech_tab") or "") if x)
|
||
return rec.get("post") or ""
|
||
return None
|
||
|
||
|
||
def main() -> None:
|
||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||
ap.add_argument("--dry-run", action="store_true", help="только посчитать, файл не менять")
|
||
args = ap.parse_args()
|
||
with LABELS.open(encoding="utf-8-sig", newline="") as f:
|
||
reader = csv.DictReader(f)
|
||
fields = reader.fieldnames
|
||
rows = list(reader)
|
||
manual = set(MANUAL.read_text(encoding="utf-8").split()) if MANUAL.exists() else set()
|
||
before = sum(r["studio"] != NO_STUDIO for r in rows)
|
||
rows, stats = relabel(rows, manual=manual)
|
||
after = sum(r["studio"] != NO_STUDIO for r in rows)
|
||
for k, v in stats.items():
|
||
print(f"{k}: {v}")
|
||
print(f"строк: {len(rows)}; со студией: {before} -> {after}")
|
||
if args.dry_run:
|
||
return
|
||
backup = LABELS.with_name(f"labels.bak_relabel_{time.strftime('%Y%m%d_%H%M%S')}.csv")
|
||
shutil.copy(LABELS, backup)
|
||
with LABELS.open("w", encoding="utf-8-sig", newline="") as f:
|
||
w = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore")
|
||
w.writeheader()
|
||
w.writerows(rows)
|
||
print(f"записано: {LABELS.relative_to(ROOT)}; копия: {backup.name}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|