Files
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 <noreply@anthropic.com>
2026-11-02 22:26:57 +07:00

247 lines
13 KiB
Markdown
Raw Permalink 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.
# Интеграция из Rust / TypeScript
Два пути. **А (HTTP)** — самый простой: Python-процесс один раз загружает
модель, любой язык ходит по REST. **Б (ONNX)** — без Python в рантайме:
модель запускается локально в вашем процессе.
| | Путь А: HTTP-сервис | Путь Б: ONNX |
|---|---|---|
| Зависимости рантайма | запущенный Python-процесс | onnxruntime (+ токенизатор) в вашем языке |
| Сегментация HTML | да, на стороне сервиса | нет — в примерах обрабатывается одна дорожка; сегментацию нужно вызвать отдельно или портировать (`html_extract.py` ~60 строк) |
| Канонизация имён | да, на стороне сервиса | по `aliases.json` (простой словарь) |
| Скорость | сеть + инференс (~мс) | локальный инференс (~мс) |
| Кому подходит | сервис/бэкенд | CLI/десктоп/edge, где Python нельзя |
## Путь А: HTTP-сервис
Запуск (из корня проекта):
```bash
python -m src.service --model models/best --port 8000
# или открыто наружу: python -m src.service --host 0.0.0.0 --port 8000
```
В Docker (VPS, FlareSolverr через переменные окружения, CI/CD в Gitea) — см. [docker.md](docker.md).
API:
* `GET /health` → `{"status": "ok", "model": "...", "device": "...", "classes": [...]}`
* `POST /predict` — ровно одно из полей (`url` принимает опциональный `cookie`;
ответ для `html`/`url` — агрегат релиза, см. docs/inference.md):
* `{"url": "https://rutracker.org/forum/viewtopic.php?t=…"}` → страница загружается сервисом;
* `{"text": "Перевод: Дублированный | MovieDalen"}` → одна дорожка;
* `{"html": "<html>…вся страница…"}` → все дорожки релиза.
Ответ: `{"tracks": [ …контракт из docs/inference.md… ]}`.
Каждая дорожка помимо типа, студии и 18+ несёт (поля только добавляются, прежние не менялись):
| Поле | Что это |
|---|---|
| `lang`, `lang_confidence`, `lang_source` | язык дорожки (`RUS`, `UKR`, `ENG`, …); источник: `model` (языковая голова), `audio` (строка «Аудио»), `text` (слова в строке дорожки) или `null` |
| `role` | `перевод` / `оригинал` / `комментарии` (по строке «Аудио» и тексту, правилами) |
| `official` | в строке «Аудио» сказано «официальный» / «лицензионный» |
| `audio` | `{codec, channels, bitrate_kbps, khz, atmos}` из строки «Аудио» (или `null`) |
Ответ релиза (`html`/`url`) кроме дорожек несёт `video`, `release` и `subtitles` — разрешение, HDR,
тип релиза, контейнер, продолжительность, языки субтитров; поля и значения — в
[api-reference.md](api-reference.md). ONNX-путь (Б) их не считает: это правила `src/techinfo.py`,
их нужно повторить на стороне клиента или ходить в HTTP-сервис.
Русский и украинский дубляж одной студии — две отдельные дорожки; так же раздельны обычная
дорожка студии и её дорожка 18+ (если на странице две строки «Аудио» с одной студией).
curl:
```bash
curl -X POST http://127.0.0.1:8000/predict -H "Content-Type: application/json" \
-d '{"text": "Перевод: Дублированный | MovieDalen"}'
```
> **Windows/Git Bash:** кириллица в аргументе `-d` может искажаться кодировкой
> консоли — используйте `--data-binary @req.json` с файлом в UTF-8.
### Rust (examples/rust/http_client)
```bash
cd examples/rust/http_client
cargo run -- --text "Перевод: Дублированный | MovieDalen"
cargo run -- --html page.html
DUB_URL=http://host:8000 cargo run -- --text "..."
```
Ядро клиента:
```rust
let resp: PredictResponse = client
.post("http://127.0.0.1:8000/predict")
.json(&serde_json::json!({ "html": page_html }))
.send().await?
.json().await?;
for t in &resp.tracks {
println!("{} — {}{}", t.translation_type,
t.studio.clone().unwrap_or_else(|| "—".into()),
if t.is_18plus { " 18+" } else { "" });
}
```
### TypeScript / Node.js (examples/typescript)
Без зависимостей (Node >= 18, глобальный fetch):
```bash
node examples/typescript/predict.mjs --text "Одноголосый закадровый — Дмитрий Есарев"
node examples/typescript/predict.mjs --html page.html
```
```ts
const res = await fetch("http://127.0.0.1:8000/predict", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: "Одноголосый закадровый — Дмитрий Есарев" }),
});
const { tracks } = await res.json();
```
Типизированная версия — `examples/typescript/predict.ts` (запуск: `npx tsx predict.ts --text "..."`).
### PowerShell (examples/powershell/predict.ps1)
Скрипт (работает и в Windows PowerShell 5.1, и в PowerShell 7):
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File examples\powershell\predict.ps1 -Text "Перевод: Дублированный | MovieDalen"
pwsh -File examples\powershell\predict.ps1 -HtmlFile page.html -Url http://host:8000
```
Однострочник (POST с кириллицей; UTF-8 байтами — важно для PS 5.1, где
строковый `-Body` уходит в ISO-8859-1):
```powershell
$b = [Text.Encoding]::UTF8.GetBytes((@{ text = "Перевод: Дублированный | MovieDalen" } | ConvertTo-Json))
(Invoke-RestMethod -Uri http://127.0.0.1:8000/predict -Method Post -ContentType "application/json" -Body $b).tracks
```
Проверка живости: `Invoke-RestMethod http://127.0.0.1:8000/health`.
Ответ сервис отдаёт с `charset=utf-8`, поэтому кириллица в ответе
корректно декодируется и в PS 5.1.
### Cloudflare / 403 на трекерах (FlareSolverr)
Как и Jackett/Prowlarr, сервис умеет ходить через **ваш** экземпляр
[FlareSolverr](https://github.com/FlareSolverr/FlareSolverr): при ответе трекера
403/429/503 или странице-заглушке («Один момент…») запрос повторяется через него.
```bash
docker run -d --name flaresolverr -p 8191:8191 ghcr.io/flaresolverr/flaresolverr:latest
python -m src.service --flaresolverr http://127.0.0.1:8191 # или env DUB_FLARESOLVERR_URL
python -m src.predict --url <ссылка> --flaresolverr http://127.0.0.1:8191
```
Адрес FlareSolverr задаётся только настройкой сервиса, не полем запроса.
Если grabber уже работает через Prowlarr/FlareSolverr, проще скачать страницу
там и прислать её в поле `html`. Ограничения: FlareSolverr не гарантирует
обход (rutracker дополнительно требует авторизацию / может блокировать по
региону), cookie сессии передаются через `cookie`.
### grabber (PHP)
Описание на kinozal часто открывается и без входа, и тогда достаточно `{"url": …}` (cookie необязателен). Для раздач,
закрытых для гостей, а также чтобы не хранить cookie в сервисе, grabber скачивает страницу сам
(`KinozalClient`) и отправляет сервису готовый HTML вместе с заголовком раздачи:
```php
$ch = curl_init('http://grabber-ai:8000/predict');
curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode(['html' => $tab, 'title' => $releaseTitle], JSON_UNESCAPED_UNICODE),
]);
$release = json_decode(curl_exec($ch), true);
// $tab — ответ get_srv_details.php (заголовка в нём нет, поэтому передаём title)
// $release['studios'], $release['translation_types'], $release['is_18plus']
```
## Путь Б: ONNX без Python
Экспорт (из корня проекта, после обучения):
```bash
python -m src.export_onnx # -> models/best-onnx/ + reports/onnx_parity.md
```
Для встраивания — малая модель: `python -m src.export_onnx --model models/tiny2
--quantize` кладёт в `models/tiny2-onnx/` ещё и `model.int8.onnx` (29 МБ вместо
~440 МБ у base; parity с PyTorch: тип 99.8%, спан 99.2%, 18+ 100% —
`reports/onnx_parity_tiny2_int8.md`).
Состав `models/best-onnx/`: `model.onnx` (входы `input_ids`,
`attention_mask`, `token_type_ids` int64 [B, seq]; выходы `type_logits`
[B, 14], `bio_logits` [B, seq, 3], `age_logits` [B, 2]), `tokenizer.json`
(HF fast-токенизатор), `label_maps.json` (классы), `aliases.json`
(нормализация имён).
Эквивалентность PyTorch проверяется на всём held-out test (совпадение
типа/спана/18+ ≥ 99%) — отчёт в `reports/onnx_parity.md`.
Клиентская постобработка (одинакова во всех языках):
1. токенизировать дорожку (max_len 128, truncation);
2. прогнать ONNX, взять `argmax`;
3. спан: первый токен с меткой B, затем цепочка I; текст спана — по offsets
токенизатора (или `decode` токенов спана);
4. `is_18plus = argmax(age_logits) == 1`;
5. канонизация автора по `aliases.json`.
### Rust (examples/rust/onnx_local)
```bash
cd examples/rust/onnx_local && cargo build
ORT_DYLIB_PATH="$(find ../../.venv -name onnxruntime.dll | head -1)" \
./target/debug/grabber-ai-onnx-local.exe ../../models/best-onnx \
"Перевод: Дублированный | MovieDalen"
```
Крейты: `ort 2.0.0-rc.15` + `tokenizers` (читает `tokenizer.json` как есть).
Библиотека onnxruntime грузится динамически — DLL можно взять из pip-пакета
`onnxruntime` (уже есть в `.venv` проекта) или из официального релиза;
проверено: тип, спан автора и 18+ совпадают с Python-инференсом.
### TypeScript (examples/typescript/onnx_local.mjs)
```bash
cd examples/typescript
npm init -y && npm i onnxruntime-node @huggingface/transformers
node onnx_local.mjs "../../models/best-onnx" "Перевод: Дублированный | MovieDalen"
```
`@huggingface/transformers` используется только как токенизатор;
инференс — `onnxruntime-node`.
## Замечание про сегментацию страниц в пути Б
Сервис (путь А) сам находит все дорожки в HTML. В пути Б примеры принимают
одну дорожку; для полной обработки страницы нужно сначала получить сегменты
(например, один вызов лёгкого HTTP-эндпоинта или порт `html_extract.py` —
это регулярки без зависимостей, кроме BeautifulSoup для HTML).
## Серии по студиям (для Sonarr)
В ответе релиза у каждой дорожки есть `episodes` («1-4», «1,3-5») и `episode_ranges`
(`[[1, 4]]`); `null` — серии у дорожки не указаны, считается, что она покрывает весь
релиз. Для сериалов добавлен блок `series`:
```json
"series": {"season": "2", "episodes": "1-9", "total": 10,
"by_studio": {"LostFilm": "1-4", "Red Head Sound": "1"},
"full_release": ["Syncmer"]}
```
`full_release` — студии, у которых серии не указаны: для сериала это значит, что озвучены
все серии, вышедшие в релизе.
`episodes` и `total` берутся из заголовка («сезон 2, серии 1-9 из 10»), `by_studio` — из
строк дорожек. Диапазоны разбирает `src/episodes.py` правилами (числа и диапазоны
точны); размеченных примеров пока мало (серии указаны примерно в 5% строк nnm), поэтому
обучаемый разбор можно делать после накопления данных сборщиком.