Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
21 KiB
Справочник API: значения ответа и принцип работы
Этот документ — единственное место, где перечислены все возможные значения полей ответа. Заглядывать
в data/labels.csv для этого не нужно: файл большой (десятки МБ), нужен только при обучении и в
работе сервиса не участвует. Список классов модели хранится в models/best/label_maps.json
(в образе — /app/models/best/label_maps.json) и отдаётся сервисом: GET /health → classes.
Запросы, переменные окружения и запуск — в docker.md; формат вызова из кода — в integration.md; CLI и Python API — в 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 |
title |
string, необязательно | заголовок раздачи для html (во фрагменте вкладки его нет); для url берётся со страницы |
cookie |
string, необязательно | Cookie для url (kinozal требует авторизацию) |
Коды ошибок: 400 — неверное тело, пустой текст, страница не разобрана или запрещённый хост;
401 — нет/неверный токен (если задан DUB_API_TOKEN); 502 — не удалось скачать страницу.
Токен передаётся Authorization: Bearer <токен> или X-API-Key: <токен>.
Ответ релиза (html и url)
{
"tracks": [ /* дорожки, см. ниже */ ],
"studios": [{"studio": "LostFilm", "translation_type": "проф. многоголосый", "is_18plus": false, "episodes": "1-4"}],
"translation_types": ["проф. многоголосый"],
"is_18plus": false,
"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, is_18plus, episodes} |
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 |
series: season — номер сезона строкой или null; episodes — выпущенные серии («1-9»);
total — всего серий; by_studio — {студия: серии}; full_release — студии без указанных серий
(для сериала это значит «озвучены все вышедшие серии»). Подробности — в integration.md.
Дорожка (элемент tracks)
| Поле | Тип | Значения / описание |
|---|---|---|
segment |
string | текст дорожки, по которому сделано предсказание |
translation_type |
string | один из 14 классов — таблица ниже |
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 |
"model" | "audio" | "text" | null |
откуда язык: нейросеть (классы ENG/RUS/UKR, порог 0.5), строка «Аудио» страницы или слова в тексте («Украинский дубляж») |
role |
string | перевод, оригинал (оригинальная дорожка без озвучки) или комментарии (комментарии режиссёра и т. п.) |
official |
bool | в строке «Аудио» есть слова «официальный» / «лицензионный» / «licensed» |
audio |
object | null | {codec, channels, bitrate_kbps} из строки «Аудио» страницы (ac3, 6, 448); каждое поле может быть 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: 14 классов
Точные строки (регистр, пробелы и точки после префикса — как есть; сортировка — по алфавиту, так их
возвращает GET /health → classes):
["авт. двухголосый", "авт. многоголосый", "авт. одноголосый", "двухголосый", "люб. двухголосый",
"люб. многоголосый", "люб. одноголосый", "многоголосый", "неизвестно", "одноголосый",
"проф. двухголосый", "проф. дубляж", "проф. многоголосый", "проф. одноголосый"]
Для клиентов, которые сводят класс к своим типам (Dub / MVO / DVO / AVO), надёжно сопоставление по
подстроке нижнего регистра: дубляж → дубляж; многоголос → многоголосый; двухголос →
двухголосый; одноголос → одноголосый; ровно неизвестно → тип не определён (такие дорожки обычно
пропускают). Подстрока дублирован в классах не встречается (дублированные страницы сводятся к
проф. дубляж). Префикс (проф./люб./авт./нет) — отдельный признак. Если набор классов в будущей
модели изменится, ориентируйтесь на /health → classes, а неизвестное значение не считайте ошибкой.
Класс — составная метка «кто делал × сколько голосов». Других значений сервис не возвращает.
| Значение | Кто озвучивал | Вид |
|---|---|---|
проф. дубляж |
профессиональная студия | дубляж (речь актёров заменяет оригинал; в страницах «Дублированный») |
проф. многоголосый |
профессиональная студия | закадровый, три и более голоса |
проф. двухголосый |
профессиональная студия | закадровый, два голоса |
проф. одноголосый |
профессиональная студия | закадровый, один голос |
люб. многоголосый |
любительская озвучка | закадровый, три и более голоса |
люб. двухголосый |
любительская озвучка | закадровый, два голоса |
люб. одноголосый |
любительская озвучка | закадровый, один голос |
авт. многоголосый |
авторская (один автор / «авторский перевод») | закадровый, три и более голоса |
авт. двухголосый |
авторская | закадровый, два голоса |
авт. одноголосый |
авторская | закадровый, один голос |
многоголосый |
в тексте не сказано | закадровый, три и более голоса |
двухголосый |
в тексте не сказано | закадровый, два голоса |
одноголосый |
в тексте не сказано | закадровый, один голос |
неизвестно |
— | тип по тексту определить нельзя |
Как читать: префикс проф. / люб. / авт. — категория озвучивающего (на трекерах это слова
«Профессиональный», «Любительский», «Авторский»); без префикса — раздача не указывает категорию.
Для фильтров в приложении обычно достаточно сопоставить по префиксу и по хвосту (…дубляж,
…многоголосый, …двухголосый, …одноголосый). Классы с единичными примерами в обучении
распознаются хуже (см. dataset.md); при низком type_confidence (< 0.6) значению стоит
доверять осторожно. Состав и названия классов задаёт обучение; актуальный список для конкретной
модели — GET /health → classes.
Схема полей и допустимые null
Дорожка (tracks[]), нотация TypeScript. Все ключи присутствуют всегда (значение может быть
null, но ключ не пропускается), кроме помеченных «только в ответе релиза».
type Track = {
segment: string; // никогда null
translation_type: string; // один из 14 классов, никогда 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" | null; // 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; is_18plus: boolean; episodes: 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"; // только для 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 или форму входа без FlareSolverr/cookie; ошибка 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 нестабилен.
Принцип работы
- Получение страницы.
urlскачивается самим сервисом: сначала напрямую, при 403/503 или Cloudflare-заглушке — через FlareSolverr (DUB_FLARESOLVERR_URL). Разрешены только rutracker, nnmclub и kinozal. - HTML → текст (BeautifulSoup: пост rutracker, вкладка описания kinozal, иначе весь документ).
- Сегментация. Обученная модель-сегментатор (
models/segmenter, rubert-tiny2) помечает строки страницы, в которых описана дорожка («Перевод: …»,«Аудио 2: …», перечисления студий). Если сегментатора нет или он ничего не нашёл — запасные правила ("segmenter": "rules"). Многодорожечные строки («1. … 2. …», «A + B») разбиваются на отдельные дорожки. - Контекст аудио. К тексту дорожки подмешивается подходящая строка «Аудио» страницы
(кодек, каналы, битрейт, язык, 18+) в виде хвоста
[RUS AC3 6ch 448kbps 18+]. - Модель-классификатор (
DubTaggerна основе ruBert-base,models/best) делает за один проход несколько предсказаний:translation_type(14 классов), спан автора (разметка BIO по токенам), флаг 18+ и язык (ENG/RUS/UKR). - Постобработка. Автор канонизируется словарём алиасов; 18+ ставится также по строке «Аудио»;
язык: нейросеть → строка «Аудио» → слова в тексте; определяются
role,official,audio. - Агрегат релиза. Дубли (тип + студия) склеиваются; студии из заголовка раздачи, которых нет
в описании, добавляются с
source: "title"; серии и сезон разбираются правилами (src/episodes.py).
Размеченные данные (data/labels.csv) используются только при обучении: на них учатся метки
translation_type, спан студии, 18+ и язык. Схема файла и происхождение меток — в
dataset.md. Источник правды для набора классов во время работы — label_maps.json
рядом с весами модели.
Пример
curl -s http://localhost:7654/predict -H "Authorization: Bearer <ТОКЕН>" \
-H "Content-Type: application/json" \
-d '{"text": "Профессиональный (многоголосый закадровый) | HDRezka Studio 18+"}'
{"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* в примере иллюстративны: они зависят от модели и текста.)