mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-02 12:08:50 +00:00
Merge pull request #2823 from heygen-com/fix/plan-v2-sparse-inactive-videos
fix(producer): materialize sparse video directories
This commit is contained in:
@@ -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 }),
|
||||
|
||||
Reference in New Issue
Block a user