This makes a short vertical reel (1080x1920, 30 fps) in a paper-doll stop-motion style. Each "look" is built from separate cut-out pieces: a head, a top, the bottoms, one shoe used twice, and optional arms and props. Each look gets a cream torn-paper outline and a soft shadow. Poses swap 6 times a second, so it reads as hand-made stop motion, not a smooth AI edit.
The two scripts do the heavy lifting. Run them, don't rewrite them:
scripts/cutout.pycuts any piece out of a photo or screenshot.scripts/render.pybuilds every doll from alooks.jsonand renders the reel.
Both need Python 3 with pillow numpy scipy rembg, plus ffmpeg. On a Mac, plain pip install is often blocked ("externally-managed-environment"), so set up a virtual environment once and run the scripts with its Python:
python3 -m venv ~/.paper-doll-venv
~/.paper-doll-venv/bin/pip install pillow numpy scipy "rembg[cpu]"
Run the scripts as ~/.paper-doll-venv/bin/python scripts/.... If ffmpeg is missing, install it with brew install ffmpeg. rembg downloads its model (about 170 MB) the first time it runs, so the first cut-out is slow. After that, a cut-out takes about 3 to 25 seconds.
Fonts. render.py needs real font files: a handwriting font, a bold font and a mono font. On a Mac, the handwriting font is already there (/System/Library/Fonts/Supplemental/ChalkboardSE.ttc, index 2). For the other two, use the person's brand fonts. If they have none, download Urbanist and Space Mono from Google Fonts and point fonts.bold and fonts.mono at the .ttf files.
Workflow
1. Collect the pieces
Ask for these, or find them in the files the person mentions:
- Heads. 1 to 3 cut-out photos of the person's head with different expressions (laugh, kiss, ooh, smile). Using a different expression per look makes the reel feel alive.
- Outfits. Flat-lay screenshots, one per look. A single photo that shows a whole outfit is fine, because you crop each item out of it.
- Optional extras:
- Props, such as a mic, laptop, phone, bag or crayon.
- A crayon or pen photo, which does the writing.
- A vehicle for the ending.
- A folder of their own post screenshots, which scroll behind one look as their "feed".
- A full-body photo or sticker, either to use whole for a look or to borrow its arms from.
2. Cut every piece out
Run python scripts/cutout.py <in> <out> [--box x0,y0,x1,y1] [--poly "..."] [--keep N] [--mode border].
- Speed: by default, small screenshots are upscaled to about 1200px on the long side before matting, and big photos are matted as they are. Don't force
--upscale 3on big photos, because it takes minutes. - Flat-lays: crop each item with
--boxin the source's pixel coordinates. Look at the image first and read the coordinates off it. Draw a labelled grid over it if that helps. - Shoes: cut out only the FRONT shoe of a pair. Use
--polyaround it, in the cut piece's pixel coordinates. The renderer places that one shoe twice, once under each leg. A photographed pair placed whole looks stuck together and can't step. - Heads from a normal photo: use
--boxaround the whole head with the shoulders in it, then--polyto cut off just below the chin. Keep the full width of the hair and cheeks down to the jaw, and only angle the polygon in under the chin and neck. A tight polygon leaves straight cuts across the cheeks. Check for leftover collar or top under the chin, and setchinto about 0.85 for these cuts. Hair worn down looks far better than hair pulled back. - Heads that already have a paper outline: use
--mode border. It removes only the flat background touching the image edges, so the outline survives. - Arms from a body photo: remove the top they're wearing, then keep the two largest skin pieces (
--keep 2), or mask by skin colour. Thin slivers of the old top left beside the hands are a common glitch, so check for them. - After every cut, check it on a coloured background. Look for leftovers like hangers, a second shoe's toe, a fake checkerboard or white halos. Re-cut until it's clean. Never fake transparency with a multiply blend.
3. Write looks.json next to the pieces
Paths in the file are relative to looks.json. The colours shown are an example brand palette, so swap in the person's own.
{
"out": "lookbook.mp4",
"head_px": 480,
"colors": {"bg": "#ED84B0", "burst": "#EFA6C2", "ink": "#C4245C", "text": "#FFFFFF", "accent": "#EFA6C2"},
"fonts": {"hand": "/System/Library/Fonts/Supplemental/ChalkboardSE.ttc", "hand_index": 2,
"bold": "fonts/Urbanist-900.ttf", "mono": "fonts/SpaceMono-Regular.ttf"},
"pen": "parts/crayon.png",
"hook": ["spend the day", "with me"],
"looks": [
{"label": "filming", "time": "9am", "feed": "feed",
"heads": ["parts/head-laugh.png", "parts/head-kiss.png"],
"top": "parts/jumper.png", "top_w": 1.0, "bottoms": "parts/jeans.png", "bottoms_w": 0.52,
"shoe": "parts/shoe.png", "shoe_faces": "right", "shoe_w": 0.4,
"hand_left": "parts/mic.png", "hand_right": "parts/laptop.png"},
{"label": "building", "time": "12pm", "body": "parts/full-body.png", "prop": "parts/phone.png", "prop_w": 0.4},
{"label": "mum mode", "time": "3pm", "heads": ["parts/head-smile.png"], "chin": 0.70,
"top": "parts/tee.png", "top_w": 0.57, "waist": 0.66, "bottoms": "parts/wide-jeans.png", "bottoms_w": 0.41,
"shoe": "parts/boot.png", "shoe_w": 0.27, "shoe_ankle": 0.7,
"arms": "parts/arms.png", "arms_w": 1.3, "arms_y": 0.3, "arms_out": 0.03, "prop": "parts/bag.png", "prop_w": 0.6}
],
"cta": ["want to learn", "how to make this?", "comment", "paper"],
"ending": {"image": "parts/car.png", "faces": "left", "line": "get in"}
}
| Field | What it does |
|---|---|
head_px |
On-screen head width, the same for every look. Bigger heads make bigger dolls. Lower it if a head touches the label. |
*_w |
Each piece's size as a multiple of the head width. Tune these until the top is in proportion to the body. |
chin |
How far down the head image the chin sits. Use 0.6 for tight cut-outs and 0.7 for stickers with long hair. |
waist |
How far down the top the bottoms start (default 0.75). The top overlaps the waistband. |
shoe_faces |
Which way the toe points in the cut shoe. The renderer flips it so both shoes point left. |
shoe_ankle |
Where the ankle sits across the shoe, from 0 to 1. Adjust it until the ankle sits under the leg. |
shoe_spread |
Pushes the two shoes apart, so they never stack when the legs swing in. |
walk / stride |
Trousers are split at the crotch automatically and the legs swing ±stride°. Set walk: false for a skirt or dress. |
body |
A whole full-body sticker used instead of separate parts. It wobbles rather than walks. |
arms, arms_w, arms_y, arms_out |
Two arms (one image) drawn BEHIND the top and nudged outward, so they hang from the sleeves. |
hand_left / hand_right |
Props held at the cuffs. |
prop |
A prop that bobs beside the doll. |
time |
When any look has a time, a day timeline runs across the top and a dot hops between the times. |
feed |
A folder of screenshots that scrolls as a 3-column grid behind that look, washed in the background colour. |
pen |
The crayon image, tip pointing RIGHT. It writes the hook and each label. Without it, the text still writes on. |
ending.faces |
Which way the vehicle faces. It always drives the way it faces: in from the right, out to the left. |
4. Check the stills, then render
- Run
python scripts/render.py looks.json --stills. It writeslook-N.pngfor each look. Look at them side by side and check:- Heads: all the same size.
- Top: in proportion to the body, not tiny and not huge.
- Shoes: one under each leg, toes the same way.
- Glitches: no slivers or halos.
- Arms: present when the top is sleeveless.
- Fix the cut-outs or the
*_wvalues, then check the stills again. - Run
python scripts/render.py looks.jsonfor the full render. It takes about a minute for 14 seconds of reel. - Pull 6 frames into a contact sheet and check them before handing over. Check against the "Taste rules" section below.
Taste rules (learned the hard way)
- Only the head is out of proportion. The body and clothes keep real human proportions: tee about shoulder width, torso about half a head-width tall, waist-to-floor about 0.85 head-width. A long top over long trousers turns the doll into a box. Tuck the top into the waistband (
waistaround 0.6) when the top is long, and hang the arms behind the top so they come out of the sleeves. - Big heads. The charm is a bubble head on a small paper body. Every look's head should be the same width on screen. Small heads read as "tiny on screen".
- Walking, not sliding. Trousers are split into two legs that swing round the hips, with one shoe under each leg. The front foot lifts a little. The waistband overlaps the leg tops, so no seam shows.
- Real crayon, kid handwriting. The crayon tip leads the writing letter by letter, then flies off. The letters wobble in size, angle and baseline. A neat font is wrong.
- Brand colours for everything designed. The background, starbursts, ink, timeline and CTA all use the brand palette. A dusty pink that's "nearly" the brand colour reads as off. Starbursts look best a lighter tint of the background.
- Graphics never sit over copy. The label, timeline and CTA stay clear of dolls, props and the vehicle. The feed fades out before it reaches the label.
- Copy. Put the hook and labels in the person's own words, lowercase for socials, and end on a CTA keyword. Check every line reads naturally out loud.
- Privacy. Before anything is posted, check the feed screenshots and photos for kids, house numbers, locations or open screens. ffmpeg strips the metadata from the render (
-map_metadata -1).
Adapting
- More or fewer looks: add or remove entries in
looks. The timeline spaces itself out. - Dropping beats: remove
hook,ctaorendingand that beat is skipped. - Another brand: change
colors, the fonts, and the pen and vehicle images. Nothing else is tied to one brand.