From bd86140cf6a337eb6ed53d7719a0d115375ff283 Mon Sep 17 00:00:00 2001 From: kang Date: Sun, 17 May 2026 21:42:09 +0800 Subject: [PATCH] auto-save 2026-05-17 21:42 (~3) --- .memory/worklog.json | 27 ++++++++++--------- docs/source-analysis.html | 36 +++++++++++++++++++++++--- web/components/ad-recreation-board.tsx | 33 ++++++++++++++++------- 3 files changed, 69 insertions(+), 27 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index 8284456..0e2e1ad 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1,19 +1,5 @@ { "entries": [ - { - "files_changed": 1, - "hash": "4df2601", - "message": "auto-save 2026-05-15 13:05 (~1)", - "ts": "2026-05-15T13:05:21+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "hash": "1e84642", - "message": "auto-save 2026-05-15 13:11 (~1)", - "ts": "2026-05-15T13:11:13+08:00", - "type": "commit" - }, { "files_changed": 1, "message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 13:11 (~1)", @@ -3261,6 +3247,19 @@ "type": "session-heartbeat", "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:auto-save 2026-05-17 21:14 (~3)", "files_changed": 1 + }, + { + "ts": "2026-05-17T21:36:46+08:00", + "type": "commit", + "message": "auto-save 2026-05-17 21:36 (~4)", + "hash": "97a1f66", + "files_changed": 4 + }, + { + "ts": "2026-05-17T13:38:30Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 3 项未提交变更 · 最近提交:auto-save 2026-05-17 21:36 (~4)", + "files_changed": 3 } ] } diff --git a/docs/source-analysis.html b/docs/source-analysis.html index 0477207..e844d39 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -682,7 +682,8 @@ api/main.py frames: KeyFrame[], transcript: TranscriptSegment[], audio_script: AudioScript, - storyboard_images?: StoryboardImage[] + storyboard_images?: StoryboardImage[], + product_refs?: ProductRefStateItem[] }
@@ -795,7 +796,7 @@ SubjectAsset {

ProductViewAnalysisItem

-

产品素材池识别结果。它不判断不同产品身份,只服务同一款挂脖肩颈按摩仪的生视频选图和方向约束;左/右按佩戴者身体左右,不按图片左右。

+

产品素材池识别结果。它不判断不同产品身份,只服务同一款挂脖肩颈按摩仪的生视频选图和方向约束;左/右按佩戴者身体左右,不按图片左右。前端会把上传图、识别标注、AI 补图、备注和删除结果写入 Job.product_refs,后端保存到 state.json,避免刷新、热更新或服务重启后丢失产品素材池。

ProductViewAnalysisItem {
   index,
   view: front | left_45 | right_45 | side_thickness | inner_contacts | back_bottom,
@@ -811,6 +812,21 @@ SubjectAsset {
   note,
   risk,
   confidence
+}
+
+ProductRefStateItem {
+  id,
+  ref: ImageRef,
+  view,
+  background,
+  useTags[],
+  orientation,
+  landmarks[],
+  note,
+  risk,
+  source: upload | ai,
+  assetMeta,
+  confidence
 }
@@ -873,7 +889,8 @@ SubjectAsset { 主体资产包POST /elements/{element_id}/subject-assetsgenerateSubjectAssets根据参考帧重新绘制一个统一主体资产包;前端默认把全部关键帧作为 source_frame_indices,如果用户手动选择了关键帧则只传已选帧,后端拼参考板。新增 subject_style=source_actorreconstruction_mode=similar 用于信息流相似主角:最多读取 12 张已选关键帧,生成 6 张白底新演员视图,保留角色气质、动作词汇、机位和服装类别,但不复刻源人物身份或像素。旧透明骨架人流程仍默认走 subject_style=transparent_human。 首尾帧资产POST /frames/{idx}/scene-assetgenerateSceneAsset同一接口兼容旧场景图和新首尾帧;新流程传 asset_role=first_frame/last_frame,后端走文字生图,参考帧只用于理解透明骨架人形象、比例、机位和光线,生成结果仍保存在 scene_assets 并自动填入产品融合镜头。 产品图库GET /product-library/skglistProductLibrary读取内置 SKG 白底图库 manifest,返回产品标题、品类、尺寸、白底评分和预览图 URL。 - 产品图入库到 jobPOST /jobs/{id}/assetsPOST /jobs/{id}/assets/product-libraryuploadStoryboardAssetcopyProductLibraryAsset上传产品图或把内置产品图库条目复制为当前 job 的普通 asset。后端统一生成最长边 1600px、JPEG 92 的 AI 工作副本,透明底铺白,过大/过小图片会在 ImageRef.asset_meta 里返回转换动作和风险;黑底/白底背景本身不强行转换。 + 产品图入库到 jobPOST /jobs/{id}/assetsPOST /jobs/{id}/assets/product-libraryuploadStoryboardAssetcopyProductLibraryAsset上传产品图或把内置产品图库条目复制为当前 job 的普通 asset。后端统一生成最长边 1600px、JPEG 92 的 AI 工作副本,透明底铺白,过大/过小图片会在 ImageRef.asset_meta 里返回转换动作和风险;黑底/白底背景本身不强行转换。注意该接口只写图片文件,产品素材池列表另由 PUT /jobs/{id}/product-refs 持久化。 + 产品素材池保存PUT /jobs/{id}/product-refssaveProductRefs把当前 job 的产品素材池列表、识别视角、用途标签、方向、结构点、备注、AI 补图和删除结果保存到 Job.product_refs / state.json。前端上传、识别完成、补角度、编辑备注和删除时都会同步保存;刷新页面或热更新后从 job 恢复,不再要求重新上传和重新识别。 产品视角识别POST /jobs/{id}/assets/product-views/analyzeanalyzeProductViews读取同一产品素材池,按批次把多张图一次性提交给视觉模型,不限制只看前 6 张;识别对象被固定为套在脖子上的 U 形肩颈按摩仪。返回 viewbackgrounduse_tagsorientationlandmarks、中文备注、生成风险和置信度;orientation 明确佩戴者左/右、上/下、内外侧和开口方向对应图中哪边,避免把图片左右误当产品左右。前端不再要求用户手动选择视角,也不做不同产品身份判断。 产品缺角度补图POST /jobs/{id}/assets/product-anglegenerateProductAngleAsset用当前产品白底图作为参考,通过图像模型自动补全缺失视角,输出新的 ImageRef(kind="asset")。Prompt 会约束白底产品图、左右非对称、厚度、内侧触点和肩颈真实佩戴比例;前端只在自动补图失败时暴露重试入口。 角色库GET /character-library/skglistCharacterLibrary读取内置 5 个透明骨架人角色 manifest,每个角色含正面、左右 45 度、侧面、背面、半身近景和背部特写 7 张参考图。 @@ -985,6 +1002,19 @@ SubjectAsset {

变更记录

这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。

+
+
+

2026-05-17 · 产品素材池持久化到 job

+ API + UI + Workflow +
+
+

问题:产品图文件虽然已写入 jobs/<jobId>/assets,但产品素材池列表、识别结果、备注和 AI 补图只存在前端 React state;代码热更新、刷新或切换任务后会清空,用户需要重新上传和重新计算。

+

改动:Job 新增 product_refs;后端新增 PUT /jobs/{id}/product-refs,前端新增 saveProductRefsAudioStoryboardPlanPanel 在上传、识别、补图、备注编辑和删除时同步保存产品素材池,组件重新挂载时从 job.product_refs 恢复。

+

影响:api/main.pyweb/lib/api.tsweb/components/ad-recreation-board.tsxdocs/source-analysis.html。后续更新 UI 或重启本地服务不应再清掉用户已上传的产品图和标注。

+
+

2026-05-17 · 参考帧改为原视频旁全局关键帧与相似主角

diff --git a/web/components/ad-recreation-board.tsx b/web/components/ad-recreation-board.tsx index ac750df..9ee8ab8 100644 --- a/web/components/ad-recreation-board.tsx +++ b/web/components/ad-recreation-board.tsx @@ -1504,7 +1504,7 @@ function AudioStoryboardPlanPanel({ ...working, createProductRefItem(ref, working.length, "ai", slot.value, `AI 补齐:${slot.hint}`, "white", undefined, undefined, undefined, "", 1), ] - setProductItems(working) + setAndPersistProductItems(working) } catch (e) { failures.push(`${slot.label}:${e instanceof Error ? e.message : String(e)}`) } @@ -1520,16 +1520,17 @@ function AudioStoryboardPlanPanel({ const analyzeAndCompleteProductViews = async (refs: ImageRef[]) => { if (!job || !refs.length) return setProductAnalyzing(true) - setProductItems(refs.map((ref, index) => createProductRefItem(ref, index, itemSourceForRef(ref), undefined, "正在自动识别视角..."))) + const pending = refs.map((ref, index) => createProductRefItem(ref, index, itemSourceForRef(ref), undefined, "正在自动识别视角...")) + setAndPersistProductItems(pending) try { const analysis = await analyzeProductViews(job.id, refs) const analyzed = buildAnalyzedProductItems(refs, analysis.items) - setProductItems(analyzed) + setAndPersistProductItems(analyzed) const completed = await completeMissingProductAngles(analyzed) toast.success(completed.length > analyzed.length ? "产品视角已自动识别并补齐缺失角度" : "产品视角已自动识别") } catch (e) { const fallback = refs.map((ref, index) => createProductRefItem(ref, index, itemSourceForRef(ref))) - setProductItems(fallback) + setAndPersistProductItems(fallback) await completeMissingProductAngles(fallback) toast.warning("产品视角识别失败,已按默认顺序标注并尝试自动补图:" + (e instanceof Error ? e.message : String(e))) } finally { @@ -1543,7 +1544,7 @@ function AudioStoryboardPlanPanel({ const baseItems = productItems const startIndex = baseItems.length setProductAnalyzing(true) - setProductItems([ + setAndPersistProductItems([ ...baseItems, ...refs.map((ref, index) => createProductRefItem(ref, startIndex + index, "upload", undefined, "正在自动识别视角...")), ]) @@ -1551,13 +1552,13 @@ function AudioStoryboardPlanPanel({ const analysis = await analyzeProductViews(job.id, refs) const newItems = buildAnalyzedProductItems(refs, analysis.items, startIndex) const combined = [...baseItems, ...newItems] - setProductItems(combined) + setAndPersistProductItems(combined) const completed = await completeMissingProductAngles(combined) toast.success(completed.length > combined.length ? "新产品图已识别,并已补齐缺失角度" : "新产品图已自动识别") } catch (e) { const fallback = refs.map((ref, index) => createProductRefItem(ref, startIndex + index, "upload")) const combined = [...baseItems, ...fallback] - setProductItems(combined) + setAndPersistProductItems(combined) await completeMissingProductAngles(combined) toast.warning("新产品图识别失败,已按默认顺序标注并尝试自动补图:" + (e instanceof Error ? e.message : String(e))) } finally { @@ -1582,11 +1583,19 @@ function AudioStoryboardPlanPanel({ } const patchProductItem = (id: string, patch: Partial) => { - setProductItems((prev) => prev.map((item) => item.id === id ? { ...item, ...patch } : item)) + setProductItems((prev) => { + const next = prev.map((item) => item.id === id ? { ...item, ...patch } : item) + void persistProductItems(next) + return next + }) } const removeProductItem = (id: string) => { - setProductItems((prev) => prev.filter((item) => item.id !== id)) + setProductItems((prev) => { + const next = prev.filter((item) => item.id !== id) + void persistProductItems(next) + return next + }) } const reanalyzeProductViews = async () => { @@ -1651,7 +1660,11 @@ function AudioStoryboardPlanPanel({ target_view: slot.label, note: slot.hint, }) - setProductItems((prev) => [...prev, createProductRefItem(ref, prev.length, "ai", slot.value, `AI 补齐:${slot.hint}`, "white", undefined, undefined, undefined, "", 1)]) + setProductItems((prev) => { + const next = [...prev, createProductRefItem(ref, prev.length, "ai", slot.value, `AI 补齐:${slot.hint}`, "white", undefined, undefined, undefined, "", 1)] + void persistProductItems(next) + return next + }) toast.success(`AI 已补全产品视角:${slot.label}`) } catch (e) { toast.error("AI 补角度失败:" + (e instanceof Error ? e.message : String(e)))