Dynamic Accent Pro v1.1 (experimental prompt-controlled Nigerian TTS); Flash-DPO default, Pro via --instruct
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +16 -0
- LICENSE +428 -0
- LICENSE-CODE +202 -0
- README.md +247 -0
- assets/dynamic_accent_banner.jpg +3 -0
- checkpoints/flash/diacritics_byt5/added_tokens.json +127 -0
- checkpoints/flash/diacritics_byt5/config.json +32 -0
- checkpoints/flash/diacritics_byt5/generation_config.json +7 -0
- checkpoints/flash/diacritics_byt5/model.safetensors +3 -0
- checkpoints/flash/diacritics_byt5/special_tokens_map.json +150 -0
- checkpoints/flash/diacritics_byt5/tokenizer_config.json +1163 -0
- checkpoints/flash/flash_dpo/args.json +1 -0
- checkpoints/flash/flash_dpo/best.pt +3 -0
- checkpoints/flash/flash_dpo/config.json +9 -0
- checkpoints/flash/flash_dpo/log.jsonl +31 -0
- checkpoints/flash/flash_dpo/spk2id.json +0 -0
- checkpoints/pro/flash_instruct/args.json +1 -0
- checkpoints/pro/flash_instruct/best.pt +3 -0
- checkpoints/pro/flash_instruct/config.json +14 -0
- checkpoints/pro/flash_instruct/log.jsonl +16 -0
- checkpoints/pro/flash_instruct/spk2id.json +0 -0
- code/da/__init__.py +0 -0
- code/da/data.py +95 -0
- code/da/model.py +368 -0
- code/da/p2common.py +308 -0
- code/da/prep2.py +259 -0
- code/da/s3_2.py +130 -0
- code/da/text.py +60 -0
- code/train_b.py +353 -0
- code/train_b2.py +590 -0
- config.json +41 -0
- cosyvoice_requirements.txt +42 -0
- cs_frontend.py +187 -0
- flash_instruct.py +433 -0
- flash_lm.py +532 -0
- frontend/__init__.py +2 -0
- frontend/frontend.py +122 -0
- frontend/normalize.py +308 -0
- infer.py +801 -0
- instruct_text.py +130 -0
- pause_text.py +42 -0
- prompts/clip0_waxal_yor_yor_1258.wav +3 -0
- prompts/clip1_waxal_yor_yor_1205.wav +3 -0
- prompts/clip2_waxal_yor_yor_1263.wav +3 -0
- prompts/clip3_waxal_yor_yor_1218.wav +3 -0
- prompts/flash_voices_text.json +3 -0
- prompts/prompts.json +602 -0
- prompts/voice_waxal_ibo_5.wav +3 -0
- prompts/voice_waxal_yor_5.wav +3 -0
- 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
|
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 |
+
}
|