你看到的区域画面工作台 · Visual Lab
-
主要源码VisualLabNode in web/components/nodes/index.tsx;它现在是素材准备看板,汇总关键帧、场景图、主体资产包、普通抠图和视频任务。
+
主要源码VisualLabNode in web/components/nodes/index.tsx;它现在是素材准备看板,汇总关键帧、场景图、主体资产包和视频任务。
适合怎么描述“画面工作台的素材准备进度、分组缩略图、关键帧审核入口和后续分镜入口应该如何组织”。
你看到的区域关键帧素材审核面板
-
主要源码FrameLightbox;按“原图/清洗、场景图、主体包、审核”四个页签组织;相关接口包括 cleanupFrame、generateSceneAsset、generateSubjectAssets、cutoutElement。
+
主要源码FrameLightbox;按“原图/清洗、场景图、主体资产、审核”四个页签组织;相关接口包括 cleanupFrame、addElement、generateSceneAsset、generateSubjectAssets。
适合怎么描述“某张关键帧的水印、场景图、主体多视角/动作/表情图和质量风险应该如何审核”。
KeyElement
-
从关键帧里识别或手动添加的可借鉴元素。Vision 给的是候选,用户可编辑,并可多次生成提取图。
+
从关键帧里识别或手动添加的主体候选。Vision 给的是候选,用户可编辑、删除,并可基于它生成主体资产包。
KeyElement {
id,
name_zh, name_en, position,
@@ -751,7 +751,7 @@ SubjectAsset {
| 画面工作台 Visual Lab |
- 作为素材准备看板:显示准备进度、质量风险、关键帧 / 场景图 / 主体包 / 分镜视频四个入口;上方缩略图按关键帧、场景图、主体包、普通抠图、视频任务分组。点击关键帧进入素材审核面板,点击资产图复制到分镜编排。 |
+ 作为素材准备看板:显示准备进度、质量风险、关键帧 / 场景图 / 主体包 / 分镜视频四个入口;上方缩略图按关键帧、场景图、主体包、视频任务分组。点击关键帧进入素材审核面板,点击资产图复制到分镜编排。 |
不要在主卡片里堆复杂表单;主卡片只做状态总览和入口。 |
VisualLabNode、FrameLightbox、generateSceneAsset、generateSubjectAssets、视频任务接口 |
@@ -787,8 +787,8 @@ SubjectAsset {
视频下载或本地保存,ffmpeg 抽关键帧。
手动按时间戳加关键帧。
关键帧清洗水印,全图或区域清洗。
-
Vision 识别关键帧,输出 scene、objects、style、suggested_prompt。
-
元素增改删、区域元素添加、元素多次提取图。
+
Vision 识别关键帧,输出 scene、objects、style、suggested_prompt,并作为主体候选来源。
+
主体候选增改删、区域主体添加、主体资产包生成。
分镜工作台 4 图槽和改造说明自动保存。
nano-banana-pro image-to-image 生图。
@@ -813,8 +813,8 @@ SubjectAsset {
需求描述模板
-
改镜头拆解 / 元素提取
-
“我在关键帧 lightbox 里,Vision 识别后的元素列表应该怎么编辑/重提取/删除;点击元素不要跳转;提取图怎么预览和复制。”
+
改关键帧素材准备
+
“我在关键帧素材准备面板里,主体候选应该怎么编辑/删除;场景图和主体资产包怎么生成、审核、复制到分镜。”
改 Storyboard 节点
@@ -839,6 +839,18 @@ SubjectAsset {
变更记录
这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。
+
+
+ 2026-05-14 · 关键帧详情改为素材准备面板
+ FrameLightbox
+ UX
+
+
+
问题:面板标题仍叫“关键帧详情 · 元素提取”,里面还露出普通 cutout 抠图、AI 提取、元素清单等旧流程入口;“选用此帧”也无法说明它其实是在维护目标关键帧集合。
+
改动:KeyframePanelNode 标题改为“关键帧素材准备”;FrameLightbox 的主体页改成“主体识别 / 主体清单 / 主体资产包”,移除普通抠图列表和 AI 提取按钮;“选用此帧”改为“加入目标帧 / 目标帧 · 点击移出”。VisualLabNode 上方缩略图也不再展示普通抠图分组,只展示关键帧、场景图、主体包和视频任务。
+
影响:web/components/lightbox.tsx、web/components/nodes/index.tsx、docs/source-analysis.html。底层旧 cutout 数据和接口暂保留兼容历史任务,但不再作为新素材准备流程的可见入口。
+
+
2026-05-14 · 画面工作台改为素材准备看板
@@ -847,7 +859,7 @@ SubjectAsset {
问题:画面工作台从展示缩略图扩展为素材生产中枢后,关键帧、场景图、主体资产包和视频任务继续混在一个列表里会让流程不清晰;关键帧详情面板也把清洗、识别、场景和主体生成都堆在一屏。
-
改动:VisualLabNode 改成素材准备进度看板,显示目标关键帧、场景图、主体资产和分镜/视频四个入口,并在上方缩略图中按关键帧、场景图、主体包、普通抠图、视频任务分组。FrameLightbox 新增“原图/清洗、场景图、主体包、审核”四个页签,素材审核信息从普通元素列表中拆出来。
+
改动:VisualLabNode 改成素材准备进度看板,显示目标关键帧、场景图、主体资产和分镜/视频四个入口。FrameLightbox 新增“原图/清洗、场景图、主体资产、审核”四个页签,素材审核信息从普通列表中拆出来。
影响:web/components/nodes/index.tsx、web/components/lightbox.tsx、docs/source-analysis.html。这轮只重排工作台信息架构,批量自动准备队列仍留到下一阶段。
diff --git a/web/components/lightbox.tsx b/web/components/lightbox.tsx
index b81372f..7d9ab54 100644
--- a/web/components/lightbox.tsx
+++ b/web/components/lightbox.tsx
@@ -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
- {/* 主体 — 左:大图 + 清洗 / 目标帧;右:主体识别 + 主体资产 */}
+ {/* 主体 — 左:大图 + 清洗状态;右:主体识别 + 主体资产 */}
{/* 左侧大图区 */}
)}
-
+
+
+
+ 已在素材准备流程
+
+
+ 抽帧留下的关键帧默认都会参与清洗、场景图和主体资产准备,不需要在这里单独选用。
+
+
{/* 右侧主体识别 + 主体资产 */}
@@ -1092,7 +1084,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
- ←/→ 切换 · Space 切换目标帧 · ESC 关闭
+ ←/→ 切换关键帧 · ESC 关闭
)
diff --git a/web/components/nodes/index.tsx b/web/components/nodes/index.tsx
index d9db426..4ea3cda 100644
--- a/web/components/nodes/index.tsx
+++ b/web/components/nodes/index.tsx
@@ -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
| 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) {
)}
- {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" ? "主体" : "视频"}
@@ -1438,26 +1418,6 @@ export function VisualLabNode({ data, selected }: any) {
)}
- {p.kind === "cutout" && d.onCopyImage && (
-
- )}
-
{p.kind === "video" && (
-
{targetFrameCount} 张目标帧
+
{targetFrameCount} 张素材帧
{qualityRiskCount > 0 ? (
@@ -1606,9 +1552,9 @@ export function VisualLabNode({ data, selected }: any) {
>
- {targetFrameCount}/{frames.length}
+ {frames.length}
- 目标关键帧
+ 关键帧素材
- {subjectAssetCount || cutoutCount}
+ {subjectAssetCount}
主体资产
@@ -1653,7 +1599,7 @@ export function VisualLabNode({ data, selected }: any) {
{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={}
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 图槽分镜编排"}
>
进入分镜编排