Skip to content

Commit 82c157e

Browse files
author
Aleksandr Shliakhov
committed
Release v2.0.0: two-product client (S2S translation + Realtime TTS)
0 parents  commit 82c157e

24 files changed

Lines changed: 2919 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
python-version: ["3.10", "3.11", "3.12", "3.13"]
15+
steps:
16+
- uses: actions/checkout@v4
17+
- uses: astral-sh/setup-uv@v5
18+
with:
19+
python-version: ${{ matrix.python-version }}
20+
- run: uv sync --dev
21+
- run: uv run ruff check src tests examples
22+
- run: uv run ruff format --check src tests examples
23+
- run: uv run pytest tests -q

.github/workflows/release.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags: ["v*"]
6+
7+
jobs:
8+
build:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v4
12+
- uses: astral-sh/setup-uv@v5
13+
with:
14+
python-version: "3.12"
15+
- run: uv sync --dev
16+
- run: uv run ruff check src tests examples
17+
- run: uv run pytest tests -q
18+
- run: uv build
19+
- uses: actions/upload-artifact@v4
20+
with:
21+
name: dist
22+
path: dist/
23+
24+
publish:
25+
needs: build
26+
runs-on: ubuntu-latest
27+
environment: pypi
28+
permissions:
29+
id-token: write # required for PyPI Trusted Publishing (OIDC)
30+
steps:
31+
- uses: actions/download-artifact@v4
32+
with:
33+
name: dist
34+
path: dist/
35+
- uses: pypa/gh-action-pypi-publish@release/v1

.gitignore

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*.egg-info/
5+
.pytest_cache/
6+
.ruff_cache/
7+
8+
# Build artifacts
9+
dist/
10+
build/
11+
12+
# Virtual environments
13+
.venv/
14+
venv/
15+
16+
# Editor / OS
17+
.idea/
18+
.vscode/
19+
.DS_Store

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Palabra AI
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Makefile

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
.PHONY: install lint format test check
2+
3+
install:
4+
uv sync --dev
5+
6+
lint:
7+
uv run ruff check src tests examples
8+
9+
format:
10+
uv run ruff format src tests examples
11+
uv run ruff check --fix src tests examples
12+
13+
test:
14+
uv run pytest tests -q
15+
16+
check: lint test
17+
uv run ruff format --check src tests examples

README.md

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
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

Comments
 (0)