$npx -y skills add gooseworks-ai/goose-skills --skill render-absurdist-explainerAssemble an absurdist animated-explainer video ad (~38s, 9:16) from per-scene i2v clips + their measured VO windows — retime each clip to its VO, re-encode every segment to identical 30fps/libx264/yuv420p so the concat demuxer never drops frames, concat, build a REAL-product PIL
| 1 | # render-absurdist-explainer |
| 2 | |
| 3 | The free, deterministic renderer for the **absurdist-explainer** video ad format — the |
| 4 | bright Pixar/Disney 3D spot where a personified villain (the problem) narrates the whole |
| 5 | ad in one voice, teaches the product's ownable mechanism through cartoon biology, lists |
| 6 | the damage, then watches its own scheme collapse when the product arrives. This |
| 7 | capability is the **FREE assembly stage only**. All generative work (nano-banana |
| 8 | keyframes, Seedance i2v clips, ElevenLabs VO + music) happens upstream in the recipe and |
| 9 | is handed to this capability as files. |
| 10 | |
| 11 | It ports the validated compose recipe from two reference runs (HUM "Big Chill" cortisol |
| 12 | absurdism and Soteri "Eczema, the pH villain"). The recipe is deterministic — iterate the |
| 13 | cut for free, re-roll only the offending paid beat. |
| 14 | |
| 15 | ## What it does (the deterministic recipe) |
| 16 | |
| 17 | 1. **Per-scene retime.** Each i2v clip is retimed to its **measured** VO window |
| 18 | (`scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,fps=30,setsar=1`, |
| 19 | then `tpad=stop_mode=clone` if the VO is longer than the clip, else `-t` trim). |
| 20 | 2. **Identical re-encode.** Every segment is re-encoded `libx264 -crf 18 -pix_fmt yuv420p |
| 21 | -r 30` even if already correct — a framerate mismatch makes the concat demuxer silently |
| 22 | drop frames. |
| 23 | 3. **Concat** all scene segments + the end card via the concat demuxer (`-c copy`). |
| 24 | 4. **Real-product end card.** `build_endcard.py` composites the REAL retail product photo |
| 25 | over the brand palette (flat, or sampled from the photo's own edge pixel) with a typeset |
| 26 | wordmark + claim rows + CTA pill in PIL `ImageDraw.text` — **never an AI cartoon bottle, |
| 27 | never AI-rendered brand text**. `compose.py` Ken-Burnses it 1.00 → 1.04 over the dwell. |
| 28 | 5. **Mix.** VO bus `loudnorm I=-14 TP=-1.5`, music bus `loudnorm I=-26 TP=-3` then |
| 29 | `volume=0.62`, `amix inputs=2 duration=first normalize=0` → master lands at |
| 30 | -14.5..-13.5 LUFS with the music ducked under the VO. |
| 31 | 6. **Captions last.** `make_captions.py` emits a libass `.ass` (one cue per scene, Arial 64 |
| 32 | white / 6px outline / MarginV=330, `start = scene_start + 0.08s`, suppressed on the end |
| 33 | card). `compose.py` burns it as the final filter so captions sit on top. |
| 34 | |
| 35 | ## Scripts (free — Python + ffmpeg + PIL, no bash, no paid calls) |
| 36 | |
| 37 | - `scripts/build_endcard.py` — PIL composite of the real product photo + typeset brand |
| 38 | layer (wordmark / product line / claim rows / accent CTA pill). Reads the same |
| 39 | `config.json`. Run this FIRST so `end_card.image` exists before `compose.py`. |
| 40 | - `scripts/make_captions.py` — emits the per-scene libass `.ass` from the SAME scene table |
| 41 | compose reads, so caption windows stay in lockstep with the cut. Run before `compose.py` |
| 42 | (or point `config.captions_ass` at nothing to skip captions). |
| 43 | - `scripts/compose.py` — the assembler: per-scene retime + identical 30fps re-encode → |
| 44 | concat → Ken-Burns end card → VO/music loudnorm mix → burn captions → master mp4. |
| 45 | - `scripts/config.example.json` — the shape of the `config` the recipe binds (the |
| 46 | brand-neutralised Soteri values as a worked reference). |
| 47 | |
| 48 | ## Inputs (all via `--config` + a runtime work dir — NO hardcoded paths) |
| 49 | |
| 50 | `config.json` carries: `scenes[]` (each `{id, clip, target_sec, vo, caption, atempo?}` |
| 51 | where `target_sec` is the **measured** VO window), `end_card{product_image, image, |
| 52 | dwell_sec, zoom_to, wordmark, product_line, claims[], cta, background?}`, `brand_palette |
| 53 | {primary, primary_lite, accent, grey}`, `music_bed`, `music_volume` (default 0.62), |
| 54 | `atempo` (compose-stage VO speed-up, default off; the reference runs used 1.3 when the VO |
| 55 | read slow), `captions_ass`, and `caption_style`. See `config.example.json`. |
| 56 | |
| 57 | ## Craft rules (load-bearing — faithful to the source molecule) |
| 58 | |
| 59 | - **The end card is the REAL product photo, composited — never an AI cartoon bottle.** |
| 60 | Both reference runs shipped an AI bottle first and had to re-shoot with the real photo. |
| 61 | - **No AI-rendered brand text anywhere.** Wordmark, claims, CTA, motif — all PIL |
| 62 | `ImageDraw.text`. AI draws the world + characters only. |
| 63 | - **Re-encode every segment to 30fps before concat**, even if already correct, or the |
| 64 | concat demuxer silently drops frames. |
| 65 | - **`target_sec` is the ME |