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

This commit is contained in:
2026-05-14 04:26:56 +08:00
parent ec96e81c02
commit 8f2b8d373c
5 changed files with 80 additions and 38 deletions

View File

@@ -3186,6 +3186,19 @@
"type": "session-heartbeat", "type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 6 项未提交变更 · 最近提交auto-save 2026-05-14 04:15 (~4)", "message": "Codex 会话活跃 · 最近命令codex · 6 项未提交变更 · 最近提交auto-save 2026-05-14 04:15 (~4)",
"files_changed": 6 "files_changed": 6
},
{
"ts": "2026-05-14T04:21:26+08:00",
"type": "commit",
"message": "auto-save 2026-05-14 04:21 (~6)",
"hash": "ec96e81",
"files_changed": 6
},
{
"ts": "2026-05-13T20:23:12Z",
"type": "session-heartbeat",
"message": "Claude 会话活跃 · 最近命令claude · 4 项未提交变更 · 最近提交auto-save 2026-05-14 04:21 (~6)",
"files_changed": 4
} }
] ]
} }

View File

@@ -640,7 +640,7 @@ async def pipeline_analyze(
existing_frames = list(job.frames) if not replacing else [] existing_frames = list(job.frames) if not replacing else []
if replacing and frames_dir.exists(): if replacing and frames_dir.exists():
shutil.rmtree(frames_dir) shutil.rmtree(frames_dir)
frames_dir.mkdir(parents=True) frames_dir.mkdir(parents=True, exist_ok=True)
scan_dir = d / "frame_scan" scan_dir = d / "frame_scan"
if scan_dir.exists(): if scan_dir.exists():
shutil.rmtree(scan_dir) shutil.rmtree(scan_dir)

View File

@@ -723,7 +723,7 @@ api/main.py
<tbody> <tbody>
<tr> <tr>
<td><span class="tag blue">输入 Input</span></td> <td><span class="tag blue">输入 Input</span></td>
<td>创建/上传任务,显示视频就绪;每个视频缩略图上方都有绑定自己的自动抽帧快捷工具条,可快速选目标、张数精度并多次追加;单击视频缩略图打开画布内抽帧面板。</td> <td>创建/上传任务,显示视频就绪;每个视频缩略图上方都有绑定自己的自动抽帧快捷工具条,默认只露出目标和抽帧按钮,张数/精度收进设置;横屏/竖屏都按真实比例显示和评分;单击视频缩略图打开画布内抽帧面板。</td>
<td>不要自动一路跑到 ASR 或生图;用户需要控制解析节奏。</td> <td>不要自动一路跑到 ASR 或生图;用户需要控制解析节奏。</td>
<td><code>page.tsx</code><code>InputNode</code><code>VideoFramePanelNode</code><code>api/main.py</code></td> <td><code>page.tsx</code><code>InputNode</code><code>VideoFramePanelNode</code><code>api/main.py</code></td>
</tr> </tr>
@@ -817,6 +817,19 @@ api/main.py
<h2>变更记录</h2> <h2>变更记录</h2>
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p> <p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
<div class="changelog"> <div class="changelog">
<article class="change">
<header>
<h3>2026-05-14 · 抽帧工具条降噪并修复追加失败</h3>
<span class="tag violet">Input</span>
<span class="tag blue">Bugfix</span>
</header>
<div class="body">
<p><strong>问题:</strong>每个缩略图上方同时放目标、张数、精度和按钮太拥挤;另外追加抽帧时可能没有新增图片。</p>
<p><strong>根因:</strong>追加模式下 <code>frames</code> 目录已经存在,但后端仍使用非 <code>exist_ok</code><code>mkdir</code>,触发 <code>File exists</code> 后任务进入解析失败。</p>
<p><strong>改动:</strong>工具条默认只显示目标、抽帧/追加和设置按钮;张数、精度折叠到设置里。后端改为允许已存在的 <code>frames</code> 目录,追加模式不再因目录存在失败。缩略图高度增大到 192px横屏/竖屏都按真实比例显示;抽帧评分也按视频原比例缩放,不固定 16:9。</p>
<p><strong>影响:</strong><code>api/main.py</code><code>web/app/page.tsx</code><code>web/components/nodes/index.tsx</code><code>docs/source-analysis.html</code>。已用临时 job 验证 append已有 1 张关键帧时追加 3 张后变为 4 张。</p>
</div>
</article>
<article class="change"> <article class="change">
<header> <header>
<h3>2026-05-14 · 自动抽帧增加本地精度模式</h3> <h3>2026-05-14 · 自动抽帧增加本地精度模式</h3>

View File

@@ -178,7 +178,7 @@ export default function Home() {
if (!targetJob) return if (!targetJob) return
const frameTarget = frameTargets[jobId] ?? "balanced" const frameTarget = frameTargets[jobId] ?? "balanced"
const frameCount = frameCounts[jobId] ?? 5 const frameCount = frameCounts[jobId] ?? 5
const frameQuality = frameQualities[jobId] ?? "accurate" const frameQuality = frameQualities[jobId] ?? "ultra"
const mode = options?.mode ?? (targetJob.frames.length > 0 ? "append" : "replace") const mode = options?.mode ?? (targetJob.frames.length > 0 ? "append" : "replace")
setActiveJobId(jobId) setActiveJobId(jobId)
setAnalyzing(true) setAnalyzing(true)

View File

@@ -10,7 +10,7 @@ import { type NodeProps, useReactFlow } from "@xyflow/react"
import { import {
Link2, Upload, Download, Scissors, Image as ImageIcon, Link2, Upload, Download, Scissors, Image as ImageIcon,
Mic, Languages, FileEdit, Film, FileVideo, Loader2, Plus, X, LayoutGrid, Maximize2, Mic, Languages, FileEdit, Film, FileVideo, Loader2, Plus, X, LayoutGrid, Maximize2,
Copy, Trash2, Move, PanelLeft, PanelRight, PanelBottom, ChevronLeft, ChevronRight, Copy, Trash2, Move, PanelLeft, PanelRight, PanelBottom, ChevronLeft, ChevronRight, SlidersHorizontal,
} from "lucide-react" } from "lucide-react"
import { toast } from "sonner" import { toast } from "sonner"
import { NodeShell, type NodeStatus, type NodeKind } from "./node-shell" import { NodeShell, type NodeStatus, type NodeKind } from "./node-shell"
@@ -124,7 +124,7 @@ function clamp(value: number, min: number, max: number) {
return Math.max(min, Math.min(max, value)) return Math.max(min, Math.min(max, value))
} }
const THUMBNAIL_HEIGHT = 176 const THUMBNAIL_HEIGHT = 192
const FLOATING_PANEL_EDGE_INSET = 8 const FLOATING_PANEL_EDGE_INSET = 8
const FRAME_TARGET_OPTIONS: Array<{ value: FrameExtractTarget; label: string; hint: string }> = [ const FRAME_TARGET_OPTIONS: Array<{ value: FrameExtractTarget; label: string; hint: string }> = [
{ value: "balanced", label: "综合关键帧", hint: "清晰、去重、变化、时间覆盖" }, { value: "balanced", label: "综合关键帧", hint: "清晰、去重、变化、时间覆盖" },
@@ -439,6 +439,7 @@ function FrameExtractQuickBar({
}) { }) {
const option = FRAME_TARGET_OPTIONS.find((item) => item.value === target) ?? FRAME_TARGET_OPTIONS[0] 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[1]
const [settingsOpen, setSettingsOpen] = useState(false)
return ( return (
<div <div
@@ -446,39 +447,16 @@ function FrameExtractQuickBar({
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()}
> >
<select <div className="flex gap-1">
value={target}
disabled={disabled}
onChange={(e) => onTargetChange(e.target.value as FrameExtractTarget)}
className="h-7 w-full cursor-pointer rounded-md border border-white/12 bg-white/[0.08] px-1.5 text-[10px] font-semibold text-white outline-none transition focus:ring-2 focus:ring-violet-300/70 disabled:cursor-not-allowed disabled:opacity-45"
aria-label="选择自动抽帧目标"
title={option.hint}
>
{FRAME_TARGET_OPTIONS.map((item) => (
<option key={item.value} value={item.value}>{item.label}</option>
))}
</select>
<div className="mt-1 flex gap-1">
<select <select
value={count} value={target}
disabled={disabled} disabled={disabled}
onChange={(e) => onCountChange(Number(e.target.value))} onChange={(e) => onTargetChange(e.target.value as FrameExtractTarget)}
className="h-7 min-w-0 flex-1 cursor-pointer rounded-md border border-white/12 bg-white/[0.08] px-1.5 text-[10px] font-bold text-white outline-none transition focus:ring-2 focus:ring-violet-300/70 disabled:cursor-not-allowed disabled:opacity-45" className="h-8 min-w-0 flex-1 cursor-pointer rounded-md border border-white/12 bg-white/[0.08] px-2 text-[10.5px] font-semibold text-white outline-none transition focus:ring-2 focus:ring-violet-300/70 disabled:cursor-not-allowed disabled:opacity-45"
aria-label="选择抽帧张数" aria-label="选择自动抽帧目标"
title={option.hint}
> >
{FRAME_COUNT_OPTIONS.map((item) => ( {FRAME_TARGET_OPTIONS.map((item) => (
<option key={item} value={item}>{item} </option>
))}
</select>
<select
value={quality}
disabled={disabled}
onChange={(e) => onQualityChange(e.target.value as FrameExtractQuality)}
title={qualityOption.hint}
className="h-7 min-w-0 flex-1 cursor-pointer rounded-md border border-white/12 bg-white/[0.08] px-1.5 text-[10px] font-bold text-white outline-none transition focus:ring-2 focus:ring-violet-300/70 disabled:cursor-not-allowed disabled:opacity-45"
aria-label="选择抽帧精度"
>
{FRAME_QUALITY_OPTIONS.map((item) => (
<option key={item.value} value={item.value}>{item.label}</option> <option key={item.value} value={item.value}>{item.label}</option>
))} ))}
</select> </select>
@@ -487,12 +465,50 @@ function FrameExtractQuickBar({
disabled={disabled} disabled={disabled}
onClick={onAnalyze} onClick={onAnalyze}
title={hasFrames ? "追加自动抽帧" : "自动抽帧"} title={hasFrames ? "追加自动抽帧" : "自动抽帧"}
className="inline-flex h-7 shrink-0 items-center justify-center gap-1 rounded-md bg-gradient-to-r from-indigo-500 to-violet-500 px-2 text-[10.5px] font-bold text-white shadow-lg shadow-violet-950/35 transition hover:opacity-95 disabled:cursor-not-allowed disabled:opacity-45" className="inline-flex h-8 shrink-0 items-center justify-center gap-1 rounded-md bg-gradient-to-r from-indigo-500 to-violet-500 px-2.5 text-[10.5px] font-bold text-white shadow-lg shadow-violet-950/35 transition hover:opacity-95 disabled:cursor-not-allowed disabled:opacity-45"
> >
{running ? <Loader2 className="h-3 w-3 animate-spin" /> : <Scissors className="h-3 w-3" />} {running ? <Loader2 className="h-3 w-3 animate-spin" /> : <Scissors className="h-3 w-3" />}
{running ? "抽取" : hasFrames ? "追加" : "抽帧"} {running ? "抽取" : hasFrames ? "追加" : "抽帧"}
</button> </button>
<button
type="button"
disabled={disabled}
onClick={() => setSettingsOpen((open) => !open)}
title={`设置 · ${count} 张 · ${qualityOption.label}`}
className={`inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md border border-white/12 transition disabled:cursor-not-allowed disabled:opacity-45 ${
settingsOpen ? "bg-white text-violet-700" : "bg-white/[0.06] text-white/75 hover:bg-white/[0.12] hover:text-white"
}`}
>
<SlidersHorizontal className="h-3.5 w-3.5" />
</button>
</div> </div>
{settingsOpen && (
<div className="mt-1.5 flex gap-1">
<select
value={count}
disabled={disabled}
onChange={(e) => onCountChange(Number(e.target.value))}
className="h-7 min-w-0 flex-1 cursor-pointer rounded-md border border-white/12 bg-white/[0.08] px-1.5 text-[10px] font-bold text-white outline-none transition focus:ring-2 focus:ring-violet-300/70 disabled:cursor-not-allowed disabled:opacity-45"
aria-label="选择抽帧张数"
>
{FRAME_COUNT_OPTIONS.map((item) => (
<option key={item} value={item}>{item} </option>
))}
</select>
<select
value={quality}
disabled={disabled}
onChange={(e) => onQualityChange(e.target.value as FrameExtractQuality)}
title={qualityOption.hint}
className="h-7 min-w-0 flex-1 cursor-pointer rounded-md border border-white/12 bg-white/[0.08] px-1.5 text-[10px] font-bold text-white outline-none transition focus:ring-2 focus:ring-violet-300/70 disabled:cursor-not-allowed disabled:opacity-45"
aria-label="选择抽帧精度"
>
{FRAME_QUALITY_OPTIONS.map((item) => (
<option key={item.value} value={item.value}>{item.label}</option>
))}
</select>
</div>
)}
</div> </div>
) )
} }
@@ -553,7 +569,7 @@ export function InputNode({ data, selected }: NodeProps<{ data: NodeData }> | an
const toolWidth = Math.max(148, thumbNaturalWidth) const toolWidth = Math.max(148, thumbNaturalWidth)
const target = d.frameTargets[j.id] ?? "balanced" const target = d.frameTargets[j.id] ?? "balanced"
const count = d.frameCounts[j.id] ?? 5 const count = d.frameCounts[j.id] ?? 5
const quality = d.frameQualities[j.id] ?? "accurate" const quality = d.frameQualities[j.id] ?? "ultra"
const jHasFrames = j.frames.length > 0 const jHasFrames = j.frames.length > 0
const jRunning = ["splitting", "transcribing"].includes(j.status) const jRunning = ["splitting", "transcribing"].includes(j.status)
return ( return (
@@ -576,7 +592,7 @@ export function InputNode({ data, selected }: NodeProps<{ data: NodeData }> | an
onAnalyze={() => d.onAnalyzeJob(j.id, { mode: jHasFrames ? "append" : "replace" })} onAnalyze={() => d.onAnalyzeJob(j.id, { mode: jHasFrames ? "append" : "replace" })}
/> />
) : ( ) : (
<div className="h-[72px] rounded-lg border border-white/10 bg-white/[0.03]" /> <div className="h-[44px] rounded-lg border border-white/10 bg-white/[0.03]" />
)} )}
<div <div
className={`relative self-center rounded-md overflow-visible border shadow-lg transition hover:-translate-y-0.5 ${ className={`relative self-center rounded-md overflow-visible border shadow-lg transition hover:-translate-y-0.5 ${