Communitygithub.com

orange9angel/dula-flat-cel

Make flat-color line-art animated shorts with an image model and scripts, from story, character designs, drawings and mouth variants to a finished video with sound. Needs only Python and ffmpeg. Use for animated shorts, TV cartoon style, limited animation, dialogue films, keeping a character consistent across shots, or fixing mouths out of sync and flickering drawing swaps.

What is dula-flat-cel?

dula-flat-cel is a Claude Code agent skill that make flat-color line-art animated shorts with an image model and scripts, from story, character designs, drawings and mouth variants to a finished video with sound. Needs only Python and ffmpeg. Use for animated shorts, TV cartoon style, limited animation, dialogue films, keeping a character consistent across shots, or fixing mouths out of sync and flickering drawing swaps.

Works with✓Claude Code✓Codex CLI~Cursor
npx skills add https://github.com/orange9angel/dula-craft/tree/HEAD/skills/dula-flat-cel

Ask in your favorite AI

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

Documentation

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 flip to 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 set file on the line. mp3 and wav both work.
  • Make a silent draft: give the line a seconds value.

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.py reports 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 bob or 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

SymptomCause and fix
Body colors flicker when mouths swapThe variant didn't go through prep_cels.py
A walk or run pulses tall and shortDon't size drawings separately. One actor shares one scale automatically. Add align_head too
A prop takes on the wrong colorA palette entry is too close. Remove it or lower SNAP
The mouth stays open during speechTwo-state mouths use a 0.5 threshold. Check whether the audio is muffled
Quiet words stay closedOne 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 heardAmbience is quiet by default. Give it a level_db, such as -35
Constant hissThe ambience clip is just noise. Replace it. Gain is capped, so it won't be amplified further
Codex says the reference image was lostImage 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.

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

Related Skills