mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 14:50:02 +00:00
fix(producer): preserve runtime audio variables in distributed plans (#2725)
* fix(producer): preserve runtime audio variables in distributed plans * fix(producer): prefer runtime duration for volume sampling
This commit is contained in:
@@ -81,6 +81,25 @@ describe("distributed warning policy", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("distributed synthetic render job", () => {
|
||||
it("threads render variables into the plan browser probe job", () => {
|
||||
const variables = {
|
||||
voiceoverSrc: "assets/voiceover.wav",
|
||||
narrationDurationSeconds: 56.738,
|
||||
};
|
||||
const job = buildSyntheticRenderJob({
|
||||
fps: { num: 30, den: 1 },
|
||||
format: "mp4",
|
||||
quality: "high",
|
||||
hdrMode: "force-sdr",
|
||||
entryFile: "index.html",
|
||||
variables,
|
||||
});
|
||||
|
||||
expect(job.config.variables).toEqual(variables);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveChunkPlan", () => {
|
||||
it("returns 1 chunk when totalFrames fits in configChunkSize", () => {
|
||||
const result = resolveChunkPlan(60, 240, 16);
|
||||
|
||||
@@ -782,6 +782,7 @@ export async function plan(
|
||||
entryFile: config.entryFile ?? "index.html",
|
||||
logger: config.logger,
|
||||
producerConfig: cfg,
|
||||
variables: config.variables,
|
||||
});
|
||||
const entryFile = config.entryFile ?? "index.html";
|
||||
const htmlPath = join(projectDir, entryFile);
|
||||
|
||||
@@ -106,6 +106,8 @@ export interface SyntheticRenderJobInput {
|
||||
entryFile: string;
|
||||
logger?: ProducerLogger;
|
||||
producerConfig?: RenderConfig["producerConfig"];
|
||||
/** Render-time overrides consumed by the plan browser probe. */
|
||||
variables?: RenderConfig["variables"];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,6 +135,7 @@ export function buildSyntheticRenderJob(input: SyntheticRenderJobInput): RenderJ
|
||||
hdrMode: input.hdrMode,
|
||||
strictness: input.strictness,
|
||||
producerConfig: input.producerConfig,
|
||||
variables: input.variables,
|
||||
};
|
||||
return createRenderJob(renderConfig);
|
||||
}
|
||||
|
||||
@@ -1763,11 +1763,11 @@ h1 { font-size: 2rem; }`;
|
||||
});
|
||||
|
||||
describe("discoverAudioVolumeAutomationFromTimeline", () => {
|
||||
it("samples video-derived audio volume without firing GSAP callbacks", async () => {
|
||||
it("prefers runtime duration over stale data-end while sampling video-derived audio", async () => {
|
||||
class TestAudioElement {}
|
||||
class TestVideoElement {
|
||||
id = "bg-video";
|
||||
dataset = { start: "0", duration: "1", volume: "0" };
|
||||
dataset = { start: "0", end: "0.25", duration: "1", volume: "0" };
|
||||
volume = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -2134,10 +2134,10 @@ export async function discoverAudioVolumeAutomationFromTimeline(
|
||||
const endAttr = Number.parseFloat(el.dataset.end ?? "");
|
||||
const durationAttr = Number.parseFloat(el.dataset.duration ?? "");
|
||||
const end =
|
||||
Number.isFinite(endAttr) && endAttr > start
|
||||
? endAttr
|
||||
: Number.isFinite(durationAttr) && durationAttr > 0
|
||||
? start + durationAttr
|
||||
Number.isFinite(durationAttr) && durationAttr > 0
|
||||
? start + durationAttr
|
||||
: Number.isFinite(endAttr) && endAttr > start
|
||||
? endAttr
|
||||
: duration;
|
||||
const sampleStart = Math.max(0, start);
|
||||
const sampleEnd = Math.min(duration, end);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { resolveBrowserMediaEnd } from "./shared.js";
|
||||
|
||||
describe("resolveBrowserMediaEnd", () => {
|
||||
it("prefers a runtime duration over a stale compiler-clamped end", () => {
|
||||
expect(resolveBrowserMediaEnd(0, 5.04, 56.738)).toBe(56.738);
|
||||
});
|
||||
|
||||
it("projects a runtime duration from the browser-local start", () => {
|
||||
expect(resolveBrowserMediaEnd(2, 7.04, 56.738)).toBe(58.738);
|
||||
});
|
||||
|
||||
it("falls back to data-end when runtime duration is unavailable", () => {
|
||||
expect(resolveBrowserMediaEnd(0, 5.04, Number.NaN)).toBe(5.04);
|
||||
expect(resolveBrowserMediaEnd(0, 5.04, 0)).toBe(5.04);
|
||||
});
|
||||
});
|
||||
@@ -53,6 +53,19 @@ export interface CompositionMetadata {
|
||||
*/
|
||||
export const BROWSER_MEDIA_EPSILON = 0.0001;
|
||||
|
||||
/**
|
||||
* Resolve the browser/runtime end for a media element.
|
||||
*
|
||||
* `data-end` is compiler-generated metadata, while `data-duration` is the
|
||||
* authored/runtime value. A render variable can replace a placeholder source
|
||||
* and update `data-duration` after compilation, leaving the compiler-clamped
|
||||
* `data-end` stale. Prefer the live duration when present so the audio/video
|
||||
* extraction window follows the runtime media slot.
|
||||
*/
|
||||
export function resolveBrowserMediaEnd(start: number, end: number, duration: number): number {
|
||||
return Number.isFinite(duration) && duration > 0 ? start + duration : end;
|
||||
}
|
||||
|
||||
export function writeFileExclusiveSync(path: string, data: NodeJS.ArrayBufferView | string): void {
|
||||
try {
|
||||
writeFileSync(path, data, { flag: "wx", mode: 0o600 });
|
||||
|
||||
@@ -103,6 +103,8 @@ mock.module("../../htmlCompiler.js", () => ({
|
||||
mock.module("../shared.js", () => ({
|
||||
BROWSER_MEDIA_EPSILON: 0.0001,
|
||||
projectBrowserEndToCompositionTimeline: () => 0,
|
||||
resolveBrowserMediaEnd: (_start: number, end: number, duration: number) =>
|
||||
Number.isFinite(duration) && duration > 0 ? _start + duration : end,
|
||||
writeCompiledArtifacts: () => {},
|
||||
}));
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ import type { ProducerLogger } from "../../../logger.js";
|
||||
import {
|
||||
BROWSER_MEDIA_EPSILON,
|
||||
projectBrowserEndToCompositionTimeline,
|
||||
resolveBrowserMediaEnd,
|
||||
writeCompiledArtifacts,
|
||||
type CompositionMetadata,
|
||||
} from "../shared.js";
|
||||
@@ -454,7 +455,7 @@ export async function runProbeStage(input: ProbeStageInput): Promise<ProbeStageR
|
||||
const projectedEnd = projectBrowserEndToCompositionTimeline(
|
||||
existing.start,
|
||||
el.start,
|
||||
el.end,
|
||||
resolveBrowserMediaEnd(el.start, el.end, el.duration),
|
||||
);
|
||||
if (
|
||||
projectedEnd > 0 &&
|
||||
@@ -482,7 +483,7 @@ export async function runProbeStage(input: ProbeStageInput): Promise<ProbeStageR
|
||||
id: el.id,
|
||||
src,
|
||||
start: el.start,
|
||||
end: el.end,
|
||||
end: resolveBrowserMediaEnd(el.start, el.end, el.duration),
|
||||
mediaStart: el.mediaStart,
|
||||
loop: el.loop,
|
||||
hasAudio: el.hasAudio && !el.muted,
|
||||
@@ -500,7 +501,7 @@ export async function runProbeStage(input: ProbeStageInput): Promise<ProbeStageR
|
||||
const projectedEnd = projectBrowserEndToCompositionTimeline(
|
||||
existing.start,
|
||||
el.start,
|
||||
el.end,
|
||||
resolveBrowserMediaEnd(el.start, el.end, el.duration),
|
||||
);
|
||||
if (
|
||||
projectedEnd > 0 &&
|
||||
@@ -527,7 +528,7 @@ export async function runProbeStage(input: ProbeStageInput): Promise<ProbeStageR
|
||||
id: el.id,
|
||||
src,
|
||||
start: el.start,
|
||||
end: el.end,
|
||||
end: resolveBrowserMediaEnd(el.start, el.end, el.duration),
|
||||
mediaStart: el.mediaStart,
|
||||
layer: 0,
|
||||
volume: el.volume,
|
||||
|
||||
Reference in New Issue
Block a user