Give it the base cut (ffmpeg-cut-clips-to-a-voiceover or any edit), the voice's word
times, the brand (refs/brand.md from guide-capture-a-brand: font, ink, accent, the
logo mark, the CTA button and its exact words) and the script's beat table; get the
layers of a designed title pass for an ad, in every aspect it ships. The spec, the
commands and the live-spec gate are hn4a-compositor's. Slides and layouts beside a
presenter are hn4a-compositor-motion-graphics; type inside the scene (behind people,
on a wall) is hn4a-compositor-in-scene-titles; type that lies on a surface or
recedes in depth (a headline on the floor, a "3D" title tilting into the set) is a
plane group, hn4a-compositor-perspective-planes, never a skew faking it.
Inputs
- The words: the take's alignment or transcript, read with
python-read-word-timings;times(words, phrase)gives the start of every word of a phrase. - The brand: the font loaded in the spec's
fonts, ink, accent and dark colours, the mark as a PNG or SVG file, the CTA's exact words and its button's fill and radius. - The beat table (
llm-write-an-ad-script): which line gets which piece below.
Run it
Write references/brand_titles.md's module into the job's scripts/ folder as
scripts/brand_titles.py, beside scripts/word_timings.py, and call it from the build
script there, once per aspect:
B = Brand(font="Inter", accent="#FF8A3D", W=1080, H=1920) # one per aspect
w = read_words("audio/voice_c.alignment.json")
L += hook_stack(B, "hook", "You've bookmarked|*50*|recipes.", times(w, "You've bookmarked 50 recipes."), 1.9)
L += accent_line(B, "l2", "You still|order *takeout.*", times(w, "You still order takeout."), 3.6)
L += lockup(B, "logo", "refs/brand/mark.png", "Globex Kitchen", times(w, "Globex Kitchen")[0], 5.6)
t = times(w, "Pick 5 dinners. And it builds the week and the shopping list.")
L += chips(B, "chips", ["5 dinners", "The week", "Shopping list"], [t[1], t[8], t[10]], 9.2)
L += stat(B, "proof", "40,000", "cooks plan here", times(w, "40,000 cooks")[0], 11.55)
L += end_card(B, "end", "refs/brand/mark.png", "Globex Kitchen", "Start planning", "globex.example",
times(w, "Start planning")[0], 13.208)
| Piece | Helper | On which line |
|---|---|---|
| Hook stack | hook_stack | The pain line: words rise one by one over a darkened frame, the *number* slams in at 1.8× the size in the accent with a glow |
| A line | accent_line | Every remaining line that needs type: low in frame over a bottom gradient, built word by word, one *accent* word, ` |
| Logo lockup | lockup | The line that says the brand's name; no other title on that line |
| Benefit chips | chips | The line that lists what you get: one glass pill per item on its word, one with an accent dot |
| Proof stat | stat | The proof line: the number big in the accent, a short kicker; no other title |
| Key phrase | key_phrase | A mascot or story ad: 2–4 words a shot, popped on the spoken word |
| End card | end_card | The CTA: a dark fade from the bottom, the lockup, the CTA button recreated, the URL small and dimmed, all in the lower part so the last shot still plays |
Each word of a stack or a line is its own layer holding the whole line, the other
words hidden, so nothing shifts as words arrive. slam and rise are the entrances;
pass your own box and size to any helper to move it.
Rules that worked
- One accent word per line, and the line breaks placed by hand with
|on the phrases: automatic grouping split lines on every small pause. - Designed titles, not captions. On a generated-world ad, karaoke captions read as subtitles; the designed lines read as part of the ad.
- Type says the gist, never the narration word for word; the brand line gets the lockup and the proof line the stat, with no title beside them.
- The last line's title runs until the CTA word, then the end card: ending it early left a gap when the voice ran into the end card.
- The CTA in the site's own words and button; the URL small and dimmed under it.
- Every aspect from the same calls, a
Brandper size: the defaults place the pieces for 16:9 and for 9:16 (lines higher, clear of the platform's buttons:guide-pace-a-short-for-social).
Tips and traps
- A font family whose name ends in a digit breaks the CSS font shorthand: quote it
(
Brand(font="'Source Serif 4'")). - A flex row needs
width:100%to centre in its box; without it the lockup sat at the left. - The mark goes in as a
data:URI inside the lockup's html (data_uri): an image by file path inside a text layer shows in the preview and renders blank (hn4a-compositor). - A number the transcript spells as digits ("50" for "fifty") is matched as
written there: read
wordsfirst (python-read-word-timings). - A bigger logo than the site's header PNG:
guide-capture-a-brand, The logo; a 444 px PNG looked poor full screen.
Check it
- Stills at every piece in every aspect, on one sheet (
python-contact-sheet,images): nothing overflows, the lockup and button centred, the end card clear of the last shot's action. - The live spec with the voice (
hn4a-compositor, Live spec, then render); after the render, the frames at the logo and the end card.


