Датасет и обучение: класс «люб. дубляж», раскладка многотиповых строк, язык из «Аудио», строки из «Аудио», потолок повторов, веса классов
docker / build (push) Successful in 1m40s
docker / build (push) Successful in 1m40s
- harvest_core: дубляж — тип, а не часть имени студии; split_multi_rows; audio_only_rows; kinozal сопоставляет все аудиодорожки - prepare_data: --max-dup, отсев остатков многотиповых строк - train: веса классов (корень из обратной частоты, потолок 4), --no-class-weights - исправлены регулярки с \b, записанные как символ BS (predict.py, audio18.py) - PARSE_VERSION 3: кэш разбора сбрасывается - docs/dataset.md, docs/api-reference.md обновлены Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e648415719
commit
05537ac9e9
10 files changed
+233
-23
No files matched your search
+1
-1
@@ -1028,7 +1028,7 @@ def cmd_build(rewrite: bool = False, relabel: bool = False, runner: "Runner | No
|
||||
|
||||
# Версия разбора: поднимите при правке парсеров (harvest_core, html_to_text) — тогда
|
||||
# кэш «уже разобрано» сбросится сам и данные пересоберутся один раз.
|
||||
PARSE_VERSION = 2
|
||||
PARSE_VERSION = 3
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict:
|
||||
|
||||
@@ -48,6 +48,7 @@ _REPO = Path(__file__).resolve().parents[2]
|
||||
if str(_REPO) not in sys.path:
|
||||
sys.path.insert(0, str(_REPO))
|
||||
from src.audio18 import audio_counts, audio_lines # noqa: E402
|
||||
from src.html_extract import RE_LINE_PREFIX, RE_TYPE_HINT, _split_entries # noqa: E402
|
||||
from src.trackinfo import audio_lines_all, lang_of_text, pick_for_row # noqa: E402
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
@@ -1029,7 +1030,9 @@ STUDIO_NOISE = re.compile(
|
||||
r"\b(закадров\w*|многоголос\w*|двухголос\w*|одноголос\w*|"
|
||||
r"профессиональн\w*|любительск\w*|авторск\w*|дублированн\w*|"
|
||||
r"неофициальн\w*|официальн\w*|полный|перевод|роли озвучивали)\b", re.I)
|
||||
ROLE_PATTERNS = [(r"дублир|дубляж", "дубляж"), (r"многоголос", "многоголосый"),
|
||||
# «дубляж» как тип, а не часть имени студии («Дубляжная», «Продубляж», «Дубляж-Студия»)
|
||||
RE_DUB_TYPE = r"дублир|(?<![а-яё])дубляж(?![а-яё])"
|
||||
ROLE_PATTERNS = [(RE_DUB_TYPE, "дубляж"), (r"многоголос", "многоголосый"),
|
||||
(r"двухголос", "двухголосый"), (r"одноголос", "одноголосый"),
|
||||
(r"субтитр", "субтитры"), (r"оригинал", "оригинал")]
|
||||
|
||||
@@ -1094,6 +1097,8 @@ def translation_type(text: str) -> tuple[str, str]:
|
||||
kind = name
|
||||
break
|
||||
if kind == "дубляж":
|
||||
if re.search(r"любительск|псевдо", t): # любительский дубляж — отдельный класс, не «проф.»
|
||||
return "люб. дубляж", "люб."
|
||||
return "проф. дубляж", "проф."
|
||||
if "профессиональн" in t:
|
||||
pref = "проф."
|
||||
@@ -1243,7 +1248,7 @@ def _kz_parse_core(rec: dict) -> tuple[list[dict], dict]:
|
||||
if t["type"] in ("оригинал", "субтитры"):
|
||||
continue
|
||||
match = None
|
||||
for j, a in enumerate(ru_audio):
|
||||
for j, a in enumerate(audios): # по имени студии — среди всех языков (украинский дубляж и т.п.)
|
||||
if j in used:
|
||||
continue
|
||||
tag, st = a["tag"].lower(), t["studio"].lower()
|
||||
@@ -1251,8 +1256,8 @@ def _kz_parse_core(rec: dict) -> tuple[list[dict], dict]:
|
||||
match = (j, a)
|
||||
break
|
||||
if match is None:
|
||||
for j, a in enumerate(ru_audio):
|
||||
if j not in used and not a["tag"]:
|
||||
for j, a in enumerate(audios): # без имени — только безымянная русская дорожка
|
||||
if j not in used and not a["tag"] and a["lang"].startswith("RU"):
|
||||
match = (j, a)
|
||||
break
|
||||
if match:
|
||||
@@ -1270,7 +1275,7 @@ def _kz_parse_core(rec: dict) -> tuple[list[dict], dict]:
|
||||
"codec": a.get("codec", ""), "channels": a.get("channels", ""),
|
||||
"khz": a.get("khz", ""), "kbps": a.get("kbps", ""),
|
||||
"subtitles": subs, "studio_source": studio_source, "raw": t["line"],
|
||||
"lang": "RUS" if match else "", # в сопоставление идут только русские аудиодорожки
|
||||
"lang": (a.get("lang") or "RUS") if match else "",
|
||||
})
|
||||
rel = {"source": "kinozal", "release_id": rec["id"], "ru_title": info["ru_title"],
|
||||
"year": info["year"], "quality": rows[0]["quality"] if rows else "",
|
||||
@@ -1622,13 +1627,102 @@ def kz_audio_only_rows(rec: dict, text: str) -> list[dict]:
|
||||
return rows
|
||||
|
||||
|
||||
def split_multi_rows(rows: list[dict]) -> list[dict]:
|
||||
"""Строка с несколькими дорожками («Проф. многоголосый, авторский (одноголосый)») -> по строке на дорожку.
|
||||
|
||||
Режет теми же правилами, что сервис при разборе страницы (src.html_extract._split_entries), чтобы
|
||||
обучение и инференс видели одинаковые фрагменты. Одну дорожку («Многоголосый, профессиональный -
|
||||
LostFilm») не трогает. nnm tech-строки уже по одной на дорожку (role_raw) — тоже не трогает.
|
||||
"""
|
||||
out: list[dict] = []
|
||||
for r in rows:
|
||||
if r.get("studio_source") == "tech" and (r.get("role_raw") or "").strip():
|
||||
out.append(r)
|
||||
continue
|
||||
body = RE_LINE_PREFIX.sub("", (r.get("raw") or "").strip())
|
||||
parts = [p for p in _split_entries(re.sub(r"\s+", " ", body)) if RE_TYPE_HINT.search(p)]
|
||||
if len(parts) < 2:
|
||||
out.append(r)
|
||||
continue
|
||||
base_studio = (r.get("studio") or "").strip()
|
||||
for part in parts:
|
||||
e = rt_parse_entry(part)
|
||||
if e["type"] in ("оригинал", "субтитры"):
|
||||
continue
|
||||
new = dict(r)
|
||||
keep = base_studio not in ("", "(не указана)") and base_studio.casefold() in part.casefold()
|
||||
new.update(raw=part, translation_type=e["type"], professional=e["pro"],
|
||||
role_raw=e["role"], studio=base_studio if keep else (e["studio"] or "(не указана)"),
|
||||
age_18=int(has_18(part) or (keep and int(r.get("age_18") or 0))))
|
||||
new["note"] = mark_18(e["note"] or "", bool(new["age_18"]))
|
||||
out.append(new)
|
||||
return out
|
||||
|
||||
|
||||
def audio_only_rows(rec: dict, text: str, rows: list[dict]) -> list[dict]:
|
||||
"""Строки «Аудио … |MVO, Студия|» с языком, которых нет среди строк разметки (украинский дубляж и т.п).
|
||||
|
||||
Берутся только дорожки с блоком |…| (тип, студия) и определимым типом; оригинал и комментарии —
|
||||
не обучающие строки. Имена из скобок («(L,C,R,l,r)» — раскладка каналов) сюда не попадают.
|
||||
"""
|
||||
def key(x: str) -> str:
|
||||
return re.sub(r"[\W_]+", "", (x or "").casefold())
|
||||
|
||||
have = key(" ".join(((r.get("studio") or "") + " " + (r.get("raw") or "")) for r in rows))
|
||||
out = []
|
||||
for a in audio_lines_all(text):
|
||||
if a["original"] or a["commentary"]:
|
||||
continue
|
||||
m = re.search(r"\|([^|]+)\|", a["raw"])
|
||||
if not m:
|
||||
continue
|
||||
chunk = m.group(1)
|
||||
tokens = [t.strip() for t in re.split(r"\s*[,;/]\s*", chunk) if t.strip()]
|
||||
kind = pro = ""
|
||||
for t in tokens:
|
||||
if t.upper().replace(".", "") in NNM_TAG_NAMES: # короткий тег: DUB, MVO, ПМ …
|
||||
kind, pro = nnm_tag_type(t)
|
||||
break
|
||||
k, pr = translation_type(t)
|
||||
if not k.endswith("неизвестно"):
|
||||
kind, pro = k, pr
|
||||
break
|
||||
if not kind:
|
||||
continue
|
||||
studio = ""
|
||||
for t in tokens:
|
||||
if t.upper().replace(".", "") in NNM_TAG_NAMES or re.search(r"(?i)голос|дубл|оригинал|original", t):
|
||||
continue
|
||||
st, note = clean_studio(t)
|
||||
st, extra = canonical_studio(st)
|
||||
if len(st) >= 3:
|
||||
studio = st
|
||||
break
|
||||
if not studio or key(studio) in have or re.match(r"(?i)(?:blu-?ray|bd|dvd|web)\b", studio): # источник, а не студия
|
||||
continue
|
||||
base = rows[0] if rows else {}
|
||||
out.append({
|
||||
"source": base.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": chunk.strip(), "actors": "",
|
||||
"note": mark_18(" ".join(x for x in (note, extra) if x).strip(), a["adult"]),
|
||||
"age_18": int(a["adult"]), "codec": a["codec"], "channels": a["channels"], "khz": "",
|
||||
"kbps": a["kbps"], "subtitles": "", "studio_source": "audio",
|
||||
"raw": a["raw"], "lang": a["lang"], "audio_raw": a["raw"]})
|
||||
have += key(studio)
|
||||
return out
|
||||
|
||||
|
||||
def _with_audio(core, texts):
|
||||
"""Обёртка парсера: lang/audio_raw у строк; число переводов в rel — по итоговым строкам."""
|
||||
def parse(rec: dict) -> tuple[list[dict], dict]:
|
||||
rows, rel = core(rec)
|
||||
text = texts(rec)
|
||||
if rows:
|
||||
rows = enrich_rows(rows, text)
|
||||
rows = enrich_rows(split_multi_rows(rows), text)
|
||||
rows += audio_only_rows(rec, text, rows)
|
||||
elif core is _kz_parse_core:
|
||||
rows = kz_audio_only_rows(rec, text)
|
||||
if rows:
|
||||
|
||||
Reference in new issue
Block a user