Supertonic 2

This commit is contained in:
ANLGBOY
2026-01-06 17:15:20 +09:00
parent ceedbbb835
commit e71c516714
44 changed files with 1105 additions and 581 deletions

View File

@@ -4,6 +4,8 @@ This guide provides examples for running TTS inference using `example_onnx.py`.
## 📰 Update News
**2026.01.06** - 🎉 **Supertonic 2** released with multilingual support! Now supports English (`en`), Korean (`ko`), Spanish (`es`), Portuguese (`pt`), and French (`fr`). [Demo](https://huggingface.co/spaces/Supertone/supertonic-2) | [Models](https://huggingface.co/Supertone/supertonic-2)
**2025.12.10** - Added `supertonic` PyPI package! Install via `pip install supertonic` for a streamlined experience. This is a separate usage method from the ONNX examples in this directory. For more details, visit [supertonic-py documentation](https://supertone-inc.github.io/supertonic-py) and see `example_pypi.py` for usage.
**2025.12.10** - Added [6 new voice styles](https://huggingface.co/Supertone/supertonic/tree/b10dbaf18b316159be75b34d24f740008fddd381) (M3, M4, M5, F3, F4, F5). See [Voices](https://supertone-inc.github.io/supertonic-py/voices/) for details
@@ -55,15 +57,16 @@ Process multiple voice styles and texts at once:
```bash
uv run example_onnx.py \
--voice-style assets/voice_styles/M1.json assets/voice_styles/F1.json \
--text "The sun sets behind the mountains, painting the sky in shades of pink and orange." "The weather is beautiful and sunny outside. A gentle breeze makes the air feel fresh and pleasant." \
--text "The sun sets behind the mountains, painting the sky in shades of pink and orange." "오늘 아침에 공원을 산책했는데, 새소리와 바람 소리가 너무 좋아서 한참을 멈춰 서서 들었어요." \
--lang en ko \
--batch
```
This will:
- Use `--batch` flag to enable batch processing mode
- Generate speech for 2 different voice-text pairs
- Use male voice style (M1.json) for the first text
- Use female voice style (F1.json) for the second text
- Use male voice style (M1.json) for the first English text
- Use female voice style (F1.json) for the second Korean text
- Process both samples in a single batch (automatic text chunking disabled)
### Example 3: High Quality Inference
@@ -128,12 +131,14 @@ This will:
| `--n-test` | int | 4 | Number of times to generate each sample |
| `--voice-style` | str+ | `assets/voice_styles/M1.json` | Voice style file path(s) |
| `--text` | str+ | (long default text) | Text(s) to synthesize |
| `--lang` | str+ | `en` | Language(s) for text(s): `en`, `ko`, `es`, `pt`, `fr` |
| `--save-dir` | str | `results` | Output directory |
| `--batch` | flag | False | Enable batch mode (disables automatic text chunking) |
## Notes
- **Batch Processing**: The number of `--voice-style` files must match the number of `--text` entries
- **Multilingual Support**: Use `--lang` to specify language(s). Available: `en` (English), `ko` (Korean), `es` (Spanish), `pt` (Portuguese), `fr` (French)
- **Long-Form Inference**: Without `--batch` flag, long texts are automatically chunked and combined into a single audio file with natural pauses
- **Quality vs Speed**: Higher `--total-step` values produce better quality but take longer
- **GPU Support**: GPU mode is not supported yet

View File

@@ -56,6 +56,13 @@ def parse_args():
],
help="Text(s) to synthesize. Can specify multiple texts for batch processing",
)
parser.add_argument(
"--lang",
type=str,
nargs="+",
default=["en"],
help="Language(s) of the text(s). Can specify multiple languages for batch processing",
)
parser.add_argument(
"--save-dir", type=str, default="results", help="Output directory"
)
@@ -73,6 +80,7 @@ n_test = args.n_test
save_dir = args.save_dir
voice_style_paths = args.voice_style
text_list = args.text
lang_list = args.lang
batch = args.batch
assert len(voice_style_paths) == len(
@@ -91,9 +99,13 @@ for n in range(n_test):
print(f"\n[{n+1}/{n_test}] Starting synthesis...")
with timer("Generating speech from text"):
if batch:
wav, duration = text_to_speech.batch(text_list, style, total_step, speed)
wav, duration = text_to_speech.batch(
text_list, lang_list, style, total_step, speed
)
else:
wav, duration = text_to_speech(text_list[0], style, total_step, speed)
wav, duration = text_to_speech(
text_list[0], lang_list[0], style, total_step, speed
)
if not os.path.exists(save_dir):
os.makedirs(save_dir)
for b in range(bsz):

View File

@@ -10,18 +10,18 @@ import onnxruntime as ort
import re
AVAILABLE_LANGS = ["en", "ko", "es", "pt", "fr"]
class UnicodeProcessor:
def __init__(self, unicode_indexer_path: str):
with open(unicode_indexer_path, "r") as f:
self.indexer = json.load(f)
def _preprocess_text(self, text: str) -> str:
def _preprocess_text(self, text: str, lang: str) -> str:
# TODO: Need advanced normalizer for better performance
text = normalize("NFKD", text)
# FIXME: this should be fixed for non-English languages
# Remove emojis (wide Unicode range)
emoji_pattern = re.compile(
"[\U0001f600-\U0001f64f" # emoticons
@@ -45,10 +45,9 @@ class UnicodeProcessor:
"": "-",
"": "-",
"": "-",
"¯": " ",
"_": " ",
"\u201C": '"', # left double quote "
"\u201D": '"', # right double quote "
"\u201c": '"', # left double quote "
"\u201d": '"', # right double quote "
"\u2018": "'", # left single quote '
"\u2019": "'", # right single quote '
"´": "'",
@@ -64,13 +63,6 @@ class UnicodeProcessor:
for k, v in replacements.items():
text = text.replace(k, v)
# Remove combining diacritics # FIXME: this should be fixed for non-English languages
text = re.sub(
r"[\u0302\u0303\u0304\u0305\u0306\u0307\u0308\u030A\u030B\u030C\u0327\u0328\u0329\u032A\u032B\u032C\u032D\u032E\u032F]",
"",
text,
)
# Remove special symbols
text = re.sub(r"[♥☆♡©\\]", "", text)
@@ -107,6 +99,9 @@ class UnicodeProcessor:
if not re.search(r"[.!?;:,'\"')\]}…。」』】〉》›»]$", text):
text += "."
if lang not in AVAILABLE_LANGS:
raise ValueError(f"Invalid language: {lang}")
text = f"<{lang}>" + text + f"</{lang}>"
return text
def _get_text_mask(self, text_ids_lengths: np.ndarray) -> np.ndarray:
@@ -119,8 +114,12 @@ class UnicodeProcessor:
) # 2 bytes
return unicode_values
def __call__(self, text_list: list[str]) -> tuple[np.ndarray, np.ndarray]:
text_list = [self._preprocess_text(t) for t in text_list]
def __call__(
self, text_list: list[str], lang_list: list[str]
) -> tuple[np.ndarray, np.ndarray]:
text_list = [
self._preprocess_text(t, lang) for t, lang in zip(text_list, lang_list)
]
text_ids_lengths = np.array([len(text) for text in text_list], dtype=np.int64)
text_ids = np.zeros((len(text_list), text_ids_lengths.max()), dtype=np.int64)
for i, text in enumerate(text_list):
@@ -176,13 +175,18 @@ class TextToSpeech:
return noisy_latent, latent_mask
def _infer(
self, text_list: list[str], style: Style, total_step: int, speed: float = 1.05
self,
text_list: list[str],
lang_list: list[str],
style: Style,
total_step: int,
speed: float = 1.05,
) -> tuple[np.ndarray, np.ndarray]:
assert (
len(text_list) == style.ttl.shape[0]
), "Number of texts must match number of style vectors"
bsz = len(text_list)
text_ids, text_mask = self.text_processor(text_list)
text_ids, text_mask = self.text_processor(text_list, lang_list)
dur_onnx, *_ = self.dp_ort.run(
None, {"text_ids": text_ids, "style_dp": style.dp, "text_mask": text_mask}
)
@@ -213,6 +217,7 @@ class TextToSpeech:
def __call__(
self,
text: str,
lang: str,
style: Style,
total_step: int,
speed: float = 1.05,
@@ -221,11 +226,12 @@ class TextToSpeech:
assert (
style.ttl.shape[0] == 1
), "Single speaker text to speech only supports single style"
text_list = chunk_text(text)
max_len = 120 if lang == "ko" else 300
text_list = chunk_text(text, max_len=max_len)
wav_cat = None
dur_cat = None
for text in text_list:
wav, dur_onnx = self._infer([text], style, total_step, speed)
wav, dur_onnx = self._infer([text], [lang], style, total_step, speed)
if wav_cat is None:
wav_cat = wav
dur_cat = dur_onnx
@@ -238,9 +244,14 @@ class TextToSpeech:
return wav_cat, dur_cat
def batch(
self, text_list: list[str], style: Style, total_step: int, speed: float = 1.05
self,
text_list: list[str],
lang_list: list[str],
style: Style,
total_step: int,
speed: float = 1.05,
) -> tuple[np.ndarray, np.ndarray]:
return self._infer(text_list, style, total_step, speed)
return self._infer(text_list, lang_list, style, total_step, speed)
def length_to_mask(lengths: np.ndarray, max_len: Optional[int] = None) -> np.ndarray:
@@ -365,11 +376,13 @@ def timer(name: str):
def sanitize_filename(text: str, max_len: int) -> str:
"""Sanitize filename by replacing non-alphanumeric characters with underscores"""
"""Sanitize filename by replacing non-alphanumeric characters with underscores (supports Unicode)"""
import re
prefix = text[:max_len]
return re.sub(r"[^a-zA-Z0-9]", "_", prefix)
# \w matches Unicode word characters (letters, digits, underscore) with re.UNICODE
# We replace non-word characters except keeping existing underscores
return re.sub(r"[^\w]", "_", prefix, flags=re.UNICODE)
def chunk_text(text: str, max_len: int = 300) -> list[str]: