mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 14:40:58 +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", () => {
|
describe("resolveChunkPlan", () => {
|
||||||
it("returns 1 chunk when totalFrames fits in configChunkSize", () => {
|
it("returns 1 chunk when totalFrames fits in configChunkSize", () => {
|
||||||
const result = resolveChunkPlan(60, 240, 16);
|
const result = resolveChunkPlan(60, 240, 16);
|
||||||
|
|||||||
@@ -782,6 +782,7 @@ export async function plan(
|
|||||||
entryFile: config.entryFile ?? "index.html",
|
entryFile: config.entryFile ?? "index.html",
|
||||||
logger: config.logger,
|
logger: config.logger,
|
||||||
producerConfig: cfg,
|
producerConfig: cfg,
|
||||||
|
variables: config.variables,
|
||||||
});
|
});
|
||||||
const entryFile = config.entryFile ?? "index.html";
|
const entryFile = config.entryFile ?? "index.html";
|
||||||
const htmlPath = join(projectDir, entryFile);
|
const htmlPath = join(projectDir, entryFile);
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ export interface SyntheticRenderJobInput {
|
|||||||
entryFile: string;
|
entryFile: string;
|
||||||
logger?: ProducerLogger;
|
logger?: ProducerLogger;
|
||||||
producerConfig?: RenderConfig["producerConfig"];
|
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,
|
hdrMode: input.hdrMode,
|
||||||
strictness: input.strictness,
|
strictness: input.strictness,
|
||||||
producerConfig: input.producerConfig,
|
producerConfig: input.producerConfig,
|
||||||
|
variables: input.variables,
|
||||||
};
|
};
|
||||||
return createRenderJob(renderConfig);
|
return createRenderJob(renderConfig);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1763,11 +1763,11 @@ h1 { font-size: 2rem; }`;
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("discoverAudioVolumeAutomationFromTimeline", () => {
|
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 TestAudioElement {}
|
||||||
class TestVideoElement {
|
class TestVideoElement {
|
||||||
id = "bg-video";
|
id = "bg-video";
|
||||||
dataset = { start: "0", duration: "1", volume: "0" };
|
dataset = { start: "0", end: "0.25", duration: "1", volume: "0" };
|
||||||
volume = 0;
|
volume = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2134,10 +2134,10 @@ export async function discoverAudioVolumeAutomationFromTimeline(
|
|||||||
const endAttr = Number.parseFloat(el.dataset.end ?? "");
|
const endAttr = Number.parseFloat(el.dataset.end ?? "");
|
||||||
const durationAttr = Number.parseFloat(el.dataset.duration ?? "");
|
const durationAttr = Number.parseFloat(el.dataset.duration ?? "");
|
||||||
const end =
|
const end =
|
||||||
Number.isFinite(endAttr) && endAttr > start
|
Number.isFinite(durationAttr) && durationAttr > 0
|
||||||
? endAttr
|
|
||||||
: Number.isFinite(durationAttr) && durationAttr > 0
|
|
||||||
? start + durationAttr
|
? start + durationAttr
|
||||||
|
: Number.isFinite(endAttr) && endAttr > start
|
||||||
|
? endAttr
|
||||||
: duration;
|
: duration;
|
||||||
const sampleStart = Math.max(0, start);
|
const sampleStart = Math.max(0, start);
|
||||||
const sampleEnd = Math.min(duration, end);
|
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;
|
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 {
|
export function writeFileExclusiveSync(path: string, data: NodeJS.ArrayBufferView | string): void {
|
||||||
try {
|
try {
|
||||||
writeFileSync(path, data, { flag: "wx", mode: 0o600 });
|
writeFileSync(path, data, { flag: "wx", mode: 0o600 });
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ mock.module("../../htmlCompiler.js", () => ({
|
|||||||
mock.module("../shared.js", () => ({
|
mock.module("../shared.js", () => ({
|
||||||
BROWSER_MEDIA_EPSILON: 0.0001,
|
BROWSER_MEDIA_EPSILON: 0.0001,
|
||||||
projectBrowserEndToCompositionTimeline: () => 0,
|
projectBrowserEndToCompositionTimeline: () => 0,
|
||||||
|
resolveBrowserMediaEnd: (_start: number, end: number, duration: number) =>
|
||||||
|
Number.isFinite(duration) && duration > 0 ? _start + duration : end,
|
||||||
writeCompiledArtifacts: () => {},
|
writeCompiledArtifacts: () => {},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import type { ProducerLogger } from "../../../logger.js";
|
|||||||
import {
|
import {
|
||||||
BROWSER_MEDIA_EPSILON,
|
BROWSER_MEDIA_EPSILON,
|
||||||
projectBrowserEndToCompositionTimeline,
|
projectBrowserEndToCompositionTimeline,
|
||||||
|
resolveBrowserMediaEnd,
|
||||||
writeCompiledArtifacts,
|
writeCompiledArtifacts,
|
||||||
type CompositionMetadata,
|
type CompositionMetadata,
|
||||||
} from "../shared.js";
|
} from "../shared.js";
|
||||||
@@ -454,7 +455,7 @@ export async function runProbeStage(input: ProbeStageInput): Promise<ProbeStageR
|
|||||||
const projectedEnd = projectBrowserEndToCompositionTimeline(
|
const projectedEnd = projectBrowserEndToCompositionTimeline(
|
||||||
existing.start,
|
existing.start,
|
||||||
el.start,
|
el.start,
|
||||||
el.end,
|
resolveBrowserMediaEnd(el.start, el.end, el.duration),
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
projectedEnd > 0 &&
|
projectedEnd > 0 &&
|
||||||
@@ -482,7 +483,7 @@ export async function runProbeStage(input: ProbeStageInput): Promise<ProbeStageR
|
|||||||
id: el.id,
|
id: el.id,
|
||||||
src,
|
src,
|
||||||
start: el.start,
|
start: el.start,
|
||||||
end: el.end,
|
end: resolveBrowserMediaEnd(el.start, el.end, el.duration),
|
||||||
mediaStart: el.mediaStart,
|
mediaStart: el.mediaStart,
|
||||||
loop: el.loop,
|
loop: el.loop,
|
||||||
hasAudio: el.hasAudio && !el.muted,
|
hasAudio: el.hasAudio && !el.muted,
|
||||||
@@ -500,7 +501,7 @@ export async function runProbeStage(input: ProbeStageInput): Promise<ProbeStageR
|
|||||||
const projectedEnd = projectBrowserEndToCompositionTimeline(
|
const projectedEnd = projectBrowserEndToCompositionTimeline(
|
||||||
existing.start,
|
existing.start,
|
||||||
el.start,
|
el.start,
|
||||||
el.end,
|
resolveBrowserMediaEnd(el.start, el.end, el.duration),
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
projectedEnd > 0 &&
|
projectedEnd > 0 &&
|
||||||
@@ -527,7 +528,7 @@ export async function runProbeStage(input: ProbeStageInput): Promise<ProbeStageR
|
|||||||
id: el.id,
|
id: el.id,
|
||||||
src,
|
src,
|
||||||
start: el.start,
|
start: el.start,
|
||||||
end: el.end,
|
end: resolveBrowserMediaEnd(el.start, el.end, el.duration),
|
||||||
mediaStart: el.mediaStart,
|
mediaStart: el.mediaStart,
|
||||||
layer: 0,
|
layer: 0,
|
||||||
volume: el.volume,
|
volume: el.volume,
|
||||||
|
|||||||
Reference in New Issue
Block a user