From 547940ea59a650d4200be2e1d3de3d455563c9cd Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Sat, 13 Jun 2026 17:42:25 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A3=D1=82=D0=BE=D1=87=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B4=D0=B5=D1=82=D0=B0=D0=BB=D0=B5=D0=B9=20?= =?UTF-8?q?=D0=B0=D1=80=D1=85=D0=B8=D1=82=D0=B5=D0=BA=D1=82=D1=83=D1=80?= =?UTF-8?q?=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/drafts/ideas.md | 8 +-- docs/drafts/roadmap.md | 20 +++--- docs/specs/README.md | 2 + docs/specs/architecture.md | 67 +++++++++++++++----- docs/specs/recognition.md | 30 +++++++-- docs/specs/review-ux.md | 123 +++++++++++++++++++++++++++++++++++++ 6 files changed, 220 insertions(+), 30 deletions(-) create mode 100644 docs/specs/review-ux.md diff --git a/docs/drafts/ideas.md b/docs/drafts/ideas.md index 8d894de..c245372 100644 --- a/docs/drafts/ideas.md +++ b/docs/drafts/ideas.md @@ -19,10 +19,10 @@ Jellyfin ждёт `SxxEyy`. Нужен пересчёт абсолютной н ## Завершение загрузки через webhook -Сейчас план — поллинг qBittorrent. Альтернатива: «Run external program on -torrent completion» в qBittorrent дёргает эндпоинт jellybit. Реагирует -быстрее, но связывает нас с конфигом qBittorrent. Решим по опыту -эксплуатации. +Сейчас принято — поллинг qBittorrent раз в несколько секунд. +Альтернатива: «Run external program on torrent completion» в qBittorrent +дёргает эндпоинт jellybit. Реагирует быстрее, но связывает нас с конфигом +qBittorrent. Решим по опыту эксплуатации. ## Нотификации о готовности diff --git a/docs/drafts/roadmap.md b/docs/drafts/roadmap.md index 6855b5e..852f150 100644 --- a/docs/drafts/roadmap.md +++ b/docs/drafts/roadmap.md @@ -6,22 +6,28 @@ ## Фазы - **Ф0 — каркас.** go.mod, раскладка пакетов, загрузка TOML-конфига, - SQLite + миграции, slog-логи, Dockerfile (static → distroless), - golangci-lint, lefthook. Документация (этот этап — частично готов). + SQLite + миграции, slog-логи, `Dockerfile` (минимальный рантайм-образ, + копирует готовый бинарь), golangci-lint, lefthook. Документация (этот + этап — частично готов). - **Ф1 — ingest + tracking (без LLM).** `Ingest()` + добавление в qBittorrent (категория `jellybit`) + `worker`-поллинг завершения + машина состояний. Наружу: HTTP API, список в веб-UI, `jellybit add`. - **Ф2 — распознавание.** `go-ptn` + LLM (structured output) → план + оценка уверенности. Без записи на диск. - **Ф3 — раскладка + минимальный review.** Хардлинки по конвенциям - Jellyfin, субтитры, идемпотентность. Авто при высокой уверенности; - низкая → экран подтверждения (htmx). + Jellyfin, субтитры, идемпотентность, **undo**. Авто при высокой + уверенности; низкая → review (htmx): подсказка + перераспознавание, из + ручного — тип, выбор кандидата базы, пометка «игнор». Полный редактор + маппинга — Ф5. См. [review-ux.md](../specs/review-ux.md). - **Ф4 — метаданные.** TMDB/TVDB опционально, provider-id в именах, валидация распознавания против числа серий. - **Ф5 — Telegram + UX.** Бот-адаптер + парсер сообщений торрент-бота, - подтверждение в боте, триггер скана Jellyfin, нотификации. -- **Ф6 — деплой.** Static-образ/бинарь + обвязка в umbar - (`playbook-jellybit.yml`). + подтверждение в боте (карточка + кнопки + reply-подсказка, эскалация в + веб), полный редактор маппинга «файл → серия», триггер скана Jellyfin, + нотификации. +- **Ф6 — деплой.** Сборка статического бинаря здесь; доставка бинаря + + `Dockerfile` на сервер, `docker build` и запуск на месте; оркестрация — + `playbook-jellybit.yml` в umbar. ## Заметки по порядку diff --git a/docs/specs/README.md b/docs/specs/README.md index 64b85c5..203e208 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -18,5 +18,7 @@ поток, машина состояний, хранилище, конфигурация. - [recognition.md](recognition.md) — распознавание контента и модель уверенности. +- [review-ux.md](review-ux.md) — ревью раскладки человеком: UI/UX-сценарии + на случай, когда система не уверена. - [jellyfin-layout.md](jellyfin-layout.md) — конвенции именования файлов Jellyfin, в которые раскладываем. diff --git a/docs/specs/architecture.md b/docs/specs/architecture.md index 4d1bd71..e5bbeb8 100644 --- a/docs/specs/architecture.md +++ b/docs/specs/architecture.md @@ -26,7 +26,8 @@ qBittorrent, определяет содержимое (фильм или сер | `ingest` | use-case приёма загрузки, общий для всех транспортов | | `qbt` | клиент qBittorrent WebUI API | | `worker` | фоновый цикл: машина состояний, поллинг завершения | -| `recognize` | пред-парс имени + LLM + модель уверенности | +| `recognize` | пред-парс имени + вызов LLM + модель уверенности | +| `llm` | провайдер LLM за интерфейсом (дискриминатор `type`) | | `metadata` | интерфейс баз метаданных + TMDB/TVDB (опц.) | | `layout` | конвенции Jellyfin + хардлинкер | | `store` | SQLite: загрузки, распознавание, ссылки | @@ -44,10 +45,12 @@ ingest → downloading → completed → recognizing ─┬─ уверенно - **ingest** — приняли источник + контекст, поставили в qBittorrent (категория `jellybit`), записали в БД. -- **downloading / completed** — `worker` поллит qBittorrent по категории. +- **downloading / completed** — `worker` поллит qBittorrent по категории + (интервал `worker.poll_interval`, по умолчанию 5 с). - **recognizing** — `recognize` строит план раскладки и оценку уверенности (см. [recognition.md](recognition.md)). -- **review** — план уходит человеку (веб-UI / Telegram), ждём решения. +- **review** — план уходит человеку (веб-UI / Telegram), ждём решения; + сценарии — в [review-ux.md](review-ux.md). - **linking** — `layout` создаёт хардлинки в библиотеке. - **done** — опционально дёргаем скан библиотеки Jellyfin. @@ -77,7 +80,10 @@ SQLite, минимум таблиц: ## Конфигурация -TOML, секреты — placeholder'ы; реальный конфиг не коммитим. Пример: +TOML. В репозитории — `config.example.toml` с placeholder'ами; реальный +`config.toml` рендерится при деплое Ansible-шаблоном из переменных umbar +(секреты — в `vars/secrets.yml` под ansible-vault) и не коммитится. +Пример: ```toml [qbittorrent] @@ -92,9 +98,11 @@ movies = "/srv/media/movies" series = "/srv/media/series" [llm] -provider = "anthropic" -model = "claude-sonnet-4-6" # сложные случаи — claude-opus-4-8 +# type — дискриминатор реализации; пока поддерживается "openai-compat" +type = "openai-compat" +base_url = "http://127.0.0.1:1234/v1" api_key = "" +model = "qwen2.5-32b-instruct" [metadata.tmdb] enabled = true @@ -104,6 +112,9 @@ api_key = "" enabled = false api_key = "" +[worker] +poll_interval = "5s" # как часто опрашивать qBittorrent + [recognition] auto_confidence_threshold = 0.85 @@ -126,20 +137,39 @@ format = "json" сквозным идентификатором; решения распознавания (почему авто/ревью) логируются явно. +## Деплой + +Jellybit работает в **docker** — в одной среде с qBittorrent и Jellyfin +(они тоже в контейнерах на umbar). Единая среда запуска перевешивает +простоту нативного systemd. + +Сборка — дёшево и сердито: статический бинарь собирается здесь; на сервер +во временную build-папку кладутся бинарь + `Dockerfile` (он просто +копирует бинарь в минимальный образ), образ собирается прямо на сервере и +запускается. Go-тулчейн на сервере не нужен — только docker. + +Разделение ответственности: + +- **jellybit** (этот репозиторий) — производит статический бинарь и + `Dockerfile`. +- **umbar** — оркестрация деплоя: доставка артефактов, `docker build` и + запуск через docker compose (`playbook-jellybit.yml`). + ## Раскладка файлов Хардлинки в `paths.movies` / `paths.series` по конвенциям Jellyfin. Детали и крайние случаи — в [jellyfin-layout.md](jellyfin-layout.md). -Требование: `downloads` и `media` на одной ФС (иначе хардлинк -невозможен). Если jellybit в docker — смонтировать общего родителя, -чтобы хардлинк работал внутри контейнера. +`/srv/downloads` и `/srv/media` — одна ФС (подтверждено), поэтому +хардлинки применимы. Так как jellybit в docker (см. «Деплой»), контейнеру +монтируем общего родителя `/srv` — чтобы внутри оба каталога остались на +одной ФС и хардлинк проходил. ## Предполагаемая структура репозитория ``` cmd/jellybit/ точка входа, сборка зависимостей internal/ - ingest/ qbt/ worker/ recognize/ metadata/ + ingest/ qbt/ worker/ recognize/ llm/ metadata/ layout/ store/ httpapi/ tgbot/ config/ migrations/ миграции SQLite web/templates/ шаблоны веб-UI @@ -147,9 +177,18 @@ docs/ specs / adr / drafts config.example.toml ``` +## Решённые вопросы + +- `/srv/downloads` и `/srv/media` — одна ФС (подтверждено); хардлинки + применимы. +- Детект завершения — поллинг qBittorrent раз в несколько секунд + (`worker.poll_interval`). Webhook — возможная оптимизация на будущее + ([drafts/ideas.md](../drafts/ideas.md)). +- Секреты — в переменных umbar; `config.toml` рендерится Ansible-шаблоном + при деплое. +- Форма запуска — **docker**, образ собирается на сервере из готового + бинаря (см. «Деплой»). + ## Открытые вопросы -- Подтвердить, что `/srv/downloads` и `/srv/media` — одна ФС. -- Способ детекта завершения: поллинг (старт) или webhook qBittorrent - «run on completion» (позже). -- Где хранить секреты при деплое (шаблонизация из umbar). +Существенных пока нет. diff --git a/docs/specs/recognition.md b/docs/specs/recognition.md index e086cde..95c17fa 100644 --- a/docs/specs/recognition.md +++ b/docs/specs/recognition.md @@ -20,9 +20,10 @@ 1. **Пред-парс** имени релиза дешёвым парсером (`go-ptn`): черновые название/год/сезон/серия и качество. Грубо, но бесплатно. -2. **LLM** (Anthropic, structured output): получает все сигналы и - пред-парс, возвращает структурированный план. Хорошо справляется с - русскими релиз-именами, чего не умеет парсер. +2. **LLM** (через провайдер-абстракцию, см. «Провайдер LLM»): получает + все сигналы и пред-парс, возвращает структурированный план в нашей + схеме. Хорошо справляется с русскими релиз-именами, чего не умеет + парсер. 3. **Сверка с базой** (опц., если включена TMDB/TVDB): подтверждаем название+год, берём официальный id и каноническое имя. 4. **Оценка уверенности** и решение: авто-раскладка или ревью. @@ -42,6 +43,24 @@ confidence 0..1 — самооценка модели по полям notes пояснения, неоднозначности ``` +## Провайдер LLM + +Доступ к LLM — за интерфейсом; конкретная реализация выбирается полем +`[llm].type` в конфиге (дискриминатор). Это позволяет подключать +локальные модели и сторонние (в т.ч. китайские) эндпоинты — ради экономии +и независимости от одного вендора. + +- Первый и пока единственный тип — **`openai-compat`**: OpenAI-совместимый + Chat Completions API (`base_url` + `api_key` + `model`). Под него + подходят локальные серверы (LM Studio, llama.cpp, Ollama) и облачные + совместимые провайдеры (DeepSeek, Qwen и др.). +- Структурированный вывод: запрашиваем JSON по нашей схеме + (`response_format` со схемой там, где поддерживается; иначе json-режим + или tool-call), **валидируем в Go** и ретраим при несоответствии — + серверы различаются по поддержке строгих схем. +- Новые типы (напр. нативный `anthropic`) добавляются, не трогая + `recognize`. + ## Модель уверенности Авто-раскладка только если выполнено всё: @@ -54,8 +73,9 @@ notes пояснения, неоднозначности - сериал: число серий бьётся с базой (если есть), нумерация S·E консистентна, без пропусков и дублей. -Иначе план уходит в **review**. На экране подтверждения всегда видно, -*почему* не авто — это страховка на дорогих файлах. +Иначе план уходит в **review** (сценарии — [review-ux.md](review-ux.md)). +На экране подтверждения всегда видно, *почему* не авто — это страховка на +дорогих файлах. ## Что делаем с краёв diff --git a/docs/specs/review-ux.md b/docs/specs/review-ux.md new file mode 100644 index 0000000..75e516e --- /dev/null +++ b/docs/specs/review-ux.md @@ -0,0 +1,123 @@ +# Ревью раскладки человеком + +Что происходит, когда система не уверена в распознавании и не +раскладывает файлы автоматически. Когда именно наступает ревью — см. +[recognition.md](recognition.md); конвенции целевых имён — +[jellyfin-layout.md](jellyfin-layout.md). + +Главный принцип: ревью — это **петля «догадка → подсказка человека → +перераспознавание»**, а не статичное «ок/нет». Человек остаётся +супервизором, а не оператором ручного ввода. + +## Когда наступает + +Загрузка уходит в `review`, если сработал любой триггер модели +уверенности: низкая самооценка LLM; нет матча в базе (или несколько +кандидатов); структурная валидация ругается (у фильма >1 основного +файла; число серий не бьётся с базой; дыры/дубли в нумерации S·E). +В интерфейсе всегда видна **конкретная причина**, а не просто «не уверен». + +## Поверхность решения (едина для всех транспортов) + +1. **Источник:** имя торрента, переданный контекст, дерево файлов с + размерами, (если из бота) распарсенное сообщение. +2. **Догадка системы:** тип, название, год, сезон, матч базы и + **превью целевой раскладки** — буквальные пути, которые создадутся. +3. **Причина сомнения.** + +## Действия + +- **Применить** — сделать хардлинки по плану. +- **Уточнить и перераспознать** — добавить подсказку текстом → LLM + перезапускается с исходными сигналами и накопленными подсказками → + новый план. Главный путь, когда «LLM не справился». +- **Поправить вручную** — объём зависит от версии (см. ниже). +- **Выбрать кандидата базы** / ввести id / «без базы». +- **Отклонить** / **Позже**. + +**Подсказка vs override.** Подсказка мягкая — LLM её интерпретирует. +Ручная правка поля — жёсткий **override**: система берёт значение как +есть и «пиннит» его, перераспознавание не затирает уже поправленное. + +## Веб-UI — точные правки + +``` +Fargo.S02.2015.WEB-DL.1080p.rus.eng 🟡 review +Причины: нет в TMDB · уверенность 0.46 + +Контекст: «второй сезон, рус+англ дорожки» [+ добавить → 🔁 перераспознать] + +Тип: ( ) фильм (•) сериал Название: Фарго Год: 2015 Сезон: 02 +База: [TMDB поиск…] [TVDB поиск…] выбрано: — (без базы) [ввести id] + +Файлы → серии: + # | файл | размер | роль | S | E + 1 | Fargo.S02E01.rus.mkv | 3.1 GB | эпизод | 02 | 01 + … [нумеровать подряд] [сброс] + 9 | sample.mkv | 40 MB | игнор | – | – + +Превью: + series/Фарго (2015)/Season 02/Фарго (2015) S02E01.mkv ← #1 +[ Применить ] [ Отклонить ] [ Позже ] +``` + +Ядро экрана для сериала — таблица «файл → серия» с живой валидацией +дыр/дублей и кнопкой «нумеровать подряд» (частый случай: файлы по +порядку, но подписаны криво). Для фильма проще: выбрать основной файл, +остальное — extra/sample/субтитры/игнор. + +## Telegram — быстро, где пользователь и так есть + +``` +🟡 Нужно подтверждение +Источник: Fargo.S02.2015.WEB-DL.1080p +Похоже на: 📺 сериал «Фарго», сезон 2 (2015) +База: TMDB не найдено · уверенность низкая +План: 10 видео → series/Фарго (2015)/Season 02/…E01–E10 + +[✅ Применить] [📺↔🎬 Тип] +[🔢 Выбрать в базе] [🔁 Уточнить] +[🌐 Открыть в вебе] [❌ Отклонить] +``` + +- **🔁 Уточнить** → бот просит подсказку ответом → перераспознаёт → + редактирует то же сообщение новым планом. Петля коррекции прямо в чате. +- **🔢 Выбрать в базе** → кнопки по кандидатам (название · год · id). +- Точечное переназначение файлов в чат не помещается → **🌐 Открыть в + вебе** (deep-link на ту же страницу). + +## Разделение труда + +Telegram = одобрить / подсказать / выбрать кандидата / эскалировать в +веб. Веб = точные правки. Состояние ревью одно (в SQLite) — действовать +можно из любого транспорта, последнее слово побеждает. + +## Крайние сценарии + +- **База неоднозначна** → выбор кандидата (часто чинит всё разом: пиннит + provider-id и каноническое имя). +- **База пустая (рус/аниме)** → «без базы» или ручной id/url. Аниме с + абсолютной нумерацией → веб-хелпер «absolute → S·E» (см. + [drafts/ideas.md](../drafts/ideas.md)). +- **Не тот тип (movie↔series)** → переключатель пересобирает форму плана. +- **Мусор (sample/extra/дубли дорожек)** → роль «игнор». +- **Полный провал** (LLM ничего не вытащил) → веб-«ручной режим»: выбрать + тип, ввести название/год, разложить файлы руками; в Telegram — сразу + эскалация в веб. + +## Вход в ревью и откат + +- Переход в `review` **пингует** (сообщение в Telegram / бейдж в вебе) — + пользователя зовут, а не он опрашивает. Таймера нет, источник + продолжает сидировать. +- После «Применить» показываем, что создано. **Undo** — убрать созданные + хардлинки одной кнопкой (источник цел); страховка от ошибочного + подтверждения. + +## Объём по версиям + +- **Ф3 (первая версия):** подсказка + перераспознавание; из ручного — + переключатель типа, выбор кандидата базы, пометка файла «игнор». Undo — + есть. +- **Ф5:** полный редактор маппинга «файл → серия», ручной режим, + подтверждение в Telegram с reply-подсказкой и эскалацией в веб.