项目文件夹

文件
2026-07-13 13:39:38 +08:00

584 行
25 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
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