diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 37a3c9864..73bbfcada 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -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; - 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 { 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,