「连贯」是量出来的:onetake 怎么把一镜到底写成了可测量的工程协议
【免费下载链接】onetakeMotion films that never cut to the next slide: every beat grows out of the one before, one continuous camera, continuity measured by an oracle. Product launch films and feature demos, made with a coding agent.项目地址: https://gitcode.com/gh_mirrors/on/onetake
AI 现在做动效视频,十有八九是"散的":标题卡、淡出、界面镜头、淡出、功能卡片、淡出、Logo。每一镜单看都还行,连起来就是翻 PPT——因为画面之间没有任何联系,后一个场景只是把前一个替换掉了。onetake 这个开源项目要解决的正是这个问题。它是一个 Claude Agent Skill(面向 Claude Code / Codex 等 Agent 的技能包),目标不是生成"幻灯片式动效拼接",而是产出一条 10–30 秒的"一镜到底"产品短片,并且把"连贯"这件事从审美直觉变成了可测量、可验收的工程协议:全片是一个 HTML 合成文件,每个像素都是时间的纯函数,在任何人观看之前先过一个自动"神谕"(oracle)判定。本文直接拆它的仓库源码,看这套协议到底是怎么把"连贯"量化出来的。
从"PPT 式拼接"到"全片一个 HTML 合成文件"的形态跃迁
先说清问题到底出在哪。onetake 自己的第一支片子 motion-web(cases/motion-web-15s/README.md)被砍了两刀,这两刀几乎就是今天所有 AI 动效视频的通病:
- v1(被否,理由「PPT 式一个一个展示」):九张页面卡片摆在一个 2.5D 场里,镜头每 0.74 秒推过一张。测量结果是:0.02 帧静止、镜头从未停过、每张卡片停留时间完全相同。镜头移动让它"感觉像画廊巡游"——而画廊巡游就是带推轨的幻灯片。
- v2(被否,理由「没有节奏变化 依然是 PPT」):改成七张页面全帧展示,每张 1.55 秒,手绘光标、推入、弹簧落定都做了。可七段等长的镜头配上同一个"切入-推近-标签"的形状,依然是七张会动的幻灯片。没有停顿、没有爆发、没有故事,是个目录。
- v3(通过):节奏曲线有了形状——三个词 0.4 秒/个弹出、字标字母下落压扁、提示卡 38 cps 打字、构建面板 7 行逐个勾选、三页真实页面倾斜飞过(各 0.9 秒构成一个爆发段)、四个微交互卡片重叠入场、浏览器窗口 1.15 秒的慢推静场、结尾 0.22 秒/个的连续硬切、黑场收尾。镜头长度从 0.22 秒到 2.2 秒,CV(变异系数)0.78。
结论被写进了仓库的硬规则(SKILL.md):永远不要在节拍器上切镜头,等长镜头不管单镜多好都读作幻灯片。所以它拒绝"视频编辑器"这个形态,改走"合成文件"路线:一条片子就是一个 HTML 文件(模板见 templates/comp.html),时间线就是文件顶部的一组数字常量:
const FPS=30, DUR=15; const W1=[0.15,0.50,0.92], W1_OUT=1.55; const PROMPT_IN=2.95, TYPE0=3.15, TYPE_CPS=38, CLICK=4.45, PROMPT_OUT=4.75; const STAC=[[13.25,'motion'],[13.50,'is'],[13.72,'the'],[13.94,'material']], STAC_END=14.3;配合这套形态的是一份明确的浏览器运行时契约(references/composition.md):
window.__seek = async (t) => { ...; return true; } // 为时间 t 设置每一处样式;图片解码完成后 resolve window.__ready = document.fonts.ready.then(() => { measure(); simulate(); return seek(0); }); window.__meta = { dur: 15, fps: 30 }; window.__track = (t) => ({ hand: [x, y], card: { x, y, w, h, op } }); window.__motion = (t0, t1) => px; // 渲染器据此为该帧计算快门采样数产品界面不用录屏,而是按截图用 HTML 重建、放在镜头平面上(lib/ui_kit.js),所以镜头可以推到任意缩放都清晰;动效全部来自动作库 lib/motion.js 里 39 个实测过的动作(弹簧、入场、衔接、碰撞、物理模拟、镜头、流体光场),速度曲线都测量过,禁止手写固定常量的缓动。到这一步,形态已经从"剪辑软件里的时间轴"跃迁为"一段可 seek、可测量、可自动检查的代码"。
每个像素都是时间 t 的纯函数意味着什么
形态跃迁只是第一步,真正撑起整套工程的,是__seek(t)的纯函数要求:用 6.4、2.0、再 6.4 调用,得到的必须是同一帧。为此不允许跨调用累积状态,弹簧全部写成闭式解;真正需要积分的物理(软体、Verlet 串、依赖手部历史的磁吸)在__ready里以固定种子在 240 Hz 预模拟一次,之后按时间索引采样——OM.sim.*就是这么实现的。渲染路径上禁止Math.random(),一切随机走OM.rng(seed)。纯函数的直接红利是:
运动模糊从"滤镜"变成了"积分"。截图是瞬时,而摄像机是快门张开期间积分。render.py 对每个运动帧在 180° 快门(半个帧间隔,以帧时刻为中心)内多次 seek 采样、在线性光下平均;先采两端,若完全一致则判定为静帧、直接写出。注释里有一个被验证过的数字:一个 100 px 盒子以 167 px/帧 移动,留下 173 px 拖尾加 27 px 实心核——正是 180° 快门该有的结果。而因为__seek是纯函数,这是真实的积分,不是blur()滤镜。成本是可数的:每次捕获约 0.2 秒(156 ms 是 Chrome 截图编码,canvas 绘制不到 1 ms),所以 render.py 用--workers多开 Chromium 并行,草稿 1080p30、验收通过后才出 4K60。
每个动作都是一组"数字进、数字出"的纯函数,DOM 和 Canvas 通用。以 carry 类的zoomThrough为例(references/motion-library.md):
const z = OM.zoomThrough(t, 6.3, 7.3, card); // 矩阵型动作:两边都是同一组数字 g.setTransform(...z.matrix); // canvas layer.style.transform = `matrix(${z.matrix.join(',')})`; // DOM,transform-origin: 0 0时间全部是绝对秒,所以时间线就是文件顶部的一列数字;这也让探针成为可能——scripts/probe.py 在页面脚本运行前注入CanvasRenderingContext2D的钩子,追踪每一次drawImage/fillText/fill,配合 DOM 的变换矩形与有效透明度,无需任何注解就能读出每一帧画面上有什么、每个元素怎么移动,并输出每个动作的 t80(走过 80% 路程所耗时长占比,0.23 是 expo 急停,0.67 是对称 S 曲线)与峰值像素/帧。
纯函数还给了镜头"操作员手感"以确定性的实现:OM.camera(t, keys)键控镜头,但"像有人扛着"来自键的摆法——在sineInOut上提前约 0.1 秒键主体来"跟随",在expoInOut上键落点来"甩",加一层双正弦慢浮动的"手"(静场时归零),每次落地加OM.shake。onetake 第一支概念片 one-dot 的教训就写在 cases/one-dot-15s/README.md:固定镜头版被评「就缺灵动的跟踪运镜」,换成会追的主体镜头后才拿到「运镜、镜头和物理都没问题」。
节奏阈值、自动验收与"每一拍从上一拍长出来"的工程闭环
量化不是目的,用阈值建自动验收才是。整条流水线是"先测量、再创作"的闭环:
输入端,参考片先拆成数字。scripts/analyze_ref.py 把参考片转成能量图——每 1/12 秒一个字符(.:-=+*#%@,从静止到硬切),同时输出静止比例、切点、爆发段窗口、最长安静段,以及光流指纹(t80、smear 是否 < 0.78 判定是否带快门模糊)。motion-web 参考片测出的数字是:34% 帧完全静止、两簇共 7 个硬切、元素始终小且留白大——"这些看一遍根本看不出来,不测量就动手的片子连续两次落在均匀节奏上"。
输出端,scripts/verify_promo.py 是那个"神谕",每个腿都打印数字并给出 PASS / WARN / FAIL:
- cadence:镜头长度变异系数 ≥ 0.25,等长镜头=幻灯片;
- rest:≥ 25% 帧在 320 px 尺度下死静,且有一段 ≥ 1.0 秒的安静;"慢推不是静场"——clearing 第一版每个静场都带 1→1.05 的推,rest 腿 0.178 直接 FAIL;
- burst:有参考片时,1.5 秒窗口内 ≥ 3 次大变化才算爆发段;
- audio:峰值 ≤ −3 dBFS、0 个削波样本、≥ 15% 的 1/12 秒帧安静;
- continuity:carry score < 0.5 判 FAIL、< 0.7 警告;
- curves:无快门模糊的片子里有元素超过 80 px/帧(归一化到 1920 宽)即 FAIL;
- framing:
__meta.inFrame声明的主体任何一帧出画即 FAIL——one-dot 第一次镜头渲染把点甩丢了,就是这个腿抓出来的。
其中 carry score 是整套协议的核心。probe.py 把"交接处"定义为 0.4 秒内画面主体身份大规模更替的窗口,然后对每个交接判定三种结果(references/rhythm.md):carried(有东西活下来并移动 ≥ 2% 画面对角线或缩放 ≥ 10%)、anchored(唯一幸存者是 ≥ 3% 帧面积的大块头且未动)、bare(什么都没活下来)。爆发段硬切、最后 1.2 秒片尾、__meta.cuts声明过的切点可以豁免。得分 = (carried + anchored/2) / 计入的交接数。
这条腿的历史战绩最有说服力——Pocket Weather Club 三版,节奏腿全部通过,只有连贯腿把版本分开:
| 影片 | 结果 | carry score | carried · anchored · bare |
|---|---|---|---|
| motion-web 第三版 | 通过 | 0.56 | 5 · 0 · 4 |
| Pocket Weather v1 | 被否 | 0.00 | 0 · 0 · 2 |
| Pocket Weather v2 | 前 6 秒保留 | 0.40 | 2 · 0 · 3 |
| Pocket Weather v3 | 通过 | 0.75 | 4 · 1 · 1 |
| onetake 发布片 | 通过 | 0.83 | — |
v1 的节奏、静止、爆发、音频全过,但每一拍都是独立场景、展示一幕就把其余display:none——probe 量出两个交接全是 bare,0.00。这就是"连贯是量出来的"最干脆的证明:节奏不能解释的否决,连贯分数能解释。
"每一拍从上一拍长出来"也因此落成了动作库里的具体函数(references/motion-library.md):被按下的东西变成结果(morphRect:矩形 A 变成矩形 B)、主体打开自己的世界(iris:从一点张到全帧)、一张卡片被推穿就是下一场(zoomThrough)、全体收拢成标志(gather→seal)。pith 案例把这条做到了极致(cases/pith-21s/README.md):从一枚磷光像素开始,像素在光标里、光标在提示行里、提示行在输出流里,窗口的边框是绕着早已存在的输出流画出来的——440× → 0.52× 一路只出不进,没有一次替换,probe 测出 continuity 1.00、3 个交接全部 carried。one-dot 同理:一颗点走完全片,每处交接(点变光标、光标变四个点、点吞掉提示、变成环、环展开成弹簧曲线、曲线变成球、球落地打开影片)都靠构造保证 carry,6 个交接 1.00。
闭环的最后一块是反馈回路:每个案例目录(cases/)都同时收成片和拆解文档——概念、节拍表、数字、被否掉的版本和原因。失败的版本不是废案而是标定数据:v1 的 0.00、v2 的 0.40 校准了 0.5 的阈值;motion-web 0.56 通过但带着四个 bare 交接,注释里写明"carried 会更好";__meta.cuts、probe 的两处盲点(单一连续世界里交接太少、rest 腿在深色地面上的过读)也都作为已知边界写进了文档。下一次创作从这些数字开始,而不是从感觉开始。
至此,onetake 把"一镜到底"这个电影感词汇完整翻译成了工程语言:一条片子 = 一个纯函数合成文件 + 一组实测动作 + 一把测量探针 + 一台自动验收神谕。参考片拆成能量图,节拍表同时约束节奏与衔接,渲染带真实快门积分,声音放进同一个混响房间,验收在人类观看之前完成。连贯不再是一种说不清的"感觉对不对",而是一组可以打印在验收报告里的数字——这正是它与所有"AI 模板生成器"的分水岭:别的工具给你动画,它给你一条可测量的、不会把观众带回 PPT 时代的连贯性协议。
【免费下载链接】onetakeMotion films that never cut to the next slide: every beat grows out of the one before, one continuous camera, continuity measured by an oracle. Product launch films and feature demos, made with a coding agent.项目地址: https://gitcode.com/gh_mirrors/on/onetake
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考