- новое поле [general].language (ru|en, дефолт en) — единый источник языка локализованного title детектора и локали запросов к метабазам; original_title всегда на языке оригинала - локаль TMDB (поиск + credits) выводится из него, [metadata.tmdb].language убран - промпт LLM явно задаёт язык title с fallback на оригинал
9.1 KiB
Context
Язык локализованных полей вывода (title, режиссёр) сейчас задаётся неявно и в
двух местах: промпт LLM говорит модели вернуть «каноническое название» без
указания языка (модель решает сама), а клиент TMDB жёстко берёт локаль из
[metadata.tmdb].language с дефолтом ru-RU. Это два несогласованных рычага
для одного решения. Требуется один явный переключатель, единый для детектора и
метабаз.
Затрагиваемые пакеты: internal/config (новое поле), internal/recognize
(промпт), internal/metadata (TMDB-клиент), проброс из cmd/jellybit в
recognizer и TMDB. Конвенция конфига — docs/conventions/config.md
(самодокументируемый config.example.toml, валидация на старте).
Goals / Non-Goals
Goals:
- Одно top-level поле
language(ru|en, дефолтen) — единственный источник языка локализованного вывода. titleот LLM и локаль TMDB (поиск + credits) следуют этому полю.original_titleне затрагивается ни при каком значении.
Non-Goals:
- Гарантированная локализация имён режиссёров (провайдеры их почти не переводят — best-effort).
- Языки помимо
ru/en, автоопределение языка по контенту, per-раздача override. - Локаль TVDB/TVMaze: TVDB extended не параметризуем языком в текущем клиенте, TVMaze — англоязычный; вне охвата этого изменения (охват — LLM и TMDB).
Decisions
1. Место конфига — [general].language, значения ru|en, дефолт en.
Поле кросс-каттинг (и recognition, и metadata-match) и по классу — это
presentation-настройка отображения, ровно как уже живущий в [general]
timezone («таймзона отображения времени»). Селим рядом с ним — единый дом
таких настроек, не плодим второй паттерн места. Значения — короткие языковые
коды, а не локали (ru-RU): локаль — диалект конкретного провайдера, его
выводит сам провайдер (см. решение 2). Значение хранится голой строкой, как
остальные enum-подобные поля конфига (llm.type, log.format) — проект
осознанно не типизирует их. Валидация: значение вне ru/en → ошибка старта
(fail-fast, по образцу llm.type, а не permissive log.level), форма
сообщения — unsupported language %q (supported: ru, en). Нормализацию «пусто →
en» держим НЕ в validate() (он чист — только собирает ошибки), а в аксессоре
Config по образцу DisplayLocation(); Default() при этом сеет "en".
Альтернативы отклонены: [recognition].language назвала бы поле по одному из
двух потребителей; top-level рядом с timezone не согласуется с уже сложившимся
домом presentation-настроек.
2. [metadata.tmdb].language удаляется; локаль выводит провайдер. В общий
слой (config/cmd) едет абстрактный ru/en; диалект TMDB ru-RU/en-US
выводит сам internal/metadata/tmdb.go — тотальным switch с default-веткой
en-US (defense in depth: невозможный вход не роняет запрос без локали). Так
знание диалекта принадлежит тому, кто на нём говорит: второй локализуемый
провайдер (TVDB с иным синтаксисом локали) добавит свой маппинг у себя, а не
расширит общий слой. Остаточный tmdbDefaultLanguage = "ru-RU" и его fallback в
NewTMDB удаляются — это скрытый второй дефолт, противоречащий заявленному
en-US. Второго способа задать язык не остаётся (нет рассинхрона). Ломающее
изменение конфига переворачивает дефолт локали TMDB ru-RU→en-US; фиксируем в
proposal и миграции. Альтернатива «оставить [metadata.tmdb].language как
override» отклонена: две ручки для одного решения противоречат «второго способа
быть не должно».
3. Директива языка в промпте — только для title, с fallback на оригинал.
Добавляем в промпт распознавания явную строку: вернуть title на языке
language, при отсутствии перевода — на языке оригинала (не выдумывать).
Семантически совпадает с поведением TMDB language (локализованное поле с
fallback на оригинал), поэтому обе стороны согласованы. Язык прокидывается в
buildMessages/systemPrompt из конфига через Recognizer.
4. Режиссёр — та же локаль в запрос credits, best-effort. Клиент TMDB уже
передаёт language в поиск; тем же значением параметризуем запрос credits
(Director). Имена людей TMDB локализует не всегда — где перевода нет, приходит
оригинал; это не ошибка и не проваливает выборку (best-effort уже закреплён
спекой metadata-match).
Risks / Trade-offs
- [Имена режиссёров редко локализованы у провайдеров] → best-effort по спеке: при отсутствии перевода — оригинал; ожидание зафиксировано в proposal, отказа не вызывает.
- [Ломающее изменение конфига: удаление
[metadata.tmdb].language+ флип дефолта наen-US] → go-toml молча игнорирует неизвестный ключ, поэтому деплой с оставленным[metadata.tmdb].language = "ru-RU"тихо переключится на английский. Решение: НЕ вводим отклонение неизвестных ключей (лишняя машинерия ради единственного деплоя), а прописываем обязательный ручной шаг в миграции ниже. Существующие «русские» деплои выставляют[general].language = "ru"и убирают старый ключ. - [Дефолт
enпереименует имена папок медиатеки на английский] → косметика: Jellyfin идентифицирует контент по id-тегу метабазы (TVDB/IMDb в имени папки), а не по языку названия, поэтому распознавание не ломается.original_title(ось поиска в базах) отlanguageне зависит. Осознанный дефолт. - [LLM может проигнорировать языковую директиву на слабой модели] → директива
best-effort, как и весь недоверенный вывод LLM; безопасность по-прежнему на
валидации и гейте матча, не на языке
title.
Migration Plan
- Выкатка бинаря с новым полем; дефолт
enактивен сразу. - Деплой, где ожидались русские названия, добавляет
[general].language = "ru"в конфиг и удаляет[metadata.tmdb].language(обязательный ручной шаг — валидатор про удалённый ключ не предупреждает). - Откат — вернуть прежний бинарь;
languageв конфиге игнорируется старой версией (unknown-поле go-toml не роняет парсинг),[metadata.tmdb].languageпри откате нужно вернуть, если он был.