Skip to main content

voixa

Python client for the Voixa AI speech API. Standard library only (no runtime dependencies); Python 3.9+.

Text to speech in ten languages (built-in voices, voice cloning from a ten-second sample, voice design from a written description, podcasts up to 100 000 characters, live streaming) and speech to text: audio or video in, transcript with timestamps, SRT and VTT out.

pip install voixa

Quick start

from voixa import Voixa

vx = Voixa()  # reads VOIXA_API_KEY; create a key in Studio -> API keys
# or: Voixa(api_key="...", base_url="https://api.voixa.vovix.io/v1", timeout=30)

voices = vx.voices(language="en")["voices"]
clip = vx.say(
    voice_id=voices[0]["voiceId"],
    text="The lighthouse keeper switched the lamp on at dusk.",
    project="episode-12",  # optional: group clips by content
)
print(clip["url"], clip["durationSec"])  # signed WAV URL, valid for 1 hour

say() returns when the clip is ready. Use speak(..., wait=False) + wait_for() to manage the wait yourself: the API answers in under a second and you poll.

Every method returns what the API returns: plain dicts with the API's camelCase keys (clipId, durationSec, ...). The package ships TypedDict definitions for all of them (voixa.Clip, voixa.Voice, voixa.Transcript, ...) so editors and type checkers know the fields. Request arguments are snake_case keywords.

timeout (default 30 s) bounds each HTTP request. The wait_* helpers and the methods that wait (say, make_podcast, create_voice, save_design, transcribe_file, ...) take their own poll_interval and timeout, in seconds.

Vietnamese: fast or normal

Vietnamese has two synthesis paths with the same voices. fast is the default and costs less; normal is the original path. Pass mode to choose (other languages ignore it); clip["mode"] tells you which path produced the clip.

clip = vx.say(voice_id="vi-truc-ly", text="Xin chào!", mode="normal")

A newly cloned Vietnamese voice is prepared for fast in the background; if a request arrives before that finishes, it is produced with normal (and clip["mode"] says so).

Streaming (Vietnamese)

Hear the first words in about two seconds: audio arrives clause by clause while the rest is voiced. stream() returns a SpeechStream, an iterator of bytes chunks (16-bit little-endian mono PCM, or WAV with format="wav"). The connection closes when the iteration ends; use with to close it early.

# Play it as it arrives
for chunk in vx.stream(voice_id="vi-truc-ly", text="Xin chào, Voixa có thể giúp gì?", sample_rate=16000):
    player.write(chunk)

# Or write it to a file
with vx.stream(voice_id="vi-truc-ly", text="Xin chào!", format="wav") as s:
    print(s.sample_rate, s.tokens)
    s.save("hello.wav")  # or: open("hello.wav", "wb").write(s.read())

sample_rate: 48000, 24000 (default), 16000 or 8000; format: pcm or wav. Costs 1.5 tokens per character, charged when the stream opens and refunded if generation fails.

Tokens

Every request is priced in tokens: 1 token = 1 Vietnamese character on fast. Free tokens refill daily; top-ups carry over.

from voixa import estimate_tokens

account = vx.account()["account"]
account["tokens"]  # {available, freeToday, dailyFree, balance, usedToday, refillsAt, rates, unlimited}
rates = account["tokens"]["rates"]
estimate_tokens(rates, language="vi", chars=1200)   # -> 1200
estimate_tokens(rates, design_options=3)             # a voice design with three options
estimate_tokens(rates, audio_seconds=600)            # ten minutes of transcription
page = vx.usage(limit=20)                            # every spend, refund and top-up

A request you cannot afford raises VoixaError with status 402 and code INSUFFICIENT_TOKENS (plus needed, available, refills_at). Failed clips and voices are refunded.

Voices and samples

languages = vx.languages()["languages"]   # codes, labels, limits, clone-recording guides
samples = vx.samples(language="fr")        # built-in voices with public sample URLs
voice = vx.get_voice("en-alba")["voice"]

Voice design: a voice from a description

No recording needed. Describe the voice, listen to up to three options, keep the one you like. A saved design is an ordinary voice of your account: use it with say, make_podcast and stream. Characters for dubbing, narrators, ads: one voice per role.

design = vx.design_voice(
    description="An old man in his seventies, hoarse and deep, speaking slowly",
    language="vi",  # any language `languages()` marks `designable`
    count=3,        # 1-3 options, `rates["design"]` tokens each
)["design"]
done = vx.wait_for_design(design["designId"])  # about a minute per option
for c in done["candidates"]:
    print(c["index"], c["status"], c.get("url"))  # listen, then pick

voice = vx.save_design(done["designId"], candidate=1, name="Grandpa Ba", gender="male")
vx.say(voice_id=voice["voiceId"], text="Ngày ấy, ông vẫn nhớ con đường làng.")

# Or in one call, keeping the first option:
narrator = vx.create_voice_from_description(
    description="A calm female narrator, warm and clear", language="en", name="Narrator",
)

vx.list_designs(limit=10)          # your designs, newest first
vx.get_design(design["designId"])  # one design with its options
vx.delete_design(design["designId"])  # saved voices are kept

Options are drafts kept for 23 hours (expiresAt). Saving one costs rates["clone"] tokens.

Voice cloning and sharing

voice = vx.create_voice(
    name="Studio narrator",
    language="en",
    gender="female",
    audio="reference.wav",  # 3-10 s of clean speech, WAV or MP3, max 10 MB
    # audio accepts bytes, a path, or a binary file object: open("ref.mp3", "rb")
    # format="mp3",          # wav by default
    # ref_text="...",        # what the speaker says; helps Chinese, Japanese, Korean
)
vx.say(voice_id=voice["voiceId"], text="Hello from my own voice.")

vx.update_voice(voice["voiceId"], shared=True)       # let every Voixa account use it
vx.update_voice(voice["voiceId"], name="Narrator", description=None)  # None clears the description
vx.delete_voice(voice["voiceId"])

create_voice uploads the recording, registers the voice and waits for its sample (check voice["status"]).

Voice cloning is available for Vietnamese, English, French, German, Italian, Spanish and Portuguese. Chinese, Japanese and Korean use built-in voices only; languages() reports cloneable per language and create_voice rejects those languages with a 400.

Podcasts

Long scripts, up to 100 000 characters, become one episode: Voixa splits the text into sentences, produces the parts in parallel and joins them into a single WAV. Blank lines become short pauses.

episode = vx.make_podcast(voice_id="vi-truc-ly", title="Episode 12", text=script, project="my-show")
episode["url"]          # one WAV, signed for an hour
episode["durationSec"]  # e.g. 4800 for an 80-minute episode

# or without waiting:
clip = vx.create_podcast(voice_id="en-alba", text=script)["clip"]
clip["parts"], clip.get("partsDone")  # progress while status is "processing"

Speech to text

Audio (or video: only the audio track is read) becomes a transcript with timed segments, SRT and VTT. Up to 5 GB and eight hours per file.

t = vx.transcribe_file(
    audio="interview.mp3",  # bytes, a path or a binary file object; files are streamed from disk
    title="Interview with the lighthouse keeper",
    # language="vi",        # optional: Voixa detects it well on its own
    # task="translate",     # recognise any language, return English
    # words=True,           # per-word timestamps
    # prompt="Voixa, Vovix",  # spelling hints for names and jargon
)
print(t["language"], t["durationSec"], t["text"])
print(t["segments"][0])  # {"id", "start", "end", "text"}
print(t["srtUrl"])       # signed SRT/VTT/TXT/JSON URLs, valid for 1 hour

The format is taken from format=, else from the file name (file_name=, or the name of the path or file you pass), else MP3.

Transcription runs on a machine that starts on demand, so it is never synchronous: transcribe() returns status: "processing" as soon as the upload finishes and you poll, while transcribe_file() does the polling for you (two to three minutes before recognition starts on a cold queue, then roughly one minute of work per five minutes of audio).

transcript = vx.transcribe(audio="talk.m4a")["transcript"]
done = vx.wait_for_transcript(transcript["transcriptId"])
# progress while processing: doneSec / durationSec, jobStatus

items = vx.list_transcripts(project="episode-12")["items"]   # no text in lists
full = vx.get_transcript(items[0]["transcriptId"])            # text + segments + URLs
vx.delete_transcripts([transcript["transcriptId"]])           # also deletes the upload

Languages you may pin, the accepted formats and the limits come from stt_languages(). Transcription is charged per second of audio once it finishes.

Library

projects = vx.projects()["projects"]                  # groups with clip counts
page = vx.list_clips(project="episode-12")            # newest first
from_api = vx.list_clips(source="api")                # clips made with an API key (vs. "studio")
podcasts = vx.list_clips(kind="podcast", limit=50)
more = vx.list_clips(project="episode-12", cursor=page.get("cursor"))
vx.delete_clips([c["clipId"] for c in page["items"]])

share = vx.share_clip(clip["clipId"])   # {"shareUrl": "https://voixa.vovix.io/s/...", "shared": True}
vx.unshare_clip(clip["clipId"])         # the link stops working
vx.get_clip(clip["clipId"])
vx.wait_for(clip["clipId"], timeout=300)

Errors and limits

Every API failure raises VoixaError with:

  • status: HTTP status code
  • message: human-readable, safe to show (also str(err))
  • code: NOT_APPROVED, INSUFFICIENT_TOKENS, CLONE_LIMIT, VOICE_NOT_READY, INVALID, AUDIO_TOO_LARGE, NO_AUDIO (QUOTA and AUDIO_QUOTA are legacy); the SDK itself sets TIMEOUT, SYNTH_FAILED, DESIGN_FAILED, TRANSCRIBE_FAILED
  • retry_after: seconds, from the Retry-After header
  • needed, available, refills_at (insufficient tokens); resets_at, used, limit (legacy quotas)
  • body: the full JSON error body
from voixa import VoixaError

try:
    vx.say(voice_id="vi-truc-ly", text=long_text)
except VoixaError as e:
    if e.code == "INSUFFICIENT_TOKENS":
        print(f"Need {e.needed}, have {e.available}; free tokens refill at {e.refills_at}")
    elif e.code == "TIMEOUT":
        ...  # still processing: poll later with vx.wait_for(...)
    else:
        raise

Requests the SDK can reject without calling the API (empty or too long text, a design description under three characters, audio over 5 GB) raise the same VoixaError with status 400. Network failures are not wrapped: they raise the standard library's OSError subclasses (urllib.error.URLError, socket timeouts).

  • One speak call takes up to 5 000 characters (fewer for Chinese, Japanese and Korean; see languages()). Line breaks and repeated spaces collapse to one space before counting, on your side and on the server.
  • One podcast takes up to 100 000 characters.
  • One transcription takes up to 5 GB and eight hours of audio.
  • API keys are rate-limited to 1 request/second and 1 000 calls/day; new keys activate after two to three minutes.
  • Speech, streaming, transcription, clones and designs are paid in tokens (see Tokens above); read the wallet with account().

Method reference

Group Methods
Account account(), usage(limit, cursor), languages(), samples(language)
Voices voices(language), get_voice(id), create_voice(...), update_voice(id, ...), delete_voice(id), wait_for_voice(id)
Voice design design_voice(...), get_design(id), list_designs(limit, cursor), wait_for_design(id), save_design(id, ...), delete_design(id), create_voice_from_description(...)
Speech speak(...), say(...), stream(...)
Podcasts create_podcast(...), make_podcast(...)
Library get_clip(id), list_clips(...), projects(), delete_clips(ids), share_clip(id), unshare_clip(id), wait_for(id)
Speech to text stt_languages(), transcribe(...), transcribe_file(...), get_transcript(id), list_transcripts(...), delete_transcripts(ids), wait_for_transcript(id)
Helpers estimate_tokens(rates, ...), constants MAX_SPEAK_CHARS, MAX_PODCAST_CHARS, MAX_AUDIO_BYTES, MAX_AUDIO_MINUTES, DEFAULT_BASE_URL

A signed-in user of your own web app can call the API with their ID token instead of a key: Voixa(id_token=...).

Development

python3 -m unittest discover -s tests   # offline, against a local fake API
VOIXA_API_KEY=... python3 scripts/e2e.py  # every method against the live API

scripts/e2e.py walks every public method against a live API with your key: account, usage, languages, samples, voices, speak/say with a project, clip listing and paging, projects, share links, cloning from a system sample, renaming and sharing the clone, a podcast, streaming, a voice design saved as a voice, transcribing a generated clip back to text, error paths, then deletes what it created. VOIXA_API_URL points it at another deployment. Skip slow or costly parts with VOIXA_E2E_SKIP_STT=1, VOIXA_E2E_SKIP_DESIGN=1, VOIXA_E2E_SKIP_STREAM=1. Exit code 1 if any step fails; cleanup still runs.

Metadata

Release files for voixa 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for voixa 0.1.0
File Size Uploaded
voixa-0.1.0.tar.gz 31.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for voixa 0.1.0
File Interpreter ABI Platform
voixa-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 57.5 kB

Release files / voixa-0.1.0.tar.gz

Download URL voixa-0.1.0.tar.gz
Size 31.3 kB
Tags Source
SHA-256 checksum
How to use checksums
5e02b8852d0a63f3f5e59b2fd5b0c8e3fce3c61490db0b92108b50ac2941bf98
BLAKE2b-256 checksum
How to use checksums
84489172659993230c4b7369db9e5960d96825792c899508fbcbb84fdded678d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.5

Release files / voixa-0.1.0-py3-none-any.whl

Download URL voixa-0.1.0-py3-none-any.whl
Size 26.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e867f4d8ca6c1c14e9f3c9ed0d437873022e332b88b70c89c34cda699561e38f
BLAKE2b-256 checksum
How to use checksums
8a3901f93d2868d742e82fa7b725c2c5829ee702d0d304eafa27b2da25ab9460
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.5

Release history Release notifications | RSS feed

0.2.0

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page