extended-wer-normalizer
jiwer-compatible text normalizer for Word Error Rate (WER) evaluation in voice AI.
Extends jiwer's built-in transforms with normalizations that matter for real-world ASR evaluation: phone numbers, emails, URLs, currency, percentages, ordinals, filler words, and stuttering.
Installation
pip install extended-wer-normalizer
Quick start
from extended_wer_normalizer import normalize_for_wer
normalize_for_wer("Call 0176 or email info@example.com, it costs $5.99")
# → "call 0 1 7 6 or email info at example dot com it costs five dollars ninety nine cents"
normalize_for_wer("Um, 1st place goes to Dr. Smith with 50% accuracy")
# → "first place goes to doctor smith with fifty percent accuracy"
jiwer integration
Every normalization is a jiwer.AbstractTransform subclass — compose them freely:
import jiwer
from extended_wer_normalizer.transforms import NormalizeEmails, ExpandDigitRuns
pipeline = jiwer.Compose([
NormalizeEmails(),
ExpandDigitRuns(),
jiwer.ToLowerCase(),
jiwer.RemovePunctuation(),
jiwer.ReduceToListOfListOfWords(),
])
wer = jiwer.wer("info at example dot com", "info@example.com", hypothesis_transform=pipeline)
Use the pre-built pipeline directly with jiwer.wer:
import jiwer
from extended_wer_normalizer import english_wer_pipeline
wer = jiwer.wer(reference, hypothesis, reference_transform=english_wer_pipeline, hypothesis_transform=english_wer_pipeline)
Available transforms
| Transform | Example |
|---|---|
ExpandDigitRuns |
"0176" → "0 1 7 6" |
DigitWordsToChars |
"zero one seven" → "0 1 7" |
NormalizeEmails |
"user@example.com" → "user at example dot com" |
NormalizeURLs |
"https://example.com/path" → "example dot com" |
NormalizeCurrency |
"$5.99" → "five dollars ninety nine cents" |
NormalizePercentages |
"50%" → "fifty percent" |
NormalizeOrdinals |
"1st" → "first", "15th" → "fifteenth" |
ExpandAbbreviations |
"Dr." → "doctor", "vs." → "versus" |
NormalizeSymbols |
"cats & dogs" → "cats and dogs" |
SplitHyphenatedWords |
"fixed-term" → "fixed term" |
RemoveFillerWords |
removes um, uh, hmm, er, ah, … |
CollapseRepetitions |
"yes yes yes yes" → "yes" (collapses runs of 4+; configurable via max_repeats) |
ExpandFrenchElisions |
"j'aime" → "j aime", "qu'il" → "qu il" (French only) |
Every transform that consumes language-specific data accepts a language="en" keyword (default English): NormalizeEmails(language="fr"), ExpandAbbreviations(language="de"), etc.
Pipeline design
The English pipeline applies transforms left-to-right in a single pass:
- Pattern-specific (before punctuation is stripped): email, URL, symbol, abbreviation, currency, percentage, ordinal
- Core: contractions (
I'm→i am), lowercase, intra-word hyphen split (fixed-term→fixed term), punctuation removal - Digit normalization: expand digit runs (
0176→0 1 7 6), convert digit words (zero→0) - Cleanup: filler words, repetition collapse
Supported languages
Full pipelines (with language-specific abbreviations, fillers, lexicons, and number/ordinal/percentage word forms via num2words) ship for English, German, and French. Pass any other language value for the minimal fallback (lowercase + punctuation + whitespace).
from extended_wer_normalizer import normalize_for_wer
# German: titles, fillers, ordinals, currency
normalize_for_wer("Hr. Müller, am 1. Januar, ähm, ungefähr 50% Rabatt", language="de")
# → "herr müller am erste januar ungefähr fünfzig prozent rabatt"
# French: elision contractions, ordinals, comma-decimal currency
normalize_for_wer("M. Dupont, le 1er janvier, c'est €5,99", language="fr")
# → "monsieur dupont le premier janvier c est cinq euros quatre vingt dix neuf centimes"
# Spanish, Italian, … fall through to the minimal pipeline
normalize_for_wer("¡Hola, mundo!", language="es")
# → "hola mundo"
# Tune the repetition-collapse threshold (default 3 → collapse runs of 4+)
normalize_for_wer("no no no", max_repeats=1) # collapse any 2+ run
# → "no"
Per-language pipelines are also exposed for direct use with jiwer.wer:
from extended_wer_normalizer import (
english_wer_pipeline,
german_wer_pipeline,
french_wer_pipeline,
)
To inspect or extend the language data:
from extended_wer_normalizer.languages import get_language_data, supported_languages
supported_languages() # ["de", "en", "fr"]
get_language_data("de").abbreviations["hr."] # "herr"
Quirks worth knowing
- Hyphenated compounds: intra-word hyphens split into spaces (
fixed-term→fixed term), so hyphenated and spaced spellings of the same words score identically. Before 0.6.0 the hyphen was deleted (fixedterm), which penalized STTs that never emit hyphens — WER numbers are not comparable across that boundary. - Comma vs. period decimals: French uses
,(€5,99,3,5%); the currency and percentage transforms accept either separator regardless of language. - German ordinals: matched as 1- to 3-digit numbers followed by
.and a word (e.g."1. Januar"but not"Es war 1990."or"1.5 Liter"). 4+ digits and decimals are skipped to avoid false positives on years. - French ordinals: matched as
1er,1ère,2e,2es,2ème,2èmes,2nde,2nds,2nd.num2wordsreturns masculine forms (premier,deuxième); feminine variants likepremièreorsecondeare not produced. - Contractions:
jiwer.ExpandCommonEnglishContractionsruns only for English. French has a customExpandFrenchElisionsthat splitsj',l',d',n',s',m',t',c',qu',jusqu',lorsqu',puisqu',quoiqu'from the following word. German has no contraction step. - German pluralization: most currency units stay invariant (
fünf Euro, notfünf Euros).
Metadata
Release files for extended-wer-normalizer 0.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| extended_wer_normalizer-0.6.0.tar.gz | 51.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| extended_wer_normalizer-0.6.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 68.5 kB
Release files / extended_wer_normalizer-0.6.0.tar.gz
| Download URL | extended_wer_normalizer-0.6.0.tar.gz |
|---|---|
| Size | 51.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1ef763523dafc5ab6fad42f1d4769fba0f0233aeeeda7c1cbed85aeb1ab4d4f2
|
|
BLAKE2b-256 checksum How to use checksums |
9487a94b4fb036a15792942053e06312de84301afdc41730d2b7f1ba5f034c5a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.20 {"installer":{"name":"uv","version":"0.12.20","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / extended_wer_normalizer-0.6.0-py3-none-any.whl
| Download URL | extended_wer_normalizer-0.6.0-py3-none-any.whl |
|---|---|
| Size | 17.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
743d0f3b533617196d23d63c547c2488db32c5f5bbe854e0691b0b87e19e33d9
|
|
BLAKE2b-256 checksum How to use checksums |
8f5e0733554e1a1885d208b6141321cd8c633cd6fe8341f29ab5931e1564229e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.20 {"installer":{"name":"uv","version":"0.12.20","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|