|
| 1 | +# Palabra AI Python Client |
| 2 | + |
| 3 | +Simple Python client for [Palabra AI](https://palabra.ai) **real-time** streaming APIs: speech-to-speech translation and low-latency text-to-speech. |
| 4 | + |
| 5 | +```bash |
| 6 | +uv add palabra-ai # or: pip install palabra-ai |
| 7 | +``` |
| 8 | + |
| 9 | +Full API documentation: [docs.palabra.ai](https://docs.palabra.ai). |
| 10 | + |
| 11 | +Palabra has two separate streaming products, and the client mirrors that at the top level: |
| 12 | + |
| 13 | +| Product | Entry point | What it is | |
| 14 | +|---|---|---| |
| 15 | +| [**Speech-to-Speech Translation API**](#speech-to-speech-translation-api) | `palabra.translation(...)` | full pipeline: ASR -> translation -> TTS | |
| 16 | +| [**Realtime TTS API**](#realtime-tts-api) | `palabra.tts(...)` | synthesis only: stream text in (e.g. from an LLM), audio out | |
| 17 | + |
| 18 | +Authentication, connection options, [errors](#errors) and [reconnection](#reconnection) are shared between the two. |
| 19 | + |
| 20 | +## Authentication |
| 21 | + |
| 22 | +Credentials come from the constructor or from the environment: |
| 23 | + |
| 24 | +```bash |
| 25 | +export PALABRA_CLIENT_ID=... |
| 26 | +export PALABRA_CLIENT_SECRET=... |
| 27 | +``` |
| 28 | + |
| 29 | +```python |
| 30 | +from palabra_ai import Palabra |
| 31 | + |
| 32 | +palabra = Palabra() # reads the env vars |
| 33 | +palabra = Palabra(client_id="...", client_secret="...") # or explicit |
| 34 | +``` |
| 35 | + |
| 36 | +Credentials are only used for the REST API (session creation/deletion). They are **not required** for the direct-connection mode below. |
| 37 | + |
| 38 | +## Connection options |
| 39 | + |
| 40 | +Both `translation()` and `tts()` accept the same three connection modes: |
| 41 | + |
| 42 | +1. **Default** — a session is created via REST on `async with` and deleted on exit. Nothing to manage. |
| 43 | +2. **`session=`** — manually create `Session` with `await palabra.create_session()`; its lifecycle is yours (the client won't delete it). |
| 44 | +3. **`ws_url=` + `token=`** — debug option: connect directly with a direct `ws_url` and already issued `publisher` token. For TTS the endpoint URL is the `ws_tts_url` field of the session. Here is an example: |
| 45 | + |
| 46 | +```python |
| 47 | +palabra = Palabra() # credentials not required in this mode |
| 48 | +async with palabra.translation( |
| 49 | + source="en", |
| 50 | + targets=["es"], |
| 51 | + ws_url=ws_url, |
| 52 | + token=publisher_token |
| 53 | +) as session: |
| 54 | + ... |
| 55 | +``` |
| 56 | + |
| 57 | +--- |
| 58 | + |
| 59 | +# Speech-to-Speech Translation API |
| 60 | + |
| 61 | +You continuously push audio chunks; Palabra streams back transcripts, translations, and synthesized speech. |
| 62 | + |
| 63 | +## Quick start |
| 64 | + |
| 65 | +Audio comes from *your* source — a microphone, a VoIP call leg, a telephony bridge — anything that hands you PCM chunks. Push them into the session and consume events: |
| 66 | + |
| 67 | +```python |
| 68 | +import asyncio |
| 69 | +from palabra_ai import Palabra, Transcript, Audio |
| 70 | + |
| 71 | +async def main(): |
| 72 | + palabra = Palabra() |
| 73 | + |
| 74 | + async with palabra.translation(source="en", targets=["es"]) as session: |
| 75 | + |
| 76 | + async def feed(): |
| 77 | + # any audio source |
| 78 | + # chunks: PCM s16le, 24 kHz, mono, ~320 ms each, real-time paced |
| 79 | + while chunk := await my_audio_buffer.get(): |
| 80 | + await session.send_audio(chunk) |
| 81 | + await session.end(eos_timeout=4) # let the tail finish, then the server closes |
| 82 | + |
| 83 | + feeder = asyncio.create_task(feed()) |
| 84 | + |
| 85 | + async for event in session: |
| 86 | + match event: |
| 87 | + case Transcript(): |
| 88 | + print(event) # "~ [lang] partial" / "[lang] final" |
| 89 | + case Audio(): |
| 90 | + play(event.pcm) # s16le, 24 kHz, mono |
| 91 | + |
| 92 | + await feeder |
| 93 | + |
| 94 | +asyncio.run(main()) |
| 95 | +``` |
| 96 | + |
| 97 | +`async with palabra.translation(...)` does everything for you: creates a session via REST, connects the WebSocket, sends translation task, waits until the pipeline actually confirms the task, and cleans up on exit. |
| 98 | + |
| 99 | +Two rules for the input stream: |
| 100 | + |
| 101 | +1. Chunks must match the format declared in the task (default: PCM s16le, 24 kHz, mono; ~320 ms per chunk is optimal). |
| 102 | +2. Push at **real-time rate** — faster/slower pacing triggers `ServerWarning` (`AUDIO_STREAM_TOO_FAST/TOO_SLOW/STALLED`) and degrades quality. If your source is a live device or call, pacing comes for free. |
| 103 | + |
| 104 | +## WebRTC (browser / client-side apps) |
| 105 | + |
| 106 | +This client uses WebSocket transport, which is the recommended option for **server-side** applications. For browser and mobile apps Palabra recommends the **WebRTC transport with a JavaScript client**: follow the [WebRTC Quick Start](https://docs.palabra.ai/docs/quick-start/webrtc), or start from the official [TypeScript example](https://github.com/PalabraAI/typescript-speech-to-speech-translation-example). WebRTC handles microphone capture, pacing, jitter, etc. in the browser natively. |
| 107 | + |
| 108 | +## Events |
| 109 | + |
| 110 | +Iterating the session yields typed events: |
| 111 | + |
| 112 | +| Event | Fields | Meaning | |
| 113 | +|---|---|---------------------------------------------------------| |
| 114 | +| `Transcript` | `text, language, id, is_eos, is_translation` | partial/validated transcription & translation | |
| 115 | +| `Audio` | `pcm, language, last_chunk, id` | TTS chunk (PCM s16le 24 kHz mono) | |
| 116 | +| `TaskInfo` | `status, task` | response to `get_task` | |
| 117 | +| `StreamEnd` | — | end-of-stream confirmation after `end(eos_timeout=...)` | |
| 118 | +| `ServerError` | `code, desc` | server-side error | |
| 119 | +| `ServerWarning` | `code, message` | `AUDIO_STREAM_TOO_FAST / TOO_SLOW / STALLED` | |
| 120 | +| `Raw` | `type, data` | anything else | |
| 121 | + |
| 122 | +## Session control |
| 123 | + |
| 124 | +```python |
| 125 | +await session.send_audio(chunk) # one raw chunk (pace it yourself) |
| 126 | + |
| 127 | +await session.speak("Hola!", "es") # speak text into the stream (note: you must have this language as one of the target languages) |
| 128 | +await session.speak("Hi all!", "en", translate=True) # translate to all targets first |
| 129 | + |
| 130 | +await session.flush() # drop the current transcription and audio (interruption) |
| 131 | +await session.pause(); await session.resume() # pause and resume your session (stops billing) |
| 132 | +await session.set_task(new_task) # change settings on the fly |
| 133 | +await session.end(eos_timeout=4) # graceful finish: waits for the tail, emits StreamEnd |
| 134 | +``` |
| 135 | + |
| 136 | +`session.speak(text, lang)` (the `tts_task` command) speaks **through the translation pipeline** and is unrelated to the standalone [Realtime TTS API](#realtime-tts-api). |
| 137 | + |
| 138 | +## Settings |
| 139 | + |
| 140 | +Common options are keyword arguments of `translation(...)`; anything beyond that — build the task dict yourself: |
| 141 | + |
| 142 | +```python |
| 143 | +from palabra_ai import build_task, Palabra |
| 144 | + |
| 145 | +# common options inline |
| 146 | +session = Palabra().translation( |
| 147 | + source="auto", |
| 148 | + targets=["es", "fr"], |
| 149 | + translate_partials=True, |
| 150 | + silence_threshold=0.8 |
| 151 | +) |
| 152 | + |
| 153 | +# or full control, including per-target overrides and any server option |
| 154 | +task = build_task( |
| 155 | + "en", |
| 156 | + {"es": {"speech_generation": {"voice_id": "default_high"}}, "fr": {}}, |
| 157 | + input_sample_rate=48000, |
| 158 | +) |
| 159 | +task["pipeline"]["transcription"]["silence_threshold"] = 0.75 |
| 160 | + |
| 161 | +async with Palabra().translation(task=task) as session: |
| 162 | + ... |
| 163 | +``` |
| 164 | + |
| 165 | +The client does **not validate settings** — invalid options are rejected by the server (`TaskError` is raised on `async with`, with the server's reason). The full list of options, their constraints, and tuning advice live in the docs: see [Recommended Settings](https://docs.palabra.ai/docs/streaming_api/recommended_settings). |
| 166 | + |
| 167 | +## Offline files (utility) |
| 168 | + |
| 169 | +For testing and batch jobs there are file helpers — but keep in mind this is a **real-time** service: the input is paced to real time, so translating a file takes roughly as long as the audio itself. For UX experiments and pipeline debugging it's convenient; for bulk offline processing it's the wrong tool. |
| 170 | + |
| 171 | +```python |
| 172 | +palabra.translate_file( |
| 173 | + "speech_en.wav", |
| 174 | + source="en", |
| 175 | + targets="es", |
| 176 | + output="speech_es.wav", |
| 177 | + on_transcript=print |
| 178 | +) |
| 179 | +# mp3/ogg/resampling need: uv add "palabra-ai[audio]" |
| 180 | +``` |
| 181 | + |
| 182 | +Related helpers: `session.send_file(path)`, `session.send_pcm(pcm)` (chunking + real-time pacing built in), `load_pcm` / `read_wav` / `write_wav`. |
| 183 | + |
| 184 | +--- |
| 185 | + |
| 186 | +# Realtime TTS API |
| 187 | + |
| 188 | +Standalone synthesis, no translation pipeline. Designed for incremental text (LLM token streams): send pieces as they come, audio chunks come back with minimal latency. |
| 189 | + |
| 190 | +Two methods. `send_text()` -- incremental streaming, e.g. straight from an LLM token stream; mark the end of each sentence with `eos=True` and consume `TtsChunk` events as they arrive: |
| 191 | + |
| 192 | +```python |
| 193 | +async with palabra.tts(language="en", voice_id="default_low") as tts: |
| 194 | + await tts.send_text("The sun was setting over the mountains,") |
| 195 | + await tts.send_text(" casting long golden shadows.", eos=True) |
| 196 | + |
| 197 | + async for chunk in tts: # TtsChunk: audio, generation_id, last_chunk, audio_len |
| 198 | + play(chunk.audio) |
| 199 | + if chunk.last_chunk: |
| 200 | + break |
| 201 | + |
| 202 | + await tts.cancel() # stop current synthesis, session stays open |
| 203 | +``` |
| 204 | + |
| 205 | +Each `send_text()` message is limited to 256 characters (the server limit); longer text raises `ValueError` -- splitting is up to you. |
| 206 | + |
| 207 | +`synthesize()` -- one sentence in, audio bytes out: |
| 208 | + |
| 209 | +```python |
| 210 | +async with palabra.tts(language="en", voice_id="default_low") as tts: |
| 211 | + pcm = await tts.synthesize("Curious minds think alike.") # bytes (pcm s16le by default) |
| 212 | +``` |
| 213 | + |
| 214 | +## Options & limits |
| 215 | + |
| 216 | +All `palabra.tts(...)` options (languages, voices, `speed`, output formats, sample rates), rate limits and constraints are described in the [Realtime TTS API docs](https://docs.palabra.ai/docs/streaming_api/realtime_tts). Per-message voice overrides can be passed as keyword arguments of `send_text()`/`synthesize()`. |
| 217 | + |
| 218 | +[Connection options](#connection-options) are the same as in `translation()`, including `ws_url=`/`token=` (the TTS endpoint is the `ws_tts_url` field of the session). |
| 219 | + |
| 220 | +--- |
| 221 | + |
| 222 | +# Common reference |
| 223 | + |
| 224 | +## Errors |
| 225 | + |
| 226 | +Shared by both APIs: |
| 227 | + |
| 228 | +- `AuthError` — missing/invalid credentials. |
| 229 | +- `SessionError` — REST/WebSocket connection problems (including a crashed receive loop — the original exception is attached as `__cause__`). |
| 230 | +- `NotReadyError` — the pipeline didn't confirm `set_task` in time (translation only). |
| 231 | +- `TaskError` — the server rejected `set_task` (raised immediately on `async with`, with the server's `code`/`desc`), or raised by `session.raise_on_error(event)` for server `error` messages; by default in-stream errors are delivered as `ServerError` events so a long-running stream survives recoverable errors. |
| 232 | + |
| 233 | +REST session creation retries transient failures (network errors, 5xx) a few times with backoff; 4xx fails immediately. |
| 234 | + |
| 235 | +## Reconnection |
| 236 | + |
| 237 | +There is **no automatic WebSocket reconnect**, by design: a session is tied to one connection and to server-side pipeline state, so a transparent resume would silently lose the audio in flight and the transcription context. When the connection drops, iteration simply ends (or `SessionError` is raised if the receive loop crashed). |
| 238 | + |
| 239 | +If your application needs resilience, build the retry loop on top — you control what state to restore: |
| 240 | + |
| 241 | +```python |
| 242 | +while True: |
| 243 | + try: |
| 244 | + async with palabra.translation(source="en", targets=["es"]) as session: |
| 245 | + ... # feed audio, consume events |
| 246 | + break # finished normally |
| 247 | + except (SessionError, NotReadyError): |
| 248 | + await asyncio.sleep(1) # reconnect with your own backoff policy |
| 249 | +``` |
| 250 | + |
| 251 | +## Examples |
| 252 | + |
| 253 | +| File | What it shows | |
| 254 | +|---|---| |
| 255 | +| [`examples/streaming.py`](examples/streaming.py) | feeding chunks + async event loop | |
| 256 | +| [`examples/mic_to_speakers.py`](examples/mic_to_speakers.py) | live microphone translation (`uv add "palabra-ai[devices]"`) | |
| 257 | +| [`examples/realtime_tts.py`](examples/realtime_tts.py) | standalone Realtime TTS API | |
| 258 | +| [`examples/multi_language.py`](examples/multi_language.py) | several targets, per-target voices | |
| 259 | +| [`examples/file_to_file.py`](examples/file_to_file.py) | offline file translation (see the caveat above) | |
| 260 | + |
| 261 | +## Development |
| 262 | + |
| 263 | +```bash |
| 264 | +uv sync --dev # editable install + pytest/ruff |
| 265 | +make check # ruff check + tests + format check |
| 266 | +``` |
| 267 | + |
| 268 | +## Migrating from 1.x |
| 269 | + |
| 270 | +| 1.x | 2.x | |
| 271 | +|---|---| |
| 272 | +| `PalabraAI()` + `Config(SourceLang(EN, reader), [TargetLang(ES, writer)])` + `palabra.run(cfg)` | `Palabra().translation(source="en", targets="es")` + `send_audio` / events | |
| 273 | +| `FileReader` / `FileWriter` / `BufferReader` / adapters | plain `bytes`: feed any source via `send_audio`; file utilities for tests | |
| 274 | +| `DeviceManager` | use `sounddevice` directly (see `mic_to_speakers.py`) | |
| 275 | +| `on_transcription=` callbacks | `async for event in session` | |
| 276 | +| WebRTC transport | not included; see the WebRTC note above | |
0 commit comments