Visual director
A great talking-head reel shows what the speaker is saying, a beat at a time: a list becomes chips popping in as each item is said; a number counts up; "I typed a prompt" becomes a chat window typing it. The pieces are small, sit around the head (never on the face), and appear exactly when the word is spoken.
You plan this in build.json → "visuals": [...]. The engine handles placement, the kit's look, animation and a sound per pop-up.
1. Read the reel as beats
Go through the locked lines (edl.json) with word indexes:
python3 -c "import json;w=json.load(open('projects/<name>/transcript.json'))['words'];c=json.load(open('projects/<name>/cut.json'));[print(' '.join(f\"{x['i']}:{x['w']}\" for x in w[a:b+1])) for a,b in (l['words'] for l in c['lines'])]"
Mark every beat that has something showable:
- a list of things
- a number
- a tool or app
- an action (typing, sending, booking)
- a before/after or a this-vs-that
- a process or steps
- a system
- a person or story
- a time span
- a rejected idea
- a key word worth landing
Filler, transitions and feelings without an object get nothing.
2. Pick the piece for each beat
| They say… | Piece | Example |
|---|---|---|
| a list ("marketing, invoices, emails") | chips, one item per word as it's said | items with word each |
| things being dropped or closed ("all those tabs in my head") | chips with "close": true | |
| a figure ("100+ clients", "3pm", "a team of 2") | number | "value": "100+", "label": "clients" |
| the key word or punch of a line | word ("style": "type" for a thought or question) | "text": "real *support*" |
| something they reject ("no 50 steps", "not another app") | strike | "pre": "No", "text": "50 steps", "strike_word": i |
| typing a prompt, asking AI, sending a DM | chat | "prompt": "...", "reply": "..." (optional) |
| a sale, a message, a booking, a notification | notify | "app": "Stripe", "title": "New payment: €497" |
| steps done, a to-do list, wins | checklist | items with word each |
| "step 1 / the first thing / part two" | step | "n": 1, "text": "*train* it" |
| a change or process (A → B → C) | flow | items with icon and text |
| a system, tools connected, "everything in one place" | hub | center plus nodes with icons |
| a time span or range ("in a month… in 3 years") | scale | marks, from, to, move_word |
| a client, a person, a story about someone | person | "name": "Abby", "note": "coach", "meter": 3 |
| this vs that, old way vs new way | versus | left (the ✗) / right (the ✓), right_word |
| a meme or GIF, only when the user asks for one | gif | "file": "projects/<name>/memes/3.mp4", "caption": "me reading my old *captions*" |
| their profile, a screenshot, an app | phone | "image": "assets/profile.png" (only images the user gave) |
Text on pieces:
- 1–3 words per chip, node or label
- use their words
- title case is fine, but no full sentences except in
chatandnotify
Icons: find real names with uv run tools/icons.py calendar money email and never guess a name. The build stops on an unknown icon.
Signature pieces (need the user's own material)
- Their reels orbiting them:
{"type": "orbit", "word": i, "until_word": j}inevents(not visuals).- Use it on a line about their content or body of work ("I've made 300 of these").
- Needs
brand/reels/. Fill it withuv run tools/reels.py add <their reel files or a folder>: their own exported reels, or downloaded from their profile with Apify if it's connected. - Their reels pass behind and in front of them. At most once per reel.
- Instagram profile follow for a call to action ("follow me", "comment BRAIN"):
{"kind": "profile", "word": i, "until_word": j, "keyword": "BRAIN"};keywordis optional and types into a comment sheet.- Numbers come only from
brand/instagram.json(name, handle, posts, followers, following, bio, avatar). If it's missing, ask them for the real numbers (or pull them from their own profile with Apify). Never estimate or invent. - The grid uses
brand/reels/. It gets a tap sound on Follow (and on Post).
- Another creator's reel as a card: a
gifwith"handle": "creatorname". The @handle shows under it.- Always credit, and only use clips the user points to.
- Custom insert (when no piece fits, e.g. a recreation of an app they use, or a file scrolling): build it with a sub-agent. See "Custom inserts" below.
Motion
The kit's visuals.motion: gentle / snappy / bouncy / blur (a blur-pop that sharpens into place) / rise (calm editorial).
"style": "rise"on awordpop makes each letter rise out of its line, one after another. It's great for serif punch words. A kit can make it the default withvisuals.word_style: "rise".- Captions can blur-pop with
captions.pop: "blur".
Custom inserts (parallel sub-agents)
- Lock the timing first: the word times it must hit, converted to the insert's own clock (t = 0 when it appears).
- Spawn one sub-agent per insert, in the background and in parallel (Agent tool), while you keep building. Brief it with:
- What it shows, and the line it plays under. Use real data from the user only; anything else is a generic placeholder. No real people, and no copied logos.
- Where to work:
projects/<name>/inserts/<insert-name>/index.html. Touch nothing else. - Size and duration: for example 900×700 px and 2.6 s. The background stays transparent outside the card or window it draws.
- The contract:
window.renderAt(t)draws the exact state at t seconds. No timers, CSS transitions or animations, because frames are captured one by one.- Local fonts only (copy from
fonts/free/). - Text at least 24 px, using the user's kit colours.
- Typed text: one character per step of t.
- Local fonts only (copy from
- Beats, with times locked to the voice.
- Render with
uv run tools/insert.py <index.html> --dur <s> --w <w> --h <h>→insert.mov, with alpha. Look at a few frames before reporting. - Report: the path, the exact duration and the beat times (for sounds). Flag anything that looks off.
- Place it with
{"kind": "insert", "file": "…/insert.mov", "word": i, "width": 640}. Addsfx_extraclicks on its beats.
Memes and GIFs (only on request)
-
Never add a meme unprompted. Memes are a taste call. You may suggest one in your report ("a David Rose 'ew' would land on line 5"), and add it only if they say yes.
-
Search:
uv run tools/memes.py search "<show> <character> <emotion or words>" --project projects/<name>. For example "schitts creek david ew" or "the office michael no god please no".- It downloads up to 6 clips and prints
SHEET=…/memes/sheet.png. Read the sheet and pick the tile that matches what they described (the right character, the right expression), not just the first. - Tiles are numbered 1…6, left to right, top to bottom.
- It downloads up to 6 clips and prints
-
Their own file ("use this GIF"):
uv run tools/memes.py add "<file>" --project projects/<name>givesmemes/own-1.mp4. Images work too. -
Place it:
{"kind": "gif", "file": "projects/<name>/memes/<n>.mp4", "word": i, "caption": "<their text>"}- The caption is optional, in their words, with one
*emphasis*. Default length is about 2.6s, or set"until_word". - Wide clips sit above the head; tall ones sit below the chin.
"at": "full"makes a 1.6s full-screen cutaway with the caption on top.
-
In the report: say which one you used, and open the sheet (
open …/sheet.png) so they can say "meme 3". The other clips are already downloaded, so swapping means just changingfileand rebuilding. -
No key (
MEMES=no-key): offer the 3-minute free setup:- They go to developers.giphy.com, sign up, create an API app (not SDK), and copy the key.
- They paste the key in the chat.
- You save it to
brand/keys.jsonas{"giphy": "<key>"}.
Never ask them to edit files themselves. Until then, their own GIF files still work.
-
Mention once, the first time they use a TV or movie meme: those clips belong to the studios. Short silent reaction GIFs are used everywhere on Instagram, but there's a small risk. Their own clips carry none.
3. Timing (word indexes from the transcript)
"word": imakes the piece appear as word i is spoken. Use the first word of the beat, not the start of the line.- Items take their own
"word", so each chip, check or node lands as it's said. This is what makes it feel edited. - Ending a piece:
"until_word": jends it after word j- otherwise it holds about 2–3s, or until 1.4s after its last item
"dur": 2.5sets the length directly
- Other moments can be timed to words too:
strike_word,reply_word(chat),right_word(versus),move_word/move_end_word(scale),meter_word(person),close_word(closing chips).
4. Density from the kit (kit.json → visuals.density)
| density | how often | feel |
|---|---|---|
calm | one piece every 8–10s | a clean expert explainer |
steady | every 4–6s | most reels |
busy | every 2–4s, nearly continuous | the fast "explainer" style |
Rules at every density:
- One piece at a time. Two can overlap only if they're in different zones and belong to the same beat.
- Never during the hook (first ~3s) or a statement. Pieces can replace statements: at
busy, use at most 1 statement. - Don't use the same kind twice in a row, unless it's the same list continuing.
- A
wordpop should condense or reframe, not mirror the caption. When it shows the same words, the engine hides the caption while it's up. That works, but a pop like "the whole business" on "comprehensive overview of your business" adds more than repeating "non-negotiables". - Don't put a highlight (
highlight_words) on a word that also gets a pop-up. chatplays a full little story by itself: it types the prompt (with key clicks), sends it (swipe sound), then shows the AI "thinking", then thereplyif given. Give it about 1.5s after the typing ends, so it can send. A shortreplyin their words (the outcome they describe) makes it land.- Leave short breathing gaps (≥0.4s) between pieces.
"at"overrides the zone (top,left,right,chest) only when the default collides with something.
5. Build and check
uv run tools/build.py projects/<name>. The build prints CHECK=…/check.png: one frame per moment and pop-up. Read it and fix anything that is:
- unreadable
- off-screen
- covering the face
- colliding with a caption
- illustrating the wrong word
- listed in a
SAFE ZONE:line (it sits under Instagram's username, caption or like / comment / share buttons). Fix every one: move the piece ("at"), shorten its text, or end it sooner, then rebuild until there are none.
What goes on screen:
- No em dashes in pop-up, hook or statement text. Use a comma, a full stop or a line break.
- Real numbers only when they're the user's own and they said them. Chat windows, comments and DMs use generic placeholders, never a real person's name or a made-up result shown as proof.
- Another creator's post or reel on screen gets their @handle on it.
Then hand back as in the new-reel skill, adding one line such as: "12 pop-ups: chips for your 4 tasks, a chat window on 'prompt', a hub for your AI system…".
When the user asks
-
"What graphics can you add?" / "show me the options": open the catalogue video if one exists for their kit (
projects/*/catalog-<kit>.mp4). Otherwise render one on their latest reel:uv run tools/build.py projects/<name> --catalog. It shows every piece once, in their look, labelled. Never describe pieces at length instead. -
"More / fewer pop-ups": change the density for this reel; save it to the kit's
visuals.densityif they say "always". -
"A checklist here" / "show my profile when I say follow": add exactly that piece on that word.
-
Style ("outlined cards", "calmer animations", "thicker icons"): the kit's
"visuals"block:card:solid/outline/glassmotion:gentle/snappy/bouncyicon_stroke: 1.5–2.5radius- colours:
accent,strong,alert,word,card_color; colour names from the kit or hex values
Change their kit, never a preset.