Files
grabber-ai/docs/plan.md
T
DenozordecandClaude Opus 5.5 1f9ee46a0d kinozal: догрузка вкладки «Релиз» по URL; кодек-строки «Аудио N» как источник студий
На kinozal во встроенном #tabs только «Техданные» («Перевод:» без студий),
а строки «Аудио N: … (MVO, LostFilm)» лежат во вкладке «Релиз», которую
страница грузит JS. Поэтому по URL определялись только типы без студий.

* fetch: для kinozal скачиваются вкладки showtab(id,N)
  (get_srv_details.php?id=…&pagesd=N, кроме «Техданных» и «Скриншотов»)
  и дописываются в #tabs; недоступная вкладка пропускается.
* release: кодек-строки с типом озвучки берутся правилом codec_segments
  (сегментатор их отбрасывает — в разметке rutracker это не raw) и идут
  второстепенным источником (source=codec / page+codec): только дорожки
  со студией, дубли студии (same_studio) пропускаются, студия вписывается
  в дорожку того же типа без студии. «English Dubbing» и т.п. не берутся.
* merge_title_tracks переведён на общий merge_extra_tracks.

e2e test rutracker без изменений (P 0.988 / R 0.982); train-страницы
P 0.975 -> 0.951. Тесты: 77 ok.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-02 11:33:01 +07:00

104 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# План доработки: модель озвучки для grabber
## Цель
По ссылке на раздачу (rutracker / kinozal / nnm-club) или её HTML определить
**на уровне релиза**: какие дорожки озвучки есть, кто их сделал (студия /
переводчик), какого они типа (дубляж, многоголосый, …) и есть ли 18+.
Модель должна быть маленькой и встраиваемой в grabber.
## Что уже есть (анализ)
* Multi-task RuBERT (тип 14 классов + BIO-спан автора + 18+), 3742 строки,
joint exact 0.946 (base) / 0.915 (tiny2); ONNX-экспорт с parity 100%;
FastAPI-сервис; клиенты Rust/TS/PowerShell; тесты и документация.
* grabber (`C:\Users\shats\Dev\grabber`) — **PHP 8.1 + Docker**, интеграции
Radarr/Sonarr/qBittorrent/Prowlarr; студии с kinozal разбирает регулярками
(`KinozalReleaseParser.php`, `ProwlarrReleaseEnricher.php`), идёт Rust-порт.
Естественный путь интеграции — **HTTP-сервис** (PHP ходит curl-ом), ONNX
нужен для Rust-порта.
## Пробелы относительно задачи
| # | Пробел | Решение |
|---|---|---|
| 1 | Нет входа «по ссылке» (только `text`/`html`) | `url` в CLI, сервисе, API: fetch (таймаут, UA, cp1251), определение трекера по домену |
| 2 | Ответ — плоский список дорожек | Агрегат релиза: дедуп дорожек, уникальные студии с типами, `is_18plus` релиза; поле `tracks` не ломаем |
| 3 | Заголовок раздачи (`/ ПМ (LostFilm) /`) не используется | fallback-парсинг title, если дорожек 0 или студии нет в дорожках |
| 4 | kinozal: авторы грузятся JS | при работе по URL запросить вкладку озвучки напрямую (эндпоинт — из `data/samples/kinozal*.html`, не угадывать) |
| 5 | «Маленькая» модель: best = base (~440 МБ) | tiny2 (30M) + ONNX + int8-квантизация; решение по цифрам parity/размера |
| 6 | Precision 0.83 на rutracker | разобрать ложные срабатывания e2e — скорее всего регулярки сегментации |
| 7 | В git закоммичен `examples/rust/*/target/` | `.gitignore` + удалить из индекса |
## Порядок работ
1. **Baseline**: tests + e2e на текущих моделях (цифры в конец документа).
2. **URL + агрегат релиза** (`src/fetch.py`, `src/release.py`, эндпоинт
`POST /predict` с `url`, CLI `--url`).
3. **Title-fallback** в `html_extract.py`.
4. **kinozal-вкладка** (если эндпоинт найден в сэмплах).
5. **Разбор false-positives** сегментации.
6. **tiny2 + int8 ONNX**, замер размера/parity.
7. **Тесты** на каждый пункт, обновление docs/README.
8. `.gitignore` + чистка `target/`.
## Критерии готовности
* `python -m src.predict --url <ссылка>` печатает агрегат релиза;
* e2e precision/recall не хуже baseline;
* малая модель < ~40 МБ (int8), parity ≥ 99%;
* тесты зелёные, docs обновлены.
## Baseline
Модель `models/best` (ruBert-base), `tests/e2e_check.py`:
* rutracker: precision 0.826 / recall 0.984 (n=290);
* kinozal: 0.107 / 0.107 (n=28) — в текстовых дампах нет авторов (JS), потолок данных;
* smoke вне датасета: тип 9/10, автор 5/10, 18+ 10/10;
* `tests/run_tests.py`: 34 ok.
## Статус
| Шаг | Состояние |
|---|---|
| 1 baseline | сделано |
| 2 URL + агрегат релиза | сделано: `src/fetch.py`, `src/release.py`, `--url`, `POST /predict {"url"}` |
| 3 title-fallback | сделано (`ПМ (LostFilm)` → дорожка с `source: "title"`); заголовок берётся из HTML (`<h1>`/`<title>`), из текстового дампа (первая строка вида «… / год / …»), либо передаётся явно (`title`). Если на странице есть дорожка того же типа без студии, студия вписывается в неё (`source: page+title`), а не дублируется. **Эффект на e2e мал**: kinozal P/R 0.107 → 0.143, rutracker без изменений (0.826 / 0.984) — в разметке датасета студия из заголовка почти не нужна |
| 4 kinozal-вкладка | сделано: `fetch_html` для kinozal скачивает вкладки `showtab(id,N)` (`get_srv_details.php?id=…&pagesd=N`, кроме «Техданных» и «Скриншотов») и дописывает их в `#tabs`. Кодек-строки «Аудио N: … (MVO, LostFilm)» сегментатор отбрасывает (в разметке rutracker это не `raw`), поэтому они берутся правилом `codec_segments` как второстепенный источник (`source: codec` / `page+codec`): только дорожки со студией, студия-дубль (`same_studio`) пропускается. e2e test rutracker без изменений (P 0.988 / R 0.982); train-страницы P 0.975 → 0.951 |
| 5 false-positives сегментации | сделано: починена резка «Авторский, одноголосый» по запятой; постобработка спана (`refine_span`: до границ слова, мусор вроде `L,C,R,l,r`/кодеков → null). Остаток — промахи модели на `kbps \| DUB Автор` и особенности разметки |
| 9 переобучение | датасет вырос 3286 → 6864 строк; обе модели переобучены (см. ниже) |
| 6 tiny2 + int8 | сделано: 29.3 МБ, parity 99.8% тип / 99.2% спан / 100% 18+ (`reports/onnx_parity_tiny2_int8.md`) |
| 7 тесты | `run_tests.py`: 57 ok (URL allowlist/SSRF, декодирование, заглушки Cloudflare/логина, title, слияние, агрегат) |
| 8 `target/` | сделано |
## Открытые вопросы
* Подтвердить, что grabber (PHP) интегрируется по HTTP (рекомендуется), а
Rust-порт — через ONNX int8 (`models/tiny2-onnx/model.int8.onnx`).
* tiny2 vs base: held-out joint exact 0.915 против 0.946 (`reports/summary.md`); int8 сверху теряет до ~0.8% по спану (parity). e2e на страницах для tiny2 не гонялся.
* `--url` живьём не проверен: rutracker отдал 403 (бот-защита/гео), nnm — 404 на тестовый id. Проверено только на allowlist, декодировании и заглушках.
* Шаг 5 (false positives сегментации) и kinozal без авторизации остаются открытыми.
* Веса (`models/`, `tiny2-onnx`) в gitignore и лежат только в этом worktree/основном репозитории.
## Итоги после переобучения (датасет 6864 строк, 7780 после подготовки)
| | было | стало |
|---|---|---|
| held-out joint exact, base | 0.946 | 0.950 (тест теперь 516 строк — сравнение условное) |
| held-out joint exact, tiny2 | 0.915 | 0.924 |
| e2e rutracker precision / recall | 0.826 / 0.984 | 0.841 / 0.973 |
| smoke: автор вне датасета | 5/10 | 7/10 |
| tiny2 int8: размер, parity тип / спан / 18+ | 29.3 МБ, 99.8 / 99.2 / 100 | 29.3 МБ, 100 / **98.8** / 100 (ниже порога 99%) |
Для e2e есть страницы только старых релизов (`data/pages`), для новых строк их нет.
Если порог 99% по спану критичен — для встраивания брать fp32 tiny2 (116 МБ).
## Сегментация страницы: правила, а не нейросеть
Нейромодель получает уже вырезанный текст одной дорожки. Вырезает его
`src/html_extract.py` — регулярками («Перевод N:», разделители, запятые).
Поэтому ошибки вида «Дубляж, Продубляж» режется на две дорожки — это ошибки правил.
Следующий шаг (не сделан): токен-классификатор по строкам страницы, который
сам находит границы дорожек.