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.
- afrispeech_synth-0.1.0/.github/workflows/release.yml +78 -0
- afrispeech_synth-0.1.0/.gitignore +17 -0
- afrispeech_synth-0.1.0/LICENSE +28 -0
- afrispeech_synth-0.1.0/PKG-INFO +405 -0
- afrispeech_synth-0.1.0/README.md +382 -0
- afrispeech_synth-0.1.0/examples/multilingual.sh +17 -0
- afrispeech_synth-0.1.0/examples/twi.yaml +41 -0
- afrispeech_synth-0.1.0/examples/yoruba.yaml +34 -0
- afrispeech_synth-0.1.0/pyproject.toml +46 -0
- afrispeech_synth-0.1.0/src/afrispeech_synth/__init__.py +23 -0
- afrispeech_synth-0.1.0/src/afrispeech_synth/card.py +268 -0
- afrispeech_synth-0.1.0/src/afrispeech_synth/cli.py +308 -0
- afrispeech_synth-0.1.0/src/afrispeech_synth/config.py +119 -0
- afrispeech_synth-0.1.0/src/afrispeech_synth/coverage.py +117 -0
- afrispeech_synth-0.1.0/src/afrispeech_synth/env.py +40 -0
- afrispeech_synth-0.1.0/src/afrispeech_synth/lang.py +147 -0
- afrispeech_synth-0.1.0/src/afrispeech_synth/normalise.py +150 -0
- afrispeech_synth-0.1.0/src/afrispeech_synth/package.py +164 -0
- afrispeech_synth-0.1.0/src/afrispeech_synth/pipeline.py +134 -0
- afrispeech_synth-0.1.0/src/afrispeech_synth/publish.py +39 -0
- afrispeech_synth-0.1.0/src/afrispeech_synth/samples.py +186 -0
- afrispeech_synth-0.1.0/src/afrispeech_synth/select.py +139 -0
- afrispeech_synth-0.1.0/src/afrispeech_synth/sources.py +190 -0
- afrispeech_synth-0.1.0/src/afrispeech_synth/space.py +286 -0
- afrispeech_synth-0.1.0/src/afrispeech_synth/synth.py +214 -0
- afrispeech_synth-0.1.0/src/afrispeech_synth/tts/__init__.py +36 -0
- afrispeech_synth-0.1.0/src/afrispeech_synth/tts/base.py +80 -0
- afrispeech_synth-0.1.0/src/afrispeech_synth/tts/gemini.py +92 -0
- afrispeech_synth-0.1.0/src/afrispeech_synth/voices.py +77 -0
- afrispeech_synth-0.1.0/tests/conftest.py +4 -0
- afrispeech_synth-0.1.0/tests/mock_backend.py +23 -0
- 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,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.
|