| name | create-cinematic-music-video |
| description | Produce a 20-30s cinematic music-video ad for any brand from a single concept.json. Original anthem (vocals + word timestamps) → 14 beat-locked tableaux → image-to-video → cinematic captions → final MP4. One-shot driver wraps every step; ten reusable atoms compose underneath. Use for fashion/lifestyle/beverage/CPG brands where craft + emotional arc beat "lookbook montage." Reference runs the design comes from — Gorgie "Club Gorgie" (paper-cut), Hype and Vice "Game Day Girls" (35mm Kodak Portra), Hype and Vice "Night Game" (CineStill 800T neon-noir). |
create-cinematic-music-video
Originally validated in content-goose at clients/gorgie/ad-runs/run-02-club-gorgie-music-video/, clients/hype-and-vice/ad-runs/run-01-game-day-girls/, and clients/hype-and-vice/ad-runs/run-02-night-game/. Adapt paths to your project.
Purpose
Turn a concept.json into a beat-locked 20–30 s cinematic music-video ad with original vocals, brand-correct visuals, and burned-in captions. The molecule generalizes everything we learned across the Gorgie + Hype and Vice reference runs into one pipeline that a fresh session can invoke against a new brand in ~30 minutes wall-clock with ~$15-25 of paid gen credits.
Three pillars make this format work:
- Original anthem with word timestamps. ElevenLabs Music API
--with-timestamps gives 80–90 word-level anchors per 28 s song. Every visual cut lands on a vocal beat. This is the difference between "music video" and "stock music with cuts."
- 14-tableau structure with assigned narrative roles. Not a montage. A 3-act arc — anticipation → energy → reflection. Each tableau has a job (intro / wide / kinetic-close / hook / product / origin / outro).
- Look pack as the swap point for aesthetic. Kodak Portra warm-day, CineStill 800T neon-night, paper-cut craft — same molecule, three completely different finished products. Adding a new pack is a one-file PR.
The output is platform-ready: 1080×1920 9:16, 30 fps, h264 + aac, loudnorm -14 LUFS, captions burned, ~20-30 MB.
When to use this molecule
Use when:
- Brand has cultural ownership of a setting / moment / vibe (tailgate, energy-drink-3pm, summer-friday)
- Format needs to deliver emotional payoff, not feature callouts
- Spot length 20-30s for paid social (TikTok, Reels, Shorts, Pinterest video pin)
- Music can be original (ElevenLabs Music) — i.e. not licensed-music-required
- Brand has a strong visual signature compatible with one of the existing look packs (or you'll add a new one)
Do NOT use for:
- UGC talking-head ads (use
ugc-ad/create-ugc-creatify or ugc-ad/create-ugc-higgsfield)
- Motion-graphic kinetic typography (use
motion-graphics/create-motion-graphic-video)
- Sub-15s short cuts (use
ugc-ad/create-shortform-cuts-from-video after a master ships)
- Photoreal product sizzle (use
product-video/create-product-sizzle-reel)
- When licensed music is mandatory (this molecule generates original)
Inputs
A single concept.json (schema below) plus:
- Brand brief (
shared/brand-brief.md in the brand's workspace)
- Lyrics in
source/lyrics-locked.md (also a content_field in concept.json so you don't have to keep them in sync)
- Look pack selection (one of
KODAK_PORTRA_DAY | CINESTILL_800T_NIGHT | PAPER_CUT_CRAFT | a custom one you add)
Environment:
ELEVENLABS_API_KEY for music generation
- Higgsfield CLI authenticated for keyframes + i2v
imageio-ffmpeg installed via pip for libass-enabled caption burn (Homebrew ffmpeg lacks libass on macOS — known issue, see Workarounds below)
concept.json schema
{
"brand": "hype-and-vice",
"run_slug": "run-02-night-game",
"title": "Night Game",
"campaign": "every day is game day",
"duration_s": 28,
"look_pack": "CINESTILL_800T_NIGHT",
"palette_anchors": ["deep indigo", "neon magenta", "sodium amber", "scarlet pop"],
"music": {
"bpm": 125,
"vibe": "indie-electronic anthem, dark synth foundation, female vocal with grit, big stadium drum, neon-night driving energy",
"structure_note": "Synth pad opening, drums punch in at the pre-chorus, full electronic-band drop at the chorus, returns to synth pad for outro."
},
Tableau roles (assigned, structure-locked):
| # | Role | What it is |
|---|
| 1 | INTRO | Cold-open, no music yet, product hero hint |
| 2 | PERSONAL_BEAT_1 | Character + product close beat |
| 3 | PERSONAL_BEAT_2 | Walk-up / movement |
| 4 | WIDE_ENSEMBLE | Group scene wide, golden/neon flare |
| 5 | KINETIC_CLOSE_1 | Emotional close-up |
| 6 | KINETIC_CLOSE_2 | Secondary energy beat |
| 7 | SUSPENDED_TIME | Slow-mo confetti / abstract beat |
| 8 | HOOK_HERO | Chorus drop — quadrant split / hero |
| 9 | KINETIC_PEAK | Biggest energy moment |
| 10 | PRODUCT_HERO | Garment / product alone in moody light |
| 11 | MICRO_MONTAGE | Rapid 4-cut beat-locked inside one slot |
| 12 | ORIGIN_WINK | Brand history Easter egg |
| 13 | TWILIGHT_OUTRO | Reflective walk-out |
| 14 | END_CARD | Wordmark + tagline + hero SKU |
This structure is load-bearing. Don't skip slots — even a 20 s cut compresses each tableau to ~1.4s, not removes them. The 3-act arc is what separates this format from generic AI-music-video slop.
Look packs
A look pack locks the photographic style across all 14 tableaux. The pack provides:
STYLE_OPENER string injected at the head of every keyframe prompt
NEGATIVE_TAIL string injected at the end of every keyframe prompt
- Palette anchor list
- Music structure default
- Caption style (placement, chunk-size, font, accent)
Built-in packs (all under lookpacks/):
| Pack | When to pick | Reference run |
|---|
| KODAK_PORTRA_DAY | Warm afternoon, autumn, golden hour, lifestyle, "Saturdays" | hype-and-vice/run-01-game-day-girls |
| CINESTILL_800T_NIGHT | Neon-noir, winter, urban, nightlife, indoor venues | hype-and-vice/run-02-night-game |
| PAPER_CUT_CRAFT | Y2K, candy-coded, kids/wellness/CPG brands, "craft" energy | gorgie/run-02-club-gorgie-music-video |
Adding a new pack: drop a file in lookpacks/<NAME>.md with the four fields above. The scripts read packs by name lookup — no code changes.
Workflow
S0 INTAKE ← read concept.json + brand-brief.md + lyrics-locked.md
S1 STORYBOARD ← gen storyboard.html (Gate 2 review surface)
S2 MUSIC_GEN ← ElevenLabs Music with --with-timestamps → music.mp3
S3 WORDS_ADAPTER ← convert ms→s, ElevenLabs shape → Whisper shape
S4 KF_T01_TEST ← generate single keyframe (Gate 3, aesthetic validation)
S5 KF_BATCH ← 13 parallel keyframes
S6 I2V_BATCH ← 14 parallel image-to-video
S7 STITCH ← concat segments to silent master
S8 MUX_AUDIO ← mix music + loudnorm to -14 LUFS
S9 CAPTION_BURN ← ASS gen + libass-binary burn → master-final.mp4
Each state is idempotent — re-running the molecule will skip completed steps (file-existence checks). The driver scripts/one_shot.py walks the whole state machine; you can also invoke individual scripts to re-run a single state.
Gates
Only two human gates (vs the 5 of motion-graphics) because the cheapest review surface is the storyboard:
- Gate 2 — Storyboard approval (after S1). Open
storyboard.html in browser. Approve the 14 tableaux + music vibe + look pack before any paid gen starts.
- Gate 3 — T01 aesthetic test (after S4). Look at
assets/keyframes/T01.png. Decide if the look pack landed correctly before committing to the parallel batch of 13.
Everything else is automatic + idempotent.
Output
<brand>/ad-runs/<run-slug>/
├── intake-brief.md
├── concept.json
├── source/
│ └── lyrics-locked.md
├── storyboard.html ← Gate 2
├── audio/
│ ├── music.mp3
│ ├── music_metadata.json ← raw ElevenLabs response
│ └── words.json ← Whisper-shape adapter
├── assets/
│ ├── keyframes/T01..T14.png ← Gate 3 = T01.png
│ └── clips/T01..T14.mp4
├── working/
│ ├── segments/seg_T01..T14.mp4
│ ├── concat.txt
│ └── logs/ ← per-job logs
├── clips/
│ ├── master-silent.mp4
│ └── master-no-captions.mp4
├── finals/
│ ├── master-final.mp4 ← the deliverable
│ └── master-final.ass
└── manifest.json
Workarounds baked in (lessons from reference runs)
These are real failure modes hit during Gorgie + H&V development. The scripts handle each automatically — no operator action needed.
-
ElevenLabs Music ToS filter on artist names. Prompts containing "Charli XCX", "Lana Del Rey", "Petra Collins", etc. return HTTP 400 with bad_prompt. The scripts strip artist names from the music vibe field. Use descriptive language ("indie-electronic anthem with female vocal grit") not name references.
-
gpt_image_2 NSFW false-flags. "Young woman", "twin XL bed", "sip", "reach into frame", "Petra Collins" all triggered false NSFW rejections during reference runs. The scripts default to object-hero framing — "Hero subject: a [garment] on a [surface]" — and only introduce people in supporting roles. The shared STYLE_OPENER per look pack avoids the trigger words.
-
Higgsfield 502s. Transient. The scripts retry once on 502/timeout per job. If a single keyframe still fails, the operator re-runs only that one (python3 gen_keyframes.py T12).
-
ElevenLabs returns ms timestamps; sync.py expects seconds. The adapter build_word_timestamps.py converts ms→s and renames start_ms/end_ms → start/end + adds text (Whisper shape). Always run S3 before S9.
-
Homebrew ffmpeg lacks libass. Caption burn fails with No such filter: 'subtitles'. The scripts use the imageio-ffmpeg static binary (pip install imageio-ffmpeg) which has libass enabled. Path resolution is auto.
-
Captions need libass-binary path injection. The sync-captions-to-music atom shells out to Homebrew ffmpeg. Workaround: let sync.py generate the .ass file (it succeeds at that part), then we manually run the burn step with the libass binary. This is what stitch.py does after S9.
-
Beat-anchored TIMELINE varies per song. ElevenLabs doesn't deliver exactly on the spec — verse can stretch a few hundred ms, hook lands at 15.4 s not exactly 14.0 s. The scripts read music_metadata.json and compute the TIMELINE per-run. Never hard-code timings across runs.
Composed Atoms
atoms/music/create-music-elevenlabs — original anthem with word timestamps (S2)
atoms/image-generation/create-image-nano-banana-fal — keyframes for T01..T14 (S4, S5)
atoms/video-generation/create-video-kling — image-to-video, kling3_0 (S6)
atoms/assembly/stitch-videos-ffmpeg — concat segments to silent master (S7)
atoms/audio-editing/mix-master — mix music + loudnorm to -14 LUFS (S8)
atoms/captions/burn-in-captions — ASS gen + libass-binary burn (S9)
Quality Checks
audio/music.mp3 exists and audio/music_metadata.json returns word-level timestamps.
audio/words.json is in Whisper shape ({words:[{text,start,end}]}) with start/end in seconds.
- All 14 keyframes T01..T14 exist under
assets/keyframes/.
- All 14 i2v clips exist under
assets/clips/ and pass through ffprobe.
finals/master-final.mp4 is 1080×1920, 30 fps, h264 + aac, duration 20-30 s.
- Loudness normalized to -14 LUFS with no clipping.
- Captions burned cleanly (no
No such filter: 'subtitles' errors — libass binary path resolved).
Decision Rules
- One look pack per run. Don't mix Kodak Portra and CineStill across tableaux — the through-line is what makes the spot read as one ad.
- Same brand mark on every tableau. Vary SKU / school / color but never the wordmark logic.
- Chorus mantra is the campaign anchor. When making a series (Run 01 → Run 02 → Run 03), the chorus + outro stay identical, only verse + pre-chorus + tableaux change.
- Hook lands at exactly the chorus drop word boundary. The TIMELINE auto-computes this from the music metadata — don't override unless you're trying to break the format on purpose.
- Captions = lower-third serif italic. Music-video preset with
--placement low --chunk-size 4 for cinematic feel. Word-burst pop is for paper-cut, not 35mm.
Failure Modes
- S2 music gen returns HTTP 400 bad_prompt. Check vibe field for artist names. The API response includes a
prompt_suggestion you can use verbatim.
- S4 T01 keyframe NSFW-flagged. Sanitize the prompt: drop "young woman", "twin XL bed", "Petra Collins"; use object-hero framing.
- S5 keyframe batch has 1-2 fails. Re-run only those:
python3 gen_keyframes.py T12 T08.
- S6 i2v fails on one clip. Re-run only that one. The kling3_0 timeout is 20 minutes.
- S9 caption burn fails with
No such filter: 'subtitles'. Homebrew ffmpeg installed instead of libass binary. pip install imageio-ffmpeg and stitch.py auto-finds it.
- TIMELINE durations don't sum to total. Re-derive from
music_metadata.json — see build_timeline_from_metadata helper in scripts/timeline.py.
Reference runs
The molecule was extracted from three working spots:
| Run | Look pack | Output | What it taught the molecule |
|---|
gorgie/ad-runs/run-02-club-gorgie-music-video/ | PAPER_CUT_CRAFT | 20 s · 18.9 MB | Word-burst captions; saturated paper-cut works for CPG |
hype-and-vice/ad-runs/run-01-game-day-girls/ | KODAK_PORTRA_DAY | 28 s · 25 MB | 14-tableau narrative arc beats 8-tableau montage; lower-third captions; founder-wink Easter egg |
hype-and-vice/ad-runs/run-02-night-game/ | CINESTILL_800T_NIGHT | 28 s · in flight | Campaign series with shared chorus; cool palette + neon halation |
Quick start (for a new brand)
mkdir -p <brand>/ad-runs/<run-slug>/{source,assets/keyframes,assets/clips,clips,finals,working/segments,working/logs,audio}
$EDITOR <brand>/shared/brand-brief.md
$EDITOR <brand>/ad-runs/<run-slug>/concept.json
$EDITOR <brand>/ad-runs/<run-slug>/source/lyrics-locked.md
python3 skills/molecules/create-cinematic-music-video/scripts/one_shot.py \
--concept <brand>/ad-runs/<run-slug>/concept.json
The driver writes a .state.json file in the run folder so it can resume from any state.