先说它解决问题的思路

让模型看视频然后报时间,是最不可靠的一类输出:「这个镜头大概 3 秒」和「2.97 秒」差的不是精度,是这份镜头表还能不能拿去对齐剪辑。video-shots 因此把分工划成死线——切点、时长、帧率、每镜的实测运动量全部由代码量出来,模型只判断五件事:景别、类别、运镜、画面、节奏;判断完再由 15 道代码质量门逐条对账。

前置条件

  • node ≥ 18(脚本只用标准库,无 npm 依赖);
  • ffmpeg 与 ffprobe(场景检测、运动测量、抽帧、拼联系表都靠它);
  • 一个能装 skill 的编码智能体(仓库官方验证过 Claude Code 与 Codex);
  • 不需要任何 API key,用的是你当前会话的额度。

第一步:装技能

git clone https://github.com/eternityspring/reelbench-skills.git
cd reelbench-skills
./scripts/install.sh              # 软链到已装的 agent(claude / codex 自动识别)
./scripts/install.sh --codex      # 也可以只装到某一个

它做的是软链,所以 git pull 之后立刻生效;不想软链就直接把 skills/video-shots 拷进 ~/.codex/skills/ 或 ~/.claude/skills/,技能是自包含的。

第二步:定切点(这一步定死,不要将就)

cd <输出目录>
node <skill目录>/scripts/video-shots.mjs seed <video> --track track.json --title "<片名>" > shots.json
  • stderr 会报片长、帧率、分辨率、检测到几个切点、合并后几个镜头——先看这一行再往下走;
  • 平均镜长十几秒、镜头数明显偏少 → 阈值高了,加 --threshold 0.15 重跑(暗戏、慢片、同机位对话多的片子都要往下调);
  • 镜头数比肉眼多出一截 → 阈值低了,往上调到 0.4,或者留到第四步用 recut --merge 并刀;
  • 一条 3 分钟的片子几秒钟就跑完,多跑两遍比将就一份烂底稿划算。

第三步:抽关键帧与联系表,先看大图

node <skill目录>/scripts/video-shots.mjs frames shots.json --video <video>
node <skill目录>/scripts/video-shots.mjs sheet shots.json --cols 4 --rows 6
node <skill目录>/scripts/video-shots.mjs sheet shots.json --cols 4 --rows 6 --pick b

每镜抽两张:S01a.jpg 取进入 15% 处的起手帧,S01b.jpg 取 85% 处的收尾帧。同一格前后对照,取景变了就是推拉摇移、取景没变只有人动了就是固定机位——这就是拼两张联系表的意义:一张表一屏二十几个镜头,只在判不准的那几个镜头上回去看单帧,不必一张张翻完整部片。

第四步:按批填五个字段

  1. 一批不超过 25 个镜头(正好一张联系表),按「景别 → 类别 → 运镜 → 画面 → 节奏」的顺序填;
  2. 运镜看 a/b 取景差加实测运动值,两者打架时信实测;
  3. 顺带记 subjects / onscreenText / audio:画面上烧录的对白字幕算台词,进 audio 并带上说话人;
  4. 节奏是可选字段——不做就一镜都别标,标了就得整片标全,门会拦半张表。

第五步:漏刀多刀用 recut 修,别手改

node <skill目录>/scripts/video-shots.mjs recut shots.json --track track.json \
  --split 63.5 --split 127.37 --merge 45.97 > shots.new.json && mv shots.new.json shots.json

叠化、暗场对暗场容易漏刀,手持晃动和闪光容易多刀,这是场景检测的两个必然失效点。recut 会自动重编号、重算时长与实测运动,补的刀记进 manualCuts;被拆被并的镜头标注会被清空并要求重新看画面,不许把旧描述顺下去。手改 start/end 挪刀过不了门——自己加的刀必须声明。

第六步:校验不能跳

node <skill目录>/scripts/video-shots.mjs validate shots.json --track track.json --frames frames

15 道门全是代码检查:时间轴连续、时长自洽、镜号连号、景别/类别/运镜三张词表、画面描述可核对(中文 ≥12 字并挡空话词与「这个镜头…」开头)、画面描述不重复、主体对账、类别要有证据(对话必须有台词、字卡必须有画面文字、空镜里不许有人)、运镜实测对账、边界必须来自检测、关键帧齐全、节奏分析可核对。最硬的一道是运镜对账:声称大幅运镜而实测帧间变化接近 0,直接拦——这是模型拉片最常见的幻觉。有违规逐条改,改完重跑。

第七步:出报告

node <skill目录>/scripts/video-shots.mjs render shots.json --md --track track.json > shots.md
node <skill目录>/scripts/video-shots.mjs render shots.json --html --track track.json \
  --video <原片相对报告的路径> > shots-report.html

HTML 报告是单文件交互页:内嵌播放器播放时同步高亮镜头、点镜头跳转,镜头节奏带按片宽显示时长占比,镜头表支持列表/卡片两种视图、可搜索可筛选可排序,首尾关键帧并排。样式与交互来自 skill 目录下的 report.css 与 report.js,render 时整段内联——想改报告长相改这两份即可;这三个文件要一起拷走,否则报告样式会缺。

踩坑清单

  • 先抽帧再 render:render 会去 frames/ 找关键帧,缺图它会明说缺、不摆占位图;
  • 「提示(不拦)」不等于错误:固定机位实测偏高多半是主体在动,需要人看一眼那一镜;
  • 跳过不是通过:没给 --track、没建 cast、关键帧目录不存在时,对应的门会明说跳过,汇报时要讲清楚;
  • 它不转写语音:没有 ASR,台词只来自画面上烧录的字幕,没有字幕的片子 audio 大面积留空是正常的;
  • 英文片加 --lang en,门的名字与违规信息都会用英文说。
  • 产出结构:shots.json 是主数据,track.json 是运动曲线(机器证据,别手改),另有 shots.md、shots-report.html、frames/、sheets/。
把自己的判断做成可对账的字段,比让模型「更认真一点」可靠得多。