BaseSmartTurn: process speech in a separate thread

This commit is contained in:
Aleix Conchillo Flaqué
2025-09-25 11:12:25 -07:00
parent 22ef61fe8d
commit 7bafc3a1bb
6 changed files with 25 additions and 11 deletions

View File

@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Include OpenAI-based LLM services cached tokens to `MetricsFrame`. - Include OpenAI-based LLM services cached tokens to `MetricsFrame`.
## Fixed
- Fixed an issue where local SmartTurn was not being ran in a separate thread.
## [0.0.86] - 2025-09-24 ## [0.0.86] - 2025-09-24
### Added ### Added

View File

@@ -11,8 +11,10 @@ machine learning models to determine when a user has finished speaking, going
beyond simple silence-based detection. beyond simple silence-based detection.
""" """
import asyncio
import time import time
from abc import abstractmethod from abc import abstractmethod
from concurrent.futures import ThreadPoolExecutor
from typing import Any, Dict, Optional, Tuple from typing import Any, Dict, Optional, Tuple
import numpy as np import numpy as np
@@ -77,6 +79,9 @@ class BaseSmartTurn(BaseTurnAnalyzer):
self._speech_triggered = False self._speech_triggered = False
self._silence_ms = 0 self._silence_ms = 0
self._speech_start_time = 0 self._speech_start_time = 0
# Thread executor that will run the model. We only need one thread per
# analyzer because one analyzer just handles one audio stream.
self._executor = ThreadPoolExecutor(max_workers=1)
@property @property
def speech_triggered(self) -> bool: def speech_triggered(self) -> bool:
@@ -151,7 +156,10 @@ class BaseSmartTurn(BaseTurnAnalyzer):
Tuple containing the end-of-turn state and optional metrics data Tuple containing the end-of-turn state and optional metrics data
from the ML model analysis. from the ML model analysis.
""" """
state, result = await self._process_speech_segment(self._audio_buffer) loop = asyncio.get_running_loop()
state, result = await loop.run_in_executor(
self._executor, self._process_speech_segment, self._audio_buffer
)
if state == EndOfTurnState.COMPLETE or USE_ONLY_LAST_VAD_SEGMENT: if state == EndOfTurnState.COMPLETE or USE_ONLY_LAST_VAD_SEGMENT:
self._clear(state) self._clear(state)
logger.debug(f"End of Turn result: {state}") logger.debug(f"End of Turn result: {state}")
@@ -169,9 +177,7 @@ class BaseSmartTurn(BaseTurnAnalyzer):
self._speech_start_time = 0 self._speech_start_time = 0
self._silence_ms = 0 self._silence_ms = 0
async def _process_speech_segment( def _process_speech_segment(self, audio_buffer) -> Tuple[EndOfTurnState, Optional[MetricsData]]:
self, audio_buffer
) -> Tuple[EndOfTurnState, Optional[MetricsData]]:
"""Process accumulated audio segment using ML model.""" """Process accumulated audio segment using ML model."""
state = EndOfTurnState.INCOMPLETE state = EndOfTurnState.INCOMPLETE
@@ -203,7 +209,7 @@ class BaseSmartTurn(BaseTurnAnalyzer):
if len(segment_audio) > 0: if len(segment_audio) > 0:
start_time = time.perf_counter() start_time = time.perf_counter()
try: try:
result = await self._predict_endpoint(segment_audio) result = self._predict_endpoint(segment_audio)
state = ( state = (
EndOfTurnState.COMPLETE EndOfTurnState.COMPLETE
if result["prediction"] == 1 if result["prediction"] == 1
@@ -249,6 +255,6 @@ class BaseSmartTurn(BaseTurnAnalyzer):
return state, result_data return state, result_data
@abstractmethod @abstractmethod
async def _predict_endpoint(self, audio_array: np.ndarray) -> Dict[str, Any]: def _predict_endpoint(self, audio_array: np.ndarray) -> Dict[str, Any]:
"""Predict end-of-turn using ML model from audio data.""" """Predict end-of-turn using ML model from audio data."""
pass pass

View File

@@ -104,11 +104,15 @@ class HttpSmartTurnAnalyzer(BaseSmartTurn):
logger.error(f"Failed to send raw request to Daily Smart Turn: {e}") logger.error(f"Failed to send raw request to Daily Smart Turn: {e}")
raise Exception("Failed to send raw request to Daily Smart Turn.") raise Exception("Failed to send raw request to Daily Smart Turn.")
async def _predict_endpoint(self, audio_array: np.ndarray) -> Dict[str, Any]: def _predict_endpoint(self, audio_array: np.ndarray) -> Dict[str, Any]:
"""Predict end-of-turn using remote HTTP ML service.""" """Predict end-of-turn using remote HTTP ML service."""
try: try:
serialized_array = self._serialize_array(audio_array) serialized_array = self._serialize_array(audio_array)
return await self._send_raw_request(serialized_array) loop = asyncio.get_running_loop()
future = asyncio.run_coroutine_threadsafe(
self._send_raw_request(serialized_array), loop
)
return future.result()
except Exception as e: except Exception as e:
logger.error(f"Smart turn prediction failed: {str(e)}") logger.error(f"Smart turn prediction failed: {str(e)}")
# Return an incomplete prediction when a failure occurs # Return an incomplete prediction when a failure occurs

View File

@@ -64,7 +64,7 @@ class LocalSmartTurnAnalyzer(BaseSmartTurn):
self._turn_model.eval() self._turn_model.eval()
logger.debug("Loaded Local Smart Turn") logger.debug("Loaded Local Smart Turn")
async def _predict_endpoint(self, audio_array: np.ndarray) -> Dict[str, Any]: def _predict_endpoint(self, audio_array: np.ndarray) -> Dict[str, Any]:
"""Predict end-of-turn using local PyTorch model.""" """Predict end-of-turn using local PyTorch model."""
inputs = self._turn_processor( inputs = self._turn_processor(
audio_array, audio_array,

View File

@@ -73,7 +73,7 @@ class LocalSmartTurnAnalyzerV2(BaseSmartTurn):
self._turn_model.eval() self._turn_model.eval()
logger.debug("Loaded Local Smart Turn v2") logger.debug("Loaded Local Smart Turn v2")
async def _predict_endpoint(self, audio_array: np.ndarray) -> Dict[str, Any]: def _predict_endpoint(self, audio_array: np.ndarray) -> Dict[str, Any]:
"""Predict end-of-turn using local PyTorch model.""" """Predict end-of-turn using local PyTorch model."""
inputs = self._turn_processor( inputs = self._turn_processor(
audio_array, audio_array,

View File

@@ -77,7 +77,7 @@ class LocalSmartTurnAnalyzerV3(BaseSmartTurn):
logger.debug("Loaded Local Smart Turn v3") logger.debug("Loaded Local Smart Turn v3")
async def _predict_endpoint(self, audio_array: np.ndarray) -> Dict[str, Any]: def _predict_endpoint(self, audio_array: np.ndarray) -> Dict[str, Any]:
"""Predict end-of-turn using local ONNX model.""" """Predict end-of-turn using local ONNX model."""
def truncate_audio_to_last_n_seconds(audio_array, n_seconds=8, sample_rate=16000): def truncate_audio_to_last_n_seconds(audio_array, n_seconds=8, sample_rate=16000):