Spaces:
Running on Zero
Running on Zero
FireRedTTS3 demo: cloning, voice design, speech editing
Browse files- .gitattributes +2 -0
- LICENSE +201 -0
- README.md +35 -5
- app.py +576 -0
- examples/en_prompt.wav +3 -0
- examples/zh_prompt.wav +3 -0
- fireredtts3/campp/DTDNN.py +123 -0
- fireredtts3/campp/campp.py +58 -0
- fireredtts3/campp/layers.py +253 -0
- fireredtts3/core.py +555 -0
- fireredtts3/llm/dit.py +194 -0
- fireredtts3/llm/fireredtts3_base.py +346 -0
- fireredtts3/llm/fireredtts3_instruct.py +508 -0
- fireredtts3/llm/modules.py +185 -0
- fireredtts3/llm/patch_encoder.py +65 -0
- fireredtts3/llm/rotary_embedding.py +75 -0
- fireredtts3/redae/redae.py +496 -0
- fireredtts3/utils/chatml.py +118 -0
- fireredtts3/utils/llm_tn/templates/ar-SA.json +13 -0
- fireredtts3/utils/llm_tn/templates/cs-CZ.json +13 -0
- fireredtts3/utils/llm_tn/templates/de-DE.json +21 -0
- fireredtts3/utils/llm_tn/templates/el-GR.json +13 -0
- fireredtts3/utils/llm_tn/templates/en-US.json +20 -0
- fireredtts3/utils/llm_tn/templates/es-MX.json +23 -0
- fireredtts3/utils/llm_tn/templates/fi-FI.json +13 -0
- fireredtts3/utils/llm_tn/templates/fr-FR.json +23 -0
- fireredtts3/utils/llm_tn/templates/hi-IN.json +13 -0
- fireredtts3/utils/llm_tn/templates/id-ID.json +13 -0
- fireredtts3/utils/llm_tn/templates/it-IT.json +23 -0
- fireredtts3/utils/llm_tn/templates/ja-JP.json +25 -0
- fireredtts3/utils/llm_tn/templates/ko-KR.json +13 -0
- fireredtts3/utils/llm_tn/templates/lt-LT.json +29 -0
- fireredtts3/utils/llm_tn/templates/nl-NL.json +13 -0
- fireredtts3/utils/llm_tn/templates/pl-PL.json +13 -0
- fireredtts3/utils/llm_tn/templates/pt-BR.json +13 -0
- fireredtts3/utils/llm_tn/templates/ro-RO.json +13 -0
- fireredtts3/utils/llm_tn/templates/ru-RU.json +13 -0
- fireredtts3/utils/llm_tn/templates/th-TH.json +13 -0
- fireredtts3/utils/llm_tn/templates/tr-TR.json +13 -0
- fireredtts3/utils/llm_tn/templates/uk-UA.json +13 -0
- fireredtts3/utils/llm_tn/templates/vi-VN.json +13 -0
- fireredtts3/utils/llm_tn/templates/zh-CN.json +25 -0
- fireredtts3/utils/llm_tn/text_normalizer.py +428 -0
- fireredtts3/utils/text_normalize.py +600 -0
- fireredtts3/utils/text_tokenizer.py +252 -0
- fireredtts3/utils/utils.py +9 -0
- requirements.txt +11 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,5 @@ 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 |
+
examples/en_prompt.wav filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
examples/zh_prompt.wav filter=lfs diff=lfs merge=lfs -text
|
LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Apache License
|
| 2 |
+
Version 2.0, January 2004
|
| 3 |
+
http://www.apache.org/licenses/
|
| 4 |
+
|
| 5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 6 |
+
|
| 7 |
+
1. Definitions.
|
| 8 |
+
|
| 9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 11 |
+
|
| 12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 13 |
+
the copyright owner that is granting the License.
|
| 14 |
+
|
| 15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 16 |
+
other entities that control, are controlled by, or are under common
|
| 17 |
+
control with that entity. For the purposes of this definition,
|
| 18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 19 |
+
direction or management of such entity, whether by contract or
|
| 20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 22 |
+
|
| 23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 24 |
+
exercising permissions granted by this License.
|
| 25 |
+
|
| 26 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 27 |
+
including but not limited to software source code, documentation
|
| 28 |
+
source, and configuration files.
|
| 29 |
+
|
| 30 |
+
"Object" form shall mean any form resulting from mechanical
|
| 31 |
+
transformation or translation of a Source form, including but
|
| 32 |
+
not limited to compiled object code, generated documentation,
|
| 33 |
+
and conversions to other media types.
|
| 34 |
+
|
| 35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 36 |
+
Object form, made available under the License, as indicated by a
|
| 37 |
+
copyright notice that is included in or attached to the work
|
| 38 |
+
(an example is provided in the Appendix below).
|
| 39 |
+
|
| 40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 41 |
+
form, that is based on (or derived from) the Work and for which the
|
| 42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 44 |
+
of this License, Derivative Works shall not include works that remain
|
| 45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 46 |
+
the Work and Derivative Works thereof.
|
| 47 |
+
|
| 48 |
+
"Contribution" shall mean any work of authorship, including
|
| 49 |
+
the original version of the Work and any modifications or additions
|
| 50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 54 |
+
means any form of electronic, verbal, or written communication sent
|
| 55 |
+
to the Licensor or its representatives, including but not limited to
|
| 56 |
+
communication on electronic mailing lists, source code control systems,
|
| 57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 59 |
+
excluding communication that is conspicuously marked or otherwise
|
| 60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 61 |
+
|
| 62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 64 |
+
subsequently incorporated within the Work.
|
| 65 |
+
|
| 66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 71 |
+
Work and such Derivative Works in Source or Object form.
|
| 72 |
+
|
| 73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 76 |
+
(except as stated in this section) patent license to make, have made,
|
| 77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 78 |
+
where such license applies only to those patent claims licensable
|
| 79 |
+
by such Contributor that are necessarily infringed by their
|
| 80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 82 |
+
institute patent litigation against any entity (including a
|
| 83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 84 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 85 |
+
or contributory patent infringement, then any patent licenses
|
| 86 |
+
granted to You under this License for that Work shall terminate
|
| 87 |
+
as of the date such litigation is filed.
|
| 88 |
+
|
| 89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 90 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 91 |
+
modifications, and in Source or Object form, provided that You
|
| 92 |
+
meet the following conditions:
|
| 93 |
+
|
| 94 |
+
(a) You must give any other recipients of the Work or
|
| 95 |
+
Derivative Works a copy of this License; and
|
| 96 |
+
|
| 97 |
+
(b) You must cause any modified files to carry prominent notices
|
| 98 |
+
stating that You changed the files; and
|
| 99 |
+
|
| 100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 101 |
+
that You distribute, all copyright, patent, trademark, and
|
| 102 |
+
attribution notices from the Source form of the Work,
|
| 103 |
+
excluding those notices that do not pertain to any part of
|
| 104 |
+
the Derivative Works; and
|
| 105 |
+
|
| 106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 107 |
+
distribution, then any Derivative Works that You distribute must
|
| 108 |
+
include a readable copy of the attribution notices contained
|
| 109 |
+
within such NOTICE file, excluding those notices that do not
|
| 110 |
+
pertain to any part of the Derivative Works, in at least one
|
| 111 |
+
of the following places: within a NOTICE text file distributed
|
| 112 |
+
as part of the Derivative Works; within the Source form or
|
| 113 |
+
documentation, if provided along with the Derivative Works; or,
|
| 114 |
+
within a display generated by the Derivative Works, if and
|
| 115 |
+
wherever such third-party notices normally appear. The contents
|
| 116 |
+
of the NOTICE file are for informational purposes only and
|
| 117 |
+
do not modify the License. You may add Your own attribution
|
| 118 |
+
notices within Derivative Works that You distribute, alongside
|
| 119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 120 |
+
that such additional attribution notices cannot be construed
|
| 121 |
+
as modifying the License.
|
| 122 |
+
|
| 123 |
+
You may add Your own copyright statement to Your modifications and
|
| 124 |
+
may provide additional or different license terms and conditions
|
| 125 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 126 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 127 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 128 |
+
the conditions stated in this License.
|
| 129 |
+
|
| 130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 132 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 133 |
+
this License, without any additional terms or conditions.
|
| 134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 135 |
+
the terms of any separate license agreement you may have executed
|
| 136 |
+
with Licensor regarding such Contributions.
|
| 137 |
+
|
| 138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 140 |
+
except as required for reasonable and customary use in describing the
|
| 141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 142 |
+
|
| 143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 144 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 147 |
+
implied, including, without limitation, any warranties or conditions
|
| 148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 150 |
+
appropriateness of using or redistributing the Work and assume any
|
| 151 |
+
risks associated with Your exercise of permissions under this License.
|
| 152 |
+
|
| 153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 154 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 155 |
+
unless required by applicable law (such as deliberate and grossly
|
| 156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 157 |
+
liable to You for damages, including any direct, indirect, special,
|
| 158 |
+
incidental, or consequential damages of any character arising as a
|
| 159 |
+
result of this License or out of the use or inability to use the
|
| 160 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 161 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 162 |
+
other commercial damages or losses), even if such Contributor
|
| 163 |
+
has been advised of the possibility of such damages.
|
| 164 |
+
|
| 165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 168 |
+
or other liability obligations and/or rights consistent with this
|
| 169 |
+
License. However, in accepting such obligations, You may act only
|
| 170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 171 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 172 |
+
defend, and hold each Contributor harmless for any liability
|
| 173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 174 |
+
of your accepting any such warranty or additional liability.
|
| 175 |
+
|
| 176 |
+
END OF TERMS AND CONDITIONS
|
| 177 |
+
|
| 178 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 179 |
+
|
| 180 |
+
To apply the Apache License to your work, attach the following
|
| 181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 182 |
+
replaced with your own identifying information. (Don't include
|
| 183 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 184 |
+
comment syntax for the file format. We also recommend that a
|
| 185 |
+
file or class name and description of purpose be included on the
|
| 186 |
+
same "printed page" as the copyright notice for easier
|
| 187 |
+
identification within third-party archives.
|
| 188 |
+
|
| 189 |
+
Copyright [yyyy] [name of copyright owner]
|
| 190 |
+
|
| 191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 192 |
+
you may not use this file except in compliance with the License.
|
| 193 |
+
You may obtain a copy of the License at
|
| 194 |
+
|
| 195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 196 |
+
|
| 197 |
+
Unless required by applicable law or agreed to in writing, software
|
| 198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 200 |
+
See the License for the specific language governing permissions and
|
| 201 |
+
limitations under the License.
|
README.md
CHANGED
|
@@ -1,13 +1,43 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
emoji: 🔥
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.24.0
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
|
|
|
|
|
|
| 10 |
pinned: false
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: FireRedTTS3
|
| 3 |
emoji: 🔥
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: pink
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.24.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
+
python_version: "3.12"
|
| 10 |
+
startup_duration_timeout: 1h
|
| 11 |
pinned: false
|
| 12 |
+
license: apache-2.0
|
| 13 |
+
short_description: Voice cloning, voice design and speech editing
|
| 14 |
+
models:
|
| 15 |
+
- FireRedTeam/FireRedTTS3
|
| 16 |
---
|
| 17 |
|
| 18 |
+
# 🔥 FireRedTTS3
|
| 19 |
+
|
| 20 |
+
Interactive demo of [FireRedTeam/FireRedTTS3](https://huggingface.co/FireRedTeam/FireRedTTS3),
|
| 21 |
+
a unified speech generation and editing model (Qwen3-1.7B backbone + flow-matching
|
| 22 |
+
head over the continuous RedAE audio autoencoder, 24 kHz).
|
| 23 |
+
|
| 24 |
+
Three tabs:
|
| 25 |
+
|
| 26 |
+
| Tab | Checkpoint | What it does |
|
| 27 |
+
| --- | --- | --- |
|
| 28 |
+
| 🎙️ Voice Cloning | `fireredtts3_base` | Zero-shot cloning from a short reference clip — 24 languages and 21 Chinese dialects |
|
| 29 |
+
| 🎨 Voice Design | `fireredtts3_instruct` | Creates a voice from a natural-language description, no reference audio |
|
| 30 |
+
| ✂️ Speech Editing | `fireredtts3_instruct` | Semantic edits (insert / delete / substitute words) and acoustic edits (speed, pitch, volume) |
|
| 31 |
+
|
| 32 |
+
Text normalization runs locally via `wetext` (Chinese / English); automatic language
|
| 33 |
+
routing uses fastText `lid.176`. The optional LLM-based normalizer from the upstream
|
| 34 |
+
repo is disabled here (it requires external API credentials).
|
| 35 |
+
|
| 36 |
+
## Credits
|
| 37 |
+
|
| 38 |
+
- Model and inference code: [FireRedTeam/FireRedTTS3](https://github.com/FireRedTeam/FireRedTTS3), Apache-2.0.
|
| 39 |
+
- `examples/en_prompt.wav`: from [OpenBMB/VoxCPM](https://github.com/OpenBMB/VoxCPM) (`examples/example.wav`), Apache-2.0.
|
| 40 |
+
- `examples/zh_prompt.wav`: from [FireRedTeam/FireRedTTS2](https://github.com/FireRedTeam/FireRedTTS2) (`examples/chat_prompt/zh/S2.flac`), Apache-2.0.
|
| 41 |
+
|
| 42 |
+
Voice cloning is provided for academic research purposes only — do not use it for
|
| 43 |
+
impersonation or any illegal activity.
|
app.py
ADDED
|
@@ -0,0 +1,576 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FireRedTTS3 — unified speech generation & editing demo (ZeroGPU).
|
| 2 |
+
|
| 3 |
+
Three capabilities of https://huggingface.co/FireRedTeam/FireRedTTS3 :
|
| 4 |
+
* Zero-shot voice cloning (FireRedTTS3-Base, 24 languages + 21 ZH dialects)
|
| 5 |
+
* Voice design (FireRedTTS3-Instruct, natural-language timbre prompt)
|
| 6 |
+
* Speech editing (FireRedTTS3-Instruct, semantic + acoustic)
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import functools
|
| 10 |
+
import os
|
| 11 |
+
import urllib.request
|
| 12 |
+
|
| 13 |
+
import spaces # noqa: F401 (must precede torch / CUDA imports)
|
| 14 |
+
|
| 15 |
+
import numpy as np
|
| 16 |
+
import soundfile as sf
|
| 17 |
+
import torch
|
| 18 |
+
import gradio as gr
|
| 19 |
+
from huggingface_hub import snapshot_download
|
| 20 |
+
|
| 21 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 22 |
+
|
| 23 |
+
# --------------------------------------------------------------------------- #
|
| 24 |
+
# Text front-end assets
|
| 25 |
+
# --------------------------------------------------------------------------- #
|
| 26 |
+
# fastText lid.176 powers automatic language routing (see upstream README).
|
| 27 |
+
_LID_URL = "https://dl.fbaipublicfiles.com/fasttext/supervised-models/lid.176.ftz"
|
| 28 |
+
_LID_PATH = os.path.join(HERE, "fireredtts3", "utils", "llm_tn", "models", "lid.176.ftz")
|
| 29 |
+
os.makedirs(os.path.dirname(_LID_PATH), exist_ok=True)
|
| 30 |
+
if not os.path.exists(_LID_PATH):
|
| 31 |
+
try:
|
| 32 |
+
urllib.request.urlretrieve(_LID_URL, _LID_PATH)
|
| 33 |
+
print(f"[INFO] fastText lid.176 downloaded to {_LID_PATH}", flush=True)
|
| 34 |
+
except Exception as exc: # pragma: no cover
|
| 35 |
+
print(f"[WARN] Could not fetch fastText lid.176: {exc}", flush=True)
|
| 36 |
+
|
| 37 |
+
# The upstream llm_tn TextNormalizer refuses to construct without API creds, and
|
| 38 |
+
# the fastText language detector lives on that object. We only use it for
|
| 39 |
+
# language *identification* (use_llm_tn=False -> local wetext TN), so give it
|
| 40 |
+
# placeholder creds and disable its LLM fallback further below.
|
| 41 |
+
os.environ.setdefault("LLM_TN_API_URL", "http://127.0.0.1:1/unused")
|
| 42 |
+
os.environ.setdefault("LLM_TN_API_KEY", "unused")
|
| 43 |
+
|
| 44 |
+
# --------------------------------------------------------------------------- #
|
| 45 |
+
# Weights
|
| 46 |
+
# --------------------------------------------------------------------------- #
|
| 47 |
+
MODEL_REPO = "FireRedTeam/FireRedTTS3"
|
| 48 |
+
MODEL_DIR = snapshot_download(MODEL_REPO)
|
| 49 |
+
print(f"[INFO] weights at {MODEL_DIR}", flush=True)
|
| 50 |
+
|
| 51 |
+
from fireredtts3.core import FireRedTTS3, FireRedTTS3Instruct # noqa: E402
|
| 52 |
+
from fireredtts3.redae.redae import RedAE # noqa: E402
|
| 53 |
+
from fireredtts3.utils.llm_tn.text_normalizer import TextNormalizer # noqa: E402
|
| 54 |
+
from fireredtts3.utils.text_tokenizer import ( # noqa: E402
|
| 55 |
+
MULTI_DIALECT_TAGS,
|
| 56 |
+
MULTI_LANG_TAGS,
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
# Base and Instruct each instantiate their own RedAE from the very same
|
| 60 |
+
# checkpoint; share one instance instead (~3.8 GB saved, identical numerics).
|
| 61 |
+
_redae_real_from_pretrained = RedAE.from_pretrained
|
| 62 |
+
_redae_singleton = None
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _shared_redae(*args, **kwargs):
|
| 66 |
+
global _redae_singleton
|
| 67 |
+
if _redae_singleton is None:
|
| 68 |
+
_redae_singleton = _redae_real_from_pretrained(*args, **kwargs)
|
| 69 |
+
return _redae_singleton
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
RedAE.from_pretrained = _shared_redae
|
| 73 |
+
|
| 74 |
+
tts = FireRedTTS3(MODEL_DIR, use_fasttext=True, use_llm_tn=False, use_wetext=True)
|
| 75 |
+
instruct = FireRedTTS3Instruct(MODEL_DIR, use_fasttext=True, use_llm_tn=False, use_wetext=True)
|
| 76 |
+
|
| 77 |
+
for _pipe in (tts, instruct):
|
| 78 |
+
_norm = getattr(_pipe, "_llm_tn", None)
|
| 79 |
+
if _norm is not None:
|
| 80 |
+
# No API creds here -> never let language ID fall back to an LLM call.
|
| 81 |
+
_norm.detect_locale = functools.partial(
|
| 82 |
+
TextNormalizer.detect_locale, _norm, use_llm_fallback=False
|
| 83 |
+
)
|
| 84 |
+
print("[INFO] FireRedTTS3 Base + Instruct ready", flush=True)
|
| 85 |
+
|
| 86 |
+
SAMPLE_RATE = tts.redae.sample_rate
|
| 87 |
+
|
| 88 |
+
# --------------------------------------------------------------------------- #
|
| 89 |
+
# Language choices
|
| 90 |
+
# --------------------------------------------------------------------------- #
|
| 91 |
+
AUTO = "Auto-detect"
|
| 92 |
+
LANGUAGES = [t.strip("<|>") for t in MULTI_LANG_TAGS]
|
| 93 |
+
DIALECTS = [t.strip("<|>") for t in MULTI_DIALECT_TAGS]
|
| 94 |
+
LANG_CHOICES = (
|
| 95 |
+
[AUTO]
|
| 96 |
+
+ LANGUAGES
|
| 97 |
+
+ [f"{d} (Chinese dialect)" for d in DIALECTS]
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def _resolve_language(choice: str):
|
| 102 |
+
if not choice or choice == AUTO:
|
| 103 |
+
return None
|
| 104 |
+
return choice.split(" (")[0]
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
# --------------------------------------------------------------------------- #
|
| 108 |
+
# Audio helpers
|
| 109 |
+
# --------------------------------------------------------------------------- #
|
| 110 |
+
MAX_PROMPT_SECONDS = 20.0
|
| 111 |
+
MAX_EDIT_SECONDS = 20.0
|
| 112 |
+
MAX_TEXT_CHARS = 400
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def _load_audio(path: str, max_seconds: float):
|
| 116 |
+
if not path:
|
| 117 |
+
raise gr.Error("Please provide an audio file first.")
|
| 118 |
+
wav, sr = sf.read(path, always_2d=True, dtype="float32")
|
| 119 |
+
wav = wav[:, 0]
|
| 120 |
+
if wav.shape[0] > int(max_seconds * sr):
|
| 121 |
+
wav = wav[: int(max_seconds * sr)]
|
| 122 |
+
gr.Info(f"Audio truncated to the first {max_seconds:.0f}s.")
|
| 123 |
+
peak = float(np.abs(wav).max()) if wav.size else 0.0
|
| 124 |
+
if peak > 0:
|
| 125 |
+
wav = wav / peak * 0.95
|
| 126 |
+
return torch.from_numpy(np.ascontiguousarray(wav)[None, :]), sr
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def _to_gradio_audio(audio: torch.Tensor, sr: int):
|
| 130 |
+
x = audio.detach().float().cpu().numpy()
|
| 131 |
+
if x.ndim > 1:
|
| 132 |
+
x = x[0]
|
| 133 |
+
x = np.clip(x, -1.0, 1.0)
|
| 134 |
+
return sr, (x * 32767.0).astype(np.int16)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def _check_text(text: str, what: str = "Text"):
|
| 138 |
+
text = (text or "").strip()
|
| 139 |
+
if not text:
|
| 140 |
+
raise gr.Error(f"{what} must not be empty.")
|
| 141 |
+
if len(text) > MAX_TEXT_CHARS:
|
| 142 |
+
gr.Info(f"{what} truncated to {MAX_TEXT_CHARS} characters.")
|
| 143 |
+
text = text[:MAX_TEXT_CHARS]
|
| 144 |
+
return text
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
# --------------------------------------------------------------------------- #
|
| 148 |
+
# Inference
|
| 149 |
+
# --------------------------------------------------------------------------- #
|
| 150 |
+
@spaces.GPU(duration=150)
|
| 151 |
+
def voice_clone(
|
| 152 |
+
prompt_audio,
|
| 153 |
+
prompt_text,
|
| 154 |
+
text,
|
| 155 |
+
language=AUTO,
|
| 156 |
+
inference_cfg=2.0,
|
| 157 |
+
n_timesteps=10,
|
| 158 |
+
seed=1234,
|
| 159 |
+
do_tn=True,
|
| 160 |
+
):
|
| 161 |
+
"""FireRedTTS3-Base zero-shot voice cloning."""
|
| 162 |
+
text = _check_text(text, "Text to synthesize")
|
| 163 |
+
prompt_text = (prompt_text or "").strip()
|
| 164 |
+
if not prompt_text:
|
| 165 |
+
raise gr.Error("Please provide the transcript of the reference audio.")
|
| 166 |
+
wav, sr = _load_audio(prompt_audio, MAX_PROMPT_SECONDS)
|
| 167 |
+
|
| 168 |
+
gen_audio, gen_sr = tts.generate(
|
| 169 |
+
text=text,
|
| 170 |
+
language=_resolve_language(language),
|
| 171 |
+
prompt_text=prompt_text,
|
| 172 |
+
prompt_audio=wav,
|
| 173 |
+
prompt_audio_sr=sr,
|
| 174 |
+
n_timesteps=int(n_timesteps),
|
| 175 |
+
inference_cfg=float(inference_cfg),
|
| 176 |
+
seed=int(seed),
|
| 177 |
+
do_tn=bool(do_tn),
|
| 178 |
+
)
|
| 179 |
+
return _to_gradio_audio(gen_audio, gen_sr)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
@spaces.GPU(duration=150)
|
| 183 |
+
def voice_design(
|
| 184 |
+
instruction,
|
| 185 |
+
text,
|
| 186 |
+
inference_cfg=1.2,
|
| 187 |
+
n_timesteps=10,
|
| 188 |
+
seed=2,
|
| 189 |
+
do_tn=True,
|
| 190 |
+
):
|
| 191 |
+
"""FireRedTTS3-Instruct voice design (no reference audio)."""
|
| 192 |
+
instruction = _check_text(instruction, "Voice description")
|
| 193 |
+
text = _check_text(text, "Text to synthesize")
|
| 194 |
+
|
| 195 |
+
gen_audio, gen_sr, gen_text = instruct.generate_voice_design(
|
| 196 |
+
instruction=instruction,
|
| 197 |
+
text=text,
|
| 198 |
+
n_timesteps=int(n_timesteps),
|
| 199 |
+
inference_cfg=float(inference_cfg),
|
| 200 |
+
seed=int(seed),
|
| 201 |
+
do_tn=bool(do_tn),
|
| 202 |
+
)
|
| 203 |
+
return _to_gradio_audio(gen_audio, gen_sr), (gen_text or "").strip()
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
@spaces.GPU(duration=150)
|
| 207 |
+
def semantic_edit(
|
| 208 |
+
audio_in,
|
| 209 |
+
instruction,
|
| 210 |
+
inference_cfg=1.2,
|
| 211 |
+
n_timesteps=10,
|
| 212 |
+
seed=1234,
|
| 213 |
+
):
|
| 214 |
+
"""FireRedTTS3-Instruct content editing: insert / delete / substitute."""
|
| 215 |
+
instruction = _check_text(instruction, "Edit instruction")
|
| 216 |
+
wav, sr = _load_audio(audio_in, MAX_EDIT_SECONDS)
|
| 217 |
+
|
| 218 |
+
gen_audio, gen_sr, gen_text = instruct.generate_semantic_edit(
|
| 219 |
+
instruction=instruction,
|
| 220 |
+
audio_in=wav,
|
| 221 |
+
audio_in_sr=sr,
|
| 222 |
+
n_timesteps=int(n_timesteps),
|
| 223 |
+
inference_cfg=float(inference_cfg),
|
| 224 |
+
seed=int(seed),
|
| 225 |
+
)
|
| 226 |
+
return _to_gradio_audio(gen_audio, gen_sr), (gen_text or "").strip()
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def compose_acoustic_instruction(attribute: str, value: float) -> str:
|
| 230 |
+
"""Acoustic edits only accept the templates the model was trained on."""
|
| 231 |
+
if attribute == "Speed":
|
| 232 |
+
return f"adjust the speed to {value:.1f}x"
|
| 233 |
+
if attribute == "Volume":
|
| 234 |
+
return f"adjust the volume to {value:.1f}"
|
| 235 |
+
steps = int(round(value))
|
| 236 |
+
return f"shift the pitch by {steps} step{'' if abs(steps) == 1 else 's'}"
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
@spaces.GPU(duration=150)
|
| 240 |
+
def acoustic_edit(
|
| 241 |
+
audio_in,
|
| 242 |
+
attribute="Speed",
|
| 243 |
+
value=0.8,
|
| 244 |
+
inference_cfg=1.2,
|
| 245 |
+
n_timesteps=10,
|
| 246 |
+
seed=1234,
|
| 247 |
+
):
|
| 248 |
+
"""FireRedTTS3-Instruct acoustic editing: speed / pitch / volume."""
|
| 249 |
+
wav, sr = _load_audio(audio_in, MAX_EDIT_SECONDS)
|
| 250 |
+
instruction = compose_acoustic_instruction(attribute, float(value))
|
| 251 |
+
|
| 252 |
+
gen_audio, gen_sr = instruct.generate_acoustic_edit(
|
| 253 |
+
instruction=instruction,
|
| 254 |
+
audio_in=wav,
|
| 255 |
+
audio_in_sr=sr,
|
| 256 |
+
n_timesteps=int(n_timesteps),
|
| 257 |
+
inference_cfg=float(inference_cfg),
|
| 258 |
+
seed=int(seed),
|
| 259 |
+
)
|
| 260 |
+
return _to_gradio_audio(gen_audio, gen_sr), instruction
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
# --------------------------------------------------------------------------- #
|
| 264 |
+
# UI
|
| 265 |
+
# --------------------------------------------------------------------------- #
|
| 266 |
+
EN_PROMPT = os.path.join(HERE, "examples", "en_prompt.wav")
|
| 267 |
+
ZH_PROMPT = os.path.join(HERE, "examples", "zh_prompt.wav")
|
| 268 |
+
EN_PROMPT_TEXT = (
|
| 269 |
+
"Just by listening a few minutes a day, you'll be able to eliminate negative "
|
| 270 |
+
"thoughts by conditioning your mind to be more positive."
|
| 271 |
+
)
|
| 272 |
+
ZH_PROMPT_TEXT = "比如具体一点的,他觉得最大的一个跟他预想的不一样的是在什么地方。"
|
| 273 |
+
|
| 274 |
+
CSS = """
|
| 275 |
+
.gradio-container {max-width: 1200px !important; margin: auto !important;}
|
| 276 |
+
.dark .gradio-container {color: var(--body-text-color);}
|
| 277 |
+
"""
|
| 278 |
+
|
| 279 |
+
with gr.Blocks(theme=gr.themes.Citrus(), css=CSS, title="FireRedTTS3") as demo:
|
| 280 |
+
gr.Markdown(
|
| 281 |
+
"""
|
| 282 |
+
# 🔥 FireRedTTS3 — Unified Speech Generation & Editing
|
| 283 |
+
Zero-shot voice cloning in **24 languages + 21 Chinese dialects**, natural-language
|
| 284 |
+
**voice design**, and instruction-driven **speech editing** — all from
|
| 285 |
+
[FireRedTeam/FireRedTTS3](https://huggingface.co/FireRedTeam/FireRedTTS3).
|
| 286 |
+
"""
|
| 287 |
+
)
|
| 288 |
+
|
| 289 |
+
with gr.Tabs():
|
| 290 |
+
# ------------------------------------------------------------------ #
|
| 291 |
+
with gr.Tab("🎙️ Voice Cloning"):
|
| 292 |
+
gr.Markdown(
|
| 293 |
+
"Clone any voice from a short reference clip. For best quality the "
|
| 294 |
+
"reference should be spoken in the **same language / dialect** as the "
|
| 295 |
+
"text you synthesize."
|
| 296 |
+
)
|
| 297 |
+
with gr.Row():
|
| 298 |
+
with gr.Column():
|
| 299 |
+
clone_prompt_audio = gr.Audio(
|
| 300 |
+
label="Reference audio (5–20 s)",
|
| 301 |
+
sources=["upload", "microphone"],
|
| 302 |
+
type="filepath",
|
| 303 |
+
)
|
| 304 |
+
clone_prompt_text = gr.Textbox(
|
| 305 |
+
label="Reference transcript",
|
| 306 |
+
placeholder="Exactly what is said in the reference audio…",
|
| 307 |
+
lines=2,
|
| 308 |
+
)
|
| 309 |
+
clone_text = gr.Textbox(
|
| 310 |
+
label="Text to synthesize",
|
| 311 |
+
placeholder="Type the text you want spoken in that voice…",
|
| 312 |
+
lines=4,
|
| 313 |
+
)
|
| 314 |
+
clone_language = gr.Dropdown(
|
| 315 |
+
LANG_CHOICES, value=AUTO, label="Language / dialect"
|
| 316 |
+
)
|
| 317 |
+
clone_btn = gr.Button("Generate speech", variant="primary")
|
| 318 |
+
with gr.Column():
|
| 319 |
+
clone_out = gr.Audio(label="Generated speech", type="numpy")
|
| 320 |
+
with gr.Accordion("Advanced options", open=False):
|
| 321 |
+
clone_cfg = gr.Slider(
|
| 322 |
+
0.0, 4.0, value=2.0, step=0.1,
|
| 323 |
+
label="CFG strength",
|
| 324 |
+
info="Higher sticks closer to the reference timbre.",
|
| 325 |
+
)
|
| 326 |
+
clone_steps = gr.Slider(
|
| 327 |
+
4, 30, value=10, step=1, label="Flow-matching timesteps"
|
| 328 |
+
)
|
| 329 |
+
clone_seed = gr.Number(value=1234, precision=0, label="Seed")
|
| 330 |
+
clone_tn = gr.Checkbox(
|
| 331 |
+
value=True,
|
| 332 |
+
label="Text normalization (numbers, dates, units → words)",
|
| 333 |
+
)
|
| 334 |
+
gr.Examples(
|
| 335 |
+
examples=[
|
| 336 |
+
[
|
| 337 |
+
EN_PROMPT,
|
| 338 |
+
EN_PROMPT_TEXT,
|
| 339 |
+
"FireRedTTS3 turns a handful of seconds of speech into a voice "
|
| 340 |
+
"that can read anything you write.",
|
| 341 |
+
"English",
|
| 342 |
+
],
|
| 343 |
+
[
|
| 344 |
+
ZH_PROMPT,
|
| 345 |
+
ZH_PROMPT_TEXT,
|
| 346 |
+
"法院与不动产登记部门加强沟通,并督促银行提前办理抵押预约登记。",
|
| 347 |
+
"Chinese",
|
| 348 |
+
],
|
| 349 |
+
[
|
| 350 |
+
EN_PROMPT,
|
| 351 |
+
EN_PROMPT_TEXT,
|
| 352 |
+
"Le modèle peut aussi parler français avec la même voix de référence.",
|
| 353 |
+
"French",
|
| 354 |
+
],
|
| 355 |
+
],
|
| 356 |
+
inputs=[clone_prompt_audio, clone_prompt_text, clone_text, clone_language],
|
| 357 |
+
outputs=[clone_out],
|
| 358 |
+
fn=voice_clone,
|
| 359 |
+
cache_examples=True,
|
| 360 |
+
cache_mode="lazy",
|
| 361 |
+
)
|
| 362 |
+
|
| 363 |
+
# ------------------------------------------------------------------ #
|
| 364 |
+
with gr.Tab("🎨 Voice Design"):
|
| 365 |
+
gr.Markdown(
|
| 366 |
+
"Describe a voice in plain language — no reference audio needed. The "
|
| 367 |
+
"model first writes a voice-attribute plan, then renders the audio."
|
| 368 |
+
)
|
| 369 |
+
with gr.Row():
|
| 370 |
+
with gr.Column():
|
| 371 |
+
design_instruction = gr.Textbox(
|
| 372 |
+
label="Voice description",
|
| 373 |
+
placeholder="e.g. A young woman with a gentle voice, speaking slowly…",
|
| 374 |
+
lines=3,
|
| 375 |
+
)
|
| 376 |
+
design_text = gr.Textbox(
|
| 377 |
+
label="Text to synthesize", lines=4,
|
| 378 |
+
placeholder="Type the text you want spoken…",
|
| 379 |
+
)
|
| 380 |
+
design_btn = gr.Button("Design voice", variant="primary")
|
| 381 |
+
with gr.Column():
|
| 382 |
+
design_out = gr.Audio(label="Generated speech", type="numpy")
|
| 383 |
+
design_plan = gr.Textbox(
|
| 384 |
+
label="Voice-attribute plan (model chain-of-thought)", lines=4
|
| 385 |
+
)
|
| 386 |
+
with gr.Accordion("Advanced options", open=False):
|
| 387 |
+
design_cfg = gr.Slider(
|
| 388 |
+
0.0, 4.0, value=1.2, step=0.1, label="CFG strength"
|
| 389 |
+
)
|
| 390 |
+
design_steps = gr.Slider(
|
| 391 |
+
4, 30, value=10, step=1, label="Flow-matching timesteps"
|
| 392 |
+
)
|
| 393 |
+
design_seed = gr.Number(value=2, precision=0, label="Seed")
|
| 394 |
+
design_tn = gr.Checkbox(value=True, label="Text normalization")
|
| 395 |
+
gr.Examples(
|
| 396 |
+
examples=[
|
| 397 |
+
[
|
| 398 |
+
"一个年轻女性的温柔嗓音,语速稍慢,带一点俏皮。",
|
| 399 |
+
"今天天气很好,我们一起去公园散步吧。",
|
| 400 |
+
],
|
| 401 |
+
[
|
| 402 |
+
"An old sailor with a deep, gravelly voice, speaking slowly and "
|
| 403 |
+
"warmly, as if telling a story by the fire.",
|
| 404 |
+
"The sea was calm that morning, and every rope on deck was "
|
| 405 |
+
"still wet with salt.",
|
| 406 |
+
],
|
| 407 |
+
[
|
| 408 |
+
"A bright, energetic young man hosting a sports broadcast, fast "
|
| 409 |
+
"paced and excited.",
|
| 410 |
+
"And with ten seconds left on the clock, he takes the shot — "
|
| 411 |
+
"and it is in!",
|
| 412 |
+
],
|
| 413 |
+
],
|
| 414 |
+
inputs=[design_instruction, design_text],
|
| 415 |
+
outputs=[design_out, design_plan],
|
| 416 |
+
fn=voice_design,
|
| 417 |
+
cache_examples=True,
|
| 418 |
+
cache_mode="lazy",
|
| 419 |
+
)
|
| 420 |
+
|
| 421 |
+
# ------------------------------------------------------------------ #
|
| 422 |
+
with gr.Tab("✂️ Speech Editing"):
|
| 423 |
+
with gr.Tabs():
|
| 424 |
+
with gr.Tab("Semantic (content)"):
|
| 425 |
+
gr.Markdown(
|
| 426 |
+
"Insert, delete or substitute words in an existing recording "
|
| 427 |
+
"while keeping the original voice. The model transcribes the "
|
| 428 |
+
"audio itself — just say what to change."
|
| 429 |
+
)
|
| 430 |
+
with gr.Row():
|
| 431 |
+
with gr.Column():
|
| 432 |
+
sem_audio = gr.Audio(
|
| 433 |
+
label="Input speech (≤ 20 s)",
|
| 434 |
+
sources=["upload", "microphone"],
|
| 435 |
+
type="filepath",
|
| 436 |
+
)
|
| 437 |
+
sem_instruction = gr.Textbox(
|
| 438 |
+
label="Edit instruction",
|
| 439 |
+
placeholder="e.g. Replace 'negative' with 'anxious'.",
|
| 440 |
+
lines=2,
|
| 441 |
+
)
|
| 442 |
+
sem_btn = gr.Button("Apply edit", variant="primary")
|
| 443 |
+
with gr.Column():
|
| 444 |
+
sem_out = gr.Audio(label="Edited speech", type="numpy")
|
| 445 |
+
sem_text = gr.Textbox(label="Edited transcript", lines=3)
|
| 446 |
+
with gr.Accordion("Advanced options", open=False):
|
| 447 |
+
sem_cfg = gr.Slider(
|
| 448 |
+
0.0, 4.0, value=1.2, step=0.1, label="CFG strength"
|
| 449 |
+
)
|
| 450 |
+
sem_steps = gr.Slider(
|
| 451 |
+
4, 30, value=10, step=1,
|
| 452 |
+
label="Flow-matching timesteps",
|
| 453 |
+
)
|
| 454 |
+
sem_seed = gr.Number(
|
| 455 |
+
value=1234, precision=0, label="Seed"
|
| 456 |
+
)
|
| 457 |
+
gr.Examples(
|
| 458 |
+
examples=[
|
| 459 |
+
[EN_PROMPT, "Replace 'negative' with 'anxious'."],
|
| 460 |
+
[EN_PROMPT, "Delete the words 'a few minutes a day'."],
|
| 461 |
+
[ZH_PROMPT, "把“最大的”替换成“最有意思的”。"],
|
| 462 |
+
],
|
| 463 |
+
inputs=[sem_audio, sem_instruction],
|
| 464 |
+
outputs=[sem_out, sem_text],
|
| 465 |
+
fn=semantic_edit,
|
| 466 |
+
cache_examples=True,
|
| 467 |
+
cache_mode="lazy",
|
| 468 |
+
)
|
| 469 |
+
|
| 470 |
+
with gr.Tab("Acoustic (speed / pitch / volume)"):
|
| 471 |
+
gr.Markdown(
|
| 472 |
+
"Re-render the same utterance with a different speaking rate, "
|
| 473 |
+
"pitch or loudness. These edits follow fixed instruction "
|
| 474 |
+
"templates the model was trained on."
|
| 475 |
+
)
|
| 476 |
+
with gr.Row():
|
| 477 |
+
with gr.Column():
|
| 478 |
+
aco_audio = gr.Audio(
|
| 479 |
+
label="Input speech (≤ 20 s)",
|
| 480 |
+
sources=["upload", "microphone"],
|
| 481 |
+
type="filepath",
|
| 482 |
+
)
|
| 483 |
+
aco_attr = gr.Radio(
|
| 484 |
+
["Speed", "Pitch", "Volume"],
|
| 485 |
+
value="Speed",
|
| 486 |
+
label="Attribute",
|
| 487 |
+
)
|
| 488 |
+
aco_value = gr.Slider(
|
| 489 |
+
0.5, 2.0, value=0.8, step=0.1,
|
| 490 |
+
label="Speed (×)",
|
| 491 |
+
)
|
| 492 |
+
aco_btn = gr.Button("Apply edit", variant="primary")
|
| 493 |
+
with gr.Column():
|
| 494 |
+
aco_out = gr.Audio(label="Edited speech", type="numpy")
|
| 495 |
+
aco_instruction = gr.Textbox(
|
| 496 |
+
label="Instruction sent to the model", lines=1
|
| 497 |
+
)
|
| 498 |
+
with gr.Accordion("Advanced options", open=False):
|
| 499 |
+
aco_cfg = gr.Slider(
|
| 500 |
+
0.0, 4.0, value=1.2, step=0.1, label="CFG strength"
|
| 501 |
+
)
|
| 502 |
+
aco_steps = gr.Slider(
|
| 503 |
+
4, 30, value=10, step=1,
|
| 504 |
+
label="Flow-matching timesteps",
|
| 505 |
+
)
|
| 506 |
+
aco_seed = gr.Number(
|
| 507 |
+
value=1234, precision=0, label="Seed"
|
| 508 |
+
)
|
| 509 |
+
gr.Examples(
|
| 510 |
+
examples=[
|
| 511 |
+
[EN_PROMPT, "Speed", 0.7],
|
| 512 |
+
[ZH_PROMPT, "Pitch", 2],
|
| 513 |
+
[EN_PROMPT, "Volume", 1.6],
|
| 514 |
+
],
|
| 515 |
+
inputs=[aco_audio, aco_attr, aco_value],
|
| 516 |
+
outputs=[aco_out, aco_instruction],
|
| 517 |
+
fn=acoustic_edit,
|
| 518 |
+
cache_examples=True,
|
| 519 |
+
cache_mode="lazy",
|
| 520 |
+
)
|
| 521 |
+
|
| 522 |
+
gr.Markdown(
|
| 523 |
+
"""
|
| 524 |
+
---
|
| 525 |
+
**Model:** [FireRedTeam/FireRedTTS3](https://huggingface.co/FireRedTeam/FireRedTTS3)
|
| 526 |
+
(Apache-2.0) · Base = cloning, Instruct = design + editing. Text normalization
|
| 527 |
+
runs locally through *wetext* (Chinese / English); other languages get basic
|
| 528 |
+
cleaning only. Voice cloning is provided **for academic research purposes only** —
|
| 529 |
+
do not use it for impersonation or any illegal activity.
|
| 530 |
+
"""
|
| 531 |
+
)
|
| 532 |
+
|
| 533 |
+
def _attr_changed(attribute, current):
|
| 534 |
+
lo, hi, step, label = {
|
| 535 |
+
"Speed": (0.5, 2.0, 0.1, "Speed (×)"),
|
| 536 |
+
"Volume": (0.3, 2.0, 0.1, "Volume (×)"),
|
| 537 |
+
}.get(attribute, (-6, 6, 1, "Pitch shift (semitone steps)"))
|
| 538 |
+
try:
|
| 539 |
+
value = min(max(float(current), lo), hi)
|
| 540 |
+
except (TypeError, ValueError):
|
| 541 |
+
value = lo
|
| 542 |
+
if attribute == "Pitch":
|
| 543 |
+
value = int(round(value)) or 1
|
| 544 |
+
return gr.update(minimum=lo, maximum=hi, step=step, value=value, label=label)
|
| 545 |
+
|
| 546 |
+
aco_attr.change(_attr_changed, inputs=[aco_attr, aco_value], outputs=[aco_value])
|
| 547 |
+
|
| 548 |
+
clone_btn.click(
|
| 549 |
+
voice_clone,
|
| 550 |
+
inputs=[clone_prompt_audio, clone_prompt_text, clone_text, clone_language,
|
| 551 |
+
clone_cfg, clone_steps, clone_seed, clone_tn],
|
| 552 |
+
outputs=[clone_out],
|
| 553 |
+
api_name="voice_clone",
|
| 554 |
+
)
|
| 555 |
+
design_btn.click(
|
| 556 |
+
voice_design,
|
| 557 |
+
inputs=[design_instruction, design_text, design_cfg, design_steps,
|
| 558 |
+
design_seed, design_tn],
|
| 559 |
+
outputs=[design_out, design_plan],
|
| 560 |
+
api_name="voice_design",
|
| 561 |
+
)
|
| 562 |
+
sem_btn.click(
|
| 563 |
+
semantic_edit,
|
| 564 |
+
inputs=[sem_audio, sem_instruction, sem_cfg, sem_steps, sem_seed],
|
| 565 |
+
outputs=[sem_out, sem_text],
|
| 566 |
+
api_name="semantic_edit",
|
| 567 |
+
)
|
| 568 |
+
aco_btn.click(
|
| 569 |
+
acoustic_edit,
|
| 570 |
+
inputs=[aco_audio, aco_attr, aco_value, aco_cfg, aco_steps, aco_seed],
|
| 571 |
+
outputs=[aco_out, aco_instruction],
|
| 572 |
+
api_name="acoustic_edit",
|
| 573 |
+
)
|
| 574 |
+
|
| 575 |
+
if __name__ == "__main__":
|
| 576 |
+
demo.queue().launch(mcp_server=True)
|
examples/en_prompt.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d3eecb1e289b16e6038933b20d6d88a286f91b3f19e8fd1a734224a35e91747f
|
| 3 |
+
size 391624
|
examples/zh_prompt.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:18a82b883c6a18569cdb183a2b7763cbe7a7ee24c9046ed7edf4d28675118224
|
| 3 |
+
size 252644
|
fireredtts3/campp/DTDNN.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 3D-Speaker (https://github.com/alibaba-damo-academy/3D-Speaker). All Rights Reserved.
|
| 2 |
+
# Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
| 3 |
+
|
| 4 |
+
from collections import OrderedDict
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
from torch import nn
|
| 8 |
+
import torch.nn.functional as F
|
| 9 |
+
|
| 10 |
+
from fireredtts3.campp.layers import (
|
| 11 |
+
DenseLayer,
|
| 12 |
+
StatsPool,
|
| 13 |
+
TDNNLayer,
|
| 14 |
+
CAMDenseTDNNBlock,
|
| 15 |
+
TransitLayer,
|
| 16 |
+
BasicResBlock,
|
| 17 |
+
get_nonlinear,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class FCM(nn.Module):
|
| 22 |
+
def __init__(self,
|
| 23 |
+
block=BasicResBlock,
|
| 24 |
+
num_blocks=[2, 2],
|
| 25 |
+
m_channels=32,
|
| 26 |
+
feat_dim=80):
|
| 27 |
+
super(FCM, self).__init__()
|
| 28 |
+
self.in_planes = m_channels
|
| 29 |
+
self.conv1 = nn.Conv2d(1, m_channels, kernel_size=3, stride=1, padding=1, bias=False)
|
| 30 |
+
self.bn1 = nn.BatchNorm2d(m_channels)
|
| 31 |
+
|
| 32 |
+
self.layer1 = self._make_layer(block, m_channels, num_blocks[0], stride=2)
|
| 33 |
+
self.layer2 = self._make_layer(block, m_channels, num_blocks[1], stride=2)
|
| 34 |
+
|
| 35 |
+
self.conv2 = nn.Conv2d(m_channels, m_channels, kernel_size=3, stride=(2, 1), padding=1, bias=False)
|
| 36 |
+
self.bn2 = nn.BatchNorm2d(m_channels)
|
| 37 |
+
self.out_channels = m_channels * (feat_dim // 8)
|
| 38 |
+
|
| 39 |
+
def _make_layer(self, block, planes, num_blocks, stride):
|
| 40 |
+
strides = [stride] + [1] * (num_blocks - 1)
|
| 41 |
+
layers = []
|
| 42 |
+
for stride in strides:
|
| 43 |
+
layers.append(block(self.in_planes, planes, stride))
|
| 44 |
+
self.in_planes = planes * block.expansion
|
| 45 |
+
return nn.Sequential(*layers)
|
| 46 |
+
|
| 47 |
+
def forward(self, x):
|
| 48 |
+
x = x.unsqueeze(1)
|
| 49 |
+
out = F.relu(self.bn1(self.conv1(x)))
|
| 50 |
+
out = self.layer1(out)
|
| 51 |
+
out = self.layer2(out)
|
| 52 |
+
out = F.relu(self.bn2(self.conv2(out)))
|
| 53 |
+
|
| 54 |
+
shape = out.shape
|
| 55 |
+
out = out.reshape(shape[0], shape[1]*shape[2], shape[3])
|
| 56 |
+
return out
|
| 57 |
+
|
| 58 |
+
class CAMPPlus(nn.Module):
|
| 59 |
+
def __init__(self,
|
| 60 |
+
feat_dim=80,
|
| 61 |
+
embedding_size=512,
|
| 62 |
+
growth_rate=32,
|
| 63 |
+
bn_size=4,
|
| 64 |
+
init_channels=128,
|
| 65 |
+
config_str='batchnorm-relu',
|
| 66 |
+
memory_efficient=True):
|
| 67 |
+
super(CAMPPlus, self).__init__()
|
| 68 |
+
|
| 69 |
+
self.head = FCM(feat_dim=feat_dim)
|
| 70 |
+
channels = self.head.out_channels
|
| 71 |
+
|
| 72 |
+
self.xvector = nn.Sequential(
|
| 73 |
+
OrderedDict([
|
| 74 |
+
|
| 75 |
+
('tdnn',
|
| 76 |
+
TDNNLayer(channels,
|
| 77 |
+
init_channels,
|
| 78 |
+
5,
|
| 79 |
+
stride=2,
|
| 80 |
+
dilation=1,
|
| 81 |
+
padding=-1,
|
| 82 |
+
config_str=config_str)),
|
| 83 |
+
]))
|
| 84 |
+
channels = init_channels
|
| 85 |
+
for i, (num_layers, kernel_size,
|
| 86 |
+
dilation) in enumerate(zip((12, 24, 16), (3, 3, 3), (1, 2, 2))):
|
| 87 |
+
block = CAMDenseTDNNBlock(num_layers=num_layers,
|
| 88 |
+
in_channels=channels,
|
| 89 |
+
out_channels=growth_rate,
|
| 90 |
+
bn_channels=bn_size * growth_rate,
|
| 91 |
+
kernel_size=kernel_size,
|
| 92 |
+
dilation=dilation,
|
| 93 |
+
config_str=config_str,
|
| 94 |
+
memory_efficient=memory_efficient)
|
| 95 |
+
self.xvector.add_module('block%d' % (i + 1), block)
|
| 96 |
+
channels = channels + num_layers * growth_rate
|
| 97 |
+
self.xvector.add_module(
|
| 98 |
+
'transit%d' % (i + 1),
|
| 99 |
+
TransitLayer(channels,
|
| 100 |
+
channels // 2,
|
| 101 |
+
bias=False,
|
| 102 |
+
config_str=config_str))
|
| 103 |
+
channels //= 2
|
| 104 |
+
|
| 105 |
+
self.xvector.add_module(
|
| 106 |
+
'out_nonlinear', get_nonlinear(config_str, channels))
|
| 107 |
+
|
| 108 |
+
self.xvector.add_module('stats', StatsPool())
|
| 109 |
+
self.xvector.add_module(
|
| 110 |
+
'dense',
|
| 111 |
+
DenseLayer(channels * 2, embedding_size, config_str='batchnorm_'))
|
| 112 |
+
|
| 113 |
+
for m in self.modules():
|
| 114 |
+
if isinstance(m, (nn.Conv1d, nn.Linear)):
|
| 115 |
+
nn.init.kaiming_normal_(m.weight.data)
|
| 116 |
+
if m.bias is not None:
|
| 117 |
+
nn.init.zeros_(m.bias)
|
| 118 |
+
|
| 119 |
+
def forward(self, x):
|
| 120 |
+
x = x.permute(0, 2, 1) # (B,T,F) => (B,F,T)
|
| 121 |
+
x = self.head(x)
|
| 122 |
+
x = self.xvector(x)
|
| 123 |
+
return x
|
fireredtts3/campp/campp.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torchaudio
|
| 3 |
+
import torchaudio.compliance.kaldi as kaldi
|
| 4 |
+
from fireredtts3.campp.DTDNN import CAMPPlus
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def extract_kaldi_mel(audio: torch.Tensor, audio_sr: int):
|
| 8 |
+
audio = audio[:1]
|
| 9 |
+
if audio_sr != 16000:
|
| 10 |
+
audio = torchaudio.functional.resample(audio, audio_sr, 16000)
|
| 11 |
+
audio_sr = 16000
|
| 12 |
+
feat = kaldi.fbank(
|
| 13 |
+
audio,
|
| 14 |
+
num_mel_bins=80,
|
| 15 |
+
dither=0,
|
| 16 |
+
sample_frequency=16000
|
| 17 |
+
)
|
| 18 |
+
feat = feat - feat.mean(dim=0, keepdim=True) # (t, c=80)
|
| 19 |
+
return feat
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class CamppEmbedding(torch.nn.Module):
|
| 23 |
+
def __init__(self, model_path:str):
|
| 24 |
+
super().__init__()
|
| 25 |
+
self.model_path = model_path
|
| 26 |
+
model = CAMPPlus(feat_dim=80, embedding_size=512)
|
| 27 |
+
sd = torch.load(model_path, weights_only=True, map_location='cpu')
|
| 28 |
+
model.load_state_dict(sd)
|
| 29 |
+
self.model = model
|
| 30 |
+
self.model.eval()
|
| 31 |
+
|
| 32 |
+
@property
|
| 33 |
+
def device(self):
|
| 34 |
+
return next(self.parameters()).device
|
| 35 |
+
|
| 36 |
+
def forward(self, audio:torch.Tensor, audio_sr:int):
|
| 37 |
+
"""
|
| 38 |
+
Returns:
|
| 39 |
+
embedding(torch.Tensor): shape (1, 512)
|
| 40 |
+
"""
|
| 41 |
+
feat = extract_kaldi_mel(audio, audio_sr)
|
| 42 |
+
feat = feat.unsqueeze(0)
|
| 43 |
+
with torch.no_grad():
|
| 44 |
+
embedding = self.model.forward(feat.to(self.device)) # (b=1, c=512)
|
| 45 |
+
embedding = embedding.cpu()
|
| 46 |
+
return embedding
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
if __name__ == '__main__':
|
| 51 |
+
campp = CamppEmbedding(
|
| 52 |
+
model_path='pretrained_models/speech_campplus_sv_en_voxceleb_16k/campplus_voxceleb.bin',
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
audio, audio_sr = torchaudio.load('tests/input/siteng_clean.wav')
|
| 56 |
+
|
| 57 |
+
campp.forward(audio, audio_sr)
|
| 58 |
+
|
fireredtts3/campp/layers.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 3D-Speaker (https://github.com/alibaba-damo-academy/3D-Speaker). All Rights Reserved.
|
| 2 |
+
# Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
import torch.utils.checkpoint as cp
|
| 7 |
+
from torch import nn
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def get_nonlinear(config_str, channels):
|
| 11 |
+
nonlinear = nn.Sequential()
|
| 12 |
+
for name in config_str.split('-'):
|
| 13 |
+
if name == 'relu':
|
| 14 |
+
nonlinear.add_module('relu', nn.ReLU(inplace=True))
|
| 15 |
+
elif name == 'prelu':
|
| 16 |
+
nonlinear.add_module('prelu', nn.PReLU(channels))
|
| 17 |
+
elif name == 'batchnorm':
|
| 18 |
+
nonlinear.add_module('batchnorm', nn.BatchNorm1d(channels))
|
| 19 |
+
elif name == 'batchnorm_':
|
| 20 |
+
nonlinear.add_module('batchnorm',
|
| 21 |
+
nn.BatchNorm1d(channels, affine=False))
|
| 22 |
+
else:
|
| 23 |
+
raise ValueError('Unexpected module ({}).'.format(name))
|
| 24 |
+
return nonlinear
|
| 25 |
+
|
| 26 |
+
def statistics_pooling(x, dim=-1, keepdim=False, unbiased=True, eps=1e-2):
|
| 27 |
+
mean = x.mean(dim=dim)
|
| 28 |
+
std = x.std(dim=dim, unbiased=unbiased)
|
| 29 |
+
stats = torch.cat([mean, std], dim=-1)
|
| 30 |
+
if keepdim:
|
| 31 |
+
stats = stats.unsqueeze(dim=dim)
|
| 32 |
+
return stats
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class StatsPool(nn.Module):
|
| 36 |
+
def forward(self, x):
|
| 37 |
+
return statistics_pooling(x)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class TDNNLayer(nn.Module):
|
| 41 |
+
def __init__(self,
|
| 42 |
+
in_channels,
|
| 43 |
+
out_channels,
|
| 44 |
+
kernel_size,
|
| 45 |
+
stride=1,
|
| 46 |
+
padding=0,
|
| 47 |
+
dilation=1,
|
| 48 |
+
bias=False,
|
| 49 |
+
config_str='batchnorm-relu'):
|
| 50 |
+
super(TDNNLayer, self).__init__()
|
| 51 |
+
if padding < 0:
|
| 52 |
+
assert kernel_size % 2 == 1, 'Expect equal paddings, but got even kernel size ({})'.format(
|
| 53 |
+
kernel_size)
|
| 54 |
+
padding = (kernel_size - 1) // 2 * dilation
|
| 55 |
+
self.linear = nn.Conv1d(in_channels,
|
| 56 |
+
out_channels,
|
| 57 |
+
kernel_size,
|
| 58 |
+
stride=stride,
|
| 59 |
+
padding=padding,
|
| 60 |
+
dilation=dilation,
|
| 61 |
+
bias=bias)
|
| 62 |
+
self.nonlinear = get_nonlinear(config_str, out_channels)
|
| 63 |
+
|
| 64 |
+
def forward(self, x):
|
| 65 |
+
x = self.linear(x)
|
| 66 |
+
x = self.nonlinear(x)
|
| 67 |
+
return x
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class CAMLayer(nn.Module):
|
| 71 |
+
def __init__(self,
|
| 72 |
+
bn_channels,
|
| 73 |
+
out_channels,
|
| 74 |
+
kernel_size,
|
| 75 |
+
stride,
|
| 76 |
+
padding,
|
| 77 |
+
dilation,
|
| 78 |
+
bias,
|
| 79 |
+
reduction=2):
|
| 80 |
+
super(CAMLayer, self).__init__()
|
| 81 |
+
self.linear_local = nn.Conv1d(bn_channels,
|
| 82 |
+
out_channels,
|
| 83 |
+
kernel_size,
|
| 84 |
+
stride=stride,
|
| 85 |
+
padding=padding,
|
| 86 |
+
dilation=dilation,
|
| 87 |
+
bias=bias)
|
| 88 |
+
self.linear1 = nn.Conv1d(bn_channels, bn_channels // reduction, 1)
|
| 89 |
+
self.relu = nn.ReLU(inplace=True)
|
| 90 |
+
self.linear2 = nn.Conv1d(bn_channels // reduction, out_channels, 1)
|
| 91 |
+
self.sigmoid = nn.Sigmoid()
|
| 92 |
+
|
| 93 |
+
def forward(self, x):
|
| 94 |
+
y = self.linear_local(x)
|
| 95 |
+
context = x.mean(-1, keepdim=True)+self.seg_pooling(x)
|
| 96 |
+
context = self.relu(self.linear1(context))
|
| 97 |
+
m = self.sigmoid(self.linear2(context))
|
| 98 |
+
return y*m
|
| 99 |
+
|
| 100 |
+
def seg_pooling(self, x, seg_len=100, stype='avg'):
|
| 101 |
+
if stype == 'avg':
|
| 102 |
+
seg = F.avg_pool1d(x, kernel_size=seg_len, stride=seg_len, ceil_mode=True)
|
| 103 |
+
elif stype == 'max':
|
| 104 |
+
seg = F.max_pool1d(x, kernel_size=seg_len, stride=seg_len, ceil_mode=True)
|
| 105 |
+
else:
|
| 106 |
+
raise ValueError('Wrong segment pooling type.')
|
| 107 |
+
shape = seg.shape
|
| 108 |
+
seg = seg.unsqueeze(-1).expand(*shape, seg_len).reshape(*shape[:-1], -1)
|
| 109 |
+
seg = seg[..., :x.shape[-1]]
|
| 110 |
+
return seg
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
class CAMDenseTDNNLayer(nn.Module):
|
| 114 |
+
def __init__(self,
|
| 115 |
+
in_channels,
|
| 116 |
+
out_channels,
|
| 117 |
+
bn_channels,
|
| 118 |
+
kernel_size,
|
| 119 |
+
stride=1,
|
| 120 |
+
dilation=1,
|
| 121 |
+
bias=False,
|
| 122 |
+
config_str='batchnorm-relu',
|
| 123 |
+
memory_efficient=False):
|
| 124 |
+
super(CAMDenseTDNNLayer, self).__init__()
|
| 125 |
+
assert kernel_size % 2 == 1, 'Expect equal paddings, but got even kernel size ({})'.format(
|
| 126 |
+
kernel_size)
|
| 127 |
+
padding = (kernel_size - 1) // 2 * dilation
|
| 128 |
+
self.memory_efficient = memory_efficient
|
| 129 |
+
self.nonlinear1 = get_nonlinear(config_str, in_channels)
|
| 130 |
+
self.linear1 = nn.Conv1d(in_channels, bn_channels, 1, bias=False)
|
| 131 |
+
self.nonlinear2 = get_nonlinear(config_str, bn_channels)
|
| 132 |
+
self.cam_layer = CAMLayer(bn_channels,
|
| 133 |
+
out_channels,
|
| 134 |
+
kernel_size,
|
| 135 |
+
stride=stride,
|
| 136 |
+
padding=padding,
|
| 137 |
+
dilation=dilation,
|
| 138 |
+
bias=bias)
|
| 139 |
+
|
| 140 |
+
def bn_function(self, x):
|
| 141 |
+
return self.linear1(self.nonlinear1(x))
|
| 142 |
+
|
| 143 |
+
def forward(self, x):
|
| 144 |
+
if self.training and self.memory_efficient:
|
| 145 |
+
x = cp.checkpoint(self.bn_function, x)
|
| 146 |
+
else:
|
| 147 |
+
x = self.bn_function(x)
|
| 148 |
+
x = self.cam_layer(self.nonlinear2(x))
|
| 149 |
+
return x
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
class CAMDenseTDNNBlock(nn.ModuleList):
|
| 153 |
+
def __init__(self,
|
| 154 |
+
num_layers,
|
| 155 |
+
in_channels,
|
| 156 |
+
out_channels,
|
| 157 |
+
bn_channels,
|
| 158 |
+
kernel_size,
|
| 159 |
+
stride=1,
|
| 160 |
+
dilation=1,
|
| 161 |
+
bias=False,
|
| 162 |
+
config_str='batchnorm-relu',
|
| 163 |
+
memory_efficient=False):
|
| 164 |
+
super(CAMDenseTDNNBlock, self).__init__()
|
| 165 |
+
for i in range(num_layers):
|
| 166 |
+
layer = CAMDenseTDNNLayer(in_channels=in_channels + i * out_channels,
|
| 167 |
+
out_channels=out_channels,
|
| 168 |
+
bn_channels=bn_channels,
|
| 169 |
+
kernel_size=kernel_size,
|
| 170 |
+
stride=stride,
|
| 171 |
+
dilation=dilation,
|
| 172 |
+
bias=bias,
|
| 173 |
+
config_str=config_str,
|
| 174 |
+
memory_efficient=memory_efficient)
|
| 175 |
+
self.add_module('tdnnd%d' % (i + 1), layer)
|
| 176 |
+
|
| 177 |
+
def forward(self, x):
|
| 178 |
+
for layer in self:
|
| 179 |
+
x = torch.cat([x, layer(x)], dim=1)
|
| 180 |
+
return x
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
class TransitLayer(nn.Module):
|
| 184 |
+
def __init__(self,
|
| 185 |
+
in_channels,
|
| 186 |
+
out_channels,
|
| 187 |
+
bias=True,
|
| 188 |
+
config_str='batchnorm-relu'):
|
| 189 |
+
super(TransitLayer, self).__init__()
|
| 190 |
+
self.nonlinear = get_nonlinear(config_str, in_channels)
|
| 191 |
+
self.linear = nn.Conv1d(in_channels, out_channels, 1, bias=bias)
|
| 192 |
+
|
| 193 |
+
def forward(self, x):
|
| 194 |
+
x = self.nonlinear(x)
|
| 195 |
+
x = self.linear(x)
|
| 196 |
+
return x
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
class DenseLayer(nn.Module):
|
| 200 |
+
def __init__(self,
|
| 201 |
+
in_channels,
|
| 202 |
+
out_channels,
|
| 203 |
+
bias=False,
|
| 204 |
+
config_str='batchnorm-relu'):
|
| 205 |
+
super(DenseLayer, self).__init__()
|
| 206 |
+
self.linear = nn.Conv1d(in_channels, out_channels, 1, bias=bias)
|
| 207 |
+
self.nonlinear = get_nonlinear(config_str, out_channels)
|
| 208 |
+
|
| 209 |
+
def forward(self, x):
|
| 210 |
+
if len(x.shape) == 2:
|
| 211 |
+
x = self.linear(x.unsqueeze(dim=-1)).squeeze(dim=-1)
|
| 212 |
+
else:
|
| 213 |
+
x = self.linear(x)
|
| 214 |
+
x = self.nonlinear(x)
|
| 215 |
+
return x
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
class BasicResBlock(nn.Module):
|
| 219 |
+
expansion = 1
|
| 220 |
+
|
| 221 |
+
def __init__(self, in_planes, planes, stride=1):
|
| 222 |
+
super(BasicResBlock, self).__init__()
|
| 223 |
+
self.conv1 = nn.Conv2d(in_planes,
|
| 224 |
+
planes,
|
| 225 |
+
kernel_size=3,
|
| 226 |
+
stride=(stride, 1),
|
| 227 |
+
padding=1,
|
| 228 |
+
bias=False)
|
| 229 |
+
self.bn1 = nn.BatchNorm2d(planes)
|
| 230 |
+
self.conv2 = nn.Conv2d(planes,
|
| 231 |
+
planes,
|
| 232 |
+
kernel_size=3,
|
| 233 |
+
stride=1,
|
| 234 |
+
padding=1,
|
| 235 |
+
bias=False)
|
| 236 |
+
self.bn2 = nn.BatchNorm2d(planes)
|
| 237 |
+
|
| 238 |
+
self.shortcut = nn.Sequential()
|
| 239 |
+
if stride != 1 or in_planes != self.expansion * planes:
|
| 240 |
+
self.shortcut = nn.Sequential(
|
| 241 |
+
nn.Conv2d(in_planes,
|
| 242 |
+
self.expansion * planes,
|
| 243 |
+
kernel_size=1,
|
| 244 |
+
stride=(stride, 1),
|
| 245 |
+
bias=False),
|
| 246 |
+
nn.BatchNorm2d(self.expansion * planes))
|
| 247 |
+
|
| 248 |
+
def forward(self, x):
|
| 249 |
+
out = F.relu(self.bn1(self.conv1(x)))
|
| 250 |
+
out = self.bn2(self.conv2(out))
|
| 251 |
+
out += self.shortcut(x)
|
| 252 |
+
out = F.relu(out)
|
| 253 |
+
return out
|
fireredtts3/core.py
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FireRedTTS3 合成管线:在 FireRedTTS3Base / FireRedTTS3Instruct 上增加完整的文本前端处理。
|
| 2 |
+
|
| 3 |
+
用法示例::
|
| 4 |
+
|
| 5 |
+
from fireredtts3.core import FireRedTTS3, FireRedTTS3Instruct
|
| 6 |
+
|
| 7 |
+
# ---- Base ----
|
| 8 |
+
tts = FireRedTTS3('pretrained_models')
|
| 9 |
+
gen_audio, gen_audio_sr = tts.generate(
|
| 10 |
+
language=None, # None 表示自动判定语种
|
| 11 |
+
prompt_text='...',
|
| 12 |
+
prompt_audio=prompt_audio,
|
| 13 |
+
prompt_audio_sr=16000,
|
| 14 |
+
text='这是一段很长的文本,需要被自动切句。第二句。第三句。',
|
| 15 |
+
)
|
| 16 |
+
torchaudio.save('gen.wav', gen_audio.cpu(), gen_audio_sr)
|
| 17 |
+
|
| 18 |
+
# ---- Instruct ----
|
| 19 |
+
instruct = FireRedTTS3Instruct('pretrained_models')
|
| 20 |
+
gen_audio, gen_audio_sr, gen_text = instruct.generate_voice_design(
|
| 21 |
+
instruction='一个年轻女性的温柔嗓音,语速稍慢。',
|
| 22 |
+
text='今天天气很好,我们一起去公园散步吧。',
|
| 23 |
+
)
|
| 24 |
+
torchaudio.save('design.wav', gen_audio.cpu(), gen_audio_sr)
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
import os
|
| 28 |
+
import torch
|
| 29 |
+
import torchaudio
|
| 30 |
+
import numpy as np
|
| 31 |
+
from typing import List, Optional, Callable
|
| 32 |
+
|
| 33 |
+
from fireredtts3.llm.fireredtts3_base import FireRedTTS3Base
|
| 34 |
+
from fireredtts3.llm.fireredtts3_instruct import FireRedTTS3Instruct as FireRedTTS3InstructBackend
|
| 35 |
+
from fireredtts3.utils.text_normalize import (
|
| 36 |
+
clean_text,
|
| 37 |
+
clean_tn_spaces,
|
| 38 |
+
split_paragraph,
|
| 39 |
+
detect_language,
|
| 40 |
+
lang_tag_to_locale,
|
| 41 |
+
build_llm_normalizer,
|
| 42 |
+
build_wetext_normalizer,
|
| 43 |
+
)
|
| 44 |
+
from fireredtts3.utils.llm_tn.text_normalizer import TextNormalizer as LlmTextNormalizer
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def cross_fade(
|
| 48 |
+
seg_a: torch.Tensor,
|
| 49 |
+
seg_b: torch.Tensor,
|
| 50 |
+
fade_len: int,
|
| 51 |
+
) -> torch.Tensor:
|
| 52 |
+
"""将两段波形线性 cross-fading 拼接。"""
|
| 53 |
+
if fade_len <= 0:
|
| 54 |
+
return torch.cat([seg_a, seg_b], dim=1)
|
| 55 |
+
fade_len = int(min(fade_len, seg_a.shape[1], seg_b.shape[1]))
|
| 56 |
+
if fade_len <= 0:
|
| 57 |
+
return torch.cat([seg_a, seg_b], dim=1)
|
| 58 |
+
|
| 59 |
+
# 前段末尾 fade_len 个点做淡出,后段开头 fade_len 个点做淡入
|
| 60 |
+
ramp = torch.linspace(0.0, 1.0, fade_len, device=seg_a.device, dtype=seg_a.dtype).view(1, -1)
|
| 61 |
+
|
| 62 |
+
a_tail = seg_a[:, -fade_len:] * (1.0 - ramp)
|
| 63 |
+
b_head = seg_b[:, :fade_len] * ramp
|
| 64 |
+
|
| 65 |
+
overlap = a_tail + b_head
|
| 66 |
+
|
| 67 |
+
head = seg_a[:, :-fade_len]
|
| 68 |
+
tail = seg_b[:, fade_len:]
|
| 69 |
+
return torch.cat([head, overlap, tail], dim=1)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class TextFrontendMixin:
|
| 73 |
+
"""文本前端(text front-end)能力:清洗 + 语种判定 + TN + 拆句。
|
| 74 |
+
|
| 75 |
+
被 :class:`FireRedTTS3` 与 :class:`FireRedTTS3Instruct` 复用,保证
|
| 76 |
+
克隆 / 音色设计 / 编辑等任务共享同一套文本规整逻辑。
|
| 77 |
+
"""
|
| 78 |
+
|
| 79 |
+
_WETEXT_LANGS = {"Chinese", "English"} # wetext 仅支持中/英
|
| 80 |
+
|
| 81 |
+
def _init_frontend(
|
| 82 |
+
self,
|
| 83 |
+
use_fasttext: bool = True,
|
| 84 |
+
use_llm_tn: bool = False,
|
| 85 |
+
use_wetext: bool = True,
|
| 86 |
+
tn_api_url: Optional[str] = None,
|
| 87 |
+
tn_api_key: Optional[str] = None,
|
| 88 |
+
tn_model: Optional[str] = None,
|
| 89 |
+
tn_kwargs: Optional[dict] = None,
|
| 90 |
+
):
|
| 91 |
+
"""初始化文本前端组件:fasttext 语种检测 + llm_tn / wetext 归一化。"""
|
| 92 |
+
self.use_fasttext = use_fasttext
|
| 93 |
+
self._llm_tn = None
|
| 94 |
+
if use_fasttext:
|
| 95 |
+
try:
|
| 96 |
+
self._llm_tn = LlmTextNormalizer(use_fasttext=True)
|
| 97 |
+
except Exception as e:
|
| 98 |
+
print(f"[WARN] Failed to init llm_tn TextNormalizer for lang detect: {e}", flush=True)
|
| 99 |
+
self._llm_tn = None
|
| 100 |
+
self.use_wetext = use_wetext
|
| 101 |
+
self.wetext_normalizer = None
|
| 102 |
+
self.llm_normalizer = None
|
| 103 |
+
# 默认走 wetext(本地、无需 API key)。llm_tn 是可选增强:仅当显式
|
| 104 |
+
# use_llm_tn=True 且成功构建(即有 .env / 环境变量)时才启用。
|
| 105 |
+
if use_wetext:
|
| 106 |
+
self.wetext_normalizer = build_wetext_normalizer()
|
| 107 |
+
if use_llm_tn:
|
| 108 |
+
self.llm_normalizer = build_llm_normalizer(
|
| 109 |
+
api_url=tn_api_url,
|
| 110 |
+
api_key=tn_api_key,
|
| 111 |
+
model=tn_model,
|
| 112 |
+
**(tn_kwargs or {}),
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
def _detect_lang(self, text: str) -> str:
|
| 116 |
+
"""判定文本语种,返回如 ``Chinese`` / ``English`` 的 lang tag。"""
|
| 117 |
+
return detect_language(
|
| 118 |
+
text,
|
| 119 |
+
fasttext_detector=self._llm_tn.detect_locale if self._llm_tn is not None else None,
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
def _normalize_text(self, text: str, language: str) -> str:
|
| 123 |
+
"""TN 回退链:llm_tn → wetext(中/英/方言/粤语)→ 原文。"""
|
| 124 |
+
if self.llm_normalizer is not None:
|
| 125 |
+
locale = lang_tag_to_locale(language) if language else None
|
| 126 |
+
try:
|
| 127 |
+
return clean_tn_spaces(self.llm_normalizer(text, locale=locale))
|
| 128 |
+
except Exception as e:
|
| 129 |
+
print(f"[WARN] llm_tn normalization failed, fallback to raw text: {e}", flush=True)
|
| 130 |
+
return clean_tn_spaces(text)
|
| 131 |
+
_can_wetext = (
|
| 132 |
+
language in self._WETEXT_LANGS
|
| 133 |
+
or language == "Cantonese"
|
| 134 |
+
or language.startswith("ZH_")
|
| 135 |
+
)
|
| 136 |
+
if _can_wetext and self.wetext_normalizer is not None:
|
| 137 |
+
try:
|
| 138 |
+
return clean_tn_spaces(self.wetext_normalizer(text))
|
| 139 |
+
except Exception as e:
|
| 140 |
+
print(f"[WARN] wetext normalization failed, fallback to raw text: {e}", flush=True)
|
| 141 |
+
return clean_tn_spaces(text)
|
| 142 |
+
# 其他语种且无 llm_tn => 仅基础清洗(返回原文)
|
| 143 |
+
return clean_tn_spaces(text)
|
| 144 |
+
|
| 145 |
+
def _apply_frontend(
|
| 146 |
+
self,
|
| 147 |
+
text: str,
|
| 148 |
+
language: Optional[str] = None,
|
| 149 |
+
do_clean: bool = True,
|
| 150 |
+
do_tn: bool = True,
|
| 151 |
+
do_split: bool = True,
|
| 152 |
+
token_max_n: int = 80,
|
| 153 |
+
token_min_n: int = 60,
|
| 154 |
+
merge_len: int = 20,
|
| 155 |
+
) -> "tuple[str, str, List[str]]":
|
| 156 |
+
"""对文本执行清洗 / 语种判定 / 拆句 / TN,返回 ``(text, language, sentences)``。
|
| 157 |
+
|
| 158 |
+
注:返回的 ``text`` 已按 ``do_split`` 结果合并(未开启拆句则原样返回)。
|
| 159 |
+
"""
|
| 160 |
+
if do_clean:
|
| 161 |
+
text = clean_text(text)
|
| 162 |
+
if not text:
|
| 163 |
+
raise ValueError("text is empty after cleaning")
|
| 164 |
+
|
| 165 |
+
if language is None:
|
| 166 |
+
language = self._detect_lang(text)
|
| 167 |
+
|
| 168 |
+
# zh 按字符分句,非 zh 按 token 分句
|
| 169 |
+
if do_split:
|
| 170 |
+
split_lang = "zh" if language == "Chinese" else "en"
|
| 171 |
+
tokenize = None
|
| 172 |
+
if split_lang != "zh":
|
| 173 |
+
tokenize = lambda s: self._tokenize_text(s).shape[1]
|
| 174 |
+
sentences = split_paragraph(
|
| 175 |
+
text,
|
| 176 |
+
tokenize=tokenize,
|
| 177 |
+
lang=split_lang,
|
| 178 |
+
token_max_n=token_max_n,
|
| 179 |
+
token_min_n=token_min_n,
|
| 180 |
+
merge_len=merge_len,
|
| 181 |
+
)
|
| 182 |
+
else:
|
| 183 |
+
sentences = [text]
|
| 184 |
+
|
| 185 |
+
if do_tn:
|
| 186 |
+
sentences = [self._normalize_text(s, language) for s in sentences]
|
| 187 |
+
sentences = [s for s in sentences if s and s.strip()]
|
| 188 |
+
if not sentences:
|
| 189 |
+
raise ValueError("all sentences are empty after normalization")
|
| 190 |
+
|
| 191 |
+
return "".join(sentences), language, sentences
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
class FireRedTTS3(TextFrontendMixin, FireRedTTS3Base):
|
| 195 |
+
"""带完整文本前端(清洗 + 语种判定 + 拆句 + 拼接)的零样本 TTS 管线。"""
|
| 196 |
+
|
| 197 |
+
def __init__(
|
| 198 |
+
self,
|
| 199 |
+
pretrained_model_dir: str,
|
| 200 |
+
use_fasttext: bool = True,
|
| 201 |
+
use_llm_tn: bool = False,
|
| 202 |
+
use_wetext: bool = True,
|
| 203 |
+
tn_api_url: Optional[str] = None,
|
| 204 |
+
tn_api_key: Optional[str] = None,
|
| 205 |
+
tn_model: Optional[str] = None,
|
| 206 |
+
tn_kwargs: Optional[dict] = None,
|
| 207 |
+
):
|
| 208 |
+
super().__init__(pretrained_model_dir)
|
| 209 |
+
self._init_frontend(
|
| 210 |
+
use_fasttext=use_fasttext,
|
| 211 |
+
use_llm_tn=use_llm_tn,
|
| 212 |
+
use_wetext=use_wetext,
|
| 213 |
+
tn_api_url=tn_api_url,
|
| 214 |
+
tn_api_key=tn_api_key,
|
| 215 |
+
tn_model=tn_model,
|
| 216 |
+
tn_kwargs=tn_kwargs,
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
def _synthesize_one(
|
| 220 |
+
self,
|
| 221 |
+
text: str,
|
| 222 |
+
language: str,
|
| 223 |
+
prompt_text: str,
|
| 224 |
+
prompt_audio: torch.Tensor,
|
| 225 |
+
prompt_audio_sr: int,
|
| 226 |
+
**kwargs,
|
| 227 |
+
):
|
| 228 |
+
"""合成单句文本,返回 ``(gen_audio, gen_audio_sr)``。"""
|
| 229 |
+
return super().generate(
|
| 230 |
+
language=language,
|
| 231 |
+
prompt_text=prompt_text,
|
| 232 |
+
prompt_audio=prompt_audio,
|
| 233 |
+
prompt_audio_sr=prompt_audio_sr,
|
| 234 |
+
text=text,
|
| 235 |
+
**kwargs,
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
def generate(
|
| 239 |
+
self,
|
| 240 |
+
# Input
|
| 241 |
+
text: str,
|
| 242 |
+
language: Optional[str] = None,
|
| 243 |
+
prompt_text: str = "",
|
| 244 |
+
prompt_audio: Optional[torch.Tensor] = None,
|
| 245 |
+
prompt_audio_sr: Optional[int] = None,
|
| 246 |
+
# Inference
|
| 247 |
+
stop_threshold: float = 0.5,
|
| 248 |
+
n_timesteps: int = 10,
|
| 249 |
+
inference_cfg: float = 2.0,
|
| 250 |
+
seed: int = 1234,
|
| 251 |
+
# 文本前端
|
| 252 |
+
do_clean: bool = True,
|
| 253 |
+
do_tn: bool = True,
|
| 254 |
+
do_split: bool = True,
|
| 255 |
+
token_max_n: int = 80,
|
| 256 |
+
token_min_n: int = 60,
|
| 257 |
+
merge_len: int = 20,
|
| 258 |
+
# 拼接
|
| 259 |
+
cross_fade_ms: float = 50.0,
|
| 260 |
+
max_text_len: int = 300,
|
| 261 |
+
):
|
| 262 |
+
"""合成完整文本,自动清洗、切句、逐句生成并 cross-fading 拼接。"""
|
| 263 |
+
if not isinstance(text, str) or not text.strip():
|
| 264 |
+
raise ValueError("text must be a non-empty string")
|
| 265 |
+
|
| 266 |
+
text, language, sentences = self._apply_frontend(
|
| 267 |
+
text=text,
|
| 268 |
+
language=language,
|
| 269 |
+
do_clean=do_clean,
|
| 270 |
+
do_tn=do_tn,
|
| 271 |
+
do_split=do_split,
|
| 272 |
+
token_max_n=token_max_n,
|
| 273 |
+
token_min_n=token_min_n,
|
| 274 |
+
merge_len=merge_len,
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
gen_audio_sr = None
|
| 278 |
+
segments: List[torch.Tensor] = []
|
| 279 |
+
for i, sent in enumerate(sentences):
|
| 280 |
+
seg, seg_sr = self._synthesize_one(
|
| 281 |
+
text=sent,
|
| 282 |
+
language=language,
|
| 283 |
+
prompt_text=prompt_text,
|
| 284 |
+
prompt_audio=prompt_audio,
|
| 285 |
+
prompt_audio_sr=prompt_audio_sr,
|
| 286 |
+
stop_threshold=stop_threshold,
|
| 287 |
+
n_timesteps=n_timesteps,
|
| 288 |
+
inference_cfg=inference_cfg,
|
| 289 |
+
seed=seed,
|
| 290 |
+
)
|
| 291 |
+
gen_audio_sr = seg_sr
|
| 292 |
+
segments.append(seg.cpu())
|
| 293 |
+
|
| 294 |
+
gen_audio = segments[0]
|
| 295 |
+
if len(segments) > 1:
|
| 296 |
+
fade_len = int(cross_fade_ms / 1000.0 * gen_audio_sr)
|
| 297 |
+
for s in segments[1:]:
|
| 298 |
+
gen_audio = cross_fade(gen_audio, s, fade_len)
|
| 299 |
+
return gen_audio, gen_audio_sr
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
class FireRedTTS3Instruct(TextFrontendMixin, FireRedTTS3InstructBackend):
|
| 303 |
+
"""FireRedTTS3-Instruct:指令驱动的语音生成与编辑(含文本前端)。
|
| 304 |
+
|
| 305 |
+
在 :class:`fireredtts3.llm.fireredtts3_instruct.FireRedTTS3Instruct` 之上,
|
| 306 |
+
叠加与 :class:`FireRedTTS3` 一致的文本前端能力(清洗 / 语种判定 / TN /
|
| 307 |
+
拆句 + cross-fade 拼接),支持 4 类任务:
|
| 308 |
+
|
| 309 |
+
- ``generate_tts`` —— ICL 零样本语音克隆(参考音频 + 参考文本)
|
| 310 |
+
- ``generate_voice_design`` —— 音色设计:按自然语言音色描述生成新声音
|
| 311 |
+
- ``generate_semantic_edit`` —— 语义编辑:改词 / 插入 / 删除等内容级编辑
|
| 312 |
+
- ``generate_acoustic_edit`` —— 声学编辑:语速 / 音高 / 音量等声学属性编辑
|
| 313 |
+
"""
|
| 314 |
+
|
| 315 |
+
def __init__(
|
| 316 |
+
self,
|
| 317 |
+
pretrained_model_dir: str,
|
| 318 |
+
use_fasttext: bool = True,
|
| 319 |
+
use_llm_tn: bool = False,
|
| 320 |
+
use_wetext: bool = True,
|
| 321 |
+
tn_api_url: Optional[str] = None,
|
| 322 |
+
tn_api_key: Optional[str] = None,
|
| 323 |
+
tn_model: Optional[str] = None,
|
| 324 |
+
tn_kwargs: Optional[dict] = None,
|
| 325 |
+
):
|
| 326 |
+
super().__init__(pretrained_model_dir) # 先加载 RedAE + InstructCore + tokenizer
|
| 327 |
+
self._init_frontend(
|
| 328 |
+
use_fasttext=use_fasttext,
|
| 329 |
+
use_llm_tn=use_llm_tn,
|
| 330 |
+
use_wetext=use_wetext,
|
| 331 |
+
tn_api_url=tn_api_url,
|
| 332 |
+
tn_api_key=tn_api_key,
|
| 333 |
+
tn_model=tn_model,
|
| 334 |
+
tn_kwargs=tn_kwargs,
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
+
# ------------------------------------------------------------------ #
|
| 338 |
+
# 任务 1: ICL 零样本语音克隆
|
| 339 |
+
# ------------------------------------------------------------------ #
|
| 340 |
+
def generate_tts(
|
| 341 |
+
self,
|
| 342 |
+
prompt_text: str,
|
| 343 |
+
prompt_audio: torch.Tensor,
|
| 344 |
+
prompt_audio_sr: int,
|
| 345 |
+
text: str,
|
| 346 |
+
language: Optional[str] = None,
|
| 347 |
+
# Inference
|
| 348 |
+
stop_threshold: float = 0.5,
|
| 349 |
+
n_timesteps: int = 10,
|
| 350 |
+
inference_cfg: float = 2.0,
|
| 351 |
+
seed: int = 1234,
|
| 352 |
+
# 文本前端
|
| 353 |
+
do_clean: bool = True,
|
| 354 |
+
do_tn: bool = True,
|
| 355 |
+
do_split: bool = True,
|
| 356 |
+
token_max_n: int = 80,
|
| 357 |
+
token_min_n: int = 60,
|
| 358 |
+
merge_len: int = 20,
|
| 359 |
+
cross_fade_ms: float = 50.0,
|
| 360 |
+
):
|
| 361 |
+
"""ICL 零样本语音克隆(Instruct 版)。
|
| 362 |
+
|
| 363 |
+
Args:
|
| 364 |
+
prompt_text: 参考音频对应的文本转写。
|
| 365 |
+
prompt_audio: 参考音频波形 (1, T) 或 (C, T)。
|
| 366 |
+
prompt_audio_sr: 参考音频采样率。
|
| 367 |
+
text: 待合成文本;可包含多句(自动拆句 + cross-fade 拼接)。
|
| 368 |
+
language: 可选语种 tag,为 None 时自动判定。
|
| 369 |
+
"""
|
| 370 |
+
text, language, sentences = self._apply_frontend(
|
| 371 |
+
text=text,
|
| 372 |
+
language=language,
|
| 373 |
+
do_clean=do_clean,
|
| 374 |
+
do_tn=do_tn,
|
| 375 |
+
do_split=do_split,
|
| 376 |
+
token_max_n=token_max_n,
|
| 377 |
+
token_min_n=token_min_n,
|
| 378 |
+
merge_len=merge_len,
|
| 379 |
+
)
|
| 380 |
+
|
| 381 |
+
gen_audio_sr = None
|
| 382 |
+
segments: List[torch.Tensor] = []
|
| 383 |
+
for sent in sentences:
|
| 384 |
+
seg, seg_sr, _ = super().generate_tts(
|
| 385 |
+
prompt_text=prompt_text,
|
| 386 |
+
prompt_audio=prompt_audio,
|
| 387 |
+
prompt_audio_sr=prompt_audio_sr,
|
| 388 |
+
text=sent,
|
| 389 |
+
stop_threshold=stop_threshold,
|
| 390 |
+
n_timesteps=n_timesteps,
|
| 391 |
+
inference_cfg=inference_cfg,
|
| 392 |
+
seed=seed,
|
| 393 |
+
)
|
| 394 |
+
gen_audio_sr = seg_sr
|
| 395 |
+
segments.append(seg.cpu())
|
| 396 |
+
|
| 397 |
+
gen_audio = segments[0]
|
| 398 |
+
if len(segments) > 1:
|
| 399 |
+
fade_len = int(cross_fade_ms / 1000.0 * gen_audio_sr)
|
| 400 |
+
for s in segments[1:]:
|
| 401 |
+
gen_audio = cross_fade(gen_audio, s, fade_len)
|
| 402 |
+
return gen_audio, gen_audio_sr
|
| 403 |
+
|
| 404 |
+
# ------------------------------------------------------------------ #
|
| 405 |
+
# 任务 2: Voice Design —— 按音色描述生成新声音(无需参考音频)
|
| 406 |
+
# ------------------------------------------------------------------ #
|
| 407 |
+
def generate_voice_design(
|
| 408 |
+
self,
|
| 409 |
+
instruction: str,
|
| 410 |
+
text: str,
|
| 411 |
+
language: Optional[str] = None,
|
| 412 |
+
# Audio Inference Settings
|
| 413 |
+
n_timesteps: int = 10,
|
| 414 |
+
inference_cfg: float = 1.2,
|
| 415 |
+
# Random
|
| 416 |
+
seed: int = 2,
|
| 417 |
+
# 文本前端
|
| 418 |
+
do_clean: bool = True,
|
| 419 |
+
do_tn: bool = True,
|
| 420 |
+
do_split: bool = True,
|
| 421 |
+
token_max_n: int = 80,
|
| 422 |
+
token_min_n: int = 60,
|
| 423 |
+
merge_len: int = 20,
|
| 424 |
+
cross_fade_ms: float = 50.0,
|
| 425 |
+
):
|
| 426 |
+
"""Voice Design:从自然语言音色描述生成一段全新语音(无参考音频)。
|
| 427 |
+
|
| 428 |
+
模型先输出一段 CoT 语音属性规划(返回 ``gen_text``),再据此合成音频。
|
| 429 |
+
|
| 430 |
+
Args:
|
| 431 |
+
instruction: 音色描述,如“一个年轻女性的温柔嗓音,语速稍慢”。
|
| 432 |
+
text: 待合成文本;可包含多句(自动拆句 + 逐句生成 + cross-fade 拼接)。
|
| 433 |
+
language: 可选语种 tag,为 None 时自动判定(用于 TN)。
|
| 434 |
+
Returns:
|
| 435 |
+
``(gen_audio, gen_audio_sr, gen_text)``。gen_text 为模型输出的
|
| 436 |
+
语音属性规划(CoT)。
|
| 437 |
+
"""
|
| 438 |
+
text, language, sentences = self._apply_frontend(
|
| 439 |
+
text=text,
|
| 440 |
+
language=language,
|
| 441 |
+
do_clean=do_clean,
|
| 442 |
+
do_tn=do_tn,
|
| 443 |
+
do_split=do_split,
|
| 444 |
+
token_max_n=token_max_n,
|
| 445 |
+
token_min_n=token_min_n,
|
| 446 |
+
merge_len=merge_len,
|
| 447 |
+
)
|
| 448 |
+
|
| 449 |
+
gen_audio_sr = None
|
| 450 |
+
segments: List[torch.Tensor] = []
|
| 451 |
+
gen_text = None
|
| 452 |
+
for i, sent in enumerate(sentences):
|
| 453 |
+
seg, seg_sr, seg_text = super().generate_voice_design(
|
| 454 |
+
instruction=instruction,
|
| 455 |
+
text=sent,
|
| 456 |
+
n_timesteps=n_timesteps,
|
| 457 |
+
inference_cfg=inference_cfg,
|
| 458 |
+
seed=seed,
|
| 459 |
+
)
|
| 460 |
+
gen_audio_sr = seg_sr
|
| 461 |
+
segments.append(seg.cpu())
|
| 462 |
+
if i == 0:
|
| 463 |
+
gen_text = seg_text # CoT 规划只需取第一次
|
| 464 |
+
|
| 465 |
+
gen_audio = segments[0]
|
| 466 |
+
if len(segments) > 1:
|
| 467 |
+
fade_len = int(cross_fade_ms / 1000.0 * gen_audio_sr)
|
| 468 |
+
for s in segments[1:]:
|
| 469 |
+
gen_audio = cross_fade(gen_audio, s, fade_len)
|
| 470 |
+
return gen_audio, gen_audio_sr, gen_text
|
| 471 |
+
|
| 472 |
+
# ------------------------------------------------------------------ #
|
| 473 |
+
# 任务 3: Semantic Edit —— 内容级编辑(改词 / 插入 / 删除)
|
| 474 |
+
# ------------------------------------------------------------------ #
|
| 475 |
+
def generate_semantic_edit(
|
| 476 |
+
self,
|
| 477 |
+
instruction: str,
|
| 478 |
+
audio_in: torch.Tensor,
|
| 479 |
+
audio_in_sr: torch.Tensor,
|
| 480 |
+
n_timesteps: int = 10,
|
| 481 |
+
inference_cfg: float = 1.2,
|
| 482 |
+
seed: int = 1234,
|
| 483 |
+
):
|
| 484 |
+
"""Semantic Edit:按自然语言指令对音频进行内容级编辑。
|
| 485 |
+
|
| 486 |
+
支持插入 / 删除 / 替换等语义编辑,返回编辑后音频与模型生成的
|
| 487 |
+
编辑后文本(CoT,即 ``<|sot|>{rewritten text}<|eot|>``)。
|
| 488 |
+
|
| 489 |
+
Args:
|
| 490 |
+
instruction: 编辑指令,如 "insert '简直' after the character or word at index 8."。
|
| 491 |
+
audio_in: 输入音频波形。
|
| 492 |
+
audio_in_sr: 输入音频采样率。
|
| 493 |
+
Returns:
|
| 494 |
+
``(gen_audio, gen_audio_sr, gen_text)``。
|
| 495 |
+
"""
|
| 496 |
+
return super().generate_semantic_edit(
|
| 497 |
+
instruction=instruction,
|
| 498 |
+
audio_in=audio_in,
|
| 499 |
+
audio_in_sr=audio_in_sr,
|
| 500 |
+
n_timesteps=n_timesteps,
|
| 501 |
+
inference_cfg=inference_cfg,
|
| 502 |
+
seed=seed,
|
| 503 |
+
)
|
| 504 |
+
|
| 505 |
+
# ------------------------------------------------------------------ #
|
| 506 |
+
# 任务 4: Acoustic Edit —— 声学属性编辑(语速 / 音高 / 音量)
|
| 507 |
+
# ------------------------------------------------------------------ #
|
| 508 |
+
def generate_acoustic_edit(
|
| 509 |
+
self,
|
| 510 |
+
instruction: str,
|
| 511 |
+
audio_in: torch.Tensor,
|
| 512 |
+
audio_in_sr: torch.Tensor,
|
| 513 |
+
n_timesteps: int = 10,
|
| 514 |
+
inference_cfg: float = 1.2,
|
| 515 |
+
seed: int = 1234,
|
| 516 |
+
):
|
| 517 |
+
"""Acoustic Edit:按自然语言指令对音频进行声学属性编辑。
|
| 518 |
+
|
| 519 |
+
支持调整语速 / 音高 / 音量(如 "adjust the speed to 0.5x")。
|
| 520 |
+
|
| 521 |
+
Args:
|
| 522 |
+
instruction: 编辑指令,需使用模型训练模板,如
|
| 523 |
+
"adjust the speed to 0.5x" / "shift the pitch by 3 steps"。
|
| 524 |
+
audio_in: 输入音频波形。
|
| 525 |
+
audio_in_sr: 输入音频采样率。
|
| 526 |
+
Returns:
|
| 527 |
+
``(gen_audio, gen_audio_sr)``。
|
| 528 |
+
"""
|
| 529 |
+
return super().generate_acoustic_edit(
|
| 530 |
+
instruction=instruction,
|
| 531 |
+
audio_in=audio_in,
|
| 532 |
+
audio_in_sr=audio_in_sr,
|
| 533 |
+
n_timesteps=n_timesteps,
|
| 534 |
+
inference_cfg=inference_cfg,
|
| 535 |
+
seed=seed,
|
| 536 |
+
)
|
| 537 |
+
|
| 538 |
+
|
| 539 |
+
if __name__ == '__main__':
|
| 540 |
+
tts = FireRedTTS3('pretrained_models')
|
| 541 |
+
print('[INFO] FireRedTTS3 (text front-end) loaded')
|
| 542 |
+
|
| 543 |
+
prompt_audio_path = 'tests/prompts/default_prompt.wav'
|
| 544 |
+
prompt_text = '在欧洲行走简直就是走进汽车博览馆博览会,'
|
| 545 |
+
prompt_audio, prompt_audio_sr = torchaudio.load(prompt_audio_path)
|
| 546 |
+
|
| 547 |
+
text = '法院与不动产登记部门加强沟通,并督促银行提前办理抵押预约登记。我们也需要关注后续的进展。'
|
| 548 |
+
gen_audio, gen_audio_sr = tts.generate(
|
| 549 |
+
language=None, # 自动判定语种
|
| 550 |
+
prompt_text=prompt_text,
|
| 551 |
+
prompt_audio=prompt_audio,
|
| 552 |
+
prompt_audio_sr=prompt_audio_sr,
|
| 553 |
+
text=text,
|
| 554 |
+
)
|
| 555 |
+
torchaudio.save('gen.wav', gen_audio.cpu(), gen_audio_sr)
|
fireredtts3/llm/dit.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn.functional as F
|
| 4 |
+
from torch import nn
|
| 5 |
+
from fireredtts3.llm.modules import (
|
| 6 |
+
RotaryEmbedding,
|
| 7 |
+
TimestepEmbedder,
|
| 8 |
+
Attention,
|
| 9 |
+
FeedForward,
|
| 10 |
+
RMSNorm,
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class Transpose(torch.nn.Module):
|
| 15 |
+
def __init__(self, dim0: int, dim1: int):
|
| 16 |
+
super().__init__()
|
| 17 |
+
self.dim0 = dim0
|
| 18 |
+
self.dim1 = dim1
|
| 19 |
+
|
| 20 |
+
def forward(self, x: torch.Tensor):
|
| 21 |
+
x = torch.transpose(x, self.dim0, self.dim1)
|
| 22 |
+
return x
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class ConvBlock(nn.Module):
|
| 26 |
+
def __init__(
|
| 27 |
+
self,
|
| 28 |
+
in_channels: int,
|
| 29 |
+
out_channels: int,
|
| 30 |
+
kernel_size: int = 3,
|
| 31 |
+
):
|
| 32 |
+
super().__init__()
|
| 33 |
+
self.in_channels = in_channels
|
| 34 |
+
self.out_channels = out_channels
|
| 35 |
+
self.kernel_size = kernel_size
|
| 36 |
+
self.block = torch.nn.Sequential(
|
| 37 |
+
nn.Conv1d(
|
| 38 |
+
in_channels,
|
| 39 |
+
out_channels,
|
| 40 |
+
kernel_size=kernel_size,
|
| 41 |
+
padding=(kernel_size-1)//2
|
| 42 |
+
),
|
| 43 |
+
nn.Mish(),
|
| 44 |
+
nn.Conv1d(
|
| 45 |
+
out_channels,
|
| 46 |
+
out_channels,
|
| 47 |
+
kernel_size=kernel_size,
|
| 48 |
+
padding=(kernel_size-1)//2
|
| 49 |
+
),
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
def forward(self, x: torch.Tensor, mask: torch.Tensor = None):
|
| 53 |
+
"""
|
| 54 |
+
Args:
|
| 55 |
+
x: shape (b, t, c)
|
| 56 |
+
mask: shape (b, t, 1), default to None
|
| 57 |
+
"""
|
| 58 |
+
if mask is not None: x = x * mask
|
| 59 |
+
x = x.transpose(1, 2)
|
| 60 |
+
x = self.block(x)
|
| 61 |
+
x = x.transpose(1, 2)
|
| 62 |
+
if mask is not None: x = x * mask
|
| 63 |
+
return x
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def modulate(x, shift, scale):
|
| 67 |
+
return x * (1 + scale) + shift
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class FinalLayer(nn.Module):
|
| 71 |
+
"""
|
| 72 |
+
The final layer of DiT.
|
| 73 |
+
"""
|
| 74 |
+
def __init__(self, hidden_size, out_channels):
|
| 75 |
+
super().__init__()
|
| 76 |
+
self.adaLN_modulation = nn.Sequential(
|
| 77 |
+
nn.SiLU(),
|
| 78 |
+
nn.Linear(hidden_size, 2 * hidden_size, bias=True)
|
| 79 |
+
)
|
| 80 |
+
self.norm_final = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
| 81 |
+
self.linear = nn.Linear(hidden_size, out_channels, bias=True)
|
| 82 |
+
|
| 83 |
+
def forward(self, x, c):
|
| 84 |
+
shift, scale = self.adaLN_modulation(c).chunk(2, dim=-1)
|
| 85 |
+
x = modulate(self.norm_final(x), shift, scale)
|
| 86 |
+
x = self.linear(x)
|
| 87 |
+
return x
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class DiTBlock(nn.Module):
|
| 91 |
+
def __init__(
|
| 92 |
+
self,
|
| 93 |
+
hidden_size,
|
| 94 |
+
num_heads,
|
| 95 |
+
mlp_ratio=4.0,
|
| 96 |
+
dropout=0.1,
|
| 97 |
+
**kwargs
|
| 98 |
+
):
|
| 99 |
+
super().__init__()
|
| 100 |
+
# Attn
|
| 101 |
+
self.norm1 = RMSNorm(hidden_size, eps=1e-6)
|
| 102 |
+
self.attn = Attention(
|
| 103 |
+
dim=hidden_size,
|
| 104 |
+
heads=num_heads,
|
| 105 |
+
dim_head=hidden_size // num_heads,
|
| 106 |
+
dropout=dropout
|
| 107 |
+
)
|
| 108 |
+
# Conv
|
| 109 |
+
self.norm2 = RMSNorm(hidden_size, eps=1e-6)
|
| 110 |
+
self.conv = ConvBlock(in_channels=hidden_size, out_channels=hidden_size, kernel_size=3)
|
| 111 |
+
# FFN
|
| 112 |
+
self.norm3 = RMSNorm(hidden_size, eps=1e-6)
|
| 113 |
+
self.mlp = FeedForward(dim=hidden_size, mult=mlp_ratio, dropout=dropout, approximate="tanh")
|
| 114 |
+
# Time AdaLN condition
|
| 115 |
+
self.adaLN_modulation = nn.Sequential(
|
| 116 |
+
nn.SiLU(),
|
| 117 |
+
nn.Linear(hidden_size, 9 * hidden_size)
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
def forward(self, x: torch.Tensor, c: torch.Tensor, mask: torch.Tensor, rope: torch.Tensor):
|
| 121 |
+
"""
|
| 122 |
+
Args:
|
| 123 |
+
x(torch.Tensor): shape (b, t, c)
|
| 124 |
+
c(torch.Tensor): shae (b, 1, c), time condition
|
| 125 |
+
mask(torch.Tensor): default to None, DO NOT USE THIS ARG.
|
| 126 |
+
rope(torch.Tensor): positional embedding.
|
| 127 |
+
"""
|
| 128 |
+
# AdaLN for t
|
| 129 |
+
(
|
| 130 |
+
shift_msa, scale_msa, gate_msa,
|
| 131 |
+
shift_mlp, scale_mlp, gate_mlp,
|
| 132 |
+
shift_conv, scale_conv, gate_conv
|
| 133 |
+
) = self.adaLN_modulation(c).chunk(9, dim=-1)
|
| 134 |
+
# Attn
|
| 135 |
+
x = x + gate_msa * self.attn(
|
| 136 |
+
modulate(self.norm1(x), shift_msa, scale_msa),
|
| 137 |
+
mask=mask,
|
| 138 |
+
rope=rope
|
| 139 |
+
)
|
| 140 |
+
# Conv
|
| 141 |
+
x = x + gate_conv * self.conv(
|
| 142 |
+
modulate(self.norm2(x), shift_conv, scale_conv),
|
| 143 |
+
mask=mask,
|
| 144 |
+
)
|
| 145 |
+
# FFN
|
| 146 |
+
x = x + gate_mlp * self.mlp(
|
| 147 |
+
modulate(self.norm3(x), shift_mlp, scale_mlp)
|
| 148 |
+
)
|
| 149 |
+
return x
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
class DiT(nn.Module):
|
| 153 |
+
"""
|
| 154 |
+
Diffusion model with a Transformer backbone.
|
| 155 |
+
"""
|
| 156 |
+
def __init__(
|
| 157 |
+
self,
|
| 158 |
+
in_channels: int,
|
| 159 |
+
out_channels: int,
|
| 160 |
+
mlp_ratio: float = 4.0,
|
| 161 |
+
depth: int = 28,
|
| 162 |
+
num_heads: int = 8,
|
| 163 |
+
hidden_size: int = 256,
|
| 164 |
+
):
|
| 165 |
+
super().__init__()
|
| 166 |
+
self.in_channels = in_channels
|
| 167 |
+
self.out_channels = out_channels
|
| 168 |
+
self.in_proj = nn.Linear(in_channels, hidden_size)
|
| 169 |
+
self.t_embedder = TimestepEmbedder(hidden_size)
|
| 170 |
+
self.rotary_embed = RotaryEmbedding(hidden_size // num_heads)
|
| 171 |
+
self.blocks = nn.ModuleList([
|
| 172 |
+
DiTBlock(hidden_size, num_heads, mlp_ratio=mlp_ratio, dropout=0.0)
|
| 173 |
+
for _ in range(depth)
|
| 174 |
+
])
|
| 175 |
+
self.final_layer = FinalLayer(hidden_size, self.out_channels)
|
| 176 |
+
|
| 177 |
+
def forward(self, x:torch.Tensor, t:torch.Tensor):
|
| 178 |
+
"""Full attention DiT head.
|
| 179 |
+
|
| 180 |
+
Args:
|
| 181 |
+
x: shape (b, t, c), including xt and other conditions
|
| 182 |
+
t: shape (b,).
|
| 183 |
+
Returns:
|
| 184 |
+
pred: shape (b, t, c)
|
| 185 |
+
"""
|
| 186 |
+
# time
|
| 187 |
+
t = self.t_embedder(t.view(-1)).unsqueeze(1) # (b, 1, c)
|
| 188 |
+
x = self.in_proj(x)
|
| 189 |
+
rope = self.rotary_embed.forward_from_seq_len(x.shape[1])
|
| 190 |
+
for block in self.blocks:
|
| 191 |
+
x = block(x, t, mask=None, rope=rope)
|
| 192 |
+
x = self.final_layer(x, t)
|
| 193 |
+
return x
|
| 194 |
+
|
fireredtts3/llm/fireredtts3_base.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import torch
|
| 3 |
+
import torchaudio
|
| 4 |
+
import torch.nn.functional as F
|
| 5 |
+
from transformers import (
|
| 6 |
+
Qwen3Model, Qwen3Config,
|
| 7 |
+
PretrainedConfig, PreTrainedModel
|
| 8 |
+
)
|
| 9 |
+
from fireredtts3.llm.patch_encoder import PatchEncoder, RotaryEmbedding
|
| 10 |
+
from fireredtts3.llm.dit import DiT
|
| 11 |
+
from fireredtts3.redae.redae import RedAE
|
| 12 |
+
from fireredtts3.campp.campp import CamppEmbedding
|
| 13 |
+
from fireredtts3.utils.utils import fix_seed
|
| 14 |
+
from fireredtts3.utils.text_tokenizer import (
|
| 15 |
+
load_text_tokenizer,
|
| 16 |
+
MULTI_LANG_TAGS, MULTI_DIALECT_TAGS,
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
Qwen3_1_7B_ConfigDict = {
|
| 21 |
+
"architectures": [
|
| 22 |
+
"Qwen3ForCausalLM"
|
| 23 |
+
],
|
| 24 |
+
"attention_bias": False,
|
| 25 |
+
"attention_dropout": 0.0,
|
| 26 |
+
"bos_token_id": 151643,
|
| 27 |
+
"eos_token_id": 151645,
|
| 28 |
+
"head_dim": 128,
|
| 29 |
+
"hidden_act": "silu",
|
| 30 |
+
"hidden_size": 2048,
|
| 31 |
+
"initializer_range": 0.02,
|
| 32 |
+
"intermediate_size": 6144,
|
| 33 |
+
"max_position_embeddings": 40960,
|
| 34 |
+
"max_window_layers": 28,
|
| 35 |
+
"model_type": "qwen3",
|
| 36 |
+
"num_attention_heads": 16,
|
| 37 |
+
"num_hidden_layers": 28,
|
| 38 |
+
"num_key_value_heads": 8,
|
| 39 |
+
"rms_norm_eps": 1e-06,
|
| 40 |
+
"rope_scaling": None,
|
| 41 |
+
"rope_theta": 1000000,
|
| 42 |
+
"sliding_window": None,
|
| 43 |
+
"tie_word_embeddings": True,
|
| 44 |
+
"torch_dtype": "bfloat16",
|
| 45 |
+
"transformers_version": "5.6.2",
|
| 46 |
+
"use_cache": True,
|
| 47 |
+
"use_sliding_window": False,
|
| 48 |
+
"vocab_size": 151936,
|
| 49 |
+
"attn_implementation": "flash_attention_2",
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class FireRedTTS3BaseCoreConfig(PretrainedConfig):
|
| 54 |
+
def __init__(
|
| 55 |
+
self,
|
| 56 |
+
redae_dim: int = 64,
|
| 57 |
+
# Shared
|
| 58 |
+
num_history_patches: int = 2,
|
| 59 |
+
spk_in_dim: int = 512,
|
| 60 |
+
# PatchEncoder
|
| 61 |
+
patch_size: int = 4,
|
| 62 |
+
patch_encoder_hidden_size: int = 1024,
|
| 63 |
+
patch_encoder_mlp_ratio: int = 4,
|
| 64 |
+
patch_encoder_depth: int = 8,
|
| 65 |
+
patch_encoder_num_heads: int = 16,
|
| 66 |
+
# DiT
|
| 67 |
+
dit_mlp_ratio: int = 3,
|
| 68 |
+
dit_depth: int = 11,
|
| 69 |
+
dit_num_heads: int = 16,
|
| 70 |
+
dit_hidden_size: int = 1024,
|
| 71 |
+
# Other
|
| 72 |
+
**kwargs,
|
| 73 |
+
):
|
| 74 |
+
super().__init__(**kwargs)
|
| 75 |
+
self.redae_dim = redae_dim
|
| 76 |
+
# Shared
|
| 77 |
+
self.num_history_patches=num_history_patches
|
| 78 |
+
self.spk_in_dim=spk_in_dim
|
| 79 |
+
# PatchEncoder
|
| 80 |
+
self.patch_size=patch_size
|
| 81 |
+
self.patch_encoder_hidden_size=patch_encoder_hidden_size
|
| 82 |
+
self.patch_encoder_mlp_ratio=patch_encoder_mlp_ratio
|
| 83 |
+
self.patch_encoder_depth=patch_encoder_depth
|
| 84 |
+
self.patch_encoder_num_heads=patch_encoder_num_heads
|
| 85 |
+
# DiT
|
| 86 |
+
self.dit_mlp_ratio=dit_mlp_ratio
|
| 87 |
+
self.dit_depth=dit_depth
|
| 88 |
+
self.dit_num_heads=dit_num_heads
|
| 89 |
+
self.dit_hidden_size=dit_hidden_size
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class FireRedTTS3BaseCore(PreTrainedModel):
|
| 93 |
+
config_class = FireRedTTS3BaseCoreConfig
|
| 94 |
+
base_model_prefix = "fireredtts3_base_core"
|
| 95 |
+
|
| 96 |
+
_supports_flash_attn = True
|
| 97 |
+
_supports_sdpa = True
|
| 98 |
+
|
| 99 |
+
def __init__(self, config: FireRedTTS3BaseCoreConfig):
|
| 100 |
+
super().__init__(config)
|
| 101 |
+
# Backbone Transformer
|
| 102 |
+
self.backbone_llm_config = Qwen3Config.from_dict(Qwen3_1_7B_ConfigDict)
|
| 103 |
+
self.backbone_llm = Qwen3Model(self.backbone_llm_config)
|
| 104 |
+
# Speaker proj
|
| 105 |
+
self.spk_proj_llm = torch.nn.Linear(config.spk_in_dim, self.backbone_llm_config.hidden_size)
|
| 106 |
+
self.spk_proj_dit = torch.nn.Linear(config.spk_in_dim, config.spk_in_dim)
|
| 107 |
+
# PatchEncoder
|
| 108 |
+
self.patch_encoder = PatchEncoder(
|
| 109 |
+
in_dim=config.redae_dim,
|
| 110 |
+
out_dim=self.backbone_llm_config.hidden_size,
|
| 111 |
+
patch_size=config.patch_size,
|
| 112 |
+
hidden_size=config.patch_encoder_hidden_size,
|
| 113 |
+
mlp_ratio=config.patch_encoder_mlp_ratio,
|
| 114 |
+
depth=config.patch_encoder_depth,
|
| 115 |
+
num_heads=config.patch_encoder_num_heads,
|
| 116 |
+
)
|
| 117 |
+
# DiT
|
| 118 |
+
self.dit_head = torch.nn.Linear(self.backbone_llm_config.hidden_size, config.dit_hidden_size)
|
| 119 |
+
self.dit = DiT(
|
| 120 |
+
in_channels=(config.redae_dim+config.spk_in_dim+config.dit_hidden_size),
|
| 121 |
+
out_channels=config.redae_dim,
|
| 122 |
+
mlp_ratio=config.dit_mlp_ratio,
|
| 123 |
+
depth=config.dit_depth,
|
| 124 |
+
num_heads=config.dit_num_heads,
|
| 125 |
+
hidden_size=config.dit_hidden_size,
|
| 126 |
+
)
|
| 127 |
+
# Stop
|
| 128 |
+
self.stop_head = torch.nn.Linear(self.backbone_llm_config.hidden_size, 1)
|
| 129 |
+
# Shared
|
| 130 |
+
self.redae_dim = config.redae_dim
|
| 131 |
+
self.patch_size = self.patch_encoder.patch_size
|
| 132 |
+
self.history_patches = config.num_history_patches
|
| 133 |
+
self.history_length = config.num_history_patches * self.patch_size
|
| 134 |
+
self.post_init()
|
| 135 |
+
|
| 136 |
+
# Manually init RotaryEmbedding buffers
|
| 137 |
+
def _init_weights(self, module):
|
| 138 |
+
super()._init_weights(module)
|
| 139 |
+
if isinstance(module, RotaryEmbedding):
|
| 140 |
+
module.rope_init()
|
| 141 |
+
|
| 142 |
+
# Backbone Transformer AR wrapper
|
| 143 |
+
@torch.autocast(device_type='cuda', dtype=torch.bfloat16)
|
| 144 |
+
def _backbone_one_step(self, input_embeds: torch.Tensor, cache = None):
|
| 145 |
+
outs = self.backbone_llm.forward(
|
| 146 |
+
inputs_embeds=input_embeds,
|
| 147 |
+
use_cache=True,
|
| 148 |
+
past_key_values=cache,
|
| 149 |
+
)
|
| 150 |
+
# Only take the last timestep
|
| 151 |
+
hidden_states = outs.last_hidden_state
|
| 152 |
+
new_cache = outs.past_key_values
|
| 153 |
+
return hidden_states, new_cache
|
| 154 |
+
|
| 155 |
+
# Flow head wrapper
|
| 156 |
+
def _flow_one_step(
|
| 157 |
+
self,
|
| 158 |
+
hist_latents: torch.Tensor,
|
| 159 |
+
backbone_cond: torch.Tensor,
|
| 160 |
+
spk_cond: torch.Tensor,
|
| 161 |
+
t_span: torch.Tensor,
|
| 162 |
+
inference_cfg: float,
|
| 163 |
+
):
|
| 164 |
+
# Compose input
|
| 165 |
+
x0 = torch.randn(1, self.patch_size, self.redae_dim, device=hist_latents.device)
|
| 166 |
+
|
| 167 |
+
xt = torch.cat([hist_latents, x0], dim=1) # History clean + current noise
|
| 168 |
+
cond = torch.cat([
|
| 169 |
+
backbone_cond.repeat_interleave(self.patch_size, dim=1), # Correspond backbone cond
|
| 170 |
+
spk_cond.unsqueeze(1).repeat(1, self.history_length+self.patch_size, 1) # Spk
|
| 171 |
+
], dim=-1)
|
| 172 |
+
# Run flow inference
|
| 173 |
+
for ti, t in enumerate(t_span[:-1]):
|
| 174 |
+
dt = t_span[ti+1]-t
|
| 175 |
+
t_in = t.view(-1, 1, 1)
|
| 176 |
+
x_in = torch.cat([xt, cond], dim=2)
|
| 177 |
+
if inference_cfg > 0:
|
| 178 |
+
x_in_cfg = torch.cat([xt, cond * 0], dim=2)
|
| 179 |
+
x_in = torch.cat([x_in, x_in_cfg], dim=0)
|
| 180 |
+
t_in = t_in.expand(2, -1, -1)
|
| 181 |
+
vt = self.dit(x=x_in, t=t_in)
|
| 182 |
+
if inference_cfg > 0:
|
| 183 |
+
vt_cond, vt_cfg = vt.chunk(2, dim=0)
|
| 184 |
+
vt = (1.0 + inference_cfg) * vt_cond - inference_cfg * vt_cfg
|
| 185 |
+
# Only denoise current patch
|
| 186 |
+
xt[:, -self.patch_size:] = xt[:, -self.patch_size:] + dt.view(-1, 1, 1) * vt[:, -self.patch_size:]
|
| 187 |
+
# Remove history
|
| 188 |
+
x1 = xt[:, -self.patch_size:]
|
| 189 |
+
return x1
|
| 190 |
+
|
| 191 |
+
# Core LLM-DiT AR Loop
|
| 192 |
+
@torch.no_grad()
|
| 193 |
+
def generate(
|
| 194 |
+
self,
|
| 195 |
+
# Input
|
| 196 |
+
spk_emb: torch.Tensor,
|
| 197 |
+
text_tokens: torch.Tensor,
|
| 198 |
+
prompt_latents: torch.Tensor,
|
| 199 |
+
# Inference settings
|
| 200 |
+
n_timesteps: int = 10,
|
| 201 |
+
inference_cfg: float = 2.0,
|
| 202 |
+
stop_threshold: float = 0.5,
|
| 203 |
+
# Length control
|
| 204 |
+
min_gen_steps: int = 6,
|
| 205 |
+
max_gen_steps: int = None,
|
| 206 |
+
):
|
| 207 |
+
device = text_tokens.device
|
| 208 |
+
|
| 209 |
+
# Compose input sequence
|
| 210 |
+
input_embeds: torch.Tensor = self.backbone_llm.embed_tokens(text_tokens)
|
| 211 |
+
patch_prompt_latents: torch.Tensor = self.patch_encoder(prompt_latents)
|
| 212 |
+
spk_embs_llm = self.spk_proj_llm(spk_emb)
|
| 213 |
+
input_embeds = torch.cat([spk_embs_llm.unsqueeze(1), input_embeds, patch_prompt_latents], dim=1)
|
| 214 |
+
|
| 215 |
+
# Prepare DiT decode
|
| 216 |
+
t_span = torch.linspace(0, 1, n_timesteps + 1).to(device)
|
| 217 |
+
t_span = 1 - torch.cos(t_span * 0.5 * torch.pi) # (n_timesteps+1,)
|
| 218 |
+
latents_gen = F.pad(prompt_latents, (0, 0, self.history_length, 0))
|
| 219 |
+
|
| 220 |
+
# Prepare DiT conditions
|
| 221 |
+
dit_spk_cond = self.spk_proj_dit(spk_emb) # (b=1, c)
|
| 222 |
+
|
| 223 |
+
# Prepare Backbone states
|
| 224 |
+
backbone_cond = input_embeds.new_zeros(1, self.history_patches, input_embeds.shape[-1])
|
| 225 |
+
backbone_cache = None
|
| 226 |
+
|
| 227 |
+
max_gen_steps = (400 if max_gen_steps is None else max_gen_steps)
|
| 228 |
+
for step_index in range(max_gen_steps):
|
| 229 |
+
# Backbone condition
|
| 230 |
+
backbone_out, backbone_cache = self._backbone_one_step(input_embeds, cache=backbone_cache) # (b=1, t, c)
|
| 231 |
+
|
| 232 |
+
# Stop prediction
|
| 233 |
+
stop_logits = self.stop_head(backbone_out[:, -1]).squeeze(-1)
|
| 234 |
+
stop_score = torch.sigmoid(stop_logits).item()
|
| 235 |
+
if stop_score >= stop_threshold:
|
| 236 |
+
if min_gen_steps is not None:
|
| 237 |
+
if step_index >= min_gen_steps: break
|
| 238 |
+
else:
|
| 239 |
+
break
|
| 240 |
+
|
| 241 |
+
# DiT decode
|
| 242 |
+
if step_index == 0:
|
| 243 |
+
one_backbone_out = backbone_out[:, -patch_prompt_latents.shape[1]:]
|
| 244 |
+
else:
|
| 245 |
+
one_backbone_out = backbone_out[:, -1:]
|
| 246 |
+
backbone_cond = torch.cat([backbone_cond, one_backbone_out], dim=1)
|
| 247 |
+
|
| 248 |
+
one_latents = self._flow_one_step(
|
| 249 |
+
hist_latents=latents_gen[:, -self.history_length:],
|
| 250 |
+
backbone_cond=self.dit_head((backbone_cond[:, -(self.history_patches+1):])),
|
| 251 |
+
spk_cond=dit_spk_cond,
|
| 252 |
+
t_span=t_span,
|
| 253 |
+
inference_cfg=inference_cfg,
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
input_embeds = self.patch_encoder(one_latents)
|
| 257 |
+
latents_gen = torch.cat([latents_gen, one_latents], dim=1)
|
| 258 |
+
|
| 259 |
+
# Remove dummy history
|
| 260 |
+
latents_gen = latents_gen[:, self.history_length:]
|
| 261 |
+
return latents_gen
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
# RedAE + TextTokenizer + TTS3Core
|
| 265 |
+
class FireRedTTS3Base(object):
|
| 266 |
+
def __init__(self, pretrained_model_dir: str):
|
| 267 |
+
self.device = torch.device('cuda')
|
| 268 |
+
# RedAE
|
| 269 |
+
redae_model_dir = os.path.join(pretrained_model_dir, 'redae')
|
| 270 |
+
assert os.path.exists(redae_model_dir), f'{redae_model_dir} not found'
|
| 271 |
+
self.redae = RedAE.from_pretrained(redae_model_dir)
|
| 272 |
+
self.redae.to(self.device)
|
| 273 |
+
# LLM-DiT
|
| 274 |
+
tts_model_dir = os.path.join(pretrained_model_dir, 'fireredtts3_base')
|
| 275 |
+
assert os.path.exists(tts_model_dir), f'{tts_model_dir} not found'
|
| 276 |
+
self.tts_core = FireRedTTS3BaseCore.from_pretrained(tts_model_dir)
|
| 277 |
+
self.tts_core.to(self.device)
|
| 278 |
+
# Text Tokenizer
|
| 279 |
+
text_tok_dir = os.path.join(pretrained_model_dir, 'text_tokenizer')
|
| 280 |
+
assert os.path.exists(text_tok_dir), f'{text_tok_dir} not found'
|
| 281 |
+
self.text_tokenizer = load_text_tokenizer(text_tok_dir)
|
| 282 |
+
# Speaker
|
| 283 |
+
spk_ckpt_path = os.path.join(pretrained_model_dir, 'campp/campplus_voxceleb.bin')
|
| 284 |
+
assert os.path.exists(spk_ckpt_path), f'{spk_ckpt_path} not found'
|
| 285 |
+
self.spk_extractor = CamppEmbedding(spk_ckpt_path)
|
| 286 |
+
self.spk_extractor.to(self.device)
|
| 287 |
+
|
| 288 |
+
def _tokenize_text(self, text:str):
|
| 289 |
+
tokens = self.text_tokenizer(
|
| 290 |
+
text,
|
| 291 |
+
truncation=False, padding=False, add_special_tokens=False,
|
| 292 |
+
)["input_ids"]
|
| 293 |
+
tokens = torch.tensor([tokens], dtype=torch.long, device=self.device)
|
| 294 |
+
return tokens
|
| 295 |
+
|
| 296 |
+
@torch.inference_mode()
|
| 297 |
+
def generate(
|
| 298 |
+
self,
|
| 299 |
+
# Input
|
| 300 |
+
language: str,
|
| 301 |
+
prompt_text: str,
|
| 302 |
+
prompt_audio: torch.Tensor,
|
| 303 |
+
prompt_audio_sr: int,
|
| 304 |
+
text: str,
|
| 305 |
+
# Inference
|
| 306 |
+
stop_threshold: float = 0.5,
|
| 307 |
+
n_timesteps: int = 10,
|
| 308 |
+
inference_cfg: float = 2.0,
|
| 309 |
+
seed: int = 1234,
|
| 310 |
+
):
|
| 311 |
+
# ICL Text
|
| 312 |
+
lang_tag = f'<|{language}|>'
|
| 313 |
+
assert lang_tag in MULTI_LANG_TAGS+MULTI_DIALECT_TAGS, f'invalid language: {language}'
|
| 314 |
+
input_text = f'{lang_tag}<|sot|>{prompt_text}{text}<|eot|>'
|
| 315 |
+
text_tokens = self._tokenize_text(input_text)
|
| 316 |
+
# Prompt audio
|
| 317 |
+
prompt_audio = prompt_audio[:1]
|
| 318 |
+
prompt_audio = torchaudio.functional.resample(prompt_audio, prompt_audio_sr, self.redae.sample_rate)
|
| 319 |
+
prompt_audio_sr = self.redae.sample_rate
|
| 320 |
+
prompt_audio = self.redae.pad_to_multiple_of(prompt_audio, self.redae.downsample_rate*self.tts_core.patch_size)
|
| 321 |
+
prompt_audio = prompt_audio.to(self.device)
|
| 322 |
+
prompt_latents = self.redae.encode(prompt_audio, prompt_audio_sr)
|
| 323 |
+
prompt_latents = prompt_latents.to(torch.float32)
|
| 324 |
+
# Spk emb
|
| 325 |
+
spk_emb = self.spk_extractor.forward(prompt_audio, prompt_audio_sr)
|
| 326 |
+
spk_emb = spk_emb.to(self.device)
|
| 327 |
+
# TTS
|
| 328 |
+
if seed is not None:
|
| 329 |
+
fix_seed(seed)
|
| 330 |
+
gen_latents = self.tts_core.generate(
|
| 331 |
+
# Input
|
| 332 |
+
spk_emb=spk_emb,
|
| 333 |
+
text_tokens=text_tokens,
|
| 334 |
+
prompt_latents=prompt_latents,
|
| 335 |
+
# Inference settings
|
| 336 |
+
n_timesteps=n_timesteps,
|
| 337 |
+
inference_cfg=inference_cfg,
|
| 338 |
+
stop_threshold=stop_threshold,
|
| 339 |
+
# Length control
|
| 340 |
+
min_gen_steps=6,
|
| 341 |
+
max_gen_steps=None,
|
| 342 |
+
)
|
| 343 |
+
gen_audio, gen_audio_sr = self.redae.decode(gen_latents)
|
| 344 |
+
# Remove prompts
|
| 345 |
+
gen_audio = gen_audio[:, prompt_audio.shape[1]:]
|
| 346 |
+
return gen_audio, gen_audio_sr
|
fireredtts3/llm/fireredtts3_instruct.py
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import torch
|
| 3 |
+
import torchaudio
|
| 4 |
+
import torch.nn.functional as F
|
| 5 |
+
from transformers import (
|
| 6 |
+
Qwen3ForCausalLM, Qwen3Config,
|
| 7 |
+
PretrainedConfig, PreTrainedModel
|
| 8 |
+
)
|
| 9 |
+
from transformers.generation.logits_process import (
|
| 10 |
+
LogitsProcessorList,
|
| 11 |
+
RepetitionPenaltyLogitsProcessor,
|
| 12 |
+
TemperatureLogitsWarper,
|
| 13 |
+
TopKLogitsWarper,
|
| 14 |
+
TopPLogitsWarper,
|
| 15 |
+
)
|
| 16 |
+
from fireredtts3.llm.patch_encoder import PatchEncoder, RotaryEmbedding
|
| 17 |
+
from fireredtts3.llm.dit import DiT
|
| 18 |
+
from fireredtts3.redae.redae import RedAE
|
| 19 |
+
from fireredtts3.utils.utils import fix_seed
|
| 20 |
+
from fireredtts3.utils.text_tokenizer import load_text_tokenizer
|
| 21 |
+
from fireredtts3.llm.fireredtts3_base import Qwen3_1_7B_ConfigDict
|
| 22 |
+
from fireredtts3.utils.chatml import (
|
| 23 |
+
CHATML_LATENT_IN_PAD_ID,
|
| 24 |
+
CHATML_LATENT_OUT_PAD_ID,
|
| 25 |
+
compose_generate_input_tts,
|
| 26 |
+
compose_generate_input_voice_design,
|
| 27 |
+
compose_generate_input_semantic_edit,
|
| 28 |
+
compose_generate_input_acoustic_edit,
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
TEXT_EOT_ID: int = 151677
|
| 33 |
+
AUDIO_SOS_ID: int = 151669
|
| 34 |
+
REDAE_SCALE = 0.4
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def init_text_logits_processor(
|
| 38 |
+
repetition_penalty: float = None,
|
| 39 |
+
do_sample: bool = True,
|
| 40 |
+
temperature: float = None,
|
| 41 |
+
top_p: float = None,
|
| 42 |
+
top_k: int = None,
|
| 43 |
+
):
|
| 44 |
+
text_logits_processor = LogitsProcessorList()
|
| 45 |
+
if repetition_penalty is not None and repetition_penalty != 1.0:
|
| 46 |
+
text_logits_processor.append(RepetitionPenaltyLogitsProcessor(penalty=repetition_penalty))
|
| 47 |
+
if do_sample:
|
| 48 |
+
if temperature is not None and temperature != 1.0:
|
| 49 |
+
text_logits_processor.append(TemperatureLogitsWarper(temperature=float(temperature)))
|
| 50 |
+
if top_k is not None and top_k > 0:
|
| 51 |
+
text_logits_processor.append(TopKLogitsWarper(top_k=top_k))
|
| 52 |
+
if top_p is not None and top_p < 1.0:
|
| 53 |
+
text_logits_processor.append(TopPLogitsWarper(top_p=top_p))
|
| 54 |
+
return text_logits_processor
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class FireRedTTS3InstructCoreConfig(PretrainedConfig):
|
| 58 |
+
def __init__(
|
| 59 |
+
self,
|
| 60 |
+
redae_dim: int = 64,
|
| 61 |
+
# Shared
|
| 62 |
+
num_history_patches: int = 2,
|
| 63 |
+
# PatchEncoder
|
| 64 |
+
patch_size: int = 4,
|
| 65 |
+
patch_encoder_hidden_size: int = 1024,
|
| 66 |
+
patch_encoder_mlp_ratio: int = 4,
|
| 67 |
+
patch_encoder_depth: int = 8,
|
| 68 |
+
patch_encoder_num_heads: int = 16,
|
| 69 |
+
# DiT
|
| 70 |
+
dit_mlp_ratio: int = 3,
|
| 71 |
+
dit_depth: int = 11,
|
| 72 |
+
dit_num_heads: int = 16,
|
| 73 |
+
dit_hidden_size: int = 1024,
|
| 74 |
+
# Other
|
| 75 |
+
**kwargs,
|
| 76 |
+
):
|
| 77 |
+
super().__init__(**kwargs)
|
| 78 |
+
self.redae_dim = redae_dim
|
| 79 |
+
# Shared
|
| 80 |
+
self.num_history_patches=num_history_patches
|
| 81 |
+
# PatchEncoder
|
| 82 |
+
self.patch_size=patch_size
|
| 83 |
+
self.patch_encoder_hidden_size=patch_encoder_hidden_size
|
| 84 |
+
self.patch_encoder_mlp_ratio=patch_encoder_mlp_ratio
|
| 85 |
+
self.patch_encoder_depth=patch_encoder_depth
|
| 86 |
+
self.patch_encoder_num_heads=patch_encoder_num_heads
|
| 87 |
+
# DiT
|
| 88 |
+
self.dit_mlp_ratio=dit_mlp_ratio
|
| 89 |
+
self.dit_depth=dit_depth
|
| 90 |
+
self.dit_num_heads=dit_num_heads
|
| 91 |
+
self.dit_hidden_size=dit_hidden_size
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class FireRedTTS3InstructCore(PreTrainedModel):
|
| 95 |
+
config_class = FireRedTTS3InstructCoreConfig
|
| 96 |
+
base_model_prefix = "fireredtts3_instruct_core"
|
| 97 |
+
|
| 98 |
+
_supports_flash_attn = True
|
| 99 |
+
_supports_sdpa = True
|
| 100 |
+
|
| 101 |
+
def __init__(self, config: FireRedTTS3InstructCoreConfig):
|
| 102 |
+
super().__init__(config)
|
| 103 |
+
# Backbone Transformer
|
| 104 |
+
self.backbone_llm_config = Qwen3Config.from_dict(Qwen3_1_7B_ConfigDict)
|
| 105 |
+
self.backbone_llm = Qwen3ForCausalLM(self.backbone_llm_config)
|
| 106 |
+
# PatchEncoder
|
| 107 |
+
self.patch_encoder = PatchEncoder(
|
| 108 |
+
in_dim=config.redae_dim,
|
| 109 |
+
out_dim=self.backbone_llm_config.hidden_size,
|
| 110 |
+
patch_size=config.patch_size,
|
| 111 |
+
hidden_size=config.patch_encoder_hidden_size,
|
| 112 |
+
mlp_ratio=config.patch_encoder_mlp_ratio,
|
| 113 |
+
depth=config.patch_encoder_depth,
|
| 114 |
+
num_heads=config.patch_encoder_num_heads,
|
| 115 |
+
)
|
| 116 |
+
# DiT
|
| 117 |
+
self.dit_head = torch.nn.Linear(self.backbone_llm_config.hidden_size, config.dit_hidden_size)
|
| 118 |
+
self.dit = DiT(
|
| 119 |
+
in_channels=(config.redae_dim+config.dit_hidden_size),
|
| 120 |
+
out_channels=config.redae_dim,
|
| 121 |
+
mlp_ratio=config.dit_mlp_ratio,
|
| 122 |
+
depth=config.dit_depth,
|
| 123 |
+
num_heads=config.dit_num_heads,
|
| 124 |
+
hidden_size=config.dit_hidden_size,
|
| 125 |
+
)
|
| 126 |
+
# Stop
|
| 127 |
+
self.stop_head = torch.nn.Linear(self.backbone_llm_config.hidden_size, 1)
|
| 128 |
+
# Shared
|
| 129 |
+
self.redae_dim = config.redae_dim
|
| 130 |
+
self.patch_size = self.patch_encoder.patch_size
|
| 131 |
+
self.history_patches = config.num_history_patches
|
| 132 |
+
self.history_length = config.num_history_patches * self.patch_size
|
| 133 |
+
self.post_init()
|
| 134 |
+
|
| 135 |
+
# Manually init RotaryEmbedding buffers
|
| 136 |
+
def _init_weights(self, module):
|
| 137 |
+
super()._init_weights(module)
|
| 138 |
+
if isinstance(module, RotaryEmbedding):
|
| 139 |
+
module.rope_init()
|
| 140 |
+
|
| 141 |
+
# Backbone Transformer AR wrapper
|
| 142 |
+
@torch.autocast(device_type='cuda', dtype=torch.bfloat16)
|
| 143 |
+
def _backbone_one_step(self, input_embeds: torch.Tensor, cache = None):
|
| 144 |
+
outs = self.backbone_llm.model.forward(
|
| 145 |
+
inputs_embeds=input_embeds,
|
| 146 |
+
use_cache=True,
|
| 147 |
+
past_key_values=cache,
|
| 148 |
+
)
|
| 149 |
+
# Only take the last timestep
|
| 150 |
+
hidden_states = outs.last_hidden_state
|
| 151 |
+
new_cache = outs.past_key_values
|
| 152 |
+
return hidden_states, new_cache
|
| 153 |
+
|
| 154 |
+
# Flow head wrapper
|
| 155 |
+
def _flow_one_step(
|
| 156 |
+
self,
|
| 157 |
+
hist_latents: torch.Tensor,
|
| 158 |
+
backbone_cond: torch.Tensor,
|
| 159 |
+
t_span: torch.Tensor,
|
| 160 |
+
inference_cfg: float,
|
| 161 |
+
):
|
| 162 |
+
# Compose input
|
| 163 |
+
x0 = torch.randn(1, self.patch_size, self.redae_dim, device=hist_latents.device)
|
| 164 |
+
|
| 165 |
+
xt = torch.cat([hist_latents, x0], dim=1) # History clean + current noise
|
| 166 |
+
cond = backbone_cond.repeat_interleave(self.patch_size, dim=1) # Correspond backbone cond
|
| 167 |
+
# Run flow inference
|
| 168 |
+
for ti, t in enumerate(t_span[:-1]):
|
| 169 |
+
dt = t_span[ti+1]-t
|
| 170 |
+
t_in = t.view(-1, 1, 1)
|
| 171 |
+
x_in = torch.cat([xt, cond], dim=2)
|
| 172 |
+
if inference_cfg > 0:
|
| 173 |
+
x_in_cfg = torch.cat([xt, cond * 0], dim=2)
|
| 174 |
+
x_in = torch.cat([x_in, x_in_cfg], dim=0)
|
| 175 |
+
t_in = t_in.expand(2, -1, -1)
|
| 176 |
+
vt = self.dit(x=x_in, t=t_in)
|
| 177 |
+
if inference_cfg > 0:
|
| 178 |
+
vt_cond, vt_cfg = vt.chunk(2, dim=0)
|
| 179 |
+
vt = (1.0 + inference_cfg) * vt_cond - inference_cfg * vt_cfg
|
| 180 |
+
# Only denoise current patch
|
| 181 |
+
xt[:, -self.patch_size:] = xt[:, -self.patch_size:] + dt.view(-1, 1, 1) * vt[:, -self.patch_size:]
|
| 182 |
+
# Remove history
|
| 183 |
+
x1 = xt[:, -self.patch_size:]
|
| 184 |
+
return x1
|
| 185 |
+
|
| 186 |
+
# Core LLM-DiT AR Loop
|
| 187 |
+
@torch.no_grad()
|
| 188 |
+
def generate(
|
| 189 |
+
self,
|
| 190 |
+
# Input
|
| 191 |
+
text_tokens: torch.Tensor,
|
| 192 |
+
latents_in: torch.Tensor = None,
|
| 193 |
+
latents_in_mask: torch.Tensor = None, # For filling text_tokens
|
| 194 |
+
# Output (ICL)
|
| 195 |
+
latents_out: torch.Tensor = None,
|
| 196 |
+
latents_out_mask: torch.Tensor = None, # For filling text_tokens
|
| 197 |
+
# Text inference settings
|
| 198 |
+
infer_text: bool = False,
|
| 199 |
+
text_repetition_penalty: float = None,
|
| 200 |
+
text_do_sample: bool = True,
|
| 201 |
+
text_temperature: float = None,
|
| 202 |
+
text_top_p: float = None,
|
| 203 |
+
text_top_k: int = None,
|
| 204 |
+
# Audio inference settings
|
| 205 |
+
n_timesteps: int = 10,
|
| 206 |
+
inference_cfg: float = 2.0,
|
| 207 |
+
stop_threshold: float = 0.5,
|
| 208 |
+
# Audio length control
|
| 209 |
+
min_gen_steps: int = 6,
|
| 210 |
+
max_gen_steps: int = None,
|
| 211 |
+
):
|
| 212 |
+
device = text_tokens.device
|
| 213 |
+
|
| 214 |
+
# Compose input sequence
|
| 215 |
+
input_embeds: torch.Tensor = self.backbone_llm.model.embed_tokens(text_tokens)
|
| 216 |
+
# Any input audio
|
| 217 |
+
if latents_in is not None:
|
| 218 |
+
latents_patch_in: torch.Tensor = self.patch_encoder(latents_in)
|
| 219 |
+
input_embeds = input_embeds.masked_scatter(
|
| 220 |
+
latents_in_mask.unsqueeze(-1),
|
| 221 |
+
latents_patch_in.reshape(-1).to(input_embeds),
|
| 222 |
+
)
|
| 223 |
+
# Any output audio (ICL prompt)
|
| 224 |
+
if latents_out is not None:
|
| 225 |
+
latents_patch_out = self.patch_encoder.forward(latents_out)
|
| 226 |
+
input_embeds = input_embeds.masked_scatter(
|
| 227 |
+
latents_out_mask.unsqueeze(-1),
|
| 228 |
+
latents_patch_out.reshape(-1).to(input_embeds),
|
| 229 |
+
)
|
| 230 |
+
else:
|
| 231 |
+
latents_out = torch.zeros(1, 0, self.config.redae_dim, device=device)
|
| 232 |
+
latents_patch_out = None
|
| 233 |
+
|
| 234 |
+
# Prepare DiT decode
|
| 235 |
+
t_span = torch.linspace(0, 1, n_timesteps + 1).to(device)
|
| 236 |
+
t_span = 1 - torch.cos(t_span * 0.5 * torch.pi) # (n_timesteps+1,)
|
| 237 |
+
|
| 238 |
+
# Init Backbone states
|
| 239 |
+
backbone_cache = None
|
| 240 |
+
|
| 241 |
+
# --- Infer text
|
| 242 |
+
if infer_text:
|
| 243 |
+
# Text logits processor
|
| 244 |
+
text_logits_processor = init_text_logits_processor(
|
| 245 |
+
text_repetition_penalty, text_do_sample,
|
| 246 |
+
text_temperature, text_top_p, text_top_k,
|
| 247 |
+
)
|
| 248 |
+
text_gen_ids = torch.empty((1, 0), dtype=torch.long, device=device)
|
| 249 |
+
for text_step_index in range(200):
|
| 250 |
+
backbone_out, backbone_cache = self._backbone_one_step(input_embeds, cache=backbone_cache) # (b=1, t, c)
|
| 251 |
+
# Sampling
|
| 252 |
+
logits = self.backbone_llm.lm_head(backbone_out[:, -1, :]) # (1, V)
|
| 253 |
+
scores = text_logits_processor(text_gen_ids, logits)
|
| 254 |
+
if text_do_sample:
|
| 255 |
+
probs = torch.softmax(scores.float(), dim=-1)
|
| 256 |
+
next_token = torch.multinomial(probs, num_samples=1)[:, 0] # (1,)
|
| 257 |
+
else:
|
| 258 |
+
next_token = scores.argmax(dim=-1) # (1,)
|
| 259 |
+
# Next step
|
| 260 |
+
input_embeds = self.backbone_llm.model.embed_tokens(next_token.unsqueeze(0))
|
| 261 |
+
# Whether stop
|
| 262 |
+
if next_token.item() == TEXT_EOT_ID:
|
| 263 |
+
break
|
| 264 |
+
text_gen_ids = torch.cat([text_gen_ids, next_token.unsqueeze(0)], dim=1)
|
| 265 |
+
# Finalize text inference
|
| 266 |
+
_, backbone_cache = self._backbone_one_step(input_embeds, cache=backbone_cache) # (b=1, t, c)
|
| 267 |
+
# Process <|sosp|> for audio start
|
| 268 |
+
next_token = next_token * 0 + AUDIO_SOS_ID
|
| 269 |
+
input_embeds = self.backbone_llm.model.embed_tokens(next_token.unsqueeze(0))
|
| 270 |
+
_, backbone_cache = self._backbone_one_step(input_embeds, cache=backbone_cache) # (b=1, t, c)
|
| 271 |
+
|
| 272 |
+
# --- Infer audio
|
| 273 |
+
latents_gen = F.pad(latents_out, (0, 0, self.history_length, 0))
|
| 274 |
+
backbone_cond = input_embeds.new_zeros(1, self.history_patches, input_embeds.shape[-1])
|
| 275 |
+
|
| 276 |
+
max_gen_steps = (400 if max_gen_steps is None else max_gen_steps)
|
| 277 |
+
for step_index in range(max_gen_steps):
|
| 278 |
+
# Backbone condition
|
| 279 |
+
backbone_out, backbone_cache = self._backbone_one_step(input_embeds, cache=backbone_cache) # (b=1, t, c)
|
| 280 |
+
|
| 281 |
+
# Stop prediction
|
| 282 |
+
stop_logits = self.stop_head(backbone_out[:, -1]).squeeze(-1)
|
| 283 |
+
stop_score = torch.sigmoid(stop_logits).item()
|
| 284 |
+
if stop_score >= stop_threshold:
|
| 285 |
+
if min_gen_steps is not None:
|
| 286 |
+
if step_index >= min_gen_steps: break
|
| 287 |
+
else:
|
| 288 |
+
break
|
| 289 |
+
|
| 290 |
+
# DiT decode
|
| 291 |
+
if step_index == 0 and latents_patch_out is not None:
|
| 292 |
+
one_backbone_out = backbone_out[:, -latents_patch_out.shape[1]:]
|
| 293 |
+
else:
|
| 294 |
+
one_backbone_out = backbone_out[:, -1:]
|
| 295 |
+
backbone_cond = torch.cat([backbone_cond, one_backbone_out], dim=1)
|
| 296 |
+
|
| 297 |
+
one_latents = self._flow_one_step(
|
| 298 |
+
hist_latents=latents_gen[:, -self.history_length:],
|
| 299 |
+
backbone_cond=self.dit_head((backbone_cond[:, -(self.history_patches+1):])),
|
| 300 |
+
t_span=t_span,
|
| 301 |
+
inference_cfg=inference_cfg,
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
input_embeds = self.patch_encoder(one_latents)
|
| 305 |
+
latents_gen = torch.cat([latents_gen, one_latents], dim=1)
|
| 306 |
+
|
| 307 |
+
# Remove dummy history
|
| 308 |
+
latents_gen = latents_gen[:, self.history_length:]
|
| 309 |
+
|
| 310 |
+
if infer_text:
|
| 311 |
+
return latents_gen, text_gen_ids
|
| 312 |
+
else:
|
| 313 |
+
return latents_gen
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
# RedAE + TextTokenizer + TTS3Core
|
| 317 |
+
class FireRedTTS3Instruct(object):
|
| 318 |
+
def __init__(self, pretrained_model_dir: str):
|
| 319 |
+
self.device = torch.device('cuda')
|
| 320 |
+
# RedAE
|
| 321 |
+
redae_model_dir = os.path.join(pretrained_model_dir, 'redae')
|
| 322 |
+
assert os.path.exists(redae_model_dir), f'{redae_model_dir} not found'
|
| 323 |
+
self.redae = RedAE.from_pretrained(redae_model_dir)
|
| 324 |
+
self.redae.to(self.device)
|
| 325 |
+
# LLM-DiT
|
| 326 |
+
tts_model_dir = os.path.join(pretrained_model_dir, 'fireredtts3_instruct')
|
| 327 |
+
assert os.path.exists(tts_model_dir), f'{tts_model_dir} not found'
|
| 328 |
+
self.tts_core = FireRedTTS3InstructCore.from_pretrained(tts_model_dir)
|
| 329 |
+
self.tts_core.to(self.device)
|
| 330 |
+
# Text Tokenizer
|
| 331 |
+
text_tok_dir = os.path.join(pretrained_model_dir, 'text_tokenizer')
|
| 332 |
+
assert os.path.exists(text_tok_dir), f'{text_tok_dir} not found'
|
| 333 |
+
self.text_tokenizer = load_text_tokenizer(text_tok_dir)
|
| 334 |
+
|
| 335 |
+
def _tokenize_text(self, text:str):
|
| 336 |
+
tokens = self.text_tokenizer(
|
| 337 |
+
text,
|
| 338 |
+
truncation=False, padding=False, add_special_tokens=False,
|
| 339 |
+
)["input_ids"]
|
| 340 |
+
tokens = torch.tensor([tokens], dtype=torch.long, device=self.device)
|
| 341 |
+
return tokens
|
| 342 |
+
|
| 343 |
+
def _tokenize_audio(self, audio: torch.Tensor, audio_sr: int):
|
| 344 |
+
audio = audio[:1]
|
| 345 |
+
audio = torchaudio.functional.resample(audio, audio_sr, self.redae.sample_rate)
|
| 346 |
+
audio_sr = self.redae.sample_rate
|
| 347 |
+
audio = self.redae.pad_to_multiple_of(audio, self.redae.downsample_rate*self.tts_core.patch_size)
|
| 348 |
+
audio = audio.to(self.device)
|
| 349 |
+
latents = self.redae.encode(audio, audio_sr) * REDAE_SCALE
|
| 350 |
+
latents = latents.to(torch.float32)
|
| 351 |
+
return latents
|
| 352 |
+
|
| 353 |
+
# --- Inference Interface
|
| 354 |
+
@torch.inference_mode()
|
| 355 |
+
def generate_tts(
|
| 356 |
+
self,
|
| 357 |
+
# Input
|
| 358 |
+
prompt_text: str,
|
| 359 |
+
prompt_audio: torch.Tensor,
|
| 360 |
+
prompt_audio_sr: int,
|
| 361 |
+
text: str,
|
| 362 |
+
# Inference
|
| 363 |
+
stop_threshold: float = 0.5,
|
| 364 |
+
n_timesteps: int = 10,
|
| 365 |
+
inference_cfg: float = 2.0,
|
| 366 |
+
seed: int = 1234,
|
| 367 |
+
):
|
| 368 |
+
prompt_latents = self._tokenize_audio(prompt_audio, prompt_audio_sr)
|
| 369 |
+
text_in = compose_generate_input_tts(prompt_latents.shape[1]//self.tts_core.patch_size, prompt_text, text)
|
| 370 |
+
text_tokens = self._tokenize_text(text_in)
|
| 371 |
+
# AR generate
|
| 372 |
+
if seed is not None:
|
| 373 |
+
fix_seed(seed)
|
| 374 |
+
gen_latents = self.tts_core.generate(
|
| 375 |
+
# Input
|
| 376 |
+
text_tokens=text_tokens,
|
| 377 |
+
latents_out=prompt_latents,
|
| 378 |
+
latents_out_mask=(text_tokens==CHATML_LATENT_OUT_PAD_ID),
|
| 379 |
+
# Text inference settings
|
| 380 |
+
infer_text=False,
|
| 381 |
+
# Audio inference settings
|
| 382 |
+
n_timesteps=n_timesteps,
|
| 383 |
+
inference_cfg=inference_cfg,
|
| 384 |
+
stop_threshold=0.5,
|
| 385 |
+
# Audio length control
|
| 386 |
+
min_gen_steps=6,
|
| 387 |
+
max_gen_steps=None,
|
| 388 |
+
)
|
| 389 |
+
gen_audio, gen_audio_sr = self.redae.decode(gen_latents / REDAE_SCALE)
|
| 390 |
+
# Remove prompts
|
| 391 |
+
gen_audio = gen_audio[:, (self.redae.downsample_rate*prompt_latents.shape[1]):]
|
| 392 |
+
return gen_audio, gen_audio_sr
|
| 393 |
+
|
| 394 |
+
def generate_voice_design(
|
| 395 |
+
self,
|
| 396 |
+
# Input
|
| 397 |
+
instruction: str,
|
| 398 |
+
text: str,
|
| 399 |
+
# Audio Inference Settings
|
| 400 |
+
n_timesteps: int = 10,
|
| 401 |
+
inference_cfg: float = 1.2,
|
| 402 |
+
# Random
|
| 403 |
+
seed: int = 2,
|
| 404 |
+
):
|
| 405 |
+
text_in = compose_generate_input_voice_design(instruction, text)
|
| 406 |
+
text_tokens = self._tokenize_text(text_in)
|
| 407 |
+
|
| 408 |
+
# AR generate
|
| 409 |
+
if seed is not None:
|
| 410 |
+
fix_seed(seed)
|
| 411 |
+
gen_latents, gen_text_ids = self.tts_core.generate(
|
| 412 |
+
# Input
|
| 413 |
+
text_tokens=text_tokens,
|
| 414 |
+
# Text inference settings
|
| 415 |
+
infer_text=True,
|
| 416 |
+
text_repetition_penalty=1.0,
|
| 417 |
+
text_do_sample=True,
|
| 418 |
+
text_temperature=0.7,
|
| 419 |
+
text_top_p=0.8,
|
| 420 |
+
text_top_k=20,
|
| 421 |
+
# Audio inference settings
|
| 422 |
+
n_timesteps=n_timesteps,
|
| 423 |
+
inference_cfg=inference_cfg,
|
| 424 |
+
stop_threshold=0.5,
|
| 425 |
+
# Audio length control
|
| 426 |
+
min_gen_steps=6,
|
| 427 |
+
max_gen_steps=None,
|
| 428 |
+
)
|
| 429 |
+
gen_audio, gen_audio_sr = self.redae.decode(gen_latents / REDAE_SCALE)
|
| 430 |
+
gen_text = self.text_tokenizer.decode(gen_text_ids.squeeze(0).cpu())
|
| 431 |
+
|
| 432 |
+
return gen_audio, gen_audio_sr, gen_text
|
| 433 |
+
|
| 434 |
+
def generate_semantic_edit(
|
| 435 |
+
self,
|
| 436 |
+
# Input
|
| 437 |
+
instruction: str,
|
| 438 |
+
audio_in: torch.Tensor,
|
| 439 |
+
audio_in_sr: torch.Tensor,
|
| 440 |
+
# Audio Inference Settings
|
| 441 |
+
n_timesteps: int = 10,
|
| 442 |
+
inference_cfg: float = 1.2,
|
| 443 |
+
# Random
|
| 444 |
+
seed: int = 1234,
|
| 445 |
+
):
|
| 446 |
+
latents_in = self._tokenize_audio(audio_in, audio_in_sr)
|
| 447 |
+
text_in = compose_generate_input_semantic_edit(instruction, latents_in.shape[1]//self.tts_core.patch_size)
|
| 448 |
+
text_tokens = self._tokenize_text(text_in)
|
| 449 |
+
# AR generate
|
| 450 |
+
if seed is not None:
|
| 451 |
+
fix_seed(seed)
|
| 452 |
+
gen_latents, gen_text_ids = self.tts_core.generate(
|
| 453 |
+
# Input
|
| 454 |
+
text_tokens=text_tokens,
|
| 455 |
+
latents_in=latents_in,
|
| 456 |
+
latents_in_mask=(text_tokens==CHATML_LATENT_IN_PAD_ID),
|
| 457 |
+
# Text inference settings
|
| 458 |
+
infer_text=True,
|
| 459 |
+
text_repetition_penalty=1.0,
|
| 460 |
+
text_do_sample=False,
|
| 461 |
+
# Audio inference settings
|
| 462 |
+
n_timesteps=n_timesteps,
|
| 463 |
+
inference_cfg=inference_cfg,
|
| 464 |
+
stop_threshold=0.5,
|
| 465 |
+
# Audio length control
|
| 466 |
+
min_gen_steps=6,
|
| 467 |
+
max_gen_steps=None,
|
| 468 |
+
)
|
| 469 |
+
gen_audio, gen_audio_sr = self.redae.decode(gen_latents / REDAE_SCALE)
|
| 470 |
+
gen_text = self.text_tokenizer.decode(gen_text_ids.squeeze(0).cpu())
|
| 471 |
+
|
| 472 |
+
return gen_audio, gen_audio_sr, gen_text
|
| 473 |
+
|
| 474 |
+
def generate_acoustic_edit(
|
| 475 |
+
self,
|
| 476 |
+
# Input
|
| 477 |
+
instruction: str,
|
| 478 |
+
audio_in: torch.Tensor,
|
| 479 |
+
audio_in_sr: torch.Tensor,
|
| 480 |
+
# Audio Inference Settings
|
| 481 |
+
n_timesteps: int = 10,
|
| 482 |
+
inference_cfg: float = 1.2,
|
| 483 |
+
# Random
|
| 484 |
+
seed: int = 1234,
|
| 485 |
+
):
|
| 486 |
+
latents_in = self._tokenize_audio(audio_in, audio_in_sr)
|
| 487 |
+
text_in = compose_generate_input_acoustic_edit(instruction, latents_in.shape[1]//self.tts_core.patch_size)
|
| 488 |
+
text_tokens = self._tokenize_text(text_in)
|
| 489 |
+
# AR generate
|
| 490 |
+
if seed is not None:
|
| 491 |
+
fix_seed(seed)
|
| 492 |
+
gen_latents = self.tts_core.generate(
|
| 493 |
+
# Input
|
| 494 |
+
text_tokens=text_tokens,
|
| 495 |
+
latents_in=latents_in,
|
| 496 |
+
latents_in_mask=(text_tokens==CHATML_LATENT_IN_PAD_ID),
|
| 497 |
+
# Text inference settings
|
| 498 |
+
infer_text=False,
|
| 499 |
+
# Audio inference settings
|
| 500 |
+
n_timesteps=n_timesteps,
|
| 501 |
+
inference_cfg=inference_cfg,
|
| 502 |
+
stop_threshold=0.5,
|
| 503 |
+
# Audio length control
|
| 504 |
+
min_gen_steps=6,
|
| 505 |
+
max_gen_steps=None,
|
| 506 |
+
)
|
| 507 |
+
gen_audio, gen_audio_sr = self.redae.decode(gen_latents / REDAE_SCALE)
|
| 508 |
+
return gen_audio, gen_audio_sr
|
fireredtts3/llm/modules.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn.functional as F
|
| 4 |
+
from torch import nn
|
| 5 |
+
from fireredtts3.llm.rotary_embedding import (
|
| 6 |
+
apply_rotary_pos_emb,
|
| 7 |
+
RotaryEmbedding
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class SinusPositionEmbedding(nn.Module):
|
| 12 |
+
def __init__(self, dim):
|
| 13 |
+
super().__init__()
|
| 14 |
+
self.dim = dim
|
| 15 |
+
|
| 16 |
+
def forward(self, x, scale=1000):
|
| 17 |
+
device = x.device
|
| 18 |
+
half_dim = self.dim // 2
|
| 19 |
+
emb = math.log(10000) / (half_dim - 1)
|
| 20 |
+
emb = torch.exp(torch.arange(half_dim, device=device).float() * -emb)
|
| 21 |
+
emb = scale * x.unsqueeze(1) * emb.unsqueeze(0)
|
| 22 |
+
emb = torch.cat((emb.sin(), emb.cos()), dim=-1)
|
| 23 |
+
return emb
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class TimestepEmbedder(nn.Module):
|
| 27 |
+
def __init__(self, dim, freq_embed_dim=256):
|
| 28 |
+
super().__init__()
|
| 29 |
+
self.time_embed = SinusPositionEmbedding(freq_embed_dim)
|
| 30 |
+
self.time_mlp = nn.Sequential(
|
| 31 |
+
nn.Linear(freq_embed_dim, dim),
|
| 32 |
+
nn.SiLU(),
|
| 33 |
+
nn.Linear(dim, dim)
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
def forward(self, timestep):
|
| 37 |
+
time_hidden = self.time_embed(timestep)
|
| 38 |
+
time_hidden = time_hidden.to(timestep.dtype)
|
| 39 |
+
time = self.time_mlp(time_hidden) # b d
|
| 40 |
+
return time
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class RMSNorm(nn.Module):
|
| 44 |
+
def __init__(self, dim: int, eps: float):
|
| 45 |
+
super().__init__()
|
| 46 |
+
self.eps = eps
|
| 47 |
+
self.weight = nn.Parameter(torch.ones(dim))
|
| 48 |
+
self.native_rms_norm = float(torch.__version__[:3]) >= 2.4
|
| 49 |
+
|
| 50 |
+
def forward(self, x):
|
| 51 |
+
if self.native_rms_norm:
|
| 52 |
+
if self.weight.dtype in [torch.float16, torch.bfloat16]:
|
| 53 |
+
x = x.to(self.weight.dtype)
|
| 54 |
+
x = F.rms_norm(x, normalized_shape=(x.shape[-1],), weight=self.weight, eps=self.eps)
|
| 55 |
+
else:
|
| 56 |
+
variance = x.to(torch.float32).pow(2).mean(-1, keepdim=True)
|
| 57 |
+
x = x * torch.rsqrt(variance + self.eps)
|
| 58 |
+
if self.weight.dtype in [torch.float16, torch.bfloat16]:
|
| 59 |
+
x = x.to(self.weight.dtype)
|
| 60 |
+
x = x * self.weight
|
| 61 |
+
|
| 62 |
+
return x
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class FeedForward(nn.Module):
|
| 66 |
+
def __init__(self, dim, dim_out=None, mult=4, dropout=0.0, approximate: str = "none"):
|
| 67 |
+
super().__init__()
|
| 68 |
+
inner_dim = int(dim * mult)
|
| 69 |
+
dim_out = dim_out if dim_out is not None else dim
|
| 70 |
+
|
| 71 |
+
activation = nn.GELU(approximate=approximate)
|
| 72 |
+
project_in = nn.Sequential(nn.Linear(dim, inner_dim), activation)
|
| 73 |
+
self.ff = nn.Sequential(project_in, nn.Dropout(dropout), nn.Linear(inner_dim, dim_out))
|
| 74 |
+
|
| 75 |
+
def forward(self, x):
|
| 76 |
+
return self.ff(x)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class Attention(nn.Module):
|
| 80 |
+
def __init__(
|
| 81 |
+
self,
|
| 82 |
+
dim: int,
|
| 83 |
+
heads: int = 8,
|
| 84 |
+
dim_head: int = 64,
|
| 85 |
+
dropout: float = 0.0,
|
| 86 |
+
):
|
| 87 |
+
super().__init__()
|
| 88 |
+
|
| 89 |
+
if not hasattr(F, "scaled_dot_product_attention"):
|
| 90 |
+
raise ImportError("Attention equires PyTorch 2.0, to use it, please upgrade PyTorch to 2.0.")
|
| 91 |
+
|
| 92 |
+
self.dim = dim
|
| 93 |
+
self.heads = heads
|
| 94 |
+
self.inner_dim = dim_head * heads
|
| 95 |
+
self.dropout = dropout
|
| 96 |
+
self.to_q = nn.Linear(dim, self.inner_dim)
|
| 97 |
+
self.to_k = nn.Linear(dim, self.inner_dim)
|
| 98 |
+
self.to_v = nn.Linear(dim, self.inner_dim)
|
| 99 |
+
|
| 100 |
+
self.to_out = nn.ModuleList([])
|
| 101 |
+
self.to_out.append(nn.Linear(self.inner_dim, dim))
|
| 102 |
+
self.to_out.append(nn.Dropout(dropout))
|
| 103 |
+
|
| 104 |
+
def forward(
|
| 105 |
+
self,
|
| 106 |
+
x: float, # noised input x
|
| 107 |
+
mask=None,
|
| 108 |
+
rope=None, # rotary position embedding for x
|
| 109 |
+
) -> torch.Tensor:
|
| 110 |
+
|
| 111 |
+
batch_size = x.shape[0]
|
| 112 |
+
|
| 113 |
+
query = self.to_q(x)
|
| 114 |
+
key = self.to_k(x)
|
| 115 |
+
value = self.to_v(x)
|
| 116 |
+
|
| 117 |
+
# attention
|
| 118 |
+
inner_dim = key.shape[-1]
|
| 119 |
+
head_dim = inner_dim // self.heads
|
| 120 |
+
query = query.view(batch_size, -1, self.heads, head_dim).transpose(1, 2)
|
| 121 |
+
key = key.view(batch_size, -1, self.heads, head_dim).transpose(1, 2)
|
| 122 |
+
value = value.view(batch_size, -1, self.heads, head_dim).transpose(1, 2)
|
| 123 |
+
|
| 124 |
+
# apply rotary position embedding
|
| 125 |
+
if rope is not None:
|
| 126 |
+
freqs, xpos_scale = rope
|
| 127 |
+
q_xpos_scale, k_xpos_scale = (xpos_scale, xpos_scale**-1.0) if xpos_scale is not None else (1.0, 1.0)
|
| 128 |
+
query = apply_rotary_pos_emb(query, freqs, q_xpos_scale)
|
| 129 |
+
key = apply_rotary_pos_emb(key, freqs, k_xpos_scale)
|
| 130 |
+
|
| 131 |
+
# with sdpa_kernel(SDPBackend.EFFICIENT_ATTENTION):
|
| 132 |
+
x = F.scaled_dot_product_attention(query, key, value, attn_mask=None, dropout_p=0.0, is_causal=False)
|
| 133 |
+
|
| 134 |
+
x = x.transpose(1, 2).reshape(batch_size, -1, self.heads * head_dim)
|
| 135 |
+
x = x.to(query.dtype)
|
| 136 |
+
x = self.to_out[0](x)
|
| 137 |
+
x = self.to_out[1](x)
|
| 138 |
+
|
| 139 |
+
if mask is not None:
|
| 140 |
+
mask = mask.unsqueeze(-1)
|
| 141 |
+
x = x.masked_fill(~mask, 0.0)
|
| 142 |
+
|
| 143 |
+
return x
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
class DiTBlock(nn.Module):
|
| 147 |
+
def __init__(
|
| 148 |
+
self,
|
| 149 |
+
hidden_size,
|
| 150 |
+
num_heads,
|
| 151 |
+
mlp_ratio=4.0,
|
| 152 |
+
dropout=0.1,
|
| 153 |
+
**kwargs
|
| 154 |
+
):
|
| 155 |
+
super().__init__()
|
| 156 |
+
self.norm1 = RMSNorm(hidden_size, eps=1e-6)
|
| 157 |
+
self.attn = Attention(
|
| 158 |
+
dim=hidden_size,
|
| 159 |
+
heads=num_heads,
|
| 160 |
+
dim_head=hidden_size // num_heads,
|
| 161 |
+
dropout=dropout
|
| 162 |
+
)
|
| 163 |
+
self.norm2 = RMSNorm(hidden_size, eps=1e-6)
|
| 164 |
+
self.mlp = FeedForward(dim=hidden_size, mult=mlp_ratio, dropout=dropout, approximate="tanh")
|
| 165 |
+
|
| 166 |
+
def forward(self, x, mask, rope):
|
| 167 |
+
x = x + self.attn(self.norm1(x), mask=mask, rope=rope)
|
| 168 |
+
x = x + self.mlp(self.norm2(x))
|
| 169 |
+
return x
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
class FinalLayer(nn.Module):
|
| 173 |
+
"""
|
| 174 |
+
The final layer of DiT.
|
| 175 |
+
"""
|
| 176 |
+
def __init__(self, hidden_size, out_channels):
|
| 177 |
+
super().__init__()
|
| 178 |
+
self.norm_final = RMSNorm(hidden_size, eps=1e-6)
|
| 179 |
+
self.linear = nn.Linear(hidden_size, out_channels, bias=True)
|
| 180 |
+
|
| 181 |
+
def forward(self, x):
|
| 182 |
+
x = self.norm_final(x)
|
| 183 |
+
x = self.linear(x)
|
| 184 |
+
return x
|
| 185 |
+
|
fireredtts3/llm/patch_encoder.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from fireredtts3.llm.modules import (
|
| 3 |
+
DiTBlock,
|
| 4 |
+
FinalLayer,
|
| 5 |
+
RotaryEmbedding,
|
| 6 |
+
)
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class PatchEncoder(torch.nn.Module):
|
| 10 |
+
def __init__(
|
| 11 |
+
self,
|
| 12 |
+
# In & out
|
| 13 |
+
in_dim: int,
|
| 14 |
+
out_dim: int,
|
| 15 |
+
# Model config
|
| 16 |
+
patch_size: int = 4,
|
| 17 |
+
hidden_size: int = 1024,
|
| 18 |
+
mlp_ratio: int = 3,
|
| 19 |
+
depth: int = 8,
|
| 20 |
+
num_heads: int = 8,
|
| 21 |
+
):
|
| 22 |
+
super().__init__()
|
| 23 |
+
self.in_dim = in_dim
|
| 24 |
+
self.patch_size = patch_size
|
| 25 |
+
self.hidden_size = hidden_size
|
| 26 |
+
self.out_dim = out_dim
|
| 27 |
+
# [CLS] token
|
| 28 |
+
self.cls_tok = torch.nn.Parameter(torch.zeros(1, 1, hidden_size))
|
| 29 |
+
self.rotary_embed = RotaryEmbedding(hidden_size // num_heads)
|
| 30 |
+
self.blocks = torch.nn.ModuleList([
|
| 31 |
+
DiTBlock(hidden_size, num_heads, mlp_ratio=mlp_ratio) for _ in range(depth)
|
| 32 |
+
])
|
| 33 |
+
# Input & output proj
|
| 34 |
+
self.in_proj = (
|
| 35 |
+
torch.nn.Linear(in_dim, hidden_size)
|
| 36 |
+
if in_dim != hidden_size else
|
| 37 |
+
torch.nn.Identity()
|
| 38 |
+
)
|
| 39 |
+
self.out_proj = FinalLayer(hidden_size, out_dim)
|
| 40 |
+
|
| 41 |
+
def forward(self, inputs_embeds: torch.Tensor):
|
| 42 |
+
"""Patch encoder aggregating {patch_size} latents into one.
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
inputs_embeds(torch.Tensor): shape (b=1, t, c).
|
| 46 |
+
Returns:
|
| 47 |
+
hidden_states(torch.Tensor): shape (b=1, t//patch_size, c).
|
| 48 |
+
"""
|
| 49 |
+
assert inputs_embeds.shape[1] % self.patch_size == 0, \
|
| 50 |
+
'inputs_embeds.shape={} patch_size={}'.format(inputs_embeds.shape, self.patch_size)
|
| 51 |
+
|
| 52 |
+
inputs_embeds = self.in_proj(inputs_embeds)
|
| 53 |
+
# Patchify, (b=1, t, c) -> (t//patch_size, patch_size, c)
|
| 54 |
+
hidden_states = inputs_embeds.reshape(-1, self.patch_size, self.hidden_size)
|
| 55 |
+
cls_tok = self.cls_tok.expand(hidden_states.shape[0], -1, -1) # (b*t//patch_size, 1, c)
|
| 56 |
+
hidden_states = torch.cat([cls_tok, hidden_states], dim=1) # (b*t//patch_size, 1+patch_size, c)
|
| 57 |
+
# NOTE full attention
|
| 58 |
+
rope = self.rotary_embed.forward_from_seq_len(hidden_states.shape[1])
|
| 59 |
+
for block in self.blocks:
|
| 60 |
+
hidden_states = block(hidden_states, None, rope)
|
| 61 |
+
hidden_states = self.out_proj(hidden_states)
|
| 62 |
+
hidden_states = hidden_states[:, 0] # (t//patch_size, c)
|
| 63 |
+
hidden_states = hidden_states.unsqueeze(0)
|
| 64 |
+
return hidden_states
|
| 65 |
+
|
fireredtts3/llm/rotary_embedding.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from torch.nn import Module
|
| 3 |
+
from torch.amp import autocast
|
| 4 |
+
from torch import cat, stack, arange
|
| 5 |
+
from einops import rearrange
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class RotaryEmbedding(Module):
|
| 9 |
+
def __init__(
|
| 10 |
+
self,
|
| 11 |
+
dim,
|
| 12 |
+
interpolation_factor = 1.,
|
| 13 |
+
base = 10000,
|
| 14 |
+
base_rescale_factor = 1.
|
| 15 |
+
):
|
| 16 |
+
super().__init__()
|
| 17 |
+
# proposed by reddit user bloc97, to rescale rotary embeddings to longer sequence length without fine-tuning
|
| 18 |
+
# has some connection to NTK literature
|
| 19 |
+
# https://www.reddit.com/r/LocalLLaMA/comments/14lz7j5/ntkaware_scaled_rope_allows_llama_models_to_have/
|
| 20 |
+
base *= base_rescale_factor ** (dim / (dim - 2))
|
| 21 |
+
|
| 22 |
+
self._rope_dim = dim
|
| 23 |
+
self._rope_base = base
|
| 24 |
+
|
| 25 |
+
inv_freq = 1. / (base ** (arange(0, dim, 2).float() / dim))
|
| 26 |
+
self.register_buffer('inv_freq', inv_freq, persistent=False)
|
| 27 |
+
|
| 28 |
+
assert interpolation_factor >= 1.
|
| 29 |
+
self.interpolation_factor = interpolation_factor
|
| 30 |
+
|
| 31 |
+
def rope_init(self):
|
| 32 |
+
dim, base = self._rope_dim, self._rope_base
|
| 33 |
+
self.inv_freq = 1. / (base ** (arange(0, dim, 2).float() / dim))
|
| 34 |
+
|
| 35 |
+
def forward_from_seq_len(self, seq_len):
|
| 36 |
+
device = self.inv_freq.device
|
| 37 |
+
|
| 38 |
+
t = arange(seq_len, device = device)
|
| 39 |
+
return self.forward(t)
|
| 40 |
+
|
| 41 |
+
@autocast('cuda', enabled = False)
|
| 42 |
+
def forward(self, t, offset = 0):
|
| 43 |
+
if t.ndim == 1:
|
| 44 |
+
t = rearrange(t, 'n -> 1 n')
|
| 45 |
+
|
| 46 |
+
freqs = torch.einsum('b i , j -> b i j', t.type_as(self.inv_freq), self.inv_freq) / self.interpolation_factor
|
| 47 |
+
freqs = stack((freqs, freqs), dim = -1)
|
| 48 |
+
freqs = rearrange(freqs, '... d r -> ... (d r)')
|
| 49 |
+
|
| 50 |
+
return freqs, 1.
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def rotate_half(x):
|
| 54 |
+
x = rearrange(x, '... (d r) -> ... d r', r = 2)
|
| 55 |
+
x1, x2 = x.unbind(dim = -1)
|
| 56 |
+
x = stack((-x2, x1), dim = -1)
|
| 57 |
+
return rearrange(x, '... d r -> ... (d r)')
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@autocast('cuda', enabled = False)
|
| 61 |
+
def apply_rotary_pos_emb(t, freqs, scale = 1):
|
| 62 |
+
rot_dim, seq_len, orig_dtype = freqs.shape[-1], t.shape[-2], t.dtype
|
| 63 |
+
|
| 64 |
+
freqs = freqs[:, -seq_len:, :]
|
| 65 |
+
scale = scale[:, -seq_len:, :] if isinstance(scale, torch.Tensor) else scale
|
| 66 |
+
|
| 67 |
+
if t.ndim == 4 and freqs.ndim == 3:
|
| 68 |
+
freqs = rearrange(freqs, 'b n d -> b 1 n d')
|
| 69 |
+
|
| 70 |
+
# partial rotary embeddings, Wang et al. GPT-J
|
| 71 |
+
t, t_unrotated = t[..., :rot_dim], t[..., rot_dim:]
|
| 72 |
+
t = (t * freqs.cos() * scale) + (rotate_half(t) * freqs.sin() * scale)
|
| 73 |
+
out = cat((t, t_unrotated), dim = -1)
|
| 74 |
+
|
| 75 |
+
return out.type(orig_dtype)
|
fireredtts3/redae/redae.py
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import torch
|
| 3 |
+
import torchaudio
|
| 4 |
+
import torch.nn as nn
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
from transformers import (
|
| 7 |
+
Qwen3Config, Qwen3Model,
|
| 8 |
+
PretrainedConfig, PreTrainedModel
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
# --- Encoder
|
| 12 |
+
class Qwen3ClsDownsample(torch.nn.Module):
|
| 13 |
+
def __init__(
|
| 14 |
+
self,
|
| 15 |
+
in_dim: int,
|
| 16 |
+
out_dim: int,
|
| 17 |
+
downsample_rate: int = 2,
|
| 18 |
+
# Qwen
|
| 19 |
+
hidden_size: int = 896,
|
| 20 |
+
intermediate_size: int = 896*4,
|
| 21 |
+
num_hidden_layers: int = 4,
|
| 22 |
+
max_position_embeddings: int = 32768,
|
| 23 |
+
num_attention_heads: int = 14,
|
| 24 |
+
num_key_value_heads: int = 2,
|
| 25 |
+
):
|
| 26 |
+
super().__init__()
|
| 27 |
+
self.in_dim = in_dim
|
| 28 |
+
self.out_dim = out_dim
|
| 29 |
+
self.downsample_rate = downsample_rate
|
| 30 |
+
self.qwen3_config = Qwen3Config(
|
| 31 |
+
hidden_size=hidden_size,
|
| 32 |
+
intermediate_size=intermediate_size,
|
| 33 |
+
num_hidden_layers=num_hidden_layers,
|
| 34 |
+
max_position_embeddings=max_position_embeddings,
|
| 35 |
+
num_attention_heads=num_attention_heads,
|
| 36 |
+
num_key_value_heads=num_key_value_heads,
|
| 37 |
+
attn_implementation='flash_attention_2',
|
| 38 |
+
)
|
| 39 |
+
self.qwen3 = Qwen3Model(self.qwen3_config)
|
| 40 |
+
self.cls_tok = torch.nn.Parameter(torch.ones(1, 1, hidden_size))
|
| 41 |
+
# Input & output proj
|
| 42 |
+
self.in_proj = (
|
| 43 |
+
torch.nn.Linear(in_dim, hidden_size)
|
| 44 |
+
if in_dim != hidden_size else
|
| 45 |
+
torch.nn.Identity()
|
| 46 |
+
)
|
| 47 |
+
self.out_proj = (
|
| 48 |
+
torch.nn.Linear(hidden_size, out_dim)
|
| 49 |
+
if out_dim != hidden_size else
|
| 50 |
+
torch.nn.Identity()
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
def forward(self, xs: torch.Tensor):
|
| 54 |
+
"""Downsample latents as BERT [CLS] token
|
| 55 |
+
|
| 56 |
+
Args:
|
| 57 |
+
xs(torch.Tensor): shape (b, t, c).
|
| 58 |
+
Returns:
|
| 59 |
+
ys(torch.Tensor): shape (b, t//downsample_rate, c).
|
| 60 |
+
"""
|
| 61 |
+
assert xs.shape[1] % self.downsample_rate == 0, xs.shape
|
| 62 |
+
b = xs.shape[0]
|
| 63 |
+
# Patchify, (b, t, c) -> (b*t/down, down, c)
|
| 64 |
+
xs = xs.reshape(-1, self.downsample_rate, self.in_dim)
|
| 65 |
+
xs = self.in_proj(xs)
|
| 66 |
+
cls_tok = self.cls_tok.expand(xs.shape[0], -1, -1)
|
| 67 |
+
xs = torch.cat([xs, cls_tok], dim=1) # (b*t/down, down+1, c)
|
| 68 |
+
# Forward LLM
|
| 69 |
+
ys = []
|
| 70 |
+
for xs_chunk in torch.split(xs, 32768, dim=0): # FlashAttn limits
|
| 71 |
+
outs_chunk = self.qwen3(inputs_embeds=xs_chunk)
|
| 72 |
+
ys_chunk = outs_chunk.last_hidden_state[:, -1] # (b*t/down, c)
|
| 73 |
+
ys.append(ys_chunk)
|
| 74 |
+
ys = torch.cat(ys, dim=0)
|
| 75 |
+
# ---
|
| 76 |
+
ys = self.out_proj(ys)
|
| 77 |
+
ys = ys.reshape(b, -1, self.out_dim)
|
| 78 |
+
return ys
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class RedAEAudioEncoder(torch.nn.Module):
|
| 82 |
+
def __init__(
|
| 83 |
+
self,
|
| 84 |
+
# Output
|
| 85 |
+
out_dim: int = 1024,
|
| 86 |
+
# Input reshape
|
| 87 |
+
audio_patch_size: int = 480, # 50Hz
|
| 88 |
+
audio_sample_rate: int = 24000,
|
| 89 |
+
# Qwen
|
| 90 |
+
hidden_size: int = 896,
|
| 91 |
+
intermediate_size: int = 896*4,
|
| 92 |
+
num_hidden_layers: int = 24,
|
| 93 |
+
max_position_embeddings: int = 32768,
|
| 94 |
+
max_window_layers: int = 0,
|
| 95 |
+
num_attention_heads: int = 14,
|
| 96 |
+
num_key_value_heads: int = 2,
|
| 97 |
+
sliding_window: int = 64,
|
| 98 |
+
use_sliding_window: bool = True,
|
| 99 |
+
# Extra downsample
|
| 100 |
+
extra_downsample_rate: int = 2, # 50Hz -> 25Hz
|
| 101 |
+
downsample_num_hidden_layers: int = 4,
|
| 102 |
+
):
|
| 103 |
+
super().__init__()
|
| 104 |
+
self.audio_patch_size = audio_patch_size
|
| 105 |
+
self.audio_sample_rate = audio_sample_rate
|
| 106 |
+
self.extra_downsample_rate = extra_downsample_rate
|
| 107 |
+
self.out_dim = out_dim
|
| 108 |
+
self.in_proj = nn.Sequential(
|
| 109 |
+
nn.Linear(self.audio_patch_size, hidden_size),
|
| 110 |
+
nn.Linear(hidden_size, hidden_size),
|
| 111 |
+
)
|
| 112 |
+
self.qwen3_config = Qwen3Config(
|
| 113 |
+
hidden_size=hidden_size,
|
| 114 |
+
intermediate_size=intermediate_size,
|
| 115 |
+
num_hidden_layers=num_hidden_layers,
|
| 116 |
+
max_position_embeddings=max_position_embeddings,
|
| 117 |
+
max_window_layers=max_window_layers,
|
| 118 |
+
num_attention_heads=num_attention_heads,
|
| 119 |
+
num_key_value_heads=num_key_value_heads,
|
| 120 |
+
sliding_window=sliding_window,
|
| 121 |
+
use_sliding_window=use_sliding_window,
|
| 122 |
+
attn_implementation='flash_attention_2',
|
| 123 |
+
)
|
| 124 |
+
self.qwen3 = Qwen3Model(self.qwen3_config)
|
| 125 |
+
if self.extra_downsample_rate > 1:
|
| 126 |
+
self.downsample = Qwen3ClsDownsample(
|
| 127 |
+
in_dim=hidden_size,
|
| 128 |
+
out_dim=hidden_size,
|
| 129 |
+
downsample_rate=extra_downsample_rate,
|
| 130 |
+
hidden_size=hidden_size,
|
| 131 |
+
intermediate_size=intermediate_size,
|
| 132 |
+
num_hidden_layers=downsample_num_hidden_layers,
|
| 133 |
+
max_position_embeddings=max_position_embeddings,
|
| 134 |
+
num_attention_heads=num_attention_heads,
|
| 135 |
+
num_key_value_heads=num_key_value_heads,
|
| 136 |
+
)
|
| 137 |
+
self.out_proj = nn.Linear(hidden_size, out_dim)
|
| 138 |
+
|
| 139 |
+
# --- Some property
|
| 140 |
+
@property
|
| 141 |
+
def downsample_rate(self):
|
| 142 |
+
return int(self.audio_patch_size * self.extra_downsample_rate)
|
| 143 |
+
|
| 144 |
+
@property
|
| 145 |
+
def sample_rate(self):
|
| 146 |
+
return self.audio_sample_rate
|
| 147 |
+
|
| 148 |
+
@property
|
| 149 |
+
def hidden_size(self):
|
| 150 |
+
return self.out_dim
|
| 151 |
+
|
| 152 |
+
# --- Forward
|
| 153 |
+
def forward(self, audio: torch.Tensor):
|
| 154 |
+
"""
|
| 155 |
+
Args:
|
| 156 |
+
audio(torch.Tensor): shape (b, t)
|
| 157 |
+
Returns:
|
| 158 |
+
xs(torch.Tensor): shape (b, t//self.downsample_rate, self.hidden_size)
|
| 159 |
+
"""
|
| 160 |
+
assert audio.shape[1] % self.downsample_rate == 0, audio.shape
|
| 161 |
+
# Patchify
|
| 162 |
+
xs = audio.unfold(
|
| 163 |
+
dimension=1,
|
| 164 |
+
size=self.audio_patch_size,
|
| 165 |
+
step=self.audio_patch_size,
|
| 166 |
+
) # (b, num_patch, patch_size) ~ (b, t, c)
|
| 167 |
+
# LLM
|
| 168 |
+
xs = self.in_proj(xs)
|
| 169 |
+
outs = self.qwen3(
|
| 170 |
+
inputs_embeds=xs,
|
| 171 |
+
attention_mask=None,
|
| 172 |
+
)
|
| 173 |
+
xs = outs.last_hidden_state # (b, t, c)
|
| 174 |
+
# Downsample
|
| 175 |
+
if self.extra_downsample_rate > 1:
|
| 176 |
+
xs = self.downsample(xs)
|
| 177 |
+
xs = self.out_proj(xs)
|
| 178 |
+
return xs
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
# --- Decoder
|
| 182 |
+
class ISTFT(nn.Module):
|
| 183 |
+
def __init__(
|
| 184 |
+
self,
|
| 185 |
+
n_fft: int,
|
| 186 |
+
hop_length: int,
|
| 187 |
+
win_length: int,
|
| 188 |
+
padding: str = "same"
|
| 189 |
+
):
|
| 190 |
+
super().__init__()
|
| 191 |
+
assert padding in ["center", "same"], "Padding must be 'center' or 'same'."
|
| 192 |
+
self.padding = padding
|
| 193 |
+
self.n_fft = n_fft
|
| 194 |
+
self.hop_length = hop_length
|
| 195 |
+
self.win_length = win_length
|
| 196 |
+
window = torch.hann_window(win_length)
|
| 197 |
+
self.register_buffer("window", window)
|
| 198 |
+
|
| 199 |
+
def forward(self, spec: torch.Tensor) -> torch.Tensor:
|
| 200 |
+
if self.padding == "center":
|
| 201 |
+
# Fallback to pytorch native implementation
|
| 202 |
+
return torch.istft(spec, self.n_fft, self.hop_length, self.win_length, self.window, center=True)
|
| 203 |
+
elif self.padding == "same":
|
| 204 |
+
pad = (self.win_length - self.hop_length) // 2
|
| 205 |
+
else:
|
| 206 |
+
raise ValueError("Padding must be 'center' or 'same'.")
|
| 207 |
+
|
| 208 |
+
assert spec.dim() == 3, "Expected a 3D tensor as input"
|
| 209 |
+
B, N, T = spec.shape
|
| 210 |
+
|
| 211 |
+
# Inverse FFT
|
| 212 |
+
ifft = torch.fft.irfft(spec, self.n_fft, dim=1, norm="backward")
|
| 213 |
+
ifft = ifft * self.window[None, :, None]
|
| 214 |
+
|
| 215 |
+
# Overlap and Add
|
| 216 |
+
output_size = (T - 1) * self.hop_length + self.win_length
|
| 217 |
+
y = torch.nn.functional.fold(
|
| 218 |
+
ifft, output_size=(1, output_size), kernel_size=(1, self.win_length), stride=(1, self.hop_length),
|
| 219 |
+
)[:, 0, 0, pad:-pad]
|
| 220 |
+
|
| 221 |
+
# Window envelope
|
| 222 |
+
window_sq = self.window.square().expand(1, T, -1).transpose(1, 2)
|
| 223 |
+
window_envelope = torch.nn.functional.fold(
|
| 224 |
+
window_sq, output_size=(1, output_size), kernel_size=(1, self.win_length), stride=(1, self.hop_length),
|
| 225 |
+
).squeeze()[pad:-pad]
|
| 226 |
+
|
| 227 |
+
# Normalize
|
| 228 |
+
assert (window_envelope > 1e-11).all()
|
| 229 |
+
y = y / window_envelope
|
| 230 |
+
|
| 231 |
+
return y
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
class ISTFTHead(nn.Module):
|
| 235 |
+
def __init__(
|
| 236 |
+
self,
|
| 237 |
+
dim: int,
|
| 238 |
+
n_fft: int,
|
| 239 |
+
hop_length: int,
|
| 240 |
+
padding: str = "same"
|
| 241 |
+
):
|
| 242 |
+
super().__init__()
|
| 243 |
+
self.hop_length = hop_length
|
| 244 |
+
out_dim = n_fft + 2
|
| 245 |
+
self.out = torch.nn.Linear(dim, out_dim)
|
| 246 |
+
self.istft = ISTFT(n_fft=n_fft, hop_length=hop_length, win_length=n_fft, padding=padding)
|
| 247 |
+
|
| 248 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 249 |
+
x_pred = self.out(x)
|
| 250 |
+
x_pred = x_pred.transpose(1, 2)
|
| 251 |
+
mag, p = x_pred.chunk(2, dim=1)
|
| 252 |
+
mag = torch.exp(mag)
|
| 253 |
+
mag = torch.clip(mag, max=1e2) # safeguard to prevent excessively large magnitudes
|
| 254 |
+
# wrapping happens here. These two lines produce real and imaginary value
|
| 255 |
+
x = torch.cos(p)
|
| 256 |
+
y = torch.sin(p)
|
| 257 |
+
# recalculating phase here does not produce anything new
|
| 258 |
+
# only costs time
|
| 259 |
+
# phase = torch.atan2(y, x)
|
| 260 |
+
# S = mag * torch.exp(phase * 1j)
|
| 261 |
+
# better directly produce the complex value
|
| 262 |
+
S = mag * (x + 1j * y)
|
| 263 |
+
audio = self.istft(S)
|
| 264 |
+
return audio
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
class RedAEAudioDecoder(torch.nn.Module):
|
| 268 |
+
def __init__(
|
| 269 |
+
self,
|
| 270 |
+
in_dim: int = 64,
|
| 271 |
+
upsample_rate: int = 2, # 25Hz -> 50Hz
|
| 272 |
+
# Output reshape
|
| 273 |
+
audio_patch_size: int = 480, # 50Hz
|
| 274 |
+
audio_sample_rate: int = 24000,
|
| 275 |
+
# Qwen(mirrors encoder)
|
| 276 |
+
hidden_size: int = 896,
|
| 277 |
+
intermediate_size: int = 896*4,
|
| 278 |
+
num_hidden_layers: int = 18,
|
| 279 |
+
max_position_embeddings: int = 32768,
|
| 280 |
+
max_window_layers: int = 0,
|
| 281 |
+
num_attention_heads: int = 14,
|
| 282 |
+
num_key_value_heads: int = 2,
|
| 283 |
+
sliding_window: int = 64,
|
| 284 |
+
use_sliding_window: bool = True,
|
| 285 |
+
):
|
| 286 |
+
super().__init__()
|
| 287 |
+
self.upsample_rate = upsample_rate
|
| 288 |
+
self.audio_patch_size = audio_patch_size
|
| 289 |
+
self.audio_sample_rate = audio_sample_rate
|
| 290 |
+
# Upsample MLP
|
| 291 |
+
self.in_proj = nn.Linear(in_dim, upsample_rate * hidden_size)
|
| 292 |
+
# Qwen3
|
| 293 |
+
self.qwen3_config = Qwen3Config(
|
| 294 |
+
hidden_size=hidden_size,
|
| 295 |
+
intermediate_size=intermediate_size,
|
| 296 |
+
num_hidden_layers=num_hidden_layers,
|
| 297 |
+
max_position_embeddings=max_position_embeddings,
|
| 298 |
+
max_window_layers=max_window_layers,
|
| 299 |
+
num_attention_heads=num_attention_heads,
|
| 300 |
+
num_key_value_heads=num_key_value_heads,
|
| 301 |
+
sliding_window=sliding_window,
|
| 302 |
+
use_sliding_window=use_sliding_window,
|
| 303 |
+
)
|
| 304 |
+
self.qwen3 = Qwen3Model(self.qwen3_config)
|
| 305 |
+
|
| 306 |
+
self.istft_head = ISTFTHead(
|
| 307 |
+
dim=hidden_size,
|
| 308 |
+
n_fft=audio_patch_size*4,
|
| 309 |
+
hop_length=audio_patch_size,
|
| 310 |
+
padding='same',
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
def forward(
|
| 314 |
+
self,
|
| 315 |
+
xs: torch.Tensor,
|
| 316 |
+
):
|
| 317 |
+
"""
|
| 318 |
+
Args:
|
| 319 |
+
xs(torch.Tensor): shape (b, t, c)
|
| 320 |
+
Returns:
|
| 321 |
+
audio(torch.Tensor): shape (b, t*upsample_rate*patch_size)
|
| 322 |
+
audio_len(torch.Tensor): shape (b,)
|
| 323 |
+
"""
|
| 324 |
+
# Upsample
|
| 325 |
+
xs = self.in_proj(xs) # (b, t, upsample_rate, c)
|
| 326 |
+
xs = xs.reshape(xs.shape[0], -1, self.qwen3_config.hidden_size) # (b, t*upsample_rate, c)
|
| 327 |
+
# Qwen3
|
| 328 |
+
outs = self.qwen3(
|
| 329 |
+
inputs_embeds=xs,
|
| 330 |
+
attention_mask=None,
|
| 331 |
+
)
|
| 332 |
+
xs = outs.last_hidden_state # (b, t*upsample_rate, c)
|
| 333 |
+
audio = self.istft_head(xs)
|
| 334 |
+
return audio
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
# --- RedAE
|
| 338 |
+
class RedAEConfig(PretrainedConfig):
|
| 339 |
+
model_type = "redae"
|
| 340 |
+
|
| 341 |
+
def __init__(
|
| 342 |
+
self,
|
| 343 |
+
# Shared
|
| 344 |
+
audio_patch_size: int = 480,
|
| 345 |
+
audio_sample_rate: int = 24000,
|
| 346 |
+
# Encoder
|
| 347 |
+
enc_hidden_size: int = 896,
|
| 348 |
+
enc_intermediate_size: int = 3584,
|
| 349 |
+
enc_num_hidden_layers: int = 18,
|
| 350 |
+
enc_max_position_embeddings: int = 32768,
|
| 351 |
+
enc_max_window_layers: int = 0,
|
| 352 |
+
enc_num_attention_heads: int = 14,
|
| 353 |
+
enc_num_key_value_heads: int = 2,
|
| 354 |
+
enc_sliding_window: int = 64,
|
| 355 |
+
enc_use_sliding_window: bool = True,
|
| 356 |
+
enc_extra_downsample_rate: int = 2, # 50Hz -> 25Hz
|
| 357 |
+
enc_downsample_num_hidden_layers: int = 4,
|
| 358 |
+
# Intermediate
|
| 359 |
+
bottleneck_dim: int = 64,
|
| 360 |
+
# Decoder
|
| 361 |
+
dec_hidden_size: int = 896,
|
| 362 |
+
dec_intermediate_size: int = 3584,
|
| 363 |
+
dec_num_hidden_layers: int = 18,
|
| 364 |
+
dec_max_position_embeddings: int = 32768,
|
| 365 |
+
dec_max_window_layers: int = 0,
|
| 366 |
+
dec_num_attention_heads: int = 14,
|
| 367 |
+
dec_num_key_value_heads: int = 2,
|
| 368 |
+
dec_sliding_window: int = 64,
|
| 369 |
+
dec_use_sliding_window: bool = True,
|
| 370 |
+
# Other
|
| 371 |
+
**kwargs,
|
| 372 |
+
):
|
| 373 |
+
super().__init__(**kwargs)
|
| 374 |
+
# Shared
|
| 375 |
+
self.audio_patch_size=audio_patch_size
|
| 376 |
+
self.audio_sample_rate=audio_sample_rate
|
| 377 |
+
# Encoder
|
| 378 |
+
self.enc_hidden_size=enc_hidden_size
|
| 379 |
+
self.enc_intermediate_size=enc_intermediate_size
|
| 380 |
+
self.enc_num_hidden_layers=enc_num_hidden_layers
|
| 381 |
+
self.enc_max_position_embeddings=enc_max_position_embeddings
|
| 382 |
+
self.enc_max_window_layers=enc_max_window_layers
|
| 383 |
+
self.enc_num_attention_heads=enc_num_attention_heads
|
| 384 |
+
self.enc_num_key_value_heads=enc_num_key_value_heads
|
| 385 |
+
self.enc_sliding_window=enc_sliding_window
|
| 386 |
+
self.enc_use_sliding_window=enc_use_sliding_window
|
| 387 |
+
self.enc_extra_downsample_rate=enc_extra_downsample_rate
|
| 388 |
+
self.enc_downsample_num_hidden_layers=enc_downsample_num_hidden_layers
|
| 389 |
+
# Intermediate
|
| 390 |
+
self.bottleneck_dim=bottleneck_dim
|
| 391 |
+
# Decoder
|
| 392 |
+
self.dec_hidden_size=dec_hidden_size
|
| 393 |
+
self.dec_intermediate_size=dec_intermediate_size
|
| 394 |
+
self.dec_num_hidden_layers=dec_num_hidden_layers
|
| 395 |
+
self.dec_max_position_embeddings=dec_max_position_embeddings
|
| 396 |
+
self.dec_max_window_layers=dec_max_window_layers
|
| 397 |
+
self.dec_num_attention_heads=dec_num_attention_heads
|
| 398 |
+
self.dec_num_key_value_heads=dec_num_key_value_heads
|
| 399 |
+
self.dec_sliding_window=dec_sliding_window
|
| 400 |
+
self.dec_use_sliding_window=dec_use_sliding_window
|
| 401 |
+
|
| 402 |
+
|
| 403 |
+
class RedAE(PreTrainedModel):
|
| 404 |
+
config_class = RedAEConfig
|
| 405 |
+
base_model_prefix = "redae"
|
| 406 |
+
|
| 407 |
+
_supports_flash_attn = True
|
| 408 |
+
_supports_sdpa = True
|
| 409 |
+
|
| 410 |
+
def __init__(self, config: RedAEConfig):
|
| 411 |
+
super().__init__(config)
|
| 412 |
+
|
| 413 |
+
self.encoder = RedAEAudioEncoder(
|
| 414 |
+
out_dim=config.bottleneck_dim,
|
| 415 |
+
# Input reshape
|
| 416 |
+
audio_patch_size=config.audio_patch_size, # 50Hz
|
| 417 |
+
audio_sample_rate=config.audio_sample_rate,
|
| 418 |
+
# Qwen
|
| 419 |
+
hidden_size=config.enc_hidden_size,
|
| 420 |
+
intermediate_size=config.enc_intermediate_size,
|
| 421 |
+
num_hidden_layers=config.enc_num_hidden_layers,
|
| 422 |
+
max_position_embeddings=config.enc_max_position_embeddings,
|
| 423 |
+
max_window_layers=config.enc_max_window_layers,
|
| 424 |
+
num_attention_heads=config.enc_num_attention_heads,
|
| 425 |
+
num_key_value_heads=config.enc_num_key_value_heads,
|
| 426 |
+
sliding_window=config.enc_sliding_window,
|
| 427 |
+
use_sliding_window=config.enc_use_sliding_window,
|
| 428 |
+
# Extra downsample
|
| 429 |
+
extra_downsample_rate=config.enc_extra_downsample_rate, # 50Hz -> 25Hz
|
| 430 |
+
downsample_num_hidden_layers=config.enc_downsample_num_hidden_layers,
|
| 431 |
+
)
|
| 432 |
+
self.decoder = RedAEAudioDecoder(
|
| 433 |
+
in_dim=config.bottleneck_dim,
|
| 434 |
+
upsample_rate=config.enc_extra_downsample_rate,
|
| 435 |
+
audio_patch_size=config.audio_patch_size,
|
| 436 |
+
audio_sample_rate=config.audio_sample_rate,
|
| 437 |
+
hidden_size=config.dec_hidden_size,
|
| 438 |
+
intermediate_size=config.dec_intermediate_size,
|
| 439 |
+
num_hidden_layers=config.dec_num_hidden_layers,
|
| 440 |
+
max_position_embeddings=config.dec_max_position_embeddings,
|
| 441 |
+
max_window_layers=config.dec_max_window_layers,
|
| 442 |
+
num_attention_heads=config.dec_num_attention_heads,
|
| 443 |
+
num_key_value_heads=config.dec_num_key_value_heads,
|
| 444 |
+
sliding_window=config.dec_sliding_window,
|
| 445 |
+
use_sliding_window=config.dec_use_sliding_window,
|
| 446 |
+
)
|
| 447 |
+
self.post_init()
|
| 448 |
+
|
| 449 |
+
# --- Shared property
|
| 450 |
+
@property
|
| 451 |
+
def downsample_rate(self):
|
| 452 |
+
return self.encoder.downsample_rate
|
| 453 |
+
|
| 454 |
+
@property
|
| 455 |
+
def sample_rate(self):
|
| 456 |
+
return self.decoder.audio_sample_rate
|
| 457 |
+
|
| 458 |
+
@property
|
| 459 |
+
def hidden_size(self):
|
| 460 |
+
return self.encoder.out_dim
|
| 461 |
+
|
| 462 |
+
@staticmethod
|
| 463 |
+
def pad_to_multiple_of(audio: torch.Tensor, multiple_of: int):
|
| 464 |
+
target_samples = math.ceil(audio.shape[-1] / multiple_of) * multiple_of
|
| 465 |
+
pad_len = target_samples - audio.shape[-1]
|
| 466 |
+
if pad_len > 0:
|
| 467 |
+
audio = F.pad(audio, (pad_len, 0)) # NOTE left pad
|
| 468 |
+
return audio
|
| 469 |
+
|
| 470 |
+
@torch.autocast(device_type='cuda', dtype=torch.bfloat16)
|
| 471 |
+
@torch.no_grad()
|
| 472 |
+
def encode(self, audio: torch.Tensor, audio_sr:int):
|
| 473 |
+
"""
|
| 474 |
+
Args:
|
| 475 |
+
audio: shape (b, t)
|
| 476 |
+
audio_sr: int
|
| 477 |
+
Returns:
|
| 478 |
+
latents: shape (b, l=t//960, c=64)
|
| 479 |
+
"""
|
| 480 |
+
audio = audio[:1]
|
| 481 |
+
audio = torchaudio.functional.resample(audio, audio_sr, self.sample_rate)
|
| 482 |
+
audio = self.pad_to_multiple_of(audio, self.downsample_rate)
|
| 483 |
+
latents = self.encoder.forward(audio)
|
| 484 |
+
return latents
|
| 485 |
+
|
| 486 |
+
@torch.no_grad()
|
| 487 |
+
def decode(self, latents: torch.Tensor):
|
| 488 |
+
"""
|
| 489 |
+
Args:
|
| 490 |
+
latents: shape (b, l=t//960, c=64)
|
| 491 |
+
Returns:
|
| 492 |
+
audio: shape (b, t)
|
| 493 |
+
audio_sr: int
|
| 494 |
+
"""
|
| 495 |
+
audio = self.decoder.forward(latents)
|
| 496 |
+
return audio, self.sample_rate
|
fireredtts3/utils/chatml.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
CHATML_LATENT_IN_PAD_SYM = '<|image_pad|>'
|
| 5 |
+
CHATML_LATENT_IN_PAD_ID = 151655
|
| 6 |
+
CHATML_LATENT_OUT_PAD_SYM = '<|video_pad|>'
|
| 7 |
+
CHATML_LATENT_OUT_PAD_ID = 151656
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def convert_to_chatml(
|
| 11 |
+
# User input
|
| 12 |
+
text_in: str, # Required
|
| 13 |
+
latent_in_len: int = 0,
|
| 14 |
+
# Assistant output
|
| 15 |
+
text_out: str = "",
|
| 16 |
+
latent_out_len: int = 0,
|
| 17 |
+
# Placeholder(will be replaced with audio latents)
|
| 18 |
+
latent_in_pad: str = CHATML_LATENT_IN_PAD_SYM,
|
| 19 |
+
latent_out_pad:str = CHATML_LATENT_OUT_PAD_SYM,
|
| 20 |
+
):
|
| 21 |
+
# System prompt
|
| 22 |
+
messages = [{'role': 'system', 'content': 'You are a helpful assistant.'}]
|
| 23 |
+
# Input
|
| 24 |
+
input_message = {
|
| 25 |
+
"role": "user",
|
| 26 |
+
"content": [
|
| 27 |
+
{
|
| 28 |
+
"type": "text",
|
| 29 |
+
"text": text_in + ' /no_think', # NOTE should add /no_think for Qwen3(not Qwen3.5)
|
| 30 |
+
}
|
| 31 |
+
]
|
| 32 |
+
}
|
| 33 |
+
if latent_in_len>0:
|
| 34 |
+
input_message['content'].insert(0,
|
| 35 |
+
{
|
| 36 |
+
"type": "audio",
|
| 37 |
+
"audio": latent_in_pad * latent_in_len,
|
| 38 |
+
}
|
| 39 |
+
)
|
| 40 |
+
messages.append(input_message)
|
| 41 |
+
# Output
|
| 42 |
+
output_message = {
|
| 43 |
+
"role": "assistant",
|
| 44 |
+
"content": [
|
| 45 |
+
{
|
| 46 |
+
"type": "text",
|
| 47 |
+
"text": f"<think>\n\n</think>\n\n" + text_out, # text_out should be wrapped in <|sot|><|eot|>
|
| 48 |
+
}
|
| 49 |
+
]
|
| 50 |
+
}
|
| 51 |
+
if latent_out_len>0:
|
| 52 |
+
output_message['content'].append(
|
| 53 |
+
{
|
| 54 |
+
"type": "audio",
|
| 55 |
+
"audio": latent_out_pad * latent_out_len,
|
| 56 |
+
}
|
| 57 |
+
)
|
| 58 |
+
messages.append(output_message)
|
| 59 |
+
# Convert to chatml string
|
| 60 |
+
chatml_str_list: List[str] = []
|
| 61 |
+
for msg in messages:
|
| 62 |
+
if isinstance(msg['content'], str):
|
| 63 |
+
chatml_str_list.append(
|
| 64 |
+
f'<|im_start|>{msg["role"]}\n{msg["content"]}<|im_end|>\n'
|
| 65 |
+
)
|
| 66 |
+
else:
|
| 67 |
+
chatml_str_list.append(f'<|im_start|>{msg["role"]}\n')
|
| 68 |
+
for content in msg["content"]:
|
| 69 |
+
if content["type"] == "text":
|
| 70 |
+
chatml_str_list.append(content["text"])
|
| 71 |
+
elif content["type"] == "audio":
|
| 72 |
+
chatml_str_list.append(
|
| 73 |
+
f"<|sosp|>{content['audio']}<|eosp|>\n"
|
| 74 |
+
)
|
| 75 |
+
chatml_str_list.append(f"<|im_end|>\n")
|
| 76 |
+
chatml_str = ''.join(chatml_str_list)
|
| 77 |
+
return chatml_str
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def compose_generate_input_tts(
|
| 81 |
+
prompt_latent_len: int,
|
| 82 |
+
prompt_text: str,
|
| 83 |
+
text: str,
|
| 84 |
+
):
|
| 85 |
+
text_in = "Convert text to speech.\n{}".format(prompt_text+text)
|
| 86 |
+
chatml_str = convert_to_chatml(text_in=text_in, latent_out_len=prompt_latent_len)
|
| 87 |
+
chatml_str = chatml_str.removesuffix('<|eosp|>\n<|im_end|>\n') # Remove ending tags
|
| 88 |
+
return chatml_str
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def compose_generate_input_voice_design(
|
| 92 |
+
instruction: str,
|
| 93 |
+
text: str,
|
| 94 |
+
):
|
| 95 |
+
text_in = "{}\n\n根据上述音色描述,首先整理成语音属性,再合成以下文本对应的音频:\n{}".format(instruction, text)
|
| 96 |
+
chatml_str = convert_to_chatml(text_in=text_in, text_out="<|sot|>")
|
| 97 |
+
chatml_str = chatml_str.removesuffix('<|im_end|>\n') # Remove ending tags
|
| 98 |
+
return chatml_str
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def compose_generate_input_semantic_edit(
|
| 102 |
+
instruction: str,
|
| 103 |
+
audio_in_latent_len: int,
|
| 104 |
+
):
|
| 105 |
+
text_in = 'Identify the content of the audio. {}'.format(instruction.strip())
|
| 106 |
+
chatml_str = convert_to_chatml(text_in=text_in, latent_in_len=audio_in_latent_len, text_out="<|sot|>")
|
| 107 |
+
chatml_str = chatml_str.removesuffix('<|im_end|>\n') # Remove ending tags
|
| 108 |
+
return chatml_str
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def compose_generate_input_acoustic_edit(
|
| 112 |
+
instruction: str,
|
| 113 |
+
audio_in_latent_len: int,
|
| 114 |
+
):
|
| 115 |
+
chatml_str = convert_to_chatml(text_in=instruction, latent_in_len=audio_in_latent_len, latent_out_len=1)
|
| 116 |
+
chatml_str = chatml_str.removesuffix('<|video_pad|><|eosp|>\n<|im_end|>\n') # Remove ending tags
|
| 117 |
+
return chatml_str
|
| 118 |
+
|
fireredtts3/utils/llm_tn/templates/ar-SA.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"locale": "ar-SA",
|
| 3 |
+
"language_name": "Arabic",
|
| 4 |
+
"system": "You are an accurate text normalizer for ar-SA (Modern Standard Arabic). Your task is to normalize unstandardized text from the following categories into the way it is actually spoken aloud, based on the context: [Cardinal, Date, Decimal, Ordinal, Fraction, Time, Currency, Unit, Electronic Address, Initialism/Acronym, ISBN, Roman Numeral, Telephone, Sports Score, Mathematical Expression, Symbol, Abbreviation, Chemical Formula, Legal Reference, Vehicle/Product Code, Geographic Coordinates, Version Number, License Plate, Musical Notation, Stock Ticker, Biological Classification, Address].\n\nRules:\n- Convert numbers and dates into spoken Arabic (2023 -> ألفان وثلاثة وعشرون; 12 أكتوبر -> الثاني عشر من أكتوبر).\n- Decimals use فاصلة (2.5 -> اثنان فاصلة خمسة); percentages use بالمئة (15% -> خمسة عشر بالمئة).\n- Phone numbers, ISBNs and codes are read digit-by-digit (050-1234567 -> صفر خمسة صفر واحد اثنان ثلاثة أربعة خمسة ستة سبعة).\n- Roman numerals are converted to Arabic numbers (XVIII -> الثامن عشر).\n- Latin acronyms/initialisms are spelled letter-by-letter (GPS -> G P S, NaOH -> N a O H). Recognizable pronounceable acronyms kept as-is (NASA).\n- Currency amounts use Arabic names (ر.س -> ريال سعودي; $ -> دولار).\n- Math operators become words (= -> يساوي, + -> زائد, - -> ناقص).\n- Keep all other words unchanged. Output ONLY the normalized sentence, with no explanations, no quotes, and no extra text.",
|
| 5 |
+
"examples": [
|
| 6 |
+
{"input": "سيُعقد الاجتماع في 12 أكتوبر 2023.", "output": "سيُعقد الاجتماع في الثاني عشر من أكتوبر ألفين وثلاثة وعشرين."},
|
| 7 |
+
{"input": "سعر المنتج هو 1,500 ريال.", "output": "سعر المنتج هو ألف وخمسمائة ريال."},
|
| 8 |
+
{"input": "بلغ التضخم 7.5% هذا العام.", "output": "بلغ التضخم سبعة فاصلة خمسة بالمئة هذا العام."},
|
| 9 |
+
{"input": "رقم جوالي هو 050-1234567.", "output": "رقم جوالي هو صفر خمسة صفر واحد اثنان ثلاثة أربعة خمسة ستة سبعة."},
|
| 10 |
+
{"input": "الجهاز يستخدم GPS وصيغة NaOH.", "output": "الجهاز يستخدم G P S وصيغة N a O H."},
|
| 11 |
+
{"input": "الصيغة (a+b)² بسيطة.", "output": "الصيغة a زائد b في المربع بسيطة."}
|
| 12 |
+
]
|
| 13 |
+
}
|
fireredtts3/utils/llm_tn/templates/cs-CZ.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"locale": "cs-CZ",
|
| 3 |
+
"language_name": "Czech",
|
| 4 |
+
"system": "You are an accurate text normalizer for cs-CZ (Czech). Your task is to normalize unstandardized text from the following categories into the way it is actually spoken aloud, based on the context: [Cardinal, Date, Decimal, Ordinal, Fraction, Time, Currency, Unit, Electronic Address, Initialism/Acronym, ISBN, Roman Numeral, Telephone, Sports Score, Mathematical Expression, Symbol, Abbreviation, Chemical Formula, Legal Reference, Vehicle/Product Code, Geographic Coordinates, Version Number, License Plate, Musical Notation, Stock Ticker, Biological Classification, Address].\n\nRules:\n- Convert numbers and dates into spoken Czech (2023 -> dva tisíce dvacet tři; 12. října -> dvanáctého října).\n- Decimals use čárka (2.5 -> dvě čárka pět); percentages use procent (15% -> patnáct procent).\n- Dates use genitive case for day/month (10. 12. 2023 -> desátého prosince dva tisíce dvacet tři).\n- Phone numbers, ISBNs and codes are read digit-by-digit (603-123456 -> šest nula tři jedna dva tři čtyři pět šest).\n- Latin acronyms/initialisms are spelled letter-by-letter (GPS -> G P S, NaOH -> N a O H). Recognizable pronounceable acronyms kept as-is (NASA).\n- Currency amounts use Czech names (Kč -> korun; $ -> dolarů; € -> eur).\n- Math operators become words (= -> se rovná, + -> plus, - -> mínus).\n- Keep all other words unchanged. Output ONLY the normalized sentence, with no explanations, no quotes, and no extra text.",
|
| 5 |
+
"examples": [
|
| 6 |
+
{"input": "Schůzka se uskuteční 12. října 2023.", "output": "Schůzka se uskuteční dvanáctého října dva tisíce dvacet tři."},
|
| 7 |
+
{"input": "Produkt stojí 1 500 korun.", "output": "Produkt stojí tisíc pět set korun."},
|
| 8 |
+
{"input": "Inflace dosáhla 7,5 % letos.", "output": "Inflace dosáhla sedm čárka pět procent letos."},
|
| 9 |
+
{"input": "Mé telefonní číslo je 603-123456.", "output": "Mé telefonní číslo je šest nula tři jedna dva tři čtyři pět šest."},
|
| 10 |
+
{"input": "Zařízení používá GPS a NaOH.", "output": "Zařízení používá G P S a N a O H."},
|
| 11 |
+
{"input": "Vzorec (a+b)² je jednoduchý.", "output": "Vzorec a plus b na druhou je jednoduchý."}
|
| 12 |
+
]
|
| 13 |
+
}
|
fireredtts3/utils/llm_tn/templates/de-DE.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"locale": "de-DE",
|
| 3 |
+
"language_name": "German",
|
| 4 |
+
"system": "You are an accurate text normalizer for de-DE (German). Your task is to normalize unstandardized text from the following categories into the way it is actually spoken aloud, based on the context: [Cardinal, Date, Decimal, Ordinal, Fraction, Time, Currency, Unit, Electronic Address, Initialism/Acronym, ISBN, Roman Numeral, Telephone, Sports Score, Mathematical Expression, Symbol, Abbreviation, Chemical Formula, Legal Reference, Vehicle/Product Code, Geographic Coordinates, Version Number, License Plate, Musical Notation, Stock Ticker, Biological Classification, Address].\n\nRules:\n- Spell out all numbers, dates, times, currencies and units the way they are read aloud in German (e.g. 2024 -> zweitausendvierundzwanzig; dates use ordinal day + ordinal month, e.g. 12.10.23 -> zwölften zehnten dreiundzwanzig).\n- Compound German numbers are written as a single word (e.g. 25 -> fünfundzwanzig).\n- When normalizing acronyms, spell out the individual letters as pronounced in German, except when the acronym is a widely recognized and pronounceable name.\n- Convert punctuation that is spoken aloud into words (e.g., write 'Punkt' instead of a period in URLs, 'at' for '@' in emails, 'Bindestrich' for hyphens).\n- Segment compound words, websites, and file names into recognizable component words.\n- Common file extensions should be spoken out. Uncommon file extensions should be spelled out.\n- Keep all other words unchanged. Output ONLY the normalized sentence, with no explanations, no quotes, and no extra text.",
|
| 5 |
+
"examples": [
|
| 6 |
+
{"input": "Die Veranstaltung ist am 12.10.23.", "output": "Die Veranstaltung ist am zwölften zehnten dreiundzwanzig."},
|
| 7 |
+
{"input": "Das Auto kostet 25.000€ und fährt maximal 120 km/h.", "output": "Das Auto kostet fünfundzwanzigtausend Euro und fährt maximal hundertzwanzig Kilometer pro Stunde."},
|
| 8 |
+
{"input": "Etwa 3/4 der Befragten stimmten zu.", "output": "Etwa drei Viertel der Befragten stimmten zu."},
|
| 9 |
+
{"input": "Schreiben Sie an info@firma.de oder besuchen Sie www.firma.de.", "output": "Schreiben Sie an info at firma Punkt d e oder besuchen Sie w w w Punkt firma Punkt d e."},
|
| 10 |
+
{"input": "Der Zug ICE 123 fährt um 18:45 Uhr ab.", "output": "Der Zug I C E hundertdreiundzwanzig fährt um achtzehn Uhr fünfundvierzig ab."},
|
| 11 |
+
{"input": "H₂SO₄ ist Schwefelsäure und C₆H₁₂O₆ ist Glukose.", "output": "H zwei S O vier ist Schwefelsäure und C sechs H zwölf O sechs ist Glukose."},
|
| 12 |
+
{"input": "Die ISBN 978-3-492-28656-1 ist gültig.", "output": "Die I S B N neun sieben acht drei vier neun zwei zwei acht sechs fünf sechs eins ist gültig."},
|
| 13 |
+
{"input": "Die Matrikelnummer ist 1234567.", "output": "Die Matrikelnummer ist eins zwei drei vier fünf sechs sieben."},
|
| 14 |
+
{"input": "Die NASA und die USA kooperieren.", "output": "Die Nasa und die U S A kooperieren."},
|
| 15 |
+
{"input": "Das MacBook Pro M1 ist schnell.", "output": "Das MacBook Pro M eins ist schnell."},
|
| 16 |
+
{"input": "Gemäß § 13 StGB ist das strafbar.", "output": "Gemäß Paragraf dreizehn S T G B ist das strafbar."},
|
| 17 |
+
{"input": "Die UNESCO hat das Denkmal geschützt.", "output": "Die Unesco hat das Denkmal geschützt."},
|
| 18 |
+
{"input": "George VI. war ein beliebter König.", "output": "George der Sechste war ein beliebter König."},
|
| 19 |
+
{"input": "Schreiben Sie an admin@server.local.", "output": "Schreiben Sie an admin at server Punkt local."}
|
| 20 |
+
]
|
| 21 |
+
}
|
fireredtts3/utils/llm_tn/templates/el-GR.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"locale": "el-GR",
|
| 3 |
+
"language_name": "Greek",
|
| 4 |
+
"system": "You are an accurate text normalizer for el-GR (Greek). Your task is to normalize unstandardized text from the following categories into the way it is actually spoken aloud, based on the context: [Cardinal, Date, Decimal, Ordinal, Fraction, Time, Currency, Unit, Electronic Address, Initialism/Acronym, ISBN, Roman Numeral, Telephone, Sports Score, Mathematical Expression, Symbol, Abbreviation, Chemical Formula, Legal Reference, Vehicle/Product Code, Geographic Coordinates, Version Number, License Plate, Musical Notation, Stock Ticker, Biological Classification, Address].\n\nRules:\n- Convert numbers and dates into spoken Greek (2023 -> δύο χιλιάδες είκοσι τρία; 12 Οκτωβρίου -> δώδεκα Οκτωβρίου).\n- Decimals use κόμμα (2.5 -> δύο κόμμα πέντε); percentages use τοις εκατό (15% -> δεκαπέντε τοις εκατό).\n- Ordinal uses -ος/-ο suffix (XVIII -> δέκατος όγδοος).\n- Phone numbers, ISBNs and codes are read digit-by-digit (210-1234567 -> δύο ένα μηδέν ένα δύο τρία τέσσερα πέντε έξι επτά).\n- Latin acronyms/initialisms are spelled letter-by-letter (GPS -> G P S, NaOH -> N a O H). Recognizable pronounceable acronyms kept as-is (NASA).\n- Currency amounts use Greek names (€ -> ευρώ; $ -> δολάρια).\n- Math operators become words (= -> ισούται με, + -> συν, - -> πλην).\n- Keep all other words unchanged. Output ONLY the normalized sentence, with no explanations, no quotes, and no extra text.",
|
| 5 |
+
"examples": [
|
| 6 |
+
{"input": "Η συνάντηση θα γίνει στις 12 Οκτωβρίου 2023.", "output": "Η συνάντηση θα γίνει στις δώδεκα Οκτωβρίου δύο χιλιάδες είκοσι τρία."},
|
| 7 |
+
{"input": "Το προϊόν κοστίζει 1.500 ευρώ.", "output": "Το προϊόν κοστίζει χίλια πεντακόσια ευρώ."},
|
| 8 |
+
{"input": "Ο πληθωρισμός έφτασε το 7,5% φέτος.", "output": "Ο πληθωρισμός έφτασε το επτά κόμμα πέντε τοις εκατό φέτος."},
|
| 9 |
+
{"input": "Το τηλέφωνό μου είναι 210-1234567.", "output": "Το τηλέφωνό μου είναι δύο ένα μηδέν ένα δύο τρία τέσσερα πέντε έξι επτά."},
|
| 10 |
+
{"input": "Η συσκευή χρησιμοποιεί GPS και NaOH.", "output": "Η συσκευή χρησιμοποιεί G P S και N a O H."},
|
| 11 |
+
{"input": "Ο τύπος (a+b)² είναι απλός.", "output": "Ο τύπος a συν b στο τετράγωνο είναι απλός."}
|
| 12 |
+
]
|
| 13 |
+
}
|
fireredtts3/utils/llm_tn/templates/en-US.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"locale": "en-US",
|
| 3 |
+
"language_name": "English",
|
| 4 |
+
"system": "You are an accurate text normalizer for en-US (English). Your task is to normalize unstandardized text from the following categories into the way it is actually spoken aloud, based on the context: [Cardinal, Date, Decimal, Ordinal, Fraction, Time, Currency, Unit, Electronic Address, Initialism/Acronym, ISBN, Roman Numeral, Telephone, Sports Score, Mathematical Expression, Symbol, Abbreviation, Chemical Formula, Legal Reference, Vehicle/Product Code, Geographic Coordinates, Version Number, License Plate, Musical Notation, Stock Ticker, Biological Classification, Address].\n\nRules:\n- Spell out all numbers, dates, times, currencies and units the way they are read aloud in English (e.g. 2023 -> twenty twenty three; $5.99 -> five dollars and ninety nine cents; 120 km/h -> one hundred and twenty kilometers per hour).\n- When normalizing acronyms, spell out the individual letters separated by spaces, except when the acronym is a widely recognized and pronounceable name (e.g. \"NASA\").\n- Convert punctuation that is spoken aloud into words (e.g., write 'dot' instead of a period in URLs, 'at' for '@' in emails, 'dash' or 'hyphen' for '-').\n- Segment compound words, websites, and file names into recognizable component words.\n- Common file extensions should be spoken out (e.g. .jpg -> jpeg). Uncommon file extensions should be spelled out letter by letter.\n- Keep all other words unchanged. Output ONLY the normalized sentence, with no explanations, no quotes, and no extra text.",
|
| 5 |
+
"examples": [
|
| 6 |
+
{"input": "He bought 2.5 kg of apples for $5.99 on 10/12/2023.", "output": "He bought two point five kilograms of apples for five dollars and ninety nine cents on October twelfth twenty twenty three."},
|
| 7 |
+
{"input": "Send the image.jpg and the config.yaml to info@nasa.gov.", "output": "Send the image dot jpeg and the config dot y a m l to info at nasa dot gov."},
|
| 8 |
+
{"input": "The LA Lakers won 102-98 against the Miami Heat.", "output": "The L A Lakers won one hundred and two to ninety eight against the Miami Heat."},
|
| 9 |
+
{"input": "Dr. Smith's clinic is at 123 W. 5th St.", "output": "Doctor Smith's clinic is at one twenty three West fifth street."},
|
| 10 |
+
{"input": "It requires >= 8GB of RAM.", "output": "It requires greater than or equal to eight gigabytes of RAM."},
|
| 11 |
+
{"input": "Glucose is C₆H₁₂O₆ and water is H₂O.", "output": "Glucose is C six H twelve O six and water is H two O."},
|
| 12 |
+
{"input": "The deadline is 2024-12-31.", "output": "The deadline is December thirty first twenty twenty four."},
|
| 13 |
+
{"input": "The building was constructed in MDCCCXLV.", "output": "The building was constructed in eighteen forty five."},
|
| 14 |
+
{"input": "Reply with #YesOrNo to win €3.1M.", "output": "Reply with hashtag yes or no to win three point one million euros."},
|
| 15 |
+
{"input": "The event is sponsored by NASA and the strain is SARS-CoV-2.", "output": "The event is sponsored by Nasa and the strain is Sars Cov two."},
|
| 16 |
+
{"input": "The bus arrives at 14:50.", "output": "The bus arrives at fourteen fifty."},
|
| 17 |
+
{"input": "He tested positive for COVID.", "output": "He tested positive for covid."},
|
| 18 |
+
{"input": "The book's ISBN is 978-0-13-468599-1.", "output": "The book's I S B N is nine seven eight zero one three four six eight five nine nine one."}
|
| 19 |
+
]
|
| 20 |
+
}
|
fireredtts3/utils/llm_tn/templates/es-MX.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"locale": "es-MX",
|
| 3 |
+
"language_name": "Spanish",
|
| 4 |
+
"system": "You are an accurate text normalizer for es-MX (Mexican Spanish). Your task is to normalize unstandardized text from the following categories into the way it is actually spoken aloud, based on the context: [Cardinal, Date, Decimal, Ordinal, Fraction, Time, Currency, Unit, Electronic Address, Initialism/Acronym, ISBN, Roman Numeral, Telephone, Sports Score, Mathematical Expression, Symbol, Abbreviation, Chemical Formula, Legal Reference, Vehicle/Product Code, Geographic Coordinates, Version Number, License Plate, Musical Notation, Stock Ticker, Biological Classification, Address].\n\nRules:\n- Spell out all numbers, dates, times, currencies and units the way they are read aloud in Mexican Spanish. Years are read as a full cardinal number (e.g. 2023 -> dos mil veintitrés; 1990 -> mil novecientos noventa).\n- Dates use the format 'day de month de year' (e.g. 15/10/2023 -> quince de octubre de dos mil veintitrés). The first day of the month is read as 'primero'.\n- When normalizing acronyms, spell out the individual letters, except when the acronym is a widely recognized and pronounceable name (e.g. \"RENFE\").\n- Convert punctuation that is spoken aloud into words (e.g., write 'punto' for URLs, 'arroba' for '@' in emails, 'guion' for hyphens).\n- Segment compound words, websites, and file names into recognizable component words.\n- Common file extensions should be spoken out. Uncommon file extensions should be spelled out.\n- Keep all other words unchanged. Output ONLY the normalized sentence, with no explanations, no quotes, and no extra text.",
|
| 5 |
+
"examples": [
|
| 6 |
+
{"input": "La reunión es el 15/10/2023.", "output": "La reunión es el quince de octubre de dos mil veintitrés."},
|
| 7 |
+
{"input": "Nací el 3 de mayo de 1990.", "output": "Nací el tres de mayo de mil novecientos noventa."},
|
| 8 |
+
{"input": "Su email es contacto@empresa.mx.", "output": "Su email es contacto arroba empresa punto e me equis."},
|
| 9 |
+
{"input": "La velocidad máx. en la autopista es de 120 km/h.", "output": "La velocidad máxima en la autopista es de ciento veinte kilómetros por hora."},
|
| 10 |
+
{"input": "El resultado fue 3/4 partes del total.", "output": "El resultado fue tres cuartas partes del total."},
|
| 11 |
+
{"input": "Es medianoche (00:00).", "output": "Es medianoche (cero horas)."},
|
| 12 |
+
{"input": "El Siglo XXI y la Sección IX.", "output": "El Siglo veintiuno y la Sección nueve."},
|
| 13 |
+
{"input": "El producto cuesta US$25.99.", "output": "El producto cuesta veinticinco dólares con noventa y nueve centavos."},
|
| 14 |
+
{"input": "La UNESCO declaró patrimonio y la PROFECO protege al consumidor.", "output": "La UNESCO declaró patrimonio y la PROFECO protege al consumidor."},
|
| 15 |
+
{"input": "Usa Python 3.10.4 en el servidor.", "output": "Usa Python tres punto diez punto cuatro en el servidor."},
|
| 16 |
+
{"input": "META (Facebook) subió en la bolsa.", "output": "M E T A (Facebook) subió en la bolsa."},
|
| 17 |
+
{"input": "La reunión es a las 12:00 en punto.", "output": "La reunión es a las doce en punto."},
|
| 18 |
+
{"input": "Participa en el concurso #Verano y menciona a @amigo.", "output": "Participa en el concurso hashtag Verano y menciona a arroba amigo."},
|
| 19 |
+
{"input": "La acción GOOG subió hoy.", "output": "La acción G O O G subió hoy."},
|
| 20 |
+
{"input": "El código es SKU: PRD-567-890.", "output": "El código es S K U: P R D guión cinco seis siete guión ocho nueve cero."},
|
| 21 |
+
{"input": "El libro tiene ISBN 978-607-16-1234-5.", "output": "El libro tiene I S B N nueve siete ocho, guión seis cero siete, guión uno seis, guión uno dos tres cuatro, guión cinco."}
|
| 22 |
+
]
|
| 23 |
+
}
|
fireredtts3/utils/llm_tn/templates/fi-FI.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"locale": "fi-FI",
|
| 3 |
+
"language_name": "Finnish",
|
| 4 |
+
"system": "You are an accurate text normalizer for fi-FI (Finnish). Your task is to normalize unstandardized text from the following categories into the way it is actually spoken aloud, based on the context: [Cardinal, Date, Decimal, Ordinal, Fraction, Time, Currency, Unit, Electronic Address, Initialism/Acronym, ISBN, Roman Numeral, Telephone, Sports Score, Mathematical Expression, Symbol, Abbreviation, Chemical Formula, Legal Reference, Vehicle/Product Code, Geographic Coordinates, Version Number, License Plate, Musical Notation, Stock Ticker, Biological Classification, Address].\n\nRules:\n- Convert numbers and dates into spoken Finnish (2023 -> kaksituhattakaksikymmentäkolme; 12. lokakuuta -> kahdestoista lokakuuta).\n- Decimals use pilkku (2.5 -> kaksi pilkku viisi); percentages use prosenttia (15% -> viisitoista prosenttia).\n- Ordinal uses -s/-nna suffix (XVIII -> kahdeksastoista).\n- Phone numbers, ISBNs and codes are read digit-by-digit (040-1234567 -> nolla neljä nolla yksi kaksi kolme neljä viisi kuusi seitsemän).\n- Latin acronyms/initialisms are spelled letter-by-letter (GPS -> G P S, NaOH -> N a O H). Recognizable pronounceable acronyms kept as-is (NASA).\n- Currency amounts use Finnish names (€ -> euroa; $ -> dollaria).\n- Math operators become words (= -> on yhtä kuin, + -> plus, - -> miinus).\n- Keep all other words unchanged. Output ONLY the normalized sentence, with no explanations, no quotes, and no extra text.",
|
| 5 |
+
"examples": [
|
| 6 |
+
{"input": "Kokous pidetään 12. lokakuuta 2023.", "output": "Kokous pidetään kahdestoista lokakuuta kaksituhattakaksikymmentäkolme."},
|
| 7 |
+
{"input": "Tuote maksaa 1 500 euroa.", "output": "Tuote maksaa tuhat viisisataa euroa."},
|
| 8 |
+
{"input": "Inflaatio oli 7,5 prosenttia tänä vuonna.", "output": "Inflaatio oli seitsemän pilkku viisi prosenttia tänä vuonna."},
|
| 9 |
+
{"input": "Puhelinnumeroni on 040-1234567.", "output": "Puhelinnumeroni on nolla neljä nolla yksi kaksi kolme neljä viisi kuusi seitsemän."},
|
| 10 |
+
{"input": "Laite käyttää GPS:ää ja NaOH:ta.", "output": "Laite käyttää G P S:ää ja N a O H:ta."},
|
| 11 |
+
{"input": "Kaava (a+b)² on yksinkertainen.", "output": "Kaava a plus b toiseen on yksinkertainen."}
|
| 12 |
+
]
|
| 13 |
+
}
|
fireredtts3/utils/llm_tn/templates/fr-FR.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"locale": "fr-FR",
|
| 3 |
+
"language_name": "French",
|
| 4 |
+
"system": "You are an accurate text normalizer for fr-FR (French). Your task is to normalize unstandardized text from the following categories into the way it is actually spoken aloud, based on the context: [Cardinal, Date, Decimal, Ordinal, Fraction, Time, Currency, Unit, Electronic Address, Initialism/Acronym, ISBN, Roman Numeral, Telephone, Sports Score, Mathematical Expression, Symbol, Abbreviation, Chemical Formula, Legal Reference, Vehicle/Product Code, Geographic Coordinates, Version Number, License Plate, Musical Notation, Stock Ticker, Biological Classification, Address].\n\nRules:\n- Spell out all numbers, dates, times, currencies and units the way they are read aloud in French.\n- When normalizing acronyms, spell out the individual letters separated by spaces (e.g. \"SNCF\" -> \"S N C F\"), except when the acronym is a widely recognized and pronounceable name (e.g. \"OTAN\").\n- Convert punctuation that is spoken aloud into words (e.g., write 'point' instead of a period in URLs, 'arobase' for '@', 'tiret' for hyphens).\n- Segment compound words, websites, and file names into recognizable component words.\n- Common file extensions should be spoken out. Uncommon file extensions should be spelled out.\n- Keep all other words unchanged. Output ONLY the normalized sentence, with no explanations, no quotes, and no extra text.",
|
| 5 |
+
"examples": [
|
| 6 |
+
{"input": "Le taux de change est de 1€ = 1,10$.", "output": "Le taux de change est de un euro égale un virgule dix dollars."},
|
| 7 |
+
{"input": "M. Dupont habite au 15, av. des Champs-Élysées.", "output": "Monsieur Dupont habite au quinze, avenue des Champs-Élysées."},
|
| 8 |
+
{"input": "Le vol AF123 partira le 1er juin à 18h45.", "output": "Le vol A F cent vingt-trois partira le premier juin à dix-huit heures quarante-cinq."},
|
| 9 |
+
{"input": "Son site est www.boutique-mode.fr.", "output": "Son site est w w w point boutique tiret mode point f r."},
|
| 10 |
+
{"input": "Louis XIV est mort en 1715.", "output": "Louis quatorze est mort en mille sept cent quinze."},
|
| 11 |
+
{"input": "L'éthanol est C₂H₅OH.", "output": "L'éthanol est C deux H cinq O H."},
|
| 12 |
+
{"input": "Le virus SARS-CoV-2 et la bactérie E. coli sont étudiés.", "output": "Le virus Sars Cov deux et la bactérie E coli sont étudiés."},
|
| 13 |
+
{"input": "L'UNICEF aide les enfants et l'OMS a publié un rapport.", "output": "L'Unicef aide les enfants et l'O M S a publié un rapport."},
|
| 14 |
+
{"input": "L'action BAC a baissé et le cours de XOM fluctue.", "output": "L'action BAC a baissé et le cours de XOM fluctue."},
|
| 15 |
+
{"input": "a² - b² = (a + b)(a - b).", "output": "a au carré moins b au carré égale a plus b fois a moins b."},
|
| 16 |
+
{"input": "La NASA a lancé une fusée.", "output": "La NASA a lancé une fusée."},
|
| 17 |
+
{"input": "La formule H₂SO₄ est un acide fort.", "output": "La formule H deux S O quatre est un acide fort."},
|
| 18 |
+
{"input": "Contactez admin@server.local.", "output": "Contactez admin arobase server point local."},
|
| 19 |
+
{"input": "L'UNICEF aide les enfants.", "output": "L'Unicef aide les enfants."},
|
| 20 |
+
{"input": "L'ISBN est 3-598-21508-8.", "output": "L'I S B N est trois, cinq cent quatre-vingt-dix-huit, vingt et un mille cinq cent huit, huit."},
|
| 21 |
+
{"input": "8 + (6 × 2) = 20.", "output": "Huit plus, entre parenthèses, six fois deux, égale vingt."}
|
| 22 |
+
]
|
| 23 |
+
}
|
fireredtts3/utils/llm_tn/templates/hi-IN.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"locale": "hi-IN",
|
| 3 |
+
"language_name": "Hindi",
|
| 4 |
+
"system": "You are an accurate text normalizer for hi-IN (Hindi). Your task is to normalize unstandardized text from the following categories into the way it is actually spoken aloud, based on the context: [Cardinal, Date, Decimal, Ordinal, Fraction, Time, Currency, Unit, Electronic Address, Initialism/Acronym, ISBN, Roman Numeral, Telephone, Sports Score, Mathematical Expression, Symbol, Abbreviation, Chemical Formula, Legal Reference, Vehicle/Product Code, Geographic Coordinates, Version Number, License Plate, Musical Notation, Stock Ticker, Biological Classification, Address].\n\nRules:\n- Convert numbers and dates into spoken Hindi (2023 -> दो हज़ार तेईस; 12 अक्टूबर -> बारह अक्टूबर).\n- Use Indian numbering system: 1000 -> हज़ार, 100000 -> लाख, 10000000 -> करोड़.\n- Decimals use दशमलव (2.5 -> दो दशमलव पाँच); percentages use प्रतिशत (15% -> पंद्रह प्रतिशत).\n- Phone numbers, ISBNs and codes are read digit-by-digit (981-1234567 -> नौ आठ एक एक दो तीन चार पाँच छह सात).\n- Roman numerals are converted to Hindi numbers (XVIII -> अठारह).\n- Latin acronyms/initialisms are spelled letter-by-letter (GPS -> G P S, NaOH -> N a O H). Recognizable pronounceable acronyms kept as-is (NASA).\n- Currency amounts use Hindi/Indian names (₹ -> रुपये; $ -> डॉलर).\n- Math operators become words (= -> बराबर है, + -> जोड़, - -> घटा).\n- Keep all other words unchanged. Output ONLY the normalized sentence, with no explanations, no quotes, and no extra text.",
|
| 5 |
+
"examples": [
|
| 6 |
+
{"input": "बैठक 12 अक्टूबर 2023 को होगी।", "output": "बैठक बारह अक्टूबर दो हज़ार तेईस को होगी।"},
|
| 7 |
+
{"input": "उत्पाद की कीमत ₹1,50,000 है।", "output": "उत्पाद की कीमत डेढ़ लाख रुपये है।"},
|
| 8 |
+
{"input": "मुद्रास्फीति इस साल 7.5% तक पहुँच गई।", "output": "मुद्रास्फीति इस साल सात दशमलव पाँच प्रतिशत तक पहुँच गई।"},
|
| 9 |
+
{"input": "मेरा फ़ोन नंबर 981-1234567 है।", "output": "मेरा फ़ोन नंबर नौ आठ एक एक दो तीन चार पाँच छह सात है।"},
|
| 10 |
+
{"input": "डिवाइस GPS और NaOH का उपयोग करता है।", "output": "डिवाइस G P S और N a O H का उपयोग करता है।"},
|
| 11 |
+
{"input": "सूत्र (a+b)² सरल है।", "output": "सूत्र a जोड़ b का वर्ग सरल है।"}
|
| 12 |
+
]
|
| 13 |
+
}
|
fireredtts3/utils/llm_tn/templates/id-ID.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"locale": "id-ID",
|
| 3 |
+
"language_name": "Indonesian",
|
| 4 |
+
"system": "You are an accurate text normalizer for id-ID (Indonesian). Your task is to normalize unstandardized text from the following categories into the way it is actually spoken aloud, based on the context: [Cardinal, Date, Decimal, Ordinal, Fraction, Time, Currency, Unit, Electronic Address, Initialism/Acronym, ISBN, Roman Numeral, Telephone, Sports Score, Mathematical Expression, Symbol, Abbreviation, Chemical Formula, Legal Reference, Vehicle/Product Code, Geographic Coordinates, Version Number, License Plate, Musical Notation, Stock Ticker, Biological Classification, Address].\n\nRules:\n- Convert numbers and dates into spoken Indonesian (2023 -> dua ribu dua puluh tiga; 12 Oktober -> dua belas Oktober).\n- Decimals use koma (2.5 -> dua koma lima); percentages use persen (15% -> lima belas persen).\n- Ordinals use ke- prefix (XVIII -> kedelapan belas).\n- Phone numbers, ISBNs and codes are read digit-by-digit (0812-345678 -> nol delapan satu dua tiga empat lima enam tujuh delapan).\n- Latin acronyms/initialisms are spelled letter-by-letter (GPS -> G P S, NaOH -> N a O H). Recognizable pronounceable acronyms kept as-is (NASA).\n- Currency amounts use Indonesian names (Rp -> rupiah; $ -> dolar).\n- Math operators become words (= -> sama dengan, + -> tambah, - -> kurang).\n- Keep all other words unchanged. Output ONLY the normalized sentence, with no explanations, no quotes, and no extra text.",
|
| 5 |
+
"examples": [
|
| 6 |
+
{"input": "Rapat akan diadakan pada 12 Oktober 2023.", "output": "Rapat akan diadakan pada dua belas Oktober dua ribu dua puluh tiga."},
|
| 7 |
+
{"input": "Harga produk adalah Rp 1.500.000.", "output": "Harga produk adalah satu juta lima ratus ribu rupiah."},
|
| 8 |
+
{"input": "Inflasi mencapai 7,5% tahun ini.", "output": "Inflasi mencapai tujuh koma lima persen tahun ini."},
|
| 9 |
+
{"input": "Nomor telepon saya 0812-345678.", "output": "Nomor telepon saya nol delapan satu dua tiga empat lima enam tujuh delapan."},
|
| 10 |
+
{"input": "Perangkat ini menggunakan GPS dan NaOH.", "output": "Perangkat ini menggunakan G P S dan N a O H."},
|
| 11 |
+
{"input": "Rumus (a+b)² sederhana.", "output": "Rumus a tambah b kuadrat sederhana."}
|
| 12 |
+
]
|
| 13 |
+
}
|
fireredtts3/utils/llm_tn/templates/it-IT.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"locale": "it-IT",
|
| 3 |
+
"language_name": "Italian",
|
| 4 |
+
"system": "You are an accurate text normalizer for it-IT (Italian). Your task is to normalize unstandardized text from the following categories into the way it is actually spoken aloud, based on the context: [Cardinal, Date, Decimal, Ordinal, Fraction, Time, Currency, Unit, Electronic Address, Initialism/Acronym, ISBN, Roman Numeral, Telephone, Sports Score, Mathematical Expression, Symbol, Abbreviation, Chemical Formula, Legal Reference, Vehicle/Product Code, Geographic Coordinates, Version Number, License Plate, Musical Notation, Stock Ticker, Biological Classification, Address].\n\nRules:\n- Spell out all numbers, dates, times, currencies and units the way they are read aloud in Italian (e.g. 2024 -> duemilaventiquattro, 120 km/h -> centoventi chilometri orari).\n- When normalizing acronyms, spell out the individual letters as pronounced in Italian, except when the acronym is a widely recognized and pronounceable name.\n- Convert punctuation that is spoken aloud into words (e.g., write 'punto' instead of a period in URLs, 'chiocciola' for '@' in emails, 'trattino' for hyphens).\n- Segment compound words, websites, and file names into recognizable component words.\n- Common file extensions should be spoken out. Uncommon file extensions should be spelled out.\n- Keep all other words unchanged. Output ONLY the normalized sentence, with no explanations, no quotes, and no extra text.",
|
| 5 |
+
"examples": [
|
| 6 |
+
{"input": "La riunione è fissata per il 15/07/2024.", "output": "La riunione è fissata per il quindici luglio duemilaventiquattro."},
|
| 7 |
+
{"input": "Il prezzo dell'auto è di 25.000€ e la velocità massima è 120 km/h.", "output": "Il prezzo dell'auto è di venticinquemila euro e la velocità massima è centoventi chilometri orari."},
|
| 8 |
+
{"input": "Il 3/4 dei partecipanti ha votato a favore.", "output": "I tre quarti dei partecipanti hanno votato a favore."},
|
| 9 |
+
{"input": "Scrivimi a info@azienda.it o visita www.azienda.it.", "output": "Scrivimi a info chiocciola azienda punto i t o visita vu vu vu punto azienda punto i t."},
|
| 10 |
+
{"input": "Papa Giovanni XXIII regnò dal 1958.", "output": "Papa Giovanni ventitreesimo regnò dal millenovecentocinquantotto."},
|
| 11 |
+
{"input": "Il centralino è 800 123456.", "output": "Il centralino è otto zero zero, uno due tre quattro cinque sei."},
|
| 12 |
+
{"input": "L'UNICEF aiuta i bambini e l'IVA è al 22%.", "output": "L'unicef aiuta i bambini e l'iva è al ventidue percento."},
|
| 13 |
+
{"input": "Vedi D.Lgs. 81/2008.", "output": "Vedi decreto legislativo ottantuno del duemilaotto."},
|
| 14 |
+
{"input": "La S.p.A. è quotata in borsa.", "output": "La società per azioni è quotata in borsa."},
|
| 15 |
+
{"input": "Il glucosio è C₆H₁₂O₆.", "output": "Il glucosio è C sei H dodici O sei."},
|
| 16 |
+
{"input": "La versione è la 8.1.4.", "output": "La versione è la otto punto uno punto quattro."},
|
| 17 |
+
{"input": "Il mio numero è 345 678 9012.", "output": "Il mio numero è tre quattro cinque, sei sette otto, nove zero uno due."},
|
| 18 |
+
{"input": "Menziona @admin nel post.", "output": "Menziona at admin nel post."},
|
| 19 |
+
{"input": "La formula H₂SO₄ è un acido.", "output": "La formula H due S O quattro è un acido."},
|
| 20 |
+
{"input": "Il modello è XJ-5000.", "output": "Il modello è X J, cinquemila."},
|
| 21 |
+
{"input": "L'ISBN è 978-88-430-1234-5.", "output": "L'I S B N è nove sette otto, otto otto, quattro tre zero, uno due tre quattro, cinque."}
|
| 22 |
+
]
|
| 23 |
+
}
|
fireredtts3/utils/llm_tn/templates/ja-JP.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"locale": "ja-JP",
|
| 3 |
+
"language_name": "Japanese",
|
| 4 |
+
"system": "You are an accurate text normalizer for ja-JP (Japanese). Your task is to rewrite the input into the way it is actually read aloud, normalizing numbers, dates, symbols, currencies, units, acronyms and addresses. CRITICAL: Keep the original Japanese writing EXACTLY as-is — every kanji must stay kanji, every hiragana stays hiragana, every katakana word stays the same, and keep the original punctuation 。、?「」. NEVER convert existing kanji/hiragana into other scripts (no romaji, no turning kanji into hiragana). Only convert the specific tokens listed below; leave everything else untouched.\n\nRules:\n- Numbers are written as Chinese numerals (kanji): 2024 -> 二千二十四, 1500 -> 千五百, 100 -> 百, 250000 -> 二十五万, 0 -> 〇 (only in phone numbers/postal codes/serial codes). Decimals use 点 (2.5 -> 二点五).\n- Dates: 5月10日 -> 五月十日; 24/06/15 -> 二十四年六月十五日; 10-30 -> 十月三十日; 2023-07-14 -> 二千二十三年七月十四日; years are read as a full number followed by 年.\n- Times: 3時15分 -> 三時十五分.\n- Fractions are read as '<denominator>分の<numerator>': 4/9 -> 九分の四.\n- Sports scores use 対: 11-9 -> 十一対九.\n- Roman numerals become kanji numbers: II -> 二.\n- Phone numbers and postal codes are read digit by digit in kanji, 0 -> 〇, '+' -> プラス, keeping the original grouping separated by spaces: 03-1234-5678 -> 〇三 一二三四 五六七八.\n- Version numbers: read each dot as 点 and each number as kanji (0 -> 〇): 18.2.0 -> 十八点二点〇.\n- License plates / serial numbers: the kanji place name stays as kanji, numbers are read digit by digit in kanji: 札幌100よ1234 -> 札幌百よ一二三四.\n- ISBN: read 'ISBN' as アイエスビーエヌ, replace each '-' with a space, and read every digit group one digit at a time: ISBN 978-4-00-123456-1 -> アイエスビーエヌ 九七八 四 〇〇 一二三四五六 一.\n- Currencies: read the amount in kanji then the unit word — ¥ -> 円, $ -> ドル(cents -> セント), € -> ユーロ(cents -> セント), £ -> ポンド(pence -> ペンス), ₩ -> ウォン, ₹ -> ルピー. e.g. €75.50 -> 七十五ユーロ五十セント.\n- Units: spell the unit as a katakana word — km -> キロメートル, kg -> キログラム, mL -> ミリリットル, °C -> 度, ㎡ -> 平方メートル, km/h -> キロメートル毎時, kW -> キロワット.\n- Acronyms/initialisms: spell each letter out in katakana joined without spaces (CPU -> シーピーユー, AI -> エーアイ), EXCEPT widely pronounceable names which become their katakana reading (NATO -> ネイトー, UNESCO -> ユネスコ, IKEA -> イケア, Wi-Fi -> ワイファイ, UFO -> ユーフォー).\n- Chemical formulas: read each letter and subscript number in katakana joined without spaces (subscripts 2->ツー 3->スリー 4->フォー 5->ファイブ 6->シックス): H₂O -> エイチツーオー, NaCl -> エヌエーシーエル, CO₂ -> シーオーツー.\n- URLs and emails: read every component in katakana joined without spaces — '@' -> アット, '.' -> ドット, '/' -> スラッシュ, ':' -> コロン, 'http' -> エイチティーティーピー, 'https' -> エイチティーティーピーエス, 'www' -> ダブリューダブリューダブリュー. e.g. user@domain.com -> ユーザーアットドメインドットコム.\n- Addresses with hyphens use 丁目/番地/号: 1-2-3 -> 一丁目二番地三号.\n- Output ONLY the normalized sentence, with no explanations, no quotes, and no extra text.",
|
| 5 |
+
"examples": [
|
| 6 |
+
{"input": "会議は2024年5月10日です。", "output": "会議は二千二十四年五月十日です。"},
|
| 7 |
+
{"input": "電話番号は03-1234-5678です。", "output": "電話番号は〇三 一二三四 五六七八です。"},
|
| 8 |
+
{"input": "費用は€75.50。", "output": "費用は七十五ユーロ五十セント。"},
|
| 9 |
+
{"input": "速度は100km/h。", "output": "速度は百キロメートル毎時。"},
|
| 10 |
+
{"input": "4/9の割合で11-9の大接戦。", "output": "九分の四の割合で十一対九の大接戦。"},
|
| 11 |
+
{"input": "CPUは重要だが、NATOも大切。", "output": "シーピーユーは重要だが、ネイトーも大切。"},
|
| 12 |
+
{"input": "水はH₂O、CO₂を排出。", "output": "水はエイチツーオー、シーオーツーを排出。"},
|
| 13 |
+
{"input": "メールはuser@domain.com。", "output": "メールはユーザーアットドメインドットコム。"},
|
| 14 |
+
{"input": "住所は東京都渋谷区神南1-2-3です。", "output": "住所は東京都渋谷区神南一丁目二番地三号です。"},
|
| 15 |
+
{"input": "ISBN 978-4-00-123456-1。", "output": "アイエスビーエヌ 九七八 四 〇〇 一二三四五六 一。"},
|
| 16 |
+
{"input": "国税通則法第65条、React 18.2.0を使う。", "output": "国税通則法第六十五条、リアクト十八点二点〇を使う。"},
|
| 17 |
+
{"input": "計算すると5 + 3 = 8、π ≈ 3.14。", "output": "計算すると五足す三は八、パイは約三点一四。"},
|
| 18 |
+
{"input": "√9 = 3で、8 ≠ 5。", "output": "九の平方根は三で、八 ノットイコール 五。"},
|
| 19 |
+
{"input": "H₂SO₄は硫酸です。", "output": "エイチツーエスオーフォーは硫酸です。"},
|
| 20 |
+
{"input": "VIII代目社長が就任した。", "output": "八代目社長が就任した。"},
|
| 21 |
+
{"input": "コード進行はC→G→Am→F。", "output": "コード進行はシーからジーからエーマイナーからエフ。"},
|
| 22 |
+
{"input": "VINは1HGCM82633A123456です。", "output": "ブイアイエヌ 一エイチジーシーエム八二六三三エー一二三四五六です。"},
|
| 23 |
+
{"input": "三菱UFJ(8306)の株価。", "output": "三菱ユーエフジェー八三〇六の株価。"}
|
| 24 |
+
]
|
| 25 |
+
}
|
fireredtts3/utils/llm_tn/templates/ko-KR.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"locale": "ko-KR",
|
| 3 |
+
"language_name": "Korean",
|
| 4 |
+
"system": "You are an accurate text normalizer for ko-KR (Korean). Your task is to normalize unstandardized text from the following categories into the way it is actually spoken aloud, based on the context: [Cardinal, Date, Decimal, Ordinal, Fraction, Time, Currency, Unit, Electronic Address, Initialism/Acronym, ISBN, Roman Numeral, Telephone, Sports Score, Mathematical Expression, Symbol, Abbreviation, Chemical Formula, Legal Reference, Vehicle/Product Code, Geographic Coordinates, Version Number, License Plate, Musical Notation, Stock Ticker, Biological Classification, Address].\n\nRules:\n- Convert numbers and dates into spoken Korean (2023년 -> 이천이십삼년, 12월 -> 십이월).\n- Use Korean units: 1000 -> 천, 10000 -> 만, 100000000 -> 억.\n- Decimals use 점 (2.5 -> 이 점 오), percentages use 퍼센트 (15% -> 십오 퍼센트).\n- Phone numbers, ISBNs and codes are read digit-by-digit (010-1234-5678 -> 공일공 일이삼사 오육칠팔).\n- Roman numerals are converted to Korean numbers (XVIII -> 십팔).\n- Latin acronyms/initialisms are spelled letter-by-letter with spaces (GPS -> G P S, NaOH -> N a O H). Recognizeable pronounceable acronyms are kept as-is (NASA, Wi-Fi).\n- Currency amounts use Korean currency names (₩, KRW -> 원; $ -> 달러).\n- Math operators become words (= -> 같다, + -> 더하기, - -> 빼기).\n- Keep all other words unchanged. Output ONLY the normalized sentence, with no explanations, no quotes, and no extra text.",
|
| 5 |
+
"examples": [
|
| 6 |
+
{"input": "2023년 10월 12일에 새로운 제품이 출시되었습니다.", "output": "이천이십삼년 십월 십이일에 새로운 제품이 출시되었습니다."},
|
| 7 |
+
{"input": "이 제품의 가격은 25,000원 입니다.", "output": "이 제품의 가격은 이만 오천원 입니다."},
|
| 8 |
+
{"input": "지지율은 75.5%에 도달했습니다.", "output": "지지율은 칠십오 점 오퍼센트에 도달했습니다."},
|
| 9 |
+
{"input": "제 전화번호는 010-1234-5678 입니다.", "output": "제 전화번호는 공일공 일이삼사 오육칠팔 입니다."},
|
| 10 |
+
{"input": "제18차 개정안에서는 GPS를 사용합니다.", "output": "제십팔차 개정안에서는 G P S를 사용합니다."},
|
| 11 |
+
{"input": "이 공식은 (a+b)² 입니다.", "output": "이 공식은 a 더하기 b의 제곱 입니다."}
|
| 12 |
+
]
|
| 13 |
+
}
|
fireredtts3/utils/llm_tn/templates/lt-LT.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"locale": "lt-LT",
|
| 3 |
+
"language_name": "Lithuanian",
|
| 4 |
+
"system": "You are an accurate text normalizer for lt-LT (Lithuanian). Your task is to normalize unstandardized text from the following categories into the way it is actually spoken aloud, based on the context: [Cardinal, Date, Decimal, Ordinal, Fraction, Time, Currency, Unit, Electronic Address, Initialism/Acronym, ISBN, Roman Numeral, Telephone, Sports Score, Mathematical Expression, Symbol, Abbreviation, Chemical Formula, Legal Reference, Vehicle/Product Code, Geographic Coordinates, Version Number, License Plate, Musical Notation, Stock Ticker, Biological Classification, Address].\n\nGuiding principle (Lithuanian TTS practice): expand only what is genuinely spoken aloud. Do NOT over-normalize. When in doubt, either read a token as an ordinary word or spell out its letters, and never rewrite an abbreviation into its full wording or reinterpret a code semantically.\n\nRules:\n- Spell out numbers, dates, times, currencies, decimals and units the way they are read aloud in Lithuanian, respecting the grammatical case required by the surrounding context (prepositions and the following noun).\n- Decimals: read the fractional part as a fraction, e.g. 0,3 -> trys dešimtosios, 0,25 -> dvidešimt penkios šimtosios, 0,02 -> dvi šimtąsias, 0,026 -> dvidešimt šešios tūkstantosios, 5,2 -> penki ir dvi dešimtąsias; 3,14 -> trys ir keturiolika šimtųjų.\n- Fractions follow Lithuanian conventions (1/2 -> pusę, 1/3 -> trečdalį, 1/4 -> ketvirtadalis, 3/5 -> tris penktadalius), inflected to agree with the sentence.\n- Mathematical operators are read as words: + plius, - minus, × kart, ÷ padalinti iš, = lygu, ≠ nelygu, > daugiau nei, < mažiau nei, ≈ apytiksliai lygu, √ kvadratinė šaknis iš, ∛ kubinė šaknis iš. A number squared -> 'pakelta kvadratu' (3² -> trys pakelta kvadratu); a variable or expression squared -> 'kvadratu' (a² -> a kvadratu). Read variable letters by their Lithuanian names (x -> iks, y -> ygrek, a -> a, b -> bė, c -> cė, r -> er, f -> ef). Do NOT read brackets or parentheses aloud.\n- Letters and acronyms: spell out the letters using their Lithuanian letter names written together without spaces (a, bė, cė, čė, dė, e, ef, gė, haš, i, jot, ka, el, em, en, o, pė, er, es, eš, tė, u, vė, iks, ygrek, zė, žė; W -> vė). E.g. WMT -> vėemtė, SN -> esen, LR -> eler, BK -> bėka. Do NOT use English letter names. Read an acronym as a word only when it is a widely recognized, pronounceable name (e.g. SARS-CoV-2 -> sars kov du). Never expand an abbreviation or acronym into its full wording (keep BK as bėka, not 'Baudžiamojo kodekso').\n- Chemical formulas: spell each element symbol with its Lithuanian letter name and read subscript digits as cardinals, e.g. H₂O -> haš du o, CH₄ -> cė haš keturi, CO₂ -> cė o du.\n- Codes, serial numbers, license plates, product codes, stock tickers and biological or gene codes: read digits one by one (not as a whole number), spell non-pronounceable letter groups by their Lithuanian letter names, read pronounceable parts as words, and do NOT read connecting hyphens aloud (separate the parts by a pause), e.g. AEI 250 -> AEI, du penki nulis.\n- Legal references: read section, part and point numbers as ordinals agreeing with the following noun (154 straipsnio -> šimtas penkiasdešimt ketvirto straipsnio; 2 dalis -> antra dalis). Read dotted article numbers digit by digit with 'taškas' for the dot and do NOT interpret them semantically (CK 1.5 -> cėka vienas taškas penki, never 'pirmo straipsnio penkta dalis'). In reference codes read '-' as 'brūkšnelis' and '/' as 'pasvirasis brūkšnys'.\n- Convert punctuation that is spoken aloud into words in URLs and emails ('taškas' for dots, 'eta' for '@', 'pasvirasis brūkšnys' for '/').\n- Keep all other words unchanged. Output ONLY the normalized sentence, with no explanations, no quotes, and no extra text.",
|
| 5 |
+
"examples": [
|
| 6 |
+
{"input": "Suvalgiau 1/2 picos.", "output": "Suvalgiau pusę picos."},
|
| 7 |
+
{"input": "Nuėjome 3/5 kelio.", "output": "Nuėjome tris penktadalius kelio."},
|
| 8 |
+
{"input": "Jis nubėgo 1/7 maršruto.", "output": "Jis nubėgo septintadalį maršruto."},
|
| 9 |
+
{"input": "0,3 yra daugiau nei 0,25.", "output": "Trys dešimtosios yra daugiau nei dvidešimt penkios šimtosios."},
|
| 10 |
+
{"input": "Paklaida siekė 0,02 mm.", "output": "Paklaida siekė dvi šimtąsias milimetro."},
|
| 11 |
+
{"input": "Jis nubėgo 5,2 km.", "output": "Jis nubėgo penkis ir dvi dešimtąsias kilometro."},
|
| 12 |
+
{"input": "2 + 3 = 5.", "output": "Du plius trys lygu penki."},
|
| 13 |
+
{"input": "20 ÷ 5 = 4.", "output": "Dvidešimt padalinti iš penkių lygu keturi."},
|
| 14 |
+
{"input": "3² = 9.", "output": "Trys pakelta kvadratu lygu devyni."},
|
| 15 |
+
{"input": "(x − a)² + (y − b)² = r²", "output": "Iks minus a kvadratu plius ygrek minus bė kvadratu lygu er kvadratu."},
|
| 16 |
+
{"input": "Konkurse jis užėmė III vietą.", "output": "Konkurse jis užėmė trečią vietą."},
|
| 17 |
+
{"input": "Vandens cheminė formulė yra H₂O.", "output": "Vandens cheminė formulė yra haš du o."},
|
| 18 |
+
{"input": "Metanas žymimas CH₄.", "output": "Metanas žymimas cė haš keturi."},
|
| 19 |
+
{"input": "CK 1.5 straipsnis nustato civilinės teisės principus.", "output": "Cėka vienas taškas penki straipsnis nustato civilinės teisės principus."},
|
| 20 |
+
{"input": "BK 154 straipsnio 2 dalis taikoma šmeižto atvejams.", "output": "Bėka šimtas penkiasdešimt ketvirto straipsnio antra dalis taikoma šmeižto atvejams."},
|
| 21 |
+
{"input": "AMZN užsidarė pakilus akcijų vertei.", "output": "aemzėen užsidarė pakilus akcijų vertei."},
|
| 22 |
+
{"input": "Šio viruso atmaina yra SARS-CoV-2.", "output": "Šio viruso atmaina yra sars kov du."},
|
| 23 |
+
{"input": "Geno kodas yra BRCA1.", "output": "Geno kodas yra bėercėa vienas."},
|
| 24 |
+
{"input": "Automobilio numeris yra AEI 250.", "output": "Automobilio numeris yra AEI, du penki nulis."},
|
| 25 |
+
{"input": "Susitikimas vyks 2025-04-20.", "output": "Susitikimas vyks du tūkstančiai dvidešimt penktų metų balandžio dvidešimtą dieną."},
|
| 26 |
+
{"input": "Sekite @oficialuspuslapis ir naudokite #vacations2024.", "output": "Sekite eta oficialuspuslapis ir naudokite grotažymė vacations du tūkstančiai dvidešimt keturi."},
|
| 27 |
+
{"input": "Įdiegiau 8.6.1 versiją.", "output": "Įdiegiau aštuoni taškas šeši taškas vienas versiją."}
|
| 28 |
+
]
|
| 29 |
+
}
|
fireredtts3/utils/llm_tn/templates/nl-NL.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"locale": "nl-NL",
|
| 3 |
+
"language_name": "Dutch",
|
| 4 |
+
"system": "You are an accurate text normalizer for nl-NL (Dutch). Your task is to normalize unstandardized text from the following categories into the way it is actually spoken aloud, based on the context: [Cardinal, Date, Decimal, Ordinal, Fraction, Time, Currency, Unit, Electronic Address, Initialism/Acronym, ISBN, Roman Numeral, Telephone, Sports Score, Mathematical Expression, Symbol, Abbreviation, Chemical Formula, Legal Reference, Vehicle/Product Code, Geographic Coordinates, Version Number, License Plate, Musical Notation, Stock Ticker, Biological Classification, Address].\n\nRules:\n- Convert numbers and dates into spoken Dutch (2023 -> tweeduizend drieëntwintig; 12 oktober -> twaalf oktober).\n- Decimals use komma (2.5 -> twee komma vijf); percentages use procent (15% -> vijftien procent).\n- Ordinal uses -de/-ste suffix (XVIII -> achttiende).\n- Phone numbers, ISBNs and codes are read digit-by-digit (06-12345678 -> nul zes een twee drie vier vijf zes zeven acht).\n- Latin acronyms/initialisms are spelled letter-by-letter (GPS -> G P S, NaOH -> N a O H). Recognizable pronounceable acronyms kept as-is (NASA).\n- Currency amounts use Dutch names (€ -> euro; $ -> dollar).\n- Math operators become words (= -> is gelijk aan, + -> plus, - -> min).\n- Keep all other words unchanged. Output ONLY the normalized sentence, with no explanations, no quotes, and no extra text.",
|
| 5 |
+
"examples": [
|
| 6 |
+
{"input": "De vergadering is op 12 oktober 2023.", "output": "De vergadering is op twaalf oktober tweeduizend drieëntwintig."},
|
| 7 |
+
{"input": "Het product kost € 1.500,00.", "output": "Het product kost vijftienhonderd euro."},
|
| 8 |
+
{"input": "De inflatie bereikte 7,5% dit jaar.", "output": "De inflatie bereikte zeven komma vijf procent dit jaar."},
|
| 9 |
+
{"input": "Mijn telefoonnummer is 06-12345678.", "output": "Mijn telefoonnummer is nul zes een twee drie vier vijf zes zeven acht."},
|
| 10 |
+
{"input": "Het apparaat gebruikt GPS en NaOH.", "output": "Het apparaat gebruikt G P S en N a O H."},
|
| 11 |
+
{"input": "De formule (a+b)² is eenvoudig.", "output": "De formule a plus b in het kwadraat is eenvoudig."}
|
| 12 |
+
]
|
| 13 |
+
}
|
fireredtts3/utils/llm_tn/templates/pl-PL.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"locale": "pl-PL",
|
| 3 |
+
"language_name": "Polish",
|
| 4 |
+
"system": "You are an accurate text normalizer for pl-PL (Polish). Your task is to normalize unstandardized text from the following categories into the way it is actually spoken aloud, based on the context: [Cardinal, Date, Decimal, Ordinal, Fraction, Time, Currency, Unit, Electronic Address, Initialism/Acronym, ISBN, Roman Numeral, Telephone, Sports Score, Mathematical Expression, Symbol, Abbreviation, Chemical Formula, Legal Reference, Vehicle/Product Code, Geographic Coordinates, Version Number, License Plate, Musical Notation, Stock Ticker, Biological Classification, Address].\n\nRules:\n- Convert numbers and dates into spoken Polish (2023 -> dwa tysiące dwadzieścia trzy; 12 października -> dwunastego października).\n- Decimals use przecinek (2.5 -> dwa przecinek pięć); percentages use procent (15% -> piętnaście procent).\n- Ordinal uses -ty/-ta suffix (XVIII -> osiemnasty).\n- Phone numbers, ISBNs and codes are read digit-by-digit (600-123456 -> sześć zero zero jeden dwa trzy cztery pięć sześć).\n- Latin acronyms/initialisms are spelled letter-by-letter (GPS -> G P S, NaOH -> N a O H). Recognizable pronounceable acronyms kept as-is (NASA).\n- Currency amounts use Polish names (zł -> złotych; $ -> dolarów).\n- Math operators become words (= -> równa się, + -> plus, - -> minus).\n- Keep all other words unchanged. Output ONLY the normalized sentence, with no explanations, no quotes, and no extra text.",
|
| 5 |
+
"examples": [
|
| 6 |
+
{"input": "Spotkanie odbędzie się 12 października 2023.", "output": "Spotkanie odbędzie się dwunastego października dwa tysiące dwadzieścia trzy."},
|
| 7 |
+
{"input": "Produkt kosztuje 1 500 złotych.", "output": "Produkt kosztuje tysiąc pięćset złotych."},
|
| 8 |
+
{"input": "Inflacja osiągnęła 7,5% w tym roku.", "output": "Inflacja osiągnęła siedem przecinek pięć procent w tym roku."},
|
| 9 |
+
{"input": "Mój numer telefonu to 600-123456.", "output": "Mój numer telefonu to sześć zero zero jeden dwa trzy cztery pięć sześć."},
|
| 10 |
+
{"input": "Urządzenie używa GPS i NaOH.", "output": "Urządzenie używa G P S i N a O H."},
|
| 11 |
+
{"input": "Wzór (a+b)² jest prosty.", "output": "Wzór a plus b do kwadratu jest prosty."}
|
| 12 |
+
]
|
| 13 |
+
}
|
fireredtts3/utils/llm_tn/templates/pt-BR.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"locale": "pt-BR",
|
| 3 |
+
"language_name": "Portuguese (Brazil)",
|
| 4 |
+
"system": "You are an accurate text normalizer for pt-BR (Brazilian Portuguese). Your task is to normalize unstandardized text from the following categories into the way it is actually spoken aloud, based on the context: [Cardinal, Date, Decimal, Ordinal, Fraction, Time, Currency, Unit, Electronic Address, Initialism/Acronym, ISBN, Roman Numeral, Telephone, Sports Score, Mathematical Expression, Symbol, Abbreviation, Chemical Formula, Legal Reference, Vehicle/Product Code, Geographic Coordinates, Version Number, License Plate, Musical Notation, Stock Ticker, Biological Classification, Address].\n\nRules:\n- Convert numbers and dates into spoken Portuguese (2023 -> dois mil e vinte e três; 10/12/2023 -> dez de dezembro de dois mil e vinte e três).\n- Decimals use vírgula (2.5 -> dois vírgula cinco); percentages use por cento (15% -> quinze por cento).\n- Ordinals use -º suffix (XVIII -> décimo oitavo).\n- Phone numbers, ISBNs and codes are read digit-by-digit (11-91234-5678 -> um um nove um dois três quatro cinco seis sete oito).\n- Latin acronyms/initialisms are spelled letter-by-letter (GPS -> G P S, NaOH -> N a O H). Recognizable pronounceable acronyms kept as-is (NASA).\n- Currency amounts use Brazilian names (R$ -> real/reais; $ -> dólares).\n- Math operators become words (= -> é igual a, + -> mais, - -> menos).\n- Keep all other words unchanged. Output ONLY the normalized sentence, with no explanations, no quotes, and no extra text.",
|
| 5 |
+
"examples": [
|
| 6 |
+
{"input": "A reunião será em 10 de dezembro de 2023.", "output": "A reunião será em dez de dezembro de dois mil e vinte e três."},
|
| 7 |
+
{"input": "O produto custa R$ 1.500,00.", "output": "O produto custa mil e quinhentos reais."},
|
| 8 |
+
{"input": "A inflação atingiu 7,5% este ano.", "output": "A inflação atingiu sete vírgula cinco por cento este ano."},
|
| 9 |
+
{"input": "Meu telefone é 11-91234-5678.", "output": "Meu telefone é um um nove um dois três quatro cinco seis sete oito."},
|
| 10 |
+
{"input": "O dispositivo usa GPS e NaOH.", "output": "O dispositivo usa G P S e N a O H."},
|
| 11 |
+
{"input": "A fórmula (a+b)² é simples.", "output": "A fórmula a mais b ao quadrado é simples."}
|
| 12 |
+
]
|
| 13 |
+
}
|
fireredtts3/utils/llm_tn/templates/ro-RO.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"locale": "ro-RO",
|
| 3 |
+
"language_name": "Romanian",
|
| 4 |
+
"system": "You are an accurate text normalizer for ro-RO (Romanian). Your task is to normalize unstandardized text from the following categories into the way it is actually spoken aloud, based on the context: [Cardinal, Date, Decimal, Ordinal, Fraction, Time, Currency, Unit, Electronic Address, Initialism/Acronym, ISBN, Roman Numeral, Telephone, Sports Score, Mathematical Expression, Symbol, Abbreviation, Chemical Formula, Legal Reference, Vehicle/Product Code, Geographic Coordinates, Version Number, License Plate, Musical Notation, Stock Ticker, Biological Classification, Address].\n\nRules:\n- Convert numbers and dates into spoken Romanian (2023 -> două mii douăzeci și trei; 12 octombrie -> doisprezece octombrie).\n- Decimals use virgulă (2.5 -> două virgulă cinci); percentages use la sută (15% -> cincisprezece la sută).\n- Ordinal uses -lea/-a suffix (XVIII -> al optsprezecelea).\n- Phone numbers, ISBNs and codes are read digit-by-digit (07-1234567 -> zero șapte unu doi trei patru cinci șase șapte).\n- Latin acronyms/initialisms are spelled letter-by-letter (GPS -> G P S, NaOH -> N a O H). Recognizable pronounceable acronyms kept as-is (NASA).\n- Currency amounts use Romanian names (lei -> lei; $ -> dolari).\n- Math operators become words (= -> este egal cu, + -> plus, - -> minus).\n- Keep all other words unchanged. Output ONLY the normalized sentence, with no explanations, no quotes, and no extra text.",
|
| 5 |
+
"examples": [
|
| 6 |
+
{"input": "Întâlnirea va avea loc pe 12 octombrie 2023.", "output": "Întâlnirea va avea loc pe doisprezece octombrie două mii douăzeci și trei."},
|
| 7 |
+
{"input": "Produsul costă 1.500 de lei.", "output": "Produsul costă o mie cinci sute de lei."},
|
| 8 |
+
{"input": "Inflația a ajuns la 7,5% anul acesta.", "output": "Inflația a ajuns la șapte virgulă cinci la sută anul acesta."},
|
| 9 |
+
{"input": "Numărul meu de telefon este 07-1234567.", "output": "Numărul meu de telefon este zero șapte unu doi trei patru cinci șase șapte."},
|
| 10 |
+
{"input": "Dispozitivul folosește GPS și NaOH.", "output": "Dispozitivul folosește G P S și N a O H."},
|
| 11 |
+
{"input": "Formula (a+b)² este simplă.", "output": "Formula a plus b la pătrat este simplă."}
|
| 12 |
+
]
|
| 13 |
+
}
|
fireredtts3/utils/llm_tn/templates/ru-RU.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"locale": "ru-RU",
|
| 3 |
+
"language_name": "Russian",
|
| 4 |
+
"system": "You are an accurate text normalizer for ru-RU (Russian). Your task is to normalize unstandardized text from the following categories into the way it is actually spoken aloud, based on the context: [Cardinal, Date, Decimal, Ordinal, Fraction, Time, Currency, Unit, Electronic Address, Initialism/Acronym, ISBN, Roman Numeral, Telephone, Sports Score, Mathematical Expression, Symbol, Abbreviation, Chemical Formula, Legal Reference, Vehicle/Product Code, Geographic Coordinates, Version Number, License Plate, Musical Notation, Stock Ticker, Biological Classification, Address].\n\nRules:\n- Convert numbers, dates and times into spoken Russian (2023 год -> две тысячи двадцать третий год, 12 октября -> двенадцатого октября).\n- Decimals use запятая/целых (2.5 -> две целых пять десятых); percentages use процентов (15% -> пятнадцать процентов).\n- Dates in Russian use genitive case for the day month (10/12/2023 -> десять декабря две тысячи двадцать третьего года).\n- Phone numbers, ISBNs and codes are read in groups (8-900-123-45-67 -> восемь девятьсот двадцать три или цифрами).\n- Roman numerals are converted to Russian numbers (XVIII -> восемнадцатый).\n- Latin acronyms/initialisms are spelled letter-by-letter (GPS -> G P S, NaOH -> N a O H). Recognizable produceable acronyms kept as-is (NASA).\n- Currency amounts use Russian names (₽ -> рублей; $ -> долларов; € -> евро).\n- Math operators become words (= -> равно, + -> плюс, - -> минус).\n- Keep all other words unchanged. Output ONLY the normalized sentence, with no explanations, no quotes, and no extra text.",
|
| 5 |
+
"examples": [
|
| 6 |
+
{"input": "Встреча состоится 10 декабря 2023 года.", "output": "Встреча состоится десятого декабря две тысячи двадцать третьего года."},
|
| 7 |
+
{"input": "Цена продукта составляет 2,500 рублей.", "output": "Цена продукта составляет две тысячи пятьсот рублей."},
|
| 8 |
+
{"input": "Инфляция достигла 7.5% в этом году.", "output": "Инфляция достигла семи целых пяти десятых процента в этом году."},
|
| 9 |
+
{"input": "Мой номер телефона 8-900-123-45-67.", "output": "Мой номер телефона восемь девятьсот сто двадцать три сорок пять шестьдесят семь."},
|
| 10 |
+
{"input": "Аппарат использует GPS и формулу NaOH.", "output": "Аппарат использует G P S и формулу N a O H."},
|
| 11 |
+
{"input": "Формула (a+b)² проста.", "output": "Формула a плюс b в квадрате проста."}
|
| 12 |
+
]
|
| 13 |
+
}
|
fireredtts3/utils/llm_tn/templates/th-TH.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"locale": "th-TH",
|
| 3 |
+
"language_name": "Thai",
|
| 4 |
+
"system": "You are an accurate text normalizer for th-TH (Thai). Your task is to normalize unstandardized text from the following categories into the way it is actually spoken aloud, based on the context: [Cardinal, Date, Decimal, Ordinal, Fraction, Time, Currency, Unit, Electronic Address, Initialism/Acronym, ISBN, Roman Numeral, Telephone, Sports Score, Mathematical Expression, Symbol, Abbreviation, Chemical Formula, Legal Reference, Vehicle/Product Code, Geographic Coordinates, Version Number, License Plate, Musical Notation, Stock Ticker, Biological Classification, Address].\n\nRules:\n- Convert numbers and dates into spoken Thai (2023 -> สองพันยี่สิบสาม; 12 ตุลาคม -> สิบสองตุลาคม).\n- Decimals use จุด (2.5 -> สองจุดห้า); percentages use เปอร์เซ็นต์ (15% -> สิบห้าเปอร์เซ็นต์).\n- Phone numbers, ISBNs and codes are read digit-by-digit (081-2345678 -> ศูนย์แปดหนึ่งสองสามสี่ห้าหกเจ็ดแปด).\n- Latin acronyms/initialisms are spelled letter-by-letter (GPS -> G P S, NaOH -> N a O H). Recognizable pronounceable acronyms kept as-is (NASA).\n- Currency amounts use Thai names (บาท -> บาท; ดอลลาร์ -> ดอลลาร์).\n- Math operators become words (= -> เท่ากับ, + -> บวก, - -> ลบ).\n- Keep all other words unchanged. Output ONLY the normalized sentence, with no explanations, no quotes, and no extra text.",
|
| 5 |
+
"examples": [
|
| 6 |
+
{"input": "การประชุมจะจัดขึ้นในวันที่ 12 ตุลาคม 2023", "output": "การประชุมจะจัดขึ้นในวันที่ สิบสอง ตุลาคม สองพันยี่สิบสาม"},
|
| 7 |
+
{"input": "สินค้าราคา 1,500 บาท", "output": "สินค้าราคา หนึ่งพันห้าร้อยบาท"},
|
| 8 |
+
{"input": "อัตราเงินเฟ้อถึง 7.5% ในปีนี้", "output": "อัตราเงินเฟ้อถึง เจ็ดจุดห้าเปอร์เซ็นต์ ในปีนี้"},
|
| 9 |
+
{"input": "เบอร์โทรศัพท์ของฉันคือ 081-2345678", "output": "เบอร์โทรศัพท์ของฉันคือ ศูนย์แปดหนึ่งสองสามสี่ห้าหกเจ็ดแปด"},
|
| 10 |
+
{"input": "อุปกรณ์ใช้ GPS และ NaOH", "output": "อุปกรณ์ใช้ G P S และ N a O H"},
|
| 11 |
+
{"input": "สูตร (a+b)² ง่ายมาก", "output": "สูตร a บวก b กำลังสอง ง่ายมาก"}
|
| 12 |
+
]
|
| 13 |
+
}
|
fireredtts3/utils/llm_tn/templates/tr-TR.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"locale": "tr-TR",
|
| 3 |
+
"language_name": "Turkish",
|
| 4 |
+
"system": "You are an accurate text normalizer for tr-TR (Turkish). Your task is to normalize unstandardized text from the following categories into the way it is actually spoken aloud, based on the context: [Cardinal, Date, Decimal, Ordinal, Fraction, Time, Currency, Unit, Electronic Address, Initialism/Acronym, ISBN, Roman Numeral, Telephone, Sports Score, Mathematical Expression, Symbol, Abbreviation, Chemical Formula, Legal Reference, Vehicle/Product Code, Geographic Coordinates, Version Number, License Plate, Musical Notation, Stock Ticker, Biological Classification, Address].\n\nRules:\n- Convert numbers and dates into spoken Turkish (2023 -> iki bin yirmi üç; 12 Ekim -> on iki Ekim).\n- Decimals use virgül (2.5 -> iki virgül beş); percentages use yüzde (15% -> yüzde on beş).\n- Ordinals use -ıncı/-nci suffix (XVIII -> on sekizinci).\n- Phone numbers, ISBNs and codes are read digit-by-digit (0532-1234567 -> sıfır beş üç iki bir iki üç dört beş altı yedi).\n- Latin acronyms/initialisms are spelled letter-by-letter (GPS -> G P S, NaOH -> N a O H). Recognizable pronounceable acronyms kept as-is (NASA).\n- Currency amounts use Turkish names (₺ -> lira; $ -> dolar; € -> avro).\n- Math operators become words (= -> eşittir, + -> artı, - -> eksi).\n- Keep all other words unchanged. Output ONLY the normalized sentence, with no explanations, no quotes, and no extra text.",
|
| 5 |
+
"examples": [
|
| 6 |
+
{"input": "Toplantı 12 Ekim 2023 tarihinde yapılacak.", "output": "Toplantı on iki Ekim iki bin yirmi üç tarihinde yapılacak."},
|
| 7 |
+
{"input": "Ürünün fiyatı 1.500 liradır.", "output": "Ürünün fiyatı bin beş yüz liradır."},
|
| 8 |
+
{"input": "Enflasyon bu yıl %7,5'e ulaştı.", "output": "Enflasyon bu yıl yüzde yedi virgül beşe ulaştı."},
|
| 9 |
+
{"input": "Telefon numaram 0532-1234567.", "output": "Telefon numaram sıfır beş üç iki bir iki üç dört beş altı yedi."},
|
| 10 |
+
{"input": "Cihaz GPS ve NaOH formülünü kullanır.", "output": "Cihaz G P S ve N a O H formülünü kullanır."},
|
| 11 |
+
{"input": "(a+b)² formülü basittir.", "output": "a artı b'nin karesi formülü basittir."}
|
| 12 |
+
]
|
| 13 |
+
}
|
fireredtts3/utils/llm_tn/templates/uk-UA.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"locale": "uk-UA",
|
| 3 |
+
"language_name": "Ukrainian",
|
| 4 |
+
"system": "You are an accurate text normalizer for uk-UA (Ukrainian). Your task is to normalize unstandardized text from the following categories into the way it is actually spoken aloud, based on the context: [Cardinal, Date, Decimal, Ordinal, Fraction, Time, Currency, Unit, Electronic Address, Initialism/Acronym, ISBN, Roman Numeral, Telephone, Sports Score, Mathematical Expression, Symbol, Abbreviation, Chemical Formula, Legal Reference, Vehicle/Product Code, Geographic Coordinates, Version Number, License Plate, Musical Notation, Stock Ticker, Biological Classification, Address].\n\nRules:\n- Convert numbers and dates into spoken Ukrainian (2023 -> дві тисячі двадцять три; 12 жовтня -> дванадцятого жовтня).\n- Decimals use кома (2.5 -> дві кома п'ять); percentages use відсотків (15% -> п'ятнадцять відсотків).\n- Dates use genitive case for day/month (10/12/2023 -> десяте грудня дві тисячі двадцять третього року).\n- Phone numbers, ISBNs and codes are read in groups (8-900-123-45-67 -> вісім дев'ятьсот двадцять три сорок п'ять шістдесят сім).\n- Latin acronyms/initialisms are spelled letter-by-letter (GPS -> G P S, NaOH -> N a O H). Recognizable pronounceable acronyms kept as-is (NASA).\n- Currency amounts use Ukrainian names (₴ -> гривень; $ -> доларів; € -> євро).\n- Math operators become words (= -> дорівнює, + -> плюс, - -> мінус).\n- Keep all other words unchanged. Output ONLY the normalized sentence, with no explanations, no quotes, and no extra text.",
|
| 5 |
+
"examples": [
|
| 6 |
+
{"input": "Зустріч відбудеться 12 жовтня 2023 року.", "output": "Зустріч відбудеться дванадцятого жовтня дві тисячі двадцять третього року."},
|
| 7 |
+
{"input": "Ціна продукту становить 1 500 гривень.", "output": "Ціна продукту становить одну тисячу п'ятсот гривень."},
|
| 8 |
+
{"input": "Інфляція досягла 7,5% цього року.", "output": "Інфляція досягла семи цілих п'яти десятих відсотка цього року."},
|
| 9 |
+
{"input": "Мій номер телефону 8-900-123-45-67.", "output": "Мій номер телефону вісім дев'ятсот двадцять три сорок п'ять шістдесят сім."},
|
| 10 |
+
{"input": "Пристрій використовує GPS та NaOH.", "output": "Пристрій використовує G P S та N a O H."},
|
| 11 |
+
{"input": "Формула (a+b)² проста.", "output": "Формула a плюс b у квадраті проста."}
|
| 12 |
+
]
|
| 13 |
+
}
|
fireredtts3/utils/llm_tn/templates/vi-VN.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"locale": "vi-VN",
|
| 3 |
+
"language_name": "Vietnamese",
|
| 4 |
+
"system": "You are an accurate text normalizer for vi-VN (Vietnamese). Your task is to normalize unstandardized text from the following categories into the way it is actually spoken aloud, based on the context: [Cardinal, Date, Decimal, Ordinal, Fraction, Time, Currency, Unit, Electronic Address, Initialism/Acronym, ISBN, Roman Numeral, Telephone, Sports Score, Mathematical Expression, Symbol, Abbreviation, Chemical Formula, Legal Reference, Vehicle/Product Code, Geographic Coordinates, Version Number, License Plate, Musical Notation, Stock Ticker, Biological Classification, Address].\n\nRules:\n- Convert numbers and dates into spoken Vietnamese (2023 -> hai nghìn không trăm hai mươi ba / hai ngàn hai mươi ba; 12/10/2023 -> ngày mười hai tháng mười năm hai nghìn không trăm hai mươi ba).\n- Decimals use phẩy (2.5 -> hai phẩy năm); percentages use phần trăm (15% -> mười lăm phần trăm).\n- Ordinal uses thứ prefix (XVIII -> thứ mười tám).\n- Phone numbers, ISBNs and codes are read digit-by-digit (090-1234567 -> không chín không một hai ba bốn năm sáu bảy).\n- Latin acronyms/initialisms are spelled letter-by-letter (GPS -> G P S, NaOH -> N a O H). Recognizable pronounceable acronyms kept as-is (NASA).\n- Currency amounts use Vietnamese names (₫ -> đồng; $ -> đô la).\n- Math operators become words (= -> bằng, + -> cộng, - -> trừ).\n- Keep all other words unchanged. Output ONLY the normalized sentence, with no explanations, no quotes, and no extra text.",
|
| 5 |
+
"examples": [
|
| 6 |
+
{"input": "Cuộc họp diễn ra vào ngày 12/10/2023.", "output": "Cuộc họp diễn ra vào ngày mười hai tháng mười năm hai nghìn không trăm hai mươi ba."},
|
| 7 |
+
{"input": "Sản phẩm có giá 1.500.000 đồng.", "output": "Sản phẩm có giá một triệu năm trăm nghìn đồng."},
|
| 8 |
+
{"input": "Lạm phát đạt 7,5% trong năm nay.", "output": "Lạm phát đạt bảy phẩy năm phần trăm trong năm nay."},
|
| 9 |
+
{"input": "Số điện thoại của tôi là 090-1234567.", "output": "Số điện thoại của tôi là không chín không một hai ba bốn năm sáu bảy."},
|
| 10 |
+
{"input": "Thiết bị dùng GPS và NaOH.", "output": "Thiết bị dùng G P S và N a O H."},
|
| 11 |
+
{"input": "Công thức (a+b)² rất đơn giản.", "output": "Công thức a cộng b bình phương rất đơn giản."}
|
| 12 |
+
]
|
| 13 |
+
}
|
fireredtts3/utils/llm_tn/templates/zh-CN.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"locale": "zh-CN",
|
| 3 |
+
"language_name": "Chinese",
|
| 4 |
+
"system": "You are an accurate text normalizer for zh-CN (Simplified Mandarin Chinese). Your task is to normalize unstandardized text from the following categories into the way it is actually spoken aloud, based on the context: [Cardinal, Date, Decimal, Ordinal, Fraction, Time, Currency, Unit, Electronic Address, Initialism/Acronym, ISBN, Roman Numeral, Telephone, Sports Score, Mathematical Expression, Symbol, Abbreviation, Chemical Formula, Legal Reference, Vehicle/Product Code, Geographic Coordinates, Version Number, License Plate, Musical Notation, Stock Ticker, Biological Classification, Address].\n\nRules:\n- Convert numbers, symbols and dates into spoken Chinese characters (Hanzi).\n- Years in dates are read digit-by-digit (2023年 -> 二零二三年); plain cardinal numbers are read as a whole (6000 -> 六千); decimals use 点 (2.5 -> 二点五).\n- Phone numbers, ISBNs and account numbers are read digit-by-digit (138-1234-5678 -> 一三八一二三四五六七八).\n- Percentages and fractions follow Chinese conventions (15% -> 百分之十五, 3/4 -> 四分之三, 3:1 -> 三比一).\n- Roman numerals are converted to Chinese numbers (XVIII -> 十八, II型 -> 二型).\n- Strings of Latin letters (acronyms, initialisms, chemical formulas, ISBN labels, technical unit abbreviations) are read letter-by-letter: insert a single space between every letter and KEEP the original capitalization (GPS -> G P S, NaOH -> N a O H, ISBN -> I S B N, 1TB -> 一T B, iOS -> i O S). Exception: a widely recognized pronounceable English word is kept as-is (NASA, Wi-Fi).\n- Latin abbreviations/titles are expanded to their full ENGLISH form, not translated (Dr. -> Doctor, Mr. -> Mister).\n- Convert spoken punctuation into words (URLs: '.' -> 点; emails: '@' -> 艾特 or 'at'). Math operators become words (|x| -> 绝对值, = -> 等于, + -> 加, - -> 减).\n- Keep all other words unchanged. Output ONLY the normalized sentence, with no explanations, no quotes, and no extra text.",
|
| 5 |
+
"examples": [
|
| 6 |
+
{"input": "2023年10月12日,苹果公司发布了iOS 17.1。", "output": "二零二三年十月十二日,苹果公司发布了I O S十七点一。"},
|
| 7 |
+
{"input": "这辆车的售价为¥250,000,最高时速120km/h。", "output": "这辆车的售价为二十五万元,最高时速一百二十千米每小时。"},
|
| 8 |
+
{"input": "μ's支持率达到了75.5%,比分是3:1。", "output": "mu's支持率达到了百分之七十五点五,比分是三比一。"},
|
| 9 |
+
{"input": "我每天上班都使用GPS导航,氢氧化钠是NaOH。", "output": "我每天上班都使用G P S导航,氢氧化钠是N a O H。"},
|
| 10 |
+
{"input": "第XVIII修正案规定数据量为1TB。", "output": "第十八修正案规定数据量为一T B。"},
|
| 11 |
+
{"input": "我的电话号码是 138-1234-5678。", "output": "我的电话号码是一三八一二三四五六七八。"},
|
| 12 |
+
{"input": "请拨打10086。", "output": "请拨打一零零八六。"},
|
| 13 |
+
{"input": "我的邮箱是happyzhang@hotmail.com", "output": "我的邮箱是 happy zhang at hot mail 点 com。"},
|
| 14 |
+
{"input": "Dr. Zhang的诊断是II型糖尿病。", "output": "Doctor Zhang的诊断是二型糖尿病。"},
|
| 15 |
+
{"input": "车架号LFV3A28K9E3012345。", "output": "车架号L F V三A二八K九E三零一二三四五。"},
|
| 16 |
+
{"input": "MMXXIII年,超级碗LVI在这里举办。", "output": "二零二三年,第五十六届超级碗在这里举办。"},
|
| 17 |
+
{"input": "机票花了AUD100,酒店CAD$25.75。", "output": "机票花了一百澳元,酒店二十五点七五加元。"},
|
| 18 |
+
{"input": "这个公式是(σ+b)²。", "output": "这个公式是sigma加B的平方。"},
|
| 19 |
+
{"input": "软件更新到v2023.12.31。", "output": "软件更新到V二零二三点一二点三一。"},
|
| 20 |
+
{"input": "这是一首5/4拍的爵士乐。", "output": "这是一首五四拍的爵士乐。"},
|
| 21 |
+
{"input": "他开车速度在6.5~7.5m/s。", "output": "他开车速度在六点五到七点五米每秒。"},
|
| 22 |
+
{"input": "罗马的坐标是41.90°N 12.50°E。", "output": "罗马的坐标是北纬四十一点九零度东经十二点五零度。"},
|
| 23 |
+
{"input": "这块手表售价HK$1000。", "output": "这块手表售价一千港币。"}
|
| 24 |
+
]
|
| 25 |
+
}
|
fireredtts3/utils/llm_tn/text_normalizer.py
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""LLM-based few-shot Text Normalizer (TN) for TTS front-ends."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
import re
|
| 8 |
+
import threading
|
| 9 |
+
import time
|
| 10 |
+
import unicodedata
|
| 11 |
+
import urllib.error
|
| 12 |
+
import urllib.request
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
from typing import Dict, List, Optional
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
from dotenv import load_dotenv
|
| 18 |
+
load_dotenv()
|
| 19 |
+
except ImportError:
|
| 20 |
+
pass
|
| 21 |
+
|
| 22 |
+
DEFAULT_API_URL = os.environ.get("LLM_TN_API_URL")
|
| 23 |
+
DEFAULT_API_KEY = os.environ.get("LLM_TN_API_KEY")
|
| 24 |
+
DEFAULT_MODEL = os.environ.get("LLM_TN_MODEL")
|
| 25 |
+
|
| 26 |
+
TEMPLATES_DIR = Path(__file__).resolve().parent / "templates"
|
| 27 |
+
|
| 28 |
+
SUPPORTED_LOCALES = [
|
| 29 |
+
"ar-SA", "cs-CZ", "de-DE", "el-GR", "en-US", "es-MX",
|
| 30 |
+
"fi-FI", "fr-FR", "hi-IN", "id-ID", "it-IT", "ja-JP",
|
| 31 |
+
"ko-KR", "lt-LT", "nl-NL", "pl-PL", "pt-BR", "ro-RO", "ru-RU",
|
| 32 |
+
"th-TH", "tr-TR", "uk-UA", "vi-VN", "zh-CN",
|
| 33 |
+
]
|
| 34 |
+
|
| 35 |
+
# Meta FastText language-id model (lid.176).
|
| 36 |
+
# Download: curl -L -o .../models/lid.176.ftz https://dl.fbaipublicfiles.com/fasttext/supervised-models/lid.176.ftz
|
| 37 |
+
FASTTEXT_MODEL_PATH = Path(__file__).resolve().parent / "models" / "lid.176.ftz"
|
| 38 |
+
|
| 39 |
+
# Map FastText ISO-639 language codes to our locale codes.
|
| 40 |
+
_FT_LANG_TO_LOCALE = {
|
| 41 |
+
"ar": "ar-SA", "cs": "cs-CZ", "de": "de-DE", "el": "el-GR",
|
| 42 |
+
"en": "en-US", "es": "es-MX", "fi": "fi-FI", "fr": "fr-FR",
|
| 43 |
+
"hi": "hi-IN", "id": "id-ID", "it": "it-IT", "ja": "ja-JP",
|
| 44 |
+
"ko": "ko-KR", "lt": "lt-LT", "nl": "nl-NL", "pl": "pl-PL",
|
| 45 |
+
"pt": "pt-BR", "ro": "ro-RO", "ru": "ru-RU", "th": "th-TH",
|
| 46 |
+
"tr": "tr-TR", "uk": "uk-UA", "vi": "vi-VN", "zh": "zh-CN",
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
# Characters indicative of a specific Latin-script language.
|
| 50 |
+
_LT_CHARS = set("ąčęėįšųūž") # Lithuanian
|
| 51 |
+
_DE_CHARS = set("äöüß") # German
|
| 52 |
+
_FR_CHARS = set("àâæçéèêëîïôœùûÿ") # French
|
| 53 |
+
_ES_CHARS = set("ñ¿¡") # Spanish
|
| 54 |
+
_IT_CHARS = set("àèéìíîòóùú") # Italian
|
| 55 |
+
|
| 56 |
+
# Stop-words to disambiguate Latin-script languages.
|
| 57 |
+
_STOPWORDS: Dict[str, set] = {
|
| 58 |
+
"de-DE": {"und", "der", "die", "das", "ist", "ein", "nicht", "mit", "den",
|
| 59 |
+
"von", "sie", "ich", "auf", "für", "auch", "wird", "im", "am"},
|
| 60 |
+
"en-US": {"the", "and", "is", "are", "of", "to", "in", "for", "on", "with",
|
| 61 |
+
"this", "that", "you", "it", "have", "was", "please", "at"},
|
| 62 |
+
"es-MX": {"el", "la", "los", "las", "de", "que", "es", "un", "una", "por",
|
| 63 |
+
"con", "para", "su", "se", "no", "más", "está", "del"},
|
| 64 |
+
"fr-FR": {"le", "la", "les", "des", "est", "et", "un", "une", "vous", "que",
|
| 65 |
+
"pour", "dans", "qui", "pas", "sur", "au", "du", "ce"},
|
| 66 |
+
"it-IT": {"il", "la", "le", "di", "che", "è", "un", "una", "per", "con",
|
| 67 |
+
"non", "sono", "del", "della", "gli", "nel", "ha", "si"},
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
class TextNormalizer:
|
| 72 |
+
"""Few-shot, LLM-driven multilingual text normalizer."""
|
| 73 |
+
|
| 74 |
+
def __init__(
|
| 75 |
+
self,
|
| 76 |
+
model: Optional[str] = None,
|
| 77 |
+
api_url: Optional[str] = None,
|
| 78 |
+
api_key: Optional[str] = None,
|
| 79 |
+
templates_dir: Path = TEMPLATES_DIR,
|
| 80 |
+
temperature: float = 0.1,
|
| 81 |
+
max_tokens: int = 1024,
|
| 82 |
+
timeout: int = 30,
|
| 83 |
+
max_retries: int = 3,
|
| 84 |
+
default_locale: str = "zh-CN",
|
| 85 |
+
use_fasttext: bool = True,
|
| 86 |
+
fasttext_path: Path = FASTTEXT_MODEL_PATH,
|
| 87 |
+
) -> None:
|
| 88 |
+
self.model = model or DEFAULT_MODEL
|
| 89 |
+
self.api_url = api_url or DEFAULT_API_URL
|
| 90 |
+
self.api_key = api_key or DEFAULT_API_KEY
|
| 91 |
+
if not self.api_url:
|
| 92 |
+
raise ValueError(
|
| 93 |
+
"TextNormalizer requires an LLM API URL. Set LLM_TN_API_URL in "
|
| 94 |
+
"your environment / .env file, or pass api_url explicitly."
|
| 95 |
+
)
|
| 96 |
+
if not self.api_key:
|
| 97 |
+
raise ValueError(
|
| 98 |
+
"TextNormalizer requires an LLM API key. Set LLM_TN_API_KEY in "
|
| 99 |
+
"your environment / .env file, or pass api_key explicitly."
|
| 100 |
+
)
|
| 101 |
+
self.templates_dir = Path(templates_dir)
|
| 102 |
+
self.temperature = temperature
|
| 103 |
+
self.max_tokens = max_tokens
|
| 104 |
+
self.timeout = timeout
|
| 105 |
+
self.max_retries = max_retries
|
| 106 |
+
self.default_locale = default_locale
|
| 107 |
+
self.stop = []
|
| 108 |
+
self.use_fasttext = use_fasttext
|
| 109 |
+
self.fasttext_path = Path(fasttext_path)
|
| 110 |
+
self._ft_model = None
|
| 111 |
+
self._ft_loaded = False
|
| 112 |
+
self._ft_lock = threading.Lock()
|
| 113 |
+
self._templates: Dict[str, Dict] = {}
|
| 114 |
+
self._load_templates()
|
| 115 |
+
|
| 116 |
+
def _get_fasttext(self):
|
| 117 |
+
"""Lazily load FastText lid.176 model (thread-safe)."""
|
| 118 |
+
if not self.use_fasttext:
|
| 119 |
+
return None
|
| 120 |
+
if self._ft_loaded:
|
| 121 |
+
return self._ft_model
|
| 122 |
+
with self._ft_lock:
|
| 123 |
+
if self._ft_loaded:
|
| 124 |
+
return self._ft_model
|
| 125 |
+
self._ft_loaded = True
|
| 126 |
+
try:
|
| 127 |
+
import fasttext
|
| 128 |
+
if self.fasttext_path.exists():
|
| 129 |
+
self._ft_model = fasttext.load_model(str(self.fasttext_path))
|
| 130 |
+
else:
|
| 131 |
+
self._ft_model = None
|
| 132 |
+
except Exception:
|
| 133 |
+
self._ft_model = None
|
| 134 |
+
return self._ft_model
|
| 135 |
+
|
| 136 |
+
def _fasttext_locale(self, text: str):
|
| 137 |
+
"""Predict a supported locale via FastText, or None if unavailable."""
|
| 138 |
+
model = self._get_fasttext()
|
| 139 |
+
if model is None:
|
| 140 |
+
return None
|
| 141 |
+
snippet = text.replace("\n", " ").strip()
|
| 142 |
+
if not snippet:
|
| 143 |
+
return None
|
| 144 |
+
try:
|
| 145 |
+
# Use low-level predictor to avoid numpy-2.0 incompatibility in predict().
|
| 146 |
+
with self._ft_lock:
|
| 147 |
+
preds = model.f.predict(snippet, 1, 0.0, "strict")
|
| 148 |
+
except Exception:
|
| 149 |
+
return None
|
| 150 |
+
if not preds:
|
| 151 |
+
return None
|
| 152 |
+
_prob, label = preds[0]
|
| 153 |
+
lang = label.replace("__label__", "")
|
| 154 |
+
return _FT_LANG_TO_LOCALE.get(lang)
|
| 155 |
+
|
| 156 |
+
def _load_templates(self) -> None:
|
| 157 |
+
for loc in SUPPORTED_LOCALES:
|
| 158 |
+
path = self.templates_dir / f"{loc}.json"
|
| 159 |
+
if path.exists():
|
| 160 |
+
with path.open(encoding="utf-8") as f:
|
| 161 |
+
self._templates[loc] = json.load(f)
|
| 162 |
+
|
| 163 |
+
def available_locales(self) -> List[str]:
|
| 164 |
+
return sorted(self._templates.keys())
|
| 165 |
+
|
| 166 |
+
_TN_SYMBOLS = set("0123456789@#$£€¥%&<>=^`~/\\+*°²³×÷")
|
| 167 |
+
|
| 168 |
+
def needs_normalization(self, text: str) -> bool:
|
| 169 |
+
"""Return False for plain prose (no digits/symbols needing TN)."""
|
| 170 |
+
if not text or not text.strip():
|
| 171 |
+
return False
|
| 172 |
+
for ch in text:
|
| 173 |
+
if ch in self._TN_SYMBOLS:
|
| 174 |
+
return True
|
| 175 |
+
if re.search(r"https?://|www\.|[\w.+-]+@[\w-]+\.", text):
|
| 176 |
+
return True
|
| 177 |
+
if re.search(r"\b[A-Z]{2,}\b", text):
|
| 178 |
+
return True
|
| 179 |
+
if re.search(r"\b[IVXLCDM]{2,}\b", text):
|
| 180 |
+
return True
|
| 181 |
+
if re.search(r"\b[A-Za-zÀ-ÿ]{1,4}\.", text):
|
| 182 |
+
return True
|
| 183 |
+
return False
|
| 184 |
+
|
| 185 |
+
@staticmethod
|
| 186 |
+
def _script_stats(text: str) -> Dict[str, int]:
|
| 187 |
+
stats = {"han": 0, "kana": 0, "latin": 0, "hangul": 0, "cyrillic": 0,
|
| 188 |
+
"arabic": 0, "devanagari": 0, "thai": 0, "greek": 0}
|
| 189 |
+
for ch in text:
|
| 190 |
+
code = ord(ch)
|
| 191 |
+
if 0x3040 <= code <= 0x30FF: # kana
|
| 192 |
+
stats["kana"] += 1
|
| 193 |
+
elif (0x4E00 <= code <= 0x9FFF or
|
| 194 |
+
0x3400 <= code <= 0x4DBF or
|
| 195 |
+
0xF900 <= code <= 0xFAFF or
|
| 196 |
+
0x20000 <= code <= 0x2FA1F):
|
| 197 |
+
stats["han"] += 1
|
| 198 |
+
elif 0xAC00 <= code <= 0xD7AF: # hangul
|
| 199 |
+
stats["hangul"] += 1
|
| 200 |
+
elif 0x0400 <= code <= 0x04FF or 0x0500 <= code <= 0x052F: # cyrillic
|
| 201 |
+
stats["cyrillic"] += 1
|
| 202 |
+
elif 0x0600 <= code <= 0x06FF or 0x0750 <= code <= 0x077F: # arabic
|
| 203 |
+
stats["arabic"] += 1
|
| 204 |
+
elif 0x0900 <= code <= 0x097F: # devanagari
|
| 205 |
+
stats["devanagari"] += 1
|
| 206 |
+
elif 0x0E00 <= code <= 0x0E7F: # thai
|
| 207 |
+
stats["thai"] += 1
|
| 208 |
+
elif 0x0370 <= code <= 0x03FF: # greek
|
| 209 |
+
stats["greek"] += 1
|
| 210 |
+
elif ch.isalpha() and code < 0x250: # latin
|
| 211 |
+
stats["latin"] += 1
|
| 212 |
+
return stats
|
| 213 |
+
|
| 214 |
+
def _detect_latin_locale(self, text: str) -> Optional[str]:
|
| 215 |
+
"""Disambiguate Latin text among de/en/es/fr/it/lt; None if ambiguous."""
|
| 216 |
+
low = text.lower()
|
| 217 |
+
chars = set(low)
|
| 218 |
+
if chars & _LT_CHARS:
|
| 219 |
+
return "lt-LT"
|
| 220 |
+
if "ß" in low or (chars & _DE_CHARS):
|
| 221 |
+
de_signal = "ß" in low
|
| 222 |
+
else:
|
| 223 |
+
de_signal = False
|
| 224 |
+
if "ñ" in low or "¿" in low or "¡" in low:
|
| 225 |
+
return "es-MX"
|
| 226 |
+
words = re.findall(r"[a-zà-ÿ]+", low)
|
| 227 |
+
if not words:
|
| 228 |
+
return None
|
| 229 |
+
wordset = set(words)
|
| 230 |
+
scores = {loc: len(wordset & sw) for loc, sw in _STOPWORDS.items()}
|
| 231 |
+
if chars & _DE_CHARS:
|
| 232 |
+
scores["de-DE"] += 1
|
| 233 |
+
if de_signal:
|
| 234 |
+
scores["de-DE"] += 2
|
| 235 |
+
if chars & _FR_CHARS:
|
| 236 |
+
scores["fr-FR"] += 1
|
| 237 |
+
if chars & _IT_CHARS:
|
| 238 |
+
scores["it-IT"] += 1
|
| 239 |
+
best = max(scores, key=scores.get)
|
| 240 |
+
if scores[best] == 0:
|
| 241 |
+
return None
|
| 242 |
+
ranked = sorted(scores.values(), reverse=True)
|
| 243 |
+
if len(ranked) > 1 and ranked[0] == ranked[1]:
|
| 244 |
+
return None
|
| 245 |
+
return best
|
| 246 |
+
|
| 247 |
+
def detect_locale(self, text: str, use_llm_fallback: bool = True,
|
| 248 |
+
fallback_locale: Optional[str] = None) -> str:
|
| 249 |
+
"""Detect the locale of *text* among SUPPORTED_LOCALES."""
|
| 250 |
+
fb = fallback_locale or self.default_locale
|
| 251 |
+
stats = self._script_stats(text)
|
| 252 |
+
if stats["kana"] > 0:
|
| 253 |
+
return "ja-JP"
|
| 254 |
+
if stats["han"] > 0:
|
| 255 |
+
return "zh-CN"
|
| 256 |
+
if re.search(r"[\u3000-\u303F\uFF01-\uFF0F\uFF1A-\uFF20]", text):
|
| 257 |
+
return "zh-CN"
|
| 258 |
+
if any(stats[k] > 0 for k in ("hangul", "cyrillic", "arabic",
|
| 259 |
+
"devanagari", "thai", "greek")):
|
| 260 |
+
ft_guess = self._fasttext_locale(text)
|
| 261 |
+
if ft_guess is not None:
|
| 262 |
+
return ft_guess
|
| 263 |
+
if use_llm_fallback:
|
| 264 |
+
llm_guess = self._detect_locale_llm(text)
|
| 265 |
+
if llm_guess in SUPPORTED_LOCALES:
|
| 266 |
+
return llm_guess
|
| 267 |
+
return fb
|
| 268 |
+
if stats["latin"] == 0 or not re.search(r"[A-Za-zÀ-ÿ]{3,}", text):
|
| 269 |
+
return fb
|
| 270 |
+
ft_guess = self._fasttext_locale(text)
|
| 271 |
+
if ft_guess is not None:
|
| 272 |
+
return ft_guess
|
| 273 |
+
guess = self._detect_latin_locale(text)
|
| 274 |
+
if guess is not None:
|
| 275 |
+
return guess
|
| 276 |
+
if use_llm_fallback:
|
| 277 |
+
llm_guess = self._detect_locale_llm(text)
|
| 278 |
+
if llm_guess in SUPPORTED_LOCALES:
|
| 279 |
+
return llm_guess
|
| 280 |
+
return fb
|
| 281 |
+
|
| 282 |
+
def _detect_locale_llm(self, text: str) -> Optional[str]:
|
| 283 |
+
prompt = (
|
| 284 |
+
"Identify the language of the text below and answer with EXACTLY one "
|
| 285 |
+
"of these locale codes and nothing else: "
|
| 286 |
+
+ ", ".join(SUPPORTED_LOCALES)
|
| 287 |
+
+ f".\n\nText: {text}\nLocale:"
|
| 288 |
+
)
|
| 289 |
+
messages = [
|
| 290 |
+
{"role": "system", "content": "You are a precise language identifier."},
|
| 291 |
+
{"role": "user", "content": prompt},
|
| 292 |
+
]
|
| 293 |
+
try:
|
| 294 |
+
out = self._chat(messages, max_tokens=16)
|
| 295 |
+
except Exception:
|
| 296 |
+
return None
|
| 297 |
+
out = out.strip()
|
| 298 |
+
for loc in SUPPORTED_LOCALES:
|
| 299 |
+
if loc.lower() in out.lower():
|
| 300 |
+
return loc
|
| 301 |
+
lang = out.lower()[:2]
|
| 302 |
+
for loc in SUPPORTED_LOCALES:
|
| 303 |
+
if loc.lower().startswith(lang):
|
| 304 |
+
return loc
|
| 305 |
+
return None
|
| 306 |
+
|
| 307 |
+
def _build_messages(self, text: str, locale: str) -> List[Dict[str, str]]:
|
| 308 |
+
tmpl = self._templates.get(locale)
|
| 309 |
+
if tmpl is None:
|
| 310 |
+
raise ValueError(f"No template available for locale '{locale}'")
|
| 311 |
+
messages: List[Dict[str, str]] = [{"role": "system", "content": tmpl["system"]}]
|
| 312 |
+
for ex in tmpl.get("examples", []):
|
| 313 |
+
messages.append({"role": "user", "content": ex["input"]})
|
| 314 |
+
messages.append({"role": "assistant", "content": ex["output"]})
|
| 315 |
+
messages.append({"role": "user", "content": text})
|
| 316 |
+
return messages
|
| 317 |
+
|
| 318 |
+
@staticmethod
|
| 319 |
+
def _clean_output(text: str, raw_input: str) -> str:
|
| 320 |
+
text = text.split("\n\n", 1)[0]
|
| 321 |
+
out = text.strip()
|
| 322 |
+
if len(out) >= 2 and out[0] in "\"'“”「" and out[-1] in "\"'“”」":
|
| 323 |
+
out = out[1:-1].strip()
|
| 324 |
+
return out or raw_input
|
| 325 |
+
|
| 326 |
+
def normalize(
|
| 327 |
+
self,
|
| 328 |
+
text: str,
|
| 329 |
+
locale: Optional[str] = None,
|
| 330 |
+
auto_detect: bool = True,
|
| 331 |
+
force: bool = False,
|
| 332 |
+
fallback_locale: Optional[str] = None,
|
| 333 |
+
) -> str:
|
| 334 |
+
"""Normalize *text* into its spoken form."""
|
| 335 |
+
if text is None:
|
| 336 |
+
return text
|
| 337 |
+
if not text.strip():
|
| 338 |
+
return text
|
| 339 |
+
if not force and not self.needs_normalization(text):
|
| 340 |
+
return text
|
| 341 |
+
if locale is None:
|
| 342 |
+
if auto_detect:
|
| 343 |
+
locale = self.detect_locale(text, fallback_locale=fallback_locale)
|
| 344 |
+
else:
|
| 345 |
+
locale = fallback_locale or self.default_locale
|
| 346 |
+
if locale not in self._templates:
|
| 347 |
+
locale = fallback_locale or self.default_locale
|
| 348 |
+
messages = self._build_messages(text, locale)
|
| 349 |
+
result = self._chat(messages)
|
| 350 |
+
return self._clean_output(result, text)
|
| 351 |
+
|
| 352 |
+
def _build_request(self, messages: List[Dict[str, str]], max_tokens: int):
|
| 353 |
+
"""Build OpenAI-compatible chat/completions request.
|
| 354 |
+
|
| 355 |
+
CoT disabled based on model name:
|
| 356 |
+
``deepseek`` -> ``thinking: {"type": "disabled"}``
|
| 357 |
+
``qwen`` -> ``chat_template_kwargs: {"enable_thinking": False}``
|
| 358 |
+
"""
|
| 359 |
+
model = (self.model or "").lower()
|
| 360 |
+
payload = {
|
| 361 |
+
"model": self.model,
|
| 362 |
+
"messages": messages,
|
| 363 |
+
"stream": False,
|
| 364 |
+
"max_tokens": max_tokens,
|
| 365 |
+
"temperature": self.temperature,
|
| 366 |
+
}
|
| 367 |
+
if "deepseek" in model:
|
| 368 |
+
payload["thinking"] = {"type": "disabled"}
|
| 369 |
+
elif "qwen" in model:
|
| 370 |
+
payload["chat_template_kwargs"] = {"enable_thinking": False}
|
| 371 |
+
if self.stop:
|
| 372 |
+
payload["stop"] = self.stop
|
| 373 |
+
headers = {
|
| 374 |
+
"Content-Type": "application/json",
|
| 375 |
+
"Authorization": f"Bearer {self.api_key}",
|
| 376 |
+
}
|
| 377 |
+
return payload, headers
|
| 378 |
+
|
| 379 |
+
def _parse_response(self, obj: Dict) -> str:
|
| 380 |
+
msg = obj["choices"][0]["message"]
|
| 381 |
+
content = msg.get("content")
|
| 382 |
+
if content:
|
| 383 |
+
return content
|
| 384 |
+
reasoning = msg.get("reasoning_content")
|
| 385 |
+
if reasoning:
|
| 386 |
+
return reasoning
|
| 387 |
+
raise RuntimeError("empty completion content")
|
| 388 |
+
|
| 389 |
+
def _chat(self, messages: List[Dict[str, str]], max_tokens: Optional[int] = None) -> str:
|
| 390 |
+
payload, headers = self._build_request(messages, max_tokens or self.max_tokens)
|
| 391 |
+
data = json.dumps(payload).encode("utf-8")
|
| 392 |
+
last_err: Optional[Exception] = None
|
| 393 |
+
for attempt in range(self.max_retries):
|
| 394 |
+
try:
|
| 395 |
+
req = urllib.request.Request(
|
| 396 |
+
self.api_url, data=data, headers=headers, method="POST"
|
| 397 |
+
)
|
| 398 |
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
| 399 |
+
body = resp.read().decode("utf-8")
|
| 400 |
+
obj = json.loads(body)
|
| 401 |
+
return self._parse_response(obj)
|
| 402 |
+
except (urllib.error.URLError, urllib.error.HTTPError, RuntimeError,
|
| 403 |
+
KeyError, IndexError, json.JSONDecodeError) as exc:
|
| 404 |
+
last_err = exc
|
| 405 |
+
time.sleep(min(2 ** attempt, 8))
|
| 406 |
+
raise RuntimeError(f"LLM request failed after {self.max_retries} retries: {last_err}")
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
if __name__ == "__main__":
|
| 410 |
+
import sys
|
| 411 |
+
tn = TextNormalizer()
|
| 412 |
+
if len(sys.argv) > 1:
|
| 413 |
+
sample = " ".join(sys.argv[1:])
|
| 414 |
+
loc = tn.detect_locale(sample)
|
| 415 |
+
print(f"[locale={loc}] {tn.normalize(sample)}")
|
| 416 |
+
else:
|
| 417 |
+
demos = [
|
| 418 |
+
"He bought 2.5 kg of apples for $5.99 on 10/12/2023.",
|
| 419 |
+
"会议定于2023年5月20日举行,电话138-1234-5678。",
|
| 420 |
+
"这是一句不需要归一化的普通中文。",
|
| 421 |
+
"Le vol AF123 partira le 1er juin à 18h45.",
|
| 422 |
+
]
|
| 423 |
+
for d in demos:
|
| 424 |
+
loc = tn.detect_locale(d)
|
| 425 |
+
need = tn.needs_normalization(d)
|
| 426 |
+
print(f"\nINPUT : {d}")
|
| 427 |
+
print(f"locale={loc} needs_tn={need}")
|
| 428 |
+
print(f"OUTPUT: {tn.normalize(d)}")
|
fireredtts3/utils/text_normalize.py
ADDED
|
@@ -0,0 +1,600 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
TTS 文本前端(text front-end)工具。
|
| 3 |
+
|
| 4 |
+
some functions are adapted from https://github.com/FunAudioLLM/CosyVoice/blob/main/cosyvoice/utils/frontend_utils.py
|
| 5 |
+
some functions are adapted from https://github.com/OpenBMB/VoxCPM/blob/main/src/voxcpm/utils/text_normalize.py
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import re
|
| 11 |
+
from typing import Callable, List, Optional
|
| 12 |
+
|
| 13 |
+
try:
|
| 14 |
+
import regex
|
| 15 |
+
except ImportError: # pragma: no cover
|
| 16 |
+
regex = None
|
| 17 |
+
|
| 18 |
+
# --------------------------------------------------------------------------- #
|
| 19 |
+
# 1) 文本初步清洗
|
| 20 |
+
# --------------------------------------------------------------------------- #
|
| 21 |
+
def preprocess_text(sentence: str) -> str:
|
| 22 |
+
"""对文本做最基础的 utf-8 级清洗(参考 FireRedTTS.preprocess_text)。"""
|
| 23 |
+
if not sentence:
|
| 24 |
+
return ""
|
| 25 |
+
|
| 26 |
+
# utf-8 编码/解码,忽略无法解码的字节
|
| 27 |
+
sentence = bytes(sentence, "utf-8").decode("utf-8", "ignore")
|
| 28 |
+
|
| 29 |
+
if regex is not None:
|
| 30 |
+
# 去除零宽字符(保留零宽连接符 \u200d)
|
| 31 |
+
sentence = regex.sub(r"[\p{Cf}--[\u200d]]", "", sentence, flags=regex.V1)
|
| 32 |
+
# 去除私有区字符
|
| 33 |
+
sentence = regex.sub(r"\p{Co}", "", sentence)
|
| 34 |
+
else:
|
| 35 |
+
sentence = re.sub(r"[\u200b-\u200f\u2028-\u202f\u2060-\u206f\ufeff]", "", sentence)
|
| 36 |
+
sentence = re.sub(r"[\ue000-\uf8ff]", "", sentence)
|
| 37 |
+
|
| 38 |
+
sentence = sentence.replace("\u00a0", " ") # 不换行空格 -> 普通空格
|
| 39 |
+
sentence = sentence.replace("\ufffd", "") # 替换字符 -> 空
|
| 40 |
+
# 行分隔符 / 段分隔符 -> 换行(U+2028 LINE SEPARATOR, U+2029 PARAGRAPH SEPARATOR)
|
| 41 |
+
sentence = sentence.replace("\u2028", "\n")
|
| 42 |
+
sentence = sentence.replace("\u2029", "\n")
|
| 43 |
+
|
| 44 |
+
return sentence
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# --------------------------------------------------------------------------- #
|
| 48 |
+
# 2) Markdown 清洗 + 去除表情符号(参考 VoxCPM.utils.text_normalize)
|
| 49 |
+
# --------------------------------------------------------------------------- #
|
| 50 |
+
def clean_markdown(md_text: str) -> str:
|
| 51 |
+
"""去除 Markdown 语法,保留纯文本内容。"""
|
| 52 |
+
if not md_text:
|
| 53 |
+
return md_text
|
| 54 |
+
# 去除图片语法 
|
| 55 |
+
md_text = re.sub(r"!\[[^\]]*\]\([^\)]+\)", "", md_text)
|
| 56 |
+
# 去除链接但保留文本 [text](url) -> text
|
| 57 |
+
md_text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", md_text)
|
| 58 |
+
# 替换无序列表符号
|
| 59 |
+
md_text = re.sub(r"^(\s*)-\s+", r"\1", md_text, flags=re.MULTILINE)
|
| 60 |
+
# 去除标题符号(#)
|
| 61 |
+
md_text = re.sub(r"^#{1,6}\s*", "", md_text, flags=re.MULTILINE)
|
| 62 |
+
# 只处理句首的加粗/斜体/删除线等强调标记(**text**、*text*、__text__、~~text~~),
|
| 63 |
+
# 行首的 * / ~ 几乎只可能是 markdown 标记;正文中的素 * / ~ (乘法、波浪号)不受影响。
|
| 64 |
+
md_text = re.sub(r"^\s*\*\*([^*]+)\*\*", r"\1", md_text, flags=re.MULTILINE)
|
| 65 |
+
md_text = re.sub(r"^\s*__([^_]+)__", r"\1", md_text, flags=re.MULTILINE)
|
| 66 |
+
md_text = re.sub(r"^\s*\*([^*]+)\*", r"\1", md_text, flags=re.MULTILINE)
|
| 67 |
+
md_text = re.sub(r"^\s*~~([^~]+)~~", r"\1", md_text, flags=re.MULTILINE)
|
| 68 |
+
# 行首多余的离散 * / ~(例如独立的 * item)
|
| 69 |
+
md_text = re.sub(r"^\s*[*~]\s+", "", md_text, flags=re.MULTILINE)
|
| 70 |
+
# 去除多余空行
|
| 71 |
+
md_text = re.sub(r"\n\s*\n", "\n", md_text)
|
| 72 |
+
md_text = md_text.strip()
|
| 73 |
+
return md_text
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def remove_emoji(text: str) -> str:
|
| 77 |
+
"""去除表情符号(Emoji_Presentation 与带变体选择符的 Emoji)。"""
|
| 78 |
+
if regex is None:
|
| 79 |
+
return text
|
| 80 |
+
return regex.compile(
|
| 81 |
+
r"\p{Emoji_Presentation}|\p{Emoji}\uFE0F", flags=regex.UNICODE
|
| 82 |
+
).sub("", text)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
_CJK = r"\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff"
|
| 86 |
+
_RE_CJK_SPACE_CJK = re.compile(r"([{}])\s+([{}])".format(_CJK, _CJK))
|
| 87 |
+
_RE_CJK_SPACE_LATIN = re.compile(r"([{0}])\s+([a-zA-Z0-9])".format(_CJK))
|
| 88 |
+
_RE_LATIN_SPACE_CJK = re.compile(r"([a-zA-Z0-9])\s+([{0}])".format(_CJK))
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def clean_tn_spaces(text: str) -> str:
|
| 92 |
+
"""清理 TN 后在汉字之间、汉字与英文/数字之间多余的空格。
|
| 93 |
+
|
| 94 |
+
TN(尤其 LLM-based)经常在汉字/数字/符号间插入不必要的空格,如:
|
| 95 |
+
"今天 天气 很好" → "今天天气很好"
|
| 96 |
+
"苹果 AI 助手" → "苹果AI助手"
|
| 97 |
+
"""
|
| 98 |
+
if not text:
|
| 99 |
+
return text
|
| 100 |
+
text = _RE_CJK_SPACE_CJK.sub(r"\1\2", text)
|
| 101 |
+
text = _RE_CJK_SPACE_LATIN.sub(r"\1\2", text)
|
| 102 |
+
text = _RE_LATIN_SPACE_CJK.sub(r"\1\2", text)
|
| 103 |
+
return text
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
# 符号化简(参考 FireRedTTS utils.py 的 symbol_reduction)。
|
| 107 |
+
# 只保留"输出会被后续 _SYMBOL_TO_SPACE / _SYMBOL_TO_COMMA 处理"的映射:
|
| 108 |
+
# - 映射到 ~(→ 逗号)、·(→ 空格)、...(→ 逗号)的条目。
|
| 109 |
+
# 其余映射的目的地(如 " ( ) . : ; ! ? - % + = 等)本身就在 _WETEXT_KEEP
|
| 110 |
+
# 允许集中,原字符也会被保留,映射纯属多余,故全部去掉。
|
| 111 |
+
_SYMBOL_REDUCTION = {
|
| 112 |
+
# 波浪/破折线 → ASCII ~(随后被 _SYMBOL_TO_COMMA 化简为逗号)
|
| 113 |
+
"〜": "~", "~": "~",
|
| 114 |
+
# 间隔点/��点 → ·(随后被 _SYMBOL_TO_SPACE 化简为空格)
|
| 115 |
+
"・": "·", "•": "·", "‧": "·",
|
| 116 |
+
# 省略号变体 → "..."(避免被 _SYMBOL_TO_COMMA 吞成一个逗号)
|
| 117 |
+
"…": "...", "⋯": "...", "〰": "...", "﹏": "...",
|
| 118 |
+
}
|
| 119 |
+
_SYMBOL_TO_SPACE = re.compile(r"[·•‧│|¦/\\]")
|
| 120 |
+
_SYMBOL_TO_COMMA = re.compile(r"[…~&*%$#^:;/\\|]+")
|
| 121 |
+
_WETEXT_KEEP = re.compile(
|
| 122 |
+
r"[^"
|
| 123 |
+
r"\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff" # CJK 汉字
|
| 124 |
+
r"0-9A-Za-z"
|
| 125 |
+
r",。、?!:;…—~·"
|
| 126 |
+
r".,:;!?()\[\]'\"\-_"
|
| 127 |
+
r"\s"
|
| 128 |
+
r"]"
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def _apply_symbol_reduction(text: str) -> str:
|
| 133 |
+
"""把全角/异体符号化简为对应的半角标准符号(参考 symbol_reduction)。"""
|
| 134 |
+
return "".join(_SYMBOL_REDUCTION.get(ch, ch) for ch in text)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def clean_wetext_output(text: str) -> str:
|
| 138 |
+
# 清洗 wetext 归一化后的中/英文输出,把无法读出的符号化简为逗号或空格。
|
| 139 |
+
if not text:
|
| 140 |
+
return text
|
| 141 |
+
text = _apply_symbol_reduction(text)
|
| 142 |
+
text = _SYMBOL_TO_SPACE.sub(" ", text)
|
| 143 |
+
text = _SYMBOL_TO_COMMA.sub(",", text)
|
| 144 |
+
text = _WETEXT_KEEP.sub("", text)
|
| 145 |
+
# 规整空白,连续逗号合并
|
| 146 |
+
text = re.sub(r"[ \t]+", " ", text).strip()
|
| 147 |
+
text = re.sub(r"[,,]{2,}", ",", text)
|
| 148 |
+
text = re.sub(r"\s+[,,]", ",", text)
|
| 149 |
+
text = re.sub(r"[,,]+\s*$", "", text)
|
| 150 |
+
return text
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def clean_text(text: str, llm_normalizer: Optional[Callable[[str], str]] = None) -> str:
|
| 154 |
+
"""文本初步清洗:utf-8 清洗 + markdown 清洗 + 去除表情符号 + 规整空白。
|
| 155 |
+
|
| 156 |
+
Args:
|
| 157 |
+
text: 输入文本。
|
| 158 |
+
llm_normalizer: 可选,一个可调用的文本归一化器(通常为
|
| 159 |
+
``llm_tn`` 的 ``TextNormalizer.normalize``)。若提供,则在基础
|
| 160 |
+
清洗之后调用它以支持多语种 TN(例如日语、韩语等 wetext
|
| 161 |
+
不支持的语种)。该调用方需自行保证模板齐全。
|
| 162 |
+
"""
|
| 163 |
+
if not text:
|
| 164 |
+
return text
|
| 165 |
+
text = preprocess_text(text)
|
| 166 |
+
text = clean_markdown(text)
|
| 167 |
+
text = remove_emoji(text)
|
| 168 |
+
text = re.sub(r"\s+", " ", text)
|
| 169 |
+
text = text.strip()
|
| 170 |
+
if llm_normalizer is not None:
|
| 171 |
+
try:
|
| 172 |
+
text = llm_normalizer(text)
|
| 173 |
+
except Exception as e: # 失败时回退为清洗后的原文
|
| 174 |
+
print(f"[WARN] llm_tn normalization failed, fallback to raw text: {e}", flush=True)
|
| 175 |
+
text = re.sub(r"\s+", " ", text).strip()
|
| 176 |
+
return text
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
# --------------------------------------------------------------------------- #
|
| 180 |
+
# 3) 分句分段(参考 VoxCPM.utils.text_normalize.split_paragraph)
|
| 181 |
+
# --------------------------------------------------------------------------- #
|
| 182 |
+
def _is_decimal_dot(text: str, i: int) -> bool:
|
| 183 |
+
"""判断位置 *i* 上的 ``.`` / ``:`` 是否为数字内部标点(而非句号/冒号)。
|
| 184 |
+
|
| 185 |
+
例如 ``€1.2M`` 中的小数点 (前后是数字)、``19:30`` 中的冒号 (前后是数字)。
|
| 186 |
+
用于分句时不切断这些数字内部的标点。
|
| 187 |
+
"""
|
| 188 |
+
return (i > 0 and i + 1 < len(text)
|
| 189 |
+
and text[i - 1].isdigit()
|
| 190 |
+
and text[i + 1].isdigit())
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def split_paragraph(
|
| 194 |
+
text: str,
|
| 195 |
+
tokenize: Optional[Callable[[str], List[str]]] = None,
|
| 196 |
+
lang: str = "zh",
|
| 197 |
+
token_max_n: int = 80,
|
| 198 |
+
token_min_n: int = 60,
|
| 199 |
+
merge_len: int = 20
|
| 200 |
+
) -> List[str]:
|
| 201 |
+
"""将段落按句号/标点拆分为若干句,并合并过短的句子。
|
| 202 |
+
|
| 203 |
+
分句逻辑:
|
| 204 |
+
1. 每个句子最大长度 ``token_max_n``、最小长度 ``token_min_n``;
|
| 205 |
+
若末尾句子长度小于 ``merge_len`` 则并入前一句。
|
| 206 |
+
2. 按语种计算句子长度(zh 按字符数,其余按 token 数)。
|
| 207 |
+
3. 按标点切分句子。
|
| 208 |
+
|
| 209 |
+
注意:紧邻两边都是数字的 ``.``(如 ``€1.2M``、``0.85``、``v1.8.3``)
|
| 210 |
+
及 ``:``(如 ``19:30``)被视为数字内部标点,不做切分。
|
| 211 |
+
"""
|
| 212 |
+
def _measure(_text: str) -> int:
|
| 213 |
+
if lang == "zh":
|
| 214 |
+
return len(_text)
|
| 215 |
+
if tokenize is not None:
|
| 216 |
+
n = tokenize(_text)
|
| 217 |
+
if isinstance(n, int):
|
| 218 |
+
return n
|
| 219 |
+
return len(n)
|
| 220 |
+
return len(_text)
|
| 221 |
+
|
| 222 |
+
def calc_utt_length(_text: str) -> int:
|
| 223 |
+
return _measure(_text)
|
| 224 |
+
|
| 225 |
+
def should_merge(_text: str) -> bool:
|
| 226 |
+
return _measure(_text) < merge_len
|
| 227 |
+
|
| 228 |
+
if lang == "zh":
|
| 229 |
+
pounc = ["。", "?", "!", ";", "、", ".", "?", "!", ";"]
|
| 230 |
+
else:
|
| 231 |
+
pounc = [".", "?", "!", ";"]
|
| 232 |
+
|
| 233 |
+
# 按标点切分(跳过数字内部的小数点 ``.`` )
|
| 234 |
+
st = 0
|
| 235 |
+
utts: List[str] = []
|
| 236 |
+
for i, c in enumerate(text):
|
| 237 |
+
if c in pounc:
|
| 238 |
+
# 对 . 和 : 检查是否在数字内部(如 €1.2M)
|
| 239 |
+
if c == "." and _is_decimal_dot(text, i):
|
| 240 |
+
continue
|
| 241 |
+
if len(text[st:i]) > 0:
|
| 242 |
+
utts.append(text[st:i] + c)
|
| 243 |
+
if i + 1 < len(text) and text[i + 1] in ['"', "”"]:
|
| 244 |
+
tmp = utts.pop(-1)
|
| 245 |
+
utts.append(tmp + text[i + 1])
|
| 246 |
+
st = i + 2
|
| 247 |
+
else:
|
| 248 |
+
st = i + 1
|
| 249 |
+
trailing = text[st:] if st < len(text) else ""
|
| 250 |
+
if trailing:
|
| 251 |
+
utts.append(trailing)
|
| 252 |
+
elif len(utts) == 0:
|
| 253 |
+
utts.append(text + ("。" if lang == "zh" else ""))
|
| 254 |
+
# 合并过短 / 超长处理
|
| 255 |
+
final_utts: List[str] = []
|
| 256 |
+
cur_utt = ""
|
| 257 |
+
for utt in utts:
|
| 258 |
+
if calc_utt_length(cur_utt + utt) > token_max_n and calc_utt_length(cur_utt) > token_min_n:
|
| 259 |
+
final_utts.append(cur_utt)
|
| 260 |
+
cur_utt = ""
|
| 261 |
+
cur_utt = cur_utt + utt
|
| 262 |
+
if len(cur_utt) > 0:
|
| 263 |
+
if should_merge(cur_utt) and len(final_utts) != 0:
|
| 264 |
+
final_utts[-1] = final_utts[-1] + cur_utt
|
| 265 |
+
else:
|
| 266 |
+
final_utts.append(cur_utt)
|
| 267 |
+
|
| 268 |
+
return final_utts
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
# --------------------------------------------------------------------------- #
|
| 272 |
+
# 4) 语种自动判定(复用 llm_tn/text_normalizer.py 的 fasttext)
|
| 273 |
+
# --------------------------------------------------------------------------- #
|
| 274 |
+
# 将 llm_tn 的 locale(如 "zh-CN"、"en-US")映射到 fireredtts3 的 lang tag。
|
| 275 |
+
_LOCALE_TO_LANG_TAG = {
|
| 276 |
+
"zh-CN": "Chinese",
|
| 277 |
+
"en-US": "English",
|
| 278 |
+
"ja-JP": "Japanese",
|
| 279 |
+
"ko-KR": "Korean",
|
| 280 |
+
"es-MX": "Spanish",
|
| 281 |
+
"fr-FR": "French",
|
| 282 |
+
"ru-RU": "Russian",
|
| 283 |
+
"ar-SA": "Arabic",
|
| 284 |
+
"tr-TR": "Turkish",
|
| 285 |
+
"id-ID": "Indonesian",
|
| 286 |
+
"pt-BR": "Portuguese",
|
| 287 |
+
"it-IT": "Italian",
|
| 288 |
+
"nl-NL": "Dutch",
|
| 289 |
+
"vi-VN": "Vietnamese",
|
| 290 |
+
"de-DE": "German",
|
| 291 |
+
"uk-UA": "Ukrainian",
|
| 292 |
+
"th-TH": "Thai",
|
| 293 |
+
"pl-PL": "Polish",
|
| 294 |
+
"ro-RO": "Romanian",
|
| 295 |
+
"el-GR": "Greek",
|
| 296 |
+
"cs-CZ": "Czech",
|
| 297 |
+
"fi-FI": "Finnish",
|
| 298 |
+
"hi-IN": "Hindi",
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
# lang tag -> llm_tn locale(反向映射)。当上层已指定语种时,用它把已知语种
|
| 302 |
+
# 传给 llm_tn,从而覆盖 llm_tn 内部的自动检测,避免误判(如俄语被当成中文)。
|
| 303 |
+
_LANG_TAG_TO_LOCALE = {v: k for k, v in _LOCALE_TO_LANG_TAG.items()}
|
| 304 |
+
# 中文方言统一走 zh-CN 的 llm_tn 模板(方言无独立模板)。
|
| 305 |
+
_LANG_DIALECT_TO_LOCALE = {
|
| 306 |
+
"Cantonese": "zh-CN",
|
| 307 |
+
"ZH_Anhui": "zh-CN", "ZH_Fujian": "zh-CN", "ZH_Gansu": "zh-CN",
|
| 308 |
+
"ZH_Guizhou": "zh-CN", "ZH_Hebei": "zh-CN", "ZH_Henan": "zh-CN",
|
| 309 |
+
"ZH_Hubei": "zh-CN", "ZH_Hunan": "zh-CN", "ZH_Jiangxi": "zh-CN",
|
| 310 |
+
"ZH_Liaoning": "zh-CN", "ZH_Minnan": "zh-CN", "ZH_Ningxia": "zh-CN",
|
| 311 |
+
"ZH_Shaanxi": "zh-CN", "ZH_Shandong": "zh-CN", "ZH_Shanghai": "zh-CN",
|
| 312 |
+
"ZH_Shanxi": "zh-CN", "ZH_Sichuan": "zh-CN", "ZH_Tianjin": "zh-CN",
|
| 313 |
+
"ZH_Wenzhou": "zh-CN", "ZH_Wu": "zh-CN", "ZH_Yunnan": "zh-CN",
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
def lang_tag_to_locale(lang_tag: str) -> Optional[str]:
|
| 318 |
+
"""把 lang tag 映射为 llm_tn 的 locale。
|
| 319 |
+
|
| 320 |
+
支持标准语言 tag(如 ``"Russian"``)与中文方言 tag(如 ``"ZH_Sichuan"``、
|
| 321 |
+
``"Cantonese"``)。方言无独立 llm_tn 模板,统一映射到 ``zh-CN``。
|
| 322 |
+
无法识别时返回 ``None``。
|
| 323 |
+
"""
|
| 324 |
+
if not lang_tag:
|
| 325 |
+
return None
|
| 326 |
+
if lang_tag in _LANG_TAG_TO_LOCALE:
|
| 327 |
+
return _LANG_TAG_TO_LOCALE[lang_tag]
|
| 328 |
+
if lang_tag in _LANG_DIALECT_TO_LOCALE:
|
| 329 |
+
return _LANG_DIALECT_TO_LOCALE[lang_tag]
|
| 330 |
+
return None
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
def detect_language(
|
| 334 |
+
text: str,
|
| 335 |
+
fasttext_detector: Optional[Callable[[str], Optional[str]]] = None,
|
| 336 |
+
default_locale: str = "zh-CN",
|
| 337 |
+
) -> str:
|
| 338 |
+
"""自动判定文本语种
|
| 339 |
+
内部使用 Meta FastText ``lid.176.ftz`` 模型。
|
| 340 |
+
"""
|
| 341 |
+
if fasttext_detector is not None:
|
| 342 |
+
try:
|
| 343 |
+
locale = fasttext_detector(text)
|
| 344 |
+
if locale:
|
| 345 |
+
tag = _LOCALE_TO_LANG_TAG.get(locale)
|
| 346 |
+
if tag:
|
| 347 |
+
return tag
|
| 348 |
+
except Exception:
|
| 349 |
+
pass
|
| 350 |
+
|
| 351 |
+
# 回退:含汉字 -> 中文;含日文假名 -> 日文;否则英文
|
| 352 |
+
if re.search(r"[\u4e00-\u9fff]", text):
|
| 353 |
+
return "Chinese"
|
| 354 |
+
if re.search(r"[\u3040-\u30ff\u31f0-\u31ff]", text): # 平假名/片假名
|
| 355 |
+
return "Japanese"
|
| 356 |
+
return "English"
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
# --------------------------------------------------------------------------- #
|
| 360 |
+
# 5) 归一化器工厂
|
| 361 |
+
# --------------------------------------------------------------------------- #
|
| 362 |
+
def build_wetext_normalizer() -> Optional[Callable[[str], str]]:
|
| 363 |
+
"""构建 wetext 归一化器,仅处理中文和英文。
|
| 364 |
+
|
| 365 |
+
实现参考 VoxCPM 的
|
| 366 |
+
``voxcpm/utils/text_normalize.py``:使用 ``wetext.Normalizer``(zh / en)
|
| 367 |
+
做文本归一化,并用 ``contains_chinese`` 自动判定中/英。仅支持中文和英文,
|
| 368 |
+
其他语种需使用 ``llm_tn``。
|
| 369 |
+
|
| 370 |
+
Returns:
|
| 371 |
+
一个以 ``text`` 为输入、返回归一化文本的可调用对象;若 wetext 不可用
|
| 372 |
+
返回 ``None``。
|
| 373 |
+
"""
|
| 374 |
+
try:
|
| 375 |
+
from wetext import Normalizer
|
| 376 |
+
|
| 377 |
+
zh_tn_model = Normalizer(lang="zh", operator="tn", remove_erhua=True)
|
| 378 |
+
en_tn_model = Normalizer(lang="en", operator="tn")
|
| 379 |
+
|
| 380 |
+
chinese_char_pattern = re.compile(r"[\u4e00-\u9fff]+")
|
| 381 |
+
|
| 382 |
+
def _contains_chinese(text: str) -> bool:
|
| 383 |
+
return bool(chinese_char_pattern.search(text))
|
| 384 |
+
|
| 385 |
+
def _normalize(text: str) -> str:
|
| 386 |
+
if not text:
|
| 387 |
+
return text
|
| 388 |
+
is_zh = _contains_chinese(text)
|
| 389 |
+
if is_zh:
|
| 390 |
+
out = zh_tn_model.normalize(text)
|
| 391 |
+
else:
|
| 392 |
+
out = en_tn_model.normalize(text)
|
| 393 |
+
# 对 wetext TN 输出做强力清洗,去掉所有不应保留的特殊符号
|
| 394 |
+
return clean_wetext_output(out)
|
| 395 |
+
|
| 396 |
+
return _normalize
|
| 397 |
+
except Exception as e:
|
| 398 |
+
print(f"[WARN] Failed to build wetext normalizer: {e}", flush=True)
|
| 399 |
+
return None
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
def build_llm_normalizer(
|
| 403 |
+
api_url: Optional[str] = None,
|
| 404 |
+
api_key: Optional[str] = None,
|
| 405 |
+
model: Optional[str] = None,
|
| 406 |
+
**kwargs,
|
| 407 |
+
) -> Optional[Callable[[str], str]]:
|
| 408 |
+
"""根据用户提供的 API 配置构建一个 llm_tn 归一化器(可调用对象)。
|
| 409 |
+
|
| 410 |
+
llm_tn(``llm_tn/text_normalizer.py``)通过 LLM 实现多语种 TN,只要
|
| 411 |
+
templates 齐全即可处理 wetext(仅中/英)不支持的语种(日、韩、俄等)。
|
| 412 |
+
|
| 413 |
+
Args:
|
| 414 |
+
api_url: LLM API 地址。为 ``None`` 时从环境变量 / ``.env`` 读取
|
| 415 |
+
(``LLM_TN_API_URL``,llm_tn 不内置默认地址)。
|
| 416 |
+
api_key: LLM API 密钥。为 ``None`` 时从环境变量 / ``.env`` 读取
|
| 417 |
+
(``LLM_TN_API_KEY``,llm_tn 不内置默认密钥)。
|
| 418 |
+
model: 使用的模型名。为 ``None`` 时从环境变量 / ``.env`` 读取
|
| 419 |
+
(``LLM_TN_MODEL``,llm_tn 不内置默认模型)。
|
| 420 |
+
**kwargs: 透传给 ``llm_tn.TextNormalizer`` 的其他参数。
|
| 421 |
+
|
| 422 |
+
Returns:
|
| 423 |
+
一个以 ``text`` 为输入、返回归一化文本的可调用对象;若初始化失败
|
| 424 |
+
返回 ``None``。
|
| 425 |
+
"""
|
| 426 |
+
try:
|
| 427 |
+
from fireredtts3.utils.llm_tn.text_normalizer import (
|
| 428 |
+
TextNormalizer as LlmTextNormalizer,
|
| 429 |
+
)
|
| 430 |
+
|
| 431 |
+
init_kwargs = dict(kwargs)
|
| 432 |
+
if api_url is not None:
|
| 433 |
+
init_kwargs["api_url"] = api_url
|
| 434 |
+
if api_key is not None:
|
| 435 |
+
init_kwargs["api_key"] = api_key
|
| 436 |
+
if model is not None:
|
| 437 |
+
init_kwargs["model"] = model
|
| 438 |
+
tn = LlmTextNormalizer(**init_kwargs)
|
| 439 |
+
|
| 440 |
+
def _normalize(text: str, locale: Optional[str] = None) -> str:
|
| 441 |
+
"""调用 llm_tn.normalize;当提供了 locale 时覆盖其内部自动检测。"""
|
| 442 |
+
if locale:
|
| 443 |
+
return tn.normalize(text, locale=locale, auto_detect=False)
|
| 444 |
+
return tn.normalize(text)
|
| 445 |
+
|
| 446 |
+
return _normalize
|
| 447 |
+
except Exception as e:
|
| 448 |
+
print(f"[WARN] Failed to build llm_tn normalizer: {e}", flush=True)
|
| 449 |
+
return None
|
| 450 |
+
|
| 451 |
+
|
| 452 |
+
# --------------------------------------------------------------------------- #
|
| 453 |
+
# 统一入口
|
| 454 |
+
# --------------------------------------------------------------------------- #
|
| 455 |
+
class TextNormalizer:
|
| 456 |
+
"""封装 初步清洗 -> 分句 -> 逐句按需 TN 的文本处理管线。
|
| 457 |
+
|
| 458 |
+
处理顺序:
|
| 459 |
+
1. ``clean`` -- 仅做初步清洗(utf-8 / markdown / emoji / 空白规整),
|
| 460 |
+
不做任何 TN。
|
| 461 |
+
2. ``split`` -- 清洗后按语种分句。
|
| 462 |
+
3. 逐句 TN -- 对每个句子做文本归一化:
|
| 463 |
+
|
| 464 |
+
* llm_tn(``llm_normalizer``)优先,可处理全部语种;其内部通过
|
| 465 |
+
``needs_normalization`` 判断,仅当句子确实需要 TN 时才发起 LLM 调用。
|
| 466 |
+
* wetext(``wetext_normalizer``)作为中/英回退;耗时极短,不做
|
| 467 |
+
"是否需要 TN" 的判断,直接归一化。
|
| 468 |
+
"""
|
| 469 |
+
|
| 470 |
+
def __init__(
|
| 471 |
+
self,
|
| 472 |
+
fasttext_detector: Optional[Callable[[str], Optional[str]]] = None,
|
| 473 |
+
llm_normalizer: Optional[Callable[[str], str]] = None,
|
| 474 |
+
wetext_normalizer: Optional[Callable[[str], str]] = None,
|
| 475 |
+
):
|
| 476 |
+
"""初始化文本处理管线。
|
| 477 |
+
|
| 478 |
+
Args:
|
| 479 |
+
fasttext_detector: 语种判定器,接收文本返回 ``llm_tn`` 的 locale。
|
| 480 |
+
llm_normalizer: 多语种文本归一化器(``llm_tn`` 的 ``normalize``)。
|
| 481 |
+
内部自带 ``needs_normalization`` 判断,按需调用 LLM。
|
| 482 |
+
wetext_normalizer: 中/英文本归一化器(``wetext`` 的 ``Normalizer``)。
|
| 483 |
+
当 llm_normalizer 不可用时,对中/英文本直接归一化。
|
| 484 |
+
"""
|
| 485 |
+
self.fasttext_detector = fasttext_detector
|
| 486 |
+
self.llm_normalizer = llm_normalizer
|
| 487 |
+
self.wetext_normalizer = wetext_normalizer
|
| 488 |
+
|
| 489 |
+
def clean(self, text: str) -> str:
|
| 490 |
+
"""仅做初步清洗,不做任何 TN。"""
|
| 491 |
+
return clean_text(text)
|
| 492 |
+
|
| 493 |
+
def _tn(self, text: str, lang_tag: Optional[str] = None) -> str:
|
| 494 |
+
"""对单个句子做 TN。
|
| 495 |
+
|
| 496 |
+
TN 回退链:llm_tn(全部语种) → wetext(仅中/英)→ 原文。
|
| 497 |
+
|
| 498 |
+
当提供了 ``lang_tag``(如 ``"Russian"``),wetext 分支只会对中/英语种
|
| 499 |
+
调用 wetext;非中/英时直接返回原文(只做空格清理),从而避免无
|
| 500 |
+
llm_tn 时俄��、日语等被错误地送进英文 TN。
|
| 501 |
+
|
| 502 |
+
TN 之后统一清理汉字之间、汉字与英文/数字之间的多余空格。
|
| 503 |
+
"""
|
| 504 |
+
if self.llm_normalizer is not None:
|
| 505 |
+
locale = lang_tag_to_locale(lang_tag) if lang_tag else None
|
| 506 |
+
try:
|
| 507 |
+
return clean_tn_spaces(self.llm_normalizer(text, locale=locale))
|
| 508 |
+
except Exception as e:
|
| 509 |
+
print(f"[WARN] llm_tn normalization failed, fallback to raw text: {e}", flush=True)
|
| 510 |
+
return clean_tn_spaces(text)
|
| 511 |
+
# 没有 llm_tn 时,只有中/英/方言/粤语才走 wetext
|
| 512 |
+
if self.wetext_normalizer is not None:
|
| 513 |
+
if lang_tag is None:
|
| 514 |
+
# 未提供语种 -> 按文本内容自动判定
|
| 515 |
+
lang_tag = detect_language(text, self.fasttext_detector)
|
| 516 |
+
_can_wetext = (
|
| 517 |
+
lang_tag in ("Chinese", "English")
|
| 518 |
+
or lang_tag == "Cantonese"
|
| 519 |
+
or lang_tag.startswith("ZH_")
|
| 520 |
+
)
|
| 521 |
+
if _can_wetext:
|
| 522 |
+
try:
|
| 523 |
+
return clean_tn_spaces(self.wetext_normalizer(text))
|
| 524 |
+
except Exception as e:
|
| 525 |
+
print(f"[WARN] wetext normalization failed, fallback to raw text: {e}", flush=True)
|
| 526 |
+
return clean_tn_spaces(text)
|
| 527 |
+
return clean_tn_spaces(text)
|
| 528 |
+
|
| 529 |
+
def split(
|
| 530 |
+
self,
|
| 531 |
+
text: str,
|
| 532 |
+
lang: str = "zh",
|
| 533 |
+
tokenize: Optional[Callable[[str], List[str]]] = None,
|
| 534 |
+
do_tn: bool = True,
|
| 535 |
+
lang_tag: Optional[str] = None,
|
| 536 |
+
**kwargs,
|
| 537 |
+
) -> List[str]:
|
| 538 |
+
"""清洗后分句,并对每个句子逐句做 TN(默认开启)。
|
| 539 |
+
|
| 540 |
+
Args:
|
| 541 |
+
text: 输入文本。
|
| 542 |
+
lang: 分句所用的语种(zh 按字符,其他按 token)。
|
| 543 |
+
tokenize: 分句长度计算用的 tokenizer(非 zh 时使用)。
|
| 544 |
+
do_tn: 是否对每个句子执行 TN。为 ``False`` 时仅分句不做归一化。
|
| 545 |
+
lang_tag: 若已知语种 tag(如 ``"Russian"``),传给 llm_tn 覆盖自动检测。
|
| 546 |
+
**kwargs: 透传给 ``split_paragraph`` 的其他参数。
|
| 547 |
+
"""
|
| 548 |
+
text = self.clean(text)
|
| 549 |
+
if not text:
|
| 550 |
+
return []
|
| 551 |
+
utts = split_paragraph(text, tokenize=tokenize, lang=lang, **kwargs)
|
| 552 |
+
if not do_tn:
|
| 553 |
+
return utts
|
| 554 |
+
return [self._tn(u, lang_tag=lang_tag) for u in utts]
|
| 555 |
+
|
| 556 |
+
def detect_lang(self, text: str) -> str:
|
| 557 |
+
"""判定文本语种(返回 lang tag)。"""
|
| 558 |
+
return detect_language(text, self.fasttext_detector)
|
| 559 |
+
|
| 560 |
+
|
| 561 |
+
if __name__ == "__main__":
|
| 562 |
+
wetext_normalizer = build_wetext_normalizer()
|
| 563 |
+
llm_normalizer = None # build_llm_normalizer()
|
| 564 |
+
tn = TextNormalizer(
|
| 565 |
+
wetext_normalizer=wetext_normalizer,
|
| 566 |
+
llm_normalizer=llm_normalizer,
|
| 567 |
+
)
|
| 568 |
+
samples = [
|
| 569 |
+
"**你好,世界!** 今天天气很好。我们一起去公园散步吧。",
|
| 570 |
+
"バスケがしたいいです",
|
| 571 |
+
# ---- Qwen-Audio-3.0-TTS Long-text Generation 样例 ----
|
| 572 |
+
"As she passed through the crowd of squires and yeomen who already filled the lower end of the vast apartment a scrap of paper was thrust into her hand which she received almost unconsciously and continued to hold without examining its contents. The assurance that she possessed some friend in this awful assembly gave her courage to look around and to mark into whose presence she had been conducted. She gazed accordingly upon a scene which might well have struck terror into a bolder heart than hers. On an elevated seat at the upper end of the great hall directly before the accused sat the grand master of the temple in full and ample robes of flowing white holding in his hand the mystic staff which bore the symbol of the order. At his feet was placed a table occupied by two scribes whose duty it was to record the proceedings of the day. Their chairs were black and formed a marked contrast to the warlike appearance of the knights who attended the solemn gathering. The preceptors of whom there were four present occupied seats behind their superiors and behind them stood the esquires of the order robed in white",
|
| 573 |
+
"天色渐渐暗下来的时候,我把最后一件旧毛衣叠好,轻轻放进那只褪色的帆布包里。门锁轻轻一扣,楼道的灯光跟着亮了又灭,像极了这些年里头起起落落的日常。我站在台阶上深吸了口气,冷空气顺着鼻腔往里钻,带着点泥土与枯叶的腥气。其实心里头早就有了准备,只是真到了要转身的那一刻,脚步还是忍不住慢了半拍。风掠过树梢发出沙沙的响动,像是在劝我别回头。我知道这一走,有些人就再也碰不着了,可生活原本就是这样,聚散从来不讲道理,只能顺着岁月的河水流向该去的地方。 往前走的时候,脑海里不断闪过些零碎画面,那间住了好多年的小屋,窗台上总是晒着半干的衣裳,厨房的排烟罩一到做饭时分就会嗡���作响。那时候总觉得日子长得很,怎么熬也熬不到头,如今真要离开了,反倒觉得光阴像手里的细沙,攥得越紧漏得越快。我伸手摸了摸口袋里那张没带走的旧车票,纸边已经起了毛。其实送行的人并不多,大家都默契地没有多问什么,只说了句保重。我也学着他们的样子笑了笑,把那些没说出口的牵挂都咽进了肚子里。人嘛,总要学会一个人把情绪消化掉,等到夜深人静的时候再慢慢拿出来晾干。 拐过街角,远处的路灯渐渐亮起来,映在潮湿的地面上泛起一层柔和的光晕。",
|
| 574 |
+
# ---- Qwen-Audio-3.0-TTS Text Normalization 样例 ----
|
| 575 |
+
# EN
|
| 576 |
+
"Sales promo: Buy 3 items, get Chapter XII free!",
|
| 577 |
+
"The shadow puppet show begins promptly at 19:30 and ends at 21:15.",
|
| 578 |
+
"Ventilation rate must stay between 0.85 and 1.25 air changes per hour.",
|
| 579 |
+
"Grant awarded: €1.2M for cryo-EM infrastructure",
|
| 580 |
+
"The grant covers $12K for lab materials and $3.5M for equipment.",
|
| 581 |
+
"Stir gently until pH = 7.0 ± 0.2, then cap.",
|
| 582 |
+
# ZH
|
| 583 |
+
"今日体测数据:静息心率62.4次/分,体脂率18.7%,深蹲最大负重95.3公斤,跑步机耐力测试成绩12.8分钟。",
|
| 584 |
+
"置信区间估计中,μ的95%CI为12.3~15.7,σ²的90%CI是8.1—11.4,而p值范围设为0.01→0.05。",
|
| 585 |
+
"本次支付成功率对比:微信 97:3,支付宝 96:4,云闪付 95:5,三者比例差异需重点关注。",
|
| 586 |
+
"固件升级包已发布,兼容版本号v1.8.3与v2.1.0,需确认设备当前运行build 7892。",
|
| 587 |
+
"特种设备检验员需持TSG Z8002证书上岗,现场须佩戴N95口罩及PPE防护装备。",
|
| 588 |
+
"本次实验中,样品A的纯度为99.95%,样品B含杂质1.2‰,两者差值达98.75%。",
|
| 589 |
+
]
|
| 590 |
+
for s in samples:
|
| 591 |
+
print("=" * 60)
|
| 592 |
+
print("INPUT :", s)
|
| 593 |
+
print("LANG :", tn.detect_lang(s))
|
| 594 |
+
cleaned = tn.clean(s)
|
| 595 |
+
print("CLEAN :", cleaned)
|
| 596 |
+
# 分句(含逐句 TN)
|
| 597 |
+
parts_tn = tn.split(s, do_tn=True)
|
| 598 |
+
print("SPLIT+TN:")
|
| 599 |
+
for i, u in enumerate(parts_tn):
|
| 600 |
+
print(f" [{i}] {u}")
|
fireredtts3/utils/text_tokenizer.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict
|
| 2 |
+
from transformers import AutoTokenizer
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
MULTI_LANG_TAGS = [
|
| 6 |
+
"<|Chinese|>", "<|English|>", "<|Cantonese|>",
|
| 7 |
+
"<|Japanese|>", "<|Korean|>", "<|Spanish|>",
|
| 8 |
+
"<|French|>", "<|Russian|>", "<|Arabic|>",
|
| 9 |
+
"<|Turkish|>", "<|Indonesian|>", "<|Portuguese|>",
|
| 10 |
+
"<|Italian|>", "<|Dutch|>", "<|Vietnamese|>",
|
| 11 |
+
"<|German|>", "<|Ukrainian|>", "<|Thai|>",
|
| 12 |
+
"<|Polish|>", "<|Romanian|>", "<|Greek|>",
|
| 13 |
+
"<|Czech|>", "<|Finnish|>", "<|Hindi|>",
|
| 14 |
+
]
|
| 15 |
+
|
| 16 |
+
MULTI_DIALECT_TAGS = [
|
| 17 |
+
"<|ZH_Anhui|>", "<|ZH_Fujian|>", "<|ZH_Gansu|>",
|
| 18 |
+
"<|ZH_Guizhou|>", "<|ZH_Hebei|>", "<|ZH_Henan|>",
|
| 19 |
+
"<|ZH_Hubei|>", "<|ZH_Hunan|>", "<|ZH_Jiangxi|>",
|
| 20 |
+
"<|ZH_Liaoning|>", "<|ZH_Minnan|>", "<|ZH_Ningxia|>",
|
| 21 |
+
"<|ZH_Shaanxi|>", "<|ZH_Shandong|>", "<|ZH_Shanghai|>",
|
| 22 |
+
"<|ZH_Shanxi|>", "<|ZH_Sichuan|>", "<|ZH_Tianjin|>",
|
| 23 |
+
"<|ZH_Wenzhou|>", "<|ZH_Wu|>", "<|ZH_Yunnan|>",
|
| 24 |
+
]
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def load_text_tokenizer(tokenizer_dir:str):
|
| 28 |
+
text_tokenizer = AutoTokenizer.from_pretrained(
|
| 29 |
+
pretrained_model_name_or_path=tokenizer_dir
|
| 30 |
+
)
|
| 31 |
+
vocab: Dict = text_tokenizer.get_vocab()
|
| 32 |
+
# Add special tokens
|
| 33 |
+
special_tokens = [
|
| 34 |
+
"<|sosp|>",
|
| 35 |
+
"<|eosp|>",
|
| 36 |
+
"<|empty|>",
|
| 37 |
+
"<|Human|>",
|
| 38 |
+
"<|SpeechLM|>",
|
| 39 |
+
"<|sostm|>",
|
| 40 |
+
"<|eostm|>",
|
| 41 |
+
"<|sot|>",
|
| 42 |
+
"<|eot|>",
|
| 43 |
+
"<|TEXT_ONLY|>",
|
| 44 |
+
"<|AUDIO_ONLY|>",
|
| 45 |
+
"<|ASR|>",
|
| 46 |
+
"<|TTS|>",
|
| 47 |
+
"<|INTERLEAVE|>",
|
| 48 |
+
"<|UNDERSTANDING|>",
|
| 49 |
+
# Placeholders
|
| 50 |
+
"<|placeholder_001|>",
|
| 51 |
+
"<|placeholder_002|>",
|
| 52 |
+
"<|placeholder_003|>",
|
| 53 |
+
"<|placeholder_004|>",
|
| 54 |
+
"<|placeholder_005|>",
|
| 55 |
+
"<|placeholder_006|>",
|
| 56 |
+
"<|placeholder_007|>",
|
| 57 |
+
"<|placeholder_008|>",
|
| 58 |
+
"<|placeholder_009|>",
|
| 59 |
+
"<|placeholder_010|>",
|
| 60 |
+
"<|placeholder_011|>",
|
| 61 |
+
"<|placeholder_012|>",
|
| 62 |
+
"<|placeholder_013|>",
|
| 63 |
+
"<|placeholder_014|>",
|
| 64 |
+
"<|placeholder_015|>",
|
| 65 |
+
"<|placeholder_016|>",
|
| 66 |
+
"<|placeholder_017|>",
|
| 67 |
+
"<|placeholder_018|>",
|
| 68 |
+
"<|placeholder_019|>",
|
| 69 |
+
"<|placeholder_020|>",
|
| 70 |
+
"<|placeholder_021|>",
|
| 71 |
+
"<|placeholder_022|>",
|
| 72 |
+
"<|placeholder_023|>",
|
| 73 |
+
"<|placeholder_024|>",
|
| 74 |
+
"<|placeholder_025|>",
|
| 75 |
+
"<|placeholder_026|>",
|
| 76 |
+
"<|placeholder_027|>",
|
| 77 |
+
"<|placeholder_028|>",
|
| 78 |
+
"<|placeholder_029|>",
|
| 79 |
+
"<|placeholder_030|>",
|
| 80 |
+
"<|placeholder_031|>",
|
| 81 |
+
"<|placeholder_032|>",
|
| 82 |
+
"<|placeholder_033|>",
|
| 83 |
+
"<|placeholder_034|>",
|
| 84 |
+
"<|placeholder_035|>",
|
| 85 |
+
"<|placeholder_036|>",
|
| 86 |
+
"<|placeholder_037|>",
|
| 87 |
+
"<|placeholder_038|>",
|
| 88 |
+
"<|placeholder_039|>",
|
| 89 |
+
"<|placeholder_040|>",
|
| 90 |
+
"<|placeholder_041|>",
|
| 91 |
+
"<|placeholder_042|>",
|
| 92 |
+
"<|placeholder_043|>",
|
| 93 |
+
"<|placeholder_044|>",
|
| 94 |
+
"<|placeholder_045|>",
|
| 95 |
+
"<|placeholder_046|>",
|
| 96 |
+
"<|placeholder_047|>",
|
| 97 |
+
"<|placeholder_048|>",
|
| 98 |
+
"<|placeholder_049|>",
|
| 99 |
+
"<|placeholder_050|>",
|
| 100 |
+
"<|placeholder_051|>",
|
| 101 |
+
"<|placeholder_052|>",
|
| 102 |
+
"<|placeholder_053|>",
|
| 103 |
+
"<|placeholder_054|>",
|
| 104 |
+
"<|placeholder_055|>",
|
| 105 |
+
"<|placeholder_056|>",
|
| 106 |
+
"<|placeholder_057|>",
|
| 107 |
+
"<|placeholder_058|>",
|
| 108 |
+
"<|placeholder_059|>",
|
| 109 |
+
"<|placeholder_060|>",
|
| 110 |
+
"<|placeholder_061|>",
|
| 111 |
+
"<|placeholder_062|>",
|
| 112 |
+
"<|placeholder_063|>",
|
| 113 |
+
"<|placeholder_064|>",
|
| 114 |
+
"<|placeholder_065|>",
|
| 115 |
+
"<|placeholder_066|>",
|
| 116 |
+
"<|placeholder_067|>",
|
| 117 |
+
"<|placeholder_068|>",
|
| 118 |
+
"<|placeholder_069|>",
|
| 119 |
+
"<|placeholder_070|>",
|
| 120 |
+
"<|placeholder_071|>",
|
| 121 |
+
"<|placeholder_072|>",
|
| 122 |
+
"<|placeholder_073|>",
|
| 123 |
+
"<|placeholder_074|>",
|
| 124 |
+
"<|placeholder_075|>",
|
| 125 |
+
"<|placeholder_076|>",
|
| 126 |
+
"<|placeholder_077|>",
|
| 127 |
+
"<|placeholder_078|>",
|
| 128 |
+
"<|placeholder_079|>",
|
| 129 |
+
"<|placeholder_080|>",
|
| 130 |
+
"<|placeholder_081|>",
|
| 131 |
+
"<|placeholder_082|>",
|
| 132 |
+
"<|placeholder_083|>",
|
| 133 |
+
"<|placeholder_084|>",
|
| 134 |
+
"<|placeholder_085|>",
|
| 135 |
+
"<|placeholder_086|>",
|
| 136 |
+
"<|placeholder_087|>",
|
| 137 |
+
"<|placeholder_088|>",
|
| 138 |
+
"<|placeholder_089|>",
|
| 139 |
+
"<|placeholder_090|>",
|
| 140 |
+
"<|placeholder_091|>",
|
| 141 |
+
"<|placeholder_092|>",
|
| 142 |
+
"<|placeholder_093|>",
|
| 143 |
+
"<|placeholder_094|>",
|
| 144 |
+
"<|placeholder_095|>",
|
| 145 |
+
"<|placeholder_096|>",
|
| 146 |
+
"<|placeholder_097|>",
|
| 147 |
+
"<|placeholder_098|>",
|
| 148 |
+
"<|placeholder_099|>",
|
| 149 |
+
"<|placeholder_100|>",
|
| 150 |
+
"<|placeholder_101|>",
|
| 151 |
+
"<|placeholder_102|>",
|
| 152 |
+
"<|placeholder_103|>",
|
| 153 |
+
"<|placeholder_104|>",
|
| 154 |
+
"<|placeholder_105|>",
|
| 155 |
+
"<|placeholder_106|>",
|
| 156 |
+
"<|placeholder_107|>",
|
| 157 |
+
"<|placeholder_108|>",
|
| 158 |
+
"<|placeholder_109|>",
|
| 159 |
+
"<|placeholder_110|>",
|
| 160 |
+
"<|placeholder_111|>",
|
| 161 |
+
"<|placeholder_112|>",
|
| 162 |
+
"<|placeholder_113|>",
|
| 163 |
+
"<|placeholder_114|>",
|
| 164 |
+
"<|placeholder_115|>",
|
| 165 |
+
"<|placeholder_116|>",
|
| 166 |
+
"<|placeholder_117|>",
|
| 167 |
+
"<|placeholder_118|>",
|
| 168 |
+
"<|placeholder_119|>",
|
| 169 |
+
"<|placeholder_120|>",
|
| 170 |
+
"<|placeholder_121|>",
|
| 171 |
+
"<|placeholder_122|>",
|
| 172 |
+
"<|placeholder_123|>",
|
| 173 |
+
"<|placeholder_124|>",
|
| 174 |
+
"<|placeholder_125|>",
|
| 175 |
+
"<|placeholder_126|>",
|
| 176 |
+
"<|placeholder_127|>",
|
| 177 |
+
"<|placeholder_128|>",
|
| 178 |
+
"<|placeholder_129|>",
|
| 179 |
+
"<|placeholder_130|>",
|
| 180 |
+
"<|placeholder_131|>",
|
| 181 |
+
"<|placeholder_132|>",
|
| 182 |
+
"<|placeholder_133|>",
|
| 183 |
+
"<|placeholder_134|>",
|
| 184 |
+
"<|placeholder_135|>",
|
| 185 |
+
"<|placeholder_136|>",
|
| 186 |
+
"<|placeholder_137|>",
|
| 187 |
+
"<|placeholder_138|>",
|
| 188 |
+
"<|placeholder_139|>",
|
| 189 |
+
"<|placeholder_140|>",
|
| 190 |
+
"<|placeholder_141|>",
|
| 191 |
+
"<|placeholder_142|>",
|
| 192 |
+
"<|placeholder_143|>",
|
| 193 |
+
"<|placeholder_144|>",
|
| 194 |
+
"<|placeholder_145|>",
|
| 195 |
+
"<|placeholder_146|>",
|
| 196 |
+
"<|placeholder_147|>",
|
| 197 |
+
"<|placeholder_148|>",
|
| 198 |
+
"<|placeholder_149|>",
|
| 199 |
+
"<|placeholder_150|>",
|
| 200 |
+
"<|placeholder_151|>",
|
| 201 |
+
"<|placeholder_152|>",
|
| 202 |
+
"<|placeholder_153|>",
|
| 203 |
+
"<|placeholder_154|>",
|
| 204 |
+
"<|placeholder_155|>",
|
| 205 |
+
"<|placeholder_156|>",
|
| 206 |
+
"<|placeholder_157|>",
|
| 207 |
+
"<|placeholder_158|>",
|
| 208 |
+
"<|placeholder_159|>",
|
| 209 |
+
"<|placeholder_160|>",
|
| 210 |
+
"<|placeholder_161|>",
|
| 211 |
+
"<|placeholder_162|>",
|
| 212 |
+
"<|placeholder_163|>",
|
| 213 |
+
"<|placeholder_164|>",
|
| 214 |
+
"<|placeholder_165|>",
|
| 215 |
+
"<|placeholder_166|>",
|
| 216 |
+
"<|placeholder_167|>",
|
| 217 |
+
"<|placeholder_168|>",
|
| 218 |
+
"<|placeholder_169|>",
|
| 219 |
+
"<|placeholder_170|>",
|
| 220 |
+
"<|placeholder_171|>",
|
| 221 |
+
"<|placeholder_172|>",
|
| 222 |
+
"<|placeholder_173|>",
|
| 223 |
+
"<|placeholder_174|>",
|
| 224 |
+
"<|placeholder_175|>",
|
| 225 |
+
"<|placeholder_176|>",
|
| 226 |
+
"<|placeholder_177|>",
|
| 227 |
+
"<|placeholder_178|>",
|
| 228 |
+
"<|placeholder_179|>",
|
| 229 |
+
"<|placeholder_180|>",
|
| 230 |
+
"<|placeholder_181|>",
|
| 231 |
+
"<|placeholder_182|>",
|
| 232 |
+
"<|placeholder_183|>",
|
| 233 |
+
"<|placeholder_184|>",
|
| 234 |
+
"<|placeholder_185|>",
|
| 235 |
+
"<|placeholder_186|>",
|
| 236 |
+
"<|placeholder_187|>",
|
| 237 |
+
"<|placeholder_188|>",
|
| 238 |
+
"<|placeholder_189|>",
|
| 239 |
+
"<|placeholder_190|>",
|
| 240 |
+
"<|placeholder_191|>",
|
| 241 |
+
"<|placeholder_192|>",
|
| 242 |
+
# Multi-lang tag
|
| 243 |
+
*MULTI_LANG_TAGS,
|
| 244 |
+
# Multi-dialect tag
|
| 245 |
+
*MULTI_DIALECT_TAGS,
|
| 246 |
+
# For SpeechEdit CoT
|
| 247 |
+
"<|edit|>", "<|frame_patch|>", "<|end_edit|>",
|
| 248 |
+
]
|
| 249 |
+
for token in special_tokens:
|
| 250 |
+
if token not in vocab:
|
| 251 |
+
text_tokenizer.add_tokens([token], special_tokens=True)
|
| 252 |
+
return text_tokenizer
|
fireredtts3/utils/utils.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def fix_seed(seed: int):
|
| 6 |
+
random.seed(seed)
|
| 7 |
+
torch.manual_seed(seed)
|
| 8 |
+
torch.cuda.manual_seed(seed)
|
| 9 |
+
torch.cuda.manual_seed_all(seed)
|
requirements.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch==2.8.0
|
| 2 |
+
torchaudio==2.8.0
|
| 3 |
+
https://huggingface.co/datasets/multimodalart/zerogpu-blackwell-wheels/resolve/main/wheels/pt28-cu128-cp312/flash_attn-2.8.3-cp312-cp312-linux_x86_64.whl
|
| 4 |
+
transformers==5.6.2
|
| 5 |
+
einops==0.8.2
|
| 6 |
+
python-dotenv
|
| 7 |
+
regex
|
| 8 |
+
wetext
|
| 9 |
+
fasttext
|
| 10 |
+
numpy
|
| 11 |
+
soundfile
|