I recorded 1,180 TTS clips to find out what voices misread in LLM output

2026-11-03来源 dev.to PythonEnglish

Every voice agent has the same seam: a language model writes text for a reader, and a text-to-speech model reads it for a listener. The two were never designed for each other. The model writes **Step 1:** run `npm install` and the voice says "asterisk asterisk step one". It writes 1,250 units and some voices say "one two five zero". It writes a Turkish phone number as 0532 123 45 67 and the voice reads it as a single number with the wrong suffix.

I kept patching these one at a time in prompts and regexes until I decided to do it properly. This post is about the result, and more about how I measured it, because the measuring turned out to be the hard part.

What the library does

speechready runs between the LLM and the TTS. Four stages:

  1. Clean. Markdown and LLM artefacts: headings, emphasis, links, tables, emoji, URLs, fenced code. Every judgment call is an option. Code blocks are dropped by default, or replaced with "See the code block."
  2. Verbalize. Numbers, ordinals, decimals, fractions, percentages, currency, dates, times, phone numbers, units, acronyms, written out in the target language. Turkish applies vowel harmony to suffixes, and reads foreign tech acronyms the way Turkish developers say them ("en pi em").
  3. Compile. Pause, spell and emphasis intents rendered to the provider's syntax: SSML for Azure, Google and Polly, break tags for ElevenLabs, punctuation for the providers that have nothing.
  4. Chunk. Sentence splitting and packing under the model's request limit, plus a streaming segmenter that releases sentences from an LLM token stream as boundaries are confirmed.

There is a Python package and a TypeScript package. Both pass the same 217 YAML fixtures and produce the same output for every one of them.

Measuring whether it helps

A text preprocessor is easy to make look good: pick an ugly input, show the tidy output. I wanted to know whether voices actually read the processed text correctly more often, so I built a loop that could tell me.

145 lines of English and Turkish: assistant answers with markdown, dates, phone numbers, prices, URLs, identifiers, and control prose that should pass through untouched. 16 voices across 7 providers. Each line synthesized twice, as written and after speechready. Both recordings transcribed back with gpt-4o-transcribe and scored against the intended words.

The scorer forgives the ways a transcription engine writes speech down: digits for spoken numbers, "21st" for "twenty-first", "NPM" for "en pi em". It only ever lowers the error, and it applies to both sides equally. A line counts as misread when more than a fifth of the words are wrong. Most of the 145 lines are the fixture inputs the rules were developed against, plus control prose and a handful of real assistant answers, so this is a go/no-go measurement on curated cases, not a held-out production sample.

Result, on the same 1,180 recordings:

As written With speechready
All 32% 8%
English 29% 7%
Turkish 37% 10%

Every voice improved. Control prose did not regress. The weakest result is Cartesia's Turkish, where the voice speaks with an English accent and the transcription model misses words that were rendered correctly.

Because most of those lines are the library's own test inputs, I also ran a held-out check: 60 answers that gpt-4.1-mini gave to typical voice-agent questions, generated after the rules were written, scored against a narration the same model wrote independently, on four voices. Misreads went from 10% to 8% and mean word error rate did not move. Concise assistant answers simply contain few of the things the library fixes; when they do contain markdown, prices, phone numbers or code, that is where the gain is. The held-out run also found two bugs, a dotted Turkish time with a suffix and a spaced minus read as a range, which is what held-out data is for.

You can listen to the pairs, with what the STT heard under each: https://berkerbatur.github.io/speechready/

What I learned from the eval

  • Scoring was harder than the library. Half of the early "misreads" were the scorer failing to forgive a transcription convention. Every fix to the scorer applied to both the raw and the processed side, so it never favored the library.
  • Providers disagree on what they handle. Google's Chirp voices read 25°C fine; others do not. The provider profiles are YAML, and this is where that knowledge lives.
  • Turkish needs its own rules, not translated English ones. Suffixes attach to the spoken form of a number, and the spoken form depends on the last vowel.

Using it

pip install speechready # Python 3.10+ npm install speechready # Node 20+, 35 kB, no dependencies 
from speechready import prepare prepare("**Step 1:** run `npm install`. [pause 1s]", provider="elevenlabs", model="eleven_multilingual_v2").text # 'Step 1: run npm install. <break time="1s" />' 

For LiveKit Agents, speechready.integrations.livekit.text_transform(...) goes in tts_text_transforms. For Pipecat, speechready.integrations.pipecat.text_transform(...) goes in a TTS service's text_transforms. Neither imports the framework.

Playground, in the browser: https://berkerbatur.github.io/speechready/play/

English and Turkish for now. If you want another language, the fixtures show exactly what a locale has to cover, and the eval harness will tell you whether it worked.

原文:https://www.a8q9.cn/en/article/i-recorded-1-180-tts-clips-to-find-out-what-voices-misread-in-llm-output-fd2ff9.html

相关文章