Time a lyric sheet
Give it the lyric sheet, the vocal stem's word timings and the song's beats.json; get
lines.json, every sung line with a start and end per word, and a printout of each
line with its bar and the words to check by ear. Use it when captions must land on the
syllable. recipe-multi-style-music-video aligns lyrics more simply (sequence-match
and interpolate, in its own steps) and is enough for captions per line.
Inputs
- The lyric sheet as text: one sung line per line; blank lines are skipped and a
[Chorus]style header names the section of the lines under it. Add ad-libs the sheet leaves out ("oh, oh") as their own lines if they should be captioned; leave them out and they are skipped. - Word timings of the vocal stem, never the mix (
demucs-split-song-stems): ElevenLabs speech-to-text JSON (wordswithtext,start,end,type) or whisper-cli JSON from-ml 1 -oj(one segment per word, offsets in milliseconds). beats.jsonfrompython-map-song-bars, for bar numbers and--repeat.
Run it
Write references/time_lyrics.md's script into the job's scripts/ as
time_lyrics.py, unchanged, and run it:
exec("python", ["scripts/time_lyrics.py", "--lyrics", "source/lyrics.txt",
"--stt", "analysis/vocals_stt.json", "--beats", "analysis/beats.json",
"--out", "captions/lines.json", "--from", "15", "--repeat", "29", "68", "7"],
cwd="<job>", skill="python-time-lyrics")
--from Sskips STT words before S seconds: a hook sung in the intro that the sheet places later would otherwise swallow the first lines.--repeat SRC DST BARSgives the lines that start in bars DST to DST+BARS the timing of the lines in SRC to SRC+BARS, moved by the bar grid. Use it when one repeat of a chorus comes back garbled and another is clean. Both ranges must hold the same number of lines; the script stops and says so when they don't. Repeatable.
What you get
lines.json: [{text, section, words: [{text, start, end, matched}]}], in sheet
order, the lyric's own spelling with the singer's timing, plus repeat_of on copied
lines. The printout:
31.42- 34.80 bar 14 Chorus Hold the line until the morning
35.10- 37.95 bar 16 Chorus we were never? meant to fall
- A misheard word (sung "there", heard "dare") keeps the STT word's time under the
lyric's text,
matched: true. - A dropped word (STT lost "I'll") gets a time 0.25 s before the next matched word
and
matched: false, printed with?. Check every?by ear. - A line with no match at all follows the line before, every word
?: look at the STT around it, then fix with--fromor--repeat, or by hand.
Tips and traps
- STT on a sung vocal mishears and drops words, even on the vocal stem, and in the
worked job it garbled the timing of one chorus out of three while the other two were
clean:
--repeatcopied a clean one onto it. - Fast repeated words ("go, go, go, go") come back merged or missing. Put them on
the beat grid (
beatsinbeats.json) instead of the STT. - Caption what is sung. Where the singer changed a word from the sheet, change the sheet line, then re-run.
- Each line is matched against the next few STT words after the line before (eight more than the line's length), so a line far out of order on the sheet won't be found: keep the sheet in sung order, repeats written out.
- After a new mix or a trimmed song head, transcribe again and re-run; old times are wrong by however much the audio moved.
Check it
- No
?left that you haven't listened to. - Every line's bar matches the shot list (
python-map-song-barsprintout). - Render captions over a few lines (
hn4a-compositor-motion-graphics) and watch with sound: each word lands on its syllable, not a word early or late.


