A production-oriented Levantine Text-to-Speech pipeline built on a fine-tuned XTTS-v2 optimized for real-time conversational agents.
| 🎯 KPI | Target | Measured | Status |
|---|---|---|---|
| Peak VRAM (inference) | ≤ 3 GB | 2.13 GB | ✅ |
| Time-to-First-Audio (p50) | < 300 ms | 565 ms | |
| Real-Time Factor (RTF) | < 0.3 | 0.21 | ✅ |
| Streaming output | required | chunked PCM + WS | ✅ |
Leva-TTS is a production-ready streaming TTS system that handles natural code-switching between Levantine Arabic dialect and English — the way real speakers actually talk.
It fine-tunes XTTS-v2 (Coqui) on 50,000 high-quality synthetic Levantine Arabic + code-switching utterances generated by Lahgtna-OmniVoice v2 — a zero-shot TTS model already fine-tuned for the Levantine Arabic dialect (ISO 639-3: apc).
| Feature | Details |
|---|---|
| 🗣️ Natural code-switching | Intra-sentence Arabic ↔ English |
| ⚡ Streaming output | First audio chunk < 300 ms |
| 💾 Low VRAM | ≤ 3 GB at inference |
| 🌿 Levantine dialect | ق→/ʔ/ glottal, ج→/ʒ/, il- article, b- prefix |
| 🔤 Smart text front-end | Partial diacritics on homographs + Levantine lexicon CSV |
| 👥 10 speakers | 5 male + 5 female, diverse Levantine accents |
| 📡 WebSocket streaming | FastAPI server with real-time chunked PCM |
| 🔌 Pipecat ready | Drop-in TTSService for voice agents |
Measured on a single NVIDIA H100 (fp16) over a 15-sentence held-out set (6 pure Levantine · 3 pure English · 6 code-switched), speaker Mohamed:
| Metric | Target | Achieved |
|---|---|---|
| 💾 Peak VRAM (inference only) | ≤ 3 GB | 2.13 GB ✅ |
| ⚡ TTFA — streaming (first chunk) | < 300 ms | ~565 ms |
| ⏱️ TTFA — batch p50 | — | 707 ms |
| 🎚️ RTF p50 / p95 | < 0.3 | 0.21 / 0.59 ✅ (p50) |
| 📡 Streaming | Required | ✅ |
Notes: RTF p50 is well under target; longer sentences raise p95. Streaming TTFA (~565 ms) is the time to the first playable audio chunk — XTTS-v2's autoregressive GPT is slower than the 300 ms streaming target on first token, but audio plays continuously thereafter. VRAM excludes the Whisper model used only during evaluation.
Run everything on a free Colab T4 GPU — no local install:
See examples/ for details.
Leva-TTS supports two usage paths:
| Path | For whom | What you get |
|---|---|---|
A — pip install |
You only want to synthesize speech | The LevaTTS Python class — synthesize, zero_shot_synthesize, stream, zero_shot_stream. The fine-tuned checkpoint + 10 reference speakers download automatically on first use. |
| B — Clone the repo | You want full control — streaming server, Pipecat, Gradio app, fine-tuning | Everything in A plus the FastAPI server, the Pipecat plugin, the Gradio demo, the evaluation suite, and the training pipeline. |
conda create -n leva-tts python=3.10 -y
conda activate leva-tts
# System audio libraries (Ubuntu/Debian)
sudo apt-get install -y espeak-ng ffmpeg libsndfile1Install PyTorch before leva-tts so pip locks a CUDA build that matches your
GPU driver — not the newest available. The defaults below install
torch 2.3.0 + cu121, the exact build Leva-TTS is developed and validated on.
# CUDA 12.x driver (most H100 / A100 / RTX setups) — recommended
pip install torch==2.3.0 torchaudio==2.3.0 --index-url https://download.pytorch.org/whl/cu121
# CUDA 11.8 driver
pip install torch==2.3.0 torchaudio==2.3.0 --index-url https://download.pytorch.org/whl/cu118
# CPU only
pip install torch==2.3.0 torchaudio==2.3.0 --index-url https://download.pytorch.org/whl/cpuWhy pin torch? PyTorch ≥ 2.9 ships CUDA-13 wheels that require a CUDA-13 driver. On a common CUDA-12.x driver they fail at startup with "The NVIDIA driver on your system is too old." Leva-TTS therefore caps
torch < 2.9; installingtorch==2.3.0+cu121first guarantees a driver-matched build. Check your driver withnvidia-smi(top-right "CUDA Version").
pip install leva-ttsBecause PyTorch is already installed and satisfies the torch>=2.3,<2.9
constraint, pip leaves it untouched and only adds the remaining dependencies.
Engine note: Leva-TTS depends on
coqui-tts— the maintained Coqui fork that exposes the sameTTS/XTTS modules. The originalTTSpackage is unmaintained and pinsnumpy==1.22.0, which cannot resolve against modernlibrosa/numbaon Python 3.10+ (the classicResolutionImpossibleerror).coqui-ttsships a coherent, numpy-2-compatible dependency set, so a plainpip install leva-ttsresolves cleanly.
First synthesis call auto-downloads the fine-tuned checkpoint and the 10 reference speakers from HuggingFace (
mohammedaly22/leva-tts), falling back to the GitHub release. To pre-download:python -c "import leva_tts; leva_tts.download_model()"
from leva_tts import LevaTTS, SPEAKERS
tts = LevaTTS(
device="cuda", # "cuda" | "cpu" (auto-detected if omitted)
preprocess_text=True, # Levantine text front-end (numbers, dates, diacritics, lexicon)
verbose=False, # print the text-processing stages
)
print(SPEAKERS)
# ['Badr', 'Mohamed', 'Saad', 'Rami', 'Fadi',
# 'Amina', 'Fatma', 'Lamyaa', 'Mona', 'Haneen']synthesize(text, speaker, language="ar", **gen_params) returns (wav, sr) —
a float32 NumPy array at 24 kHz. speaker must be one of the 10 names above,
otherwise a ValueError is raised.
import soundfile as sf
wav, sr = tts.synthesize(
"هَلَّق أنا عم أشتغل على the project",
speaker="Badr",
temperature=0.65, # generation params are optional per-call
repetition_penalty=5.0,
top_p=0.85,
top_k=50,
speed=1.0,
)
sf.write("output.wav", wav, sr) # sr == 24000zero_shot_synthesize(text, reference_audio, language="ar", **gen_params) —
same as synthesize, but you pass a path to your own 3–10 s reference clip
instead of a built-in speaker name.
wav, sr = tts.zero_shot_synthesize(
"والله the meeting today كانت important كتير",
"my_voice.wav",
language="ar",
)
sf.write("cloned.wav", wav, sr)stream(...) and zero_shot_stream(...) mirror the two methods above but
yield audio chunks as they are generated — ideal for low-latency playback or
sending over a socket.
import numpy as np, soundfile as sf
# Built-in speaker
chunks = []
for chunk in tts.stream("بِدِّي أحكيلك عن the new feature هَلَّق", speaker="Amina"):
chunks.append(chunk) # play / forward each chunk in real time
sf.write("streamed.wav", np.concatenate(chunks), 24000)
# Zero-shot streaming
for chunk in tts.zero_shot_stream("هلق عم نشتغل على الموضوع", "my_voice.wav"):
...Generation parameters (all optional, valid on every method): temperature,
length_penalty, repetition_penalty, top_k, top_p, speed.
For the streaming server, Pipecat integration, the Gradio app, evaluation, or fine-tuning, clone the repo and create the full conda environment.
git clone https://github.com/MohammedAly22/Leva-TTS.git
cd Leva-TTS
# System dependencies
sudo apt-get install -y espeak-ng ffmpeg libsndfile1
# Full conda environment (XTTS, training, server, pipecat, gradio)
conda env create -f environment.yml
conda activate leva-tts
pip install -e .
# Optional — GPU training acceleration
bash scripts/install_deepspeed.shCoqpit fork (only if you hit an import error).
coqui-ttsuses a forked Coqpit published ascoqpit-config(it still imports ascoqpit). If the originalcoqpitis also present — e.g. an environment upgraded in place from an older Leva-TTS — it shadows the fork and you'll see:ImportError: coqui-tts switched to a forked version of Coqpit ... # or, after a partial uninstall: ImportError: cannot import name 'Coqpit' from 'coqpit'Remove both and reinstall only the fork (a stale
coqpit/dir can survive an uninstall, so delete it explicitly):pip uninstall -y coqpit coqpit-config rm -rf "$(python -c 'import site; print(site.getsitepackages()[0])')/coqpit" pip install --force-reinstall --no-deps coqpit-config python -c "from coqpit import Coqpit; print('coqpit OK')"A fresh
conda env createfrom this repo installs onlycoqpit-config, so this step is not needed on a clean setup.
Download the checkpoint + reference speakers:
python -c "import leva_tts; leva_tts.download_model('./checkpoints')"# Built-in speaker
python scripts/inference.py --text "كيفك اليوم؟ أتمنى ان كل شي تمام هَلَّق." --speaker Mohamed --out output.wav
# Streaming mode
python scripts/inference.py --text "..." --speaker Badr --stream
# Zero-shot with your own reference audio
python scripts/inference.py --text "..." --ref-audio your_speaker.wav --out clone.wav# Start the server
LEVA_CHECKPOINT=./checkpoints LEVA_SPEAKER_WAV=./reference_audios/Badr.wav python -m leva_tts.server.app
# Health check
curl http://localhost:8000/health
# Batch synthesize
curl -X POST http://localhost:8000/synthesize \
-H "Content-Type: application/json" \
-d '{"text":"كيفك اليوم؟","language":"ar","format":"wav"}' \
--output output.wavEndpoints: POST /synthesize (WAV/PCM), WS /stream (real-time chunks),
GET /health, GET /metrics.
from leva_tts.pipecat_plugin import LevaTTSService
from pipecat.pipeline.pipeline import Pipeline
# Local GPU mode
tts = LevaTTSService(
mode="local",
checkpoint="./checkpoints",
speaker_wav="./reference_audios/Badr.wav",
language="ar",
)
# Remote WebSocket mode (points at the streaming server above)
tts_remote = LevaTTSService(
mode="remote",
server_url="ws://localhost:8000/stream",
language="ar",
)
pipeline = Pipeline([..., tts, ...])The service emits TTSStartedFrame → TTSAudioRawFrame(s) → TTSStoppedFrame,
streaming audio chunk-by-chunk for conversational latency.
python app.py
# Open http://localhost:7860Expected output:
Features:
- 🎤 10-speaker dropdown with reference playback
- 📝 processed-text preview (see exactly what XTTS-v2 receives)
- 🎵 batch synthesis with TTFA / RTF / VRAM metrics
- 🎙️ zero-shot upload (any 3–10 s clip)
- 💡 pre-loaded code-switching examples.
The full data pipeline (50K synthetic utterances via Lahgtna-OmniVoice v2) and the XTTS-v2 fine-tuning steps are documented in the Data Pipeline section below.
python scripts/prepare_data.py --metadata data/metadata.csv --out data/
python scripts/train.py --config configs/<YOUR_TRAINING_CONFIG>.jsonpython scripts/evaluate.py --checkpoint checkpoints/
# Skip ASR (faster)
python scripts/evaluate.py --checkpoint checkpoints/ --no-asrReports:
- TTFA p50/p95, RTF p50/p95, Peak VRAM
- CER/WER via Whisper large-v3 ASR round-trip
- UTMOS (reference-free neural MOS)
- Per-type breakdown: pure_levantine / pure_english / code_switching
Overall
| Metric | Value |
|---|---|
| Peak VRAM (inference) | 2.13 GB |
| RTF p50 / p95 | 0.36 / 0.53 |
| TTFA p50 / p95 (batch) | 1194 / 1743 ms |
| TTFA streaming (first chunk) | ~565 ms |
| CER (mean) | 0.255 |
| WER (mean) | 0.496 |
| UTMOS (reference-free MOS) | 3.13 / 5.0 |
Per-category (intelligibility via ASR round-trip)
| Category | n | CER ↓ | WER ↓ | RTF ↓ | UTMOS ↑ |
|---|---|---|---|---|---|
| Pure English | 3 | 0.144 | 0.190 | 0.365 | 3.35 |
| Pure Levantine Arabic | 6 | 0.236 | 0.544 | 0.412 | 2.97 |
| Code-Switching | 6 | 0.330 | 0.602 | 0.358 | 3.19 |
Pure English achieves the lowest CER/WER, confirming English quality is well retained. Arabic CER/WER are higher partly because Whisper large-v3 transcribes MSA-normalized Arabic while the references keep Levantine spelling and partial diacritics — so a fraction of the "errors" are orthographic differences, not pronunciation errors. Code-switching is the hardest case (language boundaries), as expected.
We provide an optimized inference path that enables TF32 matmul (Hopper/Ampere)
and torch.compile (reduce-overhead) on the autoregressive GPT — the main
latency bottleneck. Run it with:
# Baseline
python scripts/evaluate.py --checkpoint checkpoints --tag default
# Optimized (fp16 GPT path + TF32 + compiled kernels)
python scripts/evaluate.py --checkpoint checkpoints --tag optimized --optimizeDefault vs Optimized (same 15-sentence set, speaker Mohamed, H100):
| Metric | Default | Optimized | Δ |
|---|---|---|---|
| RTF p50 | 0.362 | 0.355 | −1.9% |
| RTF p95 | 0.528 | 0.494 | −6.4% |
| TTFA p50 (ms) | 1194 | 1150 | −44 ms |
| UTMOS ↑ | 3.13 | 3.24 | +3.5% |
| CER | 0.255 | 0.173 | (within sampling variance) |
The optimization lowers RTF (p95 −6.4%) and TTFA while slightly improving UTMOS — quality is preserved. The CER/WER spread between runs is dominated by the sampling temperature (0.65), not the optimization.
Tried & rejected: Full fp16 on the HiFi-GAN decoder broke the fp32 conv filters in the speaker encoder (dtype mismatch). ONNX export of the autoregressive GPT is non-trivial (KV-cache + dynamic loop) and gave no reliable speedup over
torch.compilefor streaming, so TF32 + compile is the recommended path.
python scripts/gather_levantine_text.py
# → data/levantine_50k.txtSources:
- GU-CLASP Shami Corpus — 60K real Levantine sentences (Syrian, Lebanese, Palestinian, Jordanian)
- Synthetic code-switching templates (35K+ unique combinations)
Text processing pipeline:
Raw text
→ Unicode NFC + tatweel removal
→ Number verbalization (Levantine: مية not مئة, تلاتة, etc.)
→ ه → ة correction (nouns/adjectives, names — preserves والله, pronoun suffixes)
→ Partial diacritics on homographs (ضَلّ, هَلَّق, مِشْ, بِدِّي, etc.)
→ Levantine lexicon CSV overrides (148 entries)
python scripts/generate_lahgetna_data.py
# → data/synthetic_data/wavs/<spk_id>/*.wav + metadata.csv| Property | Value |
|---|---|
| Model | oddadmix/lahgtna-omnivoice-v2 |
| Language | apc — North Levantine Arabic (ISO 639-3) |
| Speakers | 10 (5M + 5F), 5,000 utterances each |
| Generation params | temperature=0.7, top_p=0.7, repetition_penalty=1.2 |
python scripts/prepare_dataset.py --skip_downloadFinal training data:
| Source | Language | Utterances | Est. Hours |
|---|---|---|---|
| Lahgtna synthetic (primary) | Levantine AR + EN CS | 50,000 | ~70 h |
| LibriSpeech clean-100 | English | 5,888 | ~20 h |
| Total | 55,888 | ~90 h |
CUDA_VISIBLE_DEVICES=0 python scripts/train.py
# Monitor:
tensorboard --logdir checkpoints/tensorboard --port 6006Training config (configs/finetune_xtts.yaml):
| Parameter | Value |
|---|---|
| Model | XTTS-v2 GPT backbone |
| Optimizer | AdamW, lr=5e-6 |
| Batch size | 4 (grad_accum=8 → effective 32) |
| Epochs | 30 |
| Checkpoint | Every 2,000 steps |
| # | ID | Name | Gender |
|---|---|---|---|
| 1 | spk_01_male | Badr | Male |
| 2 | spk_02_male | Mohamed | Male |
| 3 | spk_03_male | Saad | Male |
| 4 | spk_04_male | Rami | Male |
| 5 | spk_05_male | Fadi | Male |
| 6 | spk_06_female | Amina | Female |
| 7 | spk_07_female | Fatma | Female |
| 8 | spk_08_female | Lamyaa | Female |
| 9 | spk_09_female | Mona | Female |
| 10 | spk_10_female | Haneen | Female |
| Requirement | XTTS-v2 | F5-TTS | Kokoro |
|---|---|---|---|
| Native Arabic | ✅ | ❌ | ❌ |
| Code-switching | ✅ | ✅ | ❌ |
| Native streaming | ✅ | ❌ | partial |
| RTF < 0.3 | ✅ | ❌ (real ~3.0) | ✅ |
| VRAM ≤ 3 GB | ✅ | ❌ | ✅ |
leva-tts/
├── leva_tts/
│ ├── text/
│ │ ├── processor.py ← TextProcessor (normalization + lexicon)
│ │ └── lexicon.py ← CSV loader
│ ├── inference/
│ │ └── engine.py ← LevaTTSEngine (streaming, DeepSpeed)
│ ├── server/
│ │ └── app.py ← FastAPI (POST /synthesize, WS /stream)
│ ├── pipecat_plugin/
│ │ └── leva_tts_service.py ← Pipecat TTSService
│ └── training/
│ └── finetune.py ← XTTS-v2 GPT fine-tune
├── scripts/
│ ├── train.py ← Fine-tuning
│ ├── inference.py ← CLI synthesis (rich UI)
│ └── evaluate.py ← Full evaluation suite
├── configs/
│ └── finetune_xtts.yaml
├── data/
│ └── levantine_lexicon.csv ← 148 Levantine dialect overrides
├── reference_audios/
│ ├── references.json ← 10 speaker reference configs
│ └── *.wav / *.mp3 ← Reference recordings
└── app.py ← Gradio demo
- Code (this repository and the
leva-ttspackage): Apache-2.0 — seeLICENSE. - Model weights (
mohammedaly22/leva-ttson HuggingFace): the fine-tuned XTTS-v2 weights inherit Coqui's non-commercial license (CPML) from the base model and are for research / non-commercial use.
@misc{leva-tts-2026,
title = {Leva-TTS: Levantine Arabic / English Code-Switching TTS},
author = {Mohammed Aly},
year = {2026},
url = {https://huggingface.co/mohammedaly22},
}
