Metadata-Version: 2.4
Name: pipecat-silma
Version: 0.1.0
Summary: Pipecat integration for SILMA AI text-to-speech (English and Arabic)
Author-email: SILMA AI <hello@silma.ai>
License-Expression: BSD-2-Clause
Project-URL: Homepage, https://silma.ai/
Project-URL: Source, https://github.com/silma-ai/pipecat-silma
Keywords: pipecat,tts,text-to-speech,arabic,english,silma,voice-ai
Classifier: Intended Audience :: Developers
Classifier: Topic :: Multimedia :: Sound/Audio
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3 :: Only
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pipecat-ai>=1.8.1
Requires-Dist: numpy>=1.26
Requires-Dist: websockets>=13.0
Requires-Dist: loguru>=0.7
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"
Dynamic: license-file

# SILMA TTS for Pipecat

A [Pipecat](https://github.com/pipecat-ai/pipecat) text-to-speech integration for
[SILMA AI](https://silma.ai/) — English and Arabic speech synthesis, with
Modern Standard Arabic and the Saudi (Najdi) dialect.

Maintained by SILMA AI. We build the TTS service this integration talks to, so
it stays current with the API.

**Tested with Pipecat v1.8.1.**

## Installation

```bash
uv add pipecat-silma
# or
pip install pipecat-silma
```

## Prerequisites

An API key from [app.silma.ai](https://app.silma.ai/api-keys):

```bash
export SILMA_API_KEY="..."
```

## Usage

```python
from pipecat_silma import SilmaTTSService

tts = SilmaTTSService(
    api_key=os.environ["SILMA_API_KEY"],
    settings=SilmaTTSService.Settings(
        model="silma-tts-v2-english",
        voice="emma",
    ),
)

pipeline = Pipeline(
    [
        transport.input(),
        stt,
        user_aggregator,
        llm,
        tts,
        transport.output(),
        assistant_aggregator,
    ]
)
```

### Models and voices

SILMA selects the language by model rather than by a language parameter. You
can still pass a Pipecat `Language`, in settings or in a
`TTSUpdateSettingsFrame`, and it will switch the model for you.

| Model | Language | Voices |
| --- | --- | --- |
| `silma-tts-v2-english` | English | `james`, `emma` |
| `silma-tts-v2-msa` | Modern Standard Arabic | `sarah`, `salma`, `salwa`, `saja`, `sultan`, `salman`, `sulaiman`, `salim` |
| `silma-tts-v2-ksa` | Arabic, Saudi (Najdi) dialect | same as MSA |

### Configuration

Constructor arguments, all also available on `SilmaTTSService.Settings`:

| Parameter | Default | Description |
| --- | --- | --- |
| `api_key` | `$SILMA_API_KEY` | Your SILMA API key |
| `model` | `silma-tts-v2-msa` | Model id, which also picks the language |
| `voice` | `sarah` | Pre-defined voice id |
| `creativity` | server default | Variance in speech prosody |
| `speed` | server default | Speed of the generated speech |
| `user_id` | `None` | Required for cloned voices and pronunciation overrides |
| `custom_audio_id` | `None` | Cloned voice id; requires `user_id` |
| `enable_server_pronunciation_overrides` | `False` | Apply account-level overrides; requires `user_id` |
| `base_url` | `https://api.silma.ai/tts/v2` | API base URL |
| `keepalive_interval_s` | `20.0` | Ping interval for an idle connection; lower it if the proxy in front of the API has a shorter read timeout |

`creativity` and `speed` are omitted from the request unless you set them, so
SILMA's own defaults apply.

Settings can be changed while the pipeline runs, via Pipecat's
`TTSUpdateSettingsFrame`:

```python
await task.queue_frame(TTSUpdateSettingsFrame(settings=SilmaTTSService.Settings(voice="salma")))
```

### Cloned voices

Upload a voice under **Custom Voices** at
[app.silma.ai/voices](https://app.silma.ai/voices), then pass its id together
with your user id:

```python
SilmaTTSService(
    settings=SilmaTTSService.Settings(
        model="silma-tts-v2-ksa",
        voice="sarah",
        user_id="...",
        custom_audio_id="voice_1769817467123",
    ),
)
```

### Pronunciation hints

SILMA reads phone numbers, emails and links correctly when they are tagged:

```
You can reach us on <STAG_PN>92005455</STAG_PN> or at <STAG_EMAIL>hi@silma.ai</STAG_EMAIL>.
```

The integration keeps these tags intact when it splits text, so a tag is never
cut in half across two requests. Instruct your LLM to emit them — the example
shows how.

Account-level pronunciation overrides configured at
[app.silma.ai/control](https://app.silma.ai/control) apply when you pass
`user_id` and `enable_server_pronunciation_overrides=True`.

## Running the example

[`examples/foundational.py`](examples/foundational.py) is a complete voice bot
over a local WebRTC transport — no telephony account, no cloud room.

```bash
uv add pipecat-silma "pipecat-ai[webrtc,silero,openai,runner]"

export SILMA_API_KEY="..."
export OPENAI_API_KEY="..."

python examples/foundational.py
```

Open the URL it prints and start talking. For English:

```bash
LANGUAGE=en python examples/foundational.py
```

## How it works

`SilmaTTSService` extends Pipecat's `InterruptibleTTSService`, which is the
right base for a websocket service with no context id and no cancel message:
the way to stop SILMA mid-utterance is to drop the socket and reconnect, and
that base class does exactly that on an interruption.

**Audio.** SILMA returns a 24 kHz mono float32 waveform. The service converts it
to the 16-bit PCM Pipecat carries in `TTSAudioRawFrame`, buffering samples that
straddle a chunk boundary so a split never produces a click. Because the API
takes no sample-rate parameter, the service pins its rate at 24000 and rejects a
request for anything else rather than mislabelling the audio — let the output
transport resample if your transport needs a different rate.

**Text limits.** The API caps a request at 250 characters, so a longer
aggregation is split on word boundaries and sent as sequential requests over the
same socket; their audio arrives in order and is concatenated into one context.

**Connection.** One websocket is held open and reused across turns, with a
keepalive ping every `keepalive_interval_s` so an idle connection is not closed
by the proxy in front of the API. On an interruption the socket is dropped and replaced — SILMA has no
cancel message, so that is how speech stops promptly, including when the user
talks over the very start of a reply. The replacement is not waited for: the
old socket is detached at once and closed in the background, because Pipecat
forwards the frame that stops playback only once the service's interruption
handler returns.


## Development

```bash
uv sync --extra dev
uv run pytest          # 40 tests, no API key or network needed
uv run ruff check .
uv run ruff format --check .
```

Two tests drive a real Pipecat pipeline to check barge-in behaviour, and
Pipecat's sentence aggregator needs NLTK's sentence data for that. They skip
themselves if the data is absent; to run them:

```bash
uv run python -c "import nltk; nltk.download('punkt_tab')"
```

## License

BSD 2-Clause, matching Pipecat. See [LICENSE](LICENSE).
