Skip to main content

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

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)

Source distribution for afrispeech-synth 0.1.0
File Size Uploaded
afrispeech_synth-0.1.0.tar.gz 48.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for afrispeech-synth 0.1.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release 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