背景
博客开始图文并茂后,「配图/配片从哪来」成了内容管线的最后一环。此前三条通道并存:
rsvg-convert(手写 SVG)、外部 AI 生图(seedream,需 ARK_API_KEY)、IDE 宿主生成。
前两者可入库,第三者对服务器是黑盒。
2026-11-07 做《四本账》封面与 15 秒摘要片时,这条路的问题一次暴露干净:
- 不可复现:同一段 prompt 无法固定种子/尺寸/码率;换个时间生成就是另一条片子。 仓库里留下的只有产物,没有「重出一次」的办法。
- 不可控规格:生成器给的是 11MB、带「AI 生成」水印的原始片;要能上网得手工裁水印、
压到 1.3MB、补
+faststart。规格全靠人盯,没有闸门。 - 不可审计:prompt 里可能带未公开的产品命名,出境即不可撤回(见 ADR-0011 的隐私口径)。
而早在做 zjchina-anim 时,设计契约里就写着这条路是「免费」的:
无 retained 状态 ⇒ 动画值 = f(t) 纯函数 ⇒ 重定目标免费(spring)+ 离线帧序列导出免费 (固定 t 步进采样)。 ——
zjchina-anim/src/lib.rs
今天要做的不是新能力,而是把这条「设计余量」兑现成「产品入口」。
这条路的血缘:对标 Impeller,抄 Qt 的纪律,借 UE 的编排
出片不是临时起意,它继承了内核选型时就写下的三笔账:
- Flutter / Impeller 的形:自绘内核的存在前提是「自有 GPU 提交路径 + 预编译管线 +
显式状态」,与 Impeller 去掉运行时 shader 编译抖动是同一个立意(
docs/renderer-roadmap.md)。 这条路径一旦成立,「同一场景 → 同一像素」就是默认属性,而不是出片时才去追的目标。 - Qt 的魂:动画计算只发生在渲染循环里,UI/数据线程零参与——所以采样器可以脱离
窗口系统单独跑,
zjchina-clip才能在无界面环境下逐帧出图 (zjchina-anim/src/lib.rs的「由来」段)。 - UE 的意:
Seq的错峰编排是演绎骨架,但我们不搬它的时间轴资产与 Sequencer 编辑器——那会把「片子」变成需要维护的第二套资产。片子由 scene JSON + delta 模板 在采样时生成,改一行就改全片。
反过来,这条路线也划清了「不抄」:公开 Web 页永不自绘(走 DOM+SSR,
LET_TIME_SEE.md),AI 生成只做一次性素材(本 ADR 的拍板)。
选项
- A. 继续用 AI 生成,仅补规格闸门。 最省事,但每次改一个像素都要重新 prompt, 且片子永远无法在服务器上原样重出;「素材可复现」这条内容纪律直接破功。
- B. 引 ffmpeg 的 Rust 绑定,把出片塞进内核。 会给 L0 内核拉入重依赖,违反 「内核只管画、不管封装」的分工;且 ffmpeg 绑定许可证与版本都难控。
- C. 固定 t 采样 → stored PNG 帧 → 系统 ffmpeg 编码(选定)。
动画值来自
zjchina-anim(纯函数),帧来自zjchina-render离屏读回(已有render_to_rgba+png::encode_png的 stored 编码,专为中间帧设计),编码交给 系统ffmpeg(命令行,零 Rust 依赖)。同一份源码 + 同一个 t ⇒ 同一串字节。
证据
- 出帧已是既有能力:
zjchina-render/tests/frame_export.rs证明离屏读回逐字节确定; 它用的 stored(不压缩)PNG 编码就是为「喂给编码器」准备的。 - 动画驱动逐帧提交已跑通:
zjchina-render-capi/tests/frame_seq.rs用Stepper+Spring采样 60 帧并逐帧submit_delta,说明 f(t) → 帧的链路成立。 - 缺的只有最后一环:全仓库没有任何一处调用
ffmpeg,也没有「采样 → 成片」的命令; 能力只以测试形式存在,没有产品入口。 - 今天的人工成本即反例:裁水印 + 二次编码 + 手写
?v=2绕缓存,全是手工步骤, 一次可以,每周一次就会出错。 - 服务端还有一个坑:视频落地时
application/octet-stream+ 不支持 Range, Safari/iOS 直接不播——这类问题只有「我们自己产片、自己验收」才拦得住。
拍板
- 必须一致的片子(产品演示、大屏录制、文章摘要片)一律自绘出片:
Stepper::at_fps固定步长遍历 t →f(t)改场景属性 →render_to_rgba→ stored PNG → 系统ffmpeg(H.264 / 720p / 无音轨 /+faststart)。 - AI 生成只做一次性素材(灵感图、非关键配图),走
zjchina-agent::media的闸门(默认关、审计只记元数据、单件 ≤3MB);产物入库同样必须过gen-asset.sh。 - 出片规格与配图共用一条闸门:
scripts/gen-asset.sh的 720p/H.264/静音/faststart/≤3MB 全部复用,不为视频另立一套。 - 不做:不造动画 DSL、不做时间轴编辑器、不引 ffmpeg 绑定;
场景用现成的
SceneJSON + 单个插值量驱动,够用即止。
落地与验证
- 采样驱动:
zjchina-clip(工具 crate,独立 workspace)——读 scene JSON + 可选 delta 模板,用zjchina-anim的曲线在固定 fps 下逐帧渲染落 PNG; - 成片:
scripts/clip.sh调ffmpeg编码(PNG 走image2pipe直连,不落中间盘), 产物随后过gen-asset.sh的 install/check; - 首个真片:
scripts/frames/2026-11-07-four-accounts.{scene,delta}.json(四本账摘要片,--anim "120,300,ease_in_out"/ 8s / 30fps / 720p)。delta 每帧整段替换world:overlay是屏幕空间(y 向下),柱子属于相机空间(y 向上),只有 world 段能按基线向上生长; - 封面也归同一条链:
scripts/frames/2026-11-07-four-accounts.cover.scene.json静态帧 1600x900 → JPEG(原 AI 封面已换掉);资产的出处写进 manifest 的source(self-drawn / ai / handmade,gen-asset.sh check拦缺失),不再靠回忆区分谁做的; - 复现性锁:
scripts/clip.sh --verify同参数连跑两遍帧流比 sha256(四本账片首验190935bf…,同机同字体同 GPU 后端逐字节一致);帧数必须等于fps × seconds; - 验收探针:
curl -r 0-1023必须 206 +video/mp4(服务端 MIME/Range 已随 2026-11-07 的静态文件修复落地)。
日期:2026-11-07。拍板者 zj。