| name | create-split-screen-creator-ad |
| description | Produce a 9:16 split-screen creator ad — a two-zone vertical layout with a product/demo clip on top (contain-fit, blurred-fill, no black bars) and an AI-creator talking head lip-synced to the voiceover on the bottom, joined by a thin brand divider, with kinetic creator-native captions burned in and a clean end card. Each scene pairs one VO line with one top-zone visual. Use when a brand wants a UGC-style "creator demos/reacts to the product" short-form ad — the format proven by perplexity/ads/concept-10-bloomberg-terminal (and concepts 03/07/11). Orchestrates ElevenLabs VO + an AI-creator anchor or UGC avatar + Veed Fabric lip-sync + a two-zone ffmpeg assembler + the burn-in-captions atom (kinetic-pop style). Stops before publishing. |
create-split-screen-creator-ad
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 in
VOICES.md or explicitly approved. Default model eleven_v3.
<top_clips> — required. Brand-owned product/demo clips for the top zone.
If the brand has its own footage, build the library first with
molecules/brand-research/build-brand-clip-library and 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_color drives the
caption punch words; divider_color is the 3px zone divider.
<output_dir> — destination project folder.
<auto> — bool, default false. If true, passes human gates with prior
operator preferences.
Composed Atoms
atoms/voiceover/create-voiceover-elevenlabs — the VO, with the
with-timestamps endpoint 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 — the kinetic-pop style: 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> against VOICES.md; if missing and not
explicitly approved, STOP and ask.
- Validate every
<top_clips> path and the <end_card> file.
- Create
<output_dir> with voiceover/, 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 is scene # · 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-elevenlabs with the with-timestamps endpoint and
the chosen <voice>. Save voiceover/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 run create-lipsync-veed-fal
on 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-scene top
visuals, and the endcard. Leave captions_ass unset 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-captions atom 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
set captions_ass in ad-spec.json so assemble.py burns it directly.
- Promote the captioned render to
<output_dir>/<name>-final-ad.mp4. Keep
the -precaption.mp4 and the .captions/ sidecar for re-caption passes.
S7 — Review + deliver (GATE 3)
- Run
atoms/review/watch on 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_end to the on-message window;
assemble.py speed-fits that window to the scene length.
- Top clip content drifts from the VO line → reseek with
top_start so
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_end to 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-pop already sits low
(margin_v_pct 0.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 --emphasis and correct the .ass sidecar.
- 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.json exists and its output_files resolve.
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.