UncleanCode commited on
Commit
e4ae2bb
·
verified ·
1 Parent(s): 85bc603

Dynamic Accent Flash: add Flash-DPO (CosyVoice3) as --version flash (default), diacritic front-end, Flash results, samples

Browse files
.gitattributes CHANGED
@@ -51,3 +51,13 @@ samples/phase3/phase3/para_ibo_pcm_eng.mp3 filter=lfs diff=lfs merge=lfs -text
51
  samples/phase3/phase3/para_yor_eng.mp3 filter=lfs diff=lfs merge=lfs -text
52
  samples/phase3/phase3_female/para_ibo_pcm_eng__waxal_ibo_5.mp3 filter=lfs diff=lfs merge=lfs -text
53
  samples/phase3/phase3_female/para_yor_eng__waxal_yor_5.mp3 filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
51
  samples/phase3/phase3/para_yor_eng.mp3 filter=lfs diff=lfs merge=lfs -text
52
  samples/phase3/phase3_female/para_ibo_pcm_eng__waxal_ibo_5.mp3 filter=lfs diff=lfs merge=lfs -text
53
  samples/phase3/phase3_female/para_yor_eng__waxal_yor_5.mp3 filter=lfs diff=lfs merge=lfs -text
54
+ samples/flash/codeswitch_ibo_pcm_eng.mp3 filter=lfs diff=lfs merge=lfs -text
55
+ samples/flash/codeswitch_yor_eng.mp3 filter=lfs diff=lfs merge=lfs -text
56
+ samples/flash/diacritics_yoruba_from_plain.mp3 filter=lfs diff=lfs merge=lfs -text
57
+ samples/flash/hausa_male.mp3 filter=lfs diff=lfs merge=lfs -text
58
+ samples/flash/igbo_para_female.mp3 filter=lfs diff=lfs merge=lfs -text
59
+ samples/flash/igbo_pure_female.mp3 filter=lfs diff=lfs merge=lfs -text
60
+ samples/flash/numbers_igbo.mp3 filter=lfs diff=lfs merge=lfs -text
61
+ samples/flash/pidgin_female.mp3 filter=lfs diff=lfs merge=lfs -text
62
+ samples/flash/yoruba_female.mp3 filter=lfs diff=lfs merge=lfs -text
63
+ samples/flash/yoruba_male.mp3 filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -6,7 +6,9 @@ language:
6
  - yo
7
  - ig
8
  - ha
9
- base_model: FunAudioLLM/CosyVoice2-0.5B
 
 
10
  pipeline_tag: text-to-speech
11
  library_name: pytorch
12
  tags:
@@ -18,6 +20,8 @@ tags:
18
  - hausa
19
  - nigerian-pidgin
20
  - cosyvoice2
 
 
21
  datasets:
22
  - google/WaxalNLP
23
  - McGill-NLP/NaijaS2ST
@@ -32,7 +36,9 @@ datasets:
32
 
33
  **Code-switching TTS for Nigerian languages.** English, Nigerian Pidgin, Igbo, Yoruba and Hausa, mixed freely inside one sentence and spoken by one consistent voice, with no model swapping between languages.
34
 
35
- Dynamic Accent is a fine-tune of [FunAudioLLM/CosyVoice2-0.5B](https://huggingface.co/FunAudioLLM/CosyVoice2-0.5B) by Modular. It adds:
 
 
36
  - per-token **language embeddings**, a **speaker token** and an adversarial (gradient-reversal) speaker classifier on the LM, so the voice stays the same across a language switch (after Zhang et al., Google, 2019, *Learning to Speak Fluently in a Foreign Language*);
37
  - training on about 178 h of Nigerian-language and code-switched speech, including about 102 h of Igbo;
38
  - learned `[pause]`/`[breath]` events and a speaking-rate instruction (Phase 3);
@@ -42,11 +48,12 @@ It is a research release, not a production system. Read **Limitations** before y
42
 
43
  ## Versions and voices
44
 
45
- Two model versions ship side by side. Pick one with `--version`, or render both with `--compare` and A/B before use.
46
 
47
  | version | LM | flow | decoding | what changed |
48
  |---|---|---|---|---|
49
- | `phase3` (**default**) | `checkpoints/phase3/p3_lm_full` (Phase 2 LM + 5,000 steps on a 178 h re-cut with ~102 h Igbo, `[pause]`/`[breath]` tags and a speed instruction, rate `normal`) | shared `checkpoints/phase2/p2_flow_sft` | one pass per paragraph, silence-aware repetition check, no audio post-processing | lower CER in Igbo/Hausa/Yoruba/code-switch/paragraphs, comma pauses at the anchor's length, clipped endings 63% → 11% |
 
50
  | `phase2` | `checkpoints/phase2/p2_lm_B` (~120 h, pause profile B) | `checkpoints/phase2/p2_flow_sft` | sentence by sentence, internal silences capped at 0.6 s | the Phase 2 final system |
51
 
52
  | voice (`--voice`) | who | notes |
@@ -55,7 +62,7 @@ Two model versions ship side by side. Pick one with `--version`, or render both
55
  | `waxal_ibo_5_female` | WAXAL Igbo speaker 5, female, native Igbo | zero-shot (prompt clip, no fine-tune); best CER of the three on the female test subset |
56
  | `waxal_yor_5_female` | WAXAL Yoruba speaker 5, female, native Yoruba | zero-shot; strongest identity match, but its 3.8 s prompt has no pauses, so pauses are short and endings often clipped |
57
 
58
- **Recommended pairing** (author's listening verdict): **Phase 3 sounds best with the female voices; Phase 2 is fine for the male anchor.** `infer.py` prints a note when you pick another pairing. The female voices were evaluated with `phase3` only; they run with `phase2` but are untested there. Settings live in `settings.json`.
59
 
60
  ## Setup (single GPU)
61
 
@@ -71,21 +78,25 @@ huggingface-cli download Modularcomputing/dynamic-accent-preview --local-dir dyn
71
  pip install -r dynamic-accent-preview/requirements.txt
72
  ```
73
 
74
- The stock base model `FunAudioLLM/CosyVoice2-0.5B` (tokenizer, vocoder, speech tokenizer, speaker encoder) downloads to `pretrained/CosyVoice2-0.5B` on first run; nothing in it needs changing. Point `--model_dir` (or `COSYVOICE2_DIR`) at an existing copy, and `--cosyvoice_dir` (or `COSYVOICE_REPO`) at your CosyVoice clone. A 16 GB GPU is enough for the CLI; the Gradio app keeps both versions loaded. Tested on an H100 (bf16). `infer.py` also runs on CPU in fp32, which is slow. T4-class GPUs fall back to fp16, which has not been tested.
75
 
76
  ## Usage
77
 
78
  ```bash
79
  cd dynamic-accent-preview
80
- # default: --version phase3 --voice anchor_male
81
  python infer.py --cosyvoice_dir ../CosyVoice --text "<eng>this is dynamic accent,</eng> <pcm>e dey sound nice abi?</pcm>" --out out.wav
82
 
83
- # recommended pairing
84
- python infer.py --cosyvoice_dir ../CosyVoice --version phase3 --voice waxal_ibo_5_female \
85
  --text "<ibo>Kedu ka ị mere?</ibo> <pcm>Abeg no vex, I go call you for evening.</pcm>" --out ibo_f.wav
86
 
87
- # A/B: the same text, seed and voice through both versions -> cmp_phase2.wav, cmp_phase3.wav
88
- python infer.py --cosyvoice_dir ../CosyVoice --compare --voice anchor_male --text "..." --out cmp.wav
 
 
 
 
89
 
90
  python infer.py --list_voices # voices, versions, recommended pairing
91
  ```
@@ -93,11 +104,16 @@ python infer.py --list_voices # voices, versions, recommended pairing
93
  | flag | what it does |
94
  |---|---|
95
  | `--text` | the text: tagged (below) or plain with `--auto_tag` |
96
- | `--version phase2\|phase3` | model version (default `phase3`) |
97
  | `--voice anchor_male\|waxal_ibo_5_female\|waxal_yor_5_female` | voice (default `anchor_male`); old ids `anchor`, `waxal_ibo_5`, `waxal_yor_5` still work |
98
  | `--list_voices` | list voices, versions and the recommended pairing |
99
- | `--compare` | render with both versions → `<out>_phase2.wav`, `<out>_phase3.wav` |
100
- | `--rate default\|none\|very slow\|slow\|normal\|fast\|very fast` | Phase 3 speaking-rate instruction to the LM (default: the version's setting, `normal`). It is a learned instruction, not time-stretching. `phase2` ignores it |
 
 
 
 
 
101
  | `--auto_tag` | tag untagged mixed text automatically (see below), print the tagged text, then synthesise |
102
  | `--tag_only` | with `--auto_tag`: print the tagged text and stop, so you can edit it and pass it back as `--text` |
103
  | `--force_lang eng\|pcm\|ibo\|yor\|hau` | read the whole text in one language (tags are ignored) |
@@ -149,7 +165,7 @@ Write Yoruba and Igbo with their diacritics where you can; text without diacriti
149
  python app.py --cosyvoice_dir ../CosyVoice # http://127.0.0.1:7860 (--share for a public link)
150
  ```
151
 
152
- It has a text box, an auto-tag toggle with an editable tagged-text preview, word overrides, a force-language option, version, voice and rate dropdowns, a seed, and a **Compare** button that plays Phase 2 and Phase 3 side by side.
153
 
154
  ### From Python
155
 
@@ -158,13 +174,33 @@ import types, soundfile as sf, infer
158
  a = types.SimpleNamespace(model_dir="pretrained/CosyVoice2-0.5B", cosyvoice_dir="../CosyVoice", lm="", flow="",
159
  prompts="prompts/prompts.json", voices="prompts/voices.json", dtype="auto", stitch=False,
160
  rate="default", voice="waxal_ibo_5_female")
161
- tts = infer.build("phase3", a)
 
162
  wav = tts.say("<ibo>Kedu ka ị mere?</ibo> <eng>The meeting has been moved to Friday morning.</eng>", seed=0)
163
  sf.write("out.wav", wav, tts.sr)
164
  ```
165
 
166
  ## Samples
167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  Seed 0, rendered with `infer.py`. Inputs: `para_ibo_pcm_eng`, `para_yor_eng`, `para_hau_eng_pcm` (code-switched paragraphs), `ibo_000` (Igbo), `custom_dynacc` ("this is dynamic accent, e dey sound nice abi?") and `omo_announce` ("omo, I'm very excited to announce this new model, ọ na-eme m obi ụtọ, I no go lie give you").
169
 
170
  | input | Phase 2, anchor_male | Phase 3, anchor_male | Phase 3, female |
@@ -178,6 +214,24 @@ Seed 0, rendered with `infer.py`. Inputs: `para_ibo_pcm_eng`, `para_yor_eng`, `p
178
 
179
  Older renders: `samples/pcm_eng_dynamic_accent_seed{0,1,2}.mp3` (Phase 2, three seeds) and `samples/prosody_v2/` (the rejected prosody-v2 post-processing, kept for reference).
180
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  ## Evaluation: Phase 2 vs Phase 3
182
 
183
  183-item eval set (30 per language, 10 code-switched, 3 hand-written + 20 native long-form paragraphs), anchor voice, seed 0. CER is ASR round-trip: MMS-1b-all for Igbo/Hausa/Yoruba, Whisper-large-v3-turbo for English and Pidgin, the lower of the two for code-switched items and paragraphs. "No tones" (in brackets) strips diacritics. SIM is ECAPA cosine to the anchor (ceiling about 0.84 on real held-out anchor clips). The Pidgin judge is weak: real Pidgin recordings score about 0.43.
@@ -221,7 +275,8 @@ On the held-out single-language set, per language (rule-based / + GlotLID): Engl
221
 
222
  ## Limitations
223
 
224
- - **Pace**: speech is still 13–28% slower than native speakers (Igbo 4.60 vs 5.30 syl/s, Hausa 3.77 vs 5.26 syl/s), and there are about half as many pauses per second as in real speech. It can sound careful rather than fully native.
 
225
  - **Pidgin is the weakest language**: only about 5 h of training data, no reliable ASR judge, and the auto-tagger's Pidgin/English split is its weakest point (~0.63 word accuracy on plain Pidgin).
226
  - **No laughter or other expressive events**: the training data has effectively no laughter, so tags like `[laughter]` do nothing useful. `[pause]`/`[breath]` were learned from data, but there is no reliable user control over them yet.
227
  - English after a Nigerian-language span keeps a strong local accent.
@@ -234,13 +289,17 @@ On the held-out single-language set, per language (rule-based / + GlotLID): Engl
234
 
235
  | path | what it is | licence |
236
  |---|---|---|
 
 
237
  | `checkpoints/phase3/p3_lm_full/` | Phase 3 LM (`best.pt`, val step 4,500), `config.json` (rate, sampler, one-pass, points at the shared Phase 2 flow), `spk2id.json`, `args.json`, `log.jsonl` | CC-BY-SA-4.0 |
238
  | `checkpoints/phase2/p2_lm_B/` | Phase 2 LM (`best.pt`) and `spk2id.json` | CC-BY-SA-4.0 |
239
  | `checkpoints/phase2/p2_flow_sft/best.pt` | anchor-SFT flow-matching decoder, used by both versions | CC-BY-SA-4.0 |
240
  | `prompts/` | anchor prompt clips (`clip0..3`, `prompts.json` with transcripts and S3 tokens) and female voice prompts (`voice_*.wav`, `voices.json`), all from WAXAL | CC-BY-SA-4.0 |
241
- | `samples/` | example renders | CC-BY-SA-4.0 |
242
  | `settings.json` | versions, voices, recommended pairing | Apache-2.0 |
243
  | `infer.py`, `tagger.py`, `app.py` | inference CLI, auto-tagger, Gradio app | Apache-2.0 |
 
 
244
  | `code/` | the original training and synthesis code, for reference | Apache-2.0 |
245
  | `requirements.txt`, `cosyvoice_requirements.txt` | dependencies (`cosyvoice_requirements.txt` is CosyVoice's own file at the commit used) | – |
246
  | `assets/` | banner | – |
@@ -255,22 +314,24 @@ The model weights are trained on, and the voice prompts are taken from, the data
255
  | [Open.Bible Igbo](https://huggingface.co/datasets/AfriSpeech/open-bible-speech-african) (Biblica® Open Igbo Contemporary Bible™, Audio Edition) | Igbo (Phase 3) | CC-BY-SA-4.0 | Biblica, Inc. via open.bible; Biblica® and Open Igbo Contemporary Bible™ are trademarks of Biblica, Inc. |
256
  | [NaijaS2ST](https://huggingface.co/datasets/McGill-NLP/NaijaS2ST) (Igbo) | Igbo, many native speakers (Phase 3) | CC-BY-4.0 | McGill-NLP, NaijaS2ST (arXiv:2604.16287) |
257
  | [IgboAPI](https://huggingface.co/datasets/nkowaokwu/ibo-dict) (ibo-dict, ibo-dict-expansion) | Igbo sentences (Phase 3) | CC-BY-4.0 | Nkọwa okwu / IgboAPI contributors |
258
- | [IgboSynCorp](https://doi.org/10.7910/dvn/yb9fwk) | natural Igbo speech (Phase 3) | CC0-1.0 | University of Ibadan and Afe Babalola University (Lacuna Fund) |
259
  | [FLEURS](https://huggingface.co/datasets/google/fleurs) (English, Hausa, Igbo, Yoruba) | multi-speaker data | CC-BY-4.0 | Google, FLEURS (Conneau et al., 2022) |
260
  | [BibleTTS](https://masakhane-io.github.io/bibleTTS/) (Yoruba, Hausa) | Yoruba and Hausa | CC-BY-SA-4.0 | Meyer et al., BibleTTS (2022); Biblica / open.bible audio |
261
  | [OpenSLR SLR70](https://openslr.org/70/) (Nigerian English) | English | CC-BY-SA-4.0 | Google, crowdsourced Nigerian English |
262
  | [OpenSLR SLR86](https://openslr.org/86/) (Yoruba) | Yoruba | CC-BY-SA-4.0 | Google, crowdsourced Yoruba |
263
  | code-switched pairs | about 9 h of synthetic code-switch training pairs | follows the source clips (CC-BY-SA-4.0 / CC-BY-4.0) | built by concatenating clips from the sets above, following the Google 2019 code-switching recipe (Zhang et al., 2019); no separate Google dataset |
 
 
264
  | base model [FunAudioLLM/CosyVoice2-0.5B](https://huggingface.co/FunAudioLLM/CosyVoice2-0.5B) | LM initialisation, flow, vocoder, tokenizers | Apache-2.0 | FunAudioLLM / Alibaba Tongyi Lab, CosyVoice 2 (Du et al., 2024) |
265
  | optional [GlotLID](https://huggingface.co/cis-lmu/glotlid) (not bundled) | `--lid glotlid` in the tagger | Apache-2.0 plus notices | Kargaran et al., GlotLID (EMNLP 2023) |
266
 
267
- Excluded on licence grounds: WaZoBiaSpeech (bans TTS use), NaijaVoices (non-commercial), and non-commercial TTS weights.
268
 
269
  ## Licence
270
 
271
  - **Model weights, voice prompts and samples** (`checkpoints/`, `prompts/`, `samples/`): **[CC-BY-SA-4.0](LICENSE)**. You may use them commercially. You must credit Modular and the datasets listed above, and anything you derive from the weights (fine-tunes, merges) must be shared under CC-BY-SA-4.0 too.
272
- - **Code** (`infer.py`, `tagger.py`, `app.py`, `code/`, `settings.json`): **[Apache-2.0](LICENSE-CODE)**.
273
- - The base model, CosyVoice2-0.5B, is Apache-2.0; its notices still apply to the parts it contributes.
274
 
275
  ## Citation
276
 
 
6
  - yo
7
  - ig
8
  - ha
9
+ base_model:
10
+ - FunAudioLLM/Fun-CosyVoice3-0.5B-2512
11
+ - FunAudioLLM/CosyVoice2-0.5B
12
  pipeline_tag: text-to-speech
13
  library_name: pytorch
14
  tags:
 
20
  - hausa
21
  - nigerian-pidgin
22
  - cosyvoice2
23
+ - cosyvoice3
24
+ - dpo
25
  datasets:
26
  - google/WaxalNLP
27
  - McGill-NLP/NaijaS2ST
 
36
 
37
  **Code-switching TTS for Nigerian languages.** English, Nigerian Pidgin, Igbo, Yoruba and Hausa, mixed freely inside one sentence and spoken by one consistent voice, with no model swapping between languages.
38
 
39
+ **New: Dynamic Accent Flash** (`--version flash`, now the default) is the same recipe ported to [FunAudioLLM/Fun-CosyVoice3-0.5B-2512](https://huggingface.co/FunAudioLLM/Fun-CosyVoice3-0.5B-2512), then preference-tuned with DPO for naturalness, intelligibility, pace and pauses, with a text front-end that restores Yoruba/Igbo diacritics. It is trained on licensed data only. See **Flash results** below.
40
+
41
+ Dynamic Accent (Phase 2/3) is a fine-tune of [FunAudioLLM/CosyVoice2-0.5B](https://huggingface.co/FunAudioLLM/CosyVoice2-0.5B) by Modular. It adds:
42
  - per-token **language embeddings**, a **speaker token** and an adversarial (gradient-reversal) speaker classifier on the LM, so the voice stays the same across a language switch (after Zhang et al., Google, 2019, *Learning to Speak Fluently in a Foreign Language*);
43
  - training on about 178 h of Nigerian-language and code-switched speech, including about 102 h of Igbo;
44
  - learned `[pause]`/`[breath]` events and a speaking-rate instruction (Phase 3);
 
48
 
49
  ## Versions and voices
50
 
51
+ Three model versions ship side by side. Pick one with `--version`, or render several with `--compare` and A/B before use.
52
 
53
  | version | LM | flow | decoding | what changed |
54
  |---|---|---|---|---|
55
+ | `flash` (**default**) | `checkpoints/flash/flash_dpo` on **Fun-CosyVoice3-0.5B-2512**: Flash SFT (CV3 LM, the Phase 3 data re-weighted towards natural speech, plus IgboSynCorp pseudo-transcripts) + DPO (1,206 preference pairs; reward = naturalness + CER + articulation rate + pause rate) | stock CV3 flow + vocoder, zero-shot from the voice clip | text front-end (numbers verbalised, missing Yoruba/Igbo diacritics restored by `checkpoints/flash/diacritics_byt5`), one pass per paragraph, silence-aware repetition check, no audio post-processing | pace about native, fewest clipped endings for every voice, lower anchor CER (see **Flash results**) |
56
+ | `phase3` | `checkpoints/phase3/p3_lm_full` (Phase 2 LM + 5,000 steps on a 178 h re-cut with ~102 h Igbo, `[pause]`/`[breath]` tags and a speed instruction, rate `normal`) | shared `checkpoints/phase2/p2_flow_sft` | one pass per paragraph, silence-aware repetition check, no audio post-processing | lower CER in Igbo/Hausa/Yoruba/code-switch/paragraphs, comma pauses at the anchor's length, clipped endings 63% → 11% |
57
  | `phase2` | `checkpoints/phase2/p2_lm_B` (~120 h, pause profile B) | `checkpoints/phase2/p2_flow_sft` | sentence by sentence, internal silences capped at 0.6 s | the Phase 2 final system |
58
 
59
  | voice (`--voice`) | who | notes |
 
62
  | `waxal_ibo_5_female` | WAXAL Igbo speaker 5, female, native Igbo | zero-shot (prompt clip, no fine-tune); best CER of the three on the female test subset |
63
  | `waxal_yor_5_female` | WAXAL Yoruba speaker 5, female, native Yoruba | zero-shot; strongest identity match, but its 3.8 s prompt has no pauses, so pauses are short and endings often clipped |
64
 
65
+ **Recommended:** `flash` with any of the three voices; it scores best overall (see **Flash results**), with one known weakness: with `anchor_male` it under-pauses in Igbo and Yoruba. Earlier listening verdict (before Flash): Phase 3 sounds best with the female voices, Phase 2 is fine for the male anchor. `infer.py` prints a note when you pick a pairing outside these. The female voices run with `phase2` but are untested there. Settings live in `settings.json`.
66
 
67
  ## Setup (single GPU)
68
 
 
78
  pip install -r dynamic-accent-preview/requirements.txt
79
  ```
80
 
81
+ The stock base models download on first run and need no changes: `FunAudioLLM/Fun-CosyVoice3-0.5B-2512` to `pretrained/Fun-CosyVoice3-0.5B-2512` for `flash` (`--cv3_dir` / `CV3_DIR` to reuse a copy), and `FunAudioLLM/CosyVoice2-0.5B` to `pretrained/CosyVoice2-0.5B` for `phase2`/`phase3`. The same CosyVoice commit runs both (it has `CosyVoice3`). **`flash` needs a CUDA GPU** (tested on an H100, bf16). Point `--model_dir` (or `COSYVOICE2_DIR`) at an existing copy, and `--cosyvoice_dir` (or `COSYVOICE_REPO`) at your CosyVoice clone. A 16 GB GPU is enough for the CLI; the Gradio app keeps both versions loaded. Tested on an H100 (bf16). `infer.py` also runs on CPU in fp32, which is slow. T4-class GPUs fall back to fp16, which has not been tested.
82
 
83
  ## Usage
84
 
85
  ```bash
86
  cd dynamic-accent-preview
87
+ # default: --version flash --voice anchor_male
88
  python infer.py --cosyvoice_dir ../CosyVoice --text "<eng>this is dynamic accent,</eng> <pcm>e dey sound nice abi?</pcm>" --out out.wav
89
 
90
+ # Flash with the female Igbo voice
91
+ python infer.py --cosyvoice_dir ../CosyVoice --version flash --voice waxal_ibo_5_female \
92
  --text "<ibo>Kedu ka ị mere?</ibo> <pcm>Abeg no vex, I go call you for evening.</pcm>" --out ibo_f.wav
93
 
94
+ # plain Yoruba without tone marks: Flash's front-end restores them before synthesis
95
+ python infer.py --cosyvoice_dir ../CosyVoice --version flash --voice waxal_yor_5_female --frontend native --frontend_lang yor \
96
+ --text "E kaaaro, se daadaa ni gbogbo yin wa?" --out yor.wav
97
+
98
+ # A/B: the same text, seed and voice through several versions -> cmp_phase2.wav, cmp_phase3.wav, cmp_flash.wav
99
+ python infer.py --cosyvoice_dir ../CosyVoice --compare --compare_versions phase2,phase3,flash --voice anchor_male --text "..." --out cmp.wav
100
 
101
  python infer.py --list_voices # voices, versions, recommended pairing
102
  ```
 
104
  | flag | what it does |
105
  |---|---|
106
  | `--text` | the text: tagged (below) or plain with `--auto_tag` |
107
+ | `--version flash\|phase3\|phase2` | model version (default `flash`) |
108
  | `--voice anchor_male\|waxal_ibo_5_female\|waxal_yor_5_female` | voice (default `anchor_male`); old ids `anchor`, `waxal_ibo_5`, `waxal_yor_5` still work |
109
  | `--list_voices` | list voices, versions and the recommended pairing |
110
+ | `--compare`, `--compare_versions phase2,phase3,flash` | render with several versions → `<out>_<version>.wav` |
111
+ | `--frontend codeswitch\|native\|accent` | `flash` text front-end: `codeswitch` (default; tagged text, or untagged text in `--frontend_lang`), `native` (whole text in `--frontend_lang`, diacritics restored), `accent` (words as written, read with the `--frontend_lang` accent) |
112
+ | `--frontend_lang eng\|pcm\|ibo\|yor\|hau` | language for untagged text / `native` / `accent` |
113
+ | `--no_diacritics` | `flash`: do not restore missing Yoruba/Igbo tone marks and underdots |
114
+ | `--frontend translate --experimental_translate` | **experimental, off by default**: translate English/Pidgin into `--frontend_lang` (ibo/yor/hau) with MADLAD-400-3B-MT (Apache-2.0, ~12 GB download) before synthesis. MADLAD often loops (repeated words or dots) on longer inputs and translates Pidgin poorly, so check the printed text |
115
+ | `--cv3_dir` | `flash`: path of the Fun-CosyVoice3-0.5B-2512 snapshot |
116
+ | `--rate default\|none\|very slow\|slow\|normal\|fast\|very fast` | Phase 3 / Flash speaking-rate instruction to the LM (default: the version's setting, `normal`). It is a learned instruction, not time-stretching. `phase2` ignores it |
117
  | `--auto_tag` | tag untagged mixed text automatically (see below), print the tagged text, then synthesise |
118
  | `--tag_only` | with `--auto_tag`: print the tagged text and stop, so you can edit it and pass it back as `--text` |
119
  | `--force_lang eng\|pcm\|ibo\|yor\|hau` | read the whole text in one language (tags are ignored) |
 
165
  python app.py --cosyvoice_dir ../CosyVoice # http://127.0.0.1:7860 (--share for a public link)
166
  ```
167
 
168
+ It has a text box, an auto-tag toggle with an editable tagged-text preview, word overrides, a force-language option, version, voice and rate dropdowns, a seed, and a **Compare** button that plays Phase 3 and Flash side by side. The app launch has not been smoke-tested yet.
169
 
170
  ### From Python
171
 
 
174
  a = types.SimpleNamespace(model_dir="pretrained/CosyVoice2-0.5B", cosyvoice_dir="../CosyVoice", lm="", flow="",
175
  prompts="prompts/prompts.json", voices="prompts/voices.json", dtype="auto", stitch=False,
176
  rate="default", voice="waxal_ibo_5_female")
177
+ tts = infer.build("phase3", a) # for "flash" also set frontend="codeswitch", frontend_lang="eng", no_diacritics=False,
178
+ # experimental_translate=False, cv3_dir="pretrained/Fun-CosyVoice3-0.5B-2512"
179
  wav = tts.say("<ibo>Kedu ka ị mere?</ibo> <eng>The meeting has been moved to Friday morning.</eng>", seed=0)
180
  sf.write("out.wav", wav, tts.sr)
181
  ```
182
 
183
  ## Samples
184
 
185
+ ### Flash (`--version flash`, Flash-DPO)
186
+
187
+ Seed 0, model pauses, speed 1.0, no post-processing. Inputs are written with diacritics unless noted.
188
+
189
+ | sample | voice | text |
190
+ |---|---|---|
191
+ | [igbo_pure_female](samples/flash/igbo_pure_female.mp3) | waxal_ibo_5_female | Chei, obi dị m ụtọ nke ukwuu ịkpọsa ụdị ọhụrụ a, ọ na-eme m obi ụtọ, agaghị m agha gị ụgha. |
192
+ | [igbo_para_female](samples/flash/igbo_para_female.mp3) | waxal_ibo_5_female | Ndewo nu, ụmụnna m. Taa bụ ụbọchị pụrụ iche n'obodo anyị. Anyị ga-ezukọ n'ama egwuregwu, rie nri, ma kparịta ụka maka ọdịnihu ụmụaka anyị. |
193
+ | [yoruba_female](samples/flash/yoruba_female.mp3) / [yoruba_male](samples/flash/yoruba_male.mp3) | waxal_yor_5_female / anchor_male | Ẹ káàsán o, ẹ̀yin ará. Inú mi dùn láti rí gbogbo yín lónìí. Ẹ jẹ́ ká jọ ṣiṣẹ́ pọ̀ fún ìlọsíwájú ìlú wa. |
194
+ | [hausa_male](samples/flash/hausa_male.mp3) | anchor_male | Barka da yamma, 'yan uwa. Muna farin cikin ganin ku a yau. Za mu fara taron nan ba da daɗewa ba, don haka ku zauna lafiya. |
195
+ | [pidgin_female](samples/flash/pidgin_female.mp3) | waxal_ibo_5_female | Omo, this traffic no be here o. Abeg make una no vex, I go reach there small time. Wetin dey happen for your side? |
196
+ | [codeswitch_ibo_pcm_eng](samples/flash/codeswitch_ibo_pcm_eng.mp3) | waxal_ibo_5_female | `<ibo>Nne m, ọ dị mma, ka anyị gaa ahịa taa.</ibo> <pcm>Abeg make we no late o,</pcm> <eng>the shop closes at 6pm.</eng>` |
197
+ | [codeswitch_yor_eng](samples/flash/codeswitch_yor_eng.mp3) | waxal_yor_5_female | `<yor>Ẹ jọ̀ọ́, ẹ dúró díẹ̀,</yor> <eng>I will send you the report this afternoon.</eng> <yor>Ẹ ṣé púpọ̀.</yor>` |
198
+ | [codeswitch_dynacc](samples/flash/codeswitch_dynacc.mp3) | waxal_ibo_5_female | `<eng>this is dynamic accent,</eng> <pcm>e dey sound nice abi?</pcm>` |
199
+ | [numbers_igbo](samples/flash/numbers_igbo.mp3) | waxal_ibo_5_female | Ọnụ ahịa ya bụ ₦2,500, anyị ga-ahụ gị na 21/10/2026 n'elekere 4:30. (numbers verbalised by the front-end) |
200
+ | [diacritics_yoruba_from_plain](samples/flash/diacritics_yoruba_from_plain.mp3) | waxal_yor_5_female | input without marks: "E kaaaro, se daadaa ni gbogbo yin wa? Oju ojo dara pupo loni." → front-end: Ẹ káàárọ̀, ṣé dáadáa ni gbogbo yín wà? Ojú ọjọ́ dára púpọ̀ lónì. |
201
+
202
+ ### Phase 2 and Phase 3
203
+
204
  Seed 0, rendered with `infer.py`. Inputs: `para_ibo_pcm_eng`, `para_yor_eng`, `para_hau_eng_pcm` (code-switched paragraphs), `ibo_000` (Igbo), `custom_dynacc` ("this is dynamic accent, e dey sound nice abi?") and `omo_announce` ("omo, I'm very excited to announce this new model, ọ na-eme m obi ụtọ, I no go lie give you").
205
 
206
  | input | Phase 2, anchor_male | Phase 3, anchor_male | Phase 3, female |
 
214
 
215
  Older renders: `samples/pcm_eng_dynamic_accent_seed{0,1,2}.mp3` (Phase 2, three seeds) and `samples/prosody_v2/` (the rejected prosody-v2 post-processing, kept for reference).
216
 
217
+ ## Flash results
218
+
219
+ Same eval set and judges as the Phase 2 vs Phase 3 tables below (183 items plus code-switched and paragraph groups), seed 0, all groups pooled. **Pace** and **pauses** are ratios to native speakers reading the same texts (1.00 = native). **Clipped endings** = share of renders whose trailing silence is under 0.1 s (native recordings: 23.8%). SIM is ECAPA cosine to the voice's own real clips.
220
+
221
+ | voice | system | CER | CER no tones | SIM | pace × native | pauses/s × native | clipped endings |
222
+ |---|---|---|---|---|---|---|---|
223
+ | anchor_male | Phase 3 | 0.174 | 0.146 | 0.815 | 0.92 | 1.11 | 9.1% |
224
+ | anchor_male | **Flash** | **0.162** | **0.135** | 0.784 | **0.99** | 0.75 | **7.1%** |
225
+ | waxal_ibo_5_female | Phase 3 | 0.164 | 0.132 | 0.683 | 1.00 | 0.74 | 22.8% |
226
+ | waxal_ibo_5_female | **Flash** | 0.165 | 0.134 | **0.700** | 1.03 | **0.95** | **10.7%** |
227
+
228
+ - Flash lowers the anchor's CER (0.174 → 0.162) and clipped endings (9.1% → 7.1%), brings its pace to about native (0.92× → 0.99×), and more than halves clipped endings on the female Igbo voice (22.8% → 10.7%) at about native pace and pause rate.
229
+ - Per language with the anchor, Flash CER: Igbo 0.189 (Phase 3: 0.210), Yoruba 0.230 (0.243), Hausa 0.123 (0.146), English 0.026 (0.039), paragraphs 0.131 (0.168), code-switch 0.208 (0.212); Pidgin 0.227 (0.209) is worse, and the Pidgin judge is weak anyway.
230
+ - **Known weakness:** with `anchor_male`, Flash **under-pauses in Igbo (0.40× native pauses per second) and Yoruba (0.30×)**. Long Igbo or Yoruba passages with the male voice can sound rushed; use a female voice or `--compare` with `phase3` there.
231
+ - Speaker similarity of the anchor drops slightly (0.815 → 0.784), because Flash uses the stock CV3 flow zero-shot instead of the anchor-SFT flow.
232
+ - **UTMOS caveat:** UTMOS22 rates real native speech (3.53) below every synthetic system (3.8–4.3), so it rewards clean studio-like synthesis rather than nativeness. It was not used to choose the model (Phase 3 has the highest UTMOS22: 4.15 anchor / 4.27 female; Flash 3.89 / 4.16; Flash UTMOSv2 3.17 / 3.51). The choice rests on CER, pace and pauses relative to native speech, and clipped endings. Native-speaker MOS / blind A/B is still pending.
233
+ - Text front-end: diacritic restoration word accuracy Yoruba 0.862 (stripped input: 0.171), Igbo 0.968 (0.640).
234
+
235
  ## Evaluation: Phase 2 vs Phase 3
236
 
237
  183-item eval set (30 per language, 10 code-switched, 3 hand-written + 20 native long-form paragraphs), anchor voice, seed 0. CER is ASR round-trip: MMS-1b-all for Igbo/Hausa/Yoruba, Whisper-large-v3-turbo for English and Pidgin, the lower of the two for code-switched items and paragraphs. "No tones" (in brackets) strips diacritics. SIM is ECAPA cosine to the anchor (ceiling about 0.84 on real held-out anchor clips). The Pidgin judge is weak: real Pidgin recordings score about 0.43.
 
275
 
276
  ## Limitations
277
 
278
+ - **Flash** (`flash`): the male anchor under-pauses in Igbo and Yoruba (0.40× / 0.30× native pause rate); Pidgin CER is slightly worse than Phase 3; CUDA only; the translate-first mode is experimental and off by default (MADLAD-400 loops on long inputs and is weak on Pidgin); diacritic restoration can still pick the wrong tone on ambiguous words, so write marks yourself where you can. The limitations below for Phase 3 (no laughter, Pidgin data, accent after a switch) apply to Flash too; the pace gap is largely closed for Flash.
279
+ - **Pace (`phase2`/`phase3`)**: speech is still 13–28% slower than native speakers (Igbo 4.60 vs 5.30 syl/s, Hausa 3.77 vs 5.26 syl/s), and there are about half as many pauses per second as in real speech. It can sound careful rather than fully native.
280
  - **Pidgin is the weakest language**: only about 5 h of training data, no reliable ASR judge, and the auto-tagger's Pidgin/English split is its weakest point (~0.63 word accuracy on plain Pidgin).
281
  - **No laughter or other expressive events**: the training data has effectively no laughter, so tags like `[laughter]` do nothing useful. `[pause]`/`[breath]` were learned from data, but there is no reliable user control over them yet.
282
  - English after a Nigerian-language span keeps a strong local accent.
 
289
 
290
  | path | what it is | licence |
291
  |---|---|---|
292
+ | `checkpoints/flash/flash_dpo/` | Flash LM (`best.pt`, sha256 `f59a6fc0…7e3a307`, CosyVoice3 LM + language/speaker/rate embeddings), `config.json`, `spk2id.json`, DPO `args.json`, `log.jsonl` | CC-BY-SA-4.0 |
293
+ | `checkpoints/flash/diacritics_byt5/` | Yoruba/Igbo diacritic-restoration model (fine-tune of [google/byt5-small](https://huggingface.co/google/byt5-small), Apache-2.0) used by the Flash front-end | CC-BY-SA-4.0 |
294
  | `checkpoints/phase3/p3_lm_full/` | Phase 3 LM (`best.pt`, val step 4,500), `config.json` (rate, sampler, one-pass, points at the shared Phase 2 flow), `spk2id.json`, `args.json`, `log.jsonl` | CC-BY-SA-4.0 |
295
  | `checkpoints/phase2/p2_lm_B/` | Phase 2 LM (`best.pt`) and `spk2id.json` | CC-BY-SA-4.0 |
296
  | `checkpoints/phase2/p2_flow_sft/best.pt` | anchor-SFT flow-matching decoder, used by both versions | CC-BY-SA-4.0 |
297
  | `prompts/` | anchor prompt clips (`clip0..3`, `prompts.json` with transcripts and S3 tokens) and female voice prompts (`voice_*.wav`, `voices.json`), all from WAXAL | CC-BY-SA-4.0 |
298
+ | `samples/` | example renders (`samples/flash/` for Flash) | CC-BY-SA-4.0 |
299
  | `settings.json` | versions, voices, recommended pairing | Apache-2.0 |
300
  | `infer.py`, `tagger.py`, `app.py` | inference CLI, auto-tagger, Gradio app | Apache-2.0 |
301
+ | `flash_lm.py`, `frontend/` | Flash model and synthesis code (training code for reference; the data manifest is not shipped), Flash text front-end (normalisation, diacritics, experimental MT) | Apache-2.0 |
302
+ | `prompts/flash_voices_text.json` | transcripts of the voice clips, used as Flash LM prompts | CC-BY-SA-4.0 |
303
  | `code/` | the original training and synthesis code, for reference | Apache-2.0 |
304
  | `requirements.txt`, `cosyvoice_requirements.txt` | dependencies (`cosyvoice_requirements.txt` is CosyVoice's own file at the commit used) | – |
305
  | `assets/` | banner | – |
 
314
  | [Open.Bible Igbo](https://huggingface.co/datasets/AfriSpeech/open-bible-speech-african) (Biblica® Open Igbo Contemporary Bible™, Audio Edition) | Igbo (Phase 3) | CC-BY-SA-4.0 | Biblica, Inc. via open.bible; Biblica® and Open Igbo Contemporary Bible™ are trademarks of Biblica, Inc. |
315
  | [NaijaS2ST](https://huggingface.co/datasets/McGill-NLP/NaijaS2ST) (Igbo) | Igbo, many native speakers (Phase 3) | CC-BY-4.0 | McGill-NLP, NaijaS2ST (arXiv:2604.16287) |
316
  | [IgboAPI](https://huggingface.co/datasets/nkowaokwu/ibo-dict) (ibo-dict, ibo-dict-expansion) | Igbo sentences (Phase 3) | CC-BY-4.0 | Nkọwa okwu / IgboAPI contributors |
317
+ | [IgboSynCorp](https://doi.org/10.7910/dvn/yb9fwk) | natural Igbo speech (Phase 3); Flash adds pseudo-transcripts of its untranscribed audio | CC0-1.0 | University of Ibadan and Afe Babalola University (Lacuna Fund) |
318
  | [FLEURS](https://huggingface.co/datasets/google/fleurs) (English, Hausa, Igbo, Yoruba) | multi-speaker data | CC-BY-4.0 | Google, FLEURS (Conneau et al., 2022) |
319
  | [BibleTTS](https://masakhane-io.github.io/bibleTTS/) (Yoruba, Hausa) | Yoruba and Hausa | CC-BY-SA-4.0 | Meyer et al., BibleTTS (2022); Biblica / open.bible audio |
320
  | [OpenSLR SLR70](https://openslr.org/70/) (Nigerian English) | English | CC-BY-SA-4.0 | Google, crowdsourced Nigerian English |
321
  | [OpenSLR SLR86](https://openslr.org/86/) (Yoruba) | Yoruba | CC-BY-SA-4.0 | Google, crowdsourced Yoruba |
322
  | code-switched pairs | about 9 h of synthetic code-switch training pairs | follows the source clips (CC-BY-SA-4.0 / CC-BY-4.0) | built by concatenating clips from the sets above, following the Google 2019 code-switching recipe (Zhang et al., 2019); no separate Google dataset |
323
+ | base model [FunAudioLLM/Fun-CosyVoice3-0.5B-2512](https://huggingface.co/FunAudioLLM/Fun-CosyVoice3-0.5B-2512) | Flash: LM initialisation, flow, vocoder, tokenizers | Apache-2.0 | FunAudioLLM / Alibaba Tongyi Lab, CosyVoice 3 |
324
+ | [google/byt5-small](https://huggingface.co/google/byt5-small) | base of the Flash diacritic-restoration model (fine-tuned on transcripts of the datasets above) | Apache-2.0 | Google, ByT5 (Xue et al., 2022) |
325
  | base model [FunAudioLLM/CosyVoice2-0.5B](https://huggingface.co/FunAudioLLM/CosyVoice2-0.5B) | LM initialisation, flow, vocoder, tokenizers | Apache-2.0 | FunAudioLLM / Alibaba Tongyi Lab, CosyVoice 2 (Du et al., 2024) |
326
  | optional [GlotLID](https://huggingface.co/cis-lmu/glotlid) (not bundled) | `--lid glotlid` in the tagger | Apache-2.0 plus notices | Kargaran et al., GlotLID (EMNLP 2023) |
327
 
328
+ Flash's training data is the licensed set above (the same sources as Phase 3, re-weighted with less Bible narration and slow read speech, plus IgboSynCorp pseudo-transcripts); its DPO pairs are the model's own renders of eval-style prompts, scored automatically. Excluded on licence grounds from every released checkpoint, Flash included: African Voices / WaZoBiaSpeech (its cards ban TTS use), NaijaVoices (non-commercial), and non-commercial TTS weights. The optional MADLAD-400-3B-MT translator (Apache-2.0) is not bundled; NLLB was avoided because it is non-commercial.
329
 
330
  ## Licence
331
 
332
  - **Model weights, voice prompts and samples** (`checkpoints/`, `prompts/`, `samples/`): **[CC-BY-SA-4.0](LICENSE)**. You may use them commercially. You must credit Modular and the datasets listed above, and anything you derive from the weights (fine-tunes, merges) must be shared under CC-BY-SA-4.0 too.
333
+ - **Code** (`infer.py`, `tagger.py`, `app.py`, `flash_lm.py`, `frontend/`, `code/`, `settings.json`): **[Apache-2.0](LICENSE-CODE)**.
334
+ - The base models, CosyVoice2-0.5B and Fun-CosyVoice3-0.5B-2512 (and byt5-small for the diacritic model), are Apache-2.0; their notices still apply to the parts they contribute.
335
 
336
  ## Citation
337
 
app.py CHANGED
@@ -5,8 +5,8 @@
5
  python app.py --cosyvoice_dir ../CosyVoice # then open http://127.0.0.1:7860
6
 
7
  Text box -> optional auto-tag (editable preview of the tagged text) -> version + voice + rate -> Synthesize, or
8
- Compare to hear Phase 2 and Phase 3 on the same text, seed and voice. Both versions stay loaded (one shared CosyVoice2
9
- base; each version has its own LM copy), so a GPU with >= 16 GB is recommended. CPU works but is slow.
10
  """
11
  import argparse, json, os, sys, types
12
 
@@ -35,7 +35,8 @@ class Engine:
35
  ns = types.SimpleNamespace(**vars(self.a))
36
  ns.voice = voice
37
  S = infer.build(version, ns, self.cosy, self.tok, copy_llm=self.cosy is not None)
38
- self.cosy, self.tok = S.cosy, S.tok
 
39
  S._rate0 = S.rate
40
  self.models[version] = S
41
  S = self.models[version]
@@ -65,7 +66,9 @@ def main():
65
  ap.add_argument("--share", action="store_true")
66
  a = ap.parse_args()
67
  for k, v in dict(lm="", flow="", prompts=os.path.join(HERE, "prompts", "prompts.json"),
68
- voices=os.path.join(HERE, "prompts", "voices.json"), dtype="auto", stitch=False, rate="default").items():
 
 
69
  setattr(a, k, v)
70
  if not os.path.isdir(a.model_dir):
71
  from huggingface_hub import snapshot_download
@@ -98,9 +101,9 @@ def main():
98
 
99
  def compare(text, tagged, auto, lang_map, use_lid, force, voice, rate, seed):
100
  t = tagged.strip() if tagged.strip() else prepare(text, auto, lang_map, use_lid, force)
101
- a2 = E.say(t, "phase2", voice, rate, seed)
102
  a3 = E.say(t, "phase3", voice, rate, seed)
103
- return t, a2, a3
 
104
 
105
  with gr.Blocks(title="Dynamic Accent: code-switching TTS") as demo:
106
  gr.Markdown("## Dynamic Accent: code-switching TTS\nEnglish, Nigerian Pidgin, Igbo, Yoruba and Hausa, mixed in one "
@@ -114,18 +117,18 @@ def main():
114
  tag_btn = gr.Button("Preview tags")
115
  tagged = gr.Textbox(label="Tagged text (edit before synthesis; leave empty to tag automatically)", lines=3)
116
  with gr.Row():
117
- version = gr.Dropdown(sorted(infer.VERSIONS), value=S.get("default_version", "phase3"), label="Version")
118
  voice = gr.Dropdown(voices, value=S.get("default_voice", "anchor_male"), label="Voice")
119
- rate = gr.Dropdown(rates, value="default", label="Rate (phase3)")
120
  seed = gr.Number(value=0, precision=0, label="Seed")
121
  tip = gr.Markdown()
122
  with gr.Row():
123
  go = gr.Button("Synthesize", variant="primary")
124
- cmp_btn = gr.Button("Compare Phase 2 vs Phase 3")
125
  out = gr.Audio(label="Output", type="numpy")
126
  with gr.Row():
127
- out2 = gr.Audio(label="Phase 2", type="numpy")
128
- out3 = gr.Audio(label="Phase 3", type="numpy")
129
  gr.Examples(EXAMPLES, inputs=text)
130
 
131
  tag_btn.click(prepare, [text, auto, lang_map, use_lid, force], tagged)
 
5
  python app.py --cosyvoice_dir ../CosyVoice # then open http://127.0.0.1:7860
6
 
7
  Text box -> optional auto-tag (editable preview of the tagged text) -> version + voice + rate -> Synthesize, or
8
+ Compare to hear Phase 3 and Flash on the same text, seed and voice. Loaded versions stay resident (Phase 2/3 share one
9
+ CosyVoice2 base; Flash has its own CosyVoice3 base), so a GPU with >= 24 GB is recommended. Flash needs CUDA.
10
  """
11
  import argparse, json, os, sys, types
12
 
 
35
  ns = types.SimpleNamespace(**vars(self.a))
36
  ns.voice = voice
37
  S = infer.build(version, ns, self.cosy, self.tok, copy_llm=self.cosy is not None)
38
+ if S.cosy is not None: # flash (CosyVoice3) keeps its own base
39
+ self.cosy, self.tok = S.cosy, S.tok
40
  S._rate0 = S.rate
41
  self.models[version] = S
42
  S = self.models[version]
 
66
  ap.add_argument("--share", action="store_true")
67
  a = ap.parse_args()
68
  for k, v in dict(lm="", flow="", prompts=os.path.join(HERE, "prompts", "prompts.json"),
69
+ voices=os.path.join(HERE, "prompts", "voices.json"), dtype="auto", stitch=False, rate="default",
70
+ frontend="codeswitch", frontend_lang="eng", no_diacritics=False, experimental_translate=False,
71
+ cv3_dir=os.environ.get("CV3_DIR", os.path.join(HERE, "pretrained", "Fun-CosyVoice3-0.5B-2512"))).items():
72
  setattr(a, k, v)
73
  if not os.path.isdir(a.model_dir):
74
  from huggingface_hub import snapshot_download
 
101
 
102
  def compare(text, tagged, auto, lang_map, use_lid, force, voice, rate, seed):
103
  t = tagged.strip() if tagged.strip() else prepare(text, auto, lang_map, use_lid, force)
 
104
  a3 = E.say(t, "phase3", voice, rate, seed)
105
+ af = E.say(t, "flash", voice, rate, seed)
106
+ return t, a3, af
107
 
108
  with gr.Blocks(title="Dynamic Accent: code-switching TTS") as demo:
109
  gr.Markdown("## Dynamic Accent: code-switching TTS\nEnglish, Nigerian Pidgin, Igbo, Yoruba and Hausa, mixed in one "
 
117
  tag_btn = gr.Button("Preview tags")
118
  tagged = gr.Textbox(label="Tagged text (edit before synthesis; leave empty to tag automatically)", lines=3)
119
  with gr.Row():
120
+ version = gr.Dropdown(sorted(infer.VERSIONS), value=S.get("default_version", "flash"), label="Version")
121
  voice = gr.Dropdown(voices, value=S.get("default_voice", "anchor_male"), label="Voice")
122
+ rate = gr.Dropdown(rates, value="default", label="Rate (phase3/flash)")
123
  seed = gr.Number(value=0, precision=0, label="Seed")
124
  tip = gr.Markdown()
125
  with gr.Row():
126
  go = gr.Button("Synthesize", variant="primary")
127
+ cmp_btn = gr.Button("Compare Phase 3 vs Flash")
128
  out = gr.Audio(label="Output", type="numpy")
129
  with gr.Row():
130
+ out2 = gr.Audio(label="Phase 3", type="numpy")
131
+ out3 = gr.Audio(label="Flash", type="numpy")
132
  gr.Examples(EXAMPLES, inputs=text)
133
 
134
  tag_btn.click(prepare, [text, auto, lang_map, use_lid, force], tagged)
checkpoints/flash/diacritics_byt5/added_tokens.json ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "<extra_id_0>": 259,
3
+ "<extra_id_100>": 359,
4
+ "<extra_id_101>": 360,
5
+ "<extra_id_102>": 361,
6
+ "<extra_id_103>": 362,
7
+ "<extra_id_104>": 363,
8
+ "<extra_id_105>": 364,
9
+ "<extra_id_106>": 365,
10
+ "<extra_id_107>": 366,
11
+ "<extra_id_108>": 367,
12
+ "<extra_id_109>": 368,
13
+ "<extra_id_10>": 269,
14
+ "<extra_id_110>": 369,
15
+ "<extra_id_111>": 370,
16
+ "<extra_id_112>": 371,
17
+ "<extra_id_113>": 372,
18
+ "<extra_id_114>": 373,
19
+ "<extra_id_115>": 374,
20
+ "<extra_id_116>": 375,
21
+ "<extra_id_117>": 376,
22
+ "<extra_id_118>": 377,
23
+ "<extra_id_119>": 378,
24
+ "<extra_id_11>": 270,
25
+ "<extra_id_120>": 379,
26
+ "<extra_id_121>": 380,
27
+ "<extra_id_122>": 381,
28
+ "<extra_id_123>": 382,
29
+ "<extra_id_124>": 383,
30
+ "<extra_id_12>": 271,
31
+ "<extra_id_13>": 272,
32
+ "<extra_id_14>": 273,
33
+ "<extra_id_15>": 274,
34
+ "<extra_id_16>": 275,
35
+ "<extra_id_17>": 276,
36
+ "<extra_id_18>": 277,
37
+ "<extra_id_19>": 278,
38
+ "<extra_id_1>": 260,
39
+ "<extra_id_20>": 279,
40
+ "<extra_id_21>": 280,
41
+ "<extra_id_22>": 281,
42
+ "<extra_id_23>": 282,
43
+ "<extra_id_24>": 283,
44
+ "<extra_id_25>": 284,
45
+ "<extra_id_26>": 285,
46
+ "<extra_id_27>": 286,
47
+ "<extra_id_28>": 287,
48
+ "<extra_id_29>": 288,
49
+ "<extra_id_2>": 261,
50
+ "<extra_id_30>": 289,
51
+ "<extra_id_31>": 290,
52
+ "<extra_id_32>": 291,
53
+ "<extra_id_33>": 292,
54
+ "<extra_id_34>": 293,
55
+ "<extra_id_35>": 294,
56
+ "<extra_id_36>": 295,
57
+ "<extra_id_37>": 296,
58
+ "<extra_id_38>": 297,
59
+ "<extra_id_39>": 298,
60
+ "<extra_id_3>": 262,
61
+ "<extra_id_40>": 299,
62
+ "<extra_id_41>": 300,
63
+ "<extra_id_42>": 301,
64
+ "<extra_id_43>": 302,
65
+ "<extra_id_44>": 303,
66
+ "<extra_id_45>": 304,
67
+ "<extra_id_46>": 305,
68
+ "<extra_id_47>": 306,
69
+ "<extra_id_48>": 307,
70
+ "<extra_id_49>": 308,
71
+ "<extra_id_4>": 263,
72
+ "<extra_id_50>": 309,
73
+ "<extra_id_51>": 310,
74
+ "<extra_id_52>": 311,
75
+ "<extra_id_53>": 312,
76
+ "<extra_id_54>": 313,
77
+ "<extra_id_55>": 314,
78
+ "<extra_id_56>": 315,
79
+ "<extra_id_57>": 316,
80
+ "<extra_id_58>": 317,
81
+ "<extra_id_59>": 318,
82
+ "<extra_id_5>": 264,
83
+ "<extra_id_60>": 319,
84
+ "<extra_id_61>": 320,
85
+ "<extra_id_62>": 321,
86
+ "<extra_id_63>": 322,
87
+ "<extra_id_64>": 323,
88
+ "<extra_id_65>": 324,
89
+ "<extra_id_66>": 325,
90
+ "<extra_id_67>": 326,
91
+ "<extra_id_68>": 327,
92
+ "<extra_id_69>": 328,
93
+ "<extra_id_6>": 265,
94
+ "<extra_id_70>": 329,
95
+ "<extra_id_71>": 330,
96
+ "<extra_id_72>": 331,
97
+ "<extra_id_73>": 332,
98
+ "<extra_id_74>": 333,
99
+ "<extra_id_75>": 334,
100
+ "<extra_id_76>": 335,
101
+ "<extra_id_77>": 336,
102
+ "<extra_id_78>": 337,
103
+ "<extra_id_79>": 338,
104
+ "<extra_id_7>": 266,
105
+ "<extra_id_80>": 339,
106
+ "<extra_id_81>": 340,
107
+ "<extra_id_82>": 341,
108
+ "<extra_id_83>": 342,
109
+ "<extra_id_84>": 343,
110
+ "<extra_id_85>": 344,
111
+ "<extra_id_86>": 345,
112
+ "<extra_id_87>": 346,
113
+ "<extra_id_88>": 347,
114
+ "<extra_id_89>": 348,
115
+ "<extra_id_8>": 267,
116
+ "<extra_id_90>": 349,
117
+ "<extra_id_91>": 350,
118
+ "<extra_id_92>": 351,
119
+ "<extra_id_93>": 352,
120
+ "<extra_id_94>": 353,
121
+ "<extra_id_95>": 354,
122
+ "<extra_id_96>": 355,
123
+ "<extra_id_97>": 356,
124
+ "<extra_id_98>": 357,
125
+ "<extra_id_99>": 358,
126
+ "<extra_id_9>": 268
127
+ }
checkpoints/flash/diacritics_byt5/config.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "T5ForConditionalGeneration"
4
+ ],
5
+ "classifier_dropout": 0.0,
6
+ "d_ff": 3584,
7
+ "d_kv": 64,
8
+ "d_model": 1472,
9
+ "decoder_start_token_id": 0,
10
+ "dense_act_fn": "gelu_new",
11
+ "dropout_rate": 0.1,
12
+ "eos_token_id": 1,
13
+ "feed_forward_proj": "gated-gelu",
14
+ "gradient_checkpointing": false,
15
+ "initializer_factor": 1.0,
16
+ "is_encoder_decoder": true,
17
+ "is_gated_act": true,
18
+ "layer_norm_epsilon": 1e-06,
19
+ "model_type": "t5",
20
+ "num_decoder_layers": 4,
21
+ "num_heads": 6,
22
+ "num_layers": 12,
23
+ "pad_token_id": 0,
24
+ "relative_attention_max_distance": 128,
25
+ "relative_attention_num_buckets": 32,
26
+ "tie_word_embeddings": false,
27
+ "tokenizer_class": "ByT5Tokenizer",
28
+ "torch_dtype": "float32",
29
+ "transformers_version": "4.51.3",
30
+ "use_cache": true,
31
+ "vocab_size": 384
32
+ }
checkpoints/flash/diacritics_byt5/generation_config.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "_from_model_config": true,
3
+ "decoder_start_token_id": 0,
4
+ "eos_token_id": 1,
5
+ "pad_token_id": 0,
6
+ "transformers_version": "4.51.3"
7
+ }
checkpoints/flash/diacritics_byt5/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:27c2f5f06306a51af9f6107c38aebff2606d7589cd4f870a4478c7b7047f789e
3
+ size 1198571496
checkpoints/flash/diacritics_byt5/special_tokens_map.json ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "additional_special_tokens": [
3
+ "<extra_id_0>",
4
+ "<extra_id_1>",
5
+ "<extra_id_2>",
6
+ "<extra_id_3>",
7
+ "<extra_id_4>",
8
+ "<extra_id_5>",
9
+ "<extra_id_6>",
10
+ "<extra_id_7>",
11
+ "<extra_id_8>",
12
+ "<extra_id_9>",
13
+ "<extra_id_10>",
14
+ "<extra_id_11>",
15
+ "<extra_id_12>",
16
+ "<extra_id_13>",
17
+ "<extra_id_14>",
18
+ "<extra_id_15>",
19
+ "<extra_id_16>",
20
+ "<extra_id_17>",
21
+ "<extra_id_18>",
22
+ "<extra_id_19>",
23
+ "<extra_id_20>",
24
+ "<extra_id_21>",
25
+ "<extra_id_22>",
26
+ "<extra_id_23>",
27
+ "<extra_id_24>",
28
+ "<extra_id_25>",
29
+ "<extra_id_26>",
30
+ "<extra_id_27>",
31
+ "<extra_id_28>",
32
+ "<extra_id_29>",
33
+ "<extra_id_30>",
34
+ "<extra_id_31>",
35
+ "<extra_id_32>",
36
+ "<extra_id_33>",
37
+ "<extra_id_34>",
38
+ "<extra_id_35>",
39
+ "<extra_id_36>",
40
+ "<extra_id_37>",
41
+ "<extra_id_38>",
42
+ "<extra_id_39>",
43
+ "<extra_id_40>",
44
+ "<extra_id_41>",
45
+ "<extra_id_42>",
46
+ "<extra_id_43>",
47
+ "<extra_id_44>",
48
+ "<extra_id_45>",
49
+ "<extra_id_46>",
50
+ "<extra_id_47>",
51
+ "<extra_id_48>",
52
+ "<extra_id_49>",
53
+ "<extra_id_50>",
54
+ "<extra_id_51>",
55
+ "<extra_id_52>",
56
+ "<extra_id_53>",
57
+ "<extra_id_54>",
58
+ "<extra_id_55>",
59
+ "<extra_id_56>",
60
+ "<extra_id_57>",
61
+ "<extra_id_58>",
62
+ "<extra_id_59>",
63
+ "<extra_id_60>",
64
+ "<extra_id_61>",
65
+ "<extra_id_62>",
66
+ "<extra_id_63>",
67
+ "<extra_id_64>",
68
+ "<extra_id_65>",
69
+ "<extra_id_66>",
70
+ "<extra_id_67>",
71
+ "<extra_id_68>",
72
+ "<extra_id_69>",
73
+ "<extra_id_70>",
74
+ "<extra_id_71>",
75
+ "<extra_id_72>",
76
+ "<extra_id_73>",
77
+ "<extra_id_74>",
78
+ "<extra_id_75>",
79
+ "<extra_id_76>",
80
+ "<extra_id_77>",
81
+ "<extra_id_78>",
82
+ "<extra_id_79>",
83
+ "<extra_id_80>",
84
+ "<extra_id_81>",
85
+ "<extra_id_82>",
86
+ "<extra_id_83>",
87
+ "<extra_id_84>",
88
+ "<extra_id_85>",
89
+ "<extra_id_86>",
90
+ "<extra_id_87>",
91
+ "<extra_id_88>",
92
+ "<extra_id_89>",
93
+ "<extra_id_90>",
94
+ "<extra_id_91>",
95
+ "<extra_id_92>",
96
+ "<extra_id_93>",
97
+ "<extra_id_94>",
98
+ "<extra_id_95>",
99
+ "<extra_id_96>",
100
+ "<extra_id_97>",
101
+ "<extra_id_98>",
102
+ "<extra_id_99>",
103
+ "<extra_id_100>",
104
+ "<extra_id_101>",
105
+ "<extra_id_102>",
106
+ "<extra_id_103>",
107
+ "<extra_id_104>",
108
+ "<extra_id_105>",
109
+ "<extra_id_106>",
110
+ "<extra_id_107>",
111
+ "<extra_id_108>",
112
+ "<extra_id_109>",
113
+ "<extra_id_110>",
114
+ "<extra_id_111>",
115
+ "<extra_id_112>",
116
+ "<extra_id_113>",
117
+ "<extra_id_114>",
118
+ "<extra_id_115>",
119
+ "<extra_id_116>",
120
+ "<extra_id_117>",
121
+ "<extra_id_118>",
122
+ "<extra_id_119>",
123
+ "<extra_id_120>",
124
+ "<extra_id_121>",
125
+ "<extra_id_122>",
126
+ "<extra_id_123>",
127
+ "<extra_id_124>"
128
+ ],
129
+ "eos_token": {
130
+ "content": "</s>",
131
+ "lstrip": false,
132
+ "normalized": true,
133
+ "rstrip": false,
134
+ "single_word": false
135
+ },
136
+ "pad_token": {
137
+ "content": "<pad>",
138
+ "lstrip": false,
139
+ "normalized": true,
140
+ "rstrip": false,
141
+ "single_word": false
142
+ },
143
+ "unk_token": {
144
+ "content": "<unk>",
145
+ "lstrip": false,
146
+ "normalized": true,
147
+ "rstrip": false,
148
+ "single_word": false
149
+ }
150
+ }
checkpoints/flash/diacritics_byt5/tokenizer_config.json ADDED
@@ -0,0 +1,1163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "added_tokens_decoder": {
3
+ "0": {
4
+ "content": "<pad>",
5
+ "lstrip": false,
6
+ "normalized": true,
7
+ "rstrip": false,
8
+ "single_word": false,
9
+ "special": true
10
+ },
11
+ "1": {
12
+ "content": "</s>",
13
+ "lstrip": false,
14
+ "normalized": true,
15
+ "rstrip": false,
16
+ "single_word": false,
17
+ "special": true
18
+ },
19
+ "2": {
20
+ "content": "<unk>",
21
+ "lstrip": false,
22
+ "normalized": true,
23
+ "rstrip": false,
24
+ "single_word": false,
25
+ "special": true
26
+ },
27
+ "259": {
28
+ "content": "<extra_id_0>",
29
+ "lstrip": false,
30
+ "normalized": false,
31
+ "rstrip": false,
32
+ "single_word": false,
33
+ "special": true
34
+ },
35
+ "260": {
36
+ "content": "<extra_id_1>",
37
+ "lstrip": false,
38
+ "normalized": false,
39
+ "rstrip": false,
40
+ "single_word": false,
41
+ "special": true
42
+ },
43
+ "261": {
44
+ "content": "<extra_id_2>",
45
+ "lstrip": false,
46
+ "normalized": false,
47
+ "rstrip": false,
48
+ "single_word": false,
49
+ "special": true
50
+ },
51
+ "262": {
52
+ "content": "<extra_id_3>",
53
+ "lstrip": false,
54
+ "normalized": false,
55
+ "rstrip": false,
56
+ "single_word": false,
57
+ "special": true
58
+ },
59
+ "263": {
60
+ "content": "<extra_id_4>",
61
+ "lstrip": false,
62
+ "normalized": false,
63
+ "rstrip": false,
64
+ "single_word": false,
65
+ "special": true
66
+ },
67
+ "264": {
68
+ "content": "<extra_id_5>",
69
+ "lstrip": false,
70
+ "normalized": false,
71
+ "rstrip": false,
72
+ "single_word": false,
73
+ "special": true
74
+ },
75
+ "265": {
76
+ "content": "<extra_id_6>",
77
+ "lstrip": false,
78
+ "normalized": false,
79
+ "rstrip": false,
80
+ "single_word": false,
81
+ "special": true
82
+ },
83
+ "266": {
84
+ "content": "<extra_id_7>",
85
+ "lstrip": false,
86
+ "normalized": false,
87
+ "rstrip": false,
88
+ "single_word": false,
89
+ "special": true
90
+ },
91
+ "267": {
92
+ "content": "<extra_id_8>",
93
+ "lstrip": false,
94
+ "normalized": false,
95
+ "rstrip": false,
96
+ "single_word": false,
97
+ "special": true
98
+ },
99
+ "268": {
100
+ "content": "<extra_id_9>",
101
+ "lstrip": false,
102
+ "normalized": false,
103
+ "rstrip": false,
104
+ "single_word": false,
105
+ "special": true
106
+ },
107
+ "269": {
108
+ "content": "<extra_id_10>",
109
+ "lstrip": false,
110
+ "normalized": false,
111
+ "rstrip": false,
112
+ "single_word": false,
113
+ "special": true
114
+ },
115
+ "270": {
116
+ "content": "<extra_id_11>",
117
+ "lstrip": false,
118
+ "normalized": false,
119
+ "rstrip": false,
120
+ "single_word": false,
121
+ "special": true
122
+ },
123
+ "271": {
124
+ "content": "<extra_id_12>",
125
+ "lstrip": false,
126
+ "normalized": false,
127
+ "rstrip": false,
128
+ "single_word": false,
129
+ "special": true
130
+ },
131
+ "272": {
132
+ "content": "<extra_id_13>",
133
+ "lstrip": false,
134
+ "normalized": false,
135
+ "rstrip": false,
136
+ "single_word": false,
137
+ "special": true
138
+ },
139
+ "273": {
140
+ "content": "<extra_id_14>",
141
+ "lstrip": false,
142
+ "normalized": false,
143
+ "rstrip": false,
144
+ "single_word": false,
145
+ "special": true
146
+ },
147
+ "274": {
148
+ "content": "<extra_id_15>",
149
+ "lstrip": false,
150
+ "normalized": false,
151
+ "rstrip": false,
152
+ "single_word": false,
153
+ "special": true
154
+ },
155
+ "275": {
156
+ "content": "<extra_id_16>",
157
+ "lstrip": false,
158
+ "normalized": false,
159
+ "rstrip": false,
160
+ "single_word": false,
161
+ "special": true
162
+ },
163
+ "276": {
164
+ "content": "<extra_id_17>",
165
+ "lstrip": false,
166
+ "normalized": false,
167
+ "rstrip": false,
168
+ "single_word": false,
169
+ "special": true
170
+ },
171
+ "277": {
172
+ "content": "<extra_id_18>",
173
+ "lstrip": false,
174
+ "normalized": false,
175
+ "rstrip": false,
176
+ "single_word": false,
177
+ "special": true
178
+ },
179
+ "278": {
180
+ "content": "<extra_id_19>",
181
+ "lstrip": false,
182
+ "normalized": false,
183
+ "rstrip": false,
184
+ "single_word": false,
185
+ "special": true
186
+ },
187
+ "279": {
188
+ "content": "<extra_id_20>",
189
+ "lstrip": false,
190
+ "normalized": false,
191
+ "rstrip": false,
192
+ "single_word": false,
193
+ "special": true
194
+ },
195
+ "280": {
196
+ "content": "<extra_id_21>",
197
+ "lstrip": false,
198
+ "normalized": false,
199
+ "rstrip": false,
200
+ "single_word": false,
201
+ "special": true
202
+ },
203
+ "281": {
204
+ "content": "<extra_id_22>",
205
+ "lstrip": false,
206
+ "normalized": false,
207
+ "rstrip": false,
208
+ "single_word": false,
209
+ "special": true
210
+ },
211
+ "282": {
212
+ "content": "<extra_id_23>",
213
+ "lstrip": false,
214
+ "normalized": false,
215
+ "rstrip": false,
216
+ "single_word": false,
217
+ "special": true
218
+ },
219
+ "283": {
220
+ "content": "<extra_id_24>",
221
+ "lstrip": false,
222
+ "normalized": false,
223
+ "rstrip": false,
224
+ "single_word": false,
225
+ "special": true
226
+ },
227
+ "284": {
228
+ "content": "<extra_id_25>",
229
+ "lstrip": false,
230
+ "normalized": false,
231
+ "rstrip": false,
232
+ "single_word": false,
233
+ "special": true
234
+ },
235
+ "285": {
236
+ "content": "<extra_id_26>",
237
+ "lstrip": false,
238
+ "normalized": false,
239
+ "rstrip": false,
240
+ "single_word": false,
241
+ "special": true
242
+ },
243
+ "286": {
244
+ "content": "<extra_id_27>",
245
+ "lstrip": false,
246
+ "normalized": false,
247
+ "rstrip": false,
248
+ "single_word": false,
249
+ "special": true
250
+ },
251
+ "287": {
252
+ "content": "<extra_id_28>",
253
+ "lstrip": false,
254
+ "normalized": false,
255
+ "rstrip": false,
256
+ "single_word": false,
257
+ "special": true
258
+ },
259
+ "288": {
260
+ "content": "<extra_id_29>",
261
+ "lstrip": false,
262
+ "normalized": false,
263
+ "rstrip": false,
264
+ "single_word": false,
265
+ "special": true
266
+ },
267
+ "289": {
268
+ "content": "<extra_id_30>",
269
+ "lstrip": false,
270
+ "normalized": false,
271
+ "rstrip": false,
272
+ "single_word": false,
273
+ "special": true
274
+ },
275
+ "290": {
276
+ "content": "<extra_id_31>",
277
+ "lstrip": false,
278
+ "normalized": false,
279
+ "rstrip": false,
280
+ "single_word": false,
281
+ "special": true
282
+ },
283
+ "291": {
284
+ "content": "<extra_id_32>",
285
+ "lstrip": false,
286
+ "normalized": false,
287
+ "rstrip": false,
288
+ "single_word": false,
289
+ "special": true
290
+ },
291
+ "292": {
292
+ "content": "<extra_id_33>",
293
+ "lstrip": false,
294
+ "normalized": false,
295
+ "rstrip": false,
296
+ "single_word": false,
297
+ "special": true
298
+ },
299
+ "293": {
300
+ "content": "<extra_id_34>",
301
+ "lstrip": false,
302
+ "normalized": false,
303
+ "rstrip": false,
304
+ "single_word": false,
305
+ "special": true
306
+ },
307
+ "294": {
308
+ "content": "<extra_id_35>",
309
+ "lstrip": false,
310
+ "normalized": false,
311
+ "rstrip": false,
312
+ "single_word": false,
313
+ "special": true
314
+ },
315
+ "295": {
316
+ "content": "<extra_id_36>",
317
+ "lstrip": false,
318
+ "normalized": false,
319
+ "rstrip": false,
320
+ "single_word": false,
321
+ "special": true
322
+ },
323
+ "296": {
324
+ "content": "<extra_id_37>",
325
+ "lstrip": false,
326
+ "normalized": false,
327
+ "rstrip": false,
328
+ "single_word": false,
329
+ "special": true
330
+ },
331
+ "297": {
332
+ "content": "<extra_id_38>",
333
+ "lstrip": false,
334
+ "normalized": false,
335
+ "rstrip": false,
336
+ "single_word": false,
337
+ "special": true
338
+ },
339
+ "298": {
340
+ "content": "<extra_id_39>",
341
+ "lstrip": false,
342
+ "normalized": false,
343
+ "rstrip": false,
344
+ "single_word": false,
345
+ "special": true
346
+ },
347
+ "299": {
348
+ "content": "<extra_id_40>",
349
+ "lstrip": false,
350
+ "normalized": false,
351
+ "rstrip": false,
352
+ "single_word": false,
353
+ "special": true
354
+ },
355
+ "300": {
356
+ "content": "<extra_id_41>",
357
+ "lstrip": false,
358
+ "normalized": false,
359
+ "rstrip": false,
360
+ "single_word": false,
361
+ "special": true
362
+ },
363
+ "301": {
364
+ "content": "<extra_id_42>",
365
+ "lstrip": false,
366
+ "normalized": false,
367
+ "rstrip": false,
368
+ "single_word": false,
369
+ "special": true
370
+ },
371
+ "302": {
372
+ "content": "<extra_id_43>",
373
+ "lstrip": false,
374
+ "normalized": false,
375
+ "rstrip": false,
376
+ "single_word": false,
377
+ "special": true
378
+ },
379
+ "303": {
380
+ "content": "<extra_id_44>",
381
+ "lstrip": false,
382
+ "normalized": false,
383
+ "rstrip": false,
384
+ "single_word": false,
385
+ "special": true
386
+ },
387
+ "304": {
388
+ "content": "<extra_id_45>",
389
+ "lstrip": false,
390
+ "normalized": false,
391
+ "rstrip": false,
392
+ "single_word": false,
393
+ "special": true
394
+ },
395
+ "305": {
396
+ "content": "<extra_id_46>",
397
+ "lstrip": false,
398
+ "normalized": false,
399
+ "rstrip": false,
400
+ "single_word": false,
401
+ "special": true
402
+ },
403
+ "306": {
404
+ "content": "<extra_id_47>",
405
+ "lstrip": false,
406
+ "normalized": false,
407
+ "rstrip": false,
408
+ "single_word": false,
409
+ "special": true
410
+ },
411
+ "307": {
412
+ "content": "<extra_id_48>",
413
+ "lstrip": false,
414
+ "normalized": false,
415
+ "rstrip": false,
416
+ "single_word": false,
417
+ "special": true
418
+ },
419
+ "308": {
420
+ "content": "<extra_id_49>",
421
+ "lstrip": false,
422
+ "normalized": false,
423
+ "rstrip": false,
424
+ "single_word": false,
425
+ "special": true
426
+ },
427
+ "309": {
428
+ "content": "<extra_id_50>",
429
+ "lstrip": false,
430
+ "normalized": false,
431
+ "rstrip": false,
432
+ "single_word": false,
433
+ "special": true
434
+ },
435
+ "310": {
436
+ "content": "<extra_id_51>",
437
+ "lstrip": false,
438
+ "normalized": false,
439
+ "rstrip": false,
440
+ "single_word": false,
441
+ "special": true
442
+ },
443
+ "311": {
444
+ "content": "<extra_id_52>",
445
+ "lstrip": false,
446
+ "normalized": false,
447
+ "rstrip": false,
448
+ "single_word": false,
449
+ "special": true
450
+ },
451
+ "312": {
452
+ "content": "<extra_id_53>",
453
+ "lstrip": false,
454
+ "normalized": false,
455
+ "rstrip": false,
456
+ "single_word": false,
457
+ "special": true
458
+ },
459
+ "313": {
460
+ "content": "<extra_id_54>",
461
+ "lstrip": false,
462
+ "normalized": false,
463
+ "rstrip": false,
464
+ "single_word": false,
465
+ "special": true
466
+ },
467
+ "314": {
468
+ "content": "<extra_id_55>",
469
+ "lstrip": false,
470
+ "normalized": false,
471
+ "rstrip": false,
472
+ "single_word": false,
473
+ "special": true
474
+ },
475
+ "315": {
476
+ "content": "<extra_id_56>",
477
+ "lstrip": false,
478
+ "normalized": false,
479
+ "rstrip": false,
480
+ "single_word": false,
481
+ "special": true
482
+ },
483
+ "316": {
484
+ "content": "<extra_id_57>",
485
+ "lstrip": false,
486
+ "normalized": false,
487
+ "rstrip": false,
488
+ "single_word": false,
489
+ "special": true
490
+ },
491
+ "317": {
492
+ "content": "<extra_id_58>",
493
+ "lstrip": false,
494
+ "normalized": false,
495
+ "rstrip": false,
496
+ "single_word": false,
497
+ "special": true
498
+ },
499
+ "318": {
500
+ "content": "<extra_id_59>",
501
+ "lstrip": false,
502
+ "normalized": false,
503
+ "rstrip": false,
504
+ "single_word": false,
505
+ "special": true
506
+ },
507
+ "319": {
508
+ "content": "<extra_id_60>",
509
+ "lstrip": false,
510
+ "normalized": false,
511
+ "rstrip": false,
512
+ "single_word": false,
513
+ "special": true
514
+ },
515
+ "320": {
516
+ "content": "<extra_id_61>",
517
+ "lstrip": false,
518
+ "normalized": false,
519
+ "rstrip": false,
520
+ "single_word": false,
521
+ "special": true
522
+ },
523
+ "321": {
524
+ "content": "<extra_id_62>",
525
+ "lstrip": false,
526
+ "normalized": false,
527
+ "rstrip": false,
528
+ "single_word": false,
529
+ "special": true
530
+ },
531
+ "322": {
532
+ "content": "<extra_id_63>",
533
+ "lstrip": false,
534
+ "normalized": false,
535
+ "rstrip": false,
536
+ "single_word": false,
537
+ "special": true
538
+ },
539
+ "323": {
540
+ "content": "<extra_id_64>",
541
+ "lstrip": false,
542
+ "normalized": false,
543
+ "rstrip": false,
544
+ "single_word": false,
545
+ "special": true
546
+ },
547
+ "324": {
548
+ "content": "<extra_id_65>",
549
+ "lstrip": false,
550
+ "normalized": false,
551
+ "rstrip": false,
552
+ "single_word": false,
553
+ "special": true
554
+ },
555
+ "325": {
556
+ "content": "<extra_id_66>",
557
+ "lstrip": false,
558
+ "normalized": false,
559
+ "rstrip": false,
560
+ "single_word": false,
561
+ "special": true
562
+ },
563
+ "326": {
564
+ "content": "<extra_id_67>",
565
+ "lstrip": false,
566
+ "normalized": false,
567
+ "rstrip": false,
568
+ "single_word": false,
569
+ "special": true
570
+ },
571
+ "327": {
572
+ "content": "<extra_id_68>",
573
+ "lstrip": false,
574
+ "normalized": false,
575
+ "rstrip": false,
576
+ "single_word": false,
577
+ "special": true
578
+ },
579
+ "328": {
580
+ "content": "<extra_id_69>",
581
+ "lstrip": false,
582
+ "normalized": false,
583
+ "rstrip": false,
584
+ "single_word": false,
585
+ "special": true
586
+ },
587
+ "329": {
588
+ "content": "<extra_id_70>",
589
+ "lstrip": false,
590
+ "normalized": false,
591
+ "rstrip": false,
592
+ "single_word": false,
593
+ "special": true
594
+ },
595
+ "330": {
596
+ "content": "<extra_id_71>",
597
+ "lstrip": false,
598
+ "normalized": false,
599
+ "rstrip": false,
600
+ "single_word": false,
601
+ "special": true
602
+ },
603
+ "331": {
604
+ "content": "<extra_id_72>",
605
+ "lstrip": false,
606
+ "normalized": false,
607
+ "rstrip": false,
608
+ "single_word": false,
609
+ "special": true
610
+ },
611
+ "332": {
612
+ "content": "<extra_id_73>",
613
+ "lstrip": false,
614
+ "normalized": false,
615
+ "rstrip": false,
616
+ "single_word": false,
617
+ "special": true
618
+ },
619
+ "333": {
620
+ "content": "<extra_id_74>",
621
+ "lstrip": false,
622
+ "normalized": false,
623
+ "rstrip": false,
624
+ "single_word": false,
625
+ "special": true
626
+ },
627
+ "334": {
628
+ "content": "<extra_id_75>",
629
+ "lstrip": false,
630
+ "normalized": false,
631
+ "rstrip": false,
632
+ "single_word": false,
633
+ "special": true
634
+ },
635
+ "335": {
636
+ "content": "<extra_id_76>",
637
+ "lstrip": false,
638
+ "normalized": false,
639
+ "rstrip": false,
640
+ "single_word": false,
641
+ "special": true
642
+ },
643
+ "336": {
644
+ "content": "<extra_id_77>",
645
+ "lstrip": false,
646
+ "normalized": false,
647
+ "rstrip": false,
648
+ "single_word": false,
649
+ "special": true
650
+ },
651
+ "337": {
652
+ "content": "<extra_id_78>",
653
+ "lstrip": false,
654
+ "normalized": false,
655
+ "rstrip": false,
656
+ "single_word": false,
657
+ "special": true
658
+ },
659
+ "338": {
660
+ "content": "<extra_id_79>",
661
+ "lstrip": false,
662
+ "normalized": false,
663
+ "rstrip": false,
664
+ "single_word": false,
665
+ "special": true
666
+ },
667
+ "339": {
668
+ "content": "<extra_id_80>",
669
+ "lstrip": false,
670
+ "normalized": false,
671
+ "rstrip": false,
672
+ "single_word": false,
673
+ "special": true
674
+ },
675
+ "340": {
676
+ "content": "<extra_id_81>",
677
+ "lstrip": false,
678
+ "normalized": false,
679
+ "rstrip": false,
680
+ "single_word": false,
681
+ "special": true
682
+ },
683
+ "341": {
684
+ "content": "<extra_id_82>",
685
+ "lstrip": false,
686
+ "normalized": false,
687
+ "rstrip": false,
688
+ "single_word": false,
689
+ "special": true
690
+ },
691
+ "342": {
692
+ "content": "<extra_id_83>",
693
+ "lstrip": false,
694
+ "normalized": false,
695
+ "rstrip": false,
696
+ "single_word": false,
697
+ "special": true
698
+ },
699
+ "343": {
700
+ "content": "<extra_id_84>",
701
+ "lstrip": false,
702
+ "normalized": false,
703
+ "rstrip": false,
704
+ "single_word": false,
705
+ "special": true
706
+ },
707
+ "344": {
708
+ "content": "<extra_id_85>",
709
+ "lstrip": false,
710
+ "normalized": false,
711
+ "rstrip": false,
712
+ "single_word": false,
713
+ "special": true
714
+ },
715
+ "345": {
716
+ "content": "<extra_id_86>",
717
+ "lstrip": false,
718
+ "normalized": false,
719
+ "rstrip": false,
720
+ "single_word": false,
721
+ "special": true
722
+ },
723
+ "346": {
724
+ "content": "<extra_id_87>",
725
+ "lstrip": false,
726
+ "normalized": false,
727
+ "rstrip": false,
728
+ "single_word": false,
729
+ "special": true
730
+ },
731
+ "347": {
732
+ "content": "<extra_id_88>",
733
+ "lstrip": false,
734
+ "normalized": false,
735
+ "rstrip": false,
736
+ "single_word": false,
737
+ "special": true
738
+ },
739
+ "348": {
740
+ "content": "<extra_id_89>",
741
+ "lstrip": false,
742
+ "normalized": false,
743
+ "rstrip": false,
744
+ "single_word": false,
745
+ "special": true
746
+ },
747
+ "349": {
748
+ "content": "<extra_id_90>",
749
+ "lstrip": false,
750
+ "normalized": false,
751
+ "rstrip": false,
752
+ "single_word": false,
753
+ "special": true
754
+ },
755
+ "350": {
756
+ "content": "<extra_id_91>",
757
+ "lstrip": false,
758
+ "normalized": false,
759
+ "rstrip": false,
760
+ "single_word": false,
761
+ "special": true
762
+ },
763
+ "351": {
764
+ "content": "<extra_id_92>",
765
+ "lstrip": false,
766
+ "normalized": false,
767
+ "rstrip": false,
768
+ "single_word": false,
769
+ "special": true
770
+ },
771
+ "352": {
772
+ "content": "<extra_id_93>",
773
+ "lstrip": false,
774
+ "normalized": false,
775
+ "rstrip": false,
776
+ "single_word": false,
777
+ "special": true
778
+ },
779
+ "353": {
780
+ "content": "<extra_id_94>",
781
+ "lstrip": false,
782
+ "normalized": false,
783
+ "rstrip": false,
784
+ "single_word": false,
785
+ "special": true
786
+ },
787
+ "354": {
788
+ "content": "<extra_id_95>",
789
+ "lstrip": false,
790
+ "normalized": false,
791
+ "rstrip": false,
792
+ "single_word": false,
793
+ "special": true
794
+ },
795
+ "355": {
796
+ "content": "<extra_id_96>",
797
+ "lstrip": false,
798
+ "normalized": false,
799
+ "rstrip": false,
800
+ "single_word": false,
801
+ "special": true
802
+ },
803
+ "356": {
804
+ "content": "<extra_id_97>",
805
+ "lstrip": false,
806
+ "normalized": false,
807
+ "rstrip": false,
808
+ "single_word": false,
809
+ "special": true
810
+ },
811
+ "357": {
812
+ "content": "<extra_id_98>",
813
+ "lstrip": false,
814
+ "normalized": false,
815
+ "rstrip": false,
816
+ "single_word": false,
817
+ "special": true
818
+ },
819
+ "358": {
820
+ "content": "<extra_id_99>",
821
+ "lstrip": false,
822
+ "normalized": false,
823
+ "rstrip": false,
824
+ "single_word": false,
825
+ "special": true
826
+ },
827
+ "359": {
828
+ "content": "<extra_id_100>",
829
+ "lstrip": false,
830
+ "normalized": false,
831
+ "rstrip": false,
832
+ "single_word": false,
833
+ "special": true
834
+ },
835
+ "360": {
836
+ "content": "<extra_id_101>",
837
+ "lstrip": false,
838
+ "normalized": false,
839
+ "rstrip": false,
840
+ "single_word": false,
841
+ "special": true
842
+ },
843
+ "361": {
844
+ "content": "<extra_id_102>",
845
+ "lstrip": false,
846
+ "normalized": false,
847
+ "rstrip": false,
848
+ "single_word": false,
849
+ "special": true
850
+ },
851
+ "362": {
852
+ "content": "<extra_id_103>",
853
+ "lstrip": false,
854
+ "normalized": false,
855
+ "rstrip": false,
856
+ "single_word": false,
857
+ "special": true
858
+ },
859
+ "363": {
860
+ "content": "<extra_id_104>",
861
+ "lstrip": false,
862
+ "normalized": false,
863
+ "rstrip": false,
864
+ "single_word": false,
865
+ "special": true
866
+ },
867
+ "364": {
868
+ "content": "<extra_id_105>",
869
+ "lstrip": false,
870
+ "normalized": false,
871
+ "rstrip": false,
872
+ "single_word": false,
873
+ "special": true
874
+ },
875
+ "365": {
876
+ "content": "<extra_id_106>",
877
+ "lstrip": false,
878
+ "normalized": false,
879
+ "rstrip": false,
880
+ "single_word": false,
881
+ "special": true
882
+ },
883
+ "366": {
884
+ "content": "<extra_id_107>",
885
+ "lstrip": false,
886
+ "normalized": false,
887
+ "rstrip": false,
888
+ "single_word": false,
889
+ "special": true
890
+ },
891
+ "367": {
892
+ "content": "<extra_id_108>",
893
+ "lstrip": false,
894
+ "normalized": false,
895
+ "rstrip": false,
896
+ "single_word": false,
897
+ "special": true
898
+ },
899
+ "368": {
900
+ "content": "<extra_id_109>",
901
+ "lstrip": false,
902
+ "normalized": false,
903
+ "rstrip": false,
904
+ "single_word": false,
905
+ "special": true
906
+ },
907
+ "369": {
908
+ "content": "<extra_id_110>",
909
+ "lstrip": false,
910
+ "normalized": false,
911
+ "rstrip": false,
912
+ "single_word": false,
913
+ "special": true
914
+ },
915
+ "370": {
916
+ "content": "<extra_id_111>",
917
+ "lstrip": false,
918
+ "normalized": false,
919
+ "rstrip": false,
920
+ "single_word": false,
921
+ "special": true
922
+ },
923
+ "371": {
924
+ "content": "<extra_id_112>",
925
+ "lstrip": false,
926
+ "normalized": false,
927
+ "rstrip": false,
928
+ "single_word": false,
929
+ "special": true
930
+ },
931
+ "372": {
932
+ "content": "<extra_id_113>",
933
+ "lstrip": false,
934
+ "normalized": false,
935
+ "rstrip": false,
936
+ "single_word": false,
937
+ "special": true
938
+ },
939
+ "373": {
940
+ "content": "<extra_id_114>",
941
+ "lstrip": false,
942
+ "normalized": false,
943
+ "rstrip": false,
944
+ "single_word": false,
945
+ "special": true
946
+ },
947
+ "374": {
948
+ "content": "<extra_id_115>",
949
+ "lstrip": false,
950
+ "normalized": false,
951
+ "rstrip": false,
952
+ "single_word": false,
953
+ "special": true
954
+ },
955
+ "375": {
956
+ "content": "<extra_id_116>",
957
+ "lstrip": false,
958
+ "normalized": false,
959
+ "rstrip": false,
960
+ "single_word": false,
961
+ "special": true
962
+ },
963
+ "376": {
964
+ "content": "<extra_id_117>",
965
+ "lstrip": false,
966
+ "normalized": false,
967
+ "rstrip": false,
968
+ "single_word": false,
969
+ "special": true
970
+ },
971
+ "377": {
972
+ "content": "<extra_id_118>",
973
+ "lstrip": false,
974
+ "normalized": false,
975
+ "rstrip": false,
976
+ "single_word": false,
977
+ "special": true
978
+ },
979
+ "378": {
980
+ "content": "<extra_id_119>",
981
+ "lstrip": false,
982
+ "normalized": false,
983
+ "rstrip": false,
984
+ "single_word": false,
985
+ "special": true
986
+ },
987
+ "379": {
988
+ "content": "<extra_id_120>",
989
+ "lstrip": false,
990
+ "normalized": false,
991
+ "rstrip": false,
992
+ "single_word": false,
993
+ "special": true
994
+ },
995
+ "380": {
996
+ "content": "<extra_id_121>",
997
+ "lstrip": false,
998
+ "normalized": false,
999
+ "rstrip": false,
1000
+ "single_word": false,
1001
+ "special": true
1002
+ },
1003
+ "381": {
1004
+ "content": "<extra_id_122>",
1005
+ "lstrip": false,
1006
+ "normalized": false,
1007
+ "rstrip": false,
1008
+ "single_word": false,
1009
+ "special": true
1010
+ },
1011
+ "382": {
1012
+ "content": "<extra_id_123>",
1013
+ "lstrip": false,
1014
+ "normalized": false,
1015
+ "rstrip": false,
1016
+ "single_word": false,
1017
+ "special": true
1018
+ },
1019
+ "383": {
1020
+ "content": "<extra_id_124>",
1021
+ "lstrip": false,
1022
+ "normalized": false,
1023
+ "rstrip": false,
1024
+ "single_word": false,
1025
+ "special": true
1026
+ }
1027
+ },
1028
+ "additional_special_tokens": [
1029
+ "<extra_id_0>",
1030
+ "<extra_id_1>",
1031
+ "<extra_id_2>",
1032
+ "<extra_id_3>",
1033
+ "<extra_id_4>",
1034
+ "<extra_id_5>",
1035
+ "<extra_id_6>",
1036
+ "<extra_id_7>",
1037
+ "<extra_id_8>",
1038
+ "<extra_id_9>",
1039
+ "<extra_id_10>",
1040
+ "<extra_id_11>",
1041
+ "<extra_id_12>",
1042
+ "<extra_id_13>",
1043
+ "<extra_id_14>",
1044
+ "<extra_id_15>",
1045
+ "<extra_id_16>",
1046
+ "<extra_id_17>",
1047
+ "<extra_id_18>",
1048
+ "<extra_id_19>",
1049
+ "<extra_id_20>",
1050
+ "<extra_id_21>",
1051
+ "<extra_id_22>",
1052
+ "<extra_id_23>",
1053
+ "<extra_id_24>",
1054
+ "<extra_id_25>",
1055
+ "<extra_id_26>",
1056
+ "<extra_id_27>",
1057
+ "<extra_id_28>",
1058
+ "<extra_id_29>",
1059
+ "<extra_id_30>",
1060
+ "<extra_id_31>",
1061
+ "<extra_id_32>",
1062
+ "<extra_id_33>",
1063
+ "<extra_id_34>",
1064
+ "<extra_id_35>",
1065
+ "<extra_id_36>",
1066
+ "<extra_id_37>",
1067
+ "<extra_id_38>",
1068
+ "<extra_id_39>",
1069
+ "<extra_id_40>",
1070
+ "<extra_id_41>",
1071
+ "<extra_id_42>",
1072
+ "<extra_id_43>",
1073
+ "<extra_id_44>",
1074
+ "<extra_id_45>",
1075
+ "<extra_id_46>",
1076
+ "<extra_id_47>",
1077
+ "<extra_id_48>",
1078
+ "<extra_id_49>",
1079
+ "<extra_id_50>",
1080
+ "<extra_id_51>",
1081
+ "<extra_id_52>",
1082
+ "<extra_id_53>",
1083
+ "<extra_id_54>",
1084
+ "<extra_id_55>",
1085
+ "<extra_id_56>",
1086
+ "<extra_id_57>",
1087
+ "<extra_id_58>",
1088
+ "<extra_id_59>",
1089
+ "<extra_id_60>",
1090
+ "<extra_id_61>",
1091
+ "<extra_id_62>",
1092
+ "<extra_id_63>",
1093
+ "<extra_id_64>",
1094
+ "<extra_id_65>",
1095
+ "<extra_id_66>",
1096
+ "<extra_id_67>",
1097
+ "<extra_id_68>",
1098
+ "<extra_id_69>",
1099
+ "<extra_id_70>",
1100
+ "<extra_id_71>",
1101
+ "<extra_id_72>",
1102
+ "<extra_id_73>",
1103
+ "<extra_id_74>",
1104
+ "<extra_id_75>",
1105
+ "<extra_id_76>",
1106
+ "<extra_id_77>",
1107
+ "<extra_id_78>",
1108
+ "<extra_id_79>",
1109
+ "<extra_id_80>",
1110
+ "<extra_id_81>",
1111
+ "<extra_id_82>",
1112
+ "<extra_id_83>",
1113
+ "<extra_id_84>",
1114
+ "<extra_id_85>",
1115
+ "<extra_id_86>",
1116
+ "<extra_id_87>",
1117
+ "<extra_id_88>",
1118
+ "<extra_id_89>",
1119
+ "<extra_id_90>",
1120
+ "<extra_id_91>",
1121
+ "<extra_id_92>",
1122
+ "<extra_id_93>",
1123
+ "<extra_id_94>",
1124
+ "<extra_id_95>",
1125
+ "<extra_id_96>",
1126
+ "<extra_id_97>",
1127
+ "<extra_id_98>",
1128
+ "<extra_id_99>",
1129
+ "<extra_id_100>",
1130
+ "<extra_id_101>",
1131
+ "<extra_id_102>",
1132
+ "<extra_id_103>",
1133
+ "<extra_id_104>",
1134
+ "<extra_id_105>",
1135
+ "<extra_id_106>",
1136
+ "<extra_id_107>",
1137
+ "<extra_id_108>",
1138
+ "<extra_id_109>",
1139
+ "<extra_id_110>",
1140
+ "<extra_id_111>",
1141
+ "<extra_id_112>",
1142
+ "<extra_id_113>",
1143
+ "<extra_id_114>",
1144
+ "<extra_id_115>",
1145
+ "<extra_id_116>",
1146
+ "<extra_id_117>",
1147
+ "<extra_id_118>",
1148
+ "<extra_id_119>",
1149
+ "<extra_id_120>",
1150
+ "<extra_id_121>",
1151
+ "<extra_id_122>",
1152
+ "<extra_id_123>",
1153
+ "<extra_id_124>"
1154
+ ],
1155
+ "clean_up_tokenization_spaces": false,
1156
+ "eos_token": "</s>",
1157
+ "extra_ids": 0,
1158
+ "extra_special_tokens": {},
1159
+ "model_max_length": 1000000000000000019884624838656,
1160
+ "pad_token": "<pad>",
1161
+ "tokenizer_class": "ByT5Tokenizer",
1162
+ "unk_token": "<unk>"
1163
+ }
checkpoints/flash/flash_dpo/args.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"cmd": "train", "lm": "/home/dev/flash/runs/flash_lm/best.pt", "out": "/home/dev/flash/runs/flash_dpo", "shard": "0/1", "k": 4, "lr": 1e-06, "beta": 0.1, "sft": 0.1, "bs": 8, "epochs": 2}
checkpoints/flash/flash_dpo/best.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f59a6fc0dacc331844cf8b513ec99e4d5c75f2583115037c8d38ba9ff7e3a307
3
+ size 2047760470
checkpoints/flash/flash_dpo/config.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "engine": "cosyvoice3",
3
+ "base": "FunAudioLLM/Fun-CosyVoice3-0.5B-2512",
4
+ "rate": "normal",
5
+ "sampler": "silexcap",
6
+ "one_pass": true,
7
+ "sha256_best_pt": "f59a6fc0dacc331844cf8b513ec99e4d5c75f2583115037c8d38ba9ff7e3a307",
8
+ "training": "Flash SFT (CV3 LM, Phase 3 licensed manifest re-weighted + IgboSynCorp pseudo-transcripts) then DPO: 1,206 preference pairs, 286 steps, beta 0.1, lr 1e-6, val pref-acc 0.65"
9
+ }
checkpoints/flash/flash_dpo/log.jsonl ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {"step": 0, "val": {"acc": 0.0, "margin": 0.0}}
2
+ {"step": 10, "of": 286, "loss": 0.9068, "acc": 0.625, "margin": 0.046, "gn": 57.21, "min": 0.1}
3
+ {"step": 20, "of": 286, "loss": 0.9388, "acc": 0.5, "margin": -0.002, "gn": 66.55, "min": 0.1}
4
+ {"step": 30, "of": 286, "loss": 0.9699, "acc": 0.375, "margin": -0.035, "gn": 61.96, "min": 0.2}
5
+ {"step": 40, "of": 286, "loss": 0.938, "acc": 0.375, "margin": 0.011, "gn": 63.67, "min": 0.3}
6
+ {"step": 50, "of": 286, "loss": 0.8633, "acc": 0.875, "margin": 0.15, "gn": 52.25, "min": 0.3}
7
+ {"step": 60, "of": 286, "loss": 0.939, "acc": 0.625, "margin": 0.002, "gn": 63.83, "min": 0.4}
8
+ {"step": 70, "of": 286, "loss": 0.8483, "acc": 0.75, "margin": 0.189, "gn": 69.48, "min": 0.4}
9
+ {"step": 80, "of": 286, "loss": 1.0202, "acc": 0.375, "margin": -0.139, "gn": 80.15, "min": 0.5}
10
+ {"step": 90, "of": 286, "loss": 0.8866, "acc": 0.625, "margin": 0.142, "gn": 59.47, "min": 0.5}
11
+ {"step": 100, "of": 286, "loss": 0.8947, "acc": 0.625, "margin": 0.134, "gn": 60.78, "min": 0.6}
12
+ {"step": 110, "of": 286, "loss": 0.9459, "acc": 0.5, "margin": 0.027, "gn": 66.75, "min": 0.6}
13
+ {"step": 120, "of": 286, "loss": 1.0125, "acc": 0.375, "margin": -0.11, "gn": 65.48, "min": 0.7}
14
+ {"step": 130, "of": 286, "loss": 0.8009, "acc": 0.75, "margin": 0.365, "gn": 50.92, "min": 0.8}
15
+ {"step": 140, "of": 286, "loss": 0.8328, "acc": 0.75, "margin": 0.277, "gn": 60.21, "min": 0.8}
16
+ {"epoch": 1, "step": 143, "val": {"acc": 0.6, "margin": 0.15548671048794252}}
17
+ {"step": 150, "of": 286, "loss": 0.5242, "acc": 1.0, "margin": 1.345, "gn": 31.71, "min": 0.9}
18
+ {"step": 160, "of": 286, "loss": 0.476, "acc": 1.0, "margin": 1.509, "gn": 28.95, "min": 0.9}
19
+ {"step": 170, "of": 286, "loss": 0.5501, "acc": 1.0, "margin": 1.171, "gn": 29.98, "min": 1.0}
20
+ {"step": 180, "of": 286, "loss": 0.474, "acc": 1.0, "margin": 1.492, "gn": 26.95, "min": 1.0}
21
+ {"step": 190, "of": 286, "loss": 0.5711, "acc": 1.0, "margin": 1.032, "gn": 34.13, "min": 1.1}
22
+ {"step": 200, "of": 286, "loss": 0.5283, "acc": 1.0, "margin": 1.181, "gn": 32.39, "min": 1.1}
23
+ {"step": 210, "of": 286, "loss": 0.6212, "acc": 0.875, "margin": 0.86, "gn": 36.1, "min": 1.2}
24
+ {"step": 220, "of": 286, "loss": 0.5229, "acc": 1.0, "margin": 1.227, "gn": 32.65, "min": 1.2}
25
+ {"step": 230, "of": 286, "loss": 0.5555, "acc": 1.0, "margin": 1.367, "gn": 35.05, "min": 1.3}
26
+ {"step": 240, "of": 286, "loss": 0.5312, "acc": 1.0, "margin": 1.314, "gn": 36.6, "min": 1.3}
27
+ {"step": 250, "of": 286, "loss": 0.493, "acc": 1.0, "margin": 1.513, "gn": 28.32, "min": 1.3}
28
+ {"step": 260, "of": 286, "loss": 0.5076, "acc": 1.0, "margin": 1.31, "gn": 25.17, "min": 1.4}
29
+ {"step": 270, "of": 286, "loss": 0.434, "acc": 1.0, "margin": 1.739, "gn": 23.64, "min": 1.4}
30
+ {"step": 280, "of": 286, "loss": 0.4263, "acc": 1.0, "margin": 1.907, "gn": 24.99, "min": 1.5}
31
+ {"epoch": 2, "step": 286, "val": {"acc": 0.65, "margin": 0.17853533582141}}
checkpoints/flash/flash_dpo/spk2id.json ADDED
The diff for this file is too large to render. See raw diff
 
flash_lm.py ADDED
@@ -0,0 +1,532 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Dynamic Accent Flash: CosyVoice3 (Fun-CosyVoice3-0.5B-2512, Apache-2.0) LM fine-tune + synthesis.
2
+
3
+ Same additions as Phase 2/3 (per-token language embedding, learned speaker token after the text, rate embedding added to
4
+ the speaker vector, GRL speaker classifier on text positions, in-context same-speaker prompt p=0.5), ported to the CV3 LM:
5
+ seq = [sos, "You are a helpful assistant.<|endofprompt|>", (prompt text) text, spk(+rate), task, (prompt speech) speech, eos]
6
+ sos/task live in speech_embedding (CV3), eos = speech_token_size+1. Targets are CV3 speech tokens (s3v3.*).
7
+ Flow (DiT) + HiFT = CV3 base, zero-shot prompted with the voice clip (no flow fine-tune).
8
+
9
+ python flash_lm.py lm --out ~/flash/runs/flash_lm --max_steps 12000 --minutes 75
10
+ python flash_lm.py gen --lm CKPT --out DIR [--voice anchor|waxal_ibo_5|waxal_yor_5] [--rate normal] [--rows ROWS]
11
+
12
+ Release note: in this repo use `infer.py --version flash` (it wraps Synth below). Training needs the Phase 3 data
13
+ manifest, which is not shipped; the training code is included for reference. Paths come from COSYVOICE_REPO (CosyVoice
14
+ clone with CosyVoice3 support) and CV3_DIR (Fun-CosyVoice3-0.5B-2512 snapshot).
15
+ """
16
+ import argparse, json, math, os, random, sys, time, inspect
17
+ import numpy as np, torch, torch.nn as nn, torch.nn.functional as F, soundfile as sf, librosa
18
+
19
+ HOME = os.path.expanduser("~")
20
+ _HERE = os.path.dirname(os.path.abspath(__file__))
21
+ _CV = os.environ.get("COSYVOICE_REPO") or (f"{_HERE}/CosyVoice" if os.path.isdir(f"{_HERE}/CosyVoice") else f"{HOME}/CosyVoice")
22
+ sys.path[:0] = [_CV, f"{_CV}/third_party/Matcha-TTS", f"{_HERE}/code", f"{HOME}/da"]
23
+ from da.text import segments # noqa: E402
24
+ from da.model import grad_reverse # noqa: E402
25
+
26
+ CV3 = os.environ.get("CV3_DIR") or (f"{_HERE}/pretrained/Fun-CosyVoice3-0.5B-2512" if os.path.isdir(f"{_HERE}/pretrained/Fun-CosyVoice3-0.5B-2512") else f"{HOME}/models/Fun-CosyVoice3-0.5B-2512")
27
+ LANGS = ["eng", "ibo", "hau", "yor", "pcm"]
28
+ L2I = {l: i for i, l in enumerate(LANGS)}
29
+ RATES = [None, "very slow", "slow", "normal", "fast", "very fast"]
30
+ R2I = {r: i for i, r in enumerate(RATES)}
31
+ TAGS = ("[pause]", "[breath]", "[quick_breath]", "[laughter]")
32
+ SIL = {1, 2, 28, 29, 55, 248, 494, 2241, 2242, 2322, 2323} # CV3 FSQ silent/breath tokens (cosyvoice/cli/model.py)
33
+ ANCHOR = "waxal_yor_4"
34
+ NEW = ["waxal", "igboapi", "open_bible_igbo", "naijas2st_igbo", "igbosyncorp"]
35
+ MAN = f"{HOME}/p3t/manifest_p3.jsonl"
36
+ PDIR = os.environ.get("DA_PROMPTS") or (f"{_HERE}/prompts" if os.path.isdir(f"{_HERE}/prompts") else f"{HOME}/hf_test/prompts")
37
+ VOICES = {
38
+ "anchor": dict(wav=f"{PDIR}/clip0_waxal_yor_yor_1258.wav" if os.path.exists(f"{PDIR}/clip0_waxal_yor_yor_1258.wav")
39
+ else f"{PDIR}/waxal_yor_yor_1258.wav", spk=ANCHOR),
40
+ "waxal_ibo_5": dict(wav=f"{PDIR}/voice_waxal_ibo_5.wav", spk="waxal_ibo_5"),
41
+ "waxal_yor_5": dict(wav=f"{PDIR}/voice_waxal_yor_5.wav", spk="waxal_yor_5"),
42
+ }
43
+ # Flash source re-weighting on top of the Phase 3 manifest weight: less Bible narration and slow phone/dictionary reads
44
+ FLASH_W = {"open_bible_igbo": 0.4, "bible_hau": 0.5, "bible_yor": 0.5, "igboapi": 0.6, "naijas2st_igbo": 0.6,
45
+ "igbosyn_pseudo": 1.0}
46
+
47
+
48
+ def strip_tags(t):
49
+ for g in TAGS:
50
+ t = t.replace(g, " ")
51
+ return " ".join(t.split()).replace(" ,", ",").replace(" .", ".")
52
+
53
+
54
+ def _sf_load_wav(wav, target_sr, min_sr=16000):
55
+ y, sr = sf.read(wav, dtype="float32", always_2d=True)
56
+ y = y.mean(1)
57
+ if sr != target_sr:
58
+ y = librosa.resample(y, orig_sr=sr, target_sr=target_sr)
59
+ return torch.from_numpy(y)[None]
60
+
61
+
62
+ def load_cv3():
63
+ import cosyvoice.utils.file_utils as fu
64
+ fu.load_wav = _sf_load_wav
65
+ import cosyvoice.cli.frontend as cf
66
+ cf.load_wav = _sf_load_wav
67
+ from cosyvoice.cli.cosyvoice import CosyVoice3
68
+ kw = dict(load_jit=False, load_trt=False, fp16=False)
69
+ sig = inspect.signature(CosyVoice3.__init__).parameters
70
+ return CosyVoice3(CV3, **{k: v for k, v in kw.items() if k in sig})
71
+
72
+
73
+ class Tok:
74
+ def __init__(self, cosy):
75
+ self.t = cosy.frontend.tokenizer
76
+ self.prefix = self.t.encode("You are a helpful assistant.") + [151646]
77
+ self.cache = {}
78
+
79
+ def __call__(self, text, default="eng"):
80
+ if text in self.cache:
81
+ return self.cache[text]
82
+ ids, langs = [], []
83
+ for l, s in segments(text, default):
84
+ t = self.t.encode(s)
85
+ ids += t; langs += [L2I[l]] * len(t)
86
+ self.cache[text] = (ids, langs)
87
+ return ids, langs
88
+
89
+
90
+ class FLM(nn.Module):
91
+ def __init__(self, q, n_spk, prefix, d=896):
92
+ super().__init__()
93
+ self.q = q
94
+ self.lang_emb = nn.Embedding(len(LANGS), d); nn.init.zeros_(self.lang_emb.weight)
95
+ self.spk_emb = nn.Embedding(n_spk, d); nn.init.normal_(self.spk_emb.weight, std=0.02)
96
+ self.rate_emb = nn.Embedding(len(RATES), d, padding_idx=0); nn.init.zeros_(self.rate_emb.weight)
97
+ self.adv = nn.Sequential(nn.Linear(d, 256), nn.ReLU(), nn.Linear(256, n_spk))
98
+ self.V = q.speech_token_size
99
+ self.EOS = q.eos_token
100
+ self.register_buffer("prefix", torch.tensor(prefix, dtype=torch.long), persistent=False)
101
+
102
+ def embed_text(self, ids, langs):
103
+ return self.q.llm.model.model.embed_tokens(ids) + self.lang_emb(langs)
104
+
105
+ def spkvec(self, spk):
106
+ return self.spk_emb(spk[0]) + self.rate_emb(spk[1])
107
+
108
+ def build(self, ids, langs, spk, sp=None, pids=None, plangs=None, psp=None):
109
+ E = self.q.speech_embedding.weight
110
+ if pids is not None:
111
+ ids = torch.cat([pids, ids]); langs = torch.cat([plangs, langs])
112
+ pre = self.q.llm.model.model.embed_tokens(self.prefix.to(ids.device))
113
+ parts = [E[self.q.sos][None], pre, self.embed_text(ids, langs), self.spkvec(spk)[None], E[self.q.task_id][None]]
114
+ npsp = 0
115
+ if psp is not None:
116
+ parts.append(self.q.speech_embedding(psp)); npsp = len(psp)
117
+ if sp is not None:
118
+ parts.append(self.q.speech_embedding(sp))
119
+ return torch.cat(parts, 0), len(pre), len(ids), npsp
120
+
121
+ def hidden(self, x, mask):
122
+ return self.q.llm.model.model(inputs_embeds=x, attention_mask=mask, return_dict=True).last_hidden_state
123
+
124
+ def pack(self, batch):
125
+ seqs, tgts, tpos, spks = [], [], [], []
126
+ for b in batch:
127
+ ids, langs, spk, sp = b[:4]
128
+ pr = b[4] if len(b) > 4 else None
129
+ x, P, nt, npsp = self.build(ids, langs, spk, sp, *(pr if pr else (None, None, None)))
130
+ tgt = torch.full((x.shape[0] + 1,), -100, device=x.device, dtype=torch.long)
131
+ start = P + nt + 2 + npsp
132
+ tgt[start: start + len(sp)] = sp
133
+ tgt[start + len(sp)] = self.EOS
134
+ seqs.append(x); tgts.append(tgt[:x.shape[0]]); tpos.append((1 + P, 1 + P + nt)); spks.append(int(spk[0]))
135
+ Tm = max(s.shape[0] for s in seqs); D = seqs[0].shape[1]
136
+ X = seqs[0].new_zeros(len(seqs), Tm, D)
137
+ Y = torch.full((len(seqs), Tm), -100, device=X.device, dtype=torch.long)
138
+ M = torch.zeros(len(seqs), Tm, device=X.device, dtype=torch.long)
139
+ for i, (s, t) in enumerate(zip(seqs, tgts)):
140
+ X[i, :len(s)] = s; Y[i, :len(t)] = t; M[i, :len(s)] = 1
141
+ return X, Y, M, tpos, spks
142
+
143
+ def forward2(self, batch, adv_lambda=1.0):
144
+ X, Y, M, tpos, spks = self.pack(batch)
145
+ H = self.hidden(X, M)
146
+ logits = self.q.llm_decoder(H)
147
+ ce = F.cross_entropy(logits.float().reshape(-1, logits.shape[-1]), Y.reshape(-1), ignore_index=-100)
148
+ valid = Y != -100
149
+ acc = (logits.argmax(-1)[valid] == Y[valid]).float().mean()
150
+ hs = torch.cat([H[i, a:b] for i, (a, b) in enumerate(tpos)])
151
+ ys = torch.cat([torch.full((b - a,), spks[i], device=X.device) for i, (a, b) in enumerate(tpos)])
152
+ al = self.adv(grad_reverse(hs.float(), adv_lambda))
153
+ return ce, acc, F.cross_entropy(al, ys), (al.argmax(-1) == ys).float().mean()
154
+
155
+ def seq_logp(self, batch):
156
+ """sum log p(speech tokens + eos) per item (for DPO)."""
157
+ X, Y, M, _, _ = self.pack(batch)
158
+ logits = self.q.llm_decoder(self.hidden(X, M)).float()
159
+ lp = logits.log_softmax(-1)
160
+ valid = Y != -100
161
+ g = lp.gather(-1, Y.clamp(min=0)[..., None])[..., 0] * valid
162
+ return g.sum(-1), valid.sum(-1)
163
+
164
+ @torch.no_grad()
165
+ def generate(self, ids, langs, spk, prompt=None, B=1, top_p=0.8, top_k=25, win=10, tau_r=0.1, seed=0,
166
+ min_ratio=2.0, max_ratio=30.0, sampler="silexcap", max_tok=900):
167
+ g = torch.Generator(device=ids.device).manual_seed(seed)
168
+ x, _, _, _ = self.build(ids, langs, spk, None, *(prompt if prompt else (None, None, None)))
169
+ inp = x[None].repeat(B, 1, 1)
170
+ n = len(ids)
171
+ min_len, max_len = int(n * min_ratio), min(int(n * max_ratio), max_tok)
172
+ past, outs, done = None, [[] for _ in range(B)], [False] * B
173
+ E = self.q.speech_embedding.weight
174
+ for i in range(max_len):
175
+ o = self.q.llm.model.model(inputs_embeds=inp, past_key_values=past, use_cache=True, return_dict=True)
176
+ past = o.past_key_values
177
+ logp = self.q.llm_decoder(o.last_hidden_state[:, -1]).float().log_softmax(-1)
178
+ eos_lp = logp[:, self.EOS].clone()
179
+ logp[:, self.V:] = -float("inf")
180
+ if i >= min_len:
181
+ logp[:, self.EOS] = eos_lp
182
+ P = logp.exp()
183
+ nxt = []
184
+ for b in range(B):
185
+ if done[b]:
186
+ nxt.append(0); continue
187
+ p = P[b]; out = outs[b]
188
+ sp_, si = p.sort(descending=True)
189
+ keep = (sp_.cumsum(0) - sp_) < top_p
190
+ keep[top_k:] = False
191
+ cp, ci = sp_[keep], si[keep]
192
+ t = int(ci[torch.multinomial(cp / cp.sum(), 1, generator=g)])
193
+ exempt = False
194
+ if sampler == "silexcap" and t in SIL:
195
+ run = 0
196
+ for u in reversed(out):
197
+ if u not in SIL:
198
+ break
199
+ run += 1
200
+ exempt = run < 25
201
+ if not exempt and out and t != self.EOS and sum(1 for u in out[-win:] if u == t) >= win * tau_r:
202
+ t = int(torch.multinomial(p, 1, generator=g))
203
+ if t >= self.V:
204
+ done[b] = True; nxt.append(0); continue
205
+ out.append(t); nxt.append(t)
206
+ if all(done):
207
+ break
208
+ inp = E[torch.tensor(nxt, device=ids.device)][:, None]
209
+ return outs
210
+
211
+
212
+ def load_model(cosy, ckpt, tok):
213
+ ck = torch.load(ckpt, map_location="cuda", weights_only=False)
214
+ m = FLM(cosy.model.llm, ck["n_spk"], tok.prefix).cuda().eval()
215
+ print("load", ckpt, m.load_state_dict(ck["model"], strict=False), flush=True)
216
+ return m, json.load(open(os.path.join(os.path.dirname(ckpt), "spk2id.json")))
217
+
218
+
219
+ # ------------------------------------------------------------------ data
220
+ def load_data(splits, max_tok=750, extra_man=None, qual=None, qual_drop=0.1):
221
+ rows = [json.loads(l) for l in open(MAN, encoding="utf-8")]
222
+ if extra_man and os.path.exists(extra_man):
223
+ rows += [json.loads(l) for l in open(extra_man, encoding="utf-8")]
224
+ want = {r["id"] for r in rows if r["split"] in splits}
225
+ drop = set()
226
+ if qual and os.path.exists(qual) and "train" in splits: # drop the bottom qual_drop of UTMOS per source
227
+ Q = [json.loads(l) for l in open(qual)]
228
+ by = {}
229
+ for q in Q:
230
+ by.setdefault(q["src"], []).append(q)
231
+ for src, L in by.items():
232
+ if src in ("cs_pairs",):
233
+ continue
234
+ thr = float(np.quantile([q["u22"] for q in L], qual_drop))
235
+ drop |= {q["id"] for q in L if q["u22"] < thr}
236
+ print("quality filter drops", len(drop), flush=True)
237
+ src_idx, mm = {}, {}
238
+ for src in sorted({r["src"] for r in rows}):
239
+ if src in NEW:
240
+ d = f"{HOME}/p3t/{src}"
241
+ elif src == "igbosyn_pseudo":
242
+ d = f"{HOME}/flash/data/igbosyn_pseudo"
243
+ else:
244
+ d = f"{HOME}/data3/{src}"
245
+ if not os.path.exists(f"{d}/s3v3.done"):
246
+ print("missing v3 tokens", d, flush=True); continue
247
+ meta = [json.loads(l) for l in open(f"{d}/meta.jsonl", encoding="utf-8")]
248
+ tk = [json.loads(l) for l in open(f"{d}/s3v3.jsonl")]
249
+ ix = {}
250
+ for k, (m, t) in enumerate(zip(meta, tk)):
251
+ assert m["id"] == t["id"]
252
+ if m["id"] in want:
253
+ ix[m["id"]] = (t["toff"], t["ntok"])
254
+ src_idx[src] = ix
255
+ mm[src] = np.memmap(f"{d}/s3v3.i32", dtype=np.int32, mode="r")
256
+ items = []
257
+ for r in rows:
258
+ if r["split"] not in splits or r["src"] not in src_idx or r["id"] in drop and r["speaker"] != ANCHOR:
259
+ continue
260
+ ix = src_idx[r["src"]]
261
+ if r["id"] not in ix:
262
+ continue
263
+ toff, ntok = ix[r["id"]]
264
+ if not (5 < ntok <= max_tok):
265
+ continue
266
+ w = float(r.get("weight", 1.0)) * FLASH_W.get(r["src"], 1.0)
267
+ items.append(dict(id=r["id"], ds=r["src"], src=r["src"], lang=r["lang"], spk=r["speaker"], split=r["split"],
268
+ dur=r["dur"], text=r["text_plain"], tags=r.get("text_tags"), rate=R2I.get(r.get("rate_bin"), 0),
269
+ w=w, toff=toff, ntok=ntok))
270
+ return items, mm
271
+
272
+
273
+ def tagged(text, lang):
274
+ return text if lang == "cs" else f"<{lang}>{text}</{lang}>"
275
+
276
+
277
+ def batches(items, budget, rng, wfn, cost):
278
+ idx = []
279
+ for i, m in enumerate(items):
280
+ w = wfn(m)
281
+ idx += [i] * int(w) + ([i] if rng.random() < w - int(w) else [])
282
+ idx.sort(key=lambda i: cost(items[i]) + rng.random() * 10)
283
+ out, cur, mx = [], [], 0
284
+ for i in idx:
285
+ L = cost(items[i])
286
+ if cur and max(mx, L) * (len(cur) + 1) > budget:
287
+ out.append(cur); cur, mx = [], 0
288
+ cur.append(i); mx = max(mx, L)
289
+ if cur:
290
+ out.append(cur)
291
+ rng.shuffle(out)
292
+ return out
293
+
294
+
295
+ VAL_SPLITS = {"val", "validation", "dev"}
296
+
297
+
298
+ def train_lm(a):
299
+ cosy = load_cv3(); tok = Tok(cosy)
300
+ items, mm = load_data({"train"}, a.max_tok, a.extra_man, a.qual, a.qual_drop)
301
+ val_all, mmv = load_data(VAL_SPLITS, a.max_tok)
302
+ mm.update(mmv)
303
+ spk2id = {}
304
+ for s in sorted({m["spk"] for m in items}):
305
+ spk2id[s] = len(spk2id)
306
+ n_spk = len(spk2id)
307
+ rng = random.Random(1); rng.shuffle(val_all)
308
+ val, per_l, per_s = [], {}, {}
309
+ for m in val_all:
310
+ if m["spk"] not in spk2id or m["lang"] == "cs":
311
+ continue
312
+ L = "anchor" if m["spk"] == ANCHOR else m["lang"]
313
+ if per_l.get(L, 0) >= 60 or per_s.get(m["spk"], 0) >= 15:
314
+ continue
315
+ per_l[L] = per_l.get(L, 0) + 1; per_s[m["spk"]] = per_s.get(m["spk"], 0) + 1; val.append(m)
316
+ by_spk = {}
317
+ for m in items:
318
+ if m["ntok"] <= 200 and m["lang"] != "cs":
319
+ by_spk.setdefault(m["spk"], []).append(m)
320
+ os.makedirs(a.out, exist_ok=True)
321
+ json.dump(spk2id, open(f"{a.out}/spk2id.json", "w")); json.dump(vars(a), open(f"{a.out}/args.json", "w"))
322
+ hrs = {}
323
+ for m in items:
324
+ hrs[m["src"]] = hrs.get(m["src"], 0) + m["dur"] / 3600
325
+ print("lm items", len(items), "hours", round(sum(hrs.values()), 1), {k: round(v, 1) for k, v in hrs.items()},
326
+ "spk", n_spk, "val", len(val), per_l, flush=True)
327
+ model = FLM(cosy.model.llm, n_spk, tok.prefix).cuda().train()
328
+ for p in list(cosy.model.flow.parameters()) + list(cosy.model.hift.parameters()):
329
+ p.requires_grad = False
330
+ new = list(model.lang_emb.parameters()) + list(model.spk_emb.parameters()) + list(model.adv.parameters()) + list(model.rate_emb.parameters())
331
+ newids = {id(p) for p in new}
332
+ old = [p for p in model.parameters() if id(p) not in newids and p.requires_grad]
333
+ opt = torch.optim.AdamW([{"params": old, "lr": a.lr}, {"params": new, "lr": a.lr_new}], weight_decay=0.01)
334
+ base = [a.lr, a.lr_new]
335
+
336
+ def text_of(m, r):
337
+ if m["tags"] and r is not None and r.random() >= a.tag_drop:
338
+ return m["tags"]
339
+ return strip_tags(m["text"])
340
+
341
+ def spk_t(m, r):
342
+ rate = m["rate"] if (r is not None and r.random() >= a.rate_drop) else 0
343
+ return torch.tensor([spk2id[m["spk"]], rate]).cuda()
344
+
345
+ def enc(m, prng=None, force_prompt=None, val_mode=False):
346
+ r = None if val_mode else prng
347
+ ids, langs = tok(tagged(text_of(m, r), m["lang"]))
348
+ sp = np.asarray(mm[m["ds"]][m["toff"]: m["toff"] + m["ntok"]], dtype=np.int64)
349
+ out = [torch.tensor(ids).cuda(), torch.tensor(langs).cuda(), spk_t(m, r), torch.from_numpy(sp).cuda()]
350
+ use = force_prompt if force_prompt is not None else (prng is not None and prng.random() < a.p_prompt)
351
+ if use:
352
+ c = by_spk.get(m["spk"], [])
353
+ c = [x for x in (prng or random).sample(c, min(6, len(c))) if x["id"] != m["id"]] if c else []
354
+ if c and c[0]["ntok"] + m["ntok"] <= a.max_tok + 150:
355
+ p = c[0]
356
+ pids, plangs = tok(tagged(strip_tags(p["text"]), p["lang"]))
357
+ psp = np.asarray(mm[p["ds"]][p["toff"]: p["toff"] + p["ntok"]], dtype=np.int64)
358
+ out.append((torch.tensor(pids).cuda(), torch.tensor(plangs).cuda(), torch.from_numpy(psp).cuda()))
359
+ return out
360
+
361
+ def validate():
362
+ model.eval(); res = {}
363
+ with torch.no_grad(), torch.autocast("cuda", dtype=torch.bfloat16):
364
+ for mode in (False, True):
365
+ vr = random.Random(7); L, W = [], []
366
+ for i in range(0, len(val), 16):
367
+ bt = val[i:i + 16]
368
+ ce, _, _, _ = model.forward2([enc(m, vr, force_prompt=mode, val_mode=True) for m in bt])
369
+ L.append(float(ce) * len(bt)); W.append(len(bt))
370
+ res["p" if mode else "np"] = sum(L) / sum(W)
371
+ model.train()
372
+ res["mean"] = (res["np"] + res["p"]) / 2
373
+ return res
374
+
375
+ wfn = lambda m: m["w"] * (a.anchor_w if m["spk"] == ANCHOR else 1.0) # noqa
376
+ cost = lambda m: (lambda L: int(L * (1 + L / a.quad)))(m["ntok"] + len(m["text"]) // 2 + 20 + int(a.p_prompt * 200)) # noqa
377
+ rng = random.Random(0); prng = random.Random(3)
378
+ step, nskip, noom, t0 = 0, 0, 0, time.time()
379
+ logf = open(f"{a.out}/log.jsonl", "a")
380
+ v = validate(); best = v["mean"]; bad = 0
381
+ print(json.dumps({"step": 0, "val": v}), flush=True); logf.write(json.dumps({"step": 0, "val": v}) + "\n")
382
+ total, done = a.max_steps, False
383
+ while not done:
384
+ for bi in batches(items, a.tokens, rng, wfn, cost):
385
+ lrm = min(1.0, (step + 1) / a.warmup) * (0.1 + 0.9 * 0.5 * (1 + math.cos(math.pi * min(1.0, step / total))))
386
+ for gp, b in zip(opt.param_groups, base):
387
+ gp["lr"] = b * lrm
388
+ batch = [enc(items[i], prng) for i in bi]
389
+ opt.zero_grad(set_to_none=True)
390
+ oom = False
391
+ try:
392
+ with torch.autocast("cuda", dtype=torch.bfloat16):
393
+ ce, acc, adv, adv_acc = model.forward2(batch, adv_lambda=min(1.0, step / 1000))
394
+ loss = ce + a.adv_w * adv
395
+ if not torch.isfinite(loss):
396
+ nskip += 1; continue
397
+ loss.backward()
398
+ except torch.cuda.OutOfMemoryError:
399
+ oom = True
400
+ if oom:
401
+ batch = ce = acc = adv = adv_acc = loss = None
402
+ opt.zero_grad(set_to_none=True); torch.cuda.empty_cache(); noom += 1; continue
403
+ gn = torch.nn.utils.clip_grad_norm_([p for p in model.parameters() if p.requires_grad], 1.0)
404
+ if not torch.isfinite(gn):
405
+ nskip += 1; opt.zero_grad(set_to_none=True); continue
406
+ opt.step(); step += 1
407
+ if step % 50 == 0:
408
+ r = dict(step=step, ce=round(float(ce), 4), acc=round(float(acc), 4), adv=round(float(adv), 3),
409
+ adv_acc=round(float(adv_acc), 3), gn=round(float(gn), 2), B=len(bi), lr=round(lrm * a.lr, 9),
410
+ skips=nskip, oom=noom, min=round((time.time() - t0) / 60, 2))
411
+ print(json.dumps(r), flush=True); logf.write(json.dumps(r) + "\n"); logf.flush()
412
+ if step % a.val_every == 0:
413
+ v = validate(); imp = v["mean"] < best - 1e-4
414
+ rec = {"step": step, "val": v, "best": imp}
415
+ print(json.dumps(rec), flush=True); logf.write(json.dumps(rec) + "\n"); logf.flush()
416
+ if imp:
417
+ best, bad = v["mean"], 0
418
+ torch.save(dict(model={k: t for k, t in model.state_dict().items()}, step=step, n_spk=n_spk, val=v), f"{a.out}/best.pt")
419
+ else:
420
+ bad += 1
421
+ if bad >= a.patience:
422
+ print("EARLY_STOP", step, flush=True); done = True; break
423
+ if (time.time() - t0) / 60 > a.minutes or step >= total:
424
+ done = True; break
425
+ print("TRAIN_LM_DONE", step, "best", best, "skips", nskip, "oom", noom, flush=True)
426
+
427
+
428
+ # ------------------------------------------------------------------ synthesis
429
+ def prompt_feats(cosy, wav):
430
+ fe = cosy.frontend
431
+ tokn, _ = fe._extract_speech_token(wav)
432
+ feat, _ = fe._extract_speech_feat(wav)
433
+ n = min(feat.shape[1] // 2, tokn.shape[1])
434
+ return tokn[:, :n].cuda(), feat[:, :2 * n].cuda(), fe._extract_spk_embedding(wav).cuda()
435
+
436
+
437
+ @torch.no_grad()
438
+ def token2wav(cosy, tokens, ptok, pfeat, emb):
439
+ flow = cosy.model.flow
440
+ t = torch.tensor([tokens], dtype=torch.int32).cuda()
441
+ mel, _ = flow.inference(token=t, token_len=torch.tensor([t.shape[1]], dtype=torch.int32).cuda(),
442
+ prompt_token=ptok.int(), prompt_token_len=torch.tensor([ptok.shape[1]], dtype=torch.int32).cuda(),
443
+ prompt_feat=pfeat, prompt_feat_len=torch.tensor([pfeat.shape[1]], dtype=torch.int32).cuda(),
444
+ embedding=emb, streaming=False, finalize=True)
445
+ w, _ = cosy.model.hift.inference(speech_feat=mel, finalize=True)
446
+ return w.squeeze().float().cpu().numpy()
447
+
448
+
449
+ def item_text(r):
450
+ return r["text"] if r["lang"] in ("cs", "para") else f"<{r['lang']}>{r['text']}</{r['lang']}>"
451
+
452
+
453
+ class Synth:
454
+ def __init__(self, lm_ckpt):
455
+ self.cosy = load_cv3(); self.tok = Tok(self.cosy)
456
+ self.m, self.spk2id = load_model(self.cosy, lm_ckpt, self.tok)
457
+ self.vcache = {}
458
+ vt = f"{PDIR}/flash_voices_text.json"
459
+ self.vtext = json.load(open(vt if os.path.exists(vt) else f"{_HERE}/voices_text.json", encoding="utf-8"))
460
+
461
+ def voice(self, name):
462
+ if name not in self.vcache:
463
+ v = VOICES[name]
464
+ ptok, pfeat, emb = prompt_feats(self.cosy, v["wav"])
465
+ ids, langs = self.tok(self.vtext[name])
466
+ lmp = (torch.tensor(ids).cuda(), torch.tensor(langs).cuda(), ptok[0].long())
467
+ self.vcache[name] = (ptok, pfeat, emb, lmp, self.spk2id.get(v["spk"], self.spk2id.get(ANCHOR)))
468
+ return self.vcache[name]
469
+
470
+ def tokens(self, text, voice="anchor", rate="normal", seed=0, B=1, sampler="silexcap"):
471
+ ptok, pfeat, emb, lmp, sid = self.voice(voice)
472
+ ids, langs = self.tok(text)
473
+ spk = torch.tensor([sid, R2I.get(None if rate == "none" else rate, 0)]).cuda()
474
+ with torch.autocast("cuda", dtype=torch.bfloat16):
475
+ return self.m.generate(torch.tensor(ids).cuda(), torch.tensor(langs).cuda(), spk, prompt=lmp, B=B, seed=seed,
476
+ sampler=sampler)
477
+
478
+ def wav(self, toks, voice="anchor"):
479
+ ptok, pfeat, emb, _, _ = self.voice(voice)
480
+ if len(toks) < 3:
481
+ toks = list(toks) + [1, 1, 1]
482
+ return token2wav(self.cosy, toks, ptok, pfeat, emb)
483
+
484
+
485
+ def gen(a):
486
+ S = Synth(a.lm)
487
+ rows = [json.loads(l) for l in open(a.rows, encoding="utf-8")]
488
+ if a.shard:
489
+ i, n = map(int, a.shard.split("/")); rows = rows[i::n]
490
+ os.makedirs(a.out, exist_ok=True)
491
+ json.dump(vars(a), open(f"{a.out}/variant.json", "w"))
492
+ for r in rows:
493
+ if os.path.exists(f"{a.out}/{r['id']}.wav"):
494
+ continue
495
+ text = r.get("tts_text") or item_text(r)
496
+ toks = S.tokens(text, a.voice, a.rate, a.seed)[0]
497
+ w = S.wav(toks, a.voice)
498
+ sf.write(f"{a.out}/{r['id']}.wav", w, S.cosy.sample_rate)
499
+ json.dump(dict(tokens=[toks], ntok=[len(toks)]), open(f"{a.out}/{r['id']}.tok.json", "w"))
500
+ print("GEN_DONE", a.out, len(rows), flush=True)
501
+
502
+
503
+ if __name__ == "__main__":
504
+ ap = argparse.ArgumentParser()
505
+ ap.add_argument("cmd")
506
+ ap.add_argument("--out", required=True)
507
+ ap.add_argument("--lm")
508
+ ap.add_argument("--rows", default=f"{HOME}/flash/score/rows_flash.jsonl")
509
+ ap.add_argument("--voice", default="anchor")
510
+ ap.add_argument("--rate", default="normal")
511
+ ap.add_argument("--seed", type=int, default=0)
512
+ ap.add_argument("--shard")
513
+ ap.add_argument("--minutes", type=float, default=75)
514
+ ap.add_argument("--max_steps", type=int, default=12000)
515
+ ap.add_argument("--lr", type=float, default=2e-5)
516
+ ap.add_argument("--lr_new", type=float, default=1e-3)
517
+ ap.add_argument("--warmup", type=int, default=300)
518
+ ap.add_argument("--tokens", type=int, default=16000)
519
+ ap.add_argument("--max_tok", type=int, default=750)
520
+ ap.add_argument("--adv_w", type=float, default=0.02)
521
+ ap.add_argument("--p_prompt", type=float, default=0.5)
522
+ ap.add_argument("--tag_drop", type=float, default=0.5)
523
+ ap.add_argument("--rate_drop", type=float, default=0.5)
524
+ ap.add_argument("--anchor_w", type=float, default=1.0)
525
+ ap.add_argument("--val_every", type=int, default=1000)
526
+ ap.add_argument("--patience", type=int, default=4)
527
+ ap.add_argument("--quad", type=float, default=600.0)
528
+ ap.add_argument("--extra_man", default=f"{HOME}/flash/data/igbosyn_pseudo/manifest.jsonl")
529
+ ap.add_argument("--qual", default=f"{HOME}/flash/qual/utmos_train.jsonl")
530
+ ap.add_argument("--qual_drop", type=float, default=0.1)
531
+ a = ap.parse_args()
532
+ {"lm": train_lm, "gen": gen}[a.cmd](a)
frontend/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ """Dynamic Accent Flash text front-end (normalisation, Yoruba/Igbo diacritic restoration, experimental MT)."""
2
+ from .frontend import FrontEnd # noqa: F401
frontend/frontend.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Dynamic Accent Flash text front-end.
2
+
3
+ from frontend import FrontEnd
4
+ fe = FrontEnd(diac_model="models/diacritics", mt=True)
5
+ fe("<eng>The meeting is at 3:30 on 21/10/2026.</eng>", mode="codeswitch") # tags kept, numbers verbalised
6
+ fe("Ojo naa dara pupo", mode="native", lang="yor") # adds Yorùbá tone marks/underdots
7
+ fe("Please come home early today.", mode="translate", lang="ibo") # MADLAD-400 eng/pcm -> ibo
8
+ fe("I go call you tomorrow", mode="accent", lang="ibo") # English/Pidgin words, Igbo accent
9
+
10
+ Modes
11
+ codeswitch : input already tagged <ibo>…</ibo> <eng>…</eng> (or untagged = one language `lang`); each segment is
12
+ normalised in its own language; Yorùbá/Igbo segments that carry no marks are diacritised.
13
+ native : the whole text is one language (`lang`); normalise + diacritise.
14
+ translate : `lang` is the target (ibo|yor|hau); the input (English or Pidgin) is translated first with
15
+ MADLAD-400-3B-MT (Apache-2.0), then normalised + diacritised (MADLAD Yorùbá is often unmarked).
16
+ accent : keep the words as written but read them with the `lang` accent (whole text tagged <lang>), i.e.
17
+ "English words with a native accent". Numbers are verbalised in English.
18
+ Output: a tagged string for infer.py / flash_lm.py.
19
+ """
20
+ import re
21
+ import unicodedata
22
+
23
+ try:
24
+ from .normalize import normalize
25
+ except ImportError: # run as a script
26
+ from normalize import normalize
27
+
28
+ SEG = re.compile(r"<(\w{3})>(.*?)</\1>", re.S)
29
+
30
+
31
+ def _strip(s):
32
+ return "".join(c for c in unicodedata.normalize("NFD", s) if unicodedata.category(c) != "Mn")
33
+
34
+
35
+ def unmarked(text, thresh=0.05):
36
+ w = re.findall(r"\w+", unicodedata.normalize("NFC", text))
37
+ if not w:
38
+ return False
39
+ return sum(_strip(x) != unicodedata.normalize("NFC", x) for x in w) / len(w) < thresh
40
+
41
+
42
+ class FrontEnd:
43
+ def __init__(self, diac_model=None, mt=False, device="cuda"):
44
+ self.diac_dir, self.device = diac_model, device
45
+ self._d = self._mt = None
46
+ self.use_mt = mt
47
+
48
+ # ---- diacritics (ByT5-small fine-tune, word-guarded)
49
+ def diacritise(self, text, lang):
50
+ if lang not in ("yor", "ibo") or not self.diac_dir:
51
+ return text
52
+ import torch
53
+ from transformers import AutoTokenizer, T5ForConditionalGeneration
54
+ if self._d is None:
55
+ self._d = (AutoTokenizer.from_pretrained(self.diac_dir),
56
+ T5ForConditionalGeneration.from_pretrained(self.diac_dir).to(self.device).eval())
57
+ tok, m = self._d
58
+ src = unicodedata.normalize("NFC", _strip(text))
59
+ x = tok([f"{lang}: {src}"], return_tensors="pt").to(self.device)
60
+ with torch.no_grad():
61
+ y = m.generate(**x, max_length=int(x.input_ids.shape[1] * 1.6) + 16)
62
+ hyp = unicodedata.normalize("NFC", tok.decode(y[0], skip_special_tokens=True))
63
+ sw, hw = src.split(), hyp.split()
64
+ if len(sw) != len(hw):
65
+ return text
66
+ return " ".join(h if unicodedata.normalize("NFC", _strip(h)) == s else s for s, h in zip(sw, hw))
67
+
68
+ # ---- MT (MADLAD-400-3B-MT)
69
+ def translate(self, text, tgt):
70
+ import torch
71
+ from transformers import T5ForConditionalGeneration, T5Tokenizer
72
+ if self._mt is None:
73
+ self._mt = (T5Tokenizer.from_pretrained("google/madlad400-3b-mt"),
74
+ T5ForConditionalGeneration.from_pretrained("google/madlad400-3b-mt", torch_dtype=torch.bfloat16).to(self.device).eval())
75
+ tok, m = self._mt
76
+ code = {"ibo": "ig", "yor": "yo", "hau": "ha", "eng": "en"}[tgt]
77
+ out = []
78
+ for sent in re.split(r"(?<=[.!?])\s+", text.strip()):
79
+ if not sent:
80
+ continue
81
+ x = tok([f"<2{code}> {sent}"], return_tensors="pt").to(self.device)
82
+ with torch.no_grad():
83
+ y = m.generate(**x, max_new_tokens=256, num_beams=4)
84
+ out.append(tok.decode(y[0], skip_special_tokens=True))
85
+ return " ".join(out)
86
+
87
+ def _seg(self, lang, s):
88
+ s = normalize(s, lang)
89
+ if lang in ("yor", "ibo") and unmarked(s):
90
+ s = self.diacritise(s, lang)
91
+ return f"<{lang}>{s}</{lang}>"
92
+
93
+ def __call__(self, text, mode="codeswitch", lang="eng"):
94
+ text = unicodedata.normalize("NFC", text).strip()
95
+ if mode == "translate":
96
+ if lang not in ("ibo", "yor", "hau"):
97
+ raise ValueError("translate mode needs lang in ibo|yor|hau")
98
+ plain = re.sub(r"</?\w{3}>", "", text)
99
+ return self._seg(lang, self.translate(normalize(plain, "eng"), lang))
100
+ if mode == "accent":
101
+ plain = re.sub(r"</?\w{3}>", "", text)
102
+ return f"<{lang}>{normalize(plain, 'eng')}</{lang}>"
103
+ if mode == "native" or not SEG.search(text):
104
+ return self._seg(lang, re.sub(r"</?\w{3}>", "", text))
105
+ parts, pos = [], 0
106
+ for m in SEG.finditer(text):
107
+ if text[pos:m.start()].strip():
108
+ parts.append(self._seg(lang, text[pos:m.start()]))
109
+ parts.append(self._seg(m.group(1), m.group(2)))
110
+ pos = m.end()
111
+ if text[pos:].strip():
112
+ parts.append(self._seg(lang, text[pos:]))
113
+ return " ".join(parts)
114
+
115
+
116
+ if __name__ == "__main__":
117
+ import argparse
118
+ ap = argparse.ArgumentParser()
119
+ ap.add_argument("text"); ap.add_argument("--mode", default="codeswitch"); ap.add_argument("--lang", default="eng")
120
+ ap.add_argument("--diac_model", default=None); ap.add_argument("--cpu", action="store_true")
121
+ a = ap.parse_args()
122
+ print(FrontEnd(a.diac_model, device="cpu" if a.cpu else "cuda")(a.text, a.mode, a.lang))
frontend/normalize.py ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Dynamic Accent Flash text normaliser: numbers, ordinals-as-cardinals, years, dates, times, currency and percent
2
+ -> words, per language (eng, pcm, ibo, hau, yor). Pure Python, no dependencies.
3
+
4
+ from normalize import normalize
5
+ normalize("Ọ bụ ₦5,000 n'ụbọchị 21/10/2026", "ibo")
6
+
7
+ Coverage: integers 0 .. 999,999,999 (larger stay as digits), decimals (point-by-digit), ₦/N/NGN/naira, $/USD,
8
+ £, kobo, %, dd/mm/yyyy and "21 October 2026" dates, hh:mm times, 1st/2nd... in eng/pcm.
9
+ The Igbo and Hausa systems are decimal and regular. Yorùbá uses the traditional vigesimal/subtractive system for
10
+ 1-99 (with tone marks) and decimal composition above 100 (ọgọ́rùn-ún, ẹgbẹ̀rún, mílíọ̀nù + "ó lé"); the Yorùbá forms and
11
+ all date/currency phrasings need a native-speaker check before release (see TEXT_FRONTEND.md).
12
+ """
13
+ import re
14
+ import unicodedata
15
+
16
+ # ------------------------------------------------------------------ English / Pidgin
17
+ _E1 = "zero one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen".split()
18
+ _E10 = "_ _ twenty thirty forty fifty sixty seventy eighty ninety".split()
19
+
20
+
21
+ def eng(n):
22
+ if n < 20:
23
+ return _E1[n]
24
+ if n < 100:
25
+ return _E10[n // 10] + ("" if n % 10 == 0 else "-" + _E1[n % 10])
26
+ if n < 1000:
27
+ return _E1[n // 100] + " hundred" + ("" if n % 100 == 0 else " and " + eng(n % 100))
28
+ for v, w in ((10 ** 9, "billion"), (10 ** 6, "million"), (1000, "thousand")):
29
+ if n >= v:
30
+ r = n % v
31
+ tail = "" if r == 0 else ((" and " if r < 100 else " ") + eng(r))
32
+ return eng(n // v) + " " + w + tail
33
+ return str(n)
34
+
35
+
36
+ def eng_year(n):
37
+ if 2000 <= n <= 2009:
38
+ return eng(n)
39
+ if 1100 <= n <= 2099 and n % 100 != 0:
40
+ return eng(n // 100) + " " + (("oh " + _E1[n % 100]) if n % 100 < 10 else eng(n % 100))
41
+ return eng(n)
42
+
43
+
44
+ _ORD = {"one": "first", "two": "second", "three": "third", "five": "fifth", "eight": "eighth", "nine": "ninth", "twelve": "twelfth"}
45
+
46
+
47
+ def eng_ord(n):
48
+ w = eng(n)
49
+ head, _, last = w.rpartition(" ") if " " in w else ("", "", w)
50
+ h2, dash, l2 = last.rpartition("-")
51
+ base = l2 if dash else last
52
+ o = _ORD.get(base) or (base[:-1] + "ieth" if base.endswith("y") else base + "th")
53
+ last = (h2 + "-" + o) if dash else o
54
+ return (head + " " + last).strip()
55
+
56
+
57
+ # ------------------------------------------------------------------ Igbo (decimal)
58
+ _I1 = ["efu", "otu", "abụọ", "atọ", "anọ", "ise", "isii", "asaa", "asatọ", "itoolu"]
59
+
60
+
61
+ def ibo(n):
62
+ if n < 10:
63
+ return _I1[n]
64
+ if n < 100:
65
+ t, u = divmod(n, 10)
66
+ s = "iri" if t == 1 else "iri " + _I1[t]
67
+ return s if u == 0 else s + " na " + _I1[u]
68
+ for v, w in ((10 ** 9, "ijeri"), (10 ** 6, "nde"), (1000, "puku"), (100, "narị")):
69
+ if n >= v:
70
+ q, r = divmod(n, v)
71
+ s = w if q == 1 else w + " " + ibo(q)
72
+ return s if r == 0 else s + " na " + ibo(r)
73
+ return str(n)
74
+
75
+
76
+ # ------------------------------------------------------------------ Hausa (decimal)
77
+ _H1 = ["sifili", "ɗaya", "biyu", "uku", "huɗu", "biyar", "shida", "bakwai", "takwas", "tara"]
78
+ _H10 = [None, "goma", "ashirin", "talatin", "arba'in", "hamsin", "sittin", "saba'in", "tamanin", "casa'in"]
79
+
80
+
81
+ def hau(n):
82
+ if n < 10:
83
+ return _H1[n]
84
+ if n < 20:
85
+ return "goma" if n == 10 else "goma sha " + _H1[n - 10]
86
+ if n < 100:
87
+ t, u = divmod(n, 10)
88
+ return _H10[t] if u == 0 else _H10[t] + " da " + _H1[u]
89
+ for v, w in ((10 ** 9, "biliyan"), (10 ** 6, "miliyan"), (1000, "dubu"), (100, "ɗari")):
90
+ if n >= v:
91
+ q, r = divmod(n, v)
92
+ s = w if q == 1 else w + " " + hau(q)
93
+ return s if r == 0 else s + " da " + hau(r)
94
+ return str(n)
95
+
96
+
97
+ # ------------------------------------------------------------------ Yorùbá
98
+ # counting forms 1-10, then vigesimal/subtractive 11-99 (standard grammar tables); decimal composition >= 100
99
+ _Y1 = ["òdo", "oókan", "eéjì", "ẹẹ́ta", "ẹẹ́rin", "aárùn-ún", "ẹẹ́fà", "eéje", "ẹẹ́jọ", "ẹẹ́sàn-án", "ẹẹ́wàá"]
100
+ _Y11 = {11: "ọ̀kanlá", 12: "èjìlá", 13: "ẹ̀tàlá", 14: "ẹ̀rìnlá"}
101
+ # tens: name, locative form used after lé/dín ("added to / less than")
102
+ _YT = {20: ("ogún", "lógún"), 30: ("ọgbọ̀n", "lọ́gbọ̀n"), 40: ("ogójì", "lógójì"), 50: ("àádọ́ta", "láàádọ́ta"),
103
+ 60: ("ọgọ́ta", "lọ́gọ́ta"), 70: ("àádọ́rin", "láàádọ́rin"), 80: ("ọgọ́rin", "lọ́gọ́rin"),
104
+ 90: ("àádọ́rùn-ún", "láàádọ́rùn-ún"), 100: ("ọgọ́rùn-ún", "lọ́gọ́rùn-ún")}
105
+ _YLE = {1: "ọ̀kàn", 2: "éjì", 3: "ẹ́tà", 4: "ẹ́rìn"} # +1..+4
106
+ _YDIN = {1: "ọ̀kàn", 2: "éjì", 3: "ẹẹ́tà", 4: "ẹẹ́rìn"} # -1..-4
107
+ _YFIVE = {20: "ẹ̀ẹ́dógún", 30: "ẹ̀ẹ́dọ́gbọ̀n", 40: "ẹ̀ẹ́dógójì", 50: "ẹ̀ẹ́dáàádọ́ta", 60: "ẹ̀ẹ́dọ́gọ́ta", 70: "ẹ̀ẹ́dáàádọ́rin",
108
+ 80: "ẹ̀ẹ́dọ́gọ́rin", 90: "ẹ̀ẹ́dáàádọ́rùn-ún", 100: "ẹ̀ẹ́dọ́gọ́rùn-ún"} # (T - 5)
109
+ _YADJ = {1: "kan", 2: "méjì", 3: "mẹ́ta", 4: "mẹ́rin", 5: "márùn-ún", 6: "mẹ́fà", 7: "méje", 8: "mẹ́jọ", 9: "mẹ́sàn-án", 10: "mẹ́wàá"}
110
+ _YH = {200: "igba", 300: "ọ̀ọ́dúnrún", 400: "irinwó", 500: "ẹ̀ẹ́dẹ́gbẹ̀ta", 600: "ẹgbẹ̀ta", 700: "ẹ̀ẹ́dẹ́gbẹ̀rin",
111
+ 800: "ẹgbẹ̀rin", 900: "ẹ̀ẹ́dẹ́gbẹ̀rún"}
112
+
113
+
114
+ def _y99(n):
115
+ if n <= 10:
116
+ return _Y1[n]
117
+ if n in _Y11:
118
+ return _Y11[n]
119
+ if n in _YT:
120
+ return _YT[n][0]
121
+ T = (n // 10) * 10
122
+ u = n - T
123
+ if u <= 4: # T + u (u-lé-T)
124
+ return _YLE[u] + "lé" + _YT[T][1]
125
+ if u == 5: # (T+10) - 5
126
+ return _YFIVE[T + 10]
127
+ return _YDIN[10 - u] + "dín" + _YT[T + 10][1] # (T+10) - (10-u)
128
+
129
+
130
+ def yor(n):
131
+ if n < 100:
132
+ return _y99(n)
133
+ if n < 1000:
134
+ h, r = divmod(n, 100)
135
+ s = _YT[100][0] if h == 1 else _YH[h * 100]
136
+ return s if r == 0 else s + " ó lé " + _y99(r)
137
+ for v, w in ((10 ** 9, "bílíọ̀nù"), (10 ** 6, "mílíọ̀nù"), (1000, "ẹgbẹ̀rún")):
138
+ if n >= v:
139
+ q, r = divmod(n, v)
140
+ s = w + " " + (_YADJ[q] if q in _YADJ else yor(q))
141
+ return s if r == 0 else s + " ó lé " + yor(r)
142
+ return str(n)
143
+
144
+
145
+ NUM = {"eng": eng, "pcm": eng, "ibo": ibo, "hau": hau, "yor": yor}
146
+
147
+ MONTHS = {
148
+ "eng": "January February March April May June July August September October November December".split(),
149
+ "ibo": "Jenụwarị Febụwarị Maachị Epreel Mee Juun Julaị Ọgọọst Septemba Ọktoba Novemba Disemba".split(),
150
+ "hau": "Janairu Fabrairu Maris Afrilu Mayu Yuni Yuli Agusta Satumba Oktoba Nuwamba Disamba".split(),
151
+ "yor": "Ṣẹ́rẹ́ Èrèlé Ẹrẹ̀nà Ìgbé Ẹ̀bìbí Òkúdu Agẹmọ Ògún Owewe Ọ̀wàrà Bélú Ọ̀pẹ̀".split(),
152
+ }
153
+ MONTHS["pcm"] = MONTHS["eng"]
154
+ _MIDX = {m.lower(): i for i, m in enumerate(MONTHS["eng"])}
155
+ _MIDX.update({m[:3].lower(): i for i, m in enumerate(MONTHS["eng"])})
156
+
157
+ CUR = { # (major, minor) per language; phrase order handled in money()
158
+ "NGN": {"eng": ("naira", "kobo"), "pcm": ("naira", "kobo"), "ibo": ("naịra", "kobo"), "hau": ("naira", "kobo"), "yor": ("náírà", "kọ́bọ̀")},
159
+ "USD": {"eng": ("dollars", "cents"), "pcm": ("dollars", "cents"), "ibo": ("dọla", "senti"), "hau": ("dala", "santi"), "yor": ("dọ́là", "sẹ́ǹtì")},
160
+ "GBP": {"eng": ("pounds", "pence"), "pcm": ("pounds", "pence"), "ibo": ("paụnd", "pens"), "hau": ("fam", "fens"), "yor": ("pọ́n-ùn", "pẹ́ńsì")},
161
+ }
162
+
163
+
164
+ def num(n, lang):
165
+ return NUM[lang](n) if n < 10 ** 9 else str(n)
166
+
167
+
168
+ def money(amount, code, lang):
169
+ whole, _, frac = amount.partition(".")
170
+ whole = int(whole.replace(",", "") or 0)
171
+ major, minor = CUR[code][lang]
172
+ if lang in ("eng", "pcm"):
173
+ s = f"{num(whole, lang)} {major}"
174
+ if code == "USD" and whole == 1:
175
+ s = "one dollar"
176
+ if frac and int(frac[:2].ljust(2, "0")):
177
+ s += f" {num(int(frac[:2].ljust(2, '0')), lang)} {minor}"
178
+ return s
179
+ s = f"{major} {num(whole, lang)}" # ibo/hau/yor: currency word first ("naịra puku ise")
180
+ if lang == "yor":
181
+ s = f"{major} {yor_adj(whole)}"
182
+ if frac and int(frac[:2].ljust(2, "0")):
183
+ k = int(frac[:2].ljust(2, "0"))
184
+ s += {"ibo": " na ", "hau": " da ", "yor": " ó lé "}[lang] + f"{minor} {yor_adj(k) if lang == 'yor' else num(k, lang)}"
185
+ return s
186
+
187
+
188
+ def yor_adj(n):
189
+ """Yorùbá number after a noun (adjectival form for 1-10, counting form otherwise)."""
190
+ return _YADJ.get(n) or yor(n)
191
+
192
+
193
+ def date(d, m, y, lang):
194
+ M = MONTHS[lang][m - 1]
195
+ if lang in ("eng", "pcm"):
196
+ return f"the {eng_ord(d)} of {M}" + (f", {eng_year(y)}" if y else "")
197
+ if lang == "ibo":
198
+ return f"ụbọchị {ibo(d)} n'ọnwa {M}" + (f", afọ {ibo(y)}" if y else "")
199
+ if lang == "hau":
200
+ return f"ranar {hau(d)} ga watan {M}" + (f", shekarar {hau(y)}" if y else "")
201
+ return f"ọjọ́ {yor(d)} oṣù {M}" + (f", ọdún {yor(y)}" if y else "")
202
+
203
+
204
+ def clock(h, mi, lang):
205
+ if lang in ("eng", "pcm"):
206
+ return eng(h) + ("" if mi == 0 else (" oh " + eng(mi) if mi < 10 else " " + eng(mi))) + (" o'clock" if mi == 0 else "")
207
+ if lang == "ibo":
208
+ return f"elekere {ibo(h)}" + ("" if mi == 0 else f" na nkeji {ibo(mi)}")
209
+ if lang == "hau":
210
+ return f"ƙarfe {hau(h)}" + ("" if mi == 0 else f" da minti {hau(mi)}")
211
+ return f"agogo {yor_adj(h)}" + ("" if mi == 0 else f" ó lé ìṣẹ́jú {yor_adj(mi)}")
212
+
213
+
214
+ def percent(x, lang):
215
+ w = decimal(x, lang)
216
+ return {"eng": f"{w} percent", "pcm": f"{w} percent", "ibo": f"pasentị {w}", "hau": f"kashi {w} cikin ɗari",
217
+ "yor": f"ìdá {w} nínú ọgọ́rùn-ún"}[lang]
218
+
219
+
220
+ _POINT = {"eng": "point", "pcm": "point", "ibo": "ntụpọ", "hau": "digo", "yor": "ààmì"}
221
+
222
+
223
+ def decimal(x, lang):
224
+ a, _, b = x.replace(",", "").partition(".")
225
+ s = num(int(a or 0), lang)
226
+ if b:
227
+ s += f" {_POINT[lang]} " + " ".join(num(int(c), lang) for c in b)
228
+ return s
229
+
230
+
231
+ _AMT = r"\d{1,3}(?:,\d{3})+(?:\.\d+)?|\d+(?:\.\d+)?"
232
+ _RE_MONEY = re.compile(r"(?:(₦|NGN\s?|N(?=\d)|\$|USD\s?|£)\s?(" + _AMT + r")(\s?(?:k|m|bn)\b)?)|(?:(" + _AMT + r")\s?(naira|kobo|dollars?)\b)", re.I)
233
+ _RE_DATE = re.compile(r"\b(\d{1,2})[/.-](\d{1,2})[/.-](\d{4})\b")
234
+ _RE_DATE2 = re.compile(r"\b(\d{1,2})(?:st|nd|rd|th)?\s+(January|February|March|April|May|June|July|August|September|October|November|December|Jan|Feb|Mar|Apr|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec)\.?,?\s*(\d{4})?\b", re.I)
235
+ _RE_TIME = re.compile(r"\b(\d{1,2}):(\d{2})(?:\s?(am|pm)\b)?", re.I)
236
+ _RE_TIME2 = re.compile(r"\b(\d{1,2})\s?(am|pm)\b", re.I)
237
+ _RE_PCT = re.compile(r"(" + _AMT + r")\s?%")
238
+ _RE_ORD = re.compile(r"\b(\d+)(st|nd|rd|th)\b", re.I)
239
+ _RE_YEAR = re.compile(r"\b(1[5-9]\d\d|20\d\d)\b")
240
+ _RE_NUM = re.compile(_AMT)
241
+ _DUP = re.compile(r"\b(n'elekere|elekere|karfe|ƙarfe|ranar|agogo|ọjọ́|ụbọchị)\s+(elekere|ƙarfe|ranar|agogo|ọjọ́|ụbọchị)\b", re.I)
242
+ _SCALE = {"k": 1000, "m": 10 ** 6, "bn": 10 ** 9}
243
+
244
+
245
+ def normalize(text, lang):
246
+ """Verbalise numbers etc. for one language segment (no <lang> tags inside)."""
247
+ lang = lang if lang in NUM else "eng"
248
+ t = unicodedata.normalize("NFC", text)
249
+
250
+ def _money(m):
251
+ if m.group(1):
252
+ sym, amt, sc = m.group(1).strip().upper(), m.group(2), (m.group(3) or "").strip().lower()
253
+ code = {"₦": "NGN", "N": "NGN", "NGN": "NGN", "$": "USD", "USD": "USD", "£": "GBP"}[sym]
254
+ else:
255
+ amt, word, sc = m.group(4), m.group(5).lower(), ""
256
+ if word == "kobo":
257
+ k = int(amt.replace(",", ""))
258
+ return f"{num(k, lang)} kobo" if lang in ("eng", "pcm") else f"{CUR['NGN'][lang][1]} {yor_adj(k) if lang == 'yor' else num(k, lang)}"
259
+ code = "NGN" if word == "naira" else "USD"
260
+ if sc:
261
+ amt = str(int(round(float(amt.replace(",", "")) * _SCALE[sc])))
262
+ return money(amt, code, lang)
263
+
264
+ t = _RE_MONEY.sub(_money, t)
265
+ t = _RE_PCT.sub(lambda m: percent(m.group(1), lang), t)
266
+ t = _RE_DATE.sub(lambda m: date(int(m.group(1)), int(m.group(2)), int(m.group(3)), lang)
267
+ if 1 <= int(m.group(1)) <= 31 and 1 <= int(m.group(2)) <= 12 else m.group(0), t)
268
+ t = _RE_DATE2.sub(lambda m: date(int(m.group(1)), _MIDX[m.group(2).lower()[:3]] + 1,
269
+ int(m.group(3)) if m.group(3) else None, lang), t)
270
+
271
+ def _time(m):
272
+ h, mi, ap = int(m.group(1)), int(m.group(2)), (m.group(3) or "").lower()
273
+ s = clock(h, mi, lang)
274
+ if ap and lang in ("eng", "pcm"):
275
+ s += " " + {"am": "a m", "pm": "p m"}[ap]
276
+ return s
277
+
278
+ t = _RE_TIME.sub(_time, t)
279
+ t = _RE_TIME2.sub(lambda m: (eng(int(m.group(1))) + " " + {"am": "a m", "pm": "p m"}[m.group(2).lower()])
280
+ if lang in ("eng", "pcm") else clock(int(m.group(1)), 0, lang), t)
281
+ if lang in ("eng", "pcm"):
282
+ t = _RE_ORD.sub(lambda m: eng_ord(int(m.group(1))), t)
283
+ t = _RE_YEAR.sub(lambda m: eng_year(int(m.group(1))), t)
284
+ else:
285
+ t = _RE_ORD.sub(lambda m: num(int(m.group(1)), lang), t)
286
+ t = _RE_NUM.sub(lambda m: decimal(m.group(0), lang) if "." in m.group(0) else num(int(m.group(0).replace(",", "")), lang), t)
287
+ t = _DUP.sub(lambda m: m.group(1) if m.group(1).lower().endswith(m.group(2).lower()) or m.group(1).lower() == "karfe" else m.group(0), t)
288
+ return re.sub(r"\s+", " ", t).strip()
289
+
290
+
291
+ def normalize_tagged(text, default="eng"):
292
+ """Apply normalize() to every <lang>…</lang> segment of a tagged string (untagged text uses `default`)."""
293
+ out, pos = [], 0
294
+ for m in re.finditer(r"<(\w{3})>(.*?)</\1>", text, re.S):
295
+ if m.start() > pos:
296
+ out.append(normalize(text[pos:m.start()], default) if text[pos:m.start()].strip() else text[pos:m.start()])
297
+ out.append(f"<{m.group(1)}>{normalize(m.group(2), m.group(1))}</{m.group(1)}>")
298
+ pos = m.end()
299
+ if pos < len(text):
300
+ rest = text[pos:]
301
+ out.append(normalize(rest, default) if rest.strip() else rest)
302
+ return "".join(out)
303
+
304
+
305
+ if __name__ == "__main__":
306
+ import sys
307
+ for L in ["eng", "pcm", "ibo", "hau", "yor"]:
308
+ print(L, "|", normalize("₦5,000 on 21/10/2026 at 7:30, 45% of 1,235 people, 3.5 km", L))
infer.py CHANGED
@@ -1,7 +1,12 @@
1
  #!/usr/bin/env python
2
- """Dynamic Accent: Nigerian multilingual, code-switching TTS on CosyVoice2-0.5B, with selectable model versions.
3
 
4
  Versions (`--version`):
 
 
 
 
 
5
  phase2 LM `p2_lm_B` + anchor-SFT flow `p2_flow_sft`. Phase 2 sampler, sentence stitching, edge trim, internal
6
  silences capped at 0.6 s, 0.6 s between sentences (exactly the Phase 2 final samples).
7
  phase3 LM `p3_lm_full` (re-cut long-form data with natural pauses, ~102 h of Igbo) + the shared Phase 2 flow (see
@@ -14,7 +19,7 @@ Voices (`--voice`): `anchor_male` (default; WAXAL Yoruba speaker 4), `waxal_ibo_
14
  speaker token is used when the LM was trained on that speaker.
15
 
16
  python infer.py --text "<eng>this is dynamic accent,</eng> <pcm>e dey sound nice abi?</pcm>" --version phase3 --out out.wav
17
- python infer.py --text "..." --compare --out cmp.wav # -> cmp_phase2.wav and cmp_phase3.wav, same text, seed, voice
18
  python infer.py --list_voices
19
  python infer.py --auto_tag --tag_only --text "Omo, this traffic no be here o. Ẹ ṣé púpọ̀." # inspect / edit the tags
20
 
@@ -382,10 +387,75 @@ def version_cfg(name):
382
  cj = os.path.join(HERE, os.path.dirname(cfg["lm"]), "config.json")
383
  if os.path.exists(cj): # a checkpoint may override flow / rate / sampler
384
  cfg.update({k: v for k, v in json.load(open(cj)).items() if k in cfg})
385
- cfg["lm"] = os.path.join(HERE, cfg["lm"]); cfg["flow"] = os.path.join(HERE, cfg["flow"])
 
 
386
  return cfg
387
 
388
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  def word_chunks(text, max_words=60):
390
  """one pass per <= max_words, split only at sentence ends (a whole paragraph usually fits in one pass)."""
391
  sents = split_sentences(text)
@@ -522,6 +592,8 @@ class DynamicAccent:
522
 
523
  def build(version, a, cosy=None, tok=None, copy_llm=False):
524
  cfg = version_cfg(version)
 
 
525
  lm = a.lm or cfg["lm"]; flow = a.flow or cfg["flow"]
526
  S = DynamicAccent(a.model_dir, a.cosyvoice_dir, lm, flow, a.prompts, a.dtype, sampler=cfg["sampler"],
527
  one_pass=cfg["one_pass"] and not a.stitch, postprocess=cfg["postprocess"],
@@ -539,8 +611,7 @@ def list_voices(voices_json):
539
  for k, v in SETTINGS.get("versions", {}).items():
540
  print(f" {k:20s} {v.get('desc', '')}")
541
  if RECOMMENDED:
542
- print("\nrecommended pairing: phase3 -> " + ", ".join(RECOMMENDED.get("phase3", [])) +
543
- "; phase2 -> " + ", ".join(RECOMMENDED.get("phase2", [])))
544
  print(" " + RECOMMENDED.get("note", ""))
545
 
546
 
@@ -564,14 +635,25 @@ def main():
564
  ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
565
  ap.add_argument("--text", help="text; tagged ('<eng>Good morning,</eng> <pcm>how body?</pcm>') or plain with --auto_tag")
566
  ap.add_argument("--version", default=SETTINGS.get("default_version", "phase3"), choices=sorted(VERSIONS),
567
- help="model version (default phase3)")
568
  ap.add_argument("--compare", action="store_true",
569
- help="render the same text, seed and voice with phase2 AND phase3 -> <out>_phase2.wav, <out>_phase3.wav")
 
570
  ap.add_argument("--voice", default=SETTINGS.get("default_voice", "anchor_male"),
571
  help="anchor_male | waxal_ibo_5_female | waxal_yor_5_female (see --list_voices)")
572
  ap.add_argument("--list_voices", action="store_true", help="list voices, versions and the recommended pairing")
573
  ap.add_argument("--rate", default="default", choices=["default", "none", "very slow", "slow", "normal", "fast", "very fast"],
574
- help="phase3 speed instruction to the LM (default: the version's setting, 'normal'; 'none' = no instruction)")
 
 
 
 
 
 
 
 
 
 
575
  # code-switching control
576
  ap.add_argument("--auto_tag", action="store_true",
577
  help="tag untagged mixed text automatically (word/span language ID), print it, then synthesise")
@@ -607,10 +689,13 @@ def main():
607
  print(f"[tagged] {text}")
608
  if a.tag_only:
609
  return
610
- if not os.path.isdir(a.model_dir):
 
 
 
 
611
  from huggingface_hub import snapshot_download
612
  snapshot_download("FunAudioLLM/CosyVoice2-0.5B", local_dir=a.model_dir)
613
- versions = ["phase2", "phase3"] if a.compare else [a.version]
614
  cosy = tok = None
615
  base, ext = os.path.splitext(a.out)
616
  for v in versions:
@@ -618,13 +703,17 @@ def main():
618
  if hint:
619
  print(hint)
620
  S = build(v, a, cosy, tok)
621
- cosy, tok = S.cosy, S.tok
 
622
  w = S.say(text, seed=a.seed, prompt_k=a.prompt_clip, stitch=not a.no_stitch, pause_mode=a.pause_mode,
623
  speed=a.speed if v == "phase2" else 1.0)
624
  out = f"{base}_{v}{ext or '.wav'}" if a.compare else a.out
625
  sf.write(out, w, S.sr)
626
  print(f"[{v}] wrote {out} ({len(w) / S.sr:.2f} s, {S.sr} Hz, voice={a.voice}, device={DEV.type})")
627
- del S.m; torch.cuda.empty_cache()
 
 
 
628
 
629
 
630
  if __name__ == "__main__":
 
1
  #!/usr/bin/env python
2
+ """Dynamic Accent: Nigerian multilingual, code-switching TTS, with selectable model versions.
3
 
4
  Versions (`--version`):
5
+ flash (default) LM `checkpoints/flash/flash_dpo` on Fun-CosyVoice3-0.5B-2512 (Apache-2.0): Flash SFT + DPO preference
6
+ tuning (naturalness + CER + pace + pause reward), licensed data only. CV3 base flow/vocoder, zero-shot from the
7
+ voice clip. Text front-end: numbers verbalised, Yoruba/Igbo diacritics restored when missing (ByT5 model in
8
+ checkpoints/flash/diacritics_byt5). One pass per paragraph (<= ~60 words), model pauses, no post-processing.
9
+ Needs a CUDA GPU and a CosyVoice clone with CosyVoice3 support.
10
  phase2 LM `p2_lm_B` + anchor-SFT flow `p2_flow_sft`. Phase 2 sampler, sentence stitching, edge trim, internal
11
  silences capped at 0.6 s, 0.6 s between sentences (exactly the Phase 2 final samples).
12
  phase3 LM `p3_lm_full` (re-cut long-form data with natural pauses, ~102 h of Igbo) + the shared Phase 2 flow (see
 
19
  speaker token is used when the LM was trained on that speaker.
20
 
21
  python infer.py --text "<eng>this is dynamic accent,</eng> <pcm>e dey sound nice abi?</pcm>" --version phase3 --out out.wav
22
+ python infer.py --text "..." --compare --out cmp.wav # -> cmp_phase2/phase3/flash.wav, same text, seed, voice
23
  python infer.py --list_voices
24
  python infer.py --auto_tag --tag_only --text "Omo, this traffic no be here o. Ẹ ṣé púpọ̀." # inspect / edit the tags
25
 
 
387
  cj = os.path.join(HERE, os.path.dirname(cfg["lm"]), "config.json")
388
  if os.path.exists(cj): # a checkpoint may override flow / rate / sampler
389
  cfg.update({k: v for k, v in json.load(open(cj)).items() if k in cfg})
390
+ for k in ("lm", "flow", "diacritics"):
391
+ if cfg.get(k):
392
+ cfg[k] = os.path.join(HERE, cfg[k])
393
  return cfg
394
 
395
 
396
+ # ------------------------------------------------------------------ Flash (CosyVoice3 engine)
397
+ FLASH_VOICE = {"anchor_male": "anchor", "waxal_ibo_5_female": "waxal_ibo_5", "waxal_yor_5_female": "waxal_yor_5"}
398
+
399
+
400
+ def canonical_voice(voice, voices_json=None):
401
+ try:
402
+ al = json.load(open(voices_json, encoding="utf-8")).get("aliases", {}) if voices_json else {}
403
+ except OSError:
404
+ al = {}
405
+ return al.get(voice, voice)
406
+
407
+
408
+ class FlashTTS:
409
+ """--version flash: CosyVoice3 base + Flash-DPO LM (flash_lm.Synth) behind the same say()/sr interface."""
410
+
411
+ def __init__(self, a, cfg):
412
+ if not torch.cuda.is_available():
413
+ raise SystemExit("--version flash needs a CUDA GPU (phase2/phase3 also run on CPU)")
414
+ cv3 = getattr(a, "cv3_dir", "") or os.environ.get("CV3_DIR") or os.path.join(HERE, "pretrained", "Fun-CosyVoice3-0.5B-2512")
415
+ if not os.path.isdir(cv3):
416
+ from huggingface_hub import snapshot_download
417
+ snapshot_download("FunAudioLLM/Fun-CosyVoice3-0.5B-2512", local_dir=cv3)
418
+ os.environ["CV3_DIR"] = cv3
419
+ os.environ.setdefault("COSYVOICE_REPO", a.cosyvoice_dir)
420
+ os.environ.setdefault("DA_PROMPTS", os.path.join(HERE, "prompts"))
421
+ import flash_lm
422
+ self.S = flash_lm.Synth(a.lm or cfg["lm"])
423
+ self.cosy = self.tok = None # not shareable with the CosyVoice2 versions
424
+ self.m = self.S.m
425
+ self.sr = self.S.cosy.sample_rate
426
+ r = a.rate if a.rate != "default" else cfg.get("rate", "normal")
427
+ self.rate = RATES.index(None if r == "none" else r) if (None if r == "none" else r) in RATES else 0
428
+ self.fe_mode = getattr(a, "frontend", "codeswitch")
429
+ self.fe_lang = getattr(a, "frontend_lang", "eng")
430
+ if self.fe_mode == "translate" and not getattr(a, "experimental_translate", False):
431
+ raise SystemExit("--frontend translate is EXPERIMENTAL and off by default (MADLAD-400 loops on long inputs and is "
432
+ "weak on Pidgin). Add --experimental_translate to use it anyway.")
433
+ from frontend import FrontEnd
434
+ diac = cfg.get("diacritics") if not getattr(a, "no_diacritics", False) else None
435
+ self.fe = FrontEnd(diac if diac and os.path.isdir(diac) else None, mt=self.fe_mode == "translate")
436
+ self.set_voice(a.voice, a.voices)
437
+
438
+ def set_voice(self, voice, voices_json=None):
439
+ v = canonical_voice(voice, voices_json)
440
+ if v not in FLASH_VOICE:
441
+ raise SystemExit(f"unknown voice '{voice}' for flash; available: {', '.join(FLASH_VOICE)}")
442
+ self.voice = FLASH_VOICE[v]
443
+
444
+ def frontend(self, text):
445
+ return self.fe(text, self.fe_mode, self.fe_lang)
446
+
447
+ def say(self, tagged_text, seed=0, return_info=False, **_):
448
+ t = self.frontend(tagged_text)
449
+ print(f"[flash front-end] {t}")
450
+ rate = RATES[self.rate]
451
+ ws = []
452
+ for i, ch in enumerate(word_chunks(t)):
453
+ toks = self.S.tokens(ch, self.voice, rate or "none", seed + 1000 * i)[0]
454
+ ws.append(self.S.wav(toks, self.voice))
455
+ w = np.concatenate(ws).astype(np.float32)
456
+ return (w, None) if return_info else w
457
+
458
+
459
  def word_chunks(text, max_words=60):
460
  """one pass per <= max_words, split only at sentence ends (a whole paragraph usually fits in one pass)."""
461
  sents = split_sentences(text)
 
592
 
593
  def build(version, a, cosy=None, tok=None, copy_llm=False):
594
  cfg = version_cfg(version)
595
+ if cfg.get("engine") == "cosyvoice3":
596
+ return FlashTTS(a, cfg)
597
  lm = a.lm or cfg["lm"]; flow = a.flow or cfg["flow"]
598
  S = DynamicAccent(a.model_dir, a.cosyvoice_dir, lm, flow, a.prompts, a.dtype, sampler=cfg["sampler"],
599
  one_pass=cfg["one_pass"] and not a.stitch, postprocess=cfg["postprocess"],
 
611
  for k, v in SETTINGS.get("versions", {}).items():
612
  print(f" {k:20s} {v.get('desc', '')}")
613
  if RECOMMENDED:
614
+ print("\nrecommended pairing: " + "; ".join(f"{k} -> {', '.join(v)}" for k, v in RECOMMENDED.items() if isinstance(v, list)))
 
615
  print(" " + RECOMMENDED.get("note", ""))
616
 
617
 
 
635
  ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
636
  ap.add_argument("--text", help="text; tagged ('<eng>Good morning,</eng> <pcm>how body?</pcm>') or plain with --auto_tag")
637
  ap.add_argument("--version", default=SETTINGS.get("default_version", "phase3"), choices=sorted(VERSIONS),
638
+ help=f"model version (default {SETTINGS.get('default_version', 'phase3')})")
639
  ap.add_argument("--compare", action="store_true",
640
+ help="render the same text, seed and voice with every version in --compare_versions -> <out>_<version>.wav")
641
+ ap.add_argument("--compare_versions", default="phase2,phase3,flash", help="versions for --compare (comma-separated)")
642
  ap.add_argument("--voice", default=SETTINGS.get("default_voice", "anchor_male"),
643
  help="anchor_male | waxal_ibo_5_female | waxal_yor_5_female (see --list_voices)")
644
  ap.add_argument("--list_voices", action="store_true", help="list voices, versions and the recommended pairing")
645
  ap.add_argument("--rate", default="default", choices=["default", "none", "very slow", "slow", "normal", "fast", "very fast"],
646
+ help="phase3/flash speed instruction to the LM (default: the version's setting, 'normal'; 'none' = no instruction)")
647
+ # flash text front-end
648
+ ap.add_argument("--frontend", default="codeswitch", choices=["codeswitch", "native", "accent", "translate"],
649
+ help="flash only: codeswitch (tagged/one-language text), native (whole text in --frontend_lang), accent "
650
+ "(words as written, read with the --frontend_lang accent), translate (EXPERIMENTAL, off by default)")
651
+ ap.add_argument("--frontend_lang", default="eng", choices=LANGS, help="flash front-end language for untagged text / native / accent / translate target")
652
+ ap.add_argument("--no_diacritics", action="store_true", help="flash: do not restore missing Yoruba/Igbo diacritics")
653
+ ap.add_argument("--experimental_translate", action="store_true",
654
+ help="flash: allow --frontend translate (MADLAD-400-3B-MT, ~12 GB download; loops on long inputs, weak on Pidgin)")
655
+ ap.add_argument("--cv3_dir", default=os.environ.get("CV3_DIR", os.path.join(HERE, "pretrained", "Fun-CosyVoice3-0.5B-2512")),
656
+ help="flash: stock FunAudioLLM/Fun-CosyVoice3-0.5B-2512 snapshot (downloaded on first run)")
657
  # code-switching control
658
  ap.add_argument("--auto_tag", action="store_true",
659
  help="tag untagged mixed text automatically (word/span language ID), print it, then synthesise")
 
689
  print(f"[tagged] {text}")
690
  if a.tag_only:
691
  return
692
+ versions = [v.strip() for v in a.compare_versions.split(",") if v.strip()] if a.compare else [a.version]
693
+ bad = [v for v in versions if v not in VERSIONS]
694
+ if bad:
695
+ ap.error(f"unknown version(s) {bad}; available: {sorted(VERSIONS)}")
696
+ if any(VERSIONS[v].get("engine") != "cosyvoice3" for v in versions) and not os.path.isdir(a.model_dir):
697
  from huggingface_hub import snapshot_download
698
  snapshot_download("FunAudioLLM/CosyVoice2-0.5B", local_dir=a.model_dir)
 
699
  cosy = tok = None
700
  base, ext = os.path.splitext(a.out)
701
  for v in versions:
 
703
  if hint:
704
  print(hint)
705
  S = build(v, a, cosy, tok)
706
+ if S.cosy is not None:
707
+ cosy, tok = S.cosy, S.tok
708
  w = S.say(text, seed=a.seed, prompt_k=a.prompt_clip, stitch=not a.no_stitch, pause_mode=a.pause_mode,
709
  speed=a.speed if v == "phase2" else 1.0)
710
  out = f"{base}_{v}{ext or '.wav'}" if a.compare else a.out
711
  sf.write(out, w, S.sr)
712
  print(f"[{v}] wrote {out} ({len(w) / S.sr:.2f} s, {S.sr} Hz, voice={a.voice}, device={DEV.type})")
713
+ del S.m
714
+ if isinstance(S, FlashTTS):
715
+ del S.S
716
+ del S; torch.cuda.empty_cache()
717
 
718
 
719
  if __name__ == "__main__":
prompts/flash_voices_text.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {"anchor": "<yor>Púpọ̀ jùlọ àwọn olókìkí ní ìlú Èkó ń gbé ní erékùṣù náà.</yor>",
2
+ "waxal_ibo_5": "<ibo>I nwere ike itinye mmemme ọbụla n'izu a ma gị atụla ụjọ maka oké mmiri ozuzo.</ibo>",
3
+ "waxal_yor_5": "<yor>Nínú oṣù Ọpẹ́ ni ó ra mọ́tò tí wọ́n jí gbé.</yor>"}
samples/flash/codeswitch_dynacc.mp3 ADDED
Binary file (75.9 kB). View file
 
samples/flash/codeswitch_ibo_pcm_eng.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:26d4ac3d1fcae8484641e547e455420f54dd987b9b1184624ef40de80c3d3d8a
3
+ size 175245
samples/flash/codeswitch_yor_eng.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3c09b28d483a2f7c740d9b05438767f2f457cb1bd2bd49029d31760a8a0fc7ad
3
+ size 104205
samples/flash/diacritics_yoruba_from_plain.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9ce4b97b0225a62356b2996e9d551ff74e2a2b16ca34b2707873095772995889
3
+ size 107085
samples/flash/hausa_male.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:21354db9f258956e861b5526289f2b49dc908d2760de89913a65af8e74ac95eb
3
+ size 208845
samples/flash/igbo_para_female.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c24da6ec6eaec6b332f1a9b0dac2eb944e08a13187e9c07dd5d1b0577ef8c0f5
3
+ size 245805
samples/flash/igbo_pure_female.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:39abdd290724dcc05cd7d5f928cde2f178044115b26795bf16da48963cfe2ff9
3
+ size 164205
samples/flash/numbers_igbo.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:daa1ea59e60f9c1b3fefde255ce5c98ec16513d64fef2aebbd1f035ae0227283
3
+ size 262605
samples/flash/pidgin_female.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:255ae0e9a883f93b5bc48c2eeedac3138bd7304ee01bfd675e9c29ff99957189
3
+ size 181005
samples/flash/yoruba_female.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8c0011b2c16edbdd88e91511ce8d455e358dee285084a7bffc4f6211e6ec6888
3
+ size 162285
samples/flash/yoruba_male.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:550aa664f313b854594ebfd263b015224a1fce1a0abae2c8ce6215541121092f
3
+ size 181005
settings.json CHANGED
@@ -1,7 +1,20 @@
1
  {
2
- "default_version": "phase3",
3
  "default_voice": "anchor_male",
4
  "versions": {
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  "phase2": {
6
  "lm": "checkpoints/phase2/p2_lm_B/best.pt",
7
  "flow": "checkpoints/phase2/p2_flow_sft/best.pt",
@@ -27,6 +40,11 @@
27
  "waxal_yor_5_female"
28
  ],
29
  "recommended": {
 
 
 
 
 
30
  "phase3": [
31
  "waxal_ibo_5_female",
32
  "waxal_yor_5_female"
@@ -34,7 +52,7 @@
34
  "phase2": [
35
  "anchor_male"
36
  ],
37
- "note": "Listening verdict (model author, 2026-10-21): Phase 3 sounds best with the female voices; Phase 2 is fine for the male anchor. Use --compare to A/B before use."
38
  },
39
  "rates": [
40
  "none",
 
1
  {
2
+ "default_version": "flash",
3
  "default_voice": "anchor_male",
4
  "versions": {
5
+ "flash": {
6
+ "engine": "cosyvoice3",
7
+ "base": "FunAudioLLM/Fun-CosyVoice3-0.5B-2512",
8
+ "lm": "checkpoints/flash/flash_dpo/best.pt",
9
+ "diacritics": "checkpoints/flash/diacritics_byt5",
10
+ "sampler": "silexcap",
11
+ "one_pass": true,
12
+ "postprocess": false,
13
+ "rate": "normal",
14
+ "frontend": "codeswitch",
15
+ "translate": "experimental, off by default (--frontend translate --experimental_translate)",
16
+ "desc": "Flash (default): Flash-DPO LM on Fun-CosyVoice3-0.5B-2512 (SFT on licensed data + DPO with a naturalness/CER/pace/pause reward), CV3 flow+vocoder zero-shot from the voice clip; text front-end restores Yoruba/Igbo diacritics; one pass per paragraph, model pauses, no post-processing"
17
+ },
18
  "phase2": {
19
  "lm": "checkpoints/phase2/p2_lm_B/best.pt",
20
  "flow": "checkpoints/phase2/p2_flow_sft/best.pt",
 
40
  "waxal_yor_5_female"
41
  ],
42
  "recommended": {
43
+ "flash": [
44
+ "anchor_male",
45
+ "waxal_ibo_5_female",
46
+ "waxal_yor_5_female"
47
+ ],
48
  "phase3": [
49
  "waxal_ibo_5_female",
50
  "waxal_yor_5_female"
 
52
  "phase2": [
53
  "anchor_male"
54
  ],
55
+ "note": "Flash is the default and the best-scoring version for every voice (clipped endings anchor 9.1%->7.1%, female Igbo 22.8%->10.7%; pace ~native). Known weakness: with anchor_male, Flash under-pauses in Igbo and Yoruba. Earlier listening verdict (2026-10-21): Phase 3 with the female voices, Phase 2 with the male anchor. Use --compare to A/B before use."
56
  },
57
  "rates": [
58
  "none",