Audio and captions in code: local narration, soundtrack, and karaoke
The same CLI that renders also speaks and listens, all on your machine: it generates the narration with a local voice, transcribes word by word with the timing of each one, and hands that to the composition. In this lesson you build the technical side of sound: narration, a rehearsal soundtrack, and the karaoke caption that lights up each word the moment it is spoken.
2transcribe: the timing of each word in a transcript.json
3<audio> with id, data-start, and data-volume in the composition
4Captions of up to three words that light up on the beat of the speech
Goal: generate local narration, get the timing of each word, and render a video with audio and karaoke captions.
01 · From text to video with sound
Click each step. Nothing leaves your machine.
Click each block in the diagram to see what it does.
Voice, soundtrack, and mixing as a creative choice: course 2 (Production). Here, only how to put sound in the code.
02 · Narration and the timing of each word
One sentence, the local voice in Portuguese, and the transcription with timing.
Terminal · tts, duration, and transcription
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'))))"
Example from the repository
tts: 0
4.074667
transcribe: 0
palavras: 14
The second line is the narration length in seconds. The word count checks the transcript, not the text: check the words by opening transcript.json.
The default transcribe model in 0.8.176 is small.en, English only. For Portuguese, pass -m small -l pt: the multilingual small doesn't appear in the --help list (tiny.en, base.en, small.en, medium.en, large-v3), but it worked on this machine (screenshot alongside). And with --engine auto (default) the CLI uses Parakeet if it's installed and covers the language; without it, whisper.cpp.
03 · The composition with sound and karaoke
A title, the narration starting at 0.5 s, a rehearsal soundtrack at 25%, and captions generated from the transcript.
#!/usr/bin/env python3
"""karaoke.py — turns transcript.json (word + timing) into legendas.js for the composition.
Usage: python3 karaoke.py transcript.json --inicio 0.5 --por-linha 3 > legendas.js
--inicio seconds at which the narration starts in the video (the <audio> data-start)
--por-linha words per caption (default 3)
Prints to stderr: "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],
})
# each caption leaves 0.15 s after the last word, but never after the next one enters
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()
The checker with --audio rejects a video with no sound track. The soundtrack is just a chord generated by FFmpeg for the rehearsal; yours goes in place of trilha.wav.
In the first version, each caption stayed 0.15 s after the last word even when the next one had already come in: two captions overlapped in the frame. karaoke.py now closes each caption at most when the next one opens. It only showed up by looking at the frames.
04 · Practice
Voice, karaoke and your sentence.
E1 · Voice and words
10 min
Run the "tts, duration and transcription" block. The first use downloads the voice and the model.
Done when:tts: 0, one line with the duration (a number greater than 0), transcribe: 0 and palavras: followed by a number greater than 0.
E2 · Video with sound
10 min
Run the "soundtrack, captions, render and check" block and watch karaoke.mp4.
Done when:◇ Check passed, render: 0 and OK: 8,0 s · 1920×1080 · 30 fps · com áudio with saída: 0.
E3 · Your sentence
15 min
Swap TEXTO for a sentence of your own, up to 5 s, run both blocks again (the second after the first), and check the video.
Done when:python3 ~/projetos/formacao-cinema-ia-motion-v72/kit/conferir.py karaoke.mp4 8 1920x1080 --audio; echo "saída: $?" → a line ending in com áudio and saída: 0.
05 · Lesson quiz
The explanation appears after you answer.
Question 1 of 4
The render came out silent, but the <audio> is in the composition. What should you check first?
Correct
In HyperFrames, audio without an id does not enter the mix, and lint reports media_missing_id. It is the most common reason for a silent video.
Not this one
In HyperFrames, audio without an id does not enter the mix, and lint reports media_missing_id. It is the most common reason for a silent video.Review: From text to video with sound
Question 2 of 4
Why do the captions come from a legendas.js saved in the folder and not from a fetch at render time?
Correct
It is the rule from lesson 1: data goes in before the render, as a file in the folder. The same legendas.js always gives the same captions.
Not this one
It is the rule from lesson 1: data goes in before the render, as a file in the folder. The same legendas.js always gives the same captions.Review: Narration and the timing of each word
Question 3 of 4
The narration starts at 0.5 s in the video. Where does that delay go?
Correct
The transcript counts time from the start of the WAV. If the audio comes in at 0.5 s, each word has to come in 0.5 s later too.