addisai (Python)
The official Addis AI SDK for Python — voice (text‑to‑speech), chat/LLM with system prompts, personas and function calling, speech‑to‑text, and translation, for Amharic (am), Afaan Oromo (om), Tigrinya (ti), Swahili (sw), Hausa (ha), Sidama (sid) and Wolaytta (wal).
Mirrors the Node.js SDK surface, Pythonic and snake_case.
pip install addisai
Requires Python 3.8+.
Quickstart
from addisai import AddisAI
addis = AddisAI() # reads ADDIS_API_KEY from the environment
# Text-to-speech
clip = addis.voice.generate(voice_id="am-hamen", text="ሰላም ለዓለም።", language="am")
clip.to_file("welcome.mp3")
# Chat
res = addis.chat.completions.create(
language="am",
system="Answer in concise bullet points.",
messages=[{"role": "user", "content": "ስለ አዲስ አበባ ንገረኝ"}],
)
print(res["choices"][0]["message"]["content"])
Configuration
addis = AddisAI(
api_key="...", # or set ADDIS_API_KEY
timeout=60.0, # seconds (voice.generate raises this to >= 95s)
max_retries=2, # backoff on 408/409/425/429/5xx
)
The key is read from api_key= or ADDIS_API_KEY, never logged, and redacted in repr(). The SDK only talks to the Addis AI API (https://api.addisassistant.com) and refuses raw backend hosts.
Languages
| Language | Code | Voices (default first) | Streaming and real time | Scribe (speech-to-text) |
|---|---|---|---|---|
| Amharic | am |
am-hamen and 18 more (voices.list(language="am")) |
yes | yes, with speakers |
| Afaan Oromo | om |
om-bikila, om-ayyaantuu, om-leensaa, om-hawi, om-diriba, om-leta, om-ebba, om-chala, om-obse |
yes | yes, with speakers |
| Tigrinya | ti |
ti-berhane, ti-senait, ti-haben, ti-rahel, ti-filimon, ti-danait, ti-yonas |
yes | yes, with speakers |
| Swahili | sw |
sw-rashid, sw-neema, sw-hassan, sw-bakari, sw-amina, sw-juma, sw-zainab |
yes | yes, no speakers |
| Hausa | ha |
ha-mustapha, ha-sani, ha-usman, ha-musa |
yes | yes, with speakers |
| Sidama | sid |
sid-tigist, sid-desta, sid-bereket, sid-selam, sid-alemu |
yes | yes, with speakers |
| Wolaytta | wal |
wal-tona, wal-almaz, wal-amado, wal-gobe, wal-hiwot |
yes | yes, with speakers |
voice.generate, voice.estimate, batches, voices.list, voice.stream and realtime take all seven languages. A voice ID is <language>-<name> and must match the request's
language (sid-tigist with language="sid"); anything else is rejected locally before any request or socket.
addisai.VOICES and addisai.DEFAULT_VOICES mirror the public catalog for offline checks; voices.list is the
source of truth and also returns each voice's name and preview.
from addisai import DEFAULT_VOICES
samples = {
"sw": "Habari, karibu sana. Leo ni siku nzuri ya kujifunza kitu kipya pamoja.",
"ha": "Barka da zuwa. Yau rana ce mai kyau don koyon sabon abu tare.",
"sid": "Kunino babbaxxinno sidaamu budu tima, adonna shaanu babbaxxinno sidaamu budi sagale leeltawo yaate.",
"wal": "Giyaa giddon daro asay shammes, baassi koshshiyabaa bayzzes.",
}
for language, text in samples.items():
addis.voice.generate(voice_id=DEFAULT_VOICES[language], text=text, language=language).to_file(f"{language}.mp3")
Chat and translation take am, om and ti (see below); Scribe takes all seven.
Voice (text‑to‑speech)
clip = addis.voice.generate(
voice_id="am-hamen",
text="ሰላም።",
language="am",
output_format="mp3_44100", # mp3_44100 | wav_44100 | pcm_16000
voice_settings={"speed": 50, "stability": 50, "similarity": 50, "style": 0},
)
clip.audio_url
clip.usage # {"credits_used": ..., "currency": "ETB", ...}
data = clip.content() # bytes
clip.to_file("out.mp3")
Idempotency keys are generated automatically and reused across retries so a retry is never double‑billed. Pass client_request_id= for full control.
addis.voices.list(language="am", gender="female")
addis.voice.estimate(voice_id="am-hamen", text="ሰላም", language="am")
addis.voice.usage()
for clip in addis.voice.clips.list(language="am"): # auto-paginates
print(clip.id)
addis.voice.clips.delete("clip_123")
Real-time voice
from uuid import uuid4
# Select a matching pair from the Languages table.
voice_id, language = "ti-berhane", "ti"
text = "ሰላም፣ እንቋዕ ናብ ኣዲስ ኤኣይ ብደሓን መጻእኩም።"
# HTTP streaming: no extra dependency needed.
audio = addis.voice.stream(
voice_id=voice_id, language=language, text=text,
client_request_id=str(uuid4()),
)
audio.to_file("speech.mp3")
print(audio.metadata["usage"])
# Persistent sockets: pip install "addisai[realtime]"
with addis.realtime.connect(voice_id=voice_id, language=language) as voice:
with open("speech.mp3", "wb") as output:
for chunk in voice.speak(text, str(uuid4())):
output.write(chunk)
print(voice.last_completion["usage"])
realtime.create_session() creates a scoped, one-use ticket valid for 60 seconds;
connect_realtime(ticket) opens its socket without a developer key. Keep keys on
your application server. Sessions last up to 10 minutes, allow one utterance at
a time, and carry a cumulative text budget of up to 5,000 characters. Cancellation
mutes delivery; an already-started synthesis completes and is billed.
Use append() and commit() to buffer a complete generated utterance before
synthesis. For early WAV pieces, select audio_format="wav_mp3", iterate raw
audio.delta events and use decode_realtime_audio(event); play each piece
according to its format. speak() yields concatenatable MP3 and requires an
mp3 session. Use any matching voice and language pair from the Languages table.
HTTP streams support MP3 only. Keep request IDs stable when recovering a failed
request in a new session to avoid a second charge; use a fresh ID for each new
utterance.
See examples/realtime.py.
Run the same example with any of the documented languages:
ADDIS_VOICE_LANGUAGE=am ADDIS_VOICE_ID=am-hamen python examples/realtime.py "ሰላም፣ እንኳን ወደ አዲስ ኤአይ በደህና መጡ።"
ADDIS_VOICE_LANGUAGE=om ADDIS_VOICE_ID=om-bikila python examples/realtime.py "Nagaa, gara Addis AI baga nagaan dhuftan."
ADDIS_VOICE_LANGUAGE=ti ADDIS_VOICE_ID=ti-berhane python examples/realtime.py "ሰላም፣ እንቋዕ ናብ ኣዲስ ኤኣይ ብደሓን መጻእኩም።"
ADDIS_VOICE_LANGUAGE=sw ADDIS_VOICE_ID=sw-rashid python examples/realtime.py "Habari, karibu sana. Leo ni siku nzuri ya kujifunza kitu kipya pamoja."
ADDIS_VOICE_LANGUAGE=ha ADDIS_VOICE_ID=ha-mustapha python examples/realtime.py "Barka da zuwa. Yau rana ce mai kyau don koyon sabon abu tare."
ADDIS_VOICE_LANGUAGE=sid ADDIS_VOICE_ID=sid-tigist python examples/realtime.py "Kunino babbaxxinno sidaamu budu tima, adonna shaanu babbaxxinno sidaamu budi sagale leeltawo yaate."
ADDIS_VOICE_LANGUAGE=wal ADDIS_VOICE_ID=wal-tona python examples/realtime.py "Giyaa giddon daro asay shammes, baassi koshshiyabaa bayzzes."
Batches (2.5 ETB a minute, within 24 hours)
Send many text to speech requests at once. They run when demand is low, every batch finishes within 24 hours, and only finished requests are billed, at 2.5 ETB per generated minute, half the Voices 2 list price. Any account with at least one paid top-up can create batches; signup credit alone does not count.
batch = client.batches.create_from_requests(
[
# Tigrinya text, e.g. "ሎሚ ጽቡቕ መዓልቲ እዩ።"
{"custom_id": f"chapter-{i}", "text": text, "voice_id": "ti-berhane", "language": "ti"}
for i, text in enumerate(chapters, start=1)
],
metadata={"project": "audiobook"},
)
done = client.batches.wait_for(batch["id"]) # polls every 30 s
for result in client.batches.results(done["id"]):
if result["status"] == "succeeded":
audio = client.files.content(result["response"]["audio_file_id"])
# save audio
else:
print(result["custom_id"], result["error"])
Batches support the same languages and voices as voice.generate: Amharic
(am), Afaan Oromo (om), Tigrinya (ti), Swahili (sw), Hausa (ha), Sidama
(sid) and Wolaytta (wal), with any voice from the Languages table.
Each request's voice_id must match its language; other languages raise
AddisAIError before anything is uploaded.
Already have a JSONL file ({"custom_id": "...", "body": {...}} per line)? Use
client.files.upload("batch.jsonl") then client.batches.create(input_file_id=...).
client.batches.cancel(batch_id) stops queued requests; running ones finish and
are billed.
Chat / LLM
res = addis.chat.completions.create(
language="am",
system="Be concise.",
persona="You are RecipeBot by AcmeCorp.",
messages=[{"role": "user", "content": "የእንጀራ አሰራር አስተምረኝ"}],
temperature=0.7,
max_tokens=1200,
)
The model is selected by Addis AI; responses report the id
addis-1-alef.
Chat accepts am, om, and ti for text, attachments, and audio input.
Function calling
def get_order_status(args):
return {"status": "shipped", "eta": "tomorrow"}
final = addis.chat.run_tools(
language="am",
messages=[{"role": "user", "content": "Check order 123 and summarize it."}],
tools=[{
"type": "function",
"function": {
"name": "get_order_status",
"description": "Fetch order status by order ID.",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"]},
"callable": get_order_status,
},
}],
)
print(final["choices"][0]["message"]["content"])
Streaming (beta)
stream=True returns a ChatStream — iterate for chat-completion chunk dicts, or use the accumulators:
stream = addis.chat.completions.create(
language="am", messages=[{"role": "user", "content": "Capital?"}], stream=True
)
for chunk in stream:
print(chunk["choices"][0]["delta"].get("content", ""), end="", flush=True)
# or, instead of iterating:
text = addis.chat.completions.create(language="am", messages=[...], stream=True).final_text()
completion = addis.chat.completions.create(language="am", messages=[...], stream=True).final_completion()
Streaming is beta and not available with tools.
Addis Scribe
Scribe transcribes Amharic (am, the default), Afaan Oromo (om), Tigrinya (ti), Hausa (ha),
Sidama (sid), Wolaytta (wal) and Swahili (sw). Choose
backend="turbo" (default) or backend="standard"; both bill the same character rate.
Save a request ID before sending audio; recover it after an interrupted response.
from addisai import AddisAI, ulid
request_id = ulid()
with AddisAI() as addis:
with open("speech.wav", "rb") as audio:
result = addis.scribe.transcribe(audio=audio, backend="standard", request_id=request_id)
print(result["text"], result["usage"]["credits_used"])
# Recovery returns the original settled result without another charge:
# recovered = addis.scribe.recover(request_id)
with open("speech.wav", "rb") as audio:
with addis.scribe.stream(audio=audio, request_id=ulid()) as stream:
for event in stream:
if event["type"] == "transcript.partial":
print(event["text"])
elif event["type"] == "transcript.completed":
print(event["data"]["usage"])
Afaan Oromo
Pass language="om" to any Scribe call: transcribe, stream, transcribe_long, jobs.upload / jobs.create
and live connect / create_session. Oromo runs on the turbo backend (the default) and supports word timestamps,
captions, speaker labels and long files at the same rate. Without language a request is Amharic, exactly as before.
with AddisAI() as addis:
with open("oromo.wav", "rb") as audio:
result = addis.scribe.transcribe(audio=audio, language="om", request_id=ulid())
print(result["language"], result["model"]) # om addis-scribe-oromo-streaming
with addis.scribe.connect(language="om", request_id=ulid()) as live: # live Oromo session
... # send_audio(frame) / finish() as in "Live audio" below
capabilities()["languages"] lists each language's model, backends and speaker support.
Tigrinya
Pass language="ti" to any Scribe call: transcribe, stream, transcribe_long, jobs.upload / jobs.create
and live connect / create_session. Tigrinya runs on the turbo backend (the default) and supports word timestamps,
captions, speaker labels and long files at the same rate as Amharic.
with AddisAI() as addis:
with open("tigrinya.wav", "rb") as audio:
result = addis.scribe.transcribe(audio=audio, language="ti", request_id=ulid())
print(result["language"], result["model"]) # ti addis-scribe-tigrinya-streaming
with addis.scribe.connect(language="ti", request_id=ulid()) as live: # live Tigrinya session
... # send_audio(frame) / finish() as in "Live audio" below
Hausa, Sidama, Wolaytta and Swahili
Pass language="ha" (Hausa), "sid" (Sidama), "wal" (Wolaytta) or "sw" (Swahili) to any Scribe call:
transcribe, stream, transcribe_long, jobs.upload / jobs.create and live connect / create_session. All
four run on the turbo backend (the default) and support word timestamps, captions and long files at the same rate as
Amharic. Speaker labels work in Hausa, Sidama and Wolaytta, not in Swahili. Hausa, Sidama and Wolaytta text is
lowercase without punctuation; Swahili text has capitals, punctuation and numbers as digits.
| Language | language |
model in results |
|---|---|---|
| Hausa | ha |
addis-scribe-hausa-streaming |
| Sidama | sid |
addis-scribe-sidama-streaming |
| Wolaytta | wal |
addis-scribe-wolaytta-streaming |
| Swahili | sw |
addis-scribe-swahili-streaming |
with AddisAI() as addis:
with open("hausa.wav", "rb") as audio:
result = addis.scribe.transcribe(audio=audio, language="ha", request_id=ulid())
print(result["language"], result["model"]) # ha addis-scribe-hausa-streaming
with addis.scribe.connect(language="sw", request_id=ulid()) as live: # live Swahili session
... # send_audio(frame) / finish() as in "Live audio" below
Word timestamps and captions
Pass timestamps="word" to transcribe() to receive words and caption-ready
segments (times in seconds from the start of the file). Timestamps cost nothing
extra. addisai.to_srt() and addisai.to_vtt() format the segments locally, without
another API call, wrapping each cue at 42 characters per line and at most 2 lines.
from addisai import AddisAI, to_srt, to_vtt, ulid
with AddisAI() as addis:
with open("speech.wav", "rb") as audio:
result = addis.scribe.transcribe(audio=audio, timestamps="word", request_id=ulid())
print(result["words"][0]) # {'text': 'ሰላም', 'start': 18.9, 'end': 19.52}
with open("speech.srt", "w", encoding="utf-8") as f:
f.write(to_srt(result))
with open("speech.vtt", "w", encoding="utf-8") as f:
f.write(to_vtt(result))
# 1
# 00:00:18,800 --> 00:00:21,800
# ሰላም ወዳጆቻችን እንዴት ከረማችሁ ዛሬ እንግዲህ እንግዳ አድርጌ
# ያቀረኩላችሁ
Timestamps are available for completed uploads only: stream() rejects
timestamps="word" locally, and live sessions do not return timestamps.
to_srt()/to_vtt() raise AddisAIError if the result has no segments; they also
work on the dict returned by recover() when the original request used timestamps.
Speaker labels
Pass speakers=True with backend="turbo" to label who is talking. It turns on
word timestamps, costs nothing extra, and adds a speaker number to every word and
segment plus a speakers count on the result. Speakers are numbered 1, 2, … in the
order they first talk; a word that could not be attributed has speaker: None.
A new caption cue starts whenever the speaker changes. to_srt() starts each labelled
cue with Speaker N: , and to_vtt() marks it with a <v Speaker N> voice tag.
with AddisAI() as addis:
with open("interview.wav", "rb") as audio:
result = addis.scribe.transcribe(audio=audio, backend="turbo", speakers=True,
request_id=ulid())
print(result["speakers"]) # 2
with open("interview.srt", "w", encoding="utf-8") as f:
f.write(to_srt(result))
# 1
# 00:00:00,600 --> 00:00:01,600
# Speaker 1: ሰላም ወዳጆቻችን
#
# 2
# 00:00:01,700 --> 00:00:03,000
# Speaker 2: እንዴት ናችሁ
Good to know:
- Speaker labels need
backend="turbo"; other backends fail locally before any request.stream()and live sessions do not support them. - On simulated Amharic conversations, 97.8% of words got the right speaker.
- Labels work best with 2 to 4 people and get less accurate when people talk over each other.
- Labels are "Speaker 1", "Speaker 2" and so on, not names.
Long audio (up to three hours)
Files longer than one upload allows (up to three hours, 512 MB) go through a job:
transcribe_long() uploads the file, waits while the API cuts it at pauses, transcribes the
pieces and stitches them into one transcript with one speaker numbering and one bill, then
returns the settled result in the same shape as transcribe() (plus chunks, the number of
pieces). Jobs run on the turbo backend and return word timestamps by default. A file of at most
95 MiB goes up in one request; a larger one is sent as 4 MiB parts, resuming from the parts the
server already holds after a dropped connection. on_progress receives each poll's progress.
from addisai import AddisAI, to_srt, ulid
request_id = ulid()
with AddisAI() as addis:
result = addis.scribe.transcribe_long(
audio="meeting.m4a", speakers=True, request_id=request_id,
on_progress=lambda p: print(f"{p.get('processed_seconds', 0):.0f}/{p.get('audio_seconds') or '?'} s"))
print(result["speakers"], result["chunks"], result["usage"]["credits_used"])
with open("meeting.srt", "w", encoding="utf-8") as f:
f.write(to_srt(result))
# Interrupted? The same request_id returns the result once it is done:
# result = addis.scribe.recover(request_id)
addis.scribe.jobs exposes the steps (upload, create, put_part, complete, status,
wait, cancel) for callers that upload from one process and wait in another. Paid steps are never
retried automatically; at most two jobs run per account at once (429 BUSY). A part sent after
complete gets 409 UPLOAD_CLOSED.
Ctrl-C (KeyboardInterrupt) during the upload cancels the job, so it does not hold one of those
two slots. An unfinished upload also expires on its own 30 minutes after its last new part
(expires_at in jobs.status): it stops counting, it is deleted, and a later transcribe_long
with the same request_id sends the file again from the start. Once the job is queued, an
interrupt only stops the waiting. addis.scribe.jobs.cancel(request_id) stops a job yourself;
nothing is charged, even for a job stopped while processing. An upload or queued job is removed
and its request ID is free again; a processing one ends with REQUEST_CANCELLED.
Live audio
Install pip install "addisai[realtime]" for addis.scribe.connect(request_id=...).
Read its event iterator while calling send_audio(frame) with raw 16 kHz mono
PCM16 little-endian (3,200 bytes per 100 ms), then call finish() and read the
settled completion. Send and receive concurrently for continuous microphone audio.
completion includes the final transcript and usage. create_session issues a
scoped one-use ticket; exported connect_scribe(session) opens that ticket's
socket without account credentials in its URL. capabilities() returns model
limits and usage() returns your wallet balance and current rate.
HTTP uploads accept 95 MiB and up to one hour of audio (speaker labels included); a live session takes up to 15 minutes;
transcribe_long takes up to three hours as a job.
capabilities() returns the current limits (max_upload_bytes, max_upload_seconds, max_live_seconds).
A long upload may answer "still processing"; transcribe then polls until the result is ready.
File streams emit partials after upload;
WebSockets accept audio incrementally. Both default to the turbo backend: HTTP with 1120ms chunks,
sockets with 320ms. Tickets expire after 60 seconds.
Only final transcript UTF-16 character units are billed; partials have no separate
charge. Paid requests/ticket creation are not automatically retried. Disconnecting
or closing a stream can still bill accepted audio. Recover settled results for 24
hours; pending settlement remains recoverable. New audio/settings need a new ID.
See the Scribe guide for complete live PCM, recovery and billing examples.
Speech‑to‑text & translation
with open("call.wav", "rb") as f:
t = addis.speech.transcribe(audio=f, language="am") # am|om|ti|en|ha|sw
print(t["text"])
out = addis.translate.create(text="Hello", source="en", target="am") # am|om|ti|en
print(out["text"])
Tigrinya translates to and from Amharic, Afaan Oromo and English.
Speech recognition uses /api/v2/stt; confidence may be None (always for Tigrinya).
Legacy audio (deprecated)
# ⚠️ deprecated — use addis.voice.generate
out = addis.legacy.audio.generate(text="ሰላም", language="am")
out.to_file("legacy.wav")
# streaming bridge — yields audio bytes, handles both legacy encodings
audio = addis.legacy.audio.stream(text="ሰላም", language="am")
audio.to_file("legacy.wav") # or: data = audio.read()
Errors
from addisai import InsufficientCreditsError, RateLimitError, APIError
try:
addis.voice.generate(voice_id="am-hamen", text="ሰላም", language="am")
except InsufficientCreditsError as e:
show_top_up(e.available_balance)
except RateLimitError as e:
sleep(e.retry_after or 1)
except APIError as e:
print(e.status, e.code, e.details)
Not yet in the Python SDK
An async client (AsyncAddisAI) is the one planned follow‑up. Everything else — including streaming — is at parity with the Node SDK.
Live smoke test
scripts/smoke.py exercises the real API end-to-end. It is not a PR gate —
the smoke workflow runs on manual dispatch and a daily schedule, only where the
repo secret ADDIS_SMOKE_API_KEY (a low-balance sandbox key) is set. Run locally
with ADDIS_API_KEY=<key> python scripts/smoke.py.
License
MIT
Metadata
Release files for addisai 0.14.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| addisai-0.14.0.tar.gz | 116.4 kB | Without provenance | Details for addisai-0.14.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| addisai-0.14.0-py3-none-any.whl | Python 3 | none | any | Details for addisai-0.14.0-py3-none-any.whl |
Total release size: 166.4 kB
Release files / addisai-0.14.0.tar.gz
| Download URL | addisai-0.14.0.tar.gz |
|---|---|
| Size | 116.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
80ab29aef2f27e3f90ec049d9c15ba73dc97361730ae0fde5ab059afc3ef30d1
|
|
BLAKE2b-256 checksum How to use checksums |
7fb3d5d2bf8deeb75a6f48246296987be042eab3df11943cc5aca8e13511d6b2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Release files / addisai-0.14.0-py3-none-any.whl
| Download URL | addisai-0.14.0-py3-none-any.whl |
|---|---|
| Size | 49.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ed57f4a9c9a2d39f48dd7b5544cdb3368fa7ad30ed15275b018a4466d89e6669
|
|
BLAKE2b-256 checksum How to use checksums |
007fdb36bb8eed85cbcad4b810e0a7d5551210e925df339aaa89dde28416a8dd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Security for addisai 0.14.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.