Animación de Subtítulos
Convierte una transcripción o locución en subtítulos sincronizados palabra por palabra que detienen el desplazamiento: cada palabra aparece en la sílaba, la palabra activa se resalta y el texto se mantiene pegado a la narración. Crea el tipo de subtítulos que aumentan el tiempo de visualización en TikTok, Reels y Shorts: legibles, al ritmo y dentro del área segura.
Cuándo usarlo
- Subtítulos palabra por palabra o estilo karaoke en video vertical corto (9:16).
- Incrustar subtítulos abiertos en una pista de locución/narración a partir de una transcripción.
- Convertir una transcripción de Whisper/SRT en texto animado en Remotion o en la web.
- Reestilizar subtítulos automáticos en una revelación "estilo Hormozi" con marca.
El pipeline
| Etapa | Tarea | Herramienta |
|---|---|---|
| Transcripción | Audio → marcas de tiempo a nivel de palabra | Whisper (@remotion/install-whisper-cpp), AssemblyAI |
| Normalización | → tokens { text, startMs, endMs }[] | Tipo Caption de @remotion/captions |
| Paginación | Agrupar palabras en "páginas" de 1 a 4 | createTikTokStyleCaptions() |
| Animación | Aparición por palabra + resalte de palabra activa | spring() de Remotion / CSS |
| Colocación | Área segura, tipografía legible | reglas de diseño a continuación |
| Exportación | Incrustado (MP4) y/o SRT/VTT externo | Renderizado de Remotion / emisión de archivo |
Dos reglas no negociables
- Sincronización a nivel de palabra, no a nivel de línea. El karaoke se siente mágico solo cuando cada palabra cae en la sílaba. Siempre transcribe a marcas de tiempo de palabra; nunca las falsifiques dividiendo una línea uniformemente en su duración: la deriva es inmediatamente visible.
- La legibilidad supera al estilo. Los subtítulos se leen en un teléfono, con luz solar, sin volumen. Sans-serif en negrita, trazo pesado o sombra, alto contraste primero; decoración después. Un subtítulo que nadie puede leer es decoración, no un subtítulo.
Sincronización de palabras a partir de una transcripción
Whisper emite marcas de tiempo a nivel de palabra directamente. Normaliza cada fuente en una forma plana de token para que el renderizador nunca se preocupe de dónde vinieron las palabras:
// Token de subtítulo: la forma única que consume todo el flujo descendente
type Token = { text: string; startMs: number; endMs: number };
// Analizar un bloque de señal SRT "00:00:01,200 --> 00:00:01,640" a ms
const toMs = (t: string) => {
const [h, m, rest] = t.split(":");
const [s, ms] = rest.split(",");
return ((+h * 60 + +m) * 60 + +s) * 1000 + +ms;
};
Si solo existen señales a nivel de oración, vuelve a transcribir para obtener marcas de tiempo de palabra — no interpoles. El modelo medium.en de Whisper es ~2 veces más rápido que large y suficientemente preciso para audio de voz limpio.
Aparición palabra por palabra (Remotion)
Impulsa la escala y opacidad de cada palabra con un resorte anclado a su propio startMs. El micro-sobreimpulso es lo que lo hace sentir "vivo".
import { useCurrentFrame, useVideoConfig, spring, interpolate } from "remotion";
const Word: React.FC<{ token: Token; active: boolean }> = ({ token, active }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const enter = (token.startMs / 1000) * fps; // fotograma de entrada propio de la palabra
const p = spring({ frame: frame - enter, fps, config: { damping: 12, mass: 0.6 } });
const scale = interpolate(p, [0, 1], [0.6, 1]); // aparecer desde 60% → 100% (sobreimpulsa)
return (
<span style={{
display: "inline-block",
transform: `scale(${scale})`,
opacity: interpolate(p, [0, 1], [0, 1]),
color: active ? "#FFE45E" : "#FFFFFF", // resalte de palabra activa
transition: "color 80ms linear",
}}>{token.text} </span>
);
};
Calcula active probando frame contra [startMs, endMs] de cada token. Mantén el resalte en un solo color de acento o en un "recuadro" relleno detrás de la palabra activa — nunca animes el color de todas las palabras a la vez.
Páginas, no muros de texto
Muestra de 1 a 4 palabras a la vez. Usa createTikTokStyleCaptions() de Remotion (o agrupa manualmente) y ajusta combineTokensWithinMilliseconds: ~200–500ms para palabra por palabra real, ~1000–1500ms para frases cortas legibles.
import { createTikTokStyleCaptions } from "@remotion/captions";
const { pages } = createTikTokStyleCaptions({
captions, combineTokensWithinMilliseconds: 1200, // ~2–3 palabras por página
});
Aparición Web/CSS (sin Remotion)
Para un reproductor DOM/en vivo, asigna a cada palabra su propio animation-delay igual a startMs:
.word { display:inline-block; opacity:0; animation: pop .26s cubic-bezier(.34,1.56,.64,1) forwards; }
@keyframes pop { from { opacity:0; transform:translateY(.18em) scale(.7) } to { opacity:1; transform:none } }
.word.active { color:#FFE45E; }
words.forEach(w => { const el = mk(w.text); el.style.animationDelay = `${w.startMs}ms`; track.append(el); });
Tipografía legible y colocación en área segura (9:16, 1080×1920)
| Propiedad | Valor | Por qué |
|---|---|---|
| Fuente | Sans-serif Negrita/ExtraNegrita (Montserrat, Inter, Helvetica) | Se lee en pantallas pequeñas y ocupadas |
| Tamaño | 56–80px (~8% de la altura del fotograma), mínimo 45px | Legible sin volumen en un teléfono |
| Trazo | 2–6px de contorno negro sólido | Sobrevive a cualquier fondo |
| Sombra | Sombra suave como respaldo del trazo | Separación en fotogramas brillantes |
| Mayúsculas | Mayúsculas o oracional; relleno de alto contraste | Impacto + escaneabilidad |
| Posición vertical | Banda central, ~62–70% hacia abajo | Encima de la UI, debajo de la acción |
| Seguridad inferior | Mantener libre de ~280px / 15% inferior | Evita subtítulos/CTA/UI de audio |
| Seguridad lateral | Mantener dentro del 80% central de ancho | Evita iconos de la barra lateral derecha |
Coloca los subtítulos en la banda central — no fijados al fondo, donde reside la UI de la plataforma (nombre de usuario, audio, botones). Aplica un máximo de ~2 líneas de texto.
Sincronización con una locución / narración
La transcripción ya lleva el tiempo del audio exacto. Coloca ese mismo audio debajo de la composición y mantén las marcas de tiempo en la base de tiempos del audio — los subtítulos y la voz se mantienen bloqueados sin ajustes manuales. Si la locución se regraba, vuelve a transcribir; nunca cambies los desplazamientos a mano.
Incrustado vs SRT/VTT externo
| Incrustado (abierto) | SRT/VTT externo (cerrado) | |
|---|---|---|
| Dónde | Píxeles en el MP4 | Archivo .srt/.vtt separado |
| Social (TikTok/Reels) | Obligatorio — garantizado, personalizable | A menudo ignorado por la plataforma |
| Accesibilidad (ADA/WCAG) | No satisface por sí solo | Obligatorio (conmutable) |
| Mejor práctica | Incrustar subtítulos animados para redes sociales | También enviar un archivo externo para web/SEO |
Emitir ambos: incrusta los subtítulos animados para redes sociales y escribe un SRT/VTT simple a partir de los mismos tokens para reproducción accesible/SEO.
Lista de verificación de salida
- Marcas de tiempo a nivel de palabra (reales, de transcripción) — sin falsificaciones de división uniforme.
- Aparición palabra por palabra con micro-sobreimpulso; un color de acento para la palabra activa.
- 1–4 palabras por página; nunca un muro de texto.
- Sans-serif en negrita, 56–80px, trazo de 2–6px + sombra, alto contraste.
- Banda central, libre de ~280px inferiores y barras laterales.
- Audio colocado debajo de la composición; marcas de tiempo en la base de tiempos del audio.
- Enviar incrustado para redes sociales y un SRT/VTT externo para accesibilidad.
Entregar y verificar (fotogramas renderizados → MP4)
Asistente empaquetado (
scripts/): organiza tus fotogramas conscripts/contact-sheet.sh sheet.png f-inicio.png f-medio.png f-fin.png, luego verifica la codificación conscripts/probe-mp4.sh out.mp4 [WxH] [fps]. Consultascripts/README.md.
Los subtítulos se entregan como una composición de Remotion (<Composition> + schema zod + defaultProps) — todo el movimiento de palabras es impulsado por fotogramas usando useCurrentFrame(), nunca Date.now() / Math.random() / temporizadores. Entregable = out/*.mp4 (incrustado) + el proyecto + el SRT/VTT externo. El valor predeterminado es 9:16 vertical (1080×1920).
Bucle de verificación — renderizar fotogramas → inspeccionar → codificar. La sincronización de palabras es lo que se rompe; verifícala en fotogramas exactos antes de gastar una codificación.
# Fotogramas al inicio / un fotograma de palabra activa muestreado / fin — CON PROPS ENVIADAS (tokens reales + audio)
npx remotion still Captions out/f-inicio.png --frame=0 --props='{"captionsSrc":"vo.json"}'
npx remotion still Captions out/f-medio.png --frame=90 --props='{"captionsSrc":"vo.json"}'
npx remotion still Captions out/f-fin.png --frame=N --props='{"captionsSrc":"vo.json"}' # N = durationInFrames-1
# Inspeccionar cada PNG:
# - la palabra resaltada en el fotograma 90 es la palabra cuyo [startMs,endMs] contiene 90/fps (sin deriva)
# - incrustado legible: sans-serif negrita, trazo+sombra se mantiene, sin recorte
# - área segura 9:16: el subtítulo se sitúa en la banda central, libre de ~12% superior y ~20-35% inferior (UI de subtítulos/CTA/audio) y del carril de acción derecho
npx remotion render Captions out/captions.mp4 --props='{"captionsSrc":"vo.json"}' # codificar una vez que los fotogramas estén bien
npx remotion render Captions out/demo.gif --codec=gif # clip de prueba para README
Usa npx remotion compositions para leer durationInFrames/fps y seleccionar la palabra activa y los fotogramas finales.
Antes de terminar:
- Los fotogramas se renderizan limpios en el fotograma 0, un fotograma de palabra activa intermedio y el último — sin errores de fuente/audio.
- La palabra correcta está resaltada en el fotograma muestreado (frame/fps cae dentro de su token); sin falsificaciones de división uniforme.
- La incrustación es legible (trazo+sombra) y el subtítulo está completamente dentro del área segura 9:16 en cada fotograma verificado.
- Solo impulsado por fotogramas — sin
Date.now()/Math.random()/ temporizadores. - Las props enviadas (tokens reales, no solo
defaultProps) se renderizan correctamente; se emiten MP4 + SRT/VTT externo, GIF opcional.
Archivos de referencia
references/word-timed-captions.md— construcción completa: transcripción con Whisper y el tipoCaption, un analizador SRT completo, componente de Remotion con sincronización de palabras y resalte activo, paginación manual, un emisor SRT/VTT, mapas de área segura por plataforma y una hoja de especificaciones de tipografía legible.