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/pcmstreaming 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, andseed - In
clonemode, supports generation and reuse ofspeaker.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-aican 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:
- Load model and voice configuration
- Initialize CUDA graphs
- Execute one warmup request
/health'sreadychanges totrue- 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
/healthand considerready=trueas the readiness indicator - The Docker
healthcheckin the documentation is primarily for detecting HTTP service liveness; if strict readiness is required, additional checks should be performed on the/healthresponse body'sreadyfield
Health Check
GET /health
This interface is used to determine whether the service has completed model loading and startup warmup.
Request:
curl https://qwen3tts-ai.${LAZYCAT_BOX_DOMAIN}/healthResponse example:
{
"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=trueis active - The
modefield can be used to determine the current service operating mode:customindicates CustomVoice fixed voice mode,cloneindicates Base voice fingerprint cloning mode
Voice List Interface
GET /v1/audio/voices
Request:
curl https://qwen3tts-ai.${LAZYCAT_BOX_DOMAIN}/v1/audio/voicesResponse example:
{
"voices": [
"aiden",
"dylan",
"eric",
"ono_anna",
"ryan",
"serena",
"sohee",
"uncle_fu",
"vivian"
],
"default_voice": "vivian",
"mode": "custom"
}voiceRequest parameters must be obtained from here- If an invalid
voiceis provided, the service will fallback todefault_voice
TTS Interface
POST /v1/audio/speech
This interface is compatible with common OpenAI TTS API call patterns.
Typical JSON request headers:
Content-Type: application/json
Accept: audio/wavJSON request body example:
{
"model": "tts-1",
"input": "欢迎使用 Qwen3TTS 服务。",
"voice": "vivian",
"response_format": "wav",
"language": "Chinese",
"instruct": "请用温和、清晰、偏慢一点的语气朗读。",
"seed": 1234
}Parameter Description:
| Parameters | Type | Required | Description |
|---|---|---|---|
model | string | no | Compatible field, suggest passing tts-1 fixedly |
input | string | Yes | Pending synthesis text, must not be empty |
voice | string | no | Voice name; suggest first obtain via /v1/audio/voices |
response_format | string | no | Support wav / pcm / mp3 |
language | string | no | Language at request level, overrides the default language configuration for this voice upon input |
instruct | string | no | Custom instructions for controlling tone, style, rhythm, or accent bias |
seed | integer | no | Per-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
customorclonemode - If
instructis passed in the request, it takes precedence over the defaultinstructconfigured in the server's static voice settings - If
instructis 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 languageis used to override the default voice languageinstructis used to control tone, style, rhythm, or accent preferenceseedenables reproducibility of the same request within the same service versionvoice_clone_ptis only available inclonemode and requires file upload viamultipart/form-data
Usage recommendations for seed:
Recommend passing a non-negative integer, e.g.,
1234To reproduce results,
input,voice,language,instructshould all be fixed simultaneously,response_formatReproduce the conclusion under the same container image version, same model weights, and same voice configuration.
When
seedis not provided, the request follows the default random path and does not guarantee consistent results across multiple requests.
language execution rules:
- When
languageis provided in the request, it takes precedence over the default language set in the server's static voice configuration. - If
languageis not provided in the request, the current service default behavior is adopted; for Orin custom images, the default is generallyAuto. - 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
voicerequest parameter is case-insensitive. It is recommended to first obtain valid values via/v1/audio/voices. - If an invalid
voiceis provided, the service will fall back todefault_voice.
Currently unsupported request-level parameters:
speakerinstructionstemperaturedo_sampletop_ktop_prepetition_penalty
Notes:
- In the OpenAI interface, use
voiceinstead ofspeaker - In the OpenAI interface, use
instructinstead ofinstructions - Internal sampling-related parameters are currently not exposed as OpenAI request fields; they should not be passed externally in the documentation
voice_clone_ptis not a JSON field; ifptfile needs to be passed, usemultipart/form-datainstead
Response Notes:
response_format | Content-Type | Is it streaming? | Description |
|---|---|---|---|
wav | audio/wav | Yes | First return the WAV header, then continuously return PCM audio frames |
pcm | audio/pcm | Yes | Return 16-bit little-endian PCM audio stream |
mp3 | audio/mpeg | no | First 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:
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
ptfile - The calling party is responsible for saving the
ptfile, for example by storing it in object storage, NAS, database, or business-side file directories - For subsequent requests from the same speaker, the same
ptcan 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:
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.wavAvailable file field names:
voice_clone_ptspeaker_ptpt
Execution rules:
- Available only in
clonemode - When uploading
voice_clone_ptvia request, it takes precedence over the staticvoices.jsonin the server'sspeaker_pt - For the same
ptpaired with different text inputs, the same speaker characteristics should be maintained; to improve reproducibility, it is recommended to also fixseed
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:
{
"alice": {
"speaker_pt": "/data/voices/alice.pt",
"language": "Chinese"
},
"narrator": {
"speaker_pt": "/data/voices/narrator.pt",
"language": "Chinese",
"instruct": "请用平稳、自然的旁白语气朗读。"
}
}After startup:
GET /v1/audio/voicesreturnsaliceandnarrator- When calling
POST /v1/audio/speech, passvoice=aliceorvoice=narratordirectly - The request body can continue using standard JSON; there's no need to re-upload
pteach time speaker_ptandinstructcan be used simultaneously; ifvoices.jsonis configured with a defaultinstruct, passinginstructin the request will override the static configuration- However, in the
speaker_ptscenario,instructis still considered experimental and its stability is not guaranteed; if you strongly depend on instruction following, prefer using the ICL mode ofref_audio + ref_text
Example of non-streaming call
Generate WAV
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.wavGenerate WAV and pass to instruct
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.wavGenerate WAV and fix seed
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.wavExplanation:
- When the same request body is called repeatedly, using the same
seedconsistently 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
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.wavGenerate MP3
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.mp3mp3encodes audio after full audio generation, making it unsuitable for low-latency first-packet scenarios- For edge reception and playback, prefer
wavorpcm
Streaming Call Specification
The "streaming" here refers to direct return of audio byte streams, not SSE text event streaming:
wav/pcmreturn audio in chunks via HTTP chunked transfermp3does not support real-time generation and return
curl streaming save
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.wavPython Stream Reading
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
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
{
"detail": "'input' text is empty"
}Or:
{
"detail": "response_format 'aac' not supported. Use: wav, pcm, mp3"
}400 Voice Tone Not Found
{
"detail": "Voice 'unknown' is not configured. Available voices: ['aiden', 'vivian']"
}503 Service Not Ready
{
"detail": "Model not loaded"
}Independent Deployment
Solution One: CustomVoice with Fixed Voice
- Save the following content into a
docker-compose.ymlfile.
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- Run
docker-compose up -dto start in the current directory - First use
http://127.0.0.1:8000/healthto check if the service is alreadyready=true - Then use
http://127.0.0.1:8000/v1/audio/voicesto get available voices - Finally invoke
http://127.0.0.1:8000/v1/audio/speech
Recommended post-start verification sequence:
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.wavIf you're debugging locally on the device instead of connecting to the AI Pod gateway, change the above expose to:
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
seedin the request speedis 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:
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-networkRecommended directory structure:
/data
├── voices.json
└── voices
├── alice.pt
└── narrator.ptRecommended call sequence:
- First invoke
POST /v1/audio/voice-clone/ptto generatespeaker.pt - Save
speaker.ptto/data/voices/*.pt - Update
/data/voices.json - Restart the service or redeploy to load the new
voices.json - Then directly call
voice=<配置名>to invokePOST /v1/audio/speech
Alternatively, if you do not wish to restart the service, you may proceed with request-level upload:
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.wavDifferences from the current tts-ai service
tts-aiThis document corresponds to theKokoroserviceqwen3tts-aiis an OpenAI-compatible Qwen3TTS serviceqwen3tts-aiis suitable for multi-voice, OpenAI-compatible clients, and streaming audio response scenarios