docker / build (push) Successful in 1m21s
Парсеры (PARSE_VERSION 9): «т/к» и единицы битрейта не режутся по слэшу, техпараметры (kb/s, Кбит/сек, 16 бит, channels, CBR, Atmos) и пол голоса (муж./жен.) не становятся студией, «от X» -> X; «N-й канал», «5 канал», «Лексикон» больше не выбрасываются как мусор; строка без типа со списком из 3+ имён (актёры, титры сабберов) не даёт дорожек; в кодек-строках имя рядом со скобкой пола становится кандидатом, а строка только с мусором даёт дорожку с типом без студии. Несколько авторов строки («Многоголосый (HDRezka Studio, LostFilm)» — верны все): prepare_data склеивает строки релиза в одну со списком spans, обучение размечает все спаны BIO, span_exact сравнивает наборы, decode_spans возвращает все прогоны; сервис отдаёт studio_spans / studios / studio_confidences, в ответе релиза — дорожка на каждого автора. Противоречий в датасете 20 % -> 1.2 %; проверочная tiny2 находит весь набор студий в 71 % строк с 2+ авторами (прежняя модель — 1 %). Разбор противоречий (collect.py, пункт 8): тексты, которые релизы разметили по-разному; решения в data/text_fixes.csv применяются в prepare_data поверх labels.csv. Метки и модель не пересобраны: нужны collect.py build --relabel, prepare_data, переобучение. Co-Authored-By: Claude Opus 5.5 <[email protected]>
108 lines
8.2 KiB
Markdown
108 lines
8.2 KiB
Markdown
# Архитектура
|
||
|
||
## Пайплайн
|
||
|
||
```
|
||
labels.csv (снимок датасета) HTML/текст страницы (инференс)
|
||
│ │
|
||
▼ ▼
|
||
src/prepare_data.py src/html_extract.py
|
||
• снять префикс «Перевод/Аудио N:» • HTML -> текст (BeautifulSoup:
|
||
• BIO-разметка автора (спан в тексте) .post_body / #tabs / весь текст)
|
||
• фильтры: мусорный studio, • сегментация на дорожки
|
||
«оригинал/субтитры», противоречия (строки «Перевод/Озвучка/Аудио N:»,
|
||
• 18+ флаг (age_18 / note / «18+» в raw) нумерация, «+ ; /», запятые-типы)
|
||
• сплиты: test 20% + 5-fold CV │
|
||
(группы = релиз ∪ одинаковый текст) ▼
|
||
│ src/predict.py (DubPredictor)
|
||
▼ • токенизация (max_len=128,
|
||
src/train.py offsets)
|
||
• multi-task обучение (CUDA, bf16) • модель -> тип, спан, 18+
|
||
• 5-fold CV + финальная модель • нормализация автора
|
||
• метрики, reports/, models/best • JSON
|
||
```
|
||
|
||
## Модель (src/model.py)
|
||
|
||
Один BERT-подобный энкодер и три головы (общий `[CLS]`/токенные состояния):
|
||
|
||
| Голова | Тип | Выход | Задача |
|
||
|---|---|---|---|
|
||
| `type_head` | Linear(hidden, 14) | `[CLS]` → класс | тип озвучки |
|
||
| `bio_head` | Linear(hidden, 3) | по токенам → O/B/I | позиция имени автора в тексте |
|
||
| `age_head` | Linear(hidden, 2) | `[CLS]` → 0/1 | дорожка 18+ |
|
||
|
||
Loss = CE(тип) + 1.0·CE(BIO, ignore_index=-100) + 0.5·CE(18+).
|
||
|
||
BIO-разметка строится по символьным спанам через `offsets_mapping`
|
||
токенизатора: токены, пересекающие спан, получают `B` (первый) и `I`
|
||
(остальные); все реальные токены вне спана — `O`; паддинг — `-100`.
|
||
Спецтокены ([CLS]/[SEP]) размечены как `O` — иначе их необученные логиты
|
||
при инференсе дают ложный «B» (эта ошибка была найдена при обучении и
|
||
исправлена; см. `decode_span` — он дополнительно пропускает токены с
|
||
пустыми offsets).
|
||
|
||
Декодирование (`decode_spans`): каждая цепочка `B`/`I` → символьный
|
||
диапазон через offsets → текст спана → каноническое имя автора через
|
||
словарь алиасов (`src/studio_norm.py`, порт из генератора датасета).
|
||
|
||
У строки может быть несколько авторов: «Многоголосый (HDRezka Studio,
|
||
LostFilm, TVShows)» значит, что верны все три. `prepare_data` склеивает
|
||
строки одного релиза с одинаковым текстом и типом в одну строку
|
||
(`spans` — все спаны, `span` — первый), BIO размечает все, `span_exact`
|
||
сравнивает наборы. Сервис отдаёт `studio_spans`/`studios`; в ответе
|
||
релиза такая строка раскладывается на дорожку на каждого автора.
|
||
|
||
## Почему автор — извлечение, а не классификация
|
||
|
||
В датасете 253+ уникальных авторов, 177 из них встречаются ровно 1 раз.
|
||
Классификацией такие классы не выучить. При этом имя автора почти всегда
|
||
дословно присутствует в тексте дорожки (95.8% + варианты написания),
|
||
поэтому BIO-извлечение решает задачу и автоматически работает для авторов,
|
||
которых в обучении не было вовсе.
|
||
|
||
## Сравнение конфигураций
|
||
|
||
Обе модели учатся одинаково (5-fold CV + финальный запуск, ранний stopping
|
||
по среднему type-F1/span/age-F1):
|
||
|
||
* `cointegrated/rubert-tiny2` — 30M, lr 1e-4, batch 64, до 20 эпох;
|
||
* `ai-forever/ruBert-base` — 110M, lr 3e-5, batch 32, до 10 эпох.
|
||
|
||
Лучшая по joint exact на held-out test копируется в `models/best`.
|
||
Итог — в `reports/summary.md`.
|
||
|
||
## Сегментатор страниц (src/segmenter.py) — главный путь
|
||
|
||
Какие строки страницы описывают дорожки, решает обученная модель
|
||
(rubert-tiny2, бинарная классификация строки с контекстом «пред. || след.»),
|
||
а не регулярки. Правила (`html_extract.extract_segments`) — запасной путь:
|
||
используются, если `models/segmenter` нет или модель не нашла ни одной строки;
|
||
режим можно форсировать: `predict_release(..., segmenter="rules")`.
|
||
|
||
* Обучение: `python -m src.segmenter --train --out models/segmenter_v2` (или меню
|
||
`tools/collect.py` → «2»). Разметка строк — по `raw` и `audio_raw` из `labels.csv`
|
||
(страницы `data/pages`); синтетика: `raw` всех train-строк вставляются в контекст
|
||
реальных строк страниц. Страницы делятся по хешу `release_id` (≈20% в тест).
|
||
* Текст kinozal берётся из `data/raw/kinozal/<id>.json` (`h1` + «Техданные» +
|
||
«Релиз», `segmenter.page_text`): в `data/pages/kz-*.txt` нет ни заголовка, ни
|
||
«Техданных», откуда взята половина меток kinozal, а сервис (`fetch_html`)
|
||
видит обе вкладки.
|
||
* Кодек-строки со студией с 02.11.2026 размечены сами (`studio_source=codec`, см.
|
||
`docs/collecting.md`): их `raw`/`audio_raw` дают позитивы. Кодек-строка русской
|
||
дорожки («Аудио 3: AC3 … Русский (MVO, LostFilm)», «Аудио Rus: … | LostFilm»),
|
||
не попавшая в разметку, в обучение не идёт
|
||
(метка -1): у rutracker `raw` — строка «Перевод N:», и как негатив она учила
|
||
модель отбрасывать кодек-строки — а на kinozal студии только в них.
|
||
* В `predict_release` выбранные моделью кодек-строки — второстепенный источник
|
||
(`source: codec`): подключаются, только если у какой-то дорожки страницы нет
|
||
студии; если других строк нет (nnm), они и есть дорожки.
|
||
* e2e: `tests/e2e_segmenter.py [каталог]` — пары (тип, студия) на тестовых
|
||
страницах, не больше `E2E_MAX` (400) на трекер; `--train-pages` — ещё и train;
|
||
основная модель — `E2E_MODEL` (по умолчанию `models/best`), например
|
||
`set E2E_MODEL=models/runs/<запуск>` перед запуском, чтобы сравнить кандидата.
|
||
* Нарезка ВНУТРИ строки («Дублированный, многоголосый») в данных не размечена
|
||
(91 группа из 6735) — остаётся на правилах `_split_entries`, но только на
|
||
строках, выбранных моделью.
|
||
* ONNX-путь (Rust/TS) сегментатор пока не включает: там по-прежнему правила.
|