diff --git a/.memory/worklog.json b/.memory/worklog.json index e6878e5..6823750 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1576,6 +1576,19 @@ "message": "auto-save 2026-05-13 13:31 (~1)", "hash": "1e06c14", "files_changed": 1 + }, + { + "ts": "2026-05-13T13:37:37+08:00", + "type": "commit", + "message": "auto-save 2026-05-13 13:37 (~1)", + "hash": "fa3fadd", + "files_changed": 1 + }, + { + "ts": "2026-05-13T05:37:39Z", + "type": "session-heartbeat", + "message": "Claude 会话活跃 · 最近命令:claude · 1 项未提交变更 · 最近提交:auto-save 2026-05-13 13:37 (~1)", + "files_changed": 1 } ] } diff --git a/web/app/page.tsx b/web/app/page.tsx index b25a0ac..4c6c945 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -14,6 +14,7 @@ import { } from "@/components/nodes" import { ThemeToggle } from "@/components/theme-toggle" import { Dashboard, type DashboardHandle } from "@/components/dashboard" +import { StoryboardBar } from "@/components/storyboard-bar" import { addManualFrame, analyzeJob, createJob, getJob, uploadJob, deleteFrame, deleteGeneratedImage, type Job } from "@/lib/api" import { VideoLightbox } from "@/components/video-lightbox" @@ -317,8 +318,14 @@ export default function Home() { - {/* 右区:紧凑 DAG 节点流图(撑满剩余宽度) */} -
+ {/* 右区:顶部 storyboard bar + DAG 节点流图 */} +
+ +
+
diff --git a/web/components/dashboard.tsx b/web/components/dashboard.tsx index aaf1e8e..6988d19 100644 --- a/web/components/dashboard.tsx +++ b/web/components/dashboard.tsx @@ -192,7 +192,7 @@ export const Dashboard = forwardRef(function Dashboard({ { key: "asr", title: "转录", type: "ai", icon: , step: 3 }, { key: "translate", title: "翻译", type: "ai", icon: , step: 4 }, { key: "rewrite", title: "改写", type: "ai", icon: , step: 5 }, - { key: "imagegen", title: "分镜头编排", type: "ai", icon: , step: 6 }, + // imagegen(分镜头编排)已移到顶部 StoryboardBar,不在 sidebar 里 { key: "videogen", title: "生视频", type: "ai", icon: , step: 7 }, { key: "compose", title: "合成", type: "output", icon: , step: 8 }, ] @@ -251,10 +251,9 @@ export const Dashboard = forwardRef(function Dashboard({
{/* 起点:输入(含下载+拆分) */} - {/* 分叉:上路 关键帧/分镜头编排/生视频 */} + {/* 分叉:上路 关键帧 / 生视频(分镜头编排在顶部 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 ( + + ) + })} +
+ ) + )} +
+ ) +}