Communitygithub.com

Doodle Comedy Series

Produces a series of white-background, black-line, big-head Chinese comedy-sketch shorts (白底简笔吐槽短视频): pick a topic, write the script, draw the scenes, voice each line, render the cut, draw a 3:4 cover, and write the WeChat Channels (视频号) title, description, hashtags and pinned comment.

What is Doodle Comedy Series?

The Doodle Comedy Series skill automates the creation of a full video pipeline for producing consistent, stylized Chinese comedy-sketch shorts with a distinctive white-background, black-line, big-head illustration style. It is designed for AI coding assistants like Claude Code, Cursor, or Codex, primarily on macOS (with Windows fallback notes). The skill guides the agent from topic selection and script writing (with strict word counts and punchline rules) through scene drawing (via Python functions using doodle primitives), voice synthesis (using MiniMax TTS/ASR via mcode-tools), rendering (1080×1440, 30fps, with subtitles), cover art generation (using the same style as the video), and finally publishing materials for WeChat Channels (three title options, description, hashtags, and a pinned comment). It enforces a "one shot per segment, one segment per line" structure, validates every output (overlap checks, ASR consistency, timing, encoding), and provides detailed error recovery strategies for common issues like TTS rate limits, subtitle length violations, or layout overlaps. The result is a complete, self-contained episode package (video, subtitles, cover, metadata) that can be delivered as a single episode or a multi-episode batch.

Works with~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/inhai-wiki/doodle-comedy-series/tree/HEAD

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

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:

OutputFileSpec
Subtitled videoout/<title>_doodle_commentary_subtitled.mp41080×1440, 30fps, H.264+AAC, ~68s
Unsubtitled videoout/<title>_doodle_commentary_nosubs.mp4Same as above, without subtitle layer
Subtitle fileout/<title>.srtSingle line ≤16 characters, timestamps increasing, no overlap
3:4 Coverout/<title>_cover.png1080×1440, same line style and font as the video
Channel materialsproject_*/publish.md + publish.json3 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": [...]}]} scene is the visual ID, red are words to highlight in red (must appear verbatim in text).

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, expr takes dead/sad/happy/angry/shocked/sleep, pose takes stand/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.curve only accepts 4 control points; c.text does 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 N to continue; the cache will skip already-synthesized sentences).
  • prepare.py reports ASR mismatch: This means asr.json and the script don't match, usually because the script was edited without re-synthesizing voice. Re-run build_voice.py; don't use --allow-asr-mismatch to paper over it.
  • render.py reports subtitle too long: A caption exceeded 16 characters. Go back to build_segments.py and adjust the split rules.
  • check_overlap.py exits 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

Windows (win32) Platform Notes

Equivalent practices on Windows:

  • Virtual environment: py -3 -m venv .venv, then use .venv\Scripts\python.exe instead 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 in scripts/deliver.py, scripts/make_preview.py, and scripts/verify.py are hardcoded macOS absolute paths — change them to ffmpeg / ffprobe for cross-platform compatibility.
  • scripts/build_voice.py depends on mcode-tools being on the PATH, and curl. Windows 10 1803+ includes curl; if missing, replace with Invoke-WebRequest -OutFile <path> <url>.
  • Path separators: All scripts use pathlib for path handling. When passing arguments, quote the paths.

Related Skills