Добавил поиск метаданных по каталогам
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user