Датасет: полное покрытие data/raw (71 339 строк из 30 401 релиза), правки парсеров и аудит
docker / build (push) Successful in 1m20s
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:
1 parent
d2cf488b9c
commit
0f359837c5
21 files changed
+38789
-22512
No files matched your search
@@ -14,6 +14,7 @@ reports/*.png
|
||||
|
||||
# локальное
|
||||
*.log
|
||||
.tmp/
|
||||
.DS_Store
|
||||
|
||||
# служебное ZCode
|
||||
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
+10
-2
@@ -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).
|
||||
|
||||
## Обучение по ссылкам
|
||||
|
||||
|
||||
@@ -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
@@ -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`.
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user