Motion por código con agente|INEMA.CLUB-PRO

Clase 7 · 18 min

Audio y subtítulos en el código: narración local, pista musical y karaoke

El mismo CLI que renderiza también habla y escucha, todo en tu máquina: genera la narración con una voz local, transcribe palabra por palabra con el tiempo de cada una y se lo entrega a la composición. En esta lección armas el lado técnico del sonido: narración, una pista de prueba y el subtítulo karaoke que ilumina cada palabra en el momento en que se dice.

Empezar la clase
Mesa de edición de sonido con micrófono de estudio en brazo articulado, audífonos cerrados y un monitor con forma de onda.

En esta clase vas a

  1. 1tts: texto en WAV con la voz Kokoro, sin nube
  2. 2transcribe: el tiempo de cada palabra en un transcript.json
  3. 3<audio> con id, data-start y data-volume en la composición
  4. 4Subtítulo de hasta tres palabras que se ilumina en el tiempo del habla

Objetivo: generar narración local, sacar el tiempo de cada palabra y renderizar un video con audio y subtítulo karaoke.

01Del texto al video con sonido

Haz clic en cada etapa. Nada sale de tu máquina.

Haz clic en cada bloque del diagrama para ver qué hace.

Voz, pista y mezcla como decisión creativa: curso 2 (Producción). Aquí, solo cómo poner el sonido en el código.

02Narración y tiempo de cada palabra

Una frase, la voz local en portugués y la transcripción con tiempo.

Terminal · tts, duración y transcripción
K=~/projetos/formacao-cinema-ia-motion-v72/kit
export HYPERFRAMES_NO_TELEMETRY=1 NO_COLOR=1
E=${TMPDIR:-/tmp}/ensaio-motion/a7; rm -rf "$E"; mkdir -p "$E"
cp -r "$K/exemplos/a7-karaoke" "$E/karaoke"; cd "$E/karaoke"
TEXTO="A voz sai do seu computador, e cada palavra sabe a hora de aparecer."
npx -y hyperframes@0.8.176 tts "$TEXTO" --voice pf_dora --lang pt-br -o narracao.wav > tts.log 2>&1; echo "tts: $?"
ffprobe -v error -show_entries format=duration -of csv=p=0 narracao.wav
npx -y hyperframes@0.8.176 transcribe narracao.wav -m small -l pt --json > transcricao.log 2>/dev/null; echo "transcribe: $?"
python3 -c "import json; print('palavras:', len(json.load(open('transcript.json'))))"
Ejemplo del repositorio
tts: 0
4.074667
transcribe: 0
palavras: 14

La segunda línea es la duración de la narración en segundos. El número de palabras verifica el transcript, no el texto: verifica las palabras abriendo el transcript.json.

03La composición con sonido y karaoke

Título, la narración empezando en 0,5 s, una pista de ensayo al 25% y los subtítulos generados a partir del transcript.

Archivo · kit/exemplos/a7-karaoke/index.html
<!doctype html>
<html lang="pt-BR">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=1920, height=1080" />
    <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
    <script src="legendas.js"></script>
    <style>
      * { margin: 0; padding: 0; box-sizing: border-box; }
      html, body { width: 1920px; height: 1080px; overflow: hidden; background: #0c0c10; }
      #root { width: 100%; height: 100%; position: relative; font-family: ui-sans-serif, system-ui, sans-serif; }
      #luz { position: absolute; inset: 0; background: radial-gradient(ellipse at 50% 40%, #1c1c22 0%, #0c0c10 70%); }
      #titulo { position: absolute; left: 0; right: 0; top: 300px; text-align: center; color: #f4f4f5; font-size: 110px; font-weight: 700; line-height: 1.1; }
      .legenda { position: absolute; left: 0; right: 0; top: 820px; text-align: center; font-size: 70px; font-weight: 700; line-height: 1.2; color: #a1a1aa; opacity: 0; }
      .legenda span { margin: 0 12px; }
    </style>
  </head>
  <body>
    <div id="root" data-composition-id="main" data-start="0" data-duration="8" data-width="1920" data-height="1080">
      <div id="luz"></div>
      <h1 id="titulo">Som no código</h1>
      <audio id="narracao" src="narracao.wav" data-start="0.5" data-track-index="1"></audio>
      <audio id="trilha" src="trilha.wav" data-start="0" data-duration="8" data-volume="0.25" data-track-index="2"></audio>
    </div>
    <script>
      // legendas.js (gerado pelo karaoke.py) traz cada legenda com o tempo de cada palavra
      const root = document.getElementById("root");
      const tl = gsap.timeline({ paused: true });
      tl.fromTo("#titulo", { y: 30, opacity: 0 }, { y: 0, opacity: 1, duration: 0.6, ease: "power3.out" }, 0.1);
      window.LEGENDAS.forEach((linha, i) => {
        const el = document.createElement("p");
        el.className = "legenda";
        el.id = "legenda-" + i;
        linha.palavras.forEach((p, j) => {
          const s = document.createElement("span");
          s.id = `palavra-${i}-${j}`;
          s.textContent = p.t;
          el.appendChild(s);
          tl.set(s, { color: "#f4f4f5" }, p.inicio);   // a palavra acende quando é dita
        });
        root.appendChild(el);
        tl.set(el, { opacity: 1 }, linha.inicio);
        tl.set(el, { opacity: 0 }, linha.fim);
      });
      window.__timelines["main"] = tl;
    </script>
  </body>
</html>
Archivo · kit/karaoke.py
#!/usr/bin/env python3
"""karaoke.py — transforma o transcript.json (palavra + tempo) em legendas.js para a composição.

Uso:  python3 karaoke.py transcript.json --inicio 0.5 --por-linha 3 > legendas.js

--inicio     segundos em que a narração começa no vídeo (o data-start do <audio>)
--por-linha  palavras por legenda (padrão 3)
Imprime na saída de erro: "palavras: N · legendas: M".
"""
import argparse
import json
import sys


def main():
    a = argparse.ArgumentParser()
    a.add_argument("transcript")
    a.add_argument("--inicio", type=float, default=0.0)
    a.add_argument("--por-linha", type=int, default=3)
    o = a.parse_args()
    palavras = json.load(open(o.transcript, encoding="utf-8"))
    linhas = []
    for i in range(0, len(palavras), o.por_linha):
        grupo = palavras[i:i + o.por_linha]
        linhas.append({
            "inicio": round(grupo[0]["start"] + o.inicio, 3),
            "fim": round(grupo[-1]["end"] + o.inicio, 3),
            "palavras": [{"t": p["text"], "inicio": round(p["start"] + o.inicio, 3)} for p in grupo],
        })
    # cada legenda sai 0,15 s depois da última palavra, mas nunca depois que a próxima entra
    for atual, prox in zip(linhas, linhas[1:] + [None]):
        atual["fim"] = round(min(atual["fim"] + 0.15, prox["inicio"]) if prox else atual["fim"] + 0.15, 3)
    print("window.LEGENDAS = " + json.dumps(linhas, ensure_ascii=False) + ";")
    print(f"palavras: {len(palavras)} · legendas: {len(linhas)}", file=sys.stderr)


if __name__ == "__main__":
    main()
Terminal · pista, subtítulos, render y verificar
K=~/projetos/formacao-cinema-ia-motion-v72/kit
export HYPERFRAMES_NO_TELEMETRY=1 NO_COLOR=1
cd ${TMPDIR:-/tmp}/ensaio-motion/a7/karaoke
# trilha de ensaio: um acorde suave de 8 s gerado no ffmpeg (a sua trilha de verdade entra no lugar)
ffmpeg -v error -y -f lavfi -i "aevalsrc=0.10*sin(2*PI*220*t)+0.07*sin(2*PI*277.18*t)+0.06*sin(2*PI*329.63*t):s=48000:d=8" \
  -af "afade=t=in:d=1,afade=t=out:st=6.5:d=1.5" trilha.wav
python3 "$K/karaoke.py" transcript.json --inicio 0.5 --por-linha 3 > legendas.js
npx -y hyperframes@0.8.176 check 2>&1 | grep -E "Check passed|✗"
npx -y hyperframes@0.8.176 render --quality draft --workers 2 --quiet --output karaoke.mp4 > render.log 2>&1
echo "render: $?"
python3 "$K/conferir.py" karaoke.mp4 8 1920x1080 --audio; echo "saída: $?"
Ejemplo del repositorio
palavras: 14 · legendas: 5
◇  Check passed
render: 0
OK: 8,0 s · 1920×1080 · 30 fps · com áudio
saída: 0

El verificador con --audio reprueba un video sin pista de sonido. La pista es solo un acorde generado por FFmpeg para el ensayo; la tuya entra en lugar de trilha.wav.

04Práctica

Voz, karaoke y tu frase.

E1 · Voz y palabras

10 min

Ejecuta el bloque "tts, duración y transcripción". El primer uso descarga la voz y el modelo.

Listo cuando: tts: 0, una línea con la duración (número mayor que 0), transcribe: 0 y palavras: seguido de un número mayor que 0.

E2 · Video con sonido

10 min

Ejecuta el bloque "pista, subtítulos, render y verificar" y mira el karaoke.mp4.

Listo cuando: ◇ Check passed, render: 0 y OK: 8,0 s · 1920×1080 · 30 fps · com áudio con saída: 0.

E3 · Tu frase

15 min

Cambia TEXTO por una frase tuya de hasta 5 s, ejecuta los dos bloques de nuevo (el segundo después del primero) y verifica el video.

Listo cuando: python3 ~/projetos/formacao-cinema-ia-motion-v72/kit/conferir.py karaoke.mp4 8 1920x1080 --audio; echo "saída: $?" → línea que termina en com áudio y saída: 0.

05Prueba de la clase

La explicación aparece después de tu respuesta.

Pregunta 1 de 4

El render salió mudo, pero el <audio> está en la composición. ¿Qué verificar primero?

06En tres frases

Localtts (Kokoro) y transcribe (whisper.cpp) corren en tu máquina, por el mismo CLI.
Tiempo por palabraEl transcript.json se convierte en legendas.js antes del render, con el retraso de la narración sumado.
Audio en el código<audio> con id y data-start; data-volume para la pista; el verificador cobra la pista.

Para ir más allá

Clase anteriorPróxima clase