🎙️ TTS Kokoro local (voz pf_dora)
Narração em PT-BR gerada 100% na máquina, gratuita e sem chave de API — o que mantém a skill auto-contida.
Kokoro é um TTS ONNX que roda local (pip install kokoro-onnx soundfile), com a voz pf_dora (PT-BR) e --speed 0.98. A primeira execução baixa ~340MB do modelo; depois é offline. Nenhuma chave de API, nenhum custo por minuto.
npx -y hyperframes tts "assets/txt/s1.txt" \
--voice pf_dora \
--speed 0.98 \
--output "assets/audio/s1.wav"
"512" → "quinhentos e doze"; "URL" → soletre; "automationsai.net" → "inema ponto club". O TTS lê literal — escrever por extenso evita leituras estranhas.
A voz é boa, mas sem atuação. Sempre peça ao usuário para validar a locução — ele é quem ouve antes do render final.
📝 Gerar os WAVs a partir do steps.json
O narration-template.sh extrai as falas do steps.json e gera um WAV por passo + CTA.
# extrai steps[].narration + ctaNarration -> assets/txt/sN.txt
node -e '
const d=JSON.parse(fs.readFileSync("steps.json","utf8"));
const lines=d.steps.map(s=>s.narration||"");
lines.push(d.ctaNarration);
lines.forEach((t,i)=>fs.writeFileSync(`assets/txt/s${i+1}.txt`, t));
'
# gera 1 WAV por txt (passos + CTA)
for i in $(seq 1 "$N"); do
npx -y hyperframes tts "assets/txt/s$i.txt" \
--voice pf_dora --speed 0.98 \
--output "assets/audio/s$i.wav"
done
- ✓Escrever as falas no próprio
steps.json - ✓Garantir 1 narração por passo + a
ctaNarration - ✓Rodar na raiz do projeto (onde está steps.json e assets/)
- ✗Manter o texto da fala separado do steps.json (vira divergência)
- ✗Esquecer a CTA — ela é o último WAV
- ✗Numerar errado: os WAVs precisam ser s1..sN na ordem dos passos
📏 Medir a duração real dos WAVs
O timing é fonte única: o gerador mede cada WAV com ffprobe — não há array de tempos digitado à mão.
const NA = STEPS.length + 1; // +1 da CTA
const AUDIO = [];
for (let i = 1; i <= NA; i++) {
const d = parseFloat(execSync(
`ffprobe -v error -show_entries format=duration \
-of default=noprint_wrappers=1:nokey=1 \
"assets/audio/s${i}.wav"`).toString().trim());
AUDIO.push(d); // duração REAL, não estimada
}
Uma fala de "10 segundos" raramente dura exatos 10s. Medir o WAV real e derivar o tempo das cenas dali é o que garante que a animação termine junto com a voz, sem ajuste manual.
Em ambientes git-bash, use ffmpeg -nostdin nas chamadas para extrair frames — sem a flag o processo pode travar lendo stdin interativo.
⚙️ A composição: build-demo.mjs
O gerador central: lê o steps.json, mede os WAVs e produz o index.html com moldura, cursor, destaque, zoom e CTA prontos.
1. lê steps.jsonCarrega viewport, window, os passos (shot + target + caption + narration) e a ctaNarration.
2. mede WAVs + calcula geometriaMonta AUDIO[] com ffprobe e mapeia cada bbox do espaço do screenshot para o canvas (mapBox, center).
3. escreve index.html (16:9)Cenas (1 por passo) + CTA, moldura, cursor global, destaques, zoom e timeline GSAP — tudo num único HTML renderizável.
node build-demo.mjs # -> index.html (16:9), pronto para lint/render
Qualquer ajuste vai no steps.json ou no build-demo.mjs, depois rode de novo. Editar o HTML direto se perde no próximo build.
🔗 Sincronizar áudio ↔ animação pelo S[]
A timeline e os áudios leem o mesmo array de tempos derivado de AUDIO[] — sincronia por construção.
const LEAD = 0.5, TAIL = 0.7, FADE = 0.4;
let t = 0;
const S = AUDIO.map((a, i) => {
const dur = LEAD + a + TAIL;
const o = { i: i+1, start: t, dur,
audioStart: t + LEAD, audioDur: a, end: t + dur };
t += dur; return o;
});
Cenas e captions ficam em tracks alternados (1/3 e 2/4): um track termina enquanto o outro já prepara o próximo áudio, evitando sobreposição nas transições com FADE.
💬 Captions no rodapé
Cada passo tem um caption que vira legenda translúcida sincronizada com a cena.
O caption de cada passo (escrito no steps.json) é renderizado no rodapé com Inter 600 sobre fundo translúcido, entrando e saindo junto com a cena, em track alternado em relação às cenas.
A narração é o que a voz fala (pode ter números por extenso); o caption é o texto curto na tela. Os dois vivem no mesmo passo do steps.json, mas servem a propósitos diferentes — feeds mudos leem o caption.
Mantenha o caption em uma frase. Texto longo no rodapé compete com a moldura e o resultado — a legenda reforça, não substitui a fala.
📣 A CTA final do AutomationsAI
A última cena é a chamada de marca — já vem pronta no template, com narração padrão.
"CONTINUA EM" + AutomationsAI (AutomationsAI creme, .CLUB âmbar com glow) + 🌐 automationsai.net. Narração padrão: "Isso é conteúdo do AutomationsAI ponto CLUB. Acesse: inema ponto club." Na CTA, o cursor some (opacity:0).
"ctaNarration": "Isso é conteúdo do AutomationsAI ponto CLUB. Acesse: inema ponto club."
🎯 O que você aprendeu
- ✓Kokoro gera a narração PT-BR local, grátis, voz pf_dora --speed 0.98
- ✓O narration-template.sh tira as falas do steps.json (passos + CTA)
- ✓O build-demo.mjs mede os WAVs com ffprobe — timing fonte única
- ✓S[] (start/dur/audioStart/end) sincroniza áudio e animação
- ✓Captions e a CTA do AutomationsAI já vêm prontos
Próximo: Módulo 3.3 — montar o projeto HyperFrames, validar com lint/inspect e renderizar o MP4 final.