Merge pull request #2823 from heygen-com/fix/plan-v2-sparse-inactive-videos

fix(producer): materialize sparse video directories
This commit is contained in:
James Russo
2026-07-26 21:59:48 -07:00
committed by GitHub
2 changed files with 76 additions and 3 deletions
@@ -4,6 +4,7 @@ import {
mkdirSync,
mkdtempSync,
readFileSync,
readdirSync,
rmSync,
statSync,
symlinkSync,
@@ -58,6 +59,8 @@ function createV1Plan(
videoId?: string;
videoSrcPath?: string;
framePattern?: string;
videoStart?: number;
videoEnd?: number;
},
): string {
const {
@@ -70,6 +73,8 @@ function createV1Plan(
videoId = "hero",
videoSrcPath = "/fixture/hero.mp4",
framePattern = "frame_%05d.jpg",
videoStart = 0,
videoEnd = 1,
} = options ?? {};
const planDir = join(root, "v1");
mkdirSync(join(planDir, "compiled"), { recursive: true });
@@ -98,8 +103,8 @@ function createV1Plan(
{
id: videoId,
src: "hero.mp4",
start: 0,
end: 1,
start: videoStart,
end: videoEnd,
mediaStart: 0,
loop: false,
hasAudio: false,
@@ -328,6 +333,41 @@ describe("Plan v2 manifest", () => {
);
});
it("materializes empty video directories for chunks where the video is inactive", () => {
const root = tempPath("hf-plan-v2-inactive-video-directory-");
const v1 = createV1Plan(root, {
video: true,
videoStart: 1 / 30,
videoEnd: 1,
});
const result = createPlanV2FromV1(v1, join(root, "v2"));
const manifest = readPlanV2Manifest(result.planDir);
const chunk0Artifacts = listPlanV2ArtifactsForTarget(manifest, {
role: "chunk",
chunkIndex: 0,
});
const chunk1Artifacts = listPlanV2ArtifactsForTarget(manifest, {
role: "chunk",
chunkIndex: 1,
});
const chunk0Dir = join(root, "chunk-0");
const chunk1Dir = join(root, "chunk-1");
expect(chunk0Artifacts.some((artifact) => artifact.path.startsWith("video-frames/hero/"))).toBe(
false,
);
expect(
chunk1Artifacts.some((artifact) => artifact.path === "video-frames/hero/frame_00001.jpg"),
).toBe(true);
materializePlanV2Target(result.planDir, { role: "chunk", chunkIndex: 0 }, chunk0Dir);
materializePlanV2Target(result.planDir, { role: "chunk", chunkIndex: 1 }, chunk1Dir);
expect(existsSync(join(chunk0Dir, "video-frames", "hero"))).toBe(true);
expect(readdirSync(join(chunk0Dir, "video-frames", "hero"))).toEqual([]);
expect(existsSync(join(chunk1Dir, "video-frames", "hero", "frame_00001.jpg"))).toBe(true);
});
const partialColorSpaceCases = [
{
name: "matrix-only",
@@ -438,6 +478,15 @@ describe("Plan v2 manifest", () => {
});
}
it("rejects an extracted video identifier that escapes the video-frame root", () => {
const root = tempPath("hf-plan-v2-unsafe-video-id-");
const v1 = createV1Plan(root, { video: true, videoId: "../escape" });
expect(() => createPlanV2FromV1(v1, join(root, "v2"))).toThrow(
'unsafe extracted video id: "../escape"',
);
});
it("falls back to the full source frame pack when video metadata is absent", () => {
const root = tempPath("hf-plan-v2-video-fallback-");
const result = createPlanV2FromV1(
@@ -248,6 +248,15 @@ function isExtractionCacheCompleteSentinelPath(path: string): boolean {
);
}
function resolveExtractedVideoOutputDir(planDir: string, videoId: string): string {
const videoRoot = resolve(planDir, "video-frames");
const outputDir = resolve(videoRoot, videoId);
if (outputDir === videoRoot || !outputDir.startsWith(`${videoRoot}${sep}`)) {
throw new PlanV2IntegrityError(`unsafe extracted video id: ${JSON.stringify(videoId)}`);
}
return outputDir;
}
// This is the fail-safe policy table for every v1 artifact class. Keeping the
// branches together makes new artifact classes visibly fall through to both roles.
// fallow-ignore-next-line complexity
@@ -279,7 +288,7 @@ function artifactTargets(
function listVideoFramePaths(planV1Dir: string, videos: PlanVideosJson): ExtractedFrames[] {
return videos.extracted.map((video) => {
const outputDir = join(planV1Dir, "video-frames", video.videoId);
const outputDir = resolveExtractedVideoOutputDir(planV1Dir, video.videoId);
const frameNames = readdirSync(outputDir).sort();
const framePaths = new Map<number, string>();
for (const frameName of frameNames) {
@@ -403,6 +412,15 @@ function parsePlanVideosJson(value: unknown): PlanVideosJson {
return { videos, extracted };
}
function materializeExtractedVideoDirectories(planDir: string): void {
const videosPath = join(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
if (!existsSync(videosPath)) return;
const videos = parsePlanVideosJson(readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH));
for (const video of videos.extracted) {
mkdirSync(resolveExtractedVideoOutputDir(planDir, video.videoId), { recursive: true });
}
}
function parseChunkSlices(value: unknown): ChunkSliceJson[] {
if (!Array.isArray(value)) {
throw new PlanV2IntegrityError("meta/chunks.json must be an array");
@@ -919,6 +937,12 @@ export function materializePlanV2Target(
mkdirSync(dirname(destinationPath), { recursive: true });
copyFileSync(sourcePath, destinationPath);
}
// Plan v2 transports only files, so a chunk where a video is inactive has
// no selected frame artifact from which to create its per-video directory.
// renderChunk consumes a v1-compatible layout and intentionally validates
// every extracted-video directory from meta/videos.json. Recreate those
// zero-byte structural directories without downloading unused frame data.
if (target.role === "chunk") materializeExtractedVideoDirectories(tempDir);
writeFileSync(
join(tempDir, PLAN_V2_MATERIALIZATION_MARKER),
canonicalJsonStringify({ manifest, target }),