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 <[email protected]>
91 lines
4.0 KiB
Python
91 lines
4.0 KiB
Python
"""18+ из строк «Аудио … |Студия| 18+».
|
||
|
||
На rutracker/nnm пометка 18+ часто стоит не в строке «Перевод:», а только в
|
||
строке «Аудио», причём у студии может быть и обычная дорожка, и 18+:
|
||
|
||
Перевод: Профессиональный (многоголосый закадровый) HDRezka
|
||
Аудио: RUS: AC3, 2 ch, 192 kbps |HDrezka|
|
||
Аудио: RUS: AC3, 2 ch, 192 kbps |HDrezka| 18+
|
||
|
||
Модель видит только строку дорожки, поэтому 18+ из соседней строки ей не
|
||
достать: он добавляется здесь, по тексту страницы.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
|
||
RE_AUDIO_LINE = re.compile(r"^\s*Аудио\b.*$", re.I | re.M)
|
||
RE_ADULT = re.compile(r"(?<!\d)18\s*\+")
|
||
_NORM = re.compile(r"[\W_]+")
|
||
|
||
|
||
def _norm(s: str) -> str:
|
||
return _NORM.sub("", (s or "").casefold())
|
||
|
||
|
||
RE_CODEC = re.compile(r"(E-?AC-?3|[AА][CС]-?3|AAC|DTS-?HD|DTS|FLAC|TrueHD|Opus|MP3)", re.I)
|
||
RE_CH = re.compile(r"(\d(?:\.\d)?)\s*ch\b", re.I)
|
||
RE_KHZ = re.compile(r"(\d+(?:\.\d+)?)\s*kHz", re.I)
|
||
RE_KBPS = re.compile(r"(\d+)\s*kbps", re.I)
|
||
|
||
|
||
def audio_lines(text: str, studios: list[str]) -> list[dict]:
|
||
"""Строки «Аудио …», в которых названа одна из студий, с параметрами дорожки."""
|
||
names = {_norm(s): s for s in studios if len(_norm(s)) >= 3}
|
||
out = []
|
||
for m in RE_AUDIO_LINE.finditer(text or ""):
|
||
line = m.group(0)
|
||
flat = _norm(line)
|
||
hits = [k for k in names if k in flat]
|
||
if not hits:
|
||
continue
|
||
g = lambda rx: (rx.search(line).group(1) if rx.search(line) else "") # noqa: E731
|
||
out.append({"studio": names[max(hits, key=len)], # «Red Head Sound», а не «Red»
|
||
"adult": bool(RE_ADULT.search(line)),
|
||
"codec": g(RE_CODEC).upper().replace("А", "A").replace("С", "C"),
|
||
"channels": g(RE_CH), "khz": g(RE_KHZ), "kbps": g(RE_KBPS)})
|
||
return out
|
||
|
||
|
||
def audio_counts(text: str, studios: list[str]) -> dict[str, list[int]]:
|
||
"""{студия: [строк Аудио с 18+, строк Аудио без 18+]} для названных студий."""
|
||
out: dict[str, list[int]] = {}
|
||
for a in audio_lines(text, studios):
|
||
out.setdefault(a["studio"], [0, 0])[0 if a["adult"] else 1] += 1
|
||
return out
|
||
|
||
|
||
def apply_audio_18(result: dict, text: str) -> dict:
|
||
"""Дополняет результат релиза: 18+ из строк «Аудио» (на месте)."""
|
||
tracks = result.get("tracks") or []
|
||
studios = sorted({t["studio"] for t in tracks if t.get("studio")})
|
||
if not studios:
|
||
return result
|
||
counts = audio_counts(text, studios)
|
||
out: list[dict] = []
|
||
for t in tracks:
|
||
out.append(t)
|
||
for studio, (adult, plain) in counts.items():
|
||
if not adult:
|
||
continue
|
||
mine = [t for t in out if t.get("studio") == studio]
|
||
if not mine or any(t["is_18plus"] for t in mine):
|
||
continue
|
||
if plain: # есть и обычная, и 18+ дорожка -> отдельная 18+
|
||
base = mine[0]
|
||
extra = dict(base)
|
||
extra["is_18plus"] = True
|
||
extra["segment"] = f"{base['segment']} 18+"
|
||
extra["source"] = "audio"
|
||
out.insert(out.index(base) + 1, extra)
|
||
else: # все дорожки студии 18+
|
||
for t in mine:
|
||
t["is_18plus"] = True
|
||
result["tracks"] = out
|
||
result["studios"] = [{"studio": t["studio"], "translation_type": t["translation_type"],
|
||
"lang": t.get("lang"), "is_18plus": t["is_18plus"], "episodes": t.get("episodes")}
|
||
for t in out if t.get("studio")]
|
||
result["is_18plus"] = any(t["is_18plus"] for t in out)
|
||
return result
|