Skip to content

Commit b6a563a

Browse files
authored
Add Swfit API for SupertonicTTS (#3286)
This pull request expands the Swift API by integrating support for the Supertonic offline text-to-speech model. It provides developers with the necessary tools to configure and utilize Supertonic for generating speech, complete with a practical example demonstrating its usage and progress reporting capabilities. This enhancement broadens the range of TTS models available within the Swift ecosystem.
1 parent 72bc3ba commit b6a563a

6 files changed

Lines changed: 158 additions & 3 deletions

File tree

.github/scripts/test-swift.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ rm -rf sherpa-onnx-fire-red-*
1919
ls -lh
2020
rm -rf sherpa-onnx-pocket-*
2121

22+
./run-tts-supertonic-en.sh
23+
ls -lh
24+
rm -rf sherpa-onnx-supertonic-*
25+
2226
./run-medasr-ctc-asr.sh
2327
rm -rf sherpa-onnx-medasr-*
2428

swift-api-examples/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,4 @@ medasr-ctc
3030
funasr-nano
3131
fire-red-asr-ctc
3232
moonshine-v2-asr
33+
tts-supertonic-en

swift-api-examples/SherpaOnnx.swift

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,26 @@ func sherpaOnnxOfflineTtsPocketModelConfig(
10651065
)
10661066
}
10671067

1068+
func sherpaOnnxOfflineTtsSupertonicModelConfig(
1069+
durationPredictor: String = "",
1070+
textEncoder: String = "",
1071+
vectorEstimator: String = "",
1072+
vocoder: String = "",
1073+
ttsJson: String = "",
1074+
unicodeIndexer: String = "",
1075+
voiceStyle: String = ""
1076+
) -> SherpaOnnxOfflineTtsSupertonicModelConfig {
1077+
return SherpaOnnxOfflineTtsSupertonicModelConfig(
1078+
duration_predictor: toCPointer(durationPredictor),
1079+
text_encoder: toCPointer(textEncoder),
1080+
vector_estimator: toCPointer(vectorEstimator),
1081+
vocoder: toCPointer(vocoder),
1082+
tts_json: toCPointer(ttsJson),
1083+
unicode_indexer: toCPointer(unicodeIndexer),
1084+
voice_style: toCPointer(voiceStyle)
1085+
)
1086+
}
1087+
10681088
func sherpaOnnxOfflineTtsModelConfig(
10691089
vits: SherpaOnnxOfflineTtsVitsModelConfig = sherpaOnnxOfflineTtsVitsModelConfig(),
10701090
matcha: SherpaOnnxOfflineTtsMatchaModelConfig = sherpaOnnxOfflineTtsMatchaModelConfig(),
@@ -1074,7 +1094,8 @@ func sherpaOnnxOfflineTtsModelConfig(
10741094
provider: String = "cpu",
10751095
kitten: SherpaOnnxOfflineTtsKittenModelConfig = sherpaOnnxOfflineTtsKittenModelConfig(),
10761096
zipvoice: SherpaOnnxOfflineTtsZipvoiceModelConfig = sherpaOnnxOfflineTtsZipvoiceModelConfig(),
1077-
pocket: SherpaOnnxOfflineTtsPocketModelConfig = sherpaOnnxOfflineTtsPocketModelConfig()
1097+
pocket: SherpaOnnxOfflineTtsPocketModelConfig = sherpaOnnxOfflineTtsPocketModelConfig(),
1098+
supertonic: SherpaOnnxOfflineTtsSupertonicModelConfig = sherpaOnnxOfflineTtsSupertonicModelConfig()
10781099
) -> SherpaOnnxOfflineTtsModelConfig {
10791100
return SherpaOnnxOfflineTtsModelConfig(
10801101
vits: vits,
@@ -1085,7 +1106,8 @@ func sherpaOnnxOfflineTtsModelConfig(
10851106
kokoro: kokoro,
10861107
kitten: kitten,
10871108
zipvoice: zipvoice,
1088-
pocket: pocket
1109+
pocket: pocket,
1110+
supertonic: supertonic
10891111
)
10901112
}
10911113

@@ -1250,7 +1272,7 @@ final class SherpaOnnxGenerationConfigC {
12501272
silence_scale: swiftConfig.silenceScale,
12511273
speed: swiftConfig.speed,
12521274
sid: Int32(swiftConfig.sid),
1253-
reference_audio: buffer.baseAddress,
1275+
reference_audio: buffer.count > 0 ? buffer.baseAddress : nil,
12541276
reference_audio_len: Int32(buffer.count),
12551277
reference_sample_rate: Int32(swiftConfig.referenceSampleRate),
12561278
reference_text: toCPointer(swiftConfig.referenceText),
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/usr/bin/env bash
2+
3+
set -ex
4+
5+
if [ ! -d ../build-swift-macos ]; then
6+
echo "Please run ../build-swift-macos.sh first!"
7+
exit 1
8+
fi
9+
10+
# please visit
11+
# https://k2-fsa.github.io/sherpa/onnx/tts/supertonic.html
12+
# to download more models
13+
if [ ! -f ./sherpa-onnx-supertonic-tts-int8-2026-03-06/duration_predictor.int8.onnx ]; then
14+
curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/sherpa-onnx-supertonic-tts-int8-2026-03-06.tar.bz2
15+
tar xf sherpa-onnx-supertonic-tts-int8-2026-03-06.tar.bz2
16+
rm sherpa-onnx-supertonic-tts-int8-2026-03-06.tar.bz2
17+
fi
18+
19+
if [ ! -e ./tts-supertonic-en ]; then
20+
# Note: We use -lc++ to link against libc++ instead of libstdc++
21+
swiftc \
22+
-lc++ \
23+
-I ../build-swift-macos/install/include \
24+
-import-objc-header ./SherpaOnnx-Bridging-Header.h \
25+
./tts-supertonic-en.swift ./SherpaOnnx.swift \
26+
-L ../build-swift-macos/install/lib/ \
27+
-l sherpa-onnx \
28+
-l onnxruntime \
29+
-o tts-supertonic-en
30+
31+
strip tts-supertonic-en
32+
else
33+
echo "./tts-supertonic-en exists - skip building"
34+
fi
35+
36+
export DYLD_LIBRARY_PATH=$PWD/../build-swift-macos/install/lib:$DYLD_LIBRARY_PATH
37+
./tts-supertonic-en

swift-api-examples/tts-pocket-en.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import Foundation
2+
13
class PocketTtsProgressHandler {
24
func progress(samples: [Float], progress: Float) {
35
print(String(format: "Received %d samples, Progress: %.2f%%", samples.count, progress * 100))
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import Foundation
2+
3+
class SupertonicTtsProgressHandler {
4+
func progress(samples: [Float], progress: Float) {
5+
print(String(format: "Received %d samples, Progress: %.2f%%", samples.count, progress * 100))
6+
}
7+
}
8+
9+
func runSupertonicTtsDemo() {
10+
let supertonic = sherpaOnnxOfflineTtsSupertonicModelConfig(
11+
durationPredictor: "./sherpa-onnx-supertonic-tts-int8-2026-03-06/duration_predictor.int8.onnx",
12+
textEncoder: "./sherpa-onnx-supertonic-tts-int8-2026-03-06/text_encoder.int8.onnx",
13+
vectorEstimator: "./sherpa-onnx-supertonic-tts-int8-2026-03-06/vector_estimator.int8.onnx",
14+
vocoder: "./sherpa-onnx-supertonic-tts-int8-2026-03-06/vocoder.int8.onnx",
15+
ttsJson: "./sherpa-onnx-supertonic-tts-int8-2026-03-06/tts.json",
16+
unicodeIndexer: "./sherpa-onnx-supertonic-tts-int8-2026-03-06/unicode_indexer.bin",
17+
voiceStyle: "./sherpa-onnx-supertonic-tts-int8-2026-03-06/voice.bin"
18+
)
19+
20+
let modelConfig = sherpaOnnxOfflineTtsModelConfig(numThreads: 2, supertonic: supertonic)
21+
var ttsConfig = sherpaOnnxOfflineTtsConfig(model: modelConfig)
22+
ttsConfig.model.debug = 1
23+
24+
let tts = SherpaOnnxOfflineTtsWrapper(config: &ttsConfig)
25+
26+
var genConfig = SherpaOnnxGenerationConfigSwift()
27+
genConfig.sid = 6
28+
genConfig.numSteps = 5
29+
genConfig.speed = 1.25
30+
genConfig.extra = ["lang": "en"]
31+
32+
let text =
33+
"Today as always, men fall into two groups: slaves and free men. Whoever "
34+
+ "does not have two-thirds of his day for himself, is a slave, whatever "
35+
+ "he may be: a statesman, a businessman, an official, or a scholar."
36+
37+
func generateAndSave(
38+
outputFile: String, callback: TtsProgressCallbackWithArg? = nil,
39+
arg: UnsafeMutableRawPointer? = nil
40+
) {
41+
let audio = tts.generateWithConfig(
42+
text: text,
43+
config: genConfig,
44+
callback: callback,
45+
arg: arg
46+
)
47+
48+
if audio.save(filename: outputFile) == 1 {
49+
print("Saved to: \(outputFile)")
50+
} else {
51+
print("Failed to save to \(outputFile)")
52+
}
53+
}
54+
55+
// -------------------------
56+
// Option 1: with callback
57+
// -------------------------
58+
let useCallback = true
59+
if useCallback {
60+
let progressHandler = SupertonicTtsProgressHandler()
61+
let arg = Unmanaged.passUnretained(progressHandler).toOpaque()
62+
63+
let callback: TtsProgressCallbackWithArg = { samples, n, progress, arg in
64+
let handler = Unmanaged<SupertonicTtsProgressHandler>.fromOpaque(arg!).takeUnretainedValue()
65+
66+
let buffer: [Float] =
67+
samples != nil ? Array(UnsafeBufferPointer(start: samples, count: Int(n))) : []
68+
handler.progress(samples: buffer, progress: progress)
69+
return 1 // continue generating
70+
}
71+
72+
generateAndSave(outputFile: "generated-supertonic-callback.wav", callback: callback, arg: arg)
73+
} else {
74+
// -------------------------
75+
// Option 2: direct generation
76+
// -------------------------
77+
generateAndSave(outputFile: "generated-supertonic-direct.wav")
78+
}
79+
}
80+
81+
// -------------------------
82+
// Run demo
83+
// -------------------------
84+
@main
85+
struct App {
86+
static func main() {
87+
runSupertonicTtsDemo()
88+
}
89+
}

0 commit comments

Comments
 (0)