182 lines
7.3 KiB
TypeScript
182 lines
7.3 KiB
TypeScript
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);
|
||
}
|