Skip to content

Limiter for normalize_audio: reach target_lufs past the peak ceiling (split from #467) #474

Closed
2 of 2 issues completed

Description

@dkackman

Split from #467. #467 is now a fix for the template and the skill: episodes are matched downward to a series loudness that every episode can reach. This issue is the other half.

Idea

Add a limiter to the audio tasks, either as a new limit_audio task or as a limit: true mode on normalize_audio. It would let target_lufs go past the peak ceiling by squashing a few transient peaks, for example the studio-audience laugh in #467. That laugh has a 22.7 dB crest and holds Rollback at -20.7 LUFS with a -3 dBFS peak.

Why it's an idea, not a fix

Loudness matching doesn't need a limiter: target_lufs can always lower gain (see #467). A limiter only matters if a series should sit louder than its most dynamic episode allows, for example the -14 to -16 LUFS streaming platforms target, compared with the roughly -21 LUFS these episodes reach without one.

What a design would have to settle

  • True-peak detection with oversampling. normalize_audio and get_gallery_metadata measure sample peak, and AAC encoding can overshoot between samples.
  • Look-ahead, attack and release, so it doesn't pump on laughter or music.
  • A dependency or in-house DSP. pyloudnorm measures loudness but doesn't limit.
  • How it's reported: gain reduction applied, and how many samples it touched.
  • Whether there's demand. So far one series has hit the ceiling, and matching downward solved it.

Filed by Claude Opus 5.5 (claude-opus-5-5, anthropic) at Don's direction.

Activity

  1. added
    ideaAn idea for the researcher agent to assess
    owner:leadFeature lead's turn (harnest R11)
    on Sep 26, 2026
  2. dkackman commented on Sep 26, 2026

    @dkackman
    OwnerAuthor

    Implementation input from an Opus 5.5 field session (local ffmpeg assembly of a musical short, 2026-09-26): ffmpeg's alimiter auto-levels its output up to the limit by default. After an AAC encode on top, that clipped. With level=disabled and a -3 dB ceiling, the final came out at -16.4 LUFS integrated, peak -2.7 dBFS. If the limiter here uses alimiter, set level=disabled, or the limiter itself becomes a gain stage.

  3. added a commit that references this issue on Sep 26, 2026
  4. dkackman commented on Sep 26, 2026

    @dkackman
    OwnerAuthor

    Plan v2: a true-peak limit mode for normalize_audio

    Designed by Claude Opus 5.5 (claude-opus-5-5, anthropic) against origin/develop 0540972. v2 (same model, same provider) re-plans stage 2 after it bounced at C-F135 on #497: the limited mix measures -3.0 dBTP, but the AAC mux of the film comes out at -2.54 dBTP. See Q6.

    Status: stage 1 (#496) is verified and stands. Stage 2 (#497) is merged at 2122558. Its template variable and skill lines passed (C-F136). Only the film's true-peak check failed.

    Verdict: build smaller

    Build it as one limit: true argument on normalize_audio: a look-ahead, true-peak limiter with fixed timings. That lets target_lufs go past the peak_dbfs ceiling. Don't add a limit_audio task and don't expose attack, release or look-ahead knobs. After that, one small stage exposes it on assemble-and-score and in the series-episodes skill.

    Correction to the idea: dw already has a limiter. compress_audio(mode="limit") (dw/tasks/audio_utils.py:1573, added in #200/#207) is a sample-peak envelope follower. It has no look-ahead, so with attack_ms > 0 the start of every transient gets through above the threshold, and it reports nothing. No template or skill uses it. It can't do this job because it lets the laugh through, but it bears on Q2.

    Value

    • Who gains: Don's driving sessions that finish a series or a song for playback at platform loudness (-14 to -16 LUFS). Today those land around -21 whenever one transient sets the peak.
    • Evidence (two field sessions):
    • Real use: Don's workspaces include several series and music productions (rack-four, shedding-skin-quartet, encanto-uprising, inner-space, acorn-wars, and others). I excluded qa-* and regression-*. Any of them could hit the same ceiling as soon as a louder target is asked for.
    • How often: occasional, at the finishing step of a piece rather than on every run. Demand is modest but real, and it is growing with the series work.

    Build cost

    Stage 1 is about $4–6 and stage 2 about $2–3, so about $6–9 in total.

    Inertia

    • Surface: one boolean argument on one task, plus one new constraint value (limiter) and new fields on normalize_audio's existing normalize_audio (target_lufs), grade, loop_audio log nothing about what they applied — a save:false step's effect is unobservable #392 log event. There is no new task, MCP tool or syntax. The task list isn't in the MCP surface budget (tests/test_mcp_server.py:1502), so the budget is unaffected.
    • Maintenance: one private DSP routine (about 60 lines of numpy/scipy) whose correctness has to stay pinned by tests, mainly that the output true peak is ≤ the ceiling.
    • Dependency: scipy is already imported at module level by dw/loudness.py:19, but it is only installed transitively through controlnet-aux. Stage 1 declares it in pyproject.toml. That fixes an existing latent gap as well.
    • Concepts and security: no new concept (dw already has dynamics tasks), and no security boundary is touched.

    Reversibility

    The argument defaults to false, so behaviour is unchanged for everyone who doesn't set it. Removing it later would break only workflows that set it: stage 2's template and anything an agent saved. That is small, but it would still be a breaking-change.

    Cheaper alternatives

    • Do nothing. Series stay around -21 LUFS, as Episodes can't be loudness-matched: assemble-and-score doesn't expose target_lufs; skill should match downward #467's downward matching gives. Anything louder means leaving dw, as idea: musical join task - spoken scene into song with the song entering under the last line, as a run output with provenance #486 did, and losing provenance.
    • Document compress_audio(mode="limit") before normalize_audio(target_lufs). This costs nothing to build, but it doesn't work: without look-ahead the transient gets through, so the true peak stays over the ceiling and normalize_audio still caps. The agent also has to pick a threshold by hand.
    • Shell out to ffmpeg alimiter. That would be the first ffmpeg subprocess in the engine (none exists today; imageio-ffmpeg bundles the binary). It auto-levels unless level=disabled, and it adds a process boundary and argument sanitising. All of that buys less than about 60 lines of numpy.
    • A standalone limit_audio task. It gets the same result with more surface: a new task, docs and domains, plus a step_cache.NORMALIZING_COMMANDS entry (dw/step_cache.py:115). The agent also has to order the two steps correctly, gain first and then limit. As a mode on normalize_audio, the one step that sets the level also holds the ceiling.

    Overlap

    Proposed follow-ups (not in this plan; Don decides whether each becomes an issue)

    • compress_audio(mode="limit") earns nothing outside its own tests. See Q2.
    • warn_if_written_above_full_scale (dw/result.py:223) judges the encoded file on sample peak, although probe_media already measures true_peak_dbfs. That is a separate question from this feature.

    Problem and non-goals

    Problem. normalize_audio(target_lufs=T, peak_dbfs=C) applies min(T - measured, C - sample_peak) as one static gain (audio_utils.py:1458). When one transient sets the peak, the track can't reach T, and the step warns target_lufs_capped.

    Non-goals:

    • a general mastering chain (multiband, stereo processing, compression below the limit);
    • user-tunable limiter timings;
    • changing any default;
    • changing compress_audio;
    • changing how the encoded file is checked.

    The design

    Engine: normalize_audio gets limit=False

    The signature becomes normalize_audio(audio, peak_dbfs=-1.0, target_lufs=None, limit=False, sample_rate=None). Positional use is unaffected in practice, because templates pass keywords.

    With limit=True:

    1. What peak_dbfs means. It becomes a true-peak ceiling: dBTP, measured with 4x oversampling (BS.1770, dw/loudness.py). It is no longer a sample-peak ceiling.
    2. Gain. The gain is T - measured_lufs when target_lufs is given. Without target_lufs, it is the peak-normalising gain as today. The gain is applied without the ceiling cap. The makeup gain comes only from target_lufs, so the limiter never becomes a gain stage. That is the alimiter auto-level trap from the comment above, avoided by construction.
    3. The limiter (a private routine in audio_utils.py):
      • Detection: linked across channels (the loudest channel drives everything, so the stereo image doesn't shift). It reads the per-sample true peak from a 4x resample_poly oversample.
      • Required gain: per sample, min(1, ceiling / peak).
      • Look-ahead: a sliding minimum over the look-ahead window, followed by a boxcar of the same length. The gain therefore ramps down before the transient and is ≤ what's required at every sample.
      • Release: a hold plus a smoothed release.
      • Timings (fixed): about 5 ms look-ahead and about 150 ms release. They are long enough not to pump on a laugh and short enough not to duck the line after it. The builder may tune them within that intent and pins the chosen numbers in tests.
      • Speed: fully vectorised (scipy.ndimage / numpy). There is no per-sample Python loop; the 2026-09-22 quality review flagged _follow_envelope's loop.
      • Delay: the output isn't delayed. The look-ahead is applied to the gain curve, never to the signal, so sync with picture is untouched.
    4. Check pass. Re-measure the output's true peak. If reconstruction still leaves it above the ceiling, apply one static trim to meet the ceiling. Then re-measure LUFS. Limiting lowers loudness a little, so make one correction pass: add the shortfall to the gain and limit again.
    5. A cap on how hard it squashes. Gain reduction never exceeds 12 dB. If reaching T would need more, the gain stops at what 12 dB of reduction allows. The existing target_lufs_capped warning then fires with limited: true and shortfall_lu, so a request like "-10 LUFS on a dialogue track" doesn't silently destroy it.
    6. Reporting. This extends the existing normalize_audio (target_lufs), grade, loop_audio log nothing about what they applied — a save:false step's effect is unobservable #392 emit_log: constraint: "limiter", gain_db, max_gain_reduction_db, limited_fraction (share of samples reduced), output_true_peak_dbfs and output_lufs. A new warning kind, limiter_heavy, fires when max_gain_reduction_db > 6, because pumping becomes audible around there.
    7. When the ceiling isn't in the way, the limiter touches nothing (limited_fraction 0). The output then equals the limit=False output, apart from the true-peak vs sample-peak difference when target_lufs isn't given.

    limit=False (the default) is byte-for-byte today's behaviour, including target_lufs_capped and the peak_ceiling constraint.

    Other surfaces

    • MCP, REST, syntax: none.
    • step_cache.NORMALIZING_COMMANDS: unchanged, since this is the same command.
    • Schema: get_task's schema picks up limit from the signature (dw/introspection.py:428).
    • task_domains.py: no entry, because a boolean has no numeric domain.

    Stages

    Stage 1: normalize_audio(limit=true) (server)

    • Builds: the engine design above.
    • Tests (tests/test_audio_utils.py):
      • the output true peak is ≤ the ceiling (+0.05 dB tolerance) on a synthetic track with a +20 dB crest transient over a steady bed;
      • LUFS lands within 0.5 LU of the target;
      • the 12 dB cap produces target_lufs_capped with limited: true;
      • limiter_heavy fires above 6 dB of reduction;
      • no reduction happens when the ceiling isn't in the way;
      • the default path is unchanged (the existing tests at :671–777 still pass untouched);
      • a silent track is returned unchanged;
      • the output has the same length as the input (no delay);
      • timing: a 3-minute stereo 48 kHz track limits in a few seconds.
    • Other changes:
      • declare scipy in pyproject.toml;
      • docs/TASKS.md: add target_lufs and limit rows to the normalize_audio table (the target_lufs row is missing today) and a line under compress_audio saying its limit mode is sample-peak with no look-ahead, and pointing to normalize_audio(limit=true) for a true-peak ceiling;
      • update the normalize_audio bullets in CLAUDE.md.
    • Deploy: server.
    • Estimate: $4–6.
    • Acceptance intent (over MCP). The tester runs inline workflows on a track whose crest makes target_lufs cap: an uploaded or kept laugh-track episode, or any dialogue cut that warns target_lufs_capped at peak_dbfs: -3.
      • limit omitted: same result and warnings as today (target_lufs_capped, and the log constraint: "peak_ceiling").
      • target_lufs: -16, peak_dbfs: -3, limit: true, saved as wav:
        • get_gallery_metadata gives integrated_lufs within 0.5 LU of -16 and true_peak_dbfs ≤ -3.0 (+0.1);
        • the job's events carry a log with constraint: "limiter", max_gain_reduction_db > 0 and limited_fraction between 0 and 1;
        • there is no target_lufs_capped warning.
      • The same limited track muxed into an mp4 (pair_audio): no audio_clipped warning, and a decoded peak below 0 dBFS.
      • A target needing more than 12 dB of reduction (e.g. -8 on the same track): target_lufs_capped with limited: true and shortfall_lu > 0, plus limiter_heavy; the output true peak is still ≤ the ceiling.
      • A target reachable without limiting: limited_fraction: 0, and the same LUFS as limit: false.
      • limit: true without target_lufs: true_peak_dbfs ≤ peak_dbfs.
      • A silent track: unchanged, with the existing silent warning.
      • The output duration equals the input's (get_gallery_metadata), and speech in the limited file stays in sync with the picture after muxing.
      • get_task("normalize_audio") lists limit with its description.

    Stage 2: expose it on assemble-and-score and in the series-episodes skill (server and plugin)

    • Builds:
    • Tests: the template test that pins assemble-and-score's variables, and one test that limit reaches the balanced step. tests/test_plugin_skills.py if it pins numbers in the skill text.
    • Deploy: server (the template) and plugin (the skills).
    • Estimate: $2–3 (spent). v2 adds about $0.5 for the description and its acceptance run.
    • Acceptance intent:
      • Over MCP: list_workflows shows limit on assemble-and-score.
      • The defaults give the same result as before.
      • target_lufs: -16, limit: true on a laugh-track episode: the film lands within 0.5 LU of -16. (v2) The balanced log has output_true_peak_dbfs ≤ -3.0. The film's true_peak_dbfs is ≤ -2.0, which is the mix ceiling plus a 1 dB encode allowance and still under the -1 dBTP streaming ceiling. There is no audio_clipped. The template description no longer claims -3 dBTP on the film.
      • The skill text is verified by the tester reading the file at develop, since the plugin has no MCP surface. Don approves that line.

    Order and dependencies

    Risks

    • Artefacts. A fixed-timing limiter can still be audible on some material (sustained music peaks). The 6 dB limiter_heavy warning and the 12 dB cap bound it. Exposed timings are the thing a field report of pumping would bring back.
    • True-peak meter vs AAC. A true-peak ceiling of -3 is sound practice for AAC (the idea: musical join task - spoken scene into song with the song entering under the last line, as a run output with provenance #486 session measured -2.7 decoded). An encoder can still overshoot; the existing audio_clipped post-encode check stays as the backstop. (v2) Confirmed and measured: the AAC mux adds about 0.46 dB on a limited laugh-track mix (16% of samples at the ceiling) and about -0.1 dB on an unlimited one. The overshoot depends on the material (CLAUDE.md already records 1.9 dB on an unnormalized song), so no fixed margin can be known to be enough.
    • Speed. The oversampled detection costs about 4x the samples. It has to stay vectorised; the tests pin the timing.
    • Test sensitivity. A synthetic crest test can pass while real laughter pumps. The acceptance cases use a real episode for that reason.

    Questions

    Q1. Should this be a mode on normalize_audio or a new limit_audio task? Default: the mode. A task would add a task, docs, domain entries and a NORMALIZING_COMMANDS entry, and agents would have to order two steps correctly. Stage 1 would rise about $1.

    Q2. What happens to compress_audio(mode="limit")? Default: leave it, and document the difference. One option is to route it through the new limiter: a behaviour change with no consumers, about +$1 in stage 1. Another is to remove the mode, which is a breaking-change and would be its own issue.

    Q3. Fixed timings and fixed thresholds (12 dB cap, 6 dB warning)? Default: fixed. Exposing lookahead_ms/release_ms/max_reduction_db adds three arguments, three domain entries and the tests for them, with no demand yet.

    Q4. With limit: true, is peak_dbfs a true-peak ceiling? Default: yes. The alternative, keeping it sample-peak, leaves the inter-sample overs that AAC turns into clipping, which is what the limiter is for.

    Q5. Should stage 2 expose limit beyond assemble-and-score? Default: assemble-and-score only (the series recipe). Adding dissolve-between-shots and minimax/music-video as well costs about +$1 and needs the same target_lufs exposure there first.

    Q6 (v2). How far should the -3 dBTP ceiling reach under limit: true: the mix or the muxed film? Default: the mix. Document that the film can land up to about 1 dB above it, and gate the acceptance at ≤ -2.0 dBTP with no audio_clipped (above). That costs about $0.5, the plan's non-goals stand (the encoded file's check is unchanged), and -2.5 is still well inside the -1 dBTP delivery ceilings the streaming target is for. #486's hand-mastered result was -2.7. Alternatives:

    • (b) A fixed encode margin. normalize_audio(limit=true) holds peak_dbfs minus about 1 dB internally. That is a behaviour change to verified stage 1, the log would report a ceiling that isn't the one asked for, and no fixed margin can be known to be enough. About +$1.5 and a re-verify of stage 1.
    • (c) Trim after the encode. The lossy mux in Result.save re-probes the written file's true peak and, over a ceiling, trims the audio and re-encodes once. This is the only option that guarantees the film, but it lifts a non-goal ("changing how the encoded file is checked") and needs a ceiling carried to pair_audio or the result. It would be a new stage 3 of about $4–6, touching dw/result.py, and it should also answer the follow-up about warn_if_written_above_full_scale reading sample peak.

    Reply in comments and hand back with owner:lead. Approve by adding status:plan-approved yourself. No agent may set it.

  5. added
    featureWork bigger than a fix: designed with Don, built in stages (harnest R11)
    owner:donParked for human decision
    status:plan-reviewA feature plan is posted and waiting for Don
    and removed
    ideaAn idea for the researcher agent to assess
    owner:leadFeature lead's turn (harnest R11)
    on Sep 26, 2026
  6. dkackman commented on Sep 27, 2026

    @dkackman
    OwnerAuthor

    approve build smaller

  7. added
    owner:leadFeature lead's turn (harnest R11)
    status:plan-approvedDon approved the feature plan (Don only)
    and removed
    owner:donParked for human decision
    status:plan-reviewA feature plan is posted and waiting for Don
    on Sep 27, 2026
  8. 22 remaining items

  9. added
    status:needs-specTester writes acceptance cases for the plan's stages
    and removed
    owner:leadFeature lead's turn (harnest R11)
    on Sep 27, 2026
  10. dkackman commented on Sep 27, 2026

    @dkackman
    OwnerAuthor

    Acceptance cases for plan v2, in regression-suite-complete.md (harnest):

    • #474 stage 1: normalize_audio(limit=true), a true-peak look-ahead limiter #496 (stage 1, unchanged and verified): C-F132, C-F133 and C-F134 stand. No new cases.
    • #474 stage 2: expose limit on assemble-and-score and in the series-episodes skill #497 (stage 2, reshaped):
      • C-F150 (new, pending: #497) covers the v2 gates:

        • the balanced log has constraint: "limiter" and output_true_peak_dbfs ≤ −3.0 (+0.05);
        • the film lands within 0.5 LU of −16, with true_peak_dbfs ≤ −2.0 and no audio_clipped or target_lufs_capped;
        • the template description says the ceiling holds on the mix, that the AAC film can land up to about 1 dB above it, and no longer promises −3 dBTP on the film.

        It also carries C-F135's checks unchanged: the limit variable with default false, omitted and false giving the same capped result (peak_ceiling), and the three films having the same shape. It records the encode overshoot (film true peak minus the mix's) in the report.

      • C-F136 (the skill text) is unaffected and stands.

    C-F135 still asserts the v1 film gate (≤ −2.9 dBTP), so it would fail against the approved v2 behaviour. I haven't edited it. Retiring it in favour of C-F150 is requested in dkackman/harnest#25 (suite + status:needs-approval). Until that is ruled on, a verify of #497 should judge it by C-F150 and C-F136.

    Before writing these, I pinned the current template shape with a read-only get_workflow("templates/assemble-and-score", variables_only=true): limit: false and target_lufs: null are present.

    Specced by Claude Opus 5.5 (claude-opus-5-5, anthropic). No GPU calls.

  11. added
    owner:leadFeature lead's turn (harnest R11)
    and removed
    status:needs-specTester writes acceptance cases for the plan's stages
    on Sep 27, 2026
  12. dkackman commented on Sep 27, 2026

    @dkackman
    OwnerAuthor

    Close-out by Claude Opus 5.5 (claude-opus-5-5, anthropic).

    Closed: stage 1 #496 (normalize_audio(limit=true), verified at 6a5cbd5) and stage 2 #497 (limit on assemble-and-score and in the series-episodes / minimax-music3 skills, verified under plan v2 at b8afddf). Each bounced once; the record says why.

    Design record: docs/proposals/complete/normalize-audio-limiter-complete.md, with a todo.md entry, in 3b84b37 and merged to develop as d4e55c7. Docs only, so there is no deploy. It lists what was deferred: the ceiling on the encoded film (Q6 b/c), true-peak in warn_if_written_above_full_scale, compress_audio's limit mode, limit on other templates, and tunable timings.

    verify: run every case whose source: names one of this feature's stages (#496, #497): C-F132, C-F133, C-F134, C-F150 and C-F136. Close the parent if they all pass. C-F135 carries the v1 film gate, and dkackman/harnest#25 asks to retire it, so judge #497 by C-F150.

  13. added and removed
    owner:leadFeature lead's turn (harnest R11)
    on Sep 27, 2026
  14. dkackman commented on Sep 28, 2026

    @dkackman
    OwnerAuthor

    Verified. Tester run on Claude Opus 5.5 (claude-opus-5-5, provider anthropic), against lem at develop @ cdfd02f, workspace regression-complete. I made every check below with an MCP call this session. The feature parent is judged by its stages' cases: C-F132, C-F133 and C-F134 for #496, and C-F150 and C-F136 for #497. #497 is judged by C-F150, not C-F135, per dkackman/harnest#25.

    get_task("normalize_audio") lists limit (default false). Its description names the true-peak ceiling (4x oversampled) and the 12 dB cap.

    C-F132: pass (job c2f3188e19b4)

    • capped and capped_false: both warn target_lufs_capped ("+1.4 dB … caps the gain at -2.0 dB"). Both logs read constraint: peak_ceiling with no limiter fields. The two files are identical: −19.39 LUFS, peak −3.000.
    • limited log:
      • constraint: limiter, gain +2.4
      • max_gain_reduction_db 4.46, limited_fraction 0.162
      • output_true_peak_dbfs −3.0, output_lufs −16.0
    • limited file: −15.96 LUFS, true peak −2.999. No capped or heavy warning names it.
    • limited_mux: 472 frames, 24 fps, 960×544, 19.667 s, true peak −2.85, no audio_clipped. Its envelope bins line up with the source's: the loudest bin and the first bin above −40 fall at the same index.

    C-F133: pass (job 31de74e78fd7)

    • heavy: limiter_heavy (8.5 dB) and no capped warning. Reduction 8.51, fraction 0.405, −14.47 LUFS, true peak −2.999.
    • squashed:
      • limiter_heavy at 12.0.
      • target_lufs_capped with limited: true and shortfall_lu 8.67. Both fields sit on the warning event, not the log.
      • The file reads −13.67 LUFS, true peak −2.999, louder than heavy.
    • full_scale: reduction 6.86 with limiter_heavy, true peak +0.0006 (≤ 0.1), no capped warning. It also carries audio_no_headroom (peak −0.01), which is expected at a 0 dBTP ceiling; the case doesn't cover it.

    C-F134: pass (job 1ca9f19d8dab)

    • quiet_true: limiter with 0 reduction and fraction 0. Its file is identical to quiet_false's (−22.00 LUFS, −5.53 dBTP).
    • peak_true: true peak −3.000 and fraction 0.
    • A one-track mix_audio with gains [0] works.
    • silent_true and silent_false both succeed at −120 dBFS with only the near-silent warning. Neither has a limiter log or limiter_heavy.

    C-F150: pass

    • list_workflows(shape="sequence"): variable_names includes limit and target_lufs, and get_workflow shows limit: false.
    • The description says the ceiling "holds on the mix 'balanced' writes, not on the film". It says the AAC mux can land "up to about 1 dB above it", and it promises no −3 dBTP on the film.
    • (a) no limit (job bb48dc3131ed) and (b) limit:false (job 729d7beeca7d) behave the same:
      • target_lufs_capped names balanced, and the log reads constraint: peak_ceiling, gain −2.0.
      • The two films are identical: −19.504 LUFS, true peak −3.103.
    • (c) limit:true (job 42a7c5237b43): validated first, valid with no warnings.
      • balanced log: constraint: limiter, output_true_peak_dbfs −3.0, max_gain_reduction_db 4.46.
      • Film: −16.01 LUFS, true peak −2.537 (≤ −2.0). That is an overshoot of +0.46 dB over the log's −3.0. There are no warnings at all: no audio_clipped and no target_lufs_capped.
    • All three films: 472 frames, 24 fps, 960×544, 19.666667 s.
    • Only (c) was validated first, as the summary on this issue says. (a) and (b) ran with acknowledged_cost: true and without a separate validate.

    C-F136: pass

    • dw:series-episodes says to match downward by default. It says to pass limit: true only when the series should sit louder than its most dynamic episode allows (e.g. −16), and that limiter_heavy means lower the series target_lufs.
    • dw:minimax-music3 says the same in one line for a master.
    • Neither skill turns limit on by default, and neither names a variable or warning that doesn't exist.

    Cleanup: every run's output has been deleted with delete_output(job_id).

  15. added
    status:verifiedTester confirmed the fix via a real MCP call
    and removed on Sep 28, 2026
  16. added a commit that references this issue on Oct 10, 2026
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

    featureWork bigger than a fix: designed with Don, built in stages (harnest R11)owner:testerTester's turn to actstatus:plan-approvedDon approved the feature plan (Don only)status:verifiedTester confirmed the fix via a real MCP call

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions