Render offer ad
Skill gooseworks-ai/goose-skills/skills/ads/capabilities/render-offer-ad
Render 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. DETERMINISTIC + FREE (a bundled Remotion project; springs + interpolate, no AI-gen for visuals). Backgrounds are engine gradient divs off the palette, props are inline SVG, the ONLY composited bitmap is the REAL product photo (objectFit:contain, never stretched), and ALL headline/claim/CTA/URL/wordmark text is typeset in the engine — never AI-rendered (the format's credibility guard). A driver binds the config to Remotion input props, renders the 9:16 master, and derives a 1:1 center-crop with ffmpeg. Two gating checks run before render (claim verbs must match the product's physical format; the claim beat needs an edge-entry mechanism prop). Use for the motion-graphics-offer-ad format.From its SKILL.md
npx -y skills add gooseworks-ai/goose-skills --skill render-offer-adAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
SKILL.md
9.1 KB, ~2.3k tokens by cl100k_base, as published. Nobody here has run it
render-offer-ad
The free, deterministic renderer for the motion-graphics-offer-ad format — the punchy ~12s vertical, music-only, direct-response offer ad built as a 4-beat kinetic -typography film: a HEADLINE slams in word-by-word → the real PRODUCT drops in → the CLAIM/proof lands → a CTA pill resolves. No character, no VO, no captions — the on-screen typeset text IS the message.
This is a bundled Remotion project (project/) driven by a thin Python driver. The
shipping master is 100% engine-rendered (springs + interpolate): backgrounds are
gradient divs off the brand_palette, props are inline SVG, and the ONLY composited
bitmap is the REAL product photo (objectFit:contain, never stretched). ALL
headline/claim/CTA/URL/wordmark text is typeset in the engine — never AI-rendered;
that is the format's credibility guard. Render cost is ~$0; the only paid step is an
optional music bed, gated upstream in the recipe to create-music-elevenlabs.
The whole ad is data: copy strings, product photo, palette, fonts, bpm, and beat
split all arrive as config.json and are bound to Remotion input props — nothing is
hardcoded in the scenes (the source run's Spoiled Child strings are generalised into
project/src/props.ts). Deterministic → iterate the cut for free.
The 4 beats (the spine)
- HEADLINE — primary-color radial ground;
headline_wordsslam in WORD-BY-WORD (slamIn, ~7-frame stagger, scale-overshoot + motion-blur smear, settling on the downbeat);subline+ an animated bobbing down-arrow drop in. - PRODUCT — light radial ground; the REAL product photo drops in (
dropIn) and idle-bobs (bob),objectFit:contain(never stretch); themotif_chippops in (popIn); optional GENERIC competitor shape + strike-through (wipe) — never a named competitor. - CLAIM — light radial ground; the
mechanism_propslides in from a frame edge (flyInovershoot, ~20% from the bottom) to add motion AND show the mechanism; the 3-line claim drops in staggered; product held bottom-right. - CTA — primary-color radial ground; the
wordmarkslams (slamIn); the CTA pill pops in as the motif-chip handoff resolving (popIn) with an arrow nudge; thecta_urlfades up.
Scene boundaries are derived from beat_split_sec (default [3.0, 3.5, 3.0, 2.5]s →
cuts at frames 0/90/195/285/360 @30fps) so the hard cuts land on the beat downbeats. The
motion kit — slamIn / dropIn / bob / flyIn / popIn / wipe — lives in
project/src/lib/anim.ts and is kept intact from the source run.
Two gating checks (run before any render — from the recipe)
- (a) CLAIM-MATCHES-FORMAT — the claim/proof verbs MUST match the product's physical
format: liquid →
spoon / sip / drink / 0 mess; powder →scoop / mix / no clumps; capsule →1 a day; gummy →chew.render.pyrejects foreign-format vocabulary (e.g. a liquid product must not borrow powder grammar likescoop / no clumps). Setproduct_formatin the config to arm this gate. - (b) CLAIM-BEAT MECHANISM PROP — the claim beat must include an edge-entry
mechanism_prop(spoonfor drinkable liquids, oraccentfor a neutral edge-entry accent bar) that supplies motion AND shows the mechanism. A text-only claim beat is too static —render.pyrejects an unsupported/missing prop.
Run
# 1) install the bundled Remotion project's deps (one-time; render.py auto-runs
# this if node_modules is absent).
cd project && npm install && cd ..
# 2) bind config → Remotion input props, render the 9:16 master, derive the 1:1 crop.
python3 scripts/render.py \
--config path/to/config.json \
--work-dir <work> \
--out <work>/master
# → <work>/master-9x16.mp4 (1080x1920) and <work>/master-1x1.mp4 (1080x1080 crop)
render.py copies the config's product_hero_image (+ optional music.bed) into
project/public/, maps every config key onto the OfferAdProps shape props.ts reads,
writes the input props to <work>/props.json, runs npx remotion render offer-ad --props <work>/props.json, then center-crops the 9:16 master to 1:1 with ffmpeg (no 2nd
composition). NO hardcoded /Users or clients paths — every asset arrives via the
config and a runtime --work-dir.
Scripts
scripts/render.py— the driver: gating checks → stage assets intoproject/public/→ bind config to Remotion input props →npx remotion render(9:16) → ffmpeg 1:1 center-crop.scripts/config.example.json— the shape of theconfigthe recipe binds (brand-neutral worked defaults from the source run; replace every/abs/...placeholder).project/— the bundled Remotion project.src/props.ts(the input-props schema + beat-layout math + safe defaults),src/lib/anim.ts(theslamIn/dropIn/bob/flyIn/ popIn/wipemotion kit — kept intact),src/scenes/Scene01–04.tsx(the four beats, all copy/palette/fonts from props),src/Main.tsx(the beat spine + audio bed),src/Root.tsx(theoffer-adcomposition; duration derived frombeat_split_sec).
Config → props mapping (what binds to what)
| config key | Remotion prop (props.ts) | scene that reads it |
|---|---|---|
product_hero_image | product_image (staged to public/) | Scene02 (drop), Scene03 (anchor) |
brand_palette{primary_ground,light_ground,ink,highlight_chip} (dict OR ≥4-hex list) | palette | all 4 (grounds/type/chip) |
fonts{display,body} | fonts (resolved in fonts.ts) | all 4 |
copy.headline_words[] / copy.subline | copy.headline_words / copy.subline | Scene01 |
copy.motif_chip | copy.motif_chip | Scene02 |
copy.claim_lines[]{big,small} | copy.claim_lines | Scene03 |
copy.cta_label / copy.cta_url / copy.wordmark | same | Scene04 |
mechanism_prop (spoon|accent) | mechanism_prop | Scene03 (inline-SVG prop) |
bpm | bpm | beat grid |
beat_split_sec [4] | beat_split_sec | derives cut frames + total duration |
music.bed / music.fade_out_frames | music.src (staged) / music.fade_out_frames | Main audio bed |
show_competitor_strike | show_competitor_strike | Scene02 |
aspects | (driver) | 9:16 always; 1:1 = ffmpeg crop |
Craft rules (load-bearing — faithful to the source run)
- The composited bitmap is the REAL product photo — never an AI packshot. Clean /
transparent bg;
objectFit:contain, never stretched. - Never AI-render text. Headline, claim, CTA, URL, wordmark — all typeset in the engine. AI never draws a letter in this format.
- Never Ken-Burns/zoompan over stills — the engine springs carry the motion; a static/zoompan cut was rejected as "too static" on the source run.
- Music-only, beat-locked — the cuts + big slams land on the 0.0/3.0/6.5/9.5s
downbeats; the bed volume ramps to 0 over the last
music.fade_out_framesinside the render. No VO, no captions. - Never name/jab a competitor — any comparison shape on the product beat is a GENERIC
silhouette (
show_competitor_strike), not a named brand. - The two gating checks are non-optional — a liquid product must not borrow powder grammar; the claim beat must have real prop motion, not a static text stack.
Requires
- Node.js (≥18) + Remotion (
@remotion/cli≥4.0, installed via the bundledproject/package.json—render.pyrunsnpm installinproject/on first use). This capability legitimately needs a node runtime — the renderer IS a Remotion project. - ffmpeg on PATH (for the 1:1 center-crop).
watch(QC the final master — confirm the headline slams WORD-BY-WORD on the beat, the real product photo drops in + idle-bobs and is NOT stretched, the mechanism prop enters from a frame edge on the claim beat, the claim lines drop in staggered, the CTA label + URL are readable at 1080×1920, the hard cuts land on the downbeats, and the music ramps out; re-confirm both gating checks visually). The recipe gates the only optional paid step (music bed →create-music-elevenlabs) — this capability itself makes NO paid calls.
What ships with it: 19 files
53.0 KB alongside SKILL.md, 6 of them executable
project/
- .gitignore143 B
- package.json503 B
- public/.gitkeep0 B
- remotion.config.tsruns116 B
- src/fonts.tsruns1.4 KB
- src/index.tsruns108 B
- src/lib/anim.tsruns4.8 KB
- src/Main.tsx1.5 KB
- src/props.tsruns4.1 KB
- src/Root.tsx1.0 KB
- src/scenes/Scene01.tsx5.0 KB
- src/scenes/Scene02.tsx4.9 KB
- src/scenes/Scene03.tsx6.5 KB
- src/scenes/Scene04.tsx3.5 KB
- tsconfig.json293 B
scripts/
- config.example.json2.1 KB
- render.pyruns11.8 KB
tests/
- smoke-test.md4.9 KB
- skill.meta.json307 B