$npx -y skills add gooseworks-ai/goose-skills --skill render-offer-adRender a punchy ~12s vertical (9:16) music-only direct-response OFFER ad as a 4-beat kinetic-typography film — HEADLINE slam → real PRODUCT drop → CLAIM/proof → CTA pill — from one config of copy slots, a real product photo, a brand palette, fonts, bpm, and beat split. DETERMINIS
| 1 | # render-offer-ad |
| 2 | |
| 3 | The free, deterministic renderer for the **motion-graphics-offer-ad** format — the |
| 4 | punchy ~12s vertical, music-only, direct-response offer ad built as a **4-beat kinetic |
| 5 | -typography film**: a HEADLINE slams in word-by-word → the real PRODUCT drops in → |
| 6 | the CLAIM/proof lands → a CTA pill resolves. No character, no VO, no captions — the |
| 7 | on-screen **typeset** text IS the message. |
| 8 | |
| 9 | This is a bundled **Remotion project** (`project/`) driven by a thin Python driver. The |
| 10 | shipping master is **100% engine-rendered** (springs + `interpolate`): backgrounds are |
| 11 | gradient divs off the `brand_palette`, props are inline SVG, and the ONLY composited |
| 12 | bitmap is the REAL product photo (`objectFit:contain`, **never stretched**). ALL |
| 13 | headline/claim/CTA/URL/wordmark text is typeset in the engine — **never AI-rendered**; |
| 14 | that is the format's credibility guard. Render cost is **~$0**; the only paid step is an |
| 15 | optional music bed, gated upstream in the recipe to `create-music-elevenlabs`. |
| 16 | |
| 17 | The whole ad is **data**: copy strings, product photo, palette, fonts, bpm, and beat |
| 18 | split all arrive as `config.json` and are bound to Remotion **input props** — nothing is |
| 19 | hardcoded in the scenes (the source run's Spoiled Child strings are generalised into |
| 20 | `project/src/props.ts`). Deterministic → **iterate the cut for free**. |
| 21 | |
| 22 | ## The 4 beats (the spine) |
| 23 | |
| 24 | 1. **HEADLINE** — primary-color radial ground; `headline_words` slam in WORD-BY-WORD |
| 25 | (`slamIn`, ~7-frame stagger, scale-overshoot + motion-blur smear, settling on the |
| 26 | downbeat); `subline` + an animated bobbing down-arrow drop in. |
| 27 | 2. **PRODUCT** — light radial ground; the REAL product photo drops in (`dropIn`) and |
| 28 | idle-bobs (`bob`), `objectFit:contain` (never stretch); the `motif_chip` pops in |
| 29 | (`popIn`); optional GENERIC competitor shape + strike-through (`wipe`) — never a |
| 30 | named competitor. |
| 31 | 3. **CLAIM** — light radial ground; the `mechanism_prop` slides in from a frame edge |
| 32 | (`flyIn` overshoot, ~20% from the bottom) to add motion AND show the mechanism; the |
| 33 | 3-line claim drops in staggered; product held bottom-right. |
| 34 | 4. **CTA** — primary-color radial ground; the `wordmark` slams (`slamIn`); the CTA pill |
| 35 | pops in as the motif-chip handoff resolving (`popIn`) with an arrow nudge; the |
| 36 | `cta_url` fades up. |
| 37 | |
| 38 | Scene boundaries are **derived** from `beat_split_sec` (default `[3.0, 3.5, 3.0, 2.5]`s → |
| 39 | cuts at frames 0/90/195/285/360 @30fps) so the hard cuts land on the beat downbeats. The |
| 40 | motion kit — `slamIn / dropIn / bob / flyIn / popIn / wipe` — lives in |
| 41 | `project/src/lib/anim.ts` and is kept intact from the source run. |
| 42 | |
| 43 | ## Two gating checks (run before any render — from the recipe) |
| 44 | |
| 45 | - **(a) CLAIM-MATCHES-FORMAT** — the claim/proof verbs MUST match the product's physical |
| 46 | format: liquid → `spoon / sip / drink / 0 mess`; powder → `scoop / mix / no clumps`; |
| 47 | capsule → `1 a day`; gummy → `chew`. `render.py` rejects foreign-format vocabulary |
| 48 | (e.g. a liquid product must not borrow powder grammar like `scoop / no clumps`). Set |
| 49 | `product_format` in the config to arm this gate. |
| 50 | - **(b) CLAIM-BEAT MECHANISM PROP** — the claim beat must include an edge-entry |
| 51 | `mechanism_prop` (`spoon` for drinkable liquids, or `accent` for a neutral edge-entry |
| 52 | accent bar) that supplies motion AND shows the mechanism. A text-only claim beat is too |
| 53 | static — `render.py` rejects an unsupported/missing prop. |
| 54 | |
| 55 | ## Run |
| 56 | |
| 57 | ```bash |
| 58 | # 1) install the bundled Remotion project's deps (one-time; render.py auto-runs |
| 59 | # this if node_modules is absent). |
| 60 | cd project && npm install && cd .. |
| 61 | |
| 62 | # 2) bind config → Remotion input props, render the 9:16 master, derive the 1:1 crop. |
| 63 | python3 scripts/render.py \ |
| 64 | --config path/to/config.json \ |
| 65 | --work-dir <work> \ |
| 66 | --out <work>/master |
| 67 | # → <work>/master-9x16.mp4 (1080x1920) and <work>/master-1x1.mp4 (1080x1080 crop) |
| 68 | ``` |
| 69 | |
| 70 | `render.py` copies the config's `product_hero_image` (+ optional `music.bed`) into |
| 71 | `project/public/`, maps every config key onto t |