Уточнение деталей архитектуры

This commit is contained in:
2026-06-13 17:42:25 +03:00
parent 5b53f4e8e8
commit 547940ea59
6 changed files with 220 additions and 30 deletions
+2
View File
@@ -18,5 +18,7 @@
поток, машина состояний, хранилище, конфигурация.
- [recognition.md](recognition.md) — распознавание контента и модель
уверенности.
- [review-ux.md](review-ux.md) — ревью раскладки человеком: UI/UX-сценарии
на случай, когда система не уверена.
- [jellyfin-layout.md](jellyfin-layout.md) — конвенции именования файлов
Jellyfin, в которые раскладываем.
+53 -14
View File
@@ -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).
Существенных пока нет.
+25 -5
View File
@@ -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)).
На экране подтверждения всегда видно, *почему* не авто — это страховка на
дорогих файлах.
## Что делаем с краёв
+123
View File
@@ -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/…E01E10
[✅ Применить] [📺↔🎬 Тип]
[🔢 Выбрать в базе] [🔁 Уточнить]
[🌐 Открыть в вебе] [❌ Отклонить]
```
- **🔁 Уточнить** → бот просит подсказку ответом → перераспознаёт →
редактирует то же сообщение новым планом. Петля коррекции прямо в чате.
- **🔢 Выбрать в базе** → кнопки по кандидатам (название · год · 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-подсказкой и эскалацией в веб.