afrispeech-synth 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. afrispeech_synth-0.1.0/.github/workflows/release.yml +78 -0
  2. afrispeech_synth-0.1.0/.gitignore +17 -0
  3. afrispeech_synth-0.1.0/LICENSE +28 -0
  4. afrispeech_synth-0.1.0/PKG-INFO +405 -0
  5. afrispeech_synth-0.1.0/README.md +382 -0
  6. afrispeech_synth-0.1.0/examples/multilingual.sh +17 -0
  7. afrispeech_synth-0.1.0/examples/twi.yaml +41 -0
  8. afrispeech_synth-0.1.0/examples/yoruba.yaml +34 -0
  9. afrispeech_synth-0.1.0/pyproject.toml +46 -0
  10. afrispeech_synth-0.1.0/src/afrispeech_synth/__init__.py +23 -0
  11. afrispeech_synth-0.1.0/src/afrispeech_synth/card.py +268 -0
  12. afrispeech_synth-0.1.0/src/afrispeech_synth/cli.py +308 -0
  13. afrispeech_synth-0.1.0/src/afrispeech_synth/config.py +119 -0
  14. afrispeech_synth-0.1.0/src/afrispeech_synth/coverage.py +117 -0
  15. afrispeech_synth-0.1.0/src/afrispeech_synth/env.py +40 -0
  16. afrispeech_synth-0.1.0/src/afrispeech_synth/lang.py +147 -0
  17. afrispeech_synth-0.1.0/src/afrispeech_synth/normalise.py +150 -0
  18. afrispeech_synth-0.1.0/src/afrispeech_synth/package.py +164 -0
  19. afrispeech_synth-0.1.0/src/afrispeech_synth/pipeline.py +134 -0
  20. afrispeech_synth-0.1.0/src/afrispeech_synth/publish.py +39 -0
  21. afrispeech_synth-0.1.0/src/afrispeech_synth/samples.py +186 -0
  22. afrispeech_synth-0.1.0/src/afrispeech_synth/select.py +139 -0
  23. afrispeech_synth-0.1.0/src/afrispeech_synth/sources.py +190 -0
  24. afrispeech_synth-0.1.0/src/afrispeech_synth/space.py +286 -0
  25. afrispeech_synth-0.1.0/src/afrispeech_synth/synth.py +214 -0
  26. afrispeech_synth-0.1.0/src/afrispeech_synth/tts/__init__.py +36 -0
  27. afrispeech_synth-0.1.0/src/afrispeech_synth/tts/base.py +80 -0
  28. afrispeech_synth-0.1.0/src/afrispeech_synth/tts/gemini.py +92 -0
  29. afrispeech_synth-0.1.0/src/afrispeech_synth/voices.py +77 -0
  30. afrispeech_synth-0.1.0/tests/conftest.py +4 -0
  31. afrispeech_synth-0.1.0/tests/mock_backend.py +23 -0
  32. afrispeech_synth-0.1.0/tests/test_pipeline.py +375 -0
@@ -0,0 +1,78 @@
1
+ # Publish to PyPI when a version tag is pushed.
2
+ #
3
+ # Uses PyPI Trusted Publishing (OIDC), so there is no API token in this repository's secrets.
4
+ # GitHub mints a short-lived identity token that PyPI verifies against a publisher configured at
5
+ # https://pypi.org/manage/account/publishing/ with owner AfriSpeech, repo afrispeech-synth,
6
+ # workflow release.yml, environment pypi. Because the project does not exist on PyPI yet, that
7
+ # has to be added as a *pending* publisher — a normal one can only be attached to a project that
8
+ # already exists. The first successful run creates the project and converts it automatically.
9
+ #
10
+ # Tag and version must agree. A tag of v0.2.0 with `version = "0.1.0"` in pyproject.toml would
11
+ # upload the wrong release and PyPI does not allow re-uploading a version, so the check below
12
+ # fails the job before the build rather than after the upload.
13
+ name: release
14
+
15
+ on:
16
+ push:
17
+ tags: ["v*"]
18
+ workflow_dispatch: # lets you dry-run the build without tagging
19
+
20
+ jobs:
21
+ build:
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+ - uses: actions/setup-python@v5
26
+ with:
27
+ python-version: "3.11"
28
+
29
+ - name: Tag matches pyproject version
30
+ if: startsWith(github.ref, 'refs/tags/v')
31
+ run: |
32
+ tag="${GITHUB_REF_NAME#v}"
33
+ ver=$(python -c "import tomllib,pathlib; \
34
+ print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'])")
35
+ echo "tag=$tag pyproject=$ver"
36
+ [ "$tag" = "$ver" ] || { echo "::error::tag $tag != version $ver"; exit 1; }
37
+
38
+ - run: pip install build twine
39
+ - run: python -m build
40
+
41
+ # `twine check` catches a malformed README before upload. PyPI renders the long
42
+ # description itself and rejects the whole upload if it cannot.
43
+ - run: twine check dist/*
44
+
45
+ - name: Import the built wheel
46
+ run: |
47
+ pip install dist/*.whl
48
+ python -c "import afrispeech_synth; print(afrispeech_synth.__version__)"
49
+ afrispeech-synth --help > /dev/null
50
+ afrispeech-synth voices > /dev/null
51
+
52
+ # Run the suite against the *installed wheel*, from outside the source tree: the
53
+ # package data and entry points are the things most likely to be missing from a
54
+ # build, and a test run from the repo root would find the source on disk whether
55
+ # or not it was packaged.
56
+ - name: Test the installed wheel
57
+ run: |
58
+ pip install pytest
59
+ cd /tmp && python -m pytest "$GITHUB_WORKSPACE/tests" -q
60
+
61
+ - uses: actions/upload-artifact@v4
62
+ with:
63
+ name: dist
64
+ path: dist/
65
+
66
+ publish:
67
+ needs: build
68
+ if: startsWith(github.ref, 'refs/tags/v')
69
+ runs-on: ubuntu-latest
70
+ environment: pypi # must match the environment name set on PyPI's publisher form
71
+ permissions:
72
+ id-token: write # required for OIDC; without it the upload cannot authenticate
73
+ steps:
74
+ - uses: actions/download-artifact@v4
75
+ with:
76
+ name: dist
77
+ path: dist/
78
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,17 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ venv/
8
+ .env
9
+ work/
10
+ out/
11
+ corpora/
12
+ data/
13
+ space/
14
+ *.wav
15
+ *.mp3
16
+ *.parquet
17
+ .pytest_cache/
@@ -0,0 +1,28 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AfriSpeech
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ The MIT license above covers the afriso source code.
26
+
27
+ The bundled datasets (src/afriso/data/) are derived from third-party sources
28
+ and remain subject to their own licenses — see DATA_LICENSE.md.
@@ -0,0 +1,405 @@
1
+ Metadata-Version: 2.5
2
+ Name: afrispeech-synth
3
+ Version: 0.1.0
4
+ Summary: Build synthetic speech datasets for African languages with Google Gemini TTS: corpus text, phoneme-coverage sentence selection, africa-g2p orthography normalisation, and a training-ready HuggingFace dataset.
5
+ Project-URL: Homepage, https://github.com/AfriSpeech/afrispeech-synth
6
+ Project-URL: Issues, https://github.com/AfriSpeech/afrispeech-synth/issues
7
+ Author: AfriSpeech
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: african-languages,asr,dataset,g2p,gemini,speech-synthesis,tts
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: africa-g2p>=0.2.3
13
+ Requires-Dist: huggingface-hub>=0.20
14
+ Requires-Dist: pyarrow>=12.0
15
+ Requires-Dist: pyyaml>=6.0
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=7.0; extra == 'dev'
18
+ Provides-Extra: gemini
19
+ Requires-Dist: google-genai>=0.3.0; extra == 'gemini'
20
+ Provides-Extra: hf
21
+ Requires-Dist: datasets>=2.14; extra == 'hf'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # afrispeech-synth
25
+
26
+ **Turn [Google Gemini TTS](https://ai.google.dev/gemini-api/docs/speech-generation) into a
27
+ speech-dataset factory for any African language — from raw text to a training-ready
28
+ HuggingFace dataset, with one command.**
29
+
30
+ Most African languages have no recorded speech corpus. Gemini TTS can speak many of them
31
+ well enough to bootstrap one, but a usable dataset is not just API calls: you need text in
32
+ the language, the *right* sentences rather than all of them, orthography the model reads
33
+ correctly, and output packaged so a trainer can consume it. This does all four around Gemini.
34
+
35
+ ```bash
36
+ pip install "afrispeech-synth[gemini]"
37
+ export GEMINI_API_KEY=... # https://aistudio.google.com/apikey
38
+
39
+ afrispeech-synth run examples/twi.yaml
40
+ ```
41
+
42
+ **[Hear what it produces](https://huggingface.co/spaces/AfriSpeech/afrispeech-synth-samples)** —
43
+ one sample per language, 215 languages, a different voice each.
44
+
45
+ ## What's around Gemini
46
+
47
+ | | |
48
+ |---|---|
49
+ | **The voice** | **[Google Gemini TTS](https://ai.google.dev/gemini-api/docs/speech-generation)** — `gemini-3.1-flash-tts-preview`, [30 voices](#4--synthesise), a paid Google API you bring your own key to. Every dataset built with this tool so far was spoken by it. |
50
+ | **The text** | [africa-corpus-builder](https://github.com/AfriSpeech/africa-corpus-builder) — source text for **693 African languages**, so a language with no corpus of its own still has a starting point |
51
+ | **The orthography** | [africa-g2p](https://github.com/AfriSpeech/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 |
52
+ | **The names** | [afriso](https://github.com/AfriSpeech/afriso) — resolves `Twi`, `tw`, `aka`, `Asante Twi` to one code all of the above agree on |
53
+ | **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 |
54
+
55
+ Gemini is the reference backend, not a dependency of the design — [swapping it
56
+ out](#adding-a-tts-backend) is one method and one line of config — but it is what this was
57
+ built on and tuned against, and the parts above exist because raw API calls alone did not
58
+ produce a dataset worth training on.
59
+
60
+ For **recorded** African speech rather than synthetic, use
61
+ [afrispeech-selector](https://github.com/AfriSpeech/afrispeech-selector).
62
+
63
+ ---
64
+
65
+ ## Which languages work
66
+
67
+ | | Languages | What you need to do |
68
+ |---|--:|---|
69
+ | **Ready** | **215** | Nothing. Name the language and run. |
70
+ | **Bring your own text** | 185 | Point a `file:` or `hf:` source at your own sentences. |
71
+ | **No G2P table** | 478 | Text is available; run with `--normalise none`. |
72
+
73
+ africa-g2p has phoneme tables for **400** African languages, africa-corpus-builder has text
74
+ for **693**, and **215 are in both** — those need nothing from you but a name:
75
+
76
+ ```bash
77
+ afrispeech-synth run --lang Zulu --source corpus:zul --out out/zul
78
+ ```
79
+
80
+ **If your language has no corpus text, it still works — supply your own:**
81
+
82
+ ```bash
83
+ afrispeech-synth run --lang Afar --source file:my_afar_sentences.txt --out out/aar
84
+ afrispeech-synth run --lang Afar --source hf:my-org/my-dataset#text --out out/aar
85
+ ```
86
+
87
+ Any source works for any language, and you can mix them. If there's no G2P table either, add
88
+ `--normalise none` and the TTS model gets your raw text — everything else in the pipeline is
89
+ unchanged.
90
+
91
+ Check where yours stands:
92
+
93
+ ```bash
94
+ afrispeech-synth langs --search zulu # zul Zulu ready Atlantic-Congo
95
+ afrispeech-synth langs --ready # the 215 that need nothing from you
96
+ ```
97
+
98
+ Languages are matched by exact code only. Matching by name would add ~73 more, but it pairs
99
+ different languages that share an alternative name — Tunisian Arabic text under an Algerian
100
+ Arabic table, Basa of Cameroon under Basa of Nigeria — so those are left out rather than
101
+ shipped wrong.
102
+
103
+ ---
104
+
105
+ ## The pipeline
106
+
107
+ ```
108
+ source ──► select ──► normalise ──► synthesise ──► package
109
+ text phoneme africa-g2p TTS API parquet + card
110
+ │ cover │ │ │
111
+ └ corpus:twi └ grapheme └ resumable └ push to the Hub
112
+ hf:org/ds#col or IPA + retries
113
+ file:x.txt
114
+ ```
115
+
116
+ **1 · Source.** Combine any number of text sources; duplicates are dropped in first-seen order.
117
+
118
+ ```yaml
119
+ sources:
120
+ - corpus:twi # africa-corpus-builder
121
+ - hf:ghanaopenai/Ghana_English-Twi_Code-switching_Speech#transcript
122
+ - file:my_sentences.txt
123
+ ```
124
+
125
+ **2 · Select.** Greedy set cover over **phoneme** units (default) or **word** units: the fewest
126
+ sentences that still contain every sound. Every sentence dropped is a TTS call you don't pay for —
127
+ on Twi it cut a 4,141-sentence pool to 115 at full phoneme coverage.
128
+
129
+ ```
130
+ covered 208/208 phoneme units (100.0%) with 115 sentences
131
+ ```
132
+
133
+ **3 · Normalise.** `africa-g2p` rewrites each sentence, then punctuation is reduced to `.` `?`
134
+ `!` `,` — the marks a voice uses for phrasing. Everything else (apostrophes, asterisks marking
135
+ proper nouns, hyphens, colons, quotes) is read as a pause or spelled out, so it goes. Stored as
136
+ `normalised_text` — it's what the TTS model is actually asked to speak.
137
+
138
+ | `--normalise` | Twi example | When |
139
+ |---|---|---|
140
+ | `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. |
141
+ | `grapheme` | `hɔ bɔbea onyankopɔn` | The language's own phoneme units, multigraphs (`ny`, `kp`) kept whole and special characters preserved. |
142
+ | `ipa` | `hɔ bɔbea oɲankʰopʰɔn` | Phonetic symbols — for phoneme-level ASR work, not for speech generation. |
143
+ | `none` | `hɔ bɔbea Onyankopɔn` | Send the raw text. Works for any language, G2P table or not. |
144
+
145
+ **Whichever you pick, look at the output before a full run.** Neither transform is right for every
146
+ language:
147
+
148
+ ```
149
+ universal grapheme
150
+ twi na ho bobea ✓ na hɔ bɔbea ɔ/ɛ may be mispronounced
151
+ yor ngi˥ i˩bɛ˩rɛ˩ ✗ ní ìbẹ̀rẹ̀ ✓
152
+ afr eng die aarde khemaak ✗ en die aarde gemaak ✓
153
+ ```
154
+
155
+ Universal is what the Ghana Twi dataset was built with and is the default, but it rewrites more
156
+ than it should in some languages. `--dry-run` prints the selection without spending any API
157
+ calls; `afrispeech-synth card config.yaml` shows the transform that will be recorded:
158
+
159
+ ```bash
160
+ afrispeech-synth run --lang yor --source corpus:yor --normalise grapheme --dry-run
161
+ ```
162
+
163
+ **4 · Synthesise.** Google Gemini TTS speaks the normalised transcript, in any of its
164
+ **30 voices**:
165
+
166
+ ```bash
167
+ afrispeech-synth voices # Zephyr Bright, Kore Firm, Sulafat Warm, …
168
+ --voices Zephyr # one speaker
169
+ --voices Zephyr,Kore,Sulafat # rotated across utterances, so the dataset has three
170
+ ```
171
+
172
+ Async, rate-limited, and **resumable**: every finished clip writes its own
173
+ audio file plus a sidecar record, so an interrupted run restarts where it stopped. Retries back
174
+ off on 429s and empty responses.
175
+
176
+ **5 · Package.** Parquet shards with audio bytes embedded (the viewer plays them inline), a
177
+ `metadata.jsonl` manifest, an LJSpeech export for Piper/VITS/MeloTTS, and a dataset card
178
+ generated from the run config.
179
+
180
+ ---
181
+
182
+ ## Install
183
+
184
+ ```bash
185
+ pip install afrispeech-synth # core
186
+ pip install "afrispeech-synth[gemini]" # + the Gemini TTS backend
187
+ pip install "afrispeech-synth[hf]" # + hf: text sources (pulls datasets)
188
+ ```
189
+
190
+ `afriso` and `africa-corpus-builder` are not on PyPI yet:
191
+
192
+ ```bash
193
+ pip install "afriso @ git+https://github.com/AfriSpeech/afriso"
194
+
195
+ git clone https://github.com/AfriSpeech/africa-corpus-builder
196
+ export AFRICA_CORPUS_PATH=$PWD/africa-corpus-builder
197
+ ```
198
+
199
+ Both are optional. Without `afriso` you pass codes rather than names; without
200
+ africa-corpus-builder every source except `corpus:` still works.
201
+
202
+ ---
203
+
204
+ ## Usage
205
+
206
+ ### Start from nothing
207
+
208
+ ```bash
209
+ afrispeech-synth init Yoruba # writes yor.yaml
210
+ afrispeech-synth run yor.yaml --dry-run # select sentences, call no API
211
+ afrispeech-synth run yor.yaml
212
+ ```
213
+
214
+ ### Or stay on the command line
215
+
216
+ ```bash
217
+ afrispeech-synth run \
218
+ --lang Twi \
219
+ --source corpus:twi \
220
+ --cover phoneme \
221
+ --max-sentences 2000 \
222
+ --voices Zephyr,Puck \
223
+ --out out/twi \
224
+ --repo AfriSpeech/twi-synthetic-speech
225
+ ```
226
+
227
+ ### Samples gallery
228
+
229
+ **[Hear it: AfriSpeech/afrispeech-synth-samples](https://huggingface.co/spaces/AfriSpeech/afrispeech-synth-samples)**
230
+ — one clip per language, each in a different voice. The page is built from this repo, so it
231
+ has no repo of its own:
232
+
233
+ ```bash
234
+ afrispeech-synth samples --limit 20 # generate clips into space/
235
+ afrispeech-synth space # build space/index.html, preview locally
236
+ afrispeech-synth space --repo org/my-samples # publish it
237
+ ```
238
+
239
+ `samples` covers every ready language by default and spreads the 30 voices evenly across them,
240
+ so the gallery is also the voice catalogue. Clips are compressed to MP3 if `ffmpeg` is on PATH.
241
+
242
+ ### One stage at a time
243
+
244
+ ```bash
245
+ afrispeech-synth select config.yaml # source + cover, writes sentences.txt
246
+ afrispeech-synth synth config.yaml # synthesise (resumes by default)
247
+ afrispeech-synth package config.yaml # parquet + manifest + card
248
+ afrispeech-synth push config.yaml --repo org/name
249
+
250
+ afrispeech-synth langs --ready # languages that need no text from you
251
+ afrispeech-synth langs --search yor # what one language needs
252
+ afrispeech-synth voices # the 30 voices you can pick from
253
+ ```
254
+
255
+ Interrupted? Run the same command again — finished clips are skipped.
256
+
257
+ ### As a library
258
+
259
+ ```python
260
+ from afrispeech_synth import RunConfig, run
261
+
262
+ config = RunConfig(language="Twi", sources=["corpus:twi"], out="out/twi")
263
+ config.select.cover = "phoneme"
264
+ config.select.max_sentences = 2000
265
+ config.tts.voices = ["Zephyr", "Puck"]
266
+
267
+ run(config)
268
+ ```
269
+
270
+ Individual stages are importable too:
271
+
272
+ ```python
273
+ from afrispeech_synth import resolve, Normaliser, stage_sources, stage_select
274
+
275
+ language = resolve("Twi")
276
+ normaliser = Normaliser(language, "grapheme")
277
+ sentences = stage_sources(config, language)
278
+ selection = stage_select(config, language, sentences, normaliser)
279
+
280
+ print(selection.coverage, len(selection.sentences))
281
+ ```
282
+
283
+ ---
284
+
285
+ ## Configuration
286
+
287
+ ```yaml
288
+ language: Twi # name, ISO 639-1/2/3 code, or alternative name
289
+ sources: # corpus: | hf: | file:
290
+ - corpus:twi
291
+ normalise: universal # universal | grapheme | ipa | none
292
+ out: out/twi
293
+
294
+ select:
295
+ cover: phoneme # phoneme | word | none
296
+ min_freq: 1 # only target units seen at least this often
297
+ max_sentences: 12000
298
+ min_chars: 20
299
+ max_chars: 240
300
+ seed: 0
301
+
302
+ tts:
303
+ backend: gemini
304
+ model: gemini-3.1-flash-tts-preview
305
+ voices: [Zephyr] # round-robined across utterances
306
+ context: speak in {language} accent
307
+ concurrency: 10
308
+ rpm: 200 # requests per minute, enforced
309
+ max_retries: 5
310
+ sample_rate: 24000
311
+ api_key_env: GEMINI_API_KEY # the key is read from the environment, never the file
312
+
313
+ package:
314
+ formats: [parquet, ljspeech]
315
+ shard_target_mb: 190
316
+ push_to: AfriSpeech/twi-synthetic-speech
317
+ private: false
318
+ ```
319
+
320
+ CLI flags override any field. API keys are read only from the environment — never
321
+ put one in a config file you intend to commit.
322
+
323
+ ---
324
+
325
+ ## Output
326
+
327
+ ```
328
+ out/twi/
329
+ ├── data/train-00000-of-00019.parquet # audio bytes embedded, viewer-playable
330
+ ├── metadata.jsonl # index, text, normalised_text, voice, shard
331
+ ├── sentences.txt # the selected source sentences
332
+ ├── README.md # generated dataset card
333
+ └── work/ # per-clip audio + sidecars (resume state)
334
+ ```
335
+
336
+ ```python
337
+ from datasets import load_dataset
338
+ ds = load_dataset("parquet", data_files="out/twi/data/*.parquet", split="train")
339
+ ds[0]["audio"]["array"], ds[0]["text"], ds[0]["normalised_text"]
340
+ ```
341
+
342
+ ---
343
+
344
+ ## Adding a TTS backend
345
+
346
+ A backend is one method. Register it and it becomes available as `tts.backend`:
347
+
348
+ ```python
349
+ from afrispeech_synth import tts
350
+ from afrispeech_synth.tts.base import Clip, TTSBackend
351
+
352
+ class MyTTS(TTSBackend):
353
+ name = "mytts"
354
+ async def synth(self, text: str, voice: str) -> Clip:
355
+ audio = await my_api(text, voice) # bytes
356
+ return Clip(audio=audio, mime_type="audio/wav",
357
+ sample_rate=self.config.sample_rate, voice=voice)
358
+
359
+ tts.register("mytts", lambda: MyTTS)
360
+ ```
361
+
362
+ Raise `RetryableTTSError` for rate limits and transient failures; the runner backs off
363
+ and retries. Raise `TTSError` for anything permanent.
364
+
365
+ ---
366
+
367
+ ## A note on synthetic speech
368
+
369
+ Synthetic audio inherits the TTS model's accent and pronunciation errors, and a model trained
370
+ only on it learns those too. Listen to a sample before training, and mix in real recordings from
371
+ [afrispeech-selector](https://github.com/AfriSpeech/afrispeech-selector) where they exist. The
372
+ generated dataset card says plainly that the audio is model-generated — leave that in.
373
+
374
+ ---
375
+
376
+ ## Development
377
+
378
+ ```bash
379
+ git clone https://github.com/AfriSpeech/afrispeech-synth
380
+ cd afrispeech-synth
381
+ python3 -m venv .venv && source .venv/bin/activate
382
+ pip install -e ".[dev]"
383
+ pytest
384
+ ```
385
+
386
+ The test suite runs the whole pipeline end to end against a mock backend, so it needs
387
+ no API key and no network.
388
+
389
+ ## Acknowledgements
390
+
391
+ - **[Google Gemini TTS](https://ai.google.dev/gemini-api/docs/speech-generation)** generates the
392
+ audio. Clips produced with it are subject to
393
+ [Google's API terms](https://ai.google.dev/gemini-api/terms); check them before publishing a
394
+ dataset, and say plainly in the dataset card that the audio is model-generated — the generated
395
+ card does.
396
+ - **[africa-g2p](https://github.com/AfriSpeech/africa-g2p)**, built on Hartell's
397
+ *Alphabets of Africa* (UNESCO, 1993) and Omniglot, for phonemisation.
398
+ - **[africa-corpus-builder](https://github.com/AfriSpeech/africa-corpus-builder)** and
399
+ **[afriso](https://github.com/AfriSpeech/afriso)** (SIL ISO 639-3 + Glottolog) for text and
400
+ language metadata.
401
+
402
+ ## License
403
+
404
+ MIT — the pipeline. Generated audio is yours subject to your TTS provider's terms, and source
405
+ text keeps the licence of its own corpus.