Communitygithub.com

Handmade Stage Film

Turn a brand's own campaign stills into a 15–20s stop-motion stage film, with paper puppets, torn-paper captions, and a last card on the product.

What is Handmade Stage Film?

Extends the real photos to 9:16, animates a few stiff movements, cuts props out of the photos themselves, and renders stepped motion, a music-box waltz, and the end card locally. Faces, products, and printed labels stay as shot. A seven-still film is about a couple of dollars in model calls; the cut is free on the machine.

Works with✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/DarthManly/handmade-stage-film-skill/tree/HEAD

Ask in your favorite AI

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

Documentation

What does Handmade Stage Film do?

Halloween night, nothing on: a girl with a black bob sits inside a painted cardboard sofa, pink drink in hand, and clicks the cardboard remote. The cardboard TV switches on into static, a little ghost dances on the screen, then peels off it and floats away. It hovers by her cup ("It wanted a sip."), lands on the checkered table where a giant fork stabs at a plate of painted berries ("Gardenia. Berries. Yogurt."), and one sip later she's sitting on a giant witch's hat while stars pop on around her ("…full magic."). The cup gets its close-up with a scribbled "Magic Pretty Water" and an arrow, and the last card has the wordmark, the cup, the ghost, "Halloween limited" and where it's on sale. Every frame comes from the brand's own seven campaign photos.

It's made in four parts:

  • scripts/generate.mjs turns stills into the pieces: 9:16 keyframes (Nano Banana 2 paints the extra set, the original pixels go back on top), Kling clips, cut-out props, re-laid labels and the wordmark.
  • scripts/reel.mjs cuts the clips into one reel on the film's clock.
  • scripts/sfx.mjs synthesizes the sound: an original music-box waltz and a sound on every event.
  • scripts/render.mjs plays it all back as a stage show: stepped motion at 12 frames a second, hand-held jitter, moving grain, puppets, torn-paper captions, tags with arrows, a TV screen that switches on, and the last card. It renders locally with headless Chrome and FFmpeg.

Setup (once per machine)

Run inside this Skill's folder:

npm install

This installs puppeteer-core and ffmpeg-static. Google Chrome must be installed, or set CHROME_PATH to a Chrome/Chromium executable (npx @puppeteer/browsers install chrome-headless-shell@stable prints one).

The footage needs an API key for Nano Banana 2 and Kling 2.5 Turbo; the scripts call them through a model API gateway, where the key is created (https://fal.ai/dashboard/keys). The user sets it in their own terminal as FAL_KEY (for example in their shell profile). Never ask them to paste the key into the chat, and never print it.

Cost

Model usage, billed to your own API key: $0.08 for each still extended to 9:16 (and each edit), $0.35 for each 5-second clip, and a fraction of a cent for each cut-out (October 2026 prices). A film of seven stills with five clips is about $2.30. The reel, the puppets, the captions, the sound and the final MP4 are made free on your machine. Tell the user the cost before generating.

What makes it work

  • Their own photos, untouched where it matters. The extension and every clip start from the brand's real stills; extend lays the original pixels back over the AI's version, and relabel lays a sharp label back over any frame where the clip smudged it.
  • A story, not a slideshow. Plan a little plot across the shots, with one thing that travels between them (here a ghost puppet that escapes from a TV). Captions are short story beats, not slogans.
  • Clumsy on purpose. Everything moves in steps of 1/12 s with a tiny shake, puppets wobble on their keys, captions pop on in three steps and arrows draw in three strokes. Don't smooth it.
  • Half product, half brand. The product is in most shots and gets one close-up with a tag; the last card is the wordmark, the product, the name and where to buy it.

Workflow

  1. Collect 4–8 stills from one shoot (3:4, 4:5 or 9:16), the product name, a line (flavours, materials), a note ("Halloween limited") and the CTA. Ask whether the stills carry a printed wordmark or badge at the bottom (it is painted out of the frames and keyed out for the last card).
  2. Script (show it before generating): the order of the shots, the plot, what moves in each clip (one or two small actions, the product kept still), which props to cut out as puppets and what they do, the captions and the tags, and the cost. Keep it to 15–20 s.
  3. Keyframes: node scripts/generate.mjs extend --in still.jpg --out keys/<shot>.png --cover <rows> for each still that isn't 9:16 (--cover = the height in pixels of the printed wordmark band at the bottom of the original, if any). Look at each one at full size: if the model left a mirrored or doubled object in the new bands, cover it with plain texture from the same photo.
  4. Trial: one clip, the shot with the plot's hook: node scripts/generate.mjs clip --start keys/<shot>.png --prompt "…" --out clips/<shot>.mp4. The prompt names only what moves ("she presses one button on the remote with her thumb; the cup stays upright and still"); a fixed suffix keeps the camera locked and the set, face and product unchanged. Show it, then do the rest.
  5. Clips for the other moving shots. Watch each one frame by frame: note where the action lands (to put the sound and puppets on it) and where small text on the product smudges. Fix labels with relabel --clip … --key keys/<shot>.png --box x,y,w,h (the label's box in the keyframe), or use only the frames before the product moves. A clip is not needed for every shot: a close-up still with a slow push-in (still, box, boxTo) is often better for the product.
  6. Props: cutout --in keys/<shot>.png --box x,y,w,h --out props/<name>.png for each puppet (a character, stars, a flower, a berry, the product in a hand); edit for a plate with something removed (a TV screen without its ghost). The wordmark: wordmark --in still.jpg --box x,y,w,h --out props/wordmark.png. The last card's background is a crop of a painted wall from one of the keyframes.
  7. Build effect.json from examples/example.json (see Shots and the last card), then node scripts/reel.mjs effect.json --out reel.mp4 and set "reel": "reel.mp4".
  8. Sound: node scripts/sfx.mjs effect.json --out stage.m4a, then "music": "stage.m4a". Run it again after any change to the timing. --no-bed --music track.mp3 swaps the waltz for a track the user is licensed to use.
  9. Validate: node scripts/validate.mjs effect.json — fix every error and read the warnings to the user.
  10. Draft: node scripts/render.mjs effect.json --quality draft --width 540 --out draft.mp4, and check a frame from every shot: no caption, tag or puppet on a face or the product, everything out of the right-hand 14% where the platform's buttons sit.
  11. Final: node scripts/render.mjs effect.json --out film.mp4 --poster poster.png. For Xiaohongshu, crop a 3:4 version: ffmpeg -i film.mp4 -vf crop=720:960:0:160 -c:a copy film-3x4.mp4 (at the default 720 width; check nothing important sits in the top or bottom 160 px).

Shots

All positions are in 1080 × 1920 frame pixels. Each shot is one of:

  • A clip shot: clip (the file reel.mjs cuts from), from and to (seconds in the clip), speed (0.5 = half speed, which with the 12-step playback uses every frame of a short good stretch), hold (seconds the first frame holds before it moves).
  • A still shot: still (an image), len, box and boxTo ([x, y, width] 9:16 windows in the image's pixels; the view moves from one to the other).

Every shot can have:

  • captions: { "text", "at", "out", "x", "y", "rot", "size" } — torn paper scraps with hand-written words; \n breaks a line.
  • tags: { "text", "at", "x", "y", "to": [x, y], "ink": "dark" | "light", "rot", "size" } — hand-written words with an arrow that draws itself to to.
  • puppets: { "image", "path": [[t, x, y, width, rotation], …], "sound", "exit", "flip", "wobble" } — a cut-out moved between its keys (t in seconds into the shot, x and y its centre). A key with width 0 then a full width one step later is a pop. It holds its last key unless "exit": true. sound plays at its first key.
  • screen: { "rect": [x, y, w, h], "off", "static", "plate", "plateAt" } — a TV or phone screen that is black until off, static until static, and shows plate (the same keyframe with the screen emptied) from plateAt, so a puppet can climb out of it.
  • sounds: [[t, "kind"], …] — click, tvon, static, ghost, boing, tap, pop, whoosh, land, stab, sip, sparkle, chime, paper, scribble, cut. Cuts, captions, tags, screens and puppets with a sound get theirs automatically.

The last card

end: len, bg (a 9:16 painted wall), wordmark (wordmarkY, wordmarkWidth), name (nameY, nameSize), line (lineY), note on a paper scrap (noteX, noteY), cta on a pill (ctaY), and puppets like a shot's (the product in a hand sliding in from the edge, the character, stars). Each has an …At time.

Fields

FieldRequiredNotes
reelyespath to a video file, relative to the JSON file — every clip shot cut into one silent 9:16 reel by scripts/reel.mjs (reel.mp4)
shotsyeslist of 2–10 entries: clip, from, to, speed, hold, still, len — the film in order: clip shots (an AI clip of one of your stills) and still shots (a still with a slow move); see SKILL.md, Shots
end.bgnopath to an image, relative to the JSON file — the last card's background: a painted wall or backdrop from your photos, 9:16
end.wordmarknopath to an image, relative to the JSON file — your wordmark on a transparent background (scripts/generate.mjs wordmark)
end.namenotext, under 4 words — the product name, hand-lettered on the last card
end.linenotext, under 6 words — a short line under the name, for example the flavours
end.notenotext, under 4 words — on a torn paper scrap, for example "Halloween limited"
end.ctanotext, under 12 words
musicnopath to an audio file, relative to the JSON file — the soundtrack from scripts/sfx.mjs (stage.m4a)
palettenocolours as #RRGGBB; keys: ink (captions and dark tags), paper (the paper scraps and light tags) and accent (the CTA pill)

Tips

  • Write the clip prompts tight: one or two small actions, a deadpan face, "the cup stays upright and still, label facing the camera". Big gestures smudge faces and labels.
  • Kling picks its own timing: watch where the action lands and put the click, the cut and the puppets on that frame, not on a round number.
  • Give the story one traveller: a puppet cut from one photo that turns up in the next shots ties seven stills into one film.
  • Cut puppets from the photos themselves, not new art: the same paint, paper and light make them belong.
  • Keep captions to three or four words; the pictures tell most of it.

Output

H.264 MP4, 1080 × 1920, 30 fps (the picture moves in 12 steps a second), 15–20 seconds with the sound, plus a poster frame (PNG).

Related Skills