src/techinfo.py — правила по строкам «Видео:», «Тип релиза:»/«Качество», «Контейнер:», «Продолжительность:», «Субтитры:» и заголовку: поля video (кодек, размер кадра, класс разрешения, fps, битрейт, битность, HDR, профиль DV), release (тип, нормализованный источник, контейнер, продолжительность), subtitles (языки с forced/full/SDH, формат, вшитые). audio дорожек и original: khz и atmos (вход модели не меняется). title_from_html больше не рвёт заголовок rutracker пробелами (<span>/<wbr> на каждом слове). Покрытие на 600 страницах каждого трекера: видео 90/93/100%, e2e озвучки без изменений. Тесты 275 ok. Co-Authored-By: Claude Opus 5.5 <[email protected]>
30 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. Ссылка на 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)
{
"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, "khz": 48, "atmos": true, "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": []},
"video": {"codec": "HEVC", "width": 3840, "height": 2160, "resolution": "2160p", "fps": 23.976, "bitrate_kbps": 24800,
"bit_depth": 10, "hdr": ["Dolby Vision", "HDR10+", "HDR10"], "dv_profile": "8", "raw": "MPEG-H HEVC Video / 24,8 Mbps / 3840x2160 / …"},
"release": {"quality": "WEB-DL 2160p Dolby Vision", "source": "WEB-DL", "container": "MKV", "duration": "01:38:12", "duration_sec": 5892},
"subtitles": {"languages": ["RUS", "UKR", "ENG"],
"tracks": [{"lang": "RUS", "forced": true, "full": false, "sdh": false},
{"lang": "UKR", "forced": true, "full": true, "sdh": true},
{"lang": "ENG", "forced": false, "full": true, "sdh": true}],
"format": "softsub (SRT)", "hardsub": false, "raw": "Русские (Forced), Украинские (Forced, 2x Full, SDH), Английские (Full, SDH)"},
"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, khz, atmos, raw} (поля могут быть null, atmos — bool); null — такой строки на странице нет |
translation_types |
array of string | отсортированный набор типов (значения — из таблицы классов ниже) |
is_18plus |
bool | true, если 18+ хотя бы у одной дорожки |
series |
object | null | сезон, выпущенные серии, «какая студия какие серии» (для Sonarr); null — сведений нет |
video |
object | null | видео из строки «Видео:» / «Video:» (у nnm — также «Видеокодек», «Размер кадра», «Битрейт видео»), см. ниже; null — строки нет |
release |
object | null | тип релиза, контейнер, продолжительность, см. ниже; null — ничего из этого на странице нет |
subtitles |
object | null | субтитры из строк «Субтитры:», «Субтитры 2:», «Язык субтитров:» (нет строк — языки из «Sub (Rus, Eng)» заголовка), см. ниже; null — о субтитрах ничего не сказано |
title |
string | null | заголовок раздачи, если найден |
segmenter |
"model" | "rules" |
чем страница разбита на строки дорожек: обученным сегментатором или запасными правилами |
tracker |
string | только для url: rutracker, nnm, kinozal или lostfilm |
video (все поля, кроме hdr и raw, могут быть null):
codec — HEVC, AVC, AV1, VP9, VC-1, MPEG-2, XviD, DivX, MPEG-4;
width/height — размер кадра; resolution — класс по ширине (2160p, 1440p, 1080p, 720p; кадр с
полосами 1920x800 — тоже 1080p), для SD — высота кадра (576p, 400p); fps; bitrate_kbps;
bit_depth — 8/10/12; hdr — массив из Dolby Vision, HDR10+, HDR10, HLG или просто HDR, если вид
не назван (ищется в строке видео, «HDR format», типе релиза и заголовке), пустой — HDR нет; dv_profile —
профиль Dolby Vision строкой («8», «5»); raw — строка видео целиком.
release: quality — значение «Тип релиза» / «Качество видео» / «Качество» как на странице (без
«| Исходник: …»); source — нормализованный источник: UHD BDRemux, BDRemux, WEB-DLRip, WEB-DL, WEBRip,
BDRip, HDRip, Blu-ray, DVDRip, DVD9, DVD5, HDTVRip, HDTV, SATRip, TVRip, DVB, VHSRip,
DCPRip, IPTV, CAMRip, DVD (из «Качества», иначе из заголовка); container — MKV, MP4, AVI, M2TS,
ISO, DVD-Video, TS, WEBM, MOV, WMV (kinozal строку контейнера обычно не пишет — тогда null);
duration — «Продолжительность» как на странице («01:38:12», «18 x ~ 00:20:00»); duration_sec — секунды,
только если это одно значение ЧЧ:ММ:СС фильма (не серии и не список файлов). Любое поле может быть null.
subtitles: languages — коды языков по порядку (те же коды, что у lang дорожки); tracks — по записи
на язык: {lang, forced, full, sdh} (флаги «Forced/форсированные/надписи», «Full/полные», «SDH/для
слабослышащих» объединяются по всем строкам); format — «Формат субтитров» как на странице или null;
hardsub — субтитры вшиты в видео; raw — исходные строки через «; ». «Субтитры: нет/отсутствуют» даёт
объект с пустым languages — субтитров точно нет; null — страница о субтитрах молчит.
series: season — номер сезона строкой или null; episodes — выпущенные серии («1-9»);
total — всего серий; by_studio — {студия: серии}; full_release — студии без указанных серий
(для сериала это значит «озвучены все вышедшие серии»). Подробности — в integration.md.
Дорожка (элемент tracks)
| Поле | Тип | Значения / описание |
|---|---|---|
segment |
string | текст дорожки, по которому сделано предсказание |
translation_type |
string | один из 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, khz, atmos} из строки «Аудио» страницы ("E-AC3", 6, 768, 48, true; channels — число каналов, «5.1» → 6; khz — частота дискретизации, 44.1 / 48; atmos — в строке есть «Atmos», кодек при этом остаётся E-AC3/TrueHD); поля, кроме atmos, могут быть 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); модели, обученные раньше, его не возвращают и отдают 14 классов. Источник истины для конкретной модели —GET /health→classes; клиенту безопасно сопоставлять по подстроке (дубляж→ дубляж), как описано ниже:люб. дубляжеё содержит.
Точные строки (регистр, пробелы и точки после префикса — как есть; сортировка — по алфавиту, так их
возвращает 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; // один из 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; khz: number | null; atmos: boolean } | 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;
khz: number | null; atmos: boolean; 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 — ни сезона, ни серий в раздаче нет
video: {
codec: string | null; width: number | null; height: number | null; resolution: string | null;
fps: number | null; bitrate_kbps: number | null; bit_depth: number | null;
hdr: string[]; // [] — HDR нет
dv_profile: string | null; raw: string;
} | null;
release: { quality: string | null; source: string | null; container: string | null;
duration: string | null; duration_sec: number | null } | null;
subtitles: {
languages: string[]; // [] — «Субтитры: нет»
tracks: { lang: string; forced: boolean; full: boolean; sdh: boolean }[];
format: string | null; hardsub: boolean; raw: string | null;
} | 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 нестабилен.
Принцип работы
- Получение страницы.
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(15 классов), спан автора (разметка BIO по токенам), флаг 18+ и язык (ENG/RUS/UKR). - Постобработка. Автор канонизируется словарём алиасов; 18+ ставится также по строке «Аудио»;
язык: строка «Аудио» → нейросеть → слова в тексте; строка «Аудио» подбирается к дорожке по имени
студии из
|Дубляж, Le Doyen|(слова-типыДубляж/MVO/… отбрасываются); защита от имён вроде «Дубляжная»: если дубляж назван только в имени студии, а в тексте указано число голосов, класс берётся по тексту; отдельно берётсяoriginal; определяютсяrole,official,audio. - Агрегат релиза. Дубли (тип + студия) склеиваются; студии из заголовка раздачи, которых нет
в описании, добавляются с
source: "title"; серии и сезон разбираются правилами (src/episodes.py). - Техданные (
video,release,subtitles) — правилами по строкам «Видео:», «Тип релиза:», «Контейнер:», «Продолжительность:», «Субтитры:» и заголовку (src/techinfo.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* в примере иллюстративны: они зависят от модели и текста.)