Язык дорожки из строки «Аудио» (UKR вместо RUS), студия |Дубляж, Имя|, каналы 5.1, оригинальная дорожка, язык в studios, защита от студий «Дубляжная»
docker / build (push) Successful in 1m40s
docker / build (push) Successful in 1m40s
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
9528113cfd
commit
e648415719
6 files changed
+88
-16
No files matched your search
+12
-6
@@ -35,9 +35,10 @@
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"tracks": [ /* дорожки, см. ниже */ ],
|
"tracks": [ /* дорожки, см. ниже */ ],
|
||||||
"studios": [{"studio": "LostFilm", "translation_type": "проф. многоголосый", "is_18plus": false, "episodes": "1-4"}],
|
"studios": [{"studio": "LostFilm", "translation_type": "проф. многоголосый", "lang": "RUS", "is_18plus": false, "episodes": "1-4"}],
|
||||||
"translation_types": ["проф. многоголосый"],
|
"translation_types": ["проф. многоголосый"],
|
||||||
"is_18plus": false,
|
"is_18plus": false,
|
||||||
|
"original": {"lang": "ENG", "codec": "E-AC3", "channels": 6, "bitrate_kbps": 768, "raw": "English E-AC3+Atmos / 5.1 / 48 kHz / 768 kbps |Original|"},
|
||||||
"series": {"season": "2", "episodes": "1-9", "total": 10, "by_studio": {"LostFilm": "1-4"}, "full_release": []},
|
"series": {"season": "2", "episodes": "1-9", "total": 10, "by_studio": {"LostFilm": "1-4"}, "full_release": []},
|
||||||
"title": "Название раздачи",
|
"title": "Название раздачи",
|
||||||
"segmenter": "model",
|
"segmenter": "model",
|
||||||
@@ -48,7 +49,8 @@
|
|||||||
| Поле | Тип | Описание |
|
| Поле | Тип | Описание |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `tracks` | array | дорожки релиза без дублей (дубль — одинаковые тип + студия) |
|
| `tracks` | array | дорожки релиза без дублей (дубль — одинаковые тип + студия) |
|
||||||
| `studios` | array | уникальные `{studio, translation_type, is_18plus, episodes}` |
|
| `studios` | array | уникальные `{studio, translation_type, lang, is_18plus, episodes}`; `lang` — язык дорожки студии (как в `tracks[].lang`, может быть `null`). Одна студия с русским и украинским дубляжом даёт две записи |
|
||||||
|
| `original` | object \| null | оригинальная дорожка из строки «Аудио … \|Original\|»: `{lang, codec, channels, bitrate_kbps, raw}` (поля могут быть `null`); `null` — такой строки на странице нет |
|
||||||
| `translation_types` | array of string | отсортированный набор типов (значения — из таблицы классов ниже) |
|
| `translation_types` | array of string | отсортированный набор типов (значения — из таблицы классов ниже) |
|
||||||
| `is_18plus` | bool | `true`, если 18+ хотя бы у одной дорожки |
|
| `is_18plus` | bool | `true`, если 18+ хотя бы у одной дорожки |
|
||||||
| `series` | object \| null | сезон, выпущенные серии, «какая студия какие серии» (для Sonarr); `null` — сведений нет |
|
| `series` | object \| null | сезон, выпущенные серии, «какая студия какие серии» (для Sonarr); `null` — сведений нет |
|
||||||
@@ -74,10 +76,10 @@
|
|||||||
| `age_confidence` | float 0..1 | вероятность предсказанного класса 18+ |
|
| `age_confidence` | float 0..1 | вероятность предсказанного класса 18+ |
|
||||||
| `lang` | string \| null | язык дорожки, ISO-подобный 3-буквенный код: `RUS`, `UKR`, `ENG`, также `BEL`, `KAZ`, `POL`, `FRA`, `DEU`, `ITA`, `SPA`, `JPN`, `KOR`, `CHI`, `CZE`, `LAV`, `LIT`, `EST`, `GEO`, `ARM`, `AZE`, `UZB`, `TUR`, `POR`, `HIN`; `null` — не определён |
|
| `lang` | string \| null | язык дорожки, ISO-подобный 3-буквенный код: `RUS`, `UKR`, `ENG`, также `BEL`, `KAZ`, `POL`, `FRA`, `DEU`, `ITA`, `SPA`, `JPN`, `KOR`, `CHI`, `CZE`, `LAV`, `LIT`, `EST`, `GEO`, `ARM`, `AZE`, `UZB`, `TUR`, `POR`, `HIN`; `null` — не определён |
|
||||||
| `lang_confidence` | float \| null | уверенность модели; заполнено только при `lang_source = "model"` |
|
| `lang_confidence` | float \| null | уверенность модели; заполнено только при `lang_source = "model"` |
|
||||||
| `lang_source` | `"model"` \| `"audio"` \| `"text"` \| null | откуда язык: нейросеть (классы `ENG`/`RUS`/`UKR`, порог 0.5), строка «Аудио» страницы или слова в тексте («Украинский дубляж») |
|
| `lang_source` | `"audio"` \| `"model"` \| `"text"` \| null | откуда язык, в порядке приоритета: строка «Аудио» страницы (`Ukrainian E-AC3 …` → `UKR`, самый надёжный источник), нейросеть (классы `ENG`/`RUS`/`UKR`, порог 0.5; без строки «Аудио» ей приходится угадывать по тексту), слова в тексте («Украинский дубляж») |
|
||||||
| `role` | string | `перевод`, `оригинал` (оригинальная дорожка без озвучки) или `комментарии` (комментарии режиссёра и т. п.) |
|
| `role` | string | `перевод`, `оригинал` (оригинальная дорожка без озвучки) или `комментарии` (комментарии режиссёра и т. п.) |
|
||||||
| `official` | bool | в строке «Аудио» есть слова «официальный» / «лицензионный» / «licensed» |
|
| `official` | bool | в строке «Аудио» есть слова «официальный» / «лицензионный» / «licensed» |
|
||||||
| `audio` | object \| null | `{codec, channels, bitrate_kbps}` из строки «Аудио» страницы (`ac3`, `6`, `448`); каждое поле может быть `null`; весь объект `null`, если строка не найдена |
|
| `audio` | object \| null | `{codec, channels, bitrate_kbps}` из строки «Аудио» страницы (`"E-AC3"`, `6`, `768`; `channels` — число каналов, «5.1» → 6); каждое поле может быть `null`; весь объект `null`, если строка не найдена |
|
||||||
| `source` | `"page"` \| `"title"` | только в ответе релиза: дорожка найдена в описании или студия взята из заголовка раздачи |
|
| `source` | `"page"` \| `"title"` | только в ответе релиза: дорожка найдена в описании или студия взята из заголовка раздачи |
|
||||||
| `episodes` | string \| null | серии, которые озвучивает дорожка («1-4», «1,3-5»); `null` — серии не указаны (покрывает весь релиз) |
|
| `episodes` | string \| null | серии, которые озвучивает дорожка («1-4», «1,3-5»); `null` — серии не указаны (покрывает весь релиз) |
|
||||||
| `episode_ranges` | array | то же в виде `[[1, 4]]`; пусто, если серий нет |
|
| `episode_ranges` | array | то же в виде `[[1, 4]]`; пусто, если серий нет |
|
||||||
@@ -158,7 +160,8 @@ type Track = {
|
|||||||
|
|
||||||
type Release = {
|
type Release = {
|
||||||
tracks: Track[]; // может быть пустым массивом
|
tracks: Track[]; // может быть пустым массивом
|
||||||
studios: { studio: string | null; translation_type: string; is_18plus: boolean; episodes: string | null }[];
|
studios: { studio: string | null; translation_type: string; lang: string | null; is_18plus: boolean; episodes: string | null }[];
|
||||||
|
original: { lang: string | null; codec: string | null; channels: number | null; bitrate_kbps: number | null; raw: string } | null;
|
||||||
translation_types: string[];
|
translation_types: string[];
|
||||||
is_18plus: boolean;
|
is_18plus: boolean;
|
||||||
series: {
|
series: {
|
||||||
@@ -211,7 +214,10 @@ Cloudflare: FlareSolverr нестабилен.
|
|||||||
несколько предсказаний: `translation_type` (14 классов), спан автора (разметка BIO по токенам),
|
несколько предсказаний: `translation_type` (14 классов), спан автора (разметка BIO по токенам),
|
||||||
флаг 18+ и язык (`ENG`/`RUS`/`UKR`).
|
флаг 18+ и язык (`ENG`/`RUS`/`UKR`).
|
||||||
6. **Постобработка.** Автор канонизируется словарём алиасов; 18+ ставится также по строке «Аудио»;
|
6. **Постобработка.** Автор канонизируется словарём алиасов; 18+ ставится также по строке «Аудио»;
|
||||||
язык: нейросеть → строка «Аудио» → слова в тексте; определяются `role`, `official`, `audio`.
|
язык: строка «Аудио» → нейросеть → слова в тексте; строка «Аудио» подбирается к дорожке по имени
|
||||||
|
студии из `|Дубляж, Le Doyen|` (слова-типы `Дубляж`/`MVO`/… отбрасываются); защита от имён вроде
|
||||||
|
«Дубляжная»: если дубляж назван только в имени студии, а в тексте указано число голосов, класс берётся
|
||||||
|
по тексту; отдельно берётся `original`; определяются `role`, `official`, `audio`.
|
||||||
7. **Агрегат релиза.** Дубли (тип + студия) склеиваются; студии из заголовка раздачи, которых нет
|
7. **Агрегат релиза.** Дубли (тип + студия) склеиваются; студии из заголовка раздачи, которых нет
|
||||||
в описании, добавляются с `source: "title"`; серии и сезон разбираются правилами (`src/episodes.py`).
|
в описании, добавляются с `source: "title"`; серии и сезон разбираются правилами (`src/episodes.py`).
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -84,7 +84,7 @@ def apply_audio_18(result: dict, text: str) -> dict:
|
|||||||
t["is_18plus"] = True
|
t["is_18plus"] = True
|
||||||
result["tracks"] = out
|
result["tracks"] = out
|
||||||
result["studios"] = [{"studio": t["studio"], "translation_type": t["translation_type"],
|
result["studios"] = [{"studio": t["studio"], "translation_type": t["translation_type"],
|
||||||
"is_18plus": t["is_18plus"], "episodes": t.get("episodes")}
|
"lang": t.get("lang"), "is_18plus": t["is_18plus"], "episodes": t.get("episodes")}
|
||||||
for t in out if t.get("studio")]
|
for t in out if t.get("studio")]
|
||||||
result["is_18plus"] = any(t["is_18plus"] for t in out)
|
result["is_18plus"] = any(t["is_18plus"] for t in out)
|
||||||
return result
|
return result
|
||||||
+27
-5
@@ -57,6 +57,27 @@ def refine_span(text: str, span: tuple[int, int] | None) -> tuple[int, int] | No
|
|||||||
return a, z
|
return a, z
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_RE_VOICES = re.compile(r"(?i)(много|двух|одно)голос")
|
||||||
|
_RE_DUB_WORD = re.compile(r"(?i)дубл")
|
||||||
|
_PREFIXES = (("проф.", re.compile(r"(?i)профессиональн")), ("люб.", re.compile(r"(?i)любительск")),
|
||||||
|
("авт.", re.compile(r"(?i)авторск")))
|
||||||
|
|
||||||
|
|
||||||
|
def guard_type(ttype: str, text: str, span_text: str | None, types: list[str]) -> str:
|
||||||
|
"""Дубляж из-за имени студии («Дубляжная», «Продубляж»): если вне имени о дубляже ничего нет,
|
||||||
|
а число голосов названо прямо («Любительский (многоголосый закадровый) …») — берём класс по тексту."""
|
||||||
|
if "дубляж" not in ttype:
|
||||||
|
return ttype
|
||||||
|
rest = text.replace(span_text, " ") if span_text else text
|
||||||
|
voices = _RE_VOICES.search(rest)
|
||||||
|
if _RE_DUB_WORD.search(rest) or not voices:
|
||||||
|
return ttype
|
||||||
|
prefix = next((p for p, rx in _PREFIXES if rx.search(rest)), "")
|
||||||
|
cand = f"{prefix} {voices.group(1).lower()}голосый".strip()
|
||||||
|
return cand if cand in types else ttype
|
||||||
|
|
||||||
|
|
||||||
class DubPredictor:
|
class DubPredictor:
|
||||||
"""Загруженная модель для предсказания типа озвучки и автора."""
|
"""Загруженная модель для предсказания типа озвучки и автора."""
|
||||||
|
|
||||||
@@ -114,6 +135,7 @@ class DubPredictor:
|
|||||||
span = refine_span(text, decode_span(
|
span = refine_span(text, decode_span(
|
||||||
bio_pred[i], offsets[i], enc["attention_mask"][i].tolist()))
|
bio_pred[i], offsets[i], enc["attention_mask"][i].tolist()))
|
||||||
span_text = text[span[0]:span[1]].strip() if span else None
|
span_text = text[span[0]:span[1]].strip() if span else None
|
||||||
|
ttype = guard_type(self.types[tid], text, span_text, self.types)
|
||||||
span_conf = None
|
span_conf = None
|
||||||
if span is not None:
|
if span is not None:
|
||||||
toks = [t for t, (a, z) in enumerate(offsets[i])
|
toks = [t for t, (a, z) in enumerate(offsets[i])
|
||||||
@@ -121,18 +143,18 @@ class DubPredictor:
|
|||||||
confs = [float(bio_prob[i, t, bio_pred[i, t]]) for t in toks]
|
confs = [float(bio_prob[i, t, bio_pred[i, t]]) for t in toks]
|
||||||
span_conf = float(np.mean(confs)) if confs else None
|
span_conf = float(np.mean(confs)) if confs else None
|
||||||
lang, lang_conf, lang_src = "", None, None
|
lang, lang_conf, lang_src = "", None, None
|
||||||
if lang_prob is not None:
|
if info and info["lang"]: # язык, прямо названный в строке «Аудио», надёжнее нейросети
|
||||||
|
lang, lang_src = info["lang"], "audio"
|
||||||
|
elif lang_prob is not None:
|
||||||
j = int(lang_prob[i].argmax())
|
j = int(lang_prob[i].argmax())
|
||||||
if lang_prob[i, j] >= 0.5:
|
if lang_prob[i, j] >= 0.5:
|
||||||
lang, lang_conf, lang_src = self.langs[j], round(float(lang_prob[i, j]), 4), "model"
|
lang, lang_conf, lang_src = self.langs[j], round(float(lang_prob[i, j]), 4), "model"
|
||||||
if not lang and info and info["lang"]:
|
|
||||||
lang, lang_src = info["lang"], "audio"
|
|
||||||
if not lang:
|
if not lang:
|
||||||
lang = lang_of_text(seg)
|
lang = lang_of_text(seg)
|
||||||
lang_src = "text" if lang else None
|
lang_src = "text" if lang else None
|
||||||
results.append({
|
results.append({
|
||||||
"segment": seg,
|
"segment": seg,
|
||||||
"translation_type": self.types[tid],
|
"translation_type": ttype,
|
||||||
"type_confidence": round(float(type_prob[i, tid]), 4),
|
"type_confidence": round(float(type_prob[i, tid]), 4),
|
||||||
"studio_span": span_text,
|
"studio_span": span_text,
|
||||||
"studio": canonical(span_text) if span_text else None,
|
"studio": canonical(span_text) if span_text else None,
|
||||||
@@ -144,7 +166,7 @@ class DubPredictor:
|
|||||||
"lang_source": lang_src,
|
"lang_source": lang_src,
|
||||||
"role": role_of(info, seg),
|
"role": role_of(info, seg),
|
||||||
"official": bool(info and info["official"]),
|
"official": bool(info and info["official"]),
|
||||||
"audio": ({"codec": info["codec"] or None, "channels": info["channels"] or None,
|
"audio": ({"codec": info["codec"] or None, "channels": int(info["channels"]) if info["channels"] else None,
|
||||||
"bitrate_kbps": int(info["kbps"]) if info["kbps"] else None}
|
"bitrate_kbps": int(info["kbps"]) if info["kbps"] else None}
|
||||||
if info else None),
|
if info else None),
|
||||||
"_audio18": info["adult"] if info else None,
|
"_audio18": info["adult"] if info else None,
|
||||||
|
|||||||
+13
-1
@@ -139,7 +139,7 @@ def aggregate(tracks: list[dict]) -> dict:
|
|||||||
typed = {t["translation_type"] for t in uniq if t.get("studio")}
|
typed = {t["translation_type"] for t in uniq if t.get("studio")}
|
||||||
uniq = [t for t in uniq if t.get("studio") or t["translation_type"] not in typed]
|
uniq = [t for t in uniq if t.get("studio") or t["translation_type"] not in typed]
|
||||||
studios = [{"studio": t["studio"], "translation_type": t["translation_type"],
|
studios = [{"studio": t["studio"], "translation_type": t["translation_type"],
|
||||||
"is_18plus": t["is_18plus"], "episodes": t.get("episodes")}
|
"lang": t.get("lang"), "is_18plus": t["is_18plus"], "episodes": t.get("episodes")}
|
||||||
for t in uniq if t.get("studio")]
|
for t in uniq if t.get("studio")]
|
||||||
return {
|
return {
|
||||||
"tracks": uniq,
|
"tracks": uniq,
|
||||||
@@ -149,6 +149,17 @@ def aggregate(tracks: list[dict]) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def original_info(lines: list[dict]) -> dict | None:
|
||||||
|
"""Оригинальная дорожка из строки «Аудио … |Original|» (без студии и комментариев); нет — None."""
|
||||||
|
for ln in lines:
|
||||||
|
if ln["original"] and not ln["names"] and not ln["commentary"]:
|
||||||
|
return {"lang": ln["lang"] or None, "codec": ln["codec"] or None,
|
||||||
|
"channels": int(ln["channels"]) if ln["channels"] else None,
|
||||||
|
"bitrate_kbps": int(ln["kbps"]) if ln["kbps"] else None, "raw": ln["raw"]}
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
RE_SEASON = re.compile(r"(?i)сезон[ыа]?\s*(\d+(?:\s*[-–]\s*\d+)?)|\bS(\d{1,2})\b(?!E)")
|
RE_SEASON = re.compile(r"(?i)сезон[ыа]?\s*(\d+(?:\s*[-–]\s*\d+)?)|\bS(\d{1,2})\b(?!E)")
|
||||||
|
|
||||||
|
|
||||||
@@ -253,6 +264,7 @@ def predict_release(predictor, *, html: str | None = None, text: str | None = No
|
|||||||
if segs:
|
if segs:
|
||||||
merge_title_tracks(tracks, predictor._predict_batch(segs))
|
merge_title_tracks(tracks, predictor._predict_batch(segs))
|
||||||
result = aggregate(tracks)
|
result = aggregate(tracks)
|
||||||
|
result["original"] = original_info(lines)
|
||||||
apply_audio_18(result, page_text)
|
apply_audio_18(result, page_text)
|
||||||
for t in result["tracks"]:
|
for t in result["tracks"]:
|
||||||
t.pop("_audio18", None)
|
t.pop("_audio18", None)
|
||||||
|
|||||||
+13
-3
@@ -34,13 +34,15 @@ RE_ADULT = re.compile(r"(?<!\d)18\s*\+")
|
|||||||
RE_CODEC = re.compile(r"(?i)(E-?AC-?3|AC-?3|DTS-HD\s*MA|DTS-HD|DTS:X|DTS|TrueHD|FLAC|AAC|Opus|MP3|L?PCM)")
|
RE_CODEC = re.compile(r"(?i)(E-?AC-?3|AC-?3|DTS-HD\s*MA|DTS-HD|DTS:X|DTS|TrueHD|FLAC|AAC|Opus|MP3|L?PCM)")
|
||||||
RE_CH = re.compile(r"(?i)(\d(?:\.\d)?)\s*(?:ch\b|канал)")
|
RE_CH = re.compile(r"(?i)(\d(?:\.\d)?)\s*(?:ch\b|канал)")
|
||||||
RE_KBPS = re.compile(r"(?i)(\d{2,5})\s*(?:k|к)(?:bps|бит)")
|
RE_KBPS = re.compile(r"(?i)(\d{2,5})\s*(?:k|к)(?:bps|бит)")
|
||||||
|
RE_LAYOUT = re.compile(r"(?<![\d.])([1-9])\.([0-2])(?![\d.])") # раскладка «5.1» -> 6 каналов
|
||||||
RE_NAMES = re.compile(r"\|\s*([^|]{2,40}?)\s*\||\(([^()]{2,40})\)")
|
RE_NAMES = re.compile(r"\|\s*([^|]{2,40}?)\s*\||\(([^()]{2,40})\)")
|
||||||
RE_ORIGINAL = re.compile(r"(?i)\boriginal\b|оригинал")
|
RE_ORIGINAL = re.compile(r"(?i)\boriginal\b|оригинал")
|
||||||
RE_COMMENT = re.compile(r"(?i)комментар|commentary")
|
RE_COMMENT = re.compile(r"(?i)комментар|commentary")
|
||||||
RE_OFFICIAL = re.compile(r"(?i)официальн|лицензион|licensed")
|
RE_OFFICIAL = re.compile(r"(?i)официальн|лицензион|licensed")
|
||||||
_HOMOGLYPH = str.maketrans("АСЕ", "ACE") # «АС3» кириллицей
|
_HOMOGLYPH = str.maketrans("АСЕ", "ACE") # «АС3» кириллицей
|
||||||
_NORM = re.compile(r"[\W_]+")
|
_NORM = re.compile(r"[\W_]+")
|
||||||
_NOT_NAME = re.compile(r"(?i)^(?:original|оригинал\w*|18\s*\+|forced|full|sdh|\d+\s*ch|\d+(?:\.\d)?)$")
|
_NOT_NAME = re.compile(r"(?i)^(?:dub|vo|mvo|dvo|avo|дубляж|дублированн(?:ый|ая|ое)|закадров(?:ый|ая|ое)|"
|
||||||
|
r"(?:много|двух|одно)голос\w*|original|оригинал\w*|18\s*\+|forced|full|sdh|\d+\s*ch|\d+(?:\.\d)?)$")
|
||||||
|
|
||||||
|
|
||||||
def norm(s: str) -> str:
|
def norm(s: str) -> str:
|
||||||
@@ -56,6 +58,12 @@ def lang_of_text(text: str) -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _channels(v: str) -> str:
|
||||||
|
"""«6» -> «6»; «5.1» -> «6» (число каналов, а не раскладка)."""
|
||||||
|
a, _, b = v.partition(".")
|
||||||
|
return str(int(a) + int(b or 0))
|
||||||
|
|
||||||
|
|
||||||
def parse_audio_line(line: str) -> dict:
|
def parse_audio_line(line: str) -> dict:
|
||||||
body = re.sub(r"\s+", " ", (line or "").strip())
|
body = re.sub(r"\s+", " ", (line or "").strip())
|
||||||
lead = RE_LEAD_CODE.match(body)
|
lead = RE_LEAD_CODE.match(body)
|
||||||
@@ -66,11 +74,13 @@ def parse_audio_line(line: str) -> dict:
|
|||||||
codec = RE_CODEC.search(fixed)
|
codec = RE_CODEC.search(fixed)
|
||||||
ch = RE_CH.search(fixed)
|
ch = RE_CH.search(fixed)
|
||||||
kbps = RE_KBPS.search(fixed)
|
kbps = RE_KBPS.search(fixed)
|
||||||
names = [n.strip() for a, b in RE_NAMES.findall(body) for n in [a or b]
|
names = [n.strip() for a, b in RE_NAMES.findall(body) for chunk in [a or b]
|
||||||
|
for n in re.split(r"\s*[,;/]\s*", chunk) # «|Дубляж, Le Doyen|» -> тип и студия отдельно
|
||||||
if n.strip() and not _NOT_NAME.match(n.strip()) and not RE_CODEC.search(n)]
|
if n.strip() and not _NOT_NAME.match(n.strip()) and not RE_CODEC.search(n)]
|
||||||
|
layout = RE_LAYOUT.search(fixed)
|
||||||
return {"raw": body, "lang": lang,
|
return {"raw": body, "lang": lang,
|
||||||
"codec": re.sub(r"\s+", "", codec.group(1)).upper() if codec else "",
|
"codec": re.sub(r"\s+", "", codec.group(1)).upper() if codec else "",
|
||||||
"channels": ch.group(1) if ch else "", "kbps": kbps.group(1) if kbps else "",
|
"channels": _channels(ch.group(1)) if ch else (_channels(layout.group(0)) if layout else ""), "kbps": kbps.group(1) if kbps else "",
|
||||||
"adult": bool(RE_ADULT.search(body)), "original": bool(RE_ORIGINAL.search(body)),
|
"adult": bool(RE_ADULT.search(body)), "original": bool(RE_ORIGINAL.search(body)),
|
||||||
"commentary": bool(RE_COMMENT.search(body)), "official": bool(RE_OFFICIAL.search(body)),
|
"commentary": bool(RE_COMMENT.search(body)), "official": bool(RE_OFFICIAL.search(body)),
|
||||||
"names": names}
|
"names": names}
|
||||||
|
|||||||
@@ -301,6 +301,28 @@ _agg2 = aggregate([_t("проф. дубляж", "Продубляж"), _t("пр
|
|||||||
check("aggregate: дорожка без студии того же типа схлопывается",
|
check("aggregate: дорожка без студии того же типа схлопывается",
|
||||||
[x["translation_type"] for x in _agg2["tracks"]] == ["проф. дубляж", "проф. двухголосый"])
|
[x["translation_type"] for x in _agg2["tracks"]] == ["проф. дубляж", "проф. двухголосый"])
|
||||||
|
|
||||||
|
print("== строки «Аудио»: язык, студии, оригинал ==")
|
||||||
|
from src.predict import guard_type
|
||||||
|
from src.release import original_info
|
||||||
|
from src.trackinfo import audio_lines_all, match_audio
|
||||||
|
_au = audio_lines_all(
|
||||||
|
"Аудио 1: Russian E-AC3 / 5.1 / 48 kHz / 768 kbps |Дубляж, Продубляж|\n"
|
||||||
|
"Аудио 4: Ukrainian E-AC3 / 5.1 / 48 kHz / 256 kbps |Дубляж, Le Doyen|\n"
|
||||||
|
"Аудио 5: English E-AC3+Atmos / 5.1 / 48 kHz / 768 kbps |Original|\n")
|
||||||
|
check("аудио: «|Дубляж, Le Doyen|» -> студия Le Doyen без слова-типа", _au[1]["names"] == ["Le Doyen"], str(_au[1]["names"]))
|
||||||
|
check("аудио: язык Ukrainian -> UKR", _au[1]["lang"] == "UKR")
|
||||||
|
check("аудио: раскладка 5.1 -> 6 каналов", _au[1]["channels"] == "6")
|
||||||
|
check("аудио: дорожка студии находит свою строку",
|
||||||
|
match_audio("Профессиональный (дублированный) Le Doyen", _au) == [_au[1]])
|
||||||
|
_orig = original_info(_au)
|
||||||
|
check("оригинал: ENG, 768 kbps, 6 каналов", _orig and _orig["lang"] == "ENG" and _orig["bitrate_kbps"] == 768 and _orig["channels"] == 6, str(_orig))
|
||||||
|
check("оригинал: нет строки Original -> None", original_info(_au[:2]) is None)
|
||||||
|
_T = ["проф. дубляж", "проф. многоголосый", "люб. многоголосый", "многоголосый"]
|
||||||
|
check("тип: «Любительский (многоголосый …) Дубляжная» не дубляж",
|
||||||
|
guard_type("проф. дубляж", "Любительский (многоголосый закадровый) Дубляжная", "Дубляжная", _T) == "люб. многоголосый")
|
||||||
|
check("тип: настоящий дубляж не трогается",
|
||||||
|
guard_type("проф. дубляж", "Профессиональный (дублированный) Продубляж", "Продубляж", _T) == "проф. дубляж")
|
||||||
|
|
||||||
print("== серии по студиям ==")
|
print("== серии по студиям ==")
|
||||||
from src.episodes import format_ranges as _fr, parse_episodes as _pe # noqa: E402
|
from src.episodes import format_ranges as _fr, parse_episodes as _pe # noqa: E402
|
||||||
from src.release import series_info as _si # noqa: E402
|
from src.release import series_info as _si # noqa: E402
|
||||||
|
|||||||
Reference in new issue
Block a user