Skip to main content

stable-twi-tts

Speech synthesis for Twi and Ghanaian English, running on ONNX — a ~50 MB dependency instead of a ~2 GB PyTorch install, many times realtime on a laptop CPU, and portable to Linux, macOS, Windows, ARM and mobile.

Two things it does that a generic TTS wrapper doesn't:

  • Voices are ranked by measured intelligibility, not by how much audio they were trained on. The checkpoint holds 1,555 pseudo-speakers; the 12 offered by name were selected by synthesising held-out text and scoring it with a phoneme recogniser. Hours turned out to be a poor predictor.
  • Batch generation is first-class. Point it at a .csv of 50,000 lines and it parallelises, resumes, records a manifest, and doesn't abort the run because line 4,012 had a character it couldn't pronounce.
pip install "stable-twi-tts[twi,eng]"

No system packages, no manual downloads. twi brings the Twi front-end (ghana-g2p); eng brings libespeak-ng 1.51 bundled in a wheel for English words; the model fetches itself from a GitHub release over the standard library with checksums verified.

The espeak version is pinned deliberately, not incidentally: this model was trained on 1.51, and espeak's English changes between releases — 1.52 differs on 7.1% of words, 1.53 on 2.4%. A system espeak-ng still works if you prefer it, and warns when its version is not 1.51. The eng extra is separate because espeak-ng is GPL-3.0 while this package is MIT, so taking on that obligation should be your choice.

stable-twi-tts --voice twi-6 --text "Akwaaba, wo ho te sɛn?" --out hello.wav

That fetches the published voice (~80 MB) on first run and caches it. Pass --model <dir> to use a local voice directory instead.

Listen

Every voice, same two sentences

So you can compare voices directly rather than across different content.

TwiAkwaaba, wo ho te sɛn? Me da wo ase paa. Code-switchedMepɛ sɛ mesua [computer science] wɔ [University of Ghana].

voice Twi sample code-switch sample twi-only err code-switch err hours
twi-1 ▶ play ▶ play 34% 60% 6.9
twi-2 ▶ play ▶ play 29% 61% 5.8
twi-3 ▶ play ▶ play 29% 61% 11.2
twi-4 ▶ play ▶ play 32% 61% 8.5
twi-5 ▶ play ▶ play 28% 61% 5.9
twi-6 ▶ play ▶ play 27% 61% 2.7
twi-7 ▶ play ▶ play 27% 61% 3.3
twi-8 ▶ play ▶ play 30% 61% 2.4
twi-9 ▶ play ▶ play 28% 66% 3.5
twi-10 ▶ play ▶ play 30% 64% 4.4
twi-11 ▶ play ▶ play 30% 63% 9.5
twi-12 ▶ play ▶ play 30% 64% 3.4

Error figures are round-trip phoneme error: synthesise, re-recognise, compare against what was asked for. Lower is better; the real-audio floor is 25.9% for Twi.

Range of text

text sample
greeting Akwaaba! Yɛma wo akwaaba wɔ Ghana. ▶ play
statement Ghana yɛ ɔman a ɛwɔ Afrika atɔeɛ fam. ▶ play
question Wo din de sɛn? Wofiri he na woreba? ▶ play
long Anɔpa yi, ɔsoro abue na awia bɔ. Nnipa pii firi wɔn afie mu rekɔ adwuma, na mmɔfra nso rekɔ sukuu. ▶ play
numbers Yɛn nsa kaa nnipa apem ne ahanum wɔ ɔmantam no mu. ▶ play
news Ɔkyerɛkyerɛni no kaa sɛ [the examination] bɛba [next week]. ▶ play
institution [Bank of Ghana] abɔ [interest rate] no so bio. ▶ play
english Good morning, and welcome to the news. ▶ play

These are epoch-7 samples, not a finished voice. Twi is solid; English is audibly weaker and band-limited to 8 kHz because its training audio was 16 kHz where Twi's was 24 kHz.

Web interface

A local GUI for people who would rather not use a terminal — and for turning documents into speech without writing a script.

pip install "stable-twi-tts[web,twi,eng]"
stable-twi-tts-web            # http://127.0.0.1:7860

Three ways in — type text, upload a PDF, or give a URL — and two ways out: a single clip, or batch mode that splits into sentences and returns one joined wav or a zip of separate files with a manifest.

Extraction and synthesis are deliberately separate steps: a PDF or web page becomes editable text first, so you can fix a heading, drop a footer or bracket the English before anything is spoken. Batch progress streams as it goes, because a 40-page PDF otherwise looks like a hang. The voice picker shows each voice's measured error, since that is the part worth choosing on.

It binds to 127.0.0.1 by default. --host 0.0.0.0 exposes it, and the URL-fetch endpoint then makes requests from your machine on a caller's behalf — private, loopback and link-local addresses are refused, but do not put this on an untrusted network.

Speak something

stable-twi-tts --text "Akwaaba, wo ho te sɛn?" --out hello.wav
stable-twi-tts --language eng --text "Good morning, Accra." --out en.wav
from stable_twi_tts import StableTwiTTS

tts = StableTwiTTS.from_pretrained()          # downloads and caches the published voice
tts.synthesize("Akwaaba, wo ho te sɛn?", voice="twi-6").save("hello.wav")

# or point at a local voice directory
tts = StableTwiTTS("voices/my_voice")

Choosing a voice

stable-twi-tts --list-voices
voice    lang   hours  code-switch  twi-only  source
twi-1    twi     6.88        59.8%     33.5%  spk_0016
twi-2    twi     5.80        60.7%     29.1%  spk_0080
twi-3    twi    11.24        61.0%     29.1%  spk_0006
...
twi-6    twi     2.72        61.2%     26.8%  spk_0002
twi-9    twi     3.51        65.5%     28.1%  spk_0165

Voices are ranked by measured intelligibility, not by training hours — and the two disagree sharply. Each voice synthesised the same held-out text, which was then re-recognised and scored for phoneme error: Twi with the Ghana phoneme ASR, the English spans with KoelLabs. 30 of the 207 Twi pseudo-speakers were measured.

Two rankings, because they do not substitute for each other:

use tier pick
text mixing English into Twi tiers.codeswitch twi-1 (59.8%)
pure Twi tiers.twi_only twi-6 (26.8%, floor is 25.9%)

twi-1 is the best code-switch voice but 21st of 30 on pure Twi; twi-9 is 3rd on Twi and among the worst on code-switch. Had we ranked by hours — as the first version did — the best mixed-text voice would have been buried and two of the three best Twi voices excluded entirely (they have under 3.3 h each).

voices.json records every measurement, so the ranking is inspectable rather than asserted. Unlisted speakers are reachable by raw index (--voice 42), unmeasured.

There are no separate English voices. These are Twi voices judged on how well they also handle English, which is what reading real Ghanaian text requires.

Batch generation

Any of .txt (one utterance per line), .csv/.tsv (needs a text column), or .jsonl.

stable-twi-tts --input corpus.csv --out synth/ --workers 8
50000 utterances -> synth/  (8 workers)
  [25/50000] 14.2/s  eta 58.7 min
  ...
49987 written, 0 skipped, 13 failed in 57.4 min
41.20 h of audio  (2586x realtime)
manifest: synth/manifest.jsonl

Per-row columns override the defaults, so one file can mix voices and languages:

id,text,voice,language
greet_01,"Akwaaba, wo ho te sɛn?",twi-1,twi
news_01,"Good morning, welcome to the news.",eng-1,eng
mix_01,"Mepɛ sɛ mesua [computer science].",twi-2,mixed

Every run writes manifest.jsonl, one record per utterance:

{"id": "greet_01", "status": "ok", "path": "synth/greet_01.wav", "duration": 1.83, "voice": "twi-1", "n_phonemes": 17}
{"id": "bad_09", "status": "error", "error": "PhonemeError: the model has no symbol for: ['ʈ']"}

Re-running skips what already exists, so an interrupted 50k job resumes where it stopped (--overwrite to force). Failures are per-item — a corpus of 50,000 lines does not lose 49,999 outputs because one line was unpronounceable.

Text really does become phonemes

The model reads phoneme ids, never letters, and it only knows the phonemes its training targets contained. So this package reproduces the training front-end exactly:

language phonemiser
Twi ghana-g2p — the same library that produced the Twi targets
English espeak-ng en-us — the same phonemiser that produced the English targets

Do not substitute a different English G2P. This is the one change most likely to quietly ruin output. An earlier version of this pipeline folded English into the Ghanaian inventory (θt, æa). It produced valid-looking IPA, disagreed with the training targets on 51% of units, and the model scored 68.6% phoneme error where Twi scored 25.6%. Nothing errored; it just sounded wrong. Same function on both sides is the difference between working and not.

Note that the accent lives in the audio, not the symbols. English input is canonical (θ æ ɹ eɪ), and the Ghanaian accent comes from the voice.

Two deployment paths

path phoneme error needs
Python + onnxruntime 30.2% Python, ghana-g2p, espeak-ng for English — unlimited vocabulary
Native port + libespeak-ng matches Python ~30 lines + 1.56 MB — no Python, unlimited vocabulary

The native path exists because the Python dependency is only the front-end, and Twi's grapheme-to-phoneme is a 42-entry longest-match table — verified identical to ghana-g2p across 20,000 words. So a Twi-only app needs no Python, no espeak and no lexicon. English cannot be ported that way (7,132 context rules plus 5,794 exceptions), so it links libespeak-ngtools/bundle_espeak_english.sh strips the 31 MB install down to the 1.56 MB English needs, verified working. For targets that cannot link native code, mobile/english_lexicon.json.gz holds 124,926 precomputed pronunciations in 0.88 MB, covering 98.2% of tokens but dropping the rest.

mobile/ has Kotlin and Swift references plus 13 test vectors, so a port is verified rather than hoped-for. Those vectors are worth taking seriously: writing them caught two bugs in the specification that a 20,000-word equivalence test had missed, and both would have produced fluent-sounding wrong audio rather than an error.

A third path exists via sherpa-onnx, but neither of the above needs it

sherpa-onnx can load model.onnx directly, but it does its own text-to-phoneme step and espeak-ng has no Twi, so it needs the generated lexicon_ascii.txt plus two string replacements (ɔq, ɛx — Akan has no q or x, so they cannot collide with a real word). It measured ~36–38% phoneme error and covers 78k words, silently dropping anything outside that. Superseded by the native port, which is both better and simpler.

Building a voice directory

python tools/export_voice.py \
    --checkpoint runs/piper/checkpoints/best.ckpt \
    --train-config runs/piper/config.json \
    --manifest data/manifest.tsv \
    --out voices/my_voice \
    --top-n 10 --min-hours 1.0 --lexicon

Produces model.onnx, config.json, voices.json, tokens.txt and optionally lexicon.txt. Voices are ranked by hours, not clip count — Twi clips average 3.9 s and English 13.6 s, so ranking by clips would systematically flatter Twi speakers.

Honest status

  • Not on PyPI. Install from git; the package name is not registered yet.
  • The model is epoch 7, the best checkpoint of a run that had plateaued. val_mos rose steeply for three epochs (2.56 → 2.99) and then oscillated between 2.75 and 3.02 for six more with no upward trend; val_mel improved 0.005 across the same span and then reversed. Epoch 7's 3.02 is the peak, but epoch 3's 2.99 is within noise of it. More training at this data scale is unlikely to help — the round-trip numbers agree, with Twi drifting slightly worse (31.3% → 33.5%) while English improved (63.7% → 59.5%), i.e. the model trading between languages rather than improving. The gains left are in the data, not the schedule.
  • The Kotlin and Swift ports have not been compiled — the algorithm is verified against test vectors, but no one has built them on a device yet.
  • English is markedly weaker than Twi and this is a data problem, not a tuning one: English had a third of the utterances (43k vs 151k) at 3.5× the length, and VITS learns alignment per utterance.

Known limits

  • English voices are band-limited to 8 kHz. The English training audio was 16 kHz; the Twi was 24 kHz. Upsampling cannot invent the missing highs, so English voices sound duller than Twi ones. This is a property of the training data, not a bug.
  • Code-switching is extrapolation. No training utterance mixed languages within a sentence. It works, but it is not a trained capability, and the language token is per-utterance so a switched sentence has to pick one frame language.
  • Speakers are derived, not real identities. Pseudo-speakers from clustering. One real person may appear as two voices, and a voice is not a consented identity.
  • Long input is split on sentence boundaries and concatenated with a short pause. Training clips were 4–14 s and VITS attention degrades well beyond that.

License

Code MIT. The model and voices carry the licences of their training data — the Twi half is cc-by-nc-4.0, so the voices are non-commercial.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

stable_twi_tts-0.2.1.tar.gz (37.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

stable_twi_tts-0.2.1-py3-none-any.whl (33.6 kB view details)

Uploaded Python 3

File details

Details for the file stable_twi_tts-0.2.1.tar.gz.

File metadata

  • Download URL: stable_twi_tts-0.2.1.tar.gz
  • Upload date:
  • Size: 37.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.10.12

File hashes

Hashes for stable_twi_tts-0.2.1.tar.gz
Algorithm Hash digest
SHA256 7c458b309de4c55e3a700187056bd83f3c87af021d5fe9cecf957bd376bcb02c
MD5 3a4001b9f366049d51a38291663904af
BLAKE2b-256 17a0786128b4ad3cbb2788d0f7c0bf8ffc7c64f2d8afd1e1607afb56487b6a67

See more details on using hashes here.

File details

Details for the file stable_twi_tts-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: stable_twi_tts-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 33.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.10.12

File hashes

Hashes for stable_twi_tts-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 f9f739209a781ebc26210ac2d10a32eeaebb0103debf0d9b0e8c0b4ebf02d6c8
MD5 c2ef94844418db487608b8c029526614
BLAKE2b-256 2ece8f0c7f2a3002e619dbc850a6191b2f5be17d2443ce533a19d517f722bd47

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 files

0.2.0

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page