Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5379cc4e42 | ||
|
|
26c1f17ee1 | ||
|
|
912c54b435 | ||
|
|
f8bd509733 | ||
|
|
12cd844e2c | ||
|
|
0a98c9f127 | ||
|
|
6fc89ea89e | ||
|
|
b7f677a765 | ||
|
|
cf29a310f2 | ||
|
|
e71c516714 | ||
|
|
ceedbbb835 | ||
|
|
2ddcc63824 | ||
|
|
3e495f8dc2 | ||
|
|
1642ed5161 | ||
|
|
c33fb70c20 | ||
|
|
0bdc2cec57 | ||
|
|
61aa34a6f4 | ||
|
|
214cf200cd | ||
|
|
9ba44107ee | ||
|
|
7941a24074 | ||
|
|
833dc1858b | ||
|
|
0ea52c526e | ||
|
|
0fd9b11372 |
11
.gitignore
vendored
@@ -1,13 +1,11 @@
|
||||
assets/*
|
||||
assets/.git
|
||||
assets/.gitignore
|
||||
assets/.gitattributes
|
||||
assets/
|
||||
|
||||
*.onnx
|
||||
onnx
|
||||
|
||||
# Output files
|
||||
results
|
||||
results_v3_sdk/
|
||||
|
||||
# Python
|
||||
__pycache__
|
||||
@@ -21,6 +19,11 @@ __pycache__
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
.dotnet/
|
||||
.m2/
|
||||
.uv-cache/
|
||||
.clang-module-cache/
|
||||
.swift-home/
|
||||
|
||||
# Node.js
|
||||
node_modules/
|
||||
|
||||
402
README.md
@@ -1,61 +1,87 @@
|
||||
# Supertonic — Lightning Fast, On-Device TTS
|
||||
|
||||
[](https://huggingface.co/spaces/Supertone/supertonic#interactive-demo)
|
||||
[](https://huggingface.co/Supertone/supertonic)
|
||||
# Supertonic — Lightning Fast, On-Device, Accurate TTS
|
||||
|
||||
<p align="center">
|
||||
<img src="img/Supertonic_IMG_v02_4x.webp" alt="Supertonic Banner">
|
||||
<img src="img/Supertonic3_HeroImage.png" alt="Supertonic 3 Banner">
|
||||
</p>
|
||||
|
||||
**Supertonic** is a lightning-fast, on-device text-to-speech system designed for **extreme performance** with minimal computational overhead. Powered by ONNX Runtime, it runs entirely on your device—no cloud, no API calls, no privacy concerns.
|
||||
[](https://github.com/supertone-inc/supertonic)
|
||||
[](https://huggingface.co/Supertone/supertonic-3)
|
||||
[](https://huggingface.co/spaces/Supertone/supertonic-3)
|
||||
[](https://supertonic3.github.io/)
|
||||
[](https://supertonic.supertone.ai/voice_builder)
|
||||
[](https://github.com/supertone-inc/supertonic-py)
|
||||
[](https://supertone-inc.github.io/supertonic-py/)
|
||||
|
||||
Watch Supertonic running on a **Raspberry Pi**—demonstrating on-device, real-time text-to-speech synthesis:
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/15657" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15657" alt="supertone-inc%2Fsupertonic | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
</p>
|
||||
|
||||
https://github.com/user-attachments/assets/ea66f6d6-7bc5-4308-8a88-1ce3e07400d2
|
||||
**Supertonic** is a lightning-fast, on-device multilingual text-to-speech system designed for local inference with minimal overhead. Powered by ONNX Runtime, it runs entirely on your device—no cloud, no API calls, no privacy concerns.
|
||||
|
||||
> 🎧 **Try it now**: Experience Supertonic in your browser with our [**Interactive Demo**](https://huggingface.co/spaces/Supertone/supertonic#interactive-demo), or get started with pre-trained models from [**Hugging Face Hub**](https://huggingface.co/Supertone/supertonic)
|
||||
### ✨ Highlights
|
||||
|
||||
## 📰 Update News
|
||||
- ⚡ **Blazingly Fast** — Low-latency, real-time synthesis across desktop, browser, mobile, and edge — fast enough to turn an entire webpage into audio in under a second
|
||||
- 🌍 **31-Language Multilingual** — Synthesize directly from text across 31 languages, or pass `lang="na"` to let Supertonic process the text language-agnostically when you don't know the input language — no separate language adapters needed
|
||||
- 🪶 **99M-Parameter Open-Weight Model** — A compact, fully open-weight checkpoint — a fraction of the size of 0.7B–2B class open TTS systems — for smaller downloads, faster cold starts, and lower memory footprint
|
||||
- 📱 **Edge-Device Ready** — Runs locally on desktop, mobile, browsers, and resource-constrained hardware like Raspberry Pi or e-readers, with zero network dependency, complete privacy, and no GPU required
|
||||
- 🔊 **44.1kHz High-Quality Audio** — Outputs studio-grade 44.1kHz 16-bit WAV directly, ready for production playback without any external upsampler
|
||||
- 🎭 **Expression Tags** — 10 inline tags (e.g. `<laugh>`, `<breath>`, `<sigh>`) bring natural human nuance into generated speech without prompt engineering or reference audio
|
||||
- 🛠️ **Multi-Runtime SDKs** — Ready-to-use examples through ONNX Runtime across Python, Node.js, Browser (WebGPU), Java, C++, C#, Go, Swift, iOS, Rust, and Flutter
|
||||
|
||||
**2025.11.24** - Added Flutter SDK support with macOS compatibility
|
||||
### 🌍 Supported Languages (31)
|
||||
|
||||
### Table of Contents
|
||||
Arabic (`ar`), Bulgarian (`bg`), Croatian (`hr`), Czech (`cs`), Danish (`da`), Dutch (`nl`), English (`en`), Estonian (`et`), Finnish (`fi`), French (`fr`), German (`de`), Greek (`el`), Hindi (`hi`), Hungarian (`hu`), Indonesian (`id`), Italian (`it`), Japanese (`ja`), Korean (`ko`), Latvian (`lv`), Lithuanian (`lt`), Polish (`pl`), Portuguese (`pt`), Romanian (`ro`), Russian (`ru`), Slovak (`sk`), Slovenian (`sl`), Spanish (`es`), Swedish (`sv`), Turkish (`tr`), Ukrainian (`uk`), Vietnamese (`vi`)
|
||||
|
||||
- [Why Supertonic?](#why-supertonic)
|
||||
- [Language Support](#language-support)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Performance](#performance)
|
||||
- [Citation](#citation)
|
||||
- [License](#license)
|
||||
> **Not sure which language your text is in?** Pass `lang="na"` and Supertonic will handle the input in a language-agnostic way — no explicit language tag required.
|
||||
|
||||
## Why Supertonic?
|
||||
### 📰 Update News
|
||||
|
||||
- **⚡ Blazingly Fast**: Generates speech up to **167× faster than real-time** on consumer hardware (M4 Pro)—unmatched by any other TTS system
|
||||
- **🪶 Ultra Lightweight**: Only **66M parameters**, optimized for efficient on-device performance with minimal footprint
|
||||
- **📱 On-Device Capable**: **Complete privacy** and **zero latency**—all processing happens locally on your device
|
||||
- **🎨 Natural Text Handling**: Seamlessly processes numbers, dates, currency, abbreviations, and complex expressions without pre-processing
|
||||
- **⚙️ Highly Configurable**: Adjust inference steps, batch processing, and other parameters to match your specific needs
|
||||
- **🧩 Flexible Deployment**: Deploy seamlessly across servers, browsers, and edge devices with multiple runtime backends.
|
||||
- **2026.04.29** - 🎉 **Supertonic 3** released with **31-language support**, improved reading accuracy, fewer repeat/skip failures, and v2-compatible public ONNX assets. [Demo](https://huggingface.co/spaces/Supertone/supertonic-3) | [Models](https://huggingface.co/Supertone/supertonic-3)
|
||||
- **2026.01.22** - **[Voice Builder](https://supertonic.supertone.ai/voice_builder)** is now live! Turn your voice into a deployable, edge-native TTS with permanent ownership.
|
||||
- **2026.01.06** - 🎉 **Supertonic 2** released with 5-language support. The v2 code path is preserved on the [`release/supertonic-2`](https://github.com/supertone-inc/supertonic/tree/release/supertonic-2) branch.
|
||||
- **2025.12.10** - Added `supertonic` PyPI package! Install via `pip install supertonic`. For details, visit [supertonic-py documentation](https://supertone-inc.github.io/supertonic-py)
|
||||
- **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
|
||||
- **2025.12.08** - Optimized ONNX models via [OnnxSlim](https://github.com/inisis/OnnxSlim) now available on [Hugging Face Models](https://huggingface.co/Supertone/supertonic)
|
||||
- **2025.11.24** - Added Flutter SDK support with macOS compatibility
|
||||
|
||||
## Language Support
|
||||
---
|
||||
|
||||
We provide ready-to-use TTS inference examples across multiple ecosystems:
|
||||
## Quick Start
|
||||
|
||||
| Language/Platform | Path | Description |
|
||||
|-------------------|------|-------------|
|
||||
| [**Python**](py/) | `py/` | ONNX Runtime inference |
|
||||
| [**Node.js**](nodejs/) | `nodejs/` | Server-side JavaScript |
|
||||
| [**Browser**](web/) | `web/` | WebGPU/WASM inference |
|
||||
| [**Java**](java/) | `java/` | Cross-platform JVM |
|
||||
| [**C++**](cpp/) | `cpp/` | High-performance C++ |
|
||||
| [**C#**](csharp/) | `csharp/` | .NET ecosystem |
|
||||
| [**Go**](go/) | `go/` | Go implementation |
|
||||
| [**Swift**](swift/) | `swift/` | macOS applications |
|
||||
| [**iOS**](ios/) | `ios/` | Native iOS apps |
|
||||
| [**Rust**](rust/) | `rust/` | Memory-safe systems |
|
||||
| [**Flutter**](flutter/) | `flutter/` | Cross-platform apps |
|
||||
Install the Python SDK and generate speech immediately. On the first run, Supertonic downloads the model assets from Hugging Face automatically.
|
||||
|
||||
> For detailed usage instructions, please refer to the README.md in each language directory.
|
||||
```bash
|
||||
pip install supertonic
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
from supertonic import TTS
|
||||
|
||||
# First run downloads the model from Hugging Face automatically.
|
||||
tts = TTS(auto_download=True)
|
||||
|
||||
style = tts.get_voice_style(voice_name="M1")
|
||||
|
||||
text = "Supertonic is a lightning fast, on-device TTS system."
|
||||
|
||||
wav, duration = tts.synthesize(
|
||||
text=text,
|
||||
lang="en", # Language code (e.g., "en", "ko", "na" for language-agnostic)
|
||||
voice_style=style, # Voice style object
|
||||
total_steps=8, # Quality: 5 (low) to 12 (high), default 8 (medium)
|
||||
speed=1.05, # Speed: 0.7 (slow) to 2.0 (fast)
|
||||
)
|
||||
# wav: numpy array of shape (1, num_samples,) with dtype=np.float32, sampled at 44100 Hz
|
||||
# duration: numpy array of shape (1,) containing the duration of the generated audio in seconds
|
||||
|
||||
tts.save_audio(wav, "output.wav")
|
||||
# import soundfile as sf
|
||||
# sf.write("output.wav", wav.squeeze(), 44100)
|
||||
|
||||
print(f"Generated {duration[0]:.2f}s of audio")
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -75,18 +101,30 @@ Before running the examples, download the ONNX models and preset voices, and pla
|
||||
> - Generic: see `https://git-lfs.com` for installers
|
||||
|
||||
```bash
|
||||
git clone https://huggingface.co/Supertone/supertonic assets
|
||||
git lfs install
|
||||
git clone https://huggingface.co/Supertone/supertonic-3 assets
|
||||
```
|
||||
|
||||
### Quick Start
|
||||
Some language examples need native runtimes:
|
||||
- **Go**: install the ONNX Runtime C library. On macOS, `brew install onnxruntime` is enough; the Go example auto-detects Homebrew paths.
|
||||
- **Java**: use a JDK, not just a JRE. On macOS, `brew install openjdk@17` works.
|
||||
- **C#**: targets .NET 9 and allows major-version roll-forward, so .NET 9 or newer runtimes can run it.
|
||||
|
||||
Then run the Python example:
|
||||
|
||||
**Python Example** ([Details](py/))
|
||||
```bash
|
||||
cd py
|
||||
uv sync
|
||||
uv run example_onnx.py
|
||||
```
|
||||
|
||||
This generates `outputs/output.wav` using the default preset voice.
|
||||
|
||||
### Other Runtime Examples
|
||||
|
||||
<details>
|
||||
<summary><b>Run Supertonic in other languages and platforms</b></summary>
|
||||
|
||||
**Node.js Example** ([Details](nodejs/))
|
||||
```bash
|
||||
cd nodejs
|
||||
@@ -150,97 +188,144 @@ cd ios/ExampleiOSApp
|
||||
xcodegen generate
|
||||
open ExampleiOSApp.xcodeproj
|
||||
```
|
||||
- In Xcode: Targets → ExampleiOSApp → Signing: select your Team
|
||||
- Choose your iPhone as run destination → Build & Run
|
||||
|
||||
|
||||
### Technical Details
|
||||
|
||||
- **Runtime**: ONNX Runtime for cross-platform inference (CPU-optimized; GPU mode is not tested)
|
||||
- **Browser Support**: onnxruntime-web for client-side inference
|
||||
- **Batch Processing**: Supports batch inference for improved throughput
|
||||
- **Audio Output**: Outputs 16-bit WAV files
|
||||
|
||||
## Performance
|
||||
|
||||
We evaluated Supertonic's performance (with 2 inference steps) using two key metrics across input texts of varying lengths: Short (59 chars), Mid (152 chars), and Long (266 chars).
|
||||
|
||||
**Metrics:**
|
||||
- **Characters per Second**: Measures throughput by dividing the number of input characters by the time required to generate audio. Higher is better.
|
||||
- **Real-time Factor (RTF)**: Measures the time taken to synthesize audio relative to its duration. Lower is better (e.g., RTF of 0.1 means it takes 0.1 seconds to generate one second of audio).
|
||||
|
||||
### Characters per Second
|
||||
| System | Short (59 chars) | Mid (152 chars) | Long (266 chars) |
|
||||
|--------|-----------------|----------------|-----------------|
|
||||
| **Supertonic** (M4 pro - CPU) | 912 | 1048 | 1263 |
|
||||
| **Supertonic** (M4 pro - WebGPU) | 996 | 1801 | 2509 |
|
||||
| **Supertonic** (RTX4090) | 2615 | 6548 | 12164 |
|
||||
| `API` [ElevenLabs Flash v2.5](https://elevenlabs.io/docs/api-reference/text-to-speech/convert) | 144 | 209 | 287 |
|
||||
| `API` [OpenAI TTS-1](https://platform.openai.com/docs/guides/text-to-speech) | 37 | 55 | 82 |
|
||||
| `API` [Gemini 2.5 Flash TTS](https://ai.google.dev/gemini-api/docs/speech-generation) | 12 | 18 | 24 |
|
||||
| `API` [Supertone Sona speech 1](https://docs.supertoneapi.com/en/api-reference/endpoints/text-to-speech) | 38 | 64 | 92 |
|
||||
| `Open` [Kokoro](https://github.com/hexgrad/kokoro/) | 104 | 107 | 117 |
|
||||
| `Open` [NeuTTS Air](https://github.com/neuphonic/neutts-air) | 37 | 42 | 47 |
|
||||
|
||||
> **Notes:**
|
||||
> `API` = Cloud-based API services (measured from Seoul)
|
||||
> `Open` = Open-source models
|
||||
> Supertonic (M4 pro - CPU) and (M4 pro - WebGPU): Tested with ONNX
|
||||
> Supertonic (RTX4090): Tested with PyTorch model
|
||||
> Kokoro: Tested on M4 Pro CPU with ONNX
|
||||
> NeuTTS Air: Tested on M4 Pro CPU with Q8-GGUF
|
||||
|
||||
### Real-time Factor
|
||||
|
||||
| System | Short (59 chars) | Mid (152 chars) | Long (266 chars) |
|
||||
|--------|-----------------|----------------|-----------------|
|
||||
| **Supertonic** (M4 pro - CPU) | 0.015 | 0.013 | 0.012 |
|
||||
| **Supertonic** (M4 pro - WebGPU) | 0.014 | 0.007 | 0.006 |
|
||||
| **Supertonic** (RTX4090) | 0.005 | 0.002 | 0.001 |
|
||||
| `API` [ElevenLabs Flash v2.5](https://elevenlabs.io/docs/api-reference/text-to-speech/convert) | 0.133 | 0.077 | 0.057 |
|
||||
| `API` [OpenAI TTS-1](https://platform.openai.com/docs/guides/text-to-speech) | 0.471 | 0.302 | 0.201 |
|
||||
| `API` [Gemini 2.5 Flash TTS](https://ai.google.dev/gemini-api/docs/speech-generation) | 1.060 | 0.673 | 0.541 |
|
||||
| `API` [Supertone Sona speech 1](https://docs.supertoneapi.com/en/api-reference/endpoints/text-to-speech) | 0.372 | 0.206 | 0.163 |
|
||||
| `Open` [Kokoro](https://github.com/hexgrad/kokoro/) | 0.144 | 0.124 | 0.126 |
|
||||
| `Open` [NeuTTS Air](https://github.com/neuphonic/neutts-air) | 0.390 | 0.338 | 0.343 |
|
||||
|
||||
<details>
|
||||
<summary><b>Additional Performance Data (5-step inference)</b></summary>
|
||||
|
||||
<br>
|
||||
|
||||
**Characters per Second (5-step)**
|
||||
|
||||
| System | Short (59 chars) | Mid (152 chars) | Long (266 chars) |
|
||||
|--------|-----------------|----------------|-----------------|
|
||||
| **Supertonic** (M4 pro - CPU) | 596 | 691 | 850 |
|
||||
| **Supertonic** (M4 pro - WebGPU) | 570 | 1118 | 1546 |
|
||||
| **Supertonic** (RTX4090) | 1286 | 3757 | 6242 |
|
||||
|
||||
**Real-time Factor (5-step)**
|
||||
|
||||
| System | Short (59 chars) | Mid (152 chars) | Long (266 chars) |
|
||||
|--------|-----------------|----------------|-----------------|
|
||||
| **Supertonic** (M4 pro - CPU) | 0.023 | 0.019 | 0.018 |
|
||||
| **Supertonic** (M4 pro - WebGPU) | 0.024 | 0.012 | 0.010 |
|
||||
| **Supertonic** (RTX4090) | 0.011 | 0.004 | 0.002 |
|
||||
In Xcode: Targets → ExampleiOSApp → Signing: select your Team, then choose your iPhone as run destination and build.
|
||||
|
||||
</details>
|
||||
|
||||
### Natural Text Handling
|
||||
---
|
||||
|
||||
Supertonic is designed to handle complex, real-world text inputs that contain numbers, currency symbols, abbreviations, dates, and proper nouns.
|
||||
### Technical Details
|
||||
|
||||
> 🎧 **View audio samples more easily**: Check out our [**Interactive Demo**](https://huggingface.co/spaces/Supertone/supertonic#text-handling) for a better viewing experience of all audio examples
|
||||
- **Runtime**: ONNX Runtime for cross-platform inference
|
||||
- **Browser Support**: onnxruntime-web for client-side inference
|
||||
- **Batch Processing**: Supports batch inference for improved throughput
|
||||
- **Audio Output**: Outputs 44.1kHz 16-bit WAV files
|
||||
|
||||
## Performance Highlights
|
||||
|
||||
Supertonic 3 is designed for practical on-device inference: compact enough to run locally, while staying competitive with much larger open TTS systems.
|
||||
|
||||
### Reading Accuracy
|
||||
|
||||
<p align="center">
|
||||
<img src="img/metrics/s3_vs_measured_wer_range_voxcpm2.png" alt="Supertonic 3 reading accuracy compared with measured model ranges and VoxCPM2">
|
||||
</p>
|
||||
|
||||
Evaluated on the **[Minimax-MLS-test](https://huggingface.co/datasets/MiniMaxAI/TTS-MLS-Test) benchmark**, Supertonic 3 stays within a competitive WER/CER range against much larger open TTS models such as VoxCPM2, while preserving a lightweight on-device deployment path. Asterisked languages (`*`) use CER; the others use WER.
|
||||
|
||||
<details>
|
||||
<summary><b>📊 Detailed per-language results (WER / CER*)</b></summary>
|
||||
|
||||
<br>
|
||||
|
||||
| Lang | VoxCPM2 | OmniVoice | Qwen3-TTS | Supertonic 2 | **Supertonic 3** |
|
||||
|---|:---:|:---:|:---:|:---:|:---:|
|
||||
| arabic\* | 4.14 | 1.74 | — | — | **2.14** |
|
||||
| czech | 23.73 | 2.40 | — | — | **3.02** |
|
||||
| dutch | 0.84 | 0.77 | — | — | **1.47** |
|
||||
| english | 2.11 | 2.02 | 2.25 | 2.52 | **2.06** |
|
||||
| finnish | 2.29 | 3.94 | — | — | **5.40** |
|
||||
| french | 4.41 | 4.74 | 3.82 | 5.09 | **4.89** |
|
||||
| german | 0.85 | 0.96 | 0.52 | — | **0.86** |
|
||||
| greek | 3.22 | 2.96 | — | — | **3.54** |
|
||||
| hindi\* | 5.85 | 5.14 | — | — | **5.34** |
|
||||
| indonesian | 1.25 | 1.67 | — | — | **1.34** |
|
||||
| italian | 1.74 | 1.29 | 1.40 | — | **1.75** |
|
||||
| japanese\* | 3.35 | 3.81 | 3.67 | — | **4.61** |
|
||||
| korean\* | 4.70 | 3.22 | 4.07 | 3.65 | **3.26** |
|
||||
| polish | 1.30 | 0.64 | — | — | **1.63** |
|
||||
| portuguese | 1.74 | 1.40 | 1.21 | 1.52 | **2.48** |
|
||||
| romanian | 22.39 | 2.29 | — | — | **2.19** |
|
||||
| russian | 3.31 | 4.53 | 4.48 | — | **3.99** |
|
||||
| spanish | 1.34 | 0.99 | 0.75 | 1.81 | **1.13** |
|
||||
| turkish | 0.88 | 2.18 | — | — | **1.00** |
|
||||
| ukrainian | 5.85 | 0.71 | — | — | **1.23** |
|
||||
| vietnamese | 1.48 | 0.79 | — | — | **4.49** |
|
||||
|
||||
> Lower is better. `*` indicates CER (character error rate); all other rows use WER (word error rate). Dashes (`—`) indicate the model does not officially support the language or no result is available.
|
||||
|
||||
</details>
|
||||
|
||||
### Supertonic 2 to Supertonic 3
|
||||
|
||||
<p align="center">
|
||||
<img src="img/metrics/supertonic2_vs_3_comparison.png" alt="Supertonic 2 and Supertonic 3 comparison">
|
||||
</p>
|
||||
|
||||
Compared with Supertonic 2, Supertonic 3 reduces repeat and skip failures, improves speaker similarity across the shared-language set, and expands language coverage from 5 to 31 languages. It keeps the v2-compatible public ONNX interface, so existing integrations can move to v3 with the same inference contract.
|
||||
|
||||
### Runtime Footprint
|
||||
|
||||
<p align="center">
|
||||
<img src="img/metrics/runtime_cpu_gpu_latency_memory.png" alt="Supertonic CPU runtime compared with GPU baselines">
|
||||
</p>
|
||||
|
||||
Supertonic 3 runs fast on CPU, even compared with larger baselines measured on A100 GPU, and uses substantially less memory. The open-weight fixed-voice setting does not require a GPU, which makes local, browser, and edge deployment much easier.
|
||||
|
||||
### Model Size
|
||||
|
||||
<p align="center">
|
||||
<img src="img/metrics/model_size_comparison.png" alt="Model size comparison">
|
||||
</p>
|
||||
|
||||
At about 99M parameters across the public ONNX assets, Supertonic 3 is much smaller than 0.7B to 2B class open TTS systems. The smaller model size is a practical advantage for download size, startup time, and on-device inference.
|
||||
|
||||
## Demo
|
||||
|
||||
> **Try it now**: Experience Supertonic in your browser with our [**Interactive Demo**](https://huggingface.co/spaces/Supertone/supertonic-3), or get started with pre-trained models from [**Hugging Face Hub**](https://huggingface.co/Supertone/supertonic-3)
|
||||
|
||||
### Raspberry Pi
|
||||
|
||||
Watch Supertonic running on a **Raspberry Pi**, demonstrating on-device, real-time text-to-speech synthesis:
|
||||
|
||||
https://github.com/user-attachments/assets/ea66f6d6-7bc5-4308-8a88-1ce3e07400d2
|
||||
|
||||
### E-Reader
|
||||
|
||||
Experience Supertonic on an **Onyx Boox Go 6** e-reader in airplane mode, achieving an average RTF of 0.3× with zero network dependency:
|
||||
|
||||
https://github.com/user-attachments/assets/64980e58-ad91-423a-9623-78c2ffc13680
|
||||
|
||||
### Chrome Extension
|
||||
|
||||
Turns any webpage into audio in under one second, delivering lightning-fast, on-device text-to-speech with zero network dependency—free, private, and effortless:
|
||||
|
||||
https://github.com/user-attachments/assets/cc8a45fc-5c3e-4b2c-8439-a14c3d00d91c
|
||||
|
||||
## Programming Language Support
|
||||
|
||||
We provide ready-to-use TTS inference examples across multiple ecosystems:
|
||||
|
||||
| Language/Platform | Path | Description |
|
||||
|-------------------|------|-------------|
|
||||
| [**Python**](py/) | `py/` | ONNX Runtime inference |
|
||||
| [**Node.js**](nodejs/) | `nodejs/` | Server-side JavaScript |
|
||||
| [**Browser**](web/) | `web/` | WebGPU/WASM inference |
|
||||
| [**Java**](java/) | `java/` | Cross-platform JVM |
|
||||
| [**C++**](cpp/) | `cpp/` | High-performance C++ |
|
||||
| [**C#**](csharp/) | `csharp/` | .NET ecosystem |
|
||||
| [**Go**](go/) | `go/` | Go implementation |
|
||||
| [**Swift**](swift/) | `swift/` | macOS applications |
|
||||
| [**iOS**](ios/) | `ios/` | Native iOS apps |
|
||||
| [**Rust**](rust/) | `rust/` | Memory-safe systems |
|
||||
| [**Flutter**](flutter/) | `flutter/` | Cross-platform apps |
|
||||
|
||||
> For detailed usage instructions, please refer to the README.md in each language directory.
|
||||
|
||||
## Natural Text Handling
|
||||
|
||||
Supertonic is designed to handle complex, real-world text inputs that contain natural prose, punctuation, abbreviations, and proper nouns.
|
||||
|
||||
> 🎧 **View audio samples more easily**: Check out our [**Interactive Demo**](https://huggingface.co/spaces/Supertone/supertonic-3) for a better viewing experience of all audio examples
|
||||
|
||||
**Overview of Test Cases:**
|
||||
|
||||
| Category | Key Challenges | Supertonic | ElevenLabs | OpenAI | Gemini |
|
||||
|:--------:|:--------------:|:----------:|:----------:|:------:|:------:|
|
||||
| Financial Expression | Decimal currency, abbreviated magnitudes (M, K), currency symbols, currency codes | ✅ | ❌ | ❌ | ❌ |
|
||||
| Time and Date | Time notation, abbreviated weekdays/months, date formats | ✅ | ❌ | ❌ | ❌ |
|
||||
| Phone Number | Area codes, hyphens, extensions (ext.) | ✅ | ❌ | ❌ | ❌ |
|
||||
| Technical Unit | Decimal numbers with units, abbreviated technical notations | ✅ | ❌ | ❌ | ❌ |
|
||||
| Category | Key Challenges | Supertonic | ElevenLabs | OpenAI | Gemini | Microsoft |
|
||||
|:--------:|:--------------:|:----------:|:----------:|:------:|:------:|:---------:|
|
||||
| Financial Expression | Decimal currency, abbreviated magnitudes (M, K), currency symbols, currency codes | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| Phone Number | Area codes, hyphens, extensions (ext.) | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| Technical Unit | Decimal numbers with units, abbreviated technical notations | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
|
||||
<details>
|
||||
<summary><b>Example 1: Financial Expression</b></summary>
|
||||
@@ -263,36 +348,12 @@ Supertonic is designed to handle complex, real-world text inputs that contain nu
|
||||
| ElevenLabs Flash v2.5 | ❌ | [🎧 Play Audio](https://drive.google.com/file/d/1-r2scv7XQ1crIDu6QOh3eqVl445W6ap_/view?usp=sharing) |
|
||||
| OpenAI TTS-1 | ❌ | [🎧 Play Audio](https://drive.google.com/file/d/1MFDXMjfmsAVOqwPx7iveS0KUJtZvcwxB/view?usp=sharing) |
|
||||
| Gemini 2.5 Flash TTS | ❌ | [🎧 Play Audio](https://drive.google.com/file/d/1dEHpNzfMUucFTJPQK0k4RcFZvPwQTt09/view?usp=sharing) |
|
||||
| VibeVoice Realtime 0.5B | ❌ | [🎧 Play Audio](https://drive.google.com/file/d/1b69XWBQnSZZ0WZeR3avv7E8mSdoN6p6P/view?usp=sharing) |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Example 2: Time and Date</b></summary>
|
||||
|
||||
<br>
|
||||
|
||||
**Text:**
|
||||
> "The train delay was announced at **4:45 PM** on **Wed, Apr 3, 2024** due to track maintenance."
|
||||
|
||||
**Challenges:**
|
||||
- Time expression with PM notation (4:45 PM)
|
||||
- Abbreviated weekday (Wed)
|
||||
- Abbreviated month (Apr)
|
||||
- Full date format (Apr 3, 2024)
|
||||
|
||||
**Audio Samples:**
|
||||
|
||||
| System | Result | Audio Sample |
|
||||
|--------|--------|--------------|
|
||||
| **Supertonic** | ✅ | [🎧 Play Audio](https://drive.google.com/file/d/1ehkZU8eiizBenG2DgR5tzBGQBvHS0Uaj/view?usp=sharing) |
|
||||
| ElevenLabs Flash v2.5 | ❌ | [🎧 Play Audio](https://drive.google.com/file/d/1ta3r6jFyebmA-sT44l8EaEQcMLVmuOEr/view?usp=sharing) |
|
||||
| OpenAI TTS-1 | ❌ | [🎧 Play Audio](https://drive.google.com/file/d/1sskmem9AzHAQ3Hv8DRSZoqX_pye-CXuU/view?usp=sharing) |
|
||||
| Gemini 2.5 Flash TTS | ❌ | [🎧 Play Audio](https://drive.google.com/file/d/1zx9X8oMsLMXW0Zx_SURoqjju-By2yh_n/view?usp=sharing) |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Example 3: Phone Number</b></summary>
|
||||
<summary><b>Example 2: Phone Number</b></summary>
|
||||
|
||||
<br>
|
||||
|
||||
@@ -313,11 +374,12 @@ Supertonic is designed to handle complex, real-world text inputs that contain nu
|
||||
| ElevenLabs Flash v2.5 | ❌ | [🎧 Play Audio](https://drive.google.com/file/d/1HAzVXFTZfZm0VEK2laSpsMTxzufcuaxA/view?usp=sharing) |
|
||||
| OpenAI TTS-1 | ❌ | [🎧 Play Audio](https://drive.google.com/file/d/15tjfAmb3GbjP_kmvD7zSdIWkhtAaCPOg/view?usp=sharing) |
|
||||
| Gemini 2.5 Flash TTS | ❌ | [🎧 Play Audio](https://drive.google.com/file/d/1BCL8n7yligUZyso970ud7Gf5NWb1OhKD/view?usp=sharing) |
|
||||
| VibeVoice Realtime 0.5B | ❌ | [🎧 Play Audio](https://drive.google.com/file/d/1c0c0YM_Qm7XxSk2uSVYLbITgEDTqaVzL/view?usp=sharing) |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Example 4: Technical Unit</b></summary>
|
||||
<summary><b>Example 3: Technical Unit</b></summary>
|
||||
|
||||
<br>
|
||||
|
||||
@@ -338,11 +400,40 @@ Supertonic is designed to handle complex, real-world text inputs that contain nu
|
||||
| ElevenLabs Flash v2.5 | ❌ | [🎧 Play Audio](https://drive.google.com/file/d/1_SzfjWJe5YEd0t3R7DztkYhHcI_av48p/view?usp=sharing) |
|
||||
| OpenAI TTS-1 | ❌ | [🎧 Play Audio](https://drive.google.com/file/d/1P5BSilj5xFPTV2Xz6yW5jitKZohO9o-6/view?usp=sharing) |
|
||||
| Gemini 2.5 Flash TTS | ❌ | [🎧 Play Audio](https://drive.google.com/file/d/1GU82SnWC50OvC8CZNjhxvNZFKQb7I9_Y/view?usp=sharing) |
|
||||
| VibeVoice Realtime 0.5B | ❌ | [🎧 Play Audio](https://drive.google.com/file/d/1lUTrxrAQy_viEK2Hlu3KLLtTCe8jvbdV/view?usp=sharing) |
|
||||
|
||||
</details>
|
||||
|
||||
> **Note:** These samples demonstrate how each system handles text normalization and pronunciation of complex expressions **without requiring pre-processing or phonetic annotations**.
|
||||
|
||||
## Built with Supertonic
|
||||
|
||||
| Project | Description | Links |
|
||||
|---------|-------------|-------|
|
||||
| **TLDRL** | Free, on-device TTS extension for reading any webpage | [Chrome](https://chromewebstore.google.com/detail/tldrl-lightning-tts-power/mdbiaajonlkomihpcaffhkagodbcgbme) |
|
||||
| **Read Aloud** | Open-source TTS browser extension | [Chrome](https://chromewebstore.google.com/detail/read-aloud-a-text-to-spee/hdhinadidafjejdhmfkjgnolgimiaplp) · [Edge](https://microsoftedge.microsoft.com/addons/detail/read-aloud-a-text-to-spe/pnfonnnmfjnpfgagnklfaccicnnjcdkm) · [GitHub](https://github.com/ken107/read-aloud) |
|
||||
| **PageEcho** | E-Book reader app for iOS | [App Store](https://apps.apple.com/us/app/pageecho/id6755965837) |
|
||||
| **VoiceChat** | On-device voice-to-voice LLM chatbot in the browser | [Demo](https://huggingface.co/spaces/RickRossTN/ai-voice-chat) · [GitHub](https://github.com/irelate-ai/voice-chat) |
|
||||
| **OmniAvatar** | Talking avatar video generator from photo + speech | [Demo](https://huggingface.co/spaces/alexnasa/OmniAvatar) |
|
||||
| **CopiloTTS** | Kotlin Multiplatform TTS SDK via ONNX Runtime | [GitHub](https://github.com/sigmadeltasoftware/CopiloTTS) |
|
||||
| **Voice Mixer** | PyQt5 tool for mixing and modifying voice styles | [GitHub](https://github.com/Topping1/Supertonic-Voice-Mixer) |
|
||||
| **Supertonic MNN** | Lightweight library based on MNN (fp32/fp16/int8) | [GitHub](https://github.com/vra/supertonic-mnn) · [PyPI](https://pypi.org/project/supertonic-mnn/) |
|
||||
| **Transformers.js** | Hugging Face's JS library with Supertonic support | [GitHub PR](https://github.com/huggingface/transformers.js/pull/1459) · [Demo](https://huggingface.co/spaces/webml-community/Supertonic-TTS-WebGPU) |
|
||||
| **Pinokio** | 1-click localhost cloud for Mac, Windows, and Linux | [Pinokio](https://pinokio.co/) · [GitHub](https://github.com/SUP3RMASS1VE/SuperTonic-TTS) |
|
||||
|
||||
## Models & Versions
|
||||
|
||||
| | **Supertonic 3** | Supertonic 2 | Supertonic 1 |
|
||||
|---|:---:|:---:|:---:|
|
||||
| **Status** | 🟢 Latest | Stable | Legacy |
|
||||
| **Parameters** | ~99M | ~66M | ~66M |
|
||||
| **Languages** | 31 | 5 | 1 (en) |
|
||||
| **Expression Tags** | ✅ 10 tags | — | — |
|
||||
| **Code** | [main](https://github.com/supertone-inc/supertonic) | [release/supertonic-2](https://github.com/supertone-inc/supertonic/tree/release/supertonic-2) | — |
|
||||
| **Weights** | [🤗 HF](https://huggingface.co/Supertone/supertonic-3) | [🤗 HF](https://huggingface.co/Supertone/supertonic-2) | [🤗 HF](https://huggingface.co/Supertone/supertonic) |
|
||||
| **Interactive Demo** | [🤗 Space](https://huggingface.co/spaces/Supertone/supertonic-3) | [🤗 Space](https://huggingface.co/spaces/Supertone/supertonic-2) | [🤗 Space](https://huggingface.co/spaces/Supertone/supertonic#interactive-demo) |
|
||||
| **Audio Samples** | [DemoPage](https://supertonic3.github.io/) | — | [DemoPage](https://supertonictts.github.io/) |
|
||||
|
||||
## Citation
|
||||
|
||||
The following papers describe the core technologies used in Supertonic. If you use this system in your research or find these techniques useful, please consider citing the relevant papers:
|
||||
@@ -393,9 +484,8 @@ This paper describes the self-purification technique for training flow matching
|
||||
|
||||
This project's sample code is released under the MIT License. - see the [LICENSE](https://github.com/supertone-inc/supertonic?tab=MIT-1-ov-file) for details.
|
||||
|
||||
The accompanying model is released under the OpenRAIL-M License. - see the [LICENSE](https://huggingface.co/Supertone/supertonic/blob/main/LICENSE) file for details.
|
||||
The accompanying model is released under the OpenRAIL-M License. - see the [LICENSE](https://huggingface.co/Supertone/supertonic-3/blob/main/LICENSE) file for details.
|
||||
|
||||
This model was trained using PyTorch, which is licensed under the BSD 3-Clause License but is not redistributed with this project. - see the [LICENSE](https://docs.pytorch.org/FBGEMM/general/License.html) for details.
|
||||
|
||||
Copyright (c) 2025 Supertone Inc.
|
||||
|
||||
Copyright (c) 2026 Supertone Inc.
|
||||
|
||||
@@ -4,6 +4,12 @@ High-performance text-to-speech inference using ONNX Runtime.
|
||||
|
||||
## 📰 Update News
|
||||
|
||||
**2026.04.29** - 🎉 **Supertonic 3** released with 31-language support, improved reading accuracy, and v2-compatible public ONNX assets. [Demo](https://huggingface.co/spaces/Supertone/supertonic-3) | [Models](https://huggingface.co/Supertone/supertonic-3)
|
||||
|
||||
**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
|
||||
|
||||
**2025.12.08** - Optimized ONNX models via [OnnxSlim](https://github.com/inisis/OnnxSlim) now available on [Hugging Face Models](https://huggingface.co/Supertone/supertonic)
|
||||
|
||||
**2025.11.23** - Enhanced text preprocessing with comprehensive normalization, emoji removal, symbol replacement, and punctuation handling for improved synthesis quality.
|
||||
|
||||
**2025.11.19** - Added `--speed` parameter to control speech synthesis speed (default: 1.05, recommended range: 0.9-1.5).
|
||||
@@ -45,8 +51,10 @@ vcpkg integrate install
|
||||
## Building
|
||||
|
||||
```bash
|
||||
cd cpp && mkdir build && cd build
|
||||
cmake .. && cmake --build . --config Release
|
||||
cd cpp
|
||||
mkdir -p build && cd build
|
||||
cmake ..
|
||||
cmake --build . --config Release
|
||||
./example_onnx
|
||||
```
|
||||
|
||||
@@ -59,26 +67,27 @@ Run inference with default settings:
|
||||
```
|
||||
|
||||
This will use:
|
||||
- Voice style: `../assets/voice_styles/M1.json`
|
||||
- Voice style: `../../assets/voice_styles/M1.json`
|
||||
- Text: "This morning, I took a walk in the park, and the sound of the birds and the breeze was so pleasant that I stopped for a long time just to listen."
|
||||
- Output directory: `results/`
|
||||
- Total steps: 5
|
||||
- Total steps: 8
|
||||
- Number of generations: 4
|
||||
|
||||
### Example 2: Batch Inference
|
||||
Process multiple voice styles and texts at once:
|
||||
```bash
|
||||
./example_onnx \
|
||||
--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." \
|
||||
--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.|오늘 아침에 공원을 산책했는데, 새소리와 바람 소리가 너무 좋아서 한참을 멈춰 서서 들었어요." \
|
||||
--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
|
||||
@@ -86,19 +95,19 @@ Increase denoising steps for better quality:
|
||||
```bash
|
||||
./example_onnx \
|
||||
--total-step 10 \
|
||||
--voice-style ../assets/voice_styles/M1.json \
|
||||
--voice-style ../../assets/voice_styles/M1.json \
|
||||
--text "Increasing the number of denoising steps improves the output's fidelity and overall quality."
|
||||
```
|
||||
|
||||
This will:
|
||||
- Use 10 denoising steps instead of the default 5
|
||||
- Use 10 denoising steps instead of the default 8
|
||||
- Produce higher quality output at the cost of slower inference
|
||||
|
||||
### Example 4: Long-Form Inference
|
||||
For long texts, the system automatically chunks the text into manageable segments and generates a single audio file:
|
||||
```bash
|
||||
./example_onnx \
|
||||
--voice-style ../assets/voice_styles/M1.json \
|
||||
--voice-style ../../assets/voice_styles/M1.json \
|
||||
--text "Once upon a time, in a small village nestled between rolling hills, there lived a young artist named Clara. Every morning, she would wake up before dawn to capture the first light of day. The golden rays streaming through her window inspired countless paintings. Her work was known throughout the region for its vibrant colors and emotional depth. People from far and wide came to see her gallery, and many said her paintings could tell stories that words never could."
|
||||
```
|
||||
|
||||
@@ -114,16 +123,19 @@ This will:
|
||||
|
||||
| Argument | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `--onnx-dir` | str | `../assets/onnx` | Path to ONNX model directory |
|
||||
| `--total-step` | int | 5 | Number of denoising steps (higher = better quality, slower) |
|
||||
| `--onnx-dir` | str | `../../assets/onnx` | Path to ONNX model directory |
|
||||
| `--total-step` | int | 8 | Number of denoising steps (higher = better quality, slower) |
|
||||
| `--speed` | float | 1.05 | Speech speed factor (higher = faster, lower = slower) |
|
||||
| `--n-test` | int | 4 | Number of times to generate each sample |
|
||||
| `--voice-style` | str | `../assets/voice_styles/M1.json` | Voice style file path(s) (comma-separated for batch) |
|
||||
| `--voice-style` | str | `../../assets/voice_styles/M1.json` | Voice style file path(s) (comma-separated for batch) |
|
||||
| `--text` | str | (long default text) | Text(s) to synthesize (pipe-separated for batch) |
|
||||
| `--lang` | str | `en` | Language(s) for text(s); see the main README for all 31 codes (comma-separated for batch) |
|
||||
| `--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: 31 languages; see the main README for the full list
|
||||
- **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
|
||||
|
||||
@@ -8,14 +8,15 @@
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
struct Args {
|
||||
std::string onnx_dir = "../assets/onnx";
|
||||
int total_step = 5;
|
||||
std::string onnx_dir = "../../assets/onnx";
|
||||
int total_step = 8;
|
||||
float speed = 1.05f;
|
||||
int n_test = 4;
|
||||
std::vector<std::string> voice_style = {"../assets/voice_styles/M1.json"};
|
||||
std::vector<std::string> voice_style = {"../../assets/voice_styles/M1.json"};
|
||||
std::vector<std::string> text = {
|
||||
"This morning, I took a walk in the park, and the sound of the birds and the breeze was so pleasant that I stopped for a long time just to listen."
|
||||
};
|
||||
std::vector<std::string> lang = {"en"};
|
||||
std::string save_dir = "results";
|
||||
bool batch = false;
|
||||
};
|
||||
@@ -41,6 +42,7 @@ Args parseArgs(int argc, char* argv[]) {
|
||||
else if (arg == "--n-test" && i + 1 < argc) args.n_test = std::stoi(argv[++i]);
|
||||
else if (arg == "--voice-style" && i + 1 < argc) args.voice_style = splitString(argv[++i], ',');
|
||||
else if (arg == "--text" && i + 1 < argc) args.text = splitString(argv[++i], '|');
|
||||
else if (arg == "--lang" && i + 1 < argc) args.lang = splitString(argv[++i], ',');
|
||||
else if (arg == "--save-dir" && i + 1 < argc) args.save_dir = argv[++i];
|
||||
else if (arg == "--batch") args.batch = true;
|
||||
}
|
||||
@@ -58,6 +60,7 @@ int main(int argc, char* argv[]) {
|
||||
std::string save_dir = args.save_dir;
|
||||
std::vector<std::string> voice_style_paths = args.voice_style;
|
||||
std::vector<std::string> text_list = args.text;
|
||||
std::vector<std::string> lang_list = args.lang;
|
||||
bool batch = args.batch;
|
||||
|
||||
if (voice_style_paths.size() != text_list.size()) {
|
||||
@@ -87,9 +90,9 @@ int main(int argc, char* argv[]) {
|
||||
|
||||
auto result = timer("Generating speech from text", [&]() {
|
||||
if (batch) {
|
||||
return text_to_speech->batch(memory_info, text_list, style, total_step, speed);
|
||||
return text_to_speech->batch(memory_info, text_list, lang_list, style, total_step, speed);
|
||||
} else {
|
||||
return text_to_speech->call(memory_info, text_list[0], style, total_step, speed);
|
||||
return text_to_speech->call(memory_info, text_list[0], lang_list[0], style, total_step, speed);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
274
cpp/helper.cpp
@@ -6,10 +6,14 @@
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <regex>
|
||||
#include <unordered_map>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
// Available languages for multilingual TTS
|
||||
const std::vector<std::string> AVAILABLE_LANGS = {"en", "ko", "ja", "ar", "bg", "cs", "da", "de", "el", "es", "et", "fi", "fr", "hi", "hr", "hu", "id", "it", "lt", "lv", "nl", "pl", "pt", "ro", "ru", "sk", "sl", "sv", "tr", "uk", "vi", "na"};
|
||||
|
||||
// Global tensor buffers for memory management
|
||||
static std::vector<std::vector<float>> g_tensor_buffers_float;
|
||||
static std::vector<std::vector<int64_t>> g_tensor_buffers_int64;
|
||||
@@ -45,7 +49,7 @@ UnicodeProcessor::UnicodeProcessor(const std::string& unicode_indexer_json_path)
|
||||
indexer_ = loadJsonInt64(unicode_indexer_json_path);
|
||||
}
|
||||
|
||||
std::string UnicodeProcessor::preprocessText(const std::string& text) {
|
||||
std::string UnicodeProcessor::preprocessText(const std::string& text, const std::string& lang) {
|
||||
// TODO: Need advanced normalizer for better performance
|
||||
// NOTE: C++ doesn't have built-in Unicode normalization like Python's NFKD
|
||||
// For full Unicode normalization, consider using ICU library
|
||||
@@ -53,17 +57,8 @@ std::string UnicodeProcessor::preprocessText(const std::string& text) {
|
||||
|
||||
std::string result = text;
|
||||
|
||||
// FIXME: this should be fixed for non-English languages
|
||||
|
||||
// Remove emojis and various Unicode symbols
|
||||
// Using regex to remove common emoji ranges and special symbols
|
||||
// Note: This is a simplified version - full emoji support needs UTF-8 handling
|
||||
std::regex emoji_pattern(
|
||||
"[\xF0\x9F][\x80-\xBF]{2}|" // Common emoji pattern in UTF-8
|
||||
"[\xE2][\x80-\xBF]{2}|" // Various symbols
|
||||
"[\xE2][\x98-\x9E][\x80-\xBF]" // More symbols
|
||||
);
|
||||
result = std::regex_replace(result, emoji_pattern, "");
|
||||
// IMPORTANT: Do symbol replacements FIRST (before emoji removal)
|
||||
// to preserve curly quotes and other punctuation that might be matched by emoji patterns
|
||||
|
||||
// Replace various dashes and symbols
|
||||
struct Replacement {
|
||||
@@ -75,12 +70,11 @@ std::string UnicodeProcessor::preprocessText(const std::string& text) {
|
||||
{"–", "-"}, // en dash
|
||||
{"‑", "-"}, // non-breaking hyphen
|
||||
{"—", "-"}, // em dash
|
||||
{"¯", " "}, // macron
|
||||
{"_", " "}, // underscore
|
||||
{""", "\""}, // left double quote (U+201C)
|
||||
{""", "\""}, // right double quote (U+201D)
|
||||
{"'", "'"}, // left single quote (U+2018)
|
||||
{"'", "'"}, // right single quote (U+2019)
|
||||
{ u8"\u201C", "\"" }, // left double quote "
|
||||
{ u8"\u201D", "\"" }, // right double quote "
|
||||
{ u8"\u2018", "'" }, // left single quote '
|
||||
{ u8"\u2019", "'" }, // right single quote '
|
||||
{"´", "'"}, // acute accent
|
||||
{"`", "'"}, // grave accent
|
||||
{"[", " "}, // left bracket
|
||||
@@ -100,12 +94,12 @@ std::string UnicodeProcessor::preprocessText(const std::string& text) {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove combining diacritics (common combining marks in UTF-8)
|
||||
// FIXME: this should be fixed for non-English languages
|
||||
std::regex diacritics_pattern(
|
||||
"[\xCC\xCD][\x80-\xBF]" // Combining diacritical marks range
|
||||
// Remove emojis AFTER symbol replacements
|
||||
// Only target actual emoji ranges (4-byte UTF-8 sequences starting with F0 9F)
|
||||
std::regex emoji_pattern(
|
||||
"[\xF0][\x9F][\x80-\xBF][\x80-\xBF]" // 4-byte emoji (U+1F000-U+1FFFF)
|
||||
);
|
||||
result = std::regex_replace(result, diacritics_pattern, "");
|
||||
result = std::regex_replace(result, emoji_pattern, "");
|
||||
|
||||
// Remove special symbols
|
||||
const char* special_symbols[] = {"♥", "☆", "♡", "©", "\\"};
|
||||
@@ -175,9 +169,9 @@ std::string UnicodeProcessor::preprocessText(const std::string& text) {
|
||||
last_three == "」" || last_three == "』" ||
|
||||
last_three == "】" || last_three == "〉" ||
|
||||
last_three == "》" || last_three == "›" ||
|
||||
last_three == "»" || last_three == """ ||
|
||||
last_three == """ || last_three == "'" ||
|
||||
last_three == "'") {
|
||||
last_three == "»" || last_three == u8"\u201C" ||
|
||||
last_three == u8"\u201D" || last_three == u8"\u2018" ||
|
||||
last_three == u8"\u2019") {
|
||||
ends_with_punct = true;
|
||||
}
|
||||
}
|
||||
@@ -187,14 +181,168 @@ std::string UnicodeProcessor::preprocessText(const std::string& text) {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate language
|
||||
bool valid_lang = false;
|
||||
for (const auto& available_lang : AVAILABLE_LANGS) {
|
||||
if (lang == available_lang) {
|
||||
valid_lang = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!valid_lang) {
|
||||
throw std::runtime_error("Invalid language: " + lang + ". See AVAILABLE_LANGS for supported codes.");
|
||||
}
|
||||
|
||||
// Wrap text with language tags
|
||||
result = "<" + lang + ">" + result + "</" + lang + ">";
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Hangul syllable decomposition constants (Unicode Standard Annex #15)
|
||||
static const uint32_t HANGUL_SBASE = 0xAC00; // Start of Hangul syllables
|
||||
static const uint32_t HANGUL_LBASE = 0x1100; // Start of Hangul Jamo (leading consonants)
|
||||
static const uint32_t HANGUL_VBASE = 0x1161; // Start of Hangul Jamo (vowels)
|
||||
static const uint32_t HANGUL_TBASE = 0x11A7; // Start of Hangul Jamo (trailing consonants)
|
||||
static const int HANGUL_LCOUNT = 19; // Number of leading consonants
|
||||
static const int HANGUL_VCOUNT = 21; // Number of vowels
|
||||
static const int HANGUL_TCOUNT = 28; // Number of trailing consonants (including none)
|
||||
static const int HANGUL_NCOUNT = HANGUL_VCOUNT * HANGUL_TCOUNT; // 588
|
||||
static const int HANGUL_SCOUNT = HANGUL_LCOUNT * HANGUL_NCOUNT; // 11172
|
||||
|
||||
// Latin character NFKD decompositions for Spanish, Portuguese, French
|
||||
static const std::unordered_map<uint32_t, std::vector<uint16_t>> LATIN_DECOMPOSITIONS = {
|
||||
// Acute accent
|
||||
{0x00C1, {0x0041, 0x0301}}, // Á → A + ́
|
||||
{0x00C9, {0x0045, 0x0301}}, // É → E + ́
|
||||
{0x00CD, {0x0049, 0x0301}}, // Í → I + ́
|
||||
{0x00D3, {0x004F, 0x0301}}, // Ó → O + ́
|
||||
{0x00DA, {0x0055, 0x0301}}, // Ú → U + ́
|
||||
{0x00E1, {0x0061, 0x0301}}, // á → a + ́
|
||||
{0x00E9, {0x0065, 0x0301}}, // é → e + ́
|
||||
{0x00ED, {0x0069, 0x0301}}, // í → i + ́
|
||||
{0x00F3, {0x006F, 0x0301}}, // ó → o + ́
|
||||
{0x00FA, {0x0075, 0x0301}}, // ú → u + ́
|
||||
// Grave accent
|
||||
{0x00C0, {0x0041, 0x0300}}, // À → A + ̀
|
||||
{0x00C8, {0x0045, 0x0300}}, // È → E + ̀
|
||||
{0x00CC, {0x0049, 0x0300}}, // Ì → I + ̀
|
||||
{0x00D2, {0x004F, 0x0300}}, // Ò → O + ̀
|
||||
{0x00D9, {0x0055, 0x0300}}, // Ù → U + ̀
|
||||
{0x00E0, {0x0061, 0x0300}}, // à → a + ̀
|
||||
{0x00E8, {0x0065, 0x0300}}, // è → e + ̀
|
||||
{0x00EC, {0x0069, 0x0300}}, // ì → i + ̀
|
||||
{0x00F2, {0x006F, 0x0300}}, // ò → o + ̀
|
||||
{0x00F9, {0x0075, 0x0300}}, // ù → u + ̀
|
||||
// Circumflex
|
||||
{0x00C2, {0x0041, 0x0302}}, // Â → A + ̂
|
||||
{0x00CA, {0x0045, 0x0302}}, // Ê → E + ̂
|
||||
{0x00CE, {0x0049, 0x0302}}, // Î → I + ̂
|
||||
{0x00D4, {0x004F, 0x0302}}, // Ô → O + ̂
|
||||
{0x00DB, {0x0055, 0x0302}}, // Û → U + ̂
|
||||
{0x00E2, {0x0061, 0x0302}}, // â → a + ̂
|
||||
{0x00EA, {0x0065, 0x0302}}, // ê → e + ̂
|
||||
{0x00EE, {0x0069, 0x0302}}, // î → i + ̂
|
||||
{0x00F4, {0x006F, 0x0302}}, // ô → o + ̂
|
||||
{0x00FB, {0x0075, 0x0302}}, // û → u + ̂
|
||||
// Tilde
|
||||
{0x00C3, {0x0041, 0x0303}}, // Ã → A + ̃
|
||||
{0x00D1, {0x004E, 0x0303}}, // Ñ → N + ̃
|
||||
{0x00D5, {0x004F, 0x0303}}, // Õ → O + ̃
|
||||
{0x00E3, {0x0061, 0x0303}}, // ã → a + ̃
|
||||
{0x00F1, {0x006E, 0x0303}}, // ñ → n + ̃
|
||||
{0x00F5, {0x006F, 0x0303}}, // õ → o + ̃
|
||||
// Diaeresis
|
||||
{0x00C4, {0x0041, 0x0308}}, // Ä → A + ̈
|
||||
{0x00CB, {0x0045, 0x0308}}, // Ë → E + ̈
|
||||
{0x00CF, {0x0049, 0x0308}}, // Ï → I + ̈
|
||||
{0x00D6, {0x004F, 0x0308}}, // Ö → O + ̈
|
||||
{0x00DC, {0x0055, 0x0308}}, // Ü → U + ̈
|
||||
{0x00E4, {0x0061, 0x0308}}, // ä → a + ̈
|
||||
{0x00EB, {0x0065, 0x0308}}, // ë → e + ̈
|
||||
{0x00EF, {0x0069, 0x0308}}, // ï → i + ̈
|
||||
{0x00F6, {0x006F, 0x0308}}, // ö → o + ̈
|
||||
{0x00FC, {0x0075, 0x0308}}, // ü → u + ̈
|
||||
// Cedilla
|
||||
{0x00C7, {0x0043, 0x0327}}, // Ç → C + ̧
|
||||
{0x00E7, {0x0063, 0x0327}}, // ç → c + ̧
|
||||
};
|
||||
|
||||
// Decompose a character using NFKD (Hangul + Latin accented)
|
||||
static void decomposeCharacter(uint32_t codepoint, std::vector<uint16_t>& output) {
|
||||
// Check Hangul syllables first
|
||||
if (codepoint >= HANGUL_SBASE && codepoint < HANGUL_SBASE + HANGUL_SCOUNT) {
|
||||
// Decompose Hangul syllable into Jamo
|
||||
uint32_t sIndex = codepoint - HANGUL_SBASE;
|
||||
uint32_t lIndex = sIndex / HANGUL_NCOUNT;
|
||||
uint32_t vIndex = (sIndex % HANGUL_NCOUNT) / HANGUL_TCOUNT;
|
||||
uint32_t tIndex = sIndex % HANGUL_TCOUNT;
|
||||
|
||||
output.push_back(static_cast<uint16_t>(HANGUL_LBASE + lIndex));
|
||||
output.push_back(static_cast<uint16_t>(HANGUL_VBASE + vIndex));
|
||||
if (tIndex > 0) {
|
||||
output.push_back(static_cast<uint16_t>(HANGUL_TBASE + tIndex));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check Latin decompositions
|
||||
auto it = LATIN_DECOMPOSITIONS.find(codepoint);
|
||||
if (it != LATIN_DECOMPOSITIONS.end()) {
|
||||
for (uint16_t cp : it->second) {
|
||||
output.push_back(cp);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep as-is
|
||||
output.push_back(static_cast<uint16_t>(codepoint & 0xFFFF));
|
||||
}
|
||||
|
||||
std::vector<uint16_t> UnicodeProcessor::textToUnicodeValues(const std::string& text) {
|
||||
std::vector<uint16_t> unicode_values;
|
||||
for (char c : text) {
|
||||
unicode_values.push_back(static_cast<uint16_t>(static_cast<unsigned char>(c)));
|
||||
size_t i = 0;
|
||||
|
||||
while (i < text.size()) {
|
||||
uint32_t codepoint = 0;
|
||||
unsigned char c = static_cast<unsigned char>(text[i]);
|
||||
|
||||
if ((c & 0x80) == 0) {
|
||||
// 1-byte ASCII (0xxxxxxx)
|
||||
codepoint = c;
|
||||
i += 1;
|
||||
}
|
||||
else if ((c & 0xE0) == 0xC0 && i + 1 < text.size()) {
|
||||
// 2-byte UTF-8 (110xxxxx 10xxxxxx)
|
||||
codepoint = (c & 0x1F) << 6;
|
||||
codepoint |= (static_cast<unsigned char>(text[i + 1]) & 0x3F);
|
||||
i += 2;
|
||||
}
|
||||
else if ((c & 0xF0) == 0xE0 && i + 2 < text.size()) {
|
||||
// 3-byte UTF-8 (1110xxxx 10xxxxxx 10xxxxxx) - includes Korean
|
||||
codepoint = (c & 0x0F) << 12;
|
||||
codepoint |= (static_cast<unsigned char>(text[i + 1]) & 0x3F) << 6;
|
||||
codepoint |= (static_cast<unsigned char>(text[i + 2]) & 0x3F);
|
||||
i += 3;
|
||||
}
|
||||
else if ((c & 0xF8) == 0xF0 && i + 3 < text.size()) {
|
||||
// 4-byte UTF-8 (11110xxx 10xxxxxx 10xxxxxx 10xxxxxx)
|
||||
codepoint = (c & 0x07) << 18;
|
||||
codepoint |= (static_cast<unsigned char>(text[i + 1]) & 0x3F) << 12;
|
||||
codepoint |= (static_cast<unsigned char>(text[i + 2]) & 0x3F) << 6;
|
||||
codepoint |= (static_cast<unsigned char>(text[i + 3]) & 0x3F);
|
||||
i += 4;
|
||||
}
|
||||
else {
|
||||
// Invalid UTF-8, skip byte
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Decompose Hangul syllables and Latin accented characters (NFKD)
|
||||
decomposeCharacter(codepoint, unicode_values);
|
||||
}
|
||||
|
||||
return unicode_values;
|
||||
}
|
||||
|
||||
@@ -206,25 +354,31 @@ std::vector<std::vector<std::vector<float>>> UnicodeProcessor::getTextMask(
|
||||
|
||||
void UnicodeProcessor::call(
|
||||
const std::vector<std::string>& text_list,
|
||||
const std::vector<std::string>& lang_list,
|
||||
std::vector<std::vector<int64_t>>& text_ids,
|
||||
std::vector<std::vector<std::vector<float>>>& text_mask
|
||||
) {
|
||||
std::vector<std::string> processed_texts;
|
||||
for (const auto& text : text_list) {
|
||||
processed_texts.push_back(preprocessText(text));
|
||||
for (size_t i = 0; i < text_list.size(); i++) {
|
||||
processed_texts.push_back(preprocessText(text_list[i], lang_list[i]));
|
||||
}
|
||||
|
||||
// Convert texts to unicode values first to get correct character counts
|
||||
std::vector<std::vector<uint16_t>> all_unicode_vals;
|
||||
std::vector<int64_t> text_ids_lengths;
|
||||
for (const auto& text : processed_texts) {
|
||||
text_ids_lengths.push_back(static_cast<int64_t>(text.length()));
|
||||
auto unicode_vals = textToUnicodeValues(text);
|
||||
// Use number of Unicode codepoints, not bytes
|
||||
text_ids_lengths.push_back(static_cast<int64_t>(unicode_vals.size()));
|
||||
all_unicode_vals.push_back(std::move(unicode_vals));
|
||||
}
|
||||
|
||||
int64_t max_len = *std::max_element(text_ids_lengths.begin(), text_ids_lengths.end());
|
||||
|
||||
text_ids.resize(text_list.size());
|
||||
for (size_t i = 0; i < processed_texts.size(); i++) {
|
||||
for (size_t i = 0; i < all_unicode_vals.size(); i++) {
|
||||
text_ids[i].resize(max_len, 0);
|
||||
auto unicode_vals = textToUnicodeValues(processed_texts[i]);
|
||||
const auto& unicode_vals = all_unicode_vals[i];
|
||||
for (size_t j = 0; j < unicode_vals.size(); j++) {
|
||||
if (unicode_vals[j] < indexer_.size()) {
|
||||
text_ids[i][j] = indexer_[unicode_vals[j]];
|
||||
@@ -315,6 +469,7 @@ void TextToSpeech::sampleNoisyLatent(
|
||||
TextToSpeech::SynthesisResult TextToSpeech::_infer(
|
||||
Ort::MemoryInfo& memory_info,
|
||||
const std::vector<std::string>& text_list,
|
||||
const std::vector<std::string>& lang_list,
|
||||
const Style& style,
|
||||
int total_step,
|
||||
float speed
|
||||
@@ -328,7 +483,7 @@ TextToSpeech::SynthesisResult TextToSpeech::_infer(
|
||||
// Process text
|
||||
std::vector<std::vector<int64_t>> text_ids;
|
||||
std::vector<std::vector<std::vector<float>>> text_mask;
|
||||
text_processor_->call(text_list, text_ids, text_mask);
|
||||
text_processor_->call(text_list, lang_list, text_ids, text_mask);
|
||||
|
||||
std::vector<int64_t> text_ids_shape = {bsz, static_cast<int64_t>(text_ids[0].size())};
|
||||
std::vector<int64_t> text_mask_shape = {bsz, 1, static_cast<int64_t>(text_mask[0][0].size())};
|
||||
@@ -530,6 +685,7 @@ TextToSpeech::SynthesisResult TextToSpeech::_infer(
|
||||
TextToSpeech::SynthesisResult TextToSpeech::call(
|
||||
Ort::MemoryInfo& memory_info,
|
||||
const std::string& text,
|
||||
const std::string& lang,
|
||||
const Style& style,
|
||||
int total_step,
|
||||
float speed,
|
||||
@@ -539,12 +695,13 @@ TextToSpeech::SynthesisResult TextToSpeech::call(
|
||||
throw std::runtime_error("Single speaker text to speech only supports single style");
|
||||
}
|
||||
|
||||
auto text_list = chunkText(text);
|
||||
int max_len = (lang == "ko" || lang == "ja") ? 120 : 300;
|
||||
auto text_list = chunkText(text, max_len);
|
||||
std::vector<float> wav_cat;
|
||||
float dur_cat = 0.0f;
|
||||
|
||||
for (const auto& chunk : text_list) {
|
||||
auto result = _infer(memory_info, {chunk}, style, total_step, speed);
|
||||
auto result = _infer(memory_info, {chunk}, {lang}, style, total_step, speed);
|
||||
|
||||
if (wav_cat.empty()) {
|
||||
wav_cat = result.wav;
|
||||
@@ -568,11 +725,12 @@ TextToSpeech::SynthesisResult TextToSpeech::call(
|
||||
TextToSpeech::SynthesisResult TextToSpeech::batch(
|
||||
Ort::MemoryInfo& memory_info,
|
||||
const std::vector<std::string>& text_list,
|
||||
const std::vector<std::string>& lang_list,
|
||||
const Style& style,
|
||||
int total_step,
|
||||
float speed
|
||||
) {
|
||||
return _infer(memory_info, text_list, style, total_step, speed);
|
||||
return _infer(memory_info, text_list, lang_list, style, total_step, speed);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -911,15 +1069,43 @@ std::vector<int64_t> loadJsonInt64(const std::string& file_path) {
|
||||
|
||||
std::string sanitizeFilename(const std::string& text, int max_len) {
|
||||
std::string result;
|
||||
int count = 0;
|
||||
for (char c : text) {
|
||||
if (count >= max_len) break;
|
||||
if (std::isalnum(static_cast<unsigned char>(c))) {
|
||||
result += c;
|
||||
} else {
|
||||
result += '_';
|
||||
int char_count = 0;
|
||||
size_t i = 0;
|
||||
|
||||
while (i < text.size() && char_count < max_len) {
|
||||
unsigned char c = static_cast<unsigned char>(text[i]);
|
||||
|
||||
// Check if it's ASCII alphanumeric or underscore
|
||||
if (std::isalnum(c) || c == '_') {
|
||||
result += text[i];
|
||||
i++;
|
||||
char_count++;
|
||||
}
|
||||
// Check for UTF-8 multi-byte sequences (preserve Unicode letters/numbers)
|
||||
else if ((c & 0xE0) == 0xC0 && i + 1 < text.size()) {
|
||||
// 2-byte UTF-8 sequence
|
||||
result += text.substr(i, 2);
|
||||
i += 2;
|
||||
char_count++;
|
||||
}
|
||||
else if ((c & 0xF0) == 0xE0 && i + 2 < text.size()) {
|
||||
// 3-byte UTF-8 sequence (includes Korean, Japanese, Chinese)
|
||||
result += text.substr(i, 3);
|
||||
i += 3;
|
||||
char_count++;
|
||||
}
|
||||
else if ((c & 0xF8) == 0xF0 && i + 3 < text.size()) {
|
||||
// 4-byte UTF-8 sequence
|
||||
result += text.substr(i, 4);
|
||||
i += 4;
|
||||
char_count++;
|
||||
}
|
||||
else {
|
||||
// Replace other characters with underscore
|
||||
result += '_';
|
||||
i++;
|
||||
char_count++;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
#include <chrono>
|
||||
#include <onnxruntime_cxx_api.h>
|
||||
|
||||
// Available languages for multilingual TTS
|
||||
extern const std::vector<std::string> AVAILABLE_LANGS;
|
||||
|
||||
/**
|
||||
* Configuration structure
|
||||
*/
|
||||
@@ -33,6 +36,7 @@ public:
|
||||
// Process text list to text IDs and mask
|
||||
void call(
|
||||
const std::vector<std::string>& text_list,
|
||||
const std::vector<std::string>& lang_list,
|
||||
std::vector<std::vector<int64_t>>& text_ids,
|
||||
std::vector<std::vector<std::vector<float>>>& text_mask
|
||||
);
|
||||
@@ -40,7 +44,7 @@ public:
|
||||
private:
|
||||
std::vector<int64_t> indexer_;
|
||||
|
||||
std::string preprocessText(const std::string& text);
|
||||
std::string preprocessText(const std::string& text, const std::string& lang);
|
||||
std::vector<uint16_t> textToUnicodeValues(const std::string& text);
|
||||
std::vector<std::vector<std::vector<float>>> getTextMask(
|
||||
const std::vector<int64_t>& text_ids_lengths
|
||||
@@ -89,6 +93,7 @@ public:
|
||||
SynthesisResult call(
|
||||
Ort::MemoryInfo& memory_info,
|
||||
const std::string& text,
|
||||
const std::string& lang,
|
||||
const Style& style,
|
||||
int total_step,
|
||||
float speed = 1.05f,
|
||||
@@ -98,6 +103,7 @@ public:
|
||||
SynthesisResult batch(
|
||||
Ort::MemoryInfo& memory_info,
|
||||
const std::vector<std::string>& text_list,
|
||||
const std::vector<std::string>& lang_list,
|
||||
const Style& style,
|
||||
int total_step,
|
||||
float speed = 1.05f
|
||||
@@ -109,6 +115,7 @@ private:
|
||||
SynthesisResult _infer(
|
||||
Ort::MemoryInfo& memory_info,
|
||||
const std::vector<std::string>& text_list,
|
||||
const std::vector<std::string>& lang_list,
|
||||
const Style& style,
|
||||
int total_step,
|
||||
float speed = 1.05f
|
||||
|
||||
@@ -10,15 +10,16 @@ namespace Supertonic
|
||||
class Args
|
||||
{
|
||||
public bool UseGpu { get; set; } = false;
|
||||
public string OnnxDir { get; set; } = "assets/onnx";
|
||||
public int TotalStep { get; set; } = 5;
|
||||
public string OnnxDir { get; set; } = "../assets/onnx";
|
||||
public int TotalStep { get; set; } = 8;
|
||||
public float Speed { get; set; } = 1.05f;
|
||||
public int NTest { get; set; } = 4;
|
||||
public List<string> VoiceStyle { get; set; } = new List<string> { "assets/voice_styles/M1.json" };
|
||||
public List<string> VoiceStyle { get; set; } = new List<string> { "../assets/voice_styles/M1.json" };
|
||||
public List<string> Text { get; set; } = new List<string>
|
||||
{
|
||||
"This morning, I took a walk in the park, and the sound of the birds and the breeze was so pleasant that I stopped for a long time just to listen."
|
||||
};
|
||||
public List<string> Lang { get; set; } = new List<string> { "en" };
|
||||
public string SaveDir { get; set; } = "results";
|
||||
public bool Batch { get; set; } = false;
|
||||
}
|
||||
@@ -55,6 +56,9 @@ namespace Supertonic
|
||||
case "--text" when i + 1 < args.Length:
|
||||
result.Text = args[++i].Split('|').ToList();
|
||||
break;
|
||||
case "--lang" when i + 1 < args.Length:
|
||||
result.Lang = args[++i].Split(',').ToList();
|
||||
break;
|
||||
case "--save-dir" when i + 1 < args.Length:
|
||||
result.SaveDir = args[++i];
|
||||
break;
|
||||
@@ -76,6 +80,7 @@ namespace Supertonic
|
||||
string saveDir = parsedArgs.SaveDir;
|
||||
var voiceStylePaths = parsedArgs.VoiceStyle;
|
||||
var textList = parsedArgs.Text;
|
||||
var langList = parsedArgs.Lang;
|
||||
bool batch = parsedArgs.Batch;
|
||||
|
||||
if (voiceStylePaths.Count != textList.Count)
|
||||
@@ -101,11 +106,11 @@ namespace Supertonic
|
||||
{
|
||||
if (batch)
|
||||
{
|
||||
return textToSpeech.Batch(textList, style, totalStep, speed);
|
||||
return textToSpeech.Batch(textList, langList, style, totalStep, speed);
|
||||
}
|
||||
else
|
||||
{
|
||||
return textToSpeech.Call(textList[0], style, totalStep, speed);
|
||||
return textToSpeech.Call(textList[0], langList[0], style, totalStep, speed);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,12 @@ using Microsoft.ML.OnnxRuntime.Tensors;
|
||||
|
||||
namespace Supertonic
|
||||
{
|
||||
// Available languages for multilingual TTS
|
||||
public static class Languages
|
||||
{
|
||||
public static readonly string[] Available = { "en", "ko", "ja", "ar", "bg", "cs", "da", "de", "el", "es", "et", "fi", "fr", "hi", "hr", "hu", "id", "it", "lt", "lv", "nl", "pl", "pt", "ro", "ru", "sk", "sl", "sv", "tr", "uk", "vi", "na" };
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Configuration classes
|
||||
// ============================================================================
|
||||
@@ -118,13 +124,11 @@ namespace Supertonic
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
private string PreprocessText(string text)
|
||||
private string PreprocessText(string text, string lang)
|
||||
{
|
||||
// TODO: Need advanced normalizer for better performance
|
||||
text = text.Normalize(NormalizationForm.FormKD);
|
||||
|
||||
// FIXME: this should be fixed for non-English languages
|
||||
|
||||
// Remove emojis (wide Unicode range)
|
||||
// C# doesn't support \u{...} syntax in regex, so we use character filtering instead
|
||||
text = RemoveEmojis(text);
|
||||
@@ -135,7 +139,6 @@ namespace Supertonic
|
||||
{"–", "-"}, // en dash
|
||||
{"‑", "-"}, // non-breaking hyphen
|
||||
{"—", "-"}, // em dash
|
||||
{"¯", " "}, // macron
|
||||
{"_", " "}, // underscore
|
||||
{"\u201C", "\""}, // left double quote
|
||||
{"\u201D", "\""}, // right double quote
|
||||
@@ -157,9 +160,6 @@ namespace Supertonic
|
||||
text = text.Replace(kvp.Key, kvp.Value);
|
||||
}
|
||||
|
||||
// Remove combining diacritics // FIXME: this should be fixed for non-English languages
|
||||
text = Regex.Replace(text, @"[\u0302\u0303\u0304\u0305\u0306\u0307\u0308\u030A\u030B\u030C\u0327\u0328\u0329\u032A\u032B\u032C\u032D\u032E\u032F]", "");
|
||||
|
||||
// Remove special symbols
|
||||
text = Regex.Replace(text, @"[♥☆♡©\\]", "");
|
||||
|
||||
@@ -208,6 +208,15 @@ namespace Supertonic
|
||||
text += ".";
|
||||
}
|
||||
|
||||
// Validate language
|
||||
if (!Languages.Available.Contains(lang))
|
||||
{
|
||||
throw new ArgumentException($"Invalid language: {lang}. Available: {string.Join(", ", Languages.Available)}");
|
||||
}
|
||||
|
||||
// Wrap text with language tags
|
||||
text = $"<{lang}>" + text + $"</{lang}>";
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
@@ -221,9 +230,9 @@ namespace Supertonic
|
||||
return Helper.LengthToMask(textIdsLengths);
|
||||
}
|
||||
|
||||
public (long[][] textIds, float[][][] textMask) Call(List<string> textList)
|
||||
public (long[][] textIds, float[][][] textMask) Call(List<string> textList, List<string> langList)
|
||||
{
|
||||
var processedTexts = textList.Select(t => PreprocessText(t)).ToList();
|
||||
var processedTexts = textList.Select((t, i) => PreprocessText(t, langList[i])).ToList();
|
||||
var textIdsLengths = processedTexts.Select(t => (long)t.Length).ToArray();
|
||||
long maxLen = textIdsLengths.Max();
|
||||
|
||||
@@ -328,7 +337,7 @@ namespace Supertonic
|
||||
return (noisyLatent, latentMask);
|
||||
}
|
||||
|
||||
private (float[] wav, float[] duration) _Infer(List<string> textList, Style style, int totalStep, float speed = 1.05f)
|
||||
private (float[] wav, float[] duration) _Infer(List<string> textList, List<string> langList, Style style, int totalStep, float speed = 1.05f)
|
||||
{
|
||||
int bsz = textList.Count;
|
||||
if (bsz != style.TtlShape[0])
|
||||
@@ -337,7 +346,7 @@ namespace Supertonic
|
||||
}
|
||||
|
||||
// Process text
|
||||
var (textIds, textMask) = _textProcessor.Call(textList);
|
||||
var (textIds, textMask) = _textProcessor.Call(textList, langList);
|
||||
var textIdsShape = new long[] { bsz, textIds[0].Length };
|
||||
var textMaskShape = new long[] { bsz, 1, textMask[0][0].Length };
|
||||
|
||||
@@ -424,20 +433,21 @@ namespace Supertonic
|
||||
return (wavTensor.ToArray(), durOnnx);
|
||||
}
|
||||
|
||||
public (float[] wav, float[] duration) Call(string text, Style style, int totalStep, float speed = 1.05f, float silenceDuration = 0.3f)
|
||||
public (float[] wav, float[] duration) Call(string text, string lang, Style style, int totalStep, float speed = 1.05f, float silenceDuration = 0.3f)
|
||||
{
|
||||
if (style.TtlShape[0] != 1)
|
||||
{
|
||||
throw new ArgumentException("Single speaker text to speech only supports single style");
|
||||
}
|
||||
|
||||
var textList = Helper.ChunkText(text);
|
||||
int maxLen = (lang == "ko" || lang == "ja") ? 120 : 300;
|
||||
var textList = Helper.ChunkText(text, maxLen);
|
||||
var wavCat = new List<float>();
|
||||
float durCat = 0.0f;
|
||||
|
||||
foreach (var chunk in textList)
|
||||
{
|
||||
var (wav, duration) = _Infer(new List<string> { chunk }, style, totalStep, speed);
|
||||
var (wav, duration) = _Infer(new List<string> { chunk }, new List<string> { lang }, style, totalStep, speed);
|
||||
|
||||
if (wavCat.Count == 0)
|
||||
{
|
||||
@@ -457,9 +467,9 @@ namespace Supertonic
|
||||
return (wavCat.ToArray(), new float[] { durCat });
|
||||
}
|
||||
|
||||
public (float[] wav, float[] duration) Batch(List<string> textList, Style style, int totalStep, float speed = 1.05f)
|
||||
public (float[] wav, float[] duration) Batch(List<string> textList, List<string> langList, Style style, int totalStep, float speed = 1.05f)
|
||||
{
|
||||
return _Infer(textList, style, totalStep, speed);
|
||||
return _Infer(textList, langList, style, totalStep, speed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,12 @@ This guide provides examples for running TTS inference using `ExampleONNX.cs`.
|
||||
|
||||
## 📰 Update News
|
||||
|
||||
**2026.04.29** - 🎉 **Supertonic 3** released with 31-language support, improved reading accuracy, and v2-compatible public ONNX assets. [Demo](https://huggingface.co/spaces/Supertone/supertonic-3) | [Models](https://huggingface.co/Supertone/supertonic-3)
|
||||
|
||||
**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
|
||||
|
||||
**2025.12.08** - Optimized ONNX models via [OnnxSlim](https://github.com/inisis/OnnxSlim) now available on [Hugging Face Models](https://huggingface.co/Supertone/supertonic)
|
||||
|
||||
**2025.11.23** - Enhanced text preprocessing with comprehensive normalization, emoji removal, symbol replacement, and punctuation handling for improved synthesis quality.
|
||||
|
||||
**2025.11.19** - Added `--speed` parameter to control speech synthesis speed (default: 1.05, recommended range: 0.9-1.5).
|
||||
@@ -13,9 +19,11 @@ This guide provides examples for running TTS inference using `ExampleONNX.cs`.
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
- .NET 9.0 SDK or later
|
||||
- .NET 9.0 SDK/runtime, or a newer .NET runtime with major-version roll-forward
|
||||
- [Download .NET SDK](https://dotnet.microsoft.com/download)
|
||||
|
||||
The project targets `net9.0` and sets `RollForward=Major`, so systems with only a newer runtime such as .NET 10 can still run the example.
|
||||
|
||||
### Install dependencies
|
||||
```bash
|
||||
dotnet restore
|
||||
@@ -30,26 +38,27 @@ dotnet run
|
||||
```
|
||||
|
||||
This will use:
|
||||
- Voice style: `assets/voice_styles/M1.json`
|
||||
- Voice style: `../assets/voice_styles/M1.json`
|
||||
- Text: "This morning, I took a walk in the park, and the sound of the birds and the breeze was so pleasant that I stopped for a long time just to listen."
|
||||
- Output directory: `results/`
|
||||
- Total steps: 5
|
||||
- Total steps: 8
|
||||
- Number of generations: 4
|
||||
|
||||
### Example 2: Batch Inference
|
||||
Process multiple voice styles and texts at once:
|
||||
```bash
|
||||
dotnet run -- \
|
||||
--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." \
|
||||
--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.|오늘 아침에 공원을 산책했는데, 새소리와 바람 소리가 너무 좋아서 한참을 멈춰 서서 들었어요." \
|
||||
--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
|
||||
@@ -57,19 +66,19 @@ Increase denoising steps for better quality:
|
||||
```bash
|
||||
dotnet run -- \
|
||||
--total-step 10 \
|
||||
--voice-style assets/voice_styles/M1.json \
|
||||
--voice-style ../assets/voice_styles/M1.json \
|
||||
--text "Increasing the number of denoising steps improves the output's fidelity and overall quality."
|
||||
```
|
||||
|
||||
This will:
|
||||
- Use 10 denoising steps instead of the default 5
|
||||
- Use 10 denoising steps instead of the default 8
|
||||
- Produce higher quality output at the cost of slower inference
|
||||
|
||||
### Example 4: Long-Form Inference
|
||||
For long texts, the system automatically chunks the text into manageable segments and generates a single audio file:
|
||||
```bash
|
||||
dotnet run -- \
|
||||
--voice-style assets/voice_styles/M1.json \
|
||||
--voice-style ../assets/voice_styles/M1.json \
|
||||
--text "Once upon a time, in a small village nestled between rolling hills, there lived a young artist named Clara. Every morning, she would wake up before dawn to capture the first light of day. The golden rays streaming through her window inspired countless paintings. Her work was known throughout the region for its vibrant colors and emotional depth. People from far and wide came to see her gallery, and many said her paintings could tell stories that words never could."
|
||||
```
|
||||
|
||||
@@ -86,17 +95,20 @@ This will:
|
||||
| Argument | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `--use-gpu` | flag | False | Use GPU for inference (not supported yet) |
|
||||
| `--onnx-dir` | str | `assets/onnx` | Path to ONNX model directory |
|
||||
| `--total-step` | int | 5 | Number of denoising steps (higher = better quality, slower) |
|
||||
| `--onnx-dir` | str | `../assets/onnx` | Path to ONNX model directory |
|
||||
| `--total-step` | int | 8 | Number of denoising steps (higher = better quality, slower) |
|
||||
| `--speed` | float | 1.05 | Speech speed factor (higher = faster, lower = slower) |
|
||||
| `--n-test` | int | 4 | Number of times to generate each sample |
|
||||
| `--voice-style` | str+ | `assets/voice_styles/M1.json` | Voice style file path(s) (comma-separated) |
|
||||
| `--voice-style` | str+ | `../assets/voice_styles/M1.json` | Voice style file path(s) (comma-separated) |
|
||||
| `--text` | str+ | (long default text) | Text(s) to synthesize (pipe-separated: `|`) |
|
||||
| `--lang` | str+ | `en` | Language(s) for text(s); see the main README for all 31 codes (comma-separated) |
|
||||
| `--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: 31 languages; see the main README for the full list
|
||||
- **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
|
||||
@@ -124,4 +136,3 @@ csharp/
|
||||
└── results/ # Output directory (created automatically)
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>13.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<RollForward>Major</RollForward>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -14,4 +15,3 @@
|
||||
|
||||
</Project>
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
# Supertonic Flutter Example
|
||||
|
||||
This example demonstrates how to use Supertonic in a Flutter application using ONNX Runtime.
|
||||
This example demonstrates how to use Supertonic 3 in a Flutter application using ONNX Runtime.
|
||||
|
||||
> **Note:** This project uses the `flutter_onnxruntime` package ([https://pub.dev/packages/flutter_onnxruntime](https://pub.dev/packages/flutter_onnxruntime)). At the moment, only the macOS platform has been tested. Although the flutter_onnxruntime package supports several other platforms, they have not been tested in this project yet and may require additional verification.
|
||||
|
||||
|
||||
## 📰 Update News
|
||||
|
||||
**2026.04.29** - 🎉 **Supertonic 3** released with 31-language support, improved reading accuracy, and v2-compatible public ONNX assets. [Demo](https://huggingface.co/spaces/Supertone/supertonic-3) | [Models](https://huggingface.co/Supertone/supertonic-3)
|
||||
|
||||
**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
|
||||
|
||||
**2025.12.08** - Optimized ONNX models via [OnnxSlim](https://github.com/inisis/OnnxSlim) now available on [Hugging Face Models](https://huggingface.co/Supertone/supertonic)
|
||||
|
||||
**2025.11.23** - Added and tested macos support.
|
||||
|
||||
## Multilingual Support
|
||||
|
||||
Supertonic 3 supports 31 languages. Select the appropriate language from the dropdown; see the main README for the full code list.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Flutter SDK version ^3.5.0
|
||||
@@ -20,4 +30,3 @@ flutter clean
|
||||
flutter pub get
|
||||
flutter run -d macos
|
||||
```
|
||||
|
||||
|
||||
@@ -11,6 +11,205 @@ final logger = Logger(
|
||||
printer: PrettyPrinter(methodCount: 0, errorMethodCount: 5, lineLength: 80),
|
||||
);
|
||||
|
||||
// Available languages for multilingual TTS
|
||||
const List<String> availableLangs = ['en', 'ko', 'ja', 'ar', 'bg', 'cs', 'da', 'de', 'el', 'es', 'et', 'fi', 'fr', 'hi', 'hr', 'hu', 'id', 'it', 'lt', 'lv', 'nl', 'pl', 'pt', 'ro', 'ru', 'sk', 'sl', 'sv', 'tr', 'uk', 'vi', 'na'];
|
||||
|
||||
bool isValidLang(String lang) => availableLangs.contains(lang);
|
||||
|
||||
// Hangul Jamo constants for NFKD decomposition
|
||||
const int _hangulSyllableBase = 0xAC00;
|
||||
const int _hangulSyllableEnd = 0xD7A3;
|
||||
const int _leadingJamoBase = 0x1100;
|
||||
const int _vowelJamoBase = 0x1161;
|
||||
const int _trailingJamoBase = 0x11A7;
|
||||
const int _vowelCount = 21;
|
||||
const int _trailingCount = 28;
|
||||
|
||||
/// Decompose a Hangul syllable into Jamo (NFKD-like decomposition)
|
||||
List<int> _decomposeHangulSyllable(int codePoint) {
|
||||
if (codePoint < _hangulSyllableBase || codePoint > _hangulSyllableEnd) {
|
||||
return [codePoint];
|
||||
}
|
||||
|
||||
final syllableIndex = codePoint - _hangulSyllableBase;
|
||||
final leadingIndex = syllableIndex ~/ (_vowelCount * _trailingCount);
|
||||
final vowelIndex =
|
||||
(syllableIndex % (_vowelCount * _trailingCount)) ~/ _trailingCount;
|
||||
final trailingIndex = syllableIndex % _trailingCount;
|
||||
|
||||
final result = <int>[
|
||||
_leadingJamoBase + leadingIndex,
|
||||
_vowelJamoBase + vowelIndex,
|
||||
];
|
||||
|
||||
if (trailingIndex > 0) {
|
||||
result.add(_trailingJamoBase + trailingIndex);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Common Latin character decompositions (NFKD) for es, pt, fr
|
||||
const Map<int, List<int>> _latinDecompositions = {
|
||||
// Uppercase with acute accent
|
||||
0x00C1: [0x0041, 0x0301], // Á → A + ́
|
||||
0x00C9: [0x0045, 0x0301], // É → E + ́
|
||||
0x00CD: [0x0049, 0x0301], // Í → I + ́
|
||||
0x00D3: [0x004F, 0x0301], // Ó → O + ́
|
||||
0x00DA: [0x0055, 0x0301], // Ú → U + ́
|
||||
// Lowercase with acute accent
|
||||
0x00E1: [0x0061, 0x0301], // á → a + ́
|
||||
0x00E9: [0x0065, 0x0301], // é → e + ́
|
||||
0x00ED: [0x0069, 0x0301], // í → i + ́
|
||||
0x00F3: [0x006F, 0x0301], // ó → o + ́
|
||||
0x00FA: [0x0075, 0x0301], // ú → u + ́
|
||||
// Grave accent
|
||||
0x00C0: [0x0041, 0x0300], // À → A + ̀
|
||||
0x00C8: [0x0045, 0x0300], // È → E + ̀
|
||||
0x00CC: [0x0049, 0x0300], // Ì → I + ̀
|
||||
0x00D2: [0x004F, 0x0300], // Ò → O + ̀
|
||||
0x00D9: [0x0055, 0x0300], // Ù → U + ̀
|
||||
0x00E0: [0x0061, 0x0300], // à → a + ̀
|
||||
0x00E8: [0x0065, 0x0300], // è → e + ̀
|
||||
0x00EC: [0x0069, 0x0300], // ì → i + ̀
|
||||
0x00F2: [0x006F, 0x0300], // ò → o + ̀
|
||||
0x00F9: [0x0075, 0x0300], // ù → u + ̀
|
||||
// Circumflex
|
||||
0x00C2: [0x0041, 0x0302], // Â → A + ̂
|
||||
0x00CA: [0x0045, 0x0302], // Ê → E + ̂
|
||||
0x00CE: [0x0049, 0x0302], // Î → I + ̂
|
||||
0x00D4: [0x004F, 0x0302], // Ô → O + ̂
|
||||
0x00DB: [0x0055, 0x0302], // Û → U + ̂
|
||||
0x00E2: [0x0061, 0x0302], // â → a + ̂
|
||||
0x00EA: [0x0065, 0x0302], // ê → e + ̂
|
||||
0x00EE: [0x0069, 0x0302], // î → i + ̂
|
||||
0x00F4: [0x006F, 0x0302], // ô → o + ̂
|
||||
0x00FB: [0x0075, 0x0302], // û → u + ̂
|
||||
// Tilde
|
||||
0x00C3: [0x0041, 0x0303], // Ã → A + ̃
|
||||
0x00D1: [0x004E, 0x0303], // Ñ → N + ̃
|
||||
0x00D5: [0x004F, 0x0303], // Õ → O + ̃
|
||||
0x00E3: [0x0061, 0x0303], // ã → a + ̃
|
||||
0x00F1: [0x006E, 0x0303], // ñ → n + ̃
|
||||
0x00F5: [0x006F, 0x0303], // õ → o + ̃
|
||||
// Diaeresis/Umlaut
|
||||
0x00C4: [0x0041, 0x0308], // Ä → A + ̈
|
||||
0x00CB: [0x0045, 0x0308], // Ë → E + ̈
|
||||
0x00CF: [0x0049, 0x0308], // Ï → I + ̈
|
||||
0x00D6: [0x004F, 0x0308], // Ö → O + ̈
|
||||
0x00DC: [0x0055, 0x0308], // Ü → U + ̈
|
||||
0x00E4: [0x0061, 0x0308], // ä → a + ̈
|
||||
0x00EB: [0x0065, 0x0308], // ë → e + ̈
|
||||
0x00EF: [0x0069, 0x0308], // ï → i + ̈
|
||||
0x00F6: [0x006F, 0x0308], // ö → o + ̈
|
||||
0x00FC: [0x0075, 0x0308], // ü → u + ̈
|
||||
// Cedilla
|
||||
0x00C7: [0x0043, 0x0327], // Ç → C + ̧
|
||||
0x00E7: [0x0063, 0x0327], // ç → c + ̧
|
||||
};
|
||||
|
||||
/// Apply NFKD-like decomposition (Hangul + Latin accented characters)
|
||||
String _applyNfkdDecomposition(String text) {
|
||||
final result = <int>[];
|
||||
for (final codePoint in text.runes) {
|
||||
// Check Hangul first
|
||||
if (codePoint >= _hangulSyllableBase && codePoint <= _hangulSyllableEnd) {
|
||||
result.addAll(_decomposeHangulSyllable(codePoint));
|
||||
}
|
||||
// Check Latin decomposition
|
||||
else if (_latinDecompositions.containsKey(codePoint)) {
|
||||
result.addAll(_latinDecompositions[codePoint]!);
|
||||
}
|
||||
// Keep as-is
|
||||
else {
|
||||
result.add(codePoint);
|
||||
}
|
||||
}
|
||||
return String.fromCharCodes(result);
|
||||
}
|
||||
|
||||
String preprocessText(String text, String lang) {
|
||||
// Apply NFKD-like decomposition (especially for Hangul syllables → Jamo)
|
||||
text = _applyNfkdDecomposition(text);
|
||||
|
||||
// Remove emojis
|
||||
text = text.replaceAll(
|
||||
RegExp(
|
||||
r'[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|'
|
||||
r'[\u{1F700}-\u{1F77F}]|[\u{1F780}-\u{1F7FF}]|[\u{1F800}-\u{1F8FF}]|'
|
||||
r'[\u{1F900}-\u{1F9FF}]|[\u{1FA00}-\u{1FA6F}]|[\u{1FA70}-\u{1FAFF}]|'
|
||||
r'[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F1E6}-\u{1F1FF}]',
|
||||
unicode: true,
|
||||
),
|
||||
'');
|
||||
|
||||
// Replace various dashes and symbols
|
||||
const replacements = {
|
||||
'–': '-',
|
||||
'‑': '-',
|
||||
'—': '-',
|
||||
'_': ' ',
|
||||
'\u201C': '"',
|
||||
'\u201D': '"',
|
||||
'\u2018': "'",
|
||||
'\u2019': "'",
|
||||
'´': "'",
|
||||
'`': "'",
|
||||
'[': ' ',
|
||||
']': ' ',
|
||||
'|': ' ',
|
||||
'/': ' ',
|
||||
'#': ' ',
|
||||
'→': ' ',
|
||||
'←': ' ',
|
||||
};
|
||||
for (final entry in replacements.entries) {
|
||||
text = text.replaceAll(entry.key, entry.value);
|
||||
}
|
||||
|
||||
// Remove special symbols
|
||||
text = text.replaceAll(RegExp(r'[♥☆♡©\\]'), '');
|
||||
|
||||
// Replace known expressions
|
||||
text = text.replaceAll('@', ' at ');
|
||||
text = text.replaceAll('e.g.,', 'for example, ');
|
||||
text = text.replaceAll('i.e.,', 'that is, ');
|
||||
|
||||
// Fix spacing around punctuation
|
||||
text = text.replaceAll(' ,', ',');
|
||||
text = text.replaceAll(' .', '.');
|
||||
text = text.replaceAll(' !', '!');
|
||||
text = text.replaceAll(' ?', '?');
|
||||
text = text.replaceAll(' ;', ';');
|
||||
text = text.replaceAll(' :', ':');
|
||||
text = text.replaceAll(" '", "'");
|
||||
|
||||
// Remove duplicate quotes
|
||||
while (text.contains('""')) text = text.replaceAll('""', '"');
|
||||
while (text.contains("''")) text = text.replaceAll("''", "'");
|
||||
while (text.contains('``')) text = text.replaceAll('``', '`');
|
||||
|
||||
// Remove extra spaces
|
||||
text = text.replaceAll(RegExp(r'\s+'), ' ').trim();
|
||||
|
||||
// Add period if needed
|
||||
if (text.isNotEmpty &&
|
||||
!RegExp(r'[.!?;:,\x27\x22\u2018\u2019)\]}…。」』】〉》›»]$').hasMatch(text)) {
|
||||
text += '.';
|
||||
}
|
||||
|
||||
// Validate language
|
||||
if (!isValidLang(lang)) {
|
||||
throw ArgumentError(
|
||||
'Invalid language: $lang. Available: ${availableLangs.join(", ")}');
|
||||
}
|
||||
|
||||
// Wrap text with language tags
|
||||
text = '<$lang>$text</$lang>';
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
class UnicodeProcessor {
|
||||
final Map<int, int> indexer;
|
||||
|
||||
@@ -34,11 +233,17 @@ class UnicodeProcessor {
|
||||
return UnicodeProcessor._(indexer);
|
||||
}
|
||||
|
||||
Map<String, dynamic> call(List<String> textList) {
|
||||
final lengths = textList.map((t) => t.length).toList();
|
||||
Map<String, dynamic> call(List<String> textList, List<String> langList) {
|
||||
// Preprocess texts with language tags
|
||||
final processedTexts = <String>[];
|
||||
for (var i = 0; i < textList.length; i++) {
|
||||
processedTexts.add(preprocessText(textList[i], langList[i]));
|
||||
}
|
||||
|
||||
final lengths = processedTexts.map((t) => t.runes.length).toList();
|
||||
final maxLen = lengths.reduce(math.max);
|
||||
|
||||
final textIds = textList.map((text) {
|
||||
final textIds = processedTexts.map((text) {
|
||||
final row = List<int>.filled(maxLen, 0);
|
||||
final runes = text.runes.toList();
|
||||
for (var i = 0; i < runes.length; i++) {
|
||||
@@ -77,14 +282,18 @@ class TextToSpeech {
|
||||
chunkCompressFactor = cfgs['ttl']['chunk_compress_factor'],
|
||||
ldim = cfgs['ttl']['latent_dim'];
|
||||
|
||||
Future<Map<String, dynamic>> call(String text, Style style, int totalStep,
|
||||
Future<Map<String, dynamic>> call(
|
||||
String text, String lang, Style style, int totalStep,
|
||||
{double speed = 1.05, double silenceDuration = 0.3}) async {
|
||||
final chunks = _chunkText(text);
|
||||
final maxLen = (lang == 'ko' || lang == 'ja') ? 120 : 300;
|
||||
final chunks = _chunkText(text, maxLen: maxLen);
|
||||
final langList = List.filled(chunks.length, lang);
|
||||
List<double>? wavCat;
|
||||
double durCat = 0;
|
||||
|
||||
for (final chunk in chunks) {
|
||||
final result = await _infer([chunk], style, totalStep, speed: speed);
|
||||
for (var i = 0; i < chunks.length; i++) {
|
||||
final result = await _infer([chunks[i]], [langList[i]], style, totalStep,
|
||||
speed: speed);
|
||||
final wav = _safeCast<double>(result['wav']);
|
||||
final duration = _safeCast<double>(result['duration']);
|
||||
|
||||
@@ -108,10 +317,10 @@ class TextToSpeech {
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _infer(
|
||||
List<String> textList, Style style, int totalStep,
|
||||
List<String> textList, List<String> langList, Style style, int totalStep,
|
||||
{double speed = 1.05}) async {
|
||||
final bsz = textList.length;
|
||||
final result = textProcessor.call(textList);
|
||||
final result = textProcessor.call(textList, langList);
|
||||
|
||||
final textIdsRaw = result['textIds'];
|
||||
final textIds = textIdsRaw is List<List<int>>
|
||||
|
||||
@@ -14,7 +14,7 @@ class SupertonicApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Supertonic',
|
||||
title: 'Supertonic 3',
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
useMaterial3: true,
|
||||
@@ -42,8 +42,9 @@ class _TTSPageState extends State<TTSPage> {
|
||||
bool _isLoading = false;
|
||||
bool _isGenerating = false;
|
||||
String _status = 'Not initialized';
|
||||
int _totalSteps = 5;
|
||||
int _totalSteps = 8;
|
||||
double _speed = 1.05;
|
||||
String _selectedLang = 'en';
|
||||
bool _isPlaying = false;
|
||||
String? _lastGeneratedFilePath;
|
||||
|
||||
@@ -119,6 +120,7 @@ class _TTSPageState extends State<TTSPage> {
|
||||
try {
|
||||
final result = await _textToSpeech!.call(
|
||||
_textController.text,
|
||||
_selectedLang,
|
||||
_style!,
|
||||
_totalSteps,
|
||||
speed: _speed,
|
||||
@@ -208,7 +210,7 @@ class _TTSPageState extends State<TTSPage> {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
title: const Text('Supertonic'),
|
||||
title: const Text('Supertonic 3'),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
@@ -310,6 +312,28 @@ class _TTSPageState extends State<TTSPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Language selector
|
||||
Row(
|
||||
children: [
|
||||
const Expanded(flex: 2, child: Text('Language:')),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: DropdownButton<String>(
|
||||
value: _selectedLang,
|
||||
isExpanded: true,
|
||||
items: availableLangs
|
||||
.map((lang) =>
|
||||
DropdownMenuItem(value: lang, child: Text(lang)))
|
||||
.toList(),
|
||||
onChanged: _isLoading || _isGenerating
|
||||
? null
|
||||
: (value) => setState(() => _selectedLang = value!),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Generate button
|
||||
|
||||
@@ -41,11 +41,11 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/objective_c/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e
|
||||
flutter_onnxruntime: 546c7c7630c9259f59021319639b30bdaff148c2
|
||||
audio_session: 728ae3823d914f809c485d390274861a24b0904e
|
||||
flutter_onnxruntime: e6887abc1032d3e5c92f84b912ad42c33e9ce1c9
|
||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||
just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed
|
||||
objective_c: ec13431e45ba099cb734eb2829a5c1cd37986cba
|
||||
just_audio: a42c63806f16995daf5b219ae1d679deb76e6a79
|
||||
objective_c: e5f8194456e8fc943e034d1af00510a1bc29c067
|
||||
onnxruntime-c: ac65025f01072d25d7d394a2b43ac30d9397b260
|
||||
onnxruntime-objc: 5fa03134356d47b642ec85b1023d9907a123d201
|
||||
|
||||
|
||||
42
go/README.md
@@ -4,6 +4,12 @@ This guide provides examples for running TTS inference using `example_onnx.go`.
|
||||
|
||||
## 📰 Update News
|
||||
|
||||
**2026.04.29** - 🎉 **Supertonic 3** released with 31-language support, improved reading accuracy, and v2-compatible public ONNX assets. [Demo](https://huggingface.co/spaces/Supertone/supertonic-3) | [Models](https://huggingface.co/Supertone/supertonic-3)
|
||||
|
||||
**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
|
||||
|
||||
**2025.12.08** - Optimized ONNX models via [OnnxSlim](https://github.com/inisis/OnnxSlim) now available on [Hugging Face Models](https://huggingface.co/Supertone/supertonic)
|
||||
|
||||
**2025.11.23** - Enhanced text preprocessing with comprehensive normalization, emoji removal, symbol replacement, and punctuation handling for improved synthesis quality.
|
||||
|
||||
**2025.11.19** - Added `--speed` parameter to control speech synthesis speed (default: 1.05, recommended range: 0.9-1.5).
|
||||
@@ -24,6 +30,8 @@ This project uses Go modules for dependency management.
|
||||
brew install onnxruntime
|
||||
```
|
||||
|
||||
The Go example auto-detects Homebrew ONNX Runtime paths on both Apple Silicon and Intel Macs.
|
||||
|
||||
**Linux:**
|
||||
```bash
|
||||
# Download ONNX Runtime from GitHub releases
|
||||
@@ -42,7 +50,7 @@ go mod download
|
||||
|
||||
### Configure ONNX Runtime Library Path (Optional)
|
||||
|
||||
If the ONNX Runtime library is not in a standard location, set the environment variable:
|
||||
If the ONNX Runtime library is not in a standard or Homebrew location, set the environment variable:
|
||||
|
||||
**Automatic Detection (Recommended):**
|
||||
|
||||
@@ -71,10 +79,10 @@ go run example_onnx.go helper.go
|
||||
```
|
||||
|
||||
This will use:
|
||||
- Voice style: `assets/voice_styles/M1.json`
|
||||
- Voice style: `../assets/voice_styles/M1.json`
|
||||
- Text: "This morning, I took a walk in the park, and the sound of the birds and the breeze was so pleasant that I stopped for a long time just to listen."
|
||||
- Output directory: `results/`
|
||||
- Total steps: 5
|
||||
- Total steps: 8
|
||||
- Number of generations: 4
|
||||
|
||||
### Example 2: Batch Inference
|
||||
@@ -82,14 +90,15 @@ Process multiple voice styles and texts at once:
|
||||
```bash
|
||||
go run example_onnx.go helper.go \
|
||||
--batch \
|
||||
-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."
|
||||
-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.|오늘 아침에 공원을 산책했는데, 새소리와 바람 소리가 너무 기분 좋았어요." \
|
||||
-lang "en,ko"
|
||||
```
|
||||
|
||||
This will:
|
||||
- Generate speech for 2 different voice-text pairs
|
||||
- Use male voice (M1.json) for the first text
|
||||
- Use female voice (F1.json) for the second text
|
||||
- Generate speech for 2 different voice-text-language pairs
|
||||
- Use male voice (M1.json) for the first text in English
|
||||
- Use female voice (F1.json) for the second text in Korean
|
||||
- Process both samples in a single batch
|
||||
|
||||
### Example 3: High Quality Inference
|
||||
@@ -97,12 +106,12 @@ Increase denoising steps for better quality:
|
||||
```bash
|
||||
go run example_onnx.go helper.go \
|
||||
-total-step 10 \
|
||||
-voice-style "assets/voice_styles/M1.json" \
|
||||
-voice-style "../assets/voice_styles/M1.json" \
|
||||
-text "Increasing the number of denoising steps improves the output's fidelity and overall quality."
|
||||
```
|
||||
|
||||
This will:
|
||||
- Use 10 denoising steps instead of the default 5
|
||||
- Use 10 denoising steps instead of the default 8
|
||||
- Produce higher quality output at the cost of slower inference
|
||||
|
||||
### Example 4: Long-Form Inference
|
||||
@@ -110,7 +119,7 @@ The system automatically chunks long texts into manageable segments, synthesizes
|
||||
|
||||
```bash
|
||||
go run example_onnx.go helper.go \
|
||||
-voice-style "assets/voice_styles/M1.json" \
|
||||
-voice-style "../assets/voice_styles/M1.json" \
|
||||
-text "This is a very long text that will be automatically split into multiple chunks. The system will process each chunk separately and then concatenate them together with natural pauses between segments. This ensures that even very long texts can be processed efficiently while maintaining natural speech flow and avoiding memory issues."
|
||||
```
|
||||
|
||||
@@ -127,17 +136,19 @@ This will:
|
||||
| Argument | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `-use-gpu` | flag | false | Use GPU for inference (default: CPU) |
|
||||
| `-onnx-dir` | str | `assets/onnx` | Path to ONNX model directory |
|
||||
| `-total-step` | int | 5 | Number of denoising steps (higher = better quality, slower) |
|
||||
| `-onnx-dir` | str | `../assets/onnx` | Path to ONNX model directory |
|
||||
| `-total-step` | int | 8 | Number of denoising steps (higher = better quality, slower) |
|
||||
| `-n-test` | int | 4 | Number of times to generate each sample |
|
||||
| `-voice-style` | str | `assets/voice_styles/M1.json` | Voice style file path(s), comma-separated |
|
||||
| `-voice-style` | str | `../assets/voice_styles/M1.json` | Voice style file path(s), comma-separated |
|
||||
| `-text` | str | (long default text) | Text(s) to synthesize, pipe-separated |
|
||||
| `-lang` | str | `en` | Language(s) for synthesis, comma-separated; see the main README for all 31 codes |
|
||||
| `-save-dir` | str | `results` | Output directory |
|
||||
| `--batch` | flag | false | Enable batch mode (multiple text-style pairs, disables automatic chunking) |
|
||||
|
||||
## Notes
|
||||
|
||||
- **Batch Processing**: When using `--batch`, the number of `-voice-style` files must match the number of `-text` entries
|
||||
- **Multilingual Support**: Use `-lang` to specify the language for each text. Available: 31 languages; see the main README for the full list
|
||||
- **Batch Processing**: When using `--batch`, the number of `-voice-style`, `-text`, and `-lang` entries must match
|
||||
- **Automatic Chunking**: Without `--batch`, long texts are automatically split and concatenated with 0.3s pauses
|
||||
- **Quality vs Speed**: Higher `-total-step` values produce better quality but take longer
|
||||
- **GPU Support**: GPU mode is not supported yet
|
||||
@@ -153,4 +164,3 @@ Then run it:
|
||||
```bash
|
||||
./tts_example -voice-style "../assets/voice_styles/M1.json" -text "Hello world"
|
||||
```
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ type Args struct {
|
||||
nTest int
|
||||
voiceStyle []string
|
||||
text []string
|
||||
lang []string
|
||||
saveDir string
|
||||
batch bool
|
||||
}
|
||||
@@ -27,16 +28,17 @@ func parseArgs() *Args {
|
||||
args := &Args{}
|
||||
|
||||
flag.BoolVar(&args.useGPU, "use-gpu", false, "Use GPU for inference (default: CPU)")
|
||||
flag.StringVar(&args.onnxDir, "onnx-dir", "assets/onnx", "Path to ONNX model directory")
|
||||
flag.IntVar(&args.totalStep, "total-step", 5, "Number of denoising steps")
|
||||
flag.StringVar(&args.onnxDir, "onnx-dir", "../assets/onnx", "Path to ONNX model directory")
|
||||
flag.IntVar(&args.totalStep, "total-step", 8, "Number of denoising steps")
|
||||
flag.Float64Var(&args.speed, "speed", 1.05, "Speech speed factor (higher = faster)")
|
||||
flag.IntVar(&args.nTest, "n-test", 4, "Number of times to generate")
|
||||
flag.StringVar(&args.saveDir, "save-dir", "results", "Output directory")
|
||||
flag.BoolVar(&args.batch, "batch", false, "Enable batch mode (multiple text-style pairs)")
|
||||
|
||||
var voiceStyleStr, textStr string
|
||||
flag.StringVar(&voiceStyleStr, "voice-style", "assets/voice_styles/M1.json", "Voice style file path(s), comma-separated")
|
||||
var voiceStyleStr, textStr, langStr string
|
||||
flag.StringVar(&voiceStyleStr, "voice-style", "../assets/voice_styles/M1.json", "Voice style file path(s), comma-separated")
|
||||
flag.StringVar(&textStr, "text", "This morning, I took a walk in the park, and the sound of the birds and the breeze was so pleasant that I stopped for a long time just to listen.", "Text(s) to synthesize, pipe-separated")
|
||||
flag.StringVar(&langStr, "lang", "en", "Language(s) for synthesis, comma-separated")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
@@ -56,6 +58,14 @@ func parseArgs() *Args {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse comma-separated lang
|
||||
if langStr != "" {
|
||||
args.lang = strings.Split(langStr, ",")
|
||||
for i := range args.lang {
|
||||
args.lang[i] = strings.TrimSpace(args.lang[i])
|
||||
}
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
@@ -70,6 +80,7 @@ func main() {
|
||||
saveDir := args.saveDir
|
||||
voiceStylePaths := args.voiceStyle
|
||||
textList := args.text
|
||||
langList := args.lang
|
||||
batch := args.batch
|
||||
|
||||
if batch {
|
||||
@@ -78,6 +89,11 @@ func main() {
|
||||
len(voiceStylePaths), len(textList))
|
||||
os.Exit(1)
|
||||
}
|
||||
if len(langList) != len(textList) {
|
||||
fmt.Printf("Error: Number of languages (%d) must match number of texts (%d)\n",
|
||||
len(langList), len(textList))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
bsz := len(voiceStylePaths)
|
||||
@@ -126,7 +142,7 @@ func main() {
|
||||
|
||||
if batch {
|
||||
Timer("Generating speech from text", func() interface{} {
|
||||
w, d, err := textToSpeech.Batch(textList, style, totalStep, speed)
|
||||
w, d, err := textToSpeech.Batch(textList, langList, style, totalStep, speed)
|
||||
if err != nil {
|
||||
fmt.Printf("Error generating speech: %v\n", err)
|
||||
os.Exit(1)
|
||||
@@ -137,7 +153,7 @@ func main() {
|
||||
})
|
||||
} else {
|
||||
Timer("Generating speech from text", func() interface{} {
|
||||
w, d, err := textToSpeech.Call(textList[0], style, totalStep, speed, 0.3)
|
||||
w, d, err := textToSpeech.Call(textList[0], langList[0], style, totalStep, speed, 0.3)
|
||||
if err != nil {
|
||||
fmt.Printf("Error generating speech: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -7,6 +7,7 @@ require (
|
||||
github.com/go-audio/wav v1.1.0
|
||||
github.com/mjibson/go-dsp v0.0.0-20180508042940-11479a337f12
|
||||
github.com/yalue/onnxruntime_go v1.11.0
|
||||
golang.org/x/text v0.14.0
|
||||
)
|
||||
|
||||
require github.com/go-audio/riff v1.0.0 // indirect
|
||||
|
||||
@@ -8,3 +8,5 @@ github.com/mjibson/go-dsp v0.0.0-20180508042940-11479a337f12 h1:dd7vnTDfjtwCETZD
|
||||
github.com/mjibson/go-dsp v0.0.0-20180508042940-11479a337f12/go.mod h1:i/KKcxEWEO8Yyl11DYafRPKOPVYTrhxiTRigjtEEXZU=
|
||||
github.com/yalue/onnxruntime_go v1.11.0 h1:aKH4yPIbqfcB3SfnQWq/WxzLelkyolntHnffL3eMBHY=
|
||||
github.com/yalue/onnxruntime_go v1.11.0/go.mod h1:b4X26A8pekNb1ACJ58wAXgNKeUCGEAQ9dmACut9Sm/4=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
|
||||
99
go/helper.go
@@ -10,12 +10,17 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/go-audio/audio"
|
||||
"github.com/go-audio/wav"
|
||||
ort "github.com/yalue/onnxruntime_go"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
// Available languages for multilingual TTS
|
||||
var AvailableLangs = []string{"en", "ko", "ja", "ar", "bg", "cs", "da", "de", "el", "es", "et", "fi", "fr", "hi", "hr", "hu", "id", "it", "lt", "lv", "nl", "pl", "pt", "ro", "ru", "sk", "sl", "sv", "tr", "uk", "vi", "na"}
|
||||
|
||||
// Config structures
|
||||
type SpecProcessorConfig struct {
|
||||
NFFT int `json:"n_fft"`
|
||||
@@ -108,11 +113,11 @@ func NewUnicodeProcessor(unicodeIndexerPath string) (*UnicodeProcessor, error) {
|
||||
}
|
||||
|
||||
// Call processes text list to text IDs and mask
|
||||
func (up *UnicodeProcessor) Call(textList []string) ([][]int64, [][][]float64) {
|
||||
func (up *UnicodeProcessor) Call(textList []string, langList []string) ([][]int64, [][][]float64) {
|
||||
// Preprocess texts
|
||||
processedTexts := make([]string, len(textList))
|
||||
for i, text := range textList {
|
||||
processedTexts[i] = preprocessText(text)
|
||||
processedTexts[i] = preprocessText(text, langList[i])
|
||||
}
|
||||
|
||||
// Get text lengths
|
||||
@@ -325,14 +330,21 @@ func splitSentences(text string) []string {
|
||||
return sentences
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
func preprocessText(text string) string {
|
||||
// TODO: Need advanced normalizer for better performance
|
||||
// NOTE: Go doesn't have built-in NFKD normalization like Python
|
||||
// For full Unicode normalization, use golang.org/x/text/unicode/norm
|
||||
// This implementation handles basic text preprocessing
|
||||
// isValidLang checks if a language is in the available languages list
|
||||
func isValidLang(lang string) bool {
|
||||
for _, l := range AvailableLangs {
|
||||
if l == lang {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// FIXME: this should be fixed for non-English languages
|
||||
// Utility functions
|
||||
func preprocessText(text string, lang string) string {
|
||||
// TODO: Need advanced normalizer for better performance
|
||||
// Apply NFKD normalization using golang.org/x/text/unicode/norm
|
||||
text = norm.NFKD.String(text)
|
||||
|
||||
// Remove emojis and various Unicode symbols
|
||||
emojiPattern := regexp.MustCompile(`[\x{1F600}-\x{1F64F}\x{1F300}-\x{1F5FF}\x{1F680}-\x{1F6FF}\x{1F700}-\x{1F77F}\x{1F780}-\x{1F7FF}\x{1F800}-\x{1F8FF}\x{1F900}-\x{1F9FF}\x{1FA00}-\x{1FA6F}\x{1FA70}-\x{1FAFF}\x{2600}-\x{26FF}\x{2700}-\x{27BF}\x{1F1E6}-\x{1F1FF}]+`)
|
||||
@@ -343,7 +355,6 @@ func preprocessText(text string) string {
|
||||
"–": "-", // en dash
|
||||
"‑": "-", // non-breaking hyphen
|
||||
"—": "-", // em dash
|
||||
"¯": " ", // macron
|
||||
"_": " ", // underscore
|
||||
"\u201C": "\"", // left double quote
|
||||
"\u201D": "\"", // right double quote
|
||||
@@ -364,11 +375,6 @@ func preprocessText(text string) string {
|
||||
text = strings.ReplaceAll(text, old, new)
|
||||
}
|
||||
|
||||
// Remove combining diacritics (common combining marks)
|
||||
// FIXME: this should be fixed for non-English languages
|
||||
diacriticsPattern := regexp.MustCompile(`[\x{0302}\x{0303}\x{0304}\x{0305}\x{0306}\x{0307}\x{0308}\x{030A}\x{030B}\x{030C}\x{0327}\x{0328}\x{0329}\x{032A}\x{032B}\x{032C}\x{032D}\x{032E}\x{032F}]`)
|
||||
text = diacriticsPattern.ReplaceAllString(text, "")
|
||||
|
||||
// Remove special symbols
|
||||
specialSymbols := []string{"♥", "☆", "♡", "©", "\\"}
|
||||
for _, symbol := range specialSymbols {
|
||||
@@ -418,6 +424,14 @@ func preprocessText(text string) string {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate language
|
||||
if !isValidLang(lang) {
|
||||
panic(fmt.Sprintf("Invalid language: %s. Available: %v", lang, AvailableLangs))
|
||||
}
|
||||
|
||||
// Wrap text with language tags
|
||||
text = fmt.Sprintf("<%s>%s</%s>", lang, text, lang)
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
@@ -661,11 +675,11 @@ func (tts *TextToSpeech) sampleNoisyLatent(durOnnx []float32) ([][][]float64, []
|
||||
return noisyLatent, latentMask
|
||||
}
|
||||
|
||||
func (tts *TextToSpeech) _infer(textList []string, style *Style, totalStep int, speed float32) ([]float32, []float32, error) {
|
||||
func (tts *TextToSpeech) _infer(textList []string, langList []string, style *Style, totalStep int, speed float32) ([]float32, []float32, error) {
|
||||
bsz := len(textList)
|
||||
|
||||
// Process text
|
||||
textIDs, textMask := tts.textProcessor.Call(textList)
|
||||
textIDs, textMask := tts.textProcessor.Call(textList, langList)
|
||||
textIDsShape := []int64{int64(bsz), int64(len(textIDs[0]))}
|
||||
textMaskShape := []int64{int64(bsz), 1, int64(len(textMask[0][0]))}
|
||||
|
||||
@@ -785,14 +799,18 @@ func (tts *TextToSpeech) _infer(textList []string, style *Style, totalStep int,
|
||||
}
|
||||
|
||||
// Call synthesizes speech from a single text with automatic chunking
|
||||
func (tts *TextToSpeech) Call(text string, style *Style, totalStep int, speed float32, silenceDuration float32) ([]float32, float32, error) {
|
||||
chunks := chunkText(text, 0)
|
||||
func (tts *TextToSpeech) Call(text string, lang string, style *Style, totalStep int, speed float32, silenceDuration float32) ([]float32, float32, error) {
|
||||
maxLen := 300
|
||||
if lang == "ko" || lang == "ja" {
|
||||
maxLen = 120
|
||||
}
|
||||
chunks := chunkText(text, maxLen)
|
||||
|
||||
var wavCat []float32
|
||||
var durCat float32
|
||||
|
||||
for i, chunk := range chunks {
|
||||
wav, duration, err := tts._infer([]string{chunk}, style, totalStep, speed)
|
||||
wav, duration, err := tts._infer([]string{chunk}, []string{lang}, style, totalStep, speed)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -818,8 +836,8 @@ func (tts *TextToSpeech) Call(text string, style *Style, totalStep int, speed fl
|
||||
}
|
||||
|
||||
// Batch synthesizes speech from multiple texts
|
||||
func (tts *TextToSpeech) Batch(textList []string, style *Style, totalStep int, speed float32) ([]float32, []float32, error) {
|
||||
return tts._infer(textList, style, totalStep, speed)
|
||||
func (tts *TextToSpeech) Batch(textList []string, langList []string, style *Style, totalStep int, speed float32) ([]float32, []float32, error) {
|
||||
return tts._infer(textList, langList, style, totalStep, speed)
|
||||
}
|
||||
|
||||
func (tts *TextToSpeech) Destroy() {
|
||||
@@ -902,30 +920,43 @@ func LoadTextToSpeech(onnxDir string, useGPU bool, cfg Config) (*TextToSpeech, e
|
||||
func InitializeONNXRuntime() error {
|
||||
libPath := os.Getenv("ONNXRUNTIME_LIB_PATH")
|
||||
if libPath == "" {
|
||||
libPath = "/usr/local/lib/libonnxruntime.so"
|
||||
if _, err := os.Stat("/usr/local/lib/libonnxruntime.dylib"); err == nil {
|
||||
libPath = "/usr/local/lib/libonnxruntime.dylib"
|
||||
} else if _, err := os.Stat("/usr/lib/libonnxruntime.so"); err == nil {
|
||||
libPath = "/usr/lib/libonnxruntime.so"
|
||||
candidates := []string{
|
||||
"/opt/homebrew/opt/onnxruntime/lib/libonnxruntime.dylib",
|
||||
"/usr/local/opt/onnxruntime/lib/libonnxruntime.dylib",
|
||||
"/opt/homebrew/lib/libonnxruntime.dylib",
|
||||
"/usr/local/lib/libonnxruntime.dylib",
|
||||
"/usr/local/lib/libonnxruntime.so",
|
||||
"/usr/lib/libonnxruntime.so",
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
if _, err := os.Stat(candidate); err == nil {
|
||||
libPath = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
if libPath == "" {
|
||||
libPath = "/usr/local/lib/libonnxruntime.so"
|
||||
}
|
||||
}
|
||||
ort.SetSharedLibraryPath(libPath)
|
||||
|
||||
if err := ort.InitializeEnvironment(); err != nil {
|
||||
return fmt.Errorf("failed to initialize ONNX Runtime: %w\nHint: Set ONNXRUNTIME_LIB_PATH environment variable", err)
|
||||
return fmt.Errorf("failed to initialize ONNX Runtime: %w\nHint: install ONNX Runtime (macOS: brew install onnxruntime) or set ONNXRUNTIME_LIB_PATH", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sanitizeFilename creates a safe filename from text
|
||||
// sanitizeFilename creates a safe filename from text (supports Unicode)
|
||||
func sanitizeFilename(text string, maxLen int) string {
|
||||
if len(text) > maxLen {
|
||||
text = text[:maxLen]
|
||||
runes := []rune(text)
|
||||
if len(runes) > maxLen {
|
||||
runes = runes[:maxLen]
|
||||
}
|
||||
|
||||
result := make([]rune, 0, len(text))
|
||||
for _, r := range text {
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
|
||||
result := make([]rune, 0, len(runes))
|
||||
for _, r := range runes {
|
||||
// unicode.IsLetter matches any Unicode letter, unicode.IsDigit matches any Unicode digit
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
||||
result = append(result, r)
|
||||
} else {
|
||||
result = append(result, '_')
|
||||
|
||||
BIN
img/Supertonic3_HeroImage.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
@@ -1,250 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_레이어_2" data-name="레이어 2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1920 1080">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1, .cls-2 {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #227cff;
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: #ff0;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
stroke: #0a0a0a;
|
||||
stroke-miterlimit: 10;
|
||||
stroke-width: 1.72px;
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
fill: #f2f2f2;
|
||||
}
|
||||
|
||||
.cls-6 {
|
||||
fill: #0a0a0a;
|
||||
}
|
||||
|
||||
.cls-7 {
|
||||
clip-path: url(#clippath);
|
||||
}
|
||||
</style>
|
||||
<clipPath id="clippath">
|
||||
<rect class="cls-1" x="181.43" width="1626.55" height="1080"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="Work">
|
||||
<g>
|
||||
<rect class="cls-5" width="1920" height="1080"/>
|
||||
<g>
|
||||
<circle class="cls-3" cx="1679.4" cy="880.43" r="59.81"/>
|
||||
<path class="cls-6" d="M1713.35,805.14c-5.14,12.55-10.93,23.87-18.55,37.52l14.39-2.21c-12.75,23.11-27.5,45.05-44.09,65.57l14.46-2.22c-18.8,23.34-39.84,44.76-62.85,63.95,37.17-24.18,63.26-49.33,92.3-75.66l-16.71,2.56c24.03-21.85,46.85-44.98,68.39-69.3l-22.1,3.39c10.89-12.33,20.91-24.28,30.11-35.66l-55.34,12.05Z"/>
|
||||
</g>
|
||||
<g class="cls-7">
|
||||
<path class="cls-4" d="M1036.15-20.65c-38.28,93.45-81.37,177.77-138.14,279.42l107.16-16.43c-94.95,172.09-204.79,335.46-328.29,488.29l107.68-16.51c-139.96,173.79-296.71,333.29-468.05,476.23,276.77-180.08,471.07-367.33,687.29-563.39l-124.4,19.07c178.91-162.71,348.9-334.99,509.26-516.04l-164.57,25.23c81.12-91.85,155.67-180.81,224.18-265.57l-412.13,89.69Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-6" d="M157.78,462.19h7.78v52.03h28.69v7.36h-36.47v-59.39Z"/>
|
||||
<path class="cls-6" d="M204.45,468.54c-2.84,0-5.19-2.26-5.19-5.1s2.34-5.1,5.19-5.1,5.02,2.34,5.02,5.1-2.17,5.1-5.02,5.1ZM200.77,479.75h7.19v41.82h-7.19v-41.82Z"/>
|
||||
<path class="cls-6" d="M237.74,539.9c-9.03,0-16.06-3.85-19.66-8.53l5.02-5.02c3.35,4.27,8.2,7.03,14.64,7.03,7.11,0,13.97-4.43,13.97-14.47v-4.77c-2.84,4.18-8.37,7.36-14.56,7.36-11.63,0-20.49-9.37-20.49-21.33s8.87-21.25,20.49-21.25c6.19,0,11.71,3.09,14.56,7.28v-6.44h7.19v39.4c0,13.97-9.2,20.74-21.16,20.74ZM238.16,514.8c8.37,0,14.14-6.19,14.14-14.64s-5.77-14.64-14.14-14.64-14.14,6.27-14.14,14.64,5.77,14.64,14.14,14.64Z"/>
|
||||
<path class="cls-6" d="M270.78,458.84h7.19v27.35c2.84-4.94,7.86-7.28,13.3-7.28,9.37,0,15.81,6.52,15.81,16.9v25.76h-7.11v-24.68c0-7.03-4.01-11.38-9.87-11.38-6.78,0-12.13,5.52-12.13,15.31v20.74h-7.19v-62.74Z"/>
|
||||
<path class="cls-6" d="M334.1,521.99c-7.28,0-12.8-4.1-12.8-12.8v-22.84h-8.87v-6.61h8.87v-11.63h7.19v11.63h12.05v6.61h-12.05v21.92c0,5.35,2.43,7.11,6.94,7.11,1.76,0,3.76-.33,5.1-.84v6.44c-1.76.59-3.85,1-6.44,1Z"/>
|
||||
<path class="cls-6" d="M348.07,479.75h7.19v6.44c2.84-4.94,7.86-7.28,13.3-7.28,9.37,0,15.81,6.52,15.81,16.9v25.76h-7.11v-24.68c0-7.03-4.01-11.38-9.87-11.38-6.78,0-12.13,5.52-12.13,15.31v20.74h-7.19v-41.82Z"/>
|
||||
<path class="cls-6" d="M399.35,468.54c-2.84,0-5.19-2.26-5.19-5.1s2.34-5.1,5.19-5.1,5.02,2.34,5.02,5.1-2.17,5.1-5.02,5.1ZM395.67,479.75h7.19v41.82h-7.19v-41.82Z"/>
|
||||
<path class="cls-6" d="M414.82,479.75h7.19v6.44c2.84-4.94,7.86-7.28,13.3-7.28,9.37,0,15.81,6.52,15.81,16.9v25.76h-7.11v-24.68c0-7.03-4.01-11.38-9.87-11.38-6.78,0-12.13,5.52-12.13,15.31v20.74h-7.19v-41.82Z"/>
|
||||
<path class="cls-6" d="M480.23,539.9c-9.03,0-16.06-3.85-19.66-8.53l5.02-5.02c3.35,4.27,8.2,7.03,14.64,7.03,7.11,0,13.97-4.43,13.97-14.47v-4.77c-2.84,4.18-8.37,7.36-14.56,7.36-11.63,0-20.49-9.37-20.49-21.33s8.87-21.25,20.49-21.25c6.19,0,11.71,3.09,14.56,7.28v-6.44h7.19v39.4c0,13.97-9.2,20.74-21.16,20.74ZM480.65,514.8c8.37,0,14.14-6.19,14.14-14.64s-5.77-14.64-14.14-14.64-14.14,6.27-14.14,14.64,5.77,14.64,14.14,14.64Z"/>
|
||||
<path class="cls-6" d="M511.93,494.56h21.33v7.19h-21.33v-7.19Z"/>
|
||||
<path class="cls-6" d="M544.97,462.19h34.55v7.36h-26.77v17.98h21.16v7.36h-21.16v26.68h-7.78v-59.39Z"/>
|
||||
<path class="cls-6" d="M600.68,478.92c6.27,0,11.96,3.26,14.72,7.28v-6.44h7.19v41.82h-7.19v-6.44c-2.76,4.02-8.45,7.28-14.72,7.28-11.71,0-20.49-9.79-20.49-21.75s8.78-21.75,20.49-21.75ZM601.77,485.52c-8.45,0-14.3,6.69-14.3,15.14s5.86,15.14,14.3,15.14,14.22-6.69,14.22-15.14-5.77-15.14-14.22-15.14Z"/>
|
||||
<path class="cls-6" d="M647.11,522.41c-7.44,0-13.89-3.18-16.39-9.7l5.86-3.26c1.5,4.27,6.02,6.61,10.62,6.61,4.01,0,7.28-2.01,7.28-5.6,0-3.01-1.84-4.85-7.28-6.52l-4.18-1.34c-6.86-2.01-10.46-6.36-10.46-12.21.08-7.19,6.36-11.46,14.39-11.46,6.19,0,11.04,2.59,13.8,7.28l-5.35,3.68c-1.84-2.68-4.68-4.77-8.7-4.77-3.51,0-6.94,1.92-6.94,5.02,0,2.51,1.34,4.6,5.94,6.02l4.6,1.42c7.19,2.17,11.38,5.94,11.38,12.3,0,8.03-6.19,12.55-14.55,12.55Z"/>
|
||||
<path class="cls-6" d="M686.25,521.99c-7.28,0-12.8-4.1-12.8-12.8v-22.84h-8.87v-6.61h8.87v-11.63h7.19v11.63h12.04v6.61h-12.04v21.92c0,5.35,2.43,7.11,6.94,7.11,1.76,0,3.76-.33,5.1-.84v6.44c-1.76.59-3.85,1-6.44,1Z"/>
|
||||
<path class="cls-6" d="M700.56,533.54h-4.77l5.6-11.96c-2.01-1-3.43-3.09-3.43-5.6,0-3.35,2.76-6.27,6.19-6.27s6.19,2.93,6.19,6.27c0,1.42-.5,2.68-1.17,3.76l-8.62,13.8Z"/>
|
||||
<path class="cls-6" d="M766.14,522.58c-17.06,0-30.7-13.47-30.7-30.7s13.63-30.7,30.7-30.7,30.7,13.47,30.7,30.7-13.55,30.7-30.7,30.7ZM766.14,515.22c13.05,0,22.92-10.29,22.92-23.34s-9.87-23.34-22.92-23.34-22.84,10.29-22.84,23.34,9.87,23.34,22.84,23.34Z"/>
|
||||
<path class="cls-6" d="M805.7,479.75h7.19v6.44c2.84-4.94,7.86-7.28,13.3-7.28,9.37,0,15.81,6.52,15.81,16.9v25.76h-7.11v-24.68c0-7.03-4.01-11.38-9.87-11.38-6.78,0-12.13,5.52-12.13,15.31v20.74h-7.19v-41.82Z"/>
|
||||
<path class="cls-6" d="M851.96,494.56h21.33v7.19h-21.33v-7.19Z"/>
|
||||
<path class="cls-6" d="M885,462.19h17.06c18.24,0,31.62,12.71,31.62,29.7s-13.38,29.7-31.62,29.7h-17.06v-59.39ZM902.06,514.3c14.3,0,23.76-9.54,23.76-22.42s-9.45-22.42-23.76-22.42h-9.29v44.84h9.29Z"/>
|
||||
<path class="cls-6" d="M961.03,478.92c10.96,0,19.83,7.61,19.91,21,0,.75,0,1.25-.08,2.17h-34.3c.25,7.86,6.19,13.72,14.47,13.72,6.44,0,10.46-2.84,13.05-7.28l5.69,3.93c-3.76,6.11-10.12,9.95-18.82,9.95-12.97,0-21.67-9.45-21.67-21.75s9.03-21.75,21.75-21.75ZM947.07,496.23h26.52c-1-6.86-6.44-10.96-12.8-10.96s-12.38,4.02-13.72,10.96Z"/>
|
||||
<path class="cls-6" d="M982.53,479.75h8.03l14.3,31.62,14.3-31.62h8.03l-19.32,41.82h-6.02l-19.32-41.82Z"/>
|
||||
<path class="cls-6" d="M1036.48,468.54c-2.84,0-5.19-2.26-5.19-5.1s2.34-5.1,5.19-5.1,5.02,2.34,5.02,5.1-2.17,5.1-5.02,5.1ZM1032.8,479.75h7.19v41.82h-7.19v-41.82Z"/>
|
||||
<path class="cls-6" d="M1070.61,522.41c-12.63,0-21.92-9.62-21.92-21.75s9.28-21.75,21.92-21.75c8.62,0,15.64,4.52,19.32,11.21l-6.27,3.51c-2.34-4.77-7.03-8.03-13.05-8.03-8.7,0-14.64,6.69-14.64,15.06s5.94,15.06,14.64,15.06c6.02,0,10.71-3.26,13.05-8.03l6.27,3.51c-3.68,6.69-10.71,11.21-19.32,11.21Z"/>
|
||||
<path class="cls-6" d="M1116.03,478.92c10.96,0,19.83,7.61,19.91,21,0,.75,0,1.25-.08,2.17h-34.3c.25,7.86,6.19,13.72,14.47,13.72,6.44,0,10.46-2.84,13.05-7.28l5.69,3.93c-3.76,6.11-10.12,9.95-18.82,9.95-12.97,0-21.67-9.45-21.67-21.75s9.03-21.75,21.75-21.75ZM1102.06,496.23h26.52c-1-6.86-6.44-10.96-12.8-10.96s-12.38,4.02-13.72,10.96Z"/>
|
||||
<path class="cls-6" d="M1173.33,469.55h-18.49v-7.36h44.58v7.36h-18.49v52.03h-7.61v-52.03Z"/>
|
||||
<path class="cls-6" d="M1219.08,469.55h-18.49v-7.36h44.58v7.36h-18.49v52.03h-7.61v-52.03Z"/>
|
||||
<path class="cls-6" d="M1253.71,507.19c3.18,5.02,8.03,8.11,14.72,8.11,6.11,0,10.96-3.35,10.96-8.87,0-4.77-3.01-7.95-8.53-10.04l-7.86-3.01c-9.29-3.43-13.47-8.28-13.47-16.4,0-9.7,7.95-15.81,18.57-15.81,7.44,0,13.63,3.35,17.32,8.11l-5.69,5.02c-3.01-3.6-6.69-5.86-11.79-5.86-5.86,0-10.71,3.26-10.71,8.2s2.93,7.28,8.95,9.54l7.19,2.76c8.78,3.35,13.8,8.36,13.8,17.15,0,10.12-7.86,16.48-18.9,16.48-9.79,0-17.73-4.6-20.83-10.71l6.27-4.68Z"/>
|
||||
<path class="cls-6" d="M1299.64,522.25c-3.43,0-6.19-2.84-6.19-6.27s2.76-6.27,6.19-6.27,6.19,2.93,6.19,6.27-2.76,6.27-6.19,6.27Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-6" d="M175.59,793.41c0,5.19-3.89,9.12-9.89,9.12h-5.03v10.5h-3.77v-28.77h8.79c6,0,9.89,3.97,9.89,9.16ZM171.86,793.41c0-3.2-2.19-5.63-6.16-5.63h-5.03v11.27h5.03c3.97,0,6.16-2.43,6.16-5.63Z"/>
|
||||
<path class="cls-6" d="M186.24,792.36c3.04,0,5.8,1.58,7.13,3.53v-3.12h3.49v20.26h-3.49v-3.12c-1.34,1.95-4.09,3.53-7.13,3.53-5.67,0-9.93-4.74-9.93-10.54s4.25-10.54,9.93-10.54ZM186.77,795.56c-4.09,0-6.93,3.24-6.93,7.34s2.84,7.34,6.93,7.34,6.89-3.24,6.89-7.34-2.8-7.34-6.89-7.34Z"/>
|
||||
<path class="cls-6" d="M201.8,792.76h3.48v3.93c.81-2.59,3.32-4.13,5.67-4.13.53,0,1.01.04,1.58.16v3.61c-.65-.24-1.22-.32-1.91-.32-2.55,0-5.35,2.23-5.35,6.97v10.05h-3.48v-20.26Z"/>
|
||||
<path class="cls-6" d="M222.67,792.36c3.04,0,5.79,1.58,7.13,3.53v-3.12h3.48v20.26h-3.48v-3.12c-1.34,1.95-4.09,3.53-7.13,3.53-5.67,0-9.93-4.74-9.93-10.54s4.26-10.54,9.93-10.54ZM223.2,795.56c-4.09,0-6.93,3.24-6.93,7.34s2.84,7.34,6.93,7.34,6.89-3.24,6.89-7.34-2.8-7.34-6.89-7.34Z"/>
|
||||
<path class="cls-6" d="M238.23,792.76h3.49v3.12c1.17-2.15,3.4-3.53,6.12-3.53,3.04,0,5.23,1.66,6.24,4.34,1.09-2.67,3.65-4.34,6.65-4.34,4.42,0,7.09,3.28,7.09,8.23v12.44h-3.49v-11.92c0-3.28-1.38-5.55-4.01-5.55-3.28,0-5.55,2.84-5.55,7.42v10.05h-3.48v-11.92c0-3.28-1.34-5.55-3.97-5.55-3.28,0-5.59,2.84-5.59,7.42v10.05h-3.49v-20.26Z"/>
|
||||
<path class="cls-6" d="M281.43,792.36c5.31,0,9.61,3.69,9.65,10.17,0,.36,0,.61-.04,1.05h-16.62c.12,3.81,3,6.65,7.01,6.65,3.12,0,5.07-1.38,6.32-3.53l2.76,1.9c-1.82,2.96-4.9,4.82-9.12,4.82-6.28,0-10.5-4.58-10.5-10.54s4.38-10.54,10.54-10.54ZM274.66,800.74h12.85c-.49-3.32-3.12-5.31-6.2-5.31s-6,1.95-6.65,5.31Z"/>
|
||||
<path class="cls-6" d="M301.98,813.23c-3.53,0-6.2-1.99-6.2-6.2v-11.06h-4.3v-3.2h4.3v-5.63h3.49v5.63h5.84v3.2h-5.84v10.62c0,2.59,1.18,3.44,3.36,3.44.85,0,1.82-.16,2.47-.41v3.12c-.85.28-1.86.49-3.12.49Z"/>
|
||||
<path class="cls-6" d="M316.61,792.36c5.31,0,9.61,3.69,9.65,10.17,0,.36,0,.61-.04,1.05h-16.62c.12,3.81,3,6.65,7.01,6.65,3.12,0,5.07-1.38,6.32-3.53l2.76,1.9c-1.82,2.96-4.9,4.82-9.12,4.82-6.28,0-10.5-4.58-10.5-10.54s4.38-10.54,10.54-10.54ZM309.84,800.74h12.85c-.49-3.32-3.12-5.31-6.2-5.31s-6,1.95-6.65,5.31Z"/>
|
||||
<path class="cls-6" d="M329.57,792.76h3.48v3.93c.81-2.59,3.32-4.13,5.67-4.13.53,0,1.01.04,1.58.16v3.61c-.65-.24-1.22-.32-1.91-.32-2.55,0-5.35,2.23-5.35,6.97v10.05h-3.48v-20.26Z"/>
|
||||
<path class="cls-6" d="M348.5,813.43c-3.61,0-6.73-1.54-7.94-4.7l2.84-1.58c.73,2.07,2.92,3.2,5.15,3.2,1.95,0,3.53-.97,3.53-2.72,0-1.46-.89-2.35-3.53-3.16l-2.03-.65c-3.32-.97-5.07-3.08-5.07-5.92.04-3.49,3.08-5.55,6.97-5.55,3,0,5.35,1.26,6.69,3.53l-2.59,1.78c-.89-1.3-2.27-2.31-4.21-2.31-1.7,0-3.36.93-3.36,2.43,0,1.22.65,2.23,2.88,2.92l2.23.69c3.49,1.05,5.51,2.88,5.51,5.96,0,3.89-3,6.08-7.05,6.08Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-6" d="M642.94,793.41c0,5.19-3.89,9.12-9.89,9.12h-5.03v10.5h-3.77v-28.77h8.79c6,0,9.89,3.97,9.89,9.16ZM639.22,793.41c0-3.2-2.19-5.63-6.16-5.63h-5.03v11.27h5.03c3.97,0,6.16-2.43,6.16-5.63Z"/>
|
||||
<path class="cls-6" d="M645.58,792.76h3.49v3.93c.81-2.59,3.32-4.13,5.67-4.13.53,0,1.01.04,1.58.16v3.61c-.65-.24-1.22-.32-1.9-.32-2.55,0-5.35,2.23-5.35,6.97v10.05h-3.49v-20.26Z"/>
|
||||
<path class="cls-6" d="M667.05,813.43c-6.12,0-10.62-4.7-10.62-10.54s4.5-10.54,10.62-10.54,10.58,4.7,10.58,10.54-4.5,10.54-10.58,10.54ZM667.05,810.19c4.21,0,7.01-3.24,7.01-7.29s-2.8-7.29-7.01-7.29-7.05,3.24-7.05,7.29,2.84,7.29,7.05,7.29Z"/>
|
||||
<path class="cls-6" d="M689.63,821.9c-4.38,0-7.78-1.86-9.52-4.13l2.43-2.43c1.62,2.07,3.97,3.4,7.09,3.4,3.44,0,6.77-2.15,6.77-7.01v-2.31c-1.38,2.03-4.05,3.57-7.05,3.57-5.63,0-9.93-4.54-9.93-10.33s4.3-10.29,9.93-10.29c3,0,5.67,1.5,7.05,3.53v-3.12h3.48v19.09c0,6.77-4.46,10.05-10.25,10.05ZM689.83,809.74c4.05,0,6.85-3,6.85-7.09s-2.8-7.09-6.85-7.09-6.85,3.04-6.85,7.09,2.8,7.09,6.85,7.09Z"/>
|
||||
<path class="cls-6" d="M704.82,792.76h3.49v3.93c.81-2.59,3.32-4.13,5.67-4.13.53,0,1.01.04,1.58.16v3.61c-.65-.24-1.22-.32-1.9-.32-2.55,0-5.35,2.23-5.35,6.97v10.05h-3.49v-20.26Z"/>
|
||||
<path class="cls-6" d="M725.69,792.36c3.04,0,5.79,1.58,7.13,3.53v-3.12h3.48v20.26h-3.48v-3.12c-1.34,1.95-4.09,3.53-7.13,3.53-5.67,0-9.93-4.74-9.93-10.54s4.26-10.54,9.93-10.54ZM726.22,795.56c-4.09,0-6.93,3.24-6.93,7.34s2.84,7.34,6.93,7.34,6.89-3.24,6.89-7.34-2.8-7.34-6.89-7.34Z"/>
|
||||
<path class="cls-6" d="M741.25,792.76h3.49v3.12c1.17-2.15,3.4-3.53,6.12-3.53,3.04,0,5.23,1.66,6.24,4.34,1.09-2.67,3.65-4.34,6.65-4.34,4.42,0,7.09,3.28,7.09,8.23v12.44h-3.49v-11.92c0-3.28-1.38-5.55-4.01-5.55-3.28,0-5.55,2.84-5.55,7.42v10.05h-3.48v-11.92c0-3.28-1.34-5.55-3.97-5.55-3.28,0-5.59,2.84-5.59,7.42v10.05h-3.49v-20.26Z"/>
|
||||
<path class="cls-6" d="M775.49,792.76h3.49v3.12c1.17-2.15,3.4-3.53,6.12-3.53,3.04,0,5.23,1.66,6.24,4.34,1.09-2.67,3.65-4.34,6.65-4.34,4.42,0,7.09,3.28,7.09,8.23v12.44h-3.49v-11.92c0-3.28-1.38-5.55-4.01-5.55-3.28,0-5.55,2.84-5.55,7.42v10.05h-3.48v-11.92c0-3.28-1.34-5.55-3.97-5.55-3.28,0-5.59,2.84-5.59,7.42v10.05h-3.49v-20.26Z"/>
|
||||
<path class="cls-6" d="M811.52,787.33c-1.38,0-2.51-1.09-2.51-2.47s1.13-2.47,2.51-2.47,2.43,1.13,2.43,2.47-1.05,2.47-2.43,2.47ZM809.73,792.76h3.49v20.26h-3.49v-20.26Z"/>
|
||||
<path class="cls-6" d="M818.2,792.76h3.49v3.12c1.38-2.39,3.81-3.53,6.44-3.53,4.54,0,7.66,3.16,7.66,8.19v12.48h-3.45v-11.96c0-3.4-1.95-5.51-4.78-5.51-3.28,0-5.88,2.67-5.88,7.42v10.05h-3.49v-20.26Z"/>
|
||||
<path class="cls-6" d="M849.08,821.9c-4.38,0-7.78-1.86-9.52-4.13l2.43-2.43c1.62,2.07,3.97,3.4,7.09,3.4,3.44,0,6.77-2.15,6.77-7.01v-2.31c-1.38,2.03-4.05,3.57-7.05,3.57-5.63,0-9.93-4.54-9.93-10.33s4.3-10.29,9.93-10.29c3,0,5.67,1.5,7.05,3.53v-3.12h3.48v19.09c0,6.77-4.46,10.05-10.25,10.05ZM849.29,809.74c4.05,0,6.85-3,6.85-7.09s-2.8-7.09-6.85-7.09-6.85,3.04-6.85,7.09,2.8,7.09,6.85,7.09Z"/>
|
||||
<path class="cls-6" d="M623.73,824.36h3.48v30.4h-3.48v-30.4Z"/>
|
||||
<path class="cls-6" d="M640.55,834.09c3.04,0,5.8,1.58,7.13,3.53v-3.12h3.49v20.26h-3.49v-3.12c-1.34,1.95-4.09,3.53-7.13,3.53-5.67,0-9.93-4.74-9.93-10.54s4.25-10.54,9.93-10.54ZM641.08,837.29c-4.09,0-6.93,3.24-6.93,7.34s2.84,7.34,6.93,7.34,6.89-3.24,6.89-7.34-2.8-7.34-6.89-7.34Z"/>
|
||||
<path class="cls-6" d="M656.11,834.49h3.48v3.12c1.38-2.39,3.81-3.53,6.44-3.53,4.54,0,7.66,3.16,7.66,8.19v12.48h-3.44v-11.96c0-3.4-1.95-5.51-4.78-5.51-3.28,0-5.88,2.67-5.88,7.42v10.05h-3.48v-20.26Z"/>
|
||||
<path class="cls-6" d="M686.99,863.63c-4.38,0-7.78-1.86-9.52-4.13l2.43-2.43c1.62,2.07,3.97,3.4,7.09,3.4,3.45,0,6.77-2.15,6.77-7.01v-2.31c-1.38,2.03-4.05,3.57-7.05,3.57-5.63,0-9.93-4.54-9.93-10.33s4.3-10.29,9.93-10.29c3,0,5.67,1.5,7.05,3.53v-3.12h3.49v19.09c0,6.77-4.46,10.05-10.25,10.05ZM687.19,851.47c4.05,0,6.85-3,6.85-7.09s-2.8-7.09-6.85-7.09-6.85,3.04-6.85,7.09,2.8,7.09,6.85,7.09Z"/>
|
||||
<path class="cls-6" d="M701.9,834.49h3.49v11.92c0,3.4,1.78,5.55,4.58,5.55,3.16,0,5.63-2.76,5.63-7.42v-10.05h3.49v20.26h-3.49v-3.12c-1.34,2.35-3.69,3.53-6.24,3.53-4.46,0-7.46-3.2-7.46-8.23v-12.44Z"/>
|
||||
<path class="cls-6" d="M732.38,834.09c3.04,0,5.8,1.58,7.13,3.53v-3.12h3.49v20.26h-3.49v-3.12c-1.34,1.95-4.09,3.53-7.13,3.53-5.67,0-9.93-4.74-9.93-10.54s4.25-10.54,9.93-10.54ZM732.9,837.29c-4.09,0-6.93,3.24-6.93,7.34s2.84,7.34,6.93,7.34,6.89-3.24,6.89-7.34-2.8-7.34-6.89-7.34Z"/>
|
||||
<path class="cls-6" d="M756.57,863.63c-4.38,0-7.78-1.86-9.52-4.13l2.43-2.43c1.62,2.07,3.97,3.4,7.09,3.4,3.45,0,6.77-2.15,6.77-7.01v-2.31c-1.38,2.03-4.05,3.57-7.05,3.57-5.63,0-9.93-4.54-9.93-10.33s4.3-10.29,9.93-10.29c3,0,5.67,1.5,7.05,3.53v-3.12h3.49v19.09c0,6.77-4.46,10.05-10.25,10.05ZM756.77,851.47c4.05,0,6.85-3,6.85-7.09s-2.8-7.09-6.85-7.09-6.85,3.04-6.85,7.09,2.8,7.09,6.85,7.09Z"/>
|
||||
<path class="cls-6" d="M780.72,834.09c5.31,0,9.6,3.69,9.65,10.17,0,.36,0,.61-.04,1.05h-16.62c.12,3.81,3,6.65,7.01,6.65,3.12,0,5.07-1.38,6.32-3.53l2.76,1.9c-1.82,2.96-4.9,4.82-9.12,4.82-6.28,0-10.5-4.58-10.5-10.54s4.38-10.54,10.54-10.54ZM773.95,842.48h12.85c-.49-3.32-3.12-5.31-6.2-5.31s-6,1.95-6.65,5.31Z"/>
|
||||
<path class="cls-6" d="M799.81,855.16c-3.61,0-6.73-1.54-7.94-4.7l2.84-1.58c.73,2.07,2.92,3.2,5.15,3.2,1.95,0,3.53-.97,3.53-2.72,0-1.46-.89-2.35-3.53-3.16l-2.03-.65c-3.32-.97-5.07-3.08-5.07-5.92.04-3.49,3.08-5.55,6.97-5.55,3,0,5.35,1.26,6.69,3.53l-2.59,1.78c-.89-1.3-2.27-2.31-4.21-2.31-1.7,0-3.36.93-3.36,2.43,0,1.22.65,2.23,2.88,2.92l2.23.69c3.49,1.05,5.51,2.88,5.51,5.96,0,3.89-3,6.08-7.05,6.08Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-6" d="M1102.25,813.51c-8.27,0-14.87-6.52-14.87-14.87s6.61-14.87,14.87-14.87,14.87,6.52,14.87,14.87-6.56,14.87-14.87,14.87ZM1102.25,809.94c6.32,0,11.1-4.98,11.1-11.31s-4.78-11.31-11.1-11.31-11.06,4.98-11.06,11.31,4.78,11.31,11.06,11.31Z"/>
|
||||
<path class="cls-6" d="M1120.61,792.76h3.49v3.12c1.38-2.39,3.81-3.53,6.44-3.53,4.54,0,7.66,3.16,7.66,8.19v12.48h-3.45v-11.96c0-3.4-1.95-5.51-4.78-5.51-3.28,0-5.88,2.67-5.88,7.42v10.05h-3.49v-20.26Z"/>
|
||||
<path class="cls-6" d="M1142.21,799.93h10.33v3.49h-10.33v-3.49Z"/>
|
||||
<path class="cls-6" d="M1157.4,784.25h8.27c8.83,0,15.32,6.16,15.32,14.39s-6.48,14.39-15.32,14.39h-8.27v-28.77ZM1165.67,809.5c6.93,0,11.51-4.62,11.51-10.86s-4.58-10.86-11.51-10.86h-4.5v21.72h4.5Z"/>
|
||||
<path class="cls-6" d="M1193.43,792.36c5.31,0,9.61,3.69,9.65,10.17,0,.36,0,.61-.04,1.05h-16.62c.12,3.81,3,6.65,7.01,6.65,3.12,0,5.07-1.38,6.32-3.53l2.76,1.9c-1.82,2.96-4.9,4.82-9.12,4.82-6.28,0-10.5-4.58-10.5-10.54s4.38-10.54,10.54-10.54ZM1186.66,800.74h12.85c-.49-3.32-3.12-5.31-6.2-5.31s-6,1.95-6.65,5.31Z"/>
|
||||
<path class="cls-6" d="M1203.03,792.76h3.89l6.93,15.32,6.93-15.32h3.89l-9.36,20.26h-2.92l-9.36-20.26Z"/>
|
||||
<path class="cls-6" d="M1228.36,787.33c-1.38,0-2.51-1.09-2.51-2.47s1.13-2.47,2.51-2.47,2.43,1.13,2.43,2.47-1.05,2.47-2.43,2.47ZM1226.57,792.76h3.49v20.26h-3.49v-20.26Z"/>
|
||||
<path class="cls-6" d="M1244.08,813.43c-6.12,0-10.62-4.66-10.62-10.54s4.5-10.54,10.62-10.54c4.17,0,7.58,2.19,9.36,5.43l-3.04,1.7c-1.13-2.31-3.4-3.89-6.32-3.89-4.21,0-7.09,3.24-7.09,7.29s2.88,7.29,7.09,7.29c2.92,0,5.19-1.58,6.32-3.89l3.04,1.7c-1.78,3.24-5.19,5.43-9.36,5.43Z"/>
|
||||
<path class="cls-6" d="M1265.27,792.36c5.31,0,9.6,3.69,9.65,10.17,0,.36,0,.61-.04,1.05h-16.62c.12,3.81,3,6.65,7.01,6.65,3.12,0,5.07-1.38,6.32-3.53l2.76,1.9c-1.82,2.96-4.9,4.82-9.12,4.82-6.28,0-10.5-4.58-10.5-10.54s4.38-10.54,10.54-10.54ZM1258.5,800.74h12.85c-.49-3.32-3.12-5.31-6.2-5.31s-6,1.95-6.65,5.31Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-6" d="M1097.68,909.11h-11.46l5.86-7.11h12.8v59.39h-7.19v-52.28Z"/>
|
||||
<path class="cls-6" d="M1135.41,962.23c-12.21,0-21-9.12-21-22.33v-16.56c0-13.13,8.78-22.33,21-22.33s21.08,9.2,21.08,22.33v16.56c0,13.22-8.87,22.33-21.08,22.33ZM1135.41,955.2c8.11,0,13.89-5.69,13.89-15.31v-16.56c0-9.62-5.77-15.31-13.89-15.31s-13.8,5.69-13.8,15.31v16.56c0,9.62,5.77,15.31,13.8,15.31Z"/>
|
||||
<path class="cls-6" d="M1184.17,962.23c-12.21,0-21-9.12-21-22.33v-16.56c0-13.13,8.78-22.33,21-22.33s21.08,9.2,21.08,22.33v16.56c0,13.22-8.87,22.33-21.08,22.33ZM1184.17,955.2c8.11,0,13.89-5.69,13.89-15.31v-16.56c0-9.62-5.77-15.31-13.89-15.31s-13.8,5.69-13.8,15.31v16.56c0,9.62,5.77,15.31,13.8,15.31Z"/>
|
||||
<path class="cls-6" d="M1226.08,934.29c-9.12,0-16.48-7.44-16.48-16.48s7.36-16.48,16.48-16.48,16.48,7.36,16.48,16.48-7.45,16.48-16.48,16.48ZM1226.08,927.76c5.44,0,9.87-4.6,9.87-9.95s-4.43-9.95-9.87-9.95-9.87,4.6-9.87,9.95,4.35,9.95,9.87,9.95ZM1263.3,902h7.78l-37.64,59.39h-7.78l37.64-59.39ZM1270.66,962.06c-9.12,0-16.48-7.44-16.48-16.48s7.36-16.48,16.48-16.48,16.48,7.36,16.48,16.48-7.44,16.48-16.48,16.48ZM1270.66,955.53c5.44,0,9.87-4.6,9.87-9.95s-4.43-9.95-9.87-9.95-9.87,4.6-9.87,9.95,4.35,9.95,9.87,9.95Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-6" d="M644.38,962.23c-11.63,0-19.99-7.86-19.99-18.65,0-7.11,3.51-12.63,9.45-15.56-3.85-2.59-6.19-6.78-6.19-11.71,0-8.87,7.19-15.31,16.73-15.31s16.65,6.44,16.65,15.31c0,4.94-2.26,9.12-6.02,11.71,5.86,2.93,9.29,8.45,9.29,15.56,0,10.79-8.11,18.65-19.91,18.65ZM644.38,955.37c7.95,0,12.63-5.27,12.63-11.79s-4.68-11.88-12.63-11.88-12.71,5.19-12.71,11.88,4.94,11.79,12.71,11.79ZM644.38,925.25c6.02,0,9.54-4.18,9.54-8.78,0-4.94-3.6-8.7-9.54-8.7s-9.62,3.76-9.62,8.7c0,4.6,3.76,8.78,9.62,8.78Z"/>
|
||||
<path class="cls-6" d="M668.05,933.95h14.47v-14.39h6.53v14.39h14.39v6.44h-14.39v14.39h-6.53v-14.39h-14.47v-6.44Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-6" d="M176.26,962.23c-11.54,0-20.16-8.78-20.16-19.57,0-6.27,3.01-10.79,6.69-15.14l21.67-25.51h9.12l-18.15,21.5c.84-.08,1.59-.17,2.34-.17,9.95,0,18.57,8.78,18.57,19.16,0,10.96-8.53,19.74-20.08,19.74ZM176.26,955.28c7.19,0,12.71-5.77,12.71-12.8s-5.52-12.8-12.71-12.8-12.8,5.77-12.8,12.8,5.52,12.8,12.8,12.8Z"/>
|
||||
<path class="cls-6" d="M219.68,962.23c-11.54,0-20.16-8.78-20.16-19.57,0-6.27,3.01-10.79,6.69-15.14l21.67-25.51h9.12l-18.15,21.5c.84-.08,1.59-.17,2.34-.17,9.95,0,18.57,8.78,18.57,19.16,0,10.96-8.53,19.74-20.08,19.74ZM219.68,955.28c7.19,0,12.71-5.77,12.71-12.8s-5.52-12.8-12.71-12.8-12.8,5.77-12.8,12.8,5.52,12.8,12.8,12.8Z"/>
|
||||
<path class="cls-6" d="M255.56,915.22v46.17h-7.78v-59.39h7.11l22.08,29.61,22.08-29.61h7.03v59.39h-7.7v-46.17l-21.41,28.78-21.41-28.78Z"/>
|
||||
</g>
|
||||
<line class="cls-2" x1="157.77" y1="878.71" x2="407.74" y2="878.71"/>
|
||||
<line class="cls-2" x1="621.9" y1="878.71" x2="871.87" y2="878.71"/>
|
||||
<line class="cls-2" x1="1086.03" y1="878.71" x2="1336" y2="878.71"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-6" d="M158.3,582.04h3.77v28.77h-3.77v-28.77Z"/>
|
||||
<path class="cls-6" d="M167.46,590.55h3.49v3.12c1.38-2.39,3.81-3.53,6.44-3.53,4.54,0,7.66,3.16,7.66,8.19v12.48h-3.45v-11.96c0-3.4-1.95-5.51-4.78-5.51-3.28,0-5.88,2.67-5.88,7.42v10.05h-3.49v-20.26Z"/>
|
||||
<path class="cls-6" d="M198.74,611.22c-6.12,0-10.62-4.66-10.62-10.54s4.5-10.54,10.62-10.54c4.17,0,7.58,2.19,9.36,5.43l-3.04,1.7c-1.13-2.31-3.4-3.89-6.32-3.89-4.21,0-7.09,3.24-7.09,7.29s2.88,7.29,7.09,7.29c2.92,0,5.19-1.58,6.32-3.89l3.04,1.7c-1.78,3.24-5.19,5.43-9.36,5.43Z"/>
|
||||
<path class="cls-6" d="M210.98,590.55h3.49v3.93c.81-2.59,3.32-4.13,5.67-4.13.53,0,1.01.04,1.58.16v3.61c-.65-.24-1.22-.32-1.9-.32-2.55,0-5.35,2.23-5.35,6.97v10.05h-3.49v-20.26Z"/>
|
||||
<path class="cls-6" d="M232.42,590.15c5.31,0,9.6,3.69,9.65,10.17,0,.36,0,.61-.04,1.05h-16.62c.12,3.81,3,6.65,7.01,6.65,3.12,0,5.07-1.38,6.32-3.53l2.76,1.9c-1.82,2.96-4.9,4.82-9.12,4.82-6.28,0-10.5-4.58-10.5-10.54s4.38-10.54,10.54-10.54ZM225.65,598.53h12.85c-.49-3.32-3.12-5.31-6.2-5.31s-6,1.95-6.65,5.31Z"/>
|
||||
<path class="cls-6" d="M253.73,590.15c3.04,0,5.79,1.58,7.13,3.53v-13.25h3.48v30.4h-3.48v-3.12c-1.34,1.95-4.09,3.53-7.13,3.53-5.67,0-9.93-4.74-9.93-10.54s4.26-10.54,9.93-10.54ZM254.26,593.35c-4.09,0-6.93,3.24-6.93,7.34s2.84,7.34,6.93,7.34,6.89-3.24,6.89-7.34-2.8-7.34-6.89-7.34Z"/>
|
||||
<path class="cls-6" d="M271.08,585.12c-1.38,0-2.51-1.09-2.51-2.47s1.13-2.47,2.51-2.47,2.43,1.13,2.43,2.47-1.05,2.47-2.43,2.47ZM269.29,590.55h3.49v20.26h-3.49v-20.26Z"/>
|
||||
<path class="cls-6" d="M281.25,610.81h-3.49v-30.4h3.49v13.25c1.34-1.95,4.09-3.53,7.13-3.53,5.63,0,9.93,4.74,9.93,10.54s-4.3,10.54-9.93,10.54c-3.04,0-5.8-1.58-7.13-3.53v3.12ZM287.85,593.35c-4.09,0-6.89,3.24-6.89,7.34s2.8,7.34,6.89,7.34,6.89-3.24,6.89-7.34-2.8-7.34-6.89-7.34Z"/>
|
||||
<path class="cls-6" d="M301.67,580.42h3.49v30.4h-3.49v-30.4Z"/>
|
||||
<path class="cls-6" d="M311.64,619.28l4.42-9.52-8.88-19.21h3.85l6.97,15.36,6.93-15.36h3.89l-13.29,28.73h-3.89Z"/>
|
||||
<path class="cls-6" d="M337.94,580.42h3.48v30.4h-3.48v-30.4Z"/>
|
||||
<path class="cls-6" d="M348.19,585.12c-1.38,0-2.51-1.09-2.51-2.47s1.13-2.47,2.51-2.47,2.43,1.13,2.43,2.47-1.05,2.47-2.43,2.47ZM346.41,590.55h3.48v20.26h-3.48v-20.26Z"/>
|
||||
<path class="cls-6" d="M363.51,619.69c-4.38,0-7.78-1.86-9.52-4.13l2.43-2.43c1.62,2.07,3.97,3.4,7.09,3.4,3.45,0,6.77-2.15,6.77-7.01v-2.31c-1.38,2.03-4.05,3.57-7.05,3.57-5.63,0-9.93-4.54-9.93-10.33s4.3-10.29,9.93-10.29c3,0,5.67,1.5,7.05,3.53v-3.12h3.49v19.09c0,6.77-4.46,10.05-10.25,10.05ZM363.71,607.53c4.05,0,6.85-3,6.85-7.09s-2.8-7.09-6.85-7.09-6.85,3.04-6.85,7.09,2.8,7.09,6.85,7.09Z"/>
|
||||
<path class="cls-6" d="M378.71,580.42h3.48v13.25c1.38-2.39,3.81-3.53,6.44-3.53,4.54,0,7.66,3.16,7.66,8.19v12.48h-3.44v-11.96c0-3.4-1.95-5.51-4.78-5.51-3.28,0-5.88,2.67-5.88,7.42v10.05h-3.48v-30.4Z"/>
|
||||
<path class="cls-6" d="M408.57,611.02c-3.53,0-6.2-1.99-6.2-6.2v-11.06h-4.3v-3.2h4.3v-5.63h3.49v5.63h5.84v3.2h-5.84v10.62c0,2.59,1.18,3.44,3.36,3.44.85,0,1.82-.16,2.47-.41v3.12c-.85.28-1.86.49-3.12.49Z"/>
|
||||
<path class="cls-6" d="M426.73,596.22l-5.03,14.59h-3.08l-6.89-20.26h3.65l4.9,14.79,5.07-14.79h2.67l5.07,14.79,4.9-14.79h3.69l-6.89,20.26h-3.08l-4.99-14.59Z"/>
|
||||
<path class="cls-6" d="M452.7,590.15c5.31,0,9.6,3.69,9.65,10.17,0,.36,0,.61-.04,1.05h-16.62c.12,3.81,3,6.65,7.01,6.65,3.12,0,5.07-1.38,6.32-3.53l2.76,1.9c-1.82,2.96-4.9,4.82-9.12,4.82-6.28,0-10.5-4.58-10.5-10.54s4.38-10.54,10.54-10.54ZM445.93,598.53h12.85c-.49-3.32-3.12-5.31-6.2-5.31s-6,1.95-6.65,5.31Z"/>
|
||||
<path class="cls-6" d="M467.45,585.12c-1.38,0-2.51-1.09-2.51-2.47s1.13-2.47,2.51-2.47,2.43,1.13,2.43,2.47-1.05,2.47-2.43,2.47ZM465.67,590.55h3.49v20.26h-3.49v-20.26Z"/>
|
||||
<path class="cls-6" d="M482.77,619.69c-4.38,0-7.78-1.86-9.52-4.13l2.43-2.43c1.62,2.07,3.97,3.4,7.09,3.4,3.45,0,6.77-2.15,6.77-7.01v-2.31c-1.38,2.03-4.05,3.57-7.05,3.57-5.63,0-9.93-4.54-9.93-10.33s4.3-10.29,9.93-10.29c3,0,5.67,1.5,7.05,3.53v-3.12h3.49v19.09c0,6.77-4.46,10.05-10.25,10.05ZM482.97,607.53c4.05,0,6.85-3,6.85-7.09s-2.8-7.09-6.85-7.09-6.85,3.04-6.85,7.09,2.8,7.09,6.85,7.09Z"/>
|
||||
<path class="cls-6" d="M497.96,580.42h3.48v13.25c1.38-2.39,3.81-3.53,6.44-3.53,4.54,0,7.66,3.16,7.66,8.19v12.48h-3.44v-11.96c0-3.4-1.95-5.51-4.78-5.51-3.28,0-5.88,2.67-5.88,7.42v10.05h-3.48v-30.4Z"/>
|
||||
<path class="cls-6" d="M527.83,611.02c-3.53,0-6.2-1.99-6.2-6.2v-11.06h-4.3v-3.2h4.3v-5.63h3.49v5.63h5.84v3.2h-5.84v10.62c0,2.59,1.18,3.44,3.36,3.44.85,0,1.82-.16,2.47-.41v3.12c-.85.28-1.86.49-3.12.49Z"/>
|
||||
<path class="cls-6" d="M550.28,590.15c3.04,0,5.79,1.58,7.13,3.53v-3.12h3.48v20.26h-3.48v-3.12c-1.34,1.95-4.09,3.53-7.13,3.53-5.67,0-9.93-4.74-9.93-10.54s4.26-10.54,9.93-10.54ZM550.81,593.35c-4.09,0-6.93,3.24-6.93,7.34s2.84,7.34,6.93,7.34,6.89-3.24,6.89-7.34-2.8-7.34-6.89-7.34Z"/>
|
||||
<path class="cls-6" d="M565.84,590.55h3.49v3.12c1.38-2.39,3.81-3.53,6.44-3.53,4.54,0,7.66,3.16,7.66,8.19v12.48h-3.45v-11.96c0-3.4-1.95-5.51-4.78-5.51-3.28,0-5.88,2.67-5.88,7.42v10.05h-3.49v-20.26Z"/>
|
||||
<path class="cls-6" d="M596.43,590.15c3.04,0,5.8,1.58,7.13,3.53v-13.25h3.49v30.4h-3.49v-3.12c-1.34,1.95-4.09,3.53-7.13,3.53-5.67,0-9.93-4.74-9.93-10.54s4.25-10.54,9.93-10.54ZM596.96,593.35c-4.09,0-6.93,3.24-6.93,7.34s2.84,7.34,6.93,7.34,6.89-3.24,6.89-7.34-2.8-7.34-6.89-7.34Z"/>
|
||||
<path class="cls-6" d="M623.62,610.81h-3.48v-30.4h3.48v13.25c1.34-1.95,4.09-3.53,7.13-3.53,5.63,0,9.93,4.74,9.93,10.54s-4.3,10.54-9.93,10.54c-3.04,0-5.79-1.58-7.13-3.53v3.12ZM630.23,593.35c-4.09,0-6.89,3.24-6.89,7.34s2.8,7.34,6.89,7.34,6.89-3.24,6.89-7.34-2.8-7.34-6.89-7.34Z"/>
|
||||
<path class="cls-6" d="M644.05,580.42h3.49v30.4h-3.49v-30.4Z"/>
|
||||
<path class="cls-6" d="M660.87,590.15c3.04,0,5.79,1.58,7.13,3.53v-3.12h3.48v20.26h-3.48v-3.12c-1.34,1.95-4.09,3.53-7.13,3.53-5.67,0-9.93-4.74-9.93-10.54s4.26-10.54,9.93-10.54ZM661.39,593.35c-4.09,0-6.93,3.24-6.93,7.34s2.84,7.34,6.93,7.34,6.89-3.24,6.89-7.34-2.8-7.34-6.89-7.34Z"/>
|
||||
<path class="cls-6" d="M674.76,608.06l11.63-14.31h-11.47v-3.2h15.93v2.8l-11.55,14.27h11.92v3.2h-16.45v-2.76Z"/>
|
||||
<path class="cls-6" d="M696.2,585.12c-1.38,0-2.51-1.09-2.51-2.47s1.13-2.47,2.51-2.47,2.43,1.13,2.43,2.47-1.05,2.47-2.43,2.47ZM694.42,590.55h3.49v20.26h-3.49v-20.26Z"/>
|
||||
<path class="cls-6" d="M702.89,590.55h3.49v3.12c1.38-2.39,3.81-3.53,6.44-3.53,4.54,0,7.66,3.16,7.66,8.19v12.48h-3.45v-11.96c0-3.4-1.95-5.51-4.78-5.51-3.28,0-5.88,2.67-5.88,7.42v10.05h-3.49v-20.26Z"/>
|
||||
<path class="cls-6" d="M733.76,619.69c-4.38,0-7.78-1.86-9.52-4.13l2.43-2.43c1.62,2.07,3.97,3.4,7.09,3.4,3.44,0,6.77-2.15,6.77-7.01v-2.31c-1.38,2.03-4.05,3.57-7.05,3.57-5.63,0-9.93-4.54-9.93-10.33s4.3-10.29,9.93-10.29c3,0,5.67,1.5,7.05,3.53v-3.12h3.48v19.09c0,6.77-4.46,10.05-10.25,10.05ZM733.97,607.53c4.05,0,6.85-3,6.85-7.09s-2.8-7.09-6.85-7.09-6.85,3.04-6.85,7.09,2.8,7.09,6.85,7.09Z"/>
|
||||
<path class="cls-6" d="M748.96,580.42h3.49v30.4h-3.49v-30.4Z"/>
|
||||
<path class="cls-6" d="M758.93,619.28l4.42-9.52-8.88-19.21h3.85l6.97,15.36,6.93-15.36h3.89l-13.29,28.73h-3.89Z"/>
|
||||
<path class="cls-6" d="M786.4,593.75h-4.3v-3.2h4.3v-4.09c0-4.17,2.67-6.28,6.2-6.28,1.22,0,2.27.2,3.16.53v3.08c-.65-.24-1.7-.41-2.51-.41-2.23,0-3.36.89-3.36,3.44v3.73h5.88v3.2h-5.88v17.06h-3.48v-17.06Z"/>
|
||||
<path class="cls-6" d="M805.78,590.15c3.04,0,5.79,1.58,7.13,3.53v-3.12h3.48v20.26h-3.48v-3.12c-1.34,1.95-4.09,3.53-7.13,3.53-5.67,0-9.93-4.74-9.93-10.54s4.26-10.54,9.93-10.54ZM806.3,593.35c-4.09,0-6.93,3.24-6.93,7.34s2.84,7.34,6.93,7.34,6.89-3.24,6.89-7.34-2.8-7.34-6.89-7.34Z"/>
|
||||
<path class="cls-6" d="M827.45,611.22c-3.61,0-6.73-1.54-7.94-4.7l2.84-1.58c.73,2.07,2.92,3.2,5.15,3.2,1.95,0,3.53-.97,3.53-2.72,0-1.46-.89-2.35-3.53-3.16l-2.03-.65c-3.32-.97-5.07-3.08-5.07-5.92.04-3.49,3.08-5.55,6.97-5.55,3,0,5.35,1.26,6.69,3.53l-2.59,1.78c-.89-1.3-2.27-2.31-4.21-2.31-1.7,0-3.36.93-3.36,2.43,0,1.22.65,2.23,2.88,2.92l2.23.69c3.49,1.05,5.51,2.88,5.51,5.96,0,3.89-3,6.08-7.05,6.08Z"/>
|
||||
<path class="cls-6" d="M845.61,611.02c-3.53,0-6.2-1.99-6.2-6.2v-11.06h-4.3v-3.2h4.3v-5.63h3.49v5.63h5.84v3.2h-5.84v10.62c0,2.59,1.17,3.44,3.36,3.44.85,0,1.82-.16,2.47-.41v3.12c-.85.28-1.86.49-3.12.49Z"/>
|
||||
<path class="cls-6" d="M851.73,616.61h-2.31l2.72-5.8c-.97-.49-1.66-1.5-1.66-2.72,0-1.62,1.34-3.04,3-3.04s3,1.42,3,3.04c0,.69-.24,1.3-.57,1.82l-4.17,6.69Z"/>
|
||||
<path class="cls-6" d="M157.78,639.18h3.48v3.93c.81-2.59,3.32-4.13,5.67-4.13.53,0,1.01.04,1.58.16v3.61c-.65-.24-1.22-.32-1.91-.32-2.55,0-5.35,2.23-5.35,6.97v10.05h-3.48v-20.26Z"/>
|
||||
<path class="cls-6" d="M170.7,639.18h3.49v11.92c0,3.4,1.78,5.55,4.58,5.55,3.16,0,5.63-2.76,5.63-7.42v-10.05h3.49v20.26h-3.49v-3.12c-1.34,2.35-3.69,3.53-6.24,3.53-4.46,0-7.46-3.2-7.46-8.23v-12.44Z"/>
|
||||
<path class="cls-6" d="M192.83,639.18h3.48v3.12c1.38-2.39,3.81-3.53,6.44-3.53,4.54,0,7.66,3.16,7.66,8.19v12.48h-3.44v-11.96c0-3.4-1.95-5.51-4.78-5.51-3.28,0-5.88,2.67-5.88,7.42v10.05h-3.48v-20.26Z"/>
|
||||
<path class="cls-6" d="M215.07,639.18h3.48v3.12c1.38-2.39,3.81-3.53,6.44-3.53,4.54,0,7.66,3.16,7.66,8.19v12.48h-3.44v-11.96c0-3.4-1.95-5.51-4.78-5.51-3.28,0-5.88,2.67-5.88,7.42v10.05h-3.48v-20.26Z"/>
|
||||
<path class="cls-6" d="M239.1,633.75c-1.38,0-2.51-1.09-2.51-2.47s1.13-2.47,2.51-2.47,2.43,1.13,2.43,2.47-1.05,2.47-2.43,2.47ZM237.32,639.18h3.49v20.26h-3.49v-20.26Z"/>
|
||||
<path class="cls-6" d="M245.79,639.18h3.48v3.12c1.38-2.39,3.81-3.53,6.44-3.53,4.54,0,7.66,3.16,7.66,8.19v12.48h-3.44v-11.96c0-3.4-1.95-5.51-4.78-5.51-3.28,0-5.88,2.67-5.88,7.42v10.05h-3.48v-20.26Z"/>
|
||||
<path class="cls-6" d="M276.67,668.32c-4.38,0-7.78-1.86-9.52-4.13l2.43-2.43c1.62,2.07,3.97,3.4,7.09,3.4,3.44,0,6.77-2.15,6.77-7.01v-2.31c-1.38,2.03-4.05,3.57-7.05,3.57-5.63,0-9.93-4.54-9.93-10.33s4.3-10.29,9.93-10.29c3,0,5.67,1.5,7.05,3.53v-3.12h3.48v19.09c0,6.77-4.46,10.05-10.25,10.05ZM276.87,656.16c4.05,0,6.85-3,6.85-7.09s-2.8-7.09-6.85-7.09-6.85,3.04-6.85,7.09,2.8,7.09,6.85,7.09Z"/>
|
||||
<path class="cls-6" d="M300.01,639.18h3.48v3.12c1.38-2.39,3.81-3.53,6.44-3.53,4.54,0,7.66,3.16,7.66,8.19v12.48h-3.44v-11.96c0-3.4-1.95-5.51-4.78-5.51-3.28,0-5.88,2.67-5.88,7.42v10.05h-3.48v-20.26Z"/>
|
||||
<path class="cls-6" d="M330.61,638.77c3.04,0,5.79,1.58,7.13,3.53v-3.12h3.48v20.26h-3.48v-3.12c-1.34,1.95-4.09,3.53-7.13,3.53-5.67,0-9.93-4.74-9.93-10.54s4.26-10.54,9.93-10.54ZM331.13,641.98c-4.09,0-6.93,3.24-6.93,7.34s2.84,7.34,6.93,7.34,6.89-3.24,6.89-7.34-2.8-7.34-6.89-7.34Z"/>
|
||||
<path class="cls-6" d="M354.03,659.65c-3.53,0-6.2-1.99-6.2-6.2v-11.06h-4.3v-3.2h4.3v-5.63h3.49v5.63h5.84v3.2h-5.84v10.62c0,2.59,1.17,3.44,3.36,3.44.85,0,1.82-.16,2.47-.41v3.12c-.85.28-1.86.49-3.12.49Z"/>
|
||||
<path class="cls-6" d="M361.77,633.75c-1.38,0-2.51-1.09-2.51-2.47s1.13-2.47,2.51-2.47,2.43,1.13,2.43,2.47-1.05,2.47-2.43,2.47ZM359.98,639.18h3.49v20.26h-3.49v-20.26Z"/>
|
||||
<path class="cls-6" d="M365.41,639.18h3.89l6.93,15.32,6.93-15.32h3.89l-9.36,20.26h-2.92l-9.36-20.26Z"/>
|
||||
<path class="cls-6" d="M397.47,638.77c5.31,0,9.6,3.69,9.65,10.17,0,.36,0,.61-.04,1.05h-16.62c.12,3.81,3,6.65,7.01,6.65,3.12,0,5.07-1.38,6.32-3.53l2.76,1.9c-1.82,2.96-4.9,4.82-9.12,4.82-6.28,0-10.5-4.58-10.5-10.54s4.38-10.54,10.54-10.54ZM390.7,647.16h12.85c-.49-3.32-3.12-5.31-6.2-5.31s-6,1.95-6.65,5.31Z"/>
|
||||
<path class="cls-6" d="M410.43,629.05h3.49v30.4h-3.49v-30.4Z"/>
|
||||
<path class="cls-6" d="M420.4,667.91l4.42-9.52-8.88-19.21h3.85l6.97,15.36,6.93-15.36h3.89l-13.29,28.73h-3.89Z"/>
|
||||
<path class="cls-6" d="M448.49,633.75c-1.38,0-2.51-1.09-2.51-2.47s1.13-2.47,2.51-2.47,2.43,1.13,2.43,2.47-1.05,2.47-2.43,2.47ZM446.7,639.18h3.48v20.26h-3.48v-20.26Z"/>
|
||||
<path class="cls-6" d="M455.17,639.18h3.48v3.12c1.38-2.39,3.81-3.53,6.44-3.53,4.54,0,7.66,3.16,7.66,8.19v12.48h-3.44v-11.96c0-3.4-1.95-5.51-4.78-5.51-3.28,0-5.88,2.67-5.88,7.42v10.05h-3.48v-20.26Z"/>
|
||||
<path class="cls-6" d="M486.13,667.91l4.42-9.52-8.88-19.21h3.85l6.97,15.36,6.93-15.36h3.89l-13.29,28.73h-3.89Z"/>
|
||||
<path class="cls-6" d="M513.73,659.85c-6.12,0-10.62-4.7-10.62-10.54s4.5-10.54,10.62-10.54,10.58,4.7,10.58,10.54-4.5,10.54-10.58,10.54ZM513.73,656.61c4.21,0,7.01-3.24,7.01-7.29s-2.8-7.29-7.01-7.29-7.05,3.24-7.05,7.29,2.84,7.29,7.05,7.29Z"/>
|
||||
<path class="cls-6" d="M527.38,639.18h3.49v11.92c0,3.4,1.78,5.55,4.58,5.55,3.16,0,5.63-2.76,5.63-7.42v-10.05h3.49v20.26h-3.49v-3.12c-1.34,2.35-3.69,3.53-6.24,3.53-4.46,0-7.46-3.2-7.46-8.23v-12.44Z"/>
|
||||
<path class="cls-6" d="M549.51,639.18h3.48v3.93c.81-2.59,3.32-4.13,5.67-4.13.53,0,1.01.04,1.58.16v3.61c-.65-.24-1.22-.32-1.91-.32-2.55,0-5.35,2.23-5.35,6.97v10.05h-3.48v-20.26Z"/>
|
||||
<path class="cls-6" d="M578.81,638.77c5.31,0,9.61,3.69,9.65,10.17,0,.36,0,.61-.04,1.05h-16.62c.12,3.81,3,6.65,7.01,6.65,3.12,0,5.07-1.38,6.32-3.53l2.76,1.9c-1.82,2.96-4.9,4.82-9.12,4.82-6.28,0-10.5-4.58-10.5-10.54s4.38-10.54,10.54-10.54ZM572.04,647.16h12.85c-.49-3.32-3.12-5.31-6.2-5.31s-6,1.95-6.65,5.31Z"/>
|
||||
<path class="cls-6" d="M591.78,639.18h3.48v3.12c1.38-2.39,3.81-3.53,6.44-3.53,4.54,0,7.66,3.16,7.66,8.19v12.48h-3.44v-11.96c0-3.4-1.95-5.51-4.78-5.51-3.28,0-5.88,2.67-5.88,7.42v10.05h-3.48v-20.26Z"/>
|
||||
<path class="cls-6" d="M610.54,639.18h3.89l6.93,15.32,6.93-15.32h3.89l-9.36,20.26h-2.92l-9.36-20.26Z"/>
|
||||
<path class="cls-6" d="M635.86,633.75c-1.38,0-2.51-1.09-2.51-2.47s1.13-2.47,2.51-2.47,2.43,1.13,2.43,2.47-1.05,2.47-2.43,2.47ZM634.08,639.18h3.49v20.26h-3.49v-20.26Z"/>
|
||||
<path class="cls-6" d="M642.55,639.18h3.49v3.93c.81-2.59,3.32-4.13,5.67-4.13.53,0,1.01.04,1.58.16v3.61c-.65-.24-1.22-.32-1.9-.32-2.55,0-5.35,2.23-5.35,6.97v10.05h-3.49v-20.26Z"/>
|
||||
<path class="cls-6" d="M664.03,659.85c-6.12,0-10.62-4.7-10.62-10.54s4.5-10.54,10.62-10.54,10.58,4.7,10.58,10.54-4.5,10.54-10.58,10.54ZM664.03,656.61c4.21,0,7.01-3.24,7.01-7.29s-2.8-7.29-7.01-7.29-7.05,3.24-7.05,7.29,2.84,7.29,7.05,7.29Z"/>
|
||||
<path class="cls-6" d="M677.97,639.18h3.49v3.12c1.38-2.39,3.81-3.53,6.44-3.53,4.54,0,7.66,3.16,7.66,8.19v12.48h-3.45v-11.96c0-3.4-1.95-5.51-4.78-5.51-3.28,0-5.88,2.67-5.88,7.42v10.05h-3.49v-20.26Z"/>
|
||||
<path class="cls-6" d="M700.21,639.18h3.49v3.12c1.17-2.15,3.4-3.53,6.12-3.53,3.04,0,5.23,1.66,6.24,4.34,1.09-2.67,3.65-4.34,6.65-4.34,4.42,0,7.09,3.28,7.09,8.23v12.44h-3.49v-11.92c0-3.28-1.38-5.55-4.01-5.55-3.28,0-5.55,2.84-5.55,7.42v10.05h-3.48v-11.92c0-3.28-1.34-5.55-3.97-5.55-3.28,0-5.59,2.84-5.59,7.42v10.05h-3.49v-20.26Z"/>
|
||||
<path class="cls-6" d="M743.41,638.77c5.31,0,9.61,3.69,9.65,10.17,0,.36,0,.61-.04,1.05h-16.62c.12,3.81,3,6.65,7.01,6.65,3.12,0,5.07-1.38,6.32-3.53l2.76,1.9c-1.82,2.96-4.9,4.82-9.12,4.82-6.28,0-10.5-4.58-10.5-10.54s4.38-10.54,10.54-10.54ZM736.64,647.16h12.85c-.49-3.32-3.12-5.31-6.2-5.31s-6,1.95-6.65,5.31Z"/>
|
||||
<path class="cls-6" d="M756.38,639.18h3.48v3.12c1.38-2.39,3.81-3.53,6.44-3.53,4.54,0,7.66,3.16,7.66,8.19v12.48h-3.44v-11.96c0-3.4-1.95-5.51-4.78-5.51-3.28,0-5.88,2.67-5.88,7.42v10.05h-3.48v-20.26Z"/>
|
||||
<path class="cls-6" d="M786.24,659.65c-3.53,0-6.2-1.99-6.2-6.2v-11.06h-4.3v-3.2h4.3v-5.63h3.49v5.63h5.84v3.2h-5.84v10.62c0,2.59,1.18,3.44,3.36,3.44.85,0,1.82-.16,2.47-.41v3.12c-.85.28-1.86.49-3.12.49Z"/>
|
||||
<path class="cls-6" d="M796.42,639.18h3.89l6.93,15.32,6.93-15.32h3.89l-9.36,20.26h-2.92l-9.36-20.26Z"/>
|
||||
<path class="cls-6" d="M821.74,633.75c-1.38,0-2.51-1.09-2.51-2.47s1.13-2.47,2.51-2.47,2.43,1.13,2.43,2.47-1.05,2.47-2.43,2.47ZM819.96,639.18h3.49v20.26h-3.49v-20.26Z"/>
|
||||
<path class="cls-6" d="M836.78,638.77c3.04,0,5.79,1.58,7.13,3.53v-3.12h3.48v20.26h-3.48v-3.12c-1.34,1.95-4.09,3.53-7.13,3.53-5.67,0-9.93-4.74-9.93-10.54s4.26-10.54,9.93-10.54ZM837.3,641.98c-4.09,0-6.93,3.24-6.93,7.34s2.84,7.34,6.93,7.34,6.89-3.24,6.89-7.34-2.8-7.34-6.89-7.34Z"/>
|
||||
<path class="cls-6" d="M873.9,659.93c-8.27,0-14.87-6.52-14.87-14.87s6.61-14.87,14.87-14.87,14.87,6.52,14.87,14.87-6.56,14.87-14.87,14.87ZM873.9,656.36c6.32,0,11.1-4.98,11.1-11.31s-4.78-11.31-11.1-11.31-11.06,4.98-11.06,11.31,4.78,11.31,11.06,11.31Z"/>
|
||||
<path class="cls-6" d="M914.1,659.44l-17.51-22.41v22.41h-3.77v-28.77h3.28l17.51,22.41v-22.41h3.73v28.77h-3.24Z"/>
|
||||
<path class="cls-6" d="M944.65,659.44l-17.51-22.41v22.41h-3.77v-28.77h3.28l17.51,22.41v-22.41h3.73v28.77h-3.24Z"/>
|
||||
<path class="cls-6" d="M963.7,647.89l-8.79,11.55h-4.62l11.1-14.55-10.66-14.23h4.5l8.47,11.23,8.47-11.23h4.5l-10.62,14.23,11.06,14.55h-4.58l-8.83-11.55Z"/>
|
||||
<path class="cls-6" d="M980.75,659.77c-1.66,0-3-1.38-3-3.04s1.34-3.04,3-3.04,3,1.42,3,3.04-1.34,3.04-3,3.04Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-3" d="M312.56,274.03h-18c0-5.18-4.04-10.02-9.4-10.07l-104.25.02c-11.33,2.12-11.32,17.43-.38,20.06l102.54.02c38.59,2.04,38.78,52.82,2.1,56.56-33.79-1.6-69.45,2.06-103.02,0-11.72-.72-21.95-7.64-26.04-18.75-1.18-3.2-1.28-6.01-1.77-9.32h18c.28,4.87,3.95,9.62,8.98,10.07l104.25-.02c12.15-2.24,11.75-18.22-.05-20.47l-105.04-.03c-34.49-3.76-34.33-52.52,0-56.14l107.07.13c13.99,1.94,24.86,13.7,25.02,27.94Z"/>
|
||||
<polygon class="cls-3" points="845.46 245.97 845.46 263.98 708.99 263.98 708.99 284.08 807.58 284.08 808.2 284.71 808.2 302.08 708.99 302.08 708.99 322.6 845.46 322.6 845.46 340.61 690.15 340.61 690.15 245.97 845.46 245.97"/>
|
||||
<path class="cls-3" d="M979.42,302.08l40.19,38.52h-24.07l-41.02-38.52h-66.35v38.52h-18.42v-94.63h127.89c2.7,0,8.65,2.55,11.1,3.97,18.71,10.84,17.89,38.72-.98,48.86-1.77.95-7.92,3.28-9.7,3.28h-18.63ZM888.16,284.08h108.63c.19,0,2.17-.96,2.59-1.18,5.89-3.09,6.76-10.78,2.58-15.72-.77-.9-3.71-3.2-4.75-3.2h-109.05v20.1Z"/>
|
||||
<path class="cls-3" d="M1223.38,246.09l104.97-.13c13.07,1.48,23.94,11.35,25.72,24.52-2.05,25.65,10.21,65.79-26.58,70.12l-101.32.02c-16.42-1.48-26.57-12.82-27.42-29.1-.57-10.96-.6-25.52,0-36.47.83-15.17,9.22-26.52,24.62-28.97ZM1225.46,264.08c-3.86,1.07-7.28,3.93-7.85,8.06,1.21,13.06-1.63,29.14.04,41.84.54,4.08,4.71,8.46,8.95,8.64l100.07-.02c4.88-.89,8.58-4.39,9.01-9.41,1.12-12.93-.84-27.49-.05-40.6-.75-4.36-4.01-8.14-8.53-8.64l-101.64.12Z"/>
|
||||
<path class="cls-3" d="M537.36,302.08v38.52h-18.42v-94.63h127.89c2.29,0,8.64,2.6,10.8,3.85,19.96,11.5,17.66,41.29-3.33,50.11-1.6.67-5.94,2.15-7.48,2.15h-109.47ZM537.36,284.08h108.21c2.55,0,6.68-3.98,7.46-6.35,1.45-4.38-.08-9.62-3.93-12.25-.44-.3-2.83-1.49-3.11-1.49h-108.63v20.1Z"/>
|
||||
<path class="cls-3" d="M1763.91,245.97v18h-128.72c-.58,0-3.75,1.77-4.41,2.29-2.94,2.33-3.53,5.62-3.78,9.2-.69,9.89-.63,24.87,0,34.79.15,2.35.5,5.46,1.76,7.45.92,1.44,4.77,4.88,6.42,4.88h128.72v18h-129.14c-14.54,0-25.37-14.67-26.18-28.25-.67-11.18-.67-26.96,0-38.14.74-12.49,10.69-28.25,24.51-28.25h130.82Z"/>
|
||||
<polygon class="cls-3" points="1517.76 323.86 1517.76 245.97 1536.6 245.97 1536.6 340.61 1510.02 340.61 1399.71 262.72 1399.71 340.61 1381.29 340.61 1381.29 245.97 1409.55 245.97 1517.76 323.86"/>
|
||||
<path class="cls-3" d="M355.26,245.97v69.3c0,2.61,5.06,7.86,8.15,7.34l101.74-.02c2.83.06,8.16-4.19,8.16-6.91v-69.72h18.42v71.39c0,11-15.16,23.94-26.14,23.26-33.52-1.59-68.88,2.04-102.18,0-10.81-.66-20.71-6.87-24.79-17.08-.41-1.02-1.77-4.96-1.77-5.76v-71.81h18.42Z"/>
|
||||
<polygon class="cls-3" points="1188.31 245.97 1188.31 263.35 1187.68 263.98 1118.4 263.98 1118.4 340.61 1099.98 340.61 1099.98 263.98 1030.49 263.98 1030.49 245.97 1188.31 245.97"/>
|
||||
<rect class="cls-3" x="1563.39" y="245.97" width="18.42" height="94.63"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-6" d="M156.1,179.65h54.1v-54.1h-54.1v54.1ZM183.15,129.76c5.25,0,10.08,1.79,13.94,4.77l-13.03,13.03,4.13,4.13,13.03-13.03c2.98,3.86,4.77,8.68,4.77,13.94,0,12.62-10.23,22.84-22.84,22.84s-22.84-10.23-22.84-22.84,10.23-22.84,22.84-22.84Z"/>
|
||||
<path class="cls-6" d="M279.9,132.95c0,8.93,0,17.85,0,26.78,0,3-1.02,5.56-3.39,7.47-2,1.62-4.32,2.26-6.87,2.03-3.3-.31-5.79-1.89-7.41-4.8-.85-1.52-1.09-3.18-1.09-4.9,0-8.82,0-17.63,0-26.45v-.79h-4.64v.59c0,8.98,0,17.96,0,26.94,0,.86.03,1.74.18,2.58.55,3.28,2.09,6.04,4.68,8.15,3.29,2.68,7.09,3.66,11.28,3.08,3.43-.48,6.33-2.02,8.62-4.62,2.3-2.61,3.28-5.7,3.28-9.14v-27.6h-4.65v.67Z"/>
|
||||
<path class="cls-6" d="M244.82,151.28c-1.95-1-4.04-1.33-6.2-1.37-1.41-.03-2.71-.42-3.92-1.17-2.75-1.7-3.99-5.22-2.86-8.25,1.07-2.87,3.65-4.63,6.95-4.71,3.71-.09,6.89,2.1,7.55,5.82.07.39.09.8.13,1.21h4.6c.03-3.38-1.3-6.12-3.67-8.34-2.21-2.06-4.9-3.07-7.93-3.15-4.59-.11-8.33,1.55-10.88,5.43-1.96,2.98-2.26,6.25-1.21,9.63.68,2.16,1.97,3.92,3.77,5.32,2.02,1.57,4.29,2.47,6.85,2.64,1.48.1,2.98.1,4.38.73,4.68,2.1,5.63,7.4,3.21,10.99-1.52,2.24-3.77,3.2-6.42,3.32-3.59.16-6.69-1.91-7.78-5.25-.23-.71-.29-1.47-.43-2.2h-4.55c-.13,4.12,2.51,8.5,6.32,10.4,3.97,1.98,8,2.08,12.03.2,4.76-2.22,7.47-7.32,6.61-12.5-.67-4.02-2.94-6.89-6.54-8.75Z"/>
|
||||
<path class="cls-6" d="M310.07,135.16c-2.37-2.08-5.22-2.88-8.31-2.9-2.36-.01-4.72,0-7.09,0h0s-4.64,0-4.64,0v40.59h.03v.02h4.6v-.02h0v-18.01h.69c2.1,0,4.2,0,6.3,0,1.61,0,3.2-.18,4.72-.76,3.81-1.47,6.44-4.06,7.31-8.12.91-4.26-.33-7.89-3.62-10.78ZM302.79,150.34c-2.59.1-5.2.05-7.79.06-.12,0-.2-.04-.29-.04v-13.68h.18c2.8,0,5.61-.09,8.4.12,3.27.24,6.14,3.08,6.07,6.88-.06,3.69-2.97,6.52-6.56,6.66Z"/>
|
||||
<path class="cls-6" d="M361.78,154.05c3.82-1.47,6.44-4.06,7.31-8.12.91-4.26-.33-7.89-3.62-10.78-2.37-2.08-5.22-2.88-8.31-2.9-3.7-.02-7.41,0-11.11,0h-.62c0,13.56.03,27.08.03,40.6h4.6v-18.03h.69c1.09,0,2.17,0,3.26,0l12.03,18.06h5.39l-12.13-18.21c.84-.11,1.67-.3,2.48-.61ZM350.4,150.4c-.12,0-.2-.04-.29-.04v-13.68h.18c2.8,0,5.61-.09,8.4.12,3.27.24,6.13,3.08,6.07,6.88-.06,3.69-2.97,6.52-6.56,6.66-2.6.1-5.2.05-7.79.06Z"/>
|
||||
<polygon class="cls-6" points="469.03 165.24 447.05 132.34 442.53 132.34 442.53 172.84 447.05 172.84 447.05 139.95 469.03 172.84 473.55 172.84 473.55 132.34 469.03 132.34 469.03 165.24"/>
|
||||
<path class="cls-6" d="M318.45,132.28h0v40.55h0c7.18,0,14.32,0,21.45,0h0s.05,0,.05,0v-4.44h-.05c-5.63,0-11.22,0-16.8,0v-14.17h16.2v-4.45h-16.2v-13.05h16.79s.05,0,.05,0v-4.44h-21.5Z"/>
|
||||
<path class="cls-6" d="M397.79,136.72v-4.43h0s-25.96-.01-25.96-.01h0v4.47h10.65v36.08h4.65v-36.1h10.66v-.02Z"/>
|
||||
<path class="cls-6" d="M479.04,132.28h0v40.55h0c6.99,0,13.94,0,20.89,0h.61v-4.44h-.05c-5.63,0-11.22,0-16.8,0v-14.17h16.2v-4.45h-16.2v-13.05h16.79s.05,0,.05,0v-4.44h-21.5Z"/>
|
||||
<path class="cls-6" d="M417.64,131.69c-11.5,0-20.85,9.35-20.85,20.85s9.35,20.85,20.85,20.85,20.85-9.35,20.85-20.85-9.35-20.85-20.85-20.85ZM417.64,168.88c-9.01,0-16.34-7.33-16.34-16.34s7.33-16.34,16.34-16.34,16.34,7.33,16.34,16.34-7.33,16.34-16.34,16.34Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 115 KiB |
BIN
img/metrics/model_size_comparison.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
img/metrics/runtime_cpu_gpu_latency_memory.png
Normal file
|
After Width: | Height: | Size: 256 KiB |
BIN
img/metrics/s3_vs_measured_wer_range.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
img/metrics/s3_vs_measured_wer_range_voxcpm2.png
Normal file
|
After Width: | Height: | Size: 193 KiB |
BIN
img/metrics/supertonic2_vs_3_comparison.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
img/supertonic_preview_0.1.jpg
Normal file
|
After Width: | Height: | Size: 558 KiB |
BIN
img/voicebuilder_img.png
Normal file
|
After Width: | Height: | Size: 448 KiB |
@@ -12,28 +12,20 @@ struct ContentView: View {
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 12) {
|
||||
Text("SupertonicTTS iOS Demo")
|
||||
Text("Supertonic 3 iOS Demo")
|
||||
.font(.title2.weight(.semibold))
|
||||
.foregroundColor(.primary)
|
||||
|
||||
ZStack(alignment: .topLeading) {
|
||||
if vm.text.isEmpty {
|
||||
Text("Type text to synthesize")
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
TextEditor(text: $vm.text)
|
||||
.frame(minHeight: 120, maxHeight: 180)
|
||||
.padding(8)
|
||||
.background(Color(.secondarySystemBackground))
|
||||
.cornerRadius(12)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(Color.secondary.opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
TextEditor(text: $vm.text)
|
||||
.frame(minHeight: 120, maxHeight: 180)
|
||||
.padding(8)
|
||||
.background(Color(.secondarySystemBackground))
|
||||
.cornerRadius(12)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(Color.secondary.opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
.padding(.horizontal)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Text("NFE")
|
||||
@@ -52,6 +44,19 @@ struct ContentView: View {
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
.padding(.horizontal)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Text("Language")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
Picker("Language", selection: $vm.language) {
|
||||
ForEach(TTSService.Language.allCases, id: \.self) { lang in
|
||||
Text(lang.displayName).tag(lang)
|
||||
}
|
||||
}
|
||||
.pickerStyle(MenuPickerStyle())
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
HStack(spacing: 16) {
|
||||
@@ -92,7 +97,3 @@ struct ContentView: View {
|
||||
.onAppear { vm.startup() }
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentView()
|
||||
}
|
||||
|
||||
@@ -3,16 +3,76 @@ import OnnxRuntimeBindings
|
||||
|
||||
final class TTSService {
|
||||
enum Voice { case male, female }
|
||||
enum Language: String, CaseIterable {
|
||||
case en = "en"
|
||||
case ko = "ko"
|
||||
case ja = "ja"
|
||||
case ar = "ar"
|
||||
case bg = "bg"
|
||||
case cs = "cs"
|
||||
case da = "da"
|
||||
case de = "de"
|
||||
case el = "el"
|
||||
case es = "es"
|
||||
case et = "et"
|
||||
case fi = "fi"
|
||||
case fr = "fr"
|
||||
case hi = "hi"
|
||||
case hr = "hr"
|
||||
case hu = "hu"
|
||||
case id = "id"
|
||||
case it = "it"
|
||||
case lt = "lt"
|
||||
case lv = "lv"
|
||||
case nl = "nl"
|
||||
case pl = "pl"
|
||||
case pt = "pt"
|
||||
case ro = "ro"
|
||||
case ru = "ru"
|
||||
case sk = "sk"
|
||||
case sl = "sl"
|
||||
case sv = "sv"
|
||||
case tr = "tr"
|
||||
case uk = "uk"
|
||||
case vi = "vi"
|
||||
case na = "na"
|
||||
|
||||
struct Settings {
|
||||
var nTest: Int = 1
|
||||
}
|
||||
|
||||
struct SynthesisResult {
|
||||
let url: URL
|
||||
let elapsedSeconds: Double
|
||||
let audioSeconds: Double
|
||||
var rtf: Double { elapsedSeconds / max(audioSeconds, 1e-6) }
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .en: return "English"
|
||||
case .ko: return "한국어"
|
||||
case .ja: return "日本語"
|
||||
case .ar: return "العربية"
|
||||
case .bg: return "Bulgarian"
|
||||
case .cs: return "Czech"
|
||||
case .da: return "Danish"
|
||||
case .de: return "Deutsch"
|
||||
case .el: return "Greek"
|
||||
case .es: return "Español"
|
||||
case .et: return "Estonian"
|
||||
case .fi: return "Finnish"
|
||||
case .fr: return "Français"
|
||||
case .hi: return "Hindi"
|
||||
case .hr: return "Croatian"
|
||||
case .hu: return "Hungarian"
|
||||
case .id: return "Indonesian"
|
||||
case .it: return "Italian"
|
||||
case .lt: return "Lithuanian"
|
||||
case .lv: return "Latvian"
|
||||
case .nl: return "Dutch"
|
||||
case .pl: return "Polish"
|
||||
case .pt: return "Português"
|
||||
case .ro: return "Romanian"
|
||||
case .ru: return "Russian"
|
||||
case .sk: return "Slovak"
|
||||
case .sl: return "Slovenian"
|
||||
case .sv: return "Swedish"
|
||||
case .tr: return "Turkish"
|
||||
case .uk: return "Ukrainian"
|
||||
case .vi: return "Vietnamese"
|
||||
case .na: return "Auto (language-agnostic)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let env: ORTEnv
|
||||
@@ -20,9 +80,6 @@ final class TTSService {
|
||||
private let bundleOnnxDir: String
|
||||
private let sampleRate: Int
|
||||
|
||||
// Cached style per voice (precomputed at startup or on first use)
|
||||
private var cachedStyle: [Voice: Style] = [:]
|
||||
|
||||
init() throws {
|
||||
bundleOnnxDir = try Self.locateOnnxDirInBundle()
|
||||
env = try ORTEnv(loggingLevel: .warning)
|
||||
@@ -30,50 +87,21 @@ final class TTSService {
|
||||
sampleRate = textToSpeech.sampleRate
|
||||
}
|
||||
|
||||
// Public warmup: precompute styles and run a quick generation to warm models
|
||||
func warmup(nfe: Int = 1) async {
|
||||
do { try precomputeStyle(for: .male) } catch { print("Warmup style (M) error: \(error)") }
|
||||
do { try precomputeStyle(for: .female) } catch { print("Warmup style (F) error: \(error)") }
|
||||
// Run a tiny synthesis to JIT/warm up kernels; discard file
|
||||
do {
|
||||
let res = try await synthesize(text: "Warm up", nfe: max(1, nfe), voice: .male)
|
||||
try? FileManager.default.removeItem(at: res.url)
|
||||
} catch {
|
||||
print("Warmup synth error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func synthesize(text: String, nfe: Int, voice: Voice, settings: Settings = Settings()) async throws -> SynthesisResult {
|
||||
let tic = Date()
|
||||
|
||||
// 1) Get or compute style for the selected voice
|
||||
let style = try getStyle(voice: voice)
|
||||
func synthesize(text: String, nfe: Int, voice: Voice, language: Language) async throws -> URL {
|
||||
// Load style for the selected voice
|
||||
let styleURL = try Self.locateVoiceStyleURL(voice: voice)
|
||||
let style = try loadVoiceStyle([styleURL.path], verbose: false)
|
||||
|
||||
// 2) Synthesize via packed TextToSpeech component
|
||||
let (wav, duration) = try textToSpeech.call([text], style, nfe)
|
||||
let audioSeconds = Double(duration[0])
|
||||
let (wav, duration) = try textToSpeech.call(text, language.rawValue, style, nfe)
|
||||
let audioSeconds = Double(duration)
|
||||
let wavLenSample = min(Int(Double(sampleRate) * audioSeconds), wav.count)
|
||||
let wavOut = Array(wav[0..<wavLenSample])
|
||||
|
||||
let tmpURL = FileManager.default.temporaryDirectory.appendingPathComponent("supertonic_tts_\(UUID().uuidString).wav")
|
||||
try writeWavFile(tmpURL.path, wavOut, sampleRate)
|
||||
|
||||
let elapsed = Date().timeIntervalSince(tic)
|
||||
return SynthesisResult(url: tmpURL, elapsedSeconds: elapsed, audioSeconds: audioSeconds)
|
||||
}
|
||||
|
||||
// MARK: - Style helpers
|
||||
private func precomputeStyle(for voice: Voice) throws {
|
||||
if cachedStyle[voice] != nil { return }
|
||||
let styleURL = try Self.locateVoiceStyleURL(voice: voice)
|
||||
let style = try loadVoiceStyle([styleURL.path], verbose: false)
|
||||
cachedStyle[voice] = style
|
||||
}
|
||||
|
||||
private func getStyle(voice: Voice) throws -> Style {
|
||||
if let style = cachedStyle[voice] { return style }
|
||||
try precomputeStyle(for: voice)
|
||||
return cachedStyle[voice]!
|
||||
return tmpURL
|
||||
}
|
||||
|
||||
// MARK: - Resource location helpers
|
||||
|
||||
@@ -4,13 +4,13 @@ import AVFoundation
|
||||
@MainActor
|
||||
final class TTSViewModel: ObservableObject {
|
||||
@Published var text: String = "This morning, I took a walk in the park, and the sound of the birds and the breeze was so pleasant that I stopped for a long time just to listen."
|
||||
@Published var nfe: Double = 5
|
||||
@Published var nfe: Double = 8
|
||||
@Published var voice: TTSService.Voice = .male
|
||||
@Published var language: TTSService.Language = .en
|
||||
@Published var isGenerating: Bool = false
|
||||
@Published var isPlaying: Bool = false
|
||||
@Published var errorMessage: String?
|
||||
@Published var audioURL: URL?
|
||||
|
||||
@Published var elapsedSeconds: Double?
|
||||
@Published var audioSeconds: Double?
|
||||
|
||||
@@ -19,14 +19,12 @@ final class TTSViewModel: ObservableObject {
|
||||
|
||||
var rtfText: String? {
|
||||
guard let e = elapsedSeconds, let a = audioSeconds, a > 0 else { return nil }
|
||||
let rtf = e / a
|
||||
return String(format: "RTF %.2fx · %.2fs / %.2fs", rtf, e, a)
|
||||
return String(format: "RTF %.2fx · %.2fs / %.2fs", e / a, e, a)
|
||||
}
|
||||
|
||||
func startup() {
|
||||
do {
|
||||
service = try TTSService()
|
||||
Task { await self.service?.warmup(nfe: 5) }
|
||||
} catch {
|
||||
errorMessage = "Failed to init TTS: \(error.localizedDescription)"
|
||||
}
|
||||
@@ -40,15 +38,18 @@ final class TTSViewModel: ObservableObject {
|
||||
elapsedSeconds = nil
|
||||
audioSeconds = nil
|
||||
Task {
|
||||
let tic = Date()
|
||||
do {
|
||||
let result = try await service.synthesize(text: text, nfe: Int(nfe), voice: voice)
|
||||
let url = try await service.synthesize(text: text, nfe: Int(nfe), voice: voice, language: language)
|
||||
let elapsed = Date().timeIntervalSince(tic)
|
||||
let audio = audioDuration(at: url)
|
||||
await MainActor.run {
|
||||
self.audioURL = result.url
|
||||
self.elapsedSeconds = result.elapsedSeconds
|
||||
self.audioSeconds = result.audioSeconds
|
||||
self.audioURL = url
|
||||
self.elapsedSeconds = elapsed
|
||||
self.audioSeconds = audio
|
||||
self.isGenerating = false
|
||||
self.play(url: url)
|
||||
}
|
||||
self.play(url: result.url)
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.errorMessage = error.localizedDescription
|
||||
@@ -73,4 +74,9 @@ final class TTSViewModel: ObservableObject {
|
||||
}
|
||||
isPlaying = true
|
||||
}
|
||||
|
||||
private func audioDuration(at url: URL) -> Double? {
|
||||
guard let file = try? AVAudioFile(forReading: url) else { return nil }
|
||||
return Double(file.length) / file.fileFormat.sampleRate
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ targets:
|
||||
deploymentTarget: "15.0"
|
||||
sources:
|
||||
- path: .
|
||||
- path: ../../swift/Sources/Helper.swift <<- 여기
|
||||
- path: ../../swift/Sources/Helper.swift
|
||||
type: file
|
||||
resources:
|
||||
- path: onnx
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
# Supertonic iOS Example App
|
||||
|
||||
A minimal iOS demo that runs Supertonic (ONNX Runtime) on-device. The app shows:
|
||||
A minimal iOS demo that runs Supertonic 3 (ONNX Runtime) on-device. The app shows:
|
||||
- Multiline text input
|
||||
- NFE (denoising steps) slider
|
||||
- Voice toggle (M/F)
|
||||
- Language selector for 31 supported languages
|
||||
- Generate & Play buttons
|
||||
- RTF display (Elapsed / Audio seconds)
|
||||
|
||||
All ONNX models/configs are reused from `Supertonic/assets/onnx`, and voice style JSON files from `Supertonic/assets/voice_styles`.
|
||||
|
||||
## 📰 Update News
|
||||
|
||||
**2026.04.29** - 🎉 **Supertonic 3** released with 31-language support, improved reading accuracy, and v2-compatible public ONNX assets. [Demo](https://huggingface.co/spaces/Supertone/supertonic-3) | [Models](https://huggingface.co/Supertone/supertonic-3)
|
||||
|
||||
**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
|
||||
|
||||
**2025.12.08** - Optimized ONNX models via [OnnxSlim](https://github.com/inisis/OnnxSlim) now available on [Hugging Face Models](https://huggingface.co/Supertone/supertonic)
|
||||
|
||||
## Prerequisites
|
||||
- macOS 13+, Xcode 15+
|
||||
- Swift 5.9+
|
||||
@@ -52,8 +61,13 @@ These references are defined in `project.yml` and added to the app bundle by Xco
|
||||
|
||||
## App Controls
|
||||
- **Text**: Multiline `TextEditor`
|
||||
- **NFE**: Denoising steps (default 5)
|
||||
- **Voice**: M1/M2/F1/F2 voice style selector (4 pre-extracted styles)
|
||||
- **NFE**: Denoising steps (default 8)
|
||||
- **Voice**: M/F voice style selector
|
||||
- **Language**: Language selector for 31 supported languages
|
||||
- **Generate**: Runs end-to-end synthesis
|
||||
- **Play/Stop**: Controls playback of the last output
|
||||
- **RTF**: Shows Elapsed / Audio seconds for quick performance intuition
|
||||
- **RTF**: Shows Elapsed / Audio seconds for quick performance intuition
|
||||
|
||||
## Multilingual Support
|
||||
|
||||
Supertonic 3 supports 31 languages. Select the appropriate language for your input text; see the main README for the full code list.
|
||||
|
||||
@@ -13,14 +13,15 @@ public class ExampleONNX {
|
||||
*/
|
||||
static class Args {
|
||||
boolean useGpu = false;
|
||||
String onnxDir = "assets/onnx";
|
||||
int totalStep = 5;
|
||||
String onnxDir = "../assets/onnx";
|
||||
int totalStep = 8;
|
||||
float speed = 1.05f;
|
||||
int nTest = 4;
|
||||
List<String> voiceStyle = Arrays.asList("assets/voice_styles/M1.json");
|
||||
List<String> voiceStyle = Arrays.asList("../assets/voice_styles/M1.json");
|
||||
List<String> text = Arrays.asList(
|
||||
"This morning, I took a walk in the park, and the sound of the birds and the breeze was so pleasant that I stopped for a long time just to listen."
|
||||
);
|
||||
List<String> lang = Arrays.asList("en");
|
||||
String saveDir = "results";
|
||||
boolean batch = false;
|
||||
}
|
||||
@@ -58,6 +59,11 @@ public class ExampleONNX {
|
||||
result.text = Arrays.asList(args[++i].split("\\|"));
|
||||
}
|
||||
break;
|
||||
case "--lang":
|
||||
if (i + 1 < args.length) {
|
||||
result.lang = Arrays.asList(args[++i].split(","));
|
||||
}
|
||||
break;
|
||||
case "--save-dir":
|
||||
if (i + 1 < args.length) result.saveDir = args[++i];
|
||||
break;
|
||||
@@ -85,6 +91,7 @@ public class ExampleONNX {
|
||||
String saveDir = parsedArgs.saveDir;
|
||||
List<String> voiceStylePaths = parsedArgs.voiceStyle;
|
||||
List<String> textList = parsedArgs.text;
|
||||
List<String> langList = parsedArgs.lang;
|
||||
boolean batch = parsedArgs.batch;
|
||||
|
||||
if (batch) {
|
||||
@@ -92,6 +99,10 @@ public class ExampleONNX {
|
||||
throw new RuntimeException("Number of voice styles (" + voiceStylePaths.size() +
|
||||
") must match number of texts (" + textList.size() + ")");
|
||||
}
|
||||
if (langList.size() != textList.size()) {
|
||||
throw new RuntimeException("Number of languages (" + langList.size() +
|
||||
") must match number of texts (" + textList.size() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
int bsz = voiceStylePaths.size();
|
||||
@@ -116,7 +127,7 @@ public class ExampleONNX {
|
||||
if (batch) {
|
||||
ttsResult = Helper.timer("Generating speech from text", () -> {
|
||||
try {
|
||||
return textToSpeech.batch(textList, style, totalStep, speed, env);
|
||||
return textToSpeech.batch(textList, langList, style, totalStep, speed, env);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -124,7 +135,7 @@ public class ExampleONNX {
|
||||
} else {
|
||||
ttsResult = Helper.timer("Generating speech from text", () -> {
|
||||
try {
|
||||
return textToSpeech.call(textList.get(0), style, totalStep, speed, 0.3f, env);
|
||||
return textToSpeech.call(textList.get(0), langList.get(0), style, totalStep, speed, 0.3f, env);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,17 @@ import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
/**
|
||||
* Available languages for multilingual TTS
|
||||
*/
|
||||
class Languages {
|
||||
public static final List<String> AVAILABLE = Arrays.asList("en", "ko", "ja", "ar", "bg", "cs", "da", "de", "el", "es", "et", "fi", "fr", "hi", "hr", "hu", "id", "it", "lt", "lv", "nl", "pl", "pt", "ro", "ru", "sk", "sl", "sv", "tr", "uk", "vi", "na");
|
||||
|
||||
public static boolean isValid(String lang) {
|
||||
return AVAILABLE.contains(lang);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration classes
|
||||
*/
|
||||
@@ -96,22 +107,28 @@ class UnicodeProcessor {
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
public TextProcessResult call(List<String> textList) {
|
||||
public TextProcessResult call(List<String> textList, List<String> langList) {
|
||||
List<String> processedTexts = new ArrayList<>();
|
||||
for (String text : textList) {
|
||||
processedTexts.add(preprocessText(text));
|
||||
for (int i = 0; i < textList.size(); i++) {
|
||||
processedTexts.add(preprocessText(textList.get(i), langList.get(i)));
|
||||
}
|
||||
|
||||
// Convert texts to unicode values first to get correct character counts
|
||||
List<int[]> allUnicodeVals = new ArrayList<>();
|
||||
for (String text : processedTexts) {
|
||||
allUnicodeVals.add(textToUnicodeValues(text));
|
||||
}
|
||||
|
||||
int[] textIdsLengths = new int[processedTexts.size()];
|
||||
int maxLen = 0;
|
||||
for (int i = 0; i < processedTexts.size(); i++) {
|
||||
textIdsLengths[i] = processedTexts.get(i).length();
|
||||
for (int i = 0; i < allUnicodeVals.size(); i++) {
|
||||
textIdsLengths[i] = allUnicodeVals.get(i).length; // Use code point count, not char count
|
||||
maxLen = Math.max(maxLen, textIdsLengths[i]);
|
||||
}
|
||||
|
||||
long[][] textIds = new long[processedTexts.size()][maxLen];
|
||||
for (int i = 0; i < processedTexts.size(); i++) {
|
||||
int[] unicodeVals = textToUnicodeValues(processedTexts.get(i));
|
||||
for (int i = 0; i < allUnicodeVals.size(); i++) {
|
||||
int[] unicodeVals = allUnicodeVals.get(i);
|
||||
for (int j = 0; j < unicodeVals.length; j++) {
|
||||
textIds[i][j] = indexer[unicodeVals[j]];
|
||||
}
|
||||
@@ -121,12 +138,10 @@ class UnicodeProcessor {
|
||||
return new TextProcessResult(textIds, textMask);
|
||||
}
|
||||
|
||||
private String preprocessText(String text) {
|
||||
private String preprocessText(String text, String lang) {
|
||||
// TODO: Need advanced normalizer for better performance
|
||||
text = Normalizer.normalize(text, Normalizer.Form.NFKD);
|
||||
|
||||
// FIXME: this should be fixed for non-English languages
|
||||
|
||||
// Remove emojis (wide Unicode range)
|
||||
// Java Pattern doesn't support \x{...} syntax for Unicode above \uFFFF
|
||||
// Use character filtering instead
|
||||
@@ -137,7 +152,6 @@ class UnicodeProcessor {
|
||||
replacements.put("–", "-"); // en dash
|
||||
replacements.put("‑", "-"); // non-breaking hyphen
|
||||
replacements.put("—", "-"); // em dash
|
||||
replacements.put("¯", " "); // macron
|
||||
replacements.put("_", " "); // underscore
|
||||
replacements.put("\u201C", "\""); // left double quote
|
||||
replacements.put("\u201D", "\""); // right double quote
|
||||
@@ -157,9 +171,6 @@ class UnicodeProcessor {
|
||||
text = text.replace(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
// Remove combining diacritics // FIXME: this should be fixed for non-English languages
|
||||
text = text.replaceAll("[\\u0302\\u0303\\u0304\\u0305\\u0306\\u0307\\u0308\\u030A\\u030B\\u030C\\u0327\\u0328\\u0329\\u032A\\u032B\\u032C\\u032D\\u032E\\u032F]", "");
|
||||
|
||||
// Remove special symbols
|
||||
text = text.replaceAll("[♥☆♡©\\\\]", "");
|
||||
|
||||
@@ -201,15 +212,20 @@ class UnicodeProcessor {
|
||||
text += ".";
|
||||
}
|
||||
|
||||
// Validate language
|
||||
if (!Languages.isValid(lang)) {
|
||||
throw new IllegalArgumentException("Invalid language: " + lang + ". Available: " + Languages.AVAILABLE);
|
||||
}
|
||||
|
||||
// Wrap text with language tags
|
||||
text = "<" + lang + ">" + text + "</" + lang + ">";
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
private int[] textToUnicodeValues(String text) {
|
||||
int[] values = new int[text.length()];
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
values[i] = text.codePointAt(i);
|
||||
}
|
||||
return values;
|
||||
// Use codePoints() stream to correctly handle surrogate pairs
|
||||
return text.codePoints().toArray();
|
||||
}
|
||||
|
||||
private float[][][] getTextMask(int[] lengths) {
|
||||
@@ -269,12 +285,12 @@ class TextToSpeech {
|
||||
this.ldim = config.ttl.latentDim;
|
||||
}
|
||||
|
||||
private TTSResult _infer(List<String> textList, Style style, int totalStep, float speed, OrtEnvironment env)
|
||||
private TTSResult _infer(List<String> textList, List<String> langList, Style style, int totalStep, float speed, OrtEnvironment env)
|
||||
throws OrtException {
|
||||
int bsz = textList.size();
|
||||
|
||||
// Process text
|
||||
UnicodeProcessor.TextProcessResult textResult = textProcessor.call(textList);
|
||||
UnicodeProcessor.TextProcessResult textResult = textProcessor.call(textList, langList);
|
||||
long[][] textIds = textResult.textIds;
|
||||
float[][][] textMask = textResult.textMask;
|
||||
|
||||
@@ -360,7 +376,18 @@ class TextToSpeech {
|
||||
|
||||
OrtSession.Result vocoderResult = vocoderSession.run(vocoderInputs);
|
||||
float[][] wavBatch = (float[][]) vocoderResult.get(0).getValue();
|
||||
float[] wav = wavBatch[0];
|
||||
|
||||
// Flatten all batch audio into a single array for batch processing
|
||||
int totalSamples = 0;
|
||||
for (float[] w : wavBatch) {
|
||||
totalSamples += w.length;
|
||||
}
|
||||
float[] wav = new float[totalSamples];
|
||||
int offset = 0;
|
||||
for (float[] w : wavBatch) {
|
||||
System.arraycopy(w, 0, wav, offset, w.length);
|
||||
offset += w.length;
|
||||
}
|
||||
|
||||
// Clean up
|
||||
textIdsTensor.close();
|
||||
@@ -421,15 +448,16 @@ class TextToSpeech {
|
||||
/**
|
||||
* Synthesize speech from a single text with automatic chunking
|
||||
*/
|
||||
public TTSResult call(String text, Style style, int totalStep, float speed, float silenceDuration, OrtEnvironment env)
|
||||
public TTSResult call(String text, String lang, Style style, int totalStep, float speed, float silenceDuration, OrtEnvironment env)
|
||||
throws OrtException {
|
||||
List<String> chunks = Helper.chunkText(text, 0);
|
||||
int maxLen = (lang.equals("ko") || lang.equals("ja")) ? 120 : 300;
|
||||
List<String> chunks = Helper.chunkText(text, maxLen);
|
||||
|
||||
List<Float> wavCat = new ArrayList<>();
|
||||
float durCat = 0.0f;
|
||||
|
||||
for (int i = 0; i < chunks.size(); i++) {
|
||||
TTSResult result = _infer(Arrays.asList(chunks.get(i)), style, totalStep, speed, env);
|
||||
TTSResult result = _infer(Arrays.asList(chunks.get(i)), Arrays.asList(lang), style, totalStep, speed, env);
|
||||
|
||||
float dur = result.duration[0];
|
||||
int wavLen = (int) (sampleRate * dur);
|
||||
@@ -464,9 +492,9 @@ class TextToSpeech {
|
||||
/**
|
||||
* Batch synthesize speech from multiple texts
|
||||
*/
|
||||
public TTSResult batch(List<String> textList, Style style, int totalStep, float speed, OrtEnvironment env)
|
||||
public TTSResult batch(List<String> textList, List<String> langList, Style style, int totalStep, float speed, OrtEnvironment env)
|
||||
throws OrtException {
|
||||
return _infer(textList, style, totalStep, speed, env);
|
||||
return _infer(textList, langList, style, totalStep, speed, env);
|
||||
}
|
||||
|
||||
public void close() throws OrtException {
|
||||
@@ -841,13 +869,20 @@ public class Helper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize filename
|
||||
* Sanitize filename (supports Unicode characters)
|
||||
*/
|
||||
public static String sanitizeFilename(String text, int maxLen) {
|
||||
if (text.length() > maxLen) {
|
||||
text = text.substring(0, maxLen);
|
||||
// Get first maxLen characters (code points, not chars for surrogate pairs)
|
||||
int[] codePoints = text.codePoints().limit(maxLen).toArray();
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (int codePoint : codePoints) {
|
||||
if (Character.isLetterOrDigit(codePoint)) {
|
||||
result.appendCodePoint(codePoint);
|
||||
} else {
|
||||
result.append('_');
|
||||
}
|
||||
}
|
||||
return text.replaceAll("[^a-zA-Z0-9]", "_");
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -917,4 +952,3 @@ public class Helper {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,12 @@ This guide provides examples for running TTS inference using `ExampleONNX.java`.
|
||||
|
||||
## 📰 Update News
|
||||
|
||||
**2026.04.29** - 🎉 **Supertonic 3** released with 31-language support, improved reading accuracy, and v2-compatible public ONNX assets. [Demo](https://huggingface.co/spaces/Supertone/supertonic-3) | [Models](https://huggingface.co/Supertone/supertonic-3)
|
||||
|
||||
**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
|
||||
|
||||
**2025.12.08** - Optimized ONNX models via [OnnxSlim](https://github.com/inisis/OnnxSlim) now available on [Hugging Face Models](https://huggingface.co/Supertone/supertonic)
|
||||
|
||||
**2025.11.23** - Enhanced text preprocessing with comprehensive normalization, emoji removal, symbol replacement, and punctuation handling for improved synthesis quality.
|
||||
|
||||
**2025.11.19** - Added `--speed` parameter to control speech synthesis speed (default: 1.05, recommended range: 0.9-1.5).
|
||||
@@ -16,9 +22,17 @@ This project uses [Maven](https://maven.apache.org/) for dependency management.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Java 11 or higher
|
||||
- Java Development Kit (JDK) 11 or higher, not just a JRE
|
||||
- Maven 3.6 or higher
|
||||
|
||||
On macOS with Homebrew:
|
||||
|
||||
```bash
|
||||
brew install openjdk@17 maven
|
||||
export JAVA_HOME="$(brew --prefix openjdk@17)/libexec/openjdk.jdk/Contents/Home"
|
||||
export PATH="$(brew --prefix openjdk@17)/bin:$PATH"
|
||||
```
|
||||
|
||||
### Install dependencies
|
||||
|
||||
```bash
|
||||
@@ -34,39 +48,39 @@ mvn exec:java
|
||||
```
|
||||
|
||||
This will use:
|
||||
- Voice style: `assets/voice_styles/M1.json`
|
||||
- Voice style: `../assets/voice_styles/M1.json`
|
||||
- Text: "This morning, I took a walk in the park, and the sound of the birds and the breeze was so pleasant that I stopped for a long time just to listen."
|
||||
- Output directory: `results/`
|
||||
- Total steps: 5
|
||||
- Total steps: 8
|
||||
- Number of generations: 4
|
||||
|
||||
### Example 2: Batch Inference
|
||||
Process multiple voice styles and texts at once:
|
||||
```bash
|
||||
mvn exec:java -Dexec.args="--batch --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.'"
|
||||
mvn exec:java -Dexec.args="--batch --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.|오늘 아침에 공원을 산책했는데, 새소리와 바람 소리가 너무 기분 좋았어요.' --lang en,ko"
|
||||
```
|
||||
|
||||
This will:
|
||||
- Generate speech for 2 different voice-text pairs
|
||||
- Use male voice (M1.json) for the first text
|
||||
- Use female voice (F1.json) for the second text
|
||||
- Generate speech for 2 different voice-text-language pairs
|
||||
- Use male voice (M1.json) for the first text in English
|
||||
- Use female voice (F1.json) for the second text in Korean
|
||||
- Process both samples in a single batch
|
||||
|
||||
### Example 3: High Quality Inference
|
||||
Increase denoising steps for better quality:
|
||||
```bash
|
||||
mvn exec:java -Dexec.args="--total-step 10 --voice-style assets/voice_styles/M1.json --text 'Increasing the number of denoising steps improves the output fidelity and overall quality.'"
|
||||
mvn exec:java -Dexec.args="--total-step 10 --voice-style ../assets/voice_styles/M1.json --text 'Increasing the number of denoising steps improves the output fidelity and overall quality.'"
|
||||
```
|
||||
|
||||
This will:
|
||||
- Use 10 denoising steps instead of the default 5
|
||||
- Use 10 denoising steps instead of the default 8
|
||||
- Produce higher quality output at the cost of slower inference
|
||||
|
||||
### Example 4: Long-Form Inference
|
||||
The system automatically chunks long texts into manageable segments, synthesizes each segment separately, and concatenates them with natural pauses (0.3 seconds by default) into a single audio file. This happens by default when you don't use the `--batch` flag:
|
||||
|
||||
```bash
|
||||
mvn exec:java -Dexec.args="--voice-style assets/voice_styles/M1.json --text 'This is a very long text that will be automatically split into multiple chunks. The system will process each chunk separately and then concatenate them together with natural pauses between segments. This ensures that even very long texts can be processed efficiently while maintaining natural speech flow and avoiding memory issues.'"
|
||||
mvn exec:java -Dexec.args="--voice-style ../assets/voice_styles/M1.json --text 'This is a very long text that will be automatically split into multiple chunks. The system will process each chunk separately and then concatenate them together with natural pauses between segments. This ensures that even very long texts can be processed efficiently while maintaining natural speech flow and avoiding memory issues.'"
|
||||
```
|
||||
|
||||
This will:
|
||||
@@ -104,19 +118,20 @@ java -jar target/tts-example.jar --total-step 10 --text "Your custom text here"
|
||||
| Argument | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `--use-gpu` | flag | False | Use GPU for inference (default: CPU) |
|
||||
| `--onnx-dir` | str | `assets/onnx` | Path to ONNX model directory |
|
||||
| `--total-step` | int | 5 | Number of denoising steps (higher = better quality, slower) |
|
||||
| `--onnx-dir` | str | `../assets/onnx` | Path to ONNX model directory |
|
||||
| `--total-step` | int | 8 | Number of denoising steps (higher = better quality, slower) |
|
||||
| `--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 |
|
||||
| `--voice-style` | str+ | `../assets/voice_styles/M1.json` | Voice style file path(s), comma-separated |
|
||||
| `--text` | str+ | (long default text) | Text(s) to synthesize, pipe-separated |
|
||||
| `--lang` | str+ | `en` | Language(s) for synthesis, comma-separated; see the main README for all 31 codes |
|
||||
| `--save-dir` | str | `results` | Output directory |
|
||||
| `--batch` | flag | False | Enable batch mode (multiple text-style pairs, disables automatic chunking) |
|
||||
|
||||
## Notes
|
||||
|
||||
- **Batch Processing**: When using `--batch`, the number of `--voice-style` files must match the number of `--text` entries
|
||||
- **Multilingual Support**: Use `--lang` to specify the language for each text. Available: 31 languages; see the main README for the full list
|
||||
- **Batch Processing**: When using `--batch`, the number of `--voice-style`, `--text`, and `--lang` entries must match
|
||||
- **Automatic Chunking**: Without `--batch`, long texts are automatically split and concatenated with 0.3s pauses
|
||||
- **Quality vs Speed**: Higher `--total-step` values produce better quality but take longer
|
||||
- **GPU Support**: GPU mode is not supported yet
|
||||
- **Voice Styles**: Uses pre-extracted voice style JSON files for fast inference
|
||||
|
||||
|
||||
@@ -4,6 +4,12 @@ Node.js implementation for TTS inference. Uses ONNX Runtime to generate speech f
|
||||
|
||||
## 📰 Update News
|
||||
|
||||
**2026.04.29** - 🎉 **Supertonic 3** released with 31-language support, improved reading accuracy, and v2-compatible public ONNX assets. [Demo](https://huggingface.co/spaces/Supertone/supertonic-3) | [Models](https://huggingface.co/Supertone/supertonic-3)
|
||||
|
||||
**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
|
||||
|
||||
**2025.12.08** - Optimized ONNX models via [OnnxSlim](https://github.com/inisis/OnnxSlim) now available on [Hugging Face Models](https://huggingface.co/Supertone/supertonic)
|
||||
|
||||
**2025.11.23** - Enhanced text preprocessing with comprehensive normalization, emoji removal, symbol replacement, and punctuation handling for improved synthesis quality.
|
||||
|
||||
**2025.11.19** - Added `--speed` parameter to control speech synthesis speed (default: 1.05, recommended range: 0.9-1.5).
|
||||
@@ -27,7 +33,7 @@ npm install
|
||||
### Example 1: Default Inference
|
||||
Run inference with default settings:
|
||||
```bash
|
||||
npm start
|
||||
node example_onnx.js
|
||||
```
|
||||
|
||||
Or:
|
||||
@@ -36,26 +42,27 @@ node example_onnx.js
|
||||
```
|
||||
|
||||
This will use:
|
||||
- Voice style: `assets/voice_styles/M1.json`
|
||||
- Voice style: `../assets/voice_styles/M1.json`
|
||||
- Text: "This morning, I took a walk in the park, and the sound of the birds and the breeze was so pleasant that I stopped for a long time just to listen."
|
||||
- Output directory: `results/`
|
||||
- Total steps: 5
|
||||
- Total steps: 8
|
||||
- Number of generations: 4
|
||||
|
||||
### Example 2: Batch Inference
|
||||
Process multiple voice styles and texts at once:
|
||||
```bash
|
||||
node example_onnx.js \
|
||||
--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." \
|
||||
--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.|오늘 아침에 공원을 산책했는데, 새소리와 바람 소리가 너무 좋아서 한참을 멈춰 서서 들었어요." \
|
||||
--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
|
||||
@@ -63,19 +70,19 @@ Increase denoising steps for better quality:
|
||||
```bash
|
||||
node example_onnx.js \
|
||||
--total-step 10 \
|
||||
--voice-style "assets/voice_styles/M1.json" \
|
||||
--voice-style "../assets/voice_styles/M1.json" \
|
||||
--text "Increasing the number of denoising steps improves the output's fidelity and overall quality."
|
||||
```
|
||||
|
||||
This will:
|
||||
- Use 10 denoising steps instead of the default 5
|
||||
- Use 10 denoising steps instead of the default 8
|
||||
- Produce higher quality output at the cost of slower inference
|
||||
|
||||
### Example 4: Long-Form Inference
|
||||
For long texts, the system automatically chunks the text into manageable segments and generates a single audio file:
|
||||
```bash
|
||||
node example_onnx.js \
|
||||
--voice-style "assets/voice_styles/M1.json" \
|
||||
--voice-style "../assets/voice_styles/M1.json" \
|
||||
--text "Once upon a time, in a small village nestled between rolling hills, there lived a young artist named Clara. Every morning, she would wake up before dawn to capture the first light of day. The golden rays streaming through her window inspired countless paintings. Her work was known throughout the region for its vibrant colors and emotional depth. People from far and wide came to see her gallery, and many said her paintings could tell stories that words never could."
|
||||
```
|
||||
|
||||
@@ -92,17 +99,20 @@ This will:
|
||||
| Argument | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `--use-gpu` | flag | False | Use GPU for inference (not supported yet) |
|
||||
| `--onnx-dir` | str | `assets/onnx` | Path to ONNX model directory |
|
||||
| `--total-step` | int | 5 | Number of denoising steps (higher = better quality, slower) |
|
||||
| `--onnx-dir` | str | `../assets/onnx` | Path to ONNX model directory |
|
||||
| `--total-step` | int | 8 | Number of denoising steps (higher = better quality, slower) |
|
||||
| `--speed` | float | 1.05 | Speech speed factor (higher = faster, lower = slower) |
|
||||
| `--n-test` | int | 4 | Number of times to generate each sample |
|
||||
| `--voice-style` | str+ | `assets/voice_styles/M1.json` | Voice style file path(s). Separate multiple files with commas |
|
||||
| `--voice-style` | str+ | `../assets/voice_styles/M1.json` | Voice style file path(s). Separate multiple files with commas |
|
||||
| `--text` | str+ | (long default text) | Text(s) to synthesize. Separate multiple texts with pipes |
|
||||
| `--lang` | str+ | `en` | Language(s) for text(s); see the main README for all 31 codes. Separate multiple with commas |
|
||||
| `--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 texts. Use commas to separate files and pipes to separate texts
|
||||
- **Multilingual Support**: Use `--lang` to specify language(s). Available: 31 languages; see the main README for the full list
|
||||
- **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
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { loadTextToSpeech, loadVoiceStyle, timer, writeWavFile } from './helper.js';
|
||||
import { loadTextToSpeech, loadVoiceStyle, timer, writeWavFile, sanitizeFilename } from './helper.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -13,12 +13,13 @@ const __dirname = path.dirname(__filename);
|
||||
function parseArgs() {
|
||||
const args = {
|
||||
useGpu: false,
|
||||
onnxDir: 'assets/onnx',
|
||||
totalStep: 5,
|
||||
onnxDir: '../assets/onnx',
|
||||
totalStep: 8,
|
||||
speed: 1.05,
|
||||
nTest: 4,
|
||||
voiceStyle: ['assets/voice_styles/M1.json'],
|
||||
voiceStyle: ['../assets/voice_styles/M1.json'],
|
||||
text: ['This morning, I took a walk in the park, and the sound of the birds and the breeze was so pleasant that I stopped for a long time just to listen.'],
|
||||
lang: ['en'],
|
||||
saveDir: 'results',
|
||||
batch: false
|
||||
};
|
||||
@@ -41,6 +42,8 @@ function parseArgs() {
|
||||
args.voiceStyle = process.argv[++i].split(',');
|
||||
} else if (arg === '--text' && i + 1 < process.argv.length) {
|
||||
args.text = process.argv[++i].split('|');
|
||||
} else if (arg === '--lang' && i + 1 < process.argv.length) {
|
||||
args.lang = process.argv[++i].split(',');
|
||||
} else if (arg === '--save-dir' && i + 1 < process.argv.length) {
|
||||
args.saveDir = process.argv[++i];
|
||||
}
|
||||
@@ -63,6 +66,7 @@ async function main() {
|
||||
const saveDir = args.saveDir;
|
||||
const voiceStylePaths = args.voiceStyle.map(p => path.resolve(__dirname, p));
|
||||
const textList = args.text;
|
||||
const langList = args.lang;
|
||||
const batch = args.batch;
|
||||
|
||||
if (voiceStylePaths.length !== textList.length) {
|
||||
@@ -83,9 +87,9 @@ async function main() {
|
||||
|
||||
const { wav, duration } = await timer('Generating speech from text', async () => {
|
||||
if (batch) {
|
||||
return await textToSpeech.batch(textList, style, totalStep, speed);
|
||||
return await textToSpeech.batch(textList, langList, style, totalStep, speed);
|
||||
} else {
|
||||
return await textToSpeech.call(textList[0], style, totalStep, speed);
|
||||
return await textToSpeech.call(textList[0], langList[0], style, totalStep, speed);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -95,7 +99,7 @@ async function main() {
|
||||
|
||||
const wavShape = [bsz, wav.length / bsz];
|
||||
for (let b = 0; b < bsz; b++) {
|
||||
const fname = `${textList[b].substring(0, 20).replace(/[^a-zA-Z0-9]/g, '_')}_${n + 1}.wav`;
|
||||
const fname = `${sanitizeFilename(textList[b], 20)}_${n + 1}.wav`;
|
||||
const wavLen = Math.floor(textToSpeech.sampleRate * duration[b]);
|
||||
const wavOut = wav.slice(b * wavShape[1], b * wavShape[1] + wavLen);
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import * as ort from 'onnxruntime-node';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
|
||||
const AVAILABLE_LANGS = ["en", "ko", "ja", "ar", "bg", "cs", "da", "de", "el", "es", "et", "fi", "fr", "hi", "hr", "hu", "id", "it", "lt", "lv", "nl", "pl", "pt", "ro", "ru", "sk", "sl", "sv", "tr", "uk", "vi", "na"];
|
||||
|
||||
/**
|
||||
* Unicode text processor
|
||||
*/
|
||||
@@ -13,12 +15,10 @@ class UnicodeProcessor {
|
||||
this.indexer = JSON.parse(fs.readFileSync(unicodeIndexerJsonPath, 'utf8'));
|
||||
}
|
||||
|
||||
_preprocessText(text) {
|
||||
_preprocessText(text, lang) {
|
||||
// TODO: Need advanced normalizer for better performance
|
||||
text = text.normalize('NFKD');
|
||||
|
||||
// FIXME: this should be fixed for non-English languages
|
||||
|
||||
// Remove emojis (wide Unicode range)
|
||||
const emojiPattern = /[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F700}-\u{1F77F}\u{1F780}-\u{1F7FF}\u{1F800}-\u{1F8FF}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F1E6}-\u{1F1FF}]+/gu;
|
||||
text = text.replace(emojiPattern, '');
|
||||
@@ -28,12 +28,11 @@ class UnicodeProcessor {
|
||||
'–': '-',
|
||||
'‑': '-',
|
||||
'—': '-',
|
||||
'¯': ' ',
|
||||
'_': ' ',
|
||||
'"': '"',
|
||||
'"': '"',
|
||||
'\u2018': "'", // left single quote
|
||||
'\u2019': "'", // right single quote
|
||||
'\u201C': '"', // left double quote "
|
||||
'\u201D': '"', // right double quote "
|
||||
'\u2018': "'", // left single quote '
|
||||
'\u2019': "'", // right single quote '
|
||||
'´': "'",
|
||||
'`': "'",
|
||||
'[': ' ',
|
||||
@@ -48,9 +47,6 @@ class UnicodeProcessor {
|
||||
text = text.replaceAll(k, v);
|
||||
}
|
||||
|
||||
// Remove combining diacritics // FIXME: this should be fixed for non-English languages
|
||||
text = text.replace(/[\u0302\u0303\u0304\u0305\u0306\u0307\u0308\u030A\u030B\u030C\u0327\u0328\u0329\u032A\u032B\u032C\u032D\u032E\u032F]/g, '');
|
||||
|
||||
// Remove special symbols
|
||||
text = text.replace(/[♥☆♡©\\]/g, '');
|
||||
|
||||
@@ -92,6 +88,14 @@ class UnicodeProcessor {
|
||||
text += '.';
|
||||
}
|
||||
|
||||
// Validate language
|
||||
if (!AVAILABLE_LANGS.includes(lang)) {
|
||||
throw new Error(`Invalid language: ${lang}. Available: ${AVAILABLE_LANGS.join(', ')}`);
|
||||
}
|
||||
|
||||
// Wrap text with language tags
|
||||
text = `<${lang}>` + text + `</${lang}>`;
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
@@ -103,8 +107,8 @@ class UnicodeProcessor {
|
||||
return lengthToMask(textIdsLengths);
|
||||
}
|
||||
|
||||
call(textList) {
|
||||
const processedTexts = textList.map(t => this._preprocessText(t));
|
||||
call(textList, langList) {
|
||||
const processedTexts = textList.map((t, i) => this._preprocessText(t, langList[i]));
|
||||
const textIdsLengths = processedTexts.map(t => t.length);
|
||||
const maxLen = Math.max(...textIdsLengths);
|
||||
|
||||
@@ -191,12 +195,12 @@ class TextToSpeech {
|
||||
return { noisyLatent, latentMask };
|
||||
}
|
||||
|
||||
async _infer(textList, style, totalStep, speed = 1.05) {
|
||||
async _infer(textList, langList, style, totalStep, speed = 1.05) {
|
||||
if (textList.length !== style.ttl.dims[0]) {
|
||||
throw new Error('Number of texts must match number of style vectors');
|
||||
}
|
||||
const bsz = textList.length;
|
||||
const { textIds, textMask } = this.textProcessor.call(textList);
|
||||
const { textIds, textMask } = this.textProcessor.call(textList, langList);
|
||||
const textIdsShape = [bsz, textIds[0].length];
|
||||
const textMaskShape = [bsz, 1, textMask[0][0].length];
|
||||
|
||||
@@ -267,16 +271,17 @@ class TextToSpeech {
|
||||
return { wav, duration: durOnnx };
|
||||
}
|
||||
|
||||
async call(text, style, totalStep, speed = 1.05, silenceDuration = 0.3) {
|
||||
async call(text, lang, style, totalStep, speed = 1.05, silenceDuration = 0.3) {
|
||||
if (style.ttl.dims[0] !== 1) {
|
||||
throw new Error('Single speaker text to speech only supports single style');
|
||||
}
|
||||
const textList = chunkText(text);
|
||||
const maxLen = (lang === 'ko' || lang === 'ja') ? 120 : 300;
|
||||
const textList = chunkText(text, maxLen);
|
||||
let wavCat = null;
|
||||
let durCat = 0;
|
||||
|
||||
for (const chunk of textList) {
|
||||
const { wav, duration } = await this._infer([chunk], style, totalStep, speed);
|
||||
const { wav, duration } = await this._infer([chunk], [lang], style, totalStep, speed);
|
||||
|
||||
if (wavCat === null) {
|
||||
wavCat = wav;
|
||||
@@ -292,8 +297,8 @@ class TextToSpeech {
|
||||
return { wav: wavCat, duration: [durCat] };
|
||||
}
|
||||
|
||||
async batch(textList, style, totalStep, speed = 1.05) {
|
||||
return await this._infer(textList, style, totalStep, speed);
|
||||
async batch(textList, langList, style, totalStep, speed = 1.05) {
|
||||
return await this._infer(textList, langList, style, totalStep, speed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,6 +507,15 @@ export async function timer(name, fn) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize filename by replacing non-alphanumeric characters with underscores (supports Unicode)
|
||||
*/
|
||||
export function sanitizeFilename(text, maxLen) {
|
||||
const prefix = text.substring(0, maxLen);
|
||||
// \p{L} matches any Unicode letter, \p{N} matches any Unicode number
|
||||
return prefix.replace(/[^\p{L}\p{N}_]/gu, '_');
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunk text into manageable segments
|
||||
*/
|
||||
|
||||
39
py/README.md
@@ -4,6 +4,14 @@ This guide provides examples for running TTS inference using `example_onnx.py`.
|
||||
|
||||
## 📰 Update News
|
||||
|
||||
**2026.04.29** - 🎉 **Supertonic 3** released with 31-language support, improved reading accuracy, and v2-compatible public ONNX assets. [Demo](https://huggingface.co/spaces/Supertone/supertonic-3) | [Models](https://huggingface.co/Supertone/supertonic-3)
|
||||
|
||||
**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
|
||||
|
||||
**2025.12.08** - Optimized ONNX models via [OnnxSlim](https://github.com/inisis/OnnxSlim) now available on [Hugging Face Models](https://huggingface.co/Supertone/supertonic)
|
||||
|
||||
**2025.11.23** - Enhanced text preprocessing with comprehensive normalization, emoji removal, symbol replacement, and punctuation handling for improved synthesis quality.
|
||||
|
||||
**2025.11.19** - Added `--speed` parameter to control speech synthesis speed. Adjust the speed factor to make speech faster or slower while maintaining natural quality.
|
||||
@@ -38,26 +46,27 @@ uv run example_onnx.py
|
||||
```
|
||||
|
||||
This will use:
|
||||
- Voice style: `assets/voice_styles/M1.json`
|
||||
- Voice style: `../assets/voice_styles/M1.json`
|
||||
- Text: "This morning, I took a walk in the park, and the sound of the birds and the breeze was so pleasant that I stopped for a long time just to listen."
|
||||
- Output directory: `results/`
|
||||
- Total steps: 5
|
||||
- Total steps: 8
|
||||
- Number of generations: 4
|
||||
|
||||
### Example 2: Batch Inference
|
||||
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." \
|
||||
--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." "오늘 아침에 공원을 산책했는데, 새소리와 바람 소리가 너무 좋아서 한참을 멈춰 서서 들었어요." \
|
||||
--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
|
||||
@@ -65,19 +74,19 @@ Increase denoising steps for better quality:
|
||||
```bash
|
||||
uv run example_onnx.py \
|
||||
--total-step 10 \
|
||||
--voice-style assets/voice_styles/M1.json \
|
||||
--voice-style ../assets/voice_styles/M1.json \
|
||||
--text "Increasing the number of denoising steps improves the output's fidelity and overall quality."
|
||||
```
|
||||
|
||||
This will:
|
||||
- Use 10 denoising steps instead of the default 5
|
||||
- Use 10 denoising steps instead of the default 8
|
||||
- Produce higher quality output at the cost of slower inference
|
||||
|
||||
### Example 4: Long-Form Inference
|
||||
For long texts, the system automatically chunks the text into manageable segments and generates a single audio file:
|
||||
```bash
|
||||
uv run example_onnx.py \
|
||||
--voice-style assets/voice_styles/M1.json \
|
||||
--voice-style ../assets/voice_styles/M1.json \
|
||||
--text "Once upon a time, in a small village nestled between rolling hills, there lived a young artist named Clara. Every morning, she would wake up before dawn to capture the first light of day. The golden rays streaming through her window inspired countless paintings. Her work was known throughout the region for its vibrant colors and emotional depth. People from far and wide came to see her gallery, and many said her paintings could tell stories that words never could."
|
||||
```
|
||||
|
||||
@@ -94,13 +103,13 @@ Control the speed of speech synthesis:
|
||||
```bash
|
||||
# Faster speech (speed > 1.0)
|
||||
uv run example_onnx.py \
|
||||
--voice-style assets/voice_styles/F2.json \
|
||||
--voice-style ../assets/voice_styles/F2.json \
|
||||
--text "This text will be synthesized at a faster pace." \
|
||||
--speed 1.2
|
||||
|
||||
# Slower speech (speed < 1.0)
|
||||
uv run example_onnx.py \
|
||||
--voice-style assets/voice_styles/M2.json \
|
||||
--voice-style ../assets/voice_styles/M2.json \
|
||||
--text "This text will be synthesized at a slower, more deliberate pace." \
|
||||
--speed 0.9
|
||||
```
|
||||
@@ -116,18 +125,20 @@ This will:
|
||||
| Argument | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `--use-gpu` | flag | False | Use GPU for inference (with CPU fallback) |
|
||||
| `--onnx-dir` | str | `assets/onnx` | Path to ONNX model directory |
|
||||
| `--total-step` | int | 5 | Number of denoising steps (higher = better quality, slower) |
|
||||
| `--onnx-dir` | str | `../assets/onnx` | Path to ONNX model directory |
|
||||
| `--total-step` | int | 8 | Number of denoising steps (higher = better quality, slower) |
|
||||
| `--speed` | float | 1.05 | Speech speed factor (higher = faster, lower = slower) |
|
||||
| `--n-test` | int | 4 | Number of times to generate each sample |
|
||||
| `--voice-style` | str+ | `assets/voice_styles/M1.json` | Voice style file path(s) |
|
||||
| `--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); see the main README for all 31 codes |
|
||||
| `--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: 31 languages; see the main README for the full list
|
||||
- **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
|
||||
|
||||
@@ -18,13 +18,13 @@ def parse_args():
|
||||
parser.add_argument(
|
||||
"--onnx-dir",
|
||||
type=str,
|
||||
default="assets/onnx",
|
||||
default="../assets/onnx",
|
||||
help="Path to ONNX model directory",
|
||||
)
|
||||
|
||||
# Synthesis parameters
|
||||
parser.add_argument(
|
||||
"--total-step", type=int, default=5, help="Number of denoising steps"
|
||||
"--total-step", type=int, default=8, help="Number of denoising steps"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--speed",
|
||||
@@ -44,7 +44,7 @@ def parse_args():
|
||||
"--voice-style",
|
||||
type=str,
|
||||
nargs="+",
|
||||
default=["assets/voice_styles/M1.json"],
|
||||
default=["../assets/voice_styles/M1.json"],
|
||||
help="Voice style file path(s). Can specify multiple files for batch processing",
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -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):
|
||||
|
||||
17
py/example_pypi.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from supertonic import TTS
|
||||
|
||||
# Note: First run downloads model automatically (~260MB)
|
||||
tts = TTS(auto_download=True)
|
||||
|
||||
# Get a voice style
|
||||
style = tts.get_voice_style(voice_name="M4")
|
||||
|
||||
# Generate speech
|
||||
text = "This morning, I took a walk in the park, and the sound of the birds and the breeze was so pleasant that I stopped for a long time just to listen."
|
||||
wav, duration = tts.synthesize(text, voice_style=style)
|
||||
# wav: np.ndarray, shape = (1, num_samples)
|
||||
# duration: np.ndarray, shape = (1,)
|
||||
|
||||
# Save to file
|
||||
tts.save_audio(wav, "results/example_pypi.wav")
|
||||
print(f"Generated {duration[0]:.2f}s of audio")
|
||||
63
py/helper.py
@@ -10,18 +10,18 @@ import onnxruntime as ort
|
||||
|
||||
import re
|
||||
|
||||
AVAILABLE_LANGS = ["en", "ko", "ja", "ar", "bg", "cs", "da", "de", "el", "es", "et", "fi", "fr", "hi", "hr", "hu", "id", "it", "lt", "lv", "nl", "pl", "pt", "ro", "ru", "sk", "sl", "sv", "tr", "uk", "vi", "na"]
|
||||
|
||||
|
||||
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,12 +45,11 @@ class UnicodeProcessor:
|
||||
"–": "-",
|
||||
"‑": "-",
|
||||
"—": "-",
|
||||
"¯": " ",
|
||||
"_": " ",
|
||||
"“": '"',
|
||||
"”": '"',
|
||||
"‘": "'",
|
||||
"’": "'",
|
||||
"\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 in ("ko", "ja") 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]:
|
||||
|
||||
@@ -8,7 +8,7 @@ edition = "2021"
|
||||
ort = "2.0.0-rc.7"
|
||||
|
||||
# Array processing (like NumPy)
|
||||
ndarray = { version = "0.16", features = ["rayon"] }
|
||||
ndarray = { version = "0.17", features = ["rayon"] }
|
||||
rand = "0.8"
|
||||
rand_distr = "0.4"
|
||||
|
||||
@@ -41,4 +41,3 @@ libc = "0.2"
|
||||
[[bin]]
|
||||
name = "example_onnx"
|
||||
path = "src/example_onnx.rs"
|
||||
|
||||
|
||||
@@ -4,6 +4,12 @@ This guide provides examples for running TTS inference using Rust.
|
||||
|
||||
## 📰 Update News
|
||||
|
||||
**2026.04.29** - 🎉 **Supertonic 3** released with 31-language support, improved reading accuracy, and v2-compatible public ONNX assets. [Demo](https://huggingface.co/spaces/Supertone/supertonic-3) | [Models](https://huggingface.co/Supertone/supertonic-3)
|
||||
|
||||
**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
|
||||
|
||||
**2025.12.08** - Optimized ONNX models via [OnnxSlim](https://github.com/inisis/OnnxSlim) now available on [Hugging Face Models](https://huggingface.co/Supertone/supertonic)
|
||||
|
||||
**2025.11.23** - Enhanced text preprocessing with comprehensive normalization, emoji removal, symbol replacement, and punctuation handling for improved synthesis quality.
|
||||
|
||||
**2025.11.19** - Added `--speed` parameter to control speech synthesis speed (default: 1.05, recommended range: 0.9-1.5).
|
||||
@@ -41,10 +47,10 @@ cargo run --release --bin example_onnx
|
||||
```
|
||||
|
||||
This will use:
|
||||
- Voice style: `assets/voice_styles/M1.json`
|
||||
- Voice style: `../assets/voice_styles/M1.json`
|
||||
- Text: "This morning, I took a walk in the park, and the sound of the birds and the breeze was so pleasant that I stopped for a long time just to listen."
|
||||
- Output directory: `results/`
|
||||
- Total steps: 5
|
||||
- Total steps: 8
|
||||
- Number of generations: 4
|
||||
|
||||
### Example 2: Batch Inference
|
||||
@@ -53,20 +59,22 @@ Process multiple voice styles and texts at once:
|
||||
# Using cargo run
|
||||
cargo run --release --bin example_onnx -- \
|
||||
--batch \
|
||||
--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."
|
||||
--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.|오늘 아침에 공원을 산책했는데, 새소리와 바람 소리가 너무 기분 좋았어요." \
|
||||
--lang en,ko
|
||||
|
||||
# Or using the binary directly
|
||||
./target/release/example_onnx \
|
||||
--batch \
|
||||
--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."
|
||||
--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.|오늘 아침에 공원을 산책했는데, 새소리와 바람 소리가 너무 기분 좋았어요." \
|
||||
--lang en,ko
|
||||
```
|
||||
|
||||
This will:
|
||||
- Generate speech for 2 different voice-text pairs
|
||||
- Use male voice (M1.json) for the first text
|
||||
- Use female voice (F1.json) for the second text
|
||||
- Generate speech for 2 different voice-text-language pairs
|
||||
- Use male voice (M1.json) for the first text in English
|
||||
- Use female voice (F1.json) for the second text in Korean
|
||||
- Process both samples in a single batch
|
||||
|
||||
### Example 3: High Quality Inference
|
||||
@@ -75,18 +83,18 @@ Increase denoising steps for better quality:
|
||||
# Using cargo run
|
||||
cargo run --release --bin example_onnx -- \
|
||||
--total-step 10 \
|
||||
--voice-style assets/voice_styles/M1.json \
|
||||
--voice-style ../assets/voice_styles/M1.json \
|
||||
--text "Increasing the number of denoising steps improves the output's fidelity and overall quality."
|
||||
|
||||
# Or using the binary directly
|
||||
./target/release/example_onnx \
|
||||
--total-step 10 \
|
||||
--voice-style assets/voice_styles/M1.json \
|
||||
--voice-style ../assets/voice_styles/M1.json \
|
||||
--text "Increasing the number of denoising steps improves the output's fidelity and overall quality."
|
||||
```
|
||||
|
||||
This will:
|
||||
- Use 10 denoising steps instead of the default 5
|
||||
- Use 10 denoising steps instead of the default 8
|
||||
- Produce higher quality output at the cost of slower inference
|
||||
|
||||
### Example 4: Long-Form Inference
|
||||
@@ -95,12 +103,12 @@ The system automatically chunks long texts into manageable segments, synthesizes
|
||||
```bash
|
||||
# Using cargo run
|
||||
cargo run --release --bin example_onnx -- \
|
||||
--voice-style assets/voice_styles/M1.json \
|
||||
--voice-style ../assets/voice_styles/M1.json \
|
||||
--text "This is a very long text that will be automatically split into multiple chunks. The system will process each chunk separately and then concatenate them together with natural pauses between segments. This ensures that even very long texts can be processed efficiently while maintaining natural speech flow and avoiding memory issues."
|
||||
|
||||
# Or using the binary directly
|
||||
./target/release/example_onnx \
|
||||
--voice-style assets/voice_styles/M1.json \
|
||||
--voice-style ../assets/voice_styles/M1.json \
|
||||
--text "This is a very long text that will be automatically split into multiple chunks. The system will process each chunk separately and then concatenate them together with natural pauses between segments. This ensures that even very long texts can be processed efficiently while maintaining natural speech flow and avoiding memory issues."
|
||||
```
|
||||
|
||||
@@ -117,17 +125,19 @@ This will:
|
||||
| Argument | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `--use-gpu` | flag | False | Use GPU for inference (default: CPU) |
|
||||
| `--onnx-dir` | str | `assets/onnx` | Path to ONNX model directory |
|
||||
| `--total-step` | int | 5 | Number of denoising steps (higher = better quality, slower) |
|
||||
| `--onnx-dir` | str | `../assets/onnx` | Path to ONNX model directory |
|
||||
| `--total-step` | int | 8 | Number of denoising steps (higher = better quality, slower) |
|
||||
| `--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 |
|
||||
| `--voice-style` | str+ | `../assets/voice_styles/M1.json` | Voice style file path(s), comma-separated |
|
||||
| `--text` | str+ | (long default text) | Text(s) to synthesize, pipe-separated |
|
||||
| `--lang` | str+ | `en` | Language(s) for synthesis, comma-separated; see the main README for all 31 codes |
|
||||
| `--save-dir` | str | `results` | Output directory |
|
||||
| `--batch` | flag | False | Enable batch mode (multiple text-style pairs, disables automatic chunking) |
|
||||
|
||||
## Notes
|
||||
|
||||
- **Batch Processing**: When using `--batch`, the number of `--voice-style` files must match the number of `--text` entries
|
||||
- **Multilingual Support**: Use `--lang` to specify the language for each text. Available: 31 languages; see the main README for the full list
|
||||
- **Batch Processing**: When using `--batch`, the number of `--voice-style`, `--text`, and `--lang` entries must match
|
||||
- **Automatic Chunking**: Without `--batch`, long texts are automatically split and concatenated with 0.3s pauses
|
||||
- **Quality vs Speed**: Higher `--total-step` values produce better quality but take longer
|
||||
- **GPU Support**: GPU mode is not supported yet
|
||||
|
||||
@@ -19,11 +19,11 @@ struct Args {
|
||||
use_gpu: bool,
|
||||
|
||||
/// Path to ONNX model directory
|
||||
#[arg(long, default_value = "assets/onnx")]
|
||||
#[arg(long, default_value = "../assets/onnx")]
|
||||
onnx_dir: String,
|
||||
|
||||
/// Number of denoising steps
|
||||
#[arg(long, default_value = "5")]
|
||||
#[arg(long, default_value = "8")]
|
||||
total_step: usize,
|
||||
|
||||
/// Speech speed factor (higher = faster)
|
||||
@@ -35,13 +35,17 @@ struct Args {
|
||||
n_test: usize,
|
||||
|
||||
/// Voice style file path(s)
|
||||
#[arg(long, value_delimiter = ',', default_values_t = vec!["assets/voice_styles/M1.json".to_string()])]
|
||||
#[arg(long, value_delimiter = ',', default_values_t = vec!["../assets/voice_styles/M1.json".to_string()])]
|
||||
voice_style: Vec<String>,
|
||||
|
||||
/// Text(s) to synthesize
|
||||
#[arg(long, value_delimiter = '|', default_values_t = vec!["This morning, I took a walk in the park, and the sound of the birds and the breeze was so pleasant that I stopped for a long time just to listen.".to_string()])]
|
||||
text: Vec<String>,
|
||||
|
||||
/// Language(s) for synthesis; see the main README for all supported codes
|
||||
#[arg(long, value_delimiter = ',', default_values_t = vec!["en".to_string()])]
|
||||
lang: Vec<String>,
|
||||
|
||||
/// Output directory
|
||||
#[arg(long, default_value = "results")]
|
||||
save_dir: String,
|
||||
@@ -61,6 +65,7 @@ fn main() -> Result<()> {
|
||||
let n_test = args.n_test;
|
||||
let voice_style_paths = &args.voice_style;
|
||||
let text_list = &args.text;
|
||||
let lang_list = &args.lang;
|
||||
let save_dir = &args.save_dir;
|
||||
let batch = args.batch;
|
||||
|
||||
@@ -72,6 +77,13 @@ fn main() -> Result<()> {
|
||||
text_list.len()
|
||||
);
|
||||
}
|
||||
if lang_list.len() != text_list.len() {
|
||||
anyhow::bail!(
|
||||
"Number of languages ({}) must match number of texts ({})",
|
||||
lang_list.len(),
|
||||
text_list.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let bsz = voice_style_paths.len();
|
||||
@@ -90,11 +102,11 @@ fn main() -> Result<()> {
|
||||
|
||||
let (wav, duration) = if batch {
|
||||
timer("Generating speech from text", || {
|
||||
text_to_speech.batch(text_list, &style, total_step, speed)
|
||||
text_to_speech.batch(text_list, lang_list, &style, total_step, speed)
|
||||
})?
|
||||
} else {
|
||||
let (w, d) = timer("Generating speech from text", || {
|
||||
text_to_speech.call(&text_list[0], &style, total_step, speed, 0.3)
|
||||
text_to_speech.call(&text_list[0], &lang_list[0], &style, total_step, speed, 0.3)
|
||||
})?;
|
||||
(w, vec![d])
|
||||
};
|
||||
|
||||
@@ -8,12 +8,19 @@ use serde_json;
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::path::Path;
|
||||
use anyhow::{Result, Context};
|
||||
use anyhow::{Result, Context, bail};
|
||||
use unicode_normalization::UnicodeNormalization;
|
||||
use hound::{WavWriter, WavSpec, SampleFormat};
|
||||
use rand_distr::{Distribution, Normal};
|
||||
use regex::Regex;
|
||||
|
||||
// Available languages for multilingual TTS
|
||||
pub const AVAILABLE_LANGS: &[&str] = &["en", "ko", "ja", "ar", "bg", "cs", "da", "de", "el", "es", "et", "fi", "fr", "hi", "hr", "hu", "id", "it", "lt", "lv", "nl", "pl", "pt", "ro", "ru", "sk", "sl", "sv", "tr", "uk", "vi", "na"];
|
||||
|
||||
pub fn is_valid_lang(lang: &str) -> bool {
|
||||
AVAILABLE_LANGS.contains(&lang)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Configuration Structures
|
||||
// ============================================================================
|
||||
@@ -79,11 +86,11 @@ impl UnicodeProcessor {
|
||||
Ok(UnicodeProcessor { indexer })
|
||||
}
|
||||
|
||||
pub fn call(&self, text_list: &[String]) -> (Vec<Vec<i64>>, Array3<f32>) {
|
||||
let processed_texts: Vec<String> = text_list
|
||||
.iter()
|
||||
.map(|t| preprocess_text(t))
|
||||
.collect();
|
||||
pub fn call(&self, text_list: &[String], lang_list: &[String]) -> Result<(Vec<Vec<i64>>, Array3<f32>)> {
|
||||
let mut processed_texts: Vec<String> = Vec::new();
|
||||
for (text, lang) in text_list.iter().zip(lang_list.iter()) {
|
||||
processed_texts.push(preprocess_text(text, lang)?);
|
||||
}
|
||||
|
||||
let text_ids_lengths: Vec<usize> = processed_texts
|
||||
.iter()
|
||||
@@ -108,16 +115,14 @@ impl UnicodeProcessor {
|
||||
|
||||
let text_mask = get_text_mask(&text_ids_lengths);
|
||||
|
||||
(text_ids, text_mask)
|
||||
Ok((text_ids, text_mask))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn preprocess_text(text: &str) -> String {
|
||||
pub fn preprocess_text(text: &str, lang: &str) -> Result<String> {
|
||||
// TODO: Need advanced normalizer for better performance
|
||||
let mut text: String = text.nfkd().collect();
|
||||
|
||||
// FIXME: this should be fixed for non-English languages
|
||||
|
||||
// Remove emojis (wide Unicode range)
|
||||
let emoji_pattern = Regex::new(r"[\x{1F600}-\x{1F64F}\x{1F300}-\x{1F5FF}\x{1F680}-\x{1F6FF}\x{1F700}-\x{1F77F}\x{1F780}-\x{1F7FF}\x{1F800}-\x{1F8FF}\x{1F900}-\x{1F9FF}\x{1FA00}-\x{1FA6F}\x{1FA70}-\x{1FAFF}\x{2600}-\x{26FF}\x{2700}-\x{27BF}\x{1F1E6}-\x{1F1FF}]+").unwrap();
|
||||
text = emoji_pattern.replace_all(&text, "").to_string();
|
||||
@@ -127,7 +132,6 @@ pub fn preprocess_text(text: &str) -> String {
|
||||
("–", "-"), // en dash
|
||||
("‑", "-"), // non-breaking hyphen
|
||||
("—", "-"), // em dash
|
||||
("¯", " "), // macron
|
||||
("_", " "), // underscore
|
||||
("\u{201C}", "\""), // left double quote
|
||||
("\u{201D}", "\""), // right double quote
|
||||
@@ -148,10 +152,6 @@ pub fn preprocess_text(text: &str) -> String {
|
||||
text = text.replace(from, to);
|
||||
}
|
||||
|
||||
// Remove combining diacritics // FIXME: this should be fixed for non-English languages
|
||||
let diacritics_pattern = Regex::new(r"[\u{0302}\u{0303}\u{0304}\u{0305}\u{0306}\u{0307}\u{0308}\u{030A}\u{030B}\u{030C}\u{0327}\u{0328}\u{0329}\u{032A}\u{032B}\u{032C}\u{032D}\u{032E}\u{032F}]").unwrap();
|
||||
text = diacritics_pattern.replace_all(&text, "").to_string();
|
||||
|
||||
// Remove special symbols
|
||||
let special_symbols = ["♥", "☆", "♡", "©", "\\"];
|
||||
for symbol in &special_symbols {
|
||||
@@ -201,7 +201,15 @@ pub fn preprocess_text(text: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
text
|
||||
// Validate language
|
||||
if !is_valid_lang(lang) {
|
||||
bail!("Invalid language: {}. Available: {:?}", lang, AVAILABLE_LANGS);
|
||||
}
|
||||
|
||||
// Wrap text with language tags
|
||||
text = format!("<{}>{}</{}>", lang, text, lang);
|
||||
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
pub fn text_to_unicode_values(text: &str) -> Vec<usize> {
|
||||
@@ -505,15 +513,12 @@ where
|
||||
}
|
||||
|
||||
pub fn sanitize_filename(text: &str, max_len: usize) -> String {
|
||||
let text = if text.len() > max_len {
|
||||
&text[..max_len]
|
||||
} else {
|
||||
text
|
||||
};
|
||||
|
||||
// Take first max_len characters (Unicode code points, not bytes)
|
||||
text.chars()
|
||||
.take(max_len)
|
||||
.map(|c| {
|
||||
if c.is_ascii_alphanumeric() {
|
||||
// is_alphanumeric() works with all Unicode letters and digits
|
||||
if c.is_alphanumeric() {
|
||||
c
|
||||
} else {
|
||||
'_'
|
||||
@@ -570,6 +575,7 @@ impl TextToSpeech {
|
||||
fn _infer(
|
||||
&mut self,
|
||||
text_list: &[String],
|
||||
lang_list: &[String],
|
||||
style: &Style,
|
||||
total_step: usize,
|
||||
speed: f32,
|
||||
@@ -577,7 +583,7 @@ impl TextToSpeech {
|
||||
let bsz = text_list.len();
|
||||
|
||||
// Process text
|
||||
let (text_ids, text_mask) = self.text_processor.call(text_list);
|
||||
let (text_ids, text_mask) = self.text_processor.call(text_list, lang_list)?;
|
||||
|
||||
let text_ids_array = {
|
||||
let text_ids_shape = (bsz, text_ids[0].len());
|
||||
@@ -676,18 +682,20 @@ impl TextToSpeech {
|
||||
pub fn call(
|
||||
&mut self,
|
||||
text: &str,
|
||||
lang: &str,
|
||||
style: &Style,
|
||||
total_step: usize,
|
||||
speed: f32,
|
||||
silence_duration: f32,
|
||||
) -> Result<(Vec<f32>, f32)> {
|
||||
let chunks = chunk_text(text, None);
|
||||
let max_len = if lang == "ko" || lang == "ja" { 120 } else { 300 };
|
||||
let chunks = chunk_text(text, Some(max_len));
|
||||
|
||||
let mut wav_cat: Vec<f32> = Vec::new();
|
||||
let mut dur_cat: f32 = 0.0;
|
||||
|
||||
for (i, chunk) in chunks.iter().enumerate() {
|
||||
let (wav, duration) = self._infer(&[chunk.clone()], style, total_step, speed)?;
|
||||
let (wav, duration) = self._infer(&[chunk.clone()], &[lang.to_string()], style, total_step, speed)?;
|
||||
|
||||
let dur = duration[0];
|
||||
let wav_len = (self.sample_rate as f32 * dur) as usize;
|
||||
@@ -712,11 +720,12 @@ impl TextToSpeech {
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
text_list: &[String],
|
||||
lang_list: &[String],
|
||||
style: &Style,
|
||||
total_step: usize,
|
||||
speed: f32,
|
||||
) -> Result<(Vec<f32>, Vec<f32>)> {
|
||||
self._infer(text_list, style, total_step, speed)
|
||||
self._infer(text_list, lang_list, style, total_step, speed)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,12 @@ This guide provides examples for running TTS inference using `example_onnx`.
|
||||
|
||||
## 📰 Update News
|
||||
|
||||
**2026.04.29** - 🎉 **Supertonic 3** released with 31-language support, improved reading accuracy, and v2-compatible public ONNX assets. [Demo](https://huggingface.co/spaces/Supertone/supertonic-3) | [Models](https://huggingface.co/Supertone/supertonic-3)
|
||||
|
||||
**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
|
||||
|
||||
**2025.12.08** - Optimized ONNX models via [OnnxSlim](https://github.com/inisis/OnnxSlim) now available on [Hugging Face Models](https://huggingface.co/Supertone/supertonic)
|
||||
|
||||
**2025.11.23** - Enhanced text preprocessing with comprehensive normalization, emoji removal, symbol replacement, and punctuation handling for improved synthesis quality.
|
||||
|
||||
**2025.11.19** - Added `--speed` parameter to control speech synthesis speed (default: 1.05, recommended range: 0.9-1.5).
|
||||
@@ -32,10 +38,10 @@ Run inference with default settings:
|
||||
```
|
||||
|
||||
This will use:
|
||||
- Voice style: `assets/voice_styles/M1.json`
|
||||
- Voice style: `../assets/voice_styles/M1.json`
|
||||
- Text: "This morning, I took a walk in the park, and the sound of the birds and the breeze was so pleasant that I stopped for a long time just to listen."
|
||||
- Output directory: `results/`
|
||||
- Total steps: 5
|
||||
- Total steps: 8
|
||||
- Number of generations: 4
|
||||
|
||||
### Example 2: Batch Inference
|
||||
@@ -43,14 +49,15 @@ Process multiple voice styles and texts at once:
|
||||
```bash
|
||||
.build/release/example_onnx \
|
||||
--batch \
|
||||
--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."
|
||||
--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.|오늘 아침에 공원을 산책했는데, 새소리와 바람 소리가 너무 기분 좋았어요." \
|
||||
--lang en,ko
|
||||
```
|
||||
|
||||
This will:
|
||||
- Generate speech for 2 different voice-text pairs
|
||||
- Use male voice (M1.json) for the first text
|
||||
- Use female voice (F1.json) for the second text
|
||||
- Generate speech for 2 different voice-text-language triplets
|
||||
- Use male voice (M1.json) for the first English text
|
||||
- Use female voice (F1.json) for the second Korean text
|
||||
- Process both samples in a single batch
|
||||
|
||||
### Example 3: High Quality Inference
|
||||
@@ -58,12 +65,12 @@ Increase denoising steps for better quality:
|
||||
```bash
|
||||
.build/release/example_onnx \
|
||||
--total-step 10 \
|
||||
--voice-style assets/voice_styles/M1.json \
|
||||
--voice-style ../assets/voice_styles/M1.json \
|
||||
--text "Increasing the number of denoising steps improves the output's fidelity and overall quality."
|
||||
```
|
||||
|
||||
This will:
|
||||
- Use 10 denoising steps instead of the default 5
|
||||
- Use 10 denoising steps instead of the default 8
|
||||
- Produce higher quality output at the cost of slower inference
|
||||
|
||||
### Example 4: Long-Form Inference
|
||||
@@ -71,7 +78,7 @@ The system automatically chunks long texts into manageable segments, synthesizes
|
||||
|
||||
```bash
|
||||
.build/release/example_onnx \
|
||||
--voice-style assets/voice_styles/M1.json \
|
||||
--voice-style ../assets/voice_styles/M1.json \
|
||||
--text "This is a very long text that will be automatically split into multiple chunks. The system will process each chunk separately and then concatenate them together with natural pauses between segments. This ensures that even very long texts can be processed efficiently while maintaining natural speech flow and avoiding memory issues."
|
||||
```
|
||||
|
||||
@@ -88,17 +95,22 @@ This will:
|
||||
| Argument | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `--use-gpu` | flag | False | Use GPU for inference (default: CPU) |
|
||||
| `--onnx-dir` | str | `assets/onnx` | Path to ONNX model directory |
|
||||
| `--total-step` | int | 5 | Number of denoising steps (higher = better quality, slower) |
|
||||
| `--onnx-dir` | str | `../assets/onnx` | Path to ONNX model directory |
|
||||
| `--total-step` | int | 8 | Number of denoising steps (higher = better quality, slower) |
|
||||
| `--n-test` | int | 4 | Number of times to generate each sample |
|
||||
| `--voice-style` | str+ | `assets/voice_styles/M1.json` | Voice style file path(s) |
|
||||
| `--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 synthesis; see the main README for all 31 codes |
|
||||
| `--save-dir` | str | `results` | Output directory |
|
||||
| `--batch` | flag | False | Enable batch mode (multiple text-style pairs, disables automatic chunking) |
|
||||
| `--batch` | flag | False | Enable batch mode (multiple text-style-lang triplets, disables automatic chunking) |
|
||||
|
||||
## Multilingual Support
|
||||
|
||||
Supertonic 3 supports 31 languages. Use the `--lang` argument to specify the language; see the main README for the full code list.
|
||||
|
||||
## Notes
|
||||
|
||||
- **Batch Processing**: When using `--batch`, the number of `--voice-style` files must match the number of `--text` entries
|
||||
- **Batch Processing**: When using `--batch`, the number of `--voice-style`, `--text`, and `--lang` entries must match
|
||||
- **Automatic Chunking**: Without `--batch`, long texts are automatically split and concatenated with 0.3s pauses
|
||||
- **Quality vs Speed**: Higher `--total-step` values produce better quality but take longer
|
||||
- **GPU Support**: GPU mode is not supported yet
|
||||
- **GPU Support**: GPU mode is not supported yet
|
||||
|
||||
@@ -3,12 +3,13 @@ import OnnxRuntimeBindings
|
||||
|
||||
struct Args {
|
||||
var useGpu: Bool = false
|
||||
var onnxDir: String = "assets/onnx"
|
||||
var totalStep: Int = 5
|
||||
var onnxDir: String = "../assets/onnx"
|
||||
var totalStep: Int = 8
|
||||
var speed: Float = 1.05
|
||||
var nTest: Int = 4
|
||||
var voiceStyle: [String] = ["assets/voice_styles/M1.json"]
|
||||
var voiceStyle: [String] = ["../assets/voice_styles/M1.json"]
|
||||
var text: [String] = ["This morning, I took a walk in the park, and the sound of the birds and the breeze was so pleasant that I stopped for a long time just to listen."]
|
||||
var lang: [String] = ["en"]
|
||||
var saveDir: String = "results"
|
||||
var batch: Bool = false
|
||||
}
|
||||
@@ -31,7 +32,7 @@ func parseArgs() -> Args {
|
||||
}
|
||||
case "--total-step":
|
||||
if i + 1 < arguments.count {
|
||||
args.totalStep = Int(arguments[i + 1]) ?? 5
|
||||
args.totalStep = Int(arguments[i + 1]) ?? 8
|
||||
i += 1
|
||||
}
|
||||
case "--speed":
|
||||
@@ -54,6 +55,11 @@ func parseArgs() -> Args {
|
||||
args.text = arguments[i + 1].components(separatedBy: "|")
|
||||
i += 1
|
||||
}
|
||||
case "--lang":
|
||||
if i + 1 < arguments.count {
|
||||
args.lang = arguments[i + 1].components(separatedBy: ",")
|
||||
i += 1
|
||||
}
|
||||
case "--save-dir":
|
||||
if i + 1 < arguments.count {
|
||||
args.saveDir = arguments[i + 1]
|
||||
@@ -84,6 +90,10 @@ struct ExampleONNX {
|
||||
print("Error: Number of voice styles (\(args.voiceStyle.count)) must match number of texts (\(args.text.count))")
|
||||
return
|
||||
}
|
||||
guard args.lang.count == args.text.count else {
|
||||
print("Error: Number of languages (\(args.lang.count)) must match number of texts (\(args.text.count))")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let bsz = args.voiceStyle.count
|
||||
@@ -108,13 +118,13 @@ struct ExampleONNX {
|
||||
|
||||
if args.batch {
|
||||
let result = try timer("Generating speech from text") {
|
||||
try textToSpeech.batch(args.text, style, args.totalStep, speed: args.speed)
|
||||
try textToSpeech.batch(args.text, args.lang, style, args.totalStep, speed: args.speed)
|
||||
}
|
||||
wav = result.wav
|
||||
duration = result.duration
|
||||
} else {
|
||||
let result = try timer("Generating speech from text") {
|
||||
try textToSpeech.call(args.text[0], style, args.totalStep, speed: args.speed, silenceDuration: 0.3)
|
||||
try textToSpeech.call(args.text[0], args.lang[0], style, args.totalStep, speed: args.speed, silenceDuration: 0.3)
|
||||
}
|
||||
wav = result.wav
|
||||
duration = [result.duration]
|
||||
|
||||
@@ -2,6 +2,14 @@ import Foundation
|
||||
import Accelerate
|
||||
import OnnxRuntimeBindings
|
||||
|
||||
// MARK: - Available Languages
|
||||
|
||||
let AVAILABLE_LANGS = ["en", "ko", "ja", "ar", "bg", "cs", "da", "de", "el", "es", "et", "fi", "fr", "hi", "hr", "hu", "id", "it", "lt", "lv", "nl", "pl", "pt", "ro", "ru", "sk", "sl", "sv", "tr", "uk", "vi", "na"]
|
||||
|
||||
func isValidLang(_ lang: String) -> Bool {
|
||||
return AVAILABLE_LANGS.contains(lang)
|
||||
}
|
||||
|
||||
// MARK: - Configuration Structures
|
||||
|
||||
struct Config: Codable {
|
||||
@@ -42,12 +50,16 @@ class UnicodeProcessor {
|
||||
self.indexer = try JSONDecoder().decode([Int64].self, from: data)
|
||||
}
|
||||
|
||||
func call(_ textList: [String]) -> (textIds: [[Int64]], textMask: [[[Float]]]) {
|
||||
let processedTexts = textList.map { preprocessText($0) }
|
||||
func call(_ textList: [String], _ langList: [String]) -> (textIds: [[Int64]], textMask: [[[Float]]]) {
|
||||
var processedTexts = [String]()
|
||||
for (i, text) in textList.enumerated() {
|
||||
processedTexts.append(preprocessText(text, lang: langList[i]))
|
||||
}
|
||||
|
||||
// Use unicodeScalars.count for correct length after NFKD decomposition
|
||||
var textIdsLengths = [Int]()
|
||||
for text in processedTexts {
|
||||
textIdsLengths.append(text.count)
|
||||
textIdsLengths.append(text.unicodeScalars.count)
|
||||
}
|
||||
|
||||
let maxLen = textIdsLengths.max() ?? 0
|
||||
@@ -71,11 +83,9 @@ class UnicodeProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
func preprocessText(_ text: String) -> String {
|
||||
// TODO: Need advanced normalizer for better performance
|
||||
var text = text.precomposedStringWithCompatibilityMapping
|
||||
|
||||
// FIXME: this should be fixed for non-English languages
|
||||
func preprocessText(_ text: String, lang: String) -> String {
|
||||
// Use NFKD (decomposed) for proper Hangul Jamo decomposition
|
||||
var text = text.decomposedStringWithCompatibilityMapping
|
||||
|
||||
// Remove emojis (wide Unicode range)
|
||||
// Swift NSRegularExpression doesn't support Unicode escapes above \uFFFF
|
||||
@@ -101,7 +111,6 @@ func preprocessText(_ text: String) -> String {
|
||||
"–": "-", // en dash
|
||||
"‑": "-", // non-breaking hyphen
|
||||
"—": "-", // em dash
|
||||
"¯": " ", // macron
|
||||
"_": " ", // underscore
|
||||
"\u{201C}": "\"", // left double quote
|
||||
"\u{201D}": "\"", // right double quote
|
||||
@@ -122,11 +131,6 @@ func preprocessText(_ text: String) -> String {
|
||||
text = text.replacingOccurrences(of: old, with: new)
|
||||
}
|
||||
|
||||
// Remove combining diacritics // FIXME: this should be fixed for non-English languages
|
||||
let diacriticsPattern = try! NSRegularExpression(pattern: "[\\u0302\\u0303\\u0304\\u0305\\u0306\\u0307\\u0308\\u030A\\u030B\\u030C\\u0327\\u0328\\u0329\\u032A\\u032B\\u032C\\u032D\\u032E\\u032F]")
|
||||
let diacriticsRange = NSRange(text.startIndex..., in: text)
|
||||
text = diacriticsPattern.stringByReplacingMatches(in: text, range: diacriticsRange, withTemplate: "")
|
||||
|
||||
// Remove special symbols
|
||||
let specialSymbols = ["♥", "☆", "♡", "©", "\\"]
|
||||
for symbol in specialSymbols {
|
||||
@@ -179,6 +183,14 @@ func preprocessText(_ text: String) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate language
|
||||
guard isValidLang(lang) else {
|
||||
fatalError("Invalid language: \(lang). Available: \(AVAILABLE_LANGS.joined(separator: ", "))")
|
||||
}
|
||||
|
||||
// Wrap text with language tags
|
||||
text = "<\(lang)>\(text)</\(lang)>"
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
@@ -560,11 +572,11 @@ class TextToSpeech {
|
||||
self.sampleRate = cfgs.ae.sample_rate
|
||||
}
|
||||
|
||||
private func _infer(_ textList: [String], _ style: Style, _ totalStep: Int, speed: Float = 1.05) throws -> (wav: [Float], duration: [Float]) {
|
||||
private func _infer(_ textList: [String], _ langList: [String], _ style: Style, _ totalStep: Int, speed: Float = 1.05) throws -> (wav: [Float], duration: [Float]) {
|
||||
let bsz = textList.count
|
||||
|
||||
// Process text
|
||||
let (textIds, textMask) = textProcessor.call(textList)
|
||||
let (textIds, textMask) = textProcessor.call(textList, langList)
|
||||
|
||||
// Flatten text IDs
|
||||
let textIdsFlat = textIds.flatMap { $0 }
|
||||
@@ -688,14 +700,16 @@ class TextToSpeech {
|
||||
return (wav, duration)
|
||||
}
|
||||
|
||||
func call(_ text: String, _ style: Style, _ totalStep: Int, speed: Float = 1.05, silenceDuration: Float = 0.3) throws -> (wav: [Float], duration: Float) {
|
||||
let chunks = chunkText(text)
|
||||
func call(_ text: String, _ lang: String, _ style: Style, _ totalStep: Int, speed: Float = 1.05, silenceDuration: Float = 0.3) throws -> (wav: [Float], duration: Float) {
|
||||
let maxLen = (lang == "ko" || lang == "ja") ? 120 : 300
|
||||
let chunks = chunkText(text, maxLen: maxLen)
|
||||
let langList = Array(repeating: lang, count: chunks.count)
|
||||
|
||||
var wavCat = [Float]()
|
||||
var durCat: Float = 0.0
|
||||
|
||||
for (i, chunk) in chunks.enumerated() {
|
||||
let result = try _infer([chunk], style, totalStep, speed: speed)
|
||||
let result = try _infer([chunk], [langList[i]], style, totalStep, speed: speed)
|
||||
|
||||
let dur = result.duration[0]
|
||||
let wavLen = Int(Float(sampleRate) * dur)
|
||||
@@ -717,8 +731,8 @@ class TextToSpeech {
|
||||
return (wavCat, durCat)
|
||||
}
|
||||
|
||||
func batch(_ textList: [String], _ style: Style, _ totalStep: Int, speed: Float = 1.05) throws -> (wav: [Float], duration: [Float]) {
|
||||
return try _infer(textList, style, totalStep, speed: speed)
|
||||
func batch(_ textList: [String], _ langList: [String], _ style: Style, _ totalStep: Int, speed: Float = 1.05) throws -> (wav: [Float], duration: [Float]) {
|
||||
return try _infer(textList, langList, style, totalStep, speed: speed)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
88
test_all.sh
@@ -4,10 +4,16 @@
|
||||
# This script runs inference tests for all supported languages except web
|
||||
|
||||
set -e # Exit on error
|
||||
set -o pipefail
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
export UV_CACHE_DIR="${UV_CACHE_DIR:-$SCRIPT_DIR/.uv-cache}"
|
||||
export CLANG_MODULE_CACHE_PATH="${CLANG_MODULE_CACHE_PATH:-$SCRIPT_DIR/.clang-module-cache}"
|
||||
SWIFT_HOME="${SWIFT_HOME:-$SCRIPT_DIR/.swift-home}"
|
||||
mkdir -p "$UV_CACHE_DIR" "$CLANG_MODULE_CACHE_PATH" "$SWIFT_HOME"
|
||||
|
||||
echo "=================================="
|
||||
echo "Supertonic - Testing All Examples"
|
||||
echo "=================================="
|
||||
@@ -57,11 +63,13 @@ case $test_mode in
|
||||
esac
|
||||
echo ""
|
||||
|
||||
# Batch inference test data - base variables
|
||||
# Batch inference test data - multilingual examples
|
||||
BATCH_VOICE_STYLE_1="assets/voice_styles/M1.json"
|
||||
BATCH_VOICE_STYLE_2="assets/voice_styles/F1.json"
|
||||
BATCH_TEXT_1="The sun sets behind the mountains, painting the sky in shades of pink and orange."
|
||||
BATCH_TEXT_2="The weather is beautiful and sunny outside. A gentle breeze makes the air feel fresh and pleasant."
|
||||
BATCH_TEXT_2="오늘 아침에 공원을 산책했는데, 새소리와 바람 소리가 너무 기분 좋았어요."
|
||||
BATCH_LANG_1="en"
|
||||
BATCH_LANG_2="ko"
|
||||
|
||||
# Long-form inference test data
|
||||
LONGFORM_VOICE_STYLE="assets/voice_styles/M1.json"
|
||||
@@ -108,6 +116,28 @@ NC='\033[0m' # No Color
|
||||
declare -a PASSED=()
|
||||
declare -a FAILED=()
|
||||
|
||||
# Local toolchain fallbacks for Homebrew keg-only installs.
|
||||
DOTNET_CMD="${DOTNET_CMD:-dotnet}"
|
||||
if ! "$DOTNET_CMD" --list-runtimes 2>/dev/null | grep -q "Microsoft.NETCore.App 9\\."; then
|
||||
if [ -x "/opt/homebrew/opt/dotnet@9/bin/dotnet" ]; then
|
||||
DOTNET_CMD="/opt/homebrew/opt/dotnet@9/bin/dotnet"
|
||||
export DOTNET_ROOT="${DOTNET_ROOT:-/opt/homebrew/opt/dotnet@9/libexec}"
|
||||
elif [ -x "/usr/local/opt/dotnet@9/bin/dotnet" ]; then
|
||||
DOTNET_CMD="/usr/local/opt/dotnet@9/bin/dotnet"
|
||||
export DOTNET_ROOT="${DOTNET_ROOT:-/usr/local/opt/dotnet@9/libexec}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! javac -version >/dev/null 2>&1; then
|
||||
if [ -x "/opt/homebrew/opt/openjdk@17/bin/javac" ]; then
|
||||
export JAVA_HOME="/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home"
|
||||
export PATH="/opt/homebrew/opt/openjdk@17/bin:$PATH"
|
||||
elif [ -x "/usr/local/opt/openjdk@17/bin/javac" ]; then
|
||||
export JAVA_HOME="/usr/local/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home"
|
||||
export PATH="/usr/local/opt/openjdk@17/bin:$PATH"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Helper function to show statistics
|
||||
show_stats() {
|
||||
local name=$1
|
||||
@@ -179,10 +209,10 @@ if [ "$TEST_DEFAULT" = true ]; then
|
||||
run_test "Python (default)" "py" "uv run example_onnx.py"
|
||||
fi
|
||||
if [ "$TEST_BATCH" = true ]; then
|
||||
run_test "Python (batch)" "py" "uv run example_onnx.py --batch --voice-style $BATCH_VOICE_STYLE_1 $BATCH_VOICE_STYLE_2 --text '$BATCH_TEXT_1' '$BATCH_TEXT_2'"
|
||||
run_test "Python (batch)" "py" "uv run example_onnx.py --batch --voice-style ../$BATCH_VOICE_STYLE_1 ../$BATCH_VOICE_STYLE_2 --text '$BATCH_TEXT_1' '$BATCH_TEXT_2' --lang $BATCH_LANG_1 $BATCH_LANG_2"
|
||||
fi
|
||||
if [ "$TEST_LONGFORM" = true ]; then
|
||||
run_test "Python (long-form)" "py" "uv run example_onnx.py --voice-style $LONGFORM_VOICE_STYLE --text '$LONGFORM_TEXT'"
|
||||
run_test "Python (long-form)" "py" "uv run example_onnx.py --voice-style ../$LONGFORM_VOICE_STYLE --text '$LONGFORM_TEXT'"
|
||||
fi
|
||||
|
||||
# ====================================
|
||||
@@ -195,10 +225,10 @@ if [ "$TEST_DEFAULT" = true ]; then
|
||||
run_test "JavaScript (default)" "nodejs" "node example_onnx.js"
|
||||
fi
|
||||
if [ "$TEST_BATCH" = true ]; then
|
||||
run_test "JavaScript (batch)" "nodejs" "node example_onnx.js --batch --voice-style $BATCH_VOICE_STYLE_1,$BATCH_VOICE_STYLE_2 --text '$BATCH_TEXT_1|$BATCH_TEXT_2'"
|
||||
run_test "JavaScript (batch)" "nodejs" "node example_onnx.js --batch --voice-style ../$BATCH_VOICE_STYLE_1,../$BATCH_VOICE_STYLE_2 --text '$BATCH_TEXT_1|$BATCH_TEXT_2' --lang $BATCH_LANG_1,$BATCH_LANG_2"
|
||||
fi
|
||||
if [ "$TEST_LONGFORM" = true ]; then
|
||||
run_test "JavaScript (long-form)" "nodejs" "node example_onnx.js --voice-style $LONGFORM_VOICE_STYLE --text '$LONGFORM_TEXT'"
|
||||
run_test "JavaScript (long-form)" "nodejs" "node example_onnx.js --voice-style ../$LONGFORM_VOICE_STYLE --text '$LONGFORM_TEXT'"
|
||||
fi
|
||||
|
||||
# ====================================
|
||||
@@ -207,15 +237,20 @@ fi
|
||||
echo -e "${YELLOW}Testing Go...${NC}"
|
||||
echo "Cleaning Go cache..."
|
||||
cd go && go clean && cd ..
|
||||
export ONNXRUNTIME_LIB_PATH=$(brew --prefix onnxruntime 2>/dev/null)/lib/libonnxruntime.dylib
|
||||
if [ -z "${ONNXRUNTIME_LIB_PATH:-}" ] && command -v brew >/dev/null 2>&1; then
|
||||
ORT_PREFIX="$(brew --prefix onnxruntime 2>/dev/null || true)"
|
||||
if [ -n "$ORT_PREFIX" ]; then
|
||||
export ONNXRUNTIME_LIB_PATH="$ORT_PREFIX/lib/libonnxruntime.dylib"
|
||||
fi
|
||||
fi
|
||||
if [ "$TEST_DEFAULT" = true ]; then
|
||||
run_test "Go (default)" "go" "go run example_onnx.go helper.go"
|
||||
fi
|
||||
if [ "$TEST_BATCH" = true ]; then
|
||||
run_test "Go (batch)" "go" "go run example_onnx.go helper.go --batch -voice-style $BATCH_VOICE_STYLE_1,$BATCH_VOICE_STYLE_2 -text '$BATCH_TEXT_1|$BATCH_TEXT_2'"
|
||||
run_test "Go (batch)" "go" "go run example_onnx.go helper.go --batch -voice-style ../$BATCH_VOICE_STYLE_1,../$BATCH_VOICE_STYLE_2 -text '$BATCH_TEXT_1|$BATCH_TEXT_2' -lang $BATCH_LANG_1,$BATCH_LANG_2"
|
||||
fi
|
||||
if [ "$TEST_LONGFORM" = true ]; then
|
||||
run_test "Go (long-form)" "go" "go run example_onnx.go helper.go -voice-style $LONGFORM_VOICE_STYLE -text '$LONGFORM_TEXT'"
|
||||
run_test "Go (long-form)" "go" "go run example_onnx.go helper.go -voice-style ../$LONGFORM_VOICE_STYLE -text '$LONGFORM_TEXT'"
|
||||
fi
|
||||
|
||||
# ====================================
|
||||
@@ -228,10 +263,10 @@ if [ "$TEST_DEFAULT" = true ]; then
|
||||
run_test "Rust (default)" "rust" "cargo run --release"
|
||||
fi
|
||||
if [ "$TEST_BATCH" = true ]; then
|
||||
run_test "Rust (batch)" "rust" "cargo run --release -- --batch --voice-style $BATCH_VOICE_STYLE_1,$BATCH_VOICE_STYLE_2 --text '$BATCH_TEXT_1|$BATCH_TEXT_2'"
|
||||
run_test "Rust (batch)" "rust" "cargo run --release -- --batch --voice-style ../$BATCH_VOICE_STYLE_1,../$BATCH_VOICE_STYLE_2 --text '$BATCH_TEXT_1|$BATCH_TEXT_2' --lang $BATCH_LANG_1,$BATCH_LANG_2"
|
||||
fi
|
||||
if [ "$TEST_LONGFORM" = true ]; then
|
||||
run_test "Rust (long-form)" "rust" "cargo run --release -- --voice-style $LONGFORM_VOICE_STYLE --text '$LONGFORM_TEXT'"
|
||||
run_test "Rust (long-form)" "rust" "cargo run --release -- --voice-style ../$LONGFORM_VOICE_STYLE --text '$LONGFORM_TEXT'"
|
||||
fi
|
||||
|
||||
# ====================================
|
||||
@@ -239,15 +274,15 @@ fi
|
||||
# ====================================
|
||||
echo -e "${YELLOW}Testing C#...${NC}"
|
||||
echo "Building C# project..."
|
||||
cd csharp && dotnet clean && cd ..
|
||||
cd csharp && DOTNET_CLI_HOME="$SCRIPT_DIR/.dotnet" "$DOTNET_CMD" clean && cd ..
|
||||
if [ "$TEST_DEFAULT" = true ]; then
|
||||
run_test "C# (default)" "csharp" "dotnet run --configuration Release"
|
||||
run_test "C# (default)" "csharp" "DOTNET_CLI_HOME='$SCRIPT_DIR/.dotnet' '$DOTNET_CMD' run --configuration Release"
|
||||
fi
|
||||
if [ "$TEST_BATCH" = true ]; then
|
||||
run_test "C# (batch)" "csharp" "dotnet run --configuration Release -- --batch --voice-style ../$BATCH_VOICE_STYLE_1,../$BATCH_VOICE_STYLE_2 --text '$BATCH_TEXT_1|$BATCH_TEXT_2'"
|
||||
run_test "C# (batch)" "csharp" "DOTNET_CLI_HOME='$SCRIPT_DIR/.dotnet' '$DOTNET_CMD' run --configuration Release -- --batch --voice-style ../$BATCH_VOICE_STYLE_1,../$BATCH_VOICE_STYLE_2 --text '$BATCH_TEXT_1|$BATCH_TEXT_2' --lang $BATCH_LANG_1,$BATCH_LANG_2"
|
||||
fi
|
||||
if [ "$TEST_LONGFORM" = true ]; then
|
||||
run_test "C# (long-form)" "csharp" "dotnet run --configuration Release -- --voice-style ../$LONGFORM_VOICE_STYLE --text '$LONGFORM_TEXT'"
|
||||
run_test "C# (long-form)" "csharp" "DOTNET_CLI_HOME='$SCRIPT_DIR/.dotnet' '$DOTNET_CMD' run --configuration Release -- --voice-style ../$LONGFORM_VOICE_STYLE --text '$LONGFORM_TEXT'"
|
||||
fi
|
||||
|
||||
# ====================================
|
||||
@@ -255,15 +290,15 @@ fi
|
||||
# ====================================
|
||||
echo -e "${YELLOW}Testing Java...${NC}"
|
||||
echo "Building Java project..."
|
||||
cd java && mvn clean install -q && cd ..
|
||||
cd java && mvn -Dmaven.repo.local="$SCRIPT_DIR/.m2/repository" clean install -q && cd ..
|
||||
if [ "$TEST_DEFAULT" = true ]; then
|
||||
run_test "Java (default)" "java" "mvn exec:java -q"
|
||||
run_test "Java (default)" "java" "mvn -Dmaven.repo.local='$SCRIPT_DIR/.m2/repository' exec:java -q"
|
||||
fi
|
||||
if [ "$TEST_BATCH" = true ]; then
|
||||
run_test "Java (batch)" "java" "mvn exec:java -q -Dexec.args='--batch --voice-style $BATCH_VOICE_STYLE_1,$BATCH_VOICE_STYLE_2 --text \"$BATCH_TEXT_1|$BATCH_TEXT_2\"'"
|
||||
run_test "Java (batch)" "java" "mvn -Dmaven.repo.local='$SCRIPT_DIR/.m2/repository' exec:java -q -Dexec.args='--batch --voice-style ../$BATCH_VOICE_STYLE_1,../$BATCH_VOICE_STYLE_2 --text \"$BATCH_TEXT_1|$BATCH_TEXT_2\" --lang $BATCH_LANG_1,$BATCH_LANG_2'"
|
||||
fi
|
||||
if [ "$TEST_LONGFORM" = true ]; then
|
||||
run_test "Java (long-form)" "java" "mvn exec:java -q -Dexec.args='--voice-style $LONGFORM_VOICE_STYLE --text \"$LONGFORM_TEXT\"'"
|
||||
run_test "Java (long-form)" "java" "mvn -Dmaven.repo.local='$SCRIPT_DIR/.m2/repository' exec:java -q -Dexec.args='--voice-style ../$LONGFORM_VOICE_STYLE --text \"$LONGFORM_TEXT\"'"
|
||||
fi
|
||||
|
||||
# ====================================
|
||||
@@ -271,15 +306,15 @@ fi
|
||||
# ====================================
|
||||
echo -e "${YELLOW}Testing Swift...${NC}"
|
||||
echo "Building Swift project..."
|
||||
cd swift && swift build -c release && cd ..
|
||||
cd swift && HOME="$SWIFT_HOME" CLANG_MODULE_CACHE_PATH="$CLANG_MODULE_CACHE_PATH" swift build --disable-sandbox -c release && cd ..
|
||||
if [ "$TEST_DEFAULT" = true ]; then
|
||||
run_test "Swift (default)" "swift" ".build/release/example_onnx"
|
||||
run_test "Swift (default)" "swift" "HOME='$SWIFT_HOME' CLANG_MODULE_CACHE_PATH='$CLANG_MODULE_CACHE_PATH' .build/release/example_onnx"
|
||||
fi
|
||||
if [ "$TEST_BATCH" = true ]; then
|
||||
run_test "Swift (batch)" "swift" ".build/release/example_onnx --batch --voice-style $BATCH_VOICE_STYLE_1,$BATCH_VOICE_STYLE_2 --text '$BATCH_TEXT_1|$BATCH_TEXT_2'"
|
||||
run_test "Swift (batch)" "swift" "HOME='$SWIFT_HOME' CLANG_MODULE_CACHE_PATH='$CLANG_MODULE_CACHE_PATH' .build/release/example_onnx --batch --voice-style ../$BATCH_VOICE_STYLE_1,../$BATCH_VOICE_STYLE_2 --text '$BATCH_TEXT_1|$BATCH_TEXT_2' --lang $BATCH_LANG_1,$BATCH_LANG_2"
|
||||
fi
|
||||
if [ "$TEST_LONGFORM" = true ]; then
|
||||
run_test "Swift (long-form)" "swift" ".build/release/example_onnx --voice-style $LONGFORM_VOICE_STYLE --text '$LONGFORM_TEXT'"
|
||||
run_test "Swift (long-form)" "swift" "HOME='$SWIFT_HOME' CLANG_MODULE_CACHE_PATH='$CLANG_MODULE_CACHE_PATH' .build/release/example_onnx --voice-style ../$LONGFORM_VOICE_STYLE --text '$LONGFORM_TEXT'"
|
||||
fi
|
||||
|
||||
# ====================================
|
||||
@@ -287,15 +322,15 @@ fi
|
||||
# ====================================
|
||||
echo -e "${YELLOW}Testing C++...${NC}"
|
||||
echo "Building C++ project..."
|
||||
cd cpp && mkdir -p build && cd build && cmake .. && make && cd ../..
|
||||
cmake -S cpp -B cpp/build && cmake --build cpp/build --config Release
|
||||
if [ "$TEST_DEFAULT" = true ]; then
|
||||
run_test "C++ (default)" "cpp/build" "./example_onnx"
|
||||
fi
|
||||
if [ "$TEST_BATCH" = true ]; then
|
||||
run_test "C++ (batch)" "cpp/build" "./example_onnx --batch --voice-style ../$BATCH_VOICE_STYLE_1,../$BATCH_VOICE_STYLE_2 --text '$BATCH_TEXT_1|$BATCH_TEXT_2'"
|
||||
run_test "C++ (batch)" "cpp/build" "./example_onnx --batch --voice-style ../../$BATCH_VOICE_STYLE_1,../../$BATCH_VOICE_STYLE_2 --text '$BATCH_TEXT_1|$BATCH_TEXT_2' --lang $BATCH_LANG_1,$BATCH_LANG_2"
|
||||
fi
|
||||
if [ "$TEST_LONGFORM" = true ]; then
|
||||
run_test "C++ (long-form)" "cpp/build" "./example_onnx --voice-style ../$LONGFORM_VOICE_STYLE --text '$LONGFORM_TEXT'"
|
||||
run_test "C++ (long-form)" "cpp/build" "./example_onnx --voice-style ../../$LONGFORM_VOICE_STYLE --text '$LONGFORM_TEXT'"
|
||||
fi
|
||||
|
||||
# ====================================
|
||||
@@ -325,4 +360,3 @@ else
|
||||
echo -e "${GREEN}All tests passed! 🎉${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -4,6 +4,12 @@ This example demonstrates how to use Supertonic in a web browser using ONNX Runt
|
||||
|
||||
## 📰 Update News
|
||||
|
||||
**2026.04.29** - 🎉 **Supertonic 3** released with 31-language support, improved reading accuracy, and v2-compatible public ONNX assets. [Demo](https://huggingface.co/spaces/Supertone/supertonic-3) | [Models](https://huggingface.co/Supertone/supertonic-3)
|
||||
|
||||
**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
|
||||
|
||||
**2025.12.08** - Optimized ONNX models via [OnnxSlim](https://github.com/inisis/OnnxSlim) now available on [Hugging Face Models](https://huggingface.co/Supertone/supertonic)
|
||||
|
||||
**2025.11.23** - Enhanced text preprocessing with comprehensive normalization, emoji removal, symbol replacement, and punctuation handling for improved synthesis quality.
|
||||
|
||||
**2025.11.19** - Added speed control slider to adjust speech synthesis speed (default: 1.05, recommended range: 0.9-1.5).
|
||||
@@ -14,9 +20,10 @@ This example demonstrates how to use Supertonic in a web browser using ONNX Runt
|
||||
|
||||
- 🌐 Runs entirely in the browser (no server required for inference)
|
||||
- 🚀 WebGPU support with automatic fallback to WebAssembly
|
||||
- 🌍 Multilingual support: 31 languages
|
||||
- ⚡ Pre-extracted voice styles for instant generation
|
||||
- 🎨 Modern, responsive UI
|
||||
- 🎭 Multiple voice style presets (2 Male, 2 Female)
|
||||
- 🎭 Multiple voice style presets (5 Male, 5 Female)
|
||||
- 💾 Download generated audio as WAV files
|
||||
- 📊 Detailed generation statistics (audio length, generation time)
|
||||
- ⏱️ Real-time progress tracking
|
||||
@@ -48,20 +55,24 @@ This will start a local development server (usually at http://localhost:3000) an
|
||||
|
||||
1. **Wait for Models to Load**: The app will automatically load models and the default voice style (M1)
|
||||
2. **Select Voice Style**: Choose from available voice presets
|
||||
- **Male 1 (M1)**: Default male voice
|
||||
- **Male 2 (M2)**: Alternative male voice
|
||||
- **Female 1 (F1)**: Default female voice
|
||||
- **Female 2 (F2)**: Alternative female voice
|
||||
3. **Enter Text**: Type or paste the text you want to convert to speech
|
||||
4. **Adjust Settings** (optional):
|
||||
- **Total Steps**: More steps = better quality but slower (default: 5)
|
||||
5. **Generate Speech**: Click the "Generate Speech" button
|
||||
6. **View Results**:
|
||||
- **Male 1-5 (M1-M5)**: Male voice styles
|
||||
- **Female 1-5 (F1-F5)**: Female voice styles
|
||||
3. **Select Language**: Choose the language that matches your input text
|
||||
- Supertonic 3 supports 31 language codes; see the main README for the full list.
|
||||
4. **Enter Text**: Type or paste the text you want to convert to speech
|
||||
5. **Adjust Settings** (optional):
|
||||
- **Total Steps**: More steps = better quality but slower (default: 8)
|
||||
6. **Generate Speech**: Click the "Generate Speech" button
|
||||
7. **View Results**:
|
||||
- See the full input text
|
||||
- View audio length and generation time statistics
|
||||
- Play the generated audio in the browser
|
||||
- Download as WAV file
|
||||
|
||||
## Multilingual Support
|
||||
|
||||
Supertonic 3 supports 31 languages. Make sure to select the correct language for your input text to get the best results. The model will automatically handle text preprocessing and pronunciation for the selected language.
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Browser Compatibility
|
||||
@@ -76,7 +87,7 @@ This demo uses:
|
||||
- The ONNX models must be accessible at `assets/onnx/` relative to the web root
|
||||
- Voice style JSON files must be accessible at `assets/voice_styles/` relative to the web root
|
||||
- Pre-extracted voice styles enable instant generation without audio processing
|
||||
- Four voice style presets are provided (M1, M2, F1, F2)
|
||||
- Ten voice style presets are provided (M1-M5, F1-F5)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -103,4 +114,4 @@ This demo uses:
|
||||
### Slow generation
|
||||
- If using WebAssembly, try a browser that supports WebGPU
|
||||
- Ensure no other heavy processes are running
|
||||
- Consider using fewer denoising steps for faster (but lower quality) results
|
||||
- Consider using fewer denoising steps for faster (but lower quality) results
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import * as ort from 'onnxruntime-web';
|
||||
|
||||
// Available languages for multilingual TTS
|
||||
export const AVAILABLE_LANGS = ['en', 'ko', 'ja', 'ar', 'bg', 'cs', 'da', 'de', 'el', 'es', 'et', 'fi', 'fr', 'hi', 'hr', 'hu', 'id', 'it', 'lt', 'lv', 'nl', 'pl', 'pt', 'ro', 'ru', 'sk', 'sl', 'sv', 'tr', 'uk', 'vi', 'na'];
|
||||
|
||||
export function isValidLang(lang) {
|
||||
return AVAILABLE_LANGS.includes(lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unicode Text Processor
|
||||
*/
|
||||
@@ -8,8 +15,8 @@ export class UnicodeProcessor {
|
||||
this.indexer = indexer;
|
||||
}
|
||||
|
||||
call(textList) {
|
||||
const processedTexts = textList.map(text => this.preprocessText(text));
|
||||
call(textList, langList) {
|
||||
const processedTexts = textList.map((text, i) => this.preprocessText(text, langList[i]));
|
||||
|
||||
const textIdsLengths = processedTexts.map(text => text.length);
|
||||
const maxLen = Math.max(...textIdsLengths);
|
||||
@@ -27,12 +34,10 @@ export class UnicodeProcessor {
|
||||
return { textIds, textMask };
|
||||
}
|
||||
|
||||
preprocessText(text) {
|
||||
preprocessText(text, lang) {
|
||||
// TODO: Need advanced normalizer for better performance
|
||||
text = text.normalize('NFKD');
|
||||
|
||||
// FIXME: this should be fixed for non-English languages
|
||||
|
||||
// Remove emojis (wide Unicode range)
|
||||
const emojiPattern = /[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F700}-\u{1F77F}\u{1F780}-\u{1F7FF}\u{1F800}-\u{1F8FF}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F1E6}-\u{1F1FF}]+/gu;
|
||||
text = text.replace(emojiPattern, '');
|
||||
@@ -42,12 +47,11 @@ export class UnicodeProcessor {
|
||||
'–': '-',
|
||||
'‑': '-',
|
||||
'—': '-',
|
||||
'¯': ' ',
|
||||
'_': ' ',
|
||||
'"': '"',
|
||||
'"': '"',
|
||||
'\u2018': "'", // left single quote
|
||||
'\u2019': "'", // right single quote
|
||||
'\u201C': '"', // left double quote "
|
||||
'\u201D': '"', // right double quote "
|
||||
'\u2018': "'", // left single quote '
|
||||
'\u2019': "'", // right single quote '
|
||||
'´': "'",
|
||||
'`': "'",
|
||||
'[': ' ',
|
||||
@@ -62,9 +66,6 @@ export class UnicodeProcessor {
|
||||
text = text.replaceAll(k, v);
|
||||
}
|
||||
|
||||
// Remove combining diacritics // FIXME: this should be fixed for non-English languages
|
||||
text = text.replace(/[\u0302\u0303\u0304\u0305\u0306\u0307\u0308\u030A\u030B\u030C\u0327\u0328\u0329\u032A\u032B\u032C\u032D\u032E\u032F]/g, '');
|
||||
|
||||
// Remove special symbols
|
||||
text = text.replace(/[♥☆♡©\\]/g, '');
|
||||
|
||||
@@ -106,6 +107,14 @@ export class UnicodeProcessor {
|
||||
text += '.';
|
||||
}
|
||||
|
||||
// Validate language
|
||||
if (!isValidLang(lang)) {
|
||||
throw new Error(`Invalid language: ${lang}. Available: ${AVAILABLE_LANGS.join(', ')}`);
|
||||
}
|
||||
|
||||
// Wrap text with language tags
|
||||
text = `<${lang}>${text}</${lang}>`;
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
@@ -150,11 +159,11 @@ export class TextToSpeech {
|
||||
this.sampleRate = cfgs.ae.sample_rate;
|
||||
}
|
||||
|
||||
async _infer(textList, style, totalStep, speed = 1.05, progressCallback = null) {
|
||||
async _infer(textList, langList, style, totalStep, speed = 1.05, progressCallback = null) {
|
||||
const bsz = textList.length;
|
||||
|
||||
// Process text
|
||||
const { textIds, textMask } = this.textProcessor.call(textList);
|
||||
const { textIds, textMask } = this.textProcessor.call(textList, langList);
|
||||
|
||||
const textIdsFlat = new BigInt64Array(textIds.flat().map(x => BigInt(x)));
|
||||
const textIdsShape = [bsz, textIds[0].length];
|
||||
@@ -259,16 +268,18 @@ export class TextToSpeech {
|
||||
return { wav, duration };
|
||||
}
|
||||
|
||||
async call(text, style, totalStep, speed = 1.05, silenceDuration = 0.3, progressCallback = null) {
|
||||
async call(text, lang, style, totalStep, speed = 1.05, silenceDuration = 0.3, progressCallback = null) {
|
||||
if (style.ttl.dims[0] !== 1) {
|
||||
throw new Error('Single speaker text to speech only supports single style');
|
||||
}
|
||||
const textList = chunkText(text);
|
||||
const maxLen = (lang === 'ko' || lang === 'ja') ? 120 : 300;
|
||||
const textList = chunkText(text, maxLen);
|
||||
const langList = new Array(textList.length).fill(lang);
|
||||
let wavCat = [];
|
||||
let durCat = 0;
|
||||
|
||||
for (const chunk of textList) {
|
||||
const { wav, duration } = await this._infer([chunk], style, totalStep, speed, progressCallback);
|
||||
for (let i = 0; i < textList.length; i++) {
|
||||
const { wav, duration } = await this._infer([textList[i]], [langList[i]], style, totalStep, speed, progressCallback);
|
||||
|
||||
if (wavCat.length === 0) {
|
||||
wavCat = wav;
|
||||
@@ -284,8 +295,8 @@ export class TextToSpeech {
|
||||
return { wav: wavCat, duration: [durCat] };
|
||||
}
|
||||
|
||||
async batch(textList, style, totalStep, speed = 1.05, progressCallback = null) {
|
||||
return await this._infer(textList, style, totalStep, speed, progressCallback);
|
||||
async batch(textList, langList, style, totalStep, speed = 1.05, progressCallback = null) {
|
||||
return await this._infer(textList, langList, style, totalStep, speed, progressCallback);
|
||||
}
|
||||
|
||||
sampleNoisyLatent(duration, sampleRate, baseChunkSize, chunkCompress, latentDim) {
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Supertonic - Web Demo</title>
|
||||
<title>Supertonic 3 - Web Demo</title>
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🎤 Supertonic</h1>
|
||||
<p class="subtitle">Text-to-Speech with ONNX Runtime Web</p>
|
||||
<h1>🎤 Supertonic 3</h1>
|
||||
<p class="subtitle">31-language Text-to-Speech with ONNX Runtime Web</p>
|
||||
|
||||
<div id="statusBox" class="status-box">
|
||||
<div class="status-text-wrapper">
|
||||
@@ -30,8 +30,51 @@
|
||||
<select id="voiceStyleSelect">
|
||||
<option value="assets/voice_styles/M1.json">Male 1 (M1)</option>
|
||||
<option value="assets/voice_styles/M2.json">Male 2 (M2)</option>
|
||||
<option value="assets/voice_styles/M3.json">Male 3 (M3)</option>
|
||||
<option value="assets/voice_styles/M4.json">Male 4 (M4)</option>
|
||||
<option value="assets/voice_styles/M5.json">Male 5 (M5)</option>
|
||||
<option value="assets/voice_styles/F1.json">Female 1 (F1)</option>
|
||||
<option value="assets/voice_styles/F2.json">Female 2 (F2)</option>
|
||||
<option value="assets/voice_styles/F3.json">Female 3 (F3)</option>
|
||||
<option value="assets/voice_styles/F4.json">Female 4 (F4)</option>
|
||||
<option value="assets/voice_styles/F5.json">Female 5 (F5)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<label for="langSelect">Language:</label>
|
||||
<select id="langSelect">
|
||||
<option value="en" selected>English (en)</option>
|
||||
<option value="ko">한국어 (ko)</option>
|
||||
<option value="ja">日本語 (ja)</option>
|
||||
<option value="ar">العربية (ar)</option>
|
||||
<option value="bg">Bulgarian (bg)</option>
|
||||
<option value="cs">Czech (cs)</option>
|
||||
<option value="da">Danish (da)</option>
|
||||
<option value="de">Deutsch (de)</option>
|
||||
<option value="el">Greek (el)</option>
|
||||
<option value="es">Español (es)</option>
|
||||
<option value="et">Estonian (et)</option>
|
||||
<option value="fi">Finnish (fi)</option>
|
||||
<option value="fr">Français (fr)</option>
|
||||
<option value="hi">Hindi (hi)</option>
|
||||
<option value="hr">Croatian (hr)</option>
|
||||
<option value="hu">Hungarian (hu)</option>
|
||||
<option value="id">Indonesian (id)</option>
|
||||
<option value="it">Italian (it)</option>
|
||||
<option value="lt">Lithuanian (lt)</option>
|
||||
<option value="lv">Latvian (lv)</option>
|
||||
<option value="nl">Dutch (nl)</option>
|
||||
<option value="pl">Polish (pl)</option>
|
||||
<option value="pt">Português (pt)</option>
|
||||
<option value="ro">Romanian (ro)</option>
|
||||
<option value="ru">Russian (ru)</option>
|
||||
<option value="sk">Slovak (sk)</option>
|
||||
<option value="sl">Slovenian (sl)</option>
|
||||
<option value="sv">Swedish (sv)</option>
|
||||
<option value="tr">Turkish (tr)</option>
|
||||
<option value="uk">Ukrainian (uk)</option>
|
||||
<option value="vi">Vietnamese (vi)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -45,7 +88,7 @@
|
||||
<div class="section">
|
||||
<label for="totalStep">Total Steps (higher = better
|
||||
quality):</label>
|
||||
<input type="number" id="totalStep" value="5"
|
||||
<input type="number" id="totalStep" value="8"
|
||||
min="1" max="50">
|
||||
</div>
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ let currentStylePath = DEFAULT_VOICE_STYLE_PATH;
|
||||
const textInput = document.getElementById('text');
|
||||
const voiceStyleSelect = document.getElementById('voiceStyleSelect');
|
||||
const voiceStyleInfo = document.getElementById('voiceStyleInfo');
|
||||
const langSelect = document.getElementById('langSelect');
|
||||
const totalStepInput = document.getElementById('totalStep');
|
||||
const speedInput = document.getElementById('speed');
|
||||
const generateBtn = document.getElementById('generateBtn');
|
||||
@@ -188,12 +189,14 @@ async function generateSpeech() {
|
||||
|
||||
const totalStep = parseInt(totalStepInput.value);
|
||||
const speed = parseFloat(speedInput.value);
|
||||
const lang = langSelect.value;
|
||||
|
||||
showStatus('ℹ️ <strong>Generating speech from text...</strong>');
|
||||
const tic = Date.now();
|
||||
|
||||
const { wav, duration } = await textToSpeech.call(
|
||||
text,
|
||||
text,
|
||||
lang,
|
||||
currentStyle,
|
||||
totalStep,
|
||||
speed,
|
||||
|
||||
@@ -1,6 +1,38 @@
|
||||
import { createReadStream, existsSync, statSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const rootAssetsDir = path.resolve(__dirname, '../assets');
|
||||
|
||||
function serveRootAssets() {
|
||||
return {
|
||||
name: 'serve-root-assets',
|
||||
configureServer(server) {
|
||||
server.middlewares.use('/assets', (req, res, next) => {
|
||||
const urlPath = decodeURIComponent((req.url || '').split('?')[0]);
|
||||
const filePath = path.resolve(rootAssetsDir, `.${urlPath}`);
|
||||
|
||||
if (!filePath.startsWith(rootAssetsDir) || !existsSync(filePath)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
const stat = statSync(filePath);
|
||||
if (!stat.isFile()) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
createReadStream(filePath).pipe(res);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [serveRootAssets()],
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true
|
||||
|
||||