Docker-сервис, выбор основы и сравнение моделей, сборщик датасета
docker / build (push) Failing after 39s
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:
1 parent
d155b8584b
commit
efc9a4f56a
411 files changed
+87495
-27363
No files matched your search
@@ -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) корня проекта.
|
||||
@@ -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
@@ -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).
|
||||
@@ -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), поэтому
|
||||
обучаемый разбор можно делать после накопления данных сборщиком.
|
||||
@@ -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`) дописывает
|
||||
|
||||
Reference in new issue
Block a user