ConectaPrimoAI commited on
Commit
dce30ce
·
verified ·
1 Parent(s): 64e1ec4

Upload folder using huggingface_hub

Browse files
LICENSE-rapido 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.
__pycache__/armazenamento.cpython-314.pyc CHANGED
Binary files a/__pycache__/armazenamento.cpython-314.pyc and b/__pycache__/armazenamento.cpython-314.pyc differ
 
__pycache__/nucleo.cpython-314.pyc CHANGED
Binary files a/__pycache__/nucleo.cpython-314.pyc and b/__pycache__/nucleo.cpython-314.pyc differ
 
app.py CHANGED
@@ -20,6 +20,8 @@ WORKER_MOCK=1 permite subir a casca inteira num notebook para teste.
20
  """
21
  from __future__ import annotations
22
 
 
 
23
  import gradio as gr
24
 
25
  import nucleo
@@ -82,6 +84,44 @@ def _fazer_endpoint(tipo: str):
82
  return endpoint
83
 
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  def _saude(segredo: str):
86
  recusa = nucleo.autenticar(segredo)
87
  if recusa:
@@ -110,6 +150,12 @@ with gr.Blocks(title="Conecta Primo — GPU") as demo:
110
  _btn.click(_fazer_endpoint(_tipo), inputs=[segredo_in, params_in],
111
  outputs=[saida], api_name=_nome)
112
 
 
 
 
 
 
 
113
  _btn_saude = gr.Button(visible=False)
114
  _btn_saude.click(_saude, inputs=[segredo_in], outputs=[saida],
115
  api_name="saude")
 
20
  """
21
  from __future__ import annotations
22
 
23
+ import os
24
+
25
  import gradio as gr
26
 
27
  import nucleo
 
84
  return endpoint
85
 
86
 
87
+ # A VOZ DO LIVE (13/10/2026): o Rápido TTS (Apache 2.0, pt-BR), na CPU deste
88
+ # Space — FORA do @spaces.GPU, então não gasta quota de GPU nenhuma. Ele mora
89
+ # aqui porque hospedar Gradio em CPU gratuita passou a exigir PRO no Hugging
90
+ # Face, e este Space já existe. O código dele é clonado pelo `publicar.sh`.
91
+ _RAPIDO = {"tts": None, "erro": ""}
92
+
93
+
94
+ def _falar_live(segredo: str, texto: str):
95
+ recusa = nucleo.autenticar(segredo)
96
+ if recusa:
97
+ return {"erro": recusa}
98
+ texto = str(texto or "").strip()[:600]
99
+ if not texto:
100
+ return {"erro": "texto vazio"}
101
+ try:
102
+ if _RAPIDO["tts"] is None:
103
+ import torch
104
+ from rapido import Rapido
105
+ torch.set_num_threads(max(1, os.cpu_count() or 2))
106
+ _RAPIDO["tts"] = Rapido.from_pretrained(device="cpu")
107
+ import base64, io, time as _t, wave
108
+ import numpy as np
109
+ t0 = _t.time()
110
+ r = _RAPIDO["tts"].synthesize(texto)
111
+ audio, taxa = r.output["audio"], r.output["sample_rate"]
112
+ if taxa == 48000:
113
+ audio = audio[: len(audio) // 2 * 2].reshape(-1, 2).mean(axis=1)
114
+ taxa = 24000
115
+ pcm = (np.clip(audio, -1, 1) * 32767).astype(np.int16)
116
+ buf = io.BytesIO()
117
+ with wave.open(buf, "wb") as w:
118
+ w.setnchannels(1); w.setsampwidth(2); w.setframerate(taxa); w.writeframes(pcm.tobytes())
119
+ return {"audio_base64": base64.b64encode(buf.getvalue()).decode(), "mime": "audio/wav",
120
+ "segundos": round(_t.time() - t0, 2)}
121
+ except Exception as e:
122
+ return {"erro": f"{type(e).__name__}: {str(e)[:200]}"}
123
+
124
+
125
  def _saude(segredo: str):
126
  recusa = nucleo.autenticar(segredo)
127
  if recusa:
 
150
  _btn.click(_fazer_endpoint(_tipo), inputs=[segredo_in, params_in],
151
  outputs=[saida], api_name=_nome)
152
 
153
+ # A VOZ TEM FILA PRÓPRIA (`concurrency_id`): na fila das gerações ela
154
+ # esperaria um vídeo de minutos terminar, e o Live ficaria mudo.
155
+ _btn_voz = gr.Button(visible=False)
156
+ _btn_voz.click(_falar_live, inputs=[segredo_in, params_in], outputs=[saida],
157
+ api_name="falar_live", concurrency_limit=2, concurrency_id="voz")
158
+
159
  _btn_saude = gr.Button(visible=False)
160
  _btn_saude.click(_saude, inputs=[segredo_in], outputs=[saida],
161
  api_name="saude")
modelos/__pycache__/__init__.cpython-314.pyc CHANGED
Binary files a/modelos/__pycache__/__init__.cpython-314.pyc and b/modelos/__pycache__/__init__.cpython-314.pyc differ
 
modelos/__pycache__/ace_step.cpython-314.pyc CHANGED
Binary files a/modelos/__pycache__/ace_step.cpython-314.pyc and b/modelos/__pycache__/ace_step.cpython-314.pyc differ
 
modelos/__pycache__/base.cpython-314.pyc CHANGED
Binary files a/modelos/__pycache__/base.cpython-314.pyc and b/modelos/__pycache__/base.cpython-314.pyc differ
 
modelos/__pycache__/escolha.cpython-314.pyc CHANGED
Binary files a/modelos/__pycache__/escolha.cpython-314.pyc and b/modelos/__pycache__/escolha.cpython-314.pyc differ
 
modelos/__pycache__/qwen3_tts.cpython-314.pyc CHANGED
Binary files a/modelos/__pycache__/qwen3_tts.cpython-314.pyc and b/modelos/__pycache__/qwen3_tts.cpython-314.pyc differ
 
modelos/__pycache__/qwen_image.cpython-314.pyc CHANGED
Binary files a/modelos/__pycache__/qwen_image.cpython-314.pyc and b/modelos/__pycache__/qwen_image.cpython-314.pyc differ
 
modelos/__pycache__/wan22.cpython-314.pyc CHANGED
Binary files a/modelos/__pycache__/wan22.cpython-314.pyc and b/modelos/__pycache__/wan22.cpython-314.pyc differ
 
modelos/wan22.py CHANGED
@@ -163,9 +163,11 @@ def gerar(params: Dict, progresso: base.Progresso) -> base.Saida:
163
  # não terminam nisso. 3 s a 20 passos é um vídeo DE VERDADE que termina —
164
  # um de 5 s que aborta não entrega nada.
165
  curto = not base.gpu_dedicada()
166
- segundos = max(2, min(int(params.get("duration") or (3 if curto else 5)),
167
- int(os.getenv("VIDEO_MAX_SEGUNDOS", "3" if curto else "8"))))
168
- fps = 24
 
 
169
  quadros = segundos * fps + 1
170
  passos = int(params.get("steps") or (20 if curto else 30))
171
 
@@ -214,8 +216,8 @@ def config_menor(params: Dict) -> Optional[Dict]:
214
  p["width"], p["height"] = menor
215
  return p
216
  dur = int(p.get("duration") or 5)
217
- if dur > 3:
218
- p["duration"] = 3
219
  return p
220
  return None
221
 
 
163
  # não terminam nisso. 3 s a 20 passos é um vídeo DE VERDADE que termina —
164
  # um de 5 s que aborta não entrega nada.
165
  curto = not base.gpu_dedicada()
166
+ # 5 s a 16 fps são 81 quadros: o mesmo custo de ~3,4 s a 24 fps, e o clipe
167
+ # sai com mais de 3 segundos dentro do mesmo prazo de GPU.
168
+ segundos = max(2, min(int(params.get("duration") or 5),
169
+ int(os.getenv("VIDEO_MAX_SEGUNDOS", "5" if curto else "8"))))
170
+ fps = 16 if curto else 24
171
  quadros = segundos * fps + 1
172
  passos = int(params.get("steps") or (20 if curto else 30))
173
 
 
216
  p["width"], p["height"] = menor
217
  return p
218
  dur = int(p.get("duration") or 5)
219
+ if dur > 4:
220
+ p["duration"] = 4
221
  return p
222
  return None
223
 
rapido/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Rápido: tiny, fast Brazilian Portuguese text-to-speech."""
2
+ from .engine import Config, Rapido, Result
3
+
4
+ __all__ = ["Rapido", "Config", "Result"]
5
+ __version__ = "0.1.0"
rapido/__main__.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """python -m rapido "Olá, tudo bem?" -o ola.wav"""
2
+ import argparse
3
+
4
+ from . import Config, Rapido
5
+
6
+
7
+ def main():
8
+ ap = argparse.ArgumentParser(prog="rapido", description="Brazilian Portuguese text-to-speech")
9
+ ap.add_argument("text", nargs="?", help="text to speak (or use -f)")
10
+ ap.add_argument("-f", "--file", help="read text from a file")
11
+ ap.add_argument("-o", "--out", default="rapido.wav")
12
+ ap.add_argument("--model", default=Config.model)
13
+ ap.add_argument("--device", default="auto")
14
+ ap.add_argument("--steps", type=int, default=8)
15
+ ap.add_argument("--guidance", type=float, default=4.0)
16
+ ap.add_argument("--speed", type=float, default=1.0)
17
+ ap.add_argument("--seed", type=int, default=0)
18
+ a = ap.parse_args()
19
+ text = open(a.file, encoding="utf-8").read() if a.file else a.text
20
+ if not text:
21
+ ap.error("give text or -f FILE")
22
+ tts = Rapido(Config(model=a.model, device=a.device, steps=a.steps, guidance=a.guidance, speed=a.speed, seed=a.seed))
23
+ r = tts.synthesize(text)
24
+ r.save(a.out)
25
+ m = r.metrics
26
+ print(f"{a.out}: {m['audio_s']}s of audio in {m['total_s']}s ({m['x_realtime']}x realtime) on {tts.device}")
27
+
28
+
29
+ if __name__ == "__main__":
30
+ main()
rapido/_dit.py ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Modified from Apache-2.0 licensed source; see LICENSE.
2
+ """Rápido acoustic network: letters in, 64-dim latents at 25 Hz out.
3
+
4
+ Both stages take padded batches with masks and return exactly what each item would get alone, so the
5
+ plain path, the batched path and the recorded CUDA graphs all run these same functions.
6
+ """
7
+ import math
8
+
9
+ import torch
10
+ import torch.nn as nn
11
+ import torch.nn.functional as F
12
+
13
+
14
+ def rope(pos, dim):
15
+ inv = 1.0 / (10000.0 ** (torch.arange(dim // 2, device=pos.device).float() / (dim // 2)))
16
+ ang = pos[..., None].float() * inv
17
+ ang = torch.cat([ang, ang], -1)
18
+ return ang.cos(), ang.sin()
19
+
20
+
21
+ def apply_rope(x, cos, sin):
22
+ a, b = x.chunk(2, -1)
23
+ return x * cos + torch.cat([-b, a], -1) * sin
24
+
25
+
26
+ class SwiGLU(nn.Module):
27
+ def __init__(self, d, mult=4):
28
+ super().__init__()
29
+ hidden = int(d * mult * 2 / 3)
30
+ self.w1 = nn.Linear(d, hidden, bias=False)
31
+ self.w3 = nn.Linear(d, hidden, bias=False)
32
+ self.w2 = nn.Linear(hidden, d, bias=False)
33
+
34
+ def forward(self, x):
35
+ return self.w2(F.silu(self.w1(x)) * self.w3(x))
36
+
37
+
38
+ class Attention(nn.Module):
39
+ def __init__(self, d, heads):
40
+ super().__init__()
41
+ self.h, self.dh = heads, d // heads
42
+ self.qkv = nn.Linear(d, 3 * d, bias=False)
43
+ self.proj = nn.Linear(d, d, bias=False)
44
+
45
+ def forward(self, x, cos, sin, mask):
46
+ B, T, D = x.shape
47
+ q, k, v = (t.view(B, T, self.h, self.dh).transpose(1, 2) for t in self.qkv(x).chunk(3, -1))
48
+ q, k = apply_rope(q, cos, sin), apply_rope(k, cos, sin)
49
+ x = F.scaled_dot_product_attention(q, k, v, attn_mask=mask[:, None, None, :])
50
+ return self.proj(x.transpose(1, 2).reshape(B, T, D))
51
+
52
+
53
+ class ConvNeXtBlock(nn.Module):
54
+ def __init__(self, d):
55
+ super().__init__()
56
+ self.dw = nn.Conv1d(d, d, 7, padding=3, groups=d)
57
+ self.norm = nn.LayerNorm(d)
58
+ self.pw1 = nn.Linear(d, 2 * d)
59
+ self.pw2 = nn.Linear(2 * d, d)
60
+ self.gamma = nn.Parameter(torch.ones(d))
61
+
62
+ def forward(self, x, mask):
63
+ y = self.norm(self.dw((x * mask[..., None]).transpose(1, 2)).transpose(1, 2)) # padding reads as zeros
64
+ return x + self.gamma * self.pw2(F.gelu(self.pw1(y)))
65
+
66
+
67
+ class TextBlock(nn.Module):
68
+ def __init__(self, d, heads):
69
+ super().__init__()
70
+ self.n1, self.n2 = nn.LayerNorm(d), nn.LayerNorm(d)
71
+ self.attn, self.ff = Attention(d, heads), SwiGLU(d)
72
+
73
+ def forward(self, x, cos, sin, mask):
74
+ x = x + self.attn(self.n1(x), cos, sin, mask)
75
+ return x + self.ff(self.n2(x))
76
+
77
+
78
+ class TextEncoder(nn.Module):
79
+ def __init__(self, n_vocab, d, n_conv, n_attn, heads):
80
+ super().__init__()
81
+ self.dh = d // heads
82
+ self.emb = nn.Embedding(n_vocab, d, padding_idx=0)
83
+ self.conv = nn.ModuleList(ConvNeXtBlock(d) for _ in range(n_conv))
84
+ self.attn = nn.ModuleList(TextBlock(d, heads) for _ in range(n_attn))
85
+ self.norm = nn.LayerNorm(d)
86
+
87
+ def forward(self, ids, mask):
88
+ x = self.emb(ids)
89
+ for block in self.conv:
90
+ x = block(x, mask)
91
+ cos, sin = rope(torch.arange(ids.shape[1], device=ids.device), self.dh)
92
+ for block in self.attn:
93
+ x = block(x, cos, sin, mask)
94
+ return self.norm(x)
95
+
96
+
97
+ def masked_group_norm(x, norm, m):
98
+ """GroupNorm(1, C) with statistics over real positions only."""
99
+ count = m.sum((1, 2), keepdim=True) * x.shape[1]
100
+ mean = (x * m).sum((1, 2), keepdim=True) / count
101
+ var = (((x - mean) * m) ** 2).sum((1, 2), keepdim=True) / count
102
+ return (x - mean) / torch.sqrt(var + norm.eps) * norm.weight[None, :, None] + norm.bias[None, :, None]
103
+
104
+
105
+ class Duration(nn.Module):
106
+ def __init__(self, d, hidden):
107
+ super().__init__()
108
+ self.net = nn.Sequential(
109
+ nn.Conv1d(d, hidden, 3, padding=1), nn.SiLU(), nn.GroupNorm(1, hidden), nn.Identity(),
110
+ nn.Conv1d(hidden, hidden, 3, padding=1), nn.SiLU(), nn.GroupNorm(1, hidden))
111
+ self.out = nn.Linear(hidden, 1)
112
+
113
+ def forward(self, h, mask):
114
+ m = mask[:, None].float()
115
+ x = masked_group_norm(F.silu(self.net[0](h.transpose(1, 2) * m)), self.net[2], m) * m
116
+ x = masked_group_norm(F.silu(self.net[4](x)), self.net[6], m)
117
+ log_d = self.out(x.transpose(1, 2)).squeeze(-1).masked_fill(~mask, 0.0)
118
+ return torch.expm1(log_d.clamp(max=6.0)).clamp(min=1e-3) * mask.float()
119
+
120
+
121
+ class Aligner(nn.Module):
122
+ def __init__(self, d, heads, pos_scale, lookback, lookahead, letter_pos):
123
+ super().__init__()
124
+ self.h, self.dh = heads, d // heads
125
+ self.pos_scale, self.lookback, self.lookahead, self.letter_pos = pos_scale, lookback, lookahead, letter_pos
126
+ self.q, self.k, self.v, self.o = (nn.Linear(d, d, bias=False) for _ in range(4))
127
+ self.frame_q = nn.Parameter(torch.zeros(1, 1, d))
128
+ self.log_sigma = nn.Parameter(torch.zeros(heads))
129
+ self.bias_w = nn.Parameter(torch.zeros(heads))
130
+ self.log_temp = nn.Parameter(torch.zeros(()))
131
+
132
+ def forward(self, h, cw, cp, fw, fp, mask, n_words):
133
+ B, L, D = h.shape
134
+ T = fw.shape[1]
135
+ if self.letter_pos:
136
+ c, f = cw.clamp(min=0), fw.clamp(min=0)
137
+ wlen = torch.zeros(B, n_words, device=h.device).scatter_add_(1, c, (cw >= 0).float()).clamp(min=1.0)
138
+ woff = wlen.cumsum(-1) - wlen
139
+ cg = woff.gather(1, c) + cp.float() * wlen.gather(1, c)
140
+ fg = woff.gather(1, f) + fp.float() * wlen.gather(1, f)
141
+ else:
142
+ cg, fg = cw.clamp(min=0).float() + cp, fw.float() + fp
143
+ q = self.q(self.frame_q.expand(B, T, D)).view(B, T, self.h, self.dh).transpose(1, 2)
144
+ k = self.k(h).view(B, L, self.h, self.dh).transpose(1, 2)
145
+ v = self.v(h).view(B, L, self.h, self.dh).transpose(1, 2)
146
+ q = apply_rope(q, *(t[:, None] for t in rope(fg * self.pos_scale, self.dh)))
147
+ k = apply_rope(k, *(t[:, None] for t in rope(cg * self.pos_scale, self.dh)))
148
+ logits = (q @ k.transpose(-2, -1)) / math.sqrt(self.dh) * self.log_temp.exp()
149
+ sig2 = (self.log_sigma.exp() ** 2).view(1, self.h, 1, 1)
150
+ dist2 = ((fg[:, :, None] - cg[:, None, :]) ** 2)[:, None]
151
+ logits = logits - self.bias_w.view(1, self.h, 1, 1) * dist2 / (2 * sig2)
152
+ rel = cw[:, None, :] - fw[:, :, None]
153
+ allow = (rel >= -self.lookback) & (rel <= self.lookahead) & mask[:, None, :] & (cw[:, None, :] >= 0)
154
+ attn = logits.masked_fill(~allow[:, None], -1e4).softmax(-1) * allow[:, None]
155
+ return self.o((attn @ v).transpose(1, 2).reshape(B, T, D))
156
+
157
+
158
+ class DiTBlock(nn.Module):
159
+ def __init__(self, d, heads, ff_mult, shared):
160
+ super().__init__()
161
+ self.n1 = nn.LayerNorm(d, elementwise_affine=False, eps=1e-6)
162
+ self.n2 = nn.LayerNorm(d, elementwise_affine=False, eps=1e-6)
163
+ self.attn, self.ff = Attention(d, heads), SwiGLU(d, ff_mult)
164
+ if shared:
165
+ self.ada_offset = nn.Parameter(torch.zeros(6, d))
166
+ else:
167
+ self.ada = nn.Sequential(nn.SiLU(), nn.Linear(d, 6 * d))
168
+
169
+ def forward(self, x, c, cos, sin, mask):
170
+ p = (c + self.ada_offset[None]).unbind(1) if hasattr(self, "ada_offset") else self.ada(c).chunk(6, -1)
171
+ sa, ga, aa, sf, gf, af = (t.unsqueeze(1) for t in p)
172
+ x = x + aa * self.attn(self.n1(x) * (1 + ga) + sa, cos, sin, mask)
173
+ return x + af * self.ff(self.n2(x) * (1 + gf) + sf)
174
+
175
+
176
+ class TimestepEmbed(nn.Module):
177
+ def __init__(self, d, freq=256):
178
+ super().__init__()
179
+ self.freq = freq
180
+ self.mlp = nn.Sequential(nn.Linear(freq, d), nn.SiLU(), nn.Linear(d, d))
181
+
182
+ def forward(self, t):
183
+ half = self.freq // 2
184
+ a = t[:, None] * 1000.0 * torch.exp(-math.log(10000) * torch.arange(half, device=t.device) / half)[None]
185
+ return self.mlp(torch.cat([a.cos(), a.sin()], -1))
186
+
187
+
188
+ class Acoustic(nn.Module):
189
+ def __init__(self, cfg, vocab, shared):
190
+ super().__init__()
191
+ d, heads = cfg["d"], cfg["n_heads"]
192
+ self.d, self.dh, self.latent_dim, self.shared = d, d // heads, cfg.get("latent_dim", 64), shared
193
+ self.vocab = list(vocab)
194
+ self.stoi = {ch: i for i, ch in enumerate(self.vocab)}
195
+ self.times = [float(t) for t in cfg["distilled"]["times"]]
196
+ self.text = TextEncoder(len(vocab), d, cfg.get("text_conv", 4), cfg.get("text_attn", 2), heads)
197
+ self.chardur = Duration(d, cfg.get("dur_hidden", 256))
198
+ self.aligner = Aligner(d, cfg.get("align_heads", 4), cfg.get("pos_scale", 24.0), cfg.get("lookback", 1),
199
+ cfg.get("lookahead", 1), bool(cfg.get("letter_pos", False)))
200
+ self.in_proj = nn.Linear(self.latent_dim, d)
201
+ self.t_embed = TimestepEmbed(d)
202
+ if shared:
203
+ self.ada_shared = nn.Sequential(nn.SiLU(), nn.Linear(d, 6 * d))
204
+ self.blocks = nn.ModuleList(DiTBlock(d, heads, cfg.get("ff_mult", 4), shared) for _ in range(cfg["n_layers"]))
205
+ self.norm_out = nn.LayerNorm(d, elementwise_affine=False, eps=1e-6)
206
+ self.ada_out = nn.Sequential(nn.SiLU(), nn.Linear(d, 2 * d))
207
+ self.out_proj = nn.Linear(d, self.latent_dim)
208
+
209
+ def backbone(self, x, cond, t, mask, cos, sin):
210
+ c = self.t_embed(t)
211
+ bc = self.ada_shared(c).view(-1, 6, self.d) if self.shared else c
212
+ x = self.in_proj(x) + cond
213
+ for block in self.blocks:
214
+ x = block(x, bc, cos, sin, mask)
215
+ s, g = self.ada_out(c).chunk(2, -1)
216
+ return self.out_proj(self.norm_out(x) * (1 + g.unsqueeze(1)) + s.unsqueeze(1))
217
+
218
+ def text_stage(self, ids, mask):
219
+ """Letters to text features and per-letter durations in frames."""
220
+ h = self.text(ids, mask)
221
+ return h, self.chardur(h, mask)
222
+
223
+ def sound_stage(self, h, dur, mask, cw, wstart, fw, fp, fmask, noise):
224
+ """Text features and a frame timeline to latents, in len(self.times) steps from the given noise."""
225
+ c = dur.clamp(min=1e-4) * mask
226
+ done = c.cumsum(-1)
227
+ before = done - c
228
+ word = cw.clamp(min=0)
229
+ total = torch.zeros_like(c).scatter_add_(1, word, c).gather(1, word)
230
+ cp = ((done - before.gather(1, wstart) - 0.5 * c) / total.clamp(min=1e-8)).clamp(0.0, 1.0) * mask
231
+ cond = self.aligner(h, cw, cp, fw, fp, mask, cw.shape[1])
232
+ cos, sin = rope(torch.arange(fw.shape[1], device=h.device), self.dh)
233
+ x = noise[:, 0]
234
+ for k, t in enumerate(self.times):
235
+ step = torch.full((x.shape[0],), t, device=x.device)
236
+ x1 = x + (1 - t) * self.backbone(x, cond, step, fmask, cos, sin)
237
+ if k + 1 < len(self.times):
238
+ x = (1 - self.times[k + 1]) * noise[:, k + 1] + self.times[k + 1] * x1
239
+ return x1
240
+
241
+
242
+ def load_acoustic(path, device):
243
+ ck = torch.load(path, map_location="cpu", weights_only=False)
244
+ sd = ck["ema"]
245
+ shared = bool(ck["cfg"].get("shared_ada", any(k.startswith("ada_shared") for k in sd)))
246
+ model = Acoustic(ck["cfg"], ck["vocab"], shared)
247
+ model.load_state_dict(sd)
248
+ return model.to(device).float().eval().requires_grad_(False)
rapido/_vae.py ADDED
@@ -0,0 +1,581 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # From VoxCPM (https://github.com/OpenBMB/VoxCPM), Apache License 2.0. AudioVAE v2.
2
+ import math
3
+ from typing import List, Optional
4
+
5
+ import numpy as np
6
+ import torch
7
+ from torch import nn
8
+ import torch.nn.functional as F
9
+ from torch.nn.utils import weight_norm
10
+ from pydantic import BaseModel
11
+
12
+
13
+ def WNConv1d(*args, **kwargs):
14
+ return weight_norm(nn.Conv1d(*args, **kwargs))
15
+
16
+
17
+ def WNConvTranspose1d(*args, **kwargs):
18
+ return weight_norm(nn.ConvTranspose1d(*args, **kwargs))
19
+
20
+
21
+ class CausalConv1d(nn.Conv1d):
22
+ def __init__(self, *args, padding: int = 0, output_padding: int = 0, **kwargs):
23
+ super().__init__(*args, **kwargs)
24
+ self.__padding = padding
25
+ self.__output_padding = output_padding
26
+
27
+ def forward(self, x):
28
+ x_pad = F.pad(x, (self.__padding * 2 - self.__output_padding, 0))
29
+ return super().forward(x_pad)
30
+
31
+
32
+ class CausalTransposeConv1d(nn.ConvTranspose1d):
33
+ def __init__(self, *args, padding: int = 0, output_padding: int = 0, **kwargs):
34
+ super().__init__(*args, **kwargs)
35
+ self.__padding = padding
36
+ self.__output_padding = output_padding
37
+
38
+ def forward(self, x):
39
+ return super().forward(x)[..., : -(self.__padding * 2 - self.__output_padding)]
40
+
41
+
42
+ def WNCausalConv1d(*args, **kwargs):
43
+ return weight_norm(CausalConv1d(*args, **kwargs))
44
+
45
+
46
+ def WNCausalTransposeConv1d(*args, **kwargs):
47
+ return weight_norm(CausalTransposeConv1d(*args, **kwargs))
48
+
49
+
50
+ # Scripting this brings model speed up 1.4x
51
+ @torch.jit.script
52
+ def snake(x, alpha):
53
+ shape = x.shape
54
+ x = x.reshape(shape[0], shape[1], -1)
55
+ x = x + (alpha + 1e-9).reciprocal() * torch.sin(alpha * x).pow(2)
56
+ x = x.reshape(shape)
57
+ return x
58
+
59
+
60
+ class Snake1d(nn.Module):
61
+ def __init__(self, channels):
62
+ super().__init__()
63
+ self.alpha = nn.Parameter(torch.ones(1, channels, 1))
64
+
65
+ def forward(self, x):
66
+ return snake(x, self.alpha)
67
+
68
+
69
+ def init_weights(m):
70
+ if isinstance(m, nn.Conv1d):
71
+ nn.init.trunc_normal_(m.weight, std=0.02)
72
+ if m.bias is not None:
73
+ nn.init.constant_(m.bias, 0)
74
+
75
+
76
+ class CausalResidualUnit(nn.Module):
77
+ def __init__(self, dim: int = 16, dilation: int = 1, kernel: int = 7, groups: int = 1):
78
+ super().__init__()
79
+ pad = ((7 - 1) * dilation) // 2
80
+ self.block = nn.Sequential(
81
+ Snake1d(dim),
82
+ WNCausalConv1d(
83
+ dim,
84
+ dim,
85
+ kernel_size=kernel,
86
+ dilation=dilation,
87
+ padding=pad,
88
+ groups=groups,
89
+ ),
90
+ Snake1d(dim),
91
+ WNCausalConv1d(dim, dim, kernel_size=1),
92
+ )
93
+
94
+ def forward(self, x):
95
+ y = self.block(x)
96
+ pad = (x.shape[-1] - y.shape[-1]) // 2
97
+ assert pad == 0
98
+ if pad > 0:
99
+ x = x[..., pad:-pad]
100
+ return x + y
101
+
102
+
103
+ class CausalEncoderBlock(nn.Module):
104
+ def __init__(self, output_dim: int = 16, input_dim=None, stride: int = 1, groups=1):
105
+ super().__init__()
106
+ input_dim = input_dim or output_dim // 2
107
+ self.block = nn.Sequential(
108
+ CausalResidualUnit(input_dim, dilation=1, groups=groups),
109
+ CausalResidualUnit(input_dim, dilation=3, groups=groups),
110
+ CausalResidualUnit(input_dim, dilation=9, groups=groups),
111
+ Snake1d(input_dim),
112
+ WNCausalConv1d(
113
+ input_dim,
114
+ output_dim,
115
+ kernel_size=2 * stride,
116
+ stride=stride,
117
+ padding=math.ceil(stride / 2),
118
+ output_padding=stride % 2,
119
+ ),
120
+ )
121
+
122
+ def forward(self, x):
123
+ return self.block(x)
124
+
125
+
126
+ class CausalEncoder(nn.Module):
127
+ def __init__(
128
+ self,
129
+ d_model: int = 64,
130
+ latent_dim: int = 32,
131
+ strides: list = [2, 4, 8, 8],
132
+ depthwise: bool = False,
133
+ ):
134
+ super().__init__()
135
+ # Create first convolution
136
+ self.block = [WNCausalConv1d(1, d_model, kernel_size=7, padding=3)]
137
+
138
+ # Create EncoderBlocks that double channels as they downsample by `stride`
139
+ for stride in strides:
140
+ d_model *= 2
141
+ groups = d_model // 2 if depthwise else 1
142
+ self.block += [CausalEncoderBlock(output_dim=d_model, stride=stride, groups=groups)]
143
+
144
+ groups = d_model if depthwise else 1
145
+
146
+ # Create two convolution, for mu and logvar
147
+ self.fc_mu = WNCausalConv1d(d_model, latent_dim, kernel_size=3, padding=1)
148
+ self.fc_logvar = WNCausalConv1d(d_model, latent_dim, kernel_size=3, padding=1)
149
+
150
+ # Wrap black into nn.Sequential
151
+ self.block = nn.Sequential(*self.block)
152
+ self.enc_dim = d_model
153
+
154
+ def forward(self, x):
155
+ hidden_state = self.block(x)
156
+ return {
157
+ "hidden_state": hidden_state,
158
+ "mu": self.fc_mu(hidden_state),
159
+ "logvar": self.fc_logvar(hidden_state),
160
+ }
161
+
162
+
163
+ class NoiseBlock(nn.Module):
164
+ def __init__(self, dim):
165
+ super().__init__()
166
+ self.linear = WNCausalConv1d(dim, dim, kernel_size=1, bias=False)
167
+
168
+ def forward(self, x):
169
+ B, C, T = x.shape
170
+ noise = torch.randn((B, 1, T), device=x.device, dtype=x.dtype)
171
+ h = self.linear(x)
172
+ n = noise * h
173
+ x = x + n
174
+ return x
175
+
176
+
177
+ class CausalDecoderBlock(nn.Module):
178
+ def __init__(
179
+ self,
180
+ input_dim: int = 16,
181
+ output_dim: int = 8,
182
+ stride: int = 1,
183
+ groups=1,
184
+ use_noise_block: bool = False,
185
+ ):
186
+ super().__init__()
187
+ layers = [
188
+ Snake1d(input_dim),
189
+ WNCausalTransposeConv1d(
190
+ input_dim,
191
+ output_dim,
192
+ kernel_size=2 * stride,
193
+ stride=stride,
194
+ padding=math.ceil(stride / 2),
195
+ output_padding=stride % 2,
196
+ ),
197
+ ]
198
+ if use_noise_block:
199
+ layers.append(NoiseBlock(output_dim))
200
+ layers.extend(
201
+ [
202
+ CausalResidualUnit(output_dim, dilation=1, groups=groups),
203
+ CausalResidualUnit(output_dim, dilation=3, groups=groups),
204
+ CausalResidualUnit(output_dim, dilation=9, groups=groups),
205
+ ]
206
+ )
207
+ self.block = nn.Sequential(*layers)
208
+ self.input_channels = input_dim
209
+
210
+ def forward(self, x):
211
+ return self.block(x)
212
+
213
+
214
+ class TransposeLastTwoDim(torch.nn.Module):
215
+ def forward(self, x):
216
+ return torch.transpose(x, -1, -2)
217
+
218
+
219
+ class SampleRateConditionLayer(nn.Module):
220
+ def __init__(
221
+ self,
222
+ input_dim: int,
223
+ sr_bin_buckets: int = None,
224
+ cond_type: str = "scale_bias",
225
+ cond_dim: int = 128,
226
+ out_layer: bool = False,
227
+ ):
228
+ super().__init__()
229
+
230
+ self.cond_type, out_layer_in_dim = cond_type, input_dim
231
+
232
+ if cond_type == "scale_bias":
233
+ self.scale_embed = nn.Embedding(sr_bin_buckets, input_dim)
234
+ self.bias_embed = nn.Embedding(sr_bin_buckets, input_dim)
235
+ nn.init.ones_(self.scale_embed.weight)
236
+ nn.init.zeros_(self.bias_embed.weight)
237
+ elif cond_type == "scale_bias_init":
238
+ self.scale_embed = nn.Embedding(sr_bin_buckets, input_dim)
239
+ self.bias_embed = nn.Embedding(sr_bin_buckets, input_dim)
240
+ nn.init.normal_(self.scale_embed.weight, mean=1)
241
+ nn.init.normal_(self.bias_embed.weight)
242
+ elif cond_type == "add":
243
+ self.cond_embed = nn.Embedding(sr_bin_buckets, input_dim)
244
+ nn.init.normal_(self.cond_embed.weight)
245
+ elif cond_type == "concat":
246
+ self.cond_embed = nn.Embedding(sr_bin_buckets, cond_dim)
247
+ assert out_layer, "out_layer must be True for concat cond_type"
248
+ out_layer_in_dim = input_dim + cond_dim
249
+ else:
250
+ raise ValueError(f"Invalid cond_type: {cond_type}")
251
+
252
+ if out_layer:
253
+ self.out_layer = nn.Sequential(
254
+ Snake1d(out_layer_in_dim),
255
+ WNCausalConv1d(out_layer_in_dim, input_dim, kernel_size=1),
256
+ )
257
+ else:
258
+ self.out_layer = nn.Identity()
259
+
260
+ def forward(self, x, sr_cond):
261
+ if self.cond_type == "scale_bias" or self.cond_type == "scale_bias_init":
262
+ x = x * self.scale_embed(sr_cond).unsqueeze(-1) + self.bias_embed(sr_cond).unsqueeze(-1)
263
+ elif self.cond_type == "add":
264
+ x = x + self.cond_embed(sr_cond).unsqueeze(-1)
265
+ elif self.cond_type == "concat":
266
+ x = torch.cat([x, self.cond_embed(sr_cond).unsqueeze(-1).repeat(1, 1, x.shape[-1])], dim=1)
267
+
268
+ return self.out_layer(x)
269
+
270
+
271
+ class CausalDecoder(nn.Module):
272
+ def __init__(
273
+ self,
274
+ input_channel,
275
+ channels,
276
+ rates,
277
+ depthwise: bool = False,
278
+ d_out: int = 1,
279
+ use_noise_block: bool = False,
280
+ sr_bin_boundaries: List[int] = None,
281
+ cond_type: str = "scale_bias",
282
+ cond_dim: int = 128,
283
+ cond_out_layer: bool = False,
284
+ ):
285
+ super().__init__()
286
+
287
+ # Add first conv layer
288
+ if depthwise:
289
+ layers = [
290
+ WNCausalConv1d(input_channel, input_channel, kernel_size=7, padding=3, groups=input_channel),
291
+ WNCausalConv1d(input_channel, channels, kernel_size=1),
292
+ ]
293
+ else:
294
+ layers = [WNCausalConv1d(input_channel, channels, kernel_size=7, padding=3)]
295
+
296
+ # Add upsampling + MRF blocks
297
+ for i, stride in enumerate(rates):
298
+ input_dim = channels // 2**i
299
+ output_dim = channels // 2 ** (i + 1)
300
+ groups = output_dim if depthwise else 1
301
+ layers += [
302
+ CausalDecoderBlock(
303
+ input_dim,
304
+ output_dim,
305
+ stride,
306
+ groups=groups,
307
+ use_noise_block=use_noise_block,
308
+ )
309
+ ]
310
+
311
+ # Add final conv layer
312
+ layers += [
313
+ Snake1d(output_dim),
314
+ WNCausalConv1d(output_dim, d_out, kernel_size=7, padding=3),
315
+ nn.Tanh(),
316
+ ]
317
+
318
+ if sr_bin_boundaries is None:
319
+ self.model = nn.Sequential(*layers)
320
+ self.sr_bin_boundaries = None
321
+ else:
322
+ self.model = nn.ModuleList(layers)
323
+
324
+ self.register_buffer("sr_bin_boundaries", torch.tensor(sr_bin_boundaries, dtype=torch.int32))
325
+ self.sr_bin_buckets = len(sr_bin_boundaries) + 1
326
+
327
+ cond_layers = []
328
+ for layer in self.model:
329
+ if layer.__class__.__name__ == "CausalDecoderBlock":
330
+ cond_layers.append(
331
+ SampleRateConditionLayer(
332
+ input_dim=layer.input_channels,
333
+ sr_bin_buckets=self.sr_bin_buckets,
334
+ cond_type=cond_type,
335
+ cond_dim=cond_dim,
336
+ out_layer=cond_out_layer,
337
+ )
338
+ )
339
+ else:
340
+ cond_layers.append(None)
341
+ self.sr_cond_model = nn.ModuleList(cond_layers)
342
+
343
+ def get_sr_idx(self, sr):
344
+ return torch.bucketize(sr, self.sr_bin_boundaries)
345
+
346
+ def forward(self, x, sr_cond=None):
347
+ if self.sr_bin_boundaries is not None:
348
+ # assert sr_cond is not None
349
+ sr_cond = self.get_sr_idx(sr_cond)
350
+
351
+ for layer, sr_cond_layer in zip(self.model, self.sr_cond_model):
352
+ if sr_cond_layer is not None:
353
+ x = sr_cond_layer(x, sr_cond)
354
+ x = layer(x)
355
+ return x
356
+ else:
357
+ return self.model(x)
358
+
359
+
360
+ class AudioVAEConfig(BaseModel):
361
+ encoder_dim: int = 128
362
+ encoder_rates: List[int] = [2, 5, 8, 8]
363
+ latent_dim: int = 64
364
+ decoder_dim: int = 2048
365
+ decoder_rates: List[int] = [8, 6, 5, 2, 2, 2]
366
+ depthwise: bool = True
367
+ sample_rate: int = 16000
368
+ out_sample_rate: int = 48000
369
+ use_noise_block: bool = False
370
+ sr_bin_boundaries: Optional[List[int]] = [20000, 30000, 40000]
371
+ cond_type: str = "scale_bias"
372
+ cond_dim: int = 128
373
+ cond_out_layer: bool = False
374
+
375
+
376
+ class AudioVAE(nn.Module):
377
+ """
378
+ Args:
379
+ """
380
+
381
+ def __init__(
382
+ self,
383
+ config: AudioVAEConfig = None,
384
+ ):
385
+ # 如果没有传入config,使用默认配置
386
+ if config is None:
387
+ config = AudioVAEConfig()
388
+
389
+ super().__init__()
390
+
391
+ encoder_dim = config.encoder_dim
392
+ encoder_rates = config.encoder_rates
393
+ latent_dim = config.latent_dim
394
+ decoder_dim = config.decoder_dim
395
+ decoder_rates = config.decoder_rates
396
+ depthwise = config.depthwise
397
+ sample_rate = config.sample_rate
398
+ out_sample_rate = config.out_sample_rate
399
+ use_noise_block = config.use_noise_block
400
+ sr_bin_boundaries = config.sr_bin_boundaries
401
+ cond_type = config.cond_type
402
+ cond_dim = config.cond_dim
403
+ cond_out_layer = config.cond_out_layer
404
+
405
+ self.encoder_dim = encoder_dim
406
+ self.encoder_rates = encoder_rates
407
+ self.decoder_dim = decoder_dim
408
+ self.decoder_rates = decoder_rates
409
+ self.depthwise = depthwise
410
+
411
+ self.use_noise_block = use_noise_block
412
+
413
+ if latent_dim is None:
414
+ latent_dim = encoder_dim * (2 ** len(encoder_rates))
415
+
416
+ self.latent_dim = latent_dim
417
+ self.hop_length = np.prod(encoder_rates)
418
+ self.encoder = CausalEncoder(
419
+ encoder_dim,
420
+ latent_dim,
421
+ encoder_rates,
422
+ depthwise=depthwise,
423
+ )
424
+
425
+ self.decoder = CausalDecoder(
426
+ latent_dim,
427
+ decoder_dim,
428
+ decoder_rates,
429
+ depthwise=depthwise,
430
+ use_noise_block=use_noise_block,
431
+ sr_bin_boundaries=sr_bin_boundaries,
432
+ cond_type=cond_type,
433
+ cond_dim=cond_dim,
434
+ cond_out_layer=cond_out_layer,
435
+ )
436
+ self.sample_rate = sample_rate
437
+ self.out_sample_rate = out_sample_rate
438
+ self.sr_bin_boundaries = sr_bin_boundaries
439
+ self.chunk_size = math.prod(encoder_rates)
440
+ self.decode_chunk_size = math.prod(decoder_rates)
441
+
442
+ def preprocess(self, audio_data, sample_rate):
443
+ if sample_rate is None:
444
+ sample_rate = self.sample_rate
445
+ assert sample_rate == self.sample_rate
446
+ pad_to = self.hop_length
447
+ length = audio_data.shape[-1]
448
+ right_pad = math.ceil(length / pad_to) * pad_to - length
449
+ audio_data = nn.functional.pad(audio_data, (0, right_pad))
450
+
451
+ return audio_data
452
+
453
+ def decode(self, z: torch.Tensor, sr_cond: torch.Tensor = None):
454
+ """Decode given latent codes and return audio data
455
+
456
+ Parameters
457
+ ----------
458
+ z : Tensor[B x D x T]
459
+ Quantized continuous representation of input
460
+ length : int, optional
461
+ Number of samples in output audio, by default None
462
+
463
+ Returns
464
+ -------
465
+ dict
466
+ A dictionary with the following keys:
467
+ "audio" : Tensor[B x 1 x length]
468
+ Decoded audio data.
469
+ """
470
+ if self.sr_bin_boundaries is not None:
471
+ # use default output sample rate
472
+ if sr_cond is None:
473
+ sr_cond = torch.tensor([self.out_sample_rate], device=z.device, dtype=torch.int32)
474
+ return self.decoder(z, sr_cond)
475
+
476
+ def streaming_decode(self):
477
+ """Return a ``StreamingVAEDecoder`` context manager for stateful
478
+ chunk-by-chunk decoding. Each call to ``decode_chunk`` processes only
479
+ the new latent patch and carries causal-conv state internally, avoiding
480
+ the redundant overlap decode used previously.
481
+
482
+ Usage::
483
+
484
+ with vae.streaming_decode() as dec:
485
+ for patch in patches:
486
+ audio_chunk = dec.decode_chunk(patch)
487
+ """
488
+ return StreamingVAEDecoder(self)
489
+
490
+ def encode(self, audio_data: torch.Tensor, sample_rate: int):
491
+ """
492
+ Args:
493
+ audio_data: Tensor[B x 1 x T]
494
+ sample_rate: int
495
+ Returns:
496
+ z: Tensor[B x D x T]
497
+ """
498
+ if audio_data.ndim == 2:
499
+ audio_data = audio_data.unsqueeze(1)
500
+
501
+ audio_data = self.preprocess(audio_data, sample_rate)
502
+ return self.encoder(audio_data)["mu"]
503
+
504
+
505
+ class StreamingVAEDecoder:
506
+ """Stateful streaming wrapper for :class:`AudioVAE`.
507
+
508
+ Carries causal-convolution padding buffers between calls so that each
509
+ ``decode_chunk`` processes only the new latent patch — no overlap needed.
510
+ """
511
+
512
+ def __init__(self, vae: AudioVAE):
513
+ self._vae = vae
514
+ self._states: dict = {}
515
+ self._originals: list = []
516
+
517
+ # -- context manager --------------------------------------------------
518
+ def __enter__(self):
519
+ self._states.clear()
520
+ self._install()
521
+ return self
522
+
523
+ def __exit__(self, *exc):
524
+ self._restore()
525
+ self._states.clear()
526
+
527
+ # -- public API --------------------------------------------------------
528
+ def decode_chunk(self, z_chunk: torch.Tensor) -> torch.Tensor:
529
+ """Decode a single latent chunk and return the audio waveform."""
530
+ return self._vae.decode(z_chunk)
531
+
532
+ # -- internals ---------------------------------------------------------
533
+ def _install(self):
534
+ for name, mod in self._vae.decoder.named_modules():
535
+ if isinstance(mod, CausalConv1d):
536
+ pad = mod._CausalConv1d__padding * 2 - mod._CausalConv1d__output_padding
537
+ if pad > 0:
538
+ self._patch_causal_conv(mod, pad)
539
+ elif isinstance(mod, CausalTransposeConv1d):
540
+ trim = mod._CausalTransposeConv1d__padding * 2 - mod._CausalTransposeConv1d__output_padding
541
+ ctx = (mod.kernel_size[0] - 1) // mod.stride[0]
542
+ if ctx > 0:
543
+ self._patch_transpose_conv(mod, ctx, trim)
544
+
545
+ def _patch_causal_conv(self, mod, pad_size):
546
+ states = self._states
547
+ key = id(mod)
548
+ orig = mod.forward
549
+
550
+ def fwd(x, _k=key, _p=pad_size, _m=mod):
551
+ x_pad = torch.cat([states[_k], x], dim=-1) if _k in states else F.pad(x, (_p, 0))
552
+ if x.shape[-1] >= _p:
553
+ states[_k] = x[:, :, -_p:].detach()
554
+ else:
555
+ prev = states.get(_k, torch.zeros(x.shape[0], x.shape[1], _p,
556
+ device=x.device, dtype=x.dtype))
557
+ states[_k] = torch.cat([prev, x], dim=-1)[:, :, -_p:].detach()
558
+ return nn.Conv1d.forward(_m, x_pad)
559
+
560
+ mod.forward = fwd
561
+ self._originals.append((mod, orig))
562
+
563
+ def _patch_transpose_conv(self, mod, ctx, trim):
564
+ states = self._states
565
+ key = id(mod)
566
+ orig = mod.forward
567
+
568
+ def fwd(x, _k=key, _c=ctx, _t=trim, _m=mod):
569
+ x_full = torch.cat([states[_k], x], dim=-1) if _k in states else F.pad(x, (_c, 0))
570
+ states[_k] = x[:, :, -_c:].detach()
571
+ out = nn.ConvTranspose1d.forward(_m, x_full)
572
+ left = _c * _m.stride[0]
573
+ return out[..., left:-_t] if _t > 0 else out[..., left:]
574
+
575
+ mod.forward = fwd
576
+ self._originals.append((mod, orig))
577
+
578
+ def _restore(self):
579
+ for mod, orig in self._originals:
580
+ mod.forward = orig
581
+ self._originals.clear()
rapido/engine.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Rápido inference: Brazilian Portuguese text in, 48 kHz speech out."""
2
+ import json
3
+ import re
4
+ import time
5
+ from dataclasses import dataclass, field
6
+
7
+ import numpy as np
8
+ import torch
9
+ from huggingface_hub import hf_hub_download
10
+
11
+ from ._vae import AudioVAE, AudioVAEConfig
12
+ from .frontend import VOCAB, encode, normalize
13
+ from .model import RapidoModel
14
+
15
+ DEFAULT_REPO = "edwixx/rapido-tts"
16
+ DECODER_REPO = "openbmb/VoxCPM2" # frozen AudioVAE decoder, Apache 2.0
17
+ SAMPLE_RATE = 48000
18
+
19
+
20
+ @dataclass
21
+ class Config:
22
+ model: str = DEFAULT_REPO # HF repo id or a local .pt path
23
+ device: str = "auto" # "auto" | "cuda" | "cpu"
24
+ steps: int = 8 # ODE steps (8 matches 16 in accuracy)
25
+ guidance: float = 4.0 # classifier-free guidance
26
+ speed: float = 1.0 # speaking rate
27
+ seed: int | None = 0 # None = random
28
+ cpu_threads: int | None = None
29
+
30
+
31
+ @dataclass
32
+ class Result:
33
+ output: dict # {"audio": np.float32 [N], "sample_rate": 48000, "text": spoken form}
34
+ metrics: dict = field(default_factory=dict)
35
+
36
+ def save(self, path):
37
+ import wave
38
+ pcm = (np.clip(self.output["audio"], -1, 1) * 32767).astype(np.int16)
39
+ with wave.open(str(path), "wb") as w:
40
+ w.setnchannels(1)
41
+ w.setsampwidth(2)
42
+ w.setframerate(self.output["sample_rate"])
43
+ w.writeframes(pcm.tobytes())
44
+ return path
45
+
46
+
47
+ class Rapido:
48
+ def __init__(self, config: Config | None = None):
49
+ self.config = config or Config()
50
+ c = self.config
51
+ self.device = ("cuda" if torch.cuda.is_available() else "cpu") if c.device == "auto" else c.device
52
+ if c.cpu_threads:
53
+ torch.set_num_threads(c.cpu_threads)
54
+ path = c.model if c.model.endswith(".pt") else hf_hub_download(c.model, "rapido.pt")
55
+ ck = torch.load(path, map_location="cpu", weights_only=False)
56
+ self.model = RapidoModel(ck["cfg"], VOCAB, len(ck["spk_order"]))
57
+ self.model.load_state_dict({k: v.float() for k, v in ck["ema"].items()})
58
+ self.model.to(self.device).eval()
59
+ vae_cfg = json.load(open(hf_hub_download(DECODER_REPO, "config.json")))["audio_vae_config"]
60
+ self.vae = AudioVAE(AudioVAEConfig(**vae_cfg))
61
+ sd = torch.load(hf_hub_download(DECODER_REPO, "audiovae.pth"), map_location="cpu", weights_only=True)
62
+ self.vae.load_state_dict(sd.get("state_dict", sd))
63
+ self.vae.to(self.device).eval()
64
+
65
+ @classmethod
66
+ def from_pretrained(cls, model: str = DEFAULT_REPO, **kwargs):
67
+ return cls(Config(model=model, **kwargs))
68
+
69
+ def normalize(self, text: str) -> str:
70
+ """The exact spoken form the model reads (numbers, money, dates... expanded in pt-BR)."""
71
+ return normalize(text)
72
+
73
+ def synthesize(self, text: str, stream: bool = False, **settings):
74
+ """Text -> Result. With stream=True, yields one Result per sentence as soon as it is ready.
75
+ settings override Config: steps, guidance, speed, seed."""
76
+ gen = self._pieces(text, settings, max_chars=0 if stream else 220)
77
+ return gen if stream else self._join(list(gen))
78
+
79
+ @torch.inference_mode()
80
+ def _pieces(self, text, settings, max_chars=220):
81
+ c = self.config
82
+ steps, guidance = settings.get("steps", c.steps), settings.get("guidance", c.guidance)
83
+ speed, seed = settings.get("speed", c.speed), settings.get("seed", c.seed)
84
+ for k, piece in enumerate(_sentences(text, max_chars)):
85
+ spoken = normalize(piece)
86
+ if not spoken:
87
+ continue
88
+ t0 = time.time()
89
+ ids = torch.tensor(encode(spoken), device=self.device)
90
+ z = self.model.generate(ids, steps=steps, guidance=guidance, speed=speed,
91
+ seed=None if seed is None else seed + k)
92
+ audio = self.vae.decode(z.T[None]).float().reshape(-1).cpu().numpy()
93
+ dt = time.time() - t0
94
+ dur = len(audio) / SAMPLE_RATE
95
+ yield Result({"audio": audio, "sample_rate": SAMPLE_RATE, "text": spoken},
96
+ {"audio_s": round(dur, 3), "total_s": round(dt, 3), "x_realtime": round(dur / dt, 2)})
97
+
98
+ @staticmethod
99
+ def _join(parts, pause=0.22):
100
+ if not parts:
101
+ raise ValueError("nothing to say after normalization")
102
+ gap = np.zeros(int(pause * SAMPLE_RATE), np.float32)
103
+ audio = np.concatenate([x for p in parts for x in (p.output["audio"], gap)][:-1])
104
+ total = sum(p.metrics["total_s"] for p in parts)
105
+ dur = len(audio) / SAMPLE_RATE
106
+ return Result({"audio": audio, "sample_rate": SAMPLE_RATE, "text": " ".join(p.output["text"] for p in parts)},
107
+ {"audio_s": round(dur, 3), "total_s": round(total, 3), "x_realtime": round(dur / total, 2)})
108
+
109
+
110
+ def _sentences(text, max_chars=220):
111
+ out, cur = [], ""
112
+ for p in re.split(r"(?<=[.!?…])\s+", text.strip()):
113
+ if cur and len(cur) + len(p) > max_chars:
114
+ out.append(cur)
115
+ cur = p
116
+ else:
117
+ cur = f"{cur} {p}".strip()
118
+ return [p for p in out + [cur] if p.strip()]
rapido/frontend.py ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Rápido text frontend: written Brazilian Portuguese in, text in the model's own alphabet out.
2
+
3
+ Numbers, money, percentages, ordinals, times, dates and common abbreviations are read aloud in
4
+ pt-BR. `modernize()` maps pre-1943 spellings found in the training audiobooks (elle, janella,
5
+ pharmacia, inglez...) to modern ones; the reform changed spelling, not pronunciation. Nothing here
6
+ raises on text.
7
+ """
8
+ import re
9
+ import unicodedata
10
+
11
+ from num2words import num2words
12
+
13
+ LETTERS = "abcdefghijklmnopqrstuvwxyzáàâãçéêíóôõúü"
14
+ PUNCT = ".,?!;:-'"
15
+ VOCAB = ["<pad>", "<unk>", " "] + list(LETTERS) + list(PUNCT)
16
+
17
+ TYPOGRAPHY = str.maketrans({"’": "'", "‘": "'", "´": "'", "`": "'", "“": '"', "”": '"', "„": '"', "«": '"',
18
+ "»": '"', "–": "-", "—": "-", "−": "-", "…": "...", " ": " "})
19
+ MONTHS = ["janeiro", "fevereiro", "março", "abril", "maio", "junho", "julho", "agosto", "setembro", "outubro",
20
+ "novembro", "dezembro"]
21
+ ABBREV = {
22
+ "sr.": "senhor", "sra.": "senhora", "srta.": "senhorita", "dr.": "doutor", "dra.": "doutora",
23
+ "prof.": "professor", "profa.": "professora", "sto.": "santo", "sta.": "santa", "av.": "avenida",
24
+ "r.": "rua", "etc.": "etcétera", "ex.": "exemplo", "obs.": "observação", "pág.": "página", "p.": "página",
25
+ "tel.": "telefone", "n.º": "número", "nº": "número", "no.": "número", "vs.": "versus", "aprox.": "aproximadamente",
26
+ }
27
+ UNITS = {"km": "quilômetros", "kg": "quilos", "g": "gramas", "m": "metros", "cm": "centímetros", "mm": "milímetros",
28
+ "l": "litros", "ml": "mililitros", "h": "horas", "min": "minutos", "s": "segundos", "km/h": "quilômetros por hora",
29
+ "°c": "graus celsius", "°": "graus", "gb": "gigabytes", "mb": "megabytes"}
30
+ FEM = {"um": "uma", "dois": "duas"}
31
+
32
+
33
+ def n2w(n, **kw):
34
+ try:
35
+ return num2words(n, lang="pt_BR", **kw).replace(",", "")
36
+ except Exception:
37
+ return " ".join(num2words(int(d), lang="pt_BR") for d in str(n) if d.isdigit())
38
+
39
+
40
+ def fem(words):
41
+ """'um', 'dois', 'duzentos'... -> feminine, for horas, pessoas-style nouns."""
42
+ out = []
43
+ for w in words.split():
44
+ w = FEM.get(w, w)
45
+ w = re.sub(r"entos$", "entas", w) if w.endswith("entos") and w != "quinhentos" else w
46
+ out.append("quinhentas" if w == "quinhentos" else w)
47
+ return " ".join(out)
48
+
49
+
50
+ def parse_num(s):
51
+ """'1.250.000' -> 1250000, '3,5' -> 3.5, '1.250,75' -> 1250.75"""
52
+ s = s.strip().rstrip(".,")
53
+ if re.fullmatch(r"\d{1,3}(\.\d{3})+(,\d+)?", s):
54
+ s = s.replace(".", "")
55
+ return float(s.replace(",", ".")) if "," in s else int(s)
56
+
57
+
58
+ def money(m):
59
+ v = parse_num(m.group(1))
60
+ reais, cent = int(v), round((v - int(v)) * 100) if isinstance(v, float) else 0
61
+ out = []
62
+ if reais:
63
+ out.append(n2w(reais) + (" de" if reais >= 1_000_000 and reais % 1_000_000 == 0 else "") +
64
+ (" real" if reais == 1 else " reais"))
65
+ if cent:
66
+ out.append(n2w(cent) + (" centavo" if cent == 1 else " centavos"))
67
+ return " " + (" e ".join(out) if out else "zero reais") + " "
68
+
69
+
70
+ def time_(m):
71
+ h = int(m.group(1))
72
+ mi = int(m.group(2) or 0) if m.re.groups > 1 else 0
73
+ if h > 24 or mi > 59:
74
+ return m.group(0)
75
+ hs = fem(n2w(h)) + (" hora" if h == 1 else " horas")
76
+ return " " + hs + (f" e {fem(n2w(mi)) if mi in (1, 2) else n2w(mi)} minuto{'s' if mi != 1 else ''}" if mi else "") + " "
77
+
78
+
79
+ def date(m):
80
+ d, mo, y = int(m.group(1)), int(m.group(2)), m.group(3)
81
+ if not (1 <= d <= 31 and 1 <= mo <= 12):
82
+ return m.group(0)
83
+ day = "primeiro" if d == 1 else n2w(d)
84
+ out = f" {day} de {MONTHS[mo - 1]}"
85
+ if y:
86
+ y = int(y) + (2000 if len(y) == 2 and int(y) < 50 else 1900 if len(y) == 2 else 0)
87
+ out += f" de {n2w(y)}"
88
+ return out + " "
89
+
90
+
91
+ def number(m):
92
+ s = m.group(0)
93
+ if re.fullmatch(r"\d{10,}", s): # phone numbers, codes: digit by digit
94
+ return " " + " ".join(n2w(int(c)) for c in s) + " "
95
+ v = parse_num(s)
96
+ if isinstance(v, float):
97
+ whole, frac = s.replace(".", "").split(",")
98
+ return f" {n2w(int(whole))} vírgula {' '.join(n2w(int(c)) for c in frac) if frac.startswith('0') else n2w(int(frac))} "
99
+ return f" {n2w(v)} "
100
+
101
+
102
+ ROMAN = {"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000}
103
+ ROMAN_CTX = r"(século|séculos|capítulo|capítulos|volume|tomo|parte|ato|livro|papa|rei|rainha|dom|dona|pio|joão|pedro|luís|henrique|elizabeth|guerra mundial|bienal|edição|congresso|fórum)"
104
+ FEM_NOUNS = {"pessoa", "pessoas", "hora", "horas", "criança", "crianças", "vez", "vezes", "semana", "semanas",
105
+ "mulher", "mulheres", "cidade", "cidades", "casa", "casas", "vaga", "vagas", "família", "famílias",
106
+ "empresa", "empresas", "escola", "escolas", "página", "páginas", "moeda", "moedas", "unidade",
107
+ "unidades", "tonelada", "toneladas", "pessoa", "noite", "noites", "mãe", "mães", "filha", "filhas",
108
+ "aluna", "alunas", "garrafa", "garrafas", "xícara", "xícaras", "colher", "colheres", "fruta", "frutas",
109
+ "árvore", "árvores", "rua", "ruas", "vida", "vidas", "morte", "mortes", "parte", "partes", "linha",
110
+ "linhas", "palavra", "palavras", "questão", "questões", "região", "regiões", "nação", "nações",
111
+ "edição", "edições", "medalha", "medalhas", "gramas" }
112
+ MASC_A = {"dia", "dias", "mapa", "mapas", "problema", "problemas", "sistema", "sistemas", "programa", "programas",
113
+ "tema", "temas", "clima", "planeta", "planetas", "idioma", "idiomas", "poema", "poemas", "telefonema",
114
+ "cinema", "cinemas", "atleta", "atletas", "jornalista", "artista", "artistas", "turista", "turistas"}
115
+
116
+
117
+ def roman_to_int(r):
118
+ total, prev = 0, 0
119
+ for ch in reversed(r):
120
+ v = ROMAN[ch]
121
+ total = total - v if v < prev else total + v
122
+ prev = max(prev, v)
123
+ return total
124
+
125
+
126
+ def romans(t):
127
+ """Roman numerals after context words (século XVIII, Dom Pedro II) and standalone II..XXXIX."""
128
+ def ctx(m):
129
+ n = roman_to_int(m.group(2))
130
+ word = m.group(1).lower()
131
+ if word in ("papa", "rei", "rainha", "dom", "dona", "pio", "joão", "pedro", "luís", "henrique", "elizabeth") and n <= 10:
132
+ return f"{m.group(1)} {n2w(n, to='ordinal')}"
133
+ return f"{m.group(1)} {n2w(n)}"
134
+ t = re.sub(r"(?i)\b" + ROMAN_CTX + r"\s+([IVXLCDM]{1,7})\b", ctx, t)
135
+ return t
136
+
137
+
138
+ def is_fem(word):
139
+ w = word.lower().strip(".,;:!?")
140
+ if w in MASC_A:
141
+ return False
142
+ return w in FEM_NOUNS or (len(w) > 3 and (w.endswith("as") or w.endswith("a")) and not w.endswith("ma"))
143
+
144
+
145
+ def agree(m):
146
+ """Cardinal + feminine noun: 'duas pessoas', 'duzentas horas', 'uma vez'."""
147
+ num, noun = m.group(1), m.group(2)
148
+ if "," in num:
149
+ return m.group(0)
150
+ v = parse_num(num)
151
+ words = n2w(v)
152
+ if is_fem(noun):
153
+ words = fem(words)
154
+ return f" {words} {noun}"
155
+
156
+
157
+ def phones(t):
158
+ """(11) 98765-4321, 0800 721 3344 -> digits/pairs, not big cardinals."""
159
+ def rd(g):
160
+ g = re.sub(r"\D", "", g)
161
+ return " ".join(n2w(int(c)) for c in g)
162
+ t = re.sub(r"\((\d{2})\)\s*(\d{4,5})[-\s]?(\d{4})", lambda m: f" {rd(m.group(1))}, {rd(m.group(2))}, {rd(m.group(3))} ", t)
163
+ t = re.sub(r"\b(0800|0300|0500|0900)[\s-]?(\d{3})[\s-]?(\d{4})\b", lambda m: f" {rd(m.group(1))}, {rd(m.group(2))}, {rd(m.group(3))} ", t)
164
+ t = re.sub(r"\b(\d{4,5})-(\d{4})\b", lambda m: f" {rd(m.group(1))}, {rd(m.group(2))} ", t)
165
+ return t
166
+
167
+
168
+ def spoken(text):
169
+ t = " " + text.translate(TYPOGRAPHY) + " "
170
+ t = phones(t)
171
+ t = romans(t)
172
+ t = re.sub(r"R\$\s*(\d{1,3}(?:\.\d{3})+(?:,\d+)?|\d+(?:,\d+)?)", money, t)
173
+ t = re.sub(r"(\d{1,3}(?:\.\d{3})+(?:,\d+)?|\d+(?:,\d+)?)\s*%", lambda m: f" {number(re.match(r'.+', m.group(1)))} por cento ", t)
174
+ t = re.sub(r"\b(\d{1,2})/(\d{1,2})(?:/(\d{2}|\d{4}))?\b", date, t)
175
+ t = re.sub(r"\b(\d{1,2})(?::|h)(\d{2})\b(?:min)?", time_, t)
176
+ t = re.sub(r"\b(\d{1,2})h\b", time_, t)
177
+ t = re.sub(r"(?i)(?<![\w.])(" + "|".join(re.escape(k) for k in sorted(ABBREV, key=len, reverse=True)) + r")(?=\s)",
178
+ lambda m: ABBREV[m.group(1).lower()], t)
179
+ t = re.sub(r"(\d{1,3}(?:\.\d{3})+(?:,\d+)?|\d+(?:,\d+)?)\s*(km/h|km|kg|mm|cm|ml|min|gb|mb|°\s*c|°|g|m|l|s|h)\b",
180
+ lambda m: m.group(1) + " " + unit_word(m.group(2)), t, flags=re.I)
181
+ t = re.sub(r"(\d+)\s*º", lambda m: f" {n2w(int(m.group(1)), to='ordinal')} ", t)
182
+ t = re.sub(r"(\d+)\s*ª", lambda m: f" {fem_ord(n2w(int(m.group(1)), to='ordinal'))} ", t)
183
+ t = re.sub(r"(?<![\d.,])(\d{1,3}(?:\.\d{3})+|\d+)\s+([A-Za-zÀ-ú]+)", agree, t)
184
+ t = re.sub(r"\d{1,3}(?:\.\d{3})+(?:,\d+)?|\d+(?:,\d+)?", number, t)
185
+ t = t.replace("&", " e ").replace("@", " arroba ").replace("+", " mais ").replace("=", " igual a ")
186
+ return t
187
+
188
+
189
+ def unit_word(u):
190
+ return UNITS[re.sub(r"\s", "", u.lower())]
191
+
192
+
193
+ def fem_ord(w):
194
+ return " ".join(x[:-1] + "a" if x.endswith("o") else x for x in w.split())
195
+
196
+
197
+ ARCHAIC = [
198
+ (r"ph", "f"), (r"th", "t"), (r"(?<=[a-z])y(?=[a-z])", "i"),
199
+ (r"ll", "l"), (r"mm", "m"), (r"nn", "n"), (r"tt", "t"), (r"pp", "p"), (r"ff", "f"), (r"bb", "b"), (r"dd", "d"),
200
+ (r"gg", "g"), (r"cc(?=[aou])", "c"),
201
+ ]
202
+ WORDS = {"hontem": "ontem", "emquanto": "enquanto", "emfim": "enfim", "sahir": "sair", "cahir": "cair",
203
+ "ás": "às", "á": "à", "sómente": "somente", "egreja": "igreja", "logar": "lugar", "comtudo": "contudo",
204
+ "emfim": "enfim", "inglez": "inglês", "portuguez": "português", "francez": "francês", "mez": "mês",
205
+ "vez": "vez", "pez": "pés", "fóra": "fora", "côr": "cor", "poz": "pôs", "poz-se": "pôs-se", "fez-se": "fez-se", "ella": "ela", "elle": "ele", "elles": "eles",
206
+ "ellas": "elas", "n'um": "num", "n'uma": "numa", "d'isto": "disto", "d'isso": "disso", "d'aqui": "daqui",
207
+ "d'ali": "dali", "d'elle": "dele", "d'ella": "dela", "d'esse": "desse", "d'essa": "dessa", "d'este": "deste",
208
+ "d'esta": "desta", "d'aquelle": "daquele", "d'aquella": "daquela"}
209
+
210
+
211
+ def modernize(text):
212
+ """Pre-reform spellings -> modern pt-BR, word by word (training text only)."""
213
+ def fix(w):
214
+ lw = w.lower()
215
+ core = lw.strip(PUNCT + '"')
216
+ if core in WORDS:
217
+ return lw.replace(core, WORDS[core])
218
+ for a, b in ARCHAIC:
219
+ lw = re.sub(a, b, lw)
220
+ return lw
221
+ return " ".join(fix(w) for w in text.split())
222
+
223
+
224
+ def alphabet(text):
225
+ text = unicodedata.normalize("NFC", text.lower())
226
+ out = []
227
+ for ch in text:
228
+ if ch in LETTERS or ch in PUNCT or ch == " ":
229
+ out.append(ch)
230
+ elif ch in "\"()[]{}":
231
+ out.append(" ")
232
+ else:
233
+ base = "".join(c for c in unicodedata.normalize("NFKD", ch) if not unicodedata.combining(c))
234
+ out.append(base if base and all(c in LETTERS for c in base) else " ")
235
+ t = re.sub(r"\s+", " ", "".join(out))
236
+ t = re.sub(r"\s+([.,?!;:])", r"\1", t) # no space before punctuation
237
+ t = re.sub(r"([.,?!;:])\1+", r"\1", t).replace("..", ".")
238
+ return t.strip(" -'")
239
+
240
+
241
+ def normalize(text, archaic=False):
242
+ try:
243
+ text = spoken(text)
244
+ except Exception: # keep the words rather than lose the sentence
245
+ pass
246
+ if archaic:
247
+ text = modernize(text)
248
+ return alphabet(text)
249
+
250
+
251
+ def encode(text):
252
+ stoi = {c: i for i, c in enumerate(VOCAB)}
253
+ return [stoi.get(c, 1) for c in text]
254
+
255
+
256
+ if __name__ == "__main__":
257
+ tests = [
258
+ "Olá! O pedido custa R$ 1.250,50 e chega em 15/10/2026 às 14:30.",
259
+ "A inflação foi de 4,5% em 2025; o 1º lugar ficou com a 2ª colocada.",
260
+ "O Sr. Silva mora na Av. Paulista, nº 1000, a 12 km daqui.",
261
+ "Ligue para 11987654321 às 9h. Temperatura: 23°C.",
262
+ "Ella abriu a janella e poz-se a mirar o theatro emquanto sahia.",
263
+ "Você vem amanhã? Sim, chego às 2h com 3 amigos.",
264
+ ]
265
+ for t in tests:
266
+ print(t, "\n ->", normalize(t, archaic=True), "\n")
rapido/model.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Rápido acoustic model: Brazilian Portuguese letters in, 64-dim latents at 25 Hz out (flow matching)."""
2
+ import torch
3
+ import torch.nn as nn
4
+
5
+ from ._dit import Acoustic, rope
6
+
7
+
8
+ def word_layout(text):
9
+ """Letter -> word index and the first letter of that word (a word owns its trailing space)."""
10
+ starts = [i for i, ch in enumerate(text) if ch != " " and (i == 0 or text[i - 1] == " ")] or [0]
11
+ bounds = [0] + starts[1:] + [len(text)]
12
+ cw, wstart = [], []
13
+ for w, (a, b) in enumerate(zip(bounds[:-1], bounds[1:])):
14
+ cw += [w] * (b - a)
15
+ wstart += [a] * (b - a)
16
+ return cw, wstart
17
+
18
+
19
+ def frame_layout(counts):
20
+ """Word frame counts -> each frame's word and its position inside that word."""
21
+ fw = torch.repeat_interleave(torch.arange(len(counts)), counts)
22
+ start = (counts.cumsum(0) - counts)[fw]
23
+ fp = ((torch.arange(len(fw)) - start).double() / counts[fw].double()).float()
24
+ return fw, fp
25
+
26
+
27
+ class RapidoModel(nn.Module):
28
+ def __init__(self, cfg, vocab, n_spk):
29
+ super().__init__()
30
+ self.cfg = cfg
31
+ self.net = Acoustic(cfg, vocab, cfg.get("shared_ada", True))
32
+ self.spk = nn.Embedding(n_spk, cfg["d"])
33
+ self.register_buffer("lat_mean", torch.zeros(cfg["latent_dim"]))
34
+ self.register_buffer("lat_std", torch.ones(cfg["latent_dim"]))
35
+
36
+ def _condition(self, ids, mask, cw, wstart, fw, fp, spk):
37
+ h, dur = self.net.text_stage(ids, mask)
38
+ c = dur.clamp(min=1e-4) * mask
39
+ done = c.cumsum(-1)
40
+ word = cw.clamp(min=0)
41
+ total = torch.zeros_like(c).scatter_add_(1, word, c).gather(1, word)
42
+ cp = ((done - (done - c).gather(1, wstart) - 0.5 * c) / total.clamp(min=1e-8)).clamp(0.0, 1.0) * mask
43
+ return self.net.aligner(h, cw, cp, fw, fp, mask, cw.shape[1]) + self.spk(spk)[:, None]
44
+
45
+ def _velocity(self, x, cond, t, fmask):
46
+ cos, sin = rope(torch.arange(x.shape[1], device=x.device), self.net.dh)
47
+ return self.net.backbone(x, cond, t, fmask, cos, sin)
48
+
49
+ @torch.inference_mode()
50
+ def generate(self, ids, steps=8, guidance=4.0, speed=1.0, seed=None, speaker=0):
51
+ """ids: letter ids [L] -> latents [T, 64] (midpoint ODE + classifier-free guidance)."""
52
+ dev = ids.device
53
+ text_ids = ids[None]
54
+ cw, ws = word_layout_from_ids(text_ids, self.net.stoi)
55
+ mask = torch.ones_like(text_ids, dtype=torch.bool)
56
+ _, dur = self.net.text_stage(text_ids, mask)
57
+ dur = dur / speed
58
+ counts = torch.zeros(1, int(cw.max()) + 1, device=dev).scatter_add_(1, cw, dur).round().clamp(1, 250).long()[0]
59
+ fw, fp = frame_layout(counts.cpu())
60
+ fw, fp = fw[None].to(dev), fp[None].to(dev)
61
+ fmask = torch.ones_like(fw, dtype=torch.bool)
62
+ cond = self._condition(text_ids, mask, cw, ws, fw, fp, torch.tensor([speaker], device=dev))
63
+ g = torch.Generator(device=dev)
64
+ g.manual_seed(seed if seed is not None else torch.seed() % (2 ** 31))
65
+ x = torch.randn(1, fw.shape[1], self.cfg["latent_dim"], device=dev, generator=g)
66
+
67
+ def v(x, t):
68
+ tt = torch.full((1,), t, device=dev)
69
+ vc = self._velocity(x, cond, tt, fmask)
70
+ if guidance == 1.0:
71
+ return vc
72
+ vu = self._velocity(x, torch.zeros_like(cond), tt, fmask)
73
+ return vu + guidance * (vc - vu)
74
+
75
+ ts = torch.linspace(0, 1, steps + 1).tolist()
76
+ for a, b in zip(ts[:-1], ts[1:]):
77
+ x = x + (b - a) * v(x + (b - a) / 2 * v(x, a), (a + b) / 2)
78
+ return (x[0] * self.lat_std + self.lat_mean).float()
79
+
80
+
81
+ def word_layout_from_ids(ids, stoi):
82
+ """Word layout from letter ids (the space id marks word ends)."""
83
+ space = stoi[" "]
84
+ row = ids[0].tolist()
85
+ text = "".join(" " if i == space else "x" for i in row)
86
+ cw, ws = word_layout(text)
87
+ return torch.tensor([cw], device=ids.device), torch.tensor([ws], device=ids.device)
requirements.txt CHANGED
@@ -50,3 +50,7 @@ imageio-ffmpeg==0.6.0
50
  # dependência extra.
51
  boto3==1.35.90
52
  requests>=2.32
 
 
 
 
 
50
  # dependência extra.
51
  boto3==1.35.90
52
  requests>=2.32
53
+
54
+ # A voz do Live (Rápido TTS): números, datas e dinheiro por extenso em pt-BR.
55
+ num2words
56
+ pydantic