Skip to content

Voice: preserve emitted whitespace in the sentence splitter - #4693

Closed
rajarshidattapy wants to merge 1 commit into
openai:mainfrom
rajarshidattapy:fix/voice-sentence-splitter-whitespace
Closed

Voice: preserve emitted whitespace in the sentence splitter#4693
rajarshidattapy wants to merge 1 commit into
openai:mainfrom
rajarshidattapy:fix/voice-sentence-splitter-whitespace

Conversation

@rajarshidattapy

Copy link
Copy Markdown
Contributor

Fixes #4687.

Problem

get_sentence_based_splitter — the default TTSModelSettings.text_splitter, applied to every streamed chunk in StreamedAudioResult — split a stripped buffer on a non-capturing pattern:

sentences = re.split(r"(?<=[.!?])\s+", text_buffer.strip())
combined_sentences = " ".join(sentences[:-1])

re.split discards the matched separator, so every inter-sentence whitespace run — \n, \n\n, multiple spaces — was normalized to a single space before the text reached the TTS model, and .strip() dropped the buffer's leading whitespace.

The symptom only exists in the audio: an agent that formats spoken output as list items or paragraphs loses the pause the engine renders for a line or paragraph break, so the speech runs together.

The function already guarded the trailing side of this class of bug (the "He " + "arrived""Hearrived" comment); the leading and inter-sentence sides were missed.

Fix

Capture the separators and split the raw buffer, then rebuild the flushed text from the alternating parts, as suggested in the issue:

parts = re.split(r"((?<=[.!?])\s+)", text_buffer)
if len(parts) >= 3:
    combined_sentences = "".join(parts[:-2])

parts alternates sentence, separator, …, sentence, so the last entry is the incomplete sentence and three or more entries mean a sentence has completed — which replaces the old len(sentences) >= 1 check that was always true.

The trailing whitespace handling is now implicit rather than reconstructed: parts[-1] is the incomplete sentence with whatever whitespace followed it, so the remainder still ends with the separator that keeps the next delta from being glued onto the last word.

All four cases from the issue now round-trip:

buffer before after
"Line one is long enough.\nLine two here.\nrest" 'Line one is long enough. Line two here.' 'Line one is long enough.\nLine two here.'
"First point.\n\nSecond point is long. tail" 'First point. Second point is long.' 'First point.\n\nSecond point is long.'
" Leading spaces here are kept? Yes indeed. tail" 'Leading spaces here are kept? Yes indeed.' ' Leading spaces here are kept? Yes indeed.'
"Hello world this is one. Double space kept? tail" 'Hello world this is one. Double space kept?' 'Hello world this is one. Double space kept?'

Scope

The separator that falls exactly on a flush boundary is still not carried over, as before: the text on either side of it becomes a separate TTS utterance, so there is nothing for the engine to render it into. What this restores is every separator inside a flushed chunk — which is the case that matters, since short sentences (bullets, list items) accumulate until they clear min_sentence_length and are spoken together.

Tests

  • test_split_preserves_the_whitespace_the_model_emitted — the four cases above.
  • test_streamed_paragraph_breaks_survive_any_delta_boundary — feeds short sentences through the splitter one delta at a time, parametrized over delta sizes, and asserts a \n\n reaches a spoken chunk regardless of where the model's deltas happen to break.

Both fail on main. The existing splitter tests are unchanged and still pass.

`get_sentence_based_splitter` split a stripped buffer on a non-capturing
pattern, so `re.split` discarded every inter-sentence separator and rejoined
the sentences with a single space. A line break or paragraph break between two
short sentences reached the TTS model as a plain space, losing the pause the
engine would have rendered, and the leading whitespace of the buffer was
dropped along with it.

Capture the separators and split the raw buffer, then rebuild the flushed text
from the alternating parts. The trailing whitespace still stays on the
remainder, so the next streamed delta is not glued onto the last word.
@seratch

seratch commented Aug 27, 2026

Copy link
Copy Markdown
Member

Please refer to #4687 (comment)

@seratch seratch closed this Aug 27, 2026
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.

Voice: get_sentence_based_splitter collapses paragraph breaks and drops leading whitespace

2 participants