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.mjsturns 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.mjscuts the clips into one reel on the film's clock.scripts/sfx.mjssynthesizes the sound: an original music-box waltz and a sound on every event.scripts/render.mjsplays 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;
extendlays the original pixels back over the AI's version, andrelabellays 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
- 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).
- 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.
- 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. - 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. - 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. - Props:
cutout --in keys/<shot>.png --box x,y,w,h --out props/<name>.pngfor each puppet (a character, stars, a flower, a berry, the product in a hand);editfor 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. - Build
effect.jsonfromexamples/example.json(see Shots and the last card), thennode scripts/reel.mjs effect.json --out reel.mp4and set"reel": "reel.mp4". - 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.mp3swaps the waltz for a track the user is licensed to use. - Validate:
node scripts/validate.mjs effect.json— fix every error and read the warnings to the user. - 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. - 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),fromandto(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,boxandboxTo([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;\nbreaks a line.tags:{ "text", "at", "x", "y", "to": [x, y], "ink": "dark" | "light", "rot", "size" }— hand-written words with an arrow that draws itself toto.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.soundplays at its first key.screen:{ "rect": [x, y, w, h], "off", "static", "plate", "plateAt" }— a TV or phone screen that is black untiloff, static untilstatic, and showsplate(the same keyframe with the screen emptied) fromplateAt, 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 asoundget 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
| Field | Required | Notes |
|---|---|---|
reel | yes | path 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) |
shots | yes | list 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.bg | no | path to an image, relative to the JSON file — the last card's background: a painted wall or backdrop from your photos, 9:16 |
end.wordmark | no | path to an image, relative to the JSON file — your wordmark on a transparent background (scripts/generate.mjs wordmark) |
end.name | no | text, under 4 words — the product name, hand-lettered on the last card |
end.line | no | text, under 6 words — a short line under the name, for example the flavours |
end.note | no | text, under 4 words — on a torn paper scrap, for example "Halloween limited" |
end.cta | no | text, under 12 words |
music | no | path to an audio file, relative to the JSON file — the soundtrack from scripts/sfx.mjs (stage.m4a) |
palette | no | colours 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).