Skip to content

fix: replace words wrapped in leading punctuation#6322

Open
NishchayMahor wants to merge 1 commit into
livekit:mainfrom
NishchayMahor:fix/replace-words-leading-punctuation
Open

fix: replace words wrapped in leading punctuation#6322
NishchayMahor wants to merge 1 commit into
livekit:mainfrom
NishchayMahor:fix/replace-words-leading-punctuation

Conversation

@NishchayMahor

Copy link
Copy Markdown

What

tokenize.utils.replace_words handles a word's trailing punctuation but not its leading punctuation, so a word wrapped in quotes/parens/brackets or with a leading dash is never replaced — while the same word with trailing punctuation is:

replace_words(text="world!",   replacements={"world": "universe"})  # -> "universe!"   ✅
replace_words(text="(world)",  replacements={"world": "universe"})  # -> "(world)"     ❌ should be "(universe)"
replace_words(text='"world"',  replacements={"world": "universe"})  # -> '"world"'     ❌
replace_words(text="-world",   replacements={"world": "universe"})  # -> "-world"      ❌

split_words(ignore_punctuation=False) returns tokens with their surrounding punctuation ("(world)", '"world"', "-world"), but _process_words computes no_punctuation = word.rstrip(PUNCTUATIONS) — trailing only — so the dict lookup misses whenever there's leading punctuation. Asymmetric and surprising for TTS pronunciation substitution.

Fix

In _process_words (shared by both the sync and async paths), strip punctuation on both sides for the lookup and preserve whatever punctuation surrounded the word:

core = word.strip(punctuations)
leading_off  = len(word) - len(word.lstrip(punctuations))
trailing_off = len(word) - len(word.rstrip(punctuations))
...
text = text[: start + offset + leading_off] + replacement + text[end + offset - trailing_off :]
offset += len(replacement) - len(core)

This reduces exactly to the previous logic when leading_off == 0, so existing behavior (trailing punctuation, the A.B.CA.B.C.D internal-dot case) is unchanged.

Testing

Added test_replace_words_leading_punctuation to tests/test_tokenizer.py covering (world), "world", -world, hello (world)., trailing-only, and a no-false-positive case (worldly). The existing test_replace_words still passes (verified). Pure string functions — offline.

replace_words only stripped trailing punctuation (word.rstrip), so a word
wrapped in leading punctuation -- "(world)", '"world"', "-world" -- never
matched the replacements dict, while the same word with trailing punctuation
did. Strip punctuation on both sides and preserve whatever surrounded the word.
Reduces to the existing behavior when there is no leading punctuation. Adds a
test; fixes both the sync and async paths (shared _process_words).
@NishchayMahor NishchayMahor requested a review from a team as a code owner July 5, 2026 08:28
@CLAassistant

CLAassistant commented Jul 5, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants