Communitygithub.com

eternityspring/reelbench-skills

把一条视频重建成「只有画面和声音」的干净文件——**源片的元数据一概不搬**: GPS、设备型号、账号 ID、创建时间、章节、GoPro 的遥测轨,全部留在原地。 走的是白名单而不是黑名单:不列要删什么,只说带什么过去(画面一条流、声音一条流), 隐私保证来自结构,不来自枚举。 难点不在容器 tag,在**看不见的那几层**:x264 把完整编码参数写成 SEI 塞在码流里、 AAC 把版本号写进 DSE、avc1 的 compressorname 里还有一份——`ffprobe` 一个都看不见。 五个藏身处逐一堵死,每一处都有实测。 默认 `--mode copy`:**画面逐字节照搬**(cmp 验证过),只重编音频,53 秒的片子 1.3 秒跑完; `--mode encode` 完整重编码,多杀掉码流域的东西。 验收不靠声称:把源片所有元数据字符串当「针」,在输出文件里做**字节级扫描**, 扎到一根就红。12 道门全部由脚本确定性检查。 零依赖、零 API key,只要 node 和 ffmpeg。 Use when asked to 清元数据、去元数据、抹掉视频信息、视频隐私、去水印信息、 strip video metadata、remove exif from video、scrub video。

reelbench-skills 是什么?

reelbench-skills is a Claude Code agent skill that 把一条视频重建成「只有画面和声音」的干净文件——**源片的元数据一概不搬**: GPS、设备型号、账号 ID、创建时间、章节、GoPro 的遥测轨,全部留在原地。 走的是白名单而不是黑名单:不列要删什么,只说带什么过去(画面一条流、声音一条流), 隐私保证来自结构,不来自枚举。 难点不在容器 tag,在**看不见的那几层**:x264 把完整编码参数写成 SEI 塞在码流里、 AAC 把版本号写进 DSE、avc1 的 compressorname 里还有一份——`ffprobe` 一个都看不见。 五个藏身处逐一堵死,每一处都有实测。 默认 `--mode copy`:**画面逐字节照搬**(cmp 验证过),只重编音频,53 秒的片子 1.3 秒跑完; `--mode encode` 完整重编码,多杀掉码流域的东西。 验收不靠声称:把源片所有元数据字符串当「针」,在输出文件里做**字节级扫描**, 扎到一根就红。12 道门全部由脚本确定性检查。 零依赖、零 API key,只要 node 和 ffmpeg。 Use when asked to 清元数据、去元数据、抹掉视频信息、视频隐私、去水印信息、 strip video metadata、remove exif from video、scrub video。.

兼容平台✓Claude Code✓Codex CLI~Cursor
npx skills add https://github.com/eternityspring/reelbench-skills/tree/HEAD/skills/video-scrub

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

video-scrub

Rebuild a video into a clean file: video and audio carried over, not a single byte of metadata.

{baseDir} = the directory containing this file. Script at {baseDir}/scripts/video-scrub.mjs, zero dependencies, run directly with node.

Principle: whitelist, not blacklist

There are two approaches to scrubbing metadata—live or die:

ApproachProblem
BlacklistList fields to delete and remove them one by one (like exiftool)You can't delete what you don't know about—vendor-private udta, GoPro telemetry tracks, SEI in the bitstream; miss one and you've leaked it
WhitelistDon't say what to remove, say what to carry over: one video stream, one audio stream, nothing else—

With the whitelist approach, the source's metadata has no channel to enter. It's not "deleted"—it was never carried over.

But ffmpeg will betray this structure twice by default, and you must stop it:

  1. It copies the source container tags to the output by default → -map_metadata -1
  2. It auto-selects all streams by default, which will carry GoPro's gpmd telemetry track along → -map 0:v:0 -map 0:a:0?

Item 2 is the only defense for GoPro / DJI sources: their GPS isn't in a tag, it's in a separate timed metadata track. Clearing tags won't remove it; only "select exactly two streams" stops it.

The real challenge: invisible layers

With those two stopped, the tag layer is clean and ffprobe is silent. But three identity strings still live inside the file:

Hidden inContentVisible to ffprobe
H.264 SEIx264 full parameter string cabac=1 ref=3 … crf=23.0✗
AAC bitstream DSELavc62.28.102✗
avc1 compressornameLavc libx264✗

Five hiding places and their countermeasures are detailed at {baseDir}/references/residue-map.md—each one has empirical test records, including several traps that look like they should work but don't (-flags:v +bitexact and -x264-params info=0 both fail to block SEI).

Two modes

ModeVideoAudioSpeedExtra removals
copy (default)Byte-for-byte copyRe-encode53-second clip in 1.3s—
encodeFull re-encodeRe-encodeMuch slowerBitstream watermarks, fragile steganography

Audio is re-encoded in both modes—AAC version number is written in DSE inside the bitstream, -c:a copy can't remove it. Re-encoding once is cheap and buys a clean audio track. Video in copy mode truly doesn't move a single bit (verified with cmp, except for the stripped SEI).

About watermarks: encode removes more than copy—bitstream watermarks and fragile steganography—but these are rare in real footage. The truly dangerous robust pixel watermarks (SynthID, forensic watermarks) are designed to survive re-encoding, so neither mode can remove them. Choose mode by quality and speed, don't rely on re-encoding to wash out watermarks.

Four profiles

The layer written back after scrubbing:

ProfileWrites
obs (default)Looks like OBS Studio's output. OBS wraps with ffmpeg anyway, so this isn't forgery—it keeps the normal appearance of similar tools
quicktimemacOS QuickTime screen recording: Core Media Video / Core Media Audio
screencaptureScreenCaptureKit screen recording
bareWrites nothing. Highest privacy strength—leaves no claims that could be disproven

One thing that can't be done, mentioned here so you don't try: the container encoder field is owned by the muxer, -metadata encoder=Lavf60.16.100 cannot override it, and setting it to empty doesn't clear it (tested). Only -fflags +bitexact can make it disappear entirely. So the obs profile doesn't fake a version number—it lets ffmpeg write its real version.


Step 0 — Inspect the source first

node {baseDir}/scripts/video-scrub.mjs inspect <video>

Shows everything in one screen: container tags, per-stream tags and side data, chapters, and the identity strings inside the bitstream that ffprobe can't see. Tells you how many needles to scan for during verification, and whether SEI surgery will happen.

Always run inspect before scrubbing. Pay special attention to:

  • Whether there are data / timecode streams—if so, the source may carry telemetry (GoPro, DJI)
  • Whether it's HDR—if so, SEI is never removed (explained below)

Step 1 — Scrub

node {baseDir}/scripts/video-scrub.mjs scrub <video> -o out.mp4

Defaults to --mode copy --profile obs. Common options:

--mode encode          # Full re-encode
--profile bare         # Writes nothing, highest privacy
--crf 18               # Only meaningful in encode mode
--date 2026-01-01T00:00:00Z   # Specify creation time; defaults to now if omitted

Step 2 — Verify ⛔ do not skip

node {baseDir}/scripts/video-scrub.mjs verify <source> <output>

Or all-in-one (use this daily):

node {baseDir}/scripts/video-scrub.mjs run <video> -o out.mp4

The core of verification is this: all previous steps are claims—"I set the right parameters"; this step is proof—"I scanned, it's gone."

The approach: extract every metadata string from the source as a "needle"—tag keys and values, handler names, chapter titles—then perform a byte-level scan of the output file. If any needle hits, it's flagged red.

Two implementation details:

  • Needles must be ≥5 bytes. Shorter strings will randomly hit false positives inside compressed data; skipped needles are listed in the output.
  • Generic strings are not needles. VideoHandler is everywhere—zero information; Lavf58.45.100 pins down a specific version—it's a fingerprint. The dividing line: "Can this string narrow down to a specific person, device, or export session?"

The 12 gates

GateBlocks
Byte-level residueSource metadata string hits the output
Tag layer residueOutput tag value came from source
Location infoAny GPS / location field
Device & software identifiersmake / model / software / artist / comment, etc.
Source timestampOutput creation_time equals source's
Rotation matrixEncode mode: rotation not baked into video (copy mode skips—removing it would distort the video)
Stream compositiondata / timecode / attachment streams mixed in
ChaptersChapters carried over
Bitstream identity stringsSEI / DSE / compressorname still contain encoder names
Profile conformanceRemaining tags don't match declared profile
DurationOutput secretly truncated
DecodabilityProduced a broken file that can't be opened

HDR and embedded subtitles: when SEI surgery is forbidden

ffmpeg only offers NAL-level granularity—filter_units removes by NAL type, and h264_metadata has no "remove only this SEI payload" option. So remove_types=6 means all SEI removed at once. But SEI contains more than x264's junk:

  • HDR static metadata (mastering display / content light level)—removing it breaks color, tone mapping collapses
  • CEA-608/708 embedded subtitles—removing it deletes subtitles

So the script's policy: only performs surgery when identity strings are actually detected; never touches HDR sources (it clearly states why it didn't). This decision is made by seiPlan(), you don't need to worry about it, but you should know it's there.

Boundaries (what we don't do)

No watermark removal (robust pixel watermarks survive re-encoding, don't count on it), no video content manipulation (no crop, scale, or rotation), no fake device models or GPS (profiles only write tool-class info, not fabricated capture devices or locations), no batch directory scanning (one video at a time), no containers other than mp4/mov.

Self-test

node {baseDir}/scripts/selftest.mjs

117 assertions, no ffmpeg required, no real files required. Each of the 12 gates has a break-case test—proof that the gate actually blocks. Run this after modifying any script.

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

eternityspring/reelbench-skills

拉片:把一条成片拆成逐镜头的分析表——每个镜头的时长、景别、类别、运镜、画面。 分工刻在骨子里:**能量的都由代码量**(切点来自 ffmpeg 场景检测,时长是切点相减, 运动量是逐帧差分的中位数),模型只判断它真正该判断的那几件事(景别 / 类别 / 运镜 / 画面 / 节奏), 然后每一条判断都被代码当场对账——**声称推拉摇移却实测几乎不动,门直接拦**。 看片走联系表(每镜起手帧 + 收尾帧各拼一张大图,一屏二十几个镜头,a/b 对照就是运镜), 不是一张张翻。检测漏刀多刀用 recut 补刀并刀,自动重编号重算时长,手改边界过不了门。 产出 shots.json + Markdown 镜头表 + **单页交互式拉片报告**:内嵌播放器(播放时同步高亮镜头、 点镜头跳转)、镜头节奏带、可搜索可筛选可排序的镜头表(列表 / 卡片两种视图、首尾关键帧并排、 点图开大图)、景别类别运镜分布、出场人物、质量门、导出 JSON。单文件零依赖,离线双击能开。 15 道质量门全部由脚本确定性检查。 零依赖、零 API key,只要 node 和 ffmpeg。 Use when asked to 拉片、拆镜头、分析视频镜头、镜头时长、景别、运镜、镜头表、 video shot breakdown、shot list from video。

eternityspring/reelbench-skills

把拉片数据和原片合成一条**能直接看的视频**:一边是画面,一边是这一镜的分镜信息 (镜号、起止、时长、景别、类别、运镜、画面描述、台词),**镜头切了信息跟着切**, 镜头表自动滚动并高亮当前这一镜。 版式只看原片的宽高比:**横版 / 方版 → 画面在上、信息在下;竖版 → 画面在左、信息在右**, 画面永远原样缩放,不裁不拉。 镜头表**随播放滚动**:切点处滚一小段把当前镜头带到锚点、随后停住,高亮条同步滑过去。 实现上只截三张图(底板 + 铺开的长图 ×2),滚动与高亮由 ffmpeg 按时间裁窗, **布局全在 scripts/panel.css 里,改它就能改版式**,不用碰脚本。 吃 video-shots 产出的 shots.json(有 frames/ 就把关键帧当缩略图用)。 零 npm 依赖,用 ffmpeg 合成、无头浏览器渲面板。 Use when asked to 导出视频、合成视频、分镜视频、带分镜信息的视频、解说版视频、 video with shot info、annotated shot video。

相关技能