Docker-сервис, выбор основы и сравнение моделей, сборщик датасета
docker / build (push) Failing after 39s

- Docker: Dockerfile (CPU), compose с FlareSolverr, настройка через DUB_* переменные,
  токен доступа к /predict, tools/pack_models.py, workflow Gitea Actions, docs/docker.md
- Модель грузится офлайн (config.json рядом с весами); общий код вынесен в src/spans.py,
  сервису не нужны sklearn/matplotlib/pandas
- Обучение: профили tiny2/base/large/modern, запуски в models/runs, реестр и честное
  сравнение на одной тестовой выборке (src/runs.py), пункт меню «Модели»
- Датасет: язык и аудио-параметры дорожек, 18+ по строкам «Аудио», ручные метки по ссылкам,
  переиндексация сохранённого HTML, параллельная сборка, сбалансированная выборка
- Тексты страниц (data/pages) больше не хранятся в git

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
DenozordecandClaude Sonnet 5.5 committed 2026-10-28 17:19:10 +07:00
1 parent d155b8584b
commit efc9a4f56a
411 files changed
+87495 -27363

No files matched your search

+1
View File
@@ -7,5 +7,6 @@
| [training-windows.md](training-windows.md) | Пошаговое обучение на Windows с GPU (RTX 5070 Ti / Blackwell), гиперпараметры, troubleshooting, переобучение на свежем датасете |
| [inference.md](inference.md) | CLI, Python API, JSON-контракт (таблица полей), обработка HTML, ограничения |
| [integration.md](integration.md) | Интеграция из Rust и TypeScript: HTTP-сервис (путь А) и ONNX без Python (путь Б), трейдоффы |
| [docker.md](docker.md) | Docker-образ сервиса, переменные окружения (в т.ч. FlareSolverr), API, модели в контейнере, CI/CD в Gitea, развёртывание на VPS |
Быстрый старт — в [README.md](../README.md) корня проекта.
+187
View File
@@ -0,0 +1,187 @@
# Сбор датасета: `tools/collect.py`
Интерактивный сборщик страниц rutracker / nnm-club / kinozal. Логин делает
человек в обычном окне Chrome, скрипт пароли и cookie не видит.
```powershell
.venv\Scripts\python tools\collect.py # меню
```
Главное меню состоит из двух пунктов и подменю:
1. **Собрать датасет**: окна, вход, сбор (новые и/или дособрать размеченные),
`build`, `prepare_data`.
2. **Переобучить и протестировать**: `prepare_data`, обучение сегментатора в
`models/segmenter_v2` (по желанию ещё и основной модели), `tests/run_tests.py`,
таблица «правила / старые веса / новые веса» по трекерам и вывод, лучше ли
новые; подмена весов только после вашего «y» (старые уходят в
`models/segmenter_old_<время>`).
3. Дополнительно: отдельные шаги из таблицы ниже.
Подменю «Дополнительно»:
| Пункт | Что делает |
|---|---|
| 1 | Запускает Chrome (один процесс, общий профиль), открывает по **окну на трекер**, ждёт Enter, проверяет статус входа (вошли / гость / не определено) |
| 2 | Новые раздачи из списков разделов (`viewforum.php?f=…`, для kinozal — адрес списка). По потоку на трекер, у каждого своё окно. Продолжает с места остановки |
| 3 | Дособрать страницы для раздач, которые **уже размечены, но страницы нет** (nnm, kinozal) |
| 4 | `data/raw` → `data/pages/*.txt` + новые строки в `data/labels.csv` |
| 5 | Импорт старого harvest без браузера (только текст поста) |
| 6 | Статус по трекерам |
Те же операции без меню: `windows`, `collect`, `backfill`, `build`,
`import-legacy`, `status` (см. `python tools/collect.py -h`).
## Интерфейс
Главное меню (rich) показывает сводку (размечено, страниц, meta, модели, порт
Chrome). Пункт 3 — статистика по трекерам: покрытие и состав датасета (тип,
разрешение, кодек, годы, языки). Во время сбора идёт живая панель: по каждому
трекеру состояние, прогресс, сохранено, ошибки, скорость (стр/мин по последним
сохранениям), время, ETA, текущий раздел и последняя раздача. `Ctrl+C`
останавливает сбор после текущей раздачи.
## Метаданные релизов: `data/meta.csv`
`build` (и команда `meta`) строит из сохранённых страниц таблицу, одна строка на
релиз (`tools/meta.py`; заходить на трекеры повторно не нужно):
| Группа | Поля |
|---|---|
| Название | `title`, `ru_title`, `orig_title`, `year`, `kind` (film / series / cartoon / anime), `is_series`, `season`, `episodes`, `episodes_total`, `section` |
| Видео | `quality`, `resolution`, `video_codec`, `hdr`, `bit_depth`, `fps`, `container`, `size_gb`, `duration_min` |
| О фильме | `countries`, `genres`, `directors`, `cast`, `age_limit`, `mpaa`, `release_group`, `source_media` |
| Звук | `audio_langs`, `orig_audio`, `n_audio`, `audio_tracks` (JSON: язык, кодек, каналы, битрейт, тип DUB/MVO/DVO/AVO/VO/ORIG, студия, серии, 18+) |
| Субтитры | `sub_langs`, `sub_forced`, `sub_format` |
Для старых данных (импорт `import-legacy`) поля берутся из текста поста;
размер раздачи rutracker и nnm виден только на полной странице, поэтому у них он
заполнится после дозагрузки (пункт «Дособрать»).
## Переиндексация без перекачки
HTML страниц хранится в `data/raw`, поэтому после правки парсеров данные
пересобираются без захода на трекеры: пункт 2 «Переобучить» начинается с вопроса
«Переиндексировать data/raw?» (то же: `collect.py build --rewrite --relabel`).
Страницы, `meta.csv` и метки размеченных релизов строятся заново текущими
парсерами; прежний `labels.csv` копируется в `data/labels.bak_<время>.csv`. Не
трогаются ручные метки (пункт 6), релизы без HTML (импорт старого harvest) и
релизы, по которым парсер ничего не нашёл.
## Обучение по ссылкам
Пункт 6 главного меню: вставляете ссылки на раздачи (rutracker / nnm / kinozal),
страницы скачиваются вашим Chrome, парсер строит метки, вы правите их в таблице
(«2 18» — переключить 18+, «3 d» — удалить, «1 t <тип>», «1 s <студия>») и
сохраняете. Метки заменяют прежние строки релиза в `data/labels.csv`
(`studio_source=manual`), id попадает в `data/manual_ids.txt`: такие релизы
всегда в train и повторяются при обучении ×5. Дальше «Переобучить».
Метки и инференс берут 18+ и параметры дорожки (кодек, каналы, битрейт) не только
из строк «Перевод», но и из строк «Аудио … |Студия| 18+» (`src/audio18.py`): у
одной студии может быть обычная дорожка и дорожка 18+.
## Автопоиск разделов
При первом запуске пункта 1 (режим «новые раздачи») после входа в аккаунты
приложение само находит разделы с зарубежными фильмами и сериалами
(`tools/discover.py`) и кэширует их в `data/raw/_sections.json`:
* **rutracker, nnm**: разделы выбираются по названию (зарубежное кино и сериалы,
мультфильмы, новинки, классика, годовые «Фильмы 2021-2025»; наше кино,
азиатское, музыка, Apple, коллекции исключены). Форматы DVD / HD / UHD / 3D
и годы покрываются самими разделами.
* **kinozal**: ваши списки «все фильмы» (`c=1002`) и «сериалы» (`c=46`), затем
сетка год × формат × категория (4K только с 2012 и т. д.).
* Лимит делится между разделами поровну, чтобы выборка не состояла из одного.
* Раздел, который не удалось проверить, остаётся в списке; результат неполной
загрузки кэш не затирает. Искать заново: «Дополнительно» → 7 или
`collect.py discover --force`.
## Сохранённые страницы читаемы
В `data/raw/<трекер>/<id>.html` лежит снимок страницы «как на сайте»: UTF-8
(`<meta charset>`), `<base>` на адрес раздачи (картинки подгружаются с сайта),
таблицы стилей вынесены в `data/raw/_css/<хеш>.css` (общие для всех страниц),
скрипты убраны, чтобы страница из файла не уходила на сайт. Открывается двойным
кликом. Таблицы стилей чужого домена (rutracker, nnm) остаются ссылкой на сайт и
работают при наличии интернета.
Страницы, сохранённые раньше, чинит `python tools/collect.py repair-html`
(кодировка, `<base>`, стили сайта одной загрузкой со страницы трекера; нужен Chrome
и вход, `--no-css` — без браузера). Время файлов не меняется, кэши разбора
остаются.
## Где что лежит
* `data/raw/<трекер>/<id>.html|json` — страница целиком (outerHTML после
загрузки вкладок kinozal) и поля для парсера. В git не попадает.
* Профиль Chrome — `%LOCALAPPDATA%\dub-ai-harvest\chrome-profile`, вне
репозитория (`DUB_CHROME_PROFILE`, `DUB_CHROME_PORT` меняют каталог и порт).
* `tools/harvest/harvest_core.py` — перенесённый `dataset.py` (CDP-клиент и
парсеры меток), единственная правка — переменные окружения для путей.
## id страниц
`release_id` в `labels.csv` совпадает с именем `data/pages/<id>.txt`.
Раздачи, размеченные раньше, сохраняют прежний числовой id (их разбиение
train/test не меняется); новые получают префикс `rt-`, `nnm-`, `kz-`, чтобы
id разных трекеров не пересекались. Коллизий в текущих данных нет, но один id
уже встречался в сырых данных двух трекеров.
## Полные и урезанные страницы
Импорт старого harvest (пункт 5) даёт страницу из сохранённого текста поста:
без шапки сайта, соседних раздач и заголовка. Такие страницы перечислены в
`data/pages_postonly.txt`. При обучении и оценке сегментатора их стоит
считать отдельно: на инференсе модель видит полную страницу.
## Что в метках слабое
Метки строятся парсерами (`tools/harvest/harvest_core.py`), поэтому они
повторяют логику правил. Для нарезки внутри строки («Дублированный,
многоголосый») нужна ручная проверка — режим ревью ещё не сделан.
## Разнообразная выборка (режим balanced)
Списки разделов идут от новых к старым, поэтому чтение подряд даёт только свежие
релизы. По умолчанию (`Настройки` → режим `balanced`, `tools/diversity.py`):
* страницы раздела берутся **вразброс по всей глубине** (число страниц читается из
пагинации; посещённые запоминаются в `_state.json`, круг по разделу продолжается
с места остановки);
* с каждой страницы берётся не всё, а `per_page` тем (по умолчанию 6) из **самых
малочисленных корзин** «год (до 2010 — десятилетие) × качество (4K / 1080 / 720 /
SD) × фильм/сериал»; уже собранное считается по `data/meta.csv`;
* режим `sequential` — прежнее чтение подряд.
## Скорость переиндексации
Разбор HTML (страницы, метки, `meta.csv`) идёт в нескольких процессах: по умолчанию
число ядер минус два (до 12); меняется в «Настройки» (`workers`, 0 — авто, 1 — без
процессов). `meta.csv` берёт текст из уже записанных `data/pages`, а не разбирает
HTML второй раз. На выборке в 600 раздач — 45 с → 8 с при тех же результатах.
## Если сбор «завис»
* В панели у трекера теперь видно номер раздачи, которая качается: `скачиваю 6894333`.
* Короткая страница (закрытая или перенесённая тема), которая загрузилась и не меняется,
принимается через ~5 с; раньше сборщик ждал таймаут 45 с и повторял его.
* Раздача дольше 40 с попадает в ошибки как «медленно, N с» (id можно открыть вручную).
* Сторож: раздача дольше 150 с — вызов к вкладке обрывается, вкладка переподключается, сбор
идёт дальше.
## Скорость и повторный разбор
* **Вкладки.** Сбор идёт в нескольких вкладках одного трекера (общий Chrome, общий
вход). Число вкладок — пункт меню «Настройки» (1–4, по умолчанию 3), запоминается;
в командах `collect` / `backfill` флаг `--tabs`. Больше вкладок — быстрее, но выше
риск лимитов и проверок Cloudflare: при ошибках снизьте до 1–2.
* **Без лишних пауз.** Ожидание вкладок kinozal и загрузки страниц опрашивает
состояние, а не спит фиксированное время; тяжёлые темы не ждут `complete`;
удалённые раздачи определяются сразу и запоминаются (`data/raw/_dead.json`).
* **Ничего не разбирается дважды.** Сбор пропускает уже сохранённые, размеченные и
удалённые раздачи и продолжает списки с места остановки. `build` запоминает
«подпись» файлов (`data/raw/_built.json`, `_meta_cache.json`): повторный запуск
разбирает только новое и изменённое (на 2800 релизах: 59 с → 2 с). Кэш сбрасывается
сам, если поднять `PARSE_VERSION` (collect.py) или `META_VERSION` (meta.py) после
правки парсеров; вручную: «Дополнительно» → 4 → «Пересобрать всё заново».
+277
View File
@@ -0,0 +1,277 @@
# Docker: запуск DUB-AI на VPS и сборка в Gitea CI/CD
Контейнер запускает HTTP-сервис (`python -m src.service`): по ссылке на раздачу или по HTML определяет
дорожки озвучки (тип, студия, 18+, язык, роль, параметры аудио). Для трекеров за Cloudflare сервис
обращается к вашему **FlareSolverr**, адрес которого задаётся переменной окружения.
```
клиент ──HTTP──▶ dub-ai (:8000) ──▶ трекер (rutracker / nnmclub / kinozal)
(grabber) │ ▲ при 403/503 или странице-заглушке
└─▶ FlareSolverr ──┘ (DUB_FLARESOLVERR_URL)
```
Содержание: [быстрый старт](#быстрый-старт) · [переменные окружения](#переменные-окружения) ·
[FlareSolverr](#flaresolverr) · [API](#api) · [модели](#модели-и-как-они-попадают-в-контейнер) ·
[CI/CD в Gitea](#cicd-в-gitea) · [VPS](#развёртывание-на-vps) · [проблемы](#если-что-то-не-работает)
## Что в репозитории
| Файл | Назначение |
|---|---|
| `Dockerfile` | образ: Python 3.12, torch (CPU), код сервиса, при наличии — модели |
| `requirements-service.txt` | зависимости сервиса (без sklearn/pandas/matplotlib/onnx — они нужны только обучению) |
| `docker-compose.yml`, `.env.example` | сервис + FlareSolverr, настройки через `.env` |
| `.dockerignore` | в контекст сборки попадают только `src/`, зависимости и `docker-models/` |
| `docker-models/models/` | сюда `tools/pack_models.py` кладёт модели для образа (в git не попадают) |
| `tools/pack_models.py` | упаковка `models/best` + `models/segmenter` для образа/CI |
| `.gitea/workflows/docker.yml` | сборка, проверка и публикация образа в реестр Gitea |
## Быстрый старт
Нужны Docker и модели (обучены и выбраны через `tools/collect.py`, пункт 7: `models/best`,
`models/segmenter`).
```bash
# 1. положить модели в контекст сборки (образ получится «с моделями»)
python tools/pack_models.py --stage docker-models
# 2. настройки
cp .env.example .env # впишите DUB_API_TOKEN (openssl rand -hex 24)
# 3. сборка и запуск вместе с FlareSolverr
docker compose up -d --build
# 4. проверка
curl http://localhost:8000/health
curl -X POST http://localhost:8000/predict \
-H "Authorization: Bearer <DUB_API_TOKEN>" -H "Content-Type: application/json" \
-d '{"url": "https://rutracker.org/forum/viewtopic.php?t=6894333"}'
```
Первый запуск контейнера грузит модель 5–15 с (`/health` отвечает после этого; healthcheck даёт на
старт до двух минут).
**Образ без моделей.** Если шаг 1 пропустить, образ соберётся пустым по моделям и при старте напишет,
какого каталога не хватает. Тогда смонтируйте модели с хоста — раскомментируйте `volumes` в
`docker-compose.yml`: `./models:/app/models:ro` (внутри нужны `models/best` и `models/segmenter`,
причём у `best` должен быть `config.json` — его создаёт `pack_models.py`; каталог из `pack_models.py
--stage` для этого и подходит: `-v ./docker-models/models:/app/models:ro`).
## Переменные окружения
Всё настраивается переменными (в `.env` для compose или `-e` у `docker run`). Аргументы командной
строки сервиса, если заданы, приоритетнее.
| Переменная | По умолчанию | Назначение |
|---|---|---|
| `DUB_FLARESOLVERR_URL` | не задана (в compose `http://flaresolverr:8191`) | адрес FlareSolverr без `/v1`; пусто — Cloudflare не обходится |
| `DUB_API_TOKEN` | не задан | если задан, `/predict` требует токен (см. [API](#api)); `/health` остаётся открытым |
| `DUB_FETCH_TIMEOUT` | `60` | таймаут загрузки страницы, с. FlareSolverr решает challenge 10–40 с, прежние 15 с не хватало |
| `DUB_TORCH_THREADS` | все ядра | потоков torch; на VPS с несколькими другими сервисами ограничьте |
| `DUB_MODEL_DIR` | `/app/models/best` | каталог основной модели |
| `DUB_SEGMENTER_DIR` | `/app/models/segmenter` | каталог сегментатора страниц; нет каталога — работают правила |
| `DUB_HOST`, `DUB_PORT` | `0.0.0.0`, `8000` | адрес/порт внутри контейнера (наружу — `DUB_PUBLISH_PORT` в compose) |
| `DUB_DEVICE` | авто | `cpu` или `cuda` (в образе только CPU-сборка torch) |
| `DUB_IMAGE` | `dub-ai:local` | (compose) какой образ запускать, напр. из реестра Gitea |
| `DUB_PUBLISH_PORT` | `8000` | (compose) порт на хосте |
В образе жёстко заданы `HF_HUB_OFFLINE=1` и `TRANSFORMERS_OFFLINE=1`: сервис ничего не скачивает с
Hugging Face. Все файлы моделей, включая архитектуру основы (`config.json`), лежат рядом с весами.
## FlareSolverr
Сервис сначала запрашивает страницу напрямую. Если трекер ответил 403/503 или вернул страницу-заглушку
(Cloudflare «Just a moment», форма входа), запрос повторяется через FlareSolverr
(`POST {DUB_FLARESOLVERR_URL}/v1`, `cmd=request.get`). Без FlareSolverr такие страницы дают ошибку 400
с подсказкой.
Три способа подключить:
1. **Вместе с compose (по умолчанию).** Сервис `flaresolverr` поднимается рядом, порт наружу не
публикуется, `dub-ai` ходит к нему по `http://flaresolverr:8191`.
2. **Свой FlareSolverr на другом хосте.** В `.env`: `DUB_FLARESOLVERR_URL=http://192.168.0.24:8191`, и
запускайте только `docker compose up -d dub-ai` (контейнер FlareSolverr не нужен). Адрес должен быть
достижим из контейнера: с VPS до домашней сети — через VPN/туннель, не открывайте FlareSolverr в интернет.
3. **`docker run`:**
```bash
docker run -d --name dub-ai -p 8000:8000 \
-e DUB_FLARESOLVERR_URL=http://flaresolverr:8191 -e DUB_API_TOKEN=... \
--network <сеть_с_flaresolverr> dub-ai:local
```
Проверить, что адрес подхватился: `GET /health` → `"flaresolverr": true`. Проверка связи:
`docker compose exec dub-ai python -c "import urllib.request as u; print(u.urlopen('http://flaresolverr:8191/').read()[:80])"`.
Замечания:
* FlareSolverr нестабилен: бывают `Timeout after 15.0 seconds` и ответ ~40 с. Это ошибка 400/502 для
одного запроса — клиенту стоит повторить запрос.
* kinozal требует авторизацию: передавайте `cookie` в теле запроса (см. ниже), он уйдёт и напрямую, и в
FlareSolverr.
* Сервис ходит только на rutracker, nnmclub и kinozal (список разрешённых хостов в `src/fetch.py`, в том
числе для редиректов): произвольные адреса через `url` не загружаются.
## API
Базовый адрес — `http://<хост>:8000`. Документация Swagger: `/docs`.
### `GET /health`
Без токена. Пример ответа:
```json
{"status": "ok", "model": "/app/models/best", "device": "cpu", "classes": ["авт. двухголосый", "..."],
"segmenter": "model", "flaresolverr": true, "auth": true}
```
`segmenter`: `model` — страницы режет обученный сегментатор, `rules` — запасные правила (нет `models/segmenter`).
### `POST /predict`
Ровно одно из полей тела:
| Поле | Что делает |
|---|---|
| `url` | сервис загружает страницу раздачи (с FlareSolverr при необходимости) и разбирает её; ответ дополнен `tracker` |
| `html` | разбор страницы, которую вы уже скачали (все дорожки релиза) |
| `text` | одна строка-дорожка: `«Перевод: Дублированный \| MovieDalen»` |
Необязательно: `title` (заголовок раздачи для `html`), `cookie` (для `url`, строка `a=b; c=d`).
Токен (если задан `DUB_API_TOKEN`): `Authorization: Bearer <токен>` либо `X-API-Key: <токен>`.
```bash
curl -s -X POST http://localhost:8000/predict \
-H "Authorization: Bearer $DUB_API_TOKEN" -H "Content-Type: application/json" \
-d '{"url": "https://rutracker.org/forum/viewtopic.php?t=6894333"}'
```
Ответ: `{"tracks": [...], "studios": [...], "is_18plus": bool, "tracker": "...", ...}`. Поля дорожки
(тип, студия, 18+, язык, роль, аудио-параметры, эпизоды) описаны в
[integration.md](integration.md) и [inference.md](inference.md).
| Код | Когда |
|---|---|
| 200 | успех |
| 400 | не то число полей; страница недоступна/заглушка (Cloudflare без FlareSolverr, FlareSolverr не справился); ссылка не с поддерживаемого трекера |
| 401 | задан `DUB_API_TOKEN`, а токена нет или он неверный |
| 502 | сеть: страницу не удалось загрузить |
## Модели и как они попадают в контейнер
Модели весят сотни МБ (`models/best` ≈ 700 МБ, `models/segmenter` ≈ 115 МБ), в git они не лежат
(`models/*` в `.gitignore`). Варианты:
| Способ | Плюсы | Минусы |
|---|---|---|
| **А. Запечь в образ** (`pack_models.py --stage docker-models`, в CI — через `MODELS_URL`) | образ самодостаточен, версия модели = тег образа | образ ≈ 2 ГБ, смена модели = новая сборка и pull |
| **Б. Смонтировать с хоста** (`-v ./models:/app/models:ro`) | образ лёгкий, модель меняется без пересборки (перезапуск контейнера) | модель нужно положить на VPS отдельно |
Для CI удобнее А. Чтобы CI знал модели, их нужно один раз положить в место, откуда он их скачает.
### Упаковка и выкладка в Gitea (Generic Packages)
```bash
python tools/pack_models.py # -> models-bundle.tar.gz (≈ 750 МБ)
# выкладка в реестр пакетов Gitea (версия — любая строка, например дата или хэш модели)
curl --fail -H "Authorization: token <GITEA_TOKEN>" \
--upload-file models-bundle.tar.gz \
https://<gitea>/api/packages/<owner>/generic/dub-ai-models/2026-10-28/models-bundle.tar.gz
```
Ссылка для скачивания (она же `MODELS_URL`):
`https://<gitea>/api/packages/<owner>/generic/dub-ai-models/2026-10-28/models-bundle.tar.gz`.
Если у вашей Gitea ограничен размер пакета (`[packages] LIMIT_SIZE_GENERIC`, по умолчанию без лимита, но
nginx перед ней часто режет загрузки ~1 МБ), поднимите лимиты или используйте способ Б.
Новая модель: обучить → выбрать в меню (пункт 7) → `pack_models.py` → выложить под новой версией →
обновить секрет `MODELS_URL` → запустить workflow (Actions → docker → Run workflow).
## CI/CD в Gitea
Workflow `.gitea/workflows/docker.yml` собирает образ и публикует его в **реестр контейнеров Gitea**
(`<gitea-host>/<owner>/<repo>/dub-ai`).
### Что настроить один раз
1. **Actions и раннер.** В Gitea включены Actions (`[actions] ENABLED=true`), в репозитории или
организации зарегистрирован `act_runner`. Раннеру нужен Docker: самый простой вариант — раннер на
хосте с метками вида `ubuntu-latest:host` (команды `docker build` идут на Docker хоста) или раннер в
контейнере с примонтированным `/var/run/docker.sock`. Образ-раннер (`ubuntu-latest:docker://…`) без
доступа к Docker daemon собрать образ не сможет.
2. **Secrets** (Settings → Actions → Secrets):
| Секрет | Значение |
|---|---|
| `REGISTRY_USER` | пользователь Gitea, под которым публикуется образ |
| `REGISTRY_TOKEN` | токен этого пользователя с правами `write:package` (и `read:package`) |
| `MODELS_URL` | необязательно: ссылка на `models-bundle.tar.gz`; нет — образ без моделей |
| `MODELS_TOKEN` | необязательно: токен с `read:package` для скачивания по `MODELS_URL` |
3. Реестр контейнеров Gitea доступен по HTTPS. Если Gitea по HTTP, добавьте её адрес в
`insecure-registries` Docker на раннере и на VPS (`/etc/docker/daemon.json`).
### Что делает workflow
| Шаг | Действие |
|---|---|
| проверка | `compileall` кода сервиса |
| модели | при заданном `MODELS_URL` скачивает архив в `docker-models/`, проверяет `models/best/model.pt` |
| теги | `sha-<8 символов коммита>`; на тег `vX.Y.Z` — ещё и `vX.Y.Z`; на `main`/`master` — ещё и `latest` |
| сборка | `docker build` |
| проверка образа | если модели внутри — запускает контейнер и ждёт ответ `/health` (до 5 минут) |
| публикация | `docker login` и `docker push` всех тегов |
Запуск: пуш в `main`/`master`, тег `v*` или вручную. Релиз: `git tag v1.0.0 && git push --tags`.
## Развёртывание на VPS
1. Установить Docker и Compose, залогиниться в реестр Gitea:
```bash
docker login <gitea-host> -u <user> # токен с read:package
```
2. Положить на VPS `docker-compose.yml` и `.env` (из `.env.example`):
```
DUB_IMAGE=<gitea-host>/<owner>/<repo>/dub-ai:latest
DUB_API_TOKEN=<openssl rand -hex 24>
DUB_FLARESOLVERR_URL=http://flaresolverr:8191
```
В `docker-compose.yml` строка `build: .` на VPS не нужна — можно удалить.
3. `docker compose pull && docker compose up -d`.
4. Обновление: после новой сборки в CI — `docker compose pull && docker compose up -d`.
**Ресурсы.** CPU-инференс; модель ruBert-base в fp32 (~0,7 ГБ) + сегментатор + torch — ожидайте
порядка 1,5–2 ГБ памяти (точный замер на VPS не делался; в compose стоит лимит 3 ГБ). Скорость: разбор
страницы раздачи на моём ПК при 2 потоках CPU — около 0,9 с (на VPS зависит от процессора). Загрузка по
`url` добавляет время сети и, при Cloudflare, 10–40 с FlareSolverr. Нужны ≥ 2 vCPU и ≥ 3 ГБ RAM; на
1 ГБ контейнер не поместится — тогда обучите и используйте `tiny2` (≈ 117 МБ весов; для него
`DUB_MODEL_DIR` указывает на каталог этой модели).
**Безопасность.**
* Не публикуйте `:8000` без токена; лучше поставить перед сервисом reverse-proxy с TLS (Caddy:
`dub.example.com { reverse_proxy 127.0.0.1:8000 }` — сертификат выдаётся автоматически) и закрыть порт
файрволом. Токен по HTTP передаётся открытым текстом.
* FlareSolverr (`:8191`) наружу не открывайте: у него нет аутентификации, он умеет открывать любые URL.
* Контейнер работает от непривилегированного пользователя, модели монтируются только на чтение.
## Если что-то не работает
| Симптом | Причина и решение |
|---|---|
| Контейнер завершился: `нет модели: /app/models/best/model.pt` | образ без моделей и каталог не смонтирован — см. [модели](#модели-и-как-они-попадают-в-контейнер) |
| Ошибка про `config.json` / доступ к huggingface.co при старте | у `models/best` нет `config.json` (старый чекпоинт); соберите через `tools/pack_models.py` — он добавит его |
| `/predict` → 400 «страница недоступна: Cloudflare challenge … (задайте FlareSolverr)» | не задан `DUB_FLARESOLVERR_URL` или контейнер его не видит (`/health` → `flaresolverr`), проверьте сеть/имя сервиса |
| 400 `FlareSolverr: … Timeout` | FlareSolverr не успел решить challenge; повторите запрос, увеличьте `DUB_FETCH_TIMEOUT`, перезапустите FlareSolverr |
| 401 | не передан токен или он отличается от `DUB_API_TOKEN` |
| Контейнер `unhealthy` в первые минуты | модель ещё грузится; `docker logs dub-ai`. Если не проходит — не хватает памяти (`docker stats`) |
| CI: `Cannot connect to the Docker daemon` | у раннера нет доступа к Docker (см. «Что настроить один раз») |
| CI: `unauthorized` при push | у `REGISTRY_TOKEN` нет `write:package` или `REGISTRY_USER` не владелец/участник пространства имён репозитория |
| CI: образ без моделей, хотя ждали с ними | секрет `MODELS_URL` пуст или шаг скачивания пропущен — смотрите лог шага «Модели» |
## Что проверено, а что нет
Проверено на ПК разработчика: сервис запускается без sklearn/matplotlib/pandas/onnx; модель грузится
полностью офлайн (`HF_HUB_OFFLINE=1`) из каталога, собранного `pack_models.py`, и даёт те же ответы, что
и обычная загрузка; токен (Bearer / X-API-Key), `DUB_FETCH_TIMEOUT`, `/health`; YAML workflow и compose
разбираются; тесты проекта проходят.
**Не проверено: `docker build`, запуск контейнера и workflow в Gitea** — на машине разработчика Docker не
установлен, а Gitea-репозитория ещё нет. Первая сборка может потребовать правок (например, имя метки
раннера, доступность колёс torch для вашей архитектуры: используется
`https://download.pytorch.org/whl/cpu`, `torch==2.11.0`, Python 3.12, x86_64 и aarch64).
+34
View File
@@ -21,6 +21,8 @@ python -m src.service --model models/best --port 8000
# или открыто наружу: python -m src.service --host 0.0.0.0 --port 8000
```
В Docker (VPS, FlareSolverr через переменные окружения, CI/CD в Gitea) — см. [docker.md](docker.md).
API:
* `GET /health` → `{"status": "ok", "model": "...", "device": "...", "classes": [...]}`
@@ -32,6 +34,18 @@ API:
Ответ: `{"tracks": [ …контракт из docs/inference.md… ]}`.
Каждая дорожка помимо типа, студии и 18+ несёт (поля только добавляются, прежние не менялись):
| Поле | Что это |
|---|---|
| `lang`, `lang_confidence`, `lang_source` | язык дорожки (`RUS`, `UKR`, `ENG`, …); источник: `model` (языковая голова), `audio` (строка «Аудио»), `text` (слова в строке дорожки) или `null` |
| `role` | `перевод` / `оригинал` / `комментарии` (по строке «Аудио» и тексту, правилами) |
| `official` | в строке «Аудио» сказано «официальный» / «лицензионный» |
| `audio` | `{codec, channels, bitrate_kbps}` из строки «Аудио» (или `null`) |
Русский и украинский дубляж одной студии — две отдельные дорожки; так же раздельны обычная
дорожка студии и её дорожка 18+ (если на странице две строки «Аудио» с одной студией).
curl:
```bash
@@ -204,3 +218,23 @@ node onnx_local.mjs "../../models/best-onnx" "Перевод: Дублирова
одну дорожку; для полной обработки страницы нужно сначала получить сегменты
(например, один вызов лёгкого HTTP-эндпоинта или порт `html_extract.py` —
это регулярки без зависимостей, кроме BeautifulSoup для HTML).
## Серии по студиям (для Sonarr)
В ответе релиза у каждой дорожки есть `episodes` («1-4», «1,3-5») и `episode_ranges`
(`[[1, 4]]`); `null` — серии у дорожки не указаны, считается, что она покрывает весь
релиз. Для сериалов добавлен блок `series`:
```json
"series": {"season": "2", "episodes": "1-9", "total": 10,
"by_studio": {"LostFilm": "1-4", "Red Head Sound": "1"},
"full_release": ["Syncmer"]}
```
`full_release` — студии, у которых серии не указаны: для сериала это значит, что озвучены
все серии, вышедшие в релизе.
`episodes` и `total` берутся из заголовка («сезон 2, серии 1-9 из 10»), `by_studio` — из
строк дорожек. Диапазоны разбирает `src/episodes.py` правилами (числа и диапазоны
точны); размеченных примеров пока мало (серии указаны примерно в 5% строк nnm), поэтому
обучаемый разбор можно делать после накопления данных сборщиком.
+27
View File
@@ -46,6 +46,33 @@ CUDA, GroupKFold-сплиты, ранний stopping (patience 3), метрик
64, до 20 эпох; base — lr 3e-5, batch 32, до 10 эпох; AdamW, weight_decay
0.01, linear warmup 10%, grad clip 1.0, seed 42.
## Выбор основы и сравнение моделей (меню, пункты 2 и 7)
`python tools/collect.py` → «2 Переобучить»: после вопроса «сегментатор + основные модели» предлагается
список основ. Каждая обучается отдельным запуском в `models/runs/<время>_<профиль>/` (`python -m src.train
--runs`), **`models/best`, `reports/final_*` и `summary.md` при этом не меняются**.
| профиль | основа | замечания |
|---|---|---|
| `tiny2` | rubert-tiny2, 30M | самая быстрая, подходит для ONNX/CPU |
| `base` | ruBert-base, 178M | текущий рабочий выбор |
| `large` | ruBert-large, 427M | помещается в 16 ГБ (bf16, batch 16); обучение ~3× дольше base, инференс на CPU медленнее |
| `modern` | RuModernBERT-base, 149M | новая архитектура; ONNX-экспорт проекта (`token_type_ids`) её не принимает |
Кросс-валидация (5 прогонов) на итоговую модель не влияет, нужна только для оценки устойчивости; без неё
обучение примерно в 6 раз короче. Флаги CLI: `--folds 0`, `--lr`, `--batch`, `--epochs-override`, `--tag`.
Пункт «7 Модели» (и конец пункта 2) сравнивает **все** модели (`models/best`, запуски, `best_backup_*`,
`models/tiny2`) на **одной и той же текущей тестовой выборке**: каждая оценивается заново, в своём входном
формате (без «[RUS AC3 …]» для моделей, обученных без аудио-контекста). Цифры кэшируются в
`<модель>/eval.json` под отпечатком выборки. Таблица показывает joint exact с 95% интервалом и парное
сравнение с `models/best`: сколько строк новая модель починила и сколько сломала, значима ли разница
(знаковый критерий). Разница в пределах шума так и помечается. Звёздочка у модели означает, что она обучена
на другом разбиении и часть текущих тестовых строк могла видеть — её цифры завышены. Выбранная модель
копируется в `models/best` (прежняя уходит в `models/best_backup_*`, откат — тем же пунктом); перед заменой
выводятся предупреждения (другая основа нужна в кэше Hugging Face на машине сервиса, вес, ONNX).
Запуски и архивные копии можно удалять из того же меню (каждая 0.1–1.4 ГБ).
## Переобучение на свежем датасете
Сборщик (`deepseek-harness/default-workspace/dataset.py`) дописывает