Communitygithub.com

手绘白板动画制作工作流(SRT → 手绘动画成片)

将 SRT 字幕或解说稿转化为「手握笔边画边讲」的白板手绘动画视频(含配音、录屏、关键截图)。基于开源项目 srt-whiteboard-animation 实测沉淀,涵盖 AI 出图、源图净化、像素级标注、即梦配音、时间轴对齐、录屏截图与全套避坑指南。

手绘白板动画制作工作流(SRT → 手绘动画成片)란 무엇인가요?

这是一个基于开源项目 [srt-whiteboard-animation](https://github.com/geeklee/srt-whiteboard-animation) 的端到端白板手绘动画制作工作流,专为开发者、内容创作者和教育者设计。它将 SRT 字幕/解说稿转化为包含配音的「手握笔边画边讲」风格的白板动画视频,并生成关键截图和操作录屏。该工作流涵盖了从写解说稿、AI 出图、像素级标注、即梦配音与时间轴对齐,到渲染合成和字幕烧录的全过程,并提供了大量实测翻车总结的「铁律」来指导每个步骤。它支持地理路线型和横向时间轴型两种底图范式,通过镜头缩放(zoom-fill)、蛇形笔刷、手部素材缩放和换场缓冲等技术确保成品流畅专业。该工作流强调叙事主线的连续性、元素与口播的节奏对齐(一元素对一句),以及严格的校验流程(如 difflib 核对文案与转写、静音压缩、字幕断行规则),并包含针对常见 bug(如 PyAV 编码问题)的自动修复脚本。

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/TaurenRen/srt-whiteboard-workflow/tree/HEAD

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

手绘白板动画制作全流程(SRT → 手绘动画成片)

基于开源项目 srt-whiteboard-animation(github.com/geeklee/srt-whiteboard-animation)实测沉淀。 产出:带配音的「手握笔边画边讲」白板动画 MP4 + 关键截图 + 操作录屏。

铁律(违反即返工,全部来自实测翻车)

  1. 禁止拿示例图当交付。项目 examples/ 里的 scene-01-monkey-mountain-banana.png 只能验证管线, 用户要什么主题就必须生成什么主题的图。用示例图交付 = 交付失败。
  2. 源图必须净化。AI 生成的米黄纸底带大量纹理颗粒,渲染器 adaptiveThreshold 会把纸纹判成墨迹, 成片线条碎成点阵、背景爬满噪点。必须跑 scripts/clean_source.py 去纸纹后再渲染。
  3. 末位元素禁止用整幅 region。allowed mask = region − 后续 region,若最后一个元素 region 是整幅画布, 它会把前面所有元素的 allowed 扣空 → 前几幕什么也画不出来、颜色全丢(实测确认)。 收尾幕要挑一块真实的小区域(如顶部云带)。
  4. 手部素材要按画布缩放。原始 drawing-hand.png 是 1069×1472,直接贴到 1280×720 会占屏 40%+, 严重遮挡画面。实测缩到 55%(587×809),手宽约占画布 21%,观感正常。
  5. 渲染输出时长会短于标注。渲染器按各区域实际可绘制像素决定帧数,sceneDurationMs 只是上限。 实测 8×8000ms=64s 的标注只渲出 45.8s。必须传 --total-ms,并以 ffprobe 实测时长校准。
  6. 录屏不能用 kill 结束。ffmpeg 被强杀会丢 moov atom,文件报 moov atom not found。 用 -t <秒> 让录制自然结束。
  7. 必须交付录屏与截图。这是需求的一部分,不是可选项。截图要覆盖:源图、标注校验图、 渲染控制台、成片关键帧。用「文档记录代替截图」属于未完成。
  8. 一张画布只讲一件事,而且必须是「连续推进」。 实测返工:把红旗、桥、雪山、宝塔平铺在一张图里一次性同时画 → 用户评价"画得很乱"。 正确做法是让元素沿一条主线(地理路线 / 时间轴)排开,笔迹按主线的行进方向连续移动, 观众看到的是"笔在走路",而不是"在一张画布上到处补细节"。
  9. 空间布局必须服从真实走向。画长征就要按真实地理:瑞金在东南 → 向西 → 北上 → 陕北。 构图错了,节奏再准也救不回来。画之前先在地图上确认每个节点的相对方位。
  10. 手绘节奏 = 口播节奏,一元素对一句。每个元素的 durationMs 必须等于它那句配音的实际时长 (用 sync_timing.py 从静音检测反推),绝不允许平均分配。第 N 幕的笔停在"第 N 句说完"的瞬间。
  11. 幕与旁白语义必须一一对应。实测踩过:宝塔(吴起镇)排在总结句那一幕,结果旁白说"抵达吴起镇" 时画面在画别的东西。收尾的总结句("二万五千里…")不要配新元素,让它落在"全部画完的定格"上, 渲染器会自动补 --total-ms 的空帧做凝视。
  12. 字幕要单独烧,不要指望 force_style。纯 SRT 没有 PlayRes,libass 用内部默认分辨率换算 FontSize/MarginV,实测调不出准值。走 make_srt.py → srt2ass.py(显式写 PlayRes = 视频尺寸, 并用 {\\an2\\pos(x,y)} 锚定底边)才可控。
  13. 字幕只允许一行。两行会同时占掉画面上下两块,中文两行读起来也断气。 一行装不下就拆成多条连续字幕(按字数比例分摊时长),不要压行、不要缩字号。 实测:46px 字号 + 单行 ≤14 字,720p 下约占屏宽 60%,手机上看刚好。
  14. 画布要能移动,别把整张图塞进一个固定镜头。观众反馈"没必要一张画布画完"—— 正确做法是把画布放大(2 倍)渲染,每幕只把当前元素取景到满屏,画完镜头摇到下一个, 最后拉远到全图收尾。一张固定全景从头看到尾 = 每个元素只占屏幕 10%,什么都看不清。
  15. 笔尖必须跟着内容走。上游 contour-wipe 是「整片自下而上擦除 + 笔尖横向乱扫」, 两者完全脱钩 → 用户看到"手在动但没东西画出来"。改成蛇形笔刷:笔扫到哪,颜色出现在哪。
  16. 讲关键节点时要写字。只画物不写字,观众记不住"哪一年、什么地方"。 把地名/年份用楷体烘进底图(add_handwriting.py),再让渲染器用 textwrite 模式逐列写出来 —— 笔顺着笔画推进,字一个个出现,跟"手写"是同一个动作。
  17. 画布放大后必须做两件事,否则要么慢死要么糊: ① 源图先用 INTER_CUBIC 预放大到画布尺寸(渲染器内部的 resize 是 INTER_AREA, 放大时等价最近邻,会把线条变成方块); ② 落墨改按包围盒局部处理(上游每段都 new 一张整画布掩码,2560 画布下慢 4 倍)。
  18. 合成音轨前的成片必须留 3–7 秒给结尾拉远。最后一幕不要排到配音结束的最后一毫秒, 否则"展示全图"这个收尾动作没有时间发生。
  19. 取景框要用「真实墨迹包围盒」,不能用标注 region。region 是手画的框,普遍比图形大一圈, 密集图里还会横跨 2–3 个邻居;直接拿它算 zoom,结果是"画面正中空白、图形缩在角落、邻居被切一半"。 渲染器已内置:set_target(element, allowed) 里用 ink_pixels & allowed 求 bbox 再外扩。 例外:文字幕不走 bbox——标签字常比 region 还宽,套 bbox 会把整句裁到框外(实测「对话式 AI」出画)。
  20. 换场要先摇镜头再落笔。每幕开头空转约 5 帧(0.22s),否则笔在镜头还没到位时就开始画, 画到画面外,观众看到"手在动却没内容"。
  21. --zoom-fill 按底图疏密分别取值,稀疏图 0.86、密集时间轴 0.70;一个参数连通吃会两头翻车。
  22. 竖长元素满不了屏要接受。16:9 画面里,纵向元素只能占 60–70% 高、25% 宽,这是几何约束; 想更满就把元素画宽,或者出竖屏。
  23. 配音整篇一次生成,禁止逐句分段再拼接。分段拼接 = 各段语速/停顿不一致 + 时间轴必然错位, 用户听到的评语就是"胡言乱语"。即梦单次可吃 1500+ 字(≈2 分钟),整篇一次喂进去。
  24. 时间轴来自配音的转写时间戳,不来自静音检测。停顿 ≠ 句子边界;句子数对不上就只能平均分配, 字幕必错。用 asr_align.py(ffmpeg → faster-whisper word_timestamps → 字符级时间轴)。
  25. 换了配音就必须重派生一遍字幕和标注时长。字幕时间戳是那条音频的属性,不是画面的属性; 旧字幕配新音频等于没做。
  26. 交付前用 difflib 核对「文案 vs 转写」。相似度 <0.8、或出现整句缺失/重复 → 音频有问题,重生成。 同音误识(弘/红、尊/遵、武/吴)属正常 ASR 现象,不算朗读错误。
  27. 配音生成后必须做静音体检。"整篇一次送 TTS"不代表平台内部不切块: 即梦会在块间插入 -120dB 绝对静音(实测 4.00s / 2.60s)。静音占比 >20% 或存在

    1.2s 的停顿 → 先用 tighten_audio.py 压掉,再谈时间轴对齐。

  28. 字幕起点要吸附到真实发声帧。ASR 在有长静音时会把下一句 start 标在静音起点, 观众会"看着字幕等声音"。asr_align.py 默认 snap_to_voice 修正,别关。

目录规范(强制)

D:/04-项目实测/<YYMMDD>-<项目简称>/
  00-资料/          项目资料与源码笔记
  01-素材/          原始 AI 出图、手部素材
  02-字幕与配音/     SRT、配音 mp3、时长表
  03-标注/          <图名>.png + <图名>.annotation.json(必须同名同目录,预览台依赖)
  04-渲染输出/      单幕 silent.mp4、成片 mp4
  05-录屏截图/      录屏 mp4 + 关键截图 png
  06-文档/          脚本、操作记录、验收清单

前置

git clone https://github.com/geeklee/srt-whiteboard-animation.git
cd srt-whiteboard-animation && python scripts/prepare_env.py   # 建 .venv 装 opencv/numpy/av

后续一切渲染命令都用 srt-whiteboard-animation/.venv/Scripts/python.exe。

若报 _lay_ink() takes 6 positional arguments but 7 were given,是仓库已知 bug: render_stream_whiteboard.py 里 self._lay_ink(writer, ink_frames, [], set(), None, allowed) 多余的 None 删掉即可(scripts/check_bug_fix.py 可自动检测修复)。

工作流(7 步:分镜 → 出图 → 空间板 → 标注 → 配音对齐 → 渲染 → 字幕)

阶段 1 · 解说稿与分镜(先确认再往下)

  1. 写 6–10 句解说,每句 6–10 秒,总长 40–90 秒。
  2. 解析分镜(可选,也可直接手排):
    <ENV_PY> srt-whiteboard-animation/scripts/parse_srt.py 字幕.srt --target-sec 30 --min-sec 25 --max-sec 35
    
  3. 输出分镜表(第几幕 / 核心表达 / 画面主体 / 字幕原文),停下等用户确认。

阶段 2 · AI 出主题手绘图

按项目「统一出图视觉规范」写出图提示词,每条都必须含:

  • 极简手绘线稿 / 纯素描草图 / Notion 式克制涂鸦,16:9,大量留白,元素互不重叠
  • 背景暖米黄 #F5EBD7,线条深灰 #333333,仅红/橙/蓝少量点缀
  • 绝对禁止任何文字(汉字/字母/数字/标签/地名/标题/水印),禁止写实/3D/渐变/高饱和

用 ImageGen 或即梦 image 出图。每张图对应一幕核心意思,元素要能被矩形分区切开。

出图后必做三件事:

  1. 去水印:AI 出图右下角常有 AI生成 WORKBUDDY,用 cv2.inpaint 抹掉。
  2. 转 16:9:生成图多为 3:2,左右补米黄纸底后缩放到 1280×720(不要居中裁切,会切掉主体)。
  3. 净化:去纸纹,保留线条与彩色点缀。
    <ENV_PY> scripts/clean_source.py 原始图.png 03-标注/主图.png
    
    原理:paper = 大核中值滤波,线条 = gray < paper-18,彩色 = 饱和度>70 且 gray<190, 只保留 线条|彩色,其余填纸底;四周 8px 强制纸底去边缘残留。

阶段 2.5 · 空间叙事板(决定成片"乱不乱",必须先画这个)

在写出图提示词之前,先用文字把主线定死。分两类:

题材主线布局原则
历史路线 / 战役 / 迁徙真实地理路线起点在真实方位,走"西→北→东"就老老实实这么排;节点的 x/y 与真实方位一致
技术发展史 / 公司史一条横向时间轴轨道贯穿全幅,节点等距,图标上下交替挂(上→下→上),避免全部堆在一侧

硬约束:

  • 只允许一条主线。不要在主线上再插画"补充说明"式的零散元素。
  • 元素之间不重叠,且每个元素都能被一个不跨主线的矩形框住(否则 region 会互相扣减)。
  • 主线要贯穿全幅:留白均匀分布在主线两侧,不要出现"全部挤在右下角、左上一大片空"。 实测长征东南起点在右下角,前 20 秒画面右下堆叠、左上全空 —— 这是构图本身的锅,出图时就要把 起点往中间收,让路线把画布用满。

出图提示词里要写明每个元素在画面中的位置("右下角一面小红旗""中间偏左一座铁索桥"), 不要只写"画一座桥" —— 位置不指定,AI 给的构图就没法用。

阶段 2.6 · 两种底图范式(实测参数,直接照抄)

成片"能不能满屏、镜头推不推得动",出图阶段就决定了。两种范式各有一套参数,别混用:

A. 地理路线型(历史/战役/迁徙) —— 代表作:红军长征路线

项取值依据
构图起点按真实方位,路线西→北→东贯穿全幅构图错,节奏再准也救不回
节点数6–8 站一站两幕(画物 + 写字)→ 12–16 元素
镜头 --zoom-fill0.86元素之间留白多,推近才不空
密度元素可大可小(桥比旗子宽 3 倍也 OK)路线本身就是不同尺度的东西

B. 横向时间轴型(技术史/公司史/产品迭代) —— 代表作:Agent 智能体发展史 (用户评价"效果很不错"的那版,参数照抄即可)

项取值依据
轨道一条 S 形/波形主轴贯穿全幅,8 个节点等距等距 → 镜头平移速度恒定,看着稳
图标上下交替(上→下→上…),不要全排一行一排就只剩半幅画面可用,取景纵向浪费一半
标签挂在图标同侧更外侧:上方节点的标签在上面,下方节点在下面和图标同处一屏,写字的镜头不用再摇走
字号主名 36–40px 楷体,年份 0.6 倍在下方取景推近后 ≈ 屏幕 60–80px,手机可读
镜头 --zoom-fill0.70节点左右挨得近,推太近会把邻居拦腰切断
一站两幕配比画图标 0.60 / 写「名字 + 年份」0.40写字幕必须留够时间,否则字还没写完就切走
时间轴本体画在相邻两站之间,跟该站 icon 同幕处理否则轴线会一次性整条出现,节奏断掉

B 型的三条硬注意:

  1. 文字幕不要套墨迹包围盒(见铁律 19 例外)。标签字常比 region 宽,套 bbox 会把整句裁到画外。
  2. 标签的 region 要按「主名 + 年份」两行留高,高度给 80–90px;只按主名一行给会切掉年份。
  3. 结尾必须拉远到全轴。时间轴的叙事价值在"整体跨度",最后 3–5 秒不拉远等于白画。

实测基线(1280×720 / 24fps):8 节点 × 2 幕 = 16 元素,成片 43.5s,渲染 33s, --zoom-fill 0.70 --zoom-max 7 --hand-height 350。

阶段 3 · 像素级标注(最容易翻车,必须校验)

先量坐标,不要目视估。实测教训:把 640×360 的预览显示尺寸当成 1280×720 实际坐标, 导致两轮坐标全错。正确做法是用颜色/线条检测拿真实 bbox:

<ENV_PY> scripts/detect_marks.py 03-标注/主图.png 标签   # 红/橙/蓝点缀的连通域 bbox
<ENV_PY> scripts/detect_lines.py 03-标注/主图.png 标签   # 深灰线条簇 bbox

需人眼校准时叠加标尺:scripts/grid_ruler.py 主图.png 带网格.png(100px 网格,标的是真实像素)。

region 覆盖率自检(决定成片是否完整):

  • 所有 region 的 x 区间要连成不间断的带(允许重叠,不允许留缝)
  • 贯穿性主体(路线、时间线)必须被某几个 region 完整覆盖,否则成片断线
  • 末位元素单独挑小块区域(见铁律 3)

写 03-标注/<名字>.annotation.json:

{
  "sceneId": "scene-01",
  "canvas": { "width": 1280, "height": 720 },
  "storyBasis": "本幕字幕事件摘要",
  "sceneDurationMs": 56000,
  "elements": [
    {
      "id": "ruijin", "label": "出发·瑞金", "sequence": 1,
      "narrativeRole": "长征起点", "subtitle": "对应字幕原文",
      "type": "point",
      "region": { "x": 265, "y": 515, "width": 145, "height": 100 },
      "reveal": { "direction": "left_to_right", "startMs": 0, "durationMs": 8100,
                  "maskPaddingPx": 16, "protectedRegions": [] },
      "handPath": { "start": [275, 570], "end": [400, 575], "easing": "easeInOut" }
    }
  ]
}
  • canvas 必须等于源图真实像素;region 必须整数像素且在画布内
  • sequence 按叙事顺序,不是视觉从左到右
  • stream 画法下 direction/handPath 只供预览台演示,不影响成片笔迹

出校验图并逐格肉眼确认:

<ENV_PY> scripts/region_sheet.py 主图.png 标注.json 05-录屏截图/校验-region.png

每格应恰好框住目标元素;框住空白或别的元素 → 坐标错,回上一步。

阶段 4 · 配音与时间轴对齐

① 配音:整篇一次生成,绝不逐句分段(2026-11-05 返工教训)

"<TTS_PYTHON>  # 任意装有 requests 的 Python 3.9+" \
  "jimeng-suite/scripts/jimeng_cli.py" audio \
  --text "整篇解说,一段话,用句号断句" \
  --model "SeedAudio 1.0" --submode "全能配音" --voice "狒狒_讲解" \
  --out "xxx-voice.mp3" --outdir "<项目>/02-字幕与配音" --timeout 900
  • Seed TTS + 文本朗读 已于 2026-10-24 下线,不要再走
  • --text 一次喂整篇。单次能吃掉 1500+ 字(≈2 分钟音频),别再拆成一堆三秒的小段: 实测把 56 秒的稿子拆成 12 段分别生成再拼接,各段语速/停顿/重音互不一致, 听感就是"胡言乱语",而且拼接后时间轴与字幕必然错位 —— 用户直接判不合格。
  • 只按叙事段落切分(一段一句话群),一段 15–40 秒,全片 1–3 段搞定;能一段就别两段。
  • 内容审核会拦:军事/暴力/伤害类措辞(血战、歼灭、击败、敌人)返回 JimengContentRejectedError。改写为中性叙述("付出沉重代价""在火力封锁下"), 年份写中文数字(一九三四年)更稳
  • 99 秒台词实测 62 秒出片(1.19MB / 99.05s)

② 静音体检与节奏压缩(必做,v5 返工教训)

"整篇一次送 TTS" 不等于平台内部合成了一整段:即梦会把长文本内部切块再拼接, 块间插入 -120dB 的绝对静音。长征 v4 实测 317 字 / 99.05s 里, 31 处 ≥0.4s 停顿合计 27.2s(27.5%),其中 4.00s @14.05、2.60s @70.60 两处 是纯数字零采样(自然停顿有底噪,约 -50~-60dB,不会到 -120dB)。 有效发声其实只有 71.85s(4.41 字/秒,并不慢)——"节奏慢、怪怪的"全是空白造成的。

# 1) 体检:任何 >1.2s 的静音都要处理
ffmpeg -hide_banner -i xxx-voice.mp3 -af "silencedetect=noise=-38dB:d=0.35" -f null - 2>&1 | grep silence_start

# 2) 压缩:分级保留自然停顿,不动任何发声内容
<ENV_PY> scripts/tighten_audio.py 02-字幕与配音/xxx-voice.mp3 \
  02-字幕与配音/xxx-voice-tight.mp3 --report 06-文档/_tighten.json

tighten_audio.py:50ms RMS 窗口、<-40dB 判静音,≥1.0s 的停顿留 0.55s、 0.35–1.0s 的留 0.32s、片头留 0.12s,按样本重新拼接。 v4→v5 实测 99.05s → 83.15s,静音占比 27.8% → 14.0%,无任何 >0.7s 停顿。

压缩会改变音频长度,之后 ASR 转写、timing、SRT、ASS、标注时长、渲染 total-ms 必须全部基于压缩后的音频重跑(铁律 25)。--reuse-asr 只用于调字幕断行, 音频一变就绝不能复用旧 dump。

③ 时间轴:用配音自己的转写时间戳反推,不要用静音检测猜

<ENV_PY> scripts/asr_align.py 02-字幕与配音/xxx-voice.mp3 06-文档/文案.txt \
  --model small \
  --out-timing 03-标注/xxx.annotation.timing.json \
  --out-srt 02-字幕与配音/xxx.srt --max-chars 18 \
  --dump-asr 06-文档/_asr.json

asr_align.py 干的事:ffmpeg 转 16k 单声道 → faster-whisper(word_timestamps=True) → 铺成逐字符时间轴 → 把文案的每一句在转写全文里定位(精确找 → 首 8 字锚点 → 兜底) → 输出 timing.json(兼容 segments[i/start/end/text])与单行 SRT。

为什么不用 silencedetect:静音只说明"这里有停顿",换气、强调都会停; 句子数和停顿数一旦对不上,就只能按字数平均分配,字幕必然错位。

asr_align.py 默认还会做句边界吸附(snap_to_voice):faster-whisper 在长静音处 习惯把下一句 start 标在静音刚开始的位置,导致字幕提前亮出、观众盯着字等声音 (v4 提前了整整 4 秒)。脚本按 20ms 网格找真实发声起点并修正;--no-snap 可关闭。

④ 必须核对音频内容(判"胡言乱语"的唯一硬标准):

<ENV_PY> -c "..."   # difflib 比对 文案 vs _asr.json 的 full

相似度应 >0.8;small 模型对中文专有名词只会同音误识(弘/红、尊/遵、武/吴、锁/索), 结构(句序、句长)一致就说明朗读正确。若出现整句缺失、重复、跳跃 → 音频本身有问题,重新生成。

⑤ 字幕断行三条规则(都在 asr_align.py::split_subs 里):

  • 先按标点切小句,只有当前块 < limit×0.55 才合并下一个小句 —— 否则会出现 "踏上了举世闻名的长征之路那一年"这种跨小句粘连
  • 合并后不得超过 limit(不要留 +3 容差,20 字被均分会切出"…要走多远只"这种单字甩行)
  • 单块仍超长才均分;尾巴 ≤4 字并进前一条

⑥ 结尾留白(铁律 18 的落地手法):给最后一个 station 加 "tail": 0.30, 该段最后 30% 不排任何元素,渲染器就会用这段时间做拉远+定格。 实测 99 秒片子留 4 秒,正好够看全图。

⑦ 分镜时长分配:一元素对一句(或一句里的一个动作),durationMs 直接取该句时长, 段内按 ratio 再切"画物 / 写字"。见 scripts/make_annotation_v3.py。

阶段 5 · 渲染、合成、校验

<ENV_PY> srt-whiteboard-animation/scripts/render_stream_whiteboard.py \
  03-标注/主图.png 03-标注/主图.annotation.json 04-渲染输出/xxx-silent.mp4 \
  01-素材/hand-55pct.png \
  --cap-long-edge 1280 --fps 24 --total-ms <配音毫秒>
  • --cap-long-edge 等于源图长边时不再缩放;预览可调小加速
  • --total-ms 传配音时长(毫秒),保证画面不早于配音结束
  • 末行输出 OUTPUT=<路径>

合成音轨:

ffmpeg -y -i xxx-silent.mp4 -i 配音.mp3 -c:v copy -c:a aac -b:a 192k -shortest 成片.mp4

必查三点(各抽一帧):

  1. 开场干净米黄底,无提前露线
  2. 中段:线条连续、颜色正确(红/橙/蓝都在)
  3. 末尾:整幅完整,无断线、无空白大块

时长偏差 >1s 就调 --total-ms 或微调各 durationMs 重渲。

阶段 5.5 · 镜头与写字(v3 核心,决定"像不像在画")

上游渲染器是一张固定画布画到底。想让画面像镜头语言,加一个 render_camera_whiteboard.py 子类, 不改仓库代码(scripts/render_camera_whiteboard.py 已可复用):

<ENV_PY> scripts/render_camera_whiteboard.py 03-标注/主图.png 03-标注/主图.annotation.json \
  04-渲染输出/xxx-silent.mp4 01-素材/hand.png \
  --canvas-long-edge 2560 --frame 1280x720 --fps 24 --grid-edge 20 \
  --zoom-fill 0.86 --zoom-max 7 --hand-height 350 --total-ms 56064

它做了六件事:

机制实现为什么
镜头取景画布 2560×1440 渲染,每帧按 viewport 裁剪缩放成 1280×720元素能"满屏画",画完摇走
墨迹包围盒取景目标框 = 「该幕 allowed 掩码内的真实墨迹 bbox」,不是 regionregion 往往比图形大一圈甚至横跨邻居,直接用它取景 → 画面正中空白、图形被顶到边角(v3 第一版就翻在这)
镜头跟随相邻幕指数平滑 tau 0.20/0.30,写字幕 pad 0.34、画物 pad 0.05天然产生摇镜,不用做转场帧
换场缓冲每幕开头空转 0.22s(约 5 帧)再落笔镜头从上一幕摇过来要 0.3–0.6s,这期间落笔会"笔在画面外画",是"手在动没内容"的主因之一
结尾全图最后一幕结束后 target 设为全画布,补帧到 --total-ms"最后展示一张全图"
手部固定屏幕尺寸手不画进画布,改贴到输出帧上(--hand-height 350)否则一推近手就占满屏;430 实测仍偏大、会盖住正在写的字

--zoom-fill 要按底图疏密分别定(同一套参数不能通吃):

底图类型zoom-fill理由
稀疏(路线图/流程图,元素间有大片留白)0.86可以放心推近,画面才不空
密集(横向时间轴,元素左右紧邻)0.70推太近会把邻居切一半,显得杂乱

16:9 的硬约束:竖长元素(棋子盘、手机)受画面高度限制,满屏只能做到纵向 60–70%, 横向 25% 左右,这是几何决定的,不是 bug。要更满就得把元素画宽,或改成竖屏输出。

写字(把地名/年份写出来):

# 1) 先把标签烘进底图(坐标用 1280×720 标注坐标,脚本会自动按字号排两行)
<ENV_PY> scripts/add_handwriting.py 底图2x.png 03-标注/主图-v3.png 02-字幕与配音/标签.json
# 2) 标注里给这一幕写 "inkPath": "textwrite",region 就是文字框

textwrite 按列从右推进,笔尖在揭示前沿,保证文字一定写完(骨架追踪会漏细笔画,别用)。 前 30% 帧持笔等镜头到位,再开始写 —— 否则字会写到画框外。 标签排版:主名 34–40px 楷体(C:/Windows/Fonts/simkai.ttf)+ 年份 0.6 倍字号在下方, 颜色 (58,56,52) 与线稿同色系但略浅。

幕的拆分:一站 = 两幕。先画物(0.6 时长),紧接着写字(0.4 时长), 写字幕的取景框 = 自己的文字框,这样镜头会轻微推过去,像"笔移到旁边写字"。

阶段 6 · 字幕烧录(SRT → ASS → 烧进画面)

字幕时间轴不要另标,直接用阶段 4 产出的 .timing.json —— 它已经和口播逐句对齐, 字幕换句的时刻就是笔停下的时刻,这才是"节奏对齐"。

# 1) timing.json -> SRT(**单行**,超长自动拆成多条连续字幕)
<ENV_PY> scripts/make_srt.py 03-标注/主图.annotation.timing.json 02-字幕与配音/x.srt \
  --max-chars 14

# 2) SRT -> ASS(显式 PlayRes + \\pos 锚定底边,字号/位置是真实像素)
<ENV_PY> scripts/srt2ass.py 02-字幕与配音/x.srt 02-字幕与配音/x.ass \
  --font-family "SimHei" --font-size 46 --primary-color "&H00E8DFD5" \
  --outline-color "&H00333333" --outline-width 1.5 --margin-v 28

--margin-v 实测:720p 下 28px = 距画面底边约 4%(不遮盖字幕时露出的画面空间刚好够看手); --primary-color 用浅米黄 &H00E8DFD5(与源图纸底 #F5EBD7 近似), 立体但不会在浅色底上变隐形; --outline-color 用深灰 &H00333333 制造轮廓对比。

烧录:用 ffmpeg 内置 subtitles 滤镜(不走 libass 库,不会有分辨率误会):

ffmpeg -y -i 成片.mp4 -vf "subtitles=02-字幕与配音/x.ass" -c:a copy 成片-字幕.mp4

若移动端字体变大:检查 ASS 里 PlayResX/PlayResY 是否等于视频尺寸。 scripts/srt2ass.py 默认写死了 PlayRes: 1280 720,若源图不是 1280×720 则要手动改。

阶段 7 · 录屏与截图(交付硬指标)

录屏用 ffmpeg 控制自然结束(不要 kill):

ffmpeg -y -f gdigrab -framerate 10 -offset_x 0 -offset_y 0 \
  -video_size 1920x1080 -i desktop -t 120 -c:v libx264 -preset ultrafast \
  -crf 28 05-录屏截图/录屏.mp4

截图必含:

  • 源图(从 01-素材/,带文件名)
  • 校验图(region_sheet.py 输出)
  • 渲染过程中控制台窗口(显示命令和输出)
  • 成片中的 3 个关键帧(开场、中部动画、结尾全图)

常见问题与自动修复

Q1 · check_bug_fix.py 自动修复

<ENV_PY> scripts/check_bug_fix.py render_stream_whiteboard.py

会检测并修复:

  • _lay_ink() 参数数量错误
  • drawing-hand.png 尺寸未缩放
  • 缺失的 --zoom-fill、--hand-height 参数

Q2 · 标注坐标与预览坐标不一致

# 先从源图导出缩略图+网格,在网格上手标坐标,再换算回源图
<ENV_PY> scripts/grid_ruler.py 03-标注/主图.png 03-标注/主图_网格.png

在 03-标注/主图_网格.png 上用鼠标或绘图板标注,读出网格交点坐标(每个格 100px), 乘以放大系数(如预览 50% 则乘 2)得到真实像素。

Q3 · 渲染输出 MP4 无法在浏览器播放

srt-whiteboard-animation 默认编码可能是 H.264 但 label 未标记, 用以下命令重封:

ffmpeg -i xxx-silent.mp4 -c:v libx264 -profile:v high -level:v 4.0 \
  -pix_fmt yuv420p -c:a aac -b:a 128k -movflags +faststart xxx-fix.mp4

Q4 · 手部素材路径错误

确保 hand-55pct.png 存在于本地。若无,用 scripts/resize_hand.py 从原图生成:

<ENV_PY> scripts/resize_hand.py 01-素材/drawing-hand.png 01-素材/hand-55pct.png --scale 0.55

Q5 · 图片裁剪后元素丢失

现象:左右补齐后元素被裁切。 原因:补齐前未检查补齐方向。修正:用 scripts/pad_to_16_9.py 自动检测并补齐。

<ENV_PY> scripts/pad_to_16_9.py 原始图.png 03-标注/主图.png --fill-color "#F5EBD7"

时间线示例(以长征 8 节点为例)

时间步骤产出
Day 1 上午写 8 句解说,确认分镜分镜表
Day 1 下午AI 出主图 + 净化、加标签净化后 1280×720 底图
Day 2 上午像素级标注(region、ink、textwrite)annotation.json
Day 2 下午生成配音 + 压缩 + ASR 对齐配音 mp3 + timing.json + SRT
Day 3 上午渲染 silent.mp4 + 拍录屏截图silent.mp4 + 截图组
Day 3 下午合成音轨、烧录字幕、验收成片-字幕.mp4

验收清单

  • 成片时长 ±0.5s 与配音一致
  • 手部素材缩放至画布宽 21% 以内(55% 原始尺寸)
  • 无提前露线、无墨迹溢出
  • 全部线条连续、颜色保留
  • 字幕单行、唯一、无错位、断行不甩字
  • 结尾有≥3s 定格展示全图
  • 录屏文件无 moov atom 错误(可用 ffprobe 检测)
  • 截图覆盖源图、校验图、渲染控制台、成片 3 帧
  • 文案与转写相似度 ≥0.8(difflib 核对)
  • 配音无 >1.2s 空白(tighten_audio 报告)
  • B 型时间轴:节点等距、图标交替、结尾拉远
  • A 型路线:空间方位正确、路线连续、起点终点清晰

관련 스킬