Files
grabber-ai/docs/api-reference.md
T

21 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
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 нестабилен.

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

  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 (14 классов), спан автора (разметка BIO по токенам), флаг 18+ и язык (ENG/RUS/UKR).
  6. Постобработка. Автор канонизируется словарём алиасов; 18+ ставится также по строке «Аудио»; язык: нейросеть → строка «Аудио» → слова в тексте; определяются role, official, audio.
  7. Агрегат релиза. Дубли (тип + студия) склеиваются; студии из заголовка раздачи, которых нет в описании, добавляются с 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* в примере иллюстративны: они зависят от модели и текста.)