powerful-magician-63563
12/29/2025, 9:10 AMtall-airline-16756
12/29/2025, 9:10 AMtall-airline-16756
12/29/2025, 9:10 AMuser_input_transcribed) that include an is_final flag. When transcript.is_final is true, that segment is finalized and safe to persist to your database; when false it’s interim and may be updated.
Would you like a short code example for the language/runtime you’re using (Python, Node, or another)?
Sources: Pipeline nodes and hooks | LiveKit docs | Transcriber | LiveKit docs | TTS Translator with Gladia STT | LiveKit docspowerful-magician-63563
12/29/2025, 10:28 AMtall-airline-16756
12/29/2025, 10:28 AMuser_input_transcribed events (what part of that would you like a code example for: Python or Node.js?).
Key recommendations
• Listen to the user_input_transcribed event on the AgentSession.
• Use the is_final flag to decide when to persist text (only save when true).
• Include timestamps/metadata from the event for ordering and playback sync.
• Buffer interim segments if you need continuous live display, but replace with final text when is_final arrives.
• Emit or store a single canonical record per final segment (e.g., transcript text, start/end timestamps, speaker id) to simplify downstream queries.
Minimal examples
• Python:
@session.on("user_input_transcribed")
def on_transcript(event):
print(event.transcript, event.is_final)
if event.is_final:
save_to_db({
"text": event.transcript,
"start": event.start_time,
"end": event.end_time,
"speaker": event.speaker_id,
})
• Node.js/TypeScript:
session.on('user_input_transcribed', (event) => {
console.log(event.transcript, event.isFinal);
if (event.isFinal) {
saveToDb({
text: event.transcript,
start: event.startTime,
end: event.endTime,
speaker: event.speakerId,
});
}
});
Questions to help tailor this
• Which runtime do you want an expanded example for (Python or Node)?
• Do you need handling for multi-speaker diarization or streaming display of interim text? Sources: Transcriber | LiveKit docs | Pipeline nodes and hooks | LiveKit docspowerful-magician-63563
12/29/2025, 10:35 AMtall-airline-16756
12/29/2025, 10:36 AMuser_input_transcribed events (with is_final) are emitted on the agent session — that’s the canonical place to decide when a segment is final and safe to persist.
• From the agent you can:
◦ Save final transcripts to your database directly when is_final is true.
◦ Attach metadata (timestamps, speaker id) and produce a single canonical record per final segment.
◦ Optionally buffer interim segments if you need them for live display, but replace them with the final text when is_final arrives.
• Forwarding to the Swift frontend:
◦ If the frontend needs live/interim text or to display finalized transcripts, send events from your Python agent to the Swift client (e.g., via your signaling channel, WebSocket, or LiveKit data messages).
◦ For UI simplicity, send interim updates for live display and send the finalized transcript (or an “update” message) when is_final is true so the client can replace the interim text and/or mark it persisted.
Would you like a short Python snippet showing handling + forwarding, and a matching Swift example for receiving/displaying interim + final messages?powerful-magician-63563
12/29/2025, 10:56 AMtall-airline-16756
12/29/2025, 10:56 AMuser_input_transcribed or transcription_node in agent.py.
b. Run analysis synchronously or in a short async task.
c. Publish results to frontend via data messages or a text stream topic (e.g., lk.analysis).
• External analyzer (best for heavy/complex work)
a. Agent captures final transcripts (is_final) and POSTs them to analysis service with segment_id, speaker, timestamps.
b. Analysis service processes and returns results.
c. Send results back to Swift via LiveKit data messages / custom text stream or have Swift subscribe to a WebSocket/REST callback endpoint.
d. Keep result messages idempotent and include segment_id for reconciliation.
• Hybrid (recommended for many apps)
◦ Stream interim text to client from agent for UI responsiveness.
◦ For final segments: agent both persists and forwards to external analyzer; when analyzer completes, agent or analyzer publishes analysis results to frontend topic lk.analysis.
Practical tips
• Always include segment_id, start/end timestamps, and speaker_id in messages for mapping results to UI.
• Use is_final to decide when to persist/forward.
• Send interim updates to UI but replace with final text on is_final.
• If external analyzer will callback the client directly, design auth and deduping; otherwise publish analyzer results via LiveKit data messages so the agent maintains a single path to clients.
If you want, I can:
• Provide a concise Python agent snippet that forwards final segments to an external analyzer and publishes analysis results via a text stream.
• Provide the matching Swift handler example for lk.analysis.
Which of those would you like first?
Sources: Gemini Live API plugin | LiveKit docs | Text and transcriptions | LiveKit docs | Pipeline nodes and hooks | LiveKit docspowerful-magician-63563
12/29/2025, 12:39 PMtall-airline-16756
12/29/2025, 12:39 PMtranscript_handler.py) and import it into agent.py. This keeps the agent responsive while keeping code modular and testable.
Why this pattern
• The agent receives user_input_transcribed events and has the canonical `is_final`/segment metadata, so it's the right place to decide how fragments map to a final segment.
• Putting logic in a separate handler file preserves separation of concerns (agent orchestration vs. transcript processing) and makes it easy to reuse, test, or move to an external service later.
• Doing merging client-side (Swift) is possible but less reliable because the agent is the authority for timestamps, speaker IDs, and session context.
Recommended approach (high level)
• Capture incoming events in agent.py (user_input_transcribed or transcription_node).
• Forward each event to functions in transcript_handler.py that:
◦ Buffer fragments per speaker/segment_id.
◦ Use heuristics to merge fragments (time gap threshold, lack of overlap, punctuation, confidence/score if available).
◦ Emit a merged final segment when either:
▪︎ is_final true for a fragment that completes a buffered sequence, or
▪︎ a configured inactivity timeout elapses (e.g., 300–800 ms) after the last fragment.
• When a merged final is ready, persist/forward it and include merged_segment_id, start/end timestamps, and original fragment IDs.
Minimal design notes
• Key fields to track: speaker_id, segment_id (if provided), start_time, end_time, received_time.
• Heuristics:
◦ Merge if consecutive fragments from same speaker arrive within a short gap (configurable).
◦ Prefer is_final as authoritative — if any fragment in a merge is final, mark merged as final.
◦ Use punctuation or capitalization if present to help boundary detection.
• Concurrency: keep per-room and per-speaker buffers; clear buffers on finalization or when room ends.
• Idempotency: include original fragment IDs in merged record so duplicates are detectable.
Example outline (conceptual)
• src/
◦ agent.py # registers event handlers and call into handler
◦ transcript_handler.py # buffering, merging logic, timeouts, emit function
agent.py:
from transcript_handler import TranscriptMerger
merger = TranscriptMerger(on_merged=publish_merged)
@session.on("user_input_transcribed")
def on_transcript(event):
merger.add_fragment(event)
transcript_handler.py (conceptual):
class TranscriptMerger:
def __init__(self, on_merged, gap_threshold_ms=500):
self.buffers = {} # keyed by (room, speaker)
self.on_merged = on_merged
self.gap = gap_threshold_ms
def add_fragment(self, event):
key = (event.room_id, event.speaker_id)
buf = self.buffers.setdefault(key, [])
buf.append(event)
# reset or start timer for inactivity-based flush
if event.is_final:
self.flush(key)
def flush(self, key):
merged = merge_fragments(self.buffers.pop(key, []))
if merged:
self.on_merged(merged)
What to emit to frontend
• Send merged transcripts via a text stream or data message (topic like lk.transcript.merged) including:
◦ text, merged_segment_id, original_fragment_ids, start, end, speaker_id, is_final.
If you want, I can:
• Provide a concrete Python implementation of TranscriptMerger with a timeout-based flush and simple heuristics.
• Provide the Swift handler example for receiving merged transcripts. Which would you like first?powerful-magician-63563
12/29/2025, 1:59 PMtall-airline-16756
12/29/2025, 2:00 PMis_final (or lk.transcription_final) to know when a segment is authoritative and replace any interim for that segment. Track per-speaker/per-segment buffers and sort by timestamps or segment_id when assembling the timeline.
What to use (concrete signals)
• segment_id (or lk.segment_id on text streams) — group interim/final updates for the same utterance.
• finality flag (is_final in agent events, lk.transcription_final on text streams) — treat final as authoritative.
• start/end timestamps (or timed tokens / TimedString when available) — order segments precisely and merge fragments.
• speaker_id / track_id — separate streams per speaker so you don’t intermix turns.
Recommended implementation patterns
• Agent-side canonical ordering:
◦ Capture user_input_transcribed events in the agent (or override transcription_node for richer timing).
◦ Buffer fragments keyed by (room, speaker, segment_id).
◦ On each fragment: update buffer, reset inactivity timer for that key.
◦ If is_final arrives, flush that segment as final (replace any interim).
◦ If inactivity timeout elapses for the buffer, flush as final candidate (optionally mark as provisional).
◦ Persist and/or forward merged final with segment_id, start, end, speaker_id, and original fragment IDs.
• Frontend-side ordering (Swift):
◦ Register a text stream handler for lk.transcription and lk.analysis (if using).
◦ Use the reader.info.attributes (lk.transcription_final, lk.segment_id, etc.) to group messages.
◦ Keep a per-segment buffer of interim messages; when final arrives, swap in final and render/persist.
◦ If messages arrive out of order, use timestamps or segment_id sequence to reorder before display.
Example flow (conceptual)
1. Agent emits interim updates with same segment_id.
2. Swift receives interim updates and displays them (buffered).
3. Agent emits final update with lk.transcription_final=true for that segment_id.
4. Swift replaces interim with final and marks persisted/complete.
Practical ordering rules
• Always group by segment_id first.
• Within a segment, use server-provided timestamps (`start_time`/`end_time` or TimedString) to order tokens if needed.
• If segment_id is missing in a context, fall back to (speaker_id + monotonic sequence or arrival timestamp) but prefer segment_id when available.
• Include received_time when you store events so you can detect reordering and reconcile later.
Edge cases & mitigations
• Duplicate or delayed final arrives after another segment started: use timestamps and segment_id to place it correctly; if it overlaps, consider merging or flagging overlap for manual review.
• Lost final: set a conservative inactivity timeout (e.g., 300–800 ms) to flush buffered fragments as a candidate final.
• Multi-speaker turns: maintain per-speaker buffers to avoid cross-speaker concatenation.
Agent vs frontend responsibility
• Agent is the canonical source of truth for finality, timestamps, and segment grouping. Do ordering/merging in the agent if you need guaranteed, server-side canonical transcripts (recommended for persistence and analysis).
• Frontend should implement buffering and replacement logic so UX remains smooth despite network reordering.
• Best practice: perform canonical merging/persistence in the agent, and stream interim + final to the frontend. This gives you a single authoritative record and a responsive UI.
If you’d like, I can:
• Provide a compact Python TranscriptMerger implementation that buffers by (room, speaker, segment_id), uses a timeout, and flushes on is_final.
• Provide the Swift handler code that registers a text stream handler, buffers interim messages by segment_id, and applies final replacements.
Which snippet would you like first?
Sources: Text and transcriptions | LiveKit docs