Обучение: * page_text/kinozal_text: текст kinozal = h1 + «Техданные» + «Релиз» из data/raw/kinozal/*.json (в pages/kz-*.txt нет заголовка и «Техданных», откуда половина меток kinozal; сервис видит обе вкладки). * label_page: позитив — raw или audio_raw; неразмеченная кодек-строка русской дорожки («Аудио 3: … Русский (MVO, X)», «Аудио Rus: … | X») получает -1 и в обучение не идёт: как негатив она учила отбрасывать кодек-строки. Сводка «Аудио: Русский, английский (…)» остаётся негативом. Инференс (release): * model_segments -> (основные, кодек); выбранные моделью кодек-строки идут второстепенным источником вместе с правилом codec_segments; иноязычные («Казахский (Dub, …)», «English Dubbing», «Original») отбрасываются. * merge_extra_tracks(adopt_type): кодек-дорожка с типом «неизвестно» получает тип единственной дорожки страницы без студии. e2e_segmenter: тот же текст kinozal, не больше E2E_MAX (400) страниц на трекер, train-страницы только с --train-pages (меню парсит только тест). Co-Authored-By: Claude Opus 5.5 <[email protected]>
97 lines
7.2 KiB
Markdown
97 lines
7.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).
|
||
|
||
Декодирование спана: первый `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/<id>.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) сегментатор пока не включает: там по-прежнему правила.
|