Allowing to define SmartTurnParams
This commit is contained in:
@@ -12,6 +12,7 @@ from typing import Dict, Optional
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class EndOfTurnState(Enum):
|
class EndOfTurnState(Enum):
|
||||||
@@ -19,18 +20,25 @@ class EndOfTurnState(Enum):
|
|||||||
INCOMPLETE = 2
|
INCOMPLETE = 2
|
||||||
|
|
||||||
|
|
||||||
# TODO: we should convert all this to params
|
STOP_SECS = 1
|
||||||
STOP_MS = 1000
|
|
||||||
PRE_SPEECH_MS = 200
|
PRE_SPEECH_MS = 200
|
||||||
MAX_DURATION_SECONDS = 8 # Maximum duration for the smart turn model
|
MAX_DURATION_SECONDS = 8 # Maximum duration for the smart turn model
|
||||||
|
|
||||||
|
|
||||||
class BaseEndOfTurnAnalyzer(ABC):
|
class SmartTurnParams(BaseModel):
|
||||||
def __init__(self, *, sample_rate: Optional[int] = None):
|
stop_secs: float = STOP_SECS
|
||||||
|
pre_speech_ms: float = PRE_SPEECH_MS
|
||||||
|
max_duration_secs: float = MAX_DURATION_SECONDS
|
||||||
|
|
||||||
|
|
||||||
|
class BaseSmartTurn(ABC):
|
||||||
|
def __init__(self, *, sample_rate: Optional[int] = None, params: SmartTurnParams = SmartTurnParams()):
|
||||||
self._init_sample_rate = sample_rate
|
self._init_sample_rate = sample_rate
|
||||||
|
self._params = params
|
||||||
# settings variables
|
# settings variables
|
||||||
self._sample_rate = 0
|
self._sample_rate = 0
|
||||||
self._chunk_size_ms = 0
|
self._chunk_size_ms = 0
|
||||||
|
self._stop_ms = self._params.stop_secs * 1000
|
||||||
# inference variables
|
# inference variables
|
||||||
self._audio_buffer = []
|
self._audio_buffer = []
|
||||||
self._speech_triggered = False
|
self._speech_triggered = False
|
||||||
@@ -67,8 +75,8 @@ class BaseEndOfTurnAnalyzer(ABC):
|
|||||||
if self._speech_triggered:
|
if self._speech_triggered:
|
||||||
self._audio_buffer.append((time.time(), audio_float32))
|
self._audio_buffer.append((time.time(), audio_float32))
|
||||||
self._silence_frames += 1
|
self._silence_frames += 1
|
||||||
if self._silence_frames * self._chunk_size_ms >= STOP_MS:
|
if self._silence_frames * self._chunk_size_ms >= self._stop_ms:
|
||||||
logger.debug("End of Turn complete due to STOP_MS.")
|
logger.debug("End of Turn complete due to stop_secs.")
|
||||||
state = EndOfTurnState.COMPLETE
|
state = EndOfTurnState.COMPLETE
|
||||||
self._clear()
|
self._clear()
|
||||||
else:
|
else:
|
||||||
@@ -76,8 +84,8 @@ class BaseEndOfTurnAnalyzer(ABC):
|
|||||||
self._audio_buffer.append((time.time(), audio_float32))
|
self._audio_buffer.append((time.time(), audio_float32))
|
||||||
# Keep the buffer size reasonable, assuming CHUNK is small
|
# Keep the buffer size reasonable, assuming CHUNK is small
|
||||||
max_buffer_time = (
|
max_buffer_time = (
|
||||||
PRE_SPEECH_MS + STOP_MS
|
self._params.pre_speech_ms + self._stop_ms
|
||||||
) / 1000 + MAX_DURATION_SECONDS # Some extra buffer
|
) / 1000 + self._params.max_duration_secs # Some extra buffer
|
||||||
while (
|
while (
|
||||||
self._audio_buffer and self._audio_buffer[0][0] < time.time() - max_buffer_time
|
self._audio_buffer and self._audio_buffer[0][0] < time.time() - max_buffer_time
|
||||||
):
|
):
|
||||||
@@ -107,7 +115,7 @@ class BaseEndOfTurnAnalyzer(ABC):
|
|||||||
return state
|
return state
|
||||||
|
|
||||||
# Find start and end indices for the segment
|
# Find start and end indices for the segment
|
||||||
start_time = self._speech_start_time - (PRE_SPEECH_MS / 1000)
|
start_time = self._speech_start_time - (self._params.pre_speech_ms / 1000)
|
||||||
start_index = 0
|
start_index = 0
|
||||||
for i, (t, _) in enumerate(audio_buffer):
|
for i, (t, _) in enumerate(audio_buffer):
|
||||||
if t >= start_time:
|
if t >= start_time:
|
||||||
@@ -120,13 +128,13 @@ class BaseEndOfTurnAnalyzer(ABC):
|
|||||||
segment_audio_chunks = [chunk for _, chunk in audio_buffer[start_index : end_index + 1]]
|
segment_audio_chunks = [chunk for _, chunk in audio_buffer[start_index : end_index + 1]]
|
||||||
segment_audio = np.concatenate(segment_audio_chunks)
|
segment_audio = np.concatenate(segment_audio_chunks)
|
||||||
|
|
||||||
# Remove (STOP_MS - 200)ms from the end of the segment
|
# Remove (self._stop_ms - 200)ms from the end of the segment
|
||||||
samples_to_remove = int((STOP_MS - 200) / 1000 * self.sample_rate)
|
samples_to_remove = int((self._stop_ms - 200) / 1000 * self.sample_rate)
|
||||||
segment_audio = segment_audio[:-samples_to_remove]
|
segment_audio = segment_audio[:-samples_to_remove]
|
||||||
|
|
||||||
# Limit maximum duration
|
# Limit maximum duration
|
||||||
if len(segment_audio) / self.sample_rate > MAX_DURATION_SECONDS:
|
if len(segment_audio) / self.sample_rate > self._params.max_duration_secs:
|
||||||
segment_audio = segment_audio[: int(MAX_DURATION_SECONDS * self.sample_rate)]
|
segment_audio = segment_audio[: int(self._params.max_duration_secs * self.sample_rate)]
|
||||||
|
|
||||||
# No resampling needed as both recording and prediction use 16000 Hz
|
# No resampling needed as both recording and prediction use 16000 Hz
|
||||||
segment_audio_resampled = segment_audio
|
segment_audio_resampled = segment_audio
|
||||||
@@ -12,9 +12,7 @@ import numpy as np
|
|||||||
import torch
|
import torch
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from pipecat.audio.turn.base_turn_analyzer import (
|
from pipecat.audio.turn.base_smart_turn import BaseSmartTurn
|
||||||
BaseEndOfTurnAnalyzer,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import coremltools as ct
|
import coremltools as ct
|
||||||
@@ -27,9 +25,9 @@ except ModuleNotFoundError as e:
|
|||||||
raise Exception(f"Missing module: {e}")
|
raise Exception(f"Missing module: {e}")
|
||||||
|
|
||||||
|
|
||||||
class LocalSmartTurnAnalyzer(BaseEndOfTurnAnalyzer):
|
class LocalSmartTurnAnalyzer(BaseSmartTurn):
|
||||||
def __init__(self):
|
def __init__(self, **kwargs):
|
||||||
super().__init__()
|
super().__init__(**kwargs)
|
||||||
# To use this locally, set the environment variable LOCAL_SMART_TURN_MODEL_PATH
|
# To use this locally, set the environment variable LOCAL_SMART_TURN_MODEL_PATH
|
||||||
# to the path where the smart-turn repo is cloned.
|
# to the path where the smart-turn repo is cloned.
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ import numpy as np
|
|||||||
import requests
|
import requests
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from pipecat.audio.turn.base_turn_analyzer import BaseEndOfTurnAnalyzer
|
from pipecat.audio.turn.base_smart_turn import BaseSmartTurn
|
||||||
|
|
||||||
|
|
||||||
class RemoteSmartTurnAnalyzer(BaseEndOfTurnAnalyzer):
|
class RemoteSmartTurnAnalyzer(BaseSmartTurn):
|
||||||
def __init__(self):
|
def __init__(self, **kwargs):
|
||||||
super().__init__()
|
super().__init__(**kwargs)
|
||||||
self.remote_smart_turn_url = os.getenv("REMOTE_SMART_TURN_URL")
|
self.remote_smart_turn_url = os.getenv("REMOTE_SMART_TURN_URL")
|
||||||
|
|
||||||
if not self.remote_smart_turn_url:
|
if not self.remote_smart_turn_url:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from typing import Optional
|
|||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from pipecat.audio.turn.base_turn_analyzer import BaseEndOfTurnAnalyzer, EndOfTurnState
|
from pipecat.audio.turn.base_smart_turn import BaseSmartTurn, EndOfTurnState
|
||||||
from pipecat.audio.vad.vad_analyzer import VADAnalyzer, VADState
|
from pipecat.audio.vad.vad_analyzer import VADAnalyzer, VADState
|
||||||
from pipecat.frames.frames import (
|
from pipecat.frames.frames import (
|
||||||
BotInterruptionFrame,
|
BotInterruptionFrame,
|
||||||
@@ -67,7 +67,7 @@ class BaseInputTransport(FrameProcessor):
|
|||||||
return self._params.vad_analyzer
|
return self._params.vad_analyzer
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def end_of_turn_analyzer(self) -> Optional[BaseEndOfTurnAnalyzer]:
|
def end_of_turn_analyzer(self) -> Optional[BaseSmartTurn]:
|
||||||
return self._params.end_of_turn_analyzer
|
return self._params.end_of_turn_analyzer
|
||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from pydantic import BaseModel, ConfigDict
|
|||||||
|
|
||||||
from pipecat.audio.filters.base_audio_filter import BaseAudioFilter
|
from pipecat.audio.filters.base_audio_filter import BaseAudioFilter
|
||||||
from pipecat.audio.mixers.base_audio_mixer import BaseAudioMixer
|
from pipecat.audio.mixers.base_audio_mixer import BaseAudioMixer
|
||||||
from pipecat.audio.turn.base_turn_analyzer import BaseEndOfTurnAnalyzer
|
from pipecat.audio.turn.base_smart_turn import BaseSmartTurn
|
||||||
from pipecat.audio.vad.vad_analyzer import VADAnalyzer
|
from pipecat.audio.vad.vad_analyzer import VADAnalyzer
|
||||||
from pipecat.processors.frame_processor import FrameProcessor
|
from pipecat.processors.frame_processor import FrameProcessor
|
||||||
from pipecat.utils.base_object import BaseObject
|
from pipecat.utils.base_object import BaseObject
|
||||||
@@ -42,7 +42,7 @@ class TransportParams(BaseModel):
|
|||||||
vad_enabled: bool = False
|
vad_enabled: bool = False
|
||||||
vad_audio_passthrough: bool = False
|
vad_audio_passthrough: bool = False
|
||||||
vad_analyzer: Optional[VADAnalyzer] = None
|
vad_analyzer: Optional[VADAnalyzer] = None
|
||||||
end_of_turn_analyzer: Optional[BaseEndOfTurnAnalyzer] = None
|
end_of_turn_analyzer: Optional[BaseSmartTurn] = None
|
||||||
|
|
||||||
|
|
||||||
class BaseTransport(BaseObject):
|
class BaseTransport(BaseObject):
|
||||||
|
|||||||
Reference in New Issue
Block a user