PTENES
MÓDULO 3.2

🔊 Narración local y composición

La voz y el montaje: TTS local gratuito de Kokoro, generación de los WAV a partir del steps.json, medición automática de las duraciones y el build-demo.mjs que sincroniza el audio y la animación con el mismo array de tiempos.

7
Temas
30
Minutos
Avanzado
Nivel
Lectura
Tipo
steps.json narration + ctaNarration Kokoro TTS pf_dora txt/sN.txt → audio/sN.wav ffprobe mide WAVs duraciones reales AUDIO[] → S[] LEAD 0.5 · TAIL 0.7 · FADE 0.4 inicio · dur · audioStart · final timeline GSAP + pistas de audio leen el MISMO S[] index.html audio ↔ animación sincronizados + CTA
1

🎙️ 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.

💡Concepto principal

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.

generación de un WAV
npx -y hyperframes tts "assets/txt/s1.txt" \
  --voice pf_dora \
  --speed 0.98 \
  --output "assets/audio/s1.wav"
⚡
Desarrolla los números y las siglas en la narración

"512" → "quinientos doce"; "URL" → deletrea; "inema.club" → "inema punto club". El TTS lee literalmente: escribirlo con palabras evita lecturas extrañas.

🚨
No escuchas el audio en el renderizado

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.

2

📝 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.

narration-template.sh — fragmento
# 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
✓HACER
  • ✓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/)
✗NO HACER
  • ✗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
3

📏 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.

build-demo.mjs — medición automática
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
}
📊
Por qué medir en vez de estimar

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.

⚡
ffmpeg/ffprobe en Windows/git-bash

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.

4

⚙️ 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.json

Carga viewport, window, los pasos (shot + target + caption + narration) y la ctaNarration.

2. mede WAVs + calcula geometria

Construye 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.

un comando
node build-demo.mjs     # -> index.html (16:9), pronto para lint/render
🚨
index.html se genera — no editar a mano

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.

5

🔗 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.

build-demo.mjs — cálculo de S[]
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;
});
▶️
inicio
inicio de la escena
⏳
dur
LEAD+audio+TAIL
🔉
audioStart
inicio + LEAD
⏹️
end
inicio + dur
📊
Tracks alternados anti-solapamiento

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.

6

💬 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.

💡Concepto principal

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.

📊
Caption ≠ narración

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.

⚡
Caption breve

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.

7

📣 La CTA final de INEMA.CLUB

La última escena es la llamada de marca: ya viene lista en la plantilla, con narración predeterminada.

💡Escena final estándar

"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).

steps.json — ctaNarration
"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.