Skip to content

fix(producer): preserve runtime audio variables in distributed plans#2725

Merged
jrusso1020 merged 2 commits into
mainfrom
fix/distributed-runtime-audio-variables
Jul 21, 2026
Merged

fix(producer): preserve runtime audio variables in distributed plans#2725
jrusso1020 merged 2 commits into
mainfrom
fix/distributed-runtime-audio-variables

Conversation

@jrusso1020

Copy link
Copy Markdown
Collaborator

Summary

  • pass render-time variables into the synthetic job used by distributed planning and browser probing
  • prefer the browser runtime media duration when compiler-generated data-end still reflects a placeholder
  • cover variable propagation and stale media-end reconciliation with focused unit tests

Root cause

Distributed render chunks received the runtime variables, but the synthetic RenderJob used 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-end to the placeholder duration. When the browser replaced the source and data-duration at 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 passed
  • bun test packages/producer/src/services/render/shared.test.ts — 3 passed
  • bun test packages/producer/src/services/render/stages/probeStage.test.ts — 29 passed
  • bun run --cwd packages/producer typecheck — passed
  • pre-commit lint, format, fallow audit, tracked-artifact check, and typecheck — passed

The 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:

  • 65.295s source narration remained active through the full narration window in the rendered output
  • 56.703s source narration remained active through the full narration window in the rendered output
  • interior silence boundaries matched the source files at the composition lead-in offset, confirming the outputs used the complete customer voiceovers rather than placeholders

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.

@miga-heygen miga-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.

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.variablesbuildSyntheticRenderJobRenderConfig.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

  1. Variable propagation chain. plan() at line 782 now passes variables: config.variables into buildSyntheticRenderJob. SyntheticRenderJobInput gains variables?: RenderConfig["variables"]. buildSyntheticRenderJob threads it into the RenderConfigcreateRenderJob. 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 on job.config.variables. ✓

  2. resolveBrowserMediaEnd logic. Number.isFinite(duration) && duration > 0 ? start + duration : end. Prefers the runtime data-duration (authored/variable-substituted) over the compiler-generated data-end (which may be clamped to a placeholder). Falls back to data-end when duration is NaN, 0, negative, or Infinity. Tests cover: runtime > stale end, start offset projection, NaN fallback, zero fallback. ✓

  3. All 4 call sites in probeStage.ts updated. Every el.end that feeds into projectedEnd computation or media-element registration is now wrapped in resolveBrowserMediaEnd(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. ✓

  4. No missed el.end sites. The 4 replacements cover every el.end usage that feeds into media extraction windows. The remaining el.start and el.mediaStart references are unaffected (start is not stale — only the compiler-clamped end can lag behind a runtime source swap). ✓

  5. 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 valid data-end. ✓
  6. Mock in probeStage.test.ts. The mock for resolveBrowserMediaEnd replicates 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 vanceingalls 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.

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.end reads in probeStage.ts are consistently wrapped (dedup projection + struct build × 2 element categories). No straggler at the head SHA.
  • Three unit tests in shared.test.ts cover 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.end behavior anywhere duration is 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 james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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-duration on a media element after applyVariableBindings swaps src — the runtime consensus (init.ts:2882/2915, media.ts:97) reads data-duration as authoritative, so whichever path populates it, this fix aligns with that convention.

Review by Rames D Jusso

@jrusso1020

Copy link
Copy Markdown
Collaborator Author

Valid

@jrusso1020

Copy link
Copy Markdown
Collaborator Author

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).

@jrusso1020
jrusso1020 merged commit 465c9e7 into main Jul 21, 2026
49 checks passed
@jrusso1020
jrusso1020 deleted the fix/distributed-runtime-audio-variables branch July 21, 2026 23:49
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.

4 participants