ElevenLabs Audio Generation

Generate AI voiceovers, sound effects, and music using ElevenLabs APIs.

ElevenLabs Audio Generation — Super Bowl-style launch ad (from the digitalsamba/claude-code-video-toolkit README)

From the digitalsamba/claude-code-video-toolkit README — shows the whole collection, not only this skill. · view on GitHub

How to use it

Claude Code
  1. Run the line below. It pulls the whole folder into ~/.claude/skills/elevenlabs, including the files SKILL.md points to.
  2. Describe your job in plain words. Claude Code follows the skill from there.
Claude Code — installs the whole folder, not just SKILL.md
npx degit digitalsamba/claude-code-video-toolkit/.claude/skills/elevenlabs#main ~/.claude/skills/elevenlabs

For one project only, change the path to .claude/skills/elevenlabs. This skill also uses reference.md, VOICEOVER-SCRIPT.md, voiceover.py, manifest.json, project.json — copying SKILL.md alone won't be enough. See the folder on GitHub.

Claude (web or desktop app)
  1. On this page open ⋯ → Download .md.
  2. Save it as SKILL.md in a folder, zip the folder, then Customize → Skills → + → Create skill → Upload a skill.
  3. Pick the file and Save. Claude shows the name and description and runs a security scan.
  4. Check the skill is switched on.
  5. Start a new chat and describe your job in plain words. The AI follows the skill from there.
ChatGPT or another app
  1. ChatGPT: make a Project and paste it into Instructions.
  2. Neither? Paste it at the top of a new chat — it works for that chat.
Not working?
  • Check which app you pasted it into — the steps above name the right one.
  • Some skills need the paid tier of Claude or ChatGPT.
Step-by-step guide with screenshots · Ask in the forum

Paste into Claude, ChatGPT or Cursor.

Source of ElevenLabs Audio Generation

Show the full text377 lines
namedescription
elevenlabsGenerate AI voiceovers, sound effects, and music using ElevenLabs APIs. Use when creating audio content for videos, podcasts, or games. Triggers include generating voiceovers, narration, dialogue, sound effects from descriptions, background music, soundtrack generation, voice cloning, or any audio synthesis task.

ElevenLabs Audio Generation

Requires ELEVENLABS_API_KEY in .env.

Text-to-Speech

from elevenlabs.client import ElevenLabs
from elevenlabs import save, VoiceSettings
import os

client = ElevenLabs(api_key=os.getenv("ELEVENLABS_API_KEY"))

audio = client.text_to_speech.convert(
    text="Welcome to my video!",
    voice_id="JBFqnCBsd6RMkjVDRZzb",
    model_id="eleven_multilingual_v2",
    voice_settings=VoiceSettings(
        stability=0.5,
        similarity_boost=0.75,
        style=0.5,
        speed=1.0
    )
)
save(audio, "voiceover.mp3")
Models
Model Quality SSML Support Notes
eleven_multilingual_v2 Highest consistency None Stable, production-ready, 29 languages
eleven_flash_v2_5 Good <break>, <phoneme> Fast, supports pause/pronunciation tags
eleven_turbo_v2_5 Good <break>, <phoneme> Fastest latency
eleven_v3 Most expressive None Alpha — unreliable, needs prompt engineering

Choose: multilingual_v2 for reliability, flash/turbo for SSML control, v3 for maximum expressiveness (expect retakes).

Voice Settings by Style
Style stability similarity style speed
Natural/professional 0.75-0.85 0.9 0.0-0.1 1.0
Conversational 0.5-0.6 0.85 0.3-0.4 0.9-1.0
Energetic/YouTuber 0.3-0.5 0.75 0.5-0.7 1.0-1.1
Pauses Between Sections

With flash/turbo models: Use SSML break tags inline:

...end of section. <break time="1.5s" /> Start of next...

Max 3 seconds per break. Excessive breaks can cause speed artifacts.

With multilingual_v2 / v3: No SSML support. Options:

  • Paragraph breaks (blank lines) — creates ~0.3-0.5s natural pause
  • Post-process with ffmpeg: split audio and insert silence

WARNING: ... (ellipsis) is NOT a reliable pause — it can be vocalized as a word/sound. Do not use ellipsis as a pause mechanism.

Pronunciation Control

Phonetic spelling (any model): Write words as you want them pronounced:

  • Janus → Jan-us
  • nginx → engine-x
  • Use dashes, capitals, apostrophes to guide pronunciation

SSML phoneme tags (flash/turbo only):

<phoneme alphabet="ipa" ph="ˈdʒeɪnəs">Janus</phoneme>
Iterative Workflow
  1. Generate → listen → identify pronunciation/pacing issues
  2. Adjust: phonetic spellings, break tags, voice settings
  3. Regenerate. If pauses aren't precise enough, add silence in post with ffmpeg rather than fighting the TTS engine.

Voice Cloning

Instant Voice Clone
with open("sample.mp3", "rb") as f:
    voice = client.voices.ivc.create(
        name="My Voice",
        files=[f],
        remove_background_noise=True
    )
print(f"Voice ID: {voice.voice_id}")
  • Use client.voices.ivc.create() (not client.voices.clone())
  • Pass file handles in binary mode ("rb"), not paths
  • Convert m4a first: ffmpeg -i input.m4a -codec:a libmp3lame -qscale:a 2 output.mp3
  • Multiple samples (2-3 clips) improve accuracy
  • Save voice ID for reuse

Professional Voice Clone: Requires Creator plan+, 30+ min audio. See reference.md.

Sound Effects

Max 22 seconds per generation.

result = client.text_to_sound_effects.convert(
    text="Thunder rumbling followed by heavy rain",
    duration_seconds=10,
    prompt_influence=0.3
)
with open("thunder.mp3", "wb") as f:
    for chunk in result:
        f.write(chunk)

Prompt tips: Be specific — "Heavy footsteps on wooden floorboards, slow and deliberate, with creaking"

Music Generation

10 seconds to 5 minutes. Use client.music.compose() (not .generate()).

result = client.music.compose(
    prompt="Upbeat indie rock, catchy guitar riff, energetic drums, travel vlog",
    music_length_ms=60000,
    force_instrumental=True
)
with open("music.mp3", "wb") as f:
    for chunk in result:
        f.write(chunk)

Prompt structure: Genre, mood, instruments, tempo, use case. Add "no vocals" or use force_instrumental=True for background music.

Remotion Integration

Complete Workflow: Script to Synchronized Scene
VOICEOVER-SCRIPT.md → voiceover.py → public/audio/ → Remotion composition
        ↓                  ↓               ↓                 ↓
  Scene narration    Generate MP3    Audio files     <Audio> component
  with durations     per scene       with timing     synced to scenes
Step 1: Generate Per-Scene Audio

Use the toolkit's voiceover tool to generate audio for each scene:

# Generate voiceover files for each scene
uv run tools/voiceover.py --scene-dir public/audio/scenes --json

# Output:
# public/audio/scenes/
#   ├── scene-01-title.mp3
#   ├── scene-02-problem.mp3
#   ├── scene-03-solution.mp3
#   └── manifest.json  (durations for each file)

The manifest.json contains timing info:

{
  "scenes": [
    { "file": "scene-01-title.mp3", "duration": 4.2 },
    { "file": "scene-02-problem.mp3", "duration": 12.8 },
    { "file": "scene-03-solution.mp3", "duration": 15.3 }
  ],
  "totalDuration": 32.3
}
Step 2: Use Audio in Remotion Composition
// src/Composition.tsx
import { Audio, staticFile, Series, useVideoConfig } from 'remotion';

// Import scene components
import { TitleSlide } from './scenes/TitleSlide';
import { ProblemSlide } from './scenes/ProblemSlide';
import { SolutionSlide } from './scenes/SolutionSlide';

// Scene durations (from manifest.json, converted to frames at 30fps)
const SCENE_DURATIONS = {
  title: Math.ceil(4.2 * 30),      // 126 frames
  problem: Math.ceil(12.8 * 30),   // 384 frames
  solution: Math.ceil(15.3 * 30),  // 459 frames
};

export const MainComposition: React.FC = () => {
  return (
    <>
      {/* Scene sequence */}
      <Series>
        <Series.Sequence durationInFrames={SCENE_DURATIONS.title}>
          <TitleSlide />
        </Series.Sequence>
        <Series.Sequence durationInFrames={SCENE_DURATIONS.problem}>
          <ProblemSlide />
        </Series.Sequence>
        <Series.Sequence durationInFrames={SCENE_DURATIONS.solution}>
          <SolutionSlide />
        </Series.Sequence>
      </Series>

      {/* Audio track - plays continuously across all scenes */}
      <Audio src={staticFile('audio/voiceover.mp3')} volume={1} />

      {/* Optional: Background music at lower volume */}
      <Audio src={staticFile('audio/music.mp3')} volume={0.15} />
    </>
  );
};
Step 3: Per-Scene Audio (Alternative)

For more control, add audio to each scene individually:

// src/scenes/ProblemSlide.tsx
import { Audio, staticFile, useCurrentFrame } from 'remotion';

export const ProblemSlide: React.FC = () => {
  const frame = useCurrentFrame();

  return (
    <div style={{ /* slide styles */ }}>
      <h1>The Problem</h1>
      {/* Scene content */}

      {/* Audio starts when this scene starts (frame 0 of this sequence) */}
      <Audio src={staticFile('audio/scenes/scene-02-problem.mp3')} />
    </div>
  );
};
Syncing Visuals to Voiceover

Calculate scene duration from audio, not the other way around:

// src/config/timing.ts
import manifest from '../../public/audio/scenes/manifest.json';

const FPS = 30;

// Convert audio durations to frame counts
export const sceneDurations = manifest.scenes.reduce((acc, scene) => {
  const name = scene.file.replace(/^scene-\d+-/, '').replace('.mp3', '');
  acc[name] = Math.ceil(scene.duration * FPS);
  return acc;
}, {} as Record<string, number>);

// Usage in composition:
// <Series.Sequence durationInFrames={sceneDurations.title}>
Audio Timing Patterns
import { Audio, Sequence, interpolate, useCurrentFrame } from 'remotion';

// Fade in audio
export const FadeInAudio: React.FC<{ src: string; fadeFrames?: number }> = ({
  src,
  fadeFrames = 30
}) => {
  const frame = useCurrentFrame();
  const volume = interpolate(frame, [0, fadeFrames], [0, 1], {
    extrapolateRight: 'clamp',
  });
  return <Audio src={src} volume={volume} />;
};

// Delayed audio start
export const DelayedAudio: React.FC<{ src: string; delayFrames: number }> = ({
  src,
  delayFrames
}) => (
  <Sequence from={delayFrames}>
    <Audio src={src} />
  </Sequence>
);

// Usage:
// <FadeInAudio src={staticFile('audio/music.mp3')} fadeFrames={60} />
// <DelayedAudio src={staticFile('audio/sfx/whoosh.mp3')} delayFrames={45} />
Voiceover + Demo Video Sync

When a scene has both voiceover and demo video:

import { Audio, OffthreadVideo, staticFile, useVideoConfig } from 'remotion';

export const DemoScene: React.FC = () => {
  const { durationInFrames, fps } = useVideoConfig();

  // Calculate playback rate to fit demo into voiceover duration
  const demoDuration = 45; // seconds (original demo length)
  const sceneDuration = durationInFrames / fps; // seconds (from voiceover)
  const playbackRate = demoDuration / sceneDuration;

  return (
    <>
      <OffthreadVideo
        src={staticFile('demos/feature-demo.mp4')}
        playbackRate={playbackRate}
      />
      <Audio src={staticFile('audio/scenes/scene-04-demo.mp3')} />
    </>
  );
};
Error Handling
import { Audio, staticFile, delayRender, continueRender } from 'remotion';
import { useEffect, useState } from 'react';

export const SafeAudio: React.FC<{ src: string }> = ({ src }) => {
  const [handle] = useState(() => delayRender());
  const [audioReady, setAudioReady] = useState(false);

  useEffect(() => {
    const audio = new window.Audio(src);
    audio.oncanplaythrough = () => {
      setAudioReady(true);
      continueRender(handle);
    };
    audio.onerror = () => {
      console.error(`Failed to load audio: ${src}`);
      continueRender(handle); // Continue without audio rather than hang
    };
  }, [src, handle]);

  if (!audioReady) return null;
  return <Audio src={src} />;
};
Toolkit Command: /generate-voiceover

The /generate-voiceover command handles the full workflow:

/generate-voiceover

1. Reads VOICEOVER-SCRIPT.md
2. Extracts narration for each scene
3. Generates audio via ElevenLabs API
4. Saves to public/audio/scenes/
5. Creates manifest.json with durations
6. Updates project.json with timing info
  • George: JBFqnCBsd6RMkjVDRZzb (warm narrator)
  • Rachel: 21m00Tcm4TlvDq8ikWAM (clear female)
  • Adam: pNInz6obpgDQGcFmaJgB (professional male)

List all: client.voices.get_all()

For full API docs, see reference.md.

1---
2name: elevenlabs
3description: Generate AI voiceovers, sound effects, and music using ElevenLabs APIs. Use when creating audio content for videos, podcasts, or games. Triggers include generating voiceovers, narration, dialogue, sound effects from descriptions, background music, soundtrack generation, voice cloning, or any audio synthesis task.
4---
5 
6# ElevenLabs Audio Generation
7 
8Requires `ELEVENLABS_API_KEY` in `.env`.
9 
10## Text-to-Speech
11 
12```python
13from elevenlabs.client import ElevenLabs
14from elevenlabs import save, VoiceSettings
15import os
16 
17client = ElevenLabs(api_key=os.getenv("ELEVENLABS_API_KEY"))
18 
19audio = client.text_to_speech.convert(
20 text="Welcome to my video!",
21 voice_id="JBFqnCBsd6RMkjVDRZzb",
22 model_id="eleven_multilingual_v2",
23 voice_settings=VoiceSettings(
24 stability=0.5,
25 similarity_boost=0.75,
26 style=0.5,
27 speed=1.0
28 )
29)
30save(audio, "voiceover.mp3")
31```
32 
33### Models
34 
35| Model | Quality | SSML Support | Notes |
36|-------|---------|--------------|-------|
37| `eleven_multilingual_v2` | Highest consistency | None | Stable, production-ready, 29 languages |
38| `eleven_flash_v2_5` | Good | `<break>`, `<phoneme>` | Fast, supports pause/pronunciation tags |
39| `eleven_turbo_v2_5` | Good | `<break>`, `<phoneme>` | Fastest latency |
40| `eleven_v3` | Most expressive | None | Alpha — unreliable, needs prompt engineering |
41 
42**Choose:** multilingual_v2 for reliability, flash/turbo for SSML control, v3 for maximum expressiveness (expect retakes).
43 
44### Voice Settings by Style
45 
46| Style | stability | similarity | style | speed |
47|-------|-----------|------------|-------|-------|
48| Natural/professional | 0.75-0.85 | 0.9 | 0.0-0.1 | 1.0 |
49| Conversational | 0.5-0.6 | 0.85 | 0.3-0.4 | 0.9-1.0 |
50| Energetic/YouTuber | 0.3-0.5 | 0.75 | 0.5-0.7 | 1.0-1.1 |
51 
52### Pauses Between Sections
53 
54**With flash/turbo models:** Use SSML break tags inline:
55```
56...end of section. <break time="1.5s" /> Start of next...
57```
58Max 3 seconds per break. Excessive breaks can cause speed artifacts.
59 
60**With multilingual_v2 / v3:** No SSML support. Options:
61- Paragraph breaks (blank lines) — creates ~0.3-0.5s natural pause
62- Post-process with ffmpeg: split audio and insert silence
63 
64**WARNING:** `...` (ellipsis) is NOT a reliable pause — it can be vocalized as a word/sound. Do not use ellipsis as a pause mechanism.
65 
66### Pronunciation Control
67 
68**Phonetic spelling (any model):** Write words as you want them pronounced:
69- `Janus` → `Jan-us`
70- `nginx` → `engine-x`
71- Use dashes, capitals, apostrophes to guide pronunciation
72 
73**SSML phoneme tags (flash/turbo only):**
74```
75<phoneme alphabet="ipa" ph="ˈdʒeɪnəs">Janus</phoneme>
76```
77 
78### Iterative Workflow
79 
801. Generate → listen → identify pronunciation/pacing issues
812. Adjust: phonetic spellings, break tags, voice settings
823. Regenerate. If pauses aren't precise enough, add silence in post with ffmpeg rather than fighting the TTS engine.
83 
84## Voice Cloning
85 
86### Instant Voice Clone
87 
88```python
89with open("sample.mp3", "rb") as f:
90 voice = client.voices.ivc.create(
91 name="My Voice",
92 files=[f],
93 remove_background_noise=True
94 )
95print(f"Voice ID: {voice.voice_id}")
96```
97 
98- Use `client.voices.ivc.create()` (not `client.voices.clone()`)
99- Pass file handles in binary mode (`"rb"`), not paths
100- Convert m4a first: `ffmpeg -i input.m4a -codec:a libmp3lame -qscale:a 2 output.mp3`
101- Multiple samples (2-3 clips) improve accuracy
102- Save voice ID for reuse
103 
104**Professional Voice Clone:** Requires Creator plan+, 30+ min audio. See [reference.md](reference.md).
105 
106## Sound Effects
107 
108Max 22 seconds per generation.
109 
110```python
111result = client.text_to_sound_effects.convert(
112 text="Thunder rumbling followed by heavy rain",
113 duration_seconds=10,
114 prompt_influence=0.3
115)
116with open("thunder.mp3", "wb") as f:
117 for chunk in result:
118 f.write(chunk)
119```
120 
121**Prompt tips:** Be specific — "Heavy footsteps on wooden floorboards, slow and deliberate, with creaking"
122 
123## Music Generation
124 
12510 seconds to 5 minutes. Use `client.music.compose()` (not `.generate()`).
126 
127```python
128result = client.music.compose(
129 prompt="Upbeat indie rock, catchy guitar riff, energetic drums, travel vlog",
130 music_length_ms=60000,
131 force_instrumental=True
132)
133with open("music.mp3", "wb") as f:
134 for chunk in result:
135 f.write(chunk)
136```
137 
138**Prompt structure:** Genre, mood, instruments, tempo, use case. Add "no vocals" or use `force_instrumental=True` for background music.
139 
140## Remotion Integration
141 
142### Complete Workflow: Script to Synchronized Scene
143 
144```
145VOICEOVER-SCRIPT.md → voiceover.py → public/audio/ → Remotion composition
146 ↓ ↓ ↓ ↓
147 Scene narration Generate MP3 Audio files <Audio> component
148 with durations per scene with timing synced to scenes
149```
150 
151### Step 1: Generate Per-Scene Audio
152 
153Use the toolkit's voiceover tool to generate audio for each scene:
154 
155```bash
156# Generate voiceover files for each scene
157uv run tools/voiceover.py --scene-dir public/audio/scenes --json
158 
159# Output:
160# public/audio/scenes/
161# ├── scene-01-title.mp3
162# ├── scene-02-problem.mp3
163# ├── scene-03-solution.mp3
164# └── manifest.json (durations for each file)
165```
166 
167The `manifest.json` contains timing info:
168```json
169{
170 "scenes": [
171 { "file": "scene-01-title.mp3", "duration": 4.2 },
172 { "file": "scene-02-problem.mp3", "duration": 12.8 },
173 { "file": "scene-03-solution.mp3", "duration": 15.3 }
174 ],
175 "totalDuration": 32.3
176}
177```
178 
179### Step 2: Use Audio in Remotion Composition
180 
181```tsx
182// src/Composition.tsx
183import { Audio, staticFile, Series, useVideoConfig } from 'remotion';
184 
185// Import scene components
186import { TitleSlide } from './scenes/TitleSlide';
187import { ProblemSlide } from './scenes/ProblemSlide';
188import { SolutionSlide } from './scenes/SolutionSlide';
189 
190// Scene durations (from manifest.json, converted to frames at 30fps)
191const SCENE_DURATIONS = {
192 title: Math.ceil(4.2 * 30), // 126 frames
193 problem: Math.ceil(12.8 * 30), // 384 frames
194 solution: Math.ceil(15.3 * 30), // 459 frames
195};
196 
197export const MainComposition: React.FC = () => {
198 return (
199 <>
200 {/* Scene sequence */}
201 <Series>
202 <Series.Sequence durationInFrames={SCENE_DURATIONS.title}>
203 <TitleSlide />
204 </Series.Sequence>
205 <Series.Sequence durationInFrames={SCENE_DURATIONS.problem}>
206 <ProblemSlide />
207 </Series.Sequence>
208 <Series.Sequence durationInFrames={SCENE_DURATIONS.solution}>
209 <SolutionSlide />
210 </Series.Sequence>
211 </Series>
212 
213 {/* Audio track - plays continuously across all scenes */}
214 <Audio src={staticFile('audio/voiceover.mp3')} volume={1} />
215 
216 {/* Optional: Background music at lower volume */}
217 <Audio src={staticFile('audio/music.mp3')} volume={0.15} />
218 </>
219 );
220};
221```
222 
223### Step 3: Per-Scene Audio (Alternative)
224 
225For more control, add audio to each scene individually:
226 
227```tsx
228// src/scenes/ProblemSlide.tsx
229import { Audio, staticFile, useCurrentFrame } from 'remotion';
230 
231export const ProblemSlide: React.FC = () => {
232 const frame = useCurrentFrame();
233 
234 return (
235 <div style={{ /* slide styles */ }}>
236 <h1>The Problem</h1>
237 {/* Scene content */}
238 
239 {/* Audio starts when this scene starts (frame 0 of this sequence) */}
240 <Audio src={staticFile('audio/scenes/scene-02-problem.mp3')} />
241 </div>
242 );
243};
244```
245 
246### Syncing Visuals to Voiceover
247 
248Calculate scene duration from audio, not the other way around:
249 
250```tsx
251// src/config/timing.ts
252import manifest from '../../public/audio/scenes/manifest.json';
253 
254const FPS = 30;
255 
256// Convert audio durations to frame counts
257export const sceneDurations = manifest.scenes.reduce((acc, scene) => {
258 const name = scene.file.replace(/^scene-\d+-/, '').replace('.mp3', '');
259 acc[name] = Math.ceil(scene.duration * FPS);
260 return acc;
261}, {} as Record<string, number>);
262 
263// Usage in composition:
264// <Series.Sequence durationInFrames={sceneDurations.title}>
265```
266 
267### Audio Timing Patterns
268 
269```tsx
270import { Audio, Sequence, interpolate, useCurrentFrame } from 'remotion';
271 
272// Fade in audio
273export const FadeInAudio: React.FC<{ src: string; fadeFrames?: number }> = ({
274 src,
275 fadeFrames = 30
276}) => {
277 const frame = useCurrentFrame();
278 const volume = interpolate(frame, [0, fadeFrames], [0, 1], {
279 extrapolateRight: 'clamp',
280 });
281 return <Audio src={src} volume={volume} />;
282};
283 
284// Delayed audio start
285export const DelayedAudio: React.FC<{ src: string; delayFrames: number }> = ({
286 src,
287 delayFrames
288}) => (
289 <Sequence from={delayFrames}>
290 <Audio src={src} />
291 </Sequence>
292);
293 
294// Usage:
295// <FadeInAudio src={staticFile('audio/music.mp3')} fadeFrames={60} />
296// <DelayedAudio src={staticFile('audio/sfx/whoosh.mp3')} delayFrames={45} />
297```
298 
299### Voiceover + Demo Video Sync
300 
301When a scene has both voiceover and demo video:
302 
303```tsx
304import { Audio, OffthreadVideo, staticFile, useVideoConfig } from 'remotion';
305 
306export const DemoScene: React.FC = () => {
307 const { durationInFrames, fps } = useVideoConfig();
308 
309 // Calculate playback rate to fit demo into voiceover duration
310 const demoDuration = 45; // seconds (original demo length)
311 const sceneDuration = durationInFrames / fps; // seconds (from voiceover)
312 const playbackRate = demoDuration / sceneDuration;
313 
314 return (
315 <>
316 <OffthreadVideo
317 src={staticFile('demos/feature-demo.mp4')}
318 playbackRate={playbackRate}
319 />
320 <Audio src={staticFile('audio/scenes/scene-04-demo.mp3')} />
321 </>
322 );
323};
324```
325 
326### Error Handling
327 
328```tsx
329import { Audio, staticFile, delayRender, continueRender } from 'remotion';
330import { useEffect, useState } from 'react';
331 
332export const SafeAudio: React.FC<{ src: string }> = ({ src }) => {
333 const [handle] = useState(() => delayRender());
334 const [audioReady, setAudioReady] = useState(false);
335 
336 useEffect(() => {
337 const audio = new window.Audio(src);
338 audio.oncanplaythrough = () => {
339 setAudioReady(true);
340 continueRender(handle);
341 };
342 audio.onerror = () => {
343 console.error(`Failed to load audio: ${src}`);
344 continueRender(handle); // Continue without audio rather than hang
345 };
346 }, [src, handle]);
347 
348 if (!audioReady) return null;
349 return <Audio src={src} />;
350};
351```
352 
353### Toolkit Command: /generate-voiceover
354 
355The `/generate-voiceover` command handles the full workflow:
356 
357```
358/generate-voiceover
359 
3601. Reads VOICEOVER-SCRIPT.md
3612. Extracts narration for each scene
3623. Generates audio via ElevenLabs API
3634. Saves to public/audio/scenes/
3645. Creates manifest.json with durations
3656. Updates project.json with timing info
366```
367 
368## Popular Voices
369 
370- George: `JBFqnCBsd6RMkjVDRZzb` (warm narrator)
371- Rachel: `21m00Tcm4TlvDq8ikWAM` (clear female)
372- Adam: `pNInz6obpgDQGcFmaJgB` (professional male)
373 
374List all: `client.voices.get_all()`
375 
376For full API docs, see [reference.md](reference.md).
377 

Discussion