auto-save 2026-05-14 05:54 (~3)

This commit is contained in:
2026-05-14 05:54:57 +08:00
parent a98639a33e
commit 6904a28c7a
3 changed files with 105 additions and 173 deletions

View File

@@ -1,19 +1,5 @@
{
"entries": [
{
"files_changed": 4,
"hash": "35b3278",
"message": "auto-save 2026-05-12 16:16 (~4)",
"ts": "2026-05-12T16:16:52+08:00",
"type": "commit"
},
{
"files_changed": 1,
"hash": "7283928",
"message": "auto-save 2026-05-12 16:22 (~1)",
"ts": "2026-05-12T16:22:23+08:00",
"type": "commit"
},
{
"files_changed": 1,
"hash": "03cd5b4",
@@ -3357,6 +3343,19 @@
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 2 项未提交变更 · 最近提交auto-save 2026-05-14 05:43 (~3)",
"files_changed": 2
},
{
"ts": "2026-05-14T05:49:26+08:00",
"type": "commit",
"message": "auto-save 2026-05-14 05:49 (~2)",
"hash": "a98639a",
"files_changed": 2
},
{
"ts": "2026-05-13T21:53:13Z",
"type": "session-heartbeat",
"message": "Claude 会话活跃 · 最近命令claude · 3 项未提交变更 · 最近提交auto-save 2026-05-14 05:49 (~2)",
"files_changed": 3
}
]
}

View File

@@ -623,7 +623,7 @@ api/main.py
</div>
<div class="flow-row">
<div><strong>你看到的区域</strong><span>关键帧素材审核面板</span></div>
<div><strong>主要源码</strong><span><code>FrameLightbox</code>;按“原图/清洗、场景图、主体资产、审核”四个页签组织;清洗页支持一键批量生成待审核清洗版,相关接口包括 <code>cleanupFrame</code><code>addElement</code><code>generateSceneAsset</code><code>generateSubjectAssets</code></span></div>
<div><strong>主要源码</strong><span><code>FrameLightbox</code>;按“原图/清洗、场景图、主体资产、审核”四个页签组织;非主体页采用左侧大图 + 右侧窄状态栏,主体资产页保留较宽右栏;清洗页支持一键批量生成待审核清洗版,相关接口包括 <code>cleanupFrame</code><code>addElement</code><code>generateSceneAsset</code><code>generateSubjectAssets</code></span></div>
<div><strong>适合怎么描述</strong><span>“某张关键帧的水印、场景图、主体多视角/动作/表情图和质量风险应该如何审核”。</span></div>
</div>
<div class="flow-row">
@@ -724,7 +724,7 @@ SubjectAsset {
<tr><td>解析视频</td><td><code>POST /jobs/{id}/analyze?frames=&amp;target=&amp;mode=&amp;quality=</code></td><td><code>analyzeJob</code></td><td>拆轨 + 目标化抽关键帧。<code>target</code> 支持综合、清晰主体、转场变化、表情瞬间、动作峰值;<code>mode=append</code> 追加新关键帧;<code>quality=auto</code> 根据本机算力和视频时长自动选择快速、精细或极准。多个抽帧请求进入后端队列顺序处理。</td></tr>
<tr><td>手动加帧</td><td><code>POST /jobs/{id}/frames?t=</code></td><td><code>addManualFrame</code></td><td>按视频时间戳抽一帧index 递增但 frames 按 timestamp 排序。</td></tr>
<tr><td>Vision 识别</td><td><code>POST /frames/{idx}/describe</code></td><td><code>describeFrame</code></td><td>写入 frame.description后续可从 objects 加候选元素。</td></tr>
<tr><td>清洗水印</td><td><code>POST /frames/{idx}/cleanup</code></td><td><code>cleanupFrame</code></td><td>支持全图和区域清洗,生成 cleaned 待应用版本;前端批量清洗会顺序调用该接口,不自动覆盖原图。</td></tr>
<tr><td>清洗水印</td><td><code>POST /frames/{idx}/cleanup</code></td><td><code>cleanupFrame</code></td><td>支持全图和区域清洗,生成 cleaned 待应用版本;前端批量清洗会顺序调用该接口,不自动覆盖原图。单帧清洗状态按 frame.index 隔离,清洗某一张不会禁用其他关键帧的清洗按钮。</td></tr>
<tr><td>应用清洗</td><td><code>POST /cleanup/apply</code></td><td><code>applyCleanedFrame</code></td><td>物理覆盖 frames/{idx}.jpg并备份原图。</td></tr>
<tr><td>元素增改删</td><td><code>POST/PATCH/DELETE /elements</code></td><td><code>addElement/updateElement/deleteElement</code></td><td>让用户修正 Vision 错误,避免候选结果锁死。</td></tr>
<tr><td>元素提取</td><td><code>POST /elements/{element_id}/cutout</code></td><td><code>cutoutElement</code></td><td>调用图像模型生成独立白底素材图,每次累积一张 cutout。</td></tr>
@@ -839,6 +839,30 @@ SubjectAsset {
<h2>变更记录</h2>
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
<div class="changelog">
<article class="change">
<header>
<h3>2026-05-14 · 关键帧素材面板右侧改为紧凑状态栏</h3>
<span class="tag violet">FrameLightbox</span>
<span class="tag blue">Layout</span>
</header>
<div class="body">
<p><strong>问题:</strong>移除旧元素提取和手工加主体入口后,关键帧详情右侧内容变少,继续占用大列会压缩左侧主图和清洗操作区。</p>
<p><strong>改动:</strong><code>FrameLightbox</code> 在“原图/清洗、场景图、审核”页把右侧改成固定窄状态栏,左侧主图和操作区获得更大宽度;“主体资产”页仍保留较宽右栏,用于主体识别、主体清单和资产包。</p>
<p><strong>影响:</strong><code>web/components/lightbox.tsx</code><code>docs/source-analysis.html</code></p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-14 · 单帧清洗不再全局锁住其他帧</h3>
<span class="tag violet">FrameLightbox</span>
<span class="tag blue">Bugfix</span>
</header>
<div class="body">
<p><strong>问题:</strong>单独清洗某一张关键帧时,前端使用全局 <code>cleaning</code> 布尔状态,导致切到其他关键帧后清洗按钮仍被禁用。</p>
<p><strong>改动:</strong><code>FrameLightbox</code> 改用 <code>cleaningFrameIds</code><code>frame.index</code> 记录正在清洗的帧,只禁用当前正在清洗的那一张;其他帧可以继续单独清洗。区域清洗完成时也只在用户仍停留于同一帧时清空当前框选,避免异步完成误清别的帧操作状态。</p>
<p><strong>影响:</strong><code>web/components/lightbox.tsx</code><code>docs/source-analysis.html</code></p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-14 · 清洗页支持一键批量清洗</h3>

View File

@@ -128,6 +128,8 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
...(f.quality_report?.warnings ?? []),
...(latestSceneAsset?.quality_report?.warnings ?? []),
]
const isSubjectTab = activeTab === "subject"
const isCleanTab = activeTab === "clean"
const handleDescribe = async () => {
setDescribing(true)
@@ -421,12 +423,16 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
</div>
</div>
{/* 主体 — 左:大图 + 清洗状态;右:主体识别 + 主体资产 */}
{/* 主体 — 左:大图 + 主操作;右:当前页上下文 / 主体资产 */}
<div className="flex gap-3 p-3 overflow-hidden flex-1 min-h-0">
{/* 左侧大图区 */}
<div
className="flex flex-col items-stretch gap-2 overflow-y-auto pr-1"
style={{ flex: "1 1 320px", minWidth: 200, maxWidth: 420, minHeight: 0 }}
style={isSubjectTab
? { flex: "1 1 360px", minWidth: 220, maxWidth: 460, minHeight: 0 }
: isCleanTab
? { flex: "1 1 500px", minWidth: 300, maxWidth: 600, minHeight: 0 }
: { flex: "1 1 560px", minWidth: 300, maxWidth: 680, minHeight: 0 }}
>
{/* 上方:主图 + 画框 overlay */}
<div
@@ -441,7 +447,13 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
src={mainSrc}
alt={`frame ${f.index}`}
className="rounded-lg object-contain w-full pointer-events-none"
style={{ maxHeight: hasCleaned ? "38vh" : "62vh" }}
style={{
maxHeight: isSubjectTab
? (hasCleaned ? "38vh" : "62vh")
: isCleanTab
? "68vh"
: (hasCleaned ? "44vh" : "68vh"),
}}
draggable={false}
/>
<div className="absolute top-2 left-2 text-[9.5px] px-1.5 py-0.5 rounded backdrop-blur bg-black/50 text-white/80 pointer-events-none">
@@ -482,144 +494,6 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
)}
</div>
{activeTab === "clean" && (
<>
<div className="rounded-lg border border-cyan-300/20 bg-cyan-500/[0.08] p-2">
<div className="mb-2 flex items-center justify-between gap-2">
<div>
<div className="text-[11.5px] font-semibold text-white"></div>
<div className="mt-0.5 text-[9.5px] text-white/42">
</div>
</div>
<span className="shrink-0 rounded bg-black/35 px-1.5 py-0.5 text-[9.5px] font-mono text-white/55">
{cleanedFrameCount}/{frames.length}
</span>
</div>
{batchCleanupProgress && (
<div className="mb-2">
<div className="mb-1 flex items-center justify-between text-[9.5px] text-white/45">
<span>{batchCleaning ? "清洗中" : "最近批量清洗"}</span>
<span>{batchCleanupProgress.done}/{batchCleanupProgress.total}{batchCleanupProgress.failed ? ` · 失败 ${batchCleanupProgress.failed}` : ""}</span>
</div>
<div className="h-1.5 overflow-hidden rounded-full bg-white/10">
<div
className="h-full rounded-full bg-cyan-300 transition-all"
style={{ width: `${Math.round((batchCleanupProgress.done / Math.max(1, batchCleanupProgress.total)) * 100)}%` }}
/>
</div>
</div>
)}
<button
type="button"
onClick={handleCleanupAllFrames}
disabled={batchCleaning || cropMode || frames.length === 0}
className="w-full rounded-md bg-cyan-500/75 px-2 py-1.5 text-[11px] font-medium text-white transition hover:bg-cyan-400 disabled:cursor-wait disabled:opacity-45 inline-flex items-center justify-center gap-1.5"
title="自动清洗所有未处理关键帧;不满意的帧再手工框选清洗"
>
{batchCleaning ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Sparkles className="h-3.5 w-3.5" />}
{batchCleaning
? "批量清洗中…"
: pendingCleanFrames.length > 0
? `一键清洗未处理 ${pendingCleanFrames.length}`
: `重新清洗全部 ${frames.length}`}
</button>
</div>
{/* 画框工具栏 */}
{cropMode ? (
<div className="space-y-1.5">
<div className="text-[10px] text-white/55 leading-snug">
{regions.length === 0
? "在图上拖动鼠标 → 框选要清洗的水印、字幕、平台 UI 或杂物"
: `已框 ${regions.length} 个 · 继续加框或点击「去掉」批量清洗`}
</div>
<div className="flex items-center gap-1">
<button
onClick={() => handleCleanup(true)}
disabled={isCleaningCurrentFrame || batchCleaning || regions.length === 0}
className="flex-1 px-1.5 py-1.5 rounded-md text-[10.5px] font-medium inline-flex items-center justify-center gap-1 transition bg-cyan-500 hover:bg-cyan-400 text-white disabled:opacity-40 disabled:cursor-not-allowed"
title="批量清洗所有框内"
>
{isCleaningCurrentFrame ? <Loader2 className="h-3 w-3 animate-spin" /> : <Sparkle className="h-3 w-3" />}
{isCleaningCurrentFrame ? "去掉中" : `去掉${regions.length > 1 ? ` ${regions.length}` : ""}`}
</button>
<button
onClick={() => setRegions((prev) => prev.slice(0, -1))}
disabled={regions.length === 0}
className="px-1.5 py-1.5 rounded-md text-[10.5px] bg-white/10 hover:bg-white/20 text-white disabled:opacity-30 disabled:cursor-not-allowed"
title="撤销上一个框"
>
</button>
<button
onClick={() => { setCropMode(false); setRegions([]); setDraftRegion(null); setDragStart(null) }}
className="px-1.5 py-1.5 rounded-md text-[10.5px] bg-white/10 hover:bg-white/20 text-white"
title="退出画框"
>
<X className="h-3 w-3" />
</button>
</div>
</div>
) : (
<button
onClick={() => { setCropMode(true); setRegions([]) }}
className="w-full px-3 py-1.5 rounded-md text-[10.5px] font-medium inline-flex items-center justify-center gap-1.5 transition bg-white/[0.06] hover:bg-cyan-500/30 border border-white/15 hover:border-cyan-300/50 text-white/80 hover:text-white"
title="可连续画多个框 · 批量清洗局部水印或杂物"
>
<Crop className="h-3 w-3" />
</button>
)}
{/* 下方:清洗版(有待应用版本时显示) */}
{hasCleaned && cleanedSrc && (
<div className="relative rounded-lg border border-emerald-400/40 bg-emerald-500/5 p-2 space-y-1.5">
<div className="flex items-center justify-between pr-5">
<div className="text-[10px] text-emerald-300 inline-flex items-center gap-1 font-medium">
<Sparkle className="h-2.5 w-2.5" />
</div>
<span className="text-[9px] text-white/40"></span>
</div>
<button
onClick={handleDiscardCleaned}
title="丢弃这次清洗结果"
className="absolute top-1.5 right-1.5 h-5 w-5 rounded-full bg-black/40 hover:bg-rose-500/80 text-white/70 hover:text-white inline-flex items-center justify-center transition"
>
<X className="h-2.5 w-2.5" />
</button>
<img
src={cleanedSrc}
alt={`cleaned ${f.index}`}
className="rounded-md object-contain w-full"
style={{ maxHeight: "32vh" }}
/>
<button
onClick={handleApplyCleaned}
disabled={applying}
className="w-full px-2 py-1.5 rounded-md text-[11px] font-medium inline-flex items-center justify-center gap-1 transition bg-emerald-500 hover:bg-emerald-400 text-white disabled:opacity-40 disabled:cursor-not-allowed"
title="替换原图为这张干净版 · 后续场景图、主体资产和分镜编排都基于干净版"
>
{applying ? <Loader2 className="h-3 w-3 animate-spin" /> : <Check className="h-3 w-3" />}
{applying ? "替换中…" : "替换原图"}
</button>
</div>
)}
{/* 清洗按钮(全图) */}
<button
onClick={() => handleCleanup(false)}
disabled={isCleaningCurrentFrame || batchCleaning || cropMode}
className="w-full px-3 py-1.5 rounded-md text-[11.5px] font-medium inline-flex items-center justify-center gap-1.5 transition bg-gradient-to-r from-cyan-500/80 to-emerald-500/80 hover:from-cyan-500 hover:to-emerald-500 text-white disabled:opacity-40 disabled:cursor-not-allowed"
title="清掉水印 / @用户名 / 字幕 / 平台 logo"
>
{isCleaningCurrentFrame ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Sparkle className="h-3.5 w-3.5" />}
{isCleaningCurrentFrame ? "清洗中…5-15 秒)" : hasCleaned ? "重新清洗" : f.cleaned_applied ? "再次清洗" : "清洗水印"}
</button>
</>
)}
{activeTab === "scene" && (
<div className="rounded-lg border border-white/10 bg-white/[0.035] p-2">
<div className="mb-2 flex items-center justify-between gap-2">
@@ -702,6 +576,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
</div>
)}
{activeTab !== "clean" && (
<div className="rounded-md border border-emerald-300/20 bg-emerald-500/10 px-3 py-2 text-[11px] leading-relaxed text-emerald-50/80">
<div className="mb-0.5 inline-flex items-center gap-1 font-medium text-emerald-100">
<Check className="h-3 w-3" />
@@ -711,25 +586,47 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
</div>
</div>
)}
</div>
{/* 右侧主体识别 + 主体资产 */}
<div className="flex flex-col gap-2.5 overflow-y-auto flex-1 min-h-0" style={{ minWidth: 240 }}>
<div
className="flex flex-col gap-2.5 overflow-y-auto min-h-0"
style={isSubjectTab
? { flex: "1 1 300px", minWidth: 260 }
: { flex: "0 0 224px", width: 224, minWidth: 208, maxWidth: 240 }}
>
{activeTab === "clean" && (
<section className="rounded-lg border border-cyan-300/15 bg-cyan-500/[0.08] p-3 text-[11px] leading-relaxed text-white/58">
<div className="mb-1.5 text-[12.5px] font-semibold text-white"></div>
<section className="rounded-lg border border-cyan-300/15 bg-cyan-500/[0.08] p-2.5 text-[10.5px] leading-relaxed text-white/58">
<div className="mb-2 text-[12px] font-semibold text-white"></div>
<div className="mb-2 grid grid-cols-2 gap-1.5">
<div className="rounded border border-white/10 bg-black/25 px-2 py-1">
<div className="text-[9px] text-white/35"></div>
<div className="text-[10.5px] text-white/80">{f.cleaned_applied ? "已应用" : hasCleaned ? "待审核" : "未清洗"}</div>
</div>
<div className="rounded border border-white/10 bg-black/25 px-2 py-1">
<div className="text-[9px] text-white/35"></div>
<div className="text-[10.5px] text-white/80">{cleanedFrameCount}/{frames.length}</div>
</div>
</div>
</section>
)}
{activeTab === "scene" && (
<section className="rounded-lg border border-emerald-300/15 bg-emerald-500/[0.08] p-3 text-[11px] leading-relaxed text-white/58">
<div className="mb-1.5 text-[12.5px] font-semibold text-white"></div>
线
<section className="rounded-lg border border-emerald-300/15 bg-emerald-500/[0.08] p-2.5 text-[10.5px] leading-relaxed text-white/58">
<div className="mb-2 text-[12px] font-semibold text-white"></div>
<div className="mb-2 rounded border border-white/10 bg-black/25 px-2 py-1">
<div className="text-[9px] text-white/35"></div>
<div className="text-[10.5px] text-white/80">
{latestSceneAsset ? `${latestSceneAsset.width}×${latestSceneAsset.height}` : "未生成"}
</div>
</div>
线
{f.scene_assets?.length ? (
<div className="mt-2 grid grid-cols-2 gap-2">
<div className="mt-2 grid grid-cols-2 gap-1.5">
{f.scene_assets.slice(-4).map((asset) => (
<div key={asset.id} className="overflow-hidden rounded-md border border-white/10 bg-black/35">
<img src={apiAssetUrl(asset.url)} alt={asset.label} className="h-24 w-full object-contain" />
<img src={apiAssetUrl(asset.url)} alt={asset.label} className="h-20 w-full object-contain" />
<div className="px-1.5 py-1 text-[9.5px] font-mono text-white/45">{asset.width}×{asset.height}</div>
</div>
))}
@@ -738,13 +635,25 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
</section>
)}
{activeTab === "review" && (
<section className="rounded-lg border border-white/10 bg-white/[0.035] p-3 text-[11px] leading-relaxed text-white/58">
<div className="mb-2 text-[12.5px] font-semibold text-white"></div>
<div className="space-y-1.5">
<div>{f.cleaned_applied ? "已应用" : hasCleaned ? "待确认" : "未处理"}</div>
<div>{latestSceneAsset ? `${latestSceneAsset.width}×${latestSceneAsset.height}` : "未生成"}</div>
<div>{elements.length} </div>
<div>{subjectAssetCount} </div>
<section className="rounded-lg border border-white/10 bg-white/[0.035] p-2.5 text-[10.5px] leading-relaxed text-white/58">
<div className="mb-2 text-[12px] font-semibold text-white"></div>
<div className="grid grid-cols-2 gap-1.5">
<div className="rounded border border-white/10 bg-black/25 px-2 py-1">
<div className="text-[9px] text-white/35"></div>
<div className="text-white/80">{f.cleaned_applied ? "已应用" : hasCleaned ? "待确认" : "未处理"}</div>
</div>
<div className="rounded border border-white/10 bg-black/25 px-2 py-1">
<div className="text-[9px] text-white/35"></div>
<div className="text-white/80">{latestSceneAsset ? "已生成" : "未生成"}</div>
</div>
<div className="rounded border border-white/10 bg-black/25 px-2 py-1">
<div className="text-[9px] text-white/35"></div>
<div className="text-white/80">{elements.length} </div>
</div>
<div className="rounded border border-white/10 bg-black/25 px-2 py-1">
<div className="text-[9px] text-white/35"></div>
<div className="text-white/80">{subjectAssetCount} </div>
</div>
</div>
</section>
)}