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

253 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Справочник 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` |
| `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` |
`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" | 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; 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"; // только для 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*` в примере иллюстративны: они зависят от модели и текста.)