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.go`.
## 📰 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)
@@ -87,13 +89,14 @@ Process multiple voice styles and texts at once:
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."
-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
@@ -136,12 +139,14 @@ This will:
| `-n-test` | int | 4 | Number of times to generate each sample |
| `-voice-style` | str | `assets/voice_styles/M1.json` | Voice style file path(s), comma-separated |
| `-text` | str | (long default text) | Text(s) to synthesize, pipe-separated |
| `-lang` | str | `en` | Language(s) for synthesis, comma-separated (en, ko, es, pt, fr) |
| `-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: `en` (English), `ko` (Korean), `es` (Spanish), `pt` (Portuguese), `fr` (French)
- **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

@@ -19,6 +19,7 @@ type Args struct {
nTest int
voiceStyle []string
text []string
lang []string
saveDir string
batch bool
}
@@ -34,9 +35,10 @@ func parseArgs() *Args {
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
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 (en, ko, es, pt, fr)")
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)

View File

@@ -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

View File

@@ -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=

View File

@@ -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", "es", "pt", "fr"}
// 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" {
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() {
@@ -917,15 +935,17 @@ func InitializeONNXRuntime() error {
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, '_')