import sharp from "sharp"; import { uploadFile } from "@/lib/storage/service"; import { fetchImageBufferFromUrl } from "@/lib/ai/rmbg/service"; import { resolveCanvasRuntimeEndpoint } from "@/lib/canvas/capabilities/internal-endpoint"; import { requestLongTaskResult } from "@/lib/client/long-task"; import type { VideoCanvasFunctionDefinition } from "@/lib/video-canvas/config"; export async function processVideoCanvasImage( params: { userId: string; request: Request; definition: VideoCanvasFunctionDefinition; imageUrl: string; args: Record; prompt: string; callId: string; }, dependencies?: { upload?: typeof uploadFile; readImage?: (url: string) => Promise }, ) { const { definition, imageUrl, args } = params; if (definition.key === "grid-split" || definition.key === "image-crop") { const bytes = await (dependencies?.readImage || fetchImageBufferFromUrl)(imageUrl), metadata = await sharp(bytes).metadata(); if (!metadata.width || !metadata.height) throw new Error("源图片无法读取"); const rows = definition.key === "grid-split" ? Number(args.rows) : 1, columns = definition.key === "grid-split" ? Number(args.columns || rows) : 1; if ( ![rows, columns].every((value) => Number.isInteger(value) && value >= 1 && value <= 5) || rows * columns > 25 ) throw new Error("宫格行列必须为 1–5,最多 25 张"); const outputs: Array<{ url: string }> = []; for (let row = 0; row < rows; row++) for (let column = 0; column < columns; column++) { const left = definition.key === "grid-split" ? Math.floor((metadata.width * column) / columns) : Math.round(Number(args.x)); const top = definition.key === "grid-split" ? Math.floor((metadata.height * row) / rows) : Math.round(Number(args.y)); const width = definition.key === "grid-split" ? Math.floor((metadata.width * (column + 1)) / columns) - left : Math.round(Number(args.width)); const height = definition.key === "grid-split" ? Math.floor((metadata.height * (row + 1)) / rows) - top : Math.round(Number(args.height)); if ( ![left, top, width, height].every(Number.isFinite) || left < 0 || top < 0 || width < 1 || height < 1 || left + width > metadata.width || top + height > metadata.height ) throw new Error("裁剪坐标超出真实图片边界"); const output = await sharp(bytes).extract({ left, top, width, height }).png().toBuffer(); const result = await (dependencies?.upload || uploadFile)( `video-agent-${crypto.randomUUID()}.png`, new Uint8Array(output).buffer, ); outputs.push({ url: result.url }); } return outputs; } if (definition.route === "remove-bg") { const { executeRmbgJob } = await import("@/lib/ai/rmbg/service"); const result = await executeRmbgJob({ userId: params.userId, imageBuffer: await fetchImageBufferFromUrl(imageUrl), sourceImageUrl: imageUrl, prompt: params.prompt, }); return [{ url: result.original_url }]; } if (definition.route === "extend") { const { executeExtendJob } = await import("@/lib/ai/extend/service"); const result = await executeExtendJob({ userId: params.userId, imageUrl, sourceImageUrl: imageUrl, prompt: params.prompt, xScale: Number(args.xScale) || 1.5, yScale: Number(args.yScale) || 1.5, requestId: params.callId, }); return [{ url: result.original_url }]; } if (definition.route === "layer-split") { const { executeLayerSplitJob } = await import("@/lib/ai/layer-split/service"); const result = await executeLayerSplitJob({ userId: params.userId, imageUrl, prompt: params.prompt, }); return result.layers.map((layer) => ({ url: layer.originalUrl })); } const cookie = params.request.headers.get("cookie"); if (definition.route === "enhance") { const result = await requestLongTaskResult({ endpoint: resolveCanvasRuntimeEndpoint("/api/enhance", params.request.url), payload: { imageUrl, scaleFactor: Number(args.scaleFactor) || 2 }, headers: cookie ? { cookie } : undefined, timeoutMs: 10 * 60_000, }); if (!result?.original_url) throw new Error("增强服务没有返回真实图片"); return [{ url: String(result.original_url) }]; } if (definition.route === "angle-edit") { const response = await fetch( resolveCanvasRuntimeEndpoint("/api/angle-edit", params.request.url), { method: "POST", headers: { "Content-Type": "application/json", ...(cookie ? { cookie } : {}) }, body: JSON.stringify({ image_url: imageUrl, horizontal_angle: Number(args.horizontalAngle) || 0, vertical_angle: Number(args.verticalAngle) || 0, zoom: Number(args.zoom) || 0, }), }, ); const result = await response.json(); const url = result.image_url || result.original_url; if (!response.ok || !url) throw new Error("多角度编辑服务没有返回真实图片"); return [{ url: String(url) }]; } throw new Error("此功能需要在对应工具面板中配置后执行"); }