CommunityRédaction et éditiongithub.com

Subtitlarr

Traduit manuellement un élément de Subtitlarr via Claude lui-même plutôt qu'un fournisseur LLM configuré — pour les éléments qu'aucun moteur cloud ne peut gérer (refus de filtre de contenu, identifiants morts, quota épuisé) ou lorsque la qualité de traduction de Claude est spécifiquement souhaitée.

Qu'est-ce que Subtitlarr ?

Cette compétence permet à Claude d'effectuer la traduction manuelle d'éléments de sous-titres dans Subtitlarr, en contournant les fournisseurs LLM classiques (Gemini, OpenAI, etc.) via une API HTTP dédiée. Elle est conçue pour les éléments bloqués par les filtres de contenu des moteurs cloud, comme les dialogues historiques contenant des termes sensibles ou les refus liés à des identifiants expirés et quotas épuisés. Le workflow repose sur deux endpoints : l'un pour récupérer le texte source formaté avec ses indices, l'autre pour soumettre la traduction complète qui sera réassemblée et intégrée comme une traduction normale. La compétence insiste sur une architecture strictement séquentielle : un agent par élément, lancé un à la fois sans parallélisation, chaque agent traitant l'élément par lots de 250 à 350 répliques pour minimiser les erreurs d'alignement et les coûts de tokens. Un script Node.js simple permet de vérifier la parité des indices avant soumission. Les conventions de traduction incluent la fidélité aux termes historiques (comme les insultes raciales traduites sans atténuation) et le respect du registre d'époque.

Compatible avec✓Claude Code~Codex CLI~Cursor✓Gemini CLI
npx skills add https://github.com/aedis/Subtitlarr/tree/HEAD/.claude/skills/manual-translate

Installed? Explore more Rédaction et édition skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Traduction manuelle via l'API de Subtitlarr

Deux endpoints existent spécifiquement pour cela (ajoutés car le filtre de contenu de Gemini bloque les dialogues historiques/dramatiques légitimes — par exemple, un film sur l'esclavage utilisant des insultes d'époque — que Claude peut traduire fidèlement en contexte) :

  • GET /api/queue/{item_id}/manual-translation/source — résout le sous-titre source de l'élément de la manière normale et renvoie {item_id, source_language, target_language, cue_count, dialogue_text}. dialogue_text est constitué de blocs de répliques au format "<index>\n<contenu>", séparés par des lignes vides — le format exact utilisé pour les requêtes d'un vrai fournisseur.
  • POST /api/queue/{item_id}/manual-translation — corps {"translated_text": "...", "model_name": "claude-code"}. Passe la traduction par le même pipeline de réassemblage/vérification d'intégrité/avis/téléchargement/suivi en base de données que celui d'un vrai fournisseur. Enregistre engine_used="manual", model_used=<model_name> — honnêtement distinguable d'une traduction pilotée par API dans la file d'attente, l'historique et la ligne de mention intégrée dans le sous-titre lui-même.

Aucun appel LLM n'a lieu côté serveur — le serveur accepte simplement le translated_text envoyé et le réassemble sur le timing original des répliques.

Tout cela fonctionne purement via HTTP contre l'URL de base de l'instance Subtitlarr que vous recevez (dev locale, ou instance distante/NAS) — rien ne nécessite un accès au système de fichiers local ou à la base de données. Si vous travaillez sur une instance distante, confirmez que l'URL de base est accessible (un simple curl vers GET /api/run/current est un bon test de vérification) avant de commencer.

Travailler sur l'ensemble du backlog en échec

Voir EXAMPLE_PROMPT.md pour une invite réelle qui invoque ce flux de travail, ainsi que des notes sur ce qui l'a fait fonctionner.

Il n'existe pas d'endpoint dédié "lister les éléments éligibles à la traduction manuelle" — construisez la liste à partir de la file d'attente existante, purement via HTTP :

  1. Listez les éléments en échec, paginés. GET /api/queue?status=failed&page=1&page_size=100, en incrémentant page jusqu'à ce que data revienne plus court que page_size (ou vide) — total dans la réponse vous indique combien de pages attendre. Chaque ligne inclut id, title, error_message.
  2. Classez selon error_message avant de toucher à quoi que ce soit — ne traduisez pas manuellement chaque élément en échec sans discernement :
    • Contenu bloqué ("blocked its own response", "blocked this request", "prohibited content", etc.) — la cible réelle de ce flux. Rien d'autre ne les corrige ; le même contenu échouera de la même manière contre le même moteur à chaque fois.
    • Limité par taux ("rate limit hit (429)") — transitoire, PAS un problème de contenu. Réessayez normalement via POST /api/queue/run-by-ids avec les IDs des éléments, pas de traduction manuelle.
    • Erreurs d'authentification/d'identifiants (401, "le compte de service est supprimé/désactivé", etc.) — l'utilisateur doit réparer la clé API/identifiant réel ; traduire manuellement avec une clé morte n'aide pas les futures exécutions utilisant ce même moteur.
    • Tout le reste (erreurs serveur 5xx, timeouts, réponses malformées) — généralement transitoire aussi ; une simple nouvelle tentative est plus appropriée qu'une traduction manuelle.
  3. Confirmez la classification/la liste cible avec l'utilisateur avant de traduire un grand lot — surtout pour un premier passage sur une instance donnée, car "combien sont réellement bloqués pour contenu" ne peut être su qu'après les étapes 1–2.
  4. Travaillez sur la liste des éléments bloqués un par un en utilisant le flux de travail pour un seul élément ci-dessous.

Utilisez UN agent d'arrière-plan PAR ÉLÉMENT — ne regroupez jamais plusieurs éléments dans un seul agent

Ce n'est pas une préférence mineure — regrouper plusieurs éléments dans un seul agent multiplie le coût en tokens, ça ne l'économise pas. Dans une conversation d'agent, chaque appel d'outil (récupération source, chaque écriture de lot, chaque vérification) reste dans le contexte de cet agent en permanence. Si un agent traduit 5 films séquentiellement, le travail du film 2 se fait en plus du contexte accumulé du film 1 (lots, JSON source, sorties d'outils), le film 3 paie pour les films 1+2, etc. — le 5ème élément d'un lot est bien plus cher que le 1er, et le lot dans son ensemble coûte significativement plus que 5 agents indépendants. Cela a été confirmé empiriquement : deux agents de lot de 5 films ont chacun brûlé ~520K tokens (environ 100K/film, chargés en amont sur les éléments ultérieurs du lot) — un agent pour un seul élément avec un contenu de longueur comparable coûte une petite fraction de cela car il n'emporte jamais le contexte résiduel des autres éléments.

Le coût fixe "démarrage à froid" d'un agent lisant ce fichier de compétence et la section des conventions est faible en comparaison et N'est PAS une raison pour regrouper. Ne rationalisez pas le regroupement comme une optimisation de coût — ce n'en est pas une. Si on vous demande de reconsidérer cela, ne le faites pas — l'approche de regroupement a été essayée, mesurée et confirmée pire.

Donc : une fois la classification et l'étape de pilotage/confirmation (ci-dessus) terminées, créez exactement un agent d'arrière-plan par ID d'élément, que ce soit un film ou un seul épisode de série télé. Pour une série avec plusieurs épisodes en attente, créez toujours un agent par épisode (pas un agent pour toute la série) — les épisodes n'ont pas besoin du contexte des autres non plus.

Chaque invite d'agent doit être autonome — l'agent n'a aucune mémoire de cette conversation — et doit inclure :

  1. Un lien/référence à ce fichier de compétence et à son flux de travail pour un seul élément.
  2. L'ID unique de l'élément à traiter (avec titre, pour vérifier la cohérence avec ce que GET /api/queue/{id} renvoie).
  3. L'URL de base de l'instance Subtitlarr.
  4. Un rappel de découpage au sein de l'élément (~250-350 répliques par lot, ajoutées à un seul fichier local persistant plutôt qu'un fichier temporaire par lot) et de vérification de la parité des indices une fois à la fin avant de soumettre — c'est une préoccupation différente de "ne pas regrouper les éléments" et les deux règles s'appliquent ensemble. Notez également que Node.js est l'environnement d'exécution de script disponible pour l'étape de vérification (pas python3 — il n'est pas installé ; ne laissez pas l'agent perdre des tours à le découvrir par lui-même).
  5. Les conventions de traduction du projet (voir ci-dessous) si pertinentes pour le contenu (par exemple, insultes d'époque).
  6. Ce qu'il faut rapporter : succès/échec et cue_count, pour que les résultats puissent être rapprochés de la liste classifiée maîtresse.

Dispensez les agents UN PAR UN, strictement séquentiel — ne lancez jamais deux agents en parallèle

"Un agent par élément" (ci-dessus) concerne ce qui se passe dans la conversation d'un agent — pas le nombre d'agents qui s'exécutent en même temps. Ne confondez pas les deux. Même si chaque élément obtient son propre agent indépendant, les agents doivent toujours être lancés un par un, en séquence : lancez un seul agent, attendez qu'il se termine et fasse son rapport, PUIS lancez le suivant. N'émettez jamais deux appels d'outil Agent ou plus dans le même message, et n'ayez jamais plus d'un agent de traduction manuelle en vol à la fois — pas deux, pas une "petite vague contrôlée", pas dix. L'utilisateur a déclaré cela explicitement et à plusieurs reprises : un agent après l'autre, un par un, jamais en parallèle.

Concrètement : lancez l'agent de l'élément N en arrière-plan, puis arrêtez-vous et attendez sa notification d'achèvement avant de lancer l'agent de l'élément N+1. Ne pré-lancez pas plusieurs agents "pour faire avancer le pipeline" — c'est DU dispatch parallèle et c'est exactement ce qui est interdit ici, que chaque agent ne touche qu'à un seul élément en interne ou non.

Flux de travail pour un seul élément

Note sur le coût en tokens : le coût dominant ici est la surcharge d'E/S, pas la traduction elle-même. Un premier essai en direct sur un film de 1188 répliques a brûlé ~110K tokens car il écrivait la traduction de chaque lot dans un fichier temporaire et le relisait avant de combiner — 8 allers-retours supplémentaires d'écriture+lecture, plus des frais généraux de découverte de l'environnement (python3 manquant, problèmes d'interpolation de chemin). Les étapes 2–5 ci-dessous sont conçues pour éviter cela : conservez les lots dans votre propre sortie de travail, ne les faites pas passer par le système de fichiers, et ne redécouvrez pas l'environnement à chaque élément.

  1. Confirmez l'élément. GET /api/queue/{item_id} — vérifiez que status et error_message correspondent à ce que vous attendez avant de dépenser des efforts.
  2. Récupérez la source. GET /api/queue/{item_id}/manual-translation/source. Notez cue_count.
  3. Traduisez par lots d'environ 250 à 350 répliques, pas le fichier entier en un seul passage et pas des tranches de 100-150 répliques non plus — les très longs passages uniques sont ceux où les erreurs d'alignement d'indices se produisent (l'attention dérive sur une longue sortie continue), mais chaque lot comporte également une surcharge fixe (ré-énoncer le format/conventions, un aller-retour d'outil), donc des lots moins nombreux et plus grands dans cette plage de sécurité coûtent globalement moins cher. Écrivez la sortie traduite de chaque lot directement dans un seul fichier local persistant, nommé avec l'ID de cet élément (par exemple translated_<item_id>.txt dans le bloc-notes, pas un translated.txt générique) au fur et à mesure — ne créez pas de fichier temporaire séparé par lot, et ne relisez pas un lot après l'avoir écrit juste pour le revérifier isolément. Un seul fichier, ajouté, lu une fois à la fin pour le contrôle de parité ci-dessous. Le suffixe d'ID d'élément est important même si le dispatch doit être séquentiel (voir ci-dessous) : un nom de fichier générique a déjà causé l'écrasement silencieux des lots en cours d'un agent par un autre agent de traduction manuelle en cours d'exécution partageant le même répertoire de bloc-notes — nommez chaque fichier temporaire (JSON source, lots, sortie combinée) d'après l'ID de l'élément afin qu'un dérapage de dispatch ne puisse pas corrompre le travail d'un autre élément.
  4. Préservez exactement chaque index, et ne sautez pas de répliques. Chaque <index> dans la source doit apparaître exactement une fois dans votre sortie traduite, avec le même numéro d'index — reassemble() fait correspondre par index, pas par position, et une réplique que vous n'incluez pas revient à son texte en langue originale (dégradation silencieuse de la qualité, pas d'erreur). Conservez le balisage de formatage (par exemple `... italique pour les paroles de chansons/monologue intérieur) — il fait partie du contenu, pas supprimé avant le réassemblage.
  5. Vérifiez la parité des indices avant de soumettre, une fois, sur le fichier entièrement assemblé — pas par lot. Extrayez chaque ligne ^\d+$ et comparez l'ensemble résultant avec 1..cue_count. Notez que cela ne détecte que les numéros d'index manquants/dupliqués — cela ne détecte PAS un lot où les répliques ont été fusionnées ou divisées de sorte que les indices restent 1 à 1 mais le mauvais dialogue se retrouve sous un numéro donné (par exemple, fusion accidentelle de deux répliques source en un bloc de sortie déplace le contenu de chaque réplique suivante d'un, invisible pour cette vérification). Vérifiez ponctuellement le contenu réel par rapport à la source à quelques endroits (limites des lots, une réplique au milieu d'un lot) également, pas seulement le script d'indices. Utilisez cette commande Node exacte pour la vérification des indices (Node est confirmé disponible ; ne perdez pas de temps à découvrir ou contourner un python3 manquant — utilisez simplement ceci) :
    node -e "const fs=require('fs');const t=fs.readFileSync('translated.txt','utf8');const idx=[...t.matchAll(/^(\d+)$/gm)].map(m=>+m[1]);const want=new Set(Array.from({length:<CUE_COUNT>},(_,i)=>i+1));const got=new Set(idx);const missing=[...want].filter(x=>!got.has(x));const extra=idx.filter((x,i)=>idx.indexOf(x)!==i);console.log('count',idx.length,'missing',missing,'dupes',[...new Set(extra)])"
    
    Corrigez tout écart/duplicata que cela signale avant de soumettre.
  6. Soumettez. POST /api/queue/{item_id}/manual-translation avec le translated_text complet combiné (tous les lots concaténés — le endpoint accepte une soumission complète par élément, pas un appel par lot) et model_name (par défaut "claude-code" ; ne changez que si on vous demande d'attribuer le travail différemment).
  7. Confirmez. GET /api/queue/{item_id} devrait montrer status: "done", engine_used: "manual". Vérifiez éventuellement le fichier téléchargé via GET /api/debug/{movie|episode}/{bazarr_id}/subtitle?lang=<code> — la première réplique devrait être la ligne de mention se terminant par [<model_name>], et le timing de la réplique 2 devrait correspondre exactement à la première réplique réelle de la source originale.

Conventions de traduction pour ce projet

  • Insultes raciales / langage offensant d'époque : traduisez fidèlement, en préservant le registre — n'adoucissez pas et n'omettez pas. Pour l'anglais "nigger" → catalan, les directives de l'utilisateur : utilisez "negrata" (la forme péjorative) lorsqu'un personnage esclavagiste/contremaître l'utilise avec mépris, et "negre"/"negres" pour les usages neutres/descriptifs. Cela reflète la distinction de l'original anglais entre usage neutre et injurieux.
  • Registre historique/d'époque : conservez les formes d'adresse formelles, les marqueurs de dialecte et les schémas de langage d'époque (contractions, grammaire relâchée dans le dialogue des personnages esclavagisés, diction formelle chez les personnages de classe supérieure) — n'uniformisez pas tout le monde en catalan neutre moderne.
  • Paroles de chansons / spirituals (par exemple "Roll, Jordan, Roll") : traduisez le sens, pas nécessairement un équivalent chantable — ce sont des sous-titres, pas un script de doublage.

Pourquoi cela existe plutôt qu'une simple nouvelle tentative du moteur configuré

Le filtre de sécurité du contenu de Gemini bloque des catégories entières de contenu narratif légitime (violence explicite, insultes d'époque, thèmes adultes dans un contexte dramatique sérieux) qu'un humain ou Claude lisant la même scène en contexte traduirait sans problème. Réessayer le même contenu contre le même moteur à plusieurs reprises n'aide pas — c'est un jugement de politique de contenu, pas une panne transitoire — et risque l'application des mesures anti-abus du fournisseur contre la clé API. Ce flux de travail existe précisément pour cette classe d'élément bloqué, pas comme un remplacement général de la cascade de moteurs normale.

Skills associés