Why Your TTS Sounds Choppy: Sentence-Boundary Segmentation Matters

When I first started generating audiobook-length narration with local TTS, I had a problem I could not name.

The voice sounded fine in short clips. Individual sentences were clean. But when I stitched everything together, the result felt wrong — not broken, just uneven. Energy levels would jump between segments. A calm sentence would be followed by one that sounded oddly rushed. Sometimes a word would get cut off at the boundary. The pacing drifted like a conversation where one person keeps interrupting.

I initially blamed the model. Then I blamed the audio mastering. Then I blamed the stitching code.

The real culprit was something I had glossed over months earlier: how I was splitting text into segments.

Building planning-first AI systems, one pipeline at a time. Get weekly deep dives into local AI, Rust, and agent orchestration — no fluff, no hype.

The Default Approach: Split on Character Count

Every TTS model has a maximum input length. For Qwen3-TTS on my hardware, reliable segments landed in the 20–40 second range. That translates to roughly 80–120 words, or somewhere around 400–600 characters.

The obvious approach — and the one I see in almost every open-source TTS wrapper — is to split text at a fixed character count.

text[..500]
text[500..1000]
text[1000..1500]

It is simple, predictable, and guarantees every segment fits within the model's input limit. It also produces audio that sounds like it was recorded by someone who keeps getting interrupted mid-thought.

Why Character Splitting Sounds Bad

The problem is invisible until you hear it. When a TTS model receives text, it generates audio that includes prosody — the rise and fall of pitch, the pauses between phrases, the emphasis on certain words. Prosody is computed over the entire input. The model decides how a sentence should sound based on its structure: where the subject is, where the verb is, where the period falls.

When you split mid-sentence, you cut the model's prosody planning in half.

The end of segment one trails off into an incomplete clause. The model has no idea that more text is coming, so it generates a downward inflection — the kind you use at the end of a statement. Then segment two starts mid-clause, and the model generates an opening inflection for words that are not actually the start of a sentence.

The result is an energy seam — a noticeable discontinuity where two separately generated clips meet. It is subtle enough that you might not catch it on a single listen. Over an hour of narration, it is exhausting.

The model does not know it is in the middle of a sentence. Every segment is a fresh start, and every fresh start has opening energy that does not belong there.

There is a second problem too. Character splitting can break words in half. If the cut happens to land inside "extraordinary," you get "extraordin" in one segment and "ary" in the next. Most pipelines handle this with a regex that snaps to the nearest space, but that still leaves you splitting between clauses, between subjects and verbs, between anything that is not a literal space character.

Sentence-Aware Segmentation

The fix sounds obvious in retrospect: split on sentence boundaries, not character counts. The implementation is less obvious.

My segmentation module in narrator-tts now works in three stages.

1. Boundary Detection

The first pass scans for sentence-ending punctuation: periods, exclamation marks, question marks. But punctuation alone is not enough. A period inside "Dr. Morrison" is not a sentence boundary. A question mark inside a quoted question might or might not be, depending on whether the narration continues after the closing quote.

I use a state-machine parser that tracks whether it is inside quotes, parentheses, or em-dashes. Only punctuation that appears in the top-level text context — not nested inside dialogue or parenthetical asides — counts as a real boundary.

This matters more than you would think. Manuscripts are full of edge cases:

"I'm not sure," she said. "But I'll find out."

That is one sentence of narration with two sentences of dialogue embedded in it. For TTS purposes, it should be one segment — the model needs to see the attribution tags to know who is speaking and how to pace the delivery.

2. Quote Handling

Dialogue is where naive segmentation falls apart completely. A passage of rapid-fire dialogue might have very short sentences:

"No."
"Yes."
"Are you sure?"
"I said no."

Each of these is technically a complete sentence. But generating them as separate TTS segments would produce a staccato mess — each line with its own opening energy, its own micro-pause at the start, its own prosodic reset.

My solution is a minimum word count per segment. If a sentence is below the threshold (currently set at 15 words), the segmenter keeps accumulating sentences until it crosses the threshold. Short dialogue lines get batched together into a single generation call.

This produces much smoother audio. The model sees enough context to maintain consistent pacing and energy across a rapid exchange.

3. Maximum Word Combining

The opposite problem also exists. Sometimes a sentence is very long — a paragraph-length run-on with multiple clauses, semicolons, and parenthetical asides. At 200+ words, it exceeds the model's comfortable input range.

For these cases, the segmenter looks for secondary split points: semicolons, em-dashes, or comma + conjunction patterns. It splits at the weakest syntactic boundary, the one where a pause is most natural. This means the energy seam lands at a point where a real narrator would also pause.

The goal is always the same: make the cut at a point where the listener expects a break.

The Result

After implementing sentence-aware segmentation, the energy seams disappeared. Not reduced — gone. The narration sounds continuous in a way that character-split audio never did.

There are still boundaries between segments, of course. But they land at sentence endings, where the model's natural prosody already produces a downward inflection and a pause. The next segment starts at the beginning of a new sentence, where opening energy is expected. The seams become inaudible.

The pacing drift also resolved. When every segment is a complete thought, the model maintains consistent pacing across the entire chapter. It does not rush through a half-sentence or trail off into an incomplete clause.

The General Principle

This is the same pattern that shows up everywhere in AI pipeline work: the boundary between chunks matters more than the content of each chunk.

I saw it with the ASR reconciliation pipeline, where word-level alignment across segment boundaries was the hardest part. I saw it with the caption burner, where frame-boundary timing determined whether captions flickered. And I saw it here, where the boundary between text segments determined whether the audio sounded continuous or chopped.

When you build a pipeline that processes data in chunks, the chunking strategy is not a preprocessing detail. It is part of the system's quality surface. Get it wrong, and no amount of post-processing will fix the result. Get it right, and the rest of the pipeline gets easier — because you are feeding the model inputs it can actually work with.

Character splitting is the path of least resistance. Sentence-aware segmentation takes more code, more edge cases, more testing. But the difference in output quality is the difference between a prototype and a product.