From d69346de429a994f2d368f899542b34d4c9b6697 Mon Sep 17 00:00:00 2001 From: 134 Date: Wed, 8 Jul 2026 01:38:51 +0800 Subject: [PATCH] fix: keep product out of scale repair crop prompt --- app/api/canvas/local-edit/route.ts | 80 ++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/app/api/canvas/local-edit/route.ts b/app/api/canvas/local-edit/route.ts index 64ad2f432..9d2755bfa 100644 --- a/app/api/canvas/local-edit/route.ts +++ b/app/api/canvas/local-edit/route.ts @@ -124,6 +124,7 @@ type ScaleAdjustCropReference = { url: string; }; type ScaleAdjustPatchReference = { + backgroundPlateCropUrl?: string | null; backgroundPlateFullUrl?: string | null; compositeClipMaskUrl?: string | null; repairBlendMaskUrl?: string | null; @@ -4024,6 +4025,7 @@ async function createScaleAdjustPatchReference(params: { ]); return { + backgroundPlateCropUrl: null, backgroundPlateFullUrl: backgroundPlateFullUpload?.url || null, compositeClipMaskUrl: compositeClipMaskUpload?.url || null, repairBlendMaskUrl: repairBlendMaskUpload?.url || null, @@ -4051,6 +4053,12 @@ async function createScaleAdjustPatchReference(params: { .png({ compressionLevel: 6 }) .toBuffer(), ]); + const backgroundPlateCropBuffer = targetDraftReference.backgroundPlateBuffer + ? await sharp(targetDraftReference.backgroundPlateBuffer, { failOn: "none" }) + .extract(workspacePixelRect) + .png({ compressionLevel: 6 }) + .toBuffer() + : null; const [sourceCropUpload, targetDraftUpload, targetDraftFullUpload] = await Promise.all([ uploadScaleAdjustPrepAsset( @@ -4068,6 +4076,7 @@ async function createScaleAdjustPatchReference(params: { ]); const [ backgroundPlateFullUpload, + backgroundPlateCropUpload, productLockOverlayUpload, productLockProtectMaskUpload, compositeClipMaskUpload, @@ -4079,6 +4088,10 @@ async function createScaleAdjustPatchReference(params: { `canvas-scale-adjust-background-plate-${suffix}.jpg`, backgroundPlateUploadBuffer ), + uploadOptionalScaleAdjustAsset( + `canvas-scale-adjust-background-plate-crop-${suffix}.png`, + backgroundPlateCropBuffer + ), uploadOptionalScaleAdjustAsset( `canvas-scale-adjust-product-lock-overlay-${suffix}.png`, targetDraftReference.productLockOverlayBuffer @@ -4112,6 +4125,7 @@ async function createScaleAdjustPatchReference(params: { } return { + backgroundPlateCropUrl: backgroundPlateCropUpload?.url || null, backgroundPlateFullUrl: backgroundPlateFullUpload?.url || null, compositeClipMaskUrl: compositeClipMaskUpload?.url || null, repairBlendMaskUrl: repairBlendMaskUpload?.url || null, @@ -5148,6 +5162,54 @@ function buildScaleAdjustProductLockBackgroundPrompt( ].join("\n"); } +function buildScaleAdjustProductLockBackgroundCropPrompt( + regions: NormalizedLocalEditRegion[], + scalePercent: ScaleAdjustPercent | null | undefined, + patchReference: ScaleAdjustPatchReference, + sourceSize: { width: number; height: number } +) { + const workspace = patchReference.workspacePixelRect; + const regionLines = regions + .map((region) => { + const targetRect = getScaleAdjustTargetRect(region, scalePercent); + const targetPixelRect = + region.targetPixelRect || + rectToPixels(targetRect, sourceSize.width, sourceSize.height); + const sourceInCrop = { + left: Math.max(0, region.pixelRect.left - workspace.left), + top: Math.max(0, region.pixelRect.top - workspace.top), + width: region.pixelRect.width, + height: region.pixelRect.height, + }; + const targetInCrop = { + left: Math.max(0, targetPixelRect.left - workspace.left), + top: Math.max(0, targetPixelRect.top - workspace.top), + width: targetPixelRect.width, + height: targetPixelRect.height, + }; + return `Region ${region.index}: in this crop, erase the old selected furniture/product footprint around x=${sourceInCrop.left}, y=${sourceInCrop.top}, w=${sourceInCrop.width}, h=${sourceInCrop.height}. Keep the future target footprint x=${targetInCrop.left}, y=${targetInCrop.top}, w=${targetInCrop.width}, h=${targetInCrop.height} empty and clean; the locked product pixels will be composited there after generation. User request: ${region.prompt}.`; + }) + .join("\n"); + + return [ + "Cropped background repair for product-locked scale adjustment.", + `Return exactly one clean realistic crop with the same framing, aspect ratio, and pixel extent as input image 1. This crop maps to full-photo pixels left=${workspace.left}, top=${workspace.top}, width=${workspace.width}, height=${workspace.height}.`, + "Input image 1 is the original crop from the source photo. Input image 2 is a rough empty-background repair guide crop where the selected furniture/product footprint has been wiped.", + "Important: do not draw, recreate, move, scale, duplicate, or redesign the selected furniture/product in your output. The system will composite the locked original product pixels into the target footprint after your crop is returned.", + "Your only job is to repair the old footprint into natural wall, curtain, floor, rug, shadow, sofa edge, and background, while keeping non-target people and non-target objects stable.", + "If input image 2 contains haze, gray wash, ghost silhouettes, crop edges, black holes, rough mask shapes, or pasted fragments, treat them as repair hints only. Do not preserve them as visible content.", + "Preserve any non-target person exactly: do not change face, body, hands, legs, clothes, pose, hair, or book. Do not cover the person with furniture.", + "Keep the target footprint empty and natural; do not place the selected furniture/product there.", + "Do not output boxes, masks, labels, coordinate marks, UI marks, arrows, guide overlays, text, seams, rectangular patch edges, haze blocks, ghost copies, or old-size remnants.", + "中文硬约束:这是产品结构锁定的局部背景修补。你只输出干净背景裁切,不要生成目标家具/商品;旧位置的目标家具/商品必须擦除并补成自然墙面、窗帘、地面、阴影和背景。目标家具/商品会在你输出后由系统用原图锁定像素叠回。最终不能有旧尺寸残影、矩形雾块、贴片边、黑洞、标注框,也不能改变人物。", + "", + "Region instructions:", + regionLines, + "", + "Success check before returning: the selected furniture/product is absent from the crop, the old footprint is naturally repaired, the future target footprint is clean, and the crop looks like one natural photograph.", + ].join("\n"); +} + export async function POST(request: NextRequest) { try { const session = await auth(); @@ -5506,6 +5568,10 @@ export async function POST(request: NextRequest) { const useScaleAdjustProductLockMaskEdit = useScaleAdjustProductLockComposite && canUseScaleAdjustProductLockMaskEdit; + const useScaleAdjustProductLockCropBackground = + useScaleAdjustProductLockComposite && + !useScaleAdjustFullFrameStructurePatch && + Boolean(scaleAdjustPatchReference?.backgroundPlateCropUrl); const useScaleAdjustPatchTargetDraftPrimary = Boolean(scaleAdjustPatchReference); const includeScaleAdjustPatchSourceReference = Boolean(scaleAdjustPatchReference); @@ -5531,6 +5597,13 @@ export async function POST(request: NextRequest) { scalePercent, { width: sourceWidth, height: sourceHeight } ) + : useScaleAdjustProductLockCropBackground + ? buildScaleAdjustProductLockBackgroundCropPrompt( + regions, + scalePercent, + scaleAdjustPatchReference, + { width: sourceWidth, height: sourceHeight } + ) : buildScaleAdjustPatchPrompt( regions, scalePercent, @@ -5613,6 +5686,13 @@ export async function POST(request: NextRequest) { ...scaleAdjustCropReferenceImageUrls, ...manualRegionReferenceImageUrls, ] + : useScaleAdjustProductLockCropBackground + ? [ + scaleAdjustPatchReference.sourceCropUrl, + scaleAdjustPatchReference.backgroundPlateCropUrl as string, + ...scaleAdjustCropReferenceImageUrls, + ...manualRegionReferenceImageUrls, + ] : useScaleAdjustPatchTargetDraftPrimary ? [ scaleAdjustPatchReference.targetDraftUrl,