← GPT-AI-VIDEO
CODE · 3.8 KB

server/index.js

Workspace snapshot · 09/04 14:52

import http from "node:http";
import path from "node:path";
import { randomUUID } from "node:crypto";
import { fileURLToPath } from "node:url";
import { validateGenerationInput, runVertexGeneration } from "./generation.js";
import { JobStore } from "./job-store.js";
import { createVertexClient } from "./vertex-client.js";
import { createFalClient, isFalConfigured, runFalGeneration } from "./fal-client.js";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const store = new JobStore(path.resolve(__dirname, "../output/jobs"));
const port = Number(process.env.AI_VIDEO_API_PORT || 8787);

function respond(response, status, body, request) {
  const origin = request?.headers?.origin;
  const allowedOrigin = /^http:\/\/(127\.0\.0\.1|localhost)(:\d+)?$/u.test(origin || "") ? origin : "http://127.0.0.1:5173";
  response.writeHead(status, {
    "content-type": "application/json; charset=utf-8",
    "access-control-allow-origin": allowedOrigin,
    "access-control-allow-methods": "GET,POST,OPTIONS",
    "access-control-allow-headers": "content-type",
    "vary": "Origin"
  });
  response.end(JSON.stringify(body));
}

async function readJson(request) {
  const chunks = [];
  for await (const chunk of request) chunks.push(chunk);
  const body = Buffer.concat(chunks).toString("utf8");
  if (body.length > 20000) throw new Error("request body is too large");
  return JSON.parse(body || "{}");
}

async function startJob(input) {
  const validated = validateGenerationInput(input);
  if (validated.estimatedCostYen > 2500) throw new Error("generation exceeds the per-project cost cap");
  const now = new Date().toISOString();
  const job = await store.save({
    id: randomUUID(),
    state: "QUEUED",
    provider: validated.provider.id,
    model: validated.provider.model,
    estimatedCostYen: validated.estimatedCostYen,
    createdAt: now,
    updatedAt: now,
    output: null,
    error: null
  });

  void (async () => {
    try {
      const client = validated.provider.engine === "vertex" ? await createVertexClient() : await createFalClient();
      job.state = "RUNNING";
      job.updatedAt = new Date().toISOString();
      await store.save(job);
      job.output = validated.provider.engine === "vertex"
        ? await runVertexGeneration(input, { client })
        : await runFalGeneration(input, { provider: validated.provider, client });
      job.state = "COMPLETED";
    } catch (error) {
      job.state = "FAILED";
      job.error = error instanceof Error ? error.message : String(error);
    }
    job.updatedAt = new Date().toISOString();
    await store.save(job);
  })();

  return job;
}

const server = http.createServer(async (request, response) => {
  try {
    if (request.method === "OPTIONS") return respond(response, 204, {}, request);
    if (request.method === "GET" && request.url === "/health") {
      return respond(response, 200, {
        ok: true,
        engines: {
          vertex: { configured: Boolean(process.env.GOOGLE_CLOUD_PROJECT), location: process.env.GOOGLE_CLOUD_LOCATION || "global" },
          fal: { configured: await isFalConfigured() }
        }
      }, request);
    }
    if (request.method === "POST" && request.url === "/api/video/jobs") {
      const job = await startJob(await readJson(request));
      return respond(response, 202, job, request);
    }
    const match = request.method === "GET" && request.url?.match(/^\/api\/video\/jobs\/([a-f0-9-]+)$/);
    if (match) return respond(response, 200, await store.get(match[1]), request);
    return respond(response, 404, { error: "not found" }, request);
  } catch (error) {
    return respond(response, 400, { error: error instanceof Error ? error.message : String(error) }, request);
  }
});

server.listen(port, "127.0.0.1", () => {
  console.log(`AI Video API listening on http://127.0.0.1:${port}`);
});