Skip to main content
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] script key names is not there, which is script.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 same number in decktalk.toml, which is script 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...
A page or render section is narrated from the ## 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.
  1. The chapter key of that section’s [[section]] table in decktalk.toml.
  2. The title of the ## N. Title heading in script.md.
  3. Section N, for a section that has neither.
Consecutive sections that end up with the same title share one chapter marker. A take is named by the input digest of the text it was voiced from, and the title is not part of that text, so retitling a section voices nothing.

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.
The rule reaches the body of every numbered section and nothing else, so a note in brackets above the first heading or under an unnumbered heading is left alone.

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
A turn is the stretch of one section that one voice reads, from one voice direction to the next. A section one voice reads is one turn. Each turn is voiced as its own take, so two turns with the same text and the same voice share one take, and changing one turn voices that turn alone. A voice direction naming the voice already reading starts no new turn. Four rules keep a direction from being misread. 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.
  1. A Markdown link becomes its own text.
  2. [pause N], and a break tag written by hand, becomes a pause marker carrying its seconds.
  3. Every other bracketed span that is not a placeholder becomes a pause marker carrying a beat.
  4. The markers of inline code, bold and emphasis are removed.
  5. Heading marks, list bullets and list numbers at the start of a line are removed.
  6. The lines of a paragraph are joined with single spaces, and a blank line separates paragraphs.
  7. Each pause marker becomes the pause after the paragraph before it.
Each turn’s take has a cache key taken over one canonical text of its paragraphs: they are joined by a blank line, a timed pause is written as a break tag after its paragraph and a beat as a dash. That text is also what ElevenLabs is sent and billed for. The words the voice actually says carry neither the dashes nor the pauses. Cue matching and the captions both read those spoken words, so write every cue phrase in 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 on decktalk 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.