fix: disable scale adjust synthetic fallback

This commit is contained in:
134 2026-06-20 18:39:49 +08:00
parent 4f06ff7151
commit eac5bb9296

View File

@ -87,6 +87,28 @@ export interface CanvasGenerateImageResult {
prompt: string;
}
class ScaleAdjustOutputRejectedError extends Error {
reason: string;
diffScores?: number[];
outsideDiffScores?: number[];
constructor(params: {
reason: string;
diffScores?: number[];
outsideDiffScores?: number[];
}) {
const message =
params.reason === "outside-frame-changed"
? "比例调整结果改变了框选区域外的画面,已拒绝该模型结果"
: "比例调整结果没有产生有效变化,已拒绝该模型结果";
super(message);
this.name = "ScaleAdjustOutputRejectedError";
this.reason = params.reason;
this.diffScores = params.diffScores;
this.outsideDiffScores = params.outsideDiffScores;
}
}
const LOCAL_EDIT_COMPOSITE_PADDING_RATIO = 0.14;
const LOCAL_EDIT_COMPOSITE_MIN_PADDING_RATIO = 0.012;
const LOCAL_EDIT_COMPOSITE_MAX_PADDING_RATIO = 0.06;
@ -97,8 +119,6 @@ const SCALE_ADJUST_OUTSIDE_DIFF_THRESHOLD = 18;
const SCALE_ADJUST_OUTSIDE_DIFF_MIN_CHANGED_RATIO = 0.45;
const SCALE_ADJUST_OUTSIDE_PROTECTED_MARGIN = 0.035;
const SCALE_ADJUST_DIFF_SAMPLE_SIZE = 112;
const SCALE_ADJUST_FALLBACK_FEATHER_MIN_PX = 8;
const SCALE_ADJUST_FALLBACK_FEATHER_MAX_PX = 36;
const SUPPORTED_CANVAS_PROVIDER_TYPES = new Set([
"gemini",
@ -398,35 +418,6 @@ function rectToPixelBox(params: {
};
}
function buildScaleAdjustSoftMaskSvg(width: number, height: number) {
const blur = Math.max(
SCALE_ADJUST_FALLBACK_FEATHER_MIN_PX,
Math.min(
SCALE_ADJUST_FALLBACK_FEATHER_MAX_PX,
Math.round(Math.min(width, height) * 0.035)
)
);
const inset = Math.min(
Math.floor(Math.min(width, height) / 4),
Math.max(1, Math.round(blur * 0.75))
);
const rectWidth = Math.max(1, width - inset * 2);
const rectHeight = Math.max(1, height - inset * 2);
return Buffer.from(`
<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="scale-adjust-soft-mask" x="-30%" y="-30%" width="160%" height="160%" color-interpolation-filters="sRGB">
<feGaussianBlur stdDeviation="${blur}" />
</filter>
</defs>
<rect x="0" y="0" width="${width}" height="${height}" fill="rgba(0,0,0,0)" />
<rect x="${inset}" y="${inset}" width="${rectWidth}" height="${rectHeight}" rx="${Math.round(blur * 0.9)}" fill="#ffffff" filter="url(#scale-adjust-soft-mask)" />
<rect x="${Math.max(0, Math.round(inset / 2))}" y="${Math.max(0, Math.round(inset / 2))}" width="${Math.max(1, width - inset)}" height="${Math.max(1, height - inset)}" rx="${Math.round(blur * 0.5)}" fill="#ffffff" opacity="0.92" />
</svg>
`);
}
async function getScaleAdjustRegionSample(params: {
buffer: Buffer;
sourceWidth: number;
@ -539,96 +530,6 @@ function isScaleAdjustOutsideChanged(scores: number[]) {
return changedCount >= requiredCount;
}
async function buildScaleAdjustFallbackImage(params: {
sourceBuffer: Buffer;
sourceWidth: number;
sourceHeight: number;
verification: CanvasScaleAdjustVerificationConfig;
}) {
let workingBuffer = await sharp(params.sourceBuffer, { failOn: "none" })
.rotate()
.resize(params.sourceWidth, params.sourceHeight, {
fit: "cover",
position: "center",
})
.png({ compressionLevel: 6 })
.toBuffer();
for (const region of params.verification.regions) {
const sourceBox = rectToPixelBox({
region,
width: params.sourceWidth,
height: params.sourceHeight,
});
const targetBox = rectToPixelBox({
region: scaleRegionAroundCenter(region, params.verification.scalePercent),
width: params.sourceWidth,
height: params.sourceHeight,
});
const targetMask = buildScaleAdjustSoftMaskSvg(
targetBox.width,
targetBox.height
);
const scaledPatch = await sharp(params.sourceBuffer, { failOn: "none" })
.rotate()
.resize(params.sourceWidth, params.sourceHeight, {
fit: "cover",
position: "center",
})
.extract(sourceBox)
.resize(targetBox.width, targetBox.height, { fit: "fill" })
.ensureAlpha()
.composite([{ input: targetMask, blend: "dest-in" }])
.png({ compressionLevel: 6 })
.toBuffer();
const composites: sharp.OverlayOptions[] = [];
if (params.verification.scalePercent < 0) {
const fillMask = buildScaleAdjustSoftMaskSvg(
sourceBox.width,
sourceBox.height
);
const fillPatch = await sharp(params.sourceBuffer, { failOn: "none" })
.rotate()
.resize(params.sourceWidth, params.sourceHeight, {
fit: "cover",
position: "center",
})
.extract(sourceBox)
.blur(
Math.max(
8,
Math.min(34, Math.round(Math.min(sourceBox.width, sourceBox.height) * 0.08))
)
)
.modulate({ brightness: 1.02, saturation: 0.98 })
.ensureAlpha()
.composite([{ input: fillMask, blend: "dest-in" }])
.png({ compressionLevel: 6 })
.toBuffer();
composites.push({
input: fillPatch,
left: sourceBox.left,
top: sourceBox.top,
});
}
composites.push({
input: scaledPatch,
left: targetBox.left,
top: targetBox.top,
});
workingBuffer = await sharp(workingBuffer, { failOn: "none" })
.ensureAlpha()
.composite(composites)
.png({ compressionLevel: 6 })
.toBuffer();
}
return workingBuffer;
}
async function loadScaleAdjustSourceImage(
verification: CanvasScaleAdjustVerificationConfig
) {
@ -653,61 +554,6 @@ async function loadScaleAdjustSourceImage(
};
}
async function createScaleAdjustFallbackUpload(params: {
sourceBuffer: Buffer;
sourceWidth: number;
sourceHeight: number;
verification: CanvasScaleAdjustVerificationConfig;
debugLabel: string;
imageIndex?: number;
reason: string;
diffScores?: number[];
outsideDiffScores?: number[];
}) {
const fallbackBuffer = await buildScaleAdjustFallbackImage({
sourceBuffer: params.sourceBuffer,
sourceHeight: params.sourceHeight,
sourceWidth: params.sourceWidth,
verification: params.verification,
});
const thumbnailBuffer = await sharp(fallbackBuffer, { failOn: "none" })
.resize(512, 512, { fit: "inside", withoutEnlargement: true })
.png({ compressionLevel: 6 })
.toBuffer();
const suffix = `${Date.now()}-${params.imageIndex || 1}-${Math.random()
.toString(36)
.slice(2, 8)}`;
const [originalUpload, thumbnailUpload] = await Promise.all([
uploadFile(
`canvas-scale-adjust-fallback-${suffix}.png`,
bufferToArrayBuffer(fallbackBuffer)
),
uploadFile(
`canvas-scale-adjust-fallback-thumb-${suffix}.png`,
bufferToArrayBuffer(thumbnailBuffer)
),
]);
console.warn("[Canvas Generate Image] Scale adjust fallback applied:", {
debugLabel: params.debugLabel,
diffScores: params.diffScores?.map((score) => Math.round(score * 10) / 10),
imageIndex: params.imageIndex,
outsideDiffScores: params.outsideDiffScores?.map(
(score) => Math.round(score * 10) / 10
),
reason: params.reason,
regionCount: params.verification.regions.length,
scalePercent: params.verification.scalePercent,
sourceHeight: params.sourceHeight,
sourceWidth: params.sourceWidth,
});
return {
originalUrl: originalUpload.url,
thumbnailUrl: thumbnailUpload.url,
};
}
async function applyScaleAdjustVerificationToImages(params: {
images: ParsedImage[];
verification: CanvasScaleAdjustVerificationConfig | null;
@ -760,22 +606,29 @@ async function applyScaleAdjustVerificationToImages(params: {
continue;
}
const fallbackImage = await createScaleAdjustFallbackUpload({
debugLabel,
const rejection = new ScaleAdjustOutputRejectedError({
diffScores,
imageIndex: index + 1,
outsideDiffScores,
reason: outsideChanged ? "outside-frame-changed" : "unchanged-output",
sourceBuffer,
sourceHeight,
sourceWidth,
verification,
});
checkedImages.push(fallbackImage);
console.warn("[Canvas Generate Image] Scale adjust output rejected:", {
debugLabel,
diffScores: diffScores.map((score) => Math.round(score * 10) / 10),
imageIndex: index + 1,
outsideDiffScores: outsideDiffScores.map(
(score) => Math.round(score * 10) / 10
),
reason: rejection.reason,
scalePercent: verification.scalePercent,
});
throw rejection;
}
return checkedImages;
} catch (error) {
if (error instanceof ScaleAdjustOutputRejectedError) {
throw error;
}
console.warn("[Canvas Generate Image] Scale adjust verification skipped:", {
debugLabel,
error: error instanceof Error ? error.message : String(error),
@ -1691,8 +1544,11 @@ export async function generateCanvasImage(
].filter(Boolean))
);
const allowFailover = request.allowFailover !== false;
const scaleAdjustRejected =
caughtError instanceof ScaleAdjustOutputRejectedError;
const shouldFailover =
shouldFailoverAiModelError(caughtError) ||
scaleAdjustRejected ||
(Boolean(debugMeta?.detailFixedModelApplied) && Boolean(caughtError));
const fallbackModel = allowFailover && shouldFailover
? findCanvasFailoverModel({
@ -1726,56 +1582,5 @@ export async function generateCanvasImage(
});
}
if (scaleAdjustVerification) {
try {
const sourceImage = await loadScaleAdjustSourceImage(scaleAdjustVerification);
if (sourceImage) {
const fallbackImage = await createScaleAdjustFallbackUpload({
debugLabel,
imageIndex: 1,
reason: allowFailover ? "provider-error-after-failover-exhausted" : "provider-error",
sourceBuffer: sourceImage.sourceBuffer,
sourceHeight: sourceImage.sourceHeight,
sourceWidth: sourceImage.sourceWidth,
verification: scaleAdjustVerification,
});
recordCanvasImageModelExecutionDuration(
resolvedModelKey,
Date.now() - startedAt
);
console.warn("[Canvas Generate Image] Scale adjust provider failed; returned deterministic fallback:", {
debugLabel,
elapsedMs: Date.now() - startedAt,
providerType: provider.type,
selectedModelRef: selectedModel.model.id,
selectedModelId: selectedModel.model.modelId,
error: caughtError instanceof Error ? caughtError.message : String(caughtError),
...debugMeta,
});
return {
images: [fallbackImage],
model: selectedModel.model.displayName,
modelRef: selectedModel.model.id,
prompt: resultPrompt,
};
}
} catch (fallbackError) {
console.warn("[Canvas Generate Image] Scale adjust provider-error fallback failed:", {
debugLabel,
error:
fallbackError instanceof Error
? fallbackError.message
: String(fallbackError),
originalError:
caughtError instanceof Error ? caughtError.message : String(caughtError),
regionCount: scaleAdjustVerification.regions.length,
scalePercent: scaleAdjustVerification.scalePercent,
});
}
}
throw caughtError;
}