feat(studio): normalize loudness and duck under voice, with a limiter report (4/8) - #4816
Conversation
miguel-heygen
left a comment
There was a problem hiding this comment.
Reviewed incremental #4816 at a244373ae644e6cf8085f7a7804cfc4a91eb08ef against 720871da192bce80bc119e2109165f28079bc460.
The measure-only endpoint correctly leaves persistence to Studio (packages/studio-server/src/routes/loudness.ts:82), and the limiter attenuation is propagated through both CLI and Vite adapters instead of existing only in a log (packages/cli/src/server/studioServer.ts:624, packages/studio/vite.adapter.ts:369).
Blocker: packages/studio/src/player/components/clipMenuAudioItems.tsx:38 — Normalize always writes data-volume, even when a volume automation lane owns the clip's gain. syncRuntimeMedia takes that lane before the static volume (packages/core/src/runtime/media.ts:402); render likewise uses the lane envelope (packages/engine/src/services/audioMixer.ts:1321). An independent runtime witness changed data-volume from 1 to 2 on a constant 0.25 lane and the applied gain remained 0.25, while the command claims normalization. Normalize the effective envelope, or explicitly refuse this unsupported case rather than announce success.
Important: packages/studio/src/player/components/clipMenuAudioItems.tsx:38 and :58 — The quiet-save callback returns a TimelineEditOutcome, including non-throwing failed/refused outcomes. Both new handlers discard that result. The browser menu witness returned {status:"failed",reason:"disk full"} and still displayed “Normalized to −16 LUFS (+6.0 dB)”. Duck can similarly continue after an individual write fails and report success for a partial chain. Check each save outcome and stop on failure.
CI: The existing @github-advanced-security bot already reports the polynomial regex at packages/studio-server/src/helpers/loudness.ts:153; this review does not duplicate that finding. The exact-head CodeQL result is failure (“1 new alert including 1 high severity security vulnerability”). Other observed checks pass.
Audited: loudness helper/route, CLI normalization move, normalize/duck menu handlers, carve output extraction, both render adapters, producer attenuation and assemble propagation; traced existing volume automation playback/render and persistence/undo consumers.
Trusting: parent PRs #4813–4815; full production rendering, complete package suites, unrelated existing runtime/UI behavior.
Verification: 93 existing targeted tests (40 loudness/menu, 37 Bun audioPadTrim, 16 assembleStage) and 2 independent witnesses passed at this exact head (cached Vitest 3.2.4, not declared 4.1.11; parser/core aliases resolve this worktree's source). The browser menu witness ran at #4818 with byte-identical clipMenuAudioItems.tsx; client-only local adapter, not a packaged/full-Studio/backend E2E claim. No customer assets or paid renders.
— Magi
Verdict: REQUEST CHANGES
Reasoning: The new command can claim normalization without changing effective loudness, and the existing CodeQL failure remains unresolved.
a244373 to
1b983f3
Compare
Edit accuracy: 951 passing here, 951 on the base branchThe gate passes. Quarantined, measured but not gated (2)
Unstable (1)
|
1b983f3 to
2f5e250
Compare
2f5e250 to
dbcbca8
Compare
miguel-heygen
left a comment
There was a problem hiding this comment.
Fix-delta review at dbcbca85d2e088cd05cb28320015aa3109aec27d, against stacked parent c16614b676aa74fdb0f83ff2c4ccb99f3439efd3; compared with my previous review at a244373a.
The prior code findings are repaired: Normalize refuses a nonempty volume lane before measurement (packages/studio/src/player/components/clipMenuAudioItems.tsx:44); Normalize/Duck inspect tagged save outcomes and stop rather than toast success (:49, :73). The hostile-markup parser uses a character scan and exact-head CodeQL is green.
Verification: 24 loudness/parser and 5 menu tests pass on stack head 88255f82e; these implementation and test files are byte-identical to this PR head. Cached Vitest 3.2.4, source aliases, happy-dom, boundary persistence fakes; no full Studio/backend, live audio or render acceptance. No new code blocker found in the repair delta. Parent layers and unrelated rebased-main changes were not re-reviewed.
The required Studio and player captures check still fails for missing Before/After evidence. This COMMENT closes the code findings in the narrative; it does not supersede my earlier changes-requested state or approve landing while that gate remains red.
— Magi
Verdict: COMMENT
Reasoning: The automation, save-result and CodeQL repairs hold in the reviewed paths. Required capture evidence remains missing.
dbcbca8 to
b8b2bb0
Compare
jerrai-bot-heygen
left a comment
There was a problem hiding this comment.
Reviewed #4816 at b8b2bb09 against #4815 bf04ab97. Normalize still refuses an automated-volume lane; Normalize and Duck check each quiet-save result before reporting success. The hostile-attribute parsing repair remains in the loudness helper. I found no new blocker in those code paths, but the required Studio/player Before/After captures check remains FAILED without evidence or waiver; prior changes-requested review still governs, so no approval. Edit-accuracy and Comments checks pass. No local tests or Studio run. — Jerrai
b8b2bb0 to
ee574e2
Compare
jerrai-bot-heygen
left a comment
There was a problem hiding this comment.
Code review of this exact #4815-based layer: Normalize refuses automated-volume lanes before claiming a change; Normalize and Duck inspect quiet-save outcomes, and the hostile-markup parser/limiter reporting repairs remain intact. Its 34-file layer patch is unchanged through the rebase and the capture check passes. The red shard-8 visual comparison (sub-comp-t0, 33 frames; adjacent #4815/#4817 pass) needs a separate CI disposition; this approval does not bypass it or Miguel's older changes-requested review. — Jerrai
ee574e2 to
4f68e17
Compare
jerrai-bot-heygen
left a comment
There was a problem hiding this comment.
Exact 4f68e172 rebase review: old and new parent→head comparisons retain the same 34 paths and per-file patches. The only concurrently changed layer file, packages/studio/src/index.ts, adds WebMCP exports on the new parent while this layer adds a separate audio-meter export; both additions survive in the new head. This preserves the previously reviewed automation/save-outcome code. This approves this layer's code, not the currently held #4815 viewport regression or any pending/cancelled required check. Miguel's older review is also a separate gate. No merge or release action. — Jerrai
miguel-heygen
left a comment
There was a problem hiding this comment.
Re-reviewed #4816 at 4f68e172022742086fded6208f12a72a00dcb990 against #4815 bff5d190e; this supersedes my changes-requested review 5374292381.
The original findings are repaired: automated-volume lanes are refused before measurement; Normalize and Duck inspect each save outcome before reporting success; the hostile-markup parser is the linear scan already checked in my dbcbca85 follow-up. The menu and loudness source remain byte-identical to that reviewed repair. I read the current handlers and reran 17 menu/normalize/duck cases using the current layer's implementations in the source export, including failed/refused saves. The later parent-dependent bed-eligibility change is not counted as part of this layer.
Current required checks, including Studio/player captures, pass. The check-run history contains a canceled file-size run; it is not a failure in the fetched required-check list. No new layer-specific code blocker found. This is a code review with cached Vitest 3.2.4/source aliases and component-boundary persistence fakes, not full Studio/backend, live audio, playback or export acceptance.
Verdict: APPROVE
Reasoning: the effective-gain, save-result and parser findings are fixed and regression-covered at the reviewed layer. The former capture-only hold is resolved.
— Magi
4f68e17 to
eaaf6e1
Compare
miguel-heygen
left a comment
There was a problem hiding this comment.
Re-approved at eaaf6e1b6d9e243490b0f6dc1d4e5fe366462794 against stacked parent a88f4fe56fa10293a121443999cfe78cca56a5b0, following my review 5387236315.
The parent-to-head patch has identical paths and hunks to the layer I previously approved, ignoring only object indexes and hunk locations. The restack preserves the repaired Normalize automation refusal and failed-save handling. This is a rebase verification; I did not repeat the earlier exact-source tests or the full package suite.
Current checks have no failure, but CI remains in progress; approval does not establish merge readiness. Prior evidence limits (cached tooling, no live Studio/audio acceptance) remain. No merge, release or protection bypass performed.
Verdict: APPROVE
Reasoning: The inspected layer is the same reviewed patch on its new stacked parent, with no additional code change or new layer-specific blocker.
— Magi
eaaf6e1 to
9849ba6
Compare
miguel-heygen
left a comment
There was a problem hiding this comment.
Re-approved at 9849ba697393839b2134f5a82b79a694163bcb5c against stacked parent 9c6f0a3ec954afc3da7591f14874f89af43aa74a, following my approval 5387641103.
I independently compared the complete old/new parent-to-head patches: same 34-path set and identical hunks, ignoring only object indexes and hunk positions. Every changed-layer source and test blob is also identical between the approved head and this head; the only differing layer file is package.json with the inherited 0.8.106 → 0.8.107 version bump. The prior repairs, test cases and save/decode behavior, survive unchanged.
This is a rebase verification, not a new local test run. The prior focused tests and their stated cached-tooling limits remain the evidence. No live Studio/playback, full package acceptance or merge/release performed. No task worktree, server or browser was created.
Current-head checks have no failure but are still in progress. Approval does not waive any required check or establish merge readiness.
Verdict: APPROVE
Reasoning: The entire inspected layer patch and its executable sources/tests match the previously approved layer on the new stacked parent.
— Magi
…et and video support normalize-audio moves its measure/plan code into @hyperframes/studio-server/loudness, gains an absolute mode (--lufs, default -16, clamped at +12 dB and -1.5 dBTP) and accepts <video> clips with sound. Adds the @hyperframes/core/audible-video subpath. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /projects/:id/loudness/normalize measures a clip's played window (audio or video) and answers the -16 LUFS data-volume plan; Studio writes the attribute. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…riptors Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… menu Normalize measures the clip on studio-server and writes data-volume as one undoable edit; Duck toggles a carve against every overlapping voice (audio or video with sound) at the default strength. Both live in clipMenuAudioItems with one hook-in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The AAC true-peak pass now returns its attenuation; the render logs "Audio lowered by X.X dB to stay under -1 dBTP" and sets RenderJob.audioLoweredDb, which Studio servers carry in the complete SSE event and the /renders record. Also exports useAudioMetersVisible from @hyperframes/studio for hosts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The start-tag regex backtracked quadratically on '<' followed by many spaces (CodeQL js/polynomial-redos). A linear character scan reads the same name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ave outcomes A volume lane owns the clip's gain in preview and render, so writing data-volume changed nothing while the toast claimed success; Normalize now refuses with a clear message. Normalize and Duck also check each quiet-save outcome, stop at the first failed or refused write and report it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9849ba6 to
c0ac120
Compare
Summary
Loudness in Studio: Normalize loudness and Duck under voice in the clip menu, backed by a shared loudness module that also measures videos. The export now reports how far the true-peak limiter turned the mix down.
Changes
@hyperframes/studio-server/loudness(measurement with an absolute target and video support), a measure-only normalize endpoint, and the subpaths registered in the package descriptors.normalize-audiouses the shared module and accepts video.clipMenuNormalize,clipMenuDuck).audioLoweredDbon the render job, the SSE complete payload and the/rendersrecord (omitted when nothing was lowered).Testing
Static gates on the layer tip:
bun install,bun run build,tsc --noEmitfor every touched package (0 errors),oxlintandoxfmt --checkon files changed vs main (clean),gen:skills-manifest --check(in sync),scripts/comment-ratchet.mjs(ok).audioPadTrim37 (bun test),assembleStage16,extractVideosStage21. All pass.cloud/downloadstream test is load-flaky (passes alone, 12/12).Notes
normalize-audio.tschurn is a move into the shared loudness module.🤖 Generated with Claude Code
Before
After
Clip menu: Normalize loudness added. Duck under voice shows only when a voice overlaps.