---
title: Text-to-Speech
description: Discover voices, estimate cost, generate speech, and manage durable audio clips.
icon: AudioLines
isNew: true
---
import { VoiceCatalog } from '@/components/voice-catalog';
Addis Voices 2 turns Amharic and Afaan Oromo text into durable audio clips. It is billed by generated duration at **5 ETB per minute**. Use `voice.generate` for completed clips, or follow [Streaming text-to-speech](/docs/capabilities/text-to-speech/streaming) for HTTP streams and persistent WebSocket sessions.
The original Text-to-Speech API is deprecated and hidden from the primary navigation. [Open the Legacy Text-to-Speech guide →](/docs/capabilities/text-to-speech-legacy)
## Discover voices
Query the catalog rather than copying an arbitrary ID into production configuration. `am-hamen` is the canonical Amharic example.
```ts
const voices = await addis.voices.list({ language: "am" });
for (const voice of voices) {
console.log(voice.id, voice.name, voice.isAvailable);
}
```
```python
voices = addis.voices.list(language="am")
for voice in voices:
print(voice["id"], voice["name"], voice["is_available"])
```
```bash
curl "https://api.addisassistant.com/api/v1/voice/voices?language=am" \
-H "x-api-key: $ADDIS_API_KEY"
```
The catalog is dynamic. Filter for the requested language and store only IDs currently marked available. Each entry includes the voice ID, name, descriptor, language, gender, style, tags, preview URL, default status, and availability.
### Catalog filters
| Filter | Node.js | Python | Values |
| --- | --- | --- | --- |
| Language | `language` | `language` | `am` or `om` |
| Gender | `gender` | `gender` | `female` or `male` |
| Search | `search` | `search` | Name, ID, descriptor, style, or tag text |
| Include unavailable voices | `includeUnavailable` | `include_unavailable` | `false` by default |
### Browse current voice IDs
The playable catalog below is synchronized with the current backend availability list and developer-platform previews, and was verified on July 23, 2026. Your application should still call `voices.list()` because availability can change without a documentation release.
## Preview a voice
```ts
const preview = await addis.voices.preview("am-hamen");
console.log(preview.audioUrl);
```
```python
preview = addis.voices.preview("am-hamen")
print(preview["audio_url"])
```
```bash
curl https://api.addisassistant.com/api/v1/voice/voices/am-hamen/preview \
-H "x-api-key: $ADDIS_API_KEY"
```
Preview URLs are signed and can expire. Fetch fresh preview metadata instead of storing the URL permanently.
## Estimate cost
Estimate before generation when a user must approve spend or the application is close to its available balance.
```ts
const input = {
text: "ሰላም፣ ይህ የአዲስ ድምፅ ሁለት ሙከራ ነው።",
voiceId: "am-hamen",
language: "am" as const,
outputFormat: "mp3_44100" as const,
};
const estimate = await addis.voice.estimate(input);
if (!estimate.canGenerate) throw new Error("Insufficient balance");
console.log(estimate.estimatedCost, estimate.currency);
```
```python
input = {
"text": "ሰላም፣ ይህ የአዲስ ድምፅ ሁለት ሙከራ ነው።",
"voice_id": "am-hamen",
"language": "am",
"output_format": "mp3_44100",
}
estimate = addis.voice.estimate(**input)
if not estimate["can_generate"]:
raise RuntimeError("Insufficient balance")
print(estimate["estimated_cost"], estimate["currency"])
```
```bash
curl https://api.addisassistant.com/api/v1/voice/estimate \
-H "x-api-key: $ADDIS_API_KEY" \
-H "content-type: application/json" \
-d '{
"text":"ሰላም፣ ይህ የአዲስ ድምፅ ሁለት ሙከራ ነው።",
"voice_id":"am-hamen",
"language":"am",
"output_format":"mp3_44100"
}'
```
The estimate reports the pricing unit, price per minute, estimated duration, billable duration, cost, current balance, projected balance, currency, and whether generation is currently allowed.
## Generate and save a clip
```ts
const clip = await addis.voice.generate({
...input,
clientRequestId: crypto.randomUUID(),
});
await clip.toFile("speech.mp3");
console.log(clip.id, clip.audioUrl, clip.usage);
```
```python
clip = addis.voice.generate(
**input,
client_request_id="order-123",
)
clip.to_file("speech.mp3")
print(clip.id, clip.audio_url, clip.usage)
```
```bash
curl https://api.addisassistant.com/api/v1/voice/generations \
-H "x-api-key: $ADDIS_API_KEY" \
-H "content-type: application/json" \
-d '{
"text":"ሰላም፣ ይህ የአዲስ ድምፅ ሁለት ሙከራ ነው።",
"voice_id":"am-hamen",
"language":"am",
"output_format":"mp3_44100",
"client_request_id":"order-123"
}' | tee voice.json
curl --location "$(jq -r '.data.audio_url' voice.json)" \
--output speech.mp3
```
The cURL example requires `jq`. A generation response contains clip metadata and a signed `audio_url`. A first response may also include an audio data URL; an idempotent replay can omit that inline field, so use the signed URL or SDK file helper.
## Streaming
Play speech while it is generated, over HTTP for single requests or a persistent WebSocket for successive turns. [Open the streaming guide →](/docs/capabilities/text-to-speech/streaming)
## Output formats
| Format | Use |
| --- | --- |
| `mp3_44100` | Compact delivery for web, mobile, and downloads. |
| `wav_44100` | Uncompressed editing and audio pipelines. |
| `pcm_16000` | Speech pipelines expecting 16 kHz PCM in a WAV container. |
Choose the format before estimating because output choice can affect generated duration and storage behavior.
## Idempotency and retries
`clientRequestId` in Node.js and `client_request_id` in Python identifies one logical generation.
- Reuse it when retrying the same request after a timeout or uncertain response.
- Generate a new value for different text, voice, language, or output format.
- Do not retry a validation error unchanged.
- For a generation-in-progress response, wait for the reported retry interval before checking again.
## Voice settings
`speed` is currently applied. `stability`, `similarity`, and `style` are accepted for compatibility but ignored by the current provider. The response reports ignored settings in `meta.ignoredVoiceSettings` or `meta["ignored_voice_settings"]`.
Do not build user-facing behavior that assumes an ignored control changes the generated clip.
## Usage and clip history
```ts
const usage = await addis.voice.usage();
const page = await addis.voice.clips.list({ limit: 20 });
const clip = await addis.voice.clips.get(page.data[0].id);
await clip.toFile("saved-clip.mp3");
await addis.voice.clips.delete(clip.id);
```
```python
usage = addis.voice.usage()
page = addis.voice.clips.list(limit=20)
clip = addis.voice.clips.get(page.data[0]["id"])
clip.to_file("saved-clip.mp3")
addis.voice.clips.delete(clip.id)
```
```bash
curl https://api.addisassistant.com/api/v1/voice/usage \
-H "x-api-key: $ADDIS_API_KEY"
curl https://api.addisassistant.com/api/v1/voice/clips \
-H "x-api-key: $ADDIS_API_KEY"
curl -X DELETE https://api.addisassistant.com/api/v1/voice/clips/CLIP_ID \
-H "x-api-key: $ADDIS_API_KEY"
```
Addis Voices 2 usage is billed at **5 ETB per generated minute**. Use the estimate before generation when a user must approve spend, and use the usage response to display current ETB balance and total spend.
## Errors
Common failures include unavailable voice IDs, unsupported formats, insufficient balance, idempotency conflicts, generation still in progress, rate limiting, and transient provider errors.
Refresh the voice catalog after an unavailable-voice error. Log the clip ID, request ID, and client request ID, but never the API key or sensitive full text.
## Best Practices
Ensure high-quality output and reliable billing with these production patterns.
Script & Punctuation
Natural pauses: Use Ethiopic punctuation and complete sentences.
Consistent script: Transliterate mixed terms explicitly when pronunciation matters.
Discover: Query and filter the catalog instead of hardcoding a voice count.
Preview: Listen before saving a voice as the application default.
Cost & Retries
Estimate first: Estimate when a user must approve spend.
Idempotency: Reuse a client request ID only when retrying the same logical generation.
Clip Delivery
Persist IDs: Store durable clip IDs instead of expiring signed URLs.
Stream or save: Use `voice.generate` for a completed clip, or [Streaming text-to-speech](/docs/capabilities/text-to-speech/streaming) to receive speech during generation.
## Migrate from Legacy Text-to-Speech
Addis Voices 2 uses the same Addis AI API key as the legacy API, so no credential migration is required. HTTP header names are case-insensitive: the SDK reads `ADDIS_API_KEY`, and raw requests send it through `x-api-key`.
| Area | Legacy Text-to-Speech | Addis Voices 2 |
| --- | --- | --- |
| Endpoint | `/api/v1/audio` | `/api/v1/voice/generations` |
| Authentication | Existing Addis AI API key in `X-API-Key` | The same API key through the SDK or `x-api-key`; no credential change |
| Request | `text`, `language`, and optional `stream` | `text`, `voice_id`, `language`, `output_format`, and optional `client_request_id` |
| Voice selection | Language selects the legacy engine | Query the [voice catalog](#discover-voices), preview an available voice, then send its exact ID |
| Response | Base64 WAV JSON or a legacy audio stream | Durable clip metadata and a signed `audio_url`; [HTTP and WebSocket streaming](/docs/capabilities/text-to-speech/streaming) are also available |
| SDK | `legacy.audio.generate()` or `legacy.audio.stream()` | `voice.generate()`, `voices.list()`, estimates, usage, and clip management |
| Pricing | Existing Voice 1 account pricing remains unchanged | **5 ETB per generated minute**, with a pre-generation estimate |
For a new integration, follow [Generate and save a clip](#generate-and-save-a-clip). Existing applications can keep using the complete [Legacy Text-to-Speech guide](/docs/capabilities/text-to-speech-legacy) while migrating one use case at a time. Review [Pricing](/docs/platform/pricing) before switching production traffic.