From 21ecdd3335e44b7de638d3da64d6c15fd8b2fac9 Mon Sep 17 00:00:00 2001 From: Linxiushen <142557582+Linxiushen@users.noreply.github.com> Date: Thu, 24 Sep 2026 16:32:05 +0800 Subject: [PATCH] Fix " and a half" handling in the English normalizer EnglishNumberNormalizer.preprocess() splits the text on "and a half" and rebuilds it, but skipped any segment that was empty after stripping. The `continue` skipped the replacement token as well as the empty segment, so the phrase was deleted whenever it started the string or two occurrences were adjacent: "and a half hours later" -> "hours later" and "one and a half and a half" -> "1.5". A normalizer used for WER scoring should never drop words. The segment's last word was also only tested against the spelled-out number words, so Arabic numerals were left unfolded and inconsistent with their spelled-out equivalents: "two and a half million" -> "2500000" but "2 and a half million" -> "2 and a half 1000000", and "forty eight and a half dollars" -> "$48.5" but "48 and a half dollars" was unchanged. Reference/hypothesis pairs that differ only in digit spelling therefore picked up spurious WER errors. Always emit a replacement for every split point, falling back to the original "and a half" when the preceding word is not a number, and accept an integer written in digits alongside the spelled-out number words. Co-Authored-By: Claude Opus 5 (1M context) --- tests/test_normalizer.py | 4 ++++ whisper/normalizers/english.py | 23 +++++++++++++---------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/tests/test_normalizer.py b/tests/test_normalizer.py index 7f89da0ec..aa3c8abd6 100644 --- a/tests/test_normalizer.py +++ b/tests/test_normalizer.py @@ -74,6 +74,10 @@ def test_number_normalizer(std): assert std("three and a half million") == "3500000" assert std("forty eight and a half dollars") == "$48.5" + assert std("2 and a half million") == "2500000" + assert std("48 and a half dollars") == "$48.5" + assert std("and a half hours later") == "and a half hours later" + assert std("one and a half and a half") == "1.5 and a half" assert std("b747") == "b 747" assert std("10 th") == "10th" assert std("10th") == "10th" diff --git a/whisper/normalizers/english.py b/whisper/normalizers/english.py index 21c3a54e5..f3b6ec080 100644 --- a/whisper/normalizers/english.py +++ b/whisper/normalizers/english.py @@ -391,17 +391,20 @@ def preprocess(self, s: str): segments = re.split(r"\band\s+a\s+half\b", s) for i, segment in enumerate(segments): - if len(segment.strip()) == 0: - continue + results.append(segment) if i == len(segments) - 1: - results.append(segment) - else: - results.append(segment) - last_word = segment.rsplit(maxsplit=2)[-1] - if last_word in self.decimals or last_word in self.multipliers: - results.append("point five") - else: - results.append("and a half") + break + + words = segment.split() + last_word = words[-1] if words else "" + if ( + last_word in self.decimals + or last_word in self.multipliers + or re.fullmatch(r"\d+", last_word) # e.g. "2 and a half" + ): + results.append("point five") + else: # not a number; keep the original words rather than dropping them + results.append("and a half") s = " ".join(results)