You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Episodes can't be loudness-matched: assemble-and-score doesn't expose target_lufs; skill should match downward #467
The series-episodes skill says to compare integrated_lufs across episodes and to use normalize_audio's target_lufs when they need to match by ear. In practice, peak-normalized dialogue episodes with laugh tracks can't be raised to match, because the -3 dBFS peak ceiling holds them.
Evidence (Known Issues, all cut with the same assemble-and-score recipe and peak_dbfs: -3)
episode
integrated LUFS
peak dBFS
rack-four (shipped cut, 2026-09-17)
−17.5
−1.04
7 Rollback (rollback-cut)
−20.7
−2.94
8 Unknown Job (unknown-job-cut)
−18.9
−3.20
Rollback's tag has a 22.7 dB crest from a studio-audience laugh. That laugh sets the peak, so gaining the episode to −18.9 LUFS would put it above the ceiling. target_lufs then holds it at the ceiling, and it stays about 1.8 LU quieter than the episode next to it.
Ask
A limiter: either a limit_audio task or a limit: true mode on normalize_audio (look-ahead, true-peak aware) that reaches target_lufs by squashing the few laugh peaks instead of giving up at the ceiling.
Until that exists, a line in the series-episodes skill saying that peak-normalized episodes can differ by about 2 LU, and that target_lufs alone can't close the gap when one peak sets the level.
Approved as a fix, not a feature. The episodes can be matched: match them downward. The limiter is split out as #474, an idea for the feature lead.
Why they can already match.normalize_audio only caps gain that would push a peak past peak_dbfs (dw/tasks/audio_utils.py). Lowering gain is never capped. With target_lufs: -21 on all three episodes, each one lands at -21:
rack-four comes down 3.5 dB.
Unknown Job comes down 1.8 dB.
Rollback comes up 0.3 dB, which stays under -3.
-21 LUFS is a sound level for dialogue with a live audience.
The actual gap.workflows/templates/assemble-and-score.json hardcodes peak_dbfs: -3.0 on the balanced step and doesn't expose target_lufs. The series-episodes skill tells the agent to use target_lufs, but the recipe gives it no way to pass one.
Fix
Template. Add an optional target_lufs variable to assemble-and-score, passed to the balanced step's normalize_audio. When it's unset, behavior is unchanged. Keep peak_dbfs: -3.0.
Skill (plugins/dw/skills/series-episodes/SKILL.md, the normalize bullet and the loudness paragraph under "Run and judge"):
Replace "use target_lufs when they need to match" with a working rule: pick one series loudness that every episode can reach, which in practice is about the quietest one the ceiling holds back, and pass that same target_lufs on every episode.
A target_lufs_capped warning means that episode sets the series' limit. Lower the target for the whole series; don't accept the mismatch.
Tests. Add a validation or template test showing target_lufs reaches the step. Update any test that pins the template's variables.
Deploy path: the template and the skill are served by the server and the plugin, so merge to develop and deploy as usual.
Verification (tester). Recut Rollback and Unknown Job, and ideally rack-four too, through assemble-and-score with the same target_lufs (about -21). get_gallery_metadata should give integrated_lufs within about 0.5 LU across the episodes, with no target_lufs_capped warning. Omitting the variable should still give the old peak-only result.
Reviewed with Claude Opus 5.5 (claude-opus-5-5), anthropic, against origin/develop91abe3e.
changed the title [-]Episodes can't be loudness-matched: -3 dBFS peak ceiling blocks target_lufs (Ep7 -20.7 vs Ep8 -18.9 LUFS) - add a limiter[/-][+]Episodes can't be loudness-matched: assemble-and-score doesn't expose target_lufs; skill should match downward[/+]on Sep 26, 2026
triage: work. This is approved by Don above as a fix: expose target_lufs on assemble-and-score, and have the series-episodes skill match episodes downward. The limiter is split out as #474. — claude-opus-5-5 via anthropic (implementer, triage)
Fixed and pushed to develop (6ddec23, formatting fixup in 4bfb747).
workflows/templates/assemble-and-score.json: added an optional target_lufs variable (default null, unset behavior unchanged) wired into the balanced step's normalize_audio call, alongside the still-hardcoded peak_dbfs: -3.0 ceiling. Description updated to explain the interaction (gain-down always works to match a series downward; gain-up can be capped, and warns target_lufs_capped).
plugins/dw/skills/series-episodes/SKILL.md: rewrote the normalize bullet and the loudness paragraph under "Run and judge" to state the match-downward rule directly - pick one series target_lufs the quietest episode's ceiling allows, pass it to every episode, and treat a target_lufs_capped warning as "lower the series target" rather than an accepted mismatch. Notes the limiter (Limiter for normalize_audio: reach target_lufs past the peak ceiling (split from #467) #474) as the only way to go louder than a capped episode's ceiling.
tests/test_assemble_and_score_target_lufs.py (new): exercises the real resolve_variable_values / replace_variables substitution path against the actual template file - confirms target_lufs reaches the balanced step's arguments when set, that peak_dbfs stays at -3.0 regardless, and that omitting target_lufs drops the key entirely (so normalize_audio falls back to its old peak-only default, matching Don's "omitting the variable should still give the old peak-only result" verification note).
Full test suite (6712 passed) and python -m dw.validate on the template both clean.
Deploy path: this is a template + plugin skill fix (target:local loop) - served by the server and refreshed from origin/develop for the plugin, no code path change, so the driver's next deploy of develop picks it up; no server restart needed for the skill, template needs the next deploy cycle to be picked up by list_workflows/run_workflow.
Suggested verification (from Don's comment): recut Rollback and Unknown Job (and ideally rack-four) through assemble-and-score with the same target_lufs (~-21), then get_gallery_metadata should show integrated_lufs within ~0.5 LU across episodes with no target_lufs_capped warning; omitting target_lufs should reproduce the old peak-only result.
Verified on the local server (mps on Mac-mini.lan, develop @ 3e8bf8d), over MCP.
The Rollback, Unknown Job and rack-four shots don't exist on this server. I stood in two cuts built from synced qa-cast fixtures: two 2-shot "episodes" that differ in loudness. Both use templates/assemble-and-score, total_frames: 240, score asset:qa-cast/ep68-score.mp3.
A:ep62-shot1-accuse + ep62-shot2-deflect
B:ep42-shot1-accuse + ep42-shot2-deflect, with score_start_frame: 300 and score_gain: 0.5
run
target_lufs
integrated_lufs
peak dBFS
warnings
A
unset
-19.15
-3.12
only concat's spread warning (old peak-only result ✓)
A
-21
-21.03
-5.07
none
B
-21
-22.98
-3.09
balanced: normalize_audio: target_lufs=-21 would need -4.6 dB of gain, but peak_dbfs=-3.0 caps it at -6.5 dB - 1.9 LU short of the target.
A
-23
-23.05
-7.06
none
B
-23
-23.06
-3.14
none
get_workflow("templates/assemble-and-score") shows target_lufs: null in variables, wired into balanced's normalize_audio next to peak_dbfs: -3.0.
With the variable unset, the result is still peak-only at -3 dBFS.
One shared target_lufs matched the two cuts to 0.01 LU (-23.05 / -23.06), with no capped warning.
The served dw:series-episodes skill (plugin tree at develop) has the match-downward rule in the normalize bullet and in "Run and judge". Following it closed the gap: B was capped at -21, so I lowered the series target to -23.
One wording point, not a bounce, filed separately: the template description and the skill both say "gain down always succeeds". Cut B at -21 was a gain-down case, and it still got held further down by the ceiling and warned. Its pre-normalize mix peaked above -3 dBFS. The skill's rule ("a capped warning → lower the series target") handled it. The absolute claim is what's inaccurate.
Scratch runs deleted. No suite case added: this is a template/skill behavior that the fixtures here only stand in for.
Activity
dkackman commentedon Sep 26, 2026
Approved as a fix, not a feature. The episodes can be matched: match them downward. The limiter is split out as #474, an
ideafor the feature lead.Why they can already match.
normalize_audioonly caps gain that would push a peak pastpeak_dbfs(dw/tasks/audio_utils.py). Lowering gain is never capped. Withtarget_lufs: -21on all three episodes, each one lands at -21:-21 LUFS is a sound level for dialogue with a live audience.
The actual gap.
workflows/templates/assemble-and-score.jsonhardcodespeak_dbfs: -3.0on thebalancedstep and doesn't exposetarget_lufs. The series-episodes skill tells the agent to usetarget_lufs, but the recipe gives it no way to pass one.Fix
target_lufsvariable toassemble-and-score, passed to thebalancedstep'snormalize_audio. When it's unset, behavior is unchanged. Keeppeak_dbfs: -3.0.plugins/dw/skills/series-episodes/SKILL.md, the normalize bullet and the loudness paragraph under "Run and judge"):target_lufson every episode.target_lufs_cappedwarning means that episode sets the series' limit. Lower the target for the whole series; don't accept the mismatch.target_lufsreaches the step. Update any test that pins the template's variables.Deploy path: the template and the skill are served by the server and the plugin, so merge to
developand deploy as usual.Verification (tester). Recut Rollback and Unknown Job, and ideally rack-four too, through
assemble-and-scorewith the sametarget_lufs(about -21).get_gallery_metadatashould giveintegrated_lufswithin about 0.5 LU across the episodes, with notarget_lufs_cappedwarning. Omitting the variable should still give the old peak-only result.Reviewed with Claude Opus 5.5 (
claude-opus-5-5), anthropic, againstorigin/develop91abe3e.[-]Episodes can't be loudness-matched: -3 dBFS peak ceiling blocks target_lufs (Ep7 -20.7 vs Ep8 -18.9 LUFS) - add a limiter[/-][+]Episodes can't be loudness-matched: assemble-and-score doesn't expose target_lufs; skill should match downward[/+]dkackman commentedon Sep 26, 2026
triage: work. This is approved by Don above as a fix: expose
target_lufsonassemble-and-score, and have the series-episodes skill match episodes downward. The limiter is split out as #474. — claude-opus-5-5 via anthropic (implementer, triage)fix(templates,plugin): #467 - expose target_lufs on assemble-and-scor…
style: #467 - ruff format test_assemble_and_score_target_lufs.py
dkackman commentedon Sep 26, 2026
Fixed and pushed to develop (6ddec23, formatting fixup in 4bfb747).
workflows/templates/assemble-and-score.json: added an optionaltarget_lufsvariable (defaultnull, unset behavior unchanged) wired into thebalancedstep'snormalize_audiocall, alongside the still-hardcodedpeak_dbfs: -3.0ceiling. Description updated to explain the interaction (gain-down always works to match a series downward; gain-up can be capped, and warnstarget_lufs_capped).plugins/dw/skills/series-episodes/SKILL.md: rewrote the normalize bullet and the loudness paragraph under "Run and judge" to state the match-downward rule directly - pick one seriestarget_lufsthe quietest episode's ceiling allows, pass it to every episode, and treat atarget_lufs_cappedwarning as "lower the series target" rather than an accepted mismatch. Notes the limiter (Limiter for normalize_audio: reach target_lufs past the peak ceiling (split from #467) #474) as the only way to go louder than a capped episode's ceiling.tests/test_assemble_and_score_target_lufs.py(new): exercises the realresolve_variable_values/replace_variablessubstitution path against the actual template file - confirmstarget_lufsreaches thebalancedstep's arguments when set, thatpeak_dbfsstays at -3.0 regardless, and that omittingtarget_lufsdrops the key entirely (sonormalize_audiofalls back to its old peak-only default, matching Don's "omitting the variable should still give the old peak-only result" verification note).Full test suite (6712 passed) and
python -m dw.validateon the template both clean.Deploy path: this is a template + plugin skill fix (
target:localloop) - served by the server and refreshed fromorigin/developfor the plugin, no code path change, so the driver's next deploy ofdeveloppicks it up; no server restart needed for the skill, template needs the next deploy cycle to be picked up bylist_workflows/run_workflow.Suggested verification (from Don's comment): recut Rollback and Unknown Job (and ideally rack-four) through
assemble-and-scorewith the sametarget_lufs(~-21), thenget_gallery_metadatashould showintegrated_lufswithin ~0.5 LU across episodes with notarget_lufs_cappedwarning; omittingtarget_lufsshould reproduce the old peak-only result.— claude-sonnet-5 via anthropic (implementer)
dkackman commentedon Sep 27, 2026
Verified on the local server (mps on Mac-mini.lan, develop @ 3e8bf8d), over MCP.
The Rollback, Unknown Job and rack-four shots don't exist on this server. I stood in two cuts built from synced
qa-castfixtures: two 2-shot "episodes" that differ in loudness. Both usetemplates/assemble-and-score,total_frames: 240, scoreasset:qa-cast/ep68-score.mp3.ep62-shot1-accuse+ep62-shot2-deflectep42-shot1-accuse+ep42-shot2-deflect, withscore_start_frame: 300andscore_gain: 0.5balanced: normalize_audio: target_lufs=-21 would need -4.6 dB of gain, but peak_dbfs=-3.0 caps it at -6.5 dB - 1.9 LU short of the target.get_workflow("templates/assemble-and-score")showstarget_lufs: nullinvariables, wired intobalanced'snormalize_audionext topeak_dbfs: -3.0.target_lufsmatched the two cuts to 0.01 LU (-23.05 / -23.06), with no capped warning.dw:series-episodesskill (plugin tree at develop) has the match-downward rule in the normalize bullet and in "Run and judge". Following it closed the gap: B was capped at -21, so I lowered the series target to -23.One wording point, not a bounce, filed separately: the template description and the skill both say "gain down always succeeds". Cut B at -21 was a gain-down case, and it still got held further down by the ceiling and warned. Its pre-normalize mix peaked above -3 dBFS. The skill's rule ("a capped warning → lower the series target") handled it. The absolute claim is what's inaccurate.
Scratch runs deleted. No suite case added: this is a template/skill behavior that the fixtures here only stand in for.
— claude-opus-5-5 via anthropic (tester)