đŸŽ€Micdrop

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:

Terminal window
npm install @micdrop/qwen-tts

Start an mlx-audio server, on Python 3.10 or above:

Terminal window
uv run --with "mlx-audio[server]" python -m mlx_audio.server --port 8000

pip 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 MicdropServer
new 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, mono
tts.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

EventPayloadDescription
AudioBufferA chunk of audio, PCM 16 bits, 16 kHz, mono, ready to be played.
Failedstring[]The server answered with an error, with the text that stayed unspoken.

See the TTS interface for the full contract.

Options

OptionTypeDefaultDescription
urlstringhttp://localhost:8000Address of the mlx-audio server
modelstringmlx-community/Qwen3-TTS-12Hz-1.7B-CustomVoice-6bitCheckpoint the server loads
voicestringRyanPreset speaker of the CustomVoice checkpoints
languagestringGuessed from the textA code (fr), a locale (fr-FR) or the name the checkpoint uses (french)
instructstringOptionalHow the sentence is read, or the voice to invent on VoiceDesign
refAudiostringOptionalPath, on the server, to a few seconds of speech to clone
refTextstringOptionalWhat the reference audio says
streamingIntervalnumber0.5Audio generated before a chunk is sent, in seconds
temperaturenumber0.9Sampling temperature
topKnumber50Sampling window, in number of candidates
topPnumber1Sampling window, in cumulated probability
repetitionPenaltynumber1.05Penalty on repeated audio tokens
maxTokensnumber500Ceiling on the audio tokens of one sentence, 12.5 per second
warmupbooleantrueLoads 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.

SpeakerRecorded in
Ryan, AidenEnglish
Vivian, Serena, Uncle_FuChinese
Dylan, EricChinese, Beijing and Sichuan dialects
Ono_AnnaJapanese
SoheeKorean

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.

CheckpointDownloadsWhat it does
mlx-community/Qwen3-TTS-12Hz-1.7B-CustomVoice-6bit2.6 GBThe default: nine speakers, style instructions
mlx-community/Qwen3-TTS-12Hz-0.6B-CustomVoice-6bit1.8 GB100 ms faster, less accurate reading
mlx-community/Qwen3-TTS-12Hz-1.7B-VoiceDesign-6bit2.6 GBDesigns a voice from the description in instruct
mlx-community/Qwen3-TTS-12Hz-1.7B-Base-6bit2.6 GBClones 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.

MeasureQwen3-TTS, 1.7B 6 bit
First audio of a sentence~400 ms
Computation per second of speech~0.75 s
Download, first request2.6 GB
Memory of the mlx-audio server2.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.