Qwen3-TTS Integration Guide

February 1, 2026 · View on GitHub

Default Server: http://localhost:8881
API: OpenAI-compatible /v1/audio/speech
GPU: Optimized for NVIDIA RTX 50-series Blackwell / RTX 40/30-series
Model: Qwen3-TTS-12Hz-1.7B-Base (Voice Cloning)

Use Case: Drop-in replacement for OpenAI TTS or Kokoro TTS. Same API, local GPU inference, 28 custom cloned voices.


Quick Start

Replace OpenAI TTS (One Line Change)

from openai import OpenAI

# BEFORE: OpenAI Cloud TTS
# client = OpenAI(api_key="sk-...")

# AFTER: Local Qwen3-TTS
client = OpenAI(
    base_url="http://localhost:8881/v1",
    api_key="not-needed"
)

# Same code works for both!
response = client.audio.speech.create(
    model="tts-1",
    voice="Jarvis",      # Your custom cloned voice
    input="Hello! This is your local TTS server speaking.",
    response_format="mp3"
)

response.stream_to_file("output.mp3")

Switching Between OpenAI and Qwen3-TTS

Option 1: Environment Variable Toggle

import os
from openai import OpenAI

# Set in environment or .env file:
# TTS_PROVIDER=qwen3  (local) or TTS_PROVIDER=openai (cloud)

TTS_PROVIDER = os.getenv("TTS_PROVIDER", "qwen3")

if TTS_PROVIDER == "qwen3":
    client = OpenAI(
        base_url="http://localhost:8881/v1",
        api_key="not-needed"
    )
    default_voice = "Jarvis"  # Custom cloned voice
else:
    client = OpenAI(
        api_key=os.getenv("OPENAI_API_KEY")
    )
    default_voice = "alloy"  # OpenAI voice

def text_to_speech(text: str, voice: str = None) -> bytes:
    """Generate speech - works with both OpenAI and Qwen3-TTS."""
    response = client.audio.speech.create(
        model="tts-1",
        voice=voice or default_voice,
        input=text,
        response_format="mp3"
    )
    return response.content

Option 2: Provider Class

import os
from openai import OpenAI
from typing import Optional

class TTSProvider:
    """Switchable TTS provider supporting OpenAI and Qwen3-TTS."""
    
    def __init__(self, provider: str = "qwen3"):
        self.provider = provider
        
        if provider == "qwen3":
            self.client = OpenAI(
                base_url="http://localhost:8881/v1",
                api_key="not-needed"
            )
            self.voices = [
                "Jarvis", "Paddington", "Professor", "Victoria",
                "Josh", "John", "Mark", "Adam", "Russell",
                "Lucy", "Carmen", "Caroline", "Joanne", "Natasha"
            ]
        else:
            self.client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
            self.voices = ["alloy", "echo", "fable", "onyx", "nova", "shimmer"]
    
    def speak(self, text: str, voice: Optional[str] = None, 
              output_file: Optional[str] = None) -> bytes:
        """Generate speech audio."""
        response = self.client.audio.speech.create(
            model="tts-1",
            voice=voice or self.voices[0],
            input=text,
            response_format="mp3"
        )
        
        if output_file:
            response.stream_to_file(output_file)
        
        return response.content
    
    def list_voices(self) -> list:
        """Return available voices for current provider."""
        return self.voices


# Usage
tts = TTSProvider("qwen3")  # or "openai"
tts.speak("Hello world!", voice="Jarvis", output_file="output.mp3")

Custom Cloned Voices (28 Total)

These voices are cloned from reference audio samples. First request per voice takes ~8-18s to build the voice profile, subsequent requests are ~4-6s.

Male Voices

VoiceDescriptionStyle
JarvisCalm and soothingAI assistant, narration
PaddingtonBritish narrator, deep and warmAudiobooks, storytelling
ProfessorBritish, trustworthyEducational, documentary
JoshDeep voice, AmericanPodcasts, voiceover
JohnDeep maleGeneral narration
MarkNatural conversationsCasual, dialogue
AdamLate night radioSmooth, relaxed
RussellDramatic British TVDrama, announcements
CurtCosmic storyteller, jokerEntertainment, fun
EustisFast speakingEnergetic content
General_JoeWWII NarratorHistorical, military
GrandpaElderly maleWarm, storytelling
NigelMysterious, intriguingMystery, thriller
RichardClear maleProfessional
ValentinoSmooth maleRomance, luxury
WildebeestDeep male voicePowerful narration

Female Voices

VoiceDescriptionStyle
LucySweet and sensualSoft narration
CarmenRealistic, casual, lovelyNatural dialogue
CarolineExcellent for narrationAudiobooks
JoannePensive, introspective, softThoughtful content
VictoriaClassy British mature womanElegant, sophisticated
NatashaValley girlCasual, youthful
BiancaCity girlUrban, modern
CecileOld woman, confident, authoritativeWise, commanding
EmmalineYoung British girlYouthful British
MonikaIndian femaleDiverse accents
TallyExpressive, phenomenal rangeDramatic, emotional
VillainSexy female villainDramatic, character

API Reference

Generate Speech

POST /v1/audio/speech

{
  "model": "tts-1",           # or "tts-1-hd", "qwen3-tts"
  "voice": "Jarvis",          # any custom voice name
  "input": "Text to speak",
  "response_format": "mp3",   # mp3, opus, aac, flac, wav, pcm
  "speed": 1.0                # 0.25 to 4.0
}

cURL Example

curl -X POST http://localhost:8881/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{
    "model": "tts-1",
    "voice": "Jarvis",
    "input": "Hello, this is Jarvis speaking.",
    "response_format": "mp3"
  }' \
  --output output.mp3

List Voices

curl http://localhost:8881/v1/voices

Health Check

curl http://localhost:8881/health

VRAM Management (Auto-Unload)

The model uses ~4.5GB VRAM. If you share your GPU with other apps (Ollama, Kokoro TTS, etc.), you can enable auto-unload to free VRAM after inactivity.

Configuration

Set in docker-compose.yml or environment:

# Unload model after 15 minutes of inactivity (0 = disabled)
TTS_INACTIVITY_TIMEOUT_MINUTES=15

Manual Control

# Free VRAM immediately
curl -X POST http://localhost:8881/admin/unload

# Pre-load model (optional - happens automatically on next request)
curl -X POST http://localhost:8881/admin/reload

# Check status (shows idle time and countdown)
curl http://localhost:8881/health

Health Response with Auto-Unload

{
  "status": "healthy",
  "backend": {
    "name": "official",
    "model_id": "Qwen/Qwen3-TTS-12Hz-1.7B-Base",
    "ready": true
  },
  "auto_unload": {
    "enabled": true,
    "timeout_minutes": 15,
    "idle_seconds": 120,
    "unload_in_seconds": 780
  }
}

Language Support

Force a specific language with model suffix:

ModelLanguage
tts-1-enEnglish
tts-1-zhChinese
tts-1-jaJapanese
tts-1-koKorean
tts-1-deGerman
tts-1-frFrench
tts-1-esSpanish
tts-1-ruRussian
tts-1-ptPortuguese
tts-1-itItalian

Audio Formats

FormatUse Case
mp3Universal, good compression
opusBest quality/size, streaming
aacApple devices
flacLossless quality
wavUncompressed, editing
pcmRaw audio data

Performance

MetricValue
First request (new voice)~8-18s (builds voice profile)
Cached voice requests~4-6s
VRAM usage~4.5 GB
Sample rate24kHz
ModelQwen3-TTS-12Hz-1.7B-Base

Framework Integration Examples

FastAPI Backend

from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from openai import OpenAI
import io

app = FastAPI()

tts_client = OpenAI(
    base_url="http://localhost:8881/v1",
    api_key="not-needed"
)

@app.post("/api/tts")
async def text_to_speech(text: str, voice: str = "Jarvis"):
    try:
        response = tts_client.audio.speech.create(
            model="tts-1",
            voice=voice,
            input=text,
            response_format="mp3"
        )
        return StreamingResponse(
            io.BytesIO(response.content),
            media_type="audio/mpeg",
            headers={"Content-Disposition": "attachment; filename=speech.mp3"}
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

Node.js / TypeScript

import OpenAI from 'openai';
import fs from 'fs';

const ttsClient = new OpenAI({
  baseURL: 'http://localhost:8881/v1',
  apiKey: 'not-needed',
});

async function speak(text: string, voice: string = 'Jarvis'): Promise<Buffer> {
  const response = await ttsClient.audio.speech.create({
    model: 'tts-1',
    voice: voice,
    input: text,
  });
  
  return Buffer.from(await response.arrayBuffer());
}

// Usage
const audio = await speak('Hello from TypeScript!', 'Paddington');
fs.writeFileSync('output.mp3', audio);

Gradio App

import gradio as gr
from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:8881/v1",
    api_key="not-needed"
)

VOICES = ["Jarvis", "Paddington", "Professor", "Victoria", "Josh", 
          "Lucy", "Carmen", "Caroline", "Natasha", "Russell"]

def generate_speech(text, voice):
    response = client.audio.speech.create(
        model="tts-1",
        voice=voice,
        input=text,
        response_format="mp3"
    )
    
    output_path = "/tmp/gradio_tts.mp3"
    with open(output_path, "wb") as f:
        f.write(response.content)
    return output_path

demo = gr.Interface(
    fn=generate_speech,
    inputs=[
        gr.Textbox(label="Text to speak", lines=3),
        gr.Dropdown(VOICES, label="Voice", value="Jarvis")
    ],
    outputs=gr.Audio(label="Generated Speech"),
    title="Qwen3-TTS Voice Cloning"
)

demo.launch()

Open WebUI Integration

Add as TTS provider in Open WebUI settings:

  • URL: http://localhost:8881/v1
  • API Key: not-needed
  • Model: tts-1
  • Voice: Jarvis (or any custom voice)

Home Assistant

tts:
  - platform: openai_tts
    api_key: "not-needed"
    base_url: "http://localhost:8881/v1"
    model: "tts-1"
    voice: "Jarvis"

Adding New Voices

To add your own voice samples:

  1. Prepare audio: 3-10 seconds of clear speech (.wav preferred)
  2. Name the file: VoiceName.wav (e.g., MyVoice.wav)
  3. Copy to voices folder:
    cp MyVoice.wav ./sample-voices-xtts/
    
  4. Restart container:
    docker compose restart qwen3-tts-gpu
    
  5. Use the voice: voice="MyVoice"

Server Management

# View logs
docker compose logs -f qwen3-tts-gpu

# Restart
docker compose restart qwen3-tts-gpu

# Stop
docker compose down

# Start
docker compose up -d qwen3-tts-gpu

# Check health
curl http://localhost:8881/health

# Free VRAM (when you need GPU for other apps)
curl -X POST http://localhost:8881/admin/unload

Troubleshooting

Voice not found

Check exact spelling. Voice names are case-insensitive but must match file names.

Slow first request for a voice

Normal - first request builds the voice profile (~8-18s). Subsequent requests are fast (~4-6s).

Connection refused

Check if container is running: docker compose ps

Out of memory

VRAM usage is ~4.5GB. Use auto-unload feature if sharing GPU with other apps.

Model unloaded (first request slow after idle)

If auto-unload is enabled, the model unloads after inactivity. First request will reload (~10-15s).


Comparison: Qwen3-TTS vs OpenAI TTS

FeatureQwen3-TTS (Local)OpenAI TTS (Cloud)
CostFree (GPU power)$15/1M chars
Privacy100% localData sent to cloud
Latency~4-6s~1-2s
Voices28 custom cloned6 preset
Voice Cloning✅ Yes❌ No
Custom Voices✅ Add your own❌ No
API Compatible✅ Same API✅ Native
Offline✅ Yes❌ No
VRAM Management✅ Auto-unloadN/A

Quick Voice Reference

Male Voices:
  Jarvis, Paddington, Professor, Josh, John, Mark, Adam, Russell,
  Curt, Eustis, General_Joe, Grandpa, Nigel, Richard, Valentino, Wildebeest

Female Voices:
  Lucy, Carmen, Caroline, Joanne, Victoria, Natasha, Bianca,
  Cecile, Emmaline, Monika, Tally, Villain

Last updated: January 2026
Model: Qwen3-TTS-12Hz-1.7B-Base