feat: route canvas agent angle variants
This commit is contained in:
parent
4588850572
commit
96827c760c
@ -81,6 +81,7 @@ export type AgentPreferredCapability =
|
||||
| "enhance"
|
||||
| "extend"
|
||||
| "resize"
|
||||
| "angle_variant"
|
||||
| "box_cutout"
|
||||
| "generate_image"
|
||||
| "generate_video"
|
||||
@ -251,6 +252,7 @@ function inferPreferredCapability(
|
||||
if (firstTool === "enhanceImage") return "enhance";
|
||||
if (firstTool === "extendImage") return "extend";
|
||||
if (firstTool === "resizeImage") return "resize";
|
||||
if (firstTool === "angleEditImage") return "angle_variant";
|
||||
if (firstTool === "boxCutout") return "box_cutout";
|
||||
if (firstTool === "publishToCanvas" || firstTool === "publishResultToCanvas") return "publish_canvas";
|
||||
if (firstTool === "generateVideo") return "generate_video";
|
||||
@ -273,6 +275,9 @@ function inferPreferredCapability(
|
||||
return "extend";
|
||||
case "resize":
|
||||
return "resize";
|
||||
case "angle_variant":
|
||||
case "generate_angle_variant":
|
||||
return "angle_variant";
|
||||
default:
|
||||
return "generic_edit";
|
||||
}
|
||||
@ -1403,6 +1408,7 @@ function sanitizeAgentPlan(
|
||||
"enhance",
|
||||
"extend",
|
||||
"resize",
|
||||
"angle_variant",
|
||||
"box_cutout",
|
||||
"generate_image",
|
||||
"generate_video",
|
||||
@ -1584,7 +1590,7 @@ async function buildExecutionPlanWithAgent(
|
||||
5. 优先保持一次只选择一个主工具;toolCalls 只从提供的工具列表里选。
|
||||
6. shouldUseStructuredReply 只在 edit_image / generate_image / generate_video 时设为 true。
|
||||
7. targetSource 要忠实反映当前目标图来源:explicit_selection/current_upload/text_reference/working_memory/latest_result/clarification/none。
|
||||
8. preferredCapability 要尽量具体,例如 remove_background / remove_watermark / enhance / extend / resize / box_cutout / publish_canvas。
|
||||
8. preferredCapability 要尽量具体,例如 remove_background / remove_watermark / enhance / extend / resize / angle_variant / box_cutout / publish_canvas。
|
||||
9. 必须优先使用新的强语义工具名:resolveTargetImage / analyzeTargetImage / editTargetImage / generateNewImage / publishResultToCanvas。
|
||||
10. 必须遵守当前专家路由约束;如果约束要求纯对话,则绝不能输出 generate_image / edit_image / generate_video。
|
||||
|
||||
@ -1595,7 +1601,7 @@ async function buildExecutionPlanWithAgent(
|
||||
"intent": "chat|vision|image_edit|image_gen|video_gen",
|
||||
"subIntent": "可选,仅 image_edit 时填写",
|
||||
"targetSource": "explicit_selection|current_upload|text_reference|working_memory|latest_result|clarification|none",
|
||||
"preferredCapability": "respond|analyze|generic_edit|remove_background|remove_watermark|enhance|extend|resize|box_cutout|generate_image|generate_video|publish_canvas",
|
||||
"preferredCapability": "respond|analyze|generic_edit|remove_background|remove_watermark|enhance|extend|resize|angle_variant|box_cutout|generate_image|generate_video|publish_canvas",
|
||||
"responseMode": "direct_answer|tool_then_answer|tool_then_execute|clarify",
|
||||
"shouldUseHistoryImage": true,
|
||||
"shouldUseStructuredReply": false,
|
||||
|
||||
@ -70,6 +70,7 @@ export function buildImageEditSpecialistRoute(params: {
|
||||
"enhanceImage",
|
||||
"extendImage",
|
||||
"resizeImage",
|
||||
"angleEditImage",
|
||||
"boxCutout",
|
||||
"useSelectedCanvasImage",
|
||||
"useLatestResultImage",
|
||||
|
||||
82
lib/ai/runtime/angle-edit-options.ts
Normal file
82
lib/ai/runtime/angle-edit-options.ts
Normal file
@ -0,0 +1,82 @@
|
||||
export interface DirectAgentAngleEditOptions {
|
||||
horizontalAngle: number;
|
||||
verticalAngle: number;
|
||||
zoom: number;
|
||||
loraScale?: number;
|
||||
}
|
||||
|
||||
export function clampAngleEditNumber(
|
||||
value: number | undefined,
|
||||
min: number,
|
||||
max: number,
|
||||
fallback: number,
|
||||
) {
|
||||
if (!Number.isFinite(value)) return fallback;
|
||||
return Math.min(max, Math.max(min, Number(value)));
|
||||
}
|
||||
|
||||
function parseFirstNumberMatch(text: string, patterns: RegExp[]) {
|
||||
for (const pattern of patterns) {
|
||||
const match = text.match(pattern);
|
||||
const value = Number(match?.[1]);
|
||||
if (Number.isFinite(value)) return value;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function inferAngleEditOptionsFromText(text: string): DirectAgentAngleEditOptions {
|
||||
const normalizedText = String(text || "").toLowerCase().replace(/\s+/g, "");
|
||||
const explicitHorizontal = parseFirstNumberMatch(normalizedText, [
|
||||
/(?:水平角|水平|环绕|左右|horizontal|h)[::]?(-?\d{1,3})(?:度|°)?/,
|
||||
/(?:旋转|转|转到|换成|改成)(-?\d{1,3})(?:度|°)/,
|
||||
]);
|
||||
const explicitVertical = parseFirstNumberMatch(normalizedText, [
|
||||
/(?:垂直角|垂直|俯仰|上下|vertical|v)[::]?(-?\d{1,3})(?:度|°)?/,
|
||||
]);
|
||||
const explicitZoom = parseFirstNumberMatch(normalizedText, [
|
||||
/(?:zoom|缩放|景别)[::]?(\d{1,2})(?:倍|x)?/,
|
||||
]);
|
||||
|
||||
let horizontalAngle = explicitHorizontal ?? 45;
|
||||
if (explicitHorizontal === undefined) {
|
||||
if (/背面|背后|背视|后视|back/.test(normalizedText)) {
|
||||
horizontalAngle = 180;
|
||||
} else if (/左侧|左边|左视|left/.test(normalizedText)) {
|
||||
horizontalAngle = 270;
|
||||
} else if (/右侧|右边|右视|侧面|侧视|profile|side/.test(normalizedText)) {
|
||||
horizontalAngle = 90;
|
||||
} else if (/正面|正视|front/.test(normalizedText)) {
|
||||
horizontalAngle = 0;
|
||||
} else if (/三分之四|四分之三|斜侧|45度|45°/.test(normalizedText)) {
|
||||
horizontalAngle = 45;
|
||||
}
|
||||
}
|
||||
|
||||
let verticalAngle = explicitVertical ?? 0;
|
||||
if (explicitVertical === undefined) {
|
||||
if (/俯视|俯拍|鸟瞰|高机位|top|overhead/.test(normalizedText)) {
|
||||
verticalAngle = 45;
|
||||
} else if (/仰视|仰拍|低机位|lowangle/.test(normalizedText)) {
|
||||
verticalAngle = -20;
|
||||
} else if (/平视|eyelevel/.test(normalizedText)) {
|
||||
verticalAngle = 0;
|
||||
}
|
||||
}
|
||||
|
||||
let zoom = explicitZoom ?? 5;
|
||||
if (explicitZoom === undefined) {
|
||||
if (/特写|近景|拉近|推近|zoomin/.test(normalizedText)) {
|
||||
zoom = 7;
|
||||
} else if (/远景|全身|拉远|zoomout/.test(normalizedText)) {
|
||||
zoom = 3;
|
||||
} else if (/中景/.test(normalizedText)) {
|
||||
zoom = 5;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
horizontalAngle: clampAngleEditNumber(horizontalAngle, 0, 360, 45),
|
||||
verticalAngle: clampAngleEditNumber(verticalAngle, -30, 90, 0),
|
||||
zoom: clampAngleEditNumber(zoom, 0, 10, 5),
|
||||
};
|
||||
}
|
||||
@ -16,6 +16,7 @@ export type DirectAgentToolCapability =
|
||||
| "enhance"
|
||||
| "resize"
|
||||
| "extend"
|
||||
| "angle_variant"
|
||||
| "box_cutout"
|
||||
| "generate_image"
|
||||
| "generate_video";
|
||||
@ -28,6 +29,7 @@ export type DirectAgentToolName =
|
||||
| "enhanceImage"
|
||||
| "resizeImage"
|
||||
| "extendImage"
|
||||
| "angleEditImage"
|
||||
| "boxCutout"
|
||||
| "generateImage"
|
||||
| "generateVideo";
|
||||
@ -98,6 +100,8 @@ function mapDirectToolNameToCapability(
|
||||
return "resize";
|
||||
case "extendImage":
|
||||
return "extend";
|
||||
case "angleEditImage":
|
||||
return "angle_variant";
|
||||
case "boxCutout":
|
||||
return "box_cutout";
|
||||
case "generateImage":
|
||||
@ -199,6 +203,13 @@ export function resolveDirectAgentToolExecution(
|
||||
if (preferredCapability === "extend" || routeSubIntent === "extend") {
|
||||
return "extendImage";
|
||||
}
|
||||
if (
|
||||
preferredCapability === "angle_variant" ||
|
||||
routeSubIntent === "angle_variant" ||
|
||||
routeSubIntent === "generate_angle_variant"
|
||||
) {
|
||||
return "angleEditImage";
|
||||
}
|
||||
if (preferredCapability === "box_cutout" || routeSubIntent === "box_cutout") {
|
||||
return "boxCutout";
|
||||
}
|
||||
@ -252,15 +263,17 @@ export function resolveDirectAgentToolExecution(
|
||||
? "resizeImage"
|
||||
: hasToolCall("extendImage")
|
||||
? "extendImage"
|
||||
: hasToolCall("boxCutout")
|
||||
? "boxCutout"
|
||||
: hasToolCall("editTargetImage") || hasToolCall("editImage")
|
||||
? preferredSpecificImageEditTool || "editImage"
|
||||
: hasToolCall("generateNewImage") || hasToolCall("generateImage")
|
||||
? "generateImage"
|
||||
: hasToolCall("generateVideo")
|
||||
? "generateVideo"
|
||||
: null) ||
|
||||
: hasToolCall("angleEditImage")
|
||||
? "angleEditImage"
|
||||
: hasToolCall("boxCutout")
|
||||
? "boxCutout"
|
||||
: hasToolCall("editTargetImage") || hasToolCall("editImage")
|
||||
? preferredSpecificImageEditTool || "editImage"
|
||||
: hasToolCall("generateNewImage") || hasToolCall("generateImage")
|
||||
? "generateImage"
|
||||
: hasToolCall("generateVideo")
|
||||
? "generateVideo"
|
||||
: null) ||
|
||||
(() => {
|
||||
if (
|
||||
execution?.preferredCapability === "analyze" ||
|
||||
@ -299,6 +312,13 @@ export function resolveDirectAgentToolExecution(
|
||||
) {
|
||||
return "extendImage";
|
||||
}
|
||||
if (
|
||||
execution?.preferredCapability === "angle_variant" ||
|
||||
execution?.routeSubIntent === "angle_variant" ||
|
||||
execution?.routeSubIntent === "generate_angle_variant"
|
||||
) {
|
||||
return "angleEditImage";
|
||||
}
|
||||
if (
|
||||
execution?.preferredCapability === "box_cutout" ||
|
||||
execution?.routeSubIntent === "box_cutout"
|
||||
@ -380,6 +400,7 @@ export function isDirectAgentBatchImageEditCapability(
|
||||
case "enhance":
|
||||
case "resize":
|
||||
case "extend":
|
||||
case "angle_variant":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
@ -410,6 +431,7 @@ export function isDirectAgentImageEditToolName(
|
||||
| "removeWatermark"
|
||||
| "resizeImage"
|
||||
| "extendImage"
|
||||
| "angleEditImage"
|
||||
| "boxCutout" {
|
||||
switch (toolName) {
|
||||
case "editImage":
|
||||
@ -418,6 +440,7 @@ export function isDirectAgentImageEditToolName(
|
||||
case "removeWatermark":
|
||||
case "resizeImage":
|
||||
case "extendImage":
|
||||
case "angleEditImage":
|
||||
case "boxCutout":
|
||||
return true;
|
||||
default:
|
||||
@ -441,6 +464,8 @@ export function getDirectAgentToolLabel(capability: DirectAgentToolCapability):
|
||||
return "Agent工具:尺寸调整";
|
||||
case "extend":
|
||||
return "Agent工具:图像拓展";
|
||||
case "angle_variant":
|
||||
return "Agent工具:多视角";
|
||||
case "box_cutout":
|
||||
return "Agent工具:框选抠图";
|
||||
case "generate_image":
|
||||
@ -486,6 +511,8 @@ export function getDirectAgentToolSuccessMessage(
|
||||
return "已完成尺寸调整,结果如下:";
|
||||
case "extend":
|
||||
return "已完成图像拓展,结果如下:";
|
||||
case "angle_variant":
|
||||
return "已完成多视角生成,结果如下:";
|
||||
case "box_cutout":
|
||||
return "已完成框选抠图,结果如下:";
|
||||
case "generate_image":
|
||||
@ -515,6 +542,8 @@ export function getDirectAgentToolTargetMissingMessage(
|
||||
return "请先上传或选中一张图片,我才能继续帮你调整尺寸。";
|
||||
case "extend":
|
||||
return "请先上传或选中一张图片,我才能继续帮你做图像拓展。";
|
||||
case "angle_variant":
|
||||
return "请先上传或选中一张图片,我才能继续帮你生成多视角变体。";
|
||||
case "box_cutout":
|
||||
return "请先选中一张图片并提供框选区域,我才能继续帮你做框选抠图。";
|
||||
case "generate_image":
|
||||
@ -553,6 +582,8 @@ export function getDirectAgentToolErrorMessage(
|
||||
return `尺寸调整暂时失败:${detail}`;
|
||||
case "extend":
|
||||
return `图像拓展暂时失败:${detail}`;
|
||||
case "angle_variant":
|
||||
return `多视角生成暂时失败:${detail}`;
|
||||
case "box_cutout":
|
||||
return `框选抠图暂时失败:${detail}`;
|
||||
case "generate_image":
|
||||
|
||||
@ -30,6 +30,11 @@ import {
|
||||
} from "@/lib/ai/runtime/direct-agent-execution";
|
||||
import { buildDirectAgentExecutionPrompt } from "@/lib/ai/runtime/optimized-prompt";
|
||||
import { buildAgentToolImagesMarkdown } from "@/lib/ai/runtime/agent-media-markdown";
|
||||
import {
|
||||
clampAngleEditNumber,
|
||||
inferAngleEditOptionsFromText,
|
||||
type DirectAgentAngleEditOptions,
|
||||
} from "@/lib/ai/runtime/angle-edit-options";
|
||||
|
||||
interface DirectAgentImageParamsSnapshot {
|
||||
negativePrompt?: string;
|
||||
@ -178,6 +183,44 @@ async function executeDirectAgentWatermarkRemoveJob(params: {
|
||||
};
|
||||
}
|
||||
|
||||
async function executeDirectAgentAngleEditJob(params: {
|
||||
requestUrl: string;
|
||||
requestHeaders: Headers;
|
||||
imageUrl: string;
|
||||
options: DirectAgentAngleEditOptions;
|
||||
}) {
|
||||
const endpoint = new URL("/api/angle-edit", params.requestUrl).toString();
|
||||
const cookieHeader = params.requestHeaders.get("cookie") || "";
|
||||
const result = await requestLongTaskResult({
|
||||
endpoint,
|
||||
payload: {
|
||||
image_url: params.imageUrl,
|
||||
horizontal_angle: params.options.horizontalAngle,
|
||||
vertical_angle: params.options.verticalAngle,
|
||||
zoom: params.options.zoom,
|
||||
...(params.options.loraScale !== undefined
|
||||
? { lora_scale: params.options.loraScale }
|
||||
: {}),
|
||||
},
|
||||
timeoutMs: 20 * 60 * 1000,
|
||||
headers: cookieHeader
|
||||
? {
|
||||
cookie: cookieHeader,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
if (!result?.success || !result?.image_url) {
|
||||
throw new Error(String(result?.error || result?.detail || "多视角生成失败"));
|
||||
}
|
||||
|
||||
return {
|
||||
originalUrl: String(result.image_url),
|
||||
thumbnailUrl: String(result.thumbnail_url || result.image_url),
|
||||
imageSize: "2K",
|
||||
};
|
||||
}
|
||||
|
||||
export function isDirectAgentImageEditServiceError(error: unknown) {
|
||||
return (
|
||||
error instanceof RmbgServiceError ||
|
||||
@ -454,6 +497,61 @@ export async function executeDirectAgentImageEditTool(
|
||||
});
|
||||
}
|
||||
|
||||
if (params.capability === "angle_variant") {
|
||||
const plannedArgs = findAgentToolCallArgs(
|
||||
"angleEditImage",
|
||||
params.smartAutoExecution,
|
||||
params.smartAutoPlan,
|
||||
);
|
||||
const inferredOptions = inferAngleEditOptionsFromText(params.requestTextContent);
|
||||
const options: DirectAgentAngleEditOptions = {
|
||||
horizontalAngle: clampAngleEditNumber(
|
||||
toFiniteNumber(
|
||||
plannedArgs?.horizontal_angle ??
|
||||
plannedArgs?.horizontalAngle ??
|
||||
plannedArgs?.angle ??
|
||||
inferredOptions.horizontalAngle,
|
||||
),
|
||||
0,
|
||||
360,
|
||||
inferredOptions.horizontalAngle,
|
||||
),
|
||||
verticalAngle: clampAngleEditNumber(
|
||||
toFiniteNumber(
|
||||
plannedArgs?.vertical_angle ??
|
||||
plannedArgs?.verticalAngle ??
|
||||
plannedArgs?.pitch ??
|
||||
inferredOptions.verticalAngle,
|
||||
),
|
||||
-30,
|
||||
90,
|
||||
inferredOptions.verticalAngle,
|
||||
),
|
||||
zoom: clampAngleEditNumber(
|
||||
toFiniteNumber(plannedArgs?.zoom ?? inferredOptions.zoom),
|
||||
0,
|
||||
10,
|
||||
inferredOptions.zoom,
|
||||
),
|
||||
loraScale: toFiniteNumber(plannedArgs?.lora_scale ?? plannedArgs?.loraScale),
|
||||
};
|
||||
const result = await executeDirectAgentAngleEditJob({
|
||||
requestUrl: params.requestUrl,
|
||||
requestHeaders: params.requestHeaders,
|
||||
imageUrl: params.sourceImageUrl,
|
||||
options,
|
||||
});
|
||||
|
||||
return finalizeDirectAgentImageEditResult({
|
||||
userId: params.userId,
|
||||
capability: params.capability,
|
||||
originalUrl: result.originalUrl,
|
||||
thumbnailUrl: result.thumbnailUrl,
|
||||
imageSize: result.imageSize,
|
||||
mediaAssetId: null,
|
||||
});
|
||||
}
|
||||
|
||||
if (params.capability === "box_cutout") {
|
||||
const plannedArgs = findAgentToolCallArgs(
|
||||
"boxCutout",
|
||||
|
||||
@ -22,6 +22,7 @@ export type IntentArbiterPreferredCapability =
|
||||
| "enhance"
|
||||
| "extend"
|
||||
| "resize"
|
||||
| "angle_variant"
|
||||
| "box_cutout"
|
||||
| "generate_image"
|
||||
| "generate_video"
|
||||
@ -114,7 +115,7 @@ function isExplicitImageGenerationRequest(text: string): boolean {
|
||||
|
||||
function isLikelyImageEditRequest(text: string): boolean {
|
||||
const normalizedText = String(text || "").toLowerCase();
|
||||
return /抠图|去背景|透明背景|去背|扣图|移除背景|去水印|水印|去logo|去\s*logo|移除logo|去除logo|换背景|改背景|背景换成|背景改成|白底|纯色背景|替换.*背景|变清晰|高清|锐化|降噪|提升质量|修复|清晰化|扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|换成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|改成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|比例|画幅|竖版|横版|去除|消除|擦掉|删除|移除|p掉|重绘|风格化|卡通化|动漫风|油画风|转.*风格|变.*风格|改.*风格|调色|调整颜色|亮度|对比度|饱和度|色调|曝光|滤镜|改成.*色|换成.*色|变成.*色|颜色改成|颜色换成|颜色变成|缩放|放大|缩小|调整尺寸|裁剪|resize|crop|修图|p图|改图|调图|编辑.*图|优化.*图|重做|再调整|继续改|继续调/.test(
|
||||
return /抠图|去背景|透明背景|去背|扣图|移除背景|去水印|水印|去logo|去\s*logo|移除logo|去除logo|换背景|改背景|背景换成|背景改成|白底|纯色背景|替换.*背景|变清晰|高清|锐化|降噪|提升质量|修复|清晰化|扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|换成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|改成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|比例|画幅|竖版|横版|多视角|换视角|改视角|视角变体|新角度|换角度|改角度|侧面|侧视|背面|背后|俯视|俯拍|仰视|仰拍|低机位|高机位|去除|消除|擦掉|删除|移除|p掉|重绘|风格化|卡通化|动漫风|油画风|转.*风格|变.*风格|改.*风格|调色|调整颜色|亮度|对比度|饱和度|色调|曝光|滤镜|改成.*色|换成.*色|变成.*色|颜色改成|颜色换成|颜色变成|缩放|放大|缩小|调整尺寸|裁剪|resize|crop|修图|p图|改图|调图|编辑.*图|优化.*图|重做|再调整|继续改|继续调/.test(
|
||||
normalizedText,
|
||||
);
|
||||
}
|
||||
@ -124,6 +125,7 @@ function inferDefaultImageEditSubIntent(text: string): string {
|
||||
if (/去水印|水印|去logo|去\s*logo|移除logo|去除logo/.test(lowerText)) return "remove_watermark";
|
||||
if (/抠图|去背景|透明背景|去背|扣图|移除背景/.test(lowerText)) return "remove_background";
|
||||
if (/换背景|改背景|背景换成|背景改成|白底|透明底/.test(lowerText)) return "replace_background";
|
||||
if (/多视角|换视角|改视角|视角变体|新角度|换角度|改角度|侧面|侧视|背面|背后|俯视|俯拍|仰视|仰拍|低机位|高机位/.test(lowerText)) return "angle_variant";
|
||||
if (/扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|比例|画幅|竖版|横版|竖图|横图|9:16|16:9|1:1/.test(lowerText)) return "extend";
|
||||
if (/风格|卡通|动漫|油画|像素风|写实|插画/.test(lowerText)) return "style_transfer";
|
||||
if (/颜色|调色|亮度|对比度|饱和度|改成.*色|换成.*色/.test(lowerText)) return "color_adjust";
|
||||
@ -169,6 +171,9 @@ function mapPreferredCapability(
|
||||
return "extend";
|
||||
case "resize":
|
||||
return "resize";
|
||||
case "angle_variant":
|
||||
case "generate_angle_variant":
|
||||
return "angle_variant";
|
||||
case "box_cutout":
|
||||
return "box_cutout";
|
||||
case "enhance":
|
||||
|
||||
@ -110,6 +110,7 @@ export function routeAgentSkill(
|
||||
"enhanceImage",
|
||||
"extendImage",
|
||||
"resizeImage",
|
||||
"angleEditImage",
|
||||
"boxCutout",
|
||||
"useSelectedCanvasImage",
|
||||
"useLatestResultImage",
|
||||
|
||||
@ -34,6 +34,7 @@ function inferDefaultImageEditSubIntent(text: string): AnalysisResult["subIntent
|
||||
if (/去水印|水印|去logo|去\s*logo|移除logo|去除logo/.test(lowerText)) return "remove_watermark";
|
||||
if (/抠图|去背景|透明背景|去背|扣图|移除背景/.test(lowerText)) return "remove_background";
|
||||
if (/换背景|改背景|背景换成|背景改成|白底|透明底/.test(lowerText)) return "replace_background";
|
||||
if (/多视角|换视角|改视角|视角变体|新角度|换角度|改角度|侧面|侧视|背面|背后|俯视|俯拍|仰视|仰拍|低机位|高机位/.test(lowerText)) return "angle_variant";
|
||||
if (/扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|比例|画幅|竖版|横版|竖图|横图|9:16|16:9|1:1/.test(lowerText)) return "extend";
|
||||
if (/风格|卡通|动漫|油画|像素风|写实|插画/.test(lowerText)) return "style_transfer";
|
||||
if (/颜色|调色|亮度|对比度|饱和度|改成.*色|换成.*色/.test(lowerText)) return "color_adjust";
|
||||
@ -138,7 +139,7 @@ export function isExplicitImageGenerationRequest(text: string): boolean {
|
||||
|
||||
function isLikelyImageEditRequest(text: string): boolean {
|
||||
const normalizedText = text.toLowerCase();
|
||||
return /抠图|去背景|透明背景|去背|扣图|移除背景|去水印|水印|去logo|去\s*logo|移除logo|去除logo|换背景|改背景|背景换成|背景改成|白底|纯色背景|替换.*背景|变清晰|高清|锐化|降噪|提升质量|修复|清晰化|扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|换成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|改成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|比例|画幅|竖版|横版|去除|消除|擦掉|删除|移除|p掉|重绘|风格化|卡通化|动漫风|油画风|转.*风格|变.*风格|改.*风格|调色|调整颜色|亮度|对比度|饱和度|色调|曝光|滤镜|改成.*色|换成.*色|变成.*色|颜色改成|颜色换成|颜色变成|缩放|放大|缩小|调整尺寸|裁剪|resize|crop|修图|p图|改图|调图|编辑.*图|优化.*图|重做|再调整|继续改|继续调/.test(
|
||||
return /抠图|去背景|透明背景|去背|扣图|移除背景|去水印|水印|去logo|去\s*logo|移除logo|去除logo|换背景|改背景|背景换成|背景改成|白底|纯色背景|替换.*背景|变清晰|高清|锐化|降噪|提升质量|修复|清晰化|扩展|拓展|延伸|扩图|outpainting|拓宽|扩大|换成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|改成.*(?:比例|竖版|横版|竖图|横图|9:16|16:9|1:1)|比例|画幅|竖版|横版|多视角|换视角|改视角|视角变体|新角度|换角度|改角度|侧面|侧视|背面|背后|俯视|俯拍|仰视|仰拍|低机位|高机位|去除|消除|擦掉|删除|移除|p掉|重绘|风格化|卡通化|动漫风|油画风|转.*风格|变.*风格|改.*风格|调色|调整颜色|亮度|对比度|饱和度|色调|曝光|滤镜|改成.*色|换成.*色|变成.*色|颜色改成|颜色换成|颜色变成|缩放|放大|缩小|调整尺寸|裁剪|resize|crop|修图|p图|改图|调图|编辑.*图|优化.*图|重做|再调整|继续改|继续调/.test(
|
||||
normalizedText,
|
||||
);
|
||||
}
|
||||
@ -388,6 +389,7 @@ const ANALYZER_SYSTEM_PROMPT = `你是一个友好的AI多模态Agent。用户
|
||||
- replace_background: 换背景、改背景、白底
|
||||
- enhance: 变清晰、高清、锐化、降噪
|
||||
- extend: 扩展、拓展、延伸画布
|
||||
- angle_variant: 多视角、换角度、侧面、背面、俯视、仰视
|
||||
- inpaint: 去除某物、消除、擦掉、P掉、局部修改
|
||||
- style_transfer: 风格化、卡通化、动漫风、改风格
|
||||
- color_adjust: 调色、调整颜色、亮度、对比度
|
||||
@ -768,7 +770,7 @@ async function analyzeWithHardcodedQwen3VlFallback(params: {
|
||||
const prompt = [
|
||||
"你是图片需求分析助手。请结合图片与用户意图,输出严格 JSON,不要输出其他内容。",
|
||||
"intent 只能是 image_edit / vision / image_gen / video_gen / chat。",
|
||||
"subIntent 可选:remove_background / remove_watermark / replace_background / enhance / extend / inpaint / style_transfer / color_adjust / redo / resize。",
|
||||
"subIntent 可选:remove_background / remove_watermark / replace_background / enhance / extend / angle_variant / inpaint / style_transfer / color_adjust / redo / resize。",
|
||||
"如果用户提到视频、首帧、尾帧、分镜,intent=video_gen。",
|
||||
"有图片且用户在修改图片时,优先 intent=image_edit。",
|
||||
"JSON 字段:intent, subIntent, confidence, conversationalReply, needsClarification, clarifyingQuestions。",
|
||||
@ -2413,6 +2415,16 @@ export function fallbackKeywordAnalysis(
|
||||
};
|
||||
}
|
||||
|
||||
// 多视角/角度变体
|
||||
if (/多视角|换视角|改视角|视角变体|新角度|换角度|改角度|侧面|侧视|背面|背后|俯视|俯拍|仰视|仰拍|低机位|高机位/.test(lowerText)) {
|
||||
return {
|
||||
intent: "image_edit",
|
||||
subIntent: "angle_variant",
|
||||
confidence: 0.9,
|
||||
conversationalReply: "好的,我来帮你生成新的视角变体~",
|
||||
};
|
||||
}
|
||||
|
||||
// 尺寸调整
|
||||
if (/缩放|放大|缩小|调整尺寸|裁剪|resize|crop/.test(lowerText)) {
|
||||
return {
|
||||
|
||||
@ -224,6 +224,13 @@ function normalizeEditCapability(
|
||||
const normalizedText = String(text || "").toLowerCase();
|
||||
const subIntent = String(analysis.subIntent || "").trim().toLowerCase();
|
||||
|
||||
if (subIntent === "angle_variant" || /多视角|换视角|改视角|视角变体|新角度|换角度|改角度|侧面|侧视|背面|背后|俯视|俯拍|仰视|仰拍|低机位|高机位/.test(normalizedText)) {
|
||||
return {
|
||||
capability: "angle_variant",
|
||||
toolName: "angleEditImage",
|
||||
subIntent: "angle_variant",
|
||||
};
|
||||
}
|
||||
if (subIntent === "remove_watermark" || /去水印|水印|去logo|去\s*logo|移除logo|去除logo/.test(normalizedText)) {
|
||||
return {
|
||||
capability: "remove_watermark",
|
||||
|
||||
@ -23,6 +23,7 @@ export type AgentProjectToolName =
|
||||
| "enhanceImage"
|
||||
| "extendImage"
|
||||
| "resizeImage"
|
||||
| "angleEditImage"
|
||||
| "boxCutout"
|
||||
| "publishToCanvas"
|
||||
| "useSelectedCanvasImage"
|
||||
@ -199,6 +200,14 @@ const INTERNAL_AGENT_TOOLS: AgentProjectToolDefinition[] = [
|
||||
requiresImage: true,
|
||||
producesMedia: true,
|
||||
},
|
||||
{
|
||||
name: "angleEditImage",
|
||||
title: "多视角",
|
||||
description: "根据水平角、俯仰角和景别缩放生成主体的新视角变体,结果作为新版本保留原图。",
|
||||
category: "image",
|
||||
requiresImage: true,
|
||||
producesMedia: true,
|
||||
},
|
||||
{
|
||||
name: "boxCutout",
|
||||
title: "框选抠图",
|
||||
|
||||
@ -130,6 +130,7 @@ import {
|
||||
resolveDirectAgentToolExecution,
|
||||
shouldRunDirectAgentBatchImageEdit,
|
||||
} from "@/lib/ai/runtime/direct-agent-execution";
|
||||
import { inferAngleEditOptionsFromText } from "@/lib/ai/runtime/angle-edit-options";
|
||||
import {
|
||||
applyCanvasOperationPlanToCanvasData,
|
||||
CanvasAgentMutationError,
|
||||
@ -1098,10 +1099,75 @@ test.describe("canvas chat pure helpers", () => {
|
||||
userText: "把这几张都改成黄昏",
|
||||
})
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldRunDirectAgentBatchImageEdit({
|
||||
capability: "angle_variant",
|
||||
sourceImageUrls: [
|
||||
"https://cdn.example.com/a.png",
|
||||
"https://cdn.example.com/b.png",
|
||||
],
|
||||
userText: "这两张都生成右侧面视角",
|
||||
})
|
||||
).toBe(true);
|
||||
expect(hasExplicitDirectAgentBatchImageEditIntent("只处理第一张")).toBe(false);
|
||||
expect(isDirectAgentBatchImageEditCapability("box_cutout")).toBe(false);
|
||||
});
|
||||
|
||||
test("routes angle variant requests through the dedicated direct image tool", () => {
|
||||
const result = resolveDirectAgentToolExecution({
|
||||
mode: "edit_image",
|
||||
routeIntent: "image_edit",
|
||||
routeSubIntent: "angle_variant",
|
||||
targetSource: "explicit_selection",
|
||||
preferredCapability: "angle_variant",
|
||||
shouldUseStructuredReply: true,
|
||||
shouldOptimizePrompt: true,
|
||||
shouldApplyImagePipeline: true,
|
||||
shouldApplyVideoPipeline: false,
|
||||
shouldStayConversational: false,
|
||||
shouldPublishToCanvas: false,
|
||||
toolCalls: [
|
||||
{
|
||||
name: "resolveTargetImage",
|
||||
args: { source: "selected_canvas" },
|
||||
},
|
||||
{
|
||||
name: "editTargetImage",
|
||||
reason: "planner only provided the generic edit tool",
|
||||
},
|
||||
],
|
||||
availableTools: ["resolveTargetImage", "editTargetImage", "angleEditImage"],
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
toolName: "angleEditImage",
|
||||
capability: "angle_variant",
|
||||
sourcePreferences: ["selected_canvas"],
|
||||
});
|
||||
expect(getDirectAgentToolLabel("angle_variant")).toBe("Agent工具:多视角");
|
||||
expect(getDirectAgentToolSuccessMessage("angle_variant")).toBe(
|
||||
"已完成多视角生成,结果如下:"
|
||||
);
|
||||
});
|
||||
|
||||
test("infers deterministic angle edit options from canvas agent text", () => {
|
||||
expect(inferAngleEditOptionsFromText("生成右侧面近景视角")).toMatchObject({
|
||||
horizontalAngle: 90,
|
||||
verticalAngle: 0,
|
||||
zoom: 7,
|
||||
});
|
||||
expect(inferAngleEditOptionsFromText("改成背面俯视远景")).toMatchObject({
|
||||
horizontalAngle: 180,
|
||||
verticalAngle: 45,
|
||||
zoom: 3,
|
||||
});
|
||||
expect(inferAngleEditOptionsFromText("水平角120度,垂直角-10度,zoom8")).toMatchObject({
|
||||
horizontalAngle: 120,
|
||||
verticalAngle: -10,
|
||||
zoom: 8,
|
||||
});
|
||||
});
|
||||
|
||||
test("plans watermark removal results as non-destructive canvas revisions", () => {
|
||||
const result = buildCanvasAgentDirectWritebackPlan({
|
||||
projectId: plannerContext.projectId || "",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user