Files
grabber-ai/tools/relabel_studios.py
DenozordecandClaude Opus 5.5 e3af778068 Студии из кодек-строк: codec_line_rows, доразметка, аудит дорожек
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 <noreply@anthropic.com>
2026-11-02 20:30:52 +07:00

137 lines
6.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Доразметка студий в 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()