auto-save 2026-05-17 20:15 (~4)

This commit is contained in:
2026-05-17 20:15:13 +08:00
parent d32e87a376
commit 72aef99592
4 changed files with 272 additions and 44 deletions

View File

@@ -92,6 +92,8 @@ type ProductRefItem = {
view: string
background: string
useTags: string[]
orientation?: ProductViewAnalysisItem["orientation"]
landmarks: string[]
note: string
risk: string
source: "upload" | "ai"
@@ -100,11 +102,11 @@ type ProductRefItem = {
}
const PRODUCT_VIEW_SLOTS = [
{ value: "front", label: "正面", hint: "整体 U 形轮廓、开口宽度、主外观" },
{ value: "left_45", label: "左 45", hint: "左侧弧度、按钮/结构差异" },
{ value: "right_45", label: "右 45", hint: "右侧弧度、另一侧非对称细节" },
{ value: "front", label: "正面/外侧", hint: "整体 U 形轮廓、开口宽度、外壳主外观" },
{ value: "left_45", label: "佩戴者左 45", hint: "戴在脖子上时佩戴者左肩一侧的弧度、按钮/结构差异" },
{ value: "right_45", label: "佩戴者右 45", hint: "戴在脖子上时佩戴者右肩一侧的弧度、非对称细节" },
{ value: "side_thickness", label: "侧面厚度", hint: "机身厚度、后颈包裹体积" },
{ value: "inner_contacts", label: "内侧触点", hint: "按摩触点、贴颈面、佩戴比例" },
{ value: "inner_contacts", label: "贴颈内侧/触点", hint: "按摩触点、贴颈面、内侧皮肤接触位置" },
{ value: "back_bottom", label: "背面/底部", hint: "底面、背部闭合结构、补缺" },
] as const
@@ -383,6 +385,40 @@ function normalizeProductUseTags(tags: string[] | undefined, view: string) {
return result.slice(0, 4)
}
function defaultProductLandmarks(view: string) {
const defaults: Record<string, string[]> = {
front: ["U形开口", "外壳主轮廓", "左右臂"],
left_45: ["佩戴者左侧臂", "侧边弧度", "按键/结构差异"],
right_45: ["佩戴者右侧臂", "侧边弧度", "按键/结构差异"],
side_thickness: ["机身厚度", "侧边轮廓", "佩戴比例"],
inner_contacts: ["贴颈内侧", "按摩触点", "皮肤接触面"],
back_bottom: ["背面/底部", "接口/底面", "材质细节"],
}
return defaults[view] ?? ["U形挂脖轮廓"]
}
function normalizeProductLandmarks(landmarks: string[] | undefined, view: string) {
const result: string[] = []
for (const item of [...(landmarks ?? []), ...defaultProductLandmarks(view)]) {
const text = item.trim()
if (text && !result.includes(text)) result.push(text)
}
return result.slice(0, 8)
}
function formatProductOrientation(orientation?: ProductViewAnalysisItem["orientation"]) {
if (!orientation) return ""
const parts = [
orientation.product_left ? `左=${orientation.product_left}` : "",
orientation.product_right ? `右=${orientation.product_right}` : "",
orientation.top ? `上=${orientation.top}` : "",
orientation.bottom ? `下=${orientation.bottom}` : "",
orientation.inner_side ? `内=${orientation.inner_side}` : "",
orientation.opening_direction ? `开口=${orientation.opening_direction}` : "",
].filter(Boolean)
return parts.join("")
}
function createProductRefItem(
ref: ImageRef,
index: number,
@@ -391,6 +427,8 @@ function createProductRefItem(
note?: string,
background = "unknown",
useTags?: string[],
orientation?: ProductViewAnalysisItem["orientation"],
landmarks?: string[],
risk = "",
confidence?: number,
): ProductRefItem {
@@ -402,6 +440,8 @@ function createProductRefItem(
view: view ?? targetSlot.value,
background,
useTags: normalizeProductUseTags(useTags, view ?? targetSlot.value),
orientation,
landmarks: normalizeProductLandmarks(landmarks, view ?? targetSlot.value),
note: note ?? targetSlot.hint,
risk,
source,
@@ -415,8 +455,11 @@ function productReferenceNotes(items: ProductRefItem[]) {
return items
.map((item, index) => {
const tags = item.useTags.map((tag) => PRODUCT_USE_TAG_LABELS[tag]).filter(Boolean).join("/")
const orientation = formatProductOrientation(item.orientation)
const direction = orientation ? `;方向:${orientation}` : ""
const landmarks = item.landmarks.length ? `;结构:${item.landmarks.join("/")}` : ""
const risk = item.risk ? `;风险:${item.risk}` : ""
return `${index + 1}. ${productViewLabel(item.view)}${productBackgroundLabel(item.background)}${tags}${item.note || "无补充备注"}${risk}`
return `${index + 1}. ${productViewLabel(item.view)}${productBackgroundLabel(item.background)}${tags}${item.note || "无补充备注"}${direction}${landmarks}${risk}`
})
.join("")
}
@@ -500,7 +543,7 @@ function buildStoryboardSceneFromAudioRow(row: AudioStoryboardRow, frame: KeyFra
const productRefs = selectedProductItems.map((item) => item.ref)
const notes = productReferenceNotes(selectedProductItems)
const productGuidance = productItems.length
? `产品素材池共有 ${productItems.length} 张,本条只选用 ${selectedProductItems.length} 张最相关参考图,不要把未选素材混入本条画面。所选图片只作为产品结构、角度、比例和细节参考,不要照搬参考图的白底/黑底/棚拍背景。视角标注:${notes}。保留左右非对称细节,不要把两边做成镜像对称;肩颈产品大小必须贴近真实佩戴比例,不能缩成耳机,也不能放大成护颈枕。`
? `产品素材池共有 ${productItems.length} 张,本条只选用 ${selectedProductItems.length} 张最相关参考图,不要把未选素材混入本条画面。产品硬定义:这是套在脖子上的 U 形肩颈按摩仪,不是耳机、头戴设备或护颈枕。坐标系硬规则:左/右按佩戴者身体左右,不能按图片左右;上=靠近下巴/脸/颈部上沿,下=靠近锁骨/肩部下沿;内侧=贴颈皮肤/按摩触点,外侧=外壳/按键/Logo。所选图片只作为产品结构、角度、比例和细节参考,不要照搬参考图的白底/黑底/棚拍背景。视角标注:${notes}。保留左右非对称细节,不要把两边做成镜像对称;肩颈产品大小必须贴近真实佩戴比例,不能缩成耳机,也不能放大成护颈枕。`
: "未上传产品图时使用默认 SKG 产品图;生成前建议先建立同一产品素材池,锁定左右差异、厚度和佩戴比例。"
return {
duration: Number(Math.max(3.2, Math.min(6.5, row.end - row.start || 4.5)).toFixed(1)),

View File

@@ -184,6 +184,16 @@ export interface ProductViewAnalysisItem {
view: string
background?: string
use_tags?: string[]
orientation?: {
product_left?: string
product_right?: string
top?: string
bottom?: string
inner_side?: string
outer_side?: string
opening_direction?: string
}
landmarks?: string[]
note: string
risk?: string
confidence: number