urdunorm
Urdu text normalization for ASR evaluation, with explicit and reproducible protocols.
pip install urdunorm
from urdunorm import normalize
normalize("کیلئے", mode="eval") # 'کے لیے'
normalize("بھائی", mode="eval") # 'بھائی' aspiration preserved
normalize("ہاں", mode="eval") # 'ہاں' nasalization preserved
urdunorm --mode eval < ref.txt > ref.norm.txt
urdunorm --mode lums-compat --jsonl-field text < hyp.jsonl
Why this exists
Urdu word error rate is not currently a well-defined quantity. Published numbers for the same model on the same benchmark differ by tens of points across studies. Most of that spread is not modelling. It comes from two choices that papers rarely state: which held-out set was used, and how the text was normalized before scoring.
Urdu makes the normalization choice unusually consequential.
The script has near-duplicate characters that carry no phonemic contrast, so
ي U+064A and ی U+06CC are the same letter typed on different
keyboards. A normalizer that ignores this charges a model for a keyboard layout.
It also has near-duplicate characters that carry a real contrast, and merging them
is the standard bug. ھ U+06BE marks aspiration: بھائی is
"brother", بہائی is not the same word. ں U+06BA marks
nasalization: ہاں is "yes", ہان is not a word. A normalizer
that folds either of these into its lookalike reports a lower WER by destroying
information.
And the same phrase is routinely written with or without an internal space.
کیلئے and کے لیے are the same two words. WER counts
whitespace-separated tokens, so that spelling difference alone scores as two
errors. This is where most of the gap between published numbers lives.
Scoring
wer and cer take a string or a list of strings, normalize both sides under a
named mode, and pool the result over the corpus.
from urdunorm import wer, cer, score
wer(references, hypotheses, mode="eval")
cer(references, hypotheses, mode="eval")
result = score(references, hypotheses, mode="eval")
result.rate, result.substitutions, result.deletions, result.insertions
str(result) # 'WER 12.40% [eval]'
Tokenization is a whitespace split, and the corpus rate is total errors over total reference tokens, never the mean of per-utterance rates. Both are stated here because leaving them to the caller is how the same model ends up with two different published numbers.
There is no dependency on a WER library. Edit distance is thirty lines, and
inheriting another package's default transform would reintroduce exactly the
unstated normalization this one removes. The test suite checks agreement with
jiwer where it is installed: WER, CER, and total edit distance match exactly.
The substitution/deletion/insertion split does not always match, because edit
distance has several equally optimal alignments and the split is a tie-breaking
choice rather than a fact.
Modes
| Mode | Characters | Diacritics | Punctuation | Digits | Space repair | Latin |
|---|---|---|---|---|---|---|
eval |
unified | stripped | dropped | unified to ASCII | on | lowercased |
display |
unified | kept | kept | kept | off | kept |
lums-compat |
unified | stripped | dropped | unified to ASCII | off | lowercased |
eval is the protocol this project reports under.
display is for human-facing transcripts. It fixes keyboard variants, removes
tatweel and zero-width controls, and folds أ إ ٱ to plain ا. It keeps
diacritics, punctuation, digits and case.
lums-compat reproduces the preprocessing of WER We Stand (COLING 2025), the
only published Urdu ASR benchmark, so numbers can be compared against it. Their
scoring code is published; one data file it loads is not, so the character
whitelist here is a reconstruction, validated by feeding it to their own
unmodified scorer and recovering their published table to a mean absolute error
of 0.046 points. urdunorm.lums carries the faithful asymmetric reproduction,
and its docstrings record what the paper leaves ambiguous and what we assumed.
Every number this project reports appears twice, once under eval and once under
lums-compat. A single-protocol Urdu WER is not a comparable quantity.
What is preserved
These are phonemic in Urdu. No mode merges them, and the package refuses to import if a rule table would:
ھ do-chashmi heh, ں noon ghunna, ے barree yeh,
ۓ, آ alef madda, ئ, ؤ, ۂ, and the
retroflex and Perso-Urdu consonants ٹ ڈ ڑ پ چ ژ گ.
Protocols are data
A mode is a frozen dataclass, not a code path. That makes ablations cheap and makes the exact configuration reportable.
from urdunorm import EVAL, apply_protocol
no_repair = EVAL.with_(repair_spaces=False)
apply_protocol(text, no_repair)
urdunorm --show-protocol --mode eval
Rules live in reviewable data files
The space-repair lexicon and the Urdu number tables are tab-separated files under
src/urdunorm/data/, not Python literals, so a native Urdu speaker can review
them without reading code. See docs/data-review.md.
The number tables and the space-repair lexicon have not yet been reviewed by a
native speaker. numbers_to_words is off in every mode until they are.
Dependencies
None at runtime, deliberately. urdunorm must stay installable by people who
only want the normalizer, so it never imports torch, transformers, or
datasets.
Citing
Not yet published. A DOI will be minted at the first tagged release.
License
Apache-2.0.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file urdunorm-0.1.0.tar.gz.
File metadata
- Download URL: urdunorm-0.1.0.tar.gz
- Upload date:
- Size: 121.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f697ea0ead3071ef339fc23cd0c3297a50930780b7f412f05b22370274488c28
|
|
| MD5 |
f2d1ed620d4e4912bb2b096d7c103a01
|
|
| BLAKE2b-256 |
d8f9aeeecf36854cd56bbbf4b3c49cdc70dad9de8a537a0dd6e8035603bd69f5
|
Provenance
The following attestation bundles were made for urdunorm-0.1.0.tar.gz:
Publisher:
publish.yml on hunzed/urdunorm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
urdunorm-0.1.0.tar.gz -
Subject digest:
f697ea0ead3071ef339fc23cd0c3297a50930780b7f412f05b22370274488c28 - Sigstore transparency entry: 2398869598
- Sigstore integration time:
-
Permalink:
hunzed/urdunorm@919171dfd3ba366905f1eca1a77d36a2a30b188f -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/hunzed
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@919171dfd3ba366905f1eca1a77d36a2a30b188f -
Trigger Event:
release
-
Statement type:
File details
Details for the file urdunorm-0.1.0-py3-none-any.whl.
File metadata
- Download URL: urdunorm-0.1.0-py3-none-any.whl
- Upload date:
- Size: 47.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
05ddfd0f14b2a815fd673c670587470ab799a474cb952693e5ed251533ef7ae8
|
|
| MD5 |
e7f0151de4d06e4a54d8da0bce3b7cf8
|
|
| BLAKE2b-256 |
15b048f030244ea5bde12868f08f4765b47b5899cdd80e7243a21699f85b4c3d
|
Provenance
The following attestation bundles were made for urdunorm-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on hunzed/urdunorm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
urdunorm-0.1.0-py3-none-any.whl -
Subject digest:
05ddfd0f14b2a815fd673c670587470ab799a474cb952693e5ed251533ef7ae8 - Sigstore transparency entry: 2398869722
- Sigstore integration time:
-
Permalink:
hunzed/urdunorm@919171dfd3ba366905f1eca1a77d36a2a30b188f -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/hunzed
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@919171dfd3ba366905f1eca1a77d36a2a30b188f -
Trigger Event:
release
-
Statement type: