Skip to content

Commit 93bd9cf

Browse files
authored
Kokoro Text-to-Speech (#112)
1 parent b360d05 commit 93bd9cf

1,604 files changed

Lines changed: 28312 additions & 174 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/tts-test.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: TTS Test
2+
3+
on:
4+
pull_request:
5+
branches: [ main ]
6+
workflow_dispatch:
7+
8+
jobs:
9+
test-tts:
10+
runs-on: macos-14
11+
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- name: Build FluidAudio
16+
run: |
17+
swift build -c release
18+
19+
- name: Generate TTS Audio
20+
run: |
21+
echo "🎤 Generating TTS audio (ground truth test)..."
22+
TEXT="I can't believe we finally made it to the summit after climbing for twelve exhausting hours through wind and rain, but wow, this view of the endless mountain ranges stretching to the horizon makes every single difficult step completely worth the journey."
23+
24+
# This will auto-download model and generate audio
25+
swift run --configuration release fluidaudio tts "$TEXT" --output kokoro_output.wav --auto-download
26+
27+
# Verify output
28+
if [ -f kokoro_output.wav ]; then
29+
SIZE=$(ls -l kokoro_output.wav | awk '{print $5}')
30+
echo "✅ TTS successful: kokoro_output.wav ($SIZE bytes)"
31+
else
32+
echo "❌ Output file not created"
33+
exit 1
34+
fi
35+
36+
- name: Upload Audio Output
37+
if: always()
38+
uses: actions/upload-artifact@v4
39+
with:
40+
name: kokoro-tts-output
41+
path: kokoro_output.wav
42+
retention-days: 7
43+

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ FluidAudioDatasets/
9797
*.wav
9898
*.mp3
9999
Resources/
100+
!Sources/FluidAudio/Resources/
101+
!Sources/FluidAudio/Resources/**
100102
scripts/
101103
Documentation/parakeet-tdt/
102104
docs/parakeet-tdt/
105+
106+
fluidaudio_cli/*

Documentation/Benchmarks.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,91 @@ iPhone 16 Pro Max run, and only for models that were reloaded during the session
8080
| Decoder | 88.49 | 8.11 | 146.01 | MLComputeUnits(rawValue: 1) |
8181
| JointDecision | 48.46 | 7.97 | 71.85 | MLComputeUnits(rawValue: 1) |
8282

83+
## Text-to-Speech
84+
85+
We generated the same strings with to gerneate audio between 1s to ~300s in order to test the speed across a range of varying inputs on Pytorch CPU, MPS, and MLX pipeline, and compared it against the native Swift version with Core ML models.
86+
87+
Each pipeline warmed up the models by running through it once with pesudo inputs, and then comparing the raw inference time with the model already loaded. You can see that for the Core ML model, we traded lower memory and very slightly faster inference for longer initial warm-up.
88+
89+
Note that the Pytorch kokoro model in Pytorch has a memory leak issue: https://github.com/hexgrad/kokoro/issues/152
90+
91+
The following tests were ran on M4 Pro, 48GB RAM, Macbook Pro. If you have another device, please do try replicating it as well!
92+
93+
### Kokoro-82M PyTorch (CPU)
94+
95+
```bash
96+
KPipeline benchmark for voice af_heart (warm-up took 0.175s) using hexgrad/kokoro
97+
Test Chars Output (s) Inf(s) RTFx Peak GB
98+
1 42 2.750 0.187 14.737x 1.44
99+
2 129 8.625 0.530 16.264x 1.85
100+
3 254 15.525 0.923 16.814x 2.65
101+
4 93 6.125 0.349 17.566x 2.66
102+
5 104 7.200 0.410 17.567x 2.70
103+
6 130 9.300 0.504 18.443x 2.72
104+
7 197 12.850 0.726 17.711x 2.83
105+
8 6 1.350 0.098 13.823x 2.83
106+
9 1228 76.200 4.342 17.551x 3.19
107+
10 567 35.200 2.069 17.014x 4.85
108+
11 4615 286.525 17.041 16.814x 4.78
109+
Total - 461.650 27.177 16.987x 4.85
110+
```
111+
112+
### Kokoro-82M PyTorch (MPS)
113+
114+
I wasn't able to run the MPS model for longer durations, even with `PYTORCH_ENABLE_MPS_FALLBACK=1` enabled, it kept crashing for the longer strings.
115+
116+
```bash
117+
KPipeline benchmark for voice af_heart (warm-up took 0.568s) using pip package
118+
Test Chars Output (s) Inf(s) RTFx Peak GB
119+
1 42 2.750 0.414 6.649x 1.41
120+
2 129 8.625 0.729 11.839x 1.54
121+
Total - 11.375 1.142 9.960x 1.54
122+
```
123+
124+
### Kokoro-82M MLX Pipeline
125+
126+
```bash
127+
TTS benchmark for voice af_heart (warm-up took an extra 2.155s) using model prince-canuma/Kokoro-82M
128+
Test Chars Output (s) Inf(s) RTFx Peak GB
129+
1 42 2.750 0.347 7.932x 1.12
130+
2 129 8.650 0.597 14.497x 2.47
131+
3 254 15.525 0.825 18.829x 2.65
132+
4 93 6.125 0.306 20.039x 2.65
133+
5 104 7.200 0.343 21.001x 2.65
134+
6 130 9.300 0.560 16.611x 2.65
135+
7 197 12.850 0.596 21.573x 2.65
136+
8 6 1.350 0.364 3.706x 2.65
137+
9 1228 76.200 2.979 25.583x 3.29
138+
10 567 35.200 1.374 25.615x 3.37
139+
11 4615 286.500 11.112 25.783x 3.37
140+
Total - 461.650 19.401 23.796x 3.37
141+
```
142+
143+
#### Swift + Fluid Audio Core ML models
144+
145+
Note that it does take `~15s` to compile the model on the first run, subsequent runs are shorter, we expect ~2s to load.
146+
147+
```bash
148+
> swift run fluidaudio tts --benchmark
149+
...
150+
FluidAudio TTS benchmark for voice af_heart (warm-up took an extra 2.348s)
151+
Test Chars Ouput (s) Inf(s) RTFx
152+
1 42 2.825 0.440 6.424x
153+
2 129 7.725 0.594 13.014x
154+
3 254 13.400 0.776 17.278x
155+
4 93 5.875 0.587 10.005x
156+
5 104 6.675 0.613 10.889x
157+
6 130 8.075 0.621 13.008x
158+
7 197 10.650 0.627 16.983x
159+
8 6 0.825 0.360 2.290x
160+
9 1228 67.625 2.362 28.625x
161+
10 567 33.025 1.341 24.619x
162+
11 4269 247.600 9.087 27.248x
163+
Total - 404.300 17.408 23.225
164+
165+
Peak memory usage (process-wide): 1.503 GB
166+
```
167+
83168
## Voice Activity Detection
84169

85170
Model is nearly identical to the base model in terms of quality, perforamnce wise we see an up to ~3.5x improvement compared to the silero Pytorch VAD model with the 256ms batch model (8 chunks of 32ms)

Documentation/EspeakFramework.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# eSpeak-NG Framework Packaging
2+
3+
FluidAudio bundles the eSpeak-NG phoneme resources so Kokoro can fall back to G2P lookups when the US lexicons don’t contain a word. The Core ML pipeline expects the resources under `Resources/espeak-ng/espeak-ng-data.bundle` with the canonical `voices/` directory inside.
4+
5+
## All Platforms (Primary Flow)
6+
- `TtsResourceDownloader.ensureEspeakDataBundle` first attempts to stage the packaged `espeak-ng-data.bundle` from SwiftPM resources (`Sources/FluidAudio/Resources/espeak-ng/`).
7+
- The bundle is copied to `~/.cache/fluidaudio/Models/kokoro/Resources/espeak-ng/`.
8+
- The `voices/` directory is validated after staging; if missing, `TTSError.downloadFailed` is raised.
9+
10+
## Fallback Behavior (macOS Only)
11+
- If the packaged bundle is unavailable, **macOS only** falls back to downloading `espeak-ng.zip` from HuggingFace and extracting it with `/usr/bin/unzip`.
12+
- **iOS/tvOS/watchOS** do not support fallback downloads and will throw `TTSError.downloadFailed` if the packaged bundle is missing.
13+
- For mobile platforms, ensure the packaged bundle is present in the Swift package resources before building.
14+
15+
## Best practices
16+
- Keep the `espeak-ng-data.bundle` (packaged copy) and the optional `espeak-ng.zip` fallback in sync with any updates to the Kokoro phoneme mapper.
17+
- If you customize the cache location, be sure the `Resources/espeak-ng/espeak-ng-data.bundle/voices/` directory is present before running TTS.
18+
- When testing on iOS, bundle the extracted resources with the app or seed the simulator cache in advance to avoid runtime failures.
19+
20+
## CocoaPods integration notes
21+
- The `ESpeakNG.xcframework` now includes support for iOS device (arm64), iOS Simulator (arm64 + x86_64), and macOS (arm64 + x86_64).
22+
- iOS Simulator support is provided via a stub framework that allows building and linking but returns failure values for ESpeakNG function calls.
23+
- Pod validation passes successfully with `pod lib lint FluidAudio.podspec --allow-warnings` for all platforms.
24+
- On iOS Simulator, ESpeakNG initialization will fail gracefully and phonemization requests will return `nil` due to the stub implementation.
25+
- Full ESpeakNG functionality is available on iOS device and macOS platforms.
26+
27+
## Licensing notes
28+
- eSpeak-NG is distributed under the GNU GPL v3 (or later). Both the core library and the `espeak-ng-data` voices inherit the same license.
29+
- The full license text now lives at `Licenses/ESpeakNG_LICENSE.txt`; ship this file (or the upstream `COPYING`) anywhere the framework is redistributed and surface it in your third-party notices UI.
30+
- If you republish the prebuilt `ESpeakNG.xcframework`, keep the license alongside the binary and ensure downstream consumers can obtain the corresponding source per GPL requirements.

Documentation/TTS/README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Text-To-Speech (TTS) Code Examples
2+
3+
> **⚠️ Beta:** The TTS system is currently in beta and only supports American English. Additional language support is planned for future releases.
4+
5+
Quick recipes for running the Kokoro synthesis stack.
6+
7+
## CLI quick start
8+
9+
```bash
10+
swift run fluidaudio tts "Welcome to FluidAudio text to speech" \
11+
--output ~/Desktop/demo.wav \
12+
--voice af_heart
13+
```
14+
15+
The first invocation downloads Kokoro models, phoneme dictionaries, and voice embeddings; later runs reuse the
16+
cached assets.
17+
18+
## Swift async usage
19+
20+
```swift
21+
import FluidAudio
22+
import Foundation
23+
24+
@main
25+
struct DemoTTS {
26+
static func main() async {
27+
guard #available(macOS 13.0, *) else {
28+
print("FluidAudio TTS requires macOS 13 or newer.")
29+
return
30+
}
31+
32+
let manager = TtSManager()
33+
34+
do {
35+
try await manager.initialize()
36+
let audioData = try await manager.synthesize(text: "Hello from FluidAudio!")
37+
38+
let outputURL = URL(fileURLWithPath: "/tmp/fluidaudio-demo.wav")
39+
try audioData.write(to: outputURL)
40+
print("Saved synthesized audio to: \(outputURL.path)")
41+
} catch {
42+
print("Synthesis failed: \(error)")
43+
}
44+
}
45+
}
46+
```
47+
48+
Swap in `manager.initialize(models:)` when you want to preload only the long-form `.fifteenSecond` variant.
49+
50+
## Inspecting chunk metadata
51+
52+
```swift
53+
let manager = TtSManager()
54+
try await manager.initialize()
55+
56+
let detailed = try await manager.synthesizeDetailed(
57+
text: "FluidAudio can report chunk splits for you.",
58+
variantPreference: .fifteenSecond
59+
)
60+
61+
for chunk in detailed.chunks {
62+
print("Chunk #\(chunk.index) -> variant: \(chunk.variant), tokens: \(chunk.tokenCount)")
63+
print(" text: \(chunk.text)")
64+
}
65+
```
66+
67+
`KokoroSynthesizer.SynthesisResult` also exposes `diagnostics` for per-run variant and audio footprint totals.

FluidAudio.podspec

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,28 @@ Pod::Spec.new do |spec|
1818

1919
spec.source = { :git => "https://github.com/FluidInference/FluidAudio.git", :tag => "v#{spec.version}" }
2020
spec.source_files = "Sources/FluidAudio/**/*.swift"
21-
21+
22+
# iOS Configuration
23+
# Exclude TTS module from iOS builds to avoid ESpeakNG xcframework linking issues.
24+
# CocoaPods has known limitations with vendored xcframeworks during pod lib lint on iOS:
25+
# the framework symbols aren't properly linked in the temporary build environment,
26+
# causing "Undefined symbols" linker errors even though the binary is valid.
27+
# iOS builds include: ASR (speech recognition), Diarization, and VAD (voice activity detection).
28+
spec.ios.exclude_files = "Sources/FluidAudio/TextToSpeech/**/*"
29+
spec.ios.frameworks = "CoreML", "AVFoundation", "Accelerate", "UIKit"
30+
31+
# macOS Configuration
32+
# ESpeakNG framework is only vendored for macOS in the podspec (not a framework limitation).
33+
# The xcframework supports iOS, but CocoaPods fails to link it during iOS validation.
34+
# This enables TTS (text-to-speech) functionality with G2P (grapheme-to-phoneme) conversion.
35+
# macOS builds include: ASR, Diarization, VAD, and TTS with ESpeakNG support.
36+
spec.osx.vendored_frameworks = "Sources/FluidAudio/Frameworks/ESpeakNG.xcframework"
37+
spec.osx.frameworks = "CoreML", "AVFoundation", "Accelerate", "Cocoa"
38+
2239
spec.swift_versions = ["5.10"]
23-
spec.frameworks = "CoreML", "AVFoundation", "Accelerate"
40+
41+
# Enable module definition for proper framework imports
42+
spec.pod_target_xcconfig = {
43+
'DEFINES_MODULE' => 'YES'
44+
}
2445
end

Package.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,15 @@ let package = Package(
1919
],
2020
dependencies: [],
2121
targets: [
22+
.binaryTarget(
23+
name: "ESpeakNG",
24+
path: "Sources/FluidAudio/Frameworks/ESpeakNG.xcframework"
25+
),
2226
.target(
2327
name: "FluidAudio",
24-
dependencies: [],
28+
dependencies: [
29+
"ESpeakNG",
30+
],
2531
path: "Sources/FluidAudio",
2632
exclude: []
2733
),

README.md

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,11 +281,61 @@ swift run fluidaudio vad-analyze path/to/audio.wav --streaming
281281
# Benchmark accuracy/precision trade-offs
282282
swift run fluidaudio vad-benchmark --num-files 50 --threshold 0.3
283283
```
284-
285284
`swift run fluidaudio vad-analyze --help` lists every tuning option, including
286285
negative-threshold overrides, max-speech splitting, padding, and chunk size.
287286
Offline mode also reports RTFx using the model's per-chunk processing time.
288287

288+
## Text‑To‑Speech (TTS)
289+
290+
> **⚠️ Beta:** The TTS system is currently in beta and only supports American English. Additional language support is planned for future releases.
291+
292+
- Model: Kokoro (CoreML unified model)
293+
- Language: American English (beta)
294+
- G2P: Dictionary first, then eSpeak NG (CEspeakNG) for OOV words
295+
- Output: 24 kHz mono WAV
296+
297+
Requirements (macOS)
298+
Ensure eSpeak NG headers/libs are available via pkg-config (`espeak-ng`).
299+
https://github.com/espeak-ng/espeak-ng/tree/master
300+
301+
### Quick Start (CLI)
302+
303+
```bash
304+
# First run will download the Kokoro model and vocab
305+
swift run fluidaudio tts "Hello from FluidAudio." --auto-download --output out.wav
306+
307+
# Another example with punctuation and OOV handling
308+
swift run fluidaudio tts "Edge-cases: URLs like https://example.com and e-mail test@example.com." --output out2.wav
309+
```
310+
311+
Notes
312+
- The TTS pipeline uses a word→phoneme dictionary first; unknown words are phonemized with eSpeak NG (C API) and mapped to the model’s token set.
313+
- OOV words are printed with their IPA and mapped tokens for visibility during synthesis.
314+
- We do not prepend any “language token” to avoid leading vowel artifacts.
315+
316+
### Quick Start (Code)
317+
318+
```swift
319+
import FluidAudio
320+
321+
Task {
322+
do {
323+
let data = try await KokoroModel.synthesize(text: "Hello from FluidAudio.")
324+
try data.write(to: URL(fileURLWithPath: "out.wav"))
325+
} catch {
326+
print("TTS error: \(error)")
327+
}
328+
}
329+
```
330+
331+
Troubleshooting
332+
Build requires eSpeak NG headers/libs for the C API discoverable via pkg-config (`espeak-ng`).
333+
- If SwiftPM cannot find headers, build with explicit paths:
334+
- `swift build -Xcc -I/opt/homebrew/include -Xlinker -L/opt/homebrew/lib`
335+
- Dictionary and model assets are cached under `~/.cache/fluidaudio/Models/kokoro`.
336+
337+
## Showcase
338+
289339
## Showcase
290340

291341
Make a PR if you want to add your app!
@@ -326,6 +376,8 @@ Parakeet-mlx: https://github.com/senstella/parakeet-mlx
326376

327377
silero-vad: https://github.com/snakers4/silero-vad
328378

379+
Kokoro-82M: https://huggingface.co/hexgrad/Kokoro-82M
380+
329381
### Citation
330382

331383
If you use FluidAudio in your work, please cite:

0 commit comments

Comments
 (0)