From eac5bb929625eaa570eda2bd2cb83ab8a68a6d04 Mon Sep 17 00:00:00 2001 From: 134 Date: Sat, 20 Jun 2026 18:39:49 +0800 Subject: [PATCH] fix: disable scale adjust synthetic fallback --- lib/canvas/generate-image.ts | 275 +++++------------------------------ 1 file changed, 40 insertions(+), 235 deletions(-) diff --git a/lib/canvas/generate-image.ts b/lib/canvas/generate-image.ts index 7cf7a4c02..f9621e4b2 100644 --- a/lib/canvas/generate-image.ts +++ b/lib/canvas/generate-image.ts @@ -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(` - - - - - - - - - - - `); -} - 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; }