ai-chatbot/lib/ai/replacement-product-identity.ts

182 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string, unknown>;
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<string, unknown>;
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);
}