Voice Mode
Voice mode lets the ConversationSimulator talk to voice agents instead of text chatbots. Rather than wrapping your application in a model_callback, you pass a VoiceConfig: each simulated user turn is spoken aloud (text-to-speech), streamed to your agent over a live connection, and your agent's spoken reply is recorded and transcribed (speech-to-text) back into the conversation.
The result is the same list of ConversationalTestCases you get from a text simulation — ready for deepeval's multi-turn metrics — except every Turn also carries the actual audio that was spoken, and every assistant Turn records how long your agent took to respond.
How It Works
In voice mode, the simulator model still role-plays the user and generates each user message as text. The difference is what happens between the simulated user and your agent:
- The user message is synthesized into speech by the
tts_model. - The audio is sent to your voice agent through the
connector, which holds one live call per conversation (connected before the first turn, disconnected when the conversation ends). - The connector captures your agent's spoken reply and measures its response latency.
- If the connector doesn't surface a transcript itself, the reply audio is transcribed by the
stt_model. - Both sides of the exchange are recorded on the conversation's
Turns.
On a full-duplex connector such as LiveKitConnector, the caller keeps listening while it speaks, so nothing the agent says mid-turn is lost; on connectors that can't carry both voices at once, such as CallbackVoiceConnector, each side takes a turn. Neither makes the caller interrupt — for barge-in, see Interruptions.
Who does the talking on the simulated side — their voice, their temperament, the noise around them — comes from the golden's Persona.
Everything else about the simulation — scenario-driven user turns, stopping logic, simulation graphs, and lifecycle hooks — works exactly as in text mode.
Setup Voice Mode
In deepeval's ConversationSimulator, voice_config and model_callback are mutually exclusive: provide exactly one.
Start by choosing the connector that matches your deployed voice agent. deepeval provides connectors for ElevenLabs conversational agents, LiveKit agents, compatible raw-audio WebSockets, and in-process Python callables. You only need to implement a custom connector when none of these can reach your agent; see Voice Connectors.
The example below connects to an existing ElevenLabs conversational agent. ElevenLabsConnector is the bridge to that agent — it is not the TTS or STT model used by the simulator.
from deepeval.voice import VoiceConfig, ElevenLabsConnector
from deepeval.simulator import ConversationSimulator
voice_config = VoiceConfig(
connector=ElevenLabsConnector(agent_id="your-agent-id"),
combine_audio_files=True,
)
simulator = ConversationSimulator(voice_config=voice_config)There are ONE mandatory and FIVE optional parameters when creating a VoiceConfig:
connector: aBaseVoiceConnectorthat carries audio to and from your voice agent.- [Optional]
tts_model: aDeepEvalBaseTTSthat speaks the simulated user's messages. Defaulted toOpenAITTSModel. - [Optional]
stt_model: aDeepEvalBaseSTTthat transcribes your agent's spoken replies. Defaulted toOpenAISTTModel. - [Optional]
output_dir: a string directory to save conversation audio files into, orNoneto skip writing audio to disk. Left unset, it falls back to theDEEPEVAL_VOICE_FOLDERenvironment variable and then to".deepeval-voice-simulations". - [Optional]
combine_audio_files: a boolean which when set toTrue(andoutput_diris set), also writes a single stitched WAV of the full conversation alongside the per-turn files. Has no effect whenoutput_dirisNone. Defaulted toTrue. - [Optional]
record_call: a boolean which when set toTrue, records the whole call as it happened — both sides, in real time — and exposes it ascall_recording_pathon eachConversationalTestCase. Defaulted toFalse.
TTS and STT Models
Voice mode introduces two speech models that sit on opposite sides of the connector (see Voice concepts for why these are separate model families from LLMs, with their own base classes):
| Config field | Base class | Role | Default | Its quality affects |
|---|---|---|---|---|
tts_model | DeepEvalBaseTTS | Speaks the simulated user's messages to your agent | OpenAITTSModel (gpt-4o-mini-tts, alloy) | What your agent hears. Unclear or unnatural speech degrades your agent's own speech recognition and skews the whole simulation. |
stt_model | DeepEvalBaseSTT | Transcribes your agent's spoken replies into Turn.content | OpenAISTTModel (gpt-4o-transcribe) | Transcript fidelity. Every multi-turn metric judges this transcript, so STT accuracy directly bounds evaluation quality. |
STT is skipped when the connector already carries a transcript. Some platforms, including ElevenLabs, send the agent's transcript alongside its audio. When present, it becomes Turn.content directly and the stt_model is not called for that turn.
Speech costs are tracked separately from the simulator model's LLM cost. After a run, simulator.tts_cost and simulator.stt_cost contain the accumulated TTS and STT spend.
Using Custom Speech Models
The OpenAI defaults are convenient, but dedicated speech providers (Deepgram, AssemblyAI, ElevenLabs, Cartesia, etc.) often matter more here than for LLM evals — especially STT, since transcription accuracy caps how faithfully your metrics see the conversation. To use one, subclass the corresponding base class:
from deepeval.models.base_model import DeepEvalBaseTTS, DeepEvalBaseSTT
from deepeval.test_case import Audio
class MyTTSModel(DeepEvalBaseTTS):
def synthesize(self, text: str, **kwargs) -> tuple[Audio, float | None]:
audio_bytes = my_tts_provider.speak(text)
return Audio.from_bytes(audio_bytes, mimeType="audio/wav"), None
async def a_synthesize(self, text: str, **kwargs) -> tuple[Audio, float | None]:
return self.synthesize(text, **kwargs)
def load_model(self):
return self
def get_model_name(self) -> str:
return "my-tts-model"
class MySTTModel(DeepEvalBaseSTT):
def transcribe(self, audio: Audio, **kwargs) -> tuple[str, float | None]:
return my_stt_provider.transcribe(audio.get_bytes()), None
async def a_transcribe(self, audio: Audio, **kwargs) -> tuple[str, float | None]:
return self.transcribe(audio, **kwargs)
def load_model(self):
return self
def get_model_name(self) -> str:
return "my-stt-model"Both synthesize and transcribe return a tuple of the result and an optional cost, which feeds tts_cost / stt_cost accounting. Pass instances to VoiceConfig(tts_model=MyTTSModel(), stt_model=MySTTModel(), ...).
STT models also carry one voice-specific knob, truncated_audio_pad_seconds. When an interruption cuts your agent off mid-word, autoregressive transcribers tend to finish the clipped word and punctuate the sentence — putting speech in Turn.content that the caller never heard. Appending a little silence presents the clip as a whole utterance and curbs the guessing:
class MySTTModel(DeepEvalBaseSTT):
truncated_audio_pad_seconds = 0.3 # 0.0 (the default) transcribes as-isThe padding is used only for transcription, never for the audio saved on the turn, so durations and timings stay true to what was spoken. OpenAISTTModel sets 0.3; leave it at 0.0 for transcribers that don't complete words.
What A Voice Simulation Produces
Each simulated conversation is returned as a regular ConversationalTestCase, enriched with voice data:
voiceis set toTrue.- Every user
Turncarries the synthesizedaudiothat was played to your agent. - Every assistant
Turncarries your agent's replyaudio, the transcript ascontent, and the measured response time inlatency_ms. - When interruptions are enabled, an assistant turn cut short by a barge-in gets
interrupted=True. Frustrated barges can also surface on user turns viametadata. - When
record_call=True,call_recording_pathpoints to a stereo WAV of the entire call — caller on the left channel, your agent on the right.
Unless output_dir is None, the audio is also written to disk — one file per turn, plus a combined recording of the whole conversation when combine_audio_files=True, plus the full-call recording as deepeval-call-recording.wav when record_call=True:
.deepeval-voice-simulations/
└── simulation-2026-08-10_12-30-00/
├── deepeval-turn-1-user.wav
├── deepeval-turn-1-assistant.wav
├── deepeval-turn-2-user.wav
├── deepeval-turn-2-assistant.wav
├── deepeval-conversation.wav
└── deepeval-call-recording.wavWhen simulating multiple goldens in one run, each conversation gets its own sub-folder named after the golden's name (or conversation-<index> when unnamed).
Each conversation also logs one INFO line from deepeval.simulator with the agent's and the caller's average and worst reply times, so you can read latency without opening every Turn.
Set DEEPEVAL_VOICE_FOLDER to keep recordings somewhere else — a scratch disk, or a path outside the repo — without changing code. An explicit VoiceConfig(output_dir=...) overrides it, and DEEPEVAL_FILE_SYSTEM=READ_ONLY overrides both, writing nothing at all.
FAQs
Do I still need a model callback in voice mode?
voice_config replaces model_callback, and providing both raises an error. In voice mode the simulator talks to your agent through the connector instead of a Python callback.Which TTS and STT models are used by default?
OpenAITTSModel speaks the simulated user and OpenAISTTModel transcribes your agent's replies, both using your OPENAI_API_KEY. Swap in any custom model by subclassing DeepEvalBaseTTS or DeepEvalBaseSTT and passing it to VoiceConfig.How is latency measured?
latency_ms on the assistant Turn. How end-of-turn is detected (silence thresholds, turn-complete events) is defined per VoiceProtocol, so latencies are comparable across connectors that share a protocol — but not across different protocols.Why do voice simulations run one conversation at a time?
max_concurrent.Do interruptions count toward the turn limit?
max_user_simulations, including barge turns, so an interrupting persona reaches the limit in fewer exchanges.My agent isn't on ElevenLabs or LiveKit — can I still simulate it?
WebSocketConnector for raw-audio agents, wrap an in-process callable with CallbackVoiceConnector, or subclass BaseVoiceConnector for other transports.