Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions stackvox/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"markdown_to_paragraphs",
"strip_emoji",
"strip_thousands_separators",
"versions_to_words",
"decimals_to_words",
"expand_units",
"apply_pronunciations",
Expand Down Expand Up @@ -84,6 +85,21 @@ def strip_thousands_separators(text: str) -> str:
return re.sub(r"(?<=\d),(?=\d)", "", text)


_VERSION = re.compile(r"(?<!\d)\d+(?:\.\d+){2,}(?!\d)")


def versions_to_words(text: str) -> 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."""
Expand Down Expand Up @@ -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()

Expand Down
26 changes: 26 additions & 0 deletions tests/test_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
shape_pauses,
strip_emoji,
strip_thousands_separators,
versions_to_words,
)

# --- numbers ---------------------------------------------------------------
Expand All @@ -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 -----------------------------------------------------------------


Expand Down
Loading