Claude Skill

render-podcast-skit

Assemble a two-host fake-podcast skit ad from a config — per-line lipsync clips hard-concatenated in script order, scaled/padded to 1080×1920, WHITE bottom-center captions (up to 5 words per cue, broken on sentence punctuation, word-wrapped to stay in-frame, held at least 0.9s) b

LLM Mart · 0 points · 5 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download gooseworks-ai-goose-skills-skills_ads_capabilities_render-podcast-skit-e1592ee.zip · 14 KB
Part of gooseworks-ai/goose-skills — 44 skills

Install

skills CLI npx skills add https://github.com/gooseworks-ai/goose-skills/tree/main/skills/ads/capabilities/render-podcast-skit
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install gooseworks-ai-goose-skills@llmmart
Git git clone https://github.com/gooseworks-ai/goose-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole gooseworks-ai/goose-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

render-podcast-skit

Assemble a two-host fake-podcast skit ad from a config: a skeptic and a believer at an absurd themed podcast desk do a snappy back-and-forth about the product (the set is deliberately unrelated — that is the joke). Each line is its own lipsync clip so the edit can cut on the dialogue beat (~1.8s avg); this capability is the FREE, deterministic assembly that concatenates those clips, renders the WHITE captions, and appends the brand end card.

scripts/config.example.json is the worked example (Ladder run-02 "Laundromat 2am", ~49s 1080×1920 9:16, ~22 lines); scripts/PIPELINE.md maps every config block to its source step and scripts/README.md documents the free assembly.

Run

This is the FREE, deterministic assembly stage — it spends nothing. The paid inputs are separate capabilities: one ElevenLabs with-timestamps VO per line (one voice per host) via create-vo-elevenlabs; two photoreal base stills at the themed desk plus ~10 expression variants (mouths NEUTRAL/CLOSED, gpt-image-2 quality=high, not nano-banana) via create-image-gpt-image-fal; and one lipsync clip per (still, VO) pair via create-video-fal. Given the per-line clips + their VO timestamps + the brand wordmark SVG, render-podcast-skit walks the scenes in script order, builds the global caption timeline, renders the WHITE captions, hard-concats the clips, auto-appends the end card, and final-encodes crf28 → the master. Re-cuts reuse the existing VOs / stills / clips and cost $0.

Contract (the free assembly)

  • Dialogue-carried, no music bed by default. The per-line VO is the audio; a podcast skit needs no music (an optional low ambience is a taste call, off by default).
  • One line = one scene = one hard cut, in script order. Hard-concat the per-line clips in order (scale/pad to 1080×1920, re-encode) — no dissolves.
  • Captions from the VO's OWN char-level timestamps, not Whisper (script-window). Build a global words.json by offsetting each line's char-level word timings by the cumulative clip start, group into ≤5-word cues broken on sentence-final punctuation, and render WHITE #FFFFFF bottom-center captions (black outline), word-wrapped to stay in-frame and held ≥0.9s — PIL PNG overlays when the host ffmpeg lacks libass (common), else ASS. (Yellow 3-word karaoke was the old style, rejected in testing.) Whisper on the rendered clips mistimes; the VO timestamps are ground truth.
  • End card via Playwright/PIL from the real wordmark — never AI-render brand text. The lockup is a deterministic HTML → PNG → 2.5s mp4 from the brand's real wordmark SVG (black bg, brand wordmark, CTA pill, URL), auto-appended after the last line. A diffusion model garbles a wordmark.
  • FFmpeg composite, deterministic, FREE. Concat the clips, overlay the WHITE caption PNGs (or burn ASS via libass), append the end-card mp4, and final-encode -preset slow -crf 28 + aac 96k → a 1080×1920 h264+aac master (~6MB for ~28s; the old -crf 20 produced ~16MB). No paid calls, no keys.
Files (goose-skills)
  • scripts
    • config.example.json 15 KB
      {
        "_comment": "Ladder run-02 'Laundromat 2am' podcast-skit — the worked example. Copy to config.json and edit. A ~49s two-host (skeptic Brittney + believer Brad) 9:16 fake-podcast skit at a 24hr laundromat set (unrelated to fitness — that's the joke). Pipeline: (1) ElevenLabs with-timestamps VO per line, one voice per host; (2) two base stills (gpt-image-2 quality=high, photoreal, mouths closed) + ~10 edit-anchored expression variants on the bases; (3) Veed Fabric 1.0 lipsync per (still, VO) pair @ 720p; (4) WHITE captions from the VO's char-level timestamps, <=5 words/cue, sentence-broken, word-wrapped; (5) hard-cut concat + a 2.5s Playwright/PIL brand end card, final-encode crf28. Master 1080x1920 h264+aac. NOTE: this Ladder example is worked-reference — write a FRESH script + hook for each brand, do not reuse these lines. See scripts/PIPELINE.md for the source scripts.",
        "brand_name": "Ladder",
        "campaign": "podcast-skit — laundromat 2am",
        "title": "Ladder Podcast Ad run-02 — Laundromat 2am",
        "width": 1080,
        "height": 1920,
        "fps": 24,
        "duration_sec": 49,
        "duration_target_sec": 42,
        "aspect_ratio": "9:16",
      
        "set_description": "24hr laundromat at 2am. Two hosts at a wedged-in podcast desk between two front-load dryers. Broadcast mic stands, over-ear headphones, fluorescent overhead light, faded WASH DRY FOLD sign behind. Lone sock tumbling in a back dryer. The set is deliberately unrelated to the product — that is the joke.",
      
        "brand_guardrails": [
          "No transformation claims ('lose 20 lbs'). Ladder sells STRUCTURE, not OUTCOME.",
          "No celebrity name-drops outside the actual coach roster.",
          "Plain-spoken voice — 'Get stronger, with a plan' beats hype.",
          "Lime/black palette only when product surfaces appear.",
          "The believer never mocks the user — the villain is the friction, not the buyer."
        ],
      
        "intonation_legend": {
          "_note": "Written into script.json so ElevenLabs delivers the intended prosody. Applied inline in each scene's `text`.",
          "...": "trailing thought / dramatic pause (~0.7-1.2s)",
          " — ": "crisp em-dash pause, sharper than comma (~0.4s)",
          "?": "rising intonation",
          ".": "hard stop, drops pitch",
          ",": "natural breath beat",
          "CAPS": "emphasized word (slightly louder + slower)"
        },
      
        "voices": {
          "_note": "Exactly TWO hosts, one voice each, held across the whole skit. Real ElevenLabs voice_ids. HER = skeptic (validated as the Ramp v1.2 skeptic); HIM = believer (Ramp v1 believer-bro). eleven_multilingual_v2 with-timestamps.",
          "HER": {
            "role": "skeptic",
            "name": "Brittney",
            "voice_id": "kPzsL2i3teMYv0FxEYQ6",
            "provider": "elevenlabs",
            "model": "eleven_multilingual_v2",
            "endpoint": "with-timestamps",
            "settings": { "stability": 0.45, "similarity_boost": 0.78, "style": 0.45, "use_speaker_boost": true }
          },
          "HIM": {
            "role": "believer",
            "name": "Brad",
            "voice_id": "T4x5CtnhOiichhcqFzgg",
            "provider": "elevenlabs",
            "model": "eleven_multilingual_v2",
            "endpoint": "with-timestamps",
            "settings": { "stability": 0.45, "similarity_boost": 0.78, "style": 0.35, "use_speaker_boost": true }
          }
        },
      
        "characters": {
          "_note": "REALISM: generate with gpt-image-2 quality=high, NOT nano-banana (nano faces read as AI-stock, rejected in testing). ALWAYS append realism_suffix. Two bases first (9:16, tight bust, mic foreground, headphones, MOUTH CLOSED). Skeptic base first, then believer via gpt-image-2/edit anchored on the skeptic's still for set consistency. Then fan out variants — each edit on its host's base as the SOLE reference, prompt describes ONLY the pose/expression change so the identity + background stay across all cuts. QC every still for hallucinations (asymmetric long hair-lock, extra fingers, morphed ears) before lipsync.",
          "engine": { "provider": "fal", "model_t2i": "openai/gpt-image-2", "model_edit": "openai/gpt-image-2/edit", "image_size": { "width": 1024, "height": 1792 }, "quality_base": "high", "quality_variant": "high", "submit": "sequential" },
          "realism_suffix": " PHOTOREALISM IS CRITICAL: a real candid documentary photograph of a real human — NOT illustration/3D/CGI/AI-stock. Real skin with pores, fine lines, uneven tone, slight asymmetry; no smoothing/retouching/waxy skin/perfect teeth. HAIR MUST BE COHERENT: one consistent hairstyle all around — no single long lock on one side that mismatches the top (a common gpt-image hallucination). No extra hands/fingers, no deformed ears.",
          "still_rules": "Tight bust framing, broadcast mic foreground, over-ear headphones. MOUTH NEUTRAL/CLOSED (open-mouth still breaks lipsync). State the hairstyle EXPLICITLY and coherently (e.g. 'short neatly tousled hair above the collar, no long strands'). Photorealistic, 35mm lens, shallow background blur.",
          "variant_template": "Use the reference image as the EXACT base — the SAME real person. Keep the face, skin texture, hair (same length + shape), headphones, clothing, background, microphone, and lighting IDENTICAL. Change ONLY: {change} Lips remain closed naturally, no open mouth, no smile showing teeth.",
          "bases": [
            { "who": "HER", "file": "brittney-base.png", "_note": "generate FIRST; sets the laundromat set" },
            { "who": "HIM", "file": "brad-base.png",     "_note": "generate SECOND with brittney-base.png as a bg reference so the set matches" }
          ],
          "expression_variants": [
            { "out_name": "brittney-eyebrow-up.png", "who": "HER", "change": "her left eyebrow raised slightly, mildly skeptical look, eyes narrowed just a touch.", "_note": "'narrowed eyes' NSFW-rejected first pass — if it fails swap to 'curious questioning look, eyes wide open and bright'" },
            { "out_name": "brittney-shrug.png",      "who": "HER", "change": "her shoulders raised slightly in a small shrug, head tilted a few degrees to her left, palms upturned just visible at bottom edge." },
            { "out_name": "brittney-hand-ear.png",   "who": "HER", "change": "her right hand lifted to touch the right headphone cup, fingers visible at side of head, focused listening expression." },
            { "out_name": "brittney-half-smile.png", "who": "HER", "change": "a subtle closed-lip half-smile, the corner of her mouth lifted slightly, eyes warm and amused." },
            { "out_name": "brittney-deal-close.png", "who": "HER", "change": "a slight lean forward toward the microphone, attentive engaged look, eyes locked on camera." },
            { "out_name": "brad-explain.png",        "who": "HIM", "change": "head turned a few degrees more toward the camera, attentive engaged look, his right hand opening upward in a small explanatory gesture just visible at bottom-right edge." },
            { "out_name": "brad-grin.png",           "who": "HIM", "change": "a subtle confident closed-lip grin starting, corner of mouth lifted slightly, eyes amused, the trace of a gotcha-look." },
            { "out_name": "brad-listing.png",        "who": "HIM", "change": "his right hand raised with three fingers extended as if counting items, hand visible at bottom-right edge, head turned slightly." },
            { "out_name": "brad-phone-up.png",       "who": "HIM", "change": "his right hand visible holding a smartphone vertically, the phone screen showing a generic dark-blue workout app interface with white text bars (no logos), held just below his chin, raised slightly toward camera.", "_note": "generic app UI only — NO real logos/screens" },
            { "out_name": "brad-leans-back.png",     "who": "HIM", "change": "leaning slightly back from the microphone, satisfied confident posture, head tilted back a few degrees, eyes relaxed." }
          ]
        },
      
        "lipsync_engine": {
          "_note": "Each (still, VO) pair -> one clip. Default veed-fal (veed/fabric-1.0) @ 720p, concurrency 4, idempotent. Hedra Character-3 fallback if fal.ai balance exhausted. Empty text-prompt on Veed Fabric — a text prompt interferes with lipsync.",
          "gateway": "veed-fal",
          "model": "veed/fabric-1.0",
          "resolution": "720p",
          "aspect_ratio": "9:16",
          "concurrency": 4,
          "text_prompt": "",
          "fallback": { "gateway": "hedra", "model": "character-3", "model_id": "d1dd37a3-e39a-4854-a298-6510289f9cf2" }
        },
      
        "captions": {
          "_note": "WHITE captions in the add-captions-veed-fal whisper house style (yellow karaoke was rejected in testing). Words come from the VO's OWN char-level timestamps (scene-NN-<who>.timestamps.json), NOT Whisper, offset by cumulative clip start. Cue = up to 5 words, FLUSHED on sentence-final punctuation (. ? !). Each cue word-wraps to stay INSIDE the frame (never off the edges) and stays on screen >= min_dur_sec. Rendered as PIL PNG overlays (libass-free) or ASS if libass is available; bottom-anchored. `caption` per scene is the phrase intent; the actual burned text comes from the VO timings.",
          "style": "white_bottom_center",
          "source": "vo_timestamps",
          "font": "Avenir Next Bold",
          "font_size": 66,
          "color_hex": "#FFFFFF",
          "stroke_hex": "#000000",
          "stroke_width": 6,
          "max_words_per_cue": 5,
          "break_on_sentence_punctuation": true,
          "min_dur_sec": 0.9,
          "wrap_max_width_px": 900,
          "position": "bottom_center",
          "bottom_anchor_px": 1720
        },
      
        "scenes": [
          { "scene": 1,  "who": "HER", "time": "0:00-0:02", "still": "brittney-base.png",       "caption": "what app today",        "text": "So... what app are we ad-reading today?" },
          { "scene": 2,  "who": "HIM", "time": "0:02-0:03", "still": "brad-explain.png",        "caption": "called LADDER",         "text": "It's called LADDER." },
          { "scene": 3,  "who": "HER", "time": "0:03-0:05", "still": "brittney-eyebrow-up.png", "caption": "thing you climb?",      "text": "Like... the thing you climb?" },
          { "scene": 4,  "who": "HIM", "time": "0:05-0:07", "still": "brad-explain.png",        "caption": "number one app",        "text": "Strength training app. Number one in the App Store." },
          { "scene": 5,  "who": "HER", "time": "0:07-0:08", "still": "brittney-shrug.png",      "caption": "I go to gym",           "text": "OK but I already go to the gym?" },
          { "scene": 6,  "who": "HIM", "time": "0:08-0:10", "still": "brad-grin.png",           "caption": "workout tomorrow?",     "text": "Cool. What workout are you doing tomorrow?" },
          { "scene": 7,  "who": "HER", "time": "0:10-0:12", "still": "brittney-base.png",       "caption": "I don't know",          "text": "I DON'T KNOW... I'll figure it out when I get there.", "_note": "v2 re-render — v1 read too clean for a skeptic" },
          { "scene": 8,  "who": "HIM", "time": "0:12-0:14", "still": "brad-explain.png",        "caption": "LADDER fixes that",     "text": "Right — that's the part LADDER fixes." },
          { "scene": 9,  "who": "HIM", "time": "0:14-0:17", "still": "brad-explain.png",        "caption": "do what it says",       "text": "Open the app. It tells you what to do. You do it." },
          { "scene": 10, "who": "HER", "time": "0:17-0:19", "still": "brittney-eyebrow-up.png", "caption": "insultingly simple",    "text": "That sounds... almost insultingly simple." },
          { "scene": 11, "who": "HIM", "time": "0:19-0:20", "still": "brad-base.png",           "caption": "it is",                 "text": "It is." },
          { "scene": 12, "who": "HER", "time": "0:20-0:21", "still": "brittney-base.png",       "caption": "who writes these",      "text": "Who's writing these workouts?" },
          { "scene": 13, "who": "HIM", "time": "0:21-0:23", "still": "brad-phone-up.png",       "caption": "nineteen coaches",      "text": "Actual coaches. NINETEEN of them." },
          { "scene": 14, "who": "HIM", "time": "0:23-0:25", "still": "brad-listing.png",        "caption": "pilates kettlebell yoga","text": "Strength... pilates... kettlebell... yoga." },
          { "scene": 15, "who": "HER", "time": "0:25-0:27", "still": "brittney-eyebrow-up.png", "caption": "new every day?",        "text": "Wait — so it's new every day?", "_note": "v2 re-render — more natural skeptic phrasing" },
          { "scene": 16, "who": "HIM", "time": "0:27-0:30", "still": "brad-phone-up.png",       "caption": "coach in ear",          "text": "New every day. Coach in your ear. Timer built in." },
          { "scene": 17, "who": "HER", "time": "0:30-0:32", "still": "brittney-hand-ear.png",   "caption": "coach in my ear?",      "text": "Wait — coach in my ear?" },
          { "scene": 18, "who": "HIM", "time": "0:32-0:34", "still": "brad-listing.png",        "caption": "counts your reps",      "text": "Like a real one. Counts your reps, talks you through it." },
          { "scene": 19, "who": "HER", "time": "0:34-0:36", "still": "brittney-half-smile.png", "caption": "stop thinking",         "text": "...so I just stop thinking?" },
          { "scene": 20, "who": "HIM", "time": "0:36-0:37", "still": "brad-base.png",           "caption": "you just lift",         "text": "You just lift." },
          { "scene": 21, "who": "HER", "time": "0:37-0:39", "still": "brittney-deal-close.png", "caption": "seven-day free trial?", "text": "Seven-day free trial?" },
          { "scene": 22, "who": "HIM", "time": "0:39-0:41", "still": "brad-leans-back.png",     "caption": "seven-day free trial",  "text": "Seven-day free trial. Go." }
        ],
      
        "end_card": {
          "_note": "Playwright HTML -> PNG (1080x1920) -> 2.5s silent mp4, appended after scene 22 by stitch.py (auto-appends clips/end-card.mp4). NEVER AI-rendered text — the wordmark is the brand's REAL SVG (Ladder wordmark pulled from run-01-app-sizzle). Black bg, lime wordmark center, lime CTA pill, URL at bottom.",
          "engine": "playwright_html",
          "script": "build_end_card.py",
          "duration_sec": 2.5,
          "background": "#000000",
          "wordmark_svg": "run-01-app-sizzle/source/brand/ladder-wordmark-white.svg",
          "accent_hex": "#DBFF00",
          "cta_text": "Start free trial",
          "url_text": "joinladder.com"
        },
      
        "assembly": {
          "_note": "stitch.py: walk scenes in order, ffprobe each clip, build a GLOBAL words.json (each word offset by cumulative clip start), group into <=5-word cues broken on sentence punctuation, render WHITE bottom-center captions word-wrapped to stay in-frame (PIL PNG overlays if the host ffmpeg lacks libass — the common case — else an ASS burn), concat all clips (scale/pad 1080x1920), auto-append clips/end-card.mp4. FINAL ENCODE libx264 -preset slow -crf 28 + aac 96k (keeps a ~28s master near ~6MB; crf20 produced ~16MB).",
          "concat": "ffmpeg_concat_reencode",
          "scale_pad": "scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2:black,setsar=1",
          "caption_burn": "pil_png_overlay_or_libass",
          "final_encode": "libx264 -preset slow -crf 28 -pix_fmt yuv420p -c:a aac -b:a 96k -movflags +faststart",
          "append_end_card": true
        },
      
        "post_production": {
          "music": { "default": "off", "note": "podcast skit is dialogue-carried; the per-line VO is the audio. No music bed (optional low ambience is a taste call, off by default)." },
          "captions": { "default": "on", "note": "WHITE #FFFFFF bottom-center from the VO's own char-level timestamps, <=5 words per cue, cue breaks on sentence punctuation, word-wrapped to stay in-frame, >=0.9s on screen (NOT yellow karaoke)" },
          "end_card": { "default": "on", "note": "Playwright/PIL end card from the brand's real wordmark SVG, default on" }
        }
      }
      
    • PIPELINE.md 7.2 KB
      # Pipeline — podcast-skit
      
      How `config.example.json` maps to the real production steps. This molecule ships a **config +
      this map**, not a bundled runner: the worked example (Ladder run-02 "Laundromat 2am") was
      produced by per-step driver scripts that live in
      `clients/ladder/ad-runs/run-02-podcast-skit/working/`. Reference those scripts directly, or drive
      the whole run via `video-orchestrator-with-control-plane` (variant mode — this format has no
      specialised orchestrator).
      
      The steps run **in order** because each depends on the last: the script sets the timeline, the VO
      gives the per-line timings, the timings + set drive the stills, the stills seed the lipsync clips,
      the VO's timestamps drive the captions, and the stitch stitches all of it + the end card.
      
      ## Field → script → phase → paid?
      
      | Config field | Source step | Script (in the run's `working/`) | Phase | Paid? |
      |---|---|---|---|---|
      | `voices.HER` / `voices.HIM` | one voice per host, `eleven_multilingual_v2` with-timestamps | `render_vo.py` | 1 | **PAID** (ElevenLabs, ~22 lines) |
      | `scenes[].text` | the per-line VO copy (intonation-marked) | `render_vo.py` | 1 | **PAID** |
      | `set_description` | the themed set fed into the base-still prompts | (hand-generated bases) | 2 | — |
      | `characters.bases[]` (2) | two base stills, mouth closed, at the desk | (skeptic first, believer on skeptic's still as ref) | 2 | **PAID** (`gpt-image-2` quality=high, 2 imgs — NOT nano-banana, which reads AI-stock) |
      | `characters.variant_template` + `expression_variants[]` | ~10 edit-anchored expression variants, base as SOLE ref | `render_variants.py` | 2 | **PAID** (`gpt-image-2/edit` quality=high, ~10 imgs, sequential; append `realism_suffix`, QC for hallucinated hair-lock/hands) |
      | `scenes[].still` | which still each line lipsyncs from | `render_clips.py` (reads `script.json`) | 3 | — |
      | `lipsync_engine` | Veed Fabric 1.0 via fal.ai per (still, VO) pair | `render_clips.py --gateway veed-fal --resolution 720p` | 3 | **PAID** (fal.ai, ~22 × 720p) |
      | `captions` | WHITE captions from the VO char-level timestamps (≤5-word cues, sentence-broken, word-wrapped, ≥0.9s) | `stitch.py` (PIL PNG overlays, or ASS if libass present) | 4 | free |
      | `end_card` | Playwright HTML → PNG → 2.5s mp4 from the real wordmark SVG | `build_end_card.py` | 4 | free |
      | `assembly` | concat + scale/pad + WHITE captions + append end card + final-encode crf28 | `stitch.py` | 4 | free |
      
      ## 1. Voiceover → ElevenLabs with-timestamps  (config: `voices`, `scenes[].text`)  [PAID]
      
      `working/render_vo.py` reads `script.json` and, per scene, POSTs to
      `/v1/text-to-speech/<voice_id>/with-timestamps?output_format=mp3_44100_128` with the host's
      `model` + `settings`. It saves `voiceovers/scene-NN-<who>.mp3`, the char-level
      `voiceovers/scene-NN-<who>.timestamps.json`, and a `manifest.json` summarising durations. One voice
      per host, held across the whole skit. **The char-level timestamps are what the captions
      sync to — do NOT Whisper.** Lines are intonation-marked (`...` pause, ` — ` em-dash, `?` rising,
      `CAPS` emphasis, `.` hard stop), ≤10 words, **no acronyms** (ElevenLabs reads them letter-by-letter).
      
      ## 2. Base stills + variants → gpt-image-2 (quality=high)  (config: `characters`)  [PAID]
      
      - **Two bases** (hand-generated): tight bust, broadcast mic foreground, over-ear headphones, at
        the themed desk, **mouth NEUTRAL/CLOSED** (an open-mouth still breaks the lipsync driver).
        Generate the skeptic base FIRST, then the believer using the skeptic's still as a background
        reference so the set matches.
        Generate with **gpt-image-2 quality=high** and append `characters.realism_suffix` — NOT
        nano-banana (its faces read as smooth "AI-stock" and were rejected in testing). State the
        hairstyle explicitly and coherently to avoid the asymmetric-long-lock hallucination.
      - **~10 expression variants** — `working/render_variants.py` edit-anchors each variant on its
        host's base as the **SOLE reference** (`gpt-image-2/edit`, prompt = `characters.variant_template`
        with `{change}` describing ONLY the pose/expression change — so identity + background stay across
        all cuts). Submitted **sequentially**. Idempotent (skips existing). `"narrowed eyes"` NSFW-rejects
        → swap to `"curious questioning look, eyes wide open and bright"`. Review all ~12 stills before
        step 3: every mouth closed, one continuous set, and **NO hallucinations** — a long hair-lock on
        one side that mismatches the top, extra/warped hands, morphed ears. If seen, regenerate the BASE
        (variants inherit its artifacts) then re-make its variants.
      
      ## 3. Lipsync clips → Veed Fabric 1.0 via fal.ai  (config: `lipsync_engine`, `scenes[]`)  [PAID]
      
      `working/render_clips.py --gateway veed-fal --resolution 720p` reads each scene's `(still, VO)`
      pair from `script.json` and feeds them to the fal.ai Veed Fabric generate script (model
      `veed/fabric-1.0`), writing `clips/scene-NN-<who>.mp4`. Concurrency 4; idempotent (skips existing —
      safe to re-run partial failures with `--only 20,21,22`). Empty text-prompt (a text prompt
      interferes with lipsync). **Hedra Character-3** (`--gateway hedra --model character-3`) is the
      fallback if the fal.ai balance is exhausted (server: "User is locked. Reason: Exhausted balance.").
      Its `--stills-dir` / `--clips-dir` are relative to the script's own `working/` root — pass bare
      `characters` / `clips`, not `working/characters` (else it double-prefixes).
      
      ## 4. Captions + end card + assembly → `stitch.py` + `build_end_card.py`  (config: `captions`, `end_card`, `assembly`)
      
      - `working/build_end_card.py` renders the brand lockup via **Playwright** from the REAL brand
        wordmark SVG (Ladder wordmark pulled from `run-01-app-sizzle/source/brand/ladder-wordmark-white.svg`):
        black background + lime (`#DBFF00`) wordmark + lime "Start free trial" pill + `joinladder.com` →
        an HTML file → screenshot to a 1080×1920 PNG → a **2.5s** silent mp4 at `clips/end-card.mp4`.
        **Never AI-render brand text.**
      - `working/stitch.py` walks the scenes in `script.json` order. For each scene it ffprobes the
        clip's duration and builds a **global `words.json`** — every word's time = its local char-level
        time (parsed from `scene-NN-<who>.timestamps.json`) **+ the cumulative clip start** (sum of the
        preceding clips' durations). It groups words into **≤5-word cues broken on sentence-final
        punctuation** and renders **WHITE `#FFFFFF`** bottom-center captions (black outline),
        **word-wrapped to stay in-frame** and held **≥0.9s** — as **PIL PNG overlays** when the host
        ffmpeg lacks libass (the common case), else an ASS burn. (Yellow 3-word karaoke was the old style,
        rejected in testing.) Then it concats all clips (`-f concat`, `scale=1080:1920:…:black,setsar=1`),
        **auto-appends `clips/end-card.mp4`**, and **final-encodes `libx264 -preset slow -crf 28` + aac
        96k** → the 1080×1920 h264+aac master (~6MB for a ~28s cut; the old `-crf 20` produced ~16MB).
      
      Re-cuts (new caption chunking, re-timed clips, swapped end card) reuse the existing VOs/stills/clips
      and cost **$0** — only steps 1–3 spend. The v2 master re-rendered only scenes 7 + 15 (more natural
      skeptic reads) and re-stitched — a targeted `render_vo.py` + `render_clips.py --only 7,15` + a
      free re-stitch.
      
    • README.md 3.1 KB
      # render-podcast-skit scripts — the FREE assembly
      
      `render-podcast-skit` is the **deterministic, $0 assembly stage** of the fake-podcast skit
      format. The paid stages — the per-line ElevenLabs with-timestamps VOs, the two base stills, the
      ~10 expression variants, and the per-line lipsync clips — are separate capabilities
      (`create-vo-elevenlabs`, `create-image-fal`, `create-video-fal`). This capability spends nothing:
      it takes the per-line clips + their VO timestamps + the brand wordmark and stitches the finished
      master. Re-cuts (new caption chunking, a re-timed slice, a swapped end card) reuse the existing
      clips and cost **$0**.
      
      `config.example.json` is the worked example (Ladder run-02 "Laundromat 2am", ~49s 1080×1920).
      `PIPELINE.md` maps every config block to its source step. This README documents the FREE
      assembly pieces that `render-podcast-skit` owns.
      
      ## 1. WHITE captions — from the VO's OWN char-level timestamps (script-window, NOT Whisper)
      
      Captions come from each line's ElevenLabs with-timestamps response — the char-level word timings
      returned **with** the VO — never from Whisper (Whisper on the rendered clips mistimes). The
      assembler walks the scenes in script order and builds a **global `words.json`**: each word's time
      is its local char-level time **+ the cumulative clip start** (the sum of the preceding clips'
      durations). It groups words into **≤5-word cues broken on sentence-final punctuation** and renders
      **WHITE `#FFFFFF`** bottom-center captions (black outline), **word-wrapped to stay inside the frame**
      (never off the edges) and held **≥0.9s** each. Yellow 3-word karaoke was the old style and was
      **rejected in testing** — match the `add-captions-veed-fal --preset whisper` house style. Render as
      **PIL PNG overlays** when the host ffmpeg lacks libass (the common case — check
      `ffmpeg -filters | grep subtitle`), else an ASS burn.
      
      ## 2. Per-line clip assembly, hard-concat in script order
      
      One line = one scene = one clip. The clips are hard-concatenated in script order (scale/pad to
      1080×1920, re-encode `libx264 -preset veryfast -crf 20`), so the edit cuts on the dialogue beat.
      No dissolves. Anchoring every still on one base upstream keeps the set pixel-identical across all
      ~22 cuts, so the many cuts read as one continuous podcast.
      
      ## 3. End card — Playwright/PIL from the real wordmark, no AI text
      
      The brand lockup is composited via **Playwright** from the brand's REAL wordmark SVG: black
      background + the brand wordmark + a CTA pill + the URL → an HTML file → a screenshot to a
      1080×1920 PNG → a **2.5s** silent mp4. The brand text is **never** AI-rendered — a diffusion
      model garbles a wordmark.
      
      ## 4. FFmpeg composite
      
      FFmpeg stitches the master: concat the per-line clips (scale/pad 1080×1920), overlay the WHITE
      caption PNGs (or burn the ASS via libass), and auto-append the 2.5s end-card mp4. The VOs carry the
      audio (no music bed by default). **Final-encode `libx264 -preset slow -crf 28` + aac 96k** so a
      ~28s master lands near **~6MB** (the old `-crf 20` produced oversized ~16MB files). Output is a
      1080×1920 h264 + aac master. Deterministic, no paid calls, no keys.
      
  • tests
    • smoke-test.md 1.1 KB
      # Smoke Test
      
      Given the per-line lipsync clips (one per script line), the character stills, the caption
      timing from the VO's char-level timestamps, and the brand's real wordmark,
      `render-podcast-skit` assembles the master: concat the line clips in script order, render the
      WHITE captions, composite the end card, final-encode crf28 → 1080×1920 h264+aac.
      
      Pass when the assembly runs to a valid MP4 and:
      - the dialogue lines play in script order with the two hosts alternating;
      - captions are WHITE, ≤5 words per cue (broken on sentence punctuation), word-wrapped to stay
        in-frame, held ≥0.9s, offset by each clip's cumulative start (tracking the spoken word), not
        Whisper-derived;
      - speakers' mouths are closed when not speaking (carried from the source clips);
      - the end card is composited from the real brand wordmark (no AI-rendered brand text);
      - **no paid call is made** — the VO, stills, and lipsync clips come from the paid capabilities
        (create-vo-elevenlabs / create-image-gpt-image-fal / create-video-fal); this assembly is $0 and a
        re-cut reuses the existing assets.
      
  • SKILL.md 3.9 KB
    ---
    name: render-podcast-skit
    description: Assemble a two-host fake-podcast skit ad from a config — per-line lipsync clips hard-concatenated in script order, scaled/padded to 1080×1920, WHITE bottom-center captions (up to 5 words per cue, broken on sentence punctuation, word-wrapped to stay in-frame, held at least 0.9s) built from each line's OWN ElevenLabs char-level timestamps (offset by cumulative clip start, never Whisper), and closed on a Playwright/PIL brand end card composited from the real wordmark — never AI-rendered text. This is the FREE deterministic assembly stage (concat + white captions + end card + crf28 encode); the per-line VOs, photoreal gpt-image-2 base stills, expression variants, and lipsync clips come from create-vo-elevenlabs / create-image-gpt-image-fal / create-video-fal. Use for the podcast-skit format.
    status: active
    ---
    
    # render-podcast-skit
    
    Assemble a **two-host fake-podcast skit** ad from a config: a skeptic and a believer at an
    absurd themed podcast desk do a snappy back-and-forth about the product (the set is
    deliberately unrelated — that is the joke). Each line is its own lipsync clip so the edit can
    cut on the dialogue beat (~1.8s avg); this capability is the **FREE, deterministic assembly**
    that concatenates those clips, renders the WHITE captions, and appends the brand end card.
    
    `scripts/config.example.json` is the worked example (Ladder run-02 "Laundromat 2am", ~49s
    1080×1920 9:16, ~22 lines); `scripts/PIPELINE.md` maps every config block to its source step
    and `scripts/README.md` documents the free assembly.
    
    ## Run
    
    This is the **FREE, deterministic** assembly stage — it spends nothing. The paid inputs are
    separate capabilities: one ElevenLabs **with-timestamps** VO per line (one voice per host) via
    `create-vo-elevenlabs`; two photoreal base stills at the themed desk plus ~10 expression variants
    (mouths NEUTRAL/CLOSED, **gpt-image-2 quality=high**, not nano-banana) via
    `create-image-gpt-image-fal`; and one lipsync clip per (still, VO) pair via `create-video-fal`.
    Given the per-line clips + their VO timestamps + the brand wordmark SVG, `render-podcast-skit`
    walks the scenes in script order, builds the global caption timeline, renders the WHITE captions,
    hard-concats the clips, auto-appends the end card, and final-encodes crf28 → the master. Re-cuts
    reuse the existing VOs / stills / clips and cost **$0**.
    
    ## Contract (the free assembly)
    
    - **Dialogue-carried, no music bed by default.** The per-line VO is the audio; a podcast skit
      needs no music (an optional low ambience is a taste call, off by default).
    - **One line = one scene = one hard cut, in script order.** Hard-concat the per-line clips in
      order (scale/pad to 1080×1920, re-encode) — no dissolves.
    - **Captions from the VO's OWN char-level timestamps, not Whisper (script-window).** Build a
      global `words.json` by offsetting each line's char-level word timings by the cumulative clip
      start, group into **≤5-word cues broken on sentence-final punctuation**, and render **WHITE
      `#FFFFFF`** bottom-center captions (black outline), **word-wrapped to stay in-frame** and held
      **≥0.9s** — PIL PNG overlays when the host ffmpeg lacks libass (common), else ASS. (Yellow 3-word
      karaoke was the old style, rejected in testing.) Whisper on the rendered clips mistimes; the VO
      timestamps are ground truth.
    - **End card via Playwright/PIL from the real wordmark — never AI-render brand text.** The
      lockup is a deterministic HTML → PNG → 2.5s mp4 from the brand's real wordmark SVG (black bg,
      brand wordmark, CTA pill, URL), auto-appended after the last line. A diffusion model garbles
      a wordmark.
    - **FFmpeg composite, deterministic, FREE.** Concat the clips, overlay the WHITE caption PNGs (or
      burn ASS via libass), append the end-card mp4, and **final-encode `-preset slow -crf 28` + aac
      96k** → a 1080×1920 h264+aac master (~6MB for ~28s; the old `-crf 20` produced ~16MB). No paid
      calls, no keys.
    
  • skill.meta.json 315 B
    {
      "slug": "render-podcast-skit",
      "category": "capabilities",
      "domain": "ads",
      "tags": [
        "ads"
      ],
      "installation": {
        "base_command": "npx goose-skills install render-podcast-skit",
        "supports": [
          "claude",
          "cursor",
          "codex"
        ]
      },
      "requires_skills": [
        "watch"
      ]
    }
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related