A macOS Swift command-line tool that runs local text-to-speech (TTS) inference using the audio.cpp C++ engine. The bridge exposes TTS through a thin Objective-C++ layer — no Python required at runtime.
Works with models from your local HuggingFace cache.
# 1. Build audio.cpp (one-time)
git clone https://github.com/0xShug0/audio.cpp /tmp/audio-cpp-clone
# ... see Build section below for full cmake + libtool steps ...
# 2. Build the Swift tool
xcodebuild -project Qwen3TTSDemo.xcodeproj -scheme Qwen3TTSDemo \
-configuration Release -derivedDataPath build build
# 3. Prepare a model from your HuggingFace cache
./scripts/prepare_models.sh qwen3-custom-voice
# 4. Generate speech
./build/Build/Products/Release/Qwen3TTSDemo custom-voice \
--model models/qwen3-custom-voice \
--speaker serena \
--text "Hello, this is a local text-to-speech demo running on my Mac." \
--out hello.wav| Model | Size | Command | How it works |
|---|---|---|---|
| Qwen3 TTS CustomVoice 0.6B | 2.3 GB | custom-voice |
Pick a built-in speaker preset, no reference audio needed |
Available speakers: serena, vivian, ryan, aiden, dylan, eric, ono_anna, sohee, uncle_fu
| Model | Size | Command | Status |
|---|---|---|---|
| Qwen3 TTS Base 1.7B | 4.2 GB (cached) | voice-clone |
Speaker encoder has Conv1d dimension mismatch with MLX-exported safetensors. Needs GGUF conversion or code fix. |
| Qwen3 TTS VoiceDesign 1.7B | ~4 GB (not cached) | voice-design |
Not in your cache. Install: python tools/model_manager.py install qwen3_tts_1_7b_voice_design |
~/.cache/huggingface/hub/
├── models--Qwen--Qwen3-TTS-12Hz-0.6B-CustomVoice/ ✅ ready
├── models--Qwen--Qwen3-TTS-12Hz-1.7B-Base/ ⚠️ dim issue
├── models--mlx-community--Qwen3-TTS-12Hz-0.6B-Base-bf16/ ⚠️ dim issue
├── models--mlx-community--Qwen3-TTS-12Hz-1.7B-Base-bf16/ ⚠️ dim issue
├── models--k2-fsa--OmniVoice/ ❌ incomplete download
├── models--aufklarer--IndexTTS2-MLX-fp16/ ❌ broken symlinks
├── models--hexgrad--Kokoro-82M/ ❌ PyTorch .pth (needs conversion)
├── models--ACE-Step--Ace-Step1.5/ ❌ not downloaded
└── models--openai--whisper-base/ ❌ ASR, not TTS
- macOS 13.3+ (Apple Silicon or Intel with Metal support)
- Xcode 16.0+ with Command Line Tools (
xcode-select --install) - CMake 3.20+ (
brew install cmake)
# Clone if not already present
git clone https://github.com/0xShug0/audio.cpp /tmp/audio-cpp-clone
# Configure with Metal backend
cmake -S /tmp/audio-cpp-clone \
-B /tmp/audio-cpp-clone/build/macos-metal-release \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DENGINE_ENABLE_METAL=ON \
-DENGINE_ENABLE_CUDA=OFF \
-DENGINE_ENABLE_VULKAN=OFF \
-DENGINE_ENABLE_LLAMAFILE=ON \
-DENGINE_ENABLE_NATIVE_CPU=ON \
-DENGINE_ENABLE_OPENMP=OFF \
-DGGML_METAL_EMBED_LIBRARY=ON \
-DENGINE_BUILD_EXAMPLES=OFF \
-DENGINE_BUILD_TESTS=OFF \
-DENGINE_BUILD_WARMBENCH=OFF \
-DBUILD_SHARED_LIBS=OFF
cmake --build /tmp/audio-cpp-clone/build/macos-metal-release \
--target engine_runtime -j $(sysctl -n hw.logicalcpu)
# Merge all static libs and copy headers
mkdir -p /tmp/audio-cpp-clone/build/xcframework/artifacts/Headers
libtool -static -o /tmp/audio-cpp-clone/build/xcframework/artifacts/libAudioCpp.a \
/tmp/audio-cpp-clone/build/macos-metal-release/libengine_runtime.a \
/tmp/audio-cpp-clone/build/macos-metal-release/ggml/src/libggml.a \
/tmp/audio-cpp-clone/build/macos-metal-release/ggml/src/libggml-base.a \
/tmp/audio-cpp-clone/build/macos-metal-release/ggml/src/libggml-cpu.a \
/tmp/audio-cpp-clone/build/macos-metal-release/ggml/src/ggml-metal/libggml-metal.a \
/tmp/audio-cpp-clone/build/macos-metal-release/ggml/src/ggml-blas/libggml-blas.a \
/tmp/audio-cpp-clone/build/macos-metal-release/libcjson_vendor.a \
/tmp/audio-cpp-clone/build/macos-metal-release/libyaml_vendor.a \
/tmp/audio-cpp-clone/build/macos-metal-release/external/sentencepiece/src/libsentencepiece.a
rsync -a /tmp/audio-cpp-clone/include/ /tmp/audio-cpp-clone/build/xcframework/artifacts/Headers/xcodebuild \
-project Qwen3TTSDemo.xcodeproj \
-scheme Qwen3TTSDemo \
-configuration Release \
-derivedDataPath build \
buildThe binary lands at build/Build/Products/Release/Qwen3TTSDemo.
Use the included script to mirror models from your HuggingFace cache. It resolves symlinks and copies the required model_specs/:
# List available models and their cache status
./scripts/prepare_models.sh list
# Prepare a specific model
./scripts/prepare_models.sh qwen3-custom-voice
# Prepare all cached models
./scripts/prepare_models.sh allYou can also point it at different directories:
MODELS_DIR=/path/to/custom/models ./scripts/prepare_models.sh qwen3-custom-voiceOr prepare models manually:
rsync -aL ~/.cache/huggingface/hub/models--Qwen--Qwen3-TTS-12Hz-0.6B-CustomVoice/snapshots/<hash>/ models/qwen3-custom-voice/
mkdir -p models/qwen3-custom-voice/model_specs
cp /tmp/audio-cpp-clone/model_specs/qwen3_tts.json models/qwen3-custom-voice/model_specs/Use built-in speakers. No reference audio needed. Works with the 0.6B CustomVoice model.
build/Build/Products/Release/Qwen3TTSDemo custom-voice \
--model models/qwen3-custom-voice \
--speaker serena \
--text "The lighthouse keeper records a detailed morning report about the tide, and the ships crossing the horizon." \
--out output.wavAll 9 speakers:
for speaker in serena vivian ryan aiden dylan eric ono_anna sohee uncle_fu; do
build/Build/Products/Release/Qwen3TTSDemo custom-voice \
--model models/qwen3-custom-voice \
--speaker "$speaker" \
--text "Hello, my name is ${speaker//_/ }." \
--out "output_${speaker}.wav"
doneGuide the delivery with --instruct:
build/Build/Products/Release/Qwen3TTSDemo custom-voice \
--model models/qwen3-custom-voice \
--speaker vivian \
--instruct "Speak in a cheerful, enthusiastic tone like a morning radio host." \
--text "Good morning everyone! Today is going to be a wonderful day." \
--out cheerful.wavRequires a Base variant model (1.7B or 0.6B). Note: currently blocked by a Conv1d weight dimension mismatch in the safetensors export. See Troubleshooting.
# After preparing a Base model:
./scripts/prepare_models.sh qwen3-base-1.7b
# Voice clone (may fail with tensor shape mismatch — see Troubleshooting)
build/Build/Products/Release/Qwen3TTSDemo voice-clone \
--model models/qwen3-base-1.7b \
--voice-ref /path/to/reference_speech.wav \
--reference-text "What the person says in the reference audio." \
--text "This text will be spoken in the cloned voice." \
--out cloned.wavUse the VoiceDesign 1.7B variant (not in cache — download first):
cd /tmp/audio-cpp-clone
pip install torch safetensors pyyaml
python tools/model_manager.py install qwen3_tts_1_7b_voice_design --models-dir /tmp/models
# Then run:
build/Build/Products/Release/Qwen3TTSDemo voice-design \
--model /tmp/models/Qwen3-TTS-12Hz-1.7B-VoiceDesign \
--instruct "A calm, deep male narrator with a warm documentary voice." \
--text "In the vast expanse of the cosmos, a single star can tell a billion stories." \
--out designed.wav| Option | Default | Description |
|---|---|---|
--model <path> |
(required) | Model directory |
--text <text> |
(required) | Text to synthesize |
--out <path> |
(required) | Output WAV path |
--speaker <name> |
(required for custom-voice) | Speaker preset: serena, vivian, ryan, etc. |
--instruct <text> |
(optional) | Voice style guidance |
--backend <name> |
metal |
metal or cpu |
--device <n> |
0 |
GPU device index |
--threads <n> |
8 |
CPU worker threads |
--language <name> |
English |
Input language |
--seed <n> |
1234 |
Random seed |
--max-new-tokens <n> |
256 |
Max output tokens |
All commands produce 16-bit PCM WAV at 24000 Hz mono. Typical output: ~45 KB per second of speech.
audio.cpp supports 25+ model families. The bridge can be extended to any of them:
| Family | Task | Size |
|---|---|---|
pocket_tts |
TTS, voice cloning | 100M |
kokoro_tts |
TTS | 82M |
chatterbox |
TTS, voice cloning, VC | 0.5B |
omnivoice |
TTS, voice cloning, voice design | 0.6B |
vibevoice |
TTS, multi-speaker dialog | 1.5B / 7B |
miotts |
TTS, voice cloning | 1.7B |
index_tts2 |
TTS, voice cloning | — |
outetts |
TTS, voice cloning | 1.0B |
voxcpm2 |
TTS, voice cloning, voice design | 2B |
supertonic |
TTS | — |
seed_vc |
Voice conversion | — |
qwen3_asr |
Speech recognition | 0.6B |
stable_audio |
Music / sound generation | 3B |
To add one:
- Add a method to
MiniTTSDemoBridge.h/.mmmapping the model's request shape - Change
family_hintininit(or make it configurable) - Add a
caseinmain.swift
You're using voice-clone with a CustomVoice model. Use custom-voice --speaker <name>.
The safetensors Conv1d weights use MLX dimension ordering (out, kernel, in) but audio.cpp expects PyTorch ordering (out, in, kernel). This affects Base-variant models. Workarounds:
- Use a GGUF-converted model — convert with
audiocpp_gguf:cd /tmp/audio-cpp-clone cmake --build build/macos-metal-release --target audiocpp_gguf build/bin/audiocpp_gguf \ --input models/qwen3-base-1.7b/model.safetensors \ --family qwen3_tts --output models/qwen3-base-1.7b/model.gguf --type q8_0 - Stick with CustomVoice — the
custom-voicecommand works without a speaker encoder.
On some Apple Silicon macs, Metal may hang after the first inference. Use --backend cpu as fallback.
audio.cpp resolves symlinks with weakly_canonical, breaking HF blob paths. Use rsync -aL or prepare_models.sh.
mkdir -p <model_dir>/model_specs
cp /tmp/audio-cpp-clone/model_specs/qwen3_tts.json <model_dir>/model_specs/.
├── scripts/
│ └── prepare_models.sh # Mirror models from HF cache
├── Qwen3TTSDemo/
│ ├── main.swift # Swift CLI entry point
│ ├── MiniTTSDemoBridge.h # ObjC bridge interface
│ ├── MiniTTSDemoBridge.mm # ObjC++ → audio.cpp C++ engine
│ └── Qwen3TTSDemo-Bridging-Header.h # Swift bridging header
└── Qwen3TTSDemo.xcodeproj/ # Xcode project (links libAudioCpp.a)
MIT — see audio.cpp for the upstream engine license.