Студии из кодек-строк: 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>
This commit is contained in:
1 parent
63878f7874
commit
e3af778068
10 files changed
+7937
-29
No files matched your search
@@ -1784,6 +1784,125 @@ def kz_title_rows(rec: dict, text: str, rows: list[dict]) -> list[dict]:
|
||||
return out
|
||||
|
||||
|
||||
# кодек-строка одной дорожки: «Аудио 2: …», «Аудио Rus: …», «Аудио pic RUS: …», «Звук #1: …»,
|
||||
# «• №3: Русский (АС3 …)», «Russian (Dolby AC3, 6 ch) …»
|
||||
RE_CODEC_LINE = re.compile(
|
||||
r"(?i)^\s*[•#№*\-\s]*(?:(?:аудио\w*|audio|звук\w*)\b[^:\n]{0,24}:|№\s*\d+\s*:|(?:russian|русск\w*|украин\w*)\b)")
|
||||
RE_CODEC_PARAM = re.compile(r"(?i)\b(?:e-?ac-?3|ac-?3|ас3|dts|aac|truehd|flac|mp3|opus|l?pcm|mp2)\b|\d\s*(?:kbps|кбит|kb/s)")
|
||||
RE_LANG_WORD = re.compile(
|
||||
r"(?i)русск|russian|\brus\b|украин|ukrain|\bukr\b|україн|англ|english|\beng\b|япон|japan|казах|kazakh"
|
||||
r"|француз|french|немецк|german|итальян|испан|корей|китайск")
|
||||
# не студия: язык, тех. параметры, тип, источник, пометки
|
||||
CODEC_NOT_STUDIO = re.compile(
|
||||
r"(?i)^(?:rus|ukr|eng|ru|ua|int|ext|pic|dub|mvo|dvo|avo|vo|лицензи\w*|ненормативн\w*.*|.*лексик\w*"
|
||||
r"|присутствует.*|внешн\w*|в составе.*|серии.*|эп\w*\.?\s*\d.*|[\d\s.,:/\-]+|.*\b(?:kbps|ch|khz|кбит)\b.*"
|
||||
r"|.*(?:русск|английск|украинск|russian|english|ukrainian|оригинал|original)\w*.*"
|
||||
r"|.*(?:голос|дублир|закадр|перевод).*|.*(?<![а-яё])дубляж(?![а-яё]).*|звук.*"
|
||||
r"|.*(?:blu-?ray|itunes|netflix|amazon|\bbd\b|\bdvd\b|web-?dl).*)$")
|
||||
|
||||
|
||||
# имя из скобок кодек-строки, которого нет в словаре: «(L,C,R,l,r)», «(АС3 …)», «(1-8 серии)», «(поздний)»
|
||||
CODEC_NAME_NOISE = re.compile(
|
||||
r"(?i)^(?:[lrc]|sl|sr|ls|rs|lfe)\b|\b(?:[lrc]|lfe)\s*[,;]|^(?:cee|eur|vhs|cbr|vbr|lfe|stereo|custom"
|
||||
r"|line|production|россия|сек|[aа]{1,2}[сc]\w?|мр\w?|front|side)\b|\bmono\b|dolby|поздн|ранн|отдельн|тифло|комментар"
|
||||
r"|сезон|сери[ия]|эпизод|файл|контейнер|bits?\b|sound\s*track|sdh|forced|вставк|сэмпл|sample|неизвестн|unknown")
|
||||
|
||||
|
||||
def _lang_key(word: str) -> str:
|
||||
w = word.lower()
|
||||
return "RUS" if w.startswith(("русс", "russ", "rus")) else "UKR" if w.startswith(("укр", "ukr")) else w[:4]
|
||||
|
||||
|
||||
def _known_studios() -> re.Pattern | None:
|
||||
"""Словарь студий из data/labels.csv (≥ 5 строк): имена, которые kinozal пишет без скобок
|
||||
(«Аудио Rus: … | LostFilm», «… - Русский, LostFilm», «… MVO DniproFilm / HDRezka Studio»)."""
|
||||
global _KNOWN_RX
|
||||
if _KNOWN_RX is None:
|
||||
path = _REPO / "data" / "labels.csv"
|
||||
cnt: Counter = Counter()
|
||||
if path.exists():
|
||||
with path.open(encoding="utf-8-sig", newline="") as f:
|
||||
for r in csv.DictReader(f):
|
||||
if r.get("studio_source") == "codec": # словарь — без своих же строк: повторный прогон стабилен
|
||||
continue
|
||||
st = (r.get("studio") or "").strip()
|
||||
if len(st) >= 3 and st != "(не указана)" and not CODEC_NOT_STUDIO.match(st) \
|
||||
and st.lower() not in SOURCE_LIKE and not CODEC_NAME_NOISE.search(st):
|
||||
cnt[st] += 1
|
||||
names = sorted((s for s, c in cnt.items() if c >= 5), key=len, reverse=True)
|
||||
_KNOWN_RX = re.compile(r"(?i)(?<![\w])(" + "|".join(map(re.escape, names)) + r")(?![\w])") \
|
||||
if names else re.compile(r"(?!x)x")
|
||||
return _KNOWN_RX
|
||||
|
||||
|
||||
_KNOWN_RX: re.Pattern | None = None
|
||||
|
||||
|
||||
def codec_line_rows(rec: dict, text: str, rows: list[dict], known: re.Pattern | None = None) -> list[dict]:
|
||||
"""Студии из кодек-строк русских/украинских дорожек, которых нет среди строк разметки.
|
||||
|
||||
kinozal: «Перевод: Профессиональный (многоголосый закадровый)» без студий + «Аудио 2: AC3 …
|
||||
(MVO, LostFilm)», «Аудио Rus: … | TVShows»; nnm/rutracker — те же кодек-строки рядом с
|
||||
«Перевод N:» со списком, где этой студии нет. Строка-вход — сегмент кодек-строки, как его
|
||||
режет сервис (`src.release._entries`); тип — из самой строки (MVO, «дубляж»), иначе
|
||||
единственный тип релиза, иначе «неизвестно». Сводка из нескольких языков
|
||||
(«Аудио: Русский (AC3…), украинский (AC3…)»), оригинал и комментарии — пропускаются.
|
||||
"""
|
||||
from src.release import _entries, same_studio
|
||||
from src.trackinfo import parse_audio_line
|
||||
|
||||
known = known if known is not None else _known_studios()
|
||||
have = [r["studio"] for r in rows if r.get("studio") and r["studio"] != "(не указана)"]
|
||||
types = {r["translation_type"] for r in rows} - {"неизвестно", ""}
|
||||
base = rows[0] if rows else {}
|
||||
out: list[dict] = []
|
||||
for line in (text or "").splitlines():
|
||||
line = re.sub(r"\s+", " ", line).strip()
|
||||
if len(line) > 300 or not RE_CODEC_LINE.match(line) or not RE_CODEC_PARAM.search(line):
|
||||
continue
|
||||
if len({_lang_key(m.group(0)) for m in RE_LANG_WORD.finditer(line)}) > 1:
|
||||
continue
|
||||
info = parse_audio_line(re.sub(r"(?i)^\s*[^:]{0,30}:\s*", "", line, count=1))
|
||||
if info["original"] or info["commentary"] or info["lang"] not in ("RUS", "UKR", ""):
|
||||
continue
|
||||
if not info["lang"] and not re.search(r"(?i)\brus\b|\bukr\b", line):
|
||||
continue
|
||||
tag = NNM_TAG_RE.search(line) or NNM_TAG_RE.search(line.translate(CYR_TO_LAT)) # «АVO» кириллицей
|
||||
kind, pro = nnm_tag_type(tag.group(1)) if tag else ("", "")
|
||||
if not kind:
|
||||
kind, pro = translation_type(line)
|
||||
if kind.endswith("неизвестно"):
|
||||
kind, pro = "", ""
|
||||
if not kind:
|
||||
kind = next(iter(types)) if len(types) == 1 else "неизвестно"
|
||||
pro = kind.split()[0] if kind.split()[0] in ("проф.", "люб.", "авт.") else ""
|
||||
cands = [(m.group(1), True) for m in known.finditer(line)] + [(n, False) for n in info["names"]]
|
||||
for name, in_vocab in cands:
|
||||
studio, note = clean_studio(name)
|
||||
studio, extra = canonical_studio(studio)
|
||||
if len(studio) < 3 or studio.lower() in SOURCE_LIKE or CODEC_NOT_STUDIO.match(studio):
|
||||
continue
|
||||
if CODEC_NAME_NOISE.search(studio) or not in_vocab and re.search(r"\d", studio):
|
||||
continue
|
||||
if any(same_studio(studio, x) for x in have):
|
||||
continue
|
||||
entries = _entries(line)
|
||||
seg = next((e for e in entries if name.lower() in e.lower()), entries[0] if entries else line)
|
||||
out.append({
|
||||
"source": base.get("source") or rec.get("source", ""), "release_id": rec.get("id", ""),
|
||||
"ru_title": base.get("ru_title", ""), "year": base.get("year", ""),
|
||||
"quality": base.get("quality", ""), "is_series": base.get("is_series", False),
|
||||
"studio": studio, "translation_type": kind, "professional": pro,
|
||||
"role_raw": "", "actors": "",
|
||||
"note": mark_18(" ".join(x for x in (note, extra) if x).strip(), info["adult"]),
|
||||
"age_18": int(info["adult"]), "codec": info["codec"], "channels": info["channels"],
|
||||
"khz": "", "kbps": info["kbps"], "subtitles": "", "studio_source": "codec",
|
||||
"raw": seg, "lang": info["lang"] or ("UKR" if re.search(r"(?i)\bukr\b", line) else "RUS"),
|
||||
"audio_raw": line})
|
||||
have.append(studio)
|
||||
return out
|
||||
|
||||
|
||||
def _with_audio(core, texts):
|
||||
"""Обёртка парсера: lang/audio_raw у строк; число переводов в rel — по итоговым строкам."""
|
||||
def parse(rec: dict) -> tuple[list[dict], dict]:
|
||||
@@ -1797,6 +1916,7 @@ def _with_audio(core, texts):
|
||||
if core is _kz_parse_core:
|
||||
rows += kz_title_rows(rec, text, rows)
|
||||
if rows:
|
||||
rows += codec_line_rows(rec, text, rows)
|
||||
rel["n_translations"], rel["ok"] = len(rows), True
|
||||
rel["studios"] = " | ".join(sorted({r["studio"] for r in rows}))
|
||||
return rows, rel
|
||||
|
||||
Reference in new issue
Block a user