Files
2026-07-13 13:39:38 +08:00

584 lines
25 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import pytest
from livekit.agents.utils.exp_filter import ExpFilter
from livekit.agents.voice.endpointing import DynamicEndpointing, create_endpointing
pytestmark = pytest.mark.unit
class TestExponentialMovingAverage:
"""Test cases for the ExponentialMovingAverage class."""
def test_initialization_with_valid_alpha(self) -> None:
"""Test that EMA initializes correctly with valid alpha values."""
ema = ExpFilter(alpha=0.5)
assert ema.value is None
ema_with_initial = ExpFilter(alpha=0.5, initial=10.0)
assert ema_with_initial.value == 10.0
ema = ExpFilter(alpha=1.0)
assert ema.value is None
def test_initialization_with_invalid_alpha(self) -> None:
"""Test with invalid alpha values."""
with pytest.raises(ValueError, match="alpha must be in"):
ExpFilter(alpha=0.0)
with pytest.raises(ValueError, match="alpha must be in"):
ExpFilter(alpha=-0.5)
with pytest.raises(ValueError, match="alpha must be in"):
ExpFilter(alpha=1.5)
def test_update_with_no_initial_value(self) -> None:
"""Test that first update sets the value directly."""
ema = ExpFilter(alpha=0.5)
result = ema.apply(1.0, 10.0)
assert result == 10.0
assert ema.value == 10.0
def test_update_with_initial_value(self) -> None:
"""Test that update applies EMA formula when initial value exists."""
ema = ExpFilter(alpha=0.5, initial=10.0)
result = ema.apply(1.0, 20.0) # 0.5 * 20 + 0.5 * 10 = 10 + 5 = 15
assert result == 15.0
assert ema.value == 15.0
def test_update_multiple_times(self) -> None:
"""Test multiple updates calculate correctly."""
ema = ExpFilter(alpha=0.5, initial=10.0)
ema.apply(1.0, 20.0) # 15.0
ema.apply(1.0, 20.0) # 0.5 * 20 + 0.5 * 15 = 17.5
assert ema.value == 17.5
def test_reset(self) -> None:
ema = ExpFilter(alpha=0.5, initial=10.0)
assert ema.value == 10.0
ema.reset()
assert ema.value == 10.0
ema = ExpFilter(alpha=0.5, initial=10.0)
ema.reset(initial=5.0)
assert ema.value == 5.0
class TestDynamicEndpointing:
"""Test cases for the DynamicEndpointing class."""
def test_initialization(self) -> None:
"""Test basic initialization with min and max delay."""
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0)
assert ep.min_delay == 0.3
assert ep.max_delay == 1.0
def test_initialization_with_custom_alpha(self) -> None:
"""Test initialization with custom alpha value."""
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0, alpha=0.2)
assert ep.min_delay == 0.3
assert ep.max_delay == 1.0
def test_initialization_uses_updated_default_alpha(self) -> None:
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0)
assert ep._utterance_pause._alpha == pytest.approx(0.9, rel=1e-5)
assert ep._turn_pause._alpha == pytest.approx(0.9, rel=1e-5)
def test_empty_delays(self) -> None:
"""Test between_utterance_delay returns 0 when no utterances recorded."""
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0)
assert ep.between_utterance_delay == 0.0
assert ep.between_turn_delay == 0.0
assert ep.immediate_interruption_delay == (0.0, 0.0)
def test_on_utterance_ended(self) -> None:
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0)
ep.on_end_of_speech(ended_at=100.0)
assert ep._utterance_ended_at == 100.0
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0)
ep.on_end_of_speech(ended_at=99.9)
assert ep._utterance_ended_at == 99.9
def test_on_utterance_started(self) -> None:
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0)
ep.on_start_of_speech(started_at=100.0)
assert ep._utterance_started_at == 100.0
def test_on_agent_speech_started(self) -> None:
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0)
ep.on_start_of_agent_speech(started_at=100.0)
assert ep._agent_speech_started_at == 100.0
def test_between_utterance_delay_calculation(self) -> None:
"""Test between_utterance_delay calculates the gap between utterances."""
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0)
ep.on_end_of_speech(ended_at=100.0)
ep.on_start_of_speech(started_at=100.5)
assert ep.between_utterance_delay == pytest.approx(0.5, rel=1e-5)
def test_between_turn_delay_calculation(self) -> None:
"""Test between_turn_delay calculates gap between utterance end and agent speech."""
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0)
ep.on_end_of_speech(ended_at=100.0)
ep.on_start_of_agent_speech(started_at=100.8)
assert ep.between_turn_delay == pytest.approx(0.8, rel=1e-5)
def test_pause_between_utterances_updates_min_delay(self) -> None:
"""Test that pauses between utterances (case 1) update min_delay via EMA."""
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0, alpha=0.5)
initial_min = ep.min_delay
ep.on_end_of_speech(ended_at=100.0)
ep.on_start_of_speech(started_at=100.4)
ep.on_end_of_speech(ended_at=100.5, should_ignore=False)
# min_delay should be updated via EMA: 0.5 * 0.4 + 0.5 * 0.3 = 0.35
expected = 0.5 * 0.4 + 0.5 * initial_min
assert ep.min_delay == pytest.approx(expected, rel=1e-5)
def test_new_turn_updates_max_delay(self) -> None:
"""Test that new turns (case 3) update max_delay via EMA."""
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0, alpha=0.5)
ep.on_end_of_speech(ended_at=100.0)
ep.on_start_of_agent_speech(started_at=100.6)
ep.on_start_of_speech(started_at=101.5)
ep.on_end_of_speech(ended_at=102.0, should_ignore=False)
assert ep.max_delay == pytest.approx(0.5 * 0.6 + 0.5 * 1.0, rel=1e-5)
def test_interruption_updates_min_delay(self) -> None:
"""Test that immediate interruptions (case 2) update min_delay."""
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0, alpha=0.5)
ep.on_end_of_speech(ended_at=100.0)
ep.on_start_of_agent_speech(started_at=100.2)
assert ep._agent_speech_started_at is not None
ep.on_start_of_speech(started_at=100.25, overlapping=True)
assert ep._overlapping is True
ep.on_end_of_speech(ended_at=100.5)
# pause = 100.25 - 100.0 = 0.25
# EMA: 0.5 * max(0.25, 0.3) + 0.5 * 0.3 = 0.3
assert ep._overlapping is False
assert ep._agent_speech_started_at is None # already used
assert ep.min_delay == pytest.approx(0.3, rel=1e-5)
def test_update_options(self) -> None:
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0)
ep.update_options(min_delay=0.5)
assert ep.min_delay == 0.5
assert ep._min_delay == 0.5
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0)
ep.update_options(max_delay=2.0)
assert ep.max_delay == 2.0
assert ep._max_delay == 2.0
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0)
ep.update_options(min_delay=0.5, max_delay=2.0)
assert ep.min_delay == 0.5
assert ep.max_delay == 2.0
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0)
ep.update_options()
assert ep.min_delay == 0.3
assert ep.max_delay == 1.0
def test_max_delay_clamped_to_configured_max(self) -> None:
"""Test that max_delay updates are clamped to the configured maximum."""
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0, alpha=1.0)
ep.on_end_of_speech(ended_at=100.0)
ep.on_start_of_agent_speech(started_at=102.0)
ep.on_start_of_speech(started_at=105.0)
assert ep.max_delay == 1.0 # pause=2.0 clamped to _max_delay
def test_max_delay_clamped_to_min_delay(self) -> None:
"""Test that max_delay updates are clamped to at least min_delay."""
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0, alpha=1.0)
ep.on_end_of_speech(ended_at=100.0)
ep.on_start_of_agent_speech(started_at=100.1)
ep.on_start_of_speech(started_at=100.5)
assert ep.max_delay >= ep._min_delay
def test_non_interruption_clears_agent_speech(self) -> None:
"""Test that non-interruption utterance start clears agent speech timestamp."""
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0)
ep.on_end_of_speech(ended_at=100.0)
ep.on_start_of_agent_speech(started_at=100.5)
assert ep._agent_speech_started_at is not None
ep.on_start_of_speech(started_at=102.0)
ep.on_end_of_speech(ended_at=103.0, should_ignore=False)
assert ep._agent_speech_started_at is None
def test_consecutive_interruptions_only_track_first(self) -> None:
"""Test that only the first interruption in a sequence updates min_delay."""
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0, alpha=0.5)
ep.on_end_of_speech(ended_at=100.0)
ep.on_start_of_agent_speech(started_at=100.2)
ep.on_start_of_speech(started_at=100.25, overlapping=True)
assert ep._overlapping is True
prev_val = ep.min_delay, ep.max_delay
ep.on_start_of_speech(started_at=100.35)
assert ep._overlapping is True
assert prev_val == (ep.min_delay, ep.max_delay)
def test_delayed_interruption_updates_max_delay_without_crashing(self) -> None:
"""Delayed interruptions should update max delay via the EMA path."""
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0, alpha=0.5)
ep.on_end_of_speech(ended_at=100.0)
ep.on_start_of_agent_speech(started_at=100.9)
ep.on_start_of_speech(started_at=101.8)
ep.on_end_of_speech(ended_at=102.0, should_ignore=False)
assert ep.max_delay == pytest.approx(0.5 * 0.9 + 0.5 * 1.0, rel=1e-5)
def test_interruption_adjusts_stale_utterance_end_time(self) -> None:
"""Interruption path should adjust stale utterance end timestamp before delay updates."""
ep = DynamicEndpointing(min_delay=0.06, max_delay=1.0, alpha=1.0)
# Simulate stale ordering where end timestamp still belongs to a previous utterance.
ep.on_end_of_speech(ended_at=99.0)
ep.on_start_of_speech(started_at=100.0)
ep.on_start_of_agent_speech(started_at=100.2)
ep.on_start_of_speech(started_at=100.25, overlapping=True)
assert ep._utterance_ended_at == pytest.approx(100.2, rel=1e-3)
assert ep.min_delay == pytest.approx(0.06, rel=1e-5)
assert ep.max_delay == pytest.approx(1.0, rel=1e-5)
def test_update_options_preserves_filter_alpha(self) -> None:
"""Changing delays should not overwrite the EMA smoothing coefficient."""
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0, alpha=0.5)
ep.update_options(min_delay=0.6, max_delay=2.0)
assert ep._utterance_pause._alpha == pytest.approx(0.5, rel=1e-5)
assert ep._turn_pause._alpha == pytest.approx(0.5, rel=1e-5)
def test_update_options_updates_alpha_in_place(self) -> None:
"""update_options(alpha=...) should update both EMA filters without resetting learned state."""
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0, alpha=0.5)
# Record some history so the filter has a non-initial value.
ep.on_end_of_speech(ended_at=100.0)
ep.on_start_of_speech(started_at=100.2)
ep.on_end_of_speech(ended_at=101.0)
learned_min = ep.min_delay
ep.update_options(alpha=0.2)
assert ep._utterance_pause._alpha == pytest.approx(0.2, rel=1e-5)
assert ep._turn_pause._alpha == pytest.approx(0.2, rel=1e-5)
# Learned value should be preserved — only the coefficient changed.
assert ep.min_delay == pytest.approx(learned_min, rel=1e-5)
def test_update_options_updates_filter_clamp_bounds(self) -> None:
"""Changing delays should propagate into exp-filter min/max clamp limits."""
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0, alpha=0.5)
ep.update_options(min_delay=0.5, max_delay=2.0)
assert ep._utterance_pause._min_val == 0.5
assert ep._turn_pause._max_val == 2.0
# min_delay updated from 0.3 to 0.5
ep.on_end_of_speech(ended_at=100.0)
ep.on_start_of_speech(started_at=100.2)
assert ep.min_delay == pytest.approx(0.5, rel=1e-5)
# max_delay updated from 1.0 to 2.0
ep.on_end_of_speech(ended_at=101.0)
ep.on_start_of_agent_speech(started_at=102.8)
ep.on_start_of_speech(started_at=103.5)
assert 1.0 < ep.max_delay <= 2.0
def test_should_ignore_skips_filter_update(self) -> None:
"""should_ignore=True with overlapping=True skips EMA updates and resets state."""
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0, alpha=0.5)
ep.on_end_of_speech(ended_at=100.0)
ep.on_start_of_agent_speech(started_at=100.5)
# user starts 1.0s after agent (well outside 0.25s grace period)
ep.on_start_of_speech(started_at=101.5, overlapping=True)
prev_min = ep.min_delay
prev_max = ep.max_delay
ep.on_end_of_speech(ended_at=101.8, should_ignore=True)
# filters should not have been updated
assert ep.min_delay == prev_min
assert ep.max_delay == prev_max
# state should be reset
assert ep._utterance_started_at is None
assert ep._utterance_ended_at is None
assert ep._overlapping is False
assert ep._speaking is False
def test_should_ignore_without_overlapping_still_updates(self) -> None:
"""should_ignore=True but overlapping=False follows the normal update path."""
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0, alpha=0.5)
initial_min = ep.min_delay
ep.on_end_of_speech(ended_at=100.0)
ep.on_start_of_speech(started_at=100.4, overlapping=False)
ep.on_end_of_speech(ended_at=100.6, should_ignore=True)
# should_ignore only gates when overlapping, so min_delay should update (case 1)
expected = 0.5 * 0.4 + 0.5 * initial_min
assert ep.min_delay == pytest.approx(expected, rel=1e-5)
def test_should_ignore_grace_period_overrides(self) -> None:
"""User speech within grace period of agent speech overrides should_ignore=True."""
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0, alpha=0.5)
ep.on_end_of_speech(ended_at=100.0)
ep.on_start_of_agent_speech(started_at=100.5)
# user starts speaking 0.1s after agent (within 0.25s grace period)
ep.on_start_of_speech(started_at=100.6, overlapping=True)
ep.on_end_of_speech(ended_at=100.8, should_ignore=True)
# grace period should override should_ignore, so the interruption path runs
# and state is properly cleaned up (not left as None)
assert ep._utterance_ended_at == 100.8
assert ep._speaking is False
def test_should_ignore_outside_grace_period(self) -> None:
"""User speech well after agent speech start is outside grace period."""
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0, alpha=0.5)
ep.on_end_of_speech(ended_at=100.0)
ep.on_start_of_agent_speech(started_at=100.5)
# user starts speaking 0.5s after agent (outside 0.25s grace period)
ep.on_start_of_speech(started_at=101.0, overlapping=True)
prev_min = ep.min_delay
prev_max = ep.max_delay
ep.on_end_of_speech(ended_at=101.5, should_ignore=True)
# outside grace period, should_ignore takes effect — no filter update
assert ep.min_delay == prev_min
assert ep.max_delay == prev_max
assert ep._utterance_started_at is None
assert ep._utterance_ended_at is None
def test_on_end_of_agent_speech_clears_state(self) -> None:
"""on_end_of_agent_speech sets ended_at, preserves started_at, clears overlapping."""
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0)
ep.on_start_of_agent_speech(started_at=100.0)
ep.on_start_of_speech(started_at=100.1, overlapping=True)
assert ep._overlapping is True
assert ep._agent_speech_started_at == 100.0
ep.on_end_of_agent_speech(ended_at=101.0)
assert ep._agent_speech_ended_at == 101.0
# _agent_speech_started_at is intentionally preserved so that
# between_turn_delay can be computed in the normal end-of-speech path
assert ep._agent_speech_started_at == 100.0
assert ep._overlapping is False
def test_overlapping_inferred_from_agent_speech(self) -> None:
"""When _agent_speech_started_at is set, on_end_of_speech takes the interruption path."""
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0, alpha=0.5)
ep.on_end_of_speech(ended_at=100.0)
ep.on_start_of_agent_speech(started_at=100.9)
# overlapping not explicitly set
ep.on_start_of_speech(started_at=101.8, overlapping=False)
ep.on_end_of_speech(ended_at=102.0)
# _agent_speech_started_at is set → interruption path → case 3 (delayed) updates max_delay
# between_turn_delay = 100.9 - 100.0 = 0.9
assert ep.max_delay == pytest.approx(0.5 * 0.9 + 0.5 * 1.0, rel=1e-5)
def test_speaking_flag_set_and_cleared(self) -> None:
"""_speaking is True after on_start_of_speech, False after on_end_of_speech."""
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0)
assert ep._speaking is False
ep.on_start_of_speech(started_at=100.0)
assert ep._speaking is True
ep.on_end_of_speech(ended_at=100.5)
assert ep._speaking is False
@pytest.mark.parametrize(
"label, agent_speech, overlapping, should_ignore, within_grace, expect_min_change, expect_max_change",
[
# --- No agent speech ---
# Case 1: pause between utterances updates min_delay
("no_agent/no_overlap/no_ignore", "none", False, False, False, True, False),
# should_ignore is ignored when not overlapping
("no_agent/no_overlap/ignore", "none", False, True, False, True, False),
# --- Agent speech ended (on_end_of_agent_speech called) ---
# agent finished speaking → normal path, between_turn_delay > 0 → case 3 updates max
("agent_ended/no_overlap/no_ignore", "ended", False, False, False, False, True),
("agent_ended/no_overlap/ignore", "ended", False, True, False, False, True),
# --- Agent speech active ---
# Inferred interruption from agent_speech_started_at → case 3 (delayed)
("agent_active/no_overlap/no_ignore", "active", False, False, False, False, True),
# should_ignore ignored when not _overlapping
("agent_active/no_overlap/ignore", "active", False, True, False, False, True),
# Explicit overlapping, immediate → case 2 updates min_delay
("agent_active/overlap/no_ignore", "active", True, False, False, True, False),
# Backchannel: overlapping + should_ignore outside grace → skip
(
"agent_active/overlap/ignore/outside_grace",
"active",
True,
True,
False,
False,
False,
),
# Grace period override: overlapping + should_ignore inside grace → case 2 still runs
("agent_active/overlap/ignore/inside_grace", "active", True, True, True, True, False),
],
)
def test_all_overlapping_and_should_ignore_combos(
self,
label: str,
agent_speech: str,
overlapping: bool,
should_ignore: bool,
within_grace: bool,
expect_min_change: bool,
expect_max_change: bool,
) -> None:
"""Exhaustive test of all agent_speech × overlapping × should_ignore combinations."""
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0, alpha=0.5)
# Previous utterance
ep.on_start_of_speech(started_at=99.0)
ep.on_end_of_speech(ended_at=100.0)
# Set up agent speech state
if agent_speech == "ended":
ep.on_start_of_agent_speech(started_at=100.5)
ep.on_end_of_agent_speech(ended_at=101.0)
user_start = 101.5
elif agent_speech == "active":
if within_grace:
# Agent at 100.15, user at 100.35 (0.2s after agent, within 0.25s grace)
# between_turn_delay=0.15, between_utterance_delay=0.35
# interruption_delay=|0.35-0.15|=0.2 <= 0.3 → case 2 triggers
# EMA: 0.5*0.35 + 0.5*0.3 = 0.325 → min changes
ep.on_start_of_agent_speech(started_at=100.15)
user_start = 100.35
elif overlapping and should_ignore:
# Outside grace: agent at 100.2, user at 101.5 (1.3s after agent)
# should_ignore + overlapping + outside grace → skip
ep.on_start_of_agent_speech(started_at=100.2)
user_start = 101.5
elif overlapping:
# Agent at 100.15, user at 100.4 (0.25s after agent, at grace boundary)
# between_turn_delay=0.15, between_utterance_delay=0.4
# interruption_delay=|0.4-0.15|=0.25 <= 0.3 → case 2 triggers
# EMA: 0.5*0.4 + 0.5*0.3 = 0.35 → min changes
ep.on_start_of_agent_speech(started_at=100.15)
user_start = 100.4
else:
# Delayed: agent spoke but user starts much later (inferred interruption)
# between_turn_delay=0.9 → case 3 updates max_delay
ep.on_start_of_agent_speech(started_at=100.9)
user_start = 101.8
else:
# No agent speech
user_start = 100.4
ep.on_start_of_speech(started_at=user_start, overlapping=overlapping)
prev_min = ep.min_delay
prev_max = ep.max_delay
ep.on_end_of_speech(ended_at=user_start + 0.5, should_ignore=should_ignore)
min_changed = ep.min_delay != prev_min
max_changed = ep.max_delay != prev_max
assert min_changed == expect_min_change, (
f"[{label}] min_delay {'should' if expect_min_change else 'should not'} change: "
f"{prev_min} -> {ep.min_delay}"
)
assert max_changed == expect_max_change, (
f"[{label}] max_delay {'should' if expect_max_change else 'should not'} change: "
f"{prev_max} -> {ep.max_delay}"
)
# State should always be cleaned up after on_end_of_speech
assert ep._speaking is False, f"[{label}] _speaking should be False"
assert ep._overlapping is False, f"[{label}] _overlapping should be False"
def test_full_conversation_sequence(self) -> None:
"""Simulate a realistic multi-turn conversation with backchannel ignored."""
ep = DynamicEndpointing(min_delay=0.3, max_delay=1.0, alpha=0.5)
# Turn 1: user speaks
ep.on_start_of_speech(started_at=100.0)
ep.on_end_of_speech(ended_at=101.0)
# Agent responds
ep.on_start_of_agent_speech(started_at=101.5)
# Turn 2: user backchannel (ignored) — overlapping with agent, 1.0s after agent start
ep.on_start_of_speech(started_at=102.5, overlapping=True)
min_before_backchannel = ep.min_delay
max_before_backchannel = ep.max_delay
ep.on_end_of_speech(ended_at=102.8, should_ignore=True)
# backchannel ignored — delays unchanged
assert ep.min_delay == min_before_backchannel
assert ep.max_delay == max_before_backchannel
# Agent finishes
ep.on_end_of_agent_speech(ended_at=103.0)
# Turn 3: user speaks again (new turn after agent)
ep.on_start_of_speech(started_at=103.5)
ep.on_end_of_speech(ended_at=104.0)
# agent_speech_ended_at was set, agent_speech_started_at is None
# This is a normal (non-interruption) path
# No agent_speech_started_at → case 1 (between utterances) doesn't apply
# because _utterance_ended_at was reset to None by the ignored backchannel
# so between_utterance_delay = 0 → no update
assert ep._speaking is False
assert ep._agent_speech_started_at is None
class TestCreateEndpointing:
def test_dynamic_mode_wires_alpha(self) -> None:
"""create_endpointing should pass alpha through to both EMA filters."""
ep = create_endpointing(
{"mode": "dynamic", "min_delay": 0.3, "max_delay": 1.0, "alpha": 0.7}
)
assert isinstance(ep, DynamicEndpointing)
assert ep._utterance_pause._alpha == pytest.approx(0.7, rel=1e-5)
assert ep._turn_pause._alpha == pytest.approx(0.7, rel=1e-5)
def test_fixed_mode_returns_base_endpointing(self) -> None:
ep = create_endpointing({"mode": "fixed", "min_delay": 0.5, "max_delay": 3.0, "alpha": 0.9})
assert not isinstance(ep, DynamicEndpointing)
assert ep.min_delay == 0.5
assert ep.max_delay == 3.0