briancconnelly commited on
Commit
70c223f
·
verified ·
1 Parent(s): 9e4618d

Initial release: Scrappy Voice Family — four voices in one checkpoint

Browse files
.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
+ ![Inflect-Micro-v2 release cover](assets/inflect-v2-repository-hero.png)
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
+ ![Community blind listening](assets/evidence/human-preference.svg)
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
+ ![Predicted quality versus footprint](assets/evidence/quality-vs-footprint.svg)
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
+ ![Two-ASR semantic WER consensus](assets/evidence/asr-consensus.svg)
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
+ ![Semantic WER across Qwen3-ASR, Nemotron 3.5, and Whisper large-v3](assets/evidence/modern400-three-asr.svg)
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
+ ![ASR evaluator robustness](assets/evidence/asr-robustness.svg)
135
+
136
+ ![Semantic WER by prompt category](assets/evidence/category-semantic-wer.svg)
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
+ ![Complete deployable model footprint](assets/evidence/model-footprint.svg)
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()