diff --git a/.memory/worklog.json b/.memory/worklog.json index 6857c51..f3c50d0 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -3160,6 +3160,19 @@ "type": "session-heartbeat", "message": "Codex 会话活跃 · 最近命令:codex · 3 项未提交变更 · 最近提交:auto-save 2026-05-14 04:04 (~6)", "files_changed": 3 + }, + { + "ts": "2026-05-14T04:10:26+08:00", + "type": "commit", + "message": "auto-save 2026-05-14 04:10 (~4)", + "hash": "0448d28", + "files_changed": 4 + }, + { + "ts": "2026-05-13T20:13:12Z", + "type": "session-heartbeat", + "message": "Claude 会话活跃 · 最近命令:claude · 3 项未提交变更 · 最近提交:auto-save 2026-05-14 04:10 (~4)", + "files_changed": 3 } ] } diff --git a/api/main.py b/api/main.py index 1d81476..3173cfa 100644 --- a/api/main.py +++ b/api/main.py @@ -90,6 +90,7 @@ JobStatus = Literal[ KEYFRAME_COUNT = int(os.getenv("KEYFRAME_COUNT", "5")) FrameExtractTarget = Literal["balanced", "subject", "transition", "expression", "motion"] FrameExtractMode = Literal["replace", "append"] +FrameExtractQuality = Literal["fast", "accurate", "ultra"] FRAME_TARGET_LABELS: dict[FrameExtractTarget, str] = { "balanced": "综合关键帧", "subject": "清晰主体", @@ -97,6 +98,11 @@ FRAME_TARGET_LABELS: dict[FrameExtractTarget, str] = { "expression": "表情瞬间", "motion": "动作峰值", } +FRAME_QUALITY_LABELS: dict[FrameExtractQuality, str] = { + "fast": "快速", + "accurate": "精细", + "ultra": "极准", +} class GeneratedImage(BaseModel): @@ -399,20 +405,23 @@ def _sharpness_from_gray(g: np.ndarray) -> float: return float(lap.var()) -def _frame_metrics(img_path: Path, idx: int, timestamp: float) -> dict | None: +def _frame_metrics(img_path: Path, idx: int, timestamp: float, metric_width: int = 160) -> dict | None: """低清候选帧的本地评分特征。只用于排序,最终仍从原视频抽原尺寸帧。""" try: with Image.open(img_path) as raw: img = raw.convert("RGB") h = imagehash.phash(img) - small = img.resize((160, 90)) + src_w, src_h = img.size + metric_height = max(1, round(metric_width * src_h / max(src_w, 1))) + small = img.resize((metric_width, metric_height)) except Exception: return None arr = np.asarray(small, dtype=np.float32) # Rec. 601 luma,保留 0-255 范围,便于和清晰度 / 对比度阈值一起看。 gray = (0.299 * arr[:, :, 0] + 0.587 * arr[:, :, 1] + 0.114 * arr[:, :, 2]).astype(np.float32) - center = gray[22:68, 40:120] + gh, gw = gray.shape + center = gray[gh // 4:max(gh // 4 + 1, gh * 3 // 4), gw // 4:max(gw // 4 + 1, gw * 3 // 4)] rg = arr[:, :, 0] - arr[:, :, 1] yb = 0.5 * (arr[:, :, 0] + arr[:, :, 1]) - arr[:, :, 2] colorfulness = float(np.sqrt(rg.var() + yb.var()) + 0.3 * np.sqrt(rg.mean() ** 2 + yb.mean() ** 2)) @@ -432,6 +441,20 @@ def _frame_metrics(img_path: Path, idx: int, timestamp: float) -> dict | None: } +def _scan_profile(duration: float, quality: FrameExtractQuality) -> tuple[float, int, int, int]: + """返回 scan_fps / scan_width / metric_width / estimated_count。""" + if quality == "ultra": + base_fps, scan_width, cap, metric_width = 12.0, 960, 1800, 320 + elif quality == "accurate": + base_fps, scan_width, cap, metric_width = 8.0, 720, 900, 240 + else: + base_fps, scan_width, cap, metric_width = 2.0, 360, 240, 160 + + estimated = max(1, min(int(duration * base_fps), cap)) + scan_fps = max(0.02, min(base_fps, estimated / max(duration, 0.1))) + return scan_fps, scan_width, metric_width, estimated + + def _attach_temporal_metrics(items: list[dict]) -> None: """相邻低清帧差异:转场 / 动作目标依赖它,不需要逐帧高分辨率扫描。""" for i, it in enumerate(items): diff --git a/docs/source-analysis.html b/docs/source-analysis.html index 9e47a2e..c9e0469 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -723,7 +723,7 @@ api/main.py 输入 Input - 创建/上传任务,显示视频就绪;视频缩略图上方提供自动抽帧快捷工具条,可快速选目标、张数并多次追加;单击视频缩略图打开画布内抽帧面板。 + 创建/上传任务,显示视频就绪;每个视频缩略图上方都有绑定自己的自动抽帧快捷工具条,可快速选目标、张数并多次追加;单击视频缩略图打开画布内抽帧面板。 不要自动一路跑到 ASR 或生图;用户需要控制解析节奏。 page.tsxInputNodeVideoFramePanelNodeapi/main.py @@ -817,6 +817,18 @@ api/main.py

变更记录

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

+
+
+

2026-05-14 · 每个输入视频缩略图绑定自己的抽帧工具条

+ Input + UX +
+
+

问题:统一放在缩略图浮条上方的抽帧工具条仍然不够明确,用户无法一眼判断当前会抽哪一个视频。

+

改动:抽帧目标、张数和抽帧按钮改为渲染在每个视频缩略图正上方,并且每个 job 独立保存目标和张数设置。点击某个缩略图上方的抽帧按钮时,前端直接把该 jobId 传给 analyzeJob,同时切换 active job 并进入该 job 的进度轮询。

+

影响:web/app/page.tsxweb/components/nodes/index.tsxdocs/source-analysis.html。后续与输入视频相关的快捷操作都应优先贴近对应缩略图,不再依赖全局当前选择的心智。

+
+

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

diff --git a/web/components/nodes/index.tsx b/web/components/nodes/index.tsx index 0def9a0..e3c9d54 100644 --- a/web/components/nodes/index.tsx +++ b/web/components/nodes/index.tsx @@ -507,7 +507,7 @@ export function InputNode({ data, selected }: NodeProps<{ data: NodeData }> | an return (
- {/* 多视频缩略图浮条 — 「+」在最左,job 按时间倒序(最新靠左高亮),统一高度 64,宽度按视频原比例,一行横滚。 + {/* 多视频缩略图浮条 — 「+」在最左,job 按时间倒序(最新靠左高亮),每个视频上方绑定独立抽帧快捷条。 浮条宽度 = 节点宽度(节点拖宽后浮条同步变宽,可见更多缩略图,少滚动)。 */} {d.jobs.length > 0 && (