From 74ae88edbb4cb22607e1811046df868590d6ed30 Mon Sep 17 00:00:00 2001 From: 134 Date: Wed, 17 Jun 2026 21:44:57 +0800 Subject: [PATCH] fix: improve scale adjust guidance and scan feedback --- app/api/canvas/local-edit/route.ts | 62 ++++++++++++++----- .../agents/AgentGenerationWorkspaceDarkV2.tsx | 59 +++++++++++++----- .../canvas/LiblibVideoCanvasReplica.tsx | 8 ++- components/canvas/nodes/ImageNode.tsx | 10 ++- lib/canvas/generate-image.ts | 8 ++- services/canvas/index.ts | 2 + 6 files changed, 110 insertions(+), 39 deletions(-) diff --git a/app/api/canvas/local-edit/route.ts b/app/api/canvas/local-edit/route.ts index a6bf18d09..eb73f081c 100644 --- a/app/api/canvas/local-edit/route.ts +++ b/app/api/canvas/local-edit/route.ts @@ -79,6 +79,7 @@ const MAX_LOCAL_EDIT_REGIONS = 8; const MAX_REFERENCE_IMAGES_PER_REGION = 3; const MIN_REGION_EDGE_PX = 24; const SCALE_ADJUST_OPERATION = "scale-adjust"; +type LocalEditOperation = "local-edit" | typeof SCALE_ADJUST_OPERATION; const SCALE_ADJUST_PERCENT_OPTIONS = [-30, -20, -10, 10, 20, 30] as const; type ScaleAdjustPercent = (typeof SCALE_ADJUST_PERCENT_OPTIONS)[number]; const DEFAULT_REGION_COLORS = [ @@ -224,6 +225,20 @@ function getScaleAdjustLabel(scalePercent: ScaleAdjustPercent) { return scalePercent > 0 ? `放大 ${scalePercent}%` : `缩小 ${Math.abs(scalePercent)}%`; } +function getScaleAdjustTargetLabel(scalePercent: ScaleAdjustPercent) { + return `${Math.round((1 + scalePercent / 100) * 100)}%`; +} + +function buildLocalEditDisplayPrompt(params: { + operation: LocalEditOperation; + scalePercent: ScaleAdjustPercent | null; +}) { + if (params.operation === SCALE_ADJUST_OPERATION && params.scalePercent !== null) { + return `比例调整:${getScaleAdjustLabel(params.scalePercent)}`; + } + return "局部编辑处理"; +} + function getColorName(hex: string) { const normalized = hex.toLowerCase(); const names: Record = { @@ -340,6 +355,7 @@ function buildAnnotationSvg(params: { width: number; height: number; regions: NormalizedLocalEditRegion[]; + hidePromptText?: boolean; }) { const { width, height, regions } = params; const minSide = Math.max(1, Math.min(width, height)); @@ -354,7 +370,9 @@ function buildAnnotationSvg(params: { const regionSvg = regions .map((region) => { const { left, top, width: rectWidth, height: rectHeight } = region.pixelRect; - const labelText = `区域${region.index} ${region.prompt}`; + const labelText = params.hidePromptText + ? `区域${region.index} 目标主体` + : `区域${region.index} ${region.prompt}`; const maxChars = Math.max(8, Math.min(24, Math.floor((width * 0.42) / Math.max(1, fontSize * 0.58)))); const lines = wrapText(labelText, maxChars, 3); const labelWidth = Math.min( @@ -443,29 +461,35 @@ function buildScaleAdjustPrompt( regions: NormalizedLocalEditRegion[], scalePercent: ScaleAdjustPercent ) { - const scaleFactor = Math.round((1 + scalePercent / 100) * 100) / 100; const actionLabel = getScaleAdjustLabel(scalePercent); + const targetLabel = getScaleAdjustTargetLabel(scalePercent); const regionLines = regions .map((region) => { const left = Math.round(region.rect.x * 1000) / 10; const top = Math.round(region.rect.y * 1000) / 10; const width = Math.round(region.rect.width * 1000) / 10; const height = Math.round(region.rect.height * 1000) / 10; - return `区域 ${region.index}(原图位置 x=${left}%,y=${top}%,w=${width}%,h=${height}%):该矩形只是“寻找目标主体”的范围,不是要缩放或贴回的矩形补丁。请识别此范围内最主要的商品/物体主体,以主体自身轮廓和视觉中心为准,执行整体等比${actionLabel},目标视觉尺寸为原主体的 ${Math.round(scaleFactor * 100)}%。如果框中只覆盖到主体的一部分,请根据连接关系推断该主体的完整可见结构,只调整该主体,不调整矩形框内背景。`; + const pixel = region.pixelRect; + const right = pixel.left + pixel.width; + const bottom = pixel.top + pixel.height; + const centerX = pixel.left + Math.round(pixel.width / 2); + const centerY = pixel.top + Math.round(pixel.height / 2); + return `区域 ${region.index}(相对位置 x=${left}%,y=${top}%,w=${width}%,h=${height}%;像素范围 left=${pixel.left}, top=${pixel.top}, right=${right}, bottom=${bottom}, center=(${centerX}, ${centerY})):该矩形只是“寻找目标主体”的范围,不是要缩放或贴回的矩形补丁。请识别此范围内最主要的商品/物体主体,以主体自身轮廓和视觉中心为准,执行整体等比${actionLabel},目标视觉尺寸必须约为原主体的 ${targetLabel}。如果框中只覆盖到主体的一部分,请根据连接关系推断该主体的完整可见结构,只调整该主体,不调整矩形框内背景。`; }) .join("\n"); return [ "输入图片1是原始干净图,也是最终输出必须继承的唯一画面基底。", - "系统已根据用户框选区域计算出下方每个区域的原图相对坐标;框选区域只是定位目标主体的搜索范围,不是最终画面的裁切块、贴片或蒙版边界。", + "输入图片2是定位参考图:它只用于告诉你用户框选的目标主体位置。输入图片2里的彩色框、编号、标签、半透明遮罩、文字和 UI 只是定位标记,绝不能出现在最终画面中。", + "系统已根据用户框选区域计算出下方每个区域的原图坐标;框选区域只是定位目标主体的搜索范围,不是最终画面的裁切块、贴片或蒙版边界。", "最终结果必须保持输入图片1的原始画幅比例、构图边界、镜头透视、光线、色温、材质质感和清晰度。", "请不要生成任何彩色框、矩形边框、编号圆点、提示词气泡、标注文字、UI 控件或半透明色块。", "", - `任务:对用户框选的主体物体做“比例调整”,统一执行${actionLabel}。`, + `任务:对用户框选的主体物体做“比例调整”,统一执行${actionLabel}。目标主体调整后的视觉尺寸必须约为原主体的 ${targetLabel},普通用户肉眼应能清楚看出主体尺寸已经变化;如果输出看起来和原图一样,就是失败。`, regionLines, "", "关键要求:", - "1. 只调整目标主体物体本身的整体视觉比例,必须等比缩放,不得压扁、拉伸、扭曲、改变外观结构、改变材质、改变颜色、改变文字或图案。", + `1. 只调整目标主体物体本身的整体视觉比例,必须等比缩放到约 ${targetLabel},不得保留原尺寸,不得压扁、拉伸、扭曲、改变外观结构、改变材质、改变颜色、改变文字或图案。`, "2. 严禁把整个矩形框选区域当成图片块整体缩放、平移、复制、裁切或贴回;最终画面不得出现矩形拼接边缘、色差硬边、补丁感、裁切线或局部贴片痕迹。", "3. 缩小时,目标主体应围绕原位置视觉中心自然变小;主体缩小后露出的区域必须根据周围真实背景、阴影、遮挡、纹理和透视进行无缝补全。", "4. 放大时,目标主体应围绕原位置视觉中心自然变大;边缘与遮挡关系要合理,不能变形,也不能产生突兀裁切。", @@ -556,8 +580,13 @@ export async function POST(request: NextRequest) { return NextResponse.json(pointsCharge.payload, { status: pointsCharge.status }); } + const isScaleAdjustOperation = + operation === SCALE_ADJUST_OPERATION && scalePercent !== null; + const displayPrompt = buildLocalEditDisplayPrompt({ operation, scalePercent }); + const annotationSvg = buildAnnotationSvg({ height: sourceHeight, + hidePromptText: isScaleAdjustOperation, regions, width: sourceWidth, }); @@ -571,14 +600,15 @@ export async function POST(request: NextRequest) { toArrayBuffer(annotatedBuffer) ); const prompt = - operation === SCALE_ADJUST_OPERATION && scalePercent !== null - ? buildScaleAdjustPrompt(regions, scalePercent) + isScaleAdjustOperation + ? buildScaleAdjustPrompt(regions, scalePercent as ScaleAdjustPercent) : buildLocalEditPrompt(regions); - const isScaleAdjustOperation = - operation === SCALE_ADJUST_OPERATION && scalePercent !== null; const regionReferenceImageUrls = regions.flatMap((region) => region.referenceImages.map((reference) => reference.url) ); + const referenceImages = isScaleAdjustOperation + ? [sourceImageUrl, annotatedUpload.url, ...regionReferenceImageUrls] + : [sourceImageUrl, ...regionReferenceImageUrls]; const resolvedAspectRatio = resolveLocalEditAspectRatio( body.aspectRatio, sourceWidth, @@ -592,7 +622,8 @@ export async function POST(request: NextRequest) { operation === SCALE_ADJUST_OPERATION ? "canvas-scale-adjust" : "canvas-local-edit", debugMeta: { annotatedReferenceUrl: annotatedUpload.url, - annotatedReferenceSentToModel: false, + annotatedReferenceSentToModel: isScaleAdjustOperation, + displayPrompt, operation, resolvedAspectRatio, regionReferenceImageCount: regionReferenceImageUrls.length, @@ -603,6 +634,7 @@ export async function POST(request: NextRequest) { sourceNodeId: body.sourceNodeId || null, sourceWidth, }, + displayPrompt, imageCount: 1, imageSize: typeof body.imageSize === "string" && body.imageSize.trim() @@ -618,10 +650,7 @@ export async function POST(request: NextRequest) { : "彩色框、蓝色边框、绿色边框、红色边框、橙色边框、矩形框、编号圆点、提示词气泡、标注文字、UI 控件、半透明色块、选区边框、白边、画幅压扁、画面拉伸", prompt, promptExtend: false, - referenceImages: [ - sourceImageUrl, - ...regionReferenceImageUrls, - ], + referenceImages, localEditComposite: isScaleAdjustOperation ? undefined : { @@ -658,7 +687,8 @@ export async function POST(request: NextRequest) { success: true, annotatedReferenceUrl: annotatedUpload.url, aspectRatio: resolvedAspectRatio, - prompt, + displayPrompt, + prompt: displayPrompt, regions, sourceHeight, sourceWidth, diff --git a/components/agents/AgentGenerationWorkspaceDarkV2.tsx b/components/agents/AgentGenerationWorkspaceDarkV2.tsx index 67dc0c4cf..3aae8dbfd 100644 --- a/components/agents/AgentGenerationWorkspaceDarkV2.tsx +++ b/components/agents/AgentGenerationWorkspaceDarkV2.tsx @@ -1931,38 +1931,63 @@ function saveStoredAgentPendingGenerations(items: LocalGenerationPendingState[]) function DetailResultProcessingScanOverlay({ label }: { label: string }) { return ( -
+
+
-
-
-
-
+
+
+
+
+
-
+
{label}
diff --git a/components/canvas/LiblibVideoCanvasReplica.tsx b/components/canvas/LiblibVideoCanvasReplica.tsx index 2346bee60..227473f3e 100644 --- a/components/canvas/LiblibVideoCanvasReplica.tsx +++ b/components/canvas/LiblibVideoCanvasReplica.tsx @@ -4350,6 +4350,8 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib return; } + const localEditDisplayPrompt = + response.data.displayPrompt || response.data.prompt || "局部编辑处理"; const sourceWidth = Number(response.data.sourceWidth || 0); const sourceHeight = Number(response.data.sourceHeight || 0); const sourceRatio = @@ -4364,7 +4366,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib height: resultHeight, mediaType: "image", mediaUrl: "", - prompt: response.data.prompt, + prompt: localEditDisplayPrompt, title: "局部编辑", width: resultWidth, x: Math.round(anchor.x + anchor.width + OFFICIAL_NODE_GAP), @@ -4391,7 +4393,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib annotatedReferenceUrl: response.data.annotatedReferenceUrl, aspectRatio: response.data.aspectRatio, mode: "local-edit", - prompt: response.data.prompt, + prompt: localEditDisplayPrompt, regions: validRegions, sourceNodeId: anchor.id, sourceHeight: response.data.sourceHeight, @@ -4400,7 +4402,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib taskId: response.data.taskId, }, pendingLabel: "局部编辑中", - prompt: response.data.prompt, + prompt: localEditDisplayPrompt, sourceNodeId: anchor.id, sourceLocalEditRegions: validRegions, sourceNaturalHeight: response.data.sourceHeight, diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index 5c1c08f48..b3237fcdd 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -3905,6 +3905,8 @@ export function ImageNode({ } const taskId = submitResult.data.taskId; + const localEditDisplayPrompt = + submitResult.data.displayPrompt || submitResult.data.prompt || `${actionName}处理中...`; const localEditSourceWidth = Number(submitResult.data.sourceWidth || 0); const localEditSourceHeight = Number(submitResult.data.sourceHeight || 0); const localEditSourceRatio = @@ -3923,7 +3925,7 @@ export function ImageNode({ data: { src: "", status: "generating", - prompt: submitResult.data.prompt || `${actionName}处理中...`, + prompt: localEditDisplayPrompt, title: `${displayTitle}-${actionName}`, originalSrc: data.src, metadata: { @@ -3961,7 +3963,11 @@ export function ImageNode({ data: { src: generatedUrl, status: "completed", - prompt: task.result?.prompt || submitResult.data.prompt || `${actionName}结果`, + prompt: + task.result?.prompt || + submitResult.data.displayPrompt || + submitResult.data.prompt || + `${actionName}结果`, title: `${displayTitle}-${actionName}`, originalSrc: data.src, model: task.result?.model || task.result?.modelRef || undefined, diff --git a/lib/canvas/generate-image.ts b/lib/canvas/generate-image.ts index 187266fb1..79affca6e 100644 --- a/lib/canvas/generate-image.ts +++ b/lib/canvas/generate-image.ts @@ -44,6 +44,7 @@ export interface CanvasLocalEditCompositeConfig { export interface CanvasGenerateImageRequest { prompt: string; + displayPrompt?: string; negativePrompt?: string; model?: string; aspectRatio?: string; @@ -675,6 +676,11 @@ export async function generateCanvasImage( hasLocalEditComposite: Boolean(localEditComposite), }); + const resultPrompt = + typeof request.displayPrompt === "string" && request.displayPrompt.trim() + ? request.displayPrompt.trim() + : prompt; + const messages = [ { role: "user" as const, @@ -1000,7 +1006,7 @@ export async function generateCanvasImage( images: finalImages, model: selectedModel.model.displayName, modelRef: selectedModel.model.id, - prompt, + prompt: resultPrompt, }; } catch (error) { console.error("[Canvas Generate Image] Failed:", { diff --git a/services/canvas/index.ts b/services/canvas/index.ts index 73fbb511e..ffda15169 100644 --- a/services/canvas/index.ts +++ b/services/canvas/index.ts @@ -345,6 +345,7 @@ export const imageGenerationService = { ApiResponse<{ annotatedReferenceUrl: string; aspectRatio?: string; + displayPrompt?: string; prompt: string; queueStatus?: unknown; sourceHeight?: number; @@ -366,6 +367,7 @@ export const imageGenerationService = { data: { annotatedReferenceUrl: data.annotatedReferenceUrl, aspectRatio: data.aspectRatio, + displayPrompt: data.displayPrompt, prompt: data.prompt, queueStatus: data.queueStatus, sourceHeight: data.sourceHeight,