Skip to content

Episodes can't be loudness-matched: assemble-and-score doesn't expose target_lufs; skill should match downward #467

Closed

Description

@dkackman

Problem

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.

Activity

  1. added
    owner:donParked for human decision
    status:needs-approvalParked pending a written proposal + human approval
    featureWork bigger than a fix: designed with Don, built in stages (harnest R11)
    on Sep 25, 2026
  2. dkackman commented on Sep 26, 2026

    @dkackman
    OwnerAuthor

    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

    1. 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.
    2. 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.
      • Reaching a louder target than the ceiling allows would need a limiter (Limiter for normalize_audio: reach target_lufs past the peak ceiling (split from #467) #474).
    3. 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/develop 91abe3e.

  3. 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
  4. added
    bugSomething isn't working
    target:localFound against a non-lem server (DW_TARGET=local); Don triages
    and removed
    owner:donParked for human decision
    status:needs-approvalParked pending a written proposal + human approval
    featureWork bigger than a fix: designed with Don, built in stages (harnest R11)
    on Sep 26, 2026
  5. dkackman commented on Sep 26, 2026

    @dkackman
    OwnerAuthor

    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)

  6. added
    target:localFound against a non-lem server (DW_TARGET=local); Don triages
    and removed
    target:localFound against a non-lem server (DW_TARGET=local); Don triages
    on Sep 26, 2026
  7. added 2 commits that reference this issue on Sep 26, 2026
  8. dkackman commented on Sep 26, 2026

    @dkackman
    OwnerAuthor

    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.

    — claude-sonnet-5 via anthropic (implementer)

  9. dkackman commented on Sep 27, 2026

    @dkackman
    OwnerAuthor

    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.

    — claude-opus-5-5 via anthropic (tester)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    backend:sharedbugSomething isn't workingowner:testerTester's turn to actstatus:verifiedTester confirmed the fix via a real MCP calltarget:localFound against a non-lem server (DW_TARGET=local); Don triagesverified-on:mps

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions