fix: limit kimi planning to reference image and extend timeout
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-03-23 22:42:51 +08:00
parent d2aa27b1c2
commit ed6ebbf80a

View File

@ -2908,8 +2908,6 @@ type ReplacementReferenceDecision = {
};
type ReplacementScenePlan = {
selectedProductImageIndexes: number[];
productSelectionReason: string;
useModelImage: boolean;
modelImageReason: string;
sceneSummary: string;
@ -2919,6 +2917,8 @@ type ReplacementScenePlan = {
styleSummary: string;
avoidSummary: string;
generationPrompt: string;
selectedProductImageIndexes: number[];
productSelectionReason: string;
analysisModelName?: string | null;
analysisSource: "local_moonshot" | "configured_vision" | "heuristic";
};
@ -2960,22 +2960,6 @@ function parseReplacementScenePlan(
try {
const parsed = JSON.parse(jsonText) as Record<string, unknown>;
const rawIndexes = Array.isArray(parsed.selectedProductImageIndexes)
? parsed.selectedProductImageIndexes
: Array.isArray(parsed.productImageIndexes)
? parsed.productImageIndexes
: [];
const selectedProductImageIndexes = Array.from(
new Set(
rawIndexes
.map((value) => Number(value))
.filter((value) => Number.isFinite(value))
.map((value) =>
Math.max(1, Math.min(params.productImageCount, Math.round(value))),
),
),
).slice(0, 3);
const useModelImageRaw =
normalizeBooleanLike(parsed.useModelImage) ??
normalizeBooleanLike(parsed.needModelImage) ??
@ -2988,14 +2972,6 @@ function parseReplacementScenePlan(
);
const scenePlan: ReplacementScenePlan = {
selectedProductImageIndexes:
selectedProductImageIndexes.length > 0
? selectedProductImageIndexes
: [1].filter((value) => value <= params.productImageCount),
productSelectionReason: truncateReplacementAnalysisText(
parsed.productSelectionReason || parsed.selectionReason || parsed.productReason,
160,
),
useModelImage: params.hasModelImage ? Boolean(useModelImageRaw) : false,
modelImageReason: truncateReplacementAnalysisText(
parsed.modelImageReason || parsed.modelReason,
@ -3026,6 +3002,14 @@ function parseReplacementScenePlan(
220,
),
generationPrompt,
selectedProductImageIndexes: Array.from(
{ length: Math.min(Math.max(params.productImageCount, 1), 2) },
(_, index) => index + 1,
),
productSelectionReason:
params.productImageCount > 1
? "默认优先使用前两张产品图作为主视角与补充细节,其余角度不强制同时融入。"
: "默认使用当前唯一产品图作为主体参考。",
analysisModelName: null,
analysisSource: "heuristic",
};
@ -3064,9 +3048,9 @@ function buildHeuristicReplacementScenePlan(params: {
params.productImageCount > 1
? "默认优先使用前两张产品图作为主视角与补充细节,其余角度不强制同时融入。"
: "默认使用当前唯一产品图作为主体参考。",
useModelImage: params.hasModelImage,
useModelImage: false,
modelImageReason: params.hasModelImage
? "未完成场景级人物需求分析,先保留模特图作为可选补充参考。"
? "前置分析未明确当前参考图存在人物展示需求,兜底阶段默认不使用模特图。"
: "当前未提供模特图。",
sceneSummary: params.shouldReplace
? "保持当前参考图版式与场景结构,只替换参考图中的产品主体。"
@ -3386,13 +3370,11 @@ async function analyzeReplacementExecutionPlan(params: {
}): Promise<ReplacementExecutionPlan> {
if (params.planningRuntime) {
try {
const analysisImageUrls = [
...params.productImageUrls,
...(params.modelImageUrl ? [params.modelImageUrl] : []),
params.referenceImageUrl,
].filter(Boolean) as string[];
const replacementScenePlanningTimeoutMs =
params.planningRuntime.kind === "local_moonshot" ? 90_000 : 45_000;
const analysisImageUrls = [params.referenceImageUrl];
const { inlineDataUrls } = await inlineVisionImages(analysisImageUrls, {
maxImages: 8,
maxImages: 1,
maxSide: 900,
timeoutMs: 12000,
retries: 2,
@ -3410,25 +3392,23 @@ async function analyzeReplacementExecutionPlan(params: {
const analysisPrompt = [
"你是电商详情替换流程中的“场景理解规划器”。",
`当前要分析的参考图:${params.referenceLabel}`,
`产品图数量:${params.productImageUrls.length} 张。产品图全部属于同一产品的多角度/多细节白底图。`,
"这些产品图只是帮助你理解同一产品的结构、材质、颜色、比例和细节,不代表必须把所有角度都同时体现在这一张结果图里。",
"你必须根据最后一张参考图底板当前这一屏的机位、主体位置和露出范围,只选择最需要的 1-2 张产品图作为本屏主参考;只有在确实必要时才允许选择 3 张,禁止无理由把所有产品图都选上。",
"你现在只分析这一张参考图本身,不分析产品图,不分析模特图。",
"产品图和模特图会在后续生图阶段单独提供给生图模型使用,你这里不要尝试理解它们的具体外观,也不要要求必须使用所有输入图。",
params.modelImageUrl
? "还提供了 1 张模特图。只有当最后一张参考图底板当前这一屏本身存在人物展示需求时,才允许使用模特图;如果这一屏不需要人物,必须明确 useModelImage=false。"
? "你只需要根据当前参考图判断:这一屏是否存在真实的人物展示需求;如果没有人物需求,useModelImage 必须为 false。"
: "本轮没有模特图,useModelImage 必须为 false。",
"最后一张图片是唯一的详情参考图底板,它决定当前这一屏的版式、文字排版、机位、构图、留白和信息层级。",
"这张图片是唯一的详情参考图底板,它决定当前这一屏的版式、文字排版、机位、构图、留白和信息层级。",
"请完成以下任务:",
"1. 判断 shouldReplace:当前参考图里是否存在清晰、主要、适合被替换的家具商品主体。",
"2. 给出 selectedProductImageIndexes:从产品图中挑出最适合当前这一屏的产品角度索引(从1开始计数)。",
"3. 判断 useModelImage,并说明原因。",
"4. 提炼 sceneSummary、cameraSummary、placementSummary、preserveSummary、styleSummary、avoidSummary。",
"5. 输出 generationPrompt:这是一段给后续生图模型的本屏生成指令,必须明确“只按当前参考图这一屏需要使用产品角度图,不需要把所有产品图全部融进结果”。",
"2. 判断 useModelImage,并说明原因。",
"3. 提炼 sceneSummary、cameraSummary、placementSummary、preserveSummary、styleSummary、avoidSummary。",
"4. 输出 generationPrompt:这是一段给后续生图模型的本屏生成指令,描述这一屏需要的场景、机位、主体位置和必须保留的内容。",
workflowPromptExcerpt
? `当前风格与场景限制(来自前置工作流与模式模板):\n${workflowPromptExcerpt}`
: "",
"只返回严格 JSON,不要任何额外解释。",
`JSON 格式:
{"shouldReplace":true,"containsFurnitureProduct":true,"reason":"不超过30字","selectedProductImageIndexes":[1,2],"productSelectionReason":"不超过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字","useModelImage":false,"modelImageReason":"不超过40字","sceneSummary":"不超过80字","cameraSummary":"不超过60字","placementSummary":"不超过60字","preserveSummary":"不超过80字","styleSummary":"不超过80字","avoidSummary":"不超过80字","generationPrompt":"不超过300字"}`,
]
.filter(Boolean)
.join("\n");
@ -3451,10 +3431,10 @@ async function analyzeReplacementExecutionPlan(params: {
},
],
system:
"你是高精度商品详情图场景规划器。重点是:多张产品图只是同一产品的多视角信息源,不是必须全部同时使用。你必须根据参考图当前这一屏的真实需求筛选少量最合适的产品图。",
"你是高精度商品详情图场景规划器。你只分析当前参考图本身,负责理解这一屏的场景、机位、主体位置和人物需求,不要臆测或依赖其他输入图片。",
temperature: params.planningRuntime.kind === "local_moonshot" ? 1 : 0.1,
maxTokens: 1200,
abortSignal: AbortSignal.timeout(45_000),
abortSignal: AbortSignal.timeout(replacementScenePlanningTimeoutMs),
} as any);
const parsedDecision = parseReplacementReferenceDecision(text);
@ -6564,9 +6544,14 @@ export async function POST(request: Request) {
await resolveReplacementScenePlanningRuntime({
models: replacementVisionModels,
});
const replacementScenePlanningTimeoutMs =
replacementScenePlanningRuntime?.kind === "local_moonshot"
? 90_000
: 45_000;
console.info("[Chat API] Replacement scene planning runtime:", {
kind: replacementScenePlanningRuntime?.kind || "none",
modelName: replacementScenePlanningRuntime?.modelName || null,
timeoutMs: replacementScenePlanningTimeoutMs,
});
const stopKeepalive = startStreamKeepalive(
writer,