extended-wer-normalizer 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,83 @@
1
+ """extended-wer-normalizer: jiwer-compatible WER normalization for voice AI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import jiwer
6
+
7
+ from .transforms import (
8
+ CollapseRepetitions,
9
+ DigitWordsToChars,
10
+ ExpandAbbreviations,
11
+ ExpandDigitRuns,
12
+ FinalDigitWordCleanup,
13
+ NormalizeCurrency,
14
+ NormalizeEmails,
15
+ NormalizeOrdinals,
16
+ NormalizePercentages,
17
+ NormalizeSymbols,
18
+ NormalizeURLs,
19
+ RemoveFillerWords,
20
+ WhisperBasicNormalize,
21
+ WhisperEnglishNormalize,
22
+ )
23
+
24
+ __all__ = [
25
+ "normalize_for_wer",
26
+ "english_wer_pipeline",
27
+ "CollapseRepetitions",
28
+ "DigitWordsToChars",
29
+ "ExpandAbbreviations",
30
+ "ExpandDigitRuns",
31
+ "FinalDigitWordCleanup",
32
+ "NormalizeCurrency",
33
+ "NormalizeEmails",
34
+ "NormalizeOrdinals",
35
+ "NormalizePercentages",
36
+ "NormalizeSymbols",
37
+ "NormalizeURLs",
38
+ "RemoveFillerWords",
39
+ "WhisperBasicNormalize",
40
+ "WhisperEnglishNormalize",
41
+ ]
42
+
43
+ english_wer_pipeline = jiwer.Compose([
44
+ # Pre-WhisperEN: handle patterns WhisperEN would mangle or remove
45
+ NormalizeEmails(), # must be first: WhisperEN strips @ and dots in emails
46
+ NormalizeURLs(), # WhisperEN strips slashes and scheme prefixes
47
+ NormalizeSymbols(), # WhisperEN removes & and + as punctuation
48
+ ExpandAbbreviations(), # expand Dr./Mr. before WhisperEN lowercases
49
+ # Core Whisper normalization (lowercase, punctuation, contractions, compound numbers)
50
+ ExpandDigitRuns(), # preserve leading zeros before WhisperEN sees e.g. "0176"
51
+ DigitWordsToChars(), # "zero one seven" → "0 1 7" before compound resolution
52
+ WhisperEnglishNormalize(),
53
+ # Post-WhisperEN: handle patterns WhisperEN preserves as-is or converts to compact form
54
+ # (WhisperEN preserves $5.99, 50%, 1st; and converts spoken forms back to compact)
55
+ ExpandDigitRuns(), # split any remaining digit runs (e.g. from compound numbers)
56
+ FinalDigitWordCleanup(), # final digit-word sweep — must run before currency/% so
57
+ # it doesn't convert "nine" inside "ninety nine cents"
58
+ NormalizeCurrency(), # $5.99 → five dollars ninety nine cents
59
+ NormalizePercentages(), # 50% → fifty percent
60
+ NormalizeOrdinals(), # 1st → first
61
+ RemoveFillerWords(),
62
+ CollapseRepetitions(),
63
+ ])
64
+
65
+
66
+ def normalize_for_wer(text: str, language: str = "en") -> str:
67
+ """Normalize text for WER comparison.
68
+
69
+ English pipeline applies, in order:
70
+ email → URL → symbol → currency → percentage → ordinal → abbreviation
71
+ → digit-run expansion → digit-word-to-char → Whisper EnglishTextNormalizer
72
+ → digit-run expansion → final digit cleanup → filler removal → repetition collapse
73
+
74
+ All number representations collapse to space-separated single digits, so
75
+ "0176", "zero one seven six", and "21" all compare correctly.
76
+
77
+ For non-English text, Whisper's BasicTextNormalizer is applied instead.
78
+ """
79
+ if language == "en":
80
+ result = english_wer_pipeline([text])
81
+ return result[0].strip()
82
+ from whisper_normalizer.basic import BasicTextNormalizer
83
+ return BasicTextNormalizer()(text).strip()
@@ -0,0 +1,400 @@
1
+ """jiwer-compatible AbstractTransform subclasses for WER normalization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from jiwer import AbstractTransform
8
+ from whisper_normalizer.basic import BasicTextNormalizer
9
+ from whisper_normalizer.english import EnglishTextNormalizer
10
+
11
+ # ---------------------------------------------------------------------------
12
+ # Private singletons
13
+ # ---------------------------------------------------------------------------
14
+
15
+ _basic_normalizer = BasicTextNormalizer()
16
+ _english_normalizer = EnglishTextNormalizer()
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Number normalization helpers (ported from aicpy)
20
+ # ---------------------------------------------------------------------------
21
+
22
+ _DIGIT_WORDS: dict[str, str] = {
23
+ "zero": "0", "one": "1", "two": "2", "three": "3", "four": "4",
24
+ "five": "5", "six": "6", "seven": "7", "eight": "8", "nine": "9",
25
+ }
26
+
27
+ _COMPOSITIONAL_NUMBER_WORDS: frozenset[str] = frozenset({
28
+ "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen",
29
+ "seventeen", "eighteen", "nineteen", "twenty", "thirty", "forty",
30
+ "fifty", "sixty", "seventy", "eighty", "ninety",
31
+ "hundred", "thousand", "million", "billion", "trillion",
32
+ })
33
+
34
+ _DIGIT_WORD_RE = re.compile(
35
+ r"\b(" + "|".join(_DIGIT_WORDS) + r")\b",
36
+ re.IGNORECASE,
37
+ )
38
+
39
+
40
+ class ExpandDigitRuns(AbstractTransform):
41
+ """Split runs of 2+ consecutive digits into space-separated single digits.
42
+
43
+ Preserves leading zeros that EnglishTextNormalizer would otherwise drop:
44
+ "0176" → "0 1 7 6"
45
+ "$5.99" → unchanged (decimal digits not split to preserve currency patterns)
46
+ """
47
+
48
+ def process_string(self, s: str) -> str:
49
+ # Lookbehind: skip runs following a decimal point or currency symbol.
50
+ # Lookahead: skip runs immediately before % or ordinal suffixes (st/nd/rd/th),
51
+ # which are handled by NormalizePercentages / NormalizeOrdinals later.
52
+ return re.sub(
53
+ r"(?<![.$€£¥₹])\d{2,}(?![%]|(?:st|nd|rd|th)\b)",
54
+ lambda m: " ".join(m.group()),
55
+ s,
56
+ )
57
+
58
+
59
+ class DigitWordsToChars(AbstractTransform):
60
+ """Convert isolated single-digit words (zero–nine) to digit characters.
61
+
62
+ Leaves digit words adjacent to compositional number words (twenty, hundred…)
63
+ so EnglishTextNormalizer can compose compound numbers correctly.
64
+ """
65
+
66
+ def process_string(self, s: str) -> str:
67
+ words = s.split()
68
+ out: list[str] = []
69
+ for i, word in enumerate(words):
70
+ bare = word.lower().rstrip(".,!?;:")
71
+ if bare in _DIGIT_WORDS:
72
+ left = words[i - 1].lower().rstrip(".,!?;:") if i > 0 else ""
73
+ right = words[i + 1].lower().rstrip(".,!?;:") if i < len(words) - 1 else ""
74
+ if left in _COMPOSITIONAL_NUMBER_WORDS or right in _COMPOSITIONAL_NUMBER_WORDS:
75
+ out.append(word)
76
+ else:
77
+ punct = word[len(word.rstrip(".,!?;:")):]
78
+ out.append(_DIGIT_WORDS[bare] + punct)
79
+ else:
80
+ out.append(word)
81
+ return " ".join(out)
82
+
83
+
84
+ class WhisperEnglishNormalize(AbstractTransform):
85
+ """Apply OpenAI Whisper's EnglishTextNormalizer.
86
+
87
+ Handles lowercase, punctuation removal, contraction expansion, and
88
+ compound number words ("twenty one" → "21").
89
+ """
90
+
91
+ def process_string(self, s: str) -> str:
92
+ return _english_normalizer(s)
93
+
94
+
95
+ class WhisperBasicNormalize(AbstractTransform):
96
+ """Apply OpenAI Whisper's BasicTextNormalizer (language-agnostic)."""
97
+
98
+ def process_string(self, s: str) -> str:
99
+ return _basic_normalizer(s)
100
+
101
+
102
+ class FinalDigitWordCleanup(AbstractTransform):
103
+ """Unconditional regex sweep: convert any residual digit word to a digit.
104
+
105
+ Safe to use after EnglishTextNormalizer has resolved all compound numbers.
106
+ """
107
+
108
+ def process_string(self, s: str) -> str:
109
+ return _DIGIT_WORD_RE.sub(lambda m: _DIGIT_WORDS[m.group().lower()], s)
110
+
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # Email normalization
114
+ # ---------------------------------------------------------------------------
115
+
116
+ # Match RFC-5321-ish local parts and domains before the normalizer lowercases
117
+ _EMAIL_RE = re.compile(
118
+ r"[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}",
119
+ )
120
+
121
+
122
+ def _email_to_words(match: re.Match) -> str: # type: ignore[type-arg]
123
+ email = match.group()
124
+ local, domain = email.split("@", 1)
125
+
126
+ def _expand(part: str) -> str:
127
+ part = re.sub(r"\.", " dot ", part)
128
+ part = re.sub(r"-", " dash ", part)
129
+ part = re.sub(r"\+", " plus ", part)
130
+ part = re.sub(r"_", " underscore ", part)
131
+ return part
132
+
133
+ return _expand(local) + " at " + _expand(domain)
134
+
135
+
136
+ class NormalizeEmails(AbstractTransform):
137
+ """Convert email addresses to spoken-word form.
138
+
139
+ Example: user.name+tag@sub.example.com
140
+ → "user dot name plus tag at sub dot example dot com"
141
+ """
142
+
143
+ def process_string(self, s: str) -> str:
144
+ return _EMAIL_RE.sub(_email_to_words, s)
145
+
146
+
147
+ # ---------------------------------------------------------------------------
148
+ # URL normalization
149
+ # ---------------------------------------------------------------------------
150
+
151
+ _URL_RE = re.compile(
152
+ r"https?://(?:www\.)?([A-Za-z0-9.\-]+)(?:/[^\s]*)?",
153
+ )
154
+
155
+
156
+ def _url_to_words(match: re.Match) -> str: # type: ignore[type-arg]
157
+ domain = match.group(1)
158
+ domain = re.sub(r"\.", " dot ", domain)
159
+ domain = re.sub(r"-", " dash ", domain)
160
+ return domain.strip()
161
+
162
+
163
+ class NormalizeURLs(AbstractTransform):
164
+ """Convert URLs to their spoken domain form, dropping scheme and path.
165
+
166
+ Example: "https://www.example-site.com/some/path?q=1" → "example dash site dot com"
167
+ """
168
+
169
+ def process_string(self, s: str) -> str:
170
+ return _URL_RE.sub(_url_to_words, s)
171
+
172
+
173
+ # ---------------------------------------------------------------------------
174
+ # Filler words & stutter/repetition
175
+ # ---------------------------------------------------------------------------
176
+
177
+ _FILLER_RE = re.compile(
178
+ r"\b(um+|uh+|hmm+|hm+|mm+|er+|ah+|eh+|mhm+|erm+)\b",
179
+ re.IGNORECASE,
180
+ )
181
+
182
+ _MULTI_SPACE_RE = re.compile(r" {2,}")
183
+
184
+
185
+ class RemoveFillerWords(AbstractTransform):
186
+ """Remove spoken filler words (um, uh, hmm, er, ah, …).
187
+
188
+ Common in spontaneous speech and often transcribed by ASR models.
189
+ """
190
+
191
+ def process_string(self, s: str) -> str:
192
+ s = _FILLER_RE.sub("", s)
193
+ return _MULTI_SPACE_RE.sub(" ", s).strip()
194
+
195
+
196
+ class CollapseRepetitions(AbstractTransform):
197
+ """Collapse consecutive identical words to a single occurrence.
198
+
199
+ Models ASR output stuttering: "I I I think" → "I think".
200
+ Case-insensitive comparison; preserves the first occurrence's casing.
201
+ """
202
+
203
+ def process_string(self, s: str) -> str:
204
+ words = s.split()
205
+ if not words:
206
+ return s
207
+ out = [words[0]]
208
+ for w in words[1:]:
209
+ if w.lower() != out[-1].lower():
210
+ out.append(w)
211
+ return " ".join(out)
212
+
213
+
214
+ # ---------------------------------------------------------------------------
215
+ # Abbreviation expansion
216
+ # ---------------------------------------------------------------------------
217
+
218
+ _ABBREVIATIONS: dict[str, str] = {
219
+ "dr.": "doctor",
220
+ "mr.": "mister",
221
+ "mrs.": "missus",
222
+ "ms.": "miss",
223
+ "prof.": "professor",
224
+ "sr.": "senior",
225
+ "jr.": "junior",
226
+ "vs.": "versus",
227
+ "etc.": "et cetera",
228
+ "approx.": "approximately",
229
+ "dept.": "department",
230
+ "est.": "established",
231
+ "inc.": "incorporated",
232
+ "ltd.": "limited",
233
+ "corp.": "corporation",
234
+ "ave.": "avenue",
235
+ "blvd.": "boulevard",
236
+ "st.": "street",
237
+ "jan.": "january",
238
+ "feb.": "february",
239
+ "mar.": "march",
240
+ "apr.": "april",
241
+ "aug.": "august",
242
+ "sep.": "september",
243
+ "oct.": "october",
244
+ "nov.": "november",
245
+ "dec.": "december",
246
+ }
247
+
248
+ # Build a single regex that matches any abbreviation (case-insensitive, word boundary on left)
249
+ _ABBREV_RE = re.compile(
250
+ r"(?<!\w)(" + "|".join(re.escape(k) for k in _ABBREVIATIONS) + r")",
251
+ re.IGNORECASE,
252
+ )
253
+
254
+
255
+ class ExpandAbbreviations(AbstractTransform):
256
+ """Expand common spoken abbreviations to their full form.
257
+
258
+ Examples: "Dr. Smith" → "doctor Smith", "vs." → "versus"
259
+ """
260
+
261
+ def process_string(self, s: str) -> str:
262
+ return _ABBREV_RE.sub(lambda m: _ABBREVIATIONS[m.group().lower()], s)
263
+
264
+
265
+ # ---------------------------------------------------------------------------
266
+ # Symbol normalization
267
+ # ---------------------------------------------------------------------------
268
+
269
+ # Order matters: longer tokens first, and don't match inside emails/URLs
270
+ _SYMBOL_MAP: list[tuple[str, str]] = [
271
+ ("&amp;", "and"),
272
+ ("&", "and"),
273
+ (" + ", " plus "),
274
+ (" @ ", " at "),
275
+ (" # ", " number "),
276
+ (" * ", " times "),
277
+ (" = ", " equals "),
278
+ (" > ", " greater than "),
279
+ (" < ", " less than "),
280
+ ("...", " "),
281
+ ]
282
+
283
+
284
+ class NormalizeSymbols(AbstractTransform):
285
+ """Replace common written symbols with their spoken equivalents.
286
+
287
+ Only replaces symbols that appear as standalone tokens (surrounded by spaces
288
+ or text boundaries) to avoid corrupting emails and URLs.
289
+ """
290
+
291
+ def process_string(self, s: str) -> str:
292
+ for symbol, word in _SYMBOL_MAP:
293
+ s = s.replace(symbol, word)
294
+ return s
295
+
296
+
297
+ # ---------------------------------------------------------------------------
298
+ # Currency normalization
299
+ # ---------------------------------------------------------------------------
300
+
301
+ _CURRENCY_SYMBOLS: dict[str, tuple[str, str]] = {
302
+ "$": ("dollar", "cent"),
303
+ "€": ("euro", "cent"),
304
+ "£": ("pound", "penny"),
305
+ "¥": ("yen", ""),
306
+ "₹": ("rupee", "paisa"),
307
+ }
308
+
309
+ _CURRENCY_RE = re.compile(
310
+ r"([$€£¥₹])\s*(\d+)(?:\.(\d{1,2}))?",
311
+ )
312
+
313
+
314
+ _IRREGULAR_PLURALS: dict[str, str] = {"penny": "pennies", "paisa": "paise"}
315
+
316
+
317
+ def _pluralize(unit: str, count: int) -> str:
318
+ if count == 1:
319
+ return unit
320
+ return _IRREGULAR_PLURALS.get(unit, unit + "s")
321
+
322
+
323
+ def _currency_to_words(match: re.Match) -> str: # type: ignore[type-arg]
324
+ from num2words import num2words
325
+
326
+ symbol = match.group(1)
327
+ major = int(match.group(2))
328
+ minor_str = match.group(3)
329
+ unit_major, unit_minor = _CURRENCY_SYMBOLS.get(symbol, ("dollar", "cent"))
330
+
331
+ def n2w(n: int) -> str:
332
+ return num2words(n).replace("-", " ")
333
+
334
+ parts = [f"{n2w(major)} {_pluralize(unit_major, major)}"]
335
+ if minor_str:
336
+ minor = int(minor_str.ljust(2, "0"))
337
+ if minor > 0 and unit_minor:
338
+ parts.append(f"{n2w(minor)} {_pluralize(unit_minor, minor)}")
339
+ return " ".join(parts)
340
+
341
+
342
+ class NormalizeCurrency(AbstractTransform):
343
+ """Convert currency amounts to spoken form.
344
+
345
+ Examples:
346
+ "$5" → "five dollars"
347
+ "$5.99" → "five dollars ninety nine cents"
348
+ "€3.50" → "three euros fifty cents"
349
+ """
350
+
351
+ def process_string(self, s: str) -> str:
352
+ return _CURRENCY_RE.sub(_currency_to_words, s)
353
+
354
+
355
+ # ---------------------------------------------------------------------------
356
+ # Percentage normalization
357
+ # ---------------------------------------------------------------------------
358
+
359
+ _PERCENT_RE = re.compile(r"(\d+(?:\.\d+)?)\s*%")
360
+
361
+
362
+ class NormalizePercentages(AbstractTransform):
363
+ """Convert percentage notation to spoken form.
364
+
365
+ Examples: "50%" → "fifty percent", "3.5%" → "three point five percent"
366
+ """
367
+
368
+ def process_string(self, s: str) -> str:
369
+ from num2words import num2words
370
+
371
+ def _replace(m: re.Match) -> str: # type: ignore[type-arg]
372
+ val_str = m.group(1)
373
+ val = float(val_str)
374
+ if val == int(val):
375
+ return f"{num2words(int(val))} percent"
376
+ return f"{num2words(val)} percent"
377
+
378
+ return _PERCENT_RE.sub(_replace, s)
379
+
380
+
381
+ # ---------------------------------------------------------------------------
382
+ # Ordinal normalization
383
+ # ---------------------------------------------------------------------------
384
+
385
+ _ORDINAL_RE = re.compile(r"\b(\d+)(st|nd|rd|th)\b", re.IGNORECASE)
386
+
387
+
388
+ class NormalizeOrdinals(AbstractTransform):
389
+ """Convert ordinal numerals to spoken word form.
390
+
391
+ Examples: "1st" → "first", "2nd" → "second", "15th" → "fifteenth"
392
+ """
393
+
394
+ def process_string(self, s: str) -> str:
395
+ from num2words import num2words
396
+
397
+ def _replace(m: re.Match) -> str: # type: ignore[type-arg]
398
+ return num2words(int(m.group(1)), to="ordinal")
399
+
400
+ return _ORDINAL_RE.sub(_replace, s)
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: extended-wer-normalizer
3
+ Version: 0.1.0
4
+ Summary: jiwer-compatible WER normalizer with number, email, URL, filler, and symbol normalization for voice AI evaluation
5
+ Project-URL: Homepage, https://github.com/ai-coustics/extended-wer-normalizer
6
+ License: MIT
7
+ Keywords: asr,jiwer,normalization,speech recognition,voice ai,wer
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: jiwer>=3.0
20
+ Requires-Dist: num2words>=0.5
21
+ Requires-Dist: whisper-normalizer>=0.1.12
22
+ Description-Content-Type: text/markdown
23
+
24
+ # extended-wer-normalizer
25
+
26
+ jiwer-compatible text normalizer for Word Error Rate (WER) evaluation in voice AI.
27
+
28
+ Extends [jiwer](https://github.com/jitsi/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.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install extended-wer-normalizer
34
+ ```
35
+
36
+ ## Quick start
37
+
38
+ ```python
39
+ from extended_wer_normalizer import normalize_for_wer
40
+
41
+ normalize_for_wer("Call 0176 or email info@example.com, it costs $5.99")
42
+ # → "call 0 1 7 6 or email info at example dot com it costs five dollars ninety nine cents"
43
+
44
+ normalize_for_wer("Um, 1st place goes to Dr. Smith with 50% accuracy")
45
+ # → "first place goes to doctor smith with fifty percent accuracy"
46
+ ```
47
+
48
+ ## jiwer integration
49
+
50
+ Every normalization is a `jiwer.AbstractTransform` subclass — compose them freely:
51
+
52
+ ```python
53
+ import jiwer
54
+ from extended_wer_normalizer.transforms import NormalizeEmails, ExpandDigitRuns
55
+
56
+ pipeline = jiwer.Compose([
57
+ NormalizeEmails(),
58
+ ExpandDigitRuns(),
59
+ jiwer.ToLowerCase(),
60
+ jiwer.RemovePunctuation(),
61
+ jiwer.ReduceToListOfListOfWords(),
62
+ ])
63
+
64
+ wer = jiwer.wer("info at example dot com", "info@example.com", hypothesis_transform=pipeline)
65
+ ```
66
+
67
+ Use the pre-built pipeline directly with `jiwer.wer`:
68
+
69
+ ```python
70
+ import jiwer
71
+ from extended_wer_normalizer import english_wer_pipeline
72
+
73
+ wer = jiwer.wer(reference, hypothesis, reference_transform=english_wer_pipeline, hypothesis_transform=english_wer_pipeline)
74
+ ```
75
+
76
+ ## Available transforms
77
+
78
+ | Transform | Example |
79
+ |---|---|
80
+ | `ExpandDigitRuns` | `"0176"` → `"0 1 7 6"` |
81
+ | `DigitWordsToChars` | `"zero one seven"` → `"0 1 7"` |
82
+ | `WhisperEnglishNormalize` | lowercase, punctuation, contractions, compound numbers |
83
+ | `WhisperBasicNormalize` | language-agnostic basic normalization |
84
+ | `FinalDigitWordCleanup` | residual digit-word sweep after compound resolution |
85
+ | `NormalizeEmails` | `"user@example.com"` → `"user at example dot com"` |
86
+ | `NormalizeURLs` | `"https://example.com/path"` → `"example dot com"` |
87
+ | `NormalizeCurrency` | `"$5.99"` → `"five dollars ninety nine cents"` |
88
+ | `NormalizePercentages` | `"50%"` → `"fifty percent"` |
89
+ | `NormalizeOrdinals` | `"1st"` → `"first"`, `"15th"` → `"fifteenth"` |
90
+ | `ExpandAbbreviations` | `"Dr."` → `"doctor"`, `"vs."` → `"versus"` |
91
+ | `NormalizeSymbols` | `"cats & dogs"` → `"cats and dogs"` |
92
+ | `RemoveFillerWords` | removes `um`, `uh`, `hmm`, `er`, `ah`, … |
93
+ | `CollapseRepetitions` | `"I I I think"` → `"I think"` |
94
+
95
+ ## Pipeline design
96
+
97
+ The English pipeline applies transforms in an order that ensures idempotence and correct interaction with Whisper's `EnglishTextNormalizer`:
98
+
99
+ 1. **Pre-Whisper**: email, URL, symbol, abbreviation (patterns Whisper would mangle)
100
+ 2. **Digit preparation**: expand digit runs, convert digit words
101
+ 3. **Core**: `WhisperEnglishNormalize` (lowercase, punctuation, contractions, compound numbers → digits)
102
+ 4. **Post-Whisper**: digit run expansion, digit word cleanup, currency, percentage, ordinal (patterns Whisper preserves or compacts)
103
+ 5. **Cleanup**: filler words, repetition collapse
104
+
105
+ ## Non-English
106
+
107
+ For non-English text, pass `language` to get Whisper's `BasicTextNormalizer`:
108
+
109
+ ```python
110
+ normalize_for_wer("Das kostet fünf Euro", language="de")
111
+ ```
@@ -0,0 +1,5 @@
1
+ extended_wer_normalizer/__init__.py,sha256=Kxqe_6iY2PNxjl_7L3M8op_xuG5dVp6qMHohKf4ZGSs,3175
2
+ extended_wer_normalizer/transforms.py,sha256=uHXHL9y8ywgZHQKBykg9IHLpdNn2vSkp3u5LeB2e3cU,12547
3
+ extended_wer_normalizer-0.1.0.dist-info/METADATA,sha256=ifw5GxTG555K6IwxocPHxkopFpHhFFfXfigwia3d1Sw,4377
4
+ extended_wer_normalizer-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
5
+ extended_wer_normalizer-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any