Scope note. Written text can have several valid spoken forms. The application owner must define the intended meaning for its market, domain and user journey before evaluating a voice system.
Key takeaways
- Forward text normalization converts written forms into words for TTS. Inverse text normalization converts an ASR transcript into display text. They solve different problems and are not lossless inverses.
- When the application already knows that a value is a date, amount, phone number or identifier, keep that type instead of asking a normalizer to infer it.
- Locale data can format a value, but it cannot decide whether
1.250is a German grouped integer, an English decimal or an identifier. - Use structural rules for changing values and pronunciation controls for stable vocabulary such as names, brands and abbreviations.
- Release the application rules, locale data, dictionary set, model and voice as one tested configuration with a documented rollback target.
Last updated August 2026. This guide defines an implementation and testing method, not a universal pronunciation for every language, dialect or structured input.
What is text normalization for voice AI?
In TTS, text normalization usually means forward normalization: converting a written form into a speakable form. The W3C's SSML 1.1 Recommendation uses the same direction and notes that strings such as 1/2 are inherently ambiguous. Inverse text normalization (ITN) runs in the other direction, turning spoken-form ASR output into written forms such as dates, amounts and times. Apple's ITN research overview uses that definition.
The distinction matters in a voice agent:
- A caller says "fifty euros."
- ASR and ITN may display
50 €. - Business logic validates the amount and asks for confirmation if required.
- A later TTS response applies forward normalization to speak the stored value.
Do not test this chain by expecting an exact round trip. Several written forms can produce the same speech, and one spoken form can map to several written forms. ITN output is still parsed input, not a trusted amount, date or account number.
Best for: typed input. Do not flatten a date object or decimal amount into an ambiguous display string and then try to recover its meaning with one large regex.
Which layer should own each spoken decision?
Use three ownership boundaries:
- Application semantics: the source system owns the value and its meaning. It decides, for example, whether cents are material or an account code must be confirmed character by character.
- Normalization: locale-aware rules render typed values and handle ordinary written text. Critical typed renderers should be deterministic and independently testable.
- Pronunciation: a narrowly scoped dictionary, phonetic form or supported tag controls recurring vocabulary after the intended words are known.
This split stops a customer-specific replacement from becoming a global language rule. It also makes failures diagnosable: retain the source value, the rendered TTS input and the release manifest, subject to the product's data-retention policy.
For KugelAudio, set normalize: true and pass language when it is known; automatic language detection can misclassify short text. The canonical text-processing documentation also documents <spell> for character spelling. Project pronunciation dictionaries can use replacement text or IPA, and are selected through project_id and optional dictionary_ids; see the pronunciation dictionary documentation.
Do not translate generic SSML examples directly into production requests. SSML defines elements such as say-as, phoneme and external lexicons, but support is processor-specific. KugelAudio documents <break>, <spell> and <prosody rate> as the interpreted tags, plus inline IPA and dictionaries. Its prompting reference is the source of truth for the currently supported controls.
How should German numbers, dates and identifiers be handled?
Start from a typed value, then render it for the listening task. The following are test prompts, not prescribed pronunciations:
Unicode's LDML number specification and date specification define locale-data structures for number, currency, date, time and timezone formatting. Use a maintained locale library based on this data instead of hand-maintaining thousands separators and month names. Locale formatting still cannot supply business meaning, grouping for a company identifier or a confirmation policy.
For a changing value, define one renderer per semantic type, such as render_money(amount, currency, locale) or render_reference(value, grouping_policy). Validate the complete value before rendering. Regex can help enforce a small, anchored field grammar, but it should not be the parser for every number-like substring in free prose.
How should Unicode and untrusted text be handled?
Choose and document a Unicode normalization form before dictionary matching. NFC is a common choice because canonically equivalent text receives one representation; the Unicode normalization specification defines the forms and their guarantees. Do not substitute NFKC casually for NFC: compatibility normalization can change distinctions that an identifier policy may need to preserve.
Canonical normalization is not a security check. Visually similar characters can come from different scripts, and invisible controls can alter matching or display. For security-sensitive identifiers, validate the allowed script, character classes, length and separators against the field's contract. Unicode UTS #39 provides confusable-detection mechanisms, but explicitly does not make its confusable skeleton suitable as a replacement or display value.
Treat user-controlled text as data, not speech markup. Do not concatenate a caller's free text inside trusted <spell> or <break> syntax without context-appropriate escaping or rejection of tag delimiters. Apply server-side length and structure limits before normalization, and bound regex work; the OWASP input-validation guidance explains why allowlists suit structured fields and denylisting a few dangerous strings does not. Include literal angle brackets, malformed tags, bidi controls, combining marks, mixed scripts and overlong input in negative tests.
How should names, brands and abbreviations be controlled?
Use a dictionary for stable terms, not for values that change on every request. Each entry needs the written form, intended spoken form, language tag, matching scope, owner, evidence and release version. The BCP 47 language-tag specification standardizes language and regional tags, but a correct tag does not prove that a pronunciation sounds right.
For an abbreviation, record whether each meaning should be expanded, pronounced as a word or spelled. Test the entry in sentences, including ordinary words that should not match. Avoid a broad replacement that fixes a brand in one context but changes a surname or substring elsewhere.
Use IPA only when the target system documents the accepted alphabet and native reviewers can approve the result. W3C SSML's phoneme element can identify IPA, but engines may differ in their phoneme inventories and rendering. For KugelAudio, dictionary IPA takes precedence over replacement text, so test both the intended term and nearby non-matches with the exact model and voice.
How can teams release and roll back normalization safely?
Treat normalization assets as a release unit. Store an immutable manifest containing:
- application-renderer version and locale-data version;
- normalizer setting and explicit language;
- dictionary IDs or a content digest;
- model, voice and relevant synthesis configuration;
- regression-set version and approval record.
Create a candidate dictionary instead of editing the only live copy. KugelAudio's per-request dictionary_ids selection can target a candidate dictionary for tests or a canary while leaving default traffic unchanged. Promote the tested set through configuration, monitor errors by input class, and keep the previous manifest ready. Rollback then means restoring known IDs and versions, not manually reversing entries under pressure.
How can teams test normalization without hiding errors?
Build a synthetic regression table with source value, semantic type, locale, timezone, expected TTS input, permitted spoken variants, criticality and fallback. Include zeros, negatives, range limits, leap days, daylight-saving transitions, unexpected precision, malformed values and Unicode edge cases.
Test in layers:
- Assert exact normalized text for deterministic application renderers.
- Generate audio with the release candidate and archive samples for critical cases.
- Use ASR or forced alignment to flag omissions and substitutions, not to certify stress, grouping or pronunciation.
- Ask native listeners to review names, regional expectations, grouping and intelligibility without showing the expected answer first.
- Exercise the complete clarification or confirmation flow for action-bearing data.
Report results by class and severity. A single average accuracy score can hide one wrong decimal point or check digit. Record the fallback rate, exact-renderer failures, content omissions and critical listener failures, together with the manifest that produced them. The existing German and dialect benchmark guide explains why intelligibility and native-listener quality need separate evidence.
What should happen when normalization is uncertain?
Do not silently guess for an action-bearing value. Reject a value that violates its declared type, or move to an approved recovery path: ask a precise question, use an unambiguous longer form, spell or group the value, present it visually, send it over an authenticated channel, or transfer to a person.
The error should name the field and violated contract without echoing secrets into logs. A plausible but incorrect reading is harder to detect than an explicit failure.
What are the limitations of this guide?
These are engineering patterns, not universal language rules. Pronunciation varies by language, region, speaker and organizational policy. This article does not claim complete support for SSML, every dialect, arbitrary names or every structured format. Test the exact production configuration and the inputs that carry business risk.
FAQ
Is text normalization the same as pronunciation control?
No. Normalization turns structures such as dates and amounts into intended words. Pronunciation control specifies how selected words or phonetic content should sound. A production system often needs both.
What is the difference between text normalization and inverse text normalization?
Forward text normalization converts written forms to speakable text for TTS. Inverse text normalization converts spoken-form ASR output to written forms. They are not lossless inverses because both directions can be ambiguous.
Should the TTS model decide how to read every number?
No. If the application knows that a value is a price, phone number or identifier, it should retain that type and construct the intended TTS input before synthesis.
Can a pronunciation dictionary fix an IBAN or customer identifier?
Not as the primary mechanism. Dictionaries suit recurring vocabulary; changing identifiers need validation, structural grouping rules and a confirmation policy.
Is ASR enough to validate pronunciation?
No. ASR can flag omissions or substitutions, but a correct transcript does not prove correct stress, grouping, regional acceptability or intelligibility.
How should dictionary changes be released?
Test a versioned candidate with representative sentences and non-matches, then release it with the normalizer, model and voice manifest. Retain the previous dictionary set as the rollback target.
What should a system do with ambiguous critical text?
Return an explicit validation error or use an approved clarification and confirmation flow. It should not silently choose a plausible reading when that choice can change meaning.
Build with KugelAudio
Put European voice infrastructure into production.
Use the EU endpoint or discuss a customer-operated Kubernetes deployment.