multimodalart HF Staff commited on
Commit
0d8b898
·
verified ·
1 Parent(s): 5801505

FireRedTTS3 demo: cloning, voice design, speech editing

Browse files
Files changed (47) hide show
  1. .gitattributes +2 -0
  2. LICENSE +201 -0
  3. README.md +35 -5
  4. app.py +576 -0
  5. examples/en_prompt.wav +3 -0
  6. examples/zh_prompt.wav +3 -0
  7. fireredtts3/campp/DTDNN.py +123 -0
  8. fireredtts3/campp/campp.py +58 -0
  9. fireredtts3/campp/layers.py +253 -0
  10. fireredtts3/core.py +555 -0
  11. fireredtts3/llm/dit.py +194 -0
  12. fireredtts3/llm/fireredtts3_base.py +346 -0
  13. fireredtts3/llm/fireredtts3_instruct.py +508 -0
  14. fireredtts3/llm/modules.py +185 -0
  15. fireredtts3/llm/patch_encoder.py +65 -0
  16. fireredtts3/llm/rotary_embedding.py +75 -0
  17. fireredtts3/redae/redae.py +496 -0
  18. fireredtts3/utils/chatml.py +118 -0
  19. fireredtts3/utils/llm_tn/templates/ar-SA.json +13 -0
  20. fireredtts3/utils/llm_tn/templates/cs-CZ.json +13 -0
  21. fireredtts3/utils/llm_tn/templates/de-DE.json +21 -0
  22. fireredtts3/utils/llm_tn/templates/el-GR.json +13 -0
  23. fireredtts3/utils/llm_tn/templates/en-US.json +20 -0
  24. fireredtts3/utils/llm_tn/templates/es-MX.json +23 -0
  25. fireredtts3/utils/llm_tn/templates/fi-FI.json +13 -0
  26. fireredtts3/utils/llm_tn/templates/fr-FR.json +23 -0
  27. fireredtts3/utils/llm_tn/templates/hi-IN.json +13 -0
  28. fireredtts3/utils/llm_tn/templates/id-ID.json +13 -0
  29. fireredtts3/utils/llm_tn/templates/it-IT.json +23 -0
  30. fireredtts3/utils/llm_tn/templates/ja-JP.json +25 -0
  31. fireredtts3/utils/llm_tn/templates/ko-KR.json +13 -0
  32. fireredtts3/utils/llm_tn/templates/lt-LT.json +29 -0
  33. fireredtts3/utils/llm_tn/templates/nl-NL.json +13 -0
  34. fireredtts3/utils/llm_tn/templates/pl-PL.json +13 -0
  35. fireredtts3/utils/llm_tn/templates/pt-BR.json +13 -0
  36. fireredtts3/utils/llm_tn/templates/ro-RO.json +13 -0
  37. fireredtts3/utils/llm_tn/templates/ru-RU.json +13 -0
  38. fireredtts3/utils/llm_tn/templates/th-TH.json +13 -0
  39. fireredtts3/utils/llm_tn/templates/tr-TR.json +13 -0
  40. fireredtts3/utils/llm_tn/templates/uk-UA.json +13 -0
  41. fireredtts3/utils/llm_tn/templates/vi-VN.json +13 -0
  42. fireredtts3/utils/llm_tn/templates/zh-CN.json +25 -0
  43. fireredtts3/utils/llm_tn/text_normalizer.py +428 -0
  44. fireredtts3/utils/text_normalize.py +600 -0
  45. fireredtts3/utils/text_tokenizer.py +252 -0
  46. fireredtts3/utils/utils.py +9 -0
  47. 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: Firered Tts3
3
  emoji: 🔥
4
- colorFrom: purple
5
- colorTo: yellow
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
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # 去除图片语法 ![alt](url)
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