DEV Community

Cover image for It speaks German. It cannot read a part number.

It speaks German. It cannot read a part number.

Local speech synthesis on a laptop, tested against the text that fills industrial documents.

TL;DR

Tested Qwen3-TTS 1.7B (Apache 2.0) on Apple Silicon against forty German sentences full of part numbers, acronyms, dates and currency. Raw text derails: the model stops mid-identifier and reads twenty seconds of unrelated fluent German, no error. A text rewrite layer removes that completely, and what's left sits in identifiers. Polly on the same sentences, raw: zero derailments, no rules. The local voice also drifts, accent and speaker identity both, and no accuracy number catches it.

Use Amazon Polly. Local is finally good enough to consider, but it costs you stability plus a rewrite layer to maintain. Worth it only for data residency, air-gapped sites, or cost at volume.

Three of forty test sentences never got read.

The model started on a currency figure, stopped, and produced twenty seconds of fluent German about something else. No error, no warning. A confident voice saying the wrong thing.

That was the raw baseline. With a rewrite layer in front of the model, forty sentences it had never seen produced zero derailments.

The setup

Qwen3-TTS, Apache 2.0, 1.7B parameters, running on Apple Silicon through MLX. Nothing leaves the laptop. The fixtures were forty German sentences of the kind that fill workshop documentation and service records: compound nouns, acronyms, AWS service names, currency in German convention, dates, alphanumeric part identifiers.

Alibaba's Qwen team shipped the model in January 2026, in two sizes and three modes. Custom voice picks from built-in speakers. Voice design builds a voice from a written description. Cloning takes three seconds of reference audio and carries that voice into the output. The official runtime targets CUDA. The Apple Silicon path came from the MLX community.

Why 1.7B and not 0.6B

The smaller size got one exploratory pass: English fixtures, raw text, no rewrite layer. It derailed zero times where 1.7B derailed twice, but it missed more terms, five against two out of twenty-seven scoreable items. Net accuracy: 22 of 27 against 23 of 27. The miss rate sent the rest of the work to 1.7B. German never went in front of the smaller model.

Cloning, which I did not test

Cloning is the interesting mode. Three seconds of audio puts a specific person's voice on the other end of a German sentence. A technician hears his own voice read the work order back. That is a different product from a synthetic assistant reading a manual.

Every clip here used Ryan, a built-in English preset, because no shipped preset speaks German natively. Whether a cloned German voice holds pronunciation accuracy on top of everything below is a separate run.

How the clips got checked

ASR, OCR and PII extraction hand you a diff: run the output against ground truth, get a number. Synthesis hands you sound. So every clip went back through Whisper, locally, and the transcript got compared against the input text. That proves the words survived. Sound quality still needs an ear.

Where it holds and where it breaks

On sentences the model never saw during rule development:

Category Score
Acronyms 6 / 6
AWS service names 6 / 6
Numbers and dates 7 / 8
Identifiers 2 / 7

Aggregate term accuracy landed near 0.78.

Two categories sit outside the number. Long German compound nouns and English engineering phrases dropped into German sentences (continuous integration, canary deployment) have no single correct rendering a machine can score, so I judged them by ear.

Almost the entire shortfall lives in one category: identifiers. That concentration is what makes the number usable. It points at one component to replace, not at the whole pipeline.

Getting there meant throwing out my first setup. Raw text into the model is the wrong baseline, and it was mine. No production system feeds 1.250,50 Euro or SN/2026/00482 to a synthesizer untouched. Every deployed system has a normalization front-end, and commercial vendors tell you to build one. Testing without one measures a configuration nobody ships.

All three derailments came from that baseline: a currency line, a serial number, a part code. One derailment stops the test whatever the accuracy score says. A mispronounced part number is audibly wrong and any listener catches it. A fluent unrelated sentence gets caught only by someone holding the source text, and no accuracy average flags it.

Four rules

1. Spell the number out

1.250,50 Euro derails. So does 1250,50 Euro, which killed the thousands-separator hypothesis. Ein Tausend Zwei Hundert Fünfzig Komma Fünfzig Euro comes back clean, and Whisper writes it into the original notation. The model cannot handle the comma-decimal token. It handles the quantity fine.

2. Chunk digit runs, after the letters

Nine digit words alone are stable. Two letter names followed by nine digit words come back scrambled. Break them up: SN, zwei null zwei sechs, null null vier acht zwei, with pauses at the group boundaries. Prefer compound number words like einunddreißig over drei eins. A compound is one phonetic unit and the decoder cannot split it.

3. Separate letters that rhyme

German letter names ending in the same vowel form a confusion set: Beh, Tseh, Deh, Geh, Peh, Teh, Weh. Feed a run of them to an autoregressive decoder and it re-lexicalizes them into words. Es En came back as Person. You hear the singsong before any recognizer touches it.

4. Keep it under five elements

Expanded into DIN 5009 spelling words, the model holds to four or five. Past that it hallucinates from roughly the third element onward.

Why the rules work is open

Hyphens fix acronyms where dots and commas fail. Commas fix digit runs. I called both prosodic separation while doing the work, and that cannot be right, because each approach fails where the other succeeds. The rules transfer to held-out data. The mechanism has no explanation, and anyone porting these rules to a new format should know that.

DIN 5009, and where it stops

DIN 5009 is how German technicians read codes aloud: Anton, Berta, Cäsar. Multisyllabic distinct words, no shared vowel ending, built for a noisy channel. It works, and it sounds idiomatic, which matters for anything a customer hears.

It also produced the best reversal here. I had written IAM down as a hard model limit, since the letter I reads as E every time and no text transformation reaches phoneme level. Then Ida-Anton-Martha passed. The limit belonged to my probe set.

Length is where it gives out. A seventeen-character VIN expands into seventeen proper nouns interleaved with digits. A human listener hears it correctly. Whisper cannot reconstruct the string. At that length the measuring instrument fails before the model does.

The hosted comparison

Twelve raw sentences also went past Amazon Polly's generative German voice, with no rewrite layer in front of either engine. Polly: nine passes, three local mispronunciations, zero derailments. Qwen3 on the identical twelve: three passes, six local mispronunciations, three derailments.

Polly reads 1.250,50 Euro and 17.11.2026 correctly where Qwen3 derails on both. Short identifiers mostly pass raw whatever the delimiter: RX3.1-UPD-B, PXW-94c-MAX and TP/2025/00317 all come back clean, where Qwen3 needs the rewrite layer to survive any of them.

What Polly gets wrong, it gets wrong for reasons no formatting change reaches. LT/2026/00193 comes back as laut 2026-00193, the two-letter prefix read as a German word. The VIN picks up a letter substitution, YV1 rendered JÖV 1. Bedrock becomes Bidrock on Polly and derails outright on Qwen3, because neither vendor carries the word in a pronunciation lexicon.

Twelve items is a spot check, not a benchmark. The shape holds anyway: locale-aware normalization has a trigger for a currency figure and a date, and no trigger at all for a VIN or a two-letter reference prefix.

Why 0.926 is worthless

On the fixtures used to develop the rules, term accuracy hit 0.926. Every failing item got its own rewrite, found by probing variants until one passed. Dots for one acronym, commas for another with the same structure. That is a lookup table fitted to forty sentences, and its accuracy on those forty sentences is close to one by construction.

Held-out data moved it to 0.778. The gap sits entirely inside the identifier category. The acronym and service-name rules transferred with no loss. That is what a real rule looks like.

What the numbers do not cover

Every clip used Ryan, one English preset. Some passed for a native speaker on a casual listen. Others carried a strong accent, and no rewrite touches accent, which lives in the voice. The numbers confound three things: the model's German, the preset's German, and one voice's accent.

Verification ended up hybrid. The design called for Whisper and Parakeet, so that disagreement between them would locate a failure. Parakeet proved unreliable on German, and human listening took that role wherever the machine verdict was ambiguous. That carries a judgment call the artifacts cannot reproduce, and it stops scaling past a few dozen clips.

What to actually use

Voice this good used to mean a service contract or a GPU. Now it is a 1.7B model under Apache 2.0, running on a laptop over MLX, with nothing leaving the machine and no account behind it. Some clips read German workshop prose that would pass for a native speaker. Three seconds of reference audio puts a named person's voice on the output. Anyone with a MacBook can run this on a train.

Some clips. That is the limit. Across forty sentences on one preset, the voice does not hold still: a few clips came back clean of any accent, others carried a strong one, and the voice identity itself shifted between clips that used identical settings. Whisper transcribed all of them correctly. Term accuracy does not move when the speaker changes, so the instability never appeared in a single number. Only listening found it.

That rules out anything a customer or a technician hears. Personal use, internal tooling, a draft read-back that a person checks: fine. A voice channel where the same speaker has to sound like the same speaker across a shift: not yet.

Polly held still. It also handled the currency figures, dates and short part codes raw, with no rules and no probe sets. Most of this article is scaffolding that hosted synthesis makes unnecessary.

Polly also takes SSML, and Qwen3-TTS takes plain text. That gap is the same one the four rules work around. A say-as tag marks a string as digits or as characters and the engine reads it that way, no probe set required. A phoneme tag sets pronunciation directly, which is the level I could not reach from text. A custom lexicon fixes a word once, for every request after it. The rewrite layer is a text-side approximation of markup that hosted engines have shipped for years.

So use Polly, Azure or Google unless offline is a hard requirement. Data residency, an air-gapped shop floor, or per-request cost at volume buys the local path. Nothing else does.

If you are on it anyway, run the rules against your own identifier formats first, because mine came from one imaginary client's part-number conventions and the five-element ceiling is the finding I would expect to move. Plan for the identifier category to need different architecture rather than better prompts: a recorded inventory of twenty-six letters, ten digits and the spelling-alphabet words, concatenated per identifier, once per voice.

Sources

Qwen3-TTS GitHub repository: license, release date, model variants, language list, and the official quickstart, which loads on cuda:0 and confirms the reference runtime targets NVIDIA GPUs.

Qwen3-TTS technical report: architecture and benchmark numbers behind the model.

Qwen3-TTS announcement, Qwen blog: the release itself, from Alibaba's Qwen team.

Qwen3-TTS-MLX: a community port for Apple Silicon, not maintained by Alibaba. Its notes describe the MLX runtime as narrower than the official PyTorch one, which is the basis for the constraint above.

Amazon Polly generative engine, AWS docs: the engine and the Vicki voice used for the German comparison.

Top comments (0)