Skip to content

Commit 78a140f

Browse files
committed
feat/elevenlabs-live: docs
1 parent 3cdb379 commit 78a140f

2 files changed

Lines changed: 153 additions & 4 deletions

File tree

website/docs/user-guide/live/live.mdx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,20 @@ sidebarTitle: "Overview"
99

1010
| Pattern | Class | Latency | Use when… |
1111
|---|---|---|---|
12-
| Turn-by-turn voice | `Agent` + `OpenAITranscriber` + `TTSObserver` | ~1–3 s per turn | You already have a text agent and want to add a voice front-end; you need tool execution, middleware, structured output, or any other text-agent feature. |
12+
| Turn-by-turn voice | `Agent` + a transcriber + a TTS observer | ~1–3 s per turn | You already have a text agent and want to add a voice front-end; you need tool execution, middleware, structured output, or any other text-agent feature. |
1313
| Realtime full-duplex | `LiveAgent` + `OpenAIRealTimeConfig` / `GeminiRealTimeConfig` | <500 ms | You want barge-in, interruption, semantic VAD, or a phone-call-like UX. |
1414

15+
For the turn-by-turn pattern, OpenAI and ElevenLabs are both available and support different halves incrementally — OpenAI streams transcription, ElevenLabs streams synthesis. See [Provider support](/docs/user-guide/live/stt_tts) for the full matrix. Gemini is realtime-only.
16+
1517
!!! note
1618
Both patterns share the same audio I/O primitives — [`SoundDevicePlayer`](/docs/user-guide/live/stt_tts) and [`SoundDeviceRecorder`](/docs/user-guide/live/stt_tts) — and the same event stream. You can mix observers (TTS, logging, persistence) across both.
1719

1820
## Installation
1921

20-
The audio I/O classes depend on `sounddevice` and `numpy`; the OpenAI and Gemini integrations need their respective SDKs.
22+
The audio I/O classes depend on `sounddevice` and `numpy`; the OpenAI, Gemini, and ElevenLabs integrations need their respective SDKs.
2123

2224
```bash
23-
pip install "ag2[openai,gemini] sounddevice[numpy]"
25+
pip install "ag2[openai,gemini,elevenlabs] sounddevice[numpy]"
2426
```
2527

2628
!!! warning

website/docs/user-guide/live/stt_tts.mdx

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,27 @@ sidebarTitle: "STT & TTS"
55

66
The STT → Agent → TTS flow turns any existing `Agent` into a voice agent without changing the agent itself. Speech-to-text is added as a **pipeline wrapper**; text-to-speech is added as an **observer** that listens to the model's streamed message chunks.
77

8+
## Provider support
9+
10+
Both halves are protocol-based, so providers are interchangeable — but they do not all support incremental delivery, and the two current providers are complementary opposites.
11+
12+
| Provider | STT | Streaming STT | TTS | Streaming TTS |
13+
|---|---|---|---|---|
14+
| OpenAI | `OpenAITranscriber`, `OpenAITranslationTranscriber` | **Yes** — emits `TranscriptionChunkEvent` deltas | `OpenAITTSConfig` | No — one buffered clip |
15+
| ElevenLabs | `ElevenLabsTranscriber` (Scribe) | No — whole-file upload | `ElevenLabsTTSConfig`, `ElevenLabsStreamingTTSConfig` | **Yes** — via `ElevenLabsStreamingTTSConfig` |
16+
| Gemini |||||
17+
18+
!!! note
19+
**ElevenLabs is currently the only streaming TTS provider**, and **OpenAI is the only streaming STT provider**. Gemini has no standalone STT/TTS config in `ag2.live` — it is realtime-only, via `GeminiRealTimeConfig` and [LiveAgent](/docs/user-guide/live/live_agent).
20+
21+
Mixing providers across the two halves is supported and often the best setup: transcribe with OpenAI for live captions, speak with ElevenLabs for low time-to-first-byte.
22+
23+
ElevenLabs classes need the optional dependency and `ELEVENLABS_API_KEY` in the environment:
24+
25+
```bash
26+
pip install "ag2[elevenlabs] sounddevice[numpy]"
27+
```
28+
829
## Audio I/O primitives
930

1031
`SoundDeviceRecorder` captures microphone input and `SoundDevicePlayer` plays synthesized speech. Both are thin wrappers around the [`sounddevice`](https://python-sounddevice.readthedocs.io/){.external-link target="_blank"} library and share the same event stream.
@@ -71,6 +92,23 @@ from ag2.live import OpenAITranslationTranscriber
7192
pipeline = OpenAITranslationTranscriber("whisper-1").pipe(agent)
7293
```
7394

95+
### ElevenLabs (Scribe)
96+
97+
`ElevenLabsTranscriber` implements the same `STTConfig` protocol, so it drops into the same `.pipe(agent)` call:
98+
99+
```python linenums="1"
100+
from ag2.live import ElevenLabsTranscriber
101+
102+
pipeline = ElevenLabsTranscriber("scribe_v2").pipe(agent)
103+
```
104+
105+
`language_code` is only sent when you set it — leave it unset and Scribe auto-detects. `diarize=True` asks for speaker labels.
106+
107+
!!! warning "Scribe is whole-file, so there are no partials"
108+
The endpoint takes a **complete** recording, so there is nothing to emit until it answers. `ElevenLabsTranscriber` only ever sends `TranscriptionCompletedEvent` — never `TranscriptionChunkEvent`. Live-caption code written against the OpenAI transcriber's deltas will go quiet if you swap in Scribe.
109+
110+
It also blocks the whole turn: `VoicePipeline.ask` awaits `transcribe(...)` before the agent starts, so nothing can be spoken during the upload and transcription. On a short turn this often dominates the latency — see [Where the latency goes](#where-the-latency-goes).
111+
74112
## Text-to-Speech
75113

76114
`TTSObserver` is an observer that listens to `ModelMessageChunk` events as the agent streams its response, batches them into sentence-sized chunks, calls a TTS provider, and emits `SynthesizedAudioEvent`s onto the stream. A `SoundDevicePlayer` attached to the same stream then plays them.
@@ -104,8 +142,10 @@ if __name__ == "__main__":
104142

105143
### Voice and speed
106144

107-
`OpenAITTSConfig` accepts the standard OpenAI TTS parameters:
145+
Any config implementing the `TTSConfig` protocol works with `TTSObserver`:
108146

147+
<Tabs>
148+
<Tab title="OpenAI">
109149
```python linenums="1"
110150
from ag2.live import OpenAITTSConfig
111151

@@ -114,8 +154,107 @@ config = OpenAITTSConfig(
114154
voice="ballad", # alloy, ash, ballad, coral, echo, sage, shimmer, verse...
115155
speed=1.1,
116156
)
157+
```
158+
</Tab>
159+
160+
<Tab title="ElevenLabs">
161+
```python linenums="1"
162+
from ag2.live import ElevenLabsTTSConfig
163+
164+
config = ElevenLabsTTSConfig(
165+
"eleven_v3",
166+
voice_id="21m00Tcm4TlvDq8ikWAM", # Rachel
167+
output_format="pcm_24000", # matches SoundDevicePlayer
168+
)
169+
```
170+
</Tab>
171+
</Tabs>
172+
173+
`ElevenLabsTTSConfig` is the buffered mode — required by `eleven_v3`, which cannot stream. For the flash and turbo families, prefer streaming (below).
174+
175+
!!! note
176+
`output_format` defaults to `pcm_24000`, matching what `SoundDevicePlayer` and `SoundDeviceRecorder` expect. If you change it, change the player to match.
177+
178+
## Streaming TTS
179+
180+
`TTSObserver` still waits for each *sentence* to be fully synthesized before any of it plays. Streaming TTS removes that wait: audio chunks are forwarded as they are generated, so playback starts at time-to-first-byte.
181+
182+
!!! note
183+
**ElevenLabs is currently the only provider with a streaming TTS config.** `OpenAITTSConfig` performs a single buffered read and has no streaming equivalent.
184+
185+
Streaming needs both a different config *and* a different observer:
186+
187+
```python linenums="1" hl_lines="6 12"
188+
import asyncio
189+
190+
from ag2 import Agent, config
191+
from ag2.live import ElevenLabsStreamingTTSConfig, ElevenLabsStreamingTTSObserver, SoundDevicePlayer
192+
193+
tts = ElevenLabsStreamingTTSConfig("eleven_flash_v2_5", voice_id="21m00Tcm4TlvDq8ikWAM")
194+
195+
agent = Agent(
196+
"assistant",
197+
prompt="You are a helpful voice assistant. Keep answers to a few sentences.",
198+
config=config.OpenAIConfig("gpt-5-mini", streaming=True),
199+
observers=[ElevenLabsStreamingTTSObserver(tts)],
200+
)
201+
202+
async def main() -> None:
203+
async with SoundDevicePlayer() as player:
204+
await agent.ask("Hello, agent!", stream=player.stream)
205+
206+
if __name__ == "__main__":
207+
asyncio.run(main())
208+
```
209+
210+
!!! warning "Pair the streaming config with the streaming observer"
211+
`ElevenLabsStreamingTTSConfig` also implements `synthesize(...)`, so it satisfies the `TTSConfig` protocol and *will* work with the plain `TTSObserver` — but `synthesize` is implemented by draining the whole stream into one buffer, which throws away the entire latency advantage. Use `ElevenLabsStreamingTTSObserver` with `ElevenLabsStreamingTTSConfig`, and `TTSObserver` with `ElevenLabsTTSConfig` / `OpenAITTSConfig`.
212+
213+
### Streaming directly to the speaker
214+
215+
`ElevenLabsStreamingTTSConfig.stream(...)` is usable on its own, outside any agent, when you just want to speak a known string:
216+
217+
```python linenums="1" hl_lines="7-9"
218+
import asyncio
219+
220+
from ag2.live import ElevenLabsStreamingTTSConfig, SoundDevicePlayer
221+
222+
async def main() -> None:
223+
tts = ElevenLabsStreamingTTSConfig("eleven_flash_v2_5")
224+
async with SoundDevicePlayer() as player:
225+
async for chunk in tts.stream("Audio arrives as it is generated."):
226+
await player.play(chunk)
227+
228+
if __name__ == "__main__":
229+
asyncio.run(main())
230+
```
231+
232+
`player.play(...)` only enqueues — a background worker thread writes to the output device, so this loop is never blocked by playback.
233+
234+
### Sentence batching
235+
236+
Both observers batch streamed text before synthesizing, since a per-token request would be slow and expensive. `min_chars` (default `60`) is the smallest amount of text worth a request; below it the buffer keeps accumulating even past a sentence boundary.
237+
238+
```python linenums="1"
239+
observers=[ElevenLabsStreamingTTSObserver(tts, min_chars=40)]
117240
```
118241

242+
Lowering it starts the first sentence sooner, at the cost of more requests and more granular sentences.
243+
244+
## Where the latency goes
245+
246+
Streaming TTS removes one source of delay, not all of them. For a single turn, in order:
247+
248+
| Stage | Cost | Streaming TTS helps? |
249+
|---|---|---|
250+
| Transcription | With Scribe, the whole clip is uploaded and transcribed before the agent starts | No |
251+
| Model time-to-first-token | Provider-dependent; large on reasoning models | No |
252+
| Sentence buffer fill | Until `min_chars` **and** a sentence boundary is reached | No — lower `min_chars` instead |
253+
| Synthesis | Time-to-first-byte vs. waiting for the full clip | **Yes** — this is the win |
254+
255+
!!! note
256+
Synthesis is awaited inline in the observer, which runs inside the model's token loop. While one sentence is being synthesized the next tokens are not being consumed, so a long reply can still develop small gaps between sentences even in streaming mode.
257+
119258
## Combining STT and TTS
120259

121260
The full round-trip — voice in, voice out — is just both halves wired up at once: pipe the agent through the transcriber, attach a `TTSObserver`, and share a stream with the player.
@@ -166,6 +305,14 @@ if __name__ == "__main__":
166305
!!! tip
167306
`player.join()` blocks the main task until the synthesized audio queue has drained. Use it between turns when you want the assistant to finish speaking before the next recording starts — otherwise the recorder will capture the tail of the assistant's voice.
168307

308+
## Examples
309+
310+
- `examples/stt_tts.py/stt_tts.py` — the OpenAI round-trip
311+
- `examples/stt_tts.py/elevenlabs_stt_tts_streaming.py` — streaming TTS, prints time-to-first-byte
312+
- `examples/stt_tts.py/elevenlabs_stt_tts_sync.py` — buffered TTS with `eleven_v3`
313+
314+
Run the last two back to back to hear the difference on the first sentence.
315+
169316
## What's next
170317

171318
- **[LiveAgent](/docs/user-guide/live/live_agent)** — drop the turn-by-turn round-trip in favor of a streaming, full-duplex realtime session.

0 commit comments

Comments
 (0)