fix: harden detail replacement page routing
This commit is contained in:
parent
87f23a0ab6
commit
7ac5bdf44f
@ -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
|
||||
? "本屏是白/浅色真实场景且词包已启用:必须让图片框内旧背景、产品、软装、灯光和空间发生清晰可见的重建;不能只微调原图或原样复刻,但也不能破坏外层白底/浅色版式和文字区。"
|
||||
|
||||
@ -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: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user