Turn a math problem into a 16:9, 1920×1080 MP4 with Chinese narration + Chinese-English bilingual subtitles + hand-drawn notebook-style animation.
Pipeline (one folder per video):
script.json ──build_audio.py──► build/timeline.json + build/mix.wav + ../<name>.srt
(旁白) (GLM-TTS + 合成音乐) │
anim.js + engine.js ◄─────────────────────────┘ (按旁白时间点驱动动画)
└──node render.mjs video──► ../<name>.mp4
Core principle: the visuals follow the narration, and the diagrams "explain the problem" too. For every narration sentence, there is an action on the diagram that performs this reasoning (a segment slides over to coincide, a triangle is overlaid, the camera turns to a top-down view, a moving point moves…), not just an extra line or label appearing. Every animation is timed using "the moment when the k-th narration sentence begins"S.at(k, f) , never with hardcoded seconds.
This skill includes all the code itself, do not copy files from other projects:
template/: a complete, directly runnable example project (right-triangle median-to-hypotenuse problem).engine.jsis the general engine (do not modify),anim.js/script.json/episode.json/problem.pngmust be rewritten for every problem.shared/pron.py+shared/pron.json: pronunciation control (GLM-TTS has no SSML/pinyin input).examples/cone-parallel/: a complete solid geometry example (anim.js + storyboard.md + script.json): camera oblique view ↔ top-down view, a side face unfolds into a sector, alternate interior angles rotate, a plane translates. Read it first when doing solid geometry, circles, or multi-question problems.scripts/:new_video.sh(create project),setup_check.sh(check environment),make_problem_png.py(word problem → image + highlight boxes),problem_boxes.py(screenshot → highlight box coordinates),contact_sheet.py(screenshot montage review).
Below, SKILL = the directory containing this file,WS = the user's current directory (your working directory,$PWD): the video folder is created here, and the mp4/srt are output here too.Do not switch WS over just because another directory (including the project this skill is in) already has .env、node_modules、pron.json Just switch WS over, and whatever is missing will be filled in by new_video.sh and step 2.PROJ = this video folder,PY = setup_check.sh the python reported by /usr/bin/python3; on macOS this is usually python3 in PATH may not have numpy).
Rules that must be followed
If the exit code is not 0, it is not done. --check exit code 1 (even if only one ⚠ polyphonic character remains),motion exit code 1, or a problem in a screenshot — none of these can be written off as "just a warning, no impact" and then continue. Fix it before moving on.
- Strictly follow the 10 steps below in order, and only move to the next step once that step's "pass criteria" are met. Do not skip steps, and do not "check everything together at the end".
- First explain the problem clearly. The first scene of the video must show the original problem image (a screenshot or an image generated by
make_problem_png.py), and the narration reads the conditions one by one, framing that condition on the image each time it is read. ttsThe field can only be pronounceable Chinese. It cannot contain Arabic numerals,= + − × ÷ / ^ √ ∠ △ ∥ ° ( )or symbols such as:AC=3is written asAC等于三,x²is written asx的平方. The subtitlezhfield keeps normal mathematical notation.--checkwill report an error and block it.- Pronunciation must be fixed. Before calling TTS,
--checkmust display未固定读音的生僻字/多音字: 0andscript errors: 0. The annotation for a polyphonic character is written after that characterafter:长[cháng]、要[yào]求。must notwrite it as中点[zhōng](that would read "点" as zhōng). - The API Key can only be provided by the user. When missing
GLM_API_KEY, first ask the user: configure a key (best audio quality, recommended), or use the fallback engine (edge-tts / macOSsay, see reference/glm-tts-setup.md). Do not make up a key, model name, or API endpoint address; do not print the key or write it into the project folder. - All on-screen content within y ≤ 860(y≈9141044 is the subtitle box). Put graphics in the left half x 60900; write the derivation text on the ruled board on the right
board(). - First write the storyboard, then write the animation.
storyboard.mdFor each line of narration, plan "point/move/keep", and follow reference/visual-design.md 's action table to choose actions;node render.mjs motionmust pass. - Preview first, then spend money. Use
--preview+stills autoto check the layout, and only run the real TTS once it's fine. - Verify by looking at the images. After every rendered screenshot, open
build/sheet.pngand check it with your own eyes (using an image-reading tool); do not just look at whether the command reported an error. When reporting, write what you actually saw; do not check everything off from impression. - Do not modify
engine.js, unless the user asks to change the style; if you need a new graphic, write a function inanim.js. - Do not copy
node_modules、package*.jsoninto the video folder; install the dependencies once in theWSroot directory, and all videos share them.
11 steps
Step 1: Understand the problem, and first solve it correctly yourself
- Get the problem text (if there is a screenshot, read the image first).First solve it completely yourself and verify the calculation, and write down: the list of known conditions, what is being asked, the key ideas (1–2), each step of the calculation, the final answer, and the verification method.
- When the answer is uncertain or the problem is unclear, ask the user; do not guess.
- Pass criteria: You can explain it in 3–6 "scenes", with each scene's purpose clear in one sentence; the answer has been verified.
Step 2: Create the project, check the environment
bash SKILL/scripts/new_video.sh "$PWD" <ascii_folder_name> # WS 就是当前目录
bash SKILL/scripts/setup_check.sh WS/<ascii_folder_name>
new_video.shwill linkpron.py/pron.jsonto the skill's built-in shared word list, and when it can find it, linknode_modulesto the existing installation (without copying); only if it cannot find it do you need toWSundernpm install. For Python packages, usePY -m pip install --user numpy requests pypinyin pillow。new_video.shreportsERROR: ... is where the skill is installed, it means you set WS to the project where the skill is located; switch to the current directory.- The default voice is
chuichui(Chuichui); the user can change it in.envusingGLM_VOICE. - Missing
GLM_API_KEY:stop, and follow reference/glm-tts-setup.md to guide the userto configure (recommended: put it in~/.config/math-problem-video/.env, works in all directories), then continue.Do not go looking for.envin other projects, and do not switch the working directory because of this. If the user has no key or does not want to configure one:TTS_ENGINE=auto(default) will automatically switch to the fallback engine,setup_check.shwill show which one is being used; get the user's consent before installing edge-tts. - Pass criteria:
setup_check.shoutputsALL OK。
Step 3: Test TTS (verify the key and voice)
cd PROJ && PY build_audio.py --say "你好,我们来看一道数学题。"
- on success it will print
WROTE build/say_xxxx.wavandvoice: ...(edge:/say:Start = fallback engine). 401/403 = wrong key or no balance; tell the user, do not retry 10 times. - The first time you make a video for this user, tell the user this wav path to preview and confirm the voice (see glm-tts-setup.md for the voice list).
- Pass criteria: wav generated.
Step 4: Prepare the problem image problem.png and highlight boxes
choose one of the two (details in reference/animation.md):
- text only:
PY SKILL/scripts/make_problem_png.py --text "完整题目" --mark "条件1" "条件2" ... "所求" --out PROJ/problem.png, it directly outputssrcW/srcH/boxes。 - has screenshot: copy to
PROJ/, if necessaryproblem_boxes.py cropcrop out irrelevant parts and save asproblem.png; thenproblem_boxes.py gridgenerate a coordinate grid image and read out the pixel box for each condition; thenproblem_boxes.py checkdraw boxes to confirm. - Fill the dimensions and boxes into
anim.jsthe top ofPROBLEM。 - Pass criteria: you opened
*.preview.pngor*.check.png, the text is complete (no ☒/□ boxes), and each box exactly covers the corresponding text.
Step 5: Write episode.json and script.json
Write according to reference/script-writing.md . Key points:
episode.json:title(video title),output_name(output filename, e.g.斜边中线_Median_to_Hypotenuse)、pop_scenes(the act that needs the "bo" sound effect).script.json: array of acts, each act{"scene": "英文id", "lines": [{"zh","tts","en"}, ...]}. First actintroreads the problem, last actoutrorecap + report the answer.- Each narration sentence ≤ 36 Chinese characters wide (otherwise subtitles wrap to two lines), and each sentence covers only one thing.
- Pass criteria: JSON is valid; every act id has a planned visual.
Step 6: Write the storyboard storyboard.md
First read reference/visual-design.md, then follow the format of template/storyboard.md to write one line for each narration sentence:| 幕id 句号 | 旁白要点 | 指 | 动 | 留 | 板书 |。
- "Point": how the element mentioned in this sentence lights up. "Move": on the imageareasoning action, choose from the action table (equal→copy slides over to coincide, congruent→triangle overlays, 3D↔2D→camera rotates, find length→number counting...). "Leave": the mark left after the action.
- Except for the first and last acts, "Move" cannot be empty, and cannot just say "appear/show". Each act must have at least one continuous motion.
- Think first: if the viewer only looks at the image and not the board, can they understand why this step holds? If not, change the action.
- First write at the top of storyboard.mdfigure list: all points, line segments, parallel/perpendicular/midpoint relationships, and known lengths in the problem must all be drawn, with positions calculated from the data.
- Pass criteria: every sentence has a line; the figure list is complete; you can state the action in each act that is "most understandable".
Step 7: Pronunciation and script check, fix to 0
cd PROJ && PY build_audio.py --check ; cat build/pron_report.txt
ERRORlines: follow the prompt to changescript.json(usually there are numbers/symbols in tts, or anim.js is missingSC.<id>; after step 8 writes anim.js, the latter will report an error, which is normal).⚠ 多音/生僻lines: handle according to reference/pronunciation.md : add after the character[拼音], or add recurring words topron.json'swords, and add words confirmed to have correct default pronunciation took。- Pass criteria:
未固定读音的生僻字/多音字: 0andscript errors: 0, exit code 0.
Step 8: Write anim.js
Implement sentence by sentence according to storyboard.md, API see reference/animation.md. First read through template/anim.js(example) before starting, keep the structure consistent:
PROBLEM(data from step 4),TAGS(label at the top-left of each act), graphics functions (such asfig(st))、SC.<id> = (lt, S) => {...}(one per act).- The appearance time of each element = the moment the narration mentions it:
S.P(S.at(k, f), 时长)。 - Each sentence:
glow(..., bump(lt, at(k)))light up the mentioned element → the action in the storyboard (slideSeg、movePoly、camTween, moving point,countTo…) → leave a mark. - On the right,
board()+bl(行号, 文字, S.at(k, f), lt)write the derivation line by line (same color as the diagram), finallystamp()stamp to give the answer. - Pass criteria:
PY build_audio.py --checkno ERROR.
Step 9: Free preview: action check + layout check
cd PROJ && PY build_audio.py --preview && node render.mjs motion && node render.mjs stills auto && PY SKILL/scripts/contact_sheet.py .
motionTake 6 frames in each sentence, compare the changes in the left-half graphics area:STATIC(no change) must be fixed; each act has at least one sentenceMOVE. If it does not pass, go back to the storyboard and replace it with a better action; do not just add a meaningless shake to make up the numbers.stills autoTake a frame near the end of each narration sentence (at this point everything that should appear in this sentence should be on screen). Command outputFAILED: page errorindicates anim.js has a JS error, fix it first.- Open
build/sheet.png(when more than 12, it issheet_1.png、sheet_2.png…) check them one by one, against the review checklist in reference/animation.md: no overlap, no out of bounds, no blocking the subtitles, everything mentioned in each sentence is drawn, and the values match the solution. - If there is a problem → modify anim.js → rerun this step.
- Pass criteria:
motion check passed; all screenshot checks pass (explain act by act in the reply what you saw).
Step 10: Actually generate the voiceover, automatically transcribe and verify, then check the visuals once more
cd PROJ && PY build_audio.py && PY build_audio.py --asr && node render.mjs stills auto && PY SKILL/scripts/contact_sheet.py .
- TTS is cached by text in
build/tts/, changing a sentence will only re-synthesize that sentence. --asrUse Zhipu speech recognition to convert each voiceover sentence back to text, write tobuild/asr_report.txt(results are cached).Sentences whose letter sequence does not match are marked ✗, exit code 1: modify that sentence (see the "letters" section of pronunciation.md) and rerun these two commands. Also scan the Chinese characters in the report; if you find obviously mispronounced characters (not homophones, and not cases like ASR writing "三" as "3"), handle them according to step 7.- The actual duration differs from the preview, review the sheet again.
- Pass criteria: prints
mix + srt written;--asrshows字母读错的句子: 0(when there is no GLM key, showsASR SKIPPED: when delivering, tell the user that the letter pronunciations have not been machine-verified, and list the sentences containing names for the user to listen to carefully); screenshot checks pass.
Step 11: Render the video and deliver
cd PROJ && node render.mjs video 6 # 6 = 并行浏览器页数(不是帧率!帧率固定 30)
- Output
WS/<output_name>.mp4andWS/<output_name>.srt(WS = the user's current directory). A 2~3 minute video takes about a few minutes. - When delivering, tell the user: mp4/srt paths, duration, content of each act;explain that you cannot listen to audio, ask the user to listen to the pronunciation, and especially list the characters whose pronunciation you annotated. When the user reports a mispronunciation: add to
pron.jsonor inline annotation → then run steps 10 and 11 again (only the changed sentences will be re-synthesized).
Common mistakes (that actually happened)
| Wrong approach | Correct approach |
|---|---|
The current directory does not have .env/node_modulesthen build the project in another directory that has these things (for example, the project where the skill is located) |
WS is always the user's current directory; missing dependencies are linked by new_video.sh, and if the key is missing, ask the user to configure it ~/.config/math-problem-video/.env |
| copy code from another video folder (chem, etc.) | Use new_video.sh from this skill's template/ to build the project |
fabricate GLM_MODEL=glm-4-voice、api.zhipuai.cn and other configurations such as |
The model is only glm-tts, and the address is already written in build_audio.py; the user only needs to provide GLM_API_KEY, optional GLM_VOICE |
node render.mjs video 24 thought it was 24fps |
the parameter is the parallelism count; use stills autofor previewing the layout, and do not repeatedly render the entire video |
中点[zhōng] |
The annotation only applies to the immediately preceding character:中[zhōng]点(actually "midpoint" does not need to be annotated) |
write in tts AC=3、60 − 7t、x² |
AC等于三、六十减七t、x的平方 |
| the problem screenshot can be placed arbitrarily at (50,50) 400×400 | Use PROBLEM + problemCard() to automatically fit into the card below the title |
Use absolute seconds to time if (lt > 12.5) |
S.at(k, f): at proportion f after the k-th sentence starts |
| deliver after rendering without looking at the visuals | look at it every time build/sheet.png |
manually write in tts C、O or C,O to separate letters |
No need to worry about it: pron.py automatically splits adjacent letters into C O(in practice, commas and enumeration commas cause extra pauses) |
--check If there is still ⚠, adjust TTS |
fix it to 0; otherwise you will spend money synthesizing incorrect pronunciations |
add all common words to pron.json or inline annotations for the sake of "safety" |
Only annotate characters that will be mispronounced; replacing them with homophones will break sentence segmentation (直搅三搅形),pron.py already only replaces when mispronounced in context |
| a clause of more than twenty characters without punctuation | add commas at breath points, clauses ≤ 18 characters (--check will warn), otherwise TTS pauses in the middle of a word by itself |
In the problem image, x² is displayed as x☒ but it was not noticed |
zoom in to check the text during review; make_problem_png.py already switches fonts automatically |
after modifying JSON with an editing tool, curly quotes appear “, parsing fails |
JSON syntax quotes must be English "; see troubleshooting.md |
| draw the diagram once and for all, then each sentence only adds one more line/label, and all explanation is on the board | every sentence has "point→move→keep"; if equal, slide over and coincide, and use camTween;render.mjs motion for 3D↔top view must pass |
| when a top view is needed, draw another diagram beside it | the same 3D model camTween rotates to pitch = π/2 |
| cram three or four knowledge points into one narration sentence | one point per sentence, and one element appears synchronously on the screen |
Reference files
- reference/glm-tts-setup.md: Zhipu GLM-TTS registration, API Key,
.env, voice, speed, error handling.Read this when guiding the user through configuration. - reference/script-writing.md:
script.jsonformat, act design, and a comparison table of spoken ways to write mathematical expressions. - reference/pronunciation.md: pronunciation control principles, annotation syntax,
pron.json, common polyphonic characters in mathematics. - reference/visual-design.md:how to design explanatory animations: Finger→Move→Stay→Connect, reasoning→animation action table, 3D to top-down view, cone unfolding, storyboard format. Must-read before writing storyboards.
- reference/animation.md: engine.js API, scene syntax, layout coordinates, geometry/function/3D/distance problems drawing methods, review checklist.
- reference/troubleshooting.md: Errors and handling.