Skip to main content
This guide uses tuner-tts-observer. It assumes you have a TunerSession set up — see the Quickstart for the session lifecycle and credentials.
tuner-tts-observer wraps your TTS provider’s synthesis stream to capture the agent side of the conversation — transcript, TTS time-to-first-byte, and end-to-end latency. Tuner observes synthesis, it does not own or drive it: removing the context managers leaves your synthesis code completely unaffected.

Supported adapters

ProviderAdapterStatus
CartesiaCartesiaAdapter✅ Supported
ElevenLabsSOON
OpenAI TTSSOON
Need a provider that isn’t listed? Extend BaseTTSAdapter — see Custom providers below.

Install

WebSocket support (recommended) requires the cartesia extra:
WebSocket is the production-standard pattern for real-time voice agents. It supports word-level interruption detection — when the user interrupts the agent, Tuner records only the words actually spoken, not the full intended response. Attach the adapter once per call, then wrap each turn’s receive loop with track_ws():
Word-accurate interruption capture requires add_timestamps=True on your Cartesia context:
Without it, interruptions are still recorded, but the spoken text cannot be cut at the word the user interrupted. TTS providers stream faster than real-time, so your loop can finish seconds before the audio finishes playing out loud. mark_interrupted() on the break path handles a mid-stream interruption; wait_for_interruption() on the else path keeps watching during that remaining gap and calls mark_interrupted() for you if one happens there. Skip the else branch and any interruption after the loop ends naturally is silently lost.

What your app owns

This package only records interruptions — it never detects or acts on them. You need: (1) a detection signal (VAD, provider event, client-side signal), (2) a shared flag or event, (3) the actual stop — breaking your loop, and telling the client to stop already-buffered audio if detection was server-side:
Both mark_interrupted() and wait_for_interruption() are optional — skip them and turns just record in full, nothing breaks.

SSE (legacy)

SSE is the simpler pattern, available for HTTP-only stacks or existing integrations. Interruption detection is supported, but spoken text cannot be cut accurately — the full intended response is recorded. Wrap your synthesis stream with track():

Custom providers

For providers other than Cartesia, extend BaseTTSAdapter from tuner_tts_observer — its docstring documents the full contract (timestamping, _record_agent_turn() / _record_tts_usage(), mark_interrupted()) with a worked example.

What gets captured

SignalSSEWebSocket
Agent transcript text✓ full text✓ spoken words only on interruption
Turn start timestamp
Turn duration
TTS TTFB
E2e latency
LLM latency✓*✓*
interrupted: true on barge-in
Word-accurate spoken text cut
* Populated from the session — requires a LangChain/LangGraph handler (tuner-langchain) attached to the same session. With no LLM handler attached, this field is absent.

Troubleshooting

  • On WebSocket, confirm add_timestamps=True is set on your Cartesia context — without word timestamps the spoken text cannot be cut.
  • Confirm your loop has the else: await tts.wait_for_interruption(barge_in_event) branch — without it, interruptions that happen after the loop ends naturally are never recorded.
  • On SSE this is expected behaviour — word-accurate cutting requires the WebSocket pattern.
  • Every synthesis turn must go through track_ws() / track() — turns synthesized outside the wrapper are invisible to Tuner.
  • Confirm the adapter was attached to the session before the first turn: tts = session.attach(CartesiaAdapter()).
  • E2e latency and LLM latency are assembled from other observers on the same session rather than measured by this adapter alone. If either is missing, confirm the relevant observer — tuner-stt-observer for e2e, tuner-langchain for LLM latency — is attached to the same session.

What’s Next?

Quickstart

The session lifecycle, credentials, and every observer wired together.

Speech-to-Text

Capture the user side — transcript and turn timing.

LangChain / LangGraph

Node transitions, tool calls, and LLM latency in your call timeline.

Create Call API

The underlying API every observer feeds into at flush time.