Несколько студий в одной строке, разбор противоречий, правки парсеров студий
docker / build (push) Successful in 1m21s
docker / build (push) Successful in 1m21s
Парсеры (PARSE_VERSION 9): «т/к» и единицы битрейта не режутся по слэшу, техпараметры (kb/s, Кбит/сек, 16 бит, channels, CBR, Atmos) и пол голоса (муж./жен.) не становятся студией, «от X» -> X; «N-й канал», «5 канал», «Лексикон» больше не выбрасываются как мусор; строка без типа со списком из 3+ имён (актёры, титры сабберов) не даёт дорожек; в кодек-строках имя рядом со скобкой пола становится кандидатом, а строка только с мусором даёт дорожку с типом без студии. Несколько авторов строки («Многоголосый (HDRezka Studio, LostFilm)» — верны все): prepare_data склеивает строки релиза в одну со списком spans, обучение размечает все спаны BIO, span_exact сравнивает наборы, decode_spans возвращает все прогоны; сервис отдаёт studio_spans / studios / studio_confidences, в ответе релиза — дорожка на каждого автора. Противоречий в датасете 20 % -> 1.2 %; проверочная tiny2 находит весь набор студий в 71 % строк с 2+ авторами (прежняя модель — 1 %). Разбор противоречий (collect.py, пункт 8): тексты, которые релизы разметили по-разному; решения в data/text_fixes.csv применяются в prepare_data поверх labels.csv. Метки и модель не пересобраны: нужны collect.py build --relabel, prepare_data, переобучение. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0f359837c5
commit
8511744d3a
15 files changed
+560
-119
No files matched your search
@@ -1100,11 +1100,18 @@ def mark_18(note: str, age: bool) -> str:
|
||||
return (note or "").strip()
|
||||
|
||||
|
||||
# пол голоса перед именем или вместо него: «(муж.)», «женская : Persona99», «муж./жен.»
|
||||
RE_GENDER_LEAD = re.compile(
|
||||
r"(?i)^(?:(?:мужск\w*|женск\w*|мужчин\w*|(?:муж|жен)\b\.?)\s*(?:[+/&,]|\s+и\s+)?\s*)+[:,\-–—]?\s*")
|
||||
|
||||
|
||||
def clean_studio(raw: str) -> tuple[str, str]:
|
||||
note = ""
|
||||
s = (raw or "").strip().strip("|").strip()
|
||||
s = RE_LANG_PREFIX.sub("", s) # «Русский, - AlexFilm» -> «AlexFilm»
|
||||
s = re.sub(r"(?i)^(?:озвучил\w*|озвучено|озвучивание|перевёл|перевел)\s*[-–—:]?\s*", "", s).strip()
|
||||
s = re.sub(r"(?i)^(?:озвучил\w*|озвучено|озвучивание|озвучк\w*|перевёл|перевел)\s*[-–—:]?\s*", "", s).strip()
|
||||
s = RE_GENDER_LEAD.sub("", s.lstrip(" :,;-–—"))
|
||||
s = re.sub(r"(?i)^от\s+", "", s) # «(муж.) от Ancord» -> «Ancord»
|
||||
m = RE_ON_ORDER.match(s) # «X по заказу Y» -> «X»
|
||||
if m:
|
||||
s = m.group(1)
|
||||
@@ -1250,7 +1257,10 @@ def _kz_parse_core(rec: dict) -> tuple[list[dict], dict]:
|
||||
if pm:
|
||||
role = pm.group(1)
|
||||
kind, pro = translation_type(body)
|
||||
for name in part_studios(body) or [""]:
|
||||
names = part_studios(body)
|
||||
if untyped_list(kind, names):
|
||||
continue
|
||||
for name in names or [""]:
|
||||
translations.append({"studio": name, "type": kind, "pro": pro,
|
||||
"role": role, "actors": actors, "note": "",
|
||||
"line": raw.strip(), "src": src})
|
||||
@@ -1467,7 +1477,7 @@ def _rt_parse_core(rec: dict) -> tuple[list[dict], dict]:
|
||||
continue
|
||||
# несколько студий в одной части: «(многоголосый) — Jaskier, TVShows, LostFilm»
|
||||
names = part_studios(part) or ([e["studio"]] if e["studio"] else [])
|
||||
if e["type"] == "неизвестно" and not names:
|
||||
if (e["type"] == "неизвестно" and not names) or untyped_list(e["type"], names):
|
||||
continue
|
||||
for name in names or [""]:
|
||||
entries.append({**e, "studio": name})
|
||||
@@ -1596,13 +1606,27 @@ STUDIO_SEP = re.compile(r"\s*[,;|/]\s*")
|
||||
SOURCE_RE = re.compile(r"(?i)^(?:blu-?ray|bd\b|dvd|web-?dl|webrip|hdtv|uhd|itunes|netflix|amazon|r5)")
|
||||
RE_CODEC_PARAM_ANY = re.compile(
|
||||
r"(?i)\b(?:e-?ac-?3|ac-?3|ас3|dts|aac|truehd|flac|mp3|opus|l?pcm|mp2|dolby|ddp?)\b"
|
||||
r"|\d+(?:[.,]\d+)?\s*(?:kbps|кбит|kb/s|khz|кгц|гц|hz|ch|канал\w*)\b")
|
||||
r"|\d+(?:[.,]\d+)?\s*(?:kbps|кбит|kb/s|khz|кгц|гц|hz|ch|канал(?:а|ов))\b")
|
||||
|
||||
|
||||
# описания дорожки, которые остаются после снятия слов типа: пол, голос, сокращения типа, единицы
|
||||
NOT_STUDIO_DESCR = re.compile(
|
||||
r"(?i)(?:мужск\w*|женск\w*|мужчин\w*|муж|жен|голос\w*|проф|люб|авт|сек|cbr|vbr|avg|kb|kbps"
|
||||
r"|\d+\s*(?:channels?|бит|bits?|kb)|\d+\s+(?:e-?ac-?3|ac-?3|dts|aac|mp3))\.?")
|
||||
# техпараметры в тексте перед разбиением на имена: «192 kb/s», «Кбит/сек», «2 channels», «5.1»
|
||||
RE_TECH_SCRUB = re.compile(
|
||||
r"(?i)\d+(?:[.,]\d+)?\s*(?:kbps|kb/s|kbit/s|кбит/с(?:ек)?|кб/с|kb|кбит|khz|кгц|гц|hz|ch|channels?|бит|bits?)\b"
|
||||
r"|\b(?:kb|kbit|кбит|кб)\s*/\s*(?:s|с|сек)\b"
|
||||
r"|\b(?:e-?ac-?3|ac-?3|ас-?3|dts(?:-hd)?|aac|truehd|atmos|flac|mp3|opus|l?pcm|dolby\s+digital|dolby|cbr|vbr|avg)\b"
|
||||
r"|\b\d\.\d\b")
|
||||
RE_TV_PREFIX = re.compile(r"(?i)\bт\s*/\s*[кc]\.?\s*") # «т/к Россия» -> «Россия» (слэш — не разделитель)
|
||||
|
||||
|
||||
def is_studio_junk(s: str) -> bool:
|
||||
"""Не имя студии: источник, тип, язык, техпараметры, «по умолчанию», число."""
|
||||
low = (s or "").strip().lower()
|
||||
return (len(low) < 3 or low in SOURCE_LIKE or low in STUDIO_PLACEHOLDERS
|
||||
or bool(NOT_STUDIO_DESCR.fullmatch(low))
|
||||
or bool(NOT_STUDIO_WORD.match(low)) or bool(CODEC_NOT_STUDIO.match(low))
|
||||
or bool(SOURCE_RE.match(low)) or bool(RE_NO_DUB.search(low))
|
||||
or bool(RE_CODEC_PARAM_ANY.match(low))
|
||||
@@ -1614,7 +1638,8 @@ def is_studio_junk(s: str) -> bool:
|
||||
def split_studios(text: str) -> list[str]:
|
||||
"""Перечисление студий -> имена: «Jaskier, TVShows, HDrezka Studio, LostFilm»."""
|
||||
out: list[str] = []
|
||||
for part in STUDIO_SEP.split((text or "").strip(" |.-–—")):
|
||||
text = RE_TECH_SCRUB.sub(" ", RE_TV_PREFIX.sub("", text or ""))
|
||||
for part in STUDIO_SEP.split(text.strip(" |.-–—")):
|
||||
st, _ = clean_studio(part.strip(" .|"))
|
||||
st, _ = canonical_studio(st)
|
||||
if st and not is_studio_junk(st) and st.casefold() not in {x.casefold() for x in out}:
|
||||
@@ -1650,6 +1675,11 @@ def split_translation_parts(body: str) -> list[str]:
|
||||
return parts if len(parts) > 1 and (roots >= 2 or starts >= 2) else [text]
|
||||
|
||||
|
||||
def untyped_list(kind: str, names: list[str]) -> bool:
|
||||
"""Строка без типа озвучки с перечнем из 3+ имён — актёры (оригинала или дубляжа), а не дорожки."""
|
||||
return kind == "неизвестно" and len(names) >= 3
|
||||
|
||||
|
||||
def part_studios(part: str) -> list[str]:
|
||||
"""Студии из части «Перевод:»: хвост после тире, скобки, остаток без слов типа."""
|
||||
names: list[str] = []
|
||||
@@ -1806,7 +1836,7 @@ def _nnm_parse_core(rec: dict) -> tuple[list[dict], dict]:
|
||||
if ttype in ("оригинал", "субтитры"):
|
||||
continue
|
||||
names = part_studios(part)
|
||||
if ttype == "неизвестно" and not names:
|
||||
if (ttype == "неизвестно" and not names) or untyped_list(ttype, names):
|
||||
continue
|
||||
# студия — если названа в части («Дублированный (Невафильм)»); без неё тип всё равно ценен
|
||||
for name in names or [""]:
|
||||
@@ -1830,10 +1860,9 @@ def _nnm_parse_core(rec: dict) -> tuple[list[dict], dict]:
|
||||
if "RUS" not in lang_tokens(body):
|
||||
continue
|
||||
names = audio_names(body)
|
||||
for team in re.findall(r"\[([^\[\]]{2,60})\]", body):
|
||||
for n in split_studios(team):
|
||||
if n.casefold() not in {x.casefold() for x in names}:
|
||||
names.append(n)
|
||||
teams = [n for team in re.findall(r"\[([^\[\]]{2,60})\]", body) for n in split_studios(team)]
|
||||
if teams: # «Русский (Berofu, Takera [AniMaunt])»: автор — команда, а не актёры
|
||||
names = list(dict.fromkeys(teams))
|
||||
for name in names[:8]:
|
||||
rows.append({
|
||||
"source": "nnm", "release_id": rec["id"], "ru_title": ru_title,
|
||||
@@ -2069,7 +2098,7 @@ RE_LANG_WORD = re.compile(
|
||||
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"(?i)^(?:rus|ukr|eng|ru|ua|int|ext|pic|dub|mvo|dvo|avo|vo|лицензи\w*|ненормативн\w*.*|.*\bлексик[аеиоу]\b.*"
|
||||
r"|присутствует.*|внешн\w*|в составе.*|серии.*|эп\w*\.?\s*\d.*|[\d\s.,:/\-]+|.*\b(?:kbps|ch|khz|кбит)\b.*"
|
||||
r"|.*(?:русск|английск|украинск|russian|english|ukrainian|оригинал|original|японск|китайск"
|
||||
r"|корейск|немецк|французск|итальянск|испанск|казахск)\w*.*"
|
||||
@@ -2127,12 +2156,19 @@ def _rec_base(rec: dict) -> dict:
|
||||
"quality": info["quality"], "is_series": info["is_series"]}
|
||||
|
||||
|
||||
RE_AFTER_GENDER = re.compile(r"(?i)\([^()]*\b(?:муж|жен)(?:\.|ск\w*|\b)[^()]*\)\s*:?\s*([^()\[\]|;,:\s][^()\[\]|;,:]{1,59}?)\s*(?=[(\[|;,]|$)")
|
||||
RE_BEFORE_GENDER = re.compile(r"(?i)\s[-–—]\s*([^()\[\]|;,:\s][^()\[\]|;,:]{1,59}?)\s*\([^()]*\b(?:муж|жен)(?:\.|ск\w*|\b)")
|
||||
|
||||
|
||||
def _line_names(line: str, info: dict, known: re.Pattern) -> list[tuple[str, bool]]:
|
||||
"""Кандидаты в студии кодек-строки: словарь, `|…|`, `<…>`, `[…]`, `(…)`."""
|
||||
cands: list[tuple[str, bool]] = [(m.group(1), True) for m in known.finditer(line)]
|
||||
cands += [(n, False) for n in info["names"]]
|
||||
for rx in (r"\|([^|]{2,60})\|", r"<([^<>]{2,60})>", r"\[([^\[\]]{2,60})\]", r"\(([^()]{2,80})\)"):
|
||||
cands += [(m.group(1), False) for m in re.finditer(rx, line)]
|
||||
# имена рядом со скобкой пола: «двухголосая (муж.+жен.) GreyMoon & Aska», «- Dejz & Mikrobelka (мужская/женская)»
|
||||
for rx in (RE_AFTER_GENDER, RE_BEFORE_GENDER):
|
||||
cands += [(m.group(1), False) for m in rx.finditer(line)]
|
||||
seen, out = set(), []
|
||||
for name, voc in cands:
|
||||
k = name.strip().casefold()
|
||||
@@ -2187,6 +2223,7 @@ def codec_line_rows(rec: dict, text: str, rows: list[dict], known: re.Pattern |
|
||||
kind = next(iter(types)) if len(types) == 1 else "неизвестно"
|
||||
pro = kind.split()[0] if kind.split()[0] in ("проф.", "люб.", "авт.") else ""
|
||||
names = _line_names(line, info, known)
|
||||
found = False # строка назвала настоящую студию (новую или уже известную)
|
||||
for name, in_vocab in names:
|
||||
studio, note = clean_studio(name)
|
||||
studio, extra = canonical_studio(studio)
|
||||
@@ -2195,6 +2232,7 @@ def codec_line_rows(rec: dict, text: str, rows: list[dict], known: re.Pattern |
|
||||
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):
|
||||
found = True
|
||||
continue
|
||||
entries = _entries(line)
|
||||
seg = next((e for e in entries if name.lower() in e.lower()), entries[0] if entries else line)
|
||||
@@ -2210,8 +2248,11 @@ def codec_line_rows(rec: dict, text: str, rows: list[dict], known: re.Pattern |
|
||||
"raw": seg, "lang": info["lang"] or ("UKR" if re.search(r"(?i)\bukr\b", line) else "RUS"),
|
||||
"audio_raw": line})
|
||||
have.append(studio)
|
||||
if not rows and not names and kind and kind != "неизвестно":
|
||||
# дорожка без имени: датасету ценен хотя бы тип; только для релизов без строк вовсе
|
||||
found = True
|
||||
junk_only = bool(names) and not found and kind not in types
|
||||
if (not rows and not names or junk_only) and kind and kind != "неизвестно":
|
||||
# дорожка без имени: датасету ценен хотя бы тип; для релизов без строк вовсе или когда
|
||||
# все кандидаты строки — мусор («(муж.+жен.)», «192 kb/s»), а такого типа у релиза ещё нет
|
||||
entries = _entries(line)
|
||||
out.append({
|
||||
"source": base.get("source") or rec.get("source", ""), "release_id": rec.get("id", ""),
|
||||
|
||||
Reference in new issue
Block a user