Flat-color animation
One drawing holds for 3–4 frames, and the performance comes from swapping drawings and mouth shapes. For simple TV cartoon designs, this is the route where image models stay most consistent. It needs neither 3D nor a video model.
The scripts are in this skill's scripts/ folder, with paths relative to this SKILL.md. Below, $S means that folder. Except for new_project.py, run them from the film folder. For environment problems, use dula-start.
Talk to the person in their own language.
Workflow
python $S/new_project.py my_film && cd my_film
1. Story and shots
Write story.md, then config/film.json (see the film.json reference). Keep a first film to 20–30 seconds, and a spoken line takes about 2–3 seconds, so 15 seconds holds 3–5 lines. Use original characters only.
For every line, think through who says it, to whom, and who the camera shows. Jokes shouldn't rely on words the audience won't understand. Cause and effect needs drawings too (runs in → grabs → the object flies off). Don't skip it with a cut.
2. Character designs (the most important step)
Put each character in config/anchors.tsv (name<TAB>appearance), then run:
python $S/paint.py anchors config/anchors.tsv
Open and look at every image yourself. Some viewers show a transparent area as a colored glow on black, which looks like a failed cutout. If in doubt, check build/check_alpha.png from step 3. Then show the designs to the person and get their approval before going on. Every later drawing follows these, so a mistake here costs the most to fix. Fix the proportions in words, such as "head one third of total height, round face, small black bead eyes, red overalls".
3. Backgrounds and drawings
Fill in config/plates.tsv (name<TAB>description) and config/cels.tsv (drawing<TAB>character<TAB>pose). Start drawing names with the character's name, like hero_run_a.
python $S/paint.py plates config/plates.tsv
python $S/paint.py cels config/cels.tsv
python $S/prep_cels.py --bases-only # Check cutouts in build/check_alpha.png before painting mouths
Images take 1–3 minutes each, and paint.py reports progress as it goes. If your tool stops long commands, run it in the background. A rerun skips finished images. To redo one, use --only <name> --force.
To keep drawings and places continuous, add one more column naming what to follow:
hero_run_a hero Running to the right, left foot forward.
hero_run_b hero Running to the right, right foot forward. hero_run_a
meadow A meadow under grey rain clouds.
meadow_sun The rain has stopped and the sun breaks through. meadow
Rules:
- Draw one character per image. The character design is always the first reference. Never use a rejected image as a reference.
- Decide which way characters face first. Images generated one at a time each pick a direction, so two people in a conversation easily end up back to back. Specify it up front, and use
flipto fix mistakes. - Each drawing in an action cycle follows the one before it (the extra column above), so direction, scale and shape stay put. Without this, a cycle pops.
- For a change in time of day, repaint the background, following the original plate. Multiplying a color can't hide a blue sky.
- Check a character's proportions in every shot. A pose description like "neck stretched up" can turn into a different body.
4. Mouths and blinks
Every drawing that talks gets local edits of the same image. In config/variants.tsv:
hero_stand_m1 hero_stand Change only the mouth to half open, small oval.
hero_stand_m2 hero_stand Change only the mouth to wide open.
hero_stand_blink hero_stand Close both eyes as simple curved lines. Change nothing else.
Names: <base>_m1 is half open, and <base>_m2 is wide open. With only _m1, the mouth has two states, open and closed, and that works too. The base drawing itself must have a closed mouth. If it's drawn with the mouth open, make a closed variant and set all three states with talk in film.json.
Every drawing of a talking character needs its own mouths, including each drawing in a walk cycle.
python $S/paint.py variants config/variants.tsv
python $S/prep_cels.py
prep_cels.py presses drawings into true flat color and locks each variant to its base. Then open build/check_alpha.png and check the cutouts on magenta. Redraw any image with leftover background, missing pieces or erased eyes. If it warns about a large changed area, the model edited the body. Add a window x,y,w,h as column 4 of variants.tsv.
Colors drifting? You can add config/palette.json ({"skin": [250, 220, 190]}), using colors sampled from the character design. Only close colors snap to it, and prep_cels.py applies it again after you change it.
5. Sound
Choose one:
- Generate voices with
dula-voice, using the person's own key. - Use existing audio: put it at
audio/voice/<line id>.wav, or setfileon the line. mp3 and wav both work. - Make a silent draft: give the line a
secondsvalue.
Mouths are calculated from this audio, so settle the voices before making the test.
Optional: ambience, bgm, sfx and songs. Use only material the person has the rights to. This project doesn't generate music. Without licensed music, leave the score out and let ambience and sound effects carry the film.
6. Test
python $S/build_film.py
python $S/render.py --stills 1,3,5,8,12
python $S/render.py --range 0,20 -o output/pilot.mp4
build_film.py stops with an error when a visible character talks but their mouth can't move, the easiest mistake to miss. Look at every still before showing the test. Don't wait until the whole film is done to show anyone.
Getting a character on the ground: guess ground, render a still, and adjust. It's faster than calculating it.
Don't let the shots sit still. Unless a beat is meant to be still, give each shot one thing that moves: a pan on the plate, a bob or shake, a change of drawing, or an overlay such as rain. End the film with fade_out on the last shot.
7. Full film and final check
python $S/render.py
python $S/check_mouths.py
Confirm each of these before handing over, and say plainly if you couldn't:
- You looked at a real frame from every shot
-
check_mouths.pyreports no frozen mouths - Each character still looks like the same person across shots (face and proportions matter more than color)
- People in a conversation face each other
- A single drawing moving across the screen has a
bobor more drawings, or it reads as sliding - You listened to it. If you can't listen, say the audio hasn't been checked by ear
A complete example
examples/rainy-slide in this repository has the prompts and shot list of a finished 15-second film, and docs/tutorial.md explains it. Find them at ../../examples/rainy-slide and ../../docs/tutorial.md from this file. Use them as a model when someone asks for one.
Pacing
Rules of thumb: shots average 3–5 seconds, characters hold each drawing for 3–4 frames (about 7 drawings a second), with frequent close-ups and exaggerated expressions. hold is the number of frames per drawing.
Common problems
| Symptom | Cause and fix |
|---|---|
| Body colors flicker when mouths swap | The variant didn't go through prep_cels.py |
| A walk or run pulses tall and short | Don't size drawings separately. One actor shares one scale automatically. Add align_head too |
| A prop takes on the wrong color | A palette entry is too close. Remove it or lower SNAP |
| The mouth stays open during speech | Two-state mouths use a 0.5 threshold. Check whether the audio is muffled |
| Quiet words stay closed | One loud word sets the scale for its line. Add a _m2, or lower mouth_open_at on the line |
| The rain or wind can't be heard | Ambience is quiet by default. Give it a level_db, such as -35 |
| Constant hiss | The ambience clip is just noise. Replace it. Gain is capped, so it won't be amplified further |
| Codex says the reference image was lost | Image generation must be its first action. paint.py handles this, so run it again |
Limits
This route isn't suited to complex fights, long continuous action or realistic painting.