← GPT-AI-VIDEO
CODE · 2.5 KB

server/fal-client.js

Workspace snapshot · 09/04 14:52

import { execFile } from "node:child_process";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);
const credentialTarget = "AgentVentureStudio:Fal:AiVideoLab";
const credentialReader = "C:\\service\\agent-venture-studio\\scripts\\read-avs-credential.ps1";

async function readStoredFalKey() {
  try {
    const { stdout } = await execFileAsync(
      "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
      ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", credentialReader, "-Target", credentialTarget],
      { timeout: 30000, maxBuffer: 4096, windowsHide: true, encoding: "utf8" }
    );
    return stdout.trim();
  } catch {
    return "";
  }
}

export async function isFalConfigured(env = process.env) {
  return Boolean(env.FAL_KEY || await readStoredFalKey());
}

export async function createFalClient(env = process.env) {
  const credentials = env.FAL_KEY || await readStoredFalKey();
  if (!credentials) throw new Error("FAL_KEY is required for this provider");
  const { fal } = await import("@fal-ai/client");
  fal.config({ credentials });
  return fal;
}

export function buildFalInput(provider, input) {
  const common = { prompt: input.prompt, aspect_ratio: input.aspectRatio, duration: input.durationSeconds };
  switch (provider.requestShape) {
    case "omni": return common;
    case "flux": return { ...common, resolution: provider.resolution, generate_audio: false, safety_tolerance: 1 };
    case "seedance": return { ...common, duration: String(input.durationSeconds), resolution: provider.resolution, generate_audio: false, bitrate_mode: "standard" };
    case "minimax": return { ...common, resolution: provider.resolution };
    case "kling": return { ...common, generate_audio: false };
    default: throw new Error(`unsupported fal request shape: ${provider.requestShape}`);
  }
}

export async function runFalGeneration(input, { provider, client, onProgress = () => {} }) {
  onProgress({ state: "RUNNING", engine: "fal", model: provider.model });
  const result = await client.subscribe(provider.model, {
    input: buildFalInput(provider, input),
    logs: true,
    onQueueUpdate(update) {
      onProgress({ state: update.status || "RUNNING", engine: "fal", model: provider.model });
    }
  });
  const data = result.data || result;
  const video = data.video || data.videos?.[0];
  if (!video?.url) throw new Error("fal completed without a video URL");
  return { uri: video.url, videoBytes: null, model: provider.model, engine: "fal", requestId: result.requestId || null };
}