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}`);
});