Programmatic Explainer Video
Environment
- Install Manim:
uv tool install manim. If it reports pycairo missing pkg-config, runbrew install pkgconffirst. - Voiceover and transcription use MiniMax's CLI tool
mmx. Install:npm install -g mmx-cli, then log in withmmx auth login --api-key <your MiniMax API Key>. Ifmmxreports not logged in or insufficient balance, stop and inform the user. contact_sheet.pyuses Pillow from the system Python, also requiresffmpegandffprobe(brew install ffmpeg).- macOS defaults to PingFang SC. On Linux, install a Chinese font first, e.g.,
fonts-noto-cjkon Debian/Ubuntu. The base class checks if the font exists and stops if missing. Custom fonts can be specified withEXPLAINER_CN_FONTandEXPLAINER_EN_FONT; the font must support all characters in the body text. On other systems, use the respective package manager to install Manim, Pillow, and FFmpeg. - Most 3Blue1Brown examples online are written with
manimlib; their API is different from Manim Community. Do not copy them directly. scripts/in this skill:gen_audio.py: Generates voiceover and word-level timestamps.explainer_base.py: Scene base class, subtitle splitting, timing, layout, and cleanup.contact_sheet.py: Extracts frames based on animation nodes and the longest subtitle per segment, prioritizing two-line subtitle layout.verify_video.py: Verifies subtitle glyphs, audio timing, and decoding results of the final video.test_subtitles.py: Reproduces old bugs using real Manim rendering and validates subtitle regression.
Workflow
Place the project directory at ~/video-studio/explainers/<topic>/.
- Write an outline. Follow the order "pose a question → concrete example → how the example changes → general rule → test with a new example → answer the question". Split into 5 to 10 segments, each covering only one point. If the total length exceeds 3 minutes, send the outline to the user for confirmation.
- Write
narration.json. Keys are segment names (s1,s2...), values are the narration. Write the narration in spoken style, short sentences, one idea per sentence. If thechinese-writing-styleskill is installed, follow its style. Each segment should have 3 to 6 sentences. If TTS might misread an English word or abbreviation, use a dict{"text": "text shown on screen and for alignment", "tts": "pronunciation for TTS"}as the value. - Run
python3 <skill>/scripts/gen_audio.py <project directory>. The default voice for narration isChinese (Mandarin)_Warm_Girl. Write years, dates, and other numbers in Chinese pronunciation intts, but keep Arabic numerals intextfor on-screen display. To redo only certain segments, add--only s3,s5. If the output says "transcription and narration do not match", determine if TTS misread a keyword. If so, fix thettsfor that segment and redo with--only. If character-level timestamps are missing, first repair the transcription; do not deliver subtitles estimated by proportion of segment length. - Create a symbolic link to the shared
scripts/explainer_base.py, then writescene.py. For existing projects, first remove local subtitle implementations and overrides ofstart/finish, keep visual logic, then back up the old base class and replace it with the link. The project should only code the visual explanations; subtitles are generated uniformly by the base class. - In the project directory, run
manim -ql --disable_caching scene.py <ClassName>. If it reports "animation cannot keep up with narration", shorten therun_timeof the corresponding animation or remove the animation, then re-render. - Run
python3 <skill>/scripts/contact_sheet.py <preview video> <project directory>and examine each contact sheet. Check subtitle layout, element overlap, and arrow direction. For animated phrases with estimated timing, verify against the character timestamps inaudio/<segment>.words.json. - Run
manim --resolution 1920,1080 --frame_rate 30 --disable_caching scene.py <ClassName>. Do not use-qh(60fps). Run<skill>/scripts/verify_video.py <final video> <project directory>with the Python that installed Manim. Foruv tool install manim, the corresponding interpreter is$(uv tool dir)/manim/bin/python. - The verification command must exit with code 0, and
qa/video-check.jsonmust havepassedequal to true. If glyphs, audio timing, or subtitle positioning fail, check the report, screenshots, and correspondingwords.json, fix, re-render, and verify again. Estimated subtitles cannot be approved based on screenshots alone. - After verification, copy the final video to
<topic>-explanation.mp4, inform the user of the video path, and if the current environment can send files, send it directly. Delete low-resolution previews undermedia/.
Scene Code
from explainer_base import *
class Topic(ExplainerScene):
def s1(self):
self.start("s1") # Play voiceover and automatically display synchronized subtitles
title = cn("learning rate", 60, HI)
self.play(Write(title))
self.until("learning rate too small") # Wait until narration reads this phrase
self.play(title.animate.to_edge(UP))
self.finish() # Wait for voiceover to finish, clean up subtitles and fade out
- Write a method named after each segment.
- Do not override
start,finish, or the subtitle updater inside the project. If the user explicitly does not want subtitles, set the class attributeSUBTITLES = False. - When transitioning between visuals within a segment, only fade out or remove objects you created. Keep the base class subtitles. Do not clear all
self.mobjectsor callself.clear(). - The phrase in
until()must appear verbatim in thetextof that segment's narration, and appear only once. - Use
cn()for Chinese,en()for English. Font size should not be smaller than 22. - Use only the base class's 4 colors:
MAINfor main body,HIfor emphasis,OKfor correct,BADfor error. A single color must have only one meaning throughout the whole video. - At most 3 groups of elements on screen at a time. Fade out content no longer being discussed before bringing in new content.
- Leave the bottom 20% of the screen for subtitles; place charts and explanatory text above this area.
- When the narration mentions an element, that element should appear, change color, or move.
Common Pitfalls
- After modifying shared subtitle code, run
test_subtitles.pywith the Python that installed Manim. It must pass checks for real waiting, inter-segment transitions, two-line subtitles, repeated short phrases, long sentence pagination, and different pronunciation alignment. Manually settingrenderer.timeand calling the updater alone cannot verify Manim's static waiting behavior. Textdoes not automatically wrap long lines; use\nto break lines.- For text placed inside a bounding box, restrict width with
fit(text, bounding_box_width * 0.8). - When crossing out an option with
Cross, only cross out the text, not the arrow; change the arrow to theBADcolor instead. - To color specific words in a sentence, use
Text(..., t2c={"word": color}).