Skip to main content
cues.json is the cue file. It says which spoken phrase each moment the deck declares lands on, section by section. The file owns seconds and the page owns moments: the page says what a thing looks like and what it means, this file says when it happens, and neither ever writes the other’s half. That is why no attribute in the markup carries a second and no row here carries a class or a style.
cues.json

Shape

The file is one JSON object with a sections key. Beside it only _comment may stand, which is where the starter keeps its comment and which nothing reads. Any other key at the top level is refused as an INPUT error naming the key, because the usual one is a section block pasted outside sections, whose cues would never fire. Each key of sections is a section number written as a string, and it must name a [[section]] in decktalk.toml. Each section is an object with a cues array and an optional min_seconds. The file is optional. A project without it resolves no cues at all, and the deck earns one CUE_UNLISTED finding counting every moment it declares, whose fix writes the whole file with a row for each of them. The load refuses a file that is not valid JSON, a file with no top-level sections object, a top-level key other than sections and _comment, a section key that is not a number, a section number that decktalk.toml does not declare, a section whose value is not an object, a row with a key no cue reads, a row with no id or no phrase, a row whose id is empty, and a row whose occurrence is below 1. Each refusal names the file, and a JSON syntax error names the line as well. A row whose phrase is empty loads, and CUE_UNRESOLVED says it has no phrase yet.

The keys of one row

Any other key is refused by name, so a typo can never move a cue in silence.

id, the cue id

id is the cue id of one moment the page declares, such as 4.1:expand. decktalk schema page prints every attribute that declares a moment.

phrase

The phrase is matched against the words of that section and nothing else. narrate wrote them beside the take as <digest>.words.json, in seconds after the take starts. cue moves them onto the section’s own clock, which begins at its lead. decktalk words prints them on that clock. Matching rules says how a phrase meets a spoken word: case, punctuation, a hyphenated word and a word the voice spells out. occurrence picks which match, counting from 1, and with no occurrence the cue takes the first and cue names every second the phrase occurs at.

offset_seconds

offset_seconds is added to the matched moment, and it may be negative. The result is never allowed before the section’s own start: a cue that would resolve earlier than zero lands at zero instead, which is the case CUE_NO_ONSET names, because a second that is the section’s start rather than a word’s has nothing to measure an onset against. offset_seconds here is not the one of a sound effect. A [[mix.effect]] row in decktalk.toml carries its own offset_seconds, which shifts the sound and not the picture.

The two anchors

Both anchors are literal strings, so a phrase is never mistaken for one. Neither is affected by occurrence or case_sensitive. A second counts from the start of its own section, and the section starts where its silence starts. A section’s lead_seconds therefore moves every word, and so every phrase cue and $end, that much later, while $start stays at zero.

What resolves a cue

decktalk cue reads the words each section speaks off the take index and writes build/cue-times.json, one row per cue with the cue id, the phrase it matched, the second it fires and the offset that was added. That file is what record, assemble and verify read, and no stage works a second out from cues.json again. Three findings come out of that pass.

The two files read against each other

A cue and the moment it fires are one thing written in two places, so each is read against the other. The page’s side is the catalog its runtime publishes, which names every slide of a scene and every moment its elements declare, already qualified into a cue id. It is read from that catalog and never from a scan of the markup, because data-spotlight, data-class and data-owns all declare moments no text scan can see. decktalk check makes the comparison against a catalog it reads live, and names every moment that this file has no row for.
decktalk check --fix applies that fix, and Project.apply(finding) applies it from the library. The fix only ever adds: it writes one row per moment nothing lists, it never rewrites a phrase you wrote and it never deletes a row, so applying it a second time adds nothing, because by then every moment of that section is listed. The row it writes leaves phrase empty, because the phrase a cue lands on is your line and not DeckTalk’s. The file still loads with an empty phrase, and the row raises CUE_UNRESOLVED until you write the phrase. A row whose cue id no page declares is the mirror of it, CUE_UNKNOWN. It is named rather than deleted. When exactly one unlisted row sits beside exactly one undeclared moment in the same section, the two are read as a rename, because the phrase is what survives a rename, and the fix changes the id alone.