SoroTTS: a natural voice for Yorùbá, Hausa, Igbo, and Nigerian Pidgin
SoroTTS (from sọ̀rọ̀, "speak" in Yorùbá) is a LoRA fine-tune of Orpheus-3B that gives natural, expressive text-to-speech in four Nigerian languages, Yorùbá, Hausa, Igbo, and Nigerian Pidgin, from a single adapter under the 4B "Tiny Titan" line.
It powers Naija Solar, a voice-first solar-sizing app that reads your plan aloud in your own language, and it was built for the Build Small Hackathon (Gradio × Hugging Face).
Off-the-shelf TTS for Nigerian languages is either robotic (Meta MMS) or simply absent. Orpheus is one of the most natural open speech LLMs, but it is English-first and speaks no Nigerian language. SoroTTS closes that gap. It keeps Orpheus's natural prosody while speaking Yorùbá, Hausa, Igbo, and the language no public model spoke before, Nigerian Pidgin, the everyday tongue of perhaps a hundred million people.
What it is
Architecture: Orpheus-3B, a Llama-3B backbone that predicts SNAC 24 kHz neural audio codes. About 3B parameters, under the 4B line.
Base:hypaai/hypaai_orpheus_v5 (Hypa-Orpheus), already adapted to Yorùbá, Hausa, and Igbo. SoroTTS adds Nigerian Pidgin and reinforces the other three.
This repo: a single LoRA adapter (r=64) covering all four languages, plus the tokenizer. Attach it to the base at inference time (see Quickstart).
Voices
Each clean data source becomes a named voice tag that you prepend to the text. An Orpheus speech LLM conditions on the voice tag, so more clean sources means more coverage. The cleanest source per language is voice 1:
Language
Cleanest voice
Other voices
Yorùbá
Yor1
Yor2, Yor3
Hausa
Hau1
Hau2, ... , HauBible (BibleTTS studio)
Igbo
Ibo1
Ibo2, Ibo3
Nigerian Pidgin
NaijaA
NaijaB, ... (one per speaker)
English falls back to a base Orpheus / Hypa voice (e.g. Eniola).
Tip: write Yorùbá and Igbo with diacritics (tone marks and dots) and Hausa with hooked letters (ɓ ɗ ƙ). The model learned diacritised text, so Ẹ kú àbọ̀ sounds far better than E ku abo.
1import os, torch, soundfile as sf
2from huggingface_hub import snapshot_download
3from unsloth import FastLanguageModel
4from peft import PeftModel
5from snac import SNAC
67# 1) load the base WITHOUT its bundled adapter, then attach SoroTTS8local = snapshot_download("hypaai/hypaai_orpheus_v5", ignore_patterns=["adapter_*"])9for f in("adapter_config.json","adapter_model.safetensors","adapter_model.bin"):10 p = os.path.join(local, f)11if os.path.exists(p):12 os.remove(p)1314model, tok = FastLanguageModel.from_pretrained(local, max_seq_length=2048, dtype=None, load_in_4bit=False)15model = PeftModel.from_pretrained(model,"Shinzmann/sorotts")# <- the SoroTTS adapter16FastLanguageModel.for_inference(model)17snac = SNAC.from_pretrained("hubertsiuzdak/snac_24khz").to("cuda").eval()1819# 2) Orpheus token scheme (must match training)20SOH, EOT, EOH, SOS, EOS, EOAI, OFF =128259,128009,128260,128257,128258,128262,12826621DEV =next(snac.parameters()).device
2223def_decode(codes):24 cl =lambda v:0if v <0else(4095if v >4095else v)# clamp so a stray code can't crash SNAC25 l1, l2, l3 =[],[],[]26for i inrange((len(codes)+1)//7):27 b =7* i
28 l1.append(cl(codes[b])); l2.append(cl(codes[b+1]-4096))29 l3.append(cl(codes[b+2]-2*4096)); l3.append(cl(codes[b+3]-3*4096))30 l2.append(cl(codes[b+4]-4*4096))31 l3.append(cl(codes[b+5]-5*4096)); l3.append(cl(codes[b+6]-6*4096))32 c =[torch.tensor(x).unsqueeze(0).to(DEV)for x in(l1, l2, l3)]33with torch.inference_mode():34return snac.decode(c).squeeze().cpu().numpy()3536defspeak(text, voice="Yor1", max_new_tokens=1024):37 ids = tok(f"{voice}: {text}", return_tensors="pt").input_ids
38 ids = torch.cat([torch.tensor([[SOH]]), ids, torch.tensor([[EOT, EOH]])], dim=1).to(model.device)39 out = model.generate(input_ids=ids, attention_mask=torch.ones_like(ids),40 max_new_tokens=max_new_tokens, do_sample=True, temperature=0.55,41 top_p=0.95, repetition_penalty=1.1,42 eos_token_id=[EOS, EOAI], use_cache=True)[0]# stop on EOS or EOAI43 sos =(out == SOS).nonzero(as_tuple=True)[0]44 seq = out[sos[-1].item()+1:]iflen(sos)else out
45 seq = seq[seq >= OFF]# keep only SNAC audio codes46 n =(seq.size(0)//7)*747return _decode([t.item()- OFF for t in seq[:n]])4849wave = speak("Ẹ kú àbọ̀ sí Nàìjíríà, orílẹ̀-èdè wa tó kún fún ìbùkún.", voice="Yor1")50sf.write("out.wav", wave,24000)
Two gotchas worth knowing (both handled above):
Stop on EOSorEOAI. The model often ends a clip with EOAI (128262); if you stop only on EOS it will run to the token cap and babble.
Keep only audio codes, and clamp them. Filter to tokens >= 128266, then clamp each SNAC value to [0, 4095]. A stray control token left in the stream corrupts the 7-token frame alignment and crashes the SNAC decoder.
Generate one sentence per call (each call has a roughly 2048-token, about 15 second budget) and stitch sentences with a short silence for longer text.
Training data
31,574 SNAC-tokenised clips (0.7 to 15 seconds each), streamed and ranked cleanest-first, across:
Because NaijaVoices (non-commercial) is included, the model is stamped cc-by-nc-sa-4.0. A permissive, commercial variant is possible by training on the CC-BY and CC-BY-SA sources only (the training script has a --commercial-only flag).
How it was trained
LoRA r=64, α=64, dropout 0, on all attention and MLP projections (the Orpheus-TTS standard), via Unsloth. Only 97.3M parameters are trained, 2.86% of the 3.40B-parameter model.
2 epochs = 7,894 steps, bf16, AdamW-8bit, lr 2e-4, 3% warmup, total batch 8 (8 per device, 1 gradient-accumulation step), on a single NVIDIA B200. The run takes about 31 minutes (1,894 s, roughly 33 samples per second) and converges to a train loss near 3.5.
Audio is encoded to Orpheus's 7-tokens-per-frame SNAC stream; each training sequence is
[SOH] voice: text [EOT][EOH] [SOAI][SOS] <snac codes> [EOS][EOAI].
Fully reproducible on Modal: streaming and SNAC-encoding the data, the LoRA training, and the Hub push all run as one serverless job. See modal/finetune_orpheus.py (train), serving_tts.py (serve), and test_sorotts.py (samples).
Intended use and limitations
Intended use: accessibility and voice interfaces in Nigerian languages, reading text aloud, IVR, narration. Built for Naija Solar.
Speed: Orpheus is autoregressive (roughly 10 to 25 seconds of compute per sentence on an A100 or H200). Great behind a "written-first" UI; for real time, serve via vLLM.
English is the base model's, not specifically fine-tuned here.
Pidgin has the thinnest data (one clean 4 to 5 hour corpus). It works because Pidgin is English-lexified and Orpheus has a strong English prior, but a dedicated single-speaker corpus would sharpen it.
Numbers and units: like most TTS, it reads digits and abbreviations literally. For natural speech, spell numbers as words (Naija Solar does this per language).
Ethics: this is a voice for accessibility. Do not use it to clone a real person's voice without consent.
License
CC-BY-NC-SA-4.0, inherited from the NaijaVoices data. The base model and the other corpora carry their own licenses; see each link above. Please attribute SoroTTS and share derivatives alike.
Acknowledgements
Built on Hypa-Orpheus, Orpheus-TTS, SNAC, and Unsloth, with data from NaijaVoices, Google WAXAL and FLEURS, BibleTTS, and the Nigerian Pidgin ASR Corpus.
Citation
bibtex
1@misc{ashinze2026sorotts,
2 title = {SoroTTS: a natural voice for Yoruba, Hausa, Igbo, and Nigerian Pidgin},
3 author = {Ashinze, Emmanuel},
4 year = {2026},
5 howpublished = {\url{https://huggingface.co/Shinzmann/sorotts}},
6 note = {LoRA fine-tune of Orpheus-3B, built for the Build Small Hackathon}
7}
By Ashinze Emmanuel, for the Build Small Hackathon.