Датасет: полное покрытие data/raw (71 339 строк из 30 401 релиза), правки парсеров и аудит
docker / build (push) Successful in 1m20s

Переразбор всех страниц data/raw: legacy-страницы импорта, форматы префиксов ([*]Перевод (1):, Озвучка:, №2, диапазоны), дорожки только в строках «Аудио» (в т.ч. «(Русский, X)», «| Rus | Звук: …»), «Язык озвучки», несколько студий в одной строке, студия без типа. Кодек-строки: «Аудио #N Русский:», «Аудио pic RUS:», «Звук N:», «Аудиодорожка 01:», кириллические АVO/МVO. «Авторский» без числа голосов -> авт. одноголосый, «двугоголосый». Чистка студий: языки, кодеки, «по заказу», «в составе контейнера», отказы от озвучки.

Итог: страниц с метками 30 260 (85.5 %), страниц с дорожкой без меток 1 259 -> 223; data/pages 30 444; после prepare_data 60 944 строки (train 54 686 / test 6 258).

Новый tools/coverage_audit.py + reports/coverage_audit.md, docs/dataset-coverage.md. Переобучен models/best (base): на общем тесте type acc 0.976 против 0.949 у прежней, span 0.674 против 0.643; e2e kinozal 0.805/0.867, rt 0.771/0.740, nnm 0.752/0.693.
This commit is contained in:
Denozordec committed 2026-11-09 13:29:53 +07:00
1 parent d2cf488b9c
commit 0f359837c5
21 files changed
+38789 -22512

No files matched your search

+1
View File
@@ -14,6 +14,7 @@ reports/*.png
# локальное
*.log
.tmp/
.DS_Store
# служебное ZCode
+28 -16
View File
@@ -10,8 +10,9 @@ kinozal / nnm-club) модель определяет:
алиасов («HDRezka Studio» → `HDrezka`, «Ю. Сербин» → «Юрий Сербин»);
* **18+** дорожка или нет — бинарная голова.
Обучено на датасете из 3286 разметанных дорожек + 458 аугментированных
морфологических вариантов (снимок `labels.csv` из deepseek-harness).
Обучено на датасете из 60 944 строк (71 339 размеченных строк из 30 401 релиза
`data/raw` + морфологическая аугментация). Как страницы превращаются в строки и
что было исправлено в сборщике — [docs/dataset-coverage.md](docs/dataset-coverage.md).
Multi-task RuBERT: сравниваются `rubert-tiny2` и `ruBert-base`,
лучшая кладётся в `models/best`.
@@ -69,6 +70,8 @@ docs/ документация
* [docs/architecture.md](docs/architecture.md) — устройство модели и пайплайна
* [docs/dataset.md](docs/dataset.md) — схема датасета, семантика классов, аудит
* [docs/dataset-coverage.md](docs/dataset-coverage.md) — покрытие всех страниц
`data/raw`, найденные дыры сборщика и что исправлено
* [docs/training-windows.md](docs/training-windows.md) — обучение на Windows+GPU, troubleshooting
* [docs/api-reference.md](docs/api-reference.md) — **все значения ответа сервиса (15 классов `translation_type` и др.) и принцип работы; `labels.csv` для этого открывать не нужно**
* [docs/inference.md](docs/inference.md) — CLI/API, JSON-контракт
@@ -76,32 +79,41 @@ docs/ документация
## Качество
Held-out test (4 377 строк, группировка по релизам и текстам; модель
`models/runs/20261102_190555_basecodec`, обучена 02.11.2026 на `labels.csv` со студиями из
кодек-строк; сравнение моделей — `python -m src.runs`):
Held-out test (6 258 строк, группировка по релизам и текстам; после полной
переиндексации `data/raw` 09.11.2026). Обе модели оценены на **одном и том же**
тесте (`python -m src.runs --dirs … --refresh`):
| модель | type acc | type macro-F1 | span exact | 18+ acc | lang acc | joint exact |
|---|---|---|---|---|---|---|
| **ruBert-base (models/best)** | **0.978** | **0.903** | **0.922** | **1.000** | **0.997** | **0.906** |
| прежняя `models/best` (02.11) | 0.949 | 0.880 | 0.643 | 1.000 | 0.994 | 0.615 |
| **новая `models/best` (09.11)** | **0.976** | **0.906** | **0.674** | **1.000** | **0.995** | **0.658** |
* joint exact — строка распознана полностью (тип + автор + 18+).
* Тест-сплит пересобирается вместе с датасетом, поэтому цифры разных снимков напрямую
не сравниваются (ранние 0.946 — на тесте из 645 строк). rubert-tiny2 на текущем
датасете не переобучалась.
* Абсолютный `span exact` ниже прежних 0.922 не потому, что модель стала хуже:
тест теперь включает форматы, которых раньше в датасете не было (авторы из
кодек-строк и списков, аниме-раздачи, «Язык озвучки», nnm `Аудио … (Русский,
X)`), и в нём много одиночных имён в перечислениях. На одном и том же тесте
новая модель лучше прежней по всем головам (+2.7 п.п. type acc, +3.1 п.п. span).
* 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).
* Smoke на формулировках, которых нет в датасете: тип 9/10, 18+ 10/10,
автор 5/10 (полное извлечение автора на непривычных форматах записи —
известное ограничение, см. ниже).
тип + студия, precision / recall) — новая модель на тех же страницах:
kinozal 0.805 / 0.867, rutracker 0.771 / 0.740, nnm 0.752 / 0.693; прежняя
модель там же: 0.752 / 0.830, 0.729 / 0.700, 0.669 / 0.604. Справочно, правила:
0.792 / 0.859, 0.697 / 0.721, 0.670 / 0.646.
* Сегментатор (`models/segmenter`) переобучен на расширенном наборе страниц
(`models/segmenter_v2`, line precision 0.933 / recall 0.977), но e2e новых весов
хуже по nnm (0.610 / 0.560) — в `models/segmenter` оставлены прежние веса.
* `models/best-onnx` не пересобран под новую модель: `python -m src.export_onnx`.
Паритет предыдущего экспорта — [reports/onnx_parity.md](reports/onnx_parity.md).
## Ограничения
* Метки датасета сгенерированы регулярками (пайплайн deepseek-harness) — метрики
измеряют согласование с этой разметкой; ценность модели — обобщение на новые
формулировки без сопровождения регулярок.
* Датасет: 55 324 строки из 27 900 релизов (kinozal 21 766 / nnm 18 363 / rutracker 15 195).
* Датасет: 71 339 строк из 30 401 релиза (kinozal 23 969 / nnm 27 092 /
rutracker 20 278); после подготовки 60 944 строки. Покрытие страниц `data/raw`
— 85.5 % (30 260 из 35 385); оставшиеся — почти все без озвучки (русский
оригинал, «не требуется», музыка/сканы), см. [docs/dataset-coverage.md](docs/dataset-coverage.md).
* Извлечение автора уверенно работает на форматах записи, представленных в
датасете (имя после типа через разделитель, в скобках у AVO/MVO/DVO-дорожек,
после тире); на непривычных форматах («Одноголосый закадровый (Фамилия)»)
+37311 -22103
View File
File diff suppressed because it is too large. Load diff
+10 -2
View File
@@ -65,8 +65,16 @@ HTML страниц хранится в `data/raw`, поэтому после п
«Переиндексировать data/raw?» (то же: `collect.py build --rewrite --relabel`).
Страницы, `meta.csv` и метки размеченных релизов строятся заново текущими
парсерами; прежний `labels.csv` копируется в `data/labels.bak_<время>.csv`. Не
трогаются ручные метки (пункт 6), релизы без HTML (импорт старого harvest) и
релизы, по которым парсер ничего не нашёл.
трогаются ручные метки (пункт 6) и релизы, по которым парсер ничего не нашёл.
С версии разбора 6 переразбираются и legacy-страницы импорта старого harvest
(`*.legacy.json`: текст поста есть, HTML нет) — метки обновляются, страница
`data/pages` не перезаписывается.
Покрытие страниц датасетом проверяется по всем `data/raw` без выборки:
`.venv\Scripts\python tools\coverage_audit.py` → `reports/coverage_audit.md`
(страницы без меток, но с дорожкой; строки, не попавшие в метки; кластеры
форматов). Выводы по последнему прогону и правки парсеров —
[docs/dataset-coverage.md](dataset-coverage.md).
## Обучение по ссылкам
+153
View File
@@ -0,0 +1,153 @@
# Покрытие страниц `data/raw` датасетом: аудит сборщика
Отчёт о том, как сборщик (`tools/collect.py` → `tools/harvest/harvest_core.py` →
`src/prepare_data.py`) превращает сохранённые страницы в обучающий датасет, какие
форматы строк он терял и что исправлено. Цифры получены на всех страницах
`data/raw` (35 385 шт.), а не на выборке.
Инструмент замера — `tools/coverage_audit.py` (пишет `reports/coverage_audit.md`,
`data/raw/_coverage_pages.csv`, `data/raw/_coverage_lines.csv`). Строка считается
«дорожкой» независимым от парсеров детектором (префикс «Перевод/Озвучка/Аудио…»,
слово типа озвучки или блок `|студия|`), а «покрытой» — если для неё есть строка
`labels.csv` этого релиза (`raw` / `audio_raw` / `role_raw`).
## Как устроен сборщик и где терялись данные
Пять стадий, у каждой свой кэш:
| стадия | вход → выход | кэш / версия |
|---|---|---|
| `collect` | браузер → `data/raw/<трекер>/<id>.json` + `.html` | `_state.json` |
| `build` | `data/raw` → `data/pages/<release_id>.txt` + строки `data/labels.csv` | `_built.json`, `PARSE_VERSION` |
| `meta` | страницы → `data/meta.csv` | `_meta_cache.json`, `META_VERSION` |
| `prepare_data` | `labels.csv` → `data/processed/dataset.json` | — |
| `train` | `dataset.json` → `models/best`, `reports/` | — |
До этого аудита датасет собирался из 28 155 релизов (56 131 строка), хотя страниц
в `data/raw` было 35 385: **7 371 страница не дала ни одной строки**. Часть из них
законно не содержит озвучки (русский оригинал, «Перевод: не требуется», немое
кино, музыка/сканы), но не менее **1 259 страниц с настоящими дорожками** в
датасет не попадали. Причины — в порядке убывания потерь:
1. **legacy-страницы не переразбирались.** Импорт старого harvest дал 2 633
страницы только с текстом поста (`.legacy.json`); `build` брал лишь пары
`json+html`, поэтому их метки остались от старых регулярок и больше не
обновлялись.
2. **Форматы строк, которых не понимали префиксы.** `[*]Перевод (1):`,
`►Перевод:`, `Перевод №2:`, `Перевод 01-02:`, `Аудио RUS 1-3:`, `Озвучка:`,
`Озвучивание:` (nnm/rutracker) не совпадали с `RT_RE_FIELD` / `NNM_RE_PEREVOD`,
потому что те ждали ровно «Перевод:» или «Перевод 2:».
3. **Дорожки, названные только строкой «Аудио»** (nnm/rutracker, аниме-раздачи):
`Аудио 4: AC3 … (внешние) (Русский, Anilibria)`, `| Rus | Звук: Zendos &
Eladiel`, `Аудио RUS: … Озвучка: двухголосая (… )`. Студия была в скобках после
языка или в хвосте «Звук:/Озвучка:», а разметка такие строки не читала: у nnm
требовался «(MVO, Студия)», а язык — только «Русский/Английский/Украинский».
4. **Студия без типа озвучки** («Перевод: Dreamers Team», «Перевод: CrunchyRoll»,
«Озвучка: Persona99») выбрасывалась: `translation_type` давал «неизвестно», и
строка отбрасывалась вместе с автором. Для головы автора это потеря: на
странице автора назвали.
5. **Несколько студий в одной строке** («Профессиональный (многоголосый) — Jaskier,
TVShows, HDrezka Studio, LostFilm», «| ОРТ, KidsCo»): в метки шла одна студия
или склеенное «Jaskier, TVShows».
6. **Строка «Язык озвучки: Русский (Berofu, Takera, Euji [AniMaunt])»** (nnm, аниме)
вообще не считалась дорожкой.
7. **kinozal читал только одну вкладку.** Если в «Релизе» была строка «Перевод:»,
«Техданные» игнорировались (и наоборот) — терялись типы или студии второй
вкладки.
8. **Кодек-строки мимо правил сервиса** (`segmenter.RE_AUDIO_TRACK`,
`release.RE_CODEC_TRACK`): «Аудио pic RUS:», «Аудио #1 Русский:», «Аудио N Rus:»,
«Звук N:», «Аудиодорожка 01:», «Audio #3:», кириллические «АVO/МVO». Это дыра не
только в метках сегментатора, но и в запасном пути инференса.
## Что исправлено
`tools/harvest/harvest_core.py` (`PARSE_VERSION` 4 → 6), `src/html_extract.py`,
`src/release.py`, `src/segmenter.py`, `src/trackinfo.py`, `tools/collect.py`,
`src/prepare_data.py`, `src/studio_norm.py`:
* общий разбор префикса дорожки: декоративные `[*] ► •`, номер `(1)`, `№2`,
диапазоны `01-02`, «Озвучка/Озвучивание/Дубляж» — для rutracker, nnm и kinozal;
* `build` переразбирает legacy-страницы (метки обновляются, страница не
перезаписывается);
* nnm: студии из `(Русский, Anilibria)`, `| Rus | Звук: …`, язык `RUS/Rus/RU`;
строка «Язык озвучки»; несколько студий в «Перевод:» → по строке на студию;
студия без типа сохраняется с типом «неизвестно»;
* rutracker: разбор `(Русский, Студия)` и `[студия]`, хвостов «Озвучка:»/«Звук:»,
списков студий; те же правила для «Перевод/Озвучка/Озвучивание»;
* kinozal: обе вкладки, несколько студий в строке, «Отсутствует/Без перевода» не
считаются автором;
* кодек-строки: префиксы «Аудио #N Русский:», «Аудио pic RUS:», «Звук N:»,
«Аудиодорожка 01:», «Audio #3:», «Track #2:», «Title :»; тип-слова с
кириллическими двойниками (АVO/МVO/DVO); если у релиза нет ни одной строки,
из кодек-строки берётся дорожка без имени (тип без студии);
* чистка имён: «Русский, - AlexFilm» → AlexFilm, «X по заказу Y» → X,
«озвучил - N» → N; мусор (коды/каналы/языки/«по умолчанию»/«в составе
контейнера»/«Отсутствует»/эпизоды) отбрасывается; в `prepare_data` строки-отказы
от озвучки удаляются отдельным счётчиком.
## Результат
| | было | стало |
|---|---|---|
| страниц в `data/raw` | 35 385 | 35 385 |
| релизов в `labels.csv` | 28 155 | 30 401 |
| строк в `labels.csv` | 56 131 | 71 339 |
| страниц с метками | 28 014 | 30 260 (85.5 %) |
| страниц без меток, но с дорожкой (по детектору) | 1 259 | 223 |
| `data/pages` (страниц для сегментатора) | 28 198 | 30 444 |
| строк после `prepare_data` | 46 894 | 60 944 (train 54 686 / test 6 258) |
| строк со студией (спан найден) | 38 583 (82.3 %) | 50 477 (82.8 %) |
| уникальных текстов | 25 614 | 30 761 |
| дорожек 18+ | 1 006 | 1 156 |
По трекерам (строки `labels.csv` → строки после подготовки, включая аугментацию):
* kinozal: 21 773 → 23 969 (15 188 после подготовки);
* nnm: 18 384 → 27 092 (24 453);
* rutracker: 15 974 → 20 278 (21 303).
Остаток 223 страниц — почти весь музыка/OST-раздачи, где «дорожкой» оказался
трек-лист, плюс единичные нестандартные строки («Аудио 6: двухголосая закадровая
Flowers Media» без кодек-параметров, «Аудио ShinkaDan: …», «Аудио #1: Russian …
| Original | Censor»). Они перечислены в `data/raw/_coverage_lines.csv` и годятся
для следующей итерации правил.
Непокрытые строки на страницах **с** метками (12 908 из 56 921) — в основном не
потерянные дорожки, а дубли: строка «Аудио N:» той же дорожки, что уже размечена
из «Перевод:», дорожки оригинала/другого языка и техданные. Их доля видна в
`reports/coverage_audit.md`.
## Как воспроизвести
```powershell
# замер покрытия (кэш текстов ускоряет повтор)
.venv\Scripts\python tools\coverage_audit.py --cache data\raw\_coverage.pkl
# переиндексация: страницы + метки + meta (после правки парсеров поднять PARSE_VERSION)
.venv\Scripts\python tools\collect.py build --rewrite --relabel
# подготовка датасета и аудит
.venv\Scripts\python -m src.prepare_data
```
Переиндексация всех 35 тыс. страниц одним процессом занимает ~30 мин (основное
время — запись новых страниц и `meta.csv`); на нескольких процессах — в разы
быстрее (`Runner(workers)`), но в песочнице DSH процессы с именованными каналами
недоступны.
## Что это дало модели
Переобучение на расширенном датасете (held-out test 6 258 строк, обе модели
оценены на одном тесте):
| модель | type acc | type macro-F1 | span exact | joint exact |
|---|---|---|---|---|
| прежняя `models/best` (02.11) | 0.949 | 0.880 | 0.643 | 0.615 |
| новая `models/best` (09.11) | 0.976 | 0.906 | 0.674 | 0.658 |
E2E по релизам (пары тип + студия, precision / recall; те же 400 страниц на
трекер): прежняя модель — kinozal 0.752 / 0.830, rutracker 0.729 / 0.700,
nnm 0.669 / 0.604; новая — 0.805 / 0.867, 0.771 / 0.740, 0.752 / 0.693.
Абсолютный `span exact` на новом тесте ниже прежних 0.922 из-за того, что в тест
попали форматы, которых раньше в датасете не было; сравнение «на одном тесте»
показывает рост по всем головам.
+8 -5
View File
@@ -10,11 +10,14 @@
регулярками** — модель учится воспроизводить эту разметку и обобщать её на
новые формулировки.
Размер (снимок 02.11.2026): 55 324 строки из 27 900 релизов, 3 источника
(rutracker, nnm, kinozal); HTML-дамп страниц — `data/raw` (~32 тыс. файлов, ≈2.6 ГБ, в git не
хранится), разобранные тексты — `data/pages`. Точные числа показывает «Статистика датасета» в
`tools/collect.py` (пункт 3) и отчёт `reports/data_audit.md` после `prepare_data`.
Ранние цифры этого документа (3286 строк) относились к первому снимку.
Размер (снимок 09.11.2026, после полной переиндексации `data/raw`): 72 239 строк
из 30 401 релиза, 3 источника (rutracker, nnm, kinozal); HTML-дамп страниц —
`data/raw` (35 385 страниц, ≈2.8 ГБ, в git не хранится), разобранные тексты —
`data/pages` (30 444). Аудит покрытия страниц и правки парсеров —
[docs/dataset-coverage.md](dataset-coverage.md).
Точные числа показывает «Статистика датасета» в `tools/collect.py` (пункт 3) и
отчёт `reports/data_audit.md` после `prepare_data`. Ранние цифры этого документа
(3286 строк) относились к первому снимку.
К строкам при подготовке добавляются аугментированные морфологические варианты
(«многоголосое», «дублированное» и т.п.). Файлы `*_entries.csv` / `*_releases.csv` — те же данные в нарезке
по источникам и по релизам; модель используется только `labels.csv`.
+360
View File
@@ -0,0 +1,360 @@
# Покрытие страниц data/raw датасетом
Сгенерировано `tools/coverage_audit.py`. Строка «дорожки» — независимый от парсеров
детектор (префикс «Перевод/Озвучка/Аудио…», тип озвучки или блок `|студия|`);
«покрыта» — для строки есть строка `labels.csv` этого релиза (`raw`/`audio_raw`/`role_raw`).
## kinozal
- страниц: 10541
- обычных: 10491
- legacy: 50
- без меток: 604
- без меток: нет аудио-строк: 197
- без меток: только русская дорожка/не озвучка: 396
- без меток: ЕСТЬ дорожка озвучки: 11
- с метками: 9937
- строк дорожек найдено: 22665
- строк покрыто: 18347
- покрытие строк: 80.9%
**без меток · аудио:** — 10
- 1853874: Аудио: English E-AC3 / 5.1 / 48 kHz / 640 kbps |original|
- 2002018: Аудио: AC3, 48 kHz, 6 ch, ~448.00 kbps avg | русский |
- 2030240: Аудио: AC3, 48 kHz, 6 ch, ~448.00 kbps avg | русский |
**без меток · аудио #N:** — 4
- 1987221: Аудио #1: Russian: AC3, 448 Kb/s (6 ch) | Original | Censor
- 1987221: Аудио #2: Russian: AC3, 448 Kb/s (6 ch) | Original | 18+
- 2155859: Аудио #01: 48 kHz, AAC, 2 ch, 223 Kbps | stereo | zxx (музыка) |
**без меток · аудио#N:** — 1
- 1531412: Аудио#2: French: 48 kHz/24-bit, DTS-HD MA 2.0, ~1679.00 kbps avg |Audio Description|
**не покрыто · аудио N:** — 1506
- 1059424: Аудио 2: 48 kHz, AC3, 2/0 (L,R), ~192.00 kbps avg |Многоголосый закадровый|
- 1059424: Аудио 2: 48 kHz, AC3, 2/0 (L,R), ~192.00 kbps avg |Многоголосый закадровый|
- 1067525: Аудио 1: 48 kHz, AC3 Dolby Digital, 3/2 (L,C,R,l,r) + LFE ch, ~640 kbps (VO) [А.Кизуб]
**не покрыто · перевод:** — 1134
- 1040622: Перевод: Профессиональный многоголосый
- 1065182: Перевод: Профессиональный многоголосый
- 1068463: Перевод: Дублированный, профессиональный многоголосый
**не покрыто · аудио #N:** — 468
- 1013995: Аудио #2: AC-3, 48.0 KHz, 6 ch, 384 kbps | Оригинал |
- 1046248: Аудио #2: Русский (DTS, 6 ch, 768 Кбит/с) – Дублированный (Blu-ray CEE )
- 1046248: Аудио #3: Украинский (AC3, 6 ch, 448 Кбит/с) - Дублированный (Blu-ray CEE )
**не покрыто · аудиоN:** — 251
- 1129830: Аудио1: (48 kHz, AC3 Dolby Digital, 3/2 (L,C,R,l,r) + LFE ch, ~384 kbps) MVO (ДВД-Групп)
- 1178400: Аудио1: Русский (AC3, 2 ch, 192 Кбит/с) - Дублированный - студия SaTI
- 1428231: Аудио1: Русский (DTS-HD MA, 6 ch, 3759 Кбит/с) - профессиональный многоголосый
**не покрыто · аудио:** — 148
- 1004884: Аудио: Russian / Dolby Digital Audio / 5.1 / 48 kHz / 448 kbps - дубляж
- 1004884: Аудио: Ukrainian / Dolby Digital Audio / 5.1 / 48 kHz / 384 kbps - дубляж
- 1026823: Аудио: Подробнее смотрите вкладку перевод
**не покрыто · аудио#N:** — 120
- 1069534: Аудио#1: Russian: 48 kHz, AC3, 3/2 (L,C,R,l,r) + LFE ch, ~384.00 kbps avg |Дубляж, R5|
- 1075593: Аудио#1: Русский (MVO) - AC3, 2/0 ((L,R), 48 kHz, 16-bit, ~192 kbps avg
- 1075593: Аудио#2: Русский (VO) - AC3, 3/2 (L,C,R,l,r) + LFE ch, 48 kHz, 16-bit, ~384 kbps avg
**не покрыто · аудио pic eng:** — 87
- 1755345: Аудио pic Eng: E-AC-3, 48 kHz, 6 ch, 640 kbps |Original|
- 1968059: Аудио pic Eng: E-AC-3 JOC, 48 kHz, 6 ch, 768 kbps |Original|
- 1974407: Аудио pic ENG: E-AC3, 6 ch, 48 kHz, 640 kbps |Original|
**не покрыто · аудио eng:** — 60
- 1781406: Аудио Eng: E-AC3, 48 kHz, 6 ch, 640 kbps |Original|
- 1913634: Аудио Eng: E-AC-3, 48 kHz, 6 ch, 768 kbps |Original|
- 1913635: Аудио Eng: E-AC-3, 48 kHz, 6 ch, 768 kbps |Original|
**не покрыто · аудио #N русский:** — 54
- 1808795: Аудио #1 Русский: AC3, 48.0 kHz, 5.1 ch, 384 Kbps [Дубляж, iTunes]
- 1808795: Аудио #2 Русский: DTS-HD MA, 48.0 kHz, 5.1 ch, 3953 Kbps [AVO, Ю.Сербин]
- 1808795: Аудио #3 Русский: DTS-HD MA, 48.0 kHz, 5.1 ch, 3809 Kbps [AVO, Д.Есарев]
**не покрыто · audio #N:** — 49
- 1775928: Audio #3: Русский (Дубляж [Мосфильм-Мастер]) / 6 channels / 448 Кб/с
- 1775928: Audio #4: Украинский (Дубляж [НЛО]) / 2 channels / 192 Кб/с
- 1775928: Audio #5: Украинский (MVO [Новий]) / 2 channels / 192 Кб/с
**не покрыто · аудио rus:** — 37
- 1279432: Аудио Rus: AC3 / 48.0 KHz / 2.0 / 192kbps / Профессиональный многоголосый - 1 канал
- 1767672: Аудио Rus: AC3, 6 ch, 640 Кбит/с; AC3, 2 ch, 640 Кбит/с Eng: E-AC3 Atmos, 48 kHz, 6 ch, 768 kbps |Original|
- 2006199: Аудио Rus: AC3, 48,0 kHz, 2 ch, 192 Кбит/сек | HDRezka Studio | Dub (1-10 серии)
**не покрыто · аудио №N:** — 31
- 1128869: Аудио №2: Украинский AC3, 48 kHz, 2.0 ch, ~192.00 kbps | Дубляж, M1
- 1299487: Аудио №1: русский (AC3, 6 ch, 640 Кбит/с) дубляж
- 1299487: Аудио №2: русский (DTS-HD MA, 6 ch, 3818 Кбит/с) авторский Гаврилов
## nnm
- страниц: 13677
- обычных: 12182
- legacy: 1495
- без меток: 2106
- без меток: нет аудио-строк: 1949
- без меток: только русская дорожка/не озвучка: 83
- без меток: ЕСТЬ дорожка озвучки: 74
- с метками: 11571
- строк дорожек найдено: 21260
- строк покрыто: 18452
- покрытие строк: 86.8%
**без меток · N | N:** — 222
- 1280472: 001 | 00:21min | 320kbps CBR | 48000 Hz | Joint Stereo | MP3 | 959,30 KB | 1.01 ???BGM | ???BGM
- 1280472: 002 | 05:52min | 320kbps CBR | 48000 Hz | Joint Stereo | MP3 | 13,55 MB | 1.02 ???????? | ????????
- 1280472: 003 | 13:17min | 320kbps CBR | 48000 Hz | Joint Stereo | MP3 | 30,55 MB | 1.03 ?????????(BGM)?????? | ?????????(BGM)??????
**без меток · N.** — 126
- 1138457: 01. Redo | Suzuki Konomi | 001 | 04:19 min | 320kbps CBR | 44100 Hz | Joint Stereo | MP3 | 9,94 MB
- 1138457: 02. Mobius | Suzuki Konomi | 002 | 04:17 min | 320kbps CBR | 44100 Hz | Joint Stereo | MP3 | 9,86 MB
- 1138457: 03. Sekai wa Kizu wo Dakishimeru (Acoustic ver.) | Suzuki Konomi | 003 | 05:47 min | 320kbps CBR | 44100 Hz | Joint Stereo | MP3 | 13,30 MB
**без меток · аудио:** — 26
- 1296989: Аудио: AC3, 2 ch, 192kbps [Закадровый перевод] MediaInfo
- 1296989: Аудио: AC3, 2 ch, 192kbps [Дублирование]
- 1296989: Аудио: AC3, 2 ch, 192kbps [Закадровый перевод Kinomania] MediaInfo
**без меток · аудио #N:** — 22
- 1320092: Аудио #1: AAC 2.0, 48.0 KHz, 192 Kbps | NewComers [Многоголосый, любительский]
- 1492421: Аудио #1: AC3, 2 ch, 48.0 KHz, 192 Kbps | Перевод: Любительский двухголосый | [AniDub]: JAM, Holly
- 1492421: Аудио #2: AC3, 2 ch, 48.0 KHz, 192 Kbps | Перевод: Любительский двухголосый | [Animedia]: Nazel, Freya
**без меток · #N:** — 17
- 1438922: #1: FLAC, Встроенное, 2 ch, 48 kHz, 24 bit, ~1351 kbps | | Оригинал;
- 1438922: #2: AAC, Встроенное, 2 ch, 48 kHz, ~192 kb/s | | AniLibria (Sharon, HectoR, Amikiri, Dejz, Hekomi, Narea, MyAska)
- 1438922: #3: AAC, Встроенное, 2 ch, 48 kHz, ~192-320 kb/s | | Wakanim
**без меток · заголовок :** — 8
- 1438660: Заголовок : x264 | 4784 Kbps | 19.84 fps | 1.36:1
- 1438660: Заголовок : [Musical Score] AC3 | 192 Kbps | 16 bits | 48 KHz | 2 chan
- 1525763: Заголовок : x264 | 4784 Kbps | 21.98 fps | 1.38:1
**без меток · переводчик:** — 7
- 1017562: Переводчик: AleX_MytH | Редактор: St_Ranger | Оформление: Jarly Список эпизодов:
- 1034792: Переводчик: SeshaRim | Редактор: Little Devil, Red-blue_heart | Оформление/Лирика: Nika_Elrik Тех. параметры:
- 682258: Переводчик: Sasha aka Zub | Переводчик/Редактор: Jenia aka Zub | Лирика: Мария Гостюхина Эпизоды:
**без меток · aac | N khz | Nch | ~N kbp** — 7
- 664483: AAC | 48 KHz | 2ch | ~192 Kbps - [BalFor & Viki] (отдельным файлом)
- 664483: AAC | 48 KHz | 2ch | ~192 Kbps - [DemonOFmooN & Lali] (отдельным файлом)
- 664483: AAC | 48 KHz | 2ch | ~128 Kbps - [loster01 & Emeri] (отдельным файлом)
**без меток · аудиодорожка N:** — 4
- 337900: Аудиодорожка 01: AC3 | 48khz | 2ch | 192kb/s Профессиональный (Многоголосый)(Рен-ТВ)
- 337900: Аудиодорожка 02: AC3 | 48khz | 2ch | 192kb/s Commentary By Chris Carter(только первая серия)
- 337900: Аудиодорожка 03: AC3 | 48khz | 2ch | 192kb/s Commentary By Dan Sackheim(только первая серия)
**без меток · aудио#N:** — 4
- 654940: Aудио#1: Русский: 48 kHz, AC3, 2/0 (L,R) ch, 192 kbps avg |Многоголосый, "Mega-Anime"|
- 665428: Aудио#1: Русский / дубляж, Dolby Digital / 5.1 / 48 kHz / 448 kbps
- 665428: Aудио#2: Русский / двухголосый (П.Гланц и И.Королёва), DTS / 5.1 / 48 kHz / 1510 kbps / 24-bit
**без меток · acN | N khz | Nch | N kbps** — 4
- 670747: AC3 | 48 KHz | 2ch | 128 Kbps - [Audio Commentary]
- 670747: AC3 | 48 KHz | 2ch | 192 Kbps - [MifSnaiper & KroshkaRu] (отдельным файлом)
- 670747: AC3 | 48 KHz | 2ch | 192 Kbps - [Eladiel & Jam] (отдельным файлом)
**без меток · кодек:** — 3
- 1138457: Кодек: MP3 | Битрейт: 320 Kbps | Тип битрейта: CBR | Источник: WEB
- 1140030: Кодек: FLAC | Битрейт: Lossless | Тип рипа: Tracks | Источник: WEB
- 1153137: Кодек: MP3 | Битрейт: 320 Kbps | Тип битрейта: CBR | Источник: WEB
**не покрыто · перевод:** — 1080
- 1014436: Перевод: Профессиональный двухголосый, авторский
- 1029447: Перевод: Профессиональный Многоголосый
- 1042803: Перевод: Многоголосый профессиональный - NewStudio
**не покрыто · аудио N:** — 793
- 1003946: Аудио 2: AC3 5.1, 384 Kbps (Польский - одноголосый закадровый)
- 1003946: Аудио 3: AC3 5.1, 384 Kbps (Русский - дублированный)
- 1013202: Аудио 1: AC3, 48 kHz, 2 channels, 192 kbps - MVO [Jaskier]
**не покрыто · аудио #N:** — 150
- 1019903: Аудио #1: PCM, 2 ch, 1536 Kbps - Немецкий дубляж
- 1019903: Аудио #2: PCM, 2 ch, 1536 Kbps - Немецкий дубляж
- 1021232: Аудио #3: AC3, 2 ch, 384 Kbps - Немецкий дубляж
**не покрыто · аудио:** — 97
- 1058509: Аудио: AC3, 6 ch, 640 kbps - Дубляж
- 1078261: Аудио: MP3, 2 ch, 128 Kbps | двухголосый закадровый Mediainfo:
- 1190443: Аудио: AC3, 2 ch, 192 kbps - Украинский, MVO "NLO TV"
**не покрыто · title :** — 76
- 1208293: Title : DD 5.1 @ 448 kbps - Dub | Blu-ray CEE |
- 1263024: Title : RUSSIAN DTS Dub
- 1568781: Title : Ukrainian DUB AC3 2.0 192 kbps (LINE)
**не покрыто · N.** — 76
- 1789505: 01. Русский, AAC LC, 2 ch, 48.0 kHz, ~192 kb/s - StudioBand (DUB)
- 1789505: 02. Русский, AAC LC, 2 ch, 48.0 kHz, ~192 kb/s - AniLibria (MVO)
- 1789505: 03. Русский, AC3, 2 ch, 48.0 kHz, ~224 kb/s - TVShows (MVO)
**не покрыто · заголовок :** — 63
- 1204506: Заголовок : | Original, AC3 CORE |
- 1212717: Заголовок : | Original, AC3 CORE |
- 1217830: Заголовок : | Original, AC3 Core |
**не покрыто · аудио#N:** — 58
- 1129717: Аудио#1: AAC-LC, 2 ch, 93 kbps, VBR - MVO Студия Drunk Tank
- 1129717: Аудио#2: AAC-LC, 2 ch, 72 kbps, VBR - MVO NewComers
- 1129717: Аудио#3: AAC-LC, 2 ch, 93 kbps, VBR - MVO Saint-Sound TV
**не покрыто · track #N:** — 46
- 1011093: Track #2: audio (DUB [iTunes])
- 1011093: Track #3: audio (AVO [Gaevsky])
- 1129717: Track #2: audio (MVO ￐ᄀ￑ツ￑テ￐ᄡ￐ᄌ￑マ Drunk Tank)
**не покрыто · #N:** — 43
- 1411675: #1: AAC LC, 2 ch, 48 kHz, ~192 kbps | | AniLibria (Lupin, Cleo-chan, Hekomi)
- 1411675: #2: AAC LC, 2 ch, 48 kHz, ~128 kbps (~192 — 1-3 эп.) | | AniDub (Berial, Jade, Trina_D, RiZZ_fisher)
- 1411675: #3: AAC LC, 2 ch, 48 kHz, ~128 kbps | | Crunchyroll
**не покрыто · аудио rus:** — 31
- 1020041: Аудио Rus: MPEG Layer 3, 48,0 kHz, 2 ch, 128 kb/s — многоголосый закадровый
- 1029753: Аудио Rus: MP3, 48,0 kHz, 2 ch, 128 kbps — многоголосый закадровый
- 1030983: Аудио Rus: AC3, 2 канала, 192 Кбит/с - двуголосый закадровый
**не покрыто · аудио №N:** — 29
- 1259348: Аудио №1: AC3, 2 ch, 192 kbps (MVO Good People с 1 -16 серию)
- 1259348: Аудио №2: AC3, 2 ch, 192 kbps (MVO OMSKBIRD records)
- 1259348: Аудио №3: AC3, 2 ch, 192 kbps (MVO IdeaFilm с 1 -11 серию)
## rutracker
- страниц: 11167
- обычных: 10029
- legacy: 1138
- без меток: 2415
- без меток: нет аудио-строк: 2048
- без меток: только русская дорожка/не озвучка: 229
- без меток: ЕСТЬ дорожка озвучки: 138
- с метками: 8752
- строк дорожек найдено: 22996
- строк покрыто: 17998
- покрытие строк: 78.3%
**без меток · аудио:** — 81
- 1370450: Аудио: RU - VO, AAC LC SBR, stereo, 48 KHz, ~80 kbps (в контейнере) (1-11 серии поверх английской дубляжа, 12-26 поверх японской аудиодорожки)
- 1623230: Аудио: язык: японский, перевод: оригинал, аудиокодек: AAC битрейт аудио: ~184 кб/с
- 2655927: Аудио: Аудио #1: язык: Английский, перевод: оригинал, кодек: AC3 2.0, битрейт: 192 кб/с
**без меток · аудио N:** — 66
- 3464619: Аудио 1: AC3, 448 kbps, 6ch - Язык Русский, озвучено - JeFerSon
- 5373913: Аудио 1: AC3, 6 ch, 384 kbps Язык Русский (многоголосый, закадровый) MC-Entertainment
- 5543025: Аудио 1: AC3, ~192 kbps, 48000 Hz, 2 ch., язык русский (в составе контейнера); озвучка: многоголосая Rokana, Баяна, Delete, Constantine
**без меток · аудиодорожка N:** — 17
- 5584690: Аудиодорожка 01: AC3 | 48khz | 2ch | 192kb/s Профессиональный (многоголосый закадровый)
- 5584690: Аудиодорожка 02: AC3 | 48khz | 2ch | 192kb/s Оригинал (english)
- 5666350: Аудиодорожка 01: AC3 | 48khz | 2ch | 192kb/s Профессиональный (многоголосый закадровый)
**без меток · ▪ rus:** — 17
- 5735110: ▪ RUS: FLAC, 48.0 kHz, 2.0 ch, 700 Kbps | двухголосая муж/жен от NIKITOS & Viki (SHIZA) [1-2]
- 5735110: ▪ RUS: AAC, 48.0 kHz, 2.0 ch, 200 Kbps | двухголосая муж/жен от NIKITOS & Viki (SHIZA) [3]
- 5735110: ▪ RUS: AC3, 48.0 kHz, 2.0 ch, 190 Kbps | одноголосая муж от aos_dubber (AOS)
**без меток · видео:** — 15
- 4011180: Видео: MPEG-4 AVC Video | NTSC | 1920x1080 | 23.976 fps | ~17851 kb/s | ~0.353 bit/pixel
- 4496716: Видео: AVC (x264) | 1280x720p | 23.976 fps | 2 044 Kbps | 0.093 bits/pix
- 4780285: Видео: MPEG-2 | 720x480 | 29.970 fps | 4 890 Kbps
**без меток · aудио:** — 8
- 6547393: Aудио: Opus, 2.0 канала, 48.0 кГц, 128 кбит/с | Crunchyroll | (многоголосая закадровая, TV)
- 6547393: Aудио: AC3, 2.0 канала, 48.0 кГц, 192 кбит/с | AniLibria | Озвучили: Silv, Hekomi (1-4, 6+ эп.), Cleo-chan (5 эп.), December, Malevich (многоголосая з
- 6547393: Aудио: AC3, 2.0 канала, 48.0 кГц, 192 кбит/с | Shiza Project | Озвучили: Лизавета, Daelit, Mamoru, AmnesiaMoon, Wozwald (многоголосая закадровая, TV+S
**без меток · аудио #N:** — 6
- 2655927: Аудио #2: язык: Французский, перевод: дублированный (дубляж), кодек: AC3 2.0, битрейт: 192 кб/с
- 4063013: Аудио #1: 48.0 kHz, 2ch, ~224 kbps avg (RUS) (DUB)
- 4506285: Аудио #1: 48 kHz, AC3 Dolby Digital, 2/0 (L,R) ch, ~256 kbps (DUB)
**без меток · качество:** — 5
- 2861316: Качество: HDTVRip | Формат: MP4 | Видео кодек: H.264 | Аудио кодек: AAC
- 3034531: Качество: WEBRip | Формат: MP4 | Видео кодек: H.264 | Аудио кодек: AAC
- 3078634: Качество: HDTVRip | Формат: MP4 | Видео кодек: H.264 | Аудио кодек: AAC
**без меток · ∙ озвучка:** — 4
- 4958219: ∙ Озвучка: многоголосая (жен. / жен. / муж. / муж.) (Jade & Nami_Li & NASTR & SpasmSound) Сэмпл.
- 4958219: ∙ Озвучка: одноголосая (жен.) (Kirigava Yuki) Сэмпл.
- 4958219: ∙ Озвучка: двухголосая (жен. / жен.) (Mutsuko_Air & Marie_Bibika) Сэмпл.
**без меток · аудио ac-N, Nhz, N kbps, N.** — 4
- 6526614: Аудио AC-3, 48000Hz, 192 Kbps, 2.0 | AniLibria | Многоголосая закадровая (Anzen, Hekomi, Ados, Amikiri, OkanaTsoy, HectoR, Gomer)
- 6526614: Аудио AC-3, 48000Hz, 448 Kbps, 5.1 | HDrezka Studio| Многоголосая закадровая
- 6526614: Аудио AC-3, 48000Hz, 192 Kbps, 2.0 | JAM | Одноголосая закадровая
**без меток · N.** — 3
- 4006090: 1. Russian (Dolby AC3, 2 ch, 192 kbps) - Дубляж Selena Int.
- 4006090: 4. Russian (Dolby AC3, 2 ch, 192 kbps) - Двухголосый, Флагман-трейд
- 4006090: 5. Russian (Dolby AC3, 2 ch, 192 kbps) - Двухголосый
**без меток · rus - aac, N kbps, N.** — 3
- 4916503: RUS - AAC, 192 kbps, 48.0 KHz, 2 ch Язык Русский (внешним файлом) двухголосый (жен+муж), NASTR и Cuba77
- 5091156: RUS - AAC, 192 kbps, 48.0 KHz, 2 ch Язык Русский (внешним файлом) двухголосый (муж+жен), Cuba77 & Trina_D
- 5135588: RUS - AAC, 192 kbps, 48.0 KHz, 2 ch Язык Русский (внешним файлом) одноголосый (жен), Eladiel
**не покрыто · аудио N:** — 3384
- 1033471: Аудио 1: Русский (Дублированный), DTS, 6ch (L, C, R, Ls, Rs +LFE), 768 кб/с
- 1083542: Аудио 1: Русский DD 2.0 (mono) 192kbps (дубляж)
- 114788: Аудио 3: Russian (Dolby AC3, 2 ch (L,R) ch, 192 kbps) дубляж (VHS-микс)
**не покрыто · аудио #N:** — 382
- 1561637: Аудио #4: 48 kHz, AC3 Dolby Digital, 2/0 (dual mono) ch, ~192.00 kbps avg (американский дубляж) -- серия 15
- 1775072: Аудио #1: Русский DTS 5.1, 48 KHz, 768 kbps - Дубляж
- 1786919: Аудио #1: Русский DTS 5.1, 48 KHz, 768 kbps - Дубляж
**не покрыто · аудио:** — 362
- 1007860: Аудио: RU - VO, AC3, stereo, 224 kbps, 48 kHz; JP - AC3, stereo, 4482 kbps, 48 kHz;
- 1028802: Аудио: Русский ДД 2.0 (дубляж) 192 Kbps., японский ДД 2.0 192Kbps
- 1165266: Аудио: Dolby AC3 48000Hz 2ch 256Kbps (дубляж); Dolby AC3 48000Hz 6ch 384Kbps (многоголосый); Dolby AC3 48000Hz 6ch 384Kbps (Английский)
**не покрыто · аудио#N:** — 114
- 1325281: Аудио#1: Russian DTS 5.1, 1536 kbps (Дубляж)
- 1643963: Аудио#1: Russian: 48 kHz, AC3, 3/2 (L,C,R,l,r) + LFE ch, ~640.00 kbps avg |Дубляж, Blu-ray CEE|
- 1643963: Аудио#2: Ukrainian: 48 kHz, AC3, 3/2 (L,C,R,l,r) + LFE ch, ~448.00 kbps avg |Дубляж|
**не покрыто · аудио N rus:** — 99
- 5399397: Аудио 1 Rus: AC3 / 48.0 KHz / 5.1 / 384kbps / Dub / R5
- 5424086: Аудио 1 Rus: AC3 / 48.0 KHz / 5.1 / 448 kbps / MVO / CP-Digital
- 5424086: Аудио 2 Rus: AC3 / 48.0 KHz / 2.0 / 192 kbps / MVO / Синема Трейд
**не покрыто · аудио №N:** — 97
- 2421299: Аудио №1: RUS AC3 48000Hz, 448 kbps, 16bit, 6ch - Дубляж XL-Media
- 2424106: Аудио №3: Russian: AC3 5.1, 448 kbps (Многоголосый закадровый)
- 2664885: Аудио №1: Русский (Дублированный), AC3 5.1, 448 kb/s
**не покрыто · аудио eng:** — 67
- 4186469: Аудио ENG: E-AC3 JOC, 6 ch, 48 kHz, 576 kbps |Original|
- 5042398: Аудио ENG: E-AC3 JOC, 6 ch, 48 kHz, 576 kbps |Original|
- 5411992: Аудио Eng: AC3, 48 kHz, 6 ch, 640 kbps |Original|
**не покрыто · русская:** — 52
- 4749024: русская: MPEG Layer 2 48000Hz stereo 256Kbps [Дубляж Нева-1]
- 5389600: русская: Dolby AC3 48000Hz ---|Stereo|--- 192Kbps [Карусель] (Серии 01-26) {Серии 16-18 DualMono}
- 5389600: русская: Dolby AC3 48000Hz -----|5.1|----- 384Kbps [ALEKS KV] (Серии 01-26)
**не покрыто · аудио rus:** — 50
- 3660326: Аудио Rus: 48 kHz, AC3 Dolby Digital, 2/0 (L,R) ch, ~192 kbps("Дубляж")
- 4321129: Аудио RUS: AC3, 5.1, 48 kHz, 16 bit, 448 kbps (полный дубляж, но песни на японском)
- 4413295: Аудио Rus: 48 kHz, AC3 Dolby Digital, 2/0 (L,R) ch, ~192 kbps (VO-production)
**не покрыто · видео:** — 37
- 2536708: Видео: H.264 | 852x480 | 23,976 fps | ~ 1500 kbps
- 3359571: Видео: H.264 | 848x480 | 23,976 fps | ~ 1083 - 1777 kbps
- 3553755: Видео: MPEG4 (H264) | 1280x720 | 23.98 fps | ~1860 kbps
**не покрыто · аудиоN:** — 23
- 1792844: Аудио1: AC3, 1/0 ch, 192Kbps, Russian (дубляж)
- 1814859: Аудио1: AC3 48000Hz 6ch 384Kbps (перевод А. Гаврилов)
- 1814859: Аудио3: AC3 48000Hz 2ch 192 Kbps (перевод С.Визгунов)
**не покрыто · N) russian:** — 11
- 1643485: 1) Russian: AC3 2.0, 192 kbps (многоголосный);
- 2219776: 1) Russian: DTS 5.1, 755 kbps (многоголоска, лицензия);
- 2432425: 1) Russian: AC3 5.1, 640 kvps (Дубляж);
+49 -49
View File
@@ -1,57 +1,57 @@
# Аудит датасета: data/labels.csv
строк в CSV: 56131
строк после подготовки: 46894 (train 42458 / test 4436)
источники: {'kinozal': 13963, 'rutracker': 16647, 'nnm': 16284}
уникальных нормализованных текстов: 25614
групп (релиз ∪ одинаковый текст): 7472
удалено: пустой текст 0, мусорный studio 156, не-озвучка (оригинал/субтитры) 6, противоречивые дубли 85 (групп-противоречий: 75), несколько дорожек в одной строке 20, лишние повторы (> 40 одинаковых) 18277
спан автора найден: 38583 (82.3%), не найден: 277
аугментировано морфологических вариантов: 9083
дорожек 18+: 1006 (из них без упоминания «18» в тексте: 19)
язык размечен: 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}
строк в CSV: 71339
строк после подготовки: 60944 (train 54686 / test 6258)
источники: {'kinozal': 15188, 'rutracker': 21303, 'nnm': 24453}
уникальных нормализованных текстов: 30761
групп (релиз ∪ одинаковый текст): 8743
удалено: пустой текст 0, мусорный studio 447, не-озвучка (оригинал/субтитры) 13, отказ от озвучки 184, противоречивые дубли 123 (групп-противоречий: 100), несколько дорожек в одной строке 7, лишние повторы (> 40 одинаковых) 20649
спан автора найден: 50477 (82.8%), не найден: 299
аугментировано морфологических вариантов: 10513
дорожек 18+: 1156 (из них без упоминания «18» в тексте: 27)
язык размечен: 38352 из 60944; со строкой «Аудио» во входе: 30844; классы: {'RUS': 36243, 'UKR': 1978, 'ENG': 90, 'KAZ': 21, 'JPN': 20}; редкие (<20, не учатся): {'DEU': 8, 'FRA': 13, 'CHI': 3, 'BEL': 2, 'GEO': 1, 'ITA': 14, 'KOR': 1, 'LAV': 1, 'UZB': 1, 'CZE': 5, 'SPA': 7}
## Распределение translation_type
- проф. многоголосый: 13192
- проф. дубляж: 12242
- авт. одноголосый: 5782
- неизвестно: 4215
- одноголосый: 2817
- проф. двухголосый: 2716
- люб. многоголосый: 1879
- многоголосый: 1346
- люб. двухголосый: 772
- люб. одноголосый: 711
- двухголосый: 456
- люб. дубляж: 408
- проф. одноголосый: 354
- авт. двухголосый: 2
- авт. многоголосый: 2
- проф. многоголосый: 15272
- проф. дубляж: 14484
- авт. одноголосый: 7096
- неизвестно: 6806
- одноголосый: 3717
- многоголосый: 3468
- проф. двухголосый: 3467
- люб. многоголосый: 2171
- двухголосый: 1557
- люб. двухголосый: 1001
- люб. одноголосый: 966
- люб. дубляж: 520
- проф. одноголосый: 410
- авт. двухголосый: 6
- авт. многоголосый: 3
## Примеры противоречий (один текст — разные типы)
- nnm:nnm-1104137 «ac-3, 48.0 khz, 1 ch, 192 kbps, cbr (русский, проф. многогол» -> ['многоголосый', 'проф. многоголосый']
- rutracker:6111749 «(св-дубль - вгтрк) [vhs] [rus ac3 2ch 192kbps]» -> ['неизвестно', 'проф. дубляж']
- nnm:nnm-1009875 «русский dts 5.1, 1509 kbps, 48.0 khz (двуголосый) п. гланц и» -> ['двухголосый', 'проф. двухголосый']
- nnm:nnm-1058230 «ac3, 2 канала, 384 кбит/сек - русский (многоголосый закадров» -> ['многоголосый', 'проф. многоголосый']
- nnm:nnm-1104203 «lpcm, 1 ch, 1152 kbps, 48,0 khz - русский (многоголосый ''ка» -> ['многоголосый', 'проф. многоголосый']
- 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 (русский - авторский ''андрей гаврилов''» -> ['авт. одноголосый', 'неизвестно']
- nnm:nnm-1126664 «1 ac3, 2 ch, 192 kbps - русский, многоголосый (св-дубль) [ru» -> ['многоголосый', 'проф. многоголосый']
- nnm:nnm-1141333 «ac3, 6 ch, 448 kbps (русский - многоголосый ''d2lab'') [rus » -> ['многоголосый', 'проф. многоголосый']
- nnm:nnm-1151582 «русский, aac, 2 ch, 192 kbps, 48,0 khz (многоголосый r5) med» -> ['многоголосый', 'проф. многоголосый']
- nnm:nnm-1154096 «ac3, 2 ch, 192 kbps (русский - двухголосый ''нтв+'') [rus ac» -> ['двухголосый', 'проф. двухголосый']
- nnm:1176495 «ac3, 2 ch, 192 kbps (русский - многоголосый ''первый канал''» -> ['многоголосый', 'проф. многоголосый']
## Спан не найден (примеры)
- [rutracker] studio=«datynet; : Куценко» text=«Одноголосый закадровый datynet; перевод: Куценко»
- [rutracker] studio=«datynet и Yuka_chan; :Куценко» text=«Любительский (двухголосый закадровый) datynet и Yuka_chan; перевод:Куц»
- [rutracker] studio=«Дубляж, Blu-ray CEE_ _2009г» text=«Профессиональный (дублированный) |Дубляж, Blu-ray CEE_[МОСФИЛЬМ]_2009г»
- [rutracker] studio=«AC3, 6ch , 448 кбит/с Ист-Вест *» text=«Русский (Дублированный), AC3, 6ch (L, C, R, Ls, Rs +LFE), 448 кбит/с |»
- [rutracker] studio=«ТК Россия, / озвучен в 2009 году» text=«Профессиональный (многоголосый закадровый) |ТК Россия, (Илья Хвостиков»
- [rutracker] studio=«den904 & DeadSno Janetta» text=«Любительский (двухголосый закадровый) den904 & DeadSno перевод Janetta»
- [rutracker] studio=«AC3, 6ch , 448 кбит/с Blu-ray CEE +» text=«Русский (Дублированный), AC3, 6ch (L, C, R, Ls, Rs +LFE), 448 кбит/с |»
- [rutracker] studio=«AC3, 6ch , 448 кбит/с Blu-ray CEE +» text=«Украинский (Дублированный), AC3, 6ch (L, C, R, Ls, Rs +LFE), 448 кбит/»
- [rutracker] studio=«AC3, 6ch , 384 кбит/с Киномания» text=«Русский (Профессиональный (многоголосый)), AC3, 6ch (L, C, R, Ls, Rs +»
- [rutracker] studio=«AC3, 6ch , 384 кбит/с Сербин» text=«Русский (Профессиональный (один голос)), AC3, 6ch (L, C, R, Ls, Rs +LF»
- [rutracker] studio=«AC3, 6ch , 384 кбит/с Гаврилов» text=«Русский (Профессиональный (один голос)), AC3, 6ch (L, C, R, Ls, Rs +LF»
- [rutracker] studio=«Oдноголосый Матвеев Алексей» text=«Oдноголосый закадровый |Матвеев Алексей|»
- [rutracker] studio=«1+1 / Новий канал украинский» text=«Профессиональный (двухголосый закадровый) 1+1 / Новий канал [Євген Паш»
- [rutracker] studio=«Дубляж, Blu-ray CEE_ _2007г» text=«Профессиональный (дублированный) |Дубляж, Blu-ray CEE_[Невафильм]_2007»
- [rutracker] studio=«Дубляж, Blu-ray CEE_ _2004г» text=«Профессиональный (дублированный) |Дубляж, Blu-ray CEE_[Невафильм]_2004»
- [rutracker] studio=«студия Кадр , по заказу РТР, 1995 г» text=«Профессиональный (дублированный): студия "Кадр" (при уч. студии "Нота"»
- [rutracker] studio=«Ancord & Noir Ancord» text=«Ancord & Noir (01-35) Ancord (36+)»
- [rutracker] studio=«Правильный от студии ПЭ http» text=«Любительский (Гоблинa, "Правильный от студии "Полный ПЭ" http://www.op»
- [rutracker] studio=«Тот самый с кассет» text=«Профессиональный (полное дублирование) + Aвторский перевод А.Михалёва »
- [rutracker] studio=«+ Aвторский А.Михалёва» text=«Профессиональный (полное дублирование) + Aвторский перевод А.Михалёва »
- [rutracker] studio=«но его довольно часто встречал» text=«Любительская (одноголосая) - озвучника не помню как зовут, но его озву»
- [rutracker] studio=«absurd95 & Eladiel» text=«absurd95 (м.) & Eladiel (ж.)»
- [rutracker] studio=«Доп.инфо о : Инна Королева и Петр Гланц» text=«Профессиональный (двухголосый закадровый) Доп.инфо о переводе: Инна Ко»
- [rutracker] studio=«Persona99 ^_^» text=«Persona99 (одноголосое закадровое женское) ^_^»
- [rutracker] studio=«Доп.инфо о НТВ+» text=«Профессиональный (двухголосый закадровый) Доп.инфо о переводе НТВ+»
- [rutracker] studio=«Доп.инфо о НТВ+» text=«Профессиональный (многоголосый закадровый) Доп.инфо о переводе НТВ+»
- [rutracker] studio=«эп. 1-9 , эп. 10-13» text=«эп. 1-9 [YakuSub Studio], эп. 10-13 [Advantage]»
- [rutracker] studio=«Кубик в кубе на ознакомительный день и преображение дома. На Банан не требуется» text=«Профессиональный (двухголосый, закадровый) Кубик в кубе на ознакомител»
- [rutracker] studio=«версия 2015» text=«Одноголосый закадровый Александр Герусов, версия озвучки 2015»
- [rutracker] studio=«Доп.инфо о НТВ+» text=«Профессиональный (многоголосый закадровый) Доп.инфо о переводе НТВ+»
+55 -90
View File
@@ -2,110 +2,75 @@
"history": [
{
"epoch": 0,
"loss": 1.0322551016006376,
"type_accuracy": 0.9871559633027523,
"type_macro_f1": 0.8369379389908281,
"span_exact": 0.8495412844036697,
"age_accuracy": 0.9926605504587156,
"age_f1": 0.9230769230769231,
"joint_exact": 0.8330275229357799
"loss": 0.874984362499072,
"lang_accuracy": 0.9868667917448405,
"lang_n": 1599,
"type_accuracy": 0.9707081942899518,
"type_macro_f1": 0.9569463713111677,
"span_exact": 0.617723396366333,
"age_accuracy": 1.0,
"age_f1": 1.0,
"joint_exact": 0.6028921023359288
},
{
"epoch": 1,
"loss": 0.06671750026621989,
"type_accuracy": 0.9889908256880734,
"type_macro_f1": 0.9153726455986941,
"span_exact": 0.9174311926605505,
"age_accuracy": 1.0,
"age_f1": 1.0,
"joint_exact": 0.9100917431192661
"loss": 0.17198748634640987,
"lang_accuracy": 0.9887429643527205,
"lang_n": 1599,
"type_accuracy": 0.9733036707452726,
"type_macro_f1": 0.9717413388263517,
"span_exact": 0.6447905079718206,
"age_accuracy": 0.9996292176492398,
"age_f1": 0.9873417721518988,
"joint_exact": 0.6292176492398962
},
{
"epoch": 2,
"loss": 0.03165916350924156,
"type_accuracy": 0.9926605504587156,
"type_macro_f1": 0.9186768671962279,
"span_exact": 0.9394495412844037,
"age_accuracy": 1.0,
"age_f1": 1.0,
"joint_exact": 0.9321100917431193
"loss": 0.1308889834726086,
"lang_accuracy": 0.9874921826141339,
"lang_n": 1599,
"type_accuracy": 0.9729328883945124,
"type_macro_f1": 0.9068459919629618,
"span_exact": 0.6566555431961438,
"age_accuracy": 0.9996292176492398,
"age_f1": 0.9873417721518988,
"joint_exact": 0.6384872080088988
},
{
"epoch": 3,
"loss": 0.018931047540375744,
"type_accuracy": 0.9908256880733946,
"type_macro_f1": 0.9158000930443979,
"span_exact": 0.926605504587156,
"age_accuracy": 1.0,
"age_f1": 1.0,
"joint_exact": 0.9192660550458716
"loss": 0.11097981555473346,
"lang_accuracy": 0.9931207004377736,
"lang_n": 1599,
"type_accuracy": 0.9699666295884316,
"type_macro_f1": 0.9059061793140248,
"span_exact": 0.6603633667037448,
"age_accuracy": 0.9996292176492398,
"age_f1": 0.9873417721518988,
"joint_exact": 0.6384872080088988
},
{
"epoch": 4,
"loss": 0.01237113052410642,
"type_accuracy": 0.9908256880733946,
"type_macro_f1": 0.9158000930443979,
"span_exact": 0.9431192660550459,
"age_accuracy": 1.0,
"age_f1": 1.0,
"joint_exact": 0.9339449541284404
},
{
"epoch": 5,
"loss": 0.009695118124439538,
"type_accuracy": 0.9908256880733946,
"type_macro_f1": 0.9158000930443979,
"span_exact": 0.9467889908256881,
"age_accuracy": 1.0,
"age_f1": 1.0,
"joint_exact": 0.9376146788990826
},
{
"epoch": 6,
"loss": 0.0085221077923288,
"type_accuracy": 0.9908256880733946,
"type_macro_f1": 0.9158000930443979,
"span_exact": 0.9431192660550459,
"age_accuracy": 1.0,
"age_f1": 1.0,
"joint_exact": 0.9357798165137615
},
{
"epoch": 7,
"loss": 0.007850127838939042,
"type_accuracy": 0.9908256880733946,
"type_macro_f1": 0.9158000930443979,
"span_exact": 0.9412844036697248,
"age_accuracy": 1.0,
"age_f1": 1.0,
"joint_exact": 0.9339449541284404
},
{
"epoch": 8,
"loss": 0.0069279176914798365,
"type_accuracy": 0.9908256880733946,
"type_macro_f1": 0.9158000930443979,
"span_exact": 0.9467889908256881,
"age_accuracy": 1.0,
"age_f1": 1.0,
"joint_exact": 0.9394495412844037
"loss": 0.09677471212526927,
"lang_accuracy": 0.9931207004377736,
"lang_n": 1599,
"type_accuracy": 0.9755283648498332,
"type_macro_f1": 0.9099159088603298,
"span_exact": 0.6674082313681868,
"age_accuracy": 0.9977753058954394,
"age_f1": 0.9285714285714286,
"joint_exact": 0.6477567667779014
}
],
"test": {
"type_accuracy": 0.996011046333231,
"type_macro_f1": 0.9608362408399977,
"span_exact": 0.9687020558453513,
"age_accuracy": 1.0,
"age_f1": 1.0,
"joint_exact": 0.9662473151273396
"lang_accuracy": 0.9946581196581197,
"lang_n": 3744,
"type_accuracy": 0.9761904761904762,
"type_macro_f1": 0.9057782572824744,
"span_exact": 0.6736976669862575,
"age_accuracy": 0.9998402045381911,
"age_f1": 0.9948186528497409,
"joint_exact": 0.6581975071907957
},
"cv_mean": {
"type_accuracy": 0.9967343071842485,
"type_macro_f1": 0.9581255225864929,
"span_exact": 0.9392830173430274,
"age_accuracy": 0.9997553679873908,
"age_f1": 0.9944724556489263,
"joint_exact": 0.9365250772813656
},
"train_seconds": 1013.3
"cv_mean": null,
"train_seconds": 773.1
}
+62 -115
View File
@@ -2,140 +2,87 @@
"history": [
{
"epoch": 0,
"loss": 1.8807837918323556,
"type_accuracy": 0.9211009174311927,
"type_macro_f1": 0.6878025559450562,
"span_exact": 0.5155963302752293,
"age_accuracy": 0.9486238532110092,
"age_f1": 0.0,
"joint_exact": 0.44403669724770645
"loss": 1.4419865617013066,
"lang_accuracy": 0.9874921826141339,
"lang_n": 1599,
"type_accuracy": 0.9588431590656284,
"type_macro_f1": 0.9580993705655323,
"span_exact": 0.5472747497219133,
"age_accuracy": 0.9985168705969596,
"age_f1": 0.9473684210526315,
"joint_exact": 0.5305895439377085
},
{
"epoch": 1,
"loss": 0.19847860801096406,
"type_accuracy": 0.9743119266055046,
"type_macro_f1": 0.9018587930989455,
"span_exact": 0.7981651376146789,
"age_accuracy": 1.0,
"age_f1": 1.0,
"joint_exact": 0.7853211009174312
"loss": 0.21983068588511678,
"lang_accuracy": 0.9881175734834271,
"lang_n": 1599,
"type_accuracy": 0.9647756766777902,
"type_macro_f1": 0.9694674213789587,
"span_exact": 0.6384872080088988,
"age_accuracy": 0.9996292176492398,
"age_f1": 0.9873417721518988,
"joint_exact": 0.6166110493140526
},
{
"epoch": 2,
"loss": 0.07505214461390836,
"type_accuracy": 0.9908256880733946,
"type_macro_f1": 0.9158000930443979,
"span_exact": 0.8330275229357799,
"age_accuracy": 1.0,
"age_f1": 1.0,
"joint_exact": 0.8293577981651377
"loss": 0.16362552792017163,
"lang_accuracy": 0.9899937460913071,
"lang_n": 1599,
"type_accuracy": 0.9736744530960326,
"type_macro_f1": 0.9647373413686515,
"span_exact": 0.6599925843529848,
"age_accuracy": 0.9996292176492398,
"age_f1": 0.9873417721518988,
"joint_exact": 0.6418242491657397
},
{
"epoch": 3,
"loss": 0.04440977191069969,
"type_accuracy": 0.9908256880733946,
"type_macro_f1": 0.9158000930443979,
"span_exact": 0.8807339449541285,
"age_accuracy": 1.0,
"age_f1": 1.0,
"joint_exact": 0.8733944954128441
"loss": 0.13469500560886233,
"lang_accuracy": 0.9912445278298937,
"lang_n": 1599,
"type_accuracy": 0.9632925472747497,
"type_macro_f1": 0.9647998951696939,
"span_exact": 0.6329254727474972,
"age_accuracy": 0.9996292176492398,
"age_f1": 0.9873417721518988,
"joint_exact": 0.6091954022988506
},
{
"epoch": 4,
"loss": 0.028807769589840285,
"type_accuracy": 0.9889908256880734,
"type_macro_f1": 0.9096810438971369,
"span_exact": 0.8660550458715597,
"age_accuracy": 1.0,
"age_f1": 1.0,
"joint_exact": 0.8605504587155963
"loss": 0.11948855609379865,
"lang_accuracy": 0.9912445278298937,
"lang_n": 1599,
"type_accuracy": 0.9655172413793104,
"type_macro_f1": 0.9657867430958291,
"span_exact": 0.6496106785317018,
"age_accuracy": 0.9996292176492398,
"age_f1": 0.9873417721518988,
"joint_exact": 0.6262513904338154
},
{
"epoch": 5,
"loss": 0.022258058590862728,
"type_accuracy": 0.9908256880733946,
"type_macro_f1": 0.9155121981076735,
"span_exact": 0.8954128440366973,
"age_accuracy": 1.0,
"age_f1": 1.0,
"joint_exact": 0.8880733944954129
},
{
"epoch": 6,
"loss": 0.017832277844529697,
"type_accuracy": 0.9908256880733946,
"type_macro_f1": 0.9155121981076735,
"span_exact": 0.9045871559633027,
"age_accuracy": 1.0,
"age_f1": 1.0,
"joint_exact": 0.8972477064220183
},
{
"epoch": 7,
"loss": 0.014963243124099362,
"type_accuracy": 0.9908256880733946,
"type_macro_f1": 0.9158000930443979,
"span_exact": 0.8862385321100917,
"age_accuracy": 1.0,
"age_f1": 1.0,
"joint_exact": 0.8788990825688073
},
{
"epoch": 8,
"loss": 0.012998920554111813,
"type_accuracy": 0.9908256880733946,
"type_macro_f1": 0.9158000930443979,
"span_exact": 0.9119266055045872,
"age_accuracy": 1.0,
"age_f1": 1.0,
"joint_exact": 0.9045871559633027
},
{
"epoch": 9,
"loss": 0.011474552309777529,
"type_accuracy": 0.9908256880733946,
"type_macro_f1": 0.9158000930443979,
"span_exact": 0.9100917431192661,
"age_accuracy": 1.0,
"age_f1": 1.0,
"joint_exact": 0.9027522935779817
},
{
"epoch": 10,
"loss": 0.010157171327384295,
"type_accuracy": 0.9908256880733946,
"type_macro_f1": 0.9158000930443979,
"span_exact": 0.9027522935779817,
"age_accuracy": 1.0,
"age_f1": 1.0,
"joint_exact": 0.8954128440366973
},
{
"epoch": 11,
"loss": 0.009997131634706275,
"type_accuracy": 0.9908256880733946,
"type_macro_f1": 0.9158000930443979,
"span_exact": 0.9045871559633027,
"age_accuracy": 1.0,
"age_f1": 1.0,
"joint_exact": 0.8972477064220183
"loss": 0.10735353268224741,
"lang_accuracy": 0.9912445278298937,
"lang_n": 1599,
"type_accuracy": 0.9695958472376714,
"type_macro_f1": 0.9629303509524766,
"span_exact": 0.639599555061179,
"age_accuracy": 0.9996292176492398,
"age_f1": 0.9873417721518988,
"joint_exact": 0.617723396366333
}
],
"test": {
"type_accuracy": 0.9941699907947223,
"type_macro_f1": 0.9576679877290362,
"span_exact": 0.941699907947223,
"lang_accuracy": 0.9959935897435898,
"lang_n": 3744,
"type_accuracy": 0.9678811121764141,
"type_macro_f1": 0.8999603592898254,
"span_exact": 0.6708213486736977,
"age_accuracy": 1.0,
"age_f1": 1.0,
"joint_exact": 0.9389383246394599
"joint_exact": 0.6506871204857783
},
"cv_mean": {
"type_accuracy": 0.9945265233277791,
"type_macro_f1": 0.9549157743958997,
"span_exact": 0.9167176505761322,
"age_accuracy": 0.9994757007005666,
"age_f1": 0.9892257817741743,
"joint_exact": 0.9125275899299806
},
"train_seconds": 589.2
"cv_mean": null,
"train_seconds": 241.3
}
+1
View File
@@ -0,0 +1 @@
{"fingerprint": "54756b3c3b", "n": 6258, "items": [{"dir": "models\\best", "name": "best", "profile": "base", "trained_fp": null, "backbone": "ai-forever/ruBert-base", "mb": 713, "created": "2026-11-09 12:57", "seconds": 773.1, "audio_context": true, "lang_head": true, "token_type_ids": true, "metrics": {"lang_accuracy": 0.9946581196581197, "lang_n": 3744, "type_accuracy": 0.9761904761904762, "type_macro_f1": 0.9057782572824744, "span_exact": 0.6736976669862575, "age_accuracy": 0.9998402045381911, "age_f1": 0.9948186528497409, "joint_exact": 0.6581975071907957}, "n": 6258, "n_unscorable": 0, "ci": [0.6463518685471816, 0.6698490395051715], "fingerprint": "54756b3c3b", "vs": null, "leak_risk": true}, {"dir": "models\\best_backup_20261109_121841", "name": "best_backup_20261109_121841", "profile": "base", "trained_fp": "6b426e4138", "backbone": "ai-forever/ruBert-base", "mb": 713, "created": "2026-11-02 19:05:56", "seconds": 1379.9, "audio_context": true, "lang_head": true, "token_type_ids": true, "metrics": {"lang_accuracy": 0.9943865276663993, "lang_n": 3741, "type_accuracy": 0.9485458612975392, "type_macro_f1": 0.8801508325527028, "span_exact": 0.6428571428571429, "age_accuracy": 0.9996804090763822, "age_f1": 0.9895833333333334, "joint_exact": 0.6152125279642058}, "n": 6258, "n_unscorable": 0, "ci": [0.6030904999707764, 0.6271931916511898], "fingerprint": "54756b3c3b", "vs": {"fixed": 422, "broken": 691, "p": 6.903749983548058e-16}, "leak_risk": true}]}
+2 -2
View File
@@ -2,7 +2,7 @@
| модель | type acc | type macro-F1 | span exact | age acc | joint exact | время, с |
|---|---|---|---|---|---|---|
| tiny2 | 0.994 | 0.958 | 0.942 | 1.000 | 0.939 | 589.2 |
| base | 0.996 | 0.961 | 0.969 | 1.000 | 0.966 | 1013.3 |
| tiny2 | 0.968 | 0.900 | 0.671 | 1.000 | 0.651 | 241.3 |
| base | 0.976 | 0.906 | 0.674 | 1.000 | 0.658 | 773.1 |
Лучшая модель: **base** -> `models/best`
+10 -8
View File
@@ -17,20 +17,22 @@ from pathlib import Path
from bs4 import BeautifulSoup
# декор и номер перед ключевым словом: «[*]Перевод (1):», «►Перевод №2:», «Аудио #1:»
_LEAD = r"^\s*(?:\[\*\]|[*•·►→▸▪◦]+\s*|[-–—]\s*)?"
_NUM = (r"(?:\(\s*\d+(?:\s*[-–]\s*\d+)?\s*\)|№\s*\d+(?:\s*[-–]\s*\d+)?"
r"|#\s*\d+(?:\s*[-–]\s*\d+)?|\d+(?:\s*[-–]\s*\d+)?)?")
_WORD = r"(?:Перевод|Озвучка|Озвучивание|Озвучено|Звук|Аудио|Дубляж)"
# строка дорожки: «Перевод:», «Перевод 2:», «Озвучивание:», «Аудио 1:», …
RE_LINE = re.compile(
r"^\s*(?:Перевод|Озвучка|Озвучивание|Озвучено|Звук|Аудио)"
r"\s*(?:#\s*\d+|\d+)?\s*[:.]\s*(?P<body>.+)$", re.I)
RE_LINE = re.compile(_LEAD + _WORD + r"\s*" + _NUM + r"\s*[:.]\s*(?P<body>.+)$", re.I)
# то же, но значение на СЛЕДУЮЩЕЙ строке (kinozal: «<b>Перевод:</b> значение»)
RE_KEY_ONLY = re.compile(
r"^\s*(?:Перевод|Озвучка|Озвучивание|Озвучено|Звук|Аудио)"
r"\s*(?:#\s*\d+|\d+)?\s*:\s*$", re.I)
RE_KEY_ONLY = re.compile(_LEAD + _WORD + r"\s*" + _NUM + r"\s*:\s*$", re.I)
# префикс строки дорожки (для получения «чистого» текста дорожки)
RE_LINE_PREFIX = re.compile(
r"^\s*(?:Перевод|Озвучиван\w*|Озвучк\w*|Озвучен\w*|Звук|Аудио)"
r"\s*(?:#\s*\d+|\d+)?\s*[:.]\s*", re.I)
_LEAD + r"(?:Перевод|Озвучиван\w*|Озвучк\w*|Озвучен\w*|Звук|Аудио|Дубляж)"
r"\s*" + _NUM + r"\s*[:.]\s*", re.I)
# признаки того, что в тексте есть информация о типе озвучки/авторе
RE_TYPE_HINT = re.compile(
+9 -2
View File
@@ -38,6 +38,10 @@ from .studio_norm import JUNK_STUDIO_RE, find_studio_span
from .trackinfo import context, model_text, parse_audio_line
RE_18 = re.compile(r"\b18\s*\+")
# строка-отказ от озвучки, попавшая в метки старыми парсерами («Перевод: Отсутствует»)
RE_NO_DUB_TEXT = re.compile(
r"(?i)^\W*(?:отсутств\w*|отсутсву\w*|не\s*требу\w*|немо[йе]\b|без\s*слов|без\s*перевода"
r"|нет\s*(?:перевода|озвучки)|без\s*диалог\w*|нет\s*диалог\w*)\b")
UNKNOWN_TYPES = {"неизвестно", "авт. неизвестно", "проф. неизвестно",
"люб. неизвестно"}
@@ -109,13 +113,16 @@ def main() -> None:
# --- 1..4: текст, тип, автор, спан --------------------------------
rows = []
dropped_empty = dropped_junk = dropped_type = dropped_multi = 0
dropped_empty = dropped_junk = dropped_type = dropped_multi = dropped_nodub = 0
span_misses: list[tuple[str, str, str]] = []
for r in raw_rows:
text = build_text(r) # без хвоста «Аудио»: спан автора ищем в чистом тексте дорожки
if not text:
dropped_empty += 1
continue
if RE_NO_DUB_TEXT.match(text):
dropped_nodub += 1
continue
# несколько дорожек в одной строке при одной метке — неоднозначный пример (после переиндексации
# такие строки уже разложены сборщиком; здесь отсекаются остатки старых данных)
nested = [p for p in _split_entries(text) if RE_TYPE_HINT.search(p)]
@@ -296,7 +303,7 @@ def main() -> None:
f"уникальных нормализованных текстов: {uniq_text}",
f"групп (релиз ∪ одинаковый текст): {len(roots)}",
f"удалено: пустой текст {dropped_empty}, мусорный studio {dropped_junk}, "
f"не-озвучка (оригинал/субтитры) {dropped_type}, "
f"не-озвучка (оригинал/субтитры) {dropped_type}, отказ от озвучки {dropped_nodub}, "
f"противоречивые дубли {len(drop)} (групп-противоречий: {len(contradictions)}), "
f"несколько дорожек в одной строке {dropped_multi}, лишние повторы (> {args.max_dup} одинаковых) {n_capped}",
f"спан автора найден: {with_studio} ({with_studio / max(len(rows), 1):.1%}), "
+10 -5
View File
@@ -258,12 +258,17 @@ def merge_title_tracks(tracks: list[dict], title_tracks: list[dict]) -> None:
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» — не тип
# кодек-строка: «Аудио 3: AC3 …», «Аудио Rus: …», «Аудиодорожка 1: …», «Audio #2: …»,
# «Аудио #1 Русский: …», «Аудио pic RUS: …», «Звук 1: …», «Track #2: …»
RE_AUDIO_PREFIX = re.compile(
r"(?i)^\s*(?:Аудио\w*|Audio|Звук\w*|Аудиодорожка\w*|Дорожка|Track)\s*[^:\n]{0,16}?[:.]")
# с типом озвучки: «Аудио 3: AC3 … (MVO, HDrezka Studio)»; «English Dubbing» — не тип.
# Аббревиатуры — с кириллическими двойниками («АVO», «МVO», «DVO»).
RE_CODEC_TYPE = (r"(?:[MМ][VВ][OО]|[DД][VВ][OО]|[AА][VВ][OО]|[DД][UУ][BВ]"
r"|(?<![A-Za-zА-Яа-яё])[VВ][OО](?![A-Za-zА-Яа-яё])"
r"|дубл|многоголос|дву[хг]?голос|одноголос|закадр|авторск)")
RE_CODEC_TRACK = re.compile(
r"(?i)^\s*(?:Аудио|Audio)\s*(?:№|#)?\s*\d*\s*:.*(?:\b(?:MVO|DVO|AVO|DUB|VO)\b"
r"|дубл|многоголос|двухголос|одноголос|закадр|авторск)")
r"(?i)^\s*(?:Аудио\w*|Audio|Звук\w*|Аудиодорожка\w*|Дорожка|Track)\s*[^:\n]{0,16}?:.*?" + RE_CODEC_TYPE)
RE_RU_TRACK = re.compile(r"(?i)русск|russian|\brus\b|украин|ukrain|\bukr\b")
RE_FOREIGN_TRACK = re.compile(
r"(?i)казах|english|англ|француз|немец|немецк|итальян|испан|японск|корейск|китайск"
+7 -5
View File
@@ -107,12 +107,12 @@ def load_raws(csv_path: Path = Path("data/labels.csv")) -> list[dict]:
# кодек-строка русской/украинской дорожки: «Аудио 2: AC3 … Русский (MVO, X)»,
# «Аудио Rus: AC3 … | LostFilm», «Аудиодорожка 1: … | Russian | TVShows»;
# «Аудио Rus: AC3 … | LostFilm», «Аудиодорожка 1: … | Russian | TVShows»,
# «Аудио #1 Русский: …», «Аудио pic RUS: …», «Звук 1: …»;
# сводка «Аудио: Русский, английский (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))")
r"(?i)^\s*(?:Аудио\w*|Audio|Звук\w*|Аудиодорожка\w*|Дорожка|Track)\s*[^:\n]{0,16}?[:.]"
r".*(?:русск|russian|\brus\b|украин|ukrain|\bukr\b)")
RE_TYPE_WORD = re.compile(
@@ -276,8 +276,10 @@ def train(out_dir: str = "models/segmenter", epochs: int = 4, batch: int = 32, l
opt.step()
sched.step()
opt.zero_grad()
if steps >= 4000 and (s // batch) % 2000 == 0:
print(f" epoch {ep + 1}: {s // batch}/{steps} шагов, loss {loss.item():.3f}", flush=True)
p, r = evaluate(te)
print(f"epoch {ep + 1}: test line precision {p:.3f} recall {r:.3f}")
print(f"epoch {ep + 1}: test line precision {p:.3f} recall {r:.3f}", flush=True)
Path(out_dir).mkdir(parents=True, exist_ok=True)
model.save_pretrained(out_dir)
tok.save_pretrained(out_dir)
+12 -2
View File
@@ -56,10 +56,20 @@ SUFFIX_NOISE = re.compile(
r"[\s\-–—|]*\(?\b(НЕНОРМАТИВНАЯ\s+ЛЕКСИКА|ненормативная\s+лексика|18\s*\+)\)?",
re.I)
# «мусорные» значения studio, которые автором не являются
# «мусорные» значения studio, которые автором не являются: отказы от озвучки,
# техпараметры, языки, источники, служебные пометки
JUNK_STUDIO_RE = re.compile(
r"^(?:\+?\s*вставки?|x4|viare|лицензия(?:\s+R5)?|полное дублирование R5"
r"|R5 Premier Digital|Гемини фильм R5)$", re.I)
r"|R5 Premier Digital|Гемини фильм R5"
r"|отсутств\w*|отсутсву\w*|без\s*перевода|не\s*требу\w*|немо[йе]|без\s*слов"
r"|в\s*составе\s*контейнера|поверх\s*\w+|по\s*умолчанию|оригинал\w*"
r"|(?:русск|украин|англ|япон|китай|корей|немец|француз|итальян|испан|казах)\w*"
r"|\d+\s*(?:ch|канал\w*|сери\w*)|.*\b(?:kbps|кбит|khz|кгц)\b.*"
r"|(?:e-?ac-?3|ac-?3|dts|aac|flac|mp3|opus|pcm|truehd)(?:\s+[\d.]+)?"
r"|\(?\d+\)?\s*[-–.)]\s*\S.*|\d+\s*[-–]?\s*\d*\s*эп\w*"
r"|тайминг\w*|редактор\w*|оформлени\w*|неизвестн\w*"
r"|\d+(?:[.,]\d+)?\s*(?:kbps|кбит|kb/s|khz|кгц|гц|hz|ch|канал\w*)"
r"|anidb|mal|shikimori|world\s*art|описание|список)$", re.I)
def clean_studio(raw: str) -> str:
+12 -2
View File
@@ -21,6 +21,7 @@ LANG_WORDS = [ # корень слова -> код; пор
("чешск", "CZE"), ("латв", "LAV"), ("литов", "LIT"), ("эстон", "EST"),
("грузин", "GEO"), ("армян", "ARM"), ("азербайдж", "AZE"), ("узбек", "UZB"),
("тюркск", "TUR"), ("турецк", "TUR"), ("португал", "POR"), ("хинди", "HIN"),
("rus", "RUS"), ("ukr", "UKR"), ("eng", "ENG"), # «Аудио RUS: …», «| Rus |»
]
LANG_CODES = {"RUS": "RUS", "UKR": "UKR", "ENG": "ENG", "BEL": "BEL", "KAZ": "KAZ",
"POL": "POL", "FRA": "FRA", "FRE": "FRA", "DEU": "DEU", "GER": "DEU",
@@ -28,7 +29,11 @@ LANG_CODES = {"RUS": "RUS", "UKR": "UKR", "ENG": "ENG", "BEL": "BEL", "KAZ": "KA
"CZE": "CZE", "LAV": "LAV", "LIT": "LIT", "EST": "EST", "GEO": "GEO",
"ARM": "ARM", "AZE": "AZE", "UZB": "UZB", "TUR": "TUR", "POR": "POR", "HIN": "HIN"}
RE_AUDIO = re.compile(r"(?im)^[^\S\n]*Аудио(?:[^\S\n]*0*\d*)?[^\S\n]*:?[^\S\n]*(?P<body>[^\n]+)$")
RE_AUDIO = re.compile(
r"(?im)^[^\S\n]*(?P<kw>Аудио\w*|Audio|Звук\w*|Аудиодорожка\w*|Дорожка|Track)"
r"(?P<mid>[^:\n]{0,16}?)[:.]\s*(?P<body>[^\n]+)$")
# «Аудио кодек: MP3», «Аудио битрейт: …» — поля описания, а не дорожка
RE_AUDIO_LABEL = re.compile(r"(?i)^\s*(?:кодек|битрейт|канал|язык)")
RE_LEAD_CODE = re.compile(r"^\s*([A-Za-z]{3})\s*:")
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)")
@@ -104,7 +109,12 @@ def audio_summary(info: dict | None) -> dict | None:
def audio_lines_all(text: str) -> list[dict]:
out = []
for m in RE_AUDIO.finditer(text or ""):
info = parse_audio_line(m.group("body"))
mid = (m.group("mid") or "").strip()
if RE_AUDIO_LABEL.match(mid):
continue
# язык может стоять до двоеточия: «Аудио RUS: АС3 …», «Аудио pic RUS: …»
info = parse_audio_line(f"{mid} {m.group('body')}".strip())
info["raw"] = m.group("body").strip()
if info["codec"] or info["channels"] or info["kbps"]: # настоящая строка дорожки
out.append(info)
return out
+8 -4
View File
@@ -929,8 +929,10 @@ def cmd_build(rewrite: bool = False, relabel: bool = False, runner: "Runner | No
if not d.exists():
continue
done = built.setdefault(site, {})
# обычные страницы — json+html; legacy (импорт старого harvest) — только json с
# текстом поста: метки по нему пересобираются текущими парсерами, страница не трогается
files = [jp for jp in sorted(d.glob("*.json"))
if not jp.name.endswith(".legacy.json") and jp.with_suffix(".html").exists()]
if jp.with_suffix(".html").exists() or jp.name.endswith(".legacy.json")]
ps = per[site] = {"files": len(files), "unchanged": 0, "pages": 0, "relabeled": 0,
"new": 0, "no_labels": 0, "t": time.time()}
task_id = prog.add_task(f"{site}: разбор HTML", total=len(files))
@@ -946,14 +948,16 @@ def cmd_build(rewrite: bool = False, relabel: bool = False, runner: "Runner | No
ps["unchanged"] += 1
prog.advance(task_id)
continue
rid = jp.stem
rid = jp.name.removesuffix(".json").removesuffix(".legacy")
stem = page_stem(site, rid, labeled)
known = rid in labeled.get(site, ()) or stem in labeled.get(site, ())
page = PAGES / f"{stem}.txt"
has_html = hp.exists()
# полная страница заменяет урезанную (текст поста) из импорта старого harvest
jobs.append({"site": site, "json": str(jp), "html": str(hp), "sig": sig, "stem": stem,
"known": known, "parse": not known or (relabel and stem not in manual),
"page": str(page) if (rewrite or not page.exists() or stem in postonly) else None})
"page": (str(page) if has_html and
(rewrite or not page.exists() or stem in postonly) else None)})
# проход 2: разбор HTML и запись страниц — параллельно
for job, res in zip(jobs, runner.map(rw.build_one, jobs)):
prog.advance(task_id)
@@ -1028,7 +1032,7 @@ def cmd_build(rewrite: bool = False, relabel: bool = False, runner: "Runner | No
# Версия разбора: поднимите при правке парсеров (harvest_core, html_to_text) — тогда
# кэш «уже разобрано» сбросится сам и данные пересоберутся один раз.
PARSE_VERSION = 4
PARSE_VERSION = 8
def load_json(path: Path) -> dict:
+273
View File
@@ -0,0 +1,273 @@
"""Аудит покрытия: все страницы data/raw против data/labels.csv.
Для каждой сохранённой страницы (35 тыс.) считает, что на ней есть строки дорожек
озвучки (независимыми от парсеров правилами), и что из этого попало в датасет:
* страницы без меток: есть ли на них дорожки вообще (русский оригинал/«не
требуется»/немой — не дорожка) и какие форматы строк не разобраны;
* страницы с метками: строки дорожек, которым не соответствует ни одна строка
датасета (пропущенные дорожки/студии);
* кластеры форматов (по префиксу строки) для правки парсеров.
Результат: reports/coverage_audit.md (цифры, кластеры, примеры),
data/raw/_coverage_pages.csv (по странице) и data/raw/_coverage_lines.csv
(непокрытые строки, до 20 тыс.).
.venv\\Scripts\\python tools\\coverage_audit.py [--cache data/raw/_coverage.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]
RAW = ROOT / "data" / "raw"
LABELS = ROOT / "data" / "labels.csv"
OUT_MD = ROOT / "reports" / "coverage_audit.md"
OUT_PAGES = RAW / "_coverage_pages.csv"
OUT_LINES = RAW / "_coverage_lines.csv"
SOURCES = ("kinozal", "nnm", "rutracker")
NO_STUDIO = "(не указана)"
# ---------------------------------------------------------------- детектор строк
# латинские/кириллические двойники в аббревиатурах: «АVO», «МVO», «DVO»
_HOMO = str.maketrans("АВЕКМНОРСТУХ", "ABEKMHOPCTYX")
def up(s: str) -> str:
return s.translate(_HOMO).upper()
RE_KW = re.compile(
r"(?i)^[\s\W]{0,6}(?:перевод\w*|озвуч\w*|озвучиван\w*|дубляж\w*|дубль\w*|звук\w*"
r"|аудио\w*|audio\w*|аудиодорожк\w*|дорожк\w*|track\w*)"
r"\s*(?:№|#)?\s*(?:\(?\d+\)?|\d+\s*[-–]\s*\d+|rus|ukr|jap|eng|pic|ru|uk)?\s*[:.]")
RE_PREFIX = re.compile(
r"(?i)^[\s\W]{0,6}(?:перевод\w*|озвуч\w*|озвучиван\w*|дубляж\w*|дубль\w*|звук\w*"
r"|аудио\w*|audio\w*|аудиодорожк\w*|дорожк\w*|track\w*)"
r"\s*(?:№|#)?\s*(?:\(?\d+\)?|\d+\s*[-–]\s*\d+|rus|ukr|jap|eng|pic|ru|uk)?\s*[:.]\s*")
RE_TYPE = re.compile(
r"(?i)(?:\bMVO\b|\bDVO\b|\bAVO\b|\bDUB\b|\bVO\b|дубл|многоголос|двухголос|двуголос"
r"|одноголос|закадр|авторск|любительск|профессиональн|озвучен|перевод\b)")
RE_NO_DUB = re.compile(
r"(?i)не\s*требу|немо[йе]|без\s*слов|без\s*диалог|нет\s*диалог|оригинал\w*\s*(?:дорожк|звук|аудио)?\s*[:.]?\s*$"
r"|^оригинал|субтитр|без\s*перевода|отсутств|не\s*озвуч|только\s*(?:музык|звук)|музыкальное\s*сопровожд")
RE_RU = re.compile(r"(?i)русск|russian|\brus\b|\bru\b|украин|ukrain|\bukr\b|україн")
RE_LANG = re.compile(
r"(?i)\b(русск\w*|russian|english|англ\w*|украин\w*|ukrainian|японск\w*|japanese"
r"|китайск\w*|корейск\w*|немецк\w*|французск\w*|итальянск\w*|испанск\w*|казахск\w*"
r"|оригинал\w*|original)\b")
RE_CODEC = re.compile(
r"(?i)\b(?:e-?ac-?3|ac-?3|ас3|dts|aac|truehd|flac|mp3|opus|l?pcm|mp2|dolby|ddp?)\b"
r"|\d\s*(?:kbps|кбит|kb/s|khz|кгц)")
RE_PIPE = re.compile(r"\|([^|]{2,60})\|")
RE_PAREN = re.compile(r"\(([^()]{2,80})\)")
def track_kind(line: str) -> str | None:
"""'dub' — строка дорожки озвучки, 'other' — аудио без признаков озвучки, None — не строка."""
s = line.strip()
if not s or len(s) > 300:
return None
m = RE_KW.match(s)
if not m:
# строка без префикса: «Russian (Dolby AC3, 6 ch) … | LostFilm» (nnm, rutracker)
if RE_CODEC.search(s) and (RE_RU.search(s) or RE_PIPE.search(s)) and len(s) < 220:
pass
else:
return None
body = s[m.end():] if m else s
if RE_NO_DUB.search(body) or (m and RE_NO_DUB.search(s[m.start():m.end() + 40])):
return None
if RE_TYPE.search(up(body)):
return "dub"
if RE_PIPE.search(body) or re.search(r"\([^()]*(?:mvo|dvo|avo|dub|дубл)", up(body)):
return "dub"
if RE_RU.search(body) and (RE_CODEC.search(s) or m and "перевод" in up(s[:30]).lower()):
return "other" # русская дорожка без типа: русский оригинал или неизвестный дубляж
return None
def norm(s: str) -> str:
return re.sub(r"\s+", " ", (s or "").replace("\u00a0", " ")).strip().casefold()
def line_prefix(line: str) -> str:
s = norm(line)
s = re.sub(r"\d+", "N", s)
m = re.match(r"^(.{0,26}?[:.])", s)
return (m.group(1) if m else s[:26]).strip()
def page_text(src: str, rec: dict) -> str:
if src == "kinozal":
return "\n".join(x for x in (rec.get("h1"), rec.get("tech_tab"), rec.get("release_tab")) if x)
return rec.get("post") or ""
# ---------------------------------------------------------------- загрузка
def load_pages(cache: Path | None) -> list[tuple[str, str, str, bool, bool]]:
if cache and cache.exists():
return pickle.loads(cache.read_bytes())
out = []
for src in SOURCES:
d = RAW / src
for name in sorted(os.listdir(d)):
if not name.endswith(".json"):
continue
rid = name.split(".")[0]
try:
rec = json.loads((d / name).read_text(encoding="utf-8"))
except (OSError, ValueError):
continue
out.append((src, rid, page_text(src, rec), "legacy" in name, bool(rec.get("ok", True))))
if cache:
cache.write_bytes(pickle.dumps(out))
return out
def row_strings(rows: list[dict]) -> list[str]:
out = []
for r in rows:
for k in ("raw", "audio_raw", "role_raw"):
v = norm(r.get(k) or "")
if len(v) >= 4:
out.append(v)
return out
def covered(line: str, rstr: list[str]) -> bool:
l = norm(line)
return any(r in l or (len(l) >= 12 and l in r) for r in rstr)
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--cache", type=Path)
args = ap.parse_args()
sys.stdout.reconfigure(encoding="utf-8")
pages = load_pages(args.cache)
rows = list(csv.DictReader(LABELS.open(encoding="utf-8-sig", newline="")))
by_rel = defaultdict(list)
for r in rows:
by_rel[(r["source"], re.sub(r"^(kz|nnm|rt)-", "", r["release_id"]))].append(r)
per = defaultdict(Counter)
fmt_unlab = defaultdict(Counter)
fmt_part = defaultdict(Counter)
per_rel = defaultdict(Counter)
gaps = [] # (src, rid, kind, line, why)
examples = defaultdict(list)
page_csv = []
rnd = random.Random(11)
for src, rid, text, legacy, ok in pages:
L = by_rel.get((src, rid), [])
lines = []
for raw in text.splitlines():
kind = track_kind(raw)
if kind:
lines.append((kind, re.sub(r"\s+", " ", raw).strip()))
dub = [l for k, l in lines if k == "dub"]
stat = per[src]
stat["страниц"] += 1
stat["legacy" if legacy else "обычных"] += 1
if not ok:
stat["ok=False"] += 1
page_csv.append({"source": src, "release_id": rid, "has_labels": int(bool(L)),
"n_rows": len(L), "n_lines": len(lines), "n_dub_lines": len(dub)})
if not L:
stat["без меток"] += 1
if not lines:
stat["без меток: нет аудио-строк"] += 1
elif not dub:
stat["без меток: только русская дорожка/не озвучка"] += 1
else:
stat["без меток: ЕСТЬ дорожка озвучки"] += 1
for l in dub:
key = line_prefix(l)
fmt_unlab[src][key] += 1
if len(examples[(src, "unlabeled", key)]) < 20:
examples[(src, "unlabeled", key)].append((rid, l))
gaps.append((src, rid, "no_labels", l, "есть дорожка, строки датасета нет"))
continue
stat["с метками"] += 1
rs = row_strings(L)
unmatched = [l for l in dub if not covered(l, rs)]
page_csv[-1]["n_unmatched"] = len(unmatched)
if unmatched:
per_rel[src]["релизов с непокрытыми строками"] += 1
per_rel[src]["непокрытых строк"] += len(unmatched)
for l in unmatched:
key = line_prefix(l)
fmt_part[src][key] += 1
if len(examples[(src, "covered", key)]) < 20:
examples[(src, "covered", key)].append((rid, l))
gaps.append((src, rid, "partial", l, "строка не попала в метки"))
stat["строк дорожек найдено"] += len(dub)
stat["строк покрыто"] += len(dub) - len(unmatched)
# ---------------------------------------------------------------- отчёт
out = ["# Покрытие страниц data/raw датасетом", "",
"Сгенерировано `tools/coverage_audit.py`. Строка «дорожки» — независимый от парсеров",
"детектор (префикс «Перевод/Озвучка/Аудио…», тип озвучки или блок `|студия|`);",
"«покрыта» — для строки есть строка `labels.csv` этого релиза (`raw`/`audio_raw`/`role_raw`).",
""]
for src in SOURCES:
stat = per[src]
out += [f"## {src}", ""]
for k in ("страниц", "обычных", "legacy", "ok=False", "без меток", "без меток: нет аудио-строк",
"без меток: только русская дорожка/не озвучка", "без меток: ЕСТЬ дорожка озвучки",
"с метками", "строк дорожек найдено", "строк покрыто"):
if stat[k]:
out.append(f"- {k}: {stat[k]}")
if stat["с метками"]:
found = stat["строк дорожек найдено"]
out.append(f"- покрытие строк: {stat['строк покрыто'] / max(found, 1):.1%}")
out.append("")
for key in sorted(fmt_unlab[src], key=fmt_unlab[src].get, reverse=True)[:12]:
out.append(f"**без меток · {key}** — {fmt_unlab[src][key]}")
for rid, l in examples[(src, "unlabeled", key)][:3]:
out.append(f" - {rid}: {l[:150]}")
out.append("")
for key in sorted(fmt_part[src], key=fmt_part[src].get, reverse=True)[:12]:
out.append(f"**не покрыто · {key}** — {fmt_part[src][key]}")
for rid, l in examples[(src, "covered", key)][:3]:
out.append(f" - {rid}: {l[:150]}")
out.append("")
OUT_MD.parent.mkdir(exist_ok=True)
OUT_MD.write_text("\n".join(out), encoding="utf-8")
with OUT_PAGES.open("w", encoding="utf-8-sig", newline="") as f:
w = csv.DictWriter(f, fieldnames=["source", "release_id", "has_labels", "n_rows",
"n_lines", "n_dub_lines", "n_unmatched"])
w.writeheader()
for r in page_csv:
w.writerow({**{"n_unmatched": ""}, **r})
with OUT_LINES.open("w", encoding="utf-8-sig", newline="") as f:
w = csv.writer(f)
w.writerow(["source", "release_id", "kind", "line", "why"])
w.writerows(gaps[:20000])
print(f"страниц {len(pages)}, непокрытых строк {len(gaps)}")
for src in SOURCES:
s = per[src]
print(f"== {src}: страниц {s['страниц']}, без меток {s['без меток']} "
f"(есть дорожка {s['без меток: ЕСТЬ дорожка озвучки']}), "
f"с метками {s['с метками']}, строк {s['строк дорожек найдено']}, "
f"непокрыто {s['строк дорожек найдено'] - s['строк покрыто']}")
print(f"отчёт: {OUT_MD.relative_to(ROOT)}")
if __name__ == "__main__":
main()
+408 -102
View File
@@ -996,6 +996,46 @@ def harvest_nnm(forums: list[str], topics: list[str], pages: int, limit: int,
CYR_TO_LAT = str.maketrans({"А": "A", "В": "B", "С": "C", "Е": "E", "Н": "H",
"К": "K", "М": "M", "О": "O", "Р": "P", "Т": "T", "Х": "X"})
# Декоративный префикс строки дорожки: «[*]Перевод (1):», «►Перевод:», «• Озвучка»,
# «- Перевод:», «№2:». Снимается до сопоставления с ключевым словом, сама строка
# (`raw`) остаётся в датасете как на странице.
RE_LEAD_DECOR = re.compile(r"^\s*(?:\[\s*\*?\s*\]|[*•·►→▸▪◦]+|[-–—]\s*|№\s*(?=\d))+\s*")
# номер дорожки: «(1)», «1-4», «№2», «#3», «1» — один шаблон для «Перевод»/«Аудио»
RE_TRACK_NUM = (r"(?:\(\s*\d+(?:\s*[-–]\s*\d+)?\s*\)|№\s*\d+(?:\s*[-–]\s*\d+)?"
r"|#\s*\d+(?:\s*[-–]\s*\d+)?|\d+(?:\s*[-–]\s*\d+)?)")
# язык в начале имени студии: «Русский, - AlexFilm», «Украинский, AMC»
RE_LANG_PREFIX = re.compile(
r"(?i)^(?:русск\w*|украин\w*|russian|ukrainian|english|англ\w*|японск\w*|китайск\w*"
r"|корейск\w*|немецк\w*|французск\w*|итальянск\w*|испанск\w*|казахск\w*)\s*[,:]\s*[-–—]?\s*")
# «X по заказу Y» — студия X, заказчик в метке не нужен
RE_ON_ORDER = re.compile(r"(?i)^(.+?)\s+по\s+заказу\s+.+$")
# «Перевод: Отсутствует», «Без слов», «Немой», «Не требуется» — дорожки озвучки нет
RE_NO_DUB = re.compile(
r"(?i)\b(?:отсутств\w*|отсутсву\w*|не\s*требу\w*|немо[йе]\b|без\s*слов|без\s*перевода"
r"|нет\s*(?:перевода|озвучки)|оригинал\w*\s*(?:дорожк\w*|звук\w*|аудио)\b"
r"|без\s*диалог\w*|нет\s*диалог\w*)\b")
# то же, но только если строка НАЧИНАЕТСЯ с «нет перевода»: «Профессиональный …,
# оставлена оригинальная дорожка» — это дорожка с пометкой, а не отказ от озвучки
RE_NO_DUB_LINE = re.compile(
r"(?i)^\W*(?:отсутств\w*|отсутсву\w*|не\s*требу\w*|немо[йе]\b|без\s*слов|без\s*перевода"
r"|нет\s*(?:перевода|озвучки)|без\s*диалог\w*|нет\s*диалог\w*)\b")
# пост nnm/rutracker: «Перевод:» может склеиться со следующим полем
RE_FIELD_CUT = re.compile(
r"\s+(?:Описание|Жанр|Год(?:\s*выпуска)?|Страна|Режиссер|Режиссёр|Актеры|Актёры"
r"|Продолжительность|Субтитры|Размер|Качество|Видео|Аудио|Формат)\s*:")
def trim_field(body: str) -> str:
"""Обрезать значение поля на следующем поле поста («… || AniDB», «… Описание:»)."""
return RE_FIELD_CUT.split((body or "").split("||")[0], maxsplit=1)[0].strip()
def strip_lead(line: str) -> str:
"""Снять декоративный префикс («[*]», «►», «•»): ключевое слово встаёт в начало."""
return RE_LEAD_DECOR.sub("", line or "", count=1)
STUDIO_ALIASES = {
"hdrezka": "HDrezka", "hd rezka": "HDrezka", "hdrezka studio": "HDrezka",
"red head sound": "Red Head Sound", "read head sound": "Red Head Sound",
@@ -1033,7 +1073,7 @@ STUDIO_NOISE = re.compile(
# «дубляж» как тип, а не часть имени студии («Дубляжная», «Продубляж», «Дубляж-Студия»)
RE_DUB_TYPE = r"дублир|(?<![а-яё])дубляж(?![а-яё])"
ROLE_PATTERNS = [(RE_DUB_TYPE, "дубляж"), (r"многоголос", "многоголосый"),
(r"двухголос", "двухголосый"), (r"одноголос", "одноголосый"),
(r"дву[хг]?о?голос", "двухголосый"), (r"одноголос", "одноголосый"),
(r"субтитр", "субтитры"), (r"оригинал", "оригинал")]
# 18+ у kinozal/rutracker/nnm стоит в строке дорожки; отдельная строка
@@ -1063,6 +1103,11 @@ def mark_18(note: str, age: bool) -> str:
def clean_studio(raw: str) -> tuple[str, str]:
note = ""
s = (raw or "").strip().strip("|").strip()
s = RE_LANG_PREFIX.sub("", s) # «Русский, - AlexFilm» -> «AlexFilm»
s = re.sub(r"(?i)^(?:озвучил\w*|озвучено|озвучивание|перевёл|перевел)\s*[-–—:]?\s*", "", s).strip()
m = RE_ON_ORDER.match(s) # «X по заказу Y» -> «X»
if m:
s = m.group(1)
s = re.sub(r"\[[^\]]*\]", " ", s)
s = re.sub(r"\([^)]*\)", " ", s)
if re.search(r"\b18\s*\+", s):
@@ -1100,6 +1145,8 @@ def translation_type(text: str) -> tuple[str, str]:
if re.search(r"любительск|псевдо", t): # любительский дубляж — отдельный класс, не «проф.»
return "люб. дубляж", "люб."
return "проф. дубляж", "проф."
if kind == "неизвестно" and "авторск" in t:
kind = "одноголосый" # «Авторский (Павел Санаев)» — один голос
if "профессиональн" in t:
pref = "проф."
elif "любительск" in t:
@@ -1115,7 +1162,8 @@ def translation_type(text: str) -> tuple[str, str]:
# ---- kinozal -------------------------------------------------------
KZ_RE_TRANSLATION = re.compile(
r"^\s*(?:Перевод|Озвучиван\w*|Озвучк\w*)\s*(?:#\s*\d+|\d+)?\s*:\s*(?P<body>.+)$", re.I)
r"^\s*(?:Перевод|Озвучиван\w*|Озвучк\w*|Дубляж)\s*" + RE_TRACK_NUM +
r"?\s*:\s*(?P<body>.+)$", re.I)
KZ_RE_AUDIO_X = re.compile(
r"^\s*[АA]удио(?:\s*#?\s*\d+)?\s*x\s*(?P<lang>[A-ZА-Я/]\w*)\s*:\s*(?P<body>.+)$", re.I)
KZ_RE_AUDIO = re.compile(r"^\s*[АA]удио\s*:\s*(?P<body>.+)$", re.I)
@@ -1172,7 +1220,7 @@ def _kz_parse_core(rec: dict) -> tuple[list[dict], dict]:
tech_text = (rec.get("tech_tab") or "").strip()
def has_transl(t: str) -> bool:
return any(KZ_RE_TRANSLATION.match(l) for l in t.splitlines())
return any(KZ_RE_TRANSLATION.match(strip_lead(l)) for l in t.splitlines())
if has_transl(release_text):
source, studio_source = release_text, "release"
@@ -1182,39 +1230,45 @@ def _kz_parse_core(rec: dict) -> tuple[list[dict], dict]:
translations: list[dict] = []
audios: list[dict] = []
subs = ""
# переводы берём только из приоритетной вкладки, иначе «Техданные»
# добавляют вторую, обезличенную строку «Перевод: Дублированный»
for line in source.splitlines():
line = line.strip()
m = KZ_RE_TRANSLATION.match(line)
if m:
body = m.group("body").strip()
def parse_tab(text: str, src: str) -> None:
"""«Перевод:» из вкладки: одна строка может называть несколько студий."""
for raw in text.splitlines():
line = strip_lead(raw.strip())
m = KZ_RE_TRANSLATION.match(line)
if not m:
continue
body = trim_field(m.group("body"))
if not body or RE_NO_DUB_LINE.match(body):
continue
actors = ""
am = KZ_RE_ACTORS.search(body)
if am:
actors = am.group("names").strip(" .")
role, candidates = "", []
role = ""
pm = re.search(r"\(([^)]*)\)", body)
if pm:
role = pm.group(1)
candidates = [body[pm.end():].strip(" |.-–—"), role,
body[:pm.start()].strip(" |.-–—")]
else:
candidates = [body]
studio, note = "", ""
for cand in candidates:
s1, n1 = clean_studio(cand)
s1, n2 = canonical_studio(s1)
if s1:
studio, note = s1, " ".join(x for x in (n1, n2) if x)
break
kind, pro = translation_type(body)
if re.search(r"не требуется", body, re.I):
kind = "оригинал"
translations.append({"studio": studio, "type": kind, "pro": pro,
"role": role, "actors": actors, "note": note,
"line": line})
for name in part_studios(body) or [""]:
translations.append({"studio": name, "type": kind, "pro": pro,
"role": role, "actors": actors, "note": "",
"line": raw.strip(), "src": src})
# переводы из обеих вкладок: «Техданные» дают тип, «Релиз» — студии
parse_tab(release_text, "release")
parse_tab(tech_text, "tech")
named_types = {t["type"] for t in translations if t["studio"]}
seen_t, uniq_t = set(), []
for t in translations:
if not t["studio"] and t["type"] in named_types:
continue # обезличенный дубль строки со студией
key = (t["type"], t["studio"].casefold(), t["line"])
if key in seen_t:
continue
seen_t.add(key)
uniq_t.append(t)
translations = uniq_t
# аудиодорожки и субтитры могут быть в любой из вкладок
for line in (source + "\n" + tech_text).splitlines():
@@ -1274,7 +1328,7 @@ def _kz_parse_core(rec: dict) -> tuple[list[dict], dict]:
"note": mark_18(note, age_18), "age_18": int(age_18),
"codec": a.get("codec", ""), "channels": a.get("channels", ""),
"khz": a.get("khz", ""), "kbps": a.get("kbps", ""),
"subtitles": subs, "studio_source": studio_source, "raw": t["line"],
"subtitles": subs, "studio_source": t["src"], "raw": t["line"],
"lang": (a.get("lang") or "RUS") if match else "",
})
rel = {"source": "kinozal", "release_id": rec["id"], "ru_title": info["ru_title"],
@@ -1287,9 +1341,11 @@ def _kz_parse_core(rec: dict) -> tuple[list[dict], dict]:
# ---- rutracker -----------------------------------------------------
RT_RE_FIELD = re.compile(
r"^\s*(?:Перевод|Озвучка|Озвучивание|Озвучено|Звук)\s*(?:#\s*\d+|\d+)?\s*:\s*(?P<body>.+)$", re.I)
r"^\s*(?:Перевод|Озвучка|Озвучивание|Озвучено|Звук|Дубляж)\s*" + RE_TRACK_NUM +
r"?\s*[:.]\s*(?P<body>.+)$", re.I)
RT_RE_BARE = re.compile(
r"^\s*(?:Перевод|Озвучка|Озвучивание|Озвучено|Звук)\s*(?:#\s*\d+|\d+)?\s*:\s*$", re.I)
r"^\s*(?:Перевод|Озвучка|Озвучивание|Озвучено|Звук|Дубляж)\s*" + RE_TRACK_NUM +
r"?\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)
@@ -1397,19 +1453,26 @@ def _rt_parse_core(rec: dict) -> tuple[list[dict], dict]:
for i, line in enumerate(lines):
if i in taken:
continue
m = RT_RE_FIELD.match(line)
bodies = [m.group("body")] if m else (rt_bare_list(lines, i, taken) if RT_RE_BARE.match(line) else [])
head = strip_lead(line)
m = RT_RE_FIELD.match(head)
bodies = [trim_field(m.group("body"))] if m else (
rt_bare_list(lines, i, taken) if RT_RE_BARE.match(head) else [])
if bodies:
for body in bodies:
for part in rt_split_entries(body):
if RE_NO_DUB_LINE.match(part):
continue
e = rt_parse_entry(part)
if e["type"] in ("оригинал", "субтитры"):
continue
if not e["studio"] and e["type"] == "неизвестно":
# несколько студий в одной части: «(многоголосый) — Jaskier, TVShows, LostFilm»
names = part_studios(part) or ([e["studio"]] if e["studio"] else [])
if e["type"] == "неизвестно" and not names:
continue
entries.append(e)
for name in names or [""]:
entries.append({**e, "studio": name})
continue
m = RT_RE_SUBS.match(line)
m = RT_RE_SUBS.match(head)
if m and not subs:
subs = m.group("body").strip()
rows = [{
@@ -1430,9 +1493,14 @@ def _rt_parse_core(rec: dict) -> tuple[list[dict], dict]:
# ---- nnmclub -------------------------------------------------------
NNM_RE_AUDIO = re.compile(r"^\s*Аудио\s*(?:#?\s*\d+)?\s*:\s*(?P<body>.+)$", re.I)
NNM_RE_PEREVOD = re.compile(r"^\s*Перевод\s*:\s*(?P<body>.+)$", re.I)
NNM_RE_AUDIO = re.compile(
r"^\s*(?:(?:Аудио|Audio)\s*[^:\n]{0,12}?|Звук\s*(?:[#№]\s*\d+|\d+)?)\s*[:.]\s*(?P<body>.+)$", re.I)
NNM_RE_PEREVOD = re.compile(
r"^\s*(?:Перевод|Озвучка|Озвучивание|Озвучено|Дубляж)\s*" + RE_TRACK_NUM +
r"?\s*:\s*(?P<body>.+)$", re.I)
NNM_LANG_RE = re.compile(r"(Русский|Английский|Украинский)", re.I)
# «Язык озвучки: Русский (Berofu, Takera, Euji [AniMaunt])» — имена без строки «Аудио»
NNM_RE_VOICE_LANG = re.compile(r"(?i)^\s*Язык\s*озвучк\w*\s*[:.]\s*(?P<body>.+)$")
NNM_LANG_MAP = {"русский": "RUS", "английский": "ENG", "украинский": "UKR"}
NNM_TAG_RE = re.compile(r"\b(DUB|MVO|DVO|AVO|VO|ДБ|ПМ|ПД|АП|ЛМ|ЛД|ЛО)\b", re.I)
NNM_RE_CODEC = re.compile(r"\b(Opus|AC-?3|E-?AC-?3|AAC|DTS-?HD|DTS|FLAC|MP3|TrueHD)\b", re.I)
@@ -1511,6 +1579,153 @@ def nnm_part_studio(part: str) -> str:
return ""
# ---------------------------------------------------------------- студии из перечислений
# корни «голосности»: по ним решается, режется ли перечисление через запятую
RE_TYPE_ROOT = re.compile(r"(?i)\b(дубл|многоголос|дву[хг]?голос|одноголос)")
# «Звук: X», «Озвучка: двухголосая (X)» — хвост с типом/именем в кодек-строке
RE_DESC_TAIL = re.compile(r"(?i)\b(?:звук|озвучк\w*|озвучиван\w*|перевод|дубляж)\s*[:.]\s*(.+)$")
# язык в начале скобки: «(Русский, Anilibria)» -> «Anilibria»
RE_PAREN_LANG = re.compile(
r"(?i)^\s*(?:русск\w*|russian|rus|ru|украин\w*|ukrainian|ukr|англ\w*|english|eng"
r"|японск\w*|japanese|jap|китайск\w*|корейск\w*|немецк\w*|французск\w*|итальянск\w*"
r"|испанск\w*|казахск\w*|оригинал\w*|original)\s*[,;:]?\s*")
NOT_STUDIO_WORD = re.compile(
r"(?i)^(?:по умолчанию|внешн\w*|встроенн\w*|в контейнере|в составе|оригинал\w*|комментар\w*"
r"|сезон\w*|сери\w*|эпизод\w*|фильм\w*|часть\w*|диск\w*|дорожк\w*|звук\w*|перевод\w*)$")
STUDIO_SEP = re.compile(r"\s*[,;|/]\s*")
SOURCE_RE = re.compile(r"(?i)^(?:blu-?ray|bd\b|dvd|web-?dl|webrip|hdtv|uhd|itunes|netflix|amazon|r5)")
RE_CODEC_PARAM_ANY = re.compile(
r"(?i)\b(?:e-?ac-?3|ac-?3|ас3|dts|aac|truehd|flac|mp3|opus|l?pcm|mp2|dolby|ddp?)\b"
r"|\d+(?:[.,]\d+)?\s*(?:kbps|кбит|kb/s|khz|кгц|гц|hz|ch|канал\w*)\b")
def is_studio_junk(s: str) -> bool:
"""Не имя студии: источник, тип, язык, техпараметры, «по умолчанию», число."""
low = (s or "").strip().lower()
return (len(low) < 3 or low in SOURCE_LIKE or low in STUDIO_PLACEHOLDERS
or bool(NOT_STUDIO_WORD.match(low)) or bool(CODEC_NOT_STUDIO.match(low))
or bool(SOURCE_RE.match(low)) or bool(RE_NO_DUB.search(low))
or bool(RE_CODEC_PARAM_ANY.match(low))
or bool(re.search(r"(?i)anidb|\bmal\b|shikimori|world\s*art|описан|список|эпизод|поверх"
r"|\bсери[ияй]\b|\bсезон\w*\b|неизвестн\w*", low))
or bool(re.fullmatch(r"[\d\s.,:/\-]+", low)))
def split_studios(text: str) -> list[str]:
"""Перечисление студий -> имена: «Jaskier, TVShows, HDrezka Studio, LostFilm»."""
out: list[str] = []
for part in STUDIO_SEP.split((text or "").strip(" |.-–—")):
st, _ = clean_studio(part.strip(" .|"))
st, _ = canonical_studio(st)
if st and not is_studio_junk(st) and st.casefold() not in {x.casefold() for x in out}:
out.append(st)
return out
def split_translation_parts(body: str) -> list[str]:
"""Значение «Перевод:» -> части-дорожки, не разрезая скобки.
«Профессиональный (многоголосый, закадровый), Авторский» — две дорожки;
«Авторский, одноголосый» — одна (второе слово уточняет первую);
«Профессиональное (многоголосое) / Любительское (одноголосое, Живов)» — две.
"""
text = (body or "").strip()
if not text:
return []
parts, depth, cur = [], 0, ""
for ch in text:
if ch in "([{":
depth += 1
elif ch in ")]}":
depth = max(0, depth - 1)
if ch in ",/" and depth == 0:
parts.append(cur)
cur = ""
else:
cur += ch
parts.append(cur)
parts = [p.strip() for p in parts if p.strip()]
roots = sum(bool(RE_TYPE_ROOT.search(p)) for p in parts)
starts = sum(bool(RT_RE_TYPE_START.match(p)) for p in parts)
return parts if len(parts) > 1 and (roots >= 2 or starts >= 2) else [text]
def part_studios(part: str) -> list[str]:
"""Студии из части «Перевод:»: хвост после тире, скобки, остаток без слов типа."""
names: list[str] = []
def add(chunk: str) -> None:
for n in split_studios(chunk):
if n.casefold() not in {x.casefold() for x in names}:
names.append(n)
m = re.search(r"\s[-–—:]\s*(.+)$", part)
if m:
add(m.group(1))
for pm in re.finditer(r"\(([^()]*)\)", part):
chunk = RE_PAREN_LANG.sub("", pm.group(1))
if chunk and not NNM_PART_TYPEISH.search(chunk):
add(chunk)
rest = re.sub(r"\([^()]*\)", " ", part)
rest = re.sub(r"(?i)\b(?:профессиональн\w*|любительск\w*|авторск\w*|дублированн\w*|многоголос\w*"
r"|дву[хг]?голос\w*|одноголос\w*|закадров\w*|перевод\w*|озвучк\w*|х?\d+\s*[xх])\b",
" ", rest)
add(rest)
return names[:8]
RE_LANG_TOKEN = re.compile(
r"(?i)(?<![a-zа-яё])(русск\w*|russian|rus|ru|украин\w*|ukrainian|ukr"
r"|англ\w*|english|eng|японск\w*|japanese|jap|китайск\w*|корейск\w*|немецк\w*"
r"|французск\w*|итальянск\w*|испанск\w*|казахск\w*|оригинал\w*|original)(?![a-zа-яё])")
def lang_tokens(text: str) -> list[str]:
"""Коды языков, названных в строке: «| Rus |», «(Русский, Anilibria)», «RUS(int)»."""
out: list[str] = []
for m in RE_LANG_TOKEN.finditer(text or ""):
w = m.group(1).lower()
code = ("RUS" if w.startswith(("русск", "russ", "rus")) or w == "ru"
else "UKR" if w.startswith(("укра", "ukra", "ukr")) or w == "uk"
else "ENG" if w.startswith(("англ", "engl", "eng")) or w == "en"
else "JPN" if w.startswith(("япон", "japan", "jap"))
else "OTHER")
if code not in out:
out.append(code)
return out
def audio_names(body: str) -> list[str]:
"""Имена студий из строки «Аудио …».
«Аудио 4: AC3 … (внешние) (Русский, Anilibria)» -> Anilibria;
«Аудио: AAC … | Rus | Звук: Zendos & Eladiel» -> Zendos & Eladiel;
«Аудио 2: … (MVO, LostFilm)» -> LostFilm.
"""
names: list[str] = []
def add(chunk: str) -> None:
chunk = (chunk or "").strip()
if not chunk or RE_CODEC_PARAM_ANY.search(chunk) and not RE_TYPE_ROOT.search(chunk):
return
for n in split_studios(chunk):
if n.casefold() not in {x.casefold() for x in names}:
names.append(n)
for seg in re.split(r"\|", body):
seg = seg.strip()
if not seg:
continue
dm = RE_DESC_TAIL.search(seg)
add(dm.group(1) if dm else seg)
for pm in re.finditer(r"\(([^()]*)\)", body):
chunk = RE_PAREN_LANG.sub("", pm.group(1))
if chunk and not NNM_PART_TYPEISH.search(chunk):
add(chunk)
return names[:8]
def _nnm_parse_core(rec: dict) -> tuple[list[dict], dict]:
post = rec.get("post") or ""
title = rec.get("page_title") or rec.get("title") or ""
@@ -1526,37 +1741,36 @@ def _nnm_parse_core(rec: dict) -> tuple[list[dict], dict]:
for line in post.splitlines():
line = line.strip()
m = NNM_RE_AUDIO.match(line)
if m:
body = m.group("body").strip()
lm = NNM_LANG_RE.search(body)
if not lm or NNM_LANG_MAP.get(lm.group(1).lower()) != "RUS":
continue # английскую/украинскую дорожку пропускаем
rest = body[lm.end():]
tm = NNM_TAG_RE.search(rest)
m = NNM_RE_AUDIO.match(strip_lead(line))
if m and not re.match(r"(?i)^\s*аудио\s*кодек", strip_lead(line)):
body = trim_field(m.group("body"))
if RE_NO_DUB_LINE.match(body):
continue
langs = lang_tokens(body)
if not langs or not ({"RUS", "UKR"} & set(langs)):
continue # английская/японская дорожка
lang = "RUS" if "RUS" in langs else "UKR"
flat = body.translate(CYR_TO_LAT)
tm = NNM_TAG_RE.search(flat)
tag = tm.group(1).upper() if tm else ""
low = body.lower()
if not tag: # формат «... - дубляж» без аббревиатуры
low = rest.lower()
if "дубляж" in low:
tag = "DUB"
elif "многоголос" in low:
tag = "MVO"
elif "двухголос" in low:
elif re.search(r"дву[хг]?голос", low):
tag = "DVO"
elif "авторск" in low:
tag = "AVO"
elif "одноголос" in low:
tag = "VO"
name_raw = ""
pm = re.search(r"\((.*)\)", rest)
if pm:
name_raw = pm.group(1).strip()
studio, note = nnm_studio(name_raw)
age_18 = has_18(rest)
note = mark_18(note, age_18)
ttype, pro = nnm_tag_type(tag)
if not ttype:
ttype, pro = "неизвестно", ""
names = audio_names(body)
if not names and ttype == "неизвестно":
continue # ни типа, ни имени — строке нечего дать датасету
codec_m = NNM_RE_CODEC.search(body)
kb_m = NNM_RE_KBP.search(body)
ch_m = NNM_RE_CH.search(body)
@@ -1565,46 +1779,78 @@ def _nnm_parse_core(rec: dict) -> tuple[list[dict], dict]:
lay = NNM_RE_LAYOUT.search(body)
if lay:
channels = str(int(lay.group(1)) + (1 if lay.group(2) != "0" else 0))
rows.append({
"source": "nnm", "release_id": rec["id"], "ru_title": ru_title,
"year": year, "quality": quality, "is_series": is_series,
"studio": studio or "(не указана)", "translation_type": ttype,
"professional": pro, "role_raw": tag, "actors": name_raw,
"note": note,
"age_18": int(age_18),
"codec": (codec_m.group(1).upper() if codec_m else ""),
"channels": channels,
"khz": "",
"kbps": (kb_m.group(1) if kb_m else ""),
"subtitles": "", "studio_source": "release", "raw": line,
})
continue
m = NNM_RE_PEREVOD.match(line)
if m:
for part in re.split(r"[,/]", m.group("body")):
part = part.strip()
if not part:
continue
ttype, pro = translation_type(part)
if ttype in ("неизвестно", "оригинал", "субтитры"):
continue
perevod_types.append(part)
# студия — если названа в части («Дублированный (Невафильм)»); без неё тип всё равно ценен
age_18 = has_18(body)
for name in names or [""]:
rows.append({
"source": "nnm", "release_id": rec["id"], "ru_title": ru_title,
"year": year, "quality": quality, "is_series": is_series,
"studio": nnm_part_studio(part) or "(не указана)", "translation_type": ttype,
"professional": pro, "role_raw": part, "actors": "",
"note": mark_18("", has_18(line)), "age_18": int(has_18(line)),
"codec": "", "channels": "", "khz": "", "kbps": "",
"subtitles": "", "studio_source": "tech", "raw": line,
"studio": name or "(не указана)", "translation_type": ttype,
"professional": pro, "role_raw": tag, "actors": name,
"note": mark_18("", age_18),
"age_18": int(age_18),
"codec": (codec_m.group(1).upper() if codec_m else ""),
"channels": channels,
"khz": "",
"kbps": (kb_m.group(1) if kb_m else ""),
"subtitles": "", "studio_source": "release", "raw": line, "lang": lang,
})
continue
m = NNM_RE_PEREVOD.match(strip_lead(line))
if m:
body = trim_field(m.group("body"))
for part in split_translation_parts(body):
if RE_NO_DUB_LINE.match(part):
continue
ttype, pro = translation_type(part)
if ttype in ("оригинал", "субтитры"):
continue
names = part_studios(part)
if ttype == "неизвестно" and not names:
continue
# студия — если названа в части («Дублированный (Невафильм)»); без неё тип всё равно ценен
for name in names or [""]:
rows.append({
"source": "nnm", "release_id": rec["id"], "ru_title": ru_title,
"year": year, "quality": quality, "is_series": is_series,
"studio": name or "(не указана)", "translation_type": ttype,
"professional": pro, "role_raw": part, "actors": name,
"note": mark_18("", has_18(line)), "age_18": int(has_18(line)),
"codec": "", "channels": "", "khz": "", "kbps": "",
"subtitles": "", "studio_source": "tech", "raw": line,
})
# страницы, где дорожка описана только полем «Язык озвучки: Русский (Актеры [Студия])»
if not rows:
for line in post.splitlines():
m = NNM_RE_VOICE_LANG.match(strip_lead(line.strip()))
if not m:
continue
body = trim_field(m.group("body"))
if "RUS" not in lang_tokens(body):
continue
names = audio_names(body)
for team in re.findall(r"\[([^\[\]]{2,60})\]", body):
for n in split_studios(team):
if n.casefold() not in {x.casefold() for x in names}:
names.append(n)
for name in names[:8]:
rows.append({
"source": "nnm", "release_id": rec["id"], "ru_title": ru_title,
"year": year, "quality": quality, "is_series": is_series,
"studio": name, "translation_type": "неизвестно", "professional": "",
"role_raw": "", "actors": "", "note": mark_18("", has_18(line)),
"age_18": int(has_18(line)), "codec": "", "channels": "", "khz": "", "kbps": "",
"subtitles": "", "studio_source": "release", "raw": line.strip(), "lang": "RUS",
})
# есть построчные «Аудио N» с именами — строки «Перевод:» (tech) не нужны; иначе
# оставляем tech-строки: типы и студии, названные в «Перевод:»
# есть построчные «Аудио N» с именами — строки «Перевод:» (tech) того же типа
# не нужны; типы, которых в «Аудио» нет, остаются (авторская дорожка и т. п.)
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"] != "(не указана)"]
rel_types = {r["translation_type"] for r in rows
if r["studio_source"] == "release" and r["studio"] != "(не указана)"}
rows = [r for r in rows
if r["studio_source"] == "release" or r["translation_type"] not in rel_types]
# дорожка без имени и без типа («- Русский») ничего не даёт датасету
rows = [r for r in rows
@@ -1815,7 +2061,8 @@ def kz_title_rows(rec: dict, text: str, rows: list[dict]) -> list[dict]:
# кодек-строка одной дорожки: «Аудио 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)")
r"(?i)^\s*[•#№*\-\s]*(?:(?:аудио\w*|audio|звук\w*|track|битрейт\s*аудио|title|заголовок)\b[^:\n]{0,24}:"
r"|№\s*\d+(?:\s*[-–]\s*\d+)?\s*:|#\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"
@@ -1824,7 +2071,8 @@ RE_LANG_WORD = re.compile(
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"|.*(?:русск|английск|украинск|russian|english|ukrainian|оригинал|original|японск|китайск"
r"|корейск|немецк|французск|итальянск|испанск|казахск)\w*.*"
r"|.*(?:голос|дублир|закадр|перевод).*|.*(?<![а-яё])дубляж(?![а-яё]).*|звук.*"
r"|.*(?:blu-?ray|itunes|netflix|amazon|\bbd\b|\bdvd\b|web-?dl).*)$")
@@ -1866,15 +2114,46 @@ def _known_studios() -> re.Pattern | None:
_KNOWN_RX: re.Pattern | None = None
def _rec_base(rec: dict) -> dict:
"""Метаданные релиза, когда размеченных строк ещё нет (для строк из кодек-линий)."""
src = rec.get("source") or ""
title = rec.get("page_title") or rec.get("title") or ""
if src == "kinozal":
info = kz_split_title(title.split(" :: ")[0])
return {"source": src, "ru_title": info["ru_title"], "year": info["year"], "quality": "",
"is_series": bool(re.search(r"сезон|серии", title, re.I))}
info = rt_parse_title(title)
return {"source": src, "ru_title": info["ru_title"], "year": info["year"],
"quality": info["quality"], "is_series": info["is_series"]}
def _line_names(line: str, info: dict, known: re.Pattern) -> list[tuple[str, bool]]:
"""Кандидаты в студии кодек-строки: словарь, `|…|`, `<…>`, `[…]`, `(…)`."""
cands: list[tuple[str, bool]] = [(m.group(1), True) for m in known.finditer(line)]
cands += [(n, False) for n in info["names"]]
for rx in (r"\|([^|]{2,60})\|", r"<([^<>]{2,60})>", r"\[([^\[\]]{2,60})\]", r"\(([^()]{2,80})\)"):
cands += [(m.group(1), False) for m in re.finditer(rx, line)]
seen, out = set(), []
for name, voc in cands:
k = name.strip().casefold()
if k and k not in seen:
seen.add(k)
out.append((name, voc))
return out
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:» со списком, где этой студии нет. Строка-вход — сегмент кодек-строки, как его
«Перевод N:» со списком, где этой студии нет, и строки вида «Аудио 1: язык: Русский,
перевод: многоголосый закадровый, кодек: AC3». Строка-вход — сегмент кодек-строки, как его
режет сервис (`src.release._entries`); тип — из самой строки (MVO, «дубляж»), иначе
единственный тип релиза, иначе «неизвестно». Сводка из нескольких языков
(«Аудио: Русский (AC3…), украинский (AC3…)»), оригинал и комментарии — пропускаются.
единственный тип релиза, иначе «неизвестно». Если у релиза ещё нет ни одной строки, из
кодек-строки берётся и дорожка без имени (тип без студии) — так размечиваются страницы,
где дорожки описаны только строками «Аудио». Сводки из нескольких языков, оригинал и
комментарии пропускаются.
"""
from src.release import _entries, same_studio
from src.trackinfo import parse_audio_line
@@ -1882,18 +2161,21 @@ def codec_line_rows(rec: dict, text: str, rows: list[dict], known: re.Pattern |
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 {}
base = rows[0] if rows else _rec_base(rec)
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:
if RE_NO_DUB_LINE.match(line):
continue
langs = set(lang_tokens(line))
if len(langs & {"RUS", "UKR", "ENG", "JPN", "OTHER"}) > 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):
if not info["lang"] and not (langs & {"RUS", "UKR"}) 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 ("", "")
@@ -1904,11 +2186,11 @@ def codec_line_rows(rec: dict, text: str, rows: list[dict], known: re.Pattern |
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:
names = _line_names(line, info, known)
for name, in_vocab in names:
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):
if len(studio) < 3 or is_studio_junk(studio):
continue
if CODEC_NAME_NOISE.search(studio) or not in_vocab and re.search(r"\d", studio):
continue
@@ -1928,7 +2210,31 @@ def codec_line_rows(rec: dict, text: str, rows: list[dict], known: re.Pattern |
"raw": seg, "lang": info["lang"] or ("UKR" if re.search(r"(?i)\bukr\b", line) else "RUS"),
"audio_raw": line})
have.append(studio)
return out
if not rows and not names and kind and kind != "неизвестно":
# дорожка без имени: датасету ценен хотя бы тип; только для релизов без строк вовсе
entries = _entries(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": "(не указана)", "translation_type": kind, "professional": pro,
"role_raw": "", "actors": "",
"note": mark_18("", info["adult"]),
"age_18": int(info["adult"]), "codec": info["codec"], "channels": info["channels"],
"khz": "", "kbps": info["kbps"], "subtitles": "", "studio_source": "codec",
"raw": entries[0] if entries else line,
"lang": info["lang"] or ("UKR" if re.search(r"(?i)\bukr\b", line) else "RUS"),
"audio_raw": line})
# один и тот же тип/студия из соседних строк-дублей
seen, uniq = set(), []
for r in out:
key = (r["studio"].casefold(), r["translation_type"],
re.sub(r"\s+", " ", r["raw"]).strip().casefold())
if key in seen:
continue
seen.add(key)
uniq.append(r)
return uniq
def _with_audio(core, texts):
@@ -1943,8 +2249,8 @@ def _with_audio(core, texts):
rows = kz_audio_only_rows(rec, text)
if core is _kz_parse_core:
rows += kz_title_rows(rec, text, rows)
rows += codec_line_rows(rec, text, rows)
if rows:
rows += codec_line_rows(rec, text, rows)
rel["n_translations"], rel["ok"] = len(rows), True
rel["studios"] = " | ".join(sorted({r["studio"] for r in rows}))
return rows, rel