From 9a5ed91e3210dc1d8f6e62c698be43297b0d3247 Mon Sep 17 00:00:00 2001 From: 134 Date: Tue, 20 Oct 2026 02:29:33 +0800 Subject: [PATCH] fix(canvas): quarantine unstable detailed inspection behind explicit opt-in --- lib/canvas/layout-replace-inputs.ts | 4 ++-- lib/canvas/layout-replace-task.ts | 4 +++- lib/canvas/layout-replace-verifier.ts | 4 ++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/canvas/layout-replace-inputs.ts b/lib/canvas/layout-replace-inputs.ts index 40c5f5826..45391c68b 100644 --- a/lib/canvas/layout-replace-inputs.ts +++ b/lib/canvas/layout-replace-inputs.ts @@ -41,7 +41,7 @@ async function stageImage(image: Omit, role: "template" | return { ...image, url: await entry.url }; } -export async function prepareLayoutReplaceInputs(sourceUrl: string, referenceUrl: string) { +export async function prepareLayoutReplaceInputs(sourceUrl: string, referenceUrl: string, options?: { checkText?: boolean }) { if (sourceUrl === referenceUrl) throw new Error("新商品图和目标模版不能是同一张图片"); const [originalSource, originalTemplate] = await Promise.all([ readImage(sourceUrl), readImage(referenceUrl), @@ -52,7 +52,7 @@ export async function prepareLayoutReplaceInputs(sourceUrl: string, referenceUrl // source so an incorrect crop cannot silently redefine the product identity. const [{ box, identity }, textBoundingBoxes] = await Promise.all([ locateLayoutReplaceProduct(originalSource.buffer, originalTemplate.buffer), - locateLayoutReplaceTemplateText(originalTemplate.buffer), + options?.checkText === false ? Promise.resolve([]) : locateLayoutReplaceTemplateText(originalTemplate.buffer), ]); const crop = layoutReplaceProductCrop(box, originalSource.width, originalSource.height); const productBuffer = await sharp(originalSource.buffer).extract(crop).png().toBuffer(); diff --git a/lib/canvas/layout-replace-task.ts b/lib/canvas/layout-replace-task.ts index 9eaef74bf..7dea57965 100644 --- a/lib/canvas/layout-replace-task.ts +++ b/lib/canvas/layout-replace-task.ts @@ -135,7 +135,8 @@ export async function executeCanvasLayoutReplaceTask(params: { throw new Error("AI 模版替换未配置可用的非 FAL 多图编辑模型"); } - const inputs = await prepareLayoutReplaceInputs(sourceImageUrl, referenceImageUrl); + const detailedChecks = process.env.CANVAS_LAYOUT_REPLACE_DETAILED_CHECKS === "1"; + const inputs = await prepareLayoutReplaceInputs(sourceImageUrl, referenceImageUrl, { checkText: detailedChecks }); const referenceGeometry = inputs.referenceGeometry; const sourceProfile = await resolveCanvasLayoutReplaceSourceProfile(sourceImageUrl, inputs.source.buffer); const sourceProductAspectRatio = sourceProfile?.silhouetteAspectRatio || inputs.productAspectRatio; @@ -200,6 +201,7 @@ export async function executeCanvasLayoutReplaceTask(params: { verify: result => verifyCanvasLayoutReplacement({ sourceBuffer: inputs.sourceOriginalBuffer, templateBuffer: inputs.template.buffer, sourceIdentity: inputs.sourceIdentity, + detailedChecks, productBuffer: inputs.source.buffer, textBoundingBoxes: inputs.textBoundingBoxes, resultUrl: result.images[0].originalUrl, diff --git a/lib/canvas/layout-replace-verifier.ts b/lib/canvas/layout-replace-verifier.ts index bea3fc82b..8ec49705c 100644 --- a/lib/canvas/layout-replace-verifier.ts +++ b/lib/canvas/layout-replace-verifier.ts @@ -112,6 +112,7 @@ export async function verifyCanvasLayoutReplacement(params: { sourceIdentity?: string; productBuffer?: Buffer; textBoundingBoxes?: NormalizedReplacementBox[]; + detailedChecks?: boolean; }) { const resultBuffer = params.resultBuffer || await fetchImageBufferFromUrl(params.resultUrl); const prompt = [LAYOUT_REPLACE_QUALITY_PROMPT, params.sourceIdentity @@ -119,6 +120,9 @@ export async function verifyCanvasLayoutReplacement(params: { const board = await makeLayoutReplaceQualityBoard({ template: params.templateBuffer, source: params.sourceBuffer, product: params.productBuffer || params.sourceBuffer, result: resultBuffer }); const quality = await inspectLayoutImages([board], prompt, parseLayoutReplaceQuality); + // Fine-grained OCR/support classification is experimental: real fixtures + // showed provider-dependent false rejections. Keep it opt-in until stable. + if (params.detailedChecks === false) return quality; if (quality.sourceProductPreserved) { // Read the actual result product at the same scale as the source crop. // Poster copy and floor shadows must not obscure its support structure.