Repository navigation
Fix "<number> and a half" handling in the English normalizer - #2867
Open
Linxiushen wants to merge 1 commit into
Open
Linxiushen wants to merge 1 commit into
Linxiushen wants to merge 1 commit into
Conversation
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) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
EnglishNumberNormalizer.preprocesscould silently delete the words"and a half"from a sentence, and failed to normalize the phrase when it followed a digit rather than a spelled-out number. This PR fixes both in the same loop and adds four regression assertions that fail onmain.2 and a half million2 and a half 1000000250000048 and a half dollars48 and a half dollars$48.5and a half hours laterhours laterand a half hours laterone and a half and a half1.51.5 and a halfRoot cause
preprocessrewrites"<number> and a half"as"<number> point five"by splitting the text on\band\s+a\s+half\band re-joining the pieces. Two defects in that loop:1.
"and a half"could be dropped from the text. Theif len(segment.strip()) == 0: continueguard skipped an empty segment and, with it, the"and a half"marker that belonged after it. So a sentence-initial or repeated occurrence lost words outright:"and a half hours later"->"hours later""and a half of the people"->"of the people""one and a half and a half"->"1.5"Because this normalizer is applied to both reference and hypothesis when computing WER, silently dropping words can mask or manufacture errors.
2. Only spelled-out numbers were recognised. The check was
last_word in self.decimals or last_word in self.multipliers, andself.decimalsholds only word forms ({*self.ones, *self.tens, *self.zeros}). A digit before the phrase fell through to theelsebranch, producing output that is neither the original text nor a normalized number:"2 and a half million"->"2 and a half 1000000""48 and a half dollars"->"48 and a half dollars"(expected"$48.5")Digit input is squarely in scope for this class:
process_wordsalready testsnext_is_numeric = re.match(r"^\d+(\.\d+)?$", next), and the existing tests coverstd("b747"),std("10 th"),std("200 thousand")andstd("minus 500"). The spelled-out form was already asserted to work (std("three and a half million") == "3500000"), so the digit form was an inconsistency rather than an unsupported input.Fix
Restructured the loop so the segment is always emitted and only the marker is decided:
breakafter the last one. Nothing can be dropped any more. The extra whitespace this can introduce is harmless —__call__already does" ".join(... s.split()).last_wordfromsegment.split()with an explicit empty-list fallback. This replaces thestrip()guard the old code relied on, and also removes a latentIndexError: the oldsegment.rsplit(maxsplit=2)[-1]would have raised on a whitespace-only segment.re.fullmatch(r"\d+", last_word)to the "is this a number?" test, using the same\dconvention asprocess_words' existingnext_is_numericcheck. Decimals are deliberately excluded, so"1.5 and a half"is left untouched rather than becoming"1.5 point five".Confined to one method; no public behaviour outside the
"and a half"path changes.Testing
Four assertions added to
test_number_normalizer, which is parametrized over bothEnglishNumberNormalizerandEnglishTextNormalizer. I verified each one fails onmainfor both normalizers and passes with the fix — by checking outorigin/mainin a throwawaygit worktree, copying in the patched test file, and running it withPYTHONPATHpinned to that worktree (confirming the worktree'senglish.pywas the module actually imported).pytest tests/test_normalizer.py— 4 passed; together withtests/test_tokenizer.py— 8 passed.black --check,isort --check-onlyandflake8(the repo's pre-commit args) are all clean on the two changed files.test_transcribe.py/test_audio.py/test_timing.py, which need model downloads and torch inference and exercise nothing in this change.One note for reviewers
"0 and a half"yields".5"whereas"zero and a half"yields"0.5". That asymmetry already exists in the digit path onmain("0 point five"->".5"); this change routes the digit form into it rather than introducing it. Happy to normalize that separately if you'd like it addressed here.🤖 Generated with Claude Code