E1 · Voz e palavras
10 minRode o bloco "tts, duração e transcrição". O primeiro uso baixa a voz e o modelo.
Pronto quando: tts: 0, uma linha com a duração (número maior que 0), transcribe: 0 e palavras: seguido de um número maior que 0.
Aula 7 · 18 min
O mesmo CLI que renderiza também fala e escuta, tudo na sua máquina: gera a narração com uma voz local, transcreve palavra por palavra com o tempo de cada uma e entrega isso à composição. Nesta aula você monta o lado técnico do som: narração, uma trilha de ensaio e a legenda karaokê que acende cada palavra na hora em que ela é dita.
Começar a aula
<audio> com id, data-start e data-volume na composiçãoObjetivo: gerar narração local, tirar o tempo de cada palavra e renderizar um vídeo com áudio e legenda karaokê.
Clique em cada etapa. Nada sai da máquina.
Voz, trilha e mixagem como escolha criativa: curso 2 (Produção). Aqui, só como pôr o som no código.
Uma frase, a voz local em português e a transcrição com tempo.
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'))))"tts: 0 4.074667 transcribe: 0 palavras: 14
A segunda linha é a duração da narração em segundos. O número de palavras confere o transcript, não o texto: confira as palavras abrindo o transcript.json.
O modelo padrão do transcribe no 0.8.176 é o small.en, só inglês. Para português, passe -m small -l pt: o small multilíngue não aparece na lista do --help (tiny.en, base.en, small.en, medium.en, large-v3), mas funcionou nesta máquina (captura ao lado). E com --engine auto (padrão) o CLI usa o Parakeet se ele estiver instalado e cobrir o idioma; sem ele, o whisper.cpp.
Título, a narração começando em 0,5 s, uma trilha de ensaio a 25% e as legendas geradas do transcript.
<!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>#!/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()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: $?"palavras: 14 · legendas: 5 ◇ Check passed render: 0 OK: 8,0 s · 1920×1080 · 30 fps · com áudio saída: 0
O conferidor com --audio reprova vídeo sem faixa de som. A trilha é só um acorde gerado pelo FFmpeg para o ensaio; a sua entra no lugar do trilha.wav.
Na primeira versão, cada legenda ficava 0,15 s depois da última palavra mesmo quando a próxima já tinha entrado: duas legendas se sobrepunham no quadro. O karaoke.py agora fecha cada legenda no máximo quando a próxima abre. Só apareceu olhando os quadros.
Voz, karaokê e a sua frase.
Rode o bloco "tts, duração e transcrição". O primeiro uso baixa a voz e o modelo.
Pronto quando: tts: 0, uma linha com a duração (número maior que 0), transcribe: 0 e palavras: seguido de um número maior que 0.
Rode o bloco "trilha, legendas, render e conferir" e assista ao karaoke.mp4.
Pronto quando: ◇ Check passed, render: 0 e OK: 8,0 s · 1920×1080 · 30 fps · com áudio com saída: 0.
Troque TEXTO por uma frase sua de até 5 s, rode os dois blocos de novo (o segundo depois do primeiro) e confira o vídeo.
Pronto quando: python3 ~/projetos/formacao-cinema-ia-motion-v72/kit/conferir.py karaoke.mp4 8 1920x1080 --audio; echo "saída: $?" → linha terminando em com áudio e saída: 0.
A explicação aparece depois da sua resposta.
Pergunta 1 de 4
O render saiu mudo, mas o <audio> está na composição. O que conferir primeiro?
Pergunta 2 de 4
Por que as legendas vêm de um legendas.js gravado na pasta e não de uma busca na hora?
Pergunta 3 de 4
A narração começa em 0,5 s no vídeo. Onde esse atraso entra?
Pergunta 4 de 4
Qual modelo do transcribe usar para narração em português?
Escolha uma alternativa. A explicação aparece depois da sua tentativa.
<audio> com id e data-start; data-volume para a trilha; o conferidor cobra a faixa.