poto-tts
English text-to-speech that pronounces Ghanaian words properly. Kwabena, Achimota, the Okuapenhene, Nyankpani, Gyasi — names, places and titles that every general TTS mangles.
To be clear about what this is not: the voices do not have a Ghanaian accent. They are Kokoro's speakers — British and American — and this library changes what they say, not who they sound like. A Ghanaian-sounding voice needs a model trained on Ghanaian speech, which is a different problem. What you get here is an English voice that no longer stumbles over Ghanaian words.
general TTS poto-tts
Kwabena kwˈeɪbnə kwabˈina
Achimota ɐtʃɪmˈoʊɾə atʃimˈota
Okuapenhene ˈoʊkjuːˌeɪpənhˌiːn okwapenhˈene
Nyankpani nˌaɪɐŋkpˈɑːni njankpˈani
Gyasi dʒaɪʲˈɑːsi dʒˈasi
pip install poto-tts
poto-tts "Kwabena went to Achimota" -o out.wav
from poto_tts import load
tts = load() # downloads the voice on first use
tts.save("The Okuapenhene met Nana Bawumia", "out.wav")
How it works
Pronunciation comes from a Ghanaian lexicon compiled into espeak's own dictionary. Your text is sent to the model unchanged:
The dictionary holds 44,321 Ghanaian words -- names, places, titles, Twi and Ga
loans, food, money, everyday coinage. Every other word is left to espeak's ordinary
English. So Kwabena, Achimota and Okuapenhene come from the lexicon, while
bus, passed and through are pronounced as any English TTS would.
That division is deliberate and was arrived at the hard way. The upstream lexicon
covers the whole language -- bus and way have entries too, recording the Ghanaian
accent of an English word rather than a pronunciation that cannot be derived. Using
all of it made every word of every sentence Ghanaian: from as /frɔm/, on as /an/.
That is a different product from an English voice that says Ghanaian names properly.
poto_tts/data/ghanaian-words.txt is the subset used, and
tools/classify_lexicon.py regenerates it.
To see what will be sent to espeak, without loading a model:
poto-tts --phonemes "Nana Addo met Kwame Nkrumah"
One more layer sits after those two: espeak/en-gh, a voice file whose rules give the
Ghanaian words their vowel qualities and a tapped r. It is applied to whatever the
dictionary and the rules produced, so it reaches ordinary English too -- late comes
out /let/. It also does one job that is easy to miss: it maps A: to a, which
suppresses espeak's British linking-r. Without it Okaija reads as /okɑːɹidʒɑːɹ/, with
an r inserted inside a name that has none.
A rule in that file may not contradict the lexicon. Rules run on every word after
lookup, so a rule whose source phoneme our own entries can emit overwrites what the
lexicon said, invisibly -- the once came out /da/ though the lexicon records
[ð, ə]. tests/test_espeak_voice.py fails if such a rule appears.
Changing any of it is docs/CUSTOMISING.md.
Voices
Eight British speakers:
| female | male |
|---|---|
| Grace, Comfort, Mercy, Patience | Emmanuel, Isaac, Ebenezer, Bright |
tts = load(voice="Emmanuel")
poto-tts --voices
British, and only British, for two reasons. It is the variety Ghanaian English is closest to -- non-rhotic, with vowels in roughly the same places. And the pronunciations shipped here are shaped for it: entries are read with espeak's British phoneme table, so the phonemes Kokoro receives are non-rhotic and use /a/ where American English has /æ/. Kokoro's American speakers were trained on American phonemes, and handing them these is a mismatch users should not have to find by ear.
Kokoro's twenty American speakers and twenty-five other-language speakers are still
in the model and still reachable by their own names -- load(voice="af_heart") -- for
anyone who wants to try. They are simply not offered.
Which words came from the lexicon
tts = load()
tts.annotate("Yaw went to Kumasi by bus")
# [('Yaw', True), ('went', False), ('to', False), ('Kumasi', True), ('by', False), ('bus', False)]
tts.coverage("Yaw went to Kumasi by bus") # 0.33
Worth having because the audio cannot show it: a name the lexicon supplied and a name espeak guessed at sound equally confident, so when one is wrong there is no way to tell whether the entry is missing or the entry is bad.
Reporting only -- speak() never calls either, so nothing about the audio depends on
them, and a port that wants the same information reads
poto_tts/data/ghanaian-words.txt and does one set lookup per word.
Cross-platform
sherpa-onnx runs on Android, iOS, WebAssembly, C++, C, Go, C#, Java, Kotlin, Rust, Dart and Swift, and they all get the same pronunciations as Python, because the front-end is data rather than code. There is no Python-only path to fall back from: the dictionary and the voice file are the front-end.
Ship four things and send plain text with lang=en-gh:
onnx/model.onnx the model
voices.bin speaker embeddings
tokens.txt phoneme -> id
espeak-ng-data/ the dictionary and the voice -- the Ghanaian part
Swap in a stock
espeak-ng-dataand you get a working voice that mispronounces every Ghanaian name, with no error. That directory is the deliverable.
examples/bare_sherpa_onnx.py does exactly that with no poto_tts import at all --
it is there to keep this claim testable rather than merely stated.
What does need Python is authoring a pronunciation: poto-tts dict compiles the
lexicon into espeak-ng-data and wants the espeak-ng binary as well. That is a
build step you run once; nothing on the device does it.
sherpa-onnx docs · the voice on the Hub
Web interface and REST API
pip install 'poto-tts[api]'
poto-tts serve # then open http://localhost:8080
Paste text and hear it, or upload a CSV for batch work and get back a ZIP of WAVs with a manifest. One HTML file, no build step, no CDN — it renders on a laptop or a Pi with no internet.
text,voice,filename
"Kwabena went to Achimota",Grace,kwabena
"The Okuapenhene met Nana Bawumia",Emmanuel,durbar
text is the only required column, and a single-column file with no header works too.
Batches are capped at 500 rows and refused rather than trimmed.
| endpoint | |
|---|---|
GET / |
the web interface |
GET /speak?text=… · POST /speak |
WAV |
POST /batch |
CSV → ZIP of WAVs plus a manifest |
GET /voices · GET /backends · GET /health |
|
GET /platforms |
how to run the same voice off-server |
Contributing a pronunciation
The lexicon will always be missing somebody's name, and the fastest way to fix that is for the person who knows the name to say so.
- a Ghanaian word missing or wrong -> ghana-english-g2p, where pronunciations live so ASR and TTS share one answer. An issue is enough; you do not need IPA to report that a name is wrong.
- a Ghanaian word read as if it were English, or an English word said the Ghanaian
way -> here: it is in or out of
poto_tts/data/ghanaian-words.txtwhen it should not be.
Please listen to an entry before opening a PR -- CONTRIBUTING.md has the commands. An entry that reads correctly and sounds wrong is worse than a missing one, because the missing word is obviously missing.
Changing how a word is said
The lexicon will still miss your grandmother's name. Put it in a TSV -- Ghana IPA, space-separated -- and rebuild the dictionary:
Owusu o w u s u
Tetteh t ɛ t ɛ
pip install 'poto-tts[lexicon]'
poto-tts dict --out build/espeak-ng-data --ghanaian-stress --extra my_words.tsv
Where each kind of change belongs -- a single word, a sound across the whole accent, or the lexicon upstream -- and why a rule must never overrule the lexicon: docs/CUSTOMISING.md.
Models
Fetched from the Hub through huggingface_hub, so downloads are counted:
ghananlpcommunity/poto-tts-kokoro-gh.
Each repo's config.json names its own files, speakers and licence, so a new voice needs
no release of this library:
tts = load(repo_id="your-org/your-voice")
POTO_TTS_CACHE moves model files off ~/.cache.
Training a voice
tools/ holds a full Piper training pipeline — dataset fetch, filtering, speaker
clustering, forced alignment, punctuation from measured pauses, training, export. It was
used to train a Ghanaian voice on 85 hours of broadcast speech, and that voice is not
shipped: at 45,000 steps it sounded worse than Kokoro with a good front-end, and its
corpus is broadcast recordings whose speakers never consented to being modelled. The
pipeline is kept because it works on any dataset you hold rights to.
Thresholds in it are measured rather than chosen, and the comments record the distributions they came from — including the ones that were wrong first.
Credits
sherpa-onnx (k2-fsa) · Kokoro-82M (hexgrad, Apache-2.0) · ghana-english-g2p (GhanaNLP) · espeak-ng, whose voice files turn out to be a perfectly good place to keep an accent
MIT, except the bundled espeak-ng-data, which is GPL-3.0.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file poto_tts-0.6.1.tar.gz.
File metadata
- Download URL: poto_tts-0.6.1.tar.gz
- Upload date:
- Size: 189.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
75b259b3ff1f07ddf681b54b66163184e16ed3bd022c2d96cdbce4f435da0d5c
|
|
| MD5 |
9e8000ea399d3af9eba3b8eb15e5421b
|
|
| BLAKE2b-256 |
6ec696068f07422cf3e5c9d7415e80368ca3d2c17e78667d048440e5638015c8
|
Provenance
The following attestation bundles were made for poto_tts-0.6.1.tar.gz:
Publisher:
release.yml on GhanaNLP/poto-tts
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
poto_tts-0.6.1.tar.gz -
Subject digest:
75b259b3ff1f07ddf681b54b66163184e16ed3bd022c2d96cdbce4f435da0d5c - Sigstore transparency entry: 2533428668
- Sigstore integration time:
-
Permalink:
GhanaNLP/poto-tts@7346137780b91be64167d0b9caa7d9ff937b892c -
Branch / Tag:
refs/tags/v0.6.1 - Owner: https://github.com/GhanaNLP
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7346137780b91be64167d0b9caa7d9ff937b892c -
Trigger Event:
push
-
Statement type:
File details
Details for the file poto_tts-0.6.1-py3-none-any.whl.
File metadata
- Download URL: poto_tts-0.6.1-py3-none-any.whl
- Upload date:
- Size: 180.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6f108344e8b6800b551b23a20ba73832a63158dff846f52b0760120a5c8d4486
|
|
| MD5 |
fd23b969187e033e1a25f336b928778b
|
|
| BLAKE2b-256 |
adc0dbda39fbe28705fe0148e49373e7f21b23fe1476236ae82a15ef8c452caf
|
Provenance
The following attestation bundles were made for poto_tts-0.6.1-py3-none-any.whl:
Publisher:
release.yml on GhanaNLP/poto-tts
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
poto_tts-0.6.1-py3-none-any.whl -
Subject digest:
6f108344e8b6800b551b23a20ba73832a63158dff846f52b0760120a5c8d4486 - Sigstore transparency entry: 2533428827
- Sigstore integration time:
-
Permalink:
GhanaNLP/poto-tts@7346137780b91be64167d0b9caa7d9ff937b892c -
Branch / Tag:
refs/tags/v0.6.1 - Owner: https://github.com/GhanaNLP
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7346137780b91be64167d0b9caa7d9ff937b892c -
Trigger Event:
push
-
Statement type: