Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -237,14 +237,23 @@ EVA_MODEL_LIST='[
#v EVA_MODEL__TURN_START_STRATEGY_PARAMS='{}'

#i Turn stop strategy: when to consider the user has finished speaking.
#i krisp_viva_turn uses Krisp's VIVA SDK turn detection v3 (streaming, frame-by-frame).
#i It needs the proprietary krisp_audio SDK installed plus KRISP_VIVA_TURN_MODEL_PATH
#i (path to a .kef model file) and KRISP_VIVA_API_KEY set below.
#d enum
#e turn_analyzer,speech_timeout,external
#e turn_analyzer,speech_timeout,krisp_viva_turn,external
#v EVA_MODEL__TURN_STOP_STRATEGY=turn_analyzer

#i Turn stop strategy parameters. For speech_timeout: {"user_speech_timeout": 0.8}.
#i For krisp_viva_turn: {"threshold": 0.5, "frame_duration_ms": 20} (model_path/api_key
#i can also be passed here to override the env vars).
#d json_object
#v EVA_MODEL__TURN_STOP_STRATEGY_PARAMS='{}'

#i Krisp VIVA SDK credentials (only used when TURN_STOP_STRATEGY=krisp_viva_turn).
#v KRISP_VIVA_TURN_MODEL_PATH=/path/to/krisp-viva-turn.kef
#v KRISP_VIVA_API_KEY=your_krisp_license_key

#i VAD (Voice Activity Detection) analyzer.
#d enum
#e silero,none
Expand Down
2 changes: 1 addition & 1 deletion src/eva/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

# Bump simulation_version when changes affect benchmark outputs (agent code,
# user simulator, orchestrator, simulation prompts, agent configs, tool mocks).
simulation_version = "2.0.1"
simulation_version = "2.0.2"

# Bump metrics_version when changes affect metric computation (metrics code,
# judge prompts, pricing tables, postprocessor).
Expand Down
19 changes: 17 additions & 2 deletions src/eva/assistant/pipeline/turn_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def create_turn_stop_strategy(
"""Create a user turn stop strategy from configuration.

Args:
strategy_type: Strategy type ('speech_timeout', 'turn_analyzer', 'external')
strategy_type: Strategy type ('speech_timeout', 'turn_analyzer', 'krisp_viva_turn', 'external')
strategy_params: Strategy-specific parameters
smart_turn_stop_secs: stop_secs for SmartTurnParams (used with turn_analyzer strategy)

Expand All @@ -117,11 +117,26 @@ def create_turn_stop_strategy(
smart_params = SmartTurnParams(stop_secs=stop_secs) if stop_secs is not None else None
turn_analyzer = LocalSmartTurnAnalyzerV3(params=smart_params)
return TurnAnalyzerUserTurnStopStrategy(turn_analyzer=turn_analyzer, **params)
elif strategy_type_lower == "krisp_viva_turn":
# KrispVivaTurn uses Krisp's VIVA SDK turn detection v3 (Tt) API, processing audio
# frame-by-frame in real time with an external VAD flag. It depends on the proprietary
# krisp_audio SDK plus a .kef model file (KRISP_VIVA_TURN_MODEL_PATH) and license key
# (KRISP_VIVA_API_KEY), so import lazily to keep the SDK optional.
from pipecat.audio.turn.krisp_viva_turn import KrispTurnParams, KrispVivaTurn

params = dict(strategy_params)
# Krisp analyzer tuning params; fall back to KrispTurnParams defaults when absent.
krisp_kwargs = {k: params.pop(k) for k in ("threshold", "frame_duration_ms") if k in params}
krisp_params = KrispTurnParams(**krisp_kwargs) if krisp_kwargs else None
# Constructor kwargs; model_path/api_key fall back to their env vars when omitted.
analyzer_kwargs = {k: params.pop(k) for k in ("model_path", "api_key", "sample_rate") if k in params}
krisp_analyzer = KrispVivaTurn(params=krisp_params, **analyzer_kwargs)
return TurnAnalyzerUserTurnStopStrategy(turn_analyzer=krisp_analyzer, **params)
elif strategy_type_lower == "external":
# ExternalUserTurnStopStrategy has no required parameters
return ExternalUserTurnStopStrategy(**strategy_params)
else:
raise ValueError(
f"Unsupported turn stop strategy: {strategy_type}. "
f"Supported types: 'speech_timeout', 'turn_analyzer', 'external'"
f"Supported types: 'speech_timeout', 'turn_analyzer', 'krisp_viva_turn', 'external'"
)
4 changes: 3 additions & 1 deletion src/eva/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,10 @@ class ModelConfig(BaseModel):
turn_stop_strategy: str = Field(
"turn_analyzer",
description=(
"User turn stop strategy: 'speech_timeout', 'turn_analyzer', or 'external'. "
"User turn stop strategy: 'speech_timeout', 'turn_analyzer', 'krisp_viva_turn', or 'external'. "
"Defaults to 'turn_analyzer' (TurnAnalyzerUserTurnStopStrategy with LocalSmartTurnAnalyzerV3). "
"'krisp_viva_turn' uses Krisp's VIVA SDK (requires the krisp_audio SDK, "
"KRISP_VIVA_TURN_MODEL_PATH, and KRISP_VIVA_API_KEY). "
"Set via EVA_MODEL__TURN_STOP_STRATEGY."
),
)
Expand Down
68 changes: 67 additions & 1 deletion tests/unit/assistant/test_turn_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,5 +226,71 @@ def test_unsupported_strategy_raises(self):

def test_unsupported_strategy_error_lists_supported(self):
"""ValueError message lists supported strategies."""
with pytest.raises(ValueError, match="speech_timeout.*turn_analyzer.*external"):
with pytest.raises(ValueError, match="speech_timeout.*turn_analyzer.*krisp_viva_turn.*external"):
create_turn_stop_strategy("unknown", {})


# ---------------------------------------------------------------------------
# create_turn_stop_strategy — krisp_viva_turn
# ---------------------------------------------------------------------------


class TestCreateTurnStopStrategyKrispViva:
"""Tests for the 'krisp_viva_turn' branch of create_turn_stop_strategy.

KrispVivaTurn depends on the proprietary ``krisp_audio`` SDK, which is not a
project dependency. The factory imports ``pipecat.audio.turn.krisp_viva_turn``
lazily, so these tests inject a fake module into ``sys.modules``.
"""

@staticmethod
def _install_fake_krisp(monkeypatch):
"""Inject a fake pipecat.audio.turn.krisp_viva_turn module and return it."""
import sys

fake_module = MagicMock()
fake_module.KrispVivaTurn.return_value = MagicMock()
monkeypatch.setitem(sys.modules, "pipecat.audio.turn.krisp_viva_turn", fake_module)
return fake_module

def test_krisp_viva_turn_strategy(self, monkeypatch):
"""'krisp_viva_turn' returns a TurnAnalyzerUserTurnStopStrategy with a KrispVivaTurn."""
fake_module = self._install_fake_krisp(monkeypatch)

result = create_turn_stop_strategy("krisp_viva_turn", {})

assert isinstance(result, TurnAnalyzerUserTurnStopStrategy)
# No tuning params given -> KrispTurnParams left at its defaults (params=None).
fake_module.KrispVivaTurn.assert_called_once_with(params=None)
fake_module.KrispTurnParams.assert_not_called()

def test_krisp_viva_turn_case_insensitive(self, monkeypatch):
"""Strategy type is matched case-insensitively."""
self._install_fake_krisp(monkeypatch)
assert isinstance(create_turn_stop_strategy("KRISP_VIVA_TURN", {}), TurnAnalyzerUserTurnStopStrategy)

def test_krisp_viva_turn_tuning_params(self, monkeypatch):
"""Tuning params threshold / frame_duration_ms are forwarded to KrispTurnParams."""
fake_module = self._install_fake_krisp(monkeypatch)

create_turn_stop_strategy("krisp_viva_turn", {"threshold": 0.7, "frame_duration_ms": 30})

fake_module.KrispTurnParams.assert_called_once_with(threshold=0.7, frame_duration_ms=30)
# The constructed params object is passed to the analyzer.
assert fake_module.KrispVivaTurn.call_args.kwargs["params"] is fake_module.KrispTurnParams.return_value

def test_krisp_viva_turn_constructor_params(self, monkeypatch):
"""model_path / api_key / sample_rate are forwarded to the KrispVivaTurn constructor."""
fake_module = self._install_fake_krisp(monkeypatch)

create_turn_stop_strategy(
"krisp_viva_turn",
{"model_path": "/models/turn.kef", "api_key": "secret", "sample_rate": 16000},
)

call_kwargs = fake_module.KrispVivaTurn.call_args.kwargs
assert call_kwargs["model_path"] == "/models/turn.kef"
assert call_kwargs["api_key"] == "secret"
assert call_kwargs["sample_rate"] == 16000
# None of these should leak into the strategy as tuning params.
fake_module.KrispTurnParams.assert_not_called()
Loading