fix(engine): preserve source frame identity above 99,999 (#3503)

* fix(engine): preserve extracted frame identity

* fix(producer): order legacy distributed frames numerically
This commit is contained in:
Miguel Ángel
2026-08-26 12:30:49 -04:00
committed by GitHub
parent 4f00336c92
commit c9f43ebcfb
7 changed files with 226 additions and 34 deletions
@@ -178,6 +178,34 @@ describe("rebuildExtractedFramesFromPlanDir", () => {
}
});
it("orders mixed-width dense-v1 filenames by numeric ordinal", () => {
const planDir = mkdtempSync(join(tmpdir(), "hf-rebuild-frames-v1-mixed-width-"));
try {
const frameNames = Array.from({ length: 10 }, (_, index) => `frame_${index + 1}.jpg`);
makeFramesDir(planDir, "vid-v1-mixed-width", frameNames.toReversed());
const [extracted] = rebuildExtractedFramesFromPlanDir(planDir, [
{
videoId: "vid-v1-mixed-width",
srcPath: "/v1-mixed-width.mp4",
framePattern: "frame_%05d.jpg",
fps: 30,
totalFrames: frameNames.length,
metadata: VIDEO_METADATA_STUB,
},
]);
expect(extracted!.framePaths.get(8)).toBe(
join(planDir, "video-frames", "vid-v1-mixed-width", "frame_9.jpg"),
);
expect(extracted!.framePaths.get(9)).toBe(
join(planDir, "video-frames", "vid-v1-mixed-width", "frame_10.jpg"),
);
} finally {
rmSync(planDir, { recursive: true, force: true });
}
});
it("preserves original indexes for a sparse v2 chunk materialization", () => {
const planDir = mkdtempSync(join(tmpdir(), "hf-rebuild-frames-sparse-"));
try {
@@ -324,6 +324,13 @@ export async function beginFrameSessionNeedsScreenshotFallback(
return !(await probe(session.page, timeoutMs, probeTick, session.beginFrameIntervalMs));
}
function frameNumberFromFileName(name: string): number | null {
const match = /(\d+)(?=\.[^.]+$)/.exec(name);
if (!match) return null;
const frameNumber = Number(match[1]);
return Number.isSafeInteger(frameNumber) ? frameNumber : null;
}
/**
* Rebuild the engine's in-memory `ExtractedFrames[]` from the on-disk
* planDir layout. `<planDir>/video-frames/<videoId>/` holds the numbered
@@ -355,13 +362,18 @@ export function rebuildExtractedFramesFromPlanDir(
);
}
// framePattern looks like `frame_%05d.jpg`; sprintf isn't available at
// runtime so list-and-sort the directory. Sorted-by-name matches
// sorted-by-frame-index because the extractor writes zero-padded
// monotonic indices.
// runtime so list the directory and order numeric names by their ordinal.
// Width changes once FFmpeg passes the padding minimum, so lexical order
// would interleave frame_100000 before frame_10001.
const ext = (extname(v.framePattern) || ".jpg").toLowerCase();
const frames = readdirSync(outputDir)
.filter((name) => name.toLowerCase().endsWith(ext))
.sort();
.sort((left, right) => {
const leftNumber = frameNumberFromFileName(left);
const rightNumber = frameNumberFromFileName(right);
if (leftNumber === null || rightNumber === null) return left.localeCompare(right);
return leftNumber - rightNumber || left.localeCompare(right);
});
const framePaths = new Map<number, string>();
for (let i = 0; i < frames.length; i++) {
const frameName = frames[i];
@@ -369,8 +381,8 @@ export function rebuildExtractedFramesFromPlanDir(
// V1 plans preserve the historical sorted-position behavior even for
// unusual zero-based filenames. V2 materialization is sparse, so only
// that mode derives the original index from ffmpeg's 1-based filename.
const numbered = indexMode === "sparse-v2" ? /(\d+)(?=\.[^.]+$)/.exec(frameName) : null;
const frameIndex = numbered ? Number(numbered[1]) - 1 : i;
const frameNumber = indexMode === "sparse-v2" ? frameNumberFromFileName(frameName) : null;
const frameIndex = frameNumber === null ? i : frameNumber - 1;
framePaths.set(frameIndex, join(outputDir, frameName));
}
result.push({