script.md holds the words the voice says, split into numbered sections. One numbered section is
one cut of the film. narrate reads this file, turns each stretch one voice reads into a spoken take, and writes
the words that cue, assemble and verify all measure against. This page is the whole rule
the parser holds you to: the heading, every bracketed direction, how a chapter title is taken, the
silence a section carries, and the six findings a script can earn.
script.md
Headings
A numbered heading opens a section, and the next heading of any kind closes it.
Text before the first numbered heading is never spoken, which is where the starter keeps its notes
above a
--- line. A ### heading inside a section does not close it: narrate removes the heading
marks and the voice reads the heading text as prose.
What the script and the project file must agree on
narrate refuses the script before it plans anything when any of these is true.
- The file the
[project] scriptkey names is not there, which isscript.md is not there. - The file holds no numbered heading at all, which is
script.md holds no '## N. Title' section. - A numbered heading has no
[[section]]with the samenumberindecktalk.toml, which isscript sections [4] have no [[section]] in decktalk.toml. - The numbered headings do not ascend, which is
the script heading for section 2 comes after section 3, so the sections do not ascend...
## N. heading with its number, and one with none
plays silent, because that heading is the speech the recording is cut to.
A clip section needs none: the voice never reads a clip section, so a heading written for one is
parsed and left unspoken, and the body under it can describe what the clip shows.
The chapter title
A section’s chapter title names it in the film’s chapter markers, in the transcript page and on the slate a missing clip plays. Three sources are read in order, and the first that has a value wins.- The
chapterkey of that section’s[[section]]table indecktalk.toml. - The title of the
## N. Titleheading inscript.md. Section N, for a section that has neither.
Directions and placeholders
Square brackets are the one piece of punctuation the parser reads rather than speaks. Five kinds of bracketed text are accepted, and every other kind is refused.
A pause is data, not text: each paragraph is sent with the pause after it, and the voice’s provider
writes that pause its own way. ElevenLabs writes a beat as a space and a dash, which is the pause the
voice makes on its own, and a timed pause as a break tag, such as
<break time="3s" />. A break tag
you write yourself, such as <break time="1.5s" /> or <break time="1500ms"/>, is read as the
[pause N] it names, so there is one way to ask for a pause, and a break tag with no length the
parser can read is refused. [pause N] is the only direction that asks for measured silence,
because frequent measured silences unsettle the voice.
Writing for the ear says when each one is the right choice.
A model that renders no timed pause cannot voice a section that holds one. ElevenLabs renders one
only on its v2 family, eleven_multilingual_v2, eleven_flash_v2_5, eleven_flash_v2,
eleven_turbo_v2_5 and eleven_turbo_v2, and v3 and v4 read no <break> tag. check reports such a section as
SCRIPT_PAUSE_DROPPED, and narrate refuses to buy its take
rather than drop the pause. A beat is a dash every model reads, so a beat is never refused.
What a script may not hold
narrate refuses a script whose spoken text holds anything else, before it plans a single request,
because a run that has already paid cannot take the money back. Three things are refused, each with
the line it sits on.
- A bracketed span inside a paragraph that is not
[beat], not[pause N], not a placeholder and not a Markdown link, because the parser would turn it into a pause nobody asked for. - An HTML comment, because the parser leaves it in the prose and the voice reads it out.
- A brace, for the same reason.
How two directions in a row combine
Voice directions and turns
A line holding only[voice NAME] is a voice direction. It hands the turns after it to the voice
[voices.NAME] declares in decktalk.toml, until the next voice direction or the next section, and
it is never spoken. Every section starts again with the default voice, which [narration] voice
names, so a section that names no voice is read by the default voice whole, exactly as before.
script.md
The captions and the transcript carry the words of each turn alone, never the voice’s name, and a
caption never spans two turns.
What the voice receives
narrate cleans each section in this order into paragraphs, each with the pause after it.
- A Markdown link becomes its own text.
[pause N], and a break tag written by hand, becomes a pause marker carrying its seconds.- Every other bracketed span that is not a placeholder becomes a pause marker carrying a beat.
- The markers of inline code, bold and emphasis are removed.
- Heading marks, list bullets and list numbers at the start of a line are removed.
- The lines of a paragraph are joined with single spaces, and a blank line separates paragraphs.
- Each pause marker becomes the pause after the paragraph before it.
cues.json as plain words.
build/narrate/takes.json carries each turn’s spoken text, which is exactly what a cue phrase
is matched against.
The silence around a section
Every spoken section opens on a lead of silence and closes on a tail of silence, so the silence across each cut is one section’s tail followed by the next one’s lead, and the picture always changes before the voice speaks.
ffmpeg places both when it joins the takes into one narration track. The speech provider never
receives them and no take file holds them, which is what lets one take serve whatever section number
it ends up under.
narrate measures where the sound of a take ends from the take’s own bytes, puts
the lead before it, cuts it at that sound end and places exactly the tail after it, so a breath or a
pause the voice left after its last word never plays. The take’s words are cut at the same place, so a
last word its voice ran on into that silence ends where the sound does.
Because placement reads the take and that section’s own two keys and nothing else, a section lands
the same way whether this run voiced its take or found it cached, and an edit to one section never
moves another. Neither key is part of the take’s input digest, so changing a lead or a tail voices
nothing and costs nothing.
A silence that ends within 0.06 seconds or one audio frame of the end of the file, whichever is
longer, counts as running to the end, because an mp3 encoder adds up to about 50 milliseconds of
padding there. Configuration lists every [narration] key with
its safe range.
How long a section runs
The take index and a placeholder work a section’s length out from its words.words is the count of spoken words, which is the cleaned text with its dashes removed. The timed
pause total covers every [pause N] and every break tag written by hand. A dash
with a space before it counts as a beat wherever it appears, including one typed into the prose. The
defaults are 140 words per minute, 150 for a placeholder, 0.7 seconds for a beat and for
tail_seconds, 0.5 seconds for lead_seconds, and 0.3 seconds for the gap between two turns,
narration.turn_gap_seconds.
The findings a script can earn
decktalk check judges the script without voicing it, reports every line at once rather than
stopping on the first, and prices what a voiced run would cost. Six codes cover the script.
narrate does not refuse unfinished text, so check is what stands between unfinished text and a
voiced take. SCRIPT_SPOKEN_SYMBOL is a note rather than a refusal, because 41 may be
exactly what you want the voice to try. Pass --allow SCRIPT_SPOKEN_SYMBOL to carry on past it, and
--fail-on warning to fail on it.
Numbers and symbols
Write numbers and symbols the way the voice should say them. A cue phrase matches the spoken words, so the script and the cue have to use the same words for the same thing. The starter writes “decktalk dot AI” in the script and shows the address on the slide. A cue ondecktalk dot AI resolves, because those are three spoken words. A cue on decktalk.ai matches
nothing, because the matcher drops the dot and is left with one token that no word equals.
Related
- Plan a script by ear: Writing for the ear
- Map a section to a page or a clip: decktalk.toml
- Land a reveal on a phrase: cues.json
- Read every spoken word with its seconds: CLI