Your dub passed every check and still said the wrong line.
48 kHz, exact duration, textbook −18 LUFS — and a line you never wrote, in a voice that is not your character’s, with a two-second hole in the middle. fx-dub gives you two receipts: one for the container, one for what was actually said.
Install
pip install fx-dub
Check the content
fxdub-dialogue scene.json words.json --only-speaker VOICE
Check the container
fxdub-receipt runs/2026-08-22-final --json receipt.json
Two receipts, because one is not enough
Sample rate and duration cannot see a model inventing dialogue. Both of the takes that made this tool necessary passed every container check.
Content, not just container
Every scripted line present and in order, no invented speech, no cross-character overlap, no mid-line pause that eats the next cue, one voice per character.
The scene script is the contract
Dialogue, cast, clip duration and per-line delivery direction live as data. A director’s phrasing note travels with the number it justifies.
Per-character stems
--only-speaker narrows the contract to one character, so a stem is checked for their lines and silence everywhere else. Checking against the whole scene hides exactly that bug.
Standards, not vibes
EBU R128 loudness, ATSC A/85 dialogue anchoring, JAES ducking-depth research. Every check cites the standard or the measured defect it traces to.
No network, no dependencies
Zero runtime dependencies and no HTTP client — CI fails the build if that ever changes. FLAC and MP4 headers are decoded with the standard library, not by shelling out to ffprobe.
Exit codes CI can act on
0 pass · 1 the audio failed its contract, read the receipt · 2 the tool could not run. Errors carry code, message and hint — never a raw traceback.
Usage
Install
pip install fx-dub Check what a take actually says
$ fxdub-dialogue docs/scenes/night-street.json words.json --only-speaker VOICE
9/10 checks pass
| PASS | line_present:0:VOICE | Hey, how’s it going?
| FAIL | no_invented_speech | 4 unscripted word(s): not bad can’t complain
| PASS | no_overlap | clean
| PASS | one_voice_per_character | clean A scene script
{
"clip_duration_s": 10.062,
"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." },
{ "speaker": "VOICE", "text": "Good to hear, good to hear." }
]
} Check the delivered run
$ fxdub-receipt runs/2026-08-22-v28 --bed-gain-db -12
19/19 checks passed.
| PASS | loudness:mix_target | -18.09 LUFS (target -18.0 +/- 2.0)
| PASS | loudness:dialogue_anchored| VO - bed = +11.17 LU (expect 8-20)
| PASS | dub:frames_intact | 161 frames Build a graph without submitting it
from fxdub import vo_graphs
graph = vo_graphs.transcribe(".flac", "run/words")
# API-format dict, ready for your own submit path.
# Nothing is sent, uploaded, or spent from here.