ksingla025 commited on
Commit
e7f5286
·
verified ·
1 Parent(s): 033a34c

SteerTTS v1 (9.6M acoustic model + Vocos; flow refiner, prompted-flow cloning, prompt adapter); CC BY-NC 4.0 weights, MIT code

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. LICENSE.md +34 -0
  2. README.md +366 -0
  3. TEST_LOG.md +78 -0
  4. build/lib/steertts/__init__.py +10 -0
  5. build/lib/steertts/__main__.py +3 -0
  6. build/lib/steertts/audio.py +36 -0
  7. build/lib/steertts/cli.py +157 -0
  8. build/lib/steertts/config.py +27 -0
  9. build/lib/steertts/flow.py +278 -0
  10. build/lib/steertts/mamba3/__init__.py +25 -0
  11. build/lib/steertts/mamba3/layer.py +338 -0
  12. build/lib/steertts/mamba3/reference.py +98 -0
  13. build/lib/steertts/mamba3/scan.py +193 -0
  14. build/lib/steertts/mamba3/ssd.py +201 -0
  15. build/lib/steertts/mamba3/tests.py +230 -0
  16. build/lib/steertts/markup.py +187 -0
  17. build/lib/steertts/measure.py +166 -0
  18. build/lib/steertts/model.py +231 -0
  19. build/lib/steertts/pipeline.py +270 -0
  20. build/lib/steertts/prompt.py +69 -0
  21. build/lib/steertts/request.py +95 -0
  22. build/lib/steertts/request.schema.json +280 -0
  23. build/lib/steertts/speaker.py +27 -0
  24. build/lib/steertts/text.py +133 -0
  25. config.json +2954 -0
  26. control_stats.json +0 -0
  27. flow.safetensors +3 -0
  28. inference.py +59 -0
  29. leaderboard/steertts/Dockerfile +12 -0
  30. leaderboard/steertts/run_eval.py +138 -0
  31. leaderboard/steertts/submit_jobs.sh +39 -0
  32. model.safetensors +3 -0
  33. pflow.safetensors +3 -0
  34. prompt_adapter.safetensors +3 -0
  35. pyproject.toml +44 -0
  36. request.schema.json +280 -0
  37. speaker_vectors.safetensors +3 -0
  38. steertts.egg-info/PKG-INFO +397 -0
  39. steertts.egg-info/SOURCES.txt +33 -0
  40. steertts.egg-info/dependency_links.txt +1 -0
  41. steertts.egg-info/entry_points.txt +2 -0
  42. steertts.egg-info/requires.txt +29 -0
  43. steertts.egg-info/top_level.txt +1 -0
  44. steertts/__init__.py +10 -0
  45. steertts/__main__.py +3 -0
  46. steertts/audio.py +36 -0
  47. steertts/cli.py +157 -0
  48. steertts/config.py +27 -0
  49. steertts/flow.py +278 -0
  50. steertts/mamba3/__init__.py +25 -0
LICENSE.md ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Licence
2
+
3
+ This repository holds two kinds of material, each under its own licence.
4
+
5
+ ## 1. Code: MIT License
6
+
7
+ This covers `steertts/`, `tests/`, `inference.py` and `leaderboard/`.
8
+
9
+ Copyright (c) 2026 The SteerTTS authors
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
12
+ documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
13
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
14
+ persons to whom the Software is furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
17
+ Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
20
+ WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
21
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
22
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ ## 2. Model weights and voice data: CC BY-NC 4.0
25
+
26
+ This covers `*.safetensors`, `voices.json`, `control_stats.json` and `config.json`.
27
+
28
+ - **Licence.** Creative Commons Attribution-NonCommercial 4.0 International,
29
+ https://creativecommons.org/licenses/by-nc/4.0/. Non-commercial use only.
30
+ - **Why.** The weights were trained partly on CC BY-NC 4.0 corpora (EARS, Expresso), on NonverbalTTS (its source
31
+ terms apply), and on synthetic speech from a commercial TTS API, whose terms of service include a
32
+ competing-model clause. Use of the weights must also respect the most restrictive of these terms. The model card
33
+ (`README.md`, "Training data") lists every source.
34
+ - **Voice cloning.** Clone only voices whose owners have consented. See "Intended use and misuse" in the model card.
README.md ADDED
@@ -0,0 +1,366 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language:
3
+ - en
4
+ license: cc-by-nc-4.0
5
+ library_name: steertts
6
+ pipeline_tag: text-to-speech
7
+ tags:
8
+ - text-to-speech
9
+ - tts
10
+ - speech-synthesis
11
+ - english
12
+ - controllable-tts
13
+ - steerable-tts
14
+ - prosody-control
15
+ - expressive-tts
16
+ - style-control
17
+ - inline-markup
18
+ - word-emphasis
19
+ - zero-shot-voice-cloning
20
+ - voice-cloning
21
+ - interpretable
22
+ - non-autoregressive
23
+ - fastpitch
24
+ - mamba
25
+ - mamba-3
26
+ - state-space-model
27
+ - hybrid-attention
28
+ - flow-matching
29
+ - vocos
30
+ - small-model
31
+ - on-device
32
+ - streaming
33
+ - non-commercial
34
+ ---
35
+
36
+ # SteerTTS v1
37
+
38
+ A small English text-to-speech model that you steer with numbers. Each request can set any subset of **12 acoustic
39
+ controls**, for example speaking rate, loudness, median pitch, pitch range, final rise or fall, pauses, spectral tilt
40
+ and breathiness. Each control is a z-score relative to the chosen voice's own neutral speech. You can also pick a
41
+ **style tag**, give a **free-text direction** (which a small adapter turns into a request you can inspect), or use
42
+ **inline markup**. The model can **clone** a voice from a short reference clip.
43
+
44
+ | | |
45
+ |---|---|
46
+ | Acoustic model | **9.59M** parameters (encoder/decoder: attention + bidirectional Mamba-3, AdaLN conditioning) |
47
+ | + Vocos vocoder | 13.53M (`charactr/vocos-mel-24khz`, frozen), **23.1M** total |
48
+ | Flow-matching refiner (optional, on by default) | 17.64M → 40.8M total |
49
+ | Prompted flow (cloning only) | 24.67M, used in place of the refiner when `ref_wav` is given → 47.8M total |
50
+ | Frozen external encoders | WavLM-base-plus-sv 101M (cloning only), all-MiniLM-L6-v2 22.7M (free-text prompts only) |
51
+ | Output | 24 kHz mono, fp32, CPU or CUDA |
52
+ | Voices | 2,825 built-in voices under anonymised names (`voice_0001` …), default `voice_1361` |
53
+
54
+ > **Licence: weights CC BY-NC 4.0 (non-commercial use only); package code MIT.** The weights were trained partly on
55
+ > non-commercial data (see [Training data](#training-data)).
56
+
57
+ ## Quick start
58
+
59
+ ```bash
60
+ git clone https://huggingface.co/WhissleAI/steer-tts && cd steer-tts # or huggingface_hub.snapshot_download("WhissleAI/steer-tts")
61
+ pip install ".[all]" # core: torch, vocos, g2p_en …; [all] adds transformers (prompts, cloning) + parselmouth (measuring)
62
+ steertts say "Hello there, nice to meet you." -o hello.wav
63
+ steertts say "I can't believe it worked." --tag happiness --ctrl rate=1,f0_iqr=1.5 -o happy.wav
64
+ steertts say "Please keep this quiet." --prompt "whispering, slow and secretive" -o secret.wav # prints the resolved request
65
+ steertts stream "Well, *that* was fast. [pause 400ms] <slow>Let me think.</slow>" -o markup.wav
66
+ steertts say "Now in your voice." --ref-wav consenting_speaker.wav -o clone.wav
67
+ steertts voices --gender female | head
68
+ ```
69
+
70
+ The CLI looks for the model in the current directory. From anywhere else, pass it before the command
71
+ (`steertts --model WhissleAI/steer-tts say "Hi." -o hi.wav`) or set `STEERTTS_MODEL`.
72
+
73
+ ```python
74
+ from steertts import SteerTTS
75
+
76
+ tts = SteerTTS.from_pretrained("WhissleAI/steer-tts") # HF repo id or a local dir; device: cuda if available, else cpu
77
+ wav = tts("The meeting moved to Thursday.") # float32 numpy, 24 kHz, default voice voice_1361
78
+ wav = tts("Are you serious?", voice="voice_0042", ctrl={"f0_end": 2.0, "f0_iqr": 1.5}) # rising, lively
79
+ wav = tts("It is over.", tag="sadness", ctrl={"loud": -1}, guidance=1.5)
80
+
81
+ req = tts.resolve({"text": "See you soon.", "prompt": "excited, a bit faster"})
82
+ print(req) # {'text': ..., 'voice': 'voice_1361', 'tag': 'excited', 'ctrl': {'rate': 0.29}, 'prompt_mapping': {...}, ...}
83
+ wav = tts.synthesize(req) # a resolved request renders exactly as the prompt would
84
+
85
+ for chunk in tts.stream("First sentence. Second sentence."): # sentence-level streaming
86
+ play(chunk.audio)
87
+ tts.save(wav, "out.wav")
88
+ ```
89
+
90
+ `inference.py` renders one example of each feature. `python -m steertts.cli --help` lists the CLI.
91
+
92
+ ## The request
93
+
94
+ Every synthesis is one JSON object. It is documented field by field in [`request.schema.json`](request.schema.json)
95
+ (JSON Schema 2020-12; `steertts schema` prints it). Only `text` is required.
96
+
97
+ | Field | Meaning |
98
+ |---|---|
99
+ | `text` | English text. Numbers, units, currency, ordinals, ranges and unknown acronyms are spelled out. |
100
+ | `voice` / `ref_wav` | A built-in voice, **or** a reference clip to clone (first 8 s, at least 0.5 s). |
101
+ | `tag` | One of 51 style tags (`none`, `whisper`, `sadness`, `anger`, `fast`, `high_pitch`, …). |
102
+ | `ctrl` | `{control: z}` over the 12 controls below, each in [-5, 5]. |
103
+ | `prompt` | Free-text direction. `resolve()` maps it to `tag` + `ctrl`; an explicit `tag` or `ctrl` dim wins. |
104
+ | `guidance` | Steering scale s (default 1). See [Guidance](#how-well-it-steers). |
105
+ | `pace` | Global tempo divisor (>1 is faster). It does not change the style. |
106
+ | `word_shift` | `{word_index: {pitch: st, energy: dB, dur: ×}}`, per-word prosody edits. |
107
+ | `markup` | Parse inline markup in `text` (see below). `stream()` always parses it. |
108
+ | `flow`, `seed` | Use the flow refiner (default true) and set the sampler seed. Renders are deterministic per device. |
109
+
110
+ **Controls and the mask.** The 12 controls are utterance-level acoustic measurements:
111
+
112
+ - `rate`: articulation rate
113
+ - `loud`: speech level
114
+ - `f0_med`: median pitch
115
+ - `f0_iqr`: pitch range
116
+ - `f0_end`: final pitch slope
117
+ - `pause`: pause share
118
+ - `tilt`: spectral tilt
119
+ - `voiced`: voiced share
120
+ - `flat`: spectral flatness
121
+ - `centroid`: spectral centroid
122
+ - `cpps`: cepstral peak prominence
123
+ - `h1h2`: H1–H2
124
+
125
+ Each value is a z-score against the chosen voice's own neutral speech, in units of the pooled within-speaker SD
126
+ (`control_stats.json`). The model also receives an availability mask:
127
+
128
+ - A dim named in `ctrl` is requested.
129
+ - A dim left out is unspecified, and the model chooses it from voice, tag and text.
130
+ - `{"rate": 0}` asks for this voice's neutral rate. Omitting `rate` lets a `fast` tag or the text decide.
131
+
132
+ `steertts measure out.wav --text "…" --voice voice_1361` reads the controls back with the same measurement code
133
+ that labelled the training data.
134
+
135
+ **Markup** (`markup: true` or `stream()`):
136
+
137
+ | Markup | Effect |
138
+ |---|---|
139
+ | `*word*` or `<emph>…</emph>` | Emphasis: +2.5 st, +4 dB, ×1.2 duration |
140
+ | `<slow> <fast> <high> <low> <loud> <soft>` | Per-word changes; the sentence is still rendered in one pass |
141
+ | `[pause]`, `[pause 300ms]`, `[pause 1.5s]` | Exact silence |
142
+ | `<whisper>…</whisper>` (any tag), `<style prompt="…">…</style>` | The span is rendered as its own segment |
143
+
144
+ Text is rendered sentence by sentence, so the first sentence can play while the rest renders.
145
+
146
+ ## Architecture
147
+
148
+ FastPitch-style explicit-variance TTS:
149
+
150
+ 1. **Front end.** g2p_en ARPAbet phonemes with word indices.
151
+ 2. **Encoder.** 4 blocks.
152
+ 3. **Variance predictors.** Per-phone duration, pitch and energy.
153
+ 4. **Length regulator.**
154
+ 5. **Decoder.** 4 blocks.
155
+ 6. **Output.** A 100-band log-mel, rendered by Vocos.
156
+
157
+ Each encoder/decoder block is a **hybrid**: multi-head self-attention, then a **bidirectional Mamba-3** branch (two
158
+ Mamba-3 layers run forward and backward and summed; complex-valued state, trapezoidal discretisation; 3.70M
159
+ parameters in total), then a conv FFN.
160
+
161
+ Every LayerNorm in every block and predictor is an **adaptive LayerNorm**. Its scale and shift are predicted from
162
+ g = MLP([speaker vector, tag embedding, ctrl ⊙ mask, mask]). The speaker slot is a linear projection of a frozen
163
+ 512-d WavLM-base-plus-sv x-vector. Built-in voices use their stored mean vector; cloning uses the vector of the
164
+ reference clip.
165
+
166
+ **Guidance** renders the request and the uninstructed request, then extrapolates durations, pitch, energy and mel:
167
+ out = out₀ + s·(out − out₀).
168
+
169
+ Optional decoders:
170
+
171
+ - **Flow refiner.** A conditional flow-matching ConvNeXt network (OT-CFM, 10 midpoint steps, temperature 0.4). It
172
+ samples mel detail given the acoustic model's mel and g, so every control still comes from the acoustic model.
173
+ - **Prompted flow.** The same network plus attention, given the reference clip's own mel as context
174
+ (Voicebox-style infilling). Used for cloning.
175
+
176
+ The `steertts` package here is self-contained. It is the inference subset of the training code, operation for
177
+ operation, and its outputs are bit-identical to the training repository's synthesis on the same device (see
178
+ [Release verification](#release-verification)).
179
+
180
+ ## How well it steers
181
+
182
+ The numbers below are **held-out**: 16 LibriTTS-R test-clean speakers who share no speaker, chapter or recording
183
+ with the training data (8 female, 8 male by a median-F0 rule; the corpus has no gender labels on disk), each
184
+ conditioned on the speaker vector of one recording, × 8 texts. They are for the acoustic model at guidance 1 **without the flow refiner**.
185
+ Steering through the refiner was not measured in the paper; the release check below confirms only the direction.
186
+
187
+ Columns:
188
+
189
+ - **Hit**: share of renders that move more than 0.5 SD the requested way.
190
+ - **Δz**: achieved change for a request of ±2.
191
+ - **ρ**: Spearman correlation between request and readback (by our extractor / by Praat).
192
+
193
+ | Tier | Control | Hit at −2 / +2 | Δz at −2 / +2 | ρ (ours / Praat) |
194
+ |---|---|---|---|---|
195
+ | **Reliable** | loud | 1.00 / 1.00 | −1.57 / +1.81 | .97 / .97 |
196
+ | | f0_med | .98 / 1.00 | −1.15 / +1.18 | .95 / .95 |
197
+ | | h1h2 | 1.00 / 1.00 | −1.59 / +1.49 | .96 / .96 |
198
+ | | tilt | 1.00 / .99 | −1.39 / +0.97 | .96 / .87 |
199
+ | | centroid | .78 / .94 | −0.79 / +1.20 | .92 / .92 |
200
+ | | f0_iqr | .66 / .91 | −0.63 / +1.09 | .88 / .88 |
201
+ | **Usable** | rate | 1.00 / 1.00 | −1.61 / +1.30 | .96 / .66 |
202
+ | | f0_end | .96 / .99 | −0.85 / +1.31 | .94 / .83 |
203
+ | | flat | .62 / .41 | −0.64 / +0.51 | .85 / .68 |
204
+ | **Weak** | pause | .38 / .64 | −0.37 / +0.71 | .76 / .70 |
205
+ | | voiced | .39 / .12 | −0.44 / +0.26 | .79 / .60 |
206
+ | | cpps | .56 / .35 | −0.45 / +0.30 | .58 / .46 |
207
+
208
+ - **Tiers.** Reliable: ρ ≥ 0.85 under both extractors. Usable: under one. Weak: under neither.
209
+ - **Gain.** The mean gain (achieved / requested) is about 0.5. `rate` and `f0_med` at +1 hit only .48 / .42.
210
+ - **Cross-talk.** Mean leakage into the other 11 dims is 0.15–0.30 SD. The off-diagonal / diagonal ratio is 0.10.
211
+ - **CPPS** is not a working control: a request moves pitch and level about as much as CPPS itself.
212
+ - **Intelligibility.** WER (Whisper-large-v3) is 0.94 % uninstructed and 0.84–1.01 % across all requests.
213
+
214
+ **Guidance** (pooled over loud, rate, f0_med, f0_iqr and tilt; ±2 requests; n = 1,280):
215
+
216
+ | s | Steer ratio (Δz / requested) | Hit | Leak | WER |
217
+ |---|---|---|---|---|
218
+ | 1 | 0.65 | .95 | .20 | 0.9 % |
219
+ | 2 | 1.35 | .99 | .34 | 1.0 % |
220
+ | 3 | 1.97 | .98 | .48 | 1.0 % |
221
+
222
+ - s ≈ 1.5 roughly delivers the requested size, at the cost of more leakage.
223
+ - Guidance on tags is riskier: `whisper` at s = 3 raises WER to 16 %.
224
+
225
+ **Markup** (8 held-out voices × 6 sentences × 2 words):
226
+
227
+ - Emphasis achieved +2.58 st and +3.92 dB, against +2.5 st and +4 dB requested.
228
+ - `<high>` / `<low>` gave +3.18 / −2.96 st for ±3.
229
+ - `<loud>` / `<soft>` gave +4.84 / −5.54 dB for ±6. The decoder attenuates level by 10–20 %.
230
+ - `<slow>` / `<fast>` and pauses are exact by construction.
231
+ - Neighbouring words move by at most 0.19 st and 0.16 dB.
232
+
233
+ **Free-text prompts** (161 hand-written prompts):
234
+
235
+ - Which-dims precision / recall: .85 / .66. Sign accuracy: .88.
236
+ - Tag top-1 / top-3: .56 / .69.
237
+ - Composition (.23) and negation (.22) mostly fail. "Not fast" can come out fast.
238
+ - Absolute-register prompts ("a deep male voice") are not supported. Pick a voice instead (`voices.json`).
239
+ - **Always look at the resolved request.**
240
+
241
+ ## Quality
242
+
243
+ Results under the Open TTS Leaderboard protocol (run locally on an RTX 5070, not an official submission;
244
+ Qwen3-ASR-1.7B WER, WavLM-large SIM):
245
+
246
+ | Setting | Seed-TTS en | CV3-Eval en | Avg |
247
+ |---|---|---|---|
248
+ | Default voice `voice_1361`, acoustic model + Vocos, WER | 1.00 % | 2.63 % | **1.81 %** |
249
+ | Cloning (prompted flow), WER / SIM ×100 | 1.01 % / 37.4 | 2.73 % / 28.7 | 1.87 % / 33.1 |
250
+
251
+ - The flow refiner was scored with an earlier acoustic checkpoint: 1.04 % / 2.70 % WER (no change).
252
+ - **Cloning similarity is low.** The board's lowest cloning SIM is about 37, and its median is about 65. Do not
253
+ expect a convincing likeness.
254
+
255
+ ## Speed
256
+
257
+ Measured on an RTX 5070 and on a shared, heavily loaded 8-thread CPU, so treat CPU figures as lower bounds:
258
+
259
+ - **GPU.** Batch-1 RTFx 61–81 for the acoustic model + Vocos (leaderboard runs).
260
+ - **CPU, 8 threads.** RTFx 3.6–7.9 for the acoustic model + Vocos. The flow refiner adds 12× the compute and
261
+ drops RTFx from 6.7 to 0.8 (0.4 with 1 thread). Use `flow=False` / `--no-flow` for real-time CPU use.
262
+ - **Cloning on CPU.** RTFx 0.2 with the prompted flow, including the WavLM speaker encoder.
263
+ - **Streaming on CPU.** First audio after about 1 s.
264
+ - See [Release verification](#release-verification) for the numbers measured on this exact package.
265
+
266
+ ## Limitations
267
+
268
+ - **English only.** The front end is ARPAbet. Single speaker per request, and the controls are utterance-level.
269
+ - **Emotion tags are style switches, not emotions.** An independent emotion classifier (emotion2vec+ large)
270
+ recognises only `sadness` (0.16 top-1 vs 0.01 uninstructed). Every other emotion tag scores at most 0.02. The same
271
+ classifier recognises 7 of 10 of these emotions on real EARS recordings. 27 of 49 tags are acoustically within 0.5
272
+ of `neutral`. Voice-quality tags such as `whisper` act strongly (voiced share −9.9 SD).
273
+ - **Event tokens only reserve time.** `[laugh]`, `[cough]`, `[dog]`, … add about 0.12 s of near-silence. An audio
274
+ tagger detects 0 of 1,728 renders. The project's demo mixed recorded sounds in, but that mixer is not part of this
275
+ release.
276
+ - **The acoustic-environment input is untrained.** The weights contain an environment projection, but the final
277
+ training stage never saw a non-zero environment vector, so this package does not expose it.
278
+ - **Cloning similarity is low** (see above), and cloning speed on CPU is slow with the prompted flow.
279
+ - **The controls are acoustic measurements, not percepts.** `voiced`, `flat` and `cpps` barely move. `rate` and
280
+ `tilt` depend on how they are measured. z = 0 is the voice's typical render, not its real recordings.
281
+ - **Test-set-informed choices.** The prompted-flow checkpoint was chosen on subsets of the Seed-TTS / CV3 test
282
+ sets, and the text-normalisation rules came from CV3 error analysis. The default voice `voice_1361` is the
283
+ pre-registered one: a sweep picked a lower-WER voice on the test sets, but it is not the default here.
284
+ - **No human evaluation.** Every number above is from automatic metrics, with one training seed.
285
+
286
+ ## Intended use and misuse
287
+
288
+ **Intended uses:**
289
+
290
+ - Research on controllable / interpretable speech synthesis.
291
+ - Accessibility and prototyping, where explicit, inspectable control matters more than peak naturalness.
292
+ - On-device experiments with a ~23M-parameter TTS.
293
+
294
+ **Voice cloning requires consent.** Clone only a voice whose owner has agreed, and do not use this model to:
295
+
296
+ - impersonate real people;
297
+ - produce deceptive or fraudulent audio;
298
+ - bypass voice authentication;
299
+ - create non-consensual content.
300
+
301
+ Disclose synthetic speech to listeners. There is no watermark yet.
302
+
303
+ **Built-in voices are anonymised.** Built-in voices are training speakers under opaque names. Speaker IDs from the
304
+ source corpora are not included. Speakers from a celebrity-interview corpus (VoxCeleb, via NonverbalTTS) were
305
+ excluded from the voice list.
306
+
307
+ ## Training data
308
+
309
+ 410,566 utterances (659 h) from 4,919 speakers:
310
+
311
+ | Source | Hours | Licence |
312
+ |---|---|---|
313
+ | LibriTTS-R (clean-100, clean-360, other-500) | 516 | CC BY 4.0 |
314
+ | EARS (read + freeform) | 78 | CC BY-NC 4.0 |
315
+ | Expresso | 11 | CC BY-NC 4.0 |
316
+ | NonverbalTTS (VoxCeleb and Expresso sources) | 12.5 | Source terms apply |
317
+ | Synthetic speech from a commercial TTS API | 43 | The provider's terms of service, incl. a competing-model clause |
318
+
319
+ Control labels are measured from the audio. Style tags come from the corpora's own style/emotion labels.
320
+
321
+ **Licence: CC BY-NC 4.0. Non-commercial use only.**
322
+
323
+ ## Files
324
+
325
+ | File | Contents |
326
+ |---|---|
327
+ | `model.safetensors` | Acoustic model (fp32) |
328
+ | `flow.safetensors`, `pflow.safetensors` | Flow refiner and prompted flow |
329
+ | `prompt_adapter.safetensors` | Free-text → request adapter (0.5M) |
330
+ | `speaker_vectors.safetensors` | 512-d vector per built-in voice, in `config.json:voices_order` |
331
+ | `voices.json` | Measured descriptions (see below) |
332
+ | `control_stats.json` | Pooled SDs, tag list, and each voice's neutral control means (for `measure_z`) |
333
+ | `config.json` | Architecture and file map |
334
+ | `request.schema.json` | The request schema |
335
+ | `steertts/` | The package (incl. `steertts/mamba3` with its own invariant tests) |
336
+ | `tests/` | pytest suite |
337
+ | `leaderboard/` | Open TTS Leaderboard backend |
338
+ | `TEST_LOG.md` | Release verification log |
339
+ | `LICENSE.md` | Code (MIT) and weights (CC BY-NC 4.0) |
340
+
341
+ `voices.json` descriptions are measured, not copied from corpus metadata:
342
+
343
+ - **Gender.** A probe on the speaker vector; 99.3 % 5-fold accuracy on 534 self-labelled speakers.
344
+ - **Pitch register.** Median F0 within that gender.
345
+ - **Pace.** Articulation rate.
346
+
347
+ ## Release verification
348
+
349
+ The package was verified in a fresh CPU environment; `TEST_LOG.md` has the full log.
350
+
351
+ - **Tests.** The pytest suite passes (16 tests).
352
+ - **Parity.** Seven renders are bit-identical to the training code's synthesis on the same device: acoustic
353
+ only, controls, tag + guidance, per-word shifts, the flow refiner, and cloning through the prompted flow.
354
+ - **Speed.** On this package, CPU with 4 threads on a loaded machine: RTFx 4.9 without the refiner, 0.7 with it.
355
+ First streamed audio arrives after 0.5 s.
356
+
357
+ ## Citation
358
+
359
+ ```bibtex
360
+ @misc{steertts2026,
361
+ title = {SteerTTS: A Tiny, Measurably Steerable Speech Synthesizer},
362
+ author = {{Whissle AI}},
363
+ year = {2026},
364
+ howpublished = {\url{https://huggingface.co/WhissleAI/steer-tts}}
365
+ }
366
+ ```
TEST_LOG.md ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Release verification log: steertts-v1
2
+
3
+ Date: 2026-10-11. Everything ran on the CPU (4 threads, `nice -n 19`) of a shared 48-core workstation, with a 1-min
4
+ load average of 20 to 40 from other jobs. The timings are lower bounds.
5
+
6
+ ## Weights
7
+
8
+ | File | Source checkpoint | sha256 (first 16) |
9
+ |---|---|---|
10
+ | `model.safetensors` | acoustic model, step 312,500 | `adc11912871113e3` |
11
+ | `flow.safetensors` | flow refiner (EMA), step 100,000 | `585ff2c931811c30` |
12
+ | `pflow.safetensors` | prompted flow with prompt attention (EMA), step 40,000 | `877f81523dfaba50` |
13
+ | `prompt_adapter.safetensors` | free-text → request adapter | `aca34bf753456e46` |
14
+ | `speaker_vectors.safetensors` | 2,825 built-in voices (public names only) | `edcabfd02dd4a520` |
15
+
16
+ These are the checkpoints behind the model card's numbers. The prompted flow is the one in the leaderboard cloning row
17
+ (1.01 % / 37.4 and 2.73 % / 28.7).
18
+
19
+ ## 1. Fresh-environment install
20
+
21
+ - A new Python 3.11 venv (uv) with the CPU torch wheel. Then `pip install ".[test]"` from a copy of the package
22
+ code, holding no weights.
23
+ - Resolved versions: torch 2.11.0+cpu, torchaudio 2.11.0+cpu, numpy 2.4.6, safetensors 0.8.0, vocos 0.1.0,
24
+ g2p_en 2.1.0, nltk 3.10.3, num2words 0.5.14, soundfile 0.14.0, soxr 1.1.0, transformers 5.19.0,
25
+ praat-parselmouth 0.4.7, pytest 9.1.1, jsonschema 4.26.0.
26
+ - Not fresh: the Hugging Face hub cache and `~/nltk_data` came from the host. A first run on a new machine
27
+ downloads `charactr/vocos-mel-24khz`, plus `microsoft/wavlm-base-plus-sv` for cloning and
28
+ `sentence-transformers/all-MiniLM-L6-v2` for free-text prompts, and the NLTK tagger and CMUdict.
29
+
30
+ ## 2. Test suite
31
+
32
+ `STEERTTS_MODEL=<this dir> pytest` from the fresh env gave **16 passed** in 6 min 11 s. The suite covers:
33
+
34
+ - the Mamba-3 invariant suite;
35
+ - text normalisation and phonemes;
36
+ - the request schema and its examples;
37
+ - request validation errors;
38
+ - deterministic renders, both acoustic-only and through the flow refiner;
39
+ - pace and rate;
40
+ - Praat readback direction for `loud`, `f0_med` and `h1h2`;
41
+ - markup and streaming;
42
+ - cloning;
43
+ - prompt → request mapping.
44
+
45
+ ## 3. Parity with the training code
46
+
47
+ The same inputs were rendered on the same device (CPU) by this package and by the training repository's
48
+ `steer.synth.Synth`, given the source checkpoints above. Every case was **bit-identical** (max |Δ| = 0):
49
+
50
+ | Case | Samples | Identical |
51
+ |---|---|---|
52
+ | default voice, acoustic only | 73,472 | yes |
53
+ | `ctrl` rate/f0_med/f0_end, acoustic only | 68,096 | yes |
54
+ | tag `sadness` + `loud` −1, guidance 1.5 | 85,760 | yes |
55
+ | `word_shift` (pitch/energy/dur on one word) | 37,376 | yes |
56
+ | another built-in voice, flow refiner | 73,216 | yes |
57
+ | `ctrl` loud/f0_iqr, flow refiner | 75,008 | yes |
58
+ | cloning from a reference clip, prompted flow | 51,456 | yes |
59
+
60
+ Cross-environment check: two renders (acoustic only, and flow + ctrl) were bit-identical between the fresh venv
61
+ (torch 2.11.0+cpu) and the training environment (torch 2.11.0+cu128, run on the CPU). GPU renders are deterministic
62
+ per device but are not bit-identical to CPU renders.
63
+
64
+ ## 4. Speed on this package (CPU, 4 threads, load average about 22)
65
+
66
+ | Path | RTFx (3 texts, 1.7–5 s) |
67
+ |---|---|
68
+ | Acoustic model + Vocos (`flow=False`) | 4.35–5.59 (median 4.89) |
69
+ | + flow refiner (default) | 0.62–0.79 (median 0.71) |
70
+ | Streaming, first chunk (`flow=False`) | 0.49 s |
71
+
72
+ ## 5. Hygiene
73
+
74
+ - No source-corpus speaker or clip identifiers appear in any file. The build fails on the pattern check in
75
+ `steer.release`, and the check was re-run here with grep.
76
+ - VoxCeleb-derived speakers are excluded from the voice list.
77
+ - The schema `$id` is a URN (`urn:steertts:request-schema:v1`), not a URL.
78
+ - `leaderboard/` names a placeholder Hugging Face repo id. It is not referenced by the package code.
build/lib/steertts/__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ """SteerTTS: a small, numerically steerable English TTS (hybrid attention + bidirectional Mamba-3, AdaLN on a
2
+ masked 12-dim control request), with optional flow-matching refiners, a free-text prompt adapter and zero-shot
3
+ cloning from a reference clip."""
4
+ from .config import CONTROL_DIMS, DEFAULT_VOICE, SR
5
+ from .pipeline import SteerTTS, SteerTTSPipeline, resolve_model_dir
6
+ from .request import normalize_request, schema
7
+
8
+ __all__ = ["SteerTTS", "SteerTTSPipeline", "CONTROL_DIMS", "DEFAULT_VOICE", "SR", "normalize_request", "schema",
9
+ "resolve_model_dir"]
10
+ __version__ = "1.0.0"
build/lib/steertts/__main__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .cli import main
2
+
3
+ main()
build/lib/steertts/audio.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Waveform I/O and the Vocos mel front end (24 kHz, 100 bands, hop 256)."""
2
+ from pathlib import Path
3
+
4
+ import numpy as np
5
+ import torch
6
+
7
+ from .config import HOP, N_FFT, N_MELS, SR
8
+
9
+ _mel_fe = None
10
+
11
+
12
+ def mel_extractor():
13
+ global _mel_fe
14
+ if _mel_fe is None:
15
+ from vocos.feature_extractors import MelSpectrogramFeatures
16
+ _mel_fe = MelSpectrogramFeatures(sample_rate=SR, n_fft=N_FFT, hop_length=HOP, n_mels=N_MELS, padding="center")
17
+ return _mel_fe
18
+
19
+
20
+ def load_wav(x, sr=None):
21
+ """path or float array (at `sr`, default 24 kHz) -> (mono float32 at 24 kHz, original sample rate)."""
22
+ if isinstance(x, (str, Path)):
23
+ import soundfile as sf
24
+ y, sr = sf.read(str(x), dtype="float32")
25
+ else:
26
+ y, sr = np.asarray(x, np.float32), (sr or SR)
27
+ y = y.mean(1) if y.ndim > 1 else y
28
+ if sr != SR:
29
+ import soxr
30
+ y = soxr.resample(y, sr, SR)
31
+ return np.asarray(y, np.float32), sr
32
+
33
+
34
+ def save_wav(wav, path):
35
+ import soundfile as sf
36
+ sf.write(str(path), np.asarray(wav, np.float32), SR)
build/lib/steertts/cli.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """steertts command line.
2
+
3
+ steertts say "Hello there." -o hello.wav # default voice, default style
4
+ steertts say "I can't believe it." --voice voice_1361 --tag anger --ctrl loud=1.5,f0_med=1 -o angry.wav
5
+ steertts say "Keep it down." --prompt "whispering and slow" --show-request -o soft.wav
6
+ steertts say "Well, *that* was fast. [pause 400ms] <slow>Let me think.</slow>" --markup -o m.wav
7
+ steertts say "Now in your voice." --ref-wav me.wav -o clone.wav
8
+ steertts say --request req.json -o out.wav # a JSON request (request.schema.json)
9
+ steertts stream "First sentence. Second sentence." -o s.wav # prints per-chunk timing
10
+ steertts voices [--gender female] | steertts tags | steertts schema | steertts prompt "soft and sad"
11
+ steertts measure out.wav --text "..." --voice voice_1361 # read the 12 controls back (z)
12
+
13
+ The model directory comes from --model, else $STEERTTS_MODEL, else the current directory if it holds config.json.
14
+ """
15
+ import argparse
16
+ import json
17
+ import os
18
+ import sys
19
+ import time
20
+
21
+
22
+ def _model_dir(a):
23
+ if a.model:
24
+ return a.model
25
+ if os.environ.get("STEERTTS_MODEL"):
26
+ return os.environ["STEERTTS_MODEL"]
27
+ if os.path.exists("config.json") and os.path.exists("voices.json"):
28
+ return "."
29
+ sys.exit("steertts: no model directory; pass --model DIR (or an HF repo id) or set STEERTTS_MODEL")
30
+
31
+
32
+ def _ctrl(s):
33
+ out = {}
34
+ for kv in filter(None, (s or "").split(",")):
35
+ k, _, v = kv.partition("=")
36
+ out[k.strip()] = float(v)
37
+ return out
38
+
39
+
40
+ def _load(a, **kw):
41
+ from . import SteerTTS
42
+ t0 = time.time()
43
+ tts = SteerTTS.from_pretrained(_model_dir(a), device=a.device, flow=not a.no_flow, **kw)
44
+ print(f"loaded {tts} in {time.time() - t0:.1f}s", file=sys.stderr)
45
+ return tts
46
+
47
+
48
+ def _request(a):
49
+ req = json.load(open(a.request)) if a.request else {}
50
+ for k, v in dict(text=a.text, voice=a.voice, ref_wav=a.ref_wav, prompt=a.prompt, tag=a.tag,
51
+ pace=a.pace, guidance=a.guidance, seed=a.seed).items():
52
+ if v is not None:
53
+ req[k] = v
54
+ if a.ctrl:
55
+ req["ctrl"] = {**req.get("ctrl", {}), **_ctrl(a.ctrl)}
56
+ if getattr(a, "markup", False):
57
+ req["markup"] = True
58
+ if a.no_flow:
59
+ req["flow"] = False
60
+ return req
61
+
62
+
63
+ def main(argv=None):
64
+ ap = argparse.ArgumentParser(prog="steertts", description="SteerTTS: steerable English TTS")
65
+ ap.add_argument("--model", help="model directory or HF repo id (default: $STEERTTS_MODEL or .)")
66
+ ap.add_argument("--device", help="cpu | cuda (default: cuda if available)")
67
+ ap.add_argument("--no-flow", action="store_true", help="skip the flow-matching refiner")
68
+ sub = ap.add_subparsers(dest="cmd", required=True)
69
+ for name in ("say", "stream"):
70
+ p = sub.add_parser(name)
71
+ p.add_argument("text", nargs="?")
72
+ p.add_argument("--request", help="JSON request file (fields as in request.schema.json)")
73
+ p.add_argument("--voice")
74
+ p.add_argument("--ref-wav", dest="ref_wav", help="clone this reference clip (consent required)")
75
+ p.add_argument("--prompt", help="free-text direction, e.g. 'soft and sad'")
76
+ p.add_argument("--tag")
77
+ p.add_argument("--ctrl", help="comma list dim=z, e.g. rate=1.5,f0_med=-1")
78
+ p.add_argument("--pace", type=float)
79
+ p.add_argument("--guidance", type=float)
80
+ p.add_argument("--seed", type=int)
81
+ p.add_argument("--markup", action="store_true")
82
+ p.add_argument("--show-request", action="store_true", help="print the resolved request JSON")
83
+ p.add_argument("-o", "--out", default="out.wav")
84
+ p = sub.add_parser("voices")
85
+ p.add_argument("--gender", choices=["female", "male"])
86
+ p.add_argument("--json", action="store_true")
87
+ sub.add_parser("tags")
88
+ sub.add_parser("schema")
89
+ p = sub.add_parser("prompt")
90
+ p.add_argument("prompt")
91
+ p = sub.add_parser("measure")
92
+ p.add_argument("wav")
93
+ p.add_argument("--text")
94
+ p.add_argument("--voice")
95
+ a = ap.parse_args(argv)
96
+
97
+ if a.cmd == "schema":
98
+ from .request import schema
99
+ print(json.dumps(schema(), indent=1))
100
+ return
101
+ if a.cmd in ("voices", "tags"):
102
+ from .pipeline import resolve_model_dir
103
+ d = resolve_model_dir(_model_dir(a))
104
+ if a.cmd == "tags":
105
+ print(" ".join(json.load(open(d / "control_stats.json"))["tags"]))
106
+ return
107
+ vs = json.load(open(d / "voices.json"))["voices"]
108
+ vs = [v for v in vs if not a.gender or v.get("gender") == a.gender]
109
+ if a.json:
110
+ print(json.dumps(vs, indent=1))
111
+ else:
112
+ for v in vs:
113
+ print(f"{v['name']} {v['description']}")
114
+ return
115
+ if a.cmd == "measure":
116
+ from .audio import load_wav
117
+ from .measure import measure, measure_z, n_phones
118
+ from .pipeline import resolve_model_dir
119
+ y, _ = load_wav(a.wav)
120
+ if a.voice:
121
+ st = json.load(open(resolve_model_dir(_model_dir(a)) / "control_stats.json"))
122
+ z = measure_z(y, a.text, st["spk_mean"][a.voice], st["pooled_sd"])
123
+ print(json.dumps({k: round(v, 2) for k, v in z.items()}))
124
+ else:
125
+ from .config import CONTROL_DIMS
126
+ raw = measure(y, n_phones=n_phones(a.text) if a.text else None, want_mel=False)["controls"]
127
+ print(json.dumps({k: round(float(v), 3) for k, v in zip(CONTROL_DIMS, raw)}))
128
+ return
129
+ tts = _load(a)
130
+ if a.cmd == "prompt":
131
+ print(json.dumps(tts.map_prompt(a.prompt), indent=1))
132
+ return
133
+ req = _request(a)
134
+ if not req.get("text"):
135
+ sys.exit("steertts: no text (positional argument or --request)")
136
+ req = tts.resolve(req)
137
+ if getattr(a, "show_request", False) or "prompt_mapping" in req:
138
+ print(json.dumps(req, indent=1), file=sys.stderr)
139
+ import numpy as np
140
+ if a.cmd == "say":
141
+ t0 = time.time()
142
+ wav = tts.synthesize(req)
143
+ dt = time.time() - t0
144
+ else:
145
+ t0, chunks = time.time(), []
146
+ for i, c in enumerate(tts.stream(request=req)):
147
+ chunks.append(c.audio)
148
+ print(f"chunk {i}: {len(c.audio) / tts.sample_rate:.2f}s audio at +{(time.time() - t0) * 1000:.0f} ms "
149
+ f"({c.segment.text or ('pause %d ms' % c.segment.pause_ms)!r})", file=sys.stderr)
150
+ wav = np.concatenate(chunks) if chunks else np.zeros(1, np.float32)
151
+ dt = time.time() - t0
152
+ tts.save(wav, a.out)
153
+ print(f"{a.out}: {len(wav) / tts.sample_rate:.2f}s audio in {dt:.2f}s", file=sys.stderr)
154
+
155
+
156
+ if __name__ == "__main__":
157
+ main()
build/lib/steertts/config.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Audio / control constants. They must match training: the charactr/vocos-mel-24khz mel front end."""
2
+ SR = 24000
3
+ N_FFT = 1024
4
+ HOP = 256
5
+ N_MELS = 100
6
+ FPS = SR / HOP # 93.75 mel frames per second
7
+ PITCH_SCALE = 3.0 # predicted token pitch is semitones re the speaker median / 3; word_shift pitch is given in st
8
+
9
+ # Utterance-level controls. Each is a SPEAKER-RELATIVE z-score: 0 = this voice's own neutral speech, +-1 = one
10
+ # pooled within-speaker standard deviation (pooled_sd in control_stats.json gives the raw unit per dim).
11
+ CONTROL_DIMS = [
12
+ "rate", # articulation rate, phones per second of speech (pauses excluded)
13
+ "loud", # mean RMS level of speech frames, dB
14
+ "f0_med", # median F0, semitones
15
+ "f0_iqr", # F0 interquartile range, semitones (pitch range / liveliness)
16
+ "f0_end", # F0 slope over the final 300 ms of voicing, st/s (+ rising, question-like)
17
+ "pause", # share of the speech span that is silent pauses >= 100 ms
18
+ "tilt", # alpha ratio: energy 1-5 kHz vs 50 Hz-1 kHz, dB (+ brighter / pressed)
19
+ "voiced", # voiced share of speech frames (whisper -> low)
20
+ "flat", # spectral flatness of speech frames, log (breathy / whisper -> high)
21
+ "centroid", # spectral centroid of speech frames, log Hz (brightness)
22
+ "cpps", # smoothed cepstral peak prominence, dB (periodicity; breathy / tense -> low)
23
+ "h1h2", # H1-H2, dB (breathy -> high)
24
+ ]
25
+ CONTROL_RANGE = (-5.0, 5.0) # accepted request range; training labels rarely exceed +-3
26
+
27
+ DEFAULT_VOICE = "voice_1361" # pre-registered default (chosen before any test-set voice sweep)
build/lib/steertts/flow.py ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Flow-matching mel decoders (optional), inference subset of recipes/steer_tts/steer/flow.py.
2
+
3
+ refiner (flow.safetensors): a conditional flow (OT-CFM, as the Matcha-TTS decoder) that samples a detailed mel
4
+ given the acoustic model's mel mu and its conditioning g. Durations, pitch, energy, the coarse spectrum and
5
+ every control still come from the acoustic model; the flow adds texture (sharper harmonics / noise).
6
+ prompted flow (pflow.safetensors): the same with an audio prompt - the reference clip's own mel is given as
7
+ context before the target (Voicebox / F5-TTS-style infilling). Used only for zero-shot cloning.
8
+ """
9
+ import math
10
+
11
+ import torch
12
+ import torch.nn as nn
13
+ import torch.nn.functional as F
14
+
15
+ from .config import N_MELS
16
+
17
+ SIGMA = 1e-4
18
+
19
+ class TimeEmb(nn.Module):
20
+ def __init__(self, d):
21
+ super().__init__()
22
+ self.d = d
23
+ self.mlp = nn.Sequential(nn.Linear(d, 4 * d), nn.SiLU(), nn.Linear(4 * d, d))
24
+
25
+ def forward(self, t): # [B] in [0, 1]
26
+ half = self.d // 2
27
+ f = torch.exp(-math.log(10000) * torch.arange(half, device=t.device) / half)
28
+ a = 1000 * t[:, None] * f[None]
29
+ return self.mlp(torch.cat([a.sin(), a.cos()], -1))
30
+
31
+
32
+ class Block(nn.Module):
33
+ """ConvNeXt block (depthwise dilated conv -> LN -> MLP) with AdaLN-zero conditioning on (t, g)."""
34
+
35
+ def __init__(self, d, c, k=7, dil=1, mult=3):
36
+ super().__init__()
37
+ self.dw = nn.Conv1d(d, d, k, padding=dil * (k // 2), dilation=dil, groups=d)
38
+ self.ln = nn.LayerNorm(d, elementwise_affine=False)
39
+ self.mod = nn.Linear(c, 3 * d)
40
+ nn.init.zeros_(self.mod.weight); nn.init.zeros_(self.mod.bias)
41
+ self.pw1, self.pw2 = nn.Linear(d, mult * d), nn.Linear(mult * d, d)
42
+
43
+ def forward(self, x, m, c): # x [B,d,T], m [B,1,T], c [B,c]
44
+ sc, sh, gate = self.mod(c)[:, None].chunk(3, -1)
45
+ h = self.dw(x * m).transpose(1, 2)
46
+ h = self.ln(h) * (1 + sc) + sh
47
+ h = self.pw2(F.gelu(self.pw1(h))) * gate
48
+ return x + h.transpose(1, 2)
49
+
50
+
51
+ class AttnBlock(nn.Module):
52
+ """Full self-attention over the sequence with AdaLN-zero conditioning: lets the audio prompt (prompted flow)
53
+ reach every target frame, beyond the conv receptive field (~170 frames each side)."""
54
+
55
+ def __init__(self, d, c, heads=6):
56
+ super().__init__()
57
+ self.ln = nn.LayerNorm(d, elementwise_affine=False)
58
+ self.mod = nn.Linear(c, 3 * d)
59
+ nn.init.zeros_(self.mod.weight); nn.init.zeros_(self.mod.bias)
60
+ self.att = nn.MultiheadAttention(d, heads, batch_first=True)
61
+
62
+ def forward(self, x, m, c):
63
+ sc, sh, gate = self.mod(c)[:, None].chunk(3, -1)
64
+ h = self.ln(x.transpose(1, 2)) * (1 + sc) + sh
65
+ h, _ = self.att(h, h, h, key_padding_mask=~m[:, 0].bool(), need_weights=False)
66
+ return x + (h * gate).transpose(1, 2) * m
67
+
68
+
69
+ def rope(x): # x [B,H,T,dh]: rotary position embedding (Su et al. 2021) over the frame axis
70
+ T, dh = x.shape[-2], x.shape[-1]
71
+ f = torch.exp(-math.log(10000) * torch.arange(0, dh, 2, device=x.device, dtype=torch.float32) / dh)
72
+ a = torch.arange(T, device=x.device, dtype=torch.float32)[:, None] * f[None]
73
+ cos, sin = a.cos().to(x.dtype), a.sin().to(x.dtype)
74
+ x1, x2 = x[..., 0::2], x[..., 1::2]
75
+ return torch.stack([x1 * cos - x2 * sin, x1 * sin + x2 * cos], -1).flatten(-2)
76
+
77
+
78
+ class PromptPool(nn.Module):
79
+ """Perceiver-resampler pooling of the prompt (XTTS; SupertonicTTS's learnable timbre queries): n learnable
80
+ queries cross-attend to the encoded prompt frames -> n global voice tokens. A learned null key is always
81
+ present, so an item without a prompt still gets defined (prompt-free) tokens."""
82
+
83
+ def __init__(self, d, n, heads=6, layers=2):
84
+ super().__init__()
85
+ self.q = nn.Parameter(torch.randn(n, d) * 0.02)
86
+ self.null = nn.Parameter(torch.zeros(1, 1, d))
87
+ self.heads = heads
88
+ self.layers = nn.ModuleList([nn.ModuleDict(dict(
89
+ lq=nn.LayerNorm(d), lk=nn.LayerNorm(d), q=nn.Linear(d, d), kv=nn.Linear(d, 2 * d), o=nn.Linear(d, d),
90
+ lf=nn.LayerNorm(d), f=nn.Sequential(nn.Linear(d, 2 * d), nn.GELU(), nn.Linear(2 * d, d))))
91
+ for _ in range(layers)])
92
+
93
+ def forward(self, e, cm): # e [B,d,T] encoded prompt (zero off-prompt), cm [B,1,T]
94
+ B, d, T = e.shape
95
+ kv_in = torch.cat([self.null.expand(B, 1, d), e.transpose(1, 2)], 1)
96
+ keep = torch.cat([torch.ones(B, 1, device=e.device, dtype=torch.bool), cm[:, 0].bool()], 1)[:, None, None]
97
+ q = self.q[None].expand(B, -1, -1)
98
+ H = self.heads
99
+ for l in self.layers:
100
+ k, v = l["kv"](l["lk"](kv_in)).chunk(2, -1)
101
+ qq = l["q"](l["lq"](q))
102
+ sp = lambda z: z.view(B, -1, H, d // H).transpose(1, 2)
103
+ h = F.scaled_dot_product_attention(sp(qq), sp(k), sp(v), attn_mask=keep)
104
+ q = q + l["o"](h.transpose(1, 2).reshape(B, -1, d))
105
+ q = q + l["f"](l["lf"](q))
106
+ return q # [B,n,d]
107
+
108
+
109
+ class RAttnBlock(nn.Module):
110
+ """Self-attention with RoPE positions (frames), plus optional cross-attention to the pooled prompt tokens; both
111
+ zero-gated (AdaLN-zero gate / zero output projection), so adding these blocks to a trained net changes nothing
112
+ at init."""
113
+
114
+ def __init__(self, d, c, heads=6, xattn=False):
115
+ super().__init__()
116
+ self.heads = heads
117
+ self.ln = nn.LayerNorm(d, elementwise_affine=False)
118
+ self.mod = nn.Linear(c, 3 * d)
119
+ nn.init.zeros_(self.mod.weight); nn.init.zeros_(self.mod.bias)
120
+ self.qkv, self.o = nn.Linear(d, 3 * d), nn.Linear(d, d)
121
+ self.xattn = xattn
122
+ if xattn:
123
+ self.lx = nn.LayerNorm(d)
124
+ self.xq, self.xkv, self.xo = nn.Linear(d, d), nn.Linear(d, 2 * d), nn.Linear(d, d)
125
+ nn.init.zeros_(self.xo.weight); nn.init.zeros_(self.xo.bias)
126
+
127
+ def forward(self, x, m, c, tok=None): # x [B,d,T], tok [B,n,d]
128
+ B, d, T = x.shape
129
+ H = self.heads
130
+ sp = lambda z: z.view(B, -1, H, d // H).transpose(1, 2)
131
+ sc, sh, gate = self.mod(c)[:, None].chunk(3, -1)
132
+ h = self.ln(x.transpose(1, 2)) * (1 + sc) + sh
133
+ q, k, v = (sp(z) for z in self.qkv(h).chunk(3, -1))
134
+ h = F.scaled_dot_product_attention(rope(q), rope(k), v, attn_mask=m[:, 0].bool()[:, None, None])
135
+ h = self.o(h.transpose(1, 2).reshape(B, T, d)) * gate
136
+ if self.xattn and tok is not None:
137
+ hx = self.lx(x.transpose(1, 2) + h)
138
+ k2, v2 = self.xkv(tok).chunk(2, -1)
139
+ h = h + self.xo(F.scaled_dot_product_attention(sp(self.xq(hx)), sp(k2), sp(v2)).transpose(1, 2).reshape(B, T, d))
140
+ return x + h.transpose(1, 2) * m
141
+
142
+
143
+ class FlowNet(nn.Module):
144
+ """prompt=True (prompted flow): also takes a context mel ctx with mask cm (1 = real audio given: the reference
145
+ clip's frames), as Voicebox / F5-TTS infilling. The context enters the input stack, a masked mean of it is added
146
+ to the conditioning (global voice), and attention layers at attn_at carry it across the whole sequence.
147
+
148
+ v5 additions (all zero-initialised, so a v4 checkpoint loads into a v5 net unchanged):
149
+ rope_at layers that get an extra RoPE self-attention block (RAttnBlock)
150
+ n_ptok pooled prompt tokens (PromptPool) that the RoPE blocks cross-attend to
151
+ skips E2-TTS-style long skips, block i -> block n-1-i
152
+ align_at layers whose target-frame mean is mapped to a speaker-verification vector (TLA-SA-style feature
153
+ alignment loss; training only)"""
154
+
155
+ def __init__(self, n_mel=N_MELS, d=384, n_layers=12, g_dim=128, dils=(1, 2, 4, 1, 2, 4, 1, 2, 4, 1, 2, 4),
156
+ prompt=False, attn_at=(3, 7, 11), rope_at=(), n_ptok=0, skips=False, align_at=(), align_dim=512):
157
+ super().__init__()
158
+ self.cfg = dict(n_mel=n_mel, d=d, n_layers=n_layers, g_dim=g_dim, dils=tuple(dils), prompt=prompt,
159
+ attn_at=tuple(attn_at), rope_at=tuple(rope_at), n_ptok=n_ptok, skips=skips,
160
+ align_at=tuple(align_at), align_dim=align_dim)
161
+ self.prompt = prompt
162
+ self.inp = nn.Conv1d((3 * n_mel + 1) if prompt else 2 * n_mel, d, 3, padding=1)
163
+ self.temb = TimeEmb(d)
164
+ self.gemb = nn.Sequential(nn.Linear(g_dim, d), nn.SiLU(), nn.Linear(d, d))
165
+ self.blocks = nn.ModuleList([Block(d, d, dil=dils[i % len(dils)]) for i in range(n_layers)])
166
+ if prompt:
167
+ self.attn = nn.ModuleDict({str(i): AttnBlock(d, d) for i in attn_at})
168
+ self.ctx_enc = nn.Sequential(nn.Conv1d(n_mel, d, 3, padding=1), nn.SiLU(), nn.Conv1d(d, d, 3, padding=1))
169
+ self.ctx_out = nn.Linear(d, d)
170
+ nn.init.zeros_(self.ctx_out.weight); nn.init.zeros_(self.ctx_out.bias)
171
+ self.rattn = nn.ModuleDict({str(i): RAttnBlock(d, d, xattn=n_ptok > 0) for i in rope_at})
172
+ self.pool = PromptPool(d, n_ptok) if n_ptok else None
173
+ self.skips = skips
174
+ if skips:
175
+ self.skip = nn.ModuleDict({str(i): nn.Linear(d, d) for i in range(n_layers - n_layers // 2, n_layers)})
176
+ for l in self.skip.values():
177
+ nn.init.zeros_(l.weight); nn.init.zeros_(l.bias)
178
+ self.align_at = tuple(align_at)
179
+ if align_at:
180
+ self.align = nn.ModuleDict({str(i): nn.Sequential(nn.Linear(d, d), nn.SiLU(), nn.Linear(d, align_dim))
181
+ for i in align_at})
182
+ self.ln = nn.LayerNorm(d, elementwise_affine=False)
183
+ self.out = nn.Conv1d(d, n_mel, 1)
184
+ nn.init.zeros_(self.out.weight); nn.init.zeros_(self.out.bias)
185
+
186
+ def forward(self, x, t, mu, g, m, ctx=None, cm=None, feats=False): # normalised mels [B,100,T]; m, cm [B,1,T]
187
+ """feats=True also returns {layer: speaker vector predicted from the target-frame mean} for align_at."""
188
+ c = self.temb(t) + self.gemb(g)
189
+ tok = None
190
+ if self.prompt:
191
+ if cm is None:
192
+ ctx, cm = torch.zeros_like(x), torch.zeros_like(m)
193
+ ctx = ctx * cm
194
+ e = self.ctx_enc(ctx) * cm
195
+ c = c + self.ctx_out(e.sum(-1) / cm.sum(-1).clamp_min(1))
196
+ if getattr(self, "pool", None) is not None:
197
+ tok = self.pool(e, cm)
198
+ h = self.inp(torch.cat([x, mu, ctx, cm], 1) * m)
199
+ else:
200
+ h = self.inp(torch.cat([x, mu], 1) * m)
201
+ rattn = getattr(self, "rattn", {})
202
+ stack, out = [], {}
203
+ n = len(self.blocks)
204
+ for i, b in enumerate(self.blocks):
205
+ if self.skips and str(i) in self.skip:
206
+ h = h + self.skip[str(i)](stack.pop().transpose(1, 2)).transpose(1, 2) * m
207
+ h = b(h, m, c)
208
+ if self.prompt and str(i) in self.attn:
209
+ h = self.attn[str(i)](h, m, c)
210
+ if str(i) in rattn:
211
+ h = rattn[str(i)](h, m, c, tok)
212
+ if self.skips and i < n // 2:
213
+ stack.append(h)
214
+ if feats and i in self.align_at:
215
+ w = m * (1 - cm) if cm is not None else m
216
+ out[i] = self.align[str(i)](((h * w).sum(-1) / w.sum(-1).clamp_min(1)).float())
217
+ v = self.out(self.ln(h.transpose(1, 2)).transpose(1, 2)) * m
218
+ return (v, out) if feats else v
219
+
220
+
221
+ class Flow(nn.Module):
222
+ """FlowNet + mel normalisation; sample() maps a base mel to a refined mel (log-mel units, same frames)."""
223
+
224
+ def __init__(self, mean=-5.0, std=2.5, mu_cmn=False, **kw):
225
+ """mu_cmn (prompted flow v2): remove the per-band time-mean of the base mel (its static spectral envelope:
226
+ the long-term voice colouring the base model guessed from a 512-d speaker vector) so the flow must take the
227
+ voice from the audio prompt (or g); timing / pitch / energy / phonetic detail of mu are kept. Without it, mu
228
+ made from the TRUE speaker vector already carries the training speaker's colouring and the prompt adds
229
+ little (v3p @20k: Seed-TTS SIM 31.2 -> 33.2 only)."""
230
+ super().__init__()
231
+ self.net = FlowNet(**kw)
232
+ self.mu_cmn = mu_cmn
233
+ self.register_buffer("mean", torch.tensor(float(mean)))
234
+ self.register_buffer("std", torch.tensor(float(std)))
235
+
236
+ def norm(self, m):
237
+ return (m - self.mean) / self.std
238
+
239
+ def _cmn(self, mu_n, w):
240
+ """mu_n minus its per-band mean over the frames w marks (the target frames)."""
241
+ if not self.mu_cmn:
242
+ return mu_n
243
+ return mu_n - (mu_n * w).sum(-1, keepdim=True) / w.sum(-1, keepdim=True).clamp_min(1)
244
+
245
+ @torch.no_grad()
246
+ def sample(self, mu, g, m=None, steps=10, temp=0.8, method="midpoint", seed=None, ctx=None, cm=None, cfg=0.0):
247
+ """ctx / cm (prompted flow): real mel on the context frames (log-mel units), mask 1 there; mu is replaced by
248
+ the context on those frames, as in training. Returns every frame (callers slice off the context).
249
+ cfg (prompted flow): classifier-free guidance on the prompt, v + cfg * (v - v_no_prompt); the no-prompt
250
+ velocity is the same net with the context removed (trained on p_none of the items)."""
251
+ m = torch.ones_like(mu[:, :1]) if m is None else m
252
+ gen = None if seed is None else torch.Generator(device=mu.device).manual_seed(seed)
253
+ mu_n = self._cmn(self.norm(mu), m if cm is None else m * (1 - cm))
254
+ if cm is not None:
255
+ ctx = self.norm(ctx) * cm
256
+ mu_n = torch.where(cm.bool(), ctx, mu_n)
257
+ x = temp * torch.randn(mu_n.shape, device=mu.device, generator=gen) * m
258
+ ts = torch.linspace(0, 1, steps + 1, device=mu.device)
259
+
260
+ def vel(x, t):
261
+ v = self.net(x, t.expand(x.shape[0]), mu_n, g, m, ctx, cm)
262
+ if cfg and cm is not None:
263
+ v = v + cfg * (v - self.net(x, t.expand(x.shape[0]), mu_n, g, m, None, None))
264
+ return v
265
+
266
+ for i in range(steps):
267
+ t0, dt = ts[i], ts[i + 1] - ts[i]
268
+ v = vel(x, t0)
269
+ if method == "midpoint":
270
+ v = vel(x + 0.5 * dt * v, t0 + 0.5 * dt)
271
+ x = x + dt * v
272
+ return x * self.std + self.mean
273
+
274
+ @staticmethod
275
+ def from_state(state, cfg, device="cpu"):
276
+ f = Flow(**cfg)
277
+ f.load_state_dict(state)
278
+ return f.to(device).eval()
build/lib/steertts/mamba3/__init__.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Mamba-3: a paper-faithful, verified implementation of Lahoti et al. (arXiv 2603.15569).
2
+
3
+ Ported from the MMSE research repo (drowsiness-uta-rldd/mamba3, commit c5280e6+), without the MMSE
4
+ multi-stream extension or the LM tooling. Checked fwd+bwd at float64 against the recurrence:
5
+ - exponential-trapezoidal discretization with DATA-DEPENDENT lambda_t (Prop 1)
6
+ - complex state as real block-diagonal rotations / data-dependent RoPE (Prop 2-4, eq 11)
7
+ - MIMO rank-R (Sec 3.3)
8
+ - BCNorm/QKNorm on B,C + learnable data-independent B,C biases
9
+ - output gating
10
+ Recorded deviations: data-independent A (Remark 1 says equivalent); MIMO input uses a full DPR
11
+ projection rather than the paper's DP+PR (R=1 identical).
12
+
13
+ Paths: `Mamba3Layer.forward` (O(T) recurrence, reference), `scan_forward` (chunked scan),
14
+ `scan_forward_ssd` (SSD reduction, the fast path for training). Run `python -m
15
+ steertts.mamba3.tests` after any change.
16
+ """
17
+ from .layer import Mamba3Layer, rotate_pairs, ssm_scan
18
+ from .scan import scan_forward, chunked_scan, rotate_by, cumulative_angles
19
+ from .ssd import scan_forward_ssd, ssd_inputs, ssd_pytorch
20
+
21
+ __all__ = [
22
+ "Mamba3Layer", "rotate_pairs", "ssm_scan",
23
+ "scan_forward", "chunked_scan", "rotate_by", "cumulative_angles",
24
+ "scan_forward_ssd", "ssd_inputs", "ssd_pytorch",
25
+ ]
build/lib/steertts/mamba3/layer.py ADDED
@@ -0,0 +1,338 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Mamba-3 (Lahoti et al., arXiv 2603.15569), implemented to the paper.
2
+
3
+ THREE CHANGES FROM MAMBA-2, implemented here in the order the paper derives
4
+ them. Nothing is improvised: where this file departs from the paper it says
5
+ so, and the equivalence tests at the bottom are how that claim is checked.
6
+
7
+ 1. EXPONENTIAL-TRAPEZOIDAL DISCRETIZATION (Sec 3.1). Mamba-1/2 approximate
8
+
9
+ integral_{t_{k-1}}^{t_k} e^{A(t_k - tau)} B(tau) x(tau) d tau
10
+
11
+ by holding the endpoint (Euler). Mamba-3 averages both endpoints
12
+ (trapezoidal), which makes the recurrence depend on x_{t-1} as well as
13
+ x_t:
14
+
15
+ h_t = alpha_t h_{t-1} + beta_t B_{t-1} x_{t-1} + gamma_t B_t x_t
16
+
17
+ with alpha_t = exp(Delta_t A), beta_t = (1 - lambda_t) Delta_t alpha_t,
18
+ gamma_t = lambda_t Delta_t and a data-dependent lambda_t = sigmoid(.)
19
+ (lambda = 1/2 is the classical trapezoid, initialised here). That two-tap dependence IS the implicit convolution
20
+ the paper describes - it is why Mamba-3 can drop the short causal conv
21
+ that Mamba-1/2 carry as a separate component (Sec 3.1, and eq 7's
22
+ factorisation of L into a 1-semiseparable matrix times a 2-band matrix).
23
+
24
+ 2. COMPLEX STATE via the RoPE trick (Sec 3.2, Prop 2 and 3). A complex SSM
25
+ of state N/2 is equivalent to a REAL SSM of state N whose transition is a
26
+ scalar decay times a block-diagonal of 2x2 rotations:
27
+
28
+ R_t = blockdiag({ R(Delta_t theta_t[i]) })
29
+ h_t = alpha_t R_t h_{t-1} + ...
30
+
31
+ Real non-negative eigenvalues cannot express rotation, which is why
32
+ Mamba-2 provably fails state tracking (parity). This file implements the
33
+ real-equivalent form, not complex arithmetic, exactly as Prop 2 licenses.
34
+
35
+ 3. MIMO (Sec 3.3). x_t: (P,) -> (P,R), B_t: (N,) -> (N,R), C_t likewise;
36
+ the state stays (N,P) and the update becomes a matmul B_t x_t^T rather
37
+ than an outer product. Memory traffic is dominated by h_t and barely
38
+ moves; FLOPs rise by R. Arithmetic intensity goes Theta(1) -> Theta(R),
39
+ which is the whole point during memory-bound decode.
40
+
41
+ WHAT THIS FILE DOES NOT DO. No fused CUDA scan - the paper's speed comes
42
+ from hardware-aware kernels and this runs on MPS/CPU with an explicit loop
43
+ for the recurrent path and a materialised mask for the parallel path. The
44
+ MODELLING claims are reproducible here; the LATENCY claims are not.
45
+ """
46
+ from __future__ import annotations
47
+
48
+ import math
49
+
50
+ import torch
51
+ import torch.nn as nn
52
+ import torch.nn.functional as F
53
+
54
+
55
+ def rotate_pairs(h: torch.Tensor, ang: torch.Tensor) -> torch.Tensor:
56
+ """Apply blockdiag 2x2 rotations to the state's (even, odd) channel pairs.
57
+
58
+ h: (..., N) state, N even
59
+ ang: (..., N/2) rotation angle per pair
60
+ """
61
+ a, b = h[..., 0::2], h[..., 1::2]
62
+ c, s = torch.cos(ang), torch.sin(ang)
63
+ out = torch.empty_like(h)
64
+ out[..., 0::2] = c * a - s * b
65
+ out[..., 1::2] = s * a + c * b
66
+ return out
67
+
68
+
69
+ def ssm_scan(alpha, beta, gamma, theta, S, Cm, P, state=None, prev_S=None):
70
+ """The Mamba-3 recurrence (paper eq 11), shared by every Mamba-3 path.
71
+
72
+ This is the SINGLE SOURCE OF TRUTH for the recurrence, so a paper-faithfulness
73
+ fix here (e.g. the beta-term rotation) applies everywhere and cannot drift.
74
+
75
+ h_t = R_t(alpha_t h_{t-1} + beta_t S_{t-1}) + gamma_t S_t
76
+ y_t = C_t^T h_t
77
+
78
+ alpha,beta,gamma: (B,T,H) per-step scalars
79
+ theta: (B,T,H,N/2) per-step rotation angles (already * dt)
80
+ S: (B,T,H,N,P) per-step write B_t x_t^T (or summed streams)
81
+ Cm: (B,T,H,N,R) readout
82
+ P: head dim; state/prev_S carry across chunks for streaming.
83
+ Returns (y_flat (B,T,H*R*P), final state h, last write S[:,-1]).
84
+ """
85
+ B, T, H, N, _ = S.shape
86
+ R = Cm.shape[-1]
87
+ h = torch.zeros(B, H, N, P, device=S.device, dtype=S.dtype) \
88
+ if state is None else state
89
+ ys = []
90
+ for t in range(T):
91
+ prev = S[:, t - 1] if t > 0 else prev_S
92
+ pre = alpha[:, t, :, None, None] * h
93
+ if prev is not None:
94
+ pre = pre + beta[:, t, :, None, None] * prev
95
+ h = rotate_pairs(pre.transpose(-1, -2), theta[:, t].unsqueeze(-2)
96
+ .expand(-1, -1, P, -1)).transpose(-1, -2)
97
+ h = h + gamma[:, t, :, None, None] * S[:, t]
98
+ ys.append((Cm[:, t].transpose(-1, -2) @ h).reshape(B, H * R * P))
99
+ return torch.stack(ys, 1), h, S[:, -1]
100
+
101
+
102
+ class Mamba3Layer(nn.Module):
103
+ """One Mamba-3 layer. Recurrent and parallel paths, provably equal.
104
+
105
+ d_model input/output width
106
+ d_state N, the SSM state size (even; pairs are rotated)
107
+ rank R of the MIMO lift. rank=1 is SISO.
108
+ head_dim P, channels sharing one (alpha, Delta, B) recurrence.
109
+ """
110
+
111
+ def __init__(self, d_model: int, d_state: int = 64, rank: int = 1,
112
+ head_dim: int = 32, dt_min: float = 1e-3, dt_max: float = 0.5):
113
+ super().__init__()
114
+ assert d_state % 2 == 0, "d_state must be even: the state rotates in pairs"
115
+ assert d_model % head_dim == 0, "d_model must divide into heads of head_dim"
116
+ self.d_model, self.N, self.R, self.P = d_model, d_state, rank, head_dim
117
+ self.H = d_model // head_dim
118
+
119
+ # Gate branch (Mamba-2/3 block, Fig 2): the SSM output is multiplied by
120
+ # SiLU(in_proj(input)) before out_proj. Dimension d_model*R to match the
121
+ # pre-out_proj SSM output (H*R*P). Was defined-but-unused before.
122
+ self.in_proj = nn.Linear(d_model, d_model * rank)
123
+ self.B_proj = nn.Linear(d_model, self.H * d_state * rank)
124
+ self.C_proj = nn.Linear(d_model, self.H * d_state * rank)
125
+ self.x_proj = nn.Linear(d_model, d_model * rank)
126
+ self.dt_proj = nn.Linear(d_model, self.H)
127
+ self.theta_proj = nn.Linear(d_model, self.H * (d_state // 2))
128
+ # lambda_t: the DATA-DEPENDENT trapezoidal weight (paper Prop 1, Remark
129
+ # 3). gamma_t = lambda_t * dt, beta_t = (1-lambda_t) * dt * alpha_t.
130
+ # lambda=1/2 is the classical trapezoid; the paper found NOT enforcing
131
+ # that (letting the model choose per step) is empirically better. Bias 0
132
+ # -> sigmoid 0.5 at init, so training starts from the classical rule.
133
+ self.lam_proj = nn.Linear(d_model, self.H)
134
+ nn.init.zeros_(self.lam_proj.weight)
135
+ nn.init.zeros_(self.lam_proj.bias)
136
+ self.out_proj = nn.Linear(d_model * rank, d_model)
137
+ self.norm = nn.RMSNorm(d_model) if hasattr(nn, "RMSNorm") else nn.LayerNorm(d_model)
138
+
139
+ # BCNorm / QKNorm (paper section 3.2). RMS normalisation AFTER the B and C
140
+ # projections, mirroring QKNorm in Transformers. The paper reports this
141
+ # is what stabilises large-scale runs, to the point that they remove
142
+ # Mamba-2's post-gate RMSNorm on the strength of it. It was missing from
143
+ # the first version of this file and E17's ssm arm then diverged
144
+ # (0.330 -> 19.313) into a constant predictor, which was patched with
145
+ # gradient clipping - a generic fix for a problem the paper had already
146
+ # solved specifically. Normalising over the N state dimensions, which is
147
+ # the axis B and C index into.
148
+ rms = nn.RMSNorm if hasattr(nn, "RMSNorm") else nn.LayerNorm
149
+ self.B_norm = rms(d_state)
150
+ self.C_norm = rms(d_state)
151
+ # B, C biases (paper appendix): learnable, head-specific, channel-wise,
152
+ # data-INDEPENDENT, added after the norm, initialised to all ones. The
153
+ # paper finds models insensitive to the init as long as it is positive,
154
+ # and that B and C biases together beat either alone.
155
+ self.B_bias = nn.Parameter(torch.ones(self.H, d_state, rank))
156
+ self.C_bias = nn.Parameter(torch.ones(self.H, d_state, rank))
157
+
158
+ # A is data-INDEPENDENT here. Remark 1: the paper found data-dependent
159
+ # A_t performs similarly and chose it for consistency; the simpler
160
+ # form is used here and the difference is recorded rather than hidden.
161
+ self.A_log = nn.Parameter(torch.log(torch.rand(self.H) * 0.9 + 0.05))
162
+ # dt bias initialised so Delta spans [dt_min, dt_max] - the standard
163
+ # Mamba init. Delta sets the memory horizon, so this is the prior on
164
+ # how long the layer remembers before it learns anything.
165
+ dt = torch.exp(torch.rand(self.H) * (math.log(dt_max) - math.log(dt_min))
166
+ + math.log(dt_min))
167
+ self.dt_proj.bias.data = dt + torch.log(-torch.expm1(-dt))
168
+
169
+ def _coeffs(self, u: torch.Tensor):
170
+ """alpha, beta, gamma, B, C, x, theta for every timestep."""
171
+ Bsz, T, _ = u.shape
172
+ H, N, R, P = self.H, self.N, self.R, self.P
173
+ dt = F.softplus(self.dt_proj(u)) # (B,T,H) > 0
174
+ A = -torch.exp(self.A_log) # (H,) < 0
175
+ alpha = torch.exp(dt * A) # (B,T,H) in (0,1)
176
+ # Exponential-trapezoidal with a DATA-DEPENDENT weight lambda_t (paper
177
+ # Prop 1 / eq 5-6): gamma_t = lambda_t*dt (current input), beta_t =
178
+ # (1-lambda_t)*dt*alpha_t (previous input). lambda=1/2 recovers the
179
+ # classical trapezoid; lambda=1 recovers Mamba-2's exponential-Euler.
180
+ lam = torch.sigmoid(self.lam_proj(u)) # (B,T,H) in (0,1)
181
+ gamma = lam * dt
182
+ beta = (1.0 - lam) * dt * alpha
183
+ # BCNorm then bias, in that order - the paper adds the bias AFTER the
184
+ # norm, so the bias is not rescaled by it.
185
+ Bm = self.B_proj(u).view(Bsz, T, H, R, N)
186
+ Bm = self.B_norm(Bm).permute(0, 1, 2, 4, 3) + self.B_bias
187
+ Cm = self.C_proj(u).view(Bsz, T, H, R, N)
188
+ Cm = self.C_norm(Cm).permute(0, 1, 2, 4, 3) + self.C_bias
189
+ x = self.x_proj(u).view(Bsz, T, H, P, R)
190
+ theta = self.theta_proj(u).view(Bsz, T, H, N // 2) * dt.unsqueeze(-1)
191
+ return alpha, beta, gamma, Bm, Cm, x, theta
192
+
193
+ def gate_and_proj(self, y_flat: torch.Tensor, nu: torch.Tensor):
194
+ """Apply the Mamba gate then the output projection.
195
+
196
+ y_flat: (..., d_model*R) SSM output before out_proj.
197
+ nu: (..., d_model) the NORMALISED layer input (in_proj sees the
198
+ same normalised input the SSM path does).
199
+ out = out_proj( y_flat * SiLU(in_proj(nu)) ).
200
+ """
201
+ return self.out_proj(y_flat * F.silu(self.in_proj(nu)))
202
+
203
+ def forward(self, u: torch.Tensor, state=None, return_state=False):
204
+ """Recurrent path. u: (B,T,d_model). This is the streaming form.
205
+
206
+ The recurrence itself lives in the shared `ssm_scan` (paper eq 11), so
207
+ callers cannot drift. Here the per-step write is the
208
+ single-stream outer product S_t = B_t x_t^T.
209
+
210
+ state: None or the (h, S_last) pair returned by a previous call with
211
+ return_state=True. Segment-wise streaming must carry S_last as well as
212
+ h: the first step of the next segment needs beta_t B_{t-1} x_{t-1}^T,
213
+ the trapezoidal term, and carrying h alone silently drops it.
214
+ """
215
+ nu = self.norm(u)
216
+ alpha, beta, gamma, Bm, Cm, x, theta = self._coeffs(nu)
217
+ S = Bm @ x.transpose(-1, -2) # (B,T,H,N,P)
218
+ h0, prev_S = (None, None) if state is None else state
219
+ y_flat, h, S_last = ssm_scan(alpha, beta, gamma, theta, S, Cm, self.P,
220
+ state=h0, prev_S=prev_S)
221
+ out = self.gate_and_proj(y_flat, nu)
222
+ return (out, (h, S_last)) if return_state else out
223
+
224
+ def step(self, u_t: torch.Tensor, state, prev_bx=None):
225
+ """One timestep, constant memory. The deployment path.
226
+
227
+ state: (B,H,N,P) or None. prev_bx: B_{t-1}x_{t-1}^T for the beta term,
228
+ which the trapezoidal rule requires and Mamba-2 does not have.
229
+ """
230
+ Bsz = u_t.shape[0]
231
+ nu = self.norm(u_t.unsqueeze(1))
232
+ alpha, beta, gamma, Bm, Cm, x, theta = self._coeffs(nu)
233
+ H, N, P = self.H, self.N, self.P
234
+ h = torch.zeros(Bsz, H, N, P, device=u_t.device, dtype=u_t.dtype) \
235
+ if state is None else state
236
+ # Match forward/eq(11): rotate (alpha*h + beta*prev_bx) by R_t, then add
237
+ # the current (unrotated) write. prev_bx = B_{t-1}x_{t-1}^T from the last
238
+ # step; bx = B_t x_t^T is returned for the next step's beta term.
239
+ pre = alpha[:, 0, :, None, None] * h
240
+ if prev_bx is not None:
241
+ pre = pre + beta[:, 0, :, None, None] * prev_bx
242
+ h = rotate_pairs(pre.transpose(-1, -2), theta[:, 0].unsqueeze(-2)
243
+ .expand(-1, -1, P, -1)).transpose(-1, -2)
244
+ bx = Bm[:, 0] @ x[:, 0].transpose(-1, -2)
245
+ h = h + gamma[:, 0, :, None, None] * bx
246
+ y = (Cm[:, 0].transpose(-1, -2) @ h).reshape(Bsz, -1)
247
+ return self.gate_and_proj(y, nu[:, 0]), h, bx
248
+
249
+
250
+ # ---------------------------------------------------------------------------
251
+ def selftest(verbose: bool = True) -> dict:
252
+ """The four invariants, at float64.
253
+
254
+ These were an ad-hoc scratch script until BCNorm was added and there was
255
+ nothing to re-run. A verified file whose verification lives somewhere else
256
+ is an unverified file the moment anyone edits it.
257
+
258
+ python -m steertts.mamba3.layer
259
+ """
260
+ torch.manual_seed(0)
261
+ torch.set_default_dtype(torch.float64)
262
+ net = Mamba3Layer(d_model=32, d_state=16, rank=2, head_dim=8).eval()
263
+ B, T = 2, 20
264
+ u = torch.randn(B, T, 32)
265
+ out = {}
266
+
267
+ with torch.no_grad():
268
+ y = net(u)
269
+
270
+ # 1. CAUSALITY, as a SCALE test rather than an equality one. Appending
271
+ # future timesteps moves the past by ~1e-15 here: the permute in
272
+ # _coeffs leaves B and C non-contiguous, so matmul takes a different
273
+ # kernel path at a different T and the last bits differ. An equality
274
+ # assertion would call that a leak, and would also fail spuriously in
275
+ # float32. A real leak scales with the size of the future; rounding
276
+ # does not. Drive the future through four orders of magnitude and
277
+ # require the drift to stay put.
278
+ drifts = []
279
+ for mag in (1.0, 1e2, 1e4):
280
+ u2 = torch.cat([u, torch.randn(B, 6, 32) * mag], 1)
281
+ drifts.append(float((net(u2)[:, :T] - y).abs().max()))
282
+ out["causality"] = max(drifts)
283
+ out["causality_scaling"] = max(drifts) / max(min(drifts), 1e-300)
284
+
285
+ # 2. STREAMING == BATCHED. The deployment path must equal training.
286
+ st = bx = None
287
+ steps = []
288
+ for t in range(T):
289
+ o, st, bx = net.step(u[:, t], st, bx)
290
+ steps.append(o)
291
+ out["streaming"] = float((torch.stack(steps, 1) - y).abs().max())
292
+
293
+ # 3. ROTATION IS ORTHOGONAL. State norm preserved, so it cannot blow up.
294
+ h = torch.randn(B, 4, 16, 8)
295
+ ang = torch.randn(B, 4, 1, 8)
296
+ hr = rotate_pairs(h.transpose(-1, -2), ang).transpose(-1, -2)
297
+ out["orthogonal"] = float(
298
+ (hr.norm(dim=-2) - h.norm(dim=-2)).abs().max())
299
+
300
+ # 4. TRAPEZOIDAL TWO-TAP. Perturbing x_{t-1} moves y_t, not y_{t-2}.
301
+ up = u.clone(); up[:, 3] += 1.0
302
+ yp = net(up)
303
+ out["twotap_y4"] = float((yp[:, 4] - y[:, 4]).abs().max())
304
+ out["twotap_y2"] = float((yp[:, 2] - y[:, 2]).abs().max())
305
+
306
+ # 5. BCNORM IS ACTIVE. B's pre-bias rows should have unit RMS. Without
307
+ # the norm this is whatever the projection happens to produce, and
308
+ # the first version of this file had no norm at all.
309
+ a, b, g, Bm, Cm, x, th = net._coeffs(net.norm(u))
310
+ # Bm is (B,T,H,N,R) after the permute, so the normalised axis is -2.
311
+ rms = (Bm - net.B_bias).pow(2).mean(-2).sqrt()
312
+ out["bcnorm_rms"] = float((rms - 1.0).abs().max())
313
+
314
+ out["params"] = sum(p.numel() for p in net.parameters())
315
+ torch.set_default_dtype(torch.float32)
316
+
317
+ if verbose:
318
+ ok = lambda c: "OK" if c else "FAIL"
319
+ print("Mamba-3 selftest (float64)")
320
+ print(f" 1 causality (future->past) {out['causality']:.3e}"
321
+ f" {ok(out['causality'] < 1e-12)}")
322
+ print(f" drift x1 vs x10000 ratio {out['causality_scaling']:.3f}"
323
+ f" {ok(out['causality_scaling'] < 1.5)} (a leak would scale)")
324
+ print(f" 2 step() == forward() {out['streaming']:.3e}"
325
+ f" {ok(out['streaming'] < 1e-10)}")
326
+ print(f" 3 rotation orthogonal {out['orthogonal']:.3e}"
327
+ f" {ok(out['orthogonal'] < 1e-10)}")
328
+ print(f" 4 trapezoidal two-tap y4 {out['twotap_y4']:.3e}"
329
+ f" y2 {out['twotap_y2']:.3e}"
330
+ f" {ok(out['twotap_y4'] > 1e-6 and out['twotap_y2'] == 0.0)}")
331
+ print(f" 5 BCNorm rows unit RMS {out['bcnorm_rms']:.3e}"
332
+ f" {ok(out['bcnorm_rms'] < 1e-10)}")
333
+ print(f" {out['params']:,} params")
334
+ return out
335
+
336
+
337
+ if __name__ == "__main__":
338
+ selftest()
build/lib/steertts/mamba3/reference.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Independent Mamba-3 references, written from the paper rather than from this package.
2
+
3
+ Nothing here calls Mamba3Layer._coeffs, ssm_scan or rotate_by: every coefficient is recomputed from
4
+ the raw parameters with the paper's definitions, so a coefficient bug in layer.py cannot hide by being
5
+ shared with the thing that checks it (tests.paper_eq11_check reuses _coeffs and so cannot catch one).
6
+
7
+ ref_complex the complex-valued SSM itself (Prop 2; App B.3 eq 24), in complex128, MIMO as an
8
+ explicit sum over ranks (eq 12-14). A sequential loop, the most literal form.
9
+ ref_parallel the SSD parallel form, eq (7): L = (1-semiseparable decay) @ (2-band beta/gamma),
10
+ applied to cumulative-RoPE'd B and C (Prop 3/4). Quadratic in T.
11
+
12
+ The two are derived independently (complex arithmetic vs real rotations + masks) and agree to ~1e-15,
13
+ so each layer path is checked against two references that do not share code with it or each other.
14
+ Both are float64 and slow; they are for tests only.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import torch
19
+ import torch.nn.functional as F
20
+
21
+
22
+ def _rms(x, w, eps):
23
+ eps = torch.finfo(x.dtype).eps if eps is None else eps
24
+ return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + eps) * w
25
+
26
+
27
+ def raw_coeffs(L, u):
28
+ """Every per-step quantity of a Mamba3Layer, from raw parameters (float64)."""
29
+ u = u.double()
30
+ Bsz, T, _ = u.shape
31
+ H, N, R, P = L.H, L.N, L.R, L.P
32
+ nu = _rms(u, L.norm.weight, L.norm.eps)
33
+ dt = F.softplus(F.linear(nu, L.dt_proj.weight, L.dt_proj.bias)) # (B,T,H)
34
+ A = -torch.exp(L.A_log) # (H,)
35
+ lam = torch.sigmoid(F.linear(nu, L.lam_proj.weight, L.lam_proj.bias)) # trapezoid weight
36
+ Bn = _rms(F.linear(nu, L.B_proj.weight, L.B_proj.bias).view(Bsz, T, H, R, N),
37
+ L.B_norm.weight, L.B_norm.eps) + L.B_bias.permute(0, 2, 1) # (B,T,H,R,N)
38
+ Cn = _rms(F.linear(nu, L.C_proj.weight, L.C_proj.bias).view(Bsz, T, H, R, N),
39
+ L.C_norm.weight, L.C_norm.eps) + L.C_bias.permute(0, 2, 1)
40
+ x = F.linear(nu, L.x_proj.weight, L.x_proj.bias).view(Bsz, T, H, P, R)
41
+ th = F.linear(nu, L.theta_proj.weight, L.theta_proj.bias).view(Bsz, T, H, N // 2)
42
+ z = F.linear(nu, L.in_proj.weight, L.in_proj.bias) # gate pre-activation
43
+ return dt, A, lam, Bn, Cn, x, th, z
44
+
45
+
46
+ def _finish(L, y, z): # y (B,T,H,R,P) -> gate, out_proj
47
+ Bsz, T = y.shape[:2]
48
+ return F.linear(y.reshape(Bsz, T, -1) * F.silu(z), L.out_proj.weight, L.out_proj.bias)
49
+
50
+
51
+ def ref_complex(L, u):
52
+ """h_t = e^{dt A} e^{i dt theta} h_{t-1} + beta_t e^{i dt theta} W_{t-1} + gamma_t W_t,
53
+ W_t = sum_r B_t^(r) x_t^(r)T, y_t^(r) = Re(conj(C_t^(r)) . h_t), state of N/2 complex numbers."""
54
+ dt, A, lam, Bn, Cn, x, th, z = raw_coeffs(L, u)
55
+ Bsz, T, H, R, N = Bn.shape
56
+ Bc = torch.complex(Bn[..., 0::2], Bn[..., 1::2]) # (B,T,H,R,N/2)
57
+ Cc = torch.complex(Cn[..., 0::2], Cn[..., 1::2])
58
+ h = torch.zeros(Bsz, H, N // 2, L.P, dtype=torch.complex128, device=u.device)
59
+ Wprev = torch.zeros_like(h)
60
+ ys = []
61
+ for t in range(T):
62
+ d = dt[:, t]
63
+ alpha = torch.exp(d * A)
64
+ rot = torch.exp(1j * (d[..., None] * th[:, t]))[..., None] # (B,H,N/2,1)
65
+ W = torch.einsum('bhrk,bhpr->bhkp', Bc[:, t], x[:, t].to(Bc.dtype))
66
+ beta = ((1 - lam[:, t]) * d * alpha)[..., None, None]
67
+ gamma = (lam[:, t] * d)[..., None, None]
68
+ h = alpha[..., None, None] * rot * h + beta * rot * Wprev + gamma * W
69
+ Wprev = W
70
+ ys.append(torch.einsum('bhrk,bhkp->bhrp', Cc[:, t].conj(), h).real)
71
+ return _finish(L, torch.stack(ys, 1), z)
72
+
73
+
74
+ def ref_parallel(L, u):
75
+ """eq (7): y = (decay @ band) * (C~ B~^T) x with C~, B~ rotated by the cumulative angle."""
76
+ dt, A, lam, Bn, Cn, x, th, z = raw_coeffs(L, u)
77
+ Bsz, T, H, R, N = Bn.shape
78
+ ang = torch.cumsum(dt[..., None] * th, 1)[:, :, :, None] # Phi_t (B,T,H,1,N/2)
79
+ c, s = torch.cos(ang), torch.sin(ang)
80
+
81
+ def rope_T(v): # (prod_{i<=t} R_i)^T v, i.e. rotate each pair by -Phi_t
82
+ a, b = v[..., 0::2], v[..., 1::2]
83
+ return torch.stack((c * a + s * b, -s * a + c * b), -1).flatten(-2)
84
+ Bb, Cb = rope_T(Bn), rope_T(Cn)
85
+ la = dt * A
86
+ alpha = torch.exp(la)
87
+ gamma, beta = lam * dt, (1 - lam) * dt * alpha
88
+ cs = torch.cumsum(la, 1)
89
+ tri = torch.tril(torch.ones(T, T, dtype=torch.bool, device=u.device))[None, :, :, None]
90
+ dec = torch.exp((cs[:, :, None] - cs[:, None, :]).masked_fill(~tri, float("-inf"))) # (B,t,s,H)
91
+ band = torch.zeros(Bsz, T, T, H, dtype=torch.float64, device=u.device)
92
+ i = torch.arange(T, device=u.device)
93
+ band[:, i, i] = gamma
94
+ band[:, i[1:], i[:-1]] = beta[:, 1:]
95
+ Lm = torch.einsum('btkh,bksh->btsh', dec, band)
96
+ G = torch.einsum('bthin,bshjn->bhtsij', Cb, Bb)
97
+ y = torch.einsum('btsh,bhtsij,bshpj->bthip', Lm, G, x)
98
+ return _finish(L, y, z)
build/lib/steertts/mamba3/scan.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Chunked parallel scan for the Mamba-3 recurrence.
2
+
3
+ WHY. The recurrent path is a Python loop of T sequential steps. Measured on
4
+ this laptop: d_model 256 / 4 layers / N 32 runs at 3,670 tok/s, while
5
+ 384 / 6 / 64 runs at 179 tok/s - 20x slower for ~4x the FLOPs. The gap is not
6
+ arithmetic, it is kernel-launch overhead: 6 layers x 128 timesteps is 768
7
+ sequential iterations each firing a handful of tiny kernels, so DEPTH
8
+ MULTIPLIES LAUNCHES. That is what blocks scaling, not parameters.
9
+
10
+ THE TRICK, and it is the paper's own (Proposition 3). The recurrence
11
+
12
+ h_t = alpha_t R(theta_t) h_{t-1} + beta_t S_{t-1} + gamma_t S_t
13
+
14
+ has a data-dependent rotation inside it, which a scan cannot factor. But
15
+ rotations compose: define the CUMULATIVE angle Phi_t = sum_{s<=t} theta_s and
16
+ change basis by R(-Phi_t). In that frame the rotation disappears and what is
17
+ left is a plain first-order linear recurrence
18
+
19
+ g_t = alpha_t g_{t-1} + w_t w_t = R(-Phi_t) (beta_t S_{t-1} + gamma_t S_t)
20
+
21
+ with h_t = R(Phi_t) g_t. The paper states this as applying data-dependent
22
+ RoPE to B and C; the same identity in state space is what makes the scan
23
+ possible at all.
24
+
25
+ THE SCAN. g_t = alpha_t g_{t-1} + w_t has the closed form
26
+
27
+ g_t = A_t * sum_{s<=t} w_s / A_s A_t = prod_{r<=t} alpha_r
28
+
29
+ which is numerically hopeless directly - A_s underflows for long sequences
30
+ and the division explodes. So this chunks: within a chunk of length L the sum
31
+ is materialised as an L x L lower-triangular decay matrix (no division, decay
32
+ is always relative to the chunk start), and chunk states are carried
33
+ recurrently. T/L sequential steps instead of T. The intra-chunk work becomes
34
+ one batched matmul, which is what GPUs are for.
35
+
36
+ CORRECTNESS IS CHECKED AGAINST THE RECURRENCE, NOT AGAINST A LOSS CURVE.
37
+ `verify()` compares this to Mamba3Layer.forward at float64. A fast scan that
38
+ computes something slightly different would show up as a slightly worse loss
39
+ curve and be indistinguishable from a hyperparameter difference - which is
40
+ exactly how this project has lost time before.
41
+
42
+ python -m steertts.mamba3.scan
43
+ """
44
+ from __future__ import annotations
45
+
46
+ import math
47
+ import torch
48
+
49
+
50
+ def cumulative_angles(theta: torch.Tensor) -> torch.Tensor:
51
+ """Phi_t = sum_{s<=t} theta_s. theta: (B,T,H,N/2)."""
52
+ return torch.cumsum(theta, dim=1)
53
+
54
+
55
+ def rotate_by(v: torch.Tensor, ang: torch.Tensor) -> torch.Tensor:
56
+ """Rotate pairs of the N axis. v: (...,N,P), ang: (...,N/2) broadcastable.
57
+
58
+ Built with stack+reshape rather than writing into an empty tensor. The
59
+ first version used `out = torch.empty_like(v)` with strided assignment,
60
+ which is correct in the forward direction and BREAKS AUTOGRAD: in-place
61
+ writes bump the version counter of a tensor the graph still needs. The
62
+ forward equivalence check passed anyway, because it ran under no_grad.
63
+ """
64
+ a, b = v[..., 0::2, :], v[..., 1::2, :]
65
+ c, s = torch.cos(ang).unsqueeze(-1), torch.sin(ang).unsqueeze(-1)
66
+ even, odd = c * a - s * b, s * a + c * b
67
+ return torch.stack((even, odd), dim=-2).flatten(-3, -2)
68
+
69
+
70
+ def chunked_scan(alpha: torch.Tensor, w: torch.Tensor, chunk: int = 64,
71
+ state=None):
72
+ """g_t = alpha_t g_{t-1} + w_t, computed in chunks.
73
+
74
+ alpha: (B,T,H) per-step decay, in (0,1)
75
+ w: (B,T,H,N,P) per-step input, already in the rotated frame
76
+ returns g: (B,T,H,N,P) and the final state.
77
+
78
+ Within a chunk, g_t = d_t * g_0 + sum_{s<=t} (d_t/d_s) w_s where
79
+ d_t = prod_{r<=t} alpha_r RELATIVE TO THE CHUNK START. Both d_t and d_t/d_s
80
+ stay bounded because the product never runs longer than `chunk` steps -
81
+ that is the whole reason for chunking rather than a global cumprod.
82
+ """
83
+ B, T, H = alpha.shape
84
+ N, P = w.shape[-2], w.shape[-1]
85
+ # Accumulate chunks in a list and cat once. Writing into a preallocated
86
+ # g_out and then reading g_out[:, e-1] back as the next chunk's state is
87
+ # an in-place write into a tensor autograd still needs.
88
+ pieces = []
89
+ g = torch.zeros(B, H, N, P, device=w.device, dtype=w.dtype) \
90
+ if state is None else state
91
+ la = torch.log(alpha.clamp_min(1e-30)) # (B,T,H)
92
+ for s in range(0, T, chunk):
93
+ e = min(s + chunk, T)
94
+ L = e - s
95
+ cl = torch.cumsum(la[:, s:e], dim=1) # (B,L,H) log d_t
96
+ d = torch.exp(cl) # decay from chunk start
97
+ # (d_t / d_s) for s<=t, lower triangular, exp of a difference of logs
98
+ rel = cl.unsqueeze(2) - cl.unsqueeze(1) # (B,L,L,H) log d_t - log d_s
99
+ # mask before exp (see ssd.ssd_pytorch): exp of the positive upper triangle overflows and where()
100
+ # of an inf back-propagates NaN
101
+ mask = torch.tril(torch.ones(L, L, device=w.device, dtype=torch.bool))
102
+ M = torch.exp(rel.masked_fill(~mask.view(1, L, L, 1), float("-inf")))
103
+ # intra-chunk: sum_s M[t,s] * w_s -> one batched matmul over (L,L) x (L, N*P)
104
+ wf = w[:, s:e].reshape(B, L, H, N * P).permute(0, 2, 1, 3) # (B,H,L,NP)
105
+ Mp = M.permute(0, 3, 1, 2) # (B,H,L,L)
106
+ acc = (Mp @ wf).permute(0, 2, 1, 3).reshape(B, L, H, N, P)
107
+ # carry the incoming chunk state, decayed
108
+ piece = acc + d.unsqueeze(-1).unsqueeze(-1) * g.unsqueeze(1)
109
+ pieces.append(piece)
110
+ g = piece[:, -1]
111
+ return torch.cat(pieces, dim=1), g
112
+
113
+
114
+ def scan_forward(layer, u: torch.Tensor, chunk: int = 64):
115
+ """Mamba3Layer.forward computed with the chunked scan instead of the loop."""
116
+ Bsz, T, _ = u.shape
117
+ H, N, R, P = layer.H, layer.N, layer.R, layer.P
118
+ nu = layer.norm(u)
119
+ alpha, beta, gamma, Bm, Cm, x, theta = layer._coeffs(nu)
120
+ S = Bm @ x.transpose(-1, -2) # (B,T,H,N,P)
121
+ # trapezoidal: beta carries the PREVIOUS step's write. Built by cat, not
122
+ # by assigning into a zeros tensor, for the same autograd reason.
123
+ Sprev = torch.cat([torch.zeros_like(S[:, :1]), S[:, :-1]], dim=1)
124
+ Phi = cumulative_angles(theta) # (B,T,H,N/2) = Phi_s
125
+ Phi_prev = torch.cat([torch.zeros_like(Phi[:, :1]), Phi[:, :-1]], dim=1)
126
+ # Paper eq (11): the gamma (current) write rotates by R(-Phi_s), the beta
127
+ # (previous) write by R(-Phi_{s-1}) - one step less. Rotate the two terms
128
+ # separately into the global frame, then scan.
129
+ w = (gamma[..., None, None] * rotate_by(S, -Phi)
130
+ + beta[..., None, None] * rotate_by(Sprev, -Phi_prev))
131
+ g, _ = chunked_scan(alpha, w, chunk)
132
+ h = rotate_by(g, Phi) # back out
133
+ y = (Cm.transpose(-1, -2) @ h).reshape(Bsz, T, H * R * P)
134
+ return layer.gate_and_proj(y, nu)
135
+
136
+
137
+ def verify(chunks=(8, 16, 64), verbose: bool = True) -> dict:
138
+ """Against the recurrence, at float64 - FORWARD AND BACKWARD.
139
+
140
+ The backward half is not optional. The first version of this file passed
141
+ every forward check and could not backpropagate at all: it built outputs
142
+ with in-place strided writes, which autograd rejects. The forward checks
143
+ ran under no_grad and saw nothing. A scan that cannot train is useless,
144
+ and one that trains with subtly wrong gradients is worse than useless -
145
+ it would surface as a slightly worse loss curve, indistinguishable from a
146
+ hyperparameter difference."""
147
+ from .layer import Mamba3Layer
148
+
149
+ torch.manual_seed(0)
150
+ torch.set_default_dtype(torch.float64)
151
+ out = {}
152
+ lay = Mamba3Layer(d_model=32, d_state=16, rank=2, head_dim=8).eval()
153
+ u = torch.randn(3, 40, 32)
154
+ with torch.no_grad():
155
+ ref = lay(u)
156
+ for c in chunks:
157
+ got = scan_forward(lay, u, chunk=c)
158
+ out[f"chunk{c}"] = float((got - ref).abs().max())
159
+ # a length that is not a multiple of the chunk
160
+ u2 = torch.randn(2, 37, 32)
161
+ r2 = lay(u2)
162
+ out["ragged"] = float((scan_forward(lay, u2, chunk=16) - r2).abs().max())
163
+ # causality of the scan path itself
164
+ u3 = torch.cat([u, torch.randn(3, 9, 32) * 50], 1)
165
+ out["causality"] = float(
166
+ (scan_forward(lay, u3, chunk=16)[:, :40]
167
+ - scan_forward(lay, u, chunk=16)).abs().max())
168
+
169
+ # ---- BACKWARD ----
170
+ ug = torch.randn(3, 24, 32, requires_grad=True)
171
+ lay.zero_grad()
172
+ lay(ug).pow(2).sum().backward()
173
+ gu_rec = ug.grad.clone()
174
+ gp_rec = {n: p.grad.clone() for n, p in lay.named_parameters()
175
+ if p.grad is not None}
176
+ ug.grad = None
177
+ lay.zero_grad()
178
+ scan_forward(lay, ug, chunk=8).pow(2).sum().backward()
179
+ out["grad_input"] = float((gu_rec - ug.grad).abs().max())
180
+ out["grad_params"] = max(
181
+ float((gp_rec[n] - p.grad).abs().max())
182
+ for n, p in lay.named_parameters() if p.grad is not None)
183
+ torch.set_default_dtype(torch.float32)
184
+ if verbose:
185
+ ok = lambda c: "OK " if c else "FAIL"
186
+ print("chunked scan vs recurrence (float64)")
187
+ for k, v in out.items():
188
+ print(f" {k:<12}{v:.3e} {ok(v < 1e-9)}")
189
+ return out
190
+
191
+
192
+ if __name__ == "__main__":
193
+ verify()
build/lib/steertts/mamba3/ssd.py ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fused Mamba-3 scan: the SSD reduction, verified, then a Triton kernel.
2
+
3
+ WHY THIS FILE. scan.py's chunked scan materialises (B,T,H,N,P) intermediates
4
+ (S, inp, w, g, h) in HBM and fires many kernels; on the RTX 5070 it is
5
+ memory-bandwidth bound and runs at ~3.4k tok/s for a 12M-param model at ctx
6
+ 1024. The Mamba-3 paper's speed comes from a fused chunked kernel that keeps the
7
+ chunk state in SRAM and never writes the expanded state to HBM. This file builds
8
+ that, test-first, against the verified recurrence in layer.py.
9
+
10
+ THE REDUCTION (paper Sec 3.2-3.3, "data-dependent RoPE on B and C"). The
11
+ recurrence
12
+
13
+ h_t = alpha_t R(theta_t) h_{t-1} + gamma_t B_t x_t^T + beta_t B_{t-1} x_{t-1}^T
14
+ y_t = C_t^T h_t
15
+
16
+ becomes, with Phi_t = cumsum(theta) and the rotations absorbed into B and C,
17
+
18
+ y_t = Ctil_t^T sum_{s<=t} decay(s,t) (Beff_s xeff_s^T), decay(s,t)=prod_{s<r<=t} alpha_r
19
+ Ctil_t = R(-Phi_t) C_t
20
+ Beff_s = [ gamma_s R(-Phi_s)B_s , beta_s R(-Phi_s)B_{s-1} ] (N, 2R)
21
+ xeff_s = [ x_s , x_{s-1} ] (P, 2R)
22
+
23
+ i.e. a standard Mamba-2 SSD scan on RoPE'd B/C with a rank-2R write that folds
24
+ in the trapezoidal two-tap. SSD is the fusible, backward-known form.
25
+
26
+ STAGE 1 (this file, first): a PyTorch chunked-SSD reference that implements
27
+ exactly the arithmetic the Triton kernel will, and is checked fwd+bwd against
28
+ Mamba3Layer at float64. Only once this matches does the Triton port begin.
29
+ """
30
+ from __future__ import annotations
31
+
32
+ import torch
33
+
34
+ from .scan import rotate_by # rotate (...,N,P) pairs of the N axis by ang (...,N/2)
35
+
36
+
37
+ def ssd_inputs(layer, u: torch.Tensor):
38
+ """Reduce Mamba3Layer's coefficients to the SSD form.
39
+
40
+ Returns alpha (B,T,H), Beff (B,T,H,N,2R), xeff (B,T,H,P,2R), Ctil (B,T,H,N,R).
41
+ All the rotation and the trapezoidal fold happen here, in cheap elementwise
42
+ ops on tensors no larger than (.,.,.,N,2R) - never the (N,P) expanded state.
43
+ """
44
+ alpha, beta, gamma, Bm, Cm, x, theta = layer._coeffs(layer.norm(u))
45
+ Phi = torch.cumsum(theta, dim=1) # (B,T,H,N/2) = Phi_s
46
+ Phi_prev = torch.cat([torch.zeros_like(Phi[:, :1]), Phi[:, :-1]], dim=1) # Phi_{s-1}
47
+ Btil = rotate_by(Bm, -Phi) # R(-Phi_s) B_s (paper gamma term)
48
+ Ctil = rotate_by(Cm, -Phi) # R(-Phi_t) C_t
49
+ # Trapezoidal previous-step write, rotated by R(-Phi_{s-1}) per paper eq (11)
50
+ # (NOT Phi_s): B_{s-1} lands rotated by prod_{i<=s-1} R_i in the global frame.
51
+ Bm_prev = torch.cat([torch.zeros_like(Bm[:, :1]), Bm[:, :-1]], dim=1)
52
+ x_prev = torch.cat([torch.zeros_like(x[:, :1]), x[:, :-1]], dim=1)
53
+ Btil_prev = rotate_by(Bm_prev, -Phi_prev) # R(-Phi_{s-1}) B_{s-1}
54
+ # rank-2R effective write: coefficients ride on B, so w_s = Beff_s xeff_s^T.
55
+ Beff = torch.cat([gamma[..., None, None] * Btil,
56
+ beta[..., None, None] * Btil_prev], dim=-1) # (B,T,H,N,2R)
57
+ xeff = torch.cat([x, x_prev], dim=-1) # (B,T,H,P,2R)
58
+ return alpha, Beff, xeff, Ctil
59
+
60
+
61
+ def ssd_pytorch(alpha, Beff, xeff, Ctil, chunk: int = 64):
62
+ """Chunked SSD forward in PyTorch - the reference the Triton kernel matches.
63
+
64
+ alpha (B,T,H) ; Beff (B,T,H,N,K) ; xeff (B,T,H,P,K) ; Ctil (B,T,H,N,R).
65
+ Returns y (B,T,H,R,P). State g (N,P) is carried per chunk (kept small).
66
+ """
67
+ B, T, H, N, K = Beff.shape
68
+ P = xeff.shape[-2]
69
+ R = Ctil.shape[-1]
70
+ la = torch.log(alpha.clamp_min(1e-30)) # (B,T,H)
71
+ g = torch.zeros(B, H, N, P, dtype=Beff.dtype, device=Beff.device)
72
+ ys = []
73
+ for s0 in range(0, T, chunk):
74
+ e = min(s0 + chunk, T)
75
+ L = e - s0
76
+ Bc = Beff[:, s0:e] # (B,L,H,N,K)
77
+ xc = xeff[:, s0:e] # (B,L,H,P,K)
78
+ Cc = Ctil[:, s0:e] # (B,L,H,N,R)
79
+ cl = torch.cumsum(la[:, s0:e], dim=1) # (B,L,H) = log prod_{s0..t}
80
+ d = torch.exp(cl) # (B,L,H) prod_{s0..t} alpha
81
+ # intra-chunk decay L[t,s] = exp(cl_t - cl_s) for s<=t else 0
82
+ rel = cl.unsqueeze(2) - cl.unsqueeze(1) # (B,L(t),L(s),H)
83
+ # mask the exponent BEFORE exp: above the diagonal rel > 0 can overflow, and where() of an inf
84
+ # still back-propagates NaN into alpha (non-finite grads once a chunk's decay exceeds ~88 nats)
85
+ tril = torch.tril(torch.ones(L, L, dtype=torch.bool, device=Beff.device))
86
+ Ldec = torch.exp(rel.masked_fill(~tril.view(1, L, L, 1), float("-inf")))
87
+ # scores G[t,s] = Ctil_t^T Beff_s over N -> (B,H,L,L,R,K)
88
+ G = torch.einsum('blhnr,bmhnk->bhlmrk', Cc, Bc)
89
+ # intra y_t = sum_{s<=t} Ldec[t,s] G[t,s] xeff_s^T
90
+ y_intra = torch.einsum('blmh,bhlmrk,bmhpk->blhrp', Ldec, G, xc)
91
+ # inter y_t = d_t * (Ctil_t^T g)
92
+ y_inter = torch.einsum('blh,blhnr,bhnp->blhrp', d, Cc, g)
93
+ ys.append(y_intra + y_inter)
94
+ # state update: g <- d_{L-1} g + sum_s (d_{L-1}/d_s) Beff_s xeff_s^T
95
+ coef = torch.exp(cl[:, -1:, :] - cl) # (B,L,H)
96
+ g = d[:, -1].unsqueeze(-1).unsqueeze(-1) * g \
97
+ + torch.einsum('blh,blhnk,blhpk->bhnp', coef, Bc, xc)
98
+ return torch.cat(ys, dim=1) # (B,T,H,R,P)
99
+
100
+
101
+ def ssd_vectorized(alpha, Beff, xeff, Ctil, chunk: int = 64):
102
+ """ssd_pytorch with NO Python loop over chunks - same arithmetic, batched.
103
+
104
+ ssd_pytorch walks the chunks sequentially, firing ~10 kernels per chunk; at T ~ 2000 that loop,
105
+ not the FLOPs, is the cost. Here every chunk's intra-chunk term is one batched einsum, and the
106
+ chunk-to-chunk state pass - g_c = D_{c-1} g_{c-1} + U_{c-1} - is unrolled into a (C x C)
107
+ lower-triangular decay matrix over chunks, the formulation of Mamba-2's `ssd_minimal`. C = T/chunk
108
+ is small (tens), so that matrix is cheap.
109
+
110
+ Exponents are masked to -inf BEFORE exp, never exp-then-where: above the diagonal cl_t - cl_s is
111
+ positive and can overflow, and where() of an inf still back-propagates NaN.
112
+ Same signature and output as ssd_pytorch; verify() checks the two agree fwd+bwd at float64.
113
+ """
114
+ B, T, H, N, K = Beff.shape
115
+ P, R = xeff.shape[-2], Ctil.shape[-1]
116
+ pad = (-T) % chunk
117
+ if pad: # alpha=1 (log 0) and zero writes/readouts: padded steps change nothing
118
+ alpha = torch.cat([alpha, alpha.new_ones(B, pad, H)], 1)
119
+ Beff, xeff, Ctil = (torch.cat([t, t.new_zeros(B, pad, *t.shape[2:])], 1) for t in (Beff, xeff, Ctil))
120
+ C, L = (T + pad) // chunk, chunk
121
+ la = torch.log(alpha.clamp_min(1e-30)).view(B, C, L, H)
122
+ Bc, xc, Cc = Beff.view(B, C, L, H, N, K), xeff.view(B, C, L, H, P, K), Ctil.view(B, C, L, H, N, R)
123
+ cl = torch.cumsum(la, 2) # (B,C,L,H) log decay from chunk start
124
+ tril = torch.tril(torch.ones(L, L, dtype=torch.bool, device=Beff.device)).view(1, 1, L, L, 1)
125
+ Ldec = torch.exp((cl.unsqueeze(3) - cl.unsqueeze(2)).masked_fill(~tril, float("-inf"))) # (B,C,L,L,H)
126
+ G = torch.einsum('bclhnr,bcmhnk->bchlmrk', Cc, Bc)
127
+ y = torch.einsum('bclmh,bchlmrk,bcmhpk->bclhrp', Ldec, G, xc) # intra-chunk
128
+ # each chunk's own contribution to the state at its end, then the state at each chunk's START
129
+ U = torch.einsum('bclh,bclhnk,bclhpk->bchnp', torch.exp(cl[:, :, -1:] - cl), Bc, xc) # (B,C,H,N,P)
130
+ tot = cl[:, :, -1] # (B,C,H) log decay across chunk c
131
+ ct = torch.cumsum(tot, 1)
132
+ # start state of chunk c = sum_{j<c} exp(ct_{c-1} - ct_j) U_j (strictly lower triangular in c, j)
133
+ stril = torch.tril(torch.ones(C, C, dtype=torch.bool, device=Beff.device), -1).view(1, C, C, 1)
134
+ ctm1 = ct - tot # ct_{c-1} (0 for c=0)
135
+ Dc = torch.exp((ctm1.unsqueeze(2) - ct.unsqueeze(1)).masked_fill(~stril, float("-inf"))) # (B,C,C,H)
136
+ g0 = torch.einsum('bcjh,bjhnp->bchnp', Dc, U)
137
+ y = y + torch.einsum('bclh,bclhnr,bchnp->bclhrp', torch.exp(cl), Cc, g0) # inter-chunk
138
+ return y.reshape(B, C * L, H, R, P)[:, :T]
139
+
140
+
141
+ def scan_forward_ssd(layer, u: torch.Tensor, chunk: int = 64, vectorized: bool = True):
142
+ """Drop-in for scan.scan_forward, via the SSD reduction (PyTorch stage).
143
+
144
+ vectorized=True (default) uses ssd_vectorized (no per-chunk Python loop; much faster at long T);
145
+ False runs the chunk loop of ssd_pytorch, kept as the readable reference."""
146
+ Bsz, T, _ = u.shape
147
+ H, R, P = layer.H, layer.R, layer.P
148
+ nu = layer.norm(u)
149
+ alpha, Beff, xeff, Ctil = ssd_inputs(layer, u)
150
+ y = (ssd_vectorized if vectorized else ssd_pytorch)(alpha, Beff, xeff, Ctil, chunk) # (B,T,H,R,P)
151
+ return layer.gate_and_proj(y.reshape(Bsz, T, H * R * P), nu)
152
+
153
+
154
+ def verify(chunks=(8, 16, 64), verbose: bool = True) -> dict:
155
+ """SSD reduction vs the recurrence, fwd + bwd, at float64 - loop and vectorized paths."""
156
+ from .layer import Mamba3Layer
157
+ torch.manual_seed(0)
158
+ torch.set_default_dtype(torch.float64)
159
+ out = {}
160
+ lay = Mamba3Layer(d_model=32, d_state=16, rank=2, head_dim=8).eval()
161
+ u = torch.randn(3, 40, 32)
162
+ u2 = torch.randn(2, 37, 32)
163
+ u3 = torch.cat([u, torch.randn(3, 9, 32) * 50], 1)
164
+ ug = torch.randn(3, 24, 32, requires_grad=True)
165
+ with torch.no_grad():
166
+ ref, ref2 = lay(u), lay(u2)
167
+ lay.zero_grad(); lay(ug).pow(2).sum().backward()
168
+ gu = ug.grad.clone(); gp = {n: p.grad.clone() for n, p in lay.named_parameters()
169
+ if p.grad is not None}
170
+ for vec in (False, True):
171
+ pre = "vec_" if vec else ""
172
+ f = lambda x, c: scan_forward_ssd(lay, x, c, vectorized=vec)
173
+ with torch.no_grad():
174
+ for c in chunks:
175
+ out[f"{pre}chunk{c}"] = float((f(u, c) - ref).abs().max())
176
+ out[f"{pre}ragged"] = float((f(u2, 16) - ref2).abs().max())
177
+ out[f"{pre}causality"] = float((f(u3, 16)[:, :40] - f(u, 16)).abs().max())
178
+ ug.grad = None; lay.zero_grad()
179
+ f(ug, 8).pow(2).sum().backward()
180
+ out[f"{pre}grad_input"] = float((gu - ug.grad).abs().max())
181
+ out[f"{pre}grad_params"] = max(float((gp[n] - p.grad).abs().max())
182
+ for n, p in lay.named_parameters() if p.grad is not None)
183
+ # long sequence with strong decay: the vectorized path's masked exponents must stay finite fwd+bwd
184
+ a = torch.full((1, 3000, 2), 1e-3) # log alpha ~ -6.9 per step
185
+ Bf, xf, Cf = (torch.randn(1, 3000, 2, 4, k, requires_grad=True) for k in (2, 2, 1))
186
+ yv = ssd_vectorized(a, Bf, xf, Cf, 64)
187
+ yv.sum().backward()
188
+ out["vec_long_finite"] = 0.0 if all(torch.isfinite(t).all() for t in (yv, Bf.grad, xf.grad, Cf.grad)) else 1.0
189
+ with torch.no_grad():
190
+ out["vec_long_vs_loop"] = float((yv - ssd_pytorch(a, Bf, xf, Cf, 64)).abs().max())
191
+ torch.set_default_dtype(torch.float32)
192
+ if verbose:
193
+ ok = lambda c: "OK " if c else "FAIL"
194
+ print("SSD reduction vs recurrence (float64, fwd+bwd)")
195
+ for k, v in out.items():
196
+ print(f" {k:<18}{v:.3e} {ok(v < 1e-9)}")
197
+ return out
198
+
199
+
200
+ if __name__ == "__main__":
201
+ verify()
build/lib/steertts/mamba3/tests.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """One runner for every Mamba-3 invariant. Exits non-zero on failure.
2
+
3
+ python -m steertts.mamba3.tests # ~1 min on CPU
4
+ python -m steertts.mamba3.tests --full # adds fp32/bf16 accuracy at T=3000
5
+
6
+ Checks, at float64 unless stated:
7
+ 1. layer selftest: causality, step == forward, rotation orthogonal, trapezoidal two-tap, BCNorm
8
+ 2. chunked scan and SSD reduction vs the recurrence, fwd + bwd (their own verify())
9
+ 3. PAPER FAITHFULNESS against reference.py, which recomputes every coefficient from raw parameters:
10
+ two independent references (complex arithmetic, eq (7) parallel form) agree with each other and
11
+ with every layer path (forward, step, scan, SSD loop, SSD vectorized) over ranks, lengths
12
+ (including T=1 and T not a multiple of the chunk) and chunk sizes
13
+ 4. gradients: torch.autograd.gradcheck wrt the input AND every parameter on each path, and gradient
14
+ agreement of each fast path with the reference
15
+ 5. causality: hard (perturb the future by 1e3, past outputs unchanged) and Jacobian (dy_t/du_s == 0
16
+ for s > t, exactly)
17
+ 6. streaming: forward() in segments carrying (h, S_last) == one forward() over the whole sequence
18
+ 7. strong decay: finite outputs and gradients (incl. wrt alpha) on every scan path in float32
19
+ 8. (--full) float32 and bf16-autocast error vs the float64 reference at T = 100/1000/3000
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import contextlib
25
+ import math
26
+ import sys
27
+
28
+ import torch
29
+
30
+ from .layer import Mamba3Layer, selftest as layer_selftest
31
+ from .reference import ref_complex, ref_parallel
32
+ from .scan import chunked_scan, scan_forward, verify as scan_verify
33
+ from .ssd import scan_forward_ssd, ssd_pytorch, ssd_vectorized, verify as ssd_verify
34
+
35
+
36
+ @contextlib.contextmanager
37
+ def default_dtype(dt):
38
+ """set_default_dtype that is restored even when a check raises (the old runner leaked float64)."""
39
+ old = torch.get_default_dtype()
40
+ torch.set_default_dtype(dt)
41
+ try:
42
+ yield
43
+ finally:
44
+ torch.set_default_dtype(old)
45
+
46
+
47
+ def make(d=16, N=8, R=1, P=8, seed=0, dtype=torch.float64, randomize=True):
48
+ """A layer moved off its symmetric init, so lambda, the biases, the norms and the rotation all matter."""
49
+ torch.manual_seed(seed)
50
+ L = Mamba3Layer(d_model=d, d_state=N, rank=R, head_dim=P).to(dtype).eval()
51
+ if randomize:
52
+ with torch.no_grad():
53
+ L.lam_proj.weight.normal_(0, 0.5); L.lam_proj.bias.normal_(0, 1)
54
+ L.B_bias.normal_(1, 0.5); L.C_bias.normal_(1, 0.5)
55
+ for n in (L.B_norm, L.C_norm, L.norm):
56
+ n.weight.normal_(1, 0.2)
57
+ L.theta_proj.weight.mul_(4) # angles large enough that the rotation is far from identity
58
+ return L
59
+
60
+
61
+ def paths(L, u, chunk):
62
+ """Every way the package can compute a layer's output."""
63
+ with torch.no_grad():
64
+ st = bx = None
65
+ steps = []
66
+ for t in range(u.shape[1]):
67
+ o, st, bx = L.step(u[:, t], st, bx)
68
+ steps.append(o)
69
+ return {"forward": L(u), "step": torch.stack(steps, 1), "scan": scan_forward(L, u, chunk),
70
+ "ssd_loop": scan_forward_ssd(L, u, chunk, vectorized=False),
71
+ "ssd_vec": scan_forward_ssd(L, u, chunk, vectorized=True)}
72
+
73
+
74
+ class _Path(torch.nn.Module):
75
+ def __init__(self, L, path):
76
+ super().__init__(); self.L, self.path = L, path
77
+
78
+ def forward(self, u):
79
+ if self.path == "forward":
80
+ return self.L(u)
81
+ if self.path == "scan":
82
+ return scan_forward(self.L, u, 3)
83
+ return scan_forward_ssd(self.L, u, 3, vectorized=self.path == "ssd_vec")
84
+
85
+
86
+ def run(full: bool) -> bool:
87
+ ok = True
88
+
89
+ def check(name, val, cond):
90
+ nonlocal ok
91
+ ok = ok and bool(cond)
92
+ print(f" [{'PASS' if cond else 'FAIL'}] {name:<62} {val:.2e}")
93
+
94
+ print("== 1. layer selftest ==")
95
+ with default_dtype(torch.float64):
96
+ L = layer_selftest(verbose=False)
97
+ check("causality (future -> past)", L["causality"], L["causality"] < 1e-12)
98
+ check("step() == forward()", L["streaming"], L["streaming"] < 1e-10)
99
+ check("rotation orthogonal", L["orthogonal"], L["orthogonal"] < 1e-10)
100
+ check("trapezoidal two-tap (y4 moves, y2 does not)", L["twotap_y4"],
101
+ L["twotap_y4"] > 1e-6 and L["twotap_y2"] == 0.0)
102
+ check("BCNorm rows unit RMS", L["bcnorm_rms"], L["bcnorm_rms"] < 1e-10)
103
+
104
+ print("== 2. scan / SSD vs recurrence (fwd + bwd) ==")
105
+ with default_dtype(torch.float64):
106
+ for name, res in (("scan", scan_verify(verbose=False)), ("ssd", ssd_verify(verbose=False))):
107
+ for k, v in res.items():
108
+ check(f"{name} {k}", v, v < 1e-9)
109
+
110
+ with default_dtype(torch.float64):
111
+ print("== 3. paper faithfulness: independent references ==")
112
+ worst = {}
113
+ for d, N, R, P in [(16, 8, 1, 8), (16, 8, 2, 8), (24, 4, 3, 8), (8, 2, 1, 4)]:
114
+ lay = make(d, N, R, P, seed=R)
115
+ for T in (1, 2, 5, 16, 37):
116
+ u = torch.randn(2, T, d)
117
+ with torch.no_grad():
118
+ rc, rp = ref_complex(lay, u), ref_parallel(lay, u)
119
+ worst["ref_complex vs ref_parallel"] = max(worst.get("ref_complex vs ref_parallel", 0),
120
+ float((rc - rp).abs().max()))
121
+ for chunk in (1, 4, 16, 64):
122
+ for k, v in paths(lay, u, chunk).items():
123
+ worst[k] = max(worst.get(k, 0.0), float((v - rc).abs().max()))
124
+ for k, v in worst.items():
125
+ check(f"{k} (4 configs x 5 lengths x 4 chunks)", v, v < 1e-11)
126
+
127
+ print("== 4. gradients ==")
128
+ lay = make(8, 4, 2, 4, seed=5)
129
+ names = [n for n, _ in lay.named_parameters()]
130
+ params = tuple(p.detach().clone().requires_grad_(True) for p in lay.parameters())
131
+ u = torch.randn(1, 7, 8, requires_grad=True)
132
+ for path in ("forward", "scan", "ssd_loop", "ssd_vec"):
133
+ mod = _Path(lay, path)
134
+ fn = lambda u, *ps: torch.func.functional_call(mod, {"L." + n: p for n, p in zip(names, ps)}, (u,))
135
+ try:
136
+ good = torch.autograd.gradcheck(fn, (u,) + params, eps=1e-6, atol=1e-6, rtol=1e-5)
137
+ except Exception as ex: # noqa: BLE001 - report, do not crash the runner
138
+ print(" ", str(ex).splitlines()[0][:120]); good = False
139
+ check(f"gradcheck {path} wrt input + all {len(params)} params", 0.0, good)
140
+ lay = make(16, 8, 2, 8, seed=7)
141
+ u, w = torch.randn(2, 37, 16), torch.randn(2, 37, 16)
142
+
143
+ def grads(f):
144
+ lay.zero_grad(); ui = u.clone().requires_grad_(True)
145
+ (f(ui) * w).sum().backward()
146
+ return [ui.grad.clone()] + [p.grad.clone() for p in lay.parameters()]
147
+ gr = grads(lambda v: ref_complex(lay, v))
148
+ for name, f in (("forward", lay), ("scan", lambda v: scan_forward(lay, v, 8)),
149
+ ("ssd_loop", lambda v: scan_forward_ssd(lay, v, 8, vectorized=False)),
150
+ ("ssd_vec", lambda v: scan_forward_ssd(lay, v, 8, vectorized=True))):
151
+ e = max(float((a - b).abs().max()) for a, b in zip(grads(f), gr))
152
+ check(f"grad(input, params) {name} vs ref_complex", e, e < 1e-9)
153
+
154
+ print("== 5. causality ==")
155
+ lay = make(16, 8, 2, 8, seed=3)
156
+ u = torch.randn(2, 40, 16)
157
+ base = paths(lay, u, 16)
158
+ for t0 in (15, 16, 39): # inside a chunk, on a chunk boundary, last step
159
+ u2 = u.clone(); u2[:, t0:] += 1e3 * torch.randn_like(u2[:, t0:])
160
+ for k, v in paths(lay, u2, 16).items():
161
+ e = float((v[:, :t0] - base[k][:, :t0]).abs().max())
162
+ check(f"hard causality {k} t0={t0}", e, e < 1e-9)
163
+ ug = torch.randn(1, 20, 16, requires_grad=True)
164
+ for name, f in (("forward", lay), ("scan", lambda v: scan_forward(lay, v, 8)),
165
+ ("ssd_loop", lambda v: scan_forward_ssd(lay, v, 8, vectorized=False)),
166
+ ("ssd_vec", lambda v: scan_forward_ssd(lay, v, 8, vectorized=True))):
167
+ g, = torch.autograd.grad(f(ug)[0, 9].sum(), ug)
168
+ e = float(g[0, 10:].abs().max())
169
+ check(f"Jacobian causality {name}: |dy_9/du_s>9|", e, e == 0.0)
170
+
171
+ print("== 6. streaming in segments ==")
172
+ lay = make(16, 8, 2, 8, seed=9)
173
+ u = torch.randn(2, 30, 16)
174
+ with torch.no_grad():
175
+ whole = lay(u)
176
+ for cuts in ((17,), (1, 2, 16, 29), tuple(range(1, 30))):
177
+ outs, st, s0 = [], None, 0
178
+ for c in cuts + (30,):
179
+ o, st = lay(u[:, s0:c], state=st, return_state=True)
180
+ outs.append(o); s0 = c
181
+ e = float((torch.cat(outs, 1) - whole).abs().max())
182
+ check(f"forward(state) over segments cut at {cuts if len(cuts) < 5 else 'every step'}", e, e < 1e-10)
183
+
184
+ print("== 7. strong decay: finite fwd + bwd in float32 ==")
185
+ with default_dtype(torch.float32):
186
+ torch.manual_seed(0)
187
+ for la_step in (-1.5, -3.0, -20.0): # per-step log decay; a 64-chunk sums to -96 .. -1280
188
+ al = torch.full((1, 64, 1), math.exp(la_step), requires_grad=True)
189
+ Bf = torch.randn(1, 64, 1, 4, 2, requires_grad=True)
190
+ xf, Cf = torch.randn(1, 64, 1, 2, 2), torch.randn(1, 64, 1, 4, 1)
191
+ for name, fn in (("ssd_loop", ssd_pytorch), ("ssd_vec", ssd_vectorized)):
192
+ al.grad = Bf.grad = None
193
+ y = fn(al, Bf, xf, Cf, 64); y.sum().backward()
194
+ bad = sum(int((~torch.isfinite(t)).sum()) for t in (y, al.grad, Bf.grad))
195
+ check(f"{name} log-decay/step {la_step}: non-finite entries", float(bad), bad == 0)
196
+ al.grad = None
197
+ g, _ = chunked_scan(al, torch.randn(1, 64, 1, 4, 2), 64); g.sum().backward()
198
+ bad = int((~torch.isfinite(al.grad)).sum()) + int((~torch.isfinite(g)).sum())
199
+ check(f"chunked_scan log-decay/step {la_step}: non-finite entries", float(bad), bad == 0)
200
+
201
+ if full:
202
+ print("== 8. float32 / bf16 accuracy vs float64 (d=256, N=32, P=32) ==")
203
+ for T in (100, 1000, 3000):
204
+ with default_dtype(torch.float64):
205
+ L64 = make(256, 32, 1, 32, seed=11, randomize=False)
206
+ u = torch.randn(2, T, 256)
207
+ with torch.no_grad():
208
+ ref = scan_forward_ssd(L64, u)
209
+ L32 = make(256, 32, 1, 32, seed=11, dtype=torch.float32, randomize=False)
210
+ L32.load_state_dict({k: v.float() for k, v in L64.state_dict().items()})
211
+ with torch.no_grad():
212
+ sc = float(ref.abs().max())
213
+ e32 = float((scan_forward_ssd(L32, u.float()).double() - ref).abs().max()) / sc
214
+ with torch.autocast("cpu", dtype=torch.bfloat16):
215
+ ebf = float((scan_forward_ssd(L32, u.float()).double() - ref).abs().max()) / sc
216
+ check(f"float32 relative error T={T}", e32, e32 < 1e-4)
217
+ check(f"bf16-autocast relative error T={T} (informational; BiMamba3 runs fp32)", ebf, ebf < 1e-1)
218
+ return ok
219
+
220
+
221
+ def main() -> int:
222
+ ap = argparse.ArgumentParser()
223
+ ap.add_argument("--full", action="store_true", help="add the slow long-sequence precision checks")
224
+ ok = run(ap.parse_args().full)
225
+ print("\n" + ("ALL PASS" if ok else "FAILURES ABOVE"))
226
+ return 0 if ok else 1
227
+
228
+
229
+ if __name__ == "__main__":
230
+ sys.exit(main())
build/lib/steertts/markup.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Inline markup + sentence-level streaming (recipes/steer_tts/steer/markup.py without the recorded-sound mixer).
2
+
3
+ Markup (nestable; unknown tags are dropped and their text spoken normally):
4
+ *word* or <emph>words</emph> emphasis: pitch +2.5 st, energy +4 dB, 20% longer (per word)
5
+ <slow> <fast> <high> <low> <loud> <soft> per-word duration / pitch / energy changes inside the
6
+ sentence - prosody stays continuous (per word)
7
+ [pause] [pause 300ms] [pause 1.5s] silence (default 400 ms, max 5 s)
8
+ <whisper>..</whisper>, <sadness>..</sadness>, any style tag the model knows
9
+ the span is rendered as its own segment with that tag (span)
10
+ <style prompt="angry and fast">..</style> the span gets the prompt adapter's tag + controls (span)
11
+ [laugh] [cough] [dog] [knock] ... event tokens: the model reserves time for them, but renders no
12
+ sound (the training-time demo mixed recorded sounds in; that mixer
13
+ is not part of this release)
14
+
15
+ Rendering: the text is cut into segments at sentence ends (. ! ?), pauses and style-span boundaries; each segment
16
+ is one forward pass with its tag/controls and per-word shifts, and segments are yielded in order, so a caller can
17
+ play the first sentence while the rest renders (stream()). A style switch mid-sentence joins two separately
18
+ rendered segments (a short audible boundary): there is no token-level style conditioning.
19
+ """
20
+ import re
21
+ from dataclasses import dataclass, field
22
+
23
+ import numpy as np
24
+
25
+ from .config import SR
26
+ from .text import to_phonemes
27
+
28
+ PROSODY = { # Synth.word_shift user units: pitch semitones, energy dB, dur factor
29
+ "emph": dict(pitch=2.5, energy=4.0, dur=1.2), "slow": dict(dur=1.4), "fast": dict(dur=0.75),
30
+ "high": dict(pitch=3.0), "low": dict(pitch=-3.0), "loud": dict(energy=6.0), "soft": dict(energy=-6.0),
31
+ }
32
+ _TOKEN = re.compile(r'(<\s*(/?)\s*([A-Za-z_]+)(?:\s+prompt\s*=\s*"([^"]*)")?\s*>)'
33
+ r'|(\[\s*([A-Za-z_]+)(?:\s+([\d.]+)\s*(ms|s)?)?\s*\])', re.I)
34
+ _SENT_END = re.compile(r"(?<=[.!?])\s+")
35
+
36
+
37
+ @dataclass
38
+ class Segment:
39
+ text: str
40
+ tag: str
41
+ ctrl: dict
42
+ word_shift: dict = field(default_factory=dict)
43
+ pause_ms: int = 0 # silence instead of speech when > 0 (text empty)
44
+ event: str = "" # environmental sound event rendered by the EnvMixer (text empty)
45
+
46
+
47
+ @dataclass
48
+ class Chunk:
49
+ audio: np.ndarray
50
+ segment: Segment
51
+ synth_ms: float
52
+
53
+
54
+ def _n_words(text):
55
+ if not text.strip():
56
+ return 0
57
+ _, widx, _ = to_phonemes(text)
58
+ return max(widx) + 1 if max(widx) >= 0 else 0
59
+
60
+
61
+ class MarkupSynth:
62
+ def __init__(self, tts):
63
+ """tts: a steertts.SteerTTS (its prompt adapter is loaded on first use of a prompt)."""
64
+ self.s = tts
65
+
66
+ # ------------------------------------------------------------------ parsing
67
+ def _style_of_prompt(self, prompt):
68
+ if not prompt:
69
+ return "none", {}
70
+ m = self.s.prompt_mapper(prompt)
71
+ # the adapter's best tag this checkpoint knows (an adapter trained on a newer tag set can rank a tag first
72
+ # that an older checkpoint lacks, e.g. 'irritation')
73
+ tag = next((t for t, _ in m["top_tags"] if t in self.s.tag2i and t != "none"), "none")
74
+ return tag, dict(m["ctrl"])
75
+
76
+ def parse(self, text, prompt=None, tag=None, ctrl=None):
77
+ """-> list[Segment]. prompt/tag/ctrl give the default style for unmarked text."""
78
+ d_tag, d_ctrl = self._style_of_prompt(prompt)
79
+ if tag:
80
+ d_tag = tag if tag in self.s.tag2i else d_tag
81
+ d_ctrl = {**d_ctrl, **(ctrl or {})}
82
+ text = re.sub(r"\*([^*\n]+)\*", r"<emph>\1</emph>", text)
83
+ runs = [] # (text, (tag, ctrl_items), frozenset(prosody)) or ("PAUSE", ms)
84
+ style_stack, pros_stack = [(d_tag, tuple(sorted(d_ctrl.items())))], []
85
+ pos = 0
86
+ for m in _TOKEN.finditer(text):
87
+ if m.start() > pos:
88
+ runs.append((text[pos:m.start()], style_stack[-1], frozenset(pros_stack)))
89
+ pos = m.end()
90
+ if m.group(5): # [pause ...] / [event]
91
+ name = m.group(6).lower()
92
+ if name == "pause":
93
+ v, unit = m.group(7), (m.group(8) or "ms").lower()
94
+ ms = int(float(v) * (1000 if unit == "s" else 1)) if v else 400
95
+ runs.append(("PAUSE", min(ms, 5000)))
96
+ else: # event token (or unknown: dropped by to_phonemes) stays in the TTS text
97
+ runs.append((f" [{name}] ", style_stack[-1], frozenset(pros_stack)))
98
+ continue
99
+ closing, name, prm = bool(m.group(2)), m.group(3).lower(), m.group(4)
100
+ if name in PROSODY:
101
+ if closing:
102
+ if name in pros_stack:
103
+ pros_stack.remove(name)
104
+ else:
105
+ pros_stack.append(name)
106
+ elif name == "style" or name in self.s.tag2i:
107
+ if closing:
108
+ if len(style_stack) > 1:
109
+ style_stack.pop()
110
+ elif name == "style":
111
+ t, c = self._style_of_prompt(prm)
112
+ style_stack.append((t, tuple(sorted(c.items()))))
113
+ else:
114
+ style_stack.append((name, ())) # a named style tag resets the default controls
115
+ # unknown tag: the marker is dropped, its inner text is spoken normally
116
+ if pos < len(text):
117
+ runs.append((text[pos:], style_stack[-1], frozenset(pros_stack)))
118
+
119
+ # split runs at sentence ends so each sentence is its own (streamable) segment
120
+ pieces = []
121
+ for r in runs:
122
+ if r[0] in ("PAUSE", "EVENT"):
123
+ pieces.append(r); continue
124
+ parts = _SENT_END.split(r[0])
125
+ for i, p in enumerate(parts):
126
+ pieces.append((p, r[1], r[2]))
127
+ if i < len(parts) - 1:
128
+ pieces.append(("BREAK",))
129
+ # group consecutive pieces with the same style into segments
130
+ segs, cur, cur_style = [], [], None
131
+
132
+ def flush():
133
+ if cur and "".join(p[0] for p in cur).strip():
134
+ segs.append(self._segment(cur, cur_style))
135
+
136
+ for p in pieces:
137
+ if p[0] in ("PAUSE", "BREAK", "EVENT"):
138
+ flush(); cur, cur_style = [], None
139
+ if p[0] == "PAUSE":
140
+ segs.append(Segment("", "none", {}, pause_ms=p[1]))
141
+ elif p[0] == "EVENT":
142
+ segs.append(Segment("", "none", {}, event=p[1]))
143
+ continue
144
+ if cur_style is not None and p[1] != cur_style:
145
+ flush(); cur = []
146
+ cur_style = p[1]
147
+ cur.append(p)
148
+ flush()
149
+ return segs
150
+
151
+ def _segment(self, pieces, style):
152
+ text, shift, offset = "", {}, 0
153
+ for p_text, _, pros in pieces:
154
+ before = _n_words(text)
155
+ text += p_text
156
+ if pros:
157
+ after = _n_words(text)
158
+ sh = {}
159
+ for name in pros:
160
+ for k, v in PROSODY[name].items():
161
+ sh[k] = sh.get(k, 1.0 if k == "dur" else 0.0) * v if k == "dur" else sh.get(k, 0.0) + v
162
+ for w in range(before, after):
163
+ shift[w] = sh
164
+ tag, ctrl_items = style
165
+ return Segment(re.sub(r"\s+", " ", text).strip(), tag, dict(ctrl_items), shift)
166
+
167
+ # ------------------------------------------------------------------ rendering
168
+ def stream(self, text, voice=None, prompt=None, tag=None, ctrl=None, pace=1.0, ref_wav=None, guidance=None,
169
+ flow=True, seed=0):
170
+ """Yields Chunk per segment, in order (first chunk = first sentence / span)."""
171
+ import time
172
+ fade = int(0.01 * SR)
173
+ for seg in self.parse(text, prompt, tag, ctrl):
174
+ t0 = time.time()
175
+ if seg.pause_ms:
176
+ y = np.zeros(int(seg.pause_ms * SR / 1000), np.float32)
177
+ else:
178
+ y = np.asarray(self.s._render(seg.text, voice, tag=seg.tag, ctrl=seg.ctrl or None, pace=pace,
179
+ word_shift=seg.word_shift or None, ref_wav=ref_wav, guidance=guidance,
180
+ flow=flow, seed=seed), np.float32)
181
+ if len(y) > 2 * fade: # 10 ms fades so concatenated segments never click
182
+ ramp = np.linspace(0, 1, fade, dtype=np.float32)
183
+ y[:fade] *= ramp; y[-fade:] *= ramp[::-1]
184
+ yield Chunk(y, seg, (time.time() - t0) * 1000)
185
+
186
+ def render(self, text, **kw):
187
+ return np.concatenate([c.audio for c in self.stream(text, **kw)] or [np.zeros(1, np.float32)])
build/lib/steertts/measure.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Read the 12 controls back from audio, in the units the model was conditioned on (the training-data measurement,
2
+ recipes/steer_tts/steer/features.py). Needs `praat-parselmouth` (the `measure` extra).
3
+
4
+ from steertts.measure import measure_z
5
+ z = measure_z(wav, text, tts.voice_stats("voice_1361"), tts.pooled_sd) # {dim: speaker-relative z}
6
+
7
+ Raw measurements are converted to speaker-relative z with the voice's neutral mean (control_stats.json:
8
+ spk_mean) and the pooled within-speaker SD. `rate` needs the text (phone count); a few dims can be undefined
9
+ (NaN) on very short or unvoiced clips (f0_* need >= 10 voiced frames, f0_end >= 8 in the last 300 ms).
10
+ """
11
+ import numpy as np
12
+ import parselmouth
13
+ import torch
14
+
15
+ from .audio import mel_extractor
16
+ from .config import CONTROL_DIMS, HOP, N_FFT, SR
17
+ from .text import SP, SYMBOLS, to_phonemes
18
+
19
+ _FREQS = np.fft.rfftfreq(N_FFT, 1 / SR)
20
+ _PUNCT = {i for i, s in enumerate(SYMBOLS) if len(s) == 1}
21
+
22
+
23
+ def stft_mag(y):
24
+ x = torch.from_numpy(y)
25
+ X = torch.stft(x, N_FFT, HOP, N_FFT, torch.hann_window(N_FFT), center=True, return_complex=True)
26
+ return X.abs().numpy() # [513, T]
27
+
28
+
29
+ def pitch_track(y, n_frames, lo=60.0, hi=700.0):
30
+ """Two-pass utterance-adaptive autocorrelation pitch (De Looze & Hirst) sampled at mel frame centres."""
31
+ snd = parselmouth.Sound(y.astype(np.float64), SR)
32
+
33
+ def run(lo, hi):
34
+ p = snd.to_pitch_ac(time_step=HOP / SR, pitch_floor=lo, pitch_ceiling=hi, very_accurate=False)
35
+ f = p.selected_array["frequency"]
36
+ return p.xs(), f
37
+
38
+ xs, f = run(lo, hi)
39
+ v = f[f > 0]
40
+ if len(v) > 20:
41
+ q25, q75 = np.percentile(v, [25, 75])
42
+ lo2, hi2 = max(50.0, 0.75 * q25), min(900.0, 1.75 * q75)
43
+ if hi2 > lo2 + 40:
44
+ xs, f = run(lo2, hi2)
45
+ t = np.arange(n_frames) * HOP / SR
46
+ idx = np.clip(np.searchsorted(xs, t), 1, len(xs) - 1) if len(xs) > 1 else np.zeros(n_frames, int)
47
+ if len(xs) > 1:
48
+ left = idx - 1
49
+ idx = np.where(np.abs(xs[left] - t) < np.abs(xs[idx] - t), left, idx)
50
+ near = np.abs(xs[idx] - t) <= HOP / SR
51
+ f0 = np.where(near, f[idx], 0.0)
52
+ else:
53
+ f0 = np.zeros(n_frames)
54
+ return f0.astype(np.float32), snd
55
+
56
+
57
+ def cpps(P, f_lo=60.0, f_hi=330.0):
58
+ """Smoothed cepstral peak prominence (dB) over the given power-spectrum frames [513, n]:
59
+ real cepstrum of the dB spectrum, peak in the 60-330 Hz quefrency band above a linear
60
+ regression trend, cepstra smoothed over 3 frames x 3 quefrency bins first (Hillenbrand-style)."""
61
+ if P.shape[1] < 3:
62
+ return np.nan
63
+ L = 10 * np.log10(P + 1e-10)
64
+ C = np.fft.irfft(L, axis=0)[: N_FFT // 2] # quefrency in samples
65
+ C = 20 * np.log10(np.abs(C) + 1e-10)
66
+ k = np.ones(3) / 3
67
+ C = np.apply_along_axis(lambda v: np.convolve(v, k, "same"), 1, C) # time smoothing
68
+ C = np.apply_along_axis(lambda v: np.convolve(v, k, "same"), 0, C) # quefrency smoothing
69
+ q = np.arange(C.shape[0]) / SR
70
+ band = (q >= 1 / f_hi) & (q <= 1 / f_lo)
71
+ fit = (q >= 0.001) & (q <= 0.02)
72
+ A = np.vstack([q[fit], np.ones(fit.sum())]).T
73
+ coef, *_ = np.linalg.lstsq(A, C[fit], rcond=None) # [2, n]
74
+ i = np.argmax(C[band], 0)
75
+ qb = q[band][i]
76
+ peak = C[band][i, np.arange(C.shape[1])]
77
+ trend = coef[0] * qb + coef[1]
78
+ return float(np.mean(peak - trend))
79
+
80
+
81
+ def measure(y, n_phones=None, want_mel=True):
82
+ """Returns dict(mel, f0, energy, controls(np.array len 12), speech_mask)."""
83
+ y = y.astype(np.float32)
84
+ out = {}
85
+ if want_mel:
86
+ mel = mel_extractor()(torch.from_numpy(y)[None])[0].numpy() # [100, T]
87
+ out["mel"] = mel
88
+ T = mel.shape[1]
89
+ S = stft_mag(y)
90
+ T = S.shape[1] if not want_mel else min(T, S.shape[1])
91
+ S = S[:, :T]
92
+ P = S ** 2
93
+ energy = 10 * np.log10(P.mean(0) + 1e-10)
94
+ f0, snd = pitch_track(y, T)
95
+ voiced = f0 > 0
96
+ # adaptive VAD: 40% of the way from the noise floor (5th pct) to the speech peak (95th pct)
97
+ lo_e, hi_e = np.percentile(energy, [5, 95])
98
+ speech = energy > lo_e + 0.4 * (hi_e - lo_e)
99
+ out.update(f0=f0, energy=energy.astype(np.float32), speech=speech)
100
+
101
+ c = dict.fromkeys(CONTROL_DIMS, np.nan)
102
+ sp = np.where(speech)[0]
103
+ if len(sp) >= 5:
104
+ a, b = sp[0], sp[-1] + 1
105
+ span = (b - a) * HOP / SR
106
+ inside = np.zeros(T, bool); inside[a:b] = True
107
+ low = inside & ~speech
108
+ # silent runs >= 100 ms inside the speech span count as pauses
109
+ pause_frames, run = 0, 0
110
+ for x in np.r_[low, False]:
111
+ if x:
112
+ run += 1
113
+ else:
114
+ if run * HOP / SR >= 0.1:
115
+ pause_frames += run
116
+ run = 0
117
+ pause_s = pause_frames * HOP / SR
118
+ c["pause"] = pause_s / span
119
+ if n_phones:
120
+ c["rate"] = n_phones / max(span - pause_s, 0.2)
121
+ sf_ = speech & inside
122
+ c["loud"] = float(energy[sf_].mean())
123
+ c["voiced"] = float(voiced[sf_].mean())
124
+ Ps = P[:, sf_]
125
+ lo_band = Ps[(_FREQS >= 50) & (_FREQS < 1000)].sum(0)
126
+ hi_band = Ps[(_FREQS >= 1000) & (_FREQS < 5000)].sum(0)
127
+ c["tilt"] = float(np.mean(10 * np.log10((hi_band + 1e-10) / (lo_band + 1e-10))))
128
+ Pb = Ps[(_FREQS >= 50) & (_FREQS < 8000)] + 1e-10
129
+ c["flat"] = float(np.mean(np.exp(np.log(Pb).mean(0)) / Pb.mean(0)))
130
+ c["flat"] = float(np.log(c["flat"]))
131
+ c["centroid"] = float(np.log((Ps * _FREQS[:, None]).sum(0).sum() / (Ps.sum() + 1e-10) + 1))
132
+ fv = f0[voiced & inside]
133
+ if len(fv) >= 10:
134
+ st = 12 * np.log2(fv / 100.0)
135
+ c["f0_med"] = float(np.median(st))
136
+ q25, q75 = np.percentile(st, [25, 75])
137
+ c["f0_iqr"] = float(q75 - q25)
138
+ tv = np.where(voiced & inside)[0] * HOP / SR
139
+ last = tv > tv[-1] - 0.3
140
+ if last.sum() >= 8:
141
+ c["f0_end"] = float(np.polyfit(tv[last], st[last], 1)[0])
142
+ # H1-H2 from harmonic peaks (uncorrected), voiced speech frames
143
+ Sdb = 10 * np.log10(P + 1e-10)
144
+ vals = []
145
+ for i in np.where(voiced & inside)[0][::2]:
146
+ f = f0[i]
147
+ m1 = (_FREQS > 0.85 * f) & (_FREQS < 1.15 * f)
148
+ m2 = (_FREQS > 1.85 * f) & (_FREQS < 2.15 * f)
149
+ if m1.any() and m2.any():
150
+ vals.append(Sdb[m1, i].max() - Sdb[m2, i].max())
151
+ if vals:
152
+ c["h1h2"] = float(np.median(vals))
153
+ c["cpps"] = cpps(P[:, speech & inside])
154
+ out["controls"] = np.array([c[k] for k in CONTROL_DIMS], np.float32)
155
+ return out
156
+
157
+
158
+ def n_phones(text):
159
+ ids, _, _ = to_phonemes(text)
160
+ return sum(1 for i in ids if i != SP and i not in _PUNCT and not SYMBOLS[i].startswith("["))
161
+
162
+
163
+ def measure_z(wav, text, spk_mean, pooled_sd):
164
+ """-> {dim: z} relative to the voice's neutral speech (spk_mean: {dim: raw}, pooled_sd: {dim: raw SD})."""
165
+ raw = measure(np.asarray(wav, np.float32), n_phones=n_phones(text) if text else None, want_mel=False)["controls"]
166
+ return {k: float((r - spk_mean[k]) / pooled_sd[k]) for k, r in zip(CONTROL_DIMS, raw)}
build/lib/steertts/model.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """SteerTTS acoustic model (inference path).
2
+
3
+ text (ARPAbet) -> encoder -> per-phone duration / pitch / energy predictors -> length regulator -> decoder ->
4
+ 100-band log-mel (Vocos-compatible). Every block is conditioned through adaptive LayerNorm on
5
+ g = MLP([speaker vector, style tag, control z-vector * mask, mask]). Encoder and decoder blocks are "hybrid":
6
+ multi-head self-attention followed by a bidirectional Mamba-3 branch.
7
+
8
+ This is the inference subset of the training code (recipes/steer_tts/steer/model.py): the same modules and
9
+ operations in the same order, so a released checkpoint gives bit-identical mels. Training-only parts (the
10
+ alignment module, losses, torch.compile paths) are left out; the released weights do not contain the aligner.
11
+ """
12
+ import math
13
+
14
+ import torch
15
+ import torch.nn as nn
16
+ import torch.nn.functional as F
17
+
18
+ from .config import CONTROL_DIMS, N_MELS
19
+ from .mamba3 import Mamba3Layer, scan_forward_ssd
20
+ from .text import N_BASE, PAD
21
+
22
+ N_CTRL = len(CONTROL_DIMS)
23
+
24
+
25
+ def seq_mask(lens, max_len=None):
26
+ max_len = max_len or int(lens.max())
27
+ return torch.arange(max_len, device=lens.device)[None] < lens[:, None]
28
+
29
+
30
+ class SinusoidalPos(nn.Module):
31
+ def __init__(self, d):
32
+ super().__init__()
33
+ self.d = d
34
+
35
+ def forward(self, T, device):
36
+ pos = torch.arange(T, device=device, dtype=torch.float32)[:, None]
37
+ div = torch.exp(torch.arange(0, self.d, 2, device=device).float() * (-math.log(10000.0) / self.d))
38
+ pe = torch.zeros(T, self.d, device=device)
39
+ pe[:, 0::2] = torch.sin(pos * div)
40
+ pe[:, 1::2] = torch.cos(pos * div)
41
+ return pe
42
+
43
+
44
+ class AdaLN(nn.Module):
45
+ """LayerNorm whose scale/shift are predicted from the conditioning vector g."""
46
+
47
+ def __init__(self, d, g_dim):
48
+ super().__init__()
49
+ self.ln = nn.LayerNorm(d, elementwise_affine=False)
50
+ self.proj = nn.Linear(g_dim, 2 * d)
51
+
52
+ def forward(self, x, g): # x [B,T,d], g [B,g]
53
+ s, b = self.proj(g)[:, None].chunk(2, -1)
54
+ return self.ln(x) * (1 + s) + b
55
+
56
+
57
+ class BiMamba3(nn.Module):
58
+ """Bidirectional Mamba-3 token mixer: a forward and a backward Mamba3Layer, summed. The backward pass reverses
59
+ each sequence within its own length, so padding never enters the state. Runs the SSD path in fp32."""
60
+
61
+ def __init__(self, d, d_state=32, head_dim=32, rank=1, chunk=64):
62
+ super().__init__()
63
+ self.fwd = Mamba3Layer(d, d_state=d_state, rank=rank, head_dim=head_dim)
64
+ self.bwd = Mamba3Layer(d, d_state=d_state, rank=rank, head_dim=head_dim)
65
+ self.chunk = chunk
66
+
67
+ def forward(self, x, mask): # x [B,T,d], mask [B,T] True=valid
68
+ T = x.shape[1]
69
+ t = torch.arange(T, device=x.device)[None]
70
+ lens = mask.sum(1, keepdim=True)
71
+ rev = torch.where(t < lens, lens - 1 - t, t)[..., None].expand(-1, -1, x.shape[2]) # an involution
72
+ with torch.autocast(x.device.type, enabled=False):
73
+ x = (x * mask[..., None]).float()
74
+ y = scan_forward_ssd(self.fwd, x, self.chunk, vectorized=True)
75
+ y = y + torch.gather(scan_forward_ssd(self.bwd, torch.gather(x, 1, rev), self.chunk, vectorized=True), 1, rev)
76
+ return y * mask[..., None]
77
+
78
+
79
+ class FFTBlock(nn.Module):
80
+ """Pre-norm block: token mixer + conv FFN, all norms adaptive on g.
81
+ mixer: "attn" (self-attention), "mamba3" (bidirectional Mamba-3), "hybrid" (attention, then a Mamba-3 branch)."""
82
+
83
+ def __init__(self, d, heads, ffn, k, g_dim, p=0.1, mixer="attn", ssm_state=32):
84
+ super().__init__()
85
+ assert mixer in ("attn", "mamba3", "hybrid"), mixer
86
+ self.mixer = mixer
87
+ self.n1 = AdaLN(d, g_dim)
88
+ if mixer == "mamba3":
89
+ self.mix = BiMamba3(d, d_state=ssm_state)
90
+ else:
91
+ self.att = nn.MultiheadAttention(d, heads, dropout=p, batch_first=True)
92
+ if mixer == "hybrid":
93
+ self.n_ssm = AdaLN(d, g_dim)
94
+ self.ssm = BiMamba3(d, d_state=ssm_state)
95
+ self.n2 = AdaLN(d, g_dim)
96
+ self.c1 = nn.Conv1d(d, ffn, k, padding=k // 2)
97
+ self.c2 = nn.Conv1d(ffn, d, 1)
98
+ self.drop = nn.Dropout(p)
99
+
100
+ def forward(self, x, mask, g): # mask [B,T] True=valid
101
+ h = self.n1(x, g)
102
+ if self.mixer == "mamba3":
103
+ h = self.mix(h, mask)
104
+ else:
105
+ h, _ = self.att(h, h, h, key_padding_mask=~mask, need_weights=False)
106
+ x = x + self.drop(h)
107
+ if self.mixer == "hybrid":
108
+ x = x + self.drop(self.ssm(self.n_ssm(x, g), mask))
109
+ h = self.n2(x, g).transpose(1, 2)
110
+ h = self.c2(F.gelu(self.c1(h))).transpose(1, 2)
111
+ x = x + self.drop(h)
112
+ return x * mask[..., None]
113
+
114
+
115
+ class Stack(nn.Module):
116
+ def __init__(self, n, d, heads, ffn, k, g_dim, p, mixer="attn", ssm_state=32):
117
+ super().__init__()
118
+ self.pos = SinusoidalPos(d)
119
+ self.layers = nn.ModuleList([FFTBlock(d, heads, ffn, k, g_dim, p, mixer, ssm_state) for _ in range(n)])
120
+ self.out_norm = AdaLN(d, g_dim)
121
+
122
+ def forward(self, x, mask, g):
123
+ x = x + self.pos(x.shape[1], x.device)[None]
124
+ for l in self.layers:
125
+ x = l(x, mask, g)
126
+ return self.out_norm(x, g) * mask[..., None]
127
+
128
+
129
+ class VariancePredictor(nn.Module):
130
+ def __init__(self, d, g_dim, k=3, p=0.15):
131
+ super().__init__()
132
+ self.c1 = nn.Conv1d(d, d, k, padding=k // 2)
133
+ self.n1 = AdaLN(d, g_dim)
134
+ self.c2 = nn.Conv1d(d, d, k, padding=k // 2)
135
+ self.n2 = AdaLN(d, g_dim)
136
+ self.out = nn.Linear(d, 1)
137
+ self.drop = nn.Dropout(p)
138
+
139
+ def forward(self, x, mask, g):
140
+ h = F.relu(self.c1(x.transpose(1, 2))).transpose(1, 2)
141
+ h = self.drop(self.n1(h, g))
142
+ h = F.relu(self.c2(h.transpose(1, 2))).transpose(1, 2)
143
+ h = self.drop(self.n2(h, g))
144
+ return self.out(h).squeeze(-1) * mask
145
+
146
+
147
+ def length_regulate(x, dur, max_len=None):
148
+ """x [B,N,d], dur [B,N] int -> [B,T,d], mel_lens."""
149
+ lens = dur.sum(1)
150
+ T = int(max_len or lens.max())
151
+ idx = torch.cumsum(dur, 1)
152
+ frames = torch.arange(T, device=x.device)[None]
153
+ tok = (frames[:, :, None] >= idx[:, None, :]).sum(-1).clamp(max=x.shape[1] - 1)
154
+ out = torch.gather(x, 1, tok[..., None].expand(-1, -1, x.shape[2]))
155
+ return out * seq_mask(lens, T)[..., None], lens
156
+
157
+
158
+ class SteerTTSModel(nn.Module):
159
+ """Arguments mirror the training class; `arch` in config.json holds the non-default ones."""
160
+
161
+ def __init__(self, n_speakers, n_tags, d=192, heads=2, ffn=384, k=3, enc_layers=4, dec_layers=4, g_dim=128,
162
+ spk_dim=64, tag_dim=32, p=0.1, enc_mixer="attn", dec_mixer="attn", ssm_state=32, spk_enc_dim=0,
163
+ n_symbols=None, n_env=0, ctrl_dims=None, no_mask=False, ctrl_norm="spk", level_norm=True):
164
+ super().__init__()
165
+ assert ctrl_dims is None and not no_mask and ctrl_norm == "spk", "ablation checkpoints are not supported"
166
+ n_symbols = n_symbols or N_BASE
167
+ self.emb = nn.Embedding(n_symbols, d, padding_idx=PAD)
168
+ self.spk = nn.Embedding(n_speakers + 1, spk_dim) # training-speaker table (unused when a speaker vector is given)
169
+ self.spk_proj = nn.Linear(spk_enc_dim, spk_dim) if spk_enc_dim else None # speaker-verification vector -> slot
170
+ self.tag = nn.Embedding(n_tags, tag_dim) # index 0 = 'none'
171
+ self.cond = nn.Sequential(nn.Linear(spk_dim + tag_dim + 2 * N_CTRL, 256), nn.SiLU(), nn.Linear(256, g_dim))
172
+ # acoustic-environment input of the training code; present in the weights but never trained to do anything
173
+ # useful in this release, so the pipeline does not expose it
174
+ self.env_proj = nn.Sequential(nn.Linear(n_env, 128), nn.SiLU(), nn.Linear(128, g_dim)) if n_env else None
175
+ self.encoder = Stack(enc_layers, d, heads, ffn, k, g_dim, p, enc_mixer, ssm_state)
176
+ self.decoder = Stack(dec_layers, d, heads, ffn, k, g_dim, p, dec_mixer, ssm_state)
177
+ self.dur_pred = VariancePredictor(d, g_dim)
178
+ self.pitch_pred = VariancePredictor(d, g_dim)
179
+ self.energy_pred = VariancePredictor(d, g_dim)
180
+ self.pitch_emb = nn.Conv1d(1, d, 3, padding=1)
181
+ self.energy_emb = nn.Conv1d(1, d, 3, padding=1)
182
+ self.to_mel = nn.Linear(d, N_MELS)
183
+
184
+ def condition(self, spk, tag, ctrl, cmask, spk_vec=None, env=None):
185
+ """ctrl [B,12] z-values, cmask [B,12] 1 = this dim is requested, 0 = unspecified (the model picks it)."""
186
+ c = torch.nan_to_num(ctrl) * cmask
187
+ s = self.spk_proj(spk_vec) if (self.spk_proj is not None and spk_vec is not None) else self.spk(spk)
188
+ g = self.cond(torch.cat([s, self.tag(tag), c, cmask], -1))
189
+ if self.env_proj is not None and env is not None:
190
+ g = g + self.env_proj(env)
191
+ return g
192
+
193
+ def encode(self, ids, text_lens, g):
194
+ tmask = seq_mask(text_lens, ids.shape[1])
195
+ e = self.emb(ids)
196
+ return e, self.encoder(e, tmask, g), tmask
197
+
198
+ @torch.no_grad()
199
+ def infer(self, ids, spk, tag, ctrl, cmask, pace=1.0, word_idx=None, word_shift=None, spk_vec=None, env=None,
200
+ guidance=1.0, base=None):
201
+ """ids [1,N]. word_shift: {word_index: dict(pitch=st/PITCH_SCALE, energy=dB/10, dur=factor)}.
202
+ guidance s != 1 with base=(tag0, ctrl0, cmask0): out = out(base) + s * (out(request) - out(base)) on every
203
+ stage (durations first, so both passes share one timeline)."""
204
+ g = self.condition(spk, tag, ctrl, cmask, spk_vec, env)
205
+ g0 = self.condition(spk, *base, spk_vec, env) if (base is not None and guidance != 1.0) else None
206
+ text_lens = torch.tensor([ids.shape[1]], device=ids.device)
207
+ _, h, tmask = self.encode(ids, text_lens, g)
208
+ logdur, pitch, energy = self.dur_pred(h, tmask, g), self.pitch_pred(h, tmask, g), self.energy_pred(h, tmask, g)
209
+ if g0 is not None:
210
+ _, h0, _ = self.encode(ids, text_lens, g0)
211
+ mix = lambda a, a0: a0 + guidance * (a - a0)
212
+ logdur = mix(logdur, self.dur_pred(h0, tmask, g0))
213
+ pitch = mix(pitch, self.pitch_pred(h0, tmask, g0))
214
+ energy = mix(energy, self.energy_pred(h0, tmask, g0))
215
+ dur = (torch.exp(logdur) - 1).clamp_min(0) / pace
216
+ if word_shift and word_idx is not None:
217
+ for w, sh in word_shift.items():
218
+ sel = word_idx[0] == w
219
+ pitch[0, sel] += sh.get("pitch", 0.0)
220
+ energy[0, sel] += sh.get("energy", 0.0)
221
+ dur[0, sel] *= sh.get("dur", 1.0)
222
+ dur = torch.round(dur).long().clamp_min(0)
223
+ dur[:, 0] = dur[:, 0].clamp_min(1)
224
+ pe = self.pitch_emb(pitch[:, None]).transpose(1, 2) + self.energy_emb(energy[:, None]).transpose(1, 2)
225
+ x, lens = length_regulate(h + pe, dur)
226
+ mel = self.to_mel(self.decoder(x, seq_mask(lens, x.shape[1]), g)).transpose(1, 2)
227
+ if g0 is not None:
228
+ x0, _ = length_regulate(h0 + pe, dur)
229
+ mel0 = self.to_mel(self.decoder(x0, seq_mask(lens, x0.shape[1]), g0)).transpose(1, 2)
230
+ mel = mel0 + guidance * (mel - mel0)
231
+ return mel, dict(dur=dur, pitch=pitch, energy=energy, g=g)
build/lib/steertts/pipeline.py ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """SteerTTS inference: a request (text + voice + tag / controls / free-text direction) -> 24 kHz waveform.
2
+
3
+ from steertts import SteerTTS
4
+ tts = SteerTTS.from_pretrained("path/to/steertts-v1") # or an HF repo id; cuda if available
5
+ wav = tts("I can't believe it worked.", voice="voice_1361", tag="happiness", ctrl={"rate": 1.0})
6
+ req = tts.resolve({"text": "Hello there.", "prompt": "soft and sad"}) # the request the prompt maps to
7
+ wav = tts.synthesize(req)
8
+ for chunk in tts.stream("First sentence. <whisper>Second one.</whisper>"): # sentence streaming + markup
9
+ play(chunk.audio)
10
+ wav = tts("Now in your voice.", ref_wav="me.wav") # zero-shot cloning
11
+
12
+ The rendering path is the training repository's steer.synth.Synth, operation for operation (same text front end,
13
+ model, flow sampler and seed, Vocos), so outputs match it bit for bit on the same device.
14
+ """
15
+ import json
16
+ import os
17
+ from pathlib import Path
18
+
19
+ import numpy as np
20
+ import torch
21
+
22
+ from .audio import load_wav, mel_extractor, save_wav
23
+ from .config import CONTROL_DIMS, DEFAULT_VOICE, PITCH_SCALE, SR
24
+ from .model import SteerTTSModel
25
+ from .request import normalize_request
26
+ from .text import to_phonemes
27
+
28
+
29
+ def resolve_model_dir(repo_or_path=None, revision=None, token=None):
30
+ """Local directory, else a Hugging Face repo id (downloaded once into the HF cache). None -> $STEERTTS_MODEL."""
31
+ repo_or_path = repo_or_path or os.environ.get("STEERTTS_MODEL")
32
+ if not repo_or_path:
33
+ raise ValueError("no model given: pass a local model directory or an HF repo id, or set STEERTTS_MODEL")
34
+ if os.path.isdir(repo_or_path):
35
+ return Path(repo_or_path)
36
+ from huggingface_hub import snapshot_download
37
+ return Path(snapshot_download(repo_or_path, revision=revision, token=token))
38
+
39
+
40
+ class SteerTTS:
41
+ sample_rate = SR
42
+ controls = list(CONTROL_DIMS)
43
+
44
+ def __init__(self, model_dir, device=None, flow=True, flow_steps=10, flow_temp=0.4, guidance=1.0):
45
+ """flow: use the flow-matching refiner (and, for cloning, the prompted flow) when the weights are present.
46
+ guidance: default steering scale (1 = plain conditional render; see the model card for the trade-off)."""
47
+ from safetensors.torch import load_file
48
+ d = Path(model_dir)
49
+ self.model_dir = d
50
+ self.dev = device or ("cuda" if torch.cuda.is_available() else "cpu")
51
+ self.cfg = json.load(open(d / "config.json"))
52
+ stats = json.load(open(d / "control_stats.json"))
53
+ self.pooled_sd, self.spk_mean, self.tags = stats["pooled_sd"], stats["spk_mean"], list(stats["tags"])
54
+ self.tag2i = {t: i for i, t in enumerate(self.tags)}
55
+ self.voice_info = {v["name"]: v for v in json.load(open(d / "voices.json"))["voices"]}
56
+ ref = load_file(str(d / self.cfg["files"]["speaker_vectors"]))
57
+ self.voices = list(self.cfg["voices_order"]) if "voices_order" in self.cfg else list(self.voice_info)
58
+ assert ref["vec"].shape[0] == len(self.voices), "speaker_vectors / voices.json mismatch"
59
+ self.spk_ref = {v: ref["vec"][i].numpy() for i, v in enumerate(self.voices)}
60
+ b = self.cfg["acoustic"]
61
+ self.n_speakers = b["n_speakers"]
62
+ self.model = SteerTTSModel(b["n_speakers"], b["n_tags"], **b["arch"]).to(self.dev).eval()
63
+ self.model.load_state_dict(load_file(str(d / self.cfg["files"]["acoustic"]), device=self.dev))
64
+ from vocos import Vocos
65
+ self.vocos = Vocos.from_pretrained(self.cfg["vocoder"]).to(self.dev).eval()
66
+ self.use_flow, self.flow_steps, self.flow_temp, self.guidance = flow, flow_steps, flow_temp, guidance
67
+ self.flow = self._load_flow("refiner") if flow else None
68
+ self._pflow, self._xv, self._pm, self._vec_cache = None, None, None, {}
69
+ self.default_voice = self.cfg.get("default_voice", DEFAULT_VOICE)
70
+
71
+ @classmethod
72
+ def from_pretrained(cls, repo_or_path=None, revision=None, token=None, **kw):
73
+ return cls(resolve_model_dir(repo_or_path, revision, token), **kw)
74
+
75
+ # ------------------------------------------------------------------ lazy parts
76
+ def _load_flow(self, which):
77
+ from safetensors.torch import load_file
78
+ from .flow import Flow
79
+ spec = self.cfg.get(which)
80
+ if not spec or not (self.model_dir / spec["file"]).exists():
81
+ return None
82
+ return Flow.from_state(load_file(str(self.model_dir / spec["file"])), spec["cfg"], self.dev)
83
+
84
+ @property
85
+ def pflow(self):
86
+ if self._pflow is None and self.use_flow:
87
+ self._pflow = self._load_flow("prompted_flow") or False
88
+ return self._pflow or None
89
+
90
+ @property
91
+ def prompt_mapper(self):
92
+ if self._pm is None:
93
+ from safetensors.torch import load_file
94
+ from .prompt import PromptMapper
95
+ p = self.cfg["prompt_adapter"]
96
+ self._pm = PromptMapper(load_file(str(self.model_dir / p["file"])), p["tags"], self.dev)
97
+ return self._pm
98
+
99
+ # ------------------------------------------------------------------ requests
100
+ def map_prompt(self, prompt):
101
+ """Free-text direction -> dict(tag, ctrl, top_tags): the tag used is the adapter's highest-ranked tag that
102
+ this model knows."""
103
+ r = self.prompt_mapper(prompt)
104
+ tag = next((t for t, _ in r["top_tags"] if t in self.tag2i), "none")
105
+ return dict(tag=tag, ctrl=dict(r["ctrl"]), top_tags=[list(t) for t in r["top_tags"]])
106
+
107
+ def resolve(self, request=None, **kw):
108
+ """Validate a request (dict or keyword arguments; see request.schema.json) and return it fully resolved:
109
+ defaults filled in, the free-text `prompt` mapped to `tag` + `ctrl` (explicit `tag` wins, explicit `ctrl`
110
+ dims override the prompt's), and the mapping recorded under `prompt_mapping`. The result is itself a valid
111
+ request and renders identically."""
112
+ req = normalize_request({**(request or {}), **kw}, self)
113
+ if req.get("prompt"):
114
+ m = self.map_prompt(req["prompt"])
115
+ req["prompt_mapping"] = m
116
+ if req.get("tag") is None:
117
+ req["tag"] = m["tag"]
118
+ req["ctrl"] = {**m["ctrl"], **req.get("ctrl", {})}
119
+ del req["prompt"]
120
+ req.setdefault("tag", "none")
121
+ if req["tag"] is None:
122
+ req["tag"] = "none"
123
+ return req
124
+
125
+ def synthesize(self, request=None, **kw):
126
+ """Render a request -> float32 waveform at 24 kHz. Markup requests are rendered segment by segment."""
127
+ req = self.resolve(request, **kw)
128
+ if req["markup"]:
129
+ return self.markup().render(**self._stream_args(req))
130
+ return self._render(req["text"], self._voice(req), req["tag"], req["ctrl"], req["pace"], req.get("word_shift"),
131
+ req.get("ref_wav"), req["guidance"], req["flow"], req["seed"])
132
+
133
+ def __call__(self, text, voice=None, **kw):
134
+ return self.synthesize(dict(kw, text=text, voice=voice) if voice else dict(kw, text=text))
135
+
136
+ say = __call__
137
+
138
+ def stream(self, text=None, request=None, **kw):
139
+ """Sentence-level streaming with inline markup: yields Chunk(audio, segment, synth_ms) in order, the first
140
+ as soon as the first sentence / span is rendered."""
141
+ if text is not None:
142
+ kw["text"] = text
143
+ req = self.resolve(request, **kw)
144
+ return self.markup().stream(**self._stream_args(req))
145
+
146
+ def markup(self):
147
+ from .markup import MarkupSynth
148
+ return MarkupSynth(self)
149
+
150
+ def _voice(self, req):
151
+ return None if req.get("ref_wav") else req["voice"]
152
+
153
+ def _stream_args(self, req):
154
+ return dict(text=req["text"], voice=self._voice(req), tag=req["tag"], ctrl=req["ctrl"], pace=req["pace"],
155
+ ref_wav=req.get("ref_wav"), guidance=req["guidance"], flow=req["flow"], seed=req["seed"])
156
+
157
+ # ------------------------------------------------------------------ rendering (== steer.synth.Synth.__call__)
158
+ @torch.no_grad()
159
+ def speaker_vector(self, voice=None, ref_wav=None):
160
+ """[1,512]: a reference clip's x-vector (path or 24 kHz float array), else the stored vector of a voice."""
161
+ if ref_wav is not None:
162
+ key = str(ref_wav) if isinstance(ref_wav, (str, Path)) else None
163
+ if key is None or key not in self._vec_cache:
164
+ if self._xv is None:
165
+ from .speaker import XVec
166
+ self._xv = XVec(self.dev)
167
+ y, _ = load_wav(ref_wav)
168
+ v = self._xv([y])[0]
169
+ if key is not None:
170
+ self._vec_cache[key] = v
171
+ v = self._vec_cache[key] if key is not None else v
172
+ else:
173
+ if voice not in self.spk_ref:
174
+ raise ValueError(f"unknown voice {voice!r}; see .voices")
175
+ v = self.spk_ref[voice]
176
+ return torch.from_numpy(np.asarray(v, np.float32))[None].to(self.dev)
177
+
178
+ @torch.no_grad()
179
+ def prompt_mel(self, ref_wav, mel, max_s=8.0):
180
+ """Log-mel of the reference clip (first max_s s), level-matched to this render; bands above a band-limited
181
+ clip's bandwidth are filled from the render's own spectral shape."""
182
+ y, sr = load_wav(ref_wav)
183
+ if not isinstance(ref_wav, (str, Path)):
184
+ sr = SR
185
+ y = y[: int(max_s * SR)]
186
+ if len(y) < SR // 2:
187
+ return None
188
+ pm = mel_extractor()(torch.from_numpy(y)[None]).to(self.dev).float()
189
+ mel = mel.float()
190
+ nb = pm.shape[1]
191
+ if sr < SR:
192
+ hz = 700 * (10 ** (np.linspace(0, 2595 * np.log10(1 + (SR / 2) / 700), nb + 2) / 2595) - 1)
193
+ kb = int(np.searchsorted(hz[2:], 0.95 * sr / 2))
194
+ nb = max(8, min(kb, pm.shape[1]))
195
+ loud = lambda m: m[:, :nb].mean(1)[0].sort().values[m.shape[-1] // 2:].mean()
196
+ pm = pm + (loud(mel) - loud(pm))
197
+ if nb < pm.shape[1]:
198
+ shape = mel.mean(-1, keepdim=True)
199
+ pm[:, nb:] = pm[:, nb - 1:nb] + (shape[:, nb:] - shape[:, nb - 1:nb])
200
+ return pm
201
+
202
+ def control_tensors(self, ctrl):
203
+ """Mask semantics: a dim named in ctrl is requested (value + mask 1); every other dim is unspecified (mask 0)
204
+ and the model chooses it from voice, tag and text. ctrl={'rate': 0} is NOT the same as omitting rate: it
205
+ asks for the voice's neutral rate."""
206
+ c = torch.zeros(1, len(CONTROL_DIMS), device=self.dev)
207
+ m = torch.zeros(1, len(CONTROL_DIMS), device=self.dev)
208
+ for k, v in (ctrl or {}).items():
209
+ i = CONTROL_DIMS.index(k)
210
+ c[0, i], m[0, i] = float(v), 1.0
211
+ return c, m
212
+
213
+ @torch.no_grad()
214
+ def _render(self, text, voice=None, tag="none", ctrl=None, pace=1.0, word_shift=None, ref_wav=None,
215
+ guidance=None, flow=True, seed=0, return_mel=False):
216
+ if ref_wav is None and voice is None:
217
+ voice = self.default_voice
218
+ ids, widx, _ = to_phonemes(text)
219
+ if not ids:
220
+ return np.zeros(0, np.float32)
221
+ n_sym = self.model.emb.num_embeddings
222
+ if max(ids) >= n_sym:
223
+ keep = [k for k, i in enumerate(ids) if i < n_sym]
224
+ ids, widx = [ids[k] for k in keep], [widx[k] for k in keep]
225
+ ids_t = torch.tensor([ids], device=self.dev)
226
+ widx_t = torch.tensor([widx], device=self.dev)
227
+ c, m = self.control_tensors(ctrl)
228
+ spk = torch.tensor([self.n_speakers], device=self.dev) # table row: unused, the speaker vector conditions
229
+ spk_vec = self.speaker_vector(voice, ref_wav)
230
+ tg = torch.tensor([self.tag2i.get(tag, 0)], device=self.dev)
231
+ if word_shift: # user units: pitch in semitones, energy in dB, dur factor
232
+ word_shift = {int(w): dict(pitch=s.get("pitch", 0) / PITCH_SCALE, energy=s.get("energy", 0) / 10.0,
233
+ dur=s.get("dur", 1.0)) for w, s in word_shift.items()}
234
+ kw = {}
235
+ s = self.guidance if guidance is None else guidance
236
+ if s != 1.0 and (ctrl or tag not in (None, "none")):
237
+ kw.update(guidance=s, base=(torch.zeros_like(tg), torch.zeros_like(c), torch.zeros_like(m)))
238
+ mel, aux = self.model.infer(ids_t, spk, tg, c, m, pace=pace, word_idx=widx_t, word_shift=word_shift,
239
+ spk_vec=spk_vec, **kw)
240
+ fl = None
241
+ if flow and self.flow is not None:
242
+ fl = self.pflow if (ref_wav is not None and self.pflow is not None) else self.flow
243
+ elif flow and ref_wav is not None:
244
+ fl = self.pflow
245
+ if fl is not None:
246
+ pm = self.prompt_mel(ref_wav, mel) if (ref_wav is not None and getattr(fl.net, "prompt", False)) else None
247
+ if pm is None:
248
+ mel = fl.sample(mel.float(), aux["g"].float(), steps=self.flow_steps, temp=self.flow_temp, seed=seed)
249
+ else:
250
+ P = pm.shape[-1]
251
+ full = torch.cat([pm, mel.float()], -1)
252
+ cm = torch.zeros_like(full[:, :1]); cm[..., :P] = 1
253
+ mel = fl.sample(full, aux["g"].float(), steps=self.flow_steps, temp=self.flow_temp, seed=seed,
254
+ ctx=full, cm=cm, cfg=0.0)[..., P:]
255
+ wav = self.vocos.decode(mel.float()).squeeze().cpu().numpy().astype(np.float32)
256
+ return (wav, mel) if return_mel else wav
257
+
258
+ # ------------------------------------------------------------------ helpers
259
+ def voice_stats(self, voice):
260
+ """The voice's neutral raw control means (for steertts.measure.measure_z)."""
261
+ return self.spk_mean[voice]
262
+
263
+ def save(self, wav, path):
264
+ save_wav(wav, path)
265
+
266
+ def __repr__(self):
267
+ return f"SteerTTS({str(self.model_dir)!r}, device={self.dev!r}, flow={self.flow is not None})"
268
+
269
+
270
+ SteerTTSPipeline = SteerTTS # name used by whissle-sdk <= 0.7
build/lib/steertts/prompt.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Free-text voice direction -> request fields (style tag + which controls + their z values).
2
+
3
+ A frozen sentence encoder (sentence-transformers/all-MiniLM-L6-v2, 22.7M, mean-pooled and L2-normalised exactly
4
+ as the sentence-transformers wrapper does) feeds a ~0.5M-parameter adapter trained on acoustically anchored
5
+ captions: for each control a "specified" probability (> 0.5 -> the dim is set) and a z value, plus a distribution
6
+ over the style tags. Needs `transformers` (the `prompt` extra).
7
+ """
8
+ import torch
9
+ import torch.nn as nn
10
+ import torch.nn.functional as F
11
+
12
+ from .config import CONTROL_DIMS
13
+
14
+ ENC = "sentence-transformers/all-MiniLM-L6-v2"
15
+ N = len(CONTROL_DIMS)
16
+
17
+
18
+ class Adapter(nn.Module):
19
+ def __init__(self, n_tags, d_in=384, h=512):
20
+ super().__init__()
21
+ self.body = nn.Sequential(nn.Linear(d_in, h), nn.GELU(), nn.Dropout(0.1), nn.Linear(h, h), nn.GELU())
22
+ self.tag = nn.Linear(h, n_tags)
23
+ self.spec = nn.Linear(h, N)
24
+ self.val = nn.Linear(h, N)
25
+
26
+ def forward(self, e):
27
+ h = self.body(e)
28
+ return self.tag(h), self.spec(h), self.val(h)
29
+
30
+
31
+ class MiniLM:
32
+ """all-MiniLM-L6-v2 sentence embedding: token embeddings mean-pooled over the attention mask, L2-normalised,
33
+ inputs truncated at 256 word pieces (the model's sentence-transformers configuration)."""
34
+
35
+ def __init__(self, device="cpu"):
36
+ from transformers import AutoModel, AutoTokenizer
37
+ self.tok = AutoTokenizer.from_pretrained(ENC)
38
+ self.m = AutoModel.from_pretrained(ENC).to(device).eval()
39
+ self.dev = device
40
+
41
+ @torch.no_grad()
42
+ def __call__(self, texts):
43
+ b = self.tok(texts, padding=True, truncation=True, max_length=256, return_tensors="pt").to(self.dev)
44
+ h = self.m(**b).last_hidden_state
45
+ m = b["attention_mask"][..., None].to(h.dtype)
46
+ e = (h * m).sum(1) / m.sum(1).clamp(min=1e-9)
47
+ return F.normalize(e, p=2, dim=1)
48
+
49
+
50
+ class PromptMapper:
51
+ def __init__(self, state_dict, tags, device="cpu"):
52
+ self.tags, self.dev = list(tags), device
53
+ self.m = Adapter(len(self.tags)).to(device).eval()
54
+ self.m.load_state_dict(state_dict)
55
+ self.enc = MiniLM(device)
56
+ self.cache = {}
57
+
58
+ @torch.no_grad()
59
+ def __call__(self, prompt, spec_threshold=0.5, top_tags=3):
60
+ """-> dict(top_tags=[(tag, p), ...], ctrl={dim: z}) for the dims whose specified-probability > threshold."""
61
+ key = " ".join(prompt.lower().split())
62
+ if key not in self.cache:
63
+ lt, ls, lv = self.m(self.enc([prompt]))
64
+ self.cache[key] = (torch.softmax(lt, -1)[0], torch.sigmoid(ls)[0], lv[0])
65
+ tp, sp, v = self.cache[key]
66
+ ctrl = {CONTROL_DIMS[i]: round(float(v[i]), 2) for i in range(N) if sp[i] > spec_threshold}
67
+ top = torch.topk(tp, top_tags)
68
+ return dict(top_tags=[(self.tags[i], round(float(p), 3)) for p, i in zip(top.values.tolist(), top.indices.tolist())],
69
+ ctrl=ctrl)
build/lib/steertts/request.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The synthesis request: one JSON object, documented by request.schema.json (shipped inside this package and at the
2
+ model repo root). normalize_request() validates a request against a loaded model and fills in defaults; it raises
3
+ ValueError with a precise message instead of silently ignoring a misspelt field, tag or control.
4
+ """
5
+ import json
6
+ from importlib import resources
7
+
8
+ from .config import CONTROL_DIMS, CONTROL_RANGE
9
+
10
+ DEFAULTS = dict(pace=1.0, guidance=1.0, flow=True, seed=0, markup=False)
11
+ FIELDS = {"text", "voice", "ref_wav", "prompt", "tag", "ctrl", "pace", "guidance", "word_shift", "markup", "flow",
12
+ "seed", "prompt_mapping", "$schema"}
13
+ WORD_SHIFT_KEYS = {"pitch": (-12.0, 12.0), "energy": (-20.0, 20.0), "dur": (0.25, 4.0)}
14
+
15
+
16
+ def schema():
17
+ """The JSON Schema (draft 2020-12) of a request, as a dict."""
18
+ return json.loads(resources.files(__package__).joinpath("request.schema.json").read_text())
19
+
20
+
21
+ def _num(name, v, lo, hi):
22
+ if isinstance(v, bool) or not isinstance(v, (int, float)):
23
+ raise ValueError(f"{name} must be a number, got {v!r}")
24
+ if not lo <= float(v) <= hi:
25
+ raise ValueError(f"{name}={v} is outside [{lo}, {hi}]")
26
+ return float(v)
27
+
28
+
29
+ def normalize_request(req, tts=None):
30
+ """-> a new dict with defaults filled in. tts (a SteerTTS) enables voice / tag checks."""
31
+ if not isinstance(req, dict):
32
+ raise ValueError("a request is a JSON object")
33
+ unknown = set(req) - FIELDS
34
+ if unknown:
35
+ raise ValueError(f"unknown request field(s) {sorted(unknown)}; see request.schema.json")
36
+ out = {k: v for k, v in req.items() if k != "$schema"}
37
+ text = out.get("text")
38
+ if not isinstance(text, str) or not text.strip():
39
+ raise ValueError("text must be a non-empty string")
40
+ if len(text) > 5000:
41
+ raise ValueError("text is longer than 5000 characters; split it (or stream it sentence by sentence)")
42
+ if out.get("ref_wav") is not None:
43
+ if out.get("voice"):
44
+ raise ValueError("give either voice (a built-in voice) or ref_wav (clone a reference clip), not both")
45
+ else:
46
+ out.pop("ref_wav", None)
47
+ out["voice"] = out.get("voice") or (tts.default_voice if tts is not None else None)
48
+ if tts is not None and out["voice"] not in tts.spk_ref:
49
+ raise ValueError(f"unknown voice {out['voice']!r}; list them with tts.voices / `steertts voices`")
50
+ if out.get("prompt") is not None and not isinstance(out["prompt"], str):
51
+ raise ValueError("prompt must be a string")
52
+ if out.get("tag") is not None:
53
+ if not isinstance(out["tag"], str):
54
+ raise ValueError("tag must be a string")
55
+ if tts is not None and out["tag"] not in tts.tag2i:
56
+ raise ValueError(f"unknown tag {out['tag']!r}; known: {', '.join(tts.tags)}")
57
+ ctrl = out.get("ctrl") or {}
58
+ if not isinstance(ctrl, dict):
59
+ raise ValueError("ctrl must be an object {control: z}")
60
+ for k in ctrl:
61
+ if k not in CONTROL_DIMS:
62
+ raise ValueError(f"unknown control {k!r}; known: {', '.join(CONTROL_DIMS)}")
63
+ out["ctrl"] = {k: _num(f"ctrl.{k}", v, *CONTROL_RANGE) for k, v in ctrl.items()}
64
+ defaults = dict(DEFAULTS)
65
+ if tts is not None: # the loaded model's own defaults (SteerTTS(guidance=..., flow=...))
66
+ defaults.update(guidance=tts.guidance, flow=bool(tts.use_flow))
67
+ for k, v in defaults.items():
68
+ if out.get(k) is None:
69
+ out[k] = v
70
+ out["pace"] = _num("pace", out["pace"], 0.25, 4.0)
71
+ out["guidance"] = _num("guidance", out["guidance"], 0.0, 4.0)
72
+ for k in ("flow", "markup"):
73
+ if not isinstance(out[k], bool):
74
+ raise ValueError(f"{k} must be true or false")
75
+ if isinstance(out["seed"], bool) or not isinstance(out["seed"], int) or out["seed"] < 0:
76
+ raise ValueError("seed must be a non-negative integer")
77
+ ws = out.get("word_shift")
78
+ if ws:
79
+ if not isinstance(ws, dict):
80
+ raise ValueError("word_shift must be an object {word_index: {pitch, energy, dur}}")
81
+ if out["markup"]:
82
+ raise ValueError("word_shift and markup cannot be combined (markup re-indexes words per segment)")
83
+ norm = {}
84
+ for w, sh in ws.items():
85
+ try:
86
+ wi = int(w)
87
+ except (TypeError, ValueError):
88
+ raise ValueError(f"word_shift key {w!r} is not a word index") from None
89
+ if wi < 0 or not isinstance(sh, dict) or set(sh) - set(WORD_SHIFT_KEYS):
90
+ raise ValueError(f"word_shift[{w!r}] must be a non-negative index -> {{pitch, energy, dur}}")
91
+ norm[str(wi)] = {k: _num(f"word_shift.{w}.{k}", v, *WORD_SHIFT_KEYS[k]) for k, v in sh.items()}
92
+ out["word_shift"] = norm
93
+ else:
94
+ out.pop("word_shift", None)
95
+ return out
build/lib/steertts/request.schema.json ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:steertts:request-schema:v1",
4
+ "title": "SteerTTS synthesis request",
5
+ "description": "One request = one rendered waveform (24 kHz mono float32). Every field except `text` is optional. Controls are SPEAKER-RELATIVE z-scores: 0 = the chosen voice's own neutral speech, +-1 = one pooled within-speaker standard deviation (raw units per dim: control_stats.json pooled_sd). Mask semantics: a control named in `ctrl` is requested (value + availability mask 1); every control NOT named is unspecified (mask 0) and the model chooses it from voice, tag and text. `{\"rate\": 0}` therefore asks for the voice's neutral rate, while omitting `rate` lets the tag/text decide. SteerTTS.resolve(request) returns the fully resolved request (defaults filled, `prompt` mapped to `tag` + `ctrl`, recorded in `prompt_mapping`), which is itself a valid request.",
6
+ "type": "object",
7
+ "required": [
8
+ "text"
9
+ ],
10
+ "additionalProperties": false,
11
+ "properties": {
12
+ "$schema": {
13
+ "type": "string"
14
+ },
15
+ "text": {
16
+ "type": "string",
17
+ "minLength": 1,
18
+ "maxLength": 5000,
19
+ "description": "English text. Numbers, units, currency, ordinals, ranges and unknown acronyms are spelled out by the front end. With `markup: true` it may contain inline markup (see `markup`). Event tokens such as [laugh] or [dog] are accepted: the model reserves a short stretch of time for them but renders no sound."
20
+ },
21
+ "voice": {
22
+ "type": "string",
23
+ "pattern": "^voice_[0-9]{4}$",
24
+ "description": "Built-in voice (anonymised public name, see voices.json). Default: the model's default_voice (voice_1361). Mutually exclusive with ref_wav."
25
+ },
26
+ "ref_wav": {
27
+ "type": "string",
28
+ "description": "Path to a reference recording (any sample rate, first 8 s used, >= 0.5 s) for zero-shot cloning: the voice is taken from its WavLM speaker vector and, when flow is on, the prompted flow uses the clip's own mel as context. Mutually exclusive with voice. Only clone voices whose owners consented."
29
+ },
30
+ "prompt": {
31
+ "type": "string",
32
+ "description": "Free-text voice direction ('soft and sad', 'excited, a bit faster'). Mapped by the prompt adapter to a style tag and a subset of controls. An explicit `tag` overrides the prompt's tag; explicit `ctrl` dims override the prompt's values for those dims."
33
+ },
34
+ "tag": {
35
+ "type": "string",
36
+ "enum": [
37
+ "none",
38
+ "adoration",
39
+ "amazement",
40
+ "amusement",
41
+ "anger",
42
+ "anxiety",
43
+ "apologetic",
44
+ "bored",
45
+ "confidence",
46
+ "confusion",
47
+ "contempt",
48
+ "contentment",
49
+ "cuteness",
50
+ "desire",
51
+ "determined",
52
+ "disappointment",
53
+ "disgust",
54
+ "distress",
55
+ "ecstasy",
56
+ "embarrassment",
57
+ "emphasis",
58
+ "enunciated",
59
+ "excited",
60
+ "fast",
61
+ "fear",
62
+ "frustration",
63
+ "guilt",
64
+ "happiness",
65
+ "hesitant",
66
+ "high_pitch",
67
+ "interest",
68
+ "irritation",
69
+ "laughing",
70
+ "loud",
71
+ "low_pitch",
72
+ "mysterious",
73
+ "neutral",
74
+ "nostalgia",
75
+ "pain",
76
+ "panic",
77
+ "pride",
78
+ "realization",
79
+ "relief",
80
+ "sadness",
81
+ "sarcasm",
82
+ "serenity",
83
+ "skeptical",
84
+ "slow",
85
+ "sympathy",
86
+ "tired",
87
+ "whisper"
88
+ ],
89
+ "default": "none",
90
+ "description": "Style tag; 'none' = unspecified. Tags act as style switches: voice-quality tags such as whisper change the voice strongly, but an independent emotion classifier recognises only sadness among the emotion tags, and about half of the tags are acoustically close to neutral (see the model card). For a specific acoustic change prefer ctrl."
91
+ },
92
+ "ctrl": {
93
+ "type": "object",
94
+ "additionalProperties": false,
95
+ "properties": {
96
+ "rate": {
97
+ "type": "number",
98
+ "minimum": -5,
99
+ "maximum": 5,
100
+ "description": "articulation rate (phones per second of speech, pauses excluded). + faster"
101
+ },
102
+ "loud": {
103
+ "type": "number",
104
+ "minimum": -5,
105
+ "maximum": 5,
106
+ "description": "mean speech level (dB). + louder"
107
+ },
108
+ "f0_med": {
109
+ "type": "number",
110
+ "minimum": -5,
111
+ "maximum": 5,
112
+ "description": "median pitch (semitones). + higher"
113
+ },
114
+ "f0_iqr": {
115
+ "type": "number",
116
+ "minimum": -5,
117
+ "maximum": 5,
118
+ "description": "pitch range, interquartile (semitones). + livelier, - monotone"
119
+ },
120
+ "f0_end": {
121
+ "type": "number",
122
+ "minimum": -5,
123
+ "maximum": 5,
124
+ "description": "pitch slope over the last 300 ms of voicing (st/s). + rising (question-like), - falling"
125
+ },
126
+ "pause": {
127
+ "type": "number",
128
+ "minimum": -5,
129
+ "maximum": 5,
130
+ "description": "share of the speech span spent in silent pauses >= 100 ms. + more pauses"
131
+ },
132
+ "tilt": {
133
+ "type": "number",
134
+ "minimum": -5,
135
+ "maximum": 5,
136
+ "description": "spectral tilt, energy 1-5 kHz vs 50 Hz-1 kHz (dB). + brighter / pressed"
137
+ },
138
+ "voiced": {
139
+ "type": "number",
140
+ "minimum": -5,
141
+ "maximum": 5,
142
+ "description": "voiced share of speech frames. - whispery"
143
+ },
144
+ "flat": {
145
+ "type": "number",
146
+ "minimum": -5,
147
+ "maximum": 5,
148
+ "description": "spectral flatness (log). + breathier / noisier"
149
+ },
150
+ "centroid": {
151
+ "type": "number",
152
+ "minimum": -5,
153
+ "maximum": 5,
154
+ "description": "spectral centroid (log Hz). + brighter"
155
+ },
156
+ "cpps": {
157
+ "type": "number",
158
+ "minimum": -5,
159
+ "maximum": 5,
160
+ "description": "cepstral peak prominence (dB), periodicity. - breathier / rougher"
161
+ },
162
+ "h1h2": {
163
+ "type": "number",
164
+ "minimum": -5,
165
+ "maximum": 5,
166
+ "description": "H1-H2 (dB). + breathier"
167
+ }
168
+ },
169
+ "default": {},
170
+ "description": "Requested controls {dim: z}. Values beyond +-3 are outside most training data."
171
+ },
172
+ "pace": {
173
+ "type": "number",
174
+ "minimum": 0.25,
175
+ "maximum": 4.0,
176
+ "default": 1.0,
177
+ "description": "Global duration divisor applied after prediction (>1 faster). Unlike ctrl.rate it does not change the style, only the timing."
178
+ },
179
+ "guidance": {
180
+ "type": "number",
181
+ "minimum": 0.0,
182
+ "maximum": 4.0,
183
+ "default": 1.0,
184
+ "description": "Steering scale s: out = out(no request) + s * (out(request) - out(no request)), on durations, pitch, energy and mel. s > 1 strengthens the requested change (control labels are noisy, so the learned effect is attenuated) at the cost of more leakage into other dims and some quality; only applied when a tag or ctrl is given."
185
+ },
186
+ "word_shift": {
187
+ "type": "object",
188
+ "description": "Per-word prosody edits {word_index: {pitch: semitones, energy: dB, dur: factor}} (word_index counts words of the normalised text from 0). Not combinable with markup (use *emph*, <slow> etc. there).",
189
+ "patternProperties": {
190
+ "^[0-9]+$": {
191
+ "type": "object",
192
+ "additionalProperties": false,
193
+ "properties": {
194
+ "pitch": {
195
+ "type": "number",
196
+ "minimum": -12,
197
+ "maximum": 12
198
+ },
199
+ "energy": {
200
+ "type": "number",
201
+ "minimum": -20,
202
+ "maximum": 20
203
+ },
204
+ "dur": {
205
+ "type": "number",
206
+ "minimum": 0.25,
207
+ "maximum": 4
208
+ }
209
+ }
210
+ }
211
+ },
212
+ "additionalProperties": false
213
+ },
214
+ "markup": {
215
+ "type": "boolean",
216
+ "default": false,
217
+ "description": "Parse inline markup in text: *emphasis* / <emph>..</emph>; per-word <slow> <fast> <high> <low> <loud> <soft>; [pause], [pause 300ms], [pause 1.5s]; span styles <whisper>..</whisper> (any tag) and <style prompt=\"...\">..</style>. The text is rendered sentence by sentence (and per span), joined with 10 ms fades. stream() always parses markup."
218
+ },
219
+ "flow": {
220
+ "type": "boolean",
221
+ "default": true,
222
+ "description": "Refine the mel with the flow-matching decoder (refiner for built-in voices, prompted flow for ref_wav). false = the acoustic model's mel straight into Vocos (faster, slightly hazier)."
223
+ },
224
+ "seed": {
225
+ "type": "integer",
226
+ "minimum": 0,
227
+ "default": 0,
228
+ "description": "Noise seed of the flow sampler (renders are deterministic per device for a given seed)."
229
+ },
230
+ "prompt_mapping": {
231
+ "type": "object",
232
+ "description": "Written by resolve(): what `prompt` mapped to {tag, ctrl, top_tags}. Informational; ignored on input.",
233
+ "properties": {
234
+ "tag": {
235
+ "type": "string"
236
+ },
237
+ "ctrl": {
238
+ "type": "object"
239
+ },
240
+ "top_tags": {
241
+ "type": "array"
242
+ }
243
+ }
244
+ }
245
+ },
246
+ "examples": [
247
+ {
248
+ "text": "The meeting has been moved to Thursday afternoon at three.",
249
+ "voice": "voice_1361"
250
+ },
251
+ {
252
+ "text": "I can't believe you did that!",
253
+ "voice": "voice_1361",
254
+ "tag": "anger",
255
+ "ctrl": {
256
+ "loud": 1.5,
257
+ "f0_med": 1.0
258
+ }
259
+ },
260
+ {
261
+ "text": "Are you serious?",
262
+ "ctrl": {
263
+ "f0_end": 2.0,
264
+ "f0_iqr": 1.5
265
+ }
266
+ },
267
+ {
268
+ "text": "Please keep this between us.",
269
+ "prompt": "whispering, slow and secretive"
270
+ },
271
+ {
272
+ "text": "Well, *that* was fast. [pause 400ms] <slow>Let me think.</slow>",
273
+ "markup": true
274
+ },
275
+ {
276
+ "text": "Now in a cloned voice.",
277
+ "ref_wav": "reference.wav"
278
+ }
279
+ ]
280
+ }
build/lib/steertts/speaker.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Speaker vectors for zero-shot cloning: frozen WavLM-base-plus-sv x-vectors (L2-normalised, 512-d), the same
2
+ encoder the acoustic model was trained with. Needs `transformers` (the `clone` extra)."""
3
+ import numpy as np
4
+ import torch
5
+
6
+ from .config import SR
7
+
8
+ MODEL = "microsoft/wavlm-base-plus-sv"
9
+ DIM = 512
10
+
11
+
12
+ class XVec:
13
+ def __init__(self, dev="cpu"):
14
+ from transformers import AutoFeatureExtractor, WavLMForXVector
15
+ self.fe = AutoFeatureExtractor.from_pretrained(MODEL)
16
+ self.m = WavLMForXVector.from_pretrained(MODEL).to(dev).eval()
17
+ self.dev = dev
18
+
19
+ @torch.no_grad()
20
+ def __call__(self, wavs24, max_s=8.0):
21
+ """list of float32 arrays at 24 kHz -> [n, 512] L2-normalised (first max_s seconds of each)."""
22
+ import soxr
23
+ ys = [soxr.resample(w[: int(max_s * SR)].astype(np.float32), SR, 16000) for w in wavs24]
24
+ ys = [np.tile(y, int(np.ceil(8000 / max(len(y), 1)))) if len(y) < 8000 else y for y in ys]
25
+ x = self.fe(ys, sampling_rate=16000, return_tensors="pt", padding=True).to(self.dev)
26
+ e = self.m(**x).embeddings
27
+ return torch.nn.functional.normalize(e, dim=-1).float().cpu().numpy()
build/lib/steertts/text.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Text -> ARPAbet phoneme ids with word indices (g2p_en). Identical to the training front end (steer/text.py)."""
2
+ import re
3
+ from functools import lru_cache
4
+
5
+ _PUNCT = list(",.?!;:-")
6
+ _ARPA = ["AA", "AE", "AH", "AO", "AW", "AY", "B", "CH", "D", "DH", "EH", "ER", "EY", "F", "G", "HH", "IH", "IY",
7
+ "JH", "K", "L", "M", "N", "NG", "OW", "OY", "P", "R", "S", "SH", "T", "TH", "UH", "UW", "V", "W", "Y", "Z", "ZH"]
8
+ _VOWELS = {"AA", "AE", "AH", "AO", "AW", "AY", "EH", "ER", "EY", "IH", "IY", "OW", "OY", "UH", "UW"}
9
+ SYMBOLS = ["<pad>", "<sp>"] + _PUNCT + [p + s for p in _ARPA for s in (["0", "1", "2"] if p in _VOWELS else [""])]
10
+ N_BASE = len(SYMBOLS) # vocabulary of every checkpoint trained before sound events (2026-10-09)
11
+ # sound-event tokens, written inline as [laugh], [dog] ... : in-voice nonverbals first, then environmental events,
12
+ # then reserved slots so the vocabulary can grow without resizing the embedding again. Appended after the phonemes,
13
+ # so ids < N_BASE are unchanged and older checkpoints keep working (Synth drops tokens a checkpoint lacks).
14
+ NONVERBAL = ["laugh", "cry", "scream", "yell", "cheer", "cough", "sneeze", "yawn", "throat", "breath", "sigh", "sniff"]
15
+ EVENTS = ["dog", "cat", "bird", "knock", "door", "phone", "bell", "horn", "applause", "glass", "siren", "footsteps",
16
+ "typing", "thunder", "clock"]
17
+ EVENT_TOKENS = NONVERBAL + EVENTS + [f"reserved{i}" for i in range(13)]
18
+ SYMBOLS = SYMBOLS + [f"[{e}]" for e in EVENT_TOKENS]
19
+ SYM2ID = {s: i for i, s in enumerate(SYMBOLS)}
20
+ PAD, SP = 0, 1
21
+ _EVENT_RE = re.compile(r"\[\s*([A-Za-z_]+)\s*\]")
22
+
23
+
24
+ def _ensure_nltk():
25
+ """g2p_en needs NLTK's POS tagger + CMUdict. nltk>=3.9 looks the tagger up under its '_eng' name, which
26
+ g2p_en's own first-run download does not fetch; get whichever is missing once (into ~/nltk_data)."""
27
+ import nltk
28
+ for res, name in (("taggers/averaged_perceptron_tagger_eng", "averaged_perceptron_tagger_eng"),
29
+ ("taggers/averaged_perceptron_tagger", "averaged_perceptron_tagger"),
30
+ ("corpora/cmudict", "cmudict")):
31
+ try:
32
+ nltk.data.find(res)
33
+ except LookupError:
34
+ nltk.download(name, quiet=True)
35
+
36
+
37
+ @lru_cache(maxsize=1)
38
+ def _g2p():
39
+ _ensure_nltk()
40
+ from g2p_en import G2p
41
+ return G2p()
42
+
43
+
44
+ _UNITS = {"km/h": "kilometers per hour", "km/hr": "kilometers per hour", "m/s": "meters per second", "mph": "miles per hour",
45
+ "km": "kilometers", "kg": "kilograms", "cm": "centimeters", "mm": "millimeters", "ft": "feet", "lbs": "pounds",
46
+ "lb": "pounds", "kHz": "kilohertz", "Hz": "hertz", "GB": "gigabytes", "MB": "megabytes"}
47
+ _CURRENCY = {"$": "dollars", "£": "pounds", "€": "euros"}
48
+
49
+
50
+ def _num(s, to="cardinal"):
51
+ from num2words import num2words
52
+ return num2words(s, to=to).replace("-", " ").replace(",", "")
53
+
54
+
55
+ def _expand(text: str) -> str:
56
+ """Spell out what g2p_en gets wrong (CV3-Eval en, 2026-10-09: 0.6 + 0.6 WER points of 3.3 came from these).
57
+ g2p_en looks words up in CMUdict and sends anything else to a neural guesser - including every hyphenated
58
+ compound ("pay-per-view" -> P EY V ER P IY, "low-tech" -> L OW T CH EH) and "4x4" / "Il-76" / "km/h". It only
59
+ expands bare integers, reading "2,243" as "two" + "two hundred forty three"."""
60
+ text = re.sub(r"(\d)\s*[-–]\s*(\d)", r"\1 to \2", text) # ranges: 1990-1995, 10-20
61
+ text = re.sub(r"(\d+)\s*[x×]\s*(\d+)", r"\1 by \2", text) # 4x4, 1920x1080
62
+ text = re.sub(r"(?<=[A-Za-z0-9])[-‐/](?=[A-Za-z0-9])", " ", text) # compounds: pay-per-view, Il-76, and/or
63
+ # the amount ends on a digit (a trailing comma is punctuation), and the space after it is only consumed together
64
+ # with a following "million/billion/thousand" (2026-10-11: "$3,000, 20%" read "three thousand, dollars20")
65
+ text = re.sub(r"([$£€])\s?(\d(?:[\d,]*\d)?(?:\.\d+)?)(?:\s*(million|billion|thousand)\b)?",
66
+ lambda m: f"{m.group(2)} {m.group(3) + ' ' if m.group(3) else ''}{_CURRENCY[m.group(1)]}", text)
67
+ text = re.sub(r"(\d[\d,]*(?:\.\d+)?)\s?%", r"\1 percent", text)
68
+ for u in sorted(_UNITS, key=len, reverse=True): # 165 km h (after the / split) / 165 km/h
69
+ pat = re.escape(u).replace("/", "[ /]")
70
+ text = re.sub(rf"(\d)\s?{pat}\b", rf"\1 {_UNITS[u]}", text)
71
+ text = re.sub(r"\b(\d+)(st|nd|rd|th)\b", lambda m: _num(int(m.group(1)), "ordinal"), text)
72
+ text = re.sub(r"\b\d{1,3}(?:,\d{3})+(?:\.\d+)?\b", lambda m: _num(float(m.group(0).replace(",", "")))
73
+ if "." in m.group(0) else _num(int(m.group(0).replace(",", ""))), text)
74
+ text = re.sub(r"\b\d+\.\d+\b", lambda m: _num(float(m.group(0))), text)
75
+ text = re.sub(r"\b\d{5,}\b", lambda m: _num(int(m.group(0))), text) # g2p_en reads long digit runs badly
76
+ cmu = _g2p().cmu # acronyms CMUdict does not know (IOC, BBC): letter by letter; NASA, UNESCO keep their entry
77
+ text = re.sub(r"\b[A-Z]{2,5}s?\b", lambda m: m.group(0) if m.group(0).lower() in cmu
78
+ else " ".join(m.group(0).rstrip("s")) + ("'s" if m.group(0).endswith("s") and len(m.group(0)) > 2 else ""), text)
79
+ return text
80
+
81
+
82
+ def normalize(text: str) -> str:
83
+ text = re.sub(r"[\"“”‘’`]", lambda m: "'" if m.group(0) in "‘’`" else "", text)
84
+ text = _expand(text) # first: it reads digit-dash-digit as a range before dashes become pauses
85
+ text = text.replace("—", ", ").replace("–", ", ").replace("…", ". ")
86
+ text = re.sub(r"\s+", " ", text).strip()
87
+ return text
88
+
89
+
90
+ def to_phonemes(text: str):
91
+ """Returns (ids, word_idx, words). Word boundaries become <sp> tokens with word_idx -1;
92
+ punctuation tokens get word_idx -1 as well. A trailing <sp> pads the end. Inline sound events ([laugh], [dog])
93
+ become one token each, between <sp> tokens, with word_idx -1; unknown [...] markers are dropped."""
94
+ text = normalize(text)
95
+ if "[" in text:
96
+ parts = _EVENT_RE.split(text) # text, name, text, name, ...
97
+ if len(parts) > 1:
98
+ ids, widx, words, nw = [SP], [-1], [], 0
99
+ for k, p in enumerate(parts):
100
+ if k % 2:
101
+ tok = f"[{p.lower()}]"
102
+ if tok in SYM2ID:
103
+ ids += [SYM2ID[tok], SP]; widx += [-1, -1]
104
+ elif p.strip(" []"):
105
+ i2, w2, wd = to_phonemes(p.replace("[", " ").replace("]", " "))
106
+ ids += i2[1:]; widx += [w + nw if w >= 0 else -1 for w in w2[1:]]
107
+ words += wd; nw = max([w for w in widx if w >= 0], default=-1) + 1
108
+ return ids, widx, words
109
+ raw = _g2p()(text)
110
+ ids, widx, words = [SP], [-1], []
111
+ cur, wi = [], -1
112
+ for tok in raw + [" "]:
113
+ if tok == " ":
114
+ if cur:
115
+ wi += 1
116
+ ids += [SYM2ID[p] for p in cur]
117
+ widx += [wi] * len(cur)
118
+ cur = []
119
+ ids.append(SP); widx.append(-1)
120
+ continue
121
+ if tok in SYM2ID and tok not in ("<pad>", "<sp>"):
122
+ if tok in _PUNCT:
123
+ if cur:
124
+ wi += 1
125
+ ids += [SYM2ID[p] for p in cur]
126
+ widx += [wi] * len(cur)
127
+ cur = []
128
+ ids.append(SYM2ID[tok]); widx.append(-1)
129
+ else:
130
+ cur.append(tok)
131
+ # words list re-derived from the normalised text for display (best effort alignment)
132
+ words = re.findall(r"[A-Za-z0-9']+", text)
133
+ return ids, widx, words
config.json ADDED
@@ -0,0 +1,2954 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "steer-tts",
3
+ "release": "steertts-v1",
4
+ "sample_rate": 24000,
5
+ "language": "en",
6
+ "default_voice": "voice_1361",
7
+ "voices_order": [
8
+ "voice_0001",
9
+ "voice_0002",
10
+ "voice_0003",
11
+ "voice_0004",
12
+ "voice_0005",
13
+ "voice_0006",
14
+ "voice_0007",
15
+ "voice_0008",
16
+ "voice_0009",
17
+ "voice_0010",
18
+ "voice_0011",
19
+ "voice_0012",
20
+ "voice_0013",
21
+ "voice_0014",
22
+ "voice_0015",
23
+ "voice_0016",
24
+ "voice_0017",
25
+ "voice_0018",
26
+ "voice_0019",
27
+ "voice_0020",
28
+ "voice_0021",
29
+ "voice_0022",
30
+ "voice_0023",
31
+ "voice_0024",
32
+ "voice_0025",
33
+ "voice_0026",
34
+ "voice_0027",
35
+ "voice_0028",
36
+ "voice_0029",
37
+ "voice_0030",
38
+ "voice_0031",
39
+ "voice_0032",
40
+ "voice_0033",
41
+ "voice_0034",
42
+ "voice_0035",
43
+ "voice_0036",
44
+ "voice_0037",
45
+ "voice_0038",
46
+ "voice_0039",
47
+ "voice_0040",
48
+ "voice_0041",
49
+ "voice_0042",
50
+ "voice_0043",
51
+ "voice_0044",
52
+ "voice_0045",
53
+ "voice_0046",
54
+ "voice_0047",
55
+ "voice_0048",
56
+ "voice_0049",
57
+ "voice_0050",
58
+ "voice_0051",
59
+ "voice_0052",
60
+ "voice_0053",
61
+ "voice_0054",
62
+ "voice_0055",
63
+ "voice_0056",
64
+ "voice_0057",
65
+ "voice_0058",
66
+ "voice_0059",
67
+ "voice_0060",
68
+ "voice_0061",
69
+ "voice_0062",
70
+ "voice_0063",
71
+ "voice_0064",
72
+ "voice_0065",
73
+ "voice_0066",
74
+ "voice_0067",
75
+ "voice_0068",
76
+ "voice_0069",
77
+ "voice_0070",
78
+ "voice_0071",
79
+ "voice_0072",
80
+ "voice_0073",
81
+ "voice_0074",
82
+ "voice_0075",
83
+ "voice_0076",
84
+ "voice_0077",
85
+ "voice_0078",
86
+ "voice_0079",
87
+ "voice_0080",
88
+ "voice_0081",
89
+ "voice_0082",
90
+ "voice_0083",
91
+ "voice_0084",
92
+ "voice_0085",
93
+ "voice_0086",
94
+ "voice_0087",
95
+ "voice_0088",
96
+ "voice_0089",
97
+ "voice_0090",
98
+ "voice_0091",
99
+ "voice_0092",
100
+ "voice_0093",
101
+ "voice_0094",
102
+ "voice_0095",
103
+ "voice_0096",
104
+ "voice_0097",
105
+ "voice_0098",
106
+ "voice_0099",
107
+ "voice_0100",
108
+ "voice_0101",
109
+ "voice_0102",
110
+ "voice_0103",
111
+ "voice_0104",
112
+ "voice_0105",
113
+ "voice_0106",
114
+ "voice_0107",
115
+ "voice_0108",
116
+ "voice_0109",
117
+ "voice_0110",
118
+ "voice_0111",
119
+ "voice_0112",
120
+ "voice_0113",
121
+ "voice_0114",
122
+ "voice_0115",
123
+ "voice_0116",
124
+ "voice_0117",
125
+ "voice_0118",
126
+ "voice_0119",
127
+ "voice_0120",
128
+ "voice_0121",
129
+ "voice_0122",
130
+ "voice_0123",
131
+ "voice_0124",
132
+ "voice_0125",
133
+ "voice_0126",
134
+ "voice_0127",
135
+ "voice_0128",
136
+ "voice_0129",
137
+ "voice_0130",
138
+ "voice_0131",
139
+ "voice_0132",
140
+ "voice_0133",
141
+ "voice_0134",
142
+ "voice_0135",
143
+ "voice_0136",
144
+ "voice_0137",
145
+ "voice_0138",
146
+ "voice_0139",
147
+ "voice_0140",
148
+ "voice_0141",
149
+ "voice_0142",
150
+ "voice_0143",
151
+ "voice_0144",
152
+ "voice_0145",
153
+ "voice_0146",
154
+ "voice_0147",
155
+ "voice_0148",
156
+ "voice_0149",
157
+ "voice_0150",
158
+ "voice_0151",
159
+ "voice_0152",
160
+ "voice_0153",
161
+ "voice_0154",
162
+ "voice_0155",
163
+ "voice_0156",
164
+ "voice_0157",
165
+ "voice_0158",
166
+ "voice_0159",
167
+ "voice_0160",
168
+ "voice_0161",
169
+ "voice_0162",
170
+ "voice_0163",
171
+ "voice_0164",
172
+ "voice_0165",
173
+ "voice_0166",
174
+ "voice_0167",
175
+ "voice_0168",
176
+ "voice_0169",
177
+ "voice_0170",
178
+ "voice_0171",
179
+ "voice_0172",
180
+ "voice_0173",
181
+ "voice_0174",
182
+ "voice_0175",
183
+ "voice_0176",
184
+ "voice_0177",
185
+ "voice_0178",
186
+ "voice_0179",
187
+ "voice_0180",
188
+ "voice_0181",
189
+ "voice_0182",
190
+ "voice_0183",
191
+ "voice_0184",
192
+ "voice_0185",
193
+ "voice_0186",
194
+ "voice_0187",
195
+ "voice_0188",
196
+ "voice_0189",
197
+ "voice_0190",
198
+ "voice_0191",
199
+ "voice_0192",
200
+ "voice_0193",
201
+ "voice_0194",
202
+ "voice_0195",
203
+ "voice_0196",
204
+ "voice_0197",
205
+ "voice_0198",
206
+ "voice_0199",
207
+ "voice_0200",
208
+ "voice_0201",
209
+ "voice_0202",
210
+ "voice_0203",
211
+ "voice_0204",
212
+ "voice_0205",
213
+ "voice_0206",
214
+ "voice_0207",
215
+ "voice_0208",
216
+ "voice_0209",
217
+ "voice_0210",
218
+ "voice_0211",
219
+ "voice_0212",
220
+ "voice_0213",
221
+ "voice_0214",
222
+ "voice_0215",
223
+ "voice_0216",
224
+ "voice_0217",
225
+ "voice_0218",
226
+ "voice_0219",
227
+ "voice_0220",
228
+ "voice_0221",
229
+ "voice_0222",
230
+ "voice_0223",
231
+ "voice_0224",
232
+ "voice_0225",
233
+ "voice_0226",
234
+ "voice_0227",
235
+ "voice_0228",
236
+ "voice_0229",
237
+ "voice_0230",
238
+ "voice_0231",
239
+ "voice_0232",
240
+ "voice_0233",
241
+ "voice_0234",
242
+ "voice_0235",
243
+ "voice_0236",
244
+ "voice_0237",
245
+ "voice_0238",
246
+ "voice_0239",
247
+ "voice_0240",
248
+ "voice_0241",
249
+ "voice_0242",
250
+ "voice_0243",
251
+ "voice_0244",
252
+ "voice_0245",
253
+ "voice_0246",
254
+ "voice_0247",
255
+ "voice_0248",
256
+ "voice_0249",
257
+ "voice_0250",
258
+ "voice_0251",
259
+ "voice_0252",
260
+ "voice_0253",
261
+ "voice_0254",
262
+ "voice_0255",
263
+ "voice_0256",
264
+ "voice_0257",
265
+ "voice_0258",
266
+ "voice_0259",
267
+ "voice_0260",
268
+ "voice_0261",
269
+ "voice_0262",
270
+ "voice_0263",
271
+ "voice_0264",
272
+ "voice_0265",
273
+ "voice_0266",
274
+ "voice_0267",
275
+ "voice_0268",
276
+ "voice_0269",
277
+ "voice_0270",
278
+ "voice_0271",
279
+ "voice_0272",
280
+ "voice_0273",
281
+ "voice_0274",
282
+ "voice_0275",
283
+ "voice_0276",
284
+ "voice_0277",
285
+ "voice_0278",
286
+ "voice_0279",
287
+ "voice_0280",
288
+ "voice_0281",
289
+ "voice_0282",
290
+ "voice_0283",
291
+ "voice_0284",
292
+ "voice_0285",
293
+ "voice_0286",
294
+ "voice_0287",
295
+ "voice_0288",
296
+ "voice_0289",
297
+ "voice_0290",
298
+ "voice_0291",
299
+ "voice_0292",
300
+ "voice_0293",
301
+ "voice_0294",
302
+ "voice_0295",
303
+ "voice_0296",
304
+ "voice_0297",
305
+ "voice_0298",
306
+ "voice_0299",
307
+ "voice_0300",
308
+ "voice_0301",
309
+ "voice_0302",
310
+ "voice_0303",
311
+ "voice_0304",
312
+ "voice_0305",
313
+ "voice_0306",
314
+ "voice_0307",
315
+ "voice_0308",
316
+ "voice_0309",
317
+ "voice_0310",
318
+ "voice_0311",
319
+ "voice_0312",
320
+ "voice_0313",
321
+ "voice_0314",
322
+ "voice_0315",
323
+ "voice_0316",
324
+ "voice_0317",
325
+ "voice_0318",
326
+ "voice_0319",
327
+ "voice_0320",
328
+ "voice_0321",
329
+ "voice_0322",
330
+ "voice_0323",
331
+ "voice_0324",
332
+ "voice_0325",
333
+ "voice_0326",
334
+ "voice_0327",
335
+ "voice_0328",
336
+ "voice_0329",
337
+ "voice_0330",
338
+ "voice_0331",
339
+ "voice_0332",
340
+ "voice_0333",
341
+ "voice_0334",
342
+ "voice_0335",
343
+ "voice_0336",
344
+ "voice_0337",
345
+ "voice_0338",
346
+ "voice_0339",
347
+ "voice_0340",
348
+ "voice_0341",
349
+ "voice_0342",
350
+ "voice_0343",
351
+ "voice_0344",
352
+ "voice_0345",
353
+ "voice_0346",
354
+ "voice_0347",
355
+ "voice_0348",
356
+ "voice_0349",
357
+ "voice_0350",
358
+ "voice_0351",
359
+ "voice_0352",
360
+ "voice_0353",
361
+ "voice_0354",
362
+ "voice_0355",
363
+ "voice_0356",
364
+ "voice_0357",
365
+ "voice_0358",
366
+ "voice_0359",
367
+ "voice_0360",
368
+ "voice_0361",
369
+ "voice_0362",
370
+ "voice_0363",
371
+ "voice_0364",
372
+ "voice_0365",
373
+ "voice_0366",
374
+ "voice_0367",
375
+ "voice_0368",
376
+ "voice_0369",
377
+ "voice_0370",
378
+ "voice_0371",
379
+ "voice_0372",
380
+ "voice_0373",
381
+ "voice_0374",
382
+ "voice_0375",
383
+ "voice_0376",
384
+ "voice_0377",
385
+ "voice_0378",
386
+ "voice_0379",
387
+ "voice_0380",
388
+ "voice_0381",
389
+ "voice_0382",
390
+ "voice_0383",
391
+ "voice_0384",
392
+ "voice_0385",
393
+ "voice_0386",
394
+ "voice_0387",
395
+ "voice_0388",
396
+ "voice_0389",
397
+ "voice_0390",
398
+ "voice_0391",
399
+ "voice_0392",
400
+ "voice_0393",
401
+ "voice_0394",
402
+ "voice_0395",
403
+ "voice_0396",
404
+ "voice_0397",
405
+ "voice_0398",
406
+ "voice_0399",
407
+ "voice_0400",
408
+ "voice_0401",
409
+ "voice_0402",
410
+ "voice_0403",
411
+ "voice_0404",
412
+ "voice_0405",
413
+ "voice_0406",
414
+ "voice_0407",
415
+ "voice_0408",
416
+ "voice_0409",
417
+ "voice_0410",
418
+ "voice_0411",
419
+ "voice_0412",
420
+ "voice_0413",
421
+ "voice_0414",
422
+ "voice_0415",
423
+ "voice_0416",
424
+ "voice_0417",
425
+ "voice_0418",
426
+ "voice_0419",
427
+ "voice_0420",
428
+ "voice_0421",
429
+ "voice_0422",
430
+ "voice_0423",
431
+ "voice_0424",
432
+ "voice_0425",
433
+ "voice_0426",
434
+ "voice_0427",
435
+ "voice_0428",
436
+ "voice_0429",
437
+ "voice_0430",
438
+ "voice_0431",
439
+ "voice_0432",
440
+ "voice_0433",
441
+ "voice_0434",
442
+ "voice_0435",
443
+ "voice_0436",
444
+ "voice_0437",
445
+ "voice_0438",
446
+ "voice_0439",
447
+ "voice_0440",
448
+ "voice_0441",
449
+ "voice_0442",
450
+ "voice_0443",
451
+ "voice_0444",
452
+ "voice_0445",
453
+ "voice_0446",
454
+ "voice_0447",
455
+ "voice_0448",
456
+ "voice_0449",
457
+ "voice_0450",
458
+ "voice_0451",
459
+ "voice_0452",
460
+ "voice_0453",
461
+ "voice_0454",
462
+ "voice_0455",
463
+ "voice_0456",
464
+ "voice_0457",
465
+ "voice_0458",
466
+ "voice_0459",
467
+ "voice_0460",
468
+ "voice_0461",
469
+ "voice_0462",
470
+ "voice_0463",
471
+ "voice_0464",
472
+ "voice_0465",
473
+ "voice_0466",
474
+ "voice_0467",
475
+ "voice_0468",
476
+ "voice_0469",
477
+ "voice_0470",
478
+ "voice_0471",
479
+ "voice_0472",
480
+ "voice_0473",
481
+ "voice_0474",
482
+ "voice_0475",
483
+ "voice_0476",
484
+ "voice_0477",
485
+ "voice_0478",
486
+ "voice_0479",
487
+ "voice_0480",
488
+ "voice_0481",
489
+ "voice_0482",
490
+ "voice_0483",
491
+ "voice_0484",
492
+ "voice_0485",
493
+ "voice_0486",
494
+ "voice_0487",
495
+ "voice_0488",
496
+ "voice_0489",
497
+ "voice_0490",
498
+ "voice_0491",
499
+ "voice_0492",
500
+ "voice_0493",
501
+ "voice_0494",
502
+ "voice_0495",
503
+ "voice_0496",
504
+ "voice_0497",
505
+ "voice_0498",
506
+ "voice_0499",
507
+ "voice_0500",
508
+ "voice_0501",
509
+ "voice_0502",
510
+ "voice_0503",
511
+ "voice_0504",
512
+ "voice_0505",
513
+ "voice_0506",
514
+ "voice_0507",
515
+ "voice_0508",
516
+ "voice_0509",
517
+ "voice_0510",
518
+ "voice_0511",
519
+ "voice_0512",
520
+ "voice_0513",
521
+ "voice_0514",
522
+ "voice_0515",
523
+ "voice_0516",
524
+ "voice_0517",
525
+ "voice_0518",
526
+ "voice_0519",
527
+ "voice_0520",
528
+ "voice_0521",
529
+ "voice_0522",
530
+ "voice_0523",
531
+ "voice_0524",
532
+ "voice_0525",
533
+ "voice_0526",
534
+ "voice_0527",
535
+ "voice_0528",
536
+ "voice_0529",
537
+ "voice_0530",
538
+ "voice_0531",
539
+ "voice_0532",
540
+ "voice_0533",
541
+ "voice_0534",
542
+ "voice_0535",
543
+ "voice_0536",
544
+ "voice_0537",
545
+ "voice_0538",
546
+ "voice_0539",
547
+ "voice_0540",
548
+ "voice_0541",
549
+ "voice_0542",
550
+ "voice_0543",
551
+ "voice_0544",
552
+ "voice_0545",
553
+ "voice_0546",
554
+ "voice_0547",
555
+ "voice_0548",
556
+ "voice_0549",
557
+ "voice_0550",
558
+ "voice_0551",
559
+ "voice_0552",
560
+ "voice_0553",
561
+ "voice_0554",
562
+ "voice_0555",
563
+ "voice_0556",
564
+ "voice_0557",
565
+ "voice_0558",
566
+ "voice_0559",
567
+ "voice_0560",
568
+ "voice_0561",
569
+ "voice_0562",
570
+ "voice_0563",
571
+ "voice_0564",
572
+ "voice_0565",
573
+ "voice_0566",
574
+ "voice_0567",
575
+ "voice_0568",
576
+ "voice_0569",
577
+ "voice_0570",
578
+ "voice_0571",
579
+ "voice_0572",
580
+ "voice_0573",
581
+ "voice_0574",
582
+ "voice_0575",
583
+ "voice_0576",
584
+ "voice_0577",
585
+ "voice_0578",
586
+ "voice_0579",
587
+ "voice_0580",
588
+ "voice_0581",
589
+ "voice_0582",
590
+ "voice_0583",
591
+ "voice_0584",
592
+ "voice_0585",
593
+ "voice_0586",
594
+ "voice_0587",
595
+ "voice_0588",
596
+ "voice_0589",
597
+ "voice_0590",
598
+ "voice_0591",
599
+ "voice_0592",
600
+ "voice_0593",
601
+ "voice_0594",
602
+ "voice_0595",
603
+ "voice_0596",
604
+ "voice_0597",
605
+ "voice_0598",
606
+ "voice_0599",
607
+ "voice_0600",
608
+ "voice_0601",
609
+ "voice_0602",
610
+ "voice_0603",
611
+ "voice_0604",
612
+ "voice_0605",
613
+ "voice_0606",
614
+ "voice_0607",
615
+ "voice_0608",
616
+ "voice_0609",
617
+ "voice_0610",
618
+ "voice_0611",
619
+ "voice_0612",
620
+ "voice_0613",
621
+ "voice_0614",
622
+ "voice_0615",
623
+ "voice_0616",
624
+ "voice_0617",
625
+ "voice_0618",
626
+ "voice_0619",
627
+ "voice_0620",
628
+ "voice_0621",
629
+ "voice_0622",
630
+ "voice_0623",
631
+ "voice_0624",
632
+ "voice_0625",
633
+ "voice_0626",
634
+ "voice_0627",
635
+ "voice_0628",
636
+ "voice_0629",
637
+ "voice_0630",
638
+ "voice_0631",
639
+ "voice_0632",
640
+ "voice_0633",
641
+ "voice_0634",
642
+ "voice_0635",
643
+ "voice_0636",
644
+ "voice_0637",
645
+ "voice_0638",
646
+ "voice_0639",
647
+ "voice_0640",
648
+ "voice_0641",
649
+ "voice_0642",
650
+ "voice_0643",
651
+ "voice_0644",
652
+ "voice_0645",
653
+ "voice_0646",
654
+ "voice_0647",
655
+ "voice_0648",
656
+ "voice_0649",
657
+ "voice_0650",
658
+ "voice_0651",
659
+ "voice_0652",
660
+ "voice_0653",
661
+ "voice_0654",
662
+ "voice_0655",
663
+ "voice_0656",
664
+ "voice_0657",
665
+ "voice_0658",
666
+ "voice_0659",
667
+ "voice_0660",
668
+ "voice_0661",
669
+ "voice_0662",
670
+ "voice_0663",
671
+ "voice_0664",
672
+ "voice_0665",
673
+ "voice_0666",
674
+ "voice_0667",
675
+ "voice_0668",
676
+ "voice_0669",
677
+ "voice_0670",
678
+ "voice_0671",
679
+ "voice_0672",
680
+ "voice_0673",
681
+ "voice_0674",
682
+ "voice_0675",
683
+ "voice_0676",
684
+ "voice_0677",
685
+ "voice_0678",
686
+ "voice_0679",
687
+ "voice_0680",
688
+ "voice_0681",
689
+ "voice_0682",
690
+ "voice_0683",
691
+ "voice_0684",
692
+ "voice_0685",
693
+ "voice_0686",
694
+ "voice_0687",
695
+ "voice_0688",
696
+ "voice_0689",
697
+ "voice_0690",
698
+ "voice_0691",
699
+ "voice_0692",
700
+ "voice_0693",
701
+ "voice_0694",
702
+ "voice_0695",
703
+ "voice_0696",
704
+ "voice_0697",
705
+ "voice_0698",
706
+ "voice_0699",
707
+ "voice_0700",
708
+ "voice_0701",
709
+ "voice_0702",
710
+ "voice_0703",
711
+ "voice_0704",
712
+ "voice_0705",
713
+ "voice_0706",
714
+ "voice_0707",
715
+ "voice_0708",
716
+ "voice_0709",
717
+ "voice_0710",
718
+ "voice_0711",
719
+ "voice_0712",
720
+ "voice_0713",
721
+ "voice_0714",
722
+ "voice_0715",
723
+ "voice_0716",
724
+ "voice_0717",
725
+ "voice_0718",
726
+ "voice_0719",
727
+ "voice_0720",
728
+ "voice_0721",
729
+ "voice_0722",
730
+ "voice_0723",
731
+ "voice_0724",
732
+ "voice_0725",
733
+ "voice_0726",
734
+ "voice_0727",
735
+ "voice_0728",
736
+ "voice_0729",
737
+ "voice_0730",
738
+ "voice_0731",
739
+ "voice_0732",
740
+ "voice_0733",
741
+ "voice_0734",
742
+ "voice_0735",
743
+ "voice_0736",
744
+ "voice_0737",
745
+ "voice_0738",
746
+ "voice_0739",
747
+ "voice_0740",
748
+ "voice_0741",
749
+ "voice_0742",
750
+ "voice_0743",
751
+ "voice_0744",
752
+ "voice_0745",
753
+ "voice_0746",
754
+ "voice_0747",
755
+ "voice_0748",
756
+ "voice_0749",
757
+ "voice_0750",
758
+ "voice_0751",
759
+ "voice_0752",
760
+ "voice_0753",
761
+ "voice_0754",
762
+ "voice_0755",
763
+ "voice_0756",
764
+ "voice_0757",
765
+ "voice_0758",
766
+ "voice_0759",
767
+ "voice_0760",
768
+ "voice_0761",
769
+ "voice_0762",
770
+ "voice_0763",
771
+ "voice_0764",
772
+ "voice_0765",
773
+ "voice_0766",
774
+ "voice_0767",
775
+ "voice_0768",
776
+ "voice_0769",
777
+ "voice_0770",
778
+ "voice_0771",
779
+ "voice_0772",
780
+ "voice_0773",
781
+ "voice_0774",
782
+ "voice_0775",
783
+ "voice_0776",
784
+ "voice_0777",
785
+ "voice_0778",
786
+ "voice_0779",
787
+ "voice_0780",
788
+ "voice_0781",
789
+ "voice_0782",
790
+ "voice_0783",
791
+ "voice_0784",
792
+ "voice_0785",
793
+ "voice_0786",
794
+ "voice_0787",
795
+ "voice_0788",
796
+ "voice_0789",
797
+ "voice_0790",
798
+ "voice_0791",
799
+ "voice_0792",
800
+ "voice_0793",
801
+ "voice_0794",
802
+ "voice_0795",
803
+ "voice_0796",
804
+ "voice_0797",
805
+ "voice_0798",
806
+ "voice_0799",
807
+ "voice_0800",
808
+ "voice_0801",
809
+ "voice_0802",
810
+ "voice_0803",
811
+ "voice_0804",
812
+ "voice_0805",
813
+ "voice_0806",
814
+ "voice_0807",
815
+ "voice_0808",
816
+ "voice_0809",
817
+ "voice_0810",
818
+ "voice_0811",
819
+ "voice_0812",
820
+ "voice_0813",
821
+ "voice_0814",
822
+ "voice_0815",
823
+ "voice_0816",
824
+ "voice_0817",
825
+ "voice_0818",
826
+ "voice_0819",
827
+ "voice_0820",
828
+ "voice_0821",
829
+ "voice_0822",
830
+ "voice_0823",
831
+ "voice_0824",
832
+ "voice_0825",
833
+ "voice_0826",
834
+ "voice_0827",
835
+ "voice_0828",
836
+ "voice_0829",
837
+ "voice_0830",
838
+ "voice_0831",
839
+ "voice_0832",
840
+ "voice_0833",
841
+ "voice_0834",
842
+ "voice_0835",
843
+ "voice_0836",
844
+ "voice_0837",
845
+ "voice_0838",
846
+ "voice_0839",
847
+ "voice_0840",
848
+ "voice_0841",
849
+ "voice_0842",
850
+ "voice_0843",
851
+ "voice_0844",
852
+ "voice_0845",
853
+ "voice_0846",
854
+ "voice_0847",
855
+ "voice_0848",
856
+ "voice_0849",
857
+ "voice_0850",
858
+ "voice_0851",
859
+ "voice_0852",
860
+ "voice_0853",
861
+ "voice_0854",
862
+ "voice_0855",
863
+ "voice_0856",
864
+ "voice_0857",
865
+ "voice_0858",
866
+ "voice_0859",
867
+ "voice_0860",
868
+ "voice_0861",
869
+ "voice_0862",
870
+ "voice_0863",
871
+ "voice_0864",
872
+ "voice_0865",
873
+ "voice_0866",
874
+ "voice_0867",
875
+ "voice_0868",
876
+ "voice_0869",
877
+ "voice_0870",
878
+ "voice_0871",
879
+ "voice_0872",
880
+ "voice_0873",
881
+ "voice_0874",
882
+ "voice_0875",
883
+ "voice_0876",
884
+ "voice_0877",
885
+ "voice_0878",
886
+ "voice_0879",
887
+ "voice_0880",
888
+ "voice_0881",
889
+ "voice_0882",
890
+ "voice_0883",
891
+ "voice_0884",
892
+ "voice_0885",
893
+ "voice_0886",
894
+ "voice_0887",
895
+ "voice_0888",
896
+ "voice_0889",
897
+ "voice_0890",
898
+ "voice_0891",
899
+ "voice_0892",
900
+ "voice_0893",
901
+ "voice_0894",
902
+ "voice_0895",
903
+ "voice_0896",
904
+ "voice_0897",
905
+ "voice_0898",
906
+ "voice_0899",
907
+ "voice_0900",
908
+ "voice_0901",
909
+ "voice_0902",
910
+ "voice_0903",
911
+ "voice_0904",
912
+ "voice_0905",
913
+ "voice_0906",
914
+ "voice_0907",
915
+ "voice_0908",
916
+ "voice_0909",
917
+ "voice_0910",
918
+ "voice_0911",
919
+ "voice_0912",
920
+ "voice_0913",
921
+ "voice_0914",
922
+ "voice_0915",
923
+ "voice_0916",
924
+ "voice_0917",
925
+ "voice_0918",
926
+ "voice_0919",
927
+ "voice_0920",
928
+ "voice_0921",
929
+ "voice_0922",
930
+ "voice_0923",
931
+ "voice_0924",
932
+ "voice_0925",
933
+ "voice_0926",
934
+ "voice_0927",
935
+ "voice_0928",
936
+ "voice_0929",
937
+ "voice_0930",
938
+ "voice_0931",
939
+ "voice_0932",
940
+ "voice_0933",
941
+ "voice_0934",
942
+ "voice_0935",
943
+ "voice_0936",
944
+ "voice_0937",
945
+ "voice_0938",
946
+ "voice_0939",
947
+ "voice_0940",
948
+ "voice_0941",
949
+ "voice_0942",
950
+ "voice_0943",
951
+ "voice_0944",
952
+ "voice_0945",
953
+ "voice_0946",
954
+ "voice_0947",
955
+ "voice_0948",
956
+ "voice_0949",
957
+ "voice_0950",
958
+ "voice_0951",
959
+ "voice_0952",
960
+ "voice_0953",
961
+ "voice_0954",
962
+ "voice_0955",
963
+ "voice_0956",
964
+ "voice_0957",
965
+ "voice_0958",
966
+ "voice_0959",
967
+ "voice_0960",
968
+ "voice_0961",
969
+ "voice_0962",
970
+ "voice_0963",
971
+ "voice_0964",
972
+ "voice_0965",
973
+ "voice_0966",
974
+ "voice_0967",
975
+ "voice_0968",
976
+ "voice_0969",
977
+ "voice_0970",
978
+ "voice_0971",
979
+ "voice_0972",
980
+ "voice_0973",
981
+ "voice_0974",
982
+ "voice_0975",
983
+ "voice_0976",
984
+ "voice_0977",
985
+ "voice_0978",
986
+ "voice_0979",
987
+ "voice_0980",
988
+ "voice_0981",
989
+ "voice_0982",
990
+ "voice_0983",
991
+ "voice_0984",
992
+ "voice_0985",
993
+ "voice_0986",
994
+ "voice_0987",
995
+ "voice_0988",
996
+ "voice_0989",
997
+ "voice_0990",
998
+ "voice_0991",
999
+ "voice_0992",
1000
+ "voice_0993",
1001
+ "voice_0994",
1002
+ "voice_0995",
1003
+ "voice_0996",
1004
+ "voice_0997",
1005
+ "voice_0998",
1006
+ "voice_0999",
1007
+ "voice_1000",
1008
+ "voice_1001",
1009
+ "voice_1002",
1010
+ "voice_1003",
1011
+ "voice_1004",
1012
+ "voice_1005",
1013
+ "voice_1006",
1014
+ "voice_1007",
1015
+ "voice_1008",
1016
+ "voice_1009",
1017
+ "voice_1010",
1018
+ "voice_1011",
1019
+ "voice_1012",
1020
+ "voice_1013",
1021
+ "voice_1014",
1022
+ "voice_1015",
1023
+ "voice_1016",
1024
+ "voice_1017",
1025
+ "voice_1018",
1026
+ "voice_1019",
1027
+ "voice_1020",
1028
+ "voice_1021",
1029
+ "voice_1022",
1030
+ "voice_1023",
1031
+ "voice_1024",
1032
+ "voice_1025",
1033
+ "voice_1026",
1034
+ "voice_1027",
1035
+ "voice_1028",
1036
+ "voice_1029",
1037
+ "voice_1030",
1038
+ "voice_1031",
1039
+ "voice_1032",
1040
+ "voice_1033",
1041
+ "voice_1034",
1042
+ "voice_1035",
1043
+ "voice_1036",
1044
+ "voice_1037",
1045
+ "voice_1038",
1046
+ "voice_1039",
1047
+ "voice_1040",
1048
+ "voice_1041",
1049
+ "voice_1042",
1050
+ "voice_1043",
1051
+ "voice_1044",
1052
+ "voice_1045",
1053
+ "voice_1046",
1054
+ "voice_1047",
1055
+ "voice_1048",
1056
+ "voice_1049",
1057
+ "voice_1050",
1058
+ "voice_1051",
1059
+ "voice_1052",
1060
+ "voice_1053",
1061
+ "voice_1054",
1062
+ "voice_1055",
1063
+ "voice_1056",
1064
+ "voice_1057",
1065
+ "voice_1058",
1066
+ "voice_1059",
1067
+ "voice_1060",
1068
+ "voice_1061",
1069
+ "voice_1062",
1070
+ "voice_1063",
1071
+ "voice_1064",
1072
+ "voice_1065",
1073
+ "voice_1066",
1074
+ "voice_1067",
1075
+ "voice_1068",
1076
+ "voice_1069",
1077
+ "voice_1070",
1078
+ "voice_1071",
1079
+ "voice_1072",
1080
+ "voice_1073",
1081
+ "voice_1074",
1082
+ "voice_1075",
1083
+ "voice_1076",
1084
+ "voice_1077",
1085
+ "voice_1078",
1086
+ "voice_1079",
1087
+ "voice_1080",
1088
+ "voice_1081",
1089
+ "voice_1082",
1090
+ "voice_1083",
1091
+ "voice_1084",
1092
+ "voice_1085",
1093
+ "voice_1086",
1094
+ "voice_1087",
1095
+ "voice_1088",
1096
+ "voice_1089",
1097
+ "voice_1090",
1098
+ "voice_1091",
1099
+ "voice_1092",
1100
+ "voice_1093",
1101
+ "voice_1094",
1102
+ "voice_1095",
1103
+ "voice_1096",
1104
+ "voice_1097",
1105
+ "voice_1098",
1106
+ "voice_1099",
1107
+ "voice_1100",
1108
+ "voice_1101",
1109
+ "voice_1102",
1110
+ "voice_1103",
1111
+ "voice_1104",
1112
+ "voice_1105",
1113
+ "voice_1106",
1114
+ "voice_1107",
1115
+ "voice_1108",
1116
+ "voice_1109",
1117
+ "voice_1110",
1118
+ "voice_1111",
1119
+ "voice_1112",
1120
+ "voice_1113",
1121
+ "voice_1114",
1122
+ "voice_1115",
1123
+ "voice_1116",
1124
+ "voice_1117",
1125
+ "voice_1118",
1126
+ "voice_1119",
1127
+ "voice_1120",
1128
+ "voice_1121",
1129
+ "voice_1122",
1130
+ "voice_1123",
1131
+ "voice_1124",
1132
+ "voice_1125",
1133
+ "voice_1126",
1134
+ "voice_1127",
1135
+ "voice_1128",
1136
+ "voice_1129",
1137
+ "voice_1130",
1138
+ "voice_1131",
1139
+ "voice_1132",
1140
+ "voice_1133",
1141
+ "voice_1134",
1142
+ "voice_1135",
1143
+ "voice_1136",
1144
+ "voice_1137",
1145
+ "voice_1138",
1146
+ "voice_1139",
1147
+ "voice_1140",
1148
+ "voice_1141",
1149
+ "voice_1142",
1150
+ "voice_1143",
1151
+ "voice_1144",
1152
+ "voice_1145",
1153
+ "voice_1146",
1154
+ "voice_1147",
1155
+ "voice_1148",
1156
+ "voice_1149",
1157
+ "voice_1150",
1158
+ "voice_1151",
1159
+ "voice_1152",
1160
+ "voice_1153",
1161
+ "voice_1154",
1162
+ "voice_1155",
1163
+ "voice_1156",
1164
+ "voice_1157",
1165
+ "voice_1158",
1166
+ "voice_1159",
1167
+ "voice_1160",
1168
+ "voice_1161",
1169
+ "voice_1162",
1170
+ "voice_1163",
1171
+ "voice_1164",
1172
+ "voice_1165",
1173
+ "voice_1166",
1174
+ "voice_1167",
1175
+ "voice_1168",
1176
+ "voice_1169",
1177
+ "voice_1170",
1178
+ "voice_1171",
1179
+ "voice_1172",
1180
+ "voice_1173",
1181
+ "voice_1174",
1182
+ "voice_1175",
1183
+ "voice_1176",
1184
+ "voice_1177",
1185
+ "voice_1178",
1186
+ "voice_1179",
1187
+ "voice_1180",
1188
+ "voice_1181",
1189
+ "voice_1182",
1190
+ "voice_1183",
1191
+ "voice_1184",
1192
+ "voice_1185",
1193
+ "voice_1186",
1194
+ "voice_1187",
1195
+ "voice_1188",
1196
+ "voice_1189",
1197
+ "voice_1190",
1198
+ "voice_1191",
1199
+ "voice_1192",
1200
+ "voice_1193",
1201
+ "voice_1194",
1202
+ "voice_1195",
1203
+ "voice_1196",
1204
+ "voice_1197",
1205
+ "voice_1198",
1206
+ "voice_1199",
1207
+ "voice_1200",
1208
+ "voice_1201",
1209
+ "voice_1202",
1210
+ "voice_1203",
1211
+ "voice_1204",
1212
+ "voice_1205",
1213
+ "voice_1206",
1214
+ "voice_1207",
1215
+ "voice_1208",
1216
+ "voice_1209",
1217
+ "voice_1210",
1218
+ "voice_1211",
1219
+ "voice_1212",
1220
+ "voice_1213",
1221
+ "voice_1214",
1222
+ "voice_1215",
1223
+ "voice_1216",
1224
+ "voice_1217",
1225
+ "voice_1218",
1226
+ "voice_1219",
1227
+ "voice_1220",
1228
+ "voice_1221",
1229
+ "voice_1222",
1230
+ "voice_1223",
1231
+ "voice_1224",
1232
+ "voice_1225",
1233
+ "voice_1226",
1234
+ "voice_1227",
1235
+ "voice_1228",
1236
+ "voice_1229",
1237
+ "voice_1230",
1238
+ "voice_1231",
1239
+ "voice_1232",
1240
+ "voice_1233",
1241
+ "voice_1234",
1242
+ "voice_1235",
1243
+ "voice_1236",
1244
+ "voice_1237",
1245
+ "voice_1238",
1246
+ "voice_1239",
1247
+ "voice_1240",
1248
+ "voice_1241",
1249
+ "voice_1242",
1250
+ "voice_1243",
1251
+ "voice_1244",
1252
+ "voice_1245",
1253
+ "voice_1246",
1254
+ "voice_1247",
1255
+ "voice_1248",
1256
+ "voice_1249",
1257
+ "voice_1250",
1258
+ "voice_1251",
1259
+ "voice_1252",
1260
+ "voice_1253",
1261
+ "voice_1254",
1262
+ "voice_1255",
1263
+ "voice_1256",
1264
+ "voice_1257",
1265
+ "voice_1258",
1266
+ "voice_1259",
1267
+ "voice_1260",
1268
+ "voice_1261",
1269
+ "voice_1262",
1270
+ "voice_1263",
1271
+ "voice_1264",
1272
+ "voice_1265",
1273
+ "voice_1266",
1274
+ "voice_1267",
1275
+ "voice_1268",
1276
+ "voice_1269",
1277
+ "voice_1270",
1278
+ "voice_1271",
1279
+ "voice_1272",
1280
+ "voice_1273",
1281
+ "voice_1274",
1282
+ "voice_1275",
1283
+ "voice_1276",
1284
+ "voice_1277",
1285
+ "voice_1278",
1286
+ "voice_1279",
1287
+ "voice_1280",
1288
+ "voice_1281",
1289
+ "voice_1282",
1290
+ "voice_1283",
1291
+ "voice_1284",
1292
+ "voice_1285",
1293
+ "voice_1286",
1294
+ "voice_1287",
1295
+ "voice_1288",
1296
+ "voice_1289",
1297
+ "voice_1290",
1298
+ "voice_1291",
1299
+ "voice_1292",
1300
+ "voice_1293",
1301
+ "voice_1294",
1302
+ "voice_1295",
1303
+ "voice_1296",
1304
+ "voice_1297",
1305
+ "voice_1298",
1306
+ "voice_1299",
1307
+ "voice_1300",
1308
+ "voice_1301",
1309
+ "voice_1302",
1310
+ "voice_1303",
1311
+ "voice_1304",
1312
+ "voice_1305",
1313
+ "voice_1306",
1314
+ "voice_1307",
1315
+ "voice_1308",
1316
+ "voice_1309",
1317
+ "voice_1310",
1318
+ "voice_1311",
1319
+ "voice_1312",
1320
+ "voice_1313",
1321
+ "voice_1314",
1322
+ "voice_1315",
1323
+ "voice_1316",
1324
+ "voice_1317",
1325
+ "voice_1318",
1326
+ "voice_1319",
1327
+ "voice_1320",
1328
+ "voice_1321",
1329
+ "voice_1322",
1330
+ "voice_1323",
1331
+ "voice_1324",
1332
+ "voice_1325",
1333
+ "voice_1326",
1334
+ "voice_1327",
1335
+ "voice_1328",
1336
+ "voice_1329",
1337
+ "voice_1330",
1338
+ "voice_1331",
1339
+ "voice_1332",
1340
+ "voice_1333",
1341
+ "voice_1334",
1342
+ "voice_1335",
1343
+ "voice_1336",
1344
+ "voice_1337",
1345
+ "voice_1338",
1346
+ "voice_1339",
1347
+ "voice_1340",
1348
+ "voice_1341",
1349
+ "voice_1342",
1350
+ "voice_1343",
1351
+ "voice_1344",
1352
+ "voice_1345",
1353
+ "voice_1346",
1354
+ "voice_1347",
1355
+ "voice_1348",
1356
+ "voice_1349",
1357
+ "voice_1350",
1358
+ "voice_1351",
1359
+ "voice_1352",
1360
+ "voice_1353",
1361
+ "voice_1354",
1362
+ "voice_1355",
1363
+ "voice_1356",
1364
+ "voice_1357",
1365
+ "voice_1358",
1366
+ "voice_1359",
1367
+ "voice_1360",
1368
+ "voice_1361",
1369
+ "voice_1362",
1370
+ "voice_1363",
1371
+ "voice_1364",
1372
+ "voice_1365",
1373
+ "voice_1366",
1374
+ "voice_1367",
1375
+ "voice_1368",
1376
+ "voice_1369",
1377
+ "voice_1370",
1378
+ "voice_1371",
1379
+ "voice_1372",
1380
+ "voice_1373",
1381
+ "voice_1374",
1382
+ "voice_1375",
1383
+ "voice_1376",
1384
+ "voice_1377",
1385
+ "voice_1378",
1386
+ "voice_1379",
1387
+ "voice_1380",
1388
+ "voice_1381",
1389
+ "voice_1382",
1390
+ "voice_1383",
1391
+ "voice_1384",
1392
+ "voice_1385",
1393
+ "voice_1386",
1394
+ "voice_1387",
1395
+ "voice_1388",
1396
+ "voice_1389",
1397
+ "voice_1390",
1398
+ "voice_1391",
1399
+ "voice_1392",
1400
+ "voice_1393",
1401
+ "voice_1394",
1402
+ "voice_1395",
1403
+ "voice_1396",
1404
+ "voice_1397",
1405
+ "voice_1398",
1406
+ "voice_1399",
1407
+ "voice_1400",
1408
+ "voice_1401",
1409
+ "voice_1402",
1410
+ "voice_1403",
1411
+ "voice_1404",
1412
+ "voice_1405",
1413
+ "voice_1406",
1414
+ "voice_1407",
1415
+ "voice_1408",
1416
+ "voice_1409",
1417
+ "voice_1410",
1418
+ "voice_1411",
1419
+ "voice_1412",
1420
+ "voice_1413",
1421
+ "voice_1414",
1422
+ "voice_1415",
1423
+ "voice_1416",
1424
+ "voice_1417",
1425
+ "voice_1418",
1426
+ "voice_1419",
1427
+ "voice_1420",
1428
+ "voice_1421",
1429
+ "voice_1422",
1430
+ "voice_1423",
1431
+ "voice_1424",
1432
+ "voice_1425",
1433
+ "voice_1426",
1434
+ "voice_1427",
1435
+ "voice_1428",
1436
+ "voice_1429",
1437
+ "voice_1430",
1438
+ "voice_1431",
1439
+ "voice_1432",
1440
+ "voice_1433",
1441
+ "voice_1434",
1442
+ "voice_1435",
1443
+ "voice_1436",
1444
+ "voice_1437",
1445
+ "voice_1438",
1446
+ "voice_1439",
1447
+ "voice_1440",
1448
+ "voice_1441",
1449
+ "voice_1442",
1450
+ "voice_1443",
1451
+ "voice_1444",
1452
+ "voice_1445",
1453
+ "voice_1446",
1454
+ "voice_1447",
1455
+ "voice_1448",
1456
+ "voice_1449",
1457
+ "voice_1450",
1458
+ "voice_1451",
1459
+ "voice_1452",
1460
+ "voice_1453",
1461
+ "voice_1454",
1462
+ "voice_1455",
1463
+ "voice_1456",
1464
+ "voice_1457",
1465
+ "voice_1458",
1466
+ "voice_1459",
1467
+ "voice_1460",
1468
+ "voice_1461",
1469
+ "voice_1462",
1470
+ "voice_1463",
1471
+ "voice_1464",
1472
+ "voice_1465",
1473
+ "voice_1466",
1474
+ "voice_1467",
1475
+ "voice_1468",
1476
+ "voice_1469",
1477
+ "voice_1470",
1478
+ "voice_1471",
1479
+ "voice_1472",
1480
+ "voice_1473",
1481
+ "voice_1474",
1482
+ "voice_1475",
1483
+ "voice_1476",
1484
+ "voice_1477",
1485
+ "voice_1478",
1486
+ "voice_1479",
1487
+ "voice_1480",
1488
+ "voice_1481",
1489
+ "voice_1482",
1490
+ "voice_1483",
1491
+ "voice_1484",
1492
+ "voice_1485",
1493
+ "voice_1486",
1494
+ "voice_1487",
1495
+ "voice_1488",
1496
+ "voice_1489",
1497
+ "voice_1490",
1498
+ "voice_1491",
1499
+ "voice_1492",
1500
+ "voice_1493",
1501
+ "voice_1494",
1502
+ "voice_1495",
1503
+ "voice_1496",
1504
+ "voice_1497",
1505
+ "voice_1498",
1506
+ "voice_1499",
1507
+ "voice_1500",
1508
+ "voice_1501",
1509
+ "voice_1502",
1510
+ "voice_1503",
1511
+ "voice_1504",
1512
+ "voice_1505",
1513
+ "voice_1506",
1514
+ "voice_1507",
1515
+ "voice_1508",
1516
+ "voice_1509",
1517
+ "voice_1510",
1518
+ "voice_1511",
1519
+ "voice_1512",
1520
+ "voice_1513",
1521
+ "voice_1514",
1522
+ "voice_1515",
1523
+ "voice_1516",
1524
+ "voice_1517",
1525
+ "voice_1518",
1526
+ "voice_1519",
1527
+ "voice_1520",
1528
+ "voice_1521",
1529
+ "voice_1522",
1530
+ "voice_1523",
1531
+ "voice_1524",
1532
+ "voice_1525",
1533
+ "voice_1526",
1534
+ "voice_1527",
1535
+ "voice_1528",
1536
+ "voice_1529",
1537
+ "voice_1530",
1538
+ "voice_1531",
1539
+ "voice_1532",
1540
+ "voice_1533",
1541
+ "voice_1534",
1542
+ "voice_1535",
1543
+ "voice_1536",
1544
+ "voice_1537",
1545
+ "voice_1538",
1546
+ "voice_1539",
1547
+ "voice_1540",
1548
+ "voice_1541",
1549
+ "voice_1542",
1550
+ "voice_1543",
1551
+ "voice_1544",
1552
+ "voice_1545",
1553
+ "voice_1546",
1554
+ "voice_1547",
1555
+ "voice_1548",
1556
+ "voice_1549",
1557
+ "voice_1550",
1558
+ "voice_1551",
1559
+ "voice_1552",
1560
+ "voice_1553",
1561
+ "voice_1554",
1562
+ "voice_1555",
1563
+ "voice_1556",
1564
+ "voice_1557",
1565
+ "voice_1558",
1566
+ "voice_1559",
1567
+ "voice_1560",
1568
+ "voice_1561",
1569
+ "voice_1562",
1570
+ "voice_1563",
1571
+ "voice_1564",
1572
+ "voice_1565",
1573
+ "voice_1566",
1574
+ "voice_1567",
1575
+ "voice_1568",
1576
+ "voice_1569",
1577
+ "voice_1570",
1578
+ "voice_1571",
1579
+ "voice_1572",
1580
+ "voice_1573",
1581
+ "voice_1574",
1582
+ "voice_1575",
1583
+ "voice_1576",
1584
+ "voice_1577",
1585
+ "voice_1578",
1586
+ "voice_1579",
1587
+ "voice_1580",
1588
+ "voice_1581",
1589
+ "voice_1582",
1590
+ "voice_1583",
1591
+ "voice_1584",
1592
+ "voice_1585",
1593
+ "voice_1586",
1594
+ "voice_1587",
1595
+ "voice_1588",
1596
+ "voice_1589",
1597
+ "voice_1590",
1598
+ "voice_1591",
1599
+ "voice_1592",
1600
+ "voice_1593",
1601
+ "voice_1594",
1602
+ "voice_1595",
1603
+ "voice_1596",
1604
+ "voice_1597",
1605
+ "voice_1598",
1606
+ "voice_1599",
1607
+ "voice_1600",
1608
+ "voice_1601",
1609
+ "voice_1602",
1610
+ "voice_1603",
1611
+ "voice_1604",
1612
+ "voice_1605",
1613
+ "voice_1606",
1614
+ "voice_1607",
1615
+ "voice_1608",
1616
+ "voice_1609",
1617
+ "voice_1610",
1618
+ "voice_1611",
1619
+ "voice_1612",
1620
+ "voice_1613",
1621
+ "voice_1614",
1622
+ "voice_1615",
1623
+ "voice_1616",
1624
+ "voice_1617",
1625
+ "voice_1618",
1626
+ "voice_1619",
1627
+ "voice_1620",
1628
+ "voice_1621",
1629
+ "voice_1622",
1630
+ "voice_1623",
1631
+ "voice_1624",
1632
+ "voice_1625",
1633
+ "voice_1626",
1634
+ "voice_1627",
1635
+ "voice_1628",
1636
+ "voice_1629",
1637
+ "voice_1630",
1638
+ "voice_1631",
1639
+ "voice_1632",
1640
+ "voice_1633",
1641
+ "voice_1634",
1642
+ "voice_1635",
1643
+ "voice_1636",
1644
+ "voice_1637",
1645
+ "voice_1638",
1646
+ "voice_1639",
1647
+ "voice_1640",
1648
+ "voice_1641",
1649
+ "voice_1642",
1650
+ "voice_1643",
1651
+ "voice_1644",
1652
+ "voice_1645",
1653
+ "voice_1646",
1654
+ "voice_1647",
1655
+ "voice_1648",
1656
+ "voice_1649",
1657
+ "voice_1650",
1658
+ "voice_1651",
1659
+ "voice_1652",
1660
+ "voice_1653",
1661
+ "voice_1654",
1662
+ "voice_1655",
1663
+ "voice_1656",
1664
+ "voice_1657",
1665
+ "voice_1658",
1666
+ "voice_1659",
1667
+ "voice_1660",
1668
+ "voice_1661",
1669
+ "voice_1662",
1670
+ "voice_1663",
1671
+ "voice_1664",
1672
+ "voice_1665",
1673
+ "voice_1666",
1674
+ "voice_1667",
1675
+ "voice_1668",
1676
+ "voice_1669",
1677
+ "voice_1670",
1678
+ "voice_1671",
1679
+ "voice_1672",
1680
+ "voice_1673",
1681
+ "voice_1674",
1682
+ "voice_1675",
1683
+ "voice_1676",
1684
+ "voice_1677",
1685
+ "voice_1678",
1686
+ "voice_1679",
1687
+ "voice_1680",
1688
+ "voice_1681",
1689
+ "voice_1682",
1690
+ "voice_1683",
1691
+ "voice_1684",
1692
+ "voice_1685",
1693
+ "voice_1686",
1694
+ "voice_1687",
1695
+ "voice_1688",
1696
+ "voice_1689",
1697
+ "voice_1690",
1698
+ "voice_1691",
1699
+ "voice_1692",
1700
+ "voice_1693",
1701
+ "voice_1694",
1702
+ "voice_1695",
1703
+ "voice_1696",
1704
+ "voice_1697",
1705
+ "voice_1698",
1706
+ "voice_1699",
1707
+ "voice_1700",
1708
+ "voice_1701",
1709
+ "voice_1702",
1710
+ "voice_1703",
1711
+ "voice_1704",
1712
+ "voice_1705",
1713
+ "voice_1706",
1714
+ "voice_1707",
1715
+ "voice_1708",
1716
+ "voice_1709",
1717
+ "voice_1710",
1718
+ "voice_1711",
1719
+ "voice_1712",
1720
+ "voice_1713",
1721
+ "voice_1714",
1722
+ "voice_1715",
1723
+ "voice_1716",
1724
+ "voice_1717",
1725
+ "voice_1718",
1726
+ "voice_1719",
1727
+ "voice_1720",
1728
+ "voice_1721",
1729
+ "voice_1722",
1730
+ "voice_1723",
1731
+ "voice_1724",
1732
+ "voice_1725",
1733
+ "voice_1726",
1734
+ "voice_1727",
1735
+ "voice_1728",
1736
+ "voice_1729",
1737
+ "voice_1730",
1738
+ "voice_1731",
1739
+ "voice_1732",
1740
+ "voice_1733",
1741
+ "voice_1734",
1742
+ "voice_1735",
1743
+ "voice_1736",
1744
+ "voice_1737",
1745
+ "voice_1738",
1746
+ "voice_1739",
1747
+ "voice_1740",
1748
+ "voice_1741",
1749
+ "voice_1742",
1750
+ "voice_1743",
1751
+ "voice_1744",
1752
+ "voice_1745",
1753
+ "voice_1746",
1754
+ "voice_1747",
1755
+ "voice_1748",
1756
+ "voice_1749",
1757
+ "voice_1750",
1758
+ "voice_1751",
1759
+ "voice_1752",
1760
+ "voice_1753",
1761
+ "voice_1754",
1762
+ "voice_1755",
1763
+ "voice_1756",
1764
+ "voice_1757",
1765
+ "voice_1758",
1766
+ "voice_1759",
1767
+ "voice_1760",
1768
+ "voice_1761",
1769
+ "voice_1762",
1770
+ "voice_1763",
1771
+ "voice_1764",
1772
+ "voice_1765",
1773
+ "voice_1766",
1774
+ "voice_1767",
1775
+ "voice_1768",
1776
+ "voice_1769",
1777
+ "voice_1770",
1778
+ "voice_1771",
1779
+ "voice_1772",
1780
+ "voice_1773",
1781
+ "voice_1774",
1782
+ "voice_1775",
1783
+ "voice_1776",
1784
+ "voice_1777",
1785
+ "voice_1778",
1786
+ "voice_1779",
1787
+ "voice_1780",
1788
+ "voice_1781",
1789
+ "voice_1782",
1790
+ "voice_1783",
1791
+ "voice_1784",
1792
+ "voice_1785",
1793
+ "voice_1786",
1794
+ "voice_1787",
1795
+ "voice_1788",
1796
+ "voice_1789",
1797
+ "voice_1790",
1798
+ "voice_1791",
1799
+ "voice_1792",
1800
+ "voice_1793",
1801
+ "voice_1794",
1802
+ "voice_1795",
1803
+ "voice_1796",
1804
+ "voice_1797",
1805
+ "voice_1798",
1806
+ "voice_1799",
1807
+ "voice_1800",
1808
+ "voice_1801",
1809
+ "voice_1802",
1810
+ "voice_1803",
1811
+ "voice_1804",
1812
+ "voice_1805",
1813
+ "voice_1806",
1814
+ "voice_1807",
1815
+ "voice_1808",
1816
+ "voice_1809",
1817
+ "voice_1810",
1818
+ "voice_1811",
1819
+ "voice_1812",
1820
+ "voice_1813",
1821
+ "voice_1814",
1822
+ "voice_1815",
1823
+ "voice_1816",
1824
+ "voice_1817",
1825
+ "voice_1818",
1826
+ "voice_1819",
1827
+ "voice_1820",
1828
+ "voice_1821",
1829
+ "voice_1822",
1830
+ "voice_1823",
1831
+ "voice_1824",
1832
+ "voice_1825",
1833
+ "voice_1826",
1834
+ "voice_1827",
1835
+ "voice_1828",
1836
+ "voice_1829",
1837
+ "voice_1830",
1838
+ "voice_1831",
1839
+ "voice_1832",
1840
+ "voice_1833",
1841
+ "voice_1834",
1842
+ "voice_1835",
1843
+ "voice_1836",
1844
+ "voice_1837",
1845
+ "voice_1838",
1846
+ "voice_1839",
1847
+ "voice_1840",
1848
+ "voice_1841",
1849
+ "voice_1842",
1850
+ "voice_1843",
1851
+ "voice_1844",
1852
+ "voice_1845",
1853
+ "voice_1846",
1854
+ "voice_1847",
1855
+ "voice_1848",
1856
+ "voice_1849",
1857
+ "voice_1850",
1858
+ "voice_1851",
1859
+ "voice_1852",
1860
+ "voice_1853",
1861
+ "voice_1854",
1862
+ "voice_1855",
1863
+ "voice_1856",
1864
+ "voice_1857",
1865
+ "voice_1858",
1866
+ "voice_1859",
1867
+ "voice_1860",
1868
+ "voice_1861",
1869
+ "voice_1862",
1870
+ "voice_1863",
1871
+ "voice_1864",
1872
+ "voice_1865",
1873
+ "voice_1866",
1874
+ "voice_1867",
1875
+ "voice_1868",
1876
+ "voice_1869",
1877
+ "voice_1870",
1878
+ "voice_1871",
1879
+ "voice_1872",
1880
+ "voice_1873",
1881
+ "voice_1874",
1882
+ "voice_1875",
1883
+ "voice_1876",
1884
+ "voice_1877",
1885
+ "voice_1878",
1886
+ "voice_1879",
1887
+ "voice_1880",
1888
+ "voice_1881",
1889
+ "voice_1882",
1890
+ "voice_1883",
1891
+ "voice_1884",
1892
+ "voice_1885",
1893
+ "voice_1886",
1894
+ "voice_1887",
1895
+ "voice_1888",
1896
+ "voice_1889",
1897
+ "voice_1890",
1898
+ "voice_1891",
1899
+ "voice_1892",
1900
+ "voice_1893",
1901
+ "voice_1894",
1902
+ "voice_1895",
1903
+ "voice_1896",
1904
+ "voice_1897",
1905
+ "voice_1898",
1906
+ "voice_1899",
1907
+ "voice_1900",
1908
+ "voice_1901",
1909
+ "voice_1902",
1910
+ "voice_1903",
1911
+ "voice_1904",
1912
+ "voice_1905",
1913
+ "voice_1906",
1914
+ "voice_1907",
1915
+ "voice_1908",
1916
+ "voice_1909",
1917
+ "voice_1910",
1918
+ "voice_1911",
1919
+ "voice_1912",
1920
+ "voice_1913",
1921
+ "voice_1914",
1922
+ "voice_1915",
1923
+ "voice_1916",
1924
+ "voice_1917",
1925
+ "voice_1918",
1926
+ "voice_1919",
1927
+ "voice_1920",
1928
+ "voice_1921",
1929
+ "voice_1922",
1930
+ "voice_1923",
1931
+ "voice_1924",
1932
+ "voice_1925",
1933
+ "voice_1926",
1934
+ "voice_1927",
1935
+ "voice_1928",
1936
+ "voice_1929",
1937
+ "voice_1930",
1938
+ "voice_1931",
1939
+ "voice_1932",
1940
+ "voice_1933",
1941
+ "voice_1934",
1942
+ "voice_1935",
1943
+ "voice_1936",
1944
+ "voice_1937",
1945
+ "voice_1938",
1946
+ "voice_1939",
1947
+ "voice_1940",
1948
+ "voice_1941",
1949
+ "voice_1942",
1950
+ "voice_1943",
1951
+ "voice_1944",
1952
+ "voice_1945",
1953
+ "voice_1946",
1954
+ "voice_1947",
1955
+ "voice_1948",
1956
+ "voice_1949",
1957
+ "voice_1950",
1958
+ "voice_1951",
1959
+ "voice_1952",
1960
+ "voice_1953",
1961
+ "voice_1954",
1962
+ "voice_1955",
1963
+ "voice_1956",
1964
+ "voice_1957",
1965
+ "voice_1958",
1966
+ "voice_1959",
1967
+ "voice_1960",
1968
+ "voice_1961",
1969
+ "voice_1962",
1970
+ "voice_1963",
1971
+ "voice_1964",
1972
+ "voice_1965",
1973
+ "voice_1966",
1974
+ "voice_1967",
1975
+ "voice_1968",
1976
+ "voice_1969",
1977
+ "voice_1970",
1978
+ "voice_1971",
1979
+ "voice_1972",
1980
+ "voice_1973",
1981
+ "voice_1974",
1982
+ "voice_1975",
1983
+ "voice_1976",
1984
+ "voice_1977",
1985
+ "voice_1978",
1986
+ "voice_1979",
1987
+ "voice_1980",
1988
+ "voice_1981",
1989
+ "voice_1982",
1990
+ "voice_1983",
1991
+ "voice_1984",
1992
+ "voice_1985",
1993
+ "voice_1986",
1994
+ "voice_1987",
1995
+ "voice_1988",
1996
+ "voice_1989",
1997
+ "voice_1990",
1998
+ "voice_1991",
1999
+ "voice_1992",
2000
+ "voice_1993",
2001
+ "voice_1994",
2002
+ "voice_1995",
2003
+ "voice_1996",
2004
+ "voice_1997",
2005
+ "voice_1998",
2006
+ "voice_1999",
2007
+ "voice_2000",
2008
+ "voice_2001",
2009
+ "voice_2002",
2010
+ "voice_2003",
2011
+ "voice_2004",
2012
+ "voice_2005",
2013
+ "voice_2006",
2014
+ "voice_2007",
2015
+ "voice_2008",
2016
+ "voice_2009",
2017
+ "voice_2010",
2018
+ "voice_2011",
2019
+ "voice_2012",
2020
+ "voice_2013",
2021
+ "voice_2014",
2022
+ "voice_2015",
2023
+ "voice_2016",
2024
+ "voice_2017",
2025
+ "voice_2018",
2026
+ "voice_2019",
2027
+ "voice_2020",
2028
+ "voice_2021",
2029
+ "voice_2022",
2030
+ "voice_2023",
2031
+ "voice_2024",
2032
+ "voice_2025",
2033
+ "voice_2026",
2034
+ "voice_2027",
2035
+ "voice_2028",
2036
+ "voice_2029",
2037
+ "voice_2030",
2038
+ "voice_2031",
2039
+ "voice_2032",
2040
+ "voice_2033",
2041
+ "voice_2034",
2042
+ "voice_2035",
2043
+ "voice_2036",
2044
+ "voice_2037",
2045
+ "voice_2038",
2046
+ "voice_2039",
2047
+ "voice_2040",
2048
+ "voice_2041",
2049
+ "voice_2042",
2050
+ "voice_2043",
2051
+ "voice_2044",
2052
+ "voice_2045",
2053
+ "voice_2046",
2054
+ "voice_2047",
2055
+ "voice_2048",
2056
+ "voice_2049",
2057
+ "voice_2050",
2058
+ "voice_2051",
2059
+ "voice_2052",
2060
+ "voice_2053",
2061
+ "voice_2054",
2062
+ "voice_2055",
2063
+ "voice_2056",
2064
+ "voice_2057",
2065
+ "voice_2058",
2066
+ "voice_2059",
2067
+ "voice_2060",
2068
+ "voice_2061",
2069
+ "voice_2062",
2070
+ "voice_2063",
2071
+ "voice_2064",
2072
+ "voice_2065",
2073
+ "voice_2066",
2074
+ "voice_2067",
2075
+ "voice_2068",
2076
+ "voice_2069",
2077
+ "voice_2070",
2078
+ "voice_2071",
2079
+ "voice_2072",
2080
+ "voice_2073",
2081
+ "voice_2074",
2082
+ "voice_2075",
2083
+ "voice_2076",
2084
+ "voice_2077",
2085
+ "voice_2078",
2086
+ "voice_2079",
2087
+ "voice_2080",
2088
+ "voice_2081",
2089
+ "voice_2082",
2090
+ "voice_2083",
2091
+ "voice_2084",
2092
+ "voice_2085",
2093
+ "voice_2086",
2094
+ "voice_2087",
2095
+ "voice_2088",
2096
+ "voice_2089",
2097
+ "voice_2090",
2098
+ "voice_2091",
2099
+ "voice_2092",
2100
+ "voice_2093",
2101
+ "voice_2094",
2102
+ "voice_2095",
2103
+ "voice_2096",
2104
+ "voice_2097",
2105
+ "voice_2098",
2106
+ "voice_2099",
2107
+ "voice_2100",
2108
+ "voice_2101",
2109
+ "voice_2102",
2110
+ "voice_2103",
2111
+ "voice_2104",
2112
+ "voice_2105",
2113
+ "voice_2106",
2114
+ "voice_2107",
2115
+ "voice_2108",
2116
+ "voice_2109",
2117
+ "voice_2110",
2118
+ "voice_2111",
2119
+ "voice_2112",
2120
+ "voice_2113",
2121
+ "voice_2114",
2122
+ "voice_2115",
2123
+ "voice_2116",
2124
+ "voice_2117",
2125
+ "voice_2118",
2126
+ "voice_2119",
2127
+ "voice_2120",
2128
+ "voice_2121",
2129
+ "voice_2122",
2130
+ "voice_2123",
2131
+ "voice_2124",
2132
+ "voice_2125",
2133
+ "voice_2126",
2134
+ "voice_2127",
2135
+ "voice_2128",
2136
+ "voice_2129",
2137
+ "voice_2130",
2138
+ "voice_2131",
2139
+ "voice_2132",
2140
+ "voice_2133",
2141
+ "voice_2134",
2142
+ "voice_2135",
2143
+ "voice_2136",
2144
+ "voice_2137",
2145
+ "voice_2138",
2146
+ "voice_2139",
2147
+ "voice_2140",
2148
+ "voice_2141",
2149
+ "voice_2142",
2150
+ "voice_2143",
2151
+ "voice_2144",
2152
+ "voice_2145",
2153
+ "voice_2146",
2154
+ "voice_2147",
2155
+ "voice_2148",
2156
+ "voice_2149",
2157
+ "voice_2150",
2158
+ "voice_2151",
2159
+ "voice_2152",
2160
+ "voice_2153",
2161
+ "voice_2154",
2162
+ "voice_2155",
2163
+ "voice_2156",
2164
+ "voice_2157",
2165
+ "voice_2158",
2166
+ "voice_2159",
2167
+ "voice_2160",
2168
+ "voice_2161",
2169
+ "voice_2162",
2170
+ "voice_2163",
2171
+ "voice_2164",
2172
+ "voice_2165",
2173
+ "voice_2166",
2174
+ "voice_2167",
2175
+ "voice_2168",
2176
+ "voice_2169",
2177
+ "voice_2170",
2178
+ "voice_2171",
2179
+ "voice_2172",
2180
+ "voice_2173",
2181
+ "voice_2174",
2182
+ "voice_2175",
2183
+ "voice_2176",
2184
+ "voice_2177",
2185
+ "voice_2178",
2186
+ "voice_2179",
2187
+ "voice_2180",
2188
+ "voice_2181",
2189
+ "voice_2182",
2190
+ "voice_2183",
2191
+ "voice_2184",
2192
+ "voice_2185",
2193
+ "voice_2186",
2194
+ "voice_2187",
2195
+ "voice_2188",
2196
+ "voice_2189",
2197
+ "voice_2190",
2198
+ "voice_2191",
2199
+ "voice_2192",
2200
+ "voice_2193",
2201
+ "voice_2194",
2202
+ "voice_2195",
2203
+ "voice_2196",
2204
+ "voice_2197",
2205
+ "voice_2198",
2206
+ "voice_2199",
2207
+ "voice_2200",
2208
+ "voice_2201",
2209
+ "voice_2202",
2210
+ "voice_2203",
2211
+ "voice_2204",
2212
+ "voice_2205",
2213
+ "voice_2206",
2214
+ "voice_2207",
2215
+ "voice_2208",
2216
+ "voice_2209",
2217
+ "voice_2210",
2218
+ "voice_2211",
2219
+ "voice_2212",
2220
+ "voice_2213",
2221
+ "voice_2214",
2222
+ "voice_2215",
2223
+ "voice_2216",
2224
+ "voice_2217",
2225
+ "voice_2218",
2226
+ "voice_2219",
2227
+ "voice_2220",
2228
+ "voice_2221",
2229
+ "voice_2222",
2230
+ "voice_2223",
2231
+ "voice_2224",
2232
+ "voice_2225",
2233
+ "voice_2226",
2234
+ "voice_2227",
2235
+ "voice_2228",
2236
+ "voice_2229",
2237
+ "voice_2230",
2238
+ "voice_2231",
2239
+ "voice_2232",
2240
+ "voice_2233",
2241
+ "voice_2234",
2242
+ "voice_2235",
2243
+ "voice_2236",
2244
+ "voice_2237",
2245
+ "voice_2238",
2246
+ "voice_2239",
2247
+ "voice_2240",
2248
+ "voice_2241",
2249
+ "voice_2242",
2250
+ "voice_2243",
2251
+ "voice_2244",
2252
+ "voice_2245",
2253
+ "voice_2246",
2254
+ "voice_2247",
2255
+ "voice_2248",
2256
+ "voice_2249",
2257
+ "voice_2250",
2258
+ "voice_2251",
2259
+ "voice_2252",
2260
+ "voice_2253",
2261
+ "voice_2254",
2262
+ "voice_2255",
2263
+ "voice_2256",
2264
+ "voice_2257",
2265
+ "voice_2258",
2266
+ "voice_2259",
2267
+ "voice_2260",
2268
+ "voice_2261",
2269
+ "voice_2262",
2270
+ "voice_2263",
2271
+ "voice_2264",
2272
+ "voice_2265",
2273
+ "voice_2266",
2274
+ "voice_2267",
2275
+ "voice_2268",
2276
+ "voice_2269",
2277
+ "voice_2270",
2278
+ "voice_2271",
2279
+ "voice_2272",
2280
+ "voice_2273",
2281
+ "voice_2274",
2282
+ "voice_2275",
2283
+ "voice_2276",
2284
+ "voice_2277",
2285
+ "voice_2278",
2286
+ "voice_2279",
2287
+ "voice_2280",
2288
+ "voice_2281",
2289
+ "voice_2282",
2290
+ "voice_2283",
2291
+ "voice_2284",
2292
+ "voice_2285",
2293
+ "voice_2286",
2294
+ "voice_2287",
2295
+ "voice_2288",
2296
+ "voice_2289",
2297
+ "voice_2290",
2298
+ "voice_2291",
2299
+ "voice_2292",
2300
+ "voice_2293",
2301
+ "voice_2294",
2302
+ "voice_2295",
2303
+ "voice_2296",
2304
+ "voice_2297",
2305
+ "voice_2298",
2306
+ "voice_2299",
2307
+ "voice_2300",
2308
+ "voice_2301",
2309
+ "voice_2302",
2310
+ "voice_2303",
2311
+ "voice_2304",
2312
+ "voice_2305",
2313
+ "voice_2306",
2314
+ "voice_2307",
2315
+ "voice_2308",
2316
+ "voice_2309",
2317
+ "voice_2310",
2318
+ "voice_2311",
2319
+ "voice_2312",
2320
+ "voice_2313",
2321
+ "voice_2314",
2322
+ "voice_2315",
2323
+ "voice_2316",
2324
+ "voice_2317",
2325
+ "voice_2318",
2326
+ "voice_2319",
2327
+ "voice_2320",
2328
+ "voice_2321",
2329
+ "voice_2322",
2330
+ "voice_2323",
2331
+ "voice_2324",
2332
+ "voice_2325",
2333
+ "voice_2326",
2334
+ "voice_2327",
2335
+ "voice_2328",
2336
+ "voice_2329",
2337
+ "voice_2330",
2338
+ "voice_2331",
2339
+ "voice_2332",
2340
+ "voice_2333",
2341
+ "voice_2334",
2342
+ "voice_2335",
2343
+ "voice_2336",
2344
+ "voice_2337",
2345
+ "voice_2338",
2346
+ "voice_2339",
2347
+ "voice_2340",
2348
+ "voice_2341",
2349
+ "voice_2342",
2350
+ "voice_2343",
2351
+ "voice_2344",
2352
+ "voice_2345",
2353
+ "voice_2346",
2354
+ "voice_2347",
2355
+ "voice_2348",
2356
+ "voice_2349",
2357
+ "voice_2350",
2358
+ "voice_2351",
2359
+ "voice_2352",
2360
+ "voice_2353",
2361
+ "voice_2354",
2362
+ "voice_2355",
2363
+ "voice_2356",
2364
+ "voice_2357",
2365
+ "voice_2358",
2366
+ "voice_2359",
2367
+ "voice_2360",
2368
+ "voice_2361",
2369
+ "voice_2362",
2370
+ "voice_2363",
2371
+ "voice_2364",
2372
+ "voice_2365",
2373
+ "voice_2366",
2374
+ "voice_2367",
2375
+ "voice_2368",
2376
+ "voice_2369",
2377
+ "voice_2370",
2378
+ "voice_2371",
2379
+ "voice_2372",
2380
+ "voice_2373",
2381
+ "voice_2374",
2382
+ "voice_2375",
2383
+ "voice_2376",
2384
+ "voice_2377",
2385
+ "voice_2378",
2386
+ "voice_2379",
2387
+ "voice_2380",
2388
+ "voice_2381",
2389
+ "voice_2382",
2390
+ "voice_2383",
2391
+ "voice_2384",
2392
+ "voice_2385",
2393
+ "voice_2386",
2394
+ "voice_2387",
2395
+ "voice_2388",
2396
+ "voice_2389",
2397
+ "voice_2390",
2398
+ "voice_2391",
2399
+ "voice_2392",
2400
+ "voice_2393",
2401
+ "voice_2394",
2402
+ "voice_2395",
2403
+ "voice_2396",
2404
+ "voice_2397",
2405
+ "voice_2398",
2406
+ "voice_2399",
2407
+ "voice_2400",
2408
+ "voice_2401",
2409
+ "voice_2402",
2410
+ "voice_2403",
2411
+ "voice_2404",
2412
+ "voice_2405",
2413
+ "voice_2406",
2414
+ "voice_2407",
2415
+ "voice_2408",
2416
+ "voice_2409",
2417
+ "voice_2410",
2418
+ "voice_2411",
2419
+ "voice_2412",
2420
+ "voice_2413",
2421
+ "voice_2414",
2422
+ "voice_2415",
2423
+ "voice_2416",
2424
+ "voice_2417",
2425
+ "voice_2418",
2426
+ "voice_2419",
2427
+ "voice_2420",
2428
+ "voice_2421",
2429
+ "voice_2422",
2430
+ "voice_2423",
2431
+ "voice_2424",
2432
+ "voice_2425",
2433
+ "voice_2426",
2434
+ "voice_2427",
2435
+ "voice_2428",
2436
+ "voice_2429",
2437
+ "voice_2430",
2438
+ "voice_2431",
2439
+ "voice_2432",
2440
+ "voice_2433",
2441
+ "voice_2434",
2442
+ "voice_2435",
2443
+ "voice_2436",
2444
+ "voice_2437",
2445
+ "voice_2438",
2446
+ "voice_2439",
2447
+ "voice_2440",
2448
+ "voice_2441",
2449
+ "voice_2442",
2450
+ "voice_2443",
2451
+ "voice_2444",
2452
+ "voice_2445",
2453
+ "voice_2446",
2454
+ "voice_2447",
2455
+ "voice_2448",
2456
+ "voice_2449",
2457
+ "voice_2450",
2458
+ "voice_2451",
2459
+ "voice_2452",
2460
+ "voice_2453",
2461
+ "voice_2454",
2462
+ "voice_2455",
2463
+ "voice_2456",
2464
+ "voice_2457",
2465
+ "voice_2458",
2466
+ "voice_2459",
2467
+ "voice_2460",
2468
+ "voice_2461",
2469
+ "voice_2462",
2470
+ "voice_2463",
2471
+ "voice_2464",
2472
+ "voice_2465",
2473
+ "voice_2466",
2474
+ "voice_2467",
2475
+ "voice_2468",
2476
+ "voice_2469",
2477
+ "voice_2470",
2478
+ "voice_2471",
2479
+ "voice_2472",
2480
+ "voice_2473",
2481
+ "voice_2474",
2482
+ "voice_2475",
2483
+ "voice_2476",
2484
+ "voice_2477",
2485
+ "voice_2478",
2486
+ "voice_2479",
2487
+ "voice_2480",
2488
+ "voice_2481",
2489
+ "voice_2482",
2490
+ "voice_2483",
2491
+ "voice_2484",
2492
+ "voice_2485",
2493
+ "voice_2486",
2494
+ "voice_2487",
2495
+ "voice_2488",
2496
+ "voice_2489",
2497
+ "voice_2490",
2498
+ "voice_2491",
2499
+ "voice_2492",
2500
+ "voice_2493",
2501
+ "voice_2494",
2502
+ "voice_2495",
2503
+ "voice_2496",
2504
+ "voice_2497",
2505
+ "voice_2498",
2506
+ "voice_2499",
2507
+ "voice_2500",
2508
+ "voice_2501",
2509
+ "voice_2502",
2510
+ "voice_2503",
2511
+ "voice_2504",
2512
+ "voice_2505",
2513
+ "voice_2506",
2514
+ "voice_2507",
2515
+ "voice_2508",
2516
+ "voice_2509",
2517
+ "voice_2510",
2518
+ "voice_2511",
2519
+ "voice_2512",
2520
+ "voice_2513",
2521
+ "voice_2514",
2522
+ "voice_2515",
2523
+ "voice_2516",
2524
+ "voice_2517",
2525
+ "voice_2518",
2526
+ "voice_2519",
2527
+ "voice_2520",
2528
+ "voice_2521",
2529
+ "voice_2522",
2530
+ "voice_2523",
2531
+ "voice_2524",
2532
+ "voice_2525",
2533
+ "voice_2526",
2534
+ "voice_2527",
2535
+ "voice_2528",
2536
+ "voice_2529",
2537
+ "voice_2530",
2538
+ "voice_2531",
2539
+ "voice_2532",
2540
+ "voice_2533",
2541
+ "voice_2534",
2542
+ "voice_2535",
2543
+ "voice_2536",
2544
+ "voice_2537",
2545
+ "voice_2538",
2546
+ "voice_2539",
2547
+ "voice_2540",
2548
+ "voice_2541",
2549
+ "voice_2542",
2550
+ "voice_2543",
2551
+ "voice_2544",
2552
+ "voice_2545",
2553
+ "voice_2546",
2554
+ "voice_2547",
2555
+ "voice_2548",
2556
+ "voice_2549",
2557
+ "voice_2550",
2558
+ "voice_2551",
2559
+ "voice_2552",
2560
+ "voice_2553",
2561
+ "voice_2554",
2562
+ "voice_2555",
2563
+ "voice_2556",
2564
+ "voice_2557",
2565
+ "voice_2558",
2566
+ "voice_2559",
2567
+ "voice_2560",
2568
+ "voice_2561",
2569
+ "voice_2562",
2570
+ "voice_2563",
2571
+ "voice_2564",
2572
+ "voice_2565",
2573
+ "voice_2566",
2574
+ "voice_2567",
2575
+ "voice_2568",
2576
+ "voice_2569",
2577
+ "voice_2570",
2578
+ "voice_2571",
2579
+ "voice_2572",
2580
+ "voice_2573",
2581
+ "voice_2574",
2582
+ "voice_2575",
2583
+ "voice_2576",
2584
+ "voice_2577",
2585
+ "voice_2578",
2586
+ "voice_2579",
2587
+ "voice_2580",
2588
+ "voice_2581",
2589
+ "voice_2582",
2590
+ "voice_2583",
2591
+ "voice_2584",
2592
+ "voice_2585",
2593
+ "voice_2586",
2594
+ "voice_2587",
2595
+ "voice_2588",
2596
+ "voice_2589",
2597
+ "voice_2590",
2598
+ "voice_2591",
2599
+ "voice_2592",
2600
+ "voice_2593",
2601
+ "voice_2594",
2602
+ "voice_2595",
2603
+ "voice_2596",
2604
+ "voice_2597",
2605
+ "voice_2598",
2606
+ "voice_2599",
2607
+ "voice_2600",
2608
+ "voice_2601",
2609
+ "voice_2602",
2610
+ "voice_2603",
2611
+ "voice_2604",
2612
+ "voice_2605",
2613
+ "voice_2606",
2614
+ "voice_2607",
2615
+ "voice_2608",
2616
+ "voice_2609",
2617
+ "voice_2610",
2618
+ "voice_2611",
2619
+ "voice_2612",
2620
+ "voice_2613",
2621
+ "voice_2614",
2622
+ "voice_2615",
2623
+ "voice_2616",
2624
+ "voice_2617",
2625
+ "voice_2618",
2626
+ "voice_2619",
2627
+ "voice_2620",
2628
+ "voice_2621",
2629
+ "voice_2622",
2630
+ "voice_2623",
2631
+ "voice_2624",
2632
+ "voice_2625",
2633
+ "voice_2626",
2634
+ "voice_2627",
2635
+ "voice_2628",
2636
+ "voice_2629",
2637
+ "voice_2630",
2638
+ "voice_2631",
2639
+ "voice_2632",
2640
+ "voice_2633",
2641
+ "voice_2634",
2642
+ "voice_2635",
2643
+ "voice_2636",
2644
+ "voice_2637",
2645
+ "voice_2638",
2646
+ "voice_2639",
2647
+ "voice_2640",
2648
+ "voice_2641",
2649
+ "voice_2642",
2650
+ "voice_2643",
2651
+ "voice_2644",
2652
+ "voice_2645",
2653
+ "voice_2646",
2654
+ "voice_2647",
2655
+ "voice_2648",
2656
+ "voice_2649",
2657
+ "voice_2650",
2658
+ "voice_2651",
2659
+ "voice_2652",
2660
+ "voice_2653",
2661
+ "voice_2654",
2662
+ "voice_2655",
2663
+ "voice_2656",
2664
+ "voice_2657",
2665
+ "voice_2658",
2666
+ "voice_2659",
2667
+ "voice_2660",
2668
+ "voice_2661",
2669
+ "voice_2662",
2670
+ "voice_2663",
2671
+ "voice_2664",
2672
+ "voice_2665",
2673
+ "voice_2666",
2674
+ "voice_2667",
2675
+ "voice_2668",
2676
+ "voice_2669",
2677
+ "voice_2670",
2678
+ "voice_2671",
2679
+ "voice_2672",
2680
+ "voice_2673",
2681
+ "voice_2674",
2682
+ "voice_2675",
2683
+ "voice_2676",
2684
+ "voice_2677",
2685
+ "voice_2678",
2686
+ "voice_2679",
2687
+ "voice_2680",
2688
+ "voice_2681",
2689
+ "voice_2682",
2690
+ "voice_2683",
2691
+ "voice_2684",
2692
+ "voice_2685",
2693
+ "voice_2686",
2694
+ "voice_2687",
2695
+ "voice_2688",
2696
+ "voice_2689",
2697
+ "voice_2690",
2698
+ "voice_2691",
2699
+ "voice_2692",
2700
+ "voice_2693",
2701
+ "voice_2694",
2702
+ "voice_2695",
2703
+ "voice_2696",
2704
+ "voice_2697",
2705
+ "voice_2698",
2706
+ "voice_2699",
2707
+ "voice_2700",
2708
+ "voice_2701",
2709
+ "voice_2702",
2710
+ "voice_2703",
2711
+ "voice_2704",
2712
+ "voice_2705",
2713
+ "voice_2706",
2714
+ "voice_2707",
2715
+ "voice_2708",
2716
+ "voice_2709",
2717
+ "voice_2710",
2718
+ "voice_2711",
2719
+ "voice_2712",
2720
+ "voice_2713",
2721
+ "voice_2714",
2722
+ "voice_2715",
2723
+ "voice_2716",
2724
+ "voice_2717",
2725
+ "voice_2718",
2726
+ "voice_2719",
2727
+ "voice_2720",
2728
+ "voice_2721",
2729
+ "voice_2722",
2730
+ "voice_2723",
2731
+ "voice_2724",
2732
+ "voice_2725",
2733
+ "voice_2726",
2734
+ "voice_2727",
2735
+ "voice_2728",
2736
+ "voice_2729",
2737
+ "voice_2730",
2738
+ "voice_2731",
2739
+ "voice_2732",
2740
+ "voice_2733",
2741
+ "voice_2734",
2742
+ "voice_2735",
2743
+ "voice_2736",
2744
+ "voice_2737",
2745
+ "voice_2738",
2746
+ "voice_2739",
2747
+ "voice_2740",
2748
+ "voice_2741",
2749
+ "voice_2742",
2750
+ "voice_2743",
2751
+ "voice_2744",
2752
+ "voice_2745",
2753
+ "voice_2746",
2754
+ "voice_2747",
2755
+ "voice_2748",
2756
+ "voice_2749",
2757
+ "voice_2750",
2758
+ "voice_2751",
2759
+ "voice_2752",
2760
+ "voice_2753",
2761
+ "voice_2754",
2762
+ "voice_2755",
2763
+ "voice_2756",
2764
+ "voice_2757",
2765
+ "voice_2758",
2766
+ "voice_2759",
2767
+ "voice_2760",
2768
+ "voice_2761",
2769
+ "voice_2762",
2770
+ "voice_2763",
2771
+ "voice_2764",
2772
+ "voice_2765",
2773
+ "voice_2766",
2774
+ "voice_2767",
2775
+ "voice_2768",
2776
+ "voice_2769",
2777
+ "voice_2770",
2778
+ "voice_2771",
2779
+ "voice_2772",
2780
+ "voice_2773",
2781
+ "voice_2774",
2782
+ "voice_2775",
2783
+ "voice_2776",
2784
+ "voice_2777",
2785
+ "voice_2778",
2786
+ "voice_2779",
2787
+ "voice_2780",
2788
+ "voice_2781",
2789
+ "voice_2782",
2790
+ "voice_2783",
2791
+ "voice_2784",
2792
+ "voice_2785",
2793
+ "voice_2786",
2794
+ "voice_2787",
2795
+ "voice_2788",
2796
+ "voice_2789",
2797
+ "voice_2790",
2798
+ "voice_2791",
2799
+ "voice_2792",
2800
+ "voice_2793",
2801
+ "voice_2794",
2802
+ "voice_2795",
2803
+ "voice_2796",
2804
+ "voice_2797",
2805
+ "voice_2798",
2806
+ "voice_2799",
2807
+ "voice_2800",
2808
+ "voice_2801",
2809
+ "voice_2802",
2810
+ "voice_2803",
2811
+ "voice_2804",
2812
+ "voice_2805",
2813
+ "voice_2806",
2814
+ "voice_2807",
2815
+ "voice_2808",
2816
+ "voice_2809",
2817
+ "voice_2810",
2818
+ "voice_2811",
2819
+ "voice_2812",
2820
+ "voice_2813",
2821
+ "voice_2814",
2822
+ "voice_2815",
2823
+ "voice_2816",
2824
+ "voice_2817",
2825
+ "voice_2818",
2826
+ "voice_2819",
2827
+ "voice_2820",
2828
+ "voice_2821",
2829
+ "voice_3226",
2830
+ "voice_3622",
2831
+ "voice_3695",
2832
+ "voice_4223"
2833
+ ],
2834
+ "hf_repo": null,
2835
+ "files": {
2836
+ "acoustic": "model.safetensors",
2837
+ "speaker_vectors": "speaker_vectors.safetensors",
2838
+ "voices": "voices.json",
2839
+ "control_stats": "control_stats.json"
2840
+ },
2841
+ "acoustic": {
2842
+ "step": 312500,
2843
+ "n_speakers": 4919,
2844
+ "n_tags": 51,
2845
+ "arch": {
2846
+ "enc_mixer": "hybrid",
2847
+ "dec_mixer": "hybrid",
2848
+ "ssm_state": 32,
2849
+ "spk_enc_dim": 512,
2850
+ "n_symbols": 118,
2851
+ "n_env": 30
2852
+ },
2853
+ "params": 9585639
2854
+ },
2855
+ "refiner": {
2856
+ "file": "flow.safetensors",
2857
+ "cfg": {
2858
+ "mean": -1.5826570987701416,
2859
+ "std": 2.13140869140625,
2860
+ "d": 384,
2861
+ "n_layers": 12,
2862
+ "g_dim": 128
2863
+ },
2864
+ "step": 100000,
2865
+ "params": 17642598,
2866
+ "steps": 10,
2867
+ "temp": 0.4
2868
+ },
2869
+ "prompted_flow": {
2870
+ "file": "pflow.safetensors",
2871
+ "cfg": {
2872
+ "mean": -1.5826570987701416,
2873
+ "std": 2.13140869140625,
2874
+ "d": 384,
2875
+ "n_layers": 12,
2876
+ "g_dim": 128,
2877
+ "prompt": true,
2878
+ "mu_cmn": true,
2879
+ "attn_at": [
2880
+ 1,
2881
+ 3,
2882
+ 5,
2883
+ 7,
2884
+ 9,
2885
+ 11
2886
+ ]
2887
+ },
2888
+ "step": 40000,
2889
+ "params": 24674406,
2890
+ "steps": 10,
2891
+ "temp": 0.4
2892
+ },
2893
+ "prompt_adapter": {
2894
+ "file": "prompt_adapter.safetensors",
2895
+ "tags": [
2896
+ "none",
2897
+ "adoration",
2898
+ "amazement",
2899
+ "amusement",
2900
+ "anger",
2901
+ "anxiety",
2902
+ "apologetic",
2903
+ "bored",
2904
+ "confidence",
2905
+ "confusion",
2906
+ "contempt",
2907
+ "contentment",
2908
+ "cuteness",
2909
+ "desire",
2910
+ "determined",
2911
+ "disappointment",
2912
+ "disgust",
2913
+ "distress",
2914
+ "ecstasy",
2915
+ "embarrassment",
2916
+ "emphasis",
2917
+ "enunciated",
2918
+ "excited",
2919
+ "fast",
2920
+ "fear",
2921
+ "frustration",
2922
+ "guilt",
2923
+ "happiness",
2924
+ "hesitant",
2925
+ "high_pitch",
2926
+ "interest",
2927
+ "irritation",
2928
+ "laughing",
2929
+ "loud",
2930
+ "low_pitch",
2931
+ "mysterious",
2932
+ "neutral",
2933
+ "nostalgia",
2934
+ "pain",
2935
+ "panic",
2936
+ "pride",
2937
+ "realization",
2938
+ "relief",
2939
+ "sadness",
2940
+ "sarcasm",
2941
+ "serenity",
2942
+ "skeptical",
2943
+ "slow",
2944
+ "sympathy",
2945
+ "tired",
2946
+ "whisper"
2947
+ ],
2948
+ "encoder": "sentence-transformers/all-MiniLM-L6-v2",
2949
+ "params": 498251
2950
+ },
2951
+ "speaker_encoder": "microsoft/wavlm-base-plus-sv",
2952
+ "vocoder": "charactr/vocos-mel-24khz",
2953
+ "text_frontend": "g2p_en ARPAbet + rule-based normalisation (numbers, units, currency, acronyms)"
2954
+ }
control_stats.json ADDED
The diff for this file is too large to render. See raw diff
 
flow.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:585ff2c931811c30d0ec953a947dc23ffb305979471f5be5133a1f2b708c1e90
3
+ size 70580400
inference.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """SteerTTS examples. Run from the model directory (or set STEERTTS_MODEL) after `pip install ".[all]"`.
2
+
3
+ python inference.py # renders every example below into ./samples/
4
+ python inference.py --device cpu
5
+ """
6
+ import argparse
7
+ import json
8
+ import os
9
+ from pathlib import Path
10
+
11
+ from steertts import SteerTTS
12
+
13
+ HERE = Path(__file__).resolve().parent
14
+
15
+
16
+ def main():
17
+ ap = argparse.ArgumentParser()
18
+ ap.add_argument("--model", default=os.environ.get("STEERTTS_MODEL", str(HERE)))
19
+ ap.add_argument("--device", default=None)
20
+ ap.add_argument("--out", default="samples")
21
+ ap.add_argument("--ref-wav", help="a consenting speaker's recording, for the cloning example")
22
+ a = ap.parse_args()
23
+ out = Path(a.out); out.mkdir(exist_ok=True)
24
+ tts = SteerTTS.from_pretrained(a.model, device=a.device)
25
+ print(tts, f"{len(tts.voices)} voices, default {tts.default_voice}")
26
+
27
+ # 1. plain text, default voice
28
+ tts.save(tts("The meeting has been moved to Thursday afternoon at three."), out / "01_default.wav")
29
+ # 2. explicit controls (speaker-relative z): faster and higher, rising at the end
30
+ tts.save(tts("Are you coming with us tonight?", ctrl={"rate": 1.0, "f0_med": 1.0, "f0_end": 2.0}), out / "02_ctrl.wav")
31
+ # 3. a style tag plus a control, stronger steering via guidance
32
+ tts.save(tts("I really thought it would work this time.", tag="sadness", ctrl={"loud": -1.0}, guidance=1.5),
33
+ out / "03_tag_guidance.wav")
34
+ # 4. another voice (see voices.json / `steertts voices`)
35
+ v = tts.voices[42]
36
+ print(v, tts.voice_info[v]["description"])
37
+ tts.save(tts("Good evening, and welcome to the program.", voice=v), out / "04_other_voice.wav")
38
+ # 5. a free-text direction -> the request it maps to (needs the `prompt` extra)
39
+ req = tts.resolve({"text": "Please keep this between the two of us.", "prompt": "whispering, slow and secretive"})
40
+ print(json.dumps(req, indent=1))
41
+ tts.save(tts.synthesize(req), out / "05_prompt.wav")
42
+ # 6. inline markup, streamed sentence by sentence
43
+ text = "Well, *that* was fast. [pause 400ms] <slow>Let me think about it.</slow> <whisper>Don't tell anyone.</whisper>"
44
+ chunks = []
45
+ for c in tts.stream(text):
46
+ print(f" chunk {c.segment.tag:8s} {len(c.audio) / tts.sample_rate:.2f}s rendered in {c.synth_ms:.0f} ms")
47
+ chunks.append(c.audio)
48
+ import numpy as np
49
+ tts.save(np.concatenate(chunks), out / "06_markup_stream.wav")
50
+ # 7. per-word prosody without markup: word 1 ("never", 0-based) higher, louder and longer
51
+ tts.save(tts("I never said she stole it.", word_shift={1: {"pitch": 3, "energy": 4, "dur": 1.3}}), out / "07_word_shift.wav")
52
+ # 8. zero-shot cloning (needs the `clone` extra and a CONSENTING speaker's clip)
53
+ if a.ref_wav:
54
+ tts.save(tts("This is a cloned voice reading a new sentence.", ref_wav=a.ref_wav), out / "08_clone.wav")
55
+ print("wrote", sorted(p.name for p in out.glob("*.wav")))
56
+
57
+
58
+ if __name__ == "__main__":
59
+ main()
leaderboard/steertts/Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stage-1 image for the Open TTS Leaderboard (a public Docker Space, e.g. WhissleAI/evals-steertts).
2
+ # The eval scripts are injected at runtime; this only installs the model's dependencies.
3
+ FROM pytorch/pytorch:2.7.1-cuda12.8-cudnn9-runtime
4
+ RUN apt-get update && apt-get install -y --no-install-recommends libsndfile1 git && rm -rf /var/lib/apt/lists/*
5
+ # torchaudio must match the image's torch (vocos imports it)
6
+ RUN pip install --no-cache-dir "torchaudio==2.7.1" datasets tqdm \
7
+ "steertts[clone] @ git+https://huggingface.co/WhissleAI/steertts-v1"
8
+ # warm the caches the first job would otherwise download (vocoder, speaker encoder, NLTK data)
9
+ RUN python -c "from vocos import Vocos; Vocos.from_pretrained('charactr/vocos-mel-24khz')" && \
10
+ python -c "from steertts.text import to_phonemes; to_phonemes('warm up')" && \
11
+ python -c "from transformers import WavLMForXVector; WavLMForXVector.from_pretrained('microsoft/wavlm-base-plus-sv')"
12
+ WORKDIR /app
leaderboard/steertts/run_eval.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ TTS synthesis for the Open TTS Leaderboard (SteerTTS backend, stage 1).
3
+
4
+ SteerTTS is a 9.6M-parameter acoustic model + Vocos (23.1M total; +17.6M flow refiner by default, or the 24.7M
5
+ prompted flow when cloning), loaded in-process from the `steertts` package. English only, so only the `en` splits.
6
+ Default voice = the pre-registered `voice_1361` (no test-set voice selection); `--voice_clone` clones each sample's
7
+ `prompt_audio` (WavLM speaker vector + prompted flow; `prompt_text` is not used). Rows are synthesized one at a
8
+ time (no batched API); generation is timed after `--warmup_steps` untimed samples. `--ttfa_probe` times the first
9
+ sentence chunk of `SteerTTS.stream()` (sentence-level streaming).
10
+
11
+ PYTHONPATH=scripts python steertts/run_eval.py --model_id WhissleAI/steertts-v1 --dataset_path bezzam/seed_tts_eval \
12
+ --dataset tts --split en --device cuda:0 [--voice_clone] [--model_path /local/steertts-v1]
13
+ """
14
+
15
+ import argparse
16
+ import os
17
+ import time
18
+
19
+ import numpy as np
20
+ import soundfile as sf
21
+ import torch
22
+ from tqdm import tqdm
23
+
24
+ from steertts import SteerTTS
25
+ from run_eval_utils import (
26
+ add_common_args, load_done_entries, load_tts_dataset, manifest_entry, open_manifest, output_paths,
27
+ pending_batches, print_model_size, print_next_steps, set_seed, warm_up, wav_rel_path, write_entry,
28
+ )
29
+
30
+ SAMPLING_RATE = 24_000
31
+
32
+
33
+ def main(args):
34
+ set_seed(args.seed)
35
+ tts = SteerTTS.from_pretrained(args.model_path or args.model_id, device=args.device, flow=not args.no_flow)
36
+ voice = args.voice or tts.default_voice
37
+ print(f"Loaded {tts} voice={voice} voice_clone={args.voice_clone}")
38
+ print_model_size(tts.model, tts.vocos, tts.flow, tts.pflow if args.voice_clone else None)
39
+ is_cuda = str(tts.dev).startswith("cuda")
40
+
41
+ mode_suffix = "_voice_clone" if args.voice_clone else ""
42
+ paths = output_paths(args, mode_suffix)
43
+ model_dir, dataset_dir_name, output_dir = paths.model_dir, paths.dataset_dir_name, paths.output_dir
44
+ prompt_dir = os.path.join(output_dir, "prompts")
45
+ if args.voice_clone:
46
+ os.makedirs(prompt_dir, exist_ok=True)
47
+
48
+ def write_prompt(sample_id, prompt_audio, root):
49
+ prel = os.path.join(dataset_dir_name, "prompts", f"prompt_{sample_id}.wav")
50
+ ppath = os.path.join(root, prel)
51
+ os.makedirs(os.path.dirname(ppath), exist_ok=True)
52
+ if not os.path.exists(ppath):
53
+ sf.write(ppath, np.asarray(prompt_audio["array"], dtype=np.float32), prompt_audio["sampling_rate"])
54
+ return prel, ppath
55
+
56
+ def request(text, ref):
57
+ return dict(text=text, ref_wav=ref) if ref else dict(text=text, voice=voice)
58
+
59
+ def synth_one(text, ref=None):
60
+ if is_cuda:
61
+ torch.cuda.synchronize()
62
+ start = time.perf_counter()
63
+ audio = tts.synthesize(request(text, ref))
64
+ if is_cuda:
65
+ torch.cuda.synchronize()
66
+ return audio, time.perf_counter() - start
67
+
68
+ dataset = load_tts_dataset(args, extra_columns=("prompt_audio",) if args.voice_clone else ())
69
+
70
+ if args.ttfa_probe != 0:
71
+ from ttfa_probe import run_probe, sample_indices
72
+ import tempfile
73
+ tmp = tempfile.mkdtemp(prefix="ttfa_prompts_")
74
+ chunk_counts = []
75
+
76
+ def _gen(job, early_stop):
77
+ ref = write_prompt(job["id"][0], job["prompt_audio"][0], tmp)[1] if args.voice_clone else None
78
+ first, chunks = None, []
79
+ for c in tts.stream(request=request(job[args.text_column][0], ref)):
80
+ if first is None:
81
+ first = time.perf_counter()
82
+ chunks.append(c.audio)
83
+ if early_stop:
84
+ break
85
+ chunk_counts.append(len(chunks))
86
+ return (np.concatenate(chunks) if chunks else np.zeros(1, np.float32)), SAMPLING_RATE, first
87
+
88
+ columns = dataset.column_names
89
+ run_probe(_gen, [{c: [dataset[i][c]] for c in columns} for i in sample_indices(args.ttfa_probe, len(dataset))],
90
+ model_id=args.model_id, dataset_path=args.dataset_path, dataset_config=args.dataset, split=args.split,
91
+ out_dir=model_dir, mode_suffix=mode_suffix, n=args.ttfa_probe,
92
+ extra={"device": args.device, "note": "batch 1, SteerTTS.stream() sentence chunks",
93
+ "chunks_per_utterance": chunk_counts})
94
+ return
95
+
96
+ done_entries = load_done_entries(paths.manifest_path, args.resume)
97
+ manifest_file = open_manifest(paths.manifest_path, done_entries)
98
+ starts = pending_batches(dataset, 1, done_entries, dataset_dir_name)
99
+
100
+ def gen_batch(batch):
101
+ out = []
102
+ for k, text in enumerate(batch[args.text_column]):
103
+ ref = write_prompt(batch["id"][k], batch["prompt_audio"][k], model_dir)[1] if args.voice_clone else None
104
+ out.append(synth_one(text, ref))
105
+ return out
106
+
107
+ warm_up(gen_batch, dataset, starts, 1, args.warmup_steps)
108
+ set_seed(args.seed)
109
+ for i in tqdm(starts, desc="Generating"):
110
+ row = dataset[i]
111
+ prel = None
112
+ ref = None
113
+ if args.voice_clone:
114
+ prel, ref = write_prompt(row["id"], row["prompt_audio"], model_dir) # I/O outside the timed region
115
+ audio, elapsed = synth_one(row[args.text_column], ref)
116
+ rel_path = wav_rel_path(dataset_dir_name, row["id"])
117
+ sf.write(os.path.join(model_dir, rel_path), audio, SAMPLING_RATE)
118
+ write_entry(manifest_file, manifest_entry(rel_path, len(audio) / SAMPLING_RATE, elapsed, row[args.text_column],
119
+ prel if args.voice_clone else None))
120
+ manifest_file.flush()
121
+ manifest_file.close()
122
+ print("Results saved at path:", os.path.abspath(paths.manifest_path))
123
+ print_next_steps(args.voice_clone)
124
+
125
+
126
+ if __name__ == "__main__":
127
+ parser = argparse.ArgumentParser()
128
+ parser.add_argument("--model_id", type=str, default="WhissleAI/steertts-v1",
129
+ help="Name used for the results folder; also the HF repo loaded unless --model_path is set.")
130
+ parser.add_argument("--model_path", type=str, default=None, help="Local model directory (overrides --model_id).")
131
+ parser.add_argument("--voice", type=str, default=None, help="Built-in voice (default: the model's default_voice).")
132
+ parser.add_argument("--no_flow", action="store_true", help="Skip the flow refiner (faster, slightly hazier).")
133
+ add_common_args(parser, voice_clone=False)
134
+ args = parser.parse_args()
135
+ print("*" * 100)
136
+ print(f"Generating TTS with {args.model_id} on {args.dataset_path} / {args.dataset} / {args.split}")
137
+ print("*" * 100)
138
+ main(args)
leaderboard/steertts/submit_jobs.sh ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # HF Jobs TTS eval — SteerTTS backend (generate → transcribe → SIM* → score; *voice-clone only).
3
+ # Usage: HF_TOKEN=hf_... [VOICE_CLONE=true] bash steertts/submit_jobs.sh ["WhissleAI/steertts-v1" ...]
4
+ # English only: SteerTTS has an English (ARPAbet) front end, so only the `en` splits are configured.
5
+ source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/scripts/tts_jobs_common.sh"
6
+
7
+ TTS_SPACE="${TTS_SPACE:-WhissleAI/evals-steertts}" # public Docker Space built from steertts/Dockerfile (TO CREATE)
8
+ RUN_EVAL_INJECT="$(inject_run_eval)"
9
+ SUPPORTS_VOICE_CLONE=true
10
+ supports_language() { [[ "$1" == "en" ]]; }
11
+
12
+ generate_stage() {
13
+ hf jobs run \
14
+ --flavor "${FLAVOR}" --timeout 8h --secrets HF_TOKEN ${NAMESPACE_ARG} \
15
+ --volume "hf://buckets/${RESULTS_BUCKET}:/results" "${TTS_IMAGE}" \
16
+ bash -c "
17
+ ${RUN_EVAL_INJECT}
18
+ cd /app/steertts && python run_eval.py \
19
+ --model_id=${MODEL_ID} \
20
+ --dataset_path=${DATASET_PATH} \
21
+ --dataset=${DATASET} \
22
+ --split=${SPLIT} \
23
+ --device=cuda:0 \
24
+ --max_eval_samples=${MAX_EVAL_SAMPLES} \
25
+ ${VOICE_CLONE_FLAG} &&
26
+ mkdir -p /results &&
27
+ cp -r results/${MODEL_SAFE} /results/
28
+ "
29
+ }
30
+
31
+ DATASET_CONFIGS=(
32
+ "tts en en"
33
+ "zero_shot en en ${CV3_EVAL_PATH:-bezzam/cv3_eval}"
34
+ )
35
+
36
+ MODEL_CONFIGS=("WhissleAI/steertts-v1")
37
+ [[ $# -gt 0 ]] && MODEL_CONFIGS=("$@")
38
+
39
+ run_pipeline
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:adc11912871113e39d3dbbc2c5f7edfe0b9ec9bf7b98f0a926f7c9edfc07b17c
3
+ size 38395628
pflow.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:877f81523dfaba5061823f6e6543b83edd88c72a3df724666ce3ee74de259327
3
+ size 98711656
prompt_adapter.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aca34bf753456e469bae22f59217512f8cbe8efb326bfbede1529eeb9e817727
3
+ size 1993780
pyproject.toml ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "steertts"
7
+ version = "1.0.0"
8
+ description = "SteerTTS: a small, numerically steerable English TTS (hybrid attention + bidirectional Mamba-3)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "Code: MIT. Weights: CC BY-NC 4.0 (TBD by authors), see README.md" }
12
+ # Versions are the ones the release was verified with (fresh env, TEST_LOG.md). torch is a range because the right
13
+ # wheel depends on your CUDA / CPU setup; torchaudio (pulled in by vocos) must match the torch you install.
14
+ dependencies = [
15
+ "torch>=2.4,<2.12",
16
+ "numpy>=1.24,<2.5",
17
+ "safetensors==0.8.0",
18
+ "huggingface_hub>=0.23",
19
+ "vocos==0.1.0",
20
+ "g2p_en==2.1.0",
21
+ "nltk==3.10.3",
22
+ "num2words==0.5.14",
23
+ "soundfile==0.14.0",
24
+ "soxr==1.1.0",
25
+ ]
26
+
27
+ [project.optional-dependencies]
28
+ prompt = ["transformers==5.19.0"] # free-text directions (MiniLM sentence encoder)
29
+ clone = ["transformers==5.19.0"] # zero-shot cloning (WavLM speaker encoder)
30
+ measure = ["praat-parselmouth==0.4.7"] # read the 12 controls back from audio
31
+ all = ["transformers==5.19.0", "praat-parselmouth==0.4.7"]
32
+ test = ["transformers==5.19.0", "praat-parselmouth==0.4.7", "pytest>=8", "jsonschema>=4.20"]
33
+
34
+ [project.scripts]
35
+ steertts = "steertts.cli:main"
36
+
37
+ [tool.setuptools]
38
+ packages = ["steertts", "steertts.mamba3"]
39
+
40
+ [tool.setuptools.package-data]
41
+ steertts = ["request.schema.json"]
42
+
43
+ [tool.pytest.ini_options]
44
+ testpaths = ["tests"]
request.schema.json ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:steertts:request-schema:v1",
4
+ "title": "SteerTTS synthesis request",
5
+ "description": "One request = one rendered waveform (24 kHz mono float32). Every field except `text` is optional. Controls are SPEAKER-RELATIVE z-scores: 0 = the chosen voice's own neutral speech, +-1 = one pooled within-speaker standard deviation (raw units per dim: control_stats.json pooled_sd). Mask semantics: a control named in `ctrl` is requested (value + availability mask 1); every control NOT named is unspecified (mask 0) and the model chooses it from voice, tag and text. `{\"rate\": 0}` therefore asks for the voice's neutral rate, while omitting `rate` lets the tag/text decide. SteerTTS.resolve(request) returns the fully resolved request (defaults filled, `prompt` mapped to `tag` + `ctrl`, recorded in `prompt_mapping`), which is itself a valid request.",
6
+ "type": "object",
7
+ "required": [
8
+ "text"
9
+ ],
10
+ "additionalProperties": false,
11
+ "properties": {
12
+ "$schema": {
13
+ "type": "string"
14
+ },
15
+ "text": {
16
+ "type": "string",
17
+ "minLength": 1,
18
+ "maxLength": 5000,
19
+ "description": "English text. Numbers, units, currency, ordinals, ranges and unknown acronyms are spelled out by the front end. With `markup: true` it may contain inline markup (see `markup`). Event tokens such as [laugh] or [dog] are accepted: the model reserves a short stretch of time for them but renders no sound."
20
+ },
21
+ "voice": {
22
+ "type": "string",
23
+ "pattern": "^voice_[0-9]{4}$",
24
+ "description": "Built-in voice (anonymised public name, see voices.json). Default: the model's default_voice (voice_1361). Mutually exclusive with ref_wav."
25
+ },
26
+ "ref_wav": {
27
+ "type": "string",
28
+ "description": "Path to a reference recording (any sample rate, first 8 s used, >= 0.5 s) for zero-shot cloning: the voice is taken from its WavLM speaker vector and, when flow is on, the prompted flow uses the clip's own mel as context. Mutually exclusive with voice. Only clone voices whose owners consented."
29
+ },
30
+ "prompt": {
31
+ "type": "string",
32
+ "description": "Free-text voice direction ('soft and sad', 'excited, a bit faster'). Mapped by the prompt adapter to a style tag and a subset of controls. An explicit `tag` overrides the prompt's tag; explicit `ctrl` dims override the prompt's values for those dims."
33
+ },
34
+ "tag": {
35
+ "type": "string",
36
+ "enum": [
37
+ "none",
38
+ "adoration",
39
+ "amazement",
40
+ "amusement",
41
+ "anger",
42
+ "anxiety",
43
+ "apologetic",
44
+ "bored",
45
+ "confidence",
46
+ "confusion",
47
+ "contempt",
48
+ "contentment",
49
+ "cuteness",
50
+ "desire",
51
+ "determined",
52
+ "disappointment",
53
+ "disgust",
54
+ "distress",
55
+ "ecstasy",
56
+ "embarrassment",
57
+ "emphasis",
58
+ "enunciated",
59
+ "excited",
60
+ "fast",
61
+ "fear",
62
+ "frustration",
63
+ "guilt",
64
+ "happiness",
65
+ "hesitant",
66
+ "high_pitch",
67
+ "interest",
68
+ "irritation",
69
+ "laughing",
70
+ "loud",
71
+ "low_pitch",
72
+ "mysterious",
73
+ "neutral",
74
+ "nostalgia",
75
+ "pain",
76
+ "panic",
77
+ "pride",
78
+ "realization",
79
+ "relief",
80
+ "sadness",
81
+ "sarcasm",
82
+ "serenity",
83
+ "skeptical",
84
+ "slow",
85
+ "sympathy",
86
+ "tired",
87
+ "whisper"
88
+ ],
89
+ "default": "none",
90
+ "description": "Style tag; 'none' = unspecified. Tags act as style switches: voice-quality tags such as whisper change the voice strongly, but an independent emotion classifier recognises only sadness among the emotion tags, and about half of the tags are acoustically close to neutral (see the model card). For a specific acoustic change prefer ctrl."
91
+ },
92
+ "ctrl": {
93
+ "type": "object",
94
+ "additionalProperties": false,
95
+ "properties": {
96
+ "rate": {
97
+ "type": "number",
98
+ "minimum": -5,
99
+ "maximum": 5,
100
+ "description": "articulation rate (phones per second of speech, pauses excluded). + faster"
101
+ },
102
+ "loud": {
103
+ "type": "number",
104
+ "minimum": -5,
105
+ "maximum": 5,
106
+ "description": "mean speech level (dB). + louder"
107
+ },
108
+ "f0_med": {
109
+ "type": "number",
110
+ "minimum": -5,
111
+ "maximum": 5,
112
+ "description": "median pitch (semitones). + higher"
113
+ },
114
+ "f0_iqr": {
115
+ "type": "number",
116
+ "minimum": -5,
117
+ "maximum": 5,
118
+ "description": "pitch range, interquartile (semitones). + livelier, - monotone"
119
+ },
120
+ "f0_end": {
121
+ "type": "number",
122
+ "minimum": -5,
123
+ "maximum": 5,
124
+ "description": "pitch slope over the last 300 ms of voicing (st/s). + rising (question-like), - falling"
125
+ },
126
+ "pause": {
127
+ "type": "number",
128
+ "minimum": -5,
129
+ "maximum": 5,
130
+ "description": "share of the speech span spent in silent pauses >= 100 ms. + more pauses"
131
+ },
132
+ "tilt": {
133
+ "type": "number",
134
+ "minimum": -5,
135
+ "maximum": 5,
136
+ "description": "spectral tilt, energy 1-5 kHz vs 50 Hz-1 kHz (dB). + brighter / pressed"
137
+ },
138
+ "voiced": {
139
+ "type": "number",
140
+ "minimum": -5,
141
+ "maximum": 5,
142
+ "description": "voiced share of speech frames. - whispery"
143
+ },
144
+ "flat": {
145
+ "type": "number",
146
+ "minimum": -5,
147
+ "maximum": 5,
148
+ "description": "spectral flatness (log). + breathier / noisier"
149
+ },
150
+ "centroid": {
151
+ "type": "number",
152
+ "minimum": -5,
153
+ "maximum": 5,
154
+ "description": "spectral centroid (log Hz). + brighter"
155
+ },
156
+ "cpps": {
157
+ "type": "number",
158
+ "minimum": -5,
159
+ "maximum": 5,
160
+ "description": "cepstral peak prominence (dB), periodicity. - breathier / rougher"
161
+ },
162
+ "h1h2": {
163
+ "type": "number",
164
+ "minimum": -5,
165
+ "maximum": 5,
166
+ "description": "H1-H2 (dB). + breathier"
167
+ }
168
+ },
169
+ "default": {},
170
+ "description": "Requested controls {dim: z}. Values beyond +-3 are outside most training data."
171
+ },
172
+ "pace": {
173
+ "type": "number",
174
+ "minimum": 0.25,
175
+ "maximum": 4.0,
176
+ "default": 1.0,
177
+ "description": "Global duration divisor applied after prediction (>1 faster). Unlike ctrl.rate it does not change the style, only the timing."
178
+ },
179
+ "guidance": {
180
+ "type": "number",
181
+ "minimum": 0.0,
182
+ "maximum": 4.0,
183
+ "default": 1.0,
184
+ "description": "Steering scale s: out = out(no request) + s * (out(request) - out(no request)), on durations, pitch, energy and mel. s > 1 strengthens the requested change (control labels are noisy, so the learned effect is attenuated) at the cost of more leakage into other dims and some quality; only applied when a tag or ctrl is given."
185
+ },
186
+ "word_shift": {
187
+ "type": "object",
188
+ "description": "Per-word prosody edits {word_index: {pitch: semitones, energy: dB, dur: factor}} (word_index counts words of the normalised text from 0). Not combinable with markup (use *emph*, <slow> etc. there).",
189
+ "patternProperties": {
190
+ "^[0-9]+$": {
191
+ "type": "object",
192
+ "additionalProperties": false,
193
+ "properties": {
194
+ "pitch": {
195
+ "type": "number",
196
+ "minimum": -12,
197
+ "maximum": 12
198
+ },
199
+ "energy": {
200
+ "type": "number",
201
+ "minimum": -20,
202
+ "maximum": 20
203
+ },
204
+ "dur": {
205
+ "type": "number",
206
+ "minimum": 0.25,
207
+ "maximum": 4
208
+ }
209
+ }
210
+ }
211
+ },
212
+ "additionalProperties": false
213
+ },
214
+ "markup": {
215
+ "type": "boolean",
216
+ "default": false,
217
+ "description": "Parse inline markup in text: *emphasis* / <emph>..</emph>; per-word <slow> <fast> <high> <low> <loud> <soft>; [pause], [pause 300ms], [pause 1.5s]; span styles <whisper>..</whisper> (any tag) and <style prompt=\"...\">..</style>. The text is rendered sentence by sentence (and per span), joined with 10 ms fades. stream() always parses markup."
218
+ },
219
+ "flow": {
220
+ "type": "boolean",
221
+ "default": true,
222
+ "description": "Refine the mel with the flow-matching decoder (refiner for built-in voices, prompted flow for ref_wav). false = the acoustic model's mel straight into Vocos (faster, slightly hazier)."
223
+ },
224
+ "seed": {
225
+ "type": "integer",
226
+ "minimum": 0,
227
+ "default": 0,
228
+ "description": "Noise seed of the flow sampler (renders are deterministic per device for a given seed)."
229
+ },
230
+ "prompt_mapping": {
231
+ "type": "object",
232
+ "description": "Written by resolve(): what `prompt` mapped to {tag, ctrl, top_tags}. Informational; ignored on input.",
233
+ "properties": {
234
+ "tag": {
235
+ "type": "string"
236
+ },
237
+ "ctrl": {
238
+ "type": "object"
239
+ },
240
+ "top_tags": {
241
+ "type": "array"
242
+ }
243
+ }
244
+ }
245
+ },
246
+ "examples": [
247
+ {
248
+ "text": "The meeting has been moved to Thursday afternoon at three.",
249
+ "voice": "voice_1361"
250
+ },
251
+ {
252
+ "text": "I can't believe you did that!",
253
+ "voice": "voice_1361",
254
+ "tag": "anger",
255
+ "ctrl": {
256
+ "loud": 1.5,
257
+ "f0_med": 1.0
258
+ }
259
+ },
260
+ {
261
+ "text": "Are you serious?",
262
+ "ctrl": {
263
+ "f0_end": 2.0,
264
+ "f0_iqr": 1.5
265
+ }
266
+ },
267
+ {
268
+ "text": "Please keep this between us.",
269
+ "prompt": "whispering, slow and secretive"
270
+ },
271
+ {
272
+ "text": "Well, *that* was fast. [pause 400ms] <slow>Let me think.</slow>",
273
+ "markup": true
274
+ },
275
+ {
276
+ "text": "Now in a cloned voice.",
277
+ "ref_wav": "reference.wav"
278
+ }
279
+ ]
280
+ }
speaker_vectors.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:edcabfd02dd4a5203309e9b1d6d6adb84d31a18285916238541cf93a0764d4ae
3
+ size 5785680
steertts.egg-info/PKG-INFO ADDED
@@ -0,0 +1,397 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.4
2
+ Name: steertts
3
+ Version: 1.0.0
4
+ Summary: SteerTTS: a small, numerically steerable English TTS (hybrid attention + bidirectional Mamba-3)
5
+ License: Code: MIT. Weights: CC BY-NC 4.0 (TBD by authors), see README.md
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE.md
9
+ Requires-Dist: torch<2.12,>=2.4
10
+ Requires-Dist: numpy<2.5,>=1.24
11
+ Requires-Dist: safetensors==0.8.0
12
+ Requires-Dist: huggingface_hub>=0.23
13
+ Requires-Dist: vocos==0.1.0
14
+ Requires-Dist: g2p_en==2.1.0
15
+ Requires-Dist: nltk==3.10.3
16
+ Requires-Dist: num2words==0.5.14
17
+ Requires-Dist: soundfile==0.14.0
18
+ Requires-Dist: soxr==1.1.0
19
+ Provides-Extra: prompt
20
+ Requires-Dist: transformers==5.19.0; extra == "prompt"
21
+ Provides-Extra: clone
22
+ Requires-Dist: transformers==5.19.0; extra == "clone"
23
+ Provides-Extra: measure
24
+ Requires-Dist: praat-parselmouth==0.4.7; extra == "measure"
25
+ Provides-Extra: all
26
+ Requires-Dist: transformers==5.19.0; extra == "all"
27
+ Requires-Dist: praat-parselmouth==0.4.7; extra == "all"
28
+ Provides-Extra: test
29
+ Requires-Dist: transformers==5.19.0; extra == "test"
30
+ Requires-Dist: praat-parselmouth==0.4.7; extra == "test"
31
+ Requires-Dist: pytest>=8; extra == "test"
32
+ Requires-Dist: jsonschema>=4.20; extra == "test"
33
+ Dynamic: license-file
34
+
35
+ ---
36
+ language:
37
+ - en
38
+ license: cc-by-nc-4.0
39
+ library_name: steertts
40
+ pipeline_tag: text-to-speech
41
+ tags:
42
+ - text-to-speech
43
+ - tts
44
+ - speech-synthesis
45
+ - english
46
+ - controllable-tts
47
+ - steerable-tts
48
+ - prosody-control
49
+ - expressive-tts
50
+ - style-control
51
+ - inline-markup
52
+ - word-emphasis
53
+ - zero-shot-voice-cloning
54
+ - voice-cloning
55
+ - interpretable
56
+ - non-autoregressive
57
+ - fastpitch
58
+ - mamba
59
+ - mamba-3
60
+ - state-space-model
61
+ - hybrid-attention
62
+ - flow-matching
63
+ - vocos
64
+ - small-model
65
+ - on-device
66
+ - streaming
67
+ - non-commercial
68
+ ---
69
+
70
+ # SteerTTS v1
71
+
72
+ A small English text-to-speech model that you steer with numbers. Each request can set any subset of **12 acoustic
73
+ controls**, for example speaking rate, loudness, median pitch, pitch range, final rise or fall, pauses, spectral tilt
74
+ and breathiness. Each control is a z-score relative to the chosen voice's own neutral speech. You can also pick a
75
+ **style tag**, give a **free-text direction** (which a small adapter turns into a request you can inspect), or use
76
+ **inline markup**. The model can **clone** a voice from a short reference clip.
77
+
78
+ | | |
79
+ |---|---|
80
+ | Acoustic model | **9.59M** parameters (encoder/decoder: attention + bidirectional Mamba-3, AdaLN conditioning) |
81
+ | + Vocos vocoder | 13.53M (`charactr/vocos-mel-24khz`, frozen), **23.1M** total |
82
+ | Flow-matching refiner (optional, on by default) | 17.64M → 40.8M total |
83
+ | Prompted flow (cloning only) | 24.67M, used in place of the refiner when `ref_wav` is given → 47.8M total |
84
+ | Frozen external encoders | WavLM-base-plus-sv 101M (cloning only), all-MiniLM-L6-v2 22.7M (free-text prompts only) |
85
+ | Output | 24 kHz mono, fp32, CPU or CUDA |
86
+ | Voices | 2,825 built-in voices under anonymised names (`voice_0001` …), default `voice_1361` |
87
+
88
+ > **Licence: weights CC BY-NC 4.0 (non-commercial use only); package code MIT.** The weights were trained partly on
89
+ > non-commercial data (see [Training data](#training-data)).
90
+
91
+ ## Quick start
92
+
93
+ ```bash
94
+ git clone https://huggingface.co/WhissleAI/steer-tts && cd steer-tts # or huggingface_hub.snapshot_download("WhissleAI/steer-tts")
95
+ pip install ".[all]" # core: torch, vocos, g2p_en …; [all] adds transformers (prompts, cloning) + parselmouth (measuring)
96
+ steertts say "Hello there, nice to meet you." -o hello.wav
97
+ steertts say "I can't believe it worked." --tag happiness --ctrl rate=1,f0_iqr=1.5 -o happy.wav
98
+ steertts say "Please keep this quiet." --prompt "whispering, slow and secretive" -o secret.wav # prints the resolved request
99
+ steertts stream "Well, *that* was fast. [pause 400ms] <slow>Let me think.</slow>" -o markup.wav
100
+ steertts say "Now in your voice." --ref-wav consenting_speaker.wav -o clone.wav
101
+ steertts voices --gender female | head
102
+ ```
103
+
104
+ ```python
105
+ from steertts import SteerTTS
106
+
107
+ tts = SteerTTS.from_pretrained("WhissleAI/steer-tts") # HF repo id or a local dir; device: cuda if available, else cpu
108
+ wav = tts("The meeting moved to Thursday.") # float32 numpy, 24 kHz, default voice voice_1361
109
+ wav = tts("Are you serious?", voice="voice_0042", ctrl={"f0_end": 2.0, "f0_iqr": 1.5}) # rising, lively
110
+ wav = tts("It is over.", tag="sadness", ctrl={"loud": -1}, guidance=1.5)
111
+
112
+ req = tts.resolve({"text": "See you soon.", "prompt": "excited, a bit faster"})
113
+ print(req) # {'text': ..., 'voice': 'voice_1361', 'tag': 'excited', 'ctrl': {'rate': 0.29}, 'prompt_mapping': {...}, ...}
114
+ wav = tts.synthesize(req) # a resolved request renders exactly as the prompt would
115
+
116
+ for chunk in tts.stream("First sentence. Second sentence."): # sentence-level streaming
117
+ play(chunk.audio)
118
+ tts.save(wav, "out.wav")
119
+ ```
120
+
121
+ `inference.py` renders one example of each feature. `python -m steertts.cli --help` lists the CLI.
122
+
123
+ ## The request
124
+
125
+ Every synthesis is one JSON object. It is documented field by field in [`request.schema.json`](request.schema.json)
126
+ (JSON Schema 2020-12; `steertts schema` prints it). Only `text` is required.
127
+
128
+ | Field | Meaning |
129
+ |---|---|
130
+ | `text` | English text. Numbers, units, currency, ordinals, ranges and unknown acronyms are spelled out. |
131
+ | `voice` / `ref_wav` | A built-in voice, **or** a reference clip to clone (first 8 s, at least 0.5 s). |
132
+ | `tag` | One of 51 style tags (`none`, `whisper`, `sadness`, `anger`, `fast`, `high_pitch`, …). |
133
+ | `ctrl` | `{control: z}` over the 12 controls below, each in [-5, 5]. |
134
+ | `prompt` | Free-text direction. `resolve()` maps it to `tag` + `ctrl`; an explicit `tag` or `ctrl` dim wins. |
135
+ | `guidance` | Steering scale s (default 1). See [Guidance](#how-well-it-steers). |
136
+ | `pace` | Global tempo divisor (>1 is faster). It does not change the style. |
137
+ | `word_shift` | `{word_index: {pitch: st, energy: dB, dur: ×}}`, per-word prosody edits. |
138
+ | `markup` | Parse inline markup in `text` (see below). `stream()` always parses it. |
139
+ | `flow`, `seed` | Use the flow refiner (default true) and set the sampler seed. Renders are deterministic per device. |
140
+
141
+ **Controls and the mask.** The 12 controls are utterance-level acoustic measurements:
142
+
143
+ - `rate`: articulation rate
144
+ - `loud`: speech level
145
+ - `f0_med`: median pitch
146
+ - `f0_iqr`: pitch range
147
+ - `f0_end`: final pitch slope
148
+ - `pause`: pause share
149
+ - `tilt`: spectral tilt
150
+ - `voiced`: voiced share
151
+ - `flat`: spectral flatness
152
+ - `centroid`: spectral centroid
153
+ - `cpps`: cepstral peak prominence
154
+ - `h1h2`: H1–H2
155
+
156
+ Each value is a z-score against the chosen voice's own neutral speech, in units of the pooled within-speaker SD
157
+ (`control_stats.json`). The model also receives an availability mask:
158
+
159
+ - A dim named in `ctrl` is requested.
160
+ - A dim left out is unspecified, and the model chooses it from voice, tag and text.
161
+ - `{"rate": 0}` asks for this voice's neutral rate. Omitting `rate` lets a `fast` tag or the text decide.
162
+
163
+ `steertts measure out.wav --text "…" --voice voice_1361` reads the controls back with the same measurement code
164
+ that labelled the training data.
165
+
166
+ **Markup** (`markup: true` or `stream()`):
167
+
168
+ | Markup | Effect |
169
+ |---|---|
170
+ | `*word*` or `<emph>…</emph>` | Emphasis: +2.5 st, +4 dB, ×1.2 duration |
171
+ | `<slow> <fast> <high> <low> <loud> <soft>` | Per-word changes; the sentence is still rendered in one pass |
172
+ | `[pause]`, `[pause 300ms]`, `[pause 1.5s]` | Exact silence |
173
+ | `<whisper>…</whisper>` (any tag), `<style prompt="…">…</style>` | The span is rendered as its own segment |
174
+
175
+ Text is rendered sentence by sentence, so the first sentence can play while the rest renders.
176
+
177
+ ## Architecture
178
+
179
+ FastPitch-style explicit-variance TTS:
180
+
181
+ 1. **Front end.** g2p_en ARPAbet phonemes with word indices.
182
+ 2. **Encoder.** 4 blocks.
183
+ 3. **Variance predictors.** Per-phone duration, pitch and energy.
184
+ 4. **Length regulator.**
185
+ 5. **Decoder.** 4 blocks.
186
+ 6. **Output.** A 100-band log-mel, rendered by Vocos.
187
+
188
+ Each encoder/decoder block is a **hybrid**: multi-head self-attention, then a **bidirectional Mamba-3** branch (two
189
+ Mamba-3 layers run forward and backward and summed; complex-valued state, trapezoidal discretisation; 3.70M
190
+ parameters in total), then a conv FFN.
191
+
192
+ Every LayerNorm in every block and predictor is an **adaptive LayerNorm**. Its scale and shift are predicted from
193
+ g = MLP([speaker vector, tag embedding, ctrl ⊙ mask, mask]). The speaker slot is a linear projection of a frozen
194
+ 512-d WavLM-base-plus-sv x-vector. Built-in voices use their stored mean vector; cloning uses the vector of the
195
+ reference clip.
196
+
197
+ **Guidance** renders the request and the uninstructed request, then extrapolates durations, pitch, energy and mel:
198
+ out = out₀ + s·(out − out₀).
199
+
200
+ Optional decoders:
201
+
202
+ - **Flow refiner.** A conditional flow-matching ConvNeXt network (OT-CFM, 10 midpoint steps, temperature 0.4). It
203
+ samples mel detail given the acoustic model's mel and g, so every control still comes from the acoustic model.
204
+ - **Prompted flow.** The same network plus attention, given the reference clip's own mel as context
205
+ (Voicebox-style infilling). Used for cloning.
206
+
207
+ The `steertts` package here is self-contained. It is the inference subset of the training code, operation for
208
+ operation, and its outputs are bit-identical to the training repository's synthesis on the same device (see
209
+ [Release verification](#release-verification)).
210
+
211
+ ## How well it steers
212
+
213
+ The numbers below are **held-out**: 16 LibriTTS-R test-clean speakers who share no speaker, chapter or recording
214
+ with the training data (8 female, 8 male by a median-F0 rule; the corpus has no gender labels on disk), each
215
+ conditioned on the speaker vector of one recording, × 8 texts. They are for the acoustic model at guidance 1 **without the flow refiner**.
216
+ Steering through the refiner was not measured in the paper; the release check below confirms only the direction.
217
+
218
+ Columns:
219
+
220
+ - **Hit**: share of renders that move more than 0.5 SD the requested way.
221
+ - **Δz**: achieved change for a request of ±2.
222
+ - **ρ**: Spearman correlation between request and readback (by our extractor / by Praat).
223
+
224
+ | Tier | Control | Hit at −2 / +2 | Δz at −2 / +2 | ρ (ours / Praat) |
225
+ |---|---|---|---|---|
226
+ | **Reliable** | loud | 1.00 / 1.00 | −1.57 / +1.81 | .97 / .97 |
227
+ | | f0_med | .98 / 1.00 | −1.15 / +1.18 | .95 / .95 |
228
+ | | h1h2 | 1.00 / 1.00 | −1.59 / +1.49 | .96 / .96 |
229
+ | | tilt | 1.00 / .99 | −1.39 / +0.97 | .96 / .87 |
230
+ | | centroid | .78 / .94 | −0.79 / +1.20 | .92 / .92 |
231
+ | | f0_iqr | .66 / .91 | −0.63 / +1.09 | .88 / .88 |
232
+ | **Usable** | rate | 1.00 / 1.00 | −1.61 / +1.30 | .96 / .66 |
233
+ | | f0_end | .96 / .99 | −0.85 / +1.31 | .94 / .83 |
234
+ | | flat | .62 / .41 | −0.64 / +0.51 | .85 / .68 |
235
+ | **Weak** | pause | .38 / .64 | −0.37 / +0.71 | .76 / .70 |
236
+ | | voiced | .39 / .12 | −0.44 / +0.26 | .79 / .60 |
237
+ | | cpps | .56 / .35 | −0.45 / +0.30 | .58 / .46 |
238
+
239
+ - **Tiers.** Reliable: ρ ≥ 0.85 under both extractors. Usable: under one. Weak: under neither.
240
+ - **Gain.** The mean gain (achieved / requested) is about 0.5. `rate` and `f0_med` at +1 hit only .48 / .42.
241
+ - **Cross-talk.** Mean leakage into the other 11 dims is 0.15–0.30 SD. The off-diagonal / diagonal ratio is 0.10.
242
+ - **CPPS** is not a working control: a request moves pitch and level about as much as CPPS itself.
243
+ - **Intelligibility.** WER (Whisper-large-v3) is 0.94 % uninstructed and 0.84–1.01 % across all requests.
244
+
245
+ **Guidance** (pooled over loud, rate, f0_med, f0_iqr and tilt; ±2 requests; n = 1,280):
246
+
247
+ | s | Steer ratio (Δz / requested) | Hit | Leak | WER |
248
+ |---|---|---|---|---|
249
+ | 1 | 0.65 | .95 | .20 | 0.9 % |
250
+ | 2 | 1.35 | .99 | .34 | 1.0 % |
251
+ | 3 | 1.97 | .98 | .48 | 1.0 % |
252
+
253
+ - s ≈ 1.5 roughly delivers the requested size, at the cost of more leakage.
254
+ - Guidance on tags is riskier: `whisper` at s = 3 raises WER to 16 %.
255
+
256
+ **Markup** (8 held-out voices × 6 sentences × 2 words):
257
+
258
+ - Emphasis achieved +2.58 st and +3.92 dB, against +2.5 st and +4 dB requested.
259
+ - `<high>` / `<low>` gave +3.18 / −2.96 st for ±3.
260
+ - `<loud>` / `<soft>` gave +4.84 / −5.54 dB for ±6. The decoder attenuates level by 10–20 %.
261
+ - `<slow>` / `<fast>` and pauses are exact by construction.
262
+ - Neighbouring words move by at most 0.19 st and 0.16 dB.
263
+
264
+ **Free-text prompts** (161 hand-written prompts):
265
+
266
+ - Which-dims precision / recall: .85 / .66. Sign accuracy: .88.
267
+ - Tag top-1 / top-3: .56 / .69.
268
+ - Composition (.23) and negation (.22) mostly fail. "Not fast" can come out fast.
269
+ - Absolute-register prompts ("a deep male voice") are not supported. Pick a voice instead (`voices.json`).
270
+ - **Always look at the resolved request.**
271
+
272
+ ## Quality
273
+
274
+ Results under the Open TTS Leaderboard protocol (run locally on an RTX 5070, not an official submission;
275
+ Qwen3-ASR-1.7B WER, WavLM-large SIM):
276
+
277
+ | Setting | Seed-TTS en | CV3-Eval en | Avg |
278
+ |---|---|---|---|
279
+ | Default voice `voice_1361`, acoustic model + Vocos, WER | 1.00 % | 2.63 % | **1.81 %** |
280
+ | Cloning (prompted flow), WER / SIM ×100 | 1.01 % / 37.4 | 2.73 % / 28.7 | 1.87 % / 33.1 |
281
+
282
+ - The flow refiner was scored with an earlier acoustic checkpoint: 1.04 % / 2.70 % WER (no change).
283
+ - **Cloning similarity is low.** The board's lowest cloning SIM is about 37, and its median is about 65. Do not
284
+ expect a convincing likeness.
285
+
286
+ ## Speed
287
+
288
+ Measured on an RTX 5070 and on a shared, heavily loaded 8-thread CPU, so treat CPU figures as lower bounds:
289
+
290
+ - **GPU.** Batch-1 RTFx 61–81 for the acoustic model + Vocos (leaderboard runs).
291
+ - **CPU, 8 threads.** RTFx 3.6–7.9 for the acoustic model + Vocos. The flow refiner adds 12× the compute and
292
+ drops RTFx from 6.7 to 0.8 (0.4 with 1 thread). Use `flow=False` / `--no-flow` for real-time CPU use.
293
+ - **Cloning on CPU.** RTFx 0.2 with the prompted flow, including the WavLM speaker encoder.
294
+ - **Streaming on CPU.** First audio after about 1 s.
295
+ - See [Release verification](#release-verification) for the numbers measured on this exact package.
296
+
297
+ ## Limitations
298
+
299
+ - **English only.** The front end is ARPAbet. Single speaker per request, and the controls are utterance-level.
300
+ - **Emotion tags are style switches, not emotions.** An independent emotion classifier (emotion2vec+ large)
301
+ recognises only `sadness` (0.16 top-1 vs 0.01 uninstructed). Every other emotion tag scores at most 0.02. The same
302
+ classifier recognises 7 of 10 of these emotions on real EARS recordings. 27 of 49 tags are acoustically within 0.5
303
+ of `neutral`. Voice-quality tags such as `whisper` act strongly (voiced share −9.9 SD).
304
+ - **Event tokens only reserve time.** `[laugh]`, `[cough]`, `[dog]`, … add about 0.12 s of near-silence. An audio
305
+ tagger detects 0 of 1,728 renders. The project's demo mixed recorded sounds in, but that mixer is not part of this
306
+ release.
307
+ - **The acoustic-environment input is untrained.** The weights contain an environment projection, but the final
308
+ training stage never saw a non-zero environment vector, so this package does not expose it.
309
+ - **Cloning similarity is low** (see above), and cloning speed on CPU is slow with the prompted flow.
310
+ - **The controls are acoustic measurements, not percepts.** `voiced`, `flat` and `cpps` barely move. `rate` and
311
+ `tilt` depend on how they are measured. z = 0 is the voice's typical render, not its real recordings.
312
+ - **Test-set-informed choices.** The prompted-flow checkpoint was chosen on subsets of the Seed-TTS / CV3 test
313
+ sets, and the text-normalisation rules came from CV3 error analysis. The default voice `voice_1361` is the
314
+ pre-registered one: a sweep picked a lower-WER voice on the test sets, but it is not the default here.
315
+ - **No human evaluation.** Every number above is from automatic metrics, with one training seed.
316
+
317
+ ## Intended use and misuse
318
+
319
+ **Intended uses:**
320
+
321
+ - Research on controllable / interpretable speech synthesis.
322
+ - Accessibility and prototyping, where explicit, inspectable control matters more than peak naturalness.
323
+ - On-device experiments with a ~23M-parameter TTS.
324
+
325
+ **Voice cloning requires consent.** Clone only a voice whose owner has agreed, and do not use this model to:
326
+
327
+ - impersonate real people;
328
+ - produce deceptive or fraudulent audio;
329
+ - bypass voice authentication;
330
+ - create non-consensual content.
331
+
332
+ Disclose synthetic speech to listeners. There is no watermark yet.
333
+
334
+ **Built-in voices are anonymised.** Built-in voices are training speakers under opaque names. Speaker IDs from the
335
+ source corpora are not included. Speakers from a celebrity-interview corpus (VoxCeleb, via NonverbalTTS) were
336
+ excluded from the voice list.
337
+
338
+ ## Training data
339
+
340
+ 410,566 utterances (659 h) from 4,919 speakers:
341
+
342
+ | Source | Hours | Licence |
343
+ |---|---|---|
344
+ | LibriTTS-R (clean-100, clean-360, other-500) | 516 | CC BY 4.0 |
345
+ | EARS (read + freeform) | 78 | CC BY-NC 4.0 |
346
+ | Expresso | 11 | CC BY-NC 4.0 |
347
+ | NonverbalTTS (VoxCeleb and Expresso sources) | 12.5 | Source terms apply |
348
+ | Synthetic speech from a commercial TTS API | 43 | The provider's terms of service, incl. a competing-model clause |
349
+
350
+ Control labels are measured from the audio. Style tags come from the corpora's own style/emotion labels.
351
+
352
+ **Licence: CC BY-NC 4.0. Non-commercial use only.**
353
+
354
+ ## Files
355
+
356
+ | File | Contents |
357
+ |---|---|
358
+ | `model.safetensors` | Acoustic model (fp32) |
359
+ | `flow.safetensors`, `pflow.safetensors` | Flow refiner and prompted flow |
360
+ | `prompt_adapter.safetensors` | Free-text → request adapter (0.5M) |
361
+ | `speaker_vectors.safetensors` | 512-d vector per built-in voice, in `config.json:voices_order` |
362
+ | `voices.json` | Measured descriptions (see below) |
363
+ | `control_stats.json` | Pooled SDs, tag list, and each voice's neutral control means (for `measure_z`) |
364
+ | `config.json` | Architecture and file map |
365
+ | `request.schema.json` | The request schema |
366
+ | `steertts/` | The package (incl. `steertts/mamba3` with its own invariant tests) |
367
+ | `tests/` | pytest suite |
368
+ | `leaderboard/` | Open TTS Leaderboard backend |
369
+ | `TEST_LOG.md` | Release verification log |
370
+ | `LICENSE.md` | Code (MIT) and weights (CC BY-NC 4.0) |
371
+
372
+ `voices.json` descriptions are measured, not copied from corpus metadata:
373
+
374
+ - **Gender.** A probe on the speaker vector; 99.3 % 5-fold accuracy on 534 self-labelled speakers.
375
+ - **Pitch register.** Median F0 within that gender.
376
+ - **Pace.** Articulation rate.
377
+
378
+ ## Release verification
379
+
380
+ The package was verified in a fresh CPU environment; `TEST_LOG.md` has the full log.
381
+
382
+ - **Tests.** The pytest suite passes (16 tests).
383
+ - **Parity.** Seven renders are bit-identical to the training code's synthesis on the same device: acoustic
384
+ only, controls, tag + guidance, per-word shifts, the flow refiner, and cloning through the prompted flow.
385
+ - **Speed.** On this package, CPU with 4 threads on a loaded machine: RTFx 4.9 without the refiner, 0.7 with it.
386
+ First streamed audio arrives after 0.5 s.
387
+
388
+ ## Citation
389
+
390
+ ```bibtex
391
+ @misc{steertts2026,
392
+ title = {SteerTTS: A Tiny, Measurably Steerable Speech Synthesizer},
393
+ author = {{Whissle AI}},
394
+ year = {2026},
395
+ howpublished = {\url{https://huggingface.co/WhissleAI/steer-tts}}
396
+ }
397
+ ```
steertts.egg-info/SOURCES.txt ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ LICENSE.md
2
+ README.md
3
+ pyproject.toml
4
+ steertts/__init__.py
5
+ steertts/__main__.py
6
+ steertts/audio.py
7
+ steertts/cli.py
8
+ steertts/config.py
9
+ steertts/flow.py
10
+ steertts/markup.py
11
+ steertts/measure.py
12
+ steertts/model.py
13
+ steertts/pipeline.py
14
+ steertts/prompt.py
15
+ steertts/request.py
16
+ steertts/request.schema.json
17
+ steertts/speaker.py
18
+ steertts/text.py
19
+ steertts.egg-info/PKG-INFO
20
+ steertts.egg-info/SOURCES.txt
21
+ steertts.egg-info/dependency_links.txt
22
+ steertts.egg-info/entry_points.txt
23
+ steertts.egg-info/requires.txt
24
+ steertts.egg-info/top_level.txt
25
+ steertts/mamba3/__init__.py
26
+ steertts/mamba3/layer.py
27
+ steertts/mamba3/reference.py
28
+ steertts/mamba3/scan.py
29
+ steertts/mamba3/ssd.py
30
+ steertts/mamba3/tests.py
31
+ tests/test_mamba3.py
32
+ tests/test_synthesis.py
33
+ tests/test_text_request.py
steertts.egg-info/dependency_links.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
steertts.egg-info/entry_points.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [console_scripts]
2
+ steertts = steertts.cli:main
steertts.egg-info/requires.txt ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch<2.12,>=2.4
2
+ numpy<2.5,>=1.24
3
+ safetensors==0.8.0
4
+ huggingface_hub>=0.23
5
+ vocos==0.1.0
6
+ g2p_en==2.1.0
7
+ nltk==3.10.3
8
+ num2words==0.5.14
9
+ soundfile==0.14.0
10
+ soxr==1.1.0
11
+
12
+ [all]
13
+ transformers==5.19.0
14
+ praat-parselmouth==0.4.7
15
+
16
+ [clone]
17
+ transformers==5.19.0
18
+
19
+ [measure]
20
+ praat-parselmouth==0.4.7
21
+
22
+ [prompt]
23
+ transformers==5.19.0
24
+
25
+ [test]
26
+ transformers==5.19.0
27
+ praat-parselmouth==0.4.7
28
+ pytest>=8
29
+ jsonschema>=4.20
steertts.egg-info/top_level.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ steertts
steertts/__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ """SteerTTS: a small, numerically steerable English TTS (hybrid attention + bidirectional Mamba-3, AdaLN on a
2
+ masked 12-dim control request), with optional flow-matching refiners, a free-text prompt adapter and zero-shot
3
+ cloning from a reference clip."""
4
+ from .config import CONTROL_DIMS, DEFAULT_VOICE, SR
5
+ from .pipeline import SteerTTS, SteerTTSPipeline, resolve_model_dir
6
+ from .request import normalize_request, schema
7
+
8
+ __all__ = ["SteerTTS", "SteerTTSPipeline", "CONTROL_DIMS", "DEFAULT_VOICE", "SR", "normalize_request", "schema",
9
+ "resolve_model_dir"]
10
+ __version__ = "1.0.0"
steertts/__main__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .cli import main
2
+
3
+ main()
steertts/audio.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Waveform I/O and the Vocos mel front end (24 kHz, 100 bands, hop 256)."""
2
+ from pathlib import Path
3
+
4
+ import numpy as np
5
+ import torch
6
+
7
+ from .config import HOP, N_FFT, N_MELS, SR
8
+
9
+ _mel_fe = None
10
+
11
+
12
+ def mel_extractor():
13
+ global _mel_fe
14
+ if _mel_fe is None:
15
+ from vocos.feature_extractors import MelSpectrogramFeatures
16
+ _mel_fe = MelSpectrogramFeatures(sample_rate=SR, n_fft=N_FFT, hop_length=HOP, n_mels=N_MELS, padding="center")
17
+ return _mel_fe
18
+
19
+
20
+ def load_wav(x, sr=None):
21
+ """path or float array (at `sr`, default 24 kHz) -> (mono float32 at 24 kHz, original sample rate)."""
22
+ if isinstance(x, (str, Path)):
23
+ import soundfile as sf
24
+ y, sr = sf.read(str(x), dtype="float32")
25
+ else:
26
+ y, sr = np.asarray(x, np.float32), (sr or SR)
27
+ y = y.mean(1) if y.ndim > 1 else y
28
+ if sr != SR:
29
+ import soxr
30
+ y = soxr.resample(y, sr, SR)
31
+ return np.asarray(y, np.float32), sr
32
+
33
+
34
+ def save_wav(wav, path):
35
+ import soundfile as sf
36
+ sf.write(str(path), np.asarray(wav, np.float32), SR)
steertts/cli.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """steertts command line.
2
+
3
+ steertts say "Hello there." -o hello.wav # default voice, default style
4
+ steertts say "I can't believe it." --voice voice_1361 --tag anger --ctrl loud=1.5,f0_med=1 -o angry.wav
5
+ steertts say "Keep it down." --prompt "whispering and slow" --show-request -o soft.wav
6
+ steertts say "Well, *that* was fast. [pause 400ms] <slow>Let me think.</slow>" --markup -o m.wav
7
+ steertts say "Now in your voice." --ref-wav me.wav -o clone.wav
8
+ steertts say --request req.json -o out.wav # a JSON request (request.schema.json)
9
+ steertts stream "First sentence. Second sentence." -o s.wav # prints per-chunk timing
10
+ steertts voices [--gender female] | steertts tags | steertts schema | steertts prompt "soft and sad"
11
+ steertts measure out.wav --text "..." --voice voice_1361 # read the 12 controls back (z)
12
+
13
+ The model directory comes from --model, else $STEERTTS_MODEL, else the current directory if it holds config.json.
14
+ """
15
+ import argparse
16
+ import json
17
+ import os
18
+ import sys
19
+ import time
20
+
21
+
22
+ def _model_dir(a):
23
+ if a.model:
24
+ return a.model
25
+ if os.environ.get("STEERTTS_MODEL"):
26
+ return os.environ["STEERTTS_MODEL"]
27
+ if os.path.exists("config.json") and os.path.exists("voices.json"):
28
+ return "."
29
+ sys.exit("steertts: no model directory; pass --model DIR (or an HF repo id) or set STEERTTS_MODEL")
30
+
31
+
32
+ def _ctrl(s):
33
+ out = {}
34
+ for kv in filter(None, (s or "").split(",")):
35
+ k, _, v = kv.partition("=")
36
+ out[k.strip()] = float(v)
37
+ return out
38
+
39
+
40
+ def _load(a, **kw):
41
+ from . import SteerTTS
42
+ t0 = time.time()
43
+ tts = SteerTTS.from_pretrained(_model_dir(a), device=a.device, flow=not a.no_flow, **kw)
44
+ print(f"loaded {tts} in {time.time() - t0:.1f}s", file=sys.stderr)
45
+ return tts
46
+
47
+
48
+ def _request(a):
49
+ req = json.load(open(a.request)) if a.request else {}
50
+ for k, v in dict(text=a.text, voice=a.voice, ref_wav=a.ref_wav, prompt=a.prompt, tag=a.tag,
51
+ pace=a.pace, guidance=a.guidance, seed=a.seed).items():
52
+ if v is not None:
53
+ req[k] = v
54
+ if a.ctrl:
55
+ req["ctrl"] = {**req.get("ctrl", {}), **_ctrl(a.ctrl)}
56
+ if getattr(a, "markup", False):
57
+ req["markup"] = True
58
+ if a.no_flow:
59
+ req["flow"] = False
60
+ return req
61
+
62
+
63
+ def main(argv=None):
64
+ ap = argparse.ArgumentParser(prog="steertts", description="SteerTTS: steerable English TTS")
65
+ ap.add_argument("--model", help="model directory or HF repo id (default: $STEERTTS_MODEL or .)")
66
+ ap.add_argument("--device", help="cpu | cuda (default: cuda if available)")
67
+ ap.add_argument("--no-flow", action="store_true", help="skip the flow-matching refiner")
68
+ sub = ap.add_subparsers(dest="cmd", required=True)
69
+ for name in ("say", "stream"):
70
+ p = sub.add_parser(name)
71
+ p.add_argument("text", nargs="?")
72
+ p.add_argument("--request", help="JSON request file (fields as in request.schema.json)")
73
+ p.add_argument("--voice")
74
+ p.add_argument("--ref-wav", dest="ref_wav", help="clone this reference clip (consent required)")
75
+ p.add_argument("--prompt", help="free-text direction, e.g. 'soft and sad'")
76
+ p.add_argument("--tag")
77
+ p.add_argument("--ctrl", help="comma list dim=z, e.g. rate=1.5,f0_med=-1")
78
+ p.add_argument("--pace", type=float)
79
+ p.add_argument("--guidance", type=float)
80
+ p.add_argument("--seed", type=int)
81
+ p.add_argument("--markup", action="store_true")
82
+ p.add_argument("--show-request", action="store_true", help="print the resolved request JSON")
83
+ p.add_argument("-o", "--out", default="out.wav")
84
+ p = sub.add_parser("voices")
85
+ p.add_argument("--gender", choices=["female", "male"])
86
+ p.add_argument("--json", action="store_true")
87
+ sub.add_parser("tags")
88
+ sub.add_parser("schema")
89
+ p = sub.add_parser("prompt")
90
+ p.add_argument("prompt")
91
+ p = sub.add_parser("measure")
92
+ p.add_argument("wav")
93
+ p.add_argument("--text")
94
+ p.add_argument("--voice")
95
+ a = ap.parse_args(argv)
96
+
97
+ if a.cmd == "schema":
98
+ from .request import schema
99
+ print(json.dumps(schema(), indent=1))
100
+ return
101
+ if a.cmd in ("voices", "tags"):
102
+ from .pipeline import resolve_model_dir
103
+ d = resolve_model_dir(_model_dir(a))
104
+ if a.cmd == "tags":
105
+ print(" ".join(json.load(open(d / "control_stats.json"))["tags"]))
106
+ return
107
+ vs = json.load(open(d / "voices.json"))["voices"]
108
+ vs = [v for v in vs if not a.gender or v.get("gender") == a.gender]
109
+ if a.json:
110
+ print(json.dumps(vs, indent=1))
111
+ else:
112
+ for v in vs:
113
+ print(f"{v['name']} {v['description']}")
114
+ return
115
+ if a.cmd == "measure":
116
+ from .audio import load_wav
117
+ from .measure import measure, measure_z, n_phones
118
+ from .pipeline import resolve_model_dir
119
+ y, _ = load_wav(a.wav)
120
+ if a.voice:
121
+ st = json.load(open(resolve_model_dir(_model_dir(a)) / "control_stats.json"))
122
+ z = measure_z(y, a.text, st["spk_mean"][a.voice], st["pooled_sd"])
123
+ print(json.dumps({k: round(v, 2) for k, v in z.items()}))
124
+ else:
125
+ from .config import CONTROL_DIMS
126
+ raw = measure(y, n_phones=n_phones(a.text) if a.text else None, want_mel=False)["controls"]
127
+ print(json.dumps({k: round(float(v), 3) for k, v in zip(CONTROL_DIMS, raw)}))
128
+ return
129
+ tts = _load(a)
130
+ if a.cmd == "prompt":
131
+ print(json.dumps(tts.map_prompt(a.prompt), indent=1))
132
+ return
133
+ req = _request(a)
134
+ if not req.get("text"):
135
+ sys.exit("steertts: no text (positional argument or --request)")
136
+ req = tts.resolve(req)
137
+ if getattr(a, "show_request", False) or "prompt_mapping" in req:
138
+ print(json.dumps(req, indent=1), file=sys.stderr)
139
+ import numpy as np
140
+ if a.cmd == "say":
141
+ t0 = time.time()
142
+ wav = tts.synthesize(req)
143
+ dt = time.time() - t0
144
+ else:
145
+ t0, chunks = time.time(), []
146
+ for i, c in enumerate(tts.stream(request=req)):
147
+ chunks.append(c.audio)
148
+ print(f"chunk {i}: {len(c.audio) / tts.sample_rate:.2f}s audio at +{(time.time() - t0) * 1000:.0f} ms "
149
+ f"({c.segment.text or ('pause %d ms' % c.segment.pause_ms)!r})", file=sys.stderr)
150
+ wav = np.concatenate(chunks) if chunks else np.zeros(1, np.float32)
151
+ dt = time.time() - t0
152
+ tts.save(wav, a.out)
153
+ print(f"{a.out}: {len(wav) / tts.sample_rate:.2f}s audio in {dt:.2f}s", file=sys.stderr)
154
+
155
+
156
+ if __name__ == "__main__":
157
+ main()
steertts/config.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Audio / control constants. They must match training: the charactr/vocos-mel-24khz mel front end."""
2
+ SR = 24000
3
+ N_FFT = 1024
4
+ HOP = 256
5
+ N_MELS = 100
6
+ FPS = SR / HOP # 93.75 mel frames per second
7
+ PITCH_SCALE = 3.0 # predicted token pitch is semitones re the speaker median / 3; word_shift pitch is given in st
8
+
9
+ # Utterance-level controls. Each is a SPEAKER-RELATIVE z-score: 0 = this voice's own neutral speech, +-1 = one
10
+ # pooled within-speaker standard deviation (pooled_sd in control_stats.json gives the raw unit per dim).
11
+ CONTROL_DIMS = [
12
+ "rate", # articulation rate, phones per second of speech (pauses excluded)
13
+ "loud", # mean RMS level of speech frames, dB
14
+ "f0_med", # median F0, semitones
15
+ "f0_iqr", # F0 interquartile range, semitones (pitch range / liveliness)
16
+ "f0_end", # F0 slope over the final 300 ms of voicing, st/s (+ rising, question-like)
17
+ "pause", # share of the speech span that is silent pauses >= 100 ms
18
+ "tilt", # alpha ratio: energy 1-5 kHz vs 50 Hz-1 kHz, dB (+ brighter / pressed)
19
+ "voiced", # voiced share of speech frames (whisper -> low)
20
+ "flat", # spectral flatness of speech frames, log (breathy / whisper -> high)
21
+ "centroid", # spectral centroid of speech frames, log Hz (brightness)
22
+ "cpps", # smoothed cepstral peak prominence, dB (periodicity; breathy / tense -> low)
23
+ "h1h2", # H1-H2, dB (breathy -> high)
24
+ ]
25
+ CONTROL_RANGE = (-5.0, 5.0) # accepted request range; training labels rarely exceed +-3
26
+
27
+ DEFAULT_VOICE = "voice_1361" # pre-registered default (chosen before any test-set voice sweep)
steertts/flow.py ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Flow-matching mel decoders (optional), inference subset of recipes/steer_tts/steer/flow.py.
2
+
3
+ refiner (flow.safetensors): a conditional flow (OT-CFM, as the Matcha-TTS decoder) that samples a detailed mel
4
+ given the acoustic model's mel mu and its conditioning g. Durations, pitch, energy, the coarse spectrum and
5
+ every control still come from the acoustic model; the flow adds texture (sharper harmonics / noise).
6
+ prompted flow (pflow.safetensors): the same with an audio prompt - the reference clip's own mel is given as
7
+ context before the target (Voicebox / F5-TTS-style infilling). Used only for zero-shot cloning.
8
+ """
9
+ import math
10
+
11
+ import torch
12
+ import torch.nn as nn
13
+ import torch.nn.functional as F
14
+
15
+ from .config import N_MELS
16
+
17
+ SIGMA = 1e-4
18
+
19
+ class TimeEmb(nn.Module):
20
+ def __init__(self, d):
21
+ super().__init__()
22
+ self.d = d
23
+ self.mlp = nn.Sequential(nn.Linear(d, 4 * d), nn.SiLU(), nn.Linear(4 * d, d))
24
+
25
+ def forward(self, t): # [B] in [0, 1]
26
+ half = self.d // 2
27
+ f = torch.exp(-math.log(10000) * torch.arange(half, device=t.device) / half)
28
+ a = 1000 * t[:, None] * f[None]
29
+ return self.mlp(torch.cat([a.sin(), a.cos()], -1))
30
+
31
+
32
+ class Block(nn.Module):
33
+ """ConvNeXt block (depthwise dilated conv -> LN -> MLP) with AdaLN-zero conditioning on (t, g)."""
34
+
35
+ def __init__(self, d, c, k=7, dil=1, mult=3):
36
+ super().__init__()
37
+ self.dw = nn.Conv1d(d, d, k, padding=dil * (k // 2), dilation=dil, groups=d)
38
+ self.ln = nn.LayerNorm(d, elementwise_affine=False)
39
+ self.mod = nn.Linear(c, 3 * d)
40
+ nn.init.zeros_(self.mod.weight); nn.init.zeros_(self.mod.bias)
41
+ self.pw1, self.pw2 = nn.Linear(d, mult * d), nn.Linear(mult * d, d)
42
+
43
+ def forward(self, x, m, c): # x [B,d,T], m [B,1,T], c [B,c]
44
+ sc, sh, gate = self.mod(c)[:, None].chunk(3, -1)
45
+ h = self.dw(x * m).transpose(1, 2)
46
+ h = self.ln(h) * (1 + sc) + sh
47
+ h = self.pw2(F.gelu(self.pw1(h))) * gate
48
+ return x + h.transpose(1, 2)
49
+
50
+
51
+ class AttnBlock(nn.Module):
52
+ """Full self-attention over the sequence with AdaLN-zero conditioning: lets the audio prompt (prompted flow)
53
+ reach every target frame, beyond the conv receptive field (~170 frames each side)."""
54
+
55
+ def __init__(self, d, c, heads=6):
56
+ super().__init__()
57
+ self.ln = nn.LayerNorm(d, elementwise_affine=False)
58
+ self.mod = nn.Linear(c, 3 * d)
59
+ nn.init.zeros_(self.mod.weight); nn.init.zeros_(self.mod.bias)
60
+ self.att = nn.MultiheadAttention(d, heads, batch_first=True)
61
+
62
+ def forward(self, x, m, c):
63
+ sc, sh, gate = self.mod(c)[:, None].chunk(3, -1)
64
+ h = self.ln(x.transpose(1, 2)) * (1 + sc) + sh
65
+ h, _ = self.att(h, h, h, key_padding_mask=~m[:, 0].bool(), need_weights=False)
66
+ return x + (h * gate).transpose(1, 2) * m
67
+
68
+
69
+ def rope(x): # x [B,H,T,dh]: rotary position embedding (Su et al. 2021) over the frame axis
70
+ T, dh = x.shape[-2], x.shape[-1]
71
+ f = torch.exp(-math.log(10000) * torch.arange(0, dh, 2, device=x.device, dtype=torch.float32) / dh)
72
+ a = torch.arange(T, device=x.device, dtype=torch.float32)[:, None] * f[None]
73
+ cos, sin = a.cos().to(x.dtype), a.sin().to(x.dtype)
74
+ x1, x2 = x[..., 0::2], x[..., 1::2]
75
+ return torch.stack([x1 * cos - x2 * sin, x1 * sin + x2 * cos], -1).flatten(-2)
76
+
77
+
78
+ class PromptPool(nn.Module):
79
+ """Perceiver-resampler pooling of the prompt (XTTS; SupertonicTTS's learnable timbre queries): n learnable
80
+ queries cross-attend to the encoded prompt frames -> n global voice tokens. A learned null key is always
81
+ present, so an item without a prompt still gets defined (prompt-free) tokens."""
82
+
83
+ def __init__(self, d, n, heads=6, layers=2):
84
+ super().__init__()
85
+ self.q = nn.Parameter(torch.randn(n, d) * 0.02)
86
+ self.null = nn.Parameter(torch.zeros(1, 1, d))
87
+ self.heads = heads
88
+ self.layers = nn.ModuleList([nn.ModuleDict(dict(
89
+ lq=nn.LayerNorm(d), lk=nn.LayerNorm(d), q=nn.Linear(d, d), kv=nn.Linear(d, 2 * d), o=nn.Linear(d, d),
90
+ lf=nn.LayerNorm(d), f=nn.Sequential(nn.Linear(d, 2 * d), nn.GELU(), nn.Linear(2 * d, d))))
91
+ for _ in range(layers)])
92
+
93
+ def forward(self, e, cm): # e [B,d,T] encoded prompt (zero off-prompt), cm [B,1,T]
94
+ B, d, T = e.shape
95
+ kv_in = torch.cat([self.null.expand(B, 1, d), e.transpose(1, 2)], 1)
96
+ keep = torch.cat([torch.ones(B, 1, device=e.device, dtype=torch.bool), cm[:, 0].bool()], 1)[:, None, None]
97
+ q = self.q[None].expand(B, -1, -1)
98
+ H = self.heads
99
+ for l in self.layers:
100
+ k, v = l["kv"](l["lk"](kv_in)).chunk(2, -1)
101
+ qq = l["q"](l["lq"](q))
102
+ sp = lambda z: z.view(B, -1, H, d // H).transpose(1, 2)
103
+ h = F.scaled_dot_product_attention(sp(qq), sp(k), sp(v), attn_mask=keep)
104
+ q = q + l["o"](h.transpose(1, 2).reshape(B, -1, d))
105
+ q = q + l["f"](l["lf"](q))
106
+ return q # [B,n,d]
107
+
108
+
109
+ class RAttnBlock(nn.Module):
110
+ """Self-attention with RoPE positions (frames), plus optional cross-attention to the pooled prompt tokens; both
111
+ zero-gated (AdaLN-zero gate / zero output projection), so adding these blocks to a trained net changes nothing
112
+ at init."""
113
+
114
+ def __init__(self, d, c, heads=6, xattn=False):
115
+ super().__init__()
116
+ self.heads = heads
117
+ self.ln = nn.LayerNorm(d, elementwise_affine=False)
118
+ self.mod = nn.Linear(c, 3 * d)
119
+ nn.init.zeros_(self.mod.weight); nn.init.zeros_(self.mod.bias)
120
+ self.qkv, self.o = nn.Linear(d, 3 * d), nn.Linear(d, d)
121
+ self.xattn = xattn
122
+ if xattn:
123
+ self.lx = nn.LayerNorm(d)
124
+ self.xq, self.xkv, self.xo = nn.Linear(d, d), nn.Linear(d, 2 * d), nn.Linear(d, d)
125
+ nn.init.zeros_(self.xo.weight); nn.init.zeros_(self.xo.bias)
126
+
127
+ def forward(self, x, m, c, tok=None): # x [B,d,T], tok [B,n,d]
128
+ B, d, T = x.shape
129
+ H = self.heads
130
+ sp = lambda z: z.view(B, -1, H, d // H).transpose(1, 2)
131
+ sc, sh, gate = self.mod(c)[:, None].chunk(3, -1)
132
+ h = self.ln(x.transpose(1, 2)) * (1 + sc) + sh
133
+ q, k, v = (sp(z) for z in self.qkv(h).chunk(3, -1))
134
+ h = F.scaled_dot_product_attention(rope(q), rope(k), v, attn_mask=m[:, 0].bool()[:, None, None])
135
+ h = self.o(h.transpose(1, 2).reshape(B, T, d)) * gate
136
+ if self.xattn and tok is not None:
137
+ hx = self.lx(x.transpose(1, 2) + h)
138
+ k2, v2 = self.xkv(tok).chunk(2, -1)
139
+ h = h + self.xo(F.scaled_dot_product_attention(sp(self.xq(hx)), sp(k2), sp(v2)).transpose(1, 2).reshape(B, T, d))
140
+ return x + h.transpose(1, 2) * m
141
+
142
+
143
+ class FlowNet(nn.Module):
144
+ """prompt=True (prompted flow): also takes a context mel ctx with mask cm (1 = real audio given: the reference
145
+ clip's frames), as Voicebox / F5-TTS infilling. The context enters the input stack, a masked mean of it is added
146
+ to the conditioning (global voice), and attention layers at attn_at carry it across the whole sequence.
147
+
148
+ v5 additions (all zero-initialised, so a v4 checkpoint loads into a v5 net unchanged):
149
+ rope_at layers that get an extra RoPE self-attention block (RAttnBlock)
150
+ n_ptok pooled prompt tokens (PromptPool) that the RoPE blocks cross-attend to
151
+ skips E2-TTS-style long skips, block i -> block n-1-i
152
+ align_at layers whose target-frame mean is mapped to a speaker-verification vector (TLA-SA-style feature
153
+ alignment loss; training only)"""
154
+
155
+ def __init__(self, n_mel=N_MELS, d=384, n_layers=12, g_dim=128, dils=(1, 2, 4, 1, 2, 4, 1, 2, 4, 1, 2, 4),
156
+ prompt=False, attn_at=(3, 7, 11), rope_at=(), n_ptok=0, skips=False, align_at=(), align_dim=512):
157
+ super().__init__()
158
+ self.cfg = dict(n_mel=n_mel, d=d, n_layers=n_layers, g_dim=g_dim, dils=tuple(dils), prompt=prompt,
159
+ attn_at=tuple(attn_at), rope_at=tuple(rope_at), n_ptok=n_ptok, skips=skips,
160
+ align_at=tuple(align_at), align_dim=align_dim)
161
+ self.prompt = prompt
162
+ self.inp = nn.Conv1d((3 * n_mel + 1) if prompt else 2 * n_mel, d, 3, padding=1)
163
+ self.temb = TimeEmb(d)
164
+ self.gemb = nn.Sequential(nn.Linear(g_dim, d), nn.SiLU(), nn.Linear(d, d))
165
+ self.blocks = nn.ModuleList([Block(d, d, dil=dils[i % len(dils)]) for i in range(n_layers)])
166
+ if prompt:
167
+ self.attn = nn.ModuleDict({str(i): AttnBlock(d, d) for i in attn_at})
168
+ self.ctx_enc = nn.Sequential(nn.Conv1d(n_mel, d, 3, padding=1), nn.SiLU(), nn.Conv1d(d, d, 3, padding=1))
169
+ self.ctx_out = nn.Linear(d, d)
170
+ nn.init.zeros_(self.ctx_out.weight); nn.init.zeros_(self.ctx_out.bias)
171
+ self.rattn = nn.ModuleDict({str(i): RAttnBlock(d, d, xattn=n_ptok > 0) for i in rope_at})
172
+ self.pool = PromptPool(d, n_ptok) if n_ptok else None
173
+ self.skips = skips
174
+ if skips:
175
+ self.skip = nn.ModuleDict({str(i): nn.Linear(d, d) for i in range(n_layers - n_layers // 2, n_layers)})
176
+ for l in self.skip.values():
177
+ nn.init.zeros_(l.weight); nn.init.zeros_(l.bias)
178
+ self.align_at = tuple(align_at)
179
+ if align_at:
180
+ self.align = nn.ModuleDict({str(i): nn.Sequential(nn.Linear(d, d), nn.SiLU(), nn.Linear(d, align_dim))
181
+ for i in align_at})
182
+ self.ln = nn.LayerNorm(d, elementwise_affine=False)
183
+ self.out = nn.Conv1d(d, n_mel, 1)
184
+ nn.init.zeros_(self.out.weight); nn.init.zeros_(self.out.bias)
185
+
186
+ def forward(self, x, t, mu, g, m, ctx=None, cm=None, feats=False): # normalised mels [B,100,T]; m, cm [B,1,T]
187
+ """feats=True also returns {layer: speaker vector predicted from the target-frame mean} for align_at."""
188
+ c = self.temb(t) + self.gemb(g)
189
+ tok = None
190
+ if self.prompt:
191
+ if cm is None:
192
+ ctx, cm = torch.zeros_like(x), torch.zeros_like(m)
193
+ ctx = ctx * cm
194
+ e = self.ctx_enc(ctx) * cm
195
+ c = c + self.ctx_out(e.sum(-1) / cm.sum(-1).clamp_min(1))
196
+ if getattr(self, "pool", None) is not None:
197
+ tok = self.pool(e, cm)
198
+ h = self.inp(torch.cat([x, mu, ctx, cm], 1) * m)
199
+ else:
200
+ h = self.inp(torch.cat([x, mu], 1) * m)
201
+ rattn = getattr(self, "rattn", {})
202
+ stack, out = [], {}
203
+ n = len(self.blocks)
204
+ for i, b in enumerate(self.blocks):
205
+ if self.skips and str(i) in self.skip:
206
+ h = h + self.skip[str(i)](stack.pop().transpose(1, 2)).transpose(1, 2) * m
207
+ h = b(h, m, c)
208
+ if self.prompt and str(i) in self.attn:
209
+ h = self.attn[str(i)](h, m, c)
210
+ if str(i) in rattn:
211
+ h = rattn[str(i)](h, m, c, tok)
212
+ if self.skips and i < n // 2:
213
+ stack.append(h)
214
+ if feats and i in self.align_at:
215
+ w = m * (1 - cm) if cm is not None else m
216
+ out[i] = self.align[str(i)](((h * w).sum(-1) / w.sum(-1).clamp_min(1)).float())
217
+ v = self.out(self.ln(h.transpose(1, 2)).transpose(1, 2)) * m
218
+ return (v, out) if feats else v
219
+
220
+
221
+ class Flow(nn.Module):
222
+ """FlowNet + mel normalisation; sample() maps a base mel to a refined mel (log-mel units, same frames)."""
223
+
224
+ def __init__(self, mean=-5.0, std=2.5, mu_cmn=False, **kw):
225
+ """mu_cmn (prompted flow v2): remove the per-band time-mean of the base mel (its static spectral envelope:
226
+ the long-term voice colouring the base model guessed from a 512-d speaker vector) so the flow must take the
227
+ voice from the audio prompt (or g); timing / pitch / energy / phonetic detail of mu are kept. Without it, mu
228
+ made from the TRUE speaker vector already carries the training speaker's colouring and the prompt adds
229
+ little (v3p @20k: Seed-TTS SIM 31.2 -> 33.2 only)."""
230
+ super().__init__()
231
+ self.net = FlowNet(**kw)
232
+ self.mu_cmn = mu_cmn
233
+ self.register_buffer("mean", torch.tensor(float(mean)))
234
+ self.register_buffer("std", torch.tensor(float(std)))
235
+
236
+ def norm(self, m):
237
+ return (m - self.mean) / self.std
238
+
239
+ def _cmn(self, mu_n, w):
240
+ """mu_n minus its per-band mean over the frames w marks (the target frames)."""
241
+ if not self.mu_cmn:
242
+ return mu_n
243
+ return mu_n - (mu_n * w).sum(-1, keepdim=True) / w.sum(-1, keepdim=True).clamp_min(1)
244
+
245
+ @torch.no_grad()
246
+ def sample(self, mu, g, m=None, steps=10, temp=0.8, method="midpoint", seed=None, ctx=None, cm=None, cfg=0.0):
247
+ """ctx / cm (prompted flow): real mel on the context frames (log-mel units), mask 1 there; mu is replaced by
248
+ the context on those frames, as in training. Returns every frame (callers slice off the context).
249
+ cfg (prompted flow): classifier-free guidance on the prompt, v + cfg * (v - v_no_prompt); the no-prompt
250
+ velocity is the same net with the context removed (trained on p_none of the items)."""
251
+ m = torch.ones_like(mu[:, :1]) if m is None else m
252
+ gen = None if seed is None else torch.Generator(device=mu.device).manual_seed(seed)
253
+ mu_n = self._cmn(self.norm(mu), m if cm is None else m * (1 - cm))
254
+ if cm is not None:
255
+ ctx = self.norm(ctx) * cm
256
+ mu_n = torch.where(cm.bool(), ctx, mu_n)
257
+ x = temp * torch.randn(mu_n.shape, device=mu.device, generator=gen) * m
258
+ ts = torch.linspace(0, 1, steps + 1, device=mu.device)
259
+
260
+ def vel(x, t):
261
+ v = self.net(x, t.expand(x.shape[0]), mu_n, g, m, ctx, cm)
262
+ if cfg and cm is not None:
263
+ v = v + cfg * (v - self.net(x, t.expand(x.shape[0]), mu_n, g, m, None, None))
264
+ return v
265
+
266
+ for i in range(steps):
267
+ t0, dt = ts[i], ts[i + 1] - ts[i]
268
+ v = vel(x, t0)
269
+ if method == "midpoint":
270
+ v = vel(x + 0.5 * dt * v, t0 + 0.5 * dt)
271
+ x = x + dt * v
272
+ return x * self.std + self.mean
273
+
274
+ @staticmethod
275
+ def from_state(state, cfg, device="cpu"):
276
+ f = Flow(**cfg)
277
+ f.load_state_dict(state)
278
+ return f.to(device).eval()
steertts/mamba3/__init__.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Mamba-3: a paper-faithful, verified implementation of Lahoti et al. (arXiv 2603.15569).
2
+
3
+ Ported from the MMSE research repo (drowsiness-uta-rldd/mamba3, commit c5280e6+), without the MMSE
4
+ multi-stream extension or the LM tooling. Checked fwd+bwd at float64 against the recurrence:
5
+ - exponential-trapezoidal discretization with DATA-DEPENDENT lambda_t (Prop 1)
6
+ - complex state as real block-diagonal rotations / data-dependent RoPE (Prop 2-4, eq 11)
7
+ - MIMO rank-R (Sec 3.3)
8
+ - BCNorm/QKNorm on B,C + learnable data-independent B,C biases
9
+ - output gating
10
+ Recorded deviations: data-independent A (Remark 1 says equivalent); MIMO input uses a full DPR
11
+ projection rather than the paper's DP+PR (R=1 identical).
12
+
13
+ Paths: `Mamba3Layer.forward` (O(T) recurrence, reference), `scan_forward` (chunked scan),
14
+ `scan_forward_ssd` (SSD reduction, the fast path for training). Run `python -m
15
+ steertts.mamba3.tests` after any change.
16
+ """
17
+ from .layer import Mamba3Layer, rotate_pairs, ssm_scan
18
+ from .scan import scan_forward, chunked_scan, rotate_by, cumulative_angles
19
+ from .ssd import scan_forward_ssd, ssd_inputs, ssd_pytorch
20
+
21
+ __all__ = [
22
+ "Mamba3Layer", "rotate_pairs", "ssm_scan",
23
+ "scan_forward", "chunked_scan", "rotate_by", "cumulative_angles",
24
+ "scan_forward_ssd", "ssd_inputs", "ssd_pytorch",
25
+ ]