Skip to content

Repository files navigation

Qwen3-TTS Mac FastAPI

An enterprise-ready web application and REST API wrapping Qwen3-TTS for Apple Silicon Macs via mlx-audio. Includes a beautiful dark-mode Studio UI and an OpenAI-compatible /v1/audio/speech endpoint for drop-in migration.


Features

  • Three TTS modes: Custom Voice (11 built-in speakers), Voice Design (describe a voice in natural language), Voice Clone (enroll any voice from a 5–30 s audio sample)
  • Six model variants: Pro 1.7B and Lite 0.6B, each available for all three modes — auto-selected based on your chosen mode
  • OpenAI-compatible API — point your existing OpenAI client at http://localhost:8765 with one line change
  • Async job queue with real-time SSE progress streaming
  • Persistent history — generated audio survives server restarts (SQLite)
  • Voice enrollment — upload any audio format (MP3, M4A, WAV…), automatically converted to PCM WAV
  • Language support — auto-detect or explicitly set one of 10 languages (EN, ZH, JA, KO, DE, FR, RU, PT, ES, IT)
  • Advanced parameters — temperature, top-p, max tokens
  • Runs 100% locally — no cloud, no data leaves your Mac

Requirements

Requirement Version
macOS Sequoia 15+ (Apple Silicon)
Python 3.10.x (via pyenv recommended)
ffmpeg Any recent version (brew install ffmpeg)
RAM 8 GB unified (16 GB+ recommended for Pro 1.7B)
HuggingFace account Optional — avoids download rate limits

Installation

# 1. Clone the repo
git clone https://github.com/bnfone/Qwen3-TTS-Mac-FastAPI.git
cd Qwen3-TTS-Mac-FastAPI

# 2. Run setup (creates venv, installs all dependencies)
./setup.sh

# 3. (Optional) Configure environment
cp .env.example .env
# Edit .env to set HF_TOKEN, API_KEY, etc.

The setup script:

  • Detects pyenv and selects Python 3.10
  • Creates .venv virtual environment
  • Installs all requirements including mlx-audio from source
  • Creates data/ directories

Running

./start.sh

Open http://localhost:8765 in your browser.

The server runs on port 8765 by default. Change PORT= in .env to use a different port.

On first use, models are downloaded automatically from HuggingFace (mlx-community). The default 1.7B CustomVoice model (~2.1 GB) is pre-loaded at startup.


Usage

Studio UI

The web UI has five tabs:

Tab Description
Generate Create speech in any of the three modes. Choose quality (Pro/Lite), language, and advanced parameters. Real-time progress bar.
Voices Browse 11 built-in speakers and manage your enrolled voice clones.
History Browse, search, play, and download all previously generated audio. Persists across restarts.
Models Load and unload model variants. View download status and memory usage.
API Docs Live curl examples and Python SDK snippet. Links to Swagger UI at /docs.

Generate Modes

Custom Voice — Pick from 11 built-in speakers and optionally add an emotion/style instruction:

  • English: Ryan, Aiden, Ethan, Chelsie, Serena, Vivian
  • Chinese: Uncle_Fu, Dylan, Eric
  • Japanese: Ono_Anna · Korean: Sohee

Voice Design — Describe a voice in natural language. The model creates a unique voice matching your description. Example: "A deep, warm male narrator with a slight British accent"

Voice Clone — First enroll a voice (5–30 seconds of clean audio + exact transcript). Then select it from the dropdown to generate speech in that voice.


API Reference

All endpoints accept and return JSON. Full interactive documentation available at http://localhost:8765/docs.

TTS Generation

POST /api/tts/generate

Submit an async generation job. Returns a job_id immediately.

curl -X POST http://localhost:8765/api/tts/generate \
  -H "Content-Type: application/json" \
  -d '{
    "text": "The quick brown fox jumps over the lazy dog.",
    "mode": "custom_voice",
    "speaker": "Ryan",
    "instruct": "Excited and enthusiastic",
    "speed": 1.1,
    "language": "en",
    "response_format": "wav"
  }'

Request fields:

Field Type Default Description
text string required Text to synthesize (max 10 000 chars)
mode string custom_voice custom_voice, voice_design, or voice_clone
speaker string Ryan Built-in speaker name (custom_voice mode)
instruct string "" Emotion/style instruction (custom_voice mode)
speed float 1.0 Speech rate (0.5–2.0)
voice_description string Voice description (voice_design mode)
cloned_voice_id string Enrolled voice ID (voice_clone mode)
language string auto Language code: auto, en, zh, ja, ko, de, fr, ru, pt, es, it
temperature float 0.6 Sampling temperature (0.0–2.0)
top_p float 0.8 Nucleus sampling (0.0–1.0)
max_tokens int 1200 Maximum audio tokens (100–4096)
response_format string wav Output format: wav or mp3

GET /api/tts/jobs/{job_id}

Poll job status.

curl http://localhost:8765/api/tts/jobs/<job_id>

GET /api/tts/jobs/{job_id}/events

Real-time SSE progress stream.

curl -N http://localhost:8765/api/tts/jobs/<job_id>/events
# → event: status,   data: Loading model…
# → event: progress, data: 0.12
# → event: progress, data: 0.45
# → event: completed, data: 20260314_123456_hello.wav

OpenAI-Compatible Endpoint

POST /v1/audio/speech

Drop-in replacement for the OpenAI TTS API.

curl http://localhost:8765/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{
    "model": "tts-1-hd",
    "input": "Hello from local Qwen3-TTS!",
    "voice": "alloy",
    "speed": 1.0,
    "response_format": "mp3",
    "language": "en"
  }' --output speech.mp3

Voice mapping:

OpenAI voice Qwen3-TTS speaker
alloy Ryan
echo Aiden
fable Ethan
onyx Uncle_Fu
nova Serena
shimmer Vivian

Model mapping:

OpenAI model Qwen3-TTS variant
tts-1 Lite 0.6B CustomVoice
tts-1-hd Pro 1.7B CustomVoice

Voice Management

# List all voices
curl http://localhost:8765/api/voices

# Enroll a new cloned voice
curl -X POST http://localhost:8765/api/voices/clone \
  -F "audio=@reference.wav" \
  -F "name=MyNarrator" \
  -F "ref_text=Exact words spoken in the audio file" \
  -F "description=My custom narrator voice"

# Delete a cloned voice
curl -X DELETE http://localhost:8765/api/voices/cloned/<voice_id>

History

# List history (paginated)
curl "http://localhost:8765/api/history?page=1&limit=20&search=hello"

# Delete a history entry + audio file
curl -X DELETE http://localhost:8765/api/history/<job_id>

# Cleanup entries older than 7 days
curl -X POST http://localhost:8765/api/history/cleanup \
  -H "Content-Type: application/json" \
  -d '{"older_than_days": 7}'

OpenAI SDK Migration

Change exactly one line in your existing code:

from openai import OpenAI

# Before:
# client = OpenAI(api_key="sk-...")

# After — only base_url changes:
client = OpenAI(
    api_key="not-needed",
    base_url="http://localhost:8765",
)

response = client.audio.speech.create(
    model="tts-1-hd",
    voice="nova",
    input="Hello from local Qwen3-TTS!",
)
response.stream_to_file("speech.mp3")

Configuration

Copy .env.example to .env and edit as needed:

Variable Default Description
DEFAULT_MODEL 1.7b-custom Model variant preloaded at startup
PRELOAD_ON_STARTUP true Whether to load the default model on start
HF_TOKEN HuggingFace token (avoids download rate limits)
API_KEY Optional bearer token to protect the API
PORT 8765 Server port
OUTPUTS_DIR data/outputs Directory for generated audio files
VOICES_DIR data/voices Directory for cloned voice enrollments
HISTORY_DB_PATH data/history.db SQLite database for generation history
OUTPUT_TTL_HOURS 72 Auto-cleanup TTL for generated files
MAX_OUTPUT_DIR_MB 2000 Maximum storage for generated audio

Testing

Install test dependencies (already in requirements.txt):

source .venv/bin/activate
pip install pytest pytest-asyncio pytest-cov httpx

Run all tests:

pytest tests/ -v

Run with coverage report:

pytest tests/ -v --cov=app --cov-report=term-missing

What the tests cover:

  • All API endpoints (TTS generation, OpenAI compat, voices, history, models)
  • Service unit tests (model variant resolution, job lifecycle, SQLite persistence)
  • Request validation (missing fields, out-of-range values → 422)
  • No real model downloads — mlx-audio is mocked

Known Limitations & Workarounds

Speed control — post-processing workaround

The speed parameter is accepted by mlx_audio's model.generate() but is not forwarded to the underlying generate_custom_voice() / generate_voice_design() methods. The library's own docstring marks it as "not directly supported yet" (see mlx_audio/tts/models/qwen3_tts/qwen3_tts.py, generate() line ~719).

Workaround implemented in app/services/tts_service.py: After generation, the raw audio array is time-stretched via numpy linear resampling before it is written to disk:

# _apply_speed() in tts_service.py
new_length = max(1, int(len(audio) / speed))
np.interp(np.linspace(0, len(audio) - 1, new_length), np.arange(len(audio)), audio)
  • speed > 1.0 → shorter array → faster playback at the same sample rate
  • speed < 1.0 → longer array → slower playback
  • speed = 1.0 → no-op (skipped entirely)

When mlx-audio adds native speed support, remove _apply_speed() and the post-processing block in TTSService.generate(), and re-add params["speed"] = request.speed in _build_params().


Architecture

Request → FastAPI Router
              ↓
         JobManager (creates job, returns job_id)
              ↓ (background task)
         generation_semaphore (asyncio.Semaphore(1) — MLX not thread-safe)
              ↓
         TTSService.generate()
           ├── ModelManager.get_model() → load from HuggingFace if needed
           ├── _generate_sync() → runs model.generate() in ThreadPoolExecutor
           │     └── pushes progress to queue.Queue (thread-safe)
           ├── _forward_progress() → bridges thread queue → asyncio SSE queue
           └── job_manager.complete_job() → persists to SQLite
              ↓
         Client streams progress via SSE (EventSource)

Tech stack:

  • Backend: FastAPI + uvicorn + sse-starlette + pydantic-settings
  • TTS engine: mlx-audio (Qwen3-TTS on Apple Silicon MLX)
  • Persistence: SQLite (stdlib sqlite3)
  • Audio conversion: ffmpeg (subprocess)
  • Frontend: Single HTML file — Tailwind CSS CDN + Alpine.js CDN (no build step)
  • Fonts: Syne (display) + DM Sans (body) + IBM Plex Mono (code/labels)

Credits


License

MIT

About

FastAPI web app for Qwen3-TTS on Apple Silicon (mlx-audio)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages