ZeliSpeech — Python SDK
A small, dependency-light client for ZeliSpeech —
self-hosted, low-latency streaming text-to-speech. Point it at your running
server and get audio back in a couple of lines. The SDK talks the same
authenticated /v1 API documented in the
API reference, so
the request/response shapes follow mainstream TTS-API conventions.
Install
pip install zeli-tts
Runtime dep: requests. Live playback (play/stream) shells out to ffplay
(ffmpeg), mpv, or afplay.
Quickstart
from zeli_tts import ZeliSpeech, play, save, stream
client = ZeliSpeech(
base_url="https://voice.zeligate.com",
api_key="sk-zeli-...", # optional if the box runs open
)
# 1) Stream — first audio after the first sentence, not the whole passage
audio = client.text_to_speech.stream(
voice_id="zeli-voice-1",
text="Hey there, this is Zeli.",
output_format="mp3_44100_128",
)
for chunk in audio:
... # audio bytes as they generate
# 2) Stream + play live (needs ffplay or mpv)
stream(client.text_to_speech.stream(voice_id="zeli-voice-1", text="Playing as I generate."))
# 3) One-shot — a finished clip
audio = client.text_to_speech.convert(voice_id="zeli-voice-1", text="Hello world.")
save(audio, "hello.mp3")
Client
ZeliSpeech(base_url, *, api_key=None, timeout=60.0)
base_url—http://host:8000orhttps://voice.zeligate.com; do not include a trailing/v1.api_key— sent asAuthorization: Bearer …on every request. Required only when the box setsZELI_API_KEY; a loopback/embedded box can run open.client.capabilities()/client.health()/client.is_ready()— engine info and readiness (handy while a box is warming up).
Synthesis — client.text_to_speech
stream(voice_id, text, *, model_id="zeli-turbo", voice_settings=None,
output_format="mp3_44100_128", timeout=None) -> AudioStream
convert(voice_id, text, *, ...same...) -> bytes
stream(...)returns anAudioStream— iterate it for audiobytesas they generate;.read()drains it to one blob. It exposes.output_format,.sample_rate,.voice(the resolved voice id), and.request_id.convert(...)returns the finished clip in the requestedoutput_format.output_formataccepts the full menu —mp3_*,wav_*, rawpcm_*,ulaw_8000,alaw_8000,opus_48000_*— see the output formats reference.voice_settings(stability/style/speed/…) maps onto Turbo delivery — see voice settings.
Voices — client.voices
client.voices.list() # -> [Voice(id, label, description, custom), ...]
client.voices.get("zeli-voice-1") # -> Voice (404s on unknown ids)
client.voices.add(name="My voice", file="me.wav", description="") # -> Voice
client.voices.delete("custom-abc123")
add clones a voice zero-shot from a short reference clip (~10–20 s of
clean single-speaker audio; file may be a path, a file object, or bytes).
Cloning and removal apply to the Turbo engine's custom voices, and require a
full API key — ephemeral tokens get 401 insufficient_scope here.
Ephemeral tokens — client.tokens
Never ship a real API key to a browser or mobile app: anyone can read it. The
intended flow is that your backend (which holds a full api_key) mints a
short-lived, synthesis-only token and hands it to the client, which then talks
to the API directly with that token.
tok = client.tokens.create_ephemeral(ttl_seconds=300)
# -> {"token": "zsk_temp_…", "expires_at": 1770000000, "scope": "tts"}
ttl_secondsis clamped to 30–600 (default 300).expires_atis the epoch second the token dies — enforced server-side on every request, so a leaked token is usable for minutes at most.- The returned token has scope
"tts": it can call synthesis and read endpoints (text_to_speech.stream/convert,voices.list/get) but notvoices.add/deleteortokens.create_ephemeral— those return401 insufficient_scope. - Minting requires a full key, and
501 not_supportedis returned on a server without the portal keys table configured.
Backend endpoint example (FastAPI):
from fastapi import FastAPI
from zeli_tts import ZeliSpeech
app = FastAPI()
zeli = ZeliSpeech(base_url="https://voice.zeligate.com", api_key=os.environ["ZELISPEECH_API_KEY"])
@app.post("/api/tts-token")
def tts_token():
return zeli.tokens.create_ephemeral(ttl_seconds=300) # browser uses this directly
More examples
# Delivery control (stability / style / speed map onto Turbo knobs)
clip = client.text_to_speech.convert(
"zeli-voice-1", "Same words, very different delivery!",
voice_settings={"stability": 0.2, "style": 0.9, "speed": 1.1},
)
# Raw PCM for your own audio pipeline (24 kHz mono int16, no container)
pcm = client.text_to_speech.convert("zeli-voice-1", "Raw samples.", output_format="pcm_24000")
# Telephony formats
ulaw = client.text_to_speech.convert("zeli-voice-1", "For the phone line.", output_format="ulaw_8000")
# Stream straight to a file as it generates
with open("out.mp3", "wb") as f:
for chunk in client.text_to_speech.stream("zeli-voice-1", "Written as it speaks."):
f.write(chunk)
# Clone a voice, use it, remove it
voice = client.voices.add(name="My narrator", file="reference.wav")
client.text_to_speech.convert(voice.id, "Cloned voice speaking.")
client.voices.delete(voice.id)
# Play inline in a Jupyter notebook
from IPython.display import Audio
Audio(data=bytes(client.text_to_speech.convert("zeli-voice-1", "Hello, notebook!")))
# Robust error handling
from zeli_tts import APIError, ConnectionError
try:
client.text_to_speech.convert("zeli-voice-1", "…")
except APIError as e:
if e.status == "invalid_api_key":
... # rotate/refresh the key
elif e.status_code == 503:
... # model still loading — retry shortly
except ConnectionError:
... # box unreachable / stopped
Helpers
from zeli_tts import play, save, stream, pcm_to_wav
play(audio) # play a finished clip (any output format)
stream(audio_stream) # play chunks live; returns the collected bytes
save(audio, "out.mp3") # write bytes to a file (pcm_* wrapped when .wav)
pcm_to_wav(pcm, sample_rate=24000)
Errors
All derive from ZeliSpeechError:
| Exception | When |
|---|---|
ConfigurationError |
bad base_url, or a missing dep/player |
ConnectionError |
server unreachable (refused / DNS / timeout) |
APIError |
non-2xx from an HTTP endpoint (.status_code, .status, .body) |
GenerationError |
the stream broke mid-synthesis |
Auth failures are APIErrors whose .status tells you why:
.status_code |
.status |
When |
|---|---|---|
| 401 | missing_api_key |
auth required but no key sent |
| 401 | invalid_api_key |
unknown, revoked, or expired key/token |
| 401 | insufficient_scope |
an ephemeral token hit a management route |
| 501 | not_supported |
minting on a server without the keys table |
Migrating from 0.1.x
0.2.0 moves the SDK onto the authenticated /v1 API and adopts the ZeliSpeech
naming. ZeliTTS / ZeliTTSError remain as aliases, but the synthesis calls
changed: stream(text, voice=...) → stream(voice_id, text), audio now
arrives in output_format (mp3 by default) instead of raw PCM, and the
engine-knob kwargs (exaggeration, cfg_weight, …) are replaced by
voice_settings.
Release files for zeli-tts 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| zeli_tts-0.3.1.tar.gz | 18.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| zeli_tts-0.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 34.6 kB
Release files / zeli_tts-0.3.1.tar.gz
| Download URL | zeli_tts-0.3.1.tar.gz |
|---|---|
| Size | 18.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5cda34278bfaccefafe5bf88344ffd7e750683e0d38ebb0f1b39ee28c9821a9e
|
|
BLAKE2b-256 checksum How to use checksums |
d11c5f06c6e19531df93ee975375fd29e4fa43f8175654dd380766a9e0407de6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|
Release files / zeli_tts-0.3.1-py3-none-any.whl
| Download URL | zeli_tts-0.3.1-py3-none-any.whl |
|---|---|
| Size | 16.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8e7180e8133f3db3be7f5184109247e2c560bfd49e8200135c09e36a0c414329
|
|
BLAKE2b-256 checksum How to use checksums |
58b09c6a930577cf2ff157716112d53ffc9ceb7002b240c620d78785fb14f0dc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|