auto-save 2026-05-17 16:27 (~4)
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
apiAssetUrl,
|
||||
cutoutElement,
|
||||
effectiveFrameUrl,
|
||||
generateProductAngleAsset,
|
||||
generateSubjectAssets,
|
||||
generatedImageUrl,
|
||||
hasCutout,
|
||||
@@ -82,6 +83,23 @@ type AudioStoryboardRow = {
|
||||
productIntegration: string
|
||||
}
|
||||
|
||||
type ProductRefItem = {
|
||||
id: string
|
||||
ref: ImageRef
|
||||
view: string
|
||||
note: string
|
||||
source: "upload" | "ai"
|
||||
}
|
||||
|
||||
const PRODUCT_VIEW_SLOTS = [
|
||||
{ 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: "back_bottom", label: "背面/底部", hint: "底面、背部闭合结构、补缺" },
|
||||
] as const
|
||||
|
||||
const controlClass =
|
||||
"h-10 rounded-md border border-white/10 bg-black/55 px-3 text-[12px] text-white outline-none transition focus:border-cyan-300/60 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
|
||||
@@ -290,9 +308,37 @@ function buildAudioStoryboardRows(job: Job | null): AudioStoryboardRow[] {
|
||||
})
|
||||
}
|
||||
|
||||
function buildStoryboardSceneFromAudioRow(row: AudioStoryboardRow, frame: KeyFrame, nextFrame?: KeyFrame | null, productRefs: ImageRef[] = []): StoryboardScene {
|
||||
const productGuidance = productRefs.length
|
||||
? "产品白底图已上传:生成时必须同时参考正面、左侧、右侧、厚度和内侧触点/佩戴比例,保留左右非对称细节,不要把两边做成镜像对称;肩颈产品大小必须贴近真实佩戴比例,不能缩成耳机,也不能放大成护颈枕。"
|
||||
function productRefKey(ref: ImageRef, index: number) {
|
||||
return `${ref.kind}:${ref.frame_idx}:${ref.element_id ?? ""}:${ref.cutout_id ?? ""}:${index}`
|
||||
}
|
||||
|
||||
function productViewLabel(view: string) {
|
||||
return PRODUCT_VIEW_SLOTS.find((slot) => slot.value === view)?.label ?? view
|
||||
}
|
||||
|
||||
function createProductRefItem(ref: ImageRef, index: number, source: ProductRefItem["source"] = "upload", view?: string): ProductRefItem {
|
||||
const slot = PRODUCT_VIEW_SLOTS[index] ?? PRODUCT_VIEW_SLOTS[PRODUCT_VIEW_SLOTS.length - 1]
|
||||
return {
|
||||
id: productRefKey(ref, index),
|
||||
ref,
|
||||
view: view ?? slot.value,
|
||||
note: slot.hint,
|
||||
source,
|
||||
}
|
||||
}
|
||||
|
||||
function productReferenceNotes(items: ProductRefItem[]) {
|
||||
if (!items.length) return ""
|
||||
return items
|
||||
.map((item, index) => `${index + 1}. ${productViewLabel(item.view)}:${item.note || "无补充备注"}`)
|
||||
.join(";")
|
||||
}
|
||||
|
||||
function buildStoryboardSceneFromAudioRow(row: AudioStoryboardRow, frame: KeyFrame, nextFrame?: KeyFrame | null, productItems: ProductRefItem[] = []): StoryboardScene {
|
||||
const productRefs = productItems.map((item) => item.ref)
|
||||
const notes = productReferenceNotes(productItems)
|
||||
const productGuidance = productItems.length
|
||||
? `产品白底图已上传:生成时必须同时参考各视角备注。视角备注:${notes}。保留左右非对称细节,不要把两边做成镜像对称;肩颈产品大小必须贴近真实佩戴比例,不能缩成耳机,也不能放大成护颈枕。`
|
||||
: "未上传产品白底图时使用默认 SKG 产品图;生成前建议补 5 张白底图锁定左右差异、厚度和佩戴比例。"
|
||||
return {
|
||||
duration: Number(Math.max(3.2, Math.min(6.5, row.end - row.start || 4.5)).toFixed(1)),
|
||||
|
||||
@@ -147,6 +147,22 @@ export async function uploadStoryboardAsset(jobId: string, file: File): Promise<
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function generateProductAngleAsset(
|
||||
jobId: string,
|
||||
body: { source_ref: ImageRef; target_view: string; note?: string },
|
||||
): Promise<ImageRef> {
|
||||
const res = await fetch(`${API_BASE}/jobs/${jobId}/assets/product-angle`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const txt = await res.text().catch(() => "")
|
||||
throw new Error(`generateProductAngleAsset ${res.status} ${txt.slice(0, 300)}`)
|
||||
}
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function listProductLibrary(): Promise<ProductLibraryItem[]> {
|
||||
const res = await fetch(`${API_BASE}/product-library/skg`)
|
||||
if (!res.ok) {
|
||||
|
||||
Reference in New Issue
Block a user