Tradução manual via API do Subtitlarr
Existem dois endpoints especificamente para isso (adicionados porque o filtro de conteúdo do Gemini bloqueia diálogos históricos/dramáticos legítimos — por exemplo, um filme sobre escravidão usando insultos historicamente precisos — que o Claude consegue traduzir fielmente no contexto):
GET /api/queue/{item_id}/manual-translation/source— resolve a legenda fonte do item da maneira normal e retorna{item_id, source_language, target_language, cue_count, dialogue_text}.dialogue_textsão blocos de falas no formato"<índice>\n<conteúdo>", separados por linhas em branco — exatamente o formato que um provedor real recebe.POST /api/queue/{item_id}/manual-translation— corpo{"translated_text": "...", "model_name": "claude-code"}. Executa a tradução através do mesmo pipeline de remontagem/verificação de integridade/aviso/ upload/rastreamento no banco de dados que a saída de um provedor real passa. Registraengine_used="manual",model_used=<model_name>— honestamente distinguível de uma tradução via API real na Fila, Histórico, e na linha de aviso incorporada na própria legenda.
Nenhuma chamada LLM ocorre no lado do servidor — o servidor apenas aceita
qualquer translated_text enviado e o remonta nos tempos originais das falas.
Tudo isso funciona puramente sobre HTTP contra a URL base da instância do
Subtitlarr que você receber (desenvolvimento local, ou uma instância remota/NAS)
— nenhum acesso ao sistema de arquivos ou banco de dados é necessário. Se estiver
trabalhando contra uma instância remota, confirme que a URL base está acessível (um
curl simples para GET /api/run/current é um bom teste de fumaça) antes de começar.
Trabalhando em todo o backlog de falhas
Veja EXAMPLE_PROMPT.md para um prompt real que invoca este fluxo de trabalho, além de notas sobre o que o fez funcionar.
Não existe um endpoint dedicado "listar itens elegíveis para tradução manual" — construa a lista a partir da listagem existente da Fila, puramente sobre HTTP:
- Liste itens com falha, paginados.
GET /api/queue?status=failed&page=1&page_size=100, incrementandopageaté quedataretorne mais curto quepage_size(ou vazio) —totalna resposta informa quantas páginas esperar. Cada linha incluiid,title,error_message. - Classifique por
error_messageantes de tocar em qualquer coisa — não traduza manualmente todo item com falha indiscriminadamente:- Conteúdo bloqueado (
"blocked its own response","blocked this request","prohibited content", etc.) — alvo real deste fluxo de trabalho. Nada mais corrige isso; o mesmo conteúdo falhará da mesma forma contra o mesmo motor sempre. - Com limite de taxa (
"rate limit hit (429)") — transitório, NÃO é um problema de conteúdo. Tente novamente estes normalmente viaPOST /api/queue/run-by-idscom os ids dos itens, não tradução manual. - Erros de autenticação/credenciais (401, "service account is deleted/disabled", etc.) — precisa que o usuário corrija a chave de API/credencial real; traduzir manualmente ignorando uma chave morta não ajuda execuções futuras usando o mesmo motor.
- Todo o resto (erros de servidor 5xx, timeouts, respostas malformadas) — geralmente transitório também; uma simples repetição é mais apropriada do que a tradução manual.
- Conteúdo bloqueado (
- Confirme a classificação/lista alvo com o usuário antes de traduzir um lote grande — especialmente para uma primeira passagem contra uma instância específica, já que "quantos estão realmente com conteúdo bloqueado" só pode ser conhecido após executar os passos 1–2.
- Trabalhe na lista de conteúdo bloqueado um item de cada vez usando o fluxo de trabalho de item único abaixo.
Use UM agente em segundo plano POR ITEM — nunca agrupe vários itens em um único agente
Isso não é uma preferência menor — agrupar vários itens em um único agente multiplica o custo de tokens, não o economiza. Dentro de uma conversa de um agente, cada chamada de ferramenta (busca de fonte, cada bloco escrito, cada execução de verificação) permanece no contexto desse agente permanentemente. Se um agente traduz 5 filmes sequencialmente, o trabalho do filme 2 acontece em cima de todo o contexto acumulado do filme 1 (blocos, JSON fonte, saída da ferramenta), o filme 3 paga pelos filmes 1+2, e assim por diante — o 5º item em um lote é muito mais caro que o 1º, e o lote como um todo custa significativamente mais do que 5 agentes independentes custariam. Isso foi confirmado empiricamente: dois agentes de lote de 5 filmes cada queimaram ~520K tokens (aproximadamente 100K/filme, concentrados nos itens posteriores do lote) — um agente de item único com conteúdo de comprimento comparável custa uma pequena fração disso porque nunca carrega contexto residual de outros itens.
O custo fixo de "inicialização a frio" de um agente ler este arquivo de habilidade e a seção de convenções é pequeno em comparação e NÃO é uma razão para agrupar. Não racionalize o agrupamento como uma otimização de custo — não é. Se for solicitado a reconsiderar isso, não o faça — a abordagem de agrupamento foi testada, medida e confirmada como pior.
Portanto: uma vez que a classificação e a etapa de piloto/confirmação (acima) estejam concluídas, crie exatamente um agente em segundo plano por id de item, seja um filme ou um único episódio de série. Para uma série com vários episódios pendentes, ainda crie um agente por episódio (não um agente para a série inteira) — os episódios também não precisam do contexto uns dos outros.
Cada prompt de agente deve ser autocontido — o agente não tem memória desta conversa — e deve incluir:
- Uma referência/linque para este arquivo de habilidade e seu fluxo de trabalho de item único.
- O id do item único para trabalhar (com título, para verificação de
sanidade contra o que
GET /api/queue/{id}retorna). - A URL base da instância do Subtitlarr.
- Um lembrete para dividir em blocos dentro do item (~250-350 falas por
bloco, anexadas a um único arquivo local em vez de um arquivo
temporário por bloco) e verificar a paridade do índice uma vez no
final antes de enviar — esta é uma preocupação diferente de "não
agrupar itens" e ambas as regras se aplicam juntas. Observe também que
Node.js é o runtime de script disponível para a etapa de verificação
(não
python3— não está instalado; não deixe o agente perder turnos descobrindo isso sozinho). - As convenções de tradução do projeto (veja abaixo) se relevantes para o conteúdo (por exemplo, insultos históricos).
- O que relatar de volta: sucesso/falha e cue_count, para que os resultados possam ser reconciliados com a lista classificada mestre.
Despache os agentes UM DE CADA VEZ, estritamente sequencial — nunca lance dois em paralelo
"Um agente por item" (acima) é sobre o que acontece dentro da conversa
de um agente — nunca sobre quantos agentes rodam ao mesmo tempo. Não
confunda as duas coisas. Mesmo que cada item tenha seu próprio agente
independente, os agentes ainda devem ser lançados um de cada vez, em
sequência: lance um único agente, espere ele terminar e relatar, ENTÃO
lance o próximo. Nunca emita duas ou mais chamadas de ferramenta Agent
na mesma mensagem, e nunca tenha mais de um agente de tradução manual em
voo ao mesmo tempo — nem dois, nem uma "pequena onda controlada", nem
dez. O usuário afirmou isso explícita e repetidamente: um agente após o
outro, um de cada vez, nunca em paralelo.
Concretamente: lance o agente do item N em segundo plano, então pare e espere sua notificação de conclusão antes de lançar o agente do item N+1. Não pré-lance vários agentes "para manter o pipeline em movimento" — isso É despacho paralelo e é exatamente o que é proibido aqui, independentemente de cada agente tocar apenas um item internamente.
Fluxo de trabalho de item único
Nota de custo de tokens: o custo dominante aqui é a sobrecarga de E/S,
não a tradução em si. Uma primeira execução ao vivo em um filme de 1188
falas queimou ~110K tokens porque escreveu a tradução de cada bloco em
um arquivo temporário e o leu de volta antes de combinar — 8
viagens de ida e volta extras de escrita+leitura, além de sobrecarga de
descoberta de ambiente (falta de python3, problemas de interpolação de
caminho). Os passos 2–5 abaixo são escritos para evitar isso: mantenha
os blocos em sua própria saída de trabalho, não os faça passar pelo
sistema de arquivos, e não redescubra o ambiente a cada item.
- Confirme o item.
GET /api/queue/{item_id}— verifique sestatuseerror_messagecorrespondem ao que você espera antes de gastar esforço. - Busque a fonte.
GET /api/queue/{item_id}/manual-translation/source. Anotecue_count. - Traduza em blocos de aproximadamente 250–350 falas, não o arquivo
inteiro em uma passada e nem fatias de 100–150 falas — passadas
únicas muito longas são onde ocorrem erros de alinhamento de índice (a
atenção se desvia em uma saída contínua longa), mas cada bloco também
carrega sobrecarga fixa (reafirmar formato/convenções, uma viagem de
ida e volta de ferramenta), então blocos maiores dentro dessa faixa
segura custam menos no geral.
Escreva a saída traduzida de cada bloco diretamente em um único
arquivo local em execução, nomeado com o id deste item (por exemplo,
translated_<item_id>.txtno scratchpad, não umtranslated.txtgenérico) conforme você avança — não crie um arquivo temporário separado por bloco, e não leia um bloco de volta após escrevê-lo apenas para re-verificá-lo isoladamente. Um arquivo, anexado, lido uma vez no final para a verificação de paridade abaixo. O sufixo de id do item importa mesmo que o despacho deva ser sequencial (veja abaixo): um nome de arquivo genérico já causou a substituição silenciosa dos blocos em andamento de um agente por outro agente de tradução manual concorrente compartilhando o mesmo diretório scratchpad — nomeie todo arquivo temporário (JSON fonte, blocos, saída combinada) após o id do item para que um deslize de despacho não possa corromper o trabalho de outro item. - Preserve cada índice exatamente e não pule falas. Cada
<índice>na fonte deve aparecer exatamente uma vez na sua saída traduzida, com o mesmo número de índice —reassemble()corresponde por índice, não por posição, e uma fala que você não inclui cai para seu texto no idioma original (degradando silenciosamente a qualidade, sem gerar erro). Mantenha a marcação de formatação (por exemplo,<i>...</i>itálico em letras de música/monólogo interno) — faz parte do conteúdo, não é removido antes da remontagem. - Verifique a paridade do índice antes de enviar, uma vez, contra o
arquivo totalmente montado — não por bloco. Extraia cada linha
^\d+$e difira o conjunto resultante contra1..cue_count. Observe que isso só detecta números de índice ausentes/duplicados — NÃO detecta um bloco onde as falas foram mescladas ou divididas de forma que os índices ainda estejam 1-a-1, mas o diálogo errado acabe sob um determinado número (por exemplo, mesclar acidentalmente duas falas fonte em um bloco de saída desloca o conteúdo de cada fala subsequente em um, invisível para esta verificação). Faça também uma verificação pontual do conteúdo real contra a fonte em alguns pontos (limites de bloco, uma fala no meio do bloco), não apenas o script de índice. Use exatamente este one-liner Node para a verificação de índice (Node está confirmado como disponível; não perca tempo descobrindo ou contornando a falta depython3— basta usar isso):
Corrija qualquer lacuna/duplicata que isso relatar antes de enviar.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)])" - Envie.
POST /api/queue/{item_id}/manual-translationcom otranslated_textcompleto combinado (todos os blocos concatenados — o endpoint aceita uma submissão completa por item, não uma chamada por bloco) emodel_name(padrão"claude-code"; altere apenas se solicitado a atribuir o trabalho de forma diferente). - Confirme.
GET /api/queue/{item_id}deve mostrarstatus: "done",engine_used: "manual". Opcionalmente, verifique o arquivo enviado viaGET /api/debug/{movie|episode}/{bazarr_id}/subtitle?lang=<código>— a primeira fala deve ser a linha de aviso terminando em[<model_name>], e o tempo da fala 2 deve corresponder exatamente à primeira fala real da fonte original.
Convenções de tradução para este projeto
- Insultos raciais / linguagem ofensiva historicamente precisa: traduza fielmente, preservando o registro — não suavize ou omita. Para o inglês "nigger" → Catalão, a orientação do usuário: use "negrata" (a forma pejorativa) onde um personagem feitor/senhor de escravos o usa com desprezo, e "negre"/"negres" para usos neutros/descritivos. Isso espelha a distinção do original em inglês entre uso neutro e insulto.
- Registro histórico/período: mantenha tratamento formal, marcadores de dialeto e padrões de fala da época (contrações, gramática omitida no diálogo de personagens escravizados, dicção formal em personagens de classe alta) — não nivele todos para um Catalão moderno neutro.
- Letras de música / espirituais (por exemplo, "Roll, Jordan, Roll"): traduza o significado, não necessariamente um equivalente cantável — estas são legendas, não um script de dublagem.
Por que isso existe em vez de apenas repetir o motor configurado
O filtro de segurança de conteúdo do Gemini bloqueia categorias inteiras de conteúdo narrativo legítimo (violência explícita, insultos históricos, temas adultos em um contexto dramático sério) que um humano ou o Claude lendo a mesma cena em contexto traduziria sem problemas. Repetir o mesmo conteúdo contra o mesmo motor repetidamente não ajuda — é um julgamento de política de conteúdo, não uma falha transitória — e arrisca a própria política de abuso do provedor contra a chave de API. Este fluxo de trabalho existe exatamente para essa classe de item travado, não como um substituto geral para a cascata normal de motores.