Supertonic 2
This commit is contained in:
15
go/README.md
15
go/README.md
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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=
|
||||
|
||||
76
go/helper.go
76
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", "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, '_')
|
||||
|
||||
Reference in New Issue
Block a user