# Voiceover Generation Guide Generate professional AI voiceover using ElevenLabs, precisely timed and content-matched to video scenes. ## Requirements - `ELEVEN_LABS_API_KEY` environment variable - `ffmpeg` installed for audio processing - `whisper` CLI or Python package for timing verification - Python 3.x for the generation script ## Critical Workflow: Scene-Voiceover Alignment **The #1 problem with promo video voiceovers is misalignment between what's being said and what's on screen.** Follow this precise workflow: ### Step 1: Extract Scene Timings from Remotion First, read the main composition file to extract exact scene timings: ```javascript // Example from AIVoicePromo.jsx - extract these values: const OPENING_DURATION = 150; // 5 seconds (frames / 30fps) const PAIN_POINT_DURATION = 120; // 4 seconds each const SOLUTION_INTRO_DURATION = 120; const FEATURE_DURATION = 150; // 5 seconds each const RESULTS_DURATION = 180; // 6 seconds const CLOSING_DURATION = 240; // 8 seconds ``` Calculate cumulative timestamps: ``` Scene | Start | End | Duration -------------------|-------|------|---------- Opening | 0s | 5s | 5s Pain Point 1 | 5s | 9s | 4s Pain Point 2 | 9s | 13s | 4s Pain Point 3 | 13s | 17s | 4s Solution Intro | 17s | 21s | 4s Feature 1 | 21s | 26s | 5s Feature 2 | 26s | 31s | 5s Feature 3 | 31s | 36s | 5s Feature 4 | 36s | 41s | 5s Feature 5 | 41s | 46s | 5s Results | 46s | 52s | 6s Closing | 52s | 60s | 8s ``` ### Step 2: Extract Scene Content for Script Matching Read each scene component to understand what's visually displayed: ```bash # For each scene, extract: # - Title text # - Subtitle text # - Stats displayed # - Key visual elements ``` **The voiceover MUST reference what's on screen.** Example mapping: | Scene | Visual Content | Voiceover Should Say | |-------|---------------|---------------------| | Opening | "Every Missed Call Costs You Money", stats: $650K, 38%, 4hrs | "Every missed call costs you money. With six hundred and fifty thousand dollars lost annually..." | | Pain Point 1 | "Missed Calls = Lost Revenue", stat: 500+ | "Your dealership handles over five hundred service calls every month..." | | Feature 1: Smart Voicemail Queue | Shows queue UI mockup | "The Smart Voicemail Queue transcribes every message instantly..." | ### Step 3: Write Time-Aligned Script Structure your script with EXACT scene boundaries: ``` [0-5s - Opening - Visual: "Every Missed Call Costs You Money"] Every missed call costs you money. With six hundred and fifty thousand dollars lost annually... [5-9s - Pain Point 1 - Visual: "Missed Calls = Lost Revenue", 500+ stat] Your dealership handles over five hundred service calls every month... [17-21s - Solution Intro - Visual: ShopLoader logo reveal] Introducing ShopLoader AI Voice. Turn voicemails into appointments in minutes. ``` **Content Rules:** - Reference the title/stat shown on screen - Don't describe something not visible - Time the "reveal" moment (e.g., "Introducing ShopLoader" exactly when logo appears) ### Step 4: Generate Voiceover Sections Use the generation script with precise start times that leave ~1s buffer before scene transitions: ```python sections = [ (1, "Every missed call costs you money."), # Scene starts at 0s, speak at 1s (6, "Your dealership handles..."), # Scene starts at 5s, speak at 6s (18, "Introducing ShopLoader AI Voice."), # Scene starts at 17s, speak at 18s # ... ] ``` ### Step 5: Verify Timing with Whisper Transcription **CRITICAL STEP - Do not skip!** After generating the voiceover, transcribe it to verify actual timing: ```bash # Using Whisper CLI whisper voiceover.mp3 --model tiny --output_format srt # Or using Python python3 -c " import whisper model = whisper.load_model('tiny') result = model.transcribe('voiceover.mp3') for s in result['segments']: print(f\"{s['start']:.1f}s - {s['end']:.1f}s: {s['text']}\") " ``` ### Step 6: Compare and Validate Compare Whisper output against scene timings: ``` Expected vs Actual Timing Analysis: ----------------------------------- Scene: Solution Intro (17-21s) Expected: "Introducing ShopLoader" starts at 18s Actual: "Introducing ShopLoader" starts at 17.2s ✓ (within scene) Scene: Feature 1 (21-26s) Expected: "Smart Voicemail Queue" at 22s Actual: "Smart Voicemail Queue" at 20.8s ✗ (OVERLAPS with previous scene!) ``` **If overlap detected:** Increase delay for that section and regenerate. ### Step 7: FIX ALL OVERLAPS (Mandatory) **If ANY overlap is detected, you MUST fix it before proceeding. Do not ask the user.** Overlap fixes (in order of preference): 1. **Shorten the text** - Make it punchier. Cut filler words. "The Smart Voicemail Queue transcribes every message instantly" → "Voicemails transcribed instantly" 2. **Add a beat** - Insert "..." in the text to create a natural pause 3. **Increase gap** - Push the next section's start time 1-2s later Other timing fixes: - **Speech ends after scene**: Shorten the text, remove unnecessary words - **Too much silence**: Decrease start time or add more content - **Wrong content timing**: Adjust start time to match visual reveal **After ANY fix: Regenerate and verify with Whisper again. Repeat until ZERO overlaps.** --- ## Script Writing Guidelines ### Content-Scene Matching Rules 1. **Reference what's visible**: If the screen shows "500+ calls", say "five hundred" 2. **Match reveal timing**: Product name first spoken when logo appears 3. **Describe features when shown**: Talk about "SMS Quick Response" during SMS mockup scene 4. **Complete the thought with benefit**: Don't just name features, explain why they matter ### DO: - Write complete thoughts, not fragments - Allow 1s buffer at scene start before speaking - Allow 0.5s buffer before scene end - Match spoken stats to visual stats - Use conversational, natural language ### DON'T: - Don't reference visuals not yet shown - Don't continue speaking into the next scene - Don't make it too dense (audio needs breathing room) - Don't skip the emotional hook in the opening --- ## Script Template (60 seconds) ``` [0-5s - Opening - Visual: Hook headline + 3 stats] [Start speaking at 1s] Every missed call costs you money. With [stat from screen] and [stat from screen], your [consequence]. [5-9s - Pain Point 1 - Visual: Title + stat] [Start speaking at 6s] [Reference the title shown]. [Expand with the stat displayed]. [9-13s - Pain Point 2 - Visual: Title + stat] [Start speaking at 10s] [Reference the title shown]. [Emotional consequence]. [13-17s - Pain Point 3 - Visual: Title + stat] [Start speaking at 14s] [Reference the title shown]. [What happens as a result]. [17-21s - Solution Intro - Visual: Product logo reveal] [Start speaking at 18s - TIME THIS TO LOGO APPEARANCE] Introducing [Product Name]. [One-line value prop]. [21-26s - Feature 1 - Visual: Feature title + UI mockup] [Start speaking at 22s] [Feature name from screen] [what it does], so [benefit to user]. [26-31s - Feature 2] [Start speaking at 27s] [Feature name from screen] [what it does], so [benefit to user]. [31-36s - Feature 3] [Start speaking at 32s] [Feature name from screen] [what it does], so [benefit to user]. [36-41s - Feature 4] [Start speaking at 37s] [Feature name from screen] [what it does], so [benefit to user]. [41-46s - Feature 5] [Start speaking at 42s] [Feature name from screen] [what it does], so [benefit to user]. [46-52s - Results - Visual: Outcome stats] [Start speaking at 47s] [Stat from screen]. [Stat from screen]. [Emotional benefit]. [52-60s - Closing - Visual: CTA + branding] [Start speaking at 53s] [Product Name]. [Tagline from screen]. [CTA]. ``` --- ## ElevenLabs Voice Settings ### Recommended Voice: Matilda - **Voice ID**: `XrExE9yKIg1WjnnlVkGX` - **Characteristics**: American, Professional, Knowledgeable, Clear ### Voice Settings for Professional Delivery: ```json { "model_id": "eleven_multilingual_v2", "voice_settings": { "stability": 0.65, "similarity_boost": 0.85, "style": 0.2, "use_speaker_boost": true } } ``` **Adjustments:** - If too breathy: Increase `stability` to 0.70-0.75 - If too robotic: Decrease `stability` to 0.55-0.60 - If speech too fast: Decrease `style`, or add "..." pauses in text --- ## Generation & Verification Workflow ### Full Pipeline: ```bash # 1. Generate voiceover sections python ${SKILL_DIR}/scripts/generate_voiceover.py # 2. Transcribe to verify timing whisper voiceover.mp3 --model tiny --output_format srt # 3. Review SRT file against scene timings cat voiceover.srt # 4. If timing issues found, adjust sections[] start times and regenerate # 5. Normalize audio ffmpeg -y -i voiceover.mp3 -af "loudnorm=I=-16:TP=-1.5:LRA=11" voiceover-normalized.mp3 # 6. Add background music ffmpeg -y -i voiceover-normalized.mp3 -i music.mp3 \ -filter_complex "[1:a]volume=0.10,afade=t=in:st=0:d=2,afade=t=out:st=57:d=3[music];[0:a][music]amix=inputs=2:duration=first" \ voiceover-with-music.mp3 # 7. Combine with video ffmpeg -y -i video.mp4 -i voiceover-with-music.mp3 -c:v copy -map 0:v:0 -map 1:a:0 final.mp4 # 8. Watch final video to confirm alignment ``` --- ## Troubleshooting **CRITICAL: Never accept overlaps. Fix them immediately and regenerate.** | Issue | Cause | Solution | |-------|-------|----------| | **Voiceover overlaps itself** | Sections too close | **FIX NOW**: Shorten text OR increase gap, regenerate, verify again | | **Even 0.4s overlap** | Text too long for scene | **FIX NOW**: Make text punchier, cut words, regenerate | | Speech doesn't match screen | Script not aligned | Re-read scene components, match text to visuals | | "Introducing X" before logo | Start time too early | Delay to 1s after scene start | | Feature description during wrong scene | Timing drift | Use Whisper to find actual timestamps, adjust | | Voice too fast | Too much text | Shorten text or add "..." for pauses | | Awkward cuts between scenes | No buffer | Leave 0.5-1s silence before scene transitions | **The loop: Generate → Whisper verify → Fix overlaps → Regenerate → Verify again → Repeat until clean** --- ## Background Music ### Auto-Download Royalty-Free Tracks (Verified Working) ```bash # Bensound - "Inspire" (corporate/uplifting) - RECOMMENDED curl -sL "https://www.bensound.com/bensound-music/bensound-inspire.mp3" -o background-music.mp3 # Bensound - "Creative Minds" (light/positive) curl -sL "https://www.bensound.com/bensound-music/bensound-creativeminds.mp3" -o background-music.mp3 # Pixabay - Corporate background (256kbps, high quality) curl -sL "https://cdn.pixabay.com/download/audio/2022/03/15/audio_8cb749d484.mp3" -o background-music.mp3 # Verify download worked ls -lah background-music.mp3 && file background-music.mp3 ``` ### Mixing Settings - **Volume**: 10% of voice level (`volume=0.10`) - **Fade in**: 2 seconds at start (`afade=t=in:st=0:d=2`) - **Fade out**: 3 seconds before video end (`afade=t=out:st=57:d=3` for 60s video) - **Style**: Subtle corporate/tech underscore, no lyrics ### Mix Command ```bash ffmpeg -y -i voiceover-normalized.mp3 -i background-music.mp3 \ -filter_complex "[1:a]volume=0.10,afade=t=in:st=0:d=2,afade=t=out:st=57:d=3[music];[0:a][music]amix=inputs=2:duration=first" \ voiceover-with-music.mp3 ``` ### Other Sources (if auto-download fails) - Bensound.com - Free with attribution - Mixkit.co - Free, no attribution needed - Pixabay.com/music - Free - Epidemic Sound (subscription) - Artlist (subscription)