Text-to-Speech
Core ML
Supertonic
speech
audio
tts
ane
apple-silicon
flow-matching
diffusion
multilingual
Instructions to use FluidInference/supertonic-3-coreml with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Supertonic
How to use FluidInference/supertonic-3-coreml with Supertonic:
from supertonic import TTS tts = TTS(auto_download=True) style = tts.get_voice_style(voice_name="M1") text = "The train delay was announced at 4:45 PM on Wed, Apr 3, 2024 due to track maintenance." wav, duration = tts.synthesize(text, voice_style=style) tts.save_audio(wav, "output.wav")
- Notebooks
- Google Colab
- Kaggle
Upload VectorEstimatorVariants/README.md
Browse files
VectorEstimatorVariants/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Supertonic-3 β CoreML conversion
|
| 2 |
+
|
| 3 |
+
Hand-port of [Supertone Supertonic-3 v1.7.3](https://huggingface.co/Supertone/supertonic-3)
|
| 4 |
+
from ONNX to PyTorch to CoreML. 31 languages, 44.1 kHz, flow-matching
|
| 5 |
+
diffusion (8 denoising steps, classifier-free guidance baked into the
|
| 6 |
+
ONNX graph via batch-2 duplication).
|
| 7 |
+
|
| 8 |
+
End-to-end pipeline:
|
| 9 |
+
|
| 10 |
+
```
|
| 11 |
+
text β UnicodeProcessor β token_ids, text_mask
|
| 12 |
+
βββ duration_predictor β duration_sec
|
| 13 |
+
βββ text_encoder β text_emb [B, 256, T]
|
| 14 |
+
β
|
| 15 |
+
sample_noisy_latent(duration_sec) β noisy [B, 144, L], latent_mask
|
| 16 |
+
β
|
| 17 |
+
for 8 steps: vector_estimator(noisy, text_emb, style, masks, step, total)
|
| 18 |
+
β
|
| 19 |
+
vocoder(denoised_latent) β wav [B, 512*6*L]
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
Audio chunk granularity:
|
| 23 |
+
- AE / vocoder frame: 512 / 44100 β **11.6 ms**
|
| 24 |
+
- TTL latent slot (model "tick"): 512 Γ 6 / 44100 β **69.7 ms**
|
| 25 |
+
|
| 26 |
+
## Layout
|
| 27 |
+
|
| 28 |
+
```
|
| 29 |
+
models/tts/supertonic-3/
|
| 30 |
+
βββ README.md
|
| 31 |
+
βββ pyproject.toml # uv project (Python 3.11, torch + coremltools 8)
|
| 32 |
+
βββ coreml/
|
| 33 |
+
βββ trials.md # numerical-parity bug log (4 vector_estimator gotchas)
|
| 34 |
+
βββ __init__.py
|
| 35 |
+
βββ common.py # ONNX-graph loader utilities (assign_param, etc.)
|
| 36 |
+
βββ text_encoder.py # PyTorch port: build_text_encoder_from_onnx
|
| 37 |
+
βββ duration_predictor.py
|
| 38 |
+
βββ vector_estimator.py
|
| 39 |
+
βββ vocoder.py
|
| 40 |
+
βββ convert_coreml.py # PyTorch trace -> .mlpackage for all 4 modules
|
| 41 |
+
βββ validate.py # ONNX vs PyTorch parity check
|
| 42 |
+
βββ verify_coreml.py # CoreML vs PyTorch parity check
|
| 43 |
+
βββ infer.py # end-to-end PyTorch TTS driver (text -> wav)
|
| 44 |
+
βββ infer_coreml.py # end-to-end CoreML TTS driver (text -> wav)
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
## Setup
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
cd models/tts/supertonic-3/
|
| 51 |
+
uv sync
|
| 52 |
+
|
| 53 |
+
# Fetch upstream ONNX + style + tokenizer assets
|
| 54 |
+
mkdir -p build/_onnx build/voice_styles
|
| 55 |
+
HF=https://huggingface.co/Supertone/supertonic-3/resolve/main
|
| 56 |
+
for f in text_encoder duration_predictor vector_estimator vocoder; do
|
| 57 |
+
curl -L $HF/_onnx/${f}.onnx -o build/_onnx/${f}.onnx
|
| 58 |
+
done
|
| 59 |
+
curl -L $HF/_onnx/tts.json -o build/_onnx/tts.json
|
| 60 |
+
curl -L $HF/_onnx/unicode_indexer.json -o build/_onnx/unicode_indexer.json
|
| 61 |
+
curl -L $HF/voice_styles/M1.json -o build/voice_styles/M1.json
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
## Convert
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
# FP32 (numerical reference; ALL modules fall back to CPU on ANE)
|
| 68 |
+
uv run python -m coreml.convert_coreml build/_onnx --out-dir build/_mlpackage
|
| 69 |
+
|
| 70 |
+
# FP16 (required for ANE residency; 3/4 modules land on ANE β see Profile below)
|
| 71 |
+
uv run python -m coreml.convert_coreml build/_onnx --fp16 --out-dir build/_mlpackage_fp16
|
| 72 |
+
|
| 73 |
+
# Fixed-shape VectorEstimator variant for ANE profiling (RangeDim/Enum hit
|
| 74 |
+
# ANE shape limits β see trials.md "Dynamic shapes vs ANE"):
|
| 75 |
+
uv run python -m coreml.convert_ve_fixed \
|
| 76 |
+
--onnx build/_onnx/vector_estimator.onnx \
|
| 77 |
+
--out build/_mlpackage_fp16_fixed/VectorEstimator_L128.mlpackage \
|
| 78 |
+
--L 128 --T 128
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
Produces four `.mlpackage` bundles (FP32 ~380 MB, FP16 ~190 MB; mlprogram,
|
| 82 |
+
iOS 18+):
|
| 83 |
+
|
| 84 |
+
| Module | FP32 | FP16 | Variable axes |
|
| 85 |
+
| ------------------ | ----- | ------ | ----------------------------------------- |
|
| 86 |
+
| vocoder | 97 MB | 48 MB | `latent.L_ttl` = RangeDim(4..512) |
|
| 87 |
+
| text_encoder | 35 MB | 17 MB | fixed `text.T = 128` |
|
| 88 |
+
| duration_predictor | 3.5 MB| 1.8 MB | fixed `text.T = 128` |
|
| 89 |
+
| vector_estimator | 244 MB| 122 MB | `latent.L` & `text.T` = RangeDim(17..512) |
|
| 90 |
+
|
| 91 |
+
## Validate
|
| 92 |
+
|
| 93 |
+
```bash
|
| 94 |
+
# ONNX vs PyTorch port (per module)
|
| 95 |
+
uv run python -m coreml.validate
|
| 96 |
+
|
| 97 |
+
# CoreML vs PyTorch port (per module)
|
| 98 |
+
uv run python -m coreml.verify_coreml
|
| 99 |
+
|
| 100 |
+
# End-to-end PyTorch (writes WAV)
|
| 101 |
+
uv run python -m coreml.infer \
|
| 102 |
+
--onnx-dir build/_onnx \
|
| 103 |
+
--voice-style build/voice_styles/M1.json \
|
| 104 |
+
--text "Hello world."
|
| 105 |
+
|
| 106 |
+
# End-to-end CoreML (writes WAV)
|
| 107 |
+
uv run python -m coreml.infer_coreml \
|
| 108 |
+
--mlpackage-dir build/_mlpackage \
|
| 109 |
+
--tts-json build/_onnx/tts.json \
|
| 110 |
+
--unicode-indexer build/_onnx/unicode_indexer.json \
|
| 111 |
+
--voice-style build/voice_styles/M1.json \
|
| 112 |
+
--text "Hello world."
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
Final parity vs ONNX-Runtime CPU:
|
| 116 |
+
|
| 117 |
+
| Module | PyTorch vs ONNX max_abs | CoreML vs PyTorch max_abs |
|
| 118 |
+
| ------------------ | ----------------------- | ------------------------- |
|
| 119 |
+
| vocoder | 2.53e-4 | 1.41e-6 |
|
| 120 |
+
| text_encoder | 9.77e-2 (relaxed tol) | 2.33e-4 |
|
| 121 |
+
| duration_predictor | 3.04e-6 | 3.82e-6 |
|
| 122 |
+
| vector_estimator | 1.21e-3 | 2.96e-5 |
|
| 123 |
+
|
| 124 |
+
End-to-end CoreML on M-series CPU+ANE: **~0.74 s** to synthesize
|
| 125 |
+
6.32 s of audio for a single English sentence (RTFx β 8.5x), 8
|
| 126 |
+
denoising steps. ASR-verified against FluidAudio Parakeet TDT.
|
| 127 |
+
|
| 128 |
+
## Profile (FP16, Apple M2, macOS 26.5, `cpu_and_neural_engine`)
|
| 129 |
+
|
| 130 |
+
| Module | CPU% | GPU% | ANE% | Predict | Notes |
|
| 131 |
+
| ----------------------------------- | ---- | ---- | ---- | ------- | ----- |
|
| 132 |
+
| duration_predictor | 100 | 0 | 0 | 0.82 ms | tiny, CPU-bound |
|
| 133 |
+
| text_encoder (T=128) | 38 | 0 | 62 | 2.15 ms | partial ANE |
|
| 134 |
+
| vocoder (RangeDim L 4..512) | 0 | 0 | 100 | 1.17 ms | full ANE, 4Γ vs FP32 |
|
| 135 |
+
| vector_estimator (RangeDim 17..512) | β | β | β | β | dynamic shapes crash on ANE β must bucket to fixed L |
|
| 136 |
+
| vector_estimator (fixed L=128 T=128)| 6 | 0 | 94 | 3.8 ms | **lands on ANE** (M5 Pro): NE 3.82 ms vs CPU-only 14.20 ms = 3.7Γ. `ANECCompile FAILED` msg is non-fatal β see trials.md "M5 Pro re-evaluation" |
|
| 137 |
+
| vector_estimator (fixed L=256/512) | 4 | 0 | 96 | 8.4 / 16.4 ms | ANE holds across buckets; int8 halves size (64.5 MB) at same speed/parity 41.5 dB |
|
| 138 |
+
|
| 139 |
+
See `coreml/trials.md` β "ANE residency profiling" for the full breakdown,
|
| 140 |
+
the float-mask refactor that eliminated the bool-tile blocker, the
|
| 141 |
+
residual opaque `ANECCompile() FAILED (11)`, and the EnumeratedShapes
|
| 142 |
+
runtime stride gotcha.
|
| 143 |
+
|
| 144 |
+
## Critical gotchas
|
| 145 |
+
|
| 146 |
+
See `coreml/trials.md` for the full log. Highlights:
|
| 147 |
+
|
| 148 |
+
1. **CFG via batch-2 duplication** β the ONNX vector_estimator tiles
|
| 149 |
+
inputs to batch=2, runs cond + uncond in parallel, then combines
|
| 150 |
+
with `(noisy + (1/total)*(4*cond - 3*uncond)) * mask`. The cond
|
| 151 |
+
style key is **not** the user `style_ttl` β it is a learned
|
| 152 |
+
initializer at `/vector_estimator/Expand_output_0`.
|
| 153 |
+
2. **Rotary is length-normalized** β `angles = (pos / sum(mask)) * theta`,
|
| 154 |
+
divisor differs for Q (latent_mask) and K (text_mask).
|
| 155 |
+
3. **Attention divisor is 16.0**, not `sqrt(dk)=8`. Off-by-2x in scoring.
|
| 156 |
+
4. **Style attention applies `tanh(K)`** before the score matmul; text
|
| 157 |
+
attention does not.
|
| 158 |
+
5. **Replicate-pad lower bound** β ConvNeXt depthwise pads scale with
|
| 159 |
+
dilation: `pad = (K-1)*D/2`. CoreML enforces `pad β€ dim-1` at load
|
| 160 |
+
time, hence `RangeDim.lower_bound = 17` for vector_estimator and
|
| 161 |
+
`4` for vocoder.
|
| 162 |
+
6. **int32 vs int64 tokens** β CoreML wants int32, PyTorch indexes int64.
|
| 163 |
+
Wrap modules with a tiny `_Int32Wrapper` that casts inside the
|
| 164 |
+
traced graph so the external input stays int32.
|
| 165 |
+
7. **Python 3.14 has no BlobWriter** β pin `requires-python = ">=3.11,<3.13"`.
|
| 166 |
+
8. **Float masking, not bool masking** β `masked_fill(mask==0, -inf)` and
|
| 167 |
+
`where(mask==0, 0, attn)` compile to `bool tile`/`select` ops that ANE
|
| 168 |
+
rejects. Use `scores - (1.0 - mask) * 1e4` (additive) and `attn * mask`
|
| 169 |
+
(multiplicative) instead. Lifts vector_estimator from 89.6% β 93.0%
|
| 170 |
+
ANE-eligible (though the residual opaque `ANECCompile() FAILED (11)`
|
| 171 |
+
still blocks final ANE landing β see trials.md).
|
| 172 |
+
9. **coremltools `_int` cast with (1,) tensor** β `aten::Int` on a
|
| 173 |
+
(1,)-shape int tensor trips `TypeError: only 0-dimensional arrays can
|
| 174 |
+
be converted to Python scalars` inside coremltools' `_cast` handler.
|
| 175 |
+
`convert_coreml.py` monkey-patches `_cast` (`_patch_int_cast`) to
|
| 176 |
+
squeeze (1,) β scalar before forwarding.
|
| 177 |
+
|
| 178 |
+
## Upstream + downstream
|
| 179 |
+
|
| 180 |
+
- Upstream: <https://huggingface.co/Supertone/supertonic-3>
|
| 181 |
+
- Reference Python driver: <https://github.com/supertone-inc/supertonic/blob/main/py/helper.py>
|
| 182 |
+
- Republished CoreML: `FluidInference/supertonic-3-coreml` (HuggingFace)
|
| 183 |
+
- FluidAudio Swift integration: `Sources/FluidAudio/TTS/Supertonic3/`
|