Skip to main content
Yanked

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

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)

Source distribution for africa-speech-synth 0.1.0
File Size Uploaded
africa_speech_synth-0.1.0.tar.gz 48.0 kB Details

Built distribution (wheel)

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

Release 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

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