fix(canvas): quarantine unstable detailed inspection behind explicit opt-in

This commit is contained in:
134 2026-10-20 02:29:33 +08:00
parent d228d01244
commit 9a5ed91e32
3 changed files with 9 additions and 3 deletions

View File

@ -41,7 +41,7 @@ async function stageImage(image: Omit<PreparedImage, "url">, 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();

View File

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

View File

@ -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.