auto-save 2026-05-14 06:16 (~5)

This commit is contained in:
2026-05-14 06:17:00 +08:00
parent 871ced6d2d
commit f3f4c56535
5 changed files with 108 additions and 68 deletions

View File

@@ -1,19 +1,5 @@
{
"entries": [
{
"files_changed": 3,
"hash": "4138bea",
"message": "auto-save 2026-05-12 17:00 (~3)",
"ts": "2026-05-12T17:01:09+08:00",
"type": "commit"
},
{
"files_changed": 4,
"hash": "94afd6d",
"message": "auto-save 2026-05-12 17:06 (+1, ~3)",
"ts": "2026-05-12T17:06:43+08:00",
"type": "commit"
},
{
"files_changed": 3,
"hash": "e1bc89a",
@@ -3353,6 +3339,19 @@
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 4 项未提交变更 · 最近提交auto-save 2026-05-14 06:05 (~1)",
"files_changed": 4
},
{
"ts": "2026-05-14T06:11:29+08:00",
"type": "commit",
"message": "auto-save 2026-05-14 06:11 (~6)",
"hash": "871ced6",
"files_changed": 6
},
{
"ts": "2026-05-13T22:13:14Z",
"type": "session-heartbeat",
"message": "Claude 会话活跃 · 最近命令claude · 2 项未提交变更 · 最近提交auto-save 2026-05-14 06:11 (~6)",
"files_changed": 2
}
]
}

View File

@@ -2077,15 +2077,18 @@ def generate_scene_asset(job_id: str, idx: int, req: GenerateSceneAssetReq) -> J
confirmed_subjects = [
(e.name_en or e.name_zh).strip()
for e in (frame.elements or [])
for ref_frame in job.frames
for e in (ref_frame.elements or [])
if (e.subject_assets or [])
]
if not confirmed_subjects:
confirmed_subjects = [
(e.name_en or e.name_zh).strip()
for e in (frame.elements or [])
for ref_frame in job.frames
for e in (ref_frame.elements or [])
if (e.name_en or e.name_zh).strip()
][:3]
confirmed_subjects = list(dict.fromkeys([x for x in confirmed_subjects if x]))[:3]
subject_clause = (
"Confirmed foreground subject(s) to remove: " + ", ".join(confirmed_subjects) + ". "
if confirmed_subjects

View File

@@ -682,6 +682,8 @@ api/main.py
<pre>SceneAsset {
id, label, url,
width, height, quality, size,
scene_mode: remove_subject | similar | style,
scene_style,
quality_report
}
@@ -859,7 +861,7 @@ SubjectAsset {
<span class="tag blue">Layout</span>
</header>
<div class="body">
<p><strong>问题:</strong>“原图/清洗、场景图、主体资产、审核”都应遵循同一结构:左侧负责看图和框选,右侧负责操作、状态和结果;旧布局把部分操作塞在左侧下方,导致左侧满、右侧空。</p>
<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>
@@ -896,7 +898,7 @@ SubjectAsset {
</header>
<div class="body">
<p><strong>问题:</strong>面板标题仍叫“关键帧详情 · 元素提取”,里面还露出普通 cutout 抠图、AI 提取、元素清单等旧流程入口;“选用此帧 / 加入目标帧”也无法说明实际价值,因为抽帧阶段已经筛过图,进入画面工作台的关键帧默认就应该参与素材准备。</p>
<p><strong>改动:</strong><code>KeyframePanelNode</code> 标题改为“关键帧素材准备”;<code>FrameLightbox</code> 的主体页改成“主体识别 / 主体清单 / 主体资产包”移除普通抠图列表、AI 提取按钮、详情内的目标帧开关以及手动/框选加主体入口,改为只显示“已在素材准备流程”的状态说明;<code>VisualLabNode</code> 上方缩略图也不再展示普通抠图分组,只展示关键帧、场景图、主体包和视频任务。</p>
<p><strong>改动:</strong><code>KeyframePanelNode</code> 标题改为“关键帧素材准备”;<code>FrameLightbox</code> 的主体页改成“主体识别 / 主体清单 / 主体资产包”移除普通抠图列表、AI 提取按钮、详情内的目标帧开关以及手动/框选加主体入口,改为只显示“已在素材准备流程”的状态说明;<code>VisualLabNode</code> 上方缩略图也不再展示普通抠图分组,只展示关键帧、主体包、场景图和视频任务。</p>
<p><strong>影响:</strong><code>web/components/lightbox.tsx</code><code>web/components/nodes/index.tsx</code><code>docs/source-analysis.html</code>。底层旧 cutout 数据和接口暂保留兼容历史任务,但不再作为新素材准备流程的可见入口。</p>
</div>
</article>
@@ -907,20 +909,20 @@ SubjectAsset {
<span class="tag blue">UX</span>
</header>
<div class="body">
<p><strong>问题:</strong>画面工作台从展示缩略图扩展为素材生产中枢后,关键帧、场景图、主体资产包和视频任务继续混在一个列表里会让流程不清晰;关键帧详情面板也把清洗、识别、场景和主体生成都堆在一屏。</p>
<p><strong>改动:</strong><code>VisualLabNode</code> 改成素材准备进度看板,显示关键帧素材、场景图、主体资产和分镜/视频四个入口。<code>FrameLightbox</code> 新增“原图/清洗、场景图、主体资产、审核”四个页签,素材审核信息从普通列表中拆出来。</p>
<p><strong>问题:</strong>画面工作台从展示缩略图扩展为素材生产中枢后,关键帧、主体资产包、场景图和视频任务继续混在一个列表里会让流程不清晰;关键帧详情面板也把清洗、识别、主体生成和场景生成都堆在一屏。</p>
<p><strong>改动:</strong><code>VisualLabNode</code> 改成素材准备进度看板,显示关键帧素材、主体资产、场景图和分镜/视频四个入口。<code>FrameLightbox</code> 新增“原图/清洗、主体资产、场景图、审核”四个页签,素材审核信息从普通列表中拆出来。</p>
<p><strong>影响:</strong><code>web/components/nodes/index.tsx</code><code>web/components/lightbox.tsx</code><code>docs/source-analysis.html</code>。这轮只重排工作台信息架构,批量自动准备队列仍留到下一阶段。</p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-14 · 画面工作台增加场景图和主体资产包</h3>
<h3>2026-05-14 · 画面工作台增加主体资产包和场景图</h3>
<span class="tag violet">Visual Lab</span>
<span class="tag blue">Assets</span>
</header>
<div class="body">
<p><strong>问题:</strong>抽帧阶段已经筛过图,画面工作台第一步应把已选关键帧转成可用于生视频的干净素材:每帧一张场景图,同一主体一套多视角/动作/表情图,而不是继续手动逐张抠普通 cutout。</p>
<p><strong>改动:</strong><code>KeyFrame</code> 新增 <code>scene_assets</code><code>quality_report</code><code>KeyElement</code> 新增 <code>subject_kind</code><code>subject_assets</code>。后端新增 <code>generateSceneAsset</code><code>generateSubjectAssets</code>,主体资产支持白/黑背景、原尺寸/固定尺寸、物体视角、人物/生物动作与喜怒哀乐等表情;当已选关键帧共同指向一个主体时,前端会把这些帧作为 <code>source_frame_indices</code> 传入,后端拼接参考板。</p>
<p><strong>问题:</strong>抽帧阶段已经筛过图,画面工作台第一步应把已选关键帧转成可用于生视频的干净素材:同一主体一套多视角/动作/表情图,再基于主体生成每帧去主体场景图,而不是继续手动逐张抠普通 cutout。</p>
<p><strong>改动:</strong><code>KeyFrame</code> 新增 <code>scene_assets</code><code>quality_report</code><code>KeyElement</code> 新增 <code>subject_kind</code><code>subject_assets</code>。后端新增 <code>generateSubjectAssets</code><code>generateSceneAsset</code>,主体资产支持白/黑背景、原尺寸/固定尺寸、物体视角、人物/生物动作与喜怒哀乐等表情;当已选关键帧共同指向一个主体时,前端会把这些帧作为 <code>source_frame_indices</code> 传入,后端拼接参考板。</p>
<p><strong>影响:</strong><code>api/main.py</code><code>web/lib/api.ts</code><code>web/components/lightbox.tsx</code><code>web/components/nodes/index.tsx</code><code>docs/source-analysis.html</code>。生成出的素材保存在 <code>jobs/&lt;jobId&gt;/assets</code>,可作为 <code>asset</code> 类型复制到后续分镜槽位。</p>
</div>
</article>

View File

@@ -86,6 +86,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
const [subjectViews, setSubjectViews] = useState<Record<string, string[]>>({})
const [activeTab, setActiveTab] = useState<LightboxTab>("clean")
const [editingElement, setEditingElement] = useState<{
frameIndex: number
id: string
name_zh: string
name_en: string
@@ -138,8 +139,28 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
const cleanedFrameCount = frames.filter((frame) => frame.cleaned_applied || frame.cleaned_url).length
const pendingCleanFrames = frames.filter((frame) => !frame.cleaned_applied && !frame.cleaned_url)
const selectedFrameIndices = Array.from(selected).sort((a, b) => a - b)
const sharedSubjectFrameIndices = selectedFrameIndices.length > 1 ? selectedFrameIndices : [f.index]
const subjectAssetCount = elements.reduce((sum, item) => sum + (item.subject_assets?.length ?? 0), 0)
const subjectReferenceFrameIndices = (selectedFrameIndices.length > 0 ? selectedFrameIndices : frames.map((frame) => frame.index))
.filter((idx, pos, arr) => arr.indexOf(idx) === pos)
const subjectReferenceLabel = selectedFrameIndices.length > 0
? `${subjectReferenceFrameIndices.length} 已选帧参考`
: `${subjectReferenceFrameIndices.length} 全部帧参考`
const subjectElementRefs = frames.flatMap((frame) =>
(frame.elements ?? []).map((element) => ({
frameIndex: frame.index,
frameLabel: `分镜 ${frame.index + 1}`,
element,
})),
)
const activeSubjectRefs = elements.map((element) => ({
frameIndex: f.index,
frameLabel: `分镜 ${f.index + 1}`,
element,
}))
const subjectDisplayRefs = activeSubjectRefs.length > 0
? activeSubjectRefs
: subjectElementRefs.slice(0, 1)
const hasUnifiedSubject = subjectElementRefs.length > 0
const subjectAssetCount = subjectElementRefs.reduce((sum, item) => sum + (item.element.subject_assets?.length ?? 0), 0)
const hasSubjectAssets = subjectAssetCount > 0
const qualityWarnings = [
...(f.quality_report?.warnings ?? []),
@@ -231,21 +252,21 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
}
}
const handleGenerateSubjectPackage = async (elementId: string) => {
const handleGenerateSubjectPackage = async (elementId: string, frameIdx = f.index) => {
const kind = subjectKinds[elementId] ?? "object"
const defaultViews = (kind === "living" ? LIVING_VIEW_OPTIONS : OBJECT_VIEW_OPTIONS).map(([value]) => value)
const views = subjectViews[elementId]?.length ? subjectViews[elementId] : defaultViews
setSubjectGenerating(elementId)
try {
const updated = await generateSubjectAssets(jobId, f.index, elementId, {
const updated = await generateSubjectAssets(jobId, frameIdx, elementId, {
subject_kind: kind,
background: subjectBackgrounds[elementId] ?? "white",
size: assetSize,
source_frame_indices: sharedSubjectFrameIndices,
source_frame_indices: subjectReferenceFrameIndices,
views,
})
onJobUpdate?.(updated)
toast.success(`主体资产包已生成 · ${views.length}`)
toast.success(`统一主体资产包已生成 · ${views.length} · ${subjectReferenceFrameIndices.length} 帧参考`)
} catch (e) {
toast.error("主体资产包生成失败:" + (e instanceof Error ? e.message : String(e)))
} finally {
@@ -325,6 +346,10 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
const handleAddElement = async (name_zh: string, name_en?: string, position?: string, source: "auto" | "manual" = "manual") => {
const zh = name_zh.trim()
if (!zh) return
if (hasUnifiedSubject) {
toast.message("当前流程只保留一个主体;如需更换,请先删除现有统一主体")
return
}
try {
const updated = await addElement(jobId, f.index, { name_zh: zh, name_en, position, source })
onJobUpdate?.(updated)
@@ -333,9 +358,9 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
}
}
const handleDeleteElement = async (id: string) => {
const handleDeleteElement = async (id: string, frameIdx = f.index) => {
try {
const updated = await deleteElement(jobId, f.index, id)
const updated = await deleteElement(jobId, frameIdx, id)
onJobUpdate?.(updated)
if (editingElement?.id === id) setEditingElement(null)
} catch (e) {
@@ -346,7 +371,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
const handleUpdateElement = async () => {
if (!editingElement || !editingElement.name_zh.trim()) return
try {
const updated = await updateElement(jobId, f.index, editingElement.id, {
const updated = await updateElement(jobId, editingElement.frameIndex, editingElement.id, {
name_zh: editingElement.name_zh,
name_en: editingElement.name_en,
position: editingElement.position,
@@ -447,7 +472,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
</button>
))}
<div className="ml-auto hidden items-center gap-2 text-[10px] text-white/42 sm:flex">
<span>{hasSubjectAssets ? `${subjectAssetCount} 主体资产` : "主体待生成"}</span>
<span>{hasSubjectAssets ? `统一主体 ${subjectAssetCount} ` : hasUnifiedSubject ? "统一主体待生成" : "统一主体待选择"}</span>
<span>·</span>
<span>{latestSceneAsset ? "场景已生成" : "场景待生成"}</span>
</div>
@@ -677,7 +702,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
</select>
</div>
<div className="mb-2 rounded-md border border-white/10 bg-black/25 px-2 py-1.5 text-[10px] leading-relaxed text-white/50">
</div>
<div className="mb-2 grid grid-cols-2 gap-1.5">
<label className="space-y-1">
@@ -773,10 +798,10 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
</div>
<div className={`rounded border px-2 py-1 ${elements.length > 0 ? "border-violet-300/35 bg-violet-500/12 text-violet-100" : "border-white/10 bg-black/25 text-white/55"}`}>
<div className="text-[9px] opacity-70"></div>
<div>{elements.length} </div>
<div>{hasUnifiedSubject ? "1 个" : "未选"}</div>
</div>
<div className={`rounded border px-2 py-1 ${subjectAssetCount > 0 ? "border-violet-300/35 bg-violet-500/12 text-violet-100" : "border-white/10 bg-black/25 text-white/55"}`}>
<div className="text-[9px] opacity-70"></div>
<div className="text-[9px] opacity-70"></div>
<div>{subjectAssetCount} </div>
</div>
<div className={`rounded border px-2 py-1 ${latestSceneAsset ? "border-emerald-300/30 bg-emerald-500/10 text-emerald-100" : "border-white/10 bg-black/25 text-white/55"}`}>
@@ -805,7 +830,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-1.5 text-white text-[12.5px] font-semibold">
<Eye className="h-3.5 w-3.5" />
{desc && <span className="text-[10px] text-emerald-400 font-mono ml-1"></span>}
</div>
<button
@@ -827,7 +852,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
</div>
) : (
<> Vision </>
<></>
)}
</div>
) : (
@@ -847,20 +872,23 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
{desc.objects && desc.objects.length > 0 && (
<div className="rounded-md bg-pink-500/10 border border-pink-400/25 px-2.5 py-1.5">
<div className="text-[9.5px] uppercase tracking-widest text-pink-300/90 mb-1.5">
·
·
</div>
<div className="space-y-1">
{desc.objects.map((o, i) => {
const alreadyIn = elements.some((e) => e.name_zh === o.name)
const locked = hasUnifiedSubject && !alreadyIn
return (
<button
key={i}
onClick={() => !alreadyIn && handleAddElement(o.name, o.extract_prompt, o.position, "auto")}
disabled={alreadyIn}
title={alreadyIn ? "已加入主体清单" : (o.position ? `位置:${o.position}` : undefined)}
onClick={() => !alreadyIn && !locked && handleAddElement(o.name, o.extract_prompt, o.position, "auto")}
disabled={alreadyIn || locked}
title={alreadyIn ? "已选择为统一主体" : locked ? "当前流程只保留一个主体;删除现有主体后可更换" : (o.position ? `位置:${o.position}` : undefined)}
className={`w-full text-left rounded border px-2 py-1 transition group/o ${
alreadyIn
? "bg-emerald-500/10 border-emerald-400/30 cursor-default"
: locked
? "bg-white/[0.02] border-white/8 opacity-45 cursor-not-allowed"
: "bg-white/[0.03] hover:bg-white/[0.08] border-white/8 hover:border-pink-300/40"
}`}
>
@@ -889,21 +917,27 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
<section className={activeTab === "subject" ? "" : "hidden"}>
<div className="flex items-center gap-1.5 mb-2 text-white text-[12.5px] font-semibold">
<Sparkles className="h-3.5 w-3.5" />
{elements.length > 0 && (
<span className="text-[10px] text-white/35 font-mono ml-0.5">· {elements.length}</span>
{hasUnifiedSubject && (
<span className="text-[10px] text-white/35 font-mono ml-0.5">· </span>
)}
<span className="text-[9.5px] text-white/35 font-normal ml-auto"> </span>
<span className="text-[9.5px] text-white/35 font-normal ml-auto"> {subjectReferenceLabel}</span>
</div>
<div className="mb-2 rounded-md border border-white/10 bg-white/[0.035] px-2.5 py-1.5 text-[10.5px] leading-relaxed text-white/45">
Vision //
</div>
{elements.length > 0 && (
{!hasUnifiedSubject && (
<div className="mb-2 rounded-lg border border-dashed border-white/15 bg-white/[0.03] p-3 text-[11px] leading-relaxed text-white/48">
使
</div>
)}
{subjectDisplayRefs.length > 0 && (
<div className="space-y-2 mb-2">
{elements.map((e) => {
{subjectDisplayRefs.map(({ frameIndex, frameLabel, element: e }) => {
const hasRegion = !!e.region
const isEditing = editingElement?.id === e.id
const isEditing = editingElement?.id === e.id && editingElement.frameIndex === frameIndex
const currentKind = subjectKinds[e.id] ?? e.subject_kind ?? "object"
const currentBg = subjectBackgrounds[e.id] ?? e.cutout_background ?? "white"
const viewOptions = currentKind === "living" ? LIVING_VIEW_OPTIONS : OBJECT_VIEW_OPTIONS
@@ -946,6 +980,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
<div className="min-w-0">
<div className="flex items-center gap-1 text-white text-[12px] font-medium leading-tight">
<span className="truncate">{e.name_zh}</span>
<span className="text-[8.5px] text-white/35 font-mono shrink-0">· {frameLabel}</span>
{e.source === "auto" && (
<span className="text-[8.5px] text-pink-300/70 font-mono"></span>
)}
@@ -992,6 +1027,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
<>
<button
onClick={() => setEditingElement({
frameIndex,
id: e.id,
name_zh: e.name_zh,
name_en: e.name_en || "",
@@ -1003,9 +1039,9 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
</button>
<button
onClick={() => handleDeleteElement(e.id)}
onClick={() => handleDeleteElement(e.id, frameIndex)}
className="ml-auto rounded px-2 py-1 text-[10.5px] inline-flex items-center gap-1 bg-rose-500/15 hover:bg-rose-500/30 text-rose-100 border border-rose-300/20"
title="删除这个主体候选和它的主体资产"
title="删除这个统一主体候选和它的主体资产"
>
<Trash2 className="h-3 w-3" />
@@ -1015,12 +1051,12 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
</div>
</div>
<div className="mt-2 rounded-md border border-violet-300/15 bg-violet-500/[0.08] p-2">
<div className="mb-2 flex items-center justify-between gap-2">
<div className="text-[11px] font-semibold text-white/90"></div>
<span className="text-[9.5px] font-mono text-white/35">
{sharedSubjectFrameIndices.length > 1 ? `${sharedSubjectFrameIndices.length} 帧参考` : "当前帧参考"}
</span>
<div className="mt-2 rounded-md border border-violet-300/15 bg-violet-500/[0.08] p-2">
<div className="mb-2 flex items-center justify-between gap-2">
<div className="text-[11px] font-semibold text-white/90"></div>
<span className="text-[9.5px] font-mono text-white/35">
{subjectReferenceLabel}
</span>
</div>
<div className="mb-2 grid grid-cols-3 gap-1">
<select
@@ -1043,12 +1079,12 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
<option value="white"></option>
<option value="black"></option>
</select>
<button
type="button"
onClick={() => handleGenerateSubjectPackage(e.id)}
disabled={isSubjectGenerating || activeViews.length === 0}
className="rounded bg-violet-500/70 px-1.5 py-1 text-[10px] font-medium text-white transition hover:bg-violet-400 disabled:cursor-wait disabled:opacity-45 inline-flex items-center justify-center gap-1"
title="生成多视角 / 动作 / 表情主体资产"
<button
type="button"
onClick={() => handleGenerateSubjectPackage(e.id, frameIndex)}
disabled={isSubjectGenerating || activeViews.length === 0}
className="rounded bg-violet-500/70 px-1.5 py-1 text-[10px] font-medium text-white transition hover:bg-violet-400 disabled:cursor-wait disabled:opacity-45 inline-flex items-center justify-center gap-1"
title="用多张关键帧参考生成同一个主体的多视角 / 动作 / 表情资产"
>
{isSubjectGenerating ? <Loader2 className="h-3 w-3 animate-spin" /> : <Wand2 className="h-3 w-3" />}
{isSubjectGenerating ? "生成" : "生成"}
@@ -1114,7 +1150,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
)}
<div className="mt-1.5 text-[10px] text-white/35 leading-relaxed">
使
</div>
</section>
</div>

View File

@@ -1561,13 +1561,13 @@ export function VisualLabNode({ data, selected }: any) {
onClick={(e) => { e.stopPropagation(); openFirstFrame() }}
disabled={!job || frames.length === 0}
className="min-h-14 rounded-md border border-white/10 px-2 py-2 text-left transition hover:border-violet-300/50 hover:bg-violet-400/10 disabled:opacity-35"
title="生成主体多视角 / 动作 / 表情资产包"
title="用多张关键帧生成统一主体多视角 / 动作 / 表情资产包"
>
<div className="mb-1 flex items-center gap-1 text-[var(--text-strong)] text-[12px] font-semibold">
<Package className="h-3 w-3 text-violet-300" />
{subjectAssetCount}
</div>
<div></div>
<div></div>
</button>
<button
type="button"
@@ -1599,7 +1599,7 @@ export function VisualLabNode({ data, selected }: any) {
<div className="mt-2 text-[10.5px] leading-snug text-[var(--text-faint)]">
{frames.length > 0 ? (
<>
{cleanedCount} · {subjectAssetCount} · {sceneAssetCount} · {targetFrameCount} · {completedVideos.length}
{cleanedCount} · {subjectAssetCount} · {sceneAssetCount} · {targetFrameCount} · {completedVideos.length}
</>
) : (
"解析后这里变成素材准备看板:先审关键帧,再生成主体资产包和去主体场景图。"