Metadata-Version: 2.4
Name: speechflow
Version: 0.8.0
Summary: Async-first TTS (Text-to-Speech) wrapper library for Python
Author-email: minamik <mia@sync.dev>
License-Expression: MIT
License-File: LICENSE
Keywords: fishaudio,gemini,kokoro,openai,speech-synthesis,style-bert-vits2,text-to-speech,tts
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Requires-Dist: numpy>=1.26.4
Provides-Extra: all
Requires-Dist: elevenlabs>=2.0.0; extra == 'all'
Requires-Dist: en-core-web-sm<4.0.0,>=3.8.0; extra == 'all'
Requires-Dist: fish-audio-sdk>=1.2.0; extra == 'all'
Requires-Dist: google-genai>=1.18.0; extra == 'all'
Requires-Dist: kokoro>=0.9.4; extra == 'all'
Requires-Dist: misaki[ja]>=0.9.4; extra == 'all'
Requires-Dist: openai>=1.84.0; extra == 'all'
Requires-Dist: qwen-tts>=0.1.1; extra == 'all'
Requires-Dist: sentencepiece>=0.2; extra == 'all'
Requires-Dist: sounddevice>=0.5.0; extra == 'all'
Requires-Dist: style-bert-vits2-mk>=2.8.8; extra == 'all'
Requires-Dist: torch; extra == 'all'
Requires-Dist: torchaudio; extra == 'all'
Requires-Dist: websockets>=13.0; extra == 'all'
Provides-Extra: elevenlabs
Requires-Dist: elevenlabs>=2.0.0; extra == 'elevenlabs'
Provides-Extra: fishaudio
Requires-Dist: fish-audio-sdk>=1.2.0; extra == 'fishaudio'
Provides-Extra: gemini
Requires-Dist: google-genai>=1.18.0; extra == 'gemini'
Provides-Extra: kokoro
Requires-Dist: en-core-web-sm<4.0.0,>=3.8.0; extra == 'kokoro'
Requires-Dist: kokoro>=0.9.4; extra == 'kokoro'
Requires-Dist: misaki[ja]>=0.9.4; extra == 'kokoro'
Requires-Dist: torch; extra == 'kokoro'
Provides-Extra: openai
Requires-Dist: openai>=1.84.0; extra == 'openai'
Provides-Extra: player
Requires-Dist: sounddevice>=0.5.0; extra == 'player'
Provides-Extra: qwen3tts
Requires-Dist: qwen-tts>=0.1.1; extra == 'qwen3tts'
Requires-Dist: torchaudio; extra == 'qwen3tts'
Provides-Extra: stylebert
Requires-Dist: sentencepiece>=0.2; extra == 'stylebert'
Requires-Dist: style-bert-vits2-mk>=2.8.8; extra == 'stylebert'
Requires-Dist: torch; extra == 'stylebert'
Provides-Extra: ws
Requires-Dist: websockets>=13.0; extra == 'ws'
Description-Content-Type: text/markdown

# SpeechFlow

English | [日本語](README.ja.md)

SpeechFlow is an async-first Python library for text-to-speech (TTS). It gives
cloud and local TTS engines the same `get()` and `stream()` interface, and it
ships small audio utilities for playback, WAV writing, and WebSocket output.

The core package depends only on NumPy. Engines and I/O integrations are
installed as optional extras, so applications can install only the backends they
use.

## What You Get

- Unified async engine API: `await engine.get(...)` and `async for chunk in engine.stream(...)`
- Optional sync wrappers for non-async applications
- Normalized `AudioData` objects: float32 PCM samples, usually mono, with an explicit sample rate
- Cloud engines: OpenAI, Google Gemini, FishAudio, ElevenLabs
- Local engines: Kokoro, Qwen3-TTS, Style-Bert-VITS2
- Audio utilities: WAV writing, sounddevice playback, and `/playback` WebSocket output
- Domain-specific exceptions: `TTSError`, `ConfigurationError`, `StreamingError`, and `AudioProcessingError`

## Installation

SpeechFlow requires Python 3.11 or newer.

Core only:

```bash
uv add speechflow
```

Install one engine:

```bash
uv add "speechflow[openai]"
```

Install an engine with playback support:

```bash
uv add "speechflow[openai,player]"
```

Install everything:

```bash
uv add "speechflow[all]"
```

Using pip:

```bash
pip install speechflow
pip install "speechflow[openai]"
pip install "speechflow[openai,player]"
pip install "speechflow[all]"
```

### Optional Extras

| Extra | Adds | Main dependency |
| --- | --- | --- |
| `openai` | `OpenAITTSEngine` | `openai` |
| `gemini` | `GeminiTTSEngine` | `google-genai` |
| `fishaudio` | `FishAudioTTSEngine` | `fish-audio-sdk` |
| `elevenlabs` | `ElevenLabsTTSEngine` | `elevenlabs` |
| `kokoro` | `KokoroTTSEngine` | `kokoro`, `torch`, Japanese language helpers |
| `qwen3tts` | `Qwen3TTSEngine` | `qwen-tts` |
| `stylebert` | `StyleBertTTSEngine` | `style-bert-vits2`, `torch` |
| `player` | `AudioPlayer` | `sounddevice` |
| `ws` | `WebSocketAudioSink` | `websockets` |
| `all` | All engines, playback, and WebSocket output | All optional dependencies above |

Optional engine classes are exported from the top-level package lazily. After
installing the `openai` extra, `from speechflow import OpenAITTSEngine`
becomes available, but importing `speechflow` itself does not load optional
engine dependencies. Accessing a class whose extra is not installed raises an
install-hint error such as `pip install "speechflow[openai]"`.

### GPU Setup For Local Engines

Local engines can be CPU-only, but GPU acceleration is often important for
Kokoro, Qwen3-TTS, and Style-Bert-VITS2. If you need a specific PyTorch build,
install it before installing the SpeechFlow extra so your environment keeps the
wheel you selected.

Example for CUDA 12.6:

```bash
# uv
uv add torch torchvision torchaudio --index https://download.pytorch.org/whl/cu126
uv add "speechflow[kokoro]"

# pip
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
pip install "speechflow[kokoro]"
```

Replace `cu126` with the CUDA wheel that matches your system.

## Quick Start

### Generate And Save Audio

```python
import asyncio

from speechflow import AudioWriter, OpenAITTSEngine


async def main() -> None:
    engine = OpenAITTSEngine(api_key="your-openai-api-key")
    writer = AudioWriter()

    audio = await engine.get("Hello from SpeechFlow.")
    await writer.save(audio, "hello.wav")

    print(audio.sample_rate, audio.channels, audio.duration)


asyncio.run(main())
```

### Stream Audio

Every engine exposes `stream()` as an async iterator of `AudioData` chunks. The
chunking behavior depends on the engine, but consuming code can treat every
stream the same way.

```python
import asyncio

from speechflow import AudioWriter, OpenAITTSEngine


async def main() -> None:
    engine = OpenAITTSEngine(api_key="your-openai-api-key")
    writer = AudioWriter()

    await writer.save_stream(
        engine.stream("This can be a longer piece of text."),
        "streamed.wav",
    )


asyncio.run(main())
```

### Play Audio

Install the `player` extra first:

```bash
uv add "speechflow[openai,player]"
```

```python
import asyncio

from speechflow import AudioPlayer, OpenAITTSEngine


async def main() -> None:
    engine = OpenAITTSEngine(api_key="your-openai-api-key")
    player = AudioPlayer()

    audio = await engine.get("This will play through the default audio device.")
    await player.play(audio)


asyncio.run(main())
```

For streaming playback:

```python
combined = await player.play_stream(engine.stream("Play chunks as they arrive."))
```

`play_stream()` returns one combined `AudioData` containing every played chunk.

### Warm Up Local Engines

Local engines can have a noticeable cold penalty on their first synthesis while lazy model and GPU resources initialize. Call `warmup()` after constructing an engine and before accepting the first request:

```python
engine = StyleBertTTSEngine(model_name="jvnv-F1-jp", device="cuda")
await engine.warmup()

audio = await engine.get("The first user request is now served warm.")
```

`warmup()` is awaitable and completes only when preparation succeeds. Failures propagate as `TTSError`; repeated calls after success are inexpensive no-ops. Engines without specialized warmup behavior inherit an immediate no-op. Synchronous applications can call the blocking `warmup_sync()` wrapper.

### Use Sync Wrappers

Sync wrappers provide a blocking API for callers that cannot use the async API directly. They submit work to one shared daemon background event loop and block the calling thread until a result is available. They also work when the caller already has a running event loop, but async code should normally prefer the async API to avoid blocking its event-loop thread.

```python
from speechflow import AudioWriter, OpenAITTSEngine

engine = OpenAITTSEngine(api_key="your-openai-api-key")
writer = AudioWriter()

audio = engine.get_sync("Hello from sync code.")
writer.save_sync(audio, "sync.wav")
```

Important sync behavior:

- Engine, writer, and player sync wrappers all use the same thread-safe background event loop.
- `stream_sync()` yields each async chunk synchronously as it arrives. Closing a partially consumed iterator also closes the underlying async stream.
- Engine-specific per-call arguments such as `speed`, `instructions`, `stability`, or `speaker` are available on async `get()` and `stream()`. The inherited engine sync wrappers only accept `text`, `model`, and `voice`.
- Every sync wrapper blocks its calling thread. Prefer `await` and `async for` in async code.

## Public API

Top-level imports from `speechflow`:

| Name | Availability | Purpose |
| --- | --- | --- |
| `AudioData` | Core | Audio container with `data`, `sample_rate`, `channels`, `format`, and `duration` |
| `TTSEngineBase` | Core | Abstract async engine contract |
| `AudioWriter` | Core | WAV writing for complete buffers and streams |
| `AudioPlayer` | `player` extra | Playback for complete buffers and streams |
| `OpenAITTSEngine` | `openai` extra | OpenAI TTS engine |
| `GeminiTTSEngine` | `gemini` extra | Google Gemini TTS engine |
| `FishAudioTTSEngine` | `fishaudio` extra | FishAudio TTS engine |
| `ElevenLabsTTSEngine` | `elevenlabs` extra | ElevenLabs TTS engine |
| `KokoroTTSEngine` | `kokoro` extra | Local Kokoro engine |
| `Qwen3TTSEngine` | `qwen3tts` extra | Local Qwen3-TTS engine |
| `StyleBertTTSEngine` | `stylebert` extra | Local Style-Bert-VITS2 engine |
| `TTSError` | Core | Base SpeechFlow exception |
| `ConfigurationError` | Core | Invalid configuration, missing credentials, unsupported parameters |
| `AudioProcessingError` | Core | Audio conversion, playback, writing, or output failures |
| `StreamingError` | Core | Mid-stream synthesis failures |
| `EngineNotFoundError` | Core | Requested engine is unavailable or not installed |

`WebSocketAudioSink` is exported from `speechflow.audio` when the `ws` extra is
installed:

```python
from speechflow.audio import WebSocketAudioSink
```

## AudioData

Engines and audio utilities exchange `AudioData`:

```python
from speechflow import AudioData

audio = AudioData(
    data=samples,       # numpy.ndarray, normally float32 samples in [-1, 1]
    sample_rate=24000,  # int
    channels=1,         # int, default 1
    format="pcm",       # str, default "pcm"
)

seconds = audio.duration
```

`AudioData` is a lightweight container. It does not validate or normalize data
when you construct it yourself. SpeechFlow engines return normalized float32 PCM
data by convention.

## Engine Behavior

All engines implement:

```python
audio = await engine.get(text, model=None, voice=None)

async for chunk in engine.stream(text, model=None, voice=None):
    ...
```

Engine-specific options are added as keyword arguments without changing the base
`text`, `model`, and `voice` shape.

| Engine | Extra | Output rate | Streaming behavior |
| --- | --- | --- | --- |
| OpenAI | `openai` | 24 kHz | True chunked streaming |
| Gemini | `gemini` | Parsed from response, usually 24 kHz | One complete-audio chunk in this implementation |
| FishAudio | `fishaudio` | 24 kHz | True chunked streaming |
| ElevenLabs | `elevenlabs` | From `output_format`, default 24 kHz | True chunked streaming |
| Kokoro | `kokoro` | 24 kHz | Segment-by-segment local generation |
| Qwen3-TTS | `qwen3tts` | 24 kHz | Sentence-by-sentence local generation |
| Style-Bert-VITS2 | `stylebert` | Model-dependent, JVNV is typically 44.1 kHz | Sentence-by-sentence local generation |

## Engines

Examples in this section show code that belongs inside an async function unless
they include an `asyncio.run()` wrapper.

### OpenAI

```python
from speechflow import OpenAITTSEngine

engine = OpenAITTSEngine(
    api_key="your-openai-api-key",
    model="gpt-4o-mini-tts",
    voice="alloy",
    instructions="Speak clearly and warmly.",
    speed=1.0,
)

audio = await engine.get(
    "Hello.",
    voice="nova",
    speed=1.05,
    instructions="Use a calm tone.",
)

async for chunk in engine.stream("Stream this text."):
    ...
```

Models:

- `gpt-4o-mini-tts` (default)
- `tts-1`
- `tts-1-hd`
- `gpt-4o-mini-tts-2025-12-15`

Voices:

- `alloy` (default)
- `ash`
- `ballad`
- `coral`
- `echo`
- `fable`
- `nova`
- `onyx`
- `sage`
- `shimmer`
- `verse`
- `marin`
- `cedar`

`instructions` is intended for `gpt-4o-mini-tts`. `speed` is passed through to
the API; it is reliably supported by `tts-1` and `tts-1-hd`.

### Google Gemini

```python
from speechflow import GeminiTTSEngine

engine = GeminiTTSEngine(api_key="your-gemini-api-key")

audio = await engine.get(
    "Hello.",
    model="gemini-2.5-flash-preview-tts",
    voice="Zephyr",
)
```

Models:

- `gemini-2.5-flash-preview-tts` (default)
- `gemini-2.5-pro-preview-tts`
- `gemini-2.5-flash-lite-preview-tts`

Voices:

- `Zephyr` (default)
- `Puck`
- `Charon`
- `Kore`
- `Fenrir`
- `Leda`
- `Orus`
- `Aoede`
- `Callirrhoe`
- `Autonoe`
- `Enceladus`
- `Iapetus`
- `Umbriel`
- `Algieba`
- `Despina`
- `Erinome`
- `Algenib`
- `Rasalgethi`
- `Laomedeia`
- `Achernar`
- `Alnilam`
- `Schedar`
- `Gacrux`
- `Pulcherrima`
- `Achird`
- `Zubenelgenubi`
- `Vindemiatrix`
- `Sadachbia`
- `Sadaltager`
- `Sulafat`

`stream()` uses the Gemini streaming API surface, but the current engine yields
complete TTS audio as a single chunk when Gemini returns it that way.

### FishAudio

```python
from speechflow import FishAudioTTSEngine

engine = FishAudioTTSEngine(
    api_key="your-fishaudio-api-key",
    model="s1",
    voice="optional-reference-id",
    speed=1.0,
    volume=0.0,
)

audio = await engine.get(
    "Hello.",
    speed=1.1,
    volume=0.0,
)
```

Models:

- `s1` (default)
- `speech-1.6`
- `speech-1.5`

`voice` is a FishAudio reference ID. `speed` is a speed multiplier (`1.0` =
normal) and `volume` is a volume adjustment in dB (`0.0` = no change); both are
passed to the SDK without engine-level clamping. Output is 24 kHz mono PCM.

### ElevenLabs

```python
from speechflow import ElevenLabsTTSEngine

engine = ElevenLabsTTSEngine(
    api_key="your-elevenlabs-api-key",
    model="eleven_multilingual_v2",
    voice="21m00Tcm4TlvDq8ikWAM",
    output_format="pcm_24000",
)

audio = await engine.get(
    "Hello.",
    stability=0.5,
    similarity_boost=0.75,
    speed=1.0,
)
```

Models:

- `eleven_multilingual_v2` (default)
- `eleven_v3`
- `eleven_flash_v2_5`
- `eleven_turbo_v2_5`

The default voice is `21m00Tcm4TlvDq8ikWAM`. `output_format` must be a PCM
format such as `pcm_24000`; the sample rate is parsed from that suffix.
Non-PCM formats are rejected with `ConfigurationError`.

### Kokoro

```python
from speechflow import KokoroTTSEngine

engine = KokoroTTSEngine(lang_code="a")
audio = await engine.get("Hello from Kokoro.", voice="af_heart", speed=1.0)

async for chunk in engine.stream("First sentence.\nSecond sentence."):
    ...
```

Constructor options:

- `lang_code`: one of the supported language codes below, default `a`
- `device`: Torch device string such as `cpu` or `cuda`; `None` lets Kokoro select

Languages and default voices:

| Code | Language | Default voice |
| --- | --- | --- |
| `a` | American English | `af_heart` |
| `b` | British English | `bf_emma` |
| `e` | Spanish | `ef_dora` |
| `f` | French | `ff_siwis` |
| `h` | Hindi | `hf_alpha` |
| `i` | Italian | `if_sara` |
| `j` | Japanese | `jf_alpha` |
| `p` | Brazilian Portuguese | `pf_dora` |
| `z` | Mandarin Chinese | `zf_xiaobei` |

The selected voice must start with the configured language code. Japanese
support checks for a UniDic dictionary and attempts to download it if missing.
If that fails, run:

```bash
python -m unidic download
```

### Qwen3-TTS

```python
from speechflow import Qwen3TTSEngine

engine = Qwen3TTSEngine()

audio = await engine.get(
    "Hello from Qwen3-TTS.",
    language="English",
    speaker="Vivian",
)
```

Models:

- `Qwen/Qwen3-TTS-12Hz-0.6B-Base`
- `Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice`
- `Qwen/Qwen3-TTS-12Hz-1.7B-Base`
- `Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice` (default)
- `Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign`

Languages:

- `Auto` (default)
- `Chinese`
- `English`
- `Japanese`
- `Korean`
- `German`
- `French`
- `Russian`
- `Portuguese`
- `Spanish`
- `Italian`

CustomVoice speakers:

- `Vivian` (default)
- `Serena`
- `Uncle_Fu`
- `Dylan`
- `Eric`
- `Ryan`
- `Aiden`
- `Ono_Anna`
- `Sohee`

Model variants control which arguments are valid:

- CustomVoice models use `speaker`; `voice` is accepted as a synonym when `speaker` is not provided.
- Base models require `ref_audio` and `ref_text`, or a stored profile created with `set_voice_profile()`.
- VoiceDesign models require `instruct`, a natural-language voice description.

Voice cloning with a Base model:

```python
engine = Qwen3TTSEngine(model_id="Qwen/Qwen3-TTS-12Hz-1.7B-Base")

await engine.set_voice_profile(
    ref_audio="/path/to/reference.wav",
    ref_text="Transcript of the reference audio.",
)

audio = await engine.get("Use the stored voice profile.", language="English")
await engine.clear_voice_profile()
```

`ref_audio` can be a file path, URL, base64 string, or a `(numpy.ndarray, sample_rate)` tuple. Output is 24 kHz mono PCM.

### Style-Bert-VITS2

```python
from speechflow import StyleBertTTSEngine

engine = StyleBertTTSEngine(model_name="jvnv-F1-jp")

audio = await engine.get(
    "こんにちは。",
    speaker_id=0,
    style="Happy",
    style_weight=5.0,
    speed=1.0,
    pitch=0.0,
)
```

English synthesis uses an English-compatible model and `language="EN"`:

```python
engine = StyleBertTTSEngine(model_name="rikka-botan-en", language="EN")

audio = await engine.get(
    "Hello from the exhibition.",
    speaker_id=0,
    style="Neutral",
)
```

You must provide either `model_name` or `model_path`.

Pretrained model names:

- `jvnv-F1-jp`
- `jvnv-F2-jp`
- `jvnv-M1-jp`
- `jvnv-M2-jp`
- `rikka-botan-en`

JVNV JP-Extra models are Japanese-only. Use `rikka-botan-en` or another
English-compatible Style-Bert-VITS2 model with `language="EN"`.

To use a local Style-Bert-VITS2 model, pass `model_path` pointing to a directory
containing the model files expected by the engine:

- one `.safetensors` file
- `config.json`
- `style_vectors.npy`

For example:

```python
engine = StyleBertTTSEngine(model_path="/path/to/stylebert-model", language="EN")
```

Styles:

- `Neutral` (default)
- `Happy`
- `Sad`
- `Angry`
- `Fear`
- `Surprise`
- `Disgust`

`style_weight` is clamped to `0.0` through `10.0`. `speed` must be greater than
`0.0`. `pitch` is specified in semitones. Advanced keyword arguments are
forwarded to the underlying Style-Bert-VITS2 `TTSModel.infer()` call.

Pretrained models are downloaded from Hugging Face on first use and cached under
your user cache directory. The sample rate comes from the loaded model
configuration; JVNV models are typically 44.1 kHz.

## Audio Utilities

### AudioWriter

`AudioWriter` is included in the core package and writes WAV files using the
standard-library `wave` module.

```python
from speechflow import AudioWriter

async def save_examples(audio, engine) -> None:
    writer = AudioWriter()

    await writer.save(audio, "output.wav")
    combined = await writer.save_stream(engine.stream("Save a stream."), "stream.wav")
    print(combined.duration)
```

Supported extensions:

- `.wav`
- `.wave`

Behavior:

- Converts float32 samples to int16 PCM.
- Clips samples to `[-1.0, 1.0]` before conversion.
- Creates parent directories when needed.
- Collects stream chunks before writing because WAV headers need the final data length.
- Returns the input `AudioData` for `save()` and combined `AudioData` for `save_stream()`.
- Raises `AudioProcessingError` for unsupported extensions, inconsistent stream parameters, empty streams, or write failures.

### AudioPlayer

Install the `player` extra to use `AudioPlayer`.

```python
from speechflow import AudioPlayer

async def play_examples(audio, engine) -> None:
    player = AudioPlayer()

    await player.play(audio)
    combined = await player.play_stream(engine.stream("Play this stream."))
    player.stop()
    print(combined.duration)
```

Behavior:

- Uses `sounddevice.OutputStream`.
- Starts stream playback after the first chunk arrives.
- Requires all chunks in a stream to share the same sample rate and channel count.
- Returns combined `AudioData` from `play_stream()`.
- Can be used as a sync or async context manager; leaving the context calls `stop()`.
- Raises `AudioProcessingError` for playback failures, empty streams, or inconsistent stream parameters.

### WebSocketAudioSink

Install the `ws` extra to send audio to a WebSocket `/playback` endpoint.

```bash
uv add "speechflow[openai,ws]"
```

```python
import asyncio

from speechflow import OpenAITTSEngine
from speechflow.audio import WebSocketAudioSink


async def main() -> None:
    engine = OpenAITTSEngine(api_key="your-openai-api-key")

    async with WebSocketAudioSink("ws://127.0.0.1:8765/playback") as sink:
        await sink.send_stream(engine.stream("Send this as streaming PCM."))


asyncio.run(main())
```

API:

- `await sink.connect()`
- `await sink.close()`
- `await sink.send_stream(stream)`
- `await sink.send_batch(audio)`
- `await sink.flush()`
- `async with WebSocketAudioSink(url) as sink: ...`

Wire behavior:

- `send_stream()` sends `{"type": "begin", "format": "f32le", "sample_rate": ...}`, then little-endian float32 mono PCM binary chunks, then `{"type": "end"}`.
- `send_batch()` sends one 16-bit PCM mono WAV binary message.
- `flush()` sends `{"type": "flush"}`.
- If `flush()` is called from another task while `send_stream()` is active, the active stream is cancelled, no `end` frame is sent, and `send_stream()` returns the audio sent so far.
- The sink supports mono audio only and does not resample.
- Mixed stream sample rates, mixed channel counts, concurrent streams, connection failures, and send failures raise `AudioProcessingError`.

## Errors

Catch `TTSError` to handle any SpeechFlow-domain error, or catch specific
subclasses when you want different recovery behavior.

```python
from speechflow import ConfigurationError, StreamingError, TTSError

try:
    audio = await engine.get("Hello.")
except ConfigurationError as exc:
    print(f"Configuration problem: {exc}")
except StreamingError as exc:
    print(f"Streaming failed: {exc}")
except TTSError as exc:
    print(f"TTS failed: {exc}")
```

Common mapping:

- Missing API keys and unsupported parameters raise `ConfigurationError`.
- Synthesis failures raise `TTSError`.
- Streaming failures raise `StreamingError`.
- Playback, WAV writing, and WebSocket output failures raise `AudioProcessingError`.

## Notes For Application Code

- Pass API keys directly to engine constructors. SpeechFlow does not load `.env` files or manage credentials.
- Keep async code async. In web servers, notebooks, and GUI applications with an existing event loop, use `await engine.get(...)` and `async for chunk in engine.stream(...)`.
- Use `AudioData.sample_rate` instead of assuming every engine returns the same rate.
- Use `stream()` when you want low-latency consumption, but check the engine table above: some engines yield sentence-sized chunks, and Gemini currently yields complete audio as one chunk.
- Use `AudioWriter.save_stream()` when you need a WAV file from an audio stream.
- Use `AudioPlayer.play_stream()` or `WebSocketAudioSink.send_stream()` when you want to consume chunks as they arrive.

## License

MIT
