Spaces:
Running on Zero
Running on Zero
Upload folder using huggingface_hub
Browse files- LICENSE-rapido +201 -0
- __pycache__/armazenamento.cpython-314.pyc +0 -0
- __pycache__/nucleo.cpython-314.pyc +0 -0
- app.py +46 -0
- modelos/__pycache__/__init__.cpython-314.pyc +0 -0
- modelos/__pycache__/ace_step.cpython-314.pyc +0 -0
- modelos/__pycache__/base.cpython-314.pyc +0 -0
- modelos/__pycache__/escolha.cpython-314.pyc +0 -0
- modelos/__pycache__/qwen3_tts.cpython-314.pyc +0 -0
- modelos/__pycache__/qwen_image.cpython-314.pyc +0 -0
- modelos/__pycache__/wan22.cpython-314.pyc +0 -0
- modelos/wan22.py +7 -5
- rapido/__init__.py +5 -0
- rapido/__main__.py +30 -0
- rapido/_dit.py +248 -0
- rapido/_vae.py +581 -0
- rapido/engine.py +118 -0
- rapido/frontend.py +266 -0
- rapido/model.py +87 -0
- requirements.txt +4 -0
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 |
-
|
| 167 |
-
|
| 168 |
-
|
|
|
|
|
|
|
| 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 >
|
| 218 |
-
p["duration"] =
|
| 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
|