Animação de Legendas
Transforme uma transcrição ou narração em legendas palavra por tempo, que param a rolagem: cada palavra aparece na sílaba, a palavra ativa é destacada e o texto permanece colado à narração. Construa o tipo de legenda que aumenta o tempo de exibição no TikTok, Reels e Shorts — legível, no ritmo e dentro da área segura.
Quando usar
- Legendas palavra por palavra ou karaokê em vídeo vertical curto (9:16).
- Inserção de legendas abertas em uma faixa de voz/narração a partir de uma transcrição.
- Transformar uma transcrição do Whisper/SRT em texto animado no Remotion ou na web.
- Reestilizar legendas automáticas em uma revelação com marca, "estilo Hormozi".
O pipeline
| Etapa | Tarefa | Ferramenta |
|---|---|---|
| Transcrever | Áudio → timestamps de nível de palavra | Whisper (@remotion/install-whisper-cpp), AssemblyAI |
| Normalizar | → tokens { text, startMs, endMs }[] | Tipo Caption do @remotion/captions |
| Paginar | Agrupar palavras em "páginas" de 1–4 palavras | createTikTokStyleCaptions() |
| Animar | Pop por palavra + destaque da palavra ativa | spring() do Remotion / CSS |
| Posicionar | Área segura, tipo legível | regras de layout abaixo |
| Exportar | Inserção (MP4) e/ou SRT/VTT lateral | Renderização do Remotion / emissão de arquivo |
Duas regras inegociáveis
- Tempo no nível da palavra, não no nível da linha. O karaokê só funciona como mágica quando cada palavra cai na sílaba. Sempre transcreva para timestamps de palavra; nunca finja dividindo uma linha igualmente por sua duração — o desvio é instantaneamente visível.
- Legibilidade supera estilo. Legendas são lidas em um telefone, sob luz do sol, sem som. Sans-serif em negrito, traço ou sombra pesados, alto contraste primeiro; decoração depois. Uma legenda que ninguém consegue ler é decoração, não uma legenda.
Tempo de palavra a partir de uma transcrição
O Whisper emite timestamps de nível de palavra diretamente. Normalize cada fonte em uma única forma de token plana para que o renderizador nunca se importe de onde vieram as palavras:
// Token de legenda — a única forma que tudo downstream consome
type Token = { text: string; startMs: number; endMs: number };
// Analisar um bloco de sinalização SRT "00:00:01,200 --> 00:00:01,640" em ms
const toMs = (t: string) => {
const [h, m, rest] = t.split(":");
const [s, ms] = rest.split(",");
return ((+h * 60 + +m) * 60 + +s) * 1000 + +ms;
};
Se existirem apenas sinalizações de nível de frase, retranscreva para tempo de palavra — não interpole. O medium.en do Whisper é ~2x mais rápido que o large e preciso o suficiente para áudio de voz limpo.
Pop-in por palavra (Remotion)
Conduza a escala e opacidade de cada palavra com uma mola ancorada em seu próprio startMs. O micro-excesso é o que faz parecer "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; // quadro de entrada da própria palavra
const p = spring({ frame: frame - enter, fps, config: { damping: 12, mass: 0.6 } });
const scale = interpolate(p, [0, 1], [0.6, 1]); // pop de 60% → 100% (excede)
return (
<span style={{
display: "inline-block",
transform: `scale(${scale})`,
opacity: interpolate(p, [0, 1], [0, 1]),
color: active ? "#FFE45E" : "#FFFFFF", // destaque da palavra ativa
transition: "color 80ms linear",
}}>{token.text} </span>
);
};
Calcule active testando frame contra [startMs, endMs] de cada token. Mantenha o destaque como uma única cor de destaque ou uma "caixa" preenchida atrás da palavra ativa — nunca anime a cor de todas as palavras ao mesmo tempo.
Páginas, não paredes de texto
Mostre de 1 a 4 palavras por vez. Use createTikTokStyleCaptions() do Remotion (ou agrupe manualmente) e ajuste combineTokensWithinMilliseconds: ~200–500ms para verdadeiro palavra por palavra, ~1000–1500ms para frases curtas legíveis.
import { createTikTokStyleCaptions } from "@remotion/captions";
const { pages } = createTikTokStyleCaptions({
captions, combineTokensWithinMilliseconds: 1200, // ~2–3 palavras por página
});
Pop Web/CSS (sem Remotion)
Para um player DOM/ao vivo, dê a cada palavra seu próprio 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); });
Tipo legível e posicionamento em área segura (9:16, 1080×1920)
| Propriedade | Valor | Por quê |
|---|---|---|
| Fonte | Sans-serif Negrito/ExtraNegrito (Montserrat, Inter, Helvetica) | Lê-se em telas pequenas e movimentadas |
| Tamanho | 56–80px (≈8% da altura do quadro), min 45px | Legível sem som em um telefone |
| Traço | 2–6px contorno preto sólido | Sobrevive a qualquer fundo |
| Sombra | Sombra suave como backup do traço | Separação em quadros claros |
| Caixa | Maiúscula ou frase; preenchimento de alto contraste | Impacto + escaneabilidade |
| Posição vertical | Faixa central, ~62–70% para baixo | Acima da UI, abaixo da ação |
| Segurança inferior | Manter longe do ~280px / 15% inferior | Evita UI de legenda/CTA/áudio |
| Segurança lateral | Manter dentro dos 80% centrais da largura | Evita ícones da barra lateral direita |
Posicione as legendas na faixa central — não fixadas na parte inferior, onde reside a UI da plataforma (nome de usuário, áudio, botões). Preencha o texto com no máximo ~2 linhas.
Sincronizar com uma narração
A transcrição já carrega o tempo do áudio exato. Coloque esse mesmo áudio sob a composição e mantenha os timestamps na base de tempo do áudio — legendas e voz permanecem travados sem ajustes manuais. Se a narração for regravada, retranscreva; nunca desloque manualmente.
Inserção vs. SRT/VTT lateral
| Inserção (aberta) | SRT/VTT lateral (fechada) | |
|---|---|---|
| Onde | Pixels no MP4 | Arquivo .srt/.vtt separado |
| Social (TikTok/Reels) | Obrigatório — garantido, estilizável | Frequentemente ignorado pela plataforma |
| Acessibilidade (ADA/WCAG) | Não satisfaz sozinho | Obrigatório (alternável) |
| Melhor prática | Grave legendas animadas para redes sociais | Também envie um lateral para a web/SEO |
Emita ambos: grave as legendas animadas para redes sociais e escreva um SRT/VTT simples a partir dos mesmos tokens para reprodução acessível/SEO.
Lista de verificação de saída
- Timestamps de nível de palavra (reais, de transcrição) — sem falsificação de divisão igual.
- Pop por palavra com micro-excesso; uma cor de destaque para a palavra ativa.
- 1–4 palavras por página; nunca uma parede de texto.
- Sans-serif em negrito, 56–80px, 2–6px traço + sombra, alto contraste.
- Faixa central, limpa do ~280px inferior e barras laterais.
- Áudio colocado sob a composição; timestamps na base de tempo do áudio.
- Envie inserção para redes sociais e um SRT/VTT lateral para acessibilidade.
Entregar e verificar (stills renderizados → MP4)
Auxiliar empacotado (
scripts/): organize seus stills comscripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png, em seguida, verifique a codificação comscripts/probe-mp4.sh out.mp4 [WxH] [fps]. Consultescripts/README.md.
As legendas são enviadas como uma composição Remotion (<Composition> + esquema zod + defaultProps) — todo movimento de palavra é baseado em quadro com useCurrentFrame(), nunca Date.now() / Math.random() / timers. Entregável = out/*.mp4 (inserido) + o projeto + o SRT/VTT lateral. 9:16 vertical (1080×1920) é o padrão.
Loop de verificação — renderizar stills → inspecionar → codificar. O tempo das palavras é a coisa que quebra; verifique-o em quadros exatos antes de gastar uma codificação.
# Stills no início / em um quadro de palavra ativa amostrado / no final — COM PROPS ENVIADOS (tokens reais + áudio)
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 = duracaoEmQuadros-1
# Inspecionar cada PNG:
# - a palavra destacada no quadro 90 é a palavra cujo [startMs,endMs] contém 90/fps (sem desvio)
# - legenda inserida legível: sans-serif em negrito, traço+sombra segura, sem recorte
# - área segura 9:16: a legenda fica na faixa central, limpa do topo ~12% e do fundo ~20-35% (UI de legendas/CTA/áudio) e da barra de ação direita
npx remotion render Captions out/captions.mp4 --props='{"captionsSrc":"vo.json"}' # codificar quando os stills estiverem corretos
npx remotion render Captions out/demo.gif --codec=gif # clipe de prova README
Use npx remotion compositions para ler durationInFrames/fps e escolher os quadros de palavra ativa e final.
Antes de finalizar:
- Os stills renderizam limpos no quadro 0, em um quadro intermediário de palavra ativa e no final — sem fontes/áudio ausentes.
- A palavra correta é destacada no quadro amostrado (quadro/fps cai dentro de seu token); sem falsificação de divisão igual.
- A legenda inserida é legível (traço+sombra) e está totalmente dentro da área segura 9:16 em cada quadro verificado.
- Apenas baseado em quadro — sem
Date.now()/Math.random()/ timers. - Props enviados (tokens reais, não apenas
defaultProps) renderizam corretamente; MP4 + SRT/VTT lateral emitidos, GIF opcional.
Arquivos de referência
references/word-timed-captions.md— construção completa: transcrição Whisper e o tipoCaption, um analisador SRT completo, componente de tempo de palavra Remotion com destaque ativo, paginação manual, um emissor SRT/VTT, mapas de área segura por plataforma e uma ficha técnica de tipo legível.