whiteboard-video-factory:白板讲解视频出片
工具就是本仓库(下文 <仓库>),CLI 是 <仓库>/bin/wb,参数全在 <仓库>/config.json。每期内容在 config.json 的 dirs.projects(默认 <仓库>/episodes/<日期 标题>/),中间产物与成片在 dirs.build(默认 <仓库>/build/<日期 标题>/)。
本文件只讲流程与规矩。细节按需翻 references/,全部在本仓库内,不依赖外部目录:
| 文件 | 讲什么 |
|---|---|
references/dsl.md | 场景与封面 API(一行代码一个元素) |
references/scene-patterns.md | 版式坐标套路(可直接抄) |
references/stickers.md | 贴纸、真实 Logo、真人漫画像 |
references/publish.md | 标题公式、各平台规格与机制、抖音怎么写 |
references/voices-volc.md | 火山 TTS:两个调用域、resourceId 与音色的配套关系、实测可用音色、报错速查 |
references/voices-mi.md | 小米 MiMo TTS:端点与 assistant 角色规矩、无语速参数、无时间戳怎么对齐、九个实测音色、三条通道怎么选 |
references/fact-check.md | 事实核查与来源台账 |
references/compliance.md | 发布前合规自查 |
references/delivery-qa.md | 成片机器与目视验收、封面验收、交付边界 |
硬规矩
- 全链路本地。 旁白走语音合成,
config.json的tts.engine三选一:volc(火山引擎,seed-tts-2.0,凭证在<仓库>/.env,音色可以是官方音色或你自己的声音复刻;返回逐字时间戳,字幕最准,默认用它)/mi(小米 MiMomimo-v2.5-tts,凭证MI_TTS_API_KEY,音色固定九个;接口没有语速参数、也不返回时间戳,语速靠本地atempo、时间轴靠静音检测估算,见references/voices-mi.md)/edge(本机 edge-tts,免费、无凭证,tts.edge.python指向装好edge_tts的解释器);语速默认 1.2 倍。贴纸走本地codexCLI 生图(codex 分派到无生图能力的模型时,改用内置 ImageGen 工具出四宫格再--rekey切图);真实 Logo 走 Wikimedia Commons 官方 SVG(wb logo);配乐放assets/bgm.mp3,没有就出无配乐成片。改配置不改代码。动了 TTS 配置先跑wb voice:它回显引擎/端点/模型/音色并真合成一句,比出片到一半才报错省事。三个引擎各有自己的音色字段(火山tts.voice/ 小米tts.mi.voice/ edgetts.edge.voice),别互相顶替。 - 一处为主。 期目录就是工程目录,
scenes.js是唯一手写源(旁白、场景、封面都在里面);scenes/、旁白稿.md、字幕.srt、封面-*.png是生成物,不手改、不外拷。README.md(资料来源)和发布.md(文案)是人写的。视频不放进期目录:成片在<dirs.build>/<期>/outputs/final.mp4,中间产物在同目录work/,旁白稿.md里自动带成片链接。 - 先旁白后画面。 旁白按
|切 beat,每 beat 34 句配一组元素。时长是变量,不是固定 3 分钟:动手前先跟用户确认目标时长,再按「1 分钟 ≈ 8 个 beat ≈ 200 字(1.2 倍语速)≈ 1.52 个场景」折算。短片 23 分钟(68 个场景)、中片 510 分钟(1525 个场景)、长片 1540 分钟(4090 个场景)都行——每个场景都保持「34 beat / 80120 字 / 一屏 10~20 个元素」的舒适区,靠加场景变长,别把一个场景写得又长又满。长片另外要分幕与呼吸点,见「做长片」。 - 画布 1920×1080。y≥960 是字幕区,右上 320×130 是水印区,元素不进去;
wb scenes的 ⚠ 必须清零。 - 公司、产品、模型用真实 Logo;人物、器械、物件用贴纸;文字、箭头、框用 Excalidraw。 讲到具体公司时主角用官方 Logo(
wb logo),不用生图拟人机器人代指,观众认不出是谁。讲到具体公众人物时用真人照片参考的漫画像(wb image ... --ref=照片 --likeness,见references/stickers.md),不用通用小人代指。Excalidraw 画人很丑,人和物件一律出贴纸。 - 事实先查再写。 数字、日期、价格要有来源,写进期目录
README.md;估算值在旁白和文案里都标"据报道/估算"。查证动作见references/fact-check.md。 - 品牌层自动带,不用每期写。 右上角手写水印(第一场景逐笔画入)、片尾品牌卡(5.5 秒,静音)、封面上的品牌标都由工具生成,名字、品牌色、slogan 在
config.json的brand;标题和点睛色优先用C.brand。 - 交付四件套:成片 + 三张封面(4:3 横版 / 3:4 竖版 / 9:16 抖音竖屏)+
发布.md,主用标题、视频号简介和抖音描述直接贴在回复里。
命令
bashW=<仓库>/bin/wb $W new "<标题>" # 建期目录:scenes.js 模板 + 发布.md 模板 $W logo "<标题>" logo-x="Commons 文件名.svg" ... [--vs=x,y] # 真实 Logo → assets/<name>.png;--vs 拼 "A VS B" 封面图;--search="词" 先看候选 $W image "<标题>" name="英文描述" ... # 贴纸,一次 ≤4 张(四宫格同风格)→ assets/<name>.png $W stills "<标题>" # 每 beat 静帧 → <后台>/<期>/work/frames/,逐张看 $W cover "<标题>" # 封面-4x3.png / 封面-3x4.png / 封面-9x16.png → 期目录(画幅见 config cover.ratios) $W captions "<标题>" # 核对字幕:每条的时间、文本,以及高亮分段([a·b·c] 里 · 的位置就是色块跳的节拍) $W voice # 配音自检:回显 engine / 端点 / 模型 / 音色,并真合成一句 → build/_voice-check.wav $W build "<标题>" # scenes → tts → render → mix → cover → clean,出 outputs/final.mp4 / 字幕.srt / 旁白稿.md
分步:scenes / tts / voice / captions / render [scene|99-brand] / mix [gain] / clean / open / list。wb build 出片后自动 clean:删 work/frames 静帧和 work/out 里已不在场景表的旧分段,各场景分段与 master 保留(单场景重渲、重混配乐要用)。期参数用标题子串(wb build 薄肌)。wb render <期> 03-xxx 只重渲一段并自动重拼 master,接 wb mix 即新成片;TTS 有缓存,只有改过的旁白会重配。
一期的流程
1. 立题与查证
- 明确选题、观众、口吻(默认冷静科普口吻,账号口吻写进
references/publish.md)。 - 搜 2~3 轮;官方页优先且读到全文,二手站数字只做线索。
- 数字、日期、来源列进
README.md「资料来源」,口径("240 倍 = 6000 万 / 25 万")单列一节。完整动作见references/fact-check.md。
2. 写旁白(scenes.js)
- 口语短句,每句一个信息点。数字用中文读法利于 TTS("三百美元");字幕用原文,所以阿拉伯数字也行,但
@、iOS这类 TTS 会念歪的词要斟酌。 - 结构:开场定义/反差 → 分解(三要素/两列对比/时间线)→ 怎么算/怎么做 → 数字与门槛 → 冷水/边界 → 一句话总结 + 评论区问题。
- 每个 beat 都要有能画出来的东西,抽象句并入相邻 beat。按原速写即可,成片语速 1.2 倍。
3. 出 Logo 与贴纸
- 先列本期出现的公司/产品/模型 →
wb logo。wb logo --search="<公司> logo"看 Commons 候选,挑官方现行版(带年份的取最新),一期一条命令取齐:标志(logo-<名>,方形,放主视觉)+ 字标(logo-<名>-word,横长,当标签/表头);两家对比再加--vs=a,b出logos-vs.png当封面主图。来源自动记在assets/logos.json,抄进 README「画面素材」。Commons 没有的,去官网 press kit / brand 页找 SVG,确认授权再下载。 - 再列本期物件(人物、器械、设备、道具),
wb image一次 ≤4 张,英文描述写姿势/服装/颜色(Excalidraw 五色:pale yellow/blue/green/red/grey)。 - 逐张看
assets/<name>.png:杂点、邻格残片、主体断块 →--single单张重出;只是抠图问题 →--rekey。封面主角贴纸也在这一步出(公司题材封面用 Logo,不另出)。
4. 画场景
- 抄
references/scene-patterns.md的版式坐标再微调;贴纸s.image(x, y, 'name', { h: 370, align: 'center' }),人形 360380 高;Logo 同样用300,卡片/柱子里 h 100~130,字标当标签用s.image:主视觉标志 h 260w。 - 一屏 10~20 个元素;一个 beat 塞不下就删元素,别指望笔画得完(排期最多溢出到下一段前 35%,再多就整体压缩)。
wb stills后逐张看静帧:重叠、越界、文字超宽、太挤。改到满意。
5. 封面
scenes.js末尾cover函数:s.coverLayout({ ratio, title, sub, sticker }),build(__dirname, scenes, { cover });wb cover按 configcover.ratios出 4:3 / 3:4 / 9:16(9:16 给抖音,标题与贴纸压在中央安全区,因为抖音封面上下会被裁到约 1080×1464)。- 标题 ≤2 行、每行 ≤8 字:第一行说对象,第二行说钩子(自动品牌色 + 马克笔高亮);副标放数字;贴纸用主角(公司题材传
sticker: 'logos-vs'或单个logo-x,宽图在竖版会自动按宽度缩)。 - 看三张 png:文字没撞贴纸、高亮压在钩子行、品牌标在角上。细则见
references/delivery-qa.md的「封面验收」。
6. 出片与验收
wb build。抽 2~3 帧看(ffmpeg -ss <t> -i final.mp4 -frames:v 1 x.png):字幕在底、跟读高亮压在正念到的那几个字上(色块紧贴字、不吞标点、不切断MiMo这类英文词)、水印在右上、贴纸擦出正常;片尾看一眼99-brand。抽帧时先看原图再下结论:把几十帧缩成一张长图看,容易把墨色字误判成"没高亮"。wb captions <期>:不打图就能核对每条字幕的时间、文本,以及高亮分段([a·b·c]里·的位置就是色块跳的节拍;每段大致 0.2~0.6 秒算正常)。- 看
字幕.srt前几条:原文拼写、数字未拆。SRT 里没有高亮(纯文本格式装不下),高亮只烧进画面。 - 机器检查与交付边界按
references/delivery-qa.md。 README.md写好,时长以ffprobe为准。
7. 发布文案与标题
- 按
references/publish.md:5 个标题候选(数字反差 / 事件主语 / 结论前置 / 生活单位换算 / 提问)选 1 主用;视频号简介、抖音描述(前 55 字钩子 + 话题 3~5 个排最后 + AIGC 标注)、小红书标题+正文+标签、B 站标题、公众号摘要、评论区置顶。 - 数字与
README.md一致;合规自查勾完(references/compliance.md)。 - 填
发布.md,交付四件套。
做长片(>5 分钟)
代码层面没有时长上限。 渲染是按场景分段出 mp4、再用 ffmpeg concat 拼成 master(lib/render.js),场景数不受限;TTS 也是逐场景合成与缓存(lib/tts-volc.mjs)。所以「能拍多长」只取决于内容规划,不取决于工具。实测(2026-09-30,plan 域流式端点):单次提交 2200 字 / 6600 字节(≈6.5 分钟音频)也正常返回,所以即使某一场写得很长也不会卡在接口上。
| 目标时长 | 场景数 | 总字数 | 分幕 |
|---|---|---|---|
| 2~3 分钟 | 6~8 | 500~700 | 不用分幕 |
| 5~10 分钟 | 15~25 | 1200~2400 | 2~3 幕 |
| 15~25 分钟 | 40~60 | 3500~6000 | 3~5 幕 |
| 30~40 分钟 | 75~90 | 7000~9000 | 5~8 幕 |
长片比短片多做的四件事:
- 分幕。 每幕开头加一张「章节卡」场景(普通
new Scene('02-act2', ...)就行:画幕标题 + 一句导览),观众才知道自己在哪;发布文案的时间戳也按幕切。片尾卡由工具自动追加,章节卡要手写。 - 每 2~3 分钟一个呼吸点。 白板讲解连续 20 分钟会累:呼吸点可以是一句小结、一个踩坑现场、一张对比表。别一路平铺。
- 配乐改用播放列表。 一首 2~3 分钟的曲子配 20 分钟的片子要循环十几次,一听就出戏——
config.json的bgm.playlist填几首([{ "file": "assets/bgm-1.mp3" }, { "file": "assets/bgm-2.mp3", "trim": [0, 90] }]),它们会按顺序拼成一整段再整体循环,重复感立刻降下来;playlist 为空时仍走bgm.file单曲循环。 - 封面标签别提时长。
cover.seriesTag默认「白板讲清楚」;要强调时长就在scenes.js的 cover 里按本期实际时长传tag,别在 config 里写死「3 分钟」。
耗时与体积(本机 4 路并行实测 + 外推;1080p30):
| 时长 | 出帧 | wb build 全程 | 成片 + 中间产物 |
|---|---|---|---|
| 2.7 分钟(《DeepSeek Harness 桌面端》实测 8 场景) | ~1.8 分钟 | ~3 分钟 | 12 MB + 24 MB |
| 10 分钟 | ~7 分钟 | ~10 分钟 | ~46 MB + ~90 MB |
| 30 分钟 | ~21 分钟 | ~30 分钟 | ~140 MB + ~270 MB |
出帧速度按本机实测 ≈44 帧/秒(4 路并行)折算;体积按实测 ≈4.5 MB/分钟。白板片画面白、运动少,比实拍素材小得多。机器吃紧就把
render.workers降到 2。
长片一定要用断点续跑:TTS 与渲染都按场景缓存,改一句旁白或一个场景不必整片重跑——
bashwb tts <期> <场景名> # 只重配改过的那一幕(缓存命中时几乎瞬时) wb render <期> <场景名> # 只重渲这一段,并自动重拼 master wb mix <期> # 出片
⚠️ wb build 会整期重渲(即使 TTS 全命中缓存),长片别拿它做小改动。另外 wb captions <期> 在长片上尤其有用:不打图就能逐条核对 90 条字幕的时间与高亮分段。
修改类请求怎么接
| 用户说 | 做法 |
|---|---|
| 改某句旁白 / 加一段 | 改 scenes.js → wb build(只重配改过的场景) |
| 用了假机器人 / 要真 Logo | wb logo 取官方 SVG → scenes.js 把 s.image 名字换成 logo-* → wb stills → wb render && wb mix && wb cover |
| 换贴纸 / 人物太丑 | wb image 重出 → wb stills → wb render && wb mix(封面用到的话再 wb cover) |
| 画得太快/太慢、文字蹦出来 | config render.pen:speed 描边 px/s、charSeconds 每字秒数区间、minSeconds 单元素下限、gapSeconds 抬笔间隙 → wb render && wb mix |
| 画面跟不上旁白(声音在讲、画还没画完,越到后面越明显) | 先量:node lib/diag-schedule.mjs <期目录绝对路径> 逐拍打印「起笔偏移」,> 0.35s 就是落后,且会累积到后面每一拍。修法(只动 config):render.pen.overflow 设 0(禁掉"一拍画不完就溢到下一拍")、speed 提到 1400~1600、charSeconds 上限压到 ~0.14、fillMaxSeconds ~0.8、gapSeconds wb render && wb mix。元素 >20 个的拍会被压缩得明显变快,想更从容就删几个元素(比让整片晚 2 |
| 字幕与语音对不齐(小米通道) | node lib/diag-captions.mjs <期目录绝对路径> 看「锚定率」与起点偏移。锚定率不是 100% 就是有字幕时间在靠字数插值,来源几乎总是长小句被 captions.maxChars 硬拆:把 maxChars 放大到装得下最长小句(本期 25 字 → 26),或给那句旁白补个逗号让它自然分成两个小句(补标点不改语义,但要重跑 wb tts)。细则见 references/voices-mi.md |
| 渲染太慢 / 机器吃紧 | config render.workers(默认 4 路,每路一个 Chromium;1 = 串行),出帧与路数无关、逐帧一致 |
| 语速快/慢 | config tts.speed(默认 1.2)→ wb build,全部场景自动重配、字幕同步。⚠️火山是原生变速,小米是本地 atempo 后处理(接口无语速参数) |
| 换配音引擎(火山 / 小米 / edge) | config tts.engine 改 "volc" / "mi" / "edge" → 必须重跑 wb tts:三家音频、语速、停顿都不一样,字幕是跟着音频走的,缓存也按引擎分开。换完跑 wb voice 试听 |
| 要更长 / 更短的片子 | 不改代码、不改配置,只改 scenes.js 的场景数(按「1 分钟 ≈ 1.5wb build。⚠️这是重写内容的活,不是调参:加长就补场景(每场景仍是 3bgm.playlist |
| 配乐大/小 | config bgm.gain → wb mix;临时试听 wb mix <期> 0.4 |
| 字幕字号/位置 | config captions.fontSize / baselineY → wb render && wb mix |
| 字幕跟读高亮(开关 / 颜色 / 粒度 / 半透明) | config captions.highlight → wb render && wb mix。enabled 关掉就是原来的静态字幕;color 留空用品牌色;opacity + keepHalo:true 出半透明马克笔效果;minUnitChars/maxUnitChars 调粒度(默认 2/4,目标 3 个字)。改完先 wb captions <期> 看分段:[用的是·小米 ·MiMo ·的配音] 里的 · 就是色块依次跳的位置|字幕文本与 字幕.srt 都不受影响 |
| 水印位置/关掉/换 logo | config brand.watermark.position/enabled、brand.logo(透明底 png)→ wb render && wb mix |
| 片尾卡 slogan / CTA / 时长 / 不要 | config brand.slogan、brand.endCard.* → wb render <期> 99-brand && wb mix |
| 封面文案 / 贴纸 / 画幅 | scenes.js 末尾 cover 函数 → wb cover;画幅 config cover.ratios(含抖音用的 9:16),标签 cover.seriesTag(tag: '' 去掉) |
| 标题 / 发布文案 / 换平台 | 改 发布.md,不用重出片 |
| 在 Obsidian 里改了图 | wb render && wb mix 直接读 .excalidraw.md;增删元素会改逐笔顺序,结构性改动回 scenes.js |
| 换声线 / 换曲 | 先 wb voice 确认档位,再改各自引擎的音色字段:火山 tts.voice(2.0 音色配 resourceId: seed-tts-2.0,声音复刻配 volc.megatts.default)/ 小米 tts.mi.voice(只有九个,见 references/voices-mi.md)/ edge tts.edge.voice;或 .env 的 VOLC_TTS_VOICE、MI_TTS_VOICE / 配乐改 bgm.playlist(多首按序拼接,长片用)或 assets/bgm.mp3(单曲循环,换曲先 volumedetect 量电平再定 gain);临时换曲 BGM=路径、临时换列表 BGM_PLAYLIST='[{...}]' |
| 配乐固定几首轮播 | config bgm.playlist 填 [{ "file": "...", "trim": [起, 止] }];trim 省略即整首,gain 可逐首配平响度 → wb mix。列表非空时 bgm.file / bgm.trim 被忽略 |
TTS 报 Invalid X-Api-Key / not granted | 先怀疑调用域不是额度:ark- 开头 Key 只走 plan,改 config tts.endpoint,见 references/voices-volc.md 的报错速查 |
小米报 Unknown voice / assistant role / 404 | 三类坑:音色不在九个之内、正文没放在 assistant 角色、路径不是 {baseUrl}/chat/completions,见 references/voices-mi.md 的报错速查 |
| 换账号品牌 | config brand.name/accent/slogan,其余不动 |
跨平台说明(Windows / macOS / Linux)
平台差异全部收在工具层,写新脚本时照抄下面几条:
- 路径展开:
lib/paths.cjs、lib/scene-dsl.js、lib/render.js里的~统一走require('os').homedir()。别用process.env.HOME——Git Bash 下它是 MSYS 写法/c/Users/x,Windows 版 Node 会解析成C:\c\Users\x而 ENOENT。 file://URL:Chromium 载入render.html和本地图片一律用require('url').pathToFileURL(p).href,不要手拼file://+path.join(Windows 反斜杠会被当转义)。- ffmpeg concat 清单:写进
list.txt的路径要p.replace(/\\/g, '/')。 - Chromium:
lib/chromium.cjs的resolveChromium()在 ms-playwright 里自动取本地最高版本,绕开"Playwright 要的版本本机没有、npx playwright install也装不上"的死路。 npm install可能以 exit 1 收尾,但依赖其实装好了:postinstall跑playwright install chromium,网络下不来 Chromium 时报Download failure, code=1;此时node_modules/里 playwright / roughjs / lz-string 都已就位,成片照跑。别为了这个 exit 1 反复重装;嫌吵就用npm install --ignore-scripts,浏览器反正走resolveChromium()。- codex 在 Windows 上只有
codex.cmd,spawn('codex')会 ENOENT;lib/gen-image.mjs的resolveCodex()会去找node_modules/@openai/codex/bin/codex.js并用process.execPath跑。 - Node 联网:Commons API 在部分网络下必须经代理,
lib/proxy.mjs的installProxyFetch()给 Node 的 fetch 装一层 HTTPS CONNECT 隧道(读HTTPS_PROXY/HTTP_PROXY环境变量,写进.env也行;没有配就是空操作)。lib/fetch-logo.mjs、lib/gen-image.mjs入口调用它。 - 本地 SVG 转 PNG:非 Commons 来源的 Logo(如官网 SVG)用
lib/svg2png.mjs过 Playwright 转透明 PNG。 bin/wb:ROOT传给 Node 前先cygpath -m转成C:/...(Git Bash 的/c/...Node 读不了;没有cygpath就原样传);open在 Windows 用explorer.exe,Linux 用xdg-open。- 期目录移出仓库时:
config.json的dirs.projects/dirs.build支持绝对路径(如D:/临时/2026/9月/2026年9月29日),临时改动完记得改回./episodes/./build,别把本机绝对路径提交进仓库。另外wb new生成的scenes.js会把__WB_ROOT__替换成仓库绝对路径,但手拷来的scenes.js第 4 行的require('../../lib/scene-dsl')是相对期目录写的,换位置后会 ENOENT——要么重写成仓库绝对路径(正斜杠),要么用wb new重建。 - Windows 上用 Git Bash 跑命令;路径含中文或空格记得加引号。
已知坑
- Logo:很多官网有验证页拦截,Logo 一律走 Commons API(
wb logo),别去官网抓图。OpenAI 2025 字标最后的 "I" 就是一根竖条,不是被裁掉;Claude 星芒 SVG 边缘略锯齿,放大到 300 高以内看不出来。 - 火山 TTS 偶尔只返回半段:
tts-volc.mjs按逐字数校验自动重试;全量重配FORCE_TTS=1。缓存键含文本+音色+语速+音量+风格+调用域,改任一项都会自动重配。 - 火山 TTS 的两个调用域互不相通(2026-09-29 实测):控制台发的
ark-开头 API Key 只能走plan域(/api/v3/plan/tts/unidirectional),直购/旧版凭证走standard域(/api/v3/tts/unidirectional)。打错域报45000010 Invalid X-Api-Key,看着像 Key 错或没额度,其实只是域不对——先查tts.endpoint再怀疑凭证。ark.cn-beijing.volces.com/api/plan/v3是方舟另一套(非 TTS)入口,别当 baseURL 拼。音色与resourceId也必须配套(2.0 音色↔seed-tts-2.0),配错报55000000 mismatched with speaker。lib/tts/tts.mjs的explainFailure()会把这些错翻成可执行的下一步;wb voice可提前自检。 - 画面落后旁白是这套排期的头号同步问题(2026-09-29 实测,小米通道):
schedule()(lib/render.html)给每拍的绘制上限是limit = 本拍结束 + overflow × 下一拍时长,而下一拍的起笔又取max(本拍起点, 上一笔结束)——所以某一拍画不完就会把后面每一拍一起推后,误差是累积的。实测默认参数(speed:1000 / overflow:0.35)下最差一拍落后 3.62 秒(旁白已经在讲第三拍,画面还在画第二拍)。修法是只动config.json的render.pen:overflow: 0+speed: 1450+charSeconds:[0.05,0.14]+fillMaxSeconds: 0.8+gapSeconds: 0.09,实测把最大起笔偏移压到 +0.07s。改完必须量,不能凭感觉:node lib/diag-schedule.mjs <期目录绝对路径>逐拍打印「起笔偏移/收笔偏移/压缩倍率」,目标每拍起笔偏移 ≤0.35s(diag-schedule.mjs支持PEN_OVERRIDE='{...}'环境变量传入旧参数做 A/B,不用改 config)。 - 小米 MiMo TTS(
tts.engine: "mi")与火山处处不同(2026-09-29 实测):① 走POST {baseUrl}/chat/completions,正文必须放在assistant角色消息里(只给 user 报messages must contain an assistant role);/audio/speech、/tts全是 404。② 接口没有语速参数——顶层speed/audio.speed/audio.speech_rate全被忽略(audio.speed=0.5与不传返回字节完全相同),tts.speed由本地atempo后处理实现。③ 不返回逐字时间戳(audio.transcript恒 null,AI 语音识别模型也只回文本),句子时间靠「标点小句 +silencedetect停顿 + 单调 DP 吸附」估算,实测边界误差约 0.05s、各句语速一致性 CV 0.144;work/audio/<scene>.json的align字段会标silence-estimate(火山是native)。小句边界不含顿号(ALIGN_VERSION = 3):顿号处停顿弱且不稳,实测 28 处里 10 处根本没停顿、DP 只能按字数插值(把小句起点推早 1.16s);改这个集合要同时改lib/tts/mi.mjs的clausesOf与lib/captions.cjs的PUNCT/splitText(两边必须逐字一致,否则配对份数不等会整片退化成等比)并ALIGN_VERSION +1。④ 音色固定九个(mimo_default/冰糖/茉莉/苏打/白桦/Mia/Chloe/Milo/Dean),写错报Unknown voice并在报错里列出全部可选值。⑤ 静音阈值-35dB/0.10s是扫出来的最优,别凭直觉调灵敏(越灵敏语速一致性越差,已复现)。细节见references/voices-mi.md。 - 改旁白后先
wb scenes再wb tts:scenes.js是手写源,TTS 读的是scenes/script.json(由wb scenes生成)。只改scenes.js就直接跑wb tts,读到的还是旧 script.json,会误以为"改了没生效"(2026-09-29 踩过)。wb build一条龙里有scenes那一步,所以走 build 不受影响。 - 验收字幕/画面同步用两个诊断脚本(都在
lib/,不改产物、可反复跑):node lib/diag-captions.mjs <期目录绝对路径>:字幕条数、有声覆盖率、起点偏移中位数、锚定率(起点是否正好落在语音小句边界上,100% 为佳)。node lib/diag-schedule.mjs <期目录绝对路径>:每拍的旁白窗口 vs 实际绘制窗口、起笔/收笔偏移、压缩倍率。 两个脚本都直接吃期目录的绝对路径(不走dirs.projects),但后台目录仍按config.json的dirs.build解析——跨目录出片时别忘了dirs.build要和成片实际所在的后台目录一致,否则量到的是上一次构建的音频(我第一轮诊断就栽在这,白改一轮参数)。
- 字幕的逐字时间戳靠
enable_subtitle: true(火山 2.0/ICL 2.0 音色生效;1.0 音色靠enable_timestamp)。plan 域复测:关掉enable_subtitle直接words=0,字幕会退化成按字数估时间——别为了"省点数据"关它。(小米通道无此开关,它本来就没有时间戳。) - 字幕文本必须用原文,TTS 词会把
@grok写成atgrok;字幕时间来自 TTS 逐字时间戳,改旁白必须重跑 tts。 - 字幕跟读高亮(
config captions.highlight,默认开)改render.html时注意三件事(2026-09-29 实测): ① 别用Range.getBoundingClientRect()量 SVG 文本的局部字符区间——Chromium 只量得出第一个字("MiMo" 量成 "M"),色块比文字窄一半,高亮段剩下的字被涂成纸色、看起来像丢了字。用tspan.getExtentOfChar(i)(返回用户坐标,本页 1:1 就是画布坐标),下标用码点数[...str],别用 UTF-16 下标。 ② 火山的wordList会吞掉空格(用的是小米 MiMo 的配音返回成...小米MiMo的配音),拿w.word拼回去 ≠ 原文;若按逐字节校验就会判"对不上"、整条退化成等比估算,英文字母还会被字数上限切成[米 MiM]·[o 的配音]。captions.cjs的wordsToUnits()按非空白字符对齐、文本取原文切片,就是为了这个。 ③ 粒度只按字数调,别按"时长太短就并"——按时长会把原生词边界冲掉([小米][MiMo]并成[米 Mi][Mo 的])。火山的词表是逐字切的("字""幕""也""能"…),靠"目标 3 个字"的贪心分组才得到[用的是]·[小米]·[MiMo]·[的配音]。 - SVG dash 在每个子路径
M处重起,rough.js 又双描边:整条 path 一起 dashoffset 会"所有边同时长、每边描两遍"。渲染器已按子路径拆节点、双描边拆 A/B 层,别回退。 - rough.js
toPaths不带 dasharray,虚线在render.html手动设。 - 文本宽度是估算值,居中用
align:'center'才准;左对齐长文本别超 1920。 - Playwright 截图偶发 30s 超时,渲染器带 3 次重试;再挂重跑
wb render。 - 不要并发跑
wb image:codex 没按指定路径落盘时会兜底抓"最近生成的图",并发会互相抓错。 - 并行出帧靠两点,别动:
render.html的seek()每帧把底色矩形原地重插,强制整屏重画(否则 Chromium 只重画变化区域,帧会跟出帧顺序有关);每路单开一个 Chromium。改渲染器后用render.workers=1和4各出一遍,ffmpeg -f framemd5对比必须 0 帧不同。 - 用
require()跑templates/scenes.js会在 templates/ 下生成产物,别这么测;冒烟测试用wb new。 - 渲染耗时看机器:参考值 2.5 分钟片 4 路并发出帧约 35 秒;本机实测 2 分钟片全量出帧约 82 秒、
wb build全程约 2.5 分钟(即使 TTS 全命中缓存也会整期重渲再 clean)。改一个场景想省时间用wb render <期> <场景>+wb mix。后台长任务不要用&,用运行环境自带的后台运行。 - 3 分钟不是上限,是旧规范(2026-09-30 核实并改掉):
lib/render.js按场景分段出 mp4 再用ffmpeg concat拼 master、lib/tts-volc.mjs逐场景合成+缓存,两者都不受场景数约束;「68 个场景 / 22.5 分钟」原本只是内容建议。实测火山 plan 域流式端点单次吃下 2200 字 / 6600 字节(≈6.5 分钟音频)无报错——所以长片不用改代码,加场景即可。真正的代价是出帧时间(≈44 帧/秒)与磁盘(≈4.5 MB/分钟),以及wb build整期重渲 —— 长片一律用wb tts/render <期> <场景>断点续跑。⚠️别在封面标签里写死时长:默认cover.seriesTag原来就是「白板 3 分钟讲清楚」,做长片会自相矛盾,已改成「白板讲清楚」;另外bgm单曲循环配 20 分钟片子会一听就出戏,长片把bgm.playlist填上(多首按序拼接后整体循环)。 wb build的清理步可能被安全钩子拦:成片已落盘、字幕封面都出完之后,如果清理work/frames时一次删超过数十个文件,某些 Agent 沙箱会抛SAFE_DELETE_BULK_CONFIRM_REQUIRED。不影响交付,手动清一次或用wb clean即可。- edge-tts 引擎(
tts.engine: "edge"):lib/tts-edge.py读work/audio/落<scene>.mp3+<scene>.json,json 里用 WordBoundary 事件拼出wordList(和火山的逐字时间戳同格式),字幕链路不用改。缓存键含文本+音色+语速,对不上会自动重配(强制重配FORCE_TTS=1)。解释器由config.json的tts.edge.python指定(指向装好edge_tts的那个),留空则用 PATH 里的python3/python。edge 读不出中文多音字,数字/英文模型名建议在旁白里写成中文读法。


