Animação de Legendas
Transforme uma transcrição ou narração em legendas com temporização de palavra, que param a rolagem: cada palavra surge na sílaba, a palavra ativa é destacada e o texto permanece colado à narração. Crie 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 de karaokê em vídeos verticais de formato curto (9:16).
- Gravar legendas abertas em uma faixa de narração/locução a partir de uma transcrição.
- Transformar uma transcrição do Whisper/SRT em texto animado no Remotion ou na web.
- Reestilizar legendas geradas automaticamente em uma revelação com marca, estilo "Hormozi".
O pipeline
| Etapa | Tarefa | Ferramenta |
|---|---|---|
| Transcrever | Áudio → carimbos de tempo 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() |
| Animação | Pop-in por palavra + destaque da palavra ativa | spring() do Remotion / CSS |
| Posicionar | Área segura, tipo legível | regras de layout abaixo |
| Exportar | Gravação interna (MP4) e/ou SRT/VTT externo | Renderização Remotion / emissão de arquivo |
Duas regras inegociáveis
- Temporização de palavra, não de linha. O karaokê só parece mágico quando cada palavra cai na sílaba. Sempre transcreva para carimbos de tempo de palavra; nunca os invente dividindo uma linha uniformemente por sua duração — o desvio é instantaneamente visível.
- Legibilidade supera estilo. Legendas são lidas em um telefone, sob luz solar, no mudo. Sans-serif em negrito, traçado ou sombra pesados, alto contraste primeiro; decoração depois. Uma legenda que ninguém consegue ler é decoração, não legenda.
Temporização de palavra a partir de uma transcrição
O Whisper emite carimbos de tempo de palavra diretamente. Normalize cada fonte em uma forma de token única para que o renderizador nunca se importe de onde as palavras vieram:
// Token de legenda — a forma única que tudo downstream consome
type Token = { text: string; startMs: number; endMs: number };
// Analisar um bloco de cue SRT "00:00:01,200 --> 00:00:01,640" para 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 dicas de nível de frase, retranscreva para temporização 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)
Dirija a escala e opacidade de cada palavra com uma mola (spring) ancorada ao 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 acento ou uma "caixa" preenchida atrás da palavra viva — nunca anime a cor de cada palavra de uma vez.
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 em 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 | Porquê |
|---|---|---|
| Fonte | Sans-serif Bold/ExtraBold (Montserrat, Inter, Helvetica) | Lê-se em telas pequenas e ocupadas |
| Tamanho | 56–80px (≈8% da altura do quadro), mínimo 45px | Legível no mudo em um telefone |
| Traçado | Contorno preto sólido de 2–6px | Sobrevive a qualquer fundo |
| Sombra | Sombra projetada suave como backup do traçado | Separação em quadros claros |
| Caixa | Maiúsculas ou sentença; preenchimento de alto contraste | Impacto + escaneabilidade |
| Posição vertical | Faixa central, ~62–70% para baixo | Acima da UI, abaixo da ação |
| Seguro inferior | Manter longe dos ~280px / 15% inferiores | Evita UI de legenda/CTA/áudio |
| Seguro lateral | Manter dentro dos 80% centrais da largura | Evita ícones da barra lateral direita |
Coloque as legendas na faixa central — não fixadas no fundo, onde a UI da plataforma (nome de usuário, áudio, botões) reside. Preencha o texto com no máximo ~2 linhas.
Sincronizar com uma narração / locução
A transcrição já carrega a temporização do áudio exato. Coloque esse mesmo áudio sob a composição e mantenha os carimbos de tempo na base de tempo do áudio — legendas e voz permanecem travados sem ajuste manual. Se a locução for regravada, retranscreva; nunca mude os deslocamentos manualmente.
Gravação interna vs SRT/VTT externo
| Gravação interna (aberto) | SRT/VTT externo (fechado) | |
|---|---|---|
| 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 social | Também envie um sidecar para web/SEO |
Emita ambos: grave as legendas animadas para social e escreva um SRT/VTT simples a partir dos mesmos tokens para reprodução acessível/SEO.
Lista de verificação de saída
- Carimbos de tempo de palavra (reais, da transcrição) — sem falsificação de divisão uniforme.
- Pop-in por palavra com micro-excesso; uma cor de acento para a palavra ativa.
- 1–4 palavras por página; nunca uma parede de texto.
- Sans-serif em negrito, 56–80px, traçado de 2–6px + sombra, alto contraste.
- Faixa central, limpa dos ~280px inferiores e barras laterais.
- Áudio colocado sob a composição; carimbos de tempo na base de tempo do áudio.
- Envie gravação interna para social e um SRT/VTT externo para acessibilidade.
Entregar e verificar (imagens renderizadas → MP4)
Pacote auxiliar (
scripts/): organize suas imagens comscripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png, depois verifique a codificação comscripts/probe-mp4.sh out.mp4 [WxH] [fps]. Vejascripts/README.md.
As legendas são enviadas como uma composição do Remotion (<Composition> + schema zod + defaultProps) — todo movimento de palavra é orientado por quadro usando useCurrentFrame(), nunca Date.now() / Math.random() / timers. Entregável = out/*.mp4 (gravado) + o projeto + o SRT/VTT externo. O padrão é vertical 9:16 (1080×1920).
Loop de verificação — renderizar imagens → inspecionar → codificar. A temporização de palavra é o que quebra; verifique-a em quadros exatos antes de gastar tempo em uma codificação.
# Imagens no início / um quadro de palavra ativa amostrado / fim — COM PROPRIEDADES ENVIADAS (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 = durationInFrames-1
# Inspecione cada PNG:
# - a palavra destacada no quadro 90 é a palavra cujo [startMs,endMs] contém 90/fps (sem desvio)
# - gravação interna legível: sans-serif em negrito, traçado+sombra segura, sem corte
# - área segura 9:16: legenda na faixa central, limpa do topo ~12% e fundo ~20-35% (legendas/CTA/UI de áudio) e da barra de ação direita
npx remotion render Captions out/captions.mp4 --props='{"captionsSrc":"vo.json"}' # codificar uma vez que as imagens estejam corretas
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 fim.
Antes de finalizar:
- As imagens renderizam limpas no quadro 0, um quadro intermediário de palavra ativa e no último — sem fonte/áudio faltando.
- A palavra correta está destacada no quadro amostrado (frame/fps cai dentro de seu token); nenhuma falsificação de divisão uniforme.
- A gravação interna é legível (traçado+sombra) e a legenda está completamente dentro da área segura 9:16 em todos os quadros verificados.
- Orientado apenas por quadro — sem
Date.now()/Math.random()/ timers. - As propriedades enviadas (tokens reais, não apenas
defaultProps) renderizam corretamente; MP4 + SRT/VTT externo emitidos, GIF opcional.
Arquivos de referência
references/word-timed-captions.md— construção ponta a ponta: transcrição Whisper e o tipoCaption, um parser SRT completo, componente Remotion com temporização de palavra e destaque ativo, paginação manual, um emissor SRT/VTT, mapas de área segura por plataforma e uma folha de especificações de tipo legível.