owensong commited on
Commit
132edf8
·
verified ·
1 Parent(s): 1cf80bf

Move verified ONNX export to its dedicated repository

Browse files
README.md CHANGED
@@ -262,11 +262,14 @@ The result is a 24 kHz mono `float32` waveform. Long input is split at punctuati
262
 
263
  ### ONNX Runtime
264
 
265
- A verified FP32 ONNX export is included under [`onnx/`](https://huggingface.co/owensong/Inflect-Micro-v2/tree/main/onnx).
266
- It supports dynamic lengths, CPU/CUDA/DirectML provider selection, deterministic
267
- seeds, and the same long-text wrapper without importing PyTorch:
 
268
 
269
  ```bash
 
 
270
  python -m pip install -r onnx/requirements.txt
271
  python onnx/inference_onnx.py \
272
  --text "The complete model now runs through ONNX Runtime." \
@@ -278,7 +281,7 @@ python onnx/inference_onnx.py \
278
  The neural model is split into `duration.onnx` and `decode.onnx`; together they
279
  contain the complete learned text-to-waveform path. The English eSpeak-ng
280
  frontend remains CPU-side code. See the
281
- [`ONNX guide`](https://huggingface.co/owensong/Inflect-Micro-v2/blob/main/onnx/README.md)
282
  for graph contracts, provenance, parity measurements, browser deployment, and
283
  re-export instructions.
284
 
@@ -342,7 +345,7 @@ This release is inference-first. New-voice and new-language adaptation are **not
342
  | `inference.py` | Public Python API and CLI |
343
  | `inflect_vits_frontend.py` | English normalization, phonemization, and punctuation frontend |
344
  | `runtime/` | Self-contained model implementation |
345
- | `onnx/` | Verified FP32 ONNX graphs, torch-free runner, parity report, checksums, and exporter |
346
  | `samples/` | Held-out example generations |
347
  | `evaluation/final/` | Frozen benchmark prompts, reports, and protocol artifacts |
348
  | `docs/` | API, deployment, evaluation, adaptation, and export documentation |
 
262
 
263
  ### ONNX Runtime
264
 
265
+ The official verified FP32 export is published separately as
266
+ [`Inflect-Micro-v2-ONNX`](https://huggingface.co/owensong/Inflect-Micro-v2-ONNX).
267
+ It supports dynamic lengths, CPU/CUDA/DirectML provider selection,
268
+ deterministic seeds, and the same long-text wrapper without importing PyTorch:
269
 
270
  ```bash
271
+ git clone https://huggingface.co/owensong/Inflect-Micro-v2-ONNX
272
+ cd Inflect-Micro-v2-ONNX
273
  python -m pip install -r onnx/requirements.txt
274
  python onnx/inference_onnx.py \
275
  --text "The complete model now runs through ONNX Runtime." \
 
281
  The neural model is split into `duration.onnx` and `decode.onnx`; together they
282
  contain the complete learned text-to-waveform path. The English eSpeak-ng
283
  frontend remains CPU-side code. See the
284
+ [`ONNX repository`](https://huggingface.co/owensong/Inflect-Micro-v2-ONNX)
285
  for graph contracts, provenance, parity measurements, browser deployment, and
286
  re-export instructions.
287
 
 
345
  | `inference.py` | Public Python API and CLI |
346
  | `inflect_vits_frontend.py` | English normalization, phonemization, and punctuation frontend |
347
  | `runtime/` | Self-contained model implementation |
348
+ | [`Inflect-Micro-v2-ONNX`](https://huggingface.co/owensong/Inflect-Micro-v2-ONNX) | Separate official FP32 ONNX graphs, torch-free runner, parity report, checksums, and exporter |
349
  | `samples/` | Held-out example generations |
350
  | `evaluation/final/` | Frozen benchmark prompts, reports, and protocol artifacts |
351
  | `docs/` | API, deployment, evaluation, adaptation, and export documentation |
docs/EXPORTS.md CHANGED
@@ -6,14 +6,16 @@
6
  | --- | --- | --- |
7
  | PyTorch FP32 | Supported | Canonical weights and the fully tested runtime. |
8
  | PyTorch FP16/BF16 | Not released | Smaller in memory, but not validated for waveform quality or CPU compatibility. |
9
- | ONNX FP32 | Supported | Verified dynamic-length graphs, torch-free Python runner, and reproducible exporter under `onnx/`. |
10
  | INT8 / INT4 | Not released | Naive weight quantization can audibly damage the integrated waveform decoder. |
11
  | GGUF | Not applicable today | GGUF targets transformer/LLM runtimes and is not a drop-in container for this VITS-family convolutional waveform model. |
12
  | Core ML / TFLite | Not released | Requires separate conversion and target-device validation. |
13
 
14
  ## Supported ONNX package
15
 
16
- The official ONNX package contains:
 
 
17
 
18
  - `onnx/duration.onnx`: token sequence to aligned acoustic distribution;
19
  - `onnx/decode.onnx`: aligned distribution and seeded noise to waveform;
@@ -26,7 +28,7 @@ seeded latent sampling, speed and variation controls, punctuation-aware
26
  long-text chunking, and CPU/CUDA/DirectML provider selection. eSpeak-ng remains
27
  the non-neural English frontend and is not embedded in the ONNX graphs.
28
 
29
- See [`onnx/README.md`](https://huggingface.co/owensong/Inflect-Micro-v2/blob/main/onnx/README.md)
30
  for installation and verified parity measurements.
31
 
32
  ## Why only verified FP32 ships
 
6
  | --- | --- | --- |
7
  | PyTorch FP32 | Supported | Canonical weights and the fully tested runtime. |
8
  | PyTorch FP16/BF16 | Not released | Smaller in memory, but not validated for waveform quality or CPU compatibility. |
9
+ | ONNX FP32 | Supported | Separate official [`Inflect-Micro-v2-ONNX`](https://huggingface.co/owensong/Inflect-Micro-v2-ONNX) repository with verified dynamic-length graphs, a torch-free Python runner, and a reproducible exporter. |
10
  | INT8 / INT4 | Not released | Naive weight quantization can audibly damage the integrated waveform decoder. |
11
  | GGUF | Not applicable today | GGUF targets transformer/LLM runtimes and is not a drop-in container for this VITS-family convolutional waveform model. |
12
  | Core ML / TFLite | Not released | Requires separate conversion and target-device validation. |
13
 
14
  ## Supported ONNX package
15
 
16
+ The official
17
+ [`Inflect-Micro-v2-ONNX`](https://huggingface.co/owensong/Inflect-Micro-v2-ONNX)
18
+ package contains:
19
 
20
  - `onnx/duration.onnx`: token sequence to aligned acoustic distribution;
21
  - `onnx/decode.onnx`: aligned distribution and seeded noise to waveform;
 
28
  long-text chunking, and CPU/CUDA/DirectML provider selection. eSpeak-ng remains
29
  the non-neural English frontend and is not embedded in the ONNX graphs.
30
 
31
+ See the [`Inflect-Micro-v2-ONNX` model card](https://huggingface.co/owensong/Inflect-Micro-v2-ONNX)
32
  for installation and verified parity measurements.
33
 
34
  ## Why only verified FP32 ships
onnx/README.md CHANGED
@@ -1,120 +1,10 @@
1
- # Inflect-Micro-v2 ONNX
2
 
3
- Official, verified FP32 ONNX Runtime export of
4
- [Inflect-Micro-v2](https://huggingface.co/owensong/Inflect-Micro-v2).
5
- The neural model is split into two graphs:
6
 
7
- | File | Purpose | Size |
8
- | --- | --- | ---: |
9
- | `duration.onnx` | Text tokens to aligned acoustic distribution | 7.32 MB |
10
- | `decode.onnx` | Acoustic distribution and seeded noise to 24 kHz waveform | 30.43 MB |
11
 
12
- The complete ONNX neural weights are about **37.75 MB**. The original FP32
13
- PyTorch checkpoint remains the canonical source of truth.
14
-
15
- ## Install
16
-
17
- ```bash
18
- python -m pip install -r onnx/requirements.txt
19
- ```
20
-
21
- ONNX Runtime CPU is installed by default. For another execution provider, use
22
- the appropriate ONNX Runtime package for the target platform.
23
-
24
- ## Generate speech
25
-
26
- From the repository root:
27
-
28
- ```bash
29
- python onnx/inference_onnx.py \
30
- --text "A compact voice can still sound clear and expressive." \
31
- --output sample.wav \
32
- --provider cpu \
33
- --speed 1.0 \
34
- --variation 0.667 \
35
- --seed 7
36
- ```
37
-
38
- Python:
39
-
40
- ```python
41
- import sys
42
-
43
- sys.path.insert(0, "onnx")
44
- from inference_onnx import InflectONNX
45
-
46
- tts = InflectONNX(".", provider="cpu")
47
- sample_rate, waveform = tts.synthesize(
48
- "Inflect runs through ONNX Runtime.",
49
- speed=1.0,
50
- variation=0.667,
51
- seed=7,
52
- )
53
- ```
54
-
55
- Accepted provider aliases are `cpu`, `cuda`, and `directml`. The corresponding
56
- provider must exist in the installed ONNX Runtime build.
57
-
58
- ## What the export includes
59
-
60
- - Dynamic text and waveform lengths
61
- - The duration predictor, acoustic generator, flow, and waveform decoder
62
- - Seeded stochastic latent sampling in the Python wrapper
63
- - The same punctuation-aware long-text chunking used by the PyTorch package
64
- - 24 kHz mono `float32` waveform output
65
-
66
- The English normalization and phonemization frontend remains Python code and
67
- uses eSpeak-ng on the CPU. The ONNX files contain the learned neural graphs,
68
- not eSpeak-ng itself.
69
-
70
- ## Verification
71
-
72
- The packaged graphs pass:
73
-
74
- - ONNX checker validation at opset 17
75
- - Dynamic-length inference tests
76
- - Short- and long-form end-to-end synthesis
77
- - Same-seed determinism and different-seed variation checks
78
- - Direct numerical comparison with the canonical PyTorch checkpoint
79
- - Independent re-export comparison from the published `model.pth`
80
-
81
- The direct PyTorch comparison measured a maximum absolute waveform error of
82
- `1.7583e-05`, mean absolute error of `2.2595e-07`, and correlation above
83
- `0.9999999999`. An independent re-export matched this package within
84
- `1.0617e-07` maximum waveform error.
85
-
86
- See [`parity_report.json`](parity_report.json), [`SOURCE.json`](SOURCE.json),
87
- and [`checksums.sha256`](checksums.sha256) for machine-readable details.
88
-
89
- ## Re-export
90
-
91
- The export script is included for reproducibility:
92
-
93
- ```bash
94
- python -m pip install torch==2.4.1 onnx==1.19.1
95
- python onnx/export_onnx.py \
96
- --model-dir . \
97
- --output-dir onnx-reexport \
98
- --model-name Inflect-Micro-v2 \
99
- --source-repository owensong/Inflect-Micro-v2 \
100
- --source-revision 3e8c567f9bce1a309eea12d38ca9fbf7050df72e
101
- ```
102
-
103
- Serialization hashes can differ across exporter versions even when numerical
104
- outputs match. Validate outputs rather than expecting byte-identical files.
105
-
106
- ## Browser implementation and credit
107
-
108
- Robert Bak produced the original ONNX conversion and a complete browser runner:
109
- [robertbak/webtts-inflect](https://github.com/robertbak/webtts-inflect).
110
- The official package preserves that conversion credit and adds checkpoint
111
- provenance, metadata, reproducible export tooling, and Python ONNX Runtime
112
- validation. The browser project is Apache-2.0 and runs synthesis locally with
113
- ONNX Runtime Web and an eSpeak-ng WASM frontend.
114
-
115
- ## Quantization status
116
-
117
- Only verified FP32 ONNX graphs are released. INT8 and INT4 are not included:
118
- naive quantization of the flow and waveform decoder can produce audible
119
- degradation, and no quantized build has passed matched listening and
120
- intelligibility validation yet.
 
1
+ # ONNX export moved
2
 
3
+ The official verified FP32 ONNX Runtime package now lives in its own model
4
+ repository:
 
5
 
6
+ ## [owensong/Inflect-Micro-v2-ONNX](https://huggingface.co/owensong/Inflect-Micro-v2-ONNX)
 
 
 
7
 
8
+ This canonical repository remains the PyTorch base model. The separate ONNX
9
+ repository includes both graphs, the torch-free runner, export script, parity
10
+ report, checksums, runtime files, and license notices.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
onnx/SOURCE.json DELETED
@@ -1,16 +0,0 @@
1
- {
2
- "format": "inflect_v2_onnx_source_v1",
3
- "model": "Inflect-Micro-v2",
4
- "source_repository": "owensong/Inflect-Micro-v2",
5
- "source_revision": "3e8c567f9bce1a309eea12d38ca9fbf7050df72e",
6
- "source_model_file": "model.pth",
7
- "source_model_sha256": "3eede065c9ccfa88ade0a5a9a5c23de34afcbbb32213e59aad44d5cf100fdee8",
8
- "conversion_repository": "https://github.com/robertbak/webtts-inflect",
9
- "conversion_commit": "60832b1978115e4ed7fdddce4d9b9fd9161f9fe7",
10
- "conversion_author": "Robert Bak",
11
- "official_packaging": "Owen Song",
12
- "precision": "FP32",
13
- "onnx_opset": 17,
14
- "sample_rate_hz": 24000,
15
- "license": "Apache-2.0"
16
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
onnx/checksums.sha256 DELETED
@@ -1,8 +0,0 @@
1
- 7940923add86f76e7fa78d910b0632ca1779f8cc9a2ca2b49236381a9ca77183 decode.onnx
2
- b728ca2564b9e5b7d6cf5e446f65e02a6fe2f1880ba281466fec93a667dd2388 duration.onnx
3
- 77bd27277861c8cae670247e3c9e3b976134a4886506fb069331aa6828752be6 export_onnx.py
4
- cf48c4812f68e8315d80568e93650c64a7ad3fdfb9a0051c61a0d1d0746df274 inference_onnx.py
5
- 10c88564d24fe65ea730dc6ec98aa19ec74d3590efa427595a3c122c4c0f7039 parity_report.json
6
- ecfaf6a3edca6ffc3152a61ccc6c27240f42daaa0ddf6efc08e1f9b277295734 README.md
7
- 43abd2c128e4296f1f0171f12946f3fc7dee3f8c8e2bb1858bcae6b43b62b3c9 requirements.txt
8
- e950bf44034b011a64bba3773b388e386418ef952c3cd17ceefda5bfa94a3aaa SOURCE.json
 
 
 
 
 
 
 
 
 
onnx/decode.onnx DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:7940923add86f76e7fa78d910b0632ca1779f8cc9a2ca2b49236381a9ca77183
3
- size 30427790
 
 
 
 
onnx/duration.onnx DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:b728ca2564b9e5b7d6cf5e446f65e02a6fe2f1880ba281466fec93a667dd2388
3
- size 7322687
 
 
 
 
onnx/export_onnx.py DELETED
@@ -1,261 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import hashlib
5
- import json
6
- import logging
7
- import sys
8
- from pathlib import Path
9
-
10
- import onnx
11
- import torch
12
- from torch import nn
13
-
14
-
15
- PACKAGE_ROOT = Path(__file__).resolve().parents[1]
16
- commons = None
17
-
18
-
19
- class DurationGraph(nn.Module):
20
- def __init__(self, model: nn.Module) -> None:
21
- super().__init__()
22
- self.enc_p = model.enc_p
23
- self.dp = model.dp
24
-
25
- def forward(
26
- self,
27
- tokens: torch.Tensor,
28
- lengths: torch.Tensor,
29
- length_scale: torch.Tensor,
30
- ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
31
- hidden, m_p, logs_p, x_mask = self.enc_p(tokens, lengths)
32
- logw = self.dp(hidden, x_mask, g=None)
33
- durations = torch.ceil(torch.exp(logw) * x_mask * length_scale)
34
- y_lengths = torch.clamp_min(torch.sum(durations, [1, 2]), 1).long()
35
- if commons is None:
36
- raise RuntimeError("Runtime helpers were not initialized.")
37
- y_mask = torch.unsqueeze(commons.sequence_mask(y_lengths, None), 1).to(
38
- x_mask.dtype
39
- )
40
- attention_mask = torch.unsqueeze(x_mask, 2) * torch.unsqueeze(y_mask, -1)
41
- attention = commons.generate_path(durations, attention_mask)
42
- m_p_exp = torch.matmul(
43
- attention.squeeze(1),
44
- m_p.transpose(1, 2),
45
- ).transpose(1, 2)
46
- logs_p_exp = torch.matmul(
47
- attention.squeeze(1),
48
- logs_p.transpose(1, 2),
49
- ).transpose(1, 2)
50
- return m_p_exp, logs_p_exp, y_mask
51
-
52
-
53
- class DecodeGraph(nn.Module):
54
- def __init__(self, model: nn.Module) -> None:
55
- super().__init__()
56
- self.flow = model.flow
57
- self.decoder = model.dec
58
-
59
- def forward(
60
- self,
61
- m_p_exp: torch.Tensor,
62
- logs_p_exp: torch.Tensor,
63
- y_mask: torch.Tensor,
64
- zp_noise: torch.Tensor,
65
- noise_scale: torch.Tensor,
66
- ) -> torch.Tensor:
67
- z_p = m_p_exp + zp_noise * torch.exp(logs_p_exp) * noise_scale
68
- z = self.flow(z_p, y_mask, g=None, reverse=True)
69
- return self.decoder(z * y_mask, g=None)
70
-
71
-
72
- def sha256(path: Path) -> str:
73
- digest = hashlib.sha256()
74
- with path.open("rb") as handle:
75
- for block in iter(lambda: handle.read(1024 * 1024), b""):
76
- digest.update(block)
77
- return digest.hexdigest()
78
-
79
-
80
- def load_model(root: Path) -> nn.Module:
81
- global commons
82
- runtime_root = root / "runtime"
83
- sys.path.insert(0, str(runtime_root))
84
- sys.path.insert(0, str(root))
85
- import commons as runtime_commons
86
- import utils
87
- from models import SynthesizerTrn
88
- from text.symbols import symbols
89
-
90
- commons = runtime_commons
91
- hps = utils.get_hparams_from_file(str(root / "config.json"))
92
- model = SynthesizerTrn(
93
- len(symbols),
94
- hps.data.filter_length // 2 + 1,
95
- hps.train.segment_size // hps.data.hop_length,
96
- **hps.model,
97
- ).cpu().eval()
98
- logger = logging.getLogger()
99
- previous_level = logger.level
100
- try:
101
- logger.setLevel(logging.WARNING)
102
- utils.load_checkpoint(str(root / "model.pth"), model, None)
103
- finally:
104
- logger.setLevel(previous_level)
105
- return model
106
-
107
-
108
- def attach_metadata(
109
- path: Path,
110
- *,
111
- model_name: str,
112
- source_repository: str,
113
- source_revision: str,
114
- source_model_sha256: str,
115
- ) -> None:
116
- graph = onnx.load(path)
117
- metadata = {
118
- "model_name": model_name,
119
- "source_repository": source_repository,
120
- "source_revision": source_revision,
121
- "source_model_sha256": source_model_sha256,
122
- "license": "Apache-2.0",
123
- "sample_rate_hz": "24000",
124
- "precision": "FP32",
125
- "onnx_opset": "17",
126
- "conversion_author": "Robert Bak (github.com/robertbak)",
127
- "official_packaging": "Owen Song (huggingface.co/owensong)",
128
- }
129
- del graph.metadata_props[:]
130
- for key, value in metadata.items():
131
- item = graph.metadata_props.add()
132
- item.key = key
133
- item.value = value
134
- onnx.checker.check_model(graph)
135
- onnx.save(graph, path)
136
-
137
-
138
- def export(
139
- root: Path,
140
- output_dir: Path,
141
- *,
142
- model_name: str,
143
- source_repository: str,
144
- source_revision: str,
145
- ) -> None:
146
- torch.manual_seed(7)
147
- model = load_model(root)
148
- duration = DurationGraph(model).eval()
149
- decode = DecodeGraph(model).eval()
150
-
151
- output_dir.mkdir(parents=True, exist_ok=True)
152
- duration_path = output_dir / "duration.onnx"
153
- decode_path = output_dir / "decode.onnx"
154
-
155
- tokens = torch.tensor(
156
- [[0, 18, 0, 61, 0, 55, 0, 48, 0, 44, 0, 46, 0]],
157
- dtype=torch.long,
158
- )
159
- lengths = torch.tensor([tokens.shape[1]], dtype=torch.long)
160
- length_scale = torch.tensor(1.0, dtype=torch.float32)
161
-
162
- torch.onnx.export(
163
- duration,
164
- (tokens, lengths, length_scale),
165
- duration_path,
166
- input_names=["tokens", "lengths", "length_scale"],
167
- output_names=["m_p_exp", "logs_p_exp", "y_mask"],
168
- dynamic_axes={
169
- "tokens": {1: "text_len"},
170
- "m_p_exp": {2: "mel_len"},
171
- "logs_p_exp": {2: "mel_len"},
172
- "y_mask": {2: "mel_len"},
173
- },
174
- opset_version=17,
175
- do_constant_folding=True,
176
- dynamo=False,
177
- )
178
-
179
- with torch.no_grad():
180
- m_p_exp, logs_p_exp, y_mask = duration(tokens, lengths, length_scale)
181
- zp_noise = torch.randn_like(m_p_exp)
182
- noise_scale = torch.tensor(0.667, dtype=torch.float32)
183
- torch.onnx.export(
184
- decode,
185
- (m_p_exp, logs_p_exp, y_mask, zp_noise, noise_scale),
186
- decode_path,
187
- input_names=[
188
- "m_p_exp",
189
- "logs_p_exp",
190
- "y_mask",
191
- "zp_noise",
192
- "noise_scale",
193
- ],
194
- output_names=["waveform"],
195
- dynamic_axes={
196
- "m_p_exp": {2: "mel_len"},
197
- "logs_p_exp": {2: "mel_len"},
198
- "y_mask": {2: "mel_len"},
199
- "zp_noise": {2: "mel_len"},
200
- "waveform": {2: "wav_len"},
201
- },
202
- opset_version=17,
203
- do_constant_folding=True,
204
- dynamo=False,
205
- )
206
-
207
- source_hash = sha256(root / "model.pth")
208
- for path in (duration_path, decode_path):
209
- attach_metadata(
210
- path,
211
- model_name=model_name,
212
- source_repository=source_repository,
213
- source_revision=source_revision,
214
- source_model_sha256=source_hash,
215
- )
216
-
217
- manifest = {
218
- "format": "inflect_v2_onnx_export_v1",
219
- "model_name": model_name,
220
- "source_repository": source_repository,
221
- "source_revision": source_revision,
222
- "source_model_sha256": source_hash,
223
- "torch_version": torch.__version__,
224
- "onnx_version": onnx.__version__,
225
- "opset": 17,
226
- "files": {
227
- duration_path.name: {
228
- "bytes": duration_path.stat().st_size,
229
- "sha256": sha256(duration_path),
230
- },
231
- decode_path.name: {
232
- "bytes": decode_path.stat().st_size,
233
- "sha256": sha256(decode_path),
234
- },
235
- },
236
- }
237
- (output_dir / "export_manifest.json").write_text(
238
- json.dumps(manifest, indent=2) + "\n",
239
- encoding="utf-8",
240
- )
241
-
242
-
243
- def main() -> None:
244
- parser = argparse.ArgumentParser(description="Export Inflect v2 to ONNX.")
245
- parser.add_argument("--model-dir", type=Path, default=PACKAGE_ROOT)
246
- parser.add_argument("--output-dir", type=Path, required=True)
247
- parser.add_argument("--model-name", required=True)
248
- parser.add_argument("--source-repository", required=True)
249
- parser.add_argument("--source-revision", required=True)
250
- args = parser.parse_args()
251
- export(
252
- args.model_dir.resolve(),
253
- args.output_dir.resolve(),
254
- model_name=args.model_name,
255
- source_repository=args.source_repository,
256
- source_revision=args.source_revision,
257
- )
258
-
259
-
260
- if __name__ == "__main__":
261
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
onnx/inference_onnx.py DELETED
@@ -1,229 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import re
5
- import sys
6
- from pathlib import Path
7
-
8
- import numpy as np
9
- import onnxruntime as ort
10
- import soundfile as sf
11
-
12
-
13
- PACKAGE_ROOT = Path(__file__).resolve().parents[1]
14
- RUNTIME_ROOT = PACKAGE_ROOT / "runtime"
15
- sys.path.insert(0, str(RUNTIME_ROOT))
16
- sys.path.insert(0, str(PACKAGE_ROOT))
17
-
18
- from inflect_vits_frontend import run_vits_frontend # noqa: E402
19
- from text.symbols import symbols # noqa: E402
20
-
21
-
22
- SAMPLE_RATE = 24_000
23
- SYMBOL_TO_ID = {symbol: index for index, symbol in enumerate(symbols)}
24
-
25
-
26
- def split_text(text: str, limit: int = 280) -> list[str]:
27
- normalized = " ".join(text.split())
28
- sentences = [
29
- part.strip()
30
- for part in re.split(r"(?<=[.!?;:])\s+", normalized)
31
- if part.strip()
32
- ]
33
- chunks: list[str] = []
34
- for sentence in sentences or [normalized]:
35
- while len(sentence) > limit:
36
- search = sentence[: limit + 1]
37
- punctuation = max(search.rfind(mark) for mark in (",", ";", ":"))
38
- split_at = (
39
- punctuation + 1
40
- if punctuation >= limit // 2
41
- else sentence.rfind(" ", 0, limit + 1)
42
- )
43
- if split_at < limit // 2:
44
- split_at = limit
45
- chunks.append(sentence[:split_at].strip())
46
- sentence = sentence[split_at:].strip()
47
- if sentence:
48
- chunks.append(sentence)
49
- return chunks
50
-
51
-
52
- def boundary_pause_seconds(chunk: str) -> float:
53
- ending = chunk.rstrip()[-1:] if chunk.strip() else ""
54
- return {
55
- "?": 0.28,
56
- "!": 0.24,
57
- ".": 0.22,
58
- ";": 0.16,
59
- ":": 0.13,
60
- ",": 0.09,
61
- }.get(ending, 0.08)
62
-
63
-
64
- def edge_fade(
65
- waveform: np.ndarray,
66
- sample_rate: int = SAMPLE_RATE,
67
- milliseconds: float = 5.0,
68
- ) -> np.ndarray:
69
- frames = min(round(sample_rate * milliseconds / 1000.0), waveform.size // 2)
70
- if frames <= 0:
71
- return waveform
72
- output = waveform.copy()
73
- ramp = np.linspace(0.0, 1.0, frames, endpoint=True, dtype=np.float32)
74
- output[:frames] *= ramp
75
- output[-frames:] *= ramp[::-1]
76
- return output
77
-
78
-
79
- def phonemes_to_tokens(phoneme_text: str) -> np.ndarray:
80
- sequence = [SYMBOL_TO_ID[symbol] for symbol in phoneme_text]
81
- if not sequence:
82
- raise ValueError("The text frontend produced no speakable tokens.")
83
- with_blanks = np.zeros(len(sequence) * 2 + 1, dtype=np.int64)
84
- with_blanks[1::2] = sequence
85
- return with_blanks[None, :]
86
-
87
-
88
- def available_provider(name: str) -> str:
89
- aliases = {
90
- "cpu": "CPUExecutionProvider",
91
- "cuda": "CUDAExecutionProvider",
92
- "directml": "DmlExecutionProvider",
93
- }
94
- provider = aliases.get(name.lower(), name)
95
- available = ort.get_available_providers()
96
- if provider not in available:
97
- raise ValueError(
98
- f"Provider {provider!r} is unavailable. Installed providers: {available}"
99
- )
100
- return provider
101
-
102
-
103
- class InflectONNX:
104
- """Torch-free Inflect v2 inference through ONNX Runtime."""
105
-
106
- def __init__(
107
- self,
108
- model_dir: str | Path = PACKAGE_ROOT,
109
- provider: str = "cpu",
110
- ) -> None:
111
- root = Path(model_dir).resolve()
112
- onnx_dir = root / "onnx"
113
- selected_provider = available_provider(provider)
114
- providers = [selected_provider]
115
- if selected_provider != "CPUExecutionProvider":
116
- providers.append("CPUExecutionProvider")
117
- self.duration = ort.InferenceSession(
118
- str(onnx_dir / "duration.onnx"),
119
- providers=providers,
120
- )
121
- self.decode = ort.InferenceSession(
122
- str(onnx_dir / "decode.onnx"),
123
- providers=providers,
124
- )
125
-
126
- def _synthesize_chunk(
127
- self,
128
- text: str,
129
- *,
130
- speed: float,
131
- variation: float,
132
- seed: int,
133
- ) -> np.ndarray:
134
- phoneme_text = run_vits_frontend(text).phoneme_text
135
- tokens = phonemes_to_tokens(phoneme_text)
136
- duration_outputs = self.duration.run(
137
- ["m_p_exp", "logs_p_exp", "y_mask"],
138
- {
139
- "tokens": tokens,
140
- "lengths": np.asarray([tokens.shape[1]], dtype=np.int64),
141
- "length_scale": np.asarray(1.0 / speed, dtype=np.float32),
142
- },
143
- )
144
- m_p_exp, logs_p_exp, y_mask = duration_outputs
145
- rng = np.random.default_rng(seed)
146
- latent_noise = rng.standard_normal(m_p_exp.shape, dtype=np.float32)
147
- waveform = self.decode.run(
148
- ["waveform"],
149
- {
150
- "m_p_exp": m_p_exp,
151
- "logs_p_exp": logs_p_exp,
152
- "y_mask": y_mask,
153
- "zp_noise": latent_noise,
154
- "noise_scale": np.asarray(variation, dtype=np.float32),
155
- },
156
- )[0]
157
- return edge_fade(np.asarray(waveform, dtype=np.float32).reshape(-1))
158
-
159
- def synthesize(
160
- self,
161
- text: str,
162
- *,
163
- speed: float = 1.0,
164
- variation: float = 0.667,
165
- seed: int = 0,
166
- ) -> tuple[int, np.ndarray]:
167
- normalized = " ".join(text.split())
168
- if not normalized:
169
- raise ValueError("Text must not be empty.")
170
- if not 0.5 <= speed <= 2.0:
171
- raise ValueError("speed must be between 0.5 and 2.0")
172
- if not 0.0 <= variation <= 1.0:
173
- raise ValueError("variation must be between 0.0 and 1.0")
174
-
175
- chunks = split_text(normalized)
176
- pieces: list[np.ndarray] = []
177
- for index, chunk in enumerate(chunks):
178
- if index:
179
- pause = boundary_pause_seconds(chunks[index - 1])
180
- pieces.append(
181
- np.zeros(round(SAMPLE_RATE * pause), dtype=np.float32)
182
- )
183
- pieces.append(
184
- self._synthesize_chunk(
185
- chunk,
186
- speed=speed,
187
- variation=variation,
188
- seed=seed + index,
189
- )
190
- )
191
- waveform = np.clip(np.concatenate(pieces), -1.0, 1.0)
192
- return SAMPLE_RATE, waveform
193
-
194
- def save(self, text: str, output: str | Path, **kwargs: object) -> Path:
195
- destination = Path(output)
196
- destination.parent.mkdir(parents=True, exist_ok=True)
197
- sample_rate, waveform = self.synthesize(text, **kwargs)
198
- sf.write(destination, waveform, sample_rate)
199
- return destination
200
-
201
-
202
- def main() -> None:
203
- parser = argparse.ArgumentParser(description="Run Inflect v2 through ONNX Runtime.")
204
- parser.add_argument("--model-dir", type=Path, default=PACKAGE_ROOT)
205
- parser.add_argument("--text", required=True)
206
- parser.add_argument("--output", type=Path, required=True)
207
- parser.add_argument(
208
- "--provider",
209
- default="cpu",
210
- help="cpu, cuda, directml, or an ONNX Runtime provider name",
211
- )
212
- parser.add_argument("--speed", type=float, default=1.0)
213
- parser.add_argument("--variation", type=float, default=0.667)
214
- parser.add_argument("--seed", type=int, default=0)
215
- args = parser.parse_args()
216
-
217
- engine = InflectONNX(args.model_dir, provider=args.provider)
218
- engine.save(
219
- args.text,
220
- args.output,
221
- speed=args.speed,
222
- variation=args.variation,
223
- seed=args.seed,
224
- )
225
- print(f"wrote {args.output} at {SAMPLE_RATE} Hz")
226
-
227
-
228
- if __name__ == "__main__":
229
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
onnx/parity_report.json DELETED
@@ -1,31 +0,0 @@
1
- {
2
- "format": "inflect_v2_onnx_parity_v1",
3
- "model": "Inflect-Micro-v2",
4
- "onnx_checker": "pass",
5
- "dynamic_lengths": "pass",
6
- "end_to_end_short_text": "pass",
7
- "end_to_end_long_text": "pass",
8
- "same_seed_determinism": "pass",
9
- "different_seed_variation": "pass",
10
- "pytorch_parity": {
11
- "duration_m_p_exp_max_abs_error": 9.5367431640625e-07,
12
- "duration_logs_p_exp_max_abs_error": 3.5762786865234375e-07,
13
- "duration_mask_exact": true,
14
- "waveform_max_abs_error": 1.75834e-05,
15
- "waveform_mean_abs_error": 2.2595e-07,
16
- "waveform_correlation": 0.9999999999394602
17
- },
18
- "independent_reexport_parity": {
19
- "duration_outputs_max_abs_error": [0.0, 0.0, 0.0],
20
- "waveform_max_abs_error": 1.0617077350616455e-07,
21
- "waveform_mean_abs_error": 4.823611288173879e-09,
22
- "waveform_correlation": 0.9999999999995599
23
- },
24
- "validation_environment": {
25
- "python": "3.12",
26
- "onnx": "1.19.1",
27
- "onnxruntime": "1.22.1",
28
- "torch_for_reference": "2.4.1+cpu",
29
- "provider": "CPUExecutionProvider"
30
- }
31
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
onnx/requirements.txt DELETED
@@ -1,7 +0,0 @@
1
- numpy>=1.26,<3
2
- onnxruntime>=1.18,<2
3
- soundfile>=0.13
4
- phonemizer>=3.3
5
- espeakng-loader>=0.2.4
6
- num2words>=0.5.14
7
- Unidecode>=1.3.8
 
 
 
 
 
 
 
 
release_manifest.json CHANGED
@@ -29,8 +29,8 @@
29
  },
30
  {
31
  "path": "README.md",
32
- "bytes": 20101,
33
- "sha256": "3250258e2ec7b8e7e312da1579dda93ff68334ed803561f4ac3dcc26a5f2d644"
34
  },
35
  {
36
  "path": "THIRD_PARTY_NOTICES.md",
@@ -144,8 +144,8 @@
144
  },
145
  {
146
  "path": "docs/EXPORTS.md",
147
- "bytes": 2808,
148
- "sha256": "bcb70436c88e92d2926941e76f141498d63e2432e3584e921a9682785c474392"
149
  },
150
  {
151
  "path": "docs/FINETUNING.md",
@@ -339,48 +339,8 @@
339
  },
340
  {
341
  "path": "onnx/README.md",
342
- "bytes": 3907,
343
- "sha256": "ecfaf6a3edca6ffc3152a61ccc6c27240f42daaa0ddf6efc08e1f9b277295734"
344
- },
345
- {
346
- "path": "onnx/SOURCE.json",
347
- "bytes": 633,
348
- "sha256": "e950bf44034b011a64bba3773b388e386418ef952c3cd17ceefda5bfa94a3aaa"
349
- },
350
- {
351
- "path": "onnx/checksums.sha256",
352
- "bytes": 653,
353
- "sha256": "d74c3c8d3a148f240d795dc603e18ac2c152a3f7ad3e45dcd66d3e14ee335b6b"
354
- },
355
- {
356
- "path": "onnx/decode.onnx",
357
- "bytes": 30427790,
358
- "sha256": "7940923add86f76e7fa78d910b0632ca1779f8cc9a2ca2b49236381a9ca77183"
359
- },
360
- {
361
- "path": "onnx/duration.onnx",
362
- "bytes": 7322687,
363
- "sha256": "b728ca2564b9e5b7d6cf5e446f65e02a6fe2f1880ba281466fec93a667dd2388"
364
- },
365
- {
366
- "path": "onnx/export_onnx.py",
367
- "bytes": 7884,
368
- "sha256": "77bd27277861c8cae670247e3c9e3b976134a4886506fb069331aa6828752be6"
369
- },
370
- {
371
- "path": "onnx/inference_onnx.py",
372
- "bytes": 7333,
373
- "sha256": "cf48c4812f68e8315d80568e93650c64a7ad3fdfb9a0051c61a0d1d0746df274"
374
- },
375
- {
376
- "path": "onnx/parity_report.json",
377
- "bytes": 1026,
378
- "sha256": "10c88564d24fe65ea730dc6ec98aa19ec74d3590efa427595a3c122c4c0f7039"
379
- },
380
- {
381
- "path": "onnx/requirements.txt",
382
- "bytes": 126,
383
- "sha256": "43abd2c128e4296f1f0171f12946f3fc7dee3f8c8e2bb1858bcae6b43b62b3c9"
384
  },
385
  {
386
  "path": "requirements-tested.txt",
@@ -496,7 +456,7 @@
496
  "format": "inflect_v2_release_manifest_v1",
497
  "onnx_release": {
498
  "format": "FP32 ONNX opset 17",
499
- "path": "onnx/",
500
  "verified": true,
501
  "released": "2026-07-25"
502
  }
 
29
  },
30
  {
31
  "path": "README.md",
32
+ "bytes": 20268,
33
+ "sha256": "d0ba29dd9605ff524fdda51ab38167c1f80cd47404a7b1ada4f9c3c2d49ad1a2"
34
  },
35
  {
36
  "path": "THIRD_PARTY_NOTICES.md",
 
144
  },
145
  {
146
  "path": "docs/EXPORTS.md",
147
+ "bytes": 2991,
148
+ "sha256": "b32a8ca478e72ee4e35a0415ef6214e18843d3253ded8cb773f7cb6e134320ae"
149
  },
150
  {
151
  "path": "docs/FINETUNING.md",
 
339
  },
340
  {
341
  "path": "onnx/README.md",
342
+ "bytes": 410,
343
+ "sha256": "933af35f9c1ce5fda876e40e183a7cd3a6dc55cc9b0684062cbf40dfdd3e5008"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
  },
345
  {
346
  "path": "requirements-tested.txt",
 
456
  "format": "inflect_v2_release_manifest_v1",
457
  "onnx_release": {
458
  "format": "FP32 ONNX opset 17",
459
+ "repository": "owensong/Inflect-Micro-v2-ONNX",
460
  "verified": true,
461
  "released": "2026-07-25"
462
  }