项目体检 · 只读分析 · 2026/06/07

DronePicker · 跑了一年的半成品,但每一块都在动。

DJI / Osmo Action 素材的自动剪辑流水线 — 设备插上即同步、AI 打分挑废片、按时间聚类成故事、Web 浏览、再吐成剪映草稿。 架构清晰、数据健全、跑得起来;问题主要在边角的工程化:实验脚本散在根目录、单文件 SPA 撑到 1016 行、若干 flag 字段漏写。

首提交 2026/05/05 4 commits ~3,955 LOC 单人 / 单机 / 自用 1.45 TB on disk
Chapter 01

这项目到底长啥样?

一句话 + 一条数据流 + 一组数字事实 + 模块清单

一句话:DJI 飞行记录仪的"按 Enter 就出片"工具。 后端是一坨 Python 脚本 (pipeline / stories / smart_picker / build_jianying_draft) 各管一截, 中间用 Flask (port 8902) 把 JSON 暴露出来, 前端是一个 Vite + React 19 的 SPA (port 5173) 把上千段视频塞进虚拟滚动网格。 剪映草稿 / FCPXML / 直接 ffmpeg 拼成片 — 三条出口都有,作者自己挑顺手的用。

数据流

Source
DJI 设备 / Osmo Action SD 卡插上 USB 即触发 launchd
launchd · com.leway.dji-sync · WatchPaths=/Volumes
Sync
~/bin/dji-sync.shrsync 到 /Volumes/Share/DCIM/DJI_001/ (MP4) + /preview/ (LRF)
Analyze
pipeline.py粗筛模糊/曝光 → 中点抽缩略图 → GPT-4o 打分/打标签/写描述
Highlight
smart_picker.py + run_highlights.py运动量 + 清晰度计算高光区间,写回 results.json
Group
stories.py30 分钟时间窗 + 标签相似度聚类 → 193 个故事 / 200 KB JSON
→ 三条出口同时存在 ↓
Output A
server.py + web/Flask 8902 + Vite 5173 — 浏览 / 播放 / 标记 selected
Output B
build_jianying_draft.py写入 ~/Movies/JianyingPro/ — 打开剪映直接看草稿
Output C
generate_fcpxml.py / render_final_ffmpeg.py导 FCPXML 或直接 ffmpeg 拼成片

数字事实

原片
2,480
Share 1988 + Windows 492
占盘
1.45TB
含 185 GB preview
拍摄日
73
2024/01 → 2026/06
AI 已分析
2,097
覆盖率 97%
故事
193
平均 10 段 / 故事
高光片段
47
未跑过的占大多数
已勾选
49
clip_status=selected
端口
2
8902 API · 5173 Web
launchd
1
dji-sync · WatchPaths

模块清单

pipeline.py Backend / Python · 271 LOC
主管线。粗筛模糊 → 抽中点缩略图 → 调 Azure GPT-4o 打分。断点续传按 filename 去重,所以重跑安全。argparse 有个老 bug:多次 --video-dir 只保留最后一个值。
stories.py Backend / Python · 169 LOC
30 分钟时间窗 + 标签 Jaccard 相似度聚类,再用 GPT-4o 起标题。当前 193 个故事,标题中文化质量肉眼可见地不错(例:"城市边缘·高速公路·车窗视角")。
smart_picker.py Backend / Python · 191 LOC
单视频 highlight detector,跑光流 + 拉普拉斯方差。被 run_highlights.py 批量调用,后者的日期区间硬编码在 main() 里,需要改源码。
server.py Backend / Flask · 292 LOC · port 8902
只读 API。8 条路由:/api/results /api/stories /api/stats /api/thumbnail/<f> /api/video/<f> /api/sync-log。CORS 全开,单机自用。
web/ Frontend / React 19 · 1,143 LOC
单页 SPA,主体在 App.tsx 一个 1016 行的大文件里。Tailwind v4 + shadcn/ui + lucide-react,@tanstack/react-virtual 撑住 2k+ 视频网格。force dark mode。
~/bin/dji-sync.sh Sync / bash + rsync
launchd 在 /Volumes 有任何变化时触发。扫描 /Volumes/*/DCIM/DJI_001,大小写不敏感去重,rsync 到 Share 盘。带文件锁防并发。
build_jianying_draft.py Exporter / 剪映
output/cuts/NN_*_highlight.mp4 按序拼成剪映草稿,加 BGM 和转场。依赖 ~/.openclaw/workspace/projects/CapCutAPI (pyJianYingDraft),路径硬编码。
generate_fcpxml.py Exporter / FCP
同上,但输出 Final Cut Pro 工程文件到 output/*.fcpxml。最近一次跑是 3 月。
实验脚本 (~14 个) Legacy · 累计 ~1,200 LOC
根目录散着 draw_text_advanced{,2,3}.py draw_text_srt.py render_video{,_stable,_list}.py test_*.py debug_*.py fix_*.py — 都是 3 月的字幕烧录 / 渲染迭代实验,作者已经不动了。
Chapter 02

有啥值得动一动的?

按价值排序 · 每条都是具体小改动 · 不空谈"换框架"
01
把 14 个实验脚本搬进 legacy/ 文件夹 根目录现在塞着 14 个 3 月写的 draw_text_*/render_video*/test_*/debug_*/fix_*,每次打开项目都得在 mtime 里大海捞针确认哪个是当前在用的。
立等可取。根目录从 ~40 个 .py 砍到 ~10 个,新会话进项目认知成本砍半。脚本本身留着不删,只是搬个家加个 README。
小 · 30 分钟
02
删 5 个 results.json.bak.* 备份 output/ 里躺着 5 个一模一样大小 (~1 MB) 的 .bak,加起来 5 MB,fix_meta / rollback 脚本留下的。git 又不进库,纯占地。
清掉就完事。需要历史快照让 Time Machine 管。顺便审一下 .log 文件:server.log 已经 344 KB,从 CLAUDE.md 提到的 6.7 MB 缩了不少但还在长。
小 · 5 分钟
03
pipeline.py--video-dir argparse bug CLAUDE.md 自己都标了:nargs='+' 写法下重复传 --video-dir 只保留最后一个。等于一个一直在咬人的脚趾绊。
换成 action='append' + 内部 flatten,5 行改动。以后多盘并行扫描不用拆三个命令、不用心算断点续传冲突。
小 · 10 分钟
04
gpt4o_analyzed=True 这一行赋值 results.json 里 2,097 条有 ai_score,但只有 9 条带 gpt4o_analyzed=true。/api/stats 显示 "已分析 9 条",前端 stats 完全看不出真实覆盖率。
pipeline.py 调完 GPT 后写一行 item['gpt4o_analyzed']=True。一次性补历史数据:for x in d: x['gpt4o_analyzed']=bool(x.get('ai_score'))。/api/stats 立刻好看,前端的 dashboard 才有意义。
小 · 15 分钟
05
run_highlights.py 的日期改成 CLI 参数 每次想给某一段日子补 highlight 就得改源码、保存、跑、记得改回来。CLAUDE.md 里都警告了"日期硬编码在 main() 里"。
--from / --to 两个 argparse 参数,默认值保留当前的硬编码区间。十几行改动,以后写 shell alias 就能跑。
小 · 20 分钟
06
video_path 改成"按 filename 反查",别再存绝对路径 现在 results.json 里 2,158 条全是 /Volumes/Share/DCIM/.../Volumes/Windows/preview/... 的绝对路径。盘换名 / 不挂 → /api/video/* 全 404。
server.py 新增 video resolver:scan 一次 Share + Windows 建个 filename→path 表,缓存 5 分钟(stats 已经有缓存模式)。pipeline 也少写一个字段。换盘 / 改 mount 点不再炸。
中 · 半天
07
App.tsx 的 1,016 行拆 3 个组件 单文件 SPA 已经撑不住:state、grid、filter bar、video player、stories drawer 全在一个 module 里。改任何一处都要重读全文。
VideoGrid.tsx / StoryDrawer.tsx / FilterBar.tsx 三个就够。state 仍然留在 App.tsx,通过 props 传 — 不引 zustand/redux,保持作者偏好的最小依赖。重构后单文件控制在 ~300 行。
中 · 1 天
08
给 server.py 加 1 条 POST /api/clip-status 写入接口 前端的"标记 selected"现在是个假按钮 — server.py 没有任何写入路由,刷新就丢。clip_status 字段空有其表。
10 行 Flask handler + filelock,把 selected/favorite 写回 results.json。Web UI 第一次变成"真编辑器"而不是只读浏览器。改完后剪映草稿可以直接按 selected 集合来挑。
中 · 半天
Chapter 03

功能正常吗?

六个核心环节 · 今天实际跑出来的证据 · 不靠感觉
设备自动同步launchd + dji-sync.sh
正常
launchd plist 存在 (~/Library/LaunchAgents/com.leway.dji-sync.plist),今天最新一次同步在 12:14 完成:MP4 +265, LRF +265, skip 0。日志 80 KB,SD 卡上的素材已落到 Share。
Pipeline AI 分析pipeline.py + GPT-4o
凑合
今天 16:21 跑过一轮 151 条,全部 200 OK。但 gpt4o_analyzed flag 历史漏写 — 2,097 条有 ai_score、只有 9 条带 flag,导致 /api/stats 显示"已分析 9 条"误报。实际覆盖率 97%,数据是好的,只是统计字段坏了
Stories 聚类stories.py
正常
stories.json 200 KB / 193 个故事,覆盖 1,961 段视频,平均 10 段/故事 (最小 2, 最大 136)。标题中文质量肉眼可见:"城市边缘·高速公路·车窗视角" "迷雾中的港城清晨"。avg_score 5.0 ~ 8.7 区间合理。
Web UIvite 5173 · App.tsx
正常
curl localhost:5173 返回 HTML、dark 模式 class 已加。后端 /api/results /api/stories /api/stats 三条每分钟被轮询一次 (server.log 里看得到),延迟稳定 200。
视频播放/api/video/<f>
正常
curl /api/video/DJI_20260131205739_0163_D.mp4 返回 200, 273 MB, video/mp4。ffprobe 抽 cuts 任意一段 duration=4.01 s,文件完整。前提是 /Volumes/Share 挂着
导剪映草稿build_jianying_draft.py
凑合
脚本本身能跑 — output/cuts/ 里有 32 个 highlight 切片,1 个 fcpxml 工程,目录结构完整。但上一次成功 build 是 3 月 (generate.log 最后一行 3/23),依赖 ~/.openclaw/workspace/projects/CapCutAPI 路径硬编码,换机器没有 symlink 会直接 import 失败。
分析备注 · 拍板说明