diff --git a/app/api/canvas/local-edit/route.ts b/app/api/canvas/local-edit/route.ts index ba38c0556..38e377102 100644 --- a/app/api/canvas/local-edit/route.ts +++ b/app/api/canvas/local-edit/route.ts @@ -765,6 +765,7 @@ function buildScaleAdjustAnnotationSvg(params: { const labelPadY = Math.round(fontSize * 0.24); const targetColor = "#111827"; const sourceColor = "#9ca3af"; + const helperColor = "#4b5563"; const label = (text: string, x: number, y: number, color: string) => { const labelWidth = Math.round(text.length * fontSize * 0.62 + labelPadX * 2); const labelHeight = Math.round(fontSize * 1.2 + labelPadY * 2); @@ -783,12 +784,45 @@ function buildScaleAdjustAnnotationSvg(params: { const target = region.targetPixelRect || rectToPixels(targetRect, width, height); const radius = Math.max(8, Math.round(strokeWidth * 2)); + const sourceCenterX = source.left + source.width / 2; + const sourceCenterY = source.top + source.height / 2; + const targetCenterX = target.left + target.width / 2; + const targetCenterY = target.top + target.height / 2; + const widthFactor = source.width > 0 ? target.width / source.width : 1; + const heightFactor = source.height > 0 ? target.height / source.height : 1; + const isShrink = widthFactor < 0.92 || heightFactor < 0.92; + const isEnlarge = widthFactor > 1.08 || heightFactor > 1.08; + const actionLabel = isShrink + ? "SHRINK SUBJECT" + : isEnlarge + ? "ENLARGE SUBJECT" + : "MOVE SUBJECT"; + const scaleLabel = `${actionLabel} ${widthFactor.toFixed(2)}W ${heightFactor.toFixed(2)}H`; + const instructionLabel = isShrink + ? "HEAL OLD AREA OUTSIDE TARGET" + : isEnlarge + ? "FINAL SUBJECT MAY EXPAND HERE" + : "FINAL SUBJECT CENTER"; + const centerMoved = + Math.hypot(targetCenterX - sourceCenterX, targetCenterY - sourceCenterY) > + Math.max(8, minSide * 0.012); return ` + + + + + ${ + centerMoved + ? `` + : "" + } ${label(`OLD ${region.index}`, source.left + strokeWidth, source.top + strokeWidth, sourceColor)} ${label(`TARGET ${region.index}`, target.left + strokeWidth, target.top + strokeWidth, targetColor)} + ${label(scaleLabel, target.left + strokeWidth, target.top + target.height + strokeWidth * 2, helperColor)} + ${label(instructionLabel, source.left + strokeWidth, source.top + source.height + strokeWidth * 2, helperColor)} `; }) .join(""); @@ -796,6 +830,11 @@ function buildScaleAdjustAnnotationSvg(params: { return Buffer.from(` + + + + + ${regionSvg} @@ -953,18 +992,25 @@ function buildFalScaleAdjustCoordinatePrompt( region.rect.width > 0 ? targetRect.width / region.rect.width : 1; const heightFactor = region.rect.height > 0 ? targetRect.height / region.rect.height : 1; + const isShrink = widthFactor < 0.92 || heightFactor < 0.92; + const isEnlarge = widthFactor > 1.08 || heightFactor > 1.08; const scaleDirection = - widthFactor < 0.92 || heightFactor < 0.92 + isShrink ? "make the furniture clearly smaller" - : widthFactor > 1.08 || heightFactor > 1.08 + : isEnlarge ? "make the furniture clearly larger" : "move/fit the furniture to the target box"; + const footprintRule = isShrink + ? "After shrinking, the original OLD footprint outside the TARGET box must be cleared and rebuilt as natural background/floor/rug/wall; the furniture must not still touch the OLD box edges." + : isEnlarge + ? "After enlarging, the same furniture should visibly occupy more of the TARGET footprint, with natural shadows and occlusion; do not just move it or stretch one axis." + : "Keep the subject centered on the TARGET box without changing the full canvas."; const cropNote = cropIndex >= 0 - ? ` Input image ${cropIndex + 3} is a clean crop reference for this same furniture identity; use it only to recognize the subject, never paste it.` + ? ` Input image ${cropIndex + 3} is a clean crop reference for this same furniture identity and silhouette; use it only to recognize the subject, never paste it, and do not preserve its old size.` : ""; - return `Region ${region.index}: OLD search box in input 1 x=${region.pixelRect.left}, y=${region.pixelRect.top}, w=${region.pixelRect.width}, h=${region.pixelRect.height}; TARGET final visual box x=${targetPixelRect.left}, y=${targetPixelRect.top}, w=${targetPixelRect.width}, h=${targetPixelRect.height}; target scale is ${widthFactor.toFixed(2)}x width and ${heightFactor.toFixed(2)}x height. Main action: ${scaleDirection}.${cropNote}`; + return `Region ${region.index}: OLD search box in input 1 x=${region.pixelRect.left}, y=${region.pixelRect.top}, w=${region.pixelRect.width}, h=${region.pixelRect.height}; TARGET final visual footprint x=${targetPixelRect.left}, y=${targetPixelRect.top}, w=${targetPixelRect.width}, h=${targetPixelRect.height}; target scale is ${widthFactor.toFixed(2)}x width and ${heightFactor.toFixed(2)}x height. Main action: ${scaleDirection}. ${footprintRule}${cropNote}`; }) .join("\n"); const cropLines = cropReferences @@ -977,20 +1023,21 @@ function buildFalScaleAdjustCoordinatePrompt( return [ "Local furniture scale edit. Use input image 1 as the clean original photo and the locked camera frame.", `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 separate neutral coordinate map, not a photo and not output content. The light gray OLD rectangle/label locates the current furniture in input image 1; the dark TARGET rectangle/label gives the final size and center.", + "Input image 2 is a separate neutral coordinate instruction map, not a photo and not output content. The light gray OLD rectangle/label locates the current furniture in input image 1; the dark TARGET rectangle/label gives the required final size and center. The map may include arrows, scale-factor text, HEAL OLD AREA, and FINAL SUBJECT AREA labels: read them as instructions only, never as visual content.", cropLines ? `${cropLines}` : "", "", "Task:", "Find the main movable furniture subject inside each OLD box in input image 1. Resize and, if needed, move that subject only so its final visible footprint matches the TARGET box.", "If an OLD box contains more than one object, choose the dominant furniture/product inside that box, usually the largest or most central movable item. Keep adjacent lamps, plants, side tables, pillows, wall decor, windows, text, and background objects at their original size unless they are clearly part of the same furniture subject.", - "Keep the furniture identity, material, perspective, objects on top, and natural aspect ratio from input image 1. Do not create a second copy or extra parts.", - "If the target is smaller, the furniture must visibly become smaller and the newly exposed floor, rug, wall, shadows, and background must be naturally healed. If the target is larger, enlarge the same furniture naturally without stretching it.", + "Keep the furniture identity, material, perspective, objects on top, and natural aspect ratio from input image 1. Do not create a second copy or extra parts. Do not preserve the original furniture size just because the scene looks realistic; the user explicitly wants a visible scale change.", + "For beds, sofas, tables, chairs, cabinets, benches, and other large furniture, still perform the scale edit on that furniture. Do not treat large furniture as locked architecture.", + "If the target is smaller, the furniture must visibly become smaller and be mostly contained by the TARGET footprint. The OLD area outside the TARGET footprint must no longer contain the full-size furniture; naturally heal newly exposed floor, rug, wall, bedding, shadows, and background. If the target is larger, enlarge the same furniture naturally without stretching it.", "Never keep the selected furniture at its original footprint. If the edited furniture still fills the OLD rectangle instead of the TARGET rectangle, the result is invalid.", "", "Region requirements:", regionLines, "", - "Success check: the selected furniture size must clearly change from the original, while the rest of the photo stays stable.", + "Success check: compare the final furniture footprint with the OLD and TARGET boxes from 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 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"); @@ -1002,6 +1049,7 @@ function buildFalScaleAdjustSystemPrompt() { "Input image 1 is the only final photo base. Input image 2 is a coordinate instruction map, not a photo layer.", "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 coordinate reference: no 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.", @@ -1301,7 +1349,7 @@ export async function POST(request: NextRequest) { .length, scaleAdjustPromptStrategy: isScaleAdjustOperation ? useFalScaleAdjustCoordinateGuide - ? "fal-locked-source-neutral-coordinate-map-v26" + ? "fal-locked-source-neutral-coordinate-map-v27-action-heal-guide" : "locked-plate-neutral-coordinate-guide-map-pixel-anchors-v8" : undefined, scaleAdjustPostCompositeApplied: false,