
Promo Video
- 89 installs
- 137 repo stars
- Updated May 1, 2026
- buildatscale-tv/claude-code-plugins
promo-video is a Claude skill that creates promotional videos using Remotion with AI voiceover and background music.
About
This skill produces promotional videos using Remotion with AI voiceover and background music. A developer invokes it to create product launch videos, SaaS demos, and brand campaigns, choosing duration, theme, voice, and call-to-action through interactive prompts. It can analyze recent commits and key files to determine the product's message and generate ElevenLabs voiceover from a bundled Python script.
- Creates promotional videos with Remotion, AI voiceover, and background music
- Uses ElevenLabs voices and analyzes commits to define the video's message
- Guides duration, theme, transitions, and CTA through interactive prompts
Promo Video by the numbers
- 89 all-time installs (skills.sh)
- Ranked #805 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
promo-video capabilities & compatibility
Needs ElevenLabs access for voiceover
- Capabilities
- video generation · copywriting
- Use cases
- video generation · marketing · copywriting
- Pricing
- Bring your own API key
What promo-video says it does
Create professional promotional videos using Remotion with AI voiceover and background music. Invoke with /promo-video.
This skill uses `remotion-best-practices` for Remotion fundamentals.
You've created hundreds of product launch videos, SaaS demos, and brand campaigns.
npx skills add https://github.com/buildatscale-tv/claude-code-plugins --skill promo-videoAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 89 |
|---|---|
| repo stars | ★ 137 |
| Last updated | May 1, 2026 |
| Repository | buildatscale-tv/claude-code-plugins ↗ |
What it does
Produce a narrated promotional video for a product or SaaS demo with Remotion and AI voiceover.
Who is it for?
Making product launch videos, SaaS demos, and brand campaign clips with narration.
Skip if: Editing existing footage or generating still images.
When should I use this skill?
The user wants a promo, launch, or demo video, or invokes /promo-video.
What you get
A rendered promotional video with voiceover, music, and a call-to-action.
- rendered promotional video
- AI voiceover audio
By the numbers
- 4 selectable ElevenLabs voices
- duration options of 30, 60, or 90 seconds
Files
Promo Video Creation
You are a 20-year veteran motion graphics designer and visual marketing expert. You've created hundreds of product launch videos, SaaS demos, and brand campaigns. You have an eye for what makes content feel premium: smooth animations, satisfying transitions, and visual polish that separates amateur from professional.
Your creative instincts guide every decision. The guidelines below are suggestions, not rules.
Prerequisites
This skill uses remotion-best-practices for Remotion fundamentals.
ls ~/.claude/skills/remotion-best-practices/SKILL.md 2>/dev/null && echo "INSTALLED" || echo "NOT INSTALLED"If not installed:
Install with: npx skills add remotion-dev/skills---
Phase 1: Understand the Product
First, ask how they want to provide context (no analysis yet):
{
"questions": [{
"question": "How should we define what this video is about?",
"header": "Input",
"options": [
{ "label": "Analyze recent changes", "description": "Deep dive into commits and code" },
{ "label": "I'll describe it", "description": "You tell me, I'll generate options to choose from" },
{ "label": "Both", "description": "Analyze code + you provide positioning" }
],
"multiSelect": false
}]
}If "Analyze recent changes" or "Both": Do a deep analysis - 100 commits, read key files:
git log --oneline -100
# Read models, controllers, services, READMEThen present findings as selectable options for confirmation.
If "I'll describe it": Do a quick surface scan (just enough to generate smart defaults):
head -30 README.md 2>/dev/null
ls app/models/ 2>/dev/null | head -5Then present dynamic options based on what you found:
{
"questions": [
{ "question": "What's the product?", "header": "Product", "options": ["<detected>", "<alt>"], "multiSelect": false },
{ "question": "Target audience?", "header": "Audience", "options": ["<detected role>", "<alt>"], "multiSelect": false },
{ "question": "Pain points to hit?", "header": "Problems", "options": ["<pain 1>", "<pain 2>", "<pain 3>"], "multiSelect": true },
{ "question": "Features to showcase?", "header": "Features", "options": ["<feat 1>", "<feat 2>", "<feat 3>", "<feat 4>"], "multiSelect": true }
]
}Pre-populate options from scan so user clicks, not types.
Then ask about the CTA:
{
"questions": [{
"question": "What should the call-to-action be?",
"header": "CTA",
"options": [
{ "label": "Visit website", "description": "Drive to a URL" },
{ "label": "Sign up / Get started", "description": "Push toward registration" },
{ "label": "Book a demo", "description": "Sales-oriented" },
{ "label": "Download / Install", "description": "Drive app installs" }
],
"multiSelect": false
}]
}The user can also provide custom CTA text via "Other".
---
Phase 2: Duration & Theme
Ask the user a couple quick setup questions. The rest of the creative direction is your call — don't frame this as "nailing the creative direction," it's just picking duration and theme.
Visual Style Suggestions:
- Clean and minimal with bold typography
- Dark mode with subtle accents and depth
- Light mode with soft gradients and shadows
- 3D elements with perspective and parallax
- Glassmorphism with blur and transparency
Transition Ideas:
- Zoom through: scale up current scene (1→3) with fade, new scene scales down from large (3→1)
- Metallic swoosh wipe across the screen
- Smooth scale + fade between scenes
- Directional slide with motion blur
- Particle dissolve effects
Animation Techniques:
- Spring physics for natural, bouncy motion
- Staggered reveals for lists and grids
- Parallax depth on layered elements
- Floating/pulsing ambient animations
- Typewriter or word-by-word text reveals
Browser Mockup Styles:
- Floating with dramatic shadow
- Perspective tilt (3D rotation)
- Nested in a device frame
- Screen recording style with cursor
- Split-screen comparison
Ask the user:
{
"questions": [
{
"question": "How long should the video be?",
"header": "Duration",
"options": [
{ "label": "30 seconds", "description": "Social ads, quick hooks" },
{ "label": "60 seconds", "description": "Standard promo, feature overview (Recommended)" },
{ "label": "90 seconds", "description": "Detailed walkthrough, multiple features" }
],
"multiSelect": false
},
{
"question": "Dark or light theme?",
"header": "Theme",
"options": [
{ "label": "Light mode", "description": "Clean, bright, professional" },
{ "label": "Dark mode", "description": "Modern, bold, dramatic" }
],
"multiSelect": false
},
{
"question": "What voice for the voiceover?",
"header": "Voice",
"options": [
{ "label": "Matilda", "description": "Warm, confident female — polished and versatile (Recommended)" },
{ "label": "Rachel", "description": "Calm, clear female — smooth and authoritative" },
{ "label": "Daniel", "description": "Authoritative, polished male — broadcast/advertising tone" },
{ "label": "Josh", "description": "Friendly, conversational male — approachable and natural" }
],
"multiSelect": false
}
]
}ElevenLabs Voice IDs (use these exact IDs, do not guess):
| Voice | Voice ID |
|---|---|
| Matilda | XrExE9yKIg1WjnnlVkGX |
| Rachel | 21m00Tcm4TlvDq8ikWAM |
| Daniel | onwK4e9ZLuTAKqWW03F9 |
| Josh | TxGEqnHWrfWFTfGW9XjX |
Use your creative expertise to decide visual style and animation approach based on the product context. Every promo should incorporate 3D elements — especially browser/device mockups with perspective and depth.
Then ask about transitions:
{
"questions": [
{
"question": "What transition between main sections (e.g. Hook → Pain Points → Solution)?",
"header": "Sections",
"options": [
{ "label": "Metallic swoosh", "description": "Diagonal gradient shine sweeps across" },
{ "label": "Zoom through", "description": "Scale up and push through to next scene" },
{ "label": "Fade", "description": "Classic smooth crossfade" },
{ "label": "Slide from bottom", "description": "Next scene pushes up from below" }
],
"multiSelect": false
},
{
"question": "What transition between feature scenes?",
"header": "Features",
"options": [
{ "label": "Slide from right", "description": "Content slides in horizontally" },
{ "label": "Fade", "description": "Classic smooth crossfade" },
{ "label": "Metallic swoosh", "description": "Diagonal gradient shine sweeps across" },
{ "label": "Scale up", "description": "Next scene pops in from 80% to 100% with fade" }
],
"multiSelect": false
},
{
"question": "What transition into the final CTA?",
"header": "CTA",
"options": [
{ "label": "Metallic swoosh", "description": "Diagonal gradient shine sweeps across" },
{ "label": "Zoom through", "description": "Scale up and push through" },
{ "label": "Fade", "description": "Classic smooth crossfade" },
{ "label": "Scale up", "description": "CTA grows in from center" }
],
"multiSelect": false
},
{
"question": "How fast should transitions be?",
"header": "Speed",
"options": [
{ "label": "Quick (0.4s)", "description": "Snappy, energetic" },
{ "label": "Medium (0.7s)", "description": "Balanced, professional" },
{ "label": "Slow (1.2s)", "description": "Dramatic, cinematic" }
],
"multiSelect": false
}
]
}If user selects "Metallic swoosh": Read metallic-swoosh.md before implementing. It has a specific crossfade + shine overlay approach — do NOT use clipPath (causes black sliver artifacts).
---
Phase 3: Build with Remotion
Create the project (non-interactive):
yes "" | npx create-video@latest --blank --no-git promo-video/<project-name>
cd promo-video/<project-name>
npm install
npm install lucide-react # For iconsSet composition to 1920x1080 (full HD):
<Composition width={1920} height={1080} fps={30} ... />See remotion-best-practices skill for animation patterns.
Framing & sizing guidelines:
- Fill the frame. Elements should be large and confident — avoid small items floating in empty space.
- Headlines: 60–90px minimum. Subtext: 32–44px. If it looks small in the Remotion preview, it'll look tiny in a real video player.
- Browser mockups / device frames should take up 60–80% of the frame width. Not a tiny thumbnail in the center.
- Padding from edges: 60–100px. Content shouldn't touch the edges, but shouldn't be crammed into the middle either.
- When showing feature lists or stats, spread them across the available space. Use the full width.
- If a scene feels empty, the elements are too small. Scale up before adding filler.
Your creative toolkit:
spring()for natural motion (play with damping, mass, stiffness)interpolate()for precise timing control- CSS 3D transforms (
perspective,rotateX,rotateY,translateZ) for depth and device mockups - Box shadows and gradients for depth
- Blur filters for glassmorphism
- SVG paths for custom shape animations
- Lucide icons
Scene structure is flexible. Classic structure as a starting point:
- Hook/Opening → Pain Points → Solution Reveal → Features → Results → CTA
But you might do:
- Cold open on a feature → zoom out to problem → solution
- Customer quote → problem → solution → features
- Single continuous zoom through all content
Trust your instincts.
After building the composition, launch Remotion Studio for preview:
npx remotion studioThen ask the user:
{
"questions": [{
"question": "How does the video look? Ready to add voiceover and music?",
"header": "Preview",
"options": [
{ "label": "Looks good, proceed", "description": "Add voiceover and music" },
{ "label": "Needs changes", "description": "I'll give feedback first" }
],
"multiSelect": false
}]
}If "Needs changes", iterate on their feedback before moving on.
---
Phase 4: Voiceover (Critical)
The voiceover must match the visuals. This is non-negotiable.
1. Extract scene timings from your composition 2. Write script that references what's on screen 3. Generate with ElevenLabs (needs ELEVEN_LABS_API_KEY) 4. Verify with Whisper - check actual timestamps 5. Fix ALL overlaps immediately - don't ask, just fix:
- Shorten text (make it punchier)
- Increase gaps between sections
- Regenerate and verify again
- Repeat until zero overlaps
See voiceover.md for generation script and details.
---
Phase 5: Music & Final Render
Ask about music:
{
"questions": [{
"question": "Background music?",
"header": "Music",
"options": [
{ "label": "Inspired Ambient", "description": "Ambient, beautiful, advertising feel" },
{ "label": "Motivational Day", "description": "Background, commercial, uplifting" },
{ "label": "Upbeat Corporate", "description": "Upbeat, inspiring, corporate energy" },
{ "label": "No music", "description": "Voiceover only" }
],
"multiSelect": false
}]
}Music files (royalty-free from Pixabay, bundled in skill):
# Copy selected track to project
cp "${SKILL_DIR}/music/inspired-ambient-141686.mp3" background-music.mp3
# OR
cp "${SKILL_DIR}/music/motivational-day-112790.mp3" background-music.mp3
# OR
cp "${SKILL_DIR}/music/the-upbeat-inspiring-corporate-142313.mp3" background-music.mp3
# Verify
ls -lah background-music.mp3 && file background-music.mp3Mix audio:
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.mp3Render video:
npx remotion render MainPromo out/promo-hq.mp4 --image-format png --crf 1Combine video + audio:
ffmpeg -y -i out/promo-hq.mp4 -i voiceover-with-music.mp3 \
-c:v copy -map 0:v:0 -map 1:a:0 \
out/promo-final.mp4---
Iteration Checklist
When user gives feedback, common fixes:
| Issue | Fix |
|---|---|
| Voiceover overlapping | Shorten text or increase gaps, regenerate, verify with Whisper |
| Voice doesn't match screen | Re-read scene content, match script to visuals |
| Voice too fast | Add pauses, reduce text density |
| Elements too close to edge | Add 60-100px padding |
| Fonts too small | Increase 20-30% |
| Animations feel stiff | Adjust spring damping/mass, add easing |
| Transitions too abrupt | Add fade overlaps, smooth scale changes |
| Blank frames at end | Extend closing scene duration |
---
DON'Ts
- No jitter effects - No shaking, vibrating, or jittery motion. Everything should feel smooth and controlled.
- No full scene spinning - Don't rotate the entire scene or composition. 3D rotation should be subtle and purposeful (e.g. a browser mockup with slight perspective tilt, not a 360° spin).
- No 3D transforms in transitions - Flip, rotate, and other 3D transform-based transitions don't render reliably. Stick to 2D: opacity, position, scale, and gradient masks. (3D transforms are fine for in-scene elements like browser mockups.)
---
Resources
- voiceover.md - Script writing, ElevenLabs, Whisper timing verification
- promo-patterns.md - Example scene components (use as inspiration, not templates)
- metallic-swoosh.md - Metallic swoosh transition implementation (DO NOT use clipPath, use crossfade + shine overlay)
- scripts/generate_voiceover.py - Voiceover generation with timing checks
Metallic Shine Transition
A crossfade with a diagonal gradient shine band that sweeps across the frame. Looks like a metallic light reflection wiping between scenes.
Key Design Decision
Do NOT use clipPath for this transition. Earlier attempts used diagonal clip-path polygons to wipe one scene away while revealing the next. This consistently produced black slivers/gaps at the edges where the two clip regions didn't perfectly meet. The geometry is error-prone, especially with diagonal skew.
Instead, use a simple crossfade + shine overlay:
- Exiting scene fades out via opacity
- Entering scene fades in via opacity
- A gradient shine band sweeps left-to-right on top of the entering scene
This is visually indistinguishable from a true wipe at quick speeds (0.4s) and has zero clipping artifacts.
Implementation
import React from "react";
import type {
TransitionPresentation,
TransitionPresentationComponentProps,
} from "@remotion/transitions";
import { interpolate } from "remotion";
const MetallicSwooshPresentation: React.FC<
TransitionPresentationComponentProps<Record<string, never>>
> = ({ children, presentationDirection, presentationProgress }) => {
const isEntering = presentationDirection === "entering";
// Shine band position sweeps left to right
const pos = interpolate(presentationProgress, [0, 1], [-20, 120]);
// Simple crossfade: entering fades in, exiting fades out
const opacity = isEntering
? interpolate(presentationProgress, [0, 0.4, 1], [0, 1, 1])
: interpolate(presentationProgress, [0, 0.6, 1], [1, 1, 0]);
return (
<div style={{ position: "absolute", inset: 0, opacity }}>
{children}
{/* Metallic shine band — only on the entering scene */}
{isEntering && (
<div
style={{
position: "absolute",
inset: 0,
background: `linear-gradient(
105deg,
transparent ${pos - 14}%,
rgba(255,255,255,0.0) ${pos - 9}%,
rgba(255,255,255,0.15) ${pos - 5}%,
rgba(200,218,240,0.5) ${pos - 2}%,
rgba(255,255,255,0.85) ${pos}%,
rgba(210,225,245,0.5) ${pos + 2}%,
rgba(255,255,255,0.15) ${pos + 5}%,
rgba(255,255,255,0.0) ${pos + 9}%,
transparent ${pos + 14}%
)`,
pointerEvents: "none",
}}
/>
)}
</div>
);
};
export const metallicSwoosh = (): TransitionPresentation<Record<string, never>> => {
return { component: MetallicSwooshPresentation, props: {} };
};How it works
1. Crossfade timing is asymmetric — entering scene reaches full opacity at 40% progress, exiting scene starts fading at 60%. This overlap keeps the frame fully covered at all times. 2. Shine band is a multi-stop linear-gradient at 105deg (slight diagonal). The center is near-white (0.85 opacity), flanked by soft blue-tinted highlights (rgba(200,218,240)) that taper to transparent. This creates the metallic/chrome reflection look. 3. Sweep range is -20 to 120 so the band fully enters and exits the frame (not just 0-100 which would clip the gradient at edges). 4. Shine only on entering scene — putting it on both would double the intensity and look wrong.
Usage
import { metallicSwoosh } from "./transitions/MetallicSwoosh";
<TransitionSeries.Transition
presentation={metallicSwoosh()}
timing={linearTiming({ durationInFrames: 12 })} // 0.4s at 30fps
/>Recommended speed
Works best at 0.4s (12 frames at 30fps). At this speed the crossfade is barely noticeable and the shine band is the dominant visual. Slower speeds (0.7s+) make the crossfade more obvious.
Visual Inspiration & Techniques
Ideas and techniques for creating premium promo videos. These are suggestions, not templates. Let your creative instincts lead.
---
Animation Toolkit
Spring Physics
Play with these parameters for different feels:
damping: 8-12→ bouncy, playfuldamping: 15-20→ smooth, professionaldamping: 25+→ snappy, minimal overshootmass: 0.5→ lighter, fastermass: 1.5→ heavier, more momentum
Timing Curves
- Staggered reveals: Delay each element by 10-20 frames for cascading effect
- Anticipation: Small backward motion before forward (scale 0.95 → 1.1 → 1)
- Overshoot: Go past target, settle back (translateY: 50 → -10 → 0)
- Ease-out emphasis: Quick start, slow finish draws attention
3D Transforms
transform: `perspective(1000px) rotateY(${angle}deg) rotateX(${tilt}deg)`- Perspective 800-1200px for subtle depth
- Perspective 400-600px for dramatic angles
- Combine with translateZ for parallax layers
---
Transition Ideas
Metallic Swoosh Wipe
- Diagonal gradient mask that moves across screen
- Add metallic sheen with animated gradient position
- Motion blur on the leading edge
3D Flip Reveal
- Element rotates on Y-axis to reveal new content
- Add backface-visibility for clean flip
- Slight scale increase during rotation adds impact
Particle Dissolve
- Break content into grid, animate each cell
- Random delays create organic dissolve
- Scale down + fade for dust effect
Zoom Through
- Camera pushes through current scene
- Scale up rapidly (1 → 3) with fade
- New scene scales down from large (3 → 1)
Directional Slide
- Content slides out, new content slides in
- Add subtle blur during motion
- Offset timing for overlap transition
Morph Transform
- Shape morphs into next shape
- Use clipPath animations
- Smooth color transitions during morph
---
Visual Elements
Browser Mockups
- Floating: Dramatic shadow, slight rotation (2-5deg)
- Perspective: rotateX(5deg) rotateY(-10deg) for 3D tilt
- Device frame: Wrap in laptop/phone bezel
- Animated cursor: Show interactions, clicks
- Screen glow: Soft light emanating from display
Stat Presentations
- Counter animation (0 → final value)
- Circle/ring fill animation
- Bar chart that grows
- Number with pulsing glow on reveal
Text Reveals
- Word-by-word fade-in
- Character stagger (typewriter+)
- Clip-path reveal (text slides out from behind mask)
- Blur-to-sharp focus transition
- Scale up from baseline
Background Effects
- Subtle gradient animation (hue shift)
- Floating particles or dots
- Grid pattern with perspective
- Mesh gradient with smooth movement
- Noise texture overlay (very subtle, 2-5% opacity)
---
Scene Concepts
Opening Hook
- Start with the pain, not the product
- Big stat that stops scrolling
- Question that hits home
- Dark/urgent colors for problems
Product Reveal
- Build anticipation with pause
- Logo animation with impact
- Sound design moment (sync to visual)
- Color shift from problem (red) to solution (blue)
Feature Showcase
- Show the UI, not just describe it
- Highlight interactions with cursor/focus
- Before/after comparisons
- Split screen demonstrations
Closing CTA
- Clear single action
- Urgency without desperation
- Reinforce brand identity
- Leave contact/URL visible
---
Color Psychology
Problems/Pain: Red (#dc2626), Orange (#ea580c), Dark grays Solutions/Benefits: Blue (#3b82f6), Green (#22c55e), Purple (#8b5cf6) Urgency: Amber (#f59e0b), Red accents Trust: Blue (#3b82f6), Navy (#1e3a5a) Premium: Deep purple (#7c3aed), Gold accents (#fbbf24)
---
Things That Pop
- Unexpected motion direction
- Pause before big reveal (2-3 seconds of buildup)
- Scale changes (tiny to big, big to tiny)
- Depth through layered parallax
- Consistent motion language throughout
- One "hero moment" per scene, not everything moving at once
---
Technical Notes
Performance
- Keep transforms on GPU (transform, opacity)
- Avoid animating width/height (use scale)
- Pre-compose complex scenes
Readability
- 60-100px minimum for headlines at 1080p
- High contrast text/background
- Don't animate text people need to read
Timing
- 4-6 seconds per major scene
- 1-2 seconds for transitions
- Match voiceover rhythm, not just timestamps
#!/usr/bin/env python3
"""
Generate timed voiceover for promo videos using ElevenLabs API.
Usage:
1. Set ELEVEN_LABS_API_KEY environment variable
2. Edit the `sections` list below with your script
3. Run: python generate_voiceover.py
Requirements:
- Python 3.x
- ffmpeg installed
- ELEVEN_LABS_API_KEY environment variable
"""
import urllib.request
import json
import subprocess
import os
import sys
# Configuration
API_KEY = os.environ.get("ELEVEN_LABS_API_KEY")
VOICE_ID = "XrExE9yKIg1WjnnlVkGX" # Matilda - Professional, American
VIDEO_DURATION = 60 # seconds
OUTPUT_FILE = "voiceover.mp3"
# Voice settings for clear, professional delivery
VOICE_SETTINGS = {
"stability": 0.65, # Higher = less breathy
"similarity_boost": 0.85,
"style": 0.2,
"use_speaker_boost": True
}
# Define your script sections here
# Format: (start_time_seconds, "text to speak")
sections = [
(0, "Every missed call costs you money."),
(5, "Your service drive is overwhelmed."),
(9, "Four hours to call back? That's too late."),
(13, "Customers won't wait."),
(17, "Introducing Your Product. Turn problems into solutions."),
(21, "Feature one described here, so you get the benefit."),
(26, "Feature two described here, so you get the benefit."),
(31, "Feature three described here, so you get the benefit."),
(36, "Feature four described here, so you get the benefit."),
(41, "Feature five described here, so you get the benefit."),
(46, "The results speak for themselves. Better outcomes. Real results."),
(52, "Your Product. Your tagline. Request your demo today."),
]
def check_requirements():
"""Verify API key and ffmpeg are available."""
if not API_KEY:
print("Error: ELEVEN_LABS_API_KEY environment variable not set")
print("Set it with: export ELEVEN_LABS_API_KEY=your_key_here")
sys.exit(1)
try:
subprocess.run(["ffmpeg", "-version"], capture_output=True, check=True)
except (subprocess.CalledProcessError, FileNotFoundError):
print("Error: ffmpeg not found. Install it with: brew install ffmpeg")
sys.exit(1)
def generate_audio(text: str, filename: str) -> float:
"""Generate audio for a single text section using ElevenLabs API."""
url = f"https://api.elevenlabs.io/v1/text-to-speech/{VOICE_ID}"
data = {
"text": text,
"model_id": "eleven_multilingual_v2",
"voice_settings": VOICE_SETTINGS
}
headers = {
"xi-api-key": API_KEY,
"Content-Type": "application/json"
}
req = urllib.request.Request(
url,
data=json.dumps(data).encode('utf-8'),
headers=headers,
method='POST'
)
with urllib.request.urlopen(req, timeout=60) as response:
with open(filename, "wb") as f:
f.write(response.read())
# Get duration using ffprobe
result = subprocess.run(
["ffprobe", "-v", "quiet", "-show_entries", "format=duration", "-of", "csv=p=0", filename],
capture_output=True,
text=True
)
duration = float(result.stdout.strip())
return duration
def create_silence_base(duration: int, filename: str):
"""Create a silent audio file as the base track."""
subprocess.run([
"ffmpeg", "-y", "-f", "lavfi", "-i", f"anullsrc=r=44100:cl=mono",
"-t", str(duration), "-q:a", "9", "-acodec", "libmp3lame", filename
], capture_output=True)
def combine_audio_sections(section_files: list, start_times: list, output: str):
"""Combine all audio sections with proper timing into final voiceover."""
inputs = ["-i", "silence_base.mp3"]
filter_parts = []
for i, (filename, start_time) in enumerate(zip(section_files, start_times)):
inputs.extend(["-i", filename])
delay_ms = int(start_time * 1000)
filter_parts.append(f"[{i+1}]adelay={delay_ms}|{delay_ms}[d{i}]")
mix_inputs = "[0]" + "".join(f"[d{i}]" for i in range(len(section_files)))
filter_str = ";".join(filter_parts) + f";{mix_inputs}amix=inputs={len(section_files)+1}:duration=first"
cmd = ["ffmpeg", "-y"] + inputs + ["-filter_complex", filter_str, output]
subprocess.run(cmd, capture_output=True)
def normalize_audio(input_file: str, output_file: str):
"""Normalize audio levels for consistent volume."""
subprocess.run([
"ffmpeg", "-y", "-i", input_file,
"-af", "loudnorm=I=-16:TP=-1.5:LRA=11",
output_file
], capture_output=True)
def cleanup(files: list):
"""Remove temporary files."""
for f in files:
try:
os.remove(f)
except OSError:
pass
def verify_timing_with_whisper(audio_file: str) -> list:
"""Use Whisper to transcribe and get actual timestamps."""
try:
import whisper
print("Transcribing with Whisper to verify timing...")
model = whisper.load_model("tiny")
result = model.transcribe(audio_file)
return result.get("segments", [])
except ImportError:
print(" Whisper not installed. Install with: pip install openai-whisper")
print(" Skipping timing verification.")
return []
except Exception as e:
print(f" Whisper error: {e}")
return []
def check_timing_alignment(segments: list, sections: list) -> list:
"""Compare Whisper timestamps against expected scene timing."""
issues = []
# Build expected timing windows
# Each section should start at its defined time and end before the next section
for i, (start_time, text) in enumerate(sections):
next_start = sections[i + 1][0] if i + 1 < len(sections) else VIDEO_DURATION
# Find matching Whisper segment
for seg in segments:
if text[:20].lower() in seg.get("text", "").lower():
actual_start = seg.get("start", 0)
actual_end = seg.get("end", 0)
if actual_start < start_time - 0.5:
issues.append(f"Section {i} starts too early: expected {start_time}s, actual {actual_start:.1f}s")
if actual_end > next_start:
issues.append(f"Section {i} overlaps into next scene: ends at {actual_end:.1f}s, next scene at {next_start}s")
break
return issues
def main():
check_requirements()
print(f"Generating voiceover with {len(sections)} sections...")
print(f"Voice: Matilda (ID: {VOICE_ID})")
print()
section_files = []
start_times = []
temp_files = ["silence_base.mp3"]
# Generate each section
for i, (start_time, text) in enumerate(sections):
filename = f"section_{i:02d}.mp3"
section_files.append(filename)
start_times.append(start_time)
temp_files.append(filename)
print(f" [{start_time:2d}s] Generating: {text[:50]}...")
duration = generate_audio(text, filename)
print(f" -> {duration:.1f}s audio")
# Check if this section will overlap with next
if i + 1 < len(sections):
next_start = sections[i + 1][0]
if start_time + duration > next_start:
print(f" WARNING: May overlap with next section (ends at {start_time + duration:.1f}s, next at {next_start}s)")
print()
print("Creating silence base track...")
create_silence_base(VIDEO_DURATION, "silence_base.mp3")
print("Combining sections...")
combine_audio_sections(section_files, start_times, "voiceover_raw.mp3")
temp_files.append("voiceover_raw.mp3")
print("Normalizing audio levels...")
normalize_audio("voiceover_raw.mp3", OUTPUT_FILE)
# Verify timing with Whisper
print()
segments = verify_timing_with_whisper(OUTPUT_FILE)
if segments:
print("\nWhisper transcription timestamps:")
for seg in segments:
print(f" {seg['start']:5.1f}s - {seg['end']:5.1f}s: {seg['text'][:60]}")
issues = check_timing_alignment(segments, sections)
if issues:
print("\n" + "="*60)
print("OVERLAPS DETECTED - MUST FIX BEFORE PROCEEDING")
print("="*60)
for issue in issues:
print(f" - {issue}")
print("\nFIX OPTIONS:")
print(" 1. Shorten the overlapping text (make it punchier)")
print(" 2. Increase the next section's start time")
print(" 3. Add '...' to create natural pauses")
print("\nThen regenerate and verify again. Do NOT proceed with overlaps.")
print("="*60)
else:
print("\nTIMING VERIFIED - No overlaps detected")
print("Cleaning up temporary files...")
cleanup(temp_files)
print()
print(f"Success! Saved to: {OUTPUT_FILE}")
print()
print("Next steps:")
print(" 1. If timing issues were detected, adjust sections[] and run again")
print(" 2. Add background music:")
print(f" ffmpeg -y -i {OUTPUT_FILE} -i music.mp3 \\")
print(' -filter_complex "[1:a]volume=0.15,afade=t=in:st=0:d=2,afade=t=out:st=57:d=3[music];[0:a][music]amix=inputs=2:duration=first" \\')
print(" voiceover-with-music.mp3")
print(" 3. Combine with video:")
print(" ffmpeg -y -i video.mp4 -i voiceover-with-music.mp3 -c:v copy -map 0:v:0 -map 1:a:0 final.mp4")
if __name__ == "__main__":
main()
Voiceover Generation Guide
Generate professional AI voiceover using ElevenLabs, precisely timed and content-matched to video scenes.
Requirements
ELEVEN_LABS_API_KEYenvironment variableffmpeginstalled for audio processingwhisperCLI 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:
// 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 secondsCalculate 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 | 8sStep 2: Extract Scene Content for Script Matching
Read each scene component to understand what's visually displayed:
# For each scene, extract:
# - Title text
# - Subtitle text
# - Stats displayed
# - Key visual elementsThe 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:
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:
# 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:
{
"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
stabilityto 0.70-0.75 - If too robotic: Decrease
stabilityto 0.55-0.60 - If speech too fast: Decrease
style, or add "..." pauses in text
---
Generation & Verification Workflow
Full Pipeline:
# 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)
# 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.mp3Mixing 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=3for 60s video) - Style: Subtle corporate/tech underscore, no lyrics
Mix Command
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.mp3Other 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)
Related skills
FAQ
What renders the video?
Remotion, with the remotion-best-practices skill as a prerequisite for fundamentals.
Where does the voiceover come from?
AI voiceover via ElevenLabs voices, generated with a bundled Python script.