Files
jellybit/openspec/specs/recognition/spec.md
T
av 9aecf757e0 recognize: название из метабазы санитизируется перед попаданием в план
- чистка стоит на каждой точке входа значения метабазы в план — сборка матча,
  копия кандидата для ревью, набор закреплённых значений источника и его
  чтение: гарантия, поставленная только на запись, обходится данными,
  сохранёнными прежними версиями
- название, непригодное как имя каталога (пустое или без единой буквы и
  цифры), не подставляется — раздача уходит в review с названной причиной
- гейт подтверждения матча не сдвинут: сравнение с планом идёт по значениям
  провайдера, чистится только копия, уходящая дальше
2026-08-10 10:41:16 +03:00

283 lines
21 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 явно задавать язык локализованного `title` из глобальной
настройки `language` (`ru`|`en`, дефолт `en`): модель SHALL возвращать `title`
на выбранном языке, а при отсутствии перевода — на языке оригинала (fallback).
Языковая директива SHALL касаться ТОЛЬКО `title`; `original_title` остаётся на
языке оригинала независимо от `language` (правила дублирования выше не
меняются). `provider_hint` и структурные поля (`files[].src`, роли, нумерация)
языковой директивой не затрагиваются.
Разбор ответа 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`)
#### Scenario: title приходит на языке из настройки
- **GIVEN** `language` = `en` и иностранный фильм с известным английским названием
- **WHEN** модель возвращает план
- **THEN** промпт требовал `title` на английском
- **AND** `original_title` остаётся на языке оригинала (директива его не касается)
#### Scenario: Промпт задаёт fallback на оригинал при отсутствии перевода
- **GIVEN** `language` = `en`
- **WHEN** собирается промпт распознавания
- **THEN** промпт требует при отсутствии перевода вернуть `title` на языке
оригинала, а не выдумывать название
#### Scenario: Дефолтный язык — английский
- **GIVEN** `language` не задан в конфиге
- **WHEN** собирается промпт распознавания
- **THEN** промпт требует `title` на английском (дефолт `en`)
### 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 быть **общим для обоих источников названия** — вывода LLM и
записи метабазы. Значение, пришедшее из метабазы и заменяющее поле плана при
подтверждённом матче, SHALL проходить тот же санитайзинг перед подстановкой (см.
`metadata-match`, требование «Подтверждение матча и каноническое имя»). Второго
способа чистить **названия плана** система SHALL NOT заводить: одна реализация
обслуживает оба источника. Речь только о полях плана — очистка компонента пути
под требования файловой системы (`layout`) и очистка отображаемого ярлыка
(`naming`) остаются своими, отдельными и законными.
Санитайзинг SHALL быть **идемпотентным**: повторное применение к уже
санитизированному значению SHALL NOT изменять его. На этом свойстве стоит
утверждение, что правка не меняет путей на диске для раздач с нормальным
названием, и оно проверяется тестом, а не подразумевается.
Следствие, на которое опирается раскладка: к моменту, когда план уходит в решение
auto/review, каждое из полей `title`, `original_title`, `provider_hint` совпадает
с тем, что дал бы санитайзинг этого значения — независимо от того, пришло оно от
LLM или из метабазы.
Свёртка двойников SHALL работать потокенно (по словам, разделённым не-буквенными
символами): токен, все буквы которого принадлежат одному скрипту, система SHALL
оставлять без изменений (билингвальность реальна — кириллические названия
неприкосновенны); в токене смешанного скрипта система SHALL определять доминирующий
скрипт по числу буквенных рун и заменять буквы-меньшинство их визуальными
двойниками из доминирующего скрипта по курируемой таблице. При отсутствии
доминирующего скрипта (равенство) токен SHALL оставаться без изменений.
Санитайзинг SHALL применяться ТОЛЬКО к перечисленным человекочитаемым полям
**плана**. Применение того же санитайзинга к значениям, пришедшим из метабазы —
каноническому названию и названиям кандидатов, — заказано отдельно, см.
`metadata-match`.
`files[].src` система SHALL NOT санитизировать — эти значения обязаны совпадать с
реальными файлами торрента, и расхождение (в т.ч. homoglyph) SHALL оставаться
основанием отклонить план, а не поводом «чинить» путь. `director` система SHALL
NOT санитизировать в плане — он не участвует в пути на диске, и его очистка
применяется при рендере отображаемого имени (см. `metadata-match`).
Когда санитайзинг реально изменил значение поля, система 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), а не к
«починке» пути
#### Scenario: Название из метабазы чистится тем же санитайзингом
- **GIVEN** подтверждённый единичный матч, каноническое название которого содержит
zero-width символ и кириллический двойник внутри латинского слова
- **WHEN** каноническое название подставляется в план
- **THEN** в плане оказывается значение, прошедшее тот же санитайзинг, что и вывод
LLM
#### Scenario: После сверки с базой несанитизированных полей в плане не остаётся
- **GIVEN** план после подтверждённого матча с базой
- **WHEN** оценивается решение auto/review
- **THEN** каждое из полей `title`, `original_title`, `provider_hint` совпадает с
тем, что дал бы санитайзинг этого значения
- **AND** `director` под это требование не подпадает: он чистится при рендере
отображаемого имени
#### Scenario: Повторный санитайзинг ничего не меняет
- **GIVEN** значение, уже прошедшее санитайзинг
- **WHEN** санитайзинг применяется к нему второй раз
- **THEN** значение не изменяется