Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions apps/internal-site/components/web-uploader/filename-parser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, expect, it } from "vitest";
import { parseUploadFilenameStem } from "./filename-parser";

describe("parseUploadFilenameStem", () => {
it.each([
["clip - 0001 - before", { prefix: "clip", frame: "0001", variant: "before" }],
["clip-0002-after", { prefix: "clip", frame: "0002", variant: "after" }],
["clip_0003_src", { prefix: "clip", frame: "0003", variant: "src" }],
["clip.0004.output", { prefix: "clip", frame: "0004", variant: "output" }],
])("parses %s", (stem, expected) => {
expect(parseUploadFilenameStem(stem)).toEqual(expected);
});

it("defaults the variant to null when only prefix and frame are present", () => {
expect(parseUploadFilenameStem("clip_0005")).toEqual({
prefix: "clip",
frame: "0005",
variant: null,
});
});
});
34 changes: 34 additions & 0 deletions apps/internal-site/components/web-uploader/filename-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export interface UploadFilenameParts {
prefix: string;
frame: string;
variant: string | null;
}

const FILENAME_SEPARATOR_RE = String.raw`(?:\s+-\s+|[_\-.])`;
const UPLOAD_FILENAME_RE = new RegExp(
String.raw`^(?<prefix>.+?)${FILENAME_SEPARATOR_RE}(?<frame>\d+)(?:${FILENAME_SEPARATOR_RE}(?<variant>[^_\-.]+?))?$`,
);

/**
* Parses the shared flat upload filename shape so scanner grouping and UI labels do not drift.
* The spaced-dash form is intentionally strict (`name - 001`) to avoid treating prose hyphens as separators.
*/
export function parseUploadFilenameStem(stem: string): UploadFilenameParts | null {
const match = UPLOAD_FILENAME_RE.exec(stem.trim());
if (!match?.groups) {
return null;
}

const prefix = match.groups.prefix.trim();
const frame = match.groups.frame.trim();
const variant = match.groups.variant?.trim() ?? null;
if (!prefix || !frame || variant === "") {
return null;
}

return {
prefix,
frame,
variant,
};
}
25 changes: 25 additions & 0 deletions apps/internal-site/components/web-uploader/source-scanner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,31 @@ describe("scanBrowserUploadFiles", () => {
expect(plan.issues).toEqual([]);
});

it("pairs flat files across supported filename separators", () => {
const plan = scanBrowserUploadFiles(
[
image("sample/clip - 001 - before.png"),
image("sample/clip - 001 - after.png"),
image("sample/clip-002-before.png"),
image("sample/clip-002-after.png"),
image("sample/clip_003_before.png"),
image("sample/clip_003_after.png"),
image("sample/clip.004.before.png"),
image("sample/clip.004.after.png"),
],
"sample",
);

expect(plan.frames).toHaveLength(4);
expect(plan.frames.map((frame) => frame.title)).toEqual([
"00_00_1",
"00_00_2",
"00_00_3",
"00_00_4",
]);
expect(plan.issues).toEqual([]);
});

it("keeps same-number flat frames separate when their prefixes differ", () => {
const plan = scanBrowserUploadFiles(
[
Expand Down
13 changes: 6 additions & 7 deletions apps/internal-site/components/web-uploader/source-scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
WebUploadPlan,
} from "./web-upload-types";
import { cjkKebabCase } from "@magic-compare/shared-utils";
import { parseUploadFilenameStem } from "./filename-parser";

const SUPPORTED_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".webp", ".avif", ".svg"]);
const SOURCE_VARIANTS = new Set(["src", "source", "ori", "origin", "before"]);
Expand Down Expand Up @@ -53,7 +54,6 @@ const MATCH_KEY_VARIANTS = [
];
const IGNORED_BASENAMES = new Set([".ds_store", "thumbs.db"]);
const IGNORED_SUFFIXES = new Set([".json", ".yaml", ".yml", ".txt", ".md", ".csv", ".db", ".log"]);
const FILENAME_RE = /(?<prefix>.+?)[_\-.](?<frame>\d+)(?:[_\-.](?<variant>[^_\-.]+))?$/;
const FALLBACK_FILENAME_RE = /^(?<frame>\d+)(?<variant>[A-Za-z][A-Za-z0-9]*)$/;
const STRUCTURED_SOURCE_FILENAME_RE =
/^(?:(?<fps>\d{2})_)?(?<title>.+)_(?<episode>\d+)(?:\.(?<sourceMarker>[^-]+))?-(?<frame>\d+)-(?<variant>[^_\-.]+)$/i;
Expand Down Expand Up @@ -274,12 +274,11 @@ function parseCandidate(entry: BrowserUploadFile, variantOverride?: string): Sou
};
}

const match = FILENAME_RE.exec(pathStem);
if (match?.groups) {
const prefix = match.groups.prefix;
const rawFrame = match.groups.frame;
const frameNumber = Number(rawFrame.replace(/^0+/, "") || "0");
const variant = (variantOverride ?? match.groups.variant ?? "output").trim().toLowerCase();
const parsedFilename = parseUploadFilenameStem(pathStem);
if (parsedFilename) {
const prefix = parsedFilename.prefix;
const frameNumber = Number(parsedFilename.frame.replace(/^0+/, "") || "0");
const variant = (variantOverride ?? parsedFilename.variant ?? "output").trim().toLowerCase();
const fps = extractFps(pathStem);
const episode = extractEpisode(prefix);
const title = `${fps}_${episode}_${frameNumber}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
buildPlanView,
compactUploadFilename,
frameIdForFrame,
fullFrameTitleFromSourcePath,
getUploadPlanHeatmapReferenceOptions,
renameUploadPlanAssetLabel,
reorderUploadPlan,
Expand Down Expand Up @@ -210,6 +211,13 @@ describe("web upload view model", () => {
expect(setUploadPlanHeatmapReference(nextPlan!, "Deband")).toBeNull();
});

it("builds full frame titles from the shared filename parser", () => {
expect(fullFrameTitleFromSourcePath("before/clip - 0001 - source.png")).toBe("clip - 0001");
expect(fullFrameTitleFromSourcePath("before/clip-0002-source.png")).toBe("clip - 0002");
expect(fullFrameTitleFromSourcePath("before/clip_0003_source.png")).toBe("clip - 0003");
expect(fullFrameTitleFromSourcePath("before/clip.0004.source.png")).toBe("clip - 0004");
});

it("compacts long upload filenames by preserving the head and tail", () => {
expect(compactUploadFilename("short.png")).toBe("short.png");
expect(compactUploadFilename("24_WATANARE_ANIME_VOL1_00000.gen.vpy-27240-output.png")).toBe(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { parseUploadFilenameStem } from "./filename-parser";
import type {
WebUploadFramePlan,
WebUploadIssue,
Expand Down Expand Up @@ -47,6 +48,18 @@ function stableHash(value: string) {
return (hash >>> 0).toString(36);
}

function stemFromSourcePath(path: string) {
const fileName = path.split("/").filter(Boolean).at(-1) ?? path;
const dotIndex = fileName.lastIndexOf(".");
return dotIndex === -1 ? fileName : fileName.slice(0, dotIndex);
}

export function fullFrameTitleFromSourcePath(path: string) {
const pathStem = stemFromSourcePath(path);
const parsedFilename = parseUploadFilenameStem(pathStem);
return parsedFilename ? `${parsedFilename.prefix} - ${parsedFilename.frame}` : pathStem;
}

export function frameIdForFrame(frame: WebUploadFramePlan) {
return `frame-${stableHash(
`${frame.title}|${frame.before.source.relativePath}|${frame.after.source.relativePath}`,
Expand Down
Loading