auto-save 2026-05-12 17:00 (~3)
This commit is contained in:
@@ -97,6 +97,13 @@
|
||||
"message": "auto-save 2026-05-12 16:49 (~3)",
|
||||
"hash": "4779c26",
|
||||
"files_changed": 3
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-12T16:55:37+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-12 16:55 (~4)",
|
||||
"hash": "345391d",
|
||||
"files_changed": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
type NodeData,
|
||||
} from "@/components/nodes"
|
||||
import { ThemeToggle } from "@/components/theme-toggle"
|
||||
import { createJob, getJob, triggerTranscribe, uploadJob, type Job } from "@/lib/api"
|
||||
import { analyzeJob, createJob, getJob, uploadJob, type Job } from "@/lib/api"
|
||||
|
||||
const NODE_TYPES = {
|
||||
input: InputNode,
|
||||
@@ -62,14 +62,13 @@ export default function Home() {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const [job, setJob] = useState<Job | null>(null)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [analyzing, setAnalyzing] = useState(false)
|
||||
const [selectedFrames, setSelectedFrames] = useState<Set<number>>(new Set())
|
||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
const transcribeTriggeredRef = useRef<string | null>(null)
|
||||
|
||||
const handleSubmit = useCallback(async (url: string) => {
|
||||
setSubmitting(true)
|
||||
setSelectedFrames(new Set())
|
||||
transcribeTriggeredRef.current = null
|
||||
try {
|
||||
const created = await createJob(url)
|
||||
setJob(created)
|
||||
@@ -84,7 +83,6 @@ export default function Home() {
|
||||
const handleUpload = useCallback(async (file: File) => {
|
||||
setSubmitting(true)
|
||||
setSelectedFrames(new Set())
|
||||
transcribeTriggeredRef.current = null
|
||||
try {
|
||||
toast.info(`上传中:${file.name} (${(file.size / 1024 / 1024).toFixed(1)} MB)`)
|
||||
const created = await uploadJob(file)
|
||||
@@ -97,19 +95,34 @@ export default function Home() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleAnalyze = useCallback(async () => {
|
||||
if (!job) return
|
||||
setAnalyzing(true)
|
||||
setSelectedFrames(new Set())
|
||||
try {
|
||||
await analyzeJob(job.id, 5)
|
||||
toast.info("开始解析:拆轨 → 抽帧 → ASR → 翻译")
|
||||
} catch (e) {
|
||||
toast.error("解析触发失败:" + (e instanceof Error ? e.message : String(e)))
|
||||
} finally {
|
||||
setAnalyzing(false)
|
||||
}
|
||||
}, [job?.id])
|
||||
|
||||
const handleToggleFrame = useCallback((idx: number) => {
|
||||
setSelectedFrames((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(idx)) next.delete(idx)
|
||||
else if (next.size < 10) next.add(idx)
|
||||
else next.add(idx)
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
// 轮询 Job
|
||||
// 轮询 Job(downloaded / transcribed / failed 三态停止)
|
||||
useEffect(() => {
|
||||
if (!job) return
|
||||
if (job.status === "transcribed" || job.status === "failed") {
|
||||
const TERMINAL: Job["status"][] = ["downloaded", "transcribed", "failed"]
|
||||
if (TERMINAL.includes(job.status)) {
|
||||
if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null }
|
||||
return
|
||||
}
|
||||
@@ -122,23 +135,16 @@ export default function Home() {
|
||||
return () => { if (pollRef.current) clearInterval(pollRef.current) }
|
||||
}, [job?.id, job?.status])
|
||||
|
||||
// 抽帧完后自动触发 transcribe
|
||||
useEffect(() => {
|
||||
if (!job) return
|
||||
if (job.status !== "frames_extracted") return
|
||||
if (transcribeTriggeredRef.current === job.id) return
|
||||
transcribeTriggeredRef.current = job.id
|
||||
triggerTranscribe(job.id).catch((e) => toast.error("启动转录失败:" + e.message))
|
||||
}, [job?.id, job?.status])
|
||||
|
||||
const nodeData: NodeData = useMemo(() => ({
|
||||
job,
|
||||
submitting,
|
||||
analyzing,
|
||||
selectedFrames,
|
||||
onSubmitUrl: handleSubmit,
|
||||
onUploadFile: handleUpload,
|
||||
onAnalyze: handleAnalyze,
|
||||
onToggleFrame: handleToggleFrame,
|
||||
}), [job, submitting, selectedFrames, handleSubmit, handleUpload, handleToggleFrame])
|
||||
}), [job, submitting, analyzing, selectedFrames, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame])
|
||||
|
||||
// 用 useNodesState 让 ReactFlow 自己管位置(避免轮询时重置 drag)
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node>(
|
||||
|
||||
@@ -221,7 +221,7 @@ export function KeyframeNode({ data, selected }: any) {
|
||||
type="ai" status={st}
|
||||
icon={<ImageIcon className="h-4 w-4" />}
|
||||
title="关键帧 · Keyframes"
|
||||
subtitle={`STEP 4 · ${d.selectedFrames.size}/10`}
|
||||
subtitle={`STEP 4 · ${d.selectedFrames.size}/${d.job?.frames.length || 5}`}
|
||||
width={360}
|
||||
selected={selected}
|
||||
>
|
||||
@@ -253,7 +253,7 @@ export function KeyframeNode({ data, selected }: any) {
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[11.5px] text-[var(--text-faint)] py-2">等待视频流,自动 + 手动抽取 ≤10 张</div>
|
||||
<div className="text-[11.5px] text-[var(--text-faint)] py-2">等待解析后抽取(默认 5 张)</div>
|
||||
)}
|
||||
</NodeShell>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user