Originally validated in content-goose at
internal/gooseworks-ads/goose-talking-head-01..03/. Adapt paths to your project.
Purpose
The production line for the 9:16 fast-paced explainer-with-mascot format. Codifies the lessons from <your-project>/goose-talking-head-01 → 02 → 03 (three iterative builds of the same format).
Specifically tuned for:
- list-style explainers — "5 tools you need", "3 mistakes to avoid", "the SaaS stack in 30s"
- product / tool walkthroughs where each beat needs a logo + a sample/visual or a stand-alone text card
- founder/mascot rundowns where credibility comes from naming the actual things, not from cinematic production value
NOT the right molecule for:
- Long-form (>40s) talking head — use
molecules/podcast-clip-animated-ad - Brand spots / concept films — use
molecules/motion-graphics/create-motion-graphic-video - Pure UGC aesthetics (handheld, real apartment, no mascot) — use
molecules/ugc-ad/create-ugc-diary-ad
Inputs
<concept>— required. Free-form paragraph describing what the mascot is explaining. Should imply a list (5 tools, 3 steps, etc.).<beats>— required if<concept>doesn't enumerate. List of{label, vo_line, tool?, sample_image?, fillText?, logo?, samples?}. The molecule produces one scene per beat plus a hook + CTA. Practical max: 6 tips + hook + CTA = 8 total. More beats and the VO crowds.<character>— defaultgoose-finger-puppet.{name, anchor_reference, descriptor_block}.anchor_referenceis a path to a single high-quality reference still on solid bg.descriptor_blockis a verbatim paragraph naming every distinguishing feature (seereferences/character-descriptors.md).<background_style>— defaultcozy-library. Seereferences/background-presets.mdfor the 4 shipped presets.<voice>— required.{voice_id, model?, settings?}. Defaultmodel: "eleven_v3"with stability 0.34, similarity 0.80, style 0.18. Voice ID must be inVOICES.mdOR explicitly approved.<tools>— required for tool scenes. List of{name, logo_path, sample_path?, samples?: [array], fillText?, accent_color?}. NEW v03 behavior: if no real sample image makes sense (e.g. "get credits", "ask claude", "use ffmpeg"), passfillTextinstead — the spotlight renders a clean beige card with brown text. If a tip has multiple samples (e.g. "look at this and this and this"), passsamples(array) — the molecule will split that beat into N sub-overlays timed to the VO's word boundaries.<music_bed>— optional. Path to instrumental track. If absent, molecule emits a TBD warning.<output_dir>— destination project folder, e.g.<your-project>/goose-talking-head-NN/.<reuse_anchor>— optional path to a previously-locked anchor. When set, skips S3 (anchor lock) entirely. Saves $0.24 + ~30s. All three shipped projects (v01/02/03) used the same anchor, so this is the common case for series content.<reuse_lipsync_drafts>— optional bool, defaultfalse. Iftrue, builds a cheap draft by concatenating previous projects'talking-head-master.mp4files trimmed to v03's duration, instead of calling Veed Fabric. The goose's mouth motion won't match the new VO phonemes but stays alive. Use for fast iteration on layout/overlays before committing to real lip-sync. See "Cheap-draft mode" below.<auto>— bool, defaultfalse. Iftrue, passes through human gates with prior operator preferences.
Composed Atoms
atoms/voiceover/create-voiceover-elevenlabs—eleven_v3with audio tags for the main VOatoms/image-generation/create-image-gpt-image-fal— character anchor; preserves identity on textured/felt charactersatoms/image-generation/create-image-nano-banana-fal— fallback for cartoon/illustrated mascots onlyatoms/lipsync/create-lipsync-veed-fal—veed/fabric-1.0at 720p, always 720p (480p reads soft in PIP)atoms/overlays/render-video-overlay-track— Playwright renderer fortool-spotlight+kinetic-title-cleanoverlaysatoms/captions/burn-in-captions—frosted-subtledefault; pair with a brand-spelling patch stepatoms/assembly/stitch-videos-ffmpeg— per-scene composite + concat + audio mix (VO + SFX pops viaadelay + amix)
In addition the molecule uses the ElevenLabs sound-generation API directly for a single pop SFX mixed at every tip boundary (see references/generate-sfx.sh), and the control-plane scripts at scripts/video_control_plane/{init_project,record_asset,record_state,validate_project}.py.
Workflow
The molecule walks a 9-state machine. Each state's contract and outputs are defined below.
S0 INTAKE → S1 SCRIPT LOCK* → S2 RAW MATERIALS → S3 ANCHOR LOCK* → S4 LIP-SYNC
↓ ↓
S8 DELIVER* ← S7 CAPTIONS ← S6 COMPOSITE + PROMOTE* ← S5 OVERLAYS ← ─ ─ ─ ─ ─ ─┘
* = human gate
S0 — Intake
- Validate
<character.anchor_reference>exists. - Validate
<voice.voice_id>againstVOICES.md; if missing AND not explicitly approved, FAIL. - Validate every
<tools[].logo_path>and<tools[].sample_path>if provided. - Run
init_project.py --video-folder <output_dir> --title "<title>" --format 9x16. - Copy
<character.anchor_reference>to<output_dir>/raw-materials/reference/. - If
<reuse_anchor>is set, copy it directly to<output_dir>/generated/character-lock/anchor-portrait.png(+.meta.jsonandLOCKED.json) and mark S3 pre-approved. - Write
idea-brief.mdanddesign-brief.md(templates inreferences/intake-templates.md).
S1 — Script lock (GATE 1)
- Author
script/script-locked.mdwith three views: clean VO, tagged VO (light[curious],[deliberate],[emphasized],[reassuring]), per-scene mapping table. - Budget: ~60 words ≈ 17–22s clean; with v3 tags expands to ~24–32s. Plan for the longer end.
- HUMAN GATE. Any change after this gate invalidates every downstream artifact.
- Sequential reveal pattern: if a beat says "this... and this... and this" (or any N-reveal pattern), keep it as ONE script line — the molecule will derive sub-overlay timings from word-level Whisper timestamps in S5.
S2 — Raw materials
- VO (main thread):
eleven_v3withwith-timestampsendpoint, voice settings from<voice>. Saveaudio/voiceover.mp3+audio/voiceover_timestamps.json. - Slice into per-scene segments: parse char-level timestamps, find each scene's start anchor (e.g. "One —", "Two —", "Three —"), compute end as the previous-char-of-next-scene-start, slice at silence-midpoints. Write
audio/scene_timing.jsonANDaudio/segments/plan.json(withtip1SubBoundariesif any beat has multi-reveal). - SFX: run
references/generate-sfx.shto generate ONE pop sound via ElevenLabs sound-generation API. Save toaudio/sfx-pop.mp3. ~$0.05. - Tool assets (sub-agent if not pre-provided): brief asks for official transparent PNG logo + ONE clean hero image per tool. Hard rule for samples: no promotional collages with text baked in — they fight the spotlight card layout.
S3 — Anchor lock (GATE 2)
- Skip if
<reuse_anchor>is set. Just mark approved. - Build prompt from character descriptor + background preset (see
references/character-descriptors.md+references/background-presets.md). - Generate 3 takes in parallel via
gpt-image-1/edit-imageat medium quality (~$0.24 total). - HUMAN GATE: operator picks one. Copy chosen to
generated/character-lock/anchor-portrait.png. WriteLOCKED.jsonwithchain_strategy: "anchor-only".
S4 — Lip-sync
- If
<reuse_lipsync_drafts>is true: skip S4 entirely. In S6 the composite uses a concat of prior projects' masters instead. Marklipsync_method: "reused-draft"in state. Use for cheap layout iteration only. - Sequential, with 3s pauses. Do NOT parallelize. The Veed Fabric storage upload endpoint (
rest.fal.ai/storage/auth/token) 403s under high parallel load if the account has churned through many calls recently. Sequential with 3s sleep is reliable. - 7 calls (one per scene segment) at
--resolution 720p. ~$0.15/sec, so ~$5 per 30s ad. Wall time ~25 min. - Sub-overlays don't need their own lip-sync clip — the same scene-NN.mp4 covers all sub-reveals; only the overlay PNG swaps in S5.
S5 — Overlays
- Write
production/overlay-plan.raw.json. One entry per overlay segment:- Hook + CTA →
kinetic-title-cleanwith{upper, lower, accent: "upper"|"lower"|"both"}. NOcaptionfield — captions are now burn-in (S7). - Tool scenes →
tool-spotlightwith EITHERsamples: [path...](real images) ORfillText: "SHORT POSTER TEXT"(beige card). NEVER both. - Multi-reveal beats (e.g. "look at this... and this... and this") → split into N sub-overlays. First sub has
noSfx: false(default — gets the pop), subs 2..N havenoSfx: true. Sub timings come from char-level "this" word timestamps in voiceover_timestamps.json.
- Hook + CTA →
- Inline-encode logo + sample paths as data: URIs via
references/inline-overlay-images.py. - Render via
node atoms/overlays/render-video-overlay-track/scripts/render-previews.js --plan <path> --out generated/overlays --width 1080 --height 1920 --transparent. - Layout invariants (the whole point of the v03 redesign):
- Sample/fill zone: top ~6% to ~51% of frame
- Chip: top ~53% (logo + tool name, on a cream pill)
- Goose PIP zone (composited later, NOT in overlay PNG): bottom ~58% to ~97%
- Overlay PNG occupies only the top half — bottom half is reserved for the lip-sync PIP. Verify: open any tool overlay PNG, the bottom 40% must be transparent.
S6 — Composite + promote (GATE 3)
- Stitch the 7 lip-sync clips:
ffmpeg concat → scale=-2:1920,crop=1080:1920 → talking-head-master.mp4. (Veed returns 768×1152; this step puts it at exact 1080×1920 9:16.) - Cheap-draft path: if
<reuse_lipsync_drafts>, concat prior projects' masters and trim to current VO duration (see "Cheap-draft mode" below) — skip step 1. - Run
references/composite.py. For each overlay:kinetic-title-clean(hook/CTA): full-frame goose + transparent title PNG.tool-spotlight: blur+dim backdrop (boxblur=30:1, eq=brightness=-0.22:saturation=0.55) + sharp goose PIP at 444×790 centered horizontally, bottom-aligned at y=1100 (PIP center is in the lower-third "safe area" the storyboard reserves) + transparent overlay PNG on top.
- Audio: mix VO directly with SFX pops via
[1:a]adelay=BOUNDARY|BOUNDARY,volume=0.55[sN]; [0:a][s0][s1]...amix=inputs=N+1,volume=1.2[aout]. SFX skipped for any overlay withnoSfx: true. The first scene also skipped (no pop on frame 1). - Concat per-scene composites → mux mixed audio →
edits/master-final-v<N>.mp4. Promote bycp v<N>.mp4 master-final.mp4. Never writemaster-final.mp4directly. - HUMAN GATE. Operator approves the pre-captions video.
S7 — Captions
- Run
atoms/captions/burn-in-captions/scripts/caption.py --video edits/master-final-v<N>.mp4 --style frosted-subtle. fal.ai Whisper transcribes, ASS template renders, libass burns. - Always run the brand-spelling patch step afterward via
references/patch-captions.py. The patch handles:- MarginV (caption Y-position): default 0.18 × 1920 = 345px from bottom. For the talking-goose-creator-video layout, drop this to ~295px (50px lower than default) so captions don't cover the goose's mouth in PIP.
- Brand-name fixes: each known mishearing (e.g. "Hicksfield" → "Higgsfield"). Add per-project mappings in the patch script's top-level config block.
- Whisper word splits: compound brand names like "Higgsfield" get tokenized as "Higgs" + "Field" by Whisper. The patch script merges them back (both same-line and cross-cue cases) with summed karaoke kf timings to preserve the fill effect.
- Re-burn the patched ASS with
ffmpeg -i precaption.mp4 -vf "ass=patched.ass" -c:v libx264 -crf 20 -c:a copy patched.mp4. - Always keep both
master-final-v<N>-precaption.mp4AND the.captions/sidecar dir. Lets you re-render captions with different style/wording without re-rendering the composite.
S8 — Deliver (GATE 4)
- Optional:
molecules/platform-variants/*for 1:1 / 16:9 reformats. - HUMAN GATE. Operator confirms ship.
- Record final cost in
.control-plane/project-state.jsonspendUsd.
Layout reference (the v03 redesign)
9:16 frame · 1080 × 1920
┌──────────────────────────────────┐
│ │ ← top 5% breathing room
│ ┌──────────────────────────┐ │
│ │ │ │
│ │ SAMPLE IMAGE │ │ ← ts-sample: top:120 height:860 (45%)
│ │ or │ │ beige card if no sample
│ │ 3-IMAGE GRID │ │
│ │ or │ │
│ │ beige fillText card │ │
│ │ (#F4DEB0 bg, #5A3D1F) │ │
│ │ │ │
│ └──────────────────────────┘ │
│ ┌───────────────────────────┐ │
│ │ [logo] TOOL NAME │ │ ← ts-chip: top:1010 (53%)
│ └───────────────────────────┘ │
│ │
│ ┌──────────┐ │
│ │ │ │
│ │ GOOSE │ │ ← PIP composited at video stitch:
│ │ PIP │ │ x=318, y=1100, 444×790 (9:16)
│ │ (lipsync)│ │ centered, white border, drop shadow
│ │ │ │
│ └──────────┘ │
│ │ ← bottom 2% breathing room
│ ┌─────────────────────────┐ │
│ │ word-level caption pill │ │ ← burn-in captions (S7)
│ └─────────────────────────┘ │ MarginV=295 (lower than default)
└──────────────────────────────────┘
Cheap-draft mode
For fast layout iteration without burning $5 + 25 min on Veed Fabric every run:
- Concat one or more prior projects'
talking-head-master.mp4files using ffmpeg's concat demuxer. - Trim to current VO duration:
-t <vo_duration>. - Drop the inherited audio (
-an) — we'll add our own VO + SFX in S6. - Save as
edits/talking-head-master.mp4and run S6 composite normally.
Result: goose's mouth moves throughout (mouth shapes match the OLD VO, not the new one) but the layout/overlays/captions are real. Good enough for review and storyboard. Re-run S4 properly for the final.
Cost saved: ~$5/run. Time saved: ~25 min.
Output
<output_dir>/
├── idea-brief.md
├── design-brief.md
├── script/
│ └── script-locked.md
├── raw-materials/
│ ├── reference/ # character anchor source still
│ ├── logos/ # transparent PNGs per tool
│ ├── samples/ # one hero image per tool (or N for multi-reveal beats)
│ └── SOURCES.md
├── audio/
│ ├── voiceover.mp3
│ ├── voiceover_timestamps.json # char-level from ElevenLabs
│ ├── scene_timing.json # per-scene start/end + tip1SubBoundaries
│ ├── sfx-pop.mp3 # single pop, mixed at every tip boundary
│ └── segments/
│ ├── scene-01.mp3 .. scene-07.mp3
│ └── plan.json
├── generated/
│ ├── anchor-options/ # all takes from S3 (kept for traceability)
│ ├── character-lock/
│ │ ├── anchor-portrait.png
│ │ └── LOCKED.json
│ └── overlays/
│ └── scene-NN-*.png # one transparent overlay per overlay segment
├── clips/
│ └── scene-NN.mp4 # per-scene lip-synced 720p (or reused-draft)
├── edits/
│ ├── talking-head-master.mp4 # 7 lip-sync clips stitched, 1080x1920
│ ├── segments/ # per-scene composited
│ ├── master-final-v<N>.mp4 # versioned masters
│ ├── master-final-v<N>-precaption.mp4 # before burn-in-captions (re-burnable)
│ ├── master-final-v<N>-precaption.captions/
│ │ ├── words.json # Whisper word-level
│ │ ├── captions.{srt,ass} # raw sidecars
│ │ └── *__frosted-subtle__{ass,fixed}.ass # rendered + brand-patched
│ └── master-final.mp4 # promoted current
├── production/
│ ├── scene-contract.json
│ ├── overlay-plan.raw.json # file paths (editable)
│ ├── overlay-plan.json # data: URIs (renderer-ready)
│ ├── audio-plan.json
│ └── asset-manifest.json
└── .control-plane/
└── project-state.json
Quality Checks
- All 9 states either ran or were explicitly skipped (with reason in state file).
- Every
clips/scene-NN.mp4duration is within ±10% of its audio segment (if<reuse_lipsync_drafts>is false). generated/character-lock/LOCKED.jsonexists.- Overlay PNGs: bottom 40% of every TOOL spotlight PNG is transparent (PIP zone uncovered).
edits/master-final.mp4exists, is 1080×1920, h264 + AAC, duration matchesaudio/voiceover.mp3±0.5s.- Captions don't cover the goose's mouth in PIP — MarginV ≥ 275px from bottom.
- Every known brand name in the script appears spelled correctly in the burned-in captions (visual review).
- The
.captions/sidecar dir is preserved.
Failure Modes
| Symptom | Likely cause | Fix |
|---|---|---|
| Veed Fabric 403 on storage auth | Too many parallel calls recently | Drop to sequential with sleep 3 between calls (see S4 step 2) |
| FAL "User is locked. Exhausted balance" | Account credits depleted | Top up at fal.ai/dashboard/billing. The error is clear in the response body. |
| Whisper hears brand name wrong (e.g. Hicksfield) | Compound name with uncommon morphology | Add to patch-captions.py REPLACEMENTS dict; brand-name patching is now a default S7 sub-step |
| Whisper splits brand into 2 words ("Higgs" + "Field") | Same compound-name issue | patch-captions.py handles both same-line and cross-cue merges with combined karaoke kf timings |
| Captions cover the goose's mouth | Default MarginV=345 puts captions at ~72% Y; goose PIP head is at ~70% Y in this layout | patch-captions.py drops MarginV to 295 by default for this molecule |
| Sub-overlay reveal lands BEFORE the word | Slice timing didn't include lead-in | Add 0.15s lead-in past each "this" word: cuts.append(this_end + 0.15) |
| Tool name wrapping ugly in chip | Multi-word tool name | Shorten to one word + · separator (e.g. "Higgsfield · Marketing Studio" → just "Higgsfield" with marketing in eyebrow) |
| Caption shows "Higgs" alone at end of cue | Whisper tokenization split across cue boundary | The cross-line merge in patch-captions.py handles this — verify it's enabled |
| Cheap-draft master is shorter than VO | v01+v02 concat = 56s; trim to VO duration | Already handled — -t <vo_duration> flag. If your VO is longer than the sum of prior masters, add more prior projects to the concat. |
| Overlay PNG covers the goose PIP | New tool-spotlight component edited away the 40% bottom transparency | Verify CSS: .ts-sample height ≤ 860px out of 1920 (~45%); .ts-chip top ≤ 1080 |
| Character drift on every GPT-image take | Descriptor block missing distinctive shape/proportion words | Add specifics: "tall and slender, NOT chubby"; "long droopy tube beak, NOT triangular bird beak"; "round white felt eyes SEPARATED, NOT touching" |
| Composite errors on sub-overlay scenes | Scene segment in PLAN["overlays"] has noSfx AND the script doesn't skip it correctly |
Verify composite.py build_audio_with_sfx continues on overlay.get("noSfx") |
Cost reference (per typical 30s run, real lip-sync)
| Line item | Provider | Unit | Count | Cost |
|---|---|---|---|---|
ElevenLabs VO (eleven_v3 w/ timestamps) |
ElevenLabs | per char | ~500 | ~$0.06 |
| ElevenLabs SFX pop (sound-generation) | ElevenLabs | per request | 1 | ~$0.05 |
Character anchor takes (skipped if <reuse_anchor>) |
FAL gpt-image-1/edit-image medium |
$0.08/img | 3 | ~$0.24 (or $0 if reused) |
| Veed Fabric lip-sync (720p, sequential) | FAL veed/fabric-1.0 |
$0.15/sec | 30s | ~$4.50 |
| Burn-in-captions Whisper transcription | fal.ai Whisper | per minute | 0.5 min | ~$0.005 |
| Playwright overlay rendering | local | — | 10 | $0 |
| ffmpeg composite + caption burn | local | — | — | $0 |
| Total (with reused anchor) | ~$4.65 | |||
| Total (fresh anchor) | ~$4.90 | |||
| Cheap-draft mode (no lipsync) | ~$0.15 |
When to escalate
- 3 review iterations completed and operator still flagging P0s: escalate to user with punch list.
- Character identity drift > 30% on 6+ takes: surface Soul ID training option.
- Voice ID isn't on
VOICES.mdAND operator hasn't approved: stop at S0 and ask. - FAL balance exhausted mid-run: surface immediately with top-up link.
Decision Rules
- Hard cap practical beats at 6 tips + hook + CTA = 8 total scenes. More beats and the VO crowds.
- Prefer
gpt-image-1/edit-imagefor the character anchor on felt/textured mascots; only fall back to Nano Banana for cartoon/illustrated characters. - Reuse a previously-locked anchor whenever the same mascot is being used (
<reuse_anchor>skips S3 entirely — saves ~$0.24 + 30s). - Run Veed Fabric lip-sync calls sequentially with a 3s sleep between calls — parallel calls 403 from the storage auth endpoint.
- Always pair
burn-in-captionswith a brand-spelling patch step; Whisper mishears compound brand names (e.g. Higgsfield → "Hicksfield" / "Higgs Field"). - Tool-spotlight overlay PNG must keep the bottom 40% transparent so the goose PIP is uncovered. Verify before composite.
- Use
<reuse_lipsync_drafts>for cheap layout iteration only — never ship a draft where mouth motion doesn't match the new VO.
Reference runs
<your-project>/goose-talking-head-01/— first build of the format. Old-style overlay (cream card with eyebrow + tagline + sample inside). No SFX, no burn-in captions.<your-project>/goose-talking-head-02/— reused v01 anchor. Same old overlay style. First validation that anchor inheritance saves time.<your-project>/goose-talking-head-03/— the v03 redesign. Clean two-zone layout, beige fillText cards, sequential multi-reveal, SFX pop on tip transitions, burn-in word-level captions with brand-spelling patches. This is the canonical reference for new runs — see the per-projectHOW_TO.md.