2026-05-13 media-toolbox: детальный план агента для unresolved, статус обновлён
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user