跳到正文
Hacker News· mr_o47·· 5 天前AI 评分62

Claude Opus 5.5 如何纯用代码做出一部定格动画

How Opus 5.5 Made a Stop-Motion Purely Using Code

AI 导读

作者把一条 Reddit 提示词交给 Claude Code 中的 Claude Opus 5.5,让它以 Docker 容器为主题,纯用 JavaScript 在 HTML canvas 上绘制,自主完成了一部 57 秒的纸艺定格动画,全程 38 分钟、75 次工具调用、零提问,付费 API 花费约 0.15 美元。

正文 · AI 翻译

Reddit 上有人分享了一个用 Claude 制作动画解说视频的提示词。我用一个自己很熟悉的主题——Docker 容器——试了试。我把它粘贴进 Claude Code,填入语音和音乐模型的 API 密钥,然后离开了工位。

等我回来时,一部 57 秒的成片已经做好了。Claude Opus 5.5 写好了脚本,用代码画出了每一帧,指导了旁白,剪辑了音乐,合成了音效,完成了混音,渲染了视频,还让第二个模型做了审阅。整个过程耗时 38 分钟。付费 API 调用花了 0.15 美元。

里面没有任何图像文件。每一处纸张纹理、撕边、角色和道具,都是 JavaScript 在 HTML canvas 上画出来的。

这部片子不错,但最让我印象深刻的不是它,而是会话日志。这个模型听不见,也看不了视频。它却仍然交付了一版干净的混音,还发现了自己草稿里的两处事实错误,因为它找到了一种方法去检查每一步。这篇帖子的大部分内容讲的就是这个。

一个听不见的模型仍然做出了干净的混音,因为它测量了每一个声音,而不是凭感觉相信它。

提示词

这是我发送时的提示词,去掉了 API 密钥。我唯一写的部分就是主题。

Create a pure javascript animation. 30s-60s stop motion paper craft style video
with appropriate audio on the topic what is docker containers and how they work

Entire video should be as high of a production value as possible. Please spend
your time on this, it's very important.

Use high quality text-to-speech model for generation. You can find open router
API key in .env file

You can use any tools you can find access to and resources on the internet. You
create the script, the assets, the animation, concept, everything.
I have to go away from my computer so please work autonomously until done.
Quality is paramount. Production value should be on professional level.

One more thing: max OpenRouter spend is $6.99

这是个很宽松的提示词,我觉得这正是它奏效的原因。“纯 JavaScript 动画”选定了媒介,所以模型只需要一个浏览器,别的什么都不用:不需要图像生成器,不需要视频模型,不需要设计工具。“你来创造一切”把每一个创作决定都交了出去。“自主工作直到完成”意味着它不该等我,而它确实没等。它进行了 75 次工具调用,问了零个问题。支出上限的重要性超出了我的预期,我后面会再谈。

成品

影片开场是一位纸片开发者坐在桌前。笔记本电脑上写着“在我机器上能跑!”应用跳到了服务器上,那里 Python 版本不对,缺了一个库,还有一个没人记录下来的设置也不存在,于是它被撕成两半。Docker 的解决办法是把应用和它需要的一切都打包进一个货运集装箱。接着它讲了一个由 Dockerfile 分层构建的镜像,docker run 批量生成一模一样的容器,容器共享一个宿主机内核,命名空间和控制组充当墙壁和资源份额,以及一个所有机器都从中拉取的注册表。影片结尾是一只纸鲸鱼驮着这些容器,上方是标题。

The paper developer at the desk, next to a laptop and a speech bubble that says “It works on my machine!”

事后我读会话日志时,工作的顺序让我意外。我以为它会先开始画画。结果它做的第一件事是旁白。它写了一个 12 行的脚本,录了一版测试,检查了一遍,又录了两版,并锁定了每一行播放的确切秒数。直到那时它才生成音乐,而只有在音乐剪好之后,它才开始处理画面。画画占了 38 分钟里的约 15 分钟。其余时间都花在声音、检查和渲染上。

纸张质感是怎么做出来的

整部影片就是一个 HTML 页面。一个函数 renderFrame(ctx, t) 负责绘制任意时间 t 的完整帧:它找到拥有那个时刻的场景,把它画出来,然后加上暗角、轻微的曝光闪烁和胶片颗粒。

纸张精灵

每张纸片在启动时都会被绘制一次,画进它自己的离屏画布中。paper() 函数用纯色填充一个形状,在上面铺一层纸张纹理,从左上角加入柔和的光线,然后切出一个斜面:将形状的副本偏移 1.6 像素,使一条边缘受光,另一条边缘落入阴影。正是这个斜面让一个扁平形状看起来像一张有厚度的卡片。纹理也是生成的,是分层噪声,上面还画了 1,500 条短“纤维”,所以没有纹理文件。

剪刀永远不会剪出直线,所以没有哪个形状是干净的。wobble() 沿着多边形的每条边行走,用会漂移而非跳变的噪声把点向内或向外推:

js

// hand-cut wobble along a closed polygon (scissors are never perfect)
P.wobble = function (pts, seed, amp = 1.2, step = 14) {
  const R = P.rng(seed), out = [];
  let d = 0;
  for (let i = 0; i < pts.length; i++) {
    const a = pts[i], b = pts[(i + 1) % pts.length];
    const len = Math.hypot(b[0] - a[0], b[1] - a[1]), n = Math.max(1, Math.ceil(len / step));
    const nx = -(b[1] - a[1]) / (len || 1), ny = (b[0] - a[0]) / (len || 1);
    for (let k = 0; k < n; k++) {
      d = d * 0.6 + (R() - 0.5) * amp;   // drift, not jitter
      const u = k / n;
      out.push([a[0] + (b[0] - a[0]) * u + nx * d, a[1] + (b[1] - a[1]) * u + ny * d]);
    }
  }
  return out;
};

所有随机性都来自带种子的生成器,所以集装箱门在每一帧里都有同样参差不齐的边缘。

让它感觉像定格动画

定格动画有两个破绽。画面是一步一步移动的,而不是平滑移动;而且每个部件在镜头之间都会轻微偏移,因为有人用手把它放回了原位。代码把这两点都伪造了出来。

时间被对齐到每秒 12 张画稿,每张画稿在 24 fps 的视频中显示两次。动画师把这称为“二拍一”拍摄。

js

P.FPS = 12;
P.step = (t) => Math.floor(t * P.FPS + 1e-6) / P.FPS;  // quantize to the grid
P.frameOf = (t) => Math.floor(t * P.FPS + 1e-6);

然后每个部件都会得到一个微小的偏移,每张画稿都会变化。它来自画稿编号和部件 ID 的哈希,所以看起来随机,但每次渲染出来都一样:

js

P.jit = (frame, id) => P.hash(frame, id) * 2 - 1;  // [-1, 1], new each drawing

P.drawJ = function (ctx, sp, x, y, r, s, f, id, amt = 1) {
  P.draw(ctx, sp,
    x + P.jit(f, id) * 0.9 * amt,          // under a pixel of drift
    y + P.jit(f, id + 0.5) * 0.9 * amt,
    (r || 0) + P.jit(f, id + 0.25) * 0.006 * amt,
    s || 1);
};

不到一个像素的移动听起来好像没什么。但它带来的差别之大让我惊讶。没有它,影片看起来像矢量动画。有了它,看起来就像有人坐在桌边摆弄纸片。

运动本身只是带缓动的普通关键帧。这两行是集装箱门甩开又砰地关上,以及图像图层带着一点过冲地拉开:

js

const open = K(T, [[14.2, 0], [14.42, 1, 'out'], [15.4, 1], [15.62, 0, 'in']]);
const gap = K(T, [[20.36, 0], [20.66, 38, 'back'], [21.5, 38], [21.8, 0, 'inOut']]);

因为 T 已经对齐到 12 fps 的网格,所以即使是这些平滑曲线也会以步进方式呈现。开发者是一组用“黄铜开口销”连接的关节纸部件,通过让每个部件绕其销钉旋转来摆姿势,就像一个真正的纸偶。

A Dockerfile next to the image, built as four paper layers

来自一个听不见声音的模型的声音

在会话早期,Opus 5.5 写道:“我无法聆听音频。”然后它围绕这一点设计了每一个音频步骤。

旁白来自通过 OpenRouter 调用的 Gemini 3.8 Flash TTS。模型没有直接发送脚本原文。它在前面写了一份配音导演简报,细致到单句的演绎方式:

# AUDIO PROFILE: The Explainer
A warm, clever science-show host in their thirties. Bright, friendly, quietly
witty. A real human voice actor, not an announcer.
...
Beats: The first line, "It works on my machine.", is a smug, sing-song
impression of a proud developer. A comic drop on "and breaks." ...
A playful, knowing question on "So... does it work on your machine?"

它录制了三条,并在我的机器上用 faster-whisper 逐条处理。转录得到的词级时间戳告诉它每句话确切从哪里开始、到哪里结束,它用这些把最好的一条剪成若干行,并把每一行放进一个 timeline.json:

json

{ "id": "breaks", "src": "ach_0", "in": 5.94, "out": 6.55, "at": 7.0,
  "text": "and breaks." }

然后每个场景都按这些时间进行动画。画面跟随声音,这正是人类剪辑师会采用的做法。

音乐是来自 Lyria 3 Pro 的一段 60 秒配乐。提示词要求了精确的结构:前七秒是一段张扬的连复段,代码崩溃时是一次坍塌,12 秒处有一个“啊哈”,结尾是一个按钮式收束。Lyria 没有踩中这些时间点。模型用两个工具处理了这个问题。它把配乐的一小段 MP3 发给 Gemini Flash,要求给出一份带时间点的段落和剪辑点列表,花费不到一美分。然后它在本地用 librosa 找出了节拍网格。有了这两者,它把配乐剪成七段并重新排列以适配影片。每一次剪切都落在强拍上。一次磁带停转效果落在“breaks”上,最后一下重击落在片尾标题上。

这48个音效是用 NumPy 和 SciPy 合成的:纸张轻拍、爆裂声、撕裂声、容器闷响、门吱呀声、橡皮图章、打字声、剪刀声、海浪、海鸥和鲸鱼喷水。一个提示表将其中248个放置在动画时间点上。这三个与上面的门关键帧对齐:

python

ev(14.2, 'creak', -20, -0.2)       # the doors start to open
ev(15.4, 'creak', -24, -0.2)       # the doors start to close
ev(15.62, 'clank', -11, -0.2)      # the doors shut

混音脚本执行通常的混音工程师工作:对人声进行均衡和压缩,音乐在语音下闪避,音乐中频段下陷以保持词语清晰,房间底噪,以及限制器。它如何在没有耳朵的情况下检查结果,详见下文。

渲染

播放影片的页面也负责渲染它。在 URL 中加入 ?render=1 后,它会跳过播放器并暴露一个函数 window.renderAt(t)。一个简短的 Puppeteer 脚本在无头 Chrome 中打开页面,并为每个绘图保存画布:

js

for (let k = n0; k < n1; k++) {
  const url = await page.evaluate((t) => {
    window.renderAt(t);
    return document.getElementById('film').toDataURL('image/png');
  }, (k + 0.5) / 12);
  save(`f_${String(k).padStart(4, '0')}.png`, url);
}

大约一分钟内绘制了690张图。ffmpeg 以12 fps读取它们,将每张加倍到24,添加混音,并编码为 H.264。由于不依赖挂钟时间,慢帧不会从视频中丢失。

它如何在没有我的情况下工作

Claude Code 给模型提供了一个 shell、我的文件系统以及查看图像的能力。这并不多。让38分钟无人值守工作成为可能的是它如何使用这些。

它按依赖顺序构建事物

电影中几乎所有东西都依赖于时间。你不知道台词何时被说出就无法为场景制作动画,而你不知道这一点直到台词被录制。因此模型先处理脚本、旁白、锁定时间线,然后根据该时间线进行音乐编辑和场景制作,再在动画之上添加音效,最后混音和渲染。当它锁定时间线时,它指出“画面和音乐提示都遵循这个时间线。”后来没有任何东西需要重做,因为之前的东西移动了。

它在花费之前先估价

6.99美元的上限是针对整个密钥的,而我已经在其他工作上用掉了大部分。大约还剩0.20美元。在第一次旁白录制后,模型询问 OpenRouter 那次调用花费了多少以及密钥使用了多少:

bash

# simplifiedcurl -s "https://openrouter.ai/api/v1/generation?id=$GEN_ID" \  -H "Authorization: Bearer $OPENROUTER_API_KEY"     # total_cost: 0.0140795curl -s https://openrouter.ai/api/v1/key \  -H "Authorization: Bearer $OPENROUTER_API_KEY"     # usage: 6.817

然后它计划:每次录制0.014美元,还有空间做一个音乐提示和大约四次录制。它用了两次。一旦音乐完成且还剩0.06美元,它停止进行付费调用,除了一个,即对草稿的0.018美元审查,它理由是它听不到自己的混音。其他一切都在我的笔记本电脑上免费运行。

项目成本
三次旁白录制(Gemini 3.8 Flash TTS)$0.042
一个音乐提示(Lyria 3 Pro)$0.080
音乐描述(Gemini 3.8 Flash)$0.008
视频审查(Gemini 3.8 Flash)$0.018
总计约0.15美元

这不包括 Claude Code 使用本身,那是分开的。

它对我含糊其辞的事情做了决定,并告诉了我

再次阅读我的提示,“OpenRouter 最大支出是6.99美元”是模糊的。我是指这部电影,还是指密钥?已经用了6.80美元,这些是非常不同的预算。一个停下来问的模型会一直坐在那里直到我回来。这个模型采取了谨慎的解读,完成了工作,并在最终报告中说明了假设:它将6.99美元视为密钥的总计,如果我是指每个项目,那么还有空间进行更多录制和音乐选项。

这就是我希望任何无人值守工作的行为。采取安全选项,继续前进,并告诉我你假设了什么。

它一路做笔记

在步骤之间,它会写一两句话,说明自己发现了什么、接下来要做什么,比如“第1到第5个场景读起来很清楚。后续要修的地方:图层标签在右边缘被切掉了,相邻的集装箱门有重叠。”这给了它一份持续更新的待解决问题清单,也给了我一份可读的日志。这篇帖子的大部分内容都来自这些笔记。

它最后还附上了一份README:脚本、带时间点的场景列表、每个部分的制作方式、花费,以及重建这部影片的确切命令。它提到frames文件夹有2.3 GB,可以安全删除,还说我应该轮换API密钥,因为我把密钥粘贴到了聊天里。

它如何检查自己的工作

这是我一直反复琢磨的部分。这个模型听不见声音,只能看到静态图像,所以它没法观看自己做的影片。它没有放弃这些检查,而是为每一项都找到了替代方案:用转录文本检查台词,用节拍检测器和文字描述检查音乐,用算术检查咔嗒声,用响度计检查平衡,用静帧拼图检查场景,再用另一个模型检查整部影片。

每个场景之后的静帧

一个小脚本可以在任意时间点列表上渲染影片,并把帧拼成一张图,render/sheet.jpg。每完成一个场景文件,模型就渲染一张拼图并查看。这样进行了五轮,修掉了一些我自己也会去修的问题。开发者在桌边显得太小,所以它把人物从2.5缩放到3.2。图层标签跑出了右边缘,所以它把图层堆叠向左移。集装箱门与相邻的门重叠,所以它把门打开到60%,而不是完全打开。登记处的笔记本电脑尺寸不对,所以它画了一台更小的,并加上了“我的笔记本电脑”标签。它把台灯换成了笔筒,把片尾卡上的鲸鱼抬高,并把喷水孔画得更大。有一个标签写着“starts in ~1 s”,它改成了“starts in seconds”,以与旁白一致。

A contact sheet of six stills from the image and kernel scenes, used for review

逐词检查

每一段旁白录音在使用前都经过了Whisper。第0条与脚本逐词吻合。第1条说的是“Every developer has set it”,所以被淘汰了。

混音之后,它转录了最终的语音轨,以确保剪辑没有剪掉任何词,而Whisper又听到了“set”。这可能意味着剪辑切掉了“said”的结尾,也可能只是Whisper听错了。我喜欢它接下来的做法。它把那一行在三个略有不同的边界处分别剪出来,并对每一段做词级置信度转录。三段都返回了“said”,置信度为1.0,所以剪辑没问题,整轨转录才是错的。

用算术听咔嗒声

糟糕的音频剪辑会表现为咔嗒声:相邻两个采样之间出现突然跳变。在七处音乐剪辑的每一处,模型都测量了最大跳变,并与整条音轨99.9百分位的跳变进行比较。最差的一处剪辑为0.0436,而正常上限为0.0498,所以没有一处特别突出。它还测得最终混音为-15.5 LUFS,峰值为-2 dB,这对在线视频来说是正常水平。

第二个模型,它检查了其笔记

对于没有任何测量手段能评判的一件事——整部影片,它把草稿1缩小成1.5 MB、640像素的副本,发给了Gemini 3.8 Flash,后者可以观看和收听视频。它要求以“资深动画导演兼混音师”的身份进行评审:一份按优先级排列的真实问题清单,并附上时间戳。

这次审查发现了两个真实的技术错误。影片中的 Dockerfile 在安装库之前就复制了代码,这是错误的顺序,因为每次代码改动都会重新安装软件包,构建缓存就不再起作用了。影片还说容器“没有操作系统”,这是一个常见的误解。容器的基础镜像中确实带有操作系统用户空间。它们省略的是自己的内核。模型把这两处都修正了:Dockerfile 和分层动画现在先安装库,而那块板现在写着“FULL OS: own kernel · drivers · boot”,并带有一个标签“no kernel of its own”。它还采纳了三条音频意见:“breaks”下方刺耳的嗡鸣声、12 秒处音乐回归得太突然,以及句子之间忽高忽低的闪避效果。

然后它对其中一条提出了反驳。审查者说音乐在 7.5 到 12 秒之间降到了“几乎完全静音”。模型测量了每一段的电平,发现那一段与其余部分处于同一电平,于是称这条意见不准确。同样的数据也显示整段配乐在语音下方偏低,所以它还是把音乐提高了 1.5 dB。

我认为这是整个会话中最有用的习惯。它用第二个模型来做判断,但对于任何可以测量的东西,它都不轻信第二个模型的说法。

Isolation: three containers in their own rooms, with namespaces and control groups

测试它要交给我的东西

在最终编码之后,它并没有止步于自己渲染出的 PNG 帧。它从完成的 MP4 中抽出四帧来查看,这检验的是编码本身。然后它在无头 Chrome 中打开了实时 index.html,计时了 57 次帧绘制,检查了音频长度,点击了播放,并确认时间在走动。没有页面错误,帧时间中位数为 0.1 ms,最差情况为 6.2 ms(每秒绘制 12 次,每次有 83 ms),音频时长 57.5 秒,点击后片刻播放器位于 1.4 秒处。直到这时,它才写下 README 并告诉我完成了。

自己动手做

你需要 Claude Code with Opus 5.5、Node.js、Google Chrome 和 Python 3。在 Node 这边,puppeteer-core 和 ffmpeg-static 是仅有的软件包。在 Python 这边,它使用了 numpy、scipy、soundfile、pyloudnorm、librosa 和 faster-whisper。你还需要一个带有支出限额的 OpenRouter 密钥,用于语音、音乐和审查模型。几美元就够做几部影片了。

Reddit 上的提示词原样就能用。读完日志后,这是我现在会用的版本。它保留了开放式简报,并明确写出了让这次运行成功的那些习惯,这样你就不必依赖模型自己想到它们:

Create a pure JavaScript animation: a 30-60 second paper-craft stop-motion
explainer video, with narration, music, and sound effects, on the topic:
<YOUR TOPIC>.

Production value is the priority. You own the concept, script, art direction,
animation, and sound. Work autonomously until the film is done. Do not stop to
ask me questions.

Constraints:
- Draw everything in code on an HTML canvas. No image files.
- The OpenRouter API key is in .env. Never print it. Total paid spend: max $3.
- Lock the narration timeline before you animate. Animate to the voice.
- Check every narration take with local speech-to-text before you use it.
- After each scene, render stills into a contact sheet and inspect it.
- Get one review of the draft from a video-capable model. Verify each note
  before you act on it.
- Keep every earlier version. Do not delete files you did not create this session.
- Finish with a README: what the film shows, how it was made, the spend, and
  the exact commands to rebuild it.

Output: a 1920x1080 24 fps MP4, and an index.html that plays the film live.

其中两行来自错误。我把 API 密钥直接粘贴到了提示词里,所以它现在以纯文本形式留在会话日志中,这样的密钥必须被视为已泄露。把它放进 .env 并说明这一点。而且一个独自工作的代理在整理时可能会删除旧的渲染文件,所以要告诉它保留这些文件。你会想要比较草稿的。

还有几件事我想转达。从 30 秒开始;短片检查起来更快,也能很快告诉你这种风格是否适合你的主题。选一个包含具体物体的主题。Docker 有盒子、层、服务器和一头鲸鱼,我怀疑抽象主题会难画得多。让模型把纸片引擎放在它自己的文件里,这样下一部影片就能复用它。即使你预算有余,也要保留支出上限,因为它让模型在调用之前先为调用定价。还有,在看 MP4 之前先打开 index.html。实时页面让你可以用方向键拖动浏览,这是检查时序最快的方法。

The registry: a laptop, a server, and a cloud machine pull the same image

仍然需要人来做的部分

这个声音是 AI 配音。它很不错,喜剧节奏也拿捏到位,但你能听出来。这个模型也分辨不出一个笑话是否好笑。它的检查能发现错词、咔嗒声和糟糕的电平,却抓不住一个冷掉的包袱。

简报和模型同样重要。风格、长度和主题都来自提示词,片子好不好,全看这些选择。

而且你仍然需要核实事实。初稿把关于 Docker 的两处内容搞错了。这次是第二个模型发现了它们,但我不会指望这个。发布之前,把成片从头到尾看一遍。

来源:Hacker News · muhammadraza.me