# Pipecat

Use Svara as the TTS service in a Pipecat pipeline.

## Overview

[Pipecat](https://docs.pipecat.ai) builds voice agents as a pipeline of frame processors. Svara plugs in as the TTS service through the official Python SDK: `from svara.pipecat import SvaraTTSService`. Everything else — transport, STT, LLM, VAD, turn-taking — is stock.

```
transport.input() ─▶ STT ─▶ user aggregator ─▶ LLM
                                              │
                       Svara TTS ◀────────────┘
                            └─▶ transport.output() ─▶ assistant aggregator
```

The service streams **24 kHz, 16-bit, mono PCM** by default. Set the transport’s `audio_out_sample_rate` to match and nothing resamples on the hot path; for telephony transports construct it with `response_format="ulaw", sample_rate=8000` and the G.711 bytes drop straight onto the call leg.

## The TTS service

Pipecat’s base class aggregates the LLM output into sentences and calls the service once per sentence; each sentence streams back over HTTP as it synthesizes, with TTFB metrics wired in.

```bash
pip install "svara-voice[pipecat] @ git+https://github.com/kenpath-labs/svara-python.git"
```

```python
from svara.pipecat import SvaraTTSService

tts = SvaraTTSService(voice="sv_enhdbrj5")        # Aanya; reads SVARA_API_KEY
# telephony: 8 kHz G.711 µ-law, no resampling anywhere
# tts = SvaraTTSService(voice="sv_enhdbrj5", response_format="ulaw", sample_rate=8000)
```

> The integration is beta — validate against your pinned `pipecat-ai` version. The service’s source is one small file ([svara/pipecat/__init__.py](https://github.com/kenpath-labs/svara-python/blob/main/src/svara/pipecat/__init__.py)) if you need to adapt it.

## Wiring the pipeline

Put the service where the TTS goes. Nothing else here is Svara-specific; swap STT or LLM for any other Pipecat service.

```bash
pip install 'pipecat-ai[openai,silero,webrtc]' pipecat-ai-prebuilt
```

```python
import os

from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.worker import PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
    LLMContextAggregatorPair, LLMUserAggregatorParams,
)
from pipecat.runner.types import RunnerArguments
from pipecat.runner.utils import create_transport
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.openai.stt import OpenAISTTService
from pipecat.transports.base_transport import TransportParams
from pipecat.workers.runner import WorkerRunner

from svara.pipecat import SvaraTTSService

SAMPLE_RATE = 24000


async def bot(runner_args: RunnerArguments) -> None:
    transport = await create_transport(runner_args, {
        "webrtc": lambda: TransportParams(
            audio_in_enabled=True,
            audio_out_enabled=True,
            audio_out_sample_rate=SAMPLE_RATE,   # match Svara; no resampling
        ),
    })

    context = LLMContext([{
        "role": "system",
        "content": "You are a helpful voice assistant. "
                   "Reply in one or two short sentences.",
    }])
    aggregators = LLMContextAggregatorPair(
        context,
        # In Pipecat 1.6 VAD and turn-taking live on the user aggregator,
        # NOT on the transport.
        user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()),
    )

    task = PipelineWorker(Pipeline([
        transport.input(),
        OpenAISTTService(api_key=os.environ["OPENAI_API_KEY"]),
        aggregators.user(),
        OpenAILLMService(api_key=os.environ["OPENAI_API_KEY"]),
        SvaraTTSService(voice="sv_enhdbrj5"),   # any id from GET /v1/voices
        transport.output(),
        aggregators.assistant(),
    ]))

    @transport.event_handler("on_client_connected")
    async def _(transport, client):
        context.add_message({"role": "system",
                             "content": "Greet the caller in one sentence."})
        await task.queue_frames([LLMRunFrame()])

    await WorkerRunner(handle_sigint=False).run(task)


if __name__ == "__main__":
    from pipecat.runner.run import main
    main()
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `SVARA_API_KEY` | env | - | Your Svara key (the SDK reads it automatically). |
| `SVARA_BASE_URL` | env | `https://api.kenpathlabs.com` | The gateway origin; override for a regional pin (api-in, api-us, api-eu). |
| `OPENAI_API_KEY` | env | - | For the stock STT and LLM services. |

`python bot.py` starts the dev runner and serves a prebuilt WebRTC client at `/client` — no media account needed.

## Token streaming

The service above receives whole sentences — Pipecat’s default text aggregation. To speak while the LLM is still writing, the underlying SDK client exposes the [input-streaming WebSocket](https://docs.kenpathlabs.com/input-streaming.md) directly: `client.speech.stream_input(token_stream, …)` takes any async iterator of text deltas and yields PCM as the model speaks. Pair it with `TextAggregationMode.TOKEN` and `append_to_audio_context()` in a custom processor; a first-class eager mode for `SvaraTTSService` is on the SDK roadmap.

## Gotchas

- **Use the gateway host.** `https://api.kenpathlabs.com` serves the API; the console host does not.
- **VAD is not a transport setting** in Pipecat 1.6. It moved to `LLMUserAggregatorParams(vad_analyzer=…)`, along with `user_turn_strategies`. Passing it to `TransportParams` fails validation.
- **`PipelineTask` and `PipelineRunner` are deprecated** in favour of `PipelineWorker` and `WorkerRunner`. They are drop-in.
- **The prebuilt UI package is `pipecat-ai-prebuilt`**. Install the wrong one and the server starts, prints “Bot ready”, and serves 404 at `/client`.
- **Errors surface as `ErrorFrame`s** with Svara’s status inside (`rate_limit_exceeded`, `too_many_concurrent_requests`, `insufficient_quota`) — three different situations that call for three different responses.
- **Leave the sampling knobs alone.** The server fills serving-tuned defaults; per-bot overrides are how long utterances start to ramble.
