Render hand swipe flavor listicle skill

Render a 'hand-swipe flavor listicle' video from a config — the brand's REAL product cutouts each float on a flat flavor-matched color field under a persistent title and slide RIGHT-TO-LEFT one to the next like a phone-feed scroll, while a green-screen hand clip (back of hand toward camera, index finger up) is trimmed to its clean finger-up window, looped once per flavor, keyed with a green-dominance alpha key, scaled down and anchored at the bottom, with an optional end card and an optional instrumental music bed — deterministic PIL plus FFmpeg assembly, FREE (the hand clip comes from create-video-fal and the bed from create-music-elevenlabs), so labels stay pixel-crisp.

by gooseworks-ai·MIT license·★ 1,220 Stars on the repo·GitHub ↗

Use now

Files of Render hand swipe flavor listicle

gooseworks-ai/main1 file shown
SKILL.md
Show the full text166 lines

render-hand-swipe-flavor-listicle

Render the hand-swipe-flavor-listicle format from a config: a short vertical (1080×1920, 30fps, ~7.5s) "N flavors of <Brand>" listicle. Each flavor is a card — a flat, saturated color field + a persistent title + the brand's REAL product cutout with a soft contact shadow. Cards change by sliding right-to-left (a phone-feed scroll), while a real-looking hand sweeps at the bottom of the frame. Music-only, no VO.

This capability is the FREE, deterministic assembly ($0, no keys). It never generates or edits the product — the cutouts are composited as-is, so labels stay crisp. It needs four kinds of input, gathered first (see Getting the inputs): product cutouts, a title TTF, a green-screen hand clip, and (optionally) a music bed.

Run

S=/tmp/gooseworks-scripts/render-hand-swipe-flavor-listicle/scripts   # after `gooseworks fetch render-hand-swipe-flavor-listicle`
python3 $S/build_hand_swipe_listicle.py config.json                 # writes config "output"
python3 $S/build_hand_swipe_listicle.py config.json --out final.mp4 # override the output
python3 $S/build_hand_swipe_listicle.py config.json --stills cards/ # one PNG per card, no video
python3 $S/build_hand_swipe_listicle.py config.json --work work/    # keep the intermediates in work/

Relative paths in config.json resolve against the config file's own folder. Render --stills first: it is instant and shows the title, colors and cutouts before the full render.

Prereqs: python3 with Pillow, and ffmpeg + ffprobe on PATH. Nothing else.

Getting the inputs

Input Free route Paid route (gated: confirm with the user first)
Product cutouts The brand's transparent PNGs (PDP/press kit). A brand-site photo is usually NOT a cutout. Remove its background with create-image-fal, model fal-ai/birefnet/v2 (see below).
Title TTF Download a TTF matching choices.title_style (table below). —
Hand clip The bundled clip (one woman's hand): curl -fL -o hand-swipe-greenscreen.mp4 https://d2m0qbg34x1ii2.cloudfront.net/ads/samples/455dc125-e41c-46dc-9fa6-9ae125e9b6bf/media-hand-swipe-greenscreen.mp4 (also in the recipe's config.hand.bundled_clip_url). A new clip via create-video-fal (see Making the hand clip).
Music bed None ("music": null, a silent cut). create-music-elevenlabs (see Making the music bed).

Cutouts. The renderer warns when an image has no transparent pixels: it would render as a white box with a rectangular shadow. Fix it before rendering, never by AI-regenerating the product. The cutout route is create-image-fal with model fal-ai/birefnet/v2 (paid, gated), payload {"image_url": "<public url of the product photo>"}, the same route render-vignette uses. Known gap: birefnet returns image, not images[], and create-image-fal's gen_image.py currently reads only images[], so check that capability's notes before relying on it. If no cutout route works, ask the user for transparent PNGs.

Title font. Always pass title_font. Without it the renderer falls back to whatever the machine has (Brush Script on macOS, DejaVu Sans Bold on Linux), so the same config renders a different title style on different machines. Free TTFs from Google Fonts, one per title_style:

choices.title_style Download
cursive / script curl -fL -o fonts/title.ttf https://github.com/google/fonts/raw/main/ofl/pacifico/Pacifico-Regular.ttf
bold condensed sans curl -fL -o fonts/title.ttf https://github.com/google/fonts/raw/main/ofl/anton/Anton-Regular.ttf
elegant serif curl -fL -o fonts/title.ttf https://github.com/google/fonts/raw/main/ofl/dmserifdisplay/DMSerifDisplay-Regular.ttf
hand-drawn marker curl -fL -o fonts/title.ttf https://github.com/google/fonts/raw/main/apache/permanentmarker/PermanentMarker-Regular.ttf
the brand's own display font The brand's TTF/OTF file if you have it; otherwise the closest row above.

The title is auto-shrunk (with a warning) until it fits 88% of the frame width, and the underline follows the drawn text, so long brand names never clip on the edges.

Config

scripts/config.example.json is the worked example (Lucky Energy "5 Flavors of Lucky"). Copy its structure, never its creative values.

Field What it is
title The persistent header line (required).
title_font A TTF in the style from choices.title_style (see Title font). Missing → a warned, machine-dependent fallback.
title_size, title_y, title_color, title_underline Title layout (defaults 118, 200, white, true). title_size is a maximum: a too-wide title is shrunk to fit.
flavors[] { "bg": "#hex", "can": "cutout.png" } per flavor, in play order (required, ≥2). image is accepted as an alias for can. Cutouts must be transparent PNGs.
can_height, can_center_y Product size + vertical center (defaults 950, 940).
slide.frames Frames per slide (default 12). slide.direction is fixed: always right-to-left (any other value is ignored with a warning).
hand_clip The green-screen hand swipe mp4 (required to render).
hand trim_start, trim_end (the clean finger-up window, default 0–1s, must lie inside the clip), speed, scale (default 0.45), green_margin (default 1.12).
end_card, end_card_hold_frames Optional PNG at the frame size (1080×1920) that slides in after the last flavor; hold (default 60 frames). Another aspect ratio is stretched, with a warning.
music { "path", "trim_intro_sec", "volume" } or a plain path string, or null for a silent cut. path is the create-music-elevenlabs output file. volume is a gain after loudness normalization (1.0 = the demo level, -18 LUFS).
output Output mp4 (default hand-swipe-listicle.mp4 next to the config).

Hard errors (exit 1): an unfilled <input:…> / <brand:…> placeholder in a required field; a missing file; a music object whose path is empty or a placeholder (set "music": null if the cut is meant to be silent); a hand.trim_start at or past the clip's end.

Timing. Each flavor lasts one hand-loop (the trimmed clip length). With 5 flavors, a 1s trim, a 12-frame end-card slide and a 60-frame end-card hold, the cut is ~7.4s.

Music length. The bed must cover trim_intro_sec + the cut. If it is too short, the renderer trims less of the intro (with a warning) so the music still runs to the end and fades out there. If the file is shorter than the whole cut, the tail is silent (warned).

Choices

The recipe asks the user these before any paid step; the renderer only draws what the config says.

  • flavor_lineup — which SKUs and in what order → flavors[]. The demo used 5 energy-drink flavors, strongest last.
  • title_style — the header style → title_font (download per the table), title_size. The demo used a cursive script.
  • hand — whose hand → hand_clip: the bundled clip (free) or a new create-video-fal clip (paid). The demo used a woman's hand.
  • music — the bed's feel → the create-music-elevenlabs prompt, then music.path. The demo used an upbeat, punchy energy underscore.

Making the hand clip (paid, gated — not this capability)

Only when choices.hand asks for a different hand than the bundled clip, or no bundled-clip URL is available. Fill {hand} from choices.hand (e.g. "a man's hand with a silver watch"). Write the payload to a file so apostrophes in the description don't break the shell:

cat > hand-payload.json <<'JSON'
{
  "prompt": "Close-up of {hand}, the BACK of the hand facing the camera, index finger extended pointing up. The hand swipes smoothly from RIGHT to LEFT across the frame like scrolling to the next photo on a phone. One clean horizontal swipe, the index finger stays extended (do not curl it). Solid flat bright green screen background, even flat lighting, fixed camera, no zoom, no camera shake.",
  "negative_prompt": "curled finger, closed fist, vertical motion, static, frozen, morphing, deformed fingers, extra fingers, text, watermark, changing background",
  "duration": "5",
  "aspect_ratio": "9:16"
}
JSON
python3 /tmp/gooseworks-scripts/create-video-fal/scripts/gen_video.py \
  --model fal-ai/kling-video/v2.5-turbo/pro/text-to-video \
  --payload @hand-payload.json --out hand-swipe-greenscreen.mp4

Then look at the first ~1s (e.g. --stills-style frame grabs with ffmpeg, or watch) and set hand.trim_start / hand.trim_end to the clean finger-up window. Set hand_clip to the file.

Making the music bed (paid, gated — not this capability)

Generate it long enough to cover the intro trim plus the cut, plus 1s of slack: at least (trim_intro_sec + target_duration_sec + 1) seconds, i.e. about 11s for the default 2.5s trim and a 7.5s cut. Keep --trim-intro 0 there; this renderer does the trim.

python3 /tmp/gooseworks-scripts/create-music-elevenlabs/scripts/gen_music.py \
  --prompt '<from choices.music; instrumental only, no vocals, no artist names>' \
  --length-ms 11000 --duration 11 --out music-bed.m4a

Then set music.path to music-bed.m4a.

Craft rules (baked into the renderer)

  • Never AI-regenerate the product. Image-to-video hallucinates the can and garbles the label. Only the hand is generative; the products are real cutouts.
  • Green-dominance key, not colorkey/chromakey. Alpha is 0 only where g > green_margin × max(r, b). It is brightness-invariant, so it survives a muddy, uneven AI green. Kept pixels are despilled (green clamped to max(r, b)), so green spill on skin turns neutral instead of see-through. Raise green_margin if skin goes see-through; lower it if green fringe remains.
  • Back of hand toward camera, finger UP. A nails-forward hand reads as the wrong hand.
  • Trim to the finger-UP window. These clips curl the finger periodically.
  • Scale the hand down and anchor it at the bottom so the fingertip reaches ~35% up (the can base) and never covers the product.
  • Flavors change by sliding RIGHT-TO-LEFT, never a vertical flick or a top-drop.
  • The hand is scaled to the frame width with its aspect kept, so a non-9:16 clip is not stretched.

Output

A WxH (default 1080×1920) H.264 mp4, with AAC audio when music is set (fade in/out + loudnorm, padded to the full length). The last line names the length and music/silent; read the WARNING lines above it. Then review the FULL mp4 with watch: the hand is opaque in every frame with no green fringe, the finger stays up, the title fits the frame, labels are crisp and readable, the slide is right-to-left, and the music runs to the end.

1---
2name: render-hand-swipe-flavor-listicle
3description: Render a 'hand-swipe flavor listicle' video from a config — the brand's REAL product cutouts each float on a flat flavor-matched color field under a persistent title and slide RIGHT-TO-LEFT one to the next like a phone-feed scroll, while a green-screen hand clip (back of hand toward camera, index finger up) is trimmed to its clean finger-up window, looped once per flavor, keyed with a green-dominance alpha key, scaled down and anchored at the bottom, with an optional end card and an optional instrumental music bed — deterministic PIL plus FFmpeg assembly, FREE (the hand clip comes from create-video-fal and the bed from create-music-elevenlabs), so labels stay pixel-crisp. Use for the hand-swipe-flavor-listicle format.
4status: active
5---
6 
7# render-hand-swipe-flavor-listicle
8 
9Render the **hand-swipe-flavor-listicle** format from a config: a short vertical
10(1080×1920, 30fps, ~7.5s) "N flavors of <Brand>" listicle. Each flavor is a card — a flat,
11saturated color field + a persistent title + the brand's REAL product cutout with a soft
12contact shadow. Cards change by sliding **right-to-left** (a phone-feed scroll), while a
13real-looking hand sweeps at the bottom of the frame. Music-only, no VO.
14 
15This capability is the **FREE, deterministic assembly** ($0, no keys). It never generates
16or edits the product — the cutouts are composited as-is, so labels stay crisp. It needs
17four kinds of input, gathered first (see **Getting the inputs**): product cutouts, a title
18TTF, a green-screen hand clip, and (optionally) a music bed.
19 
20## Run
21 
22```bash
23S=/tmp/gooseworks-scripts/render-hand-swipe-flavor-listicle/scripts # after `gooseworks fetch render-hand-swipe-flavor-listicle`
24python3 $S/build_hand_swipe_listicle.py config.json # writes config "output"
25python3 $S/build_hand_swipe_listicle.py config.json --out final.mp4 # override the output
26python3 $S/build_hand_swipe_listicle.py config.json --stills cards/ # one PNG per card, no video
27python3 $S/build_hand_swipe_listicle.py config.json --work work/ # keep the intermediates in work/
28```
29 
30Relative paths in `config.json` resolve against the config file's own folder.
31Render `--stills` first: it is instant and shows the title, colors and cutouts before the full render.
32 
33**Prereqs:** `python3` with Pillow, and `ffmpeg` + `ffprobe` on PATH. Nothing else.
34 
35## Getting the inputs
36 
37| Input | Free route | Paid route (gated: confirm with the user first) |
38|---|---|---|
39| Product cutouts | The brand's transparent PNGs (PDP/press kit). | A brand-site photo is usually NOT a cutout. Remove its background with `create-image-fal`, model `fal-ai/birefnet/v2` (see below). |
40| Title TTF | Download a TTF matching `choices.title_style` (table below). | — |
41| Hand clip | The bundled clip (one woman's hand): `curl -fL -o hand-swipe-greenscreen.mp4 https://d2m0qbg34x1ii2.cloudfront.net/ads/samples/455dc125-e41c-46dc-9fa6-9ae125e9b6bf/media-hand-swipe-greenscreen.mp4` (also in the recipe's `config.hand.bundled_clip_url`). | A new clip via `create-video-fal` (see **Making the hand clip**). |
42| Music bed | None (`"music": null`, a silent cut). | `create-music-elevenlabs` (see **Making the music bed**). |
43 
44**Cutouts.** The renderer warns when an image has no transparent pixels: it would render as
45a white box with a rectangular shadow. Fix it before rendering, never by AI-regenerating the
46product. The cutout route is `create-image-fal` with model `fal-ai/birefnet/v2` (paid, gated),
47payload `{"image_url": "<public url of the product photo>"}`, the same route `render-vignette`
48uses. **Known gap:** birefnet returns `image`, not `images[]`, and `create-image-fal`'s
49`gen_image.py` currently reads only `images[]`, so check that capability's notes before
50relying on it. If no cutout route works, ask the user for transparent PNGs.
51 
52**Title font.** Always pass `title_font`. Without it the renderer falls back to whatever the
53machine has (Brush Script on macOS, DejaVu Sans Bold on Linux), so the same config renders a
54different title style on different machines. Free TTFs from Google Fonts, one per `title_style`:
55 
56| `choices.title_style` | Download |
57|---|---|
58| cursive / script | `curl -fL -o fonts/title.ttf https://github.com/google/fonts/raw/main/ofl/pacifico/Pacifico-Regular.ttf` |
59| bold condensed sans | `curl -fL -o fonts/title.ttf https://github.com/google/fonts/raw/main/ofl/anton/Anton-Regular.ttf` |
60| elegant serif | `curl -fL -o fonts/title.ttf https://github.com/google/fonts/raw/main/ofl/dmserifdisplay/DMSerifDisplay-Regular.ttf` |
61| hand-drawn marker | `curl -fL -o fonts/title.ttf https://github.com/google/fonts/raw/main/apache/permanentmarker/PermanentMarker-Regular.ttf` |
62| the brand's own display font | The brand's TTF/OTF file if you have it; otherwise the closest row above. |
63 
64The title is auto-shrunk (with a warning) until it fits 88% of the frame width, and the
65underline follows the drawn text, so long brand names never clip on the edges.
66 
67## Config
68 
69`scripts/config.example.json` is the worked example (Lucky Energy "5 Flavors of Lucky").
70Copy its structure, never its creative values.
71 
72| Field | What it is |
73|---|---|
74| `title` | The persistent header line (required). |
75| `title_font` | A TTF in the style from `choices.title_style` (see **Title font**). Missing → a warned, machine-dependent fallback. |
76| `title_size`, `title_y`, `title_color`, `title_underline` | Title layout (defaults 118, 200, white, true). `title_size` is a maximum: a too-wide title is shrunk to fit. |
77| `flavors[]` | `{ "bg": "#hex", "can": "cutout.png" }` per flavor, in play order (required, ≥2). `image` is accepted as an alias for `can`. Cutouts must be transparent PNGs. |
78| `can_height`, `can_center_y` | Product size + vertical center (defaults 950, 940). |
79| `slide.frames` | Frames per slide (default 12). `slide.direction` is fixed: always right-to-left (any other value is ignored with a warning). |
80| `hand_clip` | The green-screen hand swipe mp4 (required to render). |
81| `hand` | `trim_start`, `trim_end` (the clean finger-up window, default 0–1s, must lie inside the clip), `speed`, `scale` (default 0.45), `green_margin` (default 1.12). |
82| `end_card`, `end_card_hold_frames` | Optional PNG at the frame size (1080×1920) that slides in after the last flavor; hold (default 60 frames). Another aspect ratio is stretched, with a warning. |
83| `music` | `{ "path", "trim_intro_sec", "volume" }` or a plain path string, or `null` for a silent cut. `path` is the `create-music-elevenlabs` output file. `volume` is a gain after loudness normalization (1.0 = the demo level, -18 LUFS). |
84| `output` | Output mp4 (default `hand-swipe-listicle.mp4` next to the config). |
85 
86**Hard errors (exit 1):** an unfilled `<input:…>` / `<brand:…>` placeholder in a required
87field; a missing file; a `music` object whose `path` is empty or a placeholder (set
88`"music": null` if the cut is meant to be silent); a `hand.trim_start` at or past the clip's end.
89 
90**Timing.** Each flavor lasts one hand-loop (the trimmed clip length). With 5 flavors, a
911s trim, a 12-frame end-card slide and a 60-frame end-card hold, the cut is ~7.4s.
92 
93**Music length.** The bed must cover `trim_intro_sec` + the cut. If it is too short, the
94renderer trims less of the intro (with a warning) so the music still runs to the end and
95fades out there. If the file is shorter than the whole cut, the tail is silent (warned).
96 
97## Choices
98 
99The recipe asks the user these before any paid step; the renderer only draws what the config says.
100 
101- `flavor_lineup` — which SKUs and in what order → `flavors[]`. The demo used 5 energy-drink flavors, strongest last.
102- `title_style` — the header style → `title_font` (download per the table), `title_size`. The demo used a cursive script.
103- `hand` — whose hand → `hand_clip`: the bundled clip (free) or a new `create-video-fal` clip (paid). The demo used a woman's hand.
104- `music` — the bed's feel → the `create-music-elevenlabs` prompt, then `music.path`. The demo used an upbeat, punchy energy underscore.
105 
106## Making the hand clip (paid, gated — not this capability)
107 
108Only when `choices.hand` asks for a different hand than the bundled clip, or no bundled-clip
109URL is available. Fill `{hand}` from `choices.hand` (e.g. "a man's hand with a silver watch").
110Write the payload to a file so apostrophes in the description don't break the shell:
111 
112```bash
113cat > hand-payload.json <<'JSON'
114{
115 "prompt": "Close-up of {hand}, the BACK of the hand facing the camera, index finger extended pointing up. The hand swipes smoothly from RIGHT to LEFT across the frame like scrolling to the next photo on a phone. One clean horizontal swipe, the index finger stays extended (do not curl it). Solid flat bright green screen background, even flat lighting, fixed camera, no zoom, no camera shake.",
116 "negative_prompt": "curled finger, closed fist, vertical motion, static, frozen, morphing, deformed fingers, extra fingers, text, watermark, changing background",
117 "duration": "5",
118 "aspect_ratio": "9:16"
119}
120JSON
121python3 /tmp/gooseworks-scripts/create-video-fal/scripts/gen_video.py \
122 --model fal-ai/kling-video/v2.5-turbo/pro/text-to-video \
123 --payload @hand-payload.json --out hand-swipe-greenscreen.mp4
124```
125 
126Then look at the first ~1s (e.g. `--stills`-style frame grabs with ffmpeg, or `watch`) and set
127`hand.trim_start` / `hand.trim_end` to the clean finger-up window. Set `hand_clip` to the file.
128 
129## Making the music bed (paid, gated — not this capability)
130 
131Generate it long enough to cover the intro trim plus the cut, plus 1s of slack: at least
132`(trim_intro_sec + target_duration_sec + 1)` seconds, i.e. about 11s for the default 2.5s
133trim and a 7.5s cut. Keep `--trim-intro 0` there; this renderer does the trim.
134 
135```bash
136python3 /tmp/gooseworks-scripts/create-music-elevenlabs/scripts/gen_music.py \
137 --prompt '<from choices.music; instrumental only, no vocals, no artist names>' \
138 --length-ms 11000 --duration 11 --out music-bed.m4a
139```
140 
141Then set `music.path` to `music-bed.m4a`.
142 
143## Craft rules (baked into the renderer)
144 
145- **Never AI-regenerate the product.** Image-to-video hallucinates the can and garbles the
146 label. Only the hand is generative; the products are real cutouts.
147- **Green-dominance key, not colorkey/chromakey.** Alpha is 0 only where
148 `g > green_margin × max(r, b)`. It is brightness-invariant, so it survives a muddy,
149 uneven AI green. Kept pixels are despilled (green clamped to `max(r, b)`), so green spill
150 on skin turns neutral instead of see-through. Raise `green_margin` if skin goes
151 see-through; lower it if green fringe remains.
152- **Back of hand toward camera, finger UP.** A nails-forward hand reads as the wrong hand.
153- **Trim to the finger-UP window.** These clips curl the finger periodically.
154- **Scale the hand down and anchor it at the bottom** so the fingertip reaches ~35% up (the
155 can base) and never covers the product.
156- **Flavors change by sliding RIGHT-TO-LEFT**, never a vertical flick or a top-drop.
157- The hand is scaled to the frame width with its aspect kept, so a non-9:16 clip is not stretched.
158 
159## Output
160 
161A `WxH` (default 1080×1920) H.264 mp4, with AAC audio when `music` is set (fade in/out +
162loudnorm, padded to the full length). The last line names the length and `music`/`silent`;
163read the WARNING lines above it. Then review the FULL mp4 with `watch`: the hand is opaque in
164every frame with no green fringe, the finger stays up, the title fits the frame, labels are
165crisp and readable, the slide is right-to-left, and the music runs to the end.
166 

Discussion