auto-save 2026-05-12 19:42 (~3)
This commit is contained in:
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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<string, boolean> = {
|
||||
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),
|
||||
|
||||
@@ -98,9 +98,14 @@ export function Dashboard({ data }: Props) {
|
||||
const isFailed = job?.status === "failed"
|
||||
|
||||
const colState: Record<string, ColState> = {
|
||||
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<string, string> = {
|
||||
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: <Link2 className="h-3.5 w-3.5" />, step: 1 },
|
||||
{ key: "download", title: "下载", type: "process", icon: <Download className="h-3.5 w-3.5" />, step: 2 },
|
||||
{ key: "split", title: "拆分", type: "process", icon: <Scissors className="h-3.5 w-3.5" />, step: 3 },
|
||||
{ key: "keyframe", title: "关键帧", type: "ai", icon: <ImageIcon className="h-3.5 w-3.5" />, step: 4 },
|
||||
{ key: "asr", title: "转录", type: "ai", icon: <Mic className="h-3.5 w-3.5" />, step: 5 },
|
||||
{ key: "translate", title: "翻译", type: "ai", icon: <Languages className="h-3.5 w-3.5" />, step: 6 },
|
||||
{ key: "rewrite", title: "改写", type: "ai", icon: <FileEdit className="h-3.5 w-3.5" />, step: 7 },
|
||||
{ key: "imagegen", title: "生图", type: "ai", icon: <Sparkles className="h-3.5 w-3.5" />, step: 8 },
|
||||
{ key: "videogen", title: "生视频", type: "ai", icon: <Film className="h-3.5 w-3.5" />, step: 9 },
|
||||
{ key: "compose", title: "合成", type: "output", icon: <FileVideo className="h-3.5 w-3.5" />, step: 10 },
|
||||
{ key: "keyframe", title: "关键帧", type: "ai", icon: <ImageIcon className="h-3.5 w-3.5" />, step: 2 },
|
||||
{ key: "asr", title: "转录", type: "ai", icon: <Mic className="h-3.5 w-3.5" />, step: 3 },
|
||||
{ key: "translate", title: "翻译", type: "ai", icon: <Languages className="h-3.5 w-3.5" />, step: 4 },
|
||||
{ key: "rewrite", title: "改写", type: "ai", icon: <FileEdit className="h-3.5 w-3.5" />, step: 5 },
|
||||
{ key: "imagegen", title: "生图", type: "ai", icon: <Sparkles className="h-3.5 w-3.5" />, step: 6 },
|
||||
{ key: "videogen", title: "生视频", type: "ai", icon: <Film className="h-3.5 w-3.5" />, step: 7 },
|
||||
{ key: "compose", title: "合成", type: "output", icon: <FileVideo className="h-3.5 w-3.5" />, step: 8 },
|
||||
]
|
||||
|
||||
// 单选展开:toggle 同一 key = 收起;点其他 key = 切换
|
||||
@@ -169,10 +170,8 @@ export function Dashboard({ data }: Props) {
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col gap-1 p-1.5">
|
||||
{/* 主线:input / download / split */}
|
||||
{/* 起点:输入(含下载+拆分) */}
|
||||
<Tile tkey="input" />
|
||||
<Tile tkey="download" />
|
||||
<Tile tkey="split" />
|
||||
{/* 分叉:上路 关键帧/生图/生视频 */}
|
||||
<div className="border-l-2 border-violet-400/30 pl-1.5 ml-1 space-y-1">
|
||||
<Tile tkey="keyframe" />
|
||||
@@ -275,24 +274,42 @@ export function Dashboard({ data }: Props) {
|
||||
/>
|
||||
</div>
|
||||
</KanbanCard>
|
||||
{hasVideo && (
|
||||
<KanbanCard tone="green" tags={["下一步"]} title="解析视频">
|
||||
<button
|
||||
type="button"
|
||||
disabled={isAnalyzing || data.analyzing}
|
||||
onClick={data.onAnalyze}
|
||||
className={`w-full text-[12.5px] py-2 rounded-md bg-gradient-to-r from-indigo-500 to-violet-500 text-white hover:opacity-95 disabled:opacity-40 inline-flex items-center justify-center gap-1.5 font-semibold shadow-lg shadow-violet-500/30 mt-1 ${!isAnalyzing && !data.analyzing && !hasFrames ? "animate-pulse" : ""}`}
|
||||
>
|
||||
{(isAnalyzing || data.analyzing) ? <><Loader2 className="h-3.5 w-3.5 animate-spin" /> 解析中…</> : hasFrames ? "重新解析" : "▶ 开始解析"}
|
||||
</button>
|
||||
{job && (
|
||||
<div className="kanban-meta">
|
||||
<span className="font-mono truncate">
|
||||
{job.url.startsWith("upload://") ? `📎 ${job.url.slice(9)}` : job.url}
|
||||
</span>
|
||||
{hasVideo && job && (
|
||||
<>
|
||||
{/* 元数据 */}
|
||||
<KanbanCard tone="orange" tags={["视频源", job.url.startsWith("upload://") ? "上传" : "yt-dlp"]} title={`${job.width}×${job.height} · ${job.duration.toFixed(1)}s`}>
|
||||
<div className="text-[10.5px] text-[var(--text-soft)] truncate font-mono">
|
||||
{job.url.startsWith("upload://") ? `📎 ${job.url.slice(9)}` : `🔗 ${job.url}`}
|
||||
</div>
|
||||
)}
|
||||
</KanbanCard>
|
||||
</KanbanCard>
|
||||
{/* 拆分流 */}
|
||||
<KanbanCard tone="amber" tags={["ffmpeg"]} title="拆分音视频流">
|
||||
<div className="grid grid-cols-2 gap-2 text-[11px]">
|
||||
<div className="rounded-md bg-violet-500/10 border border-violet-400/20 px-2 py-1.5">
|
||||
<div className="text-[9.5px] uppercase tracking-widest text-violet-300">视频流</div>
|
||||
<div className="text-[var(--text-strong)]">→ 关键帧</div>
|
||||
</div>
|
||||
<div className="rounded-md bg-pink-500/10 border border-pink-400/20 px-2 py-1.5">
|
||||
<div className="text-[9.5px] uppercase tracking-widest text-pink-300">音频流</div>
|
||||
<div className="text-[var(--text-strong)]">→ ASR (wav)</div>
|
||||
</div>
|
||||
</div>
|
||||
</KanbanCard>
|
||||
{/* 解析按钮 */}
|
||||
<KanbanCard tone="green" tags={["下一步"]} title={hasFrames ? "重新解析" : "启动全流程"}>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isAnalyzing || data.analyzing}
|
||||
onClick={data.onAnalyze}
|
||||
className={`w-full text-[12.5px] py-2 rounded-md bg-gradient-to-r from-indigo-500 to-violet-500 text-white hover:opacity-95 disabled:opacity-40 inline-flex items-center justify-center gap-1.5 font-semibold shadow-lg shadow-violet-500/30 mt-1 ${!isAnalyzing && !data.analyzing && !hasFrames ? "animate-pulse" : ""}`}
|
||||
>
|
||||
{(isAnalyzing || data.analyzing) ? <><Loader2 className="h-3.5 w-3.5 animate-spin" /> 解析中…</> : hasFrames ? "↻ 重新解析" : "▶ 开始解析(全流程)"}
|
||||
</button>
|
||||
<div className="kanban-meta">
|
||||
全流程 = 拆分 → 抽帧 → ASR → 翻译
|
||||
</div>
|
||||
</KanbanCard>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user