Communitygithub.com

Istok-Media-Tool/subtitle

Quema subtítulos/titlovi con estilo y legibles en un video usando un archivo ASS real y el renderizador libass de ffmpeg (no trucos de drawtext). Usa esta habilidad cada vez que el usuario quiera agregar o quemar subtítulos en un video, superposición de texto sincronizada con el habla, o cambiar fuente/tamaño/posición/saltos de línea en un video con subtítulos existentes.

¿Qué es subtitle?

Esta habilidad está diseñada para añadir subtítulos formateados y legibles a videos mediante el renderizador libass de ffmpeg, evitando las limitaciones del filtro drawtext. Es especialmente útil porque el ffmpeg por defecto en macOS (Homebrew) a menudo carece de soporte para libass, requiriendo una instalación alternativa como ffmpeg-full. La habilidad automatiza la creación de archivos ASS con estilos precisos, maneja la sincronización de tiempos y corrige problemas comunes como el solapamiento de eventos de diálogo (asegurando un breve espacio vacío entre subtítulos consecutivos para evitar parpadeo) y el desbordamiento de texto en los márgenes laterales (especialmente importante para plataformas como Instagram que recortan ligeramente el video). Además, incluye un proceso iterativo de verificación visual usando fotogramas de prueba antes de la renderización completa, y una metodología para corregir el texto de los subtítulos mediante un editor interactivo, lo que ahorra tiempo y recursos. La habilidad también aplica convenciones de puntuación validadas en proyectos reales, como eliminar puntos y comas al final de la última línea de una pantalla de subtítulos.

Compatible con~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Istok-Media-Tool/video-edit-skills/tree/HEAD/video-edit/skills/subtitle

Preguntar en tu IA favorita

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

Documentación

Quemado de subtítulos

Por qué existe esta habilidad

Lograr que los subtítulos se vean bien requirió varias rondas de correcciones en un proyecto real, cada una valiosa para no tener que aprender desde cero:

  1. El ffmpeg por defecto del sistema (Homebrew) no tiene el filtro drawtext/subtitles (no está compilado con libass/freetype) - quemar subtítulos con estilo real necesita una compilación diferente de ffmpeg, no una solución alternativa.
  2. Dividir líneas de subtítulos para equilibrar el recuento de caracteres produce disparates - un salto de línea debe ocurrir donde una persona haría una pausa natural al hablar la frase, nunca en medio de una frase.
  3. Las líneas que se extienden cerca de los bordes izquierdo/derecho del fotograma pueden ser recortadas por el propio recorte de Instagram una vez publicado - se requiere un margen real, no solo "cabe en el fotograma".
  4. El tamaño de fuente y la posición vertical son subjetivos por proyecto y vale la pena confirmarlos, pero hay que verificarlos visualmente antes de una renderización completa en cualquier caso: una comprobación de fotograma fijo detecta desbordamiento/recorte en segundos, una re-renderización completa no.
  5. Los subtítulos no deben moverse entre pantallas. La primera versión los anclaba a la PARTE INFERIOR (Alineación 2, MarginV 500), por lo que un subtítulo de 3 líneas comenzaba más arriba que uno de 2 o 1 línea, y los subtítulos que se superponían al anterior por unas centésimas de segundo eran empujados hacia arriba por la prevención de colisiones de libass durante toda su duración; el usuario veía los títulos "subiendo y bajando". Regla fija: anclar a la PARTE SUPERIOR (Alineación 8, MarginV = y de la primera línea; 1215 en 1080x1920 mantiene la línea inferior de un subtítulo de 3 líneas donde antes estaba el anclaje inferior) para que la primera línea de cada subtítulo esté en el mismo lugar y las líneas adicionales crezcan hacia abajo, y nunca permitir que dos eventos de Diálogo se superpongan en el tiempo. Además, dejar un breve momento VACÍO entre subtítulos consecutivos: terminar cada evento 0.12s (3 fotogramas a 25fps) antes de que comience el siguiente. El usuario lo pidió explícitamente: el breve espacio en blanco le indica al cerebro que el subtítulo cambió; los subtítulos consecutivos se leen como si el mismo texto parpadeara. Acortar el Final del subtítulo anterior, nunca retrasar el Inicio del siguiente (ese está sincronizado con el habla).
  6. El texto del subtítulo no tiene que ser una transcripción literal: corrige problemas de gramática/palabras (una palabra mal escuchada, una construcción extraña) y elimina o agrega palabras que el usuario pida cambiar, igual que harías al corregir un texto que alguien va a leer.

Paso 1: Asegurarse de que ffmpeg pueda quemar subtítulos

ffmpeg -hide_banner -filters 2>&1 | grep -E "subtitles|^\s*\S*\s+ass\b"

Si no hay coincidencias, el ffmpeg normal no puede hacer esto. Instala la compilación completa:

brew install ffmpeg-full

ffmpeg-full es keg-only (no está enlazado en PATH) - úsalo explícitamente para el paso de quemado de subtítulos: /opt/homebrew/opt/ffmpeg-full/bin/ffmpeg.

Problema que realmente ocurrió: instalar ffmpeg-full trajo una versión más nueva de x265 compartida y rompió el ffmpeg/ffprobe normal en PATH (error dyld "Library not loaded: libx265.*.dylib"), porque habían sido enlazados contra la versión anterior. Inmediatamente después de instalar ffmpeg-full, ejecuta:

ffmpeg -version && ffprobe -version

Si alguno da error, arréglalo con brew reinstall ffmpeg (la fórmula normal, no -full) y vuelve a verificar que ambos comandos funcionen antes de continuar - no asumas que el ffmpeg normal sobrevivió a la instalación intacto.

Paso 2: Obtener el texto exacto del subtítulo y su sincronización

Si el video provino del flujo de corte de la habilidad básica, ya conoces el texto exacto conservado por segmento y sus marcas de tiempo originales, pero una vez que los segmentos se cortan y concatenan, la sincronización se reinicia en la nueva línea de tiempo del video. Calcula la posición de cada segmento en el video FINAL sumando las duraciones reales de los clips renderizados en orden:

ffprobe -v error -show_entries format=duration -of csv=p=0 clip_01.mp4

...y acumula. Dentro de la ventana de cada segmento, ajusta ligeramente desde el inicio/fin del clip para que coincida con dónde comienza/termina realmente el habla (si los clips tienen relleno antes/después del paso de corte, réstalo) para que los subtítulos no aparezcan/desaparezcan en silencio.

Paso 3: Escribir el archivo ASS

Usa esta plantilla inicial: es el estilo exacto confirmado en un proyecto real (Inter Bold, texto blanco, caja negra opaca, posición inferior-media, márgenes laterales generosos):

[Script Info]
Title: <nombre del video> - titlovi
ScriptType: v4.00+
WrapStyle: 2
ScaledBorderAndShadow: yes
PlayResX: 1080
PlayResY: 1920

[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,Inter,70,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,3,16,0,8,120,120,1215,1

[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:00.15,0:00:05.93,Default,,0,0,0,,Zamisli univerzum koji postoji\Ntoliko dugo da su sve zvezde\Nodavno ugašene

Antes de quemar, haz que cada evento termine 0.12s antes de que comience el siguiente (establece el Final del evento anterior como Inicio del siguiente - 0.12; nunca muevas un Inicio) - ver lección 5.

Ajusta PlayResX/PlayResY para que coincidan con la resolución real del video (este ejemplo es vertical 1080x1920). WrapStyle: 2 significa que libass NO ajustará el texto automáticamente - cada salto de línea debe ser un \N explícito en el campo Texto. Esto es intencional: te obliga a elegir cada salto deliberadamente (ver Paso 4) en lugar de dejar que el renderizador adivine, y también es lo que detecta una línea desbordada como un borde recortado en lugar de una línea adicional añadida silenciosamente.

Fuente/tamaño/color/posición son valores predeterminados para EMPEZAR, no reglas fijas: confírmalos con el usuario si es un proyecto nuevo, o reutiliza sus últimos valores confirmados si ya los conoces para este usuario.

Convención de puntuación confirmada en un proyecto real

Los puntos finales Y las comas finales al final de la ÚLTIMA línea de una pantalla de subtítulos se eliminan (los subtítulos no necesitan un punto final como lo hace la prosa; se lee como desorden visual a ese tamaño). Esto se aplica incluso cuando la oración continúa en la siguiente pantalla de subtítulos: el cambio de pantalla en sí mismo ya se lee como la pausa, por lo que "svoje znanje kroz praksu," se muestra como "svoje znanje kroz praksu". El usuario lo pidió explícitamente (2026-11-05, "isto kao za tačku"). Los signos de interrogación y exclamación se mantienen. Las comas DENTRO de una pantalla (en medio de una línea, o al final de una línea que no es la última de la pantalla) se mantienen. Aplica esto por defecto en cada video, incluida la línea en el segundo idioma en subtítulos bilingües, y al texto de subtítulos que el usuario edite en el editor de planes.

Paso 4: Dividir cada línea donde una persona haría una pausa al hablar

Esta es la parte que más vale la pena hacer bien y la más fácil de hacer mal por pereza. Lee la oración como si la dijeras en voz alta y divide en los límites naturales de respiración/frase: límites de cláusula, después de un pensamiento completo, en comas que representen una pausa real al hablar. Nunca dividas porque una línea "necesita" ser más corta si el punto de pausa natural está en otro lugar: busca un punto de pausa natural MÁS CORTO en su lugar (casi siempre hay más de un punto de pausa válido en una oración más larga), no fuerces uno antinatural solo para alcanzar un objetivo de ancho.

Ejemplo concreto de una corrección real: incorrecto (equilibrado por recuento de caracteres, divide en medio de la frase):

Zamisli univerzum koji postoji toliko
dugo da su sve zvezde odavno ugasene

Correcto (divide en pausas naturales al hablar):

Zamisli univerzum koji postoji
toliko dugo da su sve zvezde
odavno ugašene

1, 2 o 3 líneas están bien: no hay un máximo fijo. Lo que importa es que cada división sea un lugar donde un hablante haría una pausa, y que cada línea quepa dentro de los márgenes (siguiente paso).

Paso 5: Verificar el ancho contra los MÁRGENES LATERALES reales, no contra el ancho bruto del fotograma

MarginL/MarginR en el estilo (120px cada uno en un fotograma de 1080 de ancho en la plantilla anterior, aproximadamente el 11% cada lado) existen porque la plataforma a la que se dirige el video (Instagram, etc.) aplica su propio recorte ligero después de la subida, y el texto demasiado cerca del borde bruto puede ser recortado por eso, no solo por el límite del fotograma. Trata PlayResX - MarginL - MarginR como el ancho utilizable real al juzgar si una línea es demasiado larga, no el ancho completo del fotograma.

Dado que WrapStyle: 2 desactiva el ajuste automático, una línea que es realmente demasiado ancha no se ajustará sola; se desbordará más allá del margen, lo que se lee como texto recortado/truncado. Detecta esto en el Paso 6, antes de que termine en una renderización completa.

Paso 6: Renderizar fotogramas de prueba antes de quemar todo el video

Elige una marca de tiempo dentro de cada uno de los 2-3 subtítulos más largos/riesgosos y renderiza solo ese fotograma:

/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg -nostdin -y -i video.mp4 -ss <t> -update 1 \
  -vf "subtitles=captions.ass" -frames:v 1 check.png -loglevel error

Revisa cada uno para detectar: texto que se desborda más allá de los márgenes, más líneas de las previstas (una señal de que una línea es más ancha de lo esperado y algo anterior necesita ser reformulado/re-dividido), y que la posición vertical se lea realmente como "inferior-media" en lugar de demasiado cerca del borde inferior o lo suficientemente alto como para cubrir la cara. Corrige y vuelve a verificar antes de continuar: este bucle es rápido (unos segundos por fotograma) y detecta exactamente los errores que una renderización completa perdería minutos reproduciendo.

Paso 7: Quemar el video final

/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg -nostdin -y -i video.mp4 \
  -vf "subtitles=captions.ass" -c:v libx264 -preset medium -crf 18 -pix_fmt yuv420p \
  -c:a copy output_titlovi.mp4 -loglevel error

El audio no se toca (-c:a copy) ya que los subtítulos son solo de video. Si la salida necesita pasar por una herramienta con un límite de tamaño de archivo, haz una copia comprimida secundaria en lugar de comprimir el original: el mismo patrón que las habilidades básica y zoom:

ffmpeg -nostdin -y -i output_titlovi.mp4 -c:v libx264 -preset slow -crf 24 -pix_fmt yuv420p -c:a aac -b:a 128k output_titlovi_share.mp4 -loglevel error

Iteración basada en comentarios

El trabajo de subtítulos tiende a pasar por varias rondas de correcciones pequeñas y específicas (tamaño de fuente, posición, una palabra incorrecta, puntuación). Cada ronda solo debe tocar lo que cambió: edita el archivo .ass directamente (es texto plano), vuelve a ejecutar la comprobación rápida de fotogramas del Paso 6 en lo que se vio afectado, y luego vuelve a quemar con el Paso 7. No es necesario rehacer los Pasos 1-2 a menos que el video subyacente haya cambiado.

Corrección de palabras incorrectas a través de la herramienta interactiva

El usuario prefiere corregir el texto de los subtítulos él mismo en la misma herramienta de edición de planes (habilidad plan-editor - el editor estándar del usuario): con el interruptor "Izmeni tekst" cada oración se vuelve editable en su lugar. Siempre que existan subtítulos, muestra esa herramienta con --ass captions.ass --durations ... (ver la habilidad plan-editor) en lugar de pedir correcciones en prosa, también cuando solo los subtítulos necesitan revisión y no se planea ningún zoom (entonces simplemente comienza cada clip en plano general y deja el plan de zoom intacto).

La respuesta lleva CAPTIONS_JSON: {"<índice de diálogo>": "línea 1\nlínea 2"} solo con las pantallas cambiadas. Aplícalo:

python3 scripts/apply_caption_edits.py captions.ass caption_edits.json captions_fixed.ass

Cambia solo el texto (la sincronización y el estilo se mantienen), convirtiendo cada línea en un salto \N explícito. Luego vuelve a ejecutar la comprobación de fotogramas del Paso 6 en las pantallas editadas (una palabra más larga puede desbordar los márgenes) y vuelve a quemar con el Paso 7.

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

Skills relacionados