Initial release: Scrappy Voice Family — four voices in one checkpoint
Browse files- .gitattributes +4 -0
- .gitignore +3 -0
- CITATION.cff +11 -0
- LICENSE +201 -0
- README.md +202 -0
- THIRD_PARTY_NOTICES.md +36 -0
- UPSTREAM-README.md +398 -0
- config.json +71 -0
- examples/basic.py +16 -0
- examples/long_text.py +14 -0
- examples/voices.py +21 -0
- inference.py +362 -0
- inflect_nano_v2_frontend.py +395 -0
- inflect_vits_frontend.py +67 -0
- model.pth +3 -0
- requirements-tested.txt +12 -0
- requirements.txt +8 -0
- runtime/attentions.py +303 -0
- runtime/commons.py +161 -0
- runtime/inflect_alias_free.py +143 -0
- runtime/models.py +571 -0
- runtime/modules.py +390 -0
- runtime/monotonic_align.py +4 -0
- runtime/text/LICENSE +19 -0
- runtime/text/__init__.py +54 -0
- runtime/text/cleaners.py +100 -0
- runtime/text/symbols.py +16 -0
- runtime/transforms.py +193 -0
- runtime/utils.py +256 -0
- samples/clara_intro.wav +3 -0
- samples/pip_intro.wav +3 -0
- samples/scrappy_intro.wav +3 -0
- samples/silas_intro.wav +3 -0
- speakers.json +27 -0
- third_party/ALIAS_FREE_TORCH_LICENSE.txt +201 -0
- third_party/BIGVGAN_LICENSE.txt +21 -0
- third_party/VITS_LICENSE.txt +21 -0
- trainer/eval_candidate_ms.py +87 -0
- trainer/prep_filelists_ms.py +121 -0
- trainer/train_ms.py +379 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
samples/clara_intro.wav filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
samples/pip_intro.wav filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
samples/scrappy_intro.wav filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
samples/silas_intro.wav filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__
|
| 2 |
+
*.pyc
|
| 3 |
+
.DS_Store
|
CITATION.cff
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
cff-version: 1.2.0
|
| 2 |
+
message: If you use this model, please cite it.
|
| 3 |
+
title: 'Inflect-Micro-v2'
|
| 4 |
+
type: software
|
| 5 |
+
authors:
|
| 6 |
+
- family-names: Song
|
| 7 |
+
given-names: Owen
|
| 8 |
+
version: 2.0.0
|
| 9 |
+
date-released: 2026-07-24
|
| 10 |
+
url: 'https://huggingface.co/owensong/Inflect-Micro-v2'
|
| 11 |
+
license: Apache-2.0
|
LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Apache License
|
| 2 |
+
Version 2.0, January 2004
|
| 3 |
+
http://www.apache.org/licenses/
|
| 4 |
+
|
| 5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 6 |
+
|
| 7 |
+
1. Definitions.
|
| 8 |
+
|
| 9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 11 |
+
|
| 12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 13 |
+
the copyright owner that is granting the License.
|
| 14 |
+
|
| 15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 16 |
+
other entities that control, are controlled by, or are under common
|
| 17 |
+
control with that entity. For the purposes of this definition,
|
| 18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 19 |
+
direction or management of such entity, whether by contract or
|
| 20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 22 |
+
|
| 23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 24 |
+
exercising permissions granted by this License.
|
| 25 |
+
|
| 26 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 27 |
+
including but not limited to software source code, documentation
|
| 28 |
+
source, and configuration files.
|
| 29 |
+
|
| 30 |
+
"Object" form shall mean any form resulting from mechanical
|
| 31 |
+
transformation or translation of a Source form, including but
|
| 32 |
+
not limited to compiled object code, generated documentation,
|
| 33 |
+
and conversions to other media types.
|
| 34 |
+
|
| 35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 36 |
+
Object form, made available under the License, as indicated by a
|
| 37 |
+
copyright notice that is included in or attached to the work
|
| 38 |
+
(an example is provided in the Appendix below).
|
| 39 |
+
|
| 40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 41 |
+
form, that is based on (or derived from) the Work and for which the
|
| 42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 44 |
+
of this License, Derivative Works shall not include works that remain
|
| 45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 46 |
+
the Work and Derivative Works thereof.
|
| 47 |
+
|
| 48 |
+
"Contribution" shall mean any work of authorship, including
|
| 49 |
+
the original version of the Work and any modifications or additions
|
| 50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 54 |
+
means any form of electronic, verbal, or written communication sent
|
| 55 |
+
to the Licensor or its representatives, including but not limited to
|
| 56 |
+
communication on electronic mailing lists, source code control systems,
|
| 57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 59 |
+
excluding communication that is conspicuously marked or otherwise
|
| 60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 61 |
+
|
| 62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 64 |
+
subsequently incorporated within the Work.
|
| 65 |
+
|
| 66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 71 |
+
Work and such Derivative Works in Source or Object form.
|
| 72 |
+
|
| 73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 76 |
+
(except as stated in this section) patent license to make, have made,
|
| 77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 78 |
+
where such license applies only to those patent claims licensable
|
| 79 |
+
by such Contributor that are necessarily infringed by their
|
| 80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 82 |
+
institute patent litigation against any entity (including a
|
| 83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 84 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 85 |
+
or contributory patent infringement, then any patent licenses
|
| 86 |
+
granted to You under this License for that Work shall terminate
|
| 87 |
+
as of the date such litigation is filed.
|
| 88 |
+
|
| 89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 90 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 91 |
+
modifications, and in Source or Object form, provided that You
|
| 92 |
+
meet the following conditions:
|
| 93 |
+
|
| 94 |
+
(a) You must give any other recipients of the Work or
|
| 95 |
+
Derivative Works a copy of this License; and
|
| 96 |
+
|
| 97 |
+
(b) You must cause any modified files to carry prominent notices
|
| 98 |
+
stating that You changed the files; and
|
| 99 |
+
|
| 100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 101 |
+
that You distribute, all copyright, patent, trademark, and
|
| 102 |
+
attribution notices from the Source form of the Work,
|
| 103 |
+
excluding those notices that do not pertain to any part of
|
| 104 |
+
the Derivative Works; and
|
| 105 |
+
|
| 106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 107 |
+
distribution, then any Derivative Works that You distribute must
|
| 108 |
+
include a readable copy of the attribution notices contained
|
| 109 |
+
within such NOTICE file, excluding those notices that do not
|
| 110 |
+
pertain to any part of the Derivative Works, in at least one
|
| 111 |
+
of the following places: within a NOTICE text file distributed
|
| 112 |
+
as part of the Derivative Works; within the Source form or
|
| 113 |
+
documentation, if provided along with the Derivative Works; or,
|
| 114 |
+
within a display generated by the Derivative Works, if and
|
| 115 |
+
wherever such third-party notices normally appear. The contents
|
| 116 |
+
of the NOTICE file are for informational purposes only and
|
| 117 |
+
do not modify the License. You may add Your own attribution
|
| 118 |
+
notices within Derivative Works that You distribute, alongside
|
| 119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 120 |
+
that such additional attribution notices cannot be construed
|
| 121 |
+
as modifying the License.
|
| 122 |
+
|
| 123 |
+
You may add Your own copyright statement to Your modifications and
|
| 124 |
+
may provide additional or different license terms and conditions
|
| 125 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 126 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 127 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 128 |
+
the conditions stated in this License.
|
| 129 |
+
|
| 130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 132 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 133 |
+
this License, without any additional terms or conditions.
|
| 134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 135 |
+
the terms of any separate license agreement you may have executed
|
| 136 |
+
with Licensor regarding such Contributions.
|
| 137 |
+
|
| 138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 140 |
+
except as required for reasonable and customary use in describing the
|
| 141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 142 |
+
|
| 143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 144 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 147 |
+
implied, including, without limitation, any warranties or conditions
|
| 148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 150 |
+
appropriateness of using or redistributing the Work and assume any
|
| 151 |
+
risks associated with Your exercise of permissions under this License.
|
| 152 |
+
|
| 153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 154 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 155 |
+
unless required by applicable law (such as deliberate and grossly
|
| 156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 157 |
+
liable to You for damages, including any direct, indirect, special,
|
| 158 |
+
incidental, or consequential damages of any character arising as a
|
| 159 |
+
result of this License or out of the use or inability to use the
|
| 160 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 161 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 162 |
+
other commercial damages or losses), even if such Contributor
|
| 163 |
+
has been advised of the possibility of such damages.
|
| 164 |
+
|
| 165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 168 |
+
or other liability obligations and/or rights consistent with this
|
| 169 |
+
License. However, in accepting such obligations, You may act only
|
| 170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 171 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 172 |
+
defend, and hold each Contributor harmless for any liability
|
| 173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 174 |
+
of your accepting any such warranty or additional liability.
|
| 175 |
+
|
| 176 |
+
END OF TERMS AND CONDITIONS
|
| 177 |
+
|
| 178 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 179 |
+
|
| 180 |
+
To apply the Apache License to your work, attach the following
|
| 181 |
+
boilerplate notice, with the fields enclosed by brackets "{}"
|
| 182 |
+
replaced with your own identifying information. (Don't include
|
| 183 |
+
the brackets.) The text should be enclosed in the appropriate
|
| 184 |
+
comment syntax for the file format. We also recommend that a
|
| 185 |
+
file or class name and description of purpose be included on the
|
| 186 |
+
same "printed page" as the copyright notice for easier
|
| 187 |
+
identification within third-party archives.
|
| 188 |
+
|
| 189 |
+
Copyright {yyyy} {name of copyright owner}
|
| 190 |
+
|
| 191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 192 |
+
you may not use this file except in compliance with the License.
|
| 193 |
+
You may obtain a copy of the License at
|
| 194 |
+
|
| 195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 196 |
+
|
| 197 |
+
Unless required by applicable law or agreed to in writing, software
|
| 198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 200 |
+
See the License for the specific language governing permissions and
|
| 201 |
+
limitations under the License.
|
README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
language: en
|
| 4 |
+
pipeline_tag: text-to-speech
|
| 5 |
+
tags:
|
| 6 |
+
- text-to-speech
|
| 7 |
+
- speech-synthesis
|
| 8 |
+
- vits
|
| 9 |
+
- multi-speaker
|
| 10 |
+
- multi-voice
|
| 11 |
+
- voice-blending
|
| 12 |
+
- cpu
|
| 13 |
+
- edge-ai
|
| 14 |
+
- small-model
|
| 15 |
+
- 24khz
|
| 16 |
+
- voice-distillation
|
| 17 |
+
base_model: owensong/Inflect-Micro-v2
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
# Scrappy Voice Family — four voices in one 10M-param CPU model
|
| 21 |
+
|
| 22 |
+
Four distinct narrators — **Scrappy**, **Clara**, **Silas**, and **Pip** — sharing a single
|
| 23 |
+
**10.26M-parameter / 41MB** checkpoint that runs **on a plain CPU**, no GPU, no cloud.
|
| 24 |
+
It's a multi-speaker fine-tune of
|
| 25 |
+
[owensong/Inflect-Micro-v2](https://huggingface.co/owensong/Inflect-Micro-v2) (Apache-2.0),
|
| 26 |
+
warm-started from our first voice and trained on all four corpora pooled together.
|
| 27 |
+
|
| 28 |
+
Listen: [`samples/scrappy_intro.wav`](samples/scrappy_intro.wav) ·
|
| 29 |
+
[`samples/clara_intro.wav`](samples/clara_intro.wav) ·
|
| 30 |
+
[`samples/silas_intro.wav`](samples/silas_intro.wav) ·
|
| 31 |
+
[`samples/pip_intro.wav`](samples/pip_intro.wav) — each voice introducing itself, all four
|
| 32 |
+
generated on a desktop CPU by *this one checkpoint*.
|
| 33 |
+
|
| 34 |
+
| Voice | id | Character |
|
| 35 |
+
|---|---|---|
|
| 36 |
+
| **Scrappy** | 0 | Warm, expressive narrator — the original ScrappyLabs voice |
|
| 37 |
+
| **Clara** | 1 | Clear professional female narrator, broadcast polish, neutral American with a warm undertone |
|
| 38 |
+
| **Silas** | 2 | Deep, authoritative older male — gravelly, slow, movie-trailer gravitas |
|
| 39 |
+
| **Pip** | 3 | Bright, energetic young American female — quick, playful, podcast-host diction |
|
| 40 |
+
|
| 41 |
+
**Get it:** [🤗 HuggingFace](https://huggingface.co/scrappylabsai/scrappy-voice-family-1)
|
| 42 |
+
(canonical weights) · Story: [scrappylabs.ai/scrappy-voice](https://scrappylabs.ai/scrappy-voice)
|
| 43 |
+
|
| 44 |
+
**🎮 Hear the family's first voice live:**
|
| 45 |
+
[interactive demo on Spaces](https://huggingface.co/spaces/scrappylabsai/scrappy-voice-tts)
|
| 46 |
+
— built by the Hugging Face team on free ZeroGPU. Thanks, HF 🤗
|
| 47 |
+
|
| 48 |
+
## Why a family instead of four models
|
| 49 |
+
|
| 50 |
+
We shipped [scrappy-voice-1](https://huggingface.co/scrappylabsai/scrappy-voice-1) as a
|
| 51 |
+
single-voice distill, then built three more voices the same way. The obvious next question:
|
| 52 |
+
does one shared model hold four identities without smearing them together?
|
| 53 |
+
|
| 54 |
+
It does — and pooling the data made it *better*, not worse.
|
| 55 |
+
|
| 56 |
+
Same recipe as the solo voices, end to end:
|
| 57 |
+
|
| 58 |
+
1. **Teacher renders each corpus.** A commercial cloud TTS narrator voice generated ~4,400
|
| 59 |
+
short clips per voice (~5.5h @ 24kHz each) from a text corpus we control — so every
|
| 60 |
+
transcript is known by construction.
|
| 61 |
+
2. **An ASR gate cleans it.** Every clip is round-tripped through speech recognition and
|
| 62 |
+
scored against its transcript (≥0.85 word overlap required), alongside signal checks for
|
| 63 |
+
clipping, silence and duration. Pass rates: Clara 99.4%, Silas 99.3%, Pip 98.5%
|
| 64 |
+
(Scrappy's original corpus: 98.6%). This is the check that catches audio which says the
|
| 65 |
+
wrong thing beautifully.
|
| 66 |
+
3. **Warm-start, multi-speaker.** `n_speakers=4`, `gin_channels=256`. Rather than start from
|
| 67 |
+
the stock base, we warm-started from Scrappy's own 50k checkpoint — chosen by a measured
|
| 68 |
+
A/B, not vibes: **26.4 vs 28.2 final mel loss at 12k steps**, with ASR intelligibility at
|
| 69 |
+
parity. An already-distilled voice is a better launch pad than the stock release.
|
| 70 |
+
4. **75,000 steps, batch 24, LR 5e-5, fp32**, over **16,546 pooled training clips**
|
| 71 |
+
(plus 868 held out) across the four voices.
|
| 72 |
+
|
| 73 |
+
**Final mel loss 18.4–18.9** — comfortably below the Clara (19.4) and Pip (21.0) solo runs
|
| 74 |
+
and level with Silas's 18.1, the best of the three. Four voices sharing one model landed at
|
| 75 |
+
roughly the quality of the best single-voice run rather than paying for the company, which
|
| 76 |
+
is the pleasant surprise of the whole exercise. Multi-speaker conditioning also costs
|
| 77 |
+
essentially nothing at train time: 4.36 steps/s, same as single-speaker.
|
| 78 |
+
|
| 79 |
+
Held-out intelligibility on the family model (ASR word-overlap against the intro script):
|
| 80 |
+
Clara **1.00**, Pip **1.00**, Scrappy **0.96**, Silas **0.92** — mean **0.97**. The solo
|
| 81 |
+
models score 1.00 for both Silas and Clara, so a little per-voice sharpness is the price of
|
| 82 |
+
sharing; see [Honest limitations](#honest-limitations).
|
| 83 |
+
|
| 84 |
+
## Usage
|
| 85 |
+
|
| 86 |
+
Drop-in for the packaged runtime, with two additions: `voice=` and `blend=`.
|
| 87 |
+
|
| 88 |
+
```python
|
| 89 |
+
from inference import InflectTTS
|
| 90 |
+
|
| 91 |
+
tts = InflectTTS(model_dir=".", device="cpu")
|
| 92 |
+
|
| 93 |
+
print(tts.voices) # ['scrappy', 'clara', 'silas', 'pip'] (ordered by speaker id)
|
| 94 |
+
|
| 95 |
+
# pick a voice by name (case-insensitive) or by id
|
| 96 |
+
tts.save("Clear, professional, and easy to listen to.", "clara.wav", voice="clara", seed=7)
|
| 97 |
+
tts.save("Low, slow, and built for weight.", "silas.wav", voice=2)
|
| 98 |
+
|
| 99 |
+
# blend two or more voices — weights are normalized for you
|
| 100 |
+
tts.save("Somewhere between the two of them.", "morph.wav",
|
| 101 |
+
blend={"clara": 0.7, "silas": 0.3}, seed=7)
|
| 102 |
+
|
| 103 |
+
sample_rate, audio = tts.synthesize("Returns numpy, if you'd rather.", voice="pip")
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
```bash
|
| 107 |
+
python inference.py --model-dir . --device cpu --voice clara \
|
| 108 |
+
--text "Hello from the family." --output out.wav
|
| 109 |
+
|
| 110 |
+
python inference.py --blend "clara=0.7, silas=0.3" \
|
| 111 |
+
--text "And this is a blend." --output morph.wav
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
`voice` accepts a name, an int speaker id, or a digit string; omit it and you get voice 0
|
| 115 |
+
(Scrappy). `blend` takes a `{voice: weight}` dict, normalizes the weights to sum to 1, and
|
| 116 |
+
overrides `voice` when both are given. Everything else is unchanged from upstream: English
|
| 117 |
+
only, deterministic seeds, punctuation-aware long-form chunking, `speed` 0.5–2.0,
|
| 118 |
+
`variation` 0.0–1.0. Write numbers out as words for best results.
|
| 119 |
+
|
| 120 |
+
### Why four voices don't cost four models
|
| 121 |
+
|
| 122 |
+
Each voice is **one 256-float speaker-embedding row — about 1KB**. The multi-speaker
|
| 123 |
+
machinery (the conditioning pathways through the flow, decoder and posterior encoder) is a
|
| 124 |
+
one-time ~0.9M-parameter cost over the single-voice model; after that, voice number five
|
| 125 |
+
would add roughly a kilobyte, not another 37MB. That's the whole argument for a family:
|
| 126 |
+
you pay for the plumbing once.
|
| 127 |
+
|
| 128 |
+
Blending works because those embedding rows live in a continuous space. A weighted average
|
| 129 |
+
of two rows is a valid conditioning vector, so `{"clara": 0.7, "silas": 0.3}` renders a voice
|
| 130 |
+
that genuinely sits between them rather than crossfading two renders. Weights are
|
| 131 |
+
normalized, duplicates that resolve to the same voice are summed, and negative,
|
| 132 |
+
non-finite, all-zero or unknown-name inputs are rejected outright.
|
| 133 |
+
|
| 134 |
+
## Honest limitations
|
| 135 |
+
|
| 136 |
+
- **Blends near the middle can get uncanny.** Only the four anchor voices were trained. The
|
| 137 |
+
space between them is interpolation, not supervision — light blends (say 80/20) usually
|
| 138 |
+
sound like a plausible person; 50/50 blends of very different voices (Silas and Pip, for
|
| 139 |
+
instance) can land somewhere no human throat goes. Audition before you ship one.
|
| 140 |
+
- **Sharing costs a little per-voice sharpness.** Silas scores 0.92 held-out intelligibility
|
| 141 |
+
in the family model vs 1.00 solo. If you need one voice at maximum fidelity and don't care
|
| 142 |
+
about the others, the [solo models](#the-family) still win narrowly.
|
| 143 |
+
- **Prosody is where distillation loses the most.** Timbre and identity transfer well; the
|
| 144 |
+
teacher's long-range timing instincts — dramatic pauses, phrase-level planning — get
|
| 145 |
+
averaged. The duration predictor is the smallest organ in a VITS, so expect a flatter read
|
| 146 |
+
than the source voices.
|
| 147 |
+
- **espeak-ng mispronounces "Silas"** as /siːləz/. Spell it `Sighlus` in synthesis input to
|
| 148 |
+
get /saɪləs/. A frontend quirk, not a model one — and a good reminder to spot-check how the
|
| 149 |
+
phonemizer handles proper nouns.
|
| 150 |
+
- Slight texture softness vs. a large vocoder remains at close listening.
|
| 151 |
+
- **English only.** Everything upstream says about language coverage and biases applies.
|
| 152 |
+
- **Not a cloning tool.** These are synthetic personas built from a teacher we're entitled to
|
| 153 |
+
use. Don't point this pipeline at a real person's voice without their explicit consent.
|
| 154 |
+
|
| 155 |
+
## The family
|
| 156 |
+
|
| 157 |
+
| Model | What it is |
|
| 158 |
+
|---|---|
|
| 159 |
+
| [**scrappy-voice-family-1**](https://huggingface.co/scrappylabsai/scrappy-voice-family-1) | ← you are here. All four voices + blending, one checkpoint |
|
| 160 |
+
| [scrappy-voice-1](https://huggingface.co/scrappylabsai/scrappy-voice-1) | Scrappy, solo — the original, and this model's warm start |
|
| 161 |
+
| [clara-voice-1](https://huggingface.co/scrappylabsai/clara-voice-1) | Clara, solo |
|
| 162 |
+
| [silas-voice-1](https://huggingface.co/scrappylabsai/silas-voice-1) | Silas, solo |
|
| 163 |
+
| [pip-voice-1](https://huggingface.co/scrappylabsai/pip-voice-1) | Pip, solo |
|
| 164 |
+
|
| 165 |
+
Use the family model unless you specifically want one voice at its sharpest, or want the
|
| 166 |
+
smaller single-voice checkpoint.
|
| 167 |
+
|
| 168 |
+
## Train your own family (`trainer/`)
|
| 169 |
+
|
| 170 |
+
The `trainer/` directory holds the multi-speaker version of the stack upstream deliberately
|
| 171 |
+
omits:
|
| 172 |
+
|
| 173 |
+
- `prep_filelists_ms.py` — phonemizes transcripts with the model's own frontend, validates
|
| 174 |
+
every symbol against the release inventory, and emits speaker-id-tagged filelists plus the
|
| 175 |
+
`speakers.json` name→id map.
|
| 176 |
+
- `train_ms.py` — the full loop with speaker conditioning: VITS losses, torchaudio mel
|
| 177 |
+
transforms (slaney/slaney — no librosa dependency), warm-start loading that grows a
|
| 178 |
+
single-speaker checkpoint into a multi-speaker one, and drop-in candidate export.
|
| 179 |
+
- `eval_candidate_ms.py` — renders fixed prompts from any candidate, per voice, for A/B
|
| 180 |
+
listening.
|
| 181 |
+
|
| 182 |
+
You also need the cython monotonic-alignment kernel from the
|
| 183 |
+
[canonical VITS repo](https://github.com/jaywalnut310/vits) (the release stubs it out) —
|
| 184 |
+
build it and drop the package into `runtime/`. Data contract per voice: mono 24kHz clips,
|
| 185 |
+
verified transcripts, 1–5+ hours. **Gate every corpus with ASR round-trips**, and warm-start
|
| 186 |
+
from a voice you've already distilled rather than the stock base — we measured that, and it
|
| 187 |
+
wins.
|
| 188 |
+
|
| 189 |
+
## Provenance & takedown
|
| 190 |
+
|
| 191 |
+
All training audio was synthesized by a commercial cloud TTS narrator voice — synthetic
|
| 192 |
+
personas, no real person's voice was cloned. If you're a rights holder with a concern, open
|
| 193 |
+
a discussion on this repo and we'll respond promptly.
|
| 194 |
+
|
| 195 |
+
## Credits
|
| 196 |
+
|
| 197 |
+
- **[owensong/Inflect-Micro-v2](https://huggingface.co/owensong/Inflect-Micro-v2)** —
|
| 198 |
+
base model, runtime, and an unusually honest set of docs (Apache-2.0)
|
| 199 |
+
- [VITS](https://github.com/jaywalnut310/vits) (MIT) — architecture lineage + alignment kernel
|
| 200 |
+
- **The Hugging Face team** — built and gifted us the first demo Space on free ZeroGPU 🤗
|
| 201 |
+
- Built by [ScrappyLabs](https://scrappylabs.ai) — we do this kind of thing to stay sharp.
|
| 202 |
+
Bring your own AI; we keep it wrangled.
|
THIRD_PARTY_NOTICES.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Third-Party Notices
|
| 2 |
+
|
| 3 |
+
Inflect v2 includes or adapts portions of the following open-source projects.
|
| 4 |
+
Their licenses apply to the corresponding portions; the rest of the Inflect v2
|
| 5 |
+
package is provided under the root Apache-2.0 license.
|
| 6 |
+
|
| 7 |
+
## VITS
|
| 8 |
+
|
| 9 |
+
- Project: VITS (`jaywalnut310/vits`)
|
| 10 |
+
- Copyright: Copyright (c) 2021 Jaehyeon Kim
|
| 11 |
+
- License: MIT
|
| 12 |
+
- Packaged license: `third_party/VITS_LICENSE.txt`
|
| 13 |
+
|
| 14 |
+
The compact model architecture and several inference runtime modules derive from
|
| 15 |
+
VITS. The text frontend also retains its original Keith Ito MIT license at
|
| 16 |
+
`runtime/text/LICENSE`.
|
| 17 |
+
|
| 18 |
+
## BigVGAN
|
| 19 |
+
|
| 20 |
+
- Project: BigVGAN (`NVIDIA/BigVGAN`)
|
| 21 |
+
- Copyright: Copyright (c) 2024 NVIDIA CORPORATION
|
| 22 |
+
- License: MIT
|
| 23 |
+
- Packaged license: `third_party/BIGVGAN_LICENSE.txt`
|
| 24 |
+
|
| 25 |
+
The lightweight alias-free waveform activation implementation derives from
|
| 26 |
+
BigVGAN's alias-free design and was adapted for this compact runtime.
|
| 27 |
+
|
| 28 |
+
## alias-free-torch
|
| 29 |
+
|
| 30 |
+
- Project: `alias-free-torch` (`junjun3518/alias-free-torch`)
|
| 31 |
+
- License: Apache License 2.0
|
| 32 |
+
- Packaged license: `third_party/ALIAS_FREE_TORCH_LICENSE.txt`
|
| 33 |
+
|
| 34 |
+
The anti-aliased activation resampling design used by the compact waveform
|
| 35 |
+
runtime includes concepts and adapted implementation structure from
|
| 36 |
+
`alias-free-torch`.
|
UPSTREAM-README.md
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
pipeline_tag: text-to-speech
|
| 6 |
+
model_name: Inflect-Micro-v2
|
| 7 |
+
metrics:
|
| 8 |
+
- wer
|
| 9 |
+
tags:
|
| 10 |
+
- text-to-speech
|
| 11 |
+
- speech-synthesis
|
| 12 |
+
- local-tts
|
| 13 |
+
- cpu
|
| 14 |
+
- edge-ai
|
| 15 |
+
- small-model
|
| 16 |
+
- base-model
|
| 17 |
+
- pytorch
|
| 18 |
+
- vits
|
| 19 |
+
- 24khz
|
| 20 |
+
thumbnail: assets/inflect-v2-repository-hero.png
|
| 21 |
+
inference: false
|
| 22 |
+
---
|
| 23 |
+
|
| 24 |
+

|
| 25 |
+
|
| 26 |
+
<h1 align="center">Inflect-Micro-v2</h1>
|
| 27 |
+
<p align="center"><strong>Complete local text-to-waveform speech synthesis under 10M parameters.</strong><br>
|
| 28 |
+
Fixed-voice English TTS with deterministic seeds, long-text handling, and CPU or CUDA inference.</p>
|
| 29 |
+
|
| 30 |
+
> **A note from Owen**
|
| 31 |
+
>
|
| 32 |
+
> Thanks so much for all the support shown on this project! I built and funded Inflect v2 independently - if this release finds a real audience, I would like to continue the project with a broader v3, which might include things like more langauges, voices, and stability improvements. If the model is useful to you, leaving a like on Hugging Face genuinely helps more people discover it.
|
| 33 |
+
|
| 34 |
+
<p align="center">
|
| 35 |
+
<a href="https://huggingface.co/spaces/owensong/Inflect-v2"><img alt="Live playground" src="https://img.shields.io/badge/Playground-0B2A54?style=for-the-badge&logo=huggingface&logoColor=white"></a>
|
| 36 |
+
<a href="https://github.com/owenawsong/Inflect"><img alt="GitHub" src="https://img.shields.io/badge/GitHub-1769E0?style=for-the-badge&logo=github&logoColor=white"></a>
|
| 37 |
+
<a href="https://huggingface.co/owensong/Inflect-Nano-v2"><img alt="Inflect Nano v2" src="https://img.shields.io/badge/Nano_3.96M-FFFFFF?style=for-the-badge&logo=huggingface&logoColor=1769E0"></a>
|
| 38 |
+
<a href="https://discord.gg/CVJYedvzvp"><img alt="Inflect Discord" src="https://img.shields.io/badge/Discord-1769E0?style=for-the-badge&logo=discord&logoColor=white"></a>
|
| 39 |
+
<a href="https://huggingface.co/owensong/Inflect-Micro-v2/blob/main/docs/EVALUATION.md"><img alt="Benchmarks" src="https://img.shields.io/badge/Benchmarks-0B2A54?style=for-the-badge&logo=chartdotjs&logoColor=white"></a>
|
| 40 |
+
</p>
|
| 41 |
+
|
| 42 |
+
<p align="center"><strong>9,356,513 deployable parameters</strong> · <strong>37.53 MB FP32</strong> · <strong>24 kHz mono output</strong></p>
|
| 43 |
+
|
| 44 |
+
> **New: public adaptation toolkit**
|
| 45 |
+
>
|
| 46 |
+
> Prepare data, audit train/validation splits, adapt a fixed voice or language, resume training, evaluate checkpoints, and export PyTorch or ONNX packages with the [Inflect adaptation toolkit](https://github.com/owenawsong/Inflect/tree/main/finetune). Adapted quality is experimental and depends on the dataset, frontend, and fluent-speaker evaluation.
|
| 47 |
+
|
| 48 |
+
---
|
| 49 |
+
|
| 50 |
+
Inflect v2 uses one public API across two sizes: **Micro prioritizes quality below 10M parameters; Nano prioritizes footprint below 4M.**
|
| 51 |
+
|
| 52 |
+
<details>
|
| 53 |
+
<summary><strong>Explore this model card</strong></summary>
|
| 54 |
+
|
| 55 |
+
| Start here | Technical detail |
|
| 56 |
+
| --- | --- |
|
| 57 |
+
| [Listen](#listen) | [Architecture](#architecture-and-parameter-budget) |
|
| 58 |
+
| [Evaluation](#evaluation) | [Controls and long text](#controls-determinism-and-long-text) |
|
| 59 |
+
| [Choose Micro or Nano](#choose-the-right-inflect) | [Data and adaptation](#data-voice-and-adaptation-status) |
|
| 60 |
+
| [Run locally](#run-locally) | [Exports and quantization](https://huggingface.co/owensong/Inflect-Micro-v2/blob/main/docs/EXPORTS.md) |
|
| 61 |
+
| [Adapt a voice or language](https://github.com/owenawsong/Inflect/tree/main/finetune) | [Training and export workflow](https://github.com/owenawsong/Inflect/tree/main/finetune/docs/TRAINING.md) |
|
| 62 |
+
| [Package map](#package-map) | [Evaluation and raw protocol](https://huggingface.co/owensong/Inflect-Micro-v2/blob/main/docs/EVALUATION.md) |
|
| 63 |
+
| [Limitations](#limitations) | [Deployment guide](https://huggingface.co/owensong/Inflect-Micro-v2/blob/main/docs/DEPLOYMENT.md) |
|
| 64 |
+
|
| 65 |
+
</details>
|
| 66 |
+
|
| 67 |
+
## Listen
|
| 68 |
+
|
| 69 |
+
These are held-out text generations, not reconstructions of training audio. Each transcript is shown exactly as passed to the public frontend.
|
| 70 |
+
|
| 71 |
+
| Test | Exact transcript | Generated audio |
|
| 72 |
+
| --- | --- | --- |
|
| 73 |
+
| **Conversational** | It wasn't until later that I realized what had actually happened. | <audio controls preload="metadata" src="https://huggingface.co/owensong/Inflect-Micro-v2/resolve/main/samples/male/conversational.wav"></audio> |
|
| 74 |
+
| **Punctuation** | First, close the window; second, turn off the lamp; finally, lock the door. | <audio controls preload="metadata" src="https://huggingface.co/owensong/Inflect-Micro-v2/resolve/main/samples/male/punctuation.wav"></audio> |
|
| 75 |
+
| **Numbers** | The package weighs twelve point six kilograms and arrived on July twenty-first. | <audio controls preload="metadata" src="https://huggingface.co/owensong/Inflect-Micro-v2/resolve/main/samples/male/numbers.wav"></audio> |
|
| 76 |
+
| **Names and places** | Gwendolyn photographed the eucalyptus trees outside Ljubljana. | <audio controls preload="metadata" src="https://huggingface.co/owensong/Inflect-Micro-v2/resolve/main/samples/male/names_places.wav"></audio> |
|
| 77 |
+
| **Technical** | The system runs on three core components that all have to stay in sync. | <audio controls preload="metadata" src="https://huggingface.co/owensong/Inflect-Micro-v2/resolve/main/samples/male/technical.wav"></audio> |
|
| 78 |
+
|
| 79 |
+
## Evaluation
|
| 80 |
+
|
| 81 |
+
No single metric captures TTS quality. Inflect v2 reports **human preference**, **predicted naturalness**, **multi-ASR intelligibility**, **complete footprint**, and **runtime** separately rather than compressing them into one unverifiable score.
|
| 82 |
+
|
| 83 |
+
| Community preference ↑ | UTMOS22 ↑ | Two-ASR semantic WER ↓ | Complete FP32 weights ↓ | 4-thread CPU throughput ↑ |
|
| 84 |
+
| ---: | ---: | ---: | ---: | ---: |
|
| 85 |
+
| **66.2%** | **4.395** | **3.99%** | **37.53 MB** | **6.28× real-time** |
|
| 86 |
+
|
| 87 |
+
The headline row always refers to **Inflect-Micro-v2**. Detailed competitor results and protocol boundaries are kept visible below.
|
| 88 |
+
|
| 89 |
+
**Comparison set.** Results include [KittenTTS Nano](https://huggingface.co/KittenML/kitten-tts-nano-0.8), [Piper Low](https://huggingface.co/rhasspy/piper-voices), and [Supertonic 3](https://huggingface.co/Supertone/supertonic-3), established compact or local TTS baselines with larger deployable weight footprints than both Inflect releases. Weight sizes are compared at package level, and no single metric is treated as proof of overall superiority.
|
| 90 |
+
|
| 91 |
+
### 1. Human blind preference
|
| 92 |
+
|
| 93 |
+

|
| 94 |
+
|
| 95 |
+
Inflect-Micro-v2 recorded a **66.2% preference rate** (21 wins · 10 losses · 3 ties) in the final anonymous community study. Systems were hidden, left/right order was randomized, and ties count as half a win. This is descriptive community evidence, not formal MOS.
|
| 96 |
+
|
| 97 |
+
### 2. Predicted naturalness versus footprint
|
| 98 |
+
|
| 99 |
+

|
| 100 |
+
|
| 101 |
+
The UTMOS22 run used 500 identical unseen prompts per voice. KittenTTS and Piper are equal-weight two-voice means; their observed voice ranges appear as whiskers. Supertonic 3-step is reported below the plotted range rather than flattening every other system.
|
| 102 |
+
|
| 103 |
+
**Inflect-Micro-v2: 4.395 UTMOS22**, 95% bootstrap CI **4.381–4.408**. UTMOS22 is a learned predictor, not human MOS.
|
| 104 |
+
|
| 105 |
+
### 3. Intelligibility on unseen text
|
| 106 |
+
|
| 107 |
+

|
| 108 |
+
|
| 109 |
+
The headline score is the equal-weight mean of Qwen3-ASR and Nemotron 3.5 corpus WER for **every** system. Whisper is excluded consistently from the headline because it produced insertion-heavy hallucinations on a subset of otherwise intelligible Supertonic 8-step clips. It is not deleted: the complete three-ASR evidence remains below.
|
| 110 |
+
|
| 111 |
+
<details>
|
| 112 |
+
<summary><strong>Open the complete three-ASR audit</strong></summary>
|
| 113 |
+
|
| 114 |
+

|
| 115 |
+
|
| 116 |
+
| System / voice | Qwen3-ASR ↓ | Nemotron 3.5 ↓ | Whisper large-v3 ↓ |
|
| 117 |
+
| --- | ---: | ---: | ---: |
|
| 118 |
+
| **Inflect-Micro-v2** | **2.52%** | **5.45%** | **2.73%** |
|
| 119 |
+
| **Inflect-Nano-v2** | **2.79%** | **5.63%** | **2.65%** |
|
| 120 |
+
| KittenTTS Nano · Bruno | 2.15% | 3.96% | 2.17% |
|
| 121 |
+
| KittenTTS Nano · Hugo | 2.39% | 3.80% | 2.11% |
|
| 122 |
+
| Piper Low · Danny | 2.62% | 5.60% | 2.55% |
|
| 123 |
+
| Piper Low · Ryan | 2.81% | 5.51% | 2.87% |
|
| 124 |
+
| Supertonic 3 · M2 · 3-step | 3.03% | 6.04% | 3.22% |
|
| 125 |
+
| Supertonic 3 · M2 · 8-step | 2.05% | 3.56% | 8.08% |
|
| 126 |
+
|
| 127 |
+
For Inflect-Micro-v2, the individual results are **2.52% Qwen3-ASR**, **5.45% Nemotron 3.5**, and **2.73% Whisper large-v3**. The former three-model mean, **3.57%**, is retained only as a descriptive audit value and is not used as the headline score.
|
| 128 |
+
|
| 129 |
+
</details>
|
| 130 |
+
|
| 131 |
+
<details>
|
| 132 |
+
<summary><strong>Open evaluator robustness and error-category diagnostics</strong></summary>
|
| 133 |
+
|
| 134 |
+

|
| 135 |
+
|
| 136 |
+

|
| 137 |
+
|
| 138 |
+
These views are diagnostics, not additional leaderboards. They show where the
|
| 139 |
+
recognizers disagree and which prompt categories still produce recoverable
|
| 140 |
+
transcription errors.
|
| 141 |
+
|
| 142 |
+
</details>
|
| 143 |
+
|
| 144 |
+
### 4. CPU runtime
|
| 145 |
+
|
| 146 |
+
Both Inflect releases synthesize comfortably faster than real time on CPU. The
|
| 147 |
+
managed reference run used a Hugging Face **CPU Upgrade** instance (8 vCPU,
|
| 148 |
+
32 GB RAM) with **four framework threads**, end-to-end text-to-waveform timing,
|
| 149 |
+
and 100 fixed Modern400 prompts. Three complete passes were recorded; the first
|
| 150 |
+
cache-building pass was excluded and the table pools passes two and three.
|
| 151 |
+
|
| 152 |
+
| Release | Steady-state RTF ↓ | Audio / wall time ↑ |
|
| 153 |
+
| --- | ---: | ---: |
|
| 154 |
+
| **Inflect-Micro-v2** | **0.1593** | **6.28×** |
|
| 155 |
+
| **Inflect-Nano-v2** | **0.0933** | **10.72×** |
|
| 156 |
+
|
| 157 |
+
These are package-level results from the public PyTorch runtime, not a claim
|
| 158 |
+
that Inflect is the fastest compact TTS system. Hardware, frontend behavior,
|
| 159 |
+
framework, compilation, and thread policy all affect small-model measurements.
|
| 160 |
+
|
| 161 |
+
<details>
|
| 162 |
+
<summary><strong>Open directional compact-system speed context</strong></summary>
|
| 163 |
+
|
| 164 |
+
The same managed CPU and four-thread policy were used for a shorter comparator
|
| 165 |
+
pass: the identical 50-prompt prefix, repeated twice. KittenTTS and Piper are
|
| 166 |
+
equal-work pooled across their two tested voices.
|
| 167 |
+
|
| 168 |
+
| System | Audio / wall time ↑ |
|
| 169 |
+
| --- | ---: |
|
| 170 |
+
| Piper Low | 31.37× |
|
| 171 |
+
| KittenTTS Nano | 13.33× |
|
| 172 |
+
| **Inflect-Nano-v2** | **10.72×** |
|
| 173 |
+
| Supertonic 3 · 3-step | 10.15× |
|
| 174 |
+
| **Inflect-Micro-v2** | **6.28×** |
|
| 175 |
+
| Supertonic 3 · 8-step | 4.37× |
|
| 176 |
+
|
| 177 |
+
Because Inflect uses the larger 100-prompt steady-state run while comparator
|
| 178 |
+
rows use the shorter 50-prompt confirmation pass, this table is deployment
|
| 179 |
+
context rather than a perfectly matched speed leaderboard. Several comparators
|
| 180 |
+
also use optimized ONNX runtimes, while the published Inflect benchmark above
|
| 181 |
+
uses the canonical PyTorch runtime. The separately released Inflect ONNX path
|
| 182 |
+
has not been substituted into those benchmark numbers.
|
| 183 |
+
|
| 184 |
+
</details>
|
| 185 |
+
|
| 186 |
+
### 5. Complete weight footprint
|
| 187 |
+
|
| 188 |
+

|
| 189 |
+
|
| 190 |
+
Voice variants sharing the same weights are merged. Inflect totals include the integrated waveform decoder.
|
| 191 |
+
|
| 192 |
+
<details>
|
| 193 |
+
<summary><strong>Open the frozen evaluation protocol</strong></summary>
|
| 194 |
+
|
| 195 |
+
- Modern400 uses 400 identical unseen English prompts per system: 200 fixed modern/stress prompts plus 200 deterministic FLEURS `en_us` test prompts.
|
| 196 |
+
- Exact-text exclusion was checked against 87,362 training transcripts.
|
| 197 |
+
- All ASR inputs are resampled to 16 kHz and scored with the same disclosed English normalizer.
|
| 198 |
+
- UTMOS22 uses `tarepan/SpeechMOS` v1.2.0 on a separate 500-prompt generation set.
|
| 199 |
+
- Headline intervals use 10,000 bootstrap samples.
|
| 200 |
+
- The Modern400 corpus SHA-256 is `b7504ce2dce44a2da82770a6a5dfd2a034fe17e2113980f8a69663ade417a34c`.
|
| 201 |
+
- Prompts, hypotheses, compressed row-level reports, and summaries ship under `evaluation/final/`.
|
| 202 |
+
- Runtime is evaluated separately because framework, thread policy, compilation,
|
| 203 |
+
and host load can dominate small-model comparisons.
|
| 204 |
+
|
| 205 |
+
</details>
|
| 206 |
+
|
| 207 |
+
---
|
| 208 |
+
|
| 209 |
+
## Choose the right Inflect
|
| 210 |
+
|
| 211 |
+
| | **Inflect-Nano-v2** | **Inflect-Micro-v2** |
|
| 212 |
+
| --- | ---: | ---: |
|
| 213 |
+
| Complete parameters | 3,966,721 | 9,356,513 |
|
| 214 |
+
| FP32 weights | 15.97 MB | 37.53 MB |
|
| 215 |
+
| Positioning | Smallest practical footprint | Strongest Inflect v2 quality |
|
| 216 |
+
| 24 kHz waveform decoder | Included | Included |
|
| 217 |
+
| Python API and frontend | Same | Same |
|
| 218 |
+
|
| 219 |
+
**Inflect-Micro-v2** is the quality-focused member of the family. Both models use the same public API and complete text-to-waveform packaging.
|
| 220 |
+
|
| 221 |
+
## Run locally
|
| 222 |
+
|
| 223 |
+
### Install
|
| 224 |
+
|
| 225 |
+
```bash
|
| 226 |
+
python -m pip install --upgrade huggingface_hub
|
| 227 |
+
hf download owensong/Inflect-Micro-v2 --local-dir Inflect-Micro-v2
|
| 228 |
+
cd Inflect-Micro-v2
|
| 229 |
+
python -m pip install -r requirements.txt
|
| 230 |
+
```
|
| 231 |
+
|
| 232 |
+
This uses the Hub's version-aware downloader and retrieves the complete
|
| 233 |
+
repository. A Git clone also works, but `hf download` is the recommended path
|
| 234 |
+
for ordinary model installation.
|
| 235 |
+
|
| 236 |
+
### Python
|
| 237 |
+
|
| 238 |
+
```python
|
| 239 |
+
from inference import InflectTTS
|
| 240 |
+
|
| 241 |
+
tts = InflectTTS(".", device="cpu")
|
| 242 |
+
tts.save(
|
| 243 |
+
"A small voice can still have something meaningful to say.",
|
| 244 |
+
"sample.wav",
|
| 245 |
+
speed=1.0,
|
| 246 |
+
variation=0.667,
|
| 247 |
+
seed=7,
|
| 248 |
+
)
|
| 249 |
+
```
|
| 250 |
+
|
| 251 |
+
### Download through the Hub
|
| 252 |
+
|
| 253 |
+
```python
|
| 254 |
+
import sys
|
| 255 |
+
from huggingface_hub import snapshot_download
|
| 256 |
+
|
| 257 |
+
model_dir = snapshot_download("owensong/Inflect-Micro-v2")
|
| 258 |
+
sys.path.insert(0, model_dir)
|
| 259 |
+
|
| 260 |
+
from inference import InflectTTS
|
| 261 |
+
|
| 262 |
+
tts = InflectTTS(model_dir, device="cpu")
|
| 263 |
+
sample_rate, waveform = tts.synthesize("The complete model runs locally.")
|
| 264 |
+
```
|
| 265 |
+
|
| 266 |
+
The result is a 24 kHz mono `float32` waveform. Long input is split at punctuation-aware boundaries, synthesized chunk by chunk, and joined with controlled pauses.
|
| 267 |
+
|
| 268 |
+
### ONNX Runtime
|
| 269 |
+
|
| 270 |
+
The official verified FP32 export is published separately as
|
| 271 |
+
[`Inflect-Micro-v2-ONNX`](https://huggingface.co/owensong/Inflect-Micro-v2-ONNX).
|
| 272 |
+
It supports dynamic lengths, CPU/CUDA/DirectML provider selection,
|
| 273 |
+
deterministic seeds, and the same long-text wrapper without importing PyTorch:
|
| 274 |
+
|
| 275 |
+
```bash
|
| 276 |
+
git clone https://huggingface.co/owensong/Inflect-Micro-v2-ONNX
|
| 277 |
+
cd Inflect-Micro-v2-ONNX
|
| 278 |
+
python -m pip install -r onnx/requirements.txt
|
| 279 |
+
python onnx/inference_onnx.py \
|
| 280 |
+
--text "The complete model now runs through ONNX Runtime." \
|
| 281 |
+
--output sample-onnx.wav \
|
| 282 |
+
--provider cpu \
|
| 283 |
+
--seed 7
|
| 284 |
+
```
|
| 285 |
+
|
| 286 |
+
The neural model is split into `duration.onnx` and `decode.onnx`; together they
|
| 287 |
+
contain the complete learned text-to-waveform path. The English eSpeak-ng
|
| 288 |
+
frontend remains CPU-side code. See the
|
| 289 |
+
[`ONNX repository`](https://huggingface.co/owensong/Inflect-Micro-v2-ONNX)
|
| 290 |
+
for graph contracts, provenance, parity measurements, browser deployment, and
|
| 291 |
+
re-export instructions.
|
| 292 |
+
|
| 293 |
+
## Release profile
|
| 294 |
+
|
| 295 |
+
| **Local runtime** | **Long-text handling** |
|
| 296 |
+
| --- | --- |
|
| 297 |
+
| CPU and CUDA inference through the same Python API and CLI. | Punctuation-aware segmentation with controlled pauses and edge fades. |
|
| 298 |
+
| **Repeatable output** | **Auditable evaluation** |
|
| 299 |
+
| Fixed seeds reproduce the same latent sample on the same runtime stack. | Frozen prompts, raw ASR hypotheses, intervals, hashes, and per-system reports are included. |
|
| 300 |
+
|
| 301 |
+
<details>
|
| 302 |
+
<summary id="architecture-and-parameter-budget"><strong>Architecture and parameter budget</strong></summary>
|
| 303 |
+
|
| 304 |
+
Inflect v2 is a parameter-efficient VITS-family end-to-end text-to-waveform generator with an English phoneme frontend, monotonic alignment, stochastic latent synthesis, residual coupling flow, and an integrated alias-reduced neural waveform decoder.
|
| 305 |
+
|
| 306 |
+
| Component | Inflect-Micro-v2 |
|
| 307 |
+
| --- | ---: |
|
| 308 |
+
| Latent channels | 192 |
|
| 309 |
+
| Text hidden channels | 96 |
|
| 310 |
+
| Encoder layers / heads | 3 / 2 |
|
| 311 |
+
| Feed-forward channels | 768 |
|
| 312 |
+
| Flow coupling blocks | 4 |
|
| 313 |
+
| Initial decoder channels | 320 |
|
| 314 |
+
| Upsample rates | 8, 8, 2, 2 |
|
| 315 |
+
| Training segment | 16,384 samples |
|
| 316 |
+
| Output | 24 kHz mono waveform |
|
| 317 |
+
|
| 318 |
+
The release describes the deployable architecture. Private corpus-construction and optimization details are not part of this open-weight package.
|
| 319 |
+
|
| 320 |
+
</details>
|
| 321 |
+
|
| 322 |
+
<details>
|
| 323 |
+
<summary id="controls-determinism-and-long-text"><strong>Controls, determinism, and long text</strong></summary>
|
| 324 |
+
|
| 325 |
+
| Control | Default | Public range | Meaning |
|
| 326 |
+
| --- | ---: | ---: | --- |
|
| 327 |
+
| `speed` | `1.0` | `0.5–2.0` | Lower is slower; higher is faster. |
|
| 328 |
+
| `variation` | `0.667` | `0.0–1.0` | Lower is steadier; higher samples more latent variation. |
|
| 329 |
+
| `seed` | `0` | integer | Repeats the same stochastic sample on the same runtime stack. |
|
| 330 |
+
|
| 331 |
+
Long passages are punctuation-aware chunks, not one unlimited autoregressive pass. Chunk boundaries receive short pauses and edge fades. See [`docs/API.md`](https://huggingface.co/owensong/Inflect-Micro-v2/blob/main/docs/API.md) for waveform contracts and concurrency notes.
|
| 332 |
+
|
| 333 |
+
</details>
|
| 334 |
+
|
| 335 |
+
<details>
|
| 336 |
+
<summary id="data-voice-and-adaptation-status"><strong>Data, voice, and adaptation status</strong></summary>
|
| 337 |
+
|
| 338 |
+
The release contains one fixed synthetic English voice. The package does not redistribute a real-speaker recording corpus, does not claim the voice as the identity of a real person, and requires no reference audio or external model at inference.
|
| 339 |
+
|
| 340 |
+
The base release remains inference-first, but a public **experimental fixed-voice and language adaptation workflow** is now available. A new voice replaces the built-in speaker rather than adding runtime voice cloning. A new language requires owned or licensed speech data, a compatible phoneme frontend, symbol migration, retraining, and fluent-speaker evaluation. Start with the [Inflect adaptation toolkit](https://github.com/owenawsong/Inflect/tree/main/finetune), then review [`docs/DATA_AND_VOICE.md`](https://huggingface.co/owensong/Inflect-Micro-v2/blob/main/docs/DATA_AND_VOICE.md).
|
| 341 |
+
|
| 342 |
+
</details>
|
| 343 |
+
|
| 344 |
+
## Package map
|
| 345 |
+
|
| 346 |
+
| Path | Purpose |
|
| 347 |
+
| --- | --- |
|
| 348 |
+
| `model.pth` | Inference-only generator checkpoint |
|
| 349 |
+
| `config.json` | Architecture and audio configuration; also the Hub download-count query file |
|
| 350 |
+
| `inference.py` | Public Python API and CLI |
|
| 351 |
+
| `inflect_vits_frontend.py` | English normalization, phonemization, and punctuation frontend |
|
| 352 |
+
| `runtime/` | Self-contained model implementation |
|
| 353 |
+
| [`Inflect-Micro-v2-ONNX`](https://huggingface.co/owensong/Inflect-Micro-v2-ONNX) | Separate official FP32 ONNX graphs, torch-free runner, parity report, checksums, and exporter |
|
| 354 |
+
| `samples/` | Held-out example generations |
|
| 355 |
+
| `evaluation/final/` | Frozen benchmark prompts, reports, and protocol artifacts |
|
| 356 |
+
| `docs/` | API, deployment, evaluation, adaptation, and export documentation |
|
| 357 |
+
| `release_manifest.json` | File sizes and SHA-256 hashes |
|
| 358 |
+
|
| 359 |
+
## Limitations
|
| 360 |
+
|
| 361 |
+
- English only, with one fixed male voice. This is not zero-shot voice cloning.
|
| 362 |
+
- Unfamiliar phrasing can become flatter, less expressive, or less stable.
|
| 363 |
+
- Numbers, abbreviations, homographs, and uncommon names remain frontend- and context-sensitive.
|
| 364 |
+
- Long passages use punctuation-aware chunking; transitions can differ from a native long-form model pass.
|
| 365 |
+
- Stochastic variation can alter timing and pronunciation. Fix the seed for comparisons.
|
| 366 |
+
- UTMOS22 and ASR scores do not replace controlled human MOS or MUSHRA-style evaluation.
|
| 367 |
+
- Not validated for medical, legal, emergency, or accessibility-critical communication.
|
| 368 |
+
|
| 369 |
+
## Responsible use
|
| 370 |
+
|
| 371 |
+
Do not use the included voice to impersonate a real person, deceive listeners, or create fraudulent content. Disclose synthetic speech where the context could otherwise mislead. Users are responsible for applicable laws and the Apache-2.0 license.
|
| 372 |
+
|
| 373 |
+
## License, integrity, and attribution
|
| 374 |
+
|
| 375 |
+
Original Inflect code and weights are released under Apache-2.0. Bundled third-party components retain their own notices in [`THIRD_PARTY_NOTICES.md`](https://huggingface.co/owensong/Inflect-Micro-v2/blob/main/THIRD_PARTY_NOTICES.md). `release_manifest.json` records packaged file sizes and SHA-256 hashes.
|
| 376 |
+
|
| 377 |
+
### Private training scope and contact
|
| 378 |
+
|
| 379 |
+
Inflect v2 is an **open-weight** release. Deployable weights, inference code, frontend code, evaluation prompts, and release reports are public. The training corpus-generation pipeline, private filtering infrastructure, and full optimization recipe are not part of the public package.
|
| 380 |
+
|
| 381 |
+
Owen Song may share additional technical context privately for credible research, collaboration, reproducibility, or deployment inquiries when the request has a clear purpose and does not conflict with licensing or data-provenance constraints.
|
| 382 |
+
|
| 383 |
+
- **Discord:** `b111ue` — fastest for informal technical questions
|
| 384 |
+
- **Community server:** [discord.gg/CVJYedvzvp](https://discord.gg/CVJYedvzvp)
|
| 385 |
+
- **Email:** [owen.aw.song@gmail.com](mailto:owen.aw.song@gmail.com) — preferred for professional inquiries
|
| 386 |
+
|
| 387 |
+
## Citation
|
| 388 |
+
|
| 389 |
+
```bibtex
|
| 390 |
+
@software{song2026inflectmicrov2,
|
| 391 |
+
author = {Owen Song},
|
| 392 |
+
title = {Inflect-Micro-v2: Complete Local Text-to-Waveform TTS Under 10M Parameters},
|
| 393 |
+
year = {2026},
|
| 394 |
+
url = {https://huggingface.co/owensong/Inflect-Micro-v2}
|
| 395 |
+
}
|
| 396 |
+
```
|
| 397 |
+
|
| 398 |
+
<p align="center"><sub>Designed and developed independently by Owen Song · open weights · Apache-2.0 · complete local text-to-waveform inference</sub></p>
|
config.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"format": "inflect_v2_inference_config_v1",
|
| 3 |
+
"train": {
|
| 4 |
+
"segment_size": 16384
|
| 5 |
+
},
|
| 6 |
+
"data": {
|
| 7 |
+
"text_cleaners": [],
|
| 8 |
+
"max_wav_value": 32768.0,
|
| 9 |
+
"sampling_rate": 24000,
|
| 10 |
+
"filter_length": 1024,
|
| 11 |
+
"hop_length": 256,
|
| 12 |
+
"win_length": 1024,
|
| 13 |
+
"n_mel_channels": 80,
|
| 14 |
+
"mel_fmin": 0.0,
|
| 15 |
+
"mel_fmax": 12000.0,
|
| 16 |
+
"add_blank": true,
|
| 17 |
+
"n_speakers": 4,
|
| 18 |
+
"cleaned_text": true
|
| 19 |
+
},
|
| 20 |
+
"model": {
|
| 21 |
+
"inter_channels": 192,
|
| 22 |
+
"hidden_channels": 96,
|
| 23 |
+
"filter_channels": 768,
|
| 24 |
+
"n_heads": 2,
|
| 25 |
+
"n_layers": 3,
|
| 26 |
+
"kernel_size": 3,
|
| 27 |
+
"p_dropout": 0.1,
|
| 28 |
+
"resblock": "1",
|
| 29 |
+
"resblock_kernel_sizes": [
|
| 30 |
+
3,
|
| 31 |
+
7,
|
| 32 |
+
11
|
| 33 |
+
],
|
| 34 |
+
"resblock_dilation_sizes": [
|
| 35 |
+
[
|
| 36 |
+
1,
|
| 37 |
+
3,
|
| 38 |
+
5
|
| 39 |
+
],
|
| 40 |
+
[
|
| 41 |
+
1,
|
| 42 |
+
3,
|
| 43 |
+
5
|
| 44 |
+
],
|
| 45 |
+
[
|
| 46 |
+
1,
|
| 47 |
+
3,
|
| 48 |
+
5
|
| 49 |
+
]
|
| 50 |
+
],
|
| 51 |
+
"upsample_rates": [
|
| 52 |
+
8,
|
| 53 |
+
8,
|
| 54 |
+
2,
|
| 55 |
+
2
|
| 56 |
+
],
|
| 57 |
+
"upsample_initial_channel": 320,
|
| 58 |
+
"upsample_kernel_sizes": [
|
| 59 |
+
16,
|
| 60 |
+
16,
|
| 61 |
+
4,
|
| 62 |
+
4
|
| 63 |
+
],
|
| 64 |
+
"n_layers_q": 3,
|
| 65 |
+
"use_spectral_norm": false,
|
| 66 |
+
"use_sdp": false,
|
| 67 |
+
"inference_only": true,
|
| 68 |
+
"n_speakers": 4,
|
| 69 |
+
"gin_channels": 256
|
| 70 |
+
}
|
| 71 |
+
}
|
examples/basic.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
MODEL_DIR = Path(__file__).resolve().parents[1]
|
| 5 |
+
sys.path.insert(0, str(MODEL_DIR))
|
| 6 |
+
|
| 7 |
+
from inference import InflectTTS
|
| 8 |
+
|
| 9 |
+
tts = InflectTTS(MODEL_DIR, device="cpu")
|
| 10 |
+
tts.save(
|
| 11 |
+
"A small local voice can still be useful.",
|
| 12 |
+
MODEL_DIR / "example.wav",
|
| 13 |
+
speed=1.0,
|
| 14 |
+
variation=0.667,
|
| 15 |
+
seed=7,
|
| 16 |
+
)
|
examples/long_text.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
MODEL_DIR = Path(__file__).resolve().parents[1]
|
| 5 |
+
sys.path.insert(0, str(MODEL_DIR))
|
| 6 |
+
|
| 7 |
+
from inference import InflectTTS
|
| 8 |
+
|
| 9 |
+
text = (
|
| 10 |
+
"Long input is divided at punctuation-aware boundaries. "
|
| 11 |
+
"Each segment is generated locally, then joined with a controlled pause; "
|
| 12 |
+
"this keeps memory bounded without requiring a remote service."
|
| 13 |
+
)
|
| 14 |
+
InflectTTS(MODEL_DIR, device="cpu").save(text, MODEL_DIR / "long_example.wav", seed=11)
|
examples/voices.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Speak the same line in each voice, then blend two of them."""
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import sys
|
| 4 |
+
|
| 5 |
+
MODEL_DIR = Path(__file__).resolve().parents[1]
|
| 6 |
+
sys.path.insert(0, str(MODEL_DIR))
|
| 7 |
+
|
| 8 |
+
from inference import InflectTTS
|
| 9 |
+
|
| 10 |
+
engine = InflectTTS(MODEL_DIR, device="cpu")
|
| 11 |
+
line = "The same model, wearing a different voice."
|
| 12 |
+
|
| 13 |
+
for name in engine.voices:
|
| 14 |
+
out = MODEL_DIR / f"example_{name}.wav"
|
| 15 |
+
engine.save(line, out, voice=name)
|
| 16 |
+
print(f"{name} -> {out.name}")
|
| 17 |
+
|
| 18 |
+
# Blending interpolates between voices. Weights are normalized.
|
| 19 |
+
out = MODEL_DIR / "example_blend.wav"
|
| 20 |
+
engine.save(line, out, blend={"clara": 0.7, "silas": 0.3})
|
| 21 |
+
print(f"70% clara / 30% silas -> {out.name}")
|
inference.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import contextlib
|
| 5 |
+
import io
|
| 6 |
+
import json
|
| 7 |
+
import logging
|
| 8 |
+
import math
|
| 9 |
+
import re
|
| 10 |
+
import sys
|
| 11 |
+
import warnings
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
import numpy as np
|
| 15 |
+
import soundfile as sf
|
| 16 |
+
import torch
|
| 17 |
+
|
| 18 |
+
# CPU thread cap: PyTorch's default (one thread per core) intermittently faults
|
| 19 |
+
# inside oneDNN on many-core machines — measured ~35-40% of runs on a 32-core box.
|
| 20 |
+
# Eight threads is well past this model's scaling knee, so this costs no speed.
|
| 21 |
+
_MAX_THREADS = 8
|
| 22 |
+
try:
|
| 23 |
+
if torch.get_num_threads() > _MAX_THREADS:
|
| 24 |
+
torch.set_num_threads(_MAX_THREADS)
|
| 25 |
+
except Exception: # pragma: no cover - never let a tuning hint break import
|
| 26 |
+
pass
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
PACKAGE_ROOT = Path(__file__).resolve().parent
|
| 30 |
+
RUNTIME_ROOT = PACKAGE_ROOT / "runtime"
|
| 31 |
+
sys.path.insert(0, str(RUNTIME_ROOT))
|
| 32 |
+
sys.path.insert(0, str(PACKAGE_ROOT))
|
| 33 |
+
|
| 34 |
+
import commons # noqa: E402
|
| 35 |
+
import utils # noqa: E402
|
| 36 |
+
from inflect_vits_frontend import run_vits_frontend # noqa: E402
|
| 37 |
+
from models import SynthesizerTrn # noqa: E402
|
| 38 |
+
from text import cleaned_text_to_sequence # noqa: E402
|
| 39 |
+
from text.symbols import symbols # noqa: E402
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
SPEAKER_FILE = "speakers.json"
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def split_text(text: str, limit: int = 280) -> list[str]:
|
| 46 |
+
normalized = " ".join(text.split())
|
| 47 |
+
sentences = [
|
| 48 |
+
part.strip()
|
| 49 |
+
for part in re.split(r"(?<=[.!?;:])\s+", normalized)
|
| 50 |
+
if part.strip()
|
| 51 |
+
]
|
| 52 |
+
chunks: list[str] = []
|
| 53 |
+
for sentence in sentences or [normalized]:
|
| 54 |
+
while len(sentence) > limit:
|
| 55 |
+
search = sentence[: limit + 1]
|
| 56 |
+
punctuation = max(search.rfind(mark) for mark in (",", ";", ":"))
|
| 57 |
+
split_at = (
|
| 58 |
+
punctuation + 1
|
| 59 |
+
if punctuation >= limit // 2
|
| 60 |
+
else sentence.rfind(" ", 0, limit + 1)
|
| 61 |
+
)
|
| 62 |
+
if split_at < limit // 2:
|
| 63 |
+
split_at = limit
|
| 64 |
+
chunks.append(sentence[:split_at].strip())
|
| 65 |
+
sentence = sentence[split_at:].strip()
|
| 66 |
+
if sentence:
|
| 67 |
+
chunks.append(sentence)
|
| 68 |
+
return chunks
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def boundary_pause_seconds(chunk: str) -> float:
|
| 72 |
+
ending = chunk.rstrip()[-1:] if chunk.strip() else ""
|
| 73 |
+
return {
|
| 74 |
+
"?": 0.28,
|
| 75 |
+
"!": 0.24,
|
| 76 |
+
".": 0.22,
|
| 77 |
+
";": 0.16,
|
| 78 |
+
":": 0.13,
|
| 79 |
+
",": 0.09,
|
| 80 |
+
}.get(ending, 0.08)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def edge_fade(waveform: np.ndarray, sample_rate: int, milliseconds: float = 5.0) -> np.ndarray:
|
| 84 |
+
frames = min(round(sample_rate * milliseconds / 1000.0), waveform.size // 2)
|
| 85 |
+
if frames <= 0:
|
| 86 |
+
return waveform
|
| 87 |
+
output = waveform.copy()
|
| 88 |
+
ramp = np.linspace(0.0, 1.0, frames, endpoint=True, dtype=np.float32)
|
| 89 |
+
output[:frames] *= ramp
|
| 90 |
+
output[-frames:] *= ramp[::-1]
|
| 91 |
+
return output
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def optimize_for_inference(model: SynthesizerTrn) -> None:
|
| 95 |
+
"""Collapse training-time weight normalization without changing outputs."""
|
| 96 |
+
with contextlib.redirect_stdout(io.StringIO()):
|
| 97 |
+
model.dec.remove_weight_norm()
|
| 98 |
+
for flow in model.flow.flows:
|
| 99 |
+
encoder = getattr(flow, "enc", None)
|
| 100 |
+
if encoder is not None and hasattr(encoder, "remove_weight_norm"):
|
| 101 |
+
encoder.remove_weight_norm()
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def parse_blend(specification: str) -> dict[str, float]:
|
| 105 |
+
"""Read a command line blend such as "clara=0.7,silas=0.3"."""
|
| 106 |
+
blend: dict[str, float] = {}
|
| 107 |
+
for part in specification.split(","):
|
| 108 |
+
entry = part.strip()
|
| 109 |
+
if not entry:
|
| 110 |
+
continue
|
| 111 |
+
name, separator, weight = entry.partition("=")
|
| 112 |
+
name = name.strip()
|
| 113 |
+
if not separator or not name:
|
| 114 |
+
raise ValueError(f"blend entries look like name=weight, got {entry!r}")
|
| 115 |
+
try:
|
| 116 |
+
value = float(weight)
|
| 117 |
+
except ValueError:
|
| 118 |
+
raise ValueError(f"blend weight for {name!r} is not a number: {weight.strip()!r}") from None
|
| 119 |
+
blend[name] = blend.get(name, 0.0) + value
|
| 120 |
+
if not blend:
|
| 121 |
+
raise ValueError("blend must name at least one voice.")
|
| 122 |
+
return blend
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
class InflectTTS:
|
| 126 |
+
def __init__(self, model_dir: str | Path = PACKAGE_ROOT, device: str = "cpu") -> None:
|
| 127 |
+
self.root = Path(model_dir).resolve()
|
| 128 |
+
self.device = torch.device(device)
|
| 129 |
+
self.hps = utils.get_hparams_from_file(str(self.root / "config.json"))
|
| 130 |
+
self.n_speakers = int(self.hps.model.n_speakers) if "n_speakers" in self.hps.model else 0
|
| 131 |
+
if self.n_speakers == 1:
|
| 132 |
+
raise ValueError(
|
| 133 |
+
"config.json sets n_speakers=1; use 0 for a single voice or 2 or more for a family."
|
| 134 |
+
)
|
| 135 |
+
with warnings.catch_warnings():
|
| 136 |
+
warnings.filterwarnings(
|
| 137 |
+
"ignore",
|
| 138 |
+
message="`torch.nn.utils.weight_norm` is deprecated",
|
| 139 |
+
category=FutureWarning,
|
| 140 |
+
)
|
| 141 |
+
self.model = SynthesizerTrn(
|
| 142 |
+
len(symbols),
|
| 143 |
+
self.hps.data.filter_length // 2 + 1,
|
| 144 |
+
self.hps.train.segment_size // self.hps.data.hop_length,
|
| 145 |
+
**self.hps.model,
|
| 146 |
+
).to(self.device).eval()
|
| 147 |
+
root_logger = logging.getLogger()
|
| 148 |
+
previous_level = root_logger.level
|
| 149 |
+
try:
|
| 150 |
+
root_logger.setLevel(logging.WARNING)
|
| 151 |
+
utils.load_checkpoint(str(self.root / "model.pth"), self.model, None)
|
| 152 |
+
finally:
|
| 153 |
+
root_logger.setLevel(previous_level)
|
| 154 |
+
self.checkpoint_parameters = sum(parameter.numel() for parameter in self.model.parameters())
|
| 155 |
+
optimize_for_inference(self.model)
|
| 156 |
+
self.deployed_parameters = sum(parameter.numel() for parameter in self.model.parameters())
|
| 157 |
+
self.sample_rate = int(self.hps.data.sampling_rate)
|
| 158 |
+
self.speakers = self._load_speakers()
|
| 159 |
+
self.voices = [
|
| 160 |
+
name for name, _ in sorted(self.speakers.items(), key=lambda item: (item[1], item[0]))
|
| 161 |
+
]
|
| 162 |
+
|
| 163 |
+
def _load_speakers(self) -> dict[str, int]:
|
| 164 |
+
"""Map voice name to speaker id, falling back to plain integer names."""
|
| 165 |
+
if self.n_speakers < 2:
|
| 166 |
+
return {}
|
| 167 |
+
table: object = {}
|
| 168 |
+
path = self.root / SPEAKER_FILE
|
| 169 |
+
if path.is_file():
|
| 170 |
+
document = json.loads(path.read_text(encoding="utf-8"))
|
| 171 |
+
if isinstance(document, dict):
|
| 172 |
+
nested = document.get("speakers")
|
| 173 |
+
table = nested if isinstance(nested, dict) else document
|
| 174 |
+
speakers: dict[str, int] = {}
|
| 175 |
+
if isinstance(table, dict):
|
| 176 |
+
for name, value in table.items():
|
| 177 |
+
if isinstance(value, bool) or not isinstance(value, (int, str)):
|
| 178 |
+
continue
|
| 179 |
+
try:
|
| 180 |
+
identifier = int(value)
|
| 181 |
+
except ValueError:
|
| 182 |
+
continue
|
| 183 |
+
if 0 <= identifier < self.n_speakers:
|
| 184 |
+
speakers[str(name)] = identifier
|
| 185 |
+
for identifier in range(self.n_speakers):
|
| 186 |
+
if identifier not in speakers.values():
|
| 187 |
+
speakers[str(identifier)] = identifier
|
| 188 |
+
return speakers
|
| 189 |
+
|
| 190 |
+
def voice_id(self, voice: str | int) -> int:
|
| 191 |
+
"""Resolve a voice name (case-insensitive) or a speaker id to a speaker id."""
|
| 192 |
+
if isinstance(voice, str):
|
| 193 |
+
wanted = voice.strip().lower()
|
| 194 |
+
for name, identifier in self.speakers.items():
|
| 195 |
+
if name.lower() == wanted:
|
| 196 |
+
return identifier
|
| 197 |
+
if wanted.isdigit() and 0 <= int(wanted) < self.n_speakers:
|
| 198 |
+
return int(wanted)
|
| 199 |
+
raise ValueError(
|
| 200 |
+
f"Unknown voice {voice!r}. This model speaks: {', '.join(self.voices)}."
|
| 201 |
+
)
|
| 202 |
+
if isinstance(voice, (int, np.integer)) and not isinstance(voice, bool):
|
| 203 |
+
identifier = int(voice)
|
| 204 |
+
if 0 <= identifier < self.n_speakers:
|
| 205 |
+
return identifier
|
| 206 |
+
raise ValueError(
|
| 207 |
+
f"Speaker id {identifier} is out of range. This model speaks: {', '.join(self.voices)}."
|
| 208 |
+
)
|
| 209 |
+
raise TypeError("voice must be a voice name, a speaker id, or None.")
|
| 210 |
+
|
| 211 |
+
def _blend_weights(self, blend: dict[str | int, float]) -> dict[int, float]:
|
| 212 |
+
if not isinstance(blend, dict):
|
| 213 |
+
raise TypeError("blend must be a dict of voice name or id to weight.")
|
| 214 |
+
weights: dict[int, float] = {}
|
| 215 |
+
for voice, weight in blend.items():
|
| 216 |
+
value = float(weight)
|
| 217 |
+
if not math.isfinite(value) or value < 0.0:
|
| 218 |
+
raise ValueError("blend weights must be finite and not negative.")
|
| 219 |
+
identifier = self.voice_id(voice)
|
| 220 |
+
weights[identifier] = weights.get(identifier, 0.0) + value
|
| 221 |
+
if not weights:
|
| 222 |
+
raise ValueError("blend must name at least one voice.")
|
| 223 |
+
total = sum(weights.values())
|
| 224 |
+
if total <= 0.0:
|
| 225 |
+
raise ValueError("blend weights must add up to more than zero.")
|
| 226 |
+
return {identifier: value / total for identifier, value in weights.items()}
|
| 227 |
+
|
| 228 |
+
@contextlib.contextmanager
|
| 229 |
+
def _blended_speaker(self, weights: dict[int, float]):
|
| 230 |
+
"""Stand a one-row speaker table in for emb_g so infer() sees the blend.
|
| 231 |
+
|
| 232 |
+
The weighted sum of the speaker embedding rows becomes row 0 of a
|
| 233 |
+
throwaway table, the model uses it for sid 0, and the original module
|
| 234 |
+
goes back in the finally block even if synthesis raises. The swap
|
| 235 |
+
mutates the shared model, so one InflectTTS instance must not blend on
|
| 236 |
+
two threads at once.
|
| 237 |
+
"""
|
| 238 |
+
original = self.model.emb_g
|
| 239 |
+
width = original.embedding_dim
|
| 240 |
+
vector = torch.zeros(width, dtype=original.weight.dtype, device=original.weight.device)
|
| 241 |
+
for identifier, weight in weights.items():
|
| 242 |
+
vector += original.weight[identifier] * weight
|
| 243 |
+
stand_in = torch.nn.Embedding(
|
| 244 |
+
1, width, dtype=original.weight.dtype, device=original.weight.device
|
| 245 |
+
)
|
| 246 |
+
stand_in.requires_grad_(False)
|
| 247 |
+
stand_in.weight.copy_(vector.unsqueeze(0))
|
| 248 |
+
stand_in.eval()
|
| 249 |
+
self.model.emb_g = stand_in
|
| 250 |
+
try:
|
| 251 |
+
yield
|
| 252 |
+
finally:
|
| 253 |
+
self.model.emb_g = original
|
| 254 |
+
|
| 255 |
+
def _tokens(self, text: str) -> tuple[torch.Tensor, torch.Tensor]:
|
| 256 |
+
phonemes = run_vits_frontend(text).phoneme_text
|
| 257 |
+
sequence = cleaned_text_to_sequence(phonemes)
|
| 258 |
+
if self.hps.data.add_blank:
|
| 259 |
+
sequence = commons.intersperse(sequence, 0)
|
| 260 |
+
if not sequence:
|
| 261 |
+
raise ValueError("The text frontend produced no speakable tokens.")
|
| 262 |
+
tokens = torch.LongTensor(sequence).to(self.device).unsqueeze(0)
|
| 263 |
+
lengths = torch.LongTensor([tokens.size(1)]).to(self.device)
|
| 264 |
+
return tokens, lengths
|
| 265 |
+
|
| 266 |
+
@torch.inference_mode()
|
| 267 |
+
def synthesize(
|
| 268 |
+
self,
|
| 269 |
+
text: str,
|
| 270 |
+
*,
|
| 271 |
+
voice: str | int | None = None,
|
| 272 |
+
blend: dict[str | int, float] | None = None,
|
| 273 |
+
speed: float = 1.0,
|
| 274 |
+
variation: float = 0.667,
|
| 275 |
+
seed: int = 0,
|
| 276 |
+
) -> tuple[int, np.ndarray]:
|
| 277 |
+
normalized = " ".join(text.split())
|
| 278 |
+
if not normalized:
|
| 279 |
+
raise ValueError("Text must not be empty.")
|
| 280 |
+
if not 0.5 <= speed <= 2.0:
|
| 281 |
+
raise ValueError("speed must be between 0.5 and 2.0")
|
| 282 |
+
if not 0.0 <= variation <= 1.0:
|
| 283 |
+
raise ValueError("variation must be between 0.0 and 1.0")
|
| 284 |
+
if self.n_speakers < 2:
|
| 285 |
+
if voice is not None or blend is not None:
|
| 286 |
+
raise ValueError("This model has one voice, so voice and blend do not apply.")
|
| 287 |
+
speaker = None
|
| 288 |
+
weights = None
|
| 289 |
+
elif blend is not None:
|
| 290 |
+
weights = self._blend_weights(blend)
|
| 291 |
+
speaker = torch.zeros(1, dtype=torch.long, device=self.device)
|
| 292 |
+
else:
|
| 293 |
+
weights = None
|
| 294 |
+
speaker = torch.LongTensor([self.voice_id(0 if voice is None else voice)]).to(self.device)
|
| 295 |
+
chunks = split_text(normalized)
|
| 296 |
+
pieces: list[np.ndarray] = []
|
| 297 |
+
speaking = self._blended_speaker(weights) if weights else contextlib.nullcontext()
|
| 298 |
+
with speaking:
|
| 299 |
+
for index, chunk in enumerate(chunks):
|
| 300 |
+
if index:
|
| 301 |
+
pieces.append(
|
| 302 |
+
np.zeros(
|
| 303 |
+
round(self.sample_rate * boundary_pause_seconds(chunks[index - 1])),
|
| 304 |
+
dtype=np.float32,
|
| 305 |
+
)
|
| 306 |
+
)
|
| 307 |
+
tokens, lengths = self._tokens(chunk)
|
| 308 |
+
torch.manual_seed(seed + index)
|
| 309 |
+
if self.device.type == "cuda":
|
| 310 |
+
torch.cuda.manual_seed_all(seed + index)
|
| 311 |
+
waveform = self.model.infer(
|
| 312 |
+
tokens,
|
| 313 |
+
lengths,
|
| 314 |
+
sid=speaker,
|
| 315 |
+
noise_scale=variation,
|
| 316 |
+
noise_scale_w=0.8,
|
| 317 |
+
length_scale=1.0 / speed,
|
| 318 |
+
max_len=4000,
|
| 319 |
+
)[0][0, 0].float().cpu().numpy()
|
| 320 |
+
pieces.append(edge_fade(waveform, self.sample_rate))
|
| 321 |
+
waveform = np.clip(np.concatenate(pieces), -1.0, 1.0)
|
| 322 |
+
return self.sample_rate, waveform
|
| 323 |
+
|
| 324 |
+
def save(self, text: str, output: str | Path, **kwargs: object) -> Path:
|
| 325 |
+
destination = Path(output)
|
| 326 |
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
| 327 |
+
sample_rate, waveform = self.synthesize(text, **kwargs)
|
| 328 |
+
sf.write(destination, waveform, sample_rate)
|
| 329 |
+
return destination
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
def main() -> None:
|
| 333 |
+
parser = argparse.ArgumentParser(description="Run standalone Inflect v2 synthesis.")
|
| 334 |
+
parser.add_argument("--model-dir", type=Path, default=PACKAGE_ROOT)
|
| 335 |
+
parser.add_argument("--text", required=True)
|
| 336 |
+
parser.add_argument("--output", type=Path, required=True)
|
| 337 |
+
parser.add_argument("--device", default="cpu")
|
| 338 |
+
parser.add_argument("--voice", default=None, help="voice name or speaker id, e.g. clara")
|
| 339 |
+
parser.add_argument(
|
| 340 |
+
"--blend",
|
| 341 |
+
default=None,
|
| 342 |
+
help='morph between voices, e.g. "clara=0.7,silas=0.3" (overrides --voice)',
|
| 343 |
+
)
|
| 344 |
+
parser.add_argument("--speed", type=float, default=1.0)
|
| 345 |
+
parser.add_argument("--variation", type=float, default=0.667)
|
| 346 |
+
parser.add_argument("--seed", type=int, default=0)
|
| 347 |
+
args = parser.parse_args()
|
| 348 |
+
engine = InflectTTS(args.model_dir, args.device)
|
| 349 |
+
engine.save(
|
| 350 |
+
args.text,
|
| 351 |
+
args.output,
|
| 352 |
+
voice=args.voice,
|
| 353 |
+
blend=parse_blend(args.blend) if args.blend else None,
|
| 354 |
+
speed=args.speed,
|
| 355 |
+
variation=args.variation,
|
| 356 |
+
seed=args.seed,
|
| 357 |
+
)
|
| 358 |
+
print(f"wrote {args.output} at {engine.sample_rate} Hz")
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
if __name__ == "__main__":
|
| 362 |
+
main()
|
inflect_nano_v2_frontend.py
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
import os
|
| 6 |
+
import re
|
| 7 |
+
import sys
|
| 8 |
+
from dataclasses import asdict, dataclass
|
| 9 |
+
from datetime import date
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
from num2words import num2words
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
MONTHS = [
|
| 16 |
+
"January",
|
| 17 |
+
"February",
|
| 18 |
+
"March",
|
| 19 |
+
"April",
|
| 20 |
+
"May",
|
| 21 |
+
"June",
|
| 22 |
+
"July",
|
| 23 |
+
"August",
|
| 24 |
+
"September",
|
| 25 |
+
"October",
|
| 26 |
+
"November",
|
| 27 |
+
"December",
|
| 28 |
+
]
|
| 29 |
+
|
| 30 |
+
WORD_OVERRIDES = {
|
| 31 |
+
"Qwen3": "Qwen three",
|
| 32 |
+
"Qwen": "Qwen",
|
| 33 |
+
"PyTorch": "pie torch",
|
| 34 |
+
"SQLite": "ess cue lite",
|
| 35 |
+
"USB-C": "you ess bee see",
|
| 36 |
+
"RTX 3060": "ar tee ex thirty sixty",
|
| 37 |
+
"RTX 3090": "ar tee ex thirty ninety",
|
| 38 |
+
"RTX 4090": "ar tee ex forty ninety",
|
| 39 |
+
"RTX 5080": "ar tee ex fifty eighty",
|
| 40 |
+
"RTX 5090": "ar tee ex fifty ninety",
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
LETTER_NAMES = {
|
| 44 |
+
"A": "ay",
|
| 45 |
+
"B": "bee",
|
| 46 |
+
"C": "see",
|
| 47 |
+
"D": "dee",
|
| 48 |
+
"E": "ee",
|
| 49 |
+
"F": "eff",
|
| 50 |
+
"G": "gee",
|
| 51 |
+
"H": "aitch",
|
| 52 |
+
"I": "eye",
|
| 53 |
+
"J": "jay",
|
| 54 |
+
"K": "kay",
|
| 55 |
+
"L": "ell",
|
| 56 |
+
"M": "em",
|
| 57 |
+
"N": "en",
|
| 58 |
+
"O": "oh",
|
| 59 |
+
"P": "pee",
|
| 60 |
+
"Q": "cue",
|
| 61 |
+
"R": "ar",
|
| 62 |
+
"S": "ess",
|
| 63 |
+
"T": "tee",
|
| 64 |
+
"U": "you",
|
| 65 |
+
"V": "vee",
|
| 66 |
+
"W": "double you",
|
| 67 |
+
"X": "ex",
|
| 68 |
+
"Y": "why",
|
| 69 |
+
"Z": "zee",
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
ABBREVIATIONS = {
|
| 73 |
+
"Dr.": "doctor",
|
| 74 |
+
"Mr.": "mister",
|
| 75 |
+
"Mrs.": "missus",
|
| 76 |
+
"Ms.": "miss",
|
| 77 |
+
"Prof.": "professor",
|
| 78 |
+
"St.": "saint",
|
| 79 |
+
"vs.": "versus",
|
| 80 |
+
"etc.": "et cetera",
|
| 81 |
+
"e.g.": "for example",
|
| 82 |
+
"i.e.": "that is",
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
PUNCT_TRANSLATION = str.maketrans(
|
| 86 |
+
{
|
| 87 |
+
"\u2018": "'",
|
| 88 |
+
"\u2019": "'",
|
| 89 |
+
"\u201c": '"',
|
| 90 |
+
"\u201d": '"',
|
| 91 |
+
"\u2013": "-",
|
| 92 |
+
"\u2014": ", ",
|
| 93 |
+
"\u2026": "...",
|
| 94 |
+
"(": ", ",
|
| 95 |
+
")": ", ",
|
| 96 |
+
"[": ", ",
|
| 97 |
+
"]": ", ",
|
| 98 |
+
"{": ", ",
|
| 99 |
+
"}": ", ",
|
| 100 |
+
}
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
_ESPEAK_CONFIGURED = False
|
| 104 |
+
_ESPEAK_BACKEND = None
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
@dataclass
|
| 108 |
+
class FrontendOutput:
|
| 109 |
+
raw_text: str
|
| 110 |
+
normalized_text: str
|
| 111 |
+
phoneme_text: str
|
| 112 |
+
tokens: list[str]
|
| 113 |
+
token_count: int
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def _words(value: int | float, *, ordinal: bool = False) -> str:
|
| 117 |
+
if ordinal:
|
| 118 |
+
text = num2words(value, to="ordinal")
|
| 119 |
+
else:
|
| 120 |
+
text = num2words(value)
|
| 121 |
+
return text.replace("-", " ").replace(",", "")
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def _digit_words(text: str) -> str:
|
| 125 |
+
return " ".join(_words(int(ch)) for ch in text if ch.isdigit())
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def _identifier_digits(text: str) -> str:
|
| 129 |
+
words = []
|
| 130 |
+
for index, character in enumerate(text):
|
| 131 |
+
if not character.isdigit():
|
| 132 |
+
continue
|
| 133 |
+
words.append("oh" if character == "0" and index > 0 else _words(int(character)))
|
| 134 |
+
return " ".join(words)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def _expand_identifier_token(token: str) -> str:
|
| 138 |
+
match = re.fullmatch(r"([A-Za-z]?)(\d+)([A-Za-z]?)", token)
|
| 139 |
+
if match is None:
|
| 140 |
+
return token
|
| 141 |
+
prefix, digits, suffix = match.groups()
|
| 142 |
+
pieces = []
|
| 143 |
+
if prefix:
|
| 144 |
+
pieces.append(LETTER_NAMES[prefix.upper()])
|
| 145 |
+
if len(digits) == 3 or digits.startswith("0"):
|
| 146 |
+
pieces.append(_identifier_digits(digits))
|
| 147 |
+
else:
|
| 148 |
+
pieces.append(_words(int(digits)))
|
| 149 |
+
if suffix:
|
| 150 |
+
pieces.append(LETTER_NAMES[suffix.upper()])
|
| 151 |
+
return " ".join(pieces)
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def _expand_labeled_identifier(match: re.Match[str]) -> str:
|
| 155 |
+
return f"{match.group(1)} {_expand_identifier_token(match.group(2))}"
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def _expand_street_number(match: re.Match[str]) -> str:
|
| 159 |
+
return _identifier_digits(match.group(1))
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def _expand_money(match: re.Match[str]) -> str:
|
| 163 |
+
raw = match.group(1).replace(",", "")
|
| 164 |
+
dollars, _, cents = raw.partition(".")
|
| 165 |
+
dollar_count = int(dollars)
|
| 166 |
+
parts = [_words(dollar_count), "dollar" if dollar_count == 1 else "dollars"]
|
| 167 |
+
if cents:
|
| 168 |
+
cents = cents[:2].ljust(2, "0")
|
| 169 |
+
cent_count = int(cents)
|
| 170 |
+
if cent_count:
|
| 171 |
+
parts.extend(["and", _words(cent_count), "cent" if cent_count == 1 else "cents"])
|
| 172 |
+
return " ".join(parts)
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def _expand_date_slash(match: re.Match[str]) -> str:
|
| 176 |
+
month = int(match.group(1))
|
| 177 |
+
day = int(match.group(2))
|
| 178 |
+
year = int(match.group(3))
|
| 179 |
+
try:
|
| 180 |
+
date(year, month, day)
|
| 181 |
+
except ValueError:
|
| 182 |
+
return match.group(0)
|
| 183 |
+
return f"{MONTHS[month - 1]} {_words(day, ordinal=True)} {_words(year)}"
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def _expand_time(match: re.Match[str]) -> str:
|
| 187 |
+
hour = int(match.group(1))
|
| 188 |
+
minute = int(match.group(2))
|
| 189 |
+
suffix = match.group(3) or ""
|
| 190 |
+
pieces = [_words(hour)]
|
| 191 |
+
if minute == 0:
|
| 192 |
+
pieces.append("o clock")
|
| 193 |
+
elif minute < 10:
|
| 194 |
+
pieces.extend(["oh", _words(minute)])
|
| 195 |
+
else:
|
| 196 |
+
pieces.append(_words(minute))
|
| 197 |
+
if suffix:
|
| 198 |
+
suffix = suffix.lower().replace(".", "")
|
| 199 |
+
pieces.extend(list(suffix))
|
| 200 |
+
return " ".join(pieces)
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def _expand_bare_hour_time(match: re.Match[str]) -> str:
|
| 204 |
+
hour = int(match.group(1))
|
| 205 |
+
suffix = re.sub(r"[^A-Za-z]", "", match.group(2)).lower()
|
| 206 |
+
return f"{_words(hour)} {' '.join(suffix)}"
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def _expand_version(match: re.Match[str]) -> str:
|
| 210 |
+
return " point ".join(_words(int(part)) for part in match.group(0).split("."))
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def _expand_decimal(match: re.Match[str]) -> str:
|
| 214 |
+
whole, frac = match.group(1), match.group(2)
|
| 215 |
+
return f"{_words(int(whole))} point {_digit_words(frac)}"
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def _expand_ordinal(match: re.Match[str]) -> str:
|
| 219 |
+
return _words(int(match.group(1)), ordinal=True)
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def _expand_number(match: re.Match[str]) -> str:
|
| 223 |
+
value = match.group(0).replace(",", "")
|
| 224 |
+
if len(value) >= 5 and not value.startswith("20"):
|
| 225 |
+
return _digit_words(value)
|
| 226 |
+
return _words(int(value))
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def _expand_phone(match: re.Match[str]) -> str:
|
| 230 |
+
left, right = match.group(1), match.group(2)
|
| 231 |
+
return f"{_digit_words(left)}, {_digit_words(right)}"
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def _expand_acronym(match: re.Match[str]) -> str:
|
| 235 |
+
acronym = match.group(0)
|
| 236 |
+
if len(acronym) <= 1:
|
| 237 |
+
return acronym
|
| 238 |
+
return " ".join(LETTER_NAMES.get(ch, ch) for ch in acronym)
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def normalize_text(text: str) -> str:
|
| 242 |
+
text = text.translate(PUNCT_TRANSLATION)
|
| 243 |
+
text = re.sub(r"\s+", " ", text).strip()
|
| 244 |
+
|
| 245 |
+
for src, dst in WORD_OVERRIDES.items():
|
| 246 |
+
text = re.sub(rf"\b{re.escape(src)}\b", dst, text)
|
| 247 |
+
for src, dst in ABBREVIATIONS.items():
|
| 248 |
+
text = re.sub(rf"\b{re.escape(src)}", dst, text, flags=re.IGNORECASE)
|
| 249 |
+
|
| 250 |
+
text = re.sub(r"\b([A-Z])(?:\.([A-Z]))+\.", lambda m: " ".join(re.findall(r"[A-Z]", m.group(0))), text)
|
| 251 |
+
text = re.sub(
|
| 252 |
+
r"\b(apartment|apt\.?|suite|unit|room|flight|extension|order|invoice|locker|aisle|gate)\s+([A-Za-z]?\d{1,4}[A-Za-z]?)\b",
|
| 253 |
+
_expand_labeled_identifier,
|
| 254 |
+
text,
|
| 255 |
+
flags=re.IGNORECASE,
|
| 256 |
+
)
|
| 257 |
+
text = re.sub(
|
| 258 |
+
r"\b(\d{3})(?=\s+(?:North|South|East|West)\b)",
|
| 259 |
+
_expand_street_number,
|
| 260 |
+
text,
|
| 261 |
+
flags=re.IGNORECASE,
|
| 262 |
+
)
|
| 263 |
+
text = re.sub(r"\$(\d[\d,]*(?:\.\d{1,2})?)", _expand_money, text)
|
| 264 |
+
text = re.sub(r"\b(0?[1-9]|1[0-2])/(0?[1-9]|[12]\d|3[01])/(20\d{2}|19\d{2})\b", _expand_date_slash, text)
|
| 265 |
+
text = re.sub(r"\b(\d{1,2}):(\d{2})\s*([AaPp]\.?\s*[Mm]\.?)?\b", _expand_time, text)
|
| 266 |
+
text = re.sub(r"\b(\d{1,2})\s*([AaPp]\.?\s*[Mm]\.?)\b", _expand_bare_hour_time, text)
|
| 267 |
+
text = re.sub(r"\b(\d{3})-(\d{4})\b", _expand_phone, text)
|
| 268 |
+
text = re.sub(r"\b\d+(?:\.\d+){2,}\b", _expand_version, text)
|
| 269 |
+
text = re.sub(r"\b(\d+)\.(\d+)\b", _expand_decimal, text)
|
| 270 |
+
text = re.sub(r"\b(\d+)(st|nd|rd|th)\b", _expand_ordinal, text, flags=re.IGNORECASE)
|
| 271 |
+
text = re.sub(r"\b\d[\d,]*\b", _expand_number, text)
|
| 272 |
+
text = re.sub(r"\b[A-Z]{2,}\b", _expand_acronym, text)
|
| 273 |
+
text = re.sub(r",(?:\s*,)+", ",", text)
|
| 274 |
+
text = re.sub(r",\s*([.!?])", r"\1", text)
|
| 275 |
+
text = re.sub(r"\s+([,;:.!?])", r"\1", text)
|
| 276 |
+
text = re.sub(r"([,;:.!?])(?=\S)", r"\1 ", text)
|
| 277 |
+
return re.sub(r"\s+", " ", text).strip()
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
def _configure_espeak() -> None:
|
| 281 |
+
global _ESPEAK_CONFIGURED
|
| 282 |
+
if _ESPEAK_CONFIGURED:
|
| 283 |
+
return
|
| 284 |
+
|
| 285 |
+
# Prefer the persistent distro library for long Linux preprocessing jobs.
|
| 286 |
+
# espeakng-loader extracts a temporary shared object, which can exhaust mmap
|
| 287 |
+
# resources when phonemizer repeatedly creates backends over a large corpus.
|
| 288 |
+
system_libraries = (
|
| 289 |
+
Path("/usr/lib/x86_64-linux-gnu/libespeak-ng.so.1"),
|
| 290 |
+
Path("/usr/lib/aarch64-linux-gnu/libespeak-ng.so.1"),
|
| 291 |
+
Path("/usr/lib64/libespeak-ng.so.1"),
|
| 292 |
+
)
|
| 293 |
+
system_library = next((path for path in system_libraries if path.is_file()), None)
|
| 294 |
+
if system_library is not None:
|
| 295 |
+
os.environ.setdefault("PHONEMIZER_ESPEAK_LIBRARY", str(system_library))
|
| 296 |
+
else:
|
| 297 |
+
import espeakng_loader
|
| 298 |
+
|
| 299 |
+
os.environ.setdefault("PHONEMIZER_ESPEAK_LIBRARY", espeakng_loader.get_library_path())
|
| 300 |
+
os.environ.setdefault("ESPEAK_DATA_PATH", espeakng_loader.get_data_path())
|
| 301 |
+
espeakng_loader.make_library_available()
|
| 302 |
+
espeakng_loader.load_library()
|
| 303 |
+
_ESPEAK_CONFIGURED = True
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def phonemize_normalized_text(normalized_text: str) -> str:
|
| 307 |
+
global _ESPEAK_BACKEND
|
| 308 |
+
_configure_espeak()
|
| 309 |
+
from phonemizer.backend import EspeakBackend
|
| 310 |
+
from phonemizer.separator import Separator
|
| 311 |
+
|
| 312 |
+
if _ESPEAK_BACKEND is None:
|
| 313 |
+
_ESPEAK_BACKEND = EspeakBackend(
|
| 314 |
+
language="en-us",
|
| 315 |
+
preserve_punctuation=True,
|
| 316 |
+
with_stress=True,
|
| 317 |
+
language_switch="remove-flags",
|
| 318 |
+
)
|
| 319 |
+
return _ESPEAK_BACKEND.phonemize(
|
| 320 |
+
[normalized_text],
|
| 321 |
+
separator=Separator(phone=" ", word=" | ", syllable=""),
|
| 322 |
+
strip=True,
|
| 323 |
+
njobs=1,
|
| 324 |
+
)[0]
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
def tokenize_phoneme_text(phoneme_text: str) -> list[str]:
|
| 328 |
+
text = phoneme_text.replace("|", " <word> ")
|
| 329 |
+
text = re.sub(r"([,;:.!?])", r" \1 ", text)
|
| 330 |
+
tokens = [tok for tok in re.split(r"\s+", text.strip()) if tok]
|
| 331 |
+
return tokens
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
def run_frontend(text: str) -> FrontendOutput:
|
| 335 |
+
normalized = normalize_text(text)
|
| 336 |
+
phoneme_text = phonemize_normalized_text(normalized)
|
| 337 |
+
tokens = tokenize_phoneme_text(phoneme_text)
|
| 338 |
+
return FrontendOutput(
|
| 339 |
+
raw_text=text,
|
| 340 |
+
normalized_text=normalized,
|
| 341 |
+
phoneme_text=phoneme_text,
|
| 342 |
+
tokens=tokens,
|
| 343 |
+
token_count=len(tokens),
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
def _iter_input_rows(path: Path):
|
| 348 |
+
with path.open("r", encoding="utf-8") as f:
|
| 349 |
+
for line_number, line in enumerate(f, 1):
|
| 350 |
+
line = line.strip()
|
| 351 |
+
if not line:
|
| 352 |
+
continue
|
| 353 |
+
if line.startswith("{"):
|
| 354 |
+
row = json.loads(line)
|
| 355 |
+
text = row.get("target_text") or row.get("text") or row.get("source_text")
|
| 356 |
+
if not text:
|
| 357 |
+
raise ValueError(f"No text field found at {path}:{line_number}")
|
| 358 |
+
yield row, str(text)
|
| 359 |
+
else:
|
| 360 |
+
yield {"line_number": line_number}, line
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
def main() -> None:
|
| 364 |
+
if hasattr(sys.stdout, "reconfigure"):
|
| 365 |
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
| 366 |
+
if hasattr(sys.stderr, "reconfigure"):
|
| 367 |
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
| 368 |
+
|
| 369 |
+
ap = argparse.ArgumentParser(description="Inflect-Nano-v2 English text frontend.")
|
| 370 |
+
ap.add_argument("--text", help="Single text string to normalize and phonemize.")
|
| 371 |
+
ap.add_argument("--input", type=Path, help="Text file or JSONL to process.")
|
| 372 |
+
ap.add_argument("--out", type=Path, help="Output JSONL path for --input.")
|
| 373 |
+
args = ap.parse_args()
|
| 374 |
+
|
| 375 |
+
if bool(args.text) == bool(args.input):
|
| 376 |
+
raise SystemExit("Provide exactly one of --text or --input.")
|
| 377 |
+
|
| 378 |
+
if args.text:
|
| 379 |
+
print(json.dumps(asdict(run_frontend(args.text)), ensure_ascii=False, indent=2))
|
| 380 |
+
return
|
| 381 |
+
|
| 382 |
+
if not args.out:
|
| 383 |
+
raise SystemExit("--out is required with --input.")
|
| 384 |
+
|
| 385 |
+
args.out.parent.mkdir(parents=True, exist_ok=True)
|
| 386 |
+
with args.out.open("w", encoding="utf-8") as f:
|
| 387 |
+
for metadata, text in _iter_input_rows(args.input):
|
| 388 |
+
result = asdict(run_frontend(text))
|
| 389 |
+
result["metadata"] = metadata
|
| 390 |
+
f.write(json.dumps(result, ensure_ascii=False) + "\n")
|
| 391 |
+
print(f"Wrote {args.out}")
|
| 392 |
+
|
| 393 |
+
|
| 394 |
+
if __name__ == "__main__":
|
| 395 |
+
main()
|
inflect_vits_frontend.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
|
| 6 |
+
from inflect_nano_v2_frontend import _configure_espeak, normalize_text
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
# eSpeak is the general fallback. This table contains verified exceptions only;
|
| 10 |
+
# every entry is covered by a regression test and listening audit.
|
| 11 |
+
PHONEME_OVERRIDES = {
|
| 12 |
+
"sˈæskɐtʃˌuːən": "sɐskˈætʃəwən",
|
| 13 |
+
"flʊɹɹˈɛsənt": "flʊˈɹɛsənt",
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@dataclass(frozen=True)
|
| 18 |
+
class VitsFrontendOutput:
|
| 19 |
+
raw_text: str
|
| 20 |
+
normalized_text: str
|
| 21 |
+
phoneme_text: str
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def phonemize_normalized(normalized_text: str) -> str:
|
| 25 |
+
return phonemize_normalized_batch([normalized_text], jobs=1)[0]
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _apply_phoneme_overrides(phoneme_text: str) -> str:
|
| 29 |
+
for source, replacement in PHONEME_OVERRIDES.items():
|
| 30 |
+
phoneme_text = phoneme_text.replace(source, replacement)
|
| 31 |
+
return re.sub(r"\s+", " ", phoneme_text).strip()
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def phonemize_normalized_batch(normalized_texts: list[str], *, jobs: int = 1) -> list[str]:
|
| 35 |
+
if not normalized_texts:
|
| 36 |
+
return []
|
| 37 |
+
_configure_espeak()
|
| 38 |
+
from phonemizer import phonemize
|
| 39 |
+
|
| 40 |
+
phoneme_texts = phonemize(
|
| 41 |
+
normalized_texts,
|
| 42 |
+
language="en-us",
|
| 43 |
+
backend="espeak",
|
| 44 |
+
strip=True,
|
| 45 |
+
preserve_punctuation=True,
|
| 46 |
+
with_stress=True,
|
| 47 |
+
njobs=jobs,
|
| 48 |
+
)
|
| 49 |
+
return [_apply_phoneme_overrides(text) for text in phoneme_texts]
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def run_vits_frontend_batch(texts: list[str], *, jobs: int = 1) -> list[VitsFrontendOutput]:
|
| 53 |
+
normalized = [normalize_text(text) for text in texts]
|
| 54 |
+
phonemes = phonemize_normalized_batch(normalized, jobs=jobs)
|
| 55 |
+
return [
|
| 56 |
+
VitsFrontendOutput(raw_text=raw, normalized_text=norm, phoneme_text=phones)
|
| 57 |
+
for raw, norm, phones in zip(texts, normalized, phonemes, strict=True)
|
| 58 |
+
]
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def run_vits_frontend(text: str) -> VitsFrontendOutput:
|
| 62 |
+
normalized = normalize_text(text)
|
| 63 |
+
return VitsFrontendOutput(
|
| 64 |
+
raw_text=text,
|
| 65 |
+
normalized_text=normalized,
|
| 66 |
+
phoneme_text=phonemize_normalized(normalized),
|
| 67 |
+
)
|
model.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e6cd2fb4940cad8c6bba0d4cc04ac9cefca7f012e22e54e657c29011c16cf0a7
|
| 3 |
+
size 41136135
|
requirements-tested.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Exact package versions used to validate this release.
|
| 2 |
+
# Install the CPU or CUDA PyTorch wheel appropriate for your platform first.
|
| 3 |
+
torch==2.13.0
|
| 4 |
+
numpy==2.5.1
|
| 5 |
+
scipy==1.18.0
|
| 6 |
+
soundfile==0.14.0
|
| 7 |
+
phonemizer==3.3.0
|
| 8 |
+
num2words==0.5.14
|
| 9 |
+
Unidecode==1.4.0
|
| 10 |
+
# phonemizer needs an espeak-ng backend. This release was validated against a
|
| 11 |
+
# system espeak-ng install; the espeakng-loader wheel works too if you prefer
|
| 12 |
+
# not to install one system-wide.
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch>=2.6
|
| 2 |
+
numpy>=1.26,<3
|
| 3 |
+
scipy>=1.13
|
| 4 |
+
soundfile>=0.13
|
| 5 |
+
phonemizer>=3.3
|
| 6 |
+
espeakng-loader>=0.2.4
|
| 7 |
+
num2words>=0.5.14
|
| 8 |
+
Unidecode>=1.3.8
|
runtime/attentions.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import copy
|
| 2 |
+
import math
|
| 3 |
+
import numpy as np
|
| 4 |
+
import torch
|
| 5 |
+
from torch import nn
|
| 6 |
+
from torch.nn import functional as F
|
| 7 |
+
|
| 8 |
+
import commons
|
| 9 |
+
import modules
|
| 10 |
+
from modules import LayerNorm
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class Encoder(nn.Module):
|
| 14 |
+
def __init__(self, hidden_channels, filter_channels, n_heads, n_layers, kernel_size=1, p_dropout=0., window_size=4, **kwargs):
|
| 15 |
+
super().__init__()
|
| 16 |
+
self.hidden_channels = hidden_channels
|
| 17 |
+
self.filter_channels = filter_channels
|
| 18 |
+
self.n_heads = n_heads
|
| 19 |
+
self.n_layers = n_layers
|
| 20 |
+
self.kernel_size = kernel_size
|
| 21 |
+
self.p_dropout = p_dropout
|
| 22 |
+
self.window_size = window_size
|
| 23 |
+
|
| 24 |
+
self.drop = nn.Dropout(p_dropout)
|
| 25 |
+
self.attn_layers = nn.ModuleList()
|
| 26 |
+
self.norm_layers_1 = nn.ModuleList()
|
| 27 |
+
self.ffn_layers = nn.ModuleList()
|
| 28 |
+
self.norm_layers_2 = nn.ModuleList()
|
| 29 |
+
for i in range(self.n_layers):
|
| 30 |
+
self.attn_layers.append(MultiHeadAttention(hidden_channels, hidden_channels, n_heads, p_dropout=p_dropout, window_size=window_size))
|
| 31 |
+
self.norm_layers_1.append(LayerNorm(hidden_channels))
|
| 32 |
+
self.ffn_layers.append(FFN(hidden_channels, hidden_channels, filter_channels, kernel_size, p_dropout=p_dropout))
|
| 33 |
+
self.norm_layers_2.append(LayerNorm(hidden_channels))
|
| 34 |
+
|
| 35 |
+
def forward(self, x, x_mask):
|
| 36 |
+
attn_mask = x_mask.unsqueeze(2) * x_mask.unsqueeze(-1)
|
| 37 |
+
x = x * x_mask
|
| 38 |
+
for i in range(self.n_layers):
|
| 39 |
+
y = self.attn_layers[i](x, x, attn_mask)
|
| 40 |
+
y = self.drop(y)
|
| 41 |
+
x = self.norm_layers_1[i](x + y)
|
| 42 |
+
|
| 43 |
+
y = self.ffn_layers[i](x, x_mask)
|
| 44 |
+
y = self.drop(y)
|
| 45 |
+
x = self.norm_layers_2[i](x + y)
|
| 46 |
+
x = x * x_mask
|
| 47 |
+
return x
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class Decoder(nn.Module):
|
| 51 |
+
def __init__(self, hidden_channels, filter_channels, n_heads, n_layers, kernel_size=1, p_dropout=0., proximal_bias=False, proximal_init=True, **kwargs):
|
| 52 |
+
super().__init__()
|
| 53 |
+
self.hidden_channels = hidden_channels
|
| 54 |
+
self.filter_channels = filter_channels
|
| 55 |
+
self.n_heads = n_heads
|
| 56 |
+
self.n_layers = n_layers
|
| 57 |
+
self.kernel_size = kernel_size
|
| 58 |
+
self.p_dropout = p_dropout
|
| 59 |
+
self.proximal_bias = proximal_bias
|
| 60 |
+
self.proximal_init = proximal_init
|
| 61 |
+
|
| 62 |
+
self.drop = nn.Dropout(p_dropout)
|
| 63 |
+
self.self_attn_layers = nn.ModuleList()
|
| 64 |
+
self.norm_layers_0 = nn.ModuleList()
|
| 65 |
+
self.encdec_attn_layers = nn.ModuleList()
|
| 66 |
+
self.norm_layers_1 = nn.ModuleList()
|
| 67 |
+
self.ffn_layers = nn.ModuleList()
|
| 68 |
+
self.norm_layers_2 = nn.ModuleList()
|
| 69 |
+
for i in range(self.n_layers):
|
| 70 |
+
self.self_attn_layers.append(MultiHeadAttention(hidden_channels, hidden_channels, n_heads, p_dropout=p_dropout, proximal_bias=proximal_bias, proximal_init=proximal_init))
|
| 71 |
+
self.norm_layers_0.append(LayerNorm(hidden_channels))
|
| 72 |
+
self.encdec_attn_layers.append(MultiHeadAttention(hidden_channels, hidden_channels, n_heads, p_dropout=p_dropout))
|
| 73 |
+
self.norm_layers_1.append(LayerNorm(hidden_channels))
|
| 74 |
+
self.ffn_layers.append(FFN(hidden_channels, hidden_channels, filter_channels, kernel_size, p_dropout=p_dropout, causal=True))
|
| 75 |
+
self.norm_layers_2.append(LayerNorm(hidden_channels))
|
| 76 |
+
|
| 77 |
+
def forward(self, x, x_mask, h, h_mask):
|
| 78 |
+
"""
|
| 79 |
+
x: decoder input
|
| 80 |
+
h: encoder output
|
| 81 |
+
"""
|
| 82 |
+
self_attn_mask = commons.subsequent_mask(x_mask.size(2)).to(device=x.device, dtype=x.dtype)
|
| 83 |
+
encdec_attn_mask = h_mask.unsqueeze(2) * x_mask.unsqueeze(-1)
|
| 84 |
+
x = x * x_mask
|
| 85 |
+
for i in range(self.n_layers):
|
| 86 |
+
y = self.self_attn_layers[i](x, x, self_attn_mask)
|
| 87 |
+
y = self.drop(y)
|
| 88 |
+
x = self.norm_layers_0[i](x + y)
|
| 89 |
+
|
| 90 |
+
y = self.encdec_attn_layers[i](x, h, encdec_attn_mask)
|
| 91 |
+
y = self.drop(y)
|
| 92 |
+
x = self.norm_layers_1[i](x + y)
|
| 93 |
+
|
| 94 |
+
y = self.ffn_layers[i](x, x_mask)
|
| 95 |
+
y = self.drop(y)
|
| 96 |
+
x = self.norm_layers_2[i](x + y)
|
| 97 |
+
x = x * x_mask
|
| 98 |
+
return x
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
class MultiHeadAttention(nn.Module):
|
| 102 |
+
def __init__(self, channels, out_channels, n_heads, p_dropout=0., window_size=None, heads_share=True, block_length=None, proximal_bias=False, proximal_init=False):
|
| 103 |
+
super().__init__()
|
| 104 |
+
assert channels % n_heads == 0
|
| 105 |
+
|
| 106 |
+
self.channels = channels
|
| 107 |
+
self.out_channels = out_channels
|
| 108 |
+
self.n_heads = n_heads
|
| 109 |
+
self.p_dropout = p_dropout
|
| 110 |
+
self.window_size = window_size
|
| 111 |
+
self.heads_share = heads_share
|
| 112 |
+
self.block_length = block_length
|
| 113 |
+
self.proximal_bias = proximal_bias
|
| 114 |
+
self.proximal_init = proximal_init
|
| 115 |
+
self.attn = None
|
| 116 |
+
|
| 117 |
+
self.k_channels = channels // n_heads
|
| 118 |
+
self.conv_q = nn.Conv1d(channels, channels, 1)
|
| 119 |
+
self.conv_k = nn.Conv1d(channels, channels, 1)
|
| 120 |
+
self.conv_v = nn.Conv1d(channels, channels, 1)
|
| 121 |
+
self.conv_o = nn.Conv1d(channels, out_channels, 1)
|
| 122 |
+
self.drop = nn.Dropout(p_dropout)
|
| 123 |
+
|
| 124 |
+
if window_size is not None:
|
| 125 |
+
n_heads_rel = 1 if heads_share else n_heads
|
| 126 |
+
rel_stddev = self.k_channels**-0.5
|
| 127 |
+
self.emb_rel_k = nn.Parameter(torch.randn(n_heads_rel, window_size * 2 + 1, self.k_channels) * rel_stddev)
|
| 128 |
+
self.emb_rel_v = nn.Parameter(torch.randn(n_heads_rel, window_size * 2 + 1, self.k_channels) * rel_stddev)
|
| 129 |
+
|
| 130 |
+
nn.init.xavier_uniform_(self.conv_q.weight)
|
| 131 |
+
nn.init.xavier_uniform_(self.conv_k.weight)
|
| 132 |
+
nn.init.xavier_uniform_(self.conv_v.weight)
|
| 133 |
+
if proximal_init:
|
| 134 |
+
with torch.no_grad():
|
| 135 |
+
self.conv_k.weight.copy_(self.conv_q.weight)
|
| 136 |
+
self.conv_k.bias.copy_(self.conv_q.bias)
|
| 137 |
+
|
| 138 |
+
def forward(self, x, c, attn_mask=None):
|
| 139 |
+
q = self.conv_q(x)
|
| 140 |
+
k = self.conv_k(c)
|
| 141 |
+
v = self.conv_v(c)
|
| 142 |
+
|
| 143 |
+
x, self.attn = self.attention(q, k, v, mask=attn_mask)
|
| 144 |
+
|
| 145 |
+
x = self.conv_o(x)
|
| 146 |
+
return x
|
| 147 |
+
|
| 148 |
+
def attention(self, query, key, value, mask=None):
|
| 149 |
+
# reshape [b, d, t] -> [b, n_h, t, d_k]
|
| 150 |
+
b, d, t_s, t_t = (*key.size(), query.size(2))
|
| 151 |
+
query = query.view(b, self.n_heads, self.k_channels, t_t).transpose(2, 3)
|
| 152 |
+
key = key.view(b, self.n_heads, self.k_channels, t_s).transpose(2, 3)
|
| 153 |
+
value = value.view(b, self.n_heads, self.k_channels, t_s).transpose(2, 3)
|
| 154 |
+
|
| 155 |
+
scores = torch.matmul(query / math.sqrt(self.k_channels), key.transpose(-2, -1))
|
| 156 |
+
if self.window_size is not None:
|
| 157 |
+
assert t_s == t_t, "Relative attention is only available for self-attention."
|
| 158 |
+
key_relative_embeddings = self._get_relative_embeddings(self.emb_rel_k, t_s)
|
| 159 |
+
rel_logits = self._matmul_with_relative_keys(query /math.sqrt(self.k_channels), key_relative_embeddings)
|
| 160 |
+
scores_local = self._relative_position_to_absolute_position(rel_logits)
|
| 161 |
+
scores = scores + scores_local
|
| 162 |
+
if self.proximal_bias:
|
| 163 |
+
assert t_s == t_t, "Proximal bias is only available for self-attention."
|
| 164 |
+
scores = scores + self._attention_bias_proximal(t_s).to(device=scores.device, dtype=scores.dtype)
|
| 165 |
+
if mask is not None:
|
| 166 |
+
scores = scores.masked_fill(mask == 0, -1e4)
|
| 167 |
+
if self.block_length is not None:
|
| 168 |
+
assert t_s == t_t, "Local attention is only available for self-attention."
|
| 169 |
+
block_mask = torch.ones_like(scores).triu(-self.block_length).tril(self.block_length)
|
| 170 |
+
scores = scores.masked_fill(block_mask == 0, -1e4)
|
| 171 |
+
p_attn = F.softmax(scores, dim=-1) # [b, n_h, t_t, t_s]
|
| 172 |
+
p_attn = self.drop(p_attn)
|
| 173 |
+
output = torch.matmul(p_attn, value)
|
| 174 |
+
if self.window_size is not None:
|
| 175 |
+
relative_weights = self._absolute_position_to_relative_position(p_attn)
|
| 176 |
+
value_relative_embeddings = self._get_relative_embeddings(self.emb_rel_v, t_s)
|
| 177 |
+
output = output + self._matmul_with_relative_values(relative_weights, value_relative_embeddings)
|
| 178 |
+
output = output.transpose(2, 3).contiguous().view(b, d, t_t) # [b, n_h, t_t, d_k] -> [b, d, t_t]
|
| 179 |
+
return output, p_attn
|
| 180 |
+
|
| 181 |
+
def _matmul_with_relative_values(self, x, y):
|
| 182 |
+
"""
|
| 183 |
+
x: [b, h, l, m]
|
| 184 |
+
y: [h or 1, m, d]
|
| 185 |
+
ret: [b, h, l, d]
|
| 186 |
+
"""
|
| 187 |
+
ret = torch.matmul(x, y.unsqueeze(0))
|
| 188 |
+
return ret
|
| 189 |
+
|
| 190 |
+
def _matmul_with_relative_keys(self, x, y):
|
| 191 |
+
"""
|
| 192 |
+
x: [b, h, l, d]
|
| 193 |
+
y: [h or 1, m, d]
|
| 194 |
+
ret: [b, h, l, m]
|
| 195 |
+
"""
|
| 196 |
+
ret = torch.matmul(x, y.unsqueeze(0).transpose(-2, -1))
|
| 197 |
+
return ret
|
| 198 |
+
|
| 199 |
+
def _get_relative_embeddings(self, relative_embeddings, length):
|
| 200 |
+
max_relative_position = 2 * self.window_size + 1
|
| 201 |
+
# Pad first before slice to avoid using cond ops.
|
| 202 |
+
pad_length = max(length - (self.window_size + 1), 0)
|
| 203 |
+
slice_start_position = max((self.window_size + 1) - length, 0)
|
| 204 |
+
slice_end_position = slice_start_position + 2 * length - 1
|
| 205 |
+
if pad_length > 0:
|
| 206 |
+
padded_relative_embeddings = F.pad(
|
| 207 |
+
relative_embeddings,
|
| 208 |
+
commons.convert_pad_shape([[0, 0], [pad_length, pad_length], [0, 0]]))
|
| 209 |
+
else:
|
| 210 |
+
padded_relative_embeddings = relative_embeddings
|
| 211 |
+
used_relative_embeddings = padded_relative_embeddings[:,slice_start_position:slice_end_position]
|
| 212 |
+
return used_relative_embeddings
|
| 213 |
+
|
| 214 |
+
def _relative_position_to_absolute_position(self, x):
|
| 215 |
+
"""
|
| 216 |
+
x: [b, h, l, 2*l-1]
|
| 217 |
+
ret: [b, h, l, l]
|
| 218 |
+
"""
|
| 219 |
+
batch, heads, length, _ = x.size()
|
| 220 |
+
# Concat columns of pad to shift from relative to absolute indexing.
|
| 221 |
+
x = F.pad(x, commons.convert_pad_shape([[0,0],[0,0],[0,0],[0,1]]))
|
| 222 |
+
|
| 223 |
+
# Concat extra elements so to add up to shape (len+1, 2*len-1).
|
| 224 |
+
x_flat = x.view([batch, heads, length * 2 * length])
|
| 225 |
+
x_flat = F.pad(x_flat, commons.convert_pad_shape([[0,0],[0,0],[0,length-1]]))
|
| 226 |
+
|
| 227 |
+
# Reshape and slice out the padded elements.
|
| 228 |
+
x_final = x_flat.view([batch, heads, length+1, 2*length-1])[:, :, :length, length-1:]
|
| 229 |
+
return x_final
|
| 230 |
+
|
| 231 |
+
def _absolute_position_to_relative_position(self, x):
|
| 232 |
+
"""
|
| 233 |
+
x: [b, h, l, l]
|
| 234 |
+
ret: [b, h, l, 2*l-1]
|
| 235 |
+
"""
|
| 236 |
+
batch, heads, length, _ = x.size()
|
| 237 |
+
# padd along column
|
| 238 |
+
x = F.pad(x, commons.convert_pad_shape([[0, 0], [0, 0], [0, 0], [0, length-1]]))
|
| 239 |
+
x_flat = x.view([batch, heads, length**2 + length*(length -1)])
|
| 240 |
+
# add 0's in the beginning that will skew the elements after reshape
|
| 241 |
+
x_flat = F.pad(x_flat, commons.convert_pad_shape([[0, 0], [0, 0], [length, 0]]))
|
| 242 |
+
x_final = x_flat.view([batch, heads, length, 2*length])[:,:,:,1:]
|
| 243 |
+
return x_final
|
| 244 |
+
|
| 245 |
+
def _attention_bias_proximal(self, length):
|
| 246 |
+
"""Bias for self-attention to encourage attention to close positions.
|
| 247 |
+
Args:
|
| 248 |
+
length: an integer scalar.
|
| 249 |
+
Returns:
|
| 250 |
+
a Tensor with shape [1, 1, length, length]
|
| 251 |
+
"""
|
| 252 |
+
r = torch.arange(length, dtype=torch.float32)
|
| 253 |
+
diff = torch.unsqueeze(r, 0) - torch.unsqueeze(r, 1)
|
| 254 |
+
return torch.unsqueeze(torch.unsqueeze(-torch.log1p(torch.abs(diff)), 0), 0)
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
class FFN(nn.Module):
|
| 258 |
+
def __init__(self, in_channels, out_channels, filter_channels, kernel_size, p_dropout=0., activation=None, causal=False):
|
| 259 |
+
super().__init__()
|
| 260 |
+
self.in_channels = in_channels
|
| 261 |
+
self.out_channels = out_channels
|
| 262 |
+
self.filter_channels = filter_channels
|
| 263 |
+
self.kernel_size = kernel_size
|
| 264 |
+
self.p_dropout = p_dropout
|
| 265 |
+
self.activation = activation
|
| 266 |
+
self.causal = causal
|
| 267 |
+
|
| 268 |
+
if causal:
|
| 269 |
+
self.padding = self._causal_padding
|
| 270 |
+
else:
|
| 271 |
+
self.padding = self._same_padding
|
| 272 |
+
|
| 273 |
+
self.conv_1 = nn.Conv1d(in_channels, filter_channels, kernel_size)
|
| 274 |
+
self.conv_2 = nn.Conv1d(filter_channels, out_channels, kernel_size)
|
| 275 |
+
self.drop = nn.Dropout(p_dropout)
|
| 276 |
+
|
| 277 |
+
def forward(self, x, x_mask):
|
| 278 |
+
x = self.conv_1(self.padding(x * x_mask))
|
| 279 |
+
if self.activation == "gelu":
|
| 280 |
+
x = x * torch.sigmoid(1.702 * x)
|
| 281 |
+
else:
|
| 282 |
+
x = torch.relu(x)
|
| 283 |
+
x = self.drop(x)
|
| 284 |
+
x = self.conv_2(self.padding(x * x_mask))
|
| 285 |
+
return x * x_mask
|
| 286 |
+
|
| 287 |
+
def _causal_padding(self, x):
|
| 288 |
+
if self.kernel_size == 1:
|
| 289 |
+
return x
|
| 290 |
+
pad_l = self.kernel_size - 1
|
| 291 |
+
pad_r = 0
|
| 292 |
+
padding = [[0, 0], [0, 0], [pad_l, pad_r]]
|
| 293 |
+
x = F.pad(x, commons.convert_pad_shape(padding))
|
| 294 |
+
return x
|
| 295 |
+
|
| 296 |
+
def _same_padding(self, x):
|
| 297 |
+
if self.kernel_size == 1:
|
| 298 |
+
return x
|
| 299 |
+
pad_l = (self.kernel_size - 1) // 2
|
| 300 |
+
pad_r = self.kernel_size // 2
|
| 301 |
+
padding = [[0, 0], [0, 0], [pad_l, pad_r]]
|
| 302 |
+
x = F.pad(x, commons.convert_pad_shape(padding))
|
| 303 |
+
return x
|
runtime/commons.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import numpy as np
|
| 3 |
+
import torch
|
| 4 |
+
from torch import nn
|
| 5 |
+
from torch.nn import functional as F
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def init_weights(m, mean=0.0, std=0.01):
|
| 9 |
+
classname = m.__class__.__name__
|
| 10 |
+
if classname.find("Conv") != -1:
|
| 11 |
+
m.weight.data.normal_(mean, std)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def get_padding(kernel_size, dilation=1):
|
| 15 |
+
return int((kernel_size*dilation - dilation)/2)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def convert_pad_shape(pad_shape):
|
| 19 |
+
l = pad_shape[::-1]
|
| 20 |
+
pad_shape = [item for sublist in l for item in sublist]
|
| 21 |
+
return pad_shape
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def intersperse(lst, item):
|
| 25 |
+
result = [item] * (len(lst) * 2 + 1)
|
| 26 |
+
result[1::2] = lst
|
| 27 |
+
return result
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def kl_divergence(m_p, logs_p, m_q, logs_q):
|
| 31 |
+
"""KL(P||Q)"""
|
| 32 |
+
kl = (logs_q - logs_p) - 0.5
|
| 33 |
+
kl += 0.5 * (torch.exp(2. * logs_p) + ((m_p - m_q)**2)) * torch.exp(-2. * logs_q)
|
| 34 |
+
return kl
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def rand_gumbel(shape):
|
| 38 |
+
"""Sample from the Gumbel distribution, protect from overflows."""
|
| 39 |
+
uniform_samples = torch.rand(shape) * 0.99998 + 0.00001
|
| 40 |
+
return -torch.log(-torch.log(uniform_samples))
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def rand_gumbel_like(x):
|
| 44 |
+
g = rand_gumbel(x.size()).to(dtype=x.dtype, device=x.device)
|
| 45 |
+
return g
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def slice_segments(x, ids_str, segment_size=4):
|
| 49 |
+
ret = torch.zeros_like(x[:, :, :segment_size])
|
| 50 |
+
for i in range(x.size(0)):
|
| 51 |
+
idx_str = ids_str[i]
|
| 52 |
+
idx_end = idx_str + segment_size
|
| 53 |
+
ret[i] = x[i, :, idx_str:idx_end]
|
| 54 |
+
return ret
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def rand_slice_segments(x, x_lengths=None, segment_size=4):
|
| 58 |
+
b, d, t = x.size()
|
| 59 |
+
if x_lengths is None:
|
| 60 |
+
x_lengths = t
|
| 61 |
+
ids_str_max = x_lengths - segment_size + 1
|
| 62 |
+
ids_str = (torch.rand([b]).to(device=x.device) * ids_str_max).to(dtype=torch.long)
|
| 63 |
+
ret = slice_segments(x, ids_str, segment_size)
|
| 64 |
+
return ret, ids_str
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def get_timing_signal_1d(
|
| 68 |
+
length, channels, min_timescale=1.0, max_timescale=1.0e4):
|
| 69 |
+
position = torch.arange(length, dtype=torch.float)
|
| 70 |
+
num_timescales = channels // 2
|
| 71 |
+
log_timescale_increment = (
|
| 72 |
+
math.log(float(max_timescale) / float(min_timescale)) /
|
| 73 |
+
(num_timescales - 1))
|
| 74 |
+
inv_timescales = min_timescale * torch.exp(
|
| 75 |
+
torch.arange(num_timescales, dtype=torch.float) * -log_timescale_increment)
|
| 76 |
+
scaled_time = position.unsqueeze(0) * inv_timescales.unsqueeze(1)
|
| 77 |
+
signal = torch.cat([torch.sin(scaled_time), torch.cos(scaled_time)], 0)
|
| 78 |
+
signal = F.pad(signal, [0, 0, 0, channels % 2])
|
| 79 |
+
signal = signal.view(1, channels, length)
|
| 80 |
+
return signal
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def add_timing_signal_1d(x, min_timescale=1.0, max_timescale=1.0e4):
|
| 84 |
+
b, channels, length = x.size()
|
| 85 |
+
signal = get_timing_signal_1d(length, channels, min_timescale, max_timescale)
|
| 86 |
+
return x + signal.to(dtype=x.dtype, device=x.device)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def cat_timing_signal_1d(x, min_timescale=1.0, max_timescale=1.0e4, axis=1):
|
| 90 |
+
b, channels, length = x.size()
|
| 91 |
+
signal = get_timing_signal_1d(length, channels, min_timescale, max_timescale)
|
| 92 |
+
return torch.cat([x, signal.to(dtype=x.dtype, device=x.device)], axis)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def subsequent_mask(length):
|
| 96 |
+
mask = torch.tril(torch.ones(length, length)).unsqueeze(0).unsqueeze(0)
|
| 97 |
+
return mask
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
@torch.jit.script
|
| 101 |
+
def fused_add_tanh_sigmoid_multiply(input_a, input_b, n_channels):
|
| 102 |
+
n_channels_int = n_channels[0]
|
| 103 |
+
in_act = input_a + input_b
|
| 104 |
+
t_act = torch.tanh(in_act[:, :n_channels_int, :])
|
| 105 |
+
s_act = torch.sigmoid(in_act[:, n_channels_int:, :])
|
| 106 |
+
acts = t_act * s_act
|
| 107 |
+
return acts
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def convert_pad_shape(pad_shape):
|
| 111 |
+
l = pad_shape[::-1]
|
| 112 |
+
pad_shape = [item for sublist in l for item in sublist]
|
| 113 |
+
return pad_shape
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def shift_1d(x):
|
| 117 |
+
x = F.pad(x, convert_pad_shape([[0, 0], [0, 0], [1, 0]]))[:, :, :-1]
|
| 118 |
+
return x
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def sequence_mask(length, max_length=None):
|
| 122 |
+
if max_length is None:
|
| 123 |
+
max_length = length.max()
|
| 124 |
+
x = torch.arange(max_length, dtype=length.dtype, device=length.device)
|
| 125 |
+
return x.unsqueeze(0) < length.unsqueeze(1)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def generate_path(duration, mask):
|
| 129 |
+
"""
|
| 130 |
+
duration: [b, 1, t_x]
|
| 131 |
+
mask: [b, 1, t_y, t_x]
|
| 132 |
+
"""
|
| 133 |
+
device = duration.device
|
| 134 |
+
|
| 135 |
+
b, _, t_y, t_x = mask.shape
|
| 136 |
+
cum_duration = torch.cumsum(duration, -1)
|
| 137 |
+
|
| 138 |
+
cum_duration_flat = cum_duration.view(b * t_x)
|
| 139 |
+
path = sequence_mask(cum_duration_flat, t_y).to(mask.dtype)
|
| 140 |
+
path = path.view(b, t_x, t_y)
|
| 141 |
+
path = path - F.pad(path, convert_pad_shape([[0, 0], [1, 0], [0, 0]]))[:, :-1]
|
| 142 |
+
path = path.unsqueeze(1).transpose(2,3) * mask
|
| 143 |
+
return path
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def clip_grad_value_(parameters, clip_value, norm_type=2):
|
| 147 |
+
if isinstance(parameters, torch.Tensor):
|
| 148 |
+
parameters = [parameters]
|
| 149 |
+
parameters = list(filter(lambda p: p.grad is not None, parameters))
|
| 150 |
+
norm_type = float(norm_type)
|
| 151 |
+
if clip_value is not None:
|
| 152 |
+
clip_value = float(clip_value)
|
| 153 |
+
|
| 154 |
+
total_norm = 0
|
| 155 |
+
for p in parameters:
|
| 156 |
+
param_norm = p.grad.data.norm(norm_type)
|
| 157 |
+
total_norm += param_norm.item() ** norm_type
|
| 158 |
+
if clip_value is not None:
|
| 159 |
+
p.grad.data.clamp_(min=-clip_value, max=clip_value)
|
| 160 |
+
total_norm = total_norm ** (1. / norm_type)
|
| 161 |
+
return total_norm
|
runtime/inflect_alias_free.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Lightweight alias-free waveform blocks derived from NVIDIA BigVGAN.
|
| 2 |
+
|
| 3 |
+
BigVGAN and alias-free-torch are MIT/Apache-2.0 licensed. The implementation
|
| 4 |
+
is kept local so Inflect can train without BigVGAN's optional CUDA extension.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import math
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
from torch import nn
|
| 11 |
+
from torch.nn import functional as F
|
| 12 |
+
from torch.nn.utils import remove_weight_norm, weight_norm
|
| 13 |
+
|
| 14 |
+
from commons import get_padding, init_weights
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def kaiser_sinc_filter1d(cutoff: float, half_width: float, kernel_size: int):
|
| 18 |
+
even = kernel_size % 2 == 0
|
| 19 |
+
half_size = kernel_size // 2
|
| 20 |
+
delta_f = 4 * half_width
|
| 21 |
+
attenuation = 2.285 * (half_size - 1) * math.pi * delta_f + 7.95
|
| 22 |
+
if attenuation > 50.0:
|
| 23 |
+
beta = 0.1102 * (attenuation - 8.7)
|
| 24 |
+
elif attenuation >= 21.0:
|
| 25 |
+
beta = 0.5842 * (attenuation - 21) ** 0.4 + 0.07886 * (attenuation - 21.0)
|
| 26 |
+
else:
|
| 27 |
+
beta = 0.0
|
| 28 |
+
window = torch.kaiser_window(kernel_size, beta=beta, periodic=False)
|
| 29 |
+
if even:
|
| 30 |
+
time = torch.arange(-half_size, half_size) + 0.5
|
| 31 |
+
else:
|
| 32 |
+
time = torch.arange(kernel_size) - half_size
|
| 33 |
+
values = 2 * cutoff * window * torch.sinc(2 * cutoff * time)
|
| 34 |
+
values /= values.sum()
|
| 35 |
+
return values.view(1, 1, kernel_size)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class UpSample1d(nn.Module):
|
| 39 |
+
def __init__(self, ratio=2, kernel_size=12):
|
| 40 |
+
super().__init__()
|
| 41 |
+
self.ratio = ratio
|
| 42 |
+
self.stride = ratio
|
| 43 |
+
self.kernel_size = kernel_size
|
| 44 |
+
self.pad = kernel_size // ratio - 1
|
| 45 |
+
self.pad_left = self.pad * ratio + (kernel_size - ratio) // 2
|
| 46 |
+
self.pad_right = self.pad * ratio + (kernel_size - ratio + 1) // 2
|
| 47 |
+
self.register_buffer(
|
| 48 |
+
"filter",
|
| 49 |
+
kaiser_sinc_filter1d(0.5 / ratio, 0.6 / ratio, kernel_size))
|
| 50 |
+
|
| 51 |
+
def forward(self, x):
|
| 52 |
+
channels = x.shape[1]
|
| 53 |
+
x = F.pad(x, (self.pad, self.pad), mode="replicate")
|
| 54 |
+
x = self.ratio * F.conv_transpose1d(
|
| 55 |
+
x, self.filter.expand(channels, -1, -1),
|
| 56 |
+
stride=self.stride, groups=channels)
|
| 57 |
+
return x[..., self.pad_left:-self.pad_right]
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class DownSample1d(nn.Module):
|
| 61 |
+
def __init__(self, ratio=2, kernel_size=12):
|
| 62 |
+
super().__init__()
|
| 63 |
+
self.ratio = ratio
|
| 64 |
+
self.kernel_size = kernel_size
|
| 65 |
+
self.pad_left = kernel_size // 2 - int(kernel_size % 2 == 0)
|
| 66 |
+
self.pad_right = kernel_size // 2
|
| 67 |
+
self.register_buffer(
|
| 68 |
+
"filter",
|
| 69 |
+
kaiser_sinc_filter1d(0.5 / ratio, 0.6 / ratio, kernel_size))
|
| 70 |
+
|
| 71 |
+
def forward(self, x):
|
| 72 |
+
channels = x.shape[1]
|
| 73 |
+
x = F.pad(x, (self.pad_left, self.pad_right), mode="replicate")
|
| 74 |
+
return F.conv1d(
|
| 75 |
+
x, self.filter.expand(channels, -1, -1),
|
| 76 |
+
stride=self.ratio, groups=channels)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class SnakeBeta(nn.Module):
|
| 80 |
+
def __init__(self, channels: int, logscale: bool = True):
|
| 81 |
+
super().__init__()
|
| 82 |
+
initial = torch.zeros(channels) if logscale else torch.ones(channels)
|
| 83 |
+
self.alpha = nn.Parameter(initial.clone())
|
| 84 |
+
self.beta = nn.Parameter(initial.clone())
|
| 85 |
+
self.logscale = logscale
|
| 86 |
+
|
| 87 |
+
def forward(self, x):
|
| 88 |
+
alpha = self.alpha.view(1, -1, 1)
|
| 89 |
+
beta = self.beta.view(1, -1, 1)
|
| 90 |
+
if self.logscale:
|
| 91 |
+
alpha = alpha.exp()
|
| 92 |
+
beta = beta.exp()
|
| 93 |
+
return x + torch.sin(x * alpha).square() / (beta + 1e-9)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
class AliasFreeActivation1d(nn.Module):
|
| 97 |
+
def __init__(self, activation: nn.Module):
|
| 98 |
+
super().__init__()
|
| 99 |
+
self.upsample = UpSample1d()
|
| 100 |
+
self.act = activation
|
| 101 |
+
self.downsample = DownSample1d()
|
| 102 |
+
|
| 103 |
+
def forward(self, x):
|
| 104 |
+
return self.downsample(self.act(self.upsample(x)))
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
class AliasFreeResBlock1(nn.Module):
|
| 108 |
+
"""Shape-compatible VITS ResBlock1 with filtered SnakeBeta activations."""
|
| 109 |
+
|
| 110 |
+
def __init__(self, channels, kernel_size=3, dilation=(1, 3, 5), logscale=True):
|
| 111 |
+
super().__init__()
|
| 112 |
+
self.convs1 = nn.ModuleList([
|
| 113 |
+
weight_norm(nn.Conv1d(
|
| 114 |
+
channels, channels, kernel_size, 1,
|
| 115 |
+
dilation=d, padding=get_padding(kernel_size, d)))
|
| 116 |
+
for d in dilation
|
| 117 |
+
])
|
| 118 |
+
self.convs2 = nn.ModuleList([
|
| 119 |
+
weight_norm(nn.Conv1d(
|
| 120 |
+
channels, channels, kernel_size, 1,
|
| 121 |
+
dilation=1, padding=get_padding(kernel_size, 1)))
|
| 122 |
+
for _ in dilation
|
| 123 |
+
])
|
| 124 |
+
self.convs1.apply(init_weights)
|
| 125 |
+
self.convs2.apply(init_weights)
|
| 126 |
+
self.activations = nn.ModuleList([
|
| 127 |
+
AliasFreeActivation1d(SnakeBeta(channels, logscale=logscale))
|
| 128 |
+
for _ in range(2 * len(dilation))
|
| 129 |
+
])
|
| 130 |
+
|
| 131 |
+
def forward(self, x, x_mask=None):
|
| 132 |
+
first = self.activations[::2]
|
| 133 |
+
second = self.activations[1::2]
|
| 134 |
+
for conv1, conv2, act1, act2 in zip(self.convs1, self.convs2, first, second):
|
| 135 |
+
residual = conv2(act2(conv1(act1(x))))
|
| 136 |
+
x = x + residual
|
| 137 |
+
return x
|
| 138 |
+
|
| 139 |
+
def remove_weight_norm(self):
|
| 140 |
+
for layer in self.convs1:
|
| 141 |
+
remove_weight_norm(layer)
|
| 142 |
+
for layer in self.convs2:
|
| 143 |
+
remove_weight_norm(layer)
|
runtime/models.py
ADDED
|
@@ -0,0 +1,571 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import copy
|
| 2 |
+
import math
|
| 3 |
+
import torch
|
| 4 |
+
from torch import nn
|
| 5 |
+
from torch.nn import functional as F
|
| 6 |
+
|
| 7 |
+
import commons
|
| 8 |
+
import modules
|
| 9 |
+
from inflect_alias_free import AliasFreeActivation1d, AliasFreeResBlock1, SnakeBeta
|
| 10 |
+
import attentions
|
| 11 |
+
import monotonic_align
|
| 12 |
+
|
| 13 |
+
from torch.nn import Conv1d, ConvTranspose1d, AvgPool1d, Conv2d
|
| 14 |
+
from torch.nn.utils import weight_norm, remove_weight_norm, spectral_norm
|
| 15 |
+
from commons import init_weights, get_padding
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class StochasticDurationPredictor(nn.Module):
|
| 19 |
+
def __init__(self, in_channels, filter_channels, kernel_size, p_dropout, n_flows=4, gin_channels=0):
|
| 20 |
+
super().__init__()
|
| 21 |
+
filter_channels = in_channels # it needs to be removed from future version.
|
| 22 |
+
self.in_channels = in_channels
|
| 23 |
+
self.filter_channels = filter_channels
|
| 24 |
+
self.kernel_size = kernel_size
|
| 25 |
+
self.p_dropout = p_dropout
|
| 26 |
+
self.n_flows = n_flows
|
| 27 |
+
self.gin_channels = gin_channels
|
| 28 |
+
|
| 29 |
+
self.log_flow = modules.Log()
|
| 30 |
+
self.flows = nn.ModuleList()
|
| 31 |
+
self.flows.append(modules.ElementwiseAffine(2))
|
| 32 |
+
for i in range(n_flows):
|
| 33 |
+
self.flows.append(modules.ConvFlow(2, filter_channels, kernel_size, n_layers=3))
|
| 34 |
+
self.flows.append(modules.Flip())
|
| 35 |
+
|
| 36 |
+
self.post_pre = nn.Conv1d(1, filter_channels, 1)
|
| 37 |
+
self.post_proj = nn.Conv1d(filter_channels, filter_channels, 1)
|
| 38 |
+
self.post_convs = modules.DDSConv(filter_channels, kernel_size, n_layers=3, p_dropout=p_dropout)
|
| 39 |
+
self.post_flows = nn.ModuleList()
|
| 40 |
+
self.post_flows.append(modules.ElementwiseAffine(2))
|
| 41 |
+
for i in range(4):
|
| 42 |
+
self.post_flows.append(modules.ConvFlow(2, filter_channels, kernel_size, n_layers=3))
|
| 43 |
+
self.post_flows.append(modules.Flip())
|
| 44 |
+
|
| 45 |
+
self.pre = nn.Conv1d(in_channels, filter_channels, 1)
|
| 46 |
+
self.proj = nn.Conv1d(filter_channels, filter_channels, 1)
|
| 47 |
+
self.convs = modules.DDSConv(filter_channels, kernel_size, n_layers=3, p_dropout=p_dropout)
|
| 48 |
+
if gin_channels != 0:
|
| 49 |
+
self.cond = nn.Conv1d(gin_channels, filter_channels, 1)
|
| 50 |
+
|
| 51 |
+
def forward(self, x, x_mask, w=None, g=None, reverse=False, noise_scale=1.0):
|
| 52 |
+
x = torch.detach(x)
|
| 53 |
+
x = self.pre(x)
|
| 54 |
+
if g is not None:
|
| 55 |
+
g = torch.detach(g)
|
| 56 |
+
x = x + self.cond(g)
|
| 57 |
+
x = self.convs(x, x_mask)
|
| 58 |
+
x = self.proj(x) * x_mask
|
| 59 |
+
|
| 60 |
+
if not reverse:
|
| 61 |
+
flows = self.flows
|
| 62 |
+
assert w is not None
|
| 63 |
+
|
| 64 |
+
logdet_tot_q = 0
|
| 65 |
+
h_w = self.post_pre(w)
|
| 66 |
+
h_w = self.post_convs(h_w, x_mask)
|
| 67 |
+
h_w = self.post_proj(h_w) * x_mask
|
| 68 |
+
e_q = torch.randn(w.size(0), 2, w.size(2)).to(device=x.device, dtype=x.dtype) * x_mask
|
| 69 |
+
z_q = e_q
|
| 70 |
+
for flow in self.post_flows:
|
| 71 |
+
z_q, logdet_q = flow(z_q, x_mask, g=(x + h_w))
|
| 72 |
+
logdet_tot_q += logdet_q
|
| 73 |
+
z_u, z1 = torch.split(z_q, [1, 1], 1)
|
| 74 |
+
u = torch.sigmoid(z_u) * x_mask
|
| 75 |
+
z0 = (w - u) * x_mask
|
| 76 |
+
logdet_tot_q += torch.sum((F.logsigmoid(z_u) + F.logsigmoid(-z_u)) * x_mask, [1,2])
|
| 77 |
+
logq = torch.sum(-0.5 * (math.log(2*math.pi) + (e_q**2)) * x_mask, [1,2]) - logdet_tot_q
|
| 78 |
+
|
| 79 |
+
logdet_tot = 0
|
| 80 |
+
z0, logdet = self.log_flow(z0, x_mask)
|
| 81 |
+
logdet_tot += logdet
|
| 82 |
+
z = torch.cat([z0, z1], 1)
|
| 83 |
+
for flow in flows:
|
| 84 |
+
z, logdet = flow(z, x_mask, g=x, reverse=reverse)
|
| 85 |
+
logdet_tot = logdet_tot + logdet
|
| 86 |
+
nll = torch.sum(0.5 * (math.log(2*math.pi) + (z**2)) * x_mask, [1,2]) - logdet_tot
|
| 87 |
+
return nll + logq # [b]
|
| 88 |
+
else:
|
| 89 |
+
flows = list(reversed(self.flows))
|
| 90 |
+
flows = flows[:-2] + [flows[-1]] # remove a useless vflow
|
| 91 |
+
z = torch.randn(x.size(0), 2, x.size(2)).to(device=x.device, dtype=x.dtype) * noise_scale
|
| 92 |
+
for flow in flows:
|
| 93 |
+
z = flow(z, x_mask, g=x, reverse=reverse)
|
| 94 |
+
z0, z1 = torch.split(z, [1, 1], 1)
|
| 95 |
+
logw = z0
|
| 96 |
+
return logw
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
class DurationPredictor(nn.Module):
|
| 100 |
+
def __init__(self, in_channels, filter_channels, kernel_size, p_dropout, gin_channels=0):
|
| 101 |
+
super().__init__()
|
| 102 |
+
|
| 103 |
+
self.in_channels = in_channels
|
| 104 |
+
self.filter_channels = filter_channels
|
| 105 |
+
self.kernel_size = kernel_size
|
| 106 |
+
self.p_dropout = p_dropout
|
| 107 |
+
self.gin_channels = gin_channels
|
| 108 |
+
|
| 109 |
+
self.drop = nn.Dropout(p_dropout)
|
| 110 |
+
self.conv_1 = nn.Conv1d(in_channels, filter_channels, kernel_size, padding=kernel_size//2)
|
| 111 |
+
self.norm_1 = modules.LayerNorm(filter_channels)
|
| 112 |
+
self.conv_2 = nn.Conv1d(filter_channels, filter_channels, kernel_size, padding=kernel_size//2)
|
| 113 |
+
self.norm_2 = modules.LayerNorm(filter_channels)
|
| 114 |
+
self.proj = nn.Conv1d(filter_channels, 1, 1)
|
| 115 |
+
|
| 116 |
+
if gin_channels != 0:
|
| 117 |
+
self.cond = nn.Conv1d(gin_channels, in_channels, 1)
|
| 118 |
+
|
| 119 |
+
def forward(self, x, x_mask, g=None):
|
| 120 |
+
x = torch.detach(x)
|
| 121 |
+
if g is not None:
|
| 122 |
+
g = torch.detach(g)
|
| 123 |
+
x = x + self.cond(g)
|
| 124 |
+
x = self.conv_1(x * x_mask)
|
| 125 |
+
x = torch.relu(x)
|
| 126 |
+
x = self.norm_1(x)
|
| 127 |
+
x = self.drop(x)
|
| 128 |
+
x = self.conv_2(x * x_mask)
|
| 129 |
+
x = torch.relu(x)
|
| 130 |
+
x = self.norm_2(x)
|
| 131 |
+
x = self.drop(x)
|
| 132 |
+
x = self.proj(x * x_mask)
|
| 133 |
+
return x * x_mask
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
class TextEncoder(nn.Module):
|
| 137 |
+
def __init__(self,
|
| 138 |
+
n_vocab,
|
| 139 |
+
out_channels,
|
| 140 |
+
hidden_channels,
|
| 141 |
+
filter_channels,
|
| 142 |
+
n_heads,
|
| 143 |
+
n_layers,
|
| 144 |
+
kernel_size,
|
| 145 |
+
p_dropout):
|
| 146 |
+
super().__init__()
|
| 147 |
+
self.n_vocab = n_vocab
|
| 148 |
+
self.out_channels = out_channels
|
| 149 |
+
self.hidden_channels = hidden_channels
|
| 150 |
+
self.filter_channels = filter_channels
|
| 151 |
+
self.n_heads = n_heads
|
| 152 |
+
self.n_layers = n_layers
|
| 153 |
+
self.kernel_size = kernel_size
|
| 154 |
+
self.p_dropout = p_dropout
|
| 155 |
+
|
| 156 |
+
self.emb = nn.Embedding(n_vocab, hidden_channels)
|
| 157 |
+
nn.init.normal_(self.emb.weight, 0.0, hidden_channels**-0.5)
|
| 158 |
+
|
| 159 |
+
self.encoder = attentions.Encoder(
|
| 160 |
+
hidden_channels,
|
| 161 |
+
filter_channels,
|
| 162 |
+
n_heads,
|
| 163 |
+
n_layers,
|
| 164 |
+
kernel_size,
|
| 165 |
+
p_dropout)
|
| 166 |
+
self.proj= nn.Conv1d(hidden_channels, out_channels * 2, 1)
|
| 167 |
+
|
| 168 |
+
def forward(self, x, x_lengths):
|
| 169 |
+
x = self.emb(x) * math.sqrt(self.hidden_channels) # [b, t, h]
|
| 170 |
+
x = torch.transpose(x, 1, -1) # [b, h, t]
|
| 171 |
+
x_mask = torch.unsqueeze(commons.sequence_mask(x_lengths, x.size(2)), 1).to(x.dtype)
|
| 172 |
+
|
| 173 |
+
x = self.encoder(x * x_mask, x_mask)
|
| 174 |
+
stats = self.proj(x) * x_mask
|
| 175 |
+
|
| 176 |
+
m, logs = torch.split(stats, self.out_channels, dim=1)
|
| 177 |
+
return x, m, logs, x_mask
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
class ResidualCouplingBlock(nn.Module):
|
| 181 |
+
def __init__(self,
|
| 182 |
+
channels,
|
| 183 |
+
hidden_channels,
|
| 184 |
+
kernel_size,
|
| 185 |
+
dilation_rate,
|
| 186 |
+
n_layers,
|
| 187 |
+
n_flows=4,
|
| 188 |
+
gin_channels=0):
|
| 189 |
+
super().__init__()
|
| 190 |
+
self.channels = channels
|
| 191 |
+
self.hidden_channels = hidden_channels
|
| 192 |
+
self.kernel_size = kernel_size
|
| 193 |
+
self.dilation_rate = dilation_rate
|
| 194 |
+
self.n_layers = n_layers
|
| 195 |
+
self.n_flows = n_flows
|
| 196 |
+
self.gin_channels = gin_channels
|
| 197 |
+
|
| 198 |
+
self.flows = nn.ModuleList()
|
| 199 |
+
for i in range(n_flows):
|
| 200 |
+
self.flows.append(modules.ResidualCouplingLayer(channels, hidden_channels, kernel_size, dilation_rate, n_layers, gin_channels=gin_channels, mean_only=True))
|
| 201 |
+
self.flows.append(modules.Flip())
|
| 202 |
+
|
| 203 |
+
def forward(self, x, x_mask, g=None, reverse=False):
|
| 204 |
+
if not reverse:
|
| 205 |
+
for flow in self.flows:
|
| 206 |
+
x, _ = flow(x, x_mask, g=g, reverse=reverse)
|
| 207 |
+
else:
|
| 208 |
+
for flow in reversed(self.flows):
|
| 209 |
+
x = flow(x, x_mask, g=g, reverse=reverse)
|
| 210 |
+
return x
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
class PosteriorEncoder(nn.Module):
|
| 214 |
+
def __init__(self,
|
| 215 |
+
in_channels,
|
| 216 |
+
out_channels,
|
| 217 |
+
hidden_channels,
|
| 218 |
+
kernel_size,
|
| 219 |
+
dilation_rate,
|
| 220 |
+
n_layers,
|
| 221 |
+
gin_channels=0):
|
| 222 |
+
super().__init__()
|
| 223 |
+
self.in_channels = in_channels
|
| 224 |
+
self.out_channels = out_channels
|
| 225 |
+
self.hidden_channels = hidden_channels
|
| 226 |
+
self.kernel_size = kernel_size
|
| 227 |
+
self.dilation_rate = dilation_rate
|
| 228 |
+
self.n_layers = n_layers
|
| 229 |
+
self.gin_channels = gin_channels
|
| 230 |
+
|
| 231 |
+
self.pre = nn.Conv1d(in_channels, hidden_channels, 1)
|
| 232 |
+
self.enc = modules.WN(hidden_channels, kernel_size, dilation_rate, n_layers, gin_channels=gin_channels)
|
| 233 |
+
self.proj = nn.Conv1d(hidden_channels, out_channels * 2, 1)
|
| 234 |
+
|
| 235 |
+
def forward(self, x, x_lengths, g=None):
|
| 236 |
+
x_mask = torch.unsqueeze(commons.sequence_mask(x_lengths, x.size(2)), 1).to(x.dtype)
|
| 237 |
+
x = self.pre(x) * x_mask
|
| 238 |
+
x = self.enc(x, x_mask, g=g)
|
| 239 |
+
stats = self.proj(x) * x_mask
|
| 240 |
+
m, logs = torch.split(stats, self.out_channels, dim=1)
|
| 241 |
+
z = (m + torch.randn_like(m) * torch.exp(logs)) * x_mask
|
| 242 |
+
return z, m, logs, x_mask
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
class Generator(torch.nn.Module):
|
| 246 |
+
def __init__(self, initial_channel, resblock, resblock_kernel_sizes, resblock_dilation_sizes, upsample_rates, upsample_initial_channel, upsample_kernel_sizes, gin_channels=0, decoder_alias_free=False, decoder_alias_free_start_stage=2, decoder_snake_logscale=True):
|
| 247 |
+
super(Generator, self).__init__()
|
| 248 |
+
self.num_kernels = len(resblock_kernel_sizes)
|
| 249 |
+
self.num_upsamples = len(upsample_rates)
|
| 250 |
+
self.conv_pre = Conv1d(initial_channel, upsample_initial_channel, 7, 1, padding=3)
|
| 251 |
+
resblock_class = modules.ResBlock1 if resblock == '1' else modules.ResBlock2
|
| 252 |
+
self.decoder_alias_free = bool(decoder_alias_free)
|
| 253 |
+
self.decoder_alias_free_start_stage = int(decoder_alias_free_start_stage)
|
| 254 |
+
|
| 255 |
+
self.ups = nn.ModuleList()
|
| 256 |
+
for i, (u, k) in enumerate(zip(upsample_rates, upsample_kernel_sizes)):
|
| 257 |
+
self.ups.append(weight_norm(
|
| 258 |
+
ConvTranspose1d(upsample_initial_channel//(2**i), upsample_initial_channel//(2**(i+1)),
|
| 259 |
+
k, u, padding=(k-u)//2)))
|
| 260 |
+
|
| 261 |
+
self.resblocks = nn.ModuleList()
|
| 262 |
+
for i in range(len(self.ups)):
|
| 263 |
+
ch = upsample_initial_channel//(2**(i+1))
|
| 264 |
+
for j, (k, d) in enumerate(zip(resblock_kernel_sizes, resblock_dilation_sizes)):
|
| 265 |
+
if self.decoder_alias_free and i >= self.decoder_alias_free_start_stage:
|
| 266 |
+
self.resblocks.append(AliasFreeResBlock1(
|
| 267 |
+
ch, k, d, logscale=decoder_snake_logscale))
|
| 268 |
+
else:
|
| 269 |
+
self.resblocks.append(resblock_class(ch, k, d))
|
| 270 |
+
|
| 271 |
+
self.alias_free_pre_activations = nn.ModuleList()
|
| 272 |
+
if self.decoder_alias_free:
|
| 273 |
+
for i in range(self.num_upsamples):
|
| 274 |
+
channels = upsample_initial_channel // (2 ** i)
|
| 275 |
+
if i >= self.decoder_alias_free_start_stage:
|
| 276 |
+
self.alias_free_pre_activations.append(
|
| 277 |
+
AliasFreeActivation1d(nn.LeakyReLU(modules.LRELU_SLOPE)))
|
| 278 |
+
else:
|
| 279 |
+
self.alias_free_pre_activations.append(nn.Identity())
|
| 280 |
+
self.alias_free_post_activation = AliasFreeActivation1d(
|
| 281 |
+
SnakeBeta(ch, logscale=decoder_snake_logscale))
|
| 282 |
+
|
| 283 |
+
self.conv_post = Conv1d(ch, 1, 7, 1, padding=3, bias=False)
|
| 284 |
+
self.ups.apply(init_weights)
|
| 285 |
+
|
| 286 |
+
if gin_channels != 0:
|
| 287 |
+
self.cond = nn.Conv1d(gin_channels, upsample_initial_channel, 1)
|
| 288 |
+
|
| 289 |
+
def forward(self, x, g=None):
|
| 290 |
+
x = self.conv_pre(x)
|
| 291 |
+
if g is not None:
|
| 292 |
+
x = x + self.cond(g)
|
| 293 |
+
|
| 294 |
+
for i in range(self.num_upsamples):
|
| 295 |
+
if self.decoder_alias_free and i >= self.decoder_alias_free_start_stage:
|
| 296 |
+
x = self.alias_free_pre_activations[i](x)
|
| 297 |
+
else:
|
| 298 |
+
x = F.leaky_relu(x, modules.LRELU_SLOPE)
|
| 299 |
+
x = self.ups[i](x)
|
| 300 |
+
xs = None
|
| 301 |
+
for j in range(self.num_kernels):
|
| 302 |
+
if xs is None:
|
| 303 |
+
xs = self.resblocks[i*self.num_kernels+j](x)
|
| 304 |
+
else:
|
| 305 |
+
xs += self.resblocks[i*self.num_kernels+j](x)
|
| 306 |
+
x = xs / self.num_kernels
|
| 307 |
+
if self.decoder_alias_free:
|
| 308 |
+
x = self.alias_free_post_activation(x)
|
| 309 |
+
else:
|
| 310 |
+
x = F.leaky_relu(x)
|
| 311 |
+
x = self.conv_post(x)
|
| 312 |
+
x = torch.tanh(x)
|
| 313 |
+
|
| 314 |
+
return x
|
| 315 |
+
|
| 316 |
+
def remove_weight_norm(self):
|
| 317 |
+
print('Removing weight norm...')
|
| 318 |
+
for l in self.ups:
|
| 319 |
+
remove_weight_norm(l)
|
| 320 |
+
for l in self.resblocks:
|
| 321 |
+
l.remove_weight_norm()
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
class DiscriminatorP(torch.nn.Module):
|
| 325 |
+
def __init__(self, period, kernel_size=5, stride=3, use_spectral_norm=False):
|
| 326 |
+
super(DiscriminatorP, self).__init__()
|
| 327 |
+
self.period = period
|
| 328 |
+
self.use_spectral_norm = use_spectral_norm
|
| 329 |
+
norm_f = weight_norm if use_spectral_norm == False else spectral_norm
|
| 330 |
+
self.convs = nn.ModuleList([
|
| 331 |
+
norm_f(Conv2d(1, 32, (kernel_size, 1), (stride, 1), padding=(get_padding(kernel_size, 1), 0))),
|
| 332 |
+
norm_f(Conv2d(32, 128, (kernel_size, 1), (stride, 1), padding=(get_padding(kernel_size, 1), 0))),
|
| 333 |
+
norm_f(Conv2d(128, 512, (kernel_size, 1), (stride, 1), padding=(get_padding(kernel_size, 1), 0))),
|
| 334 |
+
norm_f(Conv2d(512, 1024, (kernel_size, 1), (stride, 1), padding=(get_padding(kernel_size, 1), 0))),
|
| 335 |
+
norm_f(Conv2d(1024, 1024, (kernel_size, 1), 1, padding=(get_padding(kernel_size, 1), 0))),
|
| 336 |
+
])
|
| 337 |
+
self.conv_post = norm_f(Conv2d(1024, 1, (3, 1), 1, padding=(1, 0)))
|
| 338 |
+
|
| 339 |
+
def forward(self, x):
|
| 340 |
+
fmap = []
|
| 341 |
+
|
| 342 |
+
# 1d to 2d
|
| 343 |
+
b, c, t = x.shape
|
| 344 |
+
if t % self.period != 0: # pad first
|
| 345 |
+
n_pad = self.period - (t % self.period)
|
| 346 |
+
x = F.pad(x, (0, n_pad), "reflect")
|
| 347 |
+
t = t + n_pad
|
| 348 |
+
x = x.view(b, c, t // self.period, self.period)
|
| 349 |
+
|
| 350 |
+
for l in self.convs:
|
| 351 |
+
x = l(x)
|
| 352 |
+
x = F.leaky_relu(x, modules.LRELU_SLOPE)
|
| 353 |
+
fmap.append(x)
|
| 354 |
+
x = self.conv_post(x)
|
| 355 |
+
fmap.append(x)
|
| 356 |
+
x = torch.flatten(x, 1, -1)
|
| 357 |
+
|
| 358 |
+
return x, fmap
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
class DiscriminatorS(torch.nn.Module):
|
| 362 |
+
def __init__(self, use_spectral_norm=False):
|
| 363 |
+
super(DiscriminatorS, self).__init__()
|
| 364 |
+
norm_f = weight_norm if use_spectral_norm == False else spectral_norm
|
| 365 |
+
self.convs = nn.ModuleList([
|
| 366 |
+
norm_f(Conv1d(1, 16, 15, 1, padding=7)),
|
| 367 |
+
norm_f(Conv1d(16, 64, 41, 4, groups=4, padding=20)),
|
| 368 |
+
norm_f(Conv1d(64, 256, 41, 4, groups=16, padding=20)),
|
| 369 |
+
norm_f(Conv1d(256, 1024, 41, 4, groups=64, padding=20)),
|
| 370 |
+
norm_f(Conv1d(1024, 1024, 41, 4, groups=256, padding=20)),
|
| 371 |
+
norm_f(Conv1d(1024, 1024, 5, 1, padding=2)),
|
| 372 |
+
])
|
| 373 |
+
self.conv_post = norm_f(Conv1d(1024, 1, 3, 1, padding=1))
|
| 374 |
+
|
| 375 |
+
def forward(self, x):
|
| 376 |
+
fmap = []
|
| 377 |
+
|
| 378 |
+
for l in self.convs:
|
| 379 |
+
x = l(x)
|
| 380 |
+
x = F.leaky_relu(x, modules.LRELU_SLOPE)
|
| 381 |
+
fmap.append(x)
|
| 382 |
+
x = self.conv_post(x)
|
| 383 |
+
fmap.append(x)
|
| 384 |
+
x = torch.flatten(x, 1, -1)
|
| 385 |
+
|
| 386 |
+
return x, fmap
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
class MultiPeriodDiscriminator(torch.nn.Module):
|
| 390 |
+
def __init__(self, use_spectral_norm=False):
|
| 391 |
+
super(MultiPeriodDiscriminator, self).__init__()
|
| 392 |
+
periods = [2,3,5,7,11]
|
| 393 |
+
|
| 394 |
+
discs = [DiscriminatorS(use_spectral_norm=use_spectral_norm)]
|
| 395 |
+
discs = discs + [DiscriminatorP(i, use_spectral_norm=use_spectral_norm) for i in periods]
|
| 396 |
+
self.discriminators = nn.ModuleList(discs)
|
| 397 |
+
|
| 398 |
+
def forward(self, y, y_hat):
|
| 399 |
+
y_d_rs = []
|
| 400 |
+
y_d_gs = []
|
| 401 |
+
fmap_rs = []
|
| 402 |
+
fmap_gs = []
|
| 403 |
+
for i, d in enumerate(self.discriminators):
|
| 404 |
+
y_d_r, fmap_r = d(y)
|
| 405 |
+
y_d_g, fmap_g = d(y_hat)
|
| 406 |
+
y_d_rs.append(y_d_r)
|
| 407 |
+
y_d_gs.append(y_d_g)
|
| 408 |
+
fmap_rs.append(fmap_r)
|
| 409 |
+
fmap_gs.append(fmap_g)
|
| 410 |
+
|
| 411 |
+
return y_d_rs, y_d_gs, fmap_rs, fmap_gs
|
| 412 |
+
|
| 413 |
+
|
| 414 |
+
|
| 415 |
+
class SynthesizerTrn(nn.Module):
|
| 416 |
+
"""
|
| 417 |
+
Synthesizer for Training
|
| 418 |
+
"""
|
| 419 |
+
|
| 420 |
+
def __init__(self,
|
| 421 |
+
n_vocab,
|
| 422 |
+
spec_channels,
|
| 423 |
+
segment_size,
|
| 424 |
+
inter_channels,
|
| 425 |
+
hidden_channels,
|
| 426 |
+
filter_channels,
|
| 427 |
+
n_heads,
|
| 428 |
+
n_layers,
|
| 429 |
+
kernel_size,
|
| 430 |
+
p_dropout,
|
| 431 |
+
resblock,
|
| 432 |
+
resblock_kernel_sizes,
|
| 433 |
+
resblock_dilation_sizes,
|
| 434 |
+
upsample_rates,
|
| 435 |
+
upsample_initial_channel,
|
| 436 |
+
upsample_kernel_sizes,
|
| 437 |
+
n_speakers=0,
|
| 438 |
+
gin_channels=0,
|
| 439 |
+
use_sdp=True,
|
| 440 |
+
**kwargs):
|
| 441 |
+
|
| 442 |
+
super().__init__()
|
| 443 |
+
self.n_vocab = n_vocab
|
| 444 |
+
self.spec_channels = spec_channels
|
| 445 |
+
self.inter_channels = inter_channels
|
| 446 |
+
self.hidden_channels = hidden_channels
|
| 447 |
+
self.filter_channels = filter_channels
|
| 448 |
+
self.n_heads = n_heads
|
| 449 |
+
self.n_layers = n_layers
|
| 450 |
+
self.kernel_size = kernel_size
|
| 451 |
+
self.p_dropout = p_dropout
|
| 452 |
+
self.resblock = resblock
|
| 453 |
+
self.resblock_kernel_sizes = resblock_kernel_sizes
|
| 454 |
+
self.resblock_dilation_sizes = resblock_dilation_sizes
|
| 455 |
+
self.upsample_rates = upsample_rates
|
| 456 |
+
self.upsample_initial_channel = upsample_initial_channel
|
| 457 |
+
self.upsample_kernel_sizes = upsample_kernel_sizes
|
| 458 |
+
self.segment_size = segment_size
|
| 459 |
+
self.n_speakers = n_speakers
|
| 460 |
+
self.gin_channels = gin_channels
|
| 461 |
+
|
| 462 |
+
self.use_sdp = use_sdp
|
| 463 |
+
|
| 464 |
+
self.enc_p = TextEncoder(n_vocab,
|
| 465 |
+
inter_channels,
|
| 466 |
+
hidden_channels,
|
| 467 |
+
filter_channels,
|
| 468 |
+
n_heads,
|
| 469 |
+
n_layers,
|
| 470 |
+
kernel_size,
|
| 471 |
+
p_dropout)
|
| 472 |
+
self.dec = Generator(
|
| 473 |
+
inter_channels, resblock, resblock_kernel_sizes,
|
| 474 |
+
resblock_dilation_sizes, upsample_rates, upsample_initial_channel,
|
| 475 |
+
upsample_kernel_sizes, gin_channels=gin_channels,
|
| 476 |
+
decoder_alias_free=kwargs.get("decoder_alias_free", False),
|
| 477 |
+
decoder_alias_free_start_stage=kwargs.get("decoder_alias_free_start_stage", 2),
|
| 478 |
+
decoder_snake_logscale=kwargs.get("decoder_snake_logscale", True))
|
| 479 |
+
self.inference_only = bool(kwargs.get("inference_only", False))
|
| 480 |
+
if not self.inference_only:
|
| 481 |
+
self.enc_q = PosteriorEncoder(spec_channels, inter_channels, hidden_channels, 5, 1, 16, gin_channels=gin_channels)
|
| 482 |
+
self.flow = ResidualCouplingBlock(inter_channels, hidden_channels, 5, 1, 4, gin_channels=gin_channels)
|
| 483 |
+
|
| 484 |
+
if use_sdp:
|
| 485 |
+
self.dp = StochasticDurationPredictor(hidden_channels, 192, 3, 0.5, 4, gin_channels=gin_channels)
|
| 486 |
+
else:
|
| 487 |
+
self.dp = DurationPredictor(hidden_channels, 256, 3, 0.5, gin_channels=gin_channels)
|
| 488 |
+
|
| 489 |
+
if n_speakers > 1:
|
| 490 |
+
self.emb_g = nn.Embedding(n_speakers, gin_channels)
|
| 491 |
+
|
| 492 |
+
def forward(self, x, x_lengths, y, y_lengths, sid=None):
|
| 493 |
+
|
| 494 |
+
if self.inference_only:
|
| 495 |
+
raise RuntimeError("The public runtime is inference-only and has no posterior encoder.")
|
| 496 |
+
|
| 497 |
+
x, m_p, logs_p, x_mask = self.enc_p(x, x_lengths)
|
| 498 |
+
if self.n_speakers > 0:
|
| 499 |
+
g = self.emb_g(sid).unsqueeze(-1) # [b, h, 1]
|
| 500 |
+
else:
|
| 501 |
+
g = None
|
| 502 |
+
|
| 503 |
+
z, m_q, logs_q, y_mask = self.enc_q(y, y_lengths, g=g)
|
| 504 |
+
z_p = self.flow(z, y_mask, g=g)
|
| 505 |
+
|
| 506 |
+
with torch.no_grad():
|
| 507 |
+
# negative cross-entropy
|
| 508 |
+
s_p_sq_r = torch.exp(-2 * logs_p) # [b, d, t]
|
| 509 |
+
neg_cent1 = torch.sum(-0.5 * math.log(2 * math.pi) - logs_p, [1], keepdim=True) # [b, 1, t_s]
|
| 510 |
+
neg_cent2 = torch.matmul(-0.5 * (z_p ** 2).transpose(1, 2), s_p_sq_r) # [b, t_t, d] x [b, d, t_s] = [b, t_t, t_s]
|
| 511 |
+
neg_cent3 = torch.matmul(z_p.transpose(1, 2), (m_p * s_p_sq_r)) # [b, t_t, d] x [b, d, t_s] = [b, t_t, t_s]
|
| 512 |
+
neg_cent4 = torch.sum(-0.5 * (m_p ** 2) * s_p_sq_r, [1], keepdim=True) # [b, 1, t_s]
|
| 513 |
+
neg_cent = neg_cent1 + neg_cent2 + neg_cent3 + neg_cent4
|
| 514 |
+
|
| 515 |
+
attn_mask = torch.unsqueeze(x_mask, 2) * torch.unsqueeze(y_mask, -1)
|
| 516 |
+
attn = monotonic_align.maximum_path(neg_cent, attn_mask.squeeze(1)).unsqueeze(1).detach()
|
| 517 |
+
|
| 518 |
+
w = attn.sum(2)
|
| 519 |
+
if self.use_sdp:
|
| 520 |
+
l_length = self.dp(x, x_mask, w, g=g)
|
| 521 |
+
l_length = l_length / torch.sum(x_mask)
|
| 522 |
+
else:
|
| 523 |
+
logw_ = torch.log(w + 1e-6) * x_mask
|
| 524 |
+
logw = self.dp(x, x_mask, g=g)
|
| 525 |
+
l_length = torch.sum((logw - logw_)**2, [1,2]) / torch.sum(x_mask) # for averaging
|
| 526 |
+
|
| 527 |
+
# expand prior
|
| 528 |
+
m_p = torch.matmul(attn.squeeze(1), m_p.transpose(1, 2)).transpose(1, 2)
|
| 529 |
+
logs_p = torch.matmul(attn.squeeze(1), logs_p.transpose(1, 2)).transpose(1, 2)
|
| 530 |
+
|
| 531 |
+
z_slice, ids_slice = commons.rand_slice_segments(z, y_lengths, self.segment_size)
|
| 532 |
+
o = self.dec(z_slice, g=g)
|
| 533 |
+
return o, l_length, attn, ids_slice, x_mask, y_mask, (z, z_p, m_p, logs_p, m_q, logs_q)
|
| 534 |
+
|
| 535 |
+
def infer(self, x, x_lengths, sid=None, noise_scale=1, length_scale=1, noise_scale_w=1., max_len=None):
|
| 536 |
+
x, m_p, logs_p, x_mask = self.enc_p(x, x_lengths)
|
| 537 |
+
if self.n_speakers > 0:
|
| 538 |
+
g = self.emb_g(sid).unsqueeze(-1) # [b, h, 1]
|
| 539 |
+
else:
|
| 540 |
+
g = None
|
| 541 |
+
|
| 542 |
+
if self.use_sdp:
|
| 543 |
+
logw = self.dp(x, x_mask, g=g, reverse=True, noise_scale=noise_scale_w)
|
| 544 |
+
else:
|
| 545 |
+
logw = self.dp(x, x_mask, g=g)
|
| 546 |
+
w = torch.exp(logw) * x_mask * length_scale
|
| 547 |
+
w_ceil = torch.ceil(w)
|
| 548 |
+
y_lengths = torch.clamp_min(torch.sum(w_ceil, [1, 2]), 1).long()
|
| 549 |
+
y_mask = torch.unsqueeze(commons.sequence_mask(y_lengths, None), 1).to(x_mask.dtype)
|
| 550 |
+
attn_mask = torch.unsqueeze(x_mask, 2) * torch.unsqueeze(y_mask, -1)
|
| 551 |
+
attn = commons.generate_path(w_ceil, attn_mask)
|
| 552 |
+
|
| 553 |
+
m_p = torch.matmul(attn.squeeze(1), m_p.transpose(1, 2)).transpose(1, 2) # [b, t', t], [b, t, d] -> [b, d, t']
|
| 554 |
+
logs_p = torch.matmul(attn.squeeze(1), logs_p.transpose(1, 2)).transpose(1, 2) # [b, t', t], [b, t, d] -> [b, d, t']
|
| 555 |
+
|
| 556 |
+
z_p = m_p + torch.randn_like(m_p) * torch.exp(logs_p) * noise_scale
|
| 557 |
+
z = self.flow(z_p, y_mask, g=g, reverse=True)
|
| 558 |
+
o = self.dec((z * y_mask)[:,:,:max_len], g=g)
|
| 559 |
+
return o, attn, y_mask, (z, z_p, m_p, logs_p)
|
| 560 |
+
|
| 561 |
+
def voice_conversion(self, y, y_lengths, sid_src, sid_tgt):
|
| 562 |
+
if self.inference_only:
|
| 563 |
+
raise RuntimeError("The public runtime is inference-only and does not support voice conversion.")
|
| 564 |
+
assert self.n_speakers > 0, "n_speakers have to be larger than 0."
|
| 565 |
+
g_src = self.emb_g(sid_src).unsqueeze(-1)
|
| 566 |
+
g_tgt = self.emb_g(sid_tgt).unsqueeze(-1)
|
| 567 |
+
z, m_q, logs_q, y_mask = self.enc_q(y, y_lengths, g=g_src)
|
| 568 |
+
z_p = self.flow(z, y_mask, g=g_src)
|
| 569 |
+
z_hat = self.flow(z_p, y_mask, g=g_tgt, reverse=True)
|
| 570 |
+
o_hat = self.dec(z_hat * y_mask, g=g_tgt)
|
| 571 |
+
return o_hat, y_mask, (z, z_p, z_hat)
|
runtime/modules.py
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import copy
|
| 2 |
+
import math
|
| 3 |
+
import numpy as np
|
| 4 |
+
import scipy
|
| 5 |
+
import torch
|
| 6 |
+
from torch import nn
|
| 7 |
+
from torch.nn import functional as F
|
| 8 |
+
|
| 9 |
+
from torch.nn import Conv1d, ConvTranspose1d, AvgPool1d, Conv2d
|
| 10 |
+
from torch.nn.utils import weight_norm, remove_weight_norm
|
| 11 |
+
|
| 12 |
+
import commons
|
| 13 |
+
from commons import init_weights, get_padding
|
| 14 |
+
from transforms import piecewise_rational_quadratic_transform
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
LRELU_SLOPE = 0.1
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class LayerNorm(nn.Module):
|
| 21 |
+
def __init__(self, channels, eps=1e-5):
|
| 22 |
+
super().__init__()
|
| 23 |
+
self.channels = channels
|
| 24 |
+
self.eps = eps
|
| 25 |
+
|
| 26 |
+
self.gamma = nn.Parameter(torch.ones(channels))
|
| 27 |
+
self.beta = nn.Parameter(torch.zeros(channels))
|
| 28 |
+
|
| 29 |
+
def forward(self, x):
|
| 30 |
+
x = x.transpose(1, -1)
|
| 31 |
+
x = F.layer_norm(x, (self.channels,), self.gamma, self.beta, self.eps)
|
| 32 |
+
return x.transpose(1, -1)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class ConvReluNorm(nn.Module):
|
| 36 |
+
def __init__(self, in_channels, hidden_channels, out_channels, kernel_size, n_layers, p_dropout):
|
| 37 |
+
super().__init__()
|
| 38 |
+
self.in_channels = in_channels
|
| 39 |
+
self.hidden_channels = hidden_channels
|
| 40 |
+
self.out_channels = out_channels
|
| 41 |
+
self.kernel_size = kernel_size
|
| 42 |
+
self.n_layers = n_layers
|
| 43 |
+
self.p_dropout = p_dropout
|
| 44 |
+
assert n_layers > 1, "Number of layers should be larger than 0."
|
| 45 |
+
|
| 46 |
+
self.conv_layers = nn.ModuleList()
|
| 47 |
+
self.norm_layers = nn.ModuleList()
|
| 48 |
+
self.conv_layers.append(nn.Conv1d(in_channels, hidden_channels, kernel_size, padding=kernel_size//2))
|
| 49 |
+
self.norm_layers.append(LayerNorm(hidden_channels))
|
| 50 |
+
self.relu_drop = nn.Sequential(
|
| 51 |
+
nn.ReLU(),
|
| 52 |
+
nn.Dropout(p_dropout))
|
| 53 |
+
for _ in range(n_layers-1):
|
| 54 |
+
self.conv_layers.append(nn.Conv1d(hidden_channels, hidden_channels, kernel_size, padding=kernel_size//2))
|
| 55 |
+
self.norm_layers.append(LayerNorm(hidden_channels))
|
| 56 |
+
self.proj = nn.Conv1d(hidden_channels, out_channels, 1)
|
| 57 |
+
self.proj.weight.data.zero_()
|
| 58 |
+
self.proj.bias.data.zero_()
|
| 59 |
+
|
| 60 |
+
def forward(self, x, x_mask):
|
| 61 |
+
x_org = x
|
| 62 |
+
for i in range(self.n_layers):
|
| 63 |
+
x = self.conv_layers[i](x * x_mask)
|
| 64 |
+
x = self.norm_layers[i](x)
|
| 65 |
+
x = self.relu_drop(x)
|
| 66 |
+
x = x_org + self.proj(x)
|
| 67 |
+
return x * x_mask
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class DDSConv(nn.Module):
|
| 71 |
+
"""
|
| 72 |
+
Dialted and Depth-Separable Convolution
|
| 73 |
+
"""
|
| 74 |
+
def __init__(self, channels, kernel_size, n_layers, p_dropout=0.):
|
| 75 |
+
super().__init__()
|
| 76 |
+
self.channels = channels
|
| 77 |
+
self.kernel_size = kernel_size
|
| 78 |
+
self.n_layers = n_layers
|
| 79 |
+
self.p_dropout = p_dropout
|
| 80 |
+
|
| 81 |
+
self.drop = nn.Dropout(p_dropout)
|
| 82 |
+
self.convs_sep = nn.ModuleList()
|
| 83 |
+
self.convs_1x1 = nn.ModuleList()
|
| 84 |
+
self.norms_1 = nn.ModuleList()
|
| 85 |
+
self.norms_2 = nn.ModuleList()
|
| 86 |
+
for i in range(n_layers):
|
| 87 |
+
dilation = kernel_size ** i
|
| 88 |
+
padding = (kernel_size * dilation - dilation) // 2
|
| 89 |
+
self.convs_sep.append(nn.Conv1d(channels, channels, kernel_size,
|
| 90 |
+
groups=channels, dilation=dilation, padding=padding
|
| 91 |
+
))
|
| 92 |
+
self.convs_1x1.append(nn.Conv1d(channels, channels, 1))
|
| 93 |
+
self.norms_1.append(LayerNorm(channels))
|
| 94 |
+
self.norms_2.append(LayerNorm(channels))
|
| 95 |
+
|
| 96 |
+
def forward(self, x, x_mask, g=None):
|
| 97 |
+
if g is not None:
|
| 98 |
+
x = x + g
|
| 99 |
+
for i in range(self.n_layers):
|
| 100 |
+
y = self.convs_sep[i](x * x_mask)
|
| 101 |
+
y = self.norms_1[i](y)
|
| 102 |
+
y = F.gelu(y)
|
| 103 |
+
y = self.convs_1x1[i](y)
|
| 104 |
+
y = self.norms_2[i](y)
|
| 105 |
+
y = F.gelu(y)
|
| 106 |
+
y = self.drop(y)
|
| 107 |
+
x = x + y
|
| 108 |
+
return x * x_mask
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
class WN(torch.nn.Module):
|
| 112 |
+
def __init__(self, hidden_channels, kernel_size, dilation_rate, n_layers, gin_channels=0, p_dropout=0):
|
| 113 |
+
super(WN, self).__init__()
|
| 114 |
+
assert(kernel_size % 2 == 1)
|
| 115 |
+
self.hidden_channels =hidden_channels
|
| 116 |
+
self.kernel_size = kernel_size,
|
| 117 |
+
self.dilation_rate = dilation_rate
|
| 118 |
+
self.n_layers = n_layers
|
| 119 |
+
self.gin_channels = gin_channels
|
| 120 |
+
self.p_dropout = p_dropout
|
| 121 |
+
|
| 122 |
+
self.in_layers = torch.nn.ModuleList()
|
| 123 |
+
self.res_skip_layers = torch.nn.ModuleList()
|
| 124 |
+
self.drop = nn.Dropout(p_dropout)
|
| 125 |
+
|
| 126 |
+
if gin_channels != 0:
|
| 127 |
+
cond_layer = torch.nn.Conv1d(gin_channels, 2*hidden_channels*n_layers, 1)
|
| 128 |
+
self.cond_layer = torch.nn.utils.weight_norm(cond_layer, name='weight')
|
| 129 |
+
|
| 130 |
+
for i in range(n_layers):
|
| 131 |
+
dilation = dilation_rate ** i
|
| 132 |
+
padding = int((kernel_size * dilation - dilation) / 2)
|
| 133 |
+
in_layer = torch.nn.Conv1d(hidden_channels, 2*hidden_channels, kernel_size,
|
| 134 |
+
dilation=dilation, padding=padding)
|
| 135 |
+
in_layer = torch.nn.utils.weight_norm(in_layer, name='weight')
|
| 136 |
+
self.in_layers.append(in_layer)
|
| 137 |
+
|
| 138 |
+
# last one is not necessary
|
| 139 |
+
if i < n_layers - 1:
|
| 140 |
+
res_skip_channels = 2 * hidden_channels
|
| 141 |
+
else:
|
| 142 |
+
res_skip_channels = hidden_channels
|
| 143 |
+
|
| 144 |
+
res_skip_layer = torch.nn.Conv1d(hidden_channels, res_skip_channels, 1)
|
| 145 |
+
res_skip_layer = torch.nn.utils.weight_norm(res_skip_layer, name='weight')
|
| 146 |
+
self.res_skip_layers.append(res_skip_layer)
|
| 147 |
+
|
| 148 |
+
def forward(self, x, x_mask, g=None, **kwargs):
|
| 149 |
+
output = torch.zeros_like(x)
|
| 150 |
+
n_channels_tensor = torch.IntTensor([self.hidden_channels])
|
| 151 |
+
|
| 152 |
+
if g is not None:
|
| 153 |
+
g = self.cond_layer(g)
|
| 154 |
+
|
| 155 |
+
for i in range(self.n_layers):
|
| 156 |
+
x_in = self.in_layers[i](x)
|
| 157 |
+
if g is not None:
|
| 158 |
+
cond_offset = i * 2 * self.hidden_channels
|
| 159 |
+
g_l = g[:,cond_offset:cond_offset+2*self.hidden_channels,:]
|
| 160 |
+
else:
|
| 161 |
+
g_l = torch.zeros_like(x_in)
|
| 162 |
+
|
| 163 |
+
acts = commons.fused_add_tanh_sigmoid_multiply(
|
| 164 |
+
x_in,
|
| 165 |
+
g_l,
|
| 166 |
+
n_channels_tensor)
|
| 167 |
+
acts = self.drop(acts)
|
| 168 |
+
|
| 169 |
+
res_skip_acts = self.res_skip_layers[i](acts)
|
| 170 |
+
if i < self.n_layers - 1:
|
| 171 |
+
res_acts = res_skip_acts[:,:self.hidden_channels,:]
|
| 172 |
+
x = (x + res_acts) * x_mask
|
| 173 |
+
output = output + res_skip_acts[:,self.hidden_channels:,:]
|
| 174 |
+
else:
|
| 175 |
+
output = output + res_skip_acts
|
| 176 |
+
return output * x_mask
|
| 177 |
+
|
| 178 |
+
def remove_weight_norm(self):
|
| 179 |
+
if self.gin_channels != 0:
|
| 180 |
+
torch.nn.utils.remove_weight_norm(self.cond_layer)
|
| 181 |
+
for l in self.in_layers:
|
| 182 |
+
torch.nn.utils.remove_weight_norm(l)
|
| 183 |
+
for l in self.res_skip_layers:
|
| 184 |
+
torch.nn.utils.remove_weight_norm(l)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
class ResBlock1(torch.nn.Module):
|
| 188 |
+
def __init__(self, channels, kernel_size=3, dilation=(1, 3, 5)):
|
| 189 |
+
super(ResBlock1, self).__init__()
|
| 190 |
+
self.convs1 = nn.ModuleList([
|
| 191 |
+
weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=dilation[0],
|
| 192 |
+
padding=get_padding(kernel_size, dilation[0]))),
|
| 193 |
+
weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=dilation[1],
|
| 194 |
+
padding=get_padding(kernel_size, dilation[1]))),
|
| 195 |
+
weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=dilation[2],
|
| 196 |
+
padding=get_padding(kernel_size, dilation[2])))
|
| 197 |
+
])
|
| 198 |
+
self.convs1.apply(init_weights)
|
| 199 |
+
|
| 200 |
+
self.convs2 = nn.ModuleList([
|
| 201 |
+
weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=1,
|
| 202 |
+
padding=get_padding(kernel_size, 1))),
|
| 203 |
+
weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=1,
|
| 204 |
+
padding=get_padding(kernel_size, 1))),
|
| 205 |
+
weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=1,
|
| 206 |
+
padding=get_padding(kernel_size, 1)))
|
| 207 |
+
])
|
| 208 |
+
self.convs2.apply(init_weights)
|
| 209 |
+
|
| 210 |
+
def forward(self, x, x_mask=None):
|
| 211 |
+
for c1, c2 in zip(self.convs1, self.convs2):
|
| 212 |
+
xt = F.leaky_relu(x, LRELU_SLOPE)
|
| 213 |
+
if x_mask is not None:
|
| 214 |
+
xt = xt * x_mask
|
| 215 |
+
xt = c1(xt)
|
| 216 |
+
xt = F.leaky_relu(xt, LRELU_SLOPE)
|
| 217 |
+
if x_mask is not None:
|
| 218 |
+
xt = xt * x_mask
|
| 219 |
+
xt = c2(xt)
|
| 220 |
+
x = xt + x
|
| 221 |
+
if x_mask is not None:
|
| 222 |
+
x = x * x_mask
|
| 223 |
+
return x
|
| 224 |
+
|
| 225 |
+
def remove_weight_norm(self):
|
| 226 |
+
for l in self.convs1:
|
| 227 |
+
remove_weight_norm(l)
|
| 228 |
+
for l in self.convs2:
|
| 229 |
+
remove_weight_norm(l)
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
class ResBlock2(torch.nn.Module):
|
| 233 |
+
def __init__(self, channels, kernel_size=3, dilation=(1, 3)):
|
| 234 |
+
super(ResBlock2, self).__init__()
|
| 235 |
+
self.convs = nn.ModuleList([
|
| 236 |
+
weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=dilation[0],
|
| 237 |
+
padding=get_padding(kernel_size, dilation[0]))),
|
| 238 |
+
weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=dilation[1],
|
| 239 |
+
padding=get_padding(kernel_size, dilation[1])))
|
| 240 |
+
])
|
| 241 |
+
self.convs.apply(init_weights)
|
| 242 |
+
|
| 243 |
+
def forward(self, x, x_mask=None):
|
| 244 |
+
for c in self.convs:
|
| 245 |
+
xt = F.leaky_relu(x, LRELU_SLOPE)
|
| 246 |
+
if x_mask is not None:
|
| 247 |
+
xt = xt * x_mask
|
| 248 |
+
xt = c(xt)
|
| 249 |
+
x = xt + x
|
| 250 |
+
if x_mask is not None:
|
| 251 |
+
x = x * x_mask
|
| 252 |
+
return x
|
| 253 |
+
|
| 254 |
+
def remove_weight_norm(self):
|
| 255 |
+
for l in self.convs:
|
| 256 |
+
remove_weight_norm(l)
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
class Log(nn.Module):
|
| 260 |
+
def forward(self, x, x_mask, reverse=False, **kwargs):
|
| 261 |
+
if not reverse:
|
| 262 |
+
y = torch.log(torch.clamp_min(x, 1e-5)) * x_mask
|
| 263 |
+
logdet = torch.sum(-y, [1, 2])
|
| 264 |
+
return y, logdet
|
| 265 |
+
else:
|
| 266 |
+
x = torch.exp(x) * x_mask
|
| 267 |
+
return x
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
class Flip(nn.Module):
|
| 271 |
+
def forward(self, x, *args, reverse=False, **kwargs):
|
| 272 |
+
x = torch.flip(x, [1])
|
| 273 |
+
if not reverse:
|
| 274 |
+
logdet = torch.zeros(x.size(0)).to(dtype=x.dtype, device=x.device)
|
| 275 |
+
return x, logdet
|
| 276 |
+
else:
|
| 277 |
+
return x
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
class ElementwiseAffine(nn.Module):
|
| 281 |
+
def __init__(self, channels):
|
| 282 |
+
super().__init__()
|
| 283 |
+
self.channels = channels
|
| 284 |
+
self.m = nn.Parameter(torch.zeros(channels,1))
|
| 285 |
+
self.logs = nn.Parameter(torch.zeros(channels,1))
|
| 286 |
+
|
| 287 |
+
def forward(self, x, x_mask, reverse=False, **kwargs):
|
| 288 |
+
if not reverse:
|
| 289 |
+
y = self.m + torch.exp(self.logs) * x
|
| 290 |
+
y = y * x_mask
|
| 291 |
+
logdet = torch.sum(self.logs * x_mask, [1,2])
|
| 292 |
+
return y, logdet
|
| 293 |
+
else:
|
| 294 |
+
x = (x - self.m) * torch.exp(-self.logs) * x_mask
|
| 295 |
+
return x
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
class ResidualCouplingLayer(nn.Module):
|
| 299 |
+
def __init__(self,
|
| 300 |
+
channels,
|
| 301 |
+
hidden_channels,
|
| 302 |
+
kernel_size,
|
| 303 |
+
dilation_rate,
|
| 304 |
+
n_layers,
|
| 305 |
+
p_dropout=0,
|
| 306 |
+
gin_channels=0,
|
| 307 |
+
mean_only=False):
|
| 308 |
+
assert channels % 2 == 0, "channels should be divisible by 2"
|
| 309 |
+
super().__init__()
|
| 310 |
+
self.channels = channels
|
| 311 |
+
self.hidden_channels = hidden_channels
|
| 312 |
+
self.kernel_size = kernel_size
|
| 313 |
+
self.dilation_rate = dilation_rate
|
| 314 |
+
self.n_layers = n_layers
|
| 315 |
+
self.half_channels = channels // 2
|
| 316 |
+
self.mean_only = mean_only
|
| 317 |
+
|
| 318 |
+
self.pre = nn.Conv1d(self.half_channels, hidden_channels, 1)
|
| 319 |
+
self.enc = WN(hidden_channels, kernel_size, dilation_rate, n_layers, p_dropout=p_dropout, gin_channels=gin_channels)
|
| 320 |
+
self.post = nn.Conv1d(hidden_channels, self.half_channels * (2 - mean_only), 1)
|
| 321 |
+
self.post.weight.data.zero_()
|
| 322 |
+
self.post.bias.data.zero_()
|
| 323 |
+
|
| 324 |
+
def forward(self, x, x_mask, g=None, reverse=False):
|
| 325 |
+
x0, x1 = torch.split(x, [self.half_channels]*2, 1)
|
| 326 |
+
h = self.pre(x0) * x_mask
|
| 327 |
+
h = self.enc(h, x_mask, g=g)
|
| 328 |
+
stats = self.post(h) * x_mask
|
| 329 |
+
if not self.mean_only:
|
| 330 |
+
m, logs = torch.split(stats, [self.half_channels]*2, 1)
|
| 331 |
+
else:
|
| 332 |
+
m = stats
|
| 333 |
+
logs = torch.zeros_like(m)
|
| 334 |
+
|
| 335 |
+
if not reverse:
|
| 336 |
+
x1 = m + x1 * torch.exp(logs) * x_mask
|
| 337 |
+
x = torch.cat([x0, x1], 1)
|
| 338 |
+
logdet = torch.sum(logs, [1,2])
|
| 339 |
+
return x, logdet
|
| 340 |
+
else:
|
| 341 |
+
x1 = (x1 - m) * torch.exp(-logs) * x_mask
|
| 342 |
+
x = torch.cat([x0, x1], 1)
|
| 343 |
+
return x
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
class ConvFlow(nn.Module):
|
| 347 |
+
def __init__(self, in_channels, filter_channels, kernel_size, n_layers, num_bins=10, tail_bound=5.0):
|
| 348 |
+
super().__init__()
|
| 349 |
+
self.in_channels = in_channels
|
| 350 |
+
self.filter_channels = filter_channels
|
| 351 |
+
self.kernel_size = kernel_size
|
| 352 |
+
self.n_layers = n_layers
|
| 353 |
+
self.num_bins = num_bins
|
| 354 |
+
self.tail_bound = tail_bound
|
| 355 |
+
self.half_channels = in_channels // 2
|
| 356 |
+
|
| 357 |
+
self.pre = nn.Conv1d(self.half_channels, filter_channels, 1)
|
| 358 |
+
self.convs = DDSConv(filter_channels, kernel_size, n_layers, p_dropout=0.)
|
| 359 |
+
self.proj = nn.Conv1d(filter_channels, self.half_channels * (num_bins * 3 - 1), 1)
|
| 360 |
+
self.proj.weight.data.zero_()
|
| 361 |
+
self.proj.bias.data.zero_()
|
| 362 |
+
|
| 363 |
+
def forward(self, x, x_mask, g=None, reverse=False):
|
| 364 |
+
x0, x1 = torch.split(x, [self.half_channels]*2, 1)
|
| 365 |
+
h = self.pre(x0)
|
| 366 |
+
h = self.convs(h, x_mask, g=g)
|
| 367 |
+
h = self.proj(h) * x_mask
|
| 368 |
+
|
| 369 |
+
b, c, t = x0.shape
|
| 370 |
+
h = h.reshape(b, c, -1, t).permute(0, 1, 3, 2) # [b, cx?, t] -> [b, c, t, ?]
|
| 371 |
+
|
| 372 |
+
unnormalized_widths = h[..., :self.num_bins] / math.sqrt(self.filter_channels)
|
| 373 |
+
unnormalized_heights = h[..., self.num_bins:2*self.num_bins] / math.sqrt(self.filter_channels)
|
| 374 |
+
unnormalized_derivatives = h[..., 2 * self.num_bins:]
|
| 375 |
+
|
| 376 |
+
x1, logabsdet = piecewise_rational_quadratic_transform(x1,
|
| 377 |
+
unnormalized_widths,
|
| 378 |
+
unnormalized_heights,
|
| 379 |
+
unnormalized_derivatives,
|
| 380 |
+
inverse=reverse,
|
| 381 |
+
tails='linear',
|
| 382 |
+
tail_bound=self.tail_bound
|
| 383 |
+
)
|
| 384 |
+
|
| 385 |
+
x = torch.cat([x0, x1], 1) * x_mask
|
| 386 |
+
logdet = torch.sum(logabsdet * x_mask, [1,2])
|
| 387 |
+
if not reverse:
|
| 388 |
+
return x, logdet
|
| 389 |
+
else:
|
| 390 |
+
return x
|
runtime/monotonic_align.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Training-only alignment stub; deployable inference never calls maximum_path."""
|
| 2 |
+
|
| 3 |
+
def maximum_path(*args, **kwargs):
|
| 4 |
+
raise RuntimeError("Monotonic alignment is unavailable in the inference package.")
|
runtime/text/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Copyright (c) 2017 Keith Ito
|
| 2 |
+
|
| 3 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 4 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 5 |
+
in the Software without restriction, including without limitation the rights
|
| 6 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 7 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 8 |
+
furnished to do so, subject to the following conditions:
|
| 9 |
+
|
| 10 |
+
The above copyright notice and this permission notice shall be included in
|
| 11 |
+
all copies or substantial portions of the Software.
|
| 12 |
+
|
| 13 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 14 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 15 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 16 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 17 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 18 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
| 19 |
+
THE SOFTWARE.
|
runtime/text/__init__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
""" from https://github.com/keithito/tacotron """
|
| 2 |
+
from text import cleaners
|
| 3 |
+
from text.symbols import symbols
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
# Mappings from symbol to numeric ID and vice versa:
|
| 7 |
+
_symbol_to_id = {s: i for i, s in enumerate(symbols)}
|
| 8 |
+
_id_to_symbol = {i: s for i, s in enumerate(symbols)}
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def text_to_sequence(text, cleaner_names):
|
| 12 |
+
'''Converts a string of text to a sequence of IDs corresponding to the symbols in the text.
|
| 13 |
+
Args:
|
| 14 |
+
text: string to convert to a sequence
|
| 15 |
+
cleaner_names: names of the cleaner functions to run the text through
|
| 16 |
+
Returns:
|
| 17 |
+
List of integers corresponding to the symbols in the text
|
| 18 |
+
'''
|
| 19 |
+
sequence = []
|
| 20 |
+
|
| 21 |
+
clean_text = _clean_text(text, cleaner_names)
|
| 22 |
+
for symbol in clean_text:
|
| 23 |
+
symbol_id = _symbol_to_id[symbol]
|
| 24 |
+
sequence += [symbol_id]
|
| 25 |
+
return sequence
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def cleaned_text_to_sequence(cleaned_text):
|
| 29 |
+
'''Converts a string of text to a sequence of IDs corresponding to the symbols in the text.
|
| 30 |
+
Args:
|
| 31 |
+
text: string to convert to a sequence
|
| 32 |
+
Returns:
|
| 33 |
+
List of integers corresponding to the symbols in the text
|
| 34 |
+
'''
|
| 35 |
+
sequence = [_symbol_to_id[symbol] for symbol in cleaned_text]
|
| 36 |
+
return sequence
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def sequence_to_text(sequence):
|
| 40 |
+
'''Converts a sequence of IDs back to a string'''
|
| 41 |
+
result = ''
|
| 42 |
+
for symbol_id in sequence:
|
| 43 |
+
s = _id_to_symbol[symbol_id]
|
| 44 |
+
result += s
|
| 45 |
+
return result
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _clean_text(text, cleaner_names):
|
| 49 |
+
for name in cleaner_names:
|
| 50 |
+
cleaner = getattr(cleaners, name)
|
| 51 |
+
if not cleaner:
|
| 52 |
+
raise Exception('Unknown cleaner: %s' % name)
|
| 53 |
+
text = cleaner(text)
|
| 54 |
+
return text
|
runtime/text/cleaners.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
""" from https://github.com/keithito/tacotron """
|
| 2 |
+
|
| 3 |
+
'''
|
| 4 |
+
Cleaners are transformations that run over the input text at both training and eval time.
|
| 5 |
+
|
| 6 |
+
Cleaners can be selected by passing a comma-delimited list of cleaner names as the "cleaners"
|
| 7 |
+
hyperparameter. Some cleaners are English-specific. You'll typically want to use:
|
| 8 |
+
1. "english_cleaners" for English text
|
| 9 |
+
2. "transliteration_cleaners" for non-English text that can be transliterated to ASCII using
|
| 10 |
+
the Unidecode library (https://pypi.python.org/pypi/Unidecode)
|
| 11 |
+
3. "basic_cleaners" if you do not want to transliterate (in this case, you should also update
|
| 12 |
+
the symbols in symbols.py to match your data).
|
| 13 |
+
'''
|
| 14 |
+
|
| 15 |
+
import re
|
| 16 |
+
from unidecode import unidecode
|
| 17 |
+
from phonemizer import phonemize
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# Regular expression matching whitespace:
|
| 21 |
+
_whitespace_re = re.compile(r'\s+')
|
| 22 |
+
|
| 23 |
+
# List of (regular expression, replacement) pairs for abbreviations:
|
| 24 |
+
_abbreviations = [(re.compile('\\b%s\\.' % x[0], re.IGNORECASE), x[1]) for x in [
|
| 25 |
+
('mrs', 'misess'),
|
| 26 |
+
('mr', 'mister'),
|
| 27 |
+
('dr', 'doctor'),
|
| 28 |
+
('st', 'saint'),
|
| 29 |
+
('co', 'company'),
|
| 30 |
+
('jr', 'junior'),
|
| 31 |
+
('maj', 'major'),
|
| 32 |
+
('gen', 'general'),
|
| 33 |
+
('drs', 'doctors'),
|
| 34 |
+
('rev', 'reverend'),
|
| 35 |
+
('lt', 'lieutenant'),
|
| 36 |
+
('hon', 'honorable'),
|
| 37 |
+
('sgt', 'sergeant'),
|
| 38 |
+
('capt', 'captain'),
|
| 39 |
+
('esq', 'esquire'),
|
| 40 |
+
('ltd', 'limited'),
|
| 41 |
+
('col', 'colonel'),
|
| 42 |
+
('ft', 'fort'),
|
| 43 |
+
]]
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def expand_abbreviations(text):
|
| 47 |
+
for regex, replacement in _abbreviations:
|
| 48 |
+
text = re.sub(regex, replacement, text)
|
| 49 |
+
return text
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def expand_numbers(text):
|
| 53 |
+
return normalize_numbers(text)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def lowercase(text):
|
| 57 |
+
return text.lower()
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def collapse_whitespace(text):
|
| 61 |
+
return re.sub(_whitespace_re, ' ', text)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def convert_to_ascii(text):
|
| 65 |
+
return unidecode(text)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def basic_cleaners(text):
|
| 69 |
+
'''Basic pipeline that lowercases and collapses whitespace without transliteration.'''
|
| 70 |
+
text = lowercase(text)
|
| 71 |
+
text = collapse_whitespace(text)
|
| 72 |
+
return text
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def transliteration_cleaners(text):
|
| 76 |
+
'''Pipeline for non-English text that transliterates to ASCII.'''
|
| 77 |
+
text = convert_to_ascii(text)
|
| 78 |
+
text = lowercase(text)
|
| 79 |
+
text = collapse_whitespace(text)
|
| 80 |
+
return text
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def english_cleaners(text):
|
| 84 |
+
'''Pipeline for English text, including abbreviation expansion.'''
|
| 85 |
+
text = convert_to_ascii(text)
|
| 86 |
+
text = lowercase(text)
|
| 87 |
+
text = expand_abbreviations(text)
|
| 88 |
+
phonemes = phonemize(text, language='en-us', backend='espeak', strip=True)
|
| 89 |
+
phonemes = collapse_whitespace(phonemes)
|
| 90 |
+
return phonemes
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def english_cleaners2(text):
|
| 94 |
+
'''Pipeline for English text, including abbreviation expansion. + punctuation + stress'''
|
| 95 |
+
text = convert_to_ascii(text)
|
| 96 |
+
text = lowercase(text)
|
| 97 |
+
text = expand_abbreviations(text)
|
| 98 |
+
phonemes = phonemize(text, language='en-us', backend='espeak', strip=True, preserve_punctuation=True, with_stress=True)
|
| 99 |
+
phonemes = collapse_whitespace(phonemes)
|
| 100 |
+
return phonemes
|
runtime/text/symbols.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
""" from https://github.com/keithito/tacotron """
|
| 2 |
+
|
| 3 |
+
'''
|
| 4 |
+
Defines the set of symbols used in text input to the model.
|
| 5 |
+
'''
|
| 6 |
+
_pad = '_'
|
| 7 |
+
_punctuation = ';:,.!?¡¿—…"«»“” '
|
| 8 |
+
_letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
| 9 |
+
_letters_ipa = "ɑɐɒæɓʙβɔɕçɗɖðʤəɘɚɛɜɝɞɟʄɡɠɢʛɦɧħɥʜɨɪʝɭɬɫɮʟɱɯɰŋɳɲɴøɵɸθœɶʘɹɺɾɻʀʁɽʂʃʈʧʉʊʋⱱʌɣɤʍχʎʏʑʐʒʔʡʕʢǀǁǂǃˈˌːˑʼʴʰʱʲʷˠˤ˞↓↑→↗↘'̩'ᵻ"
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# Export all symbols:
|
| 13 |
+
symbols = [_pad] + list(_punctuation) + list(_letters) + list(_letters_ipa)
|
| 14 |
+
|
| 15 |
+
# Special symbol ids
|
| 16 |
+
SPACE_ID = symbols.index(" ")
|
runtime/transforms.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from torch.nn import functional as F
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
DEFAULT_MIN_BIN_WIDTH = 1e-3
|
| 8 |
+
DEFAULT_MIN_BIN_HEIGHT = 1e-3
|
| 9 |
+
DEFAULT_MIN_DERIVATIVE = 1e-3
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def piecewise_rational_quadratic_transform(inputs,
|
| 13 |
+
unnormalized_widths,
|
| 14 |
+
unnormalized_heights,
|
| 15 |
+
unnormalized_derivatives,
|
| 16 |
+
inverse=False,
|
| 17 |
+
tails=None,
|
| 18 |
+
tail_bound=1.,
|
| 19 |
+
min_bin_width=DEFAULT_MIN_BIN_WIDTH,
|
| 20 |
+
min_bin_height=DEFAULT_MIN_BIN_HEIGHT,
|
| 21 |
+
min_derivative=DEFAULT_MIN_DERIVATIVE):
|
| 22 |
+
|
| 23 |
+
if tails is None:
|
| 24 |
+
spline_fn = rational_quadratic_spline
|
| 25 |
+
spline_kwargs = {}
|
| 26 |
+
else:
|
| 27 |
+
spline_fn = unconstrained_rational_quadratic_spline
|
| 28 |
+
spline_kwargs = {
|
| 29 |
+
'tails': tails,
|
| 30 |
+
'tail_bound': tail_bound
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
outputs, logabsdet = spline_fn(
|
| 34 |
+
inputs=inputs,
|
| 35 |
+
unnormalized_widths=unnormalized_widths,
|
| 36 |
+
unnormalized_heights=unnormalized_heights,
|
| 37 |
+
unnormalized_derivatives=unnormalized_derivatives,
|
| 38 |
+
inverse=inverse,
|
| 39 |
+
min_bin_width=min_bin_width,
|
| 40 |
+
min_bin_height=min_bin_height,
|
| 41 |
+
min_derivative=min_derivative,
|
| 42 |
+
**spline_kwargs
|
| 43 |
+
)
|
| 44 |
+
return outputs, logabsdet
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def searchsorted(bin_locations, inputs, eps=1e-6):
|
| 48 |
+
bin_locations[..., -1] += eps
|
| 49 |
+
return torch.sum(
|
| 50 |
+
inputs[..., None] >= bin_locations,
|
| 51 |
+
dim=-1
|
| 52 |
+
) - 1
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def unconstrained_rational_quadratic_spline(inputs,
|
| 56 |
+
unnormalized_widths,
|
| 57 |
+
unnormalized_heights,
|
| 58 |
+
unnormalized_derivatives,
|
| 59 |
+
inverse=False,
|
| 60 |
+
tails='linear',
|
| 61 |
+
tail_bound=1.,
|
| 62 |
+
min_bin_width=DEFAULT_MIN_BIN_WIDTH,
|
| 63 |
+
min_bin_height=DEFAULT_MIN_BIN_HEIGHT,
|
| 64 |
+
min_derivative=DEFAULT_MIN_DERIVATIVE):
|
| 65 |
+
inside_interval_mask = (inputs >= -tail_bound) & (inputs <= tail_bound)
|
| 66 |
+
outside_interval_mask = ~inside_interval_mask
|
| 67 |
+
|
| 68 |
+
outputs = torch.zeros_like(inputs)
|
| 69 |
+
logabsdet = torch.zeros_like(inputs)
|
| 70 |
+
|
| 71 |
+
if tails == 'linear':
|
| 72 |
+
unnormalized_derivatives = F.pad(unnormalized_derivatives, pad=(1, 1))
|
| 73 |
+
constant = np.log(np.exp(1 - min_derivative) - 1)
|
| 74 |
+
unnormalized_derivatives[..., 0] = constant
|
| 75 |
+
unnormalized_derivatives[..., -1] = constant
|
| 76 |
+
|
| 77 |
+
outputs[outside_interval_mask] = inputs[outside_interval_mask]
|
| 78 |
+
logabsdet[outside_interval_mask] = 0
|
| 79 |
+
else:
|
| 80 |
+
raise RuntimeError('{} tails are not implemented.'.format(tails))
|
| 81 |
+
|
| 82 |
+
outputs[inside_interval_mask], logabsdet[inside_interval_mask] = rational_quadratic_spline(
|
| 83 |
+
inputs=inputs[inside_interval_mask],
|
| 84 |
+
unnormalized_widths=unnormalized_widths[inside_interval_mask, :],
|
| 85 |
+
unnormalized_heights=unnormalized_heights[inside_interval_mask, :],
|
| 86 |
+
unnormalized_derivatives=unnormalized_derivatives[inside_interval_mask, :],
|
| 87 |
+
inverse=inverse,
|
| 88 |
+
left=-tail_bound, right=tail_bound, bottom=-tail_bound, top=tail_bound,
|
| 89 |
+
min_bin_width=min_bin_width,
|
| 90 |
+
min_bin_height=min_bin_height,
|
| 91 |
+
min_derivative=min_derivative
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
return outputs, logabsdet
|
| 95 |
+
|
| 96 |
+
def rational_quadratic_spline(inputs,
|
| 97 |
+
unnormalized_widths,
|
| 98 |
+
unnormalized_heights,
|
| 99 |
+
unnormalized_derivatives,
|
| 100 |
+
inverse=False,
|
| 101 |
+
left=0., right=1., bottom=0., top=1.,
|
| 102 |
+
min_bin_width=DEFAULT_MIN_BIN_WIDTH,
|
| 103 |
+
min_bin_height=DEFAULT_MIN_BIN_HEIGHT,
|
| 104 |
+
min_derivative=DEFAULT_MIN_DERIVATIVE):
|
| 105 |
+
if torch.min(inputs) < left or torch.max(inputs) > right:
|
| 106 |
+
raise ValueError('Input to a transform is not within its domain')
|
| 107 |
+
|
| 108 |
+
num_bins = unnormalized_widths.shape[-1]
|
| 109 |
+
|
| 110 |
+
if min_bin_width * num_bins > 1.0:
|
| 111 |
+
raise ValueError('Minimal bin width too large for the number of bins')
|
| 112 |
+
if min_bin_height * num_bins > 1.0:
|
| 113 |
+
raise ValueError('Minimal bin height too large for the number of bins')
|
| 114 |
+
|
| 115 |
+
widths = F.softmax(unnormalized_widths, dim=-1)
|
| 116 |
+
widths = min_bin_width + (1 - min_bin_width * num_bins) * widths
|
| 117 |
+
cumwidths = torch.cumsum(widths, dim=-1)
|
| 118 |
+
cumwidths = F.pad(cumwidths, pad=(1, 0), mode='constant', value=0.0)
|
| 119 |
+
cumwidths = (right - left) * cumwidths + left
|
| 120 |
+
cumwidths[..., 0] = left
|
| 121 |
+
cumwidths[..., -1] = right
|
| 122 |
+
widths = cumwidths[..., 1:] - cumwidths[..., :-1]
|
| 123 |
+
|
| 124 |
+
derivatives = min_derivative + F.softplus(unnormalized_derivatives)
|
| 125 |
+
|
| 126 |
+
heights = F.softmax(unnormalized_heights, dim=-1)
|
| 127 |
+
heights = min_bin_height + (1 - min_bin_height * num_bins) * heights
|
| 128 |
+
cumheights = torch.cumsum(heights, dim=-1)
|
| 129 |
+
cumheights = F.pad(cumheights, pad=(1, 0), mode='constant', value=0.0)
|
| 130 |
+
cumheights = (top - bottom) * cumheights + bottom
|
| 131 |
+
cumheights[..., 0] = bottom
|
| 132 |
+
cumheights[..., -1] = top
|
| 133 |
+
heights = cumheights[..., 1:] - cumheights[..., :-1]
|
| 134 |
+
|
| 135 |
+
if inverse:
|
| 136 |
+
bin_idx = searchsorted(cumheights, inputs)[..., None]
|
| 137 |
+
else:
|
| 138 |
+
bin_idx = searchsorted(cumwidths, inputs)[..., None]
|
| 139 |
+
|
| 140 |
+
input_cumwidths = cumwidths.gather(-1, bin_idx)[..., 0]
|
| 141 |
+
input_bin_widths = widths.gather(-1, bin_idx)[..., 0]
|
| 142 |
+
|
| 143 |
+
input_cumheights = cumheights.gather(-1, bin_idx)[..., 0]
|
| 144 |
+
delta = heights / widths
|
| 145 |
+
input_delta = delta.gather(-1, bin_idx)[..., 0]
|
| 146 |
+
|
| 147 |
+
input_derivatives = derivatives.gather(-1, bin_idx)[..., 0]
|
| 148 |
+
input_derivatives_plus_one = derivatives[..., 1:].gather(-1, bin_idx)[..., 0]
|
| 149 |
+
|
| 150 |
+
input_heights = heights.gather(-1, bin_idx)[..., 0]
|
| 151 |
+
|
| 152 |
+
if inverse:
|
| 153 |
+
a = (((inputs - input_cumheights) * (input_derivatives
|
| 154 |
+
+ input_derivatives_plus_one
|
| 155 |
+
- 2 * input_delta)
|
| 156 |
+
+ input_heights * (input_delta - input_derivatives)))
|
| 157 |
+
b = (input_heights * input_derivatives
|
| 158 |
+
- (inputs - input_cumheights) * (input_derivatives
|
| 159 |
+
+ input_derivatives_plus_one
|
| 160 |
+
- 2 * input_delta))
|
| 161 |
+
c = - input_delta * (inputs - input_cumheights)
|
| 162 |
+
|
| 163 |
+
discriminant = b.pow(2) - 4 * a * c
|
| 164 |
+
assert (discriminant >= 0).all()
|
| 165 |
+
|
| 166 |
+
root = (2 * c) / (-b - torch.sqrt(discriminant))
|
| 167 |
+
outputs = root * input_bin_widths + input_cumwidths
|
| 168 |
+
|
| 169 |
+
theta_one_minus_theta = root * (1 - root)
|
| 170 |
+
denominator = input_delta + ((input_derivatives + input_derivatives_plus_one - 2 * input_delta)
|
| 171 |
+
* theta_one_minus_theta)
|
| 172 |
+
derivative_numerator = input_delta.pow(2) * (input_derivatives_plus_one * root.pow(2)
|
| 173 |
+
+ 2 * input_delta * theta_one_minus_theta
|
| 174 |
+
+ input_derivatives * (1 - root).pow(2))
|
| 175 |
+
logabsdet = torch.log(derivative_numerator) - 2 * torch.log(denominator)
|
| 176 |
+
|
| 177 |
+
return outputs, -logabsdet
|
| 178 |
+
else:
|
| 179 |
+
theta = (inputs - input_cumwidths) / input_bin_widths
|
| 180 |
+
theta_one_minus_theta = theta * (1 - theta)
|
| 181 |
+
|
| 182 |
+
numerator = input_heights * (input_delta * theta.pow(2)
|
| 183 |
+
+ input_derivatives * theta_one_minus_theta)
|
| 184 |
+
denominator = input_delta + ((input_derivatives + input_derivatives_plus_one - 2 * input_delta)
|
| 185 |
+
* theta_one_minus_theta)
|
| 186 |
+
outputs = input_cumheights + numerator / denominator
|
| 187 |
+
|
| 188 |
+
derivative_numerator = input_delta.pow(2) * (input_derivatives_plus_one * theta.pow(2)
|
| 189 |
+
+ 2 * input_delta * theta_one_minus_theta
|
| 190 |
+
+ input_derivatives * (1 - theta).pow(2))
|
| 191 |
+
logabsdet = torch.log(derivative_numerator) - 2 * torch.log(denominator)
|
| 192 |
+
|
| 193 |
+
return outputs, logabsdet
|
runtime/utils.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import glob
|
| 3 |
+
import sys
|
| 4 |
+
import argparse
|
| 5 |
+
import logging
|
| 6 |
+
import json
|
| 7 |
+
import subprocess
|
| 8 |
+
import numpy as np
|
| 9 |
+
from scipy.io.wavfile import read
|
| 10 |
+
import torch
|
| 11 |
+
|
| 12 |
+
MATPLOTLIB_FLAG = False
|
| 13 |
+
|
| 14 |
+
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
|
| 15 |
+
logger = logging
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def load_checkpoint(checkpoint_path, model, optimizer=None):
|
| 19 |
+
assert os.path.isfile(checkpoint_path)
|
| 20 |
+
checkpoint_dict = torch.load(checkpoint_path, map_location='cpu')
|
| 21 |
+
iteration = checkpoint_dict['iteration']
|
| 22 |
+
learning_rate = checkpoint_dict['learning_rate']
|
| 23 |
+
if optimizer is not None:
|
| 24 |
+
optimizer.load_state_dict(checkpoint_dict['optimizer'])
|
| 25 |
+
saved_state_dict = checkpoint_dict['model']
|
| 26 |
+
if hasattr(model, 'module'):
|
| 27 |
+
state_dict = model.module.state_dict()
|
| 28 |
+
else:
|
| 29 |
+
state_dict = model.state_dict()
|
| 30 |
+
new_state_dict= {}
|
| 31 |
+
for k, v in state_dict.items():
|
| 32 |
+
try:
|
| 33 |
+
new_state_dict[k] = saved_state_dict[k]
|
| 34 |
+
except:
|
| 35 |
+
logger.info("%s is not in the checkpoint" % k)
|
| 36 |
+
new_state_dict[k] = v
|
| 37 |
+
if hasattr(model, 'module'):
|
| 38 |
+
model.module.load_state_dict(new_state_dict)
|
| 39 |
+
else:
|
| 40 |
+
model.load_state_dict(new_state_dict)
|
| 41 |
+
logger.info("Loaded checkpoint '{}' (iteration {})" .format(
|
| 42 |
+
checkpoint_path, iteration))
|
| 43 |
+
return model, optimizer, learning_rate, iteration
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def save_checkpoint(model, optimizer, learning_rate, iteration, checkpoint_path):
|
| 47 |
+
logger.info("Saving model and optimizer state at iteration {} to {}".format(
|
| 48 |
+
iteration, checkpoint_path))
|
| 49 |
+
if hasattr(model, 'module'):
|
| 50 |
+
state_dict = model.module.state_dict()
|
| 51 |
+
else:
|
| 52 |
+
state_dict = model.state_dict()
|
| 53 |
+
torch.save({'model': state_dict,
|
| 54 |
+
'iteration': iteration,
|
| 55 |
+
'optimizer': optimizer.state_dict(),
|
| 56 |
+
'learning_rate': learning_rate}, checkpoint_path)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def summarize(writer, global_step, scalars={}, histograms={}, images={}, audios={}, audio_sampling_rate=22050):
|
| 60 |
+
for k, v in scalars.items():
|
| 61 |
+
writer.add_scalar(k, v, global_step)
|
| 62 |
+
for k, v in histograms.items():
|
| 63 |
+
writer.add_histogram(k, v, global_step)
|
| 64 |
+
for k, v in images.items():
|
| 65 |
+
writer.add_image(k, v, global_step, dataformats='HWC')
|
| 66 |
+
for k, v in audios.items():
|
| 67 |
+
writer.add_audio(k, v, global_step, audio_sampling_rate)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def latest_checkpoint_path(dir_path, regex="G_*.pth"):
|
| 71 |
+
f_list = glob.glob(os.path.join(dir_path, regex))
|
| 72 |
+
f_list.sort(key=lambda f: int("".join(filter(str.isdigit, f))))
|
| 73 |
+
x = f_list[-1]
|
| 74 |
+
print(x)
|
| 75 |
+
return x
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def plot_spectrogram_to_numpy(spectrogram):
|
| 79 |
+
global MATPLOTLIB_FLAG
|
| 80 |
+
if not MATPLOTLIB_FLAG:
|
| 81 |
+
import matplotlib
|
| 82 |
+
matplotlib.use("Agg")
|
| 83 |
+
MATPLOTLIB_FLAG = True
|
| 84 |
+
mpl_logger = logging.getLogger('matplotlib')
|
| 85 |
+
mpl_logger.setLevel(logging.WARNING)
|
| 86 |
+
import matplotlib.pylab as plt
|
| 87 |
+
import numpy as np
|
| 88 |
+
|
| 89 |
+
fig, ax = plt.subplots(figsize=(10,2))
|
| 90 |
+
im = ax.imshow(spectrogram, aspect="auto", origin="lower",
|
| 91 |
+
interpolation='none')
|
| 92 |
+
plt.colorbar(im, ax=ax)
|
| 93 |
+
plt.xlabel("Frames")
|
| 94 |
+
plt.ylabel("Channels")
|
| 95 |
+
plt.tight_layout()
|
| 96 |
+
|
| 97 |
+
fig.canvas.draw()
|
| 98 |
+
data = np.asarray(fig.canvas.buffer_rgba(), dtype=np.uint8)[..., :3].copy()
|
| 99 |
+
plt.close()
|
| 100 |
+
return data
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def plot_alignment_to_numpy(alignment, info=None):
|
| 104 |
+
global MATPLOTLIB_FLAG
|
| 105 |
+
if not MATPLOTLIB_FLAG:
|
| 106 |
+
import matplotlib
|
| 107 |
+
matplotlib.use("Agg")
|
| 108 |
+
MATPLOTLIB_FLAG = True
|
| 109 |
+
mpl_logger = logging.getLogger('matplotlib')
|
| 110 |
+
mpl_logger.setLevel(logging.WARNING)
|
| 111 |
+
import matplotlib.pylab as plt
|
| 112 |
+
import numpy as np
|
| 113 |
+
|
| 114 |
+
fig, ax = plt.subplots(figsize=(6, 4))
|
| 115 |
+
im = ax.imshow(alignment.transpose(), aspect='auto', origin='lower',
|
| 116 |
+
interpolation='none')
|
| 117 |
+
fig.colorbar(im, ax=ax)
|
| 118 |
+
xlabel = 'Decoder timestep'
|
| 119 |
+
if info is not None:
|
| 120 |
+
xlabel += '\n\n' + info
|
| 121 |
+
plt.xlabel(xlabel)
|
| 122 |
+
plt.ylabel('Encoder timestep')
|
| 123 |
+
plt.tight_layout()
|
| 124 |
+
|
| 125 |
+
fig.canvas.draw()
|
| 126 |
+
data = np.asarray(fig.canvas.buffer_rgba(), dtype=np.uint8)[..., :3].copy()
|
| 127 |
+
plt.close()
|
| 128 |
+
return data
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def load_wav_to_torch(full_path):
|
| 132 |
+
sampling_rate, data = read(full_path)
|
| 133 |
+
return torch.FloatTensor(data.astype(np.float32)), sampling_rate
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def load_filepaths_and_text(filename, split="|"):
|
| 137 |
+
with open(filename, encoding='utf-8') as f:
|
| 138 |
+
filepaths_and_text = [line.strip().split(split) for line in f]
|
| 139 |
+
return filepaths_and_text
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def get_hparams(init=True):
|
| 143 |
+
parser = argparse.ArgumentParser()
|
| 144 |
+
parser.add_argument('-c', '--config', type=str, default="./configs/base.json",
|
| 145 |
+
help='JSON file for configuration')
|
| 146 |
+
parser.add_argument('-m', '--model', type=str, required=True,
|
| 147 |
+
help='Model name')
|
| 148 |
+
|
| 149 |
+
args = parser.parse_args()
|
| 150 |
+
model_dir = os.path.join("./logs", args.model)
|
| 151 |
+
|
| 152 |
+
if not os.path.exists(model_dir):
|
| 153 |
+
os.makedirs(model_dir)
|
| 154 |
+
|
| 155 |
+
config_path = args.config
|
| 156 |
+
config_save_path = os.path.join(model_dir, "config.json")
|
| 157 |
+
if init:
|
| 158 |
+
with open(config_path, "r") as f:
|
| 159 |
+
data = f.read()
|
| 160 |
+
with open(config_save_path, "w") as f:
|
| 161 |
+
f.write(data)
|
| 162 |
+
else:
|
| 163 |
+
with open(config_save_path, "r") as f:
|
| 164 |
+
data = f.read()
|
| 165 |
+
config = json.loads(data)
|
| 166 |
+
|
| 167 |
+
hparams = HParams(**config)
|
| 168 |
+
hparams.model_dir = model_dir
|
| 169 |
+
return hparams
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def get_hparams_from_dir(model_dir):
|
| 173 |
+
config_save_path = os.path.join(model_dir, "config.json")
|
| 174 |
+
with open(config_save_path, "r") as f:
|
| 175 |
+
data = f.read()
|
| 176 |
+
config = json.loads(data)
|
| 177 |
+
|
| 178 |
+
hparams =HParams(**config)
|
| 179 |
+
hparams.model_dir = model_dir
|
| 180 |
+
return hparams
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def get_hparams_from_file(config_path):
|
| 184 |
+
with open(config_path, "r") as f:
|
| 185 |
+
data = f.read()
|
| 186 |
+
config = json.loads(data)
|
| 187 |
+
|
| 188 |
+
hparams =HParams(**config)
|
| 189 |
+
return hparams
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def check_git_hash(model_dir):
|
| 193 |
+
source_dir = os.path.dirname(os.path.realpath(__file__))
|
| 194 |
+
if not os.path.exists(os.path.join(source_dir, ".git")):
|
| 195 |
+
logger.warn("{} is not a git repository, therefore hash value comparison will be ignored.".format(
|
| 196 |
+
source_dir
|
| 197 |
+
))
|
| 198 |
+
return
|
| 199 |
+
|
| 200 |
+
cur_hash = subprocess.getoutput("git rev-parse HEAD")
|
| 201 |
+
|
| 202 |
+
path = os.path.join(model_dir, "githash")
|
| 203 |
+
if os.path.exists(path):
|
| 204 |
+
saved_hash = open(path).read()
|
| 205 |
+
if saved_hash != cur_hash:
|
| 206 |
+
logger.warn("git hash values are different. {}(saved) != {}(current)".format(
|
| 207 |
+
saved_hash[:8], cur_hash[:8]))
|
| 208 |
+
else:
|
| 209 |
+
open(path, "w").write(cur_hash)
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def get_logger(model_dir, filename="train.log"):
|
| 213 |
+
global logger
|
| 214 |
+
logger = logging.getLogger(os.path.basename(model_dir))
|
| 215 |
+
logger.setLevel(logging.DEBUG)
|
| 216 |
+
|
| 217 |
+
formatter = logging.Formatter("%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s")
|
| 218 |
+
if not os.path.exists(model_dir):
|
| 219 |
+
os.makedirs(model_dir)
|
| 220 |
+
h = logging.FileHandler(os.path.join(model_dir, filename))
|
| 221 |
+
h.setLevel(logging.DEBUG)
|
| 222 |
+
h.setFormatter(formatter)
|
| 223 |
+
logger.addHandler(h)
|
| 224 |
+
return logger
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
class HParams():
|
| 228 |
+
def __init__(self, **kwargs):
|
| 229 |
+
for k, v in kwargs.items():
|
| 230 |
+
if type(v) == dict:
|
| 231 |
+
v = HParams(**v)
|
| 232 |
+
self[k] = v
|
| 233 |
+
|
| 234 |
+
def keys(self):
|
| 235 |
+
return self.__dict__.keys()
|
| 236 |
+
|
| 237 |
+
def items(self):
|
| 238 |
+
return self.__dict__.items()
|
| 239 |
+
|
| 240 |
+
def values(self):
|
| 241 |
+
return self.__dict__.values()
|
| 242 |
+
|
| 243 |
+
def __len__(self):
|
| 244 |
+
return len(self.__dict__)
|
| 245 |
+
|
| 246 |
+
def __getitem__(self, key):
|
| 247 |
+
return getattr(self, key)
|
| 248 |
+
|
| 249 |
+
def __setitem__(self, key, value):
|
| 250 |
+
return setattr(self, key, value)
|
| 251 |
+
|
| 252 |
+
def __contains__(self, key):
|
| 253 |
+
return key in self.__dict__
|
| 254 |
+
|
| 255 |
+
def __repr__(self):
|
| 256 |
+
return self.__dict__.__repr__()
|
samples/clara_intro.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ea6341d6b631f3c2898503e30c5c6a0a6ee4f7e5fcaf4baac22f271d77f139ed
|
| 3 |
+
size 494636
|
samples/pip_intro.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bc6c66ac08a3bc6a533aae6fcde059ec30ad6386af92801f350be374122f0118
|
| 3 |
+
size 327212
|
samples/scrappy_intro.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:dd0ebed0b0a2bbeedbb4849309e7b67c49e269eb8b00ba9527dd5d2daf08ac98
|
| 3 |
+
size 525868
|
samples/silas_intro.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3c315e12d7235e2a957a90166b5d1ab44b9c49c68955ca7fb1072399540b5a02
|
| 3 |
+
size 420396
|
speakers.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"n_speakers": 4,
|
| 3 |
+
"speakers": {
|
| 4 |
+
"scrappy": 0,
|
| 5 |
+
"clara": 1,
|
| 6 |
+
"silas": 2,
|
| 7 |
+
"pip": 3
|
| 8 |
+
},
|
| 9 |
+
"counts": {
|
| 10 |
+
"scrappy": {
|
| 11 |
+
"train": 4122,
|
| 12 |
+
"val": 216
|
| 13 |
+
},
|
| 14 |
+
"clara": {
|
| 15 |
+
"train": 4155,
|
| 16 |
+
"val": 218
|
| 17 |
+
},
|
| 18 |
+
"silas": {
|
| 19 |
+
"train": 4150,
|
| 20 |
+
"val": 218
|
| 21 |
+
},
|
| 22 |
+
"pip": {
|
| 23 |
+
"train": 4119,
|
| 24 |
+
"val": 216
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
}
|
third_party/ALIAS_FREE_TORCH_LICENSE.txt
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.
|
third_party/BIGVGAN_LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2024 NVIDIA CORPORATION.
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
third_party/VITS_LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2021 Jaehyeon Kim
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
trainer/eval_candidate_ms.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Synthesize eval prompts from a MULTI-SPEAKER training candidate.
|
| 3 |
+
|
| 4 |
+
Same flow as eval_candidate.py, two differences: the model is rebuilt from a
|
| 5 |
+
config that carries n_speakers/gin_channels (train_ms.py writes
|
| 6 |
+
<run-dir>/config_ms.json — the package config.json alone reconstructs the
|
| 7 |
+
single-speaker graph and the strict load fails), and infer() gets a sid.
|
| 8 |
+
"""
|
| 9 |
+
import argparse
|
| 10 |
+
import json
|
| 11 |
+
import os
|
| 12 |
+
import sys
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
|
| 15 |
+
import soundfile as sf
|
| 16 |
+
import torch
|
| 17 |
+
|
| 18 |
+
BASE = Path(__file__).resolve().parents[1]
|
| 19 |
+
PKG = Path(os.environ.get("INFLECT_PKG", BASE / "Inflect-Micro-v2"))
|
| 20 |
+
sys.path.insert(0, str(PKG / "runtime"))
|
| 21 |
+
sys.path.insert(0, str(PKG))
|
| 22 |
+
|
| 23 |
+
import commons # noqa: E402
|
| 24 |
+
import utils # noqa: E402
|
| 25 |
+
from inference import edge_fade, optimize_for_inference # noqa: E402
|
| 26 |
+
from inflect_vits_frontend import run_vits_frontend # noqa: E402
|
| 27 |
+
from models import SynthesizerTrn # noqa: E402
|
| 28 |
+
from text import cleaned_text_to_sequence # noqa: E402
|
| 29 |
+
from text.symbols import symbols # noqa: E402
|
| 30 |
+
|
| 31 |
+
PROMPTS = [
|
| 32 |
+
("held1", "The committee will publish its findings on the fourteenth of October."),
|
| 33 |
+
("held2", "Beneath the ice, the ocean kept its own kind of time."),
|
| 34 |
+
("held3", "First, check the seal. Second, note the pressure. Finally, log both numbers."),
|
| 35 |
+
("held4", "Was it courage, or simply the absence of any other option?"),
|
| 36 |
+
("held5", "The fleet in this house runs the same quality inference that once required a data center."),
|
| 37 |
+
]
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def main():
|
| 41 |
+
ap = argparse.ArgumentParser()
|
| 42 |
+
ap.add_argument("--candidate", required=True, help="ms candidate .pth from train_ms.py")
|
| 43 |
+
ap.add_argument("--sid", required=True, type=int, help="speaker id (see speakers.json)")
|
| 44 |
+
ap.add_argument("--config", default=None,
|
| 45 |
+
help="config with model.n_speakers/gin_channels; default: config_ms.json two dirs up from the candidate (the run dir)")
|
| 46 |
+
ap.add_argument("--out", required=True)
|
| 47 |
+
ap.add_argument("--seed", type=int, default=7)
|
| 48 |
+
args = ap.parse_args()
|
| 49 |
+
|
| 50 |
+
cfg = Path(args.config) if args.config else Path(args.candidate).resolve().parent.parent / "config_ms.json"
|
| 51 |
+
assert cfg.exists(), f"no config at {cfg} — pass --config <run-dir>/config_ms.json"
|
| 52 |
+
hps = utils.get_hparams_from_file(str(cfg))
|
| 53 |
+
model_cfg = dict(hps.model)
|
| 54 |
+
n_speakers = model_cfg.get("n_speakers", 0)
|
| 55 |
+
assert n_speakers >= 2, f"{cfg} has n_speakers={n_speakers} — not a multi-speaker config"
|
| 56 |
+
assert 0 <= args.sid < n_speakers, f"--sid {args.sid} out of range for n_speakers={n_speakers}"
|
| 57 |
+
|
| 58 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 59 |
+
net = SynthesizerTrn(len(symbols), hps.data.filter_length // 2 + 1,
|
| 60 |
+
hps.train.segment_size // hps.data.hop_length,
|
| 61 |
+
**model_cfg).to(device).eval()
|
| 62 |
+
ck = torch.load(args.candidate, map_location="cpu", weights_only=True)
|
| 63 |
+
net.load_state_dict(ck["model"], strict=True)
|
| 64 |
+
optimize_for_inference(net)
|
| 65 |
+
sid = torch.LongTensor([args.sid]).to(device)
|
| 66 |
+
|
| 67 |
+
out = Path(args.out)
|
| 68 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 69 |
+
(out / "prompts.tsv").write_text("\n".join(f"{k}\t{t}" for k, t in PROMPTS) + "\n")
|
| 70 |
+
with torch.inference_mode():
|
| 71 |
+
for key, text in PROMPTS:
|
| 72 |
+
phonemes = run_vits_frontend(text).phoneme_text
|
| 73 |
+
seq = cleaned_text_to_sequence(phonemes)
|
| 74 |
+
if hps.data.add_blank:
|
| 75 |
+
seq = commons.intersperse(seq, 0)
|
| 76 |
+
tokens = torch.LongTensor(seq).unsqueeze(0).to(device)
|
| 77 |
+
lengths = torch.LongTensor([tokens.size(1)]).to(device)
|
| 78 |
+
torch.manual_seed(args.seed)
|
| 79 |
+
wav = net.infer(tokens, lengths, sid=sid, noise_scale=0.667, noise_scale_w=0.8,
|
| 80 |
+
length_scale=1.0)[0][0, 0].float().cpu().numpy()
|
| 81 |
+
sf.write(out / f"{key}.wav", edge_fade(wav, hps.data.sampling_rate), hps.data.sampling_rate)
|
| 82 |
+
print(f"{key} (sid {args.sid}): {len(wav)/hps.data.sampling_rate:.1f}s")
|
| 83 |
+
print(f"eval wavs → {out}")
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
if __name__ == "__main__":
|
| 87 |
+
main()
|
trainer/prep_filelists_ms.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Multi-corpus survivors → multi-speaker filelists (wav_path|speaker_id|phoneme_text).
|
| 3 |
+
|
| 4 |
+
Each corpus contributes one speaker: DIR/survivors.tsv (the QC pass-list) is
|
| 5 |
+
phonemized with the package's exact frontend — same as prep_filelists.py — and
|
| 6 |
+
OOV-symbol lines are dropped. The val split is stratified PER SPEAKER so a
|
| 7 |
+
small second corpus keeps val coverage. Writes filelist_train_ms.txt,
|
| 8 |
+
filelist_val_ms.txt and speakers.json (the name→id map train/eval bind to).
|
| 9 |
+
|
| 10 |
+
Speaker ids must be exactly 0..N-1 with N >= 2: upstream models.py creates
|
| 11 |
+
emb_g only for n_speakers > 1 but uses it for n_speakers > 0, so 1 crashes.
|
| 12 |
+
|
| 13 |
+
Corpora may reuse clip ids (scrappy2 renders the same nar_* ids as corpus/) —
|
| 14 |
+
the absolute wav path is the only unique key, never merge on bare id.
|
| 15 |
+
"""
|
| 16 |
+
import argparse
|
| 17 |
+
import json
|
| 18 |
+
import os
|
| 19 |
+
import random
|
| 20 |
+
import sys
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
|
| 23 |
+
BASE = Path(__file__).resolve().parents[1]
|
| 24 |
+
PKG = Path(os.environ.get("INFLECT_PKG", BASE / "Inflect-Micro-v2"))
|
| 25 |
+
sys.path.insert(0, str(PKG / "runtime"))
|
| 26 |
+
sys.path.insert(0, str(PKG))
|
| 27 |
+
|
| 28 |
+
from inflect_vits_frontend import run_vits_frontend_batch # noqa: E402
|
| 29 |
+
from text.symbols import symbols # noqa: E402
|
| 30 |
+
|
| 31 |
+
KNOWN = set(symbols)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def parse_specs(args):
|
| 35 |
+
"""[(dir, sid, name, survivors, wavs), ...] from --corpus and/or --spec."""
|
| 36 |
+
specs = []
|
| 37 |
+
for c in args.corpus or []:
|
| 38 |
+
parts = c.split(":")
|
| 39 |
+
if len(parts) != 3:
|
| 40 |
+
sys.exit(f"--corpus wants DIR:SID:NAME, got {c!r}")
|
| 41 |
+
d = Path(parts[0]).resolve()
|
| 42 |
+
specs.append({"dir": d, "sid": int(parts[1]), "name": parts[2],
|
| 43 |
+
"survivors": d / "survivors.tsv", "wavs": d / "wavs24k"})
|
| 44 |
+
if args.spec:
|
| 45 |
+
for e in json.loads(Path(args.spec).read_text()):
|
| 46 |
+
d = Path(e["dir"]).resolve()
|
| 47 |
+
specs.append({"dir": d, "sid": int(e["sid"]), "name": e["name"],
|
| 48 |
+
"survivors": (d / e["survivors"]).resolve() if "survivors" in e else d / "survivors.tsv",
|
| 49 |
+
"wavs": (d / e["wavs"]).resolve() if "wavs" in e else d / "wavs24k"})
|
| 50 |
+
if len(specs) < 2:
|
| 51 |
+
sys.exit(f"need >= 2 speakers (n_speakers=1 crashes upstream), got {len(specs)}")
|
| 52 |
+
sids = sorted(s["sid"] for s in specs)
|
| 53 |
+
if sids != list(range(len(specs))):
|
| 54 |
+
sys.exit(f"speaker ids must be exactly 0..{len(specs) - 1}, got {sids}")
|
| 55 |
+
if len({s["name"] for s in specs}) != len(specs):
|
| 56 |
+
sys.exit("duplicate speaker names")
|
| 57 |
+
for s in specs:
|
| 58 |
+
if not s["survivors"].exists():
|
| 59 |
+
sys.exit(f"{s['name']}: no QC pass-list at {s['survivors']} — run scripts/qc.py first")
|
| 60 |
+
if not s["wavs"].is_dir():
|
| 61 |
+
sys.exit(f"{s['name']}: no wav dir at {s['wavs']}")
|
| 62 |
+
return specs
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def main():
|
| 66 |
+
ap = argparse.ArgumentParser()
|
| 67 |
+
ap.add_argument("--corpus", action="append", metavar="DIR:SID:NAME",
|
| 68 |
+
help="repeatable, e.g. --corpus corpus:0:narrator --corpus corpus-clara:1:clara")
|
| 69 |
+
ap.add_argument("--spec", help="JSON list of {dir, sid, name[, survivors, wavs]} (alternative to --corpus)")
|
| 70 |
+
ap.add_argument("--val-frac", type=float, default=0.05)
|
| 71 |
+
ap.add_argument("--seed", type=int, default=7)
|
| 72 |
+
ap.add_argument("--out-dir", default=str(BASE / "corpus-ms"))
|
| 73 |
+
args = ap.parse_args()
|
| 74 |
+
|
| 75 |
+
specs = parse_specs(args)
|
| 76 |
+
rng = random.Random(args.seed)
|
| 77 |
+
train, val, counts = [], [], {}
|
| 78 |
+
for s in specs:
|
| 79 |
+
rows = [l.rstrip("\n").split("\t", 1) for l in open(s["survivors"]) if "\t" in l]
|
| 80 |
+
texts = [t for _, t in rows]
|
| 81 |
+
print(f"[{s['name']} sid={s['sid']}] phonemizing {len(rows)} lines via Inflect frontend…")
|
| 82 |
+
outs = run_vits_frontend_batch(texts, jobs=4)
|
| 83 |
+
|
| 84 |
+
entries, oov = [], 0
|
| 85 |
+
for (fid, _), out in zip(rows, outs):
|
| 86 |
+
wav = s["wavs"] / f"{fid}.wav"
|
| 87 |
+
if not (wav.exists() and out.phoneme_text):
|
| 88 |
+
continue
|
| 89 |
+
bad = set(out.phoneme_text) - KNOWN
|
| 90 |
+
if bad:
|
| 91 |
+
oov += 1
|
| 92 |
+
continue
|
| 93 |
+
entries.append(f"{wav}|{s['sid']}|{out.phoneme_text}")
|
| 94 |
+
if oov:
|
| 95 |
+
print(f"[{s['name']}] dropped {oov} lines with out-of-vocabulary phoneme symbols")
|
| 96 |
+
|
| 97 |
+
rng.shuffle(entries)
|
| 98 |
+
n_val = max(4, int(len(entries) * args.val_frac))
|
| 99 |
+
if len(entries) - n_val < 1:
|
| 100 |
+
sys.exit(f"[{s['name']}] only {len(entries)} usable clips — not enough to train")
|
| 101 |
+
val += entries[:n_val]
|
| 102 |
+
train += entries[n_val:]
|
| 103 |
+
counts[s["name"]] = {"train": len(entries) - n_val, "val": n_val}
|
| 104 |
+
print(f"[{s['name']}] {len(entries) - n_val} train / {n_val} val")
|
| 105 |
+
|
| 106 |
+
rng.shuffle(train)
|
| 107 |
+
rng.shuffle(val)
|
| 108 |
+
out_dir = Path(args.out_dir)
|
| 109 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 110 |
+
(out_dir / "filelist_train_ms.txt").write_text("\n".join(train) + "\n")
|
| 111 |
+
(out_dir / "filelist_val_ms.txt").write_text("\n".join(val) + "\n")
|
| 112 |
+
meta = {"n_speakers": len(specs),
|
| 113 |
+
"speakers": {s["name"]: s["sid"] for s in specs},
|
| 114 |
+
"corpora": {s["name"]: str(s["dir"]) for s in specs},
|
| 115 |
+
"counts": counts}
|
| 116 |
+
(out_dir / "speakers.json").write_text(json.dumps(meta, indent=2) + "\n")
|
| 117 |
+
print(f"{len(train)} train / {len(val)} val ({len(specs)} speakers) → {out_dir}/filelist_*_ms.txt + speakers.json")
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
if __name__ == "__main__":
|
| 121 |
+
main()
|
trainer/train_ms.py
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Warm-start multi-speaker fine-tune of Inflect-Micro-v2 (N voices, one model).
|
| 3 |
+
|
| 4 |
+
Same recipe as train_ft.py but VITS multi-speaker: SynthesizerTrn is built with
|
| 5 |
+
n_speakers/gin_channels so the runtime's retained hooks light up — emb_g plus
|
| 6 |
+
the g-conditioning already wired into enc_q (WN cond_layer), flow (per-coupling
|
| 7 |
+
cond_layer), dp (cond) and dec (cond). enc_p takes no g, same as upstream VITS.
|
| 8 |
+
|
| 9 |
+
Warm-start from the single-speaker model.pth with strict=False: every base
|
| 10 |
+
tensor keeps its shape (gin conditioning is additive), so 100% of the base
|
| 11 |
+
loads and ONLY {enc_q.*, emb_g.*, dec.cond.*, dp.cond.*, flow cond_layer.*}
|
| 12 |
+
initialize fresh — anything else missing fails loudly (see warm_start_ms).
|
| 13 |
+
|
| 14 |
+
Filelists are 3-column wav_path|speaker_id|phoneme_text from
|
| 15 |
+
prep_filelists_ms.py. Spec caches are redirected OUT of the corpora dirs
|
| 16 |
+
(train_ft.py writes .spec.pt next to the wavs; here corpora stay read-only —
|
| 17 |
+
a live render may be writing next to them).
|
| 18 |
+
"""
|
| 19 |
+
import argparse
|
| 20 |
+
import hashlib
|
| 21 |
+
import json
|
| 22 |
+
import math
|
| 23 |
+
import os
|
| 24 |
+
import sys
|
| 25 |
+
import time
|
| 26 |
+
from collections import Counter
|
| 27 |
+
from pathlib import Path
|
| 28 |
+
|
| 29 |
+
import numpy as np
|
| 30 |
+
import soundfile as sf
|
| 31 |
+
import torch
|
| 32 |
+
import torch.nn.functional as F
|
| 33 |
+
from torch.utils.data import DataLoader, Dataset
|
| 34 |
+
|
| 35 |
+
BASE = Path(__file__).resolve().parents[1]
|
| 36 |
+
PKG = Path(os.environ.get("INFLECT_PKG", BASE / "Inflect-Micro-v2"))
|
| 37 |
+
sys.path.insert(0, str(PKG / "runtime"))
|
| 38 |
+
sys.path.insert(0, str(PKG))
|
| 39 |
+
|
| 40 |
+
import commons # noqa: E402
|
| 41 |
+
import utils # noqa: E402
|
| 42 |
+
from models import MultiPeriodDiscriminator, SynthesizerTrn # noqa: E402
|
| 43 |
+
from text import cleaned_text_to_sequence # noqa: E402
|
| 44 |
+
from text.symbols import symbols # noqa: E402
|
| 45 |
+
|
| 46 |
+
# ------------------------------------------- transforms (same as train_ft.py)
|
| 47 |
+
_mel_basis = {}
|
| 48 |
+
_hann = {}
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _window(win_size, device, dtype):
|
| 52 |
+
key = f"{win_size}_{device}_{dtype}"
|
| 53 |
+
if key not in _hann:
|
| 54 |
+
_hann[key] = torch.hann_window(win_size).to(device=device, dtype=dtype)
|
| 55 |
+
return _hann[key]
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def spectrogram_torch(y, n_fft, hop_size, win_size):
|
| 59 |
+
y = F.pad(y.unsqueeze(1), (int((n_fft - hop_size) / 2), int((n_fft - hop_size) / 2)),
|
| 60 |
+
mode="reflect").squeeze(1)
|
| 61 |
+
spec = torch.stft(y, n_fft, hop_length=hop_size, win_length=win_size,
|
| 62 |
+
window=_window(win_size, y.device, y.dtype),
|
| 63 |
+
center=False, pad_mode="reflect", normalized=False,
|
| 64 |
+
onesided=True, return_complex=True)
|
| 65 |
+
return torch.sqrt(spec.real ** 2 + spec.imag ** 2 + 1e-6)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _mel_fb(n_fft, num_mels, sr, fmin, fmax, device, dtype):
|
| 69 |
+
key = f"{n_fft}_{num_mels}_{fmax}_{device}_{dtype}"
|
| 70 |
+
if key not in _mel_basis:
|
| 71 |
+
from torchaudio.functional import melscale_fbanks
|
| 72 |
+
# slaney/slaney == librosa_mel_fn defaults used by upstream VITS
|
| 73 |
+
fb = melscale_fbanks(n_fft // 2 + 1, fmin, fmax, num_mels, sr,
|
| 74 |
+
norm="slaney", mel_scale="slaney").T
|
| 75 |
+
_mel_basis[key] = fb.to(device=device, dtype=dtype)
|
| 76 |
+
return _mel_basis[key]
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def spec_to_mel_torch(spec, n_fft, num_mels, sr, fmin, fmax):
|
| 80 |
+
mel = torch.matmul(_mel_fb(n_fft, num_mels, sr, fmin, fmax, spec.device, spec.dtype), spec)
|
| 81 |
+
return torch.log(torch.clamp(mel, min=1e-5))
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def mel_spectrogram_torch(y, n_fft, num_mels, sr, hop_size, win_size, fmin, fmax):
|
| 85 |
+
return spec_to_mel_torch(spectrogram_torch(y, n_fft, hop_size, win_size),
|
| 86 |
+
n_fft, num_mels, sr, fmin, fmax)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
# ----------------------------------------------- losses (same as train_ft.py)
|
| 90 |
+
def feature_loss(fmap_r, fmap_g):
|
| 91 |
+
loss = 0
|
| 92 |
+
for dr, dg in zip(fmap_r, fmap_g):
|
| 93 |
+
for rl, gl in zip(dr, dg):
|
| 94 |
+
loss += torch.mean(torch.abs(rl.float().detach() - gl.float()))
|
| 95 |
+
return loss * 2
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def discriminator_loss(disc_real, disc_gen):
|
| 99 |
+
loss = 0
|
| 100 |
+
for dr, dg in zip(disc_real, disc_gen):
|
| 101 |
+
loss += torch.mean((1 - dr.float()) ** 2) + torch.mean(dg.float() ** 2)
|
| 102 |
+
return loss
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def generator_loss(disc_outputs):
|
| 106 |
+
loss = 0
|
| 107 |
+
for dg in disc_outputs:
|
| 108 |
+
loss += torch.mean((1 - dg.float()) ** 2)
|
| 109 |
+
return loss
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def kl_loss(z_p, logs_q, m_p, logs_p, z_mask):
|
| 113 |
+
z_p, logs_q = z_p.float(), logs_q.float()
|
| 114 |
+
m_p, logs_p = m_p.float(), logs_p.float()
|
| 115 |
+
z_mask = z_mask.float()
|
| 116 |
+
kl = logs_p - logs_q - 0.5
|
| 117 |
+
kl += 0.5 * ((z_p - m_p) ** 2) * torch.exp(-2.0 * logs_p)
|
| 118 |
+
return torch.sum(kl * z_mask) / torch.sum(z_mask)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
# ---------------------------------------------------------------- warm-start
|
| 122 |
+
MS_FRESH_PREFIXES = ("enc_q.", "emb_g.", "dec.cond.", "dp.cond.")
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def is_expected_fresh(key):
|
| 126 |
+
"""Keys that legitimately have no source tensor in the single-speaker base:
|
| 127 |
+
the whole posterior encoder (base ck is inference-only — already fresh in
|
| 128 |
+
train_ft.py), the speaker embedding, and every gin cond layer (dec.cond,
|
| 129 |
+
dp.cond, flow.flows.{0,2,4,6}.enc.cond_layer.{bias,weight_g,weight_v})."""
|
| 130 |
+
if key.startswith(MS_FRESH_PREFIXES):
|
| 131 |
+
return True
|
| 132 |
+
return key.startswith("flow.") and ".cond_layer." in key
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def warm_start_ms(net_g, base_sd):
|
| 136 |
+
"""strict=False load with an EXPLICIT whitelist — logs every fresh key and
|
| 137 |
+
fails loudly on anything missing outside it (or unexpected in the ck)."""
|
| 138 |
+
missing, unexpected = net_g.load_state_dict(base_sd, strict=False)
|
| 139 |
+
fresh_ok = sorted(k for k in missing if is_expected_fresh(k))
|
| 140 |
+
fresh_bad = sorted(k for k in missing if not is_expected_fresh(k))
|
| 141 |
+
n_total = len(net_g.state_dict())
|
| 142 |
+
print(f"warm-start: {len(base_sd)} tensors offered, {n_total - len(missing)} loaded, "
|
| 143 |
+
f"{len(missing)} fresh, {len(unexpected)} unexpected")
|
| 144 |
+
for k in fresh_ok:
|
| 145 |
+
print(f" fresh (expected): {k}")
|
| 146 |
+
for k in fresh_bad:
|
| 147 |
+
print(f" MISSING (not in multi-speaker whitelist): {k}")
|
| 148 |
+
for k in unexpected:
|
| 149 |
+
print(f" UNEXPECTED in checkpoint: {k}")
|
| 150 |
+
assert not unexpected, f"unexpected keys in base checkpoint: {unexpected}"
|
| 151 |
+
assert not fresh_bad, f"missing keys outside the multi-speaker whitelist: {fresh_bad}"
|
| 152 |
+
assert any(k.startswith("emb_g.") for k in missing), \
|
| 153 |
+
"emb_g not fresh — n_speakers/gin_channels did not take effect"
|
| 154 |
+
return missing, unexpected
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
# ---------------------------------------------------------------- data
|
| 158 |
+
class TextAudioSpeakerDataset(Dataset):
|
| 159 |
+
"""3-column filelist: wav_path|speaker_id|phoneme_text (split on first TWO
|
| 160 |
+
pipes). Spec cache goes to spec_cache_dir keyed by wav-path hash — never
|
| 161 |
+
next to the wavs (corpora stay read-only)."""
|
| 162 |
+
|
| 163 |
+
def __init__(self, filelist, hps, spec_cache_dir):
|
| 164 |
+
self.items = [l.strip().split("|", 2) for l in open(filelist) if l.count("|") >= 2]
|
| 165 |
+
self.hps = hps
|
| 166 |
+
self.cache = Path(spec_cache_dir)
|
| 167 |
+
self.cache.mkdir(parents=True, exist_ok=True)
|
| 168 |
+
|
| 169 |
+
def __len__(self):
|
| 170 |
+
return len(self.items)
|
| 171 |
+
|
| 172 |
+
def __getitem__(self, idx):
|
| 173 |
+
wav_path, sid, phonemes = self.items[idx]
|
| 174 |
+
seq = cleaned_text_to_sequence(phonemes)
|
| 175 |
+
if self.hps.data.add_blank:
|
| 176 |
+
seq = commons.intersperse(seq, 0)
|
| 177 |
+
text = torch.LongTensor(seq)
|
| 178 |
+
audio, sr = sf.read(wav_path, dtype="float32")
|
| 179 |
+
assert sr == self.hps.data.sampling_rate, f"{wav_path}: {sr}"
|
| 180 |
+
audio = torch.from_numpy(audio).float().unsqueeze(0)
|
| 181 |
+
st = os.stat(wav_path)
|
| 182 |
+
spec_path = self.cache / f"{hashlib.sha1(f'{wav_path}:{st.st_size}:{st.st_mtime_ns}'.encode()).hexdigest()}.spec.pt"
|
| 183 |
+
try:
|
| 184 |
+
spec = torch.load(spec_path, weights_only=True)
|
| 185 |
+
except (FileNotFoundError, RuntimeError):
|
| 186 |
+
spec = spectrogram_torch(audio, self.hps.data.filter_length,
|
| 187 |
+
self.hps.data.hop_length, self.hps.data.win_length).squeeze(0)
|
| 188 |
+
torch.save(spec, spec_path)
|
| 189 |
+
return text, spec, audio.squeeze(0), int(sid)
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def collate(batch):
|
| 193 |
+
batch = sorted(batch, key=lambda x: x[1].size(1), reverse=True)
|
| 194 |
+
max_t = max(x[0].size(0) for x in batch)
|
| 195 |
+
max_s = max(x[1].size(1) for x in batch)
|
| 196 |
+
max_w = max(x[2].size(0) for x in batch)
|
| 197 |
+
n = len(batch)
|
| 198 |
+
text = torch.zeros(n, max_t, dtype=torch.long)
|
| 199 |
+
spec = torch.zeros(n, batch[0][1].size(0), max_s)
|
| 200 |
+
wav = torch.zeros(n, 1, max_w)
|
| 201 |
+
tl, sl, wl = (torch.zeros(n, dtype=torch.long) for _ in range(3))
|
| 202 |
+
sid = torch.LongTensor([x[3] for x in batch])
|
| 203 |
+
for i, (t, s, w, _) in enumerate(batch):
|
| 204 |
+
text[i, :t.size(0)] = t
|
| 205 |
+
spec[i, :, :s.size(1)] = s
|
| 206 |
+
wav[i, 0, :w.size(0)] = w
|
| 207 |
+
tl[i], sl[i], wl[i] = t.size(0), s.size(1), w.size(0)
|
| 208 |
+
return text, tl, spec, sl, wav, wl, sid
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
# ---------------------------------------------------------------- export
|
| 212 |
+
def export_candidate(net_g, step, lr, out_dir, orig_ck_format):
|
| 213 |
+
sd = {k: v for k, v in net_g.state_dict().items() if not k.startswith("enc_q.")}
|
| 214 |
+
out = {"format": orig_ck_format, "model": sd, "iteration": step,
|
| 215 |
+
"learning_rate": lr, "deployable_parameters": sum(v.numel() for v in sd.values())}
|
| 216 |
+
path = Path(out_dir) / f"candidate_{step:06d}.pth"
|
| 217 |
+
torch.save(out, path)
|
| 218 |
+
return path
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
# ---------------------------------------------------------------- main
|
| 222 |
+
def main():
|
| 223 |
+
ap = argparse.ArgumentParser()
|
| 224 |
+
ap.add_argument("--train-filelist", default=str(BASE / "corpus-ms/filelist_train_ms.txt"))
|
| 225 |
+
ap.add_argument("--val-filelist", default=str(BASE / "corpus-ms/filelist_val_ms.txt"))
|
| 226 |
+
ap.add_argument("--speakers", default=str(BASE / "corpus-ms/speakers.json"))
|
| 227 |
+
ap.add_argument("--n-speakers", type=int, default=None,
|
| 228 |
+
help="default: n_speakers from --speakers json (must be >= 2)")
|
| 229 |
+
ap.add_argument("--gin-channels", type=int, default=256)
|
| 230 |
+
ap.add_argument("--spec-cache-dir", default=None, help="default: <run-dir>/spec_cache")
|
| 231 |
+
ap.add_argument("--run-dir", default=str(BASE / "runs/ms-pilot"))
|
| 232 |
+
ap.add_argument("--batch-size", type=int, default=24)
|
| 233 |
+
ap.add_argument("--lr-g", type=float, default=1e-4)
|
| 234 |
+
ap.add_argument("--lr-d", type=float, default=2e-4)
|
| 235 |
+
ap.add_argument("--lr-gamma", type=float, default=0.9999)
|
| 236 |
+
ap.add_argument("--freeze-dec-steps", type=int, default=1500)
|
| 237 |
+
ap.add_argument("--max-steps", type=int, default=12000)
|
| 238 |
+
ap.add_argument("--export-every", type=int, default=1000)
|
| 239 |
+
ap.add_argument("--log-every", type=int, default=50)
|
| 240 |
+
ap.add_argument("--c-mel", type=float, default=45.0)
|
| 241 |
+
ap.add_argument("--c-kl", type=float, default=1.0)
|
| 242 |
+
args = ap.parse_args()
|
| 243 |
+
|
| 244 |
+
if args.n_speakers is None:
|
| 245 |
+
meta = json.loads(Path(args.speakers).read_text())
|
| 246 |
+
args.n_speakers = int(meta["n_speakers"])
|
| 247 |
+
print(f"speakers: {meta['speakers']}")
|
| 248 |
+
assert args.n_speakers >= 2, \
|
| 249 |
+
f"n_speakers must be >= 2 (upstream creates emb_g only for > 1 but uses it for > 0), got {args.n_speakers}"
|
| 250 |
+
|
| 251 |
+
device = torch.device("cuda")
|
| 252 |
+
run_dir = Path(args.run_dir)
|
| 253 |
+
(run_dir / "candidates").mkdir(parents=True, exist_ok=True)
|
| 254 |
+
hps = utils.get_hparams_from_file(str(PKG / "config.json"))
|
| 255 |
+
orig_ck = torch.load(PKG / "model.pth", map_location="cpu", weights_only=True)
|
| 256 |
+
ck_format = orig_ck.get("format", "inflect_v2_inference_config_v1")
|
| 257 |
+
|
| 258 |
+
# deployable config for MS candidates: same as the package's, plus the two
|
| 259 |
+
# keys inference needs to rebuild this graph (eval_candidate_ms.py reads it)
|
| 260 |
+
deploy_cfg = json.loads((PKG / "config.json").read_text())
|
| 261 |
+
deploy_cfg["model"]["n_speakers"] = args.n_speakers
|
| 262 |
+
deploy_cfg["model"]["gin_channels"] = args.gin_channels
|
| 263 |
+
deploy_cfg["data"]["n_speakers"] = args.n_speakers
|
| 264 |
+
(run_dir / "config_ms.json").write_text(json.dumps(deploy_cfg, indent=2) + "\n")
|
| 265 |
+
|
| 266 |
+
model_cfg = dict(hps.model)
|
| 267 |
+
model_cfg["inference_only"] = False
|
| 268 |
+
model_cfg["n_speakers"] = args.n_speakers
|
| 269 |
+
model_cfg["gin_channels"] = args.gin_channels
|
| 270 |
+
seg_frames = hps.train.segment_size // hps.data.hop_length
|
| 271 |
+
net_g = SynthesizerTrn(len(symbols), hps.data.filter_length // 2 + 1,
|
| 272 |
+
seg_frames, **model_cfg).to(device)
|
| 273 |
+
net_d = MultiPeriodDiscriminator(model_cfg.get("use_spectral_norm", False)).to(device)
|
| 274 |
+
|
| 275 |
+
warm_start_ms(net_g, orig_ck["model"])
|
| 276 |
+
# dec.cond/dp.cond are plain Conv1d: zero-init makes them identity at step 0
|
| 277 |
+
# so the frozen decoder hears the converged base unperturbed. The WN
|
| 278 |
+
# cond_layers stay default init — zeroing weight_v under weight_norm is unsafe.
|
| 279 |
+
torch.nn.init.zeros_(net_g.dec.cond.weight)
|
| 280 |
+
torch.nn.init.zeros_(net_g.dec.cond.bias)
|
| 281 |
+
torch.nn.init.zeros_(net_g.dp.cond.weight)
|
| 282 |
+
torch.nn.init.zeros_(net_g.dp.cond.bias)
|
| 283 |
+
|
| 284 |
+
opt_g = torch.optim.AdamW(net_g.parameters(), args.lr_g, betas=(0.8, 0.99), eps=1e-9)
|
| 285 |
+
opt_d = torch.optim.AdamW(net_d.parameters(), args.lr_d, betas=(0.8, 0.99), eps=1e-9)
|
| 286 |
+
sched_g = torch.optim.lr_scheduler.ExponentialLR(opt_g, gamma=args.lr_gamma)
|
| 287 |
+
sched_d = torch.optim.lr_scheduler.ExponentialLR(opt_d, gamma=args.lr_gamma)
|
| 288 |
+
|
| 289 |
+
spec_cache = args.spec_cache_dir or (run_dir / "spec_cache")
|
| 290 |
+
ds = TextAudioSpeakerDataset(args.train_filelist, hps, spec_cache)
|
| 291 |
+
counts = Counter(int(s) for _, s, _ in ds.items)
|
| 292 |
+
assert sorted(counts) == list(range(args.n_speakers)), \
|
| 293 |
+
f"train filelist must cover every sid 0..{args.n_speakers - 1} exactly, got {sorted(counts)}"
|
| 294 |
+
dl = DataLoader(ds, batch_size=args.batch_size, shuffle=True, drop_last=True,
|
| 295 |
+
collate_fn=collate, num_workers=4, persistent_workers=True)
|
| 296 |
+
print(f"dataset: {len(ds)} clips, {len(dl)} steps/epoch, batch {args.batch_size}, "
|
| 297 |
+
f"per-speaker: {dict(sorted(counts.items()))}")
|
| 298 |
+
|
| 299 |
+
net_g.dec.requires_grad_(False)
|
| 300 |
+
net_g.dec.cond.requires_grad_(True) # fresh speaker-cond layer trains from step 0
|
| 301 |
+
dec_frozen = True
|
| 302 |
+
print(f"decoder FROZEN for first {args.freeze_dec_steps} steps (dec.cond stays live)")
|
| 303 |
+
|
| 304 |
+
step, t0 = 0, time.perf_counter()
|
| 305 |
+
log = (run_dir / "train_log.jsonl").open("a")
|
| 306 |
+
net_g.train()
|
| 307 |
+
net_d.train()
|
| 308 |
+
while step < args.max_steps:
|
| 309 |
+
for text, tl, spec, sl, wav, wl, sid in dl:
|
| 310 |
+
if step >= args.max_steps:
|
| 311 |
+
break
|
| 312 |
+
if dec_frozen and step >= args.freeze_dec_steps:
|
| 313 |
+
net_g.dec.requires_grad_(True)
|
| 314 |
+
dec_frozen = False
|
| 315 |
+
print(f"step {step}: decoder UNFROZEN")
|
| 316 |
+
text, tl = text.to(device), tl.to(device)
|
| 317 |
+
spec, sl = spec.to(device), sl.to(device)
|
| 318 |
+
wav, sid = wav.to(device), sid.to(device)
|
| 319 |
+
|
| 320 |
+
y_hat, l_length, attn, ids_slice, x_mask, z_mask, \
|
| 321 |
+
(z, z_p, m_p, logs_p, m_q, logs_q) = net_g(text, tl, spec, sl, sid=sid)
|
| 322 |
+
|
| 323 |
+
mel = spec_to_mel_torch(spec, hps.data.filter_length, hps.data.n_mel_channels,
|
| 324 |
+
hps.data.sampling_rate, hps.data.mel_fmin, hps.data.mel_fmax)
|
| 325 |
+
y_mel = commons.slice_segments(mel, ids_slice, seg_frames)
|
| 326 |
+
y_hat_mel = mel_spectrogram_torch(y_hat.squeeze(1), hps.data.filter_length,
|
| 327 |
+
hps.data.n_mel_channels, hps.data.sampling_rate,
|
| 328 |
+
hps.data.hop_length, hps.data.win_length,
|
| 329 |
+
hps.data.mel_fmin, hps.data.mel_fmax)
|
| 330 |
+
y = commons.slice_segments(wav, ids_slice * hps.data.hop_length, hps.train.segment_size)
|
| 331 |
+
|
| 332 |
+
# discriminator
|
| 333 |
+
y_d_hat_r, y_d_hat_g, _, _ = net_d(y, y_hat.detach())
|
| 334 |
+
loss_disc = discriminator_loss(y_d_hat_r, y_d_hat_g)
|
| 335 |
+
opt_d.zero_grad()
|
| 336 |
+
loss_disc.backward()
|
| 337 |
+
commons.clip_grad_value_(net_d.parameters(), None)
|
| 338 |
+
opt_d.step()
|
| 339 |
+
|
| 340 |
+
# generator
|
| 341 |
+
y_d_hat_r, y_d_hat_g, fmap_r, fmap_g = net_d(y, y_hat)
|
| 342 |
+
loss_dur = torch.sum(l_length.float())
|
| 343 |
+
loss_mel = F.l1_loss(y_mel, y_hat_mel) * args.c_mel
|
| 344 |
+
loss_kl = kl_loss(z_p, logs_q, m_p, logs_p, z_mask) * args.c_kl
|
| 345 |
+
loss_fm = feature_loss(fmap_r, fmap_g)
|
| 346 |
+
loss_gen = generator_loss(y_d_hat_g)
|
| 347 |
+
loss_all = loss_gen + loss_fm + loss_mel + loss_dur + loss_kl
|
| 348 |
+
opt_g.zero_grad()
|
| 349 |
+
loss_all.backward()
|
| 350 |
+
commons.clip_grad_value_(net_g.parameters(), None)
|
| 351 |
+
opt_g.step()
|
| 352 |
+
sched_g.step()
|
| 353 |
+
sched_d.step()
|
| 354 |
+
step += 1
|
| 355 |
+
|
| 356 |
+
if step % args.log_every == 0:
|
| 357 |
+
rate = step / (time.perf_counter() - t0)
|
| 358 |
+
rec = {"step": step, "g": round(loss_all.item(), 3),
|
| 359 |
+
"mel": round(loss_mel.item(), 3), "dur": round(loss_dur.item(), 4),
|
| 360 |
+
"kl": round(loss_kl.item(), 3), "d": round(loss_disc.item(), 3),
|
| 361 |
+
"lr": sched_g.get_last_lr()[0], "steps_per_s": round(rate, 2)}
|
| 362 |
+
print(json.dumps(rec), flush=True)
|
| 363 |
+
log.write(json.dumps(rec) + "\n")
|
| 364 |
+
log.flush()
|
| 365 |
+
|
| 366 |
+
if step % args.export_every == 0 or step == args.max_steps:
|
| 367 |
+
utils.save_checkpoint(net_g, opt_g, sched_g.get_last_lr()[0], step,
|
| 368 |
+
str(run_dir / "G_latest.pth"))
|
| 369 |
+
utils.save_checkpoint(net_d, opt_d, sched_d.get_last_lr()[0], step,
|
| 370 |
+
str(run_dir / "D_latest.pth"))
|
| 371 |
+
p = export_candidate(net_g, step, sched_g.get_last_lr()[0],
|
| 372 |
+
run_dir / "candidates", ck_format)
|
| 373 |
+
print(f"exported {p}", flush=True)
|
| 374 |
+
|
| 375 |
+
print("training complete")
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
if __name__ == "__main__":
|
| 379 |
+
main()
|