diff --git a/app/api/canvas/local-edit/route.ts b/app/api/canvas/local-edit/route.ts index 089f3da70..2f3a56f2a 100644 --- a/app/api/canvas/local-edit/route.ts +++ b/app/api/canvas/local-edit/route.ts @@ -1132,6 +1132,10 @@ function buildScaleAdjustCoordinatePrompt( region.rect.width > 0 ? targetRect.width / region.rect.width : 1; const heightFactor = region.rect.height > 0 ? targetRect.height / region.rect.height : 1; + const sourceRight = region.pixelRect.left + region.pixelRect.width; + const sourceBottom = region.pixelRect.top + region.pixelRect.height; + const targetRight = targetPixelRect.left + targetPixelRect.width; + const targetBottom = targetPixelRect.top + targetPixelRect.height; const isShrink = widthFactor < 0.92 || heightFactor < 0.92; const isEnlarge = widthFactor > 1.08 || heightFactor > 1.08; const scaleDirection = @@ -1150,7 +1154,7 @@ function buildScaleAdjustCoordinatePrompt( cropIndex >= 0 ? ` 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 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}`; + return `Region ${region.index}: OLD search box in input 1 left=${region.pixelRect.left}, top=${region.pixelRect.top}, right=${sourceRight}, bottom=${sourceBottom}, width=${region.pixelRect.width}, height=${region.pixelRect.height}; TARGET final visual footprint left=${targetPixelRect.left}, top=${targetPixelRect.top}, right=${targetRight}, bottom=${targetBottom}, width=${targetPixelRect.width}, height=${targetPixelRect.height}; target scale is ${widthFactor.toFixed(2)}x width and ${heightFactor.toFixed(2)}x height. Main action: ${scaleDirection}. The final furniture silhouette must clearly leave the OLD footprint edges and align much closer to TARGET than OLD; returning an almost unchanged furniture footprint is invalid. ${footprintRule}${cropNote}`; }) .join("\n"); const cropLines = cropReferences @@ -1162,7 +1166,9 @@ function buildScaleAdjustCoordinatePrompt( return [ "Local furniture scale edit. Use input image 1 as the clean original photo and the locked camera frame.", + "One-shot cost constraint: solve this in the first generation. Do not rely on later retries or alternate candidates; an unchanged result wastes the request and is invalid.", "中文硬约束:只调整 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 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, OLD OUTSIDE = BACKGROUND, and TARGET = FINAL FOOTPRINT labels: read them as instructions only, never as visual content.", cropLines ? `${cropLines}` : "", @@ -1186,55 +1192,6 @@ function buildScaleAdjustCoordinatePrompt( ].join("\n"); } -function buildScaleAdjustCoordinateRetryPrompt(params: { - basePrompt: string; - regions: NormalizedLocalEditRegion[]; - scalePercent: ScaleAdjustPercent | null | undefined; - sourceSize: { width: number; height: number }; -}) { - const forceLines = params.regions - .map((region) => { - const targetRect = getScaleAdjustTargetRect(region, params.scalePercent); - const targetPixelRect = - region.targetPixelRect || - rectToPixels(targetRect, params.sourceSize.width, params.sourceSize.height); - const widthFactor = - region.rect.width > 0 ? targetRect.width / region.rect.width : 1; - const heightFactor = - region.rect.height > 0 ? targetRect.height / region.rect.height : 1; - const sourceRight = region.pixelRect.left + region.pixelRect.width; - const sourceBottom = region.pixelRect.top + region.pixelRect.height; - const targetRight = targetPixelRect.left + targetPixelRect.width; - const targetBottom = targetPixelRect.top + targetPixelRect.height; - const direction = - widthFactor < 0.92 || heightFactor < 0.92 - ? "缩小" - : widthFactor > 1.08 || heightFactor > 1.08 - ? "放大" - : "移动/贴合目标框"; - return [ - `区域 ${region.index} 强制重试目标:${direction}目标家具。`, - `OLD 源框 px: left=${region.pixelRect.left}, top=${region.pixelRect.top}, right=${sourceRight}, bottom=${sourceBottom}, width=${region.pixelRect.width}, height=${region.pixelRect.height}。`, - `TARGET 最终占位 px: left=${targetPixelRect.left}, top=${targetPixelRect.top}, right=${targetRight}, bottom=${targetBottom}, width=${targetPixelRect.width}, height=${targetPixelRect.height}。`, - `目标家具最终视觉尺寸必须接近 OLD 的 ${widthFactor.toFixed(2)}x 宽、${heightFactor.toFixed(2)}x 高;必须让轮廓明显离开 OLD 原边缘,并更接近 TARGET 边缘。`, - ].join(" "); - }) - .join("\n"); - - return [ - "IMPORTANT RETRY / 重要重试:上一张候选图被系统判定为 no-effective-scale-change,也就是目标家具几乎没有发生有效比例变化。", - "这次不要保守保持原尺寸。必须执行可见的主体比例调整;如果最终看起来像原图,只是局部轻微修补,就是失败。", - "Do not return an unchanged image. Do not keep the furniture in the OLD footprint. The furniture silhouette must move/resize clearly toward TARGET while the camera frame and non-target room stay locked.", - "缩小时:OLD 中超出 TARGET 的旧家具外轮廓必须消失,并补成自然背景/地面/地毯/墙面/阴影;不要让旧尺寸家具仍然贴着 OLD 源框边缘。", - "放大时:同一件家具必须明显占据更大的 TARGET 占位,但保持原透视、材质和宽高比,不要只拉伸单轴,也不要整图放大。", - "", - forceLines, - "", - "下面是完整原始任务,请在遵守以上强制重试目标的前提下执行:", - params.basePrompt, - ].join("\n"); -} - function buildScaleAdjustCoordinateSystemPrompt() { return [ "You are an image editing model for local furniture scale adjustment.", @@ -1466,15 +1423,6 @@ export async function POST(request: NextRequest) { { width: sourceWidth, height: sourceHeight } ) : buildLocalEditPrompt(regions); - const scaleAdjustCoordinateRetryPrompt = - isScaleAdjustOperation && useScaleAdjustCoordinateGuide - ? buildScaleAdjustCoordinateRetryPrompt({ - basePrompt: prompt, - regions, - scalePercent, - sourceSize: { width: sourceWidth, height: sourceHeight }, - }) - : ""; const scaleAdjustCoordinateNegativePrompt = "visible annotation, guide overlay, translucent overlay, text label, arrows, boxes, rectangle border, UI marks, pasted patch, 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) => @@ -1572,13 +1520,9 @@ export async function POST(request: NextRequest) { .length, scaleAdjustPromptStrategy: isScaleAdjustOperation ? useScaleAdjustCoordinateGuide - ? "same-name-locked-source-neutral-coordinate-map-v30-bilingual-fal-last" + ? "same-name-locked-source-neutral-coordinate-map-v31-one-shot" : "locked-plate-neutral-coordinate-guide-map-pixel-anchors-v8" : undefined, - scaleAdjustRetryPromptStrategy: - isScaleAdjustOperation && scaleAdjustCoordinateRetryPrompt - ? "force-visible-scale-change-on-guard-reject-v1" - : undefined, scaleAdjustPostCompositeApplied: false, scaleAdjustUsesFullFrameOutput: isScaleAdjustOperation, sourceHeight, @@ -1607,19 +1551,6 @@ export async function POST(request: NextRequest) { prompt, promptExtend: false, referenceImages, - retryVariants: scaleAdjustCoordinateRetryPrompt - ? [ - { - debugMeta: { - scaleAdjustRetryPromptStrategy: - "force-visible-scale-change-on-guard-reject-v1", - }, - label: "force-visible-scale-change", - prompt: scaleAdjustCoordinateRetryPrompt, - retryOnScaleAdjustRejectionOnly: true, - }, - ] - : undefined, scaleAdjustVerification: scaleAdjustVerificationConfig, localEditComposite: isScaleAdjustOperation ? undefined diff --git a/lib/canvas/generate-image.ts b/lib/canvas/generate-image.ts index 706900f06..1e4ffb1b1 100644 --- a/lib/canvas/generate-image.ts +++ b/lib/canvas/generate-image.ts @@ -76,7 +76,6 @@ export interface CanvasGenerateImageRetryVariant { promptExtend?: boolean; referenceImage?: string; referenceImages?: string[]; - retryOnScaleAdjustRejectionOnly?: boolean; scaleAdjustVerification?: CanvasScaleAdjustVerificationConfig; } @@ -2544,28 +2543,15 @@ export async function generateCanvasImage( const retryVariants = Array.isArray(request.retryVariants) ? request.retryVariants : []; - const scaleAdjustRejection = - caughtError instanceof ScaleAdjustOutputRejectedError ? caughtError : null; - const scaleAdjustRejected = Boolean(scaleAdjustRejection); const nextRetryVariantIndex = typeof request.internalRetryVariantIndex === "number" ? request.internalRetryVariantIndex + 1 : 0; - let retryVariantIndex = nextRetryVariantIndex; - let retryVariant = retryVariants[retryVariantIndex]; - while ( - retryVariant && - retryVariant.retryOnScaleAdjustRejectionOnly && - !scaleAdjustRejected - ) { - retryVariantIndex += 1; - retryVariant = retryVariants[retryVariantIndex]; - } + const retryVariant = retryVariants[nextRetryVariantIndex]; if (retryVariant) { const { debugMeta: retryVariantDebugMeta, label: retryVariantLabel, - retryOnScaleAdjustRejectionOnly, ...retryOverrides } = retryVariant; const retryDebugMeta = { @@ -2573,14 +2559,13 @@ export async function generateCanvasImage( ...(retryVariantDebugMeta || {}), retryFromError: caughtError instanceof Error ? caughtError.message : String(caughtError), - retryScaleAdjustRejected: scaleAdjustRejected, - retryVariantIndex, - retryVariantLabel: retryVariantLabel || `variant-${retryVariantIndex + 1}`, + retryVariantIndex: nextRetryVariantIndex, + retryVariantLabel: retryVariantLabel || `variant-${nextRetryVariantIndex + 1}`, }; console.warn("[Canvas Generate Image] Retrying with prompt/reference variant:", { ...retryDebugMeta, debugLabel, - retryVariantIndex, + retryVariantIndex: nextRetryVariantIndex, retryVariantLabel: retryVariantLabel || null, selectedModelRef: selectedModel.model.id, selectedModelId: selectedModel.model.modelId, @@ -2591,11 +2576,15 @@ export async function generateCanvasImage( ...request, ...retryOverrides, debugMeta: retryDebugMeta, - internalRetryVariantIndex: retryVariantIndex, + internalRetryVariantIndex: nextRetryVariantIndex, retryVariants, }); } + const scaleAdjustRejection = + caughtError instanceof ScaleAdjustOutputRejectedError ? caughtError : null; + const scaleAdjustRejected = Boolean(scaleAdjustRejection); + if (scaleAdjustRejected) { console.info("[Canvas Generate Image] Skip model health failure for scale adjust rejection:", { debugLabel,