From 3524a4ea164cad3ee26591e090316a68530daf13 Mon Sep 17 00:00:00 2001 From: 134 Date: Tue, 23 Jun 2026 22:36:38 +0800 Subject: [PATCH] fix: guide scale adjust with target annotations --- app/api/canvas/local-edit/route.ts | 86 +++++++++++++++++++++++------- lib/canvas/generate-image.ts | 2 +- 2 files changed, 67 insertions(+), 21 deletions(-) diff --git a/app/api/canvas/local-edit/route.ts b/app/api/canvas/local-edit/route.ts index 84c67276b..e5052f873 100644 --- a/app/api/canvas/local-edit/route.ts +++ b/app/api/canvas/local-edit/route.ts @@ -750,8 +750,11 @@ function buildScaleAdjustAnnotationSvg(params: { const strokeWidth = Math.max(3, Math.round(minSide * 0.0048)); const targetColor = "#00d4ff"; const sourceColor = "#ff3b30"; + const connectorColor = "#14c8ff"; const dash = Math.max(12, Math.round(strokeWidth * 2.8)); const gap = Math.max(8, Math.round(strokeWidth * 1.8)); + const connectorWidth = Math.max(2, Math.round(strokeWidth * 0.74)); + const markerSize = Math.max(8, Math.round(strokeWidth * 3.1)); const regionSvg = regions .map((region) => { @@ -772,10 +775,58 @@ function buildScaleAdjustAnnotationSvg(params: { height - badgeSize / 2 - 4 ); const fontSize = Math.max(13, Math.round(badgeSize * 0.52)); + const sourceRight = source.left + source.width; + const sourceBottom = source.top + source.height; + const targetRight = target.left + target.width; + const targetBottom = target.top + target.height; + const sourceCorners = [ + { x: source.left, y: source.top }, + { x: sourceRight, y: source.top }, + { x: sourceRight, y: sourceBottom }, + { x: source.left, y: sourceBottom }, + ]; + const targetCorners = [ + { x: target.left, y: target.top }, + { x: targetRight, y: target.top }, + { x: targetRight, y: targetBottom }, + { x: target.left, y: targetBottom }, + ]; + const connectorSvg = sourceCorners + .map((corner, cornerIndex) => { + const targetCorner = targetCorners[cornerIndex]; + const distance = Math.hypot(targetCorner.x - corner.x, targetCorner.y - corner.y); + if (distance < Math.max(8, strokeWidth * 2.2)) return ""; + const dx = (targetCorner.x - corner.x) / distance; + const dy = (targetCorner.y - corner.y) / distance; + const startInset = Math.min(distance * 0.16, Math.max(5, strokeWidth * 1.8)); + const endInset = Math.min(distance * 0.18, Math.max(8, strokeWidth * 2.8)); + const x1 = corner.x + dx * startInset; + const y1 = corner.y + dy * startInset; + const x2 = targetCorner.x - dx * endInset; + const y2 = targetCorner.y - dy * endInset; + return ``; + }) + .join(""); + const cornerHandleSize = Math.max(7, Math.round(strokeWidth * 2.2)); + const sourceCornerSvg = sourceCorners + .map( + (corner) => + `` + ) + .join(""); + const targetCornerSvg = targetCorners + .map( + (corner) => + `` + ) + .join(""); return ` + ${connectorSvg} + ${sourceCornerSvg} + ${targetCornerSvg} ${region.index} `; @@ -784,6 +835,11 @@ function buildScaleAdjustAnnotationSvg(params: { return Buffer.from(` + + + + + ${regionSvg} `); @@ -861,7 +917,7 @@ function buildScaleAdjustPrompt( cropIndex >= 0 ? `;输入图片${cropIndex + 3}是该区域的干净局部裁切参考图,只用于识别目标主体的身份、轮廓与原始比例` : ""; - return `区域 ${region.index}${cropNote}:请在输入图片1中找到输入图片2红色实线框所标记范围内最主要、可独立调整的商品/物体主体。红色实线框只是“寻找主体”的范围,不是最终边界,也不是要缩放或贴回的矩形补丁。${targetPositionNote}请直接读取输入图片2中青色虚线框相对红色实线框的真实大小和位置关系,并按这个框线关系调整主体;不要把框线换算成文字百分比来执行。目标虚线框不是要求主体填满矩形或变成矩形比例,请以主体自身轮廓、原始宽高比和视觉中心为准,整体等比调整主体大小和位置。严禁宽度变窄但高度变高,严禁横向挤压、纵向拉伸或改变主体透视形体。如果红框中只覆盖到主体的一部分,请根据连接关系推断该主体的完整可见结构,只调整该主体,不调整矩形框内背景。`; + return `区域 ${region.index}${cropNote}:请在输入图片1中找到输入图片2红色实线框所标记范围内最主要、可独立调整的商品/物体主体。红色实线框只是“寻找主体”的范围,不是最终边界,也不是要缩放或贴回的矩形补丁。${targetPositionNote}请直接读取输入图片2中青色虚线框相对红色实线框的真实大小和位置关系,并按这个框线关系调整主体;四条蓝色箭头从红框四角指向青框四角,表示主体外接轮廓从当前位置迁移到目标大小和目标位置的方向。不要把框线换算成文字百分比来执行。目标虚线框不是要求主体填满矩形或变成矩形比例,请以主体自身轮廓、原始宽高比和视觉中心为准,整体等比调整主体大小和位置。严禁宽度变窄但高度变高,严禁横向挤压、纵向拉伸或改变主体透视形体。如果红框中只覆盖到主体的一部分,请根据连接关系推断该主体的完整可见结构,只调整该主体,不调整矩形框内背景。`; }) .join("\n"); const cropLines = cropReferences @@ -875,16 +931,16 @@ function buildScaleAdjustPrompt( "这是一项局部主体比例 inpaint/edit 任务,不是整图缩放、整图裁切或重新构图任务。", sourceSizeLine, "输入图片1是原始干净图,也是最终输出必须继承的唯一画面基底。除目标主体尺寸变化与必要背景补全外,输入图片1中的文字、Logo、装饰、排版、背景、光线、构图边界和所有未选区域必须保持不变。", - "输入图片2是白底选区参考图,只用于说明输入图片1上的红色实线框和青色目标虚线框位置;它不包含场景内容,不是输出底图,不得学习或复刻它的框线、编号、半透明遮罩或 UI 标记。", + "输入图片2是白底选区参考图,只用于说明输入图片1上的红色实线框、青色目标虚线框和四角箭头关系;它不包含场景内容,不是输出底图,不得学习或复刻它的框线、箭头、编号、角点、半透明遮罩或 UI 标记。", cropLines ? `输入图片3及之后是框选区域的干净局部裁切参考图,作用是帮助你确认“要缩放的是哪个主体”。这些裁切图不是输出底图,不得被整体放大、贴回或覆盖到原图上。\n${cropLines}` : "", - "输入图片2中的红色实线框表示输入图片1里目标主体当前大致位置;它只用于定位主体,不限制调整后的空间。青色虚线框表示用户手画的比例调整后目标大小和目标位置。最终画面不得出现这些框线,主体也不需要填满虚线矩形;必须保持主体自身原始宽高比和透视形体。", + "输入图片2中的红色实线框表示输入图片1里目标主体当前大致位置;它只用于定位主体,不限制调整后的空间。青色虚线框表示用户手画的比例调整后目标大小和目标位置。四条蓝色箭头从红框四角指向青框四角:箭头向外表示放大,箭头向内表示缩小,箭头整体偏移表示移动。最终画面不得出现这些框线、箭头或角点,主体也不需要填满虚线矩形;必须保持主体自身原始宽高比和透视形体。", "如果系统或模型只能读取一张参考图,必须优先以输入图片1为输出基底,不得清理、改写或删除输入图片1中框选区域外的任何文字和图形。", "系统已根据用户框选区域计算出下方每个区域的原图坐标;框选区域只是定位目标主体的搜索范围,不是最终画面的裁切块、贴片、蒙版边界,也不是放大后的空间上限,更不是要整体缩放的矩形图层。", "最终结果必须保持原始干净图的画幅比例、构图边界、镜头透视、光线、色温、材质质感和清晰度;严禁整张图整体缩放、裁切、平移、重新排版或重新生成。", - "Highest priority rule: do NOT zoom, crop, resize, reframe, pan, or regenerate the full canvas. Treat image 1 as a locked background plate and only inpaint the selected subject footprint.", - "请不要生成任何彩色框、矩形边框、编号圆点、提示词气泡、标注文字、UI 控件或半透明色块。", + "Highest priority rule: do NOT zoom, crop, resize, reframe, pan, or regenerate the full canvas. Treat image 1 as a locked background plate; edit only the selected subject and the smallest necessary background around its old and new footprint.", + "请不要生成任何彩色框、矩形边框、箭头、连线、角点、编号圆点、提示词气泡、标注文字、UI 控件或半透明色块。", "不要把用户手画的目标框换算成百分比文字执行;必须直接以输入图片2里青色目标框线本身作为大小和位置参考。", "", "任务:先在输入图片1中识别红色实线框范围内最主要、可独立调整的主体物体,再只对这个“主体物体”做局部比例调整。调整后的主体大小和位置必须以输入图片2中对应青色目标框线为准;如果输出看起来和原图一样,就是失败。", @@ -1138,7 +1194,7 @@ export async function POST(request: NextRequest) { annotatedReferenceUrl: annotatedUpload.url, annotatedReferenceSentToModel: isScaleAdjustOperation, annotatedReferenceType: isScaleAdjustOperation - ? "white-selection-guide" + ? "white-source-target-arrow-guide" : "source-overlay", cleanSourceReferenceIndex: 1, configuredDetailModelId: configuredDetailModel || null, @@ -1162,8 +1218,9 @@ export async function POST(request: NextRequest) { scaleAdjustTargetRegionCount: regions.filter((region) => region.targetRect) .length, scaleAdjustPromptStrategy: isScaleAdjustOperation - ? "locked-plate-local-subject-target-box-v4" + ? "locked-plate-local-subject-target-box-arrows-v5" : undefined, + scaleAdjustPostCompositeApplied: false, scaleAdjustUsesFullFrameOutput: isScaleAdjustOperation, sourceHeight, sourceNodeId: body.sourceNodeId || null, @@ -1178,7 +1235,7 @@ export async function POST(request: NextRequest) { model: resolvedModel || undefined, negativePrompt: isScaleAdjustOperation - ? "彩色框、蓝色边框、绿色边框、红色边框、橙色边框、矩形框、编号圆点、提示词气泡、标注文字、UI 控件、半透明色块、选区边框、白边、画幅压扁、画面拉伸、整张图缩放、全图放大、全图裁切、全图平移、整体重绘、删除文字、清理文字、改写文字、文字模糊、Logo消失、主体拉伸、主体压扁、主体变窄、主体变高但变窄、横向挤压、纵向拉伸、透视变形、矩形贴片、硬边、拼接线、补丁感、局部裁切、色差边缘、背景断裂、背景错位、窗框错位、文字错位、地毯纹理错位、非主体背景重绘、主体未缩放、整块框选区域缩放" + ? "彩色框、蓝色边框、绿色边框、红色边框、橙色边框、矩形框、箭头、连线、角点、编号圆点、提示词气泡、标注文字、UI 控件、半透明色块、选区边框、白边、画幅压扁、画面拉伸、整张图缩放、全图放大、全图裁切、全图平移、整体重绘、删除文字、清理文字、改写文字、文字模糊、Logo消失、主体拉伸、主体压扁、主体变窄、主体变高但变窄、横向挤压、纵向拉伸、透视变形、矩形贴片、硬边、拼接线、补丁感、局部裁切、色差边缘、背景断裂、背景错位、窗框错位、文字错位、地毯纹理错位、非主体背景重绘、主体未缩放、整块框选区域缩放" : "彩色框、蓝色边框、绿色边框、红色边框、橙色边框、矩形框、编号圆点、提示词气泡、标注文字、UI 控件、半透明色块、选区边框、白边、画幅压扁、画面拉伸、主体截断、动物截断、缺头、缺尾巴、缺四肢、贴边裁切、矩形贴片、硬边、拼接线、补丁感、色差边缘、背景错位、地面纹理断裂、椅脚错位", prompt, promptExtend: false, @@ -1206,18 +1263,7 @@ export async function POST(request: NextRequest) { } : undefined, localEditComposite: isScaleAdjustOperation - ? { - sourceImageUrl, - regions: scaleAdjustCompositeRegions, - expandRegions: false, - maskMode: "diff", - featherMinPx: 4, - featherMaxPx: 18, - featherRatio: 0.006, - diffLowThreshold: 18, - diffHighThreshold: 54, - diffCompareBlurPx: 2.4, - } + ? undefined : { sourceImageUrl, regions: regions.map((region) => region.rect), diff --git a/lib/canvas/generate-image.ts b/lib/canvas/generate-image.ts index 985fa64ac..1813b367a 100644 --- a/lib/canvas/generate-image.ts +++ b/lib/canvas/generate-image.ts @@ -1974,6 +1974,7 @@ export async function generateCanvasImage( selectedModelId: selectedModel.model.modelId, error: caughtError instanceof Error ? caughtError.message : String(caughtError), }); + throw caughtError; } else { await recordAiModelHealthFailure({ model: selectedModel.model, @@ -2012,7 +2013,6 @@ export async function generateCanvasImage( const allowFailover = request.allowFailover !== false; const shouldFailover = shouldFailoverAiModelError(caughtError) || - scaleAdjustRejected || (Boolean(debugMeta?.detailFixedModelApplied) && Boolean(caughtError)); const fallbackModel = allowFailover && shouldFailover ? findCanvasFailoverModel({