From f901b71d66f473a06f01b837322c36a705be89b4 Mon Sep 17 00:00:00 2001 From: kang Date: Tue, 12 May 2026 19:42:27 +0800 Subject: [PATCH] auto-save 2026-05-12 19:42 (~3) --- .memory/worklog.json | 7 +++ web/app/page.tsx | 36 ++++++--------- web/components/dashboard.tsx | 87 +++++++++++++++++++++--------------- 3 files changed, 73 insertions(+), 57 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index 08f699e..e3ac26d 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -258,6 +258,13 @@ "message": "auto-save 2026-05-12 19:31 (~2)", "hash": "ecef988", "files_changed": 2 + }, + { + "ts": "2026-05-12T19:36:53+08:00", + "type": "commit", + "message": "auto-save 2026-05-12 19:36 (~3)", + "hash": "902c3ed", + "files_changed": 3 } ] } diff --git a/web/app/page.tsx b/web/app/page.tsx index 6add56d..add7fae 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -8,7 +8,7 @@ import { } from "@xyflow/react" import { Toaster, toast } from "sonner" import { - InputNode, DownloadNode, SplitNode, KeyframeNode, ASRNode, + InputNode, KeyframeNode, ASRNode, TranslateNode, RewriteNode, ImageGenNode, VideoGenNode, ComposeNode, type NodeData, } from "@/components/nodes" @@ -19,8 +19,6 @@ import { FrameLightbox } from "@/components/lightbox" const NODE_TYPES = { input: InputNode, - download: DownloadNode, - split: SplitNode, keyframe: KeyframeNode, asr: ASRNode, translate: TranslateNode, @@ -30,27 +28,23 @@ const NODE_TYPES = { compose: ComposeNode, } -// 手布局:DAG,从左到右 -// 拆分后两路:上路 video → keyframe → imagegen → videogen ↘ -// 下路 audio → asr → translate → rewrite ────→ compose +// 合并 input + download + split 为一个节点 +// 分叉:上路 input → keyframe → imagegen → videogen ↘ +// 下路 input → asr → translate → rewrite ────→ compose const LAYOUT: Array<{ id: string; type: keyof typeof NODE_TYPES; x: number; y: number }> = [ { id: "input", type: "input", x: 40, y: 240 }, - { id: "download", type: "download", x: 400, y: 240 }, - { id: "split", type: "split", x: 720, y: 240 }, - { id: "keyframe", type: "keyframe", x: 1060, y: 60 }, - { id: "asr", type: "asr", x: 1060, y: 440 }, - { id: "translate", type: "translate", x: 1440, y: 440 }, - { id: "imagegen", type: "imagegen", x: 1480, y: 60 }, - { id: "rewrite", type: "rewrite", x: 1820, y: 440 }, - { id: "videogen", type: "videogen", x: 1860, y: 60 }, - { id: "compose", type: "compose", x: 2240, y: 240 }, + { id: "keyframe", type: "keyframe", x: 460, y: 60 }, + { id: "asr", type: "asr", x: 460, y: 440 }, + { id: "translate", type: "translate", x: 840, y: 440 }, + { id: "imagegen", type: "imagegen", x: 880, y: 60 }, + { id: "rewrite", type: "rewrite", x: 1220, y: 440 }, + { id: "videogen", type: "videogen", x: 1260, y: 60 }, + { id: "compose", type: "compose", x: 1640, y: 240 }, ] const EDGES_RAW: Array<[string, string]> = [ - ["input", "download"], - ["download", "split"], - ["split", "keyframe"], - ["split", "asr"], + ["input", "keyframe"], + ["input", "asr"], ["asr", "translate"], ["translate", "rewrite"], ["keyframe", "imagegen"], @@ -213,9 +207,7 @@ export default function Home() { // 边的 animated 状态跟 Job 进度联动 useEffect(() => { const doneOf: Record = { - input: !!job, - download: !!job?.video_url, - split: !!job && ["frames_extracted", "transcribing", "transcribed"].includes(job.status), + input: !!job?.video_url, keyframe: !!job && job.frames.length > 0, asr: !!job && job.transcript.length > 0, translate: !!job && (job.transcript.some((s) => s.zh) ?? false), diff --git a/web/components/dashboard.tsx b/web/components/dashboard.tsx index ab98102..07389eb 100644 --- a/web/components/dashboard.tsx +++ b/web/components/dashboard.tsx @@ -98,9 +98,14 @@ export function Dashboard({ data }: Props) { const isFailed = job?.status === "failed" const colState: Record = { - input: !job ? "pending" : "done", - download: !job ? "pending" : isDownloading ? "running" : hasVideo ? "done" : isFailed && job.progress < 30 ? "failed" : "pending", - split: !job ? "pending" : isSplitting ? "running" : hasFrames ? "done" : isFailed && job.progress >= 30 && job.progress < 50 ? "failed" : "pending", + // input 状态合并:覆盖原 input + download + split + input: !job ? "pending" + : isDownloading ? "running" + : isSplitting ? "running" + : hasFrames || hasTranscript ? "done" + : hasVideo ? "done" + : isFailed && job.progress < 50 ? "failed" + : "pending", keyframe: !job ? "pending" : (isSplitting && !hasFrames) ? "running" : hasFrames ? "done" : isFailed && job.progress >= 50 && job.progress < 70 ? "failed" : "pending", asr: !job ? "pending" : job.status === "transcribing" ? "running" : hasTranscript ? "done" : isFailed && job.progress >= 70 ? "failed" : "pending", translate: !job ? "pending" : job.status === "transcribing" ? "running" : hasZh ? "done" : "pending", @@ -112,9 +117,7 @@ export function Dashboard({ data }: Props) { /* 每列摘要 = tile 副标题 */ const colSummary: Record = { - input: job ? (job.url.startsWith("upload://") ? "📎 上传" : "🔗 链接") : "等待", - download: hasVideo && job ? `${job.width}×${job.height} · ${job.duration.toFixed(1)}s` : isDownloading ? "下载中…" : "—", - split: hasFrames ? "wav 已生成" : isSplitting ? "拆轨中…" : "—", + input: !job ? "等待" : isDownloading ? "下载中…" : hasVideo ? `${job.width}×${job.height} · ${job.duration.toFixed(1)}s` : "—", keyframe: hasFrames ? `${data.selectedFrames.size}/${job!.frames.length} 选用` : "—", asr: hasTranscript ? `${job!.transcript.length} 段` : "—", translate: hasZh ? `${job!.transcript.filter((s) => s.zh).length} 段` : "—", @@ -126,15 +129,13 @@ export function Dashboard({ data }: Props) { const TILES: Array<{ key: string; title: string; type: ColType; icon: ReactNode; step: number }> = [ { key: "input", title: "输入", type: "input", icon: , step: 1 }, - { key: "download", title: "下载", type: "process", icon: , step: 2 }, - { key: "split", title: "拆分", type: "process", icon: , step: 3 }, - { key: "keyframe", title: "关键帧", type: "ai", icon: , step: 4 }, - { key: "asr", title: "转录", type: "ai", icon: , step: 5 }, - { key: "translate", title: "翻译", type: "ai", icon: , step: 6 }, - { key: "rewrite", title: "改写", type: "ai", icon: , step: 7 }, - { key: "imagegen", title: "生图", type: "ai", icon: , step: 8 }, - { key: "videogen", title: "生视频", type: "ai", icon: , step: 9 }, - { key: "compose", title: "合成", type: "output", icon: , step: 10 }, + { key: "keyframe", title: "关键帧", type: "ai", icon: , step: 2 }, + { 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 }, + { key: "videogen", title: "生视频", type: "ai", icon: , step: 7 }, + { key: "compose", title: "合成", type: "output", icon: , step: 8 }, ] // 单选展开:toggle 同一 key = 收起;点其他 key = 切换 @@ -169,10 +170,8 @@ export function Dashboard({ data }: Props) { return (
- {/* 主线:input / download / split */} + {/* 起点:输入(含下载+拆分) */} - - {/* 分叉:上路 关键帧/生图/生视频 */}
@@ -275,24 +274,42 @@ export function Dashboard({ data }: Props) { />
- {hasVideo && ( - - - {job && ( -
- - {job.url.startsWith("upload://") ? `📎 ${job.url.slice(9)}` : job.url} - + {hasVideo && job && ( + <> + {/* 元数据 */} + +
+ {job.url.startsWith("upload://") ? `📎 ${job.url.slice(9)}` : `🔗 ${job.url}`}
- )} -
+ + {/* 拆分流 */} + +
+
+
视频流
+
→ 关键帧
+
+
+
音频流
+
→ ASR (wav)
+
+
+
+ {/* 解析按钮 */} + + +
+ 全流程 = 拆分 → 抽帧 → ASR → 翻译 +
+
+ )} )}