Build a speaking agent
Two ways to give an agent a voice: let it call the MCP voice_speak tool (simplest — Claude does the talking), or consume the streaming API directly for real-time playback.
1. MCP: the agent speaks by calling a tool
Connect the Era Voice MCP server (full setup on the MCP page):
claude mcp add --transport http era-voice https://voice.erasmuslabs.ai/mcp \
--header "Authorization: Bearer ev_your_key" The agent then lists voices and speaks; the audio is stored and fetched with the same API key:
# Inside a Claude session with the era-voice MCP server connected:
# 1. list what you can sound like
voice_list {}
# 2. speak — the tool stores the audio and returns an authed download URL
voice_speak {"text": "Deploy finished. All twelve checks green.", "voice_id": "VOICE_ID"}
# -> Generated 3.2s of wav audio from 41 characters.
# Download (requires your same Bearer API key): GET .../v1/audio/<generation_id> 2. Streaming API: real-time playback
For live agents (assistants, NPCs, call flows), consume POST /v1/tts/stream and play chunks as they land:
"""Minimal speaking agent: stream Era Voice TTS and play chunks as they arrive."""
import base64, json, subprocess, requests
API = "https://voice.erasmuslabs.ai"
KEY = "ev_your_key"
def speak(text: str, voice_id: str, language: str = "english"):
resp = requests.post(
f"{API}/v1/tts/stream",
headers={"Authorization": f"Bearer {KEY}"},
json={"text": text, "voice_id": voice_id, "language": language},
stream=True,
)
resp.raise_for_status()
for line in resp.iter_lines(decode_unicode=True):
if not line or not line.startswith("data: "):
continue # SSE frames are separated by blank lines
event = json.loads(line[len("data: "):])
if event.get("error"):
raise RuntimeError(event["error"])
if event.get("done"):
print(f"done: {event['total_duration_ms']}ms from {event['chars']} chars")
return
audio = base64.b64decode(event["audio_base64"])
# First audio lands after ~one sentence; play each chunk back-to-back.
subprocess.run(["afplay", "-"], input=audio) # macOS; use aplay/ffplay elsewhere
speak("Hello! I am your voice agent. Ask me anything.", voice_id="VOICE_ID") Latency notes
- First chunk is roughly one sentence away. Chunks are synthesized sequentially, so time-to-first-audio is one sentence of synthesis — keep the opening sentence short for snappy agents.
- Allow playback buffering: synthesis can take longer than the audio duration, so gapless playback is not guaranteed.
- Keep one agent utterance per request; the splitter handles up to 20,000 characters, but short turns stream best.
- WAV avoids compressed-audio decoding; MP3 reduces transfer size but adds encoding and decoding work.
- Non-streaming /v1/tts is simpler when the agent can wait for the full clip. Add word timestamps when you need captions or lip‑sync.
Measured September 11, 2026 through the public WebSocket endpoint using a 67-character, two-sentence English sample and the already-loaded Qwen3-TTS 1.7B Base model on NVIDIA GB10: 30 fresh TLS connections reached first audio in 2.50 seconds at p50 and 2.78 seconds at p95; 30 sequential connections with attempted TLS resumption measured 2.44 and 2.73 seconds (29 of those sessions resumed). Connect-to-ready p95 was 169 ms and 159 ms respectively. Receiving the full response took about 1.08 times the output audio duration at the median. These are observed results for this sample and load, not a latency guarantee. Model startup was not measured; longer opening sentences, concurrency, languages and network conditions can change latency.