diff --git a/stackvox/text.py b/stackvox/text.py index 8daffb7..3675c91 100644 --- a/stackvox/text.py +++ b/stackvox/text.py @@ -18,6 +18,7 @@ "markdown_to_paragraphs", "strip_emoji", "strip_thousands_separators", + "versions_to_words", "decimals_to_words", "expand_units", "apply_pronunciations", @@ -84,6 +85,21 @@ def strip_thousands_separators(text: str) -> str: return re.sub(r"(?<=\d),(?=\d)", "", text) +_VERSION = re.compile(r"(? str: + """1.2.3 -> "1 point 2 point 3"; 0.7.0 -> "0 point 7 point 0". + + A dotted version string has more than one decimal point, which + `decimals_to_words` can't handle — it splits only the first, leaving a stray + full stop mid-number ("0.7.0" -> "0 point 7.0"). Run this before it. Two-part + numbers are left untouched so ordinary decimals still read digit-by-digit, + and a trailing sentence stop (``upgrade to 1.2.3.``) is preserved. + """ + return _VERSION.sub(lambda m: m.group(0).replace(".", " point "), text) + + def decimals_to_words(text: str) -> str: """1198.9 -> "1198 point 9"; 770.72 -> "770 point 7 2". Removes the bare "." between digits, which TTS can otherwise read as a full stop.""" @@ -287,6 +303,7 @@ def _shape_paragraph( if expand_units_flag: # units BEFORE decimals (see note above) text = expand_units(text, locale) if expand_numbers_flag: + text = versions_to_words(text) # multi-dot versions BEFORE the decimal split text = decimals_to_words(text) return re.sub(r"[ \t]{2,}", " ", text).strip() diff --git a/tests/test_text.py b/tests/test_text.py index 5538445..6206c8f 100644 --- a/tests/test_text.py +++ b/tests/test_text.py @@ -10,6 +10,7 @@ shape_pauses, strip_emoji, strip_thousands_separators, + versions_to_words, ) # --- numbers --------------------------------------------------------------- @@ -28,6 +29,31 @@ def test_year_is_not_a_decimal(): assert decimals_to_words("built in 2023") == "built in 2023" +def test_semver_all_parts_spoken(): + assert versions_to_words("0.7.0") == "0 point 7 point 0" + assert versions_to_words("1.2.3") == "1 point 2 point 3" + + +def test_semver_multi_digit_component(): + assert versions_to_words("1.20.3") == "1 point 20 point 3" + + +def test_version_preserves_trailing_sentence_stop(): + assert versions_to_words("upgrade to 1.2.3.") == "upgrade to 1 point 2 point 3." + + +def test_two_part_number_is_left_for_the_decimal_pass(): + # Only two components — an ordinary decimal, not a version. + assert versions_to_words("3.14") == "3.14" + assert decimals_to_words(versions_to_words("3.14")) == "3 point 1 4" + + +def test_semver_normalizes_end_to_end(): + out = normalize_for_speech("Upgraded to 0.7.0 today.", markdown=False) + assert "0 point 7 point 0" in out + assert "0 point 7.0" not in out + + # --- units -----------------------------------------------------------------