先说清楚这类片子长什么样。笔尖走到哪,线稿长到哪;讲到瑞金,「瑞金」和「1934.10」跟着一笔一笔被写出来。镜头推近看细节,讲完再拉远,露出整张路线图,底下一行字幕稳稳跟着人声。
看着挺顺,做起来全是坑。白板动画难的不是把图画出来,是让笔、声音、字幕、镜头踩在同一条时间轴上。
自己手绘得会画画,用 AE 得逐帧打关键帧,让 AI 直接生成视频又管不住哪一笔在第几秒出现。所以常见的翻车长这样:手在动画面没东西,字幕已经出来了人声还没起,讲到第三站镜头还停在第一站。
我用 WorkBuddy 把两份解说稿跑成了两条成片:长征路线 83 秒,Agent 智能体发展史 43 秒,都是 1280×720、24fps。没有一张图是手画的,没有一帧是在剪辑软件里拖出来的,全靠一条固定产线跑完,七道工序、21 个脚本、28 条实测参数,已经开源到 GitHub。
这篇不聊过程里绕过的弯,只讲定稿版怎么跑:每一步喂什么进去、跑哪条命令、吐出来什么。
先看第一条。

七道工序,其实只在回答三个问题
拆开看挺长,其实每一道就管一件事:画面画得出来吗、镜头跟得上吗、声音和字对得上吗。
| 工序 | 关键命令 | 交出来的东西 |
|---|---|---|
| 1 解说稿与分镜 | 按句号断句、定站点 | 站点 spec |
| 2 AI 出主题手绘图 | ImageGen 出 16:9 线稿 | 1280×720 底图 |
| 3 源图净化 | clean_source.py |
去纸纹底图 |
| 4 像素级标注 | make_annotation_v3.py |
annotation.json |
| 5 配音与时间轴 | tighten_audio.py + asr_align.py |
timing.json |
| 6 镜头渲染 | render_camera_whiteboard.py |
无声 MP4 |
| 7 字幕烧录 | make_srt.py / srt2ass.py / ffmpeg |
成片 MP4 |
这张表看着唬人,真跑起来就是一串命令接一串命令,中间不用换工具。九宫格抽帧里那些手、地名、线稿、字幕,全是这七道出来的,没额外补过一帧。
下面的命令和参数抄自 2026-11-06 定稿那天的机器:Windows 11、Python 3.10 以上、FFmpeg 在 PATH、上游仓库 geeklee/srt-whiteboard-animation(MIT)。换个环境数字可能得微调,顺序不用动。

笔扫到哪,颜色才出现在哪
第一步是出题,也是最容易想省事的一步。
用户要什么主题,就生成什么主题的底图 。别拿项目里自带的 example 图顶替,看着能跑,交出来的片子跟文案对不上。定稿版两张底图,一张长征路线,一张 Agent 发展史时间线,各按各的叙事单独出。
提示词得把构图说死:16:9 横构图、极简手绘线稿、纯素描草图、克制涂鸦风,还得明写「不出现文字」。地名和年份留给后面的 add_handwriting.py 烘进底图,别指望生图模型自己把字写对。两张底图一张 1.3MB、一张 1.2MB,都是 1280 宽的线稿。

AI 出的线稿带一层纸纹。这玩意儿直接丢给渲染器,adaptiveThreshold 会把纸纹也判成墨迹,好好的一条路线碎成点阵。所以第二道先净化,clean_source.py 把纸纹压掉,只留笔画本身。

第三道给每个元素标一个 region。定稿版是「一站两幕」:先画物占 0.6 时长,紧跟着写「名字 + 年份」占 0.4,写字幕的取景框就是它自己的文字框,镜头轻轻推过去,像笔移到旁边去写字。长征 15 个元素、Agent 16 个元素,都这么数出来的。
annotation.json 的顶层就五个字段:sceneId / canvas / storyBasis / sceneDurationMs / elements。每个元素带 region、inkPath 和时长;写字幕的 inkPath 填 textwrite,画物幕走蛇形笔刷。取景时脚本读的是墨迹掩码 ink_pixels & allowed,不读 region。
画面「像不像在画」,全看笔刷。
上游渲染器原本是整片擦除,外加一支笔在旁边独立扫动,笔和内容脱钩,看着就是手在空划。定稿版换成蛇形笔刷:笔尖扫到哪一列,颜色就出现在哪一列,笔和内容变成同一件事。
一笔是不是真在画,看笔尖和墨迹有没有绑在同一列上。

画布放大一倍,镜头才有地方走
全在 1280×720 上画完,观众看到的就是一张越来越满的图。没劲。定稿版先在 2560×1440 的画布上渲染,每帧按 viewport 裁出 1280×720 输出,讲到哪就满屏画哪,画完摇走。
取景框我只认真实墨迹的包围盒 ,不认标注里的 region。标注框习惯留松,密集的图里还会横跨邻居,拿它当取景框,画面正中一片空白,图形被顶到边角。
镜头从上一幕摇过来要 0.3 到 0.6 秒,所以每幕开头留 0.22s 空转,等镜头到位了再落笔。少这 5 帧,笔就会画在画框外 ,观众只看见手动、看不见东西长出来。最后一幕结束后,取景目标设成整张画布,补帧到总时长,片子收在一张全图上。
渲染一条命令就够:python scripts/render_camera_whiteboard.py 主图.png 标注.json out.mp4 assets/drawing-hand.png --total-ms 83150 --fps 24 --grid-edge 20 --zoom-fill 0.86。Agent 那条 43.5 秒的片子,渲染实测 33 秒,比片子本身还短,挺反直觉。

讲到地名和年份,字不是直接贴上去的。add_handwriting.py 先把楷体标签烘进底图,标注里给这一幕写 "inkPath": "textwrite",笔尖按列从右推进,一个字一个字揭示出来。主名 34 到 40px,年份用 0.6 倍字号排在下方,颜色和线稿同色系但略浅。这块没什么好调的,照着给就行。

还有个挺烦的几何限制,得说清楚:++16:9 里棋盘、手机这类竖长元素,纵向最多占到画面 60% 到 70%++,横向只有四分之一左右,再推近就糊了。想让它们撑满屏,要么把元素画宽,要么干脆出竖屏,没第三条路。

声音里的空白,得先压掉再对齐
配音只有一条纪律:整篇一次送进 TTS 。--text 一次能吃掉 1500 字以上,别拆成一堆三秒小段再拼。各段语速、停顿、重音不一致,时间轴和字幕必然错位。定稿版用的是即梦 SeedAudio 1.0 的全能配音,音色「狒狒_讲解」。
但「整篇一次送」不等于平台内部合成了一整段。长文本会被内部切块再拼接,块与块之间塞进来的是 -120dB 绝对静音。自然换气有底噪,大约 -50 到 -60dB,只有拼接插入的静音帧才会是数字零。定稿版长征这条,原始音轨 99.05 秒里有 27.55 秒是静音,压缩脚本实际动刀 32 处,最长一处正好 4 秒。
配音文案也有讲究。长征这类题材,军事措辞容易被内容审核挡回来,定稿版统一改成中性叙述,年份写成中文数字「一九三四年」再送进去,一次过审。这条不在脚本里,写在 SKILL.md 的阶段 4,别漏看。
所以配音完有两件事必做。先拿 silencedetect=noise=-38dB:d=0.35 做体检,再用 tighten_audio.py 分级压缩:50ms RMS 窗口、低于 -40dB 判静音,超过 1 秒的停顿留 0.55 秒,0.35 到 1 秒的留 0.32 秒,片头留 0.12 秒,按样本重新拼接,发声内容一个样本都不动。

结果摆在这儿:99.05 秒压到 83.15 秒,静音从 27.55 秒降到 11.65 秒,全片再没有超过 0.7 秒的停顿。83 秒这个数是压出来的,先有声音,画面再跟着对齐。
音频一变,后面全部重跑 :ASR 转写、timing.json、SRT、ASS、标注时长、渲染的 --total-ms,一律基于压缩后的音频,不复用旧 dump。
时间轴来自配音自己的转写时间戳,asr_align.py 用 faster-whisper 拿到词级时间戳,再按 20ms 网格把句起点吸附到真实发声帧。这一步决定字幕是不是等人声:++停顿不等于句界++,只有发声帧才是。
字幕跟不跟得上人声,看时间轴是不是从压缩后的音频重新算的。

字幕只占一行,写不下就拆成两条
字幕不另标时间 ,直接吃第五步产出的 timing.json。换句的时刻就是笔停下的时刻。省事,也最不容易错。
make_srt.py --max-chars 14 先按标点断句,再贪心合并到 14 字(留 3 字容差);单句超长就均分拆成多条连续字幕,按时长按字数比例分摊。观众看到的是一条条往下滚,不是两行压在一起。
srt2ass.py --size 46 --anchor-y 688 负责把 SRT 转成 ASS,显式写死 PlayRes 和 \an2\pos(640,688) 锚点,字号和位置都是真实像素。米白字加深墨描边,压在线稿上也读得清。长征 25 条、Agent 22 条,全部单行。
烧录顺序也有讲究:先合音轨,再在合成结果上烧字幕,省一次转码。

第二条放这儿。同一条产线,换了底图范式和文案,别的没动。

两条片子只差三个参数
同一个仓库、同一套脚本,两条片子分叉的地方,说到底只有底图范式。这张表是整套流程里最值得抄走的。
| 长征路线(地理路线型) | Agent 发展史(横向时间轴型) | |
|---|---|---|
| 构图 | 起点按真实方位,路线西→北→东贯穿全幅 | 一条 S 形主轴,8 个节点等距、图标上下交替 |
--zoom-fill |
0.86 | 0.70 |
| 元素 / 字幕 | 15 元素 / 25 条 | 16 元素 / 22 条 |
| 成片 | 83.148 秒 / 1280×720 / 24fps | 43.458 秒 / 1280×720 / 24fps |
zoom-fill 这两个数各有各的来由。元素之间留白多的路线图可以放心推近,推到 0.86 画面才不空;节点左右紧邻的时间轴推太近会把邻居拦腰切断,只能收到 0.70。一个参数通吃两种图,两头都不成立。
时间轴型另外还有三条规矩,别照搬路线图那套:文字幕别套墨迹包围盒(标签字常比 region 宽,会把整句裁到画外)、标签 region 要按「主名 + 年份」两行留 80 到 90px 高、结尾 3 到 5 秒必须拉远到全轴,不然时间轴的跨度白画了。

仓库里有什么,没有什么
srt-whiteboard-workflow 已经挂上 GitHub,public、MIT、v1.0。地址是 github.com/TaurenRen/srt-whiteboard-workflow,公众号正文点不了外链,复制这一行到浏览器,或者点文末的阅读原文。

仓库里有什么,直说:SKILL.md 里是七阶段工作流和 28 条铁律全文,scripts/ 里 21 个脚本,核心三个分别是 render_camera_whiteboard.py(镜头运动 + 蛇形笔刷 + textwrite + 结尾拉远)、asr_align.py(配音到逐字符时间轴,含句起点吸附)、tighten_audio.py(TTS 静音压缩)。examples/ 里有这两条成片、两组抽帧九宫格,还有长征示例整套底图、标注 JSON、timing、SRT/ASS 和文案原文,照着就能复刻整条链路。
开源前我把脚本里的本机绝对路径全换成了 SRTWB_UPSTREAM / SRTWB_PROJECT 这类环境变量,渲染器会自己找同级克隆的上游仓库,21 个脚本 py_compile 全过。README 里另外给了两种底图范式的参数表和一份验收清单,清单把录屏和关键截图也算成交付物:成片之外要有渲染控制台录屏、九宫格抽帧、结尾拉远全图、镜头推近写字这几张,别人拿到仓库才知道做对了长什么样。

丑话也得说在前面:仓库里没有什么 ,没有一键出片的图形界面 ,配音音频和音色得接自己的 TTS,上游 geeklee/srt-whiteboard-animation 得另外 clone,FFmpeg 要自己在 PATH 里,脚本按 Windows 10/11 + Python 3.10 以上实测。README 末尾还老老实实列了三条已知短板,包括细长元素取景会带进邻域标签。
七道跑顺以后,下一条片子就是换张底图、换份解说稿的事。