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.
By default this loop is half-duplex (exchange_turn). To exercise 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.
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]
interruption_settings: deprecated — setPersona(interruption_behavior=...)on the golden instead. Defaulted toNone.
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. |
Two details worth knowing:
- STT is skipped when the connector already carries a transcript. Some platforms (like ElevenLabs) send the agent's own transcript alongside its audio; when present, it becomes
Turn.contentdirectly and yourstt_modelis never called for that turn. - Speech costs are tracked separately from the simulator model's LLM cost: after a run,
simulator.tts_costandsimulator.stt_costhold 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.
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:
.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.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).
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.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.