Skip to content

Scene scripts

A scene script is the contract. It holds what your characters say, who says it, how long the picture is, and — importantly — the direction that justifies every threshold.

docs/scenes/night-street.json
{
"name": "night-street",
"note": "The Director's staging, verbatim. Do not rewrite the lines.",
"clip_duration_s": 10.062,
"max_gap_within_line_s": 0.5,
"cast": {
"VOICE": "off-frame, deep and gritty",
"MAC": "on-frame, gritty, weary"
},
"lines": [
{ "speaker": "VOICE", "text": "Hey, how's it going?" },
{ "speaker": "MAC", "text": "Not bad. Can't complain.",
"max_gap_s": 0.15,
"direction": "There's no pause in between. A gap here runs into VOICE's next cue." },
{ "speaker": "VOICE", "text": "Good to hear, good to hear." },
{ "speaker": "VOICE", "text": "Hey, tell Charlie I got that thing for him, whenever he wants to drop by." }
]
}
FieldMeaning
clip_duration_sThe picture’s length. Speech must end before it.
max_gap_within_line_sDefault budget for a silence inside one line. Defaults to 0.5.
min_gap_between_speakers_sMinimum silence between turns. Defaults to 0.0.
castFree-form notes per character. Documentation, not checked.
linesThe ordered dialogue.
FieldMeaning
speakerCharacter name. Case-sensitive--only-speaker mac will not match MAC.
textWhat they say. Compared with punctuation and case stripped.
max_gap_sOverrides the scene default for this line only.
directionWhy that override exists. Never checked; always read.

Why the default is loose and the override is tight

Section titled “Why the default is loose and the override is tight”

The scene default is 0.5 s on purpose. A comma pause inside a line is normal delivery, not a defect, and a verifier that flags every one of them is noise people learn to ignore.

Where a director has specified the phrasing — “there’s no pause in between” — that is a fact about this line, not a global policy. It belongs on the line:

{ "speaker": "MAC", "text": "Not bad. Can't complain.",
"max_gap_s": 0.15,
"direction": "There's no pause in between. A gap here runs into VOICE's next cue." }

Now the verifier rejects a take that a global threshold would wave through, and six months from now the direction field explains why 0.15 and not 0.4.

Words are compared with punctuation and case removed, apostrophes kept:

  • "Not bad. Can't complain." matches a transcript of not bad can't complain
  • A comma where the model produced a full stop is not a content defect
  • A missing or extra word is

This matters because your TTS engine chooses its own punctuation and your ASR guesses at it. Neither is the thing you are verifying.

Terminal window
fxdub-dialogue scene.json voice-stem-words.json --only-speaker VOICE

This narrows the contract to VOICE’s three lines. Any other speech in that stem now fails no_invented_speech, because a per-character stem is supposed to contain that character and silence.

A misspelled or absent speaker name exits 2, not 0 — an empty contract passes every check vacuously, which is the most dangerous possible result:

Terminal window
$ fxdub-dialogue scene.json words.json --only-speaker NARRATOR
{
"error": {
"code": "unknown_speaker",
"message": "No line in scene.json is spoken by 'NARRATOR'.",
"hint": "Cast in this scene: MAC, VOICE. Speaker names are case-sensitive."
}
}