fix: force detail replacement generation
This commit is contained in:
parent
3e2261b1c7
commit
0b9d41e7e2
@ -239,6 +239,10 @@ export type ReplacementScenePlan = {
|
||||
export type ReplacementExecutionPlan = ReplacementReferenceDecision & {
|
||||
scenePlan: ReplacementScenePlan;
|
||||
};
|
||||
type ReplacementReferenceDecisionCore = Omit<
|
||||
ReplacementReferenceDecision,
|
||||
"detectionModelName" | "detectionSource"
|
||||
>;
|
||||
|
||||
export type ReplacementScenePlanningRuntime =
|
||||
| {
|
||||
@ -261,6 +265,100 @@ export function normalizeDetailReplacementStrategy(
|
||||
return value === "product_only" ? "product_only" : "scene_replace";
|
||||
}
|
||||
|
||||
function isStitchedDetailReferenceLabel(label: string) {
|
||||
return /^拼合切片\s*\d+/i.test(String(label || "").trim());
|
||||
}
|
||||
|
||||
function shouldForceDetailReferenceGeneration(params: {
|
||||
modeType?: "detail_replacement" | "sku_replacement";
|
||||
}) {
|
||||
return params.modeType === "detail_replacement";
|
||||
}
|
||||
|
||||
function ensureDetailReferenceGenerates<T extends ReplacementReferenceDecisionCore>(
|
||||
decision: T,
|
||||
scenePlan: ReplacementScenePlan,
|
||||
params: {
|
||||
modeType?: "detail_replacement" | "sku_replacement";
|
||||
referenceLabel: string;
|
||||
replacementStrategy?: AgentDetailReplacementStrategy;
|
||||
},
|
||||
): { decision: T; scenePlan: ReplacementScenePlan } {
|
||||
if (!shouldForceDetailReferenceGeneration(params)) {
|
||||
return { decision, scenePlan };
|
||||
}
|
||||
|
||||
if (decision.shouldReplace && scenePlan.generationRoute !== "direct_passthrough") {
|
||||
return { decision, scenePlan };
|
||||
}
|
||||
|
||||
const isProductOnly = params.replacementStrategy === "product_only";
|
||||
const isStitchedReference = isStitchedDetailReferenceLabel(params.referenceLabel);
|
||||
const forcedRoute: DetailReplacementGenerationRoute = isProductOnly
|
||||
? "layout_preserve_replace"
|
||||
: scenePlan.peoplePolicy !== "none"
|
||||
? "people_relation_locked"
|
||||
: scenePlan.copyImageCoupled || scenePlan.textPolicy === "lock"
|
||||
? "copy_locked_regenerate"
|
||||
: "layout_preserve_replace";
|
||||
const forcedReason = isStitchedReference
|
||||
? "拼合切片未勾选不处理,继续执行详情替换"
|
||||
: "详情替换未勾选不处理,继续执行必要替换";
|
||||
const forcedGenerationPrompt =
|
||||
"保留当前详情图的版式、文案、图标、构图和留白;先寻找可替换产品/产品展示区,没有产品则检查人物/模特展示,没有人物则根据上传图外观与参考图材质描述处理板材、材质、原材料、背景或视觉图片区,不允许整屏直出。";
|
||||
|
||||
return {
|
||||
decision: {
|
||||
...decision,
|
||||
shouldReplace: true,
|
||||
containsFurnitureProduct: true,
|
||||
reason: forcedReason,
|
||||
},
|
||||
scenePlan: {
|
||||
...scenePlan,
|
||||
generationRoute:
|
||||
scenePlan.generationRoute === "direct_passthrough"
|
||||
? forcedRoute
|
||||
: scenePlan.generationRoute,
|
||||
promptPackPolicy:
|
||||
scenePlan.generationRoute === "direct_passthrough"
|
||||
? "forbid"
|
||||
: scenePlan.promptPackPolicy,
|
||||
textPolicy: scenePlan.textPolicy === "free" ? "preserve" : scenePlan.textPolicy,
|
||||
routeReason:
|
||||
scenePlan.generationRoute === "direct_passthrough" || !decision.shouldReplace
|
||||
? "详情替换只允许选择处理策略,不允许模型直出跳过;按产品、模特、材质或视觉区继续处理。"
|
||||
: scenePlan.routeReason,
|
||||
sceneSummary:
|
||||
scenePlan.sceneSummary ||
|
||||
"优先处理产品/产品展示区;没有产品则处理人物/模特展示;没有人物则依据板材、材质、原材料、背景或视觉区做必要替换。",
|
||||
generationPrompt:
|
||||
scenePlan.generationRoute === "direct_passthrough" || !decision.shouldReplace
|
||||
? forcedGenerationPrompt
|
||||
: scenePlan.generationPrompt || forcedGenerationPrompt,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildDefaultProductImageSelection(productImageCount: number) {
|
||||
const safeProductImageCount = Math.max(0, Math.floor(productImageCount));
|
||||
return {
|
||||
selectedProductImageIndexes:
|
||||
safeProductImageCount > 0
|
||||
? Array.from(
|
||||
{ length: Math.min(safeProductImageCount, 2) },
|
||||
(_, index) => index + 1,
|
||||
)
|
||||
: [],
|
||||
productSelectionReason:
|
||||
safeProductImageCount > 1
|
||||
? "默认优先使用前两张上传图作为产品、材质或细节参考,其余上传图只作补充理解,不强制同时融入。"
|
||||
: safeProductImageCount === 1
|
||||
? "默认使用当前唯一上传图作为产品、材质或细节参考。"
|
||||
: "本轮未提供专门商品图;按模特图、模板补充图、上传图外观、参考图材质文案和当前参考底板判断可处理区域。",
|
||||
};
|
||||
}
|
||||
|
||||
export function truncateReplacementAnalysisText(value: unknown, maxLength = 240) {
|
||||
return String(value || "").trim().replace(/\s+/g, " ").slice(0, maxLength);
|
||||
}
|
||||
@ -538,6 +636,7 @@ export function parseReplacementScenePlan(
|
||||
1200,
|
||||
);
|
||||
|
||||
const productImageSelection = buildDefaultProductImageSelection(params.productImageCount);
|
||||
const scenePlan: ReplacementScenePlan = {
|
||||
generationRoute,
|
||||
promptPackPolicy,
|
||||
@ -579,14 +678,8 @@ export function parseReplacementScenePlan(
|
||||
220,
|
||||
),
|
||||
generationPrompt,
|
||||
selectedProductImageIndexes: Array.from(
|
||||
{ length: Math.min(Math.max(params.productImageCount, 1), 2) },
|
||||
(_, index) => index + 1,
|
||||
),
|
||||
productSelectionReason:
|
||||
params.productImageCount > 1
|
||||
? "默认优先使用前两张产品图作为主视角与补充细节,其余角度不强制同时融入。"
|
||||
: "默认使用当前唯一产品图作为主体参考。",
|
||||
selectedProductImageIndexes: productImageSelection.selectedProductImageIndexes,
|
||||
productSelectionReason: productImageSelection.productSelectionReason,
|
||||
analysisModelName: null,
|
||||
analysisSource: "heuristic",
|
||||
};
|
||||
@ -619,6 +712,7 @@ export function buildHeuristicReplacementScenePlan(params: {
|
||||
replacementStrategy?: AgentDetailReplacementStrategy;
|
||||
}): ReplacementScenePlan {
|
||||
const isProductOnly = params.replacementStrategy === "product_only";
|
||||
const productImageSelection = buildDefaultProductImageSelection(params.productImageCount);
|
||||
const generationRoute = params.shouldReplace
|
||||
? isProductOnly
|
||||
? "layout_preserve_replace"
|
||||
@ -636,14 +730,8 @@ export function buildHeuristicReplacementScenePlan(params: {
|
||||
? "兜底按仅替换产品执行,不使用词包。"
|
||||
: "兜底未检测到强约束,允许使用词包生成场景。"
|
||||
: params.reason,
|
||||
selectedProductImageIndexes: Array.from(
|
||||
{ length: Math.min(Math.max(params.productImageCount, 1), 2) },
|
||||
(_, index) => index + 1,
|
||||
),
|
||||
productSelectionReason:
|
||||
params.productImageCount > 1
|
||||
? "默认优先使用前两张产品图作为主视角与补充细节,其余角度不强制同时融入。"
|
||||
: "默认使用当前唯一产品图作为主体参考。",
|
||||
selectedProductImageIndexes: productImageSelection.selectedProductImageIndexes,
|
||||
productSelectionReason: productImageSelection.productSelectionReason,
|
||||
useModelImage: false,
|
||||
modelImageReason: params.hasModelImage
|
||||
? isProductOnly
|
||||
@ -929,18 +1017,19 @@ export async function analyzeReplacementReferenceImage(params: {
|
||||
? [
|
||||
"你是电商详情图产品主体替换判定器。",
|
||||
"请判断这张详情参考图里,是否存在清晰、主要、适合被新产品替换的商品主体、产品展示区或产品照片区域。",
|
||||
"你的任务不是重建整张场景,也不是替换文字排版;只判断是否有明确的产品替换目标。",
|
||||
"详情替换里 shouldReplace 固定为 true;你的任务不是决定是否跳过,而是说明优先处理哪个产品/材质/视觉区域。",
|
||||
"文字、标题、卖点标签、按钮、UI图标、分割线、品牌标识、留白和背景装饰不属于替换对象。",
|
||||
"只有当图片几乎是纯文字页、参数表、品牌介绍页、没有明确产品展示区时,shouldReplace 才能为 false。",
|
||||
"即使没有清晰产品主体,也要继续寻找产品展示区、材质/板材/原材料视觉区或可局部处理区域,不允许直接沿用参考图。",
|
||||
'只输出 JSON,不要输出额外解释。格式:{"shouldReplace":true,"containsFurnitureProduct":true,"reason":"不超过30字"}',
|
||||
].join("\n")
|
||||
: isDetailReplacement
|
||||
? [
|
||||
"你是电商详情参考图背景替换判定器。",
|
||||
"请判断这张图片里,是否存在适合被重建/替换的背景图、产品场景图、商品主体图或大面积视觉图片区。",
|
||||
"这不是只找家具主体:只要存在照片、插画、场景背景、产品展示图区、人物展示图区,都可以 shouldReplace=true。",
|
||||
"详情替换里 shouldReplace 固定为 true;你的任务不是决定是否跳过,而是说明应优先处理产品、人物/模特、材质/板材/原材料、背景或视觉区。",
|
||||
"这不是只找家具主体:只要存在照片、插画、场景背景、产品展示图区、人物展示图区、材质说明区,都必须继续处理。",
|
||||
"必须保留的文字、标题、卖点标签、UI图标、按钮、分割线、品牌标识、留白和排版不属于替换对象。",
|
||||
"只有当图片几乎是纯文字页、参数表、品牌介绍页、无明显背景图/场景图区/商品图片区时,shouldReplace 才能为 false。",
|
||||
"如果没有产品,检查是否需要模特/人物;如果没有人物,则根据上传图外观和参考图材质描述处理板材、材质、原材料、背景或视觉图片区;不允许直接沿用参考图。",
|
||||
'只输出 JSON,不要输出额外解释。格式:{"shouldReplace":true,"containsFurnitureProduct":true,"reason":"不超过30字"}',
|
||||
].join("\n")
|
||||
: [
|
||||
@ -1024,6 +1113,16 @@ export async function analyzeReplacementReferenceImage(params: {
|
||||
chatApiLogger.warn("[Chat API] Fallback Qwen3-VL reference analysis failed:", error);
|
||||
}
|
||||
|
||||
if (isDetailReplacement) {
|
||||
return {
|
||||
shouldReplace: true,
|
||||
containsFurnitureProduct: true,
|
||||
reason: "详情替换未勾选不处理,继续执行必要替换",
|
||||
detectionModelName: null,
|
||||
detectionSource: "safe_passthrough",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
shouldReplace: false,
|
||||
containsFurnitureProduct: false,
|
||||
@ -1141,6 +1240,12 @@ export async function analyzeReplacementExecutionPlan(params: {
|
||||
params.modelImageUrl && !isProductOnlyDetailReplacement
|
||||
? "如果当前参考图存在人物展示需求,后续生图必须尽量复用提供模特图中的同一人物身份,不要明显换脸、换发型、换体型。"
|
||||
: "",
|
||||
params.modelImageUrl && !isProductOnlyDetailReplacement
|
||||
? "如果当前参考图是多人/群体/互动关系图,即使只提供 1 张模特图,也必须保留参考图的人数、相对位置和互动关系;提供的模特图作为主人物身份/气质参考,其余人物按参考图生成相近人群,不要把多人压成单人。"
|
||||
: "",
|
||||
isDetailReplacement
|
||||
? "用户上传图可能是产品、材质、板材、原材料或细节参考,不要因为它不是完整商品主体就排除;后续应根据上传图外观与参考图里的材质描述共同判断处理方向。"
|
||||
: "",
|
||||
"这张图片是唯一的详情参考图底板,它决定当前这一屏的版式、文字排版、UI图标、按钮、机位、构图、留白和信息层级。",
|
||||
isProductOnlyDetailReplacement
|
||||
? "这张参考图要求最大程度原样保留。后续生图只允许替换其中的产品主体/产品展示区域,背景、文案、按钮、图标、分割线、品牌标识、留白、装饰和光影氛围都必须保留。"
|
||||
@ -1149,13 +1254,13 @@ export async function analyzeReplacementExecutionPlan(params: {
|
||||
: "这张参考图不是要求原样保留的场景照片。原图里的沙发、床、柜体、墙面、地面、窗帘、灯具、摆件、桌面道具、背景家具和环境光氛围,都可以在后续生图阶段按前置工作流风格重建。",
|
||||
"请完成以下任务:",
|
||||
isProductOnlyDetailReplacement
|
||||
? "1. 判断 shouldReplace:当前参考图里是否存在清晰、主要、适合被新产品替换的商品主体、产品照片区域或产品展示区。纯文字/纯参数/纯品牌介绍且没有产品展示区时才为 false。"
|
||||
? "1. shouldReplace 固定为 true:详情替换只允许规划处理范围,不允许判定直接沿用。若没有清晰产品主体,也要继续寻找产品展示区、材质/板材/原材料视觉区或可局部处理区域。"
|
||||
: isDetailReplacement
|
||||
? "1. 判断 shouldReplace:当前参考图里是否存在适合被替换/重建的背景图、产品场景图、商品主体图、人物展示图或大面积视觉图片区。纯文字/纯参数/纯品牌介绍且没有视觉图片区时才为 false。"
|
||||
? "1. shouldReplace 固定为 true:除非用户已经在前置流程人工勾选不处理(此处不会进入分析),你不能返回 false。若没有产品,检查人物/模特;没有人物,检查板材/材质/原材料、背景图或其它视觉区。"
|
||||
: "1. 判断 shouldReplace:当前参考图里是否存在清晰、主要、适合被替换的家具商品主体。",
|
||||
"2. 判断 useModelImage,并说明原因。",
|
||||
isDetailReplacement
|
||||
? "3. 判断 generationRoute:prompt_pack_scene=通用场景图且可吃词包;layout_preserve_replace=锁定版式/白底/产品说明,只做有限替换;copy_locked_regenerate=文案与画面强绑定,必须锁文案;people_relation_locked=多人/模特/互动关系图,必须锁人物关系;direct_passthrough=纯文字/白底说明/无可替换视觉区,直接沿用。"
|
||||
? "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:只有通用空间场景且没有强文案、白底、多人关系、词图强绑定限制时才 allow;白底图、强文案图、多人关系图、图文匹配强的详情页必须 forbid。"
|
||||
@ -1164,7 +1269,7 @@ export async function analyzeReplacementExecutionPlan(params: {
|
||||
? "5. 判断 textPolicy:画面中存在营销标题、卖点、编号、参数、标签且与图匹配时必须 lock 或 preserve,避免后续生图改字、混色或重排。"
|
||||
: "",
|
||||
isDetailReplacement
|
||||
? "6. 判断 peoplePolicy:有人物/多人/自拍/聚会/模特互动时必须 preserve_relation;如果没有提供用户模特图但参考图有人物身份,不允许把参考人物身份当成用户模特直接迁移。"
|
||||
? "6. 判断 peoplePolicy:有人物/多人/自拍/聚会/模特互动时必须 preserve_relation;如果只提供 1 张模特图但参考图多人,仍要保留参考图人数和关系,额外人物按参考图生成相近人群;如果没有提供用户模特图但参考图有人物身份,不允许把参考人物身份当成用户模特直接迁移。"
|
||||
: "",
|
||||
isDetailReplacement
|
||||
? "7. 提炼 sceneSummary、cameraSummary、placementSummary、preserveSummary、styleSummary、avoidSummary。"
|
||||
@ -1194,7 +1299,7 @@ export async function analyzeReplacementExecutionPlan(params: {
|
||||
: "",
|
||||
"只返回严格 JSON,不要任何额外解释。",
|
||||
`JSON 格式:
|
||||
{"shouldReplace":true,"containsFurnitureProduct":true,"reason":"不超过30字","generationRoute":"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字","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");
|
||||
@ -1233,15 +1338,24 @@ export async function analyzeReplacementExecutionPlan(params: {
|
||||
});
|
||||
|
||||
if (parsedDecision && parsedScenePlan) {
|
||||
const normalizedPlan = ensureDetailReferenceGenerates(
|
||||
parsedDecision,
|
||||
parsedScenePlan,
|
||||
{
|
||||
modeType: params.modeType,
|
||||
referenceLabel: params.referenceLabel,
|
||||
replacementStrategy: params.replacementStrategy,
|
||||
},
|
||||
);
|
||||
return {
|
||||
...parsedDecision,
|
||||
...normalizedPlan.decision,
|
||||
detectionModelName: params.planningRuntime.modelName,
|
||||
detectionSource:
|
||||
params.planningRuntime.kind === "local_moonshot"
|
||||
? "local_moonshot"
|
||||
: "configured_vision",
|
||||
scenePlan: {
|
||||
...parsedScenePlan,
|
||||
...normalizedPlan.scenePlan,
|
||||
analysisModelName: params.planningRuntime.modelName,
|
||||
analysisSource:
|
||||
params.planningRuntime.kind === "local_moonshot"
|
||||
@ -1265,17 +1379,27 @@ export async function analyzeReplacementExecutionPlan(params: {
|
||||
modeType: params.modeType,
|
||||
replacementStrategy: params.replacementStrategy,
|
||||
});
|
||||
return {
|
||||
...fallbackDecision,
|
||||
scenePlan: buildHeuristicReplacementScenePlan({
|
||||
productImageCount: params.productImageUrls.length,
|
||||
hasModelImage: isProductOnlyDetailReplacement
|
||||
? false
|
||||
: Boolean(params.modelImageUrl),
|
||||
shouldReplace: fallbackDecision.shouldReplace,
|
||||
reason: fallbackDecision.reason,
|
||||
const fallbackScenePlan = buildHeuristicReplacementScenePlan({
|
||||
productImageCount: params.productImageUrls.length,
|
||||
hasModelImage: isProductOnlyDetailReplacement
|
||||
? false
|
||||
: Boolean(params.modelImageUrl),
|
||||
shouldReplace: fallbackDecision.shouldReplace,
|
||||
reason: fallbackDecision.reason,
|
||||
replacementStrategy: params.replacementStrategy,
|
||||
});
|
||||
const normalizedFallbackPlan = ensureDetailReferenceGenerates(
|
||||
fallbackDecision,
|
||||
fallbackScenePlan,
|
||||
{
|
||||
modeType: params.modeType,
|
||||
referenceLabel: params.referenceLabel,
|
||||
replacementStrategy: params.replacementStrategy,
|
||||
}),
|
||||
},
|
||||
);
|
||||
return {
|
||||
...normalizedFallbackPlan.decision,
|
||||
scenePlan: normalizedFallbackPlan.scenePlan,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1334,14 +1458,14 @@ export function buildAgentReplacementPrompt(params: {
|
||||
? `图片${nextImageIndex}`
|
||||
: `图片${nextImageIndex}-${nextImageIndex + params.productImageCount - 1}`;
|
||||
imageOrderLines.push(
|
||||
`${productRangeLabel}:同一产品的白底图/细节图,必须综合理解所有产品图,不可只看第一张。`
|
||||
`${productRangeLabel}:用户上传的产品/材质/板材/原材料或细节参考图,必须综合理解外观、材质、颜色、纹理和结构,不可只看第一张,也不要因为它不是完整商品主体就忽略。`
|
||||
);
|
||||
nextImageIndex += params.productImageCount;
|
||||
}
|
||||
|
||||
if (params.hasModelImage) {
|
||||
imageOrderLines.push(
|
||||
`图片${nextImageIndex}:模特图,仅在当前参考图本身包含人物展示时才允许参考。`
|
||||
`图片${nextImageIndex}:模特图,仅在当前参考图本身包含人物展示时参考;如果参考图是多人,只把这张模特图作为主人物身份/气质参考,其余人物按参考图的人数、关系和风格补齐。`
|
||||
);
|
||||
nextImageIndex += 1;
|
||||
}
|
||||
@ -1410,6 +1534,9 @@ export function buildAgentReplacementPrompt(params: {
|
||||
? `本屏禁止使用模特图:${params.scenePlan.modelImageReason}`
|
||||
: "本屏不需要模特图,不允许把模特图强行融入结果。"
|
||||
: "",
|
||||
params.scenePlan.peoplePolicy === "preserve_relation" && params.hasModelImage
|
||||
? "若当前参考图为多人/群体/互动关系图,即使只提供 1 张模特图,也必须保留参考图人数、相对位置、动作和关系;模特图作为主人物身份/气质参考,其余人物按参考图生成相近人群。"
|
||||
: "",
|
||||
params.scenePlan.generationPrompt
|
||||
? `本屏生成方向:${params.scenePlan.generationPrompt}`
|
||||
: "",
|
||||
@ -1433,11 +1560,13 @@ export function buildAgentReplacementPrompt(params: {
|
||||
: "输出必须保持当前参考图页面画布比例。替换商品时只能等比缩放、透视匹配和自然融合,绝对禁止把商品、人物、文字、图标横向或纵向拉伸变形。",
|
||||
"镜头角度、透视关系、视平线、主体朝向和主体占比必须尽量跟随最后一张参考图,方便无缝替换当前产品区域。",
|
||||
params.productImageCount > 1
|
||||
? `前面的 ${params.productImageCount} 张商品输入图都属于同一产品的多角度/多细节白底图,请综合这些图片理解产品结构、材质、颜色、比例、边角、开孔、五金与细节,不可只参考第一张。`
|
||||
: "前面的商品输入图就是当前要替换进去的产品白底图,请准确保持该产品的结构、材质、颜色、比例和细节。",
|
||||
"你的任务只有一个:识别参考图中的旧产品主体或产品展示区,用输入产品替换它,并让新产品与原场景自然融合。",
|
||||
? `前面的 ${params.productImageCount} 张上传参考图可能是同一产品的多角度图,也可能是材质、板材、原材料或细节图;请综合理解结构、材质、颜色、比例、纹理、边角、开孔、五金与细节,不可只参考第一张。`
|
||||
: params.productImageCount === 1
|
||||
? "前面的上传参考图可能是产品图、材质图、板材图、原材料图或细节图,请准确理解它的结构、材质、颜色、比例、纹理和细节。"
|
||||
: "本轮没有专门产品/材质输入图,请依据模特图、模板补充图以及最后一张参考图里的材质描述/视觉区做最小必要处理,不允许整屏直出。",
|
||||
"你的任务是先识别参考图中的旧产品主体或产品展示区;如果没有明确产品,就继续识别材质/板材/原材料视觉区或可局部处理区域,并让处理结果与原版式自然融合。",
|
||||
"不要重建背景,不要重画空间,不要调整文案,不要修改按钮、图标、标签、装饰元素,也不要改变人物、道具和环境光氛围。",
|
||||
"如果参考图中没有清晰可替换的产品主体或产品展示区,应直接沿用参考图,不要硬生成。",
|
||||
"如果参考图中没有清晰可替换的产品主体或产品展示区,也必须继续处理产品展示区、材质/板材/原材料视觉区或与上传图外观匹配的局部区域,不允许整屏直出。",
|
||||
"不要把多个产品角度同时塞进一张结果图,不要新增无关主体,不要删除任何关键版式或 UI 元素。",
|
||||
]
|
||||
: [
|
||||
@ -1459,7 +1588,7 @@ export function buildAgentReplacementPrompt(params: {
|
||||
shouldApplyPromptPack
|
||||
? "执行时要基于这些场景组合要求,重建参考图中的背景图/产品场景图区域,使当前这一屏呈现出与第一步已选风格一致的新背景画面。"
|
||||
: "执行时必须保持参考图原有画面主题、文案语义、人物关系、构图和版式,只在可替换视觉区做有限替换。",
|
||||
"允许替换/重建的范围是参考图里的背景图、产品场景图、商品主体图和必要的人物展示图区;不允许改动版式层。",
|
||||
"允许替换/重建的范围是参考图里的背景图、产品场景图、商品主体图、材质/板材/原材料视觉区和必要的人物展示图区;不允许改动版式层。",
|
||||
"如果前面商品图、模特图与最后一张参考图底板存在冲突,始终以最后一张参考图底板的版式、机位、构图和信息层级为最高优先级。",
|
||||
"最后一张参考图是唯一底板,必须完整保留这张图的文字排版、品牌字样、标题区、卖点区、UI图标、按钮、标签、装饰元素、分割线、留白、画布比例和信息层级。",
|
||||
params.scenePlan?.textPolicy === "lock"
|
||||
@ -1478,14 +1607,16 @@ export function buildAgentReplacementPrompt(params: {
|
||||
: "输出必须保持当前参考图页面画布比例。替换背景/商品时只能等比缩放后裁切或延展背景,绝对禁止把商品、人物、文字、图标横向或纵向拉伸变形。",
|
||||
"镜头角度、透视关系、视平线、主体朝向和主体占比必须尽量跟随最后一张参考图,方便无缝替换当前版式,不要擅自改成完全不同的视角。",
|
||||
shouldApplyPromptPack
|
||||
? "前面的商品图和模特图用于生成新的背景图/产品场景图内容,并按工作流设定替换参考图中的旧背景画面,但不能改变参考图的版式设计。"
|
||||
: "前面的商品图用于必要的产品/视觉区替换;模特图只在本屏明确需要人物且策略允许时使用,不得覆盖参考图原有语义、版式和图文关系。",
|
||||
? "前面的上传图和模特图用于生成新的背景图/产品场景图/材质视觉内容,并按工作流设定替换参考图中的旧背景画面,但不能改变参考图的版式设计。"
|
||||
: "前面的上传图用于必要的产品、材质、板材、原材料或视觉区替换;模特图只在本屏明确需要人物且策略允许时使用,不得覆盖参考图原有语义、版式和图文关系。",
|
||||
shouldApplyPromptPack
|
||||
? "可被替换重建的内容包括但不限于:背景照片、产品展示照片、墙面、地面材质、沙发、床、背景柜、窗帘、灯具、地毯、挂画、摆件、桌面道具、空间色调、环境光氛围。"
|
||||
: "可被替换的内容仅限参考图里明确的产品/视觉图片区;不要新增不属于参考图语义的空间、人物、道具、主题和文案。",
|
||||
? "可被替换重建的内容包括但不限于:背景照片、产品展示照片、板材、材质、原材料、墙面、地面材质、沙发、床、背景柜、窗帘、灯具、地毯、挂画、摆件、桌面道具、空间色调、环境光氛围。"
|
||||
: "可被替换的内容仅限参考图里明确的产品/材质/板材/原材料/视觉图片区;不要新增不属于参考图语义的空间、人物、道具、主题和文案。",
|
||||
params.productImageCount > 1
|
||||
? `前面的 ${params.productImageCount} 张商品输入图都属于同一产品的多角度/多细节白底图,请综合这些图片理解产品结构、材质、颜色、比例、边角、开孔、五金与细节,不可只参考第一张。`
|
||||
: "前面的商品输入图就是当前要替换进去的产品白底图,请准确保持该产品的结构、材质、颜色、比例和细节。",
|
||||
? `前面的 ${params.productImageCount} 张上传参考图可能是产品、多角度细节、材质、板材或原材料参考,请综合理解结构、材质、颜色、比例、纹理、边角、开孔、五金与细节,不可只参考第一张。`
|
||||
: params.productImageCount === 1
|
||||
? "前面的上传参考图可能是产品图、材质图、板材图、原材料图或细节图,请准确理解它的结构、材质、颜色、比例、纹理和细节。"
|
||||
: "本轮没有专门产品/材质输入图,请依据模特图、模板补充图以及最后一张参考图里的材质描述/视觉区做最小必要处理,不允许整屏直出。",
|
||||
shouldApplyPromptPack
|
||||
? "请先识别最后一张参考图中需要被替换的背景图/场景图区域,并在同一版式骨架下生成新的背景画面;不要只做局部抠图式换产品。"
|
||||
: "请先识别最后一张参考图中真正允许替换的视觉区;如果只是白底说明、强文案页、多人关系图或图文匹配强的内容,不要强行重建整张场景。",
|
||||
@ -1495,15 +1626,15 @@ export function buildAgentReplacementPrompt(params: {
|
||||
shouldApplyPromptPack
|
||||
? "如果第一步工作流风格与参考图原场景冲突,必须保留版式骨架和UI层,但放弃原背景图物件本身,以第一步工作流风格为准。"
|
||||
: "如果第一步工作流风格与参考图原场景冲突,必须保留参考图语义与版式,不要放弃原有图文关系。",
|
||||
"前面的输入商品图用于提供商品主体、材质、颜色和细节特征;如有模特图,请只在参考图本身包含人物展示时再参考模特图。",
|
||||
"如果提供了模特图且这一屏确实有人物展示需求,必须尽量复用同一人物身份与体态关系,不要明显换脸、换发型、换体型,服装风格也尽量保持一致。",
|
||||
"前面的上传图用于提供商品主体、材质、板材、原材料、颜色、纹理和细节特征;如有模特图,请只在参考图本身包含人物展示时再参考模特图。",
|
||||
"如果提供了模特图且这一屏确实有人物展示需求,必须尽量复用同一人物身份与体态关系,不要明显换脸、换发型、换体型,服装风格也尽量保持一致;如果参考图是多人但只提供 1 张模特图,保留参考图人数、关系、动作和站位,额外人物按参考图生成相近人群。",
|
||||
"绝对不要忽略最后一张参考图去生成一张无关的新场景图。",
|
||||
shouldApplyPromptPack
|
||||
? "如果生成结果仍然保留了参考图原有的大部分背景图物件,只是把产品换掉,则视为执行失败;你必须让背景图/场景图发生清晰可见的变化。"
|
||||
: "如果本屏已禁用词包,生成结果不应追求明显改造背景,而应追求图文匹配、文案稳定、人物关系稳定和局部替换自然。",
|
||||
"绝对不要移动、删除、改写、重排参考图里的任何文字、UI图标、按钮、标签、装饰元素与排版元素。",
|
||||
"输出结果必须保持最后一张参考图底板的原始纵横比与画布尺寸,不允许擅自改成长图、横图或裁掉任何版面区域。",
|
||||
"如果参考图中没有可替换/重建的背景图、产品场景图或视觉图片区,应直接沿用参考图,不要硬生成。",
|
||||
"如果参考图中没有产品主体,继续检查人物/模特展示;如果没有人物,则根据上传图外观和参考图中的材质描述处理板材、材质、原材料、背景或视觉图片区;不允许整屏直出。",
|
||||
"不要改动参考图的整体布局,不要新增无关主体,不要删除关键版式或UI元素。",
|
||||
];
|
||||
|
||||
@ -1534,7 +1665,9 @@ export function buildAgentReplacementPrompt(params: {
|
||||
: skuInstructionLines).filter(Boolean);
|
||||
|
||||
if (params.hasModelImage) {
|
||||
instructionLines.push("如有模特图,请在需要人物展示时保持模特体态、穿搭和出镜关系一致。");
|
||||
instructionLines.push(
|
||||
"如有模特图,请在需要人物展示时保持模特体态、穿搭和出镜关系一致;多人参考图要保留参考图人数和互动关系,只把上传模特作为主人物参考,其余人物按参考图风格补齐。"
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
|
||||
@ -3167,16 +3167,6 @@ export async function POST(request: Request) {
|
||||
? modelSourceImage.url
|
||||
: null;
|
||||
|
||||
if (productInputImageUrls.length === 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
code: "bad_request:api",
|
||||
message: "详情参考图替换至少需要 1 张产品图",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const directReplacementModels = await getEnabledAiModelsWithProvider().catch((error) => {
|
||||
chatApiLogger.warn("[Chat API] Failed to load models for direct detail replacement:", error);
|
||||
return undefined;
|
||||
@ -3421,19 +3411,12 @@ export async function POST(request: Request) {
|
||||
);
|
||||
|
||||
if (!replacementExecutionPlan.shouldReplace) {
|
||||
await emitReplacementResultMessage({
|
||||
reference,
|
||||
bodyText: replacementExecutionPlan.reason,
|
||||
image: {
|
||||
originalUrl: reference.imageUrl,
|
||||
thumbnailUrl: reference.imageUrl,
|
||||
},
|
||||
modelDisplayName: null,
|
||||
sourceImageUrls: referenceSourceImageUrls,
|
||||
streamLabel: "agent-mode:image:detail-replacement:item",
|
||||
chatApiLogger.warn("[Chat API] Direct detail replacement plan returned shouldReplace=false; forcing generation:", {
|
||||
referenceIndex: referenceIndex + 1,
|
||||
totalReferences: replacementReferences.length,
|
||||
referenceLabel: reference.label,
|
||||
reason: replacementExecutionPlan.reason,
|
||||
});
|
||||
successCount += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const referenceGeometry = await resolveReplacementReferenceGeometry(reference.imageUrl);
|
||||
@ -3445,28 +3428,28 @@ export async function POST(request: Request) {
|
||||
selectedProductIndexes.length > 0
|
||||
? selectedProductIndexes.map((index) => productInputImageUrls[index]!)
|
||||
: productInputImageUrls.slice(0, Math.min(productInputImageUrls.length, 2));
|
||||
const shouldIncludeModelImage =
|
||||
detailReplacementStrategy !== "product_only" &&
|
||||
Boolean(modelInputImageUrl) &&
|
||||
(replacementExecutionPlan.scenePlan.useModelImage ||
|
||||
selectedProductContextImageUrls.length === 0);
|
||||
const sourceImageUrls = Array.from(
|
||||
new Set([
|
||||
...selectedProductContextImageUrls,
|
||||
...(replacementExecutionPlan.scenePlan.useModelImage &&
|
||||
detailReplacementStrategy !== "product_only" &&
|
||||
modelInputImageUrl
|
||||
? [modelInputImageUrl]
|
||||
: []),
|
||||
reference.imageUrl,
|
||||
...selectedProductContextImageUrls,
|
||||
...(shouldIncludeModelImage && modelInputImageUrl
|
||||
? [modelInputImageUrl]
|
||||
: []),
|
||||
reference.imageUrl,
|
||||
]),
|
||||
);
|
||||
const replacementPrompt = buildAgentReplacementPrompt({
|
||||
basePrompt: baseReplacementPrompt,
|
||||
modeType: "detail_replacement",
|
||||
replacementStrategy: detailReplacementStrategy,
|
||||
referenceLabel: reference.label,
|
||||
referenceIndex,
|
||||
totalReferences: replacementReferences.length,
|
||||
hasModelImage:
|
||||
replacementExecutionPlan.scenePlan.useModelImage &&
|
||||
detailReplacementStrategy !== "product_only" &&
|
||||
Boolean(modelInputImageUrl),
|
||||
const replacementPrompt = buildAgentReplacementPrompt({
|
||||
basePrompt: baseReplacementPrompt,
|
||||
modeType: "detail_replacement",
|
||||
replacementStrategy: detailReplacementStrategy,
|
||||
referenceLabel: reference.label,
|
||||
referenceIndex,
|
||||
totalReferences: replacementReferences.length,
|
||||
hasModelImage: shouldIncludeModelImage,
|
||||
productImageCount: selectedProductContextImageUrls.length,
|
||||
originalProductImageCount: productInputImageUrls.length,
|
||||
templateReferenceCount: 0,
|
||||
@ -3558,13 +3541,7 @@ export async function POST(request: Request) {
|
||||
image: finalImage,
|
||||
imageSizeLabel: finalImageSizeLabel,
|
||||
modelDisplayName: resolvedModelDisplayName,
|
||||
sourceImageUrls: Array.from(
|
||||
new Set([
|
||||
...productInputImageUrls,
|
||||
...(modelInputImageUrl ? [modelInputImageUrl] : []),
|
||||
...replacementReferences.map((item) => item.imageUrl),
|
||||
]),
|
||||
),
|
||||
sourceImageUrls,
|
||||
streamLabel: "agent-mode:image:detail-replacement:item",
|
||||
promptForPersistence: replacementPrompt,
|
||||
});
|
||||
@ -3855,13 +3832,15 @@ export async function POST(request: Request) {
|
||||
.filter(Boolean),
|
||||
),
|
||||
);
|
||||
const productInputImageUrls = sharedUploadedImageUrls.slice(
|
||||
0,
|
||||
Math.max(1, replacementProductSlotCount)
|
||||
);
|
||||
const modelInputImageUrl = replacementModelSlotEnabled
|
||||
? sharedUploadedImageUrls[Math.max(0, replacementProductSlotCount)] || null
|
||||
: null;
|
||||
const productInputImageUrls = isDetailReplacementPreset
|
||||
? sharedUploadedImageUrls.filter((url) => url !== modelInputImageUrl)
|
||||
: sharedUploadedImageUrls.slice(
|
||||
0,
|
||||
Math.max(1, replacementProductSlotCount)
|
||||
);
|
||||
const sourceAssetIds = Array.from(
|
||||
new Set(
|
||||
[
|
||||
@ -3895,7 +3874,7 @@ export async function POST(request: Request) {
|
||||
: "generic",
|
||||
});
|
||||
|
||||
if (productInputImageUrls.length === 0) {
|
||||
if (!isDetailReplacementPreset && productInputImageUrls.length === 0) {
|
||||
throw new Error("替换模式至少需要 1 张输入商品图");
|
||||
}
|
||||
|
||||
@ -4169,7 +4148,7 @@ export async function POST(request: Request) {
|
||||
]),
|
||||
);
|
||||
|
||||
if (!replacementExecutionPlan.shouldReplace) {
|
||||
if (!isDetailReplacementPreset && !replacementExecutionPlan.shouldReplace) {
|
||||
chatApiLogger.info("[Chat API] Replacement reference skipped and reused:", {
|
||||
mode: replacementSourceType,
|
||||
referenceIndex: referenceIndex + 1,
|
||||
@ -4194,6 +4173,14 @@ export async function POST(request: Request) {
|
||||
successCount += 1;
|
||||
return;
|
||||
}
|
||||
if (isDetailReplacementPreset && !replacementExecutionPlan.shouldReplace) {
|
||||
chatApiLogger.warn("[Chat API] Detail replacement preset plan returned shouldReplace=false; forcing generation:", {
|
||||
referenceIndex: referenceIndex + 1,
|
||||
totalReferences: replacementReferences.length,
|
||||
referenceLabel: reference.label,
|
||||
reason: replacementExecutionPlan.reason,
|
||||
});
|
||||
}
|
||||
|
||||
const referenceGeometry = await resolveReplacementReferenceGeometry(reference.imageUrl);
|
||||
const generationAspectRatio = referenceGeometry?.aspectRatio || resolvedAspectRatio;
|
||||
@ -4212,14 +4199,18 @@ export async function POST(request: Request) {
|
||||
0,
|
||||
Math.min(orderedProductContextImageUrls.length, 2),
|
||||
);
|
||||
const shouldIncludeModelImage =
|
||||
!(isDetailReplacementPreset &&
|
||||
detailReplacementStrategy === "product_only") &&
|
||||
Boolean(modelInputImageUrl) &&
|
||||
(replacementExecutionPlan.scenePlan.useModelImage ||
|
||||
(isDetailReplacementPreset &&
|
||||
selectedProductContextImageUrls.length === 0));
|
||||
const sourceImageUrls = Array.from(
|
||||
new Set(
|
||||
[
|
||||
...selectedProductContextImageUrls,
|
||||
...(replacementExecutionPlan.scenePlan.useModelImage &&
|
||||
!(isDetailReplacementPreset &&
|
||||
detailReplacementStrategy === "product_only") &&
|
||||
modelInputImageUrl
|
||||
...(shouldIncludeModelImage && modelInputImageUrl
|
||||
? [modelInputImageUrl]
|
||||
: []),
|
||||
...replacementTemplateReferenceImageUrls,
|
||||
@ -4239,11 +4230,7 @@ export async function POST(request: Request) {
|
||||
referenceLabel: reference.label,
|
||||
referenceIndex,
|
||||
totalReferences: replacementReferences.length,
|
||||
hasModelImage:
|
||||
replacementExecutionPlan.scenePlan.useModelImage &&
|
||||
!(isDetailReplacementPreset &&
|
||||
detailReplacementStrategy === "product_only") &&
|
||||
Boolean(modelInputImageUrl),
|
||||
hasModelImage: shouldIncludeModelImage,
|
||||
productImageCount: selectedProductContextImageUrls.length,
|
||||
originalProductImageCount: productInputImageUrls.length,
|
||||
templateReferenceCount: replacementTemplateReferenceImageUrls.length,
|
||||
@ -4263,9 +4250,7 @@ export async function POST(request: Request) {
|
||||
imageOrder: {
|
||||
productImages: selectedProductContextImageUrls.length,
|
||||
originalProductImages: productInputImageUrls.length,
|
||||
hasModelImage:
|
||||
replacementExecutionPlan.scenePlan.useModelImage &&
|
||||
Boolean(modelInputImageUrl),
|
||||
hasModelImage: shouldIncludeModelImage,
|
||||
templateReferenceImages: replacementTemplateReferenceImageUrls.length,
|
||||
finalReferenceImageIndex: sourceImageUrls.length,
|
||||
},
|
||||
|
||||
@ -940,9 +940,8 @@ function inferReplacementCardStatusFromText(text: string | null | undefined): Re
|
||||
}
|
||||
|
||||
if (
|
||||
normalized.includes("沿用参考图") ||
|
||||
normalized.includes("未检测到可替换的家具主体") ||
|
||||
normalized.includes("视觉判断失败")
|
||||
normalized.includes(MANUAL_PASSTHROUGH_RESULT_TEXT) ||
|
||||
normalized.includes("已按人工标记沿用参考图")
|
||||
) {
|
||||
return "passthrough";
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user