Files
grabber-ai/docs/architecture.md
T
DenozordecandClaude Opus 5.5 8511744d3a
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]>
2026-11-09 16:24:52 +07:00

8.2 KiB
Raw Blame History

Архитектура

Пайплайн

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) сегментатор пока не включает: там по-прежнему правила.