Arabic Text to Speech in Python: A Code First Tutorial

On this page
- What You Need Before You Start
- Step 1: Your First Arabic Text to Speech Request in Python
- Step 2: Find the Right Arabic Voice by Dialect
- Step 3: A Reusable Helper with Retries and Pause Checks
- Step 4: Add Tashkeel Before Synthesising Formal Arabic
- Step 5: Batch Generate Hundreds of Arabic Voice Clips
- Step 6: Turn a Long Arabic Article into One Audio File
- Step 7: Stream Arabic Audio for Voice Agents
- Writing Arabic Text That Reads Well Through Code
- Troubleshooting Arabic Text to Speech in Python
- How We Tested This Code
- Disclaimers
This tutorial shows how to generate Arabic speech from Python, step by step, with code you can copy and run. You will make your first request, pick a voice by dialect, add pauses safely, add tashkeel (vowel marks) to formal Arabic, generate hundreds of clips in a batch, turn a long article into one audio file, and stream audio for real time voice agents.
The examples use the Munsit Arabic text to speech API and the popular requests library. Each step builds on the last, and the full set is short enough to read in one sitting. If you want the bigger picture first, including when to choose an API over a studio tool, read our Arabic text to speech API guide for developers.
What You Need Before You Start
- Python 3.9 or newer.
- The requests library.
- An API key from the Munsit dashboard. New accounts get free credits, so you can test without a card. Plans are listed on the Munsit pricing page.
Install requests and store your key in an environment variable so it never ends up in your code or your Git history:
pip install requests
# macOS or Linux
export MUNSIT_API_KEY="your_key_here"
# Windows PowerShell
$env:MUNSIT_API_KEY="your_key_here"
Step 1: Your First Arabic Text to Speech Request in Python
The synthesis endpoint takes the model ID in the URL and a small JSON body. Three fields are required: a voice ID, the Arabic text and a stability value between 0 and 1. The response is a complete WAV file, so all you have to do is save the bytes.
step1_first_call.py
import os
import requests
API_KEY = os.environ["MUNSIT_API_KEY"]
BASE_URL = os.environ.get("MUNSIT_BASE_URL", "https://api.munsit.com/api/v1")
MODEL_ID = "faseeh-v1-preview"
payload = {
"voice_id": "YOUR_VOICE_ID", # copy one from the voices list (Step 2)
"text": "مرحبا بك في منصت", # "Welcome to Munsit"
"stability": 0.5, # required, 0.0 to 1.0
"speed": 1.0, # optional, 0.7 to 1.2
"sample_rate": 24000, # optional, 8000 to 48000 Hz
}
response = requests.post(
f"{BASE_URL}/text-to-speech/{MODEL_ID}",
headers={"x-api-key": API_KEY, "Content-Type": "application/json"},
json=payload,
timeout=60,
)
response.raise_for_status()
with open("speech.wav", "wb") as f:
f.write(response.content)
print("Saved speech.wav,", len(response.content), "bytes")

A few notes on the optional settings, all taken from the synthesize documentation:
- Speed runs from 0.7 (slower) to 1.2 (faster), with 1.0 as the default.
- Sample rate runs from 8000 to 48000 Hz. The default is 24000; 48000 is recommended for real time and WebRTC use because it avoids resampling.
- Dialect is an optional hint with three values: auto, emirati and fusha. For other dialects, such as Najdi or Hijazi, the accent comes from the voice you choose.
- Cost is based on the number of characters, so trimming unnecessary text saves credits.
Step 2: Find the Right Arabic Voice by Dialect
Voice IDs are opaque strings, so look them up rather than guessing. The voices endpoint accepts a dialect filter and a tags filter, for example narration or conversational:
step2_list_voices.py
import os
import requests
API_KEY = os.environ["MUNSIT_API_KEY"]
BASE_URL = os.environ.get("MUNSIT_BASE_URL", "https://api.munsit.com/api/v1")
def list_voices(dialect=None, tags=None):
params = {}
if dialect:
params["dialect"] = dialect # for example "najdi", "emirati", "fusha"
if tags:
params["tags"] = tags # for example "narration" or "conversational"
r = requests.get(f"{BASE_URL}/voices", headers={"x-api-key": API_KEY},
params=params, timeout=30)
r.raise_for_status()
data = r.json()
# Accept either a plain list or a list wrapped in an object
if isinstance(data, dict):
data = data.get("data") or data.get("voices") or data.get("items") or []
return data
for v in list_voices(dialect="najdi"):
print(v.get("voice_id") or v.get("id"), "|", v.get("name"), "|",
v.get("dialect"), "|", v.get("gender"))Two details are worth knowing. First, filtering by saudi also returns Najdi, Hijazi and southern Saudi voices, so use najdi or hijazi when you need a specific city sound. Second, the featured voice list currently leans toward Najdi voices, with fewer Hijazi and Emirati options, so check what your account returns before you plan a large project. You can also preview voices by ear in the Munsit text to speech studio.
The code above accepts the response as either a plain list or a list wrapped in an object, so it keeps working if the response format changes.
Step 3: A Reusable Helper with Retries and Pause Checks
Once you move past one request, wrap the API in a small helper. This one does three useful things: it retries when the service is busy, it raises a clear error with the server’s message when a request is wrong, and it checks your pause tags before sending them.
munsit_tts.py
"""A small, reusable helper for Munsit Arabic text to speech."""
import os
import re
import time
import requests
API_KEY = os.environ["MUNSIT_API_KEY"]
BASE_URL = os.environ.get("MUNSIT_BASE_URL", "https://api.munsit.com/api/v1")
MODEL_ID = "faseeh-v1-preview"
HEADERS = {"x-api-key": API_KEY, "Content-Type": "application/json"}
BREAK_TAG = re.compile(r'<break\s+time="([^"]*)"\s*/>')
VALID_TIME = re.compile(r"^(\d+(\.\d+)?)(ms|s)$")
def check_pauses(text):
"""Warn about pause tags the API would silently ignore or clamp."""
problems = []
tags = BREAK_TAG.findall(text)
total = 0.0
for value in tags:
m = VALID_TIME.match(value)
if not m:
problems.append(f'"{value}" has no unit (s or ms) and will be ignored')
continue
seconds = float(m.group(1)) / (1000 if m.group(3) == "ms" else 1)
if seconds > 3:
problems.append(f'"{value}" is longer than 3s and will be clamped to 3s')
total += min(seconds, 3)
if len(tags) > 20:
problems.append(f"{len(tags)} pause tags; over 20 may be dropped")
if total > 30:
problems.append(f"{total:.1f}s of pauses; about 30s is the maximum")
return problems
def synthesize(text, voice_id, stability=0.5, speed=1.0, sample_rate=24000,
dialect=None, retries=3):
"""Return WAV bytes, retrying on rate limits and server errors."""
for warning in check_pauses(text):
print("Pause tag warning:", warning)
payload = {"voice_id": voice_id, "text": text, "stability": stability,
"speed": speed, "sample_rate": sample_rate}
if dialect:
payload["dialect"] = dialect # "auto", "emirati" or "fusha"
for attempt in range(retries + 1):
r = requests.post(f"{BASE_URL}/text-to-speech/{MODEL_ID}",
headers=HEADERS, json=payload, timeout=60)
if r.status_code == 200:
return r.content
if r.status_code in (429, 500, 502, 503, 504) and attempt < retries:
wait = 2 ** attempt
print(f"Got {r.status_code}, retrying in {wait}s")
time.sleep(wait)
continue
raise RuntimeError(f"Synthesis failed ({r.status_code}): {r.text[:300]}")
def diacritize(text):
"""Add tashkeel (vowel marks) to formal Arabic text."""
r = requests.post(f"{BASE_URL}/tashkil/diacritize", headers=HEADERS,
json={"text": text}, timeout=60)
r.raise_for_status()
return r.json()["diacritized_text"]
Why check pause tags in code? Because the API applies its pause rules quietly. A tag such as <break time="1"/> has no unit, so it produces no pause and no error. A pause longer than 3 seconds is clamped to 3 seconds, and more than 20 tags or about 30 seconds of total pause in one request may be reduced or dropped. Catching these before you generate audio saves credits and a lot of confused listening.

The retry loop waits 1, 2 and then 4 seconds before giving up. Rate limiting usually returns HTTP status 429, and short exponential backoff is the standard way to handle it. Check the rate limits page in the docs for the limits on your plan.
Step 4: Add Tashkeel Before Synthesising Formal Arabic
Arabic is usually written without short vowel marks, which leaves some words ambiguous for any voice engine. The Tashkīl endpoint adds the vowel marks and returns both the original and the marked text, so you can compare them before generating audio:
step4_tashkil.py
from munsit_tts import diacritize, synthesize
formal = "يسرنا ان نرحب بكم في المعرض الدولي للكتاب"
marked = diacritize(formal)
print(marked)
audio = synthesize(marked, voice_id="YOUR_FUSHA_VOICE_ID", dialect="fusha")
with open("welcome_fusha.wav", "wb") as f:
f.write(audio)
Use this for formal Arabic, such as announcements, education and official content, where it works best. The documentation notes that dialect, informal or noisy text may need review after diacritization, so for a Najdi or Emirati script, fix individual words by hand instead.
Step 5: Batch Generate Hundreds of Arabic Voice Clips
Product videos, IVR menus and course modules often need hundreds of short clips. Keep the scripts in a CSV file with an id and a text column, then generate them with a small thread pool:
step5_batch.py
import csv
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from munsit_tts import synthesize
VOICE_ID = "YOUR_VOICE_ID"
OUT = Path("audio")
OUT.mkdir(exist_ok=True)
def render(row):
target = OUT / f"{row['id']}.wav"
if target.exists(): # skip files already made
return f"{row['id']}: skipped"
target.write_bytes(synthesize(row["text"], voice_id=VOICE_ID))
return f"{row['id']}: done"
with open("scripts.csv", encoding="utf-8") as f:
rows = list(csv.DictReader(f)) # columns: id,text
# Keep concurrency low and check the rate limits page for your plan
with ThreadPoolExecutor(max_workers=3) as pool:
for result in pool.map(render, rows):
print(result)

The script skips any clip that already exists, so if it stops halfway you can simply run it again. Three workers is a deliberately cautious default for Python’s ThreadPoolExecutor; raise it only after checking your plan’s rate limits.
Step 6: Turn a Long Arabic Article into One Audio File
The synthesize documentation does not state a maximum text length, and long requests are harder to retry if something fails. A safer pattern is to split long text at sentence endings, including the Arabic question mark (؟) and semicolon (؛), generate each part, then join the WAV files with Python’s built in wave module:
step6_long_text.py
import io
import re
import wave
from munsit_tts import synthesize
SENTENCE_END = re.compile(r"(?<=[.!؟?؛])\s+")
def split_text(text, max_chars=800):
"""Split on Arabic and Latin sentence endings, keeping chunks under max_chars."""
chunks, current = [], ""
for sentence in SENTENCE_END.split(text.strip()):
if current and len(current) + len(sentence) + 1 > max_chars:
chunks.append(current)
current = sentence
else:
current = f"{current} {sentence}".strip()
if current:
chunks.append(current)
return chunks
def join_wavs(wav_list, path):
"""Concatenate WAV files that share the same format into one file."""
with wave.open(path, "wb") as out:
for i, data in enumerate(wav_list):
with wave.open(io.BytesIO(data), "rb") as w:
if i == 0:
out.setparams(w.getparams())
out.writeframes(w.readframes(w.getnframes()))
article = open("article.txt", encoding="utf-8").read()
parts = [synthesize(chunk, voice_id="YOUR_VOICE_ID") for chunk in split_text(article)]
join_wavs(parts, "article.wav")
print(f"Joined {len(parts)} parts into article.wav")
Joining works because every part comes back in the same format. If you change the sample rate or voice between parts, the formats will no longer match, so keep those settings fixed for the whole article.
Step 7: Stream Arabic Audio for Voice Agents
For a voice agent or an IVR, waiting for the whole clip to finish before playing it adds noticeable delay. Set streaming to true and the API returns 16 bit PCM audio in chunks as it is generated. With requests, use streamed responses to read the chunks as they arrive:
step7_streaming.py
import os
import time
import wave
import requests
API_KEY = os.environ["MUNSIT_API_KEY"]
BASE_URL = os.environ.get("MUNSIT_BASE_URL", "https://api.munsit.com/api/v1")
SAMPLE_RATE = 48000 # 48 kHz is recommended for real time use
payload = {
"voice_id": "YOUR_VOICE_ID",
"text": "أهلا، كيف أقدر أساعدك اليوم؟",
"stability": 0.5,
"streaming": True, # returns PCM16 chunks as they are generated
"sample_rate": SAMPLE_RATE,
}
start = time.perf_counter()
first_chunk_at = None
with requests.post(f"{BASE_URL}/text-to-speech/faseeh-v1-preview",
headers={"x-api-key": API_KEY}, json=payload,
stream=True, timeout=60) as r:
r.raise_for_status()
# Assumes mono 16 bit PCM: confirm channel count in the streaming docs
with wave.open("stream.wav", "wb") as out:
out.setnchannels(1)
out.setsampwidth(2)
out.setframerate(SAMPLE_RATE)
for chunk in r.iter_content(chunk_size=4096):
if not chunk:
continue
if first_chunk_at is None:
first_chunk_at = time.perf_counter() - start
out.writeframes(chunk) # or send chunk straight to a player
print(f"First audio after {first_chunk_at * 1000:.0f} ms; saved stream.wav")
Two practical notes. The example measures how long the first chunk takes, which is the number that matters to a caller; measure it from your own servers rather than relying on published figures. It also assumes mono audio when wrapping the chunks in a WAV file, so confirm the format in the streaming output documentation before you build on it. For full voice agents, Munsit documents plugins and guides for LiveKit, Pipecat, Vapi and Ultravox, which handle the streaming for you.
Writing Arabic Text That Reads Well Through Code
- Write in the dialect of the voice. A Najdi voice reading formal Arabic still sounds formal. Use words such as وش and الحين in Najdi scripts.
- Write key numbers as words. Prices, phone numbers and dates are where voices most often stumble.
- Test English names both ways. Try brand names in Latin letters and in Arabic letters and keep whichever sounds right.
- Keep the text clean. Strip HTML, emoji and tracking codes before sending text from a CMS or database.
- Store scripts as UTF 8. Open files with an explicit UTF 8 encoding, as the examples do, to avoid broken Arabic characters.
Troubleshooting Arabic Text to Speech in Python
| What you see | Likely cause | Fix |
|---|---|---|
| HTTP 401 | Missing or wrong API key | Check the environment variable is set in the same terminal |
| HTTP 400 | A required field is missing or out of range, often stability | Send voice_id, text and stability between 0 and 1 |
| HTTP 413 from Tashkīl | The request body is too large | Split the text and diacritize it in parts |
| HTTP 429 | Too many requests at once | Retry with backoff and lower the number of workers |
| No pause where you expected one | Pause tag withouta unit | Use 500ms or 1s, and run the pause check |
| Audio plays as noise | Raw PCM played as if it were a WAV file | Wrap streamed chunks with the wave module, as in Step 7 |
| Joined article fails to save | Parts generated with different settings | Keep voice and sample rate the same for every part |
| Arabic looks broken in files | Wrong text encoding | Read and write files with UTF 8 encoding |
How We Tested This Code
Every snippet in this tutorial was run end to end against a local mock server that follows the documented request and response formats: required fields, WAV and streamed PCM output, the voices filter and the Tashkīl response. That confirmed the code runs, retries on a 429, reports a 400 clearly, skips existing batch files and joins long text correctly. It does not replace a test against the live API with your own key, so run Step 1 first before building on the rest. API details were checked against Munsit’s public documentation in October 2026.
Disclaimers
- API endpoints, parameters and limits in this tutorial come from Munsit’s public documentation reviewed in October 2026 and may change. Check the current documentation before deploying to production.
- The code was tested against a local mock of the documented API, not the live service. Test with your own API key before relying on it.
- The streaming example assumes mono 16 bit audio when writing a WAV file. Confirm the format in the streaming documentation.
- Code is provided as is, for learning purposes. Review it for your own security, error handling and compliance needs.
- Only clone or imitate voices with the speaker’s written consent, and follow the data protection rules that apply to you.
- Product and library names belong to their respective owners. Their mention does not imply affiliation or endorsement.
Frequently asked questions
Can I do Arabic text to speech in Python for free?
You can test for free with the credits Munsit gives new accounts, and open source Arabic models can also be run in Python. Check licences and quality before using any voice commercially.
Which Python library should I use for Arabic text to speech?
For a hosted API, the requests library is all you need. For voice agents, use the framework plugins for LiveKit or Pipecat rather than writing streaming code yourself.
How do I choose an Arabic dialect in Python?
Pick a voice that speaks the dialect, using the dialect filter on the voices endpoint. The optional dialect hint on synthesis accepts auto, emirati and fusha.
What audio format does the API return?
A complete WAV file by default, or 16 bit PCM chunks when streaming is turned on.
How do I add pauses in Arabic text to speech?
Insert a break tag with a unit, such as 500ms or 1s. Each pause is capped at 3 seconds, and tags without a unit are ignored.
Can I clone a voice from Python?
Yes. The voice cloning endpoint accepts audio files and returns a new voice ID that works in the same synthesis call. Only clone voices you have written permission to use.


