Dynamic ASR v0.1.0: real (default) + codeswitch-experimental
Browse files- LICENSE +428 -0
- LICENSE-CODE +202 -0
- README.md +195 -0
- code/asr_server.py +95 -0
- code/training/build_synth.py +23 -0
- code/training/eval_asr.py +100 -0
- code/training/gen_cs_text.py +61 -0
- code/training/patch_v2.sh +39 -0
- code/training/pipeline2.sh +22 -0
- code/training/prep_data.py +112 -0
- code/training/synth_cs.py +62 -0
- code/training/train_asr.py +185 -0
- codeswitch-experimental/config.json +100 -0
- codeswitch-experimental/model.pt +3 -0
- config.json +297 -0
- real/config.json +99 -0
- real/model.pt +3 -0
- transcribe.py +65 -0
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,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: cc-by-sa-4.0
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
- pcm
|
| 6 |
+
- ig
|
| 7 |
+
- yo
|
| 8 |
+
- ha
|
| 9 |
+
pipeline_tag: automatic-speech-recognition
|
| 10 |
+
library_name: fairseq2
|
| 11 |
+
tags:
|
| 12 |
+
- asr
|
| 13 |
+
- speech-recognition
|
| 14 |
+
- code-switching
|
| 15 |
+
- nigerian-languages
|
| 16 |
+
- igbo
|
| 17 |
+
- yoruba
|
| 18 |
+
- hausa
|
| 19 |
+
- nigerian-pidgin
|
| 20 |
+
- ctc
|
| 21 |
+
- omnilingual-asr
|
| 22 |
+
- streaming
|
| 23 |
+
---
|
| 24 |
+
|
| 25 |
+
# Dynamic ASR
|
| 26 |
+
|
| 27 |
+
**Speech recognition for Nigerian English, Nigerian Pidgin, Igbo, Yoruba and Hausa — and for speech that switches between them inside a sentence.** By [Modular](https://huggingface.co/Modularcomputing), part of the Dynamic Accent project (see also the [Dynamic Accent TTS](https://huggingface.co/Modularcomputing/dynamic-accent-preview) and the [Dynamic Accent Duplex](https://huggingface.co/Modularcomputing/dynamic-accent-duplex) voice agent, which uses this model as its ASR).
|
| 28 |
+
|
| 29 |
+
Dynamic ASR is Meta's [Omnilingual ASR](https://github.com/facebookresearch/omnilingual-asr) `omniASR_CTC_1B_v2` (1B-parameter wav2vec2 CTC, Apache-2.0) fully fine-tuned on **187.2 hours of licensed Nigerian speech**. The architecture, tokenizer and decoding are unchanged, so it is a drop-in replacement: language-agnostic (no language hint), greedy CTC, cheap enough to re-decode a growing buffer for streaming partials.
|
| 30 |
+
|
| 31 |
+
**Status: experimental research release.** Read the limitations.
|
| 32 |
+
|
| 33 |
+
## Two models
|
| 34 |
+
|
| 35 |
+
| folder | what it is | use it when |
|
| 36 |
+
|---|---|---|
|
| 37 |
+
| `real/` (**default**) | fine-tuned on licensed real speech only | general use; as good as the experimental model on real code-switched speech and better on the selection dev set (mean CER 0.071 vs 0.074) |
|
| 38 |
+
| `codeswitch-experimental/` | same recipe, plus ~4.99 h of **synthetic code-switched speech** made with our own Dynamic Accent Flash-DPO TTS (12 % of training audio) | research on synthetic code-switch data; see the ablation below before choosing it |
|
| 39 |
+
|
| 40 |
+
Both are the same size (1B, ~1.95 GB bf16 state dict) and load the same way.
|
| 41 |
+
|
| 42 |
+
## Results
|
| 43 |
+
|
| 44 |
+
All test data is held out (never trained on). Scores are WER / CER after lower-casing and stripping punctuation; "no-diacritic" CER folds tone marks and dots. Decoding: greedy CTC with a Latin-script logit mask (the duplex setting).
|
| 45 |
+
|
| 46 |
+
### Per language (duplex evaluation set: 229 clips — WAXAL test ibo/yor/hau/pcm × 40, FLEURS validation English × 29, 40 clips made by joining two held-out clips of different languages)
|
| 47 |
+
|
| 48 |
+
**CER**
|
| 49 |
+
|
| 50 |
+
| model | English | Pidgin | Igbo | Yoruba | Hausa | code-switch (concatenated) |
|
| 51 |
+
|---|---|---|---|---|---|---|
|
| 52 |
+
| omniASR_CTC_1B (stock; previous duplex ASR) | 0.065 | 0.169 | 0.206 | 0.191 | 0.090 | 0.128 |
|
| 53 |
+
| omniASR_CTC_1B_v2 (stock base) | 0.024 | 0.123 | 0.133 | 0.135 | 0.106 | 0.104 |
|
| 54 |
+
| **Dynamic ASR real** (`real/`) | 0.025 | 0.074 | 0.093 | 0.121 | 0.082 | 0.081 |
|
| 55 |
+
| Dynamic ASR code-switch, experimental (`codeswitch-experimental/`) | 0.024 | 0.076 | 0.095 | 0.118 | 0.081 | 0.072 |
|
| 56 |
+
|
| 57 |
+
**WER**
|
| 58 |
+
|
| 59 |
+
| model | English | Pidgin | Igbo | Yoruba | Hausa | code-switch (concatenated) |
|
| 60 |
+
|---|---|---|---|---|---|---|
|
| 61 |
+
| omniASR_CTC_1B (stock; previous duplex ASR) | 0.144 | 0.380 | 0.636 | 0.498 | 0.299 | 0.330 |
|
| 62 |
+
| omniASR_CTC_1B_v2 (stock base) | 0.097 | 0.336 | 0.456 | 0.401 | 0.317 | 0.292 |
|
| 63 |
+
| **Dynamic ASR real** (`real/`) | 0.095 | 0.193 | 0.315 | 0.355 | 0.253 | 0.238 |
|
| 64 |
+
| Dynamic ASR code-switch, experimental (`codeswitch-experimental/`) | 0.092 | 0.193 | 0.321 | 0.353 | 0.252 | 0.216 |
|
| 65 |
+
|
| 66 |
+
### Real code-switched speech
|
| 67 |
+
|
| 68 |
+
Licensed corpora contain little real code-switched speech, so we built the most honest test we could from held-out data: Igbo/Yoruba/Hausa utterances whose transcripts contain English words. *Natural conversational* = spontaneous Igbo speech with English (IgboSynCorp; those speakers were removed from training). *Insertional* = read Igbo/Yoruba/Hausa sentences with English names/terms (WAXAL test, NaijaS2ST dev). CER (no-diacritic CER in brackets), plus English-word recall (share of the reference's English words that appear in the output; strict exact-match, so low in absolute terms). The last column is a held-out set of synthetic code-switched clips (unseen texts) from the TTS.
|
| 69 |
+
|
| 70 |
+
| model | real CS, all (n=94) | natural conversational Igbo-English (n=36) | insertional, read (n=58) | English-word recall | synthetic CS test (n=109) |
|
| 71 |
+
|---|---|---|---|---|---|
|
| 72 |
+
| omniASR_CTC_1B (stock; previous duplex ASR) | 0.224 (0.195) | 0.372 (0.326) | 0.178 (0.154) | 0.3009 | 0.159 (0.129) |
|
| 73 |
+
| omniASR_CTC_1B_v2 (stock base) | 0.193 (0.172) | 0.353 (0.315) | 0.143 (0.128) | 0.2832 | 0.132 (0.118) |
|
| 74 |
+
| **Dynamic ASR real** (`real/`) | 0.148 (0.137) | 0.248 (0.234) | 0.117 (0.107) | 0.3186 | 0.105 (0.091) |
|
| 75 |
+
| Dynamic ASR code-switch, experimental (`codeswitch-experimental/`) | 0.149 (0.137) | 0.247 (0.234) | 0.119 (0.106) | 0.3805 | 0.038 (0.035) |
|
| 76 |
+
|
| 77 |
+
### Did synthetic code-switched audio help?
|
| 78 |
+
|
| 79 |
+
**Short answer: not on real code-switched speech — at least not measurably at this scale.** The two models share everything (data, seed, 4,000 steps) except that `codeswitch-experimental/` sampled 12 % of its audio from 2572 synthetic code-switched clips (4.99 h).
|
| 80 |
+
|
| 81 |
+
- **Real code-switched speech (94 clips): a tie.** CER 0.148 (`real`) vs 0.149 (`codeswitch-experimental`); natural conversational Igbo–English 0.248 vs 0.247; insertional 0.117 vs 0.119; WER 0.436 vs 0.438. All differences are well inside the noise of a 94-clip test.
|
| 82 |
+
- **English-word recall did go up**, 0.319 → 0.381 (share of the reference's English words the model writes out exactly). This is the one real-speech signal in favour of synthetic data: the model spells embedded English words as English more often. It did not translate into lower CER, and the test is small, so treat it as a lead, not a result.
|
| 83 |
+
- **Per-language real tests: no change.** Differences of at most 0.003 CER on English, Pidgin, Igbo, Yoruba and Hausa (B slightly better on Yoruba/Hausa/English, slightly worse on Pidgin/Igbo). On the selection dev set (WAXAL validation + OpenSLR-70) the experimental model is slightly worse (mean CER 0.074 vs 0.071).
|
| 84 |
+
- **Concatenated code-switch clips (real audio of two languages joined): small gain**, CER 0.081 → 0.072, WER 0.238 → 0.216. These are inter-sentential switches at a hard boundary, closer to what the synthetic data looks like than to natural speech.
|
| 85 |
+
- **Synthetic code-switched test (109 held-out clips, unseen texts): large gain**, CER 0.105 → 0.038 (stock v2 base: 0.132). This mostly measures adaptation to our own TTS voices and recording style, so it is not evidence about real speakers.
|
| 86 |
+
|
| 87 |
+
**Verdict:** synthetic code-switched audio from our TTS teaches the model the synthetic domain and slightly improves English-word spelling, but it does not lower error on real code-switched or monolingual speech. `real/` stays the default. A real natural code-switching test set (and more real code-switched training speech) is what would settle this.
|
| 88 |
+
|
| 89 |
+
### AfriSwitch (in-the-wild code-switched speech) — pending
|
| 90 |
+
|
| 91 |
+
We planned to score both models (and the stock v2 base) zero-shot on the Nigerian subsets of Intron Health's **AfriSwitch** benchmark (Igbo, Yoruba, Hausa, Nigerian Pidgin; evaluation only, never trained on). The dataset is gated with manual approval; we requested access on 2026-10-24 and it had not been granted when the models were finalised, so **no AfriSwitch scores are reported yet**. When we run it we will use our scorer's normalisation (lowercase, punctuation stripped; the paper scores diacritic-preserving WER, and our models drop Igbo tone marks, so Igbo WER will be inflated against its references and we will report the diacritic-folded figure alongside), corpus-level WER and CER.
|
| 92 |
+
|
| 93 |
+
For reference, the paper's zero-shot WER (%) on these subsets (Table 2; systems given the matrix language, diacritics preserved):
|
| 94 |
+
|
| 95 |
+
| language | Sahara V2.5 (best overall, avg 35.93 over 12 languages) | Omnilingual LLM 7B | Gemini 3.6 | ElevenLabs | Sahara V2 |
|
| 96 |
+
|---|---|---|---|---|---|
|
| 97 |
+
| Igbo | 46.94 | 75.52 | 75.88 | 65.37 | 83.58 |
|
| 98 |
+
| Yoruba | 45.54 | 70.27 | 77.26 | 74.62 | 89.66 |
|
| 99 |
+
| Hausa | 31.77 | 39.27 | 45.44 | 41.01 | 45.56 |
|
| 100 |
+
| Nigerian Pidgin | 32.83 | 42.71 | 38.84 | 35.33 | 42.19 |
|
| 101 |
+
|
| 102 |
+
AfriSwitch: Ashungafac, Awobade & Olatunji (Intron Health), *AfriSwitch: A Benchmark for In-the-Wild African Code-Switched Speech Recognition*, arXiv:2608.26434 (2026). Dataset: https://huggingface.co/datasets/intronhealth/AfriSwitch (dataset card licence: CC-BY-NC-SA-4.0; paper CC BY 4.0).
|
| 103 |
+
|
| 104 |
+
```bibtex
|
| 105 |
+
@misc{ashungafac2026afriswitch,
|
| 106 |
+
title={AfriSwitch: A Benchmark for In-the-Wild African Code-Switched Speech Recognition},
|
| 107 |
+
author={Ashungafac, Gabrial Zencha and Awobade, Busayo and Olatunji, Tobi},
|
| 108 |
+
year={2026}, eprint={2608.26434}, archivePrefix={arXiv}, primaryClass={cs.CL}
|
| 109 |
+
}
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
## Usage
|
| 113 |
+
|
| 114 |
+
```bash
|
| 115 |
+
pip install "omnilingual-asr>=0.2.0" huggingface_hub soundfile scipy
|
| 116 |
+
python transcribe.py audio.wav # real/ model (default)
|
| 117 |
+
python transcribe.py audio.wav --model codeswitch # codeswitch-experimental/
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
```python
|
| 121 |
+
from transcribe import DynamicASR
|
| 122 |
+
asr = DynamicASR("real") # downloads real/model.pt; base architecture omniASR_CTC_1B_v2
|
| 123 |
+
print(asr(waveform, sr)) # mono float32 array, any sample rate
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
The checkpoint is a plain PyTorch state dict (`{"model": state_dict, ...}`) for `omniASR_CTC_1B_v2`: `transcribe.py` loads the base through `fairseq2.models.hub.load_model("omniASR_CTC_1B_v2")` and overwrites its weights (the base download is needed once for the model skeleton and tokenizer). On `omnilingual-asr` 0.1.0, run `code/training/patch_v2.sh` (adjust the venv path) to register the v2 card first.
|
| 127 |
+
|
| 128 |
+
**Streaming (duplex agent).** `code/asr_server.py` is the FastAPI worker used by Dynamic Accent Duplex: `POST /asr` with raw float32 16 kHz mono → `{"text", "ms"}`; partial hypotheses come from re-decoding the growing buffer every 0.64 s (~30–90 ms per call on an H100).
|
| 129 |
+
|
| 130 |
+
```bash
|
| 131 |
+
cd code && ASR_MODEL=dynamic-asr uvicorn asr_server:app --port 8002 # real/ (default)
|
| 132 |
+
# ASR_MODEL=dynamic-asr-cs -> codeswitch-experimental/
|
| 133 |
+
# ASR_MODEL=omni -> stock omniASR_CTC_1B (also the automatic fallback if Dynamic ASR cannot load)
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
**Output format:** lower-case, no punctuation, digits as spoken or written in the training transcripts. Igbo is written **without tone marks** (dotted letters ị ọ ụ ṅ kept, standard everyday orthography); Yoruba **with** tone marks; Hausa as in the sources (hooked letters often missing, as in WAXAL).
|
| 137 |
+
|
| 138 |
+
## Training data (licensed only)
|
| 139 |
+
|
| 140 |
+
| source | language | hours used | licence |
|
| 141 |
+
|---|---|---|---|
|
| 142 |
+
| bible_hau | hau | 27.01 | CC-BY-SA-4.0 (Open.Bible / Biblica) |
|
| 143 |
+
| bible_yor | yor | 20.43 | CC-BY-SA-4.0 (Open.Bible / Biblica) |
|
| 144 |
+
| fleurs | eng | 5.63 | CC-BY-4.0 (Google FLEURS) |
|
| 145 |
+
| fleurs | hau | 7.6 | CC-BY-4.0 (Google FLEURS) |
|
| 146 |
+
| fleurs | ibo | 6.09 | CC-BY-4.0 (Google FLEURS) |
|
| 147 |
+
| fleurs | yor | 5.74 | CC-BY-4.0 (Google FLEURS) |
|
| 148 |
+
| igboapi | ibo | 34.98 | CC-BY-4.0 (IgboAPI) |
|
| 149 |
+
| igbosyncorp | ibo | 2.16 | CC0-1.0 (IgboSynCorp) |
|
| 150 |
+
| naijas2st_igbo | ibo | 14.81 | CC-BY-4.0 (NaijaS2ST) |
|
| 151 |
+
| open_bible_igbo | ibo | 22.02 | CC-BY-SA-4.0 (Open.Bible / Biblica) |
|
| 152 |
+
| slr70 | eng | 4.0 | CC-BY-SA-4.0 (OpenSLR 70, Nigerian English) |
|
| 153 |
+
| slr86 | yor | 2.98 | CC-BY-SA-4.0 (OpenSLR 86, Yoruba) |
|
| 154 |
+
| waxal | hau | 6.58 | CC-BY-SA-4.0 (WAXAL Media Trust subsets) |
|
| 155 |
+
| waxal | ibo | 10.58 | CC-BY-SA-4.0 (WAXAL Media Trust subsets) |
|
| 156 |
+
| waxal | pcm | 6.07 | CC-BY-SA-4.0 (WAXAL Media Trust subsets) |
|
| 157 |
+
| waxal | yor | 10.48 | CC-BY-SA-4.0 (WAXAL Media Trust subsets) |
|
| 158 |
+
|
| 159 |
+
Per-epoch sampling down-weights Bible narration and dictionary reads and up-weights Pidgin (≈ 115.6 h per epoch; Igbo 39, Yoruba 26, Hausa 24, Pidgin 18, English 8 h). Held out and never trained on: WAXAL test + validation, FLEURS dev/test, NaijaS2ST dev, and every IgboSynCorp speaker whose speech contained English.
|
| 160 |
+
|
| 161 |
+
**Synthetic code-switched data (`codeswitch-experimental/` only).** 2797 code-switched sentences (English with Igbo/Yoruba/Hausa/Pidgin, Pidgin with Igbo/Yoruba/Hausa; spans tagged per language) written by Qwen3-30B-A3B-Instruct-2507 (Apache-2.0), read by the Dynamic Accent **Flash-DPO** TTS (itself trained on licensed data only) in 32 voices (zero-shot prompts from WAXAL speakers), with random speaking rate (slow/normal/fast), speed perturbation 0.9–1.1, and on-the-fly reverb, phone/cheap-mic band-limiting, clipping and noise (SNR 5–30 dB). Clips the stock model could not read (CER > 0.35, i.e. TTS failures) were dropped: 2572 clips / 4.99 h kept for training, 109 unseen-text clips kept as the synthetic test.
|
| 162 |
+
|
| 163 |
+
**Not used, on licence grounds:** African Voices / WaZoBia, NaijaVoices (non-commercial), AfriSpeech-200 (non-commercial). Common Voice was not used (not fetched). No African Voices–trained TTS was used to make synthetic data.
|
| 164 |
+
|
| 165 |
+
## Training
|
| 166 |
+
|
| 167 |
+
Full fine-tune of all transformer layers (CNN feature encoder frozen), bf16 autocast, AdamW (lr 2e-5, β 0.9/0.98, wd 0.01), 300 warm-up steps then linear decay, 4000 steps × 160 s of audio per batch (~177.8 h of audio seen), CTC loss, LayerDrop 0.1, 15 % light augmentation of real clips. One H100; the two models differ only in the synthetic share (0 % vs 12 %), same seed and steps. Checkpoint selected on a WAXAL-validation/OpenSLR-70 dev set. Training, data-prep, synthesis and evaluation code is in `code/training/`.
|
| 168 |
+
|
| 169 |
+
## Limitations
|
| 170 |
+
|
| 171 |
+
- **Small, read-speech-heavy test sets.** 40 clips per language; real code-switched test = 94 clips. Differences of ~0.01 CER are within noise.
|
| 172 |
+
- **Convention matching is part of the gain.** WAXAL train and test share transcription conventions (e.g. Igbo without tones, Hausa without hooked letters, digits as numerals); some of the improvement on WAXAL tests is learning those conventions, not only better hearing.
|
| 173 |
+
- **Little natural conversational speech** (≈2 h IgboSynCorp in training; none for Yoruba/Hausa). Expect higher error rates on spontaneous, noisy, phone or far-field speech, and on accents unlike the training speakers.
|
| 174 |
+
- **Code-switching is the weakest area.** Real intra-sentential code-switched test data is scarce; the "natural conversational" CER is still ~0.25. English inside Igbo/Yoruba/Hausa is often spelt phonetically.
|
| 175 |
+
- Yoruba tone marks are frequently wrong or missing (Yoruba CER without diacritics is much lower than with); Igbo output has no tone marks by design.
|
| 176 |
+
- Not evaluated yet on the developer's own voice or other unseen real users; not evaluated for fairness across gender/age/region.
|
| 177 |
+
- No punctuation or casing (unlike the stock v2 model's written output).
|
| 178 |
+
- Not for high-stakes use (medical, legal, emergency) without human review.
|
| 179 |
+
|
| 180 |
+
## Licence and attribution
|
| 181 |
+
|
| 182 |
+
- **Weights: CC-BY-SA-4.0.** Several training sources are CC-BY-SA-4.0 (WAXAL Media Trust subsets, Open.Bible, OpenSLR 70/86), so the fine-tuned weights are shared alike under CC-BY-SA-4.0. The base model omniASR_CTC_1B_v2 is Apache-2.0 (© Meta Platforms).
|
| 183 |
+
- **Code (`transcribe.py`, `code/`): Apache-2.0** (`LICENSE-CODE`).
|
| 184 |
+
- Attribution: Google WAXAL (Media Trust subsets), Google FLEURS (CC-BY-4.0), OpenSLR 70 Nigerian English and OpenSLR 86 Yoruba (Google, CC-BY-SA-4.0), Open.Bible / Biblica (CC-BY-SA-4.0), NaijaS2ST (CC-BY-4.0), IgboAPI (CC-BY-4.0), IgboSynCorp (CC0-1.0, Harvard Dataverse doi:10.7910/DVN/YB9FWK), Meta Omnilingual ASR (Apache-2.0), Qwen3 (Apache-2.0).
|
| 185 |
+
|
| 186 |
+
## Citation
|
| 187 |
+
|
| 188 |
+
```bibtex
|
| 189 |
+
@misc{modular2026dynamicasr,
|
| 190 |
+
title = {Dynamic ASR: Nigerian multilingual and code-switched speech recognition},
|
| 191 |
+
author = {Modular (Chima Emmanuel Akuegbo)},
|
| 192 |
+
year = {2026},
|
| 193 |
+
url = {https://huggingface.co/Modularcomputing/dynamic-asr}
|
| 194 |
+
}
|
| 195 |
+
```
|
code/asr_server.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Streaming-capable ASR worker (runs in ~/venv_omni): Meta Omnilingual ASR CTC (Apache-2.0), language-agnostic decoding,
|
| 2 |
+
so code-switched speech needs no language hint. POST /asr body = raw float32 mono 16 kHz -> {"text", "ms"}.
|
| 3 |
+
Partial hypotheses = re-decoding the growing buffer (CTC is cheap: ~10-40 ms per call on H100)."""
|
| 4 |
+
import os, time, numpy as np, torch
|
| 5 |
+
import soundfile # noqa: F401 (loads the bundled libsndfile before fairseq2n looks for it)
|
| 6 |
+
from fastapi import FastAPI, Request
|
| 7 |
+
from omnilingual_asr.models.inference.pipeline import ASRInferencePipeline
|
| 8 |
+
# ASR model selection (env):
|
| 9 |
+
# ASR_MODEL=dynamic-asr (default) -> Dynamic ASR real-speech model (Modularcomputing/dynamic-asr, real/model.pt)
|
| 10 |
+
# ASR_MODEL=dynamic-asr-cs -> Dynamic ASR experimental code-switch model (codeswitch-experimental/model.pt)
|
| 11 |
+
# ASR_MODEL=omni -> stock Meta omniASR (ASR_CARD, default omniASR_CTC_1B)
|
| 12 |
+
# ASR_CKPT=<local .pt> -> any local fine-tuned state_dict on top of ASR_CARD
|
| 13 |
+
# Dynamic ASR = fine-tuned omniASR_CTC_1B_v2 (same CTC architecture; needs omnilingual-asr>=0.2.0, or patch_v2.sh on 0.1.0).
|
| 14 |
+
# If the Dynamic ASR weights cannot be loaded, the worker falls back to stock omniASR_CTC_1B.
|
| 15 |
+
LC = {"ibo": "ibo_Latn", "yor": "yor_Latn", "hau": "hau_Latn", "pcm": "pcm_Latn", "eng": "eng_Latn"}
|
| 16 |
+
import unicodedata, types
|
| 17 |
+
import omnilingual_asr.models.inference.pipeline as PL
|
| 18 |
+
DYN = {"dynamic-asr": "real/model.pt", "dynamic-asr-cs": "codeswitch-experimental/model.pt"}
|
| 19 |
+
MODEL = os.environ.get("ASR_MODEL", "dynamic-asr")
|
| 20 |
+
CKPT = os.environ.get("ASR_CKPT", "")
|
| 21 |
+
CARD = os.environ.get("ASR_CARD", "omniASR_CTC_1B_v2" if (MODEL in DYN or CKPT) else "omniASR_CTC_1B")
|
| 22 |
+
|
| 23 |
+
def _load():
|
| 24 |
+
global CARD, CKPT, MODEL
|
| 25 |
+
if MODEL in DYN:
|
| 26 |
+
CARD = "omniASR_CTC_1B_v2" # Dynamic ASR is always on the v2 base
|
| 27 |
+
if MODEL in DYN and not CKPT:
|
| 28 |
+
try:
|
| 29 |
+
from huggingface_hub import hf_hub_download
|
| 30 |
+
CKPT = hf_hub_download(os.environ.get("ASR_REPO", "Modularcomputing/dynamic-asr"), DYN[MODEL])
|
| 31 |
+
except Exception as e:
|
| 32 |
+
print("Dynamic ASR download failed, falling back to omniASR_CTC_1B:", repr(e)[:200], flush=True)
|
| 33 |
+
CARD, MODEL, CKPT = "omniASR_CTC_1B", "omni", ""
|
| 34 |
+
try:
|
| 35 |
+
pipe = ASRInferencePipeline(model_card=CARD)
|
| 36 |
+
if CKPT:
|
| 37 |
+
ck = torch.load(CKPT, map_location="cpu", weights_only=False)
|
| 38 |
+
print("ASR_CKPT", CKPT, pipe.model.load_state_dict({k: v.to(pipe.dtype) for k, v in ck["model"].items()}), flush=True)
|
| 39 |
+
del ck
|
| 40 |
+
return pipe
|
| 41 |
+
except Exception as e:
|
| 42 |
+
if CARD == "omniASR_CTC_1B":
|
| 43 |
+
raise
|
| 44 |
+
print("Dynamic ASR load failed (", repr(e)[:200], ") -> falling back to omniASR_CTC_1B", flush=True)
|
| 45 |
+
CARD, MODEL, CKPT = "omniASR_CTC_1B", "omni", ""
|
| 46 |
+
return ASRInferencePipeline(model_card=CARD)
|
| 47 |
+
|
| 48 |
+
p = _load()
|
| 49 |
+
LATIN_ONLY = os.environ.get("ASR_LATIN_ONLY", "1") == "1"
|
| 50 |
+
|
| 51 |
+
def _ok(sx):
|
| 52 |
+
for c in sx:
|
| 53 |
+
cat = unicodedata.category(c)
|
| 54 |
+
if cat[0] == "M": # combining marks: only the generic combining diacritics block
|
| 55 |
+
if not ("\u0300" <= c <= "\u036f"):
|
| 56 |
+
return False
|
| 57 |
+
elif c.isalpha() and "LATIN" not in unicodedata.name(c, ""):
|
| 58 |
+
return False
|
| 59 |
+
return True
|
| 60 |
+
|
| 61 |
+
_BAD = {}
|
| 62 |
+
def _apply_latin(self, batch):
|
| 63 |
+
"""CTC greedy decoding restricted to Latin-script tokens: all five target languages are written in Latin script, and the
|
| 64 |
+
language-agnostic CTC otherwise drifts into Arabic/other scripts on accented or synthetic speech."""
|
| 65 |
+
bl = PL.BatchLayout(batch.source_seqs.shape, seq_lens=batch.source_seq_lens, device=batch.source_seqs.device)
|
| 66 |
+
logits, bl_out = self.model(batch.source_seqs, bl)
|
| 67 |
+
V = logits.shape[-1]
|
| 68 |
+
if V not in _BAD:
|
| 69 |
+
bad = [i for i in range(V) if not _ok(self.token_decoder(torch.tensor([i])))]
|
| 70 |
+
_BAD[V] = torch.tensor(bad, dtype=torch.long, device=logits.device); print("latin mask: blocked", len(bad), "of", V, flush=True)
|
| 71 |
+
logits[..., _BAD[V]] = -float("inf")
|
| 72 |
+
pred = torch.argmax(logits, dim=-1); out = []
|
| 73 |
+
for i in range(pred.shape[0]):
|
| 74 |
+
seq = pred[i][: bl_out.seq_lens[i]]; m = torch.ones(seq.shape[0], dtype=torch.bool, device=seq.device); m[1:] = seq[1:] != seq[:-1]
|
| 75 |
+
out.append(self.token_decoder(seq[m]))
|
| 76 |
+
return out
|
| 77 |
+
|
| 78 |
+
if LATIN_ONLY and "CTC" in CARD:
|
| 79 |
+
p._apply_model_wav2vec2asr = types.MethodType(_apply_latin, p)
|
| 80 |
+
app = FastAPI()
|
| 81 |
+
w = np.zeros(16000, np.float32); p.transcribe([{"waveform": w, "sample_rate": 16000}], batch_size=1)
|
| 82 |
+
|
| 83 |
+
@app.post("/asr")
|
| 84 |
+
async def asr(req: Request, lang: str = ""):
|
| 85 |
+
y = np.frombuffer(await req.body(), dtype=np.float32).copy()
|
| 86 |
+
if len(y) < 1600:
|
| 87 |
+
return {"text": "", "ms": 0.0}
|
| 88 |
+
t = time.perf_counter()
|
| 89 |
+
kw = {} if "CTC" in CARD or not lang else dict(lang=[LC.get(lang, "eng_Latn")])
|
| 90 |
+
h = p.transcribe([{"waveform": y, "sample_rate": 16000}], batch_size=1, **kw)[0]
|
| 91 |
+
return {"text": h, "ms": round(1000 * (time.perf_counter() - t), 1), "card": CARD}
|
| 92 |
+
|
| 93 |
+
@app.get("/health")
|
| 94 |
+
def health():
|
| 95 |
+
return {"ok": True, "model": MODEL, "card": CARD, "ckpt": CKPT}
|
code/training/build_synth.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Merge synth parts, split held-out synthetic test (every 25th text id), and (after eval_asr.py --hyp_only) filter TTS failures.
|
| 2 |
+
python3 build_synth.py merge -> /scratch/asr/synth/all.jsonl
|
| 3 |
+
python3 build_synth.py filter -> /scratch/asr/synth/{manifest.jsonl (train), test.jsonl} (keep clips the stock v2 model reads at CER <= 0.35)"""
|
| 4 |
+
import json, glob, sys, collections
|
| 5 |
+
D = "/scratch/asr/synth"
|
| 6 |
+
if sys.argv[1] == "merge":
|
| 7 |
+
R = [json.loads(l) for f in sorted(glob.glob(f"{D}/parts/part_*.jsonl")) for l in open(f)]
|
| 8 |
+
R = [r for r in R if 1.0 <= r["dur"] <= 20 and r["text"]]
|
| 9 |
+
with open(f"{D}/all.jsonl", "w") as f:
|
| 10 |
+
for r in R: f.write(json.dumps(r, ensure_ascii=False) + "\n")
|
| 11 |
+
print("merged", len(R), "hours", round(sum(r["dur"] for r in R) / 3600, 2))
|
| 12 |
+
else:
|
| 13 |
+
R = [json.loads(l) for l in open(f"{D}/all.jsonl")]; H = {json.loads(l)["id"]: json.loads(l) for l in open(f"{D}/all.jsonl.hyp.jsonl")}
|
| 14 |
+
tr, te, drop = [], [], collections.Counter()
|
| 15 |
+
for r in R:
|
| 16 |
+
c = H[r["id"]]["cer"]; r["base_cer"] = c
|
| 17 |
+
if c > 0.35: drop[r["pair"]] += 1; continue
|
| 18 |
+
(te if int(r["id"][2:]) % 25 == 0 else tr).append(r)
|
| 19 |
+
for name, X in (("manifest", tr), ("test", te)):
|
| 20 |
+
with open(f"{D}/{name}.jsonl", "w") as f:
|
| 21 |
+
for r in X: f.write(json.dumps(r, ensure_ascii=False) + "\n")
|
| 22 |
+
print("train", len(tr), round(sum(r["dur"] for r in tr) / 3600, 2), "h; test", len(te), "; dropped (base CER>0.35)", sum(drop.values()), dict(drop))
|
| 23 |
+
print("pairs train", collections.Counter(r["pair"] for r in tr))
|
code/training/eval_asr.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Evaluate an omniASR CTC model (+ optional fine-tuned checkpoint) with the duplex decoding (greedy CTC + Latin-script mask).
|
| 2 |
+
~/venv_omni/bin/python eval_asr.py --tag NAME [--card omniASR_CTC_1B_v2] [--ckpt /scratch/asr/runs/A/best.pt] [--sets duplex,realcs,synthtest,ace]
|
| 3 |
+
--hyp_only MANIFEST : just transcribe a manifest (used for the synthetic-data CER filter) -> MANIFEST.hyp.jsonl
|
| 4 |
+
Writes /scratch/asr/eval/<tag>.json (scores) and <tag>_hyps.jsonl."""
|
| 5 |
+
import argparse, json, os, re, unicodedata, time, numpy as np, torch, torch.nn.functional as F
|
| 6 |
+
import soundfile as sf
|
| 7 |
+
from scipy.signal import resample_poly
|
| 8 |
+
from fairseq2.models.hub import load_model
|
| 9 |
+
from fairseq2.data.tokenizers.hub import load_tokenizer
|
| 10 |
+
from fairseq2.nn.batch_layout import BatchLayout
|
| 11 |
+
import jiwer
|
| 12 |
+
ap = argparse.ArgumentParser()
|
| 13 |
+
ap.add_argument("--tag", default="x"); ap.add_argument("--card", default="omniASR_CTC_1B_v2"); ap.add_argument("--ckpt", default="")
|
| 14 |
+
ap.add_argument("--sets", default="duplex,realcs,synthtest,ace"); ap.add_argument("--hyp_only", default=""); ap.add_argument("--latin", type=int, default=1)
|
| 15 |
+
a = ap.parse_args()
|
| 16 |
+
dev = torch.device("cuda")
|
| 17 |
+
model = load_model(a.card, device=dev, dtype=torch.bfloat16).eval()
|
| 18 |
+
if a.ckpt:
|
| 19 |
+
ck = torch.load(a.ckpt, map_location="cuda", weights_only=False); print("load", a.ckpt, model.load_state_dict(ck["model"]), ck.get("step"), ck.get("dev"))
|
| 20 |
+
tok = load_tokenizer(a.card); dec = tok.create_decoder(skip_special_tokens=True)
|
| 21 |
+
V = model.final_proj.weight.shape[0]
|
| 22 |
+
def _ok(sx):
|
| 23 |
+
for c in sx:
|
| 24 |
+
cat = unicodedata.category(c)
|
| 25 |
+
if cat[0] == "M":
|
| 26 |
+
if not ("\u0300" <= c <= "\u036f"): return False
|
| 27 |
+
elif c.isalpha() and "LATIN" not in unicodedata.name(c, ""): return False
|
| 28 |
+
return True
|
| 29 |
+
BAD = torch.tensor([i for i in range(V) if not _ok(str(dec(torch.tensor([i]))))], dtype=torch.long, device=dev) if a.latin else None
|
| 30 |
+
print("latin mask blocks", 0 if BAD is None else len(BAD), "of", V)
|
| 31 |
+
|
| 32 |
+
def load(r):
|
| 33 |
+
if r.get("wav"):
|
| 34 |
+
y, sr = sf.read(r["wav"], dtype="float32")
|
| 35 |
+
if y.ndim > 1: y = y.mean(1)
|
| 36 |
+
return y if sr == 16000 else resample_poly(y, 16000, sr).astype(np.float32)
|
| 37 |
+
m = np.memmap(r["pcm"], dtype=np.int16, mode="r"); y = m[r["off24"]: r["off24"] + r["n24"]].astype(np.float32) / 32768
|
| 38 |
+
return resample_poly(y, 2, 3).astype(np.float32)
|
| 39 |
+
|
| 40 |
+
@torch.no_grad()
|
| 41 |
+
def transcribe(items, bs=8):
|
| 42 |
+
out, lat = [], []
|
| 43 |
+
order = sorted(range(len(items)), key=lambda i: items[i].get("dur", 0)); res = [None] * len(items)
|
| 44 |
+
for s in range(0, len(order), bs):
|
| 45 |
+
idx = order[s: s + bs]; ys = [torch.from_numpy(load(items[i])) for i in idx]
|
| 46 |
+
ys = [F.layer_norm(y, y.shape) for y in ys]; L = [len(y) for y in ys]
|
| 47 |
+
X = torch.zeros(len(ys), max(L)); [X[k, : len(y)].copy_(y) for k, y in enumerate(ys)]
|
| 48 |
+
X = X.to(dev, torch.bfloat16); bl = BatchLayout(X.shape, seq_lens=L, device=dev)
|
| 49 |
+
lg, blo = model(X, bl)
|
| 50 |
+
if BAD is not None: lg[..., BAD] = -float("inf")
|
| 51 |
+
pr = lg.argmax(-1)
|
| 52 |
+
for k, i in enumerate(idx):
|
| 53 |
+
sq = pr[k, : blo.seq_lens[k]]; m = torch.ones_like(sq, dtype=torch.bool); m[1:] = sq[1:] != sq[:-1]
|
| 54 |
+
res[i] = str(dec(sq[m]))
|
| 55 |
+
return res
|
| 56 |
+
|
| 57 |
+
def n(s):
|
| 58 |
+
s = unicodedata.normalize("NFC", s.lower()); s = re.sub(r"[^\w\s']", " ", s); return re.sub(r"\s+", " ", s).strip()
|
| 59 |
+
def fold(s): return "".join(c for c in unicodedata.normalize("NFD", s) if unicodedata.category(c) != "Mn")
|
| 60 |
+
def score(refs, hyps):
|
| 61 |
+
R = [n(r) or "x" for r in refs]; H = [n(h) or "x" for h in hyps]
|
| 62 |
+
return dict(n=len(R), wer=round(jiwer.wer(R, H), 4), cer=round(jiwer.cer(R, H), 4),
|
| 63 |
+
cer_nodiac=round(jiwer.cer([fold(r) for r in R], [fold(h) for h in H]), 4))
|
| 64 |
+
|
| 65 |
+
if a.hyp_only:
|
| 66 |
+
items = [json.loads(l) for l in open(a.hyp_only)]; hy = transcribe(items, 16)
|
| 67 |
+
with open(a.hyp_only + ".hyp.jsonl", "w") as f:
|
| 68 |
+
for r, h in zip(items, hy):
|
| 69 |
+
f.write(json.dumps(dict(id=r["id"], hyp=h, cer=round(jiwer.cer(n(r["text"]) or "x", n(h) or "x"), 4)), ensure_ascii=False) + "\n")
|
| 70 |
+
print("HYP_DONE", len(items)); raise SystemExit
|
| 71 |
+
|
| 72 |
+
os.makedirs("/scratch/asr/eval", exist_ok=True); res = {}; allh = []
|
| 73 |
+
sets = a.sets.split(",")
|
| 74 |
+
if "duplex" in sets: # the duplex REPORT set: WAXAL test x40 ibo/yor/hau/pcm, FLEURS val eng x29, 40 concatenated cs clips
|
| 75 |
+
R = [json.loads(l) for l in open("/scratch/duplex/asr_eval/list.jsonl")]; H = transcribe(R)
|
| 76 |
+
for lg in ["eng", "pcm", "ibo", "yor", "hau", "cs"]:
|
| 77 |
+
ix = [i for i, r in enumerate(R) if r["lang"] == lg]; res[f"duplex_{lg}"] = score([R[i]["text"] for i in ix], [H[i] for i in ix])
|
| 78 |
+
allh += [dict(set="duplex", id=r["id"], ref=r["text"], hyp=h) for r, h in zip(R, H)]
|
| 79 |
+
if "realcs" in sets: # real speech with English inside Igbo/Yoruba/Hausa sentences (held out)
|
| 80 |
+
R = [json.loads(l) for l in open("/scratch/asr/data/realcs_test.jsonl")]; H = transcribe(R)
|
| 81 |
+
res["realcs_all"] = score([r["text"] for r in R], H)
|
| 82 |
+
for k in ["natural_conversational", "insertional"]:
|
| 83 |
+
ix = [i for i, r in enumerate(R) if r["cs_kind"] == k]; res[f"realcs_{k}"] = score([R[i]["text"] for i in ix], [H[i] for i in ix])
|
| 84 |
+
hit = tot = 0 # English-word recall: share of the English words in the reference that appear in the hypothesis
|
| 85 |
+
for r, h in zip(R, H):
|
| 86 |
+
hw = n(h).split()
|
| 87 |
+
for w in r["cs_eng"]:
|
| 88 |
+
tot += 1; hit += w in hw
|
| 89 |
+
res["realcs_eng_word_recall"] = round(hit / max(1, tot), 4)
|
| 90 |
+
allh += [dict(set="realcs", id=r["id"], ref=r["text"], hyp=h) for r, h in zip(R, H)]
|
| 91 |
+
if "synthtest" in sets and os.path.exists("/scratch/asr/synth/test.jsonl"): # held-out synthetic cs texts (TTS domain)
|
| 92 |
+
R = [json.loads(l) for l in open("/scratch/asr/synth/test.jsonl")]; H = transcribe(R); res["synthtest"] = score([r["text"] for r in R], H)
|
| 93 |
+
allh += [dict(set="synthtest", id=r["id"], ref=r["text"], hyp=h) for r, h in zip(R, H)]
|
| 94 |
+
if "ace" in sets and os.path.exists("/scratch/asr/ace/list.jsonl"): # ACE's own recordings, when available
|
| 95 |
+
R = [json.loads(l) for l in open("/scratch/asr/ace/list.jsonl")]; H = transcribe(R, 1); res["ace"] = score([r["text"] for r in R], H)
|
| 96 |
+
allh += [dict(set="ace", id=r["id"], ref=r["text"], hyp=h) for r, h in zip(R, H)]
|
| 97 |
+
json.dump(dict(tag=a.tag, card=a.card, ckpt=a.ckpt, scores=res), open(f"/scratch/asr/eval/{a.tag}.json", "w"), indent=1)
|
| 98 |
+
with open(f"/scratch/asr/eval/{a.tag}_hyps.jsonl", "w") as f:
|
| 99 |
+
for h in allh: f.write(json.dumps(h, ensure_ascii=False) + "\n")
|
| 100 |
+
print(json.dumps(res, indent=1)); print("EVAL_DONE", a.tag)
|
code/training/gen_cs_text.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Generate intra-sentence code-switched Nigerian sentences (tagged spans) with Qwen3-30B-A3B-Instruct-2507-FP8 (Apache-2.0), offline vLLM.
|
| 2 |
+
-> /scratch/asr/synth/cs_text.jsonl {id, tagged, pair, topic}
|
| 3 |
+
/scratch/vllm/bin/python gen_cs_text.py [n_prompts_per_pair]"""
|
| 4 |
+
import json, os, random, re, sys
|
| 5 |
+
from vllm import LLM, SamplingParams
|
| 6 |
+
random.seed(5)
|
| 7 |
+
OUT = "/scratch/asr/synth"; os.makedirs(OUT, exist_ok=True)
|
| 8 |
+
NAME = {"eng": "English", "ibo": "Igbo", "yor": "Yoruba", "hau": "Hausa", "pcm": "Nigerian Pidgin"}
|
| 9 |
+
# (matrix, embedded): matrix language carries most of the sentence, the other is switched in mid-sentence
|
| 10 |
+
PAIRS = [("ibo", "eng"), ("eng", "ibo"), ("yor", "eng"), ("eng", "yor"), ("hau", "eng"), ("eng", "hau"),
|
| 11 |
+
("pcm", "ibo"), ("pcm", "yor"), ("pcm", "hau"), ("eng", "pcm"), ("pcm", "eng")]
|
| 12 |
+
TOPICS = ["greeting a friend and asking about family", "a phone call about being late", "buying food at the market and bargaining",
|
| 13 |
+
"taking a bus, keke or okada in traffic", "school, exams and lecturers", "work, a boss and a deadline", "church or mosque on the weekend",
|
| 14 |
+
"football, a Premier League match", "a bank transfer that failed", "fixing a phone or laptop", "a wedding or naming ceremony",
|
| 15 |
+
"cooking jollof rice or soup at home", "rain, heat and NEPA light going off", "a hospital visit", "a WhatsApp group chat argument",
|
| 16 |
+
"a job interview", "rent and the landlord", "music, Afrobeats and a concert", "a birthday party plan", "giving directions in town",
|
| 17 |
+
"asking an AI assistant for help", "a business deal with a customer", "complaining about fuel prices", "studying for JAMB or WAEC",
|
| 18 |
+
"a village visit for Christmas", "a doctor's advice", "ordering food delivery", "a startup and a product launch"]
|
| 19 |
+
STY = ["casual and friendly", "excited", "polite and respectful", "complaining", "joking", "in a hurry", "telling a story"]
|
| 20 |
+
|
| 21 |
+
def prompt(m, e, topic, sty, n=15):
|
| 22 |
+
sw = (f"Most of each sentence is in {NAME[m]}, with {NAME[e]} words or phrases switched in mid-sentence, the way Nigerians really talk."
|
| 23 |
+
if m != "eng" else f"Most of each sentence is in English, with {NAME[e]} words, phrases or clauses switched in mid-sentence, the way Nigerians really talk.")
|
| 24 |
+
return (f"Write {n} different spoken sentences a Nigerian might say about: {topic}. Tone: {sty}. {sw}\n"
|
| 25 |
+
f"Rules: every sentence switches language at least once INSIDE the sentence (not just at the start or end); 6 to 24 words; "
|
| 26 |
+
f"natural everyday speech; use correct standard spelling ({'Yoruba with tone marks and dots' if 'yor' in (m, e) else ''}"
|
| 27 |
+
f"{'Igbo with dotted vowels (ị ọ ụ ṅ)' if 'ibo' in (m, e) else ''}{'Hausa with hooked letters (ɓ ɗ ƙ ƴ) where needed' if 'hau' in (m, e) else ''}); "
|
| 28 |
+
f"write numbers as words; no emojis, no hashtags, no quotation marks.\n"
|
| 29 |
+
f"Wrap every span in its language tag: <{m}>...</{m}> and <{e}>...</{e}>. Example format: "
|
| 30 |
+
f"<{m}>...</{m}> <{e}>...</{e}> <{m}>...</{m}>\n"
|
| 31 |
+
f"Output ONLY a JSON array of {n} strings.")
|
| 32 |
+
|
| 33 |
+
N = int(sys.argv[1]) if len(sys.argv) > 1 else 45
|
| 34 |
+
jobs = []
|
| 35 |
+
for m, e in PAIRS:
|
| 36 |
+
k = N if "pcm" not in (m, e) or (m, e) in (("pcm", "eng"), ("eng", "pcm")) else int(N * 0.6)
|
| 37 |
+
for i in range(k):
|
| 38 |
+
t, s = random.choice(TOPICS), random.choice(STY); jobs.append(dict(pair=f"{m}+{e}", topic=t, msg=prompt(m, e, t, s)))
|
| 39 |
+
llm = LLM("Qwen/Qwen3-30B-A3B-Instruct-2507-FP8", gpu_memory_utilization=float(os.environ.get("GPU_UTIL", "0.42")), max_model_len=4096,
|
| 40 |
+
enforce_eager=False)
|
| 41 |
+
sp = SamplingParams(temperature=0.9, top_p=0.95, max_tokens=1800, seed=0)
|
| 42 |
+
outs = llm.chat([[{"role": "user", "content": j["msg"]}] for j in jobs], sp)
|
| 43 |
+
TAG = re.compile(r"<(eng|ibo|yor|hau|pcm)>(.*?)</\1>", re.S)
|
| 44 |
+
seen, n_ok, n_bad = set(), 0, 0
|
| 45 |
+
with open(f"{OUT}/cs_text.jsonl", "w") as f:
|
| 46 |
+
for j, o in zip(jobs, outs):
|
| 47 |
+
txt = o.outputs[0].text; i0, i1 = txt.find("["), txt.rfind("]")
|
| 48 |
+
try: arr = json.loads(txt[i0: i1 + 1])
|
| 49 |
+
except Exception: n_bad += 1; continue
|
| 50 |
+
for s in arr:
|
| 51 |
+
if not isinstance(s, str): continue
|
| 52 |
+
spans = [(l, re.sub(r"\s+", " ", t).strip()) for l, t in TAG.findall(s)]
|
| 53 |
+
spans = [(l, t) for l, t in spans if t]
|
| 54 |
+
langs = [l for l, _ in spans]; nw = sum(len(t.split()) for _, t in spans)
|
| 55 |
+
if len(spans) < 2 or len(set(langs)) < 2 or not (5 <= nw <= 30) or set(langs) - set(j["pair"].split("+")): n_bad += 1; continue
|
| 56 |
+
if any(len(t) > 200 for _, t in spans) or re.search(r"[<>\"#@]", "".join(t for _, t in spans)): n_bad += 1; continue
|
| 57 |
+
key = " ".join(t for _, t in spans).lower()
|
| 58 |
+
if key in seen: continue
|
| 59 |
+
seen.add(key); tagged = " ".join(f"<{l}>{t}</{l}>" for l, t in spans)
|
| 60 |
+
f.write(json.dumps(dict(id=f"cs{n_ok:05d}", tagged=tagged, pair=j["pair"], topic=j["topic"]), ensure_ascii=False) + "\n"); n_ok += 1
|
| 61 |
+
print("CS_TEXT_DONE ok", n_ok, "bad", n_bad, "prompts", len(jobs))
|
code/training/patch_v2.sh
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# register omniASR_CTC_1B_v2 (arch 1b_v2 = 1b with vocab 10288, tokenizer omniASR_tokenizer_written_v2) in venv_omni 0.1.0
|
| 3 |
+
P=/home/dev/venv_omni/lib/python3.12/site-packages/omnilingual_asr
|
| 4 |
+
grep -q '"1b_v2"' $P/models/wav2vec2_asr/config.py || python3 - <<'PY'
|
| 5 |
+
p="/home/dev/venv_omni/lib/python3.12/site-packages/omnilingual_asr/models/wav2vec2_asr/config.py"
|
| 6 |
+
s=open(p).read()
|
| 7 |
+
add='''
|
| 8 |
+
@arch("1b_v2", advanced=True)
|
| 9 |
+
def _1b_asr_v2(resolver: DependencyResolver) -> Wav2Vec2AsrConfig:
|
| 10 |
+
config = get_config(resolver, Wav2Vec2AsrConfig, "base_10h")
|
| 11 |
+
config.encoder_config = get_config(resolver, Wav2Vec2Config, "1b").encoder_config
|
| 12 |
+
config.encoder_config.dropout_p = 0.0
|
| 13 |
+
config.encoder_config.attn_dropout_p = 0.0
|
| 14 |
+
config.encoder_config.ffn_inner_dropout_p = 0.1
|
| 15 |
+
config.encoder_config.layer_drop_p = 0.1
|
| 16 |
+
config.use_masking = False
|
| 17 |
+
config.max_temporal_mask_prob = 0.0
|
| 18 |
+
config.max_spatial_mask_prob = 0.0
|
| 19 |
+
config.target_vocab_size = 10288
|
| 20 |
+
return config
|
| 21 |
+
'''
|
| 22 |
+
i=s.index(' @arch("1b", advanced=True)')
|
| 23 |
+
s=s[:i]+add.lstrip("\n")+"\n"+s[i:]
|
| 24 |
+
open(p,"w").write(s); print("arch patched")
|
| 25 |
+
PY
|
| 26 |
+
cat > $P/cards/models/rc_models_v2_local.yaml <<'Y'
|
| 27 |
+
name: omniASR_tokenizer_written_v2
|
| 28 |
+
tokenizer_family: char_tokenizer
|
| 29 |
+
tokenizer: https://dl.fbaipublicfiles.com/mms/omniASR_tokenizer_written_v2.model
|
| 30 |
+
|
| 31 |
+
---
|
| 32 |
+
|
| 33 |
+
name: omniASR_CTC_1B_v2
|
| 34 |
+
model_family: wav2vec2_asr
|
| 35 |
+
model_arch: 1b_v2
|
| 36 |
+
checkpoint: https://dl.fbaipublicfiles.com/mms/omniASR-CTC-1B-v2.pt
|
| 37 |
+
tokenizer_ref: omniASR_tokenizer_written_v2
|
| 38 |
+
Y
|
| 39 |
+
echo card written
|
code/training/pipeline2.sh
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# After A_real finishes: baseline/A evals, finish synthesis (deadline), filter, train B_synth, final evals.
|
| 3 |
+
. ~/asr/env.sh; cd ~/asr; L=/scratch/logs; E="PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True"
|
| 4 |
+
while pgrep -f "runs/A_real " >/dev/null || pgrep -f "runs/A_real$" >/dev/null || pgrep -f "train_asr.py --run /scratch/asr/runs/A_real" >/dev/null; do sleep 30; done
|
| 5 |
+
echo "A finished $(date -u)"; grep -E "TRAIN_DONE|Error" $L/train_A.log | tail -2
|
| 6 |
+
nohup ~/venv/bin/python -u synth_cs.py 4 5 > $L/synth_4.log 2>&1 &
|
| 7 |
+
~/venv_omni/bin/python -u eval_asr.py --tag v1_stock --card omniASR_CTC_1B --sets duplex,realcs > $L/eval_v1.log 2>&1; echo "eval v1 $?"
|
| 8 |
+
~/venv_omni/bin/python -u eval_asr.py --tag v2_stock --card omniASR_CTC_1B_v2 --sets duplex,realcs > $L/eval_v2.log 2>&1; echo "eval v2 $?"
|
| 9 |
+
~/venv_omni/bin/python -u eval_asr.py --tag A_real --ckpt /scratch/asr/runs/A_real/best.pt --sets duplex,realcs > $L/eval_A.log 2>&1; echo "eval A $?"
|
| 10 |
+
DL=$(date -u -d "2026-10-24 18:40" +%s)
|
| 11 |
+
while [ $(date -u +%s) -lt $DL ]; do
|
| 12 |
+
n=$(grep -l SYNTH_DONE $L/synth_[0-4].log 2>/dev/null | wc -l); [ "$n" -ge 5 ] && break; sleep 30; done
|
| 13 |
+
pkill -f synth_cs.py; sleep 5; echo "synth stopped $(date -u)"
|
| 14 |
+
python3 build_synth.py merge
|
| 15 |
+
~/venv_omni/bin/python -u eval_asr.py --hyp_only /scratch/asr/synth/all.jsonl > $L/synth_hyp.log 2>&1; echo "hyp $?"
|
| 16 |
+
python3 build_synth.py filter
|
| 17 |
+
env PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True MEM_FRAC=0.9 ~/venv_omni/bin/python -u train_asr.py --run /scratch/asr/runs/B_synth --steps 4000 --batch_sec 160 --max_sec 18 --val_every 500 \
|
| 18 |
+
--synth /scratch/asr/synth/manifest.jsonl --synth_frac 0.12 > $L/train_B.log 2>&1; echo "train B $?"
|
| 19 |
+
~/venv_omni/bin/python -u eval_asr.py --tag B_synth --ckpt /scratch/asr/runs/B_synth/best.pt > $L/eval_B.log 2>&1; echo "eval B $?"
|
| 20 |
+
for t in "v1_stock --card omniASR_CTC_1B" "v2_stock --card omniASR_CTC_1B_v2" "A_real --ckpt /scratch/asr/runs/A_real/best.pt"; do
|
| 21 |
+
set -- $t; ~/venv_omni/bin/python -u eval_asr.py --tag ${1}_st $2 $3 --sets synthtest,ace > $L/eval_st_$1.log 2>&1; echo "eval st $1 $?"; done
|
| 22 |
+
echo PIPE_DONE $(date -u)
|
code/training/prep_data.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Build ASR fine-tuning manifests from licensed, public-safe Nigerian speech already on dynacc-h100.
|
| 2 |
+
-> /scratch/asr/data/{train.jsonl, dev.jsonl, realcs_test.jsonl, stats.json}
|
| 3 |
+
Each row: id, src, lang, spk, text (ASR target, normalized), dur, pcm (24 kHz int16 file), off24, n24, licence.
|
| 4 |
+
Excluded (never touched): African Voices / WaZoBia, NaijaVoices, AfriSpeech (NC), Flash-AV. Held out: WAXAL test+validation,
|
| 5 |
+
FLEURS dev/test/val, NaijaS2ST dev, IgboSynCorp speakers that produced natural code-switched clips (-> realcs test).
|
| 6 |
+
Target normalization (matches duplex asr_score.norm): NFC, lowercase, punctuation -> space (apostrophes kept);
|
| 7 |
+
Igbo: tone marks (acute/grave/macron) stripped, dot-below and n-dot kept (WAXAL/standard Igbo orthography);
|
| 8 |
+
Yoruba/Hausa: kept as written by the source."""
|
| 9 |
+
import json, os, re, unicodedata, random, collections
|
| 10 |
+
H = "/home/dev"; OUT = "/scratch/asr/data"; os.makedirs(OUT, exist_ok=True)
|
| 11 |
+
random.seed(11)
|
| 12 |
+
LIC = {"waxal": "CC-BY-SA-4.0 (WAXAL Media Trust subsets)", "fleurs": "CC-BY-4.0 (Google FLEURS)",
|
| 13 |
+
"slr70": "CC-BY-SA-4.0 (OpenSLR 70, Nigerian English)", "slr86": "CC-BY-SA-4.0 (OpenSLR 86, Yoruba)",
|
| 14 |
+
"bible_yor": "CC-BY-SA-4.0 (Open.Bible / Biblica)", "bible_hau": "CC-BY-SA-4.0 (Open.Bible / Biblica)",
|
| 15 |
+
"open_bible_igbo": "CC-BY-SA-4.0 (Open.Bible / Biblica)", "igboapi": "CC-BY-4.0 (IgboAPI)",
|
| 16 |
+
"naijas2st_igbo": "CC-BY-4.0 (NaijaS2ST)", "igbosyncorp": "CC0-1.0 (IgboSynCorp)"}
|
| 17 |
+
# per-source sampling multiplier per epoch (Bible + dictionary reads down-weighted, Pidgin up-weighted)
|
| 18 |
+
W = {"waxal:ibo": 1.0, "waxal:yor": 1.0, "waxal:hau": 1.0, "waxal:pcm": 3.0, "fleurs:eng": 1.0, "fleurs:hau": 1.0,
|
| 19 |
+
"fleurs:ibo": 1.0, "fleurs:yor": 1.0, "slr70:eng": 0.6, "slr86:yor": 1.0, "bible_yor:yor": 0.35, "bible_hau:hau": 0.35,
|
| 20 |
+
"open_bible_igbo:ibo": 0.15, "igboapi:ibo": 0.25, "naijas2st_igbo:ibo": 0.5, "igbosyncorp:ibo": 1.5}
|
| 21 |
+
TONE = {"\u0300", "\u0301", "\u0304", "\u0302", "\u030c"}
|
| 22 |
+
|
| 23 |
+
def norm(s, lang):
|
| 24 |
+
s = re.sub(r"<[^>]+>", " ", s); s = re.sub(r"\[\d+\]", " ", s)
|
| 25 |
+
s = unicodedata.normalize("NFC", s.lower())
|
| 26 |
+
if lang == "ibo":
|
| 27 |
+
s = unicodedata.normalize("NFC", "".join(c for c in unicodedata.normalize("NFD", s) if c not in TONE))
|
| 28 |
+
s = s.replace("\u2019", "'").replace("`", "'")
|
| 29 |
+
s = re.sub(r"[^\w\s']", " ", s); s = re.sub(r"(^|\s)'+|'+(\s|$)", " ", s)
|
| 30 |
+
return re.sub(r"\s+", " ", s).strip()
|
| 31 |
+
|
| 32 |
+
def load(d):
|
| 33 |
+
return [json.loads(l) for l in open(f"{d}/meta.jsonl")]
|
| 34 |
+
|
| 35 |
+
rows = []
|
| 36 |
+
for ds in ["waxal", "fleurs", "slr70", "slr86", "bible_yor", "bible_hau"]:
|
| 37 |
+
d = f"{H}/data3/{ds}"
|
| 38 |
+
for r in load(d):
|
| 39 |
+
rows.append(dict(id=r["id"], src=ds, lang=r["lang"], spk=r.get("spk"), split=r.get("split"), raw=r["text"], dur=r["dur"],
|
| 40 |
+
pcm=f"{d}/pcm24k.i16", off24=r["off24"], n24=r["n24"]))
|
| 41 |
+
for ds in ["open_bible_igbo", "igboapi", "naijas2st_igbo", "igbosyncorp"]:
|
| 42 |
+
d = f"{H}/data_p3/p3set/{ds}"
|
| 43 |
+
for r in load(d):
|
| 44 |
+
rows.append(dict(id=r["id"], src=ds, lang=r["lang"], spk=r.get("speaker"), split=r.get("split"), raw=r.get("text_plain") or r.get("text_human"),
|
| 45 |
+
dur=r.get("dur24") or r["dur"], pcm=f"{d}/pcm24k.i16", off24=r["off24"], n24=r["n24"]))
|
| 46 |
+
for r in rows:
|
| 47 |
+
r["text"] = norm(r["raw"] or "", r["lang"]); r["licence"] = LIC[r["src"]]
|
| 48 |
+
print("all rows", len(rows), collections.Counter((r["src"], r["split"]) for r in rows))
|
| 49 |
+
|
| 50 |
+
# English lexicon (for detecting real English insertions in held-out Igbo/Yoruba/Hausa speech)
|
| 51 |
+
ec, nc = collections.Counter(), collections.Counter()
|
| 52 |
+
for r in rows:
|
| 53 |
+
if r["split"] != "train": continue
|
| 54 |
+
(ec if r["lang"] == "eng" else nc if r["lang"] in ("ibo", "yor", "hau") else collections.Counter()).update(r["text"].split())
|
| 55 |
+
ENG = {w for w, c in ec.items() if c >= 2 and len(w) >= 3 and w.isascii() and w.isalpha() and nc[w] <= max(1, c // 50)}
|
| 56 |
+
def eng_words(t):
|
| 57 |
+
return [w for w in t.split() if w in ENG]
|
| 58 |
+
|
| 59 |
+
# real code-switch test: held-out ibo/yor/hau clips with >=1 English word (insertional CS, real speech) + IgboSynCorp natural CS
|
| 60 |
+
realcs, hold_spk = [], set()
|
| 61 |
+
for r in rows:
|
| 62 |
+
if r["lang"] not in ("ibo", "yor", "hau") or not r["text"]: continue
|
| 63 |
+
ew = eng_words(r["text"]); nw = len(r["text"].split())
|
| 64 |
+
if r["src"] == "igbosyncorp" and len(ew) >= 2 and nw >= 3:
|
| 65 |
+
hold_spk.add(r["spk"])
|
| 66 |
+
if r["split"] in ("test", "dev") and r["src"] in ("waxal", "naijas2st_igbo") and ew and nw >= 4 and len(ew) < nw:
|
| 67 |
+
realcs.append(dict(r, cs_eng=ew, cs_kind="insertional"))
|
| 68 |
+
for r in rows:
|
| 69 |
+
if r["src"] == "igbosyncorp" and r["spk"] in hold_spk and eng_words(r["text"]) and len(r["text"].split()) >= 3:
|
| 70 |
+
realcs.append(dict(r, cs_eng=eng_words(r["text"]), cs_kind="natural_conversational"))
|
| 71 |
+
print("realcs", len(realcs), collections.Counter((r["src"], r["lang"]) for r in realcs), "held-out igbosyncorp speakers", len(hold_spk))
|
| 72 |
+
|
| 73 |
+
# dev: WAXAL validation 40/lang + FLEURS eng dev/test 40 (FLEURS val = the duplex eval English set, untouched here)
|
| 74 |
+
dev = []
|
| 75 |
+
for L in ["ibo", "yor", "hau", "pcm"]:
|
| 76 |
+
c = [r for r in rows if r["src"] == "waxal" and r["lang"] == L and r["split"] in ("validation", "val") and 1.5 < r["dur"] < 15]
|
| 77 |
+
random.shuffle(c); dev += c[:40]
|
| 78 |
+
c = [r for r in rows if r["src"] == "slr70" and r["split"] != "train" and 1.5 < r["dur"] < 15]
|
| 79 |
+
if not c:
|
| 80 |
+
sp = sorted({r["spk"] for r in rows if r["src"] == "slr70"}); hold70 = set(sp[:4])
|
| 81 |
+
c = [r for r in rows if r["src"] == "slr70" and r["spk"] in hold70]
|
| 82 |
+
else:
|
| 83 |
+
hold70 = set()
|
| 84 |
+
random.shuffle(c); dev += c[:40]
|
| 85 |
+
|
| 86 |
+
train, drop = [], collections.Counter()
|
| 87 |
+
for r in rows:
|
| 88 |
+
k = f"{r['src']}:{r['lang']}"
|
| 89 |
+
if r["split"] != "train": drop["not_train"] += 1; continue
|
| 90 |
+
if r["src"] == "slr70" and r["spk"] in hold70: drop["slr70_dev_spk"] += 1; continue
|
| 91 |
+
if r["src"] == "igbosyncorp" and r["spk"] in hold_spk: drop["realcs_spk"] += 1; continue
|
| 92 |
+
if k not in W: drop["no_weight:" + k] += 1; continue
|
| 93 |
+
if not (0.8 <= r["dur"] <= 20) or len(r["text"]) < 2: drop["dur_or_empty"] += 1; continue
|
| 94 |
+
if len(r["text"]) > r["dur"] * 25: drop["too_many_chars"] += 1; continue
|
| 95 |
+
r["w"] = W[k]; train.append(r)
|
| 96 |
+
print("dropped", drop)
|
| 97 |
+
def dump(name, R):
|
| 98 |
+
with open(f"{OUT}/{name}.jsonl", "w") as f:
|
| 99 |
+
for r in R:
|
| 100 |
+
f.write(json.dumps({k: v for k, v in r.items() if k != "raw"} | {"ref_raw": r["raw"]}, ensure_ascii=False) + "\n")
|
| 101 |
+
dump("train", train); dump("dev", dev); dump("realcs_test", realcs)
|
| 102 |
+
st = collections.defaultdict(lambda: [0, 0.0, 0.0])
|
| 103 |
+
for r in train:
|
| 104 |
+
s = st[f"{r['src']}:{r['lang']}"]; s[0] += 1; s[1] += r["dur"] / 3600; s[2] += r["dur"] * r["w"] / 3600
|
| 105 |
+
lang_h = collections.Counter()
|
| 106 |
+
for k, v in st.items(): lang_h[k.split(":")[1]] += v[2]
|
| 107 |
+
out = dict(sources={k: dict(n=v[0], hours=round(v[1], 2), hours_per_epoch=round(v[2], 2), licence=LIC[k.split(':')[0]]) for k, v in sorted(st.items())},
|
| 108 |
+
lang_hours_per_epoch={k: round(v, 1) for k, v in lang_h.items()}, total_hours=round(sum(v[1] for v in st.values()), 1),
|
| 109 |
+
epoch_hours=round(sum(v[2] for v in st.values()), 1), dev=len(dev), realcs=len(realcs), eng_lexicon=len(ENG))
|
| 110 |
+
json.dump(out, open(f"{OUT}/stats.json", "w"), indent=1); print(json.dumps(out, indent=1))
|
| 111 |
+
json.dump(sorted(ENG), open(f"{OUT}/eng_lexicon.json", "w"))
|
| 112 |
+
print("PREP_DONE")
|
code/training/synth_cs.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Synthesize code-switched ASR training audio with Dynamic Accent Flash-DPO (public-safe, licensed-data TTS).
|
| 2 |
+
~/venv/bin/python synth_cs.py <worker> <n_workers> -> /scratch/asr/synth/wav/*.wav + parts/part_<k>.jsonl
|
| 3 |
+
Voices: 1 train clip per WAXAL speaker (ibo/yor/hau 1-8, pcm F1-4/M1-4; CC-BY-SA-4.0 Media Trust) as zero-shot prompt + LM speaker id.
|
| 4 |
+
Pace: random LM rate token (slow/normal/fast) + random speed perturbation 0.9-1.1. Channel/noise/reverb augmentation is applied on the fly in training."""
|
| 5 |
+
import os, sys, json, random, re, unicodedata, time
|
| 6 |
+
os.environ.setdefault("CV3_DIR", "/home/dev/models/Fun-CosyVoice3-0.5B-2512")
|
| 7 |
+
REL = "/home/dev/rel_flash"; sys.path[:0] = [REL]
|
| 8 |
+
import numpy as np, soundfile as sf, librosa, torch
|
| 9 |
+
K, NW = int(sys.argv[1]), int(sys.argv[2])
|
| 10 |
+
S_DIR = "/scratch/asr/synth"; os.makedirs(f"{S_DIR}/wav", exist_ok=True); os.makedirs(f"{S_DIR}/voices", exist_ok=True); os.makedirs(f"{S_DIR}/parts", exist_ok=True)
|
| 11 |
+
import flash_lm
|
| 12 |
+
TONE = {"\u0300", "\u0301", "\u0304", "\u0302", "\u030c"}
|
| 13 |
+
def norm(s, lang):
|
| 14 |
+
s = unicodedata.normalize("NFC", s.lower())
|
| 15 |
+
if lang == "ibo": s = unicodedata.normalize("NFC", "".join(c for c in unicodedata.normalize("NFD", s) if c not in TONE))
|
| 16 |
+
s = s.replace("\u2019", "'"); s = re.sub(r"[^\w\s']", " ", s); s = re.sub(r"(^|\s)'+|'+(\s|$)", " ", s)
|
| 17 |
+
return re.sub(r"\s+", " ", s).strip()
|
| 18 |
+
TAG = re.compile(r"<(eng|ibo|yor|hau|pcm)>(.*?)</\1>", re.S)
|
| 19 |
+
|
| 20 |
+
# voice pool (deterministic, built by every worker identically; worker 0 writes the wavs)
|
| 21 |
+
W = "/home/dev/data3/waxal"; meta = [json.loads(l) for l in open(f"{W}/meta.jsonl")]
|
| 22 |
+
pcm = np.memmap(f"{W}/pcm24k.i16", dtype=np.int16, mode="r")
|
| 23 |
+
rng0 = random.Random(3); by = {}
|
| 24 |
+
for r in meta:
|
| 25 |
+
if r["split"] == "train" and 4.0 <= r["dur"] <= 9.0 and r.get("ascore", 0) > 0.4: by.setdefault(r["spk"], []).append(r)
|
| 26 |
+
voices = {}
|
| 27 |
+
for spk in sorted(by):
|
| 28 |
+
r = rng0.choice(by[spk]); p = f"{S_DIR}/voices/{spk}.wav"
|
| 29 |
+
if not os.path.exists(p):
|
| 30 |
+
tmp = p + f".{K}.tmp.wav"; sf.write(tmp, pcm[r["off24"]: r["off24"] + r["n24"]].astype(np.float32) / 32768, 24000); os.replace(tmp, p)
|
| 31 |
+
voices[spk] = dict(wav=p, spk=spk, lang=r["lang"], text=f"<{r['lang']}>{r['text']}</{r['lang']}>")
|
| 32 |
+
for v, d in voices.items(): flash_lm.VOICES[v] = dict(wav=d["wav"], spk=d["spk"])
|
| 33 |
+
S = flash_lm.Synth(f"{REL}/checkpoints/flash/flash_dpo/best.pt")
|
| 34 |
+
for v, d in voices.items(): S.vtext[v] = d["text"]
|
| 35 |
+
VL = sorted(voices); print("voices", len(VL), VL, flush=True)
|
| 36 |
+
lines = [json.loads(l) for l in open(f"{S_DIR}/cs_text.jsonl")]
|
| 37 |
+
mine = [l for i, l in enumerate(lines) if i % NW == K]
|
| 38 |
+
out = open(f"{S_DIR}/parts/part_{K}.jsonl", "a"); done = {json.loads(l)["id"] for l in open(f"{S_DIR}/parts/part_{K}.jsonl")}
|
| 39 |
+
rng = random.Random(100 + K); t0 = time.time(); n = 0
|
| 40 |
+
for L in mine:
|
| 41 |
+
if L["id"] in done: continue
|
| 42 |
+
langs = L["pair"].split("+")
|
| 43 |
+
# prefer a voice whose native language is in the pair (natural accent), sometimes any voice (accent variety)
|
| 44 |
+
pool = [v for v in VL if voices[v]["lang"] in langs] if rng.random() < 0.75 else VL
|
| 45 |
+
v = rng.choice(pool or VL); rate = rng.choice(["slow", "normal", "normal", "normal", "fast"]); seed = rng.randrange(10 ** 6)
|
| 46 |
+
try:
|
| 47 |
+
toks = S.tokens(L["tagged"], v, rate, seed)[0]
|
| 48 |
+
if len(toks) < 10 or len(toks) > 25 * 30: continue
|
| 49 |
+
w = S.wav(toks, v); w = w.squeeze().detach().cpu().numpy() if torch.is_tensor(w) else np.asarray(w).squeeze()
|
| 50 |
+
except Exception as e:
|
| 51 |
+
print("ERR", L["id"], repr(e)[:200], flush=True); continue
|
| 52 |
+
y = librosa.resample(w.astype(np.float32), orig_sr=S.cosy.sample_rate, target_sr=16000)
|
| 53 |
+
sp = rng.uniform(0.9, 1.1)
|
| 54 |
+
if abs(sp - 1) > 0.02: y = librosa.resample(y, orig_sr=16000, target_sr=int(16000 / sp)) # speed perturb (played at 16 kHz)
|
| 55 |
+
y = y / (np.abs(y).max() + 1e-6) * rng.uniform(0.5, 0.95)
|
| 56 |
+
p = f"{S_DIR}/wav/{L['id']}.wav"; sf.write(p, y, 16000, subtype="PCM_16")
|
| 57 |
+
text = " ".join(norm(t, l) for l, t in TAG.findall(L["tagged"]))
|
| 58 |
+
out.write(json.dumps(dict(id=L["id"], wav=p, dur=round(len(y) / 16000, 2), lang="cs", src="synth_flash_dpo", text=text, tagged=L["tagged"],
|
| 59 |
+
pair=L["pair"], voice=v, rate=rate, speed=round(sp, 3), licence="synthetic (Flash-DPO, licensed-data TTS)"), ensure_ascii=False) + "\n")
|
| 60 |
+
out.flush(); n += 1
|
| 61 |
+
if n % 20 == 0: print(f"w{K} {n}/{len(mine)} {(time.time() - t0) / n:.2f}s/clip", flush=True)
|
| 62 |
+
print("SYNTH_DONE", K, n, flush=True)
|
code/training/train_asr.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Fine-tune Meta omniASR CTC (default omniASR_CTC_1B_v2, Apache-2.0) on Nigerian speech (+ optional synthetic code-switch).
|
| 2 |
+
Full fine-tune (CNN feature extractor frozen), bf16 autocast, AdamW, warmup + linear decay, duration-bucketed batches.
|
| 3 |
+
The model stays a plain CTC wav2vec2 (same architecture/tokenizer), so the duplex asr_server streams it unchanged.
|
| 4 |
+
~/venv_omni/bin/python train_asr.py --run /scratch/asr/runs/A --steps 4000 [--synth /scratch/asr/synth/manifest.jsonl --synth_frac 0.2]
|
| 5 |
+
"""
|
| 6 |
+
import argparse, json, math, os, random, time, re, unicodedata
|
| 7 |
+
import numpy as np, torch, torch.nn.functional as F
|
| 8 |
+
import soundfile # noqa: F401
|
| 9 |
+
from scipy.signal import resample_poly, fftconvolve
|
| 10 |
+
from fairseq2.models.hub import load_model
|
| 11 |
+
from fairseq2.data.tokenizers.hub import load_tokenizer
|
| 12 |
+
from fairseq2.nn.batch_layout import BatchLayout
|
| 13 |
+
|
| 14 |
+
ap = argparse.ArgumentParser()
|
| 15 |
+
ap.add_argument("--card", default="omniASR_CTC_1B_v2"); ap.add_argument("--run", required=True)
|
| 16 |
+
ap.add_argument("--data", default="/scratch/asr/data"); ap.add_argument("--synth", default="")
|
| 17 |
+
ap.add_argument("--synth_frac", type=float, default=0.2); ap.add_argument("--steps", type=int, default=4000)
|
| 18 |
+
ap.add_argument("--lr", type=float, default=2e-5); ap.add_argument("--warmup", type=int, default=300)
|
| 19 |
+
ap.add_argument("--batch_sec", type=float, default=280.0); ap.add_argument("--max_sec", type=float, default=20.0)
|
| 20 |
+
ap.add_argument("--real_aug_p", type=float, default=0.15); ap.add_argument("--synth_aug_p", type=float, default=0.8)
|
| 21 |
+
ap.add_argument("--val_every", type=int, default=500); ap.add_argument("--seed", type=int, default=1)
|
| 22 |
+
ap.add_argument("--workers", type=int, default=10); ap.add_argument("--smoke", action="store_true")
|
| 23 |
+
a = ap.parse_args()
|
| 24 |
+
os.makedirs(a.run, exist_ok=True); json.dump(vars(a), open(f"{a.run}/args.json", "w"), indent=1)
|
| 25 |
+
random.seed(a.seed); np.random.seed(a.seed); torch.manual_seed(a.seed)
|
| 26 |
+
dev = torch.device("cuda")
|
| 27 |
+
if os.environ.get("MEM_FRAC"): torch.cuda.set_per_process_memory_fraction(float(os.environ["MEM_FRAC"]))
|
| 28 |
+
model = load_model(a.card, device=dev, dtype=torch.float32)
|
| 29 |
+
tok = load_tokenizer(a.card); enc = tok.create_encoder(); dec = tok.create_decoder(skip_special_tokens=True)
|
| 30 |
+
VI = tok.vocab_info; BLANK = 0; PAD = VI.pad_idx if VI.pad_idx is not None else 1
|
| 31 |
+
print("vocab", VI.size, "pad", VI.pad_idx, "unk", VI.unk_idx, "bos", VI.bos_idx, "eos", VI.eos_idx, flush=True)
|
| 32 |
+
SPECIAL = {i for i in (VI.pad_idx, VI.bos_idx, VI.eos_idx) if i is not None}
|
| 33 |
+
|
| 34 |
+
def encode(t):
|
| 35 |
+
ids = [int(x) for x in enc(t).tolist()]
|
| 36 |
+
return [i for i in ids if i not in SPECIAL]
|
| 37 |
+
|
| 38 |
+
def rows(p):
|
| 39 |
+
return [json.loads(l) for l in open(p)]
|
| 40 |
+
train = rows(f"{a.data}/train.jsonl"); devset = rows(f"{a.data}/dev.jsonl")
|
| 41 |
+
synth = rows(a.synth) if a.synth else []
|
| 42 |
+
for r in train: r["kind"] = "real"
|
| 43 |
+
for r in synth: r["kind"] = "synth"; r.setdefault("w", 1.0)
|
| 44 |
+
unk = VI.unk_idx
|
| 45 |
+
def ok(r):
|
| 46 |
+
ids = encode(r["text"]); r["n_ids"] = len(ids)
|
| 47 |
+
return len(ids) > 0 and (unk is None or unk not in ids) and r["dur"] <= a.max_sec
|
| 48 |
+
n0 = len(train); train = [r for r in train if ok(r)]; synth = [r for r in synth if ok(r)]
|
| 49 |
+
print(f"train {len(train)}/{n0} after tokenizer filter, synth {len(synth)}", flush=True)
|
| 50 |
+
|
| 51 |
+
_mm = {}
|
| 52 |
+
def load_audio(r):
|
| 53 |
+
if r.get("wav"):
|
| 54 |
+
y, sr = soundfile.read(r["wav"], dtype="float32")
|
| 55 |
+
if y.ndim > 1: y = y.mean(1)
|
| 56 |
+
if sr != 16000: y = resample_poly(y, 16000, sr).astype(np.float32)
|
| 57 |
+
return y
|
| 58 |
+
if r["pcm"] not in _mm: _mm[r["pcm"]] = np.memmap(r["pcm"], dtype=np.int16, mode="r")
|
| 59 |
+
y = _mm[r["pcm"]][r["off24"]: r["off24"] + r["n24"]].astype(np.float32) / 32768
|
| 60 |
+
return resample_poly(y, 2, 3).astype(np.float32)
|
| 61 |
+
|
| 62 |
+
def augment(y, rng):
|
| 63 |
+
"""noise (white/pink/babble-ish), simple synthetic reverb, phone band-limit, gain; ~ mic/channel variety"""
|
| 64 |
+
if rng.random() < 0.5: # reverb: exponentially decaying noise RIR, RT60 0.15-0.7 s
|
| 65 |
+
rt = rng.uniform(0.15, 0.7); n = int(16000 * rt); t = np.arange(n) / 16000
|
| 66 |
+
rir = rng.standard_normal(n) * np.exp(-6.9 * t / rt); rir[0] = 1.0; rir /= np.sqrt((rir ** 2).sum())
|
| 67 |
+
y = fftconvolve(y, rir)[: len(y)].astype(np.float32)
|
| 68 |
+
if rng.random() < 0.35: # telephone / cheap mic channel
|
| 69 |
+
y = resample_poly(resample_poly(y, 1, 2), 2, 1)[: len(y)].astype(np.float32) if rng.random() < 0.5 else \
|
| 70 |
+
resample_poly(resample_poly(y, 8, 16), 16, 8)[: len(y)].astype(np.float32)
|
| 71 |
+
if rng.random() < 0.5: y = np.tanh(y * rng.uniform(1.5, 4.0)).astype(np.float32)
|
| 72 |
+
if rng.random() < 0.7: # additive noise at SNR 5-30 dB
|
| 73 |
+
n = rng.standard_normal(len(y)).astype(np.float32)
|
| 74 |
+
if rng.random() < 0.5: n = np.cumsum(n); n -= np.convolve(n, np.ones(400) / 400, "same") # brownish
|
| 75 |
+
snr = rng.uniform(5, 30); ps = (y ** 2).mean() + 1e-8; pn = (n ** 2).mean() + 1e-8
|
| 76 |
+
y = y + n * np.sqrt(ps / (pn * 10 ** (snr / 10)))
|
| 77 |
+
y = y * rng.uniform(0.3, 1.5)
|
| 78 |
+
return np.clip(y, -1, 1).astype(np.float32)
|
| 79 |
+
|
| 80 |
+
class DS(torch.utils.data.Dataset):
|
| 81 |
+
def __init__(self, items, train_mode): self.items, self.tm = items, train_mode
|
| 82 |
+
def __len__(self): return len(self.items)
|
| 83 |
+
def __getitem__(self, i):
|
| 84 |
+
r = self.items[i]; y = load_audio(r)
|
| 85 |
+
if self.tm:
|
| 86 |
+
rng = np.random.default_rng((i * 7919 + int(time.time() * 1000)) % 2 ** 32)
|
| 87 |
+
p = a.synth_aug_p if r["kind"] == "synth" else a.real_aug_p
|
| 88 |
+
if rng.random() < p: y = augment(y, rng)
|
| 89 |
+
return torch.from_numpy(y), torch.tensor(encode(r["text"])), i
|
| 90 |
+
|
| 91 |
+
def epoch_order(rng):
|
| 92 |
+
"""weighted sampling (source weights) + synth mixed at synth_frac of audio hours; bucketed by duration"""
|
| 93 |
+
pick = []
|
| 94 |
+
for r in train:
|
| 95 |
+
k = r["w"]; n = int(k) + (1 if rng.random() < k - int(k) else 0); pick += [r] * n
|
| 96 |
+
if synth:
|
| 97 |
+
real_h = sum(r["dur"] for r in pick); want = real_h * a.synth_frac / (1 - a.synth_frac); have = sum(r["dur"] for r in synth)
|
| 98 |
+
rep = want / have; s = []
|
| 99 |
+
for r in synth:
|
| 100 |
+
n = int(rep) + (1 if rng.random() < rep - int(rep) else 0); s += [r] * n
|
| 101 |
+
pick += s
|
| 102 |
+
rng.shuffle(pick)
|
| 103 |
+
batches = []
|
| 104 |
+
for c in range(0, len(pick), 4000): # sort within chunks of 4000 to bucket by duration
|
| 105 |
+
ch = sorted(pick[c: c + 4000], key=lambda r: r["dur"]); cur, cs = [], 0.0
|
| 106 |
+
for r in ch:
|
| 107 |
+
if cs + r["dur"] > a.batch_sec and cur: batches.append(cur); cur, cs = [], 0.0
|
| 108 |
+
cur.append(r); cs += r["dur"]
|
| 109 |
+
if cur: batches.append(cur)
|
| 110 |
+
rng.shuffle(batches); return batches
|
| 111 |
+
|
| 112 |
+
def collate(b):
|
| 113 |
+
ys = [F.layer_norm(x, x.shape) for x, _, _ in b]; L = torch.tensor([len(x) for x in ys])
|
| 114 |
+
X = torch.zeros(len(ys), int(L.max())); [X[i, : len(x)].copy_(x) for i, x in enumerate(ys)]
|
| 115 |
+
T = [t for _, t, _ in b]; TL = torch.tensor([len(t) for t in T]); Y = torch.cat(T)
|
| 116 |
+
return X, L, Y, TL
|
| 117 |
+
|
| 118 |
+
def forward(X, L):
|
| 119 |
+
bl = BatchLayout(X.shape, seq_lens=L.tolist(), device=dev)
|
| 120 |
+
logits, blo = model(X, bl)
|
| 121 |
+
return logits, torch.tensor(blo.seq_lens, device=dev)
|
| 122 |
+
|
| 123 |
+
for p in model.encoder_frontend.feature_extractor.parameters(): p.requires_grad = False
|
| 124 |
+
params = [p for p in model.parameters() if p.requires_grad]
|
| 125 |
+
print("trainable params %.1fM" % (sum(p.numel() for p in params) / 1e6), flush=True)
|
| 126 |
+
opt = torch.optim.AdamW(params, lr=a.lr, betas=(0.9, 0.98), eps=1e-8, weight_decay=0.01)
|
| 127 |
+
sched = torch.optim.lr_scheduler.LambdaLR(opt, lambda s: min(1.0, (s + 1) / a.warmup) * max(0.02, 1 - max(0, s - a.warmup) / max(1, a.steps - a.warmup)))
|
| 128 |
+
|
| 129 |
+
def cer_eval(items, bs=16):
|
| 130 |
+
model.eval(); hyps = []
|
| 131 |
+
with torch.no_grad(), torch.autocast("cuda", dtype=torch.bfloat16):
|
| 132 |
+
for i in range(0, len(items), bs):
|
| 133 |
+
b = [(torch.from_numpy(load_audio(r)), torch.tensor([1]), 0) for r in items[i: i + bs]]
|
| 134 |
+
X, L, _, _ = collate(b); lg, ol = forward(X.to(dev), L)
|
| 135 |
+
pr = lg.argmax(-1)
|
| 136 |
+
for j in range(pr.shape[0]):
|
| 137 |
+
s = pr[j, : ol[j]]; m = torch.ones_like(s, dtype=torch.bool); m[1:] = s[1:] != s[:-1]; s = s[m]; s = s[s != BLANK]
|
| 138 |
+
hyps.append(str(dec(s)))
|
| 139 |
+
model.train()
|
| 140 |
+
import jiwer
|
| 141 |
+
def n(x): x = unicodedata.normalize("NFC", x.lower()); x = re.sub(r"[^\w\s']", " ", x); return re.sub(r"\s+", " ", x).strip() or "x"
|
| 142 |
+
out = {}
|
| 143 |
+
for lg in sorted({r["lang"] for r in items}):
|
| 144 |
+
idx = [k for k, r in enumerate(items) if r["lang"] == lg]
|
| 145 |
+
out[lg] = round(jiwer.cer([n(items[k]["text"]) for k in idx], [n(hyps[k]) for k in idx]), 4)
|
| 146 |
+
out["mean"] = round(float(np.mean([v for k, v in out.items()])), 4)
|
| 147 |
+
return out, hyps
|
| 148 |
+
|
| 149 |
+
log = open(f"{a.run}/log.jsonl", "a")
|
| 150 |
+
v0, _ = cer_eval(devset); print("dev step0", v0, flush=True); log.write(json.dumps(dict(step=0, dev=v0)) + "\n"); log.flush()
|
| 151 |
+
best = v0["mean"]; step = 0; rng = random.Random(a.seed); model.train(); t0 = time.time(); ema = None
|
| 152 |
+
steps_total = 60 if a.smoke else a.steps
|
| 153 |
+
while step < steps_total:
|
| 154 |
+
batches = epoch_order(rng); print(f"epoch with {len(batches)} batches", flush=True)
|
| 155 |
+
lut = {}
|
| 156 |
+
flat = []
|
| 157 |
+
for b in batches:
|
| 158 |
+
flat.append([len(lut) + k for k in range(len(b))]); [lut.setdefault(len(lut), r) for r in b]
|
| 159 |
+
items = [lut[k] for k in range(len(lut))]
|
| 160 |
+
dl = torch.utils.data.DataLoader(DS(items, True), batch_sampler=flat, num_workers=a.workers, collate_fn=collate,
|
| 161 |
+
prefetch_factor=4, persistent_workers=False)
|
| 162 |
+
for X, L, Y, TL in dl:
|
| 163 |
+
X, Y = X.to(dev, non_blocking=True), Y.to(dev)
|
| 164 |
+
with torch.autocast("cuda", dtype=torch.bfloat16):
|
| 165 |
+
lg, ol = forward(X, L)
|
| 166 |
+
lp = F.log_softmax(lg.float(), -1).transpose(0, 1)
|
| 167 |
+
loss = F.ctc_loss(lp, Y, ol, TL.to(dev), blank=BLANK, reduction="sum", zero_infinity=True) / TL.sum()
|
| 168 |
+
opt.zero_grad(set_to_none=True); loss.backward()
|
| 169 |
+
gn = torch.nn.utils.clip_grad_norm_(params, 5.0)
|
| 170 |
+
if torch.isfinite(gn): opt.step()
|
| 171 |
+
sched.step(); step += 1
|
| 172 |
+
ema = loss.item() if ema is None else 0.98 * ema + 0.02 * loss.item()
|
| 173 |
+
if step % 25 == 0:
|
| 174 |
+
el = time.time() - t0
|
| 175 |
+
print(f"step {step} loss {ema:.4f} gn {gn:.2f} lr {sched.get_last_lr()[0]:.2e} {el / step:.2f}s/step mem {torch.cuda.max_memory_allocated() / 2**30:.1f}G", flush=True)
|
| 176 |
+
log.write(json.dumps(dict(step=step, loss=round(ema, 4), sps=round(el / step, 3))) + "\n"); log.flush()
|
| 177 |
+
if step % a.val_every == 0 or step == steps_total:
|
| 178 |
+
v, _ = cer_eval(devset); print("dev", step, v, flush=True); log.write(json.dumps(dict(step=step, dev=v)) + "\n"); log.flush()
|
| 179 |
+
sd = {k: t.to(torch.bfloat16) for k, t in model.state_dict().items()}
|
| 180 |
+
if v["mean"] < best and not a.smoke:
|
| 181 |
+
best = v["mean"]; torch.save(dict(model=sd, step=step, dev=v, card=a.card), f"{a.run}/best.pt"); print("saved best", flush=True)
|
| 182 |
+
if step >= steps_total: break
|
| 183 |
+
if not a.smoke:
|
| 184 |
+
torch.save(dict(model={k: t.to(torch.bfloat16) for k, t in model.state_dict().items()}, step=step, card=a.card), f"{a.run}/last.pt")
|
| 185 |
+
print("TRAIN_DONE best", best, flush=True)
|
codeswitch-experimental/config.json
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"base_model": "omniASR_CTC_1B_v2 (facebook omnilingual-asr, Apache-2.0)",
|
| 3 |
+
"model_card": "omniASR_CTC_1B_v2",
|
| 4 |
+
"architecture": "wav2vec2 CTC 1B (unchanged)",
|
| 5 |
+
"vocab_size": 10288,
|
| 6 |
+
"tokenizer": "omniASR_tokenizer_written_v2",
|
| 7 |
+
"checkpoint_format": "torch.save dict {model: state_dict (bf16), step, dev, card}",
|
| 8 |
+
"sample_rate": 16000,
|
| 9 |
+
"decoding": "greedy CTC, Latin-script logit mask, no language hint",
|
| 10 |
+
"languages": [
|
| 11 |
+
"en",
|
| 12 |
+
"pcm",
|
| 13 |
+
"ig",
|
| 14 |
+
"yo",
|
| 15 |
+
"ha"
|
| 16 |
+
],
|
| 17 |
+
"license": "cc-by-sa-4.0",
|
| 18 |
+
"training": {
|
| 19 |
+
"steps": 4000,
|
| 20 |
+
"batch_audio_sec": 160,
|
| 21 |
+
"max_clip_sec": 18,
|
| 22 |
+
"lr": 2e-05,
|
| 23 |
+
"warmup": 300,
|
| 24 |
+
"schedule": "linear decay",
|
| 25 |
+
"optimizer": "AdamW b(0.9,0.98) wd 0.01",
|
| 26 |
+
"precision": "bf16 autocast",
|
| 27 |
+
"frozen": "CNN feature encoder",
|
| 28 |
+
"seed": 1,
|
| 29 |
+
"real_aug_p": 0.15,
|
| 30 |
+
"synthetic_codeswitch_fraction": 0.12,
|
| 31 |
+
"data": "same licensed real speech + synthetic code-switched audio (Dynamic Accent Flash-DPO TTS), 12% of sampled audio",
|
| 32 |
+
"synth_aug_p": 0.8
|
| 33 |
+
},
|
| 34 |
+
"name": "dynamic-asr/codeswitch-experimental",
|
| 35 |
+
"status": "experimental",
|
| 36 |
+
"file": "model.pt",
|
| 37 |
+
"scores": {
|
| 38 |
+
"duplex_eng": {
|
| 39 |
+
"n": 29,
|
| 40 |
+
"wer": 0.0922,
|
| 41 |
+
"cer": 0.0241,
|
| 42 |
+
"cer_nodiac": 0.0241
|
| 43 |
+
},
|
| 44 |
+
"duplex_pcm": {
|
| 45 |
+
"n": 40,
|
| 46 |
+
"wer": 0.1927,
|
| 47 |
+
"cer": 0.0761,
|
| 48 |
+
"cer_nodiac": 0.0759
|
| 49 |
+
},
|
| 50 |
+
"duplex_ibo": {
|
| 51 |
+
"n": 40,
|
| 52 |
+
"wer": 0.3207,
|
| 53 |
+
"cer": 0.095,
|
| 54 |
+
"cer_nodiac": 0.0754
|
| 55 |
+
},
|
| 56 |
+
"duplex_yor": {
|
| 57 |
+
"n": 40,
|
| 58 |
+
"wer": 0.3532,
|
| 59 |
+
"cer": 0.1182,
|
| 60 |
+
"cer_nodiac": 0.0782
|
| 61 |
+
},
|
| 62 |
+
"duplex_hau": {
|
| 63 |
+
"n": 40,
|
| 64 |
+
"wer": 0.252,
|
| 65 |
+
"cer": 0.0808,
|
| 66 |
+
"cer_nodiac": 0.0808
|
| 67 |
+
},
|
| 68 |
+
"duplex_cs": {
|
| 69 |
+
"n": 40,
|
| 70 |
+
"wer": 0.2157,
|
| 71 |
+
"cer": 0.0718,
|
| 72 |
+
"cer_nodiac": 0.0648
|
| 73 |
+
},
|
| 74 |
+
"realcs_all": {
|
| 75 |
+
"n": 94,
|
| 76 |
+
"wer": 0.438,
|
| 77 |
+
"cer": 0.1492,
|
| 78 |
+
"cer_nodiac": 0.1368
|
| 79 |
+
},
|
| 80 |
+
"realcs_natural_conversational": {
|
| 81 |
+
"n": 36,
|
| 82 |
+
"wer": 0.6103,
|
| 83 |
+
"cer": 0.2472,
|
| 84 |
+
"cer_nodiac": 0.2339
|
| 85 |
+
},
|
| 86 |
+
"realcs_insertional": {
|
| 87 |
+
"n": 58,
|
| 88 |
+
"wer": 0.3727,
|
| 89 |
+
"cer": 0.1186,
|
| 90 |
+
"cer_nodiac": 0.1065
|
| 91 |
+
},
|
| 92 |
+
"realcs_eng_word_recall": 0.3805,
|
| 93 |
+
"synthtest": {
|
| 94 |
+
"n": 109,
|
| 95 |
+
"wer": 0.1261,
|
| 96 |
+
"cer": 0.0385,
|
| 97 |
+
"cer_nodiac": 0.0349
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
}
|
codeswitch-experimental/model.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7abf1f167e41f07af97c6ea261a218b7dbf931f7b18056167fa361fcb2cb05b8
|
| 3 |
+
size 1951564394
|
config.json
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "dynamic-asr",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"pipeline_tag": "automatic-speech-recognition",
|
| 5 |
+
"library_name": "fairseq2 / omnilingual-asr",
|
| 6 |
+
"base_model": "facebook omniASR_CTC_1B_v2 (Apache-2.0)",
|
| 7 |
+
"architecture": "wav2vec2 CTC, 1B (unchanged; vocab 10288, tokenizer omniASR_tokenizer_written_v2)",
|
| 8 |
+
"languages": [
|
| 9 |
+
"en",
|
| 10 |
+
"pcm",
|
| 11 |
+
"ig",
|
| 12 |
+
"yo",
|
| 13 |
+
"ha"
|
| 14 |
+
],
|
| 15 |
+
"default_model": "real",
|
| 16 |
+
"models": {
|
| 17 |
+
"real": {
|
| 18 |
+
"path": "real/model.pt",
|
| 19 |
+
"trained_on": "licensed real speech only",
|
| 20 |
+
"status": "default"
|
| 21 |
+
},
|
| 22 |
+
"codeswitch-experimental": {
|
| 23 |
+
"path": "codeswitch-experimental/model.pt",
|
| 24 |
+
"trained_on": "same real speech + synthetic code-switched audio (Flash-DPO TTS)",
|
| 25 |
+
"status": "experimental"
|
| 26 |
+
}
|
| 27 |
+
},
|
| 28 |
+
"decoding": {
|
| 29 |
+
"type": "greedy CTC",
|
| 30 |
+
"latin_script_mask": true,
|
| 31 |
+
"language_hint": null,
|
| 32 |
+
"streaming": "re-decode the growing buffer (duplex asr_server)"
|
| 33 |
+
},
|
| 34 |
+
"output": "lowercase, no punctuation; Igbo without tone marks (dot letters kept); Yoruba with tone marks",
|
| 35 |
+
"weights_license": "cc-by-sa-4.0",
|
| 36 |
+
"code_license": "apache-2.0",
|
| 37 |
+
"scores": {
|
| 38 |
+
"v1_stock": {
|
| 39 |
+
"duplex_eng": {
|
| 40 |
+
"n": 29,
|
| 41 |
+
"wer": 0.1437,
|
| 42 |
+
"cer": 0.0654,
|
| 43 |
+
"cer_nodiac": 0.0654
|
| 44 |
+
},
|
| 45 |
+
"duplex_pcm": {
|
| 46 |
+
"n": 40,
|
| 47 |
+
"wer": 0.3796,
|
| 48 |
+
"cer": 0.1693,
|
| 49 |
+
"cer_nodiac": 0.169
|
| 50 |
+
},
|
| 51 |
+
"duplex_ibo": {
|
| 52 |
+
"n": 40,
|
| 53 |
+
"wer": 0.636,
|
| 54 |
+
"cer": 0.2057,
|
| 55 |
+
"cer_nodiac": 0.1614
|
| 56 |
+
},
|
| 57 |
+
"duplex_yor": {
|
| 58 |
+
"n": 40,
|
| 59 |
+
"wer": 0.4978,
|
| 60 |
+
"cer": 0.1905,
|
| 61 |
+
"cer_nodiac": 0.1227
|
| 62 |
+
},
|
| 63 |
+
"duplex_hau": {
|
| 64 |
+
"n": 40,
|
| 65 |
+
"wer": 0.299,
|
| 66 |
+
"cer": 0.0897,
|
| 67 |
+
"cer_nodiac": 0.0885
|
| 68 |
+
},
|
| 69 |
+
"duplex_cs": {
|
| 70 |
+
"n": 40,
|
| 71 |
+
"wer": 0.3295,
|
| 72 |
+
"cer": 0.1282,
|
| 73 |
+
"cer_nodiac": 0.1053
|
| 74 |
+
},
|
| 75 |
+
"realcs_all": {
|
| 76 |
+
"n": 94,
|
| 77 |
+
"wer": 0.6062,
|
| 78 |
+
"cer": 0.2243,
|
| 79 |
+
"cer_nodiac": 0.1951
|
| 80 |
+
},
|
| 81 |
+
"realcs_natural_conversational": {
|
| 82 |
+
"n": 36,
|
| 83 |
+
"wer": 0.8351,
|
| 84 |
+
"cer": 0.3719,
|
| 85 |
+
"cer_nodiac": 0.3256
|
| 86 |
+
},
|
| 87 |
+
"realcs_insertional": {
|
| 88 |
+
"n": 58,
|
| 89 |
+
"wer": 0.5195,
|
| 90 |
+
"cer": 0.1783,
|
| 91 |
+
"cer_nodiac": 0.1544
|
| 92 |
+
},
|
| 93 |
+
"realcs_eng_word_recall": 0.3009,
|
| 94 |
+
"synthtest": {
|
| 95 |
+
"n": 109,
|
| 96 |
+
"wer": 0.4141,
|
| 97 |
+
"cer": 0.1586,
|
| 98 |
+
"cer_nodiac": 0.1293
|
| 99 |
+
}
|
| 100 |
+
},
|
| 101 |
+
"v2_stock": {
|
| 102 |
+
"duplex_eng": {
|
| 103 |
+
"n": 29,
|
| 104 |
+
"wer": 0.0969,
|
| 105 |
+
"cer": 0.0244,
|
| 106 |
+
"cer_nodiac": 0.0244
|
| 107 |
+
},
|
| 108 |
+
"duplex_pcm": {
|
| 109 |
+
"n": 40,
|
| 110 |
+
"wer": 0.3358,
|
| 111 |
+
"cer": 0.1232,
|
| 112 |
+
"cer_nodiac": 0.1223
|
| 113 |
+
},
|
| 114 |
+
"duplex_ibo": {
|
| 115 |
+
"n": 40,
|
| 116 |
+
"wer": 0.4559,
|
| 117 |
+
"cer": 0.1329,
|
| 118 |
+
"cer_nodiac": 0.1089
|
| 119 |
+
},
|
| 120 |
+
"duplex_yor": {
|
| 121 |
+
"n": 40,
|
| 122 |
+
"wer": 0.4009,
|
| 123 |
+
"cer": 0.1346,
|
| 124 |
+
"cer_nodiac": 0.0977
|
| 125 |
+
},
|
| 126 |
+
"duplex_hau": {
|
| 127 |
+
"n": 40,
|
| 128 |
+
"wer": 0.3172,
|
| 129 |
+
"cer": 0.1061,
|
| 130 |
+
"cer_nodiac": 0.1059
|
| 131 |
+
},
|
| 132 |
+
"duplex_cs": {
|
| 133 |
+
"n": 40,
|
| 134 |
+
"wer": 0.2922,
|
| 135 |
+
"cer": 0.1042,
|
| 136 |
+
"cer_nodiac": 0.0898
|
| 137 |
+
},
|
| 138 |
+
"realcs_all": {
|
| 139 |
+
"n": 94,
|
| 140 |
+
"wer": 0.5139,
|
| 141 |
+
"cer": 0.1931,
|
| 142 |
+
"cer_nodiac": 0.1725
|
| 143 |
+
},
|
| 144 |
+
"realcs_natural_conversational": {
|
| 145 |
+
"n": 36,
|
| 146 |
+
"wer": 0.7773,
|
| 147 |
+
"cer": 0.3535,
|
| 148 |
+
"cer_nodiac": 0.3152
|
| 149 |
+
},
|
| 150 |
+
"realcs_insertional": {
|
| 151 |
+
"n": 58,
|
| 152 |
+
"wer": 0.4141,
|
| 153 |
+
"cer": 0.1431,
|
| 154 |
+
"cer_nodiac": 0.1281
|
| 155 |
+
},
|
| 156 |
+
"realcs_eng_word_recall": 0.2832,
|
| 157 |
+
"synthtest": {
|
| 158 |
+
"n": 109,
|
| 159 |
+
"wer": 0.3541,
|
| 160 |
+
"cer": 0.1324,
|
| 161 |
+
"cer_nodiac": 0.1179
|
| 162 |
+
}
|
| 163 |
+
},
|
| 164 |
+
"A_real": {
|
| 165 |
+
"duplex_eng": {
|
| 166 |
+
"n": 29,
|
| 167 |
+
"wer": 0.0953,
|
| 168 |
+
"cer": 0.0252,
|
| 169 |
+
"cer_nodiac": 0.0252
|
| 170 |
+
},
|
| 171 |
+
"duplex_pcm": {
|
| 172 |
+
"n": 40,
|
| 173 |
+
"wer": 0.1927,
|
| 174 |
+
"cer": 0.0738,
|
| 175 |
+
"cer_nodiac": 0.0736
|
| 176 |
+
},
|
| 177 |
+
"duplex_ibo": {
|
| 178 |
+
"n": 40,
|
| 179 |
+
"wer": 0.3153,
|
| 180 |
+
"cer": 0.0929,
|
| 181 |
+
"cer_nodiac": 0.0761
|
| 182 |
+
},
|
| 183 |
+
"duplex_yor": {
|
| 184 |
+
"n": 40,
|
| 185 |
+
"wer": 0.3547,
|
| 186 |
+
"cer": 0.1213,
|
| 187 |
+
"cer_nodiac": 0.0841
|
| 188 |
+
},
|
| 189 |
+
"duplex_hau": {
|
| 190 |
+
"n": 40,
|
| 191 |
+
"wer": 0.2533,
|
| 192 |
+
"cer": 0.0823,
|
| 193 |
+
"cer_nodiac": 0.0823
|
| 194 |
+
},
|
| 195 |
+
"duplex_cs": {
|
| 196 |
+
"n": 40,
|
| 197 |
+
"wer": 0.2385,
|
| 198 |
+
"cer": 0.0807,
|
| 199 |
+
"cer_nodiac": 0.0646
|
| 200 |
+
},
|
| 201 |
+
"realcs_all": {
|
| 202 |
+
"n": 94,
|
| 203 |
+
"wer": 0.4357,
|
| 204 |
+
"cer": 0.1482,
|
| 205 |
+
"cer_nodiac": 0.137
|
| 206 |
+
},
|
| 207 |
+
"realcs_natural_conversational": {
|
| 208 |
+
"n": 36,
|
| 209 |
+
"wer": 0.6227,
|
| 210 |
+
"cer": 0.2481,
|
| 211 |
+
"cer_nodiac": 0.2335
|
| 212 |
+
},
|
| 213 |
+
"realcs_insertional": {
|
| 214 |
+
"n": 58,
|
| 215 |
+
"wer": 0.3648,
|
| 216 |
+
"cer": 0.117,
|
| 217 |
+
"cer_nodiac": 0.107
|
| 218 |
+
},
|
| 219 |
+
"realcs_eng_word_recall": 0.3186,
|
| 220 |
+
"synthtest": {
|
| 221 |
+
"n": 109,
|
| 222 |
+
"wer": 0.2932,
|
| 223 |
+
"cer": 0.1052,
|
| 224 |
+
"cer_nodiac": 0.0914
|
| 225 |
+
}
|
| 226 |
+
},
|
| 227 |
+
"B_synth": {
|
| 228 |
+
"duplex_eng": {
|
| 229 |
+
"n": 29,
|
| 230 |
+
"wer": 0.0922,
|
| 231 |
+
"cer": 0.0241,
|
| 232 |
+
"cer_nodiac": 0.0241
|
| 233 |
+
},
|
| 234 |
+
"duplex_pcm": {
|
| 235 |
+
"n": 40,
|
| 236 |
+
"wer": 0.1927,
|
| 237 |
+
"cer": 0.0761,
|
| 238 |
+
"cer_nodiac": 0.0759
|
| 239 |
+
},
|
| 240 |
+
"duplex_ibo": {
|
| 241 |
+
"n": 40,
|
| 242 |
+
"wer": 0.3207,
|
| 243 |
+
"cer": 0.095,
|
| 244 |
+
"cer_nodiac": 0.0754
|
| 245 |
+
},
|
| 246 |
+
"duplex_yor": {
|
| 247 |
+
"n": 40,
|
| 248 |
+
"wer": 0.3532,
|
| 249 |
+
"cer": 0.1182,
|
| 250 |
+
"cer_nodiac": 0.0782
|
| 251 |
+
},
|
| 252 |
+
"duplex_hau": {
|
| 253 |
+
"n": 40,
|
| 254 |
+
"wer": 0.252,
|
| 255 |
+
"cer": 0.0808,
|
| 256 |
+
"cer_nodiac": 0.0808
|
| 257 |
+
},
|
| 258 |
+
"duplex_cs": {
|
| 259 |
+
"n": 40,
|
| 260 |
+
"wer": 0.2157,
|
| 261 |
+
"cer": 0.0718,
|
| 262 |
+
"cer_nodiac": 0.0648
|
| 263 |
+
},
|
| 264 |
+
"realcs_all": {
|
| 265 |
+
"n": 94,
|
| 266 |
+
"wer": 0.438,
|
| 267 |
+
"cer": 0.1492,
|
| 268 |
+
"cer_nodiac": 0.1368
|
| 269 |
+
},
|
| 270 |
+
"realcs_natural_conversational": {
|
| 271 |
+
"n": 36,
|
| 272 |
+
"wer": 0.6103,
|
| 273 |
+
"cer": 0.2472,
|
| 274 |
+
"cer_nodiac": 0.2339
|
| 275 |
+
},
|
| 276 |
+
"realcs_insertional": {
|
| 277 |
+
"n": 58,
|
| 278 |
+
"wer": 0.3727,
|
| 279 |
+
"cer": 0.1186,
|
| 280 |
+
"cer_nodiac": 0.1065
|
| 281 |
+
},
|
| 282 |
+
"realcs_eng_word_recall": 0.3805,
|
| 283 |
+
"synthtest": {
|
| 284 |
+
"n": 109,
|
| 285 |
+
"wer": 0.1261,
|
| 286 |
+
"cer": 0.0385,
|
| 287 |
+
"cer_nodiac": 0.0349
|
| 288 |
+
}
|
| 289 |
+
}
|
| 290 |
+
},
|
| 291 |
+
"excluded_data": [
|
| 292 |
+
"African Voices / WaZoBia",
|
| 293 |
+
"NaijaVoices (non-commercial)",
|
| 294 |
+
"AfriSpeech-200 (non-commercial)",
|
| 295 |
+
"Common Voice (not fetched)"
|
| 296 |
+
]
|
| 297 |
+
}
|
real/config.json
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"base_model": "omniASR_CTC_1B_v2 (facebook omnilingual-asr, Apache-2.0)",
|
| 3 |
+
"model_card": "omniASR_CTC_1B_v2",
|
| 4 |
+
"architecture": "wav2vec2 CTC 1B (unchanged)",
|
| 5 |
+
"vocab_size": 10288,
|
| 6 |
+
"tokenizer": "omniASR_tokenizer_written_v2",
|
| 7 |
+
"checkpoint_format": "torch.save dict {model: state_dict (bf16), step, dev, card}",
|
| 8 |
+
"sample_rate": 16000,
|
| 9 |
+
"decoding": "greedy CTC, Latin-script logit mask, no language hint",
|
| 10 |
+
"languages": [
|
| 11 |
+
"en",
|
| 12 |
+
"pcm",
|
| 13 |
+
"ig",
|
| 14 |
+
"yo",
|
| 15 |
+
"ha"
|
| 16 |
+
],
|
| 17 |
+
"license": "cc-by-sa-4.0",
|
| 18 |
+
"training": {
|
| 19 |
+
"steps": 4000,
|
| 20 |
+
"batch_audio_sec": 160,
|
| 21 |
+
"max_clip_sec": 18,
|
| 22 |
+
"lr": 2e-05,
|
| 23 |
+
"warmup": 300,
|
| 24 |
+
"schedule": "linear decay",
|
| 25 |
+
"optimizer": "AdamW b(0.9,0.98) wd 0.01",
|
| 26 |
+
"precision": "bf16 autocast",
|
| 27 |
+
"frozen": "CNN feature encoder",
|
| 28 |
+
"seed": 1,
|
| 29 |
+
"real_aug_p": 0.15,
|
| 30 |
+
"synthetic_codeswitch_fraction": 0.0,
|
| 31 |
+
"data": "licensed real Nigerian speech only"
|
| 32 |
+
},
|
| 33 |
+
"name": "dynamic-asr/real",
|
| 34 |
+
"status": "default",
|
| 35 |
+
"file": "model.pt",
|
| 36 |
+
"scores": {
|
| 37 |
+
"duplex_eng": {
|
| 38 |
+
"n": 29,
|
| 39 |
+
"wer": 0.0953,
|
| 40 |
+
"cer": 0.0252,
|
| 41 |
+
"cer_nodiac": 0.0252
|
| 42 |
+
},
|
| 43 |
+
"duplex_pcm": {
|
| 44 |
+
"n": 40,
|
| 45 |
+
"wer": 0.1927,
|
| 46 |
+
"cer": 0.0738,
|
| 47 |
+
"cer_nodiac": 0.0736
|
| 48 |
+
},
|
| 49 |
+
"duplex_ibo": {
|
| 50 |
+
"n": 40,
|
| 51 |
+
"wer": 0.3153,
|
| 52 |
+
"cer": 0.0929,
|
| 53 |
+
"cer_nodiac": 0.0761
|
| 54 |
+
},
|
| 55 |
+
"duplex_yor": {
|
| 56 |
+
"n": 40,
|
| 57 |
+
"wer": 0.3547,
|
| 58 |
+
"cer": 0.1213,
|
| 59 |
+
"cer_nodiac": 0.0841
|
| 60 |
+
},
|
| 61 |
+
"duplex_hau": {
|
| 62 |
+
"n": 40,
|
| 63 |
+
"wer": 0.2533,
|
| 64 |
+
"cer": 0.0823,
|
| 65 |
+
"cer_nodiac": 0.0823
|
| 66 |
+
},
|
| 67 |
+
"duplex_cs": {
|
| 68 |
+
"n": 40,
|
| 69 |
+
"wer": 0.2385,
|
| 70 |
+
"cer": 0.0807,
|
| 71 |
+
"cer_nodiac": 0.0646
|
| 72 |
+
},
|
| 73 |
+
"realcs_all": {
|
| 74 |
+
"n": 94,
|
| 75 |
+
"wer": 0.4357,
|
| 76 |
+
"cer": 0.1482,
|
| 77 |
+
"cer_nodiac": 0.137
|
| 78 |
+
},
|
| 79 |
+
"realcs_natural_conversational": {
|
| 80 |
+
"n": 36,
|
| 81 |
+
"wer": 0.6227,
|
| 82 |
+
"cer": 0.2481,
|
| 83 |
+
"cer_nodiac": 0.2335
|
| 84 |
+
},
|
| 85 |
+
"realcs_insertional": {
|
| 86 |
+
"n": 58,
|
| 87 |
+
"wer": 0.3648,
|
| 88 |
+
"cer": 0.117,
|
| 89 |
+
"cer_nodiac": 0.107
|
| 90 |
+
},
|
| 91 |
+
"realcs_eng_word_recall": 0.3186,
|
| 92 |
+
"synthtest": {
|
| 93 |
+
"n": 109,
|
| 94 |
+
"wer": 0.2932,
|
| 95 |
+
"cer": 0.1052,
|
| 96 |
+
"cer_nodiac": 0.0914
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
}
|
real/model.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4ab32b443451935ec12d1cf836b7433e2057cb0f9f95913e1bf9f1a64a628b5c
|
| 3 |
+
size 1951564394
|
transcribe.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Dynamic ASR: transcribe Nigerian English, Pidgin, Igbo, Yoruba, Hausa and code-switched speech.
|
| 2 |
+
|
| 3 |
+
pip install "omnilingual-asr>=0.2.0" huggingface_hub soundfile scipy
|
| 4 |
+
python transcribe.py audio.wav [more.wav ...] [--model real|codeswitch] [--no-latin-mask]
|
| 5 |
+
|
| 6 |
+
Model: Meta omniASR_CTC_1B_v2 (Apache-2.0) fine-tuned by Modular. Greedy CTC decoding, no language hint needed.
|
| 7 |
+
Output: lowercase text without punctuation (Igbo without tone marks; Yoruba with tone marks).
|
| 8 |
+
"""
|
| 9 |
+
import argparse, unicodedata
|
| 10 |
+
import numpy as np, torch, torch.nn.functional as F
|
| 11 |
+
import soundfile as sf
|
| 12 |
+
from scipy.signal import resample_poly
|
| 13 |
+
from huggingface_hub import hf_hub_download
|
| 14 |
+
from omnilingual_asr.models.inference.pipeline import ASRInferencePipeline # noqa: F401 (registers omniASR cards)
|
| 15 |
+
from fairseq2.models.hub import load_model
|
| 16 |
+
from fairseq2.data.tokenizers.hub import load_tokenizer
|
| 17 |
+
from fairseq2.nn.batch_layout import BatchLayout
|
| 18 |
+
|
| 19 |
+
REPO = "Modularcomputing/dynamic-asr"
|
| 20 |
+
FILES = {"real": "real/model.pt", "codeswitch": "codeswitch-experimental/model.pt"}
|
| 21 |
+
BASE = "omniASR_CTC_1B_v2"
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def latin_ok(s):
|
| 25 |
+
for c in s:
|
| 26 |
+
cat = unicodedata.category(c)
|
| 27 |
+
if cat[0] == "M":
|
| 28 |
+
if not ("\u0300" <= c <= "\u036f"):
|
| 29 |
+
return False
|
| 30 |
+
elif c.isalpha() and "LATIN" not in unicodedata.name(c, ""):
|
| 31 |
+
return False
|
| 32 |
+
return True
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class DynamicASR:
|
| 36 |
+
def __init__(self, model="real", device="cuda" if torch.cuda.is_available() else "cpu", latin_mask=True, ckpt=None):
|
| 37 |
+
self.dev = torch.device(device)
|
| 38 |
+
dtype = torch.bfloat16 if self.dev.type == "cuda" else torch.float32
|
| 39 |
+
self.model = load_model(BASE, device=self.dev, dtype=dtype).eval() # base weights, then overwritten
|
| 40 |
+
ck = torch.load(ckpt or hf_hub_download(REPO, FILES[model]), map_location="cpu", weights_only=False)
|
| 41 |
+
self.model.load_state_dict({k: v.to(dtype) for k, v in ck["model"].items()})
|
| 42 |
+
self.dtype = dtype
|
| 43 |
+
self.dec = load_tokenizer(BASE).create_decoder(skip_special_tokens=True)
|
| 44 |
+
V = self.model.final_proj.weight.shape[0]
|
| 45 |
+
self.bad = torch.tensor([i for i in range(V) if not latin_ok(str(self.dec(torch.tensor([i]))))], device=self.dev) if latin_mask else None
|
| 46 |
+
|
| 47 |
+
@torch.no_grad()
|
| 48 |
+
def __call__(self, wav, sr=16000):
|
| 49 |
+
y = np.asarray(wav, dtype=np.float32)
|
| 50 |
+
if y.ndim > 1: y = y.mean(1)
|
| 51 |
+
if sr != 16000: y = resample_poly(y, 16000, sr).astype(np.float32)
|
| 52 |
+
x = torch.from_numpy(y); x = F.layer_norm(x, x.shape)[None].to(self.dev, self.dtype)
|
| 53 |
+
logits, lay = self.model(x, BatchLayout(x.shape, seq_lens=[x.shape[1]], device=self.dev))
|
| 54 |
+
if self.bad is not None: logits[..., self.bad] = -float("inf")
|
| 55 |
+
s = logits[0, : lay.seq_lens[0]].argmax(-1); m = torch.ones_like(s, dtype=torch.bool); m[1:] = s[1:] != s[:-1]
|
| 56 |
+
return str(self.dec(s[m]))
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
if __name__ == "__main__":
|
| 60 |
+
ap = argparse.ArgumentParser(); ap.add_argument("audio", nargs="+"); ap.add_argument("--model", default="real", choices=list(FILES))
|
| 61 |
+
ap.add_argument("--no-latin-mask", action="store_true"); ap.add_argument("--ckpt", default=None, help="local model.pt instead of the Hub")
|
| 62 |
+
a = ap.parse_args()
|
| 63 |
+
asr = DynamicASR(a.model, latin_mask=not a.no_latin_mask, ckpt=a.ckpt)
|
| 64 |
+
for f in a.audio:
|
| 65 |
+
w, sr = sf.read(f, dtype="float32"); print(f"{f}\t{asr(w, sr)}")
|