Skip to content

Verifying a run

Run both receipts. They answer different questions, and a take can pass one while failing the other in a way that costs a review cycle.

Terminal window
fxdub-dialogue scene.json words.json [--only-speaker NAME] [--json out.json]
CheckFails whenTraces to
line_present:N:SPEAKERa scripted line is missing or out of orderthe scene script is the contract
no_invented_speechthe take contains words no line asked forreference-mode models reproduce their reference’s dialogue, not just its timbre
no_overlaptwo lines’ spans intersecttwo characters talking over each other is the worst-sounding failure
no_internal_stragglea silence inside one line exceeds its budgeta mid-line pause eats the next character’s slot
turn_gapsa turn change is tighter than min_gap_between_speakers_sscene pacing
one_voice_per_characterone character is rendered by more than one voicea character re-cast between renders is not a character
characters_are_distincttwo characters share a voicenode-global pitch collapses two speakers into one
fits_clipspeech runs past clip_duration_sthe dub may not outrun the picture
Terminal window
$ fxdub-dialogue scene.json words.json --only-speaker VOICE
9/10 checks pass
| FAIL | no_invented_speech | 4 unscripted word(s): not bad can't complain

Four words appear in the VOICE stem that belong to MAC. The stem is contaminated; mixing it produces two men saying the same line a quarter-second apart.

Terminal window
fxdub-receipt <run_dir> [--bed-gain-db N] [--json out.json]
GroupChecks
Deliverablesmix, both stems, three LUFS manifests, caption, dubbed MP4 all present
Ratesmix and bed at 48 kHz; VO stem at 48 kHz or a known TTS-native rate; delivered rates agree
Loudnessmix within ±2.0 LU of −18.0; dialogue 8–20 LU above the bed
Videothe dub carries both a video and an audio track; frames intact; duration matches
Captionthe semantic intermediate reached the manifest and is non-empty
Terminal window
fxdub-receipt runs/my-run --bed-gain-db -12

EBU R128 integrated loudness gates a quiet bed out of the mix master entirely, so ducking depth is unmeasurable without a meter on the bed stem — and that stem reads pre-gain. Pass the mix gain you applied, or the separation figure is wrong.

Gain-stage from the meter, never from memory

Section titled “Gain-stage from the meter, never from memory”

The most expensive mixing mistake available here is reusing a working recipe’s numbers after changing engines.

VO sourceMeasured on the same line
ElevenLabs−18.34 LUFS
ByteDance−25.03 LUFS

6.7 dB apart. Applying the first recipe’s gain to the second stem buries the dialogue — while sample rate, duration and frame count all stay green.

Measure the stem, then compute:

VO gain = target_vo − measured_vo
bed gain = (target_vo − desired_separation) − measured_bed

Worked example from the delivered run: VO measured −25.03, target −18.0, so +7 dB. Bed measured −17.20, wanted ~11 LU below the VO, so −12 dB. Result: mix −18.09 LUFS, separation +11.17 LU. Both inside contract, first try.

CodeMeaning
0every check passed
1the audio failed its contract — read the receipt
2the tool could not run

1 and 2 are deliberately distinct. In CI the first wants its receipt read and a human to hear the audio; the second means the invocation is wrong and no audio was ever examined. Collapsing them turns a broken pipeline into a silent green build.

Errors print the structured shape on stderr:

{
"error": {
"code": "words_not_found",
"message": "No transcript at 'run/words.json'.",
"hint": "Produce one with vo_graphs.transcribe() and save output slot 2 (the word list) via SaveText."
}
}

Pass --debug to re-raise instead of formatting.

--json out.json writes the full result: every check with its ok flag, detail string and traces_to provenance, plus a measured block with the raw numbers. That file is designed to be archived beside the run — it is the artifact that lets someone six months later see not just that a take passed, but what it measured.