# 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` совпадает с реальным файлом торрента. #### Scenario: План сериала с per-file нумерацией - **GIVEN** сезон-пак из 10 видеофайлов - **WHEN** LLM возвращает план - **THEN** `type` = `series`, а каждый видеофайл несёт свои `season`/`episode` #### Scenario: Несуществующий src отклоняется - **GIVEN** ответ LLM, где `files[].src` не совпадает ни с одним файлом торрента - **WHEN** план разбирается - **THEN** такой план не принимается как валидный ### 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), а не к «починке» пути