Loading...
Loading...
Create professional promo videos for any SaaS product or repository using Remotion + ElevenLabs. Scans your codebase, builds animated scenes, generates voiceover with emotional presets, and renders in landscape + portrait.
npx skill4agent add akcodez/promo-video-skill promo-videoremotion-best-practicesls ~/.agents/skills/remotion-best-practices/SKILL.md 2>/dev/null && echo "INSTALLED" || echo "NOT INSTALLED"Install with:npx skills add remotion-dev/skills
npx tsx "${SKILL_DIR}/scripts/preflight.ts"ELEVENLABS_API_KEYbunx remotion ffmpegnpx tsx "${SKILL_DIR}/scripts/discover-brand.ts" "<target-repo-path>"{
"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
}]
}git log --oneline -100
# Read models, controllers, services, READMEhead -30 README.md 2>/dev/null
ls src/ 2>/dev/null | head -10{
"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 }
]
}{
"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
}]
}.ai.app{
"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
}
]
}{
"questions": [{
"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" },
{ "label": "Adam", "description": "Deep, dramatic male — cinematic and intense" },
{ "label": "Browse more voices", "description": "Search ElevenLabs for the perfect voice" }
],
"multiSelect": false
}]
}| Voice | Voice ID |
|---|---|
| Matilda | |
| Rachel | |
| Daniel | |
| Josh | |
| Adam | |
npx tsx "${SKILL_DIR}/scripts/discover-voices.ts" --query "professional" --samples 3voice-tests/payment_requiredfree_users_not_allowed{
"questions": [{
"question": "What narrative structure?",
"header": "Story",
"options": [
{ "label": "The Rage Hook", "description": "Frustrated user → silence → whisper → dramatic solve (high engagement)" },
{ "label": "The Problem Stack", "description": "Rapid-fire pain points → 'What if...' → solution reveal" },
{ "label": "The Demo First", "description": "Show the magic upfront → explain how → social proof → CTA" },
{ "label": "The Transformation", "description": "Before/after contrast → features → proof → CTA" },
{ "label": "Custom", "description": "I have my own structure in mind" }
],
"multiSelect": false
}]
}{
"questions": [
{
"question": "What transition between main sections?",
"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": "How fast should transitions be?",
"header": "Speed",
"options": [
{ "label": "Quick (0.4s / 12 frames)", "description": "Snappy, energetic" },
{ "label": "Medium (0.7s / 21 frames)", "description": "Balanced, professional" },
{ "label": "Slow (1.2s / 36 frames)", "description": "Dramatic, cinematic" }
],
"multiSelect": false
}
]
}yes "" | npx create-video@latest --blank --no-git <project-name>
cd <project-name>
npm install
npm install lucide-reactRoot.tsximport { Composition } from "remotion";
import { MyComposition } from "./Composition";
import { MyCompositionPortrait } from "./CompositionPortrait";
const DURATION = 1800; // Use timing-calculator.ts to compute
export const RemotionRoot: React.FC = () => (
<>
<Composition
id="Promo-Landscape"
component={MyComposition}
durationInFrames={DURATION}
fps={30}
width={1920}
height={1080}
/>
<Composition
id="Promo-Portrait"
component={MyCompositionPortrait}
durationInFrames={DURATION}
fps={30}
width={1080}
height={1920}
/>
</>
);Promo-LandscapePromo_LandscapeuseLayout(){ width, height, isPortrait }<LayoutProvider width={1920} height={1080}><LayoutProvider width={1080} height={1920}>effective = sum(sceneDurations) - (numTransitions × transitionDuration)npx tsx "${SKILL_DIR}/scripts/timing-calculator.ts" --scenes "120,90,60,90,90,90,120,120,120,120,120,120,120,120,120,120,60" --transition 12 --fps 30DURATIONspring()interpolate()perspectiverotateXrotateYtranslateZremotion-best-practicesuseCurrentFrame()interpolate()spring()npx remotion studio{
"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
}]
}| Emotion | Stability | Similarity | Style | Use For |
|---|---|---|---|---|
| Urgent/Rage | 0.15-0.30 | 0.85-0.95 | 0.4-0.5 | Hook frustration, anger |
| Whisper | 0.25-0.35 | 0.90-0.95 | 0.3 | Secret reveal, intimacy |
| Confident | 0.55-0.65 | 0.80-0.90 | 0.2-0.3 | Features, product reveal |
| Warm | 0.60-0.70 | 0.80-0.85 | 0.2 | Social proof, results |
| Neutral | 0.65-0.75 | 0.85 | 0.2 | Standard narration |
| Dramatic | 0.40-0.50 | 0.85-0.90 | 0.3-0.4 | CTA, closing |
npx tsx "${SKILL_DIR}/scripts/generate-voiceover.ts" --config voiceover-config.json{
"voiceId": "pNInz6obpgDQGcFmaJgB",
"model": "eleven_multilingual_v2",
"outputDir": ".",
"sections": [
{
"id": "hook",
"text": "What... what is this?",
"startTime": 1.0,
"emotion": "rage",
"settings": { "stability": 0.20, "similarity_boost": 0.90, "style": 0.4 }
},
{
"id": "reveal",
"text": "What if you never had to guess again?",
"startTime": 8.0,
"emotion": "whisper",
"settings": { "stability": 0.30, "similarity_boost": 0.90, "style": 0.3 }
}
]
}whisper voiceover.mp3 --model tiny --output_format srtpython -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']}\")
"bunx remotion ffmpeg -y -i voiceover.mp3 -af "loudnorm=I=-16:TP=-1.5:LRA=11" voiceover-normalized.mp3{
"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
}]
}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# Calculate fade-out start: total_seconds - 3
# For 60s video: st=57, for 90s video: st=87
bunx remotion 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# Landscape
npx remotion render Promo-Landscape out/promo-landscape.mp4 --image-format png --crf 1
# Portrait
npx remotion render Promo-Portrait out/promo-portrait.mp4 --image-format png --crf 1# Landscape final
bunx remotion ffmpeg -y -i out/promo-landscape.mp4 -i voiceover-with-music.mp3 \
-c:v copy -map 0:v:0 -map 1:a:0 out/promo-landscape-final.mp4
# Portrait final
bunx remotion ffmpeg -y -i out/promo-portrait.mp4 -i voiceover-with-music.mp3 \
-c:v copy -map 0:v:0 -map 1:a:0 out/promo-portrait-final.mp4# No-audio version is already the final
cp out/promo-landscape.mp4 out/promo-landscape-final.mp4
cp out/promo-portrait.mp4 out/promo-portrait-final.mp4| 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 | Increase transition duration by 6 frames |
| Blank frames at end | Extend closing scene duration |
| Audio missing in opening | Generate separate rage/emotion clips, mix with adelay |
| Music too loud | Reduce volume from 0.10 to 0.08 |
| Portrait looks cramped | Increase padding, reduce font sizes, stack layouts vertically |
| Composition ID render error | Use hyphens, not underscores |
useCurrentFrame()interpolate()spring()bunx remotion ffmpeg