Doodle Comedy Short Video Series
One video = one script + one storyboard + one audio track + one cover + one set of publishing materials. Everything is drawn with the same set of drawing primitives and fonts, so each episode in the series has a consistent style, and the cover perfectly matches the video.
Delivery Contract
Each episode must deliver these 5 groups of files, none missing:
| Output | File | Spec |
|---|---|---|
| Subtitled video | out/<title>_doodle_commentary_subtitled.mp4 | 1080×1440, 30fps, H.264+AAC, ~68s |
| Unsubtitled video | out/<title>_doodle_commentary_nosubs.mp4 | Same as above, without subtitle layer |
| Subtitle file | out/<title>.srt | Single line ≤16 characters, timestamps increasing, no overlap |
| 3:4 Cover | out/<title>_cover.png | 1080×1440, same line style and font as the video |
| Channel materials | project_*/publish.md + publish.json | 3 title options, 1 description, 5-8 hashtags, 1 pinned comment |
Also included: out/<title>_preview.png (four keyframe strip) so the user can quickly see the whole episode.
Environment
Python 3.10+, Pillow, NumPy, ffmpeg/ffprobe. First create a virtual environment, do not touch the system Python:
python3 -m venv .venv && .venv/bin/pip install -r "$skill_dir/scripts/requirements.txt"
On macOS, ffmpeg is often not in the default PATH. First run export PATH="/opt/homebrew/bin:$PATH".
Voice synthesis uses mcode-tools (MiniMax TTS/ASR), which requires the mcode-tools-master skill to be on the PATH.
If python3 reports You have not agreed to the Xcode license agreements (exit code 69),
it means the system Python is not set up properly. Use a different interpreter to create the environment, for example
/opt/homebrew/bin/python3.13 -m venv .venv, and don't touch the Xcode license.
Workflow
1. Choose Topic and Series Positioning
Single episode: just suggest a humorous direction. Multiple episodes: first give each episode a mutually exclusive humorous flavor, don't just reskin the same joke type.
- Self-deprecating absurdist (the narrator gets more ridiculous as they go)
- Dialogue twist (two people talk, last line flips the script)
- Cold observation (observational commentary, understated dry humor)
- Absurd premise (take a ridiculous premise as if it's real)
In the same series, ensure at least two different humorous flavors are adjacent. Visual language can be shared, but narrative rhythm should not be identical.
2. Write the Script
Read [references/writing-style.md]. Hard targets:
- One shot per sentence, about 40-45 sentences, 480-520 characters, lasting about 68s (characters ÷ 7.4 ≈ seconds).
- Every punchline must have a visible action or prop change to land it; it can't just rely on subtitles.
- The ending should flip the character relationship, leaving a single quotable line.
- Write as JSON:
{"title": ..., "target_seconds": 68, "segments": [{"scene": ..., "text": ..., "red": [...]}]}sceneis the visual ID,redare words to highlight in red (must appear verbatim intext).
3. Split into Shots
"$python_bin" "$skill_dir/scripts/build_segments.py" --story story.json --out project_ep2
Subtitles are split at punctuation into lines of ≤16 characters; the renderer cannot handle lines longer than 17 characters. Split points should be punctuation marks near the 10-character mark, avoiding splitting words (e.g., "大额预付" must not become "大额预" and "预付"); quoted phrases should not be broken across lines. The script validates that red words appear in subtitles, no words are lost during splitting, no empty subtitles (only punctuation) appear, and prints an estimated duration: characters ÷ 7.4 ≈ seconds. A deviation of ±10% from the target triggers a warning — add or remove sentences based on the hint, don't compensate by changing the speaking rate.
4. Draw Storyboard
Write scene_<id>(c, t, d) functions in project_ep2/scenes.py, and redefine the SCENES dictionary at the end of the file (only including IDs for this episode). See scripts/render/doodles.py for drawing primitives.
person(c,x,y,s,expr,boss,pose,t,arms,legs): head center at (x,y), body extends to y+307*s,exprtakesdead/sad/happy/angry/shocked/sleep,posetakesstand/shrug/point/phone/pockets/walk.woman(c,x,y,s,expr,t): mother character (bun + dress).speech(c,x,y,w,h,tail,size): speech bubble;c.text((x,y),'characters',size)uses center anchor.c.curveonly accepts 4 control points;c.textdoes not support outlines or multiple lines.
After editing, run two checks:
"$python_bin" "$skill_dir/scripts/check_overlap.py" project_ep2/scenes.py # Static layout audit
"$python_bin" "$skill_dir/scripts/check_project.py" project_ep2 # Contact sheet review
check_overlap.py detects text overlapping characters, text overlapping text, and elements outside the safe zone. It already recognizes two cases that "look like overlap but aren't": mutually exclusive text replacements in if/else blocks, and white background blocks covering old elements. So reported overlaps are virtually always real problems — if the exit code is non-zero, fix them first; don't render directly.
check_project.py first checks subtitles, then generates a contact sheet: subtitles concatenated must be the exact same sentence as the script (punctuation may be polished manually, but no extra words, missing words, or word substitutions). Empty subtitles (no text) are also not allowed; if anything fails, exit immediately without wasting time rendering the contact sheet.
If an overlap is intentional (e.g., a badge drawn on a character), explicitly register it in scenes.py:
OVERLAP_ALLOW = {"three_years", "me_too"}
Review the contact sheet frame by frame: does the image match that line of dialogue? Are the props clear?
5. Voice Synthesis
"$python_bin" "$skill_dir/scripts/build_voice.py" --project project_ep2 --speed 1.5 --gap 0.10
Sentence-by-sentence synthesis is key: each sentence is TTS'd individually, trimmed of leading/trailing silence, then concatenated with a fixed gap. So the start and end times of each sentence are known, and asr.json is constructed directly from these times, giving zero subtitle alignment error — no word-level ASR needed.
- Default voice:
lengdan_xiongzhang(indifferent senior). Use a different voice if the user requests one. - TTS has short-term rate limiting (HTTP 429). The script uses a fixed 10-second retry interval — fixed intervals are far more effective than exponential backoff here.
- A single episode has about 40-45 sentences, taking about 15 minutes total; runs in the background.
- After synthesis, spot-check with
scripts/tts.py asr --file project_ep2/voice.wav. - If ASR misrecognizes similar-sounding words (e.g., "办事/办事时"), only modify the characters in the ASR result; don't change the script or re-record.
6. Timeline, Rendering, and Verification
"$python_bin" "$skill_dir/scripts/deliver.py" project_ep2 <title> --target 68
"$python_bin" "$skill_dir/scripts/verify.py" project_ep2
deliver.py calculates the atempo speed based on the target duration (speed = original audio / (target - trailing silence)), generates the timeline, subtitles, mixes audio, renders subtitled and unsubtitled versions, and archives the video and SRT into out/.
verify.py checks: encoding parameters, full video decode, subtitle timing and length, ASR playback.
The episode is only considered complete when all checks pass green. If decode fails or subtitle timing has issues, fix them and re-render.
7. Cover
The cover uses no AI-generated images; it's redrawn using the same set of drawing primitives and fonts as the video, with zero style drift.
Write illustration(c) in project_ep2/cover.py (drawing primitives are injected into the namespace), and write text lines in project_ep2/cover.json:
{"lines": [
{"text": "Blind Date Scene", "size": 146, "y": 170, "red": []},
{"text": "My Mom Signed Me Up for Seven", "size": 96, "y": 312, "red": ["Seven"]},
{"text": "Meet the Parents Grandma", "size": 64, "y": 1196, "red": []},
{"text": "Eighty-Seven and Not Pushing for Kids", "size": 64, "y": 1280, "red": ["Pushing for Kids"]}
]}
"$python_bin" "$skill_dir/scripts/make_cover.py" project_ep2
Layout rules are in [references/cover-design.md]. The cover must be visually inspected before delivery.
8. WeChat Channels Publishing Materials
First gather the materials, then write the copy:
"$python_bin" "$skill_dir/scripts/make_metadata.py" project_ep2
The script prints the full script, red words, frequent words, and the last three sentences. Based on this, write:
- 3 title options, with emoji hooks. Each option should be ≤22 characters ideally (can be longer, hard limit 100 characters).
- 1 final description, 2-3 lines. The first line is the hook; hashtags are inline in the text as
#hashtag. - 5-8 hashtags, all must appear in the description (script validates this).
- 1 pinned comment, to guide replies or self-deprecate.
After writing:
"$python_bin" "$skill_dir/scripts/make_metadata.py" project_ep2 \
--titles "Title One" "Title Two" "Title Three" \
--desc "Description body #hashtag1 #hashtag2" \
--tags "hashtag1" "hashtag2" \
--comment "Pinned comment"
See [references/publish-channels.md] for detailed rules. Output: publish.md / publish.json.
Hard Constraints
- Canvas: 1080×1440 (3:4), 30fps, pure white background, 4-6px black outline, large round head, thin body.
- Main subject centered in y=400-1050, space at the top; subtitles start at y=1084, minimum font size 54.
- Subtitles use Xingkai font, white text with dark soft shadow, punchline keywords in red.
- Subtitle lines ≤16 characters; the renderer cannot handle anything longer.
- Do not bake the text into the video; subtitles are a separate layer during rendering.
- The cover must use the exact same font and line style as the video; no style changes allowed.
- Preserve already-delivered videos; use a new project directory for script changes; don't overwrite delivered episodes.
Failure Handling
- TTS keeps getting 429: The script already retries with a fixed interval 40 times. If it still fails, reduce speed to 1.4 and rerun, or split the run (
--limit Nto continue; the cache will skip already-synthesized sentences). prepare.pyreports ASR mismatch: This meansasr.jsonand the script don't match, usually because the script was edited without re-synthesizing voice. Re-runbuild_voice.py; don't use--allow-asr-mismatchto paper over it.render.pyreports subtitle too long: A caption exceeded 16 characters. Go back tobuild_segments.pyand adjust the split rules.check_overlap.pyexits non-zero: Fix the coordinates scene by scene as reported, until exit code is 0.- Don't say the audio check passed without listening to the entire audio; don't say you watched the full video without watching it. The delivery record should only include verification steps actually performed.
References
- references/writing-style.md — Script writing and joke design
- references/series-plan.md — Series topic selection and differentiation
- references/cover-design.md — Cover layout rules
- references/publish-channels.md — Channel publishing material rules
Windows (win32) Platform Notes
Equivalent practices on Windows:
- Virtual environment:
py -3 -m venv .venv, then use.venv\Scripts\python.exeinstead of$python_bin. - Dependencies:
.venv\Scripts\pip.exe install -r "<skill_dir>\scripts\requirements.txt". - ffmpeg/ffprobe: Install directly into PATH (e.g.,
winget install Gyan.FFmpeg); no need to add/opt/homebrew/bin. However, the ffmpeg paths inscripts/deliver.py,scripts/make_preview.py, andscripts/verify.pyare hardcoded macOS absolute paths — change them toffmpeg/ffprobefor cross-platform compatibility. scripts/build_voice.pydepends onmcode-toolsbeing on the PATH, andcurl. Windows 10 1803+ includes curl; if missing, replace withInvoke-WebRequest -OutFile <path> <url>.- Path separators: All scripts use
pathlibfor path handling. When passing arguments, quote the paths.