auto-save 2026-05-14 04:32 (~5)

This commit is contained in:
2026-05-14 04:32:27 +08:00
parent 8f2b8d373c
commit 4935e34eb0
5 changed files with 106 additions and 21 deletions

View File

@@ -42,6 +42,7 @@ const FRAME_TARGET_LABELS: Record<FrameExtractTarget, string> = {
motion: "动作峰值",
}
const FRAME_QUALITY_LABELS: Record<FrameExtractQuality, string> = {
auto: "自动",
fast: "快速",
accurate: "精细",
ultra: "极准",
@@ -178,7 +179,7 @@ export default function Home() {
if (!targetJob) return
const frameTarget = frameTargets[jobId] ?? "balanced"
const frameCount = frameCounts[jobId] ?? 5
const frameQuality = frameQualities[jobId] ?? "ultra"
const frameQuality = frameQualities[jobId] ?? "auto"
const mode = options?.mode ?? (targetJob.frames.length > 0 ? "append" : "replace")
setActiveJobId(jobId)
setAnalyzing(true)
@@ -497,30 +498,43 @@ export default function Home() {
})
}, [job?.id, job?.frames])
// 轮询 Jobdownloaded / transcribed / failed 三态停止)
// 轮询 Job:任一视频在下载 / 抽帧 / 生视频时都继续轮询,支持多个抽帧任务排队。
const prevStatusRef = useRef<string | null>(null)
useEffect(() => {
if (!job) return
if (jobs.length === 0) return
// 状态切到 downloaded 时提示用户点解析(仅一次)
if (job.status === "downloaded" && prevStatusRef.current !== "downloaded") {
if (job?.status === "downloaded" && prevStatusRef.current !== "downloaded") {
toast.info("📥 视频已就绪 — 请点 Input 节点里的「点这里开始解析」按钮", { duration: 6000 })
}
prevStatusRef.current = job.status
prevStatusRef.current = job?.status ?? null
const runningVideo = !!job.generated_videos?.some((v) => v.status === "queued" || v.status === "in_progress")
const TERMINAL: Job["status"][] = ["downloaded", "frames_extracted", "transcribed", "failed"]
if (TERMINAL.includes(job.status) && !runningVideo) {
const runningIds = jobs
.filter((item) => {
const runningVideo = !!item.generated_videos?.some((v) => v.status === "queued" || v.status === "in_progress")
return runningVideo || !TERMINAL.includes(item.status)
})
.map((item) => item.id)
if (runningIds.length === 0) {
if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null }
return
}
pollRef.current = setInterval(async () => {
try {
const latest = await getJob(job.id)
setJob(latest)
const latestJobs = await Promise.all(runningIds.map((id) => getJob(id).catch(() => null)))
const byId = new Map(latestJobs.filter((item): item is Job => !!item).map((item) => [item.id, item]))
if (byId.size > 0) {
setJobs((prev) => prev.map((item) => byId.get(item.id) ?? item))
}
} catch { /* silent */ }
}, 1500)
return () => { if (pollRef.current) clearInterval(pollRef.current) }
}, [job?.id, job?.status, job?.generated_videos?.map((v) => `${v.id}:${v.status}:${v.progress}`).join("|")])
}, [
job?.id,
job?.status,
jobs.map((item) => `${item.id}:${item.status}:${item.progress}:${item.generated_videos?.map((v) => `${v.id}:${v.status}:${v.progress}`).join(",")}`).join("|"),
])
const [pinnedNodes, setPinnedNodes] = useState<Set<string>>(() => new Set(loadNodePins()))
const handleToggleNodePin = useCallback((id: string) => {

View File

@@ -135,6 +135,7 @@ const FRAME_TARGET_OPTIONS: Array<{ value: FrameExtractTarget; label: string; hi
]
const FRAME_COUNT_OPTIONS = [3, 5, 8, 12]
const FRAME_QUALITY_OPTIONS: Array<{ value: FrameExtractQuality; label: string; hint: string }> = [
{ value: "auto", label: "自动", hint: "按电脑性能和视频时长自动选择" },
{ value: "fast", label: "快速", hint: "2fps / 360px长视频省电" },
{ value: "accurate", label: "精细", hint: "8fps / 720pxM2 Max 轻松可用" },
{ value: "ultra", label: "极准", hint: "12fps / 960px本机约 3 秒扫描 1 分钟视频" },
@@ -438,7 +439,7 @@ function FrameExtractQuickBar({
onAnalyze: () => void
}) {
const option = FRAME_TARGET_OPTIONS.find((item) => item.value === target) ?? FRAME_TARGET_OPTIONS[0]
const qualityOption = FRAME_QUALITY_OPTIONS.find((item) => item.value === quality) ?? FRAME_QUALITY_OPTIONS[1]
const qualityOption = FRAME_QUALITY_OPTIONS.find((item) => item.value === quality) ?? FRAME_QUALITY_OPTIONS[0]
const [settingsOpen, setSettingsOpen] = useState(false)
return (
@@ -569,7 +570,7 @@ export function InputNode({ data, selected }: NodeProps<{ data: NodeData }> | an
const toolWidth = Math.max(148, thumbNaturalWidth)
const target = d.frameTargets[j.id] ?? "balanced"
const count = d.frameCounts[j.id] ?? 5
const quality = d.frameQualities[j.id] ?? "ultra"
const quality = d.frameQualities[j.id] ?? "auto"
const jHasFrames = j.frames.length > 0
const jRunning = ["splitting", "transcribing"].includes(j.status)
return (
@@ -583,7 +584,7 @@ export function InputNode({ data, selected }: NodeProps<{ data: NodeData }> | an
target={target}
count={count}
quality={quality}
disabled={jRunning || d.analyzing}
disabled={jRunning}
running={jRunning}
hasFrames={jHasFrames}
onTargetChange={(next) => d.onFrameTargetChange(j.id, next)}

View File

@@ -130,7 +130,7 @@ export interface KeyFrame {
export type FrameExtractTarget = "balanced" | "subject" | "transition" | "expression" | "motion"
export type FrameExtractMode = "replace" | "append"
export type FrameExtractQuality = "fast" | "accurate" | "ultra"
export type FrameExtractQuality = "auto" | "fast" | "accurate" | "ultra"
export interface TranscriptSegment {
index: number
@@ -268,7 +268,7 @@ export async function analyzeJob(
frames = 5,
target: FrameExtractTarget = "balanced",
mode: FrameExtractMode = "replace",
quality: FrameExtractQuality = "accurate",
quality: FrameExtractQuality = "auto",
): Promise<Job> {
const qs = new URLSearchParams({ frames: String(frames), target, mode, quality })
const res = await fetch(`${API_BASE}/jobs/${id}/analyze?${qs.toString()}`, { method: "POST" })