-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Support adaptive interruption for realtime models #6488
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
609a946
e006a24
2edc943
1e2edc4
dde9a0f
9ce3d6a
2b70881
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -72,6 +72,8 @@ class _EndOfTurnInfo: | |
| new_transcript: str | ||
| transcript_confidence: float | ||
| metrics: _EndOfTurnMetrics | ||
| backchannel_over_agent: bool = False | ||
| """The turn's speech overlapped agent speech and was classified a backchannel by adaptive interruption.""" | ||
|
|
||
|
|
||
| def _compute_end_of_turn_metrics( | ||
|
|
@@ -131,6 +133,7 @@ class _UserTurnTracker: | |
|
|
||
| class RecognitionHooks(Protocol): | ||
| def on_interruption(self, ev: inference.OverlappingSpeechEvent) -> None: ... | ||
| def on_backchannel_confirmed(self) -> None: ... | ||
| def on_start_of_speech(self, ev: vad.VADEvent | None, speech_start_time: float) -> None: ... | ||
| def on_vad_inference_done(self, ev: vad.VADEvent) -> None: ... | ||
| def on_end_of_speech(self, ev: vad.VADEvent | None) -> None: ... | ||
|
|
@@ -293,6 +296,9 @@ def __init__( | |
| self._interruption_enabled: bool = interruption_detection is not None and vad is not None | ||
| self._agent_speaking: bool = False | ||
| self._agent_speech_started_at: float | None = None | ||
| # turn-scoped backchannel-over-agent verdict from adaptive interruption, consumed and reset at end of turn | ||
| self._overlap_in_current_turn: bool = False | ||
| self._turn_backchannel_over_agent: bool = False | ||
|
|
||
| _backchannel_boundary: float | tuple[float, float] | None = ( | ||
| session.options.interruption.get("backchannel_boundary") | ||
|
|
@@ -473,7 +479,7 @@ def _on_end_of_agent_speech(self, *, ignore_user_transcript_until: float) -> Non | |
| if self._agent_speaking: | ||
| # no interruption is detected, end the inference (idempotent) | ||
| if not is_given(self._ignore_user_transcript_until): | ||
| self._on_end_of_overlap_speech(ended_at=time.time()) | ||
| self._on_end_of_overlap_speech(ended_at=time.time(), agent_ended=True) | ||
|
|
||
| end_cooldown: float = ( | ||
| self._backchannel_boundary[1] if self._backchannel_boundary else 0.0 | ||
|
|
@@ -509,8 +515,15 @@ def _on_start_of_speech( | |
| self._endpointing.on_start_of_speech( | ||
| started_at=started_at, overlapping=self._agent_speaking | ||
| ) | ||
| # every speech onset clears the prior backchannel verdict; an overlap re-derives it below | ||
| self._turn_backchannel_over_agent = False | ||
| if not self._agent_speaking: | ||
| self._overlap_in_current_turn = False | ||
|
|
||
| if not self._adaptive_interruption_active or not self._agent_speaking: | ||
| return | ||
| # overlap over agent speech started this turn; gates verdict acceptance below | ||
| self._overlap_in_current_turn = True | ||
|
longcw marked this conversation as resolved.
|
||
| self._interruption_ch.send_nowait( # type: ignore[union-attr] | ||
| _OverlapSpeechStartedSentinel( | ||
| speech_duration=speech_duration, | ||
|
|
@@ -538,8 +551,14 @@ def _on_end_of_overlap_speech( | |
| self, | ||
| ended_at: float, | ||
| user_speaking_span: trace.Span | None = None, | ||
| agent_ended: bool = False, | ||
| ) -> None: | ||
| """End interruption inference when agent is speaking and overlap speech ends.""" | ||
| """End interruption inference when agent is speaking and overlap speech ends. | ||
|
|
||
| agent_ended is True when the overlap is force-ended because the agent finished | ||
| speaking (the user may still be talking), in which case the synthesized verdict | ||
| is inconclusive and must not be treated as a confirmed backchannel. | ||
| """ | ||
| if not self._adaptive_interruption_active or not self._agent_speaking: | ||
| return | ||
|
|
||
|
|
@@ -555,7 +574,7 @@ def _on_end_of_overlap_speech( | |
| user_speaking_span.set_attribute(trace_types.ATTR_IS_INTERRUPTION, "false") | ||
|
|
||
| self._interruption_ch.send_nowait( # type: ignore[union-attr] | ||
| _OverlapSpeechEndedSentinel(ended_at=ended_at or time.time()) | ||
| _OverlapSpeechEndedSentinel(ended_at=ended_at or time.time(), agent_ended=agent_ended) | ||
| ) | ||
|
|
||
| @property | ||
|
|
@@ -1381,6 +1400,14 @@ async def _on_overlap_speech_event(self, ev: inference.OverlappingSpeechEvent) - | |
| ) | ||
| return | ||
|
|
||
| # only honor the verdict while this turn's overlap is unresolved so a late verdict | ||
| # can't leak into the next turn; an interruption supersedes a prior backchannel | ||
| if self._overlap_in_current_turn and not ev.agent_ended: | ||
| self._turn_backchannel_over_agent = not ev.is_interruption | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The overlap speech also ends when agent finishes speaking first, so we trigger a synthesized
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added a flag
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if there are one backchannel ev + one agent ends first ev during the agent speech,
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed. |
||
| # clear the backchannel audio, but only between segments — else we'd clip a real turn | ||
| if not ev.is_interruption and not self._speaking: | ||
| self._hooks.on_backchannel_confirmed() | ||
|
|
||
| if ev.is_interruption: | ||
| self._hooks.on_interruption(ev) | ||
|
|
||
|
|
@@ -1629,6 +1656,7 @@ async def _bounce_eou_task( | |
| new_transcript=self._audio_transcript, | ||
| transcript_confidence=confidence_avg, | ||
| metrics=metrics, | ||
| backchannel_over_agent=self._turn_backchannel_over_agent, | ||
| ) | ||
| ) | ||
| if committed: | ||
|
|
@@ -1677,6 +1705,9 @@ async def _bounce_eou_task( | |
| self._turn_detector_prediction_fut = None | ||
| self._turn_detector_flushed = True | ||
|
|
||
| # reset turn-scoped barge-in state once per logical turn (commit or drop) | ||
| self._turn_backchannel_over_agent = False | ||
| self._overlap_in_current_turn = False | ||
|
longcw marked this conversation as resolved.
|
||
| self._user_turn_committed = False | ||
|
|
||
| if self._end_of_turn_task is not None: | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.