Repository navigation
milestone 6: add export-video ffmpeg baseline with snapshots #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
379d74b
58aedd4
b071c1e
43858c8
ac11e94
13a7916
05f28de
e744101
be13180
655c860
f98895b
e24048e
e74a645
f62b08c
7df489e
3d6a80f
510c50f
c6de419
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,140 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CanvasProfile, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ExportVideoInput, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Mode, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Surface, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Workflow, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from "../types/contracts"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { exportVideo } from "../domain/export_video"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { stableStringify } from "../domain/recommend"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type ParsedArgs = ExportVideoInput & { json: boolean }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function parseArgs(argv: string[]): ParsedArgs { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (argv.length === 0 || argv[0]?.startsWith("--")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error("Missing required positional arg: <file>"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const file = argv[0]!; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let out: string | undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mode: Mode | undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let surface: Surface | undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let workflow: Workflow = "unknown"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let whiteCanvas = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let canvasProfile: CanvasProfile | undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let crf: number | undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let json = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (let i = 1; i < argv.length; i += 1) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const token = argv[i]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const next = argv[i + 1]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| switch (token) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "--out": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!next || next.startsWith("--")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error("Missing value for --out"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| out = next; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| i += 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "--mode": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!next || next.startsWith("--")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error("Missing value for --mode"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mode = next as Mode; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| i += 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "--surface": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!next || next.startsWith("--")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error("Missing value for --surface"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| surface = next as Surface; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| i += 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "--workflow": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!next || next.startsWith("--")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error("Missing value for --workflow"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workflow = next as Workflow; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| i += 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "--white-canvas": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| whiteCanvas = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "--canvas-profile": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!next || next.startsWith("--")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error("Missing value for --canvas-profile"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| canvasProfile = next as CanvasProfile; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| i += 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "--crf": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!next || next.startsWith("--")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error("Missing value for --crf"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| crf = Number.parseInt(next ?? "", 10); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| i += 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "--json": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| json = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Unknown argument: ${token}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!out || !mode || !surface) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error("Missing required args: --out --mode --surface"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!["reliable", "experimental"].includes(mode)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Invalid mode: ${mode}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!["feed", "story", "reel"].includes(surface)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Invalid surface: ${surface}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!["app_direct", "api_scheduler", "unknown"].includes(workflow)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Invalid workflow: ${workflow}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (canvasProfile && !["feed_compat", "feed_app_direct"].includes(canvasProfile)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Invalid canvas profile: ${canvasProfile}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+90
to
+104
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Use imported constants for validation instead of hardcoded arrays. The validation checks duplicate the allowed values that are already defined as constants in ♻️ Proposed refactor to use shared constants+import {
+ MODES,
+ SURFACES,
+ WORKFLOWS,
+ CANVAS_PROFILES,
+ type CanvasProfile,
+ type ExportVideoInput,
+ type Mode,
+ type Surface,
+ type Workflow,
+} from "../types/contracts";
- if (!["reliable", "experimental"].includes(mode)) {
+ if (!MODES.includes(mode)) {
throw new Error(`Invalid mode: ${mode}`);
}
- if (!["feed", "story", "reel"].includes(surface)) {
+ if (!SURFACES.includes(surface)) {
throw new Error(`Invalid surface: ${surface}`);
}
- if (!["app_direct", "api_scheduler", "unknown"].includes(workflow)) {
+ if (!WORKFLOWS.includes(workflow)) {
throw new Error(`Invalid workflow: ${workflow}`);
}
- if (canvasProfile && !["feed_compat", "feed_app_direct"].includes(canvasProfile)) {
+ if (canvasProfile && !CANVAS_PROFILES.includes(canvasProfile)) {
throw new Error(`Invalid canvas profile: ${canvasProfile}`);
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (crf !== undefined && (!Number.isFinite(crf) || crf < 0 || crf > 51)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Invalid crf: ${crf}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { file, out, mode, surface, workflow, whiteCanvas, canvasProfile, crf, json }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function printHumanOutput(result: ReturnType<typeof exportVideo>): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`Summary: exported ${result.input_path} -> ${result.output_path}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`Target: ${result.target_resolution}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`Profile: ${result.selected_profile}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`Video: codec=${result.video_codec} fps=${result.fps}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`White canvas: ${result.white_canvas_enabled ? "enabled" : "disabled"}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log("Next action: run with --json for machine-readable output."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function main(): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parsed = parseArgs(process.argv.slice(2)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = exportVideo(parsed); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (parsed.json) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(stableStringify(result)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| printHumanOutput(result); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const message = error instanceof Error ? error.message : String(error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(`Error: ${message}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| main(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| import { mkdirSync } from "node:fs"; | ||
| import { dirname, resolve } from "node:path"; | ||
| import type { ExportVideoInput, ExportVideoOutput } from "../types/contracts"; | ||
| import { inspectMedia } from "./media_inspector"; | ||
| import { recommend } from "./recommend"; | ||
| import { parseResolution } from "./rules"; | ||
|
|
||
| function buildFilter(params: { | ||
| targetWidth: number; | ||
| targetHeight: number; | ||
| whiteCanvasEnabled: boolean; | ||
| margins: { left: number; top: number; right: number; bottom: number } | null; | ||
| }): string { | ||
| const { targetWidth, targetHeight, whiteCanvasEnabled, margins } = params; | ||
|
|
||
| if (whiteCanvasEnabled && margins) { | ||
| const innerWidth = targetWidth - (margins.left + margins.right); | ||
| const innerHeight = targetHeight - (margins.top + margins.bottom); | ||
|
|
||
| if (innerWidth <= 0 || innerHeight <= 0) { | ||
| throw new Error("Invalid white-canvas margins: non-positive inner frame"); | ||
| } | ||
|
|
||
| return [ | ||
| `scale=${innerWidth}:${innerHeight}:force_original_aspect_ratio=decrease`, | ||
| `pad=${innerWidth}:${innerHeight}:(ow-iw)/2:(oh-ih)/2:white`, | ||
| `pad=${targetWidth}:${targetHeight}:${margins.left}:${margins.top}:white`, | ||
| ].join(","); | ||
| } | ||
|
|
||
| return [ | ||
| `scale=${targetWidth}:${targetHeight}:force_original_aspect_ratio=increase`, | ||
| `crop=${targetWidth}:${targetHeight}`, | ||
| ].join(","); | ||
| } | ||
|
|
||
| export function exportVideo(input: ExportVideoInput): ExportVideoOutput { | ||
| const workflow = input.workflow ?? "unknown"; | ||
| const crf = input.crf ?? 23; | ||
|
|
||
| const media = inspectMedia(input.file); | ||
| const recommendation = recommend({ | ||
| mode: input.mode, | ||
| surface: input.surface, | ||
| orientation: media.orientation, | ||
| workflow, | ||
| whiteCanvas: input.whiteCanvas, | ||
| canvasProfile: input.canvasProfile, | ||
| sourceRatio: media.width / media.height, | ||
| }); | ||
|
|
||
| const { width, height } = parseResolution(recommendation.target_resolution); | ||
| const filter = buildFilter({ | ||
| targetWidth: width, | ||
| targetHeight: height, | ||
| whiteCanvasEnabled: recommendation.white_canvas.enabled, | ||
| margins: recommendation.white_canvas.margins, | ||
| }); | ||
|
|
||
| const resolvedInputPath = resolve(input.file); | ||
| const resolvedOutputPath = resolve(input.out); | ||
| mkdirSync(dirname(resolvedOutputPath), { recursive: true }); | ||
|
|
||
| const fps = media.fps > 0 ? media.fps : 30; | ||
| const proc = Bun.spawnSync({ | ||
| cmd: [ | ||
| "ffmpeg", | ||
| "-y", | ||
| "-hide_banner", | ||
| "-loglevel", | ||
| "error", | ||
| "-i", | ||
| resolvedInputPath, | ||
| "-map", | ||
| "0:v:0", | ||
| "-map", | ||
| "0:a?", | ||
| "-vf", | ||
| filter, | ||
| "-r", | ||
| String(fps), | ||
| "-c:v", | ||
| "libx264", | ||
| "-c:a", | ||
| "aac", | ||
| "-crf", | ||
| String(crf), | ||
| "-pix_fmt", | ||
| "yuv420p", | ||
| "-movflags", | ||
| "+faststart", | ||
| resolvedOutputPath, | ||
| ], | ||
| stdout: "pipe", | ||
| stderr: "pipe", | ||
| timeout: 60_000, | ||
| }); | ||
|
Comment on lines
+96
to
+97
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider making the timeout configurable for large video files. The 60-second timeout may be insufficient for longer or higher-resolution video exports. Large files could exceed this limit during encoding. 💡 Optional: Add timeout as an input parameter export type ExportVideoInput = {
file: string;
out: string;
mode: Mode;
surface: Surface;
workflow?: Workflow;
whiteCanvas?: boolean;
canvasProfile?: CanvasProfile;
crf?: number;
+ timeout?: number;
};Then in - timeout: 60_000,
+ timeout: input.timeout ?? 60_000,🤖 Prompt for AI Agents |
||
|
|
||
| if (proc.exitCode !== 0) { | ||
| throw new Error( | ||
| `ffmpeg video export failed (input: ${resolvedInputPath}, filter: ${filter}): ${proc.stderr.toString().trim()}`, | ||
| ); | ||
| } | ||
|
|
||
| return { | ||
| input_path: input.file, | ||
| output_path: input.out, | ||
| selected_profile: recommendation.selected_profile, | ||
| target_resolution: recommendation.target_resolution, | ||
| white_canvas_enabled: recommendation.white_canvas.enabled, | ||
| ffmpeg_filter: filter, | ||
| video_codec: "h264", | ||
| fps, | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,34 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { describe, expect, test } from "bun:test"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { readFileSync } from "node:fs"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { join } from "node:path"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { parseJsonStdout, runExportVideoCli } from "../helpers/cli"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import type { ExportCase } from "../helpers/types"; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const fixtureDir = join(import.meta.dir, "..", "fixtures", "e2e"); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const snapshotDir = join(fixtureDir, "snapshots", "export_video"); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const cases = JSON.parse(readFileSync(join(fixtureDir, "export_video_cases.json"), "utf8")) as ExportCase[]; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| function normalizeExportVideoPayload(payload: Record<string, unknown>): Record<string, unknown> { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const normalized = structuredClone(payload) as { input_path?: string; output_path?: string }; | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof normalized.input_path === "string" && normalized.input_path.startsWith("/")) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| normalized.input_path = normalized.input_path.replace(/^.*tests\/fixtures\//, "tests/fixtures/"); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof normalized.output_path === "string" && normalized.output_path.startsWith("/")) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| normalized.output_path = normalized.output_path.replace(/^.*tests\/fixtures\//, "tests/fixtures/"); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| return normalized as Record<string, unknown>; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+11
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle Windows absolute paths in snapshot normalization. Right now normalization only triggers for paths starting with “/”, so Windows paths like 🛠️ Suggested fix function normalizeExportVideoPayload(payload: Record<string, unknown>): Record<string, unknown> {
const normalized = structuredClone(payload) as { input_path?: string; output_path?: string };
- if (typeof normalized.input_path === "string" && normalized.input_path.startsWith("/")) {
- normalized.input_path = normalized.input_path.replace(/^.*tests\/fixtures\//, "tests/fixtures/");
- }
- if (typeof normalized.output_path === "string" && normalized.output_path.startsWith("/")) {
- normalized.output_path = normalized.output_path.replace(/^.*tests\/fixtures\//, "tests/fixtures/");
- }
+ if (typeof normalized.input_path === "string") {
+ normalized.input_path = normalized.input_path
+ .replace(/^.*tests[\\/]+fixtures[\\/]+/, "tests/fixtures/")
+ .replace(/\\/g, "/");
+ }
+ if (typeof normalized.output_path === "string") {
+ normalized.output_path = normalized.output_path
+ .replace(/^.*tests[\\/]+fixtures[\\/]+/, "tests/fixtures/")
+ .replace(/\\/g, "/");
+ }
return normalized as Record<string, unknown>;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| describe("export-video e2e snapshots", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| for (const scenario of cases) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| test(scenario.id, async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await runExportVideoCli(scenario.args); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(result.exitCode).toBe(0); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const expected = normalizeExportVideoPayload( | ||||||||||||||||||||||||||||||||||||||||||||||||
| JSON.parse(readFileSync(join(snapshotDir, `${scenario.id}.json`), "utf8")) as Record<string, unknown>, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const actual = normalizeExportVideoPayload(parseJsonStdout(result.stdout, scenario.id)); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(actual).toEqual(expected); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| [ | ||
| { | ||
| "id": "export-video-reliable-reel-portrait", | ||
| "args": ["tests/fixtures/images/portrait_video_360x640.mp4", "--out", "tests/fixtures/exports/e2e_export_video_reliable_reel_portrait.mp4", "--mode", "reliable", "--surface", "reel", "--workflow", "unknown", "--json"] | ||
| }, | ||
| { | ||
| "id": "export-video-reliable-feed-landscape-white", | ||
| "args": ["tests/fixtures/images/landscape_video_640x360.mov", "--out", "tests/fixtures/exports/e2e_export_video_reliable_feed_landscape_white.mp4", "--mode", "reliable", "--surface", "feed", "--workflow", "unknown", "--white-canvas", "--canvas-profile", "feed_compat", "--json"] | ||
| }, | ||
| { | ||
| "id": "export-video-experimental-story", | ||
| "args": ["tests/fixtures/images/portrait_video_360x640.mp4", "--out", "tests/fixtures/exports/e2e_export_video_experimental_story.mp4", "--mode", "experimental", "--surface", "story", "--workflow", "unknown", "--json"] | ||
| } | ||
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Minor style: Consider varying sentence structure.
Three consecutive bullet points begin with "Added". Consider rephrasing for variety, e.g., "Introduced export-video integration/e2e snapshot tests..." or restructuring the list.
🧰 Tools
🪛 LanguageTool
[style] ~36-~36: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ... outputs in
tests/fixtures/exports. - Added export-video integration/e2e snapshot t...(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
🤖 Prompt for AI Agents