Compare commits

...
22 Commits
Author SHA1 Message Date
DenozordecandClaude Sonnet 5.5 690275dcb0 Релиз: аудио дорожек из строк «Аудио», дорожка без студии на другом языке (укр. дубляж), профиль DV из «Dolby Vision P8»
docker / build (push) Successful in 1m36s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 13:39:57 +07:00
DenozordecandClaude Sonnet 5.5 76e887b4ba Сегментатор: веса со scene-негативами, RE_SCENE без катастрофического отката
docker / build (push) Successful in 3m11s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 12:17:31 +07:00
DenozordecandClaude Sonnet 5.5 729eeac045 Сегментатор: scene-имя релиза не может быть дорожкой
docker / build (push) Successful in 1m49s
Kinozal 2160145: после переобучения модель выбирала первую строку вкладки «Релиз»
(«Memoirs.of.a.Geisha.2005.2160p.UHD.Blu-ray.Remux…-CiNEPHiLES», оценка 0.96), и
имя релиза попадало в ответ как «студия». Прежние веса строку не выбирали.

- html_extract.RE_SCENE: без пробелов и ≥ 4 точек — дорожкой не бывает;
  LineSegmenter.select_lines отбрасывает такие строки при любой оценке модели.
- build_examples: scene-имена — трудные негативы, берутся все (раньше прореживались
  до 15 %); эффект — при следующем переобучении.
- На реальных данных страницы (вкладки Техданные/Интересно/Награды/Релиз):
  1 дорожка «проф. дубляж», без студии. Тесты: 288 ok.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 02:57:43 +07:00
DenozordecandClaude Sonnet 5.5 e6705c2b7b Сегментатор дообучен: список дорожек под пустым «Перевод:»
docker / build (push) Successful in 1m42s
Переобучен после исправления парсера меток (rt_parse): раздачи rutracker с пустым
«Перевод:» и списком дорожек ниже получили метки, в том числе вручную добавленные
6919056 и 6927571. Строки такого списка у нового сегментатора 0.94–1.00
(у прежнего 0.004–0.12); на 6919056 пайплайн с реальными весами даёт
segmenter=model, 4 дорожки, без segmenter_warning.

- docker-models/models/segmenter: новые веса (Git LFS)
- data: labels.csv, manual_ids.txt, meta.csv; reports/data_audit.md
- tests: 286 ok

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 19:33:25 +07:00
DenozordecandClaude Sonnet 5.5 e9c85c4132 Метки rutracker: «Перевод:» без значения и список дорожек ниже
docker / build (push) Successful in 2m21s
Парсер меток (rt_parse) требовал текст после «Перевод:» на той же строке, поэтому
раздачи с пустым «Перевод:» и списком «Профессиональный (…) | Студия» по строке на
дорожку (t=6919056, 6927571) не получали меток, релиз не сохранялся, а сегментатор
учился на их дорожках как на негативах. Теперь rt_bare_list берёт подряд идущие
строки со словом типа (через пустые строки; строки «тип: определение» —
глоссарий — не берутся). То же правило в html_extract.extract_segments.

На data/raw: +252 раздачи получают метки, в 12 добавляются недостающие дорожки.
PARSE_VERSION 3 -> 4 (переразбор при «Переиндексировать data/raw»). Тест rt_parse
на 6919056; 276 ok.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 16:30:10 +07:00
DenozordecandClaude Sonnet 5.5 b3ceefda4f Сегментация: список дорожек под «Перевод:» на реальной вёрстке, достройка блока, segmenter_warning
docker / build (push) Successful in 2m1s
- html_extract: строки дорожек под пустым «Перевод:» берутся и через пустые
  строки (в HTML после <br> стоит \n исходника — прежняя правка на реальной
  странице rutracker t=6919056 отдавала 1 дорожку из 4); шаблон начала строки
  требует полные слова типа озвучки.
- release: complete_block достраивает список, из которого модель выбрала не все
  строки; segmenter_warning (+ лог) — модель и правила (без кодек-строк)
  разошлись; поле segmenter_warning в ответе.
- docs: retrain-segmenter.md — как дообучить сегментатор, поле в api-reference.
- tests: 275 ok.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 16:14:50 +07:00
DenozordecandClaude Opus 5.5 e185c73714 Сегментация: все строки дорожек под пустым «Перевод:»
docker / build (push) Successful in 1m24s
На rutracker дорожки бывают перечислены по одной на строку под
«Перевод:» без значения; правила брали только первую строку и теряли
остальные (t=6919056: 1 из 4, t=6927571: 1 из 7). Теперь берутся
подряд идущие строки, начинающиеся со слова типа озвучки.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 09:49:35 +07:00
DenozordecandClaude Opus 5.5 3c42d2d4e2 Видео: 10-bit, DV и профиль DV из заголовка
docker / build (push) Successful in 5m30s
На nnm строка «Видео» не называет битность и HDR — они только в заголовке («[4K, HDR10, DV 8.1, 10-bit]»).
- bit_depth: если в строке «Видео» нет, ищется в заголовке
- «DV» считается Dolby Vision (\b не цепляет DVD/DVDRip)
- профиль из «DV 8.1», «DV P7», «DoVi P5»; только реальные профили, «DV 10-bit» — не профиль
- тесты: раздача nnm «В поисках Дори», DVDRip / DV 10-bit, DV P7

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 19:02:37 +07:00
DenozordecandClaude Opus 5.5 dc19fe6255 API: видео, тип релиза и субтитры в ответе релиза; Atmos и kHz у дорожек
docker / build (push) Successful in 1m18s
src/techinfo.py — правила по строкам «Видео:», «Тип релиза:»/«Качество»,
«Контейнер:», «Продолжительность:», «Субтитры:» и заголовку: поля video
(кодек, размер кадра, класс разрешения, fps, битрейт, битность, HDR,
профиль DV), release (тип, нормализованный источник, контейнер,
продолжительность), subtitles (языки с forced/full/SDH, формат, вшитые).
audio дорожек и original: khz и atmos (вход модели не меняется).
title_from_html больше не рвёт заголовок rutracker пробелами
(<span>/<wbr> на каждом слове). Покрытие на 600 страницах каждого
трекера: видео 90/93/100%, e2e озвучки без изменений. Тесты 275 ok.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 22:26:57 +07:00
DenozordecandClaude Opus 5.5 89ffe4375b Документация: кодек-строки, аудит дорожек, текущие метрики
docker / build (push) Successful in 39s
README: метрики модели 02.11.2026 (test 4 377 строк, e2e по трекерам), размер
датасета, kinozal-вкладки. dataset.md: размер снимка, колонки studio_source и
audio_raw. architecture.md: кодек-строки как позитивы сегментатора, E2E_MODEL.
collecting.md: tools/audit_tracks.py. training-windows.md: доразметка, сравнение
кандидата и постановка в models/best и Docker. plan.md: шаг «аудит дорожек».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 21:55:43 +07:00
DenozordecandClaude Opus 5.5 588b1f1579 Модель для Docker: base, обученная со студиями из кодек-строк
docker / build (push) Successful in 4m43s
models/runs/20261102_190555_basecodec -> models/best -> docker-models
(tools/pack_models.py --stage). Тестовая выборка (4 377 строк): joint
exact 0.891 -> 0.906, span exact 0.905 -> 0.922, type macro F1 0.893 ->
0.903; исправлено 244 строки, сломано 179 (p = 0.002). Релизы (по 400
страниц, P/R): kinozal 0.745/0.850 -> 0.766/0.879, rutracker 0.866/0.859
-> 0.877/0.870, nnm 0.845/0.829 -> 0.838/0.825. Сегментатор прежний.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:30:54 +07:00
DenozordecandClaude Opus 5.5 e3af778068 Студии из кодек-строк: 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>
2026-11-02 20:30:52 +07:00
DenozordecandClaude Opus 5.5 63878f7874 Модель для Docker: base, обученная на доразмеченных студиях
docker / build (push) Successful in 1m21s
models/runs/20261102_152919_base_relabel -> models/best -> docker-models
(tools/pack_models.py --stage). Тестовая выборка (5 235 строк): joint
exact 0.880 -> 0.953, span exact 0.880 -> 0.955; исправлено 541 строка,
сломано 157. Релизы (по 400 страниц, P/R пар тип+студия): nnm
0.583/0.590 -> 0.853/0.859, rutracker 0.891/0.921 -> 0.887/0.911,
kinozal 0.779/0.926 -> 0.781/0.930. Сегментатор прежний.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 15:43:31 +07:00
DenozordecandClaude Opus 5.5 666caa60d5 Отчёт аудита данных после доразметки студий
docker / build (push) Successful in 1m27s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 15:38:55 +07:00
DenozordecandClaude Opus 5.5 83bab13893 Доразметка студий: nnm «Перевод: … (Студия)», kinozal из заголовка раздачи
* harvest_core.nnm_part_studio: студия из части строки «Перевод:» nnm
  («Дублированный (Невафильм)», «… - Jaskier»); раньше такие строки шли с
  «(не указана)», и модель училась не извлекать студию даже из
  «Дублированный (Пифагор)». tech-строки со студиями больше не
  выбрасываются, если на странице нет строк «Аудио N» с именами.
* harvest_core.kz_title_rows: kinozal, студии из заголовка «ПМ (LostFilm,
  HDrezka Studio)», которых нет на странице; вход — сегмент как у сервиса,
  строка «Аудио Rus: … | Студия» — в audio_raw.
* tools/relabel_studios.py — то же по готовому labels.csv без пересборки
  страниц; применено к data/labels.csv: со студией 22 061 -> 29 331 строк
  (nnm +3 051, kinozal +4 219 строк в 3 007 релизах); prepare_data:
  39 418 строк, спан студии 24 004 -> 31 214.
* e2e: эталон как aggregate — «тип без студии» убирается, если у типа есть
  студия.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 15:20:03 +07:00
DenozordecandClaude Opus 5.5 f91f6cb48e Сегментатор: -1 только для кодек-строк с типом или студией
Пустая кодек-строка («Аудио 1: AC3, 6 ch, 384 kbps - Русский») снова
негатив: с -1 модель разучилась её отбрасывать и на nnm подменяла ею
строку «Перевод: …».

Пробное обучение (2 эпохи, ~34 мин на 27.9k страниц) e2e по 400 тестовых
страниц, P/R пар (тип, студия), текущие веса -> новые: nnm 0.872/0.879 ->
0.836/0.860, rutracker 0.891/0.917 -> 0.883/0.910, kinozal 0.720/0.844 ->
0.724/0.845. Новые веса не лучше текущих, models/segmenter не заменён.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 14:10:32 +07:00
DenozordecandClaude Opus 5.5 6470f882f1 Сегментатор: текст kinozal из raw JSON, кодек-строки не как негатив; e2e по 400 страниц
Обучение:
* page_text/kinozal_text: текст kinozal = h1 + «Техданные» + «Релиз» из
  data/raw/kinozal/*.json (в pages/kz-*.txt нет заголовка и «Техданных»,
  откуда половина меток kinozal; сервис видит обе вкладки).
* label_page: позитив — raw или audio_raw; неразмеченная кодек-строка
  русской дорожки («Аудио 3: … Русский (MVO, X)», «Аудио Rus: … | X»)
  получает -1 и в обучение не идёт: как негатив она учила отбрасывать
  кодек-строки. Сводка «Аудио: Русский, английский (…)» остаётся негативом.

Инференс (release):
* model_segments -> (основные, кодек); выбранные моделью кодек-строки идут
  второстепенным источником вместе с правилом codec_segments; иноязычные
  («Казахский (Dub, …)», «English Dubbing», «Original») отбрасываются.
* merge_extra_tracks(adopt_type): кодек-дорожка с типом «неизвестно»
  получает тип единственной дорожки страницы без студии.

e2e_segmenter: тот же текст kinozal, не больше E2E_MAX (400) страниц на
трекер, train-страницы только с --train-pages (меню парсит только тест).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 12:05:20 +07:00
DenozordecandClaude Opus 5.5 099d33bdaa Слияние claude/advisor-opus-78114c: модели на пересобранном датасете, Docker, язык и серии
Конфликт в src/release.py: взяты original_info, lostfilm_release,
series_info и серии дорожек из advisor; сохранены same_studio,
merge_extra_tracks и codec_segments. Кодек-дорожкам тоже проставляются
серии.

Кодек-строки теперь подключаются, только если у какой-то дорожки страницы
нет студии: с новыми моделями безусловное подключение роняло precision
студий на rutracker (0.908 -> 0.793). С условием, test-сплит, по 300
релизов, студии P/R: rutracker 0.908/0.882 -> 0.840/0.900, nnm
0.875/0.915 -> 0.875/0.940, kinozal 0.816/0.845 -> 0.800/0.863. Тесты:
240 ok.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 11:50:44 +07:00
DenozordecandClaude Opus 5.5 666a266b2f Сегментатор: truncation longest_first — не падать на строке длиннее max_length
С only_second токенизатор бросал исключение, если сама строка (до 220
символов) не помещалась в MAX_LEN и обрезать было нечего, кроме контекста.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 11:44:18 +07:00
DenozordecandClaude Opus 5.5 1f9ee46a0d kinozal: догрузка вкладки «Релиз» по URL; кодек-строки «Аудио N» как источник студий
На kinozal во встроенном #tabs только «Техданные» («Перевод:» без студий),
а строки «Аудио N: … (MVO, LostFilm)» лежат во вкладке «Релиз», которую
страница грузит JS. Поэтому по URL определялись только типы без студий.

* fetch: для kinozal скачиваются вкладки showtab(id,N)
  (get_srv_details.php?id=…&pagesd=N, кроме «Техданных» и «Скриншотов»)
  и дописываются в #tabs; недоступная вкладка пропускается.
* release: кодек-строки с типом озвучки берутся правилом codec_segments
  (сегментатор их отбрасывает — в разметке rutracker это не raw) и идут
  второстепенным источником (source=codec / page+codec): только дорожки
  со студией, дубли студии (same_studio) пропускаются, студия вписывается
  в дорожку того же типа без студии. «English Dubbing» и т.п. не берутся.
* merge_title_tracks переведён на общий merge_extra_tracks.

e2e test rutracker без изменений (P 0.988 / R 0.982); train-страницы
P 0.975 -> 0.951. Тесты: 77 ok.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 11:33:01 +07:00
DenozordecandClaude Sonnet 5.5 6a87bc993c Слияние claude/advisor-opus-78114c: сегментатор страниц, URL/FlareSolverr, агрегат релиза
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 20:07:22 +07:00
Denozordec 2eb4f8b72e Merge claude/advisor-opus-78114c: релиз целиком, URL-вход, int8 tiny2, фикс сегментации, датасет 6864 2026-10-27 17:22:31 +07:00
31 changed files with 17371 additions and 3198 deletions

No files matched your search

+16 -14
View File
@@ -76,19 +76,21 @@ docs/ документация
## Качество ## Качество
Held-out test (645 строк, группировка по релизам и текстам; полная таблица и Held-out test (4 377 строк, группировка по релизам и текстам; модель
графики — [reports/summary.md](reports/summary.md)): `models/runs/20261102_190555_basecodec`, обучена 02.11.2026 на `labels.csv` со студиями из
кодек-строк; сравнение моделей — `python -m src.runs`):
| модель | type acc | type macro-F1 | span exact | 18+ acc | joint exact | | модель | type acc | type macro-F1 | span exact | 18+ acc | lang acc | joint exact |
|---|---|---|---|---|---| |---|---|---|---|---|---|---|
| rubert-tiny2 | 0.991 | 0.970 | 0.918 | 1.000 | 0.915 | | **ruBert-base (models/best)** | **0.978** | **0.903** | **0.922** | **1.000** | **0.997** | **0.906** |
| **ruBert-base (models/best)** | **0.994** | 0.966 | **0.950** | **1.000** | **0.946** |
* joint exact — строка распознана полностью (тип + автор + 18+). * joint exact — строка распознана полностью (тип + автор + 18+).
* E2E на сохранённых страницах релизов: rutracker — precision 0.83 / recall 0.98 * Тест-сплит пересобирается вместе с датасетом, поэтому цифры разных снимков напрямую
по парам (тип, автор); kinozal-текстовые дампы содержат только тех-вкладку не сравниваются (ранние 0.946 — на тесте из 645 строк). rubert-tiny2 на текущем
без авторов (автор подгружался на сайте через JS), поэтому там автор датасете не переобучалась.
недоступен в принципе. * E2E по релизам (`tests/e2e_segmenter.py`, по 400 тестовых страниц на трекер, пары
тип + студия, precision / recall): kinozal 0.766 / 0.879, rutracker 0.877 / 0.870,
nnm 0.838 / 0.825.
* ONNX-экспорт эквивалентен PyTorch на 100% строк test — [reports/onnx_parity.md](reports/onnx_parity.md). * ONNX-экспорт эквивалентен PyTorch на 100% строк test — [reports/onnx_parity.md](reports/onnx_parity.md).
* Smoke на формулировках, которых нет в датасете: тип 9/10, 18+ 10/10, * Smoke на формулировках, которых нет в датасете: тип 9/10, 18+ 10/10,
автор 5/10 (полное извлечение автора на непривычных форматах записи — автор 5/10 (полное извлечение автора на непривычных форматах записи —
@@ -99,14 +101,14 @@ Held-out test (645 строк, группировка по релизам и т
* Метки датасета сгенерированы регулярками (пайплайн deepseek-harness) — метрики * Метки датасета сгенерированы регулярками (пайплайн deepseek-harness) — метрики
измеряют согласование с этой разметкой; ценность модели — обобщение на новые измеряют согласование с этой разметкой; ценность модели — обобщение на новые
формулировки без сопровождения регулярок. формулировки без сопровождения регулярок.
* Источники несбалансированы (rutracker 1658 / nnm 1549 / kinozal 77 строк). * Датасет: 55 324 строки из 27 900 релизов (kinozal 21 766 / nnm 18 363 / rutracker 15 195).
* Извлечение автора уверенно работает на форматах записи, представленных в * Извлечение автора уверенно работает на форматах записи, представленных в
датасете (имя после типа через разделитель, в скобках у AVO/MVO/DVO-дорожек, датасете (имя после типа через разделитель, в скобках у AVO/MVO/DVO-дорожек,
после тире); на непривычных форматах («Одноголосый закадровый (Фамилия)») после тире); на непривычных форматах («Одноголосый закадровый (Фамилия)»)
модель может не выделить имя — span остаётся пустым, тип при этом верный. модель может не выделить имя — span остаётся пустым, тип при этом верный.
* kinozal-страницы: авторы озвучки подгружаются на сайте JavaScript-ом; в * kinozal-страницы: авторы озвучки лежат во вкладках, которые сайт подгружает
статическом HTML и текстовых дампах их нет — модель их не увидит (это JavaScript-ом. По `url` сервис скачивает их сам (`fetch_html`, `showtab`); в
ограничение данных, не модели). присланном `html`/`text` без этих вкладок студий может не быть.
* Классы с 1–8 примерами почти не выучиваются (видно в macro-F1 и confusion * Классы с 1–8 примерами почти не выучиваются (видно в macro-F1 и confusion
matrix); морфологическая аугментация (458 вариантов: «многоголосое» и т.п.) matrix); морфологическая аугментация (458 вариантов: «многоголосое» и т.п.)
частично закрывает этот пробел. частично закрывает этот пробел.
+14505 -3051
View File
File diff suppressed because it is too large. Load diff
+2
View File
@@ -1 +1,3 @@
rt-6894333 rt-6894333
rt-6919056
rt-6927571
+1
View File
@@ -35093,6 +35093,7 @@ rutracker,rt-6918940,"Полицейский-экстрасенс Тёдзё / C
rutracker,rt-6918967,"Человек-паук: Новый день / Spider-Man: Brand New Day (Дестин Дэниел Креттон / Destin Daniel Cretton) [2026, США, Канада, Великобритания, Германия, фэнтези, комедия, приключения, семейный, WEB-DL 2160p, SDR] 4x Dub + 6x MVO + DVO + Original (Eng) + Sub (Rus, Eng)",Человек-паук: Новый день,Spider-Man: Brand New Day,2026,film,False,,,,Зарубежное кино (UHD Video),WEB-DL 2160p (SDR) [MA],2160p,,,,,,,145,False,"США, Канада, Великобритания, Германия","фантастика, боевик, приключения",Дестин Дэниел Креттон / Destin Daniel Cretton,Том Холланд; Зендея; Сэди Синк; Джейкоб Баталон; Джон Бернтал; Трэмелл Тиллман; Майкл Мэндо; Мелани Скрофано,,,,,ENG,ENG,0,,RUS+ENG,True, rutracker,rt-6918967,"Человек-паук: Новый день / Spider-Man: Brand New Day (Дестин Дэниел Креттон / Destin Daniel Cretton) [2026, США, Канада, Великобритания, Германия, фэнтези, комедия, приключения, семейный, WEB-DL 2160p, SDR] 4x Dub + 6x MVO + DVO + Original (Eng) + Sub (Rus, Eng)",Человек-паук: Новый день,Spider-Man: Brand New Day,2026,film,False,,,,Зарубежное кино (UHD Video),WEB-DL 2160p (SDR) [MA],2160p,,,,,,,145,False,"США, Канада, Великобритания, Германия","фантастика, боевик, приключения",Дестин Дэниел Креттон / Destin Daniel Cretton,Том Холланд; Зендея; Сэди Синк; Джейкоб Баталон; Джон Бернтал; Трэмелл Тиллман; Майкл Мэндо; Мелани Скрофано,,,,,ENG,ENG,0,,RUS+ENG,True,
rutracker,rt-6919009,"Петер Фосс - герой наших дней / Peter Voss, der Held des Tages (Георг Маришка / Georg Marischka) [1959, Западная Германия, комедия, криминал, DVDRip-AVC] VO (Grampy) + Sub Rus + Original Deu",Петер Фосс - герой наших дней,"Peter Voss, der Held des Tages",1959,film,False,,,,Классика мирового кинематографа,DVDRip-AVC,,AVC,,,,,,105,False,Западная Германия,"комедия, криминал",Георг Маришка / Georg Marischka,,,,,,DEU,DEU,2,"[{""lang"": """", ""codec"": ""AC-3"", ""channels"": """", ""bitrate_kbps"": """", ""tag"": """", ""studio"": """", ""episodes"": """", ""is_18plus"": false}, {""lang"": """", ""codec"": ""AC-3"", ""channels"": """", ""bitrate_kbps"": """", ""tag"": """", ""studio"": """", ""episodes"": """", ""is_18plus"": false}]",RUS,False,softsub (SRT) rutracker,rt-6919009,"Петер Фосс - герой наших дней / Peter Voss, der Held des Tages (Георг Маришка / Georg Marischka) [1959, Западная Германия, комедия, криминал, DVDRip-AVC] VO (Grampy) + Sub Rus + Original Deu",Петер Фосс - герой наших дней,"Peter Voss, der Held des Tages",1959,film,False,,,,Классика мирового кинематографа,DVDRip-AVC,,AVC,,,,,,105,False,Западная Германия,"комедия, криминал",Георг Маришка / Georg Marischka,,,,,,DEU,DEU,2,"[{""lang"": """", ""codec"": ""AC-3"", ""channels"": """", ""bitrate_kbps"": """", ""tag"": """", ""studio"": """", ""episodes"": """", ""is_18plus"": false}, {""lang"": """", ""codec"": ""AC-3"", ""channels"": """", ""bitrate_kbps"": """", ""tag"": """", ""studio"": """", ""episodes"": """", ""is_18plus"": false}]",RUS,False,softsub (SRT)
rutracker,rt-6919020,"Отель для нелюдей (ТВ-2) / Hotel Inhumans 2nd Season [TV] [01-03 из 13] [RUS(int), JAP+Sub] [2026, Экшен, Драма, WEB-DL] [1080p]",Отель для нелюдей (ТВ-2),Hotel Inhumans 2nd Season,2026,anime,False,,,,Онгоинги (HD Video),WEB-DL (Crunchyroll),1080p,,,,,,,,False,Япония,"Экшен, Драма",Амино Тэцуро,,,,,,,,0,,,False, rutracker,rt-6919020,"Отель для нелюдей (ТВ-2) / Hotel Inhumans 2nd Season [TV] [01-03 из 13] [RUS(int), JAP+Sub] [2026, Экшен, Драма, WEB-DL] [1080p]",Отель для нелюдей (ТВ-2),Hotel Inhumans 2nd Season,2026,anime,False,,,,Онгоинги (HD Video),WEB-DL (Crunchyroll),1080p,,,,,,,,False,Япония,"Экшен, Драма",Амино Тэцуро,,,,,,,,0,,,False,
rutracker,rt-6919056,"Астрал 6: Они уже здесь / Insidious: Out of the Further (Джейкоб Чейз / Jacob Chase) [2026, США, ужасы, триллер, WEB-DL 2160p, 4k, HDR10+, Dolby Vision] [Hybrid] Dub (Cinema Tone Production, HDRezka Studio) + MVO (HDRezka Studio, TVShows) + Original Eng + Sub (Rus, Eng)",Астрал 6: Они уже здесь,Insidious: Out of the Further,2026,film,False,,,,,WEB-DL 2160p (iT),2160p,HEVC,HDR10++HDR10+DV+HDR,,23.976,MKV,,105,False,США,"ужасы, триллер",Джейкоб Чейз / Jacob Chase,Амелия Ив; Айленд Остин; Лин Шэй; Сэм Спруэлл; Брэндон Переа; Мэйси Ричардсон-Селлерс; Waylon Devens; Джозеф Лопес,,,,,RUS+ENG,ENG,5,"[{""lang"": ""RUS"", ""codec"": ""AC3"", ""channels"": ""2"", ""bitrate_kbps"": 192, ""tag"": """", ""studio"": """", ""episodes"": """", ""is_18plus"": false}, {""lang"": ""RUS"", ""codec"": ""AAC"", ""channels"": ""2"", ""bitrate_kbps"": 335, ""tag"": ""DUB"", ""studio"": ""Дубляж"", ""episodes"": """", ""is_18plus"": false}, {""lang"": ""RUS"", ""codec"": ""AAC"", ""channels"": ""2"", ""bitrate_kbps"": 301, ""tag"": """", ""studio"": """", ""episodes"": """", ""is_18plus"": false}, {""lang"": ""RUS"", ""codec"": ""AC3"", ""channels"": ""6"", ""bitrate_kbps"": 384, ""tag"": """", ""studio"": """", ""episodes"": """", ""is_18plus"": false}, {""lang"": ""ENG"", ""codec"": ""E-AC3"", ""channels"": ""6"", ""bitrate_kbps"": 640, ""tag"": """", ""studio"": """", ""episodes"": """", ""is_18plus"": false}]",RUS+ENG,False,
rutracker,rt-6919120,"Шериф поневоле / The Deputy (Мэттью Суккар / Matt Sukkar) [2026, США, боевик, триллер, драма, криминал, WEB-DLRip-AVC] Dub",Шериф поневоле,The Deputy,2026,film,False,,,,Фильмы 2026,WEB-DLRip-AVC,,AVC,,,,,,100,False,США,"боевик, триллер, драма, криминал",Мэттью Суккар / Matt Sukkar,Дьюк Николсон; Уильям Х. Мэйси; Тиффани Хэддиш; Стивен Дорфф; Девон Росс; Билли Лурд; Джулия Фокс; Алисса Вилла,,,,,,,1,"[{""lang"": """", ""codec"": ""AC3"", ""channels"": ""6"", ""bitrate_kbps"": 448, ""tag"": """", ""studio"": """", ""episodes"": """", ""is_18plus"": false}]",RUS,True, rutracker,rt-6919120,"Шериф поневоле / The Deputy (Мэттью Суккар / Matt Sukkar) [2026, США, боевик, триллер, драма, криминал, WEB-DLRip-AVC] Dub",Шериф поневоле,The Deputy,2026,film,False,,,,Фильмы 2026,WEB-DLRip-AVC,,AVC,,,,,,100,False,США,"боевик, триллер, драма, криминал",Мэттью Суккар / Matt Sukkar,Дьюк Николсон; Уильям Х. Мэйси; Тиффани Хэддиш; Стивен Дорфф; Девон Росс; Билли Лурд; Джулия Фокс; Алисса Вилла,,,,,,,1,"[{""lang"": """", ""codec"": ""AC3"", ""channels"": ""6"", ""bitrate_kbps"": 448, ""tag"": """", ""studio"": """", ""episodes"": """", ""is_18plus"": false}]",RUS,True,
rutracker,rt-6919131,"Изгнанный читер-чародей наслаждается беззаботной второй жизнью / Tsuihou Sareta Cheat Fuyo Majutsushi wa Kimama na Second Life o (wo) Ouka Suru [TV] [01-03 из XX] [RUS(int), ENG, JAP+Sub] [2026, Фэнтези, WEB-DL] [1080p]",Изгнанный читер-чародей наслаждается беззаботной второй жизнью,Tsuihou Sareta Cheat Fuyo Majutsushi wa Kimama na Second Life o,2026,anime,False,,,,Онгоинги (HD Video),WEB-DL (Crunchyroll),1080p,,,,,,,,False,Япония,Фэнтези,Сю Хомма,,,,,,,,0,,,False, rutracker,rt-6919131,"Изгнанный читер-чародей наслаждается беззаботной второй жизнью / Tsuihou Sareta Cheat Fuyo Majutsushi wa Kimama na Second Life o (wo) Ouka Suru [TV] [01-03 из XX] [RUS(int), ENG, JAP+Sub] [2026, Фэнтези, WEB-DL] [1080p]",Изгнанный читер-чародей наслаждается беззаботной второй жизнью,Tsuihou Sareta Cheat Fuyo Majutsushi wa Kimama na Second Life o,2026,anime,False,,,,Онгоинги (HD Video),WEB-DL (Crunchyroll),1080p,,,,,,,,False,Япония,Фэнтези,Сю Хомма,,,,,,,,0,,,False,
rutracker,rt-6919143,"Война / War / Сезон: 1 / Серии: 1 из 8 (Чина Му-Ен, Бен Тэйлор, Дженнифер Шеридан) [2026, Великобритания, Триллер, драма, WEB-DL 1080p] MVO (Red Head Sound) + Original + Sub (Eng)",Война,War,2026,series,True,,,,Новинки и сериалы в стадии показа (HD Video),WEB-DL 1080p,1080p,AVC,,33,00,MKV,,53,False,Великобритания,"Триллер, драма","Чина Му-Ен, Бен Тэйлор, Дженнифер Шеридан",Сиенна Миллер; Доминик Уэст; Арчи Рено; Нина Сосанья; Пип Торренс; Джеймс МакАрдл; Ребекка Беллавиа; Свен Ванниювенхюзе,,,,,RUS+ENG,,2,"[{""lang"": ""RUS"", ""codec"": ""AC3"", ""channels"": ""6"", ""bitrate_kbps"": 640, ""tag"": """", ""studio"": """", ""episodes"": """", ""is_18plus"": false}, {""lang"": ""ENG"", ""codec"": ""E-AC3"", ""channels"": ""6"", ""bitrate_kbps"": 640, ""tag"": """", ""studio"": """", ""episodes"": """", ""is_18plus"": false}]",ENG,False, rutracker,rt-6919143,"Война / War / Сезон: 1 / Серии: 1 из 8 (Чина Му-Ен, Бен Тэйлор, Дженнифер Шеридан) [2026, Великобритания, Триллер, драма, WEB-DL 1080p] MVO (Red Head Sound) + Original + Sub (Eng)",Война,War,2026,series,True,,,,Новинки и сериалы в стадии показа (HD Video),WEB-DL 1080p,1080p,AVC,,33,00,MKV,,53,False,Великобритания,"Триллер, драма","Чина Му-Ен, Бен Тэйлор, Дженнифер Шеридан",Сиенна Миллер; Доминик Уэст; Арчи Рено; Нина Сосанья; Пип Торренс; Джеймс МакАрдл; Ребекка Беллавиа; Свен Ванниювенхюзе,,,,,RUS+ENG,,2,"[{""lang"": ""RUS"", ""codec"": ""AC3"", ""channels"": ""6"", ""bitrate_kbps"": 640, ""tag"": """", ""studio"": """", ""episodes"": """", ""is_18plus"": false}, {""lang"": ""ENG"", ""codec"": ""E-AC3"", ""channels"": ""6"", ""bitrate_kbps"": 640, ""tag"": """", ""studio"": """", ""episodes"": """", ""is_18plus"": false}]",ENG,False,
Can't render this file because it is too large.
Binary file not shown.
Binary file not shown.
+56 -5
View File
@@ -38,8 +38,16 @@
"studios": [{"studio": "LostFilm", "translation_type": "проф. многоголосый", "lang": "RUS", "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|"}, "original": {"lang": "ENG", "codec": "E-AC3", "channels": 6, "bitrate_kbps": 768, "khz": 48, "atmos": true, "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": []},
"video": {"codec": "HEVC", "width": 3840, "height": 2160, "resolution": "2160p", "fps": 23.976, "bitrate_kbps": 24800,
"bit_depth": 10, "hdr": ["Dolby Vision", "HDR10+", "HDR10"], "dv_profile": "8", "raw": "MPEG-H HEVC Video / 24,8 Mbps / 3840x2160 / …"},
"release": {"quality": "WEB-DL 2160p Dolby Vision", "source": "WEB-DL", "container": "MKV", "duration": "01:38:12", "duration_sec": 5892},
"subtitles": {"languages": ["RUS", "UKR", "ENG"],
"tracks": [{"lang": "RUS", "forced": true, "full": false, "sdh": false},
{"lang": "UKR", "forced": true, "full": true, "sdh": true},
{"lang": "ENG", "forced": false, "full": true, "sdh": true}],
"format": "softsub (SRT)", "hardsub": false, "raw": "Русские (Forced), Украинские (Forced, 2x Full, SDH), Английские (Full, SDH)"},
"title": "Название раздачи", "title": "Название раздачи",
"segmenter": "model", "segmenter": "model",
"tracker": "rutracker" "tracker": "rutracker"
@@ -50,14 +58,40 @@
|---|---|---| |---|---|---|
| `tracks` | array | дорожки релиза без дублей (дубль — одинаковые тип + студия) | | `tracks` | array | дорожки релиза без дублей (дубль — одинаковые тип + студия) |
| `studios` | array | уникальные `{studio, translation_type, lang, is_18plus, episodes}`; `lang` — язык дорожки студии (как в `tracks[].lang`, может быть `null`). Одна студия с русским и украинским дубляжом даёт две записи | | `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` — такой строки на странице нет | | `original` | object \| null | оригинальная дорожка из строки «Аудио … \|Original\|»: `{lang, codec, channels, bitrate_kbps, khz, atmos, raw}` (поля могут быть `null`, `atmos` — bool); `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` — сведений нет |
| `video` | object \| null | видео из строки «Видео:» / «Video:» (у nnm — также «Видеокодек», «Размер кадра», «Битрейт видео»), см. ниже; `null` — строки нет |
| `release` | object \| null | тип релиза, контейнер, продолжительность, см. ниже; `null` — ничего из этого на странице нет |
| `subtitles` | object \| null | субтитры из строк «Субтитры:», «Субтитры 2:», «Язык субтитров:» (нет строк — языки из «Sub (Rus, Eng)» заголовка), см. ниже; `null` — о субтитрах ничего не сказано |
| `title` | string \| null | заголовок раздачи, если найден | | `title` | string \| null | заголовок раздачи, если найден |
| `segmenter` | `"model"` \| `"rules"` | чем страница разбита на строки дорожек: обученным сегментатором или запасными правилами | | `segmenter` | `"model"` \| `"rules"` | чем страница разбита на строки дорожек: обученным сегментатором или запасными правилами |
| `segmenter_warning` | string (необязательно) | модель-сегментатор и запасные правила разошлись (модель ничего не выбрала или правила нашли больше): страница — кандидат на дообучение, см. `docs/retrain-segmenter.md` |
| `tracker` | string | только для `url`: `rutracker`, `nnm`, `kinozal` или `lostfilm` | | `tracker` | string | только для `url`: `rutracker`, `nnm`, `kinozal` или `lostfilm` |
`video` (все поля, кроме `hdr` и `raw`, могут быть `null`):
`codec` — `HEVC`, `AVC`, `AV1`, `VP9`, `VC-1`, `MPEG-2`, `XviD`, `DivX`, `MPEG-4`;
`width`/`height` — размер кадра; `resolution` — класс по ширине (`2160p`, `1440p`, `1080p`, `720p`; кадр с
полосами 1920x800 — тоже `1080p`), для SD — высота кадра (`576p`, `400p`); `fps`; `bitrate_kbps`;
`bit_depth` — 8/10/12; `hdr` — массив из `Dolby Vision`, `HDR10+`, `HDR10`, `HLG` или просто `HDR`, если вид
не назван (ищется в строке видео, «HDR format», типе релиза и заголовке), пустой — HDR нет; `dv_profile` —
профиль Dolby Vision строкой («8», «5»); `raw` — строка видео целиком.
`release`: `quality` — значение «Тип релиза» / «Качество видео» / «Качество» как на странице (без
«\| Исходник: …»); `source` — нормализованный источник: `UHD BDRemux`, `BDRemux`, `WEB-DLRip`, `WEB-DL`, `WEBRip`,
`BDRip`, `HDRip`, `Blu-ray`, `DVDRip`, `DVD9`, `DVD5`, `HDTVRip`, `HDTV`, `SATRip`, `TVRip`, `DVB`, `VHSRip`,
`DCPRip`, `IPTV`, `CAMRip`, `DVD` (из «Качества», иначе из заголовка); `container` — `MKV`, `MP4`, `AVI`, `M2TS`,
`ISO`, `DVD-Video`, `TS`, `WEBM`, `MOV`, `WMV` (kinozal строку контейнера обычно не пишет — тогда `null`);
`duration` — «Продолжительность» как на странице («01:38:12», «18 x ~ 00:20:00»); `duration_sec` — секунды,
только если это одно значение ЧЧ:ММ:СС фильма (не серии и не список файлов). Любое поле может быть `null`.
`subtitles`: `languages` — коды языков по порядку (те же коды, что у `lang` дорожки); `tracks` — по записи
на язык: `{lang, forced, full, sdh}` (флаги «Forced/форсированные/надписи», «Full/полные», «SDH/для
слабослышащих» объединяются по всем строкам); `format` — «Формат субтитров» как на странице или `null`;
`hardsub` — субтитры вшиты в видео; `raw` — исходные строки через «; ». «Субтитры: нет/отсутствуют» даёт
объект с пустым `languages` — субтитров точно нет; `null` — страница о субтитрах молчит.
`series`: `season` — номер сезона строкой или `null`; `episodes` — выпущенные серии («1-9»); `series`: `season` — номер сезона строкой или `null`; `episodes` — выпущенные серии («1-9»);
`total` — всего серий; `by_studio` — `{студия: серии}`; `full_release` — студии без указанных серий `total` — всего серий; `by_studio` — `{студия: серии}`; `full_release` — студии без указанных серий
(для сериала это значит «озвучены все вышедшие серии»). Подробности — в [integration.md](integration.md). (для сериала это значит «озвучены все вышедшие серии»). Подробности — в [integration.md](integration.md).
@@ -79,7 +113,7 @@
| `lang_source` | `"audio"` \| `"model"` \| `"text"` \| null | откуда язык, в порядке приоритета: строка «Аудио» страницы (`Ukrainian E-AC3 …` → `UKR`, самый надёжный источник), нейросеть (классы `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}` из строки «Аудио» страницы (`"E-AC3"`, `6`, `768`; `channels` — число каналов, «5.1» → 6); каждое поле может быть `null`; весь объект `null`, если строка не найдена | | `audio` | object \| null | `{codec, channels, bitrate_kbps, khz, atmos}` из строки «Аудио» страницы (`"E-AC3"`, `6`, `768`, `48`, `true`; `channels` — число каналов, «5.1» → 6; `khz` — частота дискретизации, 44.1 / 48; `atmos` — в строке есть «Atmos», кодек при этом остаётся `E-AC3`/`TrueHD`); поля, кроме `atmos`, могут быть `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]]`; пусто, если серий нет |
@@ -157,7 +191,7 @@ type Track = {
lang_source: "model" | "audio" | "text" | "url" | null; // "url" — ссылка lostfilm.tv; null вместе с lang lang_source: "model" | "audio" | "text" | "url" | null; // "url" — ссылка lostfilm.tv; null вместе с lang
role: "перевод" | "оригинал" | "комментарии"; // никогда null role: "перевод" | "оригинал" | "комментарии"; // никогда null
official: boolean; official: boolean;
audio: { codec: string | null; channels: number | null; bitrate_kbps: number | null } | null; audio: { codec: string | null; channels: number | null; bitrate_kbps: number | null; khz: number | null; atmos: boolean } | null;
// только в ответе релиза (html / url): // только в ответе релиза (html / url):
source: "page" | "title"; source: "page" | "title";
episodes: string | null; // "1-4", "1,3-5"; null — серии не указаны episodes: string | null; // "1-4", "1,3-5"; null — серии не указаны
@@ -167,7 +201,8 @@ type Track = {
type Release = { type Release = {
tracks: Track[]; // может быть пустым массивом tracks: Track[]; // может быть пустым массивом
studios: { studio: string | null; translation_type: string; lang: string | null; 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; original: { lang: string | null; codec: string | null; channels: number | null; bitrate_kbps: number | null;
khz: number | null; atmos: boolean; raw: string } | null;
translation_types: string[]; translation_types: string[];
is_18plus: boolean; is_18plus: boolean;
series: { series: {
@@ -177,8 +212,22 @@ type Release = {
by_studio: Record<string, string>; // { "LostFilm": "1-4" }, может быть {} by_studio: Record<string, string>; // { "LostFilm": "1-4" }, может быть {}
full_release: string[]; // студии без указанных серий full_release: string[]; // студии без указанных серий
} | null; // null — ни сезона, ни серий в раздаче нет } | null; // null — ни сезона, ни серий в раздаче нет
video: {
codec: string | null; width: number | null; height: number | null; resolution: string | null;
fps: number | null; bitrate_kbps: number | null; bit_depth: number | null;
hdr: string[]; // [] — HDR нет
dv_profile: string | null; raw: string;
} | null;
release: { quality: string | null; source: string | null; container: string | null;
duration: string | null; duration_sec: number | null } | null;
subtitles: {
languages: string[]; // [] — «Субтитры: нет»
tracks: { lang: string; forced: boolean; full: boolean; sdh: boolean }[];
format: string | null; hardsub: boolean; raw: string | null;
} | null; // null — о субтитрах на странице ничего нет
title: string | null; title: string | null;
segmenter: "model" | "rules"; segmenter: "model" | "rules";
segmenter_warning?: string; // только если модель и правила разошлись
tracker?: "rutracker" | "nnm" | "kinozal" | "lostfilm"; // только для url tracker?: "rutracker" | "nnm" | "kinozal" | "lostfilm"; // только для url
}; };
``` ```
@@ -226,6 +275,8 @@ Cloudflare: FlareSolverr нестабилен.
по тексту; отдельно берётся `original`; определяются `role`, `official`, `audio`. по тексту; отдельно берётся `original`; определяются `role`, `official`, `audio`.
7. **Агрегат релиза.** Дубли (тип + студия) склеиваются; студии из заголовка раздачи, которых нет 7. **Агрегат релиза.** Дубли (тип + студия) склеиваются; студии из заголовка раздачи, которых нет
в описании, добавляются с `source: "title"`; серии и сезон разбираются правилами (`src/episodes.py`). в описании, добавляются с `source: "title"`; серии и сезон разбираются правилами (`src/episodes.py`).
8. **Техданные** (`video`, `release`, `subtitles`) — правилами по строкам «Видео:», «Тип релиза:»,
«Контейнер:», «Продолжительность:», «Субтитры:» и заголовку (`src/techinfo.py`), без нейросети.
Размеченные данные (`data/labels.csv`) используются только при обучении: на них учатся метки Размеченные данные (`data/labels.csv`) используются только при обучении: на них учатся метки
`translation_type`, спан студии, 18+ и язык. Схема файла и происхождение меток — в `translation_type`, спан студии, 18+ и язык. Схема файла и происхождение меток — в
+21 -7
View File
@@ -73,13 +73,27 @@ BIO-разметка строится по символьным спанам ч
используются, если `models/segmenter` нет или модель не нашла ни одной строки; используются, если `models/segmenter` нет или модель не нашла ни одной строки;
режим можно форсировать: `predict_release(..., segmenter="rules")`. режим можно форсировать: `predict_release(..., segmenter="rules")`.
* Обучение: `python -m src.segmenter --train` (~1 мин). Разметка строк — по * Обучение: `python -m src.segmenter --train --out models/segmenter_v2` (или меню
`raw` из `labels.csv` (362 страницы в `data/pages`); синтетика: `raw` всех `tools/collect.py` → «2»). Разметка строк — по `raw` и `audio_raw` из `labels.csv`
train-строк (в т.ч. nnm/kinozal без страниц) вставляются в контекст реальных (страницы `data/pages`); синтетика: `raw` всех train-строк вставляются в контекст
строк страниц. Страницы делятся по хешу `release_id` (≈20% в тест). реальных строк страниц. Страницы делятся по хешу `release_id` (≈20% в тест).
* Метрики: строки (test) P 0.936 / R 0.994; e2e на непросмотренных страницах * Текст kinozal берётся из `data/raw/kinozal/<id>.json` (`h1` + «Техданные» +
rutracker (n=64) — precision 0.904 → 0.946 против правил, recall 0.975 → 0.975 «Релиз», `segmenter.page_text`): в `data/pages/kz-*.txt` нет ни заголовка, ни
(`tests/e2e_segmenter.py`; выборка мала, разброс большой). «Техданных», откуда взята половина меток kinozal, а сервис (`fetch_html`)
видит обе вкладки.
* Кодек-строки со студией с 02.11.2026 размечены сами (`studio_source=codec`, см.
`docs/collecting.md`): их `raw`/`audio_raw` дают позитивы. Кодек-строка русской
дорожки («Аудио 3: AC3 … Русский (MVO, LostFilm)», «Аудио Rus: … | LostFilm»),
не попавшая в разметку, в обучение не идёт
(метка -1): у rutracker `raw` — строка «Перевод N:», и как негатив она учила
модель отбрасывать кодек-строки — а на kinozal студии только в них.
* В `predict_release` выбранные моделью кодек-строки — второстепенный источник
(`source: codec`): подключаются, только если у какой-то дорожки страницы нет
студии; если других строк нет (nnm), они и есть дорожки.
* e2e: `tests/e2e_segmenter.py [каталог]` — пары (тип, студия) на тестовых
страницах, не больше `E2E_MAX` (400) на трекер; `--train-pages` — ещё и train;
основная модель — `E2E_MODEL` (по умолчанию `models/best`), например
`set E2E_MODEL=models/runs/<запуск>` перед запуском, чтобы сравнить кандидата.
* Нарезка ВНУТРИ строки («Дублированный, многоголосый») в данных не размечена * Нарезка ВНУТРИ строки («Дублированный, многоголосый») в данных не размечена
(91 группа из 6735) — остаётся на правилах `_split_entries`, но только на (91 группа из 6735) — остаётся на правилах `_split_entries`, но только на
строках, выбранных моделью. строках, выбранных моделью.
+30
View File
@@ -142,6 +142,36 @@ id разных трекеров не пересекались. Коллизий
повторяют логику правил. Для нарезки внутри строки («Дублированный, повторяют логику правил. Для нарезки внутри строки («Дублированный,
многоголосый») нужна ручная проверка — режим ревью ещё не сделан. многоголосый») нужна ручная проверка — режим ревью ещё не сделан.
Студии, которые парсеры раньше теряли (доразметка от 02.11.2026):
* **nnm, «Перевод: Дублированный (Невафильм)»** — строки «Перевод:» (tech) шли с
«(не указана)», хотя студия названа в той же части. Модель училась её не извлекать.
Теперь — `nnm_part_studio` (скобки или «… - Студия»; тип, «x2», «Лицензия», источник
вроде iTunes/Blu-ray — не студия). +3 051 студия.
* **kinozal, заголовок «… / ПМ (LostFilm, HDrezka Studio), СТ / …»** — во «Техданных»
только «Перевод: Профессиональный многоголосый», студии названы в заголовке и в
строках «Аудио Rus: … | LostFilm» без типа. Теперь — `kz_title_rows`: строка на каждую
студию из заголовка, которой нет на странице (`studio_source=title`, вход — тот же
сегмент, что строит сервис из заголовка; строка «Аудио» с этой студией — в
`audio_raw`). +4 219 строк в 3 007 релизах.
* **Студии только в кодек-строках** («Аудио 2: AC3 … (MVO, LostFilm)», «Аудио Rus: … | TVShows»,
«… • DUB | LeDoyen • UKR») — разметка шла по «Перевод:», кодек-строка была лишь `audio_raw`
у найденной дорожки. Теперь — `codec_line_rows`: строка на каждую студию русской/украинской
кодек-строки, которой нет среди меток релиза (`studio_source=codec`, вход — сегмент, как его
режет сервис; тип — из строки, иначе единственный тип релиза, иначе «неизвестно»). Студия —
из словаря меток (≥ 5 строк) или из скобок/`|…|`; раскладка, кодек, «поздний», «серии» — не
студия. +6 428 строк в 2 862 релизах (kinozal 3 444, nnm 1 609, rutracker 1 375). Аудит,
откуда взялась доработка: `reports/tracks_audit.md`.
Повторить аудит дорожек по `data/raw` (сколько студий из кодек-строк, «Перевод:»
и заголовков не попало в метки, мусор в колонке студий, форматы, которые не ловят регулярки):
`.venv\Scripts\python tools\audit_tracks.py` → `reports/tracks_audit_numbers.md`
(`--cache путь.pkl` ускоряет повторный запуск). Выводы и план — `reports/tracks_audit.md`.
Сборщик делает это при переиндексации. Для уже собранного `labels.csv` без пересборки
страниц: `.venv\Scripts\python tools\relabel_studios.py` (копия — `data/labels.bak_relabel_*.csv`;
`--dry-run` — только статистика; повторный запуск ничего не добавляет).
## Разнообразная выборка (режим balanced) ## Разнообразная выборка (режим balanced)
Списки разделов идут от новых к старым, поэтому чтение подряд даёт только свежие Списки разделов идут от новых к старым, поэтому чтение подряд даёт только свежие
+3 -1
View File
@@ -10,7 +10,7 @@
регулярками** — модель учится воспроизводить эту разметку и обобщать её на регулярками** — модель учится воспроизводить эту разметку и обобщать её на
новые формулировки. новые формулировки.
Размер (на момент последней сборки): ~41 тыс. строк из ~28 тыс. релизов, 3 источника Размер (снимок 02.11.2026): 55 324 строки из 27 900 релизов, 3 источника
(rutracker, nnm, kinozal); HTML-дамп страниц — `data/raw` (~32 тыс. файлов, ≈2.6 ГБ, в git не (rutracker, nnm, kinozal); HTML-дамп страниц — `data/raw` (~32 тыс. файлов, ≈2.6 ГБ, в git не
хранится), разобранные тексты — `data/pages`. Точные числа показывает «Статистика датасета» в хранится), разобранные тексты — `data/pages`. Точные числа показывает «Статистика датасета» в
`tools/collect.py` (пункт 3) и отчёт `reports/data_audit.md` после `prepare_data`. `tools/collect.py` (пункт 3) и отчёт `reports/data_audit.md` после `prepare_data`.
@@ -29,6 +29,8 @@
| `studio` | автор («(не указана)» если нет) | **метка 2** (спан в тексте) | | `studio` | автор («(не указана)» если нет) | **метка 2** (спан в тексте) |
| `age_18`, `note` | флаг/пометка 18+ | **метка 3** | | `age_18`, `note` | флаг/пометка 18+ | **метка 3** |
| `role_raw` | скобочная часть / конкретный тип из перечисления | вход для nnm tech-строк | | `role_raw` | скобочная часть / конкретный тип из перечисления | вход для nnm tech-строк |
| `studio_source` | откуда взята строка: `release` / `tech` (описание релиза / техданные, в т.ч. nnm «Перевод:»), `codec` (кодек-строка «Аудио N: … (MVO, LostFilm)»), `title` (заголовок kinozal), `audio` (только строка «Аудио»), `manual` (ручная разметка) | не используется; `codec` не попадает в словарь студий `codec_line_rows` |
| `audio_raw` | кодек-строка той же дорожки целиком | разметка строк для сегментатора |
| остальные (`ru_title`, `codec`, …) | контекст релиза | не используются | | остальные (`ru_title`, `codec`, …) | контекст релиза | не используются |
## Форматы входа по источникам ## Форматы входа по источникам
+6
View File
@@ -37,12 +37,18 @@ python -m src.predict --url https://kinozal.tv/details.php?id=1 --cookie "uid=..
| `studios` | уникальные `{studio, translation_type, is_18plus}` | | `studios` | уникальные `{studio, translation_type, is_18plus}` |
| `translation_types` | отсортированный список типов релиза | | `translation_types` | отсортированный список типов релиза |
| `is_18plus` | `true`, если 18+ хотя бы у одной дорожки | | `is_18plus` | `true`, если 18+ хотя бы у одной дорожки |
| `original`, `series` | оригинальная дорожка; сезон и серии |
| `video` | кодек, разрешение, fps, битрейт, битность, HDR (Dolby Vision / HDR10+ / HDR10 / HLG) |
| `release` | тип релиза («WEB-DL», «BDRemux»…), контейнер, продолжительность |
| `subtitles` | языки субтитров с пометками forced / full / SDH, формат, вшитые ли |
| `title` | заголовок раздачи (если найден в HTML) | | `title` | заголовок раздачи (если найден в HTML) |
| `tracker` | только для `--url` | | `tracker` | только для `--url` |
Дорожки дедуплицируются по (тип, студия). Студии из заголовка раздачи вида Дорожки дедуплицируются по (тип, студия). Студии из заголовка раздачи вида
`ПМ (LostFilm)`, которых нет в описании, добавляются с `source: "title"` `ПМ (LostFilm)`, которых нет в описании, добавляются с `source: "title"`
(у остальных `source: "page"`). `--text` по-прежнему возвращает только `tracks`. (у остальных `source: "page"`). `--text` по-прежнему возвращает только `tracks`.
Все поля и их значения — в [api-reference.md](api-reference.md); `video`, `release`, `subtitles`
считаются правилами (`src/techinfo.py`), без нейросети.
## Python API ## Python API
+6 -1
View File
@@ -41,7 +41,12 @@ API:
| `lang`, `lang_confidence`, `lang_source` | язык дорожки (`RUS`, `UKR`, `ENG`, …); источник: `model` (языковая голова), `audio` (строка «Аудио»), `text` (слова в строке дорожки) или `null` | | `lang`, `lang_confidence`, `lang_source` | язык дорожки (`RUS`, `UKR`, `ENG`, …); источник: `model` (языковая голова), `audio` (строка «Аудио»), `text` (слова в строке дорожки) или `null` |
| `role` | `перевод` / `оригинал` / `комментарии` (по строке «Аудио» и тексту, правилами) | | `role` | `перевод` / `оригинал` / `комментарии` (по строке «Аудио» и тексту, правилами) |
| `official` | в строке «Аудио» сказано «официальный» / «лицензионный» | | `official` | в строке «Аудио» сказано «официальный» / «лицензионный» |
| `audio` | `{codec, channels, bitrate_kbps}` из строки «Аудио» (или `null`) | | `audio` | `{codec, channels, bitrate_kbps, khz, atmos}` из строки «Аудио» (или `null`) |
Ответ релиза (`html`/`url`) кроме дорожек несёт `video`, `release` и `subtitles` — разрешение, HDR,
тип релиза, контейнер, продолжительность, языки субтитров; поля и значения — в
[api-reference.md](api-reference.md). ONNX-путь (Б) их не считает: это правила `src/techinfo.py`,
их нужно повторить на стороне клиента или ходить в HTTP-сервис.
Русский и украинский дубляж одной студии — две отдельные дорожки; так же раздельны обычная Русский и украинский дубляж одной студии — две отдельные дорожки; так же раздельны обычная
дорожка студии и её дорожка 18+ (если на странице две строки «Аудио» с одной студией). дорожка студии и её дорожка 18+ (если на странице две строки «Аудио» с одной студией).
+3 -1
View File
@@ -65,12 +65,14 @@
| 1 baseline | сделано | | 1 baseline | сделано |
| 2 URL + агрегат релиза | сделано: `src/fetch.py`, `src/release.py`, `--url`, `POST /predict {"url"}` | | 2 URL + агрегат релиза | сделано: `src/fetch.py`, `src/release.py`, `--url`, `POST /predict {"url"}` |
| 3 title-fallback | сделано (`ПМ (LostFilm)` → дорожка с `source: "title"`); заголовок берётся из HTML (`<h1>`/`<title>`), из текстового дампа (первая строка вида «… / год / …»), либо передаётся явно (`title`). Если на странице есть дорожка того же типа без студии, студия вписывается в неё (`source: page+title`), а не дублируется. **Эффект на e2e мал**: kinozal P/R 0.107 → 0.143, rutracker без изменений (0.826 / 0.984) — в разметке датасета студия из заголовка почти не нужна | | 3 title-fallback | сделано (`ПМ (LostFilm)` → дорожка с `source: "title"`); заголовок берётся из HTML (`<h1>`/`<title>`), из текстового дампа (первая строка вида «… / год / …»), либо передаётся явно (`title`). Если на странице есть дорожка того же типа без студии, студия вписывается в неё (`source: page+title`), а не дублируется. **Эффект на e2e мал**: kinozal P/R 0.107 → 0.143, rutracker без изменений (0.826 / 0.984) — в разметке датасета студия из заголовка почти не нужна |
| 4 kinozal-вкладка | **не сделано** в сервисе: страница релиза требует авторизации, а `get_srv_details.php?id=…&pagesd=N` уже вызывает `KinozalClient.php` в grabber. Рекомендация: grabber скачивает вкладку сам и шлёт `{"html": …, "title": <заголовок раздачи>}` (во фрагменте вкладки заголовка нет) | | 4 kinozal-вкладка | сделано: `fetch_html` для kinozal скачивает вкладки `showtab(id,N)` (`get_srv_details.php?id=…&pagesd=N`, кроме «Техданных» и «Скриншотов») и дописывает их в `#tabs`. Кодек-строки «Аудио N: … (MVO, LostFilm)» сегментатор отбрасывает (в разметке rutracker это не `raw`), поэтому они берутся правилом `codec_segments` как второстепенный источник (`source: codec` / `page+codec`): только дорожки со студией, студия-дубль (`same_studio`) пропускается. Кодек-строки подключаются, только если у какой-то дорожки страницы нет студии. На test-сплите модели 29.10 (по 300 релизов), студии P/R без → с кодек-строками: rutracker 0.908/0.882 → 0.840/0.900 (почти все «ложные» — реальные студии, которых нет в эталоне), nnm 0.875/0.915 → 0.875/0.940, kinozal (текст из raw JSON) 0.816/0.845 → 0.800/0.863 |
| 5 false-positives сегментации | сделано: починена резка «Авторский, одноголосый» по запятой; постобработка спана (`refine_span`: до границ слова, мусор вроде `L,C,R,l,r`/кодеков → null). Остаток — промахи модели на `kbps \| DUB Автор` и особенности разметки | | 5 false-positives сегментации | сделано: починена резка «Авторский, одноголосый» по запятой; постобработка спана (`refine_span`: до границ слова, мусор вроде `L,C,R,l,r`/кодеков → null). Остаток — промахи модели на `kbps \| DUB Автор` и особенности разметки |
| 9 переобучение | датасет вырос 3286 → 6864 строк; обе модели переобучены (см. ниже) | | 9 переобучение | датасет вырос 3286 → 6864 строк; обе модели переобучены (см. ниже) |
| 6 tiny2 + int8 | сделано: 29.3 МБ, parity 99.8% тип / 99.2% спан / 100% 18+ (`reports/onnx_parity_tiny2_int8.md`) | | 6 tiny2 + int8 | сделано: 29.3 МБ, parity 99.8% тип / 99.2% спан / 100% 18+ (`reports/onnx_parity_tiny2_int8.md`) |
| 7 тесты | `run_tests.py`: 57 ok (URL allowlist/SSRF, декодирование, заглушки Cloudflare/логина, title, слияние, агрегат) | | 7 тесты | `run_tests.py`: 57 ok (URL allowlist/SSRF, декодирование, заглушки Cloudflare/логина, title, слияние, агрегат) |
| 8 `target/` | сделано | | 8 `target/` | сделано |
| 10 аудит дорожек | сделано 02.11.2026: `tools/audit_tracks.py` → `reports/tracks_audit.md` (план из 5 пунктов). Пункт 1 — студии из кодек-строк в разметке (`codec_line_rows`, +6 428 строк); модель переобучена: test joint exact 0.891 → 0.906, e2e P/R kinozal 0.745/0.850 → 0.766/0.879, rutracker 0.866/0.859 → 0.877/0.870, nnm 0.845/0.829 → 0.838/0.825. Пункты 2–5 и сегментатор на новой разметке — открыты |
| 11 техданные релиза | сделано 02.11.2026: в ответе релиза `video` (кодек, разрешение, fps, битрейт, битность, HDR, профиль DV), `release` (тип, контейнер, продолжительность), `subtitles` (языки, forced/full/SDH, формат, вшитые); у дорожек `audio.khz` и `audio.atmos` — правила `src/techinfo.py`. Покрытие на 600 сохранённых страницах каждого трекера: видео найдено на 90% rutracker / 93% nnm / 100% kinozal, кодек — у 91% / 92% / 97% из них. Заголовок rutracker больше не рвётся пробелами («Галактическо е»). В ONNX-клиентах этих полей нет |
## Открытые вопросы ## Открытые вопросы
+93
View File
@@ -0,0 +1,93 @@
# Дообучение сегментатора страниц
Сегментатор решает, какие строки страницы описывают дорожки озвучки
(`src/segmenter.py`, rubert-tiny2). Дообучать его нужно, когда `/predict` теряет
дорожки или в ответе появилось `segmenter_warning` (модель и правила
разошлись). Делается на машине, где лежат `data/raw`, `data/pages` и `models/segmenter`
(в git их нет; в репозитории на машине разработки их не оказалось, пути ниже — относительно каталога проекта, где они есть): без них обучение выдаст более слабые веса, чем рабочие.
## 0. Понять причину (по желанию, 1 минута)
Какие оценки модель ставит строкам страницы (`docker compose exec` — на
сервере, либо то же в `.venv` локально):
```bash
docker compose exec grabber-ai python -c "from src.fetch import fetch_html; from src.html_extract import html_to_text; from src.segmenter import LineSegmenter, page_lines; h,_=fetch_html('https://rutracker.org/forum/viewtopic.php?t=6919056', timeout=60); L=page_lines(html_to_text(h)); s=LineSegmenter('/app/models/segmenter'); [print(round(float(p),3), l[:90]) for l,p in zip(L, s.score(L)) if p>0.01]"
```
* оценки 0.3–0.49 у строк-дорожек — упёрлись в порог (`THRESHOLD = 0.5`);
* оценки около 0 — модель не знает такую вёрстку, нужны примеры (шаги ниже).
## 1. Добавить проблемные раздачи
```powershell
cd <каталог проекта, где лежат data\raw и models>
.venv\Scripts\python tools\collect.py
```
Главное меню → **6** («Обучение по ссылкам»): вставьте ссылки на раздачи, у
которых дорожки определились неверно (например,
`https://rutracker.org/forum/viewtopic.php?t=6919056`). Страницы скачиваются
вашим Chrome (вход на трекер делаете сами), парсер строит метки. Проверьте
таблицу: строк-дорожек должно быть столько же, сколько на странице (для
6919056 — четыре, у двух HDRezka разные типы). Правки: «2 18» — переключить
18+, «3 d» — удалить строку, «1 t <тип>» — тип, «1 s <студия>» — студия.
Сохраните: id попадёт в `data/manual_ids.txt`, такие релизы всегда в train и
повторяются при обучении ×5.
Чем больше похожих раздач (3–10 штук с разными студиями и той же вёрсткой),
тем надёжнее; одна страница редко меняет поведение модели.
## 2. Переобучить
Главное меню → **2** («Переобучить и протестировать»). Шаги идут сами:
`prepare_data`, обучение сегментатора в `models/segmenter_v2`
(≈ минуты на RTX 5070 Ti), `tests/run_tests.py`, таблица «правила / старые веса /
новые веса» по трекерам.
Без меню:
```powershell
.venv\Scripts\python -m src.prepare_data
.venv\Scripts\python -m src.segmenter --train --out models/segmenter_v2
.venv\Scripts\python tests\run_tests.py
.venv\Scripts\python tests\e2e_segmenter.py models/segmenter_v2
```
## 3. Принять новые веса
Меню покажет, лучше ли новые веса. На «y» они заменят `models/segmenter`, старые
уйдут в `models/segmenter_old_<время>`. Если хуже — не принимайте: добавьте
больше примеров вёрстки и повторите шаг 2.
Исправленная раздача проверяется на сервере после пересборки образа (шаг 5).
## 4. Упаковать модели и пересобрать образ
```powershell
.venv\Scripts\python tools\pack_models.py --stage docker-models
docker compose up -d --build
```
(в CI — workflow `.gitea/workflows/docker.yml`, см. `docs/docker.md`).
## 5. Проверить на сервере
```powershell
irm http://192.168.100.67:7654/health # "segmenter": "model"
irm http://192.168.100.67:7654/predict -Method Post -ContentType "application/json; charset=utf-8" -Headers @{Authorization="Bearer <токен>"} -Body ([Text.Encoding]::UTF8.GetBytes('{"url":"https://rutracker.org/forum/viewtopic.php?t=6919056"}')) | ConvertTo-Json -Depth 10
```
Должно быть `"segmenter": "model"`, четыре дорожки, без `segmenter_warning`.
## Что делает сервис, пока модель не дообучена
* **Достройка блока** (`release.complete_block`): модель выбрала часть строк
списка под «Перевод:» — соседние строки с типом озвучки (Проф…, Любит…, …голос…)
добавляются в тот же список.
* **Запасные правила**: модель не нашла ни одной строки — дорожки берут правила
`html_extract.extract_segments`; в ответе `"segmenter": "rules"`.
* **`segmenter_warning`** в ответе и строка `segmenter: …` в логе сервиса — модель
и правила разошлись. На глоссариях типов озвучки правила дают ложные
срабатывания, поэтому предупреждение — повод посмотреть страницу, а не
всегда ошибка.
+8
View File
@@ -84,6 +84,14 @@ copy /Y <путь>\labels.csv data\labels.csv
.venv\Scripts\python -m src.train .venv\Scripts\python -m src.train
``` ```
Если поменялась только логика разметки студий (без перекачки страниц), сначала
`.venv\Scripts\python tools\relabel_studios.py` (см. `docs/collecting.md`), затем
`prepare_data` и обучение. Кандидата из `models/runs/` перед заменой `models/best`
сравнить: `python -m src.runs --out runs.json --dirs models/best models/runs/<запуск>` (test-сплит) и
`E2E_MODEL=models/runs/<запуск>` + `tests/e2e_segmenter.py` (релизы). Поставить —
меню `tools/collect.py`, пункт 7, или `src.runs.promote`; для Docker затем
`python tools/pack_models.py --stage docker-models`.
## Troubleshooting ## Troubleshooting
| Симптом | Причина | Решение | | Симптом | Причина | Решение |
+36 -24
View File
@@ -1,32 +1,44 @@
# Аудит датасета: data/labels.csv # Аудит датасета: data/labels.csv
строк в CSV: 44677 строк в CSV: 56131
строк после подготовки: 35383 (train 31343 / test 4040) строк после подготовки: 46894 (train 42458 / test 4436)
источники: {'kinozal': 6382, 'rutracker': 14342, 'nnm': 14659} источники: {'kinozal': 13963, 'rutracker': 16647, 'nnm': 16284}
уникальных нормализованных текстов: 18870 уникальных нормализованных текстов: 25614
групп (релиз ∪ одинаковый текст): 6681 групп (релиз ∪ одинаковый текст): 7472
удалено: пустой текст 0, мусорный studio 155, не-озвучка (оригинал/субтитры) 6, противоречивые дубли 0 (групп-противоречий: 0), несколько дорожек в одной строке 20, лишние повторы (> 40 одинаковых) 16950 удалено: пустой текст 0, мусорный studio 156, не-озвучка (оригинал/субтитры) 6, противоречивые дубли 85 (групп-противоречий: 75), несколько дорожек в одной строке 20, лишние повторы (> 40 одинаковых) 18277
спан автора найден: 24004 (67.8%), не найден: 231 спан автора найден: 38583 (82.3%), не найден: 277
аугментировано морфологических вариантов: 7658 аугментировано морфологических вариантов: 9083
дорожек 18+: 701 (из них без упоминания «18» в тексте: 8) дорожек 18+: 1006 (из них без упоминания «18» в тексте: 19)
язык размечен: 15282 из 35383; со строкой «Аудио» во входе: 11796; классы: {'RUS': 14673, 'UKR': 556, 'ENG': 53}; редкие (<20, не учатся): {'DEU': 7, 'FRA': 14, 'CHI': 3, 'KAZ': 10, 'GEO': 2, 'BEL': 2, 'KOR': 1, 'JPN': 4, 'LAV': 1, 'CZE': 1, 'ITA': 6, 'SPA': 8} язык размечен: 26084 из 46894; со строкой «Аудио» во входе: 20037; классы: {'RUS': 24483, 'UKR': 1548, 'ENG': 53}; редкие (<20, не учатся): {'DEU': 6, 'FRA': 16, 'CHI': 4, 'KAZ': 12, 'GEO': 1, 'BEL': 2, 'KOR': 1, 'JPN': 4, 'LAV': 1, 'CZE': 1, 'ITA': 6, 'SPA': 8}
## Распределение translation_type ## Распределение translation_type
- проф. дубляж: 9936 - проф. многоголосый: 13192
- проф. многоголосый: 9831 - проф. дубляж: 12242
- авт. одноголосый: 4265 - авт. одноголосый: 5782
- неизвестно: 2800 - неизвестно: 4215
- проф. двухголосый: 2153 - одноголосый: 2817
- одноголосый: 2126 - проф. двухголосый: 2716
- люб. многоголосый: 1381 - люб. многоголосый: 1879
- многоголосый: 824 - многоголосый: 1346
- люб. одноголосый: 646 - люб. двухголосый: 772
- люб. двухголосый: 551 - люб. одноголосый: 711
- люб. дубляж: 380 - двухголосый: 456
- проф. одноголосый: 293 - люб. дубляж: 408
- двухголосый: 192 - проф. одноголосый: 354
- авт. двухголосый: 3 - авт. двухголосый: 2
- авт. многоголосый: 2 - авт. многоголосый: 2
## Примеры противоречий (один текст — разные типы)
- nnm:nnm-1104137 «ac-3, 48.0 khz, 1 ch, 192 kbps, cbr (русский, проф. многогол» -> ['многоголосый', 'проф. многоголосый']
- nnm:nnm-1116916 «ac3, 2 ch, 192 kbps, 48,0 khz (русский - двухголосый ''видео» -> ['двухголосый', 'проф. двухголосый']
- nnm:nnm-1116916 «dts, 2 ch, 768 kbps, 48,0 khz (русский - авторский ''андрей » -> ['авт. одноголосый', 'неизвестно']
- nnm:nnm-1129736 «ac3, 48 khz, ~192 kbps, 2ch, русский (любительский, многогол» -> ['люб. многоголосый', 'проф. многоголосый']
- nnm:nnm-1129736 «ac3, 48 khz, ~192 kbps, 2ch, русский (любительский, двухголо» -> ['люб. двухголосый', 'проф. двухголосый']
- nnm:nnm-1129736 «ac3, 48 khz, ~192 kbps, 2ch, русский (любительский, двухголо» -> ['люб. двухголосый', 'проф. двухголосый']
- nnm:nnm-1129736 «ac3, 48 khz, ~192 kbps, 2ch, русский (любительский, одноголо» -> ['люб. одноголосый', 'одноголосый']
- nnm:nnm-1131052 «ac3, 2 ch, 192 kbps (русский - авторский ''юрий живов'') [ru» -> ['авт. одноголосый', 'неизвестно']
- nnm:nnm-1131273 «ac3, 2 ch, 192 kbps (русский - авторский ''юрий живов'') - о» -> ['авт. одноголосый', 'неизвестно']
- nnm:nnm-1141333 «ac3, 2 ch, 192 kbps (русский - авторский ''андрей гаврилов''» -> ['авт. одноголосый', 'неизвестно']
## Спан не найден (примеры) ## Спан не найден (примеры)
- [rutracker] studio=«datynet; : Куценко» text=«Одноголосый закадровый datynet; перевод: Куценко» - [rutracker] studio=«datynet; : Куценко» text=«Одноголосый закадровый datynet; перевод: Куценко»
- [rutracker] studio=«datynet и Yuka_chan; :Куценко» text=«Любительский (двухголосый закадровый) datynet и Yuka_chan; перевод:Куц» - [rutracker] studio=«datynet и Yuka_chan; :Куценко» text=«Любительский (двухголосый закадровый) datynet и Yuka_chan; перевод:Куц»
+126
View File
@@ -0,0 +1,126 @@
# Аудит дорожек в описаниях релизов (raw) — выводы и план
Вход: все сохранённые страницы `data/raw/{kinozal,nnm,rutracker}/*.json` (35 384 релиза:
kinozal 10 541, nnm 13 677, rutracker 11 166) против `data/labels.csv` (48 896 строк).
Текст как у сервиса: kinozal — «Техданные» + «Релиз», nnm/rutracker — пост.
Цифры и примеры: `reports/tracks_audit_numbers.md`, пересчёт:
.venv\Scripts\python tools\audit_tracks.py [--cache путь.pkl]
Строка дорожки = «Перевод N: / Озвучка: / Дубляж:» или кодек-строка («Аудио N: AC3 … (MVO, X)»)
русской/украинской дорожки; субтитры и сводки без типа/студии не считаются. Студии сравниваются
через `release.same_studio` («А. Гаврилов» = «Андрей Гаврилов»), словарь — студии меток (≥ 5 раз)
без мусора.
## Что нашлось
### 1. Студии из кодек-строк не попадают в метки (главное)
Студия названа в строке дорожки релиза, но её нет среди меток этого релиза:
| | релизов с метками | из них с недостающей студией | недостающих студий | из кодек-строк «Аудио» | из «Перевод:» | релизов, где у меток **нет ни одной** студии, а в кодек-строке есть |
|---|---|---|---|---|---|---|
| kinozal | 9 932 | 1 690 (17 %) | 3 736 | 3 240 | 496 | 1 129 |
| nnm | 10 085 | 1 295 (13 %) | 2 032 | 1 003 | 1 029 | 653 |
| rutracker | 7 742 | 972 (13 %) | 1 645 | 531 | 1 114 | 495 |
Топ недостающих: kinozal — HDrezka 362, TVShows 339, LostFilm 211, Red Head Sound 164, LeDoyen 117,
Jaskier 101, MovieDalen 89, Мосфильм-Мастер 80; nnm/rutracker — LeDoyen, Postmodern, НТВ+, HDrezka,
1+1, NewStation, Невафильм.
Причина: разметка строится по строкам «Перевод:», а кодек-строки идут только как `audio_raw`
к уже найденной дорожке (`studio_source=audio` — всего 992 строки). Типичный kinozal:
«Перевод: Профессиональный (многоголосый закадровый)» без студии + «Аудио 2: AC3 … (MVO, LostFilm)».
Это та же дыра, что закрыла доразметка nnm «Перевод:» (nnm 0.58 → 0.85 по релизам), только
крупнее. Обратное (студия в метках, которой нет на странице) почти не встречается: 1 / 22 / 14.
### 2. «Перевод N:» с несколькими студиями размечен частично (nnm, rutracker)
~1 000 студий на трекер — из строк вида «Перевод 3-6: Профессиональный (многоголосый) — Jaskier,
TVShows, HDrezka Studio, LostFilm» или «Перевод: Дублированный (Iyuno-SDI Group, Le Doyen),
Многоголосый (LostFilm, Jaskier, TVShows)»: в метках часть списка. Сводка prepare_data тоже
видит «несколько дорожек в одной строке».
### 3. Форматы кодек-строк, которые не ловят правила
Это правила разметки (`segmenter.RE_AUDIO_TRACK`) и запасной путь сервиса
(`release.codec_segments`), а не модель-сегментатор: промах здесь — дыра в метках сегментатора и
в фолбэке, не измеренная потеря предсказания.
| | кодек-строк | мимо RE_AUDIO_TRACK | мимо codec_segments |
|---|---|---|---|
| kinozal | 18 297 | 11 664 (из них 10 395 — «Аудио:» без номера) | 12 858 |
| nnm | 15 151 | 2 850 | 4 696 |
| rutracker | 11 471 | 2 496 | 5 058 |
* **мимо обоих**: «Аудио pic RUS:» (374), «Аудио #N Русский:» (175), «Аудио N Rus:» / «Аудио 3 Rus:» (~220),
«Аудио rus N:», «Звук N:», «Аудио #N (rus):», «• №N: Русский (АС3 …)», «Аудио 2. Русский:»,
nnm «Russian (Dolby AC3, 6 ch) …» без префикса (1 483), «Аудио (rus):».
* **«Аудио:» без номера** исключено намеренно (сводка «Аудио: Русский, английский (AC3…)»), но
~1 000 таких строк (kinozal 184, nnm 378, rt 463) — одна дорожка с типом/студией («Аудио: AC3, 6 ch, 448 kbps - Русский (MVO, Jaskier)»).
Нужно отличать сводку из нескольких языков от одиночной дорожки, а не снимать правило.
* **мимо codec_segments, но ловит RE_AUDIO_TRACK** (1 378 / 2 224 / 3 025): кодек-строка со студией,
но без тип-слова — «Аудио Rus: AC3 … |HDRezka|», «… | AlexFilm», «- Rus (HDRezka Studio)».
`RE_CODEC_TRACK` требует тип озвучки, и на kinozal это та же студия из п. 1.
* «АVO / МVO» с кириллической буквой — 99 строк, `\bAVO\b` их не видит.
### 4. Мусор в колонке studio меток
| вид | kinozal | nnm | rutracker |
|---|---|---|---|
| источник: Лицензия / Blu-ray CEE/EUR / iTunes / Netflix | 245 | 590 | 231 |
| «X по заказу Y» | 131 | 67 | 120 |
| язык-префикс «Русский, - AlexFilm», «Украинский, AMC» | 72 | — | 13 |
| тип-слово («дубляж», «полное дублирование») | 45 | 41 | 19 |
| каналы/кодек («6 ch», «AC3, 6ch, 448 кбит/с Ист-Вест») | 7 | 89 | 18 |
| язык вместо студии («Русский», «Украинский») | 7 | 26 | 18 |
| неру дорожка (казахский, «+ Английский + Комментарии») | 4 | 6 | 26 |
Всего ~1 800 строк (≈ 4 %, вместе с «субтитрами» в studio). Часть уже чистит prepare_data («мусорный studio 155»), остальное идёт в обучение.
### 5. Релизы без меток
| | без меток | нет строк дорожек | «не требуется»/немой | строки без типа | **есть тип озвучки** |
|---|---|---|---|---|---|
| kinozal | 609 | 15 | 186 | 407 (русские фильмы «Аудио: Русский (AC3…)») | 1 |
| nnm | 3 592 | 1 605 | 520 | 1 050 | **417** |
| rutracker | 3 424 | 1 537 | 535 | 790 | **562** |
legacy и `ok=False` среди них нет — все страницы обычные. ~980 релизов nnm/rutracker с
настоящими дорожками сборщик не разметил: «Перевод 1: Профессиональный (двухголосый закадровый)
Пётр Гланц и Евгения Лютая», «Перевод: Двуголосый (НТВ+)», аниме «Озвучка: … от StudioBand»,
«Аудио 1: AC-3, RUS(int) … | Озвучкa: Дубляж Reanimedia». «Строки без типа» — в основном
«Перевод: DreamersTeam» / «Озвучка: Persona99» (тип неизвестен, но студия есть).
## План (по ожидаемому эффекту на e2e)
Каждый пункт: сначала `--dry-run` со счётом строк, потом prepare → train, ворота — e2e
`tests/e2e_segmenter.py` (E2E_MAX=400) и joint exact на test-split против текущей `models/best`.
1. **Метки из кодек-строк** (kinozal 3 240, nnm 1 003, rt 531 студий; 2 277 релизов, где у меток нет
студий вовсе). В `harvest_core` + `tools/relabel_studios.py`: студия из кодек-строки русской
дорожки, которой нет среди меток, → заполнить studio-less строку того же типа или добавить строку
`studio_source=audio`, `raw` = кодек-строка. Ожидание: kinozal recall студий (сейчас 0.93 по
парам, но в gold этих студий просто нет — метрика тоже сдвинется, сравнивать на одном gold).
Dry-run: число строк по трекерам, выборка 50 глазами.
2. **Разбор списков студий в одной «Перевод:»** (~2 100 студий nnm+rt): одна дорожка на студию с
общим типом; «Дублированный (A, B), Многоголосый (C, D)» — тип у каждой группы свой.
3. **Доразметка ~980 релизов без меток с типом озвучки** (nnm 417, rt 562): найти, почему сборщик
их пропустил (вероятно, формат «Перевод 1:» в nnm и «Озвучка: … от X» в аниме), поправить
парсер, прогнать relabel.
4. **Форматы кодек-строк в правилах**: расширить `RE_AUDIO_TRACK` / `RE_AUDIO_PREFIX` на
«Аудио pic RUS», «Аудио N Rus:», «Аудио #N Русский:», «Звук N:», «• №N:», «Russian (…)»
без префикса; одиночное «Аудио:» с типом/студией; `RE_CODEC_TRACK` — студия из словаря без
тип-слова; кириллические «АVO/МVO». Тесты на каждый формат. Даёт метки сегментатору
(переобучение сегментатора — только если e2e лучше текущих весов) и покрытие фолбэка.
5. **Чистка мусорных студий** — после решений ниже; правила в prepare_data (мусор) и в сборщике
(язык-префикс, тип-слова, каналы).
## Решения за вами (не исправления)
* **Лицензия / Blu-ray CEE / iTunes / Netflix** (~1 070 строк): студия или источник дорожки?
Сейчас модель учится выдавать их как студию.
* **«X по заказу Y»** (~320): хранить целиком, только X, или X + заказчик отдельным полем?
* **«Русский, - AlexFilm»** (85): срезать язык-префикс — почти наверняка да, но это меняет метки.
* **Неру дорожки** (казахский Qazaqstan, «+ Английский + Комментарии», ~36): выкидывать из меток
или учить lang-голову (KAZ сейчас < 20 примеров и не учится).
+892
View File
@@ -0,0 +1,892 @@
# Аудит дорожек в raw-страницах (цифры)
Сгенерировано `tools/audit_tracks.py`; выводы и план — `reports/tracks_audit.md`.
## kinozal
**releases**: всего — 10541; с метками — 9932; с метками, есть недостающая студия — 1690; с метками без студий, студия есть в кодек-строке — 1129; без меток — 609
**строки**: audio — 18297; trans — 15072
**кодек-строки**: мимо RE_AUDIO_TRACK | мимо codec_segments — 11480; RE_AUDIO_TRACK | codec_segments — 5255; RE_AUDIO_TRACK | мимо codec_segments — 1378; мимо RE_AUDIO_TRACK | codec_segments — 184
**форматы мимо: seg codec**: аудио: — 10395; аудио pic rus: — 374; аудио #N русский: — 175; аудио N rus: — 62; аудио x rus: — 61; звук N: — 61; аудио #N украинский: — 41; аудио rus #N: — 33; аудио N · rus: — 20; аудио N#: — 19; аудио #N (rus): — 19; russian: — 17; • аудио №N: — 14; звук#N: — 13; русский (acN, N ch, N — 12; русская: — 11; • аудио #N русский: — 9; аудио-N: — 8; аудио #N russian: — 7; русский N (асN, N ch, — 6; аудио#N rus: — 6; • аудио N: — 5; аудио pic ukr: — 5; audio N (rus): — 5; аудио #N - (acN, N — 5
**недостающие студии: вид строки**: audio — 3240; trans — 496
**недостающие студии: топ**: HDrezka — 362; TVShows — 339; LostFilm — 211; Red Head Sound — 164; LeDoyen — 117; Jaskier — 101; MovieDalen — 89; Мосфильм-Мастер — 80; NewComers — 76; NewStudio — 71; Пифагор — 67; WinMedia — 64; Movie Dubbing — 63; Le-Production — 63; Postmodern — 61; Невафильм — 53; Кубик в Кубе — 48; Flarrow Films — 48; ViruseProject — 45; Дубляжная — 41; DniproFilm — 40; Синема УС — 35; SDI Media — 33; AlexFilm — 31; Custom — 31
**студии меток**: всего — 8462; нет в тексте/заголовке — 1
**форматы мимо: seg**: аудио: — 167; аудио : — 13; audio: — 4
**без меток**: обычный, есть строки дорожек — 594; обычный, нет строк дорожек — 15
**форматы мимо: codec**: аудио rus: — 662; аудио N: — 494; аудио #N: — 153; аудио ukr: — 26; аудиоN: — 9; аудио N. русский: — 7; аудио#N: — 6; аудио N.N: — 6; аудиодорожка N: — 6; аудио №N: — 3; аудио N. украински — 3; audio N: — 2; аудио N. украинский: — 1
**студии меток: нет в тексте, studio_source**: audio — 1
**мусор в studio меток**: источник: лицензия/Blu-ray/iTunes/Netflix — 245; «X по заказу Y» — 131; язык-префикс «Русский, - X» — 72; тип-слово — 45; каналы/кодек — 7; язык вместо студии — 7; субтитры — 7; неру дорожка — 4
<details><summary>примеры fmt:codec (1378)</summary>
- 2070202 · Аудио Rus: pic AC3, 48 kHz, 2 ch, 192 kbps \|HDRezka\|
- 1914764 · Аудио 06: Русский, AC-3, 48 kHz, 2 ch, 192 kb/s - Д. Пучков "Goblin" (2-5 сезоны)
- 2124461 · Аудио 7: Russian E-AC3 / 5.1 / 48 kHz / 1024 kbps \|Ю. Живов\| поздний
- 2158278 · Аудио Rus: AC3, 48 kHz, 2 ch, 192 kbps \|HDRezka\|
- 1630832 · Аудио #7: DTS-HD Master Audio Russian 7.1 / 48 kHz / 4232 kbps / 24-bit (DTS Core: 5.1 / 48 kHz / 1509 kbps / 24-bit) \| Юрий Немахов
- 1769927 · Аудио 3: AC3, 48 kHz, 2 ch, 192 kbps - Rus (HDRezka Studio)
- 2149720 · Аудио Rus: AC3, 48,0 kHz, 2 ch, 192 Кбит/сек \| AlexFilm
- 1795296 · Аудио #01: 48 kHz, DTS-HD MA, 5.1, 2239 Kbps \| Лицензия \| RUS \|
- 2105709 · Аудио Rus: AC3, 48,0 kHz, 6 ch, 384 Кбит/сек \|TVShows\|
- 2153009 · Аудио Rus: AC3, 48,0 kHz, 2 ch, 192 Кбит/сек \| Viju
- 1716051 · Аудио 4: Russian DTS / 5.1 / 48 kHz / 1509 kbps / 24 bits \|Ю. Живов\|
- 2145084 · Аудио Rus: AC3, 48,0 kHz, 2 ch, 192 Кбит/сек \| Red Head Sound
- 2005766 · Аудио 1: AC3, 2 ch, 192-384 kbps, русский (ПM HDRezka) (18+) без цензуры (1-10 серии)
- 1581690 · Аудио 4: Russian DTS-HD Master Audio / 7.1 / 48 kHz / 4829 kbps / 24-bit (DTS Core: 5.1 / 48 kHz / 1509 kbps / 24-bit) \|Ю. Сербин\|
- 1780718 · Аудио 6: Russian AC3 / 2.0 / 48 kHz / 192 kbps \|Д. Пучков aka Goblin\|
</details>
<details><summary>примеры fmt:seg (184)</summary>
- 2017336 · Аудио: E-AC3, 6 ch, 256 Kbps - Украинский Дублированный (Original)
- 2009954 · Аудио: AC3, 6 ch, 448 kbps \| русский (VO Александр Дасевич по переводу Groby)
- 1521470 · Аудио: Русский 48 kHz, (AC3), 6ch, 640 kbps [Профессиональный (полное дублирование)]
- 1991298 · Аудио: Е-AC3, 6 ch, 384 Kbps - Русский (DUB, LeDoyen)
- 1579711 · Аудио: Russian: 48 kHz, АС3, 2 ch, 192 kbps / 16-bit_\|Дубляж_[ТВ3]_\|_
- 2129647 · Аудио: Russian: AC3, 448 Kb/s (6 ch) \| DUB \| Марафон
- 2009954 · Аудио: AC3, 6 ch, 448 kbps \| русский (VO Вячеслав Замез по переводу Groby)
- 1481240 · Аудио: Russian / DTS Audio / 5.1 / 48 kHz / 768 kbps \| Дубляж, Blu-ray CEE
- 2138814 · Аудио: AC3, 48 kHz, 6 ch, ~384.00 kbps avg \| русский \| DUB \| WinMedia
- 1776297 · Аудио: 48 kHz, AC3, 5.1, 448 Kbps \| MVO \| RUS \|_Профессиональный (многоголосый, закадровый) \| HDRezka Studio_присутствует ненормативная лексика
- 1982638 · Аудио: Е-AC3, 6 ch, 640 Kbps Украинский профессиональный дублированный \| Postmodern (Netflix)- (8 из 8)
- 2158258 · Аудио: АC3, 48 kHz, 6 ch, ~640.00 kbps avg \| русский \| DVO \| ViruseProject \| 18+
- 2158258 · Аудио: АC3, 48 kHz, 2 ch, ~192.00 kbps avg \| русский \| DUB \| HDRezka Studio \| 18+
- 2149774 · Аудио: Dolby AC3 48000Hz 2ch 192kbps [rus DVO SDI Media]
- 2145264 · Аудио: АC3, 48 kHz, 2 ch, ~192.00 kbps avg \| русский \| DUB \| HDRezka Studio \| 18+
</details>
<details><summary>примеры fmt:seg codec (3000)</summary>
- 1704679 · Аудио: Русский, (AC3, 6 ch, 384 Кбит/с)
- 1504140 · Аудио: Русский (AC3, 6 ch, 640 Кбит/с), английский (DTS-HD MA, 8 ch, 2538 Кбит/с)
- 1046106 · Русский 2 (АС3, 6 ch, 448 Кбит/с) Профессиональный многоголосый [C.Р.И.]
- 1331558 · Аудио: Русский, английский (AC3, 6 ch, 448 Кбит/с)
- 1044555 · Аудио: Русский, английский (AC3, 2 ch, 192 Кбит/с), (AC3, 6 ch, 384 Кбит/с)
- 1658245 · Аудио: Русский (AAC, 2 ch, 192 Кбит/с)
- 1150580 · Аудио: Русский (АС3, 2 ch, 224 Кбит/с), английский (АС3, 2 ch, 192 Кбит/с)
- 1431453 · Аудио: Русский (AC3, 6 ch, 384 Кбит/с) английский (AC3, 2 ch, 192 Кбит/с)
- 1519187 · Аудио: Русский (AC3, 6 ch, 448 Кбит/с), английский (DTS-HD MA, 8 ch, 4090 Кбит/с)
- 1168919 · Аудио: Русский (AC3, 6 ch, 448 Кбит/с)
- 1634361 · Аудио: Русский (АС3, 6 ch, 384 Кбит/с), английский (английский, DTS-HD MA, 6 ch,1509 Кбит/с)
- 1122316 · Аудио: Русский, украинский, английский (АС3, 6 ch, 384 Кбит/с)
- 1681156 · Аудио: Русский (AC3, 2 ch, 192 Кбит/с), японский (AC3, 6 ch, 448 Кбит/с)
- 1449948 · Аудио: Русский (AC3, 6 ch, 448 Кбит/с), (DTS, 6 ch, 755 Кбит/с), украинский (AC3, 6 ch, 448 Кбит/с), английский (DTS, 6 ch, 1509 Кбит/с)
- 1663939 · Аудио: Русский (AC3, 2 ch, 192 Кбит/с), немецкий (AAC, 2 ch, 96 Кбит/с)
</details>
<details><summary>примеры ghost (1)</summary>
- 2010302 · TVShows · Russian E-AC3 / 5.1 / 48 kHz / 256 kbps \|MVO, TV Shows\|
</details>
<details><summary>примеры miss:audio (3240)</summary>
- 1899316 · Пифагор · Аудио #1 Русский: AC3, 48.0 kHz, 5.1 ch, 384 Kbps [Дубляж, Пифагор]
- 2149078 · LeDoyen · Аудио: AC3, 6 ch, 448 Kbps - Украинский (DUB, LeDoyen)
- 2147682 · Paragraph Media · Аудио #01: 48 kHz, AC3, 5.1, 384 Kbps \| Дублированный \| Paragraph Media \| RUS \|
- 2152575 · HTB · Аудио 2: AC3, 2 ch, 192 Kbps - русский (DVO HTB)
- 1981418 · HDrezka · Аудио RUS: АС3, 6 ch, 48 kHz, 448 kbps \|HDrezka Studio\|
- 2063569 · Jaskier · Аудио pic RUS: AC3, 6 ch, 48 kHz, 384 kbps \|Jaskier\|
- 1882053 · Ruscico · Аудио#1: Russian: 48 kHz, АС3, 2 ch, ~192.00 kbps avg \| Профессиональный (многоголосый, закадровый) [Сонотек, Ruscico]
- 2144862 · TVShows · Аудио 6: AC3, 6 ch, 384 Kbps - Русский (MVO, TVShows)
- 2157571 · Movie Dubbing · Аудио 1: 48.0 kHz, AC3, 6 ch, 384 Kbps - Русский (DUB, Movie Dubbing)
- 1811439 · AppleTV+ · Аудио #01: 48 kHz, E-AC3, 5.1, 768 Kbps \| Дублированный \| AppleTV+ (Пифагор) \| RUS \|
- 2146921 · Postmodern · Аудио 2: E-AC3, 6 ch, 640 Кбит/с - Украинский дубляж (Postmodern)
- 1810512 · Ю. Немахов · Аудио 10: Русский, АС3, 6 ch, 640 Кбит/с Одноголосый закадровый, Ю. Немахов
- 2151270 · СВЕТЛА · Аудио 1: AC3, 2 ch, 192 Kbps - русский (MVO Светла)
- 1984794 · DniproFilm · Аудио10: AC-3 • 5.1 • 384 kbps • MVO \| DniproFilm • UKR
- 2136518 · Red Head Sound · Аудио 6: AAC, 2 ch, 192 Kbps - Русский (MVO, Red Head Sound)
</details>
<details><summary>примеры miss:trans (496)</summary>
- 2138973 · AniStar · Озвучка: Любительский двухголосый \| AniStar (Mutsuko Air & Horie)
- 2094579 · Condor Films · Перевод: Дублированный \| Condor Films Studio (с цензурой)
- 2014244 · Кураж-Бамбей · Перевод #1: Профессиональный (одноголосый) (Кураж-Бамбей)
- 2153210 · SDI Media · Перевод #1. Профессиональный (многоголосый, закадровый) - SDI Media. Запись звука с т/к: василисса. Огромное спасибо soso4eg за релиз с переводом.
- 1937992 · HamsterStudio · Перевод: 1001cinema / HamsterStudio, присутствует ненормативная лексика.
- 2034410 · TVShows · Перевод: Профессиональный (многоголосый закадровый) (TVShows) (Flex)
- 2124828 · VSI Moscow · Перевод 1: Профессиональный (дублированный), русский (VSI Moscow)
- 2158761 · С. Визгунов · Перевод: Многоголосый профессиональный (РТР), авторский (С. Визгунов, В. Горчаков), одноголосый любительский (Сука-Падла)
- 2029926 · Невафильм · Перевод: Профессиональный (полное дублирование) [Невафильм]
- 1992132 · Инис · Перевод #2: Профессиональный двухголосый [Инис]
- 1914764 · HamsterStudio · Перевод 7: Русский, Профессиональный многоголосый закадровый - HamsterStudio
- 1860544 · Синема УС · Перевод: Любительский (многоголосый) ["Синема УС"]
- 2154508 · TVShows · Перевод: Дублированный (Red Head Sound), Многоголосый (NewComers, TVShows, LostFilm, HDRezka, Двухголосый (Кубик в кубе)
- 1805099 · Синема УС · Перевод: Любительский (двухголосый) ["Синема УС"]
- 2142169 · Megogo · Перевод №1 (русский): Дублированный \| Megogo
</details>
<details><summary>примеры noise:«X по заказу Y» (131)</summary>
- kz-2105683 · по заказу Амедиа · Перевод: Профессиональный многоголосый (Кравец-Рекордз), по заказу Амедиа (2025 год)
- kz-1796214 · Студия Кириллица по заказу Amedia · Перевод: Профессиональный многоголосый - Студия Кириллица по заказу Amedia.
- kz-2129097 · Дубляжная по заказу DEEP · Перевод: Дублированный (Дубляжная по заказу DEEP)
- kz-2158272 · AniLibria по заказу IVI · Перевод: Любительский (многоголосый закадровый) \| AniLibria по заказу IVI
- kz-2136502 · Force Media по заказу ИВИ · Перевод: Профессиональный (многоголосый закадровый) \| Force Media по заказу ИВИ \| Официальный
- kz-2150544 · Syncmer по заказу HBO · Перевод 1: Профессиональный (двухголосый закадровый) — Syncmer по заказу HBO
- kz-2127699 · Айгуль Шамсутдинова по заказу Digi Media · Озвучивание: Профессиональное (одноголосое) \| Айгуль Шамсутдинова по заказу Digi Media
- kz-1772489 · Greb & Creative по заказу Amedia · Перевод 1: профессиональный (многоголосый) Greb & Creative по заказу Amedia
- kz-1903750 · по заказу канала 2х2 · Перевод: Авторский (одноголосый) [Дмитрий Пучков] по заказу канала 2х2
- kz-2145848 · AniStar по заказу DEEP · Перевод: Любительский (многоголосый закадровый) \| AniStar по заказу DEEP \| Официальный
- kz-2006086 · студия Sigil по заказу Nickelodeon · Перевод: Дублированный \| студия «Sigil» по заказу Nickelodeon
- kz-1998690 · Novamedia по заказу Amedia · Перевод: Профессиональный (дублированный) Novamedia по заказу Amedia
- kz-2158114 · Дубляжная по заказу DEEP · Перевод: Дублированный \| Дубляжная по заказу DEEP
- kz-1475970 · По Заказу Дисней Россия · Озвучивание: Дублированное "Невафильм" (По Заказу Дисней Россия)
- kz-1791492 · Greb&CGC по заказу Amedia · Перевод: Профессиональный (многоголосый закадровый) - Greb&CGC по заказу Amedia.
</details>
<details><summary>примеры noise:источник: лицензия/Blu-ray/iTunes/Netflix (245)</summary>
- kz-2046269 · Лицензия · Перевод: Профессиональный многоголосый (Лицензия)
- kz-1868122 · Blu-ray RUS Лицензия · Перевод: Профессиональный (полное дублирование) \| Blu-ray RUS \| Лицензия
- kz-1876839 · iTunes · Перевод 1: Профессиональный (дублированный) iTunes
- kz-2083422 · лицензия BD · Перевод: Профессиональный (многоголосый закадровый) лицензия BD
- kz-2140559 · iTunes · Перевод 1: Профессиональный (дублированный) \| iTunes
- kz-2087781 · VSI Moscow по заказу Netflix · Перевод 1: Профессиональный (дублированный) VSI Moscow по заказу Netflix
- kz-1786227 · Пифагор по заказу Netflix · Перевод: Профессиональный (дублированный) Пифагор по заказу Netflix
- kz-1797177 · iTunes · Озвучивание: Дублированное \| iTunes
- kz-1990227 · Blu-ray CEE / Мост Видео · Перевод: Дублированный (Blu-ray CEE / Мост Видео)
- kz-1799697 · iTunes · Озвучивание: Дублированное \| iTunes
- kz-1901335 · iTunes · Озвучивание: Дублированное \| iTunes
- kz-1851685 · UHD Blu-Ray · Russian AC3 / 5.1 / 48 kHz / 640 kbps \|Dub, UHD Blu-Ray\|
- kz-1882179 · Akimbo Production iTunes · Перевод: Профессиональный (полное дублирование) \| Akimbo Production \| iTunes
- kz-2086101 · Пифагор по заказу Netflix · Перевод 1: Профессиональный (дублированный) Пифагор по заказу Netflix
- kz-1847720 · iTunes · Озвучивание: Дублированное \| iTunes
</details>
<details><summary>примеры noise:каналы/кодек (7)</summary>
- kz-1039053 · AC3, 6 ch, 448 Кбит/с · Перевод: Дублированный (AC3, 6 ch, 448 Кбит/с)
- kz-2156869 · AC3, 6 ch, 384 Кбит/с · Перевод 2: (AC3, 6 ch, 384 Кбит/с), Профессиональный многоголосый (TVShows)
- kz-2029179 · AC3, 6 ch, 448 Кбит/с · Озвучивание #1: Дублированное (неофициальное) [TVShows] - AC3, 6 ch, 448 Кбит/с
- kz-2029179 · AC3, 6 ch, 384 Кбит/с · Озвучивание #2: Профессиональное многоголосое [Jaskier \| 18 +] - AC3, 6 ch, 384 Кбит/с
- kz-2137517 · 192-224 kb/s, CBR VSI Moscow · Перевод: AC-3, 48.0 kHz, 2 ch (Stereo), 192-224 kb/s, CBR (Russian) \| Профессиональный (дублированный) \| VSI Moscow
- kz-2156869 · AC3, 2 ch, 192 Кбит/с · Перевод 1: (AC3, 2 ch, 192 Кбит/с), Профессиональный многоголосый (HDrezka Studio)
- kz-1824511 · DTS · Russian DTS-HD Master Audio / 5.1 / 48 kHz / 4265 kbps / 24-bit (DTS Core: 5.1 / 48 kHz / 1509 kbps / 24-bit) \|Дубляж, D
</details>
<details><summary>примеры noise:неру дорожка (4)</summary>
- kz-2124460 · Телеканал Qazaqstan · Kazakh AC3 / 1.0 / 48 kHz / 192 kbps \|Dub, Телеканал "Qazaqstan"\|
- kz-2124461 · Телеканал Qazaqstan · Kazakh AC3 / 1.0 / 48 kHz / 192 kbps \|Dub, Телеканал "Qazaqstan"\|
- kz-2035938 · Qazaqstan · Перевод 7: Профессиональный (дублированный) Qazaqstan
- kz-1081469 · Tannikka & 3opuH + Оригинальная английская дорожка · Перевод: Любительский (двухголосый, закадровый) Tannikka & 3opuH + Оригинальная английская дорожка
</details>
<details><summary>примеры noise:субтитры (7)</summary>
- kz-2070931 · Light Breeze по субтитрам tonyvika · Перевод: Любительский (двухголосый закадровый) Light Breeze по субтитрам tonyvika
- kz-2029956 · W³: voices по субтитрам ASIAN DRAGONS · Перевод 1: Любительский (двухголосый закадровый) W³: voices по субтитрам ASIAN DRAGONS
- kz-2024527 · Unicorn по субтитрам ФСГ Азалии · Перевод 1: Любительский (одноголосый закадровый) Unicorn по субтитрам ФСГ Азалии
- kz-1459417 · Расширенные места без русского озвучивания. Переведены русскими форсированными субтитрами. Включены по умолчанию · Озвучивание: Дублированное [iTunes] (Расширенные места без русского озвучивания. Переведены русскими форсированными субт
- kz-1633732 · О. Воротилин по субтитрам Zinovev · Перевод: Одноголосый закадровый О. Воротилин по субтитрам Zinov'ev
- kz-2058019 · den904 по субтитрам Катерины Яцук · Перевод: Одноголосый закадровый \| den904 по субтитрам Катерины Яцук
- kz-2156009 · Novamedia + субтитры на вырезанные цензурой места · Перевод: Профессиональный (многоголосый закадровый) Novamedia + субтитры на вырезанные цензурой места
</details>
<details><summary>примеры noise:тип-слово (45)</summary>
- kz-2097155 · полное дублирование · Перевод: Профессиональный (полное дублирование)
- kz-1577270 · полное дублирование · Перевод: Профессиональный (полное дублирование) [Лицензия]
- kz-2046404 · полное дублирование · Перевод: Профессиональный (полное дублирование)
- kz-1373383 · полное дублирование · Перевод: Профессиональный (полное дублирование)
- kz-1782714 · полное дублирование · Перевод: Профессиональный (полное дублирование) [iTunes]
- kz-1823534 · полное дублирование · Перевод: Профессиональный (полное дублирование) [iTunes]
- kz-1577726 · Дубляж · Перевод: Дубляж [Blu-ray RUS]
- kz-1806566 · полное дублирование · Перевод: Профессиональный (полное дублирование) [Пифагор]
- kz-2014726 · полное дублирование · Перевод: Профессиональный (полное дублирование) \| Официальный
- kz-2093319 · полное дублирование · Перевод: Профессиональный (полное дублирование)
- kz-2127996 · полное дублирование · Перевод: Профессиональный (полное дублирование) \| Официальный
- kz-1528391 · полное дублирование · Перевод: Профессиональный (полное дублирование) [Лицензия Blu-ray CEE]
- kz-1598461 · полное дублирование · Перевод: Профессиональный (полное дублирование)
- kz-2029926 · полное дублирование · Перевод: Профессиональный (полное дублирование) [Невафильм]
- kz-1748299 · полное дублирование · Перевод: Профессиональный (полное дублирование)
</details>
<details><summary>примеры noise:язык вместо студии (7)</summary>
- kz-2036420 · Русский · АС-3, 6 ch, 384 kbps \| Русский, MVO \| TVShows \|
- kz-1650921 · Английские · Перевод: Английские
- kz-1261226 · украинский · украинский профессиональный многоголосый [1+1
- kz-1872823 · Английский · Перевод 3: Английский дублированный
- kz-2124828 · русский · Перевод 1: Профессиональный (дублированный), русский (VSI Moscow)
- kz-2124828 · украинский · Перевод 2: Профессиональный (дублированный), украинский (Le Doyen)
- kz-1107366 · английский · Перевод: профессиональный многоголосый, R5 + оригинал (английский)
</details>
<details><summary>примеры noise:язык-префикс «Русский, - X» (72)</summary>
- kz-2136993 · Русский, - AlexFilm · Перевод 1: Русский, профессиональный многоголосый закадровый - AlexFilm
- kz-2136993 · Русский, - AlexFilm · Перевод 1: Русский, профессиональный многоголосый закадровый - AlexFilm
- kz-1986288 · Русский, - The Kitchen Russia · Перевод 2: Русский, дублированный - The Kitchen Russia
- kz-1986288 · Русский, - The Kitchen Russia · Перевод 2: Русский, дублированный - The Kitchen Russia
- kz-1914764 · Русский, - HDRezka · Перевод 5: Русский, Профессиональный многоголосый закадровый - HDRezka Studio
- kz-1986288 · Русский, - Good People · Перевод 1: Русский, профессиональный многоголосый закадровый - Good People
- kz-2136993 · Украинский, - Цікава Ідея · Перевод 3: Украинский, профессиональный многоголосый закадровый - Цікава Ідея
- kz-2136993 · Русский, - AlexFilm · Перевод 1: Русский, профессиональный многоголосый закадровый - AlexFilm
- kz-2136993 · Русский, - TVShows · Перевод 2: Русский, профессиональный многоголосый закадровый - TVShows
- kz-1202498 · Русские, английские · Перевод: Русские, английские
- kz-1996184 · Русский, - Red Head Sound · Перевод 1: Русский, дублированный - Red Head Sound
- kz-2136993 · Украинский, - Цікава Ідея · Перевод 3: Украинский, профессиональный многоголосый закадровый - Цікава Ідея
- kz-2081511 · Русские, английские · Перевод: Русские, английские
- kz-1914764 · Русский, - IdeaFilm · Перевод 6: Русский, Профессиональный многоголосый закадровый - IdeaFilm
- kz-1986288 · Русский, - TVShows · Перевод 3: Русский, профессиональный многоголосый закадровый - TVShows
</details>
<details><summary>примеры unlab (594)</summary>
- 2014296 · Гардемарины 1787 (Дилогия) / 2020-2023 / РУ, СТ / WEB-DLRip · Аудио: Русский (MP3, 2 ch, 192 Кбит/с)
- 1018580 · Орда / 2012 / РУ / BDRip (AVC) · Аудио: Русский (AC3, 6 ch, 448 Кбит/с)
- 1604585 · Большой (Расширенная ТВ-версия) (1-2 серии из 2) / 2016 / РУ / HDTV (1080i) · Аудио: Русский (MP2, 2 ch, 192 Кбит/с)
- 2148998 · Где моё кольцо? (Операция "Гүлдер") / 2020 / РУ / WEB-DL (1080p) · Аудио: Русский (AAC, 2 ch, 192 Кбит/с)
- 287608 · На краю стою (Гульфара) / 2008 / РУ / DVD-9 · Аудио: Русский (Dolby AC3, 6 ch), (DTS, 5 ch)
- 2128892 · Без предметов (Без объектов) / Sans objets / 2020 / БП / WEB-DL (1080p) · Перевод: Не требуется
- 861037 · Тихая семейная жизнь (2 серии из 2) / 2008 / РУ / DVD-9 · Аудио: Русский (Dolby AC3, 6 ch)
- 2088285 · Красный карандаш (Красный карандашик) / Sartulis / 2014 / БП / WEBRip · Перевод: Не требуется
- 1508276 · Взломать блогеров / 2016 / РУ / WEB-DL (720p) · Аудио: Русский (AC3, 6 ch, 384 Кбит/с)
- 2158475 · Третий удар (Южный узел) / 1948 / РУ, СТ / DVDRip · Аудио: Русский (AC3, 2 ch, 256 Кбит/с)
- 1206915 · Рихард Вагнер - Тангейзер (Большой театр Лисео) / Richard Wagner - Tannhauser (G · Перевод: Не требуется
- 2153202 · Тимур и его команда / 1940 / РУ, СТ / IPTV · Аудио: Русский (AAC, 2 ch, 125 Кбит/с)
- 715445 · Адмиралъ / 2008 / РУ / BDRip · Аудио: Русский (AC3, 6 ch, 448 Кбит/с)
- 2141802 · Марк и Галя (1 сезон: 1-5 серии из 5) / 2023-2024 / БП / WEB-DL (1080p) · Перевод: Не требуется
- 2142544 · Эпические приключения Морфа (1 сезон: 1-15 серии из 15) / The Epic Adventures of · Перевод: Не требуется
</details>
## nnm
**releases**: всего — 13677; с метками — 10085; без меток — 3592; с метками, есть недостающая студия — 1295; с метками без студий, студия есть в кодек-строке — 653
**строки**: audio — 15151; trans — 12541
**без меток**: обычный, есть строки дорожек — 1987; обычный, нет строк дорожек — 1605
**кодек-строки**: RE_AUDIO_TRACK | codec_segments — 10077; мимо RE_AUDIO_TRACK | мимо codec_segments — 2472; RE_AUDIO_TRACK | мимо codec_segments — 2224; мимо RE_AUDIO_TRACK | codec_segments — 378
**недостающие студии: вид строки**: trans — 1029; audio — 1003
**недостающие студии: топ**: LeDoyen — 69; Postmodern — 57; НТВ+ — 54; HDrezka — 53; 1+1 — 49; Так Треба Продакшн — 43; TVShows — 39; Советский дубляж — 37; ОРТ — 35; НТВ — 35; заКАДРЫ — 32; А. Михалёв — 29; А.Михалев — 28; СВ-Дубль — 28; Михалёв — 25; Ю.Живов — 24; DniproFilm — 24; LostFilm — 23; Юрий Живов — 22; Пифагор — 22; А.Гаврилов — 21; Андрей Гаврилов — 20; Киномания — 20; Le Doyen — 20; RHS — 20
**форматы мимо: seg codec**: russian (dolby acN, N — 1483; russian, acN, N/N (l, — 308; аудио: — 199; аудио (rus): — 68; ukrainian (dolby acN, N — 58; audio: — 53; russian (dts, N ch) — 35; russian acN, N khz, — 31; аудио N rus: — 25; аудио #N rus: — 21; русский: — 14; аудио »N: — 10; аудио#№N: — 9; аудио #N (rus): — 9; аудио (русский): — 9; аудио дорожка N: — 8; аудио rus(int) N: — 6; russian, dts, N/N (l, — 5; звук #N: — 5; звуковая дорожка N: — 5; аудио #N (русс): — 5; rus: — 4; ukrainian, acN, N/N (l, — 4; audio(N): — 4; аудиокодек: — 3
**форматы мимо: codec**: аудио N: — 1582; аудио #N: — 316; аудио#N: — 132; аудио rus: — 74; аудио №N: — 40; аудиоN: — 30; audio #N: — 20; аудиодорожка N: — 13; аудио # N: — 4; аудио N : — 4; audio N: — 4; аудио№N: — 2; аудио #N. acN, N ch — 2; аудио rus : — 1
**студии меток**: всего — 10003; нет в тексте/заголовке — 22
**форматы мимо: seg**: аудио: — 370; аудио N: — 8
**студии меток: нет в тексте, studio_source**: release — 20; tech — 2
**мусор в studio меток**: источник: лицензия/Blu-ray/iTunes/Netflix — 590; каналы/кодек — 89; «X по заказу Y» — 67; тип-слово — 41; язык вместо студии — 26; субтитры — 7; неру дорожка — 6
<details><summary>примеры fmt:codec (2224)</summary>
- 649043 · Аудио #3: MP3, 44.1 KHz, 2 ch, 128 Kbps, CBR (Русский)
- 626763 · Аудио #1: Русский (MP3, 2ch, 48 kHz, 128 kb/s)
- 1422226 · Аудио#1: AC3, 2 ch, 192 Kbps \| Русский
- 1728373 · Аудио #1: MP3, 2 ch, 128 Kbps (Русский) 18+ (Ненормативная лексика!)
- 655684 · Аудио 1: Dolby AC3, 48000Hz, 6ch, 384Kbps (русский)
- 1272634 · Аудио #3: AC3, 6 ch, 384 kbps - русский (АVO, Ю. Живов)
- 1735316 · Аудио 1: AC3, 2 ch, 192 Kbps, Русский, (Movie Dubbing / Bravo Records Georgia)
- 1292412 · Аудио #1: AC3, 2 ch, 192 Kbps \| Русский HDRezka
- 1771613 · Аудио 4: Е-AC3, 8 ch, 1536 Kbps, русский (АVO - В. Дохалов)
- 892406 · Аудио #1: AC3, 48.0 kHz, 2 ch, 192 kbps (Русский)
- 1608297 · Аудио 2: AAC, 48.0 KHz, 192 kbps, 2 ch (Русский, AniLibria.TV)
- 1439239 · Аудио1: AC3, 6 ch, 640 Kbps - Русский (LostFilm)
- 220287 · Аудио 1: (rus) 48 kHz, AC3 Dolby Digital, 1/0 (C) ch, 192 kbps avg
- 1222018 · Аудио #1: AC-3, 2 ch, 192 Kbps \| Русский
- 1418749 · Аудио#1: AC3, 2 ch, 192 Kbps \| Русский
</details>
<details><summary>примеры fmt:seg (378)</summary>
- 1009049 · Аудио: Русский: FLAC, 2 ch, 652 kbps, 48,0 КГц, 16 bit MVO Mediainfo:
- 482371 · Аудио: (Dolby AC3, 2 ch) ~192 kb/s \|Русский,одноголосый закадровый\|
- 1197476 · Аудио: 1 AC3, 2 ch, 192 kbps - Русский, многоголосый (ТК "Домашний")
- 453252 · Аудио: Dolby Digital Audio 5.1 / Русский / 640 Кбит/сек / 48 kHz (DUB)
- 1152863 · Аудио: AC3, 2 ch, 192 Kbps - русский, многоголосый закадровый Mediainfo:
- 1596114 · Аудио: AC3, 2 ch, 192 kbps - Русский (Дублированный)
- 553342 · Аудио: AC3, 6 ch, 384 kb/s - Русский, многоголосый
- 1054025 · Аудио: AC3, 6 ch, 640 kbps - Русский, Дубляж
- 1121293 · Аудио: 1 AC3, 6 ch, 384 kbps - Русский дубляж
- 1339316 · Аудио: MP3, 2 ch, 192 Kbps - Русский (MVO) MediaInfo:
- 1651071 · Аудио 5: TrueHD + Atmos, 8 ch, ~3661 Kbps - Українська (MVO, HDRezka Studio)
- 1196431 · Аудио: AC3, 6 ch, 384 kbps - Украинский, Дубляж
- 603650 · Аудио: (Dolby AC3, 2 ch) ~192 kb/s \|Русский, многоголосый закадровый\|
- 1432165 · Аудио: AC3, 6 ch, 384 Kbps - Русский, дубляж (iTunes) MediaInfo:
- 1564969 · Аудио: AC3, 2 ch, 192 Kbps - Русский (MVO, TVShows) MediaInfo:
</details>
<details><summary>примеры fmt:seg codec (2472)</summary>
- 961845 · Аудио #1 Rus: AC3, 2 канала, 384 Кбит/сек, CBR - многоголосый закадровый
- 464967 · Russian (Dolby AC3, 2 ch)
- 624704 · Russian (Dolby AC3, 6 ch)
- 1655275 · Russian, AC3, 2/0 (L,R) ch, 192 kbps, Delay 0 mSec
- 1617406 · Russian (Dolby AC3, 2 ch)
- 649787 · Russian (Dolby AC3, 2 ch)
- 597243 · Russian (Dolby AC3, 6 ch)
- 635903 · Russian (Dolby AC3, 2 ch)
- 643340 · Russian (Dolby AC3, 2 ch)
- 419146 · Russian (Dolby AC3, 2 ch)
- 1457385 · Russian (Dolby AC3, 2 ch)
- 1745806 · Аудио: AAC LC, 2 ch, 48.0 kHz, 192 kbps \| Звук: Русский
- 1514814 · Russian (Dolby AC3, 2 ch)
- 449635 · Russian, AC3, 2/0 (L,R) ch, 192 kbps, Delay 0 mSec
- 338450 · Аудио дорожка 1: 48,0 КГц, AC3, 192 Кбит/сек, 2 канала, русский, полное дублирование - СССР
</details>
<details><summary>примеры ghost (22)</summary>
- 1887058 · НТВ СТС · Аудио 4: AC3, 2 ch, 192 Kbps - русский (MVO НТВ \| СТС)
- 1701274 · ТК ОРТ · Аудио 07: AC3, 2 ch, 192 Kbps - Русский (MVO, ТК "ОРТ")
- 1887072 · НТВ СТС · Аудио 4: AC3, 2 ch, 192 Kbps - русский (MVO НТВ \| СТС)
- 1700762 · ТК СТС · Аудио 03: AC3, 2 ch, 192 Kbps - Русский (MVO, ТК "СТС")
- 1720458 · ТК НТВ+ · Аудио 5: AC3, 2 ch, 192 Kbps - Русский (MVO, ТК "НТВ+")
- 1671752 · TVShows · Аудио 7: AС3, 6 ch, 640 kbps - Русский, MVO (TV Shows)
- 1723189 · ТК СТС · Аудио 06: AC3, 2 ch, 192 Kbps - Русский (MVO, ТК "СТС")
- 1700762 · ТК НТВ · Аудио 06: AC3, 2 ch, 192 Kbps - Русский (MVO, ТК "НТВ")
- 1883291 · НТВ ТНТ Ren-TV · Аудио 2: AC3, 2 ch, 192 Kbps \| Русский, MVO (НТВ \| ТНТ \| Ren-TV)
- 1721934 · ТК ОРТ · Аудио 07: AC3, 2 ch, 192 Kbps - Русский (MVO, ТК "ОРТ")
- 1671752 · TVShows · Аудио 6: AС3, 6 ch, 384 kbps - Русский, MVO (TV Shows) - псевдо-дубляж
- 1723189 · ТК НТВ+ · Аудио 08: AC3, 2 ch, 192 Kbps - Русский (MVO, ТК "НТВ+")
- 1701066 · ТК ОРТ · Аудио 12: AC3, 2 ch, 192 Kbps - Русский (MVO, ТК "ОРТ")
- 1700762 · ТК ОРТ · Аудио 04: AC3, 2 ch, 192 Kbps - Русский (MVO, ТК "ОРТ")
- 1721934 · ТК РТР · Аудио 08: AC3, 2 ch, 192 Kbps - Русский (MVO, ТК "РТР")
</details>
<details><summary>примеры miss:audio (1003)</summary>
- 1763763 · TVShows · Аудио 2: AC-3, 2 ch, 48.0 kHz, 192 kbps \| Звук: Русский дубляж [TVShows]
- 1541528 · Инис · Аудио 2: AC3, 2 ch, 192 Kbps (русский, многоголосый - студия ИНИС)
- 1807127 · DniproFilm · Аудио 10: AC3, 6 ch, 640 Kbps - Украинский (MVO - DniproFilm)
- 1328029 · Film Prestige · Аудио 1: AC3, 2 ch, 224 Kbps (русский - Film Prestige)
- 1520154 · Королев · Аудио:5 DTS XLL, 8 ch, 3898 Kbps - Русский, AVO (В.Королёв)
- 857991 · П. Гланц и Т. Казакова · Аудио 2: Русский AC3 5.1, 384 Kbps, 48.0 KHz (Двуголосый) П. Гланц и Т. Казакова
- 1381272 · ОРТ · Аудио #2: AC3, 48 kHz, 6 ch, 448 kbps - Русский, Dub \| ОРТ
- 1771912 · 1+1 · Аудио5: AC3, 2 ch, 256 Kbps - Украинский, DVO (1+1)
- 1780225 · Postmodern · Аудио 5: Е-AC3, 2 ch, 256 Kbps - Украинский (DUB, Postmodern)
- 872505 · Кашкин · Аудио 5: Русский 48 kHz/24-bit, DTS-HD MA, 6 каналов ~4129 kbps \|Авторский, Кашкин (Первомайский)\|
- 1741989 · LeDoyen · Аудио 5: AC3, 6 ch, 384 Kbps - Украинский (DUB, LeDoyen)
- 1499836 · А. Михалёв · Аудио 7: DTS, 6 ch, 1509 Kbps (русский, одноголосый - А. Михалёв)
- 1888656 · LeDoyen · Аудио 6: E-AC3, 6 ch, 640 Kbps - Украинский (DUB, LeDoyen)
- 1872532 · Postmodern · Аудио 2: AC3, 6 ch, 640 Кbps - Украинский (Dub, Postmodern)
- 949884 · Л.Володарский · Аудио #2: AC-3, 48.0 KHz, 2 ch, 192 Kbps, CBR (Russian) - авторский Л.Володарский
</details>
<details><summary>примеры miss:trans (1029)</summary>
- 1887475 · НТВ+ · Перевод: профессиональный многоголосый (Светла), профессиональный двухголосый (НТВ+)
- 306718 · Союзмультфильм · Перевод: Дублированный [Союзмультфильм]
- 1801712 · ICTV · Перевод: дублированный неофициальный (Мост-Видео), профессиональный многоголосый (Novamedia, НТВ, Fox Crime, ICTV), авторский (Антон Карповский, Юрий Сербин, Дм
- 241721 · Гланц · Автор перевода: Петр Гланц
- 1891998 · СВ-Дубль · Перевод: двуголосый профессиональный (СВ-Дубль)
- 1896364 · Postmodern · Перевод: Дублированный (Movie Dubbing, укр. Postmodern)
- 897659 · СВЕТЛА · Перевод: Двуголосый (Светла), Одноголосый (Г.Либергал)
- 1181555 · LostFilm · Перевод: Многоголосый, профессиональный - NewStudio, LostFilm
- 1180186 · BadBajo · Перевод: Одноголосый авторский, А.Важенин (BadBajo)
- 541044 · Zamez · Перевод: Любительский (одноголосый) [zamez]
- 1888887 · Так Треба Продакшн · Перевод: Дублированный (Movie Dubbing, укр. Так Треба Продакшн)
- 434267 · Позитив · Перевод: Профессиональный (двухголосый, закадровый) НТВ+, профессиональный (многоголосый, закадровый) Позитив, авторский (одноголосый, закадровый) Алексей Михал
- 1884985 · AniLiberty · Перевод: AniLiberty, Sanae, Alvakarp
- 848518 · Нева-1 · Перевод: Советский Дубляж + Дубляж от "Нева-1" + Многоголосый
- 1872380 · Так Треба Продакшн · Перевод: Дублированный (Videofilm Int., укр. Так Треба Продакшн)
</details>
<details><summary>примеры noise:«X по заказу Y» (67)</summary>
- nnm-1867973 · по заказу Смотри кино · Аудио 2: AC3, 2 ch, 192 Kbps - Русский (MVO, по заказу "Смотри кино" 18+)
- nnm-1878037 · Paragraph Media по заказу Capella Film · Аудио 1: AC3, 2 ch, 192 Kbps - русский (MVO Paragraph Media по заказу Capella Film)
- 1875813 · СВ-Дубль по заказу ТК Культура · Аудио 04: AC3, 2 ch, 192 Kbps - Русский, DVO (СВ-Дубль по заказу ТК Культура)
- nnm-1379089 · студия Novamedia по заказу Amedia · Перевод: Многоголосый профессиональный (студия Novamedia по заказу Amedia), Aвторский (Ю. Сербин)
- 1229051 · студия Кравец по заказу Amedia · Перевод: Дублированный - студия "Кравец" по заказу Amedia
- 1854965 · студия TrueDubbing Studio по заказу Paramount Comedy · Аудио 3: AC3, 2 ch, 192 Kbps - Русский, MVO (студия "TrueDubbing Studio" по заказу Paramount Comedy)
- nnm-1499746 · по заказу Amedia · Аудио 1: AC3, 6 ch, 384 Kbps, русский (по заказу Amedia)
- 1865453 · студия TrueDubbing Studio по заказу Paramount Comedy · Аудио 2: AC3, 2 ch, 192 Kbps - Русский, MVO (студия "TrueDubbing Studio" по заказу Paramount Comedy)
- nnm-1087457 · 6 Канал, по заказу СТС · Аудио#3: AC3, 2 ch, 192 Kbps \| Русский - MVO (6 Канал, по заказу СТС)
- 1854965 · студия Нота по заказу РТР · Аудио 2: E-AC3, 2 ch, 224 Kbps - Русский, MVO (студия "Нота" по заказу РТР)
- nnm-806894 · студия NovaMedia по заказу Amedia · Перевод: Профессиональный, многоголосый закадровый - студия NovaMedia по заказу Amedia
- nnm-1797122 · Лексикон по заказу Гала Медиа · Аудио 1: AC3, 2 ch, 192 Kbps \| русский (MVO Лексикон по заказу Гала Медиа)
- nnm-1896805 · Селена Интернешнл по заказу ОРТ · Аудио 3: AC3, 2 ch, 192 Kbps - Русский, DVO (Селена Интернешнл по заказу ОРТ)
- nnm-1896063 · Велес по заказу CP Digital · Аудио 2: AC3, 2 ch, 224 Kbps - русский (DVO Велес по заказу CP Digital (2008 г., Станислав Войнич и Светлана Репетина))
- 1896444 · Студия Нота по заказу РТР · Аудио 04: AC3, 2 ch, 192 Kbps - Русский, MVO (Студия Нота по заказу РТР)
</details>
<details><summary>примеры noise:источник: лицензия/Blu-ray/iTunes/Netflix (500)</summary>
- nnm-1437447 · iTunes · Аудио 1: AC3, 6 ch, 384 kbps - Русский (DUB \| iTunes)
- nnm-1807653 · Blu-ray · Аудио 1: AC3, 6 ch, 640 Kbps русский ( DUB, Blu-ray)
- nnm-1525970 · iTunes · Аудио 1: AC3, 6 ch, 384 kbps - Русский (DUB, iTunes)
- nnm-1372998 · Blu-Ray · Аудио 01: DTS-HD MA, 8 ch, 3091 Kbps - Русский (DUB, Blu-Ray)
- nnm-1641984 · Лицензия · Аудио 1: AC3, 6 ch, 640 Kbps - Русский, Дубляж (Лицензия)
- nnm-1515035 · Blu-Ray CEE · Аудио 2: AC3, 6 ch, 448 Kbps - Русский (DUB \| Blu-Ray CEE)
- nnm-1026540 · iTunes · Аудио: AC3, 6 ch, 384 kbps, CBR, Русский (iTunes)
- nnm-1691690 · Blu-Ray CEE · Аудио 1: AC3, 6 ch, 640 Kbps - Русский (Dub, Blu-Ray CEE)
- nnm-1532438 · CPI Films, iTunes · Аудио 1: AC3, 6 ch, 384 Kbps - Русский (Dub CPI Films, iTunes)
- nnm-1098424 · iTunes · Аудио 1: AC3 5.1 384 Кбит/сек - Русский (iTunes)
- nnm-1404235 · BD CEE · Аудио 1: AC3, 6 ch, 640 kbps, Русский (DUB BD CEE)
- nnm-1635787 · BD CEE · Аудио 1: E-AC3, 6 ch, 448 kbps - Русский (Dub,BD CEE)
- nnm-1571635 · BD EUR · Аудио 1: AC3, 6 ch, 640 kbps - Русский (Dub, BD EUR)
- nnm-1607165 · BD EUR · Перевод: Дублированный (BD EUR)
- nnm-1312426 · Лицензия · Аудио 1: AC3, 6 ch, 640 Kbps - Русский (DUB, Лицензия)
</details>
<details><summary>примеры noise:каналы/кодек (89)</summary>
- nnm-511174 · L,R · Аудио 1: Русский: 48 kHz, AC3, 2/0 (L,R), ~384.00 kbps avg \|Дубляж, Видеосервис\|
- nnm-388165 · L, C, R, l, r · Аудио: Русский, AC3, 48 kHz, 6 каналов, 448 kbps, 16 bit, (L, C, R, l, r) + LFE \|Гоблинский, Многоголосный\|
- nnm-505307 · AC3, 2 ch, 192 kb/s · Аудио #3: Русский ( AC3, 2 ch, 192 kb/s ) (одноголосный)
- nnm-849036 · L,C,R,l,r · Аудио 2: Русский 48 kHz, AC3, 3/2 (L,C,R,l,r) + LFE ch, 640 kbps avg \| Многоголосый, Superbit
- nnm-471722 · L,C,R,l,r · Аудио#1: Русский: 48 kHz, AC3, 3/2 (L,C,R,l,r) + LFE ch, ~640 kbps avg \|Дубляж, Blu-ray CEE\|
- nnm-320358 · AC3, 6 ch, 448 kb/s · Аудио #1: Русский (AC3, 6 ch, 448 kb/s)
- nnm-492468 · L, R · Аудио 2: Русский, AC-3, 192 kbps, 2 канала, 16 bit, (L, R) - Многоголосый
- nnm-508334 · Dolby AC3, 6 ch · Аудио 2: Русский (Dolby AC3, 6 ch) 384 kb/s - Многоголосый,профессиональный - 20th Century Fox MI
- nnm-494281 · L, C, R, l, r · Аудио 1: Русский, DTS, 755 kbps, 6 каналов, 24 bit, (L, C, R, l, r) + LFE
- nnm-300455 · DTS, 6 ch, 755 kb/s · Аудио #3: Русский (DTS, 6 ch, 755 kb/s) одноголосый закадровый - Гаврилов
- nnm-374286 · AC3, 6 ch, 384 kb/s · Аудио #2: Русский ( AC3, 6 ch, 384 kb/s )
- nnm-380475 · L,C,R,l,r · Аудио#3: Русский: 48 kHz, AC3, 3/2 (L,C,R,l,r) + LFE ch, ~384.00 kbps avg \|Дубляж, R5\|
- nnm-356829 · AC3, 6 ch, 384 kb/s · Аудио #2: Русский ( AC3, 6 ch, 384 kb/s )
- nnm-1057579 · L,R · Аудио#1: Русский - 48 kHz, AC3, 2/0 (L,R) ch, 192 kbps avg
- nnm-369518 · AC3, 6 ch, 448 kb/s · Аудио #2: Русский (AC3, 6 ch, 448 kb/s)
</details>
<details><summary>примеры noise:неру дорожка (6)</summary>
- nnm-29325 · песни на английском языке · Перевод: Многоголосый профессиональный (песни на английском языке)
- nnm-1784983 · Казахфильм · Перевод: Дублированный (Казахфильм)
- nnm-1571754 · Cinema Tone Production, Казахстан · Аудио 1: AC-3, 2 ch, 192 Kbps - Русский (DUB \| Cinema Tone Production, Казахстан)
- nnm-1572189 · Cinema Tone Production, Казахстан · Аудио 1: AC-3, 2 ch, 192 Kbps - Русский (DUB \| Cinema Tone Production, Казахстан)
- nnm-51930 · слышен английский язык · Перевод: Профессиональный многоголосый (слышен английский язык)
- nnm-1450412 · Казахский язык · Перевод: Дублированный (Казахский язык)
</details>
<details><summary>примеры noise:субтитры (7)</summary>
- nnm-1843999 · Мика Бондарик, по субтитрам Lisok · Аудио 1: AC3, 2 ch, 192 Kbps \| русский (VO Мика Бондарик, по субтитрам Lisok)
- nnm-1894229 · Мика Бондарик, по субтитрам Lisok · Аудио 1: AC3, 2 ch, 192 Kbps - русский (VO Мика Бондарик, по субтитрам Lisok)
- nnm-1894835 · А. Наследников по субтитрам lobo · Аудио 1: AC3, 2 ch, 192 Kbps - русский (VO А. Наследников (MrRose) по субтитрам lobo)
- nnm-1894859 · А. Наследников по субтитрам lobo · Аудио 1: AC3, 2 ch, 192 Kbps - русский (VO А. Наследников (MrRose) по субтитрам lobo)
- nnm-1622631 · Andre1288 по субтитрам Антона Каптелова · Аудио 1: AC3, 2 ch, 192 kbps \| русский (VO Andre1288 по субтитрам Антона Каптелова)
- nnm-1874776 · Матвей Кенс по субтитрам Silken · Аудио 1: AC3, 2 ch, 192 Kbps - русский (VO Матвей Кенс по субтитрам Silken)
- nnm-1894210 · Мика Бондарик, по субтитрам Lisok · Аудио 1: AC3, 2 ch, 192 Kbps - русский (VO Мика Бондарик, по субтитрам Lisok)
</details>
<details><summary>примеры noise:тип-слово (41)</summary>
- nnm-857721 · Двуголосый · Аудио 4: Русский AC3 5.1, 384 Kbps, 48.0 KHz (Двуголосый) П. Гланц и Т. Казакова
- nnm-114327 · дубляж · Аудио1: Русский AC3 5.1, 48 KHz, 384 kbps (дубляж)
- nnm-389317 · Дубляж · Аудио 1: DTS Audio, Русский, 768 kbps, 5.1 / 48 kHz / 768 Кбит/сек / 24-bit (Дубляж)
- nnm-1365221 · Дубляж · Аудио 1: AC3, 6 ch, 448 Kbps - Русский (Дубляж)
- nnm-1457626 · дубляж · Аудио #1: AC3, 6 ch, 384 kbps - русский (дубляж)
- 1342511 · дубляж · Аудио #1: AC3, 6 ch, 640 kbps - русский (дубляж)
- nnm-1553559 · дубляж · Аудио1: AC3, 6 ch, 48.0 KHz, 448 Kbps \| русский (дубляж) [Студийная Банда]
- nnm-967458 · Двуголосый · Аудио 4: Русский AC3 5.1, 384 Kbps, 48.0 KHz (Двуголосый) Tycoon-Studio
- nnm-1650999 · дубляж · Аудио 1: AC3, 2 ch, 192 Kbps - русский (дубляж)
- nnm-1538061 · дубляж · Аудио #1: АС3, 6 ch, 384 kbps - русский (дубляж)
- nnm-312080 · полное дублирование · Аудио 2: DTS, 6 ch, 768Kbps, 48.0khZ Русский профессиональный (полное дублирование)
- nnm-821242 · Полное дублирование · Аудио 1: Русский AC3 5.1, 640 Kbps, 48.0 KHz (Полное дублирование)
- nnm-1181270 · дубляж · Аудио #1: AC3, 48.0 KHz, 6 ch, 384 Kbps, CBR - Русский (дубляж)
- nnm-997923 · Двуголосый · Аудио 3: Русский AC3 5.1, 384 Kbps, 48.0 KHz (Двуголосый) П. Гланц и Т. Казакова
- nnm-1578209 · Дубляж · Аудио #1: AC3, 6 ch, 448 Кбит/с, русский (Дубляж)
</details>
<details><summary>примеры noise:язык вместо студии (26)</summary>
- nnm-506684 · Испанский · (Dolby AC3, 2 ch) ~192 kb/s \|Испанский,дублированный\|
- nnm-268206 · английский · Перевод: Дублированный + Оригинальный (английский,отдельно)
- nnm-1074532 · украинский · Перевод: Дублированный (украинский)
- nnm-506333 · Испанский · (Dolby AC3, 2 ch) ~192 kb/s \|Испанский,дублированный\|
- nnm-528788 · Испанский · (Dolby AC3, 2 ch) ~192 kb/s \|Испанский, дублированный\|
- nnm-537011 · Французский · (Dolby AC3, 2 ch) ~192 kb/s \|Французский, дублированный\|
- nnm-534886 · Испанский · (Dolby AC3, 2 ch) ~192 kb/s \|Испанский, дублированный\|
- nnm-701414 · русский · Аудио 1: AC3; 192 Kbps; 48 kHz; 2 ch [Русский Репортаж (русский)]
- nnm-537011 · Испанский · (Dolby AC3, 2 ch) ~192 kb/s \|Испанский, дублированный\|
- nnm-649658 · Французский · (Dolby AC3, 2 ch) ~192 kb/s \|Французский, дублированный\|
- nnm-536934 · Французский · (Dolby AC3, 1 ch) ~192 kb/s \|Французский, дублированный\|
- nnm-452881 · Русский · (Dolby AC3, 6 ch) ~384 kb/s \|Русский,дублированный\|
- nnm-587730 · Украинский · Перевод: Дублированный (Русский) + Дублированный (Украинский)
- 1839838 · Украинский · Перевод: Профессиональный многоголосый (Syncmer), Дублированный (Украинский)
- nnm-372312 · английский · Перевод: Профессиональный (двухголосый, закадровый) R.U.S.C.I.C.O / Оригинальный (английский) + Профессиональный (многог
</details>
<details><summary>примеры unlab (1987)</summary>
- 429630 · Симпсоны / The Simpsons (1989 –2012) HDTVRip [720p], TVRip (23 сезона 510 серии · Перевод:
- 1396241 · Marmalade Boy \| Мальчик-мармелад [1994, TV, 76 из 76] BDRip 960p raw · Перевод: Жанна Тачёва, Tiona, KelendrosS
- 1849320 · Umibe no Etranger \| The Stranger by the Shore \| Незнакомец на берегу моря [2020, · Аудио 1: AC-3, RUS(int), 2.0 ch, 48.0 khz, 192 kbps \| Озвучкa: Закадр. (MVO) от StudioBand
- 1008316 · Тайное вторжение / The Secret Invasion (1964) HDTV [H.264/1080i-LQ] · Перевод: Двуголосый (НТВ+)
- 428151 · Fractale / Фрактал [2011, TV, 11] BDRip 720p raw + rus · Перевод: DreamersTeam
- 482868 · Ningen Kousaten / Human Crossing Point / Точки соприкосновения [2003, TV, 13 ep. · Перевод: Whizz Описание:
- 1365052 · Hanasaku Iroha \| Hanasaku Iroha: Blossoms for Tomorrow \| Азбука Цветов [2011, TV · Озвучка: Одноголосая, Двухголосая закадровая
- 1389210 · Море Солтона / The Salton Sea (2002) WEB-DL [H.264/720p] · Перевод 1: Профессиональный (двухголосый закадровый) Пётр Гланц и Евгения Лютая
- 1360458 · Sword Art Online \| Мастера Меча Онлайн [ТВ-1] [2012, TV, 25 ep] BDRip 1080p 10-b · Озвучка: Одноголосая, Двухголосая, Многоголосая закадровая
- 1753801 · Бог располагает / Gud raader (1912) WEBRip [H.264/720p] · Перевод: немой с интертитрами (В. Сачков)
- 1860621 · Дочь магазина / Magasinets Datter (1918) WEBRip [H.264/1080p] · Перевод: немой с интертитрами
- 253552 · Метрополис / Metropolis (1927) HDTV [H.264/720p] · Перевод: Немой с интертитрами
- 1331498 · Takarajima \| Treasure Island \| Остров сокровищ [1978, TV, 26 из 26] BDRip 1080p · Перевод: Gerale + Komugi-chan
- 1818600 · Evangelion Shin Gekijouban: Jo \| Evangelion: 1.0 You Are (Not) Alone \| Evangelio · Аудио 1: AC-3, RUS(int), 2.0, 48.0 khz, 192 kbps \| Озвучкa: Дубляж Reanimedia 2.0
- 1184789 · Taboo-Tattoo \| Запретная татуировка [2016, TV, 12 эп.] BDrip 1080p raw · Перевод: AOS
</details>
## rutracker
**releases**: всего — 11166; с метками — 7742; без меток — 3424; с метками, есть недостающая студия — 972; с метками без студий, студия есть в кодек-строке — 495
**строки**: trans — 15192; audio — 11471
**кодек-строки**: RE_AUDIO_TRACK | codec_segments — 5950; RE_AUDIO_TRACK | мимо codec_segments — 3025; мимо RE_AUDIO_TRACK | мимо codec_segments — 2033; мимо RE_AUDIO_TRACK | codec_segments — 463
**форматы мимо: seg codec**: аудио: — 1066; аудио N rus: — 161; аудио rus N: — 63; русская: — 59; аудио rus N-N: — 46; аудио #N русский, — 46; russian acN, N khz, — 31; russian: — 30; russian (dolby acN, N — 29; аудио #N (rus): — 28; аудио N ukr: — 23; аудио #N (внешняя) — 18; аудио : — 16; аудиопоток №N (rus): — 15; аудио rus#N (int.): — 13; аудио rus (ext): — 13; audio #N (rus): — 10; russian acN / N.N / N — 10; аудио №N (rus): — 9; звук (rus(int)): — 8; аудио N-N: — 8; russian, aac, N.N kh — 8; аудио rus N-N (ext): — 7; rus N (ext) aac, — 7; аудио #N (ext): — 7
**студии меток**: всего — 9107; нет в тексте/заголовке — 14
**без меток**: обычный, есть строки дорожек — 1887; обычный, нет строк дорожек — 1537
**недостающие студии: вид строки**: trans — 1114; audio — 531
**недостающие студии: топ**: LeDoyen — 60; NewStation — 48; HDrezka — 46; TVShows — 45; Невафильм — 40; Jaskier — 39; Киномания — 35; 2x2 — 31; LostFilm — 31; Кубик в Кубе — 31; СТС — 29; Red Head Sound — 25; Postmodern — 24; Гоблин — 22; А.Гаврилов — 22; Так Треба Продакшн — 21; Ю.Живов — 20; Андрей Гаврилов — 19; А. Михалёв — 19; Михалёв — 19; ICTV — 19; Пифагор — 17; ОРТ — 17; 1+1 — 17; Гланц — 15
**форматы мимо: seg**: аудио: — 437; аудио : — 20; аудио N: — 4; аудио #N: — 2
**форматы мимо: codec**: аудио N: — 1856; аудио rus: — 661; аудио #N: — 354; аудиоN: — 62; аудио#N: — 44; аудио №N: — 29; аудио ukr: — 10; audio #N: — 4; аудиодорожка N: — 2; audio#N: — 1; аудио N : — 1; аудио N.N: — 1
**студии меток: нет в тексте, studio_source**: release — 7; audio — 7
**мусор в studio меток**: источник: лицензия/Blu-ray/iTunes/Netflix — 231; «X по заказу Y» — 120; субтитры — 27; неру дорожка — 26; тип-слово — 19; каналы/кодек — 18; язык вместо студии — 18; язык-префикс «Русский, - X» — 13
<details><summary>примеры fmt:codec (3000)</summary>
- 6259831 · Аудио 4: Russian E-AC3 / 7.1 / 48 kHz / 1536 kbps \|Ю. Сербин\|
- 5711781 · Аудио 4: DTS-HD Master Audio Russian 3412 kbps 5.1 / 48 kHz / 3412 kbps / 24-bit (DTS Core: 5.1 / 48 kHz / 768 kbps / 24-bit) \| А. Михалев
- 6846635 · Аудио 8: Russian AAC (Advanced Audio Codec), 44.1 kHz, 2.0 ch (Stereo), 192 kb/s \| «LE-Vitation»
- 6887455 · Аудио #1: AAC (Advanced Audio Coding), 48.0 kHz, 2.0 ch (Stereo), ~128 kb/s (Russian) \| "Сыендук" (18+)
- 6899403 · Аудио Rus: AC3, 48 kHz, 2 ch, 384 kbps \|Lostfilm\|
- 6798816 · Аудио 9: Russian / Dolby Digital Audio, 256 kbps, 2.0 / 48 kHz / 256 kbps - НТВ+ (2014 / 2016 г.)
- 4767310 · Аудио #3: Russian (Dolby AC3, 6 ch) 384 Кбит/сек, 48,0 КГц
- 6902539 · Аудио Rus: AC3, 48,0 kHz, 6 ch, 640 Кбит/сек \| Red Head Sound \| Серии: 1-8
- 4186469 · Аудио RUS: АС3, 2 ch, 48 kHz, 192 kbps \|HDrezka\|
- 5921821 · Аудио 3: AC-3 384 kb/s 6 channels, 48.0 kH (Russian) [Flarrow Films]
- 5645820 · Аудио 1: 48 kHz, AC3 Dolby Digital, 3/2 (L,C,R,l,r) + LFE ch, ~448 kbps [русский]
- 6063299 · Аудио RUS: AAC, 48.0 kHz, 2.0 ch, 192 Kbps (в составе контейнера). Озвучка: многоголосая закадровая [KANSAI Studio]
- 3930926 · Аудио 6: Russian (Dolby AC3, 2 ch (2/0 (L,R) ch), 192 kbps)
- 4667342 · Аудио #2: [UKR] 48 kHz, AC3 Dolby Digital, 2/0 (L,R) Surround Encoded ch, ~192 kbps
- 6824956 · Аудио Rus: AC-3, 48 kHz, 6 ch, 384 kbps \|TVShows\|
</details>
<details><summary>примеры fmt:seg (463)</summary>
- 5838316 · Аудио: VBR ~128 Kbps, 44.1 kHz, 2ch Язык Русский; Озвучка: VO - Sahawk (AniMedia)
- 6425819 · Аудио: AC-3, 320 кбит/с, 48.0 кГц, 2 канала Язык Русский; Озвучка: Arrow Hell (NiksoN, n0active, Libra, Mufdkl) [Многоголосая закадровая]
- 2196723 · Аудио: Английский (Dolby AC3, 6 ch,448 Kbps), Русский (Dolby AC3, 6 ch, 448 Kbps) П. Гланц,И.Королёва, Русский (Dolby AC3, 6 ch,448 Kbps) дубляж, Китайский (Dol
- 2368674 · Аудио: Russian: DTS 5.1, 768 kbps (одноголосый, Немахов) (отдельно)
- 6691277 · Аудио: AC3 Dolby Digital, 48 kHz, 2/0 ch, 192 kbps, Russian MVO
- 2822758 · Аудио: russian: AC3 48000 6ch 384 kbps \|многоголосый, закадровый, студия Kravec (lostfilm)\|
- 5749545 · Аудио: Русский; AC-3; 192 Кбит/сек; 2 канала; 48,0 КГц (Студия "Кириллица" (По заказу Disney)) Дубляж
- 5053740 · Аудио: AC-3, 192kbps, 48,0Hz, 2ch Язык Русский (в составе контейнера) ; Озвучка: одноголосая женская Persona99
- 6440498 · Аудио: AC-3, 192 кбит/с, 48.0 кГц, 2 канала Язык Русский; Озвучка AniDub (Shaman, HelgardRay, Oriko, Лизавета) [Многоголосая закадровая]
- 6222916 · Аудио: Russian / Dolby Digital Audio / 5.1 / 48 kHz / 384 kbps \| Дубляж
- 6434506 · Аудио: AC-3, 320 кбит/с, 48.0 кГц, 2 канала Язык Русский; Озвучка SHIZA Project (Dancel, DroEd, Abe, Saylot, Kitsune, Bvia) [Многоголосая закадровая]
- 6292078 · Аудио: Russian AAC / 2.0 / 48 kHz / 192 kbps \| Дубляж Невафильм \|
- 3988042 · Аудио: AC3, 2/0 (L,R) ch, 192 kbps Язык Русский (Любительский одноголосый)
- 6648621 · Аудио: AC-3, 192 kbps, 48.0 kHz, 2 ch, Язык Русский; Озвучка: многоголосая закадровая, MiraiDuB
- 4811969 · Аудио: Russian (AC3, 2 ch, 192 kbps, 16 bits, 48.0 KHz) - VO
</details>
<details><summary>примеры fmt:seg codec (2033)</summary>
- 4952029 · Аудио: Russian: AC3, 2/0 (L,R) ch, 256 kbps
- 5343227 · Аудио: AC3, 2/0 (L,R) ch, 192 kbps, Delay 0 mSec (rus)
- 5454128 · русская: Dolby AC3 48000Hz --\|DualMono\|-- 192Kbps [Ren-TV] (Серии 01-13)
- 6789362 · Аудио 1 Rus: AC3, 48 kHz, 2 ch, 192 kbps (Dаблин)
- 5124321 · Аудио RUS 1: AC3, 48000Hz, ~192 Kbps, 2ch (внешними файлами)
- 5341902 · Аудио #4-8: AC3, 192 kbps, 2 ch (Русский, Греческий, Румынский, Турецкий, Чешский)
- 5169587 · Аудио: Rus, DTS, 768 kb/s (6 ch), 48 kHz
- 3874340 · Audio Русский (DTS-HD Master Audio / 5.1 / 48 kHz / 3957 kbps / 24-bit (DTS Core: 5.1 / 48 kHz / 1509 kbps / 24-bit) - Одноголосый закадровый [-Доцент-]
- 6392123 · Аудио: RUS \| AС-3, 48 кГц, ~192 кбит/с, 2 канала \| AniLibria
- 541355 · Аудио: Russian (Dolby AC3, 6 ch) Japanese (Dolby AC3, 6 ch), Russian (Dolby AC3, 2ch)
- 742076 · Audio:Russian (Dolby AC3, 2 ch 192Kbps)
- 735299 · Аудио: Japanese (Dolby AC3, 2 ch), Russian (Dolby AC3, 2 ch)
- 5220817 · Аудио: Rus, AC3, 640 kb/s (6 ch), 48 kHz
- 4224158 · Аудио: Dolby Digital Audio Russian 5.1 / 48 kHz / 384 kbps \| Попов
- 3969441 · Аудио : Русский (Dolby AC3, 2 ch, 192 кб/с, СТС)
</details>
<details><summary>примеры ghost (14)</summary>
- 6700775 · ТК 1+1 · Ukrainian AC3 / 2.0 / 48 kHz / 192 kbps \|MVO, ТК "1+1"\|
- 5854979 · RUS , UKR · Профессиональный (дублированный) \| RUS (BD 3D CEE), UKR (Ledoyen)
- 6698664 · ТК 1+1 · Ukrainian AC3 / 2.0 / 48 kHz / 192 kbps \|MVO, ТК "1+1"\|
- 5852176 · RUS , UKR · Профессиональный (дублированный) \| RUS ( iTunes), UKR (студія LeDoyen)
- 6641488 · ТК 1+1 · Ukrainian AC3 / 2.0 / 48 kHz / 192 kbps \|MVO, ТК "1+1"\|
- 5641267 · ТКТочкаТВ · Профессиональный (многоголосый закадровый) MVO ТК"ТочкаТВ";
- 6803833 · ТК 1+1 · Ukrainian AC3 / 2.0 / 48 kHz / 192 kbps \|MVO, ТК "1+1"\|
- 6821405 · ТК 1+1 · Ukrainian AC3 / 2.0 / 48 kHz / 192 kbps \|MVO, ТК "1+1"\|
- 6813764 · ТК 1+1 · Ukrainian AC3 / 2.0 / 48 kHz / 192 kbps \|DVO, ТК "1+1"\|
- 3376591 · эп. 1-9 , эп. 10-13 · эп. 1-9 [YakuSub Studio], эп. 10-13 [Advantage]
- 6891775 · ViruseProject · Любительский (двухголосый закадровый) Viruse Project
- 6821395 · ТК 1+1 · Ukrainian AC3 / 2.0 / 48 kHz / 192 kbps \|MVO, ТК "1+1"\|
- 6181833 · НТВ+ ТНТ · Профессиональный (двухголосый закадровый) - НТВ+ \| ТНТ
- 6396910 · ТО Дия · Профессиональный (дублированный) ТО "Дия"
</details>
<details><summary>примеры miss:audio (531)</summary>
- 3296081 · Королев · Аудио 3: Русский одноголосый Королёв DTS / 5.1 / 768 kbps / 48 kHz
- 5806891 · Shiza Project · Аудио: Russian (ext), AC-3, 48000Hz, ~192 Kbps, 2ch \| SHIZA Project
- 6053237 · Невафильм · Аудио 1: русский (профессиональный, дублированный, Невафильм) AC3 256 kbps 48kHz 2Ch
- 4496622 · Киномания · Аудио: Russian: 48 kHz, DTS, 3/2 (L,C,R,l,r) + LFE ch, ~1536.00 kbps avg \|Многоголосый закадровый, Киномания\|
- 5931625 · NewComers · Аудио2: AC3, 3/2 (L,C,R,SL,SR)+LFE ch, 384 kbps........................ Русский NewComers
- 1328322 · Tycoon · Аудио 3: Русский AC3, 6ch 448 Kbps (двухголосый, Tycoon)
- 5899190 · HDrezka · Аудио4: AC3, 3/2 (L,C,R,SL,SR)+LFE ch, 448 kbps.................................................................................................. Русский HDRezk
- 6130286 · Юрий Живов · Аудио 5: Russian \| AC3 / 2.0 Ch / 48 kHz / 224 kbps \| AVO \| Юрий Живов (с VHS)
- 6680266 · Garsu Pasaulis · Аудио 6: Russian \| FLAC / 2.0 Ch / 48 kHz / 24 bit / 1537 kbps \| DVO \| "Garsu Pasaulis" (Литва) [VHS Hi-Fi Stereo]
- 5148134 · Persona99 · Аудио 1: AC-3 48 КГц; 192 Кбит/сек , 2.0 Язык Русский (в составе контейнера) ; Озвучка: женская одноголосая: Persona99
- 5056191 · Нота · Аудио #03: Russian: 48 kHz, AC3, 2/0 (L,R) ch, ~192 kbps avg \|Многоголосый закадровый, Нота\|
- 6344693 · DniproFilm · Аудио 7: АС3, 6 ch, 48 kHz, 384 kbps Украинский MVO DniproFilm/HDRezka Studio
- 6096897 · AlexFilm · Аудио Rus: AC3, 48 kHz, 3/2 (L R C LFE Ls Rs) ch, 384 Кбит/сек \| AlexFilm
- 6680266 · Михаил Яроцкий · Аудио 11: Russian \| DTS-HD MA / 7.1 Ch / 48 kHz / 16 bit / 2951 kbps (core: DTS / 5.1 Ch / 48 kHz / 16 bit / 1509 kbps) \| VO \| Михаил Яроцкий aka kyberpunk
- 6086348 · Сэмпл · Аудио: AAC, 144 кбит/сек, 48 КГц, 2 канала; Язык: Русский (в составе контейнера); Озвучка: Dantes (мужской голос) & Yuki (женский) (Amazing Dubbing) Сэмпл
</details>
<details><summary>примеры miss:trans (1114)</summary>
- 4919872 · Невафильм · Перевод 1: Профессиональный (дублированный) \|Дубляж, Blu-ray CEE_[Невафильм]_2007г\|
- 6771413 · заКАДРЫ · Перевод #3: Любительский (многоголосый, закадровый) [заКАДРЫ]
- 3569656 · Видеосервис · Перевод: Профессиональный (двухголосый закадровый) x2 (ОРТ, ВидеоСервис)
- 6792103 · DniproFilm · Перевод 7: Профессиональный (многоголосый закадровый) HDRezka (Dniprofilm)
- 4431588 · 1+1 · Перевод 5: Профессиональный (двухголосый закадровый) [1+1]
- 6279898 · Невафильм · Перевод: Профессиональный (полное дублирование) [Невафильм]
- 6845439 · отсутствует · Перевод 3: Казахский, профессиональный (дублированный) (Cinema Tone Production) по заказу Nickelodeon (временно отсутствует в S02E07)
- 6713595 · Red Head Sound · Перевод 2-3: Профессиональный (псевдодублированный) — Red Head Sound, Jaskier
- 5007272 · ОРТ · Перевод #2: Профессиональный многоголосый закадровый (ОРТ)
- 6860022 · А.Михалев · Перевод 3-5: авторский (одноголосый закадровый) - В.Горчаков, А.Михалев, П.Прямостанов
- 2555358 · Persona99 · Перевод: Двухголосый (Persona99 & MaxDamage;Noir & Cuba77), Субтитры
- 6790851 · ИДДК · Перевод 3-6: профессиональный (двухголосый закадровый) - Мьюзик-Трейд, DVD Магия / Флагман, ИДДК, Дивайс
- 6100973 · Рудой · Перевод 6: Авторский (одноголосый, закадровый) [Е.Рудой]
- 5711781 · А. Михалёв · Перевод 4: Авторский (одноголосый закадровый) А. Михалев
- 2455771 · MC Entertainment · Перевод: Профессиональный (многоголосый, закадровый) [MC Entertainment]
</details>
<details><summary>примеры noise:«X по заказу Y» (120)</summary>
- rt-5389600 · СВ-Дубль по заказу ВГТРК · Профессиональный (Полное Дублирование) "СВ-Дубль" по заказу "ВГТРК" (Карусель)
- rt-5087462 · по заказу 2х2 · Профессиональный (многоголосый, закадровый) по заказу 2х2 (Быстров, Савина, Эльдаров)
- rt-6130327 · SDI Sun-Studio Russia по заказу ОРТ 2006 г · Профессиональный (дублированный) «SDI Sun-Studio Russia» по заказу ОРТ 2006 г
- rt-6572883 · VSI Moscow по заказу Nickelodeon · Русский, профессиональный (дублированный) \| «VSI Moscow» по заказу Nickelodeon
- rt-4820398 · Творческая студия работников дубляжа 1995 год по заказу Центр Скип Фильм · закадровый) Творческая студия работников дубляжа 1995 год по заказу Центр Скип Фильм
- rt-6124488 · SDI Sun-Studio Russia по заказу ОРТ 2006 г · Профессиональный (дублированный) «SDI Sun-Studio Russia» по заказу ОРТ 2006 г
- 5485340 · кинокомпания Эй Би Видео по заказу Первого канала · Профессиональный (многоголосый закадровый) кинокомпания Эй Би Видео по заказу Первого канала
- 3373172 · студия Кадр , по заказу РТР, 1995 г · Профессиональный (дублированный): студия "Кадр" (при уч. студии "Нота"), по заказу РТР, 1995 г
- rt-6895972 · Syncmer по заказу HBO · Профессиональный (двухголосый закадровый) — Syncmer по заказу HBO
- rt-6845439 · по заказу Nickelodeon · Украинский, профессиональный (дублированный) (1+1) по заказу Nickelodeon
- rt-4397160 · студия Кипарис по заказу 2х2 · Профессиональный (двухголосый закадровый) "студия "Кипарис" по заказу 2х2"
- rt-6878274 · Дубляж Reanimedia По заказу Amediateka · Дубляж Reanimedia [русский] По заказу Amediateka
- 6851700 · По заказу смотри кино · Профессиональный (многоголосый закадровый) \| По заказу "смотри кино \| 18+
- 5485351 · кинокомпания Эй Би Видео по заказу Первого канала · Профессиональный (многоголосый закадровый) кинокомпания Эй Би Видео по заказу Первого канала
- rt-4609190 · СВ-Дубль, по заказу МНВК в 1996 году · Профессиональный (дублированный) СВ-Дубль, по заказу МНВК в 1996 году
</details>
<details><summary>примеры noise:источник: лицензия/Blu-ray/iTunes/Netflix (231)</summary>
- 4659385 · iTunes Russia · Профессиональный (дублированный) \| iTunes Russia
- 3893431 · лицензия · Профессиональный (дублированный), лицензия
- rt-6292012 · Пифагор по заказу Netflix · Профессиональный (дублированный) Пифагор по заказу Netflix
- 5967155 · BD CEE со вставками Д. Есарева и Замеза · Профессиональный (дублированный) \| BD CEE со вставками Д. Есарева и Замеза
- rt-6239239 · Пифагор по заказу Netflix · Профессиональный (дублированный) Пифагор по заказу Netflix
- 5516896 · BluRay / По заказу Первого канала · Профессиональный (двухголосый закадровый) - BluRay / По заказу Первого канала
- rt-6426925 · iTunes · Ukrainian AC3 / 5.1 / 48 kHz / 384 kbps \|Dub, iTunes\|
- rt-5010737 · Blu-ray CEE · Профессиональный (дублированный) Blu-ray CEE
- rt-6591933 · BD CEE Невафильм · Профессиональный (дублированный) BD CEE \| Невафильм (2001 год)
- rt-6130327 · Blu-Ray CPIG 2010 г · Профессиональный (дублированный) Blu-Ray «CPIG» 2010 г
- 6320223 · Blu-ray CEE · Профессиональный (дублированный) - Blu-ray CEE
- rt-4822676 · Blu-ray CEE · Профессиональный (полное дублирование) Blu-ray CEE
- 5096972 · Blu-ray CEE · Профессиональный (дублированный) Blu-ray CEE
- rt-5727410 · RUS + UKR Дубляж iTunes · Профессиональный (дублированный) RUS + UKR Дубляж \| iTunes
- rt-5409044 · iTunes · #2: Ukrainian: 48 kHz, AAC, 2/0 (L,R) ch, ~160.00 kbps avg \|Дубляж, iTunes\|
</details>
<details><summary>примеры noise:каналы/кодек (18)</summary>
- rt-5521273 · AAC, 48.0 KHz, 2 ch, 317 Kbps · AAC, 48.0 KHz, 2 ch, 317 Kbps (Китайский)
- rt-4206667 · Shachiburi , внешним файломОзвучивание: Eladiel & Zendos, , внешним файлом Аудио, Japanese: AAC, 48000Hz, 2ch, 192 Kbps CBR · Shachiburi , внешним файломОзвучивание: Eladiel & Zendos, , внешним файлом[*]Аудио, Japanese: AAC, 48000Hz, 2ch, 192 Kbp
- 3163663 · AC3, 6ch , 448 кбит/с Blu-ray CEE + · Украинский (Дублированный), AC3, 6ch (L, C, R, Ls, Rs +LFE), 448 кбит/с \|Blu-ray CEE\| + (вставки из киномании)
- 3163663 · AC3, 6ch , 384 кбит/с Сербин · Русский (Профессиональный (один голос)), AC3, 6ch (L, C, R, Ls, Rs +LFE), 384 кбит/с \|Сербин\|
- 2579545 · AC3, 6ch , 448 кбит/с Ист-Вест * · Русский (Дублированный), AC3, 6ch (L, C, R, Ls, Rs +LFE), 448 кбит/с \|«Ист-Вест»\|*
- rt-4311341 · JAP AAC, 128 kbps, 44,1 Hz, Stereo · JAP AAC, 128 kbps, 44,1 Hz, Stereo
- rt-4311341 · RUS AAC, 256 kbps, 48,0 Hz, Stereo · RUS AAC, 256 kbps, 48,0 Hz, Stereo
- rt-5521273 · AAC, 44.1 KHz, 2 ch, 192 Kbps · AAC, 44.1 KHz, 2 ch, 192 Kbps (Китайский)
- rt-2985347 · Rus AC-3, 6Ch, 384 Kbps · Rus AC-3, 6Ch, 384 Kbps
- 3163663 · AC3, 6ch , 448 кбит/с Blu-ray CEE + · Русский (Дублированный), AC3, 6ch (L, C, R, Ls, Rs +LFE), 448 кбит/с \|Blu-ray CEE\| + (вставки из киномании)
- rt-5521273 · AAC, 48.0 KHz, 2 ch, 192 Kbps · AAC, 48.0 KHz, 2 ch, 192 Kbps (Русский)
- rt-4557862 · MPEG Audio, 48.0 KHz, 2 ch, ~192 Kbps · MPEG Audio, 48.0 KHz, 2 ch, ~192 Kbps
- 3163663 · AC3, 6ch , 384 кбит/с Гаврилов · Русский (Профессиональный (один голос)), AC3, 6ch (L, C, R, Ls, Rs +LFE), 384 кбит/с \|Гаврилов\|
- rt-3972129 · П.Гланц и И.Королёва , в составе контейнера Аудио, Japanese: AC3, 48000Hz, 5.1ch, 448 Kbps CBR · П.Гланц и И.Королёва , в составе контейнера[*]Аудио, Japanese: AC3, 48000Hz, 5.1ch, 448 Kbps CBR
- rt-5521273 · AAC, 44.1 KHz, 2 ch, 192 Kbps · AAC, 44.1 KHz, 2 ch, 192 Kbps (Русский)
</details>
<details><summary>примеры noise:неру дорожка (26)</summary>
- 6405953 · Qazaqstan казахский · Профессиональный (многоголосый закадровый) Qazaqstan казахский
- rt-6802844 · Телеканал Qazaqstan · Kazakh AC3 / 1.0 / 48 kHz / 192 kbps \|Dub, Телеканал "Qazaqstan"\|
- rt-5555650 · Ужастика с английского · Авторский (одноголосый закадровый) "Ужастика" с английского
- rt-6831331 · Qazaqstan · Профессиональный (дублированный) Qazaqstan
- rt-6146562 · Телеканал Qazaqstan · Kazakh AC3 / 2.0 / 48 kHz / 192 kbps \|Dub, Телеканал "Qazaqstan"\|
- 3842468 · + Английский + Комментарии · Авторский (В.Назаров / Ю.Живов) + Английский (оригинал) + Комментарии (оригинал)
- rt-6438249 · телеканал Qazaqstan · Kazakh AC3 / 2.0 / 48 kHz / 192 kbps \|MVO, телеканал "Qazaqstan"\|
- 4078156 · + Английский + Комментарии · Авторский (А.Гаврилов, В.Королев) + Английский (оригинал) + Комментарии (оригинал)
- rt-6274168 · Телеканал Qazaqstan · Kazakh AC3 / 2.0 / 48 kHz / 192 kbps \|MVO, Телеканал "Qazaqstan"\|
- rt-6304693 · Телеканал Qazaqstan · Kazakh AC3 / 2.0 / 48 kHz / 192 kbps \|MVO, Телеканал "Qazaqstan"\|
- rt-6804228 · Телеканал Qazaqstan · Kazakh AC3 / 1.0 / 48 kHz / 192 kbps \|Dub, Телеканал "Qazaqstan"\|
- rt-6274165 · Телеканал Qazaqstan · Kazakh AC3 / 2.0 / 48 kHz / 192 kbps \|MVO, Телеканал "Qazaqstan"\|
- rt-6808952 · Телеканал Qazaqstan · Kazakh AC3 / 1.0 / 48 kHz / 192 kbps \|Dub, Телеканал "Qazaqstan"\|
- rt-6688661 · Qazaqstan · Профессиональный (дублированный) Qazaqstan
- rt-649606 · + английская дорожка · Профессиональный (многоголосый, закадровый) + английская дорожка
</details>
<details><summary>примеры noise:субтитры (27)</summary>
- rt-5305351 · Субтитры · Любительский многоголосый закадровый; Субтитры (см. информацию о каждом томе)
- rt-1456998 · Субтитры · Русский закадровый (одноголосый) \| Субтитры
- 6283156 · Viruse Project по субтитрам StasSlender · Любительский (многоголосый закадровый) Viruse Project по субтитрам StasSlender
- 4325644 · den904 по субтитрам Janetta · Одноголосый (закадровый) den904 по субтитрам Janetta
- rt-2421299 · Субтитры · Профессиональный (полное дублирование, XL-Media), Субтитры
- 4429717 · Doctor Joker по субтитрам romero007 · Одноголосый закадровый Doctor Joker по субтитрам romero007
- rt-5360746 · + субтитры на французскую речь · Профессиональный (дублированный) (BD CEE) + субтитры на французскую речь
- 6926094 · Субтитры · Субтитры любительский
- rt-2555358 · Субтитры · Двухголосый (Persona99 & MaxDamage;Noir & Cuba77), Субтитры
- rt-4199725 · по субтитрам Bellena · Одноголосый закадровый (iDimo) по субтитрам Bellena
- rt-3923673 · den904 по субтитрам disluo · Одноголосый закадровый den904 по субтитрам disluo
- rt-4562795 · /Andi по субтитрам Gabo/ · одноголосый закадровый /Andi по субтитрам Gabo/
- 4322131 · den904 по субтитрам Janetta · Одноголосый (закадровый) den904 по субтитрам Janetta
- rt-6198714 · allecs2010 по субтитрам Nebëhr Gudahtt · Одноголосый закадровый allecs2010 по субтитрам Nebëhr Gudahtt
- rt-6858166 · Пифагор , дальше только оригинал + субтитры · Русский: Профессиональный (дублированный) Пифагор (1-2 сезоны) , дальше только оригинал + субтитры
</details>
<details><summary>примеры noise:тип-слово (19)</summary>
- rt-2343090 · полное дублирование · полное дублирование
- rt-2754519 · полное дублирование · полное дублирование
- rt-3174375 · Дубляж · Дубляж
- 1750614 · дубляж · дубляж
- rt-2288687 · полное дублирование · полное дублирование
- rt-4245673 · полное дублирование · Профессиональный полное дублирование
- rt-3502191 · дубляж · любительский дубляж
- rt-3280195 · Дубляж · Дубляж
- rt-5809379 · Дубляж · Дубляж
- rt-3163687 · Дубляж · Дубляж (Лексикон)
- rt-1239852 · полное дублирование · полное дублирование
- rt-2500434 · дубляж · дубляж
- rt-1024963 · дубляж · дубляж (с VHS
- rt-2985347 · дубляж · дубляж
- rt-4557862 · полное дублирование · полное дублирование [ НЕВАФИЛЬМ ]
</details>
<details><summary>примеры noise:язык вместо студии (18)</summary>
- rt-6561750 · украинский · Профессиональный (дублированный), украинский (Le Doyen)
- rt-6872631 · Украинский · Профессиональный (дублированный) \| [Cinema Sound] - Украинский
- rt-6561750 · русский · Профессиональный (дублированный), русский (VSI Moscow)
- rt-5448541 · русский · Профессиональный (дублированный) русский
- rt-4486630 · Украинский · Украинский, (многоголосый закадровый)
- rt-5801324 · Украинский · Профессиональный (дублированный) Украинский
- rt-4131293 · украинский · украинский многоголосный
- rt-6923497 · Французский · Профессиональный (дублированный) Французский
- rt-521554 · русский · Профессиональный русский(многоголосый, закадровый)
- rt-5705161 · Украинский · Профессиональный (дублированный) Украинский
- rt-3502191 · украинский · украинский многоголосный
- rt-6807308 · Украинский · Профессиональный (дублированный) \| [LeDoyen] - Украинский
- rt-5415702 · украинский · Профессиональный (дублированный) украинский
- 6287332 · Казахский · Профессиональный (дублированный) Казахский
- rt-2439828 · Русский · Русский многоголосый перевод
</details>
<details><summary>примеры noise:язык-префикс «Русский, - X» (13)</summary>
- 6105068 · Украинский, AMC · Профессиональный (многоголосый) \| Украинский, AMC
- 6008543 · Украинский, R5 · Профессиональный (дублированный) \| Украинский, R5
- rt-6887455 · Русский, - HBO · Русский, двухголосый закадровый - "HBO"
- rt-6887455 · Русский, - Сыендук · Русский, одноголосый закадровый - "Сыендук" (18+)
- rt-6887455 · Русский, - HDRezka · Русский, многоголосый закадровый - "HDRezka Studio"
- rt-6449707 · Украинский, Le Doyen / Netflix · Профессиональный (дублированный) Украинский, Le Doyen / Netflix
- rt-6887455 · Украинский, - HDRezka · Украинский, многоголосый закадровый - "HDRezka Studio"
- rt-6887455 · Русский, псевдодублированный - HDRezka · Русский, псевдодублированный - "HDRezka Studio" (18+)
- rt-6450195 · Украинский, MEGOGO · Профессиональный (многоголосый закадровый) Украинский, MEGOGO
- rt-6887455 · Русский, - HDRezka · Русский, многоголосый закадровый - "HDRezka Studio" (18+)
- rt-6887455 · Русский, - TVShows · Русский, многоголосый закадровый - "TVShows" \| (серии 1-8)
- rt-6887455 · Русский, - ТО Дубляжная по заказу ТВОЁ · Русский, многоголосый закадровый - "ТО Дубляжная" по заказу «ТВОЁ»
- rt-6887455 · Украинский, - HBO · Украинский, многоголосый закадровый - "HBO"
</details>
<details><summary>примеры unlab (1887)</summary>
- 242182 · Золотая коллекция любимых мультфильмов. Выпуск 2 (Про бегемота, который боялся п · Аудио: Russian (Dolby AC3, 2 ch)
- 5501688 · Смешарики. Пин-код. Гуманитарные технологии / Сезон: 3 / Серии: 1-20 из 20 (Алек · Перевод: Не требуется
- 4262044 · Связь сердец / Kokoro Connect / Единение сердец (Онума Син) [TV+Special] [13+4 и · Перевод:
- 4094721 · Драконий Жемчуг: Фильм второй / Dragon Ball Movie 2: Sleeping Princess in Devil' · Аудио: AC3, 2/0 (L,R) ch, 192 kbps Язык Русский (Любительский одноголосый)
- 4777768 · Мангака и все-все-все / Mangaka-san to Assistant-san to The Animation / The Comi · Перевод:
- 5441268 · Принцесса-шпионка / Princess Principal [TV] [12 из 12] [RUS(int), JAP+Sub] [2017 · Аудио 1: AC3, ~192 kbps, 48000 Hz, 2 ch., язык русский (в составе контейнера); озвучка: многоголосая Торгиль, Esther, LeeAnnushka, Demetra,
- 1078722 · Бернард: выпуски 1, 2, 3 / Bernard, Bernard 2, Bernard 3 (Клаудио Бьерн Ливирья, · Перевод: Не требуется
- 4735130 · Сломанный Меч / Break Blade [TV] [12 из 12] [RUS(int), JAP+Sub] [2014, приключен · Аудио 1: AC3, ~192 kbps, 48000 Hz, 2 ch., язык русский (в составе контейнера); озвучка: двухголосая (муж.+жен.) Absurd & Eladiel
- 5182067 · Попотан / Popotan [TV] [12 из 12] [RUS, ENG, JAP] & [5-8 из 12] [Sub] [2003, ком · Аудио 3: AC-3, 192 Kbps, 2 channels Язык Русский - мужская одноголосая от Гнус
- 5046410 · Ямада и семь ведьм / Yamada-kun to 7-nin no Majo [TV] [12 из 12] [RUS(int), JAP+ · Аудио: Русская / 2 ch / AC-3 / 44,1 kHz / 192 Kbps / RUS(int) [озвучка: Persona99]
- 5950622 · Владыка духовного меча / Ling Jian Zun / Spirit Sword Sovereign / Сезон: 1-3 / С · Аудио: AC3, 48000Hz, 2ch, 128kbps, Язык Русский (в составе контейнера)
- 5019993 · Бамбуковый Клинок / Bamboo Blade (Сайто Хисаси) [TV] [26 из 26] [RUS(ext),JAP+Su · Озвучка:
- 4629918 · Навсикая из Долины ветров / Kaze no Tani no Naushika / Nausicaa of the Valley of · Аудио 5: Русский [Сонотек/Ruscico (2007 г., дубляж)] Dolby Digital Audio 2.0 / 48 kHz / 192 kbps / DN -27dB
- 1932640 · Лучшее от Загребфильм / The Best of Zagreb Film (Dušan Vukotic,Zdenko Gašparovic · Перевод: Не требуется
- 6916333 · Абдукция / Сезон: 1 / Серии: 1-12 из 12 (Анастасия Бондаренко) [2025, Россия, му · Перевод: не требуется
</details>
+73 -11
View File
@@ -6,12 +6,19 @@
Публичный API: Публичный API:
detect_tracker(url) — 'rutracker' | 'kinozal' | 'nnm' | ValueError detect_tracker(url) — 'rutracker' | 'kinozal' | 'nnm' | ValueError
fetch_html(url, ...) — скачать страницу, вернуть (html, tracker) fetch_html(url, ...) — скачать страницу, вернуть (html, tracker)
kinozal_tabs(html) — вкладки kinozal, которые грузятся JS (Релиз, …)
inline_tabs(html, t) — вписать скачанные вкладки в блок #tabs
decode_bytes(data) — байты -> текст (utf-8 -> cp1251, чинит «двойную») decode_bytes(data) — байты -> текст (utf-8 -> cp1251, чинит «двойную»)
kinozal: описание многих раздач открывается и без входа (cookie необязателен). Если трекер kinozal: описание многих раздач открывается и без входа (cookie необязателен). Если трекер
вернул форму входа, передайте cookie (`cookie="uid=...; pass=..."`) либо скачайте страницу сами вернул форму входа, передайте cookie (`cookie="uid=...; pass=..."`) либо скачайте страницу сами
(grabber уже умеет) и отдайте сервису готовый `html`. (grabber уже умеет) и отдайте сервису готовый `html`.
На kinozal во встроенном блоке #tabs только «Техданные» (там «Перевод:» без
студий), а «Релиз» со строками «Аудио N: … (MVO, LostFilm)» грузится JS:
`showtab(id, N)` -> `get_srv_details.php?id=…&pagesd=N`. fetch_html скачивает
эти вкладки и дописывает их в #tabs, чтобы описание разбиралось целиком.
Cloudflare / защита от ботов: если задан FlareSolverr (ваш собственный Cloudflare / защита от ботов: если задан FlareSolverr (ваш собственный
экземпляр, как у Jackett/Prowlarr) — `flaresolverr="http://host:8191"` или экземпляр, как у Jackett/Prowlarr) — `flaresolverr="http://host:8191"` или
переменная окружения DUB_FLARESOLVERR_URL, — то при ответе 403/503 или переменная окружения DUB_FLARESOLVERR_URL, — то при ответе 403/503 или
@@ -23,10 +30,13 @@ from __future__ import annotations
import json import json
import os import os
import re
import urllib.error import urllib.error
import urllib.request import urllib.request
from urllib.parse import urlparse from urllib.parse import urlparse
from bs4 import BeautifulSoup
from .html_extract import _fix_mojibake from .html_extract import _fix_mojibake
TRACKER_HOSTS = { TRACKER_HOSTS = {
@@ -127,32 +137,84 @@ def _fetch_flaresolverr(endpoint: str, url: str, timeout: float,
return data["solution"]["response"] return data["solution"]["response"]
def fetch_html(url: str, *, timeout: float = 15.0, cookie: str | None = None, def _get(url: str, timeout: float, cookie: str | None,
flaresolverr: str | None = None) -> tuple[str, str]: flaresolverr: str | None) -> str:
"""Скачивает страницу релиза. Возвращает (html, tracker). """Прямой запрос; при 403/503 или странице-заглушке — через FlareSolverr."""
html = None
Сначала прямой запрос; при 403/503 или странице-заглушке и заданном
FlareSolverr — повтор через него.
"""
tracker = detect_tracker(url)
flaresolverr = flaresolverr or os.environ.get("DUB_FLARESOLVERR_URL") or None
html, direct_error = None, None
try: try:
html = _fetch_direct(url, timeout, cookie) html = _fetch_direct(url, timeout, cookie)
except urllib.error.HTTPError as e: except urllib.error.HTTPError as e:
if e.code not in RETRY_CODES or not flaresolverr: if e.code not in RETRY_CODES or not flaresolverr:
raise raise
direct_error = e
if html is not None and blocked_reason(html) and flaresolverr: if html is not None and blocked_reason(html) and flaresolverr:
html = None html = None
if html is None: if html is None:
if not flaresolverr: if not flaresolverr:
raise ValueError("страница недоступна") raise ValueError("страница недоступна")
html = _fetch_flaresolverr(flaresolverr, url, timeout, cookie) html = _fetch_flaresolverr(flaresolverr, url, timeout, cookie)
return html
# «showtab(2155882,0)»; 100 — «Техданные», они уже встроены в страницу
RE_KZ_TAB = re.compile(r"showtab\((\d+)\s*,\s*(\d+)\)[^>]*>([^<]*)<")
KZ_INLINE_TAB = 100
KZ_SKIP_TABS = ("скриншот",)
def kinozal_tabs(html: str) -> list[tuple[str, int, str]]:
"""Вкладки описания kinozal, которые грузятся JS: [(id, N, название)]."""
out, seen = [], set()
for rid, n, label in RE_KZ_TAB.findall(html):
n = int(n)
if n == KZ_INLINE_TAB or n in seen or label.strip().lower().startswith(KZ_SKIP_TABS):
continue
seen.add(n)
out.append((rid, n, label.strip()))
return out
def inline_tabs(html: str, tabs: list[tuple[str, str]]) -> str:
"""Дописывает фрагменты вкладок [(название, html)] в конец блока #tabs."""
soup = BeautifulSoup(html, "lxml")
node = soup.select_one("#tabs")
if node is None or not tabs:
return html
for label, fragment in tabs:
box = soup.new_tag("div", attrs={"class": "dub-tab", "data-tab": label})
frag = BeautifulSoup(fragment, "lxml")
for child in list((frag.body or frag).contents):
box.append(child)
node.append(box)
return str(soup)
def fetch_html(url: str, *, timeout: float = 15.0, cookie: str | None = None,
flaresolverr: str | None = None) -> tuple[str, str]:
"""Скачивает страницу релиза. Возвращает (html, tracker).
Сначала прямой запрос; при 403/503 или странице-заглушке и заданном
FlareSolverr — повтор через него. Для kinozal дополнительно скачиваются
вкладки описания («Релиз», «Внимание»); недоступная вкладка пропускается.
"""
tracker = detect_tracker(url)
flaresolverr = flaresolverr or os.environ.get("DUB_FLARESOLVERR_URL") or None
html = _get(url, timeout, cookie, flaresolverr)
reason = blocked_reason(html) reason = blocked_reason(html)
if reason: if reason:
hint = "" if flaresolverr else " (задайте FlareSolverr: DUB_FLARESOLVERR_URL)" hint = "" if flaresolverr else " (задайте FlareSolverr: DUB_FLARESOLVERR_URL)"
raise ValueError(f"страница недоступна: {reason}{hint}") raise ValueError(f"страница недоступна: {reason}{hint}")
if tracker == "kinozal":
parsed = urlparse(url)
tabs = []
for rid, n, label in kinozal_tabs(html):
tab_url = f"{parsed.scheme}://{parsed.netloc}/get_srv_details.php?id={rid}&pagesd={n}"
try:
frag = _get(tab_url, timeout, cookie, flaresolverr)
except (urllib.error.URLError, ValueError, OSError):
continue
if not blocked_reason(frag):
tabs.append((label, frag))
html = inline_tabs(html, tabs)
return html, tracker return html, tracker
+18
View File
@@ -47,6 +47,13 @@ RE_SEP = re.compile(
r"\s*(?:\+|;|/)\s*(?=Проф|Люб|Авт|Дубл|Многоголос|Двухголос|Одноголос)", r"\s*(?:\+|;|/)\s*(?=Проф|Люб|Авт|Дубл|Многоголос|Двухголос|Одноголос)",
re.I) re.I)
# строка, начинающаяся со слова типа озвучки (продолжение списка под «Перевод:»)
RE_TYPE_START = re.compile(
r"^\s*(?:Профессиональн|Любительск|Авторск|Дублирован|Многоголос|Двухголос|Одноголос)", re.I)
# scene-имя релиза «A.b.c.2005.2160p.UHD.Blu-ray.Remux-GROUP»: без пробелов, ≥ 4 точек; дорожкой не бывает
RE_SCENE = re.compile(r"^[^\s.]+(?:\.[^\s.]+){4,}$") # без вложенной неоднозначности: линейно
SELECTORS = ("div.post_body", "div[id^=post-text-]", ".postbody", "#tabs") SELECTORS = ("div.post_body", "div[id^=post-text-]", ".postbody", "#tabs")
@@ -122,6 +129,17 @@ def extract_segments(text: str) -> list[str]:
if j >= len(lines): if j >= len(lines):
break break
body, i = lines[j].strip(), j + 1 body, i = lines[j].strip(), j + 1
# rutracker: «Перевод:» и под ним по строке на дорожку — берём
# подряд идущие строки, начинающиеся со слова типа
# (в HTML после <br> стоит ещё и \n исходника — строки через пустую)
while True:
k = i
while k < len(lines) and not lines[k].strip():
k += 1
if k >= len(lines) or not RE_TYPE_START.match(lines[k]) or ":" in lines[k]: # «тип: определение» — глоссарий
break
body += " ; " + lines[k].strip()
i = k + 1
else: else:
i += 1 i += 1
continue continue
+2 -4
View File
@@ -33,7 +33,7 @@ from .html_extract import (RE_LINE_PREFIX, extract_segments, html_to_text,
from .model import load_model, predict_with_lang from .model import load_model, predict_with_lang
from .studio_norm import canonical from .studio_norm import canonical
from .spans import MAX_LEN, decode_span from .spans import MAX_LEN, decode_span
from .trackinfo import lang_of_text, model_text, role_of from .trackinfo import audio_summary, lang_of_text, model_text, role_of
# мусор, который модель иногда принимает за автора: раскладка каналов, кодеки # мусор, который модель иногда принимает за автора: раскладка каналов, кодеки
@@ -166,9 +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": int(info["channels"]) if info["channels"] else None, "audio": audio_summary(info),
"bitrate_kbps": int(info["kbps"]) if info["kbps"] else None}
if info else None),
"_audio18": info["adult"] if info else None, "_audio18": info["adult"] if info else None,
}) })
return results return results
+208 -36
View File
@@ -11,15 +11,19 @@
from __future__ import annotations from __future__ import annotations
import logging
import re import re
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from .audio18 import apply_audio_18 from .audio18 import apply_audio_18
from .trackinfo import audio_lines_all, match_audio from .techinfo import tech_info
from .trackinfo import audio_lines_all, audio_summary, match_audio
from .episodes import format_ranges, merge_ranges, parse_episodes from .episodes import format_ranges, merge_ranges, parse_episodes
from .html_extract import (RE_LINE_PREFIX, _split_entries, extract_segments, from .html_extract import (RE_LINE_PREFIX, RE_TYPE_START, _split_entries,
html_to_text) extract_segments, html_to_text)
log = logging.getLogger(__name__)
# аббревиатуры типа озвучки в заголовках раздач -> фраза, понятная модели # аббревиатуры типа озвучки в заголовках раздач -> фраза, понятная модели
TITLE_TYPES = { TITLE_TYPES = {
@@ -43,7 +47,9 @@ def title_from_html(html: str) -> str:
for sel in ("h1", "title"): for sel in ("h1", "title"):
node = soup.select_one(sel) node = soup.select_one(sel)
if node and node.get_text(strip=True): if node and node.get_text(strip=True):
return re.sub(r"\s+", " ", node.get_text(" ", strip=True)) # без разделителя: rutracker оборачивает каждое русское слово в <span> и режет <wbr>
# («Галактическо<wbr>е»), пробелы между словами — отдельные текстовые узлы
return re.sub(r"\s+", " ", node.get_text()).strip()
return "" return ""
@@ -136,8 +142,8 @@ def aggregate(tracks: list[dict]) -> dict:
uniq = rest uniq = rest
# дорожка без студии, при наличии дорожки того же типа со студией, — как # дорожка без студии, при наличии дорожки того же типа со студией, — как
# правило, та же дорожка в виде кодек-строки («AC3 … | MVO, AlexFilm») # правило, та же дорожка в виде кодек-строки («AC3 … | MVO, AlexFilm»)
typed = {t["translation_type"] for t in uniq if t.get("studio")} typed = {(t["translation_type"], t.get("lang")) 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"], t.get("lang")) not in typed]
studios = [{"studio": t["studio"], "translation_type": t["translation_type"], studios = [{"studio": t["studio"], "translation_type": t["translation_type"],
"lang": t.get("lang"), "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")]
@@ -149,14 +155,11 @@ def aggregate(tracks: list[dict]) -> dict:
} }
def original_info(lines: list[dict]) -> dict | None: def original_info(lines: list[dict]) -> dict | None:
"""Оригинальная дорожка из строки «Аудио … |Original|» (без студии и комментариев); нет — None.""" """Оригинальная дорожка из строки «Аудио … |Original|» (без студии и комментариев); нет — None."""
for ln in lines: for ln in lines:
if ln["original"] and not ln["names"] and not ln["commentary"]: if ln["original"] and not ln["names"] and not ln["commentary"]:
return {"lang": ln["lang"] or None, "codec": ln["codec"] or None, return {"lang": ln["lang"] or None, **audio_summary(ln), "raw": ln["raw"]}
"channels": int(ln["channels"]) if ln["channels"] else None,
"bitrate_kbps": int(ln["kbps"]) if ln["kbps"] else None, "raw": ln["raw"]}
return None return None
@@ -170,7 +173,7 @@ def lostfilm_release(title: str | None = None) -> dict:
"source": "url", "episode_ranges": [], "episodes": None} "source": "url", "episode_ranges": [], "episodes": None}
result = aggregate([track]) result = aggregate([track])
result.update(original=None, series=series_info(title or "", result["tracks"]), result.update(original=None, series=series_info(title or "", result["tracks"]),
title=title or None, segmenter="rules") **tech_info("", title or ""), title=title or None, segmenter="rules")
return result return result
@@ -198,17 +201,45 @@ def series_info(title: str, tracks: list[dict]) -> dict | None:
if t.get("studio") and not t.get("episodes")})} if t.get("studio") and not t.get("episodes")})}
def merge_title_tracks(tracks: list[dict], title_tracks: list[dict]) -> None: def _studio_tokens(name: str) -> set[str]:
"""Добавляет дорожки из заголовка к дорожкам страницы (на месте). return {w for w in re.findall(r"\w+", name.casefold()) if len(w) >= 4}
* студия уже есть на странице -> пропуск;
* на странице есть дорожка того же типа без студии -> студия берётся def same_studio(a: str | None, b: str | None) -> bool:
из заголовка (source=page+title): это та же дорожка; """Одна студия в разном написании: «DVD» ~ «DVD Магия», «НТВ» ~ «НТВ+»,
* иначе -> новая дорожка (source=title). «Михаил Яроцкий» ~ «М.Яроцкий/Kyberpunk» (общее слово ≥ 4 букв)."""
a, b = (a or "").casefold().strip(), (b or "").casefold().strip()
if not a or not b:
return False
return a in b or b in a or bool(_studio_tokens(a) & _studio_tokens(b))
def merge_extra_tracks(tracks: list[dict], extra: list[dict], source: str,
match=lambda a, b: (a or "").casefold() == (b or "").casefold(),
adopt_type: bool = False) -> None:
"""Добавляет дорожки второстепенного источника к дорожкам страницы (на месте).
* такая студия уже есть (по `match`) -> пропуск;
* есть дорожка того же типа без студии -> студия вписывается в неё
(source=page+<source>): это та же дорожка;
* иначе -> новая дорожка (source=<source>).
Дорожки без студии не берутся: тип без студии страница уже даёт, а в
кодек-строках он часто грубее («одноголосый» вместо «авт. одноголосый»).
Дорожка с типом «неизвестно» («Аудио Rus: AC3 … | LostFilm») берётся
только при `adopt_type` и если у страницы ровно один тип без студии
(«Перевод: Профессиональный многоголосый»): она получает этот тип.
""" """
for t in title_tracks: bare = {x["translation_type"] for x in tracks
name = (t.get("studio") or "").casefold() if not x.get("studio") and x["translation_type"] != UNKNOWN_TYPE}
if any((x.get("studio") or "").casefold() == name for x in tracks): for t in extra:
if not t.get("studio"):
continue
if t["translation_type"] == UNKNOWN_TYPE:
if not (adopt_type and len(bare) == 1):
continue
t["translation_type"] = next(iter(bare))
if any(match(x.get("studio"), t["studio"]) for x in tracks):
continue continue
same = next((x for x in tracks if not x.get("studio") same = next((x for x in tracks if not x.get("studio")
and x["translation_type"] == t["translation_type"]), None) and x["translation_type"] == t["translation_type"]), None)
@@ -216,26 +247,145 @@ def merge_title_tracks(tracks: list[dict], title_tracks: list[dict]) -> None:
same.update(studio=t["studio"], studio_span=t["studio_span"], same.update(studio=t["studio"], studio_span=t["studio_span"],
studio_confidence=t["studio_confidence"], studio_confidence=t["studio_confidence"],
is_18plus=same["is_18plus"] or t["is_18plus"], is_18plus=same["is_18plus"] or t["is_18plus"],
source="page+title") source=f"page+{source}")
else: else:
t["source"] = "title" t["source"] = source
tracks.append(t) tracks.append(t)
def model_segments(segmenter, page_text: str) -> list[str]: def merge_title_tracks(tracks: list[dict], title_tracks: list[dict]) -> None:
"""Дорожки из заголовка («ПМ (LostFilm)») -> к дорожкам страницы."""
merge_extra_tracks(tracks, title_tracks, "title")
# кодек-строка: «Аудио 3: AC3 …», «Аудио Rus: …», «Аудиодорожка 1: …», «Audio #2: …»
RE_AUDIO_PREFIX = re.compile(r"(?i)^\s*(?:Аудио\w*|Audio)\s*(?:№|#)?\s*(?:\d+|rus|ukr|eng)?\s*[:.]")
# с типом озвучки: «Аудио 3: AC3 … (MVO, HDrezka Studio)»; «English Dubbing» — не тип
RE_CODEC_TRACK = re.compile(
r"(?i)^\s*(?:Аудио|Audio)\s*(?:№|#)?\s*\d*\s*:.*(?:\b(?:MVO|DVO|AVO|DUB|VO)\b"
r"|дубл|многоголос|двухголос|одноголос|закадр|авторск)")
RE_RU_TRACK = re.compile(r"(?i)русск|russian|\brus\b|украин|ukrain|\bukr\b")
RE_FOREIGN_TRACK = re.compile(
r"(?i)казах|english|англ|француз|немец|немецк|итальян|испан|японск|корейск|китайск"
r"|\boriginal\b|оригинал|\beng\b")
def codec_ok(line: str) -> bool:
"""Кодек-строка русской (украинской) дорожки: «Казахский (Dub, TK Qazaqstan)»,
«English Dubbing», «Original» — нет."""
if RE_RU_TRACK.search(line):
return True
return not RE_FOREIGN_TRACK.search(line) and bool(RE_CODEC_TRACK.match(line))
def _entries(line: str) -> list[str]:
body = RE_LINE_PREFIX.sub("", line, count=1)
return [e for e in (re.sub(r"\s+", " ", x).strip() for x in _split_entries(body)) if e]
def codec_segments(page_text: str) -> list[str]:
"""Кодек-строки с типом озвучки («Аудио 2: AC3 … (MVO, LostFilm)») по правилу.
Запасной путь к тому, что выбирает сегментатор (`model_segments`): старые
веса сегментатора кодек-строки отбрасывают. Они нужны там, где студии есть
только в них (kinozal: вкладка «Релиз» против «Перевод:» без студий в
«Техданных»), и идут второстепенным источником, см. `merge_extra_tracks`.
"""
out = []
for line in (page_text or "").splitlines():
if RE_CODEC_TRACK.match(line) and codec_ok(line):
out += _entries(line)
return out
def complete_block(selected: list[str], page_text: str) -> list[str]:
"""Достраивает список дорожек, из которого модель выбрала не все строки.
Сегментатор оценивает строки по отдельности и может взять две из четырёх
строк списка под «Перевод:». Соседние с выбранной строки, начинающиеся со
слова типа озвучки, добавляются в тот же список; строки вдали от выбранных
не трогаются (глоссарии типов правилами не подхватываются).
"""
lines = [re.sub(r"\s+", " ", l).strip() for l in (page_text or "").splitlines()]
lines = [l for l in lines if l]
chosen = set(selected)
idx = [i for i, l in enumerate(lines) if l in chosen]
add = set()
for step in (1, -1): # вниз и вверх от каждой выбранной
for i in idx:
j = i + step
while 0 <= j < len(lines) and lines[j] not in chosen and RE_TYPE_START.match(lines[j]) and not RE_AUDIO_PREFIX.match(lines[j]):
add.add(j)
j += step
return [l for i, l in enumerate(lines) if l in chosen or i in add]
def model_segments(segmenter, page_text: str) -> tuple[list[str], list[str]]:
"""Дорожки страницы: строки выбирает обученный сегментатор. """Дорожки страницы: строки выбирает обученный сегментатор.
Возвращает (основные, кодек): строки «Перевод/Озвучка …» и кодек-строки
«Аудио N: AC3 … (MVO, LostFilm)». Кодек-строки обычно дублируют основные
в другом написании, поэтому они второстепенный источник (`predict_release`);
если основных нет (nnm: дорожки только в «Аудио N:») — они и есть дорожки.
Нарезка внутри строки («Дублированный, многоголосый») в данных не Нарезка внутри строки («Дублированный, многоголосый») в данных не
размечена и пока делается правилами `_split_entries`. размечена и пока делается правилами `_split_entries`.
""" """
out = [] main, codec = [], []
for line in segmenter.select_lines(page_text): for line in complete_block(segmenter.select_lines(page_text), page_text):
body = RE_LINE_PREFIX.sub("", line, count=1) if not RE_AUDIO_PREFIX.match(line):
for entry in _split_entries(body): main += _entries(line)
entry = re.sub(r"\s+", " ", entry).strip() elif codec_ok(line):
if entry: codec += _entries(line)
out.append(entry) return main, codec
return out
def rule_segments(page_text: str) -> list[str]:
"""Правила по строкам без «Аудио»: кодек-строки модельный путь отбирает сам (`codec_ok`)."""
kept = [l for l in (page_text or "").splitlines() if not RE_AUDIO_PREFIX.match(l)]
return extract_segments("\n".join(kept))
def segmenter_warning(model_on: bool, used: str, segs_page: list[str],
rule_segs: list[str]) -> str | None:
"""Сегментатор расходится с правилами — кандидат на дообучение.
Возвращает текст предупреждения или None. «Нашёл меньше правил» не всегда
ошибка модели (правила дают ложные срабатывания на глоссариях), но такие
страницы стоит просмотреть и добавить в обучение.
"""
if not model_on:
return None
if used == "rules" and rule_segs:
return f"модель не выбрала строк, дорожки по правилам ({len(rule_segs)})"
have = {re.sub(r"\s+", " ", x).strip() for x in segs_page}
missed = [x for x in rule_segs if x not in have]
if used == "model" and missed:
return f"правила нашли дорожек больше, чем модель: +{len(missed)} ({missed[0][:60]})"
return None
def fill_audio(predictor, tracks: list[dict], lines: list[dict]) -> None:
"""Строки «Аудио N: … |студия|» -> к дорожкам (на месте).
* у дорожки нет `audio` (студию дали кодек-строка или заголовок) -> берём строку по студии;
* строка с типом озвучки, но без студии («Ukrainian AC3 … |Dub|»), и такого языка среди
дорожек ещё нет -> новая дорожка (source=codec): тип без студии страница не даёт.
"""
for t in tracks:
if not t.get("audio") and t.get("studio"):
info = next(iter(match_audio(t["studio"], lines)), None)
if info:
t["audio"] = audio_summary(info)
have = {t.get("lang") for t in tracks}
for info in lines:
if (info["names"] or info["original"] or info["commentary"] or not info["lang"] or info["lang"] in have
or not RE_CODEC_TRACK.match("Аудио: " + info["raw"]) or not codec_ok(info["raw"])):
continue
t = predictor._predict_batch([info["raw"]], [info])[0]
t.update(source="codec", episode_ranges=[], episodes=None)
tracks.append(t)
have.add(info["lang"])
def predict_release(predictor, *, html: str | None = None, text: str | None = None, def predict_release(predictor, *, html: str | None = None, text: str | None = None,
@@ -246,17 +396,19 @@ def predict_release(predictor, *, html: str | None = None, text: str | None = No
(если он загружен и нашёл хотя бы одну строку), иначе правила; "rules" — (если он загружен и нашёл хотя бы одну строку), иначе правила; "rules" —
только правила. только правила.
Заголовок раздачи используется как дополнительный источник: студии, Кодек-строки («Аудио N: … (MVO, LostFilm)») и заголовок раздачи —
названные только там («ПМ (LostFilm)»), добавляются как дорожки с дополнительные источники: студии, названные только там, добавляются как
пометкой source=title. дорожки с пометкой source=codec / source=title.
""" """
if (html is None) == (text is None): if (html is None) == (text is None):
raise ValueError("нужно ровно одно из: html, text") raise ValueError("нужно ровно одно из: html, text")
page_text = html_to_text(html) if html is not None else text page_text = html_to_text(html) if html is not None else text
segs_page, used = [], "rules" segs_page, segs_codec, used = [], [], "rules"
seg_model = getattr(predictor, "segmenter", None) seg_model = getattr(predictor, "segmenter", None)
if segmenter == "model" and seg_model is not None: if segmenter == "model" and seg_model is not None:
segs_page = model_segments(seg_model, page_text) segs_page, segs_codec = model_segments(seg_model, page_text)
if not segs_page:
segs_page, segs_codec = segs_codec, []
used = "model" if segs_page else "rules" used = "model" if segs_page else "rules"
if not segs_page: if not segs_page:
segs_page = extract_segments(page_text) segs_page = extract_segments(page_text)
@@ -272,17 +424,37 @@ def predict_release(predictor, *, html: str | None = None, text: str | None = No
ep = parse_episodes(t["segment"]) ep = parse_episodes(t["segment"])
t["episode_ranges"] = ep["ranges"] t["episode_ranges"] = ep["ranges"]
t["episodes"] = format_ranges(ep["ranges"]) or None t["episodes"] = format_ranges(ep["ranges"]) or None
# кодек-строки — только если страница не назвала студию хотя бы у одной
# дорожки (kinozal: «Перевод: многоголосый» + «Аудио 3: … (MVO, LostFilm)»);
# иначе они дублируют «Перевод N:» в другом написании и дают ложные студии
seen = set(segs_page)
segs_codec = list(dict.fromkeys(x for x in segs_codec + codec_segments(page_text)
if x not in seen))
if segs_codec and (not tracks or any(not t.get("studio") for t in tracks)):
codec = predictor._predict_batch(segs_codec)
for t in codec:
ep = parse_episodes(t["segment"])
t["episode_ranges"] = ep["ranges"]
t["episodes"] = format_ranges(ep["ranges"]) or None
merge_extra_tracks(tracks, codec, "codec", match=same_studio, adopt_type=True)
if title is None: if title is None:
title = title_from_html(html) if html is not None else title_from_text(text) title = title_from_html(html) if html is not None else title_from_text(text)
segs = title_segments(title or "") segs = title_segments(title or "")
if segs: if segs:
merge_title_tracks(tracks, predictor._predict_batch(segs)) merge_title_tracks(tracks, predictor._predict_batch(segs))
fill_audio(predictor, tracks, lines)
result = aggregate(tracks) result = aggregate(tracks)
warn = segmenter_warning(seg_model is not None and segmenter == "model", used,
segs_page + segs_codec, rule_segments(page_text))
if warn:
log.warning("segmenter: %s", warn)
result["segmenter_warning"] = warn
result["original"] = original_info(lines) 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)
result["series"] = series_info(title or "", result["tracks"]) result["series"] = series_info(title or "", result["tracks"])
result.update(tech_info(page_text, title or ""))
result["title"] = title or None result["title"] = title or None
result["segmenter"] = used result["segmenter"] = used
return result return result
+86 -12
View File
@@ -11,8 +11,10 @@ Python API:
seg = LineSegmenter("models/segmenter") seg = LineSegmenter("models/segmenter")
seg.select_lines(page_text) -> list[str] # строки-дорожки (префикс не снят) seg.select_lines(page_text) -> list[str] # строки-дорожки (префикс не снят)
Разметка: строка страницы положительна, если содержит `raw` строки датасета Разметка: строка страницы положительна, если содержит `raw` (или `audio_raw`)
этого релиза (или наоборот). Положительные примеры добавляются синтетически: строки датасета этого релиза (или наоборот); неразмеченные кодек-строки русских
дорожок в обучение не идут (см. `label_page`). Текст kinozal собирается из
data/raw/kinozal/*.json (`kinozal_text`). Положительные примеры добавляются синтетически:
`raw` всех train-строк датасета (включая nnm/kinozal, у которых нет страниц) `raw` всех train-строк датасета (включая nnm/kinozal, у которых нет страниц)
вставляются в контекст реальных строк страниц. Нарезка ВНУТРИ строки вставляются в контекст реальных строк страниц. Нарезка ВНУТРИ строки
(«Дублированный, многоголосый») в данных не размечена и остаётся на правилах. («Дублированный, многоголосый») в данных не размечена и остаётся на правилах.
@@ -32,7 +34,7 @@ import numpy as np
import torch import torch
from transformers import AutoModelForSequenceClassification, AutoTokenizer from transformers import AutoModelForSequenceClassification, AutoTokenizer
from .html_extract import read_input from .html_extract import RE_SCENE, read_input
BACKBONE = "cointegrated/rubert-tiny2" BACKBONE = "cointegrated/rubert-tiny2"
MAX_LEN = 160 MAX_LEN = 160
@@ -69,8 +71,34 @@ def page_lines(text: str) -> list[str]:
# ---------------------------------------------------------------- данные # ---------------------------------------------------------------- данные
RAW_KINOZAL = Path("data/raw/kinozal")
def kinozal_text(release_id: str, raw_dir: Path = RAW_KINOZAL) -> str | None:
"""Текст страницы kinozal так, как его видит сервис: заголовок + «Техданные»
(«Перевод: …») + «Релиз» («Аудио N: … (MVO, LostFilm)»).
В data/pages/kz-*.txt нет ни заголовка, ни «Техданных», а метки kinozal
наполовину взяты именно оттуда, поэтому для kinozal берётся raw JSON.
"""
path = raw_dir / f"{str(release_id).removeprefix('kz-')}.json"
if not path.exists():
return None
j = json.loads(path.read_text(encoding="utf-8"))
return "\n".join(x for x in (j.get("h1"), j.get("tech_tab"), j.get("release_tab")) if x)
def page_text(path: Path) -> str:
"""Текст сохранённой страницы релиза (kinozal — из raw JSON, если он есть)."""
if path.stem.startswith("kz-"):
text = kinozal_text(path.stem, path.parent.parent / "raw" / "kinozal")
if text:
return text
return read_input(path)
def load_pages(pages_dir: Path = Path("data/pages")) -> dict[str, list[str]]: def load_pages(pages_dir: Path = Path("data/pages")) -> dict[str, list[str]]:
return {p.stem: page_lines(read_input(p)) for p in sorted(pages_dir.glob("*.txt"))} return {p.stem: page_lines(page_text(p)) for p in sorted(pages_dir.glob("*.txt"))}
def load_raws(csv_path: Path = Path("data/labels.csv")) -> list[dict]: def load_raws(csv_path: Path = Path("data/labels.csv")) -> list[dict]:
@@ -78,12 +106,53 @@ def load_raws(csv_path: Path = Path("data/labels.csv")) -> list[dict]:
return list(csv.DictReader(f)) return list(csv.DictReader(f))
# кодек-строка русской/украинской дорожки: «Аудио 2: AC3 … Русский (MVO, X)»,
# «Аудио Rus: AC3 … | LostFilm», «Аудиодорожка 1: … | Russian | TVShows»;
# сводка «Аудио: Русский, английский (AC3 …)» без номера — не дорожка
RE_AUDIO_TRACK = re.compile(
r"(?i)^\s*(?:(?:Аудио|Audio)\s*(?:rus|ukr)\s*[:.]"
r"|(?:Аудиодорожка\s*\d*|(?:Аудио|Audio)\s*(?:№|#)?\s*\d+)\s*[:.]"
r".*(?:русск|russian|\brus\b|украин|ukrain|\bukr\b))")
RE_TYPE_WORD = re.compile(
r"(?i)\b(?:MVO|DVO|AVO|DUB|VO)\b|дубл|многоголос|двухголос|одноголос|закадр|авторск")
# технические слова кодек-строки: всё, что после их удаления осталось, — имя студии/автора
RE_TECH_WORD = re.compile(
r"(?i)^(?:аудио\w*|audio|e?-?ac-?3|eac3|ac|dts|hd|ma|mpeg|mp3|aac|lc|opus|flac|pcm|lpcm|truehd"
r"|atmos|joc|dolby|digital|surround|stereo|mono|cbr|vbr|avg|constant|core|bit|ch|сh|channels?"
r"|каналов|канала|kbps|kb|кбит|бит|сек|khz|кгц|hz|lfe|dn|db|rus|ukr|eng|russian|русск\w*"
r"|украин\w*|english|англ\w*|l|c|r|sl|sr|ls|rs|lr|cbr|a)$")
def _informative(line: str) -> bool:
"""Кодек-строка называет тип озвучки или студию/автора, а не только параметры.
«Аудио 1: AC3, 6 ch, 384 kbps - Русский» — нет; «… | LostFilm»,
«… - Русский, Юрий Живов», «… (MVO)» — да.
"""
if RE_TYPE_WORD.search(line):
return True
body = re.sub(r"(?i)^\s*(?:Аудио\w*|Audio)\s*(?:№|#)?\s*(?:\d+|rus|ukr)?\s*[:.]", "", line)
words = [w for w in re.findall(r"[^\W\d_]+", body) if not RE_TECH_WORD.match(w)]
return any(len(w) >= 3 for w in words)
def label_page(lines: list[str], raws: list[str]) -> list[int]: def label_page(lines: list[str], raws: list[str]) -> list[int]:
"""1, если строка содержит raw (или raw содержит строку ≥ 15 символов).""" """1 — строка содержит raw (или raw содержит строку ≥ 15 символов);
-1 — неразмеченная кодек-строка русской дорожки с типом или студией: в
обучение не идёт; пустая кодек-строка («AC3, 6 ch, 384 kbps - Русский») — 0.
Кодек-строка «Аудио N: … (MVO, LostFilm)» — дорожка, но у rutracker
`raw` — строка «Перевод N:», а кодек-строка в разметку не попадает. Как
негатив она учила модель отбрасывать такие строки (на kinozal студии
только в них), поэтому неразмеченные кодек-строки пропускаются.
"""
out = [] out = []
for line in lines: for line in lines:
hit = any(r and (r in line or (len(line) >= 15 and line in r)) for r in raws) hit = any(r and (r in line or (len(line) >= 15 and line in r)) for r in raws)
out.append(int(hit)) skip = RE_AUDIO_TRACK.match(line) and _informative(line)
out.append(1 if hit else (-1 if skip else 0))
return out return out
@@ -93,7 +162,10 @@ def build_examples(split: str, rng: random.Random, synth_per_page: int = 6) -> l
rows = load_raws() rows = load_raws()
raws_by_rel: dict[str, list[str]] = {} raws_by_rel: dict[str, list[str]] = {}
for r in rows: for r in rows:
raws_by_rel.setdefault(r["release_id"], []).append(norm(r["raw"])) # audio_raw — кодек-строка той же дорожки («AC3 … Русский (MVO, X)»), тоже позитив
for text in (r["raw"], r.get("audio_raw")):
if norm(text):
raws_by_rel.setdefault(r["release_id"], []).append(norm(text))
want_test = split == "test" want_test = split == "test"
page_items = [] # (release_id, lines, labels) page_items = [] # (release_id, lines, labels)
@@ -105,7 +177,7 @@ def build_examples(split: str, rng: random.Random, synth_per_page: int = 6) -> l
# все raw train-релизов (без тестовых страниц) — материал для синтетики # все raw train-релизов (без тестовых страниц) — материал для синтетики
test_pos = {l for rid, lines in pages.items() if is_test_release(rid) test_pos = {l for rid, lines in pages.items() if is_test_release(rid)
for l, y in zip(lines, label_page(lines, raws_by_rel.get(rid, []))) if y} for l, y in zip(lines, label_page(lines, raws_by_rel.get(rid, []))) if y == 1}
pool = sorted({norm(r["raw"]) for r in rows pool = sorted({norm(r["raw"]) for r in rows
if r["raw"] and not is_test_release(r["release_id"]) if r["raw"] and not is_test_release(r["release_id"])
and norm(r["raw"]) not in test_pos}) and norm(r["raw"]) not in test_pos})
@@ -113,13 +185,15 @@ def build_examples(split: str, rng: random.Random, synth_per_page: int = 6) -> l
ex: list[tuple] = [] ex: list[tuple] = []
for rid, lines, labels in page_items: for rid, lines, labels in page_items:
for i, (line, y) in enumerate(zip(lines, labels)): for i, (line, y) in enumerate(zip(lines, labels)):
if y < 0:
continue # неразмеченная кодек-строка: ни позитив, ни негатив
prev = lines[i - 1] if i else "" prev = lines[i - 1] if i else ""
nxt = lines[i + 1] if i + 1 < len(lines) else "" nxt = lines[i + 1] if i + 1 < len(lines) else ""
if not y and not HINT.search(line) and rng.random() > 0.15: if not y and not HINT.search(line) and not RE_SCENE.match(line) and rng.random() > 0.15:
continue # простые негативы прореживаем, трудные (с намёками) берём все continue # простые негативы прореживаем, трудные (с намёками, scene-имена релизов) берём все
ex.append((*make_input(prev, line, nxt), y)) ex.append((*make_input(prev, line, nxt), y))
if not want_test: if not want_test:
negs = [l for l, y in zip(lines, labels) if not y] negs = [l for l, y in zip(lines, labels) if y == 0]
for _ in range(synth_per_page if negs else 0): for _ in range(synth_per_page if negs else 0):
raw = rng.choice(pool) raw = rng.choice(pool)
p, n = rng.choice(negs), rng.choice(negs) p, n = rng.choice(negs), rng.choice(negs)
@@ -157,7 +231,7 @@ class LineSegmenter:
if not lines: if not lines:
return [] return []
probs = self.score(lines) probs = self.score(lines)
return [l for l, p in zip(lines, probs) if p >= threshold] return [l for l, p in zip(lines, probs) if p >= threshold and not RE_SCENE.match(l)]
# ---------------------------------------------------------------- обучение # ---------------------------------------------------------------- обучение
+268
View File
@@ -0,0 +1,268 @@
"""Техданные релиза помимо озвучки: видео, тип релиза, контейнер, продолжительность, субтитры.
Только правила по строкам страницы («Видео: …», «Тип релиза: …», «Субтитры: …») и по
заголовку раздачи — нейросеть здесь не нужна, форматы у трекеров устойчивые.
video_info(text, title) -> {codec, width, height, resolution, fps, bitrate_kbps, bit_depth, hdr, dv_profile, raw} | None
release_info(text, title) -> {quality, source, container, duration, duration_sec} | None
subtitles_info(text, title) -> {languages, tracks, format, hardsub, raw} | None
tech_info(text, title) -> {"video": …, "release": …, "subtitles": …}
"""
from __future__ import annotations
import re
from .trackinfo import LANG_CODES, lang_of_text
# строка техданных «Ключ: значение»; ключ — в начале строки, точки-заполнители MediaInfo допустимы
def _field(text: str, key: str) -> list[str]:
rx = re.compile(rf"(?im)^[^\S\n]*(?:{key})[^\S\n.]*\.*[^\S\n]*:[^\S\n]*(?P<v>[^\n]*)$")
# nnm пишет несколько полей в одну строку через табуляцию или длинный пробел:
# «Отсутствуют Дата мировой премьеры: …» — значение до такого разрыва
return [re.split(r"\t| {3,}", m.group("v").strip(), maxsplit=1)[0].strip()
for m in rx.finditer(text or "")]
def _first(text: str, *keys: str) -> str:
for key in keys:
for v in _field(text, key):
if v:
return v
return ""
# ------------------------------------------------------------------ видео
VIDEO_CODECS = [ # (шаблон, каноническое имя); порядок важен: HEVC раньше MPEG-4
(r"HEVC|H\.?265|x265", "HEVC"),
(r"\bAV1\b", "AV1"),
(r"\bVP9\b", "VP9"),
(r"\bAVC\b|H\.?264|x264", "AVC"),
(r"VC-?1", "VC-1"),
(r"MPEG-?2|MPEG\s*Video\s*2", "MPEG-2"),
(r"XviD", "XviD"),
(r"DivX", "DivX"),
(r"MPEG-?4", "MPEG-4"),
]
RE_SIZE = re.compile(r"(?<!\d)(\d{3,4})\s*[xх×*]\s*(\d{3,4})(?!\d)")
RE_PLINE = re.compile(r"(?i)(?<!\d)(2160|1440|1080|720|576|480)[pi]\b")
RE_FPS = re.compile(r"(?i)(?<![\d.,])(\d{2,3}(?:[.,]\d{1,6})?)\s*(?:fps|кадр)")
# «8 200 Кбит/с» — пробел как разделитель тысяч, но не «640x360 1494 kb/s» (высота кадра перед числом)
RE_RATE = re.compile(r"(?i)(?<![\d.,xх×*])(?<![xх×*] )(\d{1,3}(?: \d{3})+|\d+)((?:[.,]\d+)?)\s*([kкmм])\s*(?:bps|bit|бит|b/[sсc])")
RE_DEPTH = re.compile(r"(?i)(?<![\d.,])(8|10|12)\s*-?\s*(?:bits?|бит)(?!\s*/)")
RE_DV_PROFILE = re.compile(r"(?i)dolby\s*vision[^/\n]{0,15}?profile\s*(\d+(?:\.\d+)?)|dvhe\.0?(\d+)")
# сокращение в заголовке: «DV 8.1», «DV P7», «DoVi P5»; только реальные профили, «DV 10-bit» — не профиль
RE_DV_SHORT = re.compile(r"(?i)\b(?:DV|DoVi|Dolby\s*Vision)\s*(?:P|profile\s*)?([45789](?:\.[1-4])?)\b(?!\s*-?\s*bit)")
HDR_FLAGS = [ # (шаблон, флаг) — что нашли, в этом порядке и отдаём
(r"dolby\s*vision|\bDoVi\b|\bDV\b", "Dolby Vision"), # «DV» из заголовка; \b не цепляет DVD/DVDRip
(r"HDR10\+|HDR10\s*plus", "HDR10+"),
(r"HDR10(?!\+|\s*plus)|SMPTE\s*ST\s*2086", "HDR10"),
(r"\bHLG\b", "HLG"),
]
RE_HDR_ANY = re.compile(r"(?i)\bHDR\b")
def _rate_kbps(s: str) -> int | None:
m = RE_RATE.search(s or "")
if not m:
return None
whole, frac, unit = m.group(1).replace(" ", ""), m.group(2), m.group(3).lower()
# «24,8 Mbps» — десятичная запятая; «1.050 kbps» — разделитель тысяч
if unit in "kк" and re.fullmatch(r"[.,]\d{3}", frac) and len(whole) <= 3:
whole, frac = whole + frac[1:], ""
val = float(whole + frac.replace(",", "."))
# «5763 Mбит/с», «16000 Mbps» — на деле килобиты: видео > 1 Гбит/с не бывает
return round(val * 1000) if unit in "mм" and val < 1000 else round(val)
def _resolution(width: int | None, height: int | None, line: str) -> str | None:
"""Класс разрешения: по ширине (кадр с полосами 1920x800 — всё равно 1080p), иначе «1080p» из текста."""
if width:
for w, name in ((3800, "2160p"), (2500, "1440p"), (1900, "1080p"), (1200, "720p")):
if width >= w:
return name
return f"{height}p" if height else None
m = RE_PLINE.search(line)
return f"{m.group(1)}p" if m else None
def hdr_flags(*texts: str) -> list[str]:
joined = "\n".join(t for t in texts if t)
out = [flag for rx, flag in HDR_FLAGS if re.search(rx, joined, re.I)]
if not out and RE_HDR_ANY.search(joined):
out.append("HDR")
return out
def video_info(text: str, title: str = "") -> dict | None:
"""Первая строка «Видео:»/«Video:» (MediaInfo-ключи nnm — запасной путь); нет — None."""
line = _first(text, r"Видео(?:\s*\d+)?", r"Video(?:\s*\d+)?")
extra = " ".join(x for x in (_first(text, r"Видео\s*кодек", r"Видеокодек"),
_first(text, r"Размер\s+кадра"),
_first(text, r"Битрейт\s+видео")) if x)
if not line and not extra:
return None
src = line or extra
codec = next((name for rx, name in VIDEO_CODECS if re.search(rx, src, re.I)), None)
if codec is None and line and extra:
codec = next((name for rx, name in VIDEO_CODECS if re.search(rx, extra, re.I)), None)
size = RE_SIZE.search(src) or RE_SIZE.search(extra)
width, height = (int(size.group(1)), int(size.group(2))) if size else (None, None)
fps = RE_FPS.search(src)
depth = RE_DEPTH.search(src) or RE_DEPTH.search(title or "") # nnm: «10-bit» бывает только в заголовке
dv = (RE_DV_PROFILE.search(src) or RE_DV_PROFILE.search("\n".join(_field(text, r"HDR\s*format")))
or RE_DV_SHORT.search(src) or RE_DV_SHORT.search(title or ""))
hdr = hdr_flags(src, *_field(text, r"HDR\s*format"), release_quality(text), title)
return {
"codec": codec,
"width": width,
"height": height,
"resolution": _resolution(width, height, src + " " + release_quality(text) + " " + (title or "")),
"fps": float(fps.group(1).replace(",", ".")) if fps else None,
"bitrate_kbps": _rate_kbps(src) or _rate_kbps(_first(text, r"Битрейт\s+видео")),
"bit_depth": int(depth.group(1)) if depth else None,
"hdr": hdr,
"dv_profile": (dv.group(1) or dv.group(2)) if dv else None,
"raw": src,
}
# ------------------------------------------------------------------ тип релиза
SOURCES = [ # (шаблон, тег); порядок важен: Remux раньше Rip, Rip раньше DL
(r"UHD\s*BD\s*-?Remux|UHD\s*Blu-?ray\s*Remux", "UHD BDRemux"),
(r"BD-?Remux|Blu-?ray\s*Remux|REMUX", "BDRemux"),
(r"WEB-?DL\s*-?Rip", "WEB-DLRip"),
(r"WEB-?DL", "WEB-DL"),
(r"WEB-?Rip", "WEBRip"),
(r"BD-?Rip|Blu-?ray\s*Rip", "BDRip"),
(r"HD-?Rip", "HDRip"),
(r"UHD\s*Blu-?ray|Blu-?ray\s*Disc|\bBD(?:25|50|66|100)\b|\bBlu-?ray\b", "Blu-ray"),
(r"DVD-?Rip", "DVDRip"),
(r"\bDVD-?9\b", "DVD9"),
(r"\bDVD-?5\b", "DVD5"),
(r"HDTV-?Rip", "HDTVRip"),
(r"HDTV", "HDTV"),
(r"SAT-?Rip", "SATRip"),
(r"\bTV-?Rip\b", "TVRip"),
(r"DVB", "DVB"),
(r"VHS-?Rip|VHS", "VHSRip"),
(r"DCP-?Rip", "DCPRip"),
(r"\bIPTV\b", "IPTV"),
(r"CAM-?Rip|\bCAM\b|\bTS\b|Telesync", "CAMRip"),
(r"\bDVD\b", "DVD"),
]
CONTAINERS = [
(r"\bMKV\b|Matroska", "MKV"), (r"\bMP4\b", "MP4"), (r"\bAVI\b", "AVI"), (r"\bM2TS\b", "M2TS"),
(r"\bISO\b", "ISO"), (r"DVD[-\s]?Video", "DVD-Video"), (r"\bTS\b", "TS"), (r"\bWEBM\b", "WEBM"),
(r"\bMOV\b", "MOV"), (r"\bWMV\b", "WMV"),
]
RE_HMS = re.compile(r"(?<!\d)(\d{1,2}):(\d{2}):(\d{2})(?!\d)")
def release_quality(text: str) -> str:
"""«Тип релиза» / «Качество видео» / «Качество» — без «| Исходник: …» и подписей после него."""
v = _first(text, r"Тип\s+релиза", r"Качество\s+видео", r"Качество")
v = re.split(r"\s+\|\s+|\s+Исходник\s*:", v, maxsplit=1)[0]
return v.strip(" .;")
def release_info(text: str, title: str = "") -> dict | None:
quality = release_quality(text)
# заголовок — только если по «Качеству» тип не понять («Hybrid (2160p)», «Без хардсаба»)
source = next((tag for s in (quality, title or "") for rx, tag in SOURCES
if re.search(rx, s, re.I)), None)
# «Формат : AVC» у MediaInfo nnm — не контейнер: берём первое значение, похожее на контейнер
container = next((tag for key in (r"Контейнер", r"Формат\s+видео", r"Формат")
for v in _field(text, key)
for rx, tag in CONTAINERS if re.search(rx, v, re.I)), None)
duration = _first(text, r"Продолжительность", r"Продолжительность\s+серии", r"Длительность")
# секунды — только для одного фильма: не «18 x ~ 00:20:00», не «~00:22:30 серия», не список файлов
hms = RE_HMS.search(duration)
single = hms and len(RE_HMS.findall(duration)) == 1 and not re.search(r"(?i)\d\s*[xх×]|сери|эпизод", duration)
sec = int(hms.group(1)) * 3600 + int(hms.group(2)) * 60 + int(hms.group(3)) if single else None
if not (quality or source or container or duration):
return None
return {"quality": quality or None, "source": source, "container": container,
"duration": duration or None, "duration_sec": sec}
# ------------------------------------------------------------------ субтитры
SUB_KEYS = (r"Субтитры(?:\s*#?\s*\d+|\s+[A-Za-z]{3})?", r"Subtitles?(?:\s*#?\s*\d+)?",
r"Язык\s+(?:подключаемых\s+)?субтитров")
RE_SUB_NONE = re.compile(r"(?i)^\s*(?:нет|отсутству\w*|no|none|-+)\s*\.?$")
RE_HARDSUB = re.compile(r"(?i)вшит\w*|hardsub|встроенн\w*\s+в\s+видео")
RE_FORCED = re.compile(r"(?i)forced|форсирован\w*|на\s+надписи|надписи")
RE_FULL = re.compile(r"(?i)\bfull\b|полн\w*")
RE_SDH = re.compile(r"(?i)\bSDH\b|для\s+слабослыш\w*|closed\s+captions")
RE_LANG_CODE = re.compile(r"(?<![A-Za-z])([A-Z][A-Za-z]{2})(?![A-Za-z])")
RE_TITLE_SUB = re.compile(r"(?i)\bSub\w*\s*\(([^()]{2,80})\)")
def _split_top(s: str) -> list[str]:
"""Делит по запятым/«;» вне скобок: «Русские (Forced), Украинские (Forced, SDH)» -> 2 части."""
out, depth, cur = [], 0, []
for ch in s:
if ch in "([":
depth += 1
elif ch in ")]":
depth -= 1
if ch in ",;" and depth <= 0:
out.append("".join(cur))
cur = []
else:
cur.append(ch)
out.append("".join(cur))
return [x.strip() for x in out if x.strip()]
def _lang(part: str) -> str:
code = lang_of_text(part)
if code:
return code
for m in RE_LANG_CODE.finditer(part):
c = LANG_CODES.get(m.group(1).upper())
if c:
return c
return ""
def subtitles_info(text: str, title: str = "") -> dict | None:
"""Строки «Субтитры: …» (и «Субтитры 2:», «Язык субтитров:»); нет строк — языки из «Sub (Rus, Eng)» заголовка."""
values = [v for key in SUB_KEYS for v in _field(text, key)]
fmt = _first(text, r"Формат\s+субтитров") or None
tracks, raw = [], []
hardsub = False
for v in values:
if not v or RE_SUB_NONE.match(v):
continue
raw.append(v)
hardsub = hardsub or bool(RE_HARDSUB.search(v))
for part in _split_top(v):
lang = _lang(part)
if not lang:
continue
# «Украинские (Forced, 2x Full, SDH)» — одна запись на язык, флаги объединяются
tracks.append({"lang": lang, "forced": bool(RE_FORCED.search(part)),
"full": bool(RE_FULL.search(part)), "sdh": bool(RE_SDH.search(part))})
if not raw and title:
m = RE_TITLE_SUB.search(title)
if m:
raw.append(m.group(0))
for part in _split_top(m.group(1)):
lang = _lang(part)
if lang:
tracks.append({"lang": lang, "forced": False, "full": False, "sdh": False})
if not raw and not values:
return None
merged: dict[str, dict] = {}
for t in tracks:
cur = merged.setdefault(t["lang"], {"lang": t["lang"], "forced": False, "full": False, "sdh": False})
for k in ("forced", "full", "sdh"):
cur[k] = cur[k] or t[k]
return {"languages": list(merged), "tracks": list(merged.values()), "format": fmt,
"hardsub": hardsub, "raw": "; ".join(raw or [v for v in values if v]) or None}
def tech_info(text: str, title: str = "") -> dict:
return {"video": video_info(text, title), "release": release_info(text, title),
"subtitles": subtitles_info(text, title)}
+15
View File
@@ -34,6 +34,8 @@ 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_KHZ = re.compile(r"(?i)(?<![\d.,])(\d{2}(?:[.,]\d{1,3})?)\s*(?:khz|кгц)|(?<!\d)(\d{5,6})\s*(?:hz|гц)")
RE_ATMOS = re.compile(r"(?i)\batmos\b")
RE_LAYOUT = re.compile(r"(?<![\d.])([1-9])\.([0-2])(?![\d.])") # раскладка «5.1» -> 6 каналов 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|оригинал")
@@ -78,7 +80,11 @@ def parse_audio_line(line: str) -> dict:
for n in re.split(r"\s*[,;/]\s*", chunk) # «|Дубляж, Le Doyen|» -> тип и студия отдельно 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) layout = RE_LAYOUT.search(fixed)
khz = RE_KHZ.search(fixed)
khz_val = (float(khz.group(1).replace(",", ".")) if khz.group(1) else int(khz.group(2)) / 1000) if khz else None
return {"raw": body, "lang": lang, return {"raw": body, "lang": lang,
"khz": (int(khz_val) if khz_val == int(khz_val) else khz_val) if khz_val else None,
"atmos": bool(RE_ATMOS.search(body)),
"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": _channels(ch.group(1)) if ch else (_channels(layout.group(0)) if layout 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)),
@@ -86,6 +92,15 @@ def parse_audio_line(line: str) -> dict:
"names": names} "names": names}
def audio_summary(info: dict | None) -> dict | None:
"""Поле `audio` ответа: {codec, channels, bitrate_kbps, khz, atmos}; нет строки «Аудио» — None."""
if not info:
return None
return {"codec": info["codec"] or None, "channels": int(info["channels"]) if info["channels"] else None,
"bitrate_kbps": int(info["kbps"]) if info["kbps"] else None,
"khz": info.get("khz"), "atmos": bool(info.get("atmos"))}
def audio_lines_all(text: str) -> list[dict]: def audio_lines_all(text: str) -> list[dict]:
out = [] out = []
for m in RE_AUDIO.finditer(text or ""): for m in RE_AUDIO.finditer(text or ""):
+30 -12
View File
@@ -2,24 +2,31 @@
Метрика — пары (тип, автор) на уровне релиза против меток датасета, только по Метрика — пары (тип, автор) на уровне релиза против меток датасета, только по
страницам, которых сегментатор не видел при обучении (is_test_release). страницам, которых сегментатор не видел при обучении (is_test_release).
Запуск: .venv/Scripts/python tests/e2e_segmenter.py Текст kinozal — из data/raw/kinozal/*.json (как у сервиса), см. segmenter.page_text.
На трекер берётся не больше E2E_MAX страниц (по умолчанию 400, детерминированно);
train-страницы считаются только с флагом --train-pages. Основная модель — E2E_MODEL
(по умолчанию models/best).
Запуск: .venv/Scripts/python tests/e2e_segmenter.py [каталог_сегментатора] [--train-pages]
""" """
from __future__ import annotations from __future__ import annotations
import hashlib
import json import json
import os
import sys import sys
from collections import defaultdict from collections import defaultdict
from pathlib import Path from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from src.html_extract import read_input # noqa: E402
from src.predict import DubPredictor # noqa: E402 from src.predict import DubPredictor # noqa: E402
from src.release import predict_release # noqa: E402 from src.release import predict_release # noqa: E402
from src.segmenter import is_test_release # noqa: E402 from src.segmenter import is_test_release, page_text # noqa: E402
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
MAX_PER_SOURCE = int(os.environ.get("E2E_MAX", "400"))
MODEL_DIR = os.environ.get("E2E_MODEL", "models/best")
def run(predictor, rows, mode: str, test_only: bool) -> dict: def run(predictor, rows, mode: str, test_only: bool) -> dict:
@@ -27,17 +34,25 @@ def run(predictor, rows, mode: str, test_only: bool) -> dict:
for r in rows: for r in rows:
by_rel[r["release_id"]].append(r) by_rel[r["release_id"]].append(r)
per_src = defaultdict(list) per_src = defaultdict(list)
for page in sorted((ROOT / "data" / "pages").glob("*.txt")): pages = [p for p in (ROOT / "data" / "pages").glob("*.txt")
if is_test_release(p.stem) == test_only and p.stem in by_rel]
pages.sort(key=lambda p: hashlib.md5(p.stem.encode()).hexdigest())
for page in pages:
rid = page.stem rid = page.stem
if is_test_release(rid) != test_only or rid not in by_rel: src = by_rel[rid][0]["source"]
if len(per_src[src]) >= MAX_PER_SOURCE:
continue continue
gold = {(r["translation_type"], r["studio"] or None) for r in by_rel[rid]} gold = {(r["translation_type"], r["studio"] or None) for r in by_rel[rid]}
res = predict_release(predictor, text=read_input(page), segmenter=mode) # как aggregate: «тип без студии» не отдельная дорожка, если у типа есть студия
# (kinozal: «Перевод: многоголосый» + студии из заголовка)
named = {t for t, s in gold if s}
gold = {(t, s) for t, s in gold if s or t not in named}
res = predict_release(predictor, text=page_text(page), segmenter=mode)
pred = {(t["translation_type"], t["studio"]) for t in res["tracks"]} pred = {(t["translation_type"], t["studio"]) for t in res["tracks"]}
tp = len(gold & pred) tp = len(gold & pred)
p = tp / len(pred) if pred else 0.0 p = tp / len(pred) if pred else 0.0
r = tp / len(gold) if gold else 0.0 r = tp / len(gold) if gold else 0.0
per_src[by_rel[rid][0]["source"]].append((p, r)) per_src[src].append((p, r))
out = {} out = {}
for s, v in per_src.items(): for s, v in per_src.items():
out[s] = (len(v), sum(x[0] for x in v) / len(v), sum(x[1] for x in v) / len(v)) out[s] = (len(v), sum(x[0] for x in v) / len(v), sum(x[1] for x in v) / len(v))
@@ -46,13 +61,16 @@ def run(predictor, rows, mode: str, test_only: bool) -> dict:
def main() -> None: def main() -> None:
rows = json.loads((ROOT / "data" / "processed" / "dataset.json").read_text(encoding="utf-8")) rows = json.loads((ROOT / "data" / "processed" / "dataset.json").read_text(encoding="utf-8"))
seg_dir = sys.argv[1] if len(sys.argv) > 1 else "models/segmenter" args = [a for a in sys.argv[1:] if not a.startswith("--")]
predictor = DubPredictor(segmenter_dir=seg_dir) seg_dir = args[0] if args else "models/segmenter"
print(f"сегментатор: {seg_dir}") predictor = DubPredictor(MODEL_DIR, segmenter_dir=seg_dir)
print(f"модель: {MODEL_DIR}; сегментатор: {seg_dir}")
if predictor.segmenter is None: if predictor.segmenter is None:
sys.exit("нет models/segmenter: python -m src.segmenter --train") sys.exit("нет models/segmenter: python -m src.segmenter --train")
for test_only, name in ((True, "тестовые страницы (не виделись сегментатору)"), blocks = [(True, "тестовые страницы (не виделись сегментатору)")]
(False, "train-страницы (для справки, оптимистично)")): if "--train-pages" in sys.argv:
blocks.append((False, "train-страницы (для справки, оптимистично)"))
for test_only, name in blocks:
print(f"== {name} ==") print(f"== {name} ==")
for mode in ("rules", "model"): for mode in ("rules", "model"):
res = run(predictor, rows, mode, test_only) res = run(predictor, rows, mode, test_only)
+234 -1
View File
@@ -16,7 +16,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from src.html_extract import (RE_LINE_PREFIX, _split_entries, # noqa: E402 from src.html_extract import (RE_LINE_PREFIX, RE_SCENE, _split_entries, # noqa: E402
extract_segments, html_to_text) extract_segments, html_to_text)
from src.studio_norm import canonical, find_studio_span # noqa: E402 from src.studio_norm import canonical, find_studio_span # noqa: E402
@@ -102,6 +102,29 @@ segs = extract_segments("Перевод:\nПрофессиональный (ду
check("значение на следующей строке", check("значение на следующей строке",
segs == ["Профессиональный (дублированный) Невафильм"], f"got {segs}") segs == ["Профессиональный (дублированный) Невафильм"], f"got {segs}")
segs = extract_segments(
"Перевод: \n"
"Профессиональный (дублированный) | Cinema Tone Production\n"
"Профессиональный (псевдодублированный) | HDRezka Studio\n"
"Профессиональный (многоголосый закадровый) | HDRezka Studio\n"
"Профессиональный (многоголосый закадровый) | TVShows\n"
"\nОригинальная аудиодорожка: Английская\n")
check("список строк под «Перевод:» (rt 6919056)", segs == [
"Профессиональный (дублированный) | Cinema Tone Production",
"Профессиональный (псевдодублированный) | HDRezka Studio",
"Профессиональный (многоголосый закадровый) | HDRezka Studio",
"Профессиональный (многоголосый закадровый) | TVShows"], f"got {segs}")
_rt_html = ('<div class="post_body"><span class="post-b">Перевод</span>: <br>\n'
'Профессиональный (дублированный) <span class="post-b">| Cinema Tone Production</span><br>\n'
'Профессиональный (псевдодублированный) <span class="post-b">| HDRezka Studio</span><br>\n'
'Профессиональный (многоголосый закадровый) <span class="post-b">| HDRezka Studio</span><br>\n'
'Профессиональный (многоголосый закадровый) <span class="post-b">| TVShows</span><br>\n<br>\n'
'<span class="post-b">Оригинальная аудиодорожка</span>: Английская</div>')
check("rutracker: <br>+\\n между дорожками (t=6919056)",
len(extract_segments(html_to_text(_rt_html))) == 4,
f"got {extract_segments(html_to_text(_rt_html))}")
segs = extract_segments("Перевод: Профессиональный (двухголосый закадровый) + Любительский (одноголосый закадровый)") segs = extract_segments("Перевод: Профессиональный (двухголосый закадровый) + Любительский (одноголосый закадровый)")
check("разделитель +", len(segs) == 2, f"got {segs}") check("разделитель +", len(segs) == 2, f"got {segs}")
@@ -114,6 +137,31 @@ check("codec-строка без типа пропущена", len(segs) == 0, f
check("_split_entries одиночная", _split_entries("Дублированный") == ["Дублированный"]) check("_split_entries одиночная", _split_entries("Дублированный") == ["Дублированный"])
# ---------------------------------------------------------------- сегментатор: достройка блока
print("== complete_block / segmenter_warning ==")
from src.release import complete_block, segmenter_warning # noqa: E402
_pg = ("Перевод:\nПрофессиональный (дублированный) | A\n\nПрофессиональный (псевдодублированный) | B\n"
"Профессиональный (многоголосый закадровый) | C\nПрофессиональный (многоголосый закадровый) | D\n"
"Субтитры: русские\n\nПрофессиональный дублированный: глоссарий\n")
_got = complete_block(["Профессиональный (псевдодублированный) | B"], _pg)
check("модель взяла 1 из 4 -> достроены все 4, глоссарий не тронут", len(_got) == 4
and all("глоссарий" not in x for x in _got), f"got {_got}")
check("ничего не выбрано -> ничего не добавляется", complete_block([], _pg) == [])
check("предупреждение: модель пуста, правила нашли", "не выбрала" in (segmenter_warning(True, "rules", [], ["x"]) or ""))
check("предупреждение: правила нашли больше", "больше" in (segmenter_warning(True, "model", ["a"], ["a", "b"]) or ""))
check("без предупреждения: совпало / нет модели",
segmenter_warning(True, "model", ["a"], ["a"]) is None and segmenter_warning(False, "rules", [], ["x"]) is None)
check("scene-имя релиза — не дорожка (kinozal 2160145)",
RE_SCENE.match("Memoirs.of.a.Geisha.2005.2160p.UHD.Blu-ray.Remux.DV.HDR.HEVC.TrueHD.Atmos.7.1-CiNEPHiLES")
and not RE_SCENE.match("Профессиональный (многоголосый закадровый) | HDRezka Studio")
and not RE_SCENE.match("Аудио 1: Русский (DTS-HD MA, 6 ch, 2240 Кбит/с) / Дублированный"))
from src.segmenter import LineSegmenter # noqa: E402
import numpy as _np # noqa: E402
_seg = object.__new__(LineSegmenter)
_seg.score = lambda lines: _np.ones(len(lines)) # модель «выбирает» всё
_got = _seg.select_lines("Перевод: Дублированный\nMemoirs.of.a.Geisha.2005.2160p.UHD.Remux-GRP\nАудио 1: Русский / Дублированный")
check("select_lines отбрасывает scene-имя даже при оценке 1.0", len(_got) == 2 and all("Geisha" not in x for x in _got), str(_got))
# ---------------------------------------------------------------- html # ---------------------------------------------------------------- html
print("== html_to_text ==") print("== html_to_text ==")
html = ("<html><body><div>меню форума</div>" html = ("<html><body><div>меню форума</div>"
@@ -219,6 +267,74 @@ check("title: студия вписана в дорожку без студии"
check("title: дубль студии пропущен, новая добавлена", check("title: дубль студии пропущен, новая добавлена",
len(_page) == 3 and _page[2]["source"] == "title") len(_page) == 3 and _page[2]["source"] == "title")
from src.fetch import inline_tabs, kinozal_tabs # noqa: E402
from src.release import codec_segments, merge_extra_tracks, same_studio # noqa: E402
_kz = ('<ul><li><a onclick="showtab(7,100); return false;" href="#">Техданные</a></li>'
'<li><a onclick="showtab(7,0); return false;" href="#">Релиз</a></li>'
'<li><a onclick="showtab(7,2); return false;" href="#">Скриншоты</a></li></ul>'
'<div id="tabs"><b>Перевод:</b> Профессиональный многоголосый</div>')
check("kinozal: вкладки без «Техданных» и скриншотов", kinozal_tabs(_kz) == [("7", 0, "Релиз")])
_kz_text = html_to_text(inline_tabs(_kz, [("Релиз", "Аудио 1: AC3 - Русский (MVO, LostFilm)<br>")]))
check("kinozal: вкладка «Релиз» попадает в текст",
"Перевод: Профессиональный многоголосый" in _kz_text and "(MVO, LostFilm)" in _kz_text)
check("codec_segments: русская дорожка с типом, без иноязычной",
codec_segments("Аудио 3: AC3, 2.0 ch - Русский (MVO, HDrezka Studio)\n"
"Аудио 6: E-AC3, 5.1 ch, 640 Kbps - English Dubbing\n"
"Перевод: Профессиональный") == ["AC3, 2.0 ch - Русский (MVO, HDrezka Studio)"])
check("same_studio: варианты написания",
same_studio("DVD", "DVD Магия") and same_studio("Михаил Яроцкий", "М.Яроцкий/Kyberpunk")
and not same_studio("LostFilm", "HDrezka"))
_page2 = [dict(_t("проф. многоголосый", None), studio_span=None, studio_confidence=None)]
merge_extra_tracks(_page2, [
dict(_t("проф. многоголосый", "HDrezka"), studio_span="HDrezka Studio", studio_confidence=1),
dict(_t("проф. многоголосый", "LostFilm"), studio_span="LostFilm", studio_confidence=1),
dict(_t("одноголосый", None), studio_span=None, studio_confidence=None)],
"codec", match=same_studio)
check("codec: студия вписана, новая добавлена, без студии — нет",
[(x["studio"], x["source"]) for x in _page2]
== [("HDrezka", "page+codec"), ("LostFilm", "codec")])
_page3 = [dict(_t("проф. многоголосый", None), studio_span=None, studio_confidence=None)]
merge_extra_tracks(_page3, [
dict(_t("неизвестно", "LostFilm"), studio_span="LostFilm", studio_confidence=1),
dict(_t("неизвестно", "TVShows"), studio_span="TVShows", studio_confidence=1)],
"codec", match=same_studio, adopt_type=True)
check("codec: «Аудио Rus: … | LostFilm» получает тип дорожки страницы",
[(x["translation_type"], x["studio"]) for x in _page3]
== [("проф. многоголосый", "LostFilm"), ("проф. многоголосый", "TVShows")])
from src.release import codec_ok, model_segments # noqa: E402
from src.segmenter import kinozal_text, label_page # noqa: E402
class _FakeSeg:
def select_lines(self, text):
return text.splitlines()
_main, _codec = model_segments(_FakeSeg(), "Перевод: Профессиональный многоголосый\n"
"Аудио 3: AC3 - Русский (MVO, LostFilm)\n"
"Аудио 6: AC3 - Казахский (Dub, TK Qazaqstan)")
check("model_segments: основные и кодек-строки раздельно, иноязычная отброшена",
_main == ["Профессиональный многоголосый"] and _codec == ["AC3 - Русский (MVO, LostFilm)"])
check("codec_ok: «English Dubbing» и «Original» — не русская дорожка",
not codec_ok("Аудио 6: E-AC3 - English Dubbing") and not codec_ok("Аудио 5: E-AC3 - Original (французский)")
and codec_ok("Аудио Rus: AC3, 2 ch | LostFilm"))
check("label_page: raw — 1, неразмеченная кодек-строка — -1, сводка «Аудио:» — 0",
label_page(["Перевод: Дублированный", "Аудио 2: AC3 - Русский (MVO, X)",
"Аудио: Русский, английский (AC3)", "Размер: 6 ГБ"], ["Перевод: Дублированный"])
== [1, -1, 0, 0])
import json # noqa: E402
import tempfile as _tmp # noqa: E402
with _tmp.TemporaryDirectory() as _d:
Path(_d, "123.json").write_text(json.dumps({"h1": "X / Y / 2026 / ПМ (LostFilm)",
"tech_tab": "Перевод: Профессиональный многоголосый",
"release_tab": "Аудио Rus: AC3 | LostFilm"}), encoding="utf-8")
check("kinozal_text: заголовок + Техданные + Релиз из raw JSON",
kinozal_text("kz-123", Path(_d)).splitlines()
== ["X / Y / 2026 / ПМ (LostFilm)", "Перевод: Профессиональный многоголосый", "Аудио Rus: AC3 | LostFilm"]
and kinozal_text("kz-404", Path(_d)) is None)
_dump = ("Меню\nГангстерленд / MobLand / 2025 / ПМ (LostFilm) / WEB\n" _dump = ("Меню\nГангстерленд / MobLand / 2025 / ПМ (LostFilm) / WEB\n"
"Гангстерленд / 2026 / ДБ (X) / WEB-DL") "Гангстерленд / 2026 / ДБ (X) / WEB-DL")
check("title_from_text: первая строка", check("title_from_text: первая строка",
@@ -317,6 +433,68 @@ check("аудио: дорожка студии находит свою стро
_orig = original_info(_au) _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("оригинал: 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) check("оригинал: нет строки Original -> None", original_info(_au[:2]) is None)
check("оригинал: E-AC3+Atmos -> atmos, 48 kHz", _orig and _orig["atmos"] and _orig["khz"] == 48 and _orig["codec"] == "E-AC3", str(_orig))
from src.trackinfo import audio_summary, parse_audio_line
check("аудио: без Atmos -> atmos false", audio_summary(_au[0])["atmos"] is False)
check("аудио: 44.1 KHz и 48000 Гц", parse_audio_line("AC3, 2 ch, 44.1 KHz, 192 kbps")["khz"] == 44.1
and parse_audio_line("AAC 48000 Гц 2 ch 128 kbps")["khz"] == 48)
check("аудио: «48 kHz» не меняет вход модели (кодек без +Atmos)", parse_audio_line("English E-AC3+Atmos / 5.1 / 48 kHz / 768 kbps")["codec"] == "E-AC3")
print("== техданные: видео, тип релиза, субтитры ==")
from src.release import title_from_html
from src.techinfo import release_info, subtitles_info, video_info, _rate_kbps
_fx_html = open("data/samples/rt-topic-6870900.html", encoding="utf-8").read()
_fx_title = title_from_html(_fx_html)
check("заголовок rutracker без разрывов слов (<span>/<wbr>)",
_fx_title.startswith("Супер Марио: Галактическое кино / The Super Mario Galaxy Movie (Аарон Хорват"), _fx_title[:80])
_fx_text = html_to_text(_fx_html)
_v = video_info(_fx_text, _fx_title)
check("видео: HEVC 3840x2160 2160p 23.976 fps 24800 kbps 10 бит",
_v and (_v["codec"], _v["width"], _v["height"], _v["resolution"], _v["fps"], _v["bitrate_kbps"], _v["bit_depth"])
== ("HEVC", 3840, 2160, "2160p", 23.976, 24800, 10), str(_v))
check("видео: HDR — Dolby Vision, HDR10+, HDR10; профиль DV 8",
_v and _v["hdr"] == ["Dolby Vision", "HDR10+", "HDR10"] and _v["dv_profile"] == "8", str(_v and _v["hdr"]))
_r = release_info(_fx_text, _fx_title)
check("релиз: WEB-DL, MKV, 01:38:12 = 5892 с",
_r and (_r["source"], _r["container"], _r["duration"], _r["duration_sec"]) == ("WEB-DL", "MKV", "01:38:12", 5892), str(_r))
_s = subtitles_info(_fx_text, _fx_title)
check("субтитры: RUS forced, UKR forced+full+SDH, ENG full+SDH, softsub (SRT)",
_s and [(t["lang"], t["forced"], t["full"], t["sdh"]) for t in _s["tracks"]]
== [("RUS", True, False, False), ("UKR", True, True, True), ("ENG", False, True, True)]
and _s["format"] == "softsub (SRT)", str(_s))
check("видео: битрейт не склеивается с высотой кадра", _rate_kbps("Xvid 640x272 699 kb/s") == 699
and _rate_kbps("DivX 704 x 400 699 KB/s") == 699 and _rate_kbps("8 200 Кбит/сек") == 8200)
check("видео: «5763 Mбит/с» — на деле килобиты", _rate_kbps("MPEG-4 AVC, 5763 Mбит/с, 1280x720") == 5763)
check("видео: кадр 1920x800 — всё равно 1080p; fps 23.809523 целиком",
(lambda v: v["resolution"] == "1080p" and v["fps"] == 23.809523)(video_info("Видео: AVC, 1920x800, 23.809523 fps, 1776 Kbps")))
check("видео: строки «Видео» нет -> None", video_info("Аудио: AC3, 2 ch") is None)
_vn = video_info("Видео: HEVC/H.265, 3840x2160, ~21.4 Mbps",
"В поисках Дори / Finding Dory (2016) UHD BDRip [H.265/2160p] [4K, HDR10, DV 8.1, 10-bit]")
check("видео nnm: 10-bit и «DV 8.1» только в заголовке -> bit_depth 10, Dolby Vision, профиль 8.1",
(_vn["bit_depth"], _vn["hdr"], _vn["dv_profile"]) == (10, ["Dolby Vision", "HDR10"], "8.1"), str(_vn))
check("видео kinozal: «Dolby Vision P8» в заголовке -> профиль 8",
video_info("Видео: MPEG-H HEVC, 24 Мбит/с, 3840x2074, 10 бит", "Настройщик / 4K, HEVC, HDR10+, Dolby Vision P8, Hybrid / WEB-DL")["dv_profile"] == "8")
check("видео: «DVDRip», «DV 10-bit» — не Dolby Vision-профиль",
(lambda a, b: a["hdr"] == [] and a["dv_profile"] is None and b["dv_profile"] is None and b["bit_depth"] == 10)(
video_info("Видео: XviD, 720x400", "Фильм [2001, DVDRip]"), video_info("Видео: HEVC, 3840x2160", "Фильм [DV 10-bit]")))
check("видео: «DV P7» в заголовке -> профиль 7", video_info("Видео: HEVC, 3840x2160", "Film 2160p DV P7 HDR10")["dv_profile"] == "7")
check("релиз: «18 x ~ 00:20:00» и список файлов — без duration_sec",
release_info("Продолжительность: 18 x ~ 00:20:00")["duration_sec"] is None
and release_info("Продолжительность: 01:31:25, 01:25:23")["duration_sec"] is None)
check("релиз: «Формат : AVC» MediaInfo — не контейнер, «Формат: Matroska» — MKV",
release_info("Формат : AVC\nФормат : Matroska")["container"] == "MKV")
check("релиз: тип из заголовка, если «Качество» его не называет",
release_info("Качество: Hybrid (2160p)", "Фильм / Film [2024, BDRemux 2160p]")["source"] == "BDRemux")
_sn = subtitles_info("Субтитры: отсутствуют")
check("субтитры: «отсутствуют» — пустой список языков, а не None", _sn is not None and _sn["languages"] == [], str(_sn))
check("субтитры: nnm — поле до табуляции («Отсутствуют\\tДата…»)",
subtitles_info("Субтитры: Отсутствуют\tДата мировой премьеры: 2 апреля 2011")["languages"] == [])
check("субтитры: kinozal «Субтитры1: Full | iTunes • RUS»",
subtitles_info("Субтитры1: Full | iTunes • RUS\nСубтитры2: Forced | Official • UKR")["languages"] == ["RUS", "UKR"])
check("субтитры: вшитые -> hardsub", subtitles_info("Субтитры: русские, вшитые")["hardsub"] is True)
check("субтитры: строк нет — языки из «Sub (Rus, Eng)» заголовка",
subtitles_info("", "Фильм [2024] Dub + Sub (Rus, Eng)")["languages"] == ["RUS", "ENG"])
check("субтитры: нигде нет -> None", subtitles_info("Видео: AVC") is None)
_T = ["проф. дубляж", "проф. многоголосый", "люб. многоголосый", "многоголосый"] _T = ["проф. дубляж", "проф. многоголосый", "люб. многоголосый", "многоголосый"]
check("тип: «Любительский (многоголосый …) Дубляжная» не дубляж", check("тип: «Любительский (многоголосый …) Дубляжная» не дубляж",
guard_type("проф. дубляж", "Любительский (многоголосый закадровый) Дубляжная", "Дубляжная", _T) == "люб. многоголосый") guard_type("проф. дубляж", "Любительский (многоголосый закадровый) Дубляжная", "Дубляжная", _T) == "люб. многоголосый")
@@ -353,6 +531,7 @@ check("series_info: сезон, серии, total и «студия -> сери
print("== сборщик датасета (tools/collect.py) ==") print("== сборщик датасета (tools/collect.py) ==")
import csv as _csv # noqa: E402 import csv as _csv # noqa: E402
import json as _json # noqa: E402 import json as _json # noqa: E402
import json # noqa: E402
import tempfile as _tmp # noqa: E402 import tempfile as _tmp # noqa: E402
from pathlib import Path as _P # noqa: E402 from pathlib import Path as _P # noqa: E402
@@ -656,6 +835,15 @@ check("метки rt_parse: кодек и битрейт из строки «А
all(r["codec"] == "AC3" and r["kbps"] == "192" for r in _hr), str(_hr)) all(r["codec"] == "AC3" and r["kbps"] == "192" for r in _hr), str(_hr))
check("метки rt_parse: 5 студий + 1 доп. 18+", len(_rows) == 6, str(len(_rows))) check("метки rt_parse: 5 студий + 1 доп. 18+", len(_rows) == 6, str(len(_rows)))
_bare = ("Перевод:\nПрофессиональный (дублированный) | Cinema Tone Production\n\nПрофессиональный (псевдодублированный) | HDRezka Studio\n"
"Профессиональный (многоголосый закадровый) | HDRezka Studio\nПрофессиональный (многоголосый закадровый) | TVShows\n"
"\nОригинальная аудиодорожка: Английская\nСубтитры: Русские, английские\n")
_br, _ = _hc.rt_parse({"id": "6919056", "title": "Астрал 6 [2026, WEB-DL 2160p]", "post": _bare})
check("метки rt_parse: «Перевод:» без значения + список строк (6919056)",
[(r["translation_type"], r["studio"]) for r in _br] == [
("проф. дубляж", "Cinema Tone Production"), ("люб. дубляж", "HDrezka"), # псевдодубляж = «люб. дубляж»
("проф. многоголосый", "HDrezka"), ("проф. многоголосый", "TVShows")], str([(r["translation_type"], r["studio"]) for r in _br]))
_sf = __import__("src.release", fromlist=["x"]).series_info("Сериал / Сезон 1 / Серии 1-8 из 8 [2026]", [ _sf = __import__("src.release", fromlist=["x"]).series_info("Сериал / Сезон 1 / Серии 1-8 из 8 [2026]", [
{"studio": "LostFilm", "episodes": "1-4", "episode_ranges": [[1, 4]]}, {"studio": "LostFilm", "episodes": "1-4", "episode_ranges": [[1, 4]]},
{"studio": "HDrezka", "episodes": None, "episode_ranges": []}]) {"studio": "HDrezka", "episodes": None, "episode_ranges": []}])
@@ -857,6 +1045,51 @@ check("prepare_data: у редкого языка метка пустая", [r["
print("== метки: тип, раскладка, строки из «Аудио», веса классов ==") print("== метки: тип, раскладка, строки из «Аудио», веса классов ==")
import harvest_core as _hc2 # noqa: E402 import harvest_core as _hc2 # noqa: E402
_ps = _hc2.nnm_part_studio
check("nnm «Перевод:»: студия из скобок и после тире",
_ps("Дублированный (Невафильм)") == "Невафильм" and _ps("Многоголосый профессиональный - Jaskier") == "Jaskier"
and _ps("Авторский (Живов") == "Юрий Живов")
check("nnm «Перевод:»: тип, кратность, лицензия, соседнее поле — не студия",
_ps("Двуголосый профессиональный") == "" and _ps("Одноголосый x2") == ""
and _ps("Дублированный (Лицензия BD CEE)") == ""
and _ps("одноголосый закадровый Дата мировой премьеры: 4 февраля 1983 (USA)") == "")
_kzr = _hc2.kz_title_rows(
{"id": "kz-1", "h1": "Что, если...? / What If...? / 2023 / ПМ (LostFilm, HDrezka Studio), СТ / WEB-DL"},
"Аудио Rus: AC3, 48 kHz, 2 ch, 384 Кбит/сек | LostFilm",
[{"studio": "(не указана)", "raw": "Перевод: Профессиональный многоголосый", "translation_type": "проф. многоголосый"}])
check("kinozal: студии из заголовка, строка «Аудио» — в audio_raw",
[(r["translation_type"], r["studio"], r["studio_source"], bool(r["audio_raw"])) for r in _kzr]
== [("проф. многоголосый", "LostFilm", "title", True), ("проф. многоголосый", "HDrezka", "title", False)])
check("kinozal: студия, уже названная на странице, из заголовка не дублируется",
_hc2.kz_title_rows({"id": "kz-1", "h1": "X / Y / 2023 / ПМ (LostFilm)"}, "",
[{"studio": "LostFilm", "raw": "Перевод: … LostFilm"}]) == [])
import re as _re # noqa: E402
_known = _re.compile(r"(?i)(?<![\w])(LostFilm|TVShows|HDrezka Studio|HDrezka|Дубляжная|1\+1)(?![\w])")
_kzrel = [{"source": "kinozal", "studio": "(не указана)", "raw": "Перевод: Профессиональный (многоголосый закадровый)",
"translation_type": "проф. многоголосый"}]
_cr = _hc2.codec_line_rows({"id": "kz-2"}, "\n".join([
"Аудио 1: AC3, 6 ch, 448 Кбит/с - Русский дубляж (Мосфильм-Мастер) - лицензия",
"Аудио Rus: AC3, 48,0 kHz, 6 ch, 384 Кбит/сек | TVShows",
"Аудио 3: AC3, 2 ch, 192 Kbps - Украинский (DVO, ТК \"1+1\")",
"Аудио 4: AC3, 2/0 (L,R) ch, 192 kbps, Delay 0 mSec Русский (MVO, LostFilm)",
"Аудио 5: AC3, 6 ch, 448 kbps - Английский (Original)",
"Аудио: Русский (AC3, 6 ch, 384 Кбит/с) Дубляжная, английский (AC3, 6 ch)",
]), _kzrel, known=_known)
check("кодек-строки: студия, тип из строки или единственный тип релиза, язык",
[(r["studio"], r["translation_type"], r["lang"], r["studio_source"]) for r in _cr]
== [("Мосфильм-Мастер", "проф. дубляж", "RUS", "codec"), ("TVShows", "проф. многоголосый", "RUS", "codec"),
("1+1", "проф. двухголосый", "UKR", "codec"), ("LostFilm", "проф. многоголосый", "RUS", "codec")],
str([(r["studio"], r["translation_type"]) for r in _cr]))
check("кодек-строки: вход — сегмент без префикса «Аудио N:», вся строка — в audio_raw",
_cr[0]["raw"].startswith("AC3, 6 ch") and _cr[0]["audio_raw"].startswith("Аудио 1:"))
check("кодек-строки: студия, уже размеченная, не дублируется (в т.ч. «А. Гаврилов» = «Андрей Гаврилов»)",
_hc2.codec_line_rows({"id": "x"}, "Аудио 2: AC3, 2 ch, 192 Kbps - Русский (AVO, А. Гаврилов)",
[{"studio": "Андрей Гаврилов", "translation_type": "авт. одноголосый"}], known=_known) == [])
check("кодек-строки: при нескольких типах релиза тип строки без тег-слова — «неизвестно»",
[r["translation_type"] for r in _hc2.codec_line_rows(
{"id": "x"}, "Аудио Rus: AC3, 2 ch, 192 Кбит/сек | HDrezka Studio",
[{"studio": "", "translation_type": "проф. дубляж"}, {"studio": "", "translation_type": "авт. одноголосый"}],
known=_known)] == ["неизвестно"])
_tt = _hc2.translation_type _tt = _hc2.translation_type
check("тип: любительский дубляж — отдельный класс", _tt("Любительский (дублированный) Red Head Sound")[0] == "люб. дубляж") check("тип: любительский дубляж — отдельный класс", _tt("Любительский (дублированный) Red Head Sound")[0] == "люб. дубляж")
check("тип: псевдодубляж -> люб. дубляж", _tt("Любительский (псевдодублированный)")[0] == "люб. дубляж") check("тип: псевдодубляж -> люб. дубляж", _tt("Любительский (псевдодублированный)")[0] == "люб. дубляж")
+260
View File
@@ -0,0 +1,260 @@
"""Аудит дорожек в описаниях релизов: data/raw/* против data/labels.csv.
Читает текст каждой сохранённой страницы (kinozal: «Техданные» + «Релиз»;
nnm / rutracker: пост), находит строки русских/украинских дорожек
(«Перевод N:», «Озвучка:», «Аудио N: … (MVO, X)» и т. п., без субтитров) и считает:
* какие форматы строк встречаются и какие из них не ловят правила разметки
(segmenter.RE_AUDIO_TRACK) и запасной путь сервиса (release.codec_segments);
* студии из словаря меток, названные в строках дорожек, но отсутствующие
в метках релиза (сравнение через release.same_studio — «А. Гаврилов» = «Андрей Гаврилов»);
* мусор в колонке studio (каналы, языки, тип-слова, «Лицензия», «по заказу»);
* релизы без меток: legacy / ok / есть ли в них строки дорожек.
Результат — reports/tracks_audit_numbers.md (цифры и примеры); выводы и план —
reports/tracks_audit.md. Чтение raw ~2–3 мин; --cache сохраняет тексты в pickle.
.venv\\Scripts\\python tools\\audit_tracks.py [--cache путь.pkl]
"""
from __future__ import annotations
import argparse
import csv
import json
import os
import pickle
import random
import re
import sys
from collections import Counter, defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from src.release import RE_CODEC_TRACK, codec_ok, same_studio # noqa: E402
from src.segmenter import RE_AUDIO_TRACK # noqa: E402
RAW = ROOT / "data" / "raw"
LABELS = ROOT / "data" / "labels.csv"
OUT = ROOT / "reports" / "tracks_audit_numbers.md"
SOURCES = (("kinozal", "kz-"), ("nnm", "nnm-"), ("rutracker", "rt-"))
NO_STUDIO = "(не указана)"
# строка дорожки: перевод/озвучка или кодек-строка
RE_TRANS_LINE = re.compile(
r"(?i)^\s*[#№*•\-\s]*(перевод\w*|озвуч\w*|дубляж\w*|автор перевода)\s*(№|#)?\s*[\d\-–,\s]*(\([^)]*\))?\s*[:.]")
RE_AUDIO_LINE = re.compile(
r"(?i)^\s*[#№*•\-\s]*(аудио\w*|audio\w*|звук\w*|аудиодорожк\w*|дорожк\w*|track\w*)\b")
RE_CODEC = re.compile(
r"(?i)\b(e-?ac-?3|ac-?3|ас3|dts|aac|truehd|flac|mp3|opus|l?pcm|mp2|dolby)\b|\d\s*(kbps|кбит|kb/s|khz|кгц)")
RE_RU = re.compile(r"(?i)русск|russian|\brus\b|украин|ukrain|\bukr\b|україн")
RE_NON_RU = re.compile(
r"(?i)english|\beng\b|англ|япон|japan|\bjap\b|казах|kazakh|корей|китай|немец|француз|итальян|испан|оригинал|original")
RE_TYPE = re.compile(
r"(?i)\b(MVO|DVO|AVO|DUB|VO)\b|дубл|многоголос|двухголос|двуголос|одноголос|закадр|авторск|любительск|профессиональн")
RE_SUBS = re.compile(r"(?i)субтитр")
# не студия: тех. параметры, языки, тип-слова, источник дорожки
JUNK = re.compile(
r"(?i)^(\d+\s*ch|.*\b(ac-?3|dts|aac|kbps|kb/s|ch)\b.*|l,.*|.*русск\w*.*|английск\w*|украинск\w*|французск\w*"
r"|испанск\w*|немецк\w*|итальянск\w*|японск\w*|казахск\w*|субтитр\w*|ukr|rus|eng|eur|cee|vhs|tv|ts"
r"|дубляж|полное дублирование|дублированный|дву(х)?голосый|многоголосый|одноголосый|закадровый"
r"|профессиональный|любительский|авторский|студийный|неизвестн\w*|оригинал\w*|original"
r"|.*лицензи\w*.*|звук.*|blu-?ray.*|bd.*|itunes.*|netflix|amazon|web-?dl|в составе контейнера"
r"|.*ненормативн\w*.*|18\+)$")
# виды мусора для отчёта (первое совпадение)
NOISE_KINDS = (
("каналы/кодек", r"(?i)\b(\d+\s*ch|ac-?3|dts|aac|kbps|kb/s)\b|^l,"),
("язык вместо студии", r"(?i)^(русск|английск|украинск|французск|испанск|казахск)\w*$"),
("язык-префикс «Русский, - X»", r"(?i)^(русск|украинск|english)\w*\s*,"),
("тип-слово", r"(?i)^(дубляж|полное дублирование|дву(х)?голосый|многоголосый|одноголосый|закадровый)$"),
("источник: лицензия/Blu-ray/iTunes/Netflix", r"(?i)лицензи|blu-?ray|^bd\b|itunes|netflix|amazon"),
("«X по заказу Y»", r"(?i)по заказу"),
("субтитры", r"(?i)субтитр"),
("неру дорожка", r"(?i)казах|qazaq|kazakh|английск|english"),
)
def nid(rid: str) -> str:
return re.sub(r"^(kz|nnm|rt)-", "", rid)
def norm(s: str) -> str:
return re.sub(r"\s+", " ", s).strip().lower().replace("ё", "е")
def load_texts(cache: Path | None) -> dict:
"""{(source, id): (title, text, legacy)}; kinozal — h1 и вкладки, nnm/rt — пост."""
if cache and cache.exists():
return pickle.loads(cache.read_bytes())
out = {}
for src, _ in SOURCES:
d = RAW / src
for n in sorted(os.listdir(d), key=lambda n: "legacy" in n):
if not n.endswith(".json"):
continue
rid = n.split(".")[0]
if (src, rid) in out:
continue
try:
rec = json.loads((d / n).read_text(encoding="utf-8"))
except (OSError, ValueError):
continue
if src == "kinozal":
title = rec.get("h1") or ""
text = "\n".join(x for x in (rec.get("tech_tab"), rec.get("release_tab")) if x)
else:
title, text = rec.get("title") or "", rec.get("post") or ""
out[(src, rid)] = (title, text, "legacy" in n, rec.get("ok", True))
if cache:
cache.write_bytes(pickle.dumps(out))
return out
def track_lines(text: str) -> list[tuple[str, str]]:
"""[(вид, строка)] строк русских/украинских дорожек; вид: trans | audio."""
out = []
for line in text.splitlines():
line = line.strip()
if not line or len(line) > 400 or RE_SUBS.search(line) and not RE_TYPE.search(line):
continue
if RE_TRANS_LINE.match(line):
if RE_NON_RU.search(line.split(":", 1)[0]) and not RE_RU.search(line):
continue
out.append(("trans", line))
elif (RE_AUDIO_LINE.match(line) or RE_RU.match(line)) and RE_CODEC.search(line):
if RE_RU.search(line) and (RE_TYPE.search(line) or "|" in line or "(" in line):
out.append(("audio", line))
return out
def line_prefix(line: str) -> str:
p = re.match(r"\s*\W*\w*[^:]{0,14}:?", line).group(0).strip().lower()
return re.sub(r"\s+", " ", re.sub(r"\d+", "N", p))
def studio_vocab(rows: list[dict]) -> tuple[re.Pattern, dict]:
cnt = Counter(r["studio"].strip() for r in rows if r["studio"] not in ("", NO_STUDIO))
vocab = [s for s, c in cnt.items() if c >= 5 and len(s) >= 3 and not JUNK.match(s)]
vocab.sort(key=len, reverse=True)
rx = re.compile(r"(?i)(?<![\w])(" + "|".join(map(re.escape, vocab)) + r")(?![\w])")
return rx, {norm(s): s for s in vocab}
def audit(texts: dict, rows: list[dict]) -> dict:
lab = defaultdict(list)
for r in rows:
lab[(r["source"], nid(r["release_id"]))].append(r)
voc_rx, canon = studio_vocab(rows)
R = defaultdict(lambda: defaultdict(Counter))
ex = defaultdict(list)
for (src, rid), (title, text, legacy, ok) in texts.items():
L = lab.get((src, rid), [])
lines = track_lines(text)
rel = R[src]["releases"]
rel["всего"] += 1
if not L:
rel["без меток"] += 1
key = ("legacy" if legacy else "обычный") + (", ok=False" if not ok else "") + \
(", есть строки дорожек" if lines else ", нет строк дорожек")
R[src]["без меток"][key] += 1
if lines and len(ex[(src, "unlab")]) < 3000:
ex[(src, "unlab")].append((rid, title[:80], lines[0][1][:140]))
labs = [r["studio"] for r in L if r["studio"] not in ("", NO_STUDIO) and not JUNK.match(r["studio"].strip())]
missing: dict[str, tuple[str, str]] = {}
for kind, line in lines:
R[src]["строки"][kind] += 1
if kind == "audio":
a = bool(RE_AUDIO_TRACK.match(line))
b = bool(RE_CODEC_TRACK.match(line) and codec_ok(line))
R[src]["кодек-строки"][("RE_AUDIO_TRACK " if a else "мимо RE_AUDIO_TRACK ") +
("| codec_segments" if b else "| мимо codec_segments")] += 1
if not a or not b:
miss = ("seg " if not a else "") + ("codec" if not b else "")
R[src]["форматы мимо: " + miss.strip()][line_prefix(line)] += 1
if len(ex[(src, "fmt:" + miss.strip())]) < 3000:
ex[(src, "fmt:" + miss.strip())].append((rid, line[:160]))
if not L:
continue
for m in voc_rx.finditer(line):
st = canon.get(norm(m.group(1)), m.group(1))
if any(same_studio(st, x) for x in labs) or any(same_studio(st, x) for x, _ in missing.values()):
continue
missing[norm(st)] = (st, kind)
if len(ex[(src, "miss:" + kind)]) < 5000:
ex[(src, "miss:" + kind)].append((rid, st, line[:160]))
if L:
rel["с метками"] += 1
rel["с метками, есть недостающая студия"] += bool(missing)
for st, kind in missing.values():
R[src]["недостающие студии: вид строки"][kind] += 1
R[src]["недостающие студии: топ"][st] += 1
# недостающие студии в релизе, где у меток вообще нет студий
if missing and not labs:
rel["с метками без студий, студия есть в кодек-строке"] += 1
full = norm(title + "\n" + text)
for r in L:
st = r["studio"].strip()
if st in ("", NO_STUDIO) or JUNK.match(st):
continue
R[src]["студии меток"]["всего"] += 1
words = [w for w in re.findall(r"\w{4,}", norm(st))]
if norm(st) not in full and not any(w in full for w in words):
R[src]["студии меток"]["нет в тексте/заголовке"] += 1
R[src]["студии меток: нет в тексте, studio_source"][r["studio_source"]] += 1
if len(ex[(src, "ghost")]) < 3000:
ex[(src, "ghost")].append((rid, st, r["raw"][:140]))
for r in rows:
st = r["studio"].strip()
for k, rx in NOISE_KINDS:
if re.search(rx, st):
R[r["source"]]["мусор в studio меток"][k] += 1
if len(ex[(r["source"], "noise:" + k)]) < 500:
ex[(r["source"], "noise:" + k)].append((r["release_id"], st, r["raw"][:120]))
break
return {"stats": R, "examples": ex}
def write_md(res: dict, path: Path) -> None:
R, ex = res["stats"], res["examples"]
rnd = random.Random(7)
out = ["# Аудит дорожек в raw-страницах (цифры)", "",
"Сгенерировано `tools/audit_tracks.py`; выводы и план — `reports/tracks_audit.md`.", ""]
for src, _ in SOURCES:
out += [f"## {src}", ""]
for block, cnt in R[src].items():
top = cnt.most_common(25)
out.append(f"**{block}**: " + "; ".join(f"{k} — {v}" for k, v in top))
out.append("")
for key in sorted(k for k in ex if k[0] == src):
v = ex[key]
out.append(f"<details><summary>примеры {key[1]} ({len(v)})</summary>\n")
for t in rnd.sample(v, min(15, len(v))):
out.append("- " + " · ".join(str(x).replace("|", "\\|") for x in t))
out += ["", "</details>", ""]
path.write_text("\n".join(out), encoding="utf-8")
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--cache", type=Path, help="pickle с текстами raw (создаётся, если нет)")
ap.add_argument("--out", type=Path, default=OUT)
args = ap.parse_args()
sys.stdout.reconfigure(encoding="utf-8")
with LABELS.open(encoding="utf-8-sig", newline="") as f:
rows = list(csv.DictReader(f))
res = audit(load_texts(args.cache), rows)
args.out.parent.mkdir(exist_ok=True)
write_md(res, args.out)
for src, _ in SOURCES:
print(f"== {src}")
for block, cnt in res["stats"][src].items():
print(f" {block}: " + "; ".join(f"{k} — {v}" for k, v in cnt.most_common(12)))
print(f"записано: {args.out.relative_to(ROOT)}")
if __name__ == "__main__":
main()
+1 -1
View File
@@ -1028,7 +1028,7 @@ def cmd_build(rewrite: bool = False, relabel: bool = False, runner: "Runner | No
# Версия разбора: поднимите при правке парсеров (harvest_core, html_to_text) — тогда # Версия разбора: поднимите при правке парсеров (harvest_core, html_to_text) — тогда
# кэш «уже разобрано» сбросится сам и данные пересоберутся один раз. # кэш «уже разобрано» сбросится сам и данные пересоберутся один раз.
PARSE_VERSION = 3 PARSE_VERSION = 4
def load_json(path: Path) -> dict: def load_json(path: Path) -> dict:
+234 -15
View File
@@ -1288,6 +1288,10 @@ def _kz_parse_core(rec: dict) -> tuple[list[dict], dict]:
# ---- rutracker ----------------------------------------------------- # ---- rutracker -----------------------------------------------------
RT_RE_FIELD = re.compile( RT_RE_FIELD = re.compile(
r"^\s*(?:Перевод|Озвучка|Озвучивание|Озвучено|Звук)\s*(?:#\s*\d+|\d+)?\s*:\s*(?P<body>.+)$", re.I) r"^\s*(?:Перевод|Озвучка|Озвучивание|Озвучено|Звук)\s*(?:#\s*\d+|\d+)?\s*:\s*(?P<body>.+)$", re.I)
RT_RE_BARE = re.compile(
r"^\s*(?:Перевод|Озвучка|Озвучивание|Озвучено|Звук)\s*(?:#\s*\d+|\d+)?\s*:\s*$", re.I)
RT_RE_TYPE_START = re.compile(
r"^\s*(?:Профессиональн|Любительск|Авторск|Дублирован|Многоголос|Двухголос|Одноголос)", re.I)
RT_RE_SUBS = re.compile(r"^\s*Субтитры\s*:\s*(?P<body>.+)$", re.I) RT_RE_SUBS = re.compile(r"^\s*Субтитры\s*:\s*(?P<body>.+)$", re.I)
RT_RE_BRACKET = re.compile(r"\[(?P<body>[^\]]*)\]") RT_RE_BRACKET = re.compile(r"\[(?P<body>[^\]]*)\]")
RT_RE_SEP = re.compile(r"\s*(?:\+|;|/)\s*(?=Проф|Люб|Авт|Дубл|Многоголос|Двухголос|Одноголос)", re.I) RT_RE_SEP = re.compile(r"\s*(?:\+|;|/)\s*(?=Проф|Люб|Авт|Дубл|Многоголос|Двухголос|Одноголос)", re.I)
@@ -1366,20 +1370,44 @@ def audio18_rows(rows: list[dict], post: str) -> list[dict]:
return rows + extra return rows + extra
def rt_bare_list(lines: list[str], i: int, taken: set[int]) -> list[str]:
"""«Перевод:» без значения: дорожки списком ниже, по строке на дорожку.
Берутся подряд идущие строки со словом типа озвучки; между ними могут стоять
пустые (в HTML после <br> идёт ещё и перевод строки). Номера взятых строк
кладутся в taken, чтобы их не разбирали второй раз.
"""
out, j = [], i + 1
while True:
k = j
while k < len(lines) and not lines[k].strip():
k += 1
if k >= len(lines) or not RT_RE_TYPE_START.match(lines[k]) or ":" in lines[k]: # «тип: определение» — глоссарий
return out
out.append(lines[k])
taken.add(k)
j = k + 1
def _rt_parse_core(rec: dict) -> tuple[list[dict], dict]: def _rt_parse_core(rec: dict) -> tuple[list[dict], dict]:
post = rec.get("post") or "" post = rec.get("post") or ""
info = rt_parse_title(rec.get("page_title") or rec.get("title") or "") info = rt_parse_title(rec.get("page_title") or rec.get("title") or "")
entries, subs = [], "" entries, subs, taken = [], "", set()
for line in post.splitlines(): lines = post.splitlines()
for i, line in enumerate(lines):
if i in taken:
continue
m = RT_RE_FIELD.match(line) m = RT_RE_FIELD.match(line)
if m: bodies = [m.group("body")] if m else (rt_bare_list(lines, i, taken) if RT_RE_BARE.match(line) else [])
for part in rt_split_entries(m.group("body")): if bodies:
e = rt_parse_entry(part) for body in bodies:
if e["type"] in ("оригинал", "субтитры"): for part in rt_split_entries(body):
continue e = rt_parse_entry(part)
if not e["studio"] and e["type"] == "неизвестно": if e["type"] in ("оригинал", "субтитры"):
continue continue
entries.append(e) if not e["studio"] and e["type"] == "неизвестно":
continue
entries.append(e)
continue continue
m = RT_RE_SUBS.match(line) m = RT_RE_SUBS.match(line)
if m and not subs: if m and not subs:
@@ -1460,6 +1488,29 @@ def nnm_studio(name: str) -> tuple[str, str]:
return cleaned, " ".join(x for x in (note, extra) if x).strip() return cleaned, " ".join(x for x in (note, extra) if x).strip()
# слова, которые в «Перевод: <тип> - <студия>» не студия: тип, кратность, язык
NNM_PART_TYPEISH = re.compile(
r"(?i)голос|дубл|закадр|авторск|любител|профессион|русск|оригинал|субтитр|лицензи"
r"|^\W*[xх]?\d+[xх]?\W*$")
def nnm_part_studio(part: str) -> str:
"""Студия из части строки «Перевод:» nnm: «Многоголосый профессиональный - Jaskier»,
«Дублированный (Невафильм)», «Авторский (Живов» -> имя; «Дублированный (Лицензия)»,
«Одноголосый x2», «Двуголосый профессиональный» -> ""."""
if len(part) > 90 or re.search(r"(?i)премьер|\bдата\b", part):
return "" # склеилось соседнее поле описания
m = re.search(r"\s[-–—:]\s*(.+)$", part)
cands = ([m.group(1)] if m else []) + [c for c in re.findall(r"\(([^()]*)\)?", part) if c]
for cand in cands:
studio, _ = clean_studio(cand)
studio, _ = canonical_studio(studio)
if len(studio) >= 2 and not NNM_PART_TYPEISH.search(studio) \
and studio.lower() not in SOURCE_LIKE:
return studio
return ""
def _nnm_parse_core(rec: dict) -> tuple[list[dict], dict]: def _nnm_parse_core(rec: dict) -> tuple[list[dict], dict]:
post = rec.get("post") or "" post = rec.get("post") or ""
title = rec.get("page_title") or rec.get("title") or "" title = rec.get("page_title") or rec.get("title") or ""
@@ -1539,20 +1590,21 @@ def _nnm_parse_core(rec: dict) -> tuple[list[dict], dict]:
if ttype in ("неизвестно", "оригинал", "субтитры"): if ttype in ("неизвестно", "оригинал", "субтитры"):
continue continue
perevod_types.append(part) perevod_types.append(part)
# без имён в скобках студия неизвестна, но тип перевода ценен # студия — если названа в части («Дублированный (Невафильм)»); без неё тип всё равно ценен
rows.append({ rows.append({
"source": "nnm", "release_id": rec["id"], "ru_title": ru_title, "source": "nnm", "release_id": rec["id"], "ru_title": ru_title,
"year": year, "quality": quality, "is_series": is_series, "year": year, "quality": quality, "is_series": is_series,
"studio": "(не указана)", "translation_type": ttype, "studio": nnm_part_studio(part) or "(не указана)", "translation_type": ttype,
"professional": pro, "role_raw": part, "actors": "", "professional": pro, "role_raw": part, "actors": "",
"note": mark_18("", has_18(line)), "age_18": int(has_18(line)), "note": mark_18("", has_18(line)), "age_18": int(has_18(line)),
"codec": "", "channels": "", "khz": "", "kbps": "", "codec": "", "channels": "", "khz": "", "kbps": "",
"subtitles": "", "studio_source": "tech", "raw": line, "subtitles": "", "studio_source": "tech", "raw": line,
}) })
# если построчных «Аудио N» с именами не было — оставляем только типы # есть построчные «Аудио N» с именами — строки «Перевод:» (tech) не нужны; иначе
if any(r["studio"] != "(не указана)" for r in rows): # оставляем tech-строки: типы и студии, названные в «Перевод:»
rows = [r for r in rows if r["studio"] != "(не указана)"] if any(r["studio"] != "(не указана)" and r["studio_source"] == "release" for r in rows):
rows = [r for r in rows if r["studio_source"] == "release" and r["studio"] != "(не указана)"]
# дорожка без имени и без типа («- Русский») ничего не даёт датасету # дорожка без имени и без типа («- Русский») ничего не даёт датасету
rows = [r for r in rows rows = [r for r in rows
@@ -1715,6 +1767,170 @@ def audio_only_rows(rec: dict, text: str, rows: list[dict]) -> list[dict]:
return out return out
def kz_title_rows(rec: dict, text: str, rows: list[dict]) -> list[dict]:
"""kinozal: студии из заголовка раздачи «… / ПМ (LostFilm, HDrezka Studio), ДБ (X) / …».
На kinozal студии часто названы только в заголовке и в строках «Аудио Rus: … | LostFilm»
(без типа), а «Перевод:» во «Техданных» — без студий. Строка-вход — тот же синтетический
сегмент, что строит сервис из заголовка (src.release.title_segments); строка «Аудио» с этой
студией (если есть) — в audio_raw (позитив для сегментатора).
"""
from src.release import RE_TITLE_TRACK, TITLE_TYPES
def key(x: str) -> str:
return re.sub(r"[\W_]+", "", (x or "").casefold())
title = (rec.get("h1") or rec.get("page_title") or "").split(" :: ")[0]
have = key(" ".join(((r.get("studio") or "") + " " + (r.get("raw") or "")) for r in rows))
lines = [ln for ln in audio_lines_all(text) if not ln["original"] and not ln["commentary"]]
base = rows[0] if rows else {}
info = kz_split_title(title)
out = []
for abbr, names in RE_TITLE_TRACK.findall(title):
for name in re.split(r"\s*[,;]\s*", names.strip()):
studio, note = clean_studio(name)
studio, extra = canonical_studio(studio)
k = key(studio)
if len(k) < 2 or k in have or studio.lower() in SOURCE_LIKE:
continue
kind, pro = translation_type(TITLE_TYPES[abbr])
a = next((ln for ln in lines if k in key(ln["raw"])), None)
out.append({
"source": "kinozal", "release_id": rec.get("id", ""),
"ru_title": base.get("ru_title") or info["ru_title"],
"year": base.get("year") or info["year"], "quality": base.get("quality", ""),
"is_series": base.get("is_series", bool(re.search(r"сезон|серии", title, re.I))),
"studio": studio, "translation_type": kind, "professional": pro,
"role_raw": TITLE_TYPES[abbr], "actors": "",
"note": mark_18(" ".join(x for x in (note, extra) if x).strip(), bool(a and a["adult"])),
"age_18": int(bool(a and a["adult"])),
"codec": a["codec"] if a else "", "channels": a["channels"] if a else "", "khz": "",
"kbps": a["kbps"] if a else "", "subtitles": "", "studio_source": "title",
"raw": f"{TITLE_TYPES[abbr]} | {name.strip()}",
"lang": (a["lang"] or "RUS") if a else "RUS", "audio_raw": a["raw"] if a else ""})
have += k
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): def _with_audio(core, texts):
"""Обёртка парсера: lang/audio_raw у строк; число переводов в rel — по итоговым строкам.""" """Обёртка парсера: lang/audio_raw у строк; число переводов в rel — по итоговым строкам."""
def parse(rec: dict) -> tuple[list[dict], dict]: def parse(rec: dict) -> tuple[list[dict], dict]:
@@ -1725,7 +1941,10 @@ def _with_audio(core, texts):
rows += audio_only_rows(rec, text, rows) rows += audio_only_rows(rec, text, rows)
elif core is _kz_parse_core: elif core is _kz_parse_core:
rows = kz_audio_only_rows(rec, text) rows = kz_audio_only_rows(rec, text)
if core is _kz_parse_core:
rows += kz_title_rows(rec, text, rows)
if rows: if rows:
rows += codec_line_rows(rec, text, rows)
rel["n_translations"], rel["ok"] = len(rows), True rel["n_translations"], rel["ok"] = len(rows), True
rel["studios"] = " | ".join(sorted({r["studio"] for r in rows})) rel["studios"] = " | ".join(sorted({r["studio"] for r in rows}))
return rows, rel return rows, rel
+136
View File
@@ -0,0 +1,136 @@
"""Доразметка студий в data/labels.csv без пересборки страниц.
То же, что теперь делает сборщик (tools/harvest/harvest_core.py) при переиндексации,
но по готовому labels.csv и data/raw — минуты вместо полного разбора HTML:
* nnm, строки «Перевод:» (studio_source=tech) с «(не указана)»: студия, названная
в самой части строки («Дублированный (Невафильм)», «… - Jaskier») — nnm_part_studio;
* kinozal: студии из заголовка раздачи «ПМ (LostFilm, HDrezka Studio)», которых нет
среди строк релиза (studio_source=title) — kz_title_rows по data/raw/kinozal/<id>.json;
* все трекеры: студии из кодек-строк «Аудио 2: AC3 … (MVO, LostFilm)», «Аудио Rus: … | TVShows»,
которых нет среди строк релиза (studio_source=codec) — codec_line_rows по data/raw/<трекер>/<id>.json.
Ручные релизы (data/manual_ids.txt) не трогаются. Перед записью — копия
data/labels.bak_relabel_<время>.csv. Повторный запуск ничего не добавляет.
.venv\\Scripts\\python tools\\relabel_studios.py # применить
.venv\\Scripts\\python tools\\relabel_studios.py --dry-run # только статистика
"""
from __future__ import annotations
import argparse
import csv
import json
import re
import shutil
import sys
import time
from collections import Counter, defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "tools" / "harvest"))
import harvest_core as H # noqa: E402
LABELS = ROOT / "data" / "labels.csv"
RAW = ROOT / "data" / "raw"
RAW_KZ = RAW / "kinozal"
MANUAL = ROOT / "data" / "manual_ids.txt"
NO_STUDIO = "(не указана)"
def relabel(rows: list[dict], raw_kz: Path = RAW_KZ, manual: set[str] | None = None) -> tuple[list[dict], Counter]:
"""rows -> (новые rows, статистика). Исходные dict меняются на месте."""
manual = manual or set()
stats: Counter = Counter()
for r in rows:
if r["source"] == "nnm" and r.get("studio_source") == "tech" and r["studio"] == NO_STUDIO \
and r["release_id"] not in manual:
studio = H.nnm_part_studio(r.get("role_raw") or "")
if studio:
r["studio"] = studio
stats["nnm: студия из «Перевод:»"] += 1
by_rel: dict[str, list[dict]] = defaultdict(list)
for r in rows:
if r["source"] == "kinozal":
by_rel[r["release_id"]].append(r)
added: list[dict] = []
for rid, rel_rows in by_rel.items():
if rid in manual:
continue
path = raw_kz / f"{rid.removeprefix('kz-')}.json"
if not path.exists():
stats["kinozal: нет raw JSON"] += 1
continue
rec = json.loads(path.read_text(encoding="utf-8"))
rec["id"] = rid
text = "\n".join(x for x in (rec.get("release_tab") or "", rec.get("tech_tab") or "") if x)
new = H.kz_title_rows(rec, text, rel_rows)
if new:
stats["kinozal: релизов со студиями из заголовка"] += 1
stats["kinozal: студий из заголовка"] += len(new)
stats["kinozal: из них со строкой «Аудио»"] += sum(bool(x["audio_raw"]) for x in new)
added += new
rows = rows + added
by_rel = defaultdict(list)
for r in rows:
by_rel[(r["source"], r["release_id"])].append(r)
codec: list[dict] = []
for (src, rid), rel_rows in by_rel.items():
if rid in manual:
continue
text = raw_text(raw_kz.parent, src, rid)
if text is None:
continue
new = H.codec_line_rows({"id": rid, "source": src}, text, rel_rows)
if new:
stats[f"{src}: релизов со студиями из кодек-строк"] += 1
stats[f"{src}: студий из кодек-строк"] += len(new)
codec += new
return rows + codec, stats
def raw_text(raw_dir: Path, src: str, rid: str) -> str | None:
"""Текст страницы, как у сборщика: kinozal — «Релиз» + «Техданные», nnm/rutracker — пост."""
num = re.sub(r"^(kz|nnm|rt)-", "", rid)
for name in (f"{num}.json", f"{num}.legacy.json"):
path = raw_dir / src / name
if path.exists():
rec = json.loads(path.read_text(encoding="utf-8"))
if src == "kinozal":
return "\n".join(x for x in (rec.get("release_tab") or "", rec.get("tech_tab") or "") if x)
return rec.get("post") or ""
return None
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--dry-run", action="store_true", help="только посчитать, файл не менять")
args = ap.parse_args()
with LABELS.open(encoding="utf-8-sig", newline="") as f:
reader = csv.DictReader(f)
fields = reader.fieldnames
rows = list(reader)
manual = set(MANUAL.read_text(encoding="utf-8").split()) if MANUAL.exists() else set()
before = sum(r["studio"] != NO_STUDIO for r in rows)
rows, stats = relabel(rows, manual=manual)
after = sum(r["studio"] != NO_STUDIO for r in rows)
for k, v in stats.items():
print(f"{k}: {v}")
print(f"строк: {len(rows)}; со студией: {before} -> {after}")
if args.dry_run:
return
backup = LABELS.with_name(f"labels.bak_relabel_{time.strftime('%Y%m%d_%H%M%S')}.csv")
shutil.copy(LABELS, backup)
with LABELS.open("w", encoding="utf-8-sig", newline="") as f:
w = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore")
w.writeheader()
w.writerows(rows)
print(f"записано: {LABELS.relative_to(ROOT)}; копия: {backup.name}")
if __name__ == "__main__":
main()