# The Pipecat service

> pipecat-tts-cache-router: install it, construct TTSCacheRouterTTSService, change settings mid-session, and what it reports.

## Install

`pipecat-tts-cache-router` is a Pipecat TTS service that speaks through the router, for every provider. It needs Python 3.11 or later and `pipecat-ai` 1.12 or later. This page describes version 0.1.1.

```shell
pip install pipecat-tts-cache-router
```

**bot.py**

```python
import os

from pipecat_tts_cache_router import TTSCacheRouterTTSService

tts = TTSCacheRouterTTSService(
    api_key=os.environ["TTS_CACHE_ROUTER_API_KEY"],
    settings=TTSCacheRouterTTSService.Settings(
        model="sarvam/bulbul:v3", voice="shubh", language="hi-IN"
    ),
)

# then, in your bot:
# pipeline = Pipeline([transport.input(), stt, user_aggregator, llm, tts,
#                      transport.output(), assistant_aggregator])
```

Put `tts` in your pipeline where a TTS service goes, after the LLM. Set `TTS_CACHE_ROUTER_API_KEY` to your router API key, and store your provider key under [Provider keys](https://ttscacherouter.dev/dashboard/provider-keys): the service never sends one. It connects when the pipeline starts, and the router connects to your provider at once, so the first reply doesn’t wait for either.

## TTSCacheRouterTTSService(...)

The service's one class. Give it your API key and the provider's settings.

- `api_key` (`str`, Required): Your router API key.
- `settings` (`TTSCacheRouterTTSService.Settings`, Required): The provider, model, voice and settings: see [Settings](https://docs.ttscacherouter.dev/pipecat.md#settings).
- `base_url` (`str`, Default `wss://api.ttscacherouter.dev`): The router. Change it only for a router you run yourself.
- `sample_rate` (`int | None`, Default `None`): Of the PCM audio the service produces, one the provider streams. `None`: the pipeline’s output sample rate (`PipelineParams.audio_out_sample_rate`, 24000 by default, which every provider streams). Each provider’s rates are in [Models and options](https://docs.ttscacherouter.dev/reference/models.md).

Other keyword arguments go to Pipecat’s `WebsocketTTSService`, except `text_aggregation_mode`: the router splits the text into sentences itself, so the service streams the LLM’s text to it as it arrives and each sentence starts as soon as it is complete.

## Settings

The settings are in the router’s own names, those of [/v1/audio/speech](https://docs.ttscacherouter.dev/clients/speech.md#body) and `GET /v1/models`, the same for every provider.

**Sarvam**

```python
settings=TTSCacheRouterTTSService.Settings(
    model="sarvam/bulbul:v3",
    voice="shubh",
    language="hi-IN",
    options={"temperature": 0.6},
)
```

**Raya**

```python
settings=TTSCacheRouterTTSService.Settings(
    model="raya/standard",
    voice="d6a002d0-230c-49b1-a137-b8a7d564b1ae",
    language="hi",
)
```

**ElevenLabs**

```python
settings=TTSCacheRouterTTSService.Settings(
    model="elevenlabs/eleven_flash_v2_5",
    voice="JBFqnCBsd6RMkjVDRZzb",
    options={"voice_settings": {"stability": 0.5, "similarity_boost": 0.75}},
)
```

**OpenAI**

```python
settings=TTSCacheRouterTTSService.Settings(
    model="openai/gpt-4o-mini-tts",
    voice="ash",
    options={"instructions": "Speak warmly."},
)
```

**Deepgram**

```python
settings=TTSCacheRouterTTSService.Settings(
    model="deepgram/aura-2",
    voice="thalia-en",
)
```

**xAI**

```python
settings=TTSCacheRouterTTSService.Settings(
    model="xai/xai-tts",
    voice="ara",
    language="auto",
)
```

**Soniox**

```python
settings=TTSCacheRouterTTSService.Settings(
    model="soniox/tts-rt-v2",
    voice="Adrian",
    language="en",
)
```

**Smallest AI**

```python
settings=TTSCacheRouterTTSService.Settings(
    model="smallestai/lightning_v3.1_pro",
    voice="meher",
    language="en",
)
```

**Cartesia**

```python
settings=TTSCacheRouterTTSService.Settings(
    model="cartesia/sonic-3",
    voice="f786b574-daa5-4673-aa0c-cbe3e8534c02",
    language="en",
)
```

- `model` (`str`, Required): Any model `GET /v1/models` lists, with its provider: `sarvam/bulbul:v3`, `elevenlabs/eleven_flash_v2_5`, `openai/tts-1`, `deepgram/aura-2`, `xai/xai-tts`, … ([Models and options](https://docs.ttscacherouter.dev/reference/models.md)).
- `voice` (`str`, Required): The provider’s voice id, as [GET /v1/voices](https://docs.ttscacherouter.dev/reference/voices.md) lists it.
- `language` (`str | Language | None`, Default `None`): The provider’s language code, sent as written: `hi-IN` for Sarvam, `hi` for Raya, `en` for ElevenLabs, Soniox, Smallest AI and Cartesia. Required for Sarvam, Raya and Soniox. Optional for ElevenLabs and xAI: left out, the provider detects it. Optional for Smallest AI and Cartesia: left out, `en`. Not taken by OpenAI or Deepgram.
- `speed` (`float | None`, Default `None`): The speaking rate, where the provider takes one. None: the router's default.
- `options` (`dict`, Default `{}`): The provider’s other settings, by their names in [Models and options](https://docs.ttscacherouter.dev/reference/models.md): `{"temperature": 0.6}` for Sarvam, `{"voice_settings": {...}}` for ElevenLabs, `{"instructions": "..."}` for OpenAI.

The service checks `model` and `voice` when you construct it, and raises `ValueError` for a malformed one. The router checks the rest as the pipeline starts, when the service connects: a wrong setting is reported then as an error (`bad_request`), not at the first reply.

## Changing settings

A `TTSUpdateSettingsFrame` changes the voice or settings mid-session. Replies that start afterwards use them; the reply being spoken finishes as it began.

```python
from pipecat.frames.frames import TTSUpdateSettingsFrame

# Another voice:
await worker.queue_frame(
    TTSUpdateSettingsFrame(delta=TTSCacheRouterTTSService.Settings(voice="echo"))
)

# Or another provider; its language, speed and options start from none:
await worker.queue_frame(
    TTSUpdateSettingsFrame(
        delta=TTSCacheRouterTTSService.Settings(
            model="sarvam/bulbul:v3", voice="shubh", language="hi-IN"
        )
    )
)
```

A `model` of another provider starts from no `language`, `speed` or `options` unless the update gives them, and `options` are replaced as a whole. Between replies, the router connects to the provider for the new settings at once. A sentence in a new voice or with new settings is a new cache entry: synthesized once, then cached.

## Interruptions

When the user interrupts, the service tells the router to stop the reply and keeps the connection.

The router stops sending the reply’s audio at once, and the provider stops the sentence it was synthesizing, so nothing cut off is cached (ElevenLabs and Cartesia finish their sentence, and cache it). The next reply starts on the same connection.

## Transcripts

The service's TTSTextFrames follow the audio, so the conversation context holds what was spoken.

Each sentence’s words are timed to the moment its audio starts. With ElevenLabs and Cartesia, transcripts are word by word, from the provider’s own timings (turn them off with `options={"sync_alignment": False}` or `options={"word_timestamps": False}`). Timings are cached with the audio, so a reply from the cache is aligned too.

## Metrics

The TTS usage metric counts only the characters the provider billed.

With usage metrics on, each reply reports one `TTSUsageMetricsData`: the characters of its sentences the provider synthesized. Sentences from the cache count zero, so a fully cached reply reports `0`. Its `model` is the settings’ `<provider>/<model>`, so usage dashboards can attribute cost to the provider. TTFB is Pipecat’s, as for any TTS service.

```python
from pipecat.pipeline.worker import PipelineParams, PipelineWorker

worker = PipelineWorker(
    pipeline,
    params=PipelineParams(enable_metrics=True, enable_usage_metrics=True),
)
```

## Cache stats

Once a reply is all sent, the service calls its `on_cache_stats` handlers: how much of the reply came from your org’s cache. This script speaks a sentence twice, without a transport, and prints both:

**speak_twice.py**

```python
import asyncio
import os

from pipecat.frames.frames import EndFrame, TTSSpeakFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.worker import PipelineWorker
from pipecat.workers.runner import WorkerRunner
from pipecat_tts_cache_router import TTSCacheRouterTTSService


async def main() -> None:
    tts = TTSCacheRouterTTSService(
        api_key=os.environ["TTS_CACHE_ROUTER_API_KEY"],
        settings=TTSCacheRouterTTSService.Settings(model="openai/tts-1", voice="alloy"),
    )

    @tts.event_handler("on_cache_stats")
    async def on_cache_stats(tts, stats):
        print(f"{stats.cached_sentences}/{stats.sentences} sentences from the cache, "
              f"{stats.billed_characters}/{stats.characters} characters billed by the provider")

    worker = PipelineWorker(Pipeline([tts]))
    runner = WorkerRunner(handle_sigint=False)
    await runner.add_workers(worker)
    # The second time, from the cache.
    await worker.queue_frames(
        [TTSSpeakFrame("Your order has shipped."), TTSSpeakFrame("Your order has shipped."), EndFrame()]
    )
    await runner.run()


asyncio.run(main())
```

```text
0/1 sentences from the cache, 23/23 characters billed by the provider
1/1 sentences from the cache, 0/23 characters billed by the provider
```

- `context_id` (`str`): The reply's TTS context id.
- `sentences` (`int`): The sentences the reply was split into.
- `cached_sentences` (`int`): Those served from the cache.
- `characters` (`int`): The characters of all the reply's sentences.
- `billed_characters` (`int`): The characters the provider synthesized and billed, as the usage metric. `cached_characters` is the rest.

An interrupted reply reports both too: they count the sentences the router sent in full before the interruption, so the usage is what the provider billed for the audio your bot received.

## Errors

Router errors become Pipecat ErrorFrames, with a category, so your pipeline's error handling and ServiceSwitcher work as with any TTS.

| What happened | ErrorFrame category |
| --- | --- |
| The router refused the API key | `AUTHENTICATION` |
| Your org is out of credit | `QUOTA` |
| A setting the router refused (`bad_request`) | `INVALID_REQUEST` |
| No key stored for the provider (`provider_key_missing`) | `AUTHENTICATION` |
| The provider refused the request (`provider_rejected`) | From the provider’s HTTP status, as Pipecat classifies it (`AUTHENTICATION` for 401, `AUTHORIZATION` for 403, `QUOTA` for 402); `INVALID_REQUEST` without one |
| Your provider quota (`provider_rate_limited`) | `RATE_LIMIT` |
| The provider failed or was late, or the router failed | `SERVER` |
| The connection failed or dropped | `CONNECTIVITY` |

A reply that fails ends without the rest of its audio; the next reply starts on a new connection. An `AUTHENTICATION`, `AUTHORIZATION` or `INVALID_REQUEST` error marks the service unusable, as Pipecat does for any service, until its settings change. When the router restarts, the service reconnects at once.

> Waiting for the LLM is not a timeout: the service gives up only after it has sent all of a reply’s text and the router goes quiet for 45 seconds.

- [Models and options](https://docs.ttscacherouter.dev/reference/models.md): Every provider's models, formats and options.
- [Voices](https://docs.ttscacherouter.dev/reference/voices.md): GET /v1/voices: every voice your org can speak with.
- [WebSocket protocol](https://docs.ttscacherouter.dev/clients/stream.md): What the service sends and receives.
