Слияние claude/advisor-opus-78114c: модели на пересобранном датасете, Docker, язык и серии
Конфликт в src/release.py: взяты original_info, lostfilm_release, series_info и серии дорожек из advisor; сохранены same_studio, merge_extra_tracks и codec_segments. Кодек-дорожкам тоже проставляются серии. Кодек-строки теперь подключаются, только если у какой-то дорожки страницы нет студии: с новыми моделями безусловное подключение роняло precision студий на rutracker (0.908 -> 0.793). С условием, test-сплит, по 300 релизов, студии P/R: rutracker 0.908/0.882 -> 0.840/0.900, nnm 0.875/0.915 -> 0.875/0.940, kinozal 0.816/0.845 -> 0.800/0.863. Тесты: 240 ok. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
099d33bdaa
439 files changed
+296036
-27415
No files matched your search
+4
-2
@@ -1,11 +1,13 @@
|
||||
# Документация DUB-AI
|
||||
# Документация grabber-ai
|
||||
|
||||
| Документ | Содержание |
|
||||
|---|---|
|
||||
| [architecture.md](architecture.md) | Схема пайплайна, multi-task модель (тип + BIO-спан + 18+), почему извлечение вместо классификации |
|
||||
| [dataset.md](dataset.md) | Схема labels.csv, семантика 14 классов, словарь алиасов, находки аудита и как они обработаны |
|
||||
| [dataset.md](dataset.md) | Схема labels.csv, семантика 15 классов, словарь алиасов, находки аудита и как они обработаны |
|
||||
| [training-windows.md](training-windows.md) | Пошаговое обучение на Windows с GPU (RTX 5070 Ti / Blackwell), гиперпараметры, troubleshooting, переобучение на свежем датасете |
|
||||
| [inference.md](inference.md) | CLI, Python API, JSON-контракт (таблица полей), обработка HTML, ограничения |
|
||||
| [api-reference.md](api-reference.md) | **Справочник ответа сервиса**: все 15 значений `translation_type`, все поля дорожки и релиза, принцип работы. Читать вместо `data/labels.csv` |
|
||||
| [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,252 @@
|
||||
# Справочник API: значения ответа и принцип работы
|
||||
|
||||
Этот документ — единственное место, где перечислены все возможные значения полей ответа. **Заглядывать
|
||||
в `data/labels.csv` для этого не нужно**: файл большой (десятки МБ), нужен только при обучении и в
|
||||
работе сервиса не участвует. Список классов модели хранится в `models/best/label_maps.json`
|
||||
(в образе — `/app/models/best/label_maps.json`) и отдаётся сервисом: `GET /health` → `classes`.
|
||||
|
||||
Запросы, переменные окружения и запуск — в [docker.md](docker.md); формат вызова из кода —
|
||||
в [integration.md](integration.md); CLI и Python API — в [inference.md](inference.md).
|
||||
|
||||
## Эндпоинты
|
||||
|
||||
| Метод | Путь | Назначение |
|
||||
|---|---|---|
|
||||
| GET | `/health` | статус, модель, список классов `classes`, режим сегментатора, `flaresolverr`, `auth`. Токен не нужен |
|
||||
| POST | `/predict` | предсказание. Тело — JSON с **ровно одним** из полей `text`, `html`, `url` |
|
||||
| GET | `/docs` | интерактивная Swagger-документация FastAPI |
|
||||
|
||||
Тело `/predict`:
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `text` | string | одна дорожка (строка вида «Профессиональный (многоголосый закадровый) \| LostFilm») → ответ `{"tracks": [одна дорожка]}` |
|
||||
| `html` | string | HTML (или текст) страницы раздачи → ответ-агрегат релиза |
|
||||
| `url` | string | ссылка на раздачу rutracker / nnmclub / kinozal; страницу сервис скачивает сам (при 403/503 — через FlareSolverr) → ответ-агрегат + `tracker`. Ссылка на `lostfilm.tv` (и поддомены) страницу не загружает: сразу возвращается одна дорожка «проф. многоголосый, LostFilm, RUS» (`tracker: "lostfilm"`, `lang_source: "url"`, `source: "url"`) |
|
||||
| `title` | string, необязательно | заголовок раздачи для `html` (во фрагменте вкладки его нет); для `url` берётся со страницы |
|
||||
| `cookie` | string, необязательно | Cookie сессии (`uid=...; pass=...`) для `url`. **Для kinozal обычно не нужен**: описание многих раздач открывается и без входа (проверено на `details.php?id=2158906`). Передавайте, только если сервис вернул `400` про форму входа |
|
||||
|
||||
Коды ошибок: `400` — неверное тело, пустой текст, страница не разобрана или запрещённый хост;
|
||||
`401` — нет/неверный токен (если задан `DUB_API_TOKEN`); `502` — не удалось скачать страницу.
|
||||
Токен передаётся `Authorization: Bearer <токен>` или `X-API-Key: <токен>`.
|
||||
|
||||
## Ответ релиза (`html` и `url`)
|
||||
|
||||
```json
|
||||
{
|
||||
"tracks": [ /* дорожки, см. ниже */ ],
|
||||
"studios": [{"studio": "LostFilm", "translation_type": "проф. многоголосый", "lang": "RUS", "is_18plus": false, "episodes": "1-4"}],
|
||||
"translation_types": ["проф. многоголосый"],
|
||||
"is_18plus": false,
|
||||
"original": {"lang": "ENG", "codec": "E-AC3", "channels": 6, "bitrate_kbps": 768, "raw": "English E-AC3+Atmos / 5.1 / 48 kHz / 768 kbps |Original|"},
|
||||
"series": {"season": "2", "episodes": "1-9", "total": 10, "by_studio": {"LostFilm": "1-4"}, "full_release": []},
|
||||
"title": "Название раздачи",
|
||||
"segmenter": "model",
|
||||
"tracker": "rutracker"
|
||||
}
|
||||
```
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `tracks` | array | дорожки релиза без дублей (дубль — одинаковые тип + студия) |
|
||||
| `studios` | array | уникальные `{studio, translation_type, lang, is_18plus, episodes}`; `lang` — язык дорожки студии (как в `tracks[].lang`, может быть `null`). Одна студия с русским и украинским дубляжом даёт две записи |
|
||||
| `original` | object \| null | оригинальная дорожка из строки «Аудио … \|Original\|»: `{lang, codec, channels, bitrate_kbps, raw}` (поля могут быть `null`); `null` — такой строки на странице нет |
|
||||
| `translation_types` | array of string | отсортированный набор типов (значения — из таблицы классов ниже) |
|
||||
| `is_18plus` | bool | `true`, если 18+ хотя бы у одной дорожки |
|
||||
| `series` | object \| null | сезон, выпущенные серии, «какая студия какие серии» (для Sonarr); `null` — сведений нет |
|
||||
| `title` | string \| null | заголовок раздачи, если найден |
|
||||
| `segmenter` | `"model"` \| `"rules"` | чем страница разбита на строки дорожек: обученным сегментатором или запасными правилами |
|
||||
| `tracker` | string | только для `url`: `rutracker`, `nnm`, `kinozal` или `lostfilm` |
|
||||
|
||||
`series`: `season` — номер сезона строкой или `null`; `episodes` — выпущенные серии («1-9»);
|
||||
`total` — всего серий; `by_studio` — `{студия: серии}`; `full_release` — студии без указанных серий
|
||||
(для сериала это значит «озвучены все вышедшие серии»). Подробности — в [integration.md](integration.md).
|
||||
|
||||
## Дорожка (элемент `tracks`)
|
||||
|
||||
| Поле | Тип | Значения / описание |
|
||||
|---|---|---|
|
||||
| `segment` | string | текст дорожки, по которому сделано предсказание |
|
||||
| `translation_type` | string | **один из 15 классов** — [таблица ниже](#translation_type-15-классов) |
|
||||
| `type_confidence` | float 0..1 | вероятность выбранного класса |
|
||||
| `studio_span` | string \| null | фрагмент текста, распознанный как автор/студия |
|
||||
| `studio` | string \| null | каноническое имя автора из словаря алиасов; неизвестное имя возвращается как в тексте; `null` — автора нет |
|
||||
| `studio_confidence` | float \| null | средняя вероятность токенов спана |
|
||||
| `is_18plus` | bool | дорожка помечена 18+ (по модели или по строке «Аудио … 18+» страницы) |
|
||||
| `age_confidence` | float 0..1 | вероятность предсказанного класса 18+ |
|
||||
| `lang` | string \| null | язык дорожки, ISO-подобный 3-буквенный код: `RUS`, `UKR`, `ENG`, также `BEL`, `KAZ`, `POL`, `FRA`, `DEU`, `ITA`, `SPA`, `JPN`, `KOR`, `CHI`, `CZE`, `LAV`, `LIT`, `EST`, `GEO`, `ARM`, `AZE`, `UZB`, `TUR`, `POR`, `HIN`; `null` — не определён |
|
||||
| `lang_confidence` | float \| null | уверенность модели; заполнено только при `lang_source = "model"` |
|
||||
| `lang_source` | `"audio"` \| `"model"` \| `"text"` \| null | откуда язык, в порядке приоритета: строка «Аудио» страницы (`Ukrainian E-AC3 …` → `UKR`, самый надёжный источник), нейросеть (классы `ENG`/`RUS`/`UKR`, порог 0.5; без строки «Аудио» ей приходится угадывать по тексту), слова в тексте («Украинский дубляж») |
|
||||
| `role` | string | `перевод`, `оригинал` (оригинальная дорожка без озвучки) или `комментарии` (комментарии режиссёра и т. п.) |
|
||||
| `official` | bool | в строке «Аудио» есть слова «официальный» / «лицензионный» / «licensed» |
|
||||
| `audio` | object \| null | `{codec, channels, bitrate_kbps}` из строки «Аудио» страницы (`"E-AC3"`, `6`, `768`; `channels` — число каналов, «5.1» → 6); каждое поле может быть `null`; весь объект `null`, если строка не найдена |
|
||||
| `source` | `"page"` \| `"title"` | только в ответе релиза: дорожка найдена в описании или студия взята из заголовка раздачи |
|
||||
| `episodes` | string \| null | серии, которые озвучивает дорожка («1-4», «1,3-5»); `null` — серии не указаны (покрывает весь релиз) |
|
||||
| `episode_ranges` | array | то же в виде `[[1, 4]]`; пусто, если серий нет |
|
||||
|
||||
Поле `lang` описывает только язык дорожки; `role` и `lang` не зависят от `translation_type`.
|
||||
|
||||
### translation_type: 15 классов
|
||||
|
||||
> Текущая модель знает 15 классов: к 14 из списка ниже добавлен `люб. дубляж` — любительский дубляж
|
||||
> (см. [dataset.md](dataset.md)); модели, обученные раньше, его не возвращают и отдают 14 классов.
|
||||
> Источник истины для конкретной модели — `GET /health` → `classes`; клиенту безопасно сопоставлять по подстроке
|
||||
> (`дубляж` → дубляж), как описано ниже: `люб. дубляж` её содержит.
|
||||
|
||||
Точные строки (регистр, пробелы и точки после префикса — как есть; сортировка — по алфавиту, так их
|
||||
возвращает `GET /health` → `classes`):
|
||||
|
||||
```json
|
||||
["авт. двухголосый", "авт. многоголосый", "авт. одноголосый", "двухголосый", "люб. двухголосый",
|
||||
"люб. дубляж", "люб. многоголосый", "люб. одноголосый", "многоголосый", "неизвестно", "одноголосый",
|
||||
"проф. двухголосый", "проф. дубляж", "проф. многоголосый", "проф. одноголосый"]
|
||||
```
|
||||
|
||||
Для клиентов, которые сводят класс к своим типам (Dub / MVO / DVO / AVO), надёжно сопоставление по
|
||||
**подстроке** нижнего регистра: `дубляж` → дубляж; `многоголос` → многоголосый; `двухголос` →
|
||||
двухголосый; `одноголос` → одноголосый; ровно `неизвестно` → тип не определён (такие дорожки обычно
|
||||
пропускают). Подстрока `дублирован` в классах **не встречается** (дублированные страницы сводятся к
|
||||
`проф. дубляж`). Префикс (`проф.`/`люб.`/`авт.`/нет) — отдельный признак. Если набор классов в будущей
|
||||
модели изменится, ориентируйтесь на `/health` → `classes`, а неизвестное значение не считайте ошибкой.
|
||||
|
||||
Класс — составная метка «кто делал × сколько голосов». Других значений сервис не возвращает.
|
||||
|
||||
| Значение | Кто озвучивал | Вид |
|
||||
|---|---|---|
|
||||
| `проф. дубляж` | профессиональная студия | дубляж (речь актёров заменяет оригинал; в страницах «Дублированный») |
|
||||
| `люб. дубляж` | любительская озвучка | дубляж/псевдодубляж; в текущей модели есть; в моделях, обученных раньше, его нет |
|
||||
| `проф. многоголосый` | профессиональная студия | закадровый, три и более голоса |
|
||||
| `проф. двухголосый` | профессиональная студия | закадровый, два голоса |
|
||||
| `проф. одноголосый` | профессиональная студия | закадровый, один голос |
|
||||
| `люб. многоголосый` | любительская озвучка | закадровый, три и более голоса |
|
||||
| `люб. двухголосый` | любительская озвучка | закадровый, два голоса |
|
||||
| `люб. одноголосый` | любительская озвучка | закадровый, один голос |
|
||||
| `авт. многоголосый` | авторская (один автор / «авторский перевод») | закадровый, три и более голоса |
|
||||
| `авт. двухголосый` | авторская | закадровый, два голоса |
|
||||
| `авт. одноголосый` | авторская | закадровый, один голос |
|
||||
| `многоголосый` | в тексте не сказано | закадровый, три и более голоса |
|
||||
| `двухголосый` | в тексте не сказано | закадровый, два голоса |
|
||||
| `одноголосый` | в тексте не сказано | закадровый, один голос |
|
||||
| `неизвестно` | — | тип по тексту определить нельзя |
|
||||
|
||||
Как читать: префикс `проф.` / `люб.` / `авт.` — категория озвучивающего (на трекерах это слова
|
||||
«Профессиональный», «Любительский», «Авторский»); без префикса — раздача не указывает категорию.
|
||||
Для фильтров в приложении обычно достаточно сопоставить по префиксу и по хвосту (`…дубляж`,
|
||||
`…многоголосый`, `…двухголосый`, `…одноголосый`). Классы с единичными примерами в обучении
|
||||
распознаются хуже (см. [dataset.md](dataset.md)); при низком `type_confidence` (< 0.6) значению стоит
|
||||
доверять осторожно. Состав и названия классов задаёт обучение; актуальный список для конкретной
|
||||
модели — `GET /health` → `classes`.
|
||||
|
||||
## Схема полей и допустимые null
|
||||
|
||||
Дорожка (`tracks[]`), нотация TypeScript. Все ключи **присутствуют всегда** (значение может быть
|
||||
`null`, но ключ не пропускается), кроме помеченных «только в ответе релиза».
|
||||
|
||||
```ts
|
||||
type Track = {
|
||||
segment: string; // никогда null
|
||||
translation_type: string; // один из 15 классов, никогда null
|
||||
type_confidence: number; // 0..1
|
||||
studio_span: string | null; // null — автора в тексте нет
|
||||
studio: string | null; // null вместе со studio_span
|
||||
studio_confidence: number | null; // null вместе со studio_span
|
||||
is_18plus: boolean;
|
||||
age_confidence: number; // 0..1
|
||||
lang: string | null; // 3-буквенный код или null
|
||||
lang_confidence: number | null; // не null только при lang_source == "model"
|
||||
lang_source: "model" | "audio" | "text" | "url" | null; // "url" — ссылка lostfilm.tv; null вместе с lang
|
||||
role: "перевод" | "оригинал" | "комментарии"; // никогда null
|
||||
official: boolean;
|
||||
audio: { codec: string | null; channels: number | null; bitrate_kbps: number | null } | null;
|
||||
// только в ответе релиза (html / url):
|
||||
source: "page" | "title";
|
||||
episodes: string | null; // "1-4", "1,3-5"; null — серии не указаны
|
||||
episode_ranges: [number, number][]; // [[1,4]]; пустой массив, если серий нет
|
||||
};
|
||||
|
||||
type Release = {
|
||||
tracks: Track[]; // может быть пустым массивом
|
||||
studios: { studio: string | null; translation_type: string; lang: string | null; is_18plus: boolean; episodes: string | null }[];
|
||||
original: { lang: string | null; codec: string | null; channels: number | null; bitrate_kbps: number | null; raw: string } | null;
|
||||
translation_types: string[];
|
||||
is_18plus: boolean;
|
||||
series: {
|
||||
season: string | null; // "2"
|
||||
episodes: string | null; // выпущенные серии, "1-9"
|
||||
total: number | null; // всего серий
|
||||
by_studio: Record<string, string>; // { "LostFilm": "1-4" }, может быть {}
|
||||
full_release: string[]; // студии без указанных серий
|
||||
} | null; // null — ни сезона, ни серий в раздаче нет
|
||||
title: string | null;
|
||||
segmenter: "model" | "rules";
|
||||
tracker?: "rutracker" | "nnm" | "kinozal" | "lostfilm"; // только для url
|
||||
};
|
||||
```
|
||||
|
||||
Ответ на `text` — `{"tracks": [Track]}` (без полей релиза и без `source`/`episodes`/`episode_ranges`).
|
||||
Клиентам стоит читать ответ толерантно: неизвестные поля игнорировать, отсутствующие считать `null`.
|
||||
|
||||
## Ошибки
|
||||
|
||||
Тело любой ошибки — JSON `{"detail": "<сообщение>"}`; у `401` добавлен заголовок
|
||||
`WWW-Authenticate: Bearer`.
|
||||
|
||||
| Код | Когда | Пример `detail` |
|
||||
|---|---|---|
|
||||
| 400 | передано не ровно одно из `text` / `html` / `url` | «нужно ровно одно поле: text (дорожка), html (страница) или url (ссылка на раздачу)» |
|
||||
| 400 | пустой `text`; страница не разобрана; хост ссылки не из разрешённых (rutracker / nnmclub / kinozal); страница недоступна или слишком большая; трекер отдал заглушку Cloudflare или форму входа (для kinozal — раздача, которая без входа не открывается, тогда нужен `cookie`) или Cloudflare без FlareSolverr; ошибка FlareSolverr | текст причины на русском («неподдерживаемый хост: …», «страница недоступна: …», «FlareSolverr: …») |
|
||||
| 401 | задан `DUB_API_TOKEN`, а токена нет или он неверный | «нужен токен: Authorization: Bearer <токен> или X-API-Key» |
|
||||
| 422 | тело запроса не JSON-объект или поля не тех типов (валидация FastAPI; `detail` — массив) | `[{"loc": [...], "msg": "...", "type": "..."}]` |
|
||||
| 502 | сетевая ошибка при скачивании страницы по `url` (соединение, DNS, таймаут) | «не удалось загрузить страницу: …» |
|
||||
|
||||
`GET /health` не требует токена и возвращает: `status`, `model`, `device`, `classes`, `segmenter`
|
||||
(`"model"`/`"rules"`), `flaresolverr` (bool), `auth` (bool). Страница, на которой не найдено дорожек,
|
||||
— не ошибка: ответ `200` с `"tracks": []`. Повторяйте запрос при `502` и при ответах `400` про
|
||||
Cloudflare: FlareSolverr нестабилен.
|
||||
|
||||
## Принцип работы
|
||||
|
||||
1. **Получение страницы.** `url` скачивается самим сервисом: сначала напрямую, при 403/503 или
|
||||
Cloudflare-заглушке — через FlareSolverr (`DUB_FLARESOLVERR_URL`). Разрешены только rutracker,
|
||||
nnmclub и kinozal.
|
||||
2. **HTML → текст** (BeautifulSoup: пост rutracker, вкладка описания kinozal, иначе весь документ).
|
||||
3. **Сегментация.** Обученная модель-сегментатор (`models/segmenter`, rubert-tiny2) помечает строки
|
||||
страницы, в которых описана дорожка (`«Перевод: …»`, `«Аудио 2: …»`, перечисления студий).
|
||||
Если сегментатора нет или он ничего не нашёл — запасные правила (`"segmenter": "rules"`).
|
||||
Многодорожечные строки («1. … 2. …», «A + B») разбиваются на отдельные дорожки.
|
||||
4. **Контекст аудио.** К тексту дорожки подмешивается подходящая строка «Аудио» страницы
|
||||
(кодек, каналы, битрейт, язык, 18+) в виде хвоста `[RUS AC3 6ch 448kbps 18+]`.
|
||||
5. **Модель-классификатор** (`DubTagger` на основе ruBert-base, `models/best`) делает за один проход
|
||||
несколько предсказаний: `translation_type` (15 классов), спан автора (разметка BIO по токенам),
|
||||
флаг 18+ и язык (`ENG`/`RUS`/`UKR`).
|
||||
6. **Постобработка.** Автор канонизируется словарём алиасов; 18+ ставится также по строке «Аудио»;
|
||||
язык: строка «Аудио» → нейросеть → слова в тексте; строка «Аудио» подбирается к дорожке по имени
|
||||
студии из `|Дубляж, Le Doyen|` (слова-типы `Дубляж`/`MVO`/… отбрасываются); защита от имён вроде
|
||||
«Дубляжная»: если дубляж назван только в имени студии, а в тексте указано число голосов, класс берётся
|
||||
по тексту; отдельно берётся `original`; определяются `role`, `official`, `audio`.
|
||||
7. **Агрегат релиза.** Дубли (тип + студия) склеиваются; студии из заголовка раздачи, которых нет
|
||||
в описании, добавляются с `source: "title"`; серии и сезон разбираются правилами (`src/episodes.py`).
|
||||
|
||||
Размеченные данные (`data/labels.csv`) используются только при обучении: на них учатся метки
|
||||
`translation_type`, спан студии, 18+ и язык. Схема файла и происхождение меток — в
|
||||
[dataset.md](dataset.md). Источник правды для набора классов во время работы — `label_maps.json`
|
||||
рядом с весами модели.
|
||||
|
||||
## Пример
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:7654/predict -H "Authorization: Bearer <ТОКЕН>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"text": "Профессиональный (многоголосый закадровый) | HDRezka Studio 18+"}'
|
||||
```
|
||||
|
||||
```json
|
||||
{"tracks": [{"segment": "Профессиональный (многоголосый закадровый) | HDRezka Studio 18+",
|
||||
"translation_type": "проф. многоголосый", "type_confidence": 0.99,
|
||||
"studio_span": "HDRezka Studio", "studio": "HDrezka", "studio_confidence": 0.97,
|
||||
"is_18plus": true, "age_confidence": 0.99,
|
||||
"lang": "RUS", "lang_confidence": null, "lang_source": "text",
|
||||
"role": "перевод", "official": false, "audio": null}]}
|
||||
```
|
||||
|
||||
(значения `lang*` в примере иллюстративны: они зависят от модели и текста.)
|
||||
@@ -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 → «Пересобрать всё заново».
|
||||
+27
-6
@@ -10,10 +10,13 @@
|
||||
регулярками** — модель учится воспроизводить эту разметку и обобщать её на
|
||||
новые формулировки.
|
||||
|
||||
Снимок: 3286 строк, 3 источника — rutracker 1660, nnm 1549, kinozal 77.
|
||||
После подготовки: 3284 строки + 458 аугментированных морфологических
|
||||
вариантов («многоголосое», «дублированное» и т.п.), всего 3742 обучающих
|
||||
примера. Файлы `*_entries.csv` / `*_releases.csv` — те же данные в нарезке
|
||||
Размер (на момент последней сборки): ~41 тыс. строк из ~28 тыс. релизов, 3 источника
|
||||
(rutracker, nnm, kinozal); HTML-дамп страниц — `data/raw` (~32 тыс. файлов, ≈2.6 ГБ, в git не
|
||||
хранится), разобранные тексты — `data/pages`. Точные числа показывает «Статистика датасета» в
|
||||
`tools/collect.py` (пункт 3) и отчёт `reports/data_audit.md` после `prepare_data`.
|
||||
Ранние цифры этого документа (3286 строк) относились к первому снимку.
|
||||
К строкам при подготовке добавляются аугментированные морфологические варианты
|
||||
(«многоголосое», «дублированное» и т.п.). Файлы `*_entries.csv` / `*_releases.csv` — те же данные в нарезке
|
||||
по источникам и по релизам; модель используется только `labels.csv`.
|
||||
|
||||
## Колонки labels.csv
|
||||
@@ -40,9 +43,11 @@
|
||||
`raw` берётся `role_raw` конкретной дорожки, чтобы одинаковый текст не
|
||||
получал разные метки).
|
||||
|
||||
## Семантика translation_type (14 классов)
|
||||
## Семантика translation_type (15 классов после пересборки; 14 в ранних моделях)
|
||||
|
||||
Составные метки «кто делал × сколько голосов»:
|
||||
Составные метки «кто делал × сколько голосов». Набор классов не зашит в код: `prepare_data` берёт его
|
||||
из данных (`label_maps.json`), поэтому модель, обученная до пересборки, знает 14 классов, а после —
|
||||
ещё и `люб. дубляж`.
|
||||
|
||||
* префикс: `проф.` (профессиональная), `люб.` (любительская), `авт.`
|
||||
(авторская), без префикса — в тексте не указано;
|
||||
@@ -52,6 +57,22 @@
|
||||
Все «*неизвестно»-варианты объединены в `неизвестно`; типы «оригинал» и
|
||||
«субтитры» (1–2 строки-проскока генератора) удалены.
|
||||
|
||||
### Что изменено в разметке (парсеры `tools/harvest/harvest_core.py`, `PARSE_VERSION = 3`)
|
||||
|
||||
| Проблема в старых метках | Исправление |
|
||||
|---|---|
|
||||
| Любительский дубляж («Любительский (дублированный) Red Head Sound», «псевдодублированный») размечался как `проф. дубляж` (644 из 720 строк) | Новый класс `люб. дубляж` |
|
||||
| Дубляж по имени студии: «многоголосый … Дубляжная», «Продубляж» получали тип «дубляж» | «дубляж» считается типом, только если это отдельное слово или «дублир…» |
|
||||
| Несколько дорожек в одной строке с одной меткой («Профессиональный (двухголосый закадровый), авторский (одноголосый закадровый)»; 7.3% строк) | `split_multi_rows`: строка раскладывается на дорожки теми же правилами, что и сервис (`src/html_extract._split_entries`); «Многоголосый, профессиональный - LostFilm» остаётся одной дорожкой. Остатки старых данных `prepare_data` отбрасывает (счётчик в аудите) |
|
||||
| Язык почти не размечался (122 строки `UKR`, 47 `ENG`): kinozal сопоставлял «Перевод» только с русскими аудиодорожками, а в строках `\|Дубляж, Le Doyen\|` студия читалась как одно имя | Сопоставление со всеми аудиодорожками; язык берётся из строки «Аудио»; `src/trackinfo.parse_audio_line` делит `\|Тип, Студия\|` |
|
||||
| Дорожки, названные только в строках «Аудио» (украинский дубляж и т.п.) терялись | `audio_only_rows`: строки с блоком `\|…\|`, студией и определимым типом (оригинал, комментарии, источники вроде «Blu-ray CEE» не берутся) |
|
||||
| Одни и те же короткие строки доминировали («Перевод: Дублированный» — 2727 копий) | `prepare_data --max-dup` (по умолчанию 40 копий одного текста и типа) |
|
||||
| Дисбаланс классов | Веса классов в потерях типа: корень из обратной частоты, потолок 4 (`--no-class-weights` отключает; значение записывается в `run.json`) |
|
||||
|
||||
Для применения к уже собранным данным нужна переиндексация с пересчётом меток
|
||||
(`python tools/collect.py` → пункт 2 → «Переиндексировать data/raw … y»): старые строки
|
||||
`labels.csv` заменяются, копия прежнего файла сохраняется как `data/labels.bak_<время>.csv`.
|
||||
|
||||
## Аудит (reports/data_audit.md) и что с находками сделано
|
||||
|
||||
| Находка | Обработка |
|
||||
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
# Docker: запуск grabber-ai на VPS и сборка в Gitea CI/CD
|
||||
|
||||
Контейнер запускает HTTP-сервис (`python -m src.service`): по ссылке на раздачу или по HTML определяет
|
||||
дорожки озвучки (тип, студия, 18+, язык, роль, параметры аудио). Для трекеров за Cloudflare сервис
|
||||
обращается к вашему **FlareSolverr**, адрес которого задаётся переменной окружения.
|
||||
|
||||
```
|
||||
клиент ──HTTP──▶ grabber-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)); если нет — 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` | `grabber-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` поднимается рядом, порт наружу не
|
||||
публикуется, `grabber-ai` ходит к нему по `http://flaresolverr:8191`.
|
||||
2. **Свой FlareSolverr на другом хосте.** В `.env`: `DUB_FLARESOLVERR_URL=http://192.168.0.24:8191`, и
|
||||
запускайте только `docker compose up -d grabber-ai` (контейнер FlareSolverr не нужен). Адрес должен быть
|
||||
достижим из контейнера: с VPS до домашней сети — через VPN/туннель, не открывайте FlareSolverr в интернет.
|
||||
3. **`docker run`:**
|
||||
```bash
|
||||
docker run -d --name grabber-ai -p 8000:8000 \
|
||||
-e DUB_FLARESOLVERR_URL=http://flaresolverr:8191 -e DUB_API_TOKEN=... \
|
||||
--network <сеть_с_flaresolverr> grabber-ai:local
|
||||
```
|
||||
|
||||
Проверить, что адрес подхватился: `GET /health` → `"flaresolverr": true`. Проверка связи:
|
||||
`docker compose exec grabber-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` **необязателен**. Описание большинства раздач открывается без входа, и запрос
|
||||
`{"url": "https://kinozal.me/details.php?id=…"}` работает как есть. Если сервис ответил `400` про форму
|
||||
входа (раздача закрыта для гостей), передайте `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/grabber-ai-models/2026-10-28/models-bundle.tar.gz
|
||||
```
|
||||
|
||||
Ссылка для скачивания (она же `MODELS_URL`):
|
||||
`https://<gitea>/api/packages/<owner>/generic/grabber-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>/grabber-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):
|
||||
|
||||
| Секрет | Значение |
|
||||
|---|---|
|
||||
| `ACTION_PAT` | токен Gitea (Settings → Applications → Access Tokens) с правами `write:package` и `read:package`; им образ публикуется в реестр и скачиваются модели. Имена секретов не могут начинаться с `GITEA_` и `GITHUB_`, поэтому не `GITEA_TOKEN` |
|
||||
| `REGISTRY_USER` | необязательно: владелец токена; по умолчанию — владелец репозитория (`denozord`) |
|
||||
| `MODELS_URL` | необязательно: ссылка на `models-bundle.tar.gz`; нет — образ без моделей |
|
||||
|
||||
3. Адрес реестра — `git.shx.one` (HTTPS), он задан в workflow (`REGISTRY_HOST`); другой можно указать
|
||||
переменной репозитория `REGISTRY_HOST`. Адрес берётся не из `GITHUB_SERVER_URL`: внутри раннера это
|
||||
внутренний `http://192.168.x.x:3000` без HTTPS, и `docker login` падает с «server gave HTTP response
|
||||
to HTTPS client». Если реестр всё же по HTTP, добавьте его в `insecure-registries` Docker
|
||||
(`/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>/grabber-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 grabber-ai`. Если не проходит — не хватает памяти (`docker stats`) |
|
||||
| CI: `Cannot connect to the Docker daemon` | у раннера нет доступа к Docker (см. «Что настроить один раз») |
|
||||
| CI: `server gave HTTP response to HTTPS client` | workflow взял внутренний адрес Gitea; должен быть `REGISTRY_HOST` = `git.shx.one` (уже так в workflow) |
|
||||
| CI: `unauthorized` при push | у `ACTION_PAT` нет `write:package` или токен принадлежит не владельцу/участник пространства имён репозитория |
|
||||
| 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).
|
||||
+5
-2
@@ -24,7 +24,8 @@ HTML распознаётся по содержимому, так что `--html
|
||||
|
||||
```bash
|
||||
python -m src.predict --url https://rutracker.org/forum/viewtopic.php?t=123
|
||||
python -m src.predict --url https://kinozal.tv/details.php?id=1 --cookie "uid=..; pass=.."
|
||||
python -m src.predict --url https://kinozal.tv/details.php?id=1 # kinozal без входа
|
||||
python -m src.predict --url https://kinozal.tv/details.php?id=1 --cookie "uid=..; pass=.." # если раздача закрыта для гостей
|
||||
```
|
||||
|
||||
Поддерживаются только rutracker / kinozal / nnm-club (allowlist доменов,
|
||||
@@ -61,6 +62,8 @@ model.predict_file("page.txt")
|
||||
|
||||
## JSON-контракт
|
||||
|
||||
Полный справочник значений (15 классов `translation_type`, `lang`, `role`, `audio`, агрегат релиза, принцип работы) — в [api-reference.md](api-reference.md); ниже краткий пример.
|
||||
|
||||
```json
|
||||
{
|
||||
"tracks": [
|
||||
@@ -81,7 +84,7 @@ model.predict_file("page.txt")
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `segment` | string | текст дорожки, по которому сделано предсказание (префикс снят) |
|
||||
| `translation_type` | string | один из 14 классов (см. docs/dataset.md) |
|
||||
| `translation_type` | string | один из 15 классов (см. docs/dataset.md) |
|
||||
| `type_confidence` | float | softmax-вероятность класса |
|
||||
| `studio_span` | string \| null | фрагмент текста, распознанный как имя автора |
|
||||
| `studio` | string \| null | каноническое имя автора по словарю алиасов; null — автора в тексте нет |
|
||||
|
||||
+38
-3
@@ -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
|
||||
@@ -127,11 +141,12 @@ python -m src.predict --url <ссылка> --flaresolverr http://127.0.0.1:8191
|
||||
|
||||
### grabber (PHP)
|
||||
|
||||
kinozal требует авторизации, поэтому grabber скачивает страницу сам
|
||||
Описание на kinozal часто открывается и без входа, и тогда достаточно `{"url": …}` (cookie необязателен). Для раздач,
|
||||
закрытых для гостей, а также чтобы не хранить cookie в сервисе, grabber скачивает страницу сам
|
||||
(`KinozalClient`) и отправляет сервису готовый HTML вместе с заголовком раздачи:
|
||||
|
||||
```php
|
||||
$ch = curl_init('http://dub-ai:8000/predict');
|
||||
$ch = curl_init('http://grabber-ai:8000/predict');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
||||
@@ -178,7 +193,7 @@ python -m src.export_onnx # -> models/best-onnx/ + reports/onnx_parit
|
||||
```bash
|
||||
cd examples/rust/onnx_local && cargo build
|
||||
ORT_DYLIB_PATH="$(find ../../.venv -name onnxruntime.dll | head -1)" \
|
||||
./target/debug/dub-ai-onnx-local.exe ../../models/best-onnx \
|
||||
./target/debug/grabber-ai-onnx-local.exe ../../models/best-onnx \
|
||||
"Перевод: Дублированный | MovieDalen"
|
||||
```
|
||||
|
||||
@@ -204,3 +219,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), поэтому
|
||||
обучаемый разбор можно делать после накопления данных сборщиком.
|
||||
+1
-1
@@ -65,7 +65,7 @@
|
||||
| 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 |
|
||||
| 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`) пропускается. Кодек-строки подключаются, только если у какой-то дорожки страницы нет студии. На test-сплите модели 29.10 (по 300 релизов), студии P/R без → с кодек-строками: rutracker 0.908/0.882 → 0.840/0.900 (почти все «ложные» — реальные студии, которых нет в эталоне), nnm 0.875/0.915 → 0.875/0.940, kinozal (текст из raw JSON) 0.816/0.845 → 0.800/0.863 |
|
||||
| 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`) |
|
||||
|
||||
@@ -10,7 +10,7 @@ torch 2.11.0+cu128).
|
||||
## Установка (один раз)
|
||||
|
||||
```bat
|
||||
cd C:\Users\shats\Dev\DUB-AI
|
||||
cd C:\Users\shats\Dev\grabber-ai
|
||||
python -m venv .venv
|
||||
.venv\Scripts\python -m pip install --upgrade pip
|
||||
|
||||
@@ -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