Files
grabber-ai/docs/api-reference.md
T
DenozordecandClaude Opus 5.5 dc19fe6255
docker / build (push) Successful in 1m18s
API: видео, тип релиза и субтитры в ответе релиза; Atmos и kHz у дорожек
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]>
2026-11-02 22:26:57 +07:00

30 KiB
Raw Blame History

Справочник 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 нестабилен.

Принцип работы

  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).
  8. Техданные (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* в примере иллюстративны: они зависят от модели и текста.)