UncleanCode commited on
Commit
582aab9
·
verified ·
1 Parent(s): 143d720

Dynamic Accent Pro v1.1 (experimental prompt-controlled Nigerian TTS); Flash-DPO default, Pro via --instruct

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +16 -0
  2. LICENSE +428 -0
  3. LICENSE-CODE +202 -0
  4. README.md +247 -0
  5. assets/dynamic_accent_banner.jpg +3 -0
  6. checkpoints/flash/diacritics_byt5/added_tokens.json +127 -0
  7. checkpoints/flash/diacritics_byt5/config.json +32 -0
  8. checkpoints/flash/diacritics_byt5/generation_config.json +7 -0
  9. checkpoints/flash/diacritics_byt5/model.safetensors +3 -0
  10. checkpoints/flash/diacritics_byt5/special_tokens_map.json +150 -0
  11. checkpoints/flash/diacritics_byt5/tokenizer_config.json +1163 -0
  12. checkpoints/flash/flash_dpo/args.json +1 -0
  13. checkpoints/flash/flash_dpo/best.pt +3 -0
  14. checkpoints/flash/flash_dpo/config.json +9 -0
  15. checkpoints/flash/flash_dpo/log.jsonl +31 -0
  16. checkpoints/flash/flash_dpo/spk2id.json +0 -0
  17. checkpoints/pro/flash_instruct/args.json +1 -0
  18. checkpoints/pro/flash_instruct/best.pt +3 -0
  19. checkpoints/pro/flash_instruct/config.json +14 -0
  20. checkpoints/pro/flash_instruct/log.jsonl +16 -0
  21. checkpoints/pro/flash_instruct/spk2id.json +0 -0
  22. code/da/__init__.py +0 -0
  23. code/da/data.py +95 -0
  24. code/da/model.py +368 -0
  25. code/da/p2common.py +308 -0
  26. code/da/prep2.py +259 -0
  27. code/da/s3_2.py +130 -0
  28. code/da/text.py +60 -0
  29. code/train_b.py +353 -0
  30. code/train_b2.py +590 -0
  31. config.json +41 -0
  32. cosyvoice_requirements.txt +42 -0
  33. cs_frontend.py +187 -0
  34. flash_instruct.py +433 -0
  35. flash_lm.py +532 -0
  36. frontend/__init__.py +2 -0
  37. frontend/frontend.py +122 -0
  38. frontend/normalize.py +308 -0
  39. infer.py +801 -0
  40. instruct_text.py +130 -0
  41. pause_text.py +42 -0
  42. prompts/clip0_waxal_yor_yor_1258.wav +3 -0
  43. prompts/clip1_waxal_yor_yor_1205.wav +3 -0
  44. prompts/clip2_waxal_yor_yor_1263.wav +3 -0
  45. prompts/clip3_waxal_yor_yor_1218.wav +3 -0
  46. prompts/flash_voices_text.json +3 -0
  47. prompts/prompts.json +602 -0
  48. prompts/voice_waxal_ibo_5.wav +3 -0
  49. prompts/voice_waxal_yor_5.wav +3 -0
  50. prompts/voices.json +41 -0
.gitattributes CHANGED
@@ -33,3 +33,19 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ assets/dynamic_accent_banner.jpg filter=lfs diff=lfs merge=lfs -text
37
+ prompts/clip0_waxal_yor_yor_1258.wav filter=lfs diff=lfs merge=lfs -text
38
+ prompts/clip1_waxal_yor_yor_1205.wav filter=lfs diff=lfs merge=lfs -text
39
+ prompts/clip2_waxal_yor_yor_1263.wav filter=lfs diff=lfs merge=lfs -text
40
+ prompts/clip3_waxal_yor_yor_1218.wav filter=lfs diff=lfs merge=lfs -text
41
+ prompts/voice_waxal_ibo_5.wav filter=lfs diff=lfs merge=lfs -text
42
+ prompts/voice_waxal_yor_5.wav filter=lfs diff=lfs merge=lfs -text
43
+ samples/flash_ibo_waxal_ibo_5_default.mp3 filter=lfs diff=lfs merge=lfs -text
44
+ samples/pro_cs_anchor_default.mp3 filter=lfs diff=lfs merge=lfs -text
45
+ samples/pro_cs_anchor_many_pauses.mp3 filter=lfs diff=lfs merge=lfs -text
46
+ samples/pro_cs_anchor_slow.mp3 filter=lfs diff=lfs merge=lfs -text
47
+ samples/pro_cs_waxal_ibo_5_many_pauses.mp3 filter=lfs diff=lfs merge=lfs -text
48
+ samples/pro_ibo_anchor_many_pauses.mp3 filter=lfs diff=lfs merge=lfs -text
49
+ samples/pro_ibo_anchor_slow.mp3 filter=lfs diff=lfs merge=lfs -text
50
+ samples/pro_ibo_waxal_ibo_5_default.mp3 filter=lfs diff=lfs merge=lfs -text
51
+ samples/pro_ibo_waxal_ibo_5_many_pauses.mp3 filter=lfs diff=lfs merge=lfs -text
LICENSE ADDED
@@ -0,0 +1,428 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Attribution-ShareAlike 4.0 International
2
+
3
+ =======================================================================
4
+
5
+ Creative Commons Corporation ("Creative Commons") is not a law firm and
6
+ does not provide legal services or legal advice. Distribution of
7
+ Creative Commons public licenses does not create a lawyer-client or
8
+ other relationship. Creative Commons makes its licenses and related
9
+ information available on an "as-is" basis. Creative Commons gives no
10
+ warranties regarding its licenses, any material licensed under their
11
+ terms and conditions, or any related information. Creative Commons
12
+ disclaims all liability for damages resulting from their use to the
13
+ fullest extent possible.
14
+
15
+ Using Creative Commons Public Licenses
16
+
17
+ Creative Commons public licenses provide a standard set of terms and
18
+ conditions that creators and other rights holders may use to share
19
+ original works of authorship and other material subject to copyright
20
+ and certain other rights specified in the public license below. The
21
+ following considerations are for informational purposes only, are not
22
+ exhaustive, and do not form part of our licenses.
23
+
24
+ Considerations for licensors: Our public licenses are
25
+ intended for use by those authorized to give the public
26
+ permission to use material in ways otherwise restricted by
27
+ copyright and certain other rights. Our licenses are
28
+ irrevocable. Licensors should read and understand the terms
29
+ and conditions of the license they choose before applying it.
30
+ Licensors should also secure all rights necessary before
31
+ applying our licenses so that the public can reuse the
32
+ material as expected. Licensors should clearly mark any
33
+ material not subject to the license. This includes other CC-
34
+ licensed material, or material used under an exception or
35
+ limitation to copyright. More considerations for licensors:
36
+ wiki.creativecommons.org/Considerations_for_licensors
37
+
38
+ Considerations for the public: By using one of our public
39
+ licenses, a licensor grants the public permission to use the
40
+ licensed material under specified terms and conditions. If
41
+ the licensor's permission is not necessary for any reason--for
42
+ example, because of any applicable exception or limitation to
43
+ copyright--then that use is not regulated by the license. Our
44
+ licenses grant only permissions under copyright and certain
45
+ other rights that a licensor has authority to grant. Use of
46
+ the licensed material may still be restricted for other
47
+ reasons, including because others have copyright or other
48
+ rights in the material. A licensor may make special requests,
49
+ such as asking that all changes be marked or described.
50
+ Although not required by our licenses, you are encouraged to
51
+ respect those requests where reasonable. More considerations
52
+ for the public:
53
+ wiki.creativecommons.org/Considerations_for_licensees
54
+
55
+ =======================================================================
56
+
57
+ Creative Commons Attribution-ShareAlike 4.0 International Public
58
+ License
59
+
60
+ By exercising the Licensed Rights (defined below), You accept and agree
61
+ to be bound by the terms and conditions of this Creative Commons
62
+ Attribution-ShareAlike 4.0 International Public License ("Public
63
+ License"). To the extent this Public License may be interpreted as a
64
+ contract, You are granted the Licensed Rights in consideration of Your
65
+ acceptance of these terms and conditions, and the Licensor grants You
66
+ such rights in consideration of benefits the Licensor receives from
67
+ making the Licensed Material available under these terms and
68
+ conditions.
69
+
70
+
71
+ Section 1 -- Definitions.
72
+
73
+ a. Adapted Material means material subject to Copyright and Similar
74
+ Rights that is derived from or based upon the Licensed Material
75
+ and in which the Licensed Material is translated, altered,
76
+ arranged, transformed, or otherwise modified in a manner requiring
77
+ permission under the Copyright and Similar Rights held by the
78
+ Licensor. For purposes of this Public License, where the Licensed
79
+ Material is a musical work, performance, or sound recording,
80
+ Adapted Material is always produced where the Licensed Material is
81
+ synched in timed relation with a moving image.
82
+
83
+ b. Adapter's License means the license You apply to Your Copyright
84
+ and Similar Rights in Your contributions to Adapted Material in
85
+ accordance with the terms and conditions of this Public License.
86
+
87
+ c. BY-SA Compatible License means a license listed at
88
+ creativecommons.org/compatiblelicenses, approved by Creative
89
+ Commons as essentially the equivalent of this Public License.
90
+
91
+ d. Copyright and Similar Rights means copyright and/or similar rights
92
+ closely related to copyright including, without limitation,
93
+ performance, broadcast, sound recording, and Sui Generis Database
94
+ Rights, without regard to how the rights are labeled or
95
+ categorized. For purposes of this Public License, the rights
96
+ specified in Section 2(b)(1)-(2) are not Copyright and Similar
97
+ Rights.
98
+
99
+ e. Effective Technological Measures means those measures that, in the
100
+ absence of proper authority, may not be circumvented under laws
101
+ fulfilling obligations under Article 11 of the WIPO Copyright
102
+ Treaty adopted on December 20, 1996, and/or similar international
103
+ agreements.
104
+
105
+ f. Exceptions and Limitations means fair use, fair dealing, and/or
106
+ any other exception or limitation to Copyright and Similar Rights
107
+ that applies to Your use of the Licensed Material.
108
+
109
+ g. License Elements means the license attributes listed in the name
110
+ of a Creative Commons Public License. The License Elements of this
111
+ Public License are Attribution and ShareAlike.
112
+
113
+ h. Licensed Material means the artistic or literary work, database,
114
+ or other material to which the Licensor applied this Public
115
+ License.
116
+
117
+ i. Licensed Rights means the rights granted to You subject to the
118
+ terms and conditions of this Public License, which are limited to
119
+ all Copyright and Similar Rights that apply to Your use of the
120
+ Licensed Material and that the Licensor has authority to license.
121
+
122
+ j. Licensor means the individual(s) or entity(ies) granting rights
123
+ under this Public License.
124
+
125
+ k. Share means to provide material to the public by any means or
126
+ process that requires permission under the Licensed Rights, such
127
+ as reproduction, public display, public performance, distribution,
128
+ dissemination, communication, or importation, and to make material
129
+ available to the public including in ways that members of the
130
+ public may access the material from a place and at a time
131
+ individually chosen by them.
132
+
133
+ l. Sui Generis Database Rights means rights other than copyright
134
+ resulting from Directive 96/9/EC of the European Parliament and of
135
+ the Council of 11 March 1996 on the legal protection of databases,
136
+ as amended and/or succeeded, as well as other essentially
137
+ equivalent rights anywhere in the world.
138
+
139
+ m. You means the individual or entity exercising the Licensed Rights
140
+ under this Public License. Your has a corresponding meaning.
141
+
142
+
143
+ Section 2 -- Scope.
144
+
145
+ a. License grant.
146
+
147
+ 1. Subject to the terms and conditions of this Public License,
148
+ the Licensor hereby grants You a worldwide, royalty-free,
149
+ non-sublicensable, non-exclusive, irrevocable license to
150
+ exercise the Licensed Rights in the Licensed Material to:
151
+
152
+ a. reproduce and Share the Licensed Material, in whole or
153
+ in part; and
154
+
155
+ b. produce, reproduce, and Share Adapted Material.
156
+
157
+ 2. Exceptions and Limitations. For the avoidance of doubt, where
158
+ Exceptions and Limitations apply to Your use, this Public
159
+ License does not apply, and You do not need to comply with
160
+ its terms and conditions.
161
+
162
+ 3. Term. The term of this Public License is specified in Section
163
+ 6(a).
164
+
165
+ 4. Media and formats; technical modifications allowed. The
166
+ Licensor authorizes You to exercise the Licensed Rights in
167
+ all media and formats whether now known or hereafter created,
168
+ and to make technical modifications necessary to do so. The
169
+ Licensor waives and/or agrees not to assert any right or
170
+ authority to forbid You from making technical modifications
171
+ necessary to exercise the Licensed Rights, including
172
+ technical modifications necessary to circumvent Effective
173
+ Technological Measures. For purposes of this Public License,
174
+ simply making modifications authorized by this Section 2(a)
175
+ (4) never produces Adapted Material.
176
+
177
+ 5. Downstream recipients.
178
+
179
+ a. Offer from the Licensor -- Licensed Material. Every
180
+ recipient of the Licensed Material automatically
181
+ receives an offer from the Licensor to exercise the
182
+ Licensed Rights under the terms and conditions of this
183
+ Public License.
184
+
185
+ b. Additional offer from the Licensor -- Adapted Material.
186
+ Every recipient of Adapted Material from You
187
+ automatically receives an offer from the Licensor to
188
+ exercise the Licensed Rights in the Adapted Material
189
+ under the conditions of the Adapter's License You apply.
190
+
191
+ c. No downstream restrictions. You may not offer or impose
192
+ any additional or different terms or conditions on, or
193
+ apply any Effective Technological Measures to, the
194
+ Licensed Material if doing so restricts exercise of the
195
+ Licensed Rights by any recipient of the Licensed
196
+ Material.
197
+
198
+ 6. No endorsement. Nothing in this Public License constitutes or
199
+ may be construed as permission to assert or imply that You
200
+ are, or that Your use of the Licensed Material is, connected
201
+ with, or sponsored, endorsed, or granted official status by,
202
+ the Licensor or others designated to receive attribution as
203
+ provided in Section 3(a)(1)(A)(i).
204
+
205
+ b. Other rights.
206
+
207
+ 1. Moral rights, such as the right of integrity, are not
208
+ licensed under this Public License, nor are publicity,
209
+ privacy, and/or other similar personality rights; however, to
210
+ the extent possible, the Licensor waives and/or agrees not to
211
+ assert any such rights held by the Licensor to the limited
212
+ extent necessary to allow You to exercise the Licensed
213
+ Rights, but not otherwise.
214
+
215
+ 2. Patent and trademark rights are not licensed under this
216
+ Public License.
217
+
218
+ 3. To the extent possible, the Licensor waives any right to
219
+ collect royalties from You for the exercise of the Licensed
220
+ Rights, whether directly or through a collecting society
221
+ under any voluntary or waivable statutory or compulsory
222
+ licensing scheme. In all other cases the Licensor expressly
223
+ reserves any right to collect such royalties.
224
+
225
+
226
+ Section 3 -- License Conditions.
227
+
228
+ Your exercise of the Licensed Rights is expressly made subject to the
229
+ following conditions.
230
+
231
+ a. Attribution.
232
+
233
+ 1. If You Share the Licensed Material (including in modified
234
+ form), You must:
235
+
236
+ a. retain the following if it is supplied by the Licensor
237
+ with the Licensed Material:
238
+
239
+ i. identification of the creator(s) of the Licensed
240
+ Material and any others designated to receive
241
+ attribution, in any reasonable manner requested by
242
+ the Licensor (including by pseudonym if
243
+ designated);
244
+
245
+ ii. a copyright notice;
246
+
247
+ iii. a notice that refers to this Public License;
248
+
249
+ iv. a notice that refers to the disclaimer of
250
+ warranties;
251
+
252
+ v. a URI or hyperlink to the Licensed Material to the
253
+ extent reasonably practicable;
254
+
255
+ b. indicate if You modified the Licensed Material and
256
+ retain an indication of any previous modifications; and
257
+
258
+ c. indicate the Licensed Material is licensed under this
259
+ Public License, and include the text of, or the URI or
260
+ hyperlink to, this Public License.
261
+
262
+ 2. You may satisfy the conditions in Section 3(a)(1) in any
263
+ reasonable manner based on the medium, means, and context in
264
+ which You Share the Licensed Material. For example, it may be
265
+ reasonable to satisfy the conditions by providing a URI or
266
+ hyperlink to a resource that includes the required
267
+ information.
268
+
269
+ 3. If requested by the Licensor, You must remove any of the
270
+ information required by Section 3(a)(1)(A) to the extent
271
+ reasonably practicable.
272
+
273
+ b. ShareAlike.
274
+
275
+ In addition to the conditions in Section 3(a), if You Share
276
+ Adapted Material You produce, the following conditions also apply.
277
+
278
+ 1. The Adapter's License You apply must be a Creative Commons
279
+ license with the same License Elements, this version or
280
+ later, or a BY-SA Compatible License.
281
+
282
+ 2. You must include the text of, or the URI or hyperlink to, the
283
+ Adapter's License You apply. You may satisfy this condition
284
+ in any reasonable manner based on the medium, means, and
285
+ context in which You Share Adapted Material.
286
+
287
+ 3. You may not offer or impose any additional or different terms
288
+ or conditions on, or apply any Effective Technological
289
+ Measures to, Adapted Material that restrict exercise of the
290
+ rights granted under the Adapter's License You apply.
291
+
292
+
293
+ Section 4 -- Sui Generis Database Rights.
294
+
295
+ Where the Licensed Rights include Sui Generis Database Rights that
296
+ apply to Your use of the Licensed Material:
297
+
298
+ a. for the avoidance of doubt, Section 2(a)(1) grants You the right
299
+ to extract, reuse, reproduce, and Share all or a substantial
300
+ portion of the contents of the database;
301
+
302
+ b. if You include all or a substantial portion of the database
303
+ contents in a database in which You have Sui Generis Database
304
+ Rights, then the database in which You have Sui Generis Database
305
+ Rights (but not its individual contents) is Adapted Material,
306
+ including for purposes of Section 3(b); and
307
+
308
+ c. You must comply with the conditions in Section 3(a) if You Share
309
+ all or a substantial portion of the contents of the database.
310
+
311
+ For the avoidance of doubt, this Section 4 supplements and does not
312
+ replace Your obligations under this Public License where the Licensed
313
+ Rights include other Copyright and Similar Rights.
314
+
315
+
316
+ Section 5 -- Disclaimer of Warranties and Limitation of Liability.
317
+
318
+ a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
319
+ EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
320
+ AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
321
+ ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
322
+ IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
323
+ WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
324
+ PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
325
+ ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
326
+ KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
327
+ ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
328
+
329
+ b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
330
+ TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
331
+ NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
332
+ INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
333
+ COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
334
+ USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
335
+ ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
336
+ DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
337
+ IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
338
+
339
+ c. The disclaimer of warranties and limitation of liability provided
340
+ above shall be interpreted in a manner that, to the extent
341
+ possible, most closely approximates an absolute disclaimer and
342
+ waiver of all liability.
343
+
344
+
345
+ Section 6 -- Term and Termination.
346
+
347
+ a. This Public License applies for the term of the Copyright and
348
+ Similar Rights licensed here. However, if You fail to comply with
349
+ this Public License, then Your rights under this Public License
350
+ terminate automatically.
351
+
352
+ b. Where Your right to use the Licensed Material has terminated under
353
+ Section 6(a), it reinstates:
354
+
355
+ 1. automatically as of the date the violation is cured, provided
356
+ it is cured within 30 days of Your discovery of the
357
+ violation; or
358
+
359
+ 2. upon express reinstatement by the Licensor.
360
+
361
+ For the avoidance of doubt, this Section 6(b) does not affect any
362
+ right the Licensor may have to seek remedies for Your violations
363
+ of this Public License.
364
+
365
+ c. For the avoidance of doubt, the Licensor may also offer the
366
+ Licensed Material under separate terms or conditions or stop
367
+ distributing the Licensed Material at any time; however, doing so
368
+ will not terminate this Public License.
369
+
370
+ d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
371
+ License.
372
+
373
+
374
+ Section 7 -- Other Terms and Conditions.
375
+
376
+ a. The Licensor shall not be bound by any additional or different
377
+ terms or conditions communicated by You unless expressly agreed.
378
+
379
+ b. Any arrangements, understandings, or agreements regarding the
380
+ Licensed Material not stated herein are separate from and
381
+ independent of the terms and conditions of this Public License.
382
+
383
+
384
+ Section 8 -- Interpretation.
385
+
386
+ a. For the avoidance of doubt, this Public License does not, and
387
+ shall not be interpreted to, reduce, limit, restrict, or impose
388
+ conditions on any use of the Licensed Material that could lawfully
389
+ be made without permission under this Public License.
390
+
391
+ b. To the extent possible, if any provision of this Public License is
392
+ deemed unenforceable, it shall be automatically reformed to the
393
+ minimum extent necessary to make it enforceable. If the provision
394
+ cannot be reformed, it shall be severed from this Public License
395
+ without affecting the enforceability of the remaining terms and
396
+ conditions.
397
+
398
+ c. No term or condition of this Public License will be waived and no
399
+ failure to comply consented to unless expressly agreed to by the
400
+ Licensor.
401
+
402
+ d. Nothing in this Public License constitutes or may be interpreted
403
+ as a limitation upon, or waiver of, any privileges and immunities
404
+ that apply to the Licensor or You, including from the legal
405
+ processes of any jurisdiction or authority.
406
+
407
+
408
+ =======================================================================
409
+
410
+ Creative Commons is not a party to its public
411
+ licenses. Notwithstanding, Creative Commons may elect to apply one of
412
+ its public licenses to material it publishes and in those instances
413
+ will be considered the “Licensor.” The text of the Creative Commons
414
+ public licenses is dedicated to the public domain under the CC0 Public
415
+ Domain Dedication. Except for the limited purpose of indicating that
416
+ material is shared under a Creative Commons public license or as
417
+ otherwise permitted by the Creative Commons policies published at
418
+ creativecommons.org/policies, Creative Commons does not authorize the
419
+ use of the trademark "Creative Commons" or any other trademark or logo
420
+ of Creative Commons without its prior written consent including,
421
+ without limitation, in connection with any unauthorized modifications
422
+ to any of its public licenses or any other arrangements,
423
+ understandings, or agreements concerning use of licensed material. For
424
+ the avoidance of doubt, this paragraph does not form part of the
425
+ public licenses.
426
+
427
+ Creative Commons may be contacted at creativecommons.org.
428
+
LICENSE-CODE ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
README.md ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: cc-by-sa-4.0
3
+ language:
4
+ - en
5
+ - pcm
6
+ - yo
7
+ - ig
8
+ - ha
9
+ base_model:
10
+ - FunAudioLLM/Fun-CosyVoice3-0.5B-2512
11
+ pipeline_tag: text-to-speech
12
+ library_name: pytorch
13
+ tags:
14
+ - text-to-speech
15
+ - code-switching
16
+ - nigerian-languages
17
+ - igbo
18
+ - yoruba
19
+ - hausa
20
+ - nigerian-pidgin
21
+ - cosyvoice3
22
+ - instruction-tts
23
+ - dpo
24
+ - experimental
25
+ datasets:
26
+ - google/WaxalNLP
27
+ - McGill-NLP/NaijaS2ST
28
+ - AfriSpeech/open-bible-speech-african
29
+ - nkowaokwu/ibo-dict
30
+ - google/fleurs
31
+ ---
32
+
33
+ <p align="center"><img src="assets/dynamic_accent_banner.jpg" width="600"/></p>
34
+
35
+ # Dynamic Accent Pro v1.1
36
+
37
+ **Experimental prompt-controlled Nigerian TTS and code-switching technology** by Modular. English, Nigerian Pidgin, Igbo, Yoruba and Hausa, mixed inside one sentence and spoken by one consistent voice, with a plain-English **style instruction** (`--instruct`) that steers pace and pausing.
38
+
39
+ Pro v1.1 is a research release, not a production system. It is the Flash recipe (CosyVoice3 LM fine-tuned on licensed Nigerian-language speech, then DPO) with a new instruction layer on top. Read **What works and what does not** before using it.
40
+
41
+ ## What works and what does not
42
+
43
+ | control | status | how |
44
+ |---|---|---|
45
+ | **Pace** ("speak slowly", "speak fast") | **works** | read by the LM from the instruction, sharpened by classifier-free guidance (`--guidance 2`, the default). Slow vs fast is ordered correctly on 18/18 held-out items; slow ≈ 0.89× and fast ≈ 1.15× the default articulation rate |
46
+ | **Pauses** ("plenty of pauses", "hardly any pauses") | **works through a text step** | the instruction also edits the text before synthesis (`pause_text.py`: inserts the model's learned `[pause]` tag for "many", strips commas and pause tags for "few"). Many ≈ 1.28× default pause rate; few ≈ 0.91×. The LM alone barely moves pauses, so `--no_text_pauses` mostly turns this off |
47
+ | Calm / lively tone | **not advertised** | trained, but indistinguishable from chance in our measurements. The words are accepted and ignored in effect |
48
+ | Emotion (happy, sad, angry, …) | **no** | not trained, no data |
49
+ | Code-switching from **tags** (`<ibo>…</ibo> <eng>…</eng>`) | works (the core Dynamic Accent capability) | language embeddings + a speaker token keep one voice across switches |
50
+ | Code-switching from a **request** (`--codeswitch_prompt "mostly English, Igbo greetings"`) | **experimental, separate step** | a text LLM ([Qwen/Qwen3-32B](https://huggingface.co/Qwen/Qwen3-32B), Apache-2.0, not bundled) rewrites plain text into tagged code-switched text *before* synthesis. It is not a TTS capability. Its Pidgin and Hausa are usable; **its Igbo and Yoruba beyond simple greetings are weak and often wrong**, so always read the printed tagged text and fix it |
51
+
52
+ Without `--instruct`, `infer.py` uses Flash-DPO (the default version). Passing `--instruct` switches to Pro automatically unless you set `--version`; `--version pro` without an instruction uses Pro's default prompt, trained on native-like clips.
53
+
54
+ ## Versions and voices
55
+
56
+ | version | LM | notes |
57
+ |---|---|---|
58
+ | `pro` (used automatically with `--instruct`) | `checkpoints/pro/flash_instruct` | Pro v1.1: Flash-DPO → instruction SFT → DPO polish. Instruction control, CFG 2.0 on the instruction, text pause step |
59
+ | `flash` (**default**) | `checkpoints/flash/flash_dpo` | Flash-DPO (no instruction control), the default when no `--instruct` is given: it sounds more natural on plain code-switched text. A/B: `--compare` renders the same text with both |
60
+
61
+ Both use the stock Fun-CosyVoice3-0.5B-2512 flow and vocoder, zero-shot from the voice clip, and the same text front-end (numbers verbalised; missing Yoruba/Igbo diacritics restored by `checkpoints/flash/diacritics_byt5`).
62
+
63
+ | voice (`--voice`) | who |
64
+ |---|---|
65
+ | `anchor_male` (default) | WAXAL Media Trust Yoruba speaker 4 (male), the voice both models were trained around |
66
+ | `waxal_ibo_5_female` | WAXAL Igbo speaker 5 (female), zero-shot from a 6.6 s clip |
67
+ | `waxal_yor_5_female` | WAXAL Yoruba speaker 5 (female), zero-shot; short prompt, so short pauses and more clipped endings |
68
+
69
+ ## Setup (single CUDA GPU)
70
+
71
+ ```bash
72
+ git clone --recursive https://github.com/FunAudioLLM/CosyVoice.git
73
+ cd CosyVoice && git checkout 074ca6dc9e80a2f424f1f74b48bdd7d3fea531cc && git submodule update --init --recursive && cd ..
74
+ huggingface-cli download Modularcomputing/dynamic-accent-pro-v1.1 --local-dir dynamic-accent-pro
75
+ pip install -r dynamic-accent-pro/requirements.txt
76
+ ```
77
+
78
+ Fun-CosyVoice3-0.5B-2512 downloads on first run (`--cv3_dir` / `CV3_DIR` to reuse a copy). Tested on an H100 in bf16; a 16 GB GPU is enough for TTS. The optional code-switch step needs a GPU that holds Qwen3-32B in bf16 (~65 GB); `--cs_llm Qwen/Qwen3-14B` is lighter and worse.
79
+
80
+ ## Usage
81
+
82
+ ```bash
83
+ cd dynamic-accent-pro
84
+ # default prompt (native-like pace and pauses)
85
+ python infer.py --cosyvoice_dir ../CosyVoice --text "<ibo>Ndewo nụ, ụmụnna m.</ibo> <eng>Tonight we will talk about the new market road.</eng>" --out out.wav
86
+
87
+ # pace
88
+ python infer.py --cosyvoice_dir ../CosyVoice --instruct "Please speak slowly and take your time." --text "..." --out slow.wav
89
+ python infer.py --cosyvoice_dir ../CosyVoice --instruct "Speak fast, like a quick everyday conversation." --text "..." --out fast.wav
90
+
91
+ # pauses (text step + instruction)
92
+ python infer.py --cosyvoice_dir ../CosyVoice --instruct "Speak with plenty of pauses between phrases." --text "..." --out many.wav
93
+ python infer.py --cosyvoice_dir ../CosyVoice --instruct "Say it in one smooth flow with hardly any pauses." --text "..." --out few.wav
94
+
95
+ # female Igbo voice
96
+ python infer.py --cosyvoice_dir ../CosyVoice --voice waxal_ibo_5_female --text "<ibo>Kedu ka ị mere?</ibo> <pcm>Abeg no vex, I go call you for evening.</pcm>" --out ibo_f.wav
97
+
98
+ # experimental: plain text + a code-switch request -> Qwen3-32B writes the tagged text (check it!) -> TTS
99
+ python infer.py --cosyvoice_dir ../CosyVoice --codeswitch_prompt "Nigerian Pidgin with an Igbo greeting" \
100
+ --text "My brother, the work is too much today, but we will finish it." --out cs.wav
101
+
102
+ # A/B the same text, seed and voice with Flash and Pro -> cmp_flash.wav, cmp_pro.wav
103
+ python infer.py --cosyvoice_dir ../CosyVoice --compare --compare_versions flash,pro --text "..." --out cmp.wav
104
+ ```
105
+
106
+ | flag | what it does |
107
+ |---|---|
108
+ | `--instruct "..."` | `pro`: natural-language style instruction. Reliable: slow / fast; plenty of / hardly any pauses. Phrasings close to the examples above work best |
109
+ | `--guidance G` | `pro`: classifier-free guidance strength on the instruction (default 2.0; 0 = off, weaker pace control) |
110
+ | `--no_text_pauses` | `pro`: do not edit the text for pause instructions |
111
+ | `--codeswitch_prompt "..."` | optional text-LLM step before synthesis (see above); output is validated (tags, length, loops, back-translation meaning check) and falls back to the input text |
112
+ | `--cs_llm` | the LLM for that step (default `Qwen/Qwen3-32B`) |
113
+ | `--version pro\|flash`, `--compare`, `--compare_versions` | model version / A/B rendering |
114
+ | `--voice`, `--list_voices` | voice selection |
115
+ | `--auto_tag`, `--tag_only`, `--force_lang`, `--lang_map` | language tagging of untagged text (see `tagger.py`) |
116
+ | `--frontend codeswitch\|native\|accent`, `--frontend_lang`, `--no_diacritics` | text front-end |
117
+ | `--seed N` | sampling seed (default 0). Try 2–3 seeds and keep the best take |
118
+ | `--tail_pad N` | append N silence speech tokens before the vocoder so the last syllable is not clipped (opt-in; 6 tested) |
119
+
120
+ Text: wrap each language span in `<eng>`, `<pcm>`, `<yor>`, `<ibo>` or `<hau>`; untagged text is read as English unless `--auto_tag` / `--force_lang` is given. Write Yoruba and Igbo with diacritics where you can. GPU generation is not bit-reproducible (bf16 near-ties).
121
+
122
+ ## Samples
123
+
124
+ `samples/` holds MP3 renders for all five languages and a code-switched sentence, both main voices, with the default prompt and the slow / fast / few-pauses / many-pauses instructions, plus a Flash-DPO render of the same text and seed for A/B (`samples/meta.json` lists text, voice and instruction per file). `samples/codeswitch_frontend/` shows the experimental Qwen3-32B step: input text, request, the tagged text it produced, and the audio. The Igbo and Yoruba demo sentences have **not yet been checked by a native speaker**.
125
+
126
+ ## Results
127
+
128
+ Automatic regression set: 197 held-out sentences per voice (30 each of Igbo, Yoruba, Hausa, Pidgin, English; 24 code-switched; 23 paragraphs), default prompt, seed 0. CER from automatic ASR judges (per language, same judges for every system), pace and pause rate relative to native recordings of the same languages, clipped ending = less than 0.1 s of trailing silence, SIM = speaker similarity to the prompt clip, UTMOSv2 = predicted naturalness.
129
+
130
+ | voice | group | system | CER | CER no-tone | pace × native | pauses × native | clipped endings | SIM | UTMOSv2 |
131
+ |---|---|---|---|---|---|---|---|---|---|
132
+ | anchor_male | all | Flash-DPO | 0.162 | 0.135 | 0.99 | 0.75 | 7% | 0.784 | 3.17 |
133
+ | anchor_male | all | **Pro v1.1** | 0.167 | 0.141 | 0.99 | 1.02 | 8% | 0.789 | 3.16 |
134
+ | anchor_male | ibo | Flash-DPO | 0.189 | 0.158 | 1.06 | 0.40 | 7% | 0.762 | 3.06 |
135
+ | anchor_male | ibo | **Pro v1.1** | 0.216 | 0.181 | 1.02 | 0.62 | 3% | 0.779 | 3.01 |
136
+ | anchor_male | yor | Flash-DPO | 0.230 | 0.144 | 1.08 | 0.30 | 3% | 0.762 | 2.99 |
137
+ | anchor_male | yor | **Pro v1.1** | 0.218 | 0.141 | 1.06 | 0.68 | 3% | 0.770 | 3.02 |
138
+ | anchor_male | hau | Flash-DPO | 0.123 | 0.123 | 0.83 | 0.77 | 0% | 0.788 | 3.26 |
139
+ | anchor_male | hau | **Pro v1.1** | 0.147 | 0.147 | 0.81 | 1.19 | 3% | 0.796 | 3.19 |
140
+ | anchor_male | pcm | Flash-DPO | 0.227 | 0.227 | 0.97 | 0.72 | 10% | 0.782 | 3.26 |
141
+ | anchor_male | pcm | **Pro v1.1** | 0.199 | 0.199 | 1.01 | 0.97 | 3% | 0.779 | 3.25 |
142
+ | anchor_male | eng | Flash-DPO | 0.026 | 0.026 | 0.94 | 1.85 | 17% | 0.790 | 3.26 |
143
+ | anchor_male | eng | **Pro v1.1** | 0.031 | 0.031 | 0.94 | 1.89 | 7% | 0.796 | 3.26 |
144
+ | anchor_male | cs | Flash-DPO | 0.208 | 0.159 | 1.02 | 1.00 | 12% | 0.773 | 3.09 |
145
+ | anchor_male | cs | **Pro v1.1** | 0.213 | 0.162 | 1.05 | 1.27 | 25% | 0.766 | 3.22 |
146
+ | anchor_male | para | Flash-DPO | 0.131 | 0.106 | 1.06 | 1.08 | 0% | 0.843 | 3.28 |
147
+ | anchor_male | para | **Pro v1.1** | 0.149 | 0.126 | 1.05 | 1.29 | 13% | 0.843 | 3.16 |
148
+ | waxal_ibo_5_female | all | Flash-DPO | 0.165 | 0.134 | 1.03 | 0.95 | 11% | 0.700 | 3.51 |
149
+ | waxal_ibo_5_female | all | **Pro v1.1** | 0.152 | 0.123 | 1.01 | 0.84 | 12% | 0.698 | 3.45 |
150
+ | waxal_ibo_5_female | ibo | Flash-DPO | 0.215 | 0.171 | 1.09 | 0.52 | 7% | 0.697 | 3.41 |
151
+ | waxal_ibo_5_female | ibo | **Pro v1.1** | 0.178 | 0.145 | 1.07 | 0.58 | 0% | 0.682 | 3.40 |
152
+ | waxal_ibo_5_female | yor | Flash-DPO | 0.251 | 0.150 | 1.18 | 0.86 | 7% | 0.661 | 3.45 |
153
+ | waxal_ibo_5_female | yor | **Pro v1.1** | 0.224 | 0.118 | 1.11 | 0.38 | 10% | 0.671 | 3.36 |
154
+ | waxal_ibo_5_female | hau | Flash-DPO | 0.117 | 0.117 | 0.86 | 1.20 | 7% | 0.700 | 3.51 |
155
+ | waxal_ibo_5_female | hau | **Pro v1.1** | 0.132 | 0.132 | 0.85 | 0.74 | 13% | 0.676 | 3.47 |
156
+ | waxal_ibo_5_female | pcm | Flash-DPO | 0.184 | 0.183 | 1.02 | 0.92 | 13% | 0.674 | 3.63 |
157
+ | waxal_ibo_5_female | pcm | **Pro v1.1** | 0.152 | 0.151 | 1.02 | 0.83 | 20% | 0.690 | 3.48 |
158
+ | waxal_ibo_5_female | eng | Flash-DPO | 0.031 | 0.031 | 0.99 | 1.34 | 10% | 0.726 | 3.60 |
159
+ | waxal_ibo_5_female | eng | **Pro v1.1** | 0.031 | 0.031 | 0.93 | 1.62 | 13% | 0.745 | 3.52 |
160
+ | waxal_ibo_5_female | cs | Flash-DPO | 0.230 | 0.179 | 1.01 | 1.32 | 29% | 0.682 | 3.50 |
161
+ | waxal_ibo_5_female | cs | **Pro v1.1** | 0.220 | 0.174 | 1.07 | 1.22 | 17% | 0.660 | 3.40 |
162
+ | waxal_ibo_5_female | para | Flash-DPO | 0.133 | 0.106 | 1.07 | 0.99 | 4% | 0.773 | 3.44 |
163
+ | waxal_ibo_5_female | para | **Pro v1.1** | 0.141 | 0.114 | 1.05 | 1.20 | 9% | 0.772 | 3.50 |
164
+
165
+ Native-speaker reference: pace 1.00 / pauses 1.00 by definition. Summary: Pro v1.1 matches Flash-DPO on overall CER for the anchor voice (0.167 vs 0.162) and beats it for the female Igbo voice (0.152 vs 0.165); it pauses much closer to native in Igbo and Yoruba on the anchor voice (0.62× / 0.68× vs 0.40× / 0.30×); it is worse than Flash-DPO on anchor Igbo CER (0.216 vs 0.189) and on Hausa for both voices.
166
+
167
+ Instruction adherence (anchor voice, 18 held-out Igbo/Yoruba/Hausa/Pidgin/English/code-switch items, CFG 2, text pause step on; "direction" = share of items moving the requested way vs the default prompt):
168
+
169
+ | instruction | articulation rate × default | pause rate × default | direction correct |
170
+ |---|---|---|---|
171
+ | Please speak slowly and take your time. | 0.89 | 1.16 | 89% |
172
+ | Speak fast, like a quick everyday conversation. | 1.15 | 0.87 | 94% |
173
+ | Say it in one smooth flow with hardly any pauses. | 0.92 | 0.91 | 56% |
174
+ | Speak with plenty of pauses between phrases. | 0.90 | 1.28 | 78% |
175
+ | Use a calm, gentle, even tone. *(not advertised)* | 0.98 | 1.02 | 50% |
176
+ | Sound lively and expressive. *(not advertised)* | 0.96 | 0.94 | 44% |
177
+
178
+ Slow vs fast on the same item: correct order on 100% of items; many vs few pauses: 67%; lively vs calm: 50% (chance). Note that "slow" also adds pauses (1.16× pause rate). 18 items is a small set; treat these as indicative.
179
+
180
+ ## Limitations
181
+
182
+ - **Experimental.** Pace control is real; pause control depends on the text step; calm/lively and emotion are not available.
183
+ - **Anchor Igbo** CER is still higher than Flash-DPO (0.216 vs 0.189); **Hausa** CER is worse than Flash-DPO for both voices. Flash-DPO remains available as `--version flash`.
184
+ - English stays over-paused relative to native English (about 1.9× native pause rate on the anchor voice, the same as Flash-DPO).
185
+ - Clipped endings remain on code-switched sentences and paragraphs (13–25% on the anchor voice). `--tail_pad 6` (six silence tokens appended before the vocoder, inference-only) removed every measured clipped ending in testing with no consistent CER change (+0.005 overall, within seed noise); it is opt-in until it has been checked by ear.
186
+ - The code-switch request step (Qwen3-32B) is a separate text model: weak Igbo/Yoruba beyond greetings, sometimes refuses to switch and falls back to the input text.
187
+ - Pidgin has little training data and no reliable ASR judge; Yoruba tone quality needs native listening.
188
+ - Female voices are zero-shot. Not evaluated for numbers, abbreviations or URLs.
189
+ - Do not use it to impersonate real people. The voices are WAXAL Media Trust speakers, released under CC-BY-SA-4.0 for speech technology.
190
+
191
+ ## Training
192
+
193
+ 1. **Flash-DPO** (the `flash` version): CosyVoice3 LM fine-tuned on the licensed set below (language embeddings, speaker token, adversarial speaker classifier, rate embedding), then DPO on 1,206 preference pairs.
194
+ 2. **Instruction SFT** (3,000 steps): ~100.9k licensed clips labelled from the audio (articulation rate, pause rate, energy/F0 variation); each training item gets a natural-language instruction built from its labels in the CosyVoice3 system-prompt slot, plus explicit pause/energy control embeddings; 18% of items keep the default prompt, drawn from native-like clips.
195
+ 3. **DPO polish** (134 steps, β 0.1, lr 1e-6, SFT weight 0.1): 700 prompts × 4 samples from the SFT model, 564 preference pairs; reward = CER + pace and pause-rate match to native + a clipped-ending penalty + a small UTMOS guard.
196
+
197
+ Inference: classifier-free guidance on the instruction (conditional = instruction + controls, unconditional = default prompt), g = 2.
198
+
199
+ ## What is in the repo
200
+
201
+ | path | what it is | licence |
202
+ |---|---|---|
203
+ | `checkpoints/pro/flash_instruct/` | Pro v1.1 LM (`best.pt`, sha256 `87c3a7e9…4cab3db`), `config.json`, `spk2id.json`, DPO `args.json`, `log.jsonl` | CC-BY-SA-4.0 |
204
+ | `checkpoints/flash/flash_dpo/` | Flash-DPO LM (`best.pt`, sha256 `f59a6fc0…7e3a307`) | CC-BY-SA-4.0 |
205
+ | `checkpoints/flash/diacritics_byt5/` | Yoruba/Igbo diacritic restoration (fine-tune of google/byt5-small) | CC-BY-SA-4.0 |
206
+ | `prompts/` | voice clips and transcripts (WAXAL) | CC-BY-SA-4.0 |
207
+ | `samples/` | demo renders | CC-BY-SA-4.0 |
208
+ | `config.json` | repo-level manifest (also lets the Hub count downloads) | Apache-2.0 |
209
+ | `settings.json` | versions, voices, defaults | Apache-2.0 |
210
+ | `infer.py`, `flash_lm.py`, `flash_instruct.py`, `instruct_text.py`, `pause_text.py`, `cs_frontend.py`, `tagger.py`, `frontend/`, `code/` | inference, model, instruction, text pause step, code-switch step, tagger, front-end, reference training code | Apache-2.0 |
211
+
212
+ ## Training data and attribution
213
+
214
+ Licensed data only. The weights are trained on, and the voice prompts taken from:
215
+
216
+ | dataset | licence | attribution |
217
+ |---|---|---|
218
+ | [WAXAL](https://huggingface.co/datasets/google/WaxalNLP), Media Trust TTS subsets (Igbo, Yoruba, Hausa, Nigerian Pidgin); all voice prompts | CC-BY-SA-4.0 | Google and Media Trust, WAXAL (arXiv:2602.02734) |
219
+ | [Open.Bible Igbo](https://huggingface.co/datasets/AfriSpeech/open-bible-speech-african) | CC-BY-SA-4.0 | Biblica, Inc. via open.bible |
220
+ | [NaijaS2ST](https://huggingface.co/datasets/McGill-NLP/NaijaS2ST) (Igbo) | CC-BY-4.0 | McGill-NLP (arXiv:2604.16287) |
221
+ | [IgboAPI](https://huggingface.co/datasets/nkowaokwu/ibo-dict) | CC-BY-4.0 | Nkọwa okwu / IgboAPI contributors |
222
+ | [IgboSynCorp](https://doi.org/10.7910/dvn/yb9fwk) (+ pseudo-transcripts) | CC0-1.0 | University of Ibadan and Afe Babalola University (Lacuna Fund) |
223
+ | [FLEURS](https://huggingface.co/datasets/google/fleurs) | CC-BY-4.0 | Google (Conneau et al., 2022) |
224
+ | [BibleTTS](https://masakhane-io.github.io/bibleTTS/) (Yoruba, Hausa) | CC-BY-SA-4.0 | Meyer et al. (2022) |
225
+ | [OpenSLR SLR70](https://openslr.org/70/), [SLR86](https://openslr.org/86/) | CC-BY-SA-4.0 | Google |
226
+ | synthetic code-switch pairs (concatenated clips from the sets above) | follows the sources | after Zhang et al. (2019) |
227
+ | base model [Fun-CosyVoice3-0.5B-2512](https://huggingface.co/FunAudioLLM/Fun-CosyVoice3-0.5B-2512) | Apache-2.0 | FunAudioLLM / Alibaba Tongyi Lab |
228
+ | optional [Qwen3-32B](https://huggingface.co/Qwen/Qwen3-32B) (not bundled; code-switch step only) | Apache-2.0 | Qwen team, Alibaba |
229
+
230
+ DPO preference pairs are the model's own renders, scored automatically. Excluded on licence grounds: African Voices / WaZoBiaSpeech (cards ban TTS use), NaijaVoices (non-commercial), and non-commercial TTS weights.
231
+
232
+ ## Licence
233
+
234
+ - **Model weights, voice prompts and samples** (`checkpoints/`, `prompts/`, `samples/`): **[CC-BY-SA-4.0](LICENSE)**. Commercial use is allowed; credit Modular and the datasets above; derivatives of the weights must be shared under CC-BY-SA-4.0.
235
+ - **Code** (`*.py`, `frontend/`, `code/`, `settings.json`, `config.json`): **[Apache-2.0](LICENSE-CODE)**.
236
+ - Fun-CosyVoice3-0.5B-2512, byt5-small and Qwen3 are Apache-2.0; their notices apply to the parts they contribute.
237
+
238
+ ## Citation
239
+
240
+ ```bibtex
241
+ @misc{dynamicaccentpro2026,
242
+ title = {Dynamic Accent Pro v1.1: prompt-controlled code-switching TTS for Nigerian languages},
243
+ author = {Modular},
244
+ year = {2026},
245
+ url = {https://huggingface.co/Modularcomputing/dynamic-accent-pro-v1.1}
246
+ }
247
+ ```
assets/dynamic_accent_banner.jpg ADDED

Git LFS Details

  • SHA256: 0b51f3df37d126969b4f4b5e6737e9d92d6e91e5db78bac0cc47bd6d16ca55be
  • Pointer size: 131 Bytes
  • Size of remote file: 305 kB
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
 
checkpoints/pro/flash_instruct/args.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"cmd": "train", "lm": "/home/dev/fi/run/best.pt", "out": "/home/dev/fi/run_dpo", "shard": "0/1", "k": 4, "lr": 1e-06, "beta": 0.1, "sft": 0.1, "bs": 8, "epochs": 2}
checkpoints/pro/flash_instruct/best.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:87c3a7e9a806e89cf1b77cff78989ebe63938016f5d8066cf673d350c4cab3db
3
+ size 2047789708
checkpoints/pro/flash_instruct/config.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "engine": "cosyvoice3",
3
+ "base": "FunAudioLLM/Fun-CosyVoice3-0.5B-2512",
4
+ "rate": "normal",
5
+ "sampler": "silexcap",
6
+ "one_pass": true,
7
+ "instruct": true,
8
+ "cfg": 2.0,
9
+ "text_pauses": true,
10
+ "sha256_best_pt": "87c3a7e9a806e89cf1b77cff78989ebe63938016f5d8066cf673d350c4cab3db",
11
+ "autoresearch_artifact": "art-dbr28 (private)",
12
+ "training": "Flash-DPO LM -> Flash-Instruct SFT (3,000 steps, ~100.9k licensed clips with audio-derived pace/pause/energy labels, natural-language instruction in the CosyVoice3 system-prompt slot, 18% default-prompt items, pause/energy control embeddings) -> DPO polish (700 prompts x 4 samples, 564 preference pairs, reward = CER + pace and pause match to native + clipped-ending penalty + small UTMOS guard; 134 steps, beta 0.1, lr 1e-6, SFT weight 0.1; final step kept, val pref-acc 0.61 (0.75 after epoch 1, 28-pair val set))",
13
+ "data": "licensed only (no African Voices / WaZoBiaSpeech, no NaijaVoices)"
14
+ }
checkpoints/pro/flash_instruct/log.jsonl ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {"step": 0, "val": {"acc": 0.0, "margin": 0.0}}
2
+ {"step": 10, "of": 134, "loss": 0.9194, "acc": 0.625, "margin": 0.023, "gn": 64.85, "min": 0.1}
3
+ {"step": 20, "of": 134, "loss": 0.9342, "acc": 0.875, "margin": 0.019, "gn": 75.36, "min": 0.1}
4
+ {"step": 30, "of": 134, "loss": 0.8986, "acc": 0.5, "margin": 0.06, "gn": 64.13, "min": 0.2}
5
+ {"step": 40, "of": 134, "loss": 0.9182, "acc": 0.625, "margin": 0.03, "gn": 83.13, "min": 0.2}
6
+ {"step": 50, "of": 134, "loss": 0.9765, "acc": 0.375, "margin": -0.053, "gn": 77.84, "min": 0.3}
7
+ {"step": 60, "of": 134, "loss": 0.8475, "acc": 0.875, "margin": 0.203, "gn": 61.06, "min": 0.3}
8
+ {"epoch": 1, "step": 67, "val": {"acc": 0.75, "margin": 0.12087846860023481}}
9
+ {"step": 70, "of": 134, "loss": 0.5091, "acc": 1.0, "margin": 1.357, "gn": 38.83, "min": 0.4}
10
+ {"step": 80, "of": 134, "loss": 0.4715, "acc": 1.0, "margin": 1.605, "gn": 27.66, "min": 0.5}
11
+ {"step": 90, "of": 134, "loss": 0.5161, "acc": 1.0, "margin": 1.419, "gn": 33.8, "min": 0.5}
12
+ {"step": 100, "of": 134, "loss": 0.4414, "acc": 1.0, "margin": 1.709, "gn": 27.54, "min": 0.5}
13
+ {"step": 110, "of": 134, "loss": 0.5218, "acc": 1.0, "margin": 1.279, "gn": 32.5, "min": 0.6}
14
+ {"step": 120, "of": 134, "loss": 0.4836, "acc": 1.0, "margin": 1.426, "gn": 34.52, "min": 0.6}
15
+ {"step": 130, "of": 134, "loss": 0.5242, "acc": 1.0, "margin": 1.501, "gn": 33.22, "min": 0.7}
16
+ {"epoch": 2, "step": 134, "val": {"acc": 0.6071428571428571, "margin": 0.09966237439740715}}
checkpoints/pro/flash_instruct/spk2id.json ADDED
The diff for this file is too large to render. See raw diff
 
code/da/__init__.py ADDED
File without changes
code/da/data.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Arm A data: memmapped Vocos mels + byte tokens with per-token language ids; frame-budget bucketing.
2
+ Beta-binomial alignment priors are computed in DataLoader workers."""
3
+ import json, os, random
4
+ import numpy as np
5
+ import torch
6
+ from torch.utils.data import Dataset, Sampler
7
+
8
+ from .text import encode, LANG2ID
9
+ from .model import beta_binomial_prior
10
+
11
+ N_MELS = 100
12
+
13
+
14
+ def load_meta(root, datasets, split="train", max_frames=1400, max_tokens=420):
15
+ items, mm = [], {}
16
+ for ds in datasets:
17
+ p = f"{root}/{ds}/meta.jsonl"
18
+ if not os.path.exists(p) or os.path.getsize(p) == 0:
19
+ continue
20
+ mm[ds] = np.memmap(f"{root}/{ds}/mels.f16", dtype=np.float16, mode="r").reshape(-1, N_MELS)
21
+ for l in open(p, encoding="utf-8"):
22
+ m = json.loads(l)
23
+ if m["split"] != split or m["nf"] > max_frames:
24
+ continue
25
+ if len(m["text"].encode("utf-8")) + 2 > max_tokens:
26
+ continue
27
+ items.append(m)
28
+ return items, mm
29
+
30
+
31
+ class TTSData(Dataset):
32
+ def __init__(self, items, mm, spk2id):
33
+ self.items, self.mm, self.spk2id = items, mm, spk2id
34
+
35
+ def __len__(self):
36
+ return len(self.items)
37
+
38
+ def __getitem__(self, i):
39
+ m = self.items[i]
40
+ mel = np.asarray(self.mm[m["ds"]][m["off"]: m["off"] + m["nf"]], dtype=np.float32)
41
+ tok, _ = encode(f"<{m['lang']}>{m['text']}</{m['lang']}>")
42
+ lang = [LANG2ID[m["lang"]]] * len(tok)
43
+ return dict(tok=tok, lang=lang, mel=mel, spk=self.spk2id[m["spk"]], id=m["id"])
44
+
45
+
46
+ def collate(batch):
47
+ B = len(batch)
48
+ N = max(len(b["tok"]) for b in batch)
49
+ T = max(len(b["mel"]) for b in batch)
50
+ tok = torch.zeros(B, N, dtype=torch.long)
51
+ lang = torch.zeros(B, N, dtype=torch.long)
52
+ mel = torch.zeros(B, T, N_MELS)
53
+ prior = torch.full((B, T, N), -1e4)
54
+ tl, ml = torch.zeros(B, dtype=torch.long), torch.zeros(B, dtype=torch.long)
55
+ for i, b in enumerate(batch):
56
+ n, t = len(b["tok"]), len(b["mel"])
57
+ tok[i, :n] = torch.tensor(b["tok"]); lang[i, :n] = torch.tensor(b["lang"])
58
+ mel[i, :t] = torch.from_numpy(b["mel"]); tl[i], ml[i] = n, t
59
+ prior[i, :t, :n] = beta_binomial_prior(n, t)
60
+ spk = torch.tensor([b["spk"] for b in batch])
61
+ return dict(tok=tok, lang=lang, tlens=tl, mel=mel, mlens=ml, spk=spk, prior=prior)
62
+
63
+
64
+ class FrameBucketSampler(Sampler):
65
+ """Batches of similar length under a padded-frame budget; dataset weights by repetition."""
66
+
67
+ def __init__(self, items, frame_budget=40000, weights=None, seed=0):
68
+ self.items, self.budget, self.seed, self.epoch = items, frame_budget, seed, 0
69
+ self.weights = weights or {}
70
+
71
+ def _batches(self):
72
+ rng = random.Random(self.seed + self.epoch)
73
+ idx = []
74
+ for i, m in enumerate(self.items):
75
+ w = self.weights.get(m["ds"], 1.0)
76
+ idx += [i] * int(w) + ([i] if rng.random() < (w - int(w)) else [])
77
+ idx.sort(key=lambda i: self.items[i]["nf"] + rng.random() * 30)
78
+ batches, cur, mx = [], [], 0
79
+ for i in idx:
80
+ nf = self.items[i]["nf"]
81
+ if cur and max(mx, nf) * (len(cur) + 1) > self.budget:
82
+ batches.append(cur); cur, mx = [], 0
83
+ cur.append(i); mx = max(mx, nf)
84
+ if cur:
85
+ batches.append(cur)
86
+ rng.shuffle(batches)
87
+ return batches
88
+
89
+ def __iter__(self):
90
+ b = self._batches()
91
+ self.epoch += 1
92
+ return iter(b)
93
+
94
+ def __len__(self):
95
+ return len(self._batches())
code/da/model.py ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Dynamic Accent - Arm A, written from scratch in PyTorch.
2
+
3
+ Backbone: Parallel Tacotron 2 (Elias et al. 2021, arXiv 2103.14574): non-autoregressive, token durations,
4
+ differentiable learned upsampling (MLP over start/end offsets -> token->frame weights W), iterative mel loss.
5
+ Multilingual recipe: Zhang et al. 2019 (arXiv 1907.04448): separate speaker + language embeddings,
6
+ domain-adversarial speaker classifier (gradient reversal) on text-encoder outputs, residual VAE.
7
+
8
+ Documented deviation (alignment): PT2 trains durations end-to-end with Soft-DTW. Soft-DTW is O(T*N)-sequential and
9
+ too slow for a 2 h smoke budget, so we use the internal unsupervised alignment-learning objective of
10
+ Badlani et al. 2022 ("One TTS Alignment To Rule Them All", arXiv 2108.10447): a small text/mel attention trained with
11
+ a CTC forward-sum loss + beta-binomial prior, hardened with monotonic alignment search (MAS). The hard durations
12
+ supervise the duration predictor and drive the SAME differentiable PT2 upsampler at train time; predicted durations
13
+ drive it at inference. No external aligner is used.
14
+ """
15
+ import math
16
+ import numpy as np
17
+ import torch
18
+ import torch.nn as nn
19
+ import torch.nn.functional as F
20
+
21
+ from .text import VOCAB, LANGS
22
+
23
+ N_MELS = 100
24
+
25
+
26
+ # ---------------------------------------------------------------- utils
27
+ def seq_mask(lens, maxlen=None):
28
+ maxlen = maxlen or int(lens.max())
29
+ return torch.arange(maxlen, device=lens.device)[None] < lens[:, None] # True = valid
30
+
31
+
32
+ def sinusoid(T, d, device):
33
+ pos = torch.arange(T, device=device).float()[:, None]
34
+ i = torch.arange(0, d, 2, device=device).float()[None]
35
+ ang = pos / (10000 ** (i / d))
36
+ pe = torch.zeros(T, d, device=device)
37
+ pe[:, 0::2] = torch.sin(ang)
38
+ pe[:, 1::2] = torch.cos(ang)
39
+ return pe
40
+
41
+
42
+ class GradReverse(torch.autograd.Function):
43
+ """Gradient reversal. As in Zhang et al. 2019 the reversed gradient sent into the text encoder is clipped
44
+ (here: its L2 norm is capped at `clip`), which keeps the min-max game from blowing up the encoder."""
45
+
46
+ @staticmethod
47
+ def forward(ctx, x, lam, clip):
48
+ ctx.lam, ctx.clip = lam, clip
49
+ return x.view_as(x)
50
+
51
+ @staticmethod
52
+ def backward(ctx, g):
53
+ g = -ctx.lam * g
54
+ if ctx.clip is not None:
55
+ n = g.norm()
56
+ if n > ctx.clip:
57
+ g = g * (ctx.clip / (n + 1e-12))
58
+ return g, None, None
59
+
60
+
61
+ def grad_reverse(x, lam=1.0, clip=0.5):
62
+ return GradReverse.apply(x, lam, clip)
63
+
64
+
65
+ class ConvNorm(nn.Module):
66
+ def __init__(self, cin, cout, k=5, drop=0.1):
67
+ super().__init__()
68
+ self.conv = nn.Conv1d(cin, cout, k, padding=k // 2)
69
+ self.ln = nn.LayerNorm(cout)
70
+ self.drop = nn.Dropout(drop)
71
+
72
+ def forward(self, x, mask): # x [B,T,C], mask [B,T]
73
+ x = x.masked_fill(~mask[..., None], 0)
74
+ y = self.conv(x.transpose(1, 2)).transpose(1, 2)
75
+ return self.drop(F.relu(self.ln(y)))
76
+
77
+
78
+ class FFTBlock(nn.Module):
79
+ """Pre-LN self-attention + convolutional feed-forward (FastSpeech/PT2-style lightweight block)."""
80
+
81
+ def __init__(self, d, heads=4, ff=4, k=3, drop=0.1):
82
+ super().__init__()
83
+ self.ln1 = nn.LayerNorm(d)
84
+ self.att = nn.MultiheadAttention(d, heads, dropout=drop, batch_first=True)
85
+ self.ln2 = nn.LayerNorm(d)
86
+ self.c1 = nn.Conv1d(d, d * ff, k, padding=k // 2)
87
+ self.c2 = nn.Conv1d(d * ff, d, 1)
88
+ self.drop = nn.Dropout(drop)
89
+
90
+ def forward(self, x, mask):
91
+ h = self.ln1(x)
92
+ a, _ = self.att(h, h, h, key_padding_mask=~mask, need_weights=False)
93
+ x = x + self.drop(a)
94
+ h = self.ln2(x).masked_fill(~mask[..., None], 0).transpose(1, 2)
95
+ h = self.c2(F.gelu(self.c1(h))).transpose(1, 2)
96
+ x = x + self.drop(h)
97
+ return x.masked_fill(~mask[..., None], 0)
98
+
99
+
100
+ # ---------------------------------------------------------------- modules
101
+ class TextEncoder(nn.Module):
102
+ def __init__(self, d, n_lang, layers=6):
103
+ super().__init__()
104
+ self.emb = nn.Embedding(VOCAB, d, padding_idx=0)
105
+ self.lang_in = nn.Embedding(n_lang, d) # per-token language id at the input
106
+ self.convs = nn.ModuleList([ConvNorm(d, d, 5) for _ in range(3)])
107
+ self.blocks = nn.ModuleList([FFTBlock(d) for _ in range(layers)])
108
+
109
+ def forward(self, tok, lang, mask):
110
+ x = self.emb(tok) + self.lang_in(lang)
111
+ for c in self.convs:
112
+ x = x + c(x, mask)
113
+ x = x + sinusoid(x.shape[1], x.shape[2], x.device)[None]
114
+ for b in self.blocks:
115
+ x = b(x, mask)
116
+ return x
117
+
118
+
119
+ class ResidualVAE(nn.Module):
120
+ """Global residual (prosody) encoder: mel -> q(z|x); z broadcast to every token (Zhang 2019 / Hsu 2019)."""
121
+
122
+ def __init__(self, zdim=16, h=256):
123
+ super().__init__()
124
+ self.convs = nn.Sequential(
125
+ nn.Conv1d(N_MELS, h, 3, padding=1), nn.ReLU(),
126
+ nn.Conv1d(h, h, 3, stride=2, padding=1), nn.ReLU(),
127
+ nn.Conv1d(h, h, 3, stride=2, padding=1), nn.ReLU())
128
+ self.gru = nn.GRU(h, h // 2, batch_first=True, bidirectional=True)
129
+ self.mu = nn.Linear(h, zdim)
130
+ self.logvar = nn.Linear(h, zdim)
131
+
132
+ def forward(self, mel, mel_lens):
133
+ h = self.convs(mel.transpose(1, 2)).transpose(1, 2)
134
+ lens = torch.clamp((mel_lens + 3) // 4, min=1)
135
+ h, _ = self.gru(h)
136
+ m = seq_mask(lens, h.shape[1]).float()[..., None]
137
+ h = (h * m).sum(1) / m.sum(1)
138
+ mu, lv = self.mu(h), self.logvar(h).clamp(-8, 8)
139
+ z = mu + torch.randn_like(mu) * torch.exp(0.5 * lv) if self.training else mu
140
+ kl = 0.5 * (mu.pow(2) + lv.exp() - 1 - lv).sum(-1).mean()
141
+ return z, kl
142
+
143
+
144
+ class Aligner(nn.Module):
145
+ """Unsupervised text-mel alignment (Badlani et al. 2022)."""
146
+
147
+ def __init__(self, d, h=256, temp=0.0005):
148
+ super().__init__()
149
+ self.temp = temp
150
+ self.key = nn.Sequential(nn.Conv1d(d, h * 2, 3, padding=1), nn.ReLU(), nn.Conv1d(h * 2, 80, 1))
151
+ self.query = nn.Sequential(nn.Conv1d(N_MELS, h * 2, 3, padding=1), nn.ReLU(),
152
+ nn.Conv1d(h * 2, h, 1), nn.ReLU(), nn.Conv1d(h, 80, 1))
153
+
154
+ def forward(self, txt, mel, tmask, mmask, prior_log):
155
+ k = self.key(txt.transpose(1, 2)) # B,80,N
156
+ q = self.query(mel.transpose(1, 2)) # B,80,T
157
+ q, k = q.float(), k.float()
158
+ dist = (q.pow(2).sum(1)[:, :, None] + k.pow(2).sum(1)[:, None, :]
159
+ - 2 * torch.bmm(q.transpose(1, 2), k)).clamp_min(0) # B,T,N (memory-light ||q-k||^2)
160
+ logits = -self.temp * dist
161
+ logits = logits.masked_fill(~tmask[:, None, :], -1e4)
162
+ logp = F.log_softmax(logits, -1) + prior_log # prior in log domain
163
+ logp = logp.masked_fill(~tmask[:, None, :], -1e4)
164
+ soft = F.softmax(logp, -1) * mmask[..., None]
165
+ return logits, logp, soft
166
+
167
+
168
+ def forward_sum_loss(logits, tlens, mlens, blank_logprob=-1.0):
169
+ """CTC forward-sum: every token must be visited monotonically. logits [B,T,N] (pre-softmax, masked)."""
170
+ B, T, N = logits.shape
171
+ lp = F.pad(logits, (1, 0), value=blank_logprob)
172
+ lp = F.log_softmax(lp, -1).transpose(0, 1) # T,B,N+1
173
+ tgt = torch.arange(1, N + 1, device=logits.device)[None].expand(B, N)
174
+ return F.ctc_loss(lp.float(), tgt, mlens, tlens, blank=0, reduction="mean", zero_infinity=True)
175
+
176
+
177
+ def _mas_np(logp, N, T):
178
+ """Monotonic alignment search on one [T,N] log-prob matrix -> hard [T,N]."""
179
+ Q = np.full((T, N), -np.inf, dtype=np.float32)
180
+ Q[0, 0] = logp[0, 0]
181
+ for t in range(1, T):
182
+ for n in range(min(N, t + 1)):
183
+ stay = Q[t - 1, n]
184
+ move = Q[t - 1, n - 1] if n > 0 else -np.inf
185
+ Q[t, n] = max(stay, move) + logp[t, n]
186
+ path = np.zeros((T, N), dtype=np.float32)
187
+ n = N - 1
188
+ for t in range(T - 1, -1, -1):
189
+ path[t, n] = 1
190
+ if t > 0 and n > 0 and (n == t or Q[t - 1, n - 1] > Q[t - 1, n]):
191
+ n -= 1
192
+ return path
193
+
194
+
195
+ try:
196
+ import numba
197
+
198
+ _mas_np = numba.njit(cache=True)(_mas_np) # noqa: F811
199
+ except Exception: # numba optional (CPU unit tests)
200
+ pass
201
+
202
+
203
+ @torch.no_grad()
204
+ def mas(logp, tlens, mlens):
205
+ out = torch.zeros_like(logp)
206
+ lp = logp.float().cpu().numpy()
207
+ for b in range(lp.shape[0]):
208
+ T, N = int(mlens[b]), int(tlens[b])
209
+ out[b, :T, :N] = torch.from_numpy(_mas_np(np.ascontiguousarray(lp[b, :T, :N]), N, T))
210
+ return out
211
+
212
+
213
+ _prior_cache = {}
214
+
215
+
216
+ def beta_binomial_prior(N, T, scale=1.0):
217
+ from scipy.stats import betabinom
218
+ key = (N, T)
219
+ if key not in _prior_cache:
220
+ x = np.arange(N)[None]
221
+ t = np.arange(1, T + 1)[:, None]
222
+ pmf = betabinom.pmf(x, N - 1, scale * t, scale * (T + 1 - t))
223
+ _prior_cache[key] = torch.from_numpy(np.log(pmf + 1e-8)).float()
224
+ if len(_prior_cache) > 20000:
225
+ _prior_cache.clear()
226
+ return _prior_cache[key]
227
+
228
+
229
+ def batch_prior(tlens, mlens, N, T, device):
230
+ p = torch.full((len(tlens), T, N), -1e4)
231
+ for b in range(len(tlens)):
232
+ n, t = int(tlens[b]), int(mlens[b])
233
+ p[b, :t, :n] = beta_binomial_prior(n, t)
234
+ return p.to(device)
235
+
236
+
237
+ class DurationPredictor(nn.Module):
238
+ def __init__(self, d, h=256):
239
+ super().__init__()
240
+ self.convs = nn.ModuleList([ConvNorm(d, h, 3), ConvNorm(h, h, 3), ConvNorm(h, h, 3)])
241
+ self.out = nn.Linear(h, 1)
242
+
243
+ def forward(self, x, mask):
244
+ for c in self.convs:
245
+ x = c(x, mask)
246
+ return self.out(x).squeeze(-1).masked_fill(~mask, 0) # log(1 + d)
247
+
248
+
249
+ class LearnedUpsampler(nn.Module):
250
+ """PT2 differentiable upsampling: W[t,k] = softmax_k MLP(S_tk, E_tk, h_k); O = W H + proj(sum_k W C)."""
251
+
252
+ def __init__(self, d, hw=16, hc=8):
253
+ super().__init__()
254
+ self.se_w = nn.Linear(2, hw)
255
+ self.h_w = nn.Linear(d, hw)
256
+ self.w_out = nn.Linear(hw, 1)
257
+ self.se_c = nn.Linear(2, hc)
258
+ self.h_c = nn.Linear(d, hc)
259
+ self.c_out = nn.Linear(hc, d)
260
+
261
+ def forward(self, H, dur, tmask, T):
262
+ dur = dur.masked_fill(~tmask, 0)
263
+ e = torch.cumsum(dur, 1)
264
+ s = e - dur
265
+ t = torch.arange(T, device=H.device).float()[None, :, None] + 0.5
266
+ S = (t - s[:, None, :]) / 10.0
267
+ E = (e[:, None, :] - t) / 10.0
268
+ se = torch.stack([S, E], -1) # B,T,N,2
269
+ lw = self.w_out(F.silu(self.se_w(se) + self.h_w(H)[:, None])).squeeze(-1)
270
+ lw = lw.masked_fill(~tmask[:, None, :], -1e4)
271
+ W = torch.softmax(lw, -1) # B,T,N
272
+ C = F.silu(self.se_c(se) + self.h_c(H)[:, None]) # B,T,N,hc
273
+ out = torch.bmm(W, H) + self.c_out(torch.einsum("btn,btnc->btc", W, C))
274
+ return out, W
275
+
276
+
277
+ class MelDecoder(nn.Module):
278
+ def __init__(self, d, layers=6, n_iter=3):
279
+ super().__init__()
280
+ self.blocks = nn.ModuleList([FFTBlock(d) for _ in range(layers)])
281
+ self.n_iter = n_iter
282
+ self.outs = nn.ModuleList([nn.Linear(d, N_MELS) for _ in range(n_iter)])
283
+
284
+ def forward(self, x, mask):
285
+ x = x + sinusoid(x.shape[1], x.shape[2], x.device)[None]
286
+ mels = []
287
+ L = len(self.blocks)
288
+ for i, b in enumerate(self.blocks):
289
+ x = b(x, mask)
290
+ j = i - (L - self.n_iter)
291
+ if j >= 0:
292
+ mels.append(self.outs[j](x))
293
+ return mels
294
+
295
+
296
+ # ---------------------------------------------------------------- full model
297
+ class DynamicAccentTTS(nn.Module):
298
+ def __init__(self, n_spk, n_lang=len(LANGS), d=384, enc_layers=6, dec_layers=6, spk_dim=128, lang_dim=64,
299
+ zdim=16, adv_hidden=256, mel_mean=None, mel_std=None, align_temp=0.0005):
300
+ super().__init__()
301
+ self.n_spk = n_spk
302
+ self.encoder = TextEncoder(d, n_lang, enc_layers)
303
+ self.spk_emb = nn.Embedding(n_spk, spk_dim)
304
+ self.lang_out = nn.Embedding(n_lang, lang_dim) # per-token, concatenated to encoder outputs
305
+ self.vae = ResidualVAE(zdim)
306
+ self.cond = nn.Linear(d + spk_dim + lang_dim + zdim, d)
307
+ self.adv = nn.Sequential(nn.LayerNorm(d), nn.Linear(d, adv_hidden), nn.ReLU(), nn.Linear(adv_hidden, n_spk))
308
+ self.aligner = Aligner(d, temp=align_temp)
309
+ self.dur = DurationPredictor(d)
310
+ self.up = LearnedUpsampler(d)
311
+ self.dec_spk = nn.Linear(spk_dim, d)
312
+ self.decoder = MelDecoder(d, dec_layers)
313
+ self.register_buffer("mel_mean", torch.tensor(mel_mean if mel_mean is not None else -2.5))
314
+ self.register_buffer("mel_std", torch.tensor(mel_std if mel_std is not None else 2.2))
315
+
316
+ def _cond(self, Ht, spk, lang, z):
317
+ B, N, _ = Ht.shape
318
+ s = self.spk_emb(spk)[:, None].expand(B, N, -1)
319
+ zz = z[:, None].expand(B, N, -1)
320
+ return self.cond(torch.cat([Ht, s, self.lang_out(lang), zz], -1)), s
321
+
322
+ def forward(self, tok, lang, tlens, mel, mlens, spk, step=0, adv_lambda=1.0, bin_start=4000, prior=None):
323
+ tmask, mmask = seq_mask(tlens, tok.shape[1]), seq_mask(mlens, mel.shape[1])
324
+ meln = (mel - self.mel_mean) / self.mel_std
325
+ Ht = self.encoder(tok, lang, tmask)
326
+ # adversarial speaker classifier on text encodings (Zhang 2019): GRL removes speaker info from text path
327
+ adv_logits = self.adv(grad_reverse(Ht, adv_lambda))
328
+ adv_ce = F.cross_entropy(adv_logits[tmask], spk[:, None].expand_as(tok)[tmask])
329
+ adv_acc = (adv_logits[tmask].argmax(-1) == spk[:, None].expand_as(tok)[tmask]).float().mean()
330
+ z, kl = self.vae(meln, mlens)
331
+ H, _ = self._cond(Ht, spk, lang, z)
332
+ # alignment (internal, unsupervised)
333
+ if prior is None:
334
+ prior = batch_prior(tlens, mlens, tok.shape[1], mel.shape[1], tok.device)
335
+ logits, logp, soft = self.aligner(self.encoder.emb(tok) + Ht.detach(), meln, tmask, mmask, prior)
336
+ l_fs = forward_sum_loss(logits.masked_fill(~mmask[..., None], -1e4) + prior, tlens, mlens)
337
+ hard = mas(logp, tlens, mlens)
338
+ l_bin = -(hard * torch.log(soft.clamp_min(1e-8))).sum() / hard.sum() if step >= bin_start else soft.new_zeros(())
339
+ dur_t = hard.sum(1) # B,N frames
340
+ # durations
341
+ logd = self.dur(H.detach() if step < 1000 else H, tmask)
342
+ l_dur = F.mse_loss(logd[tmask], torch.log1p(dur_t[tmask]))
343
+ # upsample with target durations (train) -> decoder
344
+ up, W = self.up(H, dur_t, tmask, mel.shape[1])
345
+ x = up + self.dec_spk(self.spk_emb(spk))[:, None]
346
+ mels = self.decoder(x, mmask)
347
+ m = mmask[..., None].float()
348
+ l_mel = sum((F.l1_loss(o, meln, reduction="none") * m).sum() / (m.sum() * N_MELS) for o in mels) / len(mels)
349
+ return dict(mel=l_mel, dur=l_dur, fs=l_fs, bin=l_bin, kl=kl, adv=adv_ce, adv_acc=adv_acc.detach()), mels[-1]
350
+
351
+ @torch.no_grad()
352
+ def infer(self, tok, lang, spk, pace=1.0, z=None):
353
+ tlens = torch.full((tok.shape[0],), tok.shape[1], device=tok.device)
354
+ tmask = seq_mask(tlens, tok.shape[1])
355
+ Ht = self.encoder(tok, lang, tmask)
356
+ if z is None:
357
+ z = torch.zeros(tok.shape[0], self.vae.mu.out_features, device=tok.device)
358
+ H, _ = self._cond(Ht, spk, lang, z)
359
+ dur = (torch.expm1(self.dur(H, tmask)).clamp(min=0) * pace).masked_fill(~tmask, 0)
360
+ T = max(int(dur.sum(1).max().round().item()), 1)
361
+ up, W = self.up(H, dur, tmask, T)
362
+ x = up + self.dec_spk(self.spk_emb(spk))[:, None]
363
+ mel = self.decoder(x, torch.ones(tok.shape[0], T, dtype=torch.bool, device=tok.device))[-1]
364
+ return mel * self.mel_std + self.mel_mean, dur
365
+
366
+
367
+ def count_params(m):
368
+ return sum(p.numel() for p in m.parameters())
code/da/p2common.py ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Phase 2 shared tools: romanisation for MMS forced alignment, chunked aligner, pause normalisation,
2
+ pause / F0 / Yoruba-tone metrics. Used by prep2.py (data) and eval2.py (metrics)."""
3
+ import re, unicodedata
4
+ import numpy as np
5
+
6
+ # ------------------------------------------------------------------ text
7
+ _MAP = {"ọ": "o", "ẹ": "e", "ṣ": "s", "ị": "i", "ụ": "u", "ṅ": "n", "ń": "n", "ǹ": "n", "ḿ": "m", "ɓ": "b", "ɗ": "d",
8
+ "ƙ": "k", "ƴ": "y", "ŋ": "n", "ɛ": "e", "ɔ": "o", "ß": "s", "æ": "ae", "œ": "oe", "ø": "o", "đ": "d", "ł": "l"}
9
+ TONE_MARK = {"\u0301": 2, "\u0300": 0, "\u0304": 1} # acute=H, grave=L, macron=M
10
+ SENT_END = set(".?!")
11
+ CLAUSE = set(";:")
12
+ COMMA = set(",")
13
+ _VOW = set("aeiou")
14
+
15
+
16
+ def roman_char(c):
17
+ """one NFC char -> (romanised latin string, tone or None)."""
18
+ lc = c.lower()
19
+ if lc in _MAP:
20
+ return _MAP[lc], None
21
+ d = unicodedata.normalize("NFD", lc)
22
+ base = "".join(x for x in d if unicodedata.category(x) != "Mn")
23
+ tone = None
24
+ for x in d:
25
+ if x in TONE_MARK:
26
+ tone = TONE_MARK[x]
27
+ base = "".join(_MAP.get(b, b) for b in base)
28
+ base = "".join(b for b in base if ("a" <= b <= "z") or b == "'")
29
+ return base, tone
30
+
31
+
32
+ def words_for_align(text):
33
+ """text -> list of dict(word, rom, punct_after, chars=[(rom_char, orig_index, tone)]).
34
+ Words that romanise to nothing (digits, symbols) are aligned with the '*' star token."""
35
+ out = []
36
+ for m in re.finditer(r"\S+", text):
37
+ w = m.group(0)
38
+ chars = []
39
+ for k, c in enumerate(w):
40
+ r, t = roman_char(c)
41
+ for j, rc in enumerate(r):
42
+ chars.append((rc, m.start() + k, t if j == len(r) - 1 else None))
43
+ trail = re.sub(r"[\w'’]+", "", w[-3:]) if w else ""
44
+ p = None
45
+ if any(c in SENT_END for c in trail):
46
+ p = "sent"
47
+ elif any(c in CLAUSE for c in trail):
48
+ p = "clause"
49
+ elif any(c in COMMA for c in trail):
50
+ p = "comma"
51
+ rom = "".join(c[0] for c in chars)
52
+ out.append(dict(word=w, rom=rom if rom else "*", punct=p, chars=chars))
53
+ return out
54
+
55
+
56
+ # ------------------------------------------------------------------ aligner
57
+ class Aligner:
58
+ """torchaudio MMS_FA (CC-BY-NC weights, used only as a data tool/judge) with chunked emissions for long audio."""
59
+
60
+ def __init__(self, device="cuda"):
61
+ import torch, torchaudio
62
+ self.torch = torch
63
+ self.F = torchaudio.functional
64
+ b = torchaudio.pipelines.MMS_FA
65
+ self.model = b.get_model().to(device).eval().half()
66
+ self.dict = b.get_dict()
67
+ self.dev = device
68
+ self.rate = 320 # samples per emission frame at 16 kHz (20 ms)
69
+
70
+ def emission(self, y16, chunk_s=30, ctx_s=2):
71
+ torch = self.torch
72
+ sr = 16000
73
+ x = torch.from_numpy(y16).float().to(self.dev)
74
+ if len(y16) <= (chunk_s + 2 * ctx_s) * sr:
75
+ with torch.inference_mode():
76
+ e, _ = self.model(x[None].half())
77
+ return e[0].float().log_softmax(-1)
78
+ outs = []
79
+ step, ctx = chunk_s * sr, ctx_s * sr
80
+ nfr_total = len(y16) // self.rate
81
+ for s in range(0, len(y16), step):
82
+ a, b = max(0, s - ctx), min(len(y16), s + step + ctx)
83
+ with torch.inference_mode():
84
+ e, _ = self.model(x[a:b][None].half())
85
+ e = e[0].float().log_softmax(-1)
86
+ f0 = (s - a) // self.rate
87
+ nfr = min(step, len(y16) - s) // self.rate
88
+ outs.append(e[f0:f0 + nfr])
89
+ e = torch.cat(outs, 0)
90
+ return e[:max(nfr_total, 1)]
91
+
92
+ def align(self, y16, words):
93
+ """-> per word (start_s, end_s, score) and per char spans [(start_s, end_s)], or None on failure."""
94
+ torch = self.torch
95
+ em = self.emission(y16)
96
+ toks, wid = [], []
97
+ for i, w in enumerate(words):
98
+ for c in w["rom"]:
99
+ if c in self.dict:
100
+ toks.append(self.dict[c]); wid.append(i)
101
+ if not toks or len(toks) * 1 > em.shape[0]:
102
+ return None
103
+ t = torch.tensor([toks], dtype=torch.int32, device=self.dev)
104
+ try:
105
+ ali, sc = self.F.forced_align(em[None], t, blank=0)
106
+ except Exception:
107
+ return None
108
+ ali, sc = ali[0].cpu().numpy(), sc[0].exp().cpu().numpy()
109
+ # token spans (merge repeats; blank=0)
110
+ spans, i, k = [], 0, 0
111
+ T = len(ali)
112
+ while i < T:
113
+ if ali[i] == 0:
114
+ i += 1; continue
115
+ j = i
116
+ while j + 1 < T and ali[j + 1] == ali[i] and not (j + 1 < T and False):
117
+ j += 1
118
+ spans.append((i, j + 1, float(sc[i:j + 1].mean())))
119
+ i = j + 1
120
+ # repeated identical consecutive tokens (e.g. "aa") collapse in the path above only if no blank between;
121
+ # forced_align always inserts a blank between identical tokens, so len(spans)==len(toks)
122
+ if len(spans) != len(toks):
123
+ return None
124
+ r = self.rate / 16000
125
+ W = [[None, None, []] for _ in words]
126
+ chars = [[] for _ in words]
127
+ for (a, b, s), w in zip(spans, wid):
128
+ if W[w][0] is None:
129
+ W[w][0] = a * r
130
+ W[w][1] = b * r; W[w][2].append(s); chars[w].append((a * r, b * r))
131
+ res = []
132
+ for i, (a, b, s) in enumerate(W):
133
+ if a is None:
134
+ return None
135
+ res.append(dict(start=a, end=b, score=float(np.mean(s)), chars=chars[i]))
136
+ return res
137
+
138
+
139
+ # ------------------------------------------------------------------ energy / pauses
140
+ def frame_db(y, sr, hop_s=0.01):
141
+ hop = int(sr * hop_s)
142
+ n = max(1, len(y) // hop)
143
+ fr = y[: n * hop].reshape(n, hop)
144
+ e = 10 * np.log10((fr.astype(np.float64) ** 2).mean(1) + 1e-10)
145
+ return e
146
+
147
+
148
+ def silence_mask(y, sr, rel_db=-40.0, hop_s=0.01):
149
+ e = frame_db(y, sr, hop_s)
150
+ ref = np.percentile(e, 98)
151
+ return e < ref + rel_db
152
+
153
+
154
+ def runs(mask):
155
+ """-> list of (start, end) index runs where mask is True."""
156
+ out, i, n = [], 0, len(mask)
157
+ while i < n:
158
+ if mask[i]:
159
+ j = i
160
+ while j < n and mask[j]:
161
+ j += 1
162
+ out.append((i, j)); i = j
163
+ else:
164
+ i += 1
165
+ return out
166
+
167
+
168
+ PAUSE_TARGET = { # (min, max) silence seconds after a word, by the punctuation that follows it
169
+ "sent": (0.30, 0.60),
170
+ "clause": (0.20, 0.50),
171
+ "comma": (0.00, 0.40),
172
+ None: (0.00, 0.20),
173
+ }
174
+ # profile B (Phase 2 v2): keeps natural phrase breaks where there is no punctuation (the real anchor pauses at
175
+ # ~14% of unpunctuated word boundaries), still removes hesitations; set P2PAUSE=B
176
+ import os as _os
177
+ if _os.environ.get("P2PAUSE") == "B":
178
+ PAUSE_TARGET = {"sent": (0.35, 0.70), "clause": (0.25, 0.55), "comma": (0.00, 0.50), None: (0.00, 0.35)}
179
+ LEAD_S, TAIL_S = 0.12, 0.18
180
+
181
+
182
+ def normalise_pauses(y, sr, words, ali, seg_a, seg_b, sil=None, lo_s=0.0, hi_s=None):
183
+ """Rebuild one segment [word seg_a .. seg_b] of y: edge silence fixed to LEAD_S/TAIL_S, every inter-word
184
+ silence clamped to PAUSE_TARGET[punct] (only truly silent frames are removed; silence is inserted, as a
185
+ copy of the quietest local frames, when a sentence/clause boundary has too little). -> (y_out, pauses)"""
186
+ hop = int(0.01 * sr)
187
+ if sil is None:
188
+ sil = silence_mask(y, sr)
189
+ hi_s = len(y) / sr if hi_s is None else hi_s
190
+ xf = int(0.005 * sr)
191
+ pieces, pauses = [], []
192
+ a0 = ali[seg_a]["start"]
193
+ start = int(max(lo_s, a0 - LEAD_S, 0.0) * sr)
194
+ cur = start
195
+ for i in range(seg_a, seg_b):
196
+ e_i, s_n = ali[i]["end"], ali[i + 1]["start"]
197
+ f_a, f_b = int(e_i / 0.01), int(s_n / 0.01) + 1
198
+ # longest silent run inside the gap (with 20 ms guard to the word edges)
199
+ best = None
200
+ for ra, rb in runs(sil[f_a:f_b]):
201
+ if best is None or rb - ra > best[1] - best[0]:
202
+ best = (ra, rb)
203
+ lo, hi = PAUSE_TARGET[words[i]["punct"]]
204
+ if best is None:
205
+ cur_sil = 0.0; mid = int((e_i + s_n) / 2 * sr)
206
+ else:
207
+ ra, rb = (f_a + best[0]) * hop, (f_a + best[1]) * hop
208
+ cur_sil = (rb - ra) / sr; mid = (ra + rb) // 2
209
+ if cur_sil > hi and best is not None: # shrink: cut the middle of the silent run
210
+ cut = int((cur_sil - hi) * sr)
211
+ c0 = mid - cut // 2
212
+ pieces.append(y[cur:c0]); cur = c0 + cut
213
+ pauses.append(hi)
214
+ elif cur_sil < lo: # insert quiet frames at the quietest point of the gap
215
+ add = int((lo - cur_sil) * sr)
216
+ g0, g1 = int(e_i * sr), max(int(s_n * sr), int(e_i * sr) + hop)
217
+ seg = y[g0:g1]
218
+ if len(seg) >= hop:
219
+ k = int(np.argmin(frame_db(seg, sr))) * hop
220
+ ins_at = g0 + k
221
+ q = y[ins_at:ins_at + hop]
222
+ else:
223
+ ins_at, q = mid, np.zeros(hop, dtype=y.dtype)
224
+ filler = np.tile(q, add // hop + 1)[:add] * 0.5
225
+ pieces.append(y[cur:ins_at]); pieces.append(filler); cur = ins_at
226
+ pauses.append(lo)
227
+ else:
228
+ pauses.append(cur_sil)
229
+ end = min(len(y), int(min(hi_s, ali[seg_b]["end"] + TAIL_S) * sr))
230
+ pieces.append(y[cur:end])
231
+ out = np.concatenate([p for p in pieces if len(p)]) if pieces else y[start:end]
232
+ # pad edges with silence if the source had less than LEAD/TAIL available
233
+ lead_have = a0 - start / sr
234
+ if lead_have < LEAD_S - 0.02:
235
+ out = np.concatenate([np.zeros(int((LEAD_S - lead_have) * sr), out.dtype), out])
236
+ tail_have = end / sr - ali[seg_b]["end"]
237
+ if tail_have < TAIL_S - 0.02:
238
+ out = np.concatenate([out, np.zeros(int((TAIL_S - tail_have) * sr), out.dtype)])
239
+ # tiny fades at the edges
240
+ f = min(xf, len(out) // 4)
241
+ if f > 0:
242
+ out[:f] *= np.linspace(0, 1, f); out[-f:] *= np.linspace(1, 0, f)
243
+ return out.astype(np.float32), pauses
244
+
245
+
246
+ # ------------------------------------------------------------------ metrics on audio
247
+ def pause_stats(y, sr, min_pause=0.15):
248
+ """VAD (energy, -40 dB vs 98th pct) pauses: leading/trailing silence and internal pauses >= min_pause."""
249
+ sil = silence_mask(y, sr)
250
+ R = runs(sil)
251
+ n = len(sil)
252
+ lead = R[0][1] * 0.01 if R and R[0][0] == 0 else 0.0
253
+ tail = (n - R[-1][0]) * 0.01 if R and R[-1][1] == n else 0.0
254
+ internal = [(b - a) * 0.01 for a, b in R if a > 0 and b < n and (b - a) * 0.01 >= min_pause]
255
+ speech = max(1e-3, n * 0.01 - lead - tail)
256
+ return dict(lead=round(lead, 3), tail=round(tail, 3), pauses=[round(p, 3) for p in internal],
257
+ n_pauses=len(internal), pause_rate=round(len(internal) / speech, 4),
258
+ pause_frac=round(sum(internal) / speech, 4), speech_s=round(speech, 3))
259
+
260
+
261
+ def f0_track(y16):
262
+ import pyworld as pw
263
+ x = y16.astype(np.float64)
264
+ f0, t = pw.dio(x, 16000, f0_floor=60, f0_ceil=500, frame_period=10)
265
+ return pw.stonemask(x, f0, t, 16000)
266
+
267
+
268
+ def f0_stats(y16):
269
+ f0 = f0_track(y16)
270
+ v = f0[f0 > 0]
271
+ if len(v) < 20:
272
+ return dict(voiced=float(len(v)), f0_med=None, st_std=None, st_range=None)
273
+ st = 12 * np.log2(v / np.median(v))
274
+ d = np.diff(st)
275
+ return dict(voiced_frac=round(len(v) / len(f0), 3), f0_med=round(float(np.median(v)), 1),
276
+ st_std=round(float(st.std()), 3), st_range=round(float(np.percentile(st, 95) - np.percentile(st, 5)), 3),
277
+ st_vel=round(float(np.abs(d[np.abs(d) < 3]).mean()), 4))
278
+
279
+
280
+ def tone_pairs(y16, words, ali):
281
+ """Yoruba tone-direction accuracy: for consecutive tone-bearing vowels whose tones differ, is the sign of the
282
+ median-F0 change (semitones) the same as the sign of the tone change? -> (n_pairs, n_correct)"""
283
+ f0 = f0_track(y16)
284
+ vals = []
285
+ for w, a in zip(words, ali):
286
+ k = 0
287
+ for (rc, oi, tone), (s, e) in zip([c for c in w["chars"] if c[0] in "abcdefghijklmnopqrstuvwxyz'"], a["chars"]):
288
+ if rc in _VOW or (rc in "nm" and tone is not None):
289
+ t = 1 if tone is None else tone
290
+ fr = f0[int(s / 0.01): int(e / 0.01) + 1]
291
+ fr = fr[fr > 0]
292
+ vals.append((t, float(np.median(12 * np.log2(fr))) if len(fr) else None))
293
+ n = c = 0
294
+ for (t1, f1), (t2, f2) in zip(vals, vals[1:]):
295
+ if t1 == t2 or f1 is None or f2 is None:
296
+ continue
297
+ n += 1
298
+ c += int(np.sign(f2 - f1) == np.sign(t2 - t1))
299
+ return n, c
300
+
301
+
302
+ def wasserstein1(a, b):
303
+ """1-D earth mover's distance between two samples."""
304
+ if not len(a) or not len(b):
305
+ return None
306
+ a, b = np.sort(a), np.sort(b)
307
+ q = np.linspace(0, 1, 101)
308
+ return float(np.mean(np.abs(np.quantile(a, q) - np.quantile(b, q))))
code/da/prep2.py ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Phase 2 data prep (H100 node): every source -> MMS forced alignment -> sentence segmentation of long clips ->
2
+ punctuation-consistent pause normalisation -> 24 kHz int16 + meta. Output ~/data2/<ds>/{pcm24k.i16, meta.jsonl, stats.json}
3
+
4
+ python -m da.prep2 waxal bible_hau bible_yor slr86 slr70 fleurs
5
+ """
6
+ import os, sys, io, json, glob, csv, hashlib, time
7
+ import numpy as np, soundfile as sf, librosa
8
+ from multiprocessing import Pool
9
+ sys.path.insert(0, os.path.expanduser("~/da"))
10
+ from da.p2common import words_for_align, Aligner, normalise_pauses # noqa: E402
11
+
12
+ RAW = "/scratch/raw"
13
+ OUT = os.path.expanduser(os.environ.get("P2OUT", "~/data2"))
14
+ OLD = os.path.expanduser("~/data")
15
+ SR = 24000
16
+ BIBLE_HAU_CAP_H = 30.0
17
+ FLOW_OK = {"waxal", "bible_yor", "bible_hau", "slr86", "slr70"} # 48 kHz studio-ish sources (FLEURS is 16 kHz)
18
+
19
+
20
+ def clean_text(t):
21
+ import unicodedata, re
22
+ t = unicodedata.normalize("NFC", t or "")
23
+ t = t.replace("\u2019", "'").replace("\u2018", "'").replace("\u201c", '"').replace("\u201d", '"')
24
+ return re.sub(r"\s+", " ", t).strip()
25
+
26
+
27
+ def hsplit(i):
28
+ return "val" if int(hashlib.md5(i.encode()).hexdigest(), 16) % 100 == 0 else "train"
29
+
30
+
31
+ # ------------------------------------------------------------------ enumerators (yield meta + loader args)
32
+ def items_waxal():
33
+ import pyarrow.parquet as pq
34
+ for lang in ["yor", "ibo", "hau", "pcm"]:
35
+ files = sorted(glob.glob(f"{RAW}/waxal/data/TTS/{lang}/*.parquet"))
36
+ files = [f for f in files if not os.path.basename(f).startswith("yor_tts")]
37
+ for f in files:
38
+ bn = os.path.basename(f)
39
+ split = "test" if "test" in bn else ("val" if "validation" in bn else "train")
40
+ pf = pq.ParquetFile(f)
41
+ for b in pf.iter_batches(batch_size=8):
42
+ for r in b.to_pylist():
43
+ yield dict(id=f"waxal_{lang}_{r['id']}", ds="waxal", lang=lang,
44
+ spk=f"waxal_{lang}_{r.get('speaker_id') or 'spk'}", gender=r.get("gender") or "",
45
+ split=split, text=r["text"]), ("bytes", r["audio"]["bytes"])
46
+
47
+
48
+ FLEURS = {"hau": "ha_ng", "ibo": "ig_ng", "yor": "yo_ng", "eng": "en_us"}
49
+
50
+
51
+ def items_fleurs():
52
+ """raw FLEURS train audio (16 kHz); speaker ids = Phase 1 ECAPA pseudo-speakers from ~/data/fleurs/meta.jsonl"""
53
+ for l in open(f"{OLD}/fleurs/meta.jsonl", encoding="utf-8"):
54
+ m = json.loads(l)
55
+ fn = m["id"].split("_", 2)[2] + ".wav"
56
+ p = f"{RAW}/fleurs/data/{FLEURS[m['lang']]}/audio/train/{fn}"
57
+ yield dict(id=m["id"], ds="fleurs", lang=m["lang"], spk=m["spk"], gender=m.get("gender", ""),
58
+ split=hsplit(m["id"]), text=m["text"]), ("path", p)
59
+
60
+
61
+ def items_slr(num, lang, prefix):
62
+ for idx in sorted(glob.glob(f"{RAW}/slr{num}/**/line_index.tsv", recursive=True)):
63
+ d = os.path.dirname(idx)
64
+ for line in open(idx, encoding="utf-8"):
65
+ parts = line.rstrip("\n").split("\t")
66
+ if len(parts) < 2:
67
+ continue
68
+ fid, text = parts[0].strip(), parts[-1]
69
+ p = os.path.join(d, fid + ".wav")
70
+ if os.path.exists(p):
71
+ spk = "_".join(fid.split("_")[:2])
72
+ yield dict(id=f"{prefix}_{fid}", ds=prefix, lang=lang, spk=f"{prefix}_{spk}",
73
+ gender="female" if fid[2] == "f" else "male", split=hsplit(fid), text=text), ("path", p)
74
+
75
+
76
+ def items_bible(lang_dir, lang, ds, cap_h=None):
77
+ root = f"{RAW}/bible/{lang_dir}"
78
+ auds = sorted(glob.glob(f"{root}/**/*.flac", recursive=True) + glob.glob(f"{root}/**/*.wav", recursive=True))
79
+ if cap_h: # deterministic random subset of the train split, roughly cap_h hours (verse ~ 9 s on average)
80
+ auds.sort(key=lambda p: hashlib.md5(p.encode()).hexdigest())
81
+ tot = 0.0
82
+ for p in auds:
83
+ txt = os.path.splitext(p)[0] + ".txt"
84
+ if not os.path.exists(txt):
85
+ continue
86
+ split = "test" if "/test/" in p else ("val" if "/dev/" in p else "train")
87
+ if cap_h and split == "train":
88
+ if tot > cap_h * 3600:
89
+ continue
90
+ try:
91
+ tot += sf.info(p).duration
92
+ except Exception:
93
+ continue
94
+ yield dict(id=f"{ds}_{os.path.splitext(os.path.basename(p))[0]}", ds=ds, lang=lang, spk=ds, gender="",
95
+ split=split, text=open(txt, encoding="utf-8").read()), ("path", p)
96
+
97
+
98
+ def enum(ds):
99
+ return {"waxal": items_waxal, "fleurs": items_fleurs,
100
+ "slr86": lambda: items_slr(86, "yor", "slr86"), "slr70": lambda: items_slr(70, "eng", "slr70"),
101
+ "bible_yor": lambda: items_bible("yoruba", "yor", "bible_yor"),
102
+ "bible_hau": lambda: items_bible("hausa", "hau", "bible_hau", BIBLE_HAU_CAP_H)}[ds]()
103
+
104
+
105
+ # ------------------------------------------------------------------ workers
106
+ def load(arg):
107
+ meta, src = arg
108
+ try:
109
+ if src[0] == "bytes":
110
+ y, sr = sf.read(io.BytesIO(src[1]), dtype="float32", always_2d=True); y = y.mean(1)
111
+ elif src[0] == "path":
112
+ y, sr = sf.read(src[1], dtype="float32", always_2d=True); y = y.mean(1)
113
+ else:
114
+ pcm = np.memmap(src[1], dtype=np.int16, mode="r")
115
+ y, sr = pcm[src[2]:src[2] + src[3]].astype(np.float32) / 32768.0, 16000
116
+ meta["text"] = clean_text(meta["text"])
117
+ meta["sr_in"] = int(sr)
118
+ if not meta["text"] or len(y) < 0.5 * sr:
119
+ return meta, None, None, "empty"
120
+ y24 = librosa.resample(y, orig_sr=sr, target_sr=SR, res_type="soxr_hq") if sr != SR else y
121
+ y16 = librosa.resample(y, orig_sr=sr, target_sr=16000, res_type="soxr_hq") if sr != 16000 else y
122
+ return meta, y16, y24, None
123
+ except Exception as e: # noqa
124
+ return meta, None, None, f"err:{type(e).__name__}"
125
+
126
+
127
+ def plan_segments(words, ali, dur):
128
+ """word-index ranges [(a, b)] (inclusive). Short clips: one segment. Long clips: whole sentences grouped
129
+ into 3-14 s segments; an over-long sentence is cut at clause/comma marks."""
130
+ n = len(words)
131
+ if dur <= 20.0:
132
+ return [(0, n - 1)]
133
+ sents, a = [], 0
134
+ for i, w in enumerate(words):
135
+ if w["punct"] == "sent" or i == n - 1:
136
+ sents.append((a, i)); a = i + 1
137
+ pieces = []
138
+ for a, b in sents:
139
+ if ali[b]["end"] - ali[a]["start"] <= 16.0:
140
+ pieces.append((a, b)); continue
141
+ s = a
142
+ for i in range(a, b + 1):
143
+ if (words[i]["punct"] in ("clause", "comma") and ali[i]["end"] - ali[s]["start"] > 3.0) or i == b:
144
+ pieces.append((s, i)); s = i + 1
145
+ segs, cur = [], None
146
+ for a, b in pieces:
147
+ if cur is None:
148
+ cur = [a, b]; continue
149
+ if ali[b]["end"] - ali[cur[0]]["start"] <= 14.0 and ali[cur[1]]["end"] - ali[cur[0]]["start"] < 4.0:
150
+ cur[1] = b
151
+ else:
152
+ segs.append(tuple(cur)); cur = [a, b]
153
+ if cur:
154
+ segs.append(tuple(cur))
155
+ return segs
156
+
157
+
158
+ def post(arg):
159
+ meta, y24, words, ali = arg
160
+ out, why = [], {}
161
+ segs = plan_segments(words, ali, len(y24) / SR)
162
+ from da.p2common import silence_mask
163
+ sil = silence_mask(y24, SR)
164
+ for k, (a, b) in enumerate(segs):
165
+ sc = float(np.mean([ali[i]["score"] for i in range(a, b + 1)]))
166
+ maxw = max(ali[i]["end"] - ali[i]["start"] for i in range(a, b + 1))
167
+ text = " ".join(words[i]["word"] for i in range(a, b + 1))
168
+ if sc < 0.30:
169
+ why["low_score"] = why.get("low_score", 0) + 1; continue
170
+ if maxw > 2.5:
171
+ why["long_word"] = why.get("long_word", 0) + 1; continue
172
+ lo = ali[a - 1]["end"] + 0.02 if a > 0 else 0.0
173
+ hi = ali[b + 1]["start"] - 0.02 if b + 1 < len(ali) else None
174
+ y, pauses = normalise_pauses(y24, SR, words, ali, a, b, sil=sil, lo_s=lo, hi_s=hi)
175
+ dur = len(y) / SR
176
+ cps = len(text) / dur
177
+ if not (0.8 <= dur <= 20.0):
178
+ why["dur"] = why.get("dur", 0) + 1; continue
179
+ if not (4.0 <= cps <= 25.0):
180
+ why["cps"] = why.get("cps", 0) + 1; continue
181
+ rms = np.sqrt((y ** 2).mean() + 1e-12)
182
+ y = y * (0.07 / rms)
183
+ pk = np.abs(y).max()
184
+ if pk > 0.99:
185
+ y = y * (0.99 / pk)
186
+ m = dict(meta)
187
+ if len(segs) > 1:
188
+ m["id"] = f"{meta['id']}_s{k:03d}"; m["src_id"] = meta["id"]; m["longform"] = True
189
+ m.update(text=text, dur=round(dur, 3), ascore=round(sc, 3), cps=round(cps, 2),
190
+ pauses=[round(p, 2) for p in pauses],
191
+ puncts=[words[i]["punct"] for i in range(a, b)], flow_ok=meta["ds"] in FLOW_OK)
192
+ out.append((m, np.clip(y * 32767, -32768, 32767).astype(np.int16)))
193
+ return out, why
194
+
195
+
196
+ def run(ds):
197
+ od = f"{OUT}/{ds}"
198
+ os.makedirs(od, exist_ok=True)
199
+ al = Aligner()
200
+ stats = dict(src_items=0, src_hours=0.0, kept=0, hours=0.0, rej={}, longform_src=0, longform_hours_in=0.0,
201
+ longform_segments=0)
202
+ t0 = time.time(); off = 0
203
+ pending = []
204
+ with open(f"{od}/pcm24k.i16", "wb") as pf, open(f"{od}/meta.jsonl", "w", encoding="utf-8") as jf, \
205
+ Pool(20) as lp, Pool(6) as pp:
206
+ def drain(block=False):
207
+ nonlocal off
208
+ keep = []
209
+ for r in pending:
210
+ if block or r.ready():
211
+ segs, why = r.get()
212
+ for k, v in why.items():
213
+ stats["rej"][k] = stats["rej"].get(k, 0) + v
214
+ for m, pcm in segs:
215
+ m["off24"] = off; m["n24"] = int(len(pcm))
216
+ pf.write(pcm.tobytes()); off += len(pcm)
217
+ jf.write(json.dumps(m, ensure_ascii=False) + "\n")
218
+ stats["kept"] += 1; stats["hours"] += m["dur"] / 3600
219
+ if m.get("longform"):
220
+ stats["longform_segments"] += 1
221
+ else:
222
+ keep.append(r)
223
+ pending[:] = keep
224
+ import itertools
225
+ src = enum(ds)
226
+ if os.environ.get("LIMIT"):
227
+ src = itertools.islice(src, 0, None, int(os.environ.get("STRIDE", "1")))
228
+ src = itertools.islice(src, int(os.environ["LIMIT"]))
229
+ for meta, y16, y24, err in lp.imap(load, src, chunksize=2):
230
+ stats["src_items"] += 1
231
+ if err:
232
+ stats["rej"][err] = stats["rej"].get(err, 0) + 1; continue
233
+ d = len(y24) / SR
234
+ stats["src_hours"] += d / 3600
235
+ if d > 20:
236
+ stats["longform_src"] += 1; stats["longform_hours_in"] += d / 3600
237
+ if d > 2400:
238
+ stats["rej"]["too_long"] = stats["rej"].get("too_long", 0) + 1; continue
239
+ words = words_for_align(meta["text"])
240
+ ali = al.align(y16, words) if words else None
241
+ if ali is None:
242
+ stats["rej"]["align_fail"] = stats["rej"].get("align_fail", 0) + 1; continue
243
+ pending.append(pp.apply_async(post, ((meta, y24, words, ali),)))
244
+ if len(pending) > 64:
245
+ drain(block=False)
246
+ while len(pending) > 128:
247
+ time.sleep(0.05); drain(block=False)
248
+ if stats["src_items"] % 2000 == 0:
249
+ print(ds, stats["src_items"], "kept", stats["kept"], "h", round(stats["hours"], 2),
250
+ "rej", stats["rej"], round(time.time() - t0), "s", flush=True)
251
+ drain(block=True)
252
+ stats = {k: (round(v, 3) if isinstance(v, float) else v) for k, v in stats.items()}
253
+ json.dump(stats, open(f"{od}/stats.json", "w"), indent=1)
254
+ print(ds, "PREP2_DONE", json.dumps(stats), round(time.time() - t0), "s", flush=True)
255
+
256
+
257
+ if __name__ == "__main__":
258
+ for ds in sys.argv[1:]:
259
+ run(ds)
code/da/s3_2.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Phase 2: S3 speech tokens (speech_tokenizer_v2.onnx, CUDA EP) + CAM++ speaker embeddings for ~/data2/<ds>,
2
+ plus code-switched training pairs (ds "cs_pairs").
3
+
4
+ python -m da.s3_2 tok waxal fleurs ... -> s3tok.i32, s3tok.jsonl, campplus.npy
5
+ python -m da.s3_2 cs 4000 -> ~/data2/cs_pairs/{pcm24k.i16, meta.jsonl} (+ run tok on it)
6
+ """
7
+ import os, sys, json, random, hashlib
8
+ import numpy as np, librosa, torch
9
+ from multiprocessing import Pool
10
+
11
+ D2 = os.path.expanduser(os.environ.get("P2OUT", "~/data2"))
12
+ MD = os.path.expanduser("~/models/CosyVoice2-0.5B")
13
+ _S = {}
14
+ _FB = torch.from_numpy(librosa.filters.mel(sr=16000, n_fft=400, n_mels=128)).float()
15
+ _WIN = torch.hann_window(400)
16
+
17
+
18
+ def log_mel_128(a):
19
+ st = torch.stft(a, 400, 160, window=_WIN, return_complex=True)
20
+ mag = st[..., :-1].abs() ** 2
21
+ lm = torch.clamp(_FB @ mag, min=1e-10).log10()
22
+ lm = torch.maximum(lm, lm.max() - 8.0)
23
+ return (lm + 4.0) / 4.0
24
+
25
+
26
+ def sess(name, gpu):
27
+ if name not in _S:
28
+ import onnxruntime as ort
29
+ torch.set_num_threads(1)
30
+ o = ort.SessionOptions(); o.intra_op_num_threads = 2; o.inter_op_num_threads = 1
31
+ prov = ["CUDAExecutionProvider", "CPUExecutionProvider"] if gpu else ["CPUExecutionProvider"]
32
+ _S[name] = ort.InferenceSession(f"{MD}/{name}", sess_options=o, providers=prov)
33
+ return _S[name]
34
+
35
+
36
+ def work(args):
37
+ path, off, n = args
38
+ import torchaudio.compliance.kaldi as kaldi
39
+ pcm = np.memmap(path, dtype=np.int16, mode="r")
40
+ y24 = pcm[off:off + n].astype(np.float32) / 32768.0
41
+ y16 = librosa.resample(y24, orig_sr=24000, target_sr=16000, res_type="soxr_hq")
42
+ a = torch.from_numpy(y16)
43
+ f = log_mel_128(a)[None]
44
+ s = sess("speech_tokenizer_v2.onnx", True)
45
+ ins = s.get_inputs()
46
+ tok = s.run(None, {ins[0].name: f.numpy(), ins[1].name: np.array([f.shape[2]], dtype=np.int32)})[0]
47
+ fb = kaldi.fbank(a[None], num_mel_bins=80, dither=0, sample_frequency=16000)
48
+ fb = fb - fb.mean(0, keepdim=True)
49
+ c = sess("campplus.onnx", False)
50
+ emb = c.run(None, {c.get_inputs()[0].name: fb[None].numpy()})[0].reshape(-1).astype(np.float32)
51
+ return np.asarray(tok).reshape(-1).astype(np.int32), emb
52
+
53
+
54
+ def tok(ds):
55
+ od = f"{D2}/{ds}"
56
+ meta = [json.loads(l) for l in open(f"{od}/meta.jsonl", encoding="utf-8")]
57
+ jobs = [(f"{od}/pcm24k.i16", m["off24"], m["n24"]) for m in meta]
58
+ toff, embs = 0, []
59
+ with open(f"{od}/s3tok.i32", "wb") as tf, open(f"{od}/s3tok.jsonl", "w") as jf, Pool(12) as pool:
60
+ for i, (m, (t, e)) in enumerate(zip(meta, pool.imap(work, jobs, chunksize=4))):
61
+ tf.write(t.tobytes()); embs.append(e)
62
+ jf.write(json.dumps({"id": m["id"], "toff": toff, "ntok": int(len(t))}) + "\n")
63
+ toff += len(t)
64
+ if i % 5000 == 0:
65
+ print(ds, i, len(meta), "tok/s", round(len(t) / max(m["dur"], 1e-3), 1), flush=True)
66
+ np.save(f"{od}/campplus.npy", np.stack(embs))
67
+ print(ds, "S3_DONE", toff, flush=True)
68
+
69
+
70
+ def build_cs(n_pairs=4000, seed=0):
71
+ """Join two train utterances in different languages (prefer same gender) into one code-switched item.
72
+ Junction silence follows the first clip's final punctuation (sentence 0.45 s, comma 0.25 s, else 0.20 s)."""
73
+ rng = random.Random(seed)
74
+ pools = {}
75
+ for ds in ["waxal", "slr70", "slr86", "fleurs"]:
76
+ p = f"{D2}/{ds}/meta.jsonl"
77
+ if not os.path.exists(p):
78
+ continue
79
+ for l in open(p, encoding="utf-8"):
80
+ m = json.loads(l)
81
+ if m["split"] != "train" or not (1.0 <= m["dur"] <= 6.5):
82
+ continue
83
+ if ds == "fleurs" and m["lang"] != "eng":
84
+ continue
85
+ pools.setdefault(m["lang"], []).append(m)
86
+ langs = sorted(pools)
87
+ print("cs pools", {l: len(v) for l, v in pools.items()})
88
+ od = f"{D2}/cs_pairs"; os.makedirs(od, exist_ok=True)
89
+ mm = {}
90
+ off = 0
91
+ with open(f"{od}/pcm24k.i16", "wb") as pf, open(f"{od}/meta.jsonl", "w", encoding="utf-8") as jf:
92
+ for k in range(n_pairs):
93
+ l1, l2 = rng.sample(langs, 2)
94
+ if rng.random() < 0.5 and "eng" in langs and "eng" not in (l1, l2):
95
+ l2 = "eng" # English is the most common partner in real Nigerian code-switching
96
+ a = rng.choice(pools[l1])
97
+ g = (a.get("gender") or "").lower()[:1]
98
+ cand = [b for b in rng.sample(pools[l2], min(50, len(pools[l2]))) if (b.get("gender") or "").lower()[:1] == g]
99
+ b = cand[0] if cand else rng.choice(pools[l2])
100
+ ys = []
101
+ for m in (a, b):
102
+ if m["ds"] not in mm:
103
+ mm[m["ds"]] = np.memmap(f"{D2}/{m['ds']}/pcm24k.i16", dtype=np.int16, mode="r")
104
+ ys.append(np.asarray(mm[m["ds"]][m["off24"]:m["off24"] + m["n24"]]))
105
+ last = a["text"].rstrip()[-1:]
106
+ junc = 0.45 if last in ".?!" else (0.25 if last in ",;:" else 0.20)
107
+ have = 0.18 + 0.12
108
+ y1 = ys[0]
109
+ if junc < have:
110
+ y1 = y1[: len(y1) - int((have - junc) * 24000)]
111
+ gap = np.zeros(0, np.int16)
112
+ else:
113
+ gap = np.zeros(int((junc - have) * 24000), np.int16)
114
+ y = np.concatenate([y1, gap, ys[1]])
115
+ text = f"<{l1}>{a['text']}</{l1}> <{l2}>{b['text']}</{l2}>"
116
+ m = dict(id=f"cs_{k:05d}", ds="cs_pairs", lang="cs", langs=[l1, l2], spk=a["spk"], gender=a.get("gender", ""),
117
+ split="train", text=text, dur=round(len(y) / 24000, 3), off24=off, n24=int(len(y)),
118
+ src=[a["id"], b["id"]], flow_ok=False)
119
+ pf.write(y.tobytes()); off += len(y)
120
+ jf.write(json.dumps(m, ensure_ascii=False) + "\n")
121
+ print("cs pairs", n_pairs, "hours", round(off / 24000 / 3600, 2), flush=True)
122
+
123
+
124
+ if __name__ == "__main__":
125
+ if sys.argv[1] == "tok":
126
+ for ds in sys.argv[2:]:
127
+ tok(ds)
128
+ elif sys.argv[1] == "cs":
129
+ build_cs(int(sys.argv[2]))
130
+ tok("cs_pairs")
code/da/text.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Text front-end: Unicode NFC, tone diacritics preserved, UTF-8 byte tokens with per-token language ids.
2
+
3
+ Code-switched input uses inline tags: "<yor>Mo fẹ́ lọ sí ọjà</yor> <eng>to buy some bread</eng>"
4
+ Untagged text takes the default language. Each byte inherits the language of the span it is in.
5
+ """
6
+ import re
7
+ import unicodedata
8
+
9
+ LANGS = ["eng", "ibo", "hau", "yor", "pcm"]
10
+ LANG2ID = {l: i for i, l in enumerate(LANGS)}
11
+ PAD, BOS, EOS = 0, 1, 2 # byte b -> token b + 3
12
+ VOCAB = 256 + 3
13
+ _TAG = re.compile(r"<(eng|ibo|hau|yor|pcm)>(.*?)</\1>", re.S)
14
+
15
+
16
+ def normalize(t: str) -> str:
17
+ t = unicodedata.normalize("NFC", t)
18
+ t = t.replace("\u2019", "'").replace("\u2018", "'").replace("\u201c", '"').replace("\u201d", '"')
19
+ return re.sub(r"\s+", " ", t).strip()
20
+
21
+
22
+ def segments(text: str, default_lang: str = "eng"):
23
+ """-> list of (lang, normalized_text) spans, order preserved."""
24
+ out, pos = [], 0
25
+ for m in _TAG.finditer(text):
26
+ pre = text[pos:m.start()]
27
+ if pre.strip():
28
+ out.append((default_lang, pre))
29
+ out.append((m.group(1), m.group(2)))
30
+ pos = m.end()
31
+ if text[pos:].strip():
32
+ out.append((default_lang, text[pos:]))
33
+ # keep a single space between spans
34
+ res = []
35
+ for i, (l, s) in enumerate(out):
36
+ s = normalize(s)
37
+ if not s:
38
+ continue
39
+ if res:
40
+ s = " " + s
41
+ res.append((l, s))
42
+ return res
43
+
44
+
45
+ def encode(text: str, default_lang: str = "eng"):
46
+ """-> (token ids, language ids) incl. BOS/EOS. BOS takes the first span's language, EOS the last."""
47
+ segs = segments(text, default_lang)
48
+ toks, langs = [BOS], [LANG2ID[segs[0][0]] if segs else LANG2ID[default_lang]]
49
+ for l, s in segs:
50
+ b = s.encode("utf-8")
51
+ toks += [x + 3 for x in b]
52
+ langs += [LANG2ID[l]] * len(b)
53
+ toks.append(EOS)
54
+ langs.append(langs[-1])
55
+ return toks, langs
56
+
57
+
58
+ def plain(text: str) -> str:
59
+ """Tagged text -> plain text (for ASR references)."""
60
+ return " ".join(s.strip() for _, s in segments(text))
code/train_b.py ADDED
@@ -0,0 +1,353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Arm B: CosyVoice2-0.5B (Apache-2.0) fine-tune with the Zhang et al. 2019 recipe added to its text->speech-token LM.
2
+
3
+ Additions (new, ours):
4
+ * per-token language embedding added to the Qwen text-token embeddings (zero-init)
5
+ * learned speaker embedding inserted as one token AFTER the text, before the task token (zero-init), so text
6
+ positions cannot see it; speech positions attend to it (anchor-voice conditioning)
7
+ * domain-adversarial speaker classifier with gradient reversal on the LM hidden states at text positions
8
+ Flow-matching decoder + HiFT vocoder stay frozen; at inference they are prompted with an anchor-voice clip.
9
+
10
+ python train_b.py train --out ~/runs/b1 --minutes 30
11
+ python train_b.py synth --ckpt ~/runs/b1/last.pt --out ~/eval/sys/armB_ft
12
+ python train_b.py zeroshot --out ~/eval/sys/cosyvoice2_zeroshot (pretrained model, cross-lingual, no fine-tune)
13
+ """
14
+ import argparse, inspect, json, math, os, random, sys, time
15
+ import numpy as np, torch, torch.nn as nn, torch.nn.functional as F, soundfile as sf, librosa
16
+
17
+ HOME = os.path.expanduser("~")
18
+ sys.path[:0] = [f"{HOME}/CosyVoice", f"{HOME}/CosyVoice/third_party/Matcha-TTS"]
19
+ MODEL_DIR = f"{HOME}/models/CosyVoice2-0.5B"
20
+ LANGS = ["eng", "ibo", "hau", "yor", "pcm"]
21
+ L2I = {l: i for i, l in enumerate(LANGS)}
22
+ sys.path.insert(0, f"{HOME}/da")
23
+ from da.text import segments # noqa: E402
24
+
25
+ from da.model import grad_reverse # noqa: E402
26
+
27
+
28
+ def _sf_load_wav(wav, target_sr, min_sr=16000):
29
+ """soundfile replacement for cosyvoice load_wav (torchaudio>=2.9 load needs torchcodec)."""
30
+ y, sr = sf.read(wav, dtype="float32", always_2d=True)
31
+ y = y.mean(1)
32
+ if sr != target_sr:
33
+ y = librosa.resample(y, orig_sr=sr, target_sr=target_sr)
34
+ return torch.from_numpy(y)[None]
35
+
36
+
37
+ def load_cosy():
38
+ import cosyvoice.utils.file_utils as fu
39
+ fu.load_wav = _sf_load_wav
40
+ import cosyvoice.cli.frontend as cf
41
+ cf.load_wav = _sf_load_wav
42
+ from cosyvoice.cli.cosyvoice import CosyVoice2
43
+ kw = dict(load_jit=False, load_trt=False, fp16=False)
44
+ sig = inspect.signature(CosyVoice2.__init__).parameters
45
+ kw = {k: v for k, v in kw.items() if k in sig}
46
+ return CosyVoice2(MODEL_DIR, **kw)
47
+
48
+
49
+ def text_tokens(tok, text, default="eng"):
50
+ """tagged text -> (qwen ids, per-token lang ids)."""
51
+ ids, langs = [], []
52
+ for l, s in segments(text, default):
53
+ t = tok(s, add_special_tokens=False)["input_ids"]
54
+ ids += t; langs += [L2I[l]] * len(t)
55
+ return ids, langs
56
+
57
+
58
+ class DALM(nn.Module):
59
+ def __init__(self, qlm, n_spk, d=896):
60
+ super().__init__()
61
+ self.q = qlm # cosyvoice Qwen2LM
62
+ self.lang_emb = nn.Embedding(len(LANGS), d); nn.init.zeros_(self.lang_emb.weight)
63
+ self.spk_emb = nn.Embedding(n_spk, d); nn.init.normal_(self.spk_emb.weight, std=0.02)
64
+ self.adv = nn.Sequential(nn.Linear(d, 256), nn.ReLU(), nn.Linear(256, n_spk))
65
+ self.V = qlm.speech_token_size
66
+
67
+ def embed_text(self, ids, langs):
68
+ return self.q.llm.model.model.embed_tokens(ids) + self.lang_emb(langs)
69
+
70
+ def build(self, ids, langs, spk, speech=None):
71
+ """one sequence: [sos, text+lang, spk, task, speech...]"""
72
+ sos = self.q.llm_embedding.weight[self.q.sos][None]
73
+ task = self.q.llm_embedding.weight[self.q.task_id][None]
74
+ parts = [sos, self.embed_text(ids, langs), self.spk_emb(spk)[None], task]
75
+ if speech is not None:
76
+ parts.append(self.q.speech_embedding(speech))
77
+ return torch.cat(parts, 0)
78
+
79
+ def hidden(self, x, mask):
80
+ out = self.q.llm.model.model(inputs_embeds=x, attention_mask=mask, return_dict=True)
81
+ return out.last_hidden_state
82
+
83
+ def forward(self, batch, adv_lambda=1.0):
84
+ seqs, tgts, tpos = [], [], []
85
+ for ids, langs, spk, sp in batch:
86
+ x = self.build(ids, langs, spk, sp)
87
+ n_text = len(ids)
88
+ tgt = torch.full((x.shape[0],), -100, device=x.device, dtype=torch.long)
89
+ # position of task token = 1 + n_text + 1; it predicts speech[0]; last speech pos predicts EOS
90
+ start = 1 + n_text + 1
91
+ tgt[start: start + len(sp)] = sp
92
+ tgt[start + len(sp)] = self.V # EOS
93
+ seqs.append(x); tgts.append(tgt); tpos.append((1, 1 + n_text))
94
+ T = max(s.shape[0] for s in seqs) + 1
95
+ D = seqs[0].shape[1]
96
+ X = seqs[0].new_zeros(len(seqs), T, D)
97
+ Y = torch.full((len(seqs), T), -100, device=X.device, dtype=torch.long)
98
+ M = torch.zeros(len(seqs), T, device=X.device, dtype=torch.long)
99
+ for i, (s, t) in enumerate(zip(seqs, tgts)):
100
+ X[i, :len(s)] = s; Y[i, :len(t)] = t[:len(s)] if len(t) > len(s) else t; M[i, :len(s)] = 1
101
+ # targets were laid out on positions; shift is implicit (position p predicts tgt[p])
102
+ H = self.hidden(X, M)
103
+ logits = self.q.llm_decoder(H)
104
+ ce = F.cross_entropy(logits.float().reshape(-1, logits.shape[-1]), Y.reshape(-1), ignore_index=-100)
105
+ valid = Y != -100
106
+ acc = (logits.argmax(-1)[valid] == Y[valid]).float().mean()
107
+ # adversarial speaker classifier on text positions
108
+ hs, ys = [], []
109
+ for i, (a, b) in enumerate(tpos):
110
+ hs.append(H[i, a:b]); ys.append(torch.full((b - a,), int(batch[i][2]), device=X.device))
111
+ hs = torch.cat(hs); ys = torch.cat(ys)
112
+ al = self.adv(grad_reverse(hs.float(), adv_lambda))
113
+ adv = F.cross_entropy(al, ys)
114
+ adv_acc = (al.argmax(-1) == ys).float().mean()
115
+ return ce, acc, adv, adv_acc
116
+
117
+ @torch.no_grad()
118
+ def generate(self, ids, langs, spk, top_p=0.8, top_k=25, win=10, tau_r=0.1, seed=0):
119
+ g = torch.Generator(device=ids.device).manual_seed(seed)
120
+ x = self.build(ids, langs, spk)[None]
121
+ n = len(ids)
122
+ min_len, max_len = int(n * 2), int(n * 20)
123
+ past, out = None, []
124
+ inp = x
125
+ for i in range(max_len):
126
+ o = self.q.llm.model.model(inputs_embeds=inp, past_key_values=past, use_cache=True, return_dict=True)
127
+ past = o.past_key_values
128
+ logp = self.q.llm_decoder(o.last_hidden_state[:, -1]).float().log_softmax(-1)[0]
129
+ if i < min_len:
130
+ logp[self.V] = -float("inf")
131
+ logp[self.V + 1:] = -float("inf")
132
+ p = logp.exp()
133
+ sp, si = p.sort(descending=True)
134
+ keep = (sp.cumsum(0) - sp) < top_p
135
+ keep[top_k:] = False
136
+ cand_p, cand_i = sp[keep], si[keep]
137
+ tokid = int(cand_i[torch.multinomial(cand_p / cand_p.sum(), 1, generator=g)])
138
+ if out and sum(1 for t in out[-win:] if t == tokid) >= win * tau_r and tokid != self.V: # RAS
139
+ tokid = int(torch.multinomial(p, 1, generator=g))
140
+ if tokid >= self.V:
141
+ break
142
+ out.append(tokid)
143
+ inp = self.q.speech_embedding.weight[tokid][None, None]
144
+ return out
145
+
146
+
147
+ # ------------------------------------------------------------------ data
148
+ def load_items(root, datasets, split, max_tok=500):
149
+ items, mm = [], {}
150
+ for ds in datasets:
151
+ if not os.path.exists(f"{root}/{ds}/s3tok.jsonl"):
152
+ continue
153
+ mm[ds] = np.memmap(f"{root}/{ds}/s3tok.i32", dtype=np.int32, mode="r")
154
+ tk = {}
155
+ for l in open(f"{root}/{ds}/s3tok.jsonl"):
156
+ r = json.loads(l); tk[r["id"]] = (r["toff"], r["ntok"])
157
+ for l in open(f"{root}/{ds}/meta.jsonl", encoding="utf-8"):
158
+ m = json.loads(l)
159
+ if m["split"] != split or m["id"] not in tk:
160
+ continue
161
+ m["toff"], m["ntok"] = tk[m["id"]]
162
+ if 5 < m["ntok"] <= max_tok:
163
+ items.append(m)
164
+ return items, mm
165
+
166
+
167
+ def batches(items, budget, weights, rng):
168
+ idx = []
169
+ for i, m in enumerate(items):
170
+ w = weights.get(m["ds"], 1.0)
171
+ idx += [i] * int(w) + ([i] if rng.random() < w - int(w) else [])
172
+ idx.sort(key=lambda i: items[i]["ntok"] + rng.random() * 10)
173
+ out, cur, mx = [], [], 0
174
+ for i in idx:
175
+ L = items[i]["ntok"] + len(items[i]["text"]) // 2 + 4
176
+ if cur and max(mx, L) * (len(cur) + 1) > budget:
177
+ out.append(cur); cur, mx = [], 0
178
+ cur.append(i); mx = max(mx, L)
179
+ if cur:
180
+ out.append(cur)
181
+ rng.shuffle(out)
182
+ return out
183
+
184
+
185
+ def train(a):
186
+ from transformers import AutoTokenizer
187
+ tok = AutoTokenizer.from_pretrained(f"{MODEL_DIR}/CosyVoice-BlankEN")
188
+ cosy = load_cosy()
189
+ items, mm = load_items(a.data, a.datasets.split(","), "train")
190
+ val, _ = load_items(a.data, a.datasets.split(","), "val")
191
+ spks = sorted({m["spk"] for m in items}); spk2id = {s: i for i, s in enumerate(spks)}
192
+ val = [m for m in val if m["spk"] in spk2id][:200]
193
+ os.makedirs(a.out, exist_ok=True)
194
+ json.dump(spk2id, open(f"{a.out}/spk2id.json", "w"))
195
+ print("armB items", len(items), "hours", round(sum(m["dur"] for m in items) / 3600, 1), "spk", len(spks), flush=True)
196
+ model = DALM(cosy.model.llm, len(spks)).cuda().train()
197
+ for p in cosy.model.flow.parameters():
198
+ p.requires_grad = False
199
+ for p in cosy.model.hift.parameters():
200
+ p.requires_grad = False
201
+ new = list(model.lang_emb.parameters()) + list(model.spk_emb.parameters()) + list(model.adv.parameters())
202
+ newids = {id(p) for p in new}
203
+ old = [p for p in model.parameters() if id(p) not in newids and p.requires_grad]
204
+ opt = torch.optim.AdamW([{"params": old, "lr": a.lr}, {"params": new, "lr": a.lr_new}], weight_decay=0.01)
205
+ base = [a.lr, a.lr_new]
206
+ cache = {}
207
+
208
+ def enc(m):
209
+ if m["id"] not in cache:
210
+ cache[m["id"]] = text_tokens(tok, f"<{m['lang']}>{m['text']}</{m['lang']}>")
211
+ ids, langs = cache[m["id"]]
212
+ sp = np.asarray(mm[m["ds"]][m["toff"]: m["toff"] + m["ntok"]], dtype=np.int64)
213
+ return (torch.tensor(ids).cuda(), torch.tensor(langs).cuda(), torch.tensor(spk2id[m["spk"]]).cuda(),
214
+ torch.from_numpy(sp).cuda())
215
+
216
+ rng = random.Random(0); step = 0; nskip = 0; t0 = time.time(); logf = open(f"{a.out}/log.jsonl", "a")
217
+
218
+ def validate():
219
+ model.eval(); L = []
220
+ with torch.no_grad(), torch.autocast("cuda", dtype=torch.bfloat16):
221
+ for i in range(0, len(val), 16):
222
+ ce, acc, _, _ = model([enc(m) for m in val[i:i + 16]]); L.append(float(ce))
223
+ model.train(); return float(np.mean(L))
224
+
225
+ v0 = validate(); print(json.dumps({"step": 0, "val_ce": v0}), flush=True); logf.write(json.dumps({"step": 0, "val_ce": v0}) + "\n")
226
+ done = False
227
+ while not done:
228
+ for bi in batches(items, a.tokens, {"waxal": a.waxal_w}, rng):
229
+ lrm = min(1.0, (step + 1) / 200)
230
+ for g, b in zip(opt.param_groups, base):
231
+ g["lr"] = b * lrm
232
+ with torch.autocast("cuda", dtype=torch.bfloat16):
233
+ ce, acc, adv, adv_acc = model([enc(items[i]) for i in bi])
234
+ loss = ce + a.adv_w * adv
235
+ opt.zero_grad(set_to_none=True)
236
+ if not torch.isfinite(loss):
237
+ nskip += 1
238
+ print(json.dumps({"skip_nonfinite_fwd": step, "ce": float(ce), "adv": float(adv), "B": len(bi),
239
+ "maxtok": max(items[i]["ntok"] for i in bi)}), flush=True)
240
+ continue
241
+ loss.backward()
242
+ gn = torch.nn.utils.clip_grad_norm_([p for p in model.parameters() if p.requires_grad], 1.0)
243
+ if not torch.isfinite(gn):
244
+ nskip += 1; opt.zero_grad(set_to_none=True)
245
+ print(json.dumps({"skip_nonfinite_grad": step}), flush=True); continue
246
+ opt.step(); step += 1
247
+ if step % 25 == 0:
248
+ r = dict(step=step, ce=round(float(ce), 4), acc=round(float(acc), 4), adv=round(float(adv), 3),
249
+ adv_acc=round(float(adv_acc), 3), gn=round(float(gn), 2), B=len(bi), min=round((time.time() - t0) / 60, 2))
250
+ print(json.dumps(r), flush=True); logf.write(json.dumps(r) + "\n"); logf.flush()
251
+ if step % 500 == 0:
252
+ v = validate(); print(json.dumps({"step": step, "val_ce": v}), flush=True)
253
+ logf.write(json.dumps({"step": step, "val_ce": v}) + "\n"); logf.flush()
254
+ if (time.time() - t0) / 60 > a.minutes:
255
+ done = True; break
256
+ v = validate(); logf.write(json.dumps({"step": step, "val_ce": v, "final": True}) + "\n")
257
+ sd = {k: v for k, v in model.state_dict().items()}
258
+ torch.save(dict(model=sd, step=step, n_spk=len(spks)), f"{a.out}/last.pt")
259
+ print("TRAIN_B_DONE", step, v, flush=True)
260
+
261
+
262
+ # ------------------------------------------------------------------ inference
263
+ def prompt_feats(cosy, wav_path):
264
+ """flow prompt (speech tokens, 24k mel feat, campplus embedding) from an anchor clip."""
265
+ fe = cosy.frontend
266
+ tokn, tokl = fe._extract_speech_token(wav_path)
267
+ feat, featl = fe._extract_speech_feat(wav_path)
268
+ # CosyVoice2 aligns: feat frames = 2 * tokens
269
+ n = min(feat.shape[1] // 2, tokn.shape[1])
270
+ feat, tokn = feat[:, :2 * n], tokn[:, :n]
271
+ emb = fe._extract_spk_embedding(wav_path)
272
+ return tokn.cuda(), feat.cuda(), emb.cuda()
273
+
274
+
275
+ @torch.no_grad()
276
+ def token2wav(cosy, tokens, ptok, pfeat, emb):
277
+ flow = cosy.model.flow
278
+ t = torch.tensor([tokens], dtype=torch.int32).cuda()
279
+ kw = dict(token=t, token_len=torch.tensor([t.shape[1]], dtype=torch.int32).cuda(),
280
+ prompt_token=ptok, prompt_token_len=torch.tensor([ptok.shape[1]], dtype=torch.int32).cuda(),
281
+ prompt_feat=pfeat, prompt_feat_len=torch.tensor([pfeat.shape[1]], dtype=torch.int32).cuda(),
282
+ embedding=emb)
283
+ sig = inspect.signature(flow.inference).parameters
284
+ if "streaming" in sig:
285
+ kw["streaming"] = False
286
+ if "finalize" in sig:
287
+ kw["finalize"] = True
288
+ if "flow_cache" in sig:
289
+ kw["flow_cache"] = torch.zeros(1, 80, 0, 2).cuda()
290
+ mel = flow.inference(**kw)
291
+ mel = mel[0] if isinstance(mel, tuple) else mel
292
+ hs = inspect.signature(cosy.model.hift.inference).parameters
293
+ w = cosy.model.hift.inference(speech_feat=mel, **({"cache_source": torch.zeros(1, 1, 0).cuda()} if "cache_source" in hs else {}))
294
+ w = w[0] if isinstance(w, tuple) else w
295
+ return w.squeeze().float().cpu().numpy()
296
+
297
+
298
+ def eval_rows():
299
+ return [json.loads(l) for l in open(f"{HOME}/eval/eval_set.jsonl", encoding="utf-8")]
300
+
301
+
302
+ def synth(a):
303
+ from transformers import AutoTokenizer
304
+ tok = AutoTokenizer.from_pretrained(f"{MODEL_DIR}/CosyVoice-BlankEN")
305
+ cosy = load_cosy()
306
+ ck = torch.load(a.ckpt, map_location="cuda")
307
+ spk2id = json.load(open(os.path.join(os.path.dirname(a.ckpt), "spk2id.json")))
308
+ model = DALM(cosy.model.llm, ck["n_spk"]).cuda().eval()
309
+ model.load_state_dict(ck["model"])
310
+ anc = json.load(open(f"{HOME}/eval/anchor.json"))
311
+ ptok, pfeat, emb = prompt_feats(cosy, anc["ref_wavs"][0])
312
+ os.makedirs(a.out, exist_ok=True)
313
+ for r in eval_rows():
314
+ text = r["text"] if r["lang"] == "cs" else f"<{r['lang']}>{r['text']}</{r['lang']}>"
315
+ ids, langs = text_tokens(tok, text)
316
+ with torch.autocast("cuda", dtype=torch.bfloat16):
317
+ sp = model.generate(torch.tensor(ids).cuda(), torch.tensor(langs).cuda(), torch.tensor(spk2id[anc["spk"]]).cuda())
318
+ sf.write(f"{a.out}/{r['id']}.wav", token2wav(cosy, sp, ptok, pfeat, emb), cosy.sample_rate)
319
+ print("synth_b done", a.out, flush=True)
320
+
321
+
322
+ def zeroshot(a):
323
+ """Unmodified pretrained CosyVoice2, cross-lingual mode with the anchor prompt (reference point)."""
324
+ cosy = load_cosy()
325
+ anc = json.load(open(f"{HOME}/eval/anchor.json"))
326
+ from cosyvoice.utils.file_utils import load_wav
327
+ pw = anc["ref_wavs"][0]
328
+ sig = inspect.signature(cosy.inference_cross_lingual).parameters
329
+ os.makedirs(a.out, exist_ok=True)
330
+ for r in eval_rows():
331
+ text = " ".join(s.strip() for _, s in segments(r["text"])) if r["lang"] == "cs" else r["text"]
332
+ prompt = pw
333
+ outs = [o["tts_speech"] for o in cosy.inference_cross_lingual(text, prompt, stream=False)]
334
+ w = torch.cat(outs, 1).squeeze().cpu().numpy()
335
+ sf.write(f"{a.out}/{r['id']}.wav", w, cosy.sample_rate)
336
+ print("zeroshot done", flush=True)
337
+
338
+
339
+ if __name__ == "__main__":
340
+ ap = argparse.ArgumentParser()
341
+ ap.add_argument("cmd")
342
+ ap.add_argument("--data", default=f"{HOME}/data")
343
+ ap.add_argument("--datasets", default="waxal,fleurs,slr86,slr70,bible_yor")
344
+ ap.add_argument("--out", default=f"{HOME}/runs/b1")
345
+ ap.add_argument("--ckpt", default="")
346
+ ap.add_argument("--minutes", type=float, default=30)
347
+ ap.add_argument("--lr", type=float, default=1e-5)
348
+ ap.add_argument("--lr_new", type=float, default=1e-3)
349
+ ap.add_argument("--tokens", type=int, default=12000)
350
+ ap.add_argument("--waxal_w", type=float, default=2.0)
351
+ ap.add_argument("--adv_w", type=float, default=0.02)
352
+ a = ap.parse_args()
353
+ {"train": train, "synth": synth, "zeroshot": zeroshot}[a.cmd](a)
code/train_b2.py ADDED
@@ -0,0 +1,590 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Arm B, Phase 2. CosyVoice2-0.5B (Apache-2.0) + language embeddings + speaker token + GRL speaker classifier.
2
+
3
+ New in Phase 2
4
+ * data from ~/data2 (24 kHz, forced-aligned, pause-normalised; long WAXAL clips segmented; BibleTTS Hausa; CS pairs)
5
+ * in-context prompts during LM training (p=0.5): [sos, prompt_text+text, spk, task, prompt_speech, speech] with
6
+ loss on the target speech only -> at inference the anchor clip steers prosody and pause style
7
+ * keep-best on validation CE (every --val_every steps), cosine LR, early stop
8
+ * flow-matching decoder fine-tune on 24 kHz studio sources (tones / pitch / timing detail)
9
+ * synthesis: anchor prompt at LM and flow, sentence-level generation for multi-sentence text with anchor-matched
10
+ inter-sentence pauses, N candidates per text (seed + prompt clip vary)
11
+
12
+ python train_b2.py lm --out ~/runs/p2_lm --minutes 90
13
+ python train_b2.py flow --out ~/runs/p2_flow --minutes 60
14
+ python train_b2.py synth --ckpt ~/runs/p2_lm/best.pt [--flow ~/runs/p2_flow/best.pt] --out DIR --n 4
15
+ """
16
+ import argparse, inspect, json, math, os, random, re, sys, time
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
+ sys.path[:0] = [f"{HOME}/CosyVoice", f"{HOME}/CosyVoice/third_party/Matcha-TTS", f"{HOME}/da"]
21
+ from train_b import load_cosy, text_tokens, DALM, LANGS, L2I, MODEL_DIR, token2wav # noqa: E402
22
+ from da.model import grad_reverse # noqa: E402
23
+ from da.text import segments # noqa: E402
24
+
25
+ D2 = os.path.expanduser(os.environ.get("P2OUT", f"{HOME}/data2"))
26
+ ANCHOR = "waxal_yor_4"
27
+
28
+
29
+ # ------------------------------------------------------------------ model
30
+ class DALM2(DALM):
31
+ def build2(self, ids, langs, spk, sp=None, pids=None, plangs=None, psp=None):
32
+ """[sos, (prompt_text) text, spk, task, (prompt_speech) speech] -> embeddings, n_text_total, n_prompt_speech"""
33
+ sos = self.q.llm_embedding.weight[self.q.sos][None]
34
+ task = self.q.llm_embedding.weight[self.q.task_id][None]
35
+ if pids is not None:
36
+ ids = torch.cat([pids, ids]); langs = torch.cat([plangs, langs])
37
+ parts = [sos, self.embed_text(ids, langs), self.spk_emb(spk)[None], task]
38
+ npsp = 0
39
+ if psp is not None:
40
+ parts.append(self.q.speech_embedding(psp)); npsp = len(psp)
41
+ if sp is not None:
42
+ parts.append(self.q.speech_embedding(sp))
43
+ return torch.cat(parts, 0), len(ids), npsp
44
+
45
+ def forward2(self, batch, adv_lambda=1.0):
46
+ seqs, tgts, tpos, spks = [], [], [], []
47
+ for b in batch:
48
+ ids, langs, spk, sp = b[:4]
49
+ pr = b[4] if len(b) > 4 else None
50
+ x, nt, npsp = self.build2(ids, langs, spk, sp, *(pr if pr else (None, None, None)))
51
+ tgt = torch.full((x.shape[0] + 1,), -100, device=x.device, dtype=torch.long)
52
+ start = 1 + nt + 2 + npsp - 1 # position whose output predicts speech[0]
53
+ tgt[start: start + len(sp)] = sp
54
+ tgt[start + len(sp)] = self.V
55
+ seqs.append(x); tgts.append(tgt[:x.shape[0]]); tpos.append((1, 1 + nt)); spks.append(int(spk))
56
+ T = max(s.shape[0] for s in seqs)
57
+ D = seqs[0].shape[1]
58
+ X = seqs[0].new_zeros(len(seqs), T, D)
59
+ Y = torch.full((len(seqs), T), -100, device=X.device, dtype=torch.long)
60
+ M = torch.zeros(len(seqs), T, device=X.device, dtype=torch.long)
61
+ for i, (s, t) in enumerate(zip(seqs, tgts)):
62
+ X[i, :len(s)] = s; Y[i, :len(t)] = t; M[i, :len(s)] = 1
63
+ H = self.hidden(X, M)
64
+ logits = self.q.llm_decoder(H)
65
+ ce = F.cross_entropy(logits.float().reshape(-1, logits.shape[-1]), Y.reshape(-1), ignore_index=-100)
66
+ valid = Y != -100
67
+ acc = (logits.argmax(-1)[valid] == Y[valid]).float().mean()
68
+ hs, ys = [], []
69
+ for i, (a, b) in enumerate(tpos):
70
+ hs.append(H[i, a:b]); ys.append(torch.full((b - a,), spks[i], device=X.device))
71
+ hs = torch.cat(hs); ys = torch.cat(ys)
72
+ al = self.adv(grad_reverse(hs.float(), adv_lambda))
73
+ adv = F.cross_entropy(al, ys)
74
+ return ce, acc, adv, (al.argmax(-1) == ys).float().mean()
75
+
76
+ @torch.no_grad()
77
+ def generate2(self, ids, langs, spk, prompt=None, top_p=0.8, top_k=25, win=10, tau_r=0.1, seed=0,
78
+ min_ratio=2.0, max_ratio=20.0):
79
+ g = torch.Generator(device=ids.device).manual_seed(seed)
80
+ x, _, _ = self.build2(ids, langs, spk, None, *(prompt if prompt else (None, None, None)))
81
+ x = x[None]
82
+ n = len(ids)
83
+ min_len, max_len = int(n * min_ratio), int(n * max_ratio)
84
+ past, out, inp = None, [], x
85
+ for i in range(max_len):
86
+ o = self.q.llm.model.model(inputs_embeds=inp, past_key_values=past, use_cache=True, return_dict=True)
87
+ past = o.past_key_values
88
+ logp = self.q.llm_decoder(o.last_hidden_state[:, -1]).float().log_softmax(-1)[0]
89
+ if i < min_len:
90
+ logp[self.V] = -float("inf")
91
+ logp[self.V + 1:] = -float("inf")
92
+ p = logp.exp()
93
+ sp, si = p.sort(descending=True)
94
+ keep = (sp.cumsum(0) - sp) < top_p
95
+ keep[top_k:] = False
96
+ cp, ci = sp[keep], si[keep]
97
+ tokid = int(ci[torch.multinomial(cp / cp.sum(), 1, generator=g)])
98
+ if out and sum(1 for t in out[-win:] if t == tokid) >= win * tau_r and tokid != self.V:
99
+ tokid = int(torch.multinomial(p, 1, generator=g))
100
+ if tokid >= self.V:
101
+ break
102
+ out.append(tokid)
103
+ inp = self.q.speech_embedding.weight[tokid][None, None]
104
+ return out
105
+
106
+
107
+ # ------------------------------------------------------------------ data
108
+ def load_items(datasets, split, max_tok=500, flow=False):
109
+ items, mm = [], {}
110
+ for ds in datasets:
111
+ od = f"{D2}/{ds}"
112
+ if not os.path.exists(f"{od}/s3tok.jsonl"):
113
+ print("missing", ds, flush=True); continue
114
+ mm[ds] = np.memmap(f"{od}/s3tok.i32", dtype=np.int32, mode="r")
115
+ tk = [json.loads(l) for l in open(f"{od}/s3tok.jsonl")]
116
+ for k, l in enumerate(open(f"{od}/meta.jsonl", encoding="utf-8")):
117
+ m = json.loads(l)
118
+ t = tk[k]
119
+ assert t["id"] == m["id"]
120
+ if m["split"] != split:
121
+ continue
122
+ if flow and not m.get("flow_ok"):
123
+ continue
124
+ m["toff"], m["ntok"], m["row"] = t["toff"], t["ntok"], k
125
+ if 5 < m["ntok"] <= max_tok:
126
+ items.append(m)
127
+ return items, mm
128
+
129
+
130
+ def tagged(m):
131
+ return m["text"] if m["lang"] == "cs" else f"<{m['lang']}>{m['text']}</{m['lang']}>"
132
+
133
+
134
+ DS_W = {"waxal": 2.0, "bible_yor": 0.5, "bible_hau": 0.6, "fleurs": 1.0, "slr86": 1.0, "slr70": 1.0, "cs_pairs": 1.0}
135
+
136
+
137
+ def batches(items, budget, rng, extra_w=None, cost=None):
138
+ idx = []
139
+ for i, m in enumerate(items):
140
+ w = DS_W.get(m["ds"], 1.0) * (extra_w(m) if extra_w else 1.0)
141
+ idx += [i] * int(w) + ([i] if rng.random() < w - int(w) else [])
142
+ cost = cost or (lambda m: m["ntok"] + len(m["text"]) // 2 + 4)
143
+ idx.sort(key=lambda i: cost(items[i]) + rng.random() * 10)
144
+ out, cur, mx = [], [], 0
145
+ for i in idx:
146
+ L = cost(items[i])
147
+ if cur and max(mx, L) * (len(cur) + 1) > budget:
148
+ out.append(cur); cur, mx = [], 0
149
+ cur.append(i); mx = max(mx, L)
150
+ if cur:
151
+ out.append(cur)
152
+ rng.shuffle(out)
153
+ return out
154
+
155
+
156
+ # ------------------------------------------------------------------ LM training
157
+ def train_lm(a):
158
+ from transformers import AutoTokenizer
159
+ tok = AutoTokenizer.from_pretrained(f"{MODEL_DIR}/CosyVoice-BlankEN")
160
+ cosy = load_cosy()
161
+ dss = a.datasets.split(",")
162
+ items, mm = load_items(dss, "train")
163
+ val_all, _ = load_items(dss, "val")
164
+ if a.anchor_only_w > 0:
165
+ print("anchor items", sum(m["spk"] == ANCHOR for m in items), flush=True)
166
+ spks = sorted({m["spk"] for m in items})
167
+ if a.init:
168
+ ck = torch.load(a.init, map_location="cpu")
169
+ spk2id = json.load(open(os.path.join(os.path.dirname(a.init), "spk2id.json")))
170
+ for s in spks:
171
+ if s not in spk2id:
172
+ spk2id[s] = len(spk2id)
173
+ else:
174
+ spk2id = {s: i for i, s in enumerate(spks)}
175
+ n_spk = max(len(spk2id), ck["n_spk"]) if a.init else len(spk2id)
176
+ # balanced val: up to 60 per language (+ anchor), never more than 15 per speaker
177
+ rng = random.Random(1); rng.shuffle(val_all)
178
+ val, per_l, per_s = [], {}, {}
179
+ for m in val_all:
180
+ if m["spk"] not in spk2id or m["lang"] == "cs":
181
+ continue
182
+ L = "anchor" if m["spk"] == ANCHOR else m["lang"]
183
+ if per_l.get(L, 0) >= 60 or per_s.get(m["spk"], 0) >= 15:
184
+ continue
185
+ per_l[L] = per_l.get(L, 0) + 1; per_s[m["spk"]] = per_s.get(m["spk"], 0) + 1; val.append(m)
186
+ by_spk = {}
187
+ for m in items:
188
+ if m["ntok"] <= 160 and m["lang"] != "cs":
189
+ by_spk.setdefault(m["spk"], []).append(m)
190
+ os.makedirs(a.out, exist_ok=True)
191
+ json.dump(spk2id, open(f"{a.out}/spk2id.json", "w"))
192
+ print("lm items", len(items), "hours", round(sum(m["dur"] for m in items) / 3600, 1), "spk", len(spk2id),
193
+ "val", len(val), per_l, flush=True)
194
+ model = DALM2(cosy.model.llm, n_spk).cuda().train()
195
+ if a.init:
196
+ sd = ck["model"]
197
+ if sd["spk_emb.weight"].shape[0] < n_spk:
198
+ for k in ["spk_emb.weight"]:
199
+ w = model.state_dict()[k].clone(); w[:sd[k].shape[0]] = sd[k]; sd[k] = w
200
+ for k in ["adv.2.weight", "adv.2.bias"]:
201
+ w = model.state_dict()[k].clone(); w[:sd[k].shape[0]] = sd[k]; sd[k] = w
202
+ print(model.load_state_dict(sd, strict=False), flush=True)
203
+ for p in cosy.model.flow.parameters():
204
+ p.requires_grad = False
205
+ for p in cosy.model.hift.parameters():
206
+ p.requires_grad = False
207
+ new = list(model.lang_emb.parameters()) + list(model.spk_emb.parameters()) + list(model.adv.parameters())
208
+ newids = {id(p) for p in new}
209
+ old = [p for p in model.parameters() if id(p) not in newids and p.requires_grad]
210
+ opt = torch.optim.AdamW([{"params": old, "lr": a.lr}, {"params": new, "lr": a.lr_new}], weight_decay=0.01)
211
+ base = [a.lr, a.lr_new]
212
+ cache = {}
213
+
214
+ def tt(text, key):
215
+ if key not in cache:
216
+ cache[key] = text_tokens(tok, text)
217
+ return cache[key]
218
+
219
+ def enc(m, prng=None, force_prompt=None):
220
+ ids, langs = tt(tagged(m), m["id"])
221
+ sp = np.asarray(mm[m["ds"]][m["toff"]: m["toff"] + m["ntok"]], dtype=np.int64)
222
+ r = [torch.tensor(ids).cuda(), torch.tensor(langs).cuda(), torch.tensor(spk2id[m["spk"]]).cuda(),
223
+ torch.from_numpy(sp).cuda()]
224
+ use = force_prompt if force_prompt is not None else (prng is not None and prng.random() < a.p_prompt)
225
+ if use:
226
+ c = by_spk.get(m["spk"], [])
227
+ c = [x for x in (prng or random).sample(c, min(6, len(c))) if x["id"] != m["id"]] if c else []
228
+ if c and c[0]["ntok"] + m["ntok"] <= 600:
229
+ p = c[0]
230
+ pids, plangs = tt(tagged(p), p["id"])
231
+ psp = np.asarray(mm[p["ds"]][p["toff"]: p["toff"] + p["ntok"]], dtype=np.int64)
232
+ r.append((torch.tensor(pids).cuda(), torch.tensor(plangs).cuda(), torch.from_numpy(psp).cuda()))
233
+ return r
234
+
235
+ def validate():
236
+ model.eval(); out = {}
237
+ with torch.no_grad(), torch.autocast("cuda", dtype=torch.bfloat16):
238
+ for mode in (False, True):
239
+ vr = random.Random(7); L, W = [], []
240
+ for i in range(0, len(val), 16):
241
+ bt = val[i:i + 16]
242
+ ce, _, _, _ = model.forward2([enc(m, vr, force_prompt=mode) for m in bt])
243
+ L.append(float(ce) * len(bt)); W.append(len(bt))
244
+ out["p" if mode else "np"] = sum(L) / sum(W)
245
+ # anchor-only val
246
+ av = [m for m in val if m["spk"] == ANCHOR]
247
+ if av:
248
+ vr = random.Random(7)
249
+ ce, _, _, _ = model.forward2([enc(m, vr, force_prompt=True) for m in av[:32]])
250
+ out["anchor_p"] = float(ce)
251
+ model.train()
252
+ out["mean"] = (out["np"] + out["p"]) / 2
253
+ return out
254
+
255
+ extra = (lambda m: a.anchor_only_w if m["spk"] == ANCHOR else 1.0) if a.anchor_only_w > 0 else None
256
+ rng = random.Random(0); prng = random.Random(3)
257
+ step, nskip, t0 = 0, 0, time.time()
258
+ logf = open(f"{a.out}/log.jsonl", "a")
259
+ v = validate(); best = v["mean"] if not a.select_anchor else 0.5 * (v["mean"] + v.get("anchor_p", v["mean"])); bad = 0
260
+ logf.write(json.dumps({"step": 0, "val": v}) + "\n"); print(json.dumps({"step": 0, "val": v}), flush=True)
261
+ torch.save(dict(model=model.state_dict(), step=0, n_spk=n_spk, val=v), f"{a.out}/best.pt")
262
+ total = a.max_steps
263
+ done = False
264
+ while not done:
265
+ for bi in batches(items, a.tokens, rng, extra, cost=lambda m: m["ntok"] + len(m["text"]) // 2 + 4 + int(a.p_prompt * 200)):
266
+ lrm = min(1.0, (step + 1) / a.warmup) * (0.1 + 0.9 * 0.5 * (1 + math.cos(math.pi * min(1.0, step / total))))
267
+ for g, b in zip(opt.param_groups, base):
268
+ g["lr"] = b * lrm
269
+ batch = [enc(items[i], prng) for i in bi]
270
+ # keep the padded length inside the budget when prompts were added
271
+ with torch.autocast("cuda", dtype=torch.bfloat16):
272
+ ce, acc, adv, adv_acc = model.forward2(batch, adv_lambda=min(1.0, step / 1000))
273
+ loss = ce + a.adv_w * adv
274
+ opt.zero_grad(set_to_none=True)
275
+ if not torch.isfinite(loss):
276
+ nskip += 1; print(json.dumps({"skip_nonfinite_fwd": step, "ce": float(ce), "adv": float(adv)}), flush=True)
277
+ continue
278
+ loss.backward()
279
+ gn = torch.nn.utils.clip_grad_norm_([p for p in model.parameters() if p.requires_grad], 1.0)
280
+ if not torch.isfinite(gn):
281
+ nskip += 1; opt.zero_grad(set_to_none=True); print(json.dumps({"skip_nonfinite_grad": step}), flush=True)
282
+ continue
283
+ opt.step(); step += 1
284
+ if step % 25 == 0:
285
+ r = dict(step=step, ce=round(float(ce), 4), acc=round(float(acc), 4), adv=round(float(adv), 3),
286
+ adv_acc=round(float(adv_acc), 3), gn=round(float(gn), 2), B=len(bi), lr=round(lrm * a.lr, 9),
287
+ skips=nskip, min=round((time.time() - t0) / 60, 2))
288
+ print(json.dumps(r), flush=True); logf.write(json.dumps(r) + "\n"); logf.flush()
289
+ if step % a.val_every == 0:
290
+ v = validate()
291
+ score = v["mean"] if not a.select_anchor else 0.5 * (v["mean"] + v.get("anchor_p", v["mean"]))
292
+ imp = score < best - 1e-4
293
+ rec = {"step": step, "val": v, "best": imp}
294
+ print(json.dumps(rec), flush=True); logf.write(json.dumps(rec) + "\n"); logf.flush()
295
+ if imp:
296
+ best, bad = score, 0
297
+ torch.save(dict(model=model.state_dict(), step=step, n_spk=n_spk, val=v), f"{a.out}/best.pt")
298
+ else:
299
+ bad += 1
300
+ if bad >= a.patience:
301
+ print("EARLY_STOP", step, flush=True); done = True; break
302
+ if (time.time() - t0) / 60 > a.minutes or step >= total:
303
+ done = True; break
304
+ print("TRAIN_LM_DONE", step, "best", best, "skips", nskip, flush=True)
305
+
306
+
307
+ # ------------------------------------------------------------------ flow training
308
+ def train_flow(a):
309
+ cosy = load_cosy()
310
+ flow = cosy.model.flow
311
+ if a.flow_init:
312
+ flow.load_state_dict(torch.load(a.flow_init, map_location="cuda")["flow"])
313
+ flow.train()
314
+ for p in cosy.model.llm.parameters():
315
+ p.requires_grad = False
316
+ fe = cosy.frontend.feat_extractor
317
+ dss = a.datasets.split(",")
318
+ items, mm = load_items(dss, "train", max_tok=400, flow=True)
319
+ val, _ = load_items(dss, "val", max_tok=400, flow=True)
320
+ rng0 = random.Random(1); rng0.shuffle(val); val = val[:256]
321
+ pcm = {ds: np.memmap(f"{D2}/{ds}/pcm24k.i16", dtype=np.int16, mode="r") for ds in dss if os.path.exists(f"{D2}/{ds}/pcm24k.i16")}
322
+ emb = {ds: np.load(f"{D2}/{ds}/campplus.npy", mmap_mode="r") for ds in dss if os.path.exists(f"{D2}/{ds}/campplus.npy")}
323
+ os.makedirs(a.out, exist_ok=True)
324
+ print("flow items", len(items), "hours", round(sum(m["dur"] for m in items) / 3600, 1), "val", len(val), flush=True)
325
+ extra = (lambda m: a.anchor_only_w if m["spk"] == ANCHOR else 1.0) if a.anchor_only_w > 0 else None
326
+
327
+ def make(bt):
328
+ toks, feats, es = [], [], []
329
+ for m in bt:
330
+ y = torch.from_numpy(np.asarray(pcm[m["ds"]][m["off24"]: m["off24"] + m["n24"]], dtype=np.float32) / 32768.0)
331
+ n = int(math.ceil(len(y) / 960)) * 960
332
+ y = F.pad(y, (0, n - len(y)))[None].cuda()
333
+ ft = fe(y).squeeze(0).transpose(0, 1) # [T,80]
334
+ tk = torch.from_numpy(np.asarray(mm[m["ds"]][m["toff"]: m["toff"] + m["ntok"]], dtype=np.int64))
335
+ k = min(ft.shape[0] // 2, len(tk))
336
+ toks.append(tk[:k]); feats.append(ft[:2 * k]); es.append(torch.from_numpy(np.array(emb[m["ds"]][m["row"]])))
337
+ tl = torch.tensor([len(t) for t in toks], dtype=torch.int32)
338
+ fl = torch.tensor([len(f) for f in feats], dtype=torch.int32)
339
+ T = int(tl.max()); Fm = int(fl.max())
340
+ tok = torch.zeros(len(bt), T, dtype=torch.long); feat = torch.zeros(len(bt), Fm, 80)
341
+ for i, (t, f) in enumerate(zip(toks, feats)):
342
+ tok[i, :len(t)] = t; feat[i, :len(f)] = f.cpu()
343
+ return dict(speech_token=tok, speech_token_len=tl, speech_feat=feat, speech_feat_len=fl,
344
+ embedding=torch.stack(es).float())
345
+
346
+ params = [p for p in flow.parameters() if p.requires_grad]
347
+ opt = torch.optim.AdamW(params, lr=a.lr, weight_decay=0.0)
348
+ logf = open(f"{a.out}/log.jsonl", "a")
349
+
350
+ def validate():
351
+ flow.eval(); L = []
352
+ st = torch.random.get_rng_state(); cst = torch.cuda.get_rng_state(); pst = random.getstate()
353
+ torch.manual_seed(0); random.seed(0)
354
+ with torch.no_grad():
355
+ for i in range(0, len(val), 16):
356
+ L.append(float(flow(make(val[i:i + 16]), torch.device("cuda"))["loss"]))
357
+ torch.random.set_rng_state(st); torch.cuda.set_rng_state(cst); random.setstate(pst)
358
+ flow.train(); return float(np.mean(L))
359
+
360
+ v = validate(); best, bad = v, 0
361
+ print(json.dumps({"step": 0, "val": v}), flush=True); logf.write(json.dumps({"step": 0, "val": v}) + "\n")
362
+ torch.save(dict(flow=flow.state_dict(), step=0, val=v), f"{a.out}/best.pt")
363
+ rng = random.Random(0); step, t0, done = 0, time.time(), False
364
+ while not done:
365
+ for bi in batches(items, a.tokens, rng, extra, cost=lambda m: m["ntok"]):
366
+ lrm = min(1.0, (step + 1) / a.warmup)
367
+ for g in opt.param_groups:
368
+ g["lr"] = a.lr * lrm
369
+ with torch.autocast("cuda", dtype=torch.bfloat16):
370
+ loss = flow(make([items[i] for i in bi]), torch.device("cuda"))["loss"]
371
+ opt.zero_grad(set_to_none=True)
372
+ if not torch.isfinite(loss):
373
+ continue
374
+ loss.backward()
375
+ gn = torch.nn.utils.clip_grad_norm_(params, 5.0)
376
+ if not torch.isfinite(gn):
377
+ continue
378
+ opt.step(); step += 1
379
+ if step % 50 == 0:
380
+ r = dict(step=step, loss=round(float(loss), 4), gn=round(float(gn), 2), B=len(bi), min=round((time.time() - t0) / 60, 2))
381
+ print(json.dumps(r), flush=True); logf.write(json.dumps(r) + "\n"); logf.flush()
382
+ if step % a.val_every == 0:
383
+ v = validate(); imp = v < best - 1e-4
384
+ print(json.dumps({"step": step, "val": v, "best": imp}), flush=True)
385
+ logf.write(json.dumps({"step": step, "val": v, "best": imp}) + "\n"); logf.flush()
386
+ if imp:
387
+ best, bad = v, 0
388
+ torch.save(dict(flow=flow.state_dict(), step=step, val=v), f"{a.out}/best.pt")
389
+ else:
390
+ bad += 1
391
+ if bad >= a.patience:
392
+ done = True; break
393
+ if (time.time() - t0) / 60 > a.minutes or step >= a.max_steps:
394
+ done = True; break
395
+ print("TRAIN_FLOW_DONE", step, "best", best, flush=True)
396
+
397
+
398
+ # ------------------------------------------------------------------ synthesis
399
+ _SENT = re.compile(r"(?<=[.?!])\s+")
400
+
401
+
402
+ def split_sentences(text):
403
+ """tagged text -> list of tagged sentences. A sentence ends at . ? ! inside a span or at the end of a span;
404
+ language spans are cut there and re-tagged, so a sentence keeps its in-sentence switches."""
405
+ out, cur = [], []
406
+ for l, s in segments(text):
407
+ parts = _SENT.split(s.strip())
408
+ for k, p in enumerate(parts):
409
+ p = p.strip()
410
+ if p:
411
+ cur.append(f"<{l}>{p}</{l}>")
412
+ if (k < len(parts) - 1 or p[-1:] in ".?!") and cur:
413
+ out.append(" ".join(cur)); cur = []
414
+ if cur:
415
+ out.append(" ".join(cur))
416
+ return [o for o in out if o.strip()]
417
+
418
+
419
+ def trim_edges(w, sr, keep_lead=0.12, keep_tail=0.18):
420
+ from da.p2common import silence_mask
421
+ sil = silence_mask(w, sr)
422
+ sp = np.where(~sil)[0]
423
+ if not len(sp):
424
+ return w
425
+ a, b = sp[0] * 0.01, (sp[-1] + 1) * 0.01
426
+ out = w[int(max(0, a - keep_lead) * sr): int(min(len(w) / sr, b + keep_tail) * sr)]
427
+ lead = a - max(0, a - keep_lead)
428
+ if lead < keep_lead:
429
+ out = np.concatenate([np.zeros(int((keep_lead - lead) * sr), out.dtype), out])
430
+ return out
431
+
432
+
433
+ def cap_internal(w, sr, cap=0.6):
434
+ """shorten every internal silence longer than cap seconds to cap (the centre of the silent run is removed)."""
435
+ from da.p2common import silence_mask, runs
436
+ sil = silence_mask(w, sr)
437
+ hop = int(0.01 * sr)
438
+ R = [(a, b) for a, b in runs(sil) if a > 0 and b < len(sil) and (b - a) * 0.01 > cap]
439
+ if not R:
440
+ return w
441
+ keep, cur = [], 0
442
+ for a, b in R:
443
+ cut0 = a * hop + int(cap / 2 * sr)
444
+ cut1 = b * hop - int(cap / 2 * sr)
445
+ keep.append(w[cur:cut0]); cur = cut1
446
+ keep.append(w[cur:])
447
+ return np.concatenate(keep)
448
+
449
+
450
+ class Synth:
451
+ def __init__(self, ckpt, flow_ckpt=None, sent_pause=0.5):
452
+ from transformers import AutoTokenizer
453
+ self.tok = AutoTokenizer.from_pretrained(f"{MODEL_DIR}/CosyVoice-BlankEN")
454
+ self.cosy = load_cosy()
455
+ if flow_ckpt:
456
+ self.cosy.model.flow.load_state_dict(torch.load(flow_ckpt, map_location="cuda")["flow"])
457
+ ck = torch.load(ckpt, map_location="cuda")
458
+ self.spk2id = json.load(open(os.path.join(os.path.dirname(ckpt), "spk2id.json")))
459
+ self.m = DALM2(self.cosy.model.llm, ck["n_spk"]).cuda().eval()
460
+ self.m.load_state_dict(ck["model"])
461
+ self.sent_pause = sent_pause
462
+ self.cap = 0.6
463
+ # anchor prompts: normalised anchor val clips from data2 (never in train), 3-7 s, highest SNR first
464
+ meta = [json.loads(l) for l in open(f"{D2}/waxal/meta.jsonl", encoding="utf-8")]
465
+ tk = [json.loads(l) for l in open(f"{D2}/waxal/s3tok.jsonl")]
466
+ pcm = np.memmap(f"{D2}/waxal/pcm24k.i16", dtype=np.int16, mode="r")
467
+ s3 = np.memmap(f"{D2}/waxal/s3tok.i32", dtype=np.int32, mode="r")
468
+ cand = [(m, t) for m, t in zip(meta, tk) if m["spk"] == ANCHOR and m["split"] == "val" and 3.0 <= m["dur"] <= 7.0
469
+ and not m.get("longform")]
470
+ cand.sort(key=lambda x: -x[0]["ascore"])
471
+ self.prompts = []
472
+ os.makedirs(f"{HOME}/eval/prompts", exist_ok=True)
473
+ for m, t in cand[:4]:
474
+ y = np.asarray(pcm[m["off24"]: m["off24"] + m["n24"]], dtype=np.float32) / 32768.0
475
+ p = f"{HOME}/eval/prompts/{m['id']}.wav"; sf.write(p, y, 24000)
476
+ from train_b import prompt_feats
477
+ ptok, pfeat, pemb = prompt_feats(self.cosy, p)
478
+ ids, langs = text_tokens(self.tok, tagged(m))
479
+ lmp = (torch.tensor(ids).cuda(), torch.tensor(langs).cuda(),
480
+ torch.from_numpy(np.asarray(s3[t["toff"]: t["toff"] + t["ntok"]], dtype=np.int64)).cuda())
481
+ self.prompts.append(dict(id=m["id"], text=m["text"], lm=lmp, flow=(ptok, pfeat, pemb)))
482
+ print("anchor prompts", [p["id"] for p in self.prompts], flush=True)
483
+
484
+ def one(self, tagged_text, seed=0, prompt_k=0, use_lm_prompt=True):
485
+ ids, langs = text_tokens(self.tok, tagged_text)
486
+ P = self.prompts[prompt_k % len(self.prompts)]
487
+ with torch.autocast("cuda", dtype=torch.bfloat16):
488
+ sp = self.m.generate2(torch.tensor(ids).cuda(), torch.tensor(langs).cuda(),
489
+ torch.tensor(self.spk2id[ANCHOR]).cuda(), prompt=P["lm"] if use_lm_prompt else None,
490
+ seed=seed)
491
+ if len(sp) < 3:
492
+ sp = sp + [0] * 3
493
+ self.last_tokens.append(sp)
494
+ return token2wav(self.cosy, sp, *P["flow"])
495
+
496
+ def join(self, ws):
497
+ sr = self.cosy.sample_rate
498
+ if len(ws) == 1:
499
+ return ws[0]
500
+ out = []
501
+ for i, w in enumerate(ws):
502
+ if i:
503
+ out.append(np.zeros(max(0, int((self.sent_pause - 0.30) * sr)), np.float32))
504
+ out.append(w)
505
+ return np.concatenate(out)
506
+
507
+ def redecode(self, toks, prompt_k=0):
508
+ """same LM tokens, (possibly different) flow -> identical post-processing"""
509
+ sr = self.cosy.sample_rate
510
+ P = self.prompts[prompt_k % len(self.prompts)]
511
+ return self.join([cap_internal(trim_edges(token2wav(self.cosy, t, *P["flow"]), sr), sr, self.cap) for t in toks])
512
+
513
+ def say(self, tagged_text, seed=0, prompt_k=0, stitch=True, use_lm_prompt=True):
514
+ sr = self.cosy.sample_rate
515
+ self.last_tokens = []
516
+ sents = split_sentences(tagged_text) if stitch else [tagged_text]
517
+ ws = [cap_internal(trim_edges(self.one(s, seed=seed + 1000 * i, prompt_k=prompt_k, use_lm_prompt=use_lm_prompt), sr), sr, self.cap)
518
+ for i, s in enumerate(sents)]
519
+ # trailing 0.18 + leading 0.12 already present; join() tops up to the anchor's sentence pause
520
+ return self.join(ws)
521
+
522
+
523
+ def redecode(a):
524
+ """decode the LM tokens saved by synth (--src DIR) with another flow checkpoint -> --out"""
525
+ S = Synth(a.ckpt, a.flow or None, a.sent_pause)
526
+ os.makedirs(a.out, exist_ok=True)
527
+ import glob
528
+ for f in sorted(glob.glob(f"{a.src}/*.tok.json")):
529
+ d = json.load(open(f))
530
+ i = os.path.basename(f)[:-9]
531
+ sf.write(f"{a.out}/{i}.wav", S.redecode(d["tokens"], d["prompt_k"]), S.cosy.sample_rate)
532
+ print("REDECODE_DONE", a.out, flush=True)
533
+
534
+
535
+ def synth(a):
536
+ S = Synth(a.ckpt, a.flow or None, a.sent_pause)
537
+ rows = [json.loads(l) for l in open(a.eval_set, encoding="utf-8")]
538
+ if a.only:
539
+ keep = set(a.only.split(",")); rows = [r for r in rows if r["id"] in keep or r["lang"] in keep]
540
+ if a.nper:
541
+ cnt, sel = {}, []
542
+ for r in rows:
543
+ if r["lang"] in ("cs", "para") or cnt.get(r["lang"], 0) < a.nper:
544
+ sel.append(r); cnt[r["lang"]] = cnt.get(r["lang"], 0) + 1
545
+ rows = sel
546
+ ks = [a.k] if a.k >= 0 else list(range(a.n))
547
+ for k in ks:
548
+ od = a.out if len(ks) == 1 else f"{a.out}/cand{k}"
549
+ os.makedirs(od, exist_ok=True)
550
+ for r in rows:
551
+ text = r["text"] if r["lang"] in ("cs", "para") else f"<{r['lang']}>{r['text']}</{r['lang']}>"
552
+ w = S.say(text, seed=k, prompt_k=k, stitch=not a.no_stitch, use_lm_prompt=not a.no_lm_prompt)
553
+ sf.write(f"{od}/{r['id']}.wav", w, S.cosy.sample_rate)
554
+ json.dump(dict(tokens=S.last_tokens, prompt_k=k), open(f"{od}/{r['id']}.tok.json", "w"))
555
+ print("synth cand", k, "done", od, flush=True)
556
+ print("SYNTH2_DONE", a.out, flush=True)
557
+
558
+
559
+ if __name__ == "__main__":
560
+ ap = argparse.ArgumentParser()
561
+ ap.add_argument("cmd")
562
+ ap.add_argument("--datasets", default="waxal,fleurs,slr86,slr70,bible_yor,bible_hau,cs_pairs")
563
+ ap.add_argument("--out", default=f"{HOME}/runs/p2_lm")
564
+ ap.add_argument("--init", default="")
565
+ ap.add_argument("--flow_init", default="")
566
+ ap.add_argument("--ckpt", default="")
567
+ ap.add_argument("--flow", default="")
568
+ ap.add_argument("--minutes", type=float, default=90)
569
+ ap.add_argument("--max_steps", type=int, default=4000)
570
+ ap.add_argument("--lr", type=float, default=1e-5)
571
+ ap.add_argument("--lr_new", type=float, default=5e-4)
572
+ ap.add_argument("--warmup", type=int, default=300)
573
+ ap.add_argument("--tokens", type=int, default=16000)
574
+ ap.add_argument("--adv_w", type=float, default=0.02)
575
+ ap.add_argument("--p_prompt", type=float, default=0.5)
576
+ ap.add_argument("--val_every", type=int, default=200)
577
+ ap.add_argument("--patience", type=int, default=5)
578
+ ap.add_argument("--anchor_only_w", type=float, default=0.0)
579
+ ap.add_argument("--select_anchor", action="store_true")
580
+ ap.add_argument("--eval_set", default=f"{HOME}/eval/eval_set2.jsonl")
581
+ ap.add_argument("--n", type=int, default=1)
582
+ ap.add_argument("--nper", type=int, default=0)
583
+ ap.add_argument("--k", type=int, default=-1)
584
+ ap.add_argument("--src", default="")
585
+ ap.add_argument("--only", default="")
586
+ ap.add_argument("--sent_pause", type=float, default=0.6)
587
+ ap.add_argument("--no_stitch", action="store_true")
588
+ ap.add_argument("--no_lm_prompt", action="store_true")
589
+ a = ap.parse_args()
590
+ {"lm": train_lm, "flow": train_flow, "synth": synth, "redecode": redecode}[a.cmd](a)
config.json ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "dynamic-accent",
3
+ "name": "Dynamic Accent Pro v1.1",
4
+ "version": "1.1",
5
+ "task": "text-to-speech",
6
+ "experimental": true,
7
+ "languages": [
8
+ "en",
9
+ "pcm",
10
+ "yo",
11
+ "ig",
12
+ "ha"
13
+ ],
14
+ "default_version": "flash",
15
+ "versions": [
16
+ "pro",
17
+ "flash"
18
+ ],
19
+ "voices": [
20
+ "anchor_male",
21
+ "waxal_ibo_5_female",
22
+ "waxal_yor_5_female"
23
+ ],
24
+ "controls": {
25
+ "instruct": "natural-language style instruction (--instruct)",
26
+ "codeswitch_prompt": "optional text-LLM code-switch step (--codeswitch_prompt)"
27
+ },
28
+ "settings": "settings.json",
29
+ "entrypoints": {
30
+ "cli": "infer.py"
31
+ },
32
+ "base_models": [
33
+ "FunAudioLLM/Fun-CosyVoice3-0.5B-2512"
34
+ ],
35
+ "codeswitch_llm": "Qwen/Qwen3-32B (Apache-2.0), optional",
36
+ "licence": {
37
+ "weights_prompts_samples": "CC-BY-SA-4.0 (LICENSE)",
38
+ "code": "Apache-2.0 (LICENSE-CODE)"
39
+ },
40
+ "note": "Repo-level manifest (lets the Hub count downloads). Runtime settings live in settings.json; per-checkpoint configs under checkpoints/. Default: flash; --instruct switches to pro unless --version is given."
41
+ }
cosyvoice_requirements.txt ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ --extra-index-url https://download.pytorch.org/whl/cu121
2
+ --extra-index-url https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/ # https://github.com/microsoft/onnxruntime/issues/21684
3
+ conformer==0.3.2
4
+ deepspeed==0.15.1; sys_platform == 'linux'
5
+ diffusers==0.29.0
6
+ fastapi==0.115.6
7
+ fastapi-cli==0.0.4
8
+ gdown==5.1.0
9
+ gradio==5.4.0
10
+ grpcio==1.57.0
11
+ grpcio-tools==1.57.0
12
+ hydra-core==1.3.2
13
+ HyperPyYAML==1.2.3
14
+ inflect==7.3.1
15
+ librosa==0.10.2
16
+ lightning==2.2.4
17
+ matplotlib==3.7.5
18
+ modelscope==1.20.0
19
+ networkx==3.1
20
+ numpy==1.26.4
21
+ omegaconf==2.3.0
22
+ onnx==1.16.0
23
+ onnxruntime-gpu==1.18.0; sys_platform == 'linux'
24
+ onnxruntime==1.18.0; sys_platform == 'darwin' or sys_platform == 'win32'
25
+ openai-whisper==20231117
26
+ protobuf==4.25
27
+ pyarrow==18.1.0
28
+ pydantic==2.7.0
29
+ pyworld==0.3.4
30
+ rich==13.7.1
31
+ soundfile==0.12.1
32
+ tensorboard==2.14.0
33
+ tensorrt-cu12==10.13.3.9; sys_platform == 'linux'
34
+ tensorrt-cu12-bindings==10.13.3.9; sys_platform == 'linux'
35
+ tensorrt-cu12-libs==10.13.3.9; sys_platform == 'linux'
36
+ torch==2.3.1
37
+ torchaudio==2.3.1
38
+ transformers==4.51.3
39
+ x-transformers==2.11.24
40
+ uvicorn==0.30.0
41
+ wetext==0.0.4
42
+ wget==3.2
cs_frontend.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Code-switch text front end (pipeline step, NOT native model capability).
2
+
3
+ Turns a plain sentence + a code-switch request ("mostly English, Igbo greetings") into language-tagged text
4
+ (<eng>..</eng> <ibo>..</ibo>) that the TTS then speaks. Uses a permissively licensed text LLM:
5
+ default Qwen/Qwen3-14B (Apache-2.0), thinking disabled. N-ATLaS was not used: its terms cap use at 1,000 active
6
+ users and require a commercial licence beyond that.
7
+
8
+ Safety rails (MADLAD looped in Flash): max_new_tokens tied to input length, repetition_penalty, no_repeat_ngram,
9
+ output validation (tags only from allowed set, full coverage, length ratio, n-gram repetition cap), up to 2 retries,
10
+ then a safe fallback (the input as single-language text) with a warning.
11
+ """
12
+ import re, os, json
13
+ from collections import Counter
14
+
15
+ LANGS = {"eng": "English", "ibo": "Igbo", "yor": "Yoruba", "hau": "Hausa", "pcm": "Nigerian Pidgin"}
16
+ SYS = """You rewrite text for a Nigerian text-to-speech system as natural code-switched speech.
17
+ Follow the user's code-switch request exactly (which languages, how much of each, which parts).
18
+ Rules:
19
+ - Keep the meaning. Do not add new information, greetings or sentences that the request does not ask for.
20
+ - Write Igbo, Yoruba and Hausa in standard orthography (use tone marks/diacritics for Yoruba and Igbo if you can).
21
+ - Wrap every span in a language tag: <eng>...</eng>, <ibo>...</ibo>, <yor>...</yor>, <hau>...</hau>, <pcm>...</pcm>.
22
+ - Nothing may appear outside tags. Output ONLY the tagged text on one line, no explanations.
23
+ - Prefer these common, correct phrases when greetings/thanks/closings are requested:
24
+ Igbo: Ndewo (hello), Ụtụtụ ọma (good morning), Ehihie ọma (good afternoon), Mgbede ọma (good evening), Kedu? (how are you), Daalụ (thank you), Nnọọ (welcome), Ka ọ dị (goodbye)
25
+ Yoruba: Ẹ káàárọ̀ (good morning), Ẹ káàsán (good afternoon), Ẹ kú ìrọ̀lẹ́ (good evening), Ẹ kú àbọ̀ (welcome), Ẹ ṣé (thank you), Ó dàbọ̀ (goodbye)
26
+ Hausa: Sannu (hello), Barka da safiya (good morning), Barka da yamma (good evening), Maraba (welcome), Na gode (thank you), Sai an jima (goodbye)
27
+ Nigerian Pidgin: How far (hello), Abeg (please), Thank you well well, We go see later (goodbye)
28
+ - If you cannot translate a part reliably, keep that part in English rather than guessing."""
29
+ SHOT_U = "Request: mostly English, Igbo greetings and closing\nText: Good morning everyone. The meeting starts at ten, please be on time. Thank you."
30
+ SHOT_A = "<ibo>Ụtụtụ ọma, unu niile.</ibo> <eng>The meeting starts at ten, please be on time.</eng> <ibo>Daalụ.</ibo>"
31
+
32
+ TAG = re.compile(r"<(eng|ibo|yor|hau|pcm)>(.*?)</\1>", re.S)
33
+
34
+
35
+ def requested_langs(req):
36
+ r = req.lower()
37
+ out = {k for k, v in LANGS.items() if v.lower().split()[-1] in r or k in r}
38
+ if "pidgin" in r:
39
+ out.add("pcm")
40
+ return out or {"eng"}
41
+
42
+
43
+ def validate(out, src, allowed):
44
+ out = out.strip()
45
+ spans = TAG.findall(out)
46
+ if not spans:
47
+ return False, "no tags"
48
+ rest = TAG.sub(" ", out).strip()
49
+ if re.sub(r"[\s.,!?;:'\"-]", "", rest):
50
+ return False, f"text outside tags: {rest[:40]}"
51
+ bad = {l for l, _ in spans} - allowed - {"eng"}
52
+ if bad:
53
+ return False, f"unrequested language {bad}"
54
+ words = " ".join(t for _, t in spans).split()
55
+ n_src = len(src.split())
56
+ if not (0.4 * n_src <= len(words) <= 2.5 * n_src + 6):
57
+ return False, f"length ratio {len(words)}/{n_src}"
58
+ low = [w.lower().strip(".,!?") for w in words]
59
+ for k in range(len(low) - 3):
60
+ if low[k] == low[k + 1] == low[k + 2] == low[k + 3]:
61
+ return False, "word loop"
62
+ tri = Counter(tuple(low[i:i + 3]) for i in range(len(low) - 2))
63
+ if tri and tri.most_common(1)[0][1] > 2:
64
+ return False, "trigram loop"
65
+ if any(not t.strip() for _, t in spans):
66
+ return False, "empty span"
67
+ return True, "ok"
68
+
69
+
70
+ def chrf(hyp, ref, n=6, beta=2.0):
71
+ def grams(s, k):
72
+ s = " ".join(s.lower().split())
73
+ return Counter(s[i:i + k] for i in range(len(s) - k + 1))
74
+ P, R = [], []
75
+ for k in range(1, n + 1):
76
+ h, r = grams(hyp, k), grams(ref, k)
77
+ if not h or not r:
78
+ continue
79
+ m = sum((h & r).values())
80
+ P.append(m / sum(h.values())); R.append(m / sum(r.values()))
81
+ if not P:
82
+ return 0.0
83
+ p, r = sum(P) / len(P), sum(R) / len(R)
84
+ return 0.0 if p + r == 0 else 100 * (1 + beta ** 2) * p * r / (beta ** 2 * p + r)
85
+
86
+
87
+ class CodeSwitchFrontEnd:
88
+ def __init__(self, model=os.environ.get("CS_LLM", "Qwen/Qwen3-14B"), device="cuda", cache_dir=None, diac=None):
89
+ import torch
90
+ from transformers import AutoTokenizer, AutoModelForCausalLM
91
+ self.torch = torch
92
+ self.tok = AutoTokenizer.from_pretrained(model, cache_dir=cache_dir)
93
+ self.lm = AutoModelForCausalLM.from_pretrained(model, torch_dtype=torch.bfloat16, cache_dir=cache_dir).to(device).eval()
94
+ self.device = device
95
+ self.diac = diac # optional flash FrontEnd (ByT5 diacritic restoration) for ibo/yor spans
96
+
97
+ def _gen(self, req, text, temp):
98
+ msgs = [{"role": "system", "content": SYS}, {"role": "user", "content": SHOT_U}, {"role": "assistant", "content": SHOT_A},
99
+ {"role": "user", "content": f"Request: {req}\nText: {text}"}]
100
+ try:
101
+ p = self.tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True, enable_thinking=False)
102
+ except TypeError:
103
+ p = self.tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
104
+ ids = self.tok(p, return_tensors="pt").to(self.device)
105
+ n_in = len(self.tok(text)["input_ids"])
106
+ # repetition rails act on the GENERATED tokens only (a prompt-wide no_repeat_ngram/repetition_penalty
107
+ # forbids copying the input and garbles spelling): hard length cap + loop stop + validation below
108
+ from transformers import StoppingCriteria, StoppingCriteriaList
109
+ P0 = ids["input_ids"].shape[1]
110
+
111
+ class LoopStop(StoppingCriteria):
112
+ def __call__(s_, input_ids, scores, **k):
113
+ g = input_ids[0, P0:].tolist()
114
+ if len(g) < 18:
115
+ return False
116
+ tail = tuple(g[-6:])
117
+ hits = sum(1 for i in range(len(g) - 6) if tuple(g[i:i + 6]) == tail)
118
+ return hits >= 2
119
+ kw = dict(max_new_tokens=int(3 * n_in + 48), pad_token_id=self.tok.eos_token_id,
120
+ stopping_criteria=StoppingCriteriaList([LoopStop()]))
121
+ if temp > 0:
122
+ kw.update(do_sample=True, temperature=temp, top_p=0.9)
123
+ else:
124
+ kw.update(do_sample=False, temperature=None, top_p=None, top_k=None)
125
+ with self.torch.no_grad():
126
+ o = self.lm.generate(**ids, **kw)
127
+ s = self.tok.decode(o[0, ids["input_ids"].shape[1]:], skip_special_tokens=True)
128
+ s = re.sub(r"<think>.*?</think>", "", s, flags=re.S).strip().splitlines()
129
+ return s[0].strip() if s else ""
130
+
131
+ def back_translate(self, span, lang):
132
+ msgs = [{"role": "user", "content": f"Translate this {LANGS[lang]} text into English. Output only the translation.\n\n{span}"}]
133
+ try:
134
+ p = self.tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True, enable_thinking=False)
135
+ except TypeError:
136
+ p = self.tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
137
+ ids = self.tok(p, return_tensors="pt").to(self.device)
138
+ with self.torch.no_grad():
139
+ o = self.lm.generate(**ids, max_new_tokens=3 * len(span.split()) + 24, do_sample=False, temperature=None,
140
+ top_p=None, top_k=None, pad_token_id=self.tok.eos_token_id)
141
+ return self.tok.decode(o[0, ids["input_ids"].shape[1]:], skip_special_tokens=True).strip()
142
+
143
+ def meaning_check(self, out, src, thr=45.0):
144
+ """round trip: back-translate every non-English span; the English reconstruction must match the input (chrF)."""
145
+ parts = [t if l == "eng" else self.back_translate(t, l) for l, t in TAG.findall(out)]
146
+ back = " ".join(parts)
147
+ score = min(chrf(back, src), chrf(back, src, beta=0.5)) # recall- and precision-weighted (added content fails)
148
+ return score >= thr, round(score, 1), back
149
+
150
+ def __call__(self, text, req, fallback_lang="eng"):
151
+ allowed = requested_langs(req)
152
+ log = []
153
+ for k, temp in enumerate((0.0, 0.6, 0.8)): # first try + at most 2 retries
154
+ out = self._gen(req, text, temp)
155
+ ok, why = validate(out, text, allowed)
156
+ rt = None
157
+ if ok:
158
+ ok, sc, back = self.meaning_check(out, text)
159
+ rt = dict(chrf=sc, back=back[:300])
160
+ if not ok:
161
+ why = f"meaning drift (round-trip chrF {sc})"
162
+ log.append(dict(try_=k, ok=ok, why=why, out=out[:300], roundtrip=rt))
163
+ if ok:
164
+ if self.diac is not None:
165
+ out = TAG.sub(lambda m: f"<{m.group(1)}>{self.diac.diacritise(m.group(2), m.group(1)) if m.group(1) in ('ibo', 'yor') else m.group(2)}</{m.group(1)}>", out)
166
+ return out, dict(ok=True, tries=log)
167
+ return f"<{fallback_lang}>{text}</{fallback_lang}>", dict(ok=False, tries=log, warning="code-switch front end failed validation; spoke the input as-is")
168
+
169
+
170
+ if __name__ == "__main__":
171
+ import sys
172
+ cache = os.environ.get("HF_CACHE")
173
+ fe = CodeSwitchFrontEnd(cache_dir=cache)
174
+ tests = json.load(open(sys.argv[1])) if len(sys.argv) > 1 else [
175
+ ["Good morning everyone, welcome to the market. Prices are good today, come and buy.", "mostly English, Igbo greetings"],
176
+ ["Please call your mother tonight, she has been waiting to hear from you.", "Igbo sentence with a few English words"],
177
+ ["Welcome to our shop. Everything is half price this weekend, so tell your friends.", "Yoruba greeting, the rest in English"],
178
+ ["Good evening. The rain will be heavy tomorrow, so carry an umbrella.", "half Hausa, half English"],
179
+ ["My brother, the work is too much today, but we will finish it.", "Nigerian Pidgin mixed with Igbo"],
180
+ ["Thank you for coming. We will start the program in five minutes.", "English with a Yoruba closing"],
181
+ ]
182
+ res = []
183
+ for t, r in tests:
184
+ o, info = fe(t, r)
185
+ res.append(dict(text=t, request=r, out=o, ok=info["ok"], tries=len(info["tries"]), log=info["tries"]))
186
+ print(json.dumps(res[-1], ensure_ascii=False), flush=True)
187
+ json.dump(res, open(os.environ.get("CS_OUT", "cs_test.json"), "w"), ensure_ascii=False, indent=1)
flash_instruct.py ADDED
@@ -0,0 +1,433 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Flash-Instruct: natural-language instruction control for the Flash-DPO CosyVoice3 LM.
2
+
3
+ The CV3 system-prompt slot ("You are a helpful assistant.<|endofprompt|>") becomes an instruction, e.g.
4
+ "Speak in an Igbo-English mix, slowly, with plenty of pauses." Labels come from label.py (audio-derived).
5
+ ~18% of training items keep the original default prompt (and the original rate embedding), so default behaviour is kept.
6
+ Instruction items use rate embedding 0 (none): the instruction carries pace.
7
+
8
+ python flash_instruct.py train --init ~/flash/runs/flash_dpo/best.pt --out /scratch/fi_run
9
+ python flash_instruct.py gen --lm CKPT --out DIR --voice anchor [--instruct "..."] [--rows ROWS] [--shard i/n]
10
+ python flash_instruct.py adh --lm CKPT --out DIR --voice anchor --shard i/n (adherence grid)
11
+ """
12
+ import argparse, json, math, os, random, sys, time
13
+ import numpy as np, torch, torch.nn.functional as F, soundfile as sf
14
+ HOME = os.path.expanduser("~")
15
+ _HERE = os.path.dirname(os.path.abspath(__file__))
16
+ sys.path[:0] = [_HERE]
17
+ import flash_lm as FL # noqa: E402
18
+ import instruct_text as IT # noqa: E402
19
+
20
+ EOP = 151646 # <|endofprompt|>
21
+ # extra source weights on top of flash_lm.FLASH_W (natural/spontaneous speech up, Bible narration down)
22
+ MIXW = {"igbosyncorp": 2.0, "igbosyn_pseudo": 1.5, "naijas2st_igbo": 1.5, "open_bible_igbo": 0.5, "bible_hau": 0.5,
23
+ "bible_yor": 0.5}
24
+
25
+
26
+ PI = {"few": 1, "normal": 2, "many": 3}
27
+ EI = {"calm": 1, "neutral": 2, "lively": 3}
28
+ RI = {"slow": FL.R2I["slow"], "normal": FL.R2I["normal"], "fast": FL.R2I["fast"]}
29
+
30
+
31
+ def add_ctrl(model, d=896):
32
+ """explicit control embeddings (pause, energy) added to the speaker vector next to the existing rate embedding."""
33
+ import torch.nn as nn
34
+ if not hasattr(model, "pause_emb"):
35
+ model.pause_emb = nn.Embedding(4, d, padding_idx=0).cuda(); nn.init.zeros_(model.pause_emb.weight)
36
+ model.energy_emb = nn.Embedding(4, d, padding_idx=0).cuda(); nn.init.zeros_(model.energy_emb.weight)
37
+ return model
38
+
39
+
40
+ def parse_controls(instr):
41
+ """keyword parser: natural-language instruction -> (rate, pause, energy) control ids (0 = not requested)."""
42
+ t = (instr or "").lower()
43
+ def has(*w):
44
+ return any(x in t for x in w)
45
+ rate = RI["slow"] if has("slow", "unhurried", "take your time", "taking your time", "deliberate") else \
46
+ RI["fast"] if has("fast", "quick", "brisk", "rapid", "hurr") else RI["normal"] if has("natural pace", "normal pace", "steady pace", "comfortable pace", "everyday pace") else 0
47
+ pause = PI["few"] if has("few pause", "no pause", "hardly any pause", "without stopping", "one smooth flow", "minimal paus", "running the phrases") else \
48
+ PI["many"] if has("plenty of pause", "many pause", "lots of pause", "pausing often", "frequent pause", "deliberate pause", "breaks between", "more pause") else \
49
+ PI["normal"] if has("natural pause", "normal phrasing") else 0
50
+ energy = EI["calm"] if has("calm", "gentle", "soft", "relaxed", "low-key", "quiet", "even-toned", "even tone") else \
51
+ EI["lively"] if has("lively", "animated", "energetic", "expressive", "upbeat", "excited", "bright") else \
52
+ EI["neutral"] if has("neutral", "matter-of-fact", "plain") else 0
53
+ return rate, pause, energy
54
+
55
+
56
+ class IFLM(FL.FLM):
57
+ """FLM whose system-prompt prefix can be set per item (self._pre); optional pause/energy control embeddings."""
58
+ _pre = None
59
+ ctrl_scale = 1.0
60
+
61
+ def spkvec(self, spk):
62
+ v = self.spk_emb(spk[0]) + self.rate_emb(spk[1])
63
+ if hasattr(self, "pause_emb") and spk.shape[0] > 2:
64
+ v = v + self.ctrl_scale * (self.pause_emb(spk[2]) + self.energy_emb(spk[3]))
65
+ return v
66
+
67
+ def build(self, ids, langs, spk, sp=None, pids=None, plangs=None, psp=None):
68
+ E = self.q.speech_embedding.weight
69
+ if pids is not None:
70
+ ids = torch.cat([pids, ids]); langs = torch.cat([plangs, langs])
71
+ pfx = self._pre if self._pre is not None else self.prefix
72
+ pre = self.q.llm.model.model.embed_tokens(pfx.to(ids.device))
73
+ parts = [E[self.q.sos][None], pre, self.embed_text(ids, langs), self.spkvec(spk)[None], E[self.q.task_id][None]]
74
+ npsp = 0
75
+ if psp is not None:
76
+ parts.append(self.q.speech_embedding(psp)); npsp = len(psp)
77
+ if sp is not None:
78
+ parts.append(self.q.speech_embedding(sp))
79
+ return torch.cat(parts, 0), len(pre), len(ids), npsp
80
+
81
+ def pack(self, batch):
82
+ seqs, tgts, tpos, spks = [], [], [], []
83
+ for b in batch:
84
+ ids, langs, spk, sp = b[:4]
85
+ pr = b[4] if len(b) > 4 else None
86
+ self._pre = b[5] if len(b) > 5 else None
87
+ x, P, nt, npsp = self.build(ids, langs, spk, sp, *(pr if pr else (None, None, None)))
88
+ self._pre = None
89
+ tgt = torch.full((x.shape[0] + 1,), -100, device=x.device, dtype=torch.long)
90
+ start = P + nt + 2 + npsp
91
+ tgt[start: start + len(sp)] = sp
92
+ tgt[start + len(sp)] = self.EOS
93
+ seqs.append(x); tgts.append(tgt[:x.shape[0]]); tpos.append((1 + P, 1 + P + nt)); spks.append(int(spk[0]))
94
+ Tm = max(s.shape[0] for s in seqs); D = seqs[0].shape[1]
95
+ X = seqs[0].new_zeros(len(seqs), Tm, D)
96
+ Y = torch.full((len(seqs), Tm), -100, device=X.device, dtype=torch.long)
97
+ M = torch.zeros(len(seqs), Tm, device=X.device, dtype=torch.long)
98
+ for i, (s, t) in enumerate(zip(seqs, tgts)):
99
+ X[i, :len(s)] = s; Y[i, :len(t)] = t; M[i, :len(s)] = 1
100
+ return X, Y, M, tpos, spks
101
+
102
+
103
+ @torch.no_grad()
104
+ def generate_cfg(m, ids, langs, spk_c, spk_u, pre_c, prompt=None, g=1.5, top_p=0.8, top_k=25, win=10, tau_r=0.1, seed=0,
105
+ min_ratio=2.0, max_ratio=30.0, max_tok=900):
106
+ """classifier-free guidance on the instruction: logp = lp_cond + g * (lp_cond - lp_default).
107
+ cond = instruction prefix + controls; uncond = the default prompt, no controls. Same silexcap sampler as Flash."""
108
+ gen = torch.Generator(device=ids.device).manual_seed(seed)
109
+ m._pre = pre_c
110
+ xc, _, _, _ = m.build(ids, langs, spk_c, None, *(prompt if prompt else (None, None, None)))
111
+ m._pre = None
112
+ xu, _, _, _ = m.build(ids, langs, spk_u, None, *(prompt if prompt else (None, None, None)))
113
+ n = len(ids); min_len, max_len = int(n * min_ratio), min(int(n * max_ratio), max_tok)
114
+ E = m.q.speech_embedding.weight
115
+ pc = pu = None; ic, iu = xc[None], xu[None]; out = []
116
+ for i in range(max_len):
117
+ oc = m.q.llm.model.model(inputs_embeds=ic, past_key_values=pc, use_cache=True, return_dict=True); pc = oc.past_key_values
118
+ ou = m.q.llm.model.model(inputs_embeds=iu, past_key_values=pu, use_cache=True, return_dict=True); pu = ou.past_key_values
119
+ lc = m.q.llm_decoder(oc.last_hidden_state[:, -1]).float().log_softmax(-1)[0]
120
+ lu = m.q.llm_decoder(ou.last_hidden_state[:, -1]).float().log_softmax(-1)[0]
121
+ logp = (lc + g * (lc - lu)).log_softmax(-1)
122
+ eos_lp = logp[m.EOS].clone()
123
+ logp[m.V:] = -float("inf")
124
+ if i >= min_len:
125
+ logp[m.EOS] = eos_lp
126
+ p = logp.exp(); p = p / p.sum()
127
+ sp_, si = p.sort(descending=True)
128
+ keep = (sp_.cumsum(0) - sp_) < top_p; keep[top_k:] = False
129
+ cp, ci = sp_[keep], si[keep]
130
+ t = int(ci[torch.multinomial(cp / cp.sum(), 1, generator=gen)])
131
+ exempt = False
132
+ if t in FL.SIL:
133
+ run = 0
134
+ for u in reversed(out):
135
+ if u not in FL.SIL:
136
+ break
137
+ run += 1
138
+ exempt = run < 25
139
+ if not exempt and out and t != m.EOS and sum(1 for u in out[-win:] if u == t) >= win * tau_r:
140
+ t = int(torch.multinomial(p, 1, generator=gen))
141
+ if t >= m.V:
142
+ break
143
+ out.append(t)
144
+ nx = E[torch.tensor([t], device=ids.device)][:, None]
145
+ ic = iu = nx
146
+ return [out]
147
+
148
+
149
+ def pre_ids(tok, instruct):
150
+ return tok.t.encode(instruct or IT.DEFAULT) + [EOP]
151
+
152
+
153
+ def train(a):
154
+ cosy = FL.load_cv3(); tok = FL.Tok(cosy)
155
+ model, spk2id = FL.load_model(cosy, a.init, tok)
156
+ model.__class__ = IFLM
157
+ if a.ctrl:
158
+ add_ctrl(model)
159
+ model.train()
160
+ L = {}
161
+ for l in open(a.labels, encoding="utf-8"):
162
+ x = json.loads(l); L[x["id"]] = x
163
+ items, mm = FL.load_data({"train"}, a.max_tok, a.extra_man, a.qual, a.qual_drop)
164
+ items = [m for m in items if m["spk"] in spk2id and not m["src"].startswith("av_")]
165
+ # ACE add-on: cleaner + more native mix. drop noisy/low-SNR/clipped clips; up-weight natural speech, down-weight Bible
166
+ MX = json.load(open(a.mix)) if a.mix and os.path.exists(a.mix) else {"drop": [], "elig": []}
167
+ drop, elig = set(MX["drop"]), set(MX["elig"])
168
+ n0 = len(items)
169
+ items = [m for m in items if m["id"] not in drop]
170
+ for m in items:
171
+ m["w"] *= MIXW.get(m["src"], 1.0)
172
+ n_el = sum(m["id"] in elig for m in items)
173
+ p_def_e = min(0.6, a.p_default * len(items) / max(1, n_el))
174
+ hrs = {}
175
+ for m in items:
176
+ hrs[m["src"]] = hrs.get(m["src"], 0) + m["dur"] * m["w"] / 3600
177
+ print("mix: dropped", n0 - len(items), "default-eligible", n_el, "p_default_eligible", round(p_def_e, 3),
178
+ "effective weighted hours", {k: round(v, 1) for k, v in sorted(hrs.items())}, flush=True)
179
+ val_all, mmv = FL.load_data(FL.VAL_SPLITS, a.max_tok, None)
180
+ mm.update(mmv)
181
+ rng = random.Random(1); rng.shuffle(val_all)
182
+ val, per_l = [], {}
183
+ for m in val_all:
184
+ if m["spk"] not in spk2id or m["lang"] == "cs" or m["id"] not in L or m["src"].startswith("av_"):
185
+ continue
186
+ if per_l.get(m["lang"], 0) >= 50:
187
+ continue
188
+ per_l[m["lang"]] = per_l.get(m["lang"], 0) + 1; val.append(m)
189
+ nl = sum(m["id"] in L for m in items)
190
+ print("items", len(items), "labelled", nl, "val", len(val), per_l, flush=True)
191
+ by_spk = {}
192
+ for m in items:
193
+ if m["ntok"] <= 200 and m["lang"] != "cs":
194
+ by_spk.setdefault(m["spk"], []).append(m)
195
+ for p in list(cosy.model.flow.parameters()) + list(cosy.model.hift.parameters()):
196
+ p.requires_grad = False
197
+ new = list(model.lang_emb.parameters()) + list(model.spk_emb.parameters()) + list(model.adv.parameters()) + list(model.rate_emb.parameters())
198
+ ctl = list(model.pause_emb.parameters()) + list(model.energy_emb.parameters()) if a.ctrl else []
199
+ newids = {id(p) for p in new + ctl}
200
+ old = [p for p in model.parameters() if id(p) not in newids and p.requires_grad]
201
+ groups = [{"params": old, "lr": a.lr}, {"params": new, "lr": a.lr_new}]
202
+ base = [a.lr, a.lr_new]
203
+ if ctl:
204
+ groups.append({"params": ctl, "lr": a.lr_ctrl}); base.append(a.lr_ctrl)
205
+ opt = torch.optim.AdamW(groups, weight_decay=0.01)
206
+ os.makedirs(a.out, exist_ok=True)
207
+ json.dump(spk2id, open(f"{a.out}/spk2id.json", "w")); json.dump(vars(a), open(f"{a.out}/args.json", "w"))
208
+ pcache = {}
209
+
210
+ def P_(s):
211
+ if s not in pcache:
212
+ pcache[s] = torch.tensor(pre_ids(tok, s)).cuda()
213
+ return pcache[s]
214
+
215
+ def full_labels(m):
216
+ x = L.get(m["id"])
217
+ if not x:
218
+ return None
219
+ return dict(lang=x["lang"], rate=x["rate"], pause=x["pause"], energy=x["energy"], gender=x["gender"])
220
+
221
+ def enc(m, r, mode="sample", shuf=None):
222
+ """mode: sample (train) | default | inst (fixed rng) ; shuf=labels from another item (diagnostic)."""
223
+ text = m["tags"] if (m["tags"] and r.random() >= a.tag_drop) else FL.strip_tags(m["text"])
224
+ ids, langs = tok(FL.tagged(text, m["lang"]))
225
+ lab = shuf if shuf is not None else full_labels(m)
226
+ # DEFAULT prompt only on native-like clips (pace and pauses close to real native speech), so the
227
+ # no-instruction behaviour moves toward native pace/pauses; other clips always carry an instruction
228
+ use_def = mode == "default" or lab is None or (mode == "sample" and m["id"] in elig and r.random() < p_def_e)
229
+ pc = ec = 0
230
+ if use_def:
231
+ rate = m["rate"] if (mode != "sample" or r.random() >= a.rate_drop) else 0
232
+ instr = IT.DEFAULT
233
+ else:
234
+ sub = IT.sample_labels(lab, r) if mode == "sample" else {k: v for k, v in lab.items() if k != "gender"}
235
+ instr = IT.make(sub, r)
236
+ rate = 0
237
+ if a.ctrl: # the instruction's controls also go in as explicit embeddings (what it mentions, nothing else)
238
+ rate = RI.get(sub.get("rate"), 0); pc = PI.get(sub.get("pause"), 0); ec = EI.get(sub.get("energy"), 0)
239
+ sp = np.asarray(mm[m["ds"]][m["toff"]: m["toff"] + m["ntok"]], dtype=np.int64)
240
+ spk_t = [spk2id[m["spk"]], rate] + ([pc, ec] if a.ctrl else [])
241
+ out = [torch.tensor(ids).cuda(), torch.tensor(langs).cuda(), torch.tensor(spk_t).cuda(),
242
+ torch.from_numpy(sp).cuda(), None, P_(instr)]
243
+ if r.random() < a.p_prompt:
244
+ c = by_spk.get(m["spk"], [])
245
+ c = [x for x in r.sample(c, min(6, len(c))) if x["id"] != m["id"]] if c else []
246
+ if c and c[0]["ntok"] + m["ntok"] <= a.max_tok + 150:
247
+ p = c[0]
248
+ pids, plangs = tok(FL.tagged(FL.strip_tags(p["text"]), p["lang"]))
249
+ psp = np.asarray(mm[p["ds"]][p["toff"]: p["toff"] + p["ntok"]], dtype=np.int64)
250
+ out[4] = (torch.tensor(pids).cuda(), torch.tensor(plangs).cuda(), torch.from_numpy(psp).cuda())
251
+ return out
252
+
253
+ def validate():
254
+ model.eval(); res = {}
255
+ with torch.no_grad(), torch.autocast("cuda", dtype=torch.bfloat16):
256
+ for mode in ("default", "inst", "shuf"):
257
+ vr = random.Random(7); sr_ = random.Random(9); Ls, W = [], []
258
+ for i in range(0, len(val), 16):
259
+ bt = val[i:i + 16]
260
+ if mode == "shuf":
261
+ b = [enc(m, vr, "inst", shuf=full_labels(sr_.choice(val))) for m in bt]
262
+ else:
263
+ b = [enc(m, vr, mode) for m in bt]
264
+ ce, _, _, _ = model.forward2(b)
265
+ Ls.append(float(ce) * len(bt)); W.append(len(bt))
266
+ res[mode] = sum(Ls) / sum(W)
267
+ model.train()
268
+ res["inst_gain"] = res["shuf"] - res["inst"] # >0: the instruction carries information the model uses
269
+ res["mean"] = (res["default"] + res["inst"]) / 2
270
+ return res
271
+
272
+ wfn = lambda m: m["w"] # noqa
273
+ cost = lambda m: (lambda Lq: int(Lq * (1 + Lq / a.quad)))(m["ntok"] + len(m["text"]) // 2 + 40 + int(a.p_prompt * 200)) # noqa
274
+ rng = random.Random(0); prng = random.Random(3)
275
+ step, nskip, t0 = 0, 0, time.time()
276
+ logf = open(f"{a.out}/log.jsonl", "a")
277
+ v = validate(); best = v["mean"]; bad = 0
278
+ print(json.dumps({"step": 0, "val": v}), flush=True); logf.write(json.dumps({"step": 0, "val": v}) + "\n")
279
+ total, done = a.max_steps, False
280
+ while not done:
281
+ for bi in FL.batches(items, a.tokens, rng, wfn, cost):
282
+ lrm = min(1.0, (step + 1) / a.warmup) * (0.1 + 0.9 * 0.5 * (1 + math.cos(math.pi * min(1.0, step / total))))
283
+ for gp, b in zip(opt.param_groups, base):
284
+ gp["lr"] = b * lrm
285
+ batch = [enc(items[i], prng) for i in bi]
286
+ opt.zero_grad(set_to_none=True)
287
+ try:
288
+ with torch.autocast("cuda", dtype=torch.bfloat16):
289
+ ce, acc, adv, adv_acc = model.forward2(batch, adv_lambda=1.0)
290
+ loss = ce + a.adv_w * adv
291
+ if not torch.isfinite(loss):
292
+ nskip += 1; continue
293
+ loss.backward()
294
+ except torch.cuda.OutOfMemoryError:
295
+ batch = None; opt.zero_grad(set_to_none=True); torch.cuda.empty_cache(); nskip += 1; continue
296
+ gn = torch.nn.utils.clip_grad_norm_([p for p in model.parameters() if p.requires_grad], 1.0)
297
+ if not torch.isfinite(gn):
298
+ nskip += 1; opt.zero_grad(set_to_none=True); continue
299
+ opt.step(); step += 1
300
+ if step % 50 == 0:
301
+ r = dict(step=step, ce=round(float(ce), 4), acc=round(float(acc), 4), gn=round(float(gn), 2), B=len(bi),
302
+ skips=nskip, min=round((time.time() - t0) / 60, 2))
303
+ print(json.dumps(r), flush=True); logf.write(json.dumps(r) + "\n"); logf.flush()
304
+ if step % a.val_every == 0:
305
+ v = validate(); imp = v["mean"] < best - 1e-4
306
+ print(json.dumps({"step": step, "val": v, "best": imp}), flush=True)
307
+ logf.write(json.dumps({"step": step, "val": v, "best": imp}) + "\n"); logf.flush()
308
+ torch.save(dict(model=model.state_dict(), step=step, n_spk=len(spk2id), val=v), f"{a.out}/last.pt")
309
+ if imp:
310
+ best, bad = v["mean"], 0
311
+ os.replace(f"{a.out}/last.pt", f"{a.out}/best.pt")
312
+ else:
313
+ bad += 1
314
+ if bad >= a.patience:
315
+ print("EARLY_STOP", step, flush=True); done = True; break
316
+ if (time.time() - t0) / 60 > a.minutes or step >= total:
317
+ done = True; break
318
+ print("TRAIN_FI_DONE", step, "best", best, "skips", nskip, flush=True)
319
+
320
+
321
+ class ISynth(FL.Synth):
322
+ cfg = 0.0 # classifier-free guidance strength on the instruction (0 = off)
323
+
324
+ def __init__(self, lm_ckpt, ctrl_scale=1.0, cfg=0.0):
325
+ super().__init__(lm_ckpt)
326
+ self.cfg = cfg
327
+ self.m.__class__ = IFLM
328
+ sd = torch.load(lm_ckpt, map_location="cuda", weights_only=False)["model"]
329
+ self.ctrl = "pause_emb.weight" in sd
330
+ if self.ctrl:
331
+ add_ctrl(self.m)
332
+ self.m.pause_emb.weight.data.copy_(sd["pause_emb.weight"]); self.m.energy_emb.weight.data.copy_(sd["energy_emb.weight"])
333
+ self.m.ctrl_scale = ctrl_scale
334
+ del sd
335
+
336
+ text_pauses = True # pipeline step: "few/many pauses" also edits the text ([pause] tags / commas), see pause_text.py
337
+
338
+ def tokens(self, text, voice="anchor", rate="normal", seed=0, B=1, sampler="silexcap", instruct=None):
339
+ ptok, pfeat, emb, lmp, sid = self.voice(voice)
340
+ if instruct and instruct != IT.DEFAULT and self.text_pauses:
341
+ import pause_text
342
+ pc_ = parse_controls(instruct)[1]
343
+ if pc_ in (PI["few"], PI["many"]):
344
+ text = pause_text.apply(text, "few" if pc_ == PI["few"] else "many")
345
+ ids, langs = self.tok(text)
346
+ if instruct and instruct != IT.DEFAULT:
347
+ self.m._pre = torch.tensor(pre_ids(self.tok, instruct)).cuda()
348
+ rc, pc, ec = parse_controls(instruct) if self.ctrl else (0, 0, 0)
349
+ spk = [sid, rc] + ([pc, ec] if self.ctrl else [])
350
+ else:
351
+ self.m._pre = None
352
+ spk = [sid, FL.R2I.get(None if rate == "none" else rate, 0)] + ([0, 0] if self.ctrl else [])
353
+ try:
354
+ if self.cfg > 0 and self.m._pre is not None and B == 1:
355
+ spk_u = [sid, FL.R2I["normal"]] + ([0, 0] if self.ctrl else [])
356
+ with torch.autocast("cuda", dtype=torch.bfloat16):
357
+ return generate_cfg(self.m, torch.tensor(ids).cuda(), torch.tensor(langs).cuda(), torch.tensor(spk).cuda(),
358
+ torch.tensor(spk_u).cuda(), self.m._pre, prompt=lmp, g=self.cfg, seed=seed)
359
+ with torch.autocast("cuda", dtype=torch.bfloat16):
360
+ return self.m.generate(torch.tensor(ids).cuda(), torch.tensor(langs).cuda(), torch.tensor(spk).cuda(),
361
+ prompt=lmp, B=B, seed=seed, sampler=sampler)
362
+ finally:
363
+ self.m._pre = None
364
+
365
+
366
+ ADH = { # condition -> instruction (held-out phrasings, not verbatim training templates)
367
+ "default": IT.DEFAULT,
368
+ "slow": "Please speak slowly and take your time.",
369
+ "fast": "Speak fast, like a quick everyday conversation.",
370
+ "few_pauses": "Say it in one smooth flow with hardly any pauses.",
371
+ "many_pauses": "Speak with plenty of pauses between phrases.",
372
+ "calm": "Use a calm, gentle, even tone.",
373
+ "lively": "Sound lively and expressive.",
374
+ }
375
+
376
+
377
+ def gen(a):
378
+ S = ISynth(a.lm, a.ctrl_scale, a.cfg); S.text_pauses = not a.no_text_pauses
379
+ rows = [json.loads(l) for l in open(a.rows, encoding="utf-8")]
380
+ if a.shard:
381
+ i, n = map(int, a.shard.split("/")); rows = rows[i::n]
382
+ os.makedirs(a.out, exist_ok=True)
383
+ json.dump(vars(a), open(f"{a.out}/variant.json", "w"))
384
+ for r in rows:
385
+ if os.path.exists(f"{a.out}/{r['id']}.wav"):
386
+ continue
387
+ text = r.get("tts_text") or FL.item_text(r)
388
+ toks = S.tokens(text, a.voice, a.rate, a.seed, instruct=r.get("instruct", a.instruct))[0]
389
+ sf.write(f"{a.out}/{r['id']}.wav", S.wav(toks, a.voice), S.cosy.sample_rate)
390
+ print("GEN_DONE", a.out, len(rows), flush=True)
391
+
392
+
393
+ def adh(a):
394
+ """adherence grid: each held-out row x each condition in ADH -> OUT/<cond>/<id>.wav"""
395
+ S = ISynth(a.lm, a.ctrl_scale, a.cfg); S.text_pauses = not a.no_text_pauses
396
+ rows = [json.loads(l) for l in open(a.rows, encoding="utf-8")]
397
+ C = a.conds.split(",") if a.conds else list(ADH)
398
+ jobs = [(c, r) for r in rows for c in C]
399
+ i, n = map(int, a.shard.split("/")); jobs = jobs[i::n]
400
+ for c, r in jobs:
401
+ d = f"{a.out}/{c}"; os.makedirs(d, exist_ok=True)
402
+ if os.path.exists(f"{d}/{r['id']}.wav"):
403
+ continue
404
+ text = r.get("tts_text") or FL.item_text(r)
405
+ toks = S.tokens(text, a.voice, "normal", a.seed, instruct=ADH[c])[0]
406
+ sf.write(f"{d}/{r['id']}.wav", S.wav(toks, a.voice), S.cosy.sample_rate)
407
+ print("ADH_DONE", i, flush=True)
408
+
409
+
410
+ if __name__ == "__main__":
411
+ ap = argparse.ArgumentParser()
412
+ ap.add_argument("cmd"); ap.add_argument("--out", required=True); ap.add_argument("--lm"); ap.add_argument("--init")
413
+ ap.add_argument("--rows", default=f"{HOME}/flash/score/rows_flash.jsonl")
414
+ ap.add_argument("--voice", default="anchor"); ap.add_argument("--rate", default="normal"); ap.add_argument("--seed", type=int, default=0)
415
+ ap.add_argument("--instruct", default=None); ap.add_argument("--shard")
416
+ ap.add_argument("--labels", default=f"{HOME}/fi/labels.jsonl"); ap.add_argument("--mix", default=f"{HOME}/fi/mix.json")
417
+ ap.add_argument("--minutes", type=float, default=50); ap.add_argument("--max_steps", type=int, default=4000)
418
+ ap.add_argument("--lr", type=float, default=1e-5); ap.add_argument("--lr_new", type=float, default=1e-4)
419
+ ap.add_argument("--warmup", type=int, default=200); ap.add_argument("--tokens", type=int, default=16000)
420
+ ap.add_argument("--max_tok", type=int, default=750); ap.add_argument("--adv_w", type=float, default=0.02)
421
+ ap.add_argument("--p_prompt", type=float, default=0.5); ap.add_argument("--p_default", type=float, default=0.18)
422
+ ap.add_argument("--tag_drop", type=float, default=0.5); ap.add_argument("--rate_drop", type=float, default=0.5)
423
+ ap.add_argument("--val_every", type=int, default=500); ap.add_argument("--patience", type=int, default=3)
424
+ ap.add_argument("--quad", type=float, default=600.0)
425
+ ap.add_argument("--ctrl", action="store_true", help="also feed the instruction's controls as explicit embeddings")
426
+ ap.add_argument("--lr_ctrl", type=float, default=1e-3); ap.add_argument("--ctrl_scale", type=float, default=1.0)
427
+ ap.add_argument("--cfg", type=float, default=0.0, help="classifier-free guidance strength on the instruction")
428
+ ap.add_argument("--no_text_pauses", action="store_true", help="do not edit the text for few/many-pause instructions")
429
+ ap.add_argument("--conds", default=None, help="adh: comma-separated subset of conditions")
430
+ ap.add_argument("--extra_man", default=f"{HOME}/flash/data/igbosyn_pseudo/manifest.jsonl")
431
+ ap.add_argument("--qual", default=f"{HOME}/flash/qual/utmos_train.jsonl"); ap.add_argument("--qual_drop", type=float, default=0.1)
432
+ a = ap.parse_args()
433
+ {"train": train, "gen": gen, "adh": adh}[a.cmd](a)
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 ADDED
@@ -0,0 +1,801 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """Dynamic Accent Pro v1.1: experimental prompt-controlled, code-switching Nigerian TTS.
3
+
4
+ New in Pro v1.1:
5
+ --instruct "..." natural-language style instruction read by the LM. Reliable: pace (slow / fast);
6
+ pauses (plenty of / hardly any) via a text step. Tone words (calm/lively) have no
7
+ measurable effect; no emotion control. e.g. --instruct "Speak slowly with plenty of pauses."
8
+ Without --instruct the LM gets its default prompt (tuned toward native pace and pauses).
9
+ --codeswitch_prompt "..." OPTIONAL text pipeline step BEFORE synthesis: a text LLM (Qwen3, Apache-2.0) rewrites plain
10
+ text as tagged code-switched text, e.g. "mostly English, Igbo greetings". This is a
11
+ pipeline step, not a native model capability; outputs are validated (tags, length,
12
+ repetition loops, back-translation meaning check) and fall back to the input text.
13
+
14
+ Versions (`--version`):
15
+ pro (used automatically with --instruct) Flash-Instruct LM `checkpoints/pro/flash_instruct` = Flash-DPO + instruction SFT (licensed data only).
16
+ phase2/phase3 checkpoints are not in this repo (see Modularcomputing/dynamic-accent-preview).
17
+ flash (default) LM `checkpoints/flash/flash_dpo` on Fun-CosyVoice3-0.5B-2512 (Apache-2.0): Flash SFT + DPO preference
18
+ tuning (naturalness + CER + pace + pause reward), licensed data only. CV3 base flow/vocoder, zero-shot from the
19
+ voice clip. Text front-end: numbers verbalised, Yoruba/Igbo diacritics restored when missing (ByT5 model in
20
+ checkpoints/flash/diacritics_byt5). One pass per paragraph (<= ~60 words), model pauses, no post-processing.
21
+ Needs a CUDA GPU and a CosyVoice clone with CosyVoice3 support.
22
+ phase2 LM `p2_lm_B` + anchor-SFT flow `p2_flow_sft`. Phase 2 sampler, sentence stitching, edge trim, internal
23
+ silences capped at 0.6 s, 0.6 s between sentences (exactly the Phase 2 final samples).
24
+ phase3 LM `p3_lm_full` (re-cut long-form data with natural pauses, ~102 h of Igbo) + the shared Phase 2 flow (see
25
+ settings.json and checkpoints/phase3/p3_lm_full/config.json). Silence-aware sampler (silence tokens exempt from the repetition check up
26
+ to a 1 s run), the whole paragraph in ONE pass (up to ~60 words per pass), no audio post-processing: every
27
+ pause and the pace come from the model.
28
+
29
+ Voices (`--voice`): `anchor_male` (default; WAXAL Yoruba speaker 4), `waxal_ibo_5_female`, `waxal_yor_5_female`
30
+ (see --list_voices; recommended: phase3 with a female voice, phase2 with anchor_male). A non-anchor voice is zero-shot: its clip is the flow prompt and the LM prompt, and its
31
+ speaker token is used when the LM was trained on that speaker.
32
+
33
+ python infer.py --text "<eng>this is dynamic accent,</eng> <pcm>e dey sound nice abi?</pcm>" --version phase3 --out out.wav
34
+ python infer.py --text "..." --compare --out cmp.wav # -> cmp_phase2/phase3/flash.wav, same text, seed, voice
35
+ python infer.py --list_voices
36
+ python infer.py --auto_tag --tag_only --text "Omo, this traffic no be here o. Ẹ ṣé púpọ̀." # inspect / edit the tags
37
+
38
+ Text: wrap each language span in <eng>, <pcm>, <yor>, <ibo> or <hau> tags. Untagged text is treated as English, unless
39
+ --auto_tag (automatic word/span language ID, see tagger.py), --force_lang or --lang_map is given.
40
+ `--pause_mode clause` / `--speed` (prosody v2: fixed gaps + WSOLA) are kept for phase2 only as an experiment; a
41
+ listening test preferred the model's own pauses.
42
+ """
43
+
44
+ import argparse, copy, inspect, json, os, re, sys, unicodedata
45
+
46
+ import numpy as np
47
+ import soundfile as sf
48
+ import librosa
49
+ import torch
50
+ import torch.nn as nn
51
+ import contextlib
52
+
53
+ HERE = os.path.dirname(os.path.abspath(__file__))
54
+ LANGS = ["eng", "ibo", "hau", "yor", "pcm"]
55
+ L2I = {l: i for i, l in enumerate(LANGS)}
56
+ ANCHOR = "waxal_yor_4"
57
+ DEV = torch.device("cuda" if torch.cuda.is_available() else "cpu") # set_device() / --device to override
58
+ DEFAULT_SPEED = 1.0 # user listening test 2026-10-21: original model pauses preferred; clause/1.05 kept as experimental
59
+
60
+
61
+ def set_device(d):
62
+ global DEV
63
+ DEV = torch.device(d if d != "auto" else ("cuda" if torch.cuda.is_available() else "cpu"))
64
+ return DEV
65
+
66
+
67
+ def autocast(dtype):
68
+ if DEV.type == "cuda" and dtype is not None:
69
+ return torch.autocast("cuda", dtype=dtype)
70
+ return contextlib.nullcontext() # CPU: plain fp32
71
+
72
+
73
+ # ------------------------------------------------------------------ text front-end (tags -> spans)
74
+ _TAG = re.compile(r"<(eng|ibo|hau|yor|pcm)>(.*?)</\1>", re.S)
75
+ _SENT = re.compile(r"(?<=[.?!])\s+")
76
+
77
+
78
+ def normalize(t):
79
+ t = unicodedata.normalize("NFC", t)
80
+ t = t.replace("\u2019", "'").replace("\u2018", "'").replace("\u201c", '"').replace("\u201d", '"')
81
+ return re.sub(r"\s+", " ", t).strip()
82
+
83
+
84
+ def segments(text, default_lang="eng"):
85
+ out, pos = [], 0
86
+ for m in _TAG.finditer(text):
87
+ pre = text[pos:m.start()]
88
+ if pre.strip():
89
+ out.append((default_lang, pre))
90
+ out.append((m.group(1), m.group(2)))
91
+ pos = m.end()
92
+ if text[pos:].strip():
93
+ out.append((default_lang, text[pos:]))
94
+ res = []
95
+ for l, s in out:
96
+ s = normalize(s)
97
+ if not s:
98
+ continue
99
+ if res:
100
+ s = " " + s
101
+ res.append((l, s))
102
+ return res
103
+
104
+
105
+ def text_tokens(tok, text, default="eng"):
106
+ ids, langs = [], []
107
+ for l, s in segments(text, default):
108
+ t = tok(s, add_special_tokens=False)["input_ids"]
109
+ ids += t; langs += [L2I[l]] * len(t)
110
+ return ids, langs
111
+
112
+
113
+ def split_sentences(text):
114
+ out, cur = [], []
115
+ for l, s in segments(text):
116
+ parts = _SENT.split(s.strip())
117
+ for k, p in enumerate(parts):
118
+ p = p.strip()
119
+ if p:
120
+ cur.append(f"<{l}>{p}</{l}>")
121
+ if (k < len(parts) - 1 or p[-1:] in ".?!") and cur:
122
+ out.append(" ".join(cur)); cur = []
123
+ if cur:
124
+ out.append(" ".join(cur))
125
+ return [o for o in out if o.strip()]
126
+
127
+
128
+ # ------------------------------------------------------------------ silence post-processing
129
+ def frame_db(y, sr, hop_s=0.01):
130
+ hop = int(sr * hop_s)
131
+ n = max(1, len(y) // hop)
132
+ fr = y[: n * hop].reshape(n, hop)
133
+ return 10 * np.log10((fr.astype(np.float64) ** 2).mean(1) + 1e-10)
134
+
135
+
136
+ def silence_mask(y, sr, rel_db=-40.0, hop_s=0.01):
137
+ e = frame_db(y, sr, hop_s)
138
+ return e < np.percentile(e, 98) + rel_db
139
+
140
+
141
+ def runs(mask):
142
+ out, i, n = [], 0, len(mask)
143
+ while i < n:
144
+ if mask[i]:
145
+ j = i
146
+ while j < n and mask[j]:
147
+ j += 1
148
+ out.append((i, j)); i = j
149
+ else:
150
+ i += 1
151
+ return out
152
+
153
+
154
+ def trim_edges(w, sr, keep_lead=0.12, keep_tail=0.18):
155
+ sil = silence_mask(w, sr)
156
+ sp = np.where(~sil)[0]
157
+ if not len(sp):
158
+ return w
159
+ a, b = sp[0] * 0.01, (sp[-1] + 1) * 0.01
160
+ out = w[int(max(0, a - keep_lead) * sr): int(min(len(w) / sr, b + keep_tail) * sr)]
161
+ lead = a - max(0, a - keep_lead)
162
+ if lead < keep_lead:
163
+ out = np.concatenate([np.zeros(int((keep_lead - lead) * sr), out.dtype), out])
164
+ return out
165
+
166
+
167
+ def cap_internal(w, sr, cap=0.6):
168
+ sil = silence_mask(w, sr)
169
+ hop = int(0.01 * sr)
170
+ R = [(a, b) for a, b in runs(sil) if a > 0 and b < len(sil) and (b - a) * 0.01 > cap]
171
+ if not R:
172
+ return w
173
+ keep, cur = [], 0
174
+ for a, b in R:
175
+ keep.append(w[cur: a * hop + int(cap / 2 * sr)]); cur = b * hop - int(cap / 2 * sr)
176
+ keep.append(w[cur:])
177
+ return np.concatenate(keep)
178
+
179
+
180
+
181
+ # ------------------------------------------------------------------ prosody v2: clause chunks, set gaps, pitch-preserving speed
182
+ _PUNCT = re.compile(r"(?<=[,;:.?!])\s+")
183
+ GAPS = {"comma": 0.40, "sent": 0.60, "switch": 0.12}
184
+ JITTER = {"comma": 0.05, "sent": 0.03, "switch": 0.02}
185
+
186
+
187
+ def clause_chunks(text):
188
+ """tagged text -> [(lang, chunk_text, boundary_after)] with boundary in comma|sent|switch|None (None = end).
189
+ Chunks are cut after , ; : . ? ! and at language switches; a one-word chunk is merged into the next chunk of the
190
+ same language (one-word LM calls are unstable), keeping its comma inside."""
191
+ raw = []
192
+ for l, s in segments(text):
193
+ for p in _PUNCT.split(s.strip()):
194
+ if p.strip():
195
+ raw.append([l, p.strip()])
196
+ merged = []
197
+ for c in raw:
198
+ if merged and merged[-1][0] == c[0] and len(merged[-1][1].split()) < 2:
199
+ merged[-1][1] = merged[-1][1] + " " + c[1]
200
+ else:
201
+ merged.append(c)
202
+ out = []
203
+ for i, (l, t) in enumerate(merged):
204
+ if i == len(merged) - 1:
205
+ b = None
206
+ elif t[-1] in ".?!":
207
+ b = "sent"
208
+ elif t[-1] in ",;:":
209
+ b = "comma"
210
+ else:
211
+ b = "switch"
212
+ out.append((l, t, b))
213
+ return out
214
+
215
+
216
+ def trim_tight(w, sr, keep=0.03):
217
+ sil = silence_mask(w, sr)
218
+ sp = np.where(~sil)[0]
219
+ if not len(sp):
220
+ return w
221
+ a, b = sp[0] * 0.01, (sp[-1] + 1) * 0.01
222
+ return w[int(max(0.0, a - keep) * sr): int(min(len(w) / sr, b + keep) * sr)]
223
+
224
+
225
+ def time_stretch(w, sr, speed):
226
+ """pitch-preserving tempo change (speed > 1 = faster). WSOLA via audiotsm, else librosa's phase vocoder."""
227
+ if abs(speed - 1.0) < 1e-3:
228
+ return w
229
+ try:
230
+ from audiotsm import wsola
231
+ from audiotsm.io.array import ArrayReader, ArrayWriter
232
+ r, wr = ArrayReader(w[None].astype(np.float32)), ArrayWriter(1)
233
+ wsola(1, speed=speed, frame_length=int(0.040 * sr), synthesis_hop=int(0.010 * sr)).run(r, wr)
234
+ return wr.data[0].astype(np.float32)
235
+ except ImportError:
236
+ return librosa.effects.time_stretch(w.astype(np.float32), rate=speed)
237
+
238
+
239
+ def fade(w, sr, ms=10):
240
+ n = min(len(w) // 2, int(sr * ms / 1000))
241
+ if n > 0:
242
+ w = w.copy(); r = np.linspace(0, 1, n, dtype=np.float32)
243
+ w[:n] *= r; w[-n:] *= r[::-1]
244
+ return w
245
+
246
+ # ------------------------------------------------------------------ CosyVoice2 base
247
+ def _sf_load_wav(wav, target_sr, min_sr=16000):
248
+ y, sr = sf.read(wav, dtype="float32", always_2d=True)
249
+ y = y.mean(1)
250
+ if sr != target_sr:
251
+ y = librosa.resample(y, orig_sr=sr, target_sr=target_sr)
252
+ return torch.from_numpy(y)[None]
253
+
254
+
255
+ def load_cosy(model_dir, cosyvoice_dir):
256
+ sys.path[:0] = [cosyvoice_dir, os.path.join(cosyvoice_dir, "third_party", "Matcha-TTS")]
257
+ try: # pyworld is only needed for CosyVoice training (F0 features); it often fails
258
+ import pyworld # noqa: F401 # to build, so inference runs without it
259
+ except ImportError:
260
+ import types
261
+ sys.modules["pyworld"] = types.ModuleType("pyworld")
262
+ import cosyvoice.utils.file_utils as fu
263
+ fu.load_wav = _sf_load_wav
264
+ import cosyvoice.cli.frontend as cf
265
+ cf.load_wav = _sf_load_wav
266
+ from cosyvoice.cli.cosyvoice import CosyVoice2
267
+ kw = dict(load_jit=False, load_trt=False, fp16=False)
268
+ sig = inspect.signature(CosyVoice2.__init__).parameters
269
+ return CosyVoice2(model_dir, **{k: v for k, v in kw.items() if k in sig})
270
+
271
+
272
+ def prompt_feats(cosy, wav_path):
273
+ fe = cosy.frontend
274
+ tokn, _ = fe._extract_speech_token(wav_path)
275
+ feat, _ = fe._extract_speech_feat(wav_path)
276
+ n = min(feat.shape[1] // 2, tokn.shape[1])
277
+ feat, tokn = feat[:, :2 * n], tokn[:, :n]
278
+ emb = fe._extract_spk_embedding(wav_path)
279
+ return tokn.to(DEV), feat.to(DEV), emb.to(DEV)
280
+
281
+
282
+ @torch.no_grad()
283
+ def token2wav(cosy, tokens, ptok, pfeat, emb):
284
+ flow = cosy.model.flow
285
+ t = torch.tensor([tokens], dtype=torch.int32).to(DEV)
286
+ kw = dict(token=t, token_len=torch.tensor([t.shape[1]], dtype=torch.int32).to(DEV),
287
+ prompt_token=ptok, prompt_token_len=torch.tensor([ptok.shape[1]], dtype=torch.int32).to(DEV),
288
+ prompt_feat=pfeat, prompt_feat_len=torch.tensor([pfeat.shape[1]], dtype=torch.int32).to(DEV),
289
+ embedding=emb)
290
+ sig = inspect.signature(flow.inference).parameters
291
+ if "streaming" in sig:
292
+ kw["streaming"] = False
293
+ if "finalize" in sig:
294
+ kw["finalize"] = True
295
+ if "flow_cache" in sig:
296
+ kw["flow_cache"] = torch.zeros(1, 80, 0, 2).to(DEV)
297
+ mel = flow.inference(**kw)
298
+ mel = mel[0] if isinstance(mel, tuple) else mel
299
+ hs = inspect.signature(cosy.model.hift.inference).parameters
300
+ w = cosy.model.hift.inference(speech_feat=mel, **({"cache_source": torch.zeros(1, 1, 0).to(DEV)} if "cache_source" in hs else {}))
301
+ w = w[0] if isinstance(w, tuple) else w
302
+ return w.squeeze().float().cpu().numpy()
303
+
304
+
305
+ # ------------------------------------------------------------------ Dynamic Accent LM (Qwen2LM + language + speaker embeddings)
306
+ # S3 tokens that fill silent frames in the anchor's recordings (Phase 3 Stage 0); used by the silence-aware sampler
307
+ SIL_TOKENS = {4137, 4218, 1950, 2031, 3894, 6324, 1707, 3677, 1701, 6405, 1947, 3645, 1944, 1704, 3891, 4134, 3680, 5838, 3651}
308
+ RATES = [None, "very slow", "slow", "normal", "fast", "very fast"]
309
+
310
+
311
+ class DALM2(nn.Module):
312
+ def __init__(self, qlm, n_spk, d=896):
313
+ super().__init__()
314
+ self.q = qlm
315
+ self.lang_emb = nn.Embedding(len(LANGS), d)
316
+ self.spk_emb = nn.Embedding(n_spk, d)
317
+ self.rate_emb = nn.Embedding(len(RATES), d, padding_idx=0) # phase3 speed instruct; zero = none (phase2)
318
+ nn.init.zeros_(self.rate_emb.weight)
319
+ self.adv = nn.Sequential(nn.Linear(d, 256), nn.ReLU(), nn.Linear(256, n_spk)) # training-only head
320
+ self.V = qlm.speech_token_size
321
+
322
+ def embed_text(self, ids, langs):
323
+ return self.q.llm.model.model.embed_tokens(ids) + self.lang_emb(langs)
324
+
325
+ def build2(self, ids, langs, spk, rate=0, pids=None, plangs=None, psp=None):
326
+ sos = self.q.llm_embedding.weight[self.q.sos][None]
327
+ task = self.q.llm_embedding.weight[self.q.task_id][None]
328
+ if pids is not None:
329
+ ids = torch.cat([pids, ids]); langs = torch.cat([plangs, langs])
330
+ sv = self.spk_emb(spk)
331
+ if rate:
332
+ sv = sv + self.rate_emb(torch.tensor(rate, device=sv.device))
333
+ parts = [sos, self.embed_text(ids, langs), sv[None], task]
334
+ if psp is not None:
335
+ parts.append(self.q.speech_embedding(psp))
336
+ return torch.cat(parts, 0)
337
+
338
+ @torch.no_grad()
339
+ def generate2(self, ids, langs, spk, prompt=None, top_p=0.8, top_k=25, win=10, tau_r=0.1, seed=0,
340
+ min_ratio=2.0, max_ratio=20.0, sampler="ras", rate=0, sil_cap=25):
341
+ """sampler 'ras' = Phase 2 repetition-aware sampling; 'silexcap' = silence tokens exempt from the repetition
342
+ check while the current silence run is shorter than sil_cap frames (25 = 1.0 s), then the normal check."""
343
+ g = torch.Generator(device=ids.device).manual_seed(seed)
344
+ x = self.build2(ids, langs, spk, rate, *(prompt if prompt else (None, None, None)))[None]
345
+ n = len(ids)
346
+ min_len, max_len = int(n * min_ratio), int(n * max_ratio)
347
+ past, out, inp = None, [], x
348
+ for i in range(max_len):
349
+ o = self.q.llm.model.model(inputs_embeds=inp, past_key_values=past, use_cache=True, return_dict=True)
350
+ past = o.past_key_values
351
+ logp = self.q.llm_decoder(o.last_hidden_state[:, -1]).float().log_softmax(-1)[0]
352
+ if i < min_len:
353
+ logp[self.V] = -float("inf")
354
+ logp[self.V + 1:] = -float("inf")
355
+ p = logp.exp()
356
+ sp, si = p.sort(descending=True)
357
+ keep = (sp.cumsum(0) - sp) < top_p
358
+ keep[top_k:] = False
359
+ cp, ci = sp[keep], si[keep]
360
+ tokid = int(ci[torch.multinomial(cp / cp.sum(), 1, generator=g)])
361
+ exempt = False
362
+ if sampler == "silexcap" and tokid in SIL_TOKENS:
363
+ run = 0
364
+ for t in reversed(out):
365
+ if t not in SIL_TOKENS:
366
+ break
367
+ run += 1
368
+ exempt = run < sil_cap
369
+ if not exempt and out and sum(1 for t in out[-win:] if t == tokid) >= win * tau_r and tokid != self.V: # RAS
370
+ tokid = int(torch.multinomial(p, 1, generator=g))
371
+ if tokid >= self.V:
372
+ break
373
+ out.append(tokid)
374
+ inp = self.q.speech_embedding.weight[tokid][None, None]
375
+ return out
376
+
377
+
378
+ SETTINGS_PATH = os.path.join(HERE, "settings.json")
379
+ SETTINGS = json.load(open(SETTINGS_PATH)) if os.path.exists(SETTINGS_PATH) else {}
380
+ VERSIONS = {k: {kk: vv for kk, vv in v.items() if kk != "desc"} for k, v in SETTINGS.get("versions", {}).items()} or {
381
+ "phase2": dict(lm="checkpoints/phase2/p2_lm_B/best.pt", flow="checkpoints/phase2/p2_flow_sft/best.pt", sampler="ras",
382
+ one_pass=False, postprocess=True, rate=None),
383
+ "phase3": dict(lm="checkpoints/phase3/p3_lm_full/best.pt", flow="checkpoints/phase2/p2_flow_sft/best.pt",
384
+ sampler="silexcap", one_pass=True, postprocess=False, rate="normal"),
385
+ }
386
+ RECOMMENDED = SETTINGS.get("recommended", {})
387
+
388
+
389
+ def recommendation(version, voice):
390
+ """one-line hint when a version/voice pairing is not the recommended one (None when it is)."""
391
+ rec = RECOMMENDED.get(version)
392
+ if rec and voice not in rec:
393
+ return f"note: {version} is recommended with {' or '.join(rec)} ({RECOMMENDED.get('note', '')})"
394
+ return None
395
+
396
+
397
+ def version_cfg(name):
398
+ cfg = dict(VERSIONS[name])
399
+ cj = os.path.join(HERE, os.path.dirname(cfg["lm"]), "config.json")
400
+ if os.path.exists(cj): # a checkpoint may override flow / rate / sampler
401
+ cfg.update({k: v for k, v in json.load(open(cj)).items() if k in cfg})
402
+ for k in ("lm", "flow", "diacritics"):
403
+ if cfg.get(k):
404
+ cfg[k] = os.path.join(HERE, cfg[k])
405
+ return cfg
406
+
407
+
408
+ # ------------------------------------------------------------------ Flash (CosyVoice3 engine)
409
+ FLASH_VOICE = {"anchor_male": "anchor", "waxal_ibo_5_female": "waxal_ibo_5", "waxal_yor_5_female": "waxal_yor_5"}
410
+
411
+
412
+ def canonical_voice(voice, voices_json=None):
413
+ try:
414
+ al = json.load(open(voices_json, encoding="utf-8")).get("aliases", {}) if voices_json else {}
415
+ except OSError:
416
+ al = {}
417
+ return al.get(voice, voice)
418
+
419
+
420
+ class FlashTTS:
421
+ """--version flash: CosyVoice3 base + Flash-DPO LM (flash_lm.Synth) behind the same say()/sr interface."""
422
+
423
+ def __init__(self, a, cfg):
424
+ if not torch.cuda.is_available():
425
+ raise SystemExit("--version flash needs a CUDA GPU (phase2/phase3 also run on CPU)")
426
+ cv3 = getattr(a, "cv3_dir", "") or os.environ.get("CV3_DIR") or os.path.join(HERE, "pretrained", "Fun-CosyVoice3-0.5B-2512")
427
+ if not os.path.isdir(cv3):
428
+ from huggingface_hub import snapshot_download
429
+ snapshot_download("FunAudioLLM/Fun-CosyVoice3-0.5B-2512", local_dir=cv3)
430
+ os.environ["CV3_DIR"] = cv3
431
+ os.environ.setdefault("COSYVOICE_REPO", a.cosyvoice_dir)
432
+ os.environ.setdefault("DA_PROMPTS", os.path.join(HERE, "prompts"))
433
+ self.instruct = getattr(a, "instruct", None) if cfg.get("instruct") else None
434
+ if cfg.get("instruct"):
435
+ import flash_instruct
436
+ g = a.guidance if getattr(a, "guidance", None) is not None else cfg.get("cfg", 2.0)
437
+ self.S = flash_instruct.ISynth(a.lm or cfg["lm"], cfg=g)
438
+ self.S.text_pauses = cfg.get("text_pauses", True) and not getattr(a, "no_text_pauses", False)
439
+ else:
440
+ if getattr(a, "instruct", None):
441
+ print("note: --instruct is ignored by this version (use --version pro)")
442
+ import flash_lm
443
+ self.S = flash_lm.Synth(a.lm or cfg["lm"])
444
+ self.cosy = self.tok = None # not shareable with the CosyVoice2 versions
445
+ self.m = self.S.m
446
+ self.sr = self.S.cosy.sample_rate
447
+ r = a.rate if a.rate != "default" else cfg.get("rate", "normal")
448
+ self.rate = RATES.index(None if r == "none" else r) if (None if r == "none" else r) in RATES else 0
449
+ self.fe_mode = getattr(a, "frontend", "codeswitch")
450
+ self.fe_lang = getattr(a, "frontend_lang", "eng")
451
+ if self.fe_mode == "translate" and not getattr(a, "experimental_translate", False):
452
+ raise SystemExit("--frontend translate is EXPERIMENTAL and off by default (MADLAD-400 loops on long inputs and is "
453
+ "weak on Pidgin). Add --experimental_translate to use it anyway.")
454
+ from frontend import FrontEnd
455
+ diac = cfg.get("diacritics") if not getattr(a, "no_diacritics", False) else None
456
+ self.fe = FrontEnd(diac if diac and os.path.isdir(diac) else None, mt=self.fe_mode == "translate")
457
+ self.tail_pad = getattr(a, "tail_pad", 0)
458
+ self.set_voice(a.voice, a.voices)
459
+
460
+ def set_voice(self, voice, voices_json=None):
461
+ v = canonical_voice(voice, voices_json)
462
+ if v not in FLASH_VOICE:
463
+ raise SystemExit(f"unknown voice '{voice}' for flash; available: {', '.join(FLASH_VOICE)}")
464
+ self.voice = FLASH_VOICE[v]
465
+
466
+ def frontend(self, text):
467
+ return self.fe(text, self.fe_mode, self.fe_lang)
468
+
469
+ def say(self, tagged_text, seed=0, return_info=False, **_):
470
+ t = self.frontend(tagged_text)
471
+ print(f"[flash front-end] {t}")
472
+ rate = RATES[self.rate]
473
+ ws = []
474
+ for i, ch in enumerate(word_chunks(t)):
475
+ if self.instruct:
476
+ toks = self.S.tokens(ch, self.voice, rate or "none", seed + 1000 * i, instruct=self.instruct)[0]
477
+ else:
478
+ toks = self.S.tokens(ch, self.voice, rate or "none", seed + 1000 * i)[0]
479
+ pad = int(getattr(self, "tail_pad", 0) or 0)
480
+ if pad > 0: # opt-in: trailing silence tokens so the last phone is not cut
481
+ toks = list(toks) + [1] * pad
482
+ ws.append(self.S.wav(toks, self.voice))
483
+ w = np.concatenate(ws).astype(np.float32)
484
+ return (w, None) if return_info else w
485
+
486
+
487
+ def word_chunks(text, max_words=60):
488
+ """one pass per <= max_words, split only at sentence ends (a whole paragraph usually fits in one pass)."""
489
+ sents = split_sentences(text)
490
+ out, cur, n = [], [], 0
491
+ for s in sents:
492
+ w = len(re.sub(r"<[^>]+>", " ", s).split())
493
+ if cur and n + w > max_words:
494
+ out.append(" ".join(cur)); cur, n = [], 0
495
+ cur.append(s); n += w
496
+ if cur:
497
+ out.append(" ".join(cur))
498
+ return out or [text]
499
+
500
+
501
+ class DynamicAccent:
502
+ def __init__(self, model_dir, cosyvoice_dir, lm_ckpt, flow_ckpt, prompts_json, dtype="auto", sent_pause=0.6, cap=0.6,
503
+ sampler="ras", one_pass=False, postprocess=True, rate=None, cosy=None, tok=None, copy_llm=False):
504
+ from transformers import AutoTokenizer
505
+ self.tok = tok or AutoTokenizer.from_pretrained(os.path.join(model_dir, "CosyVoice-BlankEN"))
506
+ self.cosy = cosy or load_cosy(model_dir, cosyvoice_dir)
507
+ self.sr = self.cosy.sample_rate
508
+ self.flow_sd = torch.load(flow_ckpt, map_location=DEV, weights_only=False)["flow"]
509
+ self.cosy.model.flow.load_state_dict(self.flow_sd)
510
+ ck = torch.load(lm_ckpt, map_location=DEV, weights_only=False)
511
+ self.spk2id = json.load(open(os.path.join(os.path.dirname(lm_ckpt), "spk2id.json")))
512
+ # two versions resident at once (app.py) must not share LM weights: copy_llm=True gives each its own copy.
513
+ # The CLI runs versions one after the other and reloads the full LM state in place (saves ~2 GB RAM).
514
+ qlm = copy.deepcopy(self.cosy.model.llm) if copy_llm else self.cosy.model.llm
515
+ self.m = DALM2(qlm, ck["n_spk"]).to(DEV).eval()
516
+ miss = self.m.load_state_dict(ck["model"], strict=False)
517
+ assert not [k for k in miss.missing_keys if not k.startswith("rate_emb")], miss
518
+ del ck
519
+ if DEV.type != "cuda":
520
+ dtype = "fp32"
521
+ elif dtype == "auto":
522
+ dtype = "bf16" if torch.cuda.is_bf16_supported() else "fp16"
523
+ self.dtype = {"bf16": torch.bfloat16, "fp16": torch.float16, "fp32": None}[dtype]
524
+ self.sent_pause, self.cap = sent_pause, cap
525
+ self.sampler, self.one_pass, self.postprocess = sampler, one_pass, postprocess
526
+ self.rate = RATES.index(rate) if rate in RATES else 0
527
+ meta = json.load(open(prompts_json, encoding="utf-8"))
528
+ root = os.path.dirname(os.path.dirname(os.path.abspath(prompts_json)))
529
+ self.prompts = []
530
+ for p in meta["prompts"]:
531
+ ids, langs = text_tokens(self.tok, p["tagged"])
532
+ lmp = (torch.tensor(ids).to(DEV), torch.tensor(langs).to(DEV), torch.tensor(p["s3_tokens"], dtype=torch.int64).to(DEV))
533
+ self.prompts.append(dict(id=p["id"], lm=lmp, flow=prompt_feats(self.cosy, os.path.join(root, p["wav"])), spk=ANCHOR))
534
+ self.voice = None
535
+
536
+ def use_flow(self):
537
+ self.cosy.model.flow.load_state_dict(self.flow_sd) # two versions may share one CosyVoice2 instance
538
+
539
+ def set_voice(self, voice, voices_json=None):
540
+ """'anchor' or an id from voices.json: its clip is the flow + LM prompt (zero-shot)."""
541
+ meta = json.load(open(voices_json, encoding="utf-8")) if voices_json else {"voices": {}}
542
+ voice = meta.get("aliases", {}).get(voice, voice)
543
+ if voice in (None, "anchor", "anchor_male"):
544
+ self.voice = None; return
545
+ if voice not in meta["voices"]:
546
+ raise SystemExit(f"unknown voice '{voice}'; available: {', '.join(meta['voices'])}")
547
+ V = meta["voices"][voice]
548
+ root = os.path.dirname(os.path.dirname(os.path.abspath(voices_json)))
549
+ ptok, pfeat, pemb = prompt_feats(self.cosy, os.path.join(root, V["wav"]))
550
+ ids, langs = text_tokens(self.tok, V["tagged"])
551
+ lmp = (torch.tensor(ids).to(DEV), torch.tensor(langs).to(DEV), ptok[0].long())
552
+ self.voice = dict(id=voice, lm=lmp, flow=(ptok, pfeat, pemb), spk=V["spk"] if V["spk"] in self.spk2id else ANCHOR)
553
+
554
+ def _prompt(self, prompt_k):
555
+ return self.voice or self.prompts[prompt_k % len(self.prompts)]
556
+
557
+ def one(self, tagged_text, seed, prompt_k, use_lm_prompt=True, lm_prompt=None):
558
+ ids, langs = text_tokens(self.tok, tagged_text)
559
+ P = self._prompt(prompt_k)
560
+ with autocast(self.dtype):
561
+ sp = self.m.generate2(torch.tensor(ids).to(DEV), torch.tensor(langs).to(DEV), torch.tensor(self.spk2id[P["spk"]]).to(DEV),
562
+ prompt=(lm_prompt or P["lm"]) if use_lm_prompt else None, seed=seed, sampler=self.sampler,
563
+ rate=self.rate, max_ratio=30.0 if self.one_pass else 20.0)
564
+ if len(sp) < 3:
565
+ sp = sp + [0] * 3
566
+ self.last_tokens = sp
567
+ return token2wav(self.cosy, sp, *P["flow"])
568
+
569
+ def say(self, tagged_text, seed=0, prompt_k=0, stitch=True, pause_mode="model", speed=1.0, return_info=False):
570
+ self.use_flow()
571
+ if self.one_pass: # phase3: the model makes every pause; nothing is trimmed or inserted
572
+ ws, prev = [], None
573
+ for i, ch in enumerate(word_chunks(tagged_text)):
574
+ w = self.one(ch, seed + 1000 * i, prompt_k, lm_prompt=prev)
575
+ ws.append(w)
576
+ ids, langs = text_tokens(self.tok, ch) # next chunk continues from this one (model sets the gap)
577
+ if len(self.last_tokens) <= 400:
578
+ prev = (torch.tensor(ids).to(DEV), torch.tensor(langs).to(DEV), torch.tensor(self.last_tokens).to(DEV))
579
+ w = np.concatenate(ws)
580
+ return (w, None) if return_info else w
581
+ if pause_mode == "clause":
582
+ return self.say_clause(tagged_text, seed, prompt_k, speed, return_info)
583
+ w = self.say_model(tagged_text, seed, prompt_k, stitch)
584
+ w = time_stretch(w, self.sr, speed)
585
+ return (w, None) if return_info else w
586
+
587
+ def say_clause(self, tagged_text, seed=0, prompt_k=0, speed=1.10, return_info=False):
588
+ rng = np.random.default_rng(seed)
589
+ chunks = clause_chunks(tagged_text)
590
+ out, info, lead = [], [], 0.03
591
+ for i, (l, t, b) in enumerate(chunks):
592
+ w = self.one(f"<{l}>{t}</{l}>", seed + 1000 * i, prompt_k)
593
+ w = cap_internal(trim_tight(w, self.sr), self.sr, self.cap)
594
+ w = fade(time_stretch(w, self.sr, speed), self.sr)
595
+ if i == 0:
596
+ out.append(np.zeros(int(0.12 * self.sr), np.float32))
597
+ out.append(w)
598
+ if b is not None:
599
+ g = GAPS[b] + rng.uniform(-JITTER[b], JITTER[b])
600
+ out.append(np.zeros(max(0, int((g - 2 * lead) * self.sr)), np.float32))
601
+ else:
602
+ out.append(np.zeros(int(0.15 * self.sr), np.float32))
603
+ info.append(dict(lang=l, text=t, boundary=b))
604
+ w = np.concatenate(out)
605
+ return (w, info) if return_info else w
606
+
607
+ def say_model(self, tagged_text, seed=0, prompt_k=0, stitch=True):
608
+ sents = split_sentences(tagged_text) if stitch else [tagged_text]
609
+ ws = [cap_internal(trim_edges(self.one(s, seed + 1000 * i, prompt_k), self.sr), self.sr, self.cap)
610
+ for i, s in enumerate(sents)]
611
+ if len(ws) == 1:
612
+ return ws[0]
613
+ out = []
614
+ for i, w in enumerate(ws):
615
+ if i:
616
+ out.append(np.zeros(max(0, int((self.sent_pause - 0.30) * self.sr)), np.float32))
617
+ out.append(w)
618
+ return np.concatenate(out)
619
+
620
+
621
+ def build(version, a, cosy=None, tok=None, copy_llm=False):
622
+ cfg = version_cfg(version)
623
+ if cfg.get("engine") == "cosyvoice3":
624
+ return FlashTTS(a, cfg)
625
+ lm = a.lm or cfg["lm"]; flow = a.flow or cfg["flow"]
626
+ S = DynamicAccent(a.model_dir, a.cosyvoice_dir, lm, flow, a.prompts, a.dtype, sampler=cfg["sampler"],
627
+ one_pass=cfg["one_pass"] and not a.stitch, postprocess=cfg["postprocess"],
628
+ rate=a.rate if a.rate != "default" else cfg["rate"], cosy=cosy, tok=tok, copy_llm=copy_llm)
629
+ S.set_voice(a.voice, a.voices)
630
+ return S
631
+
632
+
633
+ def list_voices(voices_json):
634
+ V = json.load(open(voices_json, encoding="utf-8"))["voices"] if os.path.exists(voices_json) else {}
635
+ print("voices (--voice):")
636
+ for k, v in V.items():
637
+ print(f" {k:20s} {v.get('desc', '')}")
638
+ print("\nversions (--version):")
639
+ for k, v in SETTINGS.get("versions", {}).items():
640
+ print(f" {k:20s} {v.get('desc', '')}")
641
+ if RECOMMENDED:
642
+ print("\nrecommended pairing: " + "; ".join(f"{k} -> {', '.join(v)}" for k, v in RECOMMENDED.items() if isinstance(v, list)))
643
+ print(" " + RECOMMENDED.get("note", ""))
644
+
645
+
646
+ def instruct_lang(instr):
647
+ """single language named in an instruction (None when none or a mix is asked for)."""
648
+ t = instr.lower()
649
+ names = {"igbo": "ibo", "yoruba": "yor", "hausa": "hau", "pidgin": "pcm"}
650
+ hit = [v for k, v in names.items() if k in t]
651
+ if len(hit) == 1 and not any(w in t for w in ("mix", "english", "code-switch", "switch")):
652
+ return hit[0]
653
+ return None
654
+
655
+
656
+ def instruct_gender(instr):
657
+ t = " " + instr.lower() + " "
658
+ if any(w in t for w in (" female", " woman", " woman's", " women")):
659
+ return "female"
660
+ if any(w in t for w in (" male ", " male,", " man ", " man's")):
661
+ return "male"
662
+ return None
663
+
664
+
665
+ def prepare_text(a):
666
+ """apply --auto_tag / --force_lang / --lang_map; returns the tagged text that will be synthesised."""
667
+ from tagger import Tagger, parse_map, TAGGED
668
+ text = a.text
669
+ if a.force_lang:
670
+ from tagger import strip_tags
671
+ return f"<{a.force_lang}>{strip_tags(text)}</{a.force_lang}>"
672
+ if a.auto_tag or a.lang_map:
673
+ if TAGGED.search(text) and not a.auto_tag:
674
+ return text
675
+ from tagger import strip_tags
676
+ T = Tagger(None if a.lid == "none" else a.lid, parse_map(a.lang_map))
677
+ return T.tag(strip_tags(text))
678
+ return text
679
+
680
+
681
+ def main():
682
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
683
+ ap.add_argument("--text", help="text; tagged ('<eng>Good morning,</eng> <pcm>how body?</pcm>') or plain with --auto_tag")
684
+ ap.add_argument("--version", default=SETTINGS.get("default_version", "phase3"), choices=sorted(VERSIONS),
685
+ help=f"model version (default {SETTINGS.get('default_version', 'phase3')})")
686
+ ap.add_argument("--compare", action="store_true",
687
+ help="render the same text, seed and voice with every version in --compare_versions -> <out>_<version>.wav")
688
+ ap.add_argument("--compare_versions", default="flash,pro", help="versions for --compare (comma-separated)")
689
+ ap.add_argument("--instruct", default=None,
690
+ help='pro only: natural-language style instruction, e.g. "Speak quickly with few pauses, lively tone."')
691
+ ap.add_argument("--codeswitch_prompt", default=None,
692
+ help='optional text LLM step: rewrite plain --text as code-switched text, e.g. "mostly English, Igbo greetings"')
693
+ ap.add_argument("--guidance", type=float, default=None,
694
+ help="pro: classifier-free guidance strength on --instruct (default 2.0 from settings.json; 0 = off)")
695
+ ap.add_argument("--no_text_pauses", action="store_true",
696
+ help="pro: do not edit the text for few/many-pause instructions (model-only pause control is weak)")
697
+ ap.add_argument("--cs_llm", default=os.environ.get("CS_LLM", "Qwen/Qwen3-32B"),
698
+ help="text LLM for --codeswitch_prompt (Apache-2.0 Qwen3; Qwen/Qwen3-14B needs less GPU memory)")
699
+ ap.add_argument("--voice", default=SETTINGS.get("default_voice", "anchor_male"),
700
+ help="anchor_male | waxal_ibo_5_female | waxal_yor_5_female (see --list_voices)")
701
+ ap.add_argument("--list_voices", action="store_true", help="list voices, versions and the recommended pairing")
702
+ ap.add_argument("--rate", default="default", choices=["default", "none", "very slow", "slow", "normal", "fast", "very fast"],
703
+ help="phase3/flash speed instruction to the LM (default: the version's setting, 'normal'; 'none' = no instruction)")
704
+ # flash text front-end
705
+ ap.add_argument("--frontend", default="codeswitch", choices=["codeswitch", "native", "accent", "translate"],
706
+ help="flash only: codeswitch (tagged/one-language text), native (whole text in --frontend_lang), accent "
707
+ "(words as written, read with the --frontend_lang accent), translate (EXPERIMENTAL, off by default)")
708
+ ap.add_argument("--frontend_lang", default="eng", choices=LANGS, help="flash front-end language for untagged text / native / accent / translate target")
709
+ ap.add_argument("--no_diacritics", action="store_true", help="flash: do not restore missing Yoruba/Igbo diacritics")
710
+ ap.add_argument("--experimental_translate", action="store_true",
711
+ help="flash: allow --frontend translate (MADLAD-400-3B-MT, ~12 GB download; loops on long inputs, weak on Pidgin)")
712
+ ap.add_argument("--cv3_dir", default=os.environ.get("CV3_DIR", os.path.join(HERE, "pretrained", "Fun-CosyVoice3-0.5B-2512")),
713
+ help="flash: stock FunAudioLLM/Fun-CosyVoice3-0.5B-2512 snapshot (downloaded on first run)")
714
+ # code-switching control
715
+ ap.add_argument("--auto_tag", action="store_true",
716
+ help="tag untagged mixed text automatically (word/span language ID), print it, then synthesise")
717
+ ap.add_argument("--tag_only", action="store_true", help="with --auto_tag: print the tagged text and exit (edit it, then pass it back as --text)")
718
+ ap.add_argument("--force_lang", choices=LANGS, help="read the whole text in one language (tags are ignored)")
719
+ ap.add_argument("--lang_map", default="", help='per-word language overrides for the tagger, e.g. "omo=pcm,sha=pcm"')
720
+ ap.add_argument("--lid", default="none", choices=["none", "glotlid"],
721
+ help="optional LID model for --auto_tag (glotlid: cis-lmu/glotlid, Apache-2.0, 1.7 GB, pip install fasttext)")
722
+ ap.add_argument("--seed", type=int, default=0)
723
+ ap.add_argument("--tail_pad", type=int, default=0,
724
+ help="flash/pro: append N silence speech tokens (25 per second) before the vocoder so the final syllable "
725
+ "is not clipped; 6 removed measured clipped endings in testing (opt-in, default 0)")
726
+ ap.add_argument("--out", default="out.wav")
727
+ ap.add_argument("--prompt_clip", type=int, default=0, help="anchor prompt clip 0-3 (final systems: 0)")
728
+ ap.add_argument("--model_dir", default=os.environ.get("COSYVOICE2_DIR", os.path.join(HERE, "pretrained", "CosyVoice2-0.5B")),
729
+ help="stock FunAudioLLM/CosyVoice2-0.5B snapshot (downloaded on first run)")
730
+ ap.add_argument("--cosyvoice_dir", default=os.environ.get("COSYVOICE_REPO", os.path.join(HERE, "CosyVoice")),
731
+ help="clone of github.com/FunAudioLLM/CosyVoice (with submodules)")
732
+ ap.add_argument("--lm", default="", help="override the version's LM checkpoint")
733
+ ap.add_argument("--flow", default="", help="override the version's flow checkpoint")
734
+ ap.add_argument("--prompts", default=os.path.join(HERE, "prompts", "prompts.json"))
735
+ ap.add_argument("--voices", default=os.path.join(HERE, "prompts", "voices.json"))
736
+ ap.add_argument("--dtype", default="auto", choices=["auto", "bf16", "fp16", "fp32"],
737
+ help="LM autocast dtype on GPU (samples: bf16; auto picks fp16 on GPUs without bf16). CPU always runs fp32")
738
+ ap.add_argument("--stitch", action="store_true", help="phase3: generate sentence by sentence instead of one pass")
739
+ ap.add_argument("--no_stitch", action="store_true", help="phase2: generate multi-sentence text in one pass")
740
+ ap.add_argument("--pause_mode", default="model", choices=["clause", "model"], help="phase2 only; clause = experimental prosody v2 (not recommended)")
741
+ ap.add_argument("--speed", type=float, default=DEFAULT_SPEED, help="phase2 only; pitch-preserving tempo factor (1.0 = as trained; not recommended)")
742
+ a = ap.parse_args()
743
+ a.voice_set = any(x == "--voice" or x.startswith("--voice=") for x in sys.argv)
744
+ a.version_set = any(x == "--version" or x.startswith("--version=") for x in sys.argv)
745
+ if a.instruct and not a.version_set and not a.compare and "pro" in VERSIONS:
746
+ a.version = "pro"; print("[instruct] using --version pro (instruction-controlled)")
747
+ if a.list_voices:
748
+ list_voices(a.voices); return
749
+ if not a.text:
750
+ ap.error("--text is required")
751
+ if a.codeswitch_prompt:
752
+ from cs_frontend import CodeSwitchFrontEnd, TAG
753
+ from tagger import strip_tags
754
+ fe = CodeSwitchFrontEnd(model=a.cs_llm)
755
+ cs_text, info = fe(strip_tags(a.text), a.codeswitch_prompt)
756
+ print(f"[code-switch front end] {cs_text}" + ("" if info["ok"] else f" ({info['warning']})"))
757
+ del fe; torch.cuda.empty_cache()
758
+ a.text = cs_text
759
+ elif a.instruct and not a.force_lang:
760
+ il = instruct_lang(a.instruct)
761
+ from tagger import TAGGED
762
+ if il and not TAGGED.search(a.text) and not a.auto_tag:
763
+ a.force_lang = il # "Speak in Igbo ..." on untagged text -> read it as Igbo
764
+ if a.instruct and not a.voice_set:
765
+ g = instruct_gender(a.instruct)
766
+ if g == "female":
767
+ a.voice = "waxal_ibo_5_female"; print("[instruct] female voice requested -> --voice waxal_ibo_5_female")
768
+ text = prepare_text(a)
769
+ if text != a.text or a.auto_tag:
770
+ print(f"[tagged] {text}")
771
+ if a.tag_only:
772
+ return
773
+ versions = [v.strip() for v in a.compare_versions.split(",") if v.strip()] if a.compare else [a.version]
774
+ bad = [v for v in versions if v not in VERSIONS]
775
+ if bad:
776
+ ap.error(f"unknown version(s) {bad}; available: {sorted(VERSIONS)}")
777
+ if any(VERSIONS[v].get("engine") != "cosyvoice3" for v in versions) and not os.path.isdir(a.model_dir):
778
+ from huggingface_hub import snapshot_download
779
+ snapshot_download("FunAudioLLM/CosyVoice2-0.5B", local_dir=a.model_dir)
780
+ cosy = tok = None
781
+ base, ext = os.path.splitext(a.out)
782
+ for v in versions:
783
+ hint = recommendation(v, json.load(open(a.voices)).get("aliases", {}).get(a.voice, a.voice) if os.path.exists(a.voices) else a.voice)
784
+ if hint:
785
+ print(hint)
786
+ S = build(v, a, cosy, tok)
787
+ if S.cosy is not None:
788
+ cosy, tok = S.cosy, S.tok
789
+ w = S.say(text, seed=a.seed, prompt_k=a.prompt_clip, stitch=not a.no_stitch, pause_mode=a.pause_mode,
790
+ speed=a.speed if v == "phase2" else 1.0)
791
+ out = f"{base}_{v}{ext or '.wav'}" if a.compare else a.out
792
+ sf.write(out, w, S.sr)
793
+ print(f"[{v}] wrote {out} ({len(w) / S.sr:.2f} s, {S.sr} Hz, voice={a.voice}, device={DEV.type})")
794
+ del S.m
795
+ if isinstance(S, FlashTTS):
796
+ del S.S
797
+ del S; torch.cuda.empty_cache()
798
+
799
+
800
+ if __name__ == "__main__":
801
+ main()
instruct_text.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Natural-language instruction templates for Flash-Instruct.
2
+ labels: dict(lang=<ibo|yor|hau|pcm|eng|cs:<l1>+<l2>>, rate=slow|normal|fast|None, pause=few|normal|many|None,
3
+ energy=calm|neutral|lively|None, gender=male|female|None)
4
+ make(labels, rng) -> one instruction string (many surface forms per label combination).
5
+ DEFAULT = the original CosyVoice3 system prompt; ~18% of training items keep it.
6
+ """
7
+ import random
8
+
9
+ DEFAULT = "You are a helpful assistant."
10
+ LN = {"ibo": "Igbo", "yor": "Yoruba", "hau": "Hausa", "pcm": "Nigerian Pidgin", "eng": "Nigerian English"}
11
+
12
+ RATE = {
13
+ "slow": ["slowly", "at an unhurried pace", "at a slow, relaxed pace", "taking your time", "at a gentle, slow pace",
14
+ "a little slower than usual", "slowly and deliberately"],
15
+ "normal": ["at a natural pace", "at a normal speaking pace", "at a steady, everyday pace", "at a comfortable pace"],
16
+ "fast": ["quickly", "at a brisk pace", "fast, like quick everyday conversation", "at a quick pace",
17
+ "a little faster than usual", "briskly"],
18
+ }
19
+ PAUSE = {
20
+ "few": ["with few pauses", "in one smooth flow with hardly any pauses", "without stopping much",
21
+ "with minimal pausing", "running the phrases together"],
22
+ "normal": ["with natural pauses", "with normal phrasing", "pausing where it feels natural"],
23
+ "many": ["with plenty of pauses", "pausing often between phrases", "with frequent pauses",
24
+ "with lots of breaks between phrases", "with deliberate pauses"],
25
+ }
26
+ ENERGY = {
27
+ "calm": ["calm", "soft and calm", "gentle, even-toned", "relaxed and low-key", "quiet and steady"],
28
+ "neutral": ["neutral", "plain, matter-of-fact", "even", "straightforward"],
29
+ "lively": ["lively", "animated", "expressive and energetic", "bright and lively", "upbeat, expressive"],
30
+ }
31
+ GENDER = {"male": ["male", "man's"], "female": ["female", "woman's"]}
32
+
33
+
34
+ def art(w):
35
+ return ("an " if w[:1].lower() in "aeiou" else "a ") + w
36
+
37
+
38
+ def lang_phrase(lang, rng):
39
+ if lang.startswith("cs:"):
40
+ a, b = lang[3:].split("+")
41
+ A, B = LN.get(a, a), LN.get(b, b).replace("Nigerian English", "English")
42
+ return rng.choice([f"an {A}-{B} mix" if A[0] in "AEIOU" else f"a {A}-{B} mix", f"mixed {A} and {B}",
43
+ f"{A} with some {B} mixed in", f"code-switched {A} and {B}"])
44
+ return LN.get(lang, lang)
45
+
46
+
47
+ def make(lab, rng=random):
48
+ parts = {}
49
+ if lab.get("rate"):
50
+ parts["rate"] = rng.choice(RATE[lab["rate"]])
51
+ if lab.get("pause"):
52
+ parts["pause"] = rng.choice(PAUSE[lab["pause"]])
53
+ if lab.get("energy"):
54
+ parts["energy"] = rng.choice(ENERGY[lab["energy"]])
55
+ lang = lang_phrase(lab["lang"], rng) if lab.get("lang") else None
56
+ g = rng.choice(GENDER[lab["gender"]]) if lab.get("gender") else None
57
+ tone = parts.get("energy")
58
+ adv = [parts[k] for k in ("rate", "pause") if k in parts]
59
+ advs = ", ".join(adv)
60
+ T = []
61
+ # template family 1: imperative sentence
62
+ s = "Speak"
63
+ if lang:
64
+ s += f" in {lang}" + (f", sounding {tone}," if tone else "")
65
+ elif tone:
66
+ s += f" in {art(tone)} tone"
67
+ if advs:
68
+ s += f" {advs}"
69
+ T.append(s + ".")
70
+ # family 2: read-this style
71
+ s = "Read this"
72
+ if advs:
73
+ s += f" {advs}"
74
+ if tone:
75
+ s += f", in {art(tone)} voice"
76
+ if lang:
77
+ s += f". The language is {lang}"
78
+ T.append(s + ".")
79
+ # family 3: descriptive/caption style
80
+ d = []
81
+ if g:
82
+ d.append(f"A {g} voice")
83
+ else:
84
+ d.append("A speaker")
85
+ if lang:
86
+ d.append(f"speaking {lang}")
87
+ s = " ".join(d)
88
+ if advs:
89
+ s += f", {advs}"
90
+ if tone:
91
+ s += f"; the delivery is {tone}" if (lang or advs) else f" with {art(tone)} delivery"
92
+ T.append(s + ".")
93
+ # family 4: tag list
94
+ tags = []
95
+ if lang: tags.append(f"language: {lang}")
96
+ if "rate" in parts: tags.append(f"pace: {lab['rate']}")
97
+ if "pause" in parts: tags.append(f"pauses: {lab['pause']}")
98
+ if tone: tags.append(f"tone: {lab['energy']}")
99
+ if g: tags.append(f"voice: {lab['gender']}")
100
+ if tags:
101
+ T.append("Style - " + "; ".join(tags) + ".")
102
+ # family 5: polite request
103
+ s = "Please say this"
104
+ if lang:
105
+ s += f" in {lang}"
106
+ if tone:
107
+ s += f" with {art(tone)} delivery"
108
+ if advs:
109
+ s += f", {advs}"
110
+ T.append(s + ".")
111
+ out = rng.choice(T)
112
+ return out[0].upper() + out[1:]
113
+
114
+
115
+ def sample_labels(full, rng, p=None):
116
+ """drop each control independently so instructions mention subsets; keep at least one."""
117
+ p = p or dict(lang=0.55, rate=0.75, pause=0.7, energy=0.6, gender=0.3)
118
+ while True:
119
+ lab = {k: (v if (v and rng.random() < p.get(k, 0.5)) else None) for k, v in full.items()}
120
+ if any(lab.values()):
121
+ return lab
122
+
123
+
124
+ if __name__ == "__main__":
125
+ r = random.Random(0)
126
+ full = dict(lang="cs:ibo+eng", rate="slow", pause="many", energy="calm", gender="female")
127
+ for _ in range(12):
128
+ print(make(sample_labels(full, r), r))
129
+ for l in ["ibo", "yor", "hau", "pcm", "eng"]:
130
+ print(make(dict(lang=l, rate="fast", pause="few", energy="lively"), r))
pause_text.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Text-side pause control (pipeline step): 'many pauses' -> insert the model's trained [pause] tag at commas and
2
+ every ~5 words inside long phrases; 'few pauses' -> drop commas/semicolons and [pause] tags inside sentences."""
3
+ import re
4
+ TAGRE = re.compile(r"(<\w+>)(.*?)(</\w+>)", re.S)
5
+
6
+
7
+ def _many(s, every=5):
8
+ out = []
9
+ for sent in re.split(r"(?<=[.!?])\s+", s.strip()):
10
+ parts = re.split(r"(,|;|:)", sent)
11
+ buf = []
12
+ for p in parts:
13
+ if p in (",", ";", ":"):
14
+ buf.append(p + " [pause]"); continue
15
+ w = p.split(); chunk = []
16
+ for i, x in enumerate(w):
17
+ chunk.append(x)
18
+ if (i + 1) % every == 0 and i + 1 < len(w) - 1:
19
+ chunk.append("[pause]")
20
+ buf.append(" ".join(chunk))
21
+ out.append(" ".join(b for b in buf if b).replace(" ,", ",").replace(" ;", ";"))
22
+ return " [pause] ".join(out) if len(out) > 1 else out[0]
23
+
24
+
25
+ def _few(s):
26
+ s = s.replace("[pause]", " ").replace("[breath]", " ")
27
+ s = re.sub(r"\s*[,;:]\s*", " ", s)
28
+ return " ".join(s.split())
29
+
30
+
31
+ def apply(text, mode):
32
+ f = _many if mode == "many" else _few if mode == "few" else None
33
+ if f is None:
34
+ return text
35
+ if TAGRE.search(text):
36
+ return TAGRE.sub(lambda m: m.group(1) + f(m.group(2)) + m.group(3), text)
37
+ return f(text)
38
+
39
+
40
+ if __name__ == "__main__":
41
+ t = "<ibo>Ndewo nụ, ụmụnna m.</ibo> <eng>Tonight we will talk about the new market road and the school fees,</eng> <ibo>ma ọ bụrụ na ego ezughị.</ibo>"
42
+ print(apply(t, "many")); print(apply(t, "few"))
prompts/clip0_waxal_yor_yor_1258.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e7e368595eb4c267f124164c7d8a83b03462d549c5efd235dd8ec6eb557dc418
3
+ size 285164
prompts/clip1_waxal_yor_yor_1205.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d8731a9032a1e84a88a2494b65a576794f732cf5c8555c1b94af1a5d370ad7c9
3
+ size 314924
prompts/clip2_waxal_yor_yor_1263.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0cf10737379673b123cee008f78006f4fa2fdabc779563d466f85f0772b9cec5
3
+ size 194924
prompts/clip3_waxal_yor_yor_1218.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5a0e00a437713a35429fe9423c3acf4ec668038e418fc7b85391bec53cb2368a
3
+ size 262124
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>"}
prompts/prompts.json ADDED
@@ -0,0 +1,602 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "anchor_speaker": "waxal_yor_4",
3
+ "source": "WAXAL (CC-BY-SA-4.0), anchor val clips, pause profile B (~/data3)",
4
+ "prompts": [
5
+ {
6
+ "index": 0,
7
+ "id": "waxal_yor_yor_1258",
8
+ "wav": "prompts/clip0_waxal_yor_yor_1258.wav",
9
+ "lang": "yor",
10
+ "text": "Púpọ̀ jùlọ àwọn olókìkí ní ìlú Èkó ń gbé ní erékùṣù náà.",
11
+ "tagged": "<yor>Púpọ̀ jùlọ àwọn olókìkí ní ìlú Èkó ń gbé ní erékùṣù náà.</yor>",
12
+ "dur": 5.94,
13
+ "s3_tokens": [
14
+ 3677,
15
+ 4137,
16
+ 4218,
17
+ 2166,
18
+ 1452,
19
+ 5830,
20
+ 2175,
21
+ 2112,
22
+ 696,
23
+ 5027,
24
+ 4834,
25
+ 2344,
26
+ 1860,
27
+ 5973,
28
+ 1939,
29
+ 1862,
30
+ 1558,
31
+ 3880,
32
+ 316,
33
+ 2621,
34
+ 2864,
35
+ 2864,
36
+ 2891,
37
+ 5051,
38
+ 3412,
39
+ 3888,
40
+ 3888,
41
+ 1458,
42
+ 1701,
43
+ 1950,
44
+ 1973,
45
+ 1946,
46
+ 3890,
47
+ 4133,
48
+ 692,
49
+ 3617,
50
+ 4346,
51
+ 3617,
52
+ 5030,
53
+ 4955,
54
+ 2768,
55
+ 4955,
56
+ 2789,
57
+ 2006,
58
+ 2903,
59
+ 3395,
60
+ 3771,
61
+ 6013,
62
+ 3853,
63
+ 5096,
64
+ 5095,
65
+ 1350,
66
+ 978,
67
+ 2298,
68
+ 5586,
69
+ 1122,
70
+ 1788,
71
+ 2598,
72
+ 295,
73
+ 566,
74
+ 51,
75
+ 3669,
76
+ 3748,
77
+ 2938,
78
+ 4751,
79
+ 4750,
80
+ 4741,
81
+ 2006,
82
+ 1349,
83
+ 1835,
84
+ 1510,
85
+ 1588,
86
+ 2180,
87
+ 5581,
88
+ 4606,
89
+ 2330,
90
+ 1127,
91
+ 573,
92
+ 660,
93
+ 1364,
94
+ 617,
95
+ 2290,
96
+ 5209,
97
+ 2077,
98
+ 2076,
99
+ 2184,
100
+ 967,
101
+ 4616,
102
+ 4607,
103
+ 4526,
104
+ 4769,
105
+ 2591,
106
+ 4526,
107
+ 4579,
108
+ 2446,
109
+ 4137,
110
+ 4137,
111
+ 4137,
112
+ 1959,
113
+ 3670,
114
+ 3671,
115
+ 3751,
116
+ 1479,
117
+ 4769,
118
+ 5021,
119
+ 4994,
120
+ 4751,
121
+ 5561,
122
+ 5561,
123
+ 5236,
124
+ 3255,
125
+ 4218,
126
+ 4218,
127
+ 4218,
128
+ 4137,
129
+ 5352,
130
+ 6081,
131
+ 5379,
132
+ 1006,
133
+ 1034,
134
+ 4679,
135
+ 2960,
136
+ 2411,
137
+ 1207,
138
+ 860,
139
+ 4760,
140
+ 233,
141
+ 1407,
142
+ 654,
143
+ 3567,
144
+ 5826,
145
+ 6310,
146
+ 5481,
147
+ 5563,
148
+ 3348,
149
+ 6556,
150
+ 2102,
151
+ 886,
152
+ 1563,
153
+ 1479,
154
+ 2300,
155
+ 4547,
156
+ 4546,
157
+ 2522,
158
+ 263,
159
+ 263,
160
+ 1225,
161
+ 1953,
162
+ 1947
163
+ ]
164
+ },
165
+ {
166
+ "index": 1,
167
+ "id": "waxal_yor_yor_1205",
168
+ "wav": "prompts/clip1_waxal_yor_yor_1205.wav",
169
+ "lang": "yor",
170
+ "text": "Mejìlá ní ìsọdipúpọ̀ nípasẹ̀ mẹ́rin dọ́gba mejì-mejìlá.",
171
+ "tagged": "<yor>Mejìlá ní ìsọdipúpọ̀ nípasẹ̀ mẹ́rin dọ́gba mejì-mejìlá.</yor>",
172
+ "dur": 6.56,
173
+ "s3_tokens": [
174
+ 3677,
175
+ 1968,
176
+ 1804,
177
+ 1884,
178
+ 1883,
179
+ 2312,
180
+ 4579,
181
+ 3021,
182
+ 1041,
183
+ 5250,
184
+ 4858,
185
+ 80,
186
+ 3799,
187
+ 1532,
188
+ 731,
189
+ 6158,
190
+ 4486,
191
+ 3669,
192
+ 5938,
193
+ 3748,
194
+ 4742,
195
+ 4751,
196
+ 4751,
197
+ 4751,
198
+ 4714,
199
+ 5605,
200
+ 1951,
201
+ 2031,
202
+ 1978,
203
+ 3481,
204
+ 1268,
205
+ 3721,
206
+ 4672,
207
+ 4999,
208
+ 4752,
209
+ 586,
210
+ 2894,
211
+ 2413,
212
+ 1608,
213
+ 2347,
214
+ 1940,
215
+ 1410,
216
+ 2139,
217
+ 6558,
218
+ 5829,
219
+ 2148,
220
+ 2112,
221
+ 1410,
222
+ 5099,
223
+ 5081,
224
+ 2894,
225
+ 707,
226
+ 518,
227
+ 1308,
228
+ 1950,
229
+ 4131,
230
+ 3645,
231
+ 1707,
232
+ 1959,
233
+ 1987,
234
+ 1753,
235
+ 1484,
236
+ 5857,
237
+ 1482,
238
+ 4571,
239
+ 3637,
240
+ 4299,
241
+ 1383,
242
+ 3574,
243
+ 4214,
244
+ 3647,
245
+ 56,
246
+ 5401,
247
+ 6374,
248
+ 1035,
249
+ 32,
250
+ 2303,
251
+ 2303,
252
+ 227,
253
+ 2127,
254
+ 6502,
255
+ 1882,
256
+ 4742,
257
+ 4490,
258
+ 4579,
259
+ 4550,
260
+ 3016,
261
+ 1724,
262
+ 377,
263
+ 377,
264
+ 2456,
265
+ 2456,
266
+ 25,
267
+ 3903,
268
+ 3975,
269
+ 4137,
270
+ 4218,
271
+ 4137,
272
+ 4137,
273
+ 6324,
274
+ 6324,
275
+ 6324,
276
+ 1968,
277
+ 3913,
278
+ 1727,
279
+ 1807,
280
+ 1110,
281
+ 2666,
282
+ 5078,
283
+ 5078,
284
+ 1125,
285
+ 2157,
286
+ 1452,
287
+ 389,
288
+ 5186,
289
+ 2999,
290
+ 2513,
291
+ 2270,
292
+ 3242,
293
+ 5429,
294
+ 5105,
295
+ 5103,
296
+ 2922,
297
+ 1464,
298
+ 1950,
299
+ 4218,
300
+ 1987,
301
+ 1987,
302
+ 1987,
303
+ 1804,
304
+ 6256,
305
+ 1639,
306
+ 2573,
307
+ 4849,
308
+ 4696,
309
+ 1509,
310
+ 1779,
311
+ 4440,
312
+ 5911,
313
+ 4857,
314
+ 4777,
315
+ 323,
316
+ 323,
317
+ 2445,
318
+ 978,
319
+ 1950,
320
+ 1968,
321
+ 1996,
322
+ 1885,
323
+ 1882,
324
+ 4760,
325
+ 4696,
326
+ 3966,
327
+ 5658,
328
+ 4431,
329
+ 5506,
330
+ 78,
331
+ 5986,
332
+ 74,
333
+ 1460,
334
+ 1460,
335
+ 982,
336
+ 1701,
337
+ 1947
338
+ ]
339
+ },
340
+ {
341
+ "index": 2,
342
+ "id": "waxal_yor_yor_1263",
343
+ "wav": "prompts/clip2_waxal_yor_yor_1263.wav",
344
+ "lang": "yor",
345
+ "text": "Ọmọ ọdún méji ni Nóla ní oṣù kẹwàá.",
346
+ "tagged": "<yor>Ọmọ ọdún méji ni Nóla ní oṣù kẹwàá.</yor>",
347
+ "dur": 4.06,
348
+ "s3_tokens": [
349
+ 3677,
350
+ 4218,
351
+ 2057,
352
+ 4217,
353
+ 2762,
354
+ 2127,
355
+ 4314,
356
+ 2852,
357
+ 2771,
358
+ 572,
359
+ 4835,
360
+ 963,
361
+ 1914,
362
+ 954,
363
+ 6554,
364
+ 5041,
365
+ 6502,
366
+ 4068,
367
+ 2420,
368
+ 4858,
369
+ 2265,
370
+ 1995,
371
+ 2022,
372
+ 4440,
373
+ 5262,
374
+ 5506,
375
+ 323,
376
+ 753,
377
+ 1563,
378
+ 750,
379
+ 323,
380
+ 2267,
381
+ 2591,
382
+ 2321,
383
+ 2240,
384
+ 4670,
385
+ 2455,
386
+ 4389,
387
+ 2922,
388
+ 3648,
389
+ 4137,
390
+ 1950,
391
+ 1987,
392
+ 1753,
393
+ 1481,
394
+ 1480,
395
+ 3745,
396
+ 6012,
397
+ 1073,
398
+ 4949,
399
+ 5054,
400
+ 2297,
401
+ 5905,
402
+ 5986,
403
+ 2216,
404
+ 5915,
405
+ 5186,
406
+ 5186,
407
+ 4469,
408
+ 2916,
409
+ 3645,
410
+ 1458,
411
+ 1464,
412
+ 4218,
413
+ 4137,
414
+ 1725,
415
+ 3670,
416
+ 3831,
417
+ 2941,
418
+ 5102,
419
+ 5561,
420
+ 6074,
421
+ 5234,
422
+ 2012,
423
+ 2006,
424
+ 2096,
425
+ 2912,
426
+ 5828,
427
+ 2990,
428
+ 6051,
429
+ 6054,
430
+ 1588,
431
+ 2072,
432
+ 2139,
433
+ 1734,
434
+ 2355,
435
+ 2528,
436
+ 4571,
437
+ 2384,
438
+ 881,
439
+ 644,
440
+ 1456,
441
+ 2185,
442
+ 707,
443
+ 1946,
444
+ 812,
445
+ 3242,
446
+ 5914,
447
+ 4545,
448
+ 1711,
449
+ 2031,
450
+ 6405
451
+ ]
452
+ },
453
+ {
454
+ "index": 3,
455
+ "id": "waxal_yor_yor_1218",
456
+ "wav": "prompts/clip3_waxal_yor_yor_1218.wav",
457
+ "lang": "yor",
458
+ "text": "Ìdajì àti ọ̀kan kẹ́fà dọ́gba méji kẹ́ta.",
459
+ "tagged": "<yor>Ìdajì àti ọ̀kan kẹ́fà dọ́gba méji kẹ́ta.</yor>",
460
+ "dur": 5.46,
461
+ "s3_tokens": [
462
+ 3707,
463
+ 2031,
464
+ 566,
465
+ 566,
466
+ 4778,
467
+ 861,
468
+ 1770,
469
+ 68,
470
+ 3083,
471
+ 5996,
472
+ 4570,
473
+ 777,
474
+ 3939,
475
+ 4683,
476
+ 5169,
477
+ 5911,
478
+ 3239,
479
+ 1052,
480
+ 286,
481
+ 259,
482
+ 5109,
483
+ 2922,
484
+ 2922,
485
+ 3894,
486
+ 3651,
487
+ 2031,
488
+ 1976,
489
+ 3161,
490
+ 3890,
491
+ 3890,
492
+ 2219,
493
+ 1527,
494
+ 2085,
495
+ 6380,
496
+ 6131,
497
+ 5401,
498
+ 1038,
499
+ 2267,
500
+ 4697,
501
+ 4697,
502
+ 4778,
503
+ 4697,
504
+ 4778,
505
+ 5507,
506
+ 5507,
507
+ 5507,
508
+ 3050,
509
+ 3255,
510
+ 3894,
511
+ 4218,
512
+ 4137,
513
+ 1979,
514
+ 1304,
515
+ 1385,
516
+ 2897,
517
+ 601,
518
+ 4299,
519
+ 2031,
520
+ 2754,
521
+ 4869,
522
+ 5030,
523
+ 5030,
524
+ 665,
525
+ 497,
526
+ 1723,
527
+ 2112,
528
+ 762,
529
+ 2271,
530
+ 2213,
531
+ 4523,
532
+ 4853,
533
+ 6372,
534
+ 6534,
535
+ 6534,
536
+ 688,
537
+ 650,
538
+ 1055,
539
+ 1784,
540
+ 3161,
541
+ 2917,
542
+ 2916,
543
+ 1458,
544
+ 1458,
545
+ 1461,
546
+ 2031,
547
+ 1807,
548
+ 1726,
549
+ 1806,
550
+ 1806,
551
+ 873,
552
+ 2657,
553
+ 5081,
554
+ 4852,
555
+ 1941,
556
+ 1888,
557
+ 2181,
558
+ 218,
559
+ 3242,
560
+ 2999,
561
+ 2999,
562
+ 3242,
563
+ 3170,
564
+ 2926,
565
+ 3645,
566
+ 3645,
567
+ 1701,
568
+ 2031,
569
+ 1959,
570
+ 1727,
571
+ 1727,
572
+ 3994,
573
+ 6256,
574
+ 1883,
575
+ 4841,
576
+ 4848,
577
+ 2346,
578
+ 1779,
579
+ 1284,
580
+ 4509,
581
+ 5181,
582
+ 1943,
583
+ 1923,
584
+ 1707,
585
+ 168,
586
+ 4751,
587
+ 2330,
588
+ 62,
589
+ 67,
590
+ 1707,
591
+ 2058,
592
+ 54,
593
+ 2189,
594
+ 3242,
595
+ 2927,
596
+ 2925,
597
+ 1944,
598
+ 4218
599
+ ]
600
+ }
601
+ ]
602
+ }
prompts/voice_waxal_ibo_5.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d537c83dbe167ccf0b43f03304e456fb05e59dfda6b4e359d941900ef629924f
3
+ size 318044
prompts/voice_waxal_yor_5.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ce75b93cf5166d0dc2f53cc943330b4d304f2e060f831d38deb2e664fff49ca5
3
+ size 182924
prompts/voices.json ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "note": "zero-shot voices: the clip is the flow + LM prompt; the speaker token is used when the LM saw that speaker",
3
+ "voices": {
4
+ "anchor_male": {
5
+ "wav": null,
6
+ "lang": "yor",
7
+ "spk": "waxal_yor_4",
8
+ "gender": "male",
9
+ "source": "WAXAL Media Trust TTS, Yoruba speaker 4 (prompts/clip0..3)",
10
+ "licence": "CC-BY-SA-4.0 (Media Trust TTS subset)",
11
+ "desc": "male, WAXAL Yoruba speaker 4: the anchor voice both versions were trained around (uses prompts/prompts.json clip 0)"
12
+ },
13
+ "waxal_ibo_5_female": {
14
+ "wav": "prompts/voice_waxal_ibo_5.wav",
15
+ "tagged": "<ibo>I nwere ike itinye mmemme ọbụla n'izu a ma gị atụla ụjọ maka oké mmiri ozuzo.</ibo>",
16
+ "text": "I nwere ike itinye mmemme ọbụla n'izu a ma gị atụla ụjọ maka oké mmiri ozuzo.",
17
+ "lang": "ibo",
18
+ "spk": "waxal_ibo_5",
19
+ "gender": "female",
20
+ "source_clip": "waxal_ibo_test_ibo_502_c003909",
21
+ "licence": "CC-BY-SA-4.0 (Media Trust TTS subset)",
22
+ "desc": "female native Igbo speaker (WAXAL ibo speaker 5); prompt clip waxal_ibo_test_ibo_502_c003909 (6.625 s)"
23
+ },
24
+ "waxal_yor_5_female": {
25
+ "wav": "prompts/voice_waxal_yor_5.wav",
26
+ "tagged": "<yor>Nínú oṣù Ọpẹ́ ni ó ra mọ́tò tí wọ́n jí gbé.</yor>",
27
+ "text": "Nínú oṣù Ọpẹ́ ni ó ra mọ́tò tí wọ́n jí gbé.",
28
+ "lang": "yor",
29
+ "spk": "waxal_yor_5",
30
+ "gender": "female",
31
+ "source_clip": "waxal_yor_test_yor_686",
32
+ "licence": "CC-BY-SA-4.0 (Media Trust TTS subset)",
33
+ "desc": "female native Yoruba speaker (WAXAL yor speaker 5); prompt clip waxal_yor_test_yor_686 (3.81 s)"
34
+ }
35
+ },
36
+ "aliases": {
37
+ "anchor": "anchor_male",
38
+ "waxal_ibo_5": "waxal_ibo_5_female",
39
+ "waxal_yor_5": "waxal_yor_5_female"
40
+ }
41
+ }