auto-save 2026-05-14 05:32 (~4)
This commit is contained in:
@@ -3348,6 +3348,19 @@
|
||||
"type": "session-heartbeat",
|
||||
"message": "Claude 会话活跃 · 最近命令:claude · 1 项未提交变更 · 最近提交:auto-save 2026-05-14 05:21 (~3)",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-14T05:27:24+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-14 05:27 (~3)",
|
||||
"hash": "2c19b52",
|
||||
"files_changed": 3
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-13T21:28:50Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 2 项未提交变更 · 最近提交:auto-save 2026-05-14 05:27 (~3)",
|
||||
"files_changed": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -554,9 +554,9 @@
|
||||
<div class="step"><div class="num">1</div><h3>输入</h3><p>TK 链接或本地上传,后端下载/保存源视频。</p></div>
|
||||
<div class="step"><div class="num">2</div><h3>镜头拆解</h3><p>拆轨、抽关键帧、手动加帧,形成参考分镜池。</p></div>
|
||||
<div class="step"><div class="num">3</div><h3>清洗水印</h3><p>对关键帧做全图或区域清洗,必要时应用为当前参考图。</p></div>
|
||||
<div class="step"><div class="num">4</div><h3>Vision 识别</h3><p>识别场景和候选元素,只是候选,不应锁死。</p></div>
|
||||
<div class="step"><div class="num">5</div><h3>元素提取</h3><p>编辑/新增/删除元素,对元素反复生成提取图。</p></div>
|
||||
<div class="step"><div class="num">6</div><h3>元素改造</h3><p>把参考主体、场景、动作和 SKG 产品放入分镜结构。</p></div>
|
||||
<div class="step"><div class="num">4</div><h3>主体识别</h3><p>识别场景和主体候选,只是候选,不应锁死。</p></div>
|
||||
<div class="step"><div class="num">5</div><h3>素材准备</h3><p>清洗关键帧,生成场景图和主体多视角/动作/表情资产包。</p></div>
|
||||
<div class="step"><div class="num">6</div><h3>分镜改造</h3><p>把参考主体、场景、动作和 SKG 产品放入分镜结构。</p></div>
|
||||
<div class="step"><div class="num">7</div><h3>生成视频</h3><p>用分镜 4 图槽、改造目标和时长调用 Seedance / Kling / Veo 3 生视频 API,结果回写到画面工作台节点。</p></div>
|
||||
<div class="step"><div class="num">8</div><h3>合成成品</h3><p>片段、字幕、配音、转场合成最终 mp4。当前未实现。</p></div>
|
||||
</div>
|
||||
@@ -571,7 +571,7 @@
|
||||
<tbody>
|
||||
<tr><td><code>web/app/page.tsx</code></td><td>产品工作台主状态:jobs、activeJobId、selectedFrames、clipboard、ReactFlow 节点和边;负责打开/找回画布工作面板。</td></tr>
|
||||
<tr><td><code>web/components/nodes/index.tsx</code></td><td>DAG 节点定义:Input、VisualLab、Audio、Compose,以及画布工作面板 KeyframePanel / VideoFramePanel;旧 Keyframe/Storyboard/VideoGen 组件保留但不再挂主画布。</td></tr>
|
||||
<tr><td><code>web/components/lightbox.tsx</code></td><td>镜头拆解和元素提取的主工作面板:清洗、识别、元素编辑、区域提取、抠图。</td></tr>
|
||||
<tr><td><code>web/components/lightbox.tsx</code></td><td>关键帧素材准备面板:清洗、场景图、主体候选、主体资产包和审核。</td></tr>
|
||||
<tr><td><code>web/components/storyboard-bar.tsx</code></td><td>顶部分镜编排条:展示选入编排的关键帧,并作为唯一分镜导航。</td></tr>
|
||||
<tr><td><code>web/components/storyboard-workbench.tsx</code></td><td>顶部分镜编排条下方的明细区:4 图槽、改造目标、时长、自动保存。</td></tr>
|
||||
<tr><td><code>web/lib/api.ts</code></td><td>前端类型和 API client,是前后端数据契约镜像。</td></tr>
|
||||
@@ -618,12 +618,12 @@ api/main.py
|
||||
</div>
|
||||
<div class="flow-row">
|
||||
<div><strong>你看到的区域</strong><span>画面工作台 · Visual Lab</span></div>
|
||||
<div><strong>主要源码</strong><span><code>VisualLabNode</code> in <code>web/components/nodes/index.tsx</code>;它现在是素材准备看板,汇总关键帧、场景图、主体资产包、普通抠图和视频任务。</span></div>
|
||||
<div><strong>主要源码</strong><span><code>VisualLabNode</code> in <code>web/components/nodes/index.tsx</code>;它现在是素材准备看板,汇总关键帧、场景图、主体资产包和视频任务。</span></div>
|
||||
<div><strong>适合怎么描述</strong><span>“画面工作台的素材准备进度、分组缩略图、关键帧审核入口和后续分镜入口应该如何组织”。</span></div>
|
||||
</div>
|
||||
<div class="flow-row">
|
||||
<div><strong>你看到的区域</strong><span>关键帧素材审核面板</span></div>
|
||||
<div><strong>主要源码</strong><span><code>FrameLightbox</code>;按“原图/清洗、场景图、主体包、审核”四个页签组织;相关接口包括 <code>cleanupFrame</code>、<code>generateSceneAsset</code>、<code>generateSubjectAssets</code>、<code>cutoutElement</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">
|
||||
@@ -664,7 +664,7 @@ api/main.py
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>KeyElement</h3>
|
||||
<p>从关键帧里识别或手动添加的可借鉴元素。Vision 给的是候选,用户可编辑,并可多次生成提取图。</p>
|
||||
<p>从关键帧里识别或手动添加的主体候选。Vision 给的是候选,用户可编辑、删除,并可基于它生成主体资产包。</p>
|
||||
<pre>KeyElement {
|
||||
id,
|
||||
name_zh, name_en, position,
|
||||
@@ -751,7 +751,7 @@ SubjectAsset {
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="tag violet">画面工作台 Visual Lab</span></td>
|
||||
<td>作为素材准备看板:显示准备进度、质量风险、关键帧 / 场景图 / 主体包 / 分镜视频四个入口;上方缩略图按关键帧、场景图、主体包、普通抠图、视频任务分组。点击关键帧进入素材审核面板,点击资产图复制到分镜编排。</td>
|
||||
<td>作为素材准备看板:显示准备进度、质量风险、关键帧 / 场景图 / 主体包 / 分镜视频四个入口;上方缩略图按关键帧、场景图、主体包、视频任务分组。点击关键帧进入素材审核面板,点击资产图复制到分镜编排。</td>
|
||||
<td>不要在主卡片里堆复杂表单;主卡片只做状态总览和入口。</td>
|
||||
<td><code>VisualLabNode</code>、<code>FrameLightbox</code>、<code>generateSceneAsset</code>、<code>generateSubjectAssets</code>、视频任务接口</td>
|
||||
</tr>
|
||||
@@ -787,8 +787,8 @@ SubjectAsset {
|
||||
<li>视频下载或本地保存,ffmpeg 抽关键帧。</li>
|
||||
<li>手动按时间戳加关键帧。</li>
|
||||
<li>关键帧清洗水印,全图或区域清洗。</li>
|
||||
<li>Vision 识别关键帧,输出 scene、objects、style、suggested_prompt。</li>
|
||||
<li>元素增改删、区域元素添加、元素多次提取图。</li>
|
||||
<li>Vision 识别关键帧,输出 scene、objects、style、suggested_prompt,并作为主体候选来源。</li>
|
||||
<li>主体候选增改删、区域主体添加、主体资产包生成。</li>
|
||||
<li>分镜工作台 4 图槽和改造说明自动保存。</li>
|
||||
<li>nano-banana-pro image-to-image 生图。</li>
|
||||
</ul>
|
||||
@@ -813,8 +813,8 @@ SubjectAsset {
|
||||
<h2>需求描述模板</h2>
|
||||
<div class="todo">
|
||||
<div class="todo-item">
|
||||
<h3>改镜头拆解 / 元素提取</h3>
|
||||
<p>“我在关键帧 lightbox 里,Vision 识别后的元素列表应该怎么编辑/重提取/删除;点击元素不要跳转;提取图怎么预览和复制。”</p>
|
||||
<h3>改关键帧素材准备</h3>
|
||||
<p>“我在关键帧素材准备面板里,主体候选应该怎么编辑/删除;场景图和主体资产包怎么生成、审核、复制到分镜。”</p>
|
||||
</div>
|
||||
<div class="todo-item">
|
||||
<h3>改 Storyboard 节点</h3>
|
||||
@@ -839,6 +839,18 @@ 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">UX</span>
|
||||
</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>web/components/lightbox.tsx</code>、<code>web/components/nodes/index.tsx</code>、<code>docs/source-analysis.html</code>。底层旧 cutout 数据和接口暂保留兼容历史任务,但不再作为新素材准备流程的可见入口。</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-05-14 · 画面工作台改为素材准备看板</h3>
|
||||
@@ -847,7 +859,7 @@ SubjectAsset {
|
||||
</header>
|
||||
<div class="body">
|
||||
<p><strong>问题:</strong>画面工作台从展示缩略图扩展为素材生产中枢后,关键帧、场景图、主体资产包和视频任务继续混在一个列表里会让流程不清晰;关键帧详情面板也把清洗、识别、场景和主体生成都堆在一屏。</p>
|
||||
<p><strong>改动:</strong><code>VisualLabNode</code> 改成素材准备进度看板,显示目标关键帧、场景图、主体资产和分镜/视频四个入口,并在上方缩略图中按关键帧、场景图、主体包、普通抠图、视频任务分组。<code>FrameLightbox</code> 新增“原图/清洗、场景图、主体包、审核”四个页签,素材审核信息从普通元素列表中拆出来。</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>
|
||||
|
||||
@@ -107,21 +107,16 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
|
||||
const pos = frames.findIndex((x) => x.index === activeIndex)
|
||||
if (e.key === "ArrowLeft" && pos > 0) onChange(frames[pos - 1].index)
|
||||
if (e.key === "ArrowRight" && pos < frames.length - 1) onChange(frames[pos + 1].index)
|
||||
if (e.key === " " || e.key === "Enter") {
|
||||
e.preventDefault()
|
||||
onToggleSelect(activeIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
window.addEventListener("keydown", onKey)
|
||||
return () => window.removeEventListener("keydown", onKey)
|
||||
}, [activeIndex, frames.length, onClose, onChange, onToggleSelect])
|
||||
}, [activeIndex, frames.length, onClose, onChange])
|
||||
|
||||
const f = activeIndex !== null ? frames.find((x) => x.index === activeIndex) : undefined
|
||||
const arrayPos = f ? frames.findIndex((x) => x.index === f.index) : -1
|
||||
|
||||
if (activeIndex === null || !f || !mounted) return null
|
||||
const isSelected = selected.has(f.index)
|
||||
const desc = f.description
|
||||
const elements = f.elements ?? []
|
||||
const hasCleaned = !!f.cleaned_url
|
||||
@@ -412,7 +407,7 @@ 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
|
||||
@@ -696,18 +691,15 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => onToggleSelect(f.index)}
|
||||
className={`w-full px-3 py-1.5 rounded-md text-[12px] font-medium inline-flex items-center justify-center gap-1.5 transition ${
|
||||
isSelected
|
||||
? "bg-emerald-500 text-white hover:bg-emerald-400"
|
||||
: "bg-white/10 text-white hover:bg-white/20"
|
||||
}`}
|
||||
title="目标关键帧会参与素材准备进度、主体跨帧参考和后续分镜编排"
|
||||
>
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
{isSelected ? "目标帧 · 点击移出" : "加入目标帧"}
|
||||
</button>
|
||||
<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" />
|
||||
已在素材准备流程
|
||||
</div>
|
||||
<div className="text-emerald-50/55">
|
||||
抽帧留下的关键帧默认都会参与清洗、场景图和主体资产准备,不需要在这里单独选用。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧主体识别 + 主体资产 */}
|
||||
@@ -1092,7 +1084,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-1.5 text-[10px] text-white/40 font-mono text-center border-t border-white/5 bg-white/[0.02]">
|
||||
←/→ 切换 · Space 切换目标帧 · ESC 关闭
|
||||
←/→ 切换关键帧 · ESC 关闭
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1212,11 +1212,9 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
const aspect = job && (job.width ?? 0) > 0 && (job.height ?? 0) > 0
|
||||
? `${job.width}/${job.height}`
|
||||
: "9/16"
|
||||
const elementCrops = collectElementCrops(job)
|
||||
const sceneAssets = collectSceneAssets(job)
|
||||
const subjectAssets = collectSubjectAssets(job)
|
||||
const cleanedCount = frames.filter((x) => x.cleaned_url).length
|
||||
const cutoutCount = frames.reduce((s, x) => s + (x.elements?.filter((e) => hasCutout(e)).length ?? 0), 0)
|
||||
const sceneAssetCount = sceneAssets.length
|
||||
const subjectAssetCount = subjectAssets.length
|
||||
const selectedFrameCount = frames.filter((f) => d.selectedFrames.has(f.index)).length
|
||||
@@ -1232,7 +1230,7 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
? "running"
|
||||
: failedVideo
|
||||
? "failed"
|
||||
: frames.length > 0 || elementCrops.length > 0 || completedVideos.length > 0
|
||||
: frames.length > 0 || subjectAssets.length > 0 || completedVideos.length > 0
|
||||
? "done"
|
||||
: keyframeStatus(job)
|
||||
|
||||
@@ -1240,7 +1238,6 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
| { id: string; kind: "frame"; group: string; frameIdx: number; src: string; label: string; caption: string; borderClass: string; aspect: string }
|
||||
| { id: string; kind: "scene"; group: string; frameIdx: number; assetId: string; src: string; label: string; caption: string; borderClass: string; aspect: string }
|
||||
| { id: string; kind: "subject"; group: string; frameIdx: number; assetId: string; src: string; label: string; caption: string; borderClass: string; aspect: string }
|
||||
| { id: string; kind: "cutout"; group: string; frameIdx: number; elementId: string; cutoutId: string; src: string; label: string; caption: string; borderClass: string; aspect: string }
|
||||
| { id: string; kind: "video"; group: string; videoId: string; videoSrc?: string; posterSrc?: string; label: string; caption: string; borderClass: string; aspect: string }
|
||||
|
||||
const [hoverPreview, setHoverPreview] = useState<PreviewAnchor<string> | null>(null)
|
||||
@@ -1284,19 +1281,6 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
borderClass: "border-violet-300/65",
|
||||
aspect: p.width && p.height ? `${p.width}/${p.height}` : "1/1",
|
||||
})),
|
||||
...elementCrops.map((p) => ({
|
||||
id: `cutout:${p.frameIdx}:${p.elementId}:${p.cid}`,
|
||||
kind: "cutout" as const,
|
||||
group: "普通抠图",
|
||||
frameIdx: p.frameIdx,
|
||||
elementId: p.elementId,
|
||||
cutoutId: p.cid,
|
||||
src: p.src,
|
||||
label: p.name,
|
||||
caption: `分镜 ${p.frameIdx + 1}`,
|
||||
borderClass: "border-violet-300/60",
|
||||
aspect: "1/1",
|
||||
})),
|
||||
...videos.map((v, i) => {
|
||||
const videoSrc = apiAssetUrl(v.url)
|
||||
const posterSrc = apiAssetUrl(v.poster_url)
|
||||
@@ -1344,7 +1328,7 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
p.kind === "frame"
|
||||
? isSelected ? "border-emerald-400 ring-2 ring-emerald-400/60" : "border-white/30 dark:border-white/20"
|
||||
: p.borderClass
|
||||
} ${p.kind === "cutout" || p.kind === "subject" ? "bg-white" : "bg-black"}`}
|
||||
} ${p.kind === "subject" ? "bg-white" : "bg-black"}`}
|
||||
style={{ height: THUMBNAIL_HEIGHT, aspectRatio: p.aspect }}
|
||||
onMouseEnter={(e) => setHoverPreview({ id: p.id, ...canvasThumbnailAnchor(rootRef.current, e.currentTarget) })}
|
||||
onMouseLeave={() => setHoverPreview(null)}
|
||||
@@ -1370,10 +1354,6 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
})
|
||||
if (!d.selectedFrames.has(p.frameIdx)) d.onToggleFrame(p.frameIdx)
|
||||
d.onOpenWorkbench?.(p.frameIdx)
|
||||
} else if (p.kind === "cutout") {
|
||||
if (!d.selectedFrames.has(p.frameIdx)) d.onToggleFrame(p.frameIdx)
|
||||
d.onOpenStoryboard?.(p.frameIdx)
|
||||
d.onOpenWorkbench?.(p.frameIdx)
|
||||
} else {
|
||||
const video = videos.find((v) => v.id === p.videoId)
|
||||
if (video) {
|
||||
@@ -1400,7 +1380,7 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
<div className="absolute inset-0 bg-emerald-400/15 rounded-md pointer-events-none" />
|
||||
)}
|
||||
<div className="absolute bottom-0 right-0 bg-black/70 px-1 py-0.5 text-[8.5px] font-mono leading-none text-white rounded-bl rounded-br-md">
|
||||
{p.kind === "frame" ? p.caption.replace("s", "") + "s" : p.kind === "scene" ? "场景" : p.kind === "subject" ? "主体" : p.kind === "cutout" ? "抠图" : "视频"}
|
||||
{p.kind === "frame" ? p.caption.replace("s", "") + "s" : p.kind === "scene" ? "场景" : p.kind === "subject" ? "主体" : "视频"}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -1438,26 +1418,6 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
</button>
|
||||
)}
|
||||
|
||||
{p.kind === "cutout" && d.onCopyImage && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
d.onCopyImage?.({
|
||||
kind: "cutout",
|
||||
frame_idx: p.frameIdx,
|
||||
element_id: p.elementId,
|
||||
cutout_id: p.cutoutId,
|
||||
label: p.label,
|
||||
})
|
||||
}}
|
||||
title="复制此图(到分镜头编排工作台插槽粘贴)"
|
||||
className="absolute top-1.5 left-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-violet-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-violet-400"
|
||||
>
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{p.kind === "video" && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -1489,20 +1449,6 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
</button>
|
||||
)}
|
||||
|
||||
{p.kind === "cutout" && d.onDeleteCutout && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
d.onDeleteCutout?.(p.frameIdx, p.elementId, p.cutoutId)
|
||||
}}
|
||||
title="删除该提取图"
|
||||
className="absolute top-1.5 right-1.5 z-[70] inline-flex h-7 w-7 items-center justify-center rounded-full bg-rose-500/95 text-white shadow-lg backdrop-blur transition hover:scale-110 hover:bg-rose-400"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{p.kind === "video" && d.onDeleteVideo && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -1581,7 +1527,7 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1.5 flex items-center justify-between gap-2 text-[9.5px] text-[var(--text-faint)]">
|
||||
<span>{targetFrameCount} 张目标帧</span>
|
||||
<span>{targetFrameCount} 张素材帧</span>
|
||||
{qualityRiskCount > 0 ? (
|
||||
<span className="inline-flex items-center gap-1 text-amber-300/85">
|
||||
<AlertTriangle className="h-2.5 w-2.5" />
|
||||
@@ -1606,9 +1552,9 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
>
|
||||
<div className="mb-1 flex items-center gap-1 text-[var(--text-strong)] text-[12px] font-semibold">
|
||||
<ImageIcon className="h-3 w-3 text-orange-300" />
|
||||
{targetFrameCount}/{frames.length}
|
||||
{frames.length}
|
||||
</div>
|
||||
<div>目标关键帧</div>
|
||||
<div>关键帧素材</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -1632,7 +1578,7 @@ export function VisualLabNode({ data, selected }: any) {
|
||||
>
|
||||
<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 || cutoutCount}
|
||||
{subjectAssetCount}
|
||||
</div>
|
||||
<div>主体资产</div>
|
||||
</button>
|
||||
@@ -1653,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} 已清洗 · {sceneAssetCount} 场景图 · {subjectAssetCount || cutoutCount} 主体素材 · {selectedFrameCount}/{frames.length} 入编排 · {completedVideos.length} 已完成
|
||||
{cleanedCount} 已清洗 · {sceneAssetCount} 场景图 · {subjectAssetCount} 主体资产 · {targetFrameCount} 素材帧 · {completedVideos.length} 已完成
|
||||
</>
|
||||
) : (
|
||||
"解析后这里变成素材准备看板:先审关键帧,再生成场景图和主体资产包。"
|
||||
@@ -1808,7 +1754,7 @@ export function KeyframeNode({ data, selected }: any) {
|
||||
type="process" status={st}
|
||||
icon={<ImageIcon className="h-4 w-4" />}
|
||||
title="镜头拆解 · 素材准备"
|
||||
subtitle={`STEP 2 · ${frames.length ? `${d.selectedFrames.size}/${frames.length} 目标帧` : "等待抽取"}`}
|
||||
subtitle={`STEP 2 · ${frames.length ? `${frames.length} 素材帧` : "等待抽取"}`}
|
||||
selected={selected}
|
||||
pinned={d.pinnedNodes?.has("keyframe")}
|
||||
onTogglePin={() => d.onToggleNodePin?.("keyframe")}
|
||||
@@ -2342,7 +2288,7 @@ export function StoryboardNode({ data, selected }: any) {
|
||||
onClick={(e) => { e.stopPropagation(); d.onOpenWorkbench?.() }}
|
||||
disabled={!job || storyboardCount === 0}
|
||||
className="mt-2 w-full rounded-md bg-gradient-to-r from-violet-500 to-pink-500 px-3 py-2 text-[12px] font-semibold text-white shadow-lg shadow-violet-500/25 transition hover:opacity-95 disabled:cursor-not-allowed disabled:opacity-35"
|
||||
title={storyboardCount === 0 ? "先在关键帧节点选用分镜" : "进入 4 图槽分镜编排"}
|
||||
title={storyboardCount === 0 ? "先准备关键帧素材" : "进入 4 图槽分镜编排"}
|
||||
>
|
||||
进入分镜编排
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user