Skip to content

Qwen3TTS (OpenAI-Compatible TTS) ​

The Qwen3TTS is an OpenAI-compatible text-to-speech service packaged based on faster-qwen3-tts. The recommended service name within the AI Pod is qwen3tts-ai, suitable for integration with OpenWebUI, OpenAI-compatible SDKs, or any custom HTTP client.

Compared to Kokoro tts-ai, this service features:

  • Compatibility with POST /v1/audio/speech
  • Support for wav / pcm streaming audio output
  • Warm-up of the model during startup, then marking ready externally
  • Support for multi-voice query interface /v1/audio/voices
  • Support for request-level language, instruct, and seed
  • In clone mode, supports generation and reuse of speaker.pt
  • Additional provision of POST /v1/audio/voice-clone/pt

Service Endpoints ​

  • Recommended service domain: qwen3tts-ai
  • If using the current example compose file, qwen3-tts-ai can also be used
  • Health check: /health
  • Voice list: /v1/audio/voices
  • TTS endpoint: /v1/audio/speech
  • PT generation endpoint:/v1/audio/voice-clone/pt

Example access forms after deployment to AI Pod:

  • Service address: https://qwen3tts-ai.${LAZYCAT_BOX_DOMAIN}
  • Health check: https://qwen3tts-ai.${LAZYCAT_BOX_DOMAIN}/health
  • Voice list: https://qwen3tts-ai.${LAZYCAT_BOX_DOMAIN}/v1/audio/voices
  • TTS interface: https://qwen3tts-ai.${LAZYCAT_BOX_DOMAIN}/v1/audio/speech
  • PT generation interface: https://qwen3tts-ai.${LAZYCAT_BOX_DOMAIN}/v1/audio/voice-clone/pt

Startup and Launch Behavior ​

qwen3tts-ai differs from traditional "process startup equals ready" services in that it performs a startup warmup round before officially accepting requests.

The startup process is as follows:

  1. Load model and voice configuration
  2. Initialize CUDA graphs
  3. Execute one warmup request
  4. /health's ready changes to true
  5. Only then should upstream AI applications or scripts initiate formal TTS requests

Key considerations during execution:

  • The container process being up does not mean the service is ready
  • Applications should poll /health and consider ready=true as the readiness indicator
  • The Docker healthcheck in the documentation is primarily for detecting HTTP service liveness; if strict readiness is required, additional checks should be performed on the /health response body's ready field

Health Check ​

GET /health ​

This interface is used to determine whether the service has completed model loading and startup warmup.

Request:

bash
curl https://qwen3tts-ai.${LAZYCAT_BOX_DOMAIN}/health

Response example:

json
{
  "status": "ok",
  "ready": true,
  "model_loaded": true,
  "mode": "custom",
  "voices": ["aiden", "dylan", "vivian"],
  "startup_warmup_enabled": true,
  "startup_warmup_completed": true,
  "startup_warmup_seconds": 15.11
}
  • Initiate formal TTS requests only when ready=true is active
  • The mode field can be used to determine the current service operating mode: custom indicates CustomVoice fixed voice mode, clone indicates Base voice fingerprint cloning mode

Voice List Interface ​

GET /v1/audio/voices ​

Request:

bash
curl https://qwen3tts-ai.${LAZYCAT_BOX_DOMAIN}/v1/audio/voices

Response example:

json
{
  "voices": [
    "aiden",
    "dylan",
    "eric",
    "ono_anna",
    "ryan",
    "serena",
    "sohee",
    "uncle_fu",
    "vivian"
  ],
  "default_voice": "vivian",
  "mode": "custom"
}
  • voice Request parameters must be obtained from here
  • If an invalid voice is provided, the service will fallback to default_voice

TTS Interface ​

POST /v1/audio/speech ​

This interface is compatible with common OpenAI TTS API call patterns.

Typical JSON request headers:

http
Content-Type: application/json
Accept: audio/wav

JSON request body example:

json
{
  "model": "tts-1",
  "input": "欢迎使用 Qwen3TTS 服务。",
  "voice": "vivian",
  "response_format": "wav",
  "language": "Chinese",
  "instruct": "请用温和、清晰、偏慢一点的语气朗读。",
  "seed": 1234
}

Parameter Description:

ParametersTypeRequiredDescription
modelstringnoCompatible field, suggest passing tts-1 fixedly
inputstringYesPending synthesis text, must not be empty
voicestringnoVoice name; suggest first obtain via /v1/audio/voices
response_formatstringnoSupport wav / pcm / mp3
languagestringnoLanguage at request level, overrides the default language configuration for this voice upon input
instructstringnoCustom instructions for controlling tone, style, rhythm, or accent bias
seedintegernoPer-request random seed. Same text, voice, language, instruction, and model version, fixing the same seed can reproduce the same audio output.

instruct Execution rules:

  • Available when the service is running in custom or clone mode
  • If instruct is passed in the request, it takes precedence over the default instruct configured in the server's static voice settings
  • If instruct is not provided in the request, the current service's default behavior is retained
  • If you're only concerned with OpenAI-compatible invocation, this can be understood as an optional extension field

Request-level extension parameters:

  • Exposed and recommended fields: language, instruct, seed
  • language is used to override the default voice language
  • instruct is used to control tone, style, rhythm, or accent preference
  • seed enables reproducibility of the same request within the same service version
  • voice_clone_pt is only available in clone mode and requires file upload via multipart/form-data

Usage recommendations for seed:

  • Recommend passing a non-negative integer, e.g., 1234

  • To reproduce results, input, voice, language, instruct should all be fixed simultaneously,response_format

  • Reproduce the conclusion under the same container image version, same model weights, and same voice configuration.

  • When seed is not provided, the request follows the default random path and does not guarantee consistent results across multiple requests.

language execution rules:

  • When language is provided in the request, it takes precedence over the default language set in the server's static voice configuration.
  • If language is not provided in the request, the current service default behavior is adopted; for Orin custom images, the default is generally Auto.
  • Common valid values can be used based on model capabilities, such as: Chinese, English, Japanese, Korean, German, French, Russian, Portuguese, Spanish, Italian.

voice execution rules:

  • The voice request parameter is case-insensitive. It is recommended to first obtain valid values via /v1/audio/voices.
  • If an invalid voice is provided, the service will fall back to default_voice.

Currently unsupported request-level parameters:

  • speaker
  • instructions
  • temperature do_sample
  • top_k
  • top_p
  • repetition_penalty

Notes:

  • In the OpenAI interface, use voice instead of speaker
  • In the OpenAI interface, use instruct instead of instructions
  • Internal sampling-related parameters are currently not exposed as OpenAI request fields; they should not be passed externally in the documentation
  • voice_clone_pt is not a JSON field; if pt file needs to be passed, use multipart/form-data instead

Response Notes:

response_formatContent-TypeIs it streaming?Description
wavaudio/wavYesFirst return the WAV header, then continuously return PCM audio frames
pcmaudio/pcmYesReturn 16-bit little-endian PCM audio stream
mp3audio/mpegnoFirst generate the complete audio, then encode it as an mp3.

PT Voice Cloning Interface ​

The following capabilities are available only when the service runs in clone mode. Typical container image:

  • registry.lazycat.cloud/x/faster-qwen3-tts:0.6b-base-clone-openai-orin-v2

In clone mode, it is recommended to fix voice as dynamic, and determine the speaker via request-level voice_clone_pt or static voices.json.

POST /v1/audio/voice-clone/pt ​

This interface extracts a speaker embedding from a reference audio segment and directly returns the corresponding pt file to the caller.

Request methods:

  • Content-Type: multipart/form-data
  • File field: ref_audio
  • Optional form field: filename

Request example:

bash
curl https://qwen3tts-ai.${LAZYCAT_BOX_DOMAIN}/v1/audio/voice-clone/pt \
  -F "ref_audio=@ref_audio.wav;type=audio/wav" \
  -F "filename=speaker.pt" \
  --output speaker.pt
  • The server does not persist the pt file
  • The calling party is responsible for saving the pt file, for example by storing it in object storage, NAS, database, or business-side file directories
  • For subsequent requests from the same speaker, the same pt can be reused

Use pt directly in the request ​

Once you have obtained speaker.pt, you can upload it directly in POST /v1/audio/speech.

In this case, the request must use multipart/form-data, as both text and file fields need to be transmitted.

Request example:

bash
curl https://qwen3tts-ai.${LAZYCAT_BOX_DOMAIN}/v1/audio/speech \
  -H "Accept: audio/wav" \
  -F "model=tts-1" \
  -F "input=今天的风从江面慢慢吹过来,像有人把一句话轻轻放在耳边。" \
  -F "voice=dynamic" \
  -F "response_format=wav" \
  -F "language=Chinese" \
  -F "seed=1234" \
  -F "voice_clone_pt=@speaker.pt;type=application/octet-stream" \
  --output speech.wav

Available file field names:

  • voice_clone_pt
  • speaker_pt
  • pt

Execution rules:

  • Available only in clone mode
  • When uploading voice_clone_pt via request, it takes precedence over the static voices.json in the server's speaker_pt
  • For the same pt paired with different text inputs, the same speaker characteristics should be maintained; to improve reproducibility, it is recommended to also fix seed

Mount pt statically into the service ​

If you don't want to re-upload speaker.pt on every request, you can mount the pt file along with voices.json into the container.

voices.json example:

json
{
  "alice": {
    "speaker_pt": "/data/voices/alice.pt",
    "language": "Chinese"
  },
  "narrator": {
    "speaker_pt": "/data/voices/narrator.pt",
    "language": "Chinese",
    "instruct": "请用平稳、自然的旁白语气朗读。"
  }
}

After startup:

  • GET /v1/audio/voices returns alice and narrator
  • When calling POST /v1/audio/speech, pass voice=alice or voice=narrator directly
  • The request body can continue using standard JSON; there's no need to re-upload pt each time
  • speaker_pt and instruct can be used simultaneously; if voices.json is configured with a default instruct, passing instruct in the request will override the static configuration
  • However, in the speaker_pt scenario, instruct is still considered experimental and its stability is not guaranteed; if you strongly depend on instruction following, prefer using the ICL mode of ref_audio + ref_text

Example of non-streaming call ​

Generate WAV ​

bash
curl https://qwen3tts-ai.${LAZYCAT_BOX_DOMAIN}/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{"model":"tts-1","input":"欢迎使用 Qwen3TTS 服务。","voice":"vivian","response_format":"wav"}' \
  --output speech.wav

Generate WAV and pass to instruct ​

bash
curl https://qwen3tts-ai.${LAZYCAT_BOX_DOMAIN}/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{"model":"tts-1","input":"欢迎使用 Qwen3TTS 服务。","voice":"vivian","response_format":"wav","instruct":"请用温和、清晰、偏慢一点的语气朗读。"}' \
  --output speech.wav

Generate WAV and fix seed ​

bash
curl https://qwen3tts-ai.${LAZYCAT_BOX_DOMAIN}/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{"model":"tts-1","input":"欢迎使用 Qwen3TTS 服务。","voice":"vivian","response_format":"wav","seed":1234}' \
  --output speech.wav

Explanation:

  • When the same request body is called repeatedly, using the same seed consistently yields the same result
  • If you replace the text, voice, language, instruction, model version, or container image version, the output will also change.

Generate WAV with explicit language + instruct + seed ​

bash
curl https://qwen3tts-ai.${LAZYCAT_BOX_DOMAIN}/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{"model":"tts-1","input":"其实我真的有发现,我是一个特别善于观察别人情绪的人。","voice":"vivian","response_format":"wav","language":"Chinese","instruct":"用特别愤怒的语气说","seed":1234}' \
  --output speech.wav

Generate MP3 ​

bash
curl https://qwen3tts-ai.${LAZYCAT_BOX_DOMAIN}/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{"model":"tts-1","input":"欢迎使用 Qwen3TTS 服务。","voice":"vivian","response_format":"mp3"}' \
  --output speech.mp3
  • mp3 encodes audio after full audio generation, making it unsuitable for low-latency first-packet scenarios
  • For edge reception and playback, prefer wav or pcm

Streaming Call Specification ​

The "streaming" here refers to direct return of audio byte streams, not SSE text event streaming:

  • wav / pcm return audio in chunks via HTTP chunked transfer
  • mp3 does not support real-time generation and return

curl streaming save ​

bash
curl -N https://qwen3tts-ai.${LAZYCAT_BOX_DOMAIN}/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{"model":"tts-1","input":"今天我们测试流式音频返回。","voice":"vivian","response_format":"wav"}' \
  --output stream.wav

Python Stream Reading ​

python
import requests

url = f"https://qwen3tts-ai.{LAZYCAT_BOX_DOMAIN}/v1/audio/speech"
payload = {
    "model": "tts-1",
    "input": "今天我们测试流式音频返回。",
    "voice": "vivian",
    "response_format": "wav",
    "language": "Chinese",
    "instruct": "请用自然停顿、比较平稳的语气朗读。",
}

with requests.post(url, json=payload, stream=True, timeout=600) as resp:
    resp.raise_for_status()
    with open("stream.wav", "wb") as f:
        for chunk in resp.iter_content(chunk_size=4096):
            if chunk:
                f.write(chunk)

JavaScript Streamed Reading ​

javascript
const resp = await fetch(`https://qwen3tts-ai.${boxDomain}/v1/audio/speech`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    model: "tts-1",
    input: "今天我们测试流式音频返回。",
    voice: "vivian",
    response_format: "wav",
    language: "Chinese",
    instruct: "请用自然、平稳的旁白语气朗读。"
  })
});

if (!resp.ok) {
  throw new Error(`HTTP ${resp.status}`);
}

const reader = resp.body.getReader();
const chunks = [];

while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  chunks.push(value);
}

const blob = new Blob(chunks, { type: "audio/wav" });

Common Errors ​

400 Parameter Error ​

json
{
  "detail": "'input' text is empty"
}

Or:

json
{
  "detail": "response_format 'aac' not supported. Use: wav, pcm, mp3"
}

400 Voice Tone Not Found ​

json
{
  "detail": "Voice 'unknown' is not configured. Available voices: ['aiden', 'vivian']"
}

503 Service Not Ready ​

json
{
  "detail": "Model not loaded"
}

Independent Deployment ​

Solution One: CustomVoice with Fixed Voice ​

  1. Save the following content into a docker-compose.yml file.
yml
services:
  faster-qwen3-tts:
    image: registry.lazycat.cloud/x/faster-qwen3-tts:0.6b-custom-openai-orin-v11
    environment:
      HF_HUB_OFFLINE: "1"
      TRANSFORMERS_OFFLINE: "1"
      HF_HOME: /root/.cache/huggingface
      XDG_CACHE_HOME: /root/.cache
      QWEN_TTS_MODEL: /opt/models/Qwen3-TTS-12Hz-0.6B-CustomVoice
      QWEN_TTS_MODE: custom
      QWEN_TTS_DEFAULT_VOICE: vivian
      QWEN_TTS_LANGUAGE: Auto
      QWEN_TTS_CHUNK_SIZE: "8"
      QWEN_TTS_WARMUP_MAX_NEW_TOKENS: "32"
    volumes:
      - /etc/timezone:/etc/timezone:ro
      - ${LZC_AGENT_CACHE_DIR}/cache:/root/.cache
      - ${LZC_AGENT_DATA_DIR}/data:/data
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.${LZC_SERVICE_ID}-qwen3tts.rule=Host(`qwen3tts-ai`)"
    expose:
      - 8000
    healthcheck:
      test: [ "CMD", "curl", "-f", "http://localhost:8000/health" ]
      interval: 30s
      timeout: 5s
      retries: 100
      start_period: 1s

networks:
  default:
    external: true
    name: traefik-shared-network
  1. Run docker-compose up -d to start in the current directory
  2. First use http://127.0.0.1:8000/health to check if the service is already ready=true
  3. Then use http://127.0.0.1:8000/v1/audio/voices to get available voices
  4. Finally invoke http://127.0.0.1:8000/v1/audio/speech

Recommended post-start verification sequence:

bash
curl http://127.0.0.1:8000/health
curl http://127.0.0.1:8000/v1/audio/voices
curl http://127.0.0.1:8000/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{"model":"tts-1","input":"启动校验。","voice":"vivian","response_format":"wav","instruct":"请用简短、清晰的方式朗读。"}' \
  --output verify.wav

If you're debugging locally on the device instead of connecting to the AI Pod gateway, change the above expose to:

yml
ports:
  - "8000:8000"

Current recommended deployment guidelines:

  • Recommended to use 0.6b-custom-openai-orin-v11
  • The recommended request fields for OpenAI interface currently are: voice, language, instruct, seed
  • If you need to reproduce the same audio, explicitly pass a fixed seed in the request
  • speed is currently accepted and compatible, but not yet activated; thus, it is not listed as a recommended parameter in the documentation

Option Two: Base + Clone + PT ​

If you want the caller to manage speaker.pt independently, or if you wish to bind the same pt into multiple named voice profiles, we recommend using the clone container image.

docker-compose.yml example:

yml
services:
  faster-qwen3-tts:
    image: registry.lazycat.cloud/x/faster-qwen3-tts:0.6b-base-clone-openai-orin-v2
    environment:
      HF_HUB_OFFLINE: "1"
      TRANSFORMERS_OFFLINE: "1"
      HF_HOME: /root/.cache/huggingface
      XDG_CACHE_HOME: /root/.cache
      QWEN_TTS_MODEL: /opt/models/Qwen3-TTS-12Hz-0.6B-Base
      QWEN_TTS_MODE: clone
      QWEN_TTS_DEFAULT_VOICE: dynamic
      QWEN_TTS_VOICES: /data/voices.json
      QWEN_TTS_LANGUAGE: Auto
      QWEN_TTS_CHUNK_SIZE: "8"
      QWEN_TTS_WARMUP_MAX_NEW_TOKENS: "32"
    volumes:
      - /etc/timezone:/etc/timezone:ro
      - ${LZC_AGENT_CACHE_DIR}/cache:/root/.cache
      - ${LZC_AGENT_DATA_DIR}/data:/data
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.${LZC_SERVICE_ID}-qwen3tts.rule=Host(`qwen3tts-ai`)"
    expose:
      - 8000
    healthcheck:
      test: [ "CMD", "curl", "-f", "http://localhost:8000/health" ]
      interval: 30s
      timeout: 5s
      retries: 100
      start_period: 1s

networks:
  default:
    external: true
    name: traefik-shared-network

Recommended directory structure:

text
/data
├── voices.json
└── voices
    ├── alice.pt
    └── narrator.pt

Recommended call sequence:

  1. First invoke POST /v1/audio/voice-clone/pt to generate speaker.pt
  2. Save speaker.pt to /data/voices/*.pt
  3. Update /data/voices.json
  4. Restart the service or redeploy to load the new voices.json
  5. Then directly call voice=<配置名> to invoke POST /v1/audio/speech

Alternatively, if you do not wish to restart the service, you may proceed with request-level upload:

bash
curl https://qwen3tts-ai.${LAZYCAT_BOX_DOMAIN}/v1/audio/speech \
  -H "Accept: audio/wav" \
  -F "model=tts-1" \
  -F "input=这是一次请求级 pt 调用。" \
  -F "voice=dynamic" \
  -F "response_format=wav" \
  -F "voice_clone_pt=@speaker.pt;type=application/octet-stream" \
  --output speech.wav

Differences from the current tts-ai service ​

  • tts-ai This document corresponds to the Kokoro service
  • qwen3tts-ai is an OpenAI-compatible Qwen3TTS service
  • qwen3tts-ai is suitable for multi-voice, OpenAI-compatible clients, and streaming audio response scenarios