18 Commits
Author SHA1 Message Date
eternityspring 1b51af897b Add video-scrub demo output; stop tracking demo2.mp4
demo-scrub/ is 28K of text and no video, because video-scrub's output looks
exactly like its source -- that is the design goal. What is worth reading is
the reconciliation, all of it generated by real runs, none hand-written:

- before.txt        a plain film's checkup
- dirty-before.txt  the same file with GPS, an email address, a platform video
                    id, a device name, a chapter and a data track injected
- dirty-after.txt   cleaned: 14 needles all miss, 12 gates green
- naive.txt         counter-example: a naive -c copy, 7 gates red, with byte
                    offsets
- half.txt          the one worth reading

half.txt runs the textbook recipe -- -map_metadata -1, map only the two
streams, drop chapters. ffprobe comes back spotless: no GPS, no email, no
account id, no chapters, 11 gates green. The twelfth reports x264's encoder
settings SEI in 4 places and the AAC DSE in 8. The file puts ffprobe's clean
output and the gate's verdict side by side, which makes the gap between
"looks clean" and "is clean" obvious at a glance.

demo2.mp4 is now gitignored. Nothing references it: the self-test is pure
functions that touch neither ffmpeg nor a real file, so it stays green without
it. Tracking a 12MB binary would also showcase nothing, since a scrubbed file
is visually identical to its source. demo-scrub/README.md carries commands
that reproduce the whole demo from any video.

Both root READMEs get the skill table row and an examples paragraph; both
video-scrub READMEs link to demo-scrub/.
2026-09-21 16:12:42 +08:00
eternityspring 941f42a143 Add video-scrub: rebuild a video carrying only picture and sound
Allowlist, not blocklist: never enumerate what to delete, only declare what
comes across (one video stream, one audio stream). The source's metadata has
no channel into the output. The privacy guarantee comes from the structure,
not from enumeration.

The tags are the easy part. Measured on ffmpeg 8.1.2, a tool writes its
identity into five places:

- container tag's Lavf        -> -fflags +bitexact
- avc1 compressorname         -> -metadata:s:v:0 encoder=
                                 (container-level blanking does nothing;
                                 stream-level works)
- AAC bitstream DSE           -> -flags:a +bitexact
- H.264 SEI (x264's full      -> -bsf:v filter_units=remove_types=6
  encoder settings string)
- handler_name                -> must be written explicitly; blanking falls
                                 back to ffmpeg's default VideoHandler

The fourth is the nasty one: -flags:v +bitexact and -x264-params info=0 both
fail to stop it, and no ffprobe command shows it at all. The full trail,
including what did not work, is in references/residue-map.md so it never has
to be re-derived.

One more that is not among those five: GoPro and DJI put GPS on a separate
gpmd timed-metadata track. Stripping tags does not touch it; only
-map 0:v:0 -map 0:a:0? keeps it out. Hence "stream layout" is its own gate.

Two modes:
- copy (default) keeps the picture byte-for-byte (cmp-verified), 1.3s for a
  53s film
- encode re-encodes fully, additionally killing bitstream-domain watermarks
  and fragile steganography
Audio is re-encoded in both: the DSE lives inside the audio bitstream, so
-c:a copy cannot reach it.

Four profiles: obs (default) / quicktime / screencapture / bare. obs does not
forge a version number -- the muxer owns that field, -metadata can neither
override nor clear it, so obs simply lets ffmpeg write its own real version;
OBS output is ffmpeg-muxed anyway. A profile only touches the muxing tool's
trace, handler names and creation time. It never writes a device model or GPS.

Verification is a proof, not a claim: every metadata string in the source
becomes a needle and the output is scanned byte by byte. Needles must be >=5
bytes (shorter ones collide at random inside compressed data) and generic
strings are excluded (VideoHandler carries zero information; Lavf58.45.100
pins one version and is a fingerprint).

The SEI knife only comes out conditionally. ffmpeg offers NAL-level
granularity only, so remove_types=6 also removes HDR static metadata and
CEA-608/708 embedded captions. It therefore fires only when an identity
string is actually found, never on an HDR source, and says so either way.

12 gates, 117 self-test assertions, a breach case for every gate. Mutation
tested: gutting the GPS gate, disabling the audio re-encode and disabling
needle filtering were each caught by the assertions.
2026-09-21 16:12:25 +08:00
eternityspring 18f2f63987 badge 配色从深绿改成红
- 主色 285444 → a02128(深红)
- 中性色跟着换掉:8b938a → 8a8785、e2e6df → ece9e7
  原来的鼠尾草绿配红会撞成圣诞配色,换成中性灰才干净
- 六份 README(仓库 + 两个 skill,各中英)一起改
2026-09-14 08:51:07 +08:00
eternityspring 75520c7b32 社群那段改成第一人称 2026-09-13 11:50:04 +08:00
eternityspring c432255ced README 顶部加微信交流群和 X 的入口
- 中文版顶部加两个 badge:微信交流群(跳站内锚点)、关注作者(跳 x.com)
- 中文版 skill 表之后加「AI 视频交流社群」一段:微信号 hao_dev、备注 github、
  二维码。明写 skill 本身是 Apache-2.0 开源的,用不用它和进不进群没关系
- 英文版只加 X,不加微信
- badge 沿用现有配色(#285444 / #8b938a),四个连起来是一条色带

二维码复用 shuohao-skills 那张(同一个号)
2026-09-13 11:46:00 +08:00
eternityspring 1acf6e603e 换了 demo-en.mp4,英文报告和合成视频重新生成
新素材是另一条片子(287.37s / 640x360 / 23.98fps,《肖申克的救赎》晒屋顶那场),
旧的 5 镜标注全作废,整条重拉。

- 46 镜,平均 6.25s,每分钟 9.6 切,15 道门全绿、0 条提示
- 检测出 44 刀,补了 2 刀:23.77s 和 59.77s,检测分 0.24 / 0.25,
  卡在 0.30 门槛下面
- S37 是 46.92 秒不切的长镜头(落日下摄影机贴着一排喝酒的犯人平移到 Red)。
  一开始当成漏刀查了三遍——scene score、scdet、全帧率逐帧差分——
  最高只有 1.5,真切点是 58~75。抽帧确认:它真的一刀没有。S23(27.32s)同理
- 合成视频 1280x1296 / 37MB:原片 640x360 面板排不下四列,用 --scale 2 放大;
  --crf 28 压体积(20 的话是 77MB,进仓库太大)
- 中文报告一并重渲(renderMd 改过)
- 四份 README 里「30 秒广告片 / 5 镜」的说法跟着改
2026-09-12 21:34:52 +08:00
eternityspring 866cea9704 修五个 bug:面板右边的字被啃掉、旧高亮条露脸、英文报告混中文标点
拉一条 640x360 的英文长片时撞出来的,每个都补了击穿用例
(video-shots 436 → 449 项,video-sync 106 → 122 项)。

video-sync
- 裁窗口的 x 用的是 0,盖回去用的是 view.x:长图是整块面板宽的、列表在里面缩进
  27px,于是整张表右移一个缩进,右边同样宽度的字被切掉("cashes" 显示成 "cashe")
- 没轮到的那层高亮条停在「视窗底 + 10」:视窗底下还有进度条那一条,
  旧的一条高亮就露在进度条上面
- panel.css 里景别·运镜一格 nowrap + 截断:中文词短没事,英文
  "medium close · handheld" 一截变成 "hand",这一格的信息等于扔了,改成换行
- 新增 --scale:默认仍然只缩不放,但面板是跟着画面区算的,640x360 的原片
  面板只有 640x288、四列读不了。--scale 2 把画面区放到 1280x720、面板
  1280x576,宽高比不动,上限照样夹

video-shots
- seed --lang en 不把 lang:"en" 写进底稿:后面的 recut / validate / render
  不带 --lang 就全退回中文,英文报告里混一段中文
- validate 只认 ctx.lang,不认 JSON 顶层 lang,跟报告的优先级不一致
- renderMd 里主体分隔符和节奏冒号写死成「、」「:」,跳过门的理由写死成全角括号:
  英文表格里冒出中文标点。recut 补刀写的 note 同理

文档:--scale、seed --lang、两个新踩的坑都写进 SKILL.md / README / layout.md
2026-09-12 21:34:09 +08:00
eternityspring 18912b6839 删掉 NOTICE 2026-09-12 21:33:15 +08:00
eternityspring df0d162229 README 结尾改放视频,顶部大截图去掉
- 四份 README(仓库 + skill,各中英)结尾新增「成片长这样」,直接嵌
  demo-en-sync.mp4,并留一行直链兜底(GitHub 有时不渲染仓库内的 video 标签)
- 去掉 video-sync README 顶部的成片截图——同一条信息给两遍,视频更有说服力;
  竖版那张缩小留在版式说明里,它是解释用的不是样例
- 顺手同步漏改的门数:拉片是 15 道不是 14 道,skill 一览表补上「节奏」
- 删掉已成孤儿的 assets/output-landscape.png
2026-09-12 19:26:19 +08:00
eternityspring 12668c175d 滚动改成「当前镜头钉在第二行」;行高随内容变;修好亮条长图被裁的 bug
- 锚点从「视窗高的 28%」改成**第二行的行首**:第 1、2 镜不滚,第 3 镜起每切
  一次正好往上滚一行,当前镜头始终停在第二行。按百分比算会卡在两行中间,
  整张表看着是歪的。--anchor 可改钉第几行
- 画面描述与节奏分析不再截成两行,字号调大(1.12rem),表头字号也调大;
  **行高由内容决定**,浏览器量出来多少就是多少
- 高亮条按行高分层:crop 的高度是配置期定死的(运行时只认 x/y 命令,h 命令
  试过无效),所以出现几种行高就建几层,没轮到的挪到画面外停车;上一镜那层
  晚一个缓动才停,让它陪着滑完这一程
- 修 bug:tall-lit 模式漏在「不锁 body 高度」的判断里(写的是 !== 'tall'),
  亮条长图被裁在 576px,后面全空白——表现是滚过前几镜之后高亮行整行消失。
  全片抽帧逐个核对过 S03/S07/S20/S34/S45 都对得上
- 表头不再折行(nowrap + 省略号),英文表头缩短
2026-09-12 17:56:20 +08:00
eternityspring 86db65f253 新增节奏分析字段;面板改成表头四格,列表随播放滚动
video-shots(第 15 道门):
- 新字段 rhythm + rhythmNote——这一镜为什么留得住人。八个角色按短视频
  真正抓人的点拆:钩子/铺垫/递进/重音/转折/兑现/换气/收口
- 门:可选字段,但整片标或整片不标(半张表汇总不出东西);标了就得写清
  为什么,空话词表和长度判据与画面描述同一套,中英各一套
- 三条只提示不拦:开篇 5 秒没钩子、兑现前面没铺垫、连着 6 镜节奏发平
- 报告(中英):列表与卡片里一枚节奏标 + 理由,分布多一块,播放器信息栏跟着显示;
  Markdown 镜头表多一列
- 53 镜样例逐镜标满,跑出来的两条提示都是真的(S46 实测偏高、片尾六张字卡发平)

video-sync(面板与动效):
- 表头 + 四格:镜号/时间/景别/运镜 · 画面 · 画面描述 · 节奏分析;镜号绝对定位在
  左上角,节奏写成 [钩子] 前缀、字号与描述同大;删掉当前镜头卡与片头行
- 列表随播放滚动:切点处滚 0.45 秒把当前镜头带到锚点后停住,高亮条同步滑过去
- 实现从「一镜一张 PNG」换成「量一次 + 截三张图」:暗底长图裁视窗=滚动,
  亮条长图裁一行=高亮,位置由 sendcmd 每镜下发一条小表达式。53 镜截图
  12 秒(原来两分钟)
- 两个坑记在 references/layout.md:drawbox 的表达式只在初始化时算一次;
  整片拼成一条大表达式会让 ffmpeg 在一百来项上直接配置失败
- panel.css 全部收进 .panel 作用域(要注进调参台等别的页面),版式 class 从
  body 挪到 .panel
2026-09-12 15:36:13 +08:00
eternityspring 2e3892b00f 新增 video-sync:把拉片数据和原片合成一条带分镜信息的视频
版式只看宽高比:横版/方版 → 画面在上信息在下(vstack),竖版 → 画面在左
信息在右(hstack)。两个方向都保证画面原样缩放不裁不拉,边长取偶数(h264),
面板最短边不低于 260px。

- 面板是一张 HTML 页面,**一个镜头截一张图**:高亮和滚动本来就只在切点上变,
  逐帧渲染是白烧机器。时间对齐交给 concat 的时间戳,不写 N 条 overlay enable
- 布局全在 scripts/panel.css 里,改它就能改版式;panels 顺手写一张可预览的
  panel.html,浏览器打开加 #S07 换镜头
- 字号跟着面板尺寸走(clamp(12, min(宽/38,高/26), 26)),CSS 一律用 rem——
  导出的是视频,屏幕上舒服的字号在手机上是一团糊
- 命令:plan(只算几何不动视频)/ panels / compose / export
- 自测 73 项,不碰 ffmpeg 不开浏览器:几何、面板数据契约、concat 清单、
  ffmpeg 参数(vstack/hstack、setsar、无声片不接音轨、原片必须是 0 号输入)
- demo-sync/demo-en-sync.mp4:30 秒英文广告片 + 5 镜信息,1280×1296
2026-09-12 09:56:16 +08:00
eternityspring cf507707f3 英文补齐:门名、违规信息、命令行全部双语,画面描述按语言判
- 门的名字与全部违规信息做成中英两套(MSG 表),跟着 --lang 走;
  自测逐条对账:每条文案中英都得有、不能是复制粘贴、英文里不许混中文
- 所有命令都认 --lang(seed / recut / frames / sheet / validate),
  之前只有报告界面能切,英文报告里印一排中文门名
- 画面描述的判据跟着**描述本身的语言**走:中文数字数(≥12 字),
  英文数词数(≥8 词),空话词表与废话开头各一套
- 修:下游关管道(| head)不再吐 EPIPE 栈;英文单复数(1 hint / 2 hints);
  Markdown 表头残留的「起—止」与全角括号
- demo-report-en/:30 秒英文广告片的完整拉片,5 镜,14 道门全绿,报告全英文
2026-09-11 23:07:30 +08:00
eternityspring d6d2b48882 README 去掉「许可 / Licence」一节(LICENSE 与 NOTICE 里仍然写明) 2026-09-11 13:22:33 +08:00
eternityspring 4898815e3d README 放报告截图,示例挪到安装之后
- 两份 README 加上报告截图(桌面 + 窄屏),点图跳 demo-report/shots-report.html
- 「先看成品」改名「示例 / Example」,位置移到「安装」之后
2026-09-11 13:17:19 +08:00
eternityspring 05f8da5ed2 安装改成 install.sh,README 去掉「写法」一节
- scripts/install.sh:软链 skill 到 ~/.claude/skills 和/或 ~/.codex/skills,
  git pull 之后立刻生效;支持 --claude / --codex / 指定 skill / --uninstall,
  装之前自检 node 与 ffmpeg
- 两份 README 的安装段改成 clone + ./scripts/install.sh
- 删掉「写法 / How these skills are written」一节(来源仍见 NOTICE 与 skill 的 README)
2026-09-11 13:11:57 +08:00
eternityspring a107e70b49 加入 demo 产物与英文 README
- demo-video.mp4 与 demo-report/ 进版本控制:克隆下来双击 shots-report.html
  就能看成品(报告 + 主数据 + 106 张关键帧),不用先自己跑一遍
- 新增 README.en.md(仓库与 skill 各一份),顶部加中英切换按钮
- NOTICE 与两份 README 写明:demo 画面来自短片《啥是AI》,版权归原作者,
  不适用本仓库许可证
2026-09-11 13:06:48 +08:00
eternityspring 4376ef5782 video-shots:把成片拆成逐镜头的拉片表
切点、时长、实测运动量由 ffmpeg 量出来,模型只判断景别、类别、运镜、画面这四件事,
再由 14 道确定性质量门逐条对账。最硬的一道是运镜实测对账:声称推拉摇移却实测几乎
不动,直接拦;反向(声称固定、实测很动)只提示不拦,因为主体运动也会让帧间差爆表。

- scripts/video-shots.mjs:seed / recut / frames / sheet / validate / render,零依赖
- scripts/report.css + report.js:单文件交互式报告的样式与交互,render 时整段内联,
  可直接编辑;页面上的每个数字都从 shots.json 算
- references/:词表与判据、拉片方法论、schema、报告设计约定
- examples/:一条 202.9 秒短片的完整拉片(53 镜,14 道门全绿)当质量基准与自测夹具
- scripts/selftest.mjs:160 项断言,每道门都有击穿用例
2026-09-11 12:55:10 +08:00