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.
2transcribe: el tiempo de cada palabra en un transcript.json
3<audio> con id, data-start y data-volume en la composición
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.
01 · Del 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.
02 · Narració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.
El modelo por defecto de transcribe en la 0.8.176 es small.en, solo inglés. Para portugués, pasa -m small -l pt: el small multilingüe no aparece en la lista del --help (tiny.en, base.en, small.en, medium.en, large-v3), pero funcionó en esta máquina (captura al lado). Y con --engine auto (por defecto) el CLI usa Parakeet si está instalado y cubre el idioma; si no, whisper.cpp.
03 · La 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.
#!/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: $?"
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.
En la primera versión, cada subtítulo se quedaba 0,15 s después de la última palabra incluso cuando la siguiente ya había entrado: dos subtítulos se superponían en el fotograma. Ahora karaoke.py cierra cada subtítulo como máximo cuando se abre el siguiente. Solo apareció al mirar los fotogramas.
04 · Prá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.
05 · Prueba 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?
Correcto
En HyperFrames, un audio sin id no entra en la mezcla y el lint avisa media_missing_id. Es el motivo más común de un video mudo.
No es esa
En HyperFrames, un audio sin id no entra en la mezcla y el lint avisa media_missing_id. Es el motivo más común de un video mudo.Repasar: Del texto al video con sonido
Pregunta 2 de 4
¿Por qué los subtítulos vienen de un legendas.js grabado en la carpeta y no de una consulta en el momento?
Correcto
Es la regla de la clase 1: el dato entra antes del render, como archivo de la carpeta. El mismo legendas.js da siempre los mismos subtítulos.
No es esa
Es la regla de la clase 1: el dato entra antes del render, como archivo de la carpeta. El mismo legendas.js da siempre los mismos subtítulos.Repasar: Narración y tiempo de cada palabra
Pregunta 3 de 4
La narración empieza en 0,5 s en el video. ¿Dónde entra ese retraso?
Correcto
El transcript cuenta el tiempo desde el inicio del WAV. Si el audio entra en 0,5 s, cada palabra tiene que entrar 0,5 s después también.
No es esa
El transcript cuenta el tiempo desde el inicio del WAV. Si el audio entra en 0,5 s, cada palabra tiene que entrar 0,5 s después también.Repasar: Narración y tiempo de cada palabra
Pregunta 4 de 4
¿Qué modelo del transcribe usar para una narración en portugués?
Correcto
Los modelos .en solo transcriben inglés. El small multilingüe entiende portugués; el large-v3 se equivoca menos y tarda más.