From 26d171dd22d069013993b665f46abd647159ab65 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 13 May 2026 13:36:19 +0600 Subject: [PATCH] =?UTF-8?q?2026-05-13=20media-toolbox:=20=D0=B4=D0=B5?= =?UTF-8?q?=D1=82=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20=D0=BF=D0=BB=D0=B0?= =?UTF-8?q?=D0=BD=20=D0=B0=D0=B3=D0=B5=D0=BD=D1=82=D0=B0=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20unresolved,=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81=20?= =?UTF-8?q?=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D1=91=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- family/projects/media-toolbox-kraken.md | 226 ++++++++++++++++++++++-- 1 file changed, 210 insertions(+), 16 deletions(-) diff --git a/family/projects/media-toolbox-kraken.md b/family/projects/media-toolbox-kraken.md index 8ea93131..9dea1823 100644 --- a/family/projects/media-toolbox-kraken.md +++ b/family/projects/media-toolbox-kraken.md @@ -55,21 +55,22 @@ TrueNAS (Taiga) | Шаг | Статус | Заметки | |-----|--------|---------| -| 1 — Исследование (Kodi lib на HTPC) | ✅ | Data: мультфильмы(229G) сериалы(116G) movies(673G) bbc(284G). kodi/movies/ — NFO+симлинки уже есть | -| 2 — Wake HTPC + prevent sleep | ✅ | sleep/suspend/hibernate masked (systemctl mask). loginctl linger=yes. Процессы выживают после SSH | -| 3 — Копирование медиа HTPC → Kraken | 🔄 | 4x rsync via systemd-run --user, активны. ~1.3TB total | -| 4 — Transmission на Кракене | 🔲 | уже есть контейнер в /home/kraken/media/ | -| 5 — Перенос torrent-базы с HTPC | ✅ | 7 торрентов Windows Transmission добавлены в Крaken Transmission (paused), пути обновить после rsync | -| 6 — MediaFilesScraper: изучить + настроить | ✅ | Полный Swift 6 SPM. media-pipeline CLI. Пропатчен для Linux (AVFoundation stub, Swift 6 existential fix) | -| 7 — Cross-compile Mac → Linux ARM64 | 🔲 | Swift toolchain для aarch64 | -| 8 — Docker-образ kodi-media-sync на Кракене | 🔲 | | -| 9 — Config: пути, Transmission, категории | 🔲 | | -| 10 — Dry run: проверка сортировки | 🔲 | | -| 11 — Jellyfin: настройка библиотек | 🔲 | уже есть контейнер | -| 12 — Transmission download-complete hook | 🔲 | | -| 13 — Hermes cron на Кракене | 🔲 | | -| 14 — Руtracker-скилл (поиск/закачка) | 🔲 | | -| 15 — Музыка: скачка + sync с TrueNAS | 🔲 | | +| 1 — Исследование (Kodi lib на HTPC) | ✅ | Data: мультфильмы(229G) сериалы(116G) movies(673G) bbc(284G) | +| 2 — Wake HTPC + prevent sleep | ✅ | sleep masked, linger=yes, rsync через systemd-run --user | +| 3 — Копирование медиа HTPC → Kraken | 🔄 | ~58GB/категорию скопировано, ~1.3TB всего, rsync активен | +| 4 — Transmission на Кракене | ✅ | 128 торрентов (7 старых + 121 новый), paused, категоризированы | +| 5 — Перенос torrent-базы | ✅ | 121 медиа-торрент импортирован из Windows Downloads | +| 6 — MediaFilesScraper: изучить + настроить | ✅ | Swift 6 SPM, пропатчен для Linux, OpenRouter вместо OpenAI | +| 7 — Docker build ARM64 | ✅ | Colima 24GB, `--memory=20g -j 1`, образ 78MB задеплоен | +| 8 — Конфиг на Кракене | ✅ | config.json, on-download-complete.sh на месте | +| 9 — Dry run | 🔄 | 1/6 resolved (Black Mirror S06E01 ✅), остальное ждёт rsync | +| 10 — OpenRouter ключ | ⚠️ | Нужно вставить sk-or-v1-... в config.json на Кракене | +| 11 — Агент для unresolved | 🔲 | см. шаг 16 ниже | +| 12 — Jellyfin: настройка библиотек | 🔲 | после rsync | +| 13 — Transmission download-complete hook | ✅ | установлен и прописан | +| 14 — Hermes cron (unresolved notify) | 🔲 | см. шаг 16 | +| 15 — Rutracker-скилл | 🔲 | | +| 16 — Музыка: sync с TrueNAS | 🔲 | | --- @@ -549,7 +550,200 @@ cat /srv/.../media/movies/SomeMovie/SomeMovie.nfo --- -## Шаг 11 — Jellyfin: настройка библиотек +## Шаг 11 (новый) — Встроенный агент для unresolved файлов + +### Концепция + +Когда стандартный resolve-пайплайн (TMDB → KP → IMDb → WebSearch) не смог идентифицировать файл, вместо записи "unresolved" — запускается LLM-агент с инструментами. Агент работает как последний уровень: пробует поиск по-другому, подтверждает кандидата, либо честно записывает "не могу". + +**Активация:** `media-pipeline agent --config /config/config.json [--path ...]` +Или автоматически после `resolve` если остались unresolved регионы. + +--- + +### Архитектура агента + +#### Промпт (системный) +``` +You are a media file identifier. You help identify what a file is so it can be +categorized correctly in a media library. + +You are given: +- File path and name +- If the file is NOT in a root media folder: full listing of its parent directory + (helps distinguish: single movie, series season pack, documentary collection, etc.) +- Any context already gathered (partial TMDB/KP results, failed queries) + +Your job: identify the content and return a structured JSON result. + +Tools available: +- search(query) → DuckDuckGo HTML results (titles, URLs, snippets) +- fetch(url) → raw page content (use for TMDB/IMDb/KP specific pages) + +Files can be: movie, series, cartoon, documentary, music album, game, book, +software, or other. Support "uncategorizable" as last resort. + +Always prefer TMDB ID when available. Think step by step. Use tools to verify. +``` + +#### Инструменты + +```swift +// Поверх существующего DuckDuckGo HTML scraper (уже есть в IMDbAPI/WebSearchStrategy) +struct AgentSearchTool { + func search(_ query: String) async -> [SearchResult] // DDG HTML, top 5 + func fetch(_ url: URL) async -> String // curl-like, обрезан до 8k символов +} +``` + +#### Выходной JSON (с валидацией) + +```json +{ + "status": "resolved" | "uncategorizable" | "needs_manual", + + // Если resolved: + "media_type": "movie" | "series" | "cartoon" | "documentary" | + "music" | "game" | "book" | "software" | "other", + "title": "Название на языке оригинала", + "title_ru": "Русское название (если известно)", + "year": 2024, + + // IDs (хотя бы один обязателен для resolved): + "tmdb_id": "12345", + "imdb_id": "tt1234567", + "kp_id": "12345", + + // Для сериалов: + "season": 1, + "episode": 5, // если отдельный эпизод + "is_season_pack": true, + "episodes_count": 10, + + // Метаданные: + "genres": ["драма", "триллер"], + "confidence": 0.9, // 0.0–1.0, агент сам оценивает + "notes": "...", // почему uncategorizable или что смущает + + // Для группировки: + "is_sideband": false, // субтитры, NFO, постер — не самостоятельный контент + "parent_title": "...", // если sideband — к чему относится +} +``` + +**Валидация:** после каждого ответа LLM — JSON schema check. Если невалидный — повторный запрос с описанием ошибки (до 2 попыток исправить). + +--- + +### Хранилище unresolved файлов + +``` +/srv/.../docker/media-pipeline/unresolved.json +``` + +```json +[ + { + "path": "/media/downloads/complete/Знакомимся с кабанами", + "added_at": "2026-05-13T04:00:00Z", + "last_attempt": "2026-05-13T04:00:00Z", + "attempts": 1, + "agent_notes": "Не нашлось в TMDB/KP, агент тоже не смог — слишком общее название", + "status": "needs_manual" + } +] +``` + +--- + +### Force-resolve через CLI + +```bash +# Разрезолвить конкретный файл вручную (из файла) +media-pipeline resolve-manual \ + --config /config/config.json \ + --path "/media/downloads/complete/Знакомимся с кабанами" \ + --result '{"status":"resolved","media_type":"documentary","title":"Meet the Boars","tmdb_id":"12345",...}' + +# Или из файла (удобно для скилла Кракена): +media-pipeline resolve-manual \ + --config /config/config.json \ + --from-file /tmp/manual-resolve.json +``` + +Что делает `resolve-manual`: +1. Берёт готовый JSON результат +2. Применяет (создаёт NFO, перемещает если нужно, обновляет Transmission пути) +3. Удаляет запись из `unresolved.json` +4. Пишет в лог + +--- + +### Cron: почасовой обход + Hermes notify + +**Скрипт-чекер** `/srv/.../docker/media-pipeline/check-unresolved.sh`: +```bash +#!/bin/bash +# Запускает agent только если есть новые unresolved файлы +UNRESOLVED=/srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/docker/media-pipeline/unresolved.json + +# Нет файла — нечего делать +[ -f "$UNRESOLVED" ] || exit 0 + +# Считаем needs_manual записи +COUNT=$(jq '[.[] | select(.status == "needs_manual")] | length' "$UNRESOLVED" 2>/dev/null || echo 0) + +# Нет needs_manual — тихо выходим (Hermes не получит пустое сообщение) +[ "$COUNT" -gt 0 ] || exit 0 + +echo "UNRESOLVED_COUNT=$COUNT" +# Вывод идёт в Hermes cron prompt как контекст +jq '[.[] | select(.status == "needs_manual") | {path, added_at, agent_notes}]' "$UNRESOLVED" +``` + +**Hermes cron** (каждый час, Кракен): +``` +Schedule: 0 * * * * +Script: /srv/.../docker/media-pipeline/check-unresolved.sh +Prompt: + Скрипт вернул список нераспознанных медиафайлов (вывод выше). + Для каждого файла: + 1. Посмотри на путь и agent_notes + 2. Если можешь идентифицировать (поиск, здравый смысл) — вызови: + docker run --rm ... media-pipeline resolve-manual --path "..." --result '{...}' + 3. Если не можешь — отправь уведомление в Telegram с описанием файла и попроси + меня разрезолвить вручную. + Не присылай сообщение если всё уже разрезолвлено или список пустой. +``` + +Скрипт запускается **до** обращения к модели — если пустой вывод, Hermes не получает запрос. Нет пустых сообщений. + +--- + +### Реализация в media_files_db + +**Новые файлы:** +``` +Sources/MediaPipeline/Agent/ + AgentCommand.swift ← CLICommand: media-pipeline agent + AgentSession.swift ← LLM loop: prompt → tools → validate → save + AgentTools.swift ← search() + fetch() wrappers + AgentResultSchema.swift ← AgentResult struct + JSON validation + UnresolvedStore.swift ← чтение/запись unresolved.json + +Sources/MediaPipeline/ResolveManual/ + ResolveManualCommand.swift ← media-pipeline resolve-manual +``` + +**Зависимости уже есть:** +- `ChatGPTAPI` — LLM клиент (переключён на OpenRouter) +- `IMDbAPI` / DDG HTML scraper — search tool +- `HTTPClient` — fetch tool +- Кеш — все LLM вызовы кешируются через `ChatGPTAPI.sendPrompt(cacheKey:)` + +**Оценка объёма:** ~400-500 строк Swift, 2-3 дня работы. + +--- Jellyfin уже работает на Кракене: http://192.168.1.14:8096