feat: add durable CMS-free image video subsystem and UI components
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { createReadStream } from "node:fs";
|
||||
import { HarnessError, ImageVideoDraftSchema, taskIdSchema } from "@agent-studio/harness";
|
||||
import type { FastifyInstance, FastifyRequest } from "fastify";
|
||||
import { z } from "zod";
|
||||
import type { AppDependencies } from "../app.js";
|
||||
import { requireSession } from "./sessions.js";
|
||||
|
||||
export function registerImageVideoRoutes(app: FastifyInstance, dependencies: AppDependencies) {
|
||||
const context = (request: FastifyRequest) => {
|
||||
const host = request.headers.host ?? "";
|
||||
const ports = dependencies.imageVideoPorts ?? [80, 3001, 5173, 4173];
|
||||
let url: URL;
|
||||
try { url = new URL(`http://${host}`); } catch { throw new HarnessError("I2V_ORIGIN_DENIED", "Local origin required"); }
|
||||
if (!["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) || !ports.includes(Number(url.port || 80)) || request.headers["sec-fetch-site"] === "cross-site" || (request.headers.origin !== undefined && request.headers.origin !== url.origin)) throw new HarnessError("I2V_ORIGIN_DENIED", "Image video tasks are available only from the local page");
|
||||
if (!dependencies.imageVideo) throw new HarnessError("I2V_TASK_NOT_FOUND", "Image video service unavailable");
|
||||
return dependencies.imageVideo;
|
||||
};
|
||||
app.get("/api/image-video/tasks", async (request, reply) => {
|
||||
const service = context(request); void reply.header("Cache-Control", "no-store");
|
||||
const history = await service.list();
|
||||
return { tasks: await Promise.all(history.map(task => service.refresh(task.id))) };
|
||||
});
|
||||
app.get("/api/image-video/tasks/:taskId", async (request, reply) => {
|
||||
const service = context(request); const { taskId } = z.object({ taskId: taskIdSchema }).parse(request.params);
|
||||
void reply.header("Cache-Control", "no-store"); return { task: await service.refresh(taskId) };
|
||||
});
|
||||
app.post("/api/sessions/:sessionId/image-video/tasks", async (request, reply) => {
|
||||
const service = context(request); const { sessionId } = z.object({ sessionId: z.string().regex(/^ses_[\w-]+$/) }).parse(request.params);
|
||||
requireSession(dependencies, sessionId);
|
||||
if (!request.headers["content-type"]?.startsWith("application/json")) throw new HarnessError("I2V_JSON_REQUIRED", "Draft creation requires JSON");
|
||||
void reply.header("Cache-Control", "no-store"); return { task: await service.create(sessionId, ImageVideoDraftSchema.parse(request.body)) };
|
||||
});
|
||||
for (const kind of ["reference", "video"] as const) app.get(`/api/image-video/tasks/:taskId/${kind}`, async (request, reply) => {
|
||||
const service = context(request); const { taskId } = z.object({ taskId: taskIdSchema }).parse(request.params);
|
||||
const query = z.object({ download: z.literal("1").optional() }).strict().parse(request.query);
|
||||
const media = await service.media(taskId, kind);
|
||||
void reply.header("Cache-Control", "no-store").header("X-Content-Type-Options", "nosniff").header("Cross-Origin-Resource-Policy", "same-origin").header("Accept-Ranges", "bytes").type(media.contentType);
|
||||
if (query.download === "1") void reply.header("Content-Disposition", `attachment; filename="${taskId}${kind === "video" ? ".mp4" : ".image"}"`);
|
||||
const range = request.headers.range;
|
||||
if (range !== undefined) {
|
||||
const match = /^bytes=(\d*)-(\d*)$/.exec(range); let start = 0; let end = media.size - 1;
|
||||
if (match?.[1]) { start = Number(match[1]); if (match[2]) end = Math.min(Number(match[2]), end); }
|
||||
else if (match?.[2]) start = Math.max(0, media.size - Number(match[2]));
|
||||
if (!match || (!match[1] && !match[2]) || !Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start > end || start >= media.size || end < 0) return reply.code(416).header("Content-Range", `bytes */${String(media.size)}`).send();
|
||||
return reply.code(206).header("Content-Range", `bytes ${String(start)}-${String(end)}/${String(media.size)}`).header("Content-Length", end - start + 1).send(createReadStream(media.path, { start, end }));
|
||||
}
|
||||
return reply.header("Content-Length", media.size).send(createReadStream(media.path));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user