fix: route detail replacements by scene intent

This commit is contained in:
Your Name 2026-06-03 15:41:13 +08:00
parent ce7c2bff35
commit fb240616e0

View File

@ -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图标、按钮、标签、装饰元素与排版元素。",
"输出结果必须保持最后一张参考图底板的原始纵横比与画布尺寸,不允许擅自改成长图、横图或裁掉任何版面区域。",
"如果参考图中没有产品主体,继续检查人物/模特展示;如果没有人物,则根据上传图外观和参考图中的材质描述处理板材、材质、原材料、背景或视觉图片区;不允许整屏直出。",