Instructions to use Folx/chatterbox-ONNX-polish with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Chatterbox
How to use Folx/chatterbox-ONNX-polish with Chatterbox:
# pip install chatterbox-tts import torchaudio as ta from chatterbox.tts import ChatterboxTTS model = ChatterboxTTS.from_pretrained(device="cuda") text = "Ezreal and Jinx teamed up with Ahri, Yasuo, and Teemo to take down the enemy's Nexus in an epic late-game pentakill." wav = model.generate(text) ta.save("test-1.wav", wav, model.sr) # If you want to synthesize with a different voice, specify the audio prompt AUDIO_PROMPT_PATH="YOUR_FILE.wav" wav = model.generate(text, audio_prompt_path=AUDIO_PROMPT_PATH) ta.save("test-2.wav", wav, model.sr) - Notebooks
- Google Colab
- Kaggle
Upload folder using huggingface_hub
Browse files- Cangjie5_TC.json +0 -0
- README.md +76 -10
- example_output_polish.wav +3 -0
- onnx/embed_tokens.onnx +2 -2
- onnx/embed_tokens.onnx_data +1 -1
- onnx/language_model.onnx +2 -2
- onnx/language_model.onnx_data +2 -2
- onnx/speech_encoder.onnx +2 -2
- onnx_fixed/embed_tokens.onnx +3 -0
- onnx_fixed/embed_tokens.onnx_data +3 -0
- polish-female-common-voice.wav +3 -0
- test_polish.py +288 -0
Cangjie5_TC.json
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
README.md
CHANGED
|
@@ -72,23 +72,77 @@ Chatterbox is provided in an exported ONNX format, enabling fast and portable in
|
|
| 72 |
- Easy voice conversion script using onnxruntime
|
| 73 |
- [Outperforms ElevenLabs](https://podonos.com/resembleai/chatterbox)
|
| 74 |
|
| 75 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
# Tips
|
| 82 |
- **General Use (TTS and Voice Agents):**
|
| 83 |
-
- The default settings (`exaggeration=0.5`, `
|
|
|
|
| 84 |
|
| 85 |
- **Expressive or Dramatic Speech:**
|
| 86 |
-
- Try
|
| 87 |
-
- Higher `exaggeration` tends to speed up speech
|
| 88 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
# Usage
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
```python
|
| 94 |
# !pip install --upgrade onnxruntime==1.22.1 huggingface_hub==0.34.4 transformers==4.46.3 numpy==2.2.6 tqdm==4.67.1 librosa==0.11.0 soundfile==0.13.1 resemble-perth==1.0.1
|
|
@@ -483,8 +537,9 @@ def run_inference(
|
|
| 483 |
|
| 484 |
if __name__ == "__main__":
|
| 485 |
run_inference(
|
| 486 |
-
text="
|
| 487 |
-
language_id="
|
|
|
|
| 488 |
exaggeration=0.5,
|
| 489 |
output_file_name="output.wav",
|
| 490 |
apply_watermark=False,
|
|
@@ -493,10 +548,21 @@ if __name__ == "__main__":
|
|
| 493 |
```
|
| 494 |
|
| 495 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 496 |
# Acknowledgements
|
| 497 |
- [Xenova](https://huggingface.co/Xenova)
|
| 498 |
- [Vladislav Bronzov](https://github.com/VladOS95-cyber)
|
| 499 |
- [Resemble AI](https://github.com/resemble-ai/chatterbox)
|
|
|
|
| 500 |
|
| 501 |
# Built-in PerTh Watermarking for Responsible AI
|
| 502 |
|
|
|
|
| 72 |
- Easy voice conversion script using onnxruntime
|
| 73 |
- [Outperforms ElevenLabs](https://podonos.com/resembleai/chatterbox)
|
| 74 |
|
| 75 |
+
# What's fixed in this repo
|
| 76 |
+
|
| 77 |
+
This is a corrected ONNX export of [ResembleAI/chatterbox](https://huggingface.co/ResembleAI/chatterbox) multilingual TTS. The original community ONNX exports ([onnx-community/chatterbox-multilingual-ONNX](https://huggingface.co/onnx-community/chatterbox-multilingual-ONNX)) had multiple issues that caused **incorrect pronunciation for Polish and other diacritics-heavy languages** (sounding like Russian instead of the target language). This repo fixes all of them. Polish speech generation is now on par with the PyTorch reference model.
|
| 78 |
+
|
| 79 |
+
### Issues found and fixed
|
| 80 |
+
|
| 81 |
+
**1. Wrong model weights (root cause of "Russian accent")**
|
| 82 |
+
|
| 83 |
+
The original ONNX export used weights from an older checkpoint (`t3_23lang.safetensors`), not the latest `t3_mtl23ls_v2.safetensors` that the PyTorch `ChatterboxMultilingualTTS` uses. This caused 61 weight mismatches across the language model (including a 0.42 max diff in the final layer norm) and 3 mismatched embedding weights. We verified this numerically — every weight now exactly matches the PyTorch model.
|
| 84 |
+
|
| 85 |
+
Additionally, the `language_model.onnx` was re-exported from the correct Llama backbone using `transformers==4.46.3` to ensure the RoPE (rotary position embeddings) cos/sin caches are computed correctly. Different transformers versions produce different RoPE implementations (32-dim vs 64-dim), causing completely divergent attention outputs even with identical weights.
|
| 86 |
+
|
| 87 |
+
**2. Truncated embedding vocabulary**
|
| 88 |
+
|
| 89 |
+
`embed_tokens.onnx` had `text_emb.weight` with shape `(2352, 1024)` but the tokenizer produces IDs up to 2454. The 102 missing rows caused out-of-bounds crashes for Polish diacritics (ą, ę, ć, ś, ź, ż, ń, ó) and other non-ASCII characters. Fixed by extracting the full `(2454, 1024)` embeddings from the correct PyTorch checkpoint.
|
| 90 |
+
|
| 91 |
+
**3. Missing text preprocessing**
|
| 92 |
+
|
| 93 |
+
The ONNX inference code was missing two critical preprocessing steps that the PyTorch `MTLTokenizer.preprocess_text()` always applies:
|
| 94 |
+
- `text.lower()` — the model was trained on lowercase text
|
| 95 |
+
- `unicodedata.normalize("NFKD", text)` — decomposes diacritics into base character + combining mark (e.g., `ą` → `a` + `̨`), which is how the tokenizer was trained
|
| 96 |
+
|
| 97 |
+
Without NFKD normalization, composed Polish characters hit the wrong (or missing) token embeddings.
|
| 98 |
+
|
| 99 |
+
### Recommended settings
|
| 100 |
|
| 101 |
+
| Parameter | Value | Notes |
|
| 102 |
+
|-----------|-------|-------|
|
| 103 |
+
| `temperature` | 0.3 | Low temp prevents language drift without CFG |
|
| 104 |
+
| `repetition_penalty` | 2.0 | Matches PyTorch multilingual default |
|
| 105 |
+
| `exaggeration` | 0.5 | Default emotion intensity |
|
| 106 |
+
| `cfg_weight` | 0.0 | CFG not yet implemented for ONNX inference |
|
| 107 |
+
| `min_p` | 0.05 | Filters low-probability tokens |
|
| 108 |
+
| `max_tokens` | 500 | Enough for most sentences |
|
| 109 |
+
|
| 110 |
+
> **Note:** The PyTorch model uses Classifier-Free Guidance (CFG) with `cfg_weight=0.5` and `temperature=0.8`, which allows higher temperature without language drift. CFG requires running the language model with batch=2 (conditional + unconditional branches). This is not yet implemented in the ONNX inference script, so we compensate with lower temperature.
|
| 111 |
|
| 112 |
# Tips
|
| 113 |
- **General Use (TTS and Voice Agents):**
|
| 114 |
+
- The default settings (`exaggeration=0.5`, `temperature=0.3`) work well for most prompts.
|
| 115 |
+
- Use `temperature=0` for fully deterministic (greedy) output.
|
| 116 |
|
| 117 |
- **Expressive or Dramatic Speech:**
|
| 118 |
+
- Try increasing `exaggeration` to around `0.7` or higher.
|
| 119 |
+
- Higher `exaggeration` tends to speed up speech.
|
| 120 |
|
| 121 |
+
- **If you hear wrong accent/language:**
|
| 122 |
+
- Make sure text preprocessing includes `text.lower()` + NFKD normalization.
|
| 123 |
+
- Keep temperature low (0.0–0.3) until CFG is implemented.
|
| 124 |
+
- Verify you are using the ONNX models from this repo, not the original community exports.
|
| 125 |
|
| 126 |
# Usage
|
| 127 |
+
|
| 128 |
+
Included `test_polish.py` can be used as a standalone inference script:
|
| 129 |
+
|
| 130 |
+
```bash
|
| 131 |
+
pip install onnxruntime transformers numpy tqdm librosa soundfile
|
| 132 |
+
|
| 133 |
+
# Default settings (Polish, temp=0.3)
|
| 134 |
+
python test_polish.py
|
| 135 |
+
|
| 136 |
+
# Custom text and settings
|
| 137 |
+
python test_polish.py --text "Cześć, jak się masz?" --temperature 0.5 --exaggeration 0.7 -o output.wav
|
| 138 |
+
|
| 139 |
+
# Greedy (deterministic)
|
| 140 |
+
python test_polish.py --temperature 0 -o output_greedy.wav
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
### Full inference code
|
| 144 |
+
|
| 145 |
+
[Link to original GitHub ONNX conversion scripts](https://github.com/VladOS95-cyber/onnx_conversion_scripts/tree/main/chatterbox)
|
| 146 |
|
| 147 |
```python
|
| 148 |
# !pip install --upgrade onnxruntime==1.22.1 huggingface_hub==0.34.4 transformers==4.46.3 numpy==2.2.6 tqdm==4.67.1 librosa==0.11.0 soundfile==0.13.1 resemble-perth==1.0.1
|
|
|
|
| 537 |
|
| 538 |
if __name__ == "__main__":
|
| 539 |
run_inference(
|
| 540 |
+
text="Dzień dobry, nazywam się Weronika. Dziś jest piękna pogoda i chciałabym zaprosić wszystkich na spacer po parku.",
|
| 541 |
+
language_id="pl",
|
| 542 |
+
target_voice_path="polish-female-common-voice.wav",
|
| 543 |
exaggeration=0.5,
|
| 544 |
output_file_name="output.wav",
|
| 545 |
apply_watermark=False,
|
|
|
|
| 548 |
```
|
| 549 |
|
| 550 |
|
| 551 |
+
# Changelog
|
| 552 |
+
|
| 553 |
+
- **2026-03-31**: Full model re-export and Polish language fix
|
| 554 |
+
- Re-exported `language_model.onnx` from the correct checkpoint (`t3_mtl23ls_v2.safetensors`) using `transformers==4.46.3` for correct RoPE computation
|
| 555 |
+
- Patched all 4 embedding weights in `embed_tokens.onnx` from the correct checkpoint
|
| 556 |
+
- Added NFKD normalization + lowercase preprocessing to match PyTorch training pipeline
|
| 557 |
+
- Expanded `text_emb.weight` from `(2352, 1024)` to `(2454, 1024)` to cover full tokenizer vocabulary
|
| 558 |
+
- Added `test_polish.py` with tuned defaults and CLI interface
|
| 559 |
+
- Included Polish reference voice and example output
|
| 560 |
+
|
| 561 |
# Acknowledgements
|
| 562 |
- [Xenova](https://huggingface.co/Xenova)
|
| 563 |
- [Vladislav Bronzov](https://github.com/VladOS95-cyber)
|
| 564 |
- [Resemble AI](https://github.com/resemble-ai/chatterbox)
|
| 565 |
+
- [Folx](https://huggingface.co/Folx) — Polish language fixes and correct model re-export
|
| 566 |
|
| 567 |
# Built-in PerTh Watermarking for Responsible AI
|
| 568 |
|
example_output_polish.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:812239fdc8d163d71adf4101029c0294dbc12d458a78c732dddf2732f40935db
|
| 3 |
+
size 358124
|
onnx/embed_tokens.onnx
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cd3449f120c1738c34c1908de07ee4fccbc12f2723e88b89dba5f037b254a6f3
|
| 3 |
+
size 13200
|
onnx/embed_tokens.onnx_data
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
size 68808704
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d7c50b7240475f19e94675869460a1204f6d8a72348bf72c1814e5a004b491c9
|
| 3 |
size 68808704
|
onnx/language_model.onnx
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3a38c047d205122aa36e43c8a6e5ac027b391ce90f522cfa206f41ab39a4bdea
|
| 3 |
+
size 657688
|
onnx/language_model.onnx_data
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:87a4e23357d396d42ef959165eaecfb95108963d2c2147b46b01cca130af734d
|
| 3 |
+
size 2047078400
|
onnx/speech_encoder.onnx
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:36f42eb070f7f90423b51bea3475c784b4cc88756514f527bcfb26067b59ee01
|
| 3 |
+
size 592665681
|
onnx_fixed/embed_tokens.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:aff4b0863dec868c9ccc589d7d7617ea54c6b2377822e138bb023b83acf9f794
|
| 3 |
+
size 13288
|
onnx_fixed/embed_tokens.onnx_data
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ae9479b9f126b2ca973ab4057425a3820d36571b2133d453527bab05d6c5e359
|
| 3 |
+
size 68808704
|
polish-female-common-voice.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:49acc23c4513cb957f9591a38370db089d2121fa77c01d740ffd66ce3ae3bd94
|
| 3 |
+
size 491322
|
test_polish.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Test Polish language TTS with chatterbox-ONNX-polish.
|
| 2 |
+
Verifies that Polish diacritics (ą, ę, ć, ś, ź, ż, ł, ń, ó) are handled correctly.
|
| 3 |
+
|
| 4 |
+
All generation parameters match the original PyTorch ChatterboxMultilingualTTS.generate() defaults.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import argparse
|
| 8 |
+
import os
|
| 9 |
+
import onnxruntime
|
| 10 |
+
from transformers import AutoTokenizer
|
| 11 |
+
import numpy as np
|
| 12 |
+
from tqdm import tqdm
|
| 13 |
+
import librosa
|
| 14 |
+
import soundfile as sf
|
| 15 |
+
from unicodedata import normalize
|
| 16 |
+
|
| 17 |
+
S3GEN_SR = 24000
|
| 18 |
+
START_SPEECH_TOKEN = 6561
|
| 19 |
+
STOP_SPEECH_TOKEN = 6562
|
| 20 |
+
|
| 21 |
+
# Path to this script's directory (the model repo)
|
| 22 |
+
MODEL_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class RepetitionPenaltyLogitsProcessor:
|
| 26 |
+
def __init__(self, penalty: float):
|
| 27 |
+
self.penalty = penalty
|
| 28 |
+
|
| 29 |
+
def __call__(self, input_ids, scores):
|
| 30 |
+
score = np.take_along_axis(scores, input_ids, axis=1)
|
| 31 |
+
score = np.where(score < 0, score * self.penalty, score / self.penalty)
|
| 32 |
+
scores_processed = scores.copy()
|
| 33 |
+
np.put_along_axis(scores_processed, input_ids, score, axis=1)
|
| 34 |
+
return scores_processed
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def top_p_filter(logits, top_p):
|
| 38 |
+
"""Nucleus (top-p) filtering: zero out tokens outside the top-p cumulative probability."""
|
| 39 |
+
if top_p >= 1.0:
|
| 40 |
+
return logits
|
| 41 |
+
sorted_indices = np.argsort(-logits, axis=-1)
|
| 42 |
+
sorted_logits = np.take_along_axis(logits, sorted_indices, axis=-1)
|
| 43 |
+
probs = np.exp(sorted_logits - np.max(sorted_logits, axis=-1, keepdims=True))
|
| 44 |
+
probs = probs / probs.sum(axis=-1, keepdims=True)
|
| 45 |
+
cumulative_probs = np.cumsum(probs, axis=-1)
|
| 46 |
+
# Remove tokens with cumulative probability above top_p (keep at least 1 token)
|
| 47 |
+
sorted_mask = cumulative_probs - probs > top_p
|
| 48 |
+
sorted_logits[sorted_mask] = -np.inf
|
| 49 |
+
# Unsort
|
| 50 |
+
result = np.empty_like(logits)
|
| 51 |
+
np.put_along_axis(result, sorted_indices, sorted_logits, axis=-1)
|
| 52 |
+
return result
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def min_p_filter(logits, min_p):
|
| 56 |
+
"""Min-p filtering: remove tokens with probability below min_p * max_prob."""
|
| 57 |
+
if min_p <= 0.0:
|
| 58 |
+
return logits
|
| 59 |
+
probs = np.exp(logits - np.max(logits, axis=-1, keepdims=True))
|
| 60 |
+
probs = probs / probs.sum(axis=-1, keepdims=True)
|
| 61 |
+
max_prob = probs.max(axis=-1, keepdims=True)
|
| 62 |
+
mask = probs < (min_p * max_prob)
|
| 63 |
+
logits = logits.copy()
|
| 64 |
+
logits[mask] = -np.inf
|
| 65 |
+
return logits
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def prepare_language(txt, language_id):
|
| 69 |
+
"""Match PyTorch MTLTokenizer.preprocess_text(): lowercase + NFKD normalize."""
|
| 70 |
+
txt = txt.lower()
|
| 71 |
+
txt = normalize("NFKD", txt)
|
| 72 |
+
if language_id:
|
| 73 |
+
txt = f"[{language_id.lower()}]{txt}"
|
| 74 |
+
return txt
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def run_inference(
|
| 78 |
+
text,
|
| 79 |
+
language_id="pl",
|
| 80 |
+
target_voice_path=None,
|
| 81 |
+
# Generation parameters — tuned for ONNX multilingual quality
|
| 82 |
+
max_new_tokens=500,
|
| 83 |
+
exaggeration=0.5, # 0.0-1.0: emotion/intensity control (unique to Chatterbox)
|
| 84 |
+
cfg_weight=0.0, # CFG not yet supported in ONNX, keep at 0
|
| 85 |
+
temperature=0.3, # low temp avoids language drift without CFG
|
| 86 |
+
top_p=1.0, # nucleus sampling threshold (1.0 = disabled)
|
| 87 |
+
min_p=0.05, # min probability filter (relative to top token)
|
| 88 |
+
repetition_penalty=2.0, # higher penalty prevents repetition loops
|
| 89 |
+
seed=None, # random seed for reproducibility
|
| 90 |
+
model_dir=MODEL_DIR,
|
| 91 |
+
output_file_name="example_output_polish.wav",
|
| 92 |
+
):
|
| 93 |
+
if seed is not None:
|
| 94 |
+
np.random.seed(seed)
|
| 95 |
+
|
| 96 |
+
onnx_dir = os.path.join(model_dir, "onnx")
|
| 97 |
+
|
| 98 |
+
if not target_voice_path:
|
| 99 |
+
target_voice_path = os.path.join(model_dir, "default_voice.wav")
|
| 100 |
+
|
| 101 |
+
print(f"Text: {text}")
|
| 102 |
+
print(f"Settings: exaggeration={exaggeration}, cfg_weight={cfg_weight}, "
|
| 103 |
+
f"temperature={temperature}, top_p={top_p}, min_p={min_p}, "
|
| 104 |
+
f"repetition_penalty={repetition_penalty}, seed={seed}")
|
| 105 |
+
print(f"Voice: {os.path.basename(target_voice_path)}")
|
| 106 |
+
|
| 107 |
+
speech_encoder_session = onnxruntime.InferenceSession(os.path.join(onnx_dir, "speech_encoder.onnx"))
|
| 108 |
+
embed_tokens_session = onnxruntime.InferenceSession(os.path.join(onnx_dir, "embed_tokens.onnx"))
|
| 109 |
+
llama_with_past_session = onnxruntime.InferenceSession(os.path.join(onnx_dir, "language_model.onnx"))
|
| 110 |
+
cond_decoder_session = onnxruntime.InferenceSession(os.path.join(onnx_dir, "conditional_decoder.onnx"))
|
| 111 |
+
|
| 112 |
+
audio_values, _ = librosa.load(target_voice_path, sr=S3GEN_SR)
|
| 113 |
+
audio_values = audio_values[np.newaxis, :].astype(np.float32)
|
| 114 |
+
|
| 115 |
+
tokenizer = AutoTokenizer.from_pretrained(model_dir)
|
| 116 |
+
text_prepared = prepare_language(text, language_id)
|
| 117 |
+
input_ids = tokenizer(text_prepared, return_tensors="np")["input_ids"].astype(np.int64)
|
| 118 |
+
|
| 119 |
+
position_ids = np.where(
|
| 120 |
+
input_ids >= START_SPEECH_TOKEN, 0,
|
| 121 |
+
np.arange(input_ids.shape[1])[np.newaxis, :] - 1
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
ort_embed_tokens_inputs = {
|
| 125 |
+
"input_ids": input_ids,
|
| 126 |
+
"position_ids": position_ids.astype(np.int64),
|
| 127 |
+
"exaggeration": np.array([exaggeration], dtype=np.float32)
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
repetition_penalty_processor = RepetitionPenaltyLogitsProcessor(penalty=repetition_penalty)
|
| 131 |
+
num_hidden_layers = 30
|
| 132 |
+
num_key_value_heads = 16
|
| 133 |
+
head_dim = 64
|
| 134 |
+
|
| 135 |
+
use_cfg = cfg_weight > 0.0
|
| 136 |
+
# Batch size: 2 for CFG (conditional + unconditional), 1 without
|
| 137 |
+
B = 2 if use_cfg else 1
|
| 138 |
+
|
| 139 |
+
# Load text position embeddings for CFG uncond branch
|
| 140 |
+
# PyTorch zeros text content but KEEPS position embeddings in the uncond branch
|
| 141 |
+
text_pos_emb_weight = None
|
| 142 |
+
if use_cfg:
|
| 143 |
+
import onnx
|
| 144 |
+
from onnx import numpy_helper
|
| 145 |
+
onnx_model = onnx.load(os.path.join(onnx_dir, "embed_tokens.onnx"), load_external_data=True)
|
| 146 |
+
for init in onnx_model.graph.initializer:
|
| 147 |
+
if init.name == "text_pos_emb.weight":
|
| 148 |
+
text_pos_emb_weight = numpy_helper.to_array(init) # (2050, 1024)
|
| 149 |
+
break
|
| 150 |
+
del onnx_model
|
| 151 |
+
|
| 152 |
+
generate_tokens = np.array([[START_SPEECH_TOKEN]])
|
| 153 |
+
|
| 154 |
+
# --- Initial step: build full context embeddings ---
|
| 155 |
+
inputs_embeds = embed_tokens_session.run(None, ort_embed_tokens_inputs)[0]
|
| 156 |
+
cond_emb, prompt_token, ref_x_vector, prompt_feat = speech_encoder_session.run(
|
| 157 |
+
None, {"audio_values": audio_values}
|
| 158 |
+
)
|
| 159 |
+
cond_inputs = np.concatenate((cond_emb, inputs_embeds), axis=1)
|
| 160 |
+
|
| 161 |
+
if use_cfg:
|
| 162 |
+
# CFG unconditional: replace text embeddings with ONLY position embeddings
|
| 163 |
+
# (matching PyTorch: text_emb[1].zero_() then += text_pos_emb)
|
| 164 |
+
uncond_embeds = inputs_embeds.copy()
|
| 165 |
+
text_mask = input_ids[0] < START_SPEECH_TOKEN # which tokens are text
|
| 166 |
+
for j in range(len(text_mask)):
|
| 167 |
+
if text_mask[j]:
|
| 168 |
+
pos = int(position_ids[0, j])
|
| 169 |
+
uncond_embeds[0, j, :] = text_pos_emb_weight[pos] # position only, no content
|
| 170 |
+
uncond_inputs = np.concatenate((cond_emb, uncond_embeds), axis=1)
|
| 171 |
+
current_embeds = np.concatenate([cond_inputs, uncond_inputs], axis=0) # (2, seq, dim)
|
| 172 |
+
else:
|
| 173 |
+
current_embeds = cond_inputs # (1, seq, dim)
|
| 174 |
+
|
| 175 |
+
_, seq_len, _ = current_embeds.shape
|
| 176 |
+
past_key_values = {
|
| 177 |
+
f"past_key_values.{layer}.{kv}": np.zeros([B, num_key_value_heads, 0, head_dim], dtype=np.float32)
|
| 178 |
+
for layer in range(num_hidden_layers)
|
| 179 |
+
for kv in ("key", "value")
|
| 180 |
+
}
|
| 181 |
+
attention_mask = np.ones((B, seq_len), dtype=np.int64)
|
| 182 |
+
|
| 183 |
+
# --- Generation loop ---
|
| 184 |
+
for i in tqdm(range(max_new_tokens), desc="Generating", dynamic_ncols=True):
|
| 185 |
+
logits, *present_key_values = llama_with_past_session.run(None, dict(
|
| 186 |
+
inputs_embeds=current_embeds,
|
| 187 |
+
attention_mask=attention_mask,
|
| 188 |
+
**past_key_values,
|
| 189 |
+
))
|
| 190 |
+
|
| 191 |
+
logits = logits[:, -1, :] # (B, vocab)
|
| 192 |
+
|
| 193 |
+
if use_cfg:
|
| 194 |
+
# CFG: logits = cond + cfg_weight * (cond - uncond)
|
| 195 |
+
cond_logits = logits[0:1, :]
|
| 196 |
+
uncond_logits = logits[1:2, :]
|
| 197 |
+
logits = cond_logits + cfg_weight * (cond_logits - uncond_logits)
|
| 198 |
+
|
| 199 |
+
# Apply repetition penalty
|
| 200 |
+
logits = repetition_penalty_processor(generate_tokens, logits)
|
| 201 |
+
|
| 202 |
+
# Sampling or greedy
|
| 203 |
+
if temperature > 0:
|
| 204 |
+
logits = logits / temperature
|
| 205 |
+
logits = min_p_filter(logits, min_p)
|
| 206 |
+
logits = top_p_filter(logits, top_p)
|
| 207 |
+
probs = np.exp(logits - np.max(logits, axis=-1, keepdims=True))
|
| 208 |
+
probs = probs / probs.sum(axis=-1, keepdims=True)
|
| 209 |
+
probs = np.nan_to_num(probs, nan=0.0)
|
| 210 |
+
if probs.sum() == 0:
|
| 211 |
+
probs[0, np.argmax(logits)] = 1.0
|
| 212 |
+
next_token = np.array([[np.random.choice(probs.shape[-1], p=probs[0])]], dtype=np.int64)
|
| 213 |
+
else:
|
| 214 |
+
next_token = np.argmax(logits, axis=-1, keepdims=True).astype(np.int64)
|
| 215 |
+
|
| 216 |
+
generate_tokens = np.concatenate((generate_tokens, next_token), axis=-1)
|
| 217 |
+
if (next_token.flatten() == STOP_SPEECH_TOKEN).all():
|
| 218 |
+
break
|
| 219 |
+
|
| 220 |
+
# Embed next speech token for next step
|
| 221 |
+
next_pos = np.full((1, 1), i + 1, dtype=np.int64)
|
| 222 |
+
next_embeds = embed_tokens_session.run(None, {
|
| 223 |
+
"input_ids": next_token,
|
| 224 |
+
"position_ids": next_pos,
|
| 225 |
+
"exaggeration": np.array([exaggeration], dtype=np.float32),
|
| 226 |
+
})[0] # (1, 1, dim)
|
| 227 |
+
|
| 228 |
+
# For CFG: duplicate token embedding for both branches
|
| 229 |
+
if use_cfg:
|
| 230 |
+
current_embeds = np.concatenate([next_embeds, next_embeds], axis=0)
|
| 231 |
+
else:
|
| 232 |
+
current_embeds = next_embeds
|
| 233 |
+
|
| 234 |
+
attention_mask = np.concatenate([attention_mask, np.ones((B, 1), dtype=np.int64)], axis=1)
|
| 235 |
+
# Map present.X.key/value outputs -> past_key_values.X.key/value inputs
|
| 236 |
+
for layer in range(num_hidden_layers):
|
| 237 |
+
for ki, kv in enumerate(("key", "value")):
|
| 238 |
+
out_idx = layer * 2 + ki
|
| 239 |
+
past_key_values[f"past_key_values.{layer}.{kv}"] = present_key_values[out_idx]
|
| 240 |
+
|
| 241 |
+
speech_tokens = generate_tokens[:, 1:-1]
|
| 242 |
+
speech_tokens = np.concatenate([prompt_token, speech_tokens], axis=1)
|
| 243 |
+
|
| 244 |
+
wav = cond_decoder_session.run(None, {
|
| 245 |
+
"speech_tokens": speech_tokens,
|
| 246 |
+
"speaker_embeddings": ref_x_vector,
|
| 247 |
+
"speaker_features": prompt_feat,
|
| 248 |
+
})[0]
|
| 249 |
+
wav = np.squeeze(wav, axis=0)
|
| 250 |
+
sf.write(output_file_name, wav, S3GEN_SR)
|
| 251 |
+
print(f"\n{output_file_name} saved!")
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
if __name__ == "__main__":
|
| 255 |
+
parser = argparse.ArgumentParser(description="Chatterbox Polish TTS test")
|
| 256 |
+
parser.add_argument("--text", type=str,
|
| 257 |
+
default="Dzień dobry, nazywam się Weronika. Dziś jest piękna pogoda i chciałabym zaprosić wszystkich na spacer po parku.")
|
| 258 |
+
parser.add_argument("--lang", type=str, default="pl")
|
| 259 |
+
parser.add_argument("--voice", type=str, default=os.path.join(MODEL_DIR, "polish-female-common-voice.wav"))
|
| 260 |
+
parser.add_argument("--output", "-o", type=str, default=os.path.join(MODEL_DIR, "example_output_polish.wav"))
|
| 261 |
+
parser.add_argument("--exaggeration", type=float, default=0.5, help="Emotion intensity 0.0-1.0 (default: 0.5)")
|
| 262 |
+
parser.add_argument("--cfg-weight", type=float, default=0.0, help="Classifier-free guidance weight (default: 0.0, not yet supported)")
|
| 263 |
+
parser.add_argument("--temperature", type=float, default=0.3, help="Sampling temperature, 0=greedy (default: 0.3)")
|
| 264 |
+
parser.add_argument("--top-p", type=float, default=1.0, help="Nucleus sampling threshold (default: 1.0)")
|
| 265 |
+
parser.add_argument("--min-p", type=float, default=0.05, help="Min probability filter (default: 0.05)")
|
| 266 |
+
parser.add_argument("--repetition-penalty", type=float, default=2.0, help="Repetition penalty (default: 2.0)")
|
| 267 |
+
parser.add_argument("--max-tokens", type=int, default=500, help="Max speech tokens to generate (default: 500)")
|
| 268 |
+
parser.add_argument("--seed", type=int, default=None, help="Random seed for reproducibility")
|
| 269 |
+
args = parser.parse_args()
|
| 270 |
+
|
| 271 |
+
print("=" * 60)
|
| 272 |
+
print("POLISH TTS TEST")
|
| 273 |
+
print("=" * 60)
|
| 274 |
+
|
| 275 |
+
run_inference(
|
| 276 |
+
text=args.text,
|
| 277 |
+
language_id=args.lang,
|
| 278 |
+
target_voice_path=args.voice,
|
| 279 |
+
output_file_name=args.output,
|
| 280 |
+
exaggeration=args.exaggeration,
|
| 281 |
+
cfg_weight=args.cfg_weight,
|
| 282 |
+
temperature=args.temperature,
|
| 283 |
+
top_p=args.top_p,
|
| 284 |
+
min_p=args.min_p,
|
| 285 |
+
repetition_penalty=args.repetition_penalty,
|
| 286 |
+
max_new_tokens=args.max_tokens,
|
| 287 |
+
seed=args.seed,
|
| 288 |
+
)
|