# 一张静帧，然后整部影片——以及两个角色怎样共用一棵目录树

> 这些准则是做 MECHANICAL LEPIDOPTERA 时挣来的，它就是 [example/](../example/index.md) 里那个完整示例。下面每一条规则都在那部影片上付过学费；哪里的错误是导演自己犯的，这里就照实说，因为带着伤疤的规则，人们才守得住。

**错误，而且是我的。** 我的 spec 第 3 版有八道闸门。第 4 版有七道，静帧优先，每一张审完才开始下一张：一张花毛茛设定页、六张蜕变静帧、三张设定页静帧、对目的地画面的再次审批。感觉很严谨。它实际做的事，是让客户一轮又一轮地审批一部她还看不到的影片的碎片，而她真正想要的东西——一部影片——始终停在零帧。她叫停了这道闸门，要求看完成的影片。她是对的。交回来的影片有一些任何静帧都显示不出来的问题（节奏、流畅度、那次拉远有没有动机），而静帧一直在争论的那些问题一个都没有。

**为什么一张静帧值得，七张不值得。** 一张静帧恰好回答一个问题：是不是这个**样子**。线条、填色、调色板、密度、写实程度、媒介。这个问题问起来便宜（一张静帧不到一分钟就渲染完），问晚了却是灾难（我们前一部影片的第一版角色阵容，是在场景已经搭在它上面之后才被否决的；我们的第一只蝴蝶，是在已经放进两个场景之后才被否决的）。所以这个问题你只问一次，并且好好问。其他一切——故事、节奏、摄像机、连贯性、音乐——只存在于运动之中，而从一个粗糙的整体里学得最快。

**规则。**

- **一张样子静帧**，影片里最难的那一帧：主角题材画得大，处在它的世界里，用最终的媒介、最终的光。如果题材是现实世界里的东西，再加一张标本样张（[realism-and-craft.md](realism-and-craft.md)）。**只有**确实存在另一种截然不同的样子时，才再加一张静帧（我们有两种媒介，所以是两张）。这就是上限：三张图。
- **先自己批评它**，写成文字，对照手艺及格线的对照表，并且在拿给人看的时候说清还有哪里不对。
- ✋ 一次审批。
- **然后不停顿地从头做到尾。** 第一天就有一部粗糙的完整影片，胜过一个完美的第一幕。机械检查持续运行，不需要任何人审批：类型检查、网格、契约扫描、绘制预算、确定性探针、冷场。
- **整体只审一次。** 从渲染出来的文件本身审：每拍一格的联系表（contact sheet），每个切点做成前后一对并附上差异度量，冷场数值，联系表引起怀疑的地方再抽几帧全分辨率看。一份编号的修改清单，排好优先级。一次重建。这一次审查怎么跑（四种读法、一张记分卡、闸门）：[workflows/review.md](workflows/review.md)。
- **审批是一笔预算。** 每道闸门都要花掉客户的注意力和你的 token。把它们花在出错代价高的地方，也就是样子、音乐小样和最终影片。三次，不是三十次。

**好的审查是什么样子**（过度设闸的流程里，这一部分值得保留）：看之前先冻结文件的一份副本并记下它的哈希，因为构建者可能正在你底下重新渲染（它确实这样做了，两次）。先报告哪些地方好，好让它们在修改中保留下来。给出原因，而不是印象：不是「身体是绿的」，而是「太阳圆盘叠印在胸部上，蓝乘黄」。错在你自己的 spec 时，同一口气就把它改正过来（我规定的 5 帧翅膀滞后，放在 10 帧一拍上就成了反相：一只翅膀合上时，另一只正张开）。

---

## 两个角色、书面交接，以及重置

**为什么要分开。** 构建一个镜头的人无法用新鲜的眼光看它，而审查自己作品的人是在给自己付出的努力打分。把导演（故事、spec、样子、审查）和构建者（引擎、确定性、契约、渲染）分开，给了我们一个从没见过代码那些借口的审查者，和一个从不被要求在截止期限前拿出品味的构建者。客户仍然是审批者和传话人。

**行之有效的模式。**

- **spec 是意图的唯一来源**，故事一变就**整份**重写，绝不一层层打补丁打成一张重写本。我们的 spec 一天里改了四版；第 2 版是一个「第 12 节取代第 5 节和第 9 节」的补丁，所有人——包括我——都读错了。把每一行属于导演自己拍板的内容都标出来（我们用的是 FABLE），这样客户一行字就能否决，不用到处找。
- **构建者在项目里维护一份持续更新的决策日志**：设计决策、偏离、取舍、悬而未决的问题，每批做完后追加，附上数字和哈希。导演靠它了解实际造出来的是什么，本文里最好的教训也都是在那里发现的。
- **审查就是文件**：`reviews/<thing>-r<N>.md`，条目编号、排好优先级，内容自足到可以一字不改地转达。
- **两个 agent、同一棵工作树，一定会撞车。** 构建者在我正在审的文件上重新渲染；我改了一个它正打开着的源文件。终结这件事的协议：导演审的是一份冻结的**副本**（记下哈希），只写审查文件，构建者活跃期间绝不碰共享的源文件或输出。导演的小补丁只有在构建者空闲时才允许，并且每一处都带一行注释，说明是谁、为什么。
- **一个绿色对勾是一项声明。** 重新跑一遍。构建者报的「G-AIR PASS」是真的，而那一秒近乎静止的画面也是真的。

**重置。** 长会话会腐坏。我们的会话把这一点表现得很清楚：客户的消息开始到达时前半截被截掉了，上下文在我底下被摘要，我发现自己差点就在半句话上重建一个计划。征兆是下面任意一条：你在猜对方问的是什么；你在重新推导早已决定了的东西；你的回答越来越长、越来越不确定；运行框架（harness）本身报告上下文质量低。

不要硬撑。重置：

1. **先把状态写到磁盘上。** spec 是最新的，决策日志是最新的，未结的审查已归档，再加一份简短的记忆笔记：角色分工、三句话讲完的故事、硬性规则、东西都放在哪里、已知的环境陷阱。
2. **清空会话。**
3. **用一条消息重新交代，附一份「先读」（READ-FIRST）清单**：spec、契约那一节、已经构建好的模块、记忆笔记。第一行写明角色分工。任务放在最后。
4. **新会话先核实，再信任。** 读文件、查磁盘、说出哪里和交代的内容对不上。（我们的新会话在头五分钟里就发现，那个「已经构建好」的摄像机并不在源码里。）

一次重置花了我们大约十分钟，因为状态都在文件里。如果状态只在对话里，它会让整个项目付出代价。还有，一条消息被截断到达时，先去会话记录里找完整原文，再去猜；有两次，缺掉的那一半改变了整个任务。

---

## 把 token 和时间花在买得到东西的地方

发布片花了 1.88 亿个 token，其中大约 99% 是对同一个 75 万 token 上下文的缓存重读。没有一个花在画画上。下面这些习惯把它砍掉了一个数量级：

- **每一版剪辑一个会话，配一份交接文件。** 一版剪辑完成时，把状态写下来（有什么东西、对方给出的规则、还有什么没解决、文件放在哪里），然后在一个读取这份文件的新会话里开始下一版剪辑。长会话每一轮都要把它的整个历史重读一遍。
- **帮手只拿写好的 brief，绝不用继承上下文的分叉（fork）。** 帮手拿到一份简短的书面 brief 和它需要的那几个文件。分叉会继承整段对话，并在每一次调用时为此付费。
- **审查用 0.3-0.5 倍的联系表**（`ffmpeg -vf "fps=1,scale=576:-1,tile=4x4"`），只有联系表引起怀疑的那块裁切才看全尺寸。已经判断过的大图，绝不要再读一遍。
- **任何完整渲染之前，先出一张选项页。** 转场、文字样式、调色板：一张静帧页上并排放三到六个变体，一条消息里挑定。
- **时间线即数据。** 场景放在同一条内容时间线上，剪辑是一份片段列表（`launchCut.ts`）。调整时间只改这份列表；场景代码和针对它的审查依然有效。
- **区间渲染。** `render.mjs <film> --from F --to T` 只渲染 `[F, T)` 这些帧，无声（配乐会对不上），写入 `out/<film>.<F>-<T>.mp4`；`--out` 指向完成的影片时它会拒绝。用于审查，不用于交付。
- **多张静帧，一个浏览器。** `still.mjs <film> --frames 0,700,1500` 在一个页面里画出列出的每一帧（一次构建、一次启动、样张只烘焙一次）；区间也行，`--frames 0-300:50` 是从 0 到 300 每隔 50 帧取一帧。加上 `--sheet out/sheet.jpg`，就以 `--sheet-scale`（默认 0.4）把它们拼成一张联系表。`--out out/dir/` 或 `--out out/x-{frame}.png` 指定 PNG 的命名。
- **样张缓存。** 画完的样张帧保存在 `engine/.cache/bakes/<browser version>-<binary>/` 里，以样张整个导入闭包的哈希为键，所以一面 32 个活样张的墙不会在每一张静帧上都重新烘焙一遍（约 60 秒），而改过的样张只是缓存未命中、重新画一遍。缓存从来不是必需的：随时可以删掉 `.cache/`。`--no-bake-cache`（或 `ANIDOODLE_BAKE_CACHE=0`）让每个样张都冷启动重画，怀疑缓存有问题时就这么做。
- **逐帧精确的比对。** `render.mjs --hashes out/x.md5` 在编码器碰到每一帧之前写下它的 md5；对比两份这样的文件，就能证明一次引擎改动让影片逐字节不变，或者找出它改动的第一帧。
- **定格要声明，绝不悄悄吞下。** 一个正在被读的词、一张片尾卡、一个正在被观看的循环动画：把这段区间写进 `meta.holds`，并在注释里写明理由。这样闸门的冷场检查就知道这段静止是有意的。其他任何静止不动的东西都是干等：剪掉。审查在一部已经审批过的影片里发现的等待，连同理由写进 `meta.locked`：闸门把它打印出来并持续不通过，于是这个决定始终摆在眼前。如果负责人接受它按原样交付，它就变成一个以那条理由为第三个字段的定格（`[from, to, "approved as shipped: ..."]`），闸门打印它并放行（`launch3.ts`）。
- **`npm test`**（在 `engine/` 里）不开浏览器，运行 `engine/test/*.test.ts` 里的单元测试套件（目前测的是摄像机运动）。任何引擎改动之后，都和闸门一起跑。
