From 97a1f6614087c69268a667c01d776410bbad99a2 Mon Sep 17 00:00:00 2001 From: kang Date: Sun, 17 May 2026 21:36:46 +0800 Subject: [PATCH] auto-save 2026-05-17 21:36 (~4) --- .memory/worklog.json | 38 ++++++++--------- api/main.py | 18 ++++++++ web/components/ad-recreation-board.tsx | 58 +++++++++++++++++++------- web/lib/api.ts | 29 +++++++++++++ 4 files changed, 109 insertions(+), 34 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index d7f5733..8284456 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -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 } ] } diff --git a/api/main.py b/api/main.py index 998573c..4a9dce6 100644 --- a/api/main.py +++ b/api/main.py @@ -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 翻译""" diff --git a/web/components/ad-recreation-board.tsx b/web/components/ad-recreation-board.tsx index f8434ae..ac750df 100644 --- a/web/components/ad-recreation-board.tsx +++ b/web/components/ad-recreation-board.tsx @@ -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(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) => { diff --git a/web/lib/api.ts b/web/lib/api.ts index b913806..3453c19 100644 --- a/web/lib/api.ts +++ b/web/lib/api.ts @@ -244,6 +244,19 @@ export async function analyzeProductViews( return res.json() } +export async function saveProductRefs(jobId: string, items: ProductRefStateItem[]): Promise { + 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 { 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 }