film-engine:从代码逐帧生成的电影
工具包位置: 本技能中的所有命令均在
reels-maker文件夹(即github.com/thativanng/reels-maker的克隆仓库,包含template/和tools/目录)下运行。若项目尚未创建,请先按该仓库的SETUP.md进行设置。
核心理念概述
在这里,一部电影就是一个 HTML 页面,遵循唯一规则:window.seek(t) 必须仅根据时间 t,每次都精确绘制出对应的帧。画面中的任何内容都不能读取真实时钟。然后,渲染器会以 30 fps 的速度逐步推进 t,以 2 倍分辨率截取每一帧的截图,并将其传递给 ffmpeg。由于页面是确定性的,因此重渲染的结果是一致的(平均误差在单个像素级别内;GPU 噪声可能导致个别像素多达约 60 级的色差)、17.3 秒处的静止帧就是视频将显示的画面,并且所有检查(film-audit)都可以在不播放电影的情况下查看任意时刻。
工作流程(请勿跳过任何步骤)
- 先确定需求和视觉风格。
reel-intake的产出是已签署的简报和故事板(格式、时长、声音、禁止出现的内容);reel-style的产出是选定的视觉风格。在获得这两项之前,不要开始本技能的构建步骤。 - 故事板。
reel-intake负责编写故事板(时间·屏幕文字·声音和字幕·原因;位于references/brief.md),并且在开始构建前,相关人员需签署确认。如果所选风格包含横幅,则故事板需增加一列横幅内容。 - 测试片段。 使用选定的风格构建大约前 10 秒的内容(步行风格电影可能需要 15-30 秒来展示一个章节),生成静止帧并获得肯定答复。在视觉风格未经签字确认的情况下进行完整构建是徒劳的。
- 构建 整部电影(风格包中说明了如何构建各个场景)。
- 关卡检查(film-audit):以 40 fps 进行碰撞检查、像素探针检查,然后由独立的审核人员进行审核。在此步骤完成前,不得将任何内容展示给相关人员。
- 渲染:
node tools/render.mjs --page reels/<name>/page.html --out reels/<name>/out/film-silent.mp4(2x 帧,使用 lanczos 算法缩放,无颜色标签)。 - 音频:
python3 template/audio.py out/film.wav(生成一个与电影时钟同步的提示列表)。 - 最终封装:
zsh tools/mux.sh out/film-silent.mp4 out/film.wav out/film.mp4。该命令输出响度为 −14 LUFS、带限制器、无颜色标签的 H.264 文件,以及一个与电影比例相同(短边为 720px)的手机预览版。 - 封面:
node tools/cover.mjs --page reels/<name>/page.html --out reels/<name>/out/cover.jpg。如果页面有封面模式(starter 中的?cover,或来自config的cover),则绘制设计好的静止帧;否则将输出T=时刻的帧。封面保存为电影自身的比例,大小控制在 2MB 以下,对于 9:16 的电影,还会输出一个中心 3:4 的裁剪图(用于 Instagram 网格,已记录)。LinkedIn 仅在发布时接受封面:发布后只能编辑帖子的文本(LinkedIn 帮助中心,“编辑已发布的帖子”,https://www.linkedin.com/help/linkedin/answer/a522811)。其缩略图规格(JPG 或 PNG,宽高比与视频一致,最大 2MB)来自搜索摘要中引用的 LinkedIn 帮助信息;该页面本身无法加载,因此应视为未经核实的信息。 - 发送 优先发送手机预览版,然后发送完整文件和封面。
页面合约
window.seek(T):绘制第T帧。它是唯一驱动动画的地方。window.ready:一个 promise,在字体、纹理和第一帧加载完成后 resolve。window.DUR:电影的总时长(秒)。window.FILM = { w, h, format, safe }:帧尺寸和像素单位的安全区域(keep-clear zone)。所有工具都根据此信息调整窗口大小,因此页面拥有其格式的控制权。- 可选:
window.cover()用于绘制封面而非视频帧,FILM.coverAt指定封面所在的时间点(tools/cover.mjs会使用这两个设置;如果没有,则取T=时刻的帧)。 window.collide(T)和window._dbg:供film-audit使用的钩子(参见该技能)。- 工具通过 HTTP 提供 kit 文件夹的服务(ES 模块无法从
file://加载)。要在浏览器中实时查看页面,可运行npm run serve并打开http://127.0.0.1:8765/template/starter.html。
格式、starter 模板和其他语言
template/formats.json包含四种尺寸(9:16 1080×1920 · 4:5 1080×1350 · 1:1 1080×1080 · 16:9 1920×1080)、平台到格式的映射,以及每个平台的安全区域及其来源。简报中的格式行从中选择。- 每个 reels 项目都位于自己的文件夹
reels/<name>/中(已被 .gitignore 忽略):包含其简报、故事板、page.html(从 starter 或 film.html 复制而来)、reel.json或career.json,以及out/文件夹。页面从工具包的根目录加载字体和formats.json,因此副本可以在此工作。将工具指向它:PAGE=reels/<name>/page.html。 template/starter.html+template/reel.json是除体素步行风格外所有视觉风格的基础骨架。其文字和时间信息位于配置文件中,它根据?fmt=或配置文件调整尺寸,并将所有文字保持在平台的安全核心区域内。?guides会以红色显示覆盖区域,仅用于检查;渲染时切勿启用此参数。它只是一个骨架,而非视觉风格:reel-style会重写其 tokens 和布局。template/film.html(体素模板)仅为 4:5 比例, 并且选择不适用安全区域(safe为 0):其横幅和字幕被设计为靠近边缘,因此安全区域探针检查总是通过,最终由审核人员目视确认。其摄像机关键帧是为 4:5 比例配置的;其他比例意味着需要重新配置关键帧并重新运行所有关卡检查。- 中文、日文、韩文: starter 模板在渲染时会从 Google Fonts 加载 Noto Sans TC(仅加载配置文件使用的字符),并以约 0.72 倍于拉丁文字号的大小显示 CJK 字符(因为它们的字符是全角宽度)。使用
表示需要换行的位置:中文可以在任意两个字符之间换行,因此长行需要手动指定换行位置。template/reel.zh.json是一个工作示例。其他语言脚本需要在字体栈中包含自己的字体。
时钟与时间
- 整个电影使用同一个时钟 来驱动场景、摄像机、横幅、字幕和声音。当需要在后期插入一个节拍而不重新调整所有时间时,使用时间重映射:场景时钟在插入点暂停,而叠加层(如横幅、字幕)继续使用真实时钟:
音频脚本需要相同的映射表:编写一个const INS = [[24.0, 5.0, 'detail']]; // 在场景时间 24.0 处,播放一个 5 秒的插入节拍 function remap(T) { let sh = 0; for (const [a, len, id] of INS) { if (T < a + sh) return { t: T - sh, ins: null }; if (T < a + sh + len) return { t: a, ins: id, u: T - sh - a, len }; sh += len; } return { t: T - sh, ins: null }; }RT(t)函数,将插入的时长累加到场景时间上,并将每个提示点放置于RT(t)处,否则插入点之后的所有提示都会偏移。(模板中没有插入内容,因此其 audio.py 不需要此函数。) - 缓动: 摄像机的移动使用
eio,物体出现使用eo,行走和驾驶使用trap(梯形速度),这样移动物体不会在端点处“卡顿”。 - 摄像机关键帧 是绝对的
A(pos, target),或者是跟随主角地面点的F(offset, targetOffset)。使用eio在关键帧之间进行混合。在重要的节拍上保持镜头稳定;持续运动的镜头会显得紧张。
覆盖层(在 DOM 中,位于 Canvas 之上)
- 横幅: 年份、公司、角色标识,以及一个时间线,其圆点会随着当前年份变化。横幅的变更应在新场景已完全显示时进行(其布景已升起、服装已更换、主角已到达),而非在代码开始构建场景时就改变。请通过查看变更前后 ±1 秒的帧序列来确认这一点。
- 字幕: 每个节拍一行故事文字,关键词以粗体强调色显示。使用
text-wrap: balance。为读者提供大约每秒 3 个词的速度,并在快速摄像机移动暴露其后的主体之前淡出字幕。 - 对话气泡: 跟随角色移动:映射头部点,并将气泡固定在距离画面边缘 ≥24px 的位置。
- 结束卡片: 姓名、一句标语、擅长领域、一句引语。场景在卡片后模糊。
音频
- 提示点写在电影时钟上,紧邻其所属的时刻(如走路时每一步、物体出现时的提示音、物体落地时的撞击声)。
- 不要为转场添加“嗖”声效果,这会显得廉价。使用提示音、滴答声和轻柔的撞击声。
- 默认背景音轨由
template/audio.py生成(基于正弦波),因此可免费在任何地方使用。如果有人使用 AI 生成的音轨,请检查其许可证:某些模型的权重可能只允许非商业用途。 - 响度:使用两遍 loudnorm 标准化到 −14 LUFS(先测量,再线性应用),然后加限制器。一遍处理会导致过冲。
渲染中的关键教训(更多内容请参阅 references/gotchas.md)
- 以 2 倍分辨率渲染,然后缩放。 EffectComposer 会去除 Canvas 的抗锯齿效果,因此 1 倍渲染会导致边缘出现大量锯齿。
- 不要在 MP4 中添加颜色标签。 JPEG 管道会将其标记为完整范围,导致某些播放器会错误地移动颜色。在滤镜中使用
setparams=...=unknown。 - 同一平面上的两个面会闪烁(z-fighting)。为前面的面设置深度偏移。
- 必须加载字体(使用
document.fonts.load),然后再在 Canvas 上绘制文字,否则会静默地使用后备字体。 - 在无头 Chromium 中启用 GPU(macOS 上使用
--use-angle=metal)。如果不启用,渲染速度大约会慢 10 倍。
无需每个步骤都手动提示
相关人员已一次性同意了工具包的允许规则(SETUP.md 第 4 步);请在其范围内进行操作,否则他们会被反复询问以批准每个操作:
- 使用 Write 和 Edit 工具来写入和修改文件:页面、reel.json、简报、故事板、AUDIT.md。不要使用 Python 或 Shell 脚本、heredoc 或
sed -i生成它们;这些操作都会触发批准提示。 - 使用工具包的测量工具(stills、probe、sheet、collide、render、cover),而不是用一次性
python3 -c代码片段。 - 无需使用
mkdir、循环或链条: 每个工具都会自动创建自己的输出文件夹,并允许在一次调用中接受多个时间点(例如stills.mjs … 1.5,6,12)。 - reel 项目自身的脚本(如声音脚本、辅助工具)位于
reels/<name>/目录下,并作为python3 reels/<name>/audio.py或node reels/<name>/x.mjs运行,这些操作已包含在允许规则中。
命令
每个工具都以 一条简单命令 的形式运行(使用标志、不加前导 VAR=…、不使用循环、不使用 && 链)。这样,使用者的允许规则(node tools/*)就能覆盖这些命令,他们无需批准每个步骤。环境变量仍然对脚本生效。
npm run doctor # 首先检查本机环境
npm run demo # 12 秒的验证演示:输出至 out/demo/
node tools/stills.mjs "2,8.5,15" # 生成 2x 分辨率的静止帧 → out/stills(用于体素模板)
node tools/stills.mjs --page template/starter.html --format 16:9 1.6 # 使用另一种比例测试 starter
node tools/stills.mjs --page template/starter.html --cfg reel.zh.json 1.6 # 使用另一个配置文件(这里是中文)
node tools/stills.mjs --dsf 1 --out out/chk 2,8.5 # 快速生成 1x 帧用于拼接
python3 tools/sheet.py out/chk/sheet.jpg 6 300 out/chk/t*.jpg
node tools/render.mjs --page reels/<name>/page.html --out reels/<name>/out/film-silent.mp4
python3 template/audio.py out/film.wav
zsh tools/mux.sh out/film-silent.mp4 out/film.wav out/film.mp4