mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +00:00
refactor(producer): extract compileStage from executeRenderJob
Move the pure compile sub-stage (`compileForRender` + `applyRenderModeHints` + `writeCompiledArtifacts` + `CompositionMetadata` build + DPR resolution) out of `executeRenderJob` into `services/render/stages/compileStage.ts`. No behavior change. The sequencer calls `runCompileStage` at the same code point with identical inputs and outputs. The following invariants are preserved verbatim: - `cfg.forceScreenshot` is still mutated by `applyRenderModeHints`. - `perfStages.compileOnlyMs` is set to the same wall-clock interval (around the `compileForRender` call only). - The "Compiled composition metadata" log line is emitted after artifact writes with the same payload shape. - The "Supersampling composition via deviceScaleFactor" log line is emitted only when `deviceScaleFactor > 1`. - `stage1Start`, `updateJobStatus(..., "Compiling composition", 5, ...)`, and `perfStages.compileMs` (set at the end of probe) remain at their current code points in the sequencer. The probe sub-stage (`if (needsBrowser)`) is unchanged — it is extracted separately in PR 1.3. `recompileWithResolutions` lives inside the probe block because it depends on browser-resolved durations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* compileStage — pure compile pass of `executeRenderJob`.
|
||||
*
|
||||
* Runs `compileForRender` on the entry HTML, applies render-mode hints
|
||||
* (which may flip `cfg.forceScreenshot` on for compositions that need it),
|
||||
* writes compiled artifacts to `workDir/compiled/`, builds the
|
||||
* `CompositionMetadata` view of the result, and resolves the
|
||||
* `deviceScaleFactor` for supersampling.
|
||||
*
|
||||
* The probe sub-stage (browser launch, duration discovery, recompile,
|
||||
* media reconciliation) is extracted separately in PR 1.3. This stage
|
||||
* stops at the point where the in-process renderer formerly entered the
|
||||
* `if (needsBrowser)` branch.
|
||||
*
|
||||
* Hard constraints preserved verbatim from the in-process renderer:
|
||||
* - `applyRenderModeHints(cfg, ...)` is allowed to mutate `cfg.forceScreenshot`.
|
||||
* - `perfStages.compileOnlyMs` is set to wall-clock ms around the
|
||||
* `compileForRender` call only.
|
||||
* - The `log.info("Compiled composition metadata", ...)` line is emitted
|
||||
* after writing artifacts, with the same payload shape as before.
|
||||
* - The `log.info("Supersampling composition via deviceScaleFactor", ...)`
|
||||
* line is emitted only when `deviceScaleFactor > 1`.
|
||||
*/
|
||||
|
||||
import { join } from "node:path";
|
||||
import type { EngineConfig } from "@hyperframes/engine";
|
||||
import type { CompiledComposition } from "../../htmlCompiler.js";
|
||||
import { compileForRender } from "../../htmlCompiler.js";
|
||||
import type { ProducerLogger } from "../../../logger.js";
|
||||
import {
|
||||
applyRenderModeHints,
|
||||
resolveDeviceScaleFactor,
|
||||
writeCompiledArtifacts,
|
||||
type CompositionMetadata,
|
||||
type RenderJob,
|
||||
} from "../../renderOrchestrator.js";
|
||||
|
||||
export interface CompileStageInput {
|
||||
projectDir: string;
|
||||
workDir: string;
|
||||
/** Absolute path to the entry HTML (already resolved to standalone-entry if needed). */
|
||||
htmlPath: string;
|
||||
/** The relative `entryFile` string, used only for log payloads. */
|
||||
entryFile: string;
|
||||
job: RenderJob;
|
||||
/** EngineConfig — may be mutated via `cfg.forceScreenshot = true`. */
|
||||
cfg: EngineConfig;
|
||||
/** True when the output format requires an alpha channel (webm/mov/png-sequence). */
|
||||
needsAlpha: boolean;
|
||||
log: ProducerLogger;
|
||||
/** Cooperative-cancellation probe; throws `RenderCancelledError` when aborted. */
|
||||
assertNotAborted: () => void;
|
||||
}
|
||||
|
||||
export interface CompileStageResult {
|
||||
compiled: CompiledComposition;
|
||||
composition: CompositionMetadata;
|
||||
deviceScaleFactor: number;
|
||||
outputWidth: number;
|
||||
outputHeight: number;
|
||||
/** Wall-clock ms for the pure `compileForRender` call only (excludes artifact writes). */
|
||||
compileOnlyMs: number;
|
||||
}
|
||||
|
||||
export async function runCompileStage(input: CompileStageInput): Promise<CompileStageResult> {
|
||||
const { projectDir, workDir, htmlPath, entryFile, job, cfg, needsAlpha, log, assertNotAborted } =
|
||||
input;
|
||||
|
||||
const compileStart = Date.now();
|
||||
const compiled = await compileForRender(projectDir, htmlPath, join(workDir, "downloads"));
|
||||
assertNotAborted();
|
||||
const compileOnlyMs = Date.now() - compileStart;
|
||||
applyRenderModeHints(cfg, compiled, log);
|
||||
writeCompiledArtifacts(compiled, workDir, Boolean(job.config.debug));
|
||||
|
||||
log.info("Compiled composition metadata", {
|
||||
entryFile,
|
||||
staticDuration: compiled.staticDuration,
|
||||
width: compiled.width,
|
||||
height: compiled.height,
|
||||
videoCount: compiled.videos.length,
|
||||
audioCount: compiled.audios.length,
|
||||
renderModeHints: compiled.renderModeHints,
|
||||
});
|
||||
|
||||
const composition: CompositionMetadata = {
|
||||
duration: compiled.staticDuration,
|
||||
videos: compiled.videos,
|
||||
audios: compiled.audios,
|
||||
images: compiled.images,
|
||||
width: compiled.width,
|
||||
height: compiled.height,
|
||||
};
|
||||
const { width, height } = composition;
|
||||
const deviceScaleFactor = resolveDeviceScaleFactor({
|
||||
compositionWidth: width,
|
||||
compositionHeight: height,
|
||||
outputResolution: job.config.outputResolution,
|
||||
hdrRequested: job.config.hdrMode === "force-hdr",
|
||||
alphaRequested: needsAlpha,
|
||||
});
|
||||
const outputWidth = width * deviceScaleFactor;
|
||||
const outputHeight = height * deviceScaleFactor;
|
||||
if (deviceScaleFactor > 1) {
|
||||
log.info("Supersampling composition via deviceScaleFactor", {
|
||||
compositionWidth: width,
|
||||
compositionHeight: height,
|
||||
outputResolution: job.config.outputResolution,
|
||||
outputWidth,
|
||||
outputHeight,
|
||||
deviceScaleFactor,
|
||||
});
|
||||
}
|
||||
|
||||
return { compiled, composition, deviceScaleFactor, outputWidth, outputHeight, compileOnlyMs };
|
||||
}
|
||||
@@ -109,7 +109,6 @@ import { freemem } from "os";
|
||||
import { fileURLToPath } from "url";
|
||||
import { createFileServer, type FileServerHandle, VIRTUAL_TIME_SHIM } from "./fileServer.js";
|
||||
import {
|
||||
compileForRender,
|
||||
resolveCompositionDurations,
|
||||
recompileWithResolutions,
|
||||
discoverMediaFromBrowser,
|
||||
@@ -121,6 +120,7 @@ import {
|
||||
type HdrImageTransferCache,
|
||||
createHdrImageTransferCache,
|
||||
} from "./hdrImageTransferCache.js";
|
||||
import { runCompileStage } from "./render/stages/compileStage.js";
|
||||
|
||||
/**
|
||||
* Wrap a cleanup operation so it never throws, but logs any failure.
|
||||
@@ -2125,51 +2125,22 @@ export async function executeRenderJob(
|
||||
const stage1Start = Date.now();
|
||||
updateJobStatus(job, "preprocessing", "Compiling composition", 5, onProgress);
|
||||
|
||||
const compileStart = Date.now();
|
||||
let compiled = await compileForRender(projectDir, htmlPath, join(workDir, "downloads"));
|
||||
assertNotAborted();
|
||||
perfStages.compileOnlyMs = Date.now() - compileStart;
|
||||
applyRenderModeHints(cfg, compiled, log);
|
||||
writeCompiledArtifacts(compiled, workDir, Boolean(job.config.debug));
|
||||
|
||||
log.info("Compiled composition metadata", {
|
||||
const compileResult = await runCompileStage({
|
||||
projectDir,
|
||||
workDir,
|
||||
htmlPath,
|
||||
entryFile,
|
||||
staticDuration: compiled.staticDuration,
|
||||
width: compiled.width,
|
||||
height: compiled.height,
|
||||
videoCount: compiled.videos.length,
|
||||
audioCount: compiled.audios.length,
|
||||
renderModeHints: compiled.renderModeHints,
|
||||
job,
|
||||
cfg,
|
||||
needsAlpha,
|
||||
log,
|
||||
assertNotAborted,
|
||||
});
|
||||
|
||||
const composition: CompositionMetadata = {
|
||||
duration: compiled.staticDuration,
|
||||
videos: compiled.videos,
|
||||
audios: compiled.audios,
|
||||
images: compiled.images,
|
||||
width: compiled.width,
|
||||
height: compiled.height,
|
||||
};
|
||||
let compiled = compileResult.compiled;
|
||||
const composition = compileResult.composition;
|
||||
const { deviceScaleFactor, outputWidth, outputHeight } = compileResult;
|
||||
const { width, height } = composition;
|
||||
const deviceScaleFactor = resolveDeviceScaleFactor({
|
||||
compositionWidth: width,
|
||||
compositionHeight: height,
|
||||
outputResolution: job.config.outputResolution,
|
||||
hdrRequested: job.config.hdrMode === "force-hdr",
|
||||
alphaRequested: needsAlpha,
|
||||
});
|
||||
const outputWidth = width * deviceScaleFactor;
|
||||
const outputHeight = height * deviceScaleFactor;
|
||||
if (deviceScaleFactor > 1) {
|
||||
log.info("Supersampling composition via deviceScaleFactor", {
|
||||
compositionWidth: width,
|
||||
compositionHeight: height,
|
||||
outputResolution: job.config.outputResolution,
|
||||
outputWidth,
|
||||
outputHeight,
|
||||
deviceScaleFactor,
|
||||
});
|
||||
}
|
||||
perfStages.compileOnlyMs = compileResult.compileOnlyMs;
|
||||
|
||||
const probeStart = Date.now();
|
||||
const needsBrowser = composition.duration <= 0 || compiled.unresolvedCompositions.length > 0;
|
||||
|
||||
Reference in New Issue
Block a user