ai-chatbot/lib/ai/canvas-agent-runtime/video-image-functions.ts

134 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string, unknown>;
prompt: string;
callId: string;
},
dependencies?: { upload?: typeof uploadFile; readImage?: (url: string) => Promise<Buffer> },
) {
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("此功能需要在对应工具面板中配置后执行");
}