TTS observability for Tuner — Cartesia adapter with barge-in and latency tracking.
Project description
tuner-tts-observer
TTS observability for Tuner. Wraps your TTS provider's synthesis stream to capture agent transcript and 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 |
Need a provider that isn't listed? Extend BaseTTSAdapter — see
Custom providers below.
Installation
pip install tuner-core tuner-tts-observer
WebSocket support (recommended) requires the websockets extra:
pip install "tuner-tts-observer[cartesia]"
# equivalent to: pip install "cartesia[websockets]>=1.0"
Usage
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.
import os
from cartesia import AsyncCartesia
from tuner_core import TunerConfig, TunerSession
from tuner_tts_observer import CartesiaAdapter
cartesia_client = AsyncCartesia(api_key=os.environ["CARTESIA_API_KEY"])
session = TunerSession(config=TunerConfig.from_env(), call_id=call_id)
adapter = session.attach(CartesiaAdapter())
# Open one WebSocket connection per call — reused across turns
async with cartesia_client.tts.websocket_connect() as tts_ws:
# Per turn:
ctx = await tts_ws.context(
model_id="sonic-2",
voice={"id": "your-voice-id", "mode": "id"},
output_format={"container": "raw", "encoding": "pcm_s16le", "sample_rate": 16000},
add_timestamps=True, # required for word-level interruption detection
)
await ctx.push(agent_text)
await ctx.no_more_inputs()
async with adapter.track_ws(agent_text) as tracked:
async for chunk in tracked(ctx.receive()):
if barge_in_event.is_set():
break # interrupted — Tuner records only what was spoken
if chunk.audio:
await websocket.send_bytes(chunk.audio)
await session.flush()
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.
import os
from cartesia import Cartesia
from tuner_core import TunerConfig, TunerSession
from tuner_tts_observer import CartesiaAdapter
cartesia_client = Cartesia(api_key=os.environ["CARTESIA_API_KEY"])
session = TunerSession(config=TunerConfig.from_env(), call_id=call_id)
adapter = session.attach(CartesiaAdapter())
with adapter.track(agent_text) as tracked:
for chunk in tracked(cartesia_client.tts.sse(
model_id="sonic-2",
transcript=agent_text,
voice={"id": "your-voice-id", "mode": "id"},
output_format={"container": "raw", "encoding": "pcm_s16le", "sample_rate": 16000},
)):
if chunk.audio:
await websocket.send_bytes(chunk.audio)
await session.flush()
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 automatically
| Signal | SSE | WebSocket |
|---|---|---|
| Agent transcript text | ✓ full text | ✓ spoken words only on interruption |
| Turn start timestamp | ✓ (first audio chunk) | ✓ |
| Turn duration | ✓ (acoustic length) | ✓ |
| TTS TTFB | ✓ | ✓ |
| E2e latency | ✓ | ✓ |
| LLM latency | ✓ | ✓ |
interrupted: true on barge-in |
✓ | ✓ |
| Word-accurate spoken text cut | ✗ | ✓ |
Development
uv sync
make test
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tuner_tts_observer-0.1.0.tar.gz.
File metadata
- Download URL: tuner_tts_observer-0.1.0.tar.gz
- Upload date:
- Size: 16.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d21799475913aae39e74336bb233f67c51e85f02a205e32030bb3850da285277
|
|
| MD5 |
cb1b516c3b1d95e984c89d9c0075ef89
|
|
| BLAKE2b-256 |
d0ee00b3908c9f4c434be56d8b72f45b6b19aed2af16267ce813a760b6f5b1ec
|
File details
Details for the file tuner_tts_observer-0.1.0-py3-none-any.whl.
File metadata
- Download URL: tuner_tts_observer-0.1.0-py3-none-any.whl
- Upload date:
- Size: 12.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
260b616f7dd4b4678fa2bc837b354159bf50cc8661a81727eacedda11d6e87c6
|
|
| MD5 |
0f956041c7336e7886872a1b097271f6
|
|
| BLAKE2b-256 |
46c897fd65754592e598732db1ddfbb5b349975f162c4756dc1f3a5d6875a737
|