Добавил поиск метаданных по каталогам

This commit is contained in:
2026-06-14 15:21:01 +03:00
parent 9c1b178e46
commit 5087f35861
21 changed files with 1435 additions and 72 deletions
+51 -16
View File
@@ -4,14 +4,16 @@
// Конвейер (см. docs/specs/recognition.md):
// 1. пред-парс имени релиза (go-ptn) — черновые название/год/сезон/серия;
// 2. вызов LLM со структурированным выводом → план в нашей схеме;
// 3. валидация плана в Go (схема + структура + согласованность сигналов);
// 4. решение «авто или review».
// 3. сверка с базами метаданных (TMDB/TVDB, опц.) — единичный сильный матч
// по названию+году даёт официальный id и каноническое имя;
// 4. решение «авто или review»: авто только при подтверждённом матче,
// чистой структурной валидации (для сериала — число серий бьётся с
// базой), согласованности с пред-парсом и уверенности не ниже порога.
//
// Ф2 не сверяется с метабазами (TMDB/TVDB — Ф4) и ничего не пишет на диск:
// без подтверждённого матча в базе авто-раскладка не делается, поэтому в
// этой фазе решение всегда «review». Выход LLM недоверенный — план
// принимается только если каждый files[].src совпадает с реальным файлом
// торрента; итоговая безопасность пути держится на раскладке (Ф3).
// Без включённых баз (или без матча) авто-раскладка не делается — задача
// уходит в review. Выход LLM недоверенный: план принимается только если
// каждый files[].src совпадает с реальным файлом торрента; итоговая
// безопасность пути держится на раскладке (layout).
package recognize
import (
@@ -20,6 +22,7 @@ import (
"log/slog"
"git.vakhrushev.me/av/jellybit/internal/llm"
"git.vakhrushev.me/av/jellybit/internal/metadata"
)
// MediaType — вид контента.
@@ -101,11 +104,21 @@ type Decision struct {
Reasons []string // причины ухода в review / предупреждения валидации
}
// Match — подтверждение распознавания базой метаданных.
type Match struct {
Provider string // "tmdb" | "tvdb"
ProviderID string // официальный id
Title string // каноническое название
Year int // каноничный год
SeasonEpisodeCounts map[int]int // число серий по сезонам (для сериала)
}
// Result — итог распознавания.
type Result struct {
Plan Plan
PreParse PreParse
Decision Decision
Match *Match // подтверждённый матч в базе (nil — нет)
Attempts int // сколько вызовов LLM понадобилось (вкл. ретраи разбора)
Raw string // сырой ответ LLM последней попытки (для recognition.raw_llm)
}
@@ -117,27 +130,32 @@ type LLM interface {
// Config — параметры распознавания.
type Config struct {
MaxRetries int // переразбор ответа со схемой-в-промпте ([llm].max_retries)
MaxTokens int // лимит ответа модели (0 — дефолт)
MaxFiles int // усечение списка файлов в промпте (0 — дефолт)
MaxRetries int // переразбор ответа со схемой-в-промпте ([llm].max_retries)
MaxTokens int // лимит ответа модели (0 — дефолт)
MaxFiles int // усечение списка файлов в промпте (0 — дефолт)
AutoThreshold float64 // порог уверенности для авто (0 — дефолт 0.85)
}
const (
defaultMaxTokens = 4000
defaultMaxFiles = 100
defaultMaxTokens = 4000
defaultMaxFiles = 100
defaultAutoThreshold = 0.85
)
// Recognizer — реализация распознавания.
type Recognizer struct {
llm LLM
providers []metadata.Provider
maxRetry int
maxTokens int
maxFiles int
threshold float64
log *slog.Logger
}
// New собирает распознаватель.
func New(provider LLM, cfg Config, log *slog.Logger) *Recognizer {
// New собирает распознаватель. providers — включённые базы метаданных
// (пусто → сверки нет, авто-раскладка не делается).
func New(provider LLM, providers []metadata.Provider, cfg Config, log *slog.Logger) *Recognizer {
maxTokens := cfg.MaxTokens
if maxTokens <= 0 {
maxTokens = defaultMaxTokens
@@ -150,11 +168,17 @@ func New(provider LLM, cfg Config, log *slog.Logger) *Recognizer {
if retries < 0 {
retries = 0
}
threshold := cfg.AutoThreshold
if threshold <= 0 {
threshold = defaultAutoThreshold
}
return &Recognizer{
llm: provider,
providers: providers,
maxRetry: retries,
maxTokens: maxTokens,
maxFiles: maxFiles,
threshold: threshold,
log: log,
}
}
@@ -209,15 +233,26 @@ func (r *Recognizer) Recognize(ctx context.Context, in Input) (Result, error) {
}, nil
}
dec := decide(plan, pre)
// Сверка с базой: подтверждаем id + каноническое имя; при матче имя/год
// в плане заменяем на каноничные.
match := r.matchMetadata(ctx, plan)
if match != nil {
plan.Title = match.Title
if match.Year != 0 {
plan.Year = match.Year
}
}
dec := decide(plan, pre, match, len(r.providers) > 0, r.threshold)
r.log.Info("recognize: done",
"type", plan.Type, "title", plan.Title, "year", plan.Year,
"files", len(plan.Files), "attempts", attempts,
"auto", dec.Auto, "reasons", len(dec.Reasons))
"matched", match != nil, "auto", dec.Auto, "reasons", len(dec.Reasons))
return Result{
Plan: plan,
PreParse: pre,
Decision: dec,
Match: match,
Attempts: attempts,
Raw: raw,
}, nil