auto-save 2026-05-17 21:36 (~4)

This commit is contained in:
2026-05-17 21:36:46 +08:00
parent ab2d0a8978
commit 97a1f66140
4 changed files with 109 additions and 34 deletions

View File

@@ -1,24 +1,5 @@
{
"entries": [
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 1 项未提交变更 · 最近提交auto-save 2026-05-15 12:53 (~1)",
"ts": "2026-05-15T04:54:45Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"hash": "1c75b08",
"message": "auto-save 2026-05-15 12:59 (~1)",
"ts": "2026-05-15T12:59:28+08:00",
"type": "commit"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 1 项未提交变更 · 最近提交auto-save 2026-05-15 12:59 (~1)",
"ts": "2026-05-15T05:04:45Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"hash": "4df2601",
@@ -3261,6 +3242,25 @@
"message": "auto-save 2026-05-17 21:09 (~4)",
"hash": "252cdf4",
"files_changed": 4
},
{
"ts": "2026-05-17T21:14:42+08:00",
"type": "commit",
"message": "auto-save 2026-05-17 21:14 (~3)",
"hash": "ab2d0a8",
"files_changed": 3
},
{
"ts": "2026-05-17T13:18:29Z",
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 分支 main · 1 项未提交变更 · 最近提交auto-save 2026-05-17 21:14 (~3)",
"files_changed": 1
},
{
"ts": "2026-05-17T13:28:29Z",
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 分支 main · 1 项未提交变更 · 最近提交auto-save 2026-05-17 21:14 (~3)",
"files_changed": 1
}
]
}

View File

@@ -481,6 +481,7 @@ class Job(BaseModel):
audio_script: AudioScript = Field(default_factory=AudioScript)
storyboard_images: list[StoryboardImage] = Field(default_factory=list)
generated_videos: list[GeneratedVideo] = Field(default_factory=list)
product_refs: list[dict] = Field(default_factory=list)
error: str = ""
@@ -3453,6 +3454,23 @@ class GenerateSubjectAssetsReq(BaseModel):
prompt: str = ""
class UpdateProductRefsReq(BaseModel):
items: list[dict] = Field(default_factory=list)
@app.put("/jobs/{job_id}/product-refs", response_model=Job)
def update_product_refs(job_id: str, req: UpdateProductRefsReq) -> Job:
job = JOBS.get(job_id)
if not job:
raise HTTPException(404, "job not found")
items: list[dict] = []
for item in req.items[:300]:
if isinstance(item, dict) and isinstance(item.get("ref"), dict):
items.append(item)
update(job, product_refs=items)
return job
@app.post("/jobs/{job_id}/frames/{idx}/elements", response_model=Job)
def add_element(job_id: str, idx: int, req: AddElementReq) -> Job:
"""加一条元素 · 若 name_en 缺则自动 zh→en 翻译"""

View File

@@ -17,6 +17,7 @@ import {
type KeyElement,
type KeyFrame,
type ProductViewAnalysisItem,
type ProductRefStateItem,
type StoryboardScriptRewriteSegment,
type StoryboardScene,
type SubjectAsset,
@@ -34,6 +35,7 @@ import {
representativeCutoutUrl,
resolveImageRefUrl,
rewriteStoryboardScript,
saveProductRefs,
sourceAudioUrl,
updateStoryboard,
uploadStoryboardAsset,
@@ -90,20 +92,7 @@ type AudioStoryboardRow = {
productIntegration: string
}
type ProductRefItem = {
id: string
ref: ImageRef
view: string
background: string
useTags: string[]
orientation?: ProductViewAnalysisItem["orientation"]
landmarks: string[]
note: string
risk: string
source: "upload" | "ai"
assetMeta?: ImageRef["asset_meta"]
confidence?: number
}
type ProductRefItem = ProductRefStateItem
const PRODUCT_VIEW_SLOTS = [
{ value: "front", label: "正面/外侧", hint: "整体 U 形轮廓、开口宽度、外壳主外观" },
@@ -471,6 +460,28 @@ function createProductRefItem(
}
}
function normalizeStoredProductItem(item: ProductRefItem, index: number): ProductRefItem {
const ref = { ...item.ref, asset_meta: item.ref.asset_meta ?? item.assetMeta }
const restored = createProductRefItem(
ref,
index,
item.source ?? "upload",
item.view,
item.note,
item.background ?? "unknown",
item.useTags,
item.orientation,
item.landmarks,
item.risk ?? "",
item.confidence,
)
return {
...restored,
id: item.id || restored.id,
assetMeta: item.assetMeta ?? restored.assetMeta,
}
}
function productReferenceNotes(items: ProductRefItem[]) {
if (!items.length) return ""
return items
@@ -1399,6 +1410,7 @@ function AudioStoryboardPlanPanel({
const [authorIntent, setAuthorIntent] = useState("")
const [scriptRewriteBusy, setScriptRewriteBusy] = useState<"all" | number | null>(null)
const productFileRef = useRef<HTMLInputElement | null>(null)
const productPersistSeq = useRef(0)
const rows = useMemo(() => buildAudioStoryboardRows(job), [job])
const orderedFrames = useMemo(() => job ? [...job.frames].sort((a, b) => a.timestamp - b.timestamp) : [], [job])
const selectedReferenceFrames = useMemo(
@@ -1408,12 +1420,28 @@ function AudioStoryboardPlanPanel({
const rowReferencePool = selectedReferenceFrames.length ? selectedReferenceFrames : orderedFrames
useEffect(() => {
setProductItems([])
setProductItems((job?.product_refs ?? []).map(normalizeStoredProductItem))
setCopyOverrides({})
setAuthorIntent("")
setScriptRewriteBusy(null)
}, [job?.id])
const persistProductItems = async (items: ProductRefItem[]) => {
if (!job) return
const seq = ++productPersistSeq.current
try {
const updated = await saveProductRefs(job.id, items)
if (seq === productPersistSeq.current) onJobUpdate?.(updated)
} catch (e) {
console.warn("产品素材池保存失败", e)
}
}
const setAndPersistProductItems = (items: ProductRefItem[]) => {
setProductItems(items)
void persistProductItems(items)
}
const copyForRow = (row: AudioStoryboardRow) => copyOverrides[row.index] ?? row.skgCopy
const patchRowCopy = (rowIndex: number, value: string) => {

View File

@@ -244,6 +244,19 @@ export async function analyzeProductViews(
return res.json()
}
export async function saveProductRefs(jobId: string, items: ProductRefStateItem[]): Promise<Job> {
const res = await fetch(`${API_BASE}/jobs/${jobId}/product-refs`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ items }),
})
if (!res.ok) {
const txt = await res.text().catch(() => "")
throw new Error(`saveProductRefs ${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) {
@@ -476,6 +489,21 @@ export interface StoryboardImage {
created_at?: number
}
export interface ProductRefStateItem {
id: string
ref: ImageRef
view: string
background: string
useTags: string[]
orientation?: ProductViewAnalysisItem["orientation"]
landmarks: string[]
note: string
risk: string
source: "upload" | "ai"
assetMeta?: ImageRef["asset_meta"]
confidence?: number
}
export interface Job {
id: string
url: string
@@ -492,6 +520,7 @@ export interface Job {
audio_script?: AudioScript
storyboard_images?: StoryboardImage[]
generated_videos?: GeneratedVideo[]
product_refs?: ProductRefStateItem[]
error?: string
}