手绘白板动画制作全流程(SRT → 手绘动画成片)
基于开源项目 srt-whiteboard-animation(github.com/geeklee/srt-whiteboard-animation)实测沉淀。 产出:带配音的「手握笔边画边讲」白板动画 MP4 + 关键截图 + 操作录屏。
铁律(违反即返工,全部来自实测翻车)
- 禁止拿示例图当交付。项目
examples/里的scene-01-monkey-mountain-banana.png只能验证管线, 用户要什么主题就必须生成什么主题的图。用示例图交付 = 交付失败。 - 源图必须净化。AI 生成的米黄纸底带大量纹理颗粒,渲染器
adaptiveThreshold会把纸纹判成墨迹, 成片线条碎成点阵、背景爬满噪点。必须跑scripts/clean_source.py去纸纹后再渲染。 - 末位元素禁止用整幅 region。
allowed mask = region − 后续 region,若最后一个元素 region 是整幅画布, 它会把前面所有元素的 allowed 扣空 → 前几幕什么也画不出来、颜色全丢(实测确认)。 收尾幕要挑一块真实的小区域(如顶部云带)。 - 手部素材要按画布缩放。原始
drawing-hand.png是 1069×1472,直接贴到 1280×720 会占屏 40%+, 严重遮挡画面。实测缩到 55%(587×809),手宽约占画布 21%,观感正常。 - 渲染输出时长会短于标注。渲染器按各区域实际可绘制像素决定帧数,
sceneDurationMs只是上限。 实测 8×8000ms=64s 的标注只渲出 45.8s。必须传--total-ms,并以 ffprobe 实测时长校准。 - 录屏不能用 kill 结束。ffmpeg 被强杀会丢 moov atom,文件报
moov atom not found。 用-t <秒>让录制自然结束。 - 必须交付录屏与截图。这是需求的一部分,不是可选项。截图要覆盖:源图、标注校验图、 渲染控制台、成片关键帧。用「文档记录代替截图」属于未完成。
- 一张画布只讲一件事,而且必须是「连续推进」。 实测返工:把红旗、桥、雪山、宝塔平铺在一张图里一次性同时画 → 用户评价"画得很乱"。 正确做法是让元素沿一条主线(地理路线 / 时间轴)排开,笔迹按主线的行进方向连续移动, 观众看到的是"笔在走路",而不是"在一张画布上到处补细节"。
- 空间布局必须服从真实走向。画长征就要按真实地理:瑞金在东南 → 向西 → 北上 → 陕北。 构图错了,节奏再准也救不回来。画之前先在地图上确认每个节点的相对方位。
- 手绘节奏 = 口播节奏,一元素对一句。每个元素的
durationMs必须等于它那句配音的实际时长 (用sync_timing.py从静音检测反推),绝不允许平均分配。第 N 幕的笔停在"第 N 句说完"的瞬间。 - 幕与旁白语义必须一一对应。实测踩过:宝塔(吴起镇)排在总结句那一幕,结果旁白说"抵达吴起镇"
时画面在画别的东西。收尾的总结句("二万五千里…")不要配新元素,让它落在"全部画完的定格"上,
渲染器会自动补
--total-ms的空帧做凝视。 - 字幕要单独烧,不要指望
force_style。纯 SRT 没有 PlayRes,libass 用内部默认分辨率换算FontSize/MarginV,实测调不出准值。走make_srt.py→srt2ass.py(显式写 PlayRes = 视频尺寸, 并用{\\an2\\pos(x,y)}锚定底边)才可控。 - 字幕只允许一行。两行会同时占掉画面上下两块,中文两行读起来也断气。 一行装不下就拆成多条连续字幕(按字数比例分摊时长),不要压行、不要缩字号。 实测:46px 字号 + 单行 ≤14 字,720p 下约占屏宽 60%,手机上看刚好。
- 画布要能移动,别把整张图塞进一个固定镜头。观众反馈"没必要一张画布画完"—— 正确做法是把画布放大(2 倍)渲染,每幕只把当前元素取景到满屏,画完镜头摇到下一个, 最后拉远到全图收尾。一张固定全景从头看到尾 = 每个元素只占屏幕 10%,什么都看不清。
- 笔尖必须跟着内容走。上游
contour-wipe是「整片自下而上擦除 + 笔尖横向乱扫」, 两者完全脱钩 → 用户看到"手在动但没东西画出来"。改成蛇形笔刷:笔扫到哪,颜色出现在哪。 - 讲关键节点时要写字。只画物不写字,观众记不住"哪一年、什么地方"。
把地名/年份用楷体烘进底图(
add_handwriting.py),再让渲染器用textwrite模式逐列写出来 —— 笔顺着笔画推进,字一个个出现,跟"手写"是同一个动作。 - 画布放大后必须做两件事,否则要么慢死要么糊:
① 源图先用
INTER_CUBIC预放大到画布尺寸(渲染器内部的resize是INTER_AREA, 放大时等价最近邻,会把线条变成方块); ② 落墨改按包围盒局部处理(上游每段都 new 一张整画布掩码,2560 画布下慢 4 倍)。 - 合成音轨前的成片必须留 3–7 秒给结尾拉远。最后一幕不要排到配音结束的最后一毫秒, 否则"展示全图"这个收尾动作没有时间发生。
- 取景框要用「真实墨迹包围盒」,不能用标注 region。region 是手画的框,普遍比图形大一圈,
密集图里还会横跨 2–3 个邻居;直接拿它算 zoom,结果是"画面正中空白、图形缩在角落、邻居被切一半"。
渲染器已内置:
set_target(element, allowed)里用ink_pixels & allowed求 bbox 再外扩。 例外:文字幕不走 bbox——标签字常比 region 还宽,套 bbox 会把整句裁到框外(实测「对话式 AI」出画)。 - 换场要先摇镜头再落笔。每幕开头空转约 5 帧(0.22s),否则笔在镜头还没到位时就开始画, 画到画面外,观众看到"手在动却没内容"。
--zoom-fill按底图疏密分别取值,稀疏图 0.86、密集时间轴 0.70;一个参数连通吃会两头翻车。- 竖长元素满不了屏要接受。16:9 画面里,纵向元素只能占 60–70% 高、25% 宽,这是几何约束; 想更满就把元素画宽,或者出竖屏。
- 配音整篇一次生成,禁止逐句分段再拼接。分段拼接 = 各段语速/停顿不一致 + 时间轴必然错位, 用户听到的评语就是"胡言乱语"。即梦单次可吃 1500+ 字(≈2 分钟),整篇一次喂进去。
- 时间轴来自配音的转写时间戳,不来自静音检测。停顿 ≠ 句子边界;句子数对不上就只能平均分配,
字幕必错。用
asr_align.py(ffmpeg → faster-whisper word_timestamps → 字符级时间轴)。 - 换了配音就必须重派生一遍字幕和标注时长。字幕时间戳是那条音频的属性,不是画面的属性; 旧字幕配新音频等于没做。
- 交付前用 difflib 核对「文案 vs 转写」。相似度 <0.8、或出现整句缺失/重复 → 音频有问题,重生成。 同音误识(弘/红、尊/遵、武/吴)属正常 ASR 现象,不算朗读错误。
- 配音生成后必须做静音体检。"整篇一次送 TTS"不代表平台内部不切块:
即梦会在块间插入 -120dB 绝对静音(实测 4.00s / 2.60s)。静音占比 >20% 或存在
1.2s 的停顿 → 先用
tighten_audio.py压掉,再谈时间轴对齐。 - 字幕起点要吸附到真实发声帧。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 · 解说稿与分镜(先确认再往下)
- 写 6–10 句解说,每句 6–10 秒,总长 40–90 秒。
- 解析分镜(可选,也可直接手排):
<ENV_PY> srt-whiteboard-animation/scripts/parse_srt.py 字幕.srt --target-sec 30 --min-sec 25 --max-sec 35 - 输出分镜表(第几幕 / 核心表达 / 画面主体 / 字幕原文),停下等用户确认。
阶段 2 · AI 出主题手绘图
按项目「统一出图视觉规范」写出图提示词,每条都必须含:
- 极简手绘线稿 / 纯素描草图 / Notion 式克制涂鸦,16:9,大量留白,元素互不重叠
- 背景暖米黄
#F5EBD7,线条深灰#333333,仅红/橙/蓝少量点缀 - 绝对禁止任何文字(汉字/字母/数字/标签/地名/标题/水印),禁止写实/3D/渐变/高饱和
用 ImageGen 或即梦 image 出图。每张图对应一幕核心意思,元素要能被矩形分区切开。
出图后必做三件事:
- 去水印:AI 出图右下角常有
AI生成 WORKBUDDY,用cv2.inpaint抹掉。 - 转 16:9:生成图多为 3:2,左右补米黄纸底后缩放到 1280×720(不要居中裁切,会切掉主体)。
- 净化:去纸纹,保留线条与彩色点缀。
原理:<ENV_PY> scripts/clean_source.py 原始图.png 03-标注/主图.pngpaper = 大核中值滤波,线条 = gray < paper-18,彩色 = 饱和度>70 且 gray<190, 只保留线条|彩色,其余填纸底;四周 8px 强制纸底去边缘残留。
阶段 2.5 · 空间叙事板(决定成片"乱不乱",必须先画这个)
在写出图提示词之前,先用文字把主线定死。分两类:
| 题材 | 主线 | 布局原则 |
|---|---|---|
| 历史路线 / 战役 / 迁徙 | 真实地理路线 | 起点在真实方位,走"西→北→东"就老老实实这么排;节点的 x/y 与真实方位一致 |
| 技术发展史 / 公司史 | 一条横向时间轴 | 轨道贯穿全幅,节点等距,图标上下交替挂(上→下→上),避免全部堆在一侧 |
硬约束:
- 只允许一条主线。不要在主线上再插画"补充说明"式的零散元素。
- 元素之间不重叠,且每个元素都能被一个不跨主线的矩形框住(否则 region 会互相扣减)。
- 主线要贯穿全幅:留白均匀分布在主线两侧,不要出现"全部挤在右下角、左上一大片空"。 实测长征东南起点在右下角,前 20 秒画面右下堆叠、左上全空 —— 这是构图本身的锅,出图时就要把 起点往中间收,让路线把画布用满。
出图提示词里要写明每个元素在画面中的位置("右下角一面小红旗""中间偏左一座铁索桥"), 不要只写"画一座桥" —— 位置不指定,AI 给的构图就没法用。
阶段 2.6 · 两种底图范式(实测参数,直接照抄)
成片"能不能满屏、镜头推不推得动",出图阶段就决定了。两种范式各有一套参数,别混用:
A. 地理路线型(历史/战役/迁徙) —— 代表作:红军长征路线
| 项 | 取值 | 依据 |
|---|---|---|
| 构图 | 起点按真实方位,路线西→北→东贯穿全幅 | 构图错,节奏再准也救不回 |
| 节点数 | 6–8 站 | 一站两幕(画物 + 写字)→ 12–16 元素 |
镜头 --zoom-fill | 0.86 | 元素之间留白多,推近才不空 |
| 密度 | 元素可大可小(桥比旗子宽 3 倍也 OK) | 路线本身就是不同尺度的东西 |
B. 横向时间轴型(技术史/公司史/产品迭代) —— 代表作:Agent 智能体发展史 (用户评价"效果很不错"的那版,参数照抄即可)
| 项 | 取值 | 依据 |
|---|---|---|
| 轨道 | 一条 S 形/波形主轴贯穿全幅,8 个节点等距 | 等距 → 镜头平移速度恒定,看着稳 |
| 图标 | 上下交替(上→下→上…),不要全排一行 | 一排就只剩半幅画面可用,取景纵向浪费一半 |
| 标签 | 挂在图标同侧更外侧:上方节点的标签在上面,下方节点在下面 | 和图标同处一屏,写字的镜头不用再摇走 |
| 字号 | 主名 36–40px 楷体,年份 0.6 倍在下方 | 取景推近后 ≈ 屏幕 60–80px,手机可读 |
镜头 --zoom-fill | 0.70 | 节点左右挨得近,推太近会把邻居拦腰切断 |
| 一站两幕配比 | 画图标 0.60 / 写「名字 + 年份」0.40 | 写字幕必须留够时间,否则字还没写完就切走 |
| 时间轴本体 | 画在相邻两站之间,跟该站 icon 同幕处理 | 否则轴线会一次性整条出现,节奏断掉 |
B 型的三条硬注意:
- 文字幕不要套墨迹包围盒(见铁律 19 例外)。标签字常比 region 宽,套 bbox 会把整句裁到画外。
- 标签的 region 要按「主名 + 年份」两行留高,高度给 80–90px;只按主名一行给会切掉年份。
- 结尾必须拉远到全轴。时间轴的叙事价值在"整体跨度",最后 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
必查三点(各抽一帧):
- 开场干净米黄底,无提前露线
- 中段:线条连续、颜色正确(红/橙/蓝都在)
- 末尾:整幅完整,无断线、无空白大块
时长偏差 >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」,不是 region | region 往往比图形大一圈甚至横跨邻居,直接用它取景 → 画面正中空白、图形被顶到边角(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 型路线:空间方位正确、路线连续、起点终点清晰