video-skill 是做什么的?
脚本位于此文件旁边的 scripts/ 目录中;在下面写作 $S,但在每个命令中请写出绝对路径,因为 shell 变量不会在 Bash 调用之间传递。
你无法观看视频,因此每一步都将其转换为你可以阅读的内容:你查看的帧和你检查的数字。 制作电影的过程是:编写 → 渲染帧 → 读取联系表 → 修复。理解素材是:探测 → 代理 → 带时间戳的帧格表 + 转录稿 → 判断。交付是一个检查编解码器、帧率、快启动和响度的脚本,因此“它随处可播放”是一个可验证的事实。
首次在 Mac 上使用
请自行运行 sh $S/setup.sh。它会检查 mise 和 ffmpeg(必要时通过 Homebrew 安装 ffmpeg),首次使用时安装固定版本的 OpenFilm 并运行 openfilm doctor。修复它打印出的每个 TODO,或者直接向用户解释清楚。可安全地重复运行。
制作电影:OpenFilm
OpenFilm(MIT 许可证)是一种创作格式:一部电影是一个包含 film.html 的文件夹,该 HTML 文件将 HTML 页面、素材片段、图片和声音组合在一起,每个页面在接收到请求时绘制任意时刻 t 的画面。Studio 是它的本地浏览器编辑器。
- 打印手册并完整阅读(手册很短,并且在不同版本间可能会有变化):
$S/openfilm。遵循手册指示,但本文件所述内容优先级更高。 $S/openfilm是此处运行 OpenFilm 的唯一方式:它固定版本,首次使用时通过 mise 安装,并阻止 Studio 弹出浏览器窗口。不要使用npx openfilm(会绕过固定版本)或npx skills add openfilm/openfilm(这是一个与本技能冲突的上游技能)。FILM始终是一个绝对路径,如$HOME/Movies/OpenFilm/Projects/<film-name>。相对路径会创建在当前文件夹中,例如他人的 git 仓库中。电影文件绝不能放在仓库中。
| 步骤 | 命令 |
|---|---|
| 创建或打开 | $S/openfilm open FILM |
| 检查整部电影 | $S/openfilm -C FILM look |
| 检查某个时刻、一段时间或某个文件 | … look 4.5,look 3-6,look assets/bed.wav |
| 检查快速进入的场景 | … look 0-2 --count 24 |
| 渲染 MP4 | $S/openfilm -C FILM render |
| 查看已连接的多媒体服务 | $S/openfilm get |
| 诊断安装 | $S/openfilm doctor |
look 和 render 无需打开 Studio 即可运行,并输出相对于 FILM 的路径。Studio 的地址包含一个访问密钥:仅将其提供给你正在协作的用户(地址是 127.0.0.1,需使用 Chrome 或 Edge 浏览器,手机无法访问)。
在展示任何内容之前,请先阅读每张 look 帧格表,并修复你发现的问题。它的检查(同一时刻 t 绘制相同画面、字体已加载、文本未裁剪、响度正常)不涉及审美或布局。关于入场时文本被截断的 ! 警告是正常的;但静止状态下文本被截断则是一个错误。然后以全尺寸查看几个时刻(look 12.5):300 像素的格子会隐藏小细节,包括人物。
通过实际制作一部电影来学习构建方法:
- 每个镜头对应一个页面;页面间共享的内容(如 Ken Burns 移动效果、字体、颜色)被放入一个模块中供页面导入。每个值仅是
t的函数:没有定时器,没有动画循环,没有状态。 - 页面通过透明度(由
t控制)实现淡入淡出;在同一轨道上两个镜头之间的黑场淡入淡出是最简单的过渡。交叉溶解需要将进入的镜头放在上方的轨道上。 - 照片上的文字需要底部的渐变阴影,否则会消失在繁忙的场景中。
- 字体放在
FILM/fonts/中,通过@font-face声明,并通过window.film.ready(document.fonts.ready)等待加载完成。对于照片,使用ready: img.decode()。 - 通过
#t=in,out进行裁剪,永远不要剪切新文件;如果某个音频片段比电影本身还长,它会延长电影的时长。
音频
$S/openfilm get 显示已连接的服务;生成语音、音乐和效果需要用户自己的供应商密钥(Studio:OpenFilm 徽标 → 设置 → 供应商)。如果没有密钥,可以使用 ffmpeg 创建声音,将其保存在 FILM/assets/ 下,并将命令保存在 FILM/sound-notes.txt 中以便重建,同时说明哪个服务可以做得更好。你无法听到声音,因此通过数值来判断:
- -16 LUFS 集成值 适用于最终混音(播客、大多数网络视频);流媒体平台通常标准化到 -14 左右。保持真峰值在 -1 dBTP 或以下。背景音轨在人声下方时,音量大约在 -22 左右。
- 测量:
ffmpeg -i X -af ebur128=peak=true -f null -。使用loudnorm=I=-16:TP=-1.5:LRA=7对生成的背景音轨进行标准化处理;对于最终母版,请运行两次loudnorm(先测量,然后将测量得到的measured_*值反馈回去),以便获得线性增益。 look命令会打印混音的响度,而footage.py check会在渲染结果未达到目标时报告失败。
处理收到的素材:导入
手机拍摄的视频很少看起来如你所见。在查看或剪辑任何内容之前,先对其运行以下命令:
uv run --no-project --with pillow python -I $S/footage.py probe CLIP.mov ...
uv run --no-project --with pillow python -I $S/footage.py proxy CLIP.mov <scratch>/clip.mp4
probe 会针对以下每个问题发出警告,而 proxy 则会一次性修复所有问题(SDR BT.709、旋转嵌入、恒定 30 fps、偶数尺寸、H.264 yuv420p、AAC 48kHz、快启动):
- HDR(iPhone 默认录制 HLG:
color_transfer=arib-std-b67,BT.2020)。在 SDR 模式下观看时,画面会显得褪色且颜色偏移:朱红色会变成橙色。proxy会优先使用 macOS 内置的avconvert进行色调映射,否则使用 ffmpeg 的zscale(许多 Homebrew 构建版本中缺失),如果都不行则发出警告。 - 旋转:手机以带旋转标志的方式横向存储竖屏视频;某些工具会忽略此标志。
- 可变帧率:标称 24 fps,平均 23.997 fps。帧时间漂移,音频在剪辑后会不同步。
probe会标记任何超过 0.001 fps 的差异。 - 奇数尺寸:H.264 yuv420p 需要偶数宽度和高度。
在代理文件上查看和剪辑;对于需要保持全质量的素材,请保留原始文件。
理解素材
uv run --no-project --with pillow python -I $S/footage.py frames <scratch>/clip.mp4 <scratch>/sheets --every 4
该命令会在每次场景变化时(--scene 0.3)以及至少每 --every 秒取一帧,最多 --max 60 帧,输出为 5×6 格子的帧格表,每格标注为 0007 · 1:23.4,同时生成 manifest.json 文件将每个 id 映射到对应时间。读取帧格表并用 id 和时间回答。以下是实践指南,非教条:
- 创作型电影:按镜头长度均匀采样。现成素材:场景变化加上至少每 2-5 秒一帧的保底采样。快速动作:对短片段使用
--every 0.5。 - 保持帧格表的长边不超过约 2500 像素,并一次只读取几张;对于长时间素材,可将帧格表分发给子代理,每个子代理为每个格子 id 写一个 JSONL 判决结果,然后运行
python3 -I $S/check_verdicts.py manifest.json out/*.jsonl(丢失、重复或凭空捏造的 id 会导致失败;如果一张帧格表中每个格子都得到相同的答案,则可能是被粗略扫过的)。 - 对话类视频的大部分内容在音频中。 使用本地设备进行带时间戳的语音转文字:
uvx parakeet-mlx AUDIO.wav --output-format json --output-dir <scratch>(首先提取音频:ffmpeg -i clip.mp4 -ar 16000 -ac 1 audio.wav)。通过时间将单词与帧关联起来。专有名词可能会出错(如“torii”变成“Torrey”),因此在引用之前,请结合你看到的画面核对名称。
交付
uv run --no-project --with pillow python -I $S/footage.py check FILM/<name>.mp4 --duration 30
uv run --no-project --with pillow python -I $S/footage.py share FILM/<name>.mp4 <scratch>/<name>-share.mp4
uv run --no-project --with pillow python -I $S/footage.py gif FILM/<name>.mp4 <scratch>/loop.webp --start 2.5 --duration 6 --width 480
- 在交付文件之前,
check必须通过:检查 H.264、yuv420p、偶数尺寸、恒定帧率、moov在mdat之前(快启动:边下载边播放)、时长、响度 ±1.5 LU、真峰值。 - 当用户要求提供文件或无法使用此 Mac 时进行渲染;最后一步是直接对 MP4 文件运行
look进行检查。share会生成一个小尺寸副本(宽 1280,CRF 28:一个 30 秒的 1080p 渲染从 34 MB 缩减到 3.8 MB),用于网页或聊天;原始电影 MP4 文件保持不变。 - 循环动画:优先使用动画 WebP 而非 GIF。 一个带有慢速缩放的 5.6 秒照片循环,GIF 格式为 6.7 MB,而 WebP 格式仅为 1.6 MB:当每个像素都在移动时,GIF 无法复用像素。仅在目标平台不支持 WebP 时才使用 GIF;此时
footage.py gif X.gif会使用生成的调色板(palettegen/paletteuse)。 - 移除不应传播的信息:iPhone 视频的元数据中包含 GPS、手机型号和时间。
proxy和share会丢弃所有这些信息(-map_metadata -1)。原始文件或通过 AirDrop、照片导出获得的副本会保留这些信息。使用exiftool -GPSCoordinates FILE进行检查。
电影中的照片
使用 photo-skill 技能(github.com/kylefritz/photo-skill;如果未安装,请提议将其克隆到 ~/.claude/skills/photo-skill)来查找照片(通过描述搜索照片库,然后判断联系表)并下载全分辨率原件:预览图对于 1080p 来说太小了。将电影使用的照片复制到 FILM/assets/ 中,并重新保存以去除 EXIF 信息。在包含他人照片的电影被传播之前,请以全尺寸查看每个镜头:人脸检测会遗漏人群,而 300 像素的格子会隐藏 1080p 画面中显示的人物。
其他工具
OpenFilm 最适合代理,因为一部电影就是纯 HTML,而 look 命令精确地返回你需要看到的内容。截至 2026 年底的替代方案:
| 工具 | 创作方式 | 备注 |
|---|---|---|
| HyperFrames(HeyGen,Apache-2.0) | HTML/CSS + GSAP,由无头 Chrome 渲染 | 最接近的类比;具备 check --json 代码检查/布局/对比度门控、快照功能、Claude Code 插件 |
| Remotion | React 组件 | 成熟,拥有官方代理技能;源代码可用的许可证,超过一定规模的公司需要购买许可 |
| Manim(MIT 许可证) | Python 场景 | 数学和图表解说器 |
| MoviePy(MIT 许可证)/ 原始 ffmpeg | Python / 滤镜图 | 简单的剪辑和叠加效果;ffmpeg 最好保留作为 I/O 层 |
升级 OpenFilm
上游更新频繁。请先阅读发行说明并运行 npm view openfilm scripts(preinstall 或 postinstall 条目值得留意),然后修改 scripts/openfilm 中的 OPENFILM_VERSION 并重新运行 setup.sh。之后检查 ls ~/Library/Caches/ms-playwright:OpenFilm 首次运行时,会删除没有链接标记的 Playwright 浏览器;setup.sh 会报告它删除了哪些,然后运行 npx playwright install chromium 在需要它的项目中重新安装。
常见错误
| 错误 | 修复方法 |
|---|---|
使用 npx openfilm … | 应使用 $S/openfilm …:npx 会绕过固定版本 |
在错误的文件夹中运行 look 或 render | 应使用 $S/openfilm -C FILM … |
信任 look 的检查结果而不打开其输出的 PNG | 读取帧格表;检查通过并不代表布局没问题 |
| 直接剪辑或查看 iPhone 素材 | 先 probe,然后 proxy:HDR、旋转和 VFR 都会带来麻烦 |
| 将主 MP4 文件发送到聊天工具 | 使用 share 命令,然后对副本进行 check |
| 为 README 或页面生成 GIF | 使用 WebP,大小约为 1/4 |
| 升级后 Studio 状态过时 | 停止旧的 studio/server/main.mjs 进程(pgrep -fl openfilm);一个 Studio 实例对应两个进程 |