fix: route scale shrink through fal edit fallback

This commit is contained in:
134 2026-07-04 13:23:05 +08:00
parent 2e167d38f0
commit 2a8b303508

View File

@ -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,
}