Skip to content

fix(producer): duration normalization encodes from the delivery PCM, so a compliant mix is never cut - #7199

Merged
WaterrrForever merged 4 commits into
mainfrom
miao/audio-trim-peak
Nov 5, 2026
Merged

WaterrrForever merged 4 commits into
mainfrom
miao/audio-trim-peak

Conversation

@WaterrrForever

Copy link
Copy Markdown
Collaborator

What was wrong

When the audio duration differs from the video's, the assemble stage's duration fix (padOrTrimAudioToVideoFrameCount) decoded the audio stage's audio.m4a and encoded it again. That second lossy generation can read hotter than the first. The true-peak check after it had no PCM to work from, so it cut the whole mix, re-encoding AAC from AAC on each pass and overshooting.

Measured on a real project, Jake's 3D True Crime upload from the style-sampling bug bash, instrumented local render on main with FFmpeg 4.2:

audio stage  audio.m4a                      true peak -1.8 dBTP → no correction
assemble     audio.duration-normalized.m4a  pass 0 +0.2 → pass 1 -0.3 (att -1.7) → pass 2 -2.5 (att -3.4)
Audio lowered by 3.4 dB to stay under −1 dBTP

An already compliant mix lost 3.4 dB. A sibling upload, identical except for a music bed 1.07 dB louder, escaped the cut, so the two films landed about 4 LU apart.

The fix

  • The PCM lives until assembly. In the local render, the audio stage keeps the delivered PCM (audio-source.wav) in the work dir. Pad/trim and any peak correction in assembly encode from it, so the mix is encoded once.
  • Audio-stage cuts reach the PCM too. When the audio stage cuts the whole mix, it applies the same gain to the kept PCM, so re-encoding it reproduces the delivered AAC. The decoded output is bit-identical on FFmpeg 4.2 and 7. If the PCM can't be rewritten, it is deleted and assembly falls back to the AAC path used today.
  • Cleanup: on success, failure and abort, the PCM goes when the orchestrator removes the work dir. It is kept only with --debug or KEEP_TEMP=1, as before. If the audio stage itself fails, it deletes the PCM before returning.
  • HLS goes through the same normalization, so it's covered. The distributed plan doesn't keep the PCM, and its plan dir is unchanged.

Before / after (local, LUFS / true peak)

FFmpeg Project main this PR
4.2 render-2 upload −17.4 / −2.5, lowered 3.4 dB −13.9 / −1.8, not lowered
4.2 render-3 upload −13.5 / −1.0, not lowered −13.5 / −1.8, not lowered
7.0 render-2 upload −15.9 / −2.3, lowered 2.0 dB unchanged
7.0 render-3 upload −15.3 / −1.8, lowered 1.8 dB unchanged

On FFmpeg 4.2 the two uploads now land 0.4 LU apart, consistent with their 1.07 dB music difference, and both stay ≤ −1 dBTP.

Scope

  • FFmpeg 5 and newer already make the AAC exactly the video's length, so assembly is a plain copy and this PR changes nothing there. On FFmpeg 7, the remaining ~2 dB cut comes from a different cause. The limiter holds the PCM at −3.0 dBTP, but the AAC encoder overshoots on transients (+0.5 and +0.3 dBTP on these mixes), and that triggers the audio stage's whole-mix cut.
    • The overshoot isn't stable: limiting the PCM to −4, −5 or −6 dBTP gave AAC peaks of −0.6, +1.2 and −0.9. So a lower ceiling isn't a reliable fix.
    • -aac_pns 0 makes the overshoot much smaller, but it changes every render's audio bytes. That belongs in its own follow-up.
  • Cloud evidence: re-rendering render 2's exact upload in the cloud (86ef0011) reproduced its −19.4 LUFS exactly, so the cloud is deterministic for a given input. Those cloud figures come from producer 0.9.8, whose path I didn't reproduce. On current main with FFmpeg 7, the two uploads land 0.6 LU apart, not 4.

Tests

  • Six new tests fail on main and pass here:
    • pad/trim encodes from the PCM, and a compliant mix is not cut;
    • a peak correction re-encodes from the PCM;
    • an audio-stage cut is applied to the PCM;
    • a PCM that can't be rewritten is deleted;
    • the audio stage keeps the PCM when asked;
    • assembly passes the PCM to pad/trim.
  • Two more check that the PCM is untouched when nothing is cut, and deleted when the peak check fails.
  • Producer unit lane: vitest 1180 passed, 1 skipped. Every bun file passes, including audioPadTrim.test.ts (45).
  • Gates: oxfmt 0.41.0, oxlint, producer typecheck, comment ratchet and citations, and verify:packed-manifests all pass. fallow health --complexity has no findings on the touched files.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HebQXN7wsMK6GqT3MNtqXw

WaterrrForever and others added 2 commits November 4, 2026 19:40
The assemble stage's duration fix decoded audio.m4a and encoded it again.
That second AAC generation read about 2 dB hotter on a measured mix
(-1.8 dBTP out of the audio stage, +0.2 after the trim), so the whole-mix
cut then lowered an already compliant film by 3.4 dB, overshooting to
-2.5 dBTP. A sibling project with a slightly louder music bed escaped the
cut, so the two landed about 4 LU apart.

- The local render keeps the audio stage's PCM in the work dir until
  assembly; pad/trim and any peak correction there encode from it.
- A whole-mix cut in the audio stage is applied to that PCM too, so it
  still encodes to exactly the delivered AAC. If it cannot be rewritten
  it is removed and assembly falls back to the AAC.
- The distributed plan does not keep the PCM, so its plan dir is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The comment-ratchet does not let assembleStage.ts gain a comment line, so
the new input says what it holds in its name instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HebQXN7wsMK6GqT3MNtqXw
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 267 (base branch 267), smooth 110 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (8)

Unstable (1)

  • seqplainkeys-none-px-r0-root-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo false, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 8bf04e161bd715531d5ab7078bef6a4e0069aa98.

The local handoff now carries PCM through the existing work-directory owner instead of adding an independent cache (packages/producer/src/services/renderOrchestrator.ts:3684,5551). Duration normalization and correction reuse that source; the delivered gain is applied before it is retained. Standard success/failure/abort disposal remains in the orchestrator's existing execution.dispose() owner, which waits for pending audio on failure. No new full-film in-memory buffer is introduced.

Important — a failed rewrite plus refused deletion can hand assembly the uncut PCM. packages/producer/src/services/render/audioPadTrim.ts:453–454 calls best-effort removeScratch after rewriting/replacing the PCM fails. That helper deliberately swallows deletion refusals (packages/core/src/removeScratch.ts:8–13, documented Windows behavior). The new audioStage.ts:125 then treats mere existence as validity and publishes deliverySourcePath anyway. A source handle that denies replacement/write and deletion on Windows is a concrete shared failure class for both operations; the injected probe models the documented refusal semantics rather than reproducing that OS lock.

A probe using the actual audio stage, true-peak helper and pad/trim consumer, with injected codec/process results and simulated deletion refusal, reproduces loweredDb: 2, retained: true, encoded: "UNCUT PCM", assemblySuccess: true. The successful-deletion control falls back to the cut AAC and passes. Swapping only the audio-stage producer to the merge-base version makes both controls pass because it never publishes a retained PCM. This is newly unsafe source reuse; it does not establish over-ceiling final output, a real Windows failure or the original films' loudness delta. The final peak check enforces the ceiling; it does not enforce the earlier delivery attenuation. A second probe uses real FFmpeg8 PCM/AAC encoding and actual final true-peak measurement, while injecting the initial peak decision to force the already-supported 2 dB cut and simulating source replacement/deletion refusals. Delivered mean level is −34.1 dBFS, final mean −32.1 dBFS, with stageCut=2, assemblyCut=0: the earlier 2 dB attenuation is lost. The same probe passes with the base audio-stage producer. This is a controlled codec/fault probe, not the original film's natural AAC-overshoot reproduction.

Fix: make cutSource report whether the source remains usable, propagate that through enforceAacTruePeak/constrainAacTruePeak and guardDeliveryPeaks, and return deliverySourcePath only when usable. On rewrite/rename failure, clear the usable-source reference independently of whether best-effort cleanup succeeds, so assembly uses the already verified AAC. Add keepDeliverySource + failed rewrite/replace + refused delete coverage that asserts no stale source is returned and pad/trim takes AAC. File deletion is cleanup, not a validity signal.

Scope notes: the copy decision is based on actual duration tolerance; the unchanged encoder checks FFmpeg's setts duration capability rather than parsing a version number. I did not reproduce the author's FFmpeg4.2/7 project measurements (this box has FFmpeg8). Distributed plan call/artifacts do not retain the source, but execution is not literally unchanged: a whole-mix correction now rewrites PCM before immediately deleting it. Nit: skip that rewrite when no caller will retain the PCM, or narrow the “distributed unchanged” wording.

Typical retained 48kHz stereo float PCM is 384,000 bytes/s ≈1.38 GB/hour; it overlaps capture/assembly until work-directory disposal. Debug and successful KEEP_TEMP runs intentionally retain it. No very-long-film disk benchmark or cloud re-render was run.

Validation: full env -u SLACK_BOT_TOKEN bun run build passed. Producer bun test src/services/render/audioPadTrim.test.ts: 45 passed; bunx vitest run src/services/render/stages/audioStage.test.ts src/services/render/stages/assembleStage.test.ts: 38 passed. Six fresh independent helper probes pass (47 assertions). The additional deletion-refusal probe has 1 passing control, 1 expected failure at this head; its base-producer control has 2 passed. The additional real-encoding gain-preservation assertion fails at this head and passes with the base producer. All executable review commands unset SLACK_BOT_TOKEN. No product edit retained or pushed.

Audited: seven-file diff, complete audio pad/trim/peak and stage implementations, tests, encoder capability/input behavior, distributed caller, and orchestrator audio handoff/work-directory cleanup regions. Unrelated capture branches and the author's real-project codec measurements are not independently certified. No paid provider, cloud render or deployment action.

Need: yes · Size: ok · Reuse: existing PCM/work-directory and encoder helpers · Simpler: carry source validity separately from cleanup · Structure: fine
Risk: medium — retained audio crosses failure recovery and assembly; stale PCM can change delivered gain.

Verdict: REQUEST CHANGES
Reasoning: The normal PCM path fixes the intended extra AAC generation, but failed source replacement must never leave stale PCM eligible for assembly.

— Rames (hf-review)

WaterrrForever and others added 2 commits November 5, 2026 01:16
…t applied

When the audio stage cuts the whole mix and then fails to rewrite the kept
PCM, it tries to delete the PCM. Deletion is best-effort and can be refused,
for example on Windows, so the uncut PCM could stay on disk. The audio stage
handed it to assembly because the file still existed, and assembly would
then re-encode a mix louder than the AAC that was actually delivered.

The cut now reports when the PCM no longer matches the delivered AAC. The
audio stage keeps the PCM only on an explicit success, not because the file
exists, and assembly otherwise falls back to the verified AAC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HebQXN7wsMK6GqT3MNtqXw
The true-peak correction now rewrites the kept PCM with FFmpeg and reports
the PCM as stale when that output never appears. The fake FFmpeg in this
test answered the rewrite without writing anything, so a correct run looked
like a failed rewrite. The fake now writes the output, as real FFmpeg does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HebQXN7wsMK6GqT3MNtqXw

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at d94dae43d7d3638cab77b1e594bdba2191a5240d.

Re-review of the five-file delta from 8bf04e16. My previous blocker is resolved: audioPadTrim.ts:449 now returns an explicit failure outcome when rewriting or replacing PCM fails, constrainAacTruePeak propagates sourceStale, and audioStage.ts:126 excludes that source independently of best-effort deletion. Assembly therefore receives no PCM path and uses the verified attenuated AAC. Successful-cut and no-cut paths retain the usable PCM as intended.

Validation: full env -u SLACK_BOT_TOKEN bun run build passed. Producer bun test src/services/render/audioPadTrim.test.ts: 45 passed. Vitest audio/assembly stages, fake-FFmpeg test and the three original refusal/codec probes: 49 passed. The codec probe now preserves the earlier 2 dB attenuation within its 0.2 dB tolerance; both successful-delete and refused-delete probes select the cut AAC. Reverting only the new sourceStale guard makes the new stage regression fail. Five fresh independent actual-helper probes passed: no cut, successful rewrite, rewrite returning failure, thrown rewrite, and failed rename. The final test-only successor makes the fake PCM encoder materialize its declared output rather than accidentally exercising failure; its expectations remain intact.

The original local PCM lifecycle/assembly audit still applies; the distributed caller/artifact contract is unchanged by this delta. The earlier nonblocking note about an extra PCM rewrite before distributed cleanup remains. Typical retained 48kHz stereo float PCM adds about 1.38 GB/hour until work-directory disposal. I have not run a long-film disk benchmark, a real Windows file-lock case, the author's FFmpeg4.2/7 projects, or a cloud render. The codec/fault probe uses real FFmpeg8 encoding and final measurement with controlled initial peak decisions and injected replacement/deletion refusals.

Need: yes · Size: ok · Reuse: existing peak-guard result and AAC fallback · Simpler: source validity now travels separately from cleanup · Structure: fine
Risk: medium — audio source retention spans failure recovery and assembly; the reproduced stale-source path is now closed.

Verdict: APPROVE
Reasoning: The prior source-validity blocker is fixed through producer and consumer; no further blocking finding in the delta.

— Rames / hf-review

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head d94dae43d7d3638cab77b1e594bdba2191a5240d. The local assembly path now retains the delivery PCM and uses it for pad/trim and true-peak correction instead of decoding/re-encoding AAC first; the distributed plan remains unchanged. On a failed PCM cut rewrite, sourceStale is reported even if removing the old PCM file fails, and the audio stage withholds that source so assembly uses the already verified attenuated AAC. The final successor changes only the fake FFmpeg test to write the successful PCM output it claims. Rames independently approved this head after real-encode/failure probes. Hosted CI was still running at my check, so it remains a separate gate; I ran no paid cloud render or Windows-lock reproduction. No merge. — Jerrai

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE · 0 blocker · 0 important · 0 nits · d94dae43

@jrusso1020's stale-PCM finding is fixed. A failed rewrite or rename now returns sourceStale, the audio stage keeps the PCM only on explicit success, and assembly falls back to the attenuated AAC. Every other write to audio.m4a (limiter, cut, failure) leaves the PCM matching it.

Scope

Read end to end: audioPadTrim.ts, audioStage.ts, assembleStage.ts, the orchestrator handoff, and audioDeliveryLimit.ts (the limiter renames its output over the PCM, so the kept source includes limiting). The last commit is test-only. CI, regression, CodeQL and preview-regression are green at this head. Windows render verification was still running when I posted. Tests were not run locally (no bun in the sandbox).

@WaterrrForever
WaterrrForever added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit f6ae9bf Nov 5, 2026
91 checks passed
@WaterrrForever
WaterrrForever deleted the miao/audio-trim-peak branch November 5, 2026 01:55
SanHsien added a commit to SanHsien/hyperframes that referenced this pull request Nov 5, 2026
…eview

Adopts heygen-com#7199 (delivery-PCM duration normalization), heygen-com#7194 (browser
install retries while Windows holds a file), the Studio marquee and
timeline range-selection series, waapi initial-state, lint media and
CLI check/contrast fixes, and skills docs. Not adopted with ledger
reasons: heygen-com#7203, heygen-com#7207, heygen-com#7202, heygen-com#7197; limitDeliveryPeaks and the
neverStyledStarts cache are left out because the fork lacks their
prerequisites. Watermark advances to aa34cff / PR heygen-com#7190.

Co-authored-by: WaterrrForever <miao.yang@heygen.com>
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
Co-authored-by: kevin <5299031+kevin9327@users.noreply.github.com>
Co-authored-by: Jason <jason.easyai@gmail.com>
Co-authored-by: cooleryu <53998515+cooleryu@users.noreply.github.com>
Co-authored-by: Vincent Yip <realsharkguy@gmail.com>
Co-authored-by: Vance Ingalls <vance@heygen.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants