From cd19fe0b8dd34c009f2a5b3f3856778a33008d6c Mon Sep 17 00:00:00 2001 From: 134 Date: Thu, 2 Jul 2026 15:20:58 +0800 Subject: [PATCH] fix: reduce scale adjust guide recompose bias --- app/api/canvas/local-edit/route.ts | 82 +++++------------------------- 1 file changed, 13 insertions(+), 69 deletions(-) diff --git a/app/api/canvas/local-edit/route.ts b/app/api/canvas/local-edit/route.ts index 697eaebf1..900bbce26 100644 --- a/app/api/canvas/local-edit/route.ts +++ b/app/api/canvas/local-edit/route.ts @@ -100,7 +100,6 @@ const MAX_REFERENCE_IMAGES_PER_REGION = 3; const MAX_SCALE_ADJUST_CROP_REFERENCES = 4; const SCALE_ADJUST_CROP_PADDING_RATIO = 0.18; const SCALE_ADJUST_CROP_MAX_EDGE = 1536; -const SCALE_ADJUST_LAYOUT_GUIDE_WASH_COLOR = "#f5f7fb"; const MIN_REGION_EDGE_PX = 24; const SCALE_ADJUST_OPERATION = "scale-adjust"; type LocalEditOperation = "local-edit" | typeof SCALE_ADJUST_OPERATION; @@ -834,35 +833,6 @@ function getScaleAdjustVisualGuidePlacement(params: { }; } -function buildScaleAdjustSoftWashSvg(params: { - boxes: Array<{ left: number; top: number; width: number; height: number }>; - height: number; - width: number; -}) { - const { boxes, height, width } = params; - const blur = Math.max(8, Math.min(42, Math.round(Math.min(width, height) * 0.025))); - const rects = boxes - .map((box) => { - const left = Math.max(0, box.left - blur); - const top = Math.max(0, box.top - blur); - const right = Math.min(width, box.left + box.width + blur); - const bottom = Math.min(height, box.top + box.height + blur); - return ``; - }) - .join(""); - - return Buffer.from( - ` - - - - - - ${rects} - ` - ); -} - function buildScaleAdjustPatchMaskSvg(params: { height: number; width: number }) { const { height, width } = params; const blur = Math.max(2, Math.min(18, Math.round(Math.min(width, height) * 0.055))); @@ -894,24 +864,15 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: { }) { const { regions, scalePercent, sourceBuffer, sourceHeight, sourceWidth } = params; - const washBoxes: Array<{ left: number; top: number; width: number; height: number }> = - []; const composites: Array<{ input: Buffer; left: number; top: number }> = []; for (const region of regions) { - const targetRect = getScaleAdjustTargetRect(region, scalePercent); - const workspaceBox = rectToPixels( - getScaleAdjustWorkspaceRect(region.rect, targetRect, scalePercent), - sourceWidth, - sourceHeight - ); const placement = getScaleAdjustVisualGuidePlacement({ height: sourceHeight, region, scalePercent, width: sourceWidth, }); - washBoxes.push(workspaceBox); const resizedCropBuffer = await sharp(sourceBuffer, { failOn: "none" }) .extract(placement.sourceBox) @@ -940,25 +901,8 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: { }); } - const guideComposites: Array<{ input: Buffer; left: number; top: number }> = [ - ...(washBoxes.length - ? [ - { - input: buildScaleAdjustSoftWashSvg({ - boxes: washBoxes, - height: sourceHeight, - width: sourceWidth, - }), - left: 0, - top: 0, - }, - ] - : []), - ...composites, - ]; - return sharp(sourceBuffer, { failOn: "none" }) - .composite(guideComposites) + .composite(composites) .png({ compressionLevel: 6 }) .toBuffer(); } @@ -1283,11 +1227,11 @@ function buildScaleAdjustPrompt( "这是一项局部主体比例 inpaint/edit 任务,不是整图缩放、整图裁切或重新构图任务。", sourceSizeLine, "输入图片1是原始干净图,也是最终输出必须继承的唯一画面基底。除目标主体尺寸变化与必要背景补全外,输入图片1中的文字、Logo、装饰、排版、背景、光线、构图边界和所有未选区域必须保持不变。", - "输入图片2是一张与输入图片1尺寸和画幅完全相同的完整画面粗略目标布局参考图,不是输出底图。它基于原图生成:旧主体占位被柔和淡化,只表示需要自然补背景;同一目标主体被粗略缩放并摆放到用户希望的最终位置和大小。请只读取其中目标主体的最终视觉占位和原画幅关系,不得学习、复刻或输出淡化区域、矩形裁切边缘、拼接痕迹或参考图粗糙画质。", + "输入图片2是一张与输入图片1尺寸和画幅完全相同的完整画面目标尺寸提示图,不是输出底图。它保留原图完整画幅作为稳定参考,并把源框里的同一目标主体粗略缩放后叠放到用户希望的最终位置和大小。请只读取目标主体的最终视觉占位和原画幅关系;输入图片2里仍可见的原尺寸主体/背景只用于锁定原画幅,不能表示最终还要保留原尺寸主体。不得学习、复刻或输出矩形裁切边缘、拼接痕迹或参考图粗糙画质。", cropLines ? `输入图片3及之后是框选区域的干净局部裁切参考图,作用是帮助你确认“要缩放的是哪个主体”。这些裁切图不是输出底图,不得被整体放大、贴回或覆盖到原图上。\n${cropLines}` : "", - "输入图片2中的完整画面目标布局是强约束:它展示目标主体调整后的大致视觉占位、中心位置,以及整张图不应缩放/裁切/平移的稳定画幅关系。最终输出必须回到输入图片1的真实照片环境中完成自然编辑,不得直接粘贴 input2 的矩形图块;主体不需要填满目标矩形,必须保持主体自身原始宽高比和透视形体。", + "输入图片2中的完整画面目标尺寸提示是强约束:它展示目标主体调整后的大致视觉占位、中心位置,以及整张图不应缩放/裁切/平移的稳定画幅关系。最终输出必须回到输入图片1的真实照片环境中完成自然编辑,不得直接粘贴 input2 的矩形图块;主体不需要填满目标矩形,必须保持主体自身原始宽高比和透视形体。", "如果系统或模型只能读取一张参考图,必须优先以输入图片1为输出基底,不得清理、改写或删除输入图片1中框选区域外的任何文字和图形。", "系统已根据用户框选区域计算出下方每个区域的原图坐标;框选区域只是定位目标主体的搜索范围,不是最终画面的裁切块、贴片、蒙版边界,也不是放大后的空间上限,更不是要整体缩放的矩形图层。", "最终结果必须保持原始干净图的画幅比例、构图边界、镜头透视、光线、色温、材质质感和清晰度;严禁整张图整体缩放、裁切、平移、重新排版或重新生成。", @@ -1372,7 +1316,7 @@ function buildScaleAdjustCoordinatePrompt( "中文硬约束:只调整 OLD 框中的主要家具主体。TARGET 框是最终占位;如果是缩小,OLD 框里超出 TARGET 的旧家具必须消失并补成自然背景;如果是放大,同一件家具必须明显变大。禁止整图缩放、整图裁切、整图重绘。", "一次成功硬约束:不要保守返回原尺寸。目标家具的可见轮廓必须明显从 OLD 原始占位变到 TARGET 占位;如果最终看起来和原图一样,就是失败。", `Return one seamless realistic photo with the same frame and pixel size as input image 1: ${sourceSize.width} x ${sourceSize.height} px.`, - "Input image 2 is a full-frame rough after-layout mockup with the same canvas as input image 1, not output content and not a photo layer. It keeps the original room frame locked, softly fades the OLD footprint to mean natural background healing, and places the same furniture at the required TARGET footprint. Read only the final furniture footprint, center, and locked frame relationship from input image 2; never copy faded areas, rectangular crop edges, rough patch quality, seams, or guide artifacts into the output.", + "Input image 2 is a full-frame target-size placement hint with the same canvas as input image 1, not output content and not a photo layer. It keeps the original room frame visible as the locked reference and overlays the same furniture at the required TARGET footprint. Read only the final furniture footprint, center, and locked frame relationship from input image 2. If the original-size furniture is still visible behind the rough hint, ignore that old footprint in the final; the final must contain only the TARGET-size furniture. Never copy rectangular crop edges, rough patch quality, seams, or guide artifacts into the output.", cropLines ? `${cropLines}` : "", "", "Task:", @@ -1388,8 +1332,8 @@ function buildScaleAdjustCoordinatePrompt( "Region requirements:", regionLines, "", - "Success check: compare the final furniture footprint with input image 1 and the full-frame target layout reference in input image 2. The selected furniture size must clearly change from the original and align much closer to TARGET than OLD, while the rest of the photo stays stable.", - "Do not paste rectangular image areas. Do not output layout-reference faded footprint, coordinate-map lines, labels, boxes, seams, hard patch edges, haze, color mismatch, full-canvas zoom, crop, pan, or redesign.", + "Success check: compare the final furniture footprint with input image 1 and the full-frame target-size placement hint in input image 2. The selected furniture size must clearly change from the original and align much closer to TARGET than OLD, while the rest of the photo stays stable.", + "Do not paste rectangular image areas. Do not output layout-reference overlays, coordinate-map lines, labels, boxes, seams, hard patch edges, haze, color mismatch, full-canvas zoom, crop, pan, or redesign.", "Return only the clean edited photo.", ].join("\n"); } @@ -1397,13 +1341,13 @@ function buildScaleAdjustCoordinatePrompt( function buildScaleAdjustCoordinateSystemPrompt() { return [ "You are an image editing model for local furniture scale adjustment.", - "Input image 1 is the only final photo base. Input image 2 is a full-frame rough after-layout mockup, not a photo layer.", + "Input image 1 is the only final photo base. Input image 2 is a full-frame target-size placement hint, not a photo layer.", "Follow both English and Chinese instructions. The Chinese lines are hard constraints for avoiding unchanged furniture and full-canvas zoom.", "Generate a clean realistic final photo by editing the target furniture itself and naturally rebuilding only the tiny surrounding background that becomes visible.", "Prefer a clear visible size change over a conservative unchanged result; unchanged furniture is a failed edit.", "When shrinking, remove the old full-size furniture footprint outside the target footprint and heal it as natural background. Large furniture is editable and must not remain locked at original size.", "Do not paste, composite, clone, crop, resize, or copy rectangular image patches from any input.", - "Never output guide artifacts from the layout reference: no faded old-footprint wash, rough crop rectangle, lines, labels, blocks, seams, rectangular borders, arrows, masks, UI marks, or annotations.", + "Never output guide artifacts from the layout reference: no overlay patch, rough crop rectangle, lines, labels, blocks, seams, rectangular borders, arrows, masks, UI marks, or annotations.", "Keep the camera frame, lighting, wall, floor, rug, windows, background furniture, text, logos, and all non-target regions stable.", ].join("\n"); } @@ -1627,7 +1571,7 @@ export async function POST(request: NextRequest) { ) : buildLocalEditPrompt(regions); const scaleAdjustCoordinateNegativePrompt = - "visible annotation, guide overlay, translucent overlay, faded old-footprint wash, text label, arrows, boxes, rectangle border, UI marks, pasted patch, rough layout patch, hard crop edge, seam, hard rectangular edge, color mismatch, different-resolution area, whole image zoom, whole image crop, whole image pan, whole image redesign, unchanged target furniture, stretched furniture, squashed furniture, duplicate furniture, second copy, extra legs, extra rails, side panels, shelves, cabinets, lattice panels"; + "visible annotation, guide overlay, translucent overlay, target placement overlay, text label, arrows, boxes, rectangle border, UI marks, pasted patch, rough layout patch, hard crop edge, seam, hard rectangular edge, color mismatch, different-resolution area, whole image zoom, whole image crop, whole image pan, whole image redesign, unchanged target furniture, stretched furniture, squashed furniture, duplicate furniture, second copy, extra legs, extra rails, side panels, shelves, cabinets, lattice panels"; const manualRegionReferenceImageUrls = regions.flatMap((region) => region.referenceImages.map((reference) => reference.url) ); @@ -1681,7 +1625,7 @@ export async function POST(request: NextRequest) { annotatedReferenceUrl: annotatedUpload.url, annotatedReferenceSentToModel: isScaleAdjustOperation, annotatedReferenceType: isScaleAdjustOperation - ? "scale-adjust-full-frame-rough-after-layout-guide" + ? "scale-adjust-full-frame-target-size-placement-hint" : "source-overlay", cleanSourceReferenceIndex: 1, configuredDetailModelId: configuredDetailModel || null, @@ -1725,8 +1669,8 @@ export async function POST(request: NextRequest) { .length, scaleAdjustPromptStrategy: isScaleAdjustOperation ? useScaleAdjustCoordinateGuide - ? "same-name-locked-source-full-frame-after-layout-guide-v33-one-shot" - : "locked-plate-full-frame-after-layout-guide-v10" + ? "same-name-locked-source-target-size-placement-hint-v34-one-shot" + : "locked-plate-target-size-placement-hint-v11" : undefined, scaleAdjustPostCompositeApplied: false, scaleAdjustUsesFullFrameOutput: isScaleAdjustOperation, @@ -1751,7 +1695,7 @@ export async function POST(request: NextRequest) { isScaleAdjustOperation ? useScaleAdjustCoordinateGuide ? scaleAdjustCoordinateNegativePrompt - : "彩色框、蓝色边框、绿色边框、红色边框、橙色边框、矩形框、箭头、连线、角点、编号圆点、提示词气泡、标注文字、UI 控件、半透明色块、蓝色色块、色条、灰底坐标图、淡化旧占位、选区边框、白边、画幅压扁、画面拉伸、整张图缩放、全图放大、全图裁切、全图平移、整体重绘、删除文字、清理文字、改写文字、文字模糊、Logo消失、主体拉伸、主体压扁、主体变窄、主体变高但变窄、横向挤压、纵向拉伸、透视变形、矩形贴片、硬边、拼接线、补丁感、局部裁切、色差边缘、背景断裂、背景错位、窗框错位、文字错位、地毯纹理错位、非主体背景重绘、主体未缩放、整块框选区域缩放" + : "彩色框、蓝色边框、绿色边框、红色边框、橙色边框、矩形框、箭头、连线、角点、编号圆点、提示词气泡、标注文字、UI 控件、半透明色块、蓝色色块、色条、灰底坐标图、目标占位叠图、选区边框、白边、画幅压扁、画面拉伸、整张图缩放、全图放大、全图裁切、全图平移、整体重绘、删除文字、清理文字、改写文字、文字模糊、Logo消失、主体拉伸、主体压扁、主体变窄、主体变高但变窄、横向挤压、纵向拉伸、透视变形、矩形贴片、硬边、拼接线、补丁感、局部裁切、色差边缘、背景断裂、背景错位、窗框错位、文字错位、地毯纹理错位、非主体背景重绘、主体未缩放、整块框选区域缩放" : "彩色框、蓝色边框、绿色边框、红色边框、橙色边框、矩形框、编号圆点、提示词气泡、标注文字、UI 控件、半透明色块、选区边框、白边、画幅压扁、画面拉伸、主体截断、动物截断、缺头、缺尾巴、缺四肢、贴边裁切、矩形贴片、硬边、拼接线、补丁感、色差边缘、背景错位、地面纹理断裂、椅脚错位", prompt, promptExtend: false,