Transcripts
transcript (what was spoken) vs model-text (what the model emitted alongside) — which to subscribe to for which UI.
The server emits two distinct text channels. They are different things and should not be merged.
What it is
transcript (RealtimeTranscriptDelta) — a streaming transcription of audio. For the user side, this is speech-to-text from the mic. For the assistant side, this is the transcription of the model's audio output — the words the listener actually heard. Use this to render a "what was said" conversation bubble.
model-text (RealtimeModelText) — the model's text-channel output. In audio sessions, the model may emit function-call narration here (e.g. start_analysis({...})) or written-style text that it did NOT speak aloud. In text-only sessions (audio.output: false), this carries the model's full response. Use this for debug surfaces, text-mode sessions, or logs.
The key rule
For a "what was spoken" transcript UI, subscribe to transcript only. Do not include model-text.
For a text-mode (no audio) session, subscribe to model-text only — assistant transcript events will not fire.
transcript event fields
| Field | Type | Meaning |
|---|---|---|
role | "USER" | "ASSISTANT" | Who spoke |
text | string | Streaming: the new fragment since the previous event. Final: the cumulative full transcript for the turn |
is_final | bool | false = streaming delta (append it); true = terminating event (replace what you accumulated with text) |
In the TypeScript SDK the normalized TranscriptEvent has:
| Field | Type | Meaning |
|---|---|---|
id | string | ${turnId}-${seq} — stable per delta |
turnId | string | Shared across all deltas in one turn; resets on turn-complete |
role | 'user' | 'assistant' | Normalized from the wire "USER" / "ASSISTANT" |
text | string | Fragment text |
isFinal | bool | Whether this is the terminating event for the turn |
append | bool | true if this delta should be appended to the last bubble for this turn; false for a new bubble |
Code
TypeScript — via session.on():
const unsub = session.on('transcript', (event) => {
console.log(`[${event.role}] ${event.text} (final=${event.isFinal})`);
});
// model-text for debug:
session.on('model_text', (event) => {
console.log(`[model-text] ${event.text}`);
});React apps can use the useTranscript() hook instead, which folds deltas into ready-to-render items:
import { useTranscript } from 'cosmo-ai';
function Transcript() {
const items = useTranscript();
return (
<ul>
{items.map(item => (
<li key={item.id} style={{ opacity: item.isFinal ? 1 : 0.6 }}>
<strong>{item.role}</strong>: {item.text}
</li>
))}
</ul>
);
}Python — match on the event class in the session stream:
from cosmo_ai import RealtimeModelText, RealtimeTranscriptDelta
async with agent.start() as session:
async for event in session:
if isinstance(event, RealtimeTranscriptDelta):
role = event.role.value # "USER" or "ASSISTANT"
marker = "»" if event.is_final else "…"
print(f"[{role}] {event.text}{marker}")
elif isinstance(event, RealtimeModelText):
print(f"[model-text] {event.text}")Swift — switch on the event stream:
for try await event in session.events {
switch event {
case .transcript(let delta):
let role = delta.role == .user ? "user" : "assistant"
let marker = delta.isFinal ? " »" : "…"
print("[\(role)]\(marker) \(delta.text)")
case .modelText(let text):
print("[model-text] \(text.text)")
default:
break
}
}is_final semantics
The wire contract applies to both roles:
- Streaming events (
is_final: false) —textis the new fragment since the previous event for that role's turn. Append. - Terminating event (
is_final: true) —textis the cumulative full transcript for the turn. Replace whatever you accumulated with this value.
After the terminating event, turn-complete arrives for that role.
current = ""
async for event in session:
if isinstance(event, RealtimeTranscriptDelta):
if event.is_final:
current = event.text # replace: full turn transcript
print("Final:", current)
else:
current += event.text # append: streaming previewPitfalls
- Do not merge
model-textinto the conversation transcript. In audio sessions it contains function-call narration that was never spoken, and mixing it produces confusing transcripts. - Deltas for the same turn arrive in order, but do not assume each delta is a full sentence. Append streaming fragments and let the
is_finalevent replace the accumulation. is_final: truemeans "this turn's transcription is complete", not "the assistant turn is over" — audio can still be playing out.turn-completesignals the turn boundary.