{/* 起点:输入(含下载+拆分) */}
- {/* 分叉:上路 关键帧/分镜头编排/生视频 */}
+ {/* 分叉:上路 关键帧 / 生视频(分镜头编排在顶部 bar) */}
-
{/* 分叉:下路 转录/翻译/改写 */}
diff --git a/web/components/nodes/index.tsx b/web/components/nodes/index.tsx
index 83155f6..b5b584d 100644
--- a/web/components/nodes/index.tsx
+++ b/web/components/nodes/index.tsx
@@ -583,7 +583,7 @@ export function RewriteNode({ selected }: any) {
/* ============================================================
8. ImageGenNode — 显示 selected frames 的代表生成图
============================================================ */
-const IMAGEGEN_WIDTH = 320
+const IMAGEGEN_WIDTH = 360
export function ImageGenNode({ data, selected }: any) {
const d: NodeData = data
@@ -601,24 +601,25 @@ export function ImageGenNode({ data, selected }: any) {
const totalElements = elementCrops.length
const status: NodeStatus = !job ? "pending" : totalElements > 0 ? "done" : "pending"
+ const aspect = job && job.height > 0 ? `${job.width}/${job.height}` : "9/16"
return (
- {/* 节点上方:所有元素 crop 图(编排输入素材) */}
+ {/* 节点上方:所有元素 crop 图(编排输入素材)· 跟 keyframe 节点样式一致 */}
{elementCrops.length > 0 && job && (
{elementCrops.map((p) => (
diff --git a/web/components/storyboard-bar.tsx b/web/components/storyboard-bar.tsx
new file mode 100644
index 0000000..0a89339
--- /dev/null
+++ b/web/components/storyboard-bar.tsx
@@ -0,0 +1,104 @@
+"use client"
+import { useState } from "react"
+import { LayoutGrid, ChevronDown, ChevronUp, Sparkle } from "lucide-react"
+import { type Job, effectiveFrameUrl } from "@/lib/api"
+
+interface Props {
+ job: Job | null
+ selectedFrames: Set
+ onExpandFrame: (idx: number) => void
+}
+
+export function StoryboardBar({ job, selectedFrames, onExpandFrame }: Props) {
+ const [collapsed, setCollapsed] = useState(false)
+ if (!job) return null
+
+ // 按时间序排已选用的分镜
+ const frames = job.frames
+ .filter((f) => selectedFrames.has(f.index))
+ .sort((a, b) => a.timestamp - b.timestamp)
+
+ const aspect = job.height > 0 ? `${job.width}/${job.height}` : "9/16"
+ const totalElements = frames.reduce(
+ (sum, f) => sum + (f.elements?.filter((e) => e.cutout_id).length ?? 0),
+ 0,
+ )
+
+ return (
+
+ {/* header */}
+
+
+
+ 分镜头编排
+
+ {frames.length} 分镜 · {totalElements} 元素
+
+
+ · 组织分镜画面 → 为生成视频做准备
+
+
+
+
+
+ {/* thumbnails row */}
+ {!collapsed && (
+ frames.length === 0 ? (
+
+ 还没选用分镜 · 在「关键帧」节点上点击缩略图右下勾选「选用此帧」,被选用的分镜按时间序出现在这里
+
+ ) : (
+
+ {frames.map((f, i) => {
+ const elementCount = f.elements?.filter((e) => e.cutout_id).length ?? 0
+ const totalElCount = f.elements?.length ?? 0
+ const cleaned = f.cleaned_applied
+ return (
+
+ )
+ })}
+
+ )
+ )}
+
+ )
+}