auto-save 2026-05-17 21:42 (~3)
This commit is contained in:
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -682,7 +682,8 @@ api/main.py
|
||||
frames: KeyFrame[],
|
||||
transcript: TranscriptSegment[],
|
||||
audio_script: AudioScript,
|
||||
storyboard_images?: StoryboardImage[]
|
||||
storyboard_images?: StoryboardImage[],
|
||||
product_refs?: ProductRefStateItem[]
|
||||
}</pre>
|
||||
</div>
|
||||
<div class="card">
|
||||
@@ -795,7 +796,7 @@ SubjectAsset {
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>ProductViewAnalysisItem</h3>
|
||||
<p>产品素材池识别结果。它不判断不同产品身份,只服务同一款挂脖肩颈按摩仪的生视频选图和方向约束;左/右按佩戴者身体左右,不按图片左右。</p>
|
||||
<p>产品素材池识别结果。它不判断不同产品身份,只服务同一款挂脖肩颈按摩仪的生视频选图和方向约束;左/右按佩戴者身体左右,不按图片左右。前端会把上传图、识别标注、AI 补图、备注和删除结果写入 <code>Job.product_refs</code>,后端保存到 <code>state.json</code>,避免刷新、热更新或服务重启后丢失产品素材池。</p>
|
||||
<pre>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
|
||||
}</pre>
|
||||
</div>
|
||||
<div class="card">
|
||||
@@ -873,7 +889,8 @@ SubjectAsset {
|
||||
<tr><td>主体资产包</td><td><code>POST /elements/{element_id}/subject-assets</code></td><td><code>generateSubjectAssets</code></td><td>根据参考帧重新绘制一个统一主体资产包;前端默认把全部关键帧作为 <code>source_frame_indices</code>,如果用户手动选择了关键帧则只传已选帧,后端拼参考板。新增 <code>subject_style=source_actor</code> 与 <code>reconstruction_mode=similar</code> 用于信息流相似主角:最多读取 12 张已选关键帧,生成 6 张白底新演员视图,保留角色气质、动作词汇、机位和服装类别,但不复刻源人物身份或像素。旧透明骨架人流程仍默认走 <code>subject_style=transparent_human</code>。</td></tr>
|
||||
<tr><td>首尾帧资产</td><td><code>POST /frames/{idx}/scene-asset</code></td><td><code>generateSceneAsset</code></td><td>同一接口兼容旧场景图和新首尾帧;新流程传 <code>asset_role=first_frame/last_frame</code>,后端走文字生图,参考帧只用于理解透明骨架人形象、比例、机位和光线,生成结果仍保存在 <code>scene_assets</code> 并自动填入产品融合镜头。</td></tr>
|
||||
<tr><td>产品图库</td><td><code>GET /product-library/skg</code></td><td><code>listProductLibrary</code></td><td>读取内置 SKG 白底图库 manifest,返回产品标题、品类、尺寸、白底评分和预览图 URL。</td></tr>
|
||||
<tr><td>产品图入库到 job</td><td><code>POST /jobs/{id}/assets</code>、<code>POST /jobs/{id}/assets/product-library</code></td><td><code>uploadStoryboardAsset</code>、<code>copyProductLibraryAsset</code></td><td>上传产品图或把内置产品图库条目复制为当前 job 的普通 asset。后端统一生成最长边 1600px、JPEG 92 的 AI 工作副本,透明底铺白,过大/过小图片会在 <code>ImageRef.asset_meta</code> 里返回转换动作和风险;黑底/白底背景本身不强行转换。</td></tr>
|
||||
<tr><td>产品图入库到 job</td><td><code>POST /jobs/{id}/assets</code>、<code>POST /jobs/{id}/assets/product-library</code></td><td><code>uploadStoryboardAsset</code>、<code>copyProductLibraryAsset</code></td><td>上传产品图或把内置产品图库条目复制为当前 job 的普通 asset。后端统一生成最长边 1600px、JPEG 92 的 AI 工作副本,透明底铺白,过大/过小图片会在 <code>ImageRef.asset_meta</code> 里返回转换动作和风险;黑底/白底背景本身不强行转换。注意该接口只写图片文件,产品素材池列表另由 <code>PUT /jobs/{id}/product-refs</code> 持久化。</td></tr>
|
||||
<tr><td>产品素材池保存</td><td><code>PUT /jobs/{id}/product-refs</code></td><td><code>saveProductRefs</code></td><td>把当前 job 的产品素材池列表、识别视角、用途标签、方向、结构点、备注、AI 补图和删除结果保存到 <code>Job.product_refs</code> / <code>state.json</code>。前端上传、识别完成、补角度、编辑备注和删除时都会同步保存;刷新页面或热更新后从 job 恢复,不再要求重新上传和重新识别。</td></tr>
|
||||
<tr><td>产品视角识别</td><td><code>POST /jobs/{id}/assets/product-views/analyze</code></td><td><code>analyzeProductViews</code></td><td>读取同一产品素材池,按批次把多张图一次性提交给视觉模型,不限制只看前 6 张;识别对象被固定为套在脖子上的 U 形肩颈按摩仪。返回 <code>view</code>、<code>background</code>、<code>use_tags</code>、<code>orientation</code>、<code>landmarks</code>、中文备注、生成风险和置信度;<code>orientation</code> 明确佩戴者左/右、上/下、内外侧和开口方向对应图中哪边,避免把图片左右误当产品左右。前端不再要求用户手动选择视角,也不做不同产品身份判断。</td></tr>
|
||||
<tr><td>产品缺角度补图</td><td><code>POST /jobs/{id}/assets/product-angle</code></td><td><code>generateProductAngleAsset</code></td><td>用当前产品白底图作为参考,通过图像模型自动补全缺失视角,输出新的 <code>ImageRef(kind="asset")</code>。Prompt 会约束白底产品图、左右非对称、厚度、内侧触点和肩颈真实佩戴比例;前端只在自动补图失败时暴露重试入口。</td></tr>
|
||||
<tr><td>角色库</td><td><code>GET /character-library/skg</code></td><td><code>listCharacterLibrary</code></td><td>读取内置 5 个透明骨架人角色 manifest,每个角色含正面、左右 45 度、侧面、背面、半身近景和背部特写 7 张参考图。</td></tr>
|
||||
@@ -985,6 +1002,19 @@ SubjectAsset {
|
||||
<h2>变更记录</h2>
|
||||
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
|
||||
<div class="changelog">
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-05-17 · 产品素材池持久化到 job</h3>
|
||||
<span class="tag blue">API</span>
|
||||
<span class="tag rose">UI</span>
|
||||
<span class="tag cyan">Workflow</span>
|
||||
</header>
|
||||
<div class="body">
|
||||
<p><strong>问题:</strong>产品图文件虽然已写入 <code>jobs/<jobId>/assets</code>,但产品素材池列表、识别结果、备注和 AI 补图只存在前端 React state;代码热更新、刷新或切换任务后会清空,用户需要重新上传和重新计算。</p>
|
||||
<p><strong>改动:</strong><code>Job</code> 新增 <code>product_refs</code>;后端新增 <code>PUT /jobs/{id}/product-refs</code>,前端新增 <code>saveProductRefs</code>。<code>AudioStoryboardPlanPanel</code> 在上传、识别、补图、备注编辑和删除时同步保存产品素材池,组件重新挂载时从 <code>job.product_refs</code> 恢复。</p>
|
||||
<p><strong>影响:</strong><code>api/main.py</code>、<code>web/lib/api.ts</code>、<code>web/components/ad-recreation-board.tsx</code>、<code>docs/source-analysis.html</code>。后续更新 UI 或重启本地服务不应再清掉用户已上传的产品图和标注。</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-05-17 · 参考帧改为原视频旁全局关键帧与相似主角</h3>
|
||||
|
||||
@@ -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<ProductRefItem>) => {
|
||||
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)))
|
||||
|
||||
Reference in New Issue
Block a user