auto-save 2026-05-13 17:50 (~4)
This commit is contained in:
@@ -9,14 +9,14 @@ import {
|
||||
import { Toaster, toast } from "sonner"
|
||||
import {
|
||||
InputNode, KeyframeNode, ASRNode,
|
||||
TranslateNode, RewriteNode, ImageGenNode, VideoGenNode, ComposeNode,
|
||||
TranslateNode, RewriteNode, StoryboardNode, VideoGenNode, ComposeNode,
|
||||
type NodeData,
|
||||
} from "@/components/nodes"
|
||||
import { ThemeToggle } from "@/components/theme-toggle"
|
||||
import { Dashboard, type DashboardHandle } from "@/components/dashboard"
|
||||
import { StoryboardBar } from "@/components/storyboard-bar"
|
||||
import { StoryboardWorkbench } from "@/components/storyboard-workbench"
|
||||
import { addManualFrame, analyzeJob, createJob, getJob, uploadJob, deleteFrame, deleteGeneratedImage, pushStoryboardImage, type Job, type ImageRef } from "@/lib/api"
|
||||
import { addManualFrame, analyzeJob, createJob, getJob, uploadJob, deleteFrame, deleteGeneratedImage, type Job, type ImageRef } from "@/lib/api"
|
||||
import { VideoLightbox } from "@/components/video-lightbox"
|
||||
|
||||
const NODE_TYPES = {
|
||||
@@ -25,20 +25,20 @@ const NODE_TYPES = {
|
||||
asr: ASRNode,
|
||||
translate: TranslateNode,
|
||||
rewrite: RewriteNode,
|
||||
imagegen: ImageGenNode,
|
||||
storyboard: StoryboardNode,
|
||||
videogen: VideoGenNode,
|
||||
compose: ComposeNode,
|
||||
}
|
||||
|
||||
// 合并 input + download + split 为一个节点
|
||||
// 分叉:上路 input → keyframe → imagegen → videogen ↘
|
||||
// 下路 input → asr → translate → rewrite ────→ compose
|
||||
// 分叉:上路 input → keyframe → storyboard → videogen ↘
|
||||
// 下路 input → asr → translate → rewrite ──────→ storyboard / compose
|
||||
const LAYOUT: Array<{ id: string; type: keyof typeof NODE_TYPES; x: number; y: number }> = [
|
||||
{ id: "input", type: "input", x: 40, 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: "storyboard", type: "storyboard", 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 },
|
||||
@@ -49,9 +49,9 @@ const EDGES_RAW: Array<[string, string]> = [
|
||||
["input", "asr"],
|
||||
["asr", "translate"],
|
||||
["translate", "rewrite"],
|
||||
["keyframe", "imagegen"],
|
||||
["rewrite", "imagegen"],
|
||||
["imagegen", "videogen"],
|
||||
["keyframe", "storyboard"],
|
||||
["rewrite", "storyboard"],
|
||||
["storyboard", "videogen"],
|
||||
["videogen", "compose"],
|
||||
["rewrite", "compose"],
|
||||
]
|
||||
@@ -135,7 +135,7 @@ export default function Home() {
|
||||
setSelectedFrames(new Set())
|
||||
try {
|
||||
await analyzeJob(job.id, 5)
|
||||
toast.info("开始解析:拆轨 → 抽帧 → ASR → 翻译")
|
||||
toast.info("开始解析:拆轨 → 抽帧。声音文案轨单独处理")
|
||||
// 乐观更新本地状态,让轮询 useEffect 重新启动
|
||||
setJob((prev) => prev ? { ...prev, status: "splitting", message: "拆轨中…", progress: 30 } : prev)
|
||||
} catch (e) {
|
||||
@@ -199,23 +199,6 @@ export default function Home() {
|
||||
toast.success(`已复制:${ref.label || (ref.kind === "keyframe" ? "关键帧" : "元素")} · 到分镜头编排工作台粘贴`)
|
||||
}, [])
|
||||
|
||||
const handlePushToStoryboard = useCallback(async (payload: { kind: "keyframe" | "cutout"; frameIdx: number; elementId?: string; cutoutId?: string; label?: string }) => {
|
||||
if (!activeJobId) return
|
||||
try {
|
||||
const updated = await pushStoryboardImage(activeJobId, {
|
||||
kind: payload.kind,
|
||||
frame_idx: payload.frameIdx,
|
||||
element_id: payload.elementId,
|
||||
cutout_id: payload.cutoutId,
|
||||
label: payload.label,
|
||||
})
|
||||
setJob(updated)
|
||||
toast.success("已推送到分镜头编排")
|
||||
} catch (e) {
|
||||
toast.error("推送失败:" + (e instanceof Error ? e.message : String(e)))
|
||||
}
|
||||
}, [activeJobId, setJob])
|
||||
|
||||
// URL ?job=xxx,yyy 自动恢复多个 job
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
@@ -240,6 +223,24 @@ export default function Home() {
|
||||
window.history.replaceState({}, "", url.toString())
|
||||
}, [jobs.length])
|
||||
|
||||
// 恢复已保存的分镜选择:刷新页面后,已有 storyboard 的帧仍应出现在顶部编排栏。
|
||||
useEffect(() => {
|
||||
if (!job || job.frames.length === 0) return
|
||||
const persisted = job.frames.filter((f) => !!f.storyboard).map((f) => f.index)
|
||||
if (persisted.length === 0) return
|
||||
setSelectedFrames((prev) => {
|
||||
let changed = false
|
||||
const next = new Set(prev)
|
||||
for (const idx of persisted) {
|
||||
if (!next.has(idx)) {
|
||||
next.add(idx)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return changed ? next : prev
|
||||
})
|
||||
}, [job?.id, job?.frames])
|
||||
|
||||
// 轮询 Job(downloaded / transcribed / failed 三态停止)
|
||||
const prevStatusRef = useRef<string | null>(null)
|
||||
useEffect(() => {
|
||||
@@ -250,7 +251,7 @@ export default function Home() {
|
||||
}
|
||||
prevStatusRef.current = job.status
|
||||
|
||||
const TERMINAL: Job["status"][] = ["downloaded", "transcribed", "failed"]
|
||||
const TERMINAL: Job["status"][] = ["downloaded", "frames_extracted", "transcribed", "failed"]
|
||||
if (TERMINAL.includes(job.status)) {
|
||||
if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null }
|
||||
return
|
||||
@@ -286,9 +287,9 @@ export default function Home() {
|
||||
onDeleteFrame: handleDeleteFrame,
|
||||
onDeleteGenerated: handleDeleteGenerated,
|
||||
onOpenStoryboard: (idx: number) => setStoryboardFrame(idx),
|
||||
onPushToStoryboard: handlePushToStoryboard,
|
||||
onOpenWorkbench: () => setWorkbenchOpen(true),
|
||||
onCopyImage: handleCopyImage,
|
||||
}), [job, jobs, activeJobId, submitting, analyzing, selectedFrames, expandedFrame, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame, handleAddManualFrame, handleSwitchJob, setJob, handleDeleteFrame, handleDeleteGenerated, handlePushToStoryboard, handleCopyImage])
|
||||
}), [job, jobs, activeJobId, submitting, analyzing, selectedFrames, expandedFrame, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame, handleAddManualFrame, handleSwitchJob, setJob, handleDeleteFrame, handleDeleteGenerated, handleCopyImage])
|
||||
|
||||
// 用 useNodesState 让 ReactFlow 自己管位置(避免轮询时重置 drag)
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node>(
|
||||
@@ -318,6 +319,8 @@ export default function Home() {
|
||||
keyframe: !!job && job.frames.length > 0,
|
||||
asr: !!job && job.transcript.length > 0,
|
||||
translate: !!job && (job.transcript.some((s) => s.zh) ?? false),
|
||||
rewrite: !!job && (job.transcript.some((s) => s.zh) ?? false),
|
||||
storyboard: selectedFrames.size > 0,
|
||||
}
|
||||
setEdges((prev) => prev.map((e) => ({ ...e, animated: !!doneOf[e.source] })))
|
||||
}, [job, setEdges])
|
||||
|
||||
Reference in New Issue
Block a user