enough-dentist-75383
12/17/2025, 5:08 PMtall-airline-16756
12/17/2025, 5:08 PMtall-airline-16756
12/17/2025, 5:09 PMspeech_created and check the SpeechHandle state to see if the speech was canceled/aborted, then ignore or mark its transcript accordingly.
• Use lifecycle hooks: use on_user_turn_completed to distinguish completed turns from canceled ones and suppress transcripts for canceled turns.
• Filter in the pipeline: add logic in your transcription_node to suppress or flag transcripts that originate from interrupted speech (for example, drop transcripts when the associated SpeechHandle has an interrupted/canceled flag).
Which SDK/language are you using and can you share the event payload you’re receiving for the interrupted speech? I’ll give exact code snippets for detecting and filtering those transcripts.
Sources: Text and transcriptions | LiveKit docs | examples/voice_agents | livekit/livekit_composite | DeepWiki | Pipeline nodes and hooks | LiveKit docs | Agent speech and audio | LiveKit docsenough-dentist-75383
12/17/2025, 5:10 PMtall-airline-16756
12/17/2025, 5:10 PMtranscription_node, or a custom pipeline?
3. Share example event payloads or logs you see when the bot transcript appears early (the speech_created, speech_started, speech_stopped, or transcription events and any SpeechHandle fields).
4. Do you see a separate event for TTS start/stop, or only the transcript event?
5. Is the bot transcript marked as partial or final when it arrives?
With those I’ll provide exact code snippets and where to filter (client-side listener or in transcription_node) to ensure bot transcripts are only forwarded once speech actually starts or to suppress partial transcripts from interrupted/canceled TTS.enough-dentist-75383
12/17/2025, 5:13 PM${lastText} ${newText},
};
return updatedTranscripts;
}
// Create new message block (AI message or first human message after AI)
const newTranscripts = [...prev, message];
// Auto-show transcript when first message arrives
if (!transcriptShownRef.current && prev.length === 0 && !isMobile()) {
transcriptShownRef.current = true;
setShowTranscript(true);
}
return newTranscripts;
});
},
onInterimUpdate: (text, speaker) => {
setCurrentTranscript(text);
setCurrentSpeaker(text ? speaker : null);
},
onSpeakerChange: (speaker) => {
setCurrentSpeaker(speaker);
},
}
);
}
return () => {
// Cleanup on unmount or orchestration type change
if (transcriptManagerRef.current) {
transcriptManagerRef.current.cleanup();
transcriptManagerRef.current = null;
}
};
}, [orchestrationType]);tall-airline-16756
12/17/2025, 5:13 PMspeech_started or a SpeechHandle state (started vs created), only push the AI final transcript when you see speech start. In your onFinalMessage guard, require a flag like message.speechStarted === true (or check the SpeechHandle for started) before adding an AI message.
2) Suppress transcripts from created-but-cancelled TTS
• If you get transcripts from text that was sent to TTS but later interrupted, ignore messages where message.speechCancelled === true or where SpeechHandle indicates aborted. Add an early-return in `onFinalMessage`:
if (message.isAI && (message.speechCancelled || !message.speechStarted)) return;
3) Delay display until speech actually begins (UI-friendly)
• If events for start/stop exist, buffer the AI transcript and only commit when speech_started arrives. Example approach:
◦ onFinalMessage: if isAI, store in pendingAI.
◦ onSpeechStarted: if pendingAI exists for that speaker, commit it and clear pendingAI.
◦ onSpeechStopped/cancel: clear pendingAI.
4) Use interim streaming for humans, final-only for bots
• Your onInterimUpdate is already updating live text for humans. For bots, ignore interim transcripts entirely and only display final text after speech start.
Quick code hint to integrate into your existing handler:
// inside onFinalMessage
if (message.isAI) {
// require explicit speechStarted flag, and skip duplicates as you already do
if (!message.speechStarted) {
// optionally store pending message keyed by speaker id
pendingAIRef.current[message.speaker] = message;
return;
}
// commit as before
}
Next step: can you share one example event payload for the bot that arrives early (fields on message, plus any SpeechHandle fields) and tell me which SDK/version you’re using? I’ll give the exact snippet to plug into your TranscriptManager.enough-dentist-75383
12/17/2025, 5:15 PMtall-airline-16756
12/17/2025, 5:15 PMspeech_created, speech_started, speech_stopped (or fields on the message like speechStarted, speechCancelled, or a SpeechHandle)?
2. Please paste one example payload for the premature bot transcript event you’re getting (the full message object you receive).
If you confirm you receive speech_started, I’ll give a minimal SDK v2+ patch to:
• buffer AI final messages until speech_started for that speaker, then commit; and
• drop buffered AI messages if `speech_stopped`/cancel arrives before start.enough-dentist-75383
12/17/2025, 5:16 PMenough-dentist-75383
12/17/2025, 5:16 PMtall-airline-16756
12/17/2025, 5:16 PMspeech_started does arrive, here's a concise plan and exact things to do so you can suppress the bot transcript until the TTS actually starts.
1) Quickly log incoming events to confirm fields
• Add a debug logger where you receive transcript/agent events:
console.log('agent-event', { eventType: event.type, payload: event });
• Look for speech_created, speech_started, speech_stopped or fields like speechStarted, speechCancelled, or a SpeechHandle id.
2) Minimal approach: buffer AI final messages until speech_started
• Maintain a pending map keyed by speaker or `speechHandleId`:
const pendingAI = new Map();
• Update your TranscriptManager handlers:
onFinalMessage:
if (message.isAI) {
// If speech not started yet, buffer
if (!message.speechStarted) {
pendingAI.set(message.speechHandleId || message.speaker, message);
return;
}
// commit immediately if speechStarted
commitMessage(message);
return;
}
commitMessage(message); // human messages as before
onSpeechStarted (handler triggered when speech_started event arrives):
const key = event.speechHandleId || event.speaker;
const pending = pendingAI.get(key);
if (pending) {
commitMessage(pending);
pendingAI.delete(key);
}
onSpeechStopped / onSpeechCancelled:
const key = event.speechHandleId || event.speaker;
if (pendingAI.has(key)) pendingAI.delete(key); // drop buffered AI if cancelled
3) commitMessage helper (reuse your existing dedupe/concat logic)
• Use the same logic you already have in onFinalMessage to add the message to state (dedupe, concat with last human, etc.). That keeps behavior consistent.
4) If your events only contain a generic event.type === 'speech_started' without IDs
• Use speaker as the key (assuming one concurrent TTS per speaker).
• If multiple concurrent TTS from same speaker are possible, prefer speechHandleId (or whatever unique id the event includes).
If you want, paste one logged event object from your console (the speech_created or speech_started payload) and I’ll return the precise 6–10 line patch you can drop into your TranscriptManager.