export type ReplacementProductViewSummary = { imageIndex: number; cameraSummary: string; visibleStructure: string; }; export type ReplacementProductIdentityProfile = { identitySummary: string; structureSummary: string; structuralRisks: string; viewSummaries: ReplacementProductViewSummary[]; }; function extractJsonObject(value: string) { const source = String(value || "").trim(); const fenced = source.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1]?.trim(); const candidate = fenced || source; const start = candidate.indexOf("{"); const end = candidate.lastIndexOf("}"); return start >= 0 && end > start ? candidate.slice(start, end + 1) : null; } function normalizeText(value: unknown, maxLength: number) { return String(value || "") .trim() .replace(/\s+/g, " ") .slice(0, maxLength); } export function normalizeReplacementProductImageIndexes( value: unknown, productImageCount: number, maxSelected = 2, ) { if (!Array.isArray(value)) return []; const safeCount = Math.max(0, Math.floor(productImageCount)); const safeLimit = Math.max(1, Math.floor(maxSelected)); const selected: number[] = []; value.forEach((item) => { const index = Math.round(Number(item)); if ( Number.isFinite(index) && index >= 1 && index <= safeCount && !selected.includes(index) ) { selected.push(index); } }); return selected.slice(0, safeLimit); } export function parseReplacementProductIdentityProfile( value: string, productImageCount: number, ): ReplacementProductIdentityProfile | null { const jsonText = extractJsonObject(value); if (!jsonText) return null; try { const parsed = JSON.parse(jsonText) as Record; const rawViews = Array.isArray(parsed.viewSummaries) ? parsed.viewSummaries : Array.isArray(parsed.views) ? parsed.views : []; const viewSummaries = rawViews .map((value) => { if (!value || typeof value !== "object" || Array.isArray(value)) { return null; } const record = value as Record; const [imageIndex] = normalizeReplacementProductImageIndexes( [record.imageIndex ?? record.index], productImageCount, 1, ); if (!imageIndex) return null; return { imageIndex, cameraSummary: normalizeText( record.cameraSummary ?? record.camera ?? record.view, 120, ), visibleStructure: normalizeText( record.visibleStructure ?? record.visibleFeatures ?? record.structure, 180, ), } satisfies ReplacementProductViewSummary; }) .filter((value): value is ReplacementProductViewSummary => Boolean(value)) .filter( (value, index, values) => values.findIndex((candidate) => candidate.imageIndex === value.imageIndex) === index, ) .slice(0, Math.max(0, Math.floor(productImageCount))); const identitySummary = normalizeText( parsed.identitySummary ?? parsed.productSummary ?? parsed.identity, 260, ); const structureSummary = normalizeText( parsed.structureSummary ?? parsed.structuralInvariants ?? parsed.structure, 520, ); const structuralRisks = normalizeText( parsed.structuralRisks ?? parsed.forbiddenMutations ?? parsed.risks, 320, ); if (!identitySummary && !structureSummary && viewSummaries.length === 0) { return null; } return { identitySummary, structureSummary, structuralRisks, viewSummaries, }; } catch { return null; } } export function buildReplacementProductIdentityAnalysisPrompt(productImageCount: number) { const safeCount = Math.max(1, Math.floor(productImageCount)); return [ "你是商品结构身份分析器。下面按顺序提供同一商品的多角度产品图。", `共有 ${safeCount} 张图,图片编号从 1 到 ${safeCount}。`, "只分析商品本身,不分析透明背景、白边、阴影或后期抠图瑕疵。", "identitySummary:概括商品类别、整体轮廓、材质、颜色和稳定识别特征。", "structureSummary:列出不可改变的几何拓扑,包括构件数量、左右对应关系、腿/支架/横梁/侧框/层板/把手/转轴的形状、粗细、连接点、开口方向、排列和比例。不要把不同视角下的同一构件误判为不同设计。", "structuralRisks:列出最容易被生成模型误画的结构,以及必须禁止的新增、删除、弯曲、分叉、熔合、悬空、错接、镜像和左右不一致。", "viewSummaries:逐图说明拍摄角度、可见面和最清楚展示的结构,imageIndex 必须使用 1 开始的原始图片编号。", "对于桌、椅、柜架、置物架等家具,必须明确腿型和框架拓扑,例如口字型、K/X/A/V 型或直腿;不得含糊写成‘保持结构一致’。", "只返回严格 JSON,不要解释。", '{"identitySummary":"不超过100字","structureSummary":"不超过220字","structuralRisks":"不超过140字","viewSummaries":[{"imageIndex":1,"cameraSummary":"不超过50字","visibleStructure":"不超过80字"}]}', ].join("\n"); } export function formatReplacementProductIdentityProfile( profile: ReplacementProductIdentityProfile, ) { return [ profile.identitySummary ? `商品身份:${profile.identitySummary}` : "", profile.structureSummary ? `结构拓扑:${profile.structureSummary}` : "", profile.structuralRisks ? `高风险误画:${profile.structuralRisks}` : "", ...profile.viewSummaries.map( (view) => `产品图${view.imageIndex}:${[view.cameraSummary, view.visibleStructure] .filter(Boolean) .join(";")}`, ), ] .filter(Boolean) .join("\n"); } export function buildReplacementProductStructureLockLines(params: { productImageCount: number; identitySummary?: string | null; structureSummary?: string | null; structuralRisks?: string | null; }) { if (params.productImageCount <= 0) return []; return [ "【产品结构身份锁(高于旧详情图中的旧产品结构)】", params.identitySummary ? `目标商品身份:${params.identitySummary}` : "", params.structureSummary ? `目标商品结构拓扑:${params.structureSummary}` : "", params.structuralRisks ? `重点防错:${params.structuralRisks}` : "", "前面的产品输入图共同定义要换入的新产品;最后一张详情底板只决定新产品在页面中的拍摄角度、朝向、占比、位置和透视,绝不决定新产品的腿型、支撑杆拓扑、构件数量或连接方式。", "必须逐项核对商品主体外轮廓、构件数量、左右对应关系、粗细、直线/曲线方向、开口方向,以及腿、支架、横梁、侧框、层板、把手和转轴的真实连接点;所有可见构件必须连续、合理承重且前后一致。", "对于桌、椅、柜架和置物架,严禁把口字型框架改成 K/X/A/V 型或相反,严禁新增、删除、弯曲、分叉、熔合、悬空、错接、镜像任何腿和横梁,严禁左右两侧生成不同结构。", "旧详情图里的旧商品结构必须完整移除,不得与新产品结构混合。结构准确优先于风格化和无痕融合;无法兼顾时宁可减少改动,也不能改造产品结构。", ].filter(Boolean); }