Skip to main content
The script is the clock. decktalk narrate turns script.md into spoken takes with their words, and decktalk cue turns each phrase in cues.json into a second on its section’s clock. Nothing on a deck page says when a cue fires, so the only way to move a reveal is to change the words it waits for or to nudge it off them. This guide is about the two files that decide that timing: the sentences in script.md and the phrase each row of cues.json names. Design a slide is about what the picture does once the second is known.

Write the script before the slides

Write the whole script, then read it aloud once. The words that carry the argument are the words the reveals start on, so you need them before you can decide what appears. A slide drawn first usually needs a sentence nobody would say. Give each idea one reveal and each reveal one sentence. A lesson reads best at about one reveal every ten to fifteen seconds. Three reveals in one breath read as an animation rather than an explanation, and a minute with no reveal leaves the viewer nothing new to look at.

Write numbers and symbols the way you want them said

The voice reads what it is given, and a cue phrase is matched against the words it said. So “41” is a guess about how the voice pronounces two digits, and no phrase you write can reliably match that guess. Write the words instead. The starter’s script says “Make your own at decktalk dot AI” while its end card shows decktalk.ai, and the lesson example says “four hundred billion” while its slide shows 400 billion dials. The number on screen and the number in the ear are the same figure written twice, once for each sense.

The SCRIPT_SPOKEN_SYMBOL finding

decktalk check scans the spoken text of every section and raises SCRIPT_SPOKEN_SYMBOL for a section whose words hold a digit or one of the characters $ € £ % & @ # * / + = < > ~ ^ |. The finding names the section and lists the first five offending words.
--no-pages judges the written files with no browser at all, which is the fastest way to read this one finding. The finding is a warning, because “41” may be exactly what you want the voice to try, so it does not fail a run under the default --fail-on error. Two things clear it:
  • Rewrite the word. Replace the digit or the symbol with the words the voice should say. This is the fix in almost every case, because it is also the only way a cue phrase can match.
  • Accept it deliberately. Pass --allow SCRIPT_SPOKEN_SYMBOL on the commands that would otherwise report it, and --fail-on warning when you want every warning to fail a run instead.
A respelling meant to steady the voice becomes the spoken word, the cue phrase and the caption together, so say so in the project when you respell a name.

The beat marker and the timed pause

A short silence before a reveal makes the reveal an event. Bracketed directions are the only way a script asks for one, and they are never spoken.
script.md
Only a timed pause asks for measured silence, because frequent measured silences unsettle the voice. ElevenLabs renders a timed pause only on its v2 family, and v3 and v4 read no <break> tag, so check reports a section with a [pause N] on any other model as SCRIPT_PAUSE_DROPPED and narrate refuses to buy it, while a [beat] reads on every model. Back-to-back directions do not add up and the longest one wins, and a direction before the first paragraph of a section has nothing to pause after, so it is dropped.

What a script may not hold

Inside a paragraph, [beat] and [pause N] are the only bracketed spans allowed, along with an ordinary Markdown link. Any other bracketed note inside a paragraph becomes a pause nobody asked for, so decktalk narrate refuses it and decktalk check reports it as SCRIPT_UNFINISHED with the line it sits on. A brace and an HTML comment are refused for the opposite reason: the voice reads both of them out. A bracketed word in capitals, such as [CUSTOMER_COUNT], is an unfilled blank. Write one wherever you have a claim nobody has confirmed, because a voiced run would otherwise buy the claim as it stands. SCRIPT_UNFINISHED is an error, so it fails a run until the blank is filled. script.md lists every rule about the file.

Choose a phrase the voice really says

A cue row names a cue id and a phrase. The phrase is a word or a short phrase from that section of script.md.
cues.json
The script says “So it steps downhill, again and again, and the steps shrink as the ground flattens”, and the steps on the slide arrive as the word “steps” is said. Four rules decide whether a phrase works.
  1. It has to be spoken. The match is against the words of that section alone, and a heading, a bracketed direction and a word in another section are all invisible to it.
  2. It starts on the word that names the thing. A cue time is the start of the phrase’s first word, so a phrase beginning “and then” lands the picture on nothing. Among the starter’s phrases are This is DeckTalk, what you will say, the slides, the formula and in code.
  3. It is two or three words long. A single common word such as “the”, “one” or “it” matches early and lands on the wrong sentence.
  4. It is copied, not retyped. The match drops case and every character but a letter, a digit and an apostrophe, so Four files matches “four files” and project does not match “project’s”.
Set "case_sensitive": true on a row when the case really is the difference between two matches. A phrase that is never spoken is CUE_UNRESOLVED, which is an error, because there is no second to put the picture at. Fix it by quoting the script again, not by moving the limit.

occurrence, when the phrase is not unique

A repeated phrase matches every run of it, and the first match is usually too early. occurrence picks which one, counting from 1.
cues.json
That row lands on the second time section 3 says “zero”. Prefer changing the phrase to naming an occurrence, because an occurrence is a count a later edit to the script silently changes. Every row of the lesson example names its occurrence anyway, so a phrase the script repeats later can never move a picture by accident.

offset_seconds, which moves a cue off the start of its word

offset_seconds adds seconds to the resolved second. A positive value moves the cue later and a negative value moves it earlier.
cues.json
The words "it steps downhill" sit in boxes as long as each word is spoken. A line at the start of "steps" marks the cue time with offset_seconds 0. An accent line 0.2 s later marks offset_seconds 0.2. An arrow pointing earlier marks a negative offset_seconds, for text read aloud.The words "it steps downhill" sit in boxes as long as each word is spoken. A line at the start of "steps" marks the cue time with offset_seconds 0. An accent line 0.2 s later marks offset_seconds 0.2. An arrow pointing earlier marks a negative offset_seconds, for text read aloud.

A positive offset_seconds moves the cue time after the start of its word.

  • A positive offset of 0.1 to 0.2 seconds. The reveal lands just after the word starts, while the word is still being said, which reads as the narrator pointing at something.
  • A small negative offset. Use it only where the viewer has to read along, such as a line of code the voice reads out. The text must be on screen before its first word is spoken.
Keep an offset under half a second. A reveal that needs more than that belongs on a different word, and the offset is hiding the fact that the phrase is wrong. The starter writes no offset at all, so every reveal in a new project starts on its word. The resolved second carries the offset inside it, and build/cue-times.json records the offset separately so you can see what the phrase alone would have given.

startandstart and end

Two anchors stand in for a phrase. $start is the section start, which is zero on its clock, and $end is the end of the section’s last spoken word. Give the last reveal of a section $end and it lands as the voice stops.
cues.json
$end is the end of speech and not the end of the section’s audio, so silence after the last word belongs to hold_seconds on that [[section]] instead. cues.json lists every key a row may carry.

Prove it before you spend anything

Three commands read the clock without buying a take.
decktalk narrate --no-spend keeps every take on disk and writes a placeholder and its words for each section that has none, and it spends nothing. On a project that has never been built, words has no clock to read until this runs. decktalk words prints every spoken word with its start and its end, which is the clock every phrase is written against. Before a voiced run the words are estimated from the text, and after one they are the times the voice really took. decktalk check judges script.md, cues.json and the pages together, prices the narration a voiced build would buy and writes build/storyboard.html. It reports a moment on the page that no row names as CUE_UNLISTED, a row the page does not declare as CUE_UNKNOWN, and two cues close enough together to read as one as CUE_OVERLAP. decktalk check --fix applies every safe fix, and the fix for CUE_UNLISTED adds the row with an empty phrase for you to write the phrase into.

Next