🎙️ TTS Kokoro local (voz pf_dora)
Narración en PT-BR generada 100% en la máquina, gratuita y sin clave de API — lo que mantiene la skill autocontenida.
Kokoro es un TTS ONNX que se ejecuta localmente (pip install kokoro-onnx soundfile), con la voz pf_dora (PT-BR) y --speed 0.98. La primera ejecución descarga ~340MB del modelo; después funciona sin conexión. No necesitas ninguna clave de API ni pagas por minuto.
npx -y hyperframes tts "assets/txt/s1.txt" \
--voice pf_dora \
--speed 0.98 \
--output "assets/audio/s1.wav"
"512" → "quinientos doce"; "URL" → deletrea; "inema.club" → "inema punto club". El TTS lee literalmente: escribirlo con palabras evita lecturas extrañas.
La voz suena bien, pero no tiene actuación. Pídele siempre al usuario que valide la locución: él es quien la escucha antes del render final.
📝 Generar los WAV a partir de steps.json
O narration-template.sh extrae los diálogos de steps.json y genera un WAV por paso + 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
- ✓Escribir los diálogos en el propio
steps.json - ✓Garantizar 1 narración por paso + la
ctaNarration - ✓Ejecutar en la raíz del proyecto (donde están steps.json y assets/)
- ✗Mantener el texto de la voz separado del steps.json (genera divergencias)
- ✗Olvidar la CTA: es el último WAV
- ✗Numeración incorrecta: los WAV deben ser s1..sN en el orden de los pasos
📏 Medir la duración real de los WAV
La sincronización tiene una única fuente: el generador mide cada WAV con ffprobe — no hay un array de tiempos escrito a mano.
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
}
Una frase de "10 segundos" rara vez dura exactamente 10s. Medir el WAV real y derivar de ahí la duración de las escenas es lo que garantiza que la animación termine junto con la voz, sin ajustes manuales.
En entornos git-bash, usa ffmpeg -nostdin en las llamadas para extraer frames — sin la flag, el proceso puede quedarse bloqueado leyendo la entrada interactiva estándar.
⚙️ La composición: build-demo.mjs
El generador central: lee steps.json, mide los WAV y produce el index.html con marco, cursor, resaltado, zoom y CTA listos.
1. lê steps.jsonCarga viewport, window, los pasos (shot + target + caption + narration) y la ctaNarration.
2. mede WAVs + calcula geometriaConstruye AUDIO[] con ffprobe y mapea cada bbox del espacio de la captura de pantalla al canvas (mapBox, center).
3. escreve index.html (16:9)Escenas (1 por paso) + CTA, marco, cursor global, destacados, zoom y timeline GSAP: todo en un único HTML renderizable.
node build-demo.mjs # -> index.html (16:9), pronto para lint/render
Cualquier ajuste va en el steps.json o en el build-demo.mjs, después vuelve a ejecutarlo. Editar el HTML directamente se pierde en la siguiente compilación.
🔗 Sincronizar audio ↔ animación mediante S[]
La timeline y los audios leen el mismo array de tiempos derivado de AUDIO[] — sincronía por construcción.
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;
});
Las escenas y los captions quedan en tracks alternados (1/3 y 2/4): un track termina mientras el otro ya prepara el siguiente audio, lo que evita superposiciones en las transiciones con FADE.
💬 Subtítulos en el pie de página
Cada paso tiene un caption que se convierte en un subtítulo translúcido sincronizado con la escena.
O caption de cada paso (escrito en steps.json) se renderiza en el pie de página con Inter 600 sobre un fondo translúcido; aparece y desaparece junto con la escena, en una track alternada con respecto a las escenas.
La narración es lo que dice la voz (puede incluir números escritos con palabras); el caption es el texto breve en pantalla. Ambos están en el mismo paso del steps.json, pero cumplen propósitos distintos: los feeds sin sonido muestran el caption.
Mantén el caption en una sola frase. El texto largo en el pie compite con el marco y el resultado; el subtítulo refuerza la voz, no la sustituye.
📣 La CTA final de INEMA.CLUB
La última escena es la llamada de marca: ya viene lista en la plantilla, con narración predeterminada.
"CONTINÚA EN" + INEMA.CLUB (INEMA en crema, .CLUB ámbar con glow) + 🌐 inema.club. Narración predeterminada: "Esto es contenido de INEMA punto CLUB. Accede a: inema punto club." En la CTA, el cursor desaparece (opacity:0).
"ctaNarration": "Isso é conteúdo do INEMA ponto CLUB. Acesse: inema ponto club."
🎯 Lo que aprendiste
- ✓Kokoro genera la narración en PT-BR de forma local y gratis, voz pf_dora --speed 0.98
- ✓narration-template.sh extrae las narraciones de steps.json (pasos + CTA)
- ✓build-demo.mjs mide los WAV con ffprobe: timing como fuente única
- ✓S[] (start/dur/audioStart/end) sincroniza el audio y la animación
- ✓Los captions y la CTA de INEMA.CLUB ya vienen listos
Próximo: Módulo 3.3 — montar el proyecto HyperFrames, validarlo con lint/inspect y renderizar el MP4 final.