From 0213047d2d44d6b5961d96c60aee903ac6925d02 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 18 May 2026 22:12:12 +0800 Subject: [PATCH] fix: improve scene fusion prompts and failover blacklist --- app/api/scene-fusion/route.ts | 38 ++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/app/api/scene-fusion/route.ts b/app/api/scene-fusion/route.ts index f1bfe9bda..a8a1ad4bc 100644 --- a/app/api/scene-fusion/route.ts +++ b/app/api/scene-fusion/route.ts @@ -50,8 +50,9 @@ const sceneFusionRequestSchema = z.object({ type SceneFusionMode = "scene-fission" | "multi-product-scene" | "random-bg"; -// 默认提示词(当数据库未配置时使用)- 使用"参考"的表述方式 -const FALLBACK_PROMPT = "请参考第一张图片中的产品,将其自然地融合到第二张图片的场景中。要求:1)必须保持产品的核心结构、轮廓比例、部件数量、材质颜色和设计细节与第一张图一致;2)允许为了匹配参考场景调整产品角度、朝向、大小和透视,不要把角度变化当成结构改变;3)注意光影和透视关系使融合自然。"; +// 默认提示词(当数据库未配置时使用)- 强化主体锁定,避免把产品图里的摆件/文字/背景当作产品替入。 +const FALLBACK_PROMPT = + "请参考第一张图片中的主产品主体,将其自然地融合到第二张图片的场景中。要求:1)只提取第一张图的主产品,忽略产品图中的杂志、杯子、摆件、文字、Logo、背景和装饰附件;2)必须保持主产品的核心结构、轮廓比例、部件数量、材质颜色和关键设计细节与第一张图一致;3)允许为了匹配参考场景调整产品角度、朝向、大小和透视,不要把角度变化当成结构改变;4)先移除第二张图里需要被替换的完整目标物,再放入主产品,禁止直接叠加在原目标物上;5)保留第二张图的大部分场景、机位、构图、光影方向和背景关系,使融合自然。"; const SCENE_FUSION_MODEL_COOLDOWN_MS = 2 * 60 * 1000; const SCENE_FUSION_MAX_ITEMS = 10; @@ -64,7 +65,7 @@ const SCENE_FUSION_ECHO_HASH_TIMEOUT_MS = 20_000; const SCENE_FUSION_ECHO_HASH_THRESHOLD = 8; const SCENE_FUSION_REVIEW_TIMEOUT_MS = 25_000; const SCENE_FUSION_STRICT_PROMPT_TAIL = - "硬性要求:严禁直接返回第一张参考图(产品图)原图,必须保留第二张图的场景与构图,仅替换目标主体;结果中不得同时完整保留原目标主体与新产品;产品可因场景透视调整角度、朝向和大小,但核心结构、轮廓比例、部件关系、材质颜色不得被改造;若识别不确定,请优先保持第二张图背景不变并完成主体替换。"; + "硬性要求:严禁直接返回第一张参考图(产品图)原图;严禁把产品图中的杂志、杯子、摆件、文字、Logo、背景或装饰附件当成要替换的产品;必须保留第二张图的场景与构图,仅替换目标主体;替换前必须移除原目标主体,结果中不得同时完整保留原目标主体与新产品,不得把新产品直接叠加在原物体表面;产品可因场景透视调整角度、朝向和大小,但核心结构、轮廓比例、部件关系、材质颜色不得被改造;若识别不确定,请优先保持第二张图背景不变并完成主体替换。"; const SCENE_FUSION_RANDOM_BG_PROMPT_TAIL = "硬性要求:背景必须与原图明显不同(颜色、结构、元素至少两项显著变化),禁止保留原背景纹理或直接回传原图;仅保留产品主体并保证主体结构、轮廓比例、部件关系和边缘完整自然,允许为新背景调整朝向、角度和透视。"; let sceneFusionRoundRobinCursor = 0; @@ -214,6 +215,13 @@ function getSceneFusionCandidatePriority(candidate: SceneFusionModelCandidate): return score; } +function isSceneFusionReviewRejectError(message: string): boolean { + return ( + message.includes("生成结果未通过替换质检") || + message.includes("生成结果疑似回传产品图") + ); +} + function buildPreRmbgCandidateUrls(request: Request): string[] { const urls: string[] = []; const requestUrl = new URL(request.url); @@ -419,10 +427,12 @@ async function detectProductNameWithAgent(imageUrl: string): Promise<{ } | null> { const prompt = [ "你是电商图片分析助手。", - "请识别图片中最核心、最主要的产品主体名称。", + "请识别图片中最核心、最主要、应被用于替换的产品主体名称。", "要求:", "- 名称精简,2-8个字,尽量包含材质/品类(如:皮质沙发、木质餐椅、陶瓷花瓶)", - "- 不要返回颜色描述句子,不要返回背景和场景词", + "- 如果画面里有杂志、杯子、蜡烛、摆件、文字、Logo、包装、背景或装饰附件,只把它们当作干扰项,不要作为产品主体返回", + "- 如果产品图里有多个物体,优先选择占画面最大、最像商品主卖点、可独立售卖的主体", + "- 不要返回颜色描述句子,不要返回背景和场景词,不要返回附件名", "仅返回JSON:", '{\"productName\":\"名称\",\"confidence\":0.0}', ].join("\n"); @@ -463,12 +473,14 @@ async function detectTargetNameWithAgent(params: { "你是场景替换识别助手。", `图1是待融合的产品参考图,产品名称提示:${params.productNameHint || "该产品"}`, "图2是待处理的场景图。", - "请识别图2里最应该被图1产品替换的目标物体,并给出简短位置描述,帮助后续模型准确定位。", + "请识别图2里最应该被图1主产品替换的完整目标物体,并给出简短位置描述,帮助后续模型准确定位。", "要求:", "- 返回“完整物体名”,不要返回局部部件名", "- 名称精简,2-8个字(如:餐椅、沙发、茶几、花瓶)", "- positionHint 用 4-16 个字描述目标大致位置(如:画面中央偏左、前景靠窗处、餐桌区域)", "- replaceCue 用 4-20 个字描述该目标最醒目的识别特征(如:深色办公椅、木质圆餐桌)", + "- 不要把产品图里的附件、文字、Logo、装饰物当成目标;目标必须是图2中需要被移除并替换的完整原物体", + "- 如果图2里有多个同类物体,优先选择与图1主产品用途/体量最接近、画面中最醒目的那个", "- 如果无法确定,返回最主要主体物体名,并尽量给出位置", "仅返回JSON:", '{\"targetName\":\"名称\",\"positionHint\":\"位置\",\"replaceCue\":\"识别特征\",\"confidence\":0.0}', @@ -872,6 +884,7 @@ function buildSceneFusionCandidatePool( if (!entry?.model?.isEnabled) return false; if (!entry?.model?.supportsImageGen) return false; if (entry?.model?.isSubModel) return false; + if (entry?.model?.excludeFromFailover) return false; return true; }) .map((entry) => ({ @@ -1393,15 +1406,17 @@ function buildPrompt( if (sceneMode === "random-bg") { const subject = productName || "产品"; - prompt = `请以第一张图片中的${subject}为唯一主体,对第二张图片进行背景重绘。要求:1)必须保持${subject}核心结构、轮廓比例、部件数量、材质颜色和主要设计细节与第一张图一致,不要改造成另一款产品;2)允许调整主体位置、大小、角度、朝向和透视以匹配新背景,不要把角度变化当成结构改变;3)背景需要完整重建并与原图明显不同;4)光影透视自然,输出商业摄影质感。`; + prompt = `请以第一张图片中的${subject}为唯一商品主体,对第二张图片进行背景重绘。要求:1)只保留${subject},忽略第一张图中的杂志、杯子、蜡烛、摆件、文字、Logo、包装、背景和装饰附件;2)必须保持${subject}核心结构、轮廓比例、部件数量、材质颜色和主要设计细节与第一张图一致,不要改造成另一款产品;3)允许调整主体位置、大小、角度、朝向和透视以匹配新背景,不要把角度变化当成结构改变;4)背景需要完整重建并与原图明显不同;5)光影透视自然,输出商业摄影质感。`; } else { const subject = productName || "产品"; const targetSubject = targetName || "场景主主体"; const locationHints = [positionHint, replaceCue].filter(Boolean).join(","); + const subjectLock = + `主产品锁定:第一张图中只有${subject}是要替换进场景的商品主体,必须忽略杂志、杯子、蜡烛、摆件、文字、Logo、包装、背景和其他装饰附件,不要把这些附件复制到结果图。`; if (targetName) { - prompt = `请参考第一张图片中的${subject},仅替换第二张图片中的${targetSubject},不要改成全新场景。要求:1)必须保持${subject}核心结构、轮廓比例、部件数量、材质颜色和主要设计细节与第一张图一致,不要增删关键部件或改造成另一款产品;2)先移除第二张图中原有的${targetSubject},再将${subject}放入其原位置;3)允许调整${subject}的大小、角度、朝向、光影和透视,使其与第二张图自然融合,角度变化本身是允许的;4)结果中不得同时完整保留原${targetSubject}和新的${subject}。`; + prompt = `${subjectLock} 请参考第一张图片中的${subject},仅替换第二张图片中的${targetSubject},不要改成全新场景。要求:1)必须保持${subject}核心结构、轮廓比例、部件数量、材质颜色和主要设计细节与第一张图一致,不要增删关键部件或改造成另一款产品;2)先完整移除第二张图中原有的${targetSubject},再将${subject}放入其原位置,禁止在原${targetSubject}表面直接叠加;3)允许调整${subject}的大小、角度、朝向、光影和透视,使其与第二张图自然融合,角度变化本身是允许的;4)保留第二张图的主要背景、机位、透视线、空间关系和光源方向;5)结果中不得同时完整保留原${targetSubject}和新的${subject}。`; } else { - prompt = `请参考第一张图片中的${subject},将其自然地融合到第二张图片的场景主主体位置。要求:1)必须保持${subject}核心结构、轮廓比例、部件数量、材质颜色和主要设计细节与第一张图一致,不要改造成另一款产品;2)优先替换第二张图中最主要的同类主体,不要只做风格微调;3)允许为了匹配场景调整${subject}角度、朝向、大小和透视,角度变化不代表结构变化;4)注意光影和透视关系使融合自然。`; + prompt = `${subjectLock} 请参考第一张图片中的${subject},将其自然地融合到第二张图片的场景主主体位置。要求:1)必须保持${subject}核心结构、轮廓比例、部件数量、材质颜色和主要设计细节与第一张图一致,不要改造成另一款产品;2)优先替换第二张图中最主要、用途和体量最接近的同类完整主体,不要只做风格微调;3)先移除原主体再放入${subject},禁止叠加原物体;4)允许为了匹配场景调整${subject}角度、朝向、大小和透视,角度变化不代表结构变化;5)保留第二张图主要背景、机位、透视线、空间关系和光源方向,注意光影和透视关系使融合自然。`; } if (locationHints) { prompt = `${prompt} 待替换目标定位:${locationHints}。`; @@ -1972,7 +1987,10 @@ async function processSceneWithDiversity( markSceneFusionCandidateFailed(candidate, params.modelCooldown); // Batch processing can fail because of per-image inputs, review gates, or echo checks. // Keep these failures local to this request so they do not auto-disable shared models. - console.error("[SceneFusion] Candidate failed, try next:", { + const logCandidateFailure = isSceneFusionReviewRejectError(errorMessage) + ? console.warn + : console.error; + logCandidateFailure("[SceneFusion] Candidate failed, try next:", { requestId: params.requestId, sceneIndex: params.sceneIndex, attempt: attemptIndex + 1,