Repository: livekit/agents
File: livekit-agents/livekit/agents/voice/agent_activity.py (Python)

Code before:
        self._user_silence_event.clear()

        if self._false_interruption_timer:
            # cancel the timer when user starts speaking but leave the paused state unchanged
            self._false_interruption_timer.cancel()
            self._false_interruption_timer = None

    def on_end_of_speech(self, ev: vad.VADEvent | None) -> None:
        speech_end_time = time.time()
        if ev:
            speech_end_time = speech_end_time - ev.silence_duration
        self._session._update_user_state(
            "listening",
            last_speaking_time=speech_end_time,
        )
        self._user_silence_event.set()

        if (
            self._paused_speech
            and (timeout := self._session.options.false_interruption_timeout) is not None
            and (
                # Since some STT vendors (e.g. Deepgram) will send interim and final transcripts before
                # sending the end of speech event, we need to check if:
                # 1. The resume timer has not been scheduled yet.
                # 2. The transcript is not long enough for interruption.
                self._false_interruption_timer is None
                and (
                    self._audio_recognition is None
                    or (
                        self._session.options.min_interruption_words > 0
                        and len(
                            split_words(
                                self._audio_recognition.current_transcript, split_character=True
                            )
                        )
                        < self._session.options.min_interruption_words
                    )
                )
            )
        ):
            # schedule a resume timer when user stops speaking
            self._start_false_interruption_timer(timeout)

    def on_vad_inference_done(self, ev: vad.VADEvent) -> None:
        if self._turn_detection in ("manual", "realtime_llm"):
            # ignore vad inference done event if turn_detection is manual or realtime_llm
            return

        if ev.speech_duration >= self._session.options.min_interruption_duration:
            self._interrupt_by_audio_activity()


Review comment (suggestion):
`self._start_false_interruption_timer(timeout)` will cancel the existing timer and since these are synced methods, there should always be only one timer at a time.

if the EOU is fired right after the final transcript, before the false interruption timeout, it shouldn't call the `_on_false_interruption` twice. 

Code after:
        self._session._update_user_state("speaking", last_speaking_time=speech_start_time)
        self._user_silence_event.clear()
        self._stt_eos_received = False

        if self._false_interruption_timer:
            # cancel the timer when user starts speaking but leave the paused state unchanged
            self._false_interruption_timer.cancel()
            self._false_interruption_timer = None

    def on_end_of_speech(self, ev: vad.VADEvent | None) -> None:
        speech_end_time = time.time()
        if ev:
            speech_end_time = speech_end_time - ev.silence_duration
        else:
            self._stt_eos_received = True

        self._session._update_user_state(
            "listening",
            last_speaking_time=speech_end_time,
        )
        self._user_silence_event.set()

        if (
            self._paused_speech
            and (timeout := self._session.options.false_interruption_timeout) is not None
        ):
            # schedule a resume timer when user stops speaking
            self._start_false_interruption_timer(timeout)

    def on_vad_inference_done(self, ev: vad.VADEvent) -> None:
        if self._turn_detection in ("manual", "realtime_llm"):
            # ignore vad inference done event if turn_detection is manual or realtime_llm
            return

        active_speech = ev.speech_duration >= self._session.options.min_interruption_duration
        if active_speech and (
            self._turn_detection != "stt"
            or not self._stt_eos_received
            or ev.raw_accumulated_silence == 0
        ):
            # STT may send EOS before VAD EOS, we only interrupt if:
            # 1. turn detection is not STT; or
            # 2. STT EOS hasn't been received yet; or
            # 3. VAD speech is still ongoing
            self._interrupt_by_audio_activity()

        if (
            ev.speaking
            # allow some silence between utterances during active speech
            and ev.raw_accumulated_silence <= self._session.options.min_endpointing_delay / 2
        ):