Skip to main content

Qwen3-TTS-12Hz-1.7B-CustomVoice

The Text-to-speech guide has runnable examples for the nine voices, the output formats, speaking rate, and splitting long texts.

Description

"Qwen3-TTS-12Hz-1.7B-CustomVoice" is a text-to-speech model by Alibaba with 1.7 billion parameters. You send text, you get back an audio file. It is the counterpart to Whisper-Large-V3-Turbo, which goes the other way and turns audio into text.

The model serves the /v1/audio/speech endpoint and ships nine built-in voices across thirteen languages and dialects. Speech is generated faster than real time: producing one second of audio takes roughly a fifth of a second on our hardware, and with streaming the first audio arrives in under 150 ms.

It supports and is suitable for:

  • Turning text into speech in thirteen languages and dialects
  • Nine distinct built-in voices, selected per request
  • Five output formats, including compressed ones for direct delivery to a browser
  • Adjusting the speaking rate between 0.25 and 4.0
  • Streaming, either as raw audio chunks or as OpenAI speech.audio.delta events, which starts the audio before generation has finished

The following limitations apply:

  • Voice cloning from your own reference audio is not available on this endpoint
  • response_format="aac" is not supported, although the OpenAI API defines it
  • The language parameter is validated, but a supported value changes little in practice, see Supported values for parameter language
  • voice is required. A request without it returns HTTP 400
  • Invalid parameter values return HTTP 400. Some responses name the parameter, others carry a generic message, see Error responses
  • Streaming needs response_format="wav" or "pcm". Every other format combined with stream returns HTTP 400, see Streaming
  • The model has no chat endpoint. /v1/chat/completions is not available for it
  • There is no markup language. [angry], <laugh>, and SSML tags are read out loud or dropped, see Controlling delivery with instructions
  • German text needs preparation before you send it, and short German lines are unreliable, see Writing text for German
  • seed is accepted but does not reproduce a previous result, see Reproducibility

Supported voices

All nine work on this endpoint. There is no separate "custom voice" list to request.

aiden, dylan, eric, ono_anna, ryan, serena, sohee, uncle_fu, vivian

Every voice speaks every supported language, and all nine transcribe back cleanly in German and English in our tests, so pick by how the voice sounds rather than by its native language. Measured average pitch on English text:

VoiceNative languagePitch
aidenEnglish147 Hz
dylanChinese (Beijing)164 Hz
ryanEnglish171 Hz
ericChinese (Sichuan)177 Hz
uncle_fuChinese218 Hz
soheeKorean220 Hz
serenaChinese246 Hz
vivianChinese250 Hz
ono_annaJapanese264 Hz

Two things to know before you pick one for German. The voices whose native language is not German carry an audible accent, which is a matter of taste rather than a defect. And instructions only shapes the delivery of voices with an English-native voice on English text, see Controlling delivery with instructions, so on German the voice itself is the only lever you have over the character of the output.

voice is required here, even though the OpenAI API defines it as optional: a request without it returns HTTP 400. An unknown name returns HTTP 400 as well, with a generic Got bad request. that does not name the valid values, so check your spelling against the list above rather than against the error.

Supported values for parameter language

Auto, Beijing_Dialect, Chinese, English, French, German, Italian, Japanese, Korean, Portuguese, Russian, Sichuan_Dialect, Spanish

These are the thirteen languages and dialects the model speaks, and they are the only values language accepts. Anything else, an ISO code such as de included, returns HTTP 400. Passing a supported value is allowed but rarely worth it: in our runs German text came back equally accurate with and without the parameter, so leaving it out is the simpler default.

Supported output formats

response_formatContent-TypeFirst bytes of the bodySize per second of audio
wavaudio/wavRIFFWAVE48.0 kB
pcmaudio/pcmheaderless samples48.0 kB
flacaudio/flacfLaCabout 23 kB
mp3audio/mpegmp3 frame sync FF F37.0 kB
opusaudio/oggOggS4.6 kB

wav is the default and the right choice when you process the audio further. For delivery to a browser or a phone, opus is roughly a ninth of the size at speech quality.

The sizes are normalised to one second of audio on purpose. The model samples, so two runs on the same text differ in length by a second or more, and comparing raw file sizes across formats compares the lengths rather than the formats. Normalised, pcm and wav are the same thing: 48.0 kB per second each, with wav adding a 44-byte header.

Content-Type names the format you asked for, so a player that reads the header decodes the right container. Writing the bytes to a file with the extension you asked for still works and needs no header parsing.

aac returns HTTP 400 and names the values that are allowed:

Input should be 'wav', 'pcm', 'flac', 'mp3' or 'opus'

Audio is 24 kHz, 16-bit, mono in every format. The loudness is not normalised: across our test set the peak level ranged from 0.23 to 0.89 of full scale, about 11 dB between the quietest and the loudest clip. If you play several clips one after another, run them through a loudness normaliser first.

Getting started

from openai import OpenAI

client = OpenAI(
base_url="https://llm.aihosting.mittwald.de/v1",
api_key="sk-your-api-key-here",
)

response = client.audio.speech.create(
model="Qwen3-TTS-12Hz-1.7B-CustomVoice",
voice="ryan",
input="Hallo und herzlich willkommen bei mittwald.",
response_format="mp3",
)

response.write_to_file("willkommen.mp3")
ParameterValueEffect
voiceone of the nine namesRequired. A request without it fails
languageleave it outValidated, little effect, see Supported values
response_formatwav or opuswav to process further, opus to deliver
speed1.0Between 0.25 and 4.0, see Speaking rate
instructionsfree text, ≤ 500 charactersOptional. Describes how to speak, see Controlling delivery
seedany integerAccepted, but does not reproduce, see Reproducibility

Speaking rate

speed scales the duration close to inversely. The same German sentence of 99 characters, five runs per setting, measured through the API:

speedMean duration
0.512.0 s
1.06.1 s
2.03.2 s

Output length varies between runs even with identical input, because the model samples: the five runs at 1.0 spanned 5.2 s to 6.6 s. Treat the numbers as the shape of the curve, not as exact values.

speed applies to streamed audio as well. Streaming at 0.5 produced 12.8 s and 16.5 s where 2.0 produced 2.9 s, so you do not need to correct the playback rate on your side.

Controlling delivery with instructions

There is no markup language on this endpoint. Square-bracket tags, angle-bracket tags, and SSML are not parsed. They are either read out loud or dropped, and which one you get is not predictable:

You sendWhat we heard, two runs each
[wütend] Die Lieferung kommt am Dienstagmorgen an."Für ihn, die Lieferung kommt am Dienstagmorgen an." / "Hey, wütend! Die Lieferung kommt am Dienstagmorgen an."
[angry] The delivery arrives on Tuesday morning."And the delivery arrives on Tuesday morning." / the sentence alone
<laugh> The delivery arrives on Tuesday morning.the sentence, plus 1 to 5 seconds of extra audio the tag turned into; two earlier runs produced continuous noise instead
(excited) The delivery arrives on Tuesday morning!"A delivery arrives on Tuesday morning." / the sentence alone, no emotion either time
The delivery arrives … <break time="1s"/> Thank you."The delivery arrives to thank you when number first to get, huh, thank you." / "The delivery arrives. Thank you. Thank you."
<speak><prosody rate="slow">…</prosody></speak>the sentence alone / "Persody Ray Slow, the delivery arrives on Tuesday morning."

Strip any markup from your text and put the intent into the instructions parameter instead. It takes a plain sentence describing how the line should be delivered, up to 500 characters. Longer values return HTTP 400.

<laugh> is the clearest case for stripping it. Four runs of the same sentence with the tag in front produced 4.2 s to 8.7 s of audio where the same sentence without it took 3.1 s to 3.8 s, so the tag turns into extra sound rather than into a laugh you asked for, and two runs on an earlier day produced continuous noise with no sentence in it at all.

Example: section-by-section voiceover for a client site

A web agency builds a landing page for a client and wants a spoken version of each section: a calm product intro, a friendlier feature paragraph, and a call to action with some drive. One voice throughout keeps it recognisable as the same narrator, instructions change the delivery, and opus keeps the files small enough to ship with the page.

instructions is a first-class parameter in the OpenAI SDKs, so it needs no extra_body. Leave language out: the model reads English copy as English on its own.

from openai import OpenAI

client = OpenAI(
base_url="https://llm.aihosting.mittwald.de/v1",
api_key="sk-your-api-key-here",
)

sections = [
(
"01-intro",
"Your hosting, fully managed. No servers to patch and no pager at three in the morning.",
"Speak calmly and clearly, like a product narrator.",
),
(
"02-feature",
"Every deploy ships in seconds, and any change is one click away from a rollback.",
"Speak in a cheerful, upbeat tone.",
),
(
"03-cta",
"Start your free trial today and put your first site online in ten minutes.",
"Speak fast and excitedly, like a short radio advert.",
),
]

for name, text, instructions in sections:
response = client.audio.speech.create(
model="Qwen3-TTS-12Hz-1.7B-CustomVoice",
voice="ryan",
input=text,
instructions=instructions,
response_format="opus",
)
response.write_to_file(f"{name}.opus")

Running that produced three files of 28 kB, 23 kB, and 21 kB, and the delivery follows the instructions. Each row is the mean of two runs:

SectionInstructionDurationSpeaking rate
01-introcalm product narrator6.20 s13.9 characters/s
02-featurecheerful, upbeat5.04 s15.9 characters/s
03-ctafast and excited4.36 s17.0 characters/s

For the German version of the same page, drop instructions and vary voice and speed:

sections_de = [
("de-01-intro", "Dein Hosting, vollständig verwaltet. Keine Server zum Patchen und kein Pager um drei Uhr nachts.", "serena", 0.95),
("de-02-feature", "Jedes Deployment ist in Sekunden ausgerollt, und jede Änderung lässt sich mit einem Klick zurücknehmen.", "serena", 1.0),
("de-03-cta", "Starte heute deine kostenlose Testphase und bring deine erste Website in zehn Minuten online.", "vivian", 1.15),
]

for name, text, voice, speed in sections_de:
response = client.audio.speech.create(
model="Qwen3-TTS-12Hz-1.7B-CustomVoice",
voice=voice,
input=text,
speed=speed,
response_format="opus",
)
response.write_to_file(f"{name}.opus")

That gave 12.6, 14.8, and 15.7 characters per second across the three sections, so the same build-up in pace without relying on instructions.

Writing text for German

German copy needs a pass before you send it. The model reads well-formed German prose correctly, but three things in ordinary source text come out wrong, and none of them fail loudly.

Second, expand abbreviations and write numbers out. The compact forms a CMS or a language model produces are read incorrectly:

You sendThe model saysWrite instead
Bestellung 45312"Bestellung 4FOR 5002"fünfundvierzigtausenddreihundertzwölf
1.299,99 Euro"1.29,999", "Euro" droppedeintausendzweihundertneunundneunzig Euro und neunundneunzig Cent
u. a. … z. B. … ggf. inkl."O.A. … setz bei … die geben English"unter anderem … zum Beispiel … gegebenenfalls inklusive
GmbH & Co. KG"GmbH & Co, Kagi"GmbH und Co. KG

Dates such as 20.05.2026, percentages such as 19 %, and phone numbers such as 05772 293-100 are read correctly as written. Phone numbers are spoken digit by digit, which is right but slow: budget roughly four times the audio length of ordinary prose for the same number of characters.

English is not affected. Order 45312 shipped on May 20th, 2026. The total is 1,299.99 euros including 19 percent VAT. comes back essentially word for word.

Third, English technical terms inside German sentences are the weakest case we found. Kubernetes-Cluster, Continuous Deployment, and Zero-Downtime-Updates in one German sentence came back partly garbled. Proper nouns are unreliable in the same way, and no spelling trick fixes them: we tried a brand name plain, respelled, hyphenated and spaced, and with instructions asking for German pronunciation, and 1 of 21 attempts came out right. There is no pronunciation lexicon on this endpoint. If a name has to be right every time, keep it out of the synthesized copy or splice in a recording of it.

Reproducibility

seed is accepted and returns HTTP 200, but it does not give you the same audio twice. Three identical requests with seed: 42 produced three different files with durations of 6.64 s, 6.48 s, and 6.32 s. All three were perfectly intelligible, so this is a reproducibility limit and not a quality problem.

If you need a clip to stay identical across deploys, generate it once and cache the bytes.

Latency

Measured on our hardware, German and English, one request at a time:

Input lengthWait for the full bodyAudio produced
41 characters619 ms (390 to 877)2.5 s
100 characters1343 ms (986 to 1677)6.7 s
272 characters3242 ms (2047 to 4278)17.8 s

Each row is the median of four runs with the range in brackets, buffered, mp3. The ratio of wall-clock time to audio length falls from 0.235 at 41 characters to 0.170 at 272, so the constant part of the request weighs more on short texts than on long ones. Estimate a fifth of the playback time you are asking for and treat the spread as real: the same 272-character text took 2.0 s in one run and 4.3 s in another.

Streaming cuts the wait for the first audio to under 150 ms, see Streaming. Use it whenever somebody is waiting.

Streaming

Set stream to true to get the audio while it is still being generated. The first audio then arrives in under 150 ms instead of after the full generation, so this is the setting that matters for anything a person waits for.

Streaming works with response_format="wav" and "pcm" only. Any other format together with stream returns HTTP 400:

Streaming (stream=true, stream_format='audio', or stream_format='sse') requires response_format='pcm' or 'wav'. Got response_format='mp3'.

Measured on the same 100-character sentence as the table above, response_format="pcm":

ModeContent typeFirst audioWhat arrives
buffered (default)audio/pcm870 msone body
streaming with "audio"audio/pcm91 ms97 chunks, spread over 123 to 999 ms
streaming with "sse"text/event-stream95 ms7 speech.audio.delta, 1 speech.audio.done

stream_format picks the framing. "audio" gives you a chunked body of raw audio in the format you asked for, which is what you want when you write to a file or pipe into a player. "sse" gives you the OpenAI event framing: speech.audio.delta events carrying the audio base64-encoded, closed by one speech.audio.done.

event: speech.audio.delta
data: {"type":"speech.audio.delta","audio":"UklGRv////9XQVZF…"}
from openai import OpenAI

client = OpenAI(
base_url="https://llm.aihosting.mittwald.de/v1",
api_key="sk-your-api-key-here",
)

with client.audio.speech.with_streaming_response.create(
model="Qwen3-TTS-12Hz-1.7B-CustomVoice",
voice="ryan",
input="A longer text whose playback should start immediately.",
response_format="wav",
stream_format="audio",
extra_body={"stream": True},
) as response:
response.stream_to_file("ausgabe.wav")

For very long texts, splitting still helps even with streaming, because it lets you start the next request while the current one plays. The Text-to-speech guide has a worked example.

Error responses

SituationStatusMessage
input is an empty string400Got bad request.
voice missing400Got bad request.
Unknown voice400Got bad request.
Unknown language400Got bad request.
instructions over 500 characters400Got bad request.
response_format="aac" or any other unknown format400Input should be 'wav', 'pcm', 'flac', 'mp3' or 'opus'
speed outside 0.25 to 4.0400Input should be less than or equal to 4
stream with a format other than wav or pcm400… requires response_format='pcm' or 'wav'
ref_audio, ref_text, speaker_embedding or task_type in the request500Internal error., see Voice cloning
Unknown model name404Got unknown model name.

Rejected values come back in two shapes. response_format, speed and the streaming constraint are validated with a message that names the field:

{
"error": {
"message": "litellm.BadRequestError: Hosted_vllmException - 1 validation error:\n {'type': 'literal_error', 'loc': ('body', 'response_format'), 'msg': \"Input should be 'wav', 'pcm', 'flac', 'mp3' or 'opus'\"}",
"code": "400"
}
}

voice, language, instructions and an empty input come back generic, with no hint as to which parameter was wrong:

{
"error": {
"message": "Got bad request.",
"code": "400"
}
}

So validate voice and language against the lists on this page before you send them: on those two the status code tells you that something is wrong, and nothing tells you what.

Voice cloning

Cloning a voice from your own reference audio is not available. This model ships the nine built-in voices and no speaker encoder, so there is nothing that could turn a sample of your voice into one it can speak with. Requests carrying ref_audio, ref_text, speaker_embedding or task_type fail with HTTP 500 and Internal error. rather than producing an unusable result.

If you need a specific voice, work with the nine you have: pick by pitch as described in Supported voices, and use instructions and speed to shape the delivery.

Licence and terms of use

The model is published by Alibaba under the Apache 2.0 licence. Before you can use it, accept its terms of use in mStudio. See Terms of use.