This release has been yanked by its maintainers, and will be ignored by installers, except when explicitly specified.
Reason given by maintainers: Renamed to afrispeech-synth
africa-speech-synth
Generate a synthetic speech dataset for any African language — from raw text to a training-ready HuggingFace dataset — with one command.
Most African languages have no recorded speech corpus. This builds a synthetic one: it pulls
text for the language, picks the fewest sentences that still cover every phoneme, normalises
them with a rule-based G2P, synthesises audio, and packages it as sharded Parquet with the
audio embedded — playable in the dataset viewer, loadable with one load_dataset call.
pip install africa-speech-synth
export GEMINI_API_KEY=...
africa-speech-synth run examples/twi.yaml
Hear what it produces — one sample per language, 215 languages, a different voice each.
It sits on the rest of the AfriSpeech stack:
| Library | What it does here |
|---|---|
| africa-corpus-builder | Supplies source text for 693 African languages |
| africa-g2p | Phoneme normalisation for 400 languages, and the phoneme units selection covers |
| afriso | Resolves Twi, tw, aka, Asante Twi to one code all three libraries agree on |
The audio is generated by Google Gemini TTS
(gemini-3.1-flash-tts-preview, 30 voices) — a paid Google API you bring your own key to, and what
every dataset built with this tool so far was made with. It's the reference backend, not a
dependency of the design: swapping it out is one method and one line of
config.
For recorded African speech rather than synthetic, use afrispeech-selector.
Which languages work
| Languages | What you need to do | |
|---|---|---|
| Ready | 215 | Nothing. Name the language and run. |
| Bring your own text | 185 | Point a file: or hf: source at your own sentences. |
| No G2P table | 478 | Text is available; run with --normalise none. |
africa-g2p has phoneme tables for 400 African languages, africa-corpus-builder has text for 693, and 215 are in both — those need nothing from you but a name:
africa-speech-synth run --lang Zulu --source corpus:zul --out out/zul
If your language has no corpus text, it still works — supply your own:
africa-speech-synth run --lang Afar --source file:my_afar_sentences.txt --out out/aar
africa-speech-synth run --lang Afar --source hf:my-org/my-dataset#text --out out/aar
Any source works for any language, and you can mix them. If there's no G2P table either, add
--normalise none and the TTS model gets your raw text — everything else in the pipeline is
unchanged.
Check where yours stands:
africa-speech-synth langs --search zulu # zul Zulu ready Atlantic-Congo
africa-speech-synth langs --ready # the 215 that need nothing from you
Languages are matched by exact code only. Matching by name would add ~73 more, but it pairs different languages that share an alternative name — Tunisian Arabic text under an Algerian Arabic table, Basa of Cameroon under Basa of Nigeria — so those are left out rather than shipped wrong.
The pipeline
source ──► select ──► normalise ──► synthesise ──► package
text phoneme africa-g2p TTS API parquet + card
│ cover │ │ │
└ corpus:twi └ grapheme └ resumable └ push to the Hub
hf:org/ds#col or IPA + retries
file:x.txt
1 · Source. Combine any number of text sources; duplicates are dropped in first-seen order.
sources:
- corpus:twi # africa-corpus-builder
- hf:ghanaopenai/Ghana_English-Twi_Code-switching_Speech#transcript
- file:my_sentences.txt
2 · Select. Greedy set cover over phoneme units (default) or word units: the fewest sentences that still contain every sound. Every sentence dropped is a TTS call you don't pay for — on Twi it cut a 4,141-sentence pool to 115 at full phoneme coverage.
covered 208/208 phoneme units (100.0%) with 115 sentences
3 · Normalise. africa-g2p rewrites each sentence, then punctuation is reduced to . ?
! , — the marks a voice uses for phrasing. Everything else (apostrophes, asterisks marking
proper nouns, hyphens, colons, quotes) is read as a pause or spelled out, so it goes. Stored as
normalised_text — it's what the TTS model is actually asked to speak.
--normalise |
Twi example | When |
|---|---|---|
universal (default) |
ho bobea onyankopon |
Every phoneme written with the letter most African languages use for it (ɔ→o, ɛ→e). Plain a-z only — needs africa-g2p ≥ 0.2.3. |
grapheme |
hɔ bɔbea onyankopɔn |
The language's own phoneme units, multigraphs (ny, kp) kept whole and special characters preserved. |
ipa |
hɔ bɔbea oɲankʰopʰɔn |
Phonetic symbols — for phoneme-level ASR work, not for speech generation. |
none |
hɔ bɔbea Onyankopɔn |
Send the raw text. Works for any language, G2P table or not. |
Whichever you pick, look at the output before a full run. Neither transform is right for every language:
universal grapheme
twi na ho bobea ✓ na hɔ bɔbea ɔ/ɛ may be mispronounced
yor ngi˥ i˩bɛ˩rɛ˩ ✗ ní ìbẹ̀rẹ̀ ✓
afr eng die aarde khemaak ✗ en die aarde gemaak ✓
Universal is what the Ghana Twi dataset was built with and is the default, but it rewrites more
than it should in some languages. --dry-run prints the selection without spending any API
calls; africa-speech-synth card config.yaml shows the transform that will be recorded:
africa-speech-synth run --lang yor --source corpus:yor --normalise grapheme --dry-run
4 · Synthesise. Google Gemini TTS speaks the normalised transcript, in any of its 30 voices:
africa-speech-synth voices # Zephyr Bright, Kore Firm, Sulafat Warm, …
--voices Zephyr # one speaker
--voices Zephyr,Kore,Sulafat # rotated across utterances, so the dataset has three
Async, rate-limited, and resumable: every finished clip writes its own audio file plus a sidecar record, so an interrupted run restarts where it stopped. Retries back off on 429s and empty responses.
5 · Package. Parquet shards with audio bytes embedded (the viewer plays them inline), a
metadata.jsonl manifest, an LJSpeech export for Piper/VITS/MeloTTS, and a dataset card
generated from the run config.
Install
pip install africa-speech-synth # core
pip install "africa-speech-synth[gemini]" # + the Gemini TTS backend
pip install "africa-speech-synth[hf]" # + hf: text sources (pulls datasets)
afriso and africa-corpus-builder are not on PyPI yet:
pip install "afriso @ git+https://github.com/AfriSpeech/afriso"
git clone https://github.com/AfriSpeech/africa-corpus-builder
export AFRICA_CORPUS_PATH=$PWD/africa-corpus-builder
Both are optional. Without afriso you pass codes rather than names; without
africa-corpus-builder every source except corpus: still works.
Usage
Start from nothing
africa-speech-synth init Yoruba # writes yor.yaml
africa-speech-synth run yor.yaml --dry-run # select sentences, call no API
africa-speech-synth run yor.yaml
Or stay on the command line
africa-speech-synth run \
--lang Twi \
--source corpus:twi \
--cover phoneme \
--max-sentences 2000 \
--voices Zephyr,Puck \
--out out/twi \
--repo AfriSpeech/twi-synthetic-speech
Samples gallery
Hear it: AfriSpeech/africa-speech-synth-samples — one clip per language, each in a different voice. The page is built from this repo, so it has no repo of its own:
africa-speech-synth samples --limit 20 # generate clips into space/
africa-speech-synth space # build space/index.html, preview locally
africa-speech-synth space --repo org/my-samples # publish it
samples covers every ready language by default and spreads the 30 voices evenly across them,
so the gallery is also the voice catalogue. Clips are compressed to MP3 if ffmpeg is on PATH.
One stage at a time
africa-speech-synth select config.yaml # source + cover, writes sentences.txt
africa-speech-synth synth config.yaml # synthesise (resumes by default)
africa-speech-synth package config.yaml # parquet + manifest + card
africa-speech-synth push config.yaml --repo org/name
africa-speech-synth langs --ready # languages that need no text from you
africa-speech-synth langs --search yor # what one language needs
africa-speech-synth voices # the 30 voices you can pick from
Interrupted? Run the same command again — finished clips are skipped.
As a library
from africa_speech_synth import RunConfig, run
config = RunConfig(language="Twi", sources=["corpus:twi"], out="out/twi")
config.select.cover = "phoneme"
config.select.max_sentences = 2000
config.tts.voices = ["Zephyr", "Puck"]
run(config)
Individual stages are importable too:
from africa_speech_synth import resolve, Normaliser, stage_sources, stage_select
language = resolve("Twi")
normaliser = Normaliser(language, "grapheme")
sentences = stage_sources(config, language)
selection = stage_select(config, language, sentences, normaliser)
print(selection.coverage, len(selection.sentences))
Configuration
language: Twi # name, ISO 639-1/2/3 code, or alternative name
sources: # corpus: | hf: | file:
- corpus:twi
normalise: universal # universal | grapheme | ipa | none
out: out/twi
select:
cover: phoneme # phoneme | word | none
min_freq: 1 # only target units seen at least this often
max_sentences: 12000
min_chars: 20
max_chars: 240
seed: 0
tts:
backend: gemini
model: gemini-3.1-flash-tts-preview
voices: [Zephyr] # round-robined across utterances
context: speak in {language} accent
concurrency: 10
rpm: 200 # requests per minute, enforced
max_retries: 5
sample_rate: 24000
api_key_env: GEMINI_API_KEY # the key is read from the environment, never the file
package:
formats: [parquet, ljspeech]
shard_target_mb: 190
push_to: AfriSpeech/twi-synthetic-speech
private: false
CLI flags override any field. API keys are read only from the environment — never put one in a config file you intend to commit.
Output
out/twi/
├── data/train-00000-of-00019.parquet # audio bytes embedded, viewer-playable
├── metadata.jsonl # index, text, normalised_text, voice, shard
├── sentences.txt # the selected source sentences
├── README.md # generated dataset card
└── work/ # per-clip audio + sidecars (resume state)
from datasets import load_dataset
ds = load_dataset("parquet", data_files="out/twi/data/*.parquet", split="train")
ds[0]["audio"]["array"], ds[0]["text"], ds[0]["normalised_text"]
Adding a TTS backend
A backend is one method. Register it and it becomes available as tts.backend:
from africa_speech_synth import tts
from africa_speech_synth.tts.base import Clip, TTSBackend
class MyTTS(TTSBackend):
name = "mytts"
async def synth(self, text: str, voice: str) -> Clip:
audio = await my_api(text, voice) # bytes
return Clip(audio=audio, mime_type="audio/wav",
sample_rate=self.config.sample_rate, voice=voice)
tts.register("mytts", lambda: MyTTS)
Raise RetryableTTSError for rate limits and transient failures; the runner backs off
and retries. Raise TTSError for anything permanent.
A note on synthetic speech
Synthetic audio inherits the TTS model's accent and pronunciation errors, and a model trained only on it learns those too. Listen to a sample before training, and mix in real recordings from afrispeech-selector where they exist. The generated dataset card says plainly that the audio is model-generated — leave that in.
Development
git clone https://github.com/AfriSpeech/africa-speech-synth
cd africa-speech-synth
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest
The test suite runs the whole pipeline end to end against a mock backend, so it needs no API key and no network.
Acknowledgements
- Google Gemini TTS generates the audio. Clips produced with it are subject to Google's API terms; check them before publishing a dataset, and say plainly in the dataset card that the audio is model-generated — the generated card does.
- africa-g2p, built on Hartell's Alphabets of Africa (UNESCO, 1993) and Omniglot, for phonemisation.
- africa-corpus-builder and afriso (SIL ISO 639-3 + Glottolog) for text and language metadata.
License
MIT — the pipeline. Generated audio is yours subject to your TTS provider's terms, and source text keeps the licence of its own corpus.
Metadata
Release files for africa-speech-synth 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| africa_speech_synth-0.1.0.tar.gz | 48.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| africa_speech_synth-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 99.6 kB
Release files / africa_speech_synth-0.1.0.tar.gz
| Download URL | africa_speech_synth-0.1.0.tar.gz |
|---|---|
| Size | 48.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
67ba4db48f0d585ff1ba7f62e59c390aab02ad023cf2ebe9b2bb4275e83ff78e
|
|
BLAKE2b-256 checksum How to use checksums |
daf331f542d4e6333cbfb85091b6f159732e105836e4e40884518889a265376b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.
Transparency logRelease files / africa_speech_synth-0.1.0-py3-none-any.whl
| Download URL | africa_speech_synth-0.1.0-py3-none-any.whl |
|---|---|
| Size | 51.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a503808605ded50d9d0e84a3ff9531df33fe4f8211516edd0b4ab56e707329d3
|
|
BLAKE2b-256 checksum How to use checksums |
9d7527a2e171a4c515bbc34cb3102a088f6e3bc17281708095cd9c6e511608d3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.
Transparency log