Qwen3-TTS in Node.js and TypeScript
@micdrop/qwen-tts runs Qwen3-TTS locally from Node: a TypeScript class that
sends each sentence to an mlx-audio
server holding the Qwen3-TTS weights
and streams the speech back. It needs no API key, and the audio stays on your
machine. Use it as the voice of a @micdrop/server call, or on
its own from any Node script.
Qwen3-TTS is an open source text to speech model from Alibaba. It is the only
local voice in Micdrop that speaks ten languages with one checkpoint: Chinese,
English, French, German, Italian, Japanese, Korean, Portuguese, Russian and
Spanish. It accepts an instruction such as "Very happy" or "speak slowly"
that changes how a sentence is read, can design a voice from a description, and
can clone a voice from three seconds of reference audio.
It also needs more computation than the other local voices, because it generates audio with a language model. It needs a GPU and a server running next to your app. On a machine without a GPU, use Piper or Kokoro.
Installation
Install the package:
npm install @micdrop/qwen-ttsStart an mlx-audio server, on Python 3.10 or above:
uv run --with "mlx-audio[server]" python -m mlx_audio.server --port 8000pip install "mlx-audio[server]" in a virtual environment does the same. The
server downloads the checkpoint on its first request, 2.6 GB for the default
one, and holds it in memory afterwards.
mlx-audio uses the Apple Silicon GPU through MLX, so it only runs on a Mac. On another machine, any server that implements the route the package calls can replace it.
Usage with MicdropServer
import { MicdropServer } from '@micdrop/server'import { Qwen3TTS } from '@micdrop/qwen-tts'
const tts = new Qwen3TTS({ url: 'http://localhost:8000', voice: 'Ryan', language: 'en-US',})
// Use with MicdropServernew MicdropServer(socket, { tts, // ... other options})Usage without MicdropServer
import { Qwen3TTS } from '@micdrop/qwen-tts'import { Readable } from 'stream'
const tts = new Qwen3TTS({ voice: 'Ryan' })
// Audio is raw PCM, 16 bits, 16 kHz, monotts.on('Audio', (chunk) => console.log('Audio:', chunk.length, 'bytes'))tts.on('Failed', (texts) => console.error('Failed:', texts))
tts.speak(Readable.from(['Hello! ', 'What can I do for you?']))Events
| Event | Payload | Description |
|---|---|---|
Audio | Buffer | A chunk of audio, PCM 16 bits, 16 kHz, mono, ready to be played. |
Failed | string[] | The server answered with an error, with the text that stayed unspoken. |
See the TTS interface for the full contract.
Options
| Option | Type | Default | Description |
|---|---|---|---|
url | string | http://localhost:8000 | Address of the mlx-audio server |
model | string | mlx-community/Qwen3-TTS-12Hz-1.7B-CustomVoice-6bit | Checkpoint the server loads |
voice | string | Ryan | Preset speaker of the CustomVoice checkpoints |
language | string | Guessed from the text | A code (fr), a locale (fr-FR) or the name the checkpoint uses (french) |
instruct | string | Optional | How the sentence is read, or the voice to invent on VoiceDesign |
refAudio | string | Optional | Path, on the server, to a few seconds of speech to clone |
refText | string | Optional | What the reference audio says |
streamingInterval | number | 0.5 | Audio generated before a chunk is sent, in seconds |
temperature | number | 0.9 | Sampling temperature |
topK | number | 50 | Sampling window, in number of candidates |
topP | number | 1 | Sampling window, in cumulated probability |
repetitionPenalty | number | 1.05 | Penalty on repeated audio tokens |
maxTokens | number | 500 | Ceiling on the audio tokens of one sentence, 12.5 per second |
warmup | boolean | true | Loads the checkpoint on the server at startup |
Voices
The CustomVoice checkpoints include nine speakers, so changing the voice costs
nothing at runtime. Each speaker was recorded in one language and can read all
ten. language sets the language of the call, whatever the speaker.
| Speaker | Recorded in |
|---|---|
Ryan, Aiden | English |
Vivian, Serena, Uncle_Fu | Chinese |
Dylan, Eric | Chinese, Beijing and Sichuan dialects |
Ono_Anna | Japanese |
Sohee | Korean |
language accepts a locale such as fr-FR, a code such as fr, or the
checkpoint name french. resolveLanguage() returns the value sent to the
model. For a language outside the ten, it returns auto, and the model detects
the language from the text.
QWEN_SPEAKERS exports this list with the recording language of each voice.
The demo uses it to fill its voice select.
A voice recorded in Chinese reads French as accurately as an English one. In a
test of nine French sentences across four speakers, Whisper transcribed eight
word for word, and one had "quatorze" mispronounced. Expect an occasional
mispronounced word with any speaker.
Reading with an intention
instruct describes how to read each sentence:
new Qwen3TTS({ voice: 'Serena', instruct: 'Warm and reassuring, speak slowly' })The instruction applies to every sentence of the call, so use it for the overall style of the assistant. It changes how the speaker talks and keeps the same voice.
Checkpoints
model can be any Qwen3-TTS export the server can load. The server keeps each
checkpoint it has loaded in memory.
| Checkpoint | Downloads | What it does |
|---|---|---|
mlx-community/Qwen3-TTS-12Hz-1.7B-CustomVoice-6bit | 2.6 GB | The default: nine speakers, style instructions |
mlx-community/Qwen3-TTS-12Hz-0.6B-CustomVoice-6bit | 1.8 GB | 100 ms faster, less accurate reading |
mlx-community/Qwen3-TTS-12Hz-1.7B-VoiceDesign-6bit | 2.6 GB | Designs a voice from the description in instruct |
mlx-community/Qwen3-TTS-12Hz-1.7B-Base-6bit | 2.6 GB | Clones the voice of refAudio |
The 0.6B checkpoint is smaller and returns its first audio about 100 ms sooner. But it starts each sentence with half a second of silence, against a tenth of a second for the 1.7B, and it more often produces audio much longer than the sentence. The 8 bit export of the 1.7B is slower than real time on a MacBook Pro M2, so prefer the 6 bit one.
Inventing a voice
A VoiceDesign checkpoint has no preset speakers. It generates the voice from
the description in instruct.
new Qwen3TTS({ model: 'mlx-community/Qwen3-TTS-12Hz-1.7B-VoiceDesign-6bit', instruct: 'A calm British man in his fifties, low pitch, unhurried', language: 'en-US',})Cloning a voice
A Base checkpoint has no preset speakers either. It reads a sentence in the voice of a recording, given the recording and its transcript. The mlx-audio server opens the file, so the path must exist on the machine that runs the server.
new Qwen3TTS({ model: 'mlx-community/Qwen3-TTS-12Hz-1.7B-Base-6bit', refAudio: '/srv/voices/marie.wav', refText: 'Bonjour, je suis Marie et je vous accompagne aujourdâhui.', language: 'fr-FR',})Clone a voice only with the consent of the person it belongs to.
How the audio arrives
The server decodes audio tokens as they are generated, so a sentence starts
playing before it is fully generated. streamingInterval sets the length of
each chunk, half a second by default. A smaller value gives an earlier first
word, but adds a decoder pass per chunk.
Sentences are generated one after the other, so they stay in order and the GPU handles one sentence at a time. When the user interrupts, the package closes the connection and the server stops generating.
Latency and memory
The first audio of a sentence arrives after about 400 ms, measured on a MacBook Pro M2 with 24 GB of memory and the default checkpoint loaded. The rest of the sentence takes about 0.75 seconds of computation per second of speech, so playback never waits for generation. In the demo, with the assistant speaking first, the first audio reached the browser 1.1 to 1.3 seconds after the call opened.
The mlx-audio server holds 2.5 GB with the default checkpoint, outside the Node
process. A full local call with Qwen3 4B as the agent and Whisper french for
transcription holds about 6.5 GB and answers in about 1.6 seconds. It fits on a
16 GB Mac, with little memory left. You can compare its latency and memory with
the other local stacks.
| Measure | Qwen3-TTS, 1.7B 6 bit |
|---|---|
| First audio of a sentence | ~400 ms |
| Computation per second of speech | ~0.75 s |
| Download, first request | 2.6 GB |
| Memory of the mlx-audio server | 2.5 GB |
| First word of a full local call | ~1.6 s |
The 0.6B checkpoint answers about 100 ms sooner. With warmup
on, the server loads the checkpoint at startup, so the first call starts without
waiting for the load.
Another server with the same API
The package sends POST /v1/audio/speech with the OpenAI fields (model,
input, voice) plus the Qwen ones (instruct, lang_code, ref_audio,
ref_text), sets stream, and reads raw PCM at 24 kHz with response_format: "pcm", then resamples it to the 16 kHz the browser client plays. Any server
that implements this route can replace mlx-audio, for instance one running the
weights on an NVIDIA card.
FAQ
Can I call Qwen3-TTS through an API from Node?
Yes. The mlx-audio server exposes an OpenAI style route, POST /v1/audio/speech, and @micdrop/qwen-tts wraps it in a TypeScript class
that streams PCM audio sentence by sentence. The server runs on your
machine, so the call needs no API key and costs nothing per character.
How do I clone a voice with Qwen3-TTS?
Load a Base checkpoint such as
mlx-community/Qwen3-TTS-12Hz-1.7B-Base-6bit, then pass refAudio, the
path to about three seconds of speech on the server, and refText, its
transcript. Every sentence is then read in that voice. Clone a voice only
with the consent of the person it belongs to.
Does Qwen3-TTS run without a Mac?
Yes, with a server other than mlx-audio, which runs only on the Apple Silicon
GPU through MLX. On Linux or Windows, run the weights on an NVIDIA card, in a
server that implements the same /v1/audio/speech route, and point url at
it. On a machine without a GPU, use Piper or
Kokoro, which run on the CPU.
Documentation
You can run a whole voice call on local models, with Qwen3-TTS next to a local LLM and local speech to text, and compare Qwen3-TTS with the other local voices by language, setup and latency.
License
Qwen3-TTS is released under the Apache 2.0 license, including the weights, so it can be used commercially. Cloning a personâs voice still requires their consent.