134 lines
5.3 KiB
TypeScript
134 lines
5.3 KiB
TypeScript
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("此功能需要在对应工具面板中配置后执行");
|
||
}
|