|
| 1 | +# Supertonic-3 TTS port (#434) |
| 2 | + |
| 3 | +## NOW — active work |
| 4 | + |
| 5 | +- [x] Licence verified from HF card 2026-09-12: model = OpenRAIL-M (`license: openrail` |
| 6 | + tag on Supertone/supertonic-3, not gated), sample code MIT. Attribution + |
| 7 | + responsible-use restrictions → registry `license` field must carry it. |
| 8 | +- [x] Blueprint read line-by-line: `supertone-inc/supertonic` `py/example_onnx.py` + |
| 9 | + `py/helper.py` (MIT). Upstream pipeline reproduced locally with onnxruntime |
| 10 | + (3.1 s clip, `M1` voice) as ground truth. |
| 11 | +- [x] All four ONNX graphs reverse-engineered (see notes below). |
| 12 | +- [ ] Converter → GGUF (single file, all voices embedded) |
| 13 | +- [ ] Reference dumper (onnxruntime intermediates, fixed noise) |
| 14 | +- [ ] C++ runtime + 12-point wiring |
| 15 | +- [ ] Kaggle build + diff + TTS→ASR roundtrip |
| 16 | +- [ ] HF upload + registry |
| 17 | + |
| 18 | +## Model facts (from onnx/tts.json + graph inspection, NOT guessed) |
| 19 | + |
| 20 | +- 4 ONNX components, opset 19, all feed-forward/deterministic: |
| 21 | + `duration_predictor` (4 MB), `text_encoder` (36 MB), `vector_estimator` |
| 22 | + (257 MB), `vocoder` (101 MB). 44.1 kHz output, 31 languages, 10 preset |
| 23 | + voices (`voice_styles/{F1..F5,M1..M5}.json`: style_ttl [1,50,256], |
| 24 | + style_dp [1,8,16]). |
| 25 | +- Text processing: NFKD → emoji strip → punctuation replacements → append "." |
| 26 | + if no trailing punctuation → wrap `<lang>`…`</lang>` → per-CHARACTER unicode |
| 27 | + codepoint → `unicode_indexer.json` (list[65536] → id, vocab 8322). |
| 28 | +- Chunking: 300 chars max (120 for ko/ja), 0.3 s silence joins. |
| 29 | + |
| 30 | +### duration_predictor |
| 31 | +char_emb(8322×64) *mask → prepend learned sentence_token (CLS at pos 0, mask |
| 32 | +extended with 1) → 6× ConvNeXt-1D (k5 dil 1..1, masked, LN eps per graph, |
| 33 | +GELU-erf, gamma) → VITS attn encoder ×2 layers (2 heads, head 32, |
| 34 | +emb_rel_k/v [1,9,32] window 4, post-LN, ReLU FFN k1 convs, masked) with |
| 35 | +GLOBAL residual: out = attn_out + convnext_out → slice CLS pos → 1×1 conv |
| 36 | +(64→64, no bias) → concat [sent(64) | style_dp.flat(128)] → Gemm 192→128 → |
| 37 | +PReLU → Gemm 128→1 → Exp = seconds. Then dur /= speed (default 1.05). |
| 38 | + |
| 39 | +### text_encoder |
| 40 | +char_emb(8322×256) → 6× ConvNeXt (dil 1,1,2,2,4,4) → VITS attn ×4 (4 heads, |
| 41 | +head 64, emb_rel [1,9,64]) → global residual add → *mask → |
| 42 | +speech_prompted_text_encoder: 2× GST cross-attn (2 heads, split axis2 → |
| 43 | +stack axis0): Q=text(256→256), K=tanh(W_k·style_key_prototype[1,50,256]), |
| 44 | +V=W_v·style_ttl, scores/scale, softmax over 50, out_fc, *mask, residual; |
| 45 | +then LayerNorm at end. Output text_emb [B,256,L]. |
| 46 | + |
| 47 | +### vector_estimator (flow matching, CFG INSIDE the graph) |
| 48 | +- Batch doubled: cond half + uncond half (text→text_special_token bcast, |
| 49 | + style k/v→style_{key,value}_special_token). |
| 50 | +- t = current_step/total_step; time emb: sin/cos(t·1000·freqs[32]), |
| 51 | + freqs_i = 10000^(−i/31) → MLP 64→256 →Mish→ 64. |
| 52 | +- proj_in conv1x1 144→512 (no bias), *mask. |
| 53 | +- 4 blocks, flat module list per block: |
| 54 | + [convnext×4 (dil 1,2,4,8) | time: x+=Linear64→512(t_emb) | convnext×1 | |
| 55 | + attn: rotary cross-attn text (8 heads, head 64, Q=x·mask, K/V=text_emb, |
| 56 | + angle=(arange(L)/actual_len)·theta_i, theta_i=10·10000^(−i/32), |
| 57 | + rotate-half 32|32, scores/16, mask=−inf, out_fc, *mask, residual, |
| 58 | + post-LN eps 1e-6) | convnext×1 | attention: GST style cross-attn |
| 59 | + (2 heads, Q=x·mask 512→256, K=tanh(style_key), V=style_value, out 512, |
| 60 | + residual, post-LN)] |
| 61 | +- last_convnext ×4 (dil 1,1,1,1) → proj_out conv1x1 512→144 (no bias), *mask. |
| 62 | +- Update (Euler, INSIDE graph): v = 4·v_cond − 3·v_uncond; |
| 63 | + xt ← (xt + (1/total_step)·v)·latent_mask. |
| 64 | +- Noise: xt0 = randn(B,144,ceil(dur·sr/3072)) · latent_mask (chunk 512·6). |
| 65 | + |
| 66 | +### vocoder |
| 67 | +latent [B,144,L] → denorm (normalizer scale 0.25, ae.latent_mean/std |
| 68 | +[1,24,1]) + decompress 144→24 ch × 6L frames → conv k7 24→512 → 10× |
| 69 | +ConvNeXt (k7, dil 1,2,4,1,2,4,1,1,1,1, sym pad) → BatchNorm(512) → |
| 70 | +head: conv k3 512→2048 → PReLU → conv k1 2048→512 (no bias) → reshape: |
| 71 | +each frame = 512 samples → wav [B, 6L·512]. Trim to sr·dur. |
| 72 | + |
| 73 | +### Constants verified from the graphs |
| 74 | +- CFG: 4.0 / 3.0. Rotary theta[32] = 10·10000^(−i/32). Attn score /16. |
| 75 | +- Time freqs[32] = 10000^(−i/31), t scale 1000. Mish in time MLP. |
| 76 | +- ConvNeXt dwconv pads symmetric 2·dil (k5) / 3·dil (k7) — EDGE (replicate) mode in ALL four graphs, incl. vocoder embed + head layer1; zero-mode pads exist only inside VITS relative attention. |
| 77 | +- softmax mask value −inf; post-softmax re-mask with 0 (VITS style). |
| 78 | + |
| 79 | +## Design decisions |
| 80 | +- ONE GGUF: all 4 nets + unicode indexer (i32[65536]) + NFKD decomposition |
| 81 | + tables (generated from Python unicodedata at convert time) + all 10 voices |
| 82 | + (`voice.<name>.{ttl,dp}`). arch = "supertonic-tts". |
| 83 | +- CFG = two vector-field passes per step (cond/uncond share xt); 8 steps |
| 84 | + default ⇒ 16 VF passes. |
| 85 | +- Diff harness: reference = onnxruntime with intermediates promoted to |
| 86 | + graph outputs; fixed np.random noise saved in ref.gguf and injected into |
| 87 | + the C++ side for parity (production uses its own RNG; CRISPASR_SEED-able). |
| 88 | +- Acceptance: TTS→ASR roundtrip (HARD RULE #3), on Kaggle CUDA + CPU. |
| 89 | + |
| 90 | +## Worktree |
| 91 | +`.claude/worktrees/feat-434-supertonic`, branch `feat/434-supertonic`. |
| 92 | +Kaggle account: chr1str (chr1s4 taken by a parallel agent). |
0 commit comments