fix: route detail replacement scene prompts correctly

This commit is contained in:
Your Name 2026-06-02 18:03:16 +08:00
parent 5e896b8e56
commit 5aab184a97
2 changed files with 56 additions and 13 deletions

View File

@ -324,9 +324,11 @@ type ReplacementReferenceDecisionCore = Omit<
>;
const DETAIL_WHITE_LAYOUT_HINT_PATTERN =
/白底|浅色底|留白|产品说明|参数|规格|卖点|标签|说明图|信息图|海报|版式|排版|文字|标题|编号|材质|工艺|桌腿|木蜡油|代名词|典雅|高贵/i;
/白底|浅色底|大面积留白|产品说明|参数图|规格图|卖点图|标签排版|说明图|信息图|海报|版式图|排版页|文字页|标题页|编号|尺寸|规格|厘米|\bcm\b/i;
const DETAIL_STRONG_COPY_HINT_PATTERN =
/白底|浅色底|产品说明|参数|规格|尺寸|卖点|标签|说明图|信息图|海报|版式|排版|文字|标题|编号|标注|材质标注|工艺标注|代名词|典雅|高贵/i;
const DETAIL_PEOPLE_ACTION_HINT_PATTERN =
/自拍|合照|拍照|拿手机|手机|聚会|好友|朋友|闺蜜|夫妻|情侣|家庭|亲子|陪伴|吃饭|用餐|举杯|互动|模特|人物|人像|多人|坐|站|走|逛|使用|体验/i;
/自拍|合照|拍照|拿手机|手机自拍|聚会|好友|朋友|闺蜜|夫妻|情侣|家庭|亲子|陪伴|举杯|互动|模特|人物|人像|多人|人群|男士|女士|男人|女人|男生|女生|小孩|儿童|宝宝|顾客|吃饭的人|用餐的人|正在吃饭|坐在|站在|走动|行走|手持|试坐|倚靠/i;
export type ReplacementScenePlanningRuntime =
| {
@ -384,12 +386,16 @@ function ensureDetailReferenceGenerates<T extends ReplacementReferenceDecisionCo
? "people_relation_locked"
: scenePlan.copyImageCoupled || scenePlan.textPolicy === "lock"
? "copy_locked_regenerate"
: "layout_preserve_replace";
: scenePlan.whiteBackgroundLikely
? "layout_preserve_replace"
: "prompt_pack_scene";
const forcedReason = isStitchedReference
? "拼合切片未勾选不处理,继续执行详情替换"
: "详情替换未勾选不处理,继续执行必要替换";
const forcedGenerationPrompt =
"保留当前详情图的版式、文案、图标、构图和留白;先寻找可替换产品/产品展示区,没有产品则检查人物/模特展示,没有人物则根据上传图外观与参考图材质描述处理板材、材质、原材料、背景或视觉图片区,不允许整屏直出。";
forcedRoute === "prompt_pack_scene"
? "保留当前详情图的版式、文案、图标、构图和留白,但必须按前置工作流词包重建旧背景、空间、软装、灯光和产品场景;先寻找可替换产品/产品展示区,不允许整屏直出。"
: "保留当前详情图的版式、文案、图标、构图和留白;先寻找可替换产品/产品展示区,没有产品则检查人物/模特展示,没有人物则根据上传图外观与参考图材质描述处理板材、材质、原材料、背景或视觉图片区,不允许整屏直出。";
return {
decision: {
@ -406,7 +412,7 @@ function ensureDetailReferenceGenerates<T extends ReplacementReferenceDecisionCo
: scenePlan.generationRoute,
promptPackPolicy:
scenePlan.generationRoute === "direct_passthrough"
? "forbid"
? inferPromptPackPolicy(forcedRoute)
: scenePlan.promptPackPolicy,
textPolicy: scenePlan.textPolicy === "free" ? "preserve" : scenePlan.textPolicy,
routeReason:
@ -966,18 +972,26 @@ export function buildHeuristicReplacementScenePlan(params: {
const isDetailReplacement = params.modeType === "detail_replacement";
const isProductOnly = params.replacementStrategy === "product_only";
const productImageSelection = buildDefaultProductImageSelection(params.productImageCount);
const fallbackText = String(params.reason || "");
const shouldProtectFallbackLayout =
isDetailReplacement &&
!isProductOnly &&
(Boolean(params.visualProfile?.likelyWhiteLayout) ||
DETAIL_STRONG_COPY_HINT_PATTERN.test(fallbackText));
const generationRoute = params.shouldReplace
? isProductOnly
? "layout_preserve_replace"
: isDetailReplacement
? "layout_preserve_replace"
? shouldProtectFallbackLayout
? "layout_preserve_replace"
: "prompt_pack_scene"
: "prompt_pack_scene"
: "direct_passthrough";
const scenePlan: ReplacementScenePlan = {
generationRoute,
promptPackPolicy: inferPromptPackPolicy(generationRoute),
textPolicy:
generationRoute === "direct_passthrough" || isDetailReplacement
generationRoute === "direct_passthrough" || shouldProtectFallbackLayout
? "preserve"
: "free",
peoplePolicy: "none",
@ -987,7 +1001,9 @@ export function buildHeuristicReplacementScenePlan(params: {
? isProductOnly
? "兜底按仅替换产品执行,不使用词包。"
: isDetailReplacement
? "兜底按详情版式保护执行,不使用词包,避免白底/强文案/人物页被误场景化。"
? shouldProtectFallbackLayout
? "兜底命中白底/说明/强文案特征,按详情版式保护执行,不使用词包。"
: "兜底未命中白底、强文案或明确人物关系,按通用场景图执行词包场景替换。"
: "兜底未检测到强约束,允许使用词包生成场景。"
: params.reason,
selectedProductImageIndexes: productImageSelection.selectedProductImageIndexes,
@ -1001,7 +1017,9 @@ export function buildHeuristicReplacementScenePlan(params: {
sceneSummary: params.shouldReplace
? isProductOnly
? "严格保留参考图的场景、文案和版式,只把产品展示区中的旧产品替换成输入产品。"
: "保留参考图版式骨架、构图和主体位置,但把旧场景视觉内容按工作流目标风格重建。"
: shouldProtectFallbackLayout
? "保留参考图版式骨架、构图、文字和主体位置,只做必要的产品或视觉区替换。"
: "保留参考图版式骨架、构图和主体位置,但把旧场景视觉内容按工作流目标风格重建。"
: "当前参考图不满足替换条件。",
cameraSummary: "保持参考图原有机位、构图和景别,不做整体重画。",
placementSummary: isProductOnly
@ -1012,15 +1030,21 @@ export function buildHeuristicReplacementScenePlan(params: {
: "文字排版、标题区、分割线、留白和信息层级必须保留。",
styleSummary: isProductOnly
? "延续参考图原有风格、背景和氛围,不引入新的场景改造要求。"
: "除版式骨架外,墙面、地面、背景家具、软装、灯光、摆件和空间氛围都要按工作流风格重建。",
: shouldProtectFallbackLayout
? "延续参考图原有版式和图文语义,不引入新的空间场景。"
: "除版式骨架外,墙面、地面、背景家具、软装、灯光、摆件和空间氛围都要按工作流风格重建。",
avoidSummary: isProductOnly
? "不要改动背景、文案、按钮、图标、排版和装饰,不要重建整个场景,不要把多个产品角度同时塞进一张图。"
: "不要只换产品不换场景,不要把参考图原有背景物件和空间氛围原样保留下来,不要把所有输入产品角度图同时强行出现在一张图中。",
: shouldProtectFallbackLayout
? "不要把白底/说明/强文案页场景化,不要新增厨房/客厅/人物/软装,不要让文字和图片失配。"
: "不要只换产品不换场景,不要把参考图原有背景物件和空间氛围原样保留下来,不要把所有输入产品角度图同时强行出现在一张图中。",
generationPrompt: params.shouldReplace
? isProductOnly
? `仅替换参考图中的产品主体,严格保留其余背景、文案、排版与光影关系。${params.reason}`
: isDetailReplacement && shouldProtectFallbackLayout
? `保留参考图的版式、文案、白底/留白和图文语义,不使用前置词包;只在产品、材质、板材、原材料或视觉图片区做必要替换。${params.reason}`
: isDetailReplacement
? `保留参考图的版式、文案、人物关系、白底/留白和图文语义,不使用前置词包;只在产品、材质、板材、原材料或视觉图片区做必要替换。${params.reason}`
? `当前参考图按通用空间/场景详情页处理:必须使用前置工作流词包重建旧背景、空间、软装、灯光和产品场景,只保留参考图版式骨架、文字区、机位和信息层级。${params.reason}`
: `以工作流目标风格重建当前这一屏的场景,只保留版式、机位、文字区与留白结构。${params.reason}`
: params.reason,
analysisModelName: null,
@ -1030,7 +1054,7 @@ export function buildHeuristicReplacementScenePlan(params: {
return applyDetailReplacementScenePlanGuards(scenePlan, {
modeType: params.modeType,
hasModelImage: params.hasModelImage,
hasImportantText: isDetailReplacement,
hasImportantText: shouldProtectFallbackLayout,
containsPeople: false,
containsMultiplePeople: false,
visualProfile: params.visualProfile,

View File

@ -3399,8 +3399,14 @@ export async function POST(request: Request) {
scenePlanSource: replacementExecutionPlan.scenePlan.analysisSource,
generationRoute: replacementExecutionPlan.scenePlan.generationRoute,
promptPackPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy,
packPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy,
textPolicy: replacementExecutionPlan.scenePlan.textPolicy,
peoplePolicy: replacementExecutionPlan.scenePlan.peoplePolicy,
whiteLayout: replacementExecutionPlan.scenePlan.whiteBackgroundLikely,
copyCoupled: replacementExecutionPlan.scenePlan.copyImageCoupled,
useModelImage: replacementExecutionPlan.scenePlan.useModelImage,
sourceProductImages: productInputImageUrls.length,
sourceModelImage: Boolean(modelInputImageUrl),
});
const referenceSourceImageUrls = Array.from(
new Set([
@ -3442,6 +3448,19 @@ export async function POST(request: Request) {
reference.imageUrl,
]),
);
chatApiLogger.info("[Chat API] Direct detail replacement image generation started:", {
referenceIndex: referenceIndex + 1,
totalReferences: replacementReferences.length,
referenceLabel: reference.label,
generationRoute: replacementExecutionPlan.scenePlan.generationRoute,
packPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy,
selectedProductImages: selectedProductContextImageUrls.length,
sourceProductImages: productInputImageUrls.length,
includeModelImage: shouldIncludeModelImage,
sourceImageCount: sourceImageUrls.length,
whiteLayout: replacementExecutionPlan.scenePlan.whiteBackgroundLikely,
copyCoupled: replacementExecutionPlan.scenePlan.copyImageCoupled,
});
const replacementPrompt = buildAgentReplacementPrompt({
basePrompt: baseReplacementPrompt,
modeType: "detail_replacement",