SuperAPI AI Directory

whiteboard-video-factory

Marketing & growth

View repository

Updated 10-17 15:13

About this skill

whiteboard-video-factory: whiteboard explainer video production

The tool is this repository (below <仓库>), the CLI is <仓库>/bin/wb, and all parameters are in <仓库>/config.json. Each episode's content is in config.json under dirs.projects(default <仓库>/episodes/<日期 标题>/), and intermediate artifacts and the final video are in dirs.build(default <仓库>/build/<日期 标题>/)。

This file covers only the process and rules. For details, consult references/as needed; everything is inside this repository and does not depend on external directories:

File What it covers
references/dsl.md Scene and cover API (one element per line of code)
references/scene-patterns.md Layout coordinate patterns (ready to copy)
references/stickers.md Stickers, real logos, real-person comic portraits
references/publish.md Title formulas, platform specs and mechanics, how to write for Douyin
references/voices-volc.md Volcano TTS: the two call domains, the pairing between resourceId and voice, voices verified to work, quick error reference
references/voices-mi.md Xiaomi MiMo TTS: endpoint and assistant role rules, no speed parameter, how to align without timestamps, nine verified voices, how to choose among the three channels
references/fact-check.md Fact-checking and source ledger
references/compliance.md Pre-publish compliance self-check
references/delivery-qa.md Final-video machine and visual acceptance, cover acceptance, delivery boundaries

Hard rules

  1. Fully local pipeline. Voiceover uses speech synthesis; choose one of the three forconfig.json in tts.engine :volc(Volcano Engine,seed-tts-2.0, credentials in <仓库>/.env; the voice can be an official voice or your own voice clone;returns word-level timestamps, making subtitles the most accurate; use it by default)/ mi(Xiaomi MiMo mimo-v2.5-tts, credentials MI_TTS_API_KEY, fixed set of nine voices;the API has no speed parameter and returns no timestamps, so speed relies on local atempoand the timeline is estimated via silence detection, see references/voices-mi.md)/ edge(local edge-tts, free, no credentials, withtts.edge.python pointing to the interpreter where edge_tts is installed); default speed is 1.2x. Stickers use the local codex CLI for image generation (when codex dispatches to a model without image-generation capability, switch to the built-in ImageGen tool to produce a four-panel grid and then use --rekey to split it); real logos use official SVGs from Wikimedia Commons (wb logo); background music goes in assets/bgm.mp3, and if absent, output a video without music. Change configuration, not code.After changing the TTS configuration, first run wb voice: it echoes the engine/endpoint/model/voice and actually synthesizes one sentence, which is easier than hitting an error halfway through rendering. The three engines each have their own voice field (Volcano tts.voice / Xiaomi tts.mi.voice / edge tts.edge.voice); do not substitute one for another.
  2. One source of truth. The episode directory is the project directory;scenes.js is the only hand-written source (narration, scenes, and covers are all in it);scenes/、旁白稿.md、字幕.srt、封面-*.png are generated artifacts: do not edit by hand or copy out.README.md(sources) and 发布.md(copy) are written by humans. Videos are not placed in the episode directory: the final video is in <dirs.build>/<期>/outputs/final.mp4, and intermediate artifacts in the same directory's work/,旁白稿.md automatically include a link to the final video.
  3. Narration first, visuals second. Narrator follows | to cut beats, 3 per beat4 sentences paired with one set of elements.Duration is a variable, not a fixed 3 minutes: before starting, confirm the target duration with the user, then convert by "1 minute ≈ 8 beats ≈ 200 characters (1.2x speech rate) ≈ 1.52 scenes". Short film 23 minutes (68 scenes), medium film 510 minutes (1525 scenes), long film 1540 minutes (4090 scenes) are all fine—keep each scene in the comfort zone of "34 beat / 80120 characters / 10~20 elements per screen",lengthen by adding scenes, don't write one scene long and full. Long films additionally need act divisions and breathing points, see "Making Long Films".
  4. Canvas 1920×1080. y≥960 is the subtitle area, the top-right 320×130 is the watermark area, elements must not enter;wb scenes 's ⚠ must be cleared to zero.
  5. Use real logos for companies, products, and models; use stickers for people, equipment, and objects; use Excalidraw for text, arrows, and boxes. When talking about a specific company, use the official logo for the protagonist (wb logo), don't use a generated anthropomorphic robot as a stand-in, viewers can't tell who it is. When talking about a specific public figure, use a cartoon portrait based on a real photo (wb image ... --ref=照片 --likeness, see references/stickers.md), don't use a generic figure as a stand-in. Excalidraw draws ugly people, always use stickers for people and objects.
  6. Verify facts before writing. Numbers, dates, and prices must have sources, write them into the episode directory README.md; estimated values are marked "reportedly/estimated" in both narration and copy. For verification actions see references/fact-check.md。
  7. Brand layer comes automatically, no need to write it each episode. The handwritten watermark in the top-right (drawn stroke by stroke in the first scene), the end-card brand card (5.5 seconds, silent), and the brand mark on the cover are all generated by the tool, with name, brand color, and slogan in config.json 's brand; titles and accent colors prefer C.brand。
  8. Delivery four-piece set: final video + three covers (4:3 landscape / 3:4 portrait / 9:16 Douyin vertical) + 发布.md, main title, WeChat Channels intro, and Douyin description pasted directly into the reply.

Command

W=<仓库>/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

Steps:scenes / tts / voice / captions / render [scene|99-brand] / mix [gain] / clean / open / list。wb build After the video is produced, automatically clean: delete work/frames still frames and work/out old segments no longer in the scene table, keep each scene segment and master (needed for single-scene re-render, re-mixing music). Episode parameter uses title substring (wb build 薄肌)。wb render <期> 03-xxx only re-renders one segment and automatically reassembles the master, then wb mix gives the new final video; TTS is cached, only modified narration is re-dubbed.

One episode's workflow

1. Topic selection and verification

  • Clarify topic, audience, tone (default calm science-explainer tone, account tone written into references/publish.md)。
  • Search 2~3 rounds;official pages take priority and read the full text, second-hand site numbers are only clues.
  • Numbers, dates, sources listed in README.md"Sources", with the framing ("240x = 60 million / 250 thousand") in a separate section. Full actions see references/fact-check.md。

2. Write narration (scenes.js)

  • Colloquial short sentences, one information point per sentence. Use Chinese reading for numbers to help TTS ("three hundred dollars"); subtitles use the original text, so Arabic numerals are also fine, but @、iOS words that TTS would mispronounce need consideration.
  • Structure: opening definition/contrast → breakdown (three elements/two-column comparison/timeline) → how to calculate/how to do it → numbers and thresholds → caveats/boundaries → one-sentence summary + comment-section question.
  • Every beat needs something that can be drawn; merge abstract sentences into the adjacent beat. Write at normal speed; the final video plays at 1.2x speed.

3. Produce logos and stickers

  • First list the companies/products/models appearing in this episode → wb logo。 wb logo --search="<公司> logo" check Commons candidates, pick the official current version (for year-tagged ones, take the latest), and fetch them all in one command per episode: logo (logo-<名>, square, for the main visual) + wordmark (logo-<名>-word, horizontal/wide, used as labels/headers); for a two-company comparison also add --vs=a,b to output logos-vs.png as the cover main image. Sources are automatically recorded in assets/logos.json; copy them into the README "Visual assets". If Commons doesn't have it, go to the official website's press kit / brand page to find the SVG, confirm the license, then download.
  • Then list the objects in this episode (people, instruments, equipment, props),wb image at most 4 at a time, and write the English description with pose/clothing/color (Excalidraw five colors: pale yellow/blue/green/red/grey).
  • Check each one assets/<name>.png: stray dots, fragments from neighboring cells, broken blocks in the subject → --single regenerate that single image; if it's just a cutout issue → --rekey. The cover protagonist sticker is also produced at this step (for company-themed covers use the logo; don't produce a separate one).

4. Draw the scenes

  • Copy the layout coordinates from references/scene-patterns.md and fine-tune; sticker s.image(x, y, 'name', { h: 370, align: 'center' }), human figure 360380 high; for logos also use s.image: main visual logo h 260300, inside cards/bars h 100~130, wordmark used as a label w。
  • 10~20 elements per screen; if a beat can't fit them, delete elements—don't count on drawing them all (scheduling can overflow into at most the first 35% of the next segment; beyond that, compress everything).
  • wb stills aftercheck each still frame: overlaps, out-of-bounds, text too wide, too crowded. Revise until satisfied.

5. Cover

  • scenes.js at the end cover function:s.coverLayout({ ratio, title, sub, sticker }),build(__dirname, scenes, { cover });wb cover according to config cover.ratios output 4:3 / 3:4 / 9:16 (9:16 for Douyin; the title and stickers are pressed into the central safe area because the Douyin cover gets cropped top and bottom to about 1080×1464).
  • Title ≤2 lines, ≤8 characters per line: the first line states the subject, the second line states the hook (automatic brand color + marker highlight); the subtitle holds the numbers; the sticker uses the protagonist (for company themes pass sticker: 'logos-vs' or a single logo-x; wide images automatically scale by width in vertical format).
  • Check the three pngs: text doesn't collide with stickers, the highlight sits on the hook line, the brand mark is in the corner. For details see references/delivery-qa.md 's "Cover acceptance".

6. Render and accept

  • wb build. Sample 2~3 frames to check (ffmpeg -ss <t> -i final.mp4 -frames:v 1 x.png): subtitles at the bottom,the follow-along highlight sits on exactly the characters being read aloud(the color block hugs the text tightly, doesn't swallow punctuation, doesn't cut off English words like MiMo ), the watermark is in the top right, stickers erase normally; check the ending at 99-brand. When sampling frames,look at the original image before drawing conclusions: shrinking dozens of frames into one long image makes it easy to misjudge ink-colored text as "not highlighted".
  • wb captions <期>: without rendering images you can verify each subtitle's timing, text, and highlight segments ([a·b·c] in · 's position is exactly the beat where the color block jumps; roughly 0.2~0.6 seconds per segment is normal).
  • Check 字幕.srt First few notes: original text spelling, numbers not split.No highlighting in the SRT(plain text format can't hold it), highlighting is only burned into the picture.
  • Machine checks and delivery boundaries follow references/delivery-qa.md。
  • README.md and are written accordingly; duration is based on ffprobe .

7. Publishing copy and titles

  • According to references/publish.md: 5 title candidates (number contrast / event subject / conclusion first / life-unit conversion / question) pick 1 as primary; WeChat Channels intro, Douyin description (first 55 characters hook + 3~5 hashtags at the end + AIGC label), Xiaohongshu title+body+tags, Bilibili title, WeChat official account summary, pinned comment.
  • Numbers match README.md ; compliance self-check completed (references/compliance.md)。
  • fill in 发布.md, deliver the four-piece set.

Making long videos (>5 minutes)

There is no duration limit at the code level. Rendering outputs mp4 per scene in segments, then uses ffmpeg concat to stitch into the master (lib/render.js); the number of scenes is unlimited; TTS is also synthesized and cached scene by scene (lib/tts-volc.mjs). So "how long you can make it" depends only on content planning, not on the tool. Tested (2026-09-30, plan domain streaming endpoint): a single submission of 2200 characters / 6600 bytes (≈6.5 minutes of audio) also returns normally, so even if a certain scene is written very long it won't get stuck at the interface.

Target duration Number of scenes Total characters Acts
2~3 minutes 6~8 500~700 No acts needed
5~10 minutes 15~25 1200~2400 2~3 acts
15~25 minutes 40~60 3500~6000 3~5 acts
30~40 minutes 75~90 7000~9000 5~8 acts

Four extra things long videos do compared to short videos:

  1. Acts. Add a "chapter card" scene at the start of each act (a regular new Scene('02-act2', ...) is fine: act title + a one-sentence guide) so viewers know where they are; the timestamps in the publishing copy are also split by act. The end card is automatically appended by the tool, but chapter cards must be written by hand.
  2. A breathing point every 2~3 minutes. Whiteboard explanations get tiring after 20 straight minutes: a breathing point can be a one-sentence summary, a pitfall-in-action moment, or a comparison table. Don't just lay it all out flat.
  3. Switch the background music to a playlist. A 2~3 minute track paired with a 20 minute video has to loop a dozen or so times, which immediately breaks immersion—config.json 's bgm.playlist fill in several tracks ([{ "file": "assets/bgm-1.mp3" }, { "file": "assets/bgm-2.mp3", "trim": [0, 90] }]), they will be stitched in order into one whole segment and then looped as a whole, instantly reducing the sense of repetition; when playlist is empty it still falls back to bgm.file single-track looping.
  4. Don't mention duration in the cover label. cover.seriesTag Defaults to "whiteboard explained clearly"; if you want to emphasize duration, pass it in scenes.js 's cover according to this episode's actual duration as tag, don't hardcode "3 minutes" in config.

Time cost and size (measured on this machine with 4-way parallelism + extrapolation; 1080p30):

Duration Frame output wb build Full process Final video + intermediate artifacts
2.7 minutes ("DeepSeek Harness Desktop" measured 8 scenes) ~1.8 minutes ~3 minutes 12 MB + 24 MB
10 minutes ~7 minutes ~10 minutes ~46 MB + ~90 MB
30 minutes ~21 minutes ~30 minutes ~140 MB + ~270 MB

The frame rate is converted based on the measured value on this machine of ≈44 frames/sec (4 parallel lanes); the size is based on the measured value of ≈4.5 MB/min. Whiteboard-style frames are white and have little motion, so they are much smaller than live-action footage. If the machine is under strain, lower render.workers to 2.

For long videos, be sure to use resumable runs: both TTS and rendering are cached per scene, so changing one line of narration or one scene does not require rerunning the whole video —

wb tts   <期> <场景名>     # 只重配改过的那一幕(缓存命中时几乎瞬时)
wb render <期> <场景名>    # 只重渲这一段,并自动重拼 master
wb mix   <期>              # 出片

⚠️ wb build willre-render the entire episode(even if all TTS hits the cache); for long videos, do not use it for small changes. Also, wb captions <期> is especially useful on long videos: without rendering images, you can check the timing and highlight segmentation of 90 subtitles one by one.

How to handle modification requests

The user says Solution
Change a line of narration / add a segment Change scenes.js → wb build(only re-synthesize the changed scenes)
A fake robot was used / a real logo is wanted wb logo Get the official SVG → in scenes.js replace the s.image name with logo-* → wb stills → wb render && wb mix && wb cover
Change the sticker / the character looks ugly wb image Regenerate → wb stills → wb render && wb mix(if used in the cover, then also wb cover)
Drawing too fast/too slow, text pops out config render.pen:speed stroke px/s,charSeconds seconds-per-character range,minSeconds per-element minimum,gapSeconds pen-lift gap → wb render && wb mix
Visuals cannot keep up with narration(the audio is speaking, but the drawing is not finished; the later it gets, the more obvious it becomes) Measure first:node lib/diag-schedule.mjs <期目录绝对路径> prints the "stroke start offset" for each beat; > 0.35s means it is lagging, and it willaccumulate into every later beat. Fix (change config only):render.pen.overflow set 0(disable "if a beat cannot be finished, it overflows into the next beat"),speed raise to 1400~1600,charSeconds lower the upper limit to ~0.14,fillMaxSeconds ~0.8、gapSeconds 0.09; run diag again to verify ≤0.1s, and finally wb render && wb mix. Beats with >20 elements will be compressed and become noticeably faster; if you want more room, delete a few elements (this is more worthwhile than making the whole video 23 seconds later)
Subtitles and speech are misaligned(Xiaomi channel) node lib/diag-captions.mjs <期目录绝对路径> Check the "anchor rate" and start offset. If the anchor rate is not 100%, then some subtitle timings are relying on character-count interpolation; the source is almost alwaysa long short-sentence being forcibly split by captions.maxChars Hard split: increase maxChars so it can fit the longest short sentence (this episode: 25 characters → 26), or add a comma to that line of narration so it naturally splits into two short sentences (adding punctuation does not change the semantics, but you must rerun wb tts). See details in references/voices-mi.md
Rendering too slow / machine under strain config render.workers(default 4 lanes, one Chromium per lane; 1 = serial); the output frames are independent of the number of lanes and are identical frame by frame
Speech speed fast/slow config tts.speed(default 1.2) → wb build, all scenes are automatically re-synthesized, and subtitles stay in sync. ⚠️ Volcano is native speed change;Xiaomi is local atempo post-processing(the API has no speech-rate parameter)
Change the voice engine (Volcano / Xiaomi / edge) config tts.engine change "volc" / "mi" / "edge" → must rerun wb tts: the three providers have different audio, speech rates, and pauses; subtitles follow the audio, and the cache is also separated by engine. After changing, run wb voice Preview
Want a longer / shorter video Without changing code or config, only change scenes.js the scene count (converted per "1 minute ≈ 1.52 scenes ≈ 200 characters", see "Making long videos") → wb build. ⚠️This isrewriting contentwork, not tuning parameters: to lengthen, add scenes (each scene still 34 beats), don't stuff more narration into existing scenes. For >5 minutes remember to split into acts + add breathing points + change bgm.playlist
Music major/minor config bgm.gain → wb mix; for temporary preview wb mix <期> 0.4
Subtitle font size/position config captions.fontSize / baselineY → wb render && wb mix
Subtitle follow-along highlight (toggle / color / granularity / semi-transparent) config captions.highlight → wb render && wb mix。enabled turn it off to get the original static subtitles;color leave empty to use the brand color;opacity + keepHalo:true gives a semi-transparent marker effect;minUnitChars/maxUnitChars adjusts granularity (default 2/4, target 3 characters). After changing, first wb captions <期> to check the segmentation:[用的是·小米 ·MiMo ·的配音] in · is exactly where the color blocks jump in sequence | subtitle text and 字幕.srt are both unaffected
Watermark position/turn off/change logo config brand.watermark.position/enabled、brand.logo(transparent-background png) → wb render && wb mix
End card slogan / CTA / duration / none config brand.slogan、brand.endCard.* → wb render <期> 99-brand && wb mix
Cover copy / sticker / aspect ratio scenes.js at the end, the cover function → wb cover; aspect ratio config cover.ratios(including 9:16 for Douyin), labels cover.seriesTag(tag: '' removed)
Title / publish copy / switch platform Change 发布.md, no need to re-render the video
Changed the diagram in Obsidian wb render && wb mix directly reads .excalidraw.md; adding/removing elements changes the stroke order, for structural changes go back to scenes.js
Change voice / change track First wb voice to confirm the tier, then change the voice field of the respective engine: Volcano tts.voice(2.0 voices use resourceId: seed-tts-2.0, voice cloning uses volc.megatts.default) / Xiaomi tts.mi.voice(only nine, see references/voices-mi.md)/ edge tts.edge.voice; or .env the VOLC_TTS_VOICE、MI_TTS_VOICE / for music change bgm.playlist(multiple tracks concatenated in order, for long videos) or assets/bgm.mp3(single track loop; before changing tracks, first measure levels with volumedetect then set gain); temporary track change BGM=路径, temporary list change BGM_PLAYLIST='[{...}]'
Music rotates over a fixed few tracks config bgm.playlist fill in [{ "file": "...", "trim": [起, 止] }];trim omit to use the whole track,gain can balance loudness per track → wb mix. When the list is non-empty, bgm.file / bgm.trim is ignored
TTS reports Invalid X-Api-Key / not granted first suspectthe calling domainnot the quota:ark- Keys starting with planonly go to tts.endpoint, change config references/voices-volc.md error quick reference
Xiaomi reports Unknown voice / assistant role / 404 three pitfalls: voice not among the nine, body text not placed in the assistant role, path not {baseUrl}/chat/completions, see references/voices-mi.md error quick reference
Change account branding config brand.name/accent/slogan, leave the rest untouched

Platform notes: Windows, macOS and Linux

Platform differences are handled in the tool layer. Follow these rules in new scripts:

  • Path expansion:lib/paths.cjs、lib/scene-dsl.js、lib/render.js the ~ values all use require('os').homedir()。Do not use process.env.HOME: Git Bash represents it as the MSYS path /c/Users/x, which Windows Node resolves as C:\c\Users\x and fails with ENOENT.
  • file:// URL: Chromium must load render.html and local images using require('url').pathToFileURL(p).href, rather than manually constructing file:// + path.join(Windows backslashes are interpreted as escapes).
  • ffmpeg concat manifest: paths written into list.txt must use p.replace(/\\/g, '/')。
  • Chromium:lib/chromium.cjs 's resolveChromium() automatically selects the highest locally installed version in ms-playwright, avoiding a dead end where the required Playwright version is absent andnpx playwright install cannot install it either.
  • npm install may finish with exit 1 even though dependencies were installed:postinstall runs playwright install chromium, which reports Download failure, code=1if Chromium cannot be downloaded; at that point node_modules/ already contains playwright, roughjs and lz-string, so video production still works.Do not reinstall repeatedly because of this exit 1; to suppress it, use npm install --ignore-scripts, since the browser is selected through resolveChromium()。
  • On Windows, Codex has only codex.cmd,spawn('codex') fails with ENOENT;lib/gen-image.mjs 's resolveCodex() locates node_modules/@openai/codex/bin/codex.js and runs it through process.execPath .
  • Node networking: the Commons API requires a proxy on some networks;lib/proxy.mjs 's installProxyFetch() adds an HTTPS CONNECT tunnel to Node fetch (reads the HTTPS_PROXY / HTTP_PROXY environment variable, which may also be set in .env ; without configuration it does nothing).lib/fetch-logo.mjs、lib/gen-image.mjs entry points call it.
  • Local SVG to PNG conversion: for logos from outside Commons, such as official-site SVGs, use lib/svg2png.mjs through Playwright to create transparent PNGs.
  • bin/wb:ROOT Before passing it to Node, use cygpath -m to convert it into C:/...(Git Bash's /c/... is unreadable by Node; without cygpath , pass it unchanged);open uses explorer.exeon Windows and xdg-open。
  • When moving an episode directory outside the repository:config.json 's dirs.projects / dirs.build supports absolute paths, such as D:/临时/2026/9月/2026年9月29日. Restore temporary changes to ./episodes / ./buildafterward, and do not commit machine-specific absolute paths. Also, wb new generates scenes.js with __WB_ROOT__ replaced by the repository's absolute path, buta manually copied scenes.js has require('../../lib/scene-dsl') on line 4 relative to the episode directory and will fail with ENOENT after moving. Rewrite it as the repository's absolute path with forward slashes, or use wb new to regenerate it.
  • Use Git Bash on Windows; quote paths containing Chinese characters or spaces.

Known pitfalls

  • Logos: many official sites block retrieval with verification pages. Obtain logos through the Commons API (wb logo), rather than scraping official sites. The final I in OpenAI's 2025 wordmark is a vertical stroke, not a cropping error. The Claude star SVG has slightly jagged edges that are imperceptible below 300 pixels in height.
  • Volcengine TTS occasionally returns only half the narration:tts-volc.mjs validates character counts and retries automatically; regenerate all narration with FORCE_TTS=1. Cache keys include text, voice, speed, volume, style andAPI domain; changing any one regenerates the narration automatically.
  • The two Volcengine TTS API domains are separate(tested on 2026-09-29): API keys issued in the console beginning with ark- work only through the plan domain (/api/v3/plan/tts/unidirectional); direct-purchase or legacy credentials use standard domain (/api/v3/tts/unidirectional)。The wrong domain reports 45000010 Invalid X-Api-Key, resembling an invalid key or insufficient quota even though the domain is the problem. Check tts.endpoint before suspecting credentials.ark.cn-beijing.volces.com/api/plan/v3 is a separate Ark endpoint, not TTS; do not use it as the baseURL. The voice and resourceId must also match (2.0 voices ↔seed-tts-2.0); mismatches report 55000000 mismatched with speaker。lib/tts/tts.mjs 's explainFailure() translates these errors into actionable next steps;wb voice performs an early self-check.
  • Visuals lagging narration is the main synchronization problem in this scheduling scheme(tested on 2026-09-29 with the Xiaomi route):schedule()(lib/render.html) limits each beat's drawing duration to limit = 本拍结束 + overflow × 下一拍时长, whilethe next beat starts at max(本拍起点, 上一笔结束). If one beat cannot finish, every following beat is delayed;the error accumulates. With the tested defaults (speed:1000 / overflow:0.35), the worst beat lagged by 3.62 seconds: narration had reached beat three while visuals were still drawing beat two. The fix changesonly config.json 's render.pen:overflow: 0 + speed: 1450 + charSeconds:[0.05,0.14] + fillMaxSeconds: 0.8 + gapSeconds: 0.09and reduced the measured maximum start offset to +0.07s。Measure after changing it; do not rely on feel:node lib/diag-schedule.mjs <期目录绝对路径> prints each beat's start offset, end offset and compression ratio; target a start offset no greater than 0.35 seconds for every beat (diag-schedule.mjs accepts the PEN_OVERRIDE='{...}' environment variable for A/B tests with old parameters, without changing config).
  • Xiaomi MiMo TTS (tts.engine: "mi") differs from Volcengine throughout(tested on 2026-09-29): 1. Use POST {baseUrl}/chat/completions,The text body must appear in an assistant role message(user alone reports messages must contain an assistant role);/audio/speech、/tts all return 404. 2. The API has no speed parameter: top-level speed/audio.speed/audio.speech_rate values are ignored (audio.speed=0.5 and omitting it returnexactly the same bytes),tts.speed is implemented locally through atempo post-processing. 3. No word-level timestamps are returned(audio.transcript is always null, and the speech-recognition model returns only text). Sentence timing is estimated using punctuation-delimited clauses, silencedetect pauses and monotonic DP alignment. Measured boundary error was approximately 0.05 seconds, with per-sentence speed CV of 0.144;work/audio/<scene>.json 's align fields indicate silence-estimate(Volcengine uses native)。Clause boundaries exclude the enumeration comma(ALIGN_VERSION = 3): pauses there are weak and unstable. Of 28 tested instances, ten had no pause; DP could only interpolate by character count, moving a clause start 1.16 seconds early. Changing this set requiresupdating lib/tts/mi.mjs 's clausesOf and lib/captions.cjs 's PUNCT/splitTexttogether (both must match character for character; unequal segment counts otherwise degrade the entire video to proportional timing), then ALIGN_VERSION +1. 4. There are nine fixed voices (mimo_default / 冰糖 / 茉莉 / 苏打 / 白桦 / Mia / Chloe / Milo / Dean); an invalid name reports Unknown voice and lists every available value. 5. The silence threshold -35dB/0.10s is theoptimum found by a parameter sweep, not a sensitivity to adjust by intuition. Greater sensitivity worsened speed consistency in reproduction. See details in references/voices-mi.md。
  • After editing narration, first run wb scenes , then wb tts:scenes.js is the handwritten source, whereas TTS reads scenes/script.json(generated by wb scenes ). Editing only scenes.js and immediately running wb tts,still reads the old script.json, creating the false impression that edits did not apply (encountered on 2026-09-29).wb build includes scenes in its full pipeline, so build is unaffected.
  • Use two diagnostic scripts to verify subtitle and visual synchronization(both in lib/; neither changes artifacts and both may be rerun):
    • node lib/diag-captions.mjs <期目录绝对路径>: subtitle count, voiced coverage, median start offset andanchor rate(whether starts fall exactly on spoken-clause boundaries; ideally 100%).
    • node lib/diag-schedule.mjs <期目录绝对路径>: each beat's narration window versus actual drawing window, start/end offsets and compression ratio. Both scriptsaccept the episode directory's absolute path directly(without using dirs.projects), but still resolve the build-output directory through config.json 's dirs.build ;when producing videos across directories, ensure dirs.build matches the actual build-output directory containing the video, or measurements use audio from the previous build (the first diagnostic pass made this mistake and wasted a parameter-adjustment round).
  • Word-level subtitle timestamps depend on enable_subtitle: true(effective for Volcengine 2.0/ICL 2.0 voices; 1.0 voices use enable_timestamp)。Retesting on the plan domain: disabling enable_subtitle and directly running words=0degrades subtitle timing to character-count estimation. Do not disable it merely to save response data. Xiaomi has no such switch because it supplies no timestamps.
  • Subtitle text must use the original script; TTS words may turn @grok into atgrok. Subtitle timing comes from TTS word timestamps, so rerun TTS after changing narration.
  • Subtitle follow-along highlighting(config captions.highlight, enabled by default). When changing render.html , note three points (tested on 2026-09-29): 1. Do not use Range.getBoundingClientRect() measures a local character range in SVG text, but Chromium measures only the first character, turning MiMo into M. The color block becomes half the text width, and remaining highlighted characters are painted the paper color, appearing tolose characters. Use tspan.getExtentOfChar(i)(returns user coordinates, which map 1:1 to this page's canvas coordinates), and index bycode points [...str]rather than UTF-16 offsets. 2. Volcengine's wordList drops spaces(用的是小米 MiMo 的配音 becomes ...小米MiMo的配音), so reconstructing text from w.word is not the original script. Byte-for-byte checks would reject the match and fall back to proportional estimation; character limits may also split English letters into [米 MiM]·[o 的配音]。captions.cjs 's wordsToUnits() aligns bynon-whitespace charactersand takes text from original-script slices for this reason. 3. Adjust granularity only by character count, rather than merging short durations: duration-based merging destroys native word boundaries ([小米][MiMo] merges into [米 Mi][Mo 的]). Volcengine's token list ischaracter by character(individual characters such as subtitle letters). Greedy grouping targeting three characters produces [用的是]·[小米]·[MiMo]·[的配音]。
  • SVG dashes restart at each subpath's M , and rough.js adds a second stroke. Applying one dashoffset to the entire path makes all edges grow simultaneously and draws each twice. The renderer already splits subpaths into nodes and duplicate strokes into A/B layers; do not revert it.
  • rough.js toPaths has no dasharray; set dashed lines manually in render.html .
  • Text width is estimated; accurate centering requires align:'center' . Long left-aligned text must remain within 1920 pixels.
  • Playwright screenshots occasionally time out after 30 seconds. The renderer retries three times; if it still fails, rerun wb render。
  • Do not run wb imageconcurrently: if Codex does not save at the requested path, fallback retrieval uses the most recently generated image, so concurrent jobs may take each other's images.
  • Parallel frame output depends on two behaviors; preserve both:render.html 's seek() reinsert the background rectangle in place for every frame to force a full-screen repaint. Otherwise Chromium repaints only changed regions and frames depend on output order. Each worker opens its own Chromium. After changing the renderer, run render.workers=1 and 4 separately;ffmpeg -f framemd5 must find zero differing frames.
  • Using require() runs templates/scenes.js creates artifacts under templates/, so do not test that way. Use wb new。
  • Rendering time depends on the machine: a reference 2.5-minute video takes about 35 seconds to generate frames with four workers. On this machine, a tested two-minute video took about 82 seconds for all frames, andwb build took about 2.5 minutes overall (even a complete TTS cache hit rerenders and cleans the entire episode). To save time when changing one scene, use wb render <期> <场景> + wb mix. Do not use &for long background jobs; use the execution environment's built-in background facility.
  • Three minutes is an old guideline, not a limit(verified and corrected on 2026-09-30):lib/render.js produces MP4 segments by scene and combines the master with ffmpeg concat ;lib/tts-volc.mjs composites and caches each scene. Neither limits the scene count. Six–eight scenes or two–2.5 minutes were only content suggestions. The tested Volcengine plan-domainstreamingendpoint accepted 2,200 Chinese characters, or 6,600 bytes, yielding approximately 6.5 minutes of audio without error. Longer videos therefore need more scenes rather than code changes. The real costs areframe-output time (approximately 44 frames per second) and disk space (approximately 4.5 MB per minute), plus wb build rerendering the entire episode. Use wb tts/render <期> <场景> to resume long videos from checkpoints. Warning:do not hard-code duration in cover labels. The default cover.seriesTag previously said Explain It Clearly on a Whiteboard in 3 Minutes, contradicting longer videos; it now says Explain It Clearly on a Whiteboard. Also, bgm looping one song through a 20-minute video becomes distracting. For long videos, fill in bgm.playlist (multiple tracks are concatenated in order, then looped as a whole).
  • wb build 's cleanup step may be blocked by a safety hook. After the finished video, subtitles and covers are saved, deleting work/frames in batches of more than a few dozen files may make some agent sandboxes raise SAFE_DELETE_BULK_CONFIRM_REQUIRED. Delivery is unaffected; clean up manually once or use wb clean .
  • edge-tts engine(tts.engine: "edge"):lib/tts-edge.py reads work/audio/ and writes <scene>.mp3 + <scene>.json; WordBoundary events in the JSON form wordListin the same format as Volcengine word timestamps, so the subtitle pipeline stays unchanged. Cache keys include text, voice and speed;mismatches regenerate narration automatically(force regeneration with FORCE_TTS=1). The interpreter is specified by config.json 's tts.edge.python (point it to the installation containing edge_tts ); leave it empty to use python3 / pythonfrom PATH. Edge cannot reliably read Chinese polyphonic characters; spell numbers and English model names phonetically in Chinese in the narration.