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
Metadata
Release files for tuner-tts-observer 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| tuner_tts_observer-0.1.0.tar.gz | 16.2 kB | Without provenance | Details for tuner_tts_observer-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| tuner_tts_observer-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for tuner_tts_observer-0.1.0-py3-none-any.whl |
Total release size: 29.0 kB
Release files / tuner_tts_observer-0.1.0.tar.gz
| Download URL | tuner_tts_observer-0.1.0.tar.gz |
|---|---|
| Size | 16.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d21799475913aae39e74336bb233f67c51e85f02a205e32030bb3850da285277
|
|
BLAKE2b-256 checksum How to use checksums |
d0ee00b3908c9f4c434be56d8b72f45b6b19aed2af16267ce813a760b6f5b1ec
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.7
|
Release files / tuner_tts_observer-0.1.0-py3-none-any.whl
| Download URL | tuner_tts_observer-0.1.0-py3-none-any.whl |
|---|---|
| Size | 12.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
260b616f7dd4b4678fa2bc837b354159bf50cc8661a81727eacedda11d6e87c6
|
|
BLAKE2b-256 checksum How to use checksums |
46c897fd65754592e598732db1ddfbb5b349975f162c4756dc1f3a5d6875a737
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.7
|
Security for tuner-tts-observer 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.