Purpose
The production line for the 9:16 split-screen creator-ad format: an AI
creator narrates while product/demo footage plays above them. Codifies the
build proven by perplexity/ads/concept-10-bloomberg-terminal and its sibling
concepts (03, 07, 11).
Every scene is the same two-zone vertical split:
9:16 frame · 1080 × 1920
┌──────────────────────────────────┐
│ │
│ TOP ZONE (1080 × 998) │ ← the scene's product/demo clip
│ product / demo clip │ CONTAIN-fit; letterbox margins
│ or the hook graphic │ filled with a darkened blurred
│ (blurred-fill, never bars) │ cover-scale of the same visual
│ │
├───────────────────────────────────┤ ← 3px brand-color divider
│ │
│ BOTTOM ZONE (1080 × 919) │ ← the AI creator, lip-synced to
│ AI creator talking head │ the VO, COVER-fit (crop biased
│ (lip-synced to the VO) │ to top so the face stays framed)
│ │
│ [ KINETIC CAPTIONS ] │ ← burn-in-captions, kinetic-pop
└──────────────────────────────────┘
→ then a full-frame end card
Use it for: creator demos, "I tried X" reaction ads, feature walkthroughs, side-by-side proof ads — anywhere a talking creator should narrate over live product footage.
NOT the right molecule for:
- A felt-puppet / mascot explainer with text cards on top — use
molecules/talking-goose-creator-video. - A full-bleed single-shot UGC diary ad (no split, no top clip) — use
molecules/ugc-ad/create-ugc-diary-ad. - A voiceless full-frame motion ad — use
molecules/ugc-ad/create-programmatic-motion-video-ad.
Inputs
<brief>— required. What the ad sells, the audience, the platform, the approved claims/CTA. If a script already exists, pass it.<scenes>— required if<brief>doesn't already enumerate. An ordered list of{vo_line, top_visual, top_start?, top_end?}. One scene = one VO sentence/idea + one top-zone visual. Practical range: 5-7 scenes — fewer and it's thin, more and a <45s ad crowds.<creator>— required. The bottom-zone talking head. Either:{anchor_reference}— a path to one high-quality AI-creator portrait still (drives the anchor → lip-sync path), or{ugc_avatar}— a turnkey UGC avatar id (HeyGen / Creatify / Arcads).
<voice>— required.{voice_id, model?, settings?}. Voice ID must be inVOICES.mdor explicitly approved. Default modeleleven_v3.<top_clips>— required. Brand-owned product/demo clips for the top zone. If the brand has its own footage, build the library first withmolecules/brand-research/build-brand-clip-libraryand pass the indexed clips here. The hook scene is usually a designed graphic, not a clip.<end_card>— required. A logo end-card clip or a static brand image.<brand>—{accent_color, divider_color}.accent_colordrives the caption punch words;divider_coloris the 3px zone divider.<output_dir>— destination project folder.<auto>— bool, defaultfalse. Iftrue, passes human gates with prior operator preferences.
Composed Atoms
atoms/voiceover/create-voiceover-elevenlabs— the VO, with thewith-timestampsendpoint so per-scene timing can be computed.atoms/image-generation/create-image-gpt-image-fal— generate/lock the AI-creator anchor portrait (anchor → lip-sync path).atoms/lipsync/create-lipsync-veed-fal—veed/fabric-1.0, lip-sync the creator to the full VO. (Alternative talking-head source:atoms/ugc-generation/create-ugc-heygen/create-ugc-creatify/create-ugc-arcads— a turnkey avatar that speaks the script directly.)atoms/captions/burn-in-captions— thekinetic-popstyle: kinetic creator-native captions, word-for-word with the VO, accent color on the punch word.atoms/end-cards/create-end-card-ffmpeg— build the end card if one was not supplied.atoms/review/watch— frame-grounded QC review of the assembled cut.
See also: molecules/brand-research/build-brand-clip-library (source the
top-zone clips), atoms/editing/trim-video-clips (cut clips to length).
This molecule also ships references/assemble.py — the deterministic
two-zone assembler (blurred-fill top, divider, cover-fit creator, per-scene
clip windowing, end-card trim, optional ASS burn). It is driven by one
ad-spec.json (schema in references/ad-spec.md).
Workflow
S0 — Intake
- Validate
<creator.anchor_reference>(or<creator.ugc_avatar>) exists. - Validate
<voice.voice_id>againstVOICES.md; if missing and not explicitly approved, STOP and ask. - Validate every
<top_clips>path and the<end_card>file. - Create
<output_dir>withvoiceover/,avatar/,assets/,captions/subfolders.
S1 — Script + scene plan lock (GATE 1)
- Write
script/script-locked.md: the clean VO, and a per-scene mapping table — each row isscene # · VO line · top-zone visual · notes. - Budget: ~6 scenes ≈ 35-45s. The hook scene (scene 1) is usually a designed stat/claim graphic; the rest are product/demo clips.
- For every scene, name the exact top visual and confirm it shows what the VO line says (this is the #1 quality lever — see Decision Rules).
- HUMAN GATE. Any change after this invalidates downstream artifacts.
S2 — Voiceover
- Run
create-voiceover-elevenlabswith thewith-timestampsendpoint and the chosen<voice>. Savevoiceover/vo.mp3+ the timestamp JSON. - Apply any speed adjustment (creator-native VO often reads ~1.15-1.2×).
- Slice into per-scene boundaries from the character timestamps; write
voiceover/timing.json—{"scenes":[{"scene":N,"start":s,"end":e}]}.
S3 — Creator talking head
- Anchor path: generate 2-3 AI-creator portrait takes with
create-image-gpt-image-fal, lock one, then runcreate-lipsync-veed-falon the full VO → one creator video covering the whole ad. - UGC-avatar path: drive
create-ugc-*with the script + voice → the talking-head video directly. - Lip-sync must cover the entire VO duration in one continuous take — the
assembler slices it per scene by
timing.json.
S4 — Top-zone visuals + end card
- Gather the product/demo clips for scenes 2..N (from the brand clip library). Each clip stays 16:9 — the assembler contain-fits it.
- Render the hook graphic for scene 1 (a designed stat/claim card).
- Confirm or build the end card (
create-end-card-ffmpeg).
S5 — Assemble (GATE 2)
- Write
ad-spec.json(schema:references/ad-spec.md): canvas, divider color, the creator lip-sync video,timing.json, the per-scenetopvisuals, and theendcard. Leavecaptions_assunset for now. - Run
python3 references/assemble.py ad-spec.json→ the precaption cut. - Review the precaption cut with
atoms/review/watch. Check, per scene: does the top clip show what the VO says? Are there black bars? Does the last frame end clean? Fix per the Decision Rules and re-assemble. - HUMAN GATE. Operator approves the precaption cut.
S6 — Kinetic captions
- Run the
burn-in-captionsatom on the assembled precaption video:caption.py --video <precaption>.mp4 --style kinetic-pop --accent "<brand.accent_color>". It transcribes the cut's own audio (fal Whisper, word-level), builds kinetic chunks, and burns them. - Transcribe the assembled video, not the raw VO file — concat drops the inter-scene silence, so the ad timeline differs from the VO timeline. Only the final cut's audio yields correctly-synced caption timing.
- Optionally pass
--emphasis "<brand>,<product>,<numbers>"to force the punch words. For a hand-tuned caption track, build the ASS yourself and setcaptions_assinad-spec.jsonsoassemble.pyburns it directly. - Promote the captioned render to
<output_dir>/<name>-final-ad.mp4. Keep the-precaption.mp4and the.captions/sidecar for re-caption passes.
S7 — Review + deliver (GATE 3)
- Run
atoms/review/watchon the final captioned ad. Verify the Quality Checks below. - Write
manifest.json(the Output Manifest Standard). - HUMAN GATE. Operator confirms ship. This molecule stops before any publishing step.
Decision Rules
- Top clip shorter than its scene → do NOT loop it (looping replays into
sparse/black tails). Set
top_start/top_endto the on-message window;assemble.pyspeed-fits that window to the scene length. - Top clip content drifts from the VO line → reseek with
top_startso the relevant moment lands under the words, or swap the clip. The top visual must match what the creator is saying — flag every mismatch in S5 review. - Hook scene → use a designed graphic (stat/claim card), not raw footage.
- End-card clip fades to black → set
endcard.clip_endto the last sharp second; the assembler clone-holds that frame so the ad never ends on a black rectangle. - Captions cover the creator's face →
kinetic-popalready sits low (margin_v_pct0.12); if a specific creator is framed high, lower further. - Whisper mis-splits numbers/brand names (e.g.
$25+,000) → merge in a caption patch pass or pass--emphasisand correct the.asssidecar. - Prefer composed atoms over re-implementing provider calls in this molecule.
- Keep all three human gates.
Output
<output_dir>/
├── script/
│ └── script-locked.md
├── voiceover/
│ ├── vo.mp3
│ ├── vo_timestamps.json # char-level from ElevenLabs
│ └── timing.json # per-scene start/end
├── avatar/
│ └── creator-lipsync.mp4 # full-VO lip-sync take
├── assets/
│ └── scene-01-graphic.png # hook graphic
├── captions/
│ └── karaoke.ass # kinetic-pop ASS (sidecar)
├── ad-spec.json # assembler input
├── <name>-final-ad.mp4-precaption.mp4 # before captions (re-burnable)
├── <name>-final-ad.mp4 # delivered ad
└── manifest.json
Quality Checks
- Final video is
<canvas>(default 1080×1920), h264 + AAC, 30fps; duration matches the VO + end card within ±0.5s. - Two-zone layout holds every scene; the divider is visible; the top zone has no black bars (blurred-fill present).
- Every top clip plausibly matches its VO line (S5 review confirmed it).
- Captions are word-for-word with the VO, kinetic, accent color on punch words, and do not cover the creator's face.
- The final frame is a clean end card — no black rectangle, no fade-to-black artifact.
- No clip loops into a sparse/black tail.
manifest.jsonexists and itsoutput_filesresolve.
Failure Modes
| Symptom | Likely cause | Fix |
|---|---|---|
| Veed Fabric 403 on storage auth | Too many parallel lip-sync calls | Run lip-sync sequentially with sleep 3 between calls |
| FAL "User is locked / exhausted balance" | FAL credits depleted | Top up at fal.ai billing; the error body is explicit |
| Black bars around the top clip | Assembler not used, or charcoal-pad fallback | Use references/assemble.py; it blurred-fills the letterbox |
| Top clip goes black/sparse mid-scene | Clip shorter than the scene, looped | Set top_start/top_end; the assembler speed-fits the window |
| Last frame is a black rectangle | End-card clip fades to black | Set endcard.clip_end to the last sharp second |
| Captions out of sync with the VO | Transcribed the raw VO file, not the cut | Transcribe the assembled video — concat drops inter-scene silence |
| Caption text doesn't match the VO words | Used an old/paraphrased script | kinetic-pop transcribes the actual audio; never hand-type caption text |
Numbers split mid-caption ($25 / ,000) |
Whisper word tokenization | Patch the .ass sidecar; merge the tokens |
| Creator face cropped oddly in the bottom zone | Lip-sync framing too tight | Re-frame the anchor portrait (head-and-shoulders, centered) |
Reference build
perplexity/ads/concept-10-bloomberg-terminal/ — the canonical build of this
format: 6 scenes, AI creator (Arlo voice) over Perplexity product clips,
kinetic captions, logo end card. Concepts 03/07/11 are sibling builds.