原理流程类动画+讲解(配音讲解版)
为一个知识点(原理、流程、机制)制作讲解词与画面逐词同步的配音动画。交付两件东西:
- 字幕版 HTML(无配音)——阶段③的用户审阅物:讲解词逐句上屏,时间轴即成片时间轴;
- MP4(含配音音轨)——阶段④的成片:字幕版 HTML 逐帧渲染后并入配音音轨。
方法论一句话:不是「做完动画再配语音」,而是「先有语音,才有时钟」——动画时间轴由配音音频的实测时长反推而来。台词是唯一真相,画面是台词的函数。
两类错误必须分开管:
- 同步类错误(「画面比语音慢」「文字重叠」)——本技能主战场,下文全程覆盖。
- 学科类错误(概念讲错、隔离框圈错、公式脱成白板)——本技能管不了,须查 references/subject-accuracy.md。
同步做得再准,知识讲错,等于把错误包装得更精致。渲染前先过学科自检清单。
与上游技能的分工:teaching-animation-maker(无配音版)提供视觉风格(三风格/情景类)、Canvas 骨架、抽帧接触表、版式自检;本技能解决配音工程与音画同步,并自带 scripts/render_mp4.js(逐帧渲染)、syntax_check.js、fix_quotes.py,故发布后无需依赖任何上游技能即可完成全流程。
1 何时用
| 适用 | 不适用 |
|---|---|
| 要教学动画/原理动画/讲解视频,且要求配音 | AI 生图/生视频(文字必错、不可逐帧复现) |
| 抱怨「画面比语音慢」「音画对不上」「文字重叠」 | 真人出镜 |
| 需要语速中等偏慢、便于学生消化的讲解 | 影视级外挂字幕轨(本技能字幕做在画面上) |
| 需要 HTML 预览 + MP4 双形态且时间轴一致 | 纯背景音乐类视频(无逐句对齐需求) |
2 核心原理:以音定时
2.1 三层时钟
| 层 | 载体 | 生成方式 | 失效表现 |
|---|---|---|---|
| 数据层 | cues.json |
TTS 实测 + 能量反推 | 手改或未重跑 → 误差随时间增长 |
| 资产层 | audio/voice_track.wav |
build_audio.py 按 cues 摆放 |
padding 未抵消 → 全片恒定偏移 |
| 视图层 | HTML 的 LINES/TP |
inject_clock.py 注入 |
忘了注入 → 只有个别配合点偏 |
定位口诀:误差随时间增长 → 查数据层;全片恒定偏移 → 查资产层;只有个别点偏 → 查视图层。
2.2 LEAD(画面提前量)
所有画面事件 = 台词关键词起音时刻 − LEAD(默认 500ms)。依据:人对声画的错位会自动「对齐」,视觉在前、声音在后听感最自然;反过来则像反应慢半拍。
| 场景 | LEAD |
|---|---|
| 讲解型知识点(默认) | 500ms |
| 情绪型导入/悬念铺垫 | 600~800ms |
| 逐字跟随型(跟读/字幕/笔顺) | 0 |
LEAD 是 HTML 里的一个常量,改它只需改一处,不重跑任何脚本。
3 七条铁律
- 时长一律实测,绝不估算。 每句 MP3 用
ffprobe读真实 duration;「字数 × 经验语速」只能用于方案预算,禁止用于定轴。 - 配合点查每字起音时钟表,绝不线性插值。 标点停顿与语速波动会让按字数均分累积 1~2 秒误差。
- 画面事件挂台词关键词,绝不手写毫秒常量。
if(t>43000)这类写法在改词后必然漏改、必然错位。 - 音轨按 cues 逐句摆放(
build_audio.py),禁止 ffmpegconcat/apad。 concat 会吃掉设计的句间停顿与幕间留白,语音逐渐早于画面。 - 改词后先清缓存、再重跑四步链:清空
audio/下S*-L*.mp3、_bound/、_pcm*/、voice_track.wav,然后tts_build → align_clock → build_audio → inject_clock。断点续跑凭audio/_bound/复用已成功句——不清缓存,改过的词根本不会被重新合成。区分两种场景:网络失败续跑 → 不要清缓存;改词重跑 → 必须清。漏任何一步,音画错位。 - 重渲必须换新帧目录 + 新 total_ms。 改词后
TOTAL变化 → 帧数变化 → 旧帧目录必须清空(或换新目录),渲染脚本传入新 total_ms,ffmpeg-frames:v用新帧数。否则旧帧混入,成片尾段冻结或错位。 - 改配音参数(术语标注/语速/音色/句式)必先做片段 A/B 对照:取一句或一幕,两版同音色同速率合成,客观比时长 + 请用户听感拍板,确认后才铺全片重跑。禁止凭推测直接全片重跑(见 §5.2)。
- 四段闸门:每段经用户确认才进入下一段(用户要求全自动化时,把可决策项一次列全、择优选定并在方案中注明)。
- 符号只上屏,不入口播。 台词里禁止出现孤立单字符的物理量符号与下标(
TGa₁G₁)——TTS 会把它们读成拼音。台词改说「两个一样大的拉力,等于一个重量」,符号只在画面上出现。 - 多步推演必须互斥,且步数 > 3 时互斥窗口放宽到 0.6。 每步 alpha 乘
1-sg(下一步,0,0.6)(前一步先淡出后一步才进),且每步内容压到 2 行以内。窗口 0.45 在 4 步以上仍会叠字。
4 执行流程(四段闸门)
① 方案+逐幕脚本 →确认→ ② TTS合成+cues →确认→ ③ 字幕版HTML(无配音)→确认→ ④ 渲染MP4+配音音轨
① 教学动画设计方案 + 逐幕脚本
先写 教学动画设计方案.md 交用户审阅,审阅通过才写代码。方案包含:一句话方案、配音参数(音色/语速/换算公式)、合拍机制说明、总体参数、逐幕脚本、讲解词连读稿、数据与画面设定、待拍板项。
逐幕脚本格式(每幕四栏,让用户能直接判断合拍是否合理):
| 幕 | 画面动作 | 讲解词 | 音画配合点 | 时长预算 |
|---|---|---|---|---|
| 3·正常合闸 | 把手下压→触头闭合→电路接通 | 「按下把手,机构带动触头闭合,电路接通。」 | 「按下把手」→把手开始下压(画面早0.5s);「触头闭合」→触头合到位 | ~15s |
台词写作要求:
- 每句 15~32 字、句号结尾;关键动作必须落在句中某个词上——那个词就是配合点。
- 语速教学讲解用
rate=-10%(实测约 226 字/分、0.265 s/字);以实测为准,估值只用于方案预算。 - 数字与单位要读得出来(「16 安」不写「16A」);少用书面语长句。
- 体量:每幕 3~5 句,全片 20~30 句 / 140~170 秒。
- 术语断句规则(实测有效):专业术语、英文缩写(token / Prompt)、核心概念(神经网络、前向传播、概率分布、采样、激活等)一律用「」直角引号括起。原理:zh-CN 神经 TTS 把「」视为隐形分组标记——不读出声,却在术语前后形成轻微停顿(实测约 +0.17s),改善术语被读散。若某术语加「」仍读散(典型:被定义的新术语,如「这叫自回归」),改用破折号点在术语前(「这叫——自回归:」),破折号制造约 0.4s 的明显停顿且不发声。统一用「」不用弯引号——弯引号会被写入工具规范化为 ASCII 引号,且易被 TTS 误读。
估时公式(仅方案预算用):
句时长 ≈ 汉字数 × 0.265s (rate=-10% 实测值)
全片 ≈ Σ句时长 + (句数-1)×0.25s + Σ幕末gap(2.5~4.5s)
② TTS 合成 + cues.json
python <skill>/scripts/tts_build.py 台词.json #→ audio/*.mp3 + cues.json + _cues_report.txt
python <skill>/scripts/align_clock.py #→ cues.json 的 clock[] + _clock_report.txt(必跑)
检查两个报告:
_cues_report.txt:marks全 0 属正常(edge-tts 7.2.8 已移除 WordBoundary,所以align_clock.py是必跑项);末尾的实测语速(字/分)要写回方案。_clock_report.txt:所有句「单调 = OK」。若 clock 呈399/400/401机械等差,说明退回了均分保底值,对齐失败,查音频。
> 批量合成会超过单次命令 120s 上限,且网络失败率高(连接重置/NoAudioReceived):脚本已内置 4 次重试 + 断点续跑,必须后台跑,失败后重跑同一条命令即可续。
③ 字幕版 HTML(无配音)
复制 templates/sub_skeleton.html,替换画布绘制部分(结构契约见 §6),然后注入:
python <skill>/scripts/inject_clock.py xxx_sub.html #→ 就地改写 LINES/SC/TOTAL/tOf + _inject_report.txt
本阶段的 HTML 不含任何音频:讲解词由 drawSubtitle(t) 按 LINES 逐句显示在画面上,用户据此审阅「画面与讲解词是否合拍」。字幕即配音的占位——每句字幕的显示区间 = 该句配音区间,成片后与声音逐句对位。
验收 _inject_report.txt:「未命中 = 0」才算通过(走 §5.3 remap 时除外,那是已知豁免)。未命中说明配合点关键词不在台词里(多为改词后没同步),画面会退化成句中点兜底,肉眼可见地不准。
写画面三条约束:
- 一切时刻挂
tOf('S3-L1','按下把手'),sg(t, a, b)取局部时间窗做淡入淡出。 TP表声明在前、绘制在后,便于排查哪个事件没触发。- 相邻元素各自独立淡入淡出:共用同一 alpha 会叠字;跨幕换位用「旧位淡出+新位淡入」两段式,不要滑行穿场。
- 推演板多步(>3)时互斥窗口放宽到 0.6,见铁律 10。
line(x1,y1,x2,y2,w,color,dash)的dash必须是数组且在第 7 位——传字符串或错位会抛setLineDash TypeError,只在逐帧 seek 时炸,读代码看不出来。- 画绳绕滑轮要单独设绳槽半径
RS=R+8,配arc(cx,cy,RS,Math.PI,0,true)才走下半圈。 - 公式推演不要脱离装置:把装置绘制器参数化,做「左装置 + 右方程」,每步同步高亮装置上对应的力。详见
references/subject-accuracy.md铁律 D。
④ 渲染 MP4 + 并入配音音轨
渲染前必跑回归(把整条时间轴走一遍,报错为 0 才值得花 8~11 分钟去渲):
node <skill>/scripts/regress.js "<项目>/xxx_sub.html" --step=400
# 期望:坏帧 0 报错 0 + ✓ 回归通过,可以渲染。
# 退出码 1 = 有报错(必修);2 = 未跑 inject_clock(时间轴未注入)
它能抓到逐幕抽帧看不见的作用域错误(如某变量只在幕3 赋值、幕4 之后引用)。
可选 --shots=DIR 同时存抽样帧,省去单独调 capture.js。详见 references/render-compose.md §1.5。
先摆音轨,后渲染合成:
python <skill>/scripts/build_audio.py #→ audio/voice_track.wav + _audio_build.log
验收 _audio_build.log 逐句误差:除首句外应全部 ≤ 10ms;首句约 200~300ms 属正常(起点被 clamp 到 0,padding 无法完全抵消,偏差由 LEAD 覆盖);中间某句 > 100ms 才说明摆放逻辑有问题,需排查。
渲染规则(分批续渲、ffmpeg-static)见 references/render-compose.md,成片合成:
ffmpeg -y -framerate 30 -i "frames/%05d.png" -i "audio/voice_track.wav" \
-c:v libx264 -preset medium -crf 18 -pix_fmt yuv420p -movflags +faststart \
-c:a aac -b:a 128k -shortest -frames:v <帧数> out.mp4
-frames:v(防旧帧混入)与 -pix_fmt yuv420p(部分播放器必需)必给。第二路输入必须是 build_audio.py 产出的 voice_track.wav;禁止在 filter 里现场拼配音(铁律 4)。
5 迭代修订
成片交付后改台词、改配音参数、改画面,是常态。修订有自己的流程,乱改必错。
5.1 改词/改参数的重跑清单
1. 清缓存 audio/S*-L*.mp3、audio/_bound/、audio/_pcm*/、audio/voice_track.wav
2. 重跑四步链 tts_build → align_clock → build_audio → inject_clock
3. 重渲 清空(或换新)frames/ → 传新 total_ms → 全量重渲
4. 重合成 ffmpeg,-frames:v 用新帧数
5. 旧版备份 成片改名 *_prev.mp4 供对比
判定信号:cues.total 变了 → 帧数必变 → 第 3 步不可跳过。断点续跑只按文件存在与否判断,不校验内容——不清缓存就等于没改。
5.2 片段 A/B 对照法(改参数的标准验证)
全片 30 句重跑约 2 分钟 + 用户重听全片成本高,所以先用最小片段验证参数:
- 取受影响最大的一句或一幕(如术语所在句),原版与改版同音色同速率各合成一遍;
- 客观数据:
ffprobe比时长差(估算标注效果强弱,如「」≈+0.17s、破折号≈+0.41s); - 主观听感:把两版连成一个对照音频(A→1s 静音→B)交用户拍板;
- 确认后才铺全片、走 §5.1 清单。
同参数下时长差 ≈ 0 说明标注没被 TTS 读懂(如被读出声会 +0.5s 级);差值落在 0.1~0.5s 说明只产生了停顿——这正是「」和破折号的预期行为。
5.3 段映射 remap(音频时间轴大幅变化时的应急路径)
适用:幕数不变、只改句内文字/参数,而已有 HTML 的视觉代码内部时间耦合过紧(画面事件互相咬合、逐点改 tOf 风险高)。做法:保留旧视觉时间轴,在 render(nt) 入口把新音频时间 nt 映射回旧视觉时间 ot——按新旧幕区间(SC vs 旧区间表)逐段对齐、段内按比例拉伸:
function remap(nt){
for(var i=0;i<SC.length;i++){
if(nt<=SC[i].e){
var u=(nt-SC[i].s)/Math.max(1,SC[i].e-SC[i].s);
return OLD_SC[i][0]+u*(OLD_SC[i][1]-OLD_SC[i][0]);
}
}
return OLD_SC[SC.length-1][1];
}
代价与豁免:
- 进度条等 chrome 元素要用真实音频时间(映射前),画面用映射后时间;
inject_clock找不到tOf锚点 → 「未命中」计数失真,属预期,音画对齐改由跨时间轴抽帧目视核验;- 幕序或幕数变了 → remap 失效,必须走
tOf正路重写配合点。
6 HTML 结构契约
inject_clock.py 依赖以下结构,改写 HTML 时必须保留(完整骨架见 templates/sub_skeleton.html):
var LINES={ 'S1-L1':{s:0,d:4872,c:[199,300],x:'原文'}, ... }; // inject_clock 注入,勿手改
var SC=[ {id:1,t:'幕名',s:0,e:19080}, ... ]; // 幕区间,同上
var TOTAL=156970;
/* 关键词 -> 时刻:查 clock 表 */
var LEAD=500;
function tOf(id,phrase){
var L=LINES[id]; if(!L) return 0;
var i=L.x.indexOf(phrase);
if(i<0||!L.c) return Math.max(0,L.s+L.d*0.5-LEAD); // 兜底:句中点
return Math.max(0,L.s+L.c[i]-LEAD); // 正路:查每字时钟
}
var TP={ arcCh: tOf('S2-L2','左边是灭弧室'), ... }; // 配合点集中声明
window.__TOTAL=TOTAL; window.__seek=function(ms){manual=true;render(ms);};
数据流:
台词.json ─(tts_build)→ mp3 + cues.json ─(align_clock)→ clock[]
├─(build_audio)→ voice_track.wav ─┐
└─(inject_clock)→ HTML LINES/TP ──┴→ ffmpeg 合成 MP4
7 坑速查
细节与修法见 references/troubleshooting.md(按「症状→根因→修法」组织)。
| 症状 | 根因 | 对策 |
|---|---|---|
| 画面整段比语音晚 1~2s,越往后越晚 | 配合点按字数线性插值 | 查 clock 表(align_clock.py + inject_clock.py) |
| 改了词但配合点没动 | 忘了重跑 inject_clock.py |
重跑四步链 |
| 改了词但配音没变 | 清缓存漏了 _bound/,续跑复用旧 mp3 |
按 §5.1 第 1 步清全缓存再跑 |
| 成片尾段冻结/画面错位 | 重渲混入旧帧或 total_ms 仍是旧值 | 清 frames/ + 新 total_ms + 新 -frames:v(铁律 6) |
| 全片语音恒定偏早/偏晚 ~0.24s | MP3 解码 padding 未抵消 | build_audio.py 自动标定;勿手拼音轨 |
| 音轨后段全哑但时长正确 | ffmpeg concat 中途丢段 |
只用 build_audio.py(Python 拼 PCM) |
marks 全 0 |
edge-tts 7.2.8 无 WordBoundary | 正常,跑 align_clock.py |
_clock_report 出现机械等差 clock |
对齐失败退回均分保底 | 查 mp3 是否损坏、门限是否合理 |
| 逐句误差普遍 > 100ms | 复核与放置的 RMS 门限/窗不一致 | 同用 0.0015 门限、10ms 窗 |
| 画面文字重叠/叠字 | 相邻元素共用同一 alpha、占位未算 | 逐幕抽帧 Read 看图修版式,别靠读代码想象 |
| 术语被读散 / 专有名词断错 | TTS 把多字术语按单字节奏念 | 术语用「」括起;仍不准则术语前加破折号「——」(§4①) |
只有某段时刻报 xxx is not defined |
变量声明在某个 if 块内,块外引用即炸 |
跑 scripts/regress.js 报出首次出现时刻,声明提到模块级 |
| 抽帧看着都好、渲染却中断 | 抽帧只覆盖几个特定时刻,漏掉坏帧 | 渲染前必跑 regress.js 走完时间轴 |
| 全片画面静止 / 成片明显偏短 | 忘了 inject_clock.py,__TOTAL=0 |
regress 以退出码 2 报「未注入时间轴」 |
工程环境坑(引号还原、safe-delete、PowerShell 回显等)见 references/render-compose.md 末节。
8 交付物清单
<project>/
├── 教学动画设计方案.md # ①配音参数/合拍机制/逐幕脚本(含配合点栏)/实测总账
├── 台词.json # 唯一手写的数据文件
├── cues.json # 时间轴唯一真相(句首/句长/每字时钟/幕区间),脚本生成
├── audio/
│ ├── S*-L*.mp3 # 每句配音
│ ├── _bound/*.json # 断点续跑缓存
│ └── voice_track.wav # 按 cues 摆放的总音轨
├── xxx_sub.html # ③字幕版动画(无配音,内嵌 LINES/TP,讲解词逐句上屏)
├── xxx.mp4 # ④成片(画面+字幕+配音音轨)
├── _cues_report.txt # TTS 实测总账(时长/语速/幕区间)
├── _clock_report.txt # 每字时钟报告(单调性)
├── _audio_build.log # 音轨摆放逐句误差(验收物)
├── _inject_report.txt # 配合点绝对时刻 + 未命中检查(验收物)
├── frames/ # 逐帧 PNG 中间产物,确认成片后可删
├── _regress.log # ④渲染前全时间轴回归报告(坏帧/报错数/首次出错时刻)
└── _shots/ # 抽帧证据,QA 副产物,不进 HTML
9 自检清单
学科正确性(渲染前必查,见 references/subject-accuracy.md)
- [ ] 只看画面不听声音,能把这一页的知识点讲对
- [ ] 隔离框/选区只含研究对象本身,环境元素已虚化
- [ ] 每个力的作用点都在研究对象上(受力图类必查)
- [ ] 公式在原对象场景内推演,未脱成独立白板
- [ ] 台词中无孤立单字符符号与下标
- [ ] 每一处简化假设都有口头声明 + 画面标注
- [ ] 结论与画面标注逐字一致(不能台词说「一半」画面写「1/2」)
方案(①)
- [ ] 设计方案已获用户确认,含逐幕四栏表与配合点栏
- [ ] 每句 15~32 字,关键动作词已标出
- [ ] 专业术语已用「」括起;被定义的新术语视需要用破折号
TTS(②)
- [ ] 改词场景已清空 audio/ 缓存(mp3、_bound/、_pcm*/、voice_track.wav)
- [ ] _cues_report.txt 已读,实测语速已写回方案
- [ ] _clock_report.txt 全部单调,clock 非机械等差
- [ ] 幕末 gap 明显大于句间 gap
HTML(③)
- [ ] _inject_report.txt「未命中 = 0」(remap 模式除外,见 §5.3)
- [ ] 画面无手写毫秒常量,一切挂 tOf()
- [ ] 字幕逐句显示、随句淡入淡出、与画面元素无遮挡
- [ ] HTML 内无任何音频元素/音轨引用(本阶段无配音)
- [ ] scripts/syntax_check.js 通过
- [ ] regress.js --step=400 坏帧 0、报错 0(退出码 0)
- [ ] 逐幕抽帧 Read 看图,无文字重叠
- [ ] 页面说明条时长与 cues.total 一致
- [ ] 零素材自证 grep -cE "<img|url\(|https?://|<script src|<link|@import" 为 0
- [ ] 中文引号已用 scripts/fix_quotes.py 还原核验(改HTML 后必做,勿手写临时脚本)
成片(④)
- [ ] _audio_build.log 除首句外误差 ≤ 10ms
- [ ] 分批续渲帧号连续、总帧数 = round(TOTAL/1000×fps)、无旧帧混入
- [ ] ffmpeg -i 核验:h264 / 1280×720 / yuv420p / 30fps
- [ ] 音轨时长 ≈ cues.total + 1.5
- [ ] 成片反抽帧 + 实际听三处配合点,音画同步
修订(§5)
- [ ] 改参数已先做片段 A/B 对照并经用户听感拍板
- [ ] 修订版旧成片已备份 *_prev.mp4
10 参考文件
| 文件 | 内容 |
|---|---|
references/sync-mechanism.md |
以音定时的推导:为何不能先做动画、每字时钟算法与参数、LEAD 感知依据、音轨摆放精度 |
references/render-compose.md |
分批续渲与全新重渲、ffmpeg 合成与音轨并入、ffmpeg-static、工程环境坑 |
references/troubleshooting.md |
用户抱怨→根因→修法对照 |
references/subject-accuracy.md |
学科正确性检查表(受力图六条、符号不上屏、简化须声明、公式不脱场景;含化学/数学预留章) |
templates/script.template.json |
台词数据结构与写作要求(含术语断句规则) |
templates/sub_skeleton.html |
字幕版 HTML 骨架(§6 契约 + drawSubtitle 的完整实现) |