afrispeech-synth
Turn Google Gemini TTS into a speech-dataset factory for any African language — from raw text to a training-ready HuggingFace dataset, with one command.
Most African languages have no recorded speech corpus. Gemini TTS can speak many of them well enough to bootstrap one, but a usable dataset is not just API calls: you need text in the language, the right sentences rather than all of them, orthography the model reads correctly, and output packaged so a trainer can consume it. This does all four around Gemini.
pip install "afrispeech-synth[gemini]"
export GEMINI_API_KEY=... # https://aistudio.google.com/apikey
afrispeech-synth run examples/twi.yaml
Hear what it produces — one sample per language, 215 languages, a different voice each.
What's around Gemini
| The voice | Google Gemini TTS — gemini-3.1-flash-tts-preview, 30 voices, a paid Google API you bring your own key to. Every dataset built with this tool so far was spoken by it. |
| The text | africa-corpus-builder — source text for 693 African languages, so a language with no corpus of its own still has a starting point |
| The orthography | africa-g2p — phoneme tables for 400 languages. Feeding Gemini a language's raw orthography gets you its guess at ɔ, ɛ and ŋ; feeding it the universal grapheme set gets you the sound |
| The names | afriso — resolves Twi, tw, aka, Asante Twi to one code all of the above agree on |
| The selection | Greedy set cover over phoneme units — on Twi, 4,141 candidate sentences reduced to 115 at full phoneme coverage. 97% fewer Gemini calls for the same coverage |
Gemini is the reference backend, not a dependency of the design — swapping it out is one method and one line of config — but it is what this was built on and tuned against, and the parts above exist because raw API calls alone did not produce a dataset worth training on.
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:
afrispeech-synth run --lang Zulu --source corpus:zul --out out/zul
If your language has no corpus text, it still works — supply your own:
afrispeech-synth run --lang Afar --source file:my_afar_sentences.txt --out out/aar
afrispeech-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:
afrispeech-synth langs --search zulu # zul Zulu ready Atlantic-Congo
afrispeech-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; afrispeech-synth card config.yaml shows the transform that will be recorded:
afrispeech-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:
afrispeech-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 afrispeech-synth # core
pip install "afrispeech-synth[gemini]" # + the Gemini TTS backend
pip install "afrispeech-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
afrispeech-synth init Yoruba # writes yor.yaml
afrispeech-synth run yor.yaml --dry-run # select sentences, call no API
afrispeech-synth run yor.yaml
Or stay on the command line
afrispeech-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/afrispeech-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:
afrispeech-synth samples --limit 20 # generate clips into space/
afrispeech-synth space # build space/index.html, preview locally
afrispeech-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
afrispeech-synth select config.yaml # source + cover, writes sentences.txt
afrispeech-synth synth config.yaml # synthesise (resumes by default)
afrispeech-synth package config.yaml # parquet + manifest + card
afrispeech-synth push config.yaml --repo org/name
afrispeech-synth langs --ready # languages that need no text from you
afrispeech-synth langs --search yor # what one language needs
afrispeech-synth voices # the 30 voices you can pick from
Interrupted? Run the same command again — finished clips are skipped.
As a library
from afrispeech_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 afrispeech_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 afrispeech_synth import tts
from afrispeech_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/afrispeech-synth
cd afrispeech-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 afrispeech-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 | |
|---|---|---|---|
| afrispeech_synth-0.1.0.tar.gz | 48.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| afrispeech_synth-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 99.9 kB
Release files / afrispeech_synth-0.1.0.tar.gz
| Download URL | afrispeech_synth-0.1.0.tar.gz |
|---|---|
| Size | 48.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a53c31dcfd08bb9fe34a858a78226db6134fa1092fdfe9235f3234457f2e0ee9
|
|
BLAKE2b-256 checksum How to use checksums |
ac135681ce1ee297fac90232879299a542f5ebd1684f4446ef1be81518981614
|
| 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 / afrispeech_synth-0.1.0-py3-none-any.whl
| Download URL | afrispeech_synth-0.1.0-py3-none-any.whl |
|---|---|
| Size | 51.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d2ffcb274250f2f162b12b15752efe43b4b426ce62dddf5c5454c01de5999fc0
|
|
BLAKE2b-256 checksum How to use checksums |
4bab197146edd4d68074a167cefd0af8d5b4b85bf39105ed8963347f1cff9684
|
| 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