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
| Provider | Adapter | Status |
|---|---|---|
| Cartesia | CartesiaAdapter | ✅ Supported |
| ElevenLabs | — | SOON |
| OpenAI TTS | — | SOON |
BaseTTSAdapter — see Custom providers below.
Install
cartesia extra:
WebSocket (recommended)
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 withtrack_ws():
add_timestamps=True on your Cartesia context:
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: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 withtrack():
Custom providers
For providers other than Cartesia, extendBaseTTSAdapter 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
| Signal | SSE | WebSocket |
|---|---|---|
| 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 | ✗ | ✓ |
tuner-langchain) attached to the same session. With no LLM handler attached, this field is absent.
Troubleshooting
Interrupted turns record the full intended response
Interrupted turns record the full intended response
- On WebSocket, confirm
add_timestamps=Trueis 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.
Agent turns are missing from the call
Agent turns are missing from the call
- 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()).
Latency numbers look wrong or missing
Latency numbers look wrong or missing
- 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-observerfor e2e,tuner-langchainfor 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.