Vertical Reels Video Studio (pro)
End-to-end: (first run: ask for the user's handles/watermark/CTA/voice) → topic → research → draft → approval → voiceover → timing → Remotion scenes → render → Reels export → QA → covers + captions → deliver.
This is the fuller version of vertical-reels-video-studio (basic). The brand config lives in brand.config.json (read by src/brand.ts and passed to compositions as the brand prop).
0a. First run: ask the user for THEIR settings (never use defaults)
Before the first video, if brand.config.json is missing, has "configured": false, or still contains @yourhandle,
ask the user (one question at a time) and save the answers in brand.config.json:
- Social handles: Instagram, Threads, TikTok, X (whichever they use).
- Watermark text (exactly as it should appear, e.g.
@yourhandleor@ig / @x) and position (bottom-center,bottom-left,top-left,top-right). - CTA text and the handle it mentions.
- Voice: their ElevenLabs voice id + model + language, or "I'll record my own voice". The API key is requested only as the
ELEVENLABS_API_KEYsecret. - Language and tone; how the voice should say their name/handle.
Then set "configured": true. Never invent a handle, never reuse someone else's (including the author of this kit), and never render a watermark that still says @yourhandle.
0b. Brand config (brand.config.json, filled from the user's answers)
| Key | Placeholder | Notes |
|---|---|---|
| Handles | {{IG_HANDLE}}, {{THREADS_HANDLE}}, {{TIKTOK_HANDLE}}, {{X_HANDLE}} | handles.*, the user's own |
| Watermark | {{WATERMARK}} + position | watermark.text / watermark.position, exact text the user gave; never hard-code in scenes |
| Language / tone | {{LANGUAGE}}, {{TONE}} | e.g. casual Indonesian gua/lu, sparse emoji |
| Spoken name | {{SPOKEN_NAME}} | How the voice says your handle (e.g. "Jane Doe" for @janedoe_). Captions still show the real handle |
| Disclaimer | {{DISCLAIMER}} | For finance topics, e.g. "Education, not financial advice" |
| Voice | {{VOICE_ID}}, {{TTS_MODEL}}, {{LANGUAGE_CODE}} | voice.*, the user's choice (or their own recording) |
| API key | env ELEVENLABS_API_KEY | Never print, log or commit it |
| CTA | {{CTA}}, {{CTA_HANDLE}} | cta.text / cta.handle, asked from the user (e.g. "Follow {{CTA_HANDLE}} for more", "Comment TUTORIAL") |
| Hard rules | {{HARD_RULES}} | Brands never to mock, names to blur, words never to say |
| Render device | {{RENDER_DEVICE}} | GPU machine or CPU. Always report which one was actually used |
1. Research and draft (no audio, no render)
- Fact-check every claim with primary sources: official docs/PRs, the author's own post, registries, dated screenshots. Exec posts count as official statements. Label estimates (
ESTIMATE · not official) and allegations (ALLEGED). Drop what you cannot verify instead of softening it. - One story per video, everyday words, slow pace. Hard product names go on screen, not in the voiceover.
- Offer 2–4 hook options (H1, H2, …): a why/how question or a bold contrast. Let the user pick.
- Timed table:
time | on-screen | VO. Target 45–65 s, hard max 90 s (aim ≤ 88 s so a revision still fits). - Write
factcheck.md(claim → source → status) andsources.md. - Send the draft and wait for approval. "Don't render" from the user is final until they say go.
2. Voiceover (ElevenLabs)
- Two scripts:
script-display.txt(what captions show) andscript-tts.txt(spelled for the voice). Keep a display↔TTS map. - Pronunciation (see
docs/VOICEOVER.md):A.I.with dots so it sounds English; tickers letter by letter in non-English VO (ETH →E, T, H); numbers as words; English terms in plain English spelling (no hyphen respellings); a comma after a final /t/ before a vowel; brand names respelled only when tests prove it helps. - Post-process
atempo1.05 (1.1 for fast content). Loudness −14 LUFS integrated, ≤ −1 dBTP. - No duplicate lines. If a line repeats the hook or an earlier beat, cut or reword it.
- Check the character quota first. Re-voice only failing lines (pass
previous_textfor continuity).
3. Timing
- Word timestamps from the TTS alignment (divide by the tempo factor) or faster-whisper →
timing.json{duration, phrases:[{s,e,words:[{w,s,e}]}], scenes:[{id,start}]}(scripts/build-timing.py). - Merge respelled tokens back to display words (
A.I.→AI,E, T, H→ETH) or captions start late. - Scenes key off spoken words:
wt(scene, word)→ frame where that word starts.
4. Motion style (Remotion, 1080x1920, 30 fps)
Look: base #0A1016, cream #EFE6D2, amber #FFB000, red #E2553F, green #7BD88F. Fonts via @remotion/google-fonts: Big Shoulders 800 (headlines), Plus Jakarta Sans (body/captions), JetBrains Mono (chips/code).
Layout: important content y 200–1500. Footer cluster: karaoke captions ~y 1310–1353, watermark (if the user chose bottom) ~y 1427–1446, progress ~y 1474, all left of x 960 (Reels draws its buttons on the right).
No empty frames: the hook is readable at frame 0; the opening and the ending are as full as the middle (a hero image, the presenter, a chip), never a lone line on a blank background.
Easing helpers (src/motion/ease.ts):
const clamp01=(x:number)=>Math.max(0,Math.min(1,x));
const P=(t:number,a:number,b:number)=>clamp01((t-a)/(b-a));
const eOut=(x:number)=>1-Math.pow(1-x,3);
const eIn=(x:number)=>x*x*x;
const backOut=(x:number,s=1.70158)=>1+(s+1)*Math.pow(x-1,3)+s*Math.pow(x-1,2);
const sec=(f:number,at:number)=>(f-at)/30;
const ap=(f:number,at:number,d=0.3)=>eOut(P(sec(f,at),0,d));
const pop=(f:number,at:number)=>Math.max(0,backOut(P(sec(f,at),0,0.35),2.2));
const punch=(f:number,at:number)=>{const t=sec(f,at);if(t<0)return 1;return 1+0.12*eOut(P(t,0,0.2))-0.06*eOut(P(t,0.2,2.5));};
const shake=(f:number,at:number)=>{const t=sec(f,at);return t<0||t>0.4?0:14*Math.exp(-t*10)*Math.sin(t*70);};
Moves: kinetic type (word lands on its VO time, scale(0.6+0.4*pop), ±4° settling over 0.25 s, stressed words bigger + amber/red), zoom punch on key images, circle-mask reveal, zoom-through transitions (scale 1→4.2 over ~9 frames, next scene 1.3→1), rotated stamps and mono chips, red highlight boxes on screenshots, step counter, karaoke captions (current word amber + 1.08×), CTA pop with shake, whoosh/pop SFX under the VO. About one beat every 1.5–3 s.
Video clips: wrap every <OffthreadVideo> in a <Sequence from={at}> and set startFrom relative to it; otherwise the clip clamps to its last frame and looks frozen. Loop short clips for long beats. Clips start on the first usable frame.
Audio transitions: when an opening uses live audio (a song, a clip) before the narration, fade it to silence and leave ~0.3–0.5 s (a flash/whoosh transition) before the first VO word so it is never masked.
Optional presenter (mascot) with lip sync
- One consistent character identity across every video. Separate layers: body (closed mouth painted out), arm on a shoulder pivot under the coat edge, a static shoulder cap, mouth sprites
closed/half/open/wide/round. - Drive the mouth from the VO wav, not a loop:
scripts/build-mouth-track.py→ per-frame states (closed in silence and end hold, 2-frame minimum hold). Clamp the arm to the rig's range; a whole-image bob alone is not enough. - Mount the presenter only while on screen; covers use the static closed-mouth body.
5. Render
- Prefer a GPU machine if you have one (Remotion
--hardware-acceleration, a config withoutencodingBufferSize/encodingMaxRate, which break required NVENC); fall back to CPU only if allowed. Always say which device rendered. - Raw render:
yuv420p+bt709inremotion.config.ts, then the Reels pass.
6. Reels-safe export (scripts/reels-export.sh, details in docs/REELS_SAFE_EXPORT.md)
1080x1920 SAR 1, H.264 High level 4.1, yuv420p TV range, BT.709 tags, CFR 30, GOP 60 closed, 10 Mbps (min 8, max 12), two-pass loudnorm −14 LUFS / −1.5 dBTP, AAC 192k 48 kHz stereo, +faststart, < 90 s. Also a small preview < 40 MB (scripts/make-preview.sh).
7. QA (mandatory, before delivery)
- Whisper the final MP4 (
scripts/whisper-check.py): wrong pronunciations, stutters, stray sounds, duplicate lines, first word of every scene audible. - Contact sheet at every scene boundary (
scripts/contact-sheet.sh): glitches, overlaps, cut-off text, safe zone, frozen clips, empty opening/ending, watermark present, matchingbrand.config.jsonand spelled exactly. scripts/qa-check.sh: codec/profile/level, pix_fmt, range, colour tags, CFR, AAC 48k, moov before mdat, duration < 90 s, −14 LUFS.- Fix and re-render. Keep the previous version when you revise.
8. Always deliver with the video
- Captions for IG (hook, body, CTA, light hashtags), Threads (shorter, no hashtags), TikTok, plus an
.srt. - Four covers A–D: 1080x1920 + 1080x1350 grid, key text inside the centre 1080x1080; one hook, one main photo, max 2–3 number cards; contact sheet + let the user pick.
- Source credits and the fact-check file.
- A line in
videos-log.md: title, files, duration, loudness, device, sources, status "not posted".
Rules
- Never post, DM, upload or buy. Deliver files.
- No fabricated facts, stars, prices or results. Credit open-source authors on screen.
- Real photos, logos and clips only; no AI-generated faces of real people. Mark mock-ups
illustration. - Use media you have rights to; tell the user when music or clips are copyrighted (mute/claim risk).
- No secrets in files. Ask any question last, in its own message.