diff --git a/packages/producer/src/services/distributed/plan.ts b/packages/producer/src/services/distributed/plan.ts index 414e1b031..249bac143 100644 --- a/packages/producer/src/services/distributed/plan.ts +++ b/packages/producer/src/services/distributed/plan.ts @@ -24,10 +24,24 @@ * never have to handle them. */ -import { cpSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "node:fs"; +import { + cpSync, + existsSync, + mkdirSync, + readdirSync, + renameSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; import { join } from "node:path"; import { type CanvasResolution } from "@hyperframes/core"; -import { type EngineConfig, resolveConfig } from "@hyperframes/engine"; +import { + type EngineConfig, + type ExtractedFrames, + resolveConfig, + type VideoElement, +} from "@hyperframes/engine"; import { defaultLogger, type ProducerLogger } from "../../logger.js"; import { runAudioStage } from "../render/stages/audioStage.js"; import { runCompileStage } from "../render/stages/compileStage.js"; @@ -419,6 +433,42 @@ function buildLockedRenderConfig(input: { }; } +/** + * Persisted shape of the video-extraction outputs from `runExtractVideosStage`, + * written to `/meta/videos.json` for `renderChunk` to reconstruct + * a `FrameLookupTable` from. The in-process renderer keeps these structures + * in memory; the distributed pipeline writes them out so a separate chunk + * worker process can rebuild the video-frame injector without re-running + * the extract stage. + * + * `ExtractedFrames` from the engine carries an absolute `outputDir`, + * an open file descriptor in some paths, and a `framePaths` map — none of + * those survive a serialize → re-deserialize round trip across processes. + * The serialized form keeps only what plan-time produced and lets + * `renderChunk` re-derive `outputDir` (always `/video-frames/`) + * and `framePaths` (re-listed from that directory). + */ +interface PlanVideosJson { + /** + * Composition's `