From 0448d28b99b17a14f27c1510c65a7e4d61192689 Mon Sep 17 00:00:00 2001 From: kang Date: Thu, 14 May 2026 04:10:26 +0800 Subject: [PATCH] auto-save 2026-05-14 04:10 (~4) --- .memory/worklog.json | 13 +++ docs/source-analysis.html | 14 ++- web/app/page.tsx | 49 +++++--- web/components/nodes/index.tsx | 200 +++++++++++++++++---------------- 4 files changed, 163 insertions(+), 113 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index e6c985a..6857c51 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -3147,6 +3147,19 @@ "type": "session-heartbeat", "message": "Claude 会话活跃 · 最近命令:claude · 4 项未提交变更 · 最近提交:auto-save 2026-05-14 03:59 (~3)", "files_changed": 4 + }, + { + "ts": "2026-05-14T04:04:54+08:00", + "type": "commit", + "message": "auto-save 2026-05-14 04:04 (~6)", + "hash": "87f1182", + "files_changed": 6 + }, + { + "ts": "2026-05-13T20:08:50Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 3 项未提交变更 · 最近提交:auto-save 2026-05-14 04:04 (~6)", + "files_changed": 3 } ] } diff --git a/docs/source-analysis.html b/docs/source-analysis.html index a7e28c5..9e47a2e 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -817,6 +817,18 @@ api/main.py

变更记录

这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。

+
+
+

2026-05-14 · 自动抽帧快捷工具条移到缩略图上方

+ Input + Frame Target +
+
+

问题:自动抽帧入口放在 Input 卡片正文里,离视频缩略图和预览工作区较远;用户需要在看缩略图时快速切目标、切张数并反复抽取。

+

改动:输入视频缩略图浮条上方新增自动抽帧快捷工具条,包含抽帧目标、张数快捷项和抽帧按钮。前端新增 frameCount 状态并把目标 / 张数传给 analyzeJob;已有关键帧时默认用 mode=append 追加抽取。

+

影响:api/main.pyweb/lib/api.tsweb/app/page.tsxweb/components/nodes/index.tsxdocs/source-analysis.html。后端追加模式会保留已有关键帧,避开非常接近的时间点,并用新的 frame index 落盘。

+
+

2026-05-14 · 自动抽帧支持目标化扫描

@@ -825,7 +837,7 @@ api/main.py

问题:单一“自动抽帧”无法表达这次要清晰人物、下次要转场变化或表情瞬间的不同目标;但把抽帧做成复杂参数面板会破坏 Input 卡片的轻量工作流。

-

改动:Input 节点新增一个抽帧目标下拉,默认“综合关键帧”,可切换清晰主体、转场变化、表情瞬间、动作峰值。后端 /jobs/{id}/analyze 新增 target 参数,先低清低帧率扫描候选,再按目标评分、pHash 去重、时序分桶,最后只对选中的时间点从原视频抽高质量关键帧。

+

改动:Input 节点新增抽帧目标,默认“综合关键帧”,可切换清晰主体、转场变化、表情瞬间、动作峰值。后端 /jobs/{id}/analyze 新增 target 参数,先低清低帧率扫描候选,再按目标评分、pHash 去重、时序分桶,最后只对选中的时间点从原视频抽高质量关键帧。

影响:api/main.pyweb/lib/api.tsweb/app/page.tsxweb/components/nodes/index.tsxdocs/source-analysis.html。当前“人物/动物表情”是本地近似评分,后续可把候选小图接入视觉模型重排。

diff --git a/web/app/page.tsx b/web/app/page.tsx index d21beab..73de92b 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -92,8 +92,8 @@ export default function Home() { const job = useMemo(() => jobs.find((j) => j.id === activeJobId) ?? null, [jobs, activeJobId]) const [submitting, setSubmitting] = useState(false) const [analyzing, setAnalyzing] = useState(false) - const [frameTarget, setFrameTarget] = useState("balanced") - const [frameCount, setFrameCount] = useState(5) + const [frameTargets, setFrameTargets] = useState>({}) + const [frameCounts, setFrameCounts] = useState>({}) const [selectedFrames, setSelectedFrames] = useState>(new Set()) const [expandedFrame, setExpandedFrame] = useState(null) const [framePanelScale, setFramePanelScale] = useState(1) @@ -167,27 +167,43 @@ export default function Home() { } }, [addJob]) - const handleAnalyze = useCallback(async (options?: { mode?: FrameExtractMode }) => { - if (!job) return - const mode = options?.mode ?? (job.frames.length > 0 ? "append" : "replace") + const handleAnalyzeJob = useCallback(async (jobId: string, options?: { mode?: FrameExtractMode }) => { + const targetJob = jobs.find((item) => item.id === jobId) + if (!targetJob) return + const frameTarget = frameTargets[jobId] ?? "balanced" + const frameCount = frameCounts[jobId] ?? 5 + const mode = options?.mode ?? (targetJob.frames.length > 0 ? "append" : "replace") + setActiveJobId(jobId) setAnalyzing(true) if (mode === "replace") setSelectedFrames(new Set()) try { - await analyzeJob(job.id, frameCount, frameTarget, mode) + await analyzeJob(jobId, frameCount, frameTarget, mode) toast.info(`${mode === "append" ? "追加抽帧" : "开始解析"}:${FRAME_TARGET_LABELS[frameTarget]} · ${frameCount} 张`) - // 乐观更新本地状态,让轮询 useEffect 重新启动 - setJob((prev) => prev ? { - ...prev, + setJobs((prev) => prev.map((item) => item.id === jobId ? { + ...item, status: "splitting", message: `${mode === "append" ? "追加抽帧中" : "拆轨中"} · ${FRAME_TARGET_LABELS[frameTarget]}…`, progress: 30, - } : prev) + } : item)) } catch (e) { toast.error("解析触发失败:" + (e instanceof Error ? e.message : String(e))) } finally { setAnalyzing(false) } - }, [job?.id, job?.frames.length, frameCount, frameTarget]) + }, [jobs, frameCounts, frameTargets]) + + const handleAnalyze = useCallback(async (options?: { mode?: FrameExtractMode }) => { + if (!job) return + await handleAnalyzeJob(job.id, options) + }, [job?.id, handleAnalyzeJob]) + + const handleFrameTargetChange = useCallback((jobId: string, target: FrameExtractTarget) => { + setFrameTargets((prev) => ({ ...prev, [jobId]: target })) + }, []) + + const handleFrameCountChange = useCallback((jobId: string, count: number) => { + setFrameCounts((prev) => ({ ...prev, [jobId]: Math.max(1, Math.min(20, count)) })) + }, []) const handleAddManualFrameForJob = useCallback(async (jobId: string, t: number) => { try { @@ -511,8 +527,8 @@ export default function Home() { activeJobId, submitting, analyzing, - frameTarget, - frameCount, + frameTargets, + frameCounts, selectedFrames, expandedFrame, framePanelScale, @@ -524,8 +540,9 @@ export default function Home() { onSubmitUrl: handleSubmit, onUploadFile: handleUpload, onAnalyze: handleAnalyze, - onFrameTargetChange: setFrameTarget, - onFrameCountChange: setFrameCount, + onAnalyzeJob: handleAnalyzeJob, + onFrameTargetChange: handleFrameTargetChange, + onFrameCountChange: handleFrameCountChange, onToggleFrame: handleToggleFrame, onExpandFrame: setExpandedFrame, onOpenFramePanel: handleOpenFramePanel, @@ -555,7 +572,7 @@ export default function Home() { onCopyImage: handleCopyImage, pinnedNodes, onToggleNodePin: handleToggleNodePin, - }), [job, jobs, activeJobId, submitting, analyzing, frameTarget, frameCount, selectedFrames, expandedFrame, framePanelScale, framePanelPinned, framePanelDock, videoPanelJobId, videoPanelScale, videoPanelDock, handleSubmit, handleUpload, handleAnalyze, handleToggleFrame, handleOpenFramePanel, handleFramePanelScaleChange, handleAddManualFrame, handleAddManualFrameForJob, handleOpenVideoPanel, handleVideoPanelScaleChange, handleSwitchJob, setJob, handleDeleteJob, handleDeleteFrame, handleDeleteFrameForJob, handleDeleteGenerated, handleDeleteVideo, handleDeleteCutout, handleCopyImage, pinnedNodes, handleToggleNodePin]) + }), [job, jobs, activeJobId, submitting, analyzing, frameTargets, frameCounts, selectedFrames, expandedFrame, framePanelScale, framePanelPinned, framePanelDock, videoPanelJobId, videoPanelScale, videoPanelDock, handleSubmit, handleUpload, handleAnalyze, handleAnalyzeJob, handleFrameTargetChange, handleFrameCountChange, handleToggleFrame, handleOpenFramePanel, handleFramePanelScaleChange, handleAddManualFrame, handleAddManualFrameForJob, handleOpenVideoPanel, handleVideoPanelScaleChange, handleSwitchJob, setJob, handleDeleteJob, handleDeleteFrame, handleDeleteFrameForJob, handleDeleteGenerated, handleDeleteVideo, handleDeleteCutout, handleCopyImage, pinnedNodes, handleToggleNodePin]) // 用 useNodesState 让 ReactFlow 自己管位置(避免轮询时重置 drag) const savedSizes = useMemo(() => loadNodeSizes(), []) diff --git a/web/components/nodes/index.tsx b/web/components/nodes/index.tsx index 14b32a7..0def9a0 100644 --- a/web/components/nodes/index.tsx +++ b/web/components/nodes/index.tsx @@ -29,8 +29,8 @@ export interface NodeData { activeJobId: string | null submitting: boolean analyzing: boolean - frameTarget: FrameExtractTarget - frameCount: number + frameTargets: Record + frameCounts: Record selectedFrames: Set expandedFrame: number | null framePanelScale?: number @@ -42,8 +42,9 @@ export interface NodeData { onSubmitUrl: (url: string) => void onUploadFile: (file: File) => void onAnalyze: (options?: { mode?: FrameExtractMode }) => void - onFrameTargetChange: (target: FrameExtractTarget) => void - onFrameCountChange: (count: number) => void + onAnalyzeJob: (jobId: string, options?: { mode?: FrameExtractMode }) => void + onFrameTargetChange: (jobId: string, target: FrameExtractTarget) => void + onFrameCountChange: (jobId: string, count: number) => void onToggleFrame: (idx: number) => void onExpandFrame: (idx: number) => void onOpenFramePanel?: (idx: number) => void // 打开/找回画布内关键帧详情面板 @@ -429,7 +430,7 @@ function FrameExtractQuickBar({ return (
e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()} > @@ -437,7 +438,7 @@ function FrameExtractQuickBar({ value={target} disabled={disabled} onChange={(e) => onTargetChange(e.target.value as FrameExtractTarget)} - className="h-8 min-w-0 flex-1 cursor-pointer rounded-md border border-white/12 bg-white/[0.08] px-2 text-[11px] font-semibold text-white outline-none transition focus:ring-2 focus:ring-violet-300/70 disabled:cursor-not-allowed disabled:opacity-45" + 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} > @@ -445,32 +446,29 @@ function FrameExtractQuickBar({ ))} -
- {FRAME_COUNT_OPTIONS.map((item) => ( - - ))} +
+ +
-
) } @@ -512,21 +510,7 @@ export function InputNode({ data, selected }: NodeProps<{ data: NodeData }> | an {/* 多视频缩略图浮条 — 「+」在最左,job 按时间倒序(最新靠左高亮),统一高度 64,宽度按视频原比例,一行横滚。 浮条宽度 = 节点宽度(节点拖宽后浮条同步变宽,可见更多缩略图,少滚动)。 */} {d.jobs.length > 0 && ( - d.onAnalyze({ mode: hasFrames ? "append" : "replace" })} - /> - ) : null} - > + {/* + 再上传一个(放在最前面) */} - {d.onDeleteJob && ( - )} + {d.onDeleteJob && ( + + )} +
) })}