fix(producer): preserve runtime audio variables in distributed plans#2725
Conversation
miga-heygen
left a comment
There was a problem hiding this comment.
SSOT Review: fix(producer): preserve runtime audio variables in distributed plans
PR #2725 — 7 files, +60/-4
SSOT inventory
| Concept | Source of truth | Consumers | Verdict |
|---|---|---|---|
| Runtime variable propagation | SyntheticRenderJobInput.variables → buildSyntheticRenderJob → RenderConfig.variables |
plan() |
Single owner |
| Media end reconciliation | resolveBrowserMediaEnd(start, end, duration) |
runProbeStage (4 call sites) |
Single owner |
BROWSER_MEDIA_EPSILON |
shared.ts (pre-existing) |
runProbeStage |
Single owner |
What I checked
-
Variable propagation chain.
plan()at line 782 now passesvariables: config.variablesintobuildSyntheticRenderJob.SyntheticRenderJobInputgainsvariables?: RenderConfig["variables"].buildSyntheticRenderJobthreads it into theRenderConfig→createRenderJob. The synthetic job used by the plan browser probe now carries the same runtime overrides that distributed chunks receive. Test verifies the variable object lands onjob.config.variables. ✓ -
resolveBrowserMediaEndlogic.Number.isFinite(duration) && duration > 0 ? start + duration : end. Prefers the runtimedata-duration(authored/variable-substituted) over the compiler-generateddata-end(which may be clamped to a placeholder). Falls back todata-endwhen duration is NaN, 0, negative, or Infinity. Tests cover: runtime > stale end, start offset projection, NaN fallback, zero fallback. ✓ -
All 4 call sites in
probeStage.tsupdated. Everyel.endthat feeds intoprojectedEndcomputation or media-element registration is now wrapped inresolveBrowserMediaEnd(el.start, el.end, el.duration):- Line 457: video projected-end reconciliation (existing media, update end)
- Line 484: video media-element registration (new entry)
- Line 503: audio projected-end reconciliation (existing media, update end)
- Line 530: audio media-element registration (new entry)
Both video and audio paths, both new-entry and existing-update paths. Complete coverage. ✓
-
No missed
el.endsites. The 4 replacements cover everyel.endusage that feeds into media extraction windows. The remainingel.startandel.mediaStartreferences are unaffected (start is not stale — only the compiler-clamped end can lag behind a runtime source swap). ✓ -
Sentinel-value scan:
Number.isFinite(duration)— rejects NaN and ±Infinity. ✓duration > 0— rejects zero and negative. ✓- Fallback to
end(compiler-generated) when no runtime duration — correct, since a composition without variable-bound audio has a validdata-end. ✓
-
Mock in
probeStage.test.ts. The mock forresolveBrowserMediaEndreplicates the real implementation inline. This is correct — the probe stage tests test the probe stage, not the utility; the utility has its own focused tests. ✓
Blocking SSOT issues
None found.
Verdict
Approve. Clean two-part fix: (1) thread variables into the synthetic plan job so the browser probe discovers runtime media, not placeholders; (2) prefer data-duration over data-end when they disagree, so the audio extraction window follows the runtime media slot instead of the stale compiler-clamped end. The root cause (plan/probe metadata mismatch + stale end reconciliation) is correctly addressed at both layers, and the customer-project validation on 0.7.67-alpha.0 confirms full narration windows in rendered output.
--- Miga
vanceingalls
left a comment
There was a problem hiding this comment.
R1: APPROVE. Two independent fixes bundled, both correctly scoped and focus-tested. Alpha-deploy validation on 12 dev producer replicas with two real affected customer projects (65.295s + 56.703s narrations) is the strongest live signal in the PR body.
Fix 1 — variable propagation into the synthetic RenderJob. SyntheticRenderJobInput gains an optional variables?: RenderConfig["variables"] field; plan.ts threads config.variables through the sole call site. Additive on the input contract — any caller not passing variables is unaffected. Unit test in plan.test.ts verifies buildSyntheticRenderJob({ variables }).config.variables === variables.
Fix 2 — prefer runtime data-duration over stale compiler-clamped data-end.
- New pure helper
resolveBrowserMediaEnd(start, end, duration):Number.isFinite(duration) && duration > 0 ? start + duration : end - Grep confirms all four
el.endreads inprobeStage.tsare consistently wrapped (dedup projection + struct build × 2 element categories). No straggler at the head SHA. - Three unit tests in
shared.test.tscover the runtime-preferred, browser-local-start-projected, and NaN/0-fallback cases. Numbers check out arithmetically. - Defensive gate excludes 0/negative/NaN/Infinity — good shape; preserves the old
el.endbehavior anywheredurationis unset.
Semantic sanity.
data-end is compiler-generated metadata (may reflect a placeholder source). data-duration is the runtime-observed value that the browser updates after variable substitution replaces the source. Preferring start + duration when the runtime value is present correctly follows the runtime media slot — matches the reported failure mode (short extraction window followed by silence padding after runtime source swap).
Passing the resolved end into projectBrowserEndToCompositionTimeline at both dedup sites is right — the browser→composition projection should be based on the actual runtime duration, not the stale compiler-clamped end.
CI. Detect changes / CodeQL / Format / Lint / Fallow / Typecheck / Test / Producer unit + integration / SDK / CLI smoke / Windows tests / player-perf / preview-regression / Semantic PR title — all green. Linux regression shards + Windows render still in progress; nothing red at time of review.
One tiny stylistic nit (non-blocking). The probeStage.test.ts mock reproduces resolveBrowserMediaEnd's logic inline rather than importing the real helper. That's the common mock.module shape here so I don't think it's worth touching, but if you're ever untangling this mock in a follow-up, importing from shared.js would remove the duplication and prevent drift.
Diagnosis reframing is clean too — separating this from AAC muxing failure and explicitly noting independence from EF #42776 saves the next investigator a false-positive hunt.
— Via
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
LGTM from my side — small, focused fix that aligns the producer probe with the runtime path (init.ts:2882 and init.ts:2915 already treat data-duration as authoritative and don't fall back to data-end). Test coverage on resolveBrowserMediaEnd plus the plan-side variable-plumbing assertion is sufficient. Compilation-tester invariant (data-end == data-start + data-duration) still holds for statically-consistent HTML — the fallback branch (Number.isFinite(duration) && duration > 0 ? ... : end) keeps existing behavior intact when the two agree.
Concern (non-blocking, scope question)
discoverAudioVolumeAutomationFromTimeline at packages/producer/src/services/htmlCompiler.ts:2136-2141 uses the inverse precedence.
const end =
Number.isFinite(endAttr) && endAttr > start
? endAttr // prefers data-end
: Number.isFinite(durationAttr) && durationAttr > 0
? start + durationAttr // falls back to data-duration
: duration;Same probe flow (called from probeStage.ts:547 right after discoverMediaFromBrowser at line 428), same audios, same runtime-variable premise — if the compiler-clamped data-end is stale for a variable-bound audio that also has scripted volume automation, the sampler picks endAttr = start + placeholder_duration (the stale short window), computes sampleEnd = Math.min(compositionDuration, end), and misses every keyframe past the placeholder end. That's the same failure mode as the reported extraction-window bug, one layer up the stack.
It's not the reported symptom (silence-padded extraction, not clipped volume automation) so I have no repro claim — but structurally the sibling path is at risk under the same conditions. Worth deciding: extract resolveBrowserMediaEnd into a shared browser-visible helper and mirror the fix in the volume-automation sampler, OR explicitly scope-note that variable-bound audio with scripted volume automation is out of scope for this PR.
Nits
None.
What I didn't verify
- End-to-end reproduction of the volume-automation sibling scenario — flagged from code-reading only, not from a repro. If you have a variable-bound-audio-with-scripted-volume test project handy, that would settle it.
- The exact runtime write-path that updates
data-durationon a media element afterapplyVariableBindingsswapssrc— the runtime consensus (init.ts:2882/2915, media.ts:97) readsdata-durationas authoritative, so whichever path populates it, this fix aligns with that convention.
|
Valid |
|
Fixed in 8489979: scripted audio-volume sampling now gives runtime data-duration the same precedence over stale data-end, with a regression test covering a 0.25s stale end and 1s runtime duration. The four focused suites pass (177 tests). |
Summary
data-endstill reflects a placeholderRoot cause
Distributed render chunks received the runtime variables, but the synthetic
RenderJobused by the plan/browser-probe stage did not. For variable-bound audio, the plan could therefore discover the placeholder source while chunks rendered the runtime composition.The static compiler could also clamp
data-endto the placeholder duration. When the browser replaced the source anddata-durationat runtime, reconciliation continued to use the stale end, producing a short audio extraction window followed by silence padding.This was a distributed planning/probe metadata issue, not an AAC muxing failure.
Validation
bun test packages/producer/src/services/distributed/plan.test.ts— 41 passedbun test packages/producer/src/services/render/shared.test.ts— 3 passedbun test packages/producer/src/services/render/stages/probeStage.test.ts— 29 passedbun run --cwd packages/producer typecheck— passedThe equivalent patch was also published as
0.7.67-alpha.0, deployed to all 12 dev producer replicas, and tested with both exact affected customer projects and their original runtime-variable payloads:Release CI: https://github.com/heygen-com/hyperframes/actions/runs/29870778955
Dev deployment: https://github.com/heygen-com/hyperframes-internal/actions/runs/29871146880
Scope
This is independent of experiment-framework PR 42776. That PR loop-fills genuinely short Astral-generated background-music assets before render; this change fixes distributed runtime-variable propagation and media timing for already-complete inputs.