Add inference code (prosody v2), README, LICENSE, prompts, samples
Browse files- .gitattributes +7 -0
- LICENSE +202 -0
- README.md +184 -0
- code/da/__init__.py +0 -0
- code/da/data.py +95 -0
- code/da/model.py +368 -0
- code/da/p2common.py +308 -0
- code/da/prep2.py +259 -0
- code/da/s3_2.py +130 -0
- code/da/text.py +60 -0
- code/train_b.py +353 -0
- code/train_b2.py +590 -0
- cosyvoice_requirements.txt +42 -0
- infer.py +418 -0
- prompts/clip0_waxal_yor_yor_1258.wav +3 -0
- prompts/clip1_waxal_yor_yor_1205.wav +3 -0
- prompts/clip2_waxal_yor_yor_1263.wav +3 -0
- prompts/clip3_waxal_yor_yor_1218.wav +3 -0
- prompts/prompts.json +602 -0
- requirements.txt +33 -0
- samples/pcm_eng_dynamic_accent_seed0.mp3 +0 -0
- samples/pcm_eng_dynamic_accent_seed1.mp3 +0 -0
- samples/pcm_eng_dynamic_accent_seed2.mp3 +0 -0
- samples/prosody_v2/custom_pcm_eng.mp3 +0 -0
- samples/prosody_v2/ibo_000.mp3 +0 -0
- samples/prosody_v2/para_hau_eng_pcm.mp3 +3 -0
- samples/prosody_v2/para_ibo_pcm_eng.mp3 +3 -0
- samples/prosody_v2/para_yor_eng.mp3 +3 -0
- samples/prosody_v2/yor_000.mp3 +0 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,10 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
prompts/clip0_waxal_yor_yor_1258.wav filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
prompts/clip1_waxal_yor_yor_1205.wav filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
prompts/clip2_waxal_yor_yor_1263.wav filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
prompts/clip3_waxal_yor_yor_1218.wav filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
samples/prosody_v2/para_hau_eng_pcm.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
samples/prosody_v2/para_ibo_pcm_eng.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
samples/prosody_v2/para_yor_eng.mp3 filter=lfs diff=lfs merge=lfs -text
|
LICENSE
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,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
- pcm
|
| 6 |
+
- yo
|
| 7 |
+
- ig
|
| 8 |
+
- ha
|
| 9 |
+
base_model: FunAudioLLM/CosyVoice2-0.5B
|
| 10 |
+
pipeline_tag: text-to-speech
|
| 11 |
+
tags:
|
| 12 |
+
- text-to-speech
|
| 13 |
+
- code-switching
|
| 14 |
+
- nigerian-languages
|
| 15 |
+
- cosyvoice2
|
| 16 |
+
- research-preview
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
# Dynamic Accent (Phase 2, research preview)
|
| 20 |
+
|
| 21 |
+
Nigerian multilingual, **code-switching** text-to-speech in one consistent voice: English, Nigerian Pidgin, Yoruba, Igbo and Hausa, mixed freely inside one sentence.
|
| 22 |
+
|
| 23 |
+
It is built on [FunAudioLLM/CosyVoice2-0.5B](https://huggingface.co/FunAudioLLM/CosyVoice2-0.5B) and adds:
|
| 24 |
+
- per-token **language embeddings**, a **speaker token** and an adversarial (GRL) speaker classifier on the LM, fine-tuned on about 120 h of Nigerian-language and code-switched speech with pause-normalised data (pause profile B);
|
| 25 |
+
- in-context **anchor prompts** at the LM and the flow, so prosody and pause style follow the anchor voice (WAXAL Yoruba speaker 4);
|
| 26 |
+
- a **flow-matching decoder fine-tuned on the anchor voice** (anchor SFT) for tone and pitch detail.
|
| 27 |
+
|
| 28 |
+
This repo is a **private research preview** for testing. It is not a production model.
|
| 29 |
+
|
| 30 |
+
Inference defaults to **prosody v2**: clause-level generation with anchor-matched pause gaps, and a 1.05 pitch-preserving speed-up. See the section below.
|
| 31 |
+
|
| 32 |
+
## What is in the repo
|
| 33 |
+
|
| 34 |
+
| path | what it is |
|
| 35 |
+
|---|---|
|
| 36 |
+
| `checkpoints/p2_lm_B/best.pt` | Dynamic Accent LM (CosyVoice2 Qwen2 LM + language/speaker embeddings), pause profile B, 4,000 steps, best validation CE |
|
| 37 |
+
| `checkpoints/p2_lm_B/spk2id.json` | speaker-token map (the anchor is `waxal_yor_4`) |
|
| 38 |
+
| `checkpoints/p2_flow_sft/best.pt` | anchor-SFT flow-matching decoder (replaces the CosyVoice2 flow weights) |
|
| 39 |
+
| `prompts/clip{0..3}_*.wav`, `prompts/prompts.json` | the 4 anchor prompt clips (24 kHz) with their transcripts and S3 speech tokens. The final system uses clip 0 |
|
| 40 |
+
| `infer.py` | self-contained inference: the same path as the Phase 2 final samples |
|
| 41 |
+
| `code/` | the original training and synthesis code (`train_b.py`, `train_b2.py`, `da/`) for reference |
|
| 42 |
+
| `cosyvoice_requirements.txt` | CosyVoice's own requirements file, at the commit used |
|
| 43 |
+
| `samples/` | example renders |
|
| 44 |
+
|
| 45 |
+
Everything else comes from the stock base model **`FunAudioLLM/CosyVoice2-0.5B`** (Qwen tokenizer `CosyVoice-BlankEN`, `llm.pt`, `hift.pt` vocoder, `campplus.onnx`, `speech_tokenizer_v2.onnx`, `cosyvoice2.yaml`). Nothing in the base needs changing; `infer.py` downloads it on first run.
|
| 46 |
+
|
| 47 |
+
## Setup (single GPU, e.g. Colab T4 or A100)
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
# 1. CosyVoice code (the commit the model was built and tested on)
|
| 51 |
+
git clone --recursive https://github.com/FunAudioLLM/CosyVoice.git
|
| 52 |
+
cd CosyVoice && git checkout 074ca6dc9e80a2f424f1f74b48bdd7d3fea531cc && git submodule update --init --recursive && cd ..
|
| 53 |
+
|
| 54 |
+
# 2. This repo (private: log in with a token that can read Modularcomputing)
|
| 55 |
+
huggingface-cli login
|
| 56 |
+
huggingface-cli download Modularcomputing/dynamic-accent-preview --local-dir dynamic-accent-preview
|
| 57 |
+
|
| 58 |
+
# 3. Python deps (Python 3.10; on Colab keep the preinstalled torch/torchaudio)
|
| 59 |
+
pip install -r dynamic-accent-preview/requirements.txt
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
The base model downloads to `dynamic-accent-preview/pretrained/CosyVoice2-0.5B` the first time you run `infer.py`. You can also point `--model_dir` (or `COSYVOICE2_DIR`) at an existing copy. Point `--cosyvoice_dir` (or `COSYVOICE_REPO`) at your CosyVoice clone if it is not inside the repo folder.
|
| 63 |
+
|
| 64 |
+
The 0.5B model should fit comfortably on a 16 GB T4. This build was tested on an H100 only, not yet on Colab.
|
| 65 |
+
|
| 66 |
+
## Usage
|
| 67 |
+
|
| 68 |
+
```bash
|
| 69 |
+
cd dynamic-accent-preview
|
| 70 |
+
python infer.py --cosyvoice_dir ../CosyVoice \
|
| 71 |
+
--text "<eng>this is dynamic accent,</eng> <pcm>e dey sound nice abi?</pcm>" \
|
| 72 |
+
--seed 0 --out out.wav # defaults: --pause_mode clause --speed 1.05
|
| 73 |
+
# Phase 2 final behaviour: add --pause_mode model --speed 1.0
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
- **Language tags:** wrap each span in `<eng>`, `<pcm>`, `<yor>`, `<ibo>` or `<hau>`. Untagged text is read as English. Keep punctuation inside the span it belongs to.
|
| 77 |
+
- **`--seed`:** the final system is a single sample at seed 0. Try seeds 1 and 2 (or more) and keep the take you like. Best-of-N picking lowered CER by about a third in evaluation.
|
| 78 |
+
- **`--pause_mode clause`** (default, prosody v2): the text is cut at `, ; : . ? !` and at language switches. Each chunk is generated separately and its edge silence is trimmed (energy VAD, 30 ms kept). Chunks are then joined with set gaps taken from the real anchor's pause statistics: 0.40 s ± 0.05 at commas and `;:`, 0.60 s at sentence ends, and 0.12 s at an unpunctuated language switch, with 10 ms fades. A one-word chunk is merged into the next chunk of the same language.
|
| 79 |
+
- **`--speed`** (default 1.05): a pitch-preserving tempo factor (WSOLA via `audiotsm`) applied to the speech only, so the gaps stay as set. The allowed range is 1.0–1.2.
|
| 80 |
+
- **`--pause_mode model --speed 1.0`** reproduces the Phase 2 final system: model-generated pauses, sentence-by-sentence generation, internal silences capped at 0.6 s, and 0.6 s between sentences. Both modes use anchor prompt clip 0 at the LM and flow (`--prompt_clip 0`).
|
| 81 |
+
- **`--dtype`:** the LM ran under bf16 autocast. T4 has no bf16, so `auto` uses fp16 there. fp16 has not been tested yet; if a T4 run sounds wrong, compare against an A100 (bf16) run.
|
| 82 |
+
- **Determinism:** generation is not bit-reproducible across runs on GPU. Sampling under bf16 can pick a different token on near-ties, so the same seed can give a slightly different take.
|
| 83 |
+
|
| 84 |
+
From Python:
|
| 85 |
+
|
| 86 |
+
```python
|
| 87 |
+
from infer import DynamicAccent
|
| 88 |
+
tts = DynamicAccent("pretrained/CosyVoice2-0.5B", "../CosyVoice",
|
| 89 |
+
"checkpoints/p2_lm_B/best.pt", "checkpoints/p2_flow_sft/best.pt", "prompts/prompts.json")
|
| 90 |
+
wav = tts.say("<hau>Ina kwana, yaya aiki?</hau> <eng>The meeting has been moved to Friday morning.</eng>",
|
| 91 |
+
seed=0, pause_mode="clause", speed=1.05)
|
| 92 |
+
import soundfile as sf; sf.write("hau_eng.wav", wav, tts.sr)
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
### Example code-switched inputs
|
| 96 |
+
|
| 97 |
+
```text
|
| 98 |
+
<eng>this is dynamic accent,</eng> <pcm>e dey sound nice abi?</pcm>
|
| 99 |
+
<yor>Ẹ káàárọ̀ o, ṣé dáadáa ni?</yor> <eng>I will send you the report this afternoon.</eng>
|
| 100 |
+
<ibo>Kedu ka ị mere?</ibo> <pcm>Abeg no vex, I go call you for evening.</pcm>
|
| 101 |
+
<hau>Ina kwana, yaya aiki?</hau> <eng>The meeting has been moved to Friday morning.</eng>
|
| 102 |
+
<ibo>Ndewo nụ, ụmụnna m.</ibo> <pcm>Una well done o, I know say the road no easy.</pcm> <eng>We have three things to discuss this evening.</eng>
|
| 103 |
+
<hau>Barka da yamma, yaya kuke?</hau> <eng>The weather report says it will rain tomorrow morning.</eng> <pcm>Abeg no forget o, the rain fit heavy well well.</pcm>
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
The Yoruba, Igbo and Hausa example lines are hand-written and have not been checked by native speakers.
|
| 107 |
+
|
| 108 |
+
## Samples
|
| 109 |
+
|
| 110 |
+
- `samples/pcm_eng_dynamic_accent_seed{0,1,2}.mp3`: "this is dynamic accent, e dey sound nice abi?" (English then Pidgin), seeds 0, 1 and 2. These are Phase 2 final renders (`--pause_mode model --speed 1.0`).
|
| 111 |
+
- `samples/prosody_v2/*.mp3`: the same custom sentence plus `para_ibo_pcm_eng`, `para_yor_eng`, `para_hau_eng_pcm`, `ibo_000` and `yor_000`, rendered with the defaults (`--pause_mode clause --speed 1.05`, seed 0).
|
| 112 |
+
|
| 113 |
+
## Prosody v2 (inference-time pause and pace fix)
|
| 114 |
+
|
| 115 |
+
**Speaking rate** is articulation rate: syllables per second of speech, with pauses of 0.15 s or more excluded. It uses the same syllable counter for every source. Reference speech is 150 clips per source. The system rate comes from the Phase 2 final renders of the eval set.
|
| 116 |
+
|
| 117 |
+
| | anchor (yor) | yor | ibo | hau | pcm | eng |
|
| 118 |
+
|---|---|---|---|---|---|---|
|
| 119 |
+
| reference speech (syl/s) | 4.45 | 4.78 (WAXAL, other speakers) | 4.73 | 5.71 | 3.87 | 4.53 (FLEURS) |
|
| 120 |
+
| Phase 2 final system (syl/s) | 4.20 (yor) | 4.20 | 4.50 | 3.98 | 3.63 | 4.77 |
|
| 121 |
+
| reference ÷ system | 1.06 | 1.14 | 1.05 | 1.43 | 1.06 | 0.95 |
|
| 122 |
+
|
| 123 |
+
The median ratio is 1.06, so the default `--speed` is **1.05** (rounded to 0.05 and clamped to 1.0–1.2). Hausa is the outlier. Its vowel-based syllable count is the roughest of the five.
|
| 124 |
+
|
| 125 |
+
**Before and after** on the six sample items (custom sentence, 3 paragraphs, ibo_000, yor_000), seed 0. Pauses are measured at forced-aligned word boundaries with the Phase 2 report harness. The anchor row is real WAXAL recordings. With only 6 items, read these as a direction, not a result.
|
| 126 |
+
|
| 127 |
+
| | comma pause (s) | sentence pause (s) | unpunctuated pause rate (>0.25 s) | articulation rate (syl/s) | para CER | para CER (no tones) | all-6 CER | SIM |
|
| 128 |
+
|---|---|---|---|---|---|---|---|---|
|
| 129 |
+
| real anchor | 0.417 | 0.543 | 0.137 | 4.45 | – | – | – | ceiling ≈0.84 |
|
| 130 |
+
| before: Phase 2 final (`model`, 1.0) | 0.339 | 0.488 | 0.057 | 4.26 | 0.175 | 0.124 | 0.175 | 0.814 |
|
| 131 |
+
| clause, speed 1.0 | 0.339 | 0.526 | 0.095 | 3.95 | 0.179 | 0.123 | 0.166 | 0.775 |
|
| 132 |
+
| **after: clause, speed 1.05 (default)** | 0.275 (≈0.37 *) | 0.550 | 0.086 | 4.18 | 0.193 | 0.143 | 0.207 | 0.750 |
|
| 133 |
+
| clause, speed 1.10 | 0.315 | 0.550 | 0.067 | 4.37 | 0.244 | 0.194 | 0.224 | 0.748 |
|
| 134 |
+
| model pauses, speed 1.05 | 0.235 | 0.441 | 0.029 | 4.35 | 0.189 | 0.149 | 0.188 | 0.788 |
|
| 135 |
+
|
| 136 |
+
\* At two commas that follow the one-letter Pidgin particle "o," the aligner moves the inserted gap to the boundary before "o". This reads as a 0.0 s comma plus one unpunctuated pause. The other comma gaps in clause mode measure 0.37–0.39 s, against the 0.40 s target.
|
| 137 |
+
|
| 138 |
+
- **Pauses:** clause mode moves comma and sentence pauses and the phrase-break rate toward the anchor's numbers. Sentence pauses go from 0.49 to 0.55 s against the anchor's 0.54 s.
|
| 139 |
+
- **Pace:** generating short chunks one at a time makes speech about 7% slower (4.26 to 3.95 syl/s). So 1.05 only brings the rate back to about where the final system was, and it is still below the anchor.
|
| 140 |
+
- **Cost:** WSOLA speed-up and chunking each lower ECAPA similarity (0.81 to 0.75–0.79). At 1.10, paragraph CER rises clearly (0.175 to 0.244). At 1.05 the rise is small (0.175 to 0.193), and mixed across paragraphs: Hausa got better, Igbo and Yoruba got worse. A blind listening check should decide between 1.0 and 1.05.
|
| 141 |
+
|
| 142 |
+
## Evaluation (Phase 2 report)
|
| 143 |
+
|
| 144 |
+
CER is ASR round-trip: MMS-1b-all for ibo/hau/yor, Whisper-large-v3-turbo for eng and pcm, and min(MMS, Whisper) for code-switched (CS) items and paragraphs. "No tones" strips the diacritics. SIM is ECAPA cosine to the anchor centroid; the ceiling is about 0.84 on real held-out anchor clips. Pidgin CER is limited by the judge: real Pidgin recordings score 0.43. Full eval set: 30 items per language + 9 CS sentences + 3 paragraphs = 162 items, single sample (seed 0, anchor prompt).
|
| 145 |
+
|
| 146 |
+
| system | ibo CER | hau CER | yor CER (no tones) | pcm CER | eng CER | CS CER | para CER | SIM to anchor (mean of 5 langs) | SIM spread | CS half-vs-half SIM |
|
| 147 |
+
|---|---|---|---|---|---|---|---|---|---|---|
|
| 148 |
+
| Phase 1 Arm B (smoke) | 0.212 | 0.244 | 0.407 (0.294) | 0.412 | 0.035 | 0.201 | 0.195 | 0.804 | 0.030 | 0.649 |
|
| 149 |
+
| p2_lm (profile A) + SFT flow | 0.199 | 0.183 | 0.223 (0.140) | 0.190 | 0.038 | 0.184 | 0.176 | 0.769 | 0.069 | 0.634 |
|
| 150 |
+
| **p2_lm_B (profile B) + SFT flow (this release)** | 0.188 | 0.188 | 0.224 (0.137) | 0.174 | 0.034 | 0.167 | 0.173 | 0.782 | 0.039 | 0.642 |
|
| 151 |
+
|
| 152 |
+
Pause and pitch (energy VAD on forced-aligned word boundaries):
|
| 153 |
+
|
| 154 |
+
| system | int. pause p90 (s) | pauses / s | spurious pause rate | comma pause (s) | pause-length W1 vs anchor | para: sentence pause (s) | para: pause p90 (s) | F0 std (st) | F0 range (st) | yor tone-direction acc (pairs) |
|
| 155 |
+
|---|---|---|---|---|---|---|---|---|---|---|
|
| 156 |
+
| **real anchor recordings** (WAXAL val+test, raw) | 1.071 | 0.624 | 0.137 | 0.417 | 0 | 0.543 | – | 3.152 | 10.173 | 0.750 (2990) |
|
| 157 |
+
| Phase 1 Arm B (smoke) | 1.334 | 0.584 | 0.044 | 0.245 | 0.147 | 0.315 | 1.035 | 3.058 | 9.999 | 0.777 (251) |
|
| 158 |
+
| p2_lm (profile A) + SFT flow | 0.414 | 0.328 | 0.008 | 0.263 | 0.330 | 0.509 | 0.547 | 3.038 | 10.069 | 0.752 (254) |
|
| 159 |
+
| **p2_lm_B (profile B) + SFT flow (this release)** | 0.460 | 0.288 | 0.087 | 0.251 | 0.262 | 0.488 | 0.531 | 3.057 | 10.134 | 0.749 (255) |
|
| 160 |
+
|
| 161 |
+
Zero-shot baselines on the 62-item stage subset (for context): CosyVoice2 zero-shot ibo/hau/yor CER 0.689 / 0.281 / 0.852, and CosyVoice3 zero-shot 0.629 / 0.197 / 0.717, against 0.173 / 0.220 / 0.226 for Phase 2 (p2_lm + SFT flow).
|
| 162 |
+
|
| 163 |
+
## Known limitations
|
| 164 |
+
|
| 165 |
+
- In `model` mode, comma pauses are short (0.25 s on the full eval set, against the anchor's 0.42 s). `clause` mode sets them directly (see Prosody v2).
|
| 166 |
+
- Speaker similarity is 0.78 against Phase 1's 0.80. English and Pidgin drift the most from the anchor.
|
| 167 |
+
- English after a Nigerian-language span keeps a strong local accent.
|
| 168 |
+
- Pidgin has only about 6 h of training data and no reliable ASR judge.
|
| 169 |
+
- Yoruba tone quality needs human listening; the automatic tone metric is saturated.
|
| 170 |
+
- Naturalness has not been rated by native speakers yet (no MOS / A/B).
|
| 171 |
+
|
| 172 |
+
## Training data
|
| 173 |
+
|
| 174 |
+
The training data includes CC-BY-SA sources. Datasets used:
|
| 175 |
+
- **WAXAL** (Yoruba, Igbo, Hausa, Nigerian Pidgin; includes the anchor voice, Yoruba speaker 4), CC-BY-SA-4.0
|
| 176 |
+
- **BibleTTS** (Yoruba and Hausa), CC-BY-SA
|
| 177 |
+
- **FLEURS** (English, Hausa, Igbo, Yoruba)
|
| 178 |
+
- **OpenSLR SLR70** (Nigerian English)
|
| 179 |
+
- **OpenSLR SLR86** (Yoruba)
|
| 180 |
+
- 4,000 code-switched pairs built from the sets above
|
| 181 |
+
|
| 182 |
+
## Licence
|
| 183 |
+
|
| 184 |
+
Apache-2.0 (see `LICENSE`). The base model, CosyVoice2-0.5B, is Apache-2.0.
|
code/da/__init__.py
ADDED
|
File without changes
|
code/da/data.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Arm A data: memmapped Vocos mels + byte tokens with per-token language ids; frame-budget bucketing.
|
| 2 |
+
Beta-binomial alignment priors are computed in DataLoader workers."""
|
| 3 |
+
import json, os, random
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
from torch.utils.data import Dataset, Sampler
|
| 7 |
+
|
| 8 |
+
from .text import encode, LANG2ID
|
| 9 |
+
from .model import beta_binomial_prior
|
| 10 |
+
|
| 11 |
+
N_MELS = 100
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def load_meta(root, datasets, split="train", max_frames=1400, max_tokens=420):
|
| 15 |
+
items, mm = [], {}
|
| 16 |
+
for ds in datasets:
|
| 17 |
+
p = f"{root}/{ds}/meta.jsonl"
|
| 18 |
+
if not os.path.exists(p) or os.path.getsize(p) == 0:
|
| 19 |
+
continue
|
| 20 |
+
mm[ds] = np.memmap(f"{root}/{ds}/mels.f16", dtype=np.float16, mode="r").reshape(-1, N_MELS)
|
| 21 |
+
for l in open(p, encoding="utf-8"):
|
| 22 |
+
m = json.loads(l)
|
| 23 |
+
if m["split"] != split or m["nf"] > max_frames:
|
| 24 |
+
continue
|
| 25 |
+
if len(m["text"].encode("utf-8")) + 2 > max_tokens:
|
| 26 |
+
continue
|
| 27 |
+
items.append(m)
|
| 28 |
+
return items, mm
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class TTSData(Dataset):
|
| 32 |
+
def __init__(self, items, mm, spk2id):
|
| 33 |
+
self.items, self.mm, self.spk2id = items, mm, spk2id
|
| 34 |
+
|
| 35 |
+
def __len__(self):
|
| 36 |
+
return len(self.items)
|
| 37 |
+
|
| 38 |
+
def __getitem__(self, i):
|
| 39 |
+
m = self.items[i]
|
| 40 |
+
mel = np.asarray(self.mm[m["ds"]][m["off"]: m["off"] + m["nf"]], dtype=np.float32)
|
| 41 |
+
tok, _ = encode(f"<{m['lang']}>{m['text']}</{m['lang']}>")
|
| 42 |
+
lang = [LANG2ID[m["lang"]]] * len(tok)
|
| 43 |
+
return dict(tok=tok, lang=lang, mel=mel, spk=self.spk2id[m["spk"]], id=m["id"])
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def collate(batch):
|
| 47 |
+
B = len(batch)
|
| 48 |
+
N = max(len(b["tok"]) for b in batch)
|
| 49 |
+
T = max(len(b["mel"]) for b in batch)
|
| 50 |
+
tok = torch.zeros(B, N, dtype=torch.long)
|
| 51 |
+
lang = torch.zeros(B, N, dtype=torch.long)
|
| 52 |
+
mel = torch.zeros(B, T, N_MELS)
|
| 53 |
+
prior = torch.full((B, T, N), -1e4)
|
| 54 |
+
tl, ml = torch.zeros(B, dtype=torch.long), torch.zeros(B, dtype=torch.long)
|
| 55 |
+
for i, b in enumerate(batch):
|
| 56 |
+
n, t = len(b["tok"]), len(b["mel"])
|
| 57 |
+
tok[i, :n] = torch.tensor(b["tok"]); lang[i, :n] = torch.tensor(b["lang"])
|
| 58 |
+
mel[i, :t] = torch.from_numpy(b["mel"]); tl[i], ml[i] = n, t
|
| 59 |
+
prior[i, :t, :n] = beta_binomial_prior(n, t)
|
| 60 |
+
spk = torch.tensor([b["spk"] for b in batch])
|
| 61 |
+
return dict(tok=tok, lang=lang, tlens=tl, mel=mel, mlens=ml, spk=spk, prior=prior)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class FrameBucketSampler(Sampler):
|
| 65 |
+
"""Batches of similar length under a padded-frame budget; dataset weights by repetition."""
|
| 66 |
+
|
| 67 |
+
def __init__(self, items, frame_budget=40000, weights=None, seed=0):
|
| 68 |
+
self.items, self.budget, self.seed, self.epoch = items, frame_budget, seed, 0
|
| 69 |
+
self.weights = weights or {}
|
| 70 |
+
|
| 71 |
+
def _batches(self):
|
| 72 |
+
rng = random.Random(self.seed + self.epoch)
|
| 73 |
+
idx = []
|
| 74 |
+
for i, m in enumerate(self.items):
|
| 75 |
+
w = self.weights.get(m["ds"], 1.0)
|
| 76 |
+
idx += [i] * int(w) + ([i] if rng.random() < (w - int(w)) else [])
|
| 77 |
+
idx.sort(key=lambda i: self.items[i]["nf"] + rng.random() * 30)
|
| 78 |
+
batches, cur, mx = [], [], 0
|
| 79 |
+
for i in idx:
|
| 80 |
+
nf = self.items[i]["nf"]
|
| 81 |
+
if cur and max(mx, nf) * (len(cur) + 1) > self.budget:
|
| 82 |
+
batches.append(cur); cur, mx = [], 0
|
| 83 |
+
cur.append(i); mx = max(mx, nf)
|
| 84 |
+
if cur:
|
| 85 |
+
batches.append(cur)
|
| 86 |
+
rng.shuffle(batches)
|
| 87 |
+
return batches
|
| 88 |
+
|
| 89 |
+
def __iter__(self):
|
| 90 |
+
b = self._batches()
|
| 91 |
+
self.epoch += 1
|
| 92 |
+
return iter(b)
|
| 93 |
+
|
| 94 |
+
def __len__(self):
|
| 95 |
+
return len(self._batches())
|
code/da/model.py
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Dynamic Accent - Arm A, written from scratch in PyTorch.
|
| 2 |
+
|
| 3 |
+
Backbone: Parallel Tacotron 2 (Elias et al. 2021, arXiv 2103.14574): non-autoregressive, token durations,
|
| 4 |
+
differentiable learned upsampling (MLP over start/end offsets -> token->frame weights W), iterative mel loss.
|
| 5 |
+
Multilingual recipe: Zhang et al. 2019 (arXiv 1907.04448): separate speaker + language embeddings,
|
| 6 |
+
domain-adversarial speaker classifier (gradient reversal) on text-encoder outputs, residual VAE.
|
| 7 |
+
|
| 8 |
+
Documented deviation (alignment): PT2 trains durations end-to-end with Soft-DTW. Soft-DTW is O(T*N)-sequential and
|
| 9 |
+
too slow for a 2 h smoke budget, so we use the internal unsupervised alignment-learning objective of
|
| 10 |
+
Badlani et al. 2022 ("One TTS Alignment To Rule Them All", arXiv 2108.10447): a small text/mel attention trained with
|
| 11 |
+
a CTC forward-sum loss + beta-binomial prior, hardened with monotonic alignment search (MAS). The hard durations
|
| 12 |
+
supervise the duration predictor and drive the SAME differentiable PT2 upsampler at train time; predicted durations
|
| 13 |
+
drive it at inference. No external aligner is used.
|
| 14 |
+
"""
|
| 15 |
+
import math
|
| 16 |
+
import numpy as np
|
| 17 |
+
import torch
|
| 18 |
+
import torch.nn as nn
|
| 19 |
+
import torch.nn.functional as F
|
| 20 |
+
|
| 21 |
+
from .text import VOCAB, LANGS
|
| 22 |
+
|
| 23 |
+
N_MELS = 100
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# ---------------------------------------------------------------- utils
|
| 27 |
+
def seq_mask(lens, maxlen=None):
|
| 28 |
+
maxlen = maxlen or int(lens.max())
|
| 29 |
+
return torch.arange(maxlen, device=lens.device)[None] < lens[:, None] # True = valid
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def sinusoid(T, d, device):
|
| 33 |
+
pos = torch.arange(T, device=device).float()[:, None]
|
| 34 |
+
i = torch.arange(0, d, 2, device=device).float()[None]
|
| 35 |
+
ang = pos / (10000 ** (i / d))
|
| 36 |
+
pe = torch.zeros(T, d, device=device)
|
| 37 |
+
pe[:, 0::2] = torch.sin(ang)
|
| 38 |
+
pe[:, 1::2] = torch.cos(ang)
|
| 39 |
+
return pe
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class GradReverse(torch.autograd.Function):
|
| 43 |
+
"""Gradient reversal. As in Zhang et al. 2019 the reversed gradient sent into the text encoder is clipped
|
| 44 |
+
(here: its L2 norm is capped at `clip`), which keeps the min-max game from blowing up the encoder."""
|
| 45 |
+
|
| 46 |
+
@staticmethod
|
| 47 |
+
def forward(ctx, x, lam, clip):
|
| 48 |
+
ctx.lam, ctx.clip = lam, clip
|
| 49 |
+
return x.view_as(x)
|
| 50 |
+
|
| 51 |
+
@staticmethod
|
| 52 |
+
def backward(ctx, g):
|
| 53 |
+
g = -ctx.lam * g
|
| 54 |
+
if ctx.clip is not None:
|
| 55 |
+
n = g.norm()
|
| 56 |
+
if n > ctx.clip:
|
| 57 |
+
g = g * (ctx.clip / (n + 1e-12))
|
| 58 |
+
return g, None, None
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def grad_reverse(x, lam=1.0, clip=0.5):
|
| 62 |
+
return GradReverse.apply(x, lam, clip)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class ConvNorm(nn.Module):
|
| 66 |
+
def __init__(self, cin, cout, k=5, drop=0.1):
|
| 67 |
+
super().__init__()
|
| 68 |
+
self.conv = nn.Conv1d(cin, cout, k, padding=k // 2)
|
| 69 |
+
self.ln = nn.LayerNorm(cout)
|
| 70 |
+
self.drop = nn.Dropout(drop)
|
| 71 |
+
|
| 72 |
+
def forward(self, x, mask): # x [B,T,C], mask [B,T]
|
| 73 |
+
x = x.masked_fill(~mask[..., None], 0)
|
| 74 |
+
y = self.conv(x.transpose(1, 2)).transpose(1, 2)
|
| 75 |
+
return self.drop(F.relu(self.ln(y)))
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class FFTBlock(nn.Module):
|
| 79 |
+
"""Pre-LN self-attention + convolutional feed-forward (FastSpeech/PT2-style lightweight block)."""
|
| 80 |
+
|
| 81 |
+
def __init__(self, d, heads=4, ff=4, k=3, drop=0.1):
|
| 82 |
+
super().__init__()
|
| 83 |
+
self.ln1 = nn.LayerNorm(d)
|
| 84 |
+
self.att = nn.MultiheadAttention(d, heads, dropout=drop, batch_first=True)
|
| 85 |
+
self.ln2 = nn.LayerNorm(d)
|
| 86 |
+
self.c1 = nn.Conv1d(d, d * ff, k, padding=k // 2)
|
| 87 |
+
self.c2 = nn.Conv1d(d * ff, d, 1)
|
| 88 |
+
self.drop = nn.Dropout(drop)
|
| 89 |
+
|
| 90 |
+
def forward(self, x, mask):
|
| 91 |
+
h = self.ln1(x)
|
| 92 |
+
a, _ = self.att(h, h, h, key_padding_mask=~mask, need_weights=False)
|
| 93 |
+
x = x + self.drop(a)
|
| 94 |
+
h = self.ln2(x).masked_fill(~mask[..., None], 0).transpose(1, 2)
|
| 95 |
+
h = self.c2(F.gelu(self.c1(h))).transpose(1, 2)
|
| 96 |
+
x = x + self.drop(h)
|
| 97 |
+
return x.masked_fill(~mask[..., None], 0)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
# ---------------------------------------------------------------- modules
|
| 101 |
+
class TextEncoder(nn.Module):
|
| 102 |
+
def __init__(self, d, n_lang, layers=6):
|
| 103 |
+
super().__init__()
|
| 104 |
+
self.emb = nn.Embedding(VOCAB, d, padding_idx=0)
|
| 105 |
+
self.lang_in = nn.Embedding(n_lang, d) # per-token language id at the input
|
| 106 |
+
self.convs = nn.ModuleList([ConvNorm(d, d, 5) for _ in range(3)])
|
| 107 |
+
self.blocks = nn.ModuleList([FFTBlock(d) for _ in range(layers)])
|
| 108 |
+
|
| 109 |
+
def forward(self, tok, lang, mask):
|
| 110 |
+
x = self.emb(tok) + self.lang_in(lang)
|
| 111 |
+
for c in self.convs:
|
| 112 |
+
x = x + c(x, mask)
|
| 113 |
+
x = x + sinusoid(x.shape[1], x.shape[2], x.device)[None]
|
| 114 |
+
for b in self.blocks:
|
| 115 |
+
x = b(x, mask)
|
| 116 |
+
return x
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
class ResidualVAE(nn.Module):
|
| 120 |
+
"""Global residual (prosody) encoder: mel -> q(z|x); z broadcast to every token (Zhang 2019 / Hsu 2019)."""
|
| 121 |
+
|
| 122 |
+
def __init__(self, zdim=16, h=256):
|
| 123 |
+
super().__init__()
|
| 124 |
+
self.convs = nn.Sequential(
|
| 125 |
+
nn.Conv1d(N_MELS, h, 3, padding=1), nn.ReLU(),
|
| 126 |
+
nn.Conv1d(h, h, 3, stride=2, padding=1), nn.ReLU(),
|
| 127 |
+
nn.Conv1d(h, h, 3, stride=2, padding=1), nn.ReLU())
|
| 128 |
+
self.gru = nn.GRU(h, h // 2, batch_first=True, bidirectional=True)
|
| 129 |
+
self.mu = nn.Linear(h, zdim)
|
| 130 |
+
self.logvar = nn.Linear(h, zdim)
|
| 131 |
+
|
| 132 |
+
def forward(self, mel, mel_lens):
|
| 133 |
+
h = self.convs(mel.transpose(1, 2)).transpose(1, 2)
|
| 134 |
+
lens = torch.clamp((mel_lens + 3) // 4, min=1)
|
| 135 |
+
h, _ = self.gru(h)
|
| 136 |
+
m = seq_mask(lens, h.shape[1]).float()[..., None]
|
| 137 |
+
h = (h * m).sum(1) / m.sum(1)
|
| 138 |
+
mu, lv = self.mu(h), self.logvar(h).clamp(-8, 8)
|
| 139 |
+
z = mu + torch.randn_like(mu) * torch.exp(0.5 * lv) if self.training else mu
|
| 140 |
+
kl = 0.5 * (mu.pow(2) + lv.exp() - 1 - lv).sum(-1).mean()
|
| 141 |
+
return z, kl
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
class Aligner(nn.Module):
|
| 145 |
+
"""Unsupervised text-mel alignment (Badlani et al. 2022)."""
|
| 146 |
+
|
| 147 |
+
def __init__(self, d, h=256, temp=0.0005):
|
| 148 |
+
super().__init__()
|
| 149 |
+
self.temp = temp
|
| 150 |
+
self.key = nn.Sequential(nn.Conv1d(d, h * 2, 3, padding=1), nn.ReLU(), nn.Conv1d(h * 2, 80, 1))
|
| 151 |
+
self.query = nn.Sequential(nn.Conv1d(N_MELS, h * 2, 3, padding=1), nn.ReLU(),
|
| 152 |
+
nn.Conv1d(h * 2, h, 1), nn.ReLU(), nn.Conv1d(h, 80, 1))
|
| 153 |
+
|
| 154 |
+
def forward(self, txt, mel, tmask, mmask, prior_log):
|
| 155 |
+
k = self.key(txt.transpose(1, 2)) # B,80,N
|
| 156 |
+
q = self.query(mel.transpose(1, 2)) # B,80,T
|
| 157 |
+
q, k = q.float(), k.float()
|
| 158 |
+
dist = (q.pow(2).sum(1)[:, :, None] + k.pow(2).sum(1)[:, None, :]
|
| 159 |
+
- 2 * torch.bmm(q.transpose(1, 2), k)).clamp_min(0) # B,T,N (memory-light ||q-k||^2)
|
| 160 |
+
logits = -self.temp * dist
|
| 161 |
+
logits = logits.masked_fill(~tmask[:, None, :], -1e4)
|
| 162 |
+
logp = F.log_softmax(logits, -1) + prior_log # prior in log domain
|
| 163 |
+
logp = logp.masked_fill(~tmask[:, None, :], -1e4)
|
| 164 |
+
soft = F.softmax(logp, -1) * mmask[..., None]
|
| 165 |
+
return logits, logp, soft
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def forward_sum_loss(logits, tlens, mlens, blank_logprob=-1.0):
|
| 169 |
+
"""CTC forward-sum: every token must be visited monotonically. logits [B,T,N] (pre-softmax, masked)."""
|
| 170 |
+
B, T, N = logits.shape
|
| 171 |
+
lp = F.pad(logits, (1, 0), value=blank_logprob)
|
| 172 |
+
lp = F.log_softmax(lp, -1).transpose(0, 1) # T,B,N+1
|
| 173 |
+
tgt = torch.arange(1, N + 1, device=logits.device)[None].expand(B, N)
|
| 174 |
+
return F.ctc_loss(lp.float(), tgt, mlens, tlens, blank=0, reduction="mean", zero_infinity=True)
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def _mas_np(logp, N, T):
|
| 178 |
+
"""Monotonic alignment search on one [T,N] log-prob matrix -> hard [T,N]."""
|
| 179 |
+
Q = np.full((T, N), -np.inf, dtype=np.float32)
|
| 180 |
+
Q[0, 0] = logp[0, 0]
|
| 181 |
+
for t in range(1, T):
|
| 182 |
+
for n in range(min(N, t + 1)):
|
| 183 |
+
stay = Q[t - 1, n]
|
| 184 |
+
move = Q[t - 1, n - 1] if n > 0 else -np.inf
|
| 185 |
+
Q[t, n] = max(stay, move) + logp[t, n]
|
| 186 |
+
path = np.zeros((T, N), dtype=np.float32)
|
| 187 |
+
n = N - 1
|
| 188 |
+
for t in range(T - 1, -1, -1):
|
| 189 |
+
path[t, n] = 1
|
| 190 |
+
if t > 0 and n > 0 and (n == t or Q[t - 1, n - 1] > Q[t - 1, n]):
|
| 191 |
+
n -= 1
|
| 192 |
+
return path
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
try:
|
| 196 |
+
import numba
|
| 197 |
+
|
| 198 |
+
_mas_np = numba.njit(cache=True)(_mas_np) # noqa: F811
|
| 199 |
+
except Exception: # numba optional (CPU unit tests)
|
| 200 |
+
pass
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
@torch.no_grad()
|
| 204 |
+
def mas(logp, tlens, mlens):
|
| 205 |
+
out = torch.zeros_like(logp)
|
| 206 |
+
lp = logp.float().cpu().numpy()
|
| 207 |
+
for b in range(lp.shape[0]):
|
| 208 |
+
T, N = int(mlens[b]), int(tlens[b])
|
| 209 |
+
out[b, :T, :N] = torch.from_numpy(_mas_np(np.ascontiguousarray(lp[b, :T, :N]), N, T))
|
| 210 |
+
return out
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
_prior_cache = {}
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def beta_binomial_prior(N, T, scale=1.0):
|
| 217 |
+
from scipy.stats import betabinom
|
| 218 |
+
key = (N, T)
|
| 219 |
+
if key not in _prior_cache:
|
| 220 |
+
x = np.arange(N)[None]
|
| 221 |
+
t = np.arange(1, T + 1)[:, None]
|
| 222 |
+
pmf = betabinom.pmf(x, N - 1, scale * t, scale * (T + 1 - t))
|
| 223 |
+
_prior_cache[key] = torch.from_numpy(np.log(pmf + 1e-8)).float()
|
| 224 |
+
if len(_prior_cache) > 20000:
|
| 225 |
+
_prior_cache.clear()
|
| 226 |
+
return _prior_cache[key]
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def batch_prior(tlens, mlens, N, T, device):
|
| 230 |
+
p = torch.full((len(tlens), T, N), -1e4)
|
| 231 |
+
for b in range(len(tlens)):
|
| 232 |
+
n, t = int(tlens[b]), int(mlens[b])
|
| 233 |
+
p[b, :t, :n] = beta_binomial_prior(n, t)
|
| 234 |
+
return p.to(device)
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
class DurationPredictor(nn.Module):
|
| 238 |
+
def __init__(self, d, h=256):
|
| 239 |
+
super().__init__()
|
| 240 |
+
self.convs = nn.ModuleList([ConvNorm(d, h, 3), ConvNorm(h, h, 3), ConvNorm(h, h, 3)])
|
| 241 |
+
self.out = nn.Linear(h, 1)
|
| 242 |
+
|
| 243 |
+
def forward(self, x, mask):
|
| 244 |
+
for c in self.convs:
|
| 245 |
+
x = c(x, mask)
|
| 246 |
+
return self.out(x).squeeze(-1).masked_fill(~mask, 0) # log(1 + d)
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
class LearnedUpsampler(nn.Module):
|
| 250 |
+
"""PT2 differentiable upsampling: W[t,k] = softmax_k MLP(S_tk, E_tk, h_k); O = W H + proj(sum_k W C)."""
|
| 251 |
+
|
| 252 |
+
def __init__(self, d, hw=16, hc=8):
|
| 253 |
+
super().__init__()
|
| 254 |
+
self.se_w = nn.Linear(2, hw)
|
| 255 |
+
self.h_w = nn.Linear(d, hw)
|
| 256 |
+
self.w_out = nn.Linear(hw, 1)
|
| 257 |
+
self.se_c = nn.Linear(2, hc)
|
| 258 |
+
self.h_c = nn.Linear(d, hc)
|
| 259 |
+
self.c_out = nn.Linear(hc, d)
|
| 260 |
+
|
| 261 |
+
def forward(self, H, dur, tmask, T):
|
| 262 |
+
dur = dur.masked_fill(~tmask, 0)
|
| 263 |
+
e = torch.cumsum(dur, 1)
|
| 264 |
+
s = e - dur
|
| 265 |
+
t = torch.arange(T, device=H.device).float()[None, :, None] + 0.5
|
| 266 |
+
S = (t - s[:, None, :]) / 10.0
|
| 267 |
+
E = (e[:, None, :] - t) / 10.0
|
| 268 |
+
se = torch.stack([S, E], -1) # B,T,N,2
|
| 269 |
+
lw = self.w_out(F.silu(self.se_w(se) + self.h_w(H)[:, None])).squeeze(-1)
|
| 270 |
+
lw = lw.masked_fill(~tmask[:, None, :], -1e4)
|
| 271 |
+
W = torch.softmax(lw, -1) # B,T,N
|
| 272 |
+
C = F.silu(self.se_c(se) + self.h_c(H)[:, None]) # B,T,N,hc
|
| 273 |
+
out = torch.bmm(W, H) + self.c_out(torch.einsum("btn,btnc->btc", W, C))
|
| 274 |
+
return out, W
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
class MelDecoder(nn.Module):
|
| 278 |
+
def __init__(self, d, layers=6, n_iter=3):
|
| 279 |
+
super().__init__()
|
| 280 |
+
self.blocks = nn.ModuleList([FFTBlock(d) for _ in range(layers)])
|
| 281 |
+
self.n_iter = n_iter
|
| 282 |
+
self.outs = nn.ModuleList([nn.Linear(d, N_MELS) for _ in range(n_iter)])
|
| 283 |
+
|
| 284 |
+
def forward(self, x, mask):
|
| 285 |
+
x = x + sinusoid(x.shape[1], x.shape[2], x.device)[None]
|
| 286 |
+
mels = []
|
| 287 |
+
L = len(self.blocks)
|
| 288 |
+
for i, b in enumerate(self.blocks):
|
| 289 |
+
x = b(x, mask)
|
| 290 |
+
j = i - (L - self.n_iter)
|
| 291 |
+
if j >= 0:
|
| 292 |
+
mels.append(self.outs[j](x))
|
| 293 |
+
return mels
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
# ---------------------------------------------------------------- full model
|
| 297 |
+
class DynamicAccentTTS(nn.Module):
|
| 298 |
+
def __init__(self, n_spk, n_lang=len(LANGS), d=384, enc_layers=6, dec_layers=6, spk_dim=128, lang_dim=64,
|
| 299 |
+
zdim=16, adv_hidden=256, mel_mean=None, mel_std=None, align_temp=0.0005):
|
| 300 |
+
super().__init__()
|
| 301 |
+
self.n_spk = n_spk
|
| 302 |
+
self.encoder = TextEncoder(d, n_lang, enc_layers)
|
| 303 |
+
self.spk_emb = nn.Embedding(n_spk, spk_dim)
|
| 304 |
+
self.lang_out = nn.Embedding(n_lang, lang_dim) # per-token, concatenated to encoder outputs
|
| 305 |
+
self.vae = ResidualVAE(zdim)
|
| 306 |
+
self.cond = nn.Linear(d + spk_dim + lang_dim + zdim, d)
|
| 307 |
+
self.adv = nn.Sequential(nn.LayerNorm(d), nn.Linear(d, adv_hidden), nn.ReLU(), nn.Linear(adv_hidden, n_spk))
|
| 308 |
+
self.aligner = Aligner(d, temp=align_temp)
|
| 309 |
+
self.dur = DurationPredictor(d)
|
| 310 |
+
self.up = LearnedUpsampler(d)
|
| 311 |
+
self.dec_spk = nn.Linear(spk_dim, d)
|
| 312 |
+
self.decoder = MelDecoder(d, dec_layers)
|
| 313 |
+
self.register_buffer("mel_mean", torch.tensor(mel_mean if mel_mean is not None else -2.5))
|
| 314 |
+
self.register_buffer("mel_std", torch.tensor(mel_std if mel_std is not None else 2.2))
|
| 315 |
+
|
| 316 |
+
def _cond(self, Ht, spk, lang, z):
|
| 317 |
+
B, N, _ = Ht.shape
|
| 318 |
+
s = self.spk_emb(spk)[:, None].expand(B, N, -1)
|
| 319 |
+
zz = z[:, None].expand(B, N, -1)
|
| 320 |
+
return self.cond(torch.cat([Ht, s, self.lang_out(lang), zz], -1)), s
|
| 321 |
+
|
| 322 |
+
def forward(self, tok, lang, tlens, mel, mlens, spk, step=0, adv_lambda=1.0, bin_start=4000, prior=None):
|
| 323 |
+
tmask, mmask = seq_mask(tlens, tok.shape[1]), seq_mask(mlens, mel.shape[1])
|
| 324 |
+
meln = (mel - self.mel_mean) / self.mel_std
|
| 325 |
+
Ht = self.encoder(tok, lang, tmask)
|
| 326 |
+
# adversarial speaker classifier on text encodings (Zhang 2019): GRL removes speaker info from text path
|
| 327 |
+
adv_logits = self.adv(grad_reverse(Ht, adv_lambda))
|
| 328 |
+
adv_ce = F.cross_entropy(adv_logits[tmask], spk[:, None].expand_as(tok)[tmask])
|
| 329 |
+
adv_acc = (adv_logits[tmask].argmax(-1) == spk[:, None].expand_as(tok)[tmask]).float().mean()
|
| 330 |
+
z, kl = self.vae(meln, mlens)
|
| 331 |
+
H, _ = self._cond(Ht, spk, lang, z)
|
| 332 |
+
# alignment (internal, unsupervised)
|
| 333 |
+
if prior is None:
|
| 334 |
+
prior = batch_prior(tlens, mlens, tok.shape[1], mel.shape[1], tok.device)
|
| 335 |
+
logits, logp, soft = self.aligner(self.encoder.emb(tok) + Ht.detach(), meln, tmask, mmask, prior)
|
| 336 |
+
l_fs = forward_sum_loss(logits.masked_fill(~mmask[..., None], -1e4) + prior, tlens, mlens)
|
| 337 |
+
hard = mas(logp, tlens, mlens)
|
| 338 |
+
l_bin = -(hard * torch.log(soft.clamp_min(1e-8))).sum() / hard.sum() if step >= bin_start else soft.new_zeros(())
|
| 339 |
+
dur_t = hard.sum(1) # B,N frames
|
| 340 |
+
# durations
|
| 341 |
+
logd = self.dur(H.detach() if step < 1000 else H, tmask)
|
| 342 |
+
l_dur = F.mse_loss(logd[tmask], torch.log1p(dur_t[tmask]))
|
| 343 |
+
# upsample with target durations (train) -> decoder
|
| 344 |
+
up, W = self.up(H, dur_t, tmask, mel.shape[1])
|
| 345 |
+
x = up + self.dec_spk(self.spk_emb(spk))[:, None]
|
| 346 |
+
mels = self.decoder(x, mmask)
|
| 347 |
+
m = mmask[..., None].float()
|
| 348 |
+
l_mel = sum((F.l1_loss(o, meln, reduction="none") * m).sum() / (m.sum() * N_MELS) for o in mels) / len(mels)
|
| 349 |
+
return dict(mel=l_mel, dur=l_dur, fs=l_fs, bin=l_bin, kl=kl, adv=adv_ce, adv_acc=adv_acc.detach()), mels[-1]
|
| 350 |
+
|
| 351 |
+
@torch.no_grad()
|
| 352 |
+
def infer(self, tok, lang, spk, pace=1.0, z=None):
|
| 353 |
+
tlens = torch.full((tok.shape[0],), tok.shape[1], device=tok.device)
|
| 354 |
+
tmask = seq_mask(tlens, tok.shape[1])
|
| 355 |
+
Ht = self.encoder(tok, lang, tmask)
|
| 356 |
+
if z is None:
|
| 357 |
+
z = torch.zeros(tok.shape[0], self.vae.mu.out_features, device=tok.device)
|
| 358 |
+
H, _ = self._cond(Ht, spk, lang, z)
|
| 359 |
+
dur = (torch.expm1(self.dur(H, tmask)).clamp(min=0) * pace).masked_fill(~tmask, 0)
|
| 360 |
+
T = max(int(dur.sum(1).max().round().item()), 1)
|
| 361 |
+
up, W = self.up(H, dur, tmask, T)
|
| 362 |
+
x = up + self.dec_spk(self.spk_emb(spk))[:, None]
|
| 363 |
+
mel = self.decoder(x, torch.ones(tok.shape[0], T, dtype=torch.bool, device=tok.device))[-1]
|
| 364 |
+
return mel * self.mel_std + self.mel_mean, dur
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
def count_params(m):
|
| 368 |
+
return sum(p.numel() for p in m.parameters())
|
code/da/p2common.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Phase 2 shared tools: romanisation for MMS forced alignment, chunked aligner, pause normalisation,
|
| 2 |
+
pause / F0 / Yoruba-tone metrics. Used by prep2.py (data) and eval2.py (metrics)."""
|
| 3 |
+
import re, unicodedata
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
# ------------------------------------------------------------------ text
|
| 7 |
+
_MAP = {"ọ": "o", "ẹ": "e", "ṣ": "s", "ị": "i", "ụ": "u", "ṅ": "n", "ń": "n", "ǹ": "n", "ḿ": "m", "ɓ": "b", "ɗ": "d",
|
| 8 |
+
"ƙ": "k", "ƴ": "y", "ŋ": "n", "ɛ": "e", "ɔ": "o", "ß": "s", "æ": "ae", "œ": "oe", "ø": "o", "đ": "d", "ł": "l"}
|
| 9 |
+
TONE_MARK = {"\u0301": 2, "\u0300": 0, "\u0304": 1} # acute=H, grave=L, macron=M
|
| 10 |
+
SENT_END = set(".?!")
|
| 11 |
+
CLAUSE = set(";:")
|
| 12 |
+
COMMA = set(",")
|
| 13 |
+
_VOW = set("aeiou")
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def roman_char(c):
|
| 17 |
+
"""one NFC char -> (romanised latin string, tone or None)."""
|
| 18 |
+
lc = c.lower()
|
| 19 |
+
if lc in _MAP:
|
| 20 |
+
return _MAP[lc], None
|
| 21 |
+
d = unicodedata.normalize("NFD", lc)
|
| 22 |
+
base = "".join(x for x in d if unicodedata.category(x) != "Mn")
|
| 23 |
+
tone = None
|
| 24 |
+
for x in d:
|
| 25 |
+
if x in TONE_MARK:
|
| 26 |
+
tone = TONE_MARK[x]
|
| 27 |
+
base = "".join(_MAP.get(b, b) for b in base)
|
| 28 |
+
base = "".join(b for b in base if ("a" <= b <= "z") or b == "'")
|
| 29 |
+
return base, tone
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def words_for_align(text):
|
| 33 |
+
"""text -> list of dict(word, rom, punct_after, chars=[(rom_char, orig_index, tone)]).
|
| 34 |
+
Words that romanise to nothing (digits, symbols) are aligned with the '*' star token."""
|
| 35 |
+
out = []
|
| 36 |
+
for m in re.finditer(r"\S+", text):
|
| 37 |
+
w = m.group(0)
|
| 38 |
+
chars = []
|
| 39 |
+
for k, c in enumerate(w):
|
| 40 |
+
r, t = roman_char(c)
|
| 41 |
+
for j, rc in enumerate(r):
|
| 42 |
+
chars.append((rc, m.start() + k, t if j == len(r) - 1 else None))
|
| 43 |
+
trail = re.sub(r"[\w'’]+", "", w[-3:]) if w else ""
|
| 44 |
+
p = None
|
| 45 |
+
if any(c in SENT_END for c in trail):
|
| 46 |
+
p = "sent"
|
| 47 |
+
elif any(c in CLAUSE for c in trail):
|
| 48 |
+
p = "clause"
|
| 49 |
+
elif any(c in COMMA for c in trail):
|
| 50 |
+
p = "comma"
|
| 51 |
+
rom = "".join(c[0] for c in chars)
|
| 52 |
+
out.append(dict(word=w, rom=rom if rom else "*", punct=p, chars=chars))
|
| 53 |
+
return out
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# ------------------------------------------------------------------ aligner
|
| 57 |
+
class Aligner:
|
| 58 |
+
"""torchaudio MMS_FA (CC-BY-NC weights, used only as a data tool/judge) with chunked emissions for long audio."""
|
| 59 |
+
|
| 60 |
+
def __init__(self, device="cuda"):
|
| 61 |
+
import torch, torchaudio
|
| 62 |
+
self.torch = torch
|
| 63 |
+
self.F = torchaudio.functional
|
| 64 |
+
b = torchaudio.pipelines.MMS_FA
|
| 65 |
+
self.model = b.get_model().to(device).eval().half()
|
| 66 |
+
self.dict = b.get_dict()
|
| 67 |
+
self.dev = device
|
| 68 |
+
self.rate = 320 # samples per emission frame at 16 kHz (20 ms)
|
| 69 |
+
|
| 70 |
+
def emission(self, y16, chunk_s=30, ctx_s=2):
|
| 71 |
+
torch = self.torch
|
| 72 |
+
sr = 16000
|
| 73 |
+
x = torch.from_numpy(y16).float().to(self.dev)
|
| 74 |
+
if len(y16) <= (chunk_s + 2 * ctx_s) * sr:
|
| 75 |
+
with torch.inference_mode():
|
| 76 |
+
e, _ = self.model(x[None].half())
|
| 77 |
+
return e[0].float().log_softmax(-1)
|
| 78 |
+
outs = []
|
| 79 |
+
step, ctx = chunk_s * sr, ctx_s * sr
|
| 80 |
+
nfr_total = len(y16) // self.rate
|
| 81 |
+
for s in range(0, len(y16), step):
|
| 82 |
+
a, b = max(0, s - ctx), min(len(y16), s + step + ctx)
|
| 83 |
+
with torch.inference_mode():
|
| 84 |
+
e, _ = self.model(x[a:b][None].half())
|
| 85 |
+
e = e[0].float().log_softmax(-1)
|
| 86 |
+
f0 = (s - a) // self.rate
|
| 87 |
+
nfr = min(step, len(y16) - s) // self.rate
|
| 88 |
+
outs.append(e[f0:f0 + nfr])
|
| 89 |
+
e = torch.cat(outs, 0)
|
| 90 |
+
return e[:max(nfr_total, 1)]
|
| 91 |
+
|
| 92 |
+
def align(self, y16, words):
|
| 93 |
+
"""-> per word (start_s, end_s, score) and per char spans [(start_s, end_s)], or None on failure."""
|
| 94 |
+
torch = self.torch
|
| 95 |
+
em = self.emission(y16)
|
| 96 |
+
toks, wid = [], []
|
| 97 |
+
for i, w in enumerate(words):
|
| 98 |
+
for c in w["rom"]:
|
| 99 |
+
if c in self.dict:
|
| 100 |
+
toks.append(self.dict[c]); wid.append(i)
|
| 101 |
+
if not toks or len(toks) * 1 > em.shape[0]:
|
| 102 |
+
return None
|
| 103 |
+
t = torch.tensor([toks], dtype=torch.int32, device=self.dev)
|
| 104 |
+
try:
|
| 105 |
+
ali, sc = self.F.forced_align(em[None], t, blank=0)
|
| 106 |
+
except Exception:
|
| 107 |
+
return None
|
| 108 |
+
ali, sc = ali[0].cpu().numpy(), sc[0].exp().cpu().numpy()
|
| 109 |
+
# token spans (merge repeats; blank=0)
|
| 110 |
+
spans, i, k = [], 0, 0
|
| 111 |
+
T = len(ali)
|
| 112 |
+
while i < T:
|
| 113 |
+
if ali[i] == 0:
|
| 114 |
+
i += 1; continue
|
| 115 |
+
j = i
|
| 116 |
+
while j + 1 < T and ali[j + 1] == ali[i] and not (j + 1 < T and False):
|
| 117 |
+
j += 1
|
| 118 |
+
spans.append((i, j + 1, float(sc[i:j + 1].mean())))
|
| 119 |
+
i = j + 1
|
| 120 |
+
# repeated identical consecutive tokens (e.g. "aa") collapse in the path above only if no blank between;
|
| 121 |
+
# forced_align always inserts a blank between identical tokens, so len(spans)==len(toks)
|
| 122 |
+
if len(spans) != len(toks):
|
| 123 |
+
return None
|
| 124 |
+
r = self.rate / 16000
|
| 125 |
+
W = [[None, None, []] for _ in words]
|
| 126 |
+
chars = [[] for _ in words]
|
| 127 |
+
for (a, b, s), w in zip(spans, wid):
|
| 128 |
+
if W[w][0] is None:
|
| 129 |
+
W[w][0] = a * r
|
| 130 |
+
W[w][1] = b * r; W[w][2].append(s); chars[w].append((a * r, b * r))
|
| 131 |
+
res = []
|
| 132 |
+
for i, (a, b, s) in enumerate(W):
|
| 133 |
+
if a is None:
|
| 134 |
+
return None
|
| 135 |
+
res.append(dict(start=a, end=b, score=float(np.mean(s)), chars=chars[i]))
|
| 136 |
+
return res
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
# ------------------------------------------------------------------ energy / pauses
|
| 140 |
+
def frame_db(y, sr, hop_s=0.01):
|
| 141 |
+
hop = int(sr * hop_s)
|
| 142 |
+
n = max(1, len(y) // hop)
|
| 143 |
+
fr = y[: n * hop].reshape(n, hop)
|
| 144 |
+
e = 10 * np.log10((fr.astype(np.float64) ** 2).mean(1) + 1e-10)
|
| 145 |
+
return e
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def silence_mask(y, sr, rel_db=-40.0, hop_s=0.01):
|
| 149 |
+
e = frame_db(y, sr, hop_s)
|
| 150 |
+
ref = np.percentile(e, 98)
|
| 151 |
+
return e < ref + rel_db
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def runs(mask):
|
| 155 |
+
"""-> list of (start, end) index runs where mask is True."""
|
| 156 |
+
out, i, n = [], 0, len(mask)
|
| 157 |
+
while i < n:
|
| 158 |
+
if mask[i]:
|
| 159 |
+
j = i
|
| 160 |
+
while j < n and mask[j]:
|
| 161 |
+
j += 1
|
| 162 |
+
out.append((i, j)); i = j
|
| 163 |
+
else:
|
| 164 |
+
i += 1
|
| 165 |
+
return out
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
PAUSE_TARGET = { # (min, max) silence seconds after a word, by the punctuation that follows it
|
| 169 |
+
"sent": (0.30, 0.60),
|
| 170 |
+
"clause": (0.20, 0.50),
|
| 171 |
+
"comma": (0.00, 0.40),
|
| 172 |
+
None: (0.00, 0.20),
|
| 173 |
+
}
|
| 174 |
+
# profile B (Phase 2 v2): keeps natural phrase breaks where there is no punctuation (the real anchor pauses at
|
| 175 |
+
# ~14% of unpunctuated word boundaries), still removes hesitations; set P2PAUSE=B
|
| 176 |
+
import os as _os
|
| 177 |
+
if _os.environ.get("P2PAUSE") == "B":
|
| 178 |
+
PAUSE_TARGET = {"sent": (0.35, 0.70), "clause": (0.25, 0.55), "comma": (0.00, 0.50), None: (0.00, 0.35)}
|
| 179 |
+
LEAD_S, TAIL_S = 0.12, 0.18
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def normalise_pauses(y, sr, words, ali, seg_a, seg_b, sil=None, lo_s=0.0, hi_s=None):
|
| 183 |
+
"""Rebuild one segment [word seg_a .. seg_b] of y: edge silence fixed to LEAD_S/TAIL_S, every inter-word
|
| 184 |
+
silence clamped to PAUSE_TARGET[punct] (only truly silent frames are removed; silence is inserted, as a
|
| 185 |
+
copy of the quietest local frames, when a sentence/clause boundary has too little). -> (y_out, pauses)"""
|
| 186 |
+
hop = int(0.01 * sr)
|
| 187 |
+
if sil is None:
|
| 188 |
+
sil = silence_mask(y, sr)
|
| 189 |
+
hi_s = len(y) / sr if hi_s is None else hi_s
|
| 190 |
+
xf = int(0.005 * sr)
|
| 191 |
+
pieces, pauses = [], []
|
| 192 |
+
a0 = ali[seg_a]["start"]
|
| 193 |
+
start = int(max(lo_s, a0 - LEAD_S, 0.0) * sr)
|
| 194 |
+
cur = start
|
| 195 |
+
for i in range(seg_a, seg_b):
|
| 196 |
+
e_i, s_n = ali[i]["end"], ali[i + 1]["start"]
|
| 197 |
+
f_a, f_b = int(e_i / 0.01), int(s_n / 0.01) + 1
|
| 198 |
+
# longest silent run inside the gap (with 20 ms guard to the word edges)
|
| 199 |
+
best = None
|
| 200 |
+
for ra, rb in runs(sil[f_a:f_b]):
|
| 201 |
+
if best is None or rb - ra > best[1] - best[0]:
|
| 202 |
+
best = (ra, rb)
|
| 203 |
+
lo, hi = PAUSE_TARGET[words[i]["punct"]]
|
| 204 |
+
if best is None:
|
| 205 |
+
cur_sil = 0.0; mid = int((e_i + s_n) / 2 * sr)
|
| 206 |
+
else:
|
| 207 |
+
ra, rb = (f_a + best[0]) * hop, (f_a + best[1]) * hop
|
| 208 |
+
cur_sil = (rb - ra) / sr; mid = (ra + rb) // 2
|
| 209 |
+
if cur_sil > hi and best is not None: # shrink: cut the middle of the silent run
|
| 210 |
+
cut = int((cur_sil - hi) * sr)
|
| 211 |
+
c0 = mid - cut // 2
|
| 212 |
+
pieces.append(y[cur:c0]); cur = c0 + cut
|
| 213 |
+
pauses.append(hi)
|
| 214 |
+
elif cur_sil < lo: # insert quiet frames at the quietest point of the gap
|
| 215 |
+
add = int((lo - cur_sil) * sr)
|
| 216 |
+
g0, g1 = int(e_i * sr), max(int(s_n * sr), int(e_i * sr) + hop)
|
| 217 |
+
seg = y[g0:g1]
|
| 218 |
+
if len(seg) >= hop:
|
| 219 |
+
k = int(np.argmin(frame_db(seg, sr))) * hop
|
| 220 |
+
ins_at = g0 + k
|
| 221 |
+
q = y[ins_at:ins_at + hop]
|
| 222 |
+
else:
|
| 223 |
+
ins_at, q = mid, np.zeros(hop, dtype=y.dtype)
|
| 224 |
+
filler = np.tile(q, add // hop + 1)[:add] * 0.5
|
| 225 |
+
pieces.append(y[cur:ins_at]); pieces.append(filler); cur = ins_at
|
| 226 |
+
pauses.append(lo)
|
| 227 |
+
else:
|
| 228 |
+
pauses.append(cur_sil)
|
| 229 |
+
end = min(len(y), int(min(hi_s, ali[seg_b]["end"] + TAIL_S) * sr))
|
| 230 |
+
pieces.append(y[cur:end])
|
| 231 |
+
out = np.concatenate([p for p in pieces if len(p)]) if pieces else y[start:end]
|
| 232 |
+
# pad edges with silence if the source had less than LEAD/TAIL available
|
| 233 |
+
lead_have = a0 - start / sr
|
| 234 |
+
if lead_have < LEAD_S - 0.02:
|
| 235 |
+
out = np.concatenate([np.zeros(int((LEAD_S - lead_have) * sr), out.dtype), out])
|
| 236 |
+
tail_have = end / sr - ali[seg_b]["end"]
|
| 237 |
+
if tail_have < TAIL_S - 0.02:
|
| 238 |
+
out = np.concatenate([out, np.zeros(int((TAIL_S - tail_have) * sr), out.dtype)])
|
| 239 |
+
# tiny fades at the edges
|
| 240 |
+
f = min(xf, len(out) // 4)
|
| 241 |
+
if f > 0:
|
| 242 |
+
out[:f] *= np.linspace(0, 1, f); out[-f:] *= np.linspace(1, 0, f)
|
| 243 |
+
return out.astype(np.float32), pauses
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
# ------------------------------------------------------------------ metrics on audio
|
| 247 |
+
def pause_stats(y, sr, min_pause=0.15):
|
| 248 |
+
"""VAD (energy, -40 dB vs 98th pct) pauses: leading/trailing silence and internal pauses >= min_pause."""
|
| 249 |
+
sil = silence_mask(y, sr)
|
| 250 |
+
R = runs(sil)
|
| 251 |
+
n = len(sil)
|
| 252 |
+
lead = R[0][1] * 0.01 if R and R[0][0] == 0 else 0.0
|
| 253 |
+
tail = (n - R[-1][0]) * 0.01 if R and R[-1][1] == n else 0.0
|
| 254 |
+
internal = [(b - a) * 0.01 for a, b in R if a > 0 and b < n and (b - a) * 0.01 >= min_pause]
|
| 255 |
+
speech = max(1e-3, n * 0.01 - lead - tail)
|
| 256 |
+
return dict(lead=round(lead, 3), tail=round(tail, 3), pauses=[round(p, 3) for p in internal],
|
| 257 |
+
n_pauses=len(internal), pause_rate=round(len(internal) / speech, 4),
|
| 258 |
+
pause_frac=round(sum(internal) / speech, 4), speech_s=round(speech, 3))
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def f0_track(y16):
|
| 262 |
+
import pyworld as pw
|
| 263 |
+
x = y16.astype(np.float64)
|
| 264 |
+
f0, t = pw.dio(x, 16000, f0_floor=60, f0_ceil=500, frame_period=10)
|
| 265 |
+
return pw.stonemask(x, f0, t, 16000)
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def f0_stats(y16):
|
| 269 |
+
f0 = f0_track(y16)
|
| 270 |
+
v = f0[f0 > 0]
|
| 271 |
+
if len(v) < 20:
|
| 272 |
+
return dict(voiced=float(len(v)), f0_med=None, st_std=None, st_range=None)
|
| 273 |
+
st = 12 * np.log2(v / np.median(v))
|
| 274 |
+
d = np.diff(st)
|
| 275 |
+
return dict(voiced_frac=round(len(v) / len(f0), 3), f0_med=round(float(np.median(v)), 1),
|
| 276 |
+
st_std=round(float(st.std()), 3), st_range=round(float(np.percentile(st, 95) - np.percentile(st, 5)), 3),
|
| 277 |
+
st_vel=round(float(np.abs(d[np.abs(d) < 3]).mean()), 4))
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
def tone_pairs(y16, words, ali):
|
| 281 |
+
"""Yoruba tone-direction accuracy: for consecutive tone-bearing vowels whose tones differ, is the sign of the
|
| 282 |
+
median-F0 change (semitones) the same as the sign of the tone change? -> (n_pairs, n_correct)"""
|
| 283 |
+
f0 = f0_track(y16)
|
| 284 |
+
vals = []
|
| 285 |
+
for w, a in zip(words, ali):
|
| 286 |
+
k = 0
|
| 287 |
+
for (rc, oi, tone), (s, e) in zip([c for c in w["chars"] if c[0] in "abcdefghijklmnopqrstuvwxyz'"], a["chars"]):
|
| 288 |
+
if rc in _VOW or (rc in "nm" and tone is not None):
|
| 289 |
+
t = 1 if tone is None else tone
|
| 290 |
+
fr = f0[int(s / 0.01): int(e / 0.01) + 1]
|
| 291 |
+
fr = fr[fr > 0]
|
| 292 |
+
vals.append((t, float(np.median(12 * np.log2(fr))) if len(fr) else None))
|
| 293 |
+
n = c = 0
|
| 294 |
+
for (t1, f1), (t2, f2) in zip(vals, vals[1:]):
|
| 295 |
+
if t1 == t2 or f1 is None or f2 is None:
|
| 296 |
+
continue
|
| 297 |
+
n += 1
|
| 298 |
+
c += int(np.sign(f2 - f1) == np.sign(t2 - t1))
|
| 299 |
+
return n, c
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
def wasserstein1(a, b):
|
| 303 |
+
"""1-D earth mover's distance between two samples."""
|
| 304 |
+
if not len(a) or not len(b):
|
| 305 |
+
return None
|
| 306 |
+
a, b = np.sort(a), np.sort(b)
|
| 307 |
+
q = np.linspace(0, 1, 101)
|
| 308 |
+
return float(np.mean(np.abs(np.quantile(a, q) - np.quantile(b, q))))
|
code/da/prep2.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Phase 2 data prep (H100 node): every source -> MMS forced alignment -> sentence segmentation of long clips ->
|
| 2 |
+
punctuation-consistent pause normalisation -> 24 kHz int16 + meta. Output ~/data2/<ds>/{pcm24k.i16, meta.jsonl, stats.json}
|
| 3 |
+
|
| 4 |
+
python -m da.prep2 waxal bible_hau bible_yor slr86 slr70 fleurs
|
| 5 |
+
"""
|
| 6 |
+
import os, sys, io, json, glob, csv, hashlib, time
|
| 7 |
+
import numpy as np, soundfile as sf, librosa
|
| 8 |
+
from multiprocessing import Pool
|
| 9 |
+
sys.path.insert(0, os.path.expanduser("~/da"))
|
| 10 |
+
from da.p2common import words_for_align, Aligner, normalise_pauses # noqa: E402
|
| 11 |
+
|
| 12 |
+
RAW = "/scratch/raw"
|
| 13 |
+
OUT = os.path.expanduser(os.environ.get("P2OUT", "~/data2"))
|
| 14 |
+
OLD = os.path.expanduser("~/data")
|
| 15 |
+
SR = 24000
|
| 16 |
+
BIBLE_HAU_CAP_H = 30.0
|
| 17 |
+
FLOW_OK = {"waxal", "bible_yor", "bible_hau", "slr86", "slr70"} # 48 kHz studio-ish sources (FLEURS is 16 kHz)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def clean_text(t):
|
| 21 |
+
import unicodedata, re
|
| 22 |
+
t = unicodedata.normalize("NFC", t or "")
|
| 23 |
+
t = t.replace("\u2019", "'").replace("\u2018", "'").replace("\u201c", '"').replace("\u201d", '"')
|
| 24 |
+
return re.sub(r"\s+", " ", t).strip()
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def hsplit(i):
|
| 28 |
+
return "val" if int(hashlib.md5(i.encode()).hexdigest(), 16) % 100 == 0 else "train"
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# ------------------------------------------------------------------ enumerators (yield meta + loader args)
|
| 32 |
+
def items_waxal():
|
| 33 |
+
import pyarrow.parquet as pq
|
| 34 |
+
for lang in ["yor", "ibo", "hau", "pcm"]:
|
| 35 |
+
files = sorted(glob.glob(f"{RAW}/waxal/data/TTS/{lang}/*.parquet"))
|
| 36 |
+
files = [f for f in files if not os.path.basename(f).startswith("yor_tts")]
|
| 37 |
+
for f in files:
|
| 38 |
+
bn = os.path.basename(f)
|
| 39 |
+
split = "test" if "test" in bn else ("val" if "validation" in bn else "train")
|
| 40 |
+
pf = pq.ParquetFile(f)
|
| 41 |
+
for b in pf.iter_batches(batch_size=8):
|
| 42 |
+
for r in b.to_pylist():
|
| 43 |
+
yield dict(id=f"waxal_{lang}_{r['id']}", ds="waxal", lang=lang,
|
| 44 |
+
spk=f"waxal_{lang}_{r.get('speaker_id') or 'spk'}", gender=r.get("gender") or "",
|
| 45 |
+
split=split, text=r["text"]), ("bytes", r["audio"]["bytes"])
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
FLEURS = {"hau": "ha_ng", "ibo": "ig_ng", "yor": "yo_ng", "eng": "en_us"}
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def items_fleurs():
|
| 52 |
+
"""raw FLEURS train audio (16 kHz); speaker ids = Phase 1 ECAPA pseudo-speakers from ~/data/fleurs/meta.jsonl"""
|
| 53 |
+
for l in open(f"{OLD}/fleurs/meta.jsonl", encoding="utf-8"):
|
| 54 |
+
m = json.loads(l)
|
| 55 |
+
fn = m["id"].split("_", 2)[2] + ".wav"
|
| 56 |
+
p = f"{RAW}/fleurs/data/{FLEURS[m['lang']]}/audio/train/{fn}"
|
| 57 |
+
yield dict(id=m["id"], ds="fleurs", lang=m["lang"], spk=m["spk"], gender=m.get("gender", ""),
|
| 58 |
+
split=hsplit(m["id"]), text=m["text"]), ("path", p)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def items_slr(num, lang, prefix):
|
| 62 |
+
for idx in sorted(glob.glob(f"{RAW}/slr{num}/**/line_index.tsv", recursive=True)):
|
| 63 |
+
d = os.path.dirname(idx)
|
| 64 |
+
for line in open(idx, encoding="utf-8"):
|
| 65 |
+
parts = line.rstrip("\n").split("\t")
|
| 66 |
+
if len(parts) < 2:
|
| 67 |
+
continue
|
| 68 |
+
fid, text = parts[0].strip(), parts[-1]
|
| 69 |
+
p = os.path.join(d, fid + ".wav")
|
| 70 |
+
if os.path.exists(p):
|
| 71 |
+
spk = "_".join(fid.split("_")[:2])
|
| 72 |
+
yield dict(id=f"{prefix}_{fid}", ds=prefix, lang=lang, spk=f"{prefix}_{spk}",
|
| 73 |
+
gender="female" if fid[2] == "f" else "male", split=hsplit(fid), text=text), ("path", p)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def items_bible(lang_dir, lang, ds, cap_h=None):
|
| 77 |
+
root = f"{RAW}/bible/{lang_dir}"
|
| 78 |
+
auds = sorted(glob.glob(f"{root}/**/*.flac", recursive=True) + glob.glob(f"{root}/**/*.wav", recursive=True))
|
| 79 |
+
if cap_h: # deterministic random subset of the train split, roughly cap_h hours (verse ~ 9 s on average)
|
| 80 |
+
auds.sort(key=lambda p: hashlib.md5(p.encode()).hexdigest())
|
| 81 |
+
tot = 0.0
|
| 82 |
+
for p in auds:
|
| 83 |
+
txt = os.path.splitext(p)[0] + ".txt"
|
| 84 |
+
if not os.path.exists(txt):
|
| 85 |
+
continue
|
| 86 |
+
split = "test" if "/test/" in p else ("val" if "/dev/" in p else "train")
|
| 87 |
+
if cap_h and split == "train":
|
| 88 |
+
if tot > cap_h * 3600:
|
| 89 |
+
continue
|
| 90 |
+
try:
|
| 91 |
+
tot += sf.info(p).duration
|
| 92 |
+
except Exception:
|
| 93 |
+
continue
|
| 94 |
+
yield dict(id=f"{ds}_{os.path.splitext(os.path.basename(p))[0]}", ds=ds, lang=lang, spk=ds, gender="",
|
| 95 |
+
split=split, text=open(txt, encoding="utf-8").read()), ("path", p)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def enum(ds):
|
| 99 |
+
return {"waxal": items_waxal, "fleurs": items_fleurs,
|
| 100 |
+
"slr86": lambda: items_slr(86, "yor", "slr86"), "slr70": lambda: items_slr(70, "eng", "slr70"),
|
| 101 |
+
"bible_yor": lambda: items_bible("yoruba", "yor", "bible_yor"),
|
| 102 |
+
"bible_hau": lambda: items_bible("hausa", "hau", "bible_hau", BIBLE_HAU_CAP_H)}[ds]()
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
# ------------------------------------------------------------------ workers
|
| 106 |
+
def load(arg):
|
| 107 |
+
meta, src = arg
|
| 108 |
+
try:
|
| 109 |
+
if src[0] == "bytes":
|
| 110 |
+
y, sr = sf.read(io.BytesIO(src[1]), dtype="float32", always_2d=True); y = y.mean(1)
|
| 111 |
+
elif src[0] == "path":
|
| 112 |
+
y, sr = sf.read(src[1], dtype="float32", always_2d=True); y = y.mean(1)
|
| 113 |
+
else:
|
| 114 |
+
pcm = np.memmap(src[1], dtype=np.int16, mode="r")
|
| 115 |
+
y, sr = pcm[src[2]:src[2] + src[3]].astype(np.float32) / 32768.0, 16000
|
| 116 |
+
meta["text"] = clean_text(meta["text"])
|
| 117 |
+
meta["sr_in"] = int(sr)
|
| 118 |
+
if not meta["text"] or len(y) < 0.5 * sr:
|
| 119 |
+
return meta, None, None, "empty"
|
| 120 |
+
y24 = librosa.resample(y, orig_sr=sr, target_sr=SR, res_type="soxr_hq") if sr != SR else y
|
| 121 |
+
y16 = librosa.resample(y, orig_sr=sr, target_sr=16000, res_type="soxr_hq") if sr != 16000 else y
|
| 122 |
+
return meta, y16, y24, None
|
| 123 |
+
except Exception as e: # noqa
|
| 124 |
+
return meta, None, None, f"err:{type(e).__name__}"
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def plan_segments(words, ali, dur):
|
| 128 |
+
"""word-index ranges [(a, b)] (inclusive). Short clips: one segment. Long clips: whole sentences grouped
|
| 129 |
+
into 3-14 s segments; an over-long sentence is cut at clause/comma marks."""
|
| 130 |
+
n = len(words)
|
| 131 |
+
if dur <= 20.0:
|
| 132 |
+
return [(0, n - 1)]
|
| 133 |
+
sents, a = [], 0
|
| 134 |
+
for i, w in enumerate(words):
|
| 135 |
+
if w["punct"] == "sent" or i == n - 1:
|
| 136 |
+
sents.append((a, i)); a = i + 1
|
| 137 |
+
pieces = []
|
| 138 |
+
for a, b in sents:
|
| 139 |
+
if ali[b]["end"] - ali[a]["start"] <= 16.0:
|
| 140 |
+
pieces.append((a, b)); continue
|
| 141 |
+
s = a
|
| 142 |
+
for i in range(a, b + 1):
|
| 143 |
+
if (words[i]["punct"] in ("clause", "comma") and ali[i]["end"] - ali[s]["start"] > 3.0) or i == b:
|
| 144 |
+
pieces.append((s, i)); s = i + 1
|
| 145 |
+
segs, cur = [], None
|
| 146 |
+
for a, b in pieces:
|
| 147 |
+
if cur is None:
|
| 148 |
+
cur = [a, b]; continue
|
| 149 |
+
if ali[b]["end"] - ali[cur[0]]["start"] <= 14.0 and ali[cur[1]]["end"] - ali[cur[0]]["start"] < 4.0:
|
| 150 |
+
cur[1] = b
|
| 151 |
+
else:
|
| 152 |
+
segs.append(tuple(cur)); cur = [a, b]
|
| 153 |
+
if cur:
|
| 154 |
+
segs.append(tuple(cur))
|
| 155 |
+
return segs
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def post(arg):
|
| 159 |
+
meta, y24, words, ali = arg
|
| 160 |
+
out, why = [], {}
|
| 161 |
+
segs = plan_segments(words, ali, len(y24) / SR)
|
| 162 |
+
from da.p2common import silence_mask
|
| 163 |
+
sil = silence_mask(y24, SR)
|
| 164 |
+
for k, (a, b) in enumerate(segs):
|
| 165 |
+
sc = float(np.mean([ali[i]["score"] for i in range(a, b + 1)]))
|
| 166 |
+
maxw = max(ali[i]["end"] - ali[i]["start"] for i in range(a, b + 1))
|
| 167 |
+
text = " ".join(words[i]["word"] for i in range(a, b + 1))
|
| 168 |
+
if sc < 0.30:
|
| 169 |
+
why["low_score"] = why.get("low_score", 0) + 1; continue
|
| 170 |
+
if maxw > 2.5:
|
| 171 |
+
why["long_word"] = why.get("long_word", 0) + 1; continue
|
| 172 |
+
lo = ali[a - 1]["end"] + 0.02 if a > 0 else 0.0
|
| 173 |
+
hi = ali[b + 1]["start"] - 0.02 if b + 1 < len(ali) else None
|
| 174 |
+
y, pauses = normalise_pauses(y24, SR, words, ali, a, b, sil=sil, lo_s=lo, hi_s=hi)
|
| 175 |
+
dur = len(y) / SR
|
| 176 |
+
cps = len(text) / dur
|
| 177 |
+
if not (0.8 <= dur <= 20.0):
|
| 178 |
+
why["dur"] = why.get("dur", 0) + 1; continue
|
| 179 |
+
if not (4.0 <= cps <= 25.0):
|
| 180 |
+
why["cps"] = why.get("cps", 0) + 1; continue
|
| 181 |
+
rms = np.sqrt((y ** 2).mean() + 1e-12)
|
| 182 |
+
y = y * (0.07 / rms)
|
| 183 |
+
pk = np.abs(y).max()
|
| 184 |
+
if pk > 0.99:
|
| 185 |
+
y = y * (0.99 / pk)
|
| 186 |
+
m = dict(meta)
|
| 187 |
+
if len(segs) > 1:
|
| 188 |
+
m["id"] = f"{meta['id']}_s{k:03d}"; m["src_id"] = meta["id"]; m["longform"] = True
|
| 189 |
+
m.update(text=text, dur=round(dur, 3), ascore=round(sc, 3), cps=round(cps, 2),
|
| 190 |
+
pauses=[round(p, 2) for p in pauses],
|
| 191 |
+
puncts=[words[i]["punct"] for i in range(a, b)], flow_ok=meta["ds"] in FLOW_OK)
|
| 192 |
+
out.append((m, np.clip(y * 32767, -32768, 32767).astype(np.int16)))
|
| 193 |
+
return out, why
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def run(ds):
|
| 197 |
+
od = f"{OUT}/{ds}"
|
| 198 |
+
os.makedirs(od, exist_ok=True)
|
| 199 |
+
al = Aligner()
|
| 200 |
+
stats = dict(src_items=0, src_hours=0.0, kept=0, hours=0.0, rej={}, longform_src=0, longform_hours_in=0.0,
|
| 201 |
+
longform_segments=0)
|
| 202 |
+
t0 = time.time(); off = 0
|
| 203 |
+
pending = []
|
| 204 |
+
with open(f"{od}/pcm24k.i16", "wb") as pf, open(f"{od}/meta.jsonl", "w", encoding="utf-8") as jf, \
|
| 205 |
+
Pool(20) as lp, Pool(6) as pp:
|
| 206 |
+
def drain(block=False):
|
| 207 |
+
nonlocal off
|
| 208 |
+
keep = []
|
| 209 |
+
for r in pending:
|
| 210 |
+
if block or r.ready():
|
| 211 |
+
segs, why = r.get()
|
| 212 |
+
for k, v in why.items():
|
| 213 |
+
stats["rej"][k] = stats["rej"].get(k, 0) + v
|
| 214 |
+
for m, pcm in segs:
|
| 215 |
+
m["off24"] = off; m["n24"] = int(len(pcm))
|
| 216 |
+
pf.write(pcm.tobytes()); off += len(pcm)
|
| 217 |
+
jf.write(json.dumps(m, ensure_ascii=False) + "\n")
|
| 218 |
+
stats["kept"] += 1; stats["hours"] += m["dur"] / 3600
|
| 219 |
+
if m.get("longform"):
|
| 220 |
+
stats["longform_segments"] += 1
|
| 221 |
+
else:
|
| 222 |
+
keep.append(r)
|
| 223 |
+
pending[:] = keep
|
| 224 |
+
import itertools
|
| 225 |
+
src = enum(ds)
|
| 226 |
+
if os.environ.get("LIMIT"):
|
| 227 |
+
src = itertools.islice(src, 0, None, int(os.environ.get("STRIDE", "1")))
|
| 228 |
+
src = itertools.islice(src, int(os.environ["LIMIT"]))
|
| 229 |
+
for meta, y16, y24, err in lp.imap(load, src, chunksize=2):
|
| 230 |
+
stats["src_items"] += 1
|
| 231 |
+
if err:
|
| 232 |
+
stats["rej"][err] = stats["rej"].get(err, 0) + 1; continue
|
| 233 |
+
d = len(y24) / SR
|
| 234 |
+
stats["src_hours"] += d / 3600
|
| 235 |
+
if d > 20:
|
| 236 |
+
stats["longform_src"] += 1; stats["longform_hours_in"] += d / 3600
|
| 237 |
+
if d > 2400:
|
| 238 |
+
stats["rej"]["too_long"] = stats["rej"].get("too_long", 0) + 1; continue
|
| 239 |
+
words = words_for_align(meta["text"])
|
| 240 |
+
ali = al.align(y16, words) if words else None
|
| 241 |
+
if ali is None:
|
| 242 |
+
stats["rej"]["align_fail"] = stats["rej"].get("align_fail", 0) + 1; continue
|
| 243 |
+
pending.append(pp.apply_async(post, ((meta, y24, words, ali),)))
|
| 244 |
+
if len(pending) > 64:
|
| 245 |
+
drain(block=False)
|
| 246 |
+
while len(pending) > 128:
|
| 247 |
+
time.sleep(0.05); drain(block=False)
|
| 248 |
+
if stats["src_items"] % 2000 == 0:
|
| 249 |
+
print(ds, stats["src_items"], "kept", stats["kept"], "h", round(stats["hours"], 2),
|
| 250 |
+
"rej", stats["rej"], round(time.time() - t0), "s", flush=True)
|
| 251 |
+
drain(block=True)
|
| 252 |
+
stats = {k: (round(v, 3) if isinstance(v, float) else v) for k, v in stats.items()}
|
| 253 |
+
json.dump(stats, open(f"{od}/stats.json", "w"), indent=1)
|
| 254 |
+
print(ds, "PREP2_DONE", json.dumps(stats), round(time.time() - t0), "s", flush=True)
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
if __name__ == "__main__":
|
| 258 |
+
for ds in sys.argv[1:]:
|
| 259 |
+
run(ds)
|
code/da/s3_2.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Phase 2: S3 speech tokens (speech_tokenizer_v2.onnx, CUDA EP) + CAM++ speaker embeddings for ~/data2/<ds>,
|
| 2 |
+
plus code-switched training pairs (ds "cs_pairs").
|
| 3 |
+
|
| 4 |
+
python -m da.s3_2 tok waxal fleurs ... -> s3tok.i32, s3tok.jsonl, campplus.npy
|
| 5 |
+
python -m da.s3_2 cs 4000 -> ~/data2/cs_pairs/{pcm24k.i16, meta.jsonl} (+ run tok on it)
|
| 6 |
+
"""
|
| 7 |
+
import os, sys, json, random, hashlib
|
| 8 |
+
import numpy as np, librosa, torch
|
| 9 |
+
from multiprocessing import Pool
|
| 10 |
+
|
| 11 |
+
D2 = os.path.expanduser(os.environ.get("P2OUT", "~/data2"))
|
| 12 |
+
MD = os.path.expanduser("~/models/CosyVoice2-0.5B")
|
| 13 |
+
_S = {}
|
| 14 |
+
_FB = torch.from_numpy(librosa.filters.mel(sr=16000, n_fft=400, n_mels=128)).float()
|
| 15 |
+
_WIN = torch.hann_window(400)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def log_mel_128(a):
|
| 19 |
+
st = torch.stft(a, 400, 160, window=_WIN, return_complex=True)
|
| 20 |
+
mag = st[..., :-1].abs() ** 2
|
| 21 |
+
lm = torch.clamp(_FB @ mag, min=1e-10).log10()
|
| 22 |
+
lm = torch.maximum(lm, lm.max() - 8.0)
|
| 23 |
+
return (lm + 4.0) / 4.0
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def sess(name, gpu):
|
| 27 |
+
if name not in _S:
|
| 28 |
+
import onnxruntime as ort
|
| 29 |
+
torch.set_num_threads(1)
|
| 30 |
+
o = ort.SessionOptions(); o.intra_op_num_threads = 2; o.inter_op_num_threads = 1
|
| 31 |
+
prov = ["CUDAExecutionProvider", "CPUExecutionProvider"] if gpu else ["CPUExecutionProvider"]
|
| 32 |
+
_S[name] = ort.InferenceSession(f"{MD}/{name}", sess_options=o, providers=prov)
|
| 33 |
+
return _S[name]
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def work(args):
|
| 37 |
+
path, off, n = args
|
| 38 |
+
import torchaudio.compliance.kaldi as kaldi
|
| 39 |
+
pcm = np.memmap(path, dtype=np.int16, mode="r")
|
| 40 |
+
y24 = pcm[off:off + n].astype(np.float32) / 32768.0
|
| 41 |
+
y16 = librosa.resample(y24, orig_sr=24000, target_sr=16000, res_type="soxr_hq")
|
| 42 |
+
a = torch.from_numpy(y16)
|
| 43 |
+
f = log_mel_128(a)[None]
|
| 44 |
+
s = sess("speech_tokenizer_v2.onnx", True)
|
| 45 |
+
ins = s.get_inputs()
|
| 46 |
+
tok = s.run(None, {ins[0].name: f.numpy(), ins[1].name: np.array([f.shape[2]], dtype=np.int32)})[0]
|
| 47 |
+
fb = kaldi.fbank(a[None], num_mel_bins=80, dither=0, sample_frequency=16000)
|
| 48 |
+
fb = fb - fb.mean(0, keepdim=True)
|
| 49 |
+
c = sess("campplus.onnx", False)
|
| 50 |
+
emb = c.run(None, {c.get_inputs()[0].name: fb[None].numpy()})[0].reshape(-1).astype(np.float32)
|
| 51 |
+
return np.asarray(tok).reshape(-1).astype(np.int32), emb
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def tok(ds):
|
| 55 |
+
od = f"{D2}/{ds}"
|
| 56 |
+
meta = [json.loads(l) for l in open(f"{od}/meta.jsonl", encoding="utf-8")]
|
| 57 |
+
jobs = [(f"{od}/pcm24k.i16", m["off24"], m["n24"]) for m in meta]
|
| 58 |
+
toff, embs = 0, []
|
| 59 |
+
with open(f"{od}/s3tok.i32", "wb") as tf, open(f"{od}/s3tok.jsonl", "w") as jf, Pool(12) as pool:
|
| 60 |
+
for i, (m, (t, e)) in enumerate(zip(meta, pool.imap(work, jobs, chunksize=4))):
|
| 61 |
+
tf.write(t.tobytes()); embs.append(e)
|
| 62 |
+
jf.write(json.dumps({"id": m["id"], "toff": toff, "ntok": int(len(t))}) + "\n")
|
| 63 |
+
toff += len(t)
|
| 64 |
+
if i % 5000 == 0:
|
| 65 |
+
print(ds, i, len(meta), "tok/s", round(len(t) / max(m["dur"], 1e-3), 1), flush=True)
|
| 66 |
+
np.save(f"{od}/campplus.npy", np.stack(embs))
|
| 67 |
+
print(ds, "S3_DONE", toff, flush=True)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def build_cs(n_pairs=4000, seed=0):
|
| 71 |
+
"""Join two train utterances in different languages (prefer same gender) into one code-switched item.
|
| 72 |
+
Junction silence follows the first clip's final punctuation (sentence 0.45 s, comma 0.25 s, else 0.20 s)."""
|
| 73 |
+
rng = random.Random(seed)
|
| 74 |
+
pools = {}
|
| 75 |
+
for ds in ["waxal", "slr70", "slr86", "fleurs"]:
|
| 76 |
+
p = f"{D2}/{ds}/meta.jsonl"
|
| 77 |
+
if not os.path.exists(p):
|
| 78 |
+
continue
|
| 79 |
+
for l in open(p, encoding="utf-8"):
|
| 80 |
+
m = json.loads(l)
|
| 81 |
+
if m["split"] != "train" or not (1.0 <= m["dur"] <= 6.5):
|
| 82 |
+
continue
|
| 83 |
+
if ds == "fleurs" and m["lang"] != "eng":
|
| 84 |
+
continue
|
| 85 |
+
pools.setdefault(m["lang"], []).append(m)
|
| 86 |
+
langs = sorted(pools)
|
| 87 |
+
print("cs pools", {l: len(v) for l, v in pools.items()})
|
| 88 |
+
od = f"{D2}/cs_pairs"; os.makedirs(od, exist_ok=True)
|
| 89 |
+
mm = {}
|
| 90 |
+
off = 0
|
| 91 |
+
with open(f"{od}/pcm24k.i16", "wb") as pf, open(f"{od}/meta.jsonl", "w", encoding="utf-8") as jf:
|
| 92 |
+
for k in range(n_pairs):
|
| 93 |
+
l1, l2 = rng.sample(langs, 2)
|
| 94 |
+
if rng.random() < 0.5 and "eng" in langs and "eng" not in (l1, l2):
|
| 95 |
+
l2 = "eng" # English is the most common partner in real Nigerian code-switching
|
| 96 |
+
a = rng.choice(pools[l1])
|
| 97 |
+
g = (a.get("gender") or "").lower()[:1]
|
| 98 |
+
cand = [b for b in rng.sample(pools[l2], min(50, len(pools[l2]))) if (b.get("gender") or "").lower()[:1] == g]
|
| 99 |
+
b = cand[0] if cand else rng.choice(pools[l2])
|
| 100 |
+
ys = []
|
| 101 |
+
for m in (a, b):
|
| 102 |
+
if m["ds"] not in mm:
|
| 103 |
+
mm[m["ds"]] = np.memmap(f"{D2}/{m['ds']}/pcm24k.i16", dtype=np.int16, mode="r")
|
| 104 |
+
ys.append(np.asarray(mm[m["ds"]][m["off24"]:m["off24"] + m["n24"]]))
|
| 105 |
+
last = a["text"].rstrip()[-1:]
|
| 106 |
+
junc = 0.45 if last in ".?!" else (0.25 if last in ",;:" else 0.20)
|
| 107 |
+
have = 0.18 + 0.12
|
| 108 |
+
y1 = ys[0]
|
| 109 |
+
if junc < have:
|
| 110 |
+
y1 = y1[: len(y1) - int((have - junc) * 24000)]
|
| 111 |
+
gap = np.zeros(0, np.int16)
|
| 112 |
+
else:
|
| 113 |
+
gap = np.zeros(int((junc - have) * 24000), np.int16)
|
| 114 |
+
y = np.concatenate([y1, gap, ys[1]])
|
| 115 |
+
text = f"<{l1}>{a['text']}</{l1}> <{l2}>{b['text']}</{l2}>"
|
| 116 |
+
m = dict(id=f"cs_{k:05d}", ds="cs_pairs", lang="cs", langs=[l1, l2], spk=a["spk"], gender=a.get("gender", ""),
|
| 117 |
+
split="train", text=text, dur=round(len(y) / 24000, 3), off24=off, n24=int(len(y)),
|
| 118 |
+
src=[a["id"], b["id"]], flow_ok=False)
|
| 119 |
+
pf.write(y.tobytes()); off += len(y)
|
| 120 |
+
jf.write(json.dumps(m, ensure_ascii=False) + "\n")
|
| 121 |
+
print("cs pairs", n_pairs, "hours", round(off / 24000 / 3600, 2), flush=True)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
if __name__ == "__main__":
|
| 125 |
+
if sys.argv[1] == "tok":
|
| 126 |
+
for ds in sys.argv[2:]:
|
| 127 |
+
tok(ds)
|
| 128 |
+
elif sys.argv[1] == "cs":
|
| 129 |
+
build_cs(int(sys.argv[2]))
|
| 130 |
+
tok("cs_pairs")
|
code/da/text.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Text front-end: Unicode NFC, tone diacritics preserved, UTF-8 byte tokens with per-token language ids.
|
| 2 |
+
|
| 3 |
+
Code-switched input uses inline tags: "<yor>Mo fẹ́ lọ sí ọjà</yor> <eng>to buy some bread</eng>"
|
| 4 |
+
Untagged text takes the default language. Each byte inherits the language of the span it is in.
|
| 5 |
+
"""
|
| 6 |
+
import re
|
| 7 |
+
import unicodedata
|
| 8 |
+
|
| 9 |
+
LANGS = ["eng", "ibo", "hau", "yor", "pcm"]
|
| 10 |
+
LANG2ID = {l: i for i, l in enumerate(LANGS)}
|
| 11 |
+
PAD, BOS, EOS = 0, 1, 2 # byte b -> token b + 3
|
| 12 |
+
VOCAB = 256 + 3
|
| 13 |
+
_TAG = re.compile(r"<(eng|ibo|hau|yor|pcm)>(.*?)</\1>", re.S)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def normalize(t: str) -> str:
|
| 17 |
+
t = unicodedata.normalize("NFC", t)
|
| 18 |
+
t = t.replace("\u2019", "'").replace("\u2018", "'").replace("\u201c", '"').replace("\u201d", '"')
|
| 19 |
+
return re.sub(r"\s+", " ", t).strip()
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def segments(text: str, default_lang: str = "eng"):
|
| 23 |
+
"""-> list of (lang, normalized_text) spans, order preserved."""
|
| 24 |
+
out, pos = [], 0
|
| 25 |
+
for m in _TAG.finditer(text):
|
| 26 |
+
pre = text[pos:m.start()]
|
| 27 |
+
if pre.strip():
|
| 28 |
+
out.append((default_lang, pre))
|
| 29 |
+
out.append((m.group(1), m.group(2)))
|
| 30 |
+
pos = m.end()
|
| 31 |
+
if text[pos:].strip():
|
| 32 |
+
out.append((default_lang, text[pos:]))
|
| 33 |
+
# keep a single space between spans
|
| 34 |
+
res = []
|
| 35 |
+
for i, (l, s) in enumerate(out):
|
| 36 |
+
s = normalize(s)
|
| 37 |
+
if not s:
|
| 38 |
+
continue
|
| 39 |
+
if res:
|
| 40 |
+
s = " " + s
|
| 41 |
+
res.append((l, s))
|
| 42 |
+
return res
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def encode(text: str, default_lang: str = "eng"):
|
| 46 |
+
"""-> (token ids, language ids) incl. BOS/EOS. BOS takes the first span's language, EOS the last."""
|
| 47 |
+
segs = segments(text, default_lang)
|
| 48 |
+
toks, langs = [BOS], [LANG2ID[segs[0][0]] if segs else LANG2ID[default_lang]]
|
| 49 |
+
for l, s in segs:
|
| 50 |
+
b = s.encode("utf-8")
|
| 51 |
+
toks += [x + 3 for x in b]
|
| 52 |
+
langs += [LANG2ID[l]] * len(b)
|
| 53 |
+
toks.append(EOS)
|
| 54 |
+
langs.append(langs[-1])
|
| 55 |
+
return toks, langs
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def plain(text: str) -> str:
|
| 59 |
+
"""Tagged text -> plain text (for ASR references)."""
|
| 60 |
+
return " ".join(s.strip() for _, s in segments(text))
|
code/train_b.py
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Arm B: CosyVoice2-0.5B (Apache-2.0) fine-tune with the Zhang et al. 2019 recipe added to its text->speech-token LM.
|
| 2 |
+
|
| 3 |
+
Additions (new, ours):
|
| 4 |
+
* per-token language embedding added to the Qwen text-token embeddings (zero-init)
|
| 5 |
+
* learned speaker embedding inserted as one token AFTER the text, before the task token (zero-init), so text
|
| 6 |
+
positions cannot see it; speech positions attend to it (anchor-voice conditioning)
|
| 7 |
+
* domain-adversarial speaker classifier with gradient reversal on the LM hidden states at text positions
|
| 8 |
+
Flow-matching decoder + HiFT vocoder stay frozen; at inference they are prompted with an anchor-voice clip.
|
| 9 |
+
|
| 10 |
+
python train_b.py train --out ~/runs/b1 --minutes 30
|
| 11 |
+
python train_b.py synth --ckpt ~/runs/b1/last.pt --out ~/eval/sys/armB_ft
|
| 12 |
+
python train_b.py zeroshot --out ~/eval/sys/cosyvoice2_zeroshot (pretrained model, cross-lingual, no fine-tune)
|
| 13 |
+
"""
|
| 14 |
+
import argparse, inspect, json, math, os, random, sys, time
|
| 15 |
+
import numpy as np, torch, torch.nn as nn, torch.nn.functional as F, soundfile as sf, librosa
|
| 16 |
+
|
| 17 |
+
HOME = os.path.expanduser("~")
|
| 18 |
+
sys.path[:0] = [f"{HOME}/CosyVoice", f"{HOME}/CosyVoice/third_party/Matcha-TTS"]
|
| 19 |
+
MODEL_DIR = f"{HOME}/models/CosyVoice2-0.5B"
|
| 20 |
+
LANGS = ["eng", "ibo", "hau", "yor", "pcm"]
|
| 21 |
+
L2I = {l: i for i, l in enumerate(LANGS)}
|
| 22 |
+
sys.path.insert(0, f"{HOME}/da")
|
| 23 |
+
from da.text import segments # noqa: E402
|
| 24 |
+
|
| 25 |
+
from da.model import grad_reverse # noqa: E402
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _sf_load_wav(wav, target_sr, min_sr=16000):
|
| 29 |
+
"""soundfile replacement for cosyvoice load_wav (torchaudio>=2.9 load needs torchcodec)."""
|
| 30 |
+
y, sr = sf.read(wav, dtype="float32", always_2d=True)
|
| 31 |
+
y = y.mean(1)
|
| 32 |
+
if sr != target_sr:
|
| 33 |
+
y = librosa.resample(y, orig_sr=sr, target_sr=target_sr)
|
| 34 |
+
return torch.from_numpy(y)[None]
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def load_cosy():
|
| 38 |
+
import cosyvoice.utils.file_utils as fu
|
| 39 |
+
fu.load_wav = _sf_load_wav
|
| 40 |
+
import cosyvoice.cli.frontend as cf
|
| 41 |
+
cf.load_wav = _sf_load_wav
|
| 42 |
+
from cosyvoice.cli.cosyvoice import CosyVoice2
|
| 43 |
+
kw = dict(load_jit=False, load_trt=False, fp16=False)
|
| 44 |
+
sig = inspect.signature(CosyVoice2.__init__).parameters
|
| 45 |
+
kw = {k: v for k, v in kw.items() if k in sig}
|
| 46 |
+
return CosyVoice2(MODEL_DIR, **kw)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def text_tokens(tok, text, default="eng"):
|
| 50 |
+
"""tagged text -> (qwen ids, per-token lang ids)."""
|
| 51 |
+
ids, langs = [], []
|
| 52 |
+
for l, s in segments(text, default):
|
| 53 |
+
t = tok(s, add_special_tokens=False)["input_ids"]
|
| 54 |
+
ids += t; langs += [L2I[l]] * len(t)
|
| 55 |
+
return ids, langs
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class DALM(nn.Module):
|
| 59 |
+
def __init__(self, qlm, n_spk, d=896):
|
| 60 |
+
super().__init__()
|
| 61 |
+
self.q = qlm # cosyvoice Qwen2LM
|
| 62 |
+
self.lang_emb = nn.Embedding(len(LANGS), d); nn.init.zeros_(self.lang_emb.weight)
|
| 63 |
+
self.spk_emb = nn.Embedding(n_spk, d); nn.init.normal_(self.spk_emb.weight, std=0.02)
|
| 64 |
+
self.adv = nn.Sequential(nn.Linear(d, 256), nn.ReLU(), nn.Linear(256, n_spk))
|
| 65 |
+
self.V = qlm.speech_token_size
|
| 66 |
+
|
| 67 |
+
def embed_text(self, ids, langs):
|
| 68 |
+
return self.q.llm.model.model.embed_tokens(ids) + self.lang_emb(langs)
|
| 69 |
+
|
| 70 |
+
def build(self, ids, langs, spk, speech=None):
|
| 71 |
+
"""one sequence: [sos, text+lang, spk, task, speech...]"""
|
| 72 |
+
sos = self.q.llm_embedding.weight[self.q.sos][None]
|
| 73 |
+
task = self.q.llm_embedding.weight[self.q.task_id][None]
|
| 74 |
+
parts = [sos, self.embed_text(ids, langs), self.spk_emb(spk)[None], task]
|
| 75 |
+
if speech is not None:
|
| 76 |
+
parts.append(self.q.speech_embedding(speech))
|
| 77 |
+
return torch.cat(parts, 0)
|
| 78 |
+
|
| 79 |
+
def hidden(self, x, mask):
|
| 80 |
+
out = self.q.llm.model.model(inputs_embeds=x, attention_mask=mask, return_dict=True)
|
| 81 |
+
return out.last_hidden_state
|
| 82 |
+
|
| 83 |
+
def forward(self, batch, adv_lambda=1.0):
|
| 84 |
+
seqs, tgts, tpos = [], [], []
|
| 85 |
+
for ids, langs, spk, sp in batch:
|
| 86 |
+
x = self.build(ids, langs, spk, sp)
|
| 87 |
+
n_text = len(ids)
|
| 88 |
+
tgt = torch.full((x.shape[0],), -100, device=x.device, dtype=torch.long)
|
| 89 |
+
# position of task token = 1 + n_text + 1; it predicts speech[0]; last speech pos predicts EOS
|
| 90 |
+
start = 1 + n_text + 1
|
| 91 |
+
tgt[start: start + len(sp)] = sp
|
| 92 |
+
tgt[start + len(sp)] = self.V # EOS
|
| 93 |
+
seqs.append(x); tgts.append(tgt); tpos.append((1, 1 + n_text))
|
| 94 |
+
T = max(s.shape[0] for s in seqs) + 1
|
| 95 |
+
D = seqs[0].shape[1]
|
| 96 |
+
X = seqs[0].new_zeros(len(seqs), T, D)
|
| 97 |
+
Y = torch.full((len(seqs), T), -100, device=X.device, dtype=torch.long)
|
| 98 |
+
M = torch.zeros(len(seqs), T, device=X.device, dtype=torch.long)
|
| 99 |
+
for i, (s, t) in enumerate(zip(seqs, tgts)):
|
| 100 |
+
X[i, :len(s)] = s; Y[i, :len(t)] = t[:len(s)] if len(t) > len(s) else t; M[i, :len(s)] = 1
|
| 101 |
+
# targets were laid out on positions; shift is implicit (position p predicts tgt[p])
|
| 102 |
+
H = self.hidden(X, M)
|
| 103 |
+
logits = self.q.llm_decoder(H)
|
| 104 |
+
ce = F.cross_entropy(logits.float().reshape(-1, logits.shape[-1]), Y.reshape(-1), ignore_index=-100)
|
| 105 |
+
valid = Y != -100
|
| 106 |
+
acc = (logits.argmax(-1)[valid] == Y[valid]).float().mean()
|
| 107 |
+
# adversarial speaker classifier on text positions
|
| 108 |
+
hs, ys = [], []
|
| 109 |
+
for i, (a, b) in enumerate(tpos):
|
| 110 |
+
hs.append(H[i, a:b]); ys.append(torch.full((b - a,), int(batch[i][2]), device=X.device))
|
| 111 |
+
hs = torch.cat(hs); ys = torch.cat(ys)
|
| 112 |
+
al = self.adv(grad_reverse(hs.float(), adv_lambda))
|
| 113 |
+
adv = F.cross_entropy(al, ys)
|
| 114 |
+
adv_acc = (al.argmax(-1) == ys).float().mean()
|
| 115 |
+
return ce, acc, adv, adv_acc
|
| 116 |
+
|
| 117 |
+
@torch.no_grad()
|
| 118 |
+
def generate(self, ids, langs, spk, top_p=0.8, top_k=25, win=10, tau_r=0.1, seed=0):
|
| 119 |
+
g = torch.Generator(device=ids.device).manual_seed(seed)
|
| 120 |
+
x = self.build(ids, langs, spk)[None]
|
| 121 |
+
n = len(ids)
|
| 122 |
+
min_len, max_len = int(n * 2), int(n * 20)
|
| 123 |
+
past, out = None, []
|
| 124 |
+
inp = x
|
| 125 |
+
for i in range(max_len):
|
| 126 |
+
o = self.q.llm.model.model(inputs_embeds=inp, past_key_values=past, use_cache=True, return_dict=True)
|
| 127 |
+
past = o.past_key_values
|
| 128 |
+
logp = self.q.llm_decoder(o.last_hidden_state[:, -1]).float().log_softmax(-1)[0]
|
| 129 |
+
if i < min_len:
|
| 130 |
+
logp[self.V] = -float("inf")
|
| 131 |
+
logp[self.V + 1:] = -float("inf")
|
| 132 |
+
p = logp.exp()
|
| 133 |
+
sp, si = p.sort(descending=True)
|
| 134 |
+
keep = (sp.cumsum(0) - sp) < top_p
|
| 135 |
+
keep[top_k:] = False
|
| 136 |
+
cand_p, cand_i = sp[keep], si[keep]
|
| 137 |
+
tokid = int(cand_i[torch.multinomial(cand_p / cand_p.sum(), 1, generator=g)])
|
| 138 |
+
if out and sum(1 for t in out[-win:] if t == tokid) >= win * tau_r and tokid != self.V: # RAS
|
| 139 |
+
tokid = int(torch.multinomial(p, 1, generator=g))
|
| 140 |
+
if tokid >= self.V:
|
| 141 |
+
break
|
| 142 |
+
out.append(tokid)
|
| 143 |
+
inp = self.q.speech_embedding.weight[tokid][None, None]
|
| 144 |
+
return out
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
# ------------------------------------------------------------------ data
|
| 148 |
+
def load_items(root, datasets, split, max_tok=500):
|
| 149 |
+
items, mm = [], {}
|
| 150 |
+
for ds in datasets:
|
| 151 |
+
if not os.path.exists(f"{root}/{ds}/s3tok.jsonl"):
|
| 152 |
+
continue
|
| 153 |
+
mm[ds] = np.memmap(f"{root}/{ds}/s3tok.i32", dtype=np.int32, mode="r")
|
| 154 |
+
tk = {}
|
| 155 |
+
for l in open(f"{root}/{ds}/s3tok.jsonl"):
|
| 156 |
+
r = json.loads(l); tk[r["id"]] = (r["toff"], r["ntok"])
|
| 157 |
+
for l in open(f"{root}/{ds}/meta.jsonl", encoding="utf-8"):
|
| 158 |
+
m = json.loads(l)
|
| 159 |
+
if m["split"] != split or m["id"] not in tk:
|
| 160 |
+
continue
|
| 161 |
+
m["toff"], m["ntok"] = tk[m["id"]]
|
| 162 |
+
if 5 < m["ntok"] <= max_tok:
|
| 163 |
+
items.append(m)
|
| 164 |
+
return items, mm
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def batches(items, budget, weights, rng):
|
| 168 |
+
idx = []
|
| 169 |
+
for i, m in enumerate(items):
|
| 170 |
+
w = weights.get(m["ds"], 1.0)
|
| 171 |
+
idx += [i] * int(w) + ([i] if rng.random() < w - int(w) else [])
|
| 172 |
+
idx.sort(key=lambda i: items[i]["ntok"] + rng.random() * 10)
|
| 173 |
+
out, cur, mx = [], [], 0
|
| 174 |
+
for i in idx:
|
| 175 |
+
L = items[i]["ntok"] + len(items[i]["text"]) // 2 + 4
|
| 176 |
+
if cur and max(mx, L) * (len(cur) + 1) > budget:
|
| 177 |
+
out.append(cur); cur, mx = [], 0
|
| 178 |
+
cur.append(i); mx = max(mx, L)
|
| 179 |
+
if cur:
|
| 180 |
+
out.append(cur)
|
| 181 |
+
rng.shuffle(out)
|
| 182 |
+
return out
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def train(a):
|
| 186 |
+
from transformers import AutoTokenizer
|
| 187 |
+
tok = AutoTokenizer.from_pretrained(f"{MODEL_DIR}/CosyVoice-BlankEN")
|
| 188 |
+
cosy = load_cosy()
|
| 189 |
+
items, mm = load_items(a.data, a.datasets.split(","), "train")
|
| 190 |
+
val, _ = load_items(a.data, a.datasets.split(","), "val")
|
| 191 |
+
spks = sorted({m["spk"] for m in items}); spk2id = {s: i for i, s in enumerate(spks)}
|
| 192 |
+
val = [m for m in val if m["spk"] in spk2id][:200]
|
| 193 |
+
os.makedirs(a.out, exist_ok=True)
|
| 194 |
+
json.dump(spk2id, open(f"{a.out}/spk2id.json", "w"))
|
| 195 |
+
print("armB items", len(items), "hours", round(sum(m["dur"] for m in items) / 3600, 1), "spk", len(spks), flush=True)
|
| 196 |
+
model = DALM(cosy.model.llm, len(spks)).cuda().train()
|
| 197 |
+
for p in cosy.model.flow.parameters():
|
| 198 |
+
p.requires_grad = False
|
| 199 |
+
for p in cosy.model.hift.parameters():
|
| 200 |
+
p.requires_grad = False
|
| 201 |
+
new = list(model.lang_emb.parameters()) + list(model.spk_emb.parameters()) + list(model.adv.parameters())
|
| 202 |
+
newids = {id(p) for p in new}
|
| 203 |
+
old = [p for p in model.parameters() if id(p) not in newids and p.requires_grad]
|
| 204 |
+
opt = torch.optim.AdamW([{"params": old, "lr": a.lr}, {"params": new, "lr": a.lr_new}], weight_decay=0.01)
|
| 205 |
+
base = [a.lr, a.lr_new]
|
| 206 |
+
cache = {}
|
| 207 |
+
|
| 208 |
+
def enc(m):
|
| 209 |
+
if m["id"] not in cache:
|
| 210 |
+
cache[m["id"]] = text_tokens(tok, f"<{m['lang']}>{m['text']}</{m['lang']}>")
|
| 211 |
+
ids, langs = cache[m["id"]]
|
| 212 |
+
sp = np.asarray(mm[m["ds"]][m["toff"]: m["toff"] + m["ntok"]], dtype=np.int64)
|
| 213 |
+
return (torch.tensor(ids).cuda(), torch.tensor(langs).cuda(), torch.tensor(spk2id[m["spk"]]).cuda(),
|
| 214 |
+
torch.from_numpy(sp).cuda())
|
| 215 |
+
|
| 216 |
+
rng = random.Random(0); step = 0; nskip = 0; t0 = time.time(); logf = open(f"{a.out}/log.jsonl", "a")
|
| 217 |
+
|
| 218 |
+
def validate():
|
| 219 |
+
model.eval(); L = []
|
| 220 |
+
with torch.no_grad(), torch.autocast("cuda", dtype=torch.bfloat16):
|
| 221 |
+
for i in range(0, len(val), 16):
|
| 222 |
+
ce, acc, _, _ = model([enc(m) for m in val[i:i + 16]]); L.append(float(ce))
|
| 223 |
+
model.train(); return float(np.mean(L))
|
| 224 |
+
|
| 225 |
+
v0 = validate(); print(json.dumps({"step": 0, "val_ce": v0}), flush=True); logf.write(json.dumps({"step": 0, "val_ce": v0}) + "\n")
|
| 226 |
+
done = False
|
| 227 |
+
while not done:
|
| 228 |
+
for bi in batches(items, a.tokens, {"waxal": a.waxal_w}, rng):
|
| 229 |
+
lrm = min(1.0, (step + 1) / 200)
|
| 230 |
+
for g, b in zip(opt.param_groups, base):
|
| 231 |
+
g["lr"] = b * lrm
|
| 232 |
+
with torch.autocast("cuda", dtype=torch.bfloat16):
|
| 233 |
+
ce, acc, adv, adv_acc = model([enc(items[i]) for i in bi])
|
| 234 |
+
loss = ce + a.adv_w * adv
|
| 235 |
+
opt.zero_grad(set_to_none=True)
|
| 236 |
+
if not torch.isfinite(loss):
|
| 237 |
+
nskip += 1
|
| 238 |
+
print(json.dumps({"skip_nonfinite_fwd": step, "ce": float(ce), "adv": float(adv), "B": len(bi),
|
| 239 |
+
"maxtok": max(items[i]["ntok"] for i in bi)}), flush=True)
|
| 240 |
+
continue
|
| 241 |
+
loss.backward()
|
| 242 |
+
gn = torch.nn.utils.clip_grad_norm_([p for p in model.parameters() if p.requires_grad], 1.0)
|
| 243 |
+
if not torch.isfinite(gn):
|
| 244 |
+
nskip += 1; opt.zero_grad(set_to_none=True)
|
| 245 |
+
print(json.dumps({"skip_nonfinite_grad": step}), flush=True); continue
|
| 246 |
+
opt.step(); step += 1
|
| 247 |
+
if step % 25 == 0:
|
| 248 |
+
r = dict(step=step, ce=round(float(ce), 4), acc=round(float(acc), 4), adv=round(float(adv), 3),
|
| 249 |
+
adv_acc=round(float(adv_acc), 3), gn=round(float(gn), 2), B=len(bi), min=round((time.time() - t0) / 60, 2))
|
| 250 |
+
print(json.dumps(r), flush=True); logf.write(json.dumps(r) + "\n"); logf.flush()
|
| 251 |
+
if step % 500 == 0:
|
| 252 |
+
v = validate(); print(json.dumps({"step": step, "val_ce": v}), flush=True)
|
| 253 |
+
logf.write(json.dumps({"step": step, "val_ce": v}) + "\n"); logf.flush()
|
| 254 |
+
if (time.time() - t0) / 60 > a.minutes:
|
| 255 |
+
done = True; break
|
| 256 |
+
v = validate(); logf.write(json.dumps({"step": step, "val_ce": v, "final": True}) + "\n")
|
| 257 |
+
sd = {k: v for k, v in model.state_dict().items()}
|
| 258 |
+
torch.save(dict(model=sd, step=step, n_spk=len(spks)), f"{a.out}/last.pt")
|
| 259 |
+
print("TRAIN_B_DONE", step, v, flush=True)
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
# ------------------------------------------------------------------ inference
|
| 263 |
+
def prompt_feats(cosy, wav_path):
|
| 264 |
+
"""flow prompt (speech tokens, 24k mel feat, campplus embedding) from an anchor clip."""
|
| 265 |
+
fe = cosy.frontend
|
| 266 |
+
tokn, tokl = fe._extract_speech_token(wav_path)
|
| 267 |
+
feat, featl = fe._extract_speech_feat(wav_path)
|
| 268 |
+
# CosyVoice2 aligns: feat frames = 2 * tokens
|
| 269 |
+
n = min(feat.shape[1] // 2, tokn.shape[1])
|
| 270 |
+
feat, tokn = feat[:, :2 * n], tokn[:, :n]
|
| 271 |
+
emb = fe._extract_spk_embedding(wav_path)
|
| 272 |
+
return tokn.cuda(), feat.cuda(), emb.cuda()
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
@torch.no_grad()
|
| 276 |
+
def token2wav(cosy, tokens, ptok, pfeat, emb):
|
| 277 |
+
flow = cosy.model.flow
|
| 278 |
+
t = torch.tensor([tokens], dtype=torch.int32).cuda()
|
| 279 |
+
kw = dict(token=t, token_len=torch.tensor([t.shape[1]], dtype=torch.int32).cuda(),
|
| 280 |
+
prompt_token=ptok, prompt_token_len=torch.tensor([ptok.shape[1]], dtype=torch.int32).cuda(),
|
| 281 |
+
prompt_feat=pfeat, prompt_feat_len=torch.tensor([pfeat.shape[1]], dtype=torch.int32).cuda(),
|
| 282 |
+
embedding=emb)
|
| 283 |
+
sig = inspect.signature(flow.inference).parameters
|
| 284 |
+
if "streaming" in sig:
|
| 285 |
+
kw["streaming"] = False
|
| 286 |
+
if "finalize" in sig:
|
| 287 |
+
kw["finalize"] = True
|
| 288 |
+
if "flow_cache" in sig:
|
| 289 |
+
kw["flow_cache"] = torch.zeros(1, 80, 0, 2).cuda()
|
| 290 |
+
mel = flow.inference(**kw)
|
| 291 |
+
mel = mel[0] if isinstance(mel, tuple) else mel
|
| 292 |
+
hs = inspect.signature(cosy.model.hift.inference).parameters
|
| 293 |
+
w = cosy.model.hift.inference(speech_feat=mel, **({"cache_source": torch.zeros(1, 1, 0).cuda()} if "cache_source" in hs else {}))
|
| 294 |
+
w = w[0] if isinstance(w, tuple) else w
|
| 295 |
+
return w.squeeze().float().cpu().numpy()
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
def eval_rows():
|
| 299 |
+
return [json.loads(l) for l in open(f"{HOME}/eval/eval_set.jsonl", encoding="utf-8")]
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
def synth(a):
|
| 303 |
+
from transformers import AutoTokenizer
|
| 304 |
+
tok = AutoTokenizer.from_pretrained(f"{MODEL_DIR}/CosyVoice-BlankEN")
|
| 305 |
+
cosy = load_cosy()
|
| 306 |
+
ck = torch.load(a.ckpt, map_location="cuda")
|
| 307 |
+
spk2id = json.load(open(os.path.join(os.path.dirname(a.ckpt), "spk2id.json")))
|
| 308 |
+
model = DALM(cosy.model.llm, ck["n_spk"]).cuda().eval()
|
| 309 |
+
model.load_state_dict(ck["model"])
|
| 310 |
+
anc = json.load(open(f"{HOME}/eval/anchor.json"))
|
| 311 |
+
ptok, pfeat, emb = prompt_feats(cosy, anc["ref_wavs"][0])
|
| 312 |
+
os.makedirs(a.out, exist_ok=True)
|
| 313 |
+
for r in eval_rows():
|
| 314 |
+
text = r["text"] if r["lang"] == "cs" else f"<{r['lang']}>{r['text']}</{r['lang']}>"
|
| 315 |
+
ids, langs = text_tokens(tok, text)
|
| 316 |
+
with torch.autocast("cuda", dtype=torch.bfloat16):
|
| 317 |
+
sp = model.generate(torch.tensor(ids).cuda(), torch.tensor(langs).cuda(), torch.tensor(spk2id[anc["spk"]]).cuda())
|
| 318 |
+
sf.write(f"{a.out}/{r['id']}.wav", token2wav(cosy, sp, ptok, pfeat, emb), cosy.sample_rate)
|
| 319 |
+
print("synth_b done", a.out, flush=True)
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
def zeroshot(a):
|
| 323 |
+
"""Unmodified pretrained CosyVoice2, cross-lingual mode with the anchor prompt (reference point)."""
|
| 324 |
+
cosy = load_cosy()
|
| 325 |
+
anc = json.load(open(f"{HOME}/eval/anchor.json"))
|
| 326 |
+
from cosyvoice.utils.file_utils import load_wav
|
| 327 |
+
pw = anc["ref_wavs"][0]
|
| 328 |
+
sig = inspect.signature(cosy.inference_cross_lingual).parameters
|
| 329 |
+
os.makedirs(a.out, exist_ok=True)
|
| 330 |
+
for r in eval_rows():
|
| 331 |
+
text = " ".join(s.strip() for _, s in segments(r["text"])) if r["lang"] == "cs" else r["text"]
|
| 332 |
+
prompt = pw
|
| 333 |
+
outs = [o["tts_speech"] for o in cosy.inference_cross_lingual(text, prompt, stream=False)]
|
| 334 |
+
w = torch.cat(outs, 1).squeeze().cpu().numpy()
|
| 335 |
+
sf.write(f"{a.out}/{r['id']}.wav", w, cosy.sample_rate)
|
| 336 |
+
print("zeroshot done", flush=True)
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
if __name__ == "__main__":
|
| 340 |
+
ap = argparse.ArgumentParser()
|
| 341 |
+
ap.add_argument("cmd")
|
| 342 |
+
ap.add_argument("--data", default=f"{HOME}/data")
|
| 343 |
+
ap.add_argument("--datasets", default="waxal,fleurs,slr86,slr70,bible_yor")
|
| 344 |
+
ap.add_argument("--out", default=f"{HOME}/runs/b1")
|
| 345 |
+
ap.add_argument("--ckpt", default="")
|
| 346 |
+
ap.add_argument("--minutes", type=float, default=30)
|
| 347 |
+
ap.add_argument("--lr", type=float, default=1e-5)
|
| 348 |
+
ap.add_argument("--lr_new", type=float, default=1e-3)
|
| 349 |
+
ap.add_argument("--tokens", type=int, default=12000)
|
| 350 |
+
ap.add_argument("--waxal_w", type=float, default=2.0)
|
| 351 |
+
ap.add_argument("--adv_w", type=float, default=0.02)
|
| 352 |
+
a = ap.parse_args()
|
| 353 |
+
{"train": train, "synth": synth, "zeroshot": zeroshot}[a.cmd](a)
|
code/train_b2.py
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Arm B, Phase 2. CosyVoice2-0.5B (Apache-2.0) + language embeddings + speaker token + GRL speaker classifier.
|
| 2 |
+
|
| 3 |
+
New in Phase 2
|
| 4 |
+
* data from ~/data2 (24 kHz, forced-aligned, pause-normalised; long WAXAL clips segmented; BibleTTS Hausa; CS pairs)
|
| 5 |
+
* in-context prompts during LM training (p=0.5): [sos, prompt_text+text, spk, task, prompt_speech, speech] with
|
| 6 |
+
loss on the target speech only -> at inference the anchor clip steers prosody and pause style
|
| 7 |
+
* keep-best on validation CE (every --val_every steps), cosine LR, early stop
|
| 8 |
+
* flow-matching decoder fine-tune on 24 kHz studio sources (tones / pitch / timing detail)
|
| 9 |
+
* synthesis: anchor prompt at LM and flow, sentence-level generation for multi-sentence text with anchor-matched
|
| 10 |
+
inter-sentence pauses, N candidates per text (seed + prompt clip vary)
|
| 11 |
+
|
| 12 |
+
python train_b2.py lm --out ~/runs/p2_lm --minutes 90
|
| 13 |
+
python train_b2.py flow --out ~/runs/p2_flow --minutes 60
|
| 14 |
+
python train_b2.py synth --ckpt ~/runs/p2_lm/best.pt [--flow ~/runs/p2_flow/best.pt] --out DIR --n 4
|
| 15 |
+
"""
|
| 16 |
+
import argparse, inspect, json, math, os, random, re, sys, time
|
| 17 |
+
import numpy as np, torch, torch.nn as nn, torch.nn.functional as F, soundfile as sf, librosa
|
| 18 |
+
|
| 19 |
+
HOME = os.path.expanduser("~")
|
| 20 |
+
sys.path[:0] = [f"{HOME}/CosyVoice", f"{HOME}/CosyVoice/third_party/Matcha-TTS", f"{HOME}/da"]
|
| 21 |
+
from train_b import load_cosy, text_tokens, DALM, LANGS, L2I, MODEL_DIR, token2wav # noqa: E402
|
| 22 |
+
from da.model import grad_reverse # noqa: E402
|
| 23 |
+
from da.text import segments # noqa: E402
|
| 24 |
+
|
| 25 |
+
D2 = os.path.expanduser(os.environ.get("P2OUT", f"{HOME}/data2"))
|
| 26 |
+
ANCHOR = "waxal_yor_4"
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
# ------------------------------------------------------------------ model
|
| 30 |
+
class DALM2(DALM):
|
| 31 |
+
def build2(self, ids, langs, spk, sp=None, pids=None, plangs=None, psp=None):
|
| 32 |
+
"""[sos, (prompt_text) text, spk, task, (prompt_speech) speech] -> embeddings, n_text_total, n_prompt_speech"""
|
| 33 |
+
sos = self.q.llm_embedding.weight[self.q.sos][None]
|
| 34 |
+
task = self.q.llm_embedding.weight[self.q.task_id][None]
|
| 35 |
+
if pids is not None:
|
| 36 |
+
ids = torch.cat([pids, ids]); langs = torch.cat([plangs, langs])
|
| 37 |
+
parts = [sos, self.embed_text(ids, langs), self.spk_emb(spk)[None], task]
|
| 38 |
+
npsp = 0
|
| 39 |
+
if psp is not None:
|
| 40 |
+
parts.append(self.q.speech_embedding(psp)); npsp = len(psp)
|
| 41 |
+
if sp is not None:
|
| 42 |
+
parts.append(self.q.speech_embedding(sp))
|
| 43 |
+
return torch.cat(parts, 0), len(ids), npsp
|
| 44 |
+
|
| 45 |
+
def forward2(self, batch, adv_lambda=1.0):
|
| 46 |
+
seqs, tgts, tpos, spks = [], [], [], []
|
| 47 |
+
for b in batch:
|
| 48 |
+
ids, langs, spk, sp = b[:4]
|
| 49 |
+
pr = b[4] if len(b) > 4 else None
|
| 50 |
+
x, nt, npsp = self.build2(ids, langs, spk, sp, *(pr if pr else (None, None, None)))
|
| 51 |
+
tgt = torch.full((x.shape[0] + 1,), -100, device=x.device, dtype=torch.long)
|
| 52 |
+
start = 1 + nt + 2 + npsp - 1 # position whose output predicts speech[0]
|
| 53 |
+
tgt[start: start + len(sp)] = sp
|
| 54 |
+
tgt[start + len(sp)] = self.V
|
| 55 |
+
seqs.append(x); tgts.append(tgt[:x.shape[0]]); tpos.append((1, 1 + nt)); spks.append(int(spk))
|
| 56 |
+
T = max(s.shape[0] for s in seqs)
|
| 57 |
+
D = seqs[0].shape[1]
|
| 58 |
+
X = seqs[0].new_zeros(len(seqs), T, D)
|
| 59 |
+
Y = torch.full((len(seqs), T), -100, device=X.device, dtype=torch.long)
|
| 60 |
+
M = torch.zeros(len(seqs), T, device=X.device, dtype=torch.long)
|
| 61 |
+
for i, (s, t) in enumerate(zip(seqs, tgts)):
|
| 62 |
+
X[i, :len(s)] = s; Y[i, :len(t)] = t; M[i, :len(s)] = 1
|
| 63 |
+
H = self.hidden(X, M)
|
| 64 |
+
logits = self.q.llm_decoder(H)
|
| 65 |
+
ce = F.cross_entropy(logits.float().reshape(-1, logits.shape[-1]), Y.reshape(-1), ignore_index=-100)
|
| 66 |
+
valid = Y != -100
|
| 67 |
+
acc = (logits.argmax(-1)[valid] == Y[valid]).float().mean()
|
| 68 |
+
hs, ys = [], []
|
| 69 |
+
for i, (a, b) in enumerate(tpos):
|
| 70 |
+
hs.append(H[i, a:b]); ys.append(torch.full((b - a,), spks[i], device=X.device))
|
| 71 |
+
hs = torch.cat(hs); ys = torch.cat(ys)
|
| 72 |
+
al = self.adv(grad_reverse(hs.float(), adv_lambda))
|
| 73 |
+
adv = F.cross_entropy(al, ys)
|
| 74 |
+
return ce, acc, adv, (al.argmax(-1) == ys).float().mean()
|
| 75 |
+
|
| 76 |
+
@torch.no_grad()
|
| 77 |
+
def generate2(self, ids, langs, spk, prompt=None, top_p=0.8, top_k=25, win=10, tau_r=0.1, seed=0,
|
| 78 |
+
min_ratio=2.0, max_ratio=20.0):
|
| 79 |
+
g = torch.Generator(device=ids.device).manual_seed(seed)
|
| 80 |
+
x, _, _ = self.build2(ids, langs, spk, None, *(prompt if prompt else (None, None, None)))
|
| 81 |
+
x = x[None]
|
| 82 |
+
n = len(ids)
|
| 83 |
+
min_len, max_len = int(n * min_ratio), int(n * max_ratio)
|
| 84 |
+
past, out, inp = None, [], x
|
| 85 |
+
for i in range(max_len):
|
| 86 |
+
o = self.q.llm.model.model(inputs_embeds=inp, past_key_values=past, use_cache=True, return_dict=True)
|
| 87 |
+
past = o.past_key_values
|
| 88 |
+
logp = self.q.llm_decoder(o.last_hidden_state[:, -1]).float().log_softmax(-1)[0]
|
| 89 |
+
if i < min_len:
|
| 90 |
+
logp[self.V] = -float("inf")
|
| 91 |
+
logp[self.V + 1:] = -float("inf")
|
| 92 |
+
p = logp.exp()
|
| 93 |
+
sp, si = p.sort(descending=True)
|
| 94 |
+
keep = (sp.cumsum(0) - sp) < top_p
|
| 95 |
+
keep[top_k:] = False
|
| 96 |
+
cp, ci = sp[keep], si[keep]
|
| 97 |
+
tokid = int(ci[torch.multinomial(cp / cp.sum(), 1, generator=g)])
|
| 98 |
+
if out and sum(1 for t in out[-win:] if t == tokid) >= win * tau_r and tokid != self.V:
|
| 99 |
+
tokid = int(torch.multinomial(p, 1, generator=g))
|
| 100 |
+
if tokid >= self.V:
|
| 101 |
+
break
|
| 102 |
+
out.append(tokid)
|
| 103 |
+
inp = self.q.speech_embedding.weight[tokid][None, None]
|
| 104 |
+
return out
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
# ------------------------------------------------------------------ data
|
| 108 |
+
def load_items(datasets, split, max_tok=500, flow=False):
|
| 109 |
+
items, mm = [], {}
|
| 110 |
+
for ds in datasets:
|
| 111 |
+
od = f"{D2}/{ds}"
|
| 112 |
+
if not os.path.exists(f"{od}/s3tok.jsonl"):
|
| 113 |
+
print("missing", ds, flush=True); continue
|
| 114 |
+
mm[ds] = np.memmap(f"{od}/s3tok.i32", dtype=np.int32, mode="r")
|
| 115 |
+
tk = [json.loads(l) for l in open(f"{od}/s3tok.jsonl")]
|
| 116 |
+
for k, l in enumerate(open(f"{od}/meta.jsonl", encoding="utf-8")):
|
| 117 |
+
m = json.loads(l)
|
| 118 |
+
t = tk[k]
|
| 119 |
+
assert t["id"] == m["id"]
|
| 120 |
+
if m["split"] != split:
|
| 121 |
+
continue
|
| 122 |
+
if flow and not m.get("flow_ok"):
|
| 123 |
+
continue
|
| 124 |
+
m["toff"], m["ntok"], m["row"] = t["toff"], t["ntok"], k
|
| 125 |
+
if 5 < m["ntok"] <= max_tok:
|
| 126 |
+
items.append(m)
|
| 127 |
+
return items, mm
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def tagged(m):
|
| 131 |
+
return m["text"] if m["lang"] == "cs" else f"<{m['lang']}>{m['text']}</{m['lang']}>"
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
DS_W = {"waxal": 2.0, "bible_yor": 0.5, "bible_hau": 0.6, "fleurs": 1.0, "slr86": 1.0, "slr70": 1.0, "cs_pairs": 1.0}
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def batches(items, budget, rng, extra_w=None, cost=None):
|
| 138 |
+
idx = []
|
| 139 |
+
for i, m in enumerate(items):
|
| 140 |
+
w = DS_W.get(m["ds"], 1.0) * (extra_w(m) if extra_w else 1.0)
|
| 141 |
+
idx += [i] * int(w) + ([i] if rng.random() < w - int(w) else [])
|
| 142 |
+
cost = cost or (lambda m: m["ntok"] + len(m["text"]) // 2 + 4)
|
| 143 |
+
idx.sort(key=lambda i: cost(items[i]) + rng.random() * 10)
|
| 144 |
+
out, cur, mx = [], [], 0
|
| 145 |
+
for i in idx:
|
| 146 |
+
L = cost(items[i])
|
| 147 |
+
if cur and max(mx, L) * (len(cur) + 1) > budget:
|
| 148 |
+
out.append(cur); cur, mx = [], 0
|
| 149 |
+
cur.append(i); mx = max(mx, L)
|
| 150 |
+
if cur:
|
| 151 |
+
out.append(cur)
|
| 152 |
+
rng.shuffle(out)
|
| 153 |
+
return out
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
# ------------------------------------------------------------------ LM training
|
| 157 |
+
def train_lm(a):
|
| 158 |
+
from transformers import AutoTokenizer
|
| 159 |
+
tok = AutoTokenizer.from_pretrained(f"{MODEL_DIR}/CosyVoice-BlankEN")
|
| 160 |
+
cosy = load_cosy()
|
| 161 |
+
dss = a.datasets.split(",")
|
| 162 |
+
items, mm = load_items(dss, "train")
|
| 163 |
+
val_all, _ = load_items(dss, "val")
|
| 164 |
+
if a.anchor_only_w > 0:
|
| 165 |
+
print("anchor items", sum(m["spk"] == ANCHOR for m in items), flush=True)
|
| 166 |
+
spks = sorted({m["spk"] for m in items})
|
| 167 |
+
if a.init:
|
| 168 |
+
ck = torch.load(a.init, map_location="cpu")
|
| 169 |
+
spk2id = json.load(open(os.path.join(os.path.dirname(a.init), "spk2id.json")))
|
| 170 |
+
for s in spks:
|
| 171 |
+
if s not in spk2id:
|
| 172 |
+
spk2id[s] = len(spk2id)
|
| 173 |
+
else:
|
| 174 |
+
spk2id = {s: i for i, s in enumerate(spks)}
|
| 175 |
+
n_spk = max(len(spk2id), ck["n_spk"]) if a.init else len(spk2id)
|
| 176 |
+
# balanced val: up to 60 per language (+ anchor), never more than 15 per speaker
|
| 177 |
+
rng = random.Random(1); rng.shuffle(val_all)
|
| 178 |
+
val, per_l, per_s = [], {}, {}
|
| 179 |
+
for m in val_all:
|
| 180 |
+
if m["spk"] not in spk2id or m["lang"] == "cs":
|
| 181 |
+
continue
|
| 182 |
+
L = "anchor" if m["spk"] == ANCHOR else m["lang"]
|
| 183 |
+
if per_l.get(L, 0) >= 60 or per_s.get(m["spk"], 0) >= 15:
|
| 184 |
+
continue
|
| 185 |
+
per_l[L] = per_l.get(L, 0) + 1; per_s[m["spk"]] = per_s.get(m["spk"], 0) + 1; val.append(m)
|
| 186 |
+
by_spk = {}
|
| 187 |
+
for m in items:
|
| 188 |
+
if m["ntok"] <= 160 and m["lang"] != "cs":
|
| 189 |
+
by_spk.setdefault(m["spk"], []).append(m)
|
| 190 |
+
os.makedirs(a.out, exist_ok=True)
|
| 191 |
+
json.dump(spk2id, open(f"{a.out}/spk2id.json", "w"))
|
| 192 |
+
print("lm items", len(items), "hours", round(sum(m["dur"] for m in items) / 3600, 1), "spk", len(spk2id),
|
| 193 |
+
"val", len(val), per_l, flush=True)
|
| 194 |
+
model = DALM2(cosy.model.llm, n_spk).cuda().train()
|
| 195 |
+
if a.init:
|
| 196 |
+
sd = ck["model"]
|
| 197 |
+
if sd["spk_emb.weight"].shape[0] < n_spk:
|
| 198 |
+
for k in ["spk_emb.weight"]:
|
| 199 |
+
w = model.state_dict()[k].clone(); w[:sd[k].shape[0]] = sd[k]; sd[k] = w
|
| 200 |
+
for k in ["adv.2.weight", "adv.2.bias"]:
|
| 201 |
+
w = model.state_dict()[k].clone(); w[:sd[k].shape[0]] = sd[k]; sd[k] = w
|
| 202 |
+
print(model.load_state_dict(sd, strict=False), flush=True)
|
| 203 |
+
for p in cosy.model.flow.parameters():
|
| 204 |
+
p.requires_grad = False
|
| 205 |
+
for p in cosy.model.hift.parameters():
|
| 206 |
+
p.requires_grad = False
|
| 207 |
+
new = list(model.lang_emb.parameters()) + list(model.spk_emb.parameters()) + list(model.adv.parameters())
|
| 208 |
+
newids = {id(p) for p in new}
|
| 209 |
+
old = [p for p in model.parameters() if id(p) not in newids and p.requires_grad]
|
| 210 |
+
opt = torch.optim.AdamW([{"params": old, "lr": a.lr}, {"params": new, "lr": a.lr_new}], weight_decay=0.01)
|
| 211 |
+
base = [a.lr, a.lr_new]
|
| 212 |
+
cache = {}
|
| 213 |
+
|
| 214 |
+
def tt(text, key):
|
| 215 |
+
if key not in cache:
|
| 216 |
+
cache[key] = text_tokens(tok, text)
|
| 217 |
+
return cache[key]
|
| 218 |
+
|
| 219 |
+
def enc(m, prng=None, force_prompt=None):
|
| 220 |
+
ids, langs = tt(tagged(m), m["id"])
|
| 221 |
+
sp = np.asarray(mm[m["ds"]][m["toff"]: m["toff"] + m["ntok"]], dtype=np.int64)
|
| 222 |
+
r = [torch.tensor(ids).cuda(), torch.tensor(langs).cuda(), torch.tensor(spk2id[m["spk"]]).cuda(),
|
| 223 |
+
torch.from_numpy(sp).cuda()]
|
| 224 |
+
use = force_prompt if force_prompt is not None else (prng is not None and prng.random() < a.p_prompt)
|
| 225 |
+
if use:
|
| 226 |
+
c = by_spk.get(m["spk"], [])
|
| 227 |
+
c = [x for x in (prng or random).sample(c, min(6, len(c))) if x["id"] != m["id"]] if c else []
|
| 228 |
+
if c and c[0]["ntok"] + m["ntok"] <= 600:
|
| 229 |
+
p = c[0]
|
| 230 |
+
pids, plangs = tt(tagged(p), p["id"])
|
| 231 |
+
psp = np.asarray(mm[p["ds"]][p["toff"]: p["toff"] + p["ntok"]], dtype=np.int64)
|
| 232 |
+
r.append((torch.tensor(pids).cuda(), torch.tensor(plangs).cuda(), torch.from_numpy(psp).cuda()))
|
| 233 |
+
return r
|
| 234 |
+
|
| 235 |
+
def validate():
|
| 236 |
+
model.eval(); out = {}
|
| 237 |
+
with torch.no_grad(), torch.autocast("cuda", dtype=torch.bfloat16):
|
| 238 |
+
for mode in (False, True):
|
| 239 |
+
vr = random.Random(7); L, W = [], []
|
| 240 |
+
for i in range(0, len(val), 16):
|
| 241 |
+
bt = val[i:i + 16]
|
| 242 |
+
ce, _, _, _ = model.forward2([enc(m, vr, force_prompt=mode) for m in bt])
|
| 243 |
+
L.append(float(ce) * len(bt)); W.append(len(bt))
|
| 244 |
+
out["p" if mode else "np"] = sum(L) / sum(W)
|
| 245 |
+
# anchor-only val
|
| 246 |
+
av = [m for m in val if m["spk"] == ANCHOR]
|
| 247 |
+
if av:
|
| 248 |
+
vr = random.Random(7)
|
| 249 |
+
ce, _, _, _ = model.forward2([enc(m, vr, force_prompt=True) for m in av[:32]])
|
| 250 |
+
out["anchor_p"] = float(ce)
|
| 251 |
+
model.train()
|
| 252 |
+
out["mean"] = (out["np"] + out["p"]) / 2
|
| 253 |
+
return out
|
| 254 |
+
|
| 255 |
+
extra = (lambda m: a.anchor_only_w if m["spk"] == ANCHOR else 1.0) if a.anchor_only_w > 0 else None
|
| 256 |
+
rng = random.Random(0); prng = random.Random(3)
|
| 257 |
+
step, nskip, t0 = 0, 0, time.time()
|
| 258 |
+
logf = open(f"{a.out}/log.jsonl", "a")
|
| 259 |
+
v = validate(); best = v["mean"] if not a.select_anchor else 0.5 * (v["mean"] + v.get("anchor_p", v["mean"])); bad = 0
|
| 260 |
+
logf.write(json.dumps({"step": 0, "val": v}) + "\n"); print(json.dumps({"step": 0, "val": v}), flush=True)
|
| 261 |
+
torch.save(dict(model=model.state_dict(), step=0, n_spk=n_spk, val=v), f"{a.out}/best.pt")
|
| 262 |
+
total = a.max_steps
|
| 263 |
+
done = False
|
| 264 |
+
while not done:
|
| 265 |
+
for bi in batches(items, a.tokens, rng, extra, cost=lambda m: m["ntok"] + len(m["text"]) // 2 + 4 + int(a.p_prompt * 200)):
|
| 266 |
+
lrm = min(1.0, (step + 1) / a.warmup) * (0.1 + 0.9 * 0.5 * (1 + math.cos(math.pi * min(1.0, step / total))))
|
| 267 |
+
for g, b in zip(opt.param_groups, base):
|
| 268 |
+
g["lr"] = b * lrm
|
| 269 |
+
batch = [enc(items[i], prng) for i in bi]
|
| 270 |
+
# keep the padded length inside the budget when prompts were added
|
| 271 |
+
with torch.autocast("cuda", dtype=torch.bfloat16):
|
| 272 |
+
ce, acc, adv, adv_acc = model.forward2(batch, adv_lambda=min(1.0, step / 1000))
|
| 273 |
+
loss = ce + a.adv_w * adv
|
| 274 |
+
opt.zero_grad(set_to_none=True)
|
| 275 |
+
if not torch.isfinite(loss):
|
| 276 |
+
nskip += 1; print(json.dumps({"skip_nonfinite_fwd": step, "ce": float(ce), "adv": float(adv)}), flush=True)
|
| 277 |
+
continue
|
| 278 |
+
loss.backward()
|
| 279 |
+
gn = torch.nn.utils.clip_grad_norm_([p for p in model.parameters() if p.requires_grad], 1.0)
|
| 280 |
+
if not torch.isfinite(gn):
|
| 281 |
+
nskip += 1; opt.zero_grad(set_to_none=True); print(json.dumps({"skip_nonfinite_grad": step}), flush=True)
|
| 282 |
+
continue
|
| 283 |
+
opt.step(); step += 1
|
| 284 |
+
if step % 25 == 0:
|
| 285 |
+
r = dict(step=step, ce=round(float(ce), 4), acc=round(float(acc), 4), adv=round(float(adv), 3),
|
| 286 |
+
adv_acc=round(float(adv_acc), 3), gn=round(float(gn), 2), B=len(bi), lr=round(lrm * a.lr, 9),
|
| 287 |
+
skips=nskip, min=round((time.time() - t0) / 60, 2))
|
| 288 |
+
print(json.dumps(r), flush=True); logf.write(json.dumps(r) + "\n"); logf.flush()
|
| 289 |
+
if step % a.val_every == 0:
|
| 290 |
+
v = validate()
|
| 291 |
+
score = v["mean"] if not a.select_anchor else 0.5 * (v["mean"] + v.get("anchor_p", v["mean"]))
|
| 292 |
+
imp = score < best - 1e-4
|
| 293 |
+
rec = {"step": step, "val": v, "best": imp}
|
| 294 |
+
print(json.dumps(rec), flush=True); logf.write(json.dumps(rec) + "\n"); logf.flush()
|
| 295 |
+
if imp:
|
| 296 |
+
best, bad = score, 0
|
| 297 |
+
torch.save(dict(model=model.state_dict(), step=step, n_spk=n_spk, val=v), f"{a.out}/best.pt")
|
| 298 |
+
else:
|
| 299 |
+
bad += 1
|
| 300 |
+
if bad >= a.patience:
|
| 301 |
+
print("EARLY_STOP", step, flush=True); done = True; break
|
| 302 |
+
if (time.time() - t0) / 60 > a.minutes or step >= total:
|
| 303 |
+
done = True; break
|
| 304 |
+
print("TRAIN_LM_DONE", step, "best", best, "skips", nskip, flush=True)
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
# ------------------------------------------------------------------ flow training
|
| 308 |
+
def train_flow(a):
|
| 309 |
+
cosy = load_cosy()
|
| 310 |
+
flow = cosy.model.flow
|
| 311 |
+
if a.flow_init:
|
| 312 |
+
flow.load_state_dict(torch.load(a.flow_init, map_location="cuda")["flow"])
|
| 313 |
+
flow.train()
|
| 314 |
+
for p in cosy.model.llm.parameters():
|
| 315 |
+
p.requires_grad = False
|
| 316 |
+
fe = cosy.frontend.feat_extractor
|
| 317 |
+
dss = a.datasets.split(",")
|
| 318 |
+
items, mm = load_items(dss, "train", max_tok=400, flow=True)
|
| 319 |
+
val, _ = load_items(dss, "val", max_tok=400, flow=True)
|
| 320 |
+
rng0 = random.Random(1); rng0.shuffle(val); val = val[:256]
|
| 321 |
+
pcm = {ds: np.memmap(f"{D2}/{ds}/pcm24k.i16", dtype=np.int16, mode="r") for ds in dss if os.path.exists(f"{D2}/{ds}/pcm24k.i16")}
|
| 322 |
+
emb = {ds: np.load(f"{D2}/{ds}/campplus.npy", mmap_mode="r") for ds in dss if os.path.exists(f"{D2}/{ds}/campplus.npy")}
|
| 323 |
+
os.makedirs(a.out, exist_ok=True)
|
| 324 |
+
print("flow items", len(items), "hours", round(sum(m["dur"] for m in items) / 3600, 1), "val", len(val), flush=True)
|
| 325 |
+
extra = (lambda m: a.anchor_only_w if m["spk"] == ANCHOR else 1.0) if a.anchor_only_w > 0 else None
|
| 326 |
+
|
| 327 |
+
def make(bt):
|
| 328 |
+
toks, feats, es = [], [], []
|
| 329 |
+
for m in bt:
|
| 330 |
+
y = torch.from_numpy(np.asarray(pcm[m["ds"]][m["off24"]: m["off24"] + m["n24"]], dtype=np.float32) / 32768.0)
|
| 331 |
+
n = int(math.ceil(len(y) / 960)) * 960
|
| 332 |
+
y = F.pad(y, (0, n - len(y)))[None].cuda()
|
| 333 |
+
ft = fe(y).squeeze(0).transpose(0, 1) # [T,80]
|
| 334 |
+
tk = torch.from_numpy(np.asarray(mm[m["ds"]][m["toff"]: m["toff"] + m["ntok"]], dtype=np.int64))
|
| 335 |
+
k = min(ft.shape[0] // 2, len(tk))
|
| 336 |
+
toks.append(tk[:k]); feats.append(ft[:2 * k]); es.append(torch.from_numpy(np.array(emb[m["ds"]][m["row"]])))
|
| 337 |
+
tl = torch.tensor([len(t) for t in toks], dtype=torch.int32)
|
| 338 |
+
fl = torch.tensor([len(f) for f in feats], dtype=torch.int32)
|
| 339 |
+
T = int(tl.max()); Fm = int(fl.max())
|
| 340 |
+
tok = torch.zeros(len(bt), T, dtype=torch.long); feat = torch.zeros(len(bt), Fm, 80)
|
| 341 |
+
for i, (t, f) in enumerate(zip(toks, feats)):
|
| 342 |
+
tok[i, :len(t)] = t; feat[i, :len(f)] = f.cpu()
|
| 343 |
+
return dict(speech_token=tok, speech_token_len=tl, speech_feat=feat, speech_feat_len=fl,
|
| 344 |
+
embedding=torch.stack(es).float())
|
| 345 |
+
|
| 346 |
+
params = [p for p in flow.parameters() if p.requires_grad]
|
| 347 |
+
opt = torch.optim.AdamW(params, lr=a.lr, weight_decay=0.0)
|
| 348 |
+
logf = open(f"{a.out}/log.jsonl", "a")
|
| 349 |
+
|
| 350 |
+
def validate():
|
| 351 |
+
flow.eval(); L = []
|
| 352 |
+
st = torch.random.get_rng_state(); cst = torch.cuda.get_rng_state(); pst = random.getstate()
|
| 353 |
+
torch.manual_seed(0); random.seed(0)
|
| 354 |
+
with torch.no_grad():
|
| 355 |
+
for i in range(0, len(val), 16):
|
| 356 |
+
L.append(float(flow(make(val[i:i + 16]), torch.device("cuda"))["loss"]))
|
| 357 |
+
torch.random.set_rng_state(st); torch.cuda.set_rng_state(cst); random.setstate(pst)
|
| 358 |
+
flow.train(); return float(np.mean(L))
|
| 359 |
+
|
| 360 |
+
v = validate(); best, bad = v, 0
|
| 361 |
+
print(json.dumps({"step": 0, "val": v}), flush=True); logf.write(json.dumps({"step": 0, "val": v}) + "\n")
|
| 362 |
+
torch.save(dict(flow=flow.state_dict(), step=0, val=v), f"{a.out}/best.pt")
|
| 363 |
+
rng = random.Random(0); step, t0, done = 0, time.time(), False
|
| 364 |
+
while not done:
|
| 365 |
+
for bi in batches(items, a.tokens, rng, extra, cost=lambda m: m["ntok"]):
|
| 366 |
+
lrm = min(1.0, (step + 1) / a.warmup)
|
| 367 |
+
for g in opt.param_groups:
|
| 368 |
+
g["lr"] = a.lr * lrm
|
| 369 |
+
with torch.autocast("cuda", dtype=torch.bfloat16):
|
| 370 |
+
loss = flow(make([items[i] for i in bi]), torch.device("cuda"))["loss"]
|
| 371 |
+
opt.zero_grad(set_to_none=True)
|
| 372 |
+
if not torch.isfinite(loss):
|
| 373 |
+
continue
|
| 374 |
+
loss.backward()
|
| 375 |
+
gn = torch.nn.utils.clip_grad_norm_(params, 5.0)
|
| 376 |
+
if not torch.isfinite(gn):
|
| 377 |
+
continue
|
| 378 |
+
opt.step(); step += 1
|
| 379 |
+
if step % 50 == 0:
|
| 380 |
+
r = dict(step=step, loss=round(float(loss), 4), gn=round(float(gn), 2), B=len(bi), min=round((time.time() - t0) / 60, 2))
|
| 381 |
+
print(json.dumps(r), flush=True); logf.write(json.dumps(r) + "\n"); logf.flush()
|
| 382 |
+
if step % a.val_every == 0:
|
| 383 |
+
v = validate(); imp = v < best - 1e-4
|
| 384 |
+
print(json.dumps({"step": step, "val": v, "best": imp}), flush=True)
|
| 385 |
+
logf.write(json.dumps({"step": step, "val": v, "best": imp}) + "\n"); logf.flush()
|
| 386 |
+
if imp:
|
| 387 |
+
best, bad = v, 0
|
| 388 |
+
torch.save(dict(flow=flow.state_dict(), step=step, val=v), f"{a.out}/best.pt")
|
| 389 |
+
else:
|
| 390 |
+
bad += 1
|
| 391 |
+
if bad >= a.patience:
|
| 392 |
+
done = True; break
|
| 393 |
+
if (time.time() - t0) / 60 > a.minutes or step >= a.max_steps:
|
| 394 |
+
done = True; break
|
| 395 |
+
print("TRAIN_FLOW_DONE", step, "best", best, flush=True)
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
# ------------------------------------------------------------------ synthesis
|
| 399 |
+
_SENT = re.compile(r"(?<=[.?!])\s+")
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
def split_sentences(text):
|
| 403 |
+
"""tagged text -> list of tagged sentences. A sentence ends at . ? ! inside a span or at the end of a span;
|
| 404 |
+
language spans are cut there and re-tagged, so a sentence keeps its in-sentence switches."""
|
| 405 |
+
out, cur = [], []
|
| 406 |
+
for l, s in segments(text):
|
| 407 |
+
parts = _SENT.split(s.strip())
|
| 408 |
+
for k, p in enumerate(parts):
|
| 409 |
+
p = p.strip()
|
| 410 |
+
if p:
|
| 411 |
+
cur.append(f"<{l}>{p}</{l}>")
|
| 412 |
+
if (k < len(parts) - 1 or p[-1:] in ".?!") and cur:
|
| 413 |
+
out.append(" ".join(cur)); cur = []
|
| 414 |
+
if cur:
|
| 415 |
+
out.append(" ".join(cur))
|
| 416 |
+
return [o for o in out if o.strip()]
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
def trim_edges(w, sr, keep_lead=0.12, keep_tail=0.18):
|
| 420 |
+
from da.p2common import silence_mask
|
| 421 |
+
sil = silence_mask(w, sr)
|
| 422 |
+
sp = np.where(~sil)[0]
|
| 423 |
+
if not len(sp):
|
| 424 |
+
return w
|
| 425 |
+
a, b = sp[0] * 0.01, (sp[-1] + 1) * 0.01
|
| 426 |
+
out = w[int(max(0, a - keep_lead) * sr): int(min(len(w) / sr, b + keep_tail) * sr)]
|
| 427 |
+
lead = a - max(0, a - keep_lead)
|
| 428 |
+
if lead < keep_lead:
|
| 429 |
+
out = np.concatenate([np.zeros(int((keep_lead - lead) * sr), out.dtype), out])
|
| 430 |
+
return out
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
def cap_internal(w, sr, cap=0.6):
|
| 434 |
+
"""shorten every internal silence longer than cap seconds to cap (the centre of the silent run is removed)."""
|
| 435 |
+
from da.p2common import silence_mask, runs
|
| 436 |
+
sil = silence_mask(w, sr)
|
| 437 |
+
hop = int(0.01 * sr)
|
| 438 |
+
R = [(a, b) for a, b in runs(sil) if a > 0 and b < len(sil) and (b - a) * 0.01 > cap]
|
| 439 |
+
if not R:
|
| 440 |
+
return w
|
| 441 |
+
keep, cur = [], 0
|
| 442 |
+
for a, b in R:
|
| 443 |
+
cut0 = a * hop + int(cap / 2 * sr)
|
| 444 |
+
cut1 = b * hop - int(cap / 2 * sr)
|
| 445 |
+
keep.append(w[cur:cut0]); cur = cut1
|
| 446 |
+
keep.append(w[cur:])
|
| 447 |
+
return np.concatenate(keep)
|
| 448 |
+
|
| 449 |
+
|
| 450 |
+
class Synth:
|
| 451 |
+
def __init__(self, ckpt, flow_ckpt=None, sent_pause=0.5):
|
| 452 |
+
from transformers import AutoTokenizer
|
| 453 |
+
self.tok = AutoTokenizer.from_pretrained(f"{MODEL_DIR}/CosyVoice-BlankEN")
|
| 454 |
+
self.cosy = load_cosy()
|
| 455 |
+
if flow_ckpt:
|
| 456 |
+
self.cosy.model.flow.load_state_dict(torch.load(flow_ckpt, map_location="cuda")["flow"])
|
| 457 |
+
ck = torch.load(ckpt, map_location="cuda")
|
| 458 |
+
self.spk2id = json.load(open(os.path.join(os.path.dirname(ckpt), "spk2id.json")))
|
| 459 |
+
self.m = DALM2(self.cosy.model.llm, ck["n_spk"]).cuda().eval()
|
| 460 |
+
self.m.load_state_dict(ck["model"])
|
| 461 |
+
self.sent_pause = sent_pause
|
| 462 |
+
self.cap = 0.6
|
| 463 |
+
# anchor prompts: normalised anchor val clips from data2 (never in train), 3-7 s, highest SNR first
|
| 464 |
+
meta = [json.loads(l) for l in open(f"{D2}/waxal/meta.jsonl", encoding="utf-8")]
|
| 465 |
+
tk = [json.loads(l) for l in open(f"{D2}/waxal/s3tok.jsonl")]
|
| 466 |
+
pcm = np.memmap(f"{D2}/waxal/pcm24k.i16", dtype=np.int16, mode="r")
|
| 467 |
+
s3 = np.memmap(f"{D2}/waxal/s3tok.i32", dtype=np.int32, mode="r")
|
| 468 |
+
cand = [(m, t) for m, t in zip(meta, tk) if m["spk"] == ANCHOR and m["split"] == "val" and 3.0 <= m["dur"] <= 7.0
|
| 469 |
+
and not m.get("longform")]
|
| 470 |
+
cand.sort(key=lambda x: -x[0]["ascore"])
|
| 471 |
+
self.prompts = []
|
| 472 |
+
os.makedirs(f"{HOME}/eval/prompts", exist_ok=True)
|
| 473 |
+
for m, t in cand[:4]:
|
| 474 |
+
y = np.asarray(pcm[m["off24"]: m["off24"] + m["n24"]], dtype=np.float32) / 32768.0
|
| 475 |
+
p = f"{HOME}/eval/prompts/{m['id']}.wav"; sf.write(p, y, 24000)
|
| 476 |
+
from train_b import prompt_feats
|
| 477 |
+
ptok, pfeat, pemb = prompt_feats(self.cosy, p)
|
| 478 |
+
ids, langs = text_tokens(self.tok, tagged(m))
|
| 479 |
+
lmp = (torch.tensor(ids).cuda(), torch.tensor(langs).cuda(),
|
| 480 |
+
torch.from_numpy(np.asarray(s3[t["toff"]: t["toff"] + t["ntok"]], dtype=np.int64)).cuda())
|
| 481 |
+
self.prompts.append(dict(id=m["id"], text=m["text"], lm=lmp, flow=(ptok, pfeat, pemb)))
|
| 482 |
+
print("anchor prompts", [p["id"] for p in self.prompts], flush=True)
|
| 483 |
+
|
| 484 |
+
def one(self, tagged_text, seed=0, prompt_k=0, use_lm_prompt=True):
|
| 485 |
+
ids, langs = text_tokens(self.tok, tagged_text)
|
| 486 |
+
P = self.prompts[prompt_k % len(self.prompts)]
|
| 487 |
+
with torch.autocast("cuda", dtype=torch.bfloat16):
|
| 488 |
+
sp = self.m.generate2(torch.tensor(ids).cuda(), torch.tensor(langs).cuda(),
|
| 489 |
+
torch.tensor(self.spk2id[ANCHOR]).cuda(), prompt=P["lm"] if use_lm_prompt else None,
|
| 490 |
+
seed=seed)
|
| 491 |
+
if len(sp) < 3:
|
| 492 |
+
sp = sp + [0] * 3
|
| 493 |
+
self.last_tokens.append(sp)
|
| 494 |
+
return token2wav(self.cosy, sp, *P["flow"])
|
| 495 |
+
|
| 496 |
+
def join(self, ws):
|
| 497 |
+
sr = self.cosy.sample_rate
|
| 498 |
+
if len(ws) == 1:
|
| 499 |
+
return ws[0]
|
| 500 |
+
out = []
|
| 501 |
+
for i, w in enumerate(ws):
|
| 502 |
+
if i:
|
| 503 |
+
out.append(np.zeros(max(0, int((self.sent_pause - 0.30) * sr)), np.float32))
|
| 504 |
+
out.append(w)
|
| 505 |
+
return np.concatenate(out)
|
| 506 |
+
|
| 507 |
+
def redecode(self, toks, prompt_k=0):
|
| 508 |
+
"""same LM tokens, (possibly different) flow -> identical post-processing"""
|
| 509 |
+
sr = self.cosy.sample_rate
|
| 510 |
+
P = self.prompts[prompt_k % len(self.prompts)]
|
| 511 |
+
return self.join([cap_internal(trim_edges(token2wav(self.cosy, t, *P["flow"]), sr), sr, self.cap) for t in toks])
|
| 512 |
+
|
| 513 |
+
def say(self, tagged_text, seed=0, prompt_k=0, stitch=True, use_lm_prompt=True):
|
| 514 |
+
sr = self.cosy.sample_rate
|
| 515 |
+
self.last_tokens = []
|
| 516 |
+
sents = split_sentences(tagged_text) if stitch else [tagged_text]
|
| 517 |
+
ws = [cap_internal(trim_edges(self.one(s, seed=seed + 1000 * i, prompt_k=prompt_k, use_lm_prompt=use_lm_prompt), sr), sr, self.cap)
|
| 518 |
+
for i, s in enumerate(sents)]
|
| 519 |
+
# trailing 0.18 + leading 0.12 already present; join() tops up to the anchor's sentence pause
|
| 520 |
+
return self.join(ws)
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
def redecode(a):
|
| 524 |
+
"""decode the LM tokens saved by synth (--src DIR) with another flow checkpoint -> --out"""
|
| 525 |
+
S = Synth(a.ckpt, a.flow or None, a.sent_pause)
|
| 526 |
+
os.makedirs(a.out, exist_ok=True)
|
| 527 |
+
import glob
|
| 528 |
+
for f in sorted(glob.glob(f"{a.src}/*.tok.json")):
|
| 529 |
+
d = json.load(open(f))
|
| 530 |
+
i = os.path.basename(f)[:-9]
|
| 531 |
+
sf.write(f"{a.out}/{i}.wav", S.redecode(d["tokens"], d["prompt_k"]), S.cosy.sample_rate)
|
| 532 |
+
print("REDECODE_DONE", a.out, flush=True)
|
| 533 |
+
|
| 534 |
+
|
| 535 |
+
def synth(a):
|
| 536 |
+
S = Synth(a.ckpt, a.flow or None, a.sent_pause)
|
| 537 |
+
rows = [json.loads(l) for l in open(a.eval_set, encoding="utf-8")]
|
| 538 |
+
if a.only:
|
| 539 |
+
keep = set(a.only.split(",")); rows = [r for r in rows if r["id"] in keep or r["lang"] in keep]
|
| 540 |
+
if a.nper:
|
| 541 |
+
cnt, sel = {}, []
|
| 542 |
+
for r in rows:
|
| 543 |
+
if r["lang"] in ("cs", "para") or cnt.get(r["lang"], 0) < a.nper:
|
| 544 |
+
sel.append(r); cnt[r["lang"]] = cnt.get(r["lang"], 0) + 1
|
| 545 |
+
rows = sel
|
| 546 |
+
ks = [a.k] if a.k >= 0 else list(range(a.n))
|
| 547 |
+
for k in ks:
|
| 548 |
+
od = a.out if len(ks) == 1 else f"{a.out}/cand{k}"
|
| 549 |
+
os.makedirs(od, exist_ok=True)
|
| 550 |
+
for r in rows:
|
| 551 |
+
text = r["text"] if r["lang"] in ("cs", "para") else f"<{r['lang']}>{r['text']}</{r['lang']}>"
|
| 552 |
+
w = S.say(text, seed=k, prompt_k=k, stitch=not a.no_stitch, use_lm_prompt=not a.no_lm_prompt)
|
| 553 |
+
sf.write(f"{od}/{r['id']}.wav", w, S.cosy.sample_rate)
|
| 554 |
+
json.dump(dict(tokens=S.last_tokens, prompt_k=k), open(f"{od}/{r['id']}.tok.json", "w"))
|
| 555 |
+
print("synth cand", k, "done", od, flush=True)
|
| 556 |
+
print("SYNTH2_DONE", a.out, flush=True)
|
| 557 |
+
|
| 558 |
+
|
| 559 |
+
if __name__ == "__main__":
|
| 560 |
+
ap = argparse.ArgumentParser()
|
| 561 |
+
ap.add_argument("cmd")
|
| 562 |
+
ap.add_argument("--datasets", default="waxal,fleurs,slr86,slr70,bible_yor,bible_hau,cs_pairs")
|
| 563 |
+
ap.add_argument("--out", default=f"{HOME}/runs/p2_lm")
|
| 564 |
+
ap.add_argument("--init", default="")
|
| 565 |
+
ap.add_argument("--flow_init", default="")
|
| 566 |
+
ap.add_argument("--ckpt", default="")
|
| 567 |
+
ap.add_argument("--flow", default="")
|
| 568 |
+
ap.add_argument("--minutes", type=float, default=90)
|
| 569 |
+
ap.add_argument("--max_steps", type=int, default=4000)
|
| 570 |
+
ap.add_argument("--lr", type=float, default=1e-5)
|
| 571 |
+
ap.add_argument("--lr_new", type=float, default=5e-4)
|
| 572 |
+
ap.add_argument("--warmup", type=int, default=300)
|
| 573 |
+
ap.add_argument("--tokens", type=int, default=16000)
|
| 574 |
+
ap.add_argument("--adv_w", type=float, default=0.02)
|
| 575 |
+
ap.add_argument("--p_prompt", type=float, default=0.5)
|
| 576 |
+
ap.add_argument("--val_every", type=int, default=200)
|
| 577 |
+
ap.add_argument("--patience", type=int, default=5)
|
| 578 |
+
ap.add_argument("--anchor_only_w", type=float, default=0.0)
|
| 579 |
+
ap.add_argument("--select_anchor", action="store_true")
|
| 580 |
+
ap.add_argument("--eval_set", default=f"{HOME}/eval/eval_set2.jsonl")
|
| 581 |
+
ap.add_argument("--n", type=int, default=1)
|
| 582 |
+
ap.add_argument("--nper", type=int, default=0)
|
| 583 |
+
ap.add_argument("--k", type=int, default=-1)
|
| 584 |
+
ap.add_argument("--src", default="")
|
| 585 |
+
ap.add_argument("--only", default="")
|
| 586 |
+
ap.add_argument("--sent_pause", type=float, default=0.6)
|
| 587 |
+
ap.add_argument("--no_stitch", action="store_true")
|
| 588 |
+
ap.add_argument("--no_lm_prompt", action="store_true")
|
| 589 |
+
a = ap.parse_args()
|
| 590 |
+
{"lm": train_lm, "flow": train_flow, "synth": synth, "redecode": redecode}[a.cmd](a)
|
cosyvoice_requirements.txt
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
--extra-index-url https://download.pytorch.org/whl/cu121
|
| 2 |
+
--extra-index-url https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/ # https://github.com/microsoft/onnxruntime/issues/21684
|
| 3 |
+
conformer==0.3.2
|
| 4 |
+
deepspeed==0.15.1; sys_platform == 'linux'
|
| 5 |
+
diffusers==0.29.0
|
| 6 |
+
fastapi==0.115.6
|
| 7 |
+
fastapi-cli==0.0.4
|
| 8 |
+
gdown==5.1.0
|
| 9 |
+
gradio==5.4.0
|
| 10 |
+
grpcio==1.57.0
|
| 11 |
+
grpcio-tools==1.57.0
|
| 12 |
+
hydra-core==1.3.2
|
| 13 |
+
HyperPyYAML==1.2.3
|
| 14 |
+
inflect==7.3.1
|
| 15 |
+
librosa==0.10.2
|
| 16 |
+
lightning==2.2.4
|
| 17 |
+
matplotlib==3.7.5
|
| 18 |
+
modelscope==1.20.0
|
| 19 |
+
networkx==3.1
|
| 20 |
+
numpy==1.26.4
|
| 21 |
+
omegaconf==2.3.0
|
| 22 |
+
onnx==1.16.0
|
| 23 |
+
onnxruntime-gpu==1.18.0; sys_platform == 'linux'
|
| 24 |
+
onnxruntime==1.18.0; sys_platform == 'darwin' or sys_platform == 'win32'
|
| 25 |
+
openai-whisper==20231117
|
| 26 |
+
protobuf==4.25
|
| 27 |
+
pyarrow==18.1.0
|
| 28 |
+
pydantic==2.7.0
|
| 29 |
+
pyworld==0.3.4
|
| 30 |
+
rich==13.7.1
|
| 31 |
+
soundfile==0.12.1
|
| 32 |
+
tensorboard==2.14.0
|
| 33 |
+
tensorrt-cu12==10.13.3.9; sys_platform == 'linux'
|
| 34 |
+
tensorrt-cu12-bindings==10.13.3.9; sys_platform == 'linux'
|
| 35 |
+
tensorrt-cu12-libs==10.13.3.9; sys_platform == 'linux'
|
| 36 |
+
torch==2.3.1
|
| 37 |
+
torchaudio==2.3.1
|
| 38 |
+
transformers==4.51.3
|
| 39 |
+
x-transformers==2.11.24
|
| 40 |
+
uvicorn==0.30.0
|
| 41 |
+
wetext==0.0.4
|
| 42 |
+
wget==3.2
|
infer.py
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""Dynamic Accent (Phase 2 preview): Nigerian multilingual, code-switching TTS on CosyVoice2-0.5B.
|
| 3 |
+
|
| 4 |
+
Same inference path as the Phase 2 final samples: LM `p2_lm_B` (pause profile B) + anchor-SFT flow,
|
| 5 |
+
anchor prompt clip 0 at the LM and the flow, single sample, sentence-level stitching, edge trim and
|
| 6 |
+
internal-silence cap (0.6 s), 0.6 s between sentences.
|
| 7 |
+
|
| 8 |
+
python infer.py --text "<eng>this is dynamic accent,</eng> <pcm>e dey sound nice abi?</pcm>" --seed 0 --out out.wav
|
| 9 |
+
|
| 10 |
+
Prosody v2 (default, `--pause_mode clause`): the text is cut at , ; : . ? ! and at language-switch points; each chunk
|
| 11 |
+
is generated with the same LM/flow/prompt, its edge silence is trimmed (energy VAD, 30 ms kept), its speech is sped
|
| 12 |
+
up pitch-preserving by `--speed` (WSOLA, default 1.05), and chunks are joined with set gaps from the real anchor's pause statistics:
|
| 13 |
+
comma/;/: 0.40 s (+-0.05 jitter), sentence end 0.60 s, unpunctuated language switch 0.12 s, with 10 ms fades.
|
| 14 |
+
`--pause_mode model --speed 1.0` reproduces the Phase 2 final samples (model-generated pauses, sentence stitching).
|
| 15 |
+
|
| 16 |
+
Text: wrap each language span in <eng>, <pcm>, <yor>, <ibo> or <hau> tags. Untagged text is treated as English.
|
| 17 |
+
"""
|
| 18 |
+
import argparse, inspect, json, os, re, sys, unicodedata
|
| 19 |
+
|
| 20 |
+
import numpy as np
|
| 21 |
+
import soundfile as sf
|
| 22 |
+
import librosa
|
| 23 |
+
import torch
|
| 24 |
+
import torch.nn as nn
|
| 25 |
+
|
| 26 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 27 |
+
LANGS = ["eng", "ibo", "hau", "yor", "pcm"]
|
| 28 |
+
L2I = {l: i for i, l in enumerate(LANGS)}
|
| 29 |
+
ANCHOR = "waxal_yor_4"
|
| 30 |
+
DEFAULT_SPEED = 1.05 # from the speaking-rate measurement (README, "Prosody v2"): median ref/system ratio 1.06
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# ------------------------------------------------------------------ text front-end (tags -> spans)
|
| 34 |
+
_TAG = re.compile(r"<(eng|ibo|hau|yor|pcm)>(.*?)</\1>", re.S)
|
| 35 |
+
_SENT = re.compile(r"(?<=[.?!])\s+")
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def normalize(t):
|
| 39 |
+
t = unicodedata.normalize("NFC", t)
|
| 40 |
+
t = t.replace("\u2019", "'").replace("\u2018", "'").replace("\u201c", '"').replace("\u201d", '"')
|
| 41 |
+
return re.sub(r"\s+", " ", t).strip()
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def segments(text, default_lang="eng"):
|
| 45 |
+
out, pos = [], 0
|
| 46 |
+
for m in _TAG.finditer(text):
|
| 47 |
+
pre = text[pos:m.start()]
|
| 48 |
+
if pre.strip():
|
| 49 |
+
out.append((default_lang, pre))
|
| 50 |
+
out.append((m.group(1), m.group(2)))
|
| 51 |
+
pos = m.end()
|
| 52 |
+
if text[pos:].strip():
|
| 53 |
+
out.append((default_lang, text[pos:]))
|
| 54 |
+
res = []
|
| 55 |
+
for l, s in out:
|
| 56 |
+
s = normalize(s)
|
| 57 |
+
if not s:
|
| 58 |
+
continue
|
| 59 |
+
if res:
|
| 60 |
+
s = " " + s
|
| 61 |
+
res.append((l, s))
|
| 62 |
+
return res
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def text_tokens(tok, text, default="eng"):
|
| 66 |
+
ids, langs = [], []
|
| 67 |
+
for l, s in segments(text, default):
|
| 68 |
+
t = tok(s, add_special_tokens=False)["input_ids"]
|
| 69 |
+
ids += t; langs += [L2I[l]] * len(t)
|
| 70 |
+
return ids, langs
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def split_sentences(text):
|
| 74 |
+
out, cur = [], []
|
| 75 |
+
for l, s in segments(text):
|
| 76 |
+
parts = _SENT.split(s.strip())
|
| 77 |
+
for k, p in enumerate(parts):
|
| 78 |
+
p = p.strip()
|
| 79 |
+
if p:
|
| 80 |
+
cur.append(f"<{l}>{p}</{l}>")
|
| 81 |
+
if (k < len(parts) - 1 or p[-1:] in ".?!") and cur:
|
| 82 |
+
out.append(" ".join(cur)); cur = []
|
| 83 |
+
if cur:
|
| 84 |
+
out.append(" ".join(cur))
|
| 85 |
+
return [o for o in out if o.strip()]
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
# ------------------------------------------------------------------ silence post-processing
|
| 89 |
+
def frame_db(y, sr, hop_s=0.01):
|
| 90 |
+
hop = int(sr * hop_s)
|
| 91 |
+
n = max(1, len(y) // hop)
|
| 92 |
+
fr = y[: n * hop].reshape(n, hop)
|
| 93 |
+
return 10 * np.log10((fr.astype(np.float64) ** 2).mean(1) + 1e-10)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def silence_mask(y, sr, rel_db=-40.0, hop_s=0.01):
|
| 97 |
+
e = frame_db(y, sr, hop_s)
|
| 98 |
+
return e < np.percentile(e, 98) + rel_db
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def runs(mask):
|
| 102 |
+
out, i, n = [], 0, len(mask)
|
| 103 |
+
while i < n:
|
| 104 |
+
if mask[i]:
|
| 105 |
+
j = i
|
| 106 |
+
while j < n and mask[j]:
|
| 107 |
+
j += 1
|
| 108 |
+
out.append((i, j)); i = j
|
| 109 |
+
else:
|
| 110 |
+
i += 1
|
| 111 |
+
return out
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def trim_edges(w, sr, keep_lead=0.12, keep_tail=0.18):
|
| 115 |
+
sil = silence_mask(w, sr)
|
| 116 |
+
sp = np.where(~sil)[0]
|
| 117 |
+
if not len(sp):
|
| 118 |
+
return w
|
| 119 |
+
a, b = sp[0] * 0.01, (sp[-1] + 1) * 0.01
|
| 120 |
+
out = w[int(max(0, a - keep_lead) * sr): int(min(len(w) / sr, b + keep_tail) * sr)]
|
| 121 |
+
lead = a - max(0, a - keep_lead)
|
| 122 |
+
if lead < keep_lead:
|
| 123 |
+
out = np.concatenate([np.zeros(int((keep_lead - lead) * sr), out.dtype), out])
|
| 124 |
+
return out
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def cap_internal(w, sr, cap=0.6):
|
| 128 |
+
sil = silence_mask(w, sr)
|
| 129 |
+
hop = int(0.01 * sr)
|
| 130 |
+
R = [(a, b) for a, b in runs(sil) if a > 0 and b < len(sil) and (b - a) * 0.01 > cap]
|
| 131 |
+
if not R:
|
| 132 |
+
return w
|
| 133 |
+
keep, cur = [], 0
|
| 134 |
+
for a, b in R:
|
| 135 |
+
keep.append(w[cur: a * hop + int(cap / 2 * sr)]); cur = b * hop - int(cap / 2 * sr)
|
| 136 |
+
keep.append(w[cur:])
|
| 137 |
+
return np.concatenate(keep)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
# ------------------------------------------------------------------ prosody v2: clause chunks, set gaps, pitch-preserving speed
|
| 142 |
+
_PUNCT = re.compile(r"(?<=[,;:.?!])\s+")
|
| 143 |
+
GAPS = {"comma": 0.40, "sent": 0.60, "switch": 0.12}
|
| 144 |
+
JITTER = {"comma": 0.05, "sent": 0.03, "switch": 0.02}
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def clause_chunks(text):
|
| 148 |
+
"""tagged text -> [(lang, chunk_text, boundary_after)] with boundary in comma|sent|switch|None (None = end).
|
| 149 |
+
Chunks are cut after , ; : . ? ! and at language switches; a one-word chunk is merged into the next chunk of the
|
| 150 |
+
same language (one-word LM calls are unstable), keeping its comma inside."""
|
| 151 |
+
raw = []
|
| 152 |
+
for l, s in segments(text):
|
| 153 |
+
for p in _PUNCT.split(s.strip()):
|
| 154 |
+
if p.strip():
|
| 155 |
+
raw.append([l, p.strip()])
|
| 156 |
+
merged = []
|
| 157 |
+
for c in raw:
|
| 158 |
+
if merged and merged[-1][0] == c[0] and len(merged[-1][1].split()) < 2:
|
| 159 |
+
merged[-1][1] = merged[-1][1] + " " + c[1]
|
| 160 |
+
else:
|
| 161 |
+
merged.append(c)
|
| 162 |
+
out = []
|
| 163 |
+
for i, (l, t) in enumerate(merged):
|
| 164 |
+
if i == len(merged) - 1:
|
| 165 |
+
b = None
|
| 166 |
+
elif t[-1] in ".?!":
|
| 167 |
+
b = "sent"
|
| 168 |
+
elif t[-1] in ",;:":
|
| 169 |
+
b = "comma"
|
| 170 |
+
else:
|
| 171 |
+
b = "switch"
|
| 172 |
+
out.append((l, t, b))
|
| 173 |
+
return out
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def trim_tight(w, sr, keep=0.03):
|
| 177 |
+
sil = silence_mask(w, sr)
|
| 178 |
+
sp = np.where(~sil)[0]
|
| 179 |
+
if not len(sp):
|
| 180 |
+
return w
|
| 181 |
+
a, b = sp[0] * 0.01, (sp[-1] + 1) * 0.01
|
| 182 |
+
return w[int(max(0.0, a - keep) * sr): int(min(len(w) / sr, b + keep) * sr)]
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def time_stretch(w, sr, speed):
|
| 186 |
+
"""pitch-preserving tempo change (speed > 1 = faster). WSOLA via audiotsm, else librosa's phase vocoder."""
|
| 187 |
+
if abs(speed - 1.0) < 1e-3:
|
| 188 |
+
return w
|
| 189 |
+
try:
|
| 190 |
+
from audiotsm import wsola
|
| 191 |
+
from audiotsm.io.array import ArrayReader, ArrayWriter
|
| 192 |
+
r, wr = ArrayReader(w[None].astype(np.float32)), ArrayWriter(1)
|
| 193 |
+
wsola(1, speed=speed, frame_length=int(0.040 * sr), synthesis_hop=int(0.010 * sr)).run(r, wr)
|
| 194 |
+
return wr.data[0].astype(np.float32)
|
| 195 |
+
except ImportError:
|
| 196 |
+
return librosa.effects.time_stretch(w.astype(np.float32), rate=speed)
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def fade(w, sr, ms=10):
|
| 200 |
+
n = min(len(w) // 2, int(sr * ms / 1000))
|
| 201 |
+
if n > 0:
|
| 202 |
+
w = w.copy(); r = np.linspace(0, 1, n, dtype=np.float32)
|
| 203 |
+
w[:n] *= r; w[-n:] *= r[::-1]
|
| 204 |
+
return w
|
| 205 |
+
|
| 206 |
+
# ------------------------------------------------------------------ CosyVoice2 base
|
| 207 |
+
def _sf_load_wav(wav, target_sr, min_sr=16000):
|
| 208 |
+
y, sr = sf.read(wav, dtype="float32", always_2d=True)
|
| 209 |
+
y = y.mean(1)
|
| 210 |
+
if sr != target_sr:
|
| 211 |
+
y = librosa.resample(y, orig_sr=sr, target_sr=target_sr)
|
| 212 |
+
return torch.from_numpy(y)[None]
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def load_cosy(model_dir, cosyvoice_dir):
|
| 216 |
+
sys.path[:0] = [cosyvoice_dir, os.path.join(cosyvoice_dir, "third_party", "Matcha-TTS")]
|
| 217 |
+
import cosyvoice.utils.file_utils as fu
|
| 218 |
+
fu.load_wav = _sf_load_wav
|
| 219 |
+
import cosyvoice.cli.frontend as cf
|
| 220 |
+
cf.load_wav = _sf_load_wav
|
| 221 |
+
from cosyvoice.cli.cosyvoice import CosyVoice2
|
| 222 |
+
kw = dict(load_jit=False, load_trt=False, fp16=False)
|
| 223 |
+
sig = inspect.signature(CosyVoice2.__init__).parameters
|
| 224 |
+
return CosyVoice2(model_dir, **{k: v for k, v in kw.items() if k in sig})
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def prompt_feats(cosy, wav_path):
|
| 228 |
+
fe = cosy.frontend
|
| 229 |
+
tokn, _ = fe._extract_speech_token(wav_path)
|
| 230 |
+
feat, _ = fe._extract_speech_feat(wav_path)
|
| 231 |
+
n = min(feat.shape[1] // 2, tokn.shape[1])
|
| 232 |
+
feat, tokn = feat[:, :2 * n], tokn[:, :n]
|
| 233 |
+
emb = fe._extract_spk_embedding(wav_path)
|
| 234 |
+
return tokn.cuda(), feat.cuda(), emb.cuda()
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
@torch.no_grad()
|
| 238 |
+
def token2wav(cosy, tokens, ptok, pfeat, emb):
|
| 239 |
+
flow = cosy.model.flow
|
| 240 |
+
t = torch.tensor([tokens], dtype=torch.int32).cuda()
|
| 241 |
+
kw = dict(token=t, token_len=torch.tensor([t.shape[1]], dtype=torch.int32).cuda(),
|
| 242 |
+
prompt_token=ptok, prompt_token_len=torch.tensor([ptok.shape[1]], dtype=torch.int32).cuda(),
|
| 243 |
+
prompt_feat=pfeat, prompt_feat_len=torch.tensor([pfeat.shape[1]], dtype=torch.int32).cuda(),
|
| 244 |
+
embedding=emb)
|
| 245 |
+
sig = inspect.signature(flow.inference).parameters
|
| 246 |
+
if "streaming" in sig:
|
| 247 |
+
kw["streaming"] = False
|
| 248 |
+
if "finalize" in sig:
|
| 249 |
+
kw["finalize"] = True
|
| 250 |
+
if "flow_cache" in sig:
|
| 251 |
+
kw["flow_cache"] = torch.zeros(1, 80, 0, 2).cuda()
|
| 252 |
+
mel = flow.inference(**kw)
|
| 253 |
+
mel = mel[0] if isinstance(mel, tuple) else mel
|
| 254 |
+
hs = inspect.signature(cosy.model.hift.inference).parameters
|
| 255 |
+
w = cosy.model.hift.inference(speech_feat=mel, **({"cache_source": torch.zeros(1, 1, 0).cuda()} if "cache_source" in hs else {}))
|
| 256 |
+
w = w[0] if isinstance(w, tuple) else w
|
| 257 |
+
return w.squeeze().float().cpu().numpy()
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
# ------------------------------------------------------------------ Dynamic Accent LM (Qwen2LM + language + speaker embeddings)
|
| 261 |
+
class DALM2(nn.Module):
|
| 262 |
+
def __init__(self, qlm, n_spk, d=896):
|
| 263 |
+
super().__init__()
|
| 264 |
+
self.q = qlm
|
| 265 |
+
self.lang_emb = nn.Embedding(len(LANGS), d)
|
| 266 |
+
self.spk_emb = nn.Embedding(n_spk, d)
|
| 267 |
+
self.adv = nn.Sequential(nn.Linear(d, 256), nn.ReLU(), nn.Linear(256, n_spk)) # training-only head
|
| 268 |
+
self.V = qlm.speech_token_size
|
| 269 |
+
|
| 270 |
+
def embed_text(self, ids, langs):
|
| 271 |
+
return self.q.llm.model.model.embed_tokens(ids) + self.lang_emb(langs)
|
| 272 |
+
|
| 273 |
+
def build2(self, ids, langs, spk, pids=None, plangs=None, psp=None):
|
| 274 |
+
sos = self.q.llm_embedding.weight[self.q.sos][None]
|
| 275 |
+
task = self.q.llm_embedding.weight[self.q.task_id][None]
|
| 276 |
+
if pids is not None:
|
| 277 |
+
ids = torch.cat([pids, ids]); langs = torch.cat([plangs, langs])
|
| 278 |
+
parts = [sos, self.embed_text(ids, langs), self.spk_emb(spk)[None], task]
|
| 279 |
+
if psp is not None:
|
| 280 |
+
parts.append(self.q.speech_embedding(psp))
|
| 281 |
+
return torch.cat(parts, 0)
|
| 282 |
+
|
| 283 |
+
@torch.no_grad()
|
| 284 |
+
def generate2(self, ids, langs, spk, prompt=None, top_p=0.8, top_k=25, win=10, tau_r=0.1, seed=0,
|
| 285 |
+
min_ratio=2.0, max_ratio=20.0):
|
| 286 |
+
g = torch.Generator(device=ids.device).manual_seed(seed)
|
| 287 |
+
x = self.build2(ids, langs, spk, *(prompt if prompt else (None, None, None)))[None]
|
| 288 |
+
n = len(ids)
|
| 289 |
+
min_len, max_len = int(n * min_ratio), int(n * max_ratio)
|
| 290 |
+
past, out, inp = None, [], x
|
| 291 |
+
for i in range(max_len):
|
| 292 |
+
o = self.q.llm.model.model(inputs_embeds=inp, past_key_values=past, use_cache=True, return_dict=True)
|
| 293 |
+
past = o.past_key_values
|
| 294 |
+
logp = self.q.llm_decoder(o.last_hidden_state[:, -1]).float().log_softmax(-1)[0]
|
| 295 |
+
if i < min_len:
|
| 296 |
+
logp[self.V] = -float("inf")
|
| 297 |
+
logp[self.V + 1:] = -float("inf")
|
| 298 |
+
p = logp.exp()
|
| 299 |
+
sp, si = p.sort(descending=True)
|
| 300 |
+
keep = (sp.cumsum(0) - sp) < top_p
|
| 301 |
+
keep[top_k:] = False
|
| 302 |
+
cp, ci = sp[keep], si[keep]
|
| 303 |
+
tokid = int(ci[torch.multinomial(cp / cp.sum(), 1, generator=g)])
|
| 304 |
+
if out and sum(1 for t in out[-win:] if t == tokid) >= win * tau_r and tokid != self.V: # RAS
|
| 305 |
+
tokid = int(torch.multinomial(p, 1, generator=g))
|
| 306 |
+
if tokid >= self.V:
|
| 307 |
+
break
|
| 308 |
+
out.append(tokid)
|
| 309 |
+
inp = self.q.speech_embedding.weight[tokid][None, None]
|
| 310 |
+
return out
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
class DynamicAccent:
|
| 314 |
+
def __init__(self, model_dir, cosyvoice_dir, lm_ckpt, flow_ckpt, prompts_json, dtype="auto", sent_pause=0.6, cap=0.6):
|
| 315 |
+
from transformers import AutoTokenizer
|
| 316 |
+
self.tok = AutoTokenizer.from_pretrained(os.path.join(model_dir, "CosyVoice-BlankEN"))
|
| 317 |
+
self.cosy = load_cosy(model_dir, cosyvoice_dir)
|
| 318 |
+
self.sr = self.cosy.sample_rate
|
| 319 |
+
self.cosy.model.flow.load_state_dict(torch.load(flow_ckpt, map_location="cuda")["flow"])
|
| 320 |
+
ck = torch.load(lm_ckpt, map_location="cuda")
|
| 321 |
+
self.spk2id = json.load(open(os.path.join(os.path.dirname(lm_ckpt), "spk2id.json")))
|
| 322 |
+
self.m = DALM2(self.cosy.model.llm, ck["n_spk"]).cuda().eval()
|
| 323 |
+
self.m.load_state_dict(ck["model"])
|
| 324 |
+
if dtype == "auto":
|
| 325 |
+
dtype = "bf16" if torch.cuda.is_bf16_supported() else "fp16"
|
| 326 |
+
self.dtype = torch.bfloat16 if dtype == "bf16" else torch.float16
|
| 327 |
+
self.sent_pause, self.cap = sent_pause, cap
|
| 328 |
+
meta = json.load(open(prompts_json, encoding="utf-8"))
|
| 329 |
+
root = os.path.dirname(os.path.dirname(os.path.abspath(prompts_json)))
|
| 330 |
+
self.prompts = []
|
| 331 |
+
for p in meta["prompts"]:
|
| 332 |
+
ids, langs = text_tokens(self.tok, p["tagged"])
|
| 333 |
+
lmp = (torch.tensor(ids).cuda(), torch.tensor(langs).cuda(), torch.tensor(p["s3_tokens"], dtype=torch.int64).cuda())
|
| 334 |
+
self.prompts.append(dict(id=p["id"], lm=lmp, flow=prompt_feats(self.cosy, os.path.join(root, p["wav"]))))
|
| 335 |
+
|
| 336 |
+
def one(self, tagged_text, seed, prompt_k, use_lm_prompt=True):
|
| 337 |
+
ids, langs = text_tokens(self.tok, tagged_text)
|
| 338 |
+
P = self.prompts[prompt_k % len(self.prompts)]
|
| 339 |
+
with torch.autocast("cuda", dtype=self.dtype):
|
| 340 |
+
sp = self.m.generate2(torch.tensor(ids).cuda(), torch.tensor(langs).cuda(),
|
| 341 |
+
torch.tensor(self.spk2id[ANCHOR]).cuda(), prompt=P["lm"] if use_lm_prompt else None, seed=seed)
|
| 342 |
+
if len(sp) < 3:
|
| 343 |
+
sp = sp + [0] * 3
|
| 344 |
+
return token2wav(self.cosy, sp, *P["flow"])
|
| 345 |
+
|
| 346 |
+
def say(self, tagged_text, seed=0, prompt_k=0, stitch=True, pause_mode="clause", speed=1.10, return_info=False):
|
| 347 |
+
if pause_mode == "clause":
|
| 348 |
+
return self.say_clause(tagged_text, seed, prompt_k, speed, return_info)
|
| 349 |
+
w = self.say_model(tagged_text, seed, prompt_k, stitch)
|
| 350 |
+
w = time_stretch(w, self.sr, speed)
|
| 351 |
+
return (w, None) if return_info else w
|
| 352 |
+
|
| 353 |
+
def say_clause(self, tagged_text, seed=0, prompt_k=0, speed=1.10, return_info=False):
|
| 354 |
+
rng = np.random.default_rng(seed)
|
| 355 |
+
chunks = clause_chunks(tagged_text)
|
| 356 |
+
out, info, lead = [], [], 0.03
|
| 357 |
+
for i, (l, t, b) in enumerate(chunks):
|
| 358 |
+
w = self.one(f"<{l}>{t}</{l}>", seed + 1000 * i, prompt_k)
|
| 359 |
+
w = cap_internal(trim_tight(w, self.sr), self.sr, self.cap)
|
| 360 |
+
w = fade(time_stretch(w, self.sr, speed), self.sr)
|
| 361 |
+
if i == 0:
|
| 362 |
+
out.append(np.zeros(int(0.12 * self.sr), np.float32)) # same 0.12 s lead as model mode
|
| 363 |
+
out.append(w)
|
| 364 |
+
if b is not None:
|
| 365 |
+
g = GAPS[b] + rng.uniform(-JITTER[b], JITTER[b])
|
| 366 |
+
out.append(np.zeros(max(0, int((g - 2 * lead) * self.sr)), np.float32)) # 30 ms kept each side
|
| 367 |
+
else:
|
| 368 |
+
out.append(np.zeros(int(0.15 * self.sr), np.float32))
|
| 369 |
+
info.append(dict(lang=l, text=t, boundary=b))
|
| 370 |
+
w = np.concatenate(out)
|
| 371 |
+
return (w, info) if return_info else w
|
| 372 |
+
|
| 373 |
+
def say_model(self, tagged_text, seed=0, prompt_k=0, stitch=True):
|
| 374 |
+
sents = split_sentences(tagged_text) if stitch else [tagged_text]
|
| 375 |
+
ws = [cap_internal(trim_edges(self.one(s, seed + 1000 * i, prompt_k), self.sr), self.sr, self.cap)
|
| 376 |
+
for i, s in enumerate(sents)]
|
| 377 |
+
if len(ws) == 1:
|
| 378 |
+
return ws[0]
|
| 379 |
+
out = []
|
| 380 |
+
for i, w in enumerate(ws):
|
| 381 |
+
if i:
|
| 382 |
+
out.append(np.zeros(max(0, int((self.sent_pause - 0.30) * self.sr)), np.float32))
|
| 383 |
+
out.append(w)
|
| 384 |
+
return np.concatenate(out)
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
def main():
|
| 388 |
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 389 |
+
ap.add_argument("--text", required=True, help="tagged text, e.g. '<eng>Good morning,</eng> <pcm>how body?</pcm>'")
|
| 390 |
+
ap.add_argument("--seed", type=int, default=0)
|
| 391 |
+
ap.add_argument("--out", default="out.wav")
|
| 392 |
+
ap.add_argument("--prompt_clip", type=int, default=0, help="anchor prompt clip 0-3 (final system: 0)")
|
| 393 |
+
ap.add_argument("--model_dir", default=os.environ.get("COSYVOICE2_DIR", os.path.join(HERE, "pretrained", "CosyVoice2-0.5B")),
|
| 394 |
+
help="stock FunAudioLLM/CosyVoice2-0.5B snapshot")
|
| 395 |
+
ap.add_argument("--cosyvoice_dir", default=os.environ.get("COSYVOICE_REPO", os.path.join(HERE, "CosyVoice")),
|
| 396 |
+
help="clone of github.com/FunAudioLLM/CosyVoice (with submodules)")
|
| 397 |
+
ap.add_argument("--lm", default=os.path.join(HERE, "checkpoints", "p2_lm_B", "best.pt"))
|
| 398 |
+
ap.add_argument("--flow", default=os.path.join(HERE, "checkpoints", "p2_flow_sft", "best.pt"))
|
| 399 |
+
ap.add_argument("--prompts", default=os.path.join(HERE, "prompts", "prompts.json"))
|
| 400 |
+
ap.add_argument("--dtype", default="auto", choices=["auto", "bf16", "fp16"],
|
| 401 |
+
help="LM autocast dtype. Samples were made with bf16; T4 has no bf16, so auto picks fp16 there")
|
| 402 |
+
ap.add_argument("--no_stitch", action="store_true", help="generate multi-sentence text in one pass")
|
| 403 |
+
ap.add_argument("--pause_mode", default="clause", choices=["clause", "model"],
|
| 404 |
+
help="clause: clause-level chunks joined with anchor-matched gaps (default); model: Phase 2 final behaviour")
|
| 405 |
+
ap.add_argument("--speed", type=float, default=DEFAULT_SPEED,
|
| 406 |
+
help="pitch-preserving tempo factor applied to speech (gaps are kept as set). 1.0 = Phase 2 final pace")
|
| 407 |
+
a = ap.parse_args()
|
| 408 |
+
if not os.path.isdir(a.model_dir):
|
| 409 |
+
from huggingface_hub import snapshot_download
|
| 410 |
+
snapshot_download("FunAudioLLM/CosyVoice2-0.5B", local_dir=a.model_dir)
|
| 411 |
+
S = DynamicAccent(a.model_dir, a.cosyvoice_dir, a.lm, a.flow, a.prompts, a.dtype)
|
| 412 |
+
w = S.say(a.text, seed=a.seed, prompt_k=a.prompt_clip, stitch=not a.no_stitch, pause_mode=a.pause_mode, speed=a.speed)
|
| 413 |
+
sf.write(a.out, w, S.sr)
|
| 414 |
+
print(f"wrote {a.out} ({len(w) / S.sr:.2f} s, {S.sr} Hz)")
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
if __name__ == "__main__":
|
| 418 |
+
main()
|
prompts/clip0_waxal_yor_yor_1258.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e7e368595eb4c267f124164c7d8a83b03462d549c5efd235dd8ec6eb557dc418
|
| 3 |
+
size 285164
|
prompts/clip1_waxal_yor_yor_1205.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d8731a9032a1e84a88a2494b65a576794f732cf5c8555c1b94af1a5d370ad7c9
|
| 3 |
+
size 314924
|
prompts/clip2_waxal_yor_yor_1263.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0cf10737379673b123cee008f78006f4fa2fdabc779563d466f85f0772b9cec5
|
| 3 |
+
size 194924
|
prompts/clip3_waxal_yor_yor_1218.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5a0e00a437713a35429fe9423c3acf4ec668038e418fc7b85391bec53cb2368a
|
| 3 |
+
size 262124
|
prompts/prompts.json
ADDED
|
@@ -0,0 +1,602 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"anchor_speaker": "waxal_yor_4",
|
| 3 |
+
"source": "WAXAL (CC-BY-SA-4.0), anchor val clips, pause profile B (~/data3)",
|
| 4 |
+
"prompts": [
|
| 5 |
+
{
|
| 6 |
+
"index": 0,
|
| 7 |
+
"id": "waxal_yor_yor_1258",
|
| 8 |
+
"wav": "prompts/clip0_waxal_yor_yor_1258.wav",
|
| 9 |
+
"lang": "yor",
|
| 10 |
+
"text": "Púpọ̀ jùlọ àwọn olókìkí ní ìlú Èkó ń gbé ní erékùṣù náà.",
|
| 11 |
+
"tagged": "<yor>Púpọ̀ jùlọ àwọn olókìkí ní ìlú Èkó ń gbé ní erékùṣù náà.</yor>",
|
| 12 |
+
"dur": 5.94,
|
| 13 |
+
"s3_tokens": [
|
| 14 |
+
3677,
|
| 15 |
+
4137,
|
| 16 |
+
4218,
|
| 17 |
+
2166,
|
| 18 |
+
1452,
|
| 19 |
+
5830,
|
| 20 |
+
2175,
|
| 21 |
+
2112,
|
| 22 |
+
696,
|
| 23 |
+
5027,
|
| 24 |
+
4834,
|
| 25 |
+
2344,
|
| 26 |
+
1860,
|
| 27 |
+
5973,
|
| 28 |
+
1939,
|
| 29 |
+
1862,
|
| 30 |
+
1558,
|
| 31 |
+
3880,
|
| 32 |
+
316,
|
| 33 |
+
2621,
|
| 34 |
+
2864,
|
| 35 |
+
2864,
|
| 36 |
+
2891,
|
| 37 |
+
5051,
|
| 38 |
+
3412,
|
| 39 |
+
3888,
|
| 40 |
+
3888,
|
| 41 |
+
1458,
|
| 42 |
+
1701,
|
| 43 |
+
1950,
|
| 44 |
+
1973,
|
| 45 |
+
1946,
|
| 46 |
+
3890,
|
| 47 |
+
4133,
|
| 48 |
+
692,
|
| 49 |
+
3617,
|
| 50 |
+
4346,
|
| 51 |
+
3617,
|
| 52 |
+
5030,
|
| 53 |
+
4955,
|
| 54 |
+
2768,
|
| 55 |
+
4955,
|
| 56 |
+
2789,
|
| 57 |
+
2006,
|
| 58 |
+
2903,
|
| 59 |
+
3395,
|
| 60 |
+
3771,
|
| 61 |
+
6013,
|
| 62 |
+
3853,
|
| 63 |
+
5096,
|
| 64 |
+
5095,
|
| 65 |
+
1350,
|
| 66 |
+
978,
|
| 67 |
+
2298,
|
| 68 |
+
5586,
|
| 69 |
+
1122,
|
| 70 |
+
1788,
|
| 71 |
+
2598,
|
| 72 |
+
295,
|
| 73 |
+
566,
|
| 74 |
+
51,
|
| 75 |
+
3669,
|
| 76 |
+
3748,
|
| 77 |
+
2938,
|
| 78 |
+
4751,
|
| 79 |
+
4750,
|
| 80 |
+
4741,
|
| 81 |
+
2006,
|
| 82 |
+
1349,
|
| 83 |
+
1835,
|
| 84 |
+
1510,
|
| 85 |
+
1588,
|
| 86 |
+
2180,
|
| 87 |
+
5581,
|
| 88 |
+
4606,
|
| 89 |
+
2330,
|
| 90 |
+
1127,
|
| 91 |
+
573,
|
| 92 |
+
660,
|
| 93 |
+
1364,
|
| 94 |
+
617,
|
| 95 |
+
2290,
|
| 96 |
+
5209,
|
| 97 |
+
2077,
|
| 98 |
+
2076,
|
| 99 |
+
2184,
|
| 100 |
+
967,
|
| 101 |
+
4616,
|
| 102 |
+
4607,
|
| 103 |
+
4526,
|
| 104 |
+
4769,
|
| 105 |
+
2591,
|
| 106 |
+
4526,
|
| 107 |
+
4579,
|
| 108 |
+
2446,
|
| 109 |
+
4137,
|
| 110 |
+
4137,
|
| 111 |
+
4137,
|
| 112 |
+
1959,
|
| 113 |
+
3670,
|
| 114 |
+
3671,
|
| 115 |
+
3751,
|
| 116 |
+
1479,
|
| 117 |
+
4769,
|
| 118 |
+
5021,
|
| 119 |
+
4994,
|
| 120 |
+
4751,
|
| 121 |
+
5561,
|
| 122 |
+
5561,
|
| 123 |
+
5236,
|
| 124 |
+
3255,
|
| 125 |
+
4218,
|
| 126 |
+
4218,
|
| 127 |
+
4218,
|
| 128 |
+
4137,
|
| 129 |
+
5352,
|
| 130 |
+
6081,
|
| 131 |
+
5379,
|
| 132 |
+
1006,
|
| 133 |
+
1034,
|
| 134 |
+
4679,
|
| 135 |
+
2960,
|
| 136 |
+
2411,
|
| 137 |
+
1207,
|
| 138 |
+
860,
|
| 139 |
+
4760,
|
| 140 |
+
233,
|
| 141 |
+
1407,
|
| 142 |
+
654,
|
| 143 |
+
3567,
|
| 144 |
+
5826,
|
| 145 |
+
6310,
|
| 146 |
+
5481,
|
| 147 |
+
5563,
|
| 148 |
+
3348,
|
| 149 |
+
6556,
|
| 150 |
+
2102,
|
| 151 |
+
886,
|
| 152 |
+
1563,
|
| 153 |
+
1479,
|
| 154 |
+
2300,
|
| 155 |
+
4547,
|
| 156 |
+
4546,
|
| 157 |
+
2522,
|
| 158 |
+
263,
|
| 159 |
+
263,
|
| 160 |
+
1225,
|
| 161 |
+
1953,
|
| 162 |
+
1947
|
| 163 |
+
]
|
| 164 |
+
},
|
| 165 |
+
{
|
| 166 |
+
"index": 1,
|
| 167 |
+
"id": "waxal_yor_yor_1205",
|
| 168 |
+
"wav": "prompts/clip1_waxal_yor_yor_1205.wav",
|
| 169 |
+
"lang": "yor",
|
| 170 |
+
"text": "Mejìlá ní ìsọdipúpọ̀ nípasẹ̀ mẹ́rin dọ́gba mejì-mejìlá.",
|
| 171 |
+
"tagged": "<yor>Mejìlá ní ìsọdipúpọ̀ nípasẹ̀ mẹ́rin dọ́gba mejì-mejìlá.</yor>",
|
| 172 |
+
"dur": 6.56,
|
| 173 |
+
"s3_tokens": [
|
| 174 |
+
3677,
|
| 175 |
+
1968,
|
| 176 |
+
1804,
|
| 177 |
+
1884,
|
| 178 |
+
1883,
|
| 179 |
+
2312,
|
| 180 |
+
4579,
|
| 181 |
+
3021,
|
| 182 |
+
1041,
|
| 183 |
+
5250,
|
| 184 |
+
4858,
|
| 185 |
+
80,
|
| 186 |
+
3799,
|
| 187 |
+
1532,
|
| 188 |
+
731,
|
| 189 |
+
6158,
|
| 190 |
+
4486,
|
| 191 |
+
3669,
|
| 192 |
+
5938,
|
| 193 |
+
3748,
|
| 194 |
+
4742,
|
| 195 |
+
4751,
|
| 196 |
+
4751,
|
| 197 |
+
4751,
|
| 198 |
+
4714,
|
| 199 |
+
5605,
|
| 200 |
+
1951,
|
| 201 |
+
2031,
|
| 202 |
+
1978,
|
| 203 |
+
3481,
|
| 204 |
+
1268,
|
| 205 |
+
3721,
|
| 206 |
+
4672,
|
| 207 |
+
4999,
|
| 208 |
+
4752,
|
| 209 |
+
586,
|
| 210 |
+
2894,
|
| 211 |
+
2413,
|
| 212 |
+
1608,
|
| 213 |
+
2347,
|
| 214 |
+
1940,
|
| 215 |
+
1410,
|
| 216 |
+
2139,
|
| 217 |
+
6558,
|
| 218 |
+
5829,
|
| 219 |
+
2148,
|
| 220 |
+
2112,
|
| 221 |
+
1410,
|
| 222 |
+
5099,
|
| 223 |
+
5081,
|
| 224 |
+
2894,
|
| 225 |
+
707,
|
| 226 |
+
518,
|
| 227 |
+
1308,
|
| 228 |
+
1950,
|
| 229 |
+
4131,
|
| 230 |
+
3645,
|
| 231 |
+
1707,
|
| 232 |
+
1959,
|
| 233 |
+
1987,
|
| 234 |
+
1753,
|
| 235 |
+
1484,
|
| 236 |
+
5857,
|
| 237 |
+
1482,
|
| 238 |
+
4571,
|
| 239 |
+
3637,
|
| 240 |
+
4299,
|
| 241 |
+
1383,
|
| 242 |
+
3574,
|
| 243 |
+
4214,
|
| 244 |
+
3647,
|
| 245 |
+
56,
|
| 246 |
+
5401,
|
| 247 |
+
6374,
|
| 248 |
+
1035,
|
| 249 |
+
32,
|
| 250 |
+
2303,
|
| 251 |
+
2303,
|
| 252 |
+
227,
|
| 253 |
+
2127,
|
| 254 |
+
6502,
|
| 255 |
+
1882,
|
| 256 |
+
4742,
|
| 257 |
+
4490,
|
| 258 |
+
4579,
|
| 259 |
+
4550,
|
| 260 |
+
3016,
|
| 261 |
+
1724,
|
| 262 |
+
377,
|
| 263 |
+
377,
|
| 264 |
+
2456,
|
| 265 |
+
2456,
|
| 266 |
+
25,
|
| 267 |
+
3903,
|
| 268 |
+
3975,
|
| 269 |
+
4137,
|
| 270 |
+
4218,
|
| 271 |
+
4137,
|
| 272 |
+
4137,
|
| 273 |
+
6324,
|
| 274 |
+
6324,
|
| 275 |
+
6324,
|
| 276 |
+
1968,
|
| 277 |
+
3913,
|
| 278 |
+
1727,
|
| 279 |
+
1807,
|
| 280 |
+
1110,
|
| 281 |
+
2666,
|
| 282 |
+
5078,
|
| 283 |
+
5078,
|
| 284 |
+
1125,
|
| 285 |
+
2157,
|
| 286 |
+
1452,
|
| 287 |
+
389,
|
| 288 |
+
5186,
|
| 289 |
+
2999,
|
| 290 |
+
2513,
|
| 291 |
+
2270,
|
| 292 |
+
3242,
|
| 293 |
+
5429,
|
| 294 |
+
5105,
|
| 295 |
+
5103,
|
| 296 |
+
2922,
|
| 297 |
+
1464,
|
| 298 |
+
1950,
|
| 299 |
+
4218,
|
| 300 |
+
1987,
|
| 301 |
+
1987,
|
| 302 |
+
1987,
|
| 303 |
+
1804,
|
| 304 |
+
6256,
|
| 305 |
+
1639,
|
| 306 |
+
2573,
|
| 307 |
+
4849,
|
| 308 |
+
4696,
|
| 309 |
+
1509,
|
| 310 |
+
1779,
|
| 311 |
+
4440,
|
| 312 |
+
5911,
|
| 313 |
+
4857,
|
| 314 |
+
4777,
|
| 315 |
+
323,
|
| 316 |
+
323,
|
| 317 |
+
2445,
|
| 318 |
+
978,
|
| 319 |
+
1950,
|
| 320 |
+
1968,
|
| 321 |
+
1996,
|
| 322 |
+
1885,
|
| 323 |
+
1882,
|
| 324 |
+
4760,
|
| 325 |
+
4696,
|
| 326 |
+
3966,
|
| 327 |
+
5658,
|
| 328 |
+
4431,
|
| 329 |
+
5506,
|
| 330 |
+
78,
|
| 331 |
+
5986,
|
| 332 |
+
74,
|
| 333 |
+
1460,
|
| 334 |
+
1460,
|
| 335 |
+
982,
|
| 336 |
+
1701,
|
| 337 |
+
1947
|
| 338 |
+
]
|
| 339 |
+
},
|
| 340 |
+
{
|
| 341 |
+
"index": 2,
|
| 342 |
+
"id": "waxal_yor_yor_1263",
|
| 343 |
+
"wav": "prompts/clip2_waxal_yor_yor_1263.wav",
|
| 344 |
+
"lang": "yor",
|
| 345 |
+
"text": "Ọmọ ọdún méji ni Nóla ní oṣù kẹwàá.",
|
| 346 |
+
"tagged": "<yor>Ọmọ ọdún méji ni Nóla ní oṣù kẹwàá.</yor>",
|
| 347 |
+
"dur": 4.06,
|
| 348 |
+
"s3_tokens": [
|
| 349 |
+
3677,
|
| 350 |
+
4218,
|
| 351 |
+
2057,
|
| 352 |
+
4217,
|
| 353 |
+
2762,
|
| 354 |
+
2127,
|
| 355 |
+
4314,
|
| 356 |
+
2852,
|
| 357 |
+
2771,
|
| 358 |
+
572,
|
| 359 |
+
4835,
|
| 360 |
+
963,
|
| 361 |
+
1914,
|
| 362 |
+
954,
|
| 363 |
+
6554,
|
| 364 |
+
5041,
|
| 365 |
+
6502,
|
| 366 |
+
4068,
|
| 367 |
+
2420,
|
| 368 |
+
4858,
|
| 369 |
+
2265,
|
| 370 |
+
1995,
|
| 371 |
+
2022,
|
| 372 |
+
4440,
|
| 373 |
+
5262,
|
| 374 |
+
5506,
|
| 375 |
+
323,
|
| 376 |
+
753,
|
| 377 |
+
1563,
|
| 378 |
+
750,
|
| 379 |
+
323,
|
| 380 |
+
2267,
|
| 381 |
+
2591,
|
| 382 |
+
2321,
|
| 383 |
+
2240,
|
| 384 |
+
4670,
|
| 385 |
+
2455,
|
| 386 |
+
4389,
|
| 387 |
+
2922,
|
| 388 |
+
3648,
|
| 389 |
+
4137,
|
| 390 |
+
1950,
|
| 391 |
+
1987,
|
| 392 |
+
1753,
|
| 393 |
+
1481,
|
| 394 |
+
1480,
|
| 395 |
+
3745,
|
| 396 |
+
6012,
|
| 397 |
+
1073,
|
| 398 |
+
4949,
|
| 399 |
+
5054,
|
| 400 |
+
2297,
|
| 401 |
+
5905,
|
| 402 |
+
5986,
|
| 403 |
+
2216,
|
| 404 |
+
5915,
|
| 405 |
+
5186,
|
| 406 |
+
5186,
|
| 407 |
+
4469,
|
| 408 |
+
2916,
|
| 409 |
+
3645,
|
| 410 |
+
1458,
|
| 411 |
+
1464,
|
| 412 |
+
4218,
|
| 413 |
+
4137,
|
| 414 |
+
1725,
|
| 415 |
+
3670,
|
| 416 |
+
3831,
|
| 417 |
+
2941,
|
| 418 |
+
5102,
|
| 419 |
+
5561,
|
| 420 |
+
6074,
|
| 421 |
+
5234,
|
| 422 |
+
2012,
|
| 423 |
+
2006,
|
| 424 |
+
2096,
|
| 425 |
+
2912,
|
| 426 |
+
5828,
|
| 427 |
+
2990,
|
| 428 |
+
6051,
|
| 429 |
+
6054,
|
| 430 |
+
1588,
|
| 431 |
+
2072,
|
| 432 |
+
2139,
|
| 433 |
+
1734,
|
| 434 |
+
2355,
|
| 435 |
+
2528,
|
| 436 |
+
4571,
|
| 437 |
+
2384,
|
| 438 |
+
881,
|
| 439 |
+
644,
|
| 440 |
+
1456,
|
| 441 |
+
2185,
|
| 442 |
+
707,
|
| 443 |
+
1946,
|
| 444 |
+
812,
|
| 445 |
+
3242,
|
| 446 |
+
5914,
|
| 447 |
+
4545,
|
| 448 |
+
1711,
|
| 449 |
+
2031,
|
| 450 |
+
6405
|
| 451 |
+
]
|
| 452 |
+
},
|
| 453 |
+
{
|
| 454 |
+
"index": 3,
|
| 455 |
+
"id": "waxal_yor_yor_1218",
|
| 456 |
+
"wav": "prompts/clip3_waxal_yor_yor_1218.wav",
|
| 457 |
+
"lang": "yor",
|
| 458 |
+
"text": "Ìdajì àti ọ̀kan kẹ́fà dọ́gba méji kẹ́ta.",
|
| 459 |
+
"tagged": "<yor>Ìdajì àti ọ̀kan kẹ́fà dọ́gba méji kẹ́ta.</yor>",
|
| 460 |
+
"dur": 5.46,
|
| 461 |
+
"s3_tokens": [
|
| 462 |
+
3707,
|
| 463 |
+
2031,
|
| 464 |
+
566,
|
| 465 |
+
566,
|
| 466 |
+
4778,
|
| 467 |
+
861,
|
| 468 |
+
1770,
|
| 469 |
+
68,
|
| 470 |
+
3083,
|
| 471 |
+
5996,
|
| 472 |
+
4570,
|
| 473 |
+
777,
|
| 474 |
+
3939,
|
| 475 |
+
4683,
|
| 476 |
+
5169,
|
| 477 |
+
5911,
|
| 478 |
+
3239,
|
| 479 |
+
1052,
|
| 480 |
+
286,
|
| 481 |
+
259,
|
| 482 |
+
5109,
|
| 483 |
+
2922,
|
| 484 |
+
2922,
|
| 485 |
+
3894,
|
| 486 |
+
3651,
|
| 487 |
+
2031,
|
| 488 |
+
1976,
|
| 489 |
+
3161,
|
| 490 |
+
3890,
|
| 491 |
+
3890,
|
| 492 |
+
2219,
|
| 493 |
+
1527,
|
| 494 |
+
2085,
|
| 495 |
+
6380,
|
| 496 |
+
6131,
|
| 497 |
+
5401,
|
| 498 |
+
1038,
|
| 499 |
+
2267,
|
| 500 |
+
4697,
|
| 501 |
+
4697,
|
| 502 |
+
4778,
|
| 503 |
+
4697,
|
| 504 |
+
4778,
|
| 505 |
+
5507,
|
| 506 |
+
5507,
|
| 507 |
+
5507,
|
| 508 |
+
3050,
|
| 509 |
+
3255,
|
| 510 |
+
3894,
|
| 511 |
+
4218,
|
| 512 |
+
4137,
|
| 513 |
+
1979,
|
| 514 |
+
1304,
|
| 515 |
+
1385,
|
| 516 |
+
2897,
|
| 517 |
+
601,
|
| 518 |
+
4299,
|
| 519 |
+
2031,
|
| 520 |
+
2754,
|
| 521 |
+
4869,
|
| 522 |
+
5030,
|
| 523 |
+
5030,
|
| 524 |
+
665,
|
| 525 |
+
497,
|
| 526 |
+
1723,
|
| 527 |
+
2112,
|
| 528 |
+
762,
|
| 529 |
+
2271,
|
| 530 |
+
2213,
|
| 531 |
+
4523,
|
| 532 |
+
4853,
|
| 533 |
+
6372,
|
| 534 |
+
6534,
|
| 535 |
+
6534,
|
| 536 |
+
688,
|
| 537 |
+
650,
|
| 538 |
+
1055,
|
| 539 |
+
1784,
|
| 540 |
+
3161,
|
| 541 |
+
2917,
|
| 542 |
+
2916,
|
| 543 |
+
1458,
|
| 544 |
+
1458,
|
| 545 |
+
1461,
|
| 546 |
+
2031,
|
| 547 |
+
1807,
|
| 548 |
+
1726,
|
| 549 |
+
1806,
|
| 550 |
+
1806,
|
| 551 |
+
873,
|
| 552 |
+
2657,
|
| 553 |
+
5081,
|
| 554 |
+
4852,
|
| 555 |
+
1941,
|
| 556 |
+
1888,
|
| 557 |
+
2181,
|
| 558 |
+
218,
|
| 559 |
+
3242,
|
| 560 |
+
2999,
|
| 561 |
+
2999,
|
| 562 |
+
3242,
|
| 563 |
+
3170,
|
| 564 |
+
2926,
|
| 565 |
+
3645,
|
| 566 |
+
3645,
|
| 567 |
+
1701,
|
| 568 |
+
2031,
|
| 569 |
+
1959,
|
| 570 |
+
1727,
|
| 571 |
+
1727,
|
| 572 |
+
3994,
|
| 573 |
+
6256,
|
| 574 |
+
1883,
|
| 575 |
+
4841,
|
| 576 |
+
4848,
|
| 577 |
+
2346,
|
| 578 |
+
1779,
|
| 579 |
+
1284,
|
| 580 |
+
4509,
|
| 581 |
+
5181,
|
| 582 |
+
1943,
|
| 583 |
+
1923,
|
| 584 |
+
1707,
|
| 585 |
+
168,
|
| 586 |
+
4751,
|
| 587 |
+
2330,
|
| 588 |
+
62,
|
| 589 |
+
67,
|
| 590 |
+
1707,
|
| 591 |
+
2058,
|
| 592 |
+
54,
|
| 593 |
+
2189,
|
| 594 |
+
3242,
|
| 595 |
+
2927,
|
| 596 |
+
2925,
|
| 597 |
+
1944,
|
| 598 |
+
4218
|
| 599 |
+
]
|
| 600 |
+
}
|
| 601 |
+
]
|
| 602 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python 3.10. Install torch/torchaudio for your CUDA first (Colab already has them), then:
|
| 2 |
+
# pip install -r requirements.txt
|
| 3 |
+
# and clone CosyVoice (see README). Versions below are the ones the samples were rendered with
|
| 4 |
+
# (pins loosened where Colab's preinstalled versions also work).
|
| 5 |
+
torch>=2.3
|
| 6 |
+
torchaudio>=2.3
|
| 7 |
+
transformers==4.51.3
|
| 8 |
+
huggingface_hub>=0.23
|
| 9 |
+
numpy==1.26.4
|
| 10 |
+
soundfile>=0.12
|
| 11 |
+
librosa>=0.10
|
| 12 |
+
onnxruntime-gpu>=1.18
|
| 13 |
+
HyperPyYAML>=1.2.2
|
| 14 |
+
modelscope>=1.20
|
| 15 |
+
conformer==0.3.2
|
| 16 |
+
diffusers==0.29.0
|
| 17 |
+
lightning>=2.2.4
|
| 18 |
+
omegaconf>=2.3.0
|
| 19 |
+
hydra-core==1.3.2
|
| 20 |
+
inflect>=7.3
|
| 21 |
+
x-transformers>=2.11
|
| 22 |
+
openai-whisper>=20231117
|
| 23 |
+
einops>=0.8
|
| 24 |
+
pyworld>=0.3.4
|
| 25 |
+
pyarrow>=18
|
| 26 |
+
wget==3.2
|
| 27 |
+
gdown>=5.1
|
| 28 |
+
rich>=13.7
|
| 29 |
+
matplotlib>=3.7
|
| 30 |
+
protobuf>=4.25
|
| 31 |
+
pydantic>=2.7
|
| 32 |
+
wetext==0.0.4
|
| 33 |
+
audiotsm==0.1.2 # WSOLA time-stretch for --speed (falls back to librosa if missing)
|
samples/pcm_eng_dynamic_accent_seed0.mp3
ADDED
|
Binary file (62.6 kB). View file
|
|
|
samples/pcm_eng_dynamic_accent_seed1.mp3
ADDED
|
Binary file (51.1 kB). View file
|
|
|
samples/pcm_eng_dynamic_accent_seed2.mp3
ADDED
|
Binary file (56.5 kB). View file
|
|
|
samples/prosody_v2/custom_pcm_eng.mp3
ADDED
|
Binary file (63.4 kB). View file
|
|
|
samples/prosody_v2/ibo_000.mp3
ADDED
|
Binary file (84.9 kB). View file
|
|
|
samples/prosody_v2/para_hau_eng_pcm.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ce6ab62b8647c8325ffb8ba499ea2b9ec3db623aa3e68aaafe5667225751abcb
|
| 3 |
+
size 289964
|
samples/prosody_v2/para_ibo_pcm_eng.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fa85fb1f58a0778a7cc0027b4d9919ed36537f518054b7f9f8b3f9a9a5cff232
|
| 3 |
+
size 291116
|
samples/prosody_v2/para_yor_eng.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e2a0f0280ac149d9691975ba6f1e8c763c9d91a365632202d5b1a42b675e36c3
|
| 3 |
+
size 241196
|
samples/prosody_v2/yor_000.mp3
ADDED
|
Binary file (32.7 kB). View file
|
|
|