auto-save 2026-05-14 04:26 (~5)
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 ${
|
||||||
|
|||||||
Reference in New Issue
Block a user