61 秒全片
61.000s / 1830f开场打字 → 11 轮风格展示(每轮 1 秒大字卡 + 3 秒展示)→「还有更多?」→ 八连闪 → 电影感落版。30fps、1830 帧、2-pass 3.4Mbps,音画互相关偏移 0.00ms。
视频为网页演示用 720p 转码;原始成片 1920×1080。
CODE → FRAME → BEAT
19 种视觉风格、1830 帧画面、120 BPM 配乐。 没有提示词生成模型,也没有素材库——每一帧都由 HTML / Canvas / SVG / Three.js 逐帧渲染, 每一秒音乐都由 numpy 从零合成。
02 / PIPELINE
核心原则只有一条:画面是时间的纯函数。给定 t,渲染结果永远一致,所以可以逐帧拆开、并行重渲、精确对拍。
每个片段一个 HTML,暴露 window.DURATION 与 window.renderFrame(t)。没有 rAF、没有 Date.now、没有 Math.random——随机数走带种子的 mulberry32,同一帧可以无限次复现。卡点冲击(白闪 0.85→0 / 缩放 1.13→1 / 抖动 14px / 推镜 1→1.05)由 common.js 统一注入,风格段左上角自动生成胶囊标签。
无头 Chromium 打开页面,evaluate(renderFrame(i/30)) 之后经 CDP 截 JPEG(q95),直接通过 image2pipe 管道交给 ffmpeg 编码 libx264 crf16——不落盘、不丢帧。WebGL 页面固定 SwiftShader 软渲染并开启 preserveDrawingBuffer,保证每帧都能截到。
120 BPM、A 小调、不采样。kick、snare、clap、808、超级锯齿、古筝、笛子等 15 类音色全部用 numpy/scipy 合成;每段风格配专属乐器。底鼓包络驱动 55% 侧链闪避,2.2 秒合成混响,tanh 软削波,最终归一化到 -0.6 dBFS。
26 段按剪辑表 concat,2-pass 3.4Mbps 输出 H.264 + AAC。每段 render 后 ffprobe 核对帧数,合计必须是 1830;成片音轨与母带互相关校验偏移 0.00ms;全片抽 1fps 接触表复核结构。
03 / SCORE
120 BPM → 1 拍 0.5 秒。11 轮展示里,每段 第 1 帧就是“啪”的卡点,之后每 0.5 秒一个动作;配乐与画面共用同一组时间常数。
t0 / speed:片段模式把片段时间 u 映射为场景时间 t = t0 + u × speed。
配乐卡点:每轮第 1 拍 kick+tom,第 2 拍 kick+clap+sub boom+whoosh;展示段首帧 6 层大卡点(kick+clap+snare+crash+sub boom+高频爆裂)。
04 / STYLES
11 段 3 秒展示 + 8 个 0.25 秒闪现。每一张都是渲染管线直接输出的帧,点击可放大看细节。
01
02
03
04
05
06
07
08
09
10
11
F1闪
F2闪
F3闪
F4闪
F5闪
F6闪
F7闪
F8闪05 / THE PROMPT
规格、剪辑表、11 段分镜、配乐设计、质量检查——一份可直接交给代码 Agent 的任务书。
这是本片最初的任务提示词,完整保留。它不描述“感觉”,而是把能数字化的部分全部钉死:1920×1080 / 30fps / 1830 帧 / 120BPM、每段 t0 与 speed、每个风格的技术要点、配器与和弦、以及一条条质量检查。
原文品牌为 Opus 5.5;本项目 v2 已整体替换为 DeepSeek V4.1-Flash,其余规格与分镜完全一致。
复制后可直接开一个新会话,把整份提示词交给代码 Agent;建议一次只做 2–3 个片段,先出接触表再整段渲染。
06 / CODE
卡点封装、逐帧渲染、配乐侧链——工程里最关键的三个函数,原样摘录。
// common.js —— 片段模式:u 是本片时间,t = t0 + u * speed function applyFrame(u) { const t = t0 + u * speed; wrap(t, u); // 卡点冲击:白闪 0.85→0(0.12s) / 缩放 1.13→1(0.32s outCubic) / 抖动 14px(0.16s) if (o.shock && u < 0.45) { const f = Math.max(0, 1 - u / 0.12); flash.style.opacity = (0.85 * f * f).toFixed(4); sc *= lerp(1.13, 1, ease.outCubic(clamp(u / 0.32))); const shake = 14 * Math.max(0, 1 - u / 0.16); } // 之后整段缓慢推镜 1 → 1.05 if (o.push) sc *= 1 + 0.05 * ease.inOutSine(clamp(u / duration)); stage.style.transform = `translate3d(${dx}px,${dy}px,0) scale(${sc})`; }
// 逐帧:不落盘,JPEG 直接推进 ffmpeg stdin for (let i = 0; i < frames; i++) { const t = i / fps; await page.evaluate((tt) => window.renderFrame(tt), t); // 等两帧合成,确保 CDP 截到刷新后的 DOM / canvas await page.evaluate(() => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)))); const shot = await cdp.send('Page.captureScreenshot', { format: 'jpeg', quality: 95 }); const buf = Buffer.from(shot.data, 'base64'); if (!ff.stdin.write(buf)) await new Promise((r) => ff.stdin.once('drain', r)); }
# 底鼓:正弦 120→45 Hz 扫频 + 高频 click + tanh def synth_kick(f0=120.0, f1=45.0, dur=0.5, tau=0.13): n = smp(dur); t = tvec(n) f = f1 + (f0 - f1) * np.exp(-t / 0.030) ph = 2.0 * np.pi * np.cumsum(f) / SR body = np.sin(ph) * xenv_ad(n, 0.0015, tau) click = hp(RNG.standard_normal(n), 2500) * np.exp(-t / 0.0035) * 0.30 return fade_tail(norm_peak(np.tanh(1.6 * (body + click)), 0.98), 8) # 侧链总线:每个 kick 触发 55% 闪避,严格整数样本 def build_sidechain(): duck = np.zeros(N) for s, w in DUCKS: m = min(env_len, N - s) cur = duck[s:s + m] np.maximum(cur, base[:m] * w, out=cur) return 1.0 - 0.55 * np.clip(duck, 0.0, 1.0)
07 / PITFALLS
这些坑都真实发生过——记下来是为了下一次不用再踩。
某镜像下到 12.5MB 就断,解压报 Unexpected end of input。解法:改用系统包,或 pip 的 imageio-ffmpeg;视频管线只用 stdin/stdout,不依赖额外构建。
raw.githubusercontent 只有 ~90KB/s,一个 6MB 字体要一分多钟。解法:换 jsDelivr 镜像(gcore 节点实测 ~1MB/s),并只下载真正用到的字重。
模糊/发光一旦每帧实时计算,单帧从 80ms 涨到 400ms+。解法:预渲染成离屏 canvas 贴图,逐帧只做 drawImage 与交叉淡化。
ffmpeg fps=2 采样会取到 u≈0.23s,看不到首帧白闪。解法:用 select='eq(n,0)+eq(n,15)…' 精确取帧,或单帧 -ss 抽帧核对。
WSL 里 Chromium 报 libnss3 / libnspr4 not found。解法:装系统依赖;WebGL 页面另加 --use-angle=swiftshader 与 preserveDrawingBuffer。
两个进程同时写同一个 .part.mp4,faststart 重定位失败,产物只有 2 帧。解法:一个片段同时只允许一个渲染进程;失败先删 .part 再重跑。
成片音轨比母带多 640 个样本,肉眼听不出但会累积。解法:用互相关对成片音轨与母带做偏移校验,本片实测 0.00ms。
任何一段少一帧,concat 后整条时间轴都会错。解法:每段 render 后 ffprobe -count_frames 核对,26 段合计必须精确 1830。
08 / REPRODUCE
Node + Playwright + FFmpeg + Python(numpy/scipy) 即可完整重建;本页所有画面都来自这套工程。
ffmpeg -version · node -v · python -m venv .venv && pip install numpy scipynode ci: playwright install chromium · 字体从 google/fonts 镜像下载node scripts/render.mjs --page pages/s01.html --out out/s01.mp4 --dur 3 --t0 0.35 --speed 1.4node scripts/build.mjs --all --force --jobs=7python audio/synth.py # 61.000s / 48kHz / -0.6dBFSnode scripts/build.mjs --concat # → 61.000s / 1830 帧 / 26.9MBproject/ ├── common.js # 缓动 / 种子随机 / 片段封装 / 胶囊标签 ├── pages/ # 26 个片段页面 │ ├── intro.html end.html │ ├── card.html flash.html │ └── s01.html … s11.html ├── scripts/ │ ├── render.mjs # Playwright → CDP → ffmpeg │ └── build.mjs # 渲染 / concat / 2-pass ├── audio/ │ └── synth.py # 61s 配乐(numpy/scipy) ├── assets/ │ ├── fonts/ # 11 个开源字体 │ └── vendor/ # three.js / KaTeX / RoomEnvironment └── out/ # 分段 / 接触表 / 成片 / 音频
“以上每一帧,
都是一行一行代码写出来的。”
19 种风格 · 1830 帧 · 0 个视频生成模型