fix: harden detail replacement page routing

This commit is contained in:
Your Name 2026-06-03 16:47:50 +08:00
parent 87f23a0ab6
commit 7ac5bdf44f
2 changed files with 492 additions and 39 deletions

View File

@ -279,9 +279,6 @@ export async function normalizeReplacementResultImage(params: {
generatedMetadata.width && generatedMetadata.height
? generatedMetadata.width / generatedMetadata.height
: null;
const shouldCropTallComposite =
Boolean(targetAspectRatio && generatedAspectRatio) &&
Number(generatedAspectRatio) < Number(targetAspectRatio) * 0.78;
const shouldFillSceneCanvas =
params.scenePlan?.promptPackPolicy !== "forbid" &&
(params.scenePlan?.generationRoute === "prompt_pack_scene" ||
@ -292,6 +289,10 @@ export async function normalizeReplacementResultImage(params: {
params.scenePlan?.textPolicy === "lock" ||
params.scenePlan?.generationRoute === "copy_locked_regenerate" ||
params.scenePlan?.generationRoute === "layout_preserve_replace");
const shouldCropTallComposite =
!shouldPreserveFullGeneratedCanvas &&
Boolean(targetAspectRatio && generatedAspectRatio) &&
Number(generatedAspectRatio) < Number(targetAspectRatio) * 0.78;
const resizeFit = shouldCropTallComposite
? "cover"
: shouldPreserveFullGeneratedCanvas
@ -343,6 +344,16 @@ export type DetailReplacementPeoplePolicy =
| "none"
| "preserve_relation"
| "require_user_model";
export type DetailReplacementPageKind =
| "prompt_pack_scene"
| "white_scene"
| "white_layout"
| "copy_locked"
| "people_scene"
| "action_poster"
| "dimension_spec"
| "material_detail"
| "unknown";
export type NormalizedAgentModeSourceImage = {
url: string;
@ -369,6 +380,7 @@ export type ReplacementReferenceDecision = {
};
export type ReplacementScenePlan = {
pageKind: DetailReplacementPageKind;
generationRoute: DetailReplacementGenerationRoute;
promptPackPolicy: DetailReplacementPromptPackPolicy;
textPolicy: DetailReplacementTextPolicy;
@ -401,10 +413,14 @@ type ReplacementReferenceDecisionCore = Omit<
const DETAIL_HARD_LAYOUT_HINT_PATTERN =
/产品尺寸|尺寸|规格|参数|单位|厘米|\bcm\b|产品说明|说明图|信息图|参数图|规格图|材质标注|工艺标注|结构标注|功能图标|图标说明|尺寸标注|承重参数|安装说明|标签图|卖点卡|白底产品|白底参数|白底规格/i;
const DETAIL_DIMENSION_LAYOUT_HINT_PATTERN =
/产品尺寸|尺寸图|尺寸说明|尺寸标注|尺寸线|标尺|长宽高|宽度|高度|深度|单位[::]?\s*(cm|厘米)|\bcm\b|160\s*\/\s*180\s*\/\s*210/i;
const DETAIL_STRONG_COPY_HINT_PATTERN =
/白底|浅色底|产品说明|参数|规格|尺寸|卖点|标签|说明图|信息图|海报|版式|排版|文字|标题|编号|标注|材质标注|工艺标注|代名词|典雅|高贵/i;
const DETAIL_REFERENCE_POSTER_ACTION_HINT_PATTERN =
/动作海报|测试海报|测试图|来张自拍|自拍|合照|多人围坐|围坐|聚餐|多人聚餐|横向受力|受力测试|单侧承重|承重测试|重心|依旧平稳|依然稳固|试坐|坐在桌|坐桌|坐上桌/i;
const DETAIL_PEOPLE_ACTION_HINT_PATTERN =
/自拍|合照|拍照|拿手机|手机自拍|聚会|好友|朋友|闺蜜|夫妻|情侣|家庭|亲子|陪伴|举杯|互动|模特|人物|人像|多人|人群|男士|女士|男人|女人|男生|女生|小孩|儿童|宝宝|顾客|吃饭的人|用餐的人|正在吃饭|坐在|坐着|站在|走动|行走|手持|试坐|倚靠|承重|受力|测试|稳定|稳固|使用产品|产品使用|体验/i;
/自拍|合照|拍照|拿手机|手机自拍|聚会|聚餐|围坐|好友|朋友|闺蜜|夫妻|情侣|家庭|亲子|陪伴|举杯|互动|模特|人物|人像|多人|人群|男士|女士|男人|女人|男生|女生|小孩|儿童|宝宝|顾客|吃饭的人|用餐的人|正在吃饭|坐在|坐着|站在|走动|行走|手持|试坐|倚靠|承重|受力|受力测试|重心|测试|稳定|稳固|使用产品|产品使用|体验/i;
export type ReplacementScenePlanningRuntime =
| {
@ -490,6 +506,20 @@ function ensureDetailReferenceGenerates<T extends ReplacementReferenceDecisionCo
scenePlan.generationRoute === "direct_passthrough"
? inferPromptPackPolicy(forcedRoute)
: scenePlan.promptPackPolicy,
pageKind:
scenePlan.generationRoute === "direct_passthrough" ||
scenePlan.pageKind === "unknown"
? inferDetailPageKind({
route: forcedRoute,
promptPackPolicy: inferPromptPackPolicy(forcedRoute),
textPolicy: scenePlan.textPolicy,
peoplePolicy: scenePlan.peoplePolicy,
whiteBackgroundLikely: scenePlan.whiteBackgroundLikely,
copyImageCoupled: scenePlan.copyImageCoupled,
hasDimensionLayoutHint: false,
hasReferencePosterActionHint: false,
})
: scenePlan.pageKind,
textPolicy: scenePlan.textPolicy === "free" ? "preserve" : scenePlan.textPolicy,
routeReason:
scenePlan.generationRoute === "direct_passthrough" || !decision.shouldReplace
@ -655,6 +685,158 @@ function normalizePeoplePolicy(value: unknown): DetailReplacementPeoplePolicy |
return null;
}
function normalizeDetailPageKind(value: unknown): DetailReplacementPageKind | null {
const normalized = String(value || "")
.trim()
.toLowerCase()
.replace(/[\s-]+/g, "_");
if (!normalized) {
return null;
}
if (
[
"prompt_pack_scene",
"generic_scene",
"scene",
"space_scene",
"场景图",
"通用场景",
"吃词包场景",
].includes(normalized)
) {
return "prompt_pack_scene";
}
if (
[
"white_scene",
"light_scene",
"bright_scene",
"白场景",
"浅色场景",
"白色真实场景",
].includes(normalized)
) {
return "white_scene";
}
if (
[
"white_layout",
"layout",
"white_background_layout",
"white_poster",
"info_layout",
"白底说明",
"白底排版",
"说明页",
].includes(normalized)
) {
return "white_layout";
}
if (
[
"copy_locked",
"copy_locked_regenerate",
"text_locked",
"text_image_coupled",
"strong_copy",
"强文案",
"图文绑定",
].includes(normalized)
) {
return "copy_locked";
}
if (
[
"people_scene",
"people_relation",
"people_relation_locked",
"model_scene",
"人物场景",
"模特场景",
].includes(normalized)
) {
return "people_scene";
}
if (
[
"action_poster",
"action_test_poster",
"test_poster",
"people_action_poster",
"动作海报",
"测试海报",
"承重海报",
].includes(normalized)
) {
return "action_poster";
}
if (
[
"dimension_spec",
"dimension_layout",
"size_spec",
"size_chart",
"product_dimension",
"尺寸说明",
"产品尺寸",
"尺寸图",
].includes(normalized)
) {
return "dimension_spec";
}
if (
[
"material_detail",
"material_layout",
"material_spec",
"材质说明",
"板材说明",
"材料细节",
].includes(normalized)
) {
return "material_detail";
}
if (["unknown", "other", "其他", "未知"].includes(normalized)) {
return "unknown";
}
return null;
}
function inferDetailPageKind(params: {
route: DetailReplacementGenerationRoute;
promptPackPolicy?: DetailReplacementPromptPackPolicy;
textPolicy?: DetailReplacementTextPolicy;
peoplePolicy?: DetailReplacementPeoplePolicy;
whiteBackgroundLikely: boolean;
copyImageCoupled: boolean;
hasDimensionLayoutHint: boolean;
hasReferencePosterActionHint: boolean;
visualProfile?: ReplacementReferenceVisualProfile | null;
}): DetailReplacementPageKind {
if (params.hasDimensionLayoutHint) {
return "dimension_spec";
}
if (params.hasReferencePosterActionHint) {
return "action_poster";
}
if (params.route === "people_relation_locked") {
return params.promptPackPolicy === "forbid" ? "action_poster" : "people_scene";
}
if (params.route === "copy_locked_regenerate" || params.copyImageCoupled) {
return "copy_locked";
}
if (params.route === "layout_preserve_replace") {
return params.whiteBackgroundLikely ? "white_layout" : "material_detail";
}
if (params.visualProfile?.likelyWhiteScene && !params.visualProfile?.likelyWhiteLayout) {
return "white_scene";
}
if (params.route === "prompt_pack_scene") {
return "prompt_pack_scene";
}
return "unknown";
}
function inferDetailGenerationRoute(params: {
shouldReplace: boolean;
isProductOnly: boolean;
@ -715,6 +897,7 @@ function inferPeoplePolicy(params: {
function planTextForPolicyGuards(scenePlan: ReplacementScenePlan) {
return [
scenePlan.pageKind,
scenePlan.sceneSummary,
scenePlan.cameraSummary,
scenePlan.placementSummary,
@ -758,11 +941,25 @@ function applyDetailReplacementScenePlanGuards(
const isProductOnly = params.replacementStrategy === "product_only";
const planText = planTextForPolicyGuards(scenePlan);
const pageKind = scenePlan.pageKind || "unknown";
const alreadyAllowsPromptPack =
scenePlan.generationRoute === "prompt_pack_scene" &&
scenePlan.promptPackPolicy !== "forbid";
const hasHardLayoutHint = DETAIL_HARD_LAYOUT_HINT_PATTERN.test(planText);
const hasPeopleActionHint = DETAIL_PEOPLE_ACTION_HINT_PATTERN.test(planText);
const shouldTrustPromptPackPageKind = pageKind === "prompt_pack_scene";
const shouldTrustPromptPackRoute =
shouldTrustPromptPackPageKind && alreadyAllowsPromptPack;
const hasDimensionLayoutHint =
pageKind === "dimension_spec" ||
(!shouldTrustPromptPackPageKind &&
DETAIL_DIMENSION_LAYOUT_HINT_PATTERN.test(planText));
const hasHardLayoutHint =
!shouldTrustPromptPackPageKind && DETAIL_HARD_LAYOUT_HINT_PATTERN.test(planText);
const hasReferencePosterActionHint =
pageKind === "action_poster" ||
(!shouldTrustPromptPackPageKind &&
DETAIL_REFERENCE_POSTER_ACTION_HINT_PATTERN.test(planText));
const hasPeopleActionHint =
!shouldTrustPromptPackPageKind && DETAIL_PEOPLE_ACTION_HINT_PATTERN.test(planText);
const hasPeopleSignals =
!isProductOnly &&
(params.containsPeople ||
@ -776,8 +973,15 @@ function applyDetailReplacementScenePlanGuards(
const visualProfileSuggestsLightBase =
params.visualProfile?.likelyWhiteScene === true ||
params.visualProfile?.likelyWhiteLayout === true;
const shouldProtectDimensionLayout = !isProductOnly && hasDimensionLayoutHint;
const shouldProtectReferencePosterAction =
!isProductOnly &&
hasPeopleSignals &&
!shouldProtectDimensionLayout &&
hasReferencePosterActionHint;
const scenePlanSuggestsWhiteLayout =
scenePlan.whiteBackgroundLikely &&
(pageKind === "white_layout" ||
scenePlan.whiteBackgroundLikely) &&
!visualProfileSuggestsWhiteScene &&
!hasPeopleSignals &&
(hasHardLayoutHint ||
@ -785,30 +989,130 @@ function applyDetailReplacementScenePlanGuards(
const shouldProtectWhiteLayout =
!isProductOnly &&
!hasPeopleSignals &&
((visualProfileSuggestsWhiteLayout &&
(hasHardLayoutHint ||
(scenePlan.copyImageCoupled && scenePlan.textPolicy === "lock"))) ||
(pageKind === "white_layout" ||
(visualProfileSuggestsWhiteLayout &&
(hasHardLayoutHint ||
(scenePlan.copyImageCoupled && scenePlan.textPolicy === "lock"))) ||
scenePlanSuggestsWhiteLayout ||
(hasHardLayoutHint && !visualProfileSuggestsWhiteScene));
const shouldProtectWhiteScene =
!isProductOnly &&
!shouldProtectWhiteLayout &&
visualProfileSuggestsLightBase;
(pageKind === "white_scene" || visualProfileSuggestsLightBase);
const shouldProtectPeople =
!isProductOnly &&
(hasPeopleSignals || (!alreadyAllowsPromptPack && scenePlan.peoplePolicy !== "none"));
const shouldLockCopy =
!isProductOnly &&
(scenePlan.copyImageCoupled ||
params.hasImportantText ||
scenePlan.textPolicy === "lock" ||
hasHardLayoutHint);
pageKind === "copy_locked" ||
shouldProtectDimensionLayout ||
(!shouldTrustPromptPackRoute &&
(params.hasImportantText ||
scenePlan.textPolicy === "lock" ||
hasHardLayoutHint)));
let next: ReplacementScenePlan = { ...scenePlan };
if (shouldProtectPeople && shouldProtectWhiteLayout) {
if (shouldProtectDimensionLayout) {
next = {
...next,
pageKind: "dimension_spec",
generationRoute: "layout_preserve_replace",
promptPackPolicy: "forbid",
textPolicy: "lock",
peoplePolicy: "none",
useModelImage: false,
modelImageReason: params.hasModelImage
? "产品尺寸说明页不使用模特图,必须以产品抠图和尺寸标注为主。"
: next.modelImageReason,
routeReason: appendUniqueInstruction(
next.routeReason,
"命中产品尺寸/单位/尺寸线说明页,按白底抠图尺寸标注处理,禁止套用场景词包。",
240,
),
sceneSummary: appendUniqueInstruction(
next.sceneSummary,
"产品主体应抠出在白底或米白底上展示,保留产品外轮廓、比例、透视角度和尺寸标注关系。",
260,
),
generationPrompt: appendUniqueInstruction(
next.generationPrompt,
"本屏是产品尺寸说明图:不使用前置词包;必须把产品主体抠出放在干净白底/米白底上,保留标题“产品尺寸”、单位 cm、尺寸箭头/尺寸线/数字标注和原版式位置;产品形状、桌面比例、边角圆角、腿部结构、透视角度必须跟参考图一致;禁止生成厨房、餐桌椅组合、室内背景、人物、软装或多余道具。",
),
avoidSummary: appendUniqueInstruction(
next.avoidSummary,
"不要把尺寸页做成室内场景,不要新增椅子、柜体、灯具、花瓶或人物,不要改变产品形状,不要丢失尺寸线和单位。",
320,
),
whiteBackgroundLikely: true,
copyImageCoupled: true,
};
} else if (shouldProtectReferencePosterAction) {
next = {
...next,
pageKind: "action_poster",
generationRoute: "people_relation_locked",
promptPackPolicy: "forbid",
textPolicy: next.textPolicy === "free" ? "lock" : next.textPolicy,
peoplePolicy: "preserve_relation",
useModelImage: params.hasModelImage,
modelImageReason: params.hasModelImage
? next.modelImageReason ||
"参考图是带明确人物动作的详情海报,模特图只用于人物身份/气质参考,不能覆盖参考图人数、动作和版式。"
: next.modelImageReason ||
"参考图是带明确人物动作的详情海报,必须按参考图人数、动作、姿态和版式生成类似图。",
routeReason: appendUniqueInstruction(
next.routeReason,
"命中多人围坐/承重测试等参考动作海报,参考图语义优先,禁止套用前置场景词包。",
240,
),
sceneSummary: appendUniqueInstruction(
next.sceneSummary,
"按参考图做同版式类似图:保留人物数量、围坐/试坐/承重动作、产品形状、机位和图文关系,只做必要产品/材质/视觉区替换。",
320,
),
generationPrompt: appendUniqueInstruction(
next.generationPrompt,
"本屏是参考动作海报,不使用前置词包;标题/标签中的“多人围坐、聚餐、横向受力测试、单侧承重、重心平稳”等语义是硬约束。必须根据参考图做一张同版式、同动作、同人物关系的类似详情图:多人就保持多人围坐,承重就保持人物坐在产品上的受力姿态;产品主体的形状、外轮廓、长宽比例、桌面圆角、腿部结构和透视角度必须跟参考图一致。禁止生成无关餐厨空间、走动路人、无人空场景或改成另一种产品形状。",
),
avoidSummary: appendUniqueInstruction(
next.avoidSummary,
"不要让前置厨房/餐厨/软装词包覆盖参考动作,不要把多人改成单人或无人,不要把坐姿/承重动作改成走动人物,不要改变产品形状。",
340,
),
whiteBackgroundLikely: next.whiteBackgroundLikely || visualProfileSuggestsLightBase,
copyImageCoupled: true,
};
} else if (
pageKind === "prompt_pack_scene" &&
!shouldProtectWhiteLayout &&
!shouldProtectPeople &&
!shouldLockCopy
) {
next = {
...next,
pageKind: "prompt_pack_scene",
generationRoute: "prompt_pack_scene",
promptPackPolicy: "allow",
textPolicy: next.textPolicy === "lock" ? "preserve" : next.textPolicy,
peoplePolicy: "none",
useModelImage: false,
copyImageCoupled: false,
routeReason: appendUniqueInstruction(
next.routeReason,
"多模态已判定为普通可吃词包场景页,纠正为使用前置词包重建场景。",
240,
),
generationPrompt: appendUniqueInstruction(
next.generationPrompt,
"本屏是普通空间/场景详情页:必须使用前置工作流词包重建图片框内旧背景、产品、空间、软装和灯光,同时保留参考图版式骨架、文字区、机位和信息层级。",
),
};
} else if (shouldProtectPeople && shouldProtectWhiteLayout) {
next = {
...next,
pageKind: "white_layout",
generationRoute: "layout_preserve_replace",
promptPackPolicy: "forbid",
textPolicy: next.textPolicy === "free" ? "preserve" : next.textPolicy,
@ -839,6 +1143,7 @@ function applyDetailReplacementScenePlanGuards(
} else if (shouldProtectPeople && shouldProtectWhiteScene) {
next = {
...next,
pageKind: "white_scene",
generationRoute: "people_relation_locked",
promptPackPolicy: "soft",
textPolicy: next.textPolicy === "free" ? "preserve" : next.textPolicy,
@ -868,6 +1173,7 @@ function applyDetailReplacementScenePlanGuards(
} else if (shouldProtectPeople) {
next = {
...next,
pageKind: "people_scene",
generationRoute: "people_relation_locked",
promptPackPolicy: "allow",
textPolicy: next.textPolicy === "free" ? "preserve" : next.textPolicy,
@ -896,7 +1202,12 @@ function applyDetailReplacementScenePlanGuards(
} else if (shouldLockCopy) {
next = {
...next,
generationRoute: shouldProtectWhiteLayout
pageKind: pageKind === "copy_locked"
? "copy_locked"
: shouldProtectWhiteLayout
? "white_layout"
: "copy_locked",
generationRoute: shouldProtectWhiteLayout && pageKind !== "copy_locked"
? "layout_preserve_replace"
: "copy_locked_regenerate",
promptPackPolicy: "forbid",
@ -905,14 +1216,14 @@ function applyDetailReplacementScenePlanGuards(
useModelImage: false,
routeReason: appendUniqueInstruction(
next.routeReason,
shouldProtectWhiteLayout
shouldProtectWhiteLayout && pageKind !== "copy_locked"
? "命中白底/说明图或强文案版式,禁止套用场景词包。"
: "命中强图文绑定,禁止套用场景词包。",
220,
),
generationPrompt: appendUniqueInstruction(
next.generationPrompt,
shouldProtectWhiteLayout
shouldProtectWhiteLayout && pageKind !== "copy_locked"
? "本屏按白底产品说明/参数排版处理:严格保留白底、留白、标题、标签、参数、文字颜色层级和产品位置,只做必要的产品/材质/视觉区替换,不允许生成室内场景、厨房、客厅、人物或软装背景。"
: "本屏文案与画面强绑定:文字内容、颜色、位置、层级和对应画面语义必须一致,只做必要局部替换,不允许按前置词包改造成另一套场景。",
),
@ -927,6 +1238,7 @@ function applyDetailReplacementScenePlanGuards(
} else if (shouldProtectWhiteLayout) {
next = {
...next,
pageKind: "white_layout",
generationRoute: "layout_preserve_replace",
promptPackPolicy: "forbid",
textPolicy: next.textPolicy === "free" ? "preserve" : next.textPolicy,
@ -945,6 +1257,7 @@ function applyDetailReplacementScenePlanGuards(
} else if (shouldProtectWhiteScene) {
next = {
...next,
pageKind: "white_scene",
generationRoute: "prompt_pack_scene",
promptPackPolicy: "soft",
textPolicy: next.textPolicy === "free" ? "preserve" : next.textPolicy,
@ -1064,9 +1377,49 @@ export function parseReplacementScenePlan(
parsed.generationPrompt || parsed.recommendedScenePrompt || parsed.scenePrompt,
1200,
);
const rawPlanText = [
parsed.pageKind,
parsed.detailPageKind,
parsed.pageType,
parsed.referencePageKind,
parsed.routeReason,
parsed.strategyReason,
parsed.policyReason,
parsed.reason,
parsed.sceneSummary,
parsed.sceneDescription,
parsed.preserveSummary,
parsed.mustKeep,
parsed.styleSummary,
parsed.avoidSummary,
generationPrompt,
]
.filter(Boolean)
.join(" ");
const pageKind =
normalizeDetailPageKind(
parsed.pageKind ||
parsed.detailPageKind ||
parsed.pageType ||
parsed.referencePageKind,
) ||
inferDetailPageKind({
route: generationRoute,
promptPackPolicy,
textPolicy,
peoplePolicy,
whiteBackgroundLikely,
copyImageCoupled,
hasDimensionLayoutHint:
DETAIL_DIMENSION_LAYOUT_HINT_PATTERN.test(rawPlanText),
hasReferencePosterActionHint:
DETAIL_REFERENCE_POSTER_ACTION_HINT_PATTERN.test(rawPlanText),
visualProfile: params.visualProfile,
});
const productImageSelection = buildDefaultProductImageSelection(params.productImageCount);
let scenePlan: ReplacementScenePlan = {
pageKind,
generationRoute,
promptPackPolicy,
textPolicy,
@ -1156,9 +1509,23 @@ export function buildHeuristicReplacementScenePlan(params: {
const isProductOnly = params.replacementStrategy === "product_only";
const productImageSelection = buildDefaultProductImageSelection(params.productImageCount);
const fallbackText = String(params.reason || "");
const fallbackHasDimensionLayout =
DETAIL_DIMENSION_LAYOUT_HINT_PATTERN.test(fallbackText);
const fallbackHasReferencePosterAction =
DETAIL_REFERENCE_POSTER_ACTION_HINT_PATTERN.test(fallbackText);
const fallbackHasPeopleAction = DETAIL_PEOPLE_ACTION_HINT_PATTERN.test(fallbackText);
const fallbackHasHardLayout = DETAIL_HARD_LAYOUT_HINT_PATTERN.test(fallbackText);
const shouldProtectFallbackWhiteLayout =
isDetailReplacement &&
!isProductOnly &&
fallbackHasDimensionLayout;
const shouldProtectFallbackReferencePosterAction =
isDetailReplacement &&
!isProductOnly &&
!shouldProtectFallbackWhiteLayout &&
fallbackHasPeopleAction &&
fallbackHasReferencePosterAction;
const shouldProtectFallbackCopyLayout =
isDetailReplacement &&
!isProductOnly &&
!fallbackHasPeopleAction &&
@ -1169,16 +1536,19 @@ export function buildHeuristicReplacementScenePlan(params: {
isDetailReplacement &&
!isProductOnly &&
!shouldProtectFallbackWhiteLayout &&
!shouldProtectFallbackReferencePosterAction &&
(fallbackHasPeopleAction ||
Boolean(params.visualProfile?.likelyWhiteScene) ||
Boolean(params.visualProfile?.likelyWhiteLayout));
const shouldProtectFallbackLayout =
shouldProtectFallbackWhiteLayout;
shouldProtectFallbackWhiteLayout || shouldProtectFallbackCopyLayout;
const generationRoute = params.shouldReplace
? isProductOnly
? "layout_preserve_replace"
: isDetailReplacement
? fallbackHasPeopleAction
? shouldProtectFallbackWhiteLayout
? "layout_preserve_replace"
: fallbackHasPeopleAction
? "people_relation_locked"
: shouldProtectFallbackLayout
? "layout_preserve_replace"
@ -1187,7 +1557,9 @@ export function buildHeuristicReplacementScenePlan(params: {
: "direct_passthrough";
const promptPackPolicy: DetailReplacementPromptPackPolicy =
params.shouldReplace && isDetailReplacement && !isProductOnly
? fallbackHasPeopleAction
? shouldProtectFallbackWhiteLayout || shouldProtectFallbackReferencePosterAction
? "forbid"
: fallbackHasPeopleAction
? shouldRebuildFallbackWhiteScene
? "soft"
: "allow"
@ -1195,12 +1567,35 @@ export function buildHeuristicReplacementScenePlan(params: {
? "soft"
: inferPromptPackPolicy(generationRoute)
: inferPromptPackPolicy(generationRoute);
const fallbackPageKind = inferDetailPageKind({
route: generationRoute,
promptPackPolicy,
textPolicy:
shouldProtectFallbackWhiteLayout || shouldProtectFallbackReferencePosterAction
? "lock"
: fallbackHasPeopleAction || shouldRebuildFallbackWhiteScene
? "preserve"
: "free",
peoplePolicy: fallbackHasPeopleAction ? "preserve_relation" : "none",
whiteBackgroundLikely: Boolean(
params.visualProfile?.likelyWhiteLayout || params.visualProfile?.likelyWhiteScene,
),
copyImageCoupled: false,
hasDimensionLayoutHint: fallbackHasDimensionLayout,
hasReferencePosterActionHint: fallbackHasReferencePosterAction,
visualProfile: params.visualProfile,
});
const scenePlan: ReplacementScenePlan = {
pageKind: fallbackPageKind,
generationRoute,
promptPackPolicy,
textPolicy:
generationRoute === "direct_passthrough" || shouldProtectFallbackLayout
? "preserve"
? shouldProtectFallbackWhiteLayout
? "lock"
: "preserve"
: shouldProtectFallbackReferencePosterAction
? "lock"
: fallbackHasPeopleAction || shouldRebuildFallbackWhiteScene
? "preserve"
: "free",
@ -1213,7 +1608,11 @@ export function buildHeuristicReplacementScenePlan(params: {
? isProductOnly
? "兜底按仅替换产品执行,不使用词包。"
: isDetailReplacement
? fallbackHasPeopleAction
? shouldProtectFallbackWhiteLayout
? "兜底命中产品尺寸说明页,按白底抠图和尺寸标注执行,不使用词包。"
: shouldProtectFallbackReferencePosterAction
? "兜底命中参考动作海报,保留人物动作和产品形状,不使用词包。"
: fallbackHasPeopleAction
? "兜底命中人物/动作/使用场景,按人物关系锁定并允许词包重建背景和产品场景。"
: shouldProtectFallbackLayout
? "兜底命中白底说明/强文案版式,按详情版式保护执行,不使用词包。"
@ -1233,6 +1632,10 @@ export function buildHeuristicReplacementScenePlan(params: {
sceneSummary: params.shouldReplace
? isProductOnly
? "严格保留参考图的场景、文案和版式,只把产品展示区中的旧产品替换成输入产品。"
: shouldProtectFallbackWhiteLayout
? "产品主体抠出在白底或米白底中展示,保留产品尺寸标题、单位、尺寸线、尺寸数字和原版式关系。"
: shouldProtectFallbackReferencePosterAction
? "按参考图动作海报做类似图,保留人物数量、动作、产品形状、机位、标题标签和图文关系,不套用前置词包。"
: fallbackHasPeopleAction
? "保留参考图人物数量、动作、互动关系、文字层级和版式骨架,同时按工作流目标重建背景、产品、软装和灯光。"
: shouldProtectFallbackLayout
@ -1247,9 +1650,17 @@ export function buildHeuristicReplacementScenePlan(params: {
: "产品应落在参考图当前主体所在区域,并围绕该区域同步重建新的场景内容。",
preserveSummary: isProductOnly
? "除产品主体外,文字排版、标题区、分割线、留白、背景、灯光和装饰都必须保留。"
: shouldProtectFallbackWhiteLayout
? "标题、单位、尺寸线、数字标注、白底留白、产品外轮廓、比例和透视必须保留。"
: shouldProtectFallbackReferencePosterAction
? "标题标签、人物数量、动作姿态、产品外轮廓、长宽比例、腿部结构、机位和版式必须保留。"
: "文字排版、标题区、分割线、留白和信息层级必须保留。",
styleSummary: isProductOnly
? "延续参考图原有风格、背景和氛围,不引入新的场景改造要求。"
: shouldProtectFallbackWhiteLayout
? "白底产品尺寸说明图,只允许产品抠图和尺寸标注,不引入室内场景。"
: shouldProtectFallbackReferencePosterAction
? "参考图动作海报语义优先,做同动作同版式类似图,不引入前置空间词包。"
: fallbackHasPeopleAction
? "人物动作和关系保真,背景、产品场景、软装和灯光按前置词包明显重建。"
: shouldProtectFallbackLayout
@ -1259,6 +1670,10 @@ export function buildHeuristicReplacementScenePlan(params: {
: "除版式骨架外,墙面、地面、背景家具、软装、灯光、摆件和空间氛围都要按工作流风格重建。",
avoidSummary: isProductOnly
? "不要改动背景、文案、按钮、图标、排版和装饰,不要重建整个场景,不要把多个产品角度同时塞进一张图。"
: shouldProtectFallbackWhiteLayout
? "不要生成厨房、餐椅、柜体、灯具、花瓶、人物或软装背景,不要改变产品形状,不要丢尺寸线和单位。"
: shouldProtectFallbackReferencePosterAction
? "不要把多人改成单人或无人,不要把坐姿/承重动作改成走动人物,不要套用厨房/餐厨词包,不要改产品形状。"
: fallbackHasPeopleAction
? "不要生成白底单品图,不要删掉人物,不要把多人压成单人,不要只小幅复刻原图,也不要破坏文字版式。"
: shouldProtectFallbackLayout
@ -1269,6 +1684,10 @@ export function buildHeuristicReplacementScenePlan(params: {
generationPrompt: params.shouldReplace
? isProductOnly
? `仅替换参考图中的产品主体,严格保留其余背景、文案、排版与光影关系。${params.reason}`
: isDetailReplacement && shouldProtectFallbackWhiteLayout
? `当前参考图按产品尺寸说明页处理:不使用前置词包;必须把产品主体抠出放在白底/米白底上,保留标题“产品尺寸”、单位 cm、尺寸箭头/尺寸线/数字标注和原版式位置;禁止生成室内场景、人物、椅子、柜体、灯具、花瓶或软装背景;产品形状、桌面比例、腿部结构、圆角和透视必须跟参考图一致。${params.reason}`
: isDetailReplacement && shouldProtectFallbackReferencePosterAction
? `当前参考图按动作海报处理:不使用前置词包;必须根据参考图做同版式类似图,保留标题标签、人物数量、多人围坐/试坐/单侧承重/受力测试等动作、产品外轮廓、长宽比例、桌面圆角、腿部结构、透视角度和机位;禁止生成无关餐厨空间、走动人物、无人空场景或另一种产品形状。${params.reason}`
: isDetailReplacement && fallbackHasPeopleAction
? `当前参考图按人物/动作详情页处理:必须保留参考图中的人物数量、动作、朝向、互动关系和出镜比例,同时使用前置工作流词包明显重建图片框内旧背景、产品、空间、软装和灯光;禁止生成白底单品图或无人物空场景,保留文字区、版式骨架、机位和信息层级。${params.reason}`
: isDetailReplacement && shouldProtectFallbackLayout
@ -1288,7 +1707,8 @@ export function buildHeuristicReplacementScenePlan(params: {
hasModelImage: params.hasModelImage,
hasImportantText: shouldProtectFallbackLayout,
containsPeople: false,
containsMultiplePeople: fallbackHasPeopleAction,
containsMultiplePeople:
fallbackHasPeopleAction || shouldProtectFallbackReferencePosterAction,
visualProfile: params.visualProfile,
replacementStrategy: params.replacementStrategy,
});
@ -1768,7 +2188,7 @@ export async function analyzeReplacementExecutionPlan(params: {
? `像素初筛:nearWhite=${referenceVisualProfile.nearWhitePixelRatio.toFixed(2)}, lightLowSaturation=${referenceVisualProfile.lightLowSaturationPixelRatio.toFixed(2)}, topLight=${referenceVisualProfile.topLightLowSaturationPixelRatio.toFixed(2)}, borderLight=${referenceVisualProfile.borderLightLowSaturationPixelRatio.toFixed(2)}, dark=${referenceVisualProfile.darkPixelRatio.toFixed(2)}, likelyWhiteLayout=${referenceVisualProfile.likelyWhiteLayout ? "true" : "false"}, likelyWhiteScene=${referenceVisualProfile.likelyWhiteScene ? "true" : "false"}。${referenceVisualProfile.reason}`
: "",
referenceVisualProfile?.likelyWhiteLayout
? "像素初筛已经命中白底/浅色说明页:只要画面存在标题、标签、参数、尺寸、卖点、图标、白底图片区、圆角图片区或明显留白排版,generationRoute 必须是 layout_preserve_replace 或 copy_locked_regenerate,promptPackPolicy 必须 forbid;即使包含人物/动作,也只能保留人物关系与白底版式,不允许套用厨房、客厅、灯光、软装等场景词包。"
? "像素初筛已经命中白底/浅色说明页候选:请继续读取画面文字和图像语义。若存在标题、标签、参数、尺寸、卖点、图标、白底图片区、圆角图片区、人物动作海报或明显留白排版,generationRoute 必须是 layout_preserve_replace、copy_locked_regenerate 或 people_relation_locked,promptPackPolicy 必须 forbid;人物动作只能按参考图保留,不允许套用厨房、客厅、灯光、软装等场景词包。"
: "",
!referenceVisualProfile?.likelyWhiteLayout && referenceVisualProfile?.likelyWhiteScene
? "像素初筛已经命中白/浅色低饱和真实场景:这不是白底说明页。若画面存在真实空间、人物或产品照片,promptPackPolicy 应为 soft 或 allow,必须使用前置工作流词包重建图片框内产品、空间、软装和灯光,使差异清晰可见;但最终仍保持白/浅色低饱和基调,不要改造成深色厨房、黑墙、复杂吊灯或强软装。"
@ -1792,6 +2212,12 @@ export async function analyzeReplacementExecutionPlan(params: {
: isDetailReplacement
? "这张参考图可能是场景图、白底说明图、强文案图、人物关系图或纯版式图。你必须先判断它适合哪一种执行路由,再决定是否允许前置工作流词包参与。文字、标题、卖点标签、UI图标、按钮、分割线、品牌标识、留白和排版必须保留。"
: "这张参考图不是要求原样保留的场景照片。原图里的沙发、床、柜体、墙面、地面、窗帘、灯具、摆件、桌面道具、背景家具和环境光氛围,都可以在后续生图阶段按前置工作流风格重建。",
isDetailReplacement
? "重要分流例子:如果标题/标签或画面写着“多人围坐、聚餐、横向受力测试、单侧承重、重心依旧平稳、桌体依然稳固”等动作海报语义,这不是通用 prompt pack 场景图;必须 people_relation_locked + promptPackPolicy=forbid,根据参考图做同版式同动作类似图。"
: "",
isDetailReplacement
? "重要分流例子:如果标题或标注写着“产品尺寸、尺寸、单位 cm、160/180/210、90、75、尺寸线、尺寸箭头”等产品尺寸说明语义,必须 layout_preserve_replace + promptPackPolicy=forbid;后续应生成白底/米白底产品抠图和尺寸标注,不允许生成室内场景、椅子、人物、柜体、灯具或软装背景。"
: "",
"请完成以下任务:",
isProductOnlyDetailReplacement
? "1. shouldReplace 固定为 true:详情替换只允许规划处理范围,不允许判定直接沿用。若没有清晰产品主体,也要继续寻找产品展示区、材质/板材/原材料视觉区或可局部处理区域。"
@ -1799,20 +2225,23 @@ export async function analyzeReplacementExecutionPlan(params: {
? "1. shouldReplace 固定为 true:除非用户已经在前置流程人工勾选不处理(此处不会进入分析),你不能返回 false。若没有产品,检查人物/模特;没有人物,检查板材/材质/原材料、背景图或其它视觉区。"
: "1. 判断 shouldReplace:当前参考图里是否存在清晰、主要、适合被替换的家具商品主体。",
"2. 判断 useModelImage,并说明原因。",
isDetailReplacement
? "2a. 先输出 pageKind,这是后续策略的核心分类:prompt_pack_scene=普通可吃词包空间/场景页;white_scene=白/浅色真实场景且可软吃词包;white_layout=白底说明/纯排版/卖点参数页;copy_locked=文案和图强绑定页;people_scene=普通人物/模特真实场景,可保留人物并软吃词包;action_poster=文字或画面明确要求多人围坐、自拍、聚餐、试坐、承重、受力测试等动作的海报页,参考图动作优先;dimension_spec=产品尺寸/单位/尺寸线/标注页;material_detail=材质/板材/原材料说明页。"
: "",
isDetailReplacement
? "3. 判断 generationRoute:prompt_pack_scene=通用场景图且可吃词包;layout_preserve_replace=锁定版式/白底/产品说明/材质说明,只做有限替换;copy_locked_regenerate=文案与画面强绑定,必须锁文案;people_relation_locked=多人/模特/互动关系图,必须锁人物关系。详情替换不允许 direct_passthrough。"
: "3. 提炼 sceneSummary、cameraSummary、placementSummary、preserveSummary、styleSummary、avoidSummary。",
isDetailReplacement
? "4. 判断 promptPackPolicy:纯白底参数/说明/卖点页、强图文绑定且没有真实展示动作的详情页必须 forbid;通用空间场景必须 allow;人物/自拍/聚会/承重测试/使用产品等动作展示页可以 people_relation_locked + allow/soft,含义是保留人物动作与文字层级,但允许前置词包重建背景、灯光、软装和产品场景。"
? "4. 判断 promptPackPolicy:纯白底参数/说明/卖点页、产品尺寸页、强图文绑定页、以及标题/标签已经明确动作的详情海报页必须 forbid;通用空间场景必须 allow;只有没有强文案/海报动作绑定的真实人物生活场景,才可以 people_relation_locked + allow/soft。"
: "",
isDetailReplacement
? "4a. 白底/白场景分流规则:白底参数、规格、卖点、强文案、图标说明或纯版式页必须 forbid;白/浅色真实场景页不能 forbid,应使用 soft/allow 词包重造图片框内产品、空间、软装和灯光,但保持白/浅色低饱和基调,禁止变成深色厨房、黑墙、复杂吊灯或强软装。"
? "4a. 白底/白场景分流规则:白底参数、规格、卖点、产品尺寸、强文案、图标说明、纯版式页或动作测试海报必须 forbid;白/浅色真实场景页只有在没有这些强绑定文字时才使用 soft/allow 词包重造图片框内产品、空间、软装和灯光。"
: "",
isDetailReplacement
? "5. 判断 textPolicy:画面中存在营销标题、卖点、编号、参数、标签且与图匹配时必须 lock 或 preserve,避免后续生图改字、混色或重排。"
: "",
isDetailReplacement
? "6. 判断 peoplePolicy:有人物/多人/自拍/聚会/模特互动/拿手机/用餐动作时必须 preserve_relation;如果文字明确写了“来张自拍、合照、聚会、用餐、互动”等动作,generationPrompt 必须把这个动作写成硬约束;如果只提供 1 张模特图但参考图多人,仍要保留参考图人数和关系,额外人物按参考图生成相近人群。"
? "6. 判断 peoplePolicy:有人物/多人/自拍/聚会/围坐/聚餐/模特互动/拿手机/用餐/试坐/承重动作时必须 preserve_relation;如果文字明确写了“多人围坐、聚餐、合照、用餐、单侧承重、横向受力测试、拿手机”等动作,generationPrompt 必须把这个动作写成硬约束;如果只提供 1 张模特图但参考图多人,仍要保留参考图人数和关系,额外人物按参考图生成相近人群。"
: "",
isDetailReplacement
? "7. 提炼 sceneSummary、cameraSummary、placementSummary、preserveSummary、styleSummary、avoidSummary。"
@ -1820,7 +2249,7 @@ export async function analyzeReplacementExecutionPlan(params: {
isProductOnlyDetailReplacement
? "4. 输出 generationPrompt:这是一段给后续生图模型的本屏生成指令,要求只替换产品主体,保留其余背景、文案、排版、装饰和光影完全不变。"
: isDetailReplacement
? "8. 输出 generationPrompt:这是一段给后续生图模型的本屏生成指令。若 promptPackPolicy=forbid,必须明确“不使用前置词包,参考图语义优先”;若 promptPackPolicy=allow/soft 且 peoplePolicy=preserve_relation,必须明确“保留人物数量/动作/关系与文字层级,同时使用前置词包重建背景/产品场景”;若 textPolicy=lock,必须明确文字内容、颜色、层级和位置不可改。"
? "8. 输出 generationPrompt:这是一段给后续生图模型的本屏生成指令。若 promptPackPolicy=forbid,必须明确“不使用前置词包,参考图语义优先”;若是动作海报,必须写清人物数量、围坐/试坐/承重等动作、产品形状和版式必须照参考图;若是产品尺寸页,必须写清白底抠图、尺寸线、单位和数字标注,不允许室内场景;若 promptPackPolicy=allow/soft 且 peoplePolicy=preserve_relation,才明确“保留人物数量/动作/关系与文字层级,同时使用前置词包重建背景/产品场景”。"
: "4. 输出 generationPrompt:这是一段给后续生图模型的本屏生成指令,描述这一屏需要的场景、机位、主体位置、周边环境重建方向以及必须保留的内容。",
isProductOnlyDetailReplacement
? "5. sceneSummary、styleSummary 和 generationPrompt 必须强调‘只替换产品,不动其他内容’,不要引导重建场景。"
@ -1842,7 +2271,7 @@ export async function analyzeReplacementExecutionPlan(params: {
: "",
"只返回严格 JSON,不要任何额外解释。",
`JSON 格式:
{"shouldReplace":true,"containsFurnitureProduct":true,"reason":"不超过30字","generationRoute":"${isDetailReplacement ? "prompt_pack_scene|layout_preserve_replace|copy_locked_regenerate|people_relation_locked" : "prompt_pack_scene|layout_preserve_replace|copy_locked_regenerate|people_relation_locked|direct_passthrough"}","promptPackPolicy":"allow|soft|forbid","textPolicy":"free|preserve|lock","peoplePolicy":"none|preserve_relation|require_user_model","whiteBackgroundLikely":false,"copyImageCoupled":false,"containsPeople":false,"containsMultiplePeople":false,"routeReason":"不超过50字","useModelImage":false,"modelImageReason":"不超过40字","sceneSummary":"不超过80字","cameraSummary":"不超过60字","placementSummary":"不超过60字","preserveSummary":"不超过80字","styleSummary":"不超过80字","avoidSummary":"不超过80字","generationPrompt":"不超过300字"}`,
{"shouldReplace":true,"containsFurnitureProduct":true,"reason":"不超过30字","pageKind":"prompt_pack_scene|white_scene|white_layout|copy_locked|people_scene|action_poster|dimension_spec|material_detail|unknown","generationRoute":"${isDetailReplacement ? "prompt_pack_scene|layout_preserve_replace|copy_locked_regenerate|people_relation_locked" : "prompt_pack_scene|layout_preserve_replace|copy_locked_regenerate|people_relation_locked|direct_passthrough"}","promptPackPolicy":"allow|soft|forbid","textPolicy":"free|preserve|lock","peoplePolicy":"none|preserve_relation|require_user_model","whiteBackgroundLikely":false,"copyImageCoupled":false,"containsPeople":false,"containsMultiplePeople":false,"routeReason":"不超过50字","useModelImage":false,"modelImageReason":"不超过40字","sceneSummary":"不超过80字","cameraSummary":"不超过60字","placementSummary":"不超过60字","preserveSummary":"不超过80字","styleSummary":"不超过80字","avoidSummary":"不超过80字","generationPrompt":"不超过300字"}`,
]
.filter(Boolean)
.join("\n");
@ -1873,14 +2302,14 @@ export async function analyzeReplacementExecutionPlan(params: {
const parsedDecision = parseReplacementReferenceDecision(text);
const parsedScenePlan = parseReplacementScenePlan(text, {
productImageCount: params.productImageUrls.length,
hasModelImage: isProductOnlyDetailReplacement
? false
: Boolean(params.modelImageUrl),
modeType: params.modeType,
replacementStrategy: params.replacementStrategy,
visualProfile: referenceVisualProfile,
});
productImageCount: params.productImageUrls.length,
hasModelImage: isProductOnlyDetailReplacement
? false
: Boolean(params.modelImageUrl),
modeType: params.modeType,
replacementStrategy: params.replacementStrategy,
visualProfile: referenceVisualProfile,
});
if (parsedDecision && parsedScenePlan) {
const normalizedPlan = ensureDetailReferenceGenerates(
@ -2001,6 +2430,17 @@ export function buildAgentReplacementPrompt(params: {
replacementStrategy: params.replacementStrategy,
scenePlan: params.scenePlan,
});
const detailScenePlanText = params.scenePlan
? planTextForPolicyGuards(params.scenePlan)
: "";
const isDimensionDetailPlan =
params.modeType === "detail_replacement" &&
(params.scenePlan?.pageKind === "dimension_spec" ||
DETAIL_DIMENSION_LAYOUT_HINT_PATTERN.test(detailScenePlanText));
const isReferencePosterActionPlan =
params.modeType === "detail_replacement" &&
(params.scenePlan?.pageKind === "action_poster" ||
DETAIL_REFERENCE_POSTER_ACTION_HINT_PATTERN.test(detailScenePlanText));
if (params.productImageCount > 0) {
const productRangeLabel =
@ -2046,6 +2486,7 @@ export function buildAgentReplacementPrompt(params: {
const scenePlanLines = params.scenePlan
? [
"【本屏场景预分析】",
`页面类型:${params.scenePlan.pageKind}`,
`自动路由:${params.scenePlan.generationRoute}`,
`词包策略:${params.scenePlan.promptPackPolicy}`,
`文案策略:${params.scenePlan.textPolicy}`,
@ -2147,6 +2588,12 @@ export function buildAgentReplacementPrompt(params: {
...marketplaceBrandRemovalLines,
"你现在做的是“详情参考图背景/场景图替换”,不是重新设计整张详情图。",
"【版式锁层规则】把最后一张参考图当成 PSD 模板:文字、标题、编号、卖点标签、按钮、图标、白底/浅色底、留白、分割线、圆角图片框、图片框裁切边界都属于锁定版式层,不允许移动、缩放、重排或重新设计。",
isReferencePosterActionPlan
? "【参考动作海报硬约束】本屏不使用前置词包;必须按照最后一张参考图做同版式类似图。若标题/标签写多人围坐、聚餐、横向受力测试、单侧承重、重心平稳等,人物数量、相对位置、围坐/试坐/承重动作、产品受力关系、产品外轮廓、桌面长宽比例、圆角、腿部结构和透视角度都必须跟参考图一致。"
: "",
isDimensionDetailPlan
? "【产品尺寸图硬约束】本屏不使用前置词包;必须是白底/米白底产品抠图尺寸说明页。保留标题“产品尺寸”、单位 cm、尺寸线/箭头/数字标注和原版式位置;产品主体外轮廓、长宽比例、圆角、腿部结构和透视必须跟参考图一致;禁止生成室内场景、椅子、柜体、灯具、花瓶、人物或软装背景。"
: "",
"如果参考图是“上方文字/标签白底区 + 下方图片/场景圆角框”的结构,必须原样保留标题区高度、文字换行、文字字号、文字左上角位置、标签位置、图片框起始高度、圆角、左右边距和上下留白;禁止把新场景铺到标题白底区背后。",
"如果参考图里存在嵌入式图片框、产品照片框、人物展示框或圆角大图区域,只能在该原始框内重建背景/产品/人物画面;新画面必须被裁切在原图片框边界内,不能扩大图片框,不能覆盖文字区、标签区或外层白底。",
"版式层优先级高于词包:前置词包只负责图片框内部的空间风格、灯光、软装、背景和产品场景,不授权修改页面排版、文字位置、标签样式、留白比例或视觉层级。",
@ -2175,7 +2622,7 @@ export function buildAgentReplacementPrompt(params: {
params.scenePlan?.peoplePolicy === "require_user_model"
? "本屏包含人物但没有提供用户模特图:禁止把参考图中的人物身份当作用户模特迁移;只能保留人物数量、关系、动作和构图语义,不要换成不相关强人物主体。"
: params.scenePlan?.peoplePolicy === "preserve_relation"
? "本屏包含人物关系:必须保留人物数量、相对位置、互动关系、动作方向和出镜比例;如果参考图文字写了自拍/合照/聚会/用餐/拿手机等动作,必须让人物执行该动作,不要把多人图改成单人图、无人图或无关模特图。"
? "本屏包含人物关系:必须保留人物数量、相对位置、互动关系、动作方向和出镜比例;如果参考图文字写了自拍/合照/多人围坐/聚餐/用餐/单侧承重/横向受力测试/拿手机等动作,必须让人物执行该动作,不要把多人图改成单人图、无人图或无关模特图。"
: "如果参考图没有明确人物展示需求,不要凭空新增人物主体。",
params.scenePlan?.whiteBackgroundLikely && shouldApplyPromptPack
? "本屏是白/浅色真实场景且词包已启用:必须让图片框内旧背景、产品、软装、灯光和空间发生清晰可见的重建;不能只微调原图或原样复刻,但也不能破坏外层白底/浅色版式和文字区。"

View File

@ -3398,6 +3398,7 @@ export async function POST(request: Request) {
reason: replacementExecutionPlan.reason,
detectionSource: replacementExecutionPlan.detectionSource,
scenePlanSource: replacementExecutionPlan.scenePlan.analysisSource,
pageKind: replacementExecutionPlan.scenePlan.pageKind,
generationRoute: replacementExecutionPlan.scenePlan.generationRoute,
promptPackPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy,
packPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy,
@ -3453,6 +3454,7 @@ export async function POST(request: Request) {
referenceIndex: referenceIndex + 1,
totalReferences: replacementReferences.length,
referenceLabel: reference.label,
pageKind: replacementExecutionPlan.scenePlan.pageKind,
generationRoute: replacementExecutionPlan.scenePlan.generationRoute,
packPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy,
selectedProductImages: selectedProductContextImageUrls.length,
@ -4160,6 +4162,7 @@ export async function POST(request: Request) {
replacementExecutionPlan.scenePlan.productSelectionReason,
useModelImage: replacementExecutionPlan.scenePlan.useModelImage,
modelImageReason: replacementExecutionPlan.scenePlan.modelImageReason,
pageKind: replacementExecutionPlan.scenePlan.pageKind,
generationRoute: replacementExecutionPlan.scenePlan.generationRoute,
promptPackPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy,
routeReason: replacementExecutionPlan.scenePlan.routeReason,
@ -4273,6 +4276,9 @@ export async function POST(request: Request) {
referenceLabel: reference.label,
pairedProductIndex:
typeof pairedProductIndex === "number" ? pairedProductIndex + 1 : null,
pageKind: replacementExecutionPlan.scenePlan.pageKind,
generationRoute: replacementExecutionPlan.scenePlan.generationRoute,
promptPackPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy,
sourceImageCount: sourceImageUrls.length,
generationAspectRatio,
imageOrder: {