diff --git a/app/(chat)/api/chat/_lib/detail-replacement.ts b/app/(chat)/api/chat/_lib/detail-replacement.ts index 460efaf32..a1f19af30 100644 --- a/app/(chat)/api/chat/_lib/detail-replacement.ts +++ b/app/(chat)/api/chat/_lib/detail-replacement.ts @@ -399,8 +399,8 @@ type ReplacementReferenceDecisionCore = Omit< "detectionModelName" | "detectionSource" >; -const DETAIL_WHITE_LAYOUT_HINT_PATTERN = - /白底|浅色底|大面积留白|产品说明|参数图|规格图|卖点图|标签排版|说明图|信息图|海报|版式图|排版页|文字页|标题页|编号|尺寸|规格|厘米|\bcm\b/i; +const DETAIL_HARD_LAYOUT_HINT_PATTERN = + /产品尺寸|尺寸|规格|参数|单位|厘米|\bcm\b|产品说明|说明图|信息图|参数图|规格图|材质标注|工艺标注|结构标注|功能图标|图标说明|尺寸标注|承重参数|安装说明|标签图|卖点卡|白底产品|白底参数|白底规格/i; const DETAIL_STRONG_COPY_HINT_PATTERN = /白底|浅色底|产品说明|参数|规格|尺寸|卖点|标签|说明图|信息图|海报|版式|排版|文字|标题|编号|标注|材质标注|工艺标注|代名词|典雅|高贵/i; const DETAIL_PEOPLE_ACTION_HINT_PATTERN = @@ -761,37 +761,48 @@ function applyDetailReplacementScenePlanGuards( const alreadyAllowsPromptPack = scenePlan.generationRoute === "prompt_pack_scene" && scenePlan.promptPackPolicy !== "forbid"; - const hasWhiteLayoutHint = DETAIL_WHITE_LAYOUT_HINT_PATTERN.test(planText); + const hasHardLayoutHint = DETAIL_HARD_LAYOUT_HINT_PATTERN.test(planText); const hasPeopleActionHint = DETAIL_PEOPLE_ACTION_HINT_PATTERN.test(planText); + const hasPeopleSignals = + !isProductOnly && + (params.containsPeople || + params.containsMultiplePeople || + scenePlan.peoplePolicy !== "none" || + hasPeopleActionHint); const visualProfileSuggestsWhiteLayout = params.visualProfile?.likelyWhiteLayout === true; const visualProfileSuggestsWhiteScene = params.visualProfile?.likelyWhiteScene === true && !visualProfileSuggestsWhiteLayout; + const visualProfileSuggestsLightBase = + params.visualProfile?.likelyWhiteScene === true || + params.visualProfile?.likelyWhiteLayout === true; const scenePlanSuggestsWhiteLayout = scenePlan.whiteBackgroundLikely && !visualProfileSuggestsWhiteScene && - (scenePlan.textPolicy === "lock" || scenePlan.copyImageCoupled); + !hasPeopleSignals && + (hasHardLayoutHint || + (scenePlan.copyImageCoupled && scenePlan.textPolicy === "lock")); const shouldProtectWhiteLayout = !isProductOnly && - (visualProfileSuggestsWhiteLayout || + !hasPeopleSignals && + ((visualProfileSuggestsWhiteLayout && + (hasHardLayoutHint || + (scenePlan.copyImageCoupled && scenePlan.textPolicy === "lock"))) || scenePlanSuggestsWhiteLayout || - (hasWhiteLayoutHint && !visualProfileSuggestsWhiteScene)); + (hasHardLayoutHint && !visualProfileSuggestsWhiteScene)); const shouldProtectWhiteScene = !isProductOnly && !shouldProtectWhiteLayout && - visualProfileSuggestsWhiteScene; + visualProfileSuggestsLightBase; const shouldProtectPeople = !isProductOnly && - (params.containsPeople || - params.containsMultiplePeople || - (!alreadyAllowsPromptPack && scenePlan.peoplePolicy !== "none") || - hasPeopleActionHint); + (hasPeopleSignals || (!alreadyAllowsPromptPack && scenePlan.peoplePolicy !== "none")); const shouldLockCopy = !isProductOnly && (scenePlan.copyImageCoupled || params.hasImportantText || scenePlan.textPolicy === "lock" || - hasWhiteLayoutHint); + hasHardLayoutHint); let next: ReplacementScenePlan = { ...scenePlan }; @@ -1145,35 +1156,55 @@ export function buildHeuristicReplacementScenePlan(params: { const isProductOnly = params.replacementStrategy === "product_only"; const productImageSelection = buildDefaultProductImageSelection(params.productImageCount); const fallbackText = String(params.reason || ""); + const fallbackHasPeopleAction = DETAIL_PEOPLE_ACTION_HINT_PATTERN.test(fallbackText); + const fallbackHasHardLayout = DETAIL_HARD_LAYOUT_HINT_PATTERN.test(fallbackText); const shouldProtectFallbackWhiteLayout = isDetailReplacement && !isProductOnly && - (Boolean(params.visualProfile?.likelyWhiteLayout) || - DETAIL_STRONG_COPY_HINT_PATTERN.test(fallbackText)); + !fallbackHasPeopleAction && + (fallbackHasHardLayout || + (Boolean(params.visualProfile?.likelyWhiteLayout) && + DETAIL_STRONG_COPY_HINT_PATTERN.test(fallbackText))); const shouldRebuildFallbackWhiteScene = isDetailReplacement && !isProductOnly && !shouldProtectFallbackWhiteLayout && - Boolean(params.visualProfile?.likelyWhiteScene); + (fallbackHasPeopleAction || + Boolean(params.visualProfile?.likelyWhiteScene) || + Boolean(params.visualProfile?.likelyWhiteLayout)); const shouldProtectFallbackLayout = shouldProtectFallbackWhiteLayout; const generationRoute = params.shouldReplace ? isProductOnly ? "layout_preserve_replace" : isDetailReplacement - ? shouldProtectFallbackLayout + ? fallbackHasPeopleAction + ? "people_relation_locked" + : shouldProtectFallbackLayout ? "layout_preserve_replace" : "prompt_pack_scene" : "prompt_pack_scene" : "direct_passthrough"; + const promptPackPolicy: DetailReplacementPromptPackPolicy = + params.shouldReplace && isDetailReplacement && !isProductOnly + ? fallbackHasPeopleAction + ? shouldRebuildFallbackWhiteScene + ? "soft" + : "allow" + : shouldRebuildFallbackWhiteScene + ? "soft" + : inferPromptPackPolicy(generationRoute) + : inferPromptPackPolicy(generationRoute); const scenePlan: ReplacementScenePlan = { generationRoute, - promptPackPolicy: inferPromptPackPolicy(generationRoute), + promptPackPolicy, textPolicy: generationRoute === "direct_passthrough" || shouldProtectFallbackLayout ? "preserve" + : fallbackHasPeopleAction || shouldRebuildFallbackWhiteScene + ? "preserve" : "free", - peoplePolicy: "none", + peoplePolicy: fallbackHasPeopleAction ? "preserve_relation" : "none", whiteBackgroundLikely: Boolean( params.visualProfile?.likelyWhiteLayout || params.visualProfile?.likelyWhiteScene, ), @@ -1182,7 +1213,9 @@ export function buildHeuristicReplacementScenePlan(params: { ? isProductOnly ? "兜底按仅替换产品执行,不使用词包。" : isDetailReplacement - ? shouldProtectFallbackLayout + ? fallbackHasPeopleAction + ? "兜底命中人物/动作/使用场景,按人物关系锁定并允许词包重建背景和产品场景。" + : shouldProtectFallbackLayout ? "兜底命中白底说明/强文案版式,按详情版式保护执行,不使用词包。" : shouldRebuildFallbackWhiteScene ? "兜底命中白/浅色真实场景,允许词包软重造,但保持白场景基调。" @@ -1200,7 +1233,9 @@ export function buildHeuristicReplacementScenePlan(params: { sceneSummary: params.shouldReplace ? isProductOnly ? "严格保留参考图的场景、文案和版式,只把产品展示区中的旧产品替换成输入产品。" - : shouldProtectFallbackLayout + : fallbackHasPeopleAction + ? "保留参考图人物数量、动作、互动关系、文字层级和版式骨架,同时按工作流目标重建背景、产品、软装和灯光。" + : shouldProtectFallbackLayout ? "保留参考图白底说明版式、构图、文字和主体位置,只做必要的产品或视觉区替换。" : shouldRebuildFallbackWhiteScene ? "保留参考图白/浅色低饱和基调与版式骨架,但按工作流目标明显重建图片框内产品、空间、软装和灯光。" @@ -1215,14 +1250,18 @@ export function buildHeuristicReplacementScenePlan(params: { : "文字排版、标题区、分割线、留白和信息层级必须保留。", styleSummary: isProductOnly ? "延续参考图原有风格、背景和氛围,不引入新的场景改造要求。" - : shouldProtectFallbackLayout + : fallbackHasPeopleAction + ? "人物动作和关系保真,背景、产品场景、软装和灯光按前置词包明显重建。" + : shouldProtectFallbackLayout ? "延续参考图原有白底说明版式和图文语义,不引入新的空间场景。" : shouldRebuildFallbackWhiteScene ? "按前置词包重建为白/浅色低饱和真实场景,保持干净浅色基调,不走深色复杂空间。" : "除版式骨架外,墙面、地面、背景家具、软装、灯光、摆件和空间氛围都要按工作流风格重建。", avoidSummary: isProductOnly ? "不要改动背景、文案、按钮、图标、排版和装饰,不要重建整个场景,不要把多个产品角度同时塞进一张图。" - : shouldProtectFallbackLayout + : fallbackHasPeopleAction + ? "不要生成白底单品图,不要删掉人物,不要把多人压成单人,不要只小幅复刻原图,也不要破坏文字版式。" + : shouldProtectFallbackLayout ? "不要把白底说明/强文案页场景化,不要新增厨房/客厅/黑墙/吊灯/复杂软装,不要让文字和图片失配。" : shouldRebuildFallbackWhiteScene ? "不要只小幅复刻原图,不要把白场景改成深色厨房、黑墙、复杂吊灯或强软装,不要破坏原版式。" @@ -1230,6 +1269,8 @@ export function buildHeuristicReplacementScenePlan(params: { generationPrompt: params.shouldReplace ? isProductOnly ? `仅替换参考图中的产品主体,严格保留其余背景、文案、排版与光影关系。${params.reason}` + : isDetailReplacement && fallbackHasPeopleAction + ? `当前参考图按人物/动作详情页处理:必须保留参考图中的人物数量、动作、朝向、互动关系和出镜比例,同时使用前置工作流词包明显重建图片框内旧背景、产品、空间、软装和灯光;禁止生成白底单品图或无人物空场景,保留文字区、版式骨架、机位和信息层级。${params.reason}` : isDetailReplacement && shouldProtectFallbackLayout ? `保留参考图的版式、文案、白底/留白和图文语义,不使用前置词包;只在产品、材质、板材、原材料或视觉图片区做必要替换。${params.reason}` : isDetailReplacement && shouldRebuildFallbackWhiteScene @@ -1247,7 +1288,7 @@ export function buildHeuristicReplacementScenePlan(params: { hasModelImage: params.hasModelImage, hasImportantText: shouldProtectFallbackLayout, containsPeople: false, - containsMultiplePeople: false, + containsMultiplePeople: fallbackHasPeopleAction, visualProfile: params.visualProfile, replacementStrategy: params.replacementStrategy, }); @@ -2059,6 +2100,9 @@ export function buildAgentReplacementPrompt(params: { params.scenePlan.peoplePolicy === "preserve_relation" ? "人物动作硬约束:如果当前参考图文字或画面包含自拍、合照、聚会、用餐、拿手机、使用产品、人与人互动等语义,生成结果必须出现对应人物数量、动作、朝向和互动关系;不允许生成无人空场景。" : "", + shouldApplyPromptPack + ? "词包生效硬约束:本屏不能只做轻微改色、轻微换物或原图复刻;图片框内旧背景、产品主体、软装、灯光和空间氛围必须有清晰可见的重建痕迹。" + : "", params.scenePlan.generationPrompt ? `本屏生成方向:${params.scenePlan.generationPrompt}` : "", @@ -2118,12 +2162,16 @@ export function buildAgentReplacementPrompt(params: { shouldApplyPromptPack ? "执行时要基于这些场景组合要求,重建参考图中的背景图/产品场景图区域,使当前这一屏呈现出与第一步已选风格一致的新背景画面。" : "执行时必须保持参考图原有画面主题、文案语义、人物关系、构图和版式,只在可替换视觉区做有限替换。", + shouldApplyPromptPack + ? "失败判定:如果最终图变成白底单品图、产品参数卡、单独商品抠图、或只是把原参考图小幅修饰,均视为失败;除非本屏已明确判定为白底参数/规格说明页。" + : "", "允许替换/重建的范围是参考图里的背景图、产品场景图、商品主体图、材质/板材/原材料视觉区和必要的人物展示图区;不允许改动版式层。", "如果前面商品图、模特图与最后一张参考图底板存在冲突,始终以最后一张参考图底板的版式、机位、构图和信息层级为最高优先级。", "最后一张参考图是唯一底板,必须完整保留这张图的文字排版、品牌字样、标题区、卖点区、UI图标、按钮、标签、装饰元素、分割线、留白、画布比例和信息层级。", params.scenePlan?.textPolicy === "lock" ? "本屏文案已判定为强锁定:标题、卖点、参数、编号、标签文字的内容、颜色层级、位置和排版都不得改写、变色或拆成多种颜色。" : "如果参考图里存在文字或标签,必须保持文字区位置和信息层级,不要擅自改写或重排。", + "文案存在性硬约束:如果参考图原本有标题、卖点、编号、参数、标签或图标说明,结果图必须仍然保留对应文本块的数量、相对位置、字号层级和颜色风格;禁止整段文案消失,禁止新增参考图没有的营销标签或小贴纸。", params.scenePlan?.peoplePolicy === "require_user_model" ? "本屏包含人物但没有提供用户模特图:禁止把参考图中的人物身份当作用户模特迁移;只能保留人物数量、关系、动作和构图语义,不要换成不相关强人物主体。" : params.scenePlan?.peoplePolicy === "preserve_relation" @@ -2170,6 +2218,9 @@ export function buildAgentReplacementPrompt(params: { shouldApplyPromptPack ? "如果生成结果仍然保留了参考图原有的大部分背景图物件,只是把产品换掉,则视为执行失败;你必须让背景图/场景图发生清晰可见的变化。" : "如果本屏已禁用词包,生成结果不应追求明显改造背景,而应追求图文匹配、文案稳定、人物关系稳定和局部替换自然。", + !shouldApplyPromptPack + ? "版式说明页额外约束:不得新增参考图没有的标签、角标、贴纸、说明卡或营销文案;尺寸线、单位、参数和产品图片区要按原图关系保留。" + : "", "绝对不要移动、删除、改写、重排参考图里的任何文字、UI图标、按钮、标签、装饰元素与排版元素。", "输出结果必须保持最后一张参考图底板的原始纵横比与画布尺寸,不允许擅自改成长图、横图或裁掉任何版面区域。", "如果参考图中没有产品主体,继续检查人物/模特展示;如果没有人物,则根据上传图外观和参考图中的材质描述处理板材、材质、原材料、背景或视觉图片区;不允许整屏直出。",