Glass Edit
Turns a raw talking-head clip into a finished 1080x1920 / 30 fps short in the style reverse-engineered frame-by-frame from a reference short (57.6 s, 1729 frames, 27 scenes). Everything is rendered programmatically by a Remotion engine (React -> headless Chromium -> MP4): no templates, no stock footage, no music.
Source, updates and issues: https://github.com/keemfinity/glass-edit (MIT).
Work order: engine setup -> media prep (scripts) -> creative step (beat sheet -> edit.json) -> QA -> render -> deliver. The creative step is where the style lives - follow the Style Bible (2) and Beat Grammar (3) exactly.
0. Engine setup (once per session, ~3-5 min)
The engine (39 files: Remotion 4.0.533 project + Python scripts, ~300 KB) ships INSIDE this skill: engine/ next to
this file, plus the face-detector model in assets/. Find the skill folder and copy the engine out:
SK="${CLAUDE_SKILL_DIR}"
for d in "$SK" /root/.claude/skills/synced/*/glass-edit ~/.claude/skills/glass-edit /mnt/skills/*/glass-edit; do
[ -f "$d/engine/src/Main.tsx" ] && SK="$d" && break; done
[ -f "$SK/engine/src/Main.tsx" ] || SK=$(find / \( -path /proc -o -path /sys -o -name node_modules \) -prune -o \
-path '*glass-edit/engine/src/Main.tsx' -print 2>/dev/null | grep -v "^$HOME/glass-edit/" | head -1 | sed 's|/engine/src/Main.tsx$||')
echo "skill folder: $SK"
[ -f $HOME/glass-edit/src/Main.tsx ] || { mkdir -p $HOME/glass-edit && cp -r "$SK/engine/." $HOME/glass-edit/; }
mkdir -p ~/models && cp -n "$SK"/assets/*.tflite ~/models/ 2>/dev/null; ls $HOME/glass-edit/src/Main.tsx
If no copy of the skill has engine/ (it was saved as text only), fetch it from GitHub:
git clone --depth 1 https://github.com/keemfinity/glass-edit /tmp/glass-edit-src
mkdir -p $HOME/glass-edit ~/models && cp -r /tmp/glass-edit-src/skills/glass-edit/engine/. $HOME/glass-edit/ \
&& cp -n /tmp/glass-edit-src/skills/glass-edit/assets/*.tflite ~/models/
Last resort: ask the user to attach glass-edit.zip (from the repo's Releases) and copy its glass-edit/engine/ folder.
Install + assets (needs code execution with network access to npm, PyPI, GitHub and huggingface.co (speech model); fontshare.com gives the Satoshi caption font (else DM Sans), jsDelivr gives brand logos (else monograms); storage.googleapis.com is only needed if Chromium must be downloaded):
cd $HOME/glass-edit && chmod +x scripts/*.sh
npm install --no-audit --no-fund >/dev/null # exact versions from package.json (Remotion 4.0.533)
pip install --break-system-packages -q faster-whisper mediapipe onnxruntime opencv-python-headless scipy pillow
bash scripts/assets.sh # fonts -> public/fonts, models -> ~/models (RVM ResNet50 107 MB), paper -> public/bg_*.jpg, github logo
node scripts/stills.mjs scripts/demo_edit.json work/qa 10 # smoke test: must write work/qa/f_00010.png
- Chromium:
CHROME=$(ls /opt/pw-browsers/chromium_headless_shell-*/chrome-linux/headless_shell 2>/dev/null | head -1). Pass--browser-executable=$CHROMEtonpx remotion renderwhen set (stills.mjs finds it itself). If empty, runnpx remotion browser ensureonce and omit the flag. - Facts (typical Claude cloud sandbox: 2 CPU, no GPU): matting ~1.3 fps (ResNet50 + edge clean-up: ~25 min per minute of video), rendering ~7 fps.
matte.py --model ~/models/rvm_mobilenetv3_fp32.onnxis ~2.5x faster for drafts but turns mics / held objects into smoke - never ship it. faster-whisper is fed raw PCM by the script (its own decoder breaks on this PyAV). If Satoshi can't be fetched, assets.sh falls back consistently (DM Sans). - Remotion is free for individuals and companies of up to 3 people; larger companies need a Remotion company license.
1. Pipeline
INPUT = the user's video: chat attachments land in /mnt/user-data/uploads/ (or ~/.claude/uploads/<id>/); a file
on the user's computer is staged first (device_stage_files). Ask nothing up front unless the brief is unclear - the
style is fixed; optional inputs worth using if offered: brand list, screenshots / b-roll for media scenes, CTA keyword.
cd $HOME/glass-edit; mkdir -p public/src work out
# 1 ingest: constant 30 fps, long side <= 1920, keep audio
ffmpeg -v error -y -i "$INPUT" -vf "scale='if(gt(iw,ih),min(1920,iw),-2)':'if(gt(iw,ih),-2,min(1920,ih))',fps=30" \
-c:v libx264 -crf 16 -preset medium -pix_fmt yuv420p -c:a aac -b:a 192k -ar 48000 public/src/raw.mp4
# 2 transcribe - list every brand / product / name you expect in --prompt (fixes spellings);
# English by default (small.en) - for any other language add --model small (multilingual)
python3 scripts/captions.py transcribe public/src/raw.mp4 work/words_raw.json --prompt "Ollama, ChatGPT, ..."
# 3 tighten pacing like the reference (pauses > 0.40 s -> 0.28 s); remaps the word times
python3 scripts/trim_silence.py public/src/raw.mp4 work/words_raw.json public/src/clip.mp4 work/words.json
# 4 person cut-out in the background (ResNet50 RVM + clean edges, ~25 min per minute of video) -> transparent VP9 webm
nohup python3 scripts/matte.py public/src/clip.mp4 public/src/clip_fg.webm > work/matte.log 2>&1 &
# 5 speaker framing (eye line + scale for full-screen and PiP; detects missing headroom and off-shots)
python3 scripts/framing.py public/src/clip.mp4 > work/framing.json
# 6 logos for every brand you will show: slug[:domain] (lobe-icons / simple-icons / favicon fallback)
python3 scripts/fetch_logos.py public/logos github openai:openai.com notion:notion.so
# 7 captions: --serif emphasis words/phrases (word@t0-t1 scopes one occurrence), --accent CTA keyword (lime),
# --fix spelling corrections "heard=written,..."
python3 scripts/captions.py chunk work/words.json work/captions.json --serif "never,zero,no monthly" --accent "local" --fix "olama=ollama"
# 8 read the transcript, write the beat sheet, then work/plan_edit.py (sections 3-4) -> work/edit.json
python3 -c "import sys;sys.path.insert(0,'scripts');from plan import Words;[print(f'{a:6.2f}-{b:6.2f} {t}') for a,b,t in Words('work/words.json').sentences()]"
python3 work/plan_edit.py
# 9 validate + QA stills (frame = seconds*30) + contact sheet - LOOK at it
python3 scripts/validate.py work/edit.json
node scripts/stills.mjs work/edit.json work/qa 0 45 120 ... && python3 scripts/sheet.py work/qa work/qa_sheet.png
# 9b matte check (after work/matte.log says "done"): every 64th frame over magenta - LOOK at it
ffmpeg -v error -y -f lavfi -i color=c=magenta:s=1080x1920:r=30 -c:v libvpx-vp9 -i public/src/clip_fg.webm \
-filter_complex "[0][1]overlay=shortest=1,select='not(mod(n\,64))',scale=270:-2,tile=7x1" -frames:v 1 work/matte_check.jpg
# 10 render
npx remotion render src/index.ts Main out/final.mp4 --props=work/edit.json --concurrency=2 --crf=18 --browser-executable=$CHROME
Deliver: cp out/final.mp4 /mnt/user-data/outputs/<name>_glass.mp4, SendUserFile it, and when a folder is connected
also write it there (device_commit_files). Summarise the beat sheet in 5-10 lines and offer tweaks (swap a scene
type, move a cue, change accent words) - edits are cheap: change plan_edit.py, re-run, re-render. If the matte failed, drop fg (full-screen shows the raw
footage, PiP loses the pop-out) and say so. framing.py prints "no headroom" when the head touches the top of the
source: it then uses a tighter full-screen crop and a PiP without pop-out (expected, not an error).
Off-shots (framing.json -> offShots, also printed as notes): stretches where the face is missing or framed very
differently - another camera angle, a whip/blur frame, b-roll. Podcast clips often OPEN on one. Never show the speaker
there: for an opening off-shot give scene 1 pipIn = the first clean frame (the hook card stands alone, then the PiP
rises in and the column glides up to make room); elsewhere use a speaker-less scene. Confirm the exact frame with a strip:
ffmpeg -v error -i public/src/clip.mp4 -vf "select='lt(n,30)',scale=216:-2,tile=10x3" -frames:v 1 work/first30.jpg.
validate.py warns when a pip/full scene overlaps an off-shot.
Cut-out quality (matte.py v3): ResNet50 RVM keeps whatever the speaker talks into or holds (podcast mic, shock
mount, cable) solid; specks and haze are removed, the outline is snapped to the image with a guided filter and tightened,
and rim pixels take the subject's own colour, so the source wall never shows as a grey (on black paper) or dark (on
cream) halo. In work/matte_check.jpg the outline must be crisp with no coloured rim, objects in front of the speaker
solid, no floating specks. If an edge still looks soft, re-run with --tight 0.2,0.8; to keep fine flyaway hair use
--tight 0.05,0.95 --gf 4. A failed or missing ResNet50 download falls back to mobilenet with a printed note.
2. Style Bible (measured from the reference)
2.1 Pacing and structure
- 1080x1920, 30 fps, voice only (no music, no SFX), pauses trimmed to ~0.3 s; cuts land mid-sentence on word onsets.
- A new visual beat every ~2 s (27 scenes / 57.6 s, range 0.7-3.6 s). A scene may run longer only if something new happens inside it every <= 1.5 s (focus pull, stat, strike, X, typing, orbit, world switch).
- ~50 % light world / ~50 % dark world. Speaker visible ~45 %: PiP ~30 %, full-screen ~15 %.
- Full-screen speaker = short breathers (0.7-1.7 s, every ~6-8 s), always hard-cut, on connective / personal lines.
- Every element appears on the word that names it (0-4 frames after onset): numbers when said, sub-labels on their word, X when the competitor is named, strike-through on the negated word.
2.2 Two worlds
| world | look | for |
|---|---|---|
light | warm cream paper #EAE3D6, faint mottling, soft vignette | hook, lists, comparisons, demos, explanations |
dark | crumpled black paper, warm-brown top-left, olive bottom-right | introducing a product/tool/person, caveats, payoff, outro; every full-screen speaker shot |
- light <-> dark is ALWAYS
enter: "dissolve"(4-frame blur-dissolve). Same-world changes:cut, whips ormorph. - A world can flip inside a scene with
worldAtwhile the hero element stays (dark title card -> light orbit).
2.3 Speaker treatments
| speaker | look | caption centre |
|---|---|---|
pip | rounded card (x 70-1010, top 1400, r 34) with the ORIGINAL footage running off the bottom; the cut-out head pops out above the card edge | y 1143 |
full | cut-out person over dark paper, eyes at y~1051, eye distance ~290 px | y 1425 (over the chin) |
none | graphics only | y 1425 |
- PiP slides in (5 f) / out (3 f) with motion blur on whips and morphs; appears/disappears instantly on cuts. The caption glides 1143 <-> 1425 (~9 f) when the PiP leaves/returns.
2.4 Captions (always on)
- lowercase, 1-2 words per chunk (3 if all tiny), punctuation kept, continuous except across pauses; 3-frame blur-in.
- Satoshi Medium 54 px,
#2B2723on cream,#ECE9E4on dark / over the speaker. - Instrument Serif Italic 66 px accents: the hook words, the paid competitor in "instead of X", negations and money
("never", "zero", "no monthly", "bill.", "api key."). Max ~3 per 10 s. CTA keyword in lime
#C6E33C.
2.5 Type
| role | font | size |
|---|---|---|
| tool / entity title | Anybody 500, wdth 112, -0.02em | 155 px (<= 800 px wide), centre y 433, mask-rise |
| heavy display ("STOP", "no API key") | Archivo 800 (wdth 100 caps / 104 lower) | 110-200 px |
| big numbers ("$60", "$0") | Archivo 800 | 290 px; hero payoff ~550 px |
| thin partner word ("paying" under "STOP") | Anybody 200, wdth 118 | ~160 px, tight under the heavy word |
| descriptor lines | Satoshi 400 | 70 px white / 66 px grey #BDBCB6; stat 76 px |
| pills | Archivo 800 100 px, cream pill h168 | chips: Satoshi 500 60 px on #141312 |
| micro labels | JetBrains Mono 27-31 px uppercase, tracking 0.32em |
2.6 Colour - the whole palette
cream paper #EAE3D6 · crumpled black paper · ink #141210 · white on dark #F3F1ED · greys #BDBCB6 / #6F6A64
· lime #C6E33C (the only accent: highlight pill, "after", typed keyword, cloned-voice wave) · orange strike #E8643C
· red #E8343A (X marks, old price) · chip #141312 · pill #F3EEE6 · UI card #1C1C1C.
Brand colours appear only inside clear glass tiles; on dark tiles logos are white.
2.7 Liquid-glass components
- Dark tile (smoked glass app icon): radius 20 %, bright top-left bevel, light bottom lip, cast shadow, white mono logo at 62 %. Hero 300 px; rows: 3 = 280, 4 = 215. Colourful app icons with no mono version are shown as-is.
- Clear tile (thick clear glass): green-tinted rim with specular streaks, frosted body, logo in brand colour,
optional word under it or a
rowlockup; warm rim = emphasised. Single 545, pair 388, orbit 110-200 px. - Sheen sweeps every glass surface (14 f) ~20 f after it lands, then every ~3 s.
- Cream repo pills (GitHub icon + label, labels blurred to tease), lime "+1" pill, dark chips, dark UI cards.
2.8 Motion (frames @ 30 fps)
blur-in 6 f (opacity, 16 px blur, +12 px rise, 0.97 scale) · mask-rise 7 f (titles, heavy text, $0) · focus pull 5 f (placeholders sit blurred until named) · strike 7 f L->R · X: two 4 f strokes, 2nd starts +3 f · connector 6 f then descriptor · typing ~0.9 char/f, CTA keyword 1 char / 2.5 f · orbit: satellites spring out from behind the hero (front first, 3 f apart), hero sinks 160 px, named ones grow x1.42 to the front · list fly-in 7 f from the right with motion blur · list drop: reverse cascade 1.6 f apart with spring · before/after wipe 12 f with lime handle.
2.9 Transitions (enter of the incoming scene)
| enter | use | effect |
|---|---|---|
cut | default; all full-screen speaker in/out | hard cut |
dissolve | only light <-> dark | old graphics gone in 2 f; paper blends 50/85/97 % with 14 px blur |
whipLeft | hook / list -> next light layout | graphics shoot left (1/3 gone on the cut frame), PiP drops |
whipUp | same topic -> PiP statement | PiP rises; graphics accelerate up ~300 px, smear and vanish (8 f) |
morph | in-place layout change (teaser -> list -> CTA) | old graphics blur out in 2 f, new blur in, PiP slides |
3. Beat Grammar - script -> scenes
Read the sentences, mark the words that carry a new idea, then map each idea to a recipe:
| what is said | recipe |
|---|---|
| Hook (first 3-4 s): "Stop X-ing", "N tools that...", "You're doing X wrong" | column light + pip, frame 0 already built: heavy key word + thin partner word + a row of 3-4 dark tiles as blurred placeholders that focus-pull on each name |
| "these N things / here are N..." | list fly, light, whipLeft, one pill per item on its beat, labels blur (tease) |
| "The first / next is X" (product, app, person, concept) | tool, dark, dissolve: title + hero dark tile + connector + 1-2 descriptor lines on cue (stat + unit, or what it is + what it's for) |
| X works with / supports A, B, C | same tool + worldAt -> light + satellites (orbit; emphasise the first two named) |
| "without / no more / don't need X" | column heavy text with strikeAt on that word (light + pip, often whipUp) |
| "instead of paying for X", "replaces X" | column clear tile(s) of X, xAt when X is named |
| prices / cost comparison | column: label brand, big red price + strikeAt + strikeTilt: 3, chip new thing, big "$0" |
| demo of an AI / chat / prompt | chat (types the prompt, sends, thinking dots, optional reply) |
| UI features ("tools, masks, layers") | media + highlights cycling on each word (needs a screenshot) |
| input -> output (voice, audio, before/after image) | wave cards, or media + compare |
| requirement / caveat ("you need A or B") | column dark + pip: small label, pill A, italic "or", pill B |
| payoff / punchline | column dark, no speaker: struck heavy + giant big (~550 px) + small grey label |
| teaser ("one more I couldn't fit") | column dark + pip, repo lime pill, blur, badge: "+1", y: 640 |
| "want all of them?" | list drop (dark, morph) of all items above the lime pill |
| CTA "comment WORD" | cta dark + pip (morph): mono label, box types WORD in lime, italic follow-up line |
| personal / connective line, sign-off | full, hard cut, 0.7-1.7 s; also the final sentence |
For other kinds of talking heads (story, opinion, tutorial) use the same kit: claims and numbers -> column
heavy/big (+strike for "not X"), named things -> tool cards or tiles, steps -> list fly with icon pills
({icon:'check'}, monograms {text:'1'}), feelings / asides -> full breathers, payoff -> dark column, CTA -> cta.
Rules: start every scene ON its first word (W.at), never in a pause · cue elements to their exact words · alternate
worlds, never > ~8 s in one world, a full breather every 6-8 s · one idea per scene, <= 5 text elements · frame 0
must already look finished · end on full for the last sentence · no emoji; without a brand logo use a monogram
{text:"F5"} or a line icon {icon:"bolt"} (bolt dollar clock chart trend check x lock mic play image code globe heart
star user users chat cpu cloud search doc spark rocket video mail link gear phone shield fire) · without a screenshot
use chat / wave / column / tool instead of media - never invent a product UI beyond its logo.
3.0 Worked example for a non-tool video (generalising the grammar)
Script: "I saved ten thousand dollars in six months. And no, I didn't get a raise. First, I cancelled every subscription I wasn't using - Netflix, Spotify, Adobe - that was 240 dollars a month. Second, I moved my savings to a high-yield account. Third, I automated everything, so the money leaves before I can spend it. Want my spreadsheet? Comment SAVE and I'll send it to you."
0.0 column/light/pip big "$10,000" (built at frame 0) + label "in 6 months" on "six months"
2.4 full cut "And no, I didn't get a raise." (aside -> breather)
4.1 column/light/pip cut mono "step 01" + heavy "cancel" + 3 clear tiles netflix/spotify/adobe, xAt on each name
7.9 column/light cut label "subscriptions" / big red "$240" + strikeAt "240" / chip "every month"
9.6 tool/dark dissolve title "high-yield", logo {icon:'trend'}, lines "4.5% APY" on "high-yield", "vs 0.01%" sub
12.8 column/dark/pip cut mono "step 03" + heavy "automate" + italic "before you can spend it" on "before"
15.4 full cut "so the money leaves before I can spend it" tail / breather
16.9 cta/dark/pip morph label "comment below", word "SAVE", line "and I'll send you the sheet."
19.5 full cut "...send it to you."
captions: --serif "no,didn't,never" --accent "save"; logos: netflix spotify adobe (fetch_logos.py).
3.0b Field-tested: 13 s quote clip, no brands (podcast guest quoting an author, opens on another camera angle; names changed)
Script: "Ava Moreno says, accept that all of your heroes are full of shit. Your heroes aren't gods, they're just regular people who got particularly good at one thing by sacrificing literally everything else. That's the price that you pay for success, and most people wouldn't pay it."
0.00 column/light/pip cut dark tile {text:'AM'} + heavy "AVA MORENO" (built) + thin "says" on "says"; y 650,
pipIn 0.47 (opening off-shot: close-up angle + whip frame)
1.64 column/light/pip whipUp italic "accept that" / heavy "YOUR HEROES" on "your" / thin "are full of sh*t" on "full"
3.45 column/dark dissolve mono "your heroes" / big "GODS", strikeColor orange, strikeAt "aren't"
4.47 tool/dark cut title "regular people", {icon:'users'} tileAt "people"; "particularly good" / sub "at one thing"
7.17 column/light/pip dissolve italic "by sacrificing" / 3 clear tiles heart, clock, globe - xAt literally, everything,
else / heavy "EVERYTHING" on "everything" / thin "else." on "else"
9.47 column/dark dissolve dark tile {icon:'dollar'} / heavy "THE PRICE" on "price" / thin "you pay" / lime italic
"for success." on "success" (the edit's single lime moment)
11.71 full cut "and most people wouldn't pay it."
captions: --serif "[email protected],aren't gods,everything else,wouldn't". Lessons: a quote -> attribution on frame 0, the line itself in the next beat; a person with no logo -> monogram tile; abstract nouns ("everything else") -> line-icon tiles you can X; a payoff builds top-down, one element per spoken word (no element idle > ~1.2 s); profanity -> soften in the graphics ("sh*t"), keep it in the captions as spoken unless the user asks; the same icon never means two things.
3.1 The reference edit as a beat sheet (calibration)
0.0 column/light/pip STOP paying + 3 blurred dark tiles (chatgpt, midjourney, elevenlabs focus on their words)
3.5 list/light whipLeft 5 blurred repo pills fly in
6.6 tool/dark dissolve "ollama" + llama tile; 180k / GitHub stars; worldAt 10.5 -> light orbit of 6 model tiles (gemma + deepseek grow)
14.4 column/light/pip whipUp "no API key", struck on "api key"
16.6 tool/dark dissolve "open webui"; a chat app / on top of ollama
18.7 full cut "it sits on top of ollama and works"
20.0 chat/light/pip cut llama3.2 card types "Write me a business plan", sends
22.7 tool/dark dissolve "invokeai"; local image generation / with a full editing canvas
24.7 column/light/pip dissolve big clear midjourney tile, red X on "midjourney"
26.3 full cut
27.8 media/light cut screenshot; tools -> masks -> layers highlights + zoom card
29.8 tool/dark dissolve "f5-tts" (F5 monogram); voice cloning / from a short clip
32.0 full cut
33.1 wave/light/pip cut SHORT AUDIO CLIP -> CLONED VOICE (lime)
35.2 column/light cut elevenlabs / $60 red struck / f5-tts chip / $0
37.0 tool/dark dissolve "upscayl"; 50k / GitHub stars
40.6 media/light dissolve before/after wipe
43.3 full cut
44.4 column/light cut Topaz + Magnific clear tiles, X on each name; upscayl chip; $0
46.3 full cut
47.3 column/dark/pip cut some of these want / a decent GPU / or / an M-series Mac
49.3 column/dark cut monthly bill (struck) / $0 (550 px) / after that
50.5 full cut
51.2 column/dark/pip cut lime +1 pill (blurred name)
53.4 list/dark (drop) morph all repos drop above the lime pill
54.5 cta/dark/pip morph COMMENT BELOW, types LOCAL; "and the link lands in your dms."
56.0 full cut "...send them to you directly."
4. Writing the edit - scripts/plan.py
# work/plan_edit.py
import sys; sys.path.insert(0, 'scripts')
from plan import *
W = Words('work/words.json') # W.at("phrase", after=sec, offset=..) -> start time of the spoken phrase
S = [
scene('column', 0, 'light', 'pip', 'cut', items=[heavy('STOP'), thin('paying'),
tiles('dark', [tile('openai', W.at('chatgpt')), tile('midjourney', W.at('midjourney'))], placeholder=True)]),
scene('list', W.at('these five'), 'light', 'none', 'whipLeft', mode='fly', blurLabels=True,
items=[{'label': 'ollama', 'at': W.at('these')}, {'label': 'open webui', 'at': W.at('five')}]),
scene('tool', W.at('the first'), 'dark', 'none', 'dissolve', title='ollama', logo=logo('ollama'),
lines=[line('180k', W.at('180,000')), line('GitHub stars', W.at('stars'))]),
scene('full', W.at('it sits'), 'dark', 'full', 'cut'),
]
write_edit('work/edit.json', S, W, captions='work/captions.json', framing='work/framing.json',
logos='public/logos/logos.json', video='src/clip.mp4', fg='src/clip_fg.webm')
Builders: scene, logo, tile, tiles, heavy, thin, big, label, italic, mono, pill, chip, repo, line (extra keywords
become props). write_edit makes scenes contiguous. Paths in edit.json are relative to public/.
5. edit.json reference
Top: {duration, speaker:{video, fg?, srcW, srcH, full:{scale,x,y}, pip:{scale,x,y}}, logos, captions:[{text,s,e,style}], scenes}.
Scene: type, start, end (s), world: light|dark, speaker: none|pip|full, enter: cut|dissolve|whipUp|whipLeft|morph,
optional worldAt:[{t,world}], hideCaptions, captionDy, y (vertical centre), pipIn (s; pip scenes: the PiP
rises in at that time instead of at the scene start - a column then sits at yFree (default 770) and glides to y).
Top-level offShots:[[t0,t1,why]] is copied from framing.json by write_edit (validate.py checks it).
Logo: {slug} | {text:"F5"} | {icon:"bolt"} + word, row, wordmark, mono, src, color.
| type | props |
|---|---|
column | items stacked, centred (y 600 with PiP, 770 without). kind: heavy, thin, big, label, italic, mono, pill, chip, repo, tiles, spacer. Item props: text, at, size, color, gap, anim:"rise"/"blur"/"none", strikeAt, strikeColor, strikeTilt. tiles: {style:"dark"/"clear", placeholder, size, tiles:[{logo, at, xAt}]}. repo: {label, variant:"cream"/"lime", blur, badge, icon, w, h} |
tool | title, logo, titleAt, tileAt, titleSize, lines:[{text, at, kind:"stat"/"text"/"sub"}], satellites:{at, items:[{logo:{slug,word}}], focus:{at, idx:[i,j]}} (6 satellites: 0 = top, clockwise, 3 = bottom front) |
list | mode:"fly"/"drop", items:[{label, at, blur, icon, badge}], blurLabels, pillH, y; drop: dropAt, highlight:{label, fromY, badge, blur} |
chat | model, tag, logo, greeting, prompt, typeAt, sendAt, reply, replyAt, cps, placeholder, at, h, y |
wave | top:{label, at}, bottom:{label, at}, growAt, y |
media | src (public/), aspect (w/h), at, w, h, y, highlights:[{box:[x,y,w,h] 0-1, label, at}], compare:{at, dur, before?, labels} |
cta | label, word, typeAt, line, lineAt, button, at, y |
full | - |
Cue times (at, xAt, strikeAt, typeAt, sendAt, lineAt, growAt, dropAt, titleAt, tileAt, pipIn, worldAt.t) are absolute | |
seconds inside their scene; omitted at = scene start. Defaults are tuned to the reference - override sparingly. |
6. QA (always)
validate.pyhas no ERROR; read every WARN (an off-shot WARN means the speaker shows a wrong angle - fix it).- Stills at frame 0, mid-scene for each scene, +2 f after every dissolve/whip, and on each cue. Contact sheet -> look: text inside x 70-1010, nothing under the caption or the PiP head, logos right (white on dark tiles), captions readable, no empty frames, matte clean (no holes / halos / smoky objects) on full-screen shots and on the PiP head (work/matte_check.jpg). Frame 0 is the thumbnail: finished text, finished caption, no half-cut head or foreign camera angle in the PiP.
- After rendering:
ffprobe(1080x1920, 30 fps, audio, duration = clip) and 3-4 extracted frames.