CommunityRedacción y edicióngithub.com

aedis/Subtitlarr

Traduce manualmente un elemento de Subtitlarr usando Claude en lugar de un proveedor LLM configurado, ideal para elementos que ningún motor en la nube puede manejar (rechazos por filtro de contenido, credenciales caducadas, cuota agotada) o cuando se desea la calidad de traducción de Claude específicamente.

¿Qué es Subtitlarr?

Esta skill de agente permite realizar traducciones manuales de subtítulos a través de la API de Subtitlarr, sin necesidad de que el servidor invoque ningún LLM. Está diseñada específicamente para elementos fallidos cuyo proveedor habitual (como Gemini) los rechaza por filtros de contenido, credenciales muertas o cuota agotada, o cuando el usuario quiere que Claude maneje la traducción directamente. El flujo de trabajo incluye listar elementos fallidos, clasificarlos por tipo de error (contenido bloqueado, límite de velocidad, error de autenticación), y traducir cada elemento bloqueado por contenido en agentes independientes secuenciales, procesando fragmentos de 250-350 señales para minimizar errores de alineación de índices. La habilidad se ejecuta puramente sobre HTTP contra cualquier instancia de Subtitlarr (local, remota o NAS) sin necesidad de acceso local al sistema de archivos o base de datos. Incluye convenciones de traducción específicas para el proyecto, como el manejo fiel de lenguaje ofensivo histórico y registro de época.

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

Installed? Explore more Redacción y edición skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Traducción manual mediante la API de Subtitlarr

Existen dos endpoints específicos para esto (añadidos porque el filtro de contenido de Gemini bloquea diálogos históricos/dramáticos legítimos — p.ej., una película sobre esclavitud que usa insultos precisos de la época — que Claude puede traducir fielmente en contexto):

  • GET /api/queue/{item_id}/manual-translation/source — resuelve el subtítulo fuente del elemento de la forma habitual y devuelve {item_id, source_language, target_language, cue_count, dialogue_text}. dialogue_text son bloques de señales en formato "<index>\n<content>", separados por líneas en blanco — el formato exacto con el que se incita a un proveedor real.
  • POST /api/queue/{item_id}/manual-translation — cuerpo {"translated_text": "...", "model_name": "claude-code"}. Ejecuta la traducción a través del mismo pipeline de reensamblado/verificación de integridad/descargo de responsabilidad/subida/seguimiento en BD que utiliza la salida de un proveedor real. Registra engine_used="manual", model_used=<model_name> — claramente distinguible de una traducción real impulsada por API en la Cola, el Historial y la línea de descargo de responsabilidad incrustada en el propio subtítulo.

No se realiza ninguna llamada LLM en el servidor — el servidor simplemente acepta el translated_text publicado y lo reensambla en la sincronización original de las señales.

Todo esto funciona puramente a través de HTTP contra la URL base de la instancia de Subtitlarr que se te proporcione (desarrollo local, o una instancia remota/NAS) — no requiere acceso local al sistema de archivos ni a la base de datos. Si trabajas contra una instancia remota, confirma que la URL base es accesible (un simple curl a GET /api/run/current es una buena prueba de humo) antes de empezar.

Trabajando con todo el backlog fallido

Consulta EXAMPLE_PROMPT.md para ver un prompt real que invoca este flujo de trabajo, junto con notas sobre lo que lo hizo funcionar.

No existe un endpoint dedicado "listar elementos elegibles para traducción manual" — construye la lista a partir del listado existente de la Cola, puramente a través de HTTP:

  1. Lista elementos fallidos, paginados. GET /api/queue?status=failed&page=1&page_size=100, incrementando page hasta que data devuelva menos elementos que page_size (o vacío) — total en la respuesta te indica cuántas páginas esperar. Cada fila incluye id, title, error_message.
  2. Clasifica por error_message antes de tocar nada — no traduzcas manualmente todos los elementos fallidos sin discriminación:
    • Contenido bloqueado ("blocked its own response", "blocked this request", "prohibited content", etc.) — el objetivo real de este flujo de trabajo. Nada más soluciona esto; el mismo contenido fallará de la misma manera contra el mismo motor cada vez.
    • Límite de velocidad ("rate limit hit (429)") — transitorio, NO es un problema de contenido. Reintenta estos normalmente mediante POST /api/queue/run-by-ids con los ids de los elementos, no con traducción manual.
    • Errores de autenticación/credenciales (401, "la cuenta de servicio está eliminada/deshabilitada", etc.) — necesita que el usuario arregle la clave/credencial API real; traducir manualmente alrededor de una clave muerta no ayuda a futuras ejecuciones que usen ese mismo motor.
    • Todo lo demás (errores de servidor 5xx, tiempos de espera, respuestas malformadas) — generalmente también transitorio; un simple reintento es más apropiado que la traducción manual.
  3. Confirma la clasificación/lista objetivo con el usuario antes de traducir un lote grande — especialmente para una primera pasada contra una instancia determinada, ya que "cuántos están realmente bloqueados por contenido" solo se puede saber después de ejecutar los pasos 1–2.
  4. Trabaja a través de la lista de bloqueados por contenido un elemento a la vez usando el flujo de trabajo de un solo elemento a continuación.

Usa UN agente en segundo plano POR ELEMENTO — nunca agrupes varios elementos en un solo agente

Esto no es una preferencia menor — agrupar varios elementos en un solo agente multiplica el costo de tokens, no lo ahorra. Dentro de la conversación de un agente, cada llamada de herramienta (obtención de fuente, cada escritura de fragmento, cada ejecución de verificación) permanece en el contexto de ese agente permanentemente. Si un agente traduce 5 películas secuencialmente, el trabajo de la película 2 ocurre sobre el contexto acumulado completo de la película 1 (fragmentos, JSON fuente, salida de herramientas), la película 3 paga por las películas 1+2, y así sucesivamente — el quinto elemento en un lote es mucho más costoso que el primero, y el lote en su conjunto cuesta significativamente más que 5 agentes independientes. Esto se confirmó empíricamente: dos agentes de lotes de 5 películas cada uno quemaron ~520K tokens (aproximadamente 100K/película, cargados principalmente en los elementos posteriores del lote) — un agente de un solo elemento que maneja contenido de longitud comparable cuesta una pequeña fracción de eso porque nunca lleva el contexto sobrante de otros elementos.

El costo fijo de "arranque en frío" de un agente que lee este archivo de skill y la sección de convenciones es pequeño en comparación y NO es una razón para agrupar. No racionalices la agrupación como una optimización de costos — no lo es. Si te piden reconsiderar esto, no lo hagas — el enfoque de agrupación se probó, se midió y se confirmó peor.

Por lo tanto: una vez que se hayan realizado la clasificación y el paso de piloto/confirmación (arriba), genera exactamente un agente en segundo plano por id de elemento, ya sea una película o un solo episodio de TV. Para una serie de TV con múltiples episodios pendientes, sigue generando un agente por episodio (no un agente para toda la serie) — los episodios tampoco necesitan el contexto de los demás.

Cada indicación de agente debe ser autocontenida — el agente no tiene memoria de esta conversación — y debe incluir:

  1. Un enlace/referencia a este archivo de skill y su flujo de trabajo de un solo elemento.
  2. El id único del elemento a trabajar (con título, para verificar cordura contra lo que devuelve GET /api/queue/{id}).
  3. La URL base de la instancia de Subtitlarr.
  4. Un recordatorio de dividir en fragmentos dentro del elemento (~250-350 señales por fragmento, añadidos a un único archivo local en ejecución en lugar de un archivo temporal por fragmento) y verificar la paridad de índices una vez al final antes de enviar — esta es una preocupación diferente de "no agrupar elementos" y ambas reglas se aplican juntas. También ten en cuenta que Node.js es el entorno de scripting disponible para el paso de verificación (no python3 — no está instalado; no dejes que el agente pierda turnos descubriendo eso por sí mismo).
  5. Las convenciones de traducción del proyecto (ver más abajo) si son relevantes para el contenido (p.ej., insultos de época).
  6. Qué informar: éxito/fracaso y cue_count, para que los resultados puedan conciliarse con la lista clasificada maestra.

Despacha agentes UNO A LA VEZ, estrictamente secuencial — nunca lances dos en paralelo

"Un agente por elemento" (arriba) se refiere a lo que sucede dentro de la conversación de un agente — nunca sobre cuántos agentes se ejecutan a la vez. No confundas las dos cosas. Incluso si cada elemento tiene su propio agente independiente, los agentes deben lanzarse uno a la vez, en secuencia: lanza un solo agente, espera a que termine e informe, y LUEGO lanza el siguiente. Nunca emitas dos o más llamadas de herramienta Agent en el mismo mensaje, y nunca tengas más de un agente de traducción manual en vuelo a la vez — ni dos, ni una "pequeña ola controlada", ni diez. El usuario ha declarado esto explícita y repetidamente: un agente después de otro, uno a la vez, nunca en paralelo.

Concretamente: lanza el agente del elemento N en segundo plano, luego detente y espera su notificación de finalización antes de lanzar el agente del elemento N+1. No pre-lances varios agentes "para mantener el pipeline en movimiento" — eso ES un despacho paralelo y es exactamente lo que está prohibido aquí, independientemente de si cada agente solo toca un elemento internamente.

Flujo de trabajo de un solo elemento

Nota sobre el costo de tokens: el costo dominante aquí es la sobrecarga de E/S, no la traducción en sí. Una primera ejecución en vivo en una película de 1188 señales quemó ~110K tokens porque escribió la traducción de cada fragmento en un archivo temporal y lo leyó de nuevo antes de combinar — 8 viajes de ida y vuelta adicionales de escritura+lectura, más sobrecarga de descubrimiento del entorno (falta de python3, problemas de interpolación de rutas). Los pasos 2–5 a continuación están escritos para evitar eso: mantén los fragmentos en tu propia salida de trabajo, no los hagas viajar de ida y vuelta a través del sistema de archivos, y no redescubras el entorno en cada elemento.

  1. Confirma el elemento. GET /api/queue/{item_id} — verifica que status y error_message coincidan con lo que esperas antes de invertir esfuerzo.
  2. Obtén la fuente. GET /api/queue/{item_id}/manual-translation/source. Anota cue_count.
  3. Traduce en fragmentos de aproximadamente 250–350 señales, no el archivo completo en una sola pasada ni tampoco porciones de 100–150 señales — las pasadas únicas muy largas son donde ocurren errores de alineación de índices (la atención se desvía en una salida continua larga), pero cada fragmento también conlleva una sobrecarga fija (reformular formato/convenciones, un viaje de ida y vuelta de herramienta), por lo que menos fragmentos/más grandes dentro de ese rango seguro cuestan menos en general. Escribe la salida traducida de cada fragmento directamente en un único archivo local en ejecución, nombrado con el id de este elemento (p.ej., translated_<item_id>.txt en el bloc de notas, no un translated.txt genérico) a medida que avanzas — no crees un archivo temporal separado por fragmento, y no leas un fragmento de nuevo después de escribirlo solo para verificarlo de forma aislada. Un solo archivo, al que se añade, y se lee una vez al final para la verificación de paridad a continuación. El sufijo del id del elemento importa incluso si el despacho debe ser secuencial (ver más abajo): un nombre de archivo genérico ya ha causado que los fragmentos en curso de un agente se sobrescriban silenciosamente por otro agente de traducción manual que se ejecuta concurrentemente y comparte el mismo directorio del bloc de notas — nombra cada archivo temporal (JSON fuente, fragmentos, salida combinada) con el id del elemento para que un error de despacho no pueda corromper el trabajo de otro elemento.
  4. Preserva cada índice exactamente, y no omitas señales. Cada <index> en la fuente debe aparecer exactamente una vez en tu salida traducida, con el mismo número de índice — reassemble() coincide por índice, no por posición, y una señal que no incluyas volverá a su texto en idioma original (degradando silenciosamente la calidad, sin generar error). Mantén el marcado de formato (p.ej., <i>...</i> cursiva en letras de canciones/monólogo interno) — es parte del contenido, no se elimina antes del reensamblado.
  5. Verifica la paridad de índices antes de enviar, una vez, contra el archivo completamente ensamblado — no por fragmento. Extrae cada línea ^\d+$ y compara el conjunto resultante con 1..cue_count. Ten en cuenta que esto solo detecta números de índice faltantes/duplicados — NO detecta un fragmento donde las señales se fusionaron o dividieron para que los índices sigan siendo 1 a 1 pero el diálogo incorrecto termina bajo un número determinado (p.ej., fusionar accidentalmente dos señales fuente en un bloque de salida desplaza el contenido de cada señal subsiguiente en uno, de forma invisible para esta verificación). También verifica el contenido real contra la fuente en algunos puntos (límites de fragmentos, una señal en medio de un fragmento), no solo el script de índices. Usa esta línea de una sola línea de Node para la verificación de índices (Node está confirmado como disponible; no pierdas tiempo descubriendo o solucionando la falta de python3 — solo usa esto):
    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)])"
    
    Corrige cualquier brecha/duplicado que esto informe antes de enviar.
  6. Envía. POST /api/queue/{item_id}/manual-translation con el translated_text completo combinado (todos los fragmentos concatenados — el endpoint acepta una presentación completa por elemento, no una llamada por fragmento) y model_name (por defecto "claude-code"; cámbialo solo si se te pide atribuir el trabajo de manera diferente).
  7. Confirma. GET /api/queue/{item_id} debería mostrar status: "done", engine_used: "manual". Opcionalmente, verifica el archivo subido mediante GET /api/debug/{movie|episode}/{bazarr_id}/subtitle?lang=<code> — la primera señal debe ser la línea de descargo de responsabilidad que termina en [<model_name>], y la sincronización de la señal 2 debe coincidir exactamente con la primera señal real de la fuente original.

Convenciones de traducción para este proyecto

  • Insultos raciales / lenguaje ofensivo preciso de época: traduce fielmente, preservando el registro — no suavices ni omitas. Para el inglés "nigger" → catalán, la guía del usuario: usa "negrata" (la forma peyorativa) cuando un personaje esclavista/capataz lo usa con desprecio, y "negre"/"negres" para usos neutrales/descriptivos. Esto refleja la distinción del original en inglés entre uso neutral e insultante.
  • Registro histórico/época: mantén el tratamiento formal, los marcadores de dialecto y los patrones de habla de la época (contracciones, gramática omitida en el diálogo de personajes esclavizados, dicción formal en personajes de clase alta) — no aplastes a todos en un catalán moderno neutral.
  • Letras de canciones / espirituales (p.ej., "Roll, Jordan, Roll"): traduce el significado, no necesariamente un equivalente cantable — son subtítulos, no un guion de doblaje.

Por qué existe esto en lugar de simplemente reintentar el motor configurado

El filtro de seguridad de contenido de Gemini bloquea categorías enteras de contenido narrativo legítimo (violencia explícita, insultos de época, temas adultos en un contexto dramático serio) que un humano o Claude leyendo la misma escena en contexto traducirían sin problema. Reintentar el mismo contenido contra el mismo motor repetidamente no ayuda — es un juicio de política de contenido, no un fallo transitorio — y corre el riesgo de que el proveedor aplique medidas contra el abuso a la clave API. Este flujo de trabajo existe exactamente para esa clase de elemento atascado, no como un reemplazo general para la cascada de motores normal.

Skills relacionados