Toon Studio
A single-file Python pipeline (studio.py, stdlib + elevenlabs SDK only) that
produces a mixed MP4 cartoon film from a JSON script. Runs on Windows, macOS and
Linux. No GPU, no accounts beyond the API keys the user supplies.
Setup (do this once per machine)
- Python 3.10+.
pip install elevenlabs imageio-ffmpeg(ffmpeg binary ships with imageio-ffmpeg; override with theFFMPEGenv var if desired).- Copy
.env.exampleto.envand fill in:KIE_API_KEY(required) - from https://kie.ai - covers image, video and music generation.ELEVENLABS_API_KEY(recommended) - from https://elevenlabs.io - direct voice generation. KIE also resells ElevenLabs TTS, but that endpoint has recurring outages; the default story routes voice to the direct backend.- Model picks default to the budget/quality sweet spot measured 2026-11:
qwen3/pro-text-to-image(stills, ~6.4 credits),minimax-h3/image-to-video(clips, ~48 credits / 6 s), SunoV4_5(music bed). Any KIE model id can be substituted viaKIE_IMAGE_MODEL/KIE_VIDEO_MODEL/KIE_MUSIC_MODEL.
Workflow
- Script. Write a
story.json(seeexamples/moodcast-eva/story.jsonand the header docstring instudio.py): title, style, characters (name, design, voice description, optionalphototo cartoonize a real person, optional ElevenLabsvoice_id), scenes (description, motion, duration 4-15 s, dialogue lines with speaker), music_prompt. Or draft one with an LLM:python studio.py new "a kitten knight finds a golden key" --scenes 3 --out story.json - Build.
python studio.py build story.json --outdir my-filmStages: character reference -> storyboard stills -> animation -> voiceover -> music + mix. Every stage is cached; re-running only generates what's missing. - Direct (optional, recommended). Add
--directorto pause after the storyboard stills: view each image, then accept, regenerate with a note (r wider shot), or swap in your own image (s path.png) before animation credits are spent. In an agent without interactive stdin, use the folder protocol instead:build --stop-after stills, review/overwritesceneN.pngin the outdir (orpython studio.py regen story.json --outdir DIR <scene#> "director's note"), then re-run the same build. - Producer (optional). Add
--producerfor a post-assembly fix loop:r 2re-renders scene 2's clip,v 2 1re-voices line 1 of scene 2,m 0.2re-mixes with a louder music bed,aaccepts. - Check first (no credits).
python studio.py check story.jsonvalidates the schema, reports speech fit per scene (estimated speech seconds vs. the scene window), and quotes the KIE credit cost of the missing stages plus the current balance - approve before spending. - Approve the cast (cheap).
python studio.py preview story.jsonrenders the first line of each distinct voice topreview_<speaker>.mp3so the user can approve voices before any video credits are spent.
Every build also writes <film>.srt next to the MP4 - a caption sidecar
with each line's exact placement on the film timeline - and burns the
captions into the video by default (white with dark outline, bottom third;
disable with build --no-captions, or restyle via CAPTION_STYLE in
studio.py).
Hard-won model facts (don't re-derive)
- KIE task API:
POST {KIE_BASE_URL}/api/v1/jobs/createTaskwith{"model", "input": {...}}, pollGET /api/v1/jobs/recordInfo?taskId=...; success =successFlag == 1; result atdata.response.resultUrls[0]. Upload local files tohttps://kieai.redpandaai.co/api/file-base64-upload(JSON base64), NOT to api.kie.ai. - Reference images: nano-banana family reads
image_input(a list of URLs); the fieldimage_urlsis silently ignored and produces off-brief characters. - MiniMax H3 (
minimax-h3/image-to-video): input takesfirst_frame_url, integer duration 4-15, resolution768Por2K. It natively generates its OWN soundtrack - often full music with vocals - which the studio silences by default (AMB_VOL = 0.0in studio.py). Raise it only for renders you have verified are clean sound effects. - Hailuo (
hailuo/2-3-image-to-video-pro):image_url, string duration ("6"/"10"), resolution768P/1080P. - Seedance (
bytedance/seedance-2-fast):first_frame_url, integer duration,generate_audio: false. - Suno music via KIE:
POST /api/v1/generate(a placeholdercallBackUrlis required; pollGET /api/v1/generate/record-info?taskId=...forstatus == SUCCESS, audio atresponse.sunoData[0].audioUrl). - All motion prompts carry two hard rules (constants
NO_SPEAK,AUDIO_FXin studio.py): characters never move their lips (off-screen dialogue otherwise reads as broken lip-sync) and no music/voices in clip soundtracks.
Backends
story.json "backends" routes each primitive independently:
{"image": "kie", "video": "kie", "voice": "elevenlabs", "music": "kie"}.
Available: kie (image/video/music/voice), elevenlabs (voice only, direct
API), higgsfield (alternative CLI backend). Default for everything is
higgsfield unless TOON_BACKEND or story backend says otherwise.
Cost transparency (measured on KIE, 2026-11)
| Model | Cost | Notes |
|---|---|---|
| qwen3/pro-text-to-image | ~6.4 credits/img | 1344x768, refs work, ~60 s |
| z-image | ~0.8 credits/img | cheaper, lower fidelity |
| nano-banana-pro | ~18 credits/img | premium quality |
| minimax-h3/image-to-video | ~48-56 credits / 6 s | fast (~90 s), makes own audio |
| hailuo/2-3 i2v pro | ~45 credits / 6 s | ~130 s |
| seedance-2-fast | ~148.8 credits / 6 s | premium motion |
| Suno V4_5 | ~24 credits/song | full song, mix trims it |
ElevenLabs voices: plan characters (e.g. eleven_v4 ~ a few hundred chars/line).
Check GET /api/v1/chat/credit for the KIE balance; the build prints credits
left after every run.
Troubleshooting
- Characters look wrong / ignore the reference: check the reference actually
uploaded (log shows the task id); nano-banana needs
image_input, notimage_urls. - Extra music or voices appear in the film: clip-level soundtrack from H3 or
similar - keep
AMB_VOL = 0.0, or re-render the clip (it is nondeterministic). - Lip-flap over narration: the NO_SPEAK rule usually holds; if one clip
slips through, delete that
clipN.mp4and re-run the build (a fresh render usually complies), or re-render with--producerand a note. - KIE voice tasks fail with 500: route
voicetoelevenlabs(direct). - A generation failed mid-build: just re-run the same command; finished stages are reused, only the missing asset regenerates.