104 lines
8.7 KiB
Markdown
104 lines
8.7 KiB
Markdown
# План доработки: модель озвучки для 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-вкладка | **не сделано** в сервисе: страница релиза требует авторизации, а `get_srv_details.php?id=…&pagesd=N` уже вызывает `KinozalClient.php` в grabber. Рекомендация: grabber скачивает вкладку сам и шлёт `{"html": …, "title": <заголовок раздачи>}` (во фрагменте вкладки заголовка нет) |
|
||
| 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:», разделители, запятые).
|
||
Поэтому ошибки вида «Дубляж, Продубляж» режется на две дорожки — это ошибки правил.
|
||
Следующий шаг (не сделан): токен-классификатор по строкам страницы, который
|
||
сам находит границы дорожек.
|