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.