Two topics
Frames are separated by topic, and the split is not cosmetic — the two carry different kinds of truth and are read with different rules.glot.transcript
What is being said. Live chunks of each translation, then the confirmed utterance in both languages. This is what you render as captions.
glot.system
What the translator is doing. Dialling its model, retrying it, losing it. This is what keeps a stalled translation from looking like silence.
v field so you can recognize a frame you cannot read.
Transcript frames
The model streams a translation before it confirms it, so an utterance arrives in two forms. Both carry asegment id, which is what ties them together.
- partial
- segment
One chunk of the translation as it is produced, for liveness only.
JSON
string
The chunk verbatim — a delta, not a running total. Accumulate deltas per
segment for the typing effect.boolean
Marks the partial that ended the utterance. It means “this stopped growing”, not “this is complete” — it exists so you can stop a cursor even when the
segment frame never arrives.integer
required
Frame-shape version, currently
1.string
required
Groups the partials of one utterance with the
segment frame that supersedes them.string
required
ISO 639-1 code of the language this text was translated into.
System messages
The translator runs on a model it reaches over the network, and when that model is slow to answer or drops, the translated audio simply stops — which from your user’s side is indistinguishable from nobody talking. These messages are how you tell those two apart.JSON
model.reconnecting is the only kind carrying extra fields:
integer
How many times in a row the connection has failed. It resets once a connection survives long enough to be considered healthy, so it says “how bad is this right now”, not “how many times has this call reconnected”.
number
Seconds until the next attempt. Relative, never a timestamp — your clock and the translator’s do not agree — so count down from the delta.
integer
required
Envelope version, currently
1.string
required
Dot-namespaced as
subject.happening. The list above will grow.string
required
info, warning, or error — so a kind you have never heard of is still rankable.Short interruptions are deliberately not announced. A connection that was healthy and comes straight back costs the room about a second, and firing
model.reconnecting for that would flash a warning nobody needed. Expect messages for outages worth reporting, not for every blip.Versions and unknown kinds
The translator and your client deploy independently, so both topics carryv — but what v covers differs, and so does what you do with a kind you don’t recognize.
That asymmetry is the one thing to carry away from this page. A system parser that only admits the kinds it knows today will silently swallow every message added after your last release; a transcript parser that admits an unknown kind has nothing sensible to do with it.
Reading frames off a topic
Register handlers before you connect, so nothing published in the first moments of the session arrives without one. The quick start’s Reading frames off the topic shows the transcript handler in full — the system topic is registered on the sameRoom, the same way.
Reads are chained rather than run concurrently. A text-stream handler returns void, so the SDK does not await it — two overlapping readAll() promises can settle out of order and interleave one utterance’s chunks into another’s.
What is specific to having two topics is that each needs its own chain. Sharing the transcript’s would queue “the model is reconnecting” behind whatever captions are already in flight, and that is precisely the moment the message matters.
TypeScript
A
Room throws if you register twice for the same topic — but glot.transcript and glot.system are different topics, so both handlers sit happily on one Room. Register each once, before room.connect().Parsing
Parsing runs on network input inside a stream handler, where a throw takes out your read chain rather than surfacing anywhere useful. Both parsers below are total: they returnnull for anything they cannot read, and never raise.
- Transcript frames
- System messages
Treat malformed JSON, a non-object, an unknown
kind, and a future v all as “not a frame I can read”.TypeScript
Folding into state
The two topics fold differently, and the difference follows from what they describe. A transcript is a growing list of utterances; the system state is one connection, so its messages collapse onto each other.Transcript: extend, then replace
A partial extends its utterance, a segment replaces it. Appending a segment’s text would duplicate the whole utterance, because the segment carries what the partials already spelled out.TypeScript
An utterance interrupted by a model reconnect stops without ever being confirmed, so it stays
stopped forever. A UI that treated stopped as done would claim text was confirmed when it never was.
System: collapse onto the newest
These messages describe where one connection is, so a secondmodel.reconnecting replaces the first. They arrive in order, so the newest wins outright.
TypeScript
Messages tell you what happened, attributes tell you what is true
This is the part to get right, and it is why aglot.system handler alone is not enough.
A text stream is not replayed to a participant who joins after it was published, and the two terminal messages — model.unavailable and model.lost — are followed immediately by the translator leaving the room, so they can lose the race and never arrive at all.
The translator therefore also keeps its current state on its participant attribute status, which is replayed to late joiners and is set before it leaves:
So: drive your UI from the messages, and use
status === "error" as the authority that overrides them. Without that fallback a client can sit on “reconnecting…” forever for a translator that has already given up and left.
TypeScript
room.connect() resolves, for the case where you joined a session already in progress.
Showing the translator’s status
Captions have their own guidance in Rendering captions. For the system state:- Say nothing when
modelisreadyornull. A working translator needs no commentary, and a badge that is always present stops being read. - Pair it with, not instead of, your connection status. They fail independently: your WebRTC connection can be perfectly healthy while the translator has lost its model, and “Connected” alone would then claim translation is working.
- Render the countdown only while there is one.
retryInSecondscan be absent or effectively zero, and “retrying in 0s” reads as broken. Round up — a countdown showing1sfor 1.4 seconds finishes before the retry does. - Announce it with
aria-live="polite". This narrates something the user cannot see happening — audio that has gone quiet — so a screen reader gets no signal otherwise.
Pitfalls
Captions duplicate themselves
Captions duplicate themselves
You are appending
segment text to the accumulated partials. A segment replaces, it never extends.Chunks from two utterances interleave
Chunks from two utterances interleave
Your reads run concurrently. Chain them per topic, as shown above.
An utterance is stuck mid-sentence and never confirms
An utterance is stuck mid-sentence and never confirms
The model reconnected. That starts a new session, which never receives the rest of the utterance — so its caption stays at
stopped and no segment frame is coming. That is what stopped is for; don’t render it as confirmed. A model.reconnecting message is how you know this is what happened.The UI is stuck on 'reconnecting' and never recovers
The UI is stuck on 'reconnecting' and never recovers
You are driving it from
glot.system alone. model.lost is published as the translator shuts down and can lose that race, so it is not guaranteed to arrive. Read the translator’s status attribute as well and treat error as authoritative — see messages and attributes.Translation paused briefly but no message explained it
Translation paused briefly but no message explained it
Expected. A connection that drops and comes straight back is not announced, because flashing a warning for a one-second gap is worse than staying quiet. Messages cover outages worth reporting, not every interruption.
A message we added never reaches your handler
A message we added never reaches your handler
Your parser is rejecting unknown
kinds. New system kinds ship without a v bump, by design — keep any frame whose envelope is valid and ignore the ones your UI has no use for. See versions and unknown kinds.A new session opens showing the last call's state
A new session opens showing the last call's state
Clear both the transcript and the system state when the token changes. A fresh token is a fresh session, and state that outlives the connection won’t reset itself.
See it working
Translation Playground, in the Glot dashboard, renders both topics side by side — watch partial and segment frames land in the live transcript, and the translator’s status change beside them.