CODE · 3.9 KB
tools/generate-i2v.mjs
Workspace snapshot · 09/04 14:52
import { readFile, writeFile, mkdir, appendFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { createFalClient } from "../server/fal-client.js";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const args = new Map();
for (let i = 2; i < process.argv.length; i += 2) args.set(process.argv[i], process.argv[i + 1]);
const specPath = path.resolve(process.cwd(), args.get("--spec") || "");
if (!args.get("--spec")) throw new Error("Usage: node tools/generate-i2v.mjs --spec path/to/spec.json");
const spec = JSON.parse(await readFile(specPath, "utf8"));
const models = {
"veo-3.1-fast": { endpoint: "fal-ai/veo3.1/fast/image-to-video", usdPerSecond: 0.10 },
"kling-v3-pro": { endpoint: "fal-ai/kling-video/v3/pro/image-to-video", usdPerSecond: 0.112 },
"seedance-2": { endpoint: "bytedance/seedance-2.0/image-to-video", usdPerSecond: spec.resolution === "1080p" ? 0.682 : 0.3034 },
};
const selected = models[spec.model];
if (!selected) throw new Error(`Unknown model: ${spec.model}`);
if (!spec.id || !spec.image || !spec.prompt || !Number.isInteger(spec.duration)) throw new Error("spec requires id, image, prompt, integer duration");
const imagePath = path.resolve(root, spec.image);
const imageBytes = await readFile(imagePath);
const ext = path.extname(imagePath).toLowerCase();
const mime = ext === ".png" ? "image/png" : ext === ".webp" ? "image/webp" : "image/jpeg";
const fal = await createFalClient();
const imageUrl = await fal.storage.upload(new File([imageBytes], path.basename(imagePath), { type: mime }));
let input;
if (spec.model === "veo-3.1-fast") {
input = { prompt: spec.prompt, image_url: imageUrl, aspect_ratio: "9:16", duration: `${spec.duration}s`, resolution: spec.resolution || "720p", generate_audio: Boolean(spec.generateAudio), safety_tolerance: "4" };
} else if (spec.model === "kling-v3-pro") {
input = { prompt: spec.prompt, start_image_url: imageUrl, duration: String(spec.duration), generate_audio: Boolean(spec.generateAudio), shot_type: "customize", cfg_scale: 0.5, negative_prompt: spec.negativePrompt || "blur, low quality, morphing, warped faces, extra people, extra fingers, broken hands, unreadable text, fake logos" };
} else {
input = { prompt: spec.prompt, image_url: imageUrl, duration: String(spec.duration), resolution: spec.resolution || "720p", aspect_ratio: "9:16", generate_audio: Boolean(spec.generateAudio), end_user_id: "avs-pengin-public-film" };
}
const startedAt = new Date().toISOString();
const result = await fal.subscribe(selected.endpoint, {
input,
logs: true,
onQueueUpdate(update) {
const latest = update.logs?.at(-1)?.message;
console.log(`${update.status}${latest ? `\t${latest}` : ""}`);
},
});
const videoUrl = result.data?.video?.url || result.video?.url;
if (!videoUrl) throw new Error(`No video URL returned for ${result.requestId}`);
const response = await fetch(videoUrl);
if (!response.ok) throw new Error(`${response.status} ${response.statusText}: ${videoUrl}`);
const output = Buffer.from(await response.arrayBuffer());
const outputDir = path.resolve(root, "output", "benchmark-reset-v1");
await mkdir(outputDir, { recursive: true });
const outputPath = path.join(outputDir, `${spec.id}-${spec.model}.mp4`);
await writeFile(outputPath, output);
const record = {
id: spec.id,
startedAt,
finishedAt: new Date().toISOString(),
model: spec.model,
endpoint: selected.endpoint,
duration: spec.duration,
resolution: spec.resolution || "720p",
generateAudio: Boolean(spec.generateAudio),
estimatedUsd: Number((selected.usdPerSecond * spec.duration).toFixed(4)),
inputFile: path.relative(root, imagePath),
specFile: path.relative(root, specPath),
outputFile: path.relative(root, outputPath),
outputBytes: output.length,
requestId: result.requestId || null,
};
await appendFile(path.join(outputDir, "generation.ndjson"), `${JSON.stringify(record)}\n`);
console.log(JSON.stringify(record, null, 2));