Supertonic 2

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

View File

@@ -4,6 +4,8 @@ This guide provides examples for running TTS inference using `example_onnx`.
## 📰 Update News
**2026.01.06** - 🎉 **Supertonic 2** released with multilingual support! Now supports English (`en`), Korean (`ko`), Spanish (`es`), Portuguese (`pt`), and French (`fr`). [Demo](https://huggingface.co/spaces/Supertone/supertonic-2) | [Models](https://huggingface.co/Supertone/supertonic-2)
**2025.12.10** - Added [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)
@@ -48,13 +50,14 @@ Process multiple voice styles and texts at once:
.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."
--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
@@ -97,12 +100,23 @@ This will:
| `--n-test` | int | 4 | Number of times to generate each sample |
| `--voice-style` | str+ | `assets/voice_styles/M1.json` | Voice style file path(s) |
| `--text` | str+ | (long default text) | Text(s) to synthesize |
| `--lang` | str+ | `en` | Language(s) for synthesis (en, ko, es, pt, fr) |
| `--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 2 supports multiple languages. Use the `--lang` argument to specify the language:
- `en` - English (default)
- `ko` - Korean (한국어)
- `es` - Spanish (Español)
- `pt` - Portuguese (Português)
- `fr` - French (Français)
## 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

View File

@@ -9,6 +9,7 @@ struct Args {
var nTest: Int = 4
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
}
@@ -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]

View File

@@ -2,6 +2,14 @@ import Foundation
import Accelerate
import OnnxRuntimeBindings
// MARK: - Available Languages
let AVAILABLE_LANGS = ["en", "ko", "es", "pt", "fr"]
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" ? 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)
}
}