From 2a8b303508463308f12c75fdd757203f63ef1b92 Mon Sep 17 00:00:00 2001 From: 134 Date: Sat, 4 Jul 2026 13:23:05 +0800 Subject: [PATCH] fix: route scale shrink through fal edit fallback --- app/api/canvas/local-edit/route.ts | 93 +++++++++++++++++++++++++----- 1 file changed, 79 insertions(+), 14 deletions(-) diff --git a/app/api/canvas/local-edit/route.ts b/app/api/canvas/local-edit/route.ts index b18316821..004fa0011 100644 --- a/app/api/canvas/local-edit/route.ts +++ b/app/api/canvas/local-edit/route.ts @@ -326,6 +326,28 @@ function isScaleAdjustCoordinateGuideModel( return getScaleAdjustModelFamily(entry) === "nano-banana-2"; } +function isScaleAdjustFalNanoBananaEditModel( + entry: AiModelWithProviderForSelection | null | undefined +) { + if ( + !entry || + entry.provider.type !== "fal-image" || + getScaleAdjustModelFamily(entry) !== "nano-banana-2" + ) { + return false; + } + const label = `${entry.model.displayName || ""} ${entry.model.modelId || ""}` + .toLowerCase() + .replace(/\s+/g, " "); + return label.includes("/edit") || /\bedit\b/.test(label); +} + +function isScaleAdjustHealthyModel( + entry: AiModelWithProviderForSelection | null | undefined +) { + return !entry?.model?.healthStatus || entry.model.healthStatus === "healthy"; +} + function getScaleAdjustProviderPriority(providerType: string) { switch (providerType) { case "ruxa-image": @@ -399,6 +421,7 @@ function compareScaleAdjustProviderCandidates( function resolveScaleAdjustPreferredModelId(params: { configuredModelId: string; honorRequestedModelBeforeGpt?: boolean; + preferFalNanoBananaEdit?: boolean; preferMaskCapableGpt?: boolean; preferProviderTypes?: string[]; requestedModelId: string; @@ -433,19 +456,36 @@ function resolveScaleAdjustPreferredModelId(params: { if (params.preferMaskCapableGpt) { const maskCapableGptImage2 = gptImage2Candidates .filter(isScaleAdjustMaskCapableModel) - .filter((entry) => entry.model.healthStatus === "healthy"); + .filter(isScaleAdjustHealthyModel); if (maskCapableGptImage2[0]) { return maskCapableGptImage2[0].model.id; } } for (const providerType of params.preferProviderTypes || []) { const providerCandidate = gptImage2Candidates.find( - (entry) => entry.provider.type === providerType + (entry) => + entry.provider.type === providerType && isScaleAdjustHealthyModel(entry) ); if (providerCandidate) { return providerCandidate.model.id; } } + if (params.preferFalNanoBananaEdit) { + const falEditCandidate = params.models + .filter((entry) => isScaleAdjustUsableImageEditModel(entry)) + .filter(isScaleAdjustFalNanoBananaEditModel) + .filter(isScaleAdjustHealthyModel) + .sort((left, right) => + compareScaleAdjustProviderCandidates( + left, + right, + params.requestedModelId || params.configuredModelId + ) + )[0]; + if (falEditCandidate) { + return falEditCandidate.model.id; + } + } if (gptImage2Candidates[0]) { return gptImage2Candidates[0].model.id; } @@ -1426,7 +1466,6 @@ function buildScaleAdjustStructureInitialMask(params: { for (let localY = 0; localY < box.height; localY += 1) { const y = box.top + localY; for (let localX = 0; localX < box.width; localX += 1) { - if (localX >= box.width * 0.9) continue; const x = box.left + localX; const rawIndex = getScaleAdjustRawPixelIndex(width, x, y); const r = raw[rawIndex] || 0; @@ -2311,8 +2350,10 @@ async function buildScaleAdjustStructureLockedComposite(params: { } = params; // The current RMBG provider can bake horizontal matte stripes into furniture cutouts. const allowRmbgStructureLock = - process.env.CANVAS_SCALE_ADJUST_ENABLE_RMBG_STRUCTURE === "1"; - const rmbgComposite = includePersonPixels || !allowRmbgStructureLock + process.env.CANVAS_SCALE_ADJUST_ENABLE_RMBG_STRUCTURE === "1" || + (includePersonPixels && + process.env.CANVAS_SCALE_ADJUST_DISABLE_PERSON_RMBG !== "1"); + const rmbgComposite = !allowRmbgStructureLock ? null : await buildScaleAdjustRmbgStructureLockedComposite({ placement, @@ -2323,6 +2364,7 @@ async function buildScaleAdjustStructureLockedComposite(params: { userId, }); if (rmbgComposite) return rmbgComposite; + if (includePersonPixels) return null; const initialMask = buildScaleAdjustStructureInitialMask({ box: placement.sourceBox, @@ -2535,6 +2577,7 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: { sourceHeight: number; sourceImageUrl?: string; sourceWidth: number; + structureLockAppliedRef?: { value: boolean }; structureLockedDraft?: boolean; userId?: string; }) { @@ -2545,6 +2588,7 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: { sourceHeight, sourceImageUrl, sourceWidth, + structureLockAppliedRef, structureLockedDraft = false, userId, } = params; @@ -2610,6 +2654,9 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: { : null; if (structureLockedComposite) { + if (structureLockAppliedRef) { + structureLockAppliedRef.value = true; + } subjectWashComposites.push(structureLockedComposite.wash); composites.push(structureLockedComposite.overlay); continue; @@ -2736,9 +2783,11 @@ async function createScaleAdjustPatchReference(params: { } = params; const workspaceRect = getScaleAdjustPatchWorkspaceRect(regions, scalePercent); const workspacePixelRect = rectToPixels(workspaceRect, sourceWidth, sourceHeight); - // Pixel-mask structure drafts are useful for enlargement, but in shrink - // cases they can turn dense interior details into black mask blocks. - const structureLockedDraft = hasScaleAdjustEnlargeTarget(regions, scalePercent); + const shouldUseStructureLockedDraft = hasScaleAdjustStructureTarget( + regions, + scalePercent + ); + const structureLockAppliedRef = { value: false }; const targetDraftBuffer = await buildScaleAdjustVisualLayoutGuideBuffer({ regions, scalePercent, @@ -2746,7 +2795,8 @@ async function createScaleAdjustPatchReference(params: { sourceHeight, sourceImageUrl, sourceWidth, - structureLockedDraft, + structureLockAppliedRef, + structureLockedDraft: shouldUseStructureLockedDraft, userId, }); const [sourceCropBuffer, targetDraftCropBuffer] = await Promise.all([ @@ -2773,7 +2823,7 @@ async function createScaleAdjustPatchReference(params: { return { sourceCropUrl: sourceCropUpload.url, - structureLockedDraft, + structureLockedDraft: structureLockAppliedRef.value, targetDraftUrl: targetDraftUpload.url, workspacePixelRect, workspaceRect, @@ -3463,7 +3513,7 @@ function buildScaleAdjustCoordinateSystemPrompt() { function buildScaleAdjustTextOnlyPrompt( regions: NormalizedLocalEditRegion[], scalePercent: ScaleAdjustPercent | null | undefined, - cropReferences: ScaleAdjustCropReference[] = [], + cropReferences: ScaleAdjustCropReference[], sourceSize: { width: number; height: number } ) { const cropLines = cropReferences @@ -3780,6 +3830,8 @@ export async function POST(request: NextRequest) { : false; const preferScaleAdjustMaskCapableModel = hasScaleAdjustShrink && !hasScaleAdjustEnlarge; + const preferScaleAdjustFalNanoBananaEdit = + hasScaleAdjustShrink && !hasScaleAdjustEnlarge; const preferScaleAdjustProviderTypes = hasScaleAdjustShrink && !hasScaleAdjustEnlarge ? ["grsai-gpt-image"] : []; const displayPrompt = buildLocalEditDisplayPrompt({ operation, scalePercent }); @@ -3800,6 +3852,7 @@ export async function POST(request: NextRequest) { ? resolveScaleAdjustPreferredModelId({ configuredModelId: configuredDetailModel, honorRequestedModelBeforeGpt: hasScaleAdjustEnlarge, + preferFalNanoBananaEdit: preferScaleAdjustFalNanoBananaEdit, preferMaskCapableGpt: preferScaleAdjustMaskCapableModel, preferProviderTypes: preferScaleAdjustProviderTypes, requestedModelId: requestedModel || "", @@ -3840,8 +3893,12 @@ export async function POST(request: NextRequest) { !useProviderTextOnlyCoordinatePrompt; const useScaleAdjustMarkedPhotoGuide = useScaleAdjustCoordinatePrompt && !useScaleAdjustVisualLayoutGuide; + const useScaleAdjustFullFrameFalEdit = + isScaleAdjustOperation && + isScaleAdjustFalNanoBananaEditModel(resolvedScaleAdjustModelEntry); const useScaleAdjustStructureLockedPatchWorkflow = isScaleAdjustOperation && + !useScaleAdjustFullFrameFalEdit && hasScaleAdjustStructureTarget(regions, scalePercent) && (!preferScaleAdjustMaskCapableModel || !isScaleAdjustMaskCapableModel(resolvedScaleAdjustModelEntry)); @@ -3851,6 +3908,7 @@ export async function POST(request: NextRequest) { isScaleAdjustMaskCapableModel(resolvedScaleAdjustModelEntry); const useScaleAdjustPatchWorkflow = isScaleAdjustOperation && + !useScaleAdjustFullFrameFalEdit && (useScaleAdjustStructureLockedPatchWorkflow || !useScaleAdjustEditMask); const sendScaleAdjustAnnotatedReferenceToModel = isScaleAdjustOperation && @@ -4050,7 +4108,9 @@ export async function POST(request: NextRequest) { : []; const scaleAdjustProtectedRegions = isScaleAdjustOperation && scaleAdjustPatchReference - ? scaleAdjustProtectedChangeRegions + ? scaleAdjustPatchReference.structureLockedDraft + ? [scaleAdjustPatchReference.workspaceRect] + : scaleAdjustProtectedChangeRegions : scaleAdjustCompositeRegions; const scaleAdjustVerificationConfig = isScaleAdjustOperation ? { @@ -4137,6 +4197,7 @@ export async function POST(request: NextRequest) { scaleAdjustPatchWorkflowApplied: Boolean(scaleAdjustPatchReference), scaleAdjustPatchStructureLockedDraftApplied: scaleAdjustPatchReference?.structureLockedDraft === true, + scaleAdjustFullFrameFalEditApplied: useScaleAdjustFullFrameFalEdit, scaleAdjustPatchReferenceOrder: scaleAdjustPatchReference && useScaleAdjustPatchTargetDraftPrimary ? includeScaleAdjustPatchSourceReference @@ -4256,8 +4317,12 @@ export async function POST(request: NextRequest) { featherMaxPx: 42, featherMinPx: 10, featherRatio: 0.018, - maskMode: "diff", - regions: scaleAdjustPatchCompositeRegions, + maskMode: scaleAdjustPatchReference.structureLockedDraft + ? "workspace" + : "diff", + regions: scaleAdjustPatchReference.structureLockedDraft + ? undefined + : scaleAdjustPatchCompositeRegions, sourceImageUrl, workspaceRegion: scaleAdjustPatchReference.workspaceRect, }