Animación de Subtítulos
Convierte una transcripción o locución en subtítulos con sincronización 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. Construye el tipo de subtítulos que impulsan el tiempo de visualización en TikTok, Reels y Shorts: legibles, al ritmo y dentro del área segura.
Cuándo usar
- Subtítulos palabra por palabra o karaoke en video vertical de formato corto (9:16).
- Incrustar subtítulos abiertos sobre 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 generados automáticamente en una revelación de marca "estilo Hormozi".
El pipeline
| Etapa | Trabajo | Herramienta |
|---|---|---|
| Transcribir | Audio → marcas de tiempo a nivel de palabra | Whisper (@remotion/install-whisper-cpp), AssemblyAI |
| Normalizar | → tokens { text, startMs, endMs }[] | Tipo Caption de @remotion/captions |
| Paginar | Agrupar palabras en "páginas" de 1 a 4 palabras | createTikTokStyleCaptions() |
| Animar | Entrada por palabra + resaltado de palabra activa | spring() de Remotion / CSS |
| Colocar | Área segura, tipografía legible | reglas de diseño a continuación |
| Exportar | Incrustación (MP4) y/o SRT/VTT externo | Renderizado de Remotion / emisión de archivo |
Dos reglas no negociables
- Marcas de tiempo a nivel de palabra, no a nivel de línea. El karaoke solo funciona como magia cuando cada palabra cae en la sílaba. Siempre transcribe a marcas de tiempo de palabra; nunca las falsifiques dividiendo una línea de manera uniforme en su duración: la deriva es instantáneamente visible.
- La legibilidad supera al estilo. Los subtítulos se leen en un teléfono, con luz solar, en silencio. Sans-serif negrita, trazo grueso o sombra, alto contraste primero; decoración después. Un subtítulo que nadie puede leer es decoración, no un subtítulo.
Marca de tiempo de palabra a partir de una transcripción
Whisper emite marcas de tiempo a nivel de palabra directamente. Normaliza cada fuente en una forma de token plana única para que el renderizador nunca se preocupe de dónde vinieron las palabras:
// Token de subtítulo: la única forma que todo el pipeline consume
```ts
// Token de subtítulo: la única forma que todo el pipeline consume
type Token = { text: string; startMs: number; endMs: number };
// Analiza un bloque de referencia SRT "00:00:01,200 --> 00:00:01,640" en 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 referencias a nivel de oración, vuelve a transcribir para obtener marcas de tiempo de palabra; no interpoles. El modelo medium.en de Whisper es aproximadamente 2x más rápido que large y lo suficientemente preciso para audio de voz limpio.
Entrada por palabra (Remotion)
Impulsa la escala y opacidad de cada palabra con un resorte anclado a su propio startMs. El microexceso 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; // cuadro 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]); // aparece de 60% → 100% (con exceso)
return (
<span style={{
display: "inline-block",
transform: `scale(${scale})`,
opacity: interpolate(p, [0, 1], [0, 1]),
color: active ? "#FFE45E" : "#FFFFFF", // resaltado de palabra activa
transition: "color 80ms linear",
}}>{token.text} </span>
);
};
Calcula active probando frame contra el [startMs, endMs] de cada token. Mantén el resaltado como un solo color de acento o una "caja" rellena detrás de la palabra viva; 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–500 ms para verdadero palabra por palabra, ~1000–1500 ms para frases cortas legibles.
import { createTikTokStyleCaptions } from "@remotion/captions";
const { pages } = createTikTokStyleCaptions({
captions, combineTokensWithinMilliseconds: 1200, // ~2–3 palabras por página
});
Entrada en 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–80 px (≈8% de la altura del cuadro), mínimo 45 px | Legible en silencio en un teléfono |
| Trazo | 2–6 px de contorno negro sólido | Sobrevive a cualquier fondo |
| Sombra | Sombra suave como respaldo al trazo | Separación en cuadros brillantes |
| Mayúsculas | Mayúsculas o oración; relleno de alto contraste | Impacto + escaneabilidad |
| Posición vertical | Banda central, ~62–70% hacia abajo | Por encima de la interfaz de usuario, por debajo de la acción |
| Seguridad inferior | Mantener despejado ~280 px / 15% inferiores | Evita interfaz de subtítulos/CTA/audio |
| Seguridad lateral | Mantener dentro del 80% central del ancho | Evita iconos del carril derecho |
Coloca los subtítulos en la banda central, no fijados al fondo, donde vive la interfaz de usuario de la plataforma (nombre de usuario, audio, botones). Acolchona el texto a un máximo de ~2 líneas.
Sincronizar 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 tiempo del audio: los subtítulos y la voz permanecen bloqueados sin ajuste manual. Si se vuelve a grabar la VO, vuelve a transcribir; nunca desplaces compensaciones manualmente.
Incrustación vs. SRT/VTT externo
| Incrustación (abiertos) | Externo SRT/VTT (cerrados) | |
|---|---|---|
| Dónde | Píxeles en el MP4 | Archivo .srt/.vtt separado |
| Redes sociales (TikTok/Reels) | Requerido: garantizado, personalizable | A menudo ignorado por la plataforma |
| Accesibilidad (ADA/WCAG) | No satisface por sí solo | Requerido (conmutable) |
| Mejor práctica | Incrustar subtítulos animados para redes sociales | También enviar un archivo externo para web/SEO |
Emite 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 falsificación de división uniforme.
- Entrada por palabra con microexceso; un color de acento para la palabra activa.
- 1 a 4 palabras por página; nunca un muro de texto.
- Sans-serif negrita, 56–80 px, trazo de 2–6 px + sombra, alto contraste.
- Banda central, despejada ~280 px inferiores y carriles laterales.
- Audio colocado debajo de la composición; marcas de tiempo en la base de tiempo del audio.
- Enviar incrustación para redes sociales y un archivo SRT/VTT externo para accesibilidad.
Entregar y verificar (fotogramas renderizados → MP4)
Asistente empaquetado (
scripts/): junta tus fotogramas conscripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.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> + esquema zod + defaultProps): todo el movimiento de palabras se controla por fotogramas usando useCurrentFrame(), nunca Date.now() / Math.random() / temporizadores. Entregable = out/*.mp4 (incrustados) + el proyecto + el SRT/VTT externo. El vertical 9:16 (1080×1920) es el predeterminado.
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 realizar una codificación.
# Fotogramas al inicio / un fotograma de palabra activa muestreado / final — CON PROPIEDADES ENTREGADAS (tokens reales + audio)
npx remotion still Captions out/f-start.png --frame=0 --props='{"captionsSrc":"vo.json"}'
npx remotion still Captions out/f-mid.png --frame=90 --props='{"captionsSrc":"vo.json"}'
npx remotion still Captions out/f-end.png --frame=N --props='{"captionsSrc":"vo.json"}' # N = duraciónEnFotogramas-1
# Inspeccionar cada PNG:
# - la palabra resaltada en el fotograma 90 es la palabra cuyo [startMs,endMs] contiene 90/fps (sin deriva)
# - incrustación legible: sans-serif negrita, trazo+sombra se mantiene, sin recortes
# - área segura 9:16: el subtítulo se sienta en la banda central, despejado del ~12% superior y del ~20-35% inferior (subtítulos/CTA/interfaz de 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 README
Usa npx remotion compositions para leer duraciónEnFotogramas/fps y elegir los fotogramas de palabra activa y final.
Antes de terminar:
- Los fotogramas se renderizan limpiamente en el fotograma 0, un fotograma medio de palabra activa y el último, sin falta de fuente/audio.
- La palabra correcta está resaltada en el fotograma muestreado (fotograma/fps cae dentro de su token); sin falsificación 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 controlado por fotogramas: sin
Date.now()/Math.random()/ temporizadores. - Las propiedades entregadas (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 de extremo a extremo: transcripción de Whisper y el tipoCaption, un analizador SRT completo, componente de Remotion con sincronización de palabras y resaltado activo, paginación manual, un emisor SRT/VTT, mapas de área segura por plataforma y una hoja de especificaciones de tipografía legible.