# Архитектура ## Пайплайн ``` 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). Декодирование спана: первый `B`-токен, затем цепочка `I` → символьный диапазон через offsets → текст спана → каноническое имя автора через словарь алиасов (`src/studio_norm.py`, порт из генератора датасета). ## Почему автор — извлечение, а не классификация В датасете 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/.json` (`h1` + «Техданные» + «Релиз», `segmenter.page_text`): в `data/pages/kz-*.txt` нет ни заголовка, ни «Техданных», откуда взята половина меток kinozal, а сервис (`fetch_html`) видит обе вкладки. * Кодек-строка русской дорожки («Аудио 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. * Нарезка ВНУТРИ строки («Дублированный, многоголосый») в данных не размечена (91 группа из 6735) — остаётся на правилах `_split_entries`, но только на строках, выбранных моделью. * ONNX-путь (Rust/TS) сегментатор пока не включает: там по-прежнему правила.