BlogText-to-Speech

Thai has no full stop: where streaming speech should break

Thai ends a sentence with a space and no full stop, and a voice agent streaming text to a speech socket has to find that boundary before the voice can start.

· 7 min read

Thai writing ends a sentence with a space. The same space separates clauses inside the sentence, and the full stop is absent from ordinary prose. A voice agent that streams a reply to a speech socket as the language model writes it has to decide where one sentence ends and the next begins. The character it would wait for in English never arrives.

This guide is about that decision. It covers what the live socket does with a stream of Thai text, which boundaries a Thai reply carries, and where a break sounds wrong. It ends with a way to have the language model mark the breaks itself.

A space does the work of a full stop#

Written Thai uses one character for two jobs. A space closes a sentence, and a space also closes a clause inside a sentence. The comma appears in numbers and lists, the question mark appears in chat more than in print, and the full stop appears after an abbreviation and almost nowhere else. A reader tells a sentence break from a clause break by the words on either side of the space.

The spacing rules taught in school put the space after a complete sentence, before a conjunction such as แต่ or เพราะ, and around a number or a quotation. A list gets one between its items. A well-written paragraph is a chain of short spaced phrases, and the sentence boundary inside the chain carries no mark of its own.

This becomes a problem the moment text is spoken as it arrives. English streaming code waits for a period. Thai streaming code has nothing to wait for.

Why the boundary decides the latency#

A live voice agent sends text to the speech socket as the language model produces it, and the socket synthesizes in legs. A leg starts when the buffer holds something worth speaking. The listener hears the first leg after the model's first sentence plus one synthesis pass, and every boundary after that is a place where the voice takes a breath.

Two things go wrong at a bad boundary. A leg cut too early speaks half a clause, pauses, and speaks the rest with the pitch reset, which sounds like a hesitation. A leg cut too late waits for text the model is still writing, and the silence before the first word grows with every character the model adds.

The socket flushes on three triggers, which are a newline in the frame, sentence-ending punctuation at the end of the buffer, and a buffered-length threshold. English dialogue trips the second trigger at every full stop. Thai dialogue trips neither of the first two and rides the length threshold, which suits a paragraph and arrives late for a reply of one sentence. The API reference documents the three triggers and the flush frame that forces a leg by hand.

Where a Thai sentence ends in dialogue#

Spoken Thai marks the end of an utterance more clearly than written Thai does. A reply from an assistant ends on a final particle far more often than a paragraph of prose does. ครับ and ค่ะ close a polite statement, คะ and ครับ close a polite question, นะ and สิ and เถอะ soften or urge, and ล่ะ and ไหม and มั้ย ask. A particle followed by a space is the strongest boundary signal a Thai stream carries.

  • A final particle followed by a space: ค่ะ ครับ คะ นะคะ นะครับ สิ เถอะ ล่ะ ไหม มั้ย หรือเปล่า. In a polite reply this closes nearly every sentence.
  • A newline. A language model writes one when asked to, and the socket flushes on it without any client logic.
  • A space before a sentence-opening word such as แต่ แล้ว ส่วน ถ้า เพราะ ดังนั้น. This is a clause boundary at least, and a safe pause on a long reply.
  • A closing quotation mark or bracket followed by a space.

Particles carry the register of the reply, and the politeness post explains why a formal assistant ends nearly every sentence on one. That habit is what makes the particle rule reliable. An assistant instructed to write polite Thai produces its own boundaries.

Where a break sounds wrong#

A space is a pause, and the script guide covers how a script uses one. A leg boundary is a longer pause with a pitch reset, and there are places a speaker would never put one.

  • Before a final particle. A flush that lands before ค่ะ leaves the particle as its own leg, spoken alone after a gap.
  • Inside a number, an amount, or a time. ๒,๕๐๐ บาท and 14:30 น. are one spoken unit.
  • Between a title and a name, as in คุณ สมชาย, or between a name and the particle that follows it.
  • After a conjunction. A leg that ends on แต่ hangs in the air until the next leg arrives.
  • Between a verb and its object across a clause space, where a reader runs straight on.

A short reply is one leg. Splitting a twelve-word answer into three legs costs nothing extra in credits and gains nothing in latency, because the first leg already started the voice.

Ask the model to write the breaks#

The cheapest boundary detector is the language model itself. Instruct it to end every sentence with a newline and to keep each line to one or two clauses. The socket flushes on the newline, each leg is one sentence, and the client carries no Thai-specific logic at all.

The client still sends a flush frame at the end of the turn, because the last sentence can end without a newline when the model stops. Flushing an empty buffer is a safe no-op, and the flushed event confirms that the buffer drained.

A model that ignores the instruction on a long answer still hits the length threshold, and the particle rule above is the fallback for a client that wants to flush sooner. Check the particle, then the space after it, and never the particle alone. ค่ะ in the middle of a quoted line is a particle inside speech, and the space is what says the sentence closed.

What an interruption costs#

A speaker talks over the reply. The client sends cancel. The leg being spoken stops, and its charge stays, because that text was synthesized before the interruption arrived. The buffered text that had not started was never charged, and it is discarded. The connection stays open, and the next text frame begins a new leg. The live guide walks through the exchange.

Sending one sentence per leg bounds the cost of an interruption to the sentence being spoken. Credits follow characters however many legs carry them, and the 0.1 credit minimum applies once per connection. Each leg also counts as one request against the plan's rate limit, and a client that flushes after every word spends that limit without gaining anything a listener can hear.

A turn, frame by frame#

The trace below is one assistant turn. The first two sentences end on a newline the model wrote, and the socket flushes each one as it lands. The third sentence arrives without a newline, and the client's flush at the end of the turn closes it.

one turn on the live socket
client  text     ได้เลยค่ะ\n
server  charged  leg 1
server  audio    ...
client  text     ร้านเปิดเก้าโมงเช้า
client  text     ถึงสองทุ่มค่ะ\n
server  charged  leg 2
server  audio    ...
client  text     วันอาทิตย์ปิดนะคะ
client  flush
server  charged  leg 3
server  audio    ...
server  flushed

Each charged event is the receipt for one leg. The listener hears ได้เลยค่ะ while the model is still writing the opening hours, which is the whole point of streaming a reply.

Next steps#