Files
jellybit/openspec/specs/recognition/spec.md
T
avandClaude Opus 4.8 02d4ecc2aa display_name: слоистое разрешение полей + сохранение режиссёра из контекста
Единый источник полей отображаемого имени и один рендер полного ярлыка на
всех путях (старт и «Обновить имя»/авто-перелив). Раньше старт давал полный
«Название (режиссёр, год). Сезон N» но выбрасывал структуру, а перелив по
распознаванию — усечённый «Title (Year)».

- Слоистое разрешение скаляров имени: override → recognition(+match) →
  новый базовый слой «контекст» (download.parsed_context, JSON naming.Fields).
- naming: публичные Fields/Label/Derive, вынесен единый рендер; удалён
  FormatTitleYear. Сводка сезонов вынесена в recognize.SeasonSummary.
- Режиссёр из метабазы (решение A2): TMDB/TVDB credits через опциональный
  metadata.DirectorProvider; авто-матч кладёт в plan.Director, ручной выбор
  кандидата тянет credits и пиннит ovrDirector. Метабаза бьёт контекст.
- refreshDisplayNameLocked строит полный ярлык из эффективных полей;
  инфо-панель ревью показывает загруженного режиссёра.
- Миграция 0011_parsed_context + ER-схема. Всё косметика: на пути/раскладку
  не влияет, приём/вывод имени не валятся (best-effort).

Закрывает беклог-задачу «Кнопка „Обновить имя“: полный формат ярлыка».
OpenSpec: archive/2026-07-11-field-resolution-display-name (ingest,
recognition, metadata-match, review).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 11:50:08 +03:00

208 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# recognition Specification
## Purpose
Распознавание: разбор недоверенных сигналов раздачи моделью в структурированный
план (тип фильм/сериал, каноническое название и год, файлы → серии). Capability
описывает пред-парс имени, контракт и провайдер LLM со структурированным
выводом, роли файлов на краях и модель уверенности (решение auto/review). Сверка
с внешними базами метаданных — в `metadata-match`.
## Requirements
### 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: Пред-парс имени релиза
Перед вызовом LLM система SHALL выполнять дешёвый пред-парс имени торрента
(`go-ptn`): извлекать черновые название, год, сезон, серию и качество. Результат
пред-парса SHALL использоваться как вспомогательный сигнал в промпте и как
сторона проверки согласованности при решении auto/review, но НЕ SHALL считаться
итоговым распознаванием.
#### Scenario: Пред-парс даёт черновые поля
- **WHEN** на вход распознавания поступает имя релиза `Fargo.S02.2015.WEB-DL.1080p`
- **THEN** пред-парс возвращает черновые `title`, `year`, `season`, `quality`
- **AND** эти значения передаются в промпт LLM как подсказка
### Requirement: Разбор сигналов LLM в структурированный план
Система SHALL передавать LLM недоверенные сигналы (имя торрента, дерево файлов с
размерами, текстовый контекст и накопленные подсказки, пред-парс) и получать
структурированный план в схеме: `type` (`movie`|`series`), `title`,
`original_title`, `year`, `provider_hint`, `files[]` и `confidence`. Каждый
элемент `files[]` SHALL нести `src`, `role`
(`main`|`episode`|`subtitle`|`extra`|`sample`|`ignore`) и, для сериала,
per-file `season`/`episode` (отдельного скалярного `season` быть SHALL NOT — так
выражаются мультисезонные паки и спецвыпуски). План SHALL приниматься только
если каждый `files[].src` совпадает с реальным файлом торрента.
План MAY дополнительно нести опциональное скалярное поле `director` (режиссёр).
Это поле НЕ требуется от LLM и НЕ участвует в структурной валидации или гейте
авто-раскладки; его заполняют подтверждённый матч метабазы (авто) или закреплённый
в ревью выбранный источник (через override, см. `metadata-match`/`review`) как
недоверенное косметическое значение для вывода отображаемого имени. Как недоверенное
человекочитаемое поле, `director` SHALL NOT входить в plan-санитайзинг (он чистит
`title`/`original_title`/`provider_hint`); очистка режиссёра применяется при рендере
имени. Пустой `director` SHALL быть штатным (режиссёр неизвестен).
#### Scenario: План сериала с per-file нумерацией
- **GIVEN** сезон-пак из 10 видеофайлов
- **WHEN** LLM возвращает план
- **THEN** `type` = `series`, а каждый видеофайл несёт свои `season`/`episode`
#### Scenario: Несуществующий src отклоняется
- **GIVEN** ответ LLM, где `files[].src` не совпадает ни с одним файлом торрента
- **WHEN** план разбирается
- **THEN** такой план не принимается как валидный
#### Scenario: Режиссёр не требуется от LLM и не влияет на гейт
- **GIVEN** ответ LLM без поля `director`
- **WHEN** план разбирается и оценивается
- **THEN** разбор успешен, `director` пуст
- **AND** отсутствие режиссёра не влияет на структурную валидацию и решение
auto/review
### Requirement: Провайдер LLM за абстракцией со структурированным выводом
Доступ к LLM SHALL быть за интерфейсом с выбором реализации по полю `[llm].type`
(первый тип — `openai-compat`). Система SHALL запрашивать JSON-режим
(`response_format: {"type":"json_object"}`), срезать ```-ограждения и
валидировать ответ в Go против схемы плана. При ошибке разбора система SHALL
ретраить до `[llm].max_retries`, передавая модели саму ошибку и схему. Если после
ретраев ответ не разобран, задача SHALL уходить в `review` (НЕ в `failed`) с
причиной «ответ LLM не разобран».
#### Scenario: Неразобранный ответ уходит в review
- **GIVEN** LLM, чей ответ не проходит валидацию схемы после всех ретраев
- **WHEN** завершается распознавание
- **THEN** задача переходит в `review` с причиной «ответ LLM не разобран»
- **AND** задача НЕ переходит в `failed`
### Requirement: Модель уверенности и решение auto/review
Система SHALL раскладывать автоматически (без review) только при выполнении
ВСЕГО: (1) подтверждённый единичный сильный матч в базе (`metadata-match`) с
`provider_id`; (2) структурная валидация без предупреждений (фильм — ровно один
основной видеофайл; сериал — число серий бьётся с базой, нумерация S·E
консистентна); (3) согласованность пред-парса и LLM по типу/названию/году. Иначе
задача SHALL уходить в `review` с явной причиной. Самооценку LLM (`confidence`)
система SHALL учитывать лишь как вспомогательный сигнал, НЕ как единственный гейт.
#### Scenario: Нет матча в базе — всегда review
- **GIVEN** план без подтверждённого матча в базе (база выключена или матча нет)
- **WHEN** принимается решение auto/review
- **THEN** задача уходит в `review`, авто-раскладка не делается
#### Scenario: Матч и чистая валидация — авто
- **GIVEN** подтверждённый единичный матч, чистая структурная валидация и
согласованность сигналов
- **WHEN** принимается решение
- **THEN** допускается авто-раскладка (при отсутствии `force_review`)
### Requirement: Роли файлов на краях раздачи
Система SHALL относить семплы, «экстра» и мусор к роли `ignore` (эвристики размер/
имя + LLM), а внешние субтитры (`.srt`, `.ass`, пары VobSub `.idx`+`.sub`) —
привязывать к соответствующему видео. Любую неоднозначность нумерации (дыры,
дубли, спорные спецвыпуски) система SHALL эскалировать в `review`, а не разрешать
молча.
#### Scenario: Семпл помечается ignore
- **GIVEN** раздача с файлом `sample.mkv` малого размера
- **WHEN** строится план
- **THEN** этот файл получает роль `ignore` и в раскладку не попадает
### Requirement: Санитайзинг человекочитаемых полей плана
Перед структурной валидацией плана и сверкой с базами система SHALL санитизировать
человекочитаемые поля плана — `title`, `original_title`, `provider_hint` — как
недоверенный вывод LLM. Санитайзинг SHALL: (1) удалять управляющие и zero-width
символы (C0/C1, `U+200B` и родственные, BOM `U+FEFF`); (2) сводить внутренние
последовательности пробельных к одиночному пробелу и обрезать края; (3) сворачивать
кирилло-латинские homoglyph-двойники (см. ниже).
Свёртка двойников SHALL работать потокенно (по словам, разделённым не-буквенными
символами): токен, все буквы которого принадлежат одному скрипту, система SHALL
оставлять без изменений (билингвальность реальна — кириллические названия
неприкосновенны); в токене смешанного скрипта система SHALL определять доминирующий
скрипт по числу буквенных рун и заменять буквы-меньшинство их визуальными
двойниками из доминирующего скрипта по курируемой таблице. При отсутствии
доминирующего скрипта (равенство) токен SHALL оставаться без изменений.
Санитайзинг SHALL применяться ТОЛЬКО к перечисленным человекочитаемым полям.
`files[].src` система SHALL NOT санитизировать — эти значения обязаны совпадать с
реальными файлами торрента, и расхождение (в т.ч. homoglyph) SHALL оставаться
основанием отклонить план, а не поводом «чинить» путь.
Когда санитайзинг реально изменил значение поля, система SHALL логировать это на
уровне `Debug` (названия не относятся к секретам).
#### Scenario: Кириллический двойник в англоязычном названии сворачивается
- **GIVEN** план, где `title` = `Hаrold and the Purple Crayon` (буква `а` в первом
слове — кириллическая `U+0430`)
- **WHEN** план санитизируется
- **THEN** первое слово становится `Harold` (все буквы латинские)
- **AND** в запрос к базе и в сравнение уходит латинское название
#### Scenario: Честное кириллическое название не трогается
- **GIVEN** план российского фильма с `title` = `Тёмный рыцарь`, где все буквы
каждого слова кириллические
- **WHEN** план санитизируется
- **THEN** название остаётся кириллическим без замены букв
#### Scenario: Токен без доминирующего скрипта не трогается
- **GIVEN** план, где короткий токен содержит поровну латинских и кириллических
букв (доминирующего скрипта нет)
- **WHEN** план санитизируется
- **THEN** этот токен остаётся без замены букв (осознанный trade-off: двухбуквенный
homoglyph-typo не сворачивается)
#### Scenario: Zero-width и лишние пробелы вычищаются
- **GIVEN** план, где `title` содержит zero-width символ и сдвоенные пробелы
- **WHEN** план санитизируется
- **THEN** zero-width удалён, внутренние пробелы сведены к одиночным, края обрезаны
#### Scenario: files[].src не санитизируется
- **GIVEN** ответ LLM, где `files[].src` содержит символ-двойник и не совпадает ни
с одним реальным файлом торрента
- **WHEN** план обрабатывается
- **THEN** `files[].src` НЕ изменяется санитайзингом
- **AND** несовпадение src приводит к отклонению плана (эскалация в review), а не к
«починке» пути