手绘白板动画的生产线拆解:AI 出图、笔迹对齐、镜头渲染七道工序(附 21 个开源脚本)

0 阅读10分钟

先说清楚这类片子长什么样。笔尖走到哪,线稿长到哪;讲到瑞金,「瑞金」和「1934.10」跟着一笔一笔被写出来。镜头推近看细节,讲完再拉远,露出整张路线图,底下一行字幕稳稳跟着人声。

看着挺顺,做起来全是坑。白板动画难的不是把图画出来,是让笔、声音、字幕、镜头踩在同一条时间轴上。

自己手绘得会画画,用 AE 得逐帧打关键帧,让 AI 直接生成视频又管不住哪一笔在第几秒出现。所以常见的翻车长这样:手在动画面没东西,字幕已经出来了人声还没起,讲到第三站镜头还停在第一站。

我用 WorkBuddy 把两份解说稿跑成了两条成片:长征路线 83 秒,Agent 智能体发展史 43 秒,都是 1280×720、24fps。没有一张图是手画的,没有一帧是在剪辑软件里拖出来的,全靠一条固定产线跑完,七道工序、21 个脚本、28 条实测参数,已经开源到 GitHub。

这篇不聊过程里绕过的弯,只讲定稿版怎么跑:每一步喂什么进去、跑哪条命令、吐出来什么。

先看第一条。

01-changzheng-9frames.jpg

七道工序,其实只在回答三个问题

拆开看挺长,其实每一道就管一件事:画面画得出来吗、镜头跟得上吗、声音和字对得上吗。

工序关键命令交出来的东西
1 解说稿与分镜按句号断句、定站点站点 spec
2 AI 出主题手绘图ImageGen 出 16:9 线稿1280×720 底图
3 源图净化clean_source.py去纸纹底图
4 像素级标注make_annotation_v3.pyannotation.json
5 配音与时间轴tighten_audio.py + asr_align.pytiming.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)。换个环境数字可能得微调,顺序不用动。

02-pipeline-7steps.jpg

笔扫到哪,颜色才出现在哪

第一步是出题,也是最容易想省事的一步。

用户要什么主题,就生成什么主题的底图。别拿项目里自带的 example 图顶替,看着能跑,交出来的片子跟文案对不上。定稿版两张底图,一张长征路线,一张 Agent 发展史时间线,各按各的叙事单独出。

提示词得把构图说死:16:9 横构图、极简手绘线稿、纯素描草图、克制涂鸦风,还得明写「不出现文字」。地名和年份留给后面的 add_handwriting.py 烘进底图,别指望生图模型自己把字写对。两张底图一张 1.3MB、一张 1.2MB,都是 1280 宽的线稿。

03-handdrawn-source.jpg

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

04-clean-compare.jpg

第三道给每个元素标一个 region。定稿版是「一站两幕」:先画物占 0.6 时长,紧跟着写「名字 + 年份」占 0.4,写字幕的取景框就是它自己的文字框,镜头轻轻推过去,像笔移到旁边去写字。长征 15 个元素、Agent 16 个元素,都这么数出来的。

annotation.json 的顶层就五个字段:sceneId / canvas / storyBasis / sceneDurationMs / elements。每个元素带 region、inkPath 和时长;写字幕的 inkPath 填 textwrite,画物幕走蛇形笔刷。取景时脚本读的是墨迹掩码 ink_pixels & allowed,不读 region。

画面「像不像在画」,全看笔刷。

上游渲染器原本是整片擦除,外加一支笔在旁边独立扫动,笔和内容脱钩,看着就是手在空划。定稿版换成蛇形笔刷:笔尖扫到哪一列,颜色就出现在哪一列,笔和内容变成同一件事。

一笔是不是真在画,看笔尖和墨迹有没有绑在同一列上。

05-annotation-json.jpg

画布放大一倍,镜头才有地方走

全在 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 秒,比片子本身还短,挺反直觉。

06-camera-textwrite.jpg

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

07-zoomout-fullmap.jpg

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

08-framing-bbox.jpg

声音里的空白,得先压掉再对齐

配音只有一条纪律:整篇一次送进 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 秒,按样本重新拼接,发声内容一个样本都不动。

09-silence-report.jpg

结果摆在这儿: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 网格把句起点吸附到真实发声帧。这一步决定字幕是不是等人声:++停顿不等于句界++,只有发声帧才是。

字幕跟不跟得上人声,看时间轴是不是从压缩后的音频重新算的。

10-srt-single-line.jpg

字幕只占一行,写不下就拆成两条

字幕不另标时间,直接吃第五步产出的 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 条,全部单行。

烧录顺序也有讲究:先合音轨,再在合成结果上烧字幕,省一次转码。

11-render-console.jpg

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

12-agent-8frames.jpg

两条片子只差三个参数

同一个仓库、同一套脚本,两条片子分叉的地方,说到底只有底图范式。这张表是整套流程里最值得抄走的。

长征路线(地理路线型)Agent 发展史(横向时间轴型)
构图起点按真实方位,路线西→北→东贯穿全幅一条 S 形主轴,8 个节点等距、图标上下交替
--zoom-fill0.860.70
元素 / 字幕15 元素 / 25 条16 元素 / 22 条
成片83.148 秒 / 1280×720 / 24fps43.458 秒 / 1280×720 / 24fps

zoom-fill 这两个数各有各的来由。元素之间留白多的路线图可以放心推近,推到 0.86 画面才不空;节点左右紧邻的时间轴推太近会把邻居拦腰切断,只能收到 0.70。一个参数通吃两种图,两头都不成立。

时间轴型另外还有三条规矩,别照搬路线图那套:文字幕别套墨迹包围盒(标签字常比 region 宽,会把整句裁到画外)、标签 region 要按「主名 + 年份」两行留 80 到 90px 高、结尾 3 到 5 秒必须拉远到全轴,不然时间轴的跨度白画了。

13-two-paradigms.jpg

仓库里有什么,没有什么

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

14-repo-quickstart.jpg

仓库里有什么,直说: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 里另外给了两种底图范式的参数表和一份验收清单,清单把录屏和关键截图也算成交付物:成片之外要有渲染控制台录屏、九宫格抽帧、结尾拉远全图、镜头推近写字这几张,别人拿到仓库才知道做对了长什么样。

15-repo-tree.jpg

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

七道跑顺以后,下一条片子就是换张底图、换份解说稿的事。