Поиск по нескольким названиям при сверке с базой (recognition)
Сверка с метабазой промахивалась на иностранных фильмах с русским релиз-именем (кейс «Тёмный рыцарь»): поиск шёл по одной строке provider_hint||title и игнорировал original_title, а базы индексированы прежде всего по оригинальным названиям. - matchMetadata ищет по ключам original_title → title → provider_hint с ранним стопом на первом единичном сильном матче; пустые и нормализованно-дублирующие ключи пропускаются, кандидаты для review копятся из всех заходов. - Промпт требует всегда заполнять title и original_title (дублировать при отсутствии оригинала / российском контенте; при неуверенности дублировать, не выдумывать). Разбор остаётся мягким к пустому original_title. - TMDB-поиск передаёт language (по умолчанию ru-RU, настраивается [metadata.tmdb].language); original_title не зависит от локали. - Нормализация названий сводит ё→е. Инварианты не ослаблены: авто только при подтверждённом единичном матче + структурной валидации + согласованности сигналов. Capability recognition впервые перенесена в OpenSpec; change архивирован. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-06-29
|
||||
@@ -0,0 +1,92 @@
|
||||
## Context
|
||||
|
||||
Сверка распознавания (`internal/recognize/metadata.go`, `matchMetadata`)
|
||||
сейчас формирует один поисковый ключ `searchTitle = provider_hint || title`
|
||||
и шлёт его всем включённым провайдерам. Гейт `strongMatches` сравнивает
|
||||
нормализованные `{plan.Title, plan.OriginalTitle}` против
|
||||
`{cand.Title, cand.OriginalTitle}` и требует год ±1. Базы TMDB/TVDB
|
||||
индексированы прежде всего по оригинальным названиям, поэтому русское
|
||||
релиз-имя часто не находится, а если `original_title` пуст — английская
|
||||
сторона гейта вообще не работает. TMDB-поиск не передаёт `language`, так что
|
||||
локализованный `Title` приходит в дефолтной локали.
|
||||
|
||||
Источник истины по recognition пока `docs/specs/recognition.md` (capability
|
||||
ещё не перенесена в OpenSpec) — дельту синхронизируем с ним.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Повысить попадаемость сверки на иностранных фильмах с русским релиз-именем,
|
||||
не ослабляя гейты авто-раскладки.
|
||||
- Сделать оригинальное название всегда доступным для запроса и сравнения.
|
||||
- Минимальные, локальные правки в существующих функциях.
|
||||
|
||||
**Non-Goals:**
|
||||
- Не меняем модель уверенности и условия авто (матч в базе + структурная
|
||||
валидация + согласованность сигналов остаются как есть).
|
||||
- Не вводим fuzzy-сравнение названий (только точечная нормализация `ё`→`е`).
|
||||
- Не добавляем поле страны/языка происхождения в `Plan`.
|
||||
- Не трогаем TVDB/TVMaze-клиентов по части локали (вне объёма).
|
||||
|
||||
## Decisions
|
||||
|
||||
**1. Стратегия поиска «оригинал → fallback», а не мёрж всех запросов.**
|
||||
Перебираем ключи `[original_title, title, provider_hint]` по порядку,
|
||||
останавливаемся на первом, давшем единичный сильный матч. Оригинал —
|
||||
сильнейший ключ баз, обычно хватает первого захода; меньше обращений к
|
||||
API/квотам. Альтернатива — гнать все запросы и мёржить кандидатов — даёт чуть
|
||||
полнее список для review, но дороже по обращениям; отвергнута как избыточная.
|
||||
Кандидатов для review всё равно копим из всех фактически выполненных заходов.
|
||||
Дубль-ключи (нормализованно равные уже выполненному) пропускаем.
|
||||
|
||||
**2. `provider_hint` остаётся третьим фолбэком, а не удаляется.**
|
||||
Два канонических названия покрывают основной кейс, но `hint` иногда
|
||||
сформулирован удачнее (очищен от мусора релиз-имени) — дешёвая страховка,
|
||||
когда оба названия не нашлись. Удаление поля — лишняя правка схемы без явной
|
||||
выгоды.
|
||||
|
||||
**3. «Всегда заполнять оба названия» — через промпт, не через жёсткую схему.**
|
||||
Требование к модели: заполнять `title` и `original_title`, при отсутствии
|
||||
отдельного оригинала или для российского контента — дублировать `title`; при
|
||||
неуверенности — **дублировать, а не выдумывать**. Это и есть защита от
|
||||
ложного авто-матча: «не знаю» схлопывается в безопасное дублирование русского
|
||||
названия, а не в галлюцинацию английского, которая могла бы случайно
|
||||
сматчиться с реальным фильмом и привести к авто-раскладке не того тайтла.
|
||||
`parsePlan` остаётся мягким: пустой `original_title` не отбраковываем
|
||||
(graceful-фолбэк на `title`), чтобы не плодить correction-ретраи и не уходить
|
||||
в review зря.
|
||||
|
||||
**4. `language=ru-RU` для TMDB, всегда, параметризуемо конфигом.**
|
||||
Поле `original_title`/`original_name` у TMDB не зависит от `language` —
|
||||
английская сторона гейта не страдает. Локализованный `Title` приходит
|
||||
по-русски: сходится русская сторона гейта и аккуратнее карточки кандидатов в
|
||||
review. Деление на «российский/нероссийский» не нужно — `ru-RU` полезен
|
||||
именно зарубежке, а для русского контента нейтрален. Дефолт `ru-RU`, поле
|
||||
`[metadata.tmdb].language`.
|
||||
|
||||
**5. `ё`→`е` в `normalize`, и только это.**
|
||||
Узкая правка под реальный класс расхождений написания. `й`→`и` и прочие
|
||||
свёртки НЕ делаем — меняют смысл, риск ложных совпадений.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Модель всё же выдумывает оригинал вопреки промпту** → гейт по-прежнему
|
||||
требует год ±1 и единичность матча; авто только при подтверждённом матче.
|
||||
Промпт явно предписывает дублирование при неуверенности. Остаточный риск
|
||||
низкий и не выше текущего (галлюцинация `title` возможна и сейчас).
|
||||
- **`ru-RU` для контента без русской локализации** → TMDB отдаёт fallback
|
||||
(оригинал/английский), хуже текущего поведения не становится.
|
||||
- **Лишние обращения к базам при фолбэках** → ограничены порядком из 1–3
|
||||
запросов на провайдера, дубль-ключи пропускаются, ранний стоп на первом
|
||||
сильном матче. Ошибки провайдера по-прежнему не валят распознавание.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
- Изменения обратносовместимы по хранимым данным и API. Новое поле конфига
|
||||
`[metadata.tmdb].language` опционально (дефолт `ru-RU`).
|
||||
- Откат — ревёрт; персистентных миграций БД нет.
|
||||
- После apply синхронизировать `docs/specs/recognition.md` с дельтой.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Нет.
|
||||
@@ -0,0 +1,62 @@
|
||||
## Why
|
||||
|
||||
Сверка распознавания с базой метаданных промахивается на иностранных
|
||||
фильмах с русским релиз-именем: реальный кейс — «Тёмный рыцарь» (The Dark
|
||||
Knight), поиск не нашёл ничего, раскладку пришлось делать вручную. Базы
|
||||
(TMDB/TVDB) индексированы прежде всего по оригинальным названиям, а поиск
|
||||
сейчас идёт по одной строке `provider_hint || title` и игнорирует
|
||||
`original_title`. Из-за этого сильное звено сверки — оригинальное название —
|
||||
не используется ни в запросе, ни (когда модель оставила его пустым) в гейте
|
||||
сравнения.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Поиск по нескольким названиям** в сверке с базой: стратегия
|
||||
«оригинал → fallback» — сначала запрос по `original_title`, при единичном
|
||||
сильном матче стоп; иначе запрос по локализованному `title`; третьим
|
||||
фолбеком — `provider_hint`. Кандидатов для review копим из всех заходов
|
||||
(дедуп по `provider:id`). Избыточный запрос-дубль (когда нормализованные
|
||||
названия совпадают) пропускаем.
|
||||
- **Контракт LLM на оба названия:** промпт требует всегда заполнять и
|
||||
`title`, и `original_title`. Нет отдельного оригинала / российское
|
||||
происхождение → продублировать `title`. Не уверен в оригинале → дублировать,
|
||||
а **не выдумывать** (защита от ложного авто-матча по галлюцинации). Схема
|
||||
разбора остаётся мягкой: пустой `original_title` не отбраковываем, а
|
||||
graceful-фолбэк на `title`.
|
||||
- **Локаль TMDB:** TMDB-поиск всегда передаёт `language` (по умолчанию
|
||||
`ru-RU`), параметризуемый конфигом. Локализованный `Title` приходит
|
||||
по-русски — сходится русская сторона гейта и аккуратнее карточки кандидатов
|
||||
в review. `original_title` у TMDB остаётся на языке оригинала, английская
|
||||
сторона гейта не страдает.
|
||||
- **Нормализация названий:** в сравнении сводим `ё`→`е`, чтобы «Тёмный» и
|
||||
«Темный» считались одним названием.
|
||||
|
||||
Гейты авто-раскладки не ослабляются: авто по-прежнему только при
|
||||
подтверждённом единичном матче в базе + структурной валидации +
|
||||
согласованности сигналов. Изменение лишь повышает попадаемость сверки.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `recognition`: распознавание контента раздачи (фильм/сериал, название,
|
||||
год, сезон/серия), сверка с базами метаданных и решение «авто или review».
|
||||
Первый перенос capability из `docs/specs/recognition.md` в OpenSpec; в этот
|
||||
change фиксируем требования к сверке с базой и контракту названий,
|
||||
затронутые изменением (остальное мигрируется отдельно).
|
||||
|
||||
### Modified Capabilities
|
||||
<!-- Нет: recognition в OpenSpec ещё не было, заводим как новую. -->
|
||||
|
||||
## Impact
|
||||
|
||||
- `internal/recognize/metadata.go` — `matchMetadata` (многозапросный поиск),
|
||||
`normalize` (`ё`→`е`).
|
||||
- `internal/recognize/prompt.go` — правила промпта по `title`/`original_title`.
|
||||
- `internal/metadata/tmdb.go` — параметр `language` в запросе поиска.
|
||||
- Конфигурация — новое поле `[metadata.tmdb].language` (дефолт `ru-RU`),
|
||||
валидация на старте.
|
||||
- `docs/specs/recognition.md` — синхронизируем с дельтой (источник истины до
|
||||
полного переноса).
|
||||
- Внешнее API/схема ответа LLM: `provider_hint` сохраняется; новые требования
|
||||
к заполнению `original_title`. Обратная совместимость хранимых данных не
|
||||
затрагивается.
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Сверка с базой по нескольким названиям
|
||||
|
||||
При сверке плана с включёнными базами метаданных система SHALL искать по
|
||||
нескольким названиям в порядке убывания силы ключа: сначала по
|
||||
`original_title`, затем по локализованному `title`, затем по `provider_hint`.
|
||||
Поиск SHALL останавливаться, как только очередной запрос дал единичный
|
||||
сильный матч (ровно один кандидат с совпадением названия и года). Запрос с
|
||||
названием, нормализованно совпадающим с уже выполненным, система SHALL
|
||||
пропускать, чтобы не обращаться к базе повторно с тем же ключом.
|
||||
|
||||
Кандидаты для ручного выбора в review система SHALL собирать из всех
|
||||
выполненных заходов с дедупликацией по `provider:id` и общим потолком.
|
||||
|
||||
#### Scenario: Иностранный фильм находится по оригинальному названию
|
||||
|
||||
- **GIVEN** план с `title` «Тёмный рыцарь», `original_title` «The Dark Knight», год 2008
|
||||
- **WHEN** выполняется сверка с базой
|
||||
- **THEN** первый запрос идёт по «The Dark Knight»
|
||||
- **AND** при единичном сильном матче дальнейшие запросы (по `title`, `provider_hint`) не выполняются
|
||||
|
||||
#### Scenario: Фолбэк на локализованное название
|
||||
|
||||
- **GIVEN** план, для которого запрос по `original_title` не дал единичного сильного матча
|
||||
- **WHEN** продолжается сверка
|
||||
- **THEN** выполняется запрос по локализованному `title`
|
||||
- **AND** при отсутствии матча и там — запрос по `provider_hint`
|
||||
|
||||
#### Scenario: Дублирующий запрос пропускается
|
||||
|
||||
- **GIVEN** план, у которого `original_title` нормализованно совпадает с `title`
|
||||
- **WHEN** выполняется сверка
|
||||
- **THEN** база запрашивается этим названием один раз, повторный заход по `title` не делается
|
||||
|
||||
### Requirement: Контракт LLM на оригинальное и локализованное названия
|
||||
|
||||
Промпт распознавания SHALL требовать от модели всегда заполнять и `title`, и
|
||||
`original_title`. Если отдельного оригинального названия нет или контент
|
||||
российского происхождения, модель SHALL дублировать `title` в
|
||||
`original_title`. При неуверенности в оригинальном названии модель SHALL
|
||||
дублировать `title`, а не выдумывать название (защита от ложного авто-матча).
|
||||
|
||||
Разбор ответа SHALL оставаться устойчивым к пустому `original_title`: пустое
|
||||
значение не отбраковывается и не вызывает correction-ретрай; сверка
|
||||
gracefully использует доступные названия.
|
||||
|
||||
#### Scenario: Российский фильм — дублирование
|
||||
|
||||
- **GIVEN** раздача российского фильма без отдельного оригинального названия
|
||||
- **WHEN** модель возвращает план
|
||||
- **THEN** `title` и `original_title` заполнены одинаковым каноническим названием
|
||||
|
||||
#### Scenario: Пустой original_title не ломает разбор
|
||||
|
||||
- **GIVEN** ответ модели с пустым `original_title`
|
||||
- **WHEN** план разбирается
|
||||
- **THEN** разбор успешен без correction-ретрая
|
||||
- **AND** сверка использует `title` (и `provider_hint`)
|
||||
|
||||
### Requirement: Локаль запроса к TMDB
|
||||
|
||||
Запрос поиска к TMDB SHALL передавать параметр `language`, по умолчанию
|
||||
`ru-RU`, со значением, настраиваемым конфигом `[metadata.tmdb].language`.
|
||||
Это влияет только на локализованное поле `Title`/`Name`; поле
|
||||
`original_title`/`original_name` остаётся на языке оригинала, поэтому
|
||||
оригинальная сторона сравнения не затрагивается.
|
||||
|
||||
#### Scenario: Локализованный заголовок приходит по-русски
|
||||
|
||||
- **GIVEN** TMDB включён, `language` не задан в конфиге
|
||||
- **WHEN** выполняется поиск фильма с русской локализацией
|
||||
- **THEN** запрос содержит `language=ru-RU`
|
||||
- **AND** в кандидате `Title` приходит на русском, а `OriginalTitle` — на языке оригинала
|
||||
|
||||
### Requirement: Нормализация названий при сравнении
|
||||
|
||||
Нормализация названий для гейта сильного матча SHALL сводить букву `ё` к `е`,
|
||||
чтобы написания, различающиеся только `ё`/`е`, считались одним названием.
|
||||
|
||||
#### Scenario: «Тёмный» и «Темный» совпадают
|
||||
|
||||
- **GIVEN** план с названием «Тёмный рыцарь» и кандидат базы «Темный рыцарь»
|
||||
- **WHEN** сравниваются нормализованные названия
|
||||
- **THEN** они считаются совпадающими
|
||||
@@ -0,0 +1,32 @@
|
||||
## 1. Конфигурация TMDB language
|
||||
|
||||
- [x] 1.1 Добавить поле `language` в конфиг TMDB (`[metadata.tmdb].language`), дефолт `ru-RU`; проброс в клиент TMDB
|
||||
- [x] 1.2 Безопасный дефолт `ru-RU` при пустой локали (в `Default()` и фолбэком в клиенте — отдельная валидация-реджект не нужна); обновить `config.example.toml`
|
||||
|
||||
## 2. Локаль в TMDB-клиенте
|
||||
|
||||
- [x] 2.1 В `internal/metadata/tmdb.go` передавать `language` в `Search` (`params.Set("language", ...)`)
|
||||
- [x] 2.2 Тест: запрос содержит `language=ru-RU`; `OriginalTitle` не зависит от локали
|
||||
|
||||
## 3. Нормализация названий
|
||||
|
||||
- [x] 3.1 В `internal/recognize/metadata.go` `normalize` сводить `ё`→`е`
|
||||
- [x] 3.2 Тест: «Тёмный рыцарь» и «Темный рыцарь» нормализуются одинаково
|
||||
|
||||
## 4. Многозапросный поиск в matchMetadata
|
||||
|
||||
- [x] 4.1 Сформировать упорядоченный список ключей `[original_title, title, provider_hint]`, отбросив пустые и нормализованные дубли
|
||||
- [x] 4.2 Перебирать ключи: для каждого — поиск по всем провайдерам, ранний стоп на первом единичном сильном матче (`strongMatches`)
|
||||
- [x] 4.3 Кандидатов для review копить из всех выполненных заходов (дедуп по `provider:id`, потолок `maxCandidates`)
|
||||
- [x] 4.4 Тесты: матч по original при пустом совпадении по title; фолбэк на title; фолбэк на provider_hint; пропуск дубль-ключа
|
||||
|
||||
## 5. Контракт LLM на названия (промпт)
|
||||
|
||||
- [x] 5.1 В `internal/recognize/prompt.go` усилить правила: всегда заполнять `title` и `original_title`; дублировать при отсутствии оригинала / российском происхождении; при неуверенности дублировать, не выдумывать
|
||||
- [x] 5.2 Убедиться, что `parsePlan` остаётся мягким к пустому `original_title` (нет отбраковки/лишнего correction-ретрая); тест на graceful-фолбэк
|
||||
|
||||
## 6. Синхронизация спеки и проверка
|
||||
|
||||
- [x] 6.1 Синхронизировать `docs/specs/recognition.md` с дельтой (конвейер сверки, контракт названий, локаль TMDB, нормализация)
|
||||
- [x] 6.2 `task test` и `task lint` зелёные
|
||||
- [x] 6.3 `openspec validate recognition-multi-title-match --strict` проходит
|
||||
Reference in New Issue
Block a user