Skip to content

Commit bca7bf0

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

2 files changed

Lines changed: 59 additions & 32 deletions

File tree

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ title: "Voice & Realtime Overview"
33
sidebarTitle: "Overview"
44
---
55

6-
`ag2.live` is the AG2 module for building voice-enabled agents. It covers two complementary patterns: a turn-by-turn **STT → Agent → TTS** pipeline built on top of a regular `Agent`, and a full-duplex **`LiveAgent`** that streams audio to and from a provider's realtime API.
6+
`ag2.live` is the AG2 module for building voice-enabled agents. It covers two complementary patterns:
7+
8+
1. **CascadeTurn/Turn-by-Turn/Pipeline** `STT → Agent → TTS` approach built on top of a regular `Agent`
9+
10+
2. **Realtime/Speech-to-Speech/S2S** `LiveAgent` approach that streams audio to and from a provider's realtime API.
711

812
## When to use which
913

@@ -12,7 +16,7 @@ sidebarTitle: "Overview"
1216
| 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. |
1317
| Realtime full-duplex | `LiveAgent` + `OpenAIRealTimeConfig` / `GeminiRealTimeConfig` | <500 ms | You want barge-in, interruption, semantic VAD, or a phone-call-like UX. |
1418

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.
19+
See [Provider support](/docs/user-guide/live/stt_tts) for the matrix of currently supported providers.
1620

1721
!!! note
1822
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.

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

Lines changed: 53 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,12 @@ The STT → Agent → TTS flow turns any existing `Agent` into a voice agent wit
77

88
## Provider support
99

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.
10+
| Provider | STT | Streaming STT | TTS | Streaming TTS | Audio to Audio |
11+
|---|---|---|---|---|---|
12+
| OpenAI ||||||
13+
| ElevenLabs ||||||
14+
| Gemini ||||||
1115

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).
2016

2117
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.
2218

@@ -104,10 +100,6 @@ pipeline = ElevenLabsTranscriber("scribe_v2").pipe(agent)
104100

105101
`language_code` is only sent when you set it — leave it unset and Scribe auto-detects. `diarize=True` asks for speaker labels.
106102

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).
111103

112104
## Text-to-Speech
113105

@@ -207,9 +199,6 @@ if __name__ == "__main__":
207199
asyncio.run(main())
208200
```
209201

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-
213202
### Streaming directly to the speaker
214203

215204
`ElevenLabsStreamingTTSConfig.stream(...)` is usable on its own, outside any agent, when you just want to speak a known string:
@@ -241,20 +230,6 @@ observers=[ElevenLabsStreamingTTSObserver(tts, min_chars=40)]
241230

242231
Lowering it starts the first sentence sooner, at the cost of more requests and more granular sentences.
243232

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-
258233
## Combining STT and TTS
259234

260235
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.
@@ -305,6 +280,54 @@ if __name__ == "__main__":
305280
!!! tip
306281
`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.
307282

283+
### Mixing providers
284+
285+
This is an example of how one can use `openai` STT and `elevenlabs` TTS together.
286+
287+
```python linenums="1" hl_lines="19 23 27 35"
288+
import asyncio
289+
290+
from ag2 import Agent, config
291+
from ag2.events import TranscriptionChunkEvent
292+
from ag2.live import (
293+
ElevenLabsStreamingTTSConfig,
294+
ElevenLabsStreamingTTSObserver,
295+
OpenAITranscriber,
296+
SoundDevicePlayer,
297+
SoundDeviceRecorder,
298+
)
299+
300+
SAMPLE_RATE = 24000 # pcm_24000, what the player and recorder use
301+
302+
agent = Agent(
303+
"assistant",
304+
prompt="You are a helpful voice assistant. Keep answers to a few sentences.",
305+
config=config.OpenAIConfig("gpt-5-mini", streaming=True),
306+
observers=[ElevenLabsStreamingTTSObserver(ElevenLabsStreamingTTSConfig("eleven_flash_v2_5"))],
307+
)
308+
309+
async def main() -> None:
310+
pipeline = OpenAITranscriber("gpt-4o-mini-transcribe").pipe(agent)
311+
recorder = SoundDeviceRecorder(sample_rate=SAMPLE_RATE)
312+
313+
# OpenAI streams the transcript back as deltas — print them as live captions.
314+
async def on_caption(event: TranscriptionChunkEvent) -> None:
315+
print(event.content, end="", flush=True)
316+
317+
async with SoundDevicePlayer() as player:
318+
player.stream.where(TranscriptionChunkEvent).subscribe(on_caption)
319+
320+
print("Say something...")
321+
voice_input = recorder.record(duration=5)
322+
reply = await pipeline.ask(voice_input, stream=player.stream)
323+
print(f"\n{reply.body}")
324+
325+
if __name__ == "__main__":
326+
asyncio.run(main())
327+
```
328+
329+
Both halves share one stream, so `TranscriptionChunkEvent` (from the transcriber) and `SynthesizedAudioEvent` (from the observer) land in the same place — the player only subscribes to the latter, leaving the captions to your own subscriber.
330+
308331
## Examples
309332

310333
- `examples/stt_tts.py/stt_tts.py` — the OpenAI round-trip

0 commit comments

Comments
 (0)