← GPT-AI-VIDEO
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));