Add VADUserStartedSpeakingFrame and VADUserStoppedSpeakingFrame

This commit is contained in:
Mark Backman
2025-04-25 09:02:04 -04:00
parent 7cfefe4f84
commit b298376766
4 changed files with 54 additions and 5 deletions

View File

@@ -6,7 +6,7 @@
from abc import ABC, abstractmethod
from enum import Enum
from typing import Optional
from typing import Optional, Tuple
from loguru import logger
from pydantic import BaseModel
@@ -88,12 +88,24 @@ class VADAnalyzer(ABC):
volume = calculate_audio_volume(audio, self.sample_rate)
return exp_smoothing(volume, self._prev_volume, self._smoothing_factor)
def analyze_audio(self, buffer) -> VADState:
def analyze_audio(self, buffer) -> Tuple[VADState, Optional[str]]:
"""Analyze audio for voice activity.
Args:
buffer: Audio buffer to analyze
Returns:
Tuple containing:
- VADState: Current VAD state
- Optional[str]: Event type if a speech event occurred ("speech_started",
"speech_stopped"), or None if no event occurred
"""
self._vad_buffer += buffer
event_type = None
num_required_bytes = self._vad_frames_num_bytes
if len(self._vad_buffer) < num_required_bytes:
return self._vad_state
return self._vad_state, event_type
audio_frames = self._vad_buffer[:num_required_bytes]
self._vad_buffer = self._vad_buffer[num_required_bytes:]
@@ -132,6 +144,7 @@ class VADAnalyzer(ABC):
):
self._vad_state = VADState.SPEAKING
self._vad_starting_count = 0
event_type = "speech_started"
if (
self._vad_state == VADState.STOPPING
@@ -139,5 +152,6 @@ class VADAnalyzer(ABC):
):
self._vad_state = VADState.QUIET
self._vad_stopping_count = 0
event_type = "speech_stopped"
return self._vad_state
return self._vad_state, event_type

View File

@@ -587,6 +587,20 @@ class EmulateUserStoppedSpeakingFrame(SystemFrame):
pass
@dataclass
class VADUserStartedSpeakingFrame(SystemFrame):
"""Frame emitted when VAD detects the user has definitively started speaking."""
pass
@dataclass
class VADUserStoppedSpeakingFrame(SystemFrame):
"""Frame emitted when VAD detects the user has definitively stopped speaking."""
pass
@dataclass
class BotInterruptionFrame(SystemFrame):
"""Emitted by when the bot should be interrupted. This will mainly cause the

View File

@@ -32,6 +32,8 @@ from pipecat.frames.frames import (
UserStartedSpeakingFrame,
UserStoppedSpeakingFrame,
VADParamsUpdateFrame,
VADUserStartedSpeakingFrame,
VADUserStoppedSpeakingFrame,
)
from pipecat.metrics.metrics import MetricsData
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
@@ -228,9 +230,13 @@ class BaseInputTransport(FrameProcessor):
async def _vad_analyze(self, audio_frame: InputAudioRawFrame) -> VADState:
state = VADState.QUIET
if self.vad_analyzer:
state = await self.get_event_loop().run_in_executor(
state, event_type = await self.get_event_loop().run_in_executor(
self._executor, self.vad_analyzer.analyze_audio, audio_frame.audio
)
if event_type:
await self._handle_vad_event(event_type)
return state
async def _handle_vad(self, audio_frame: InputAudioRawFrame, vad_state: VADState):
@@ -260,6 +266,16 @@ class BaseInputTransport(FrameProcessor):
vad_state = new_vad_state
return vad_state
async def _handle_vad_event(self, event_type: str):
"""Handle VAD speech events by creating and pushing appropriate frames."""
if event_type == "speech_started":
logger.debug("VAD detected definitive speech start")
await self.push_frame(VADUserStartedSpeakingFrame())
elif event_type == "speech_stopped":
logger.debug("VAD detected definitive speech stop")
await self.push_frame(VADUserStoppedSpeakingFrame())
async def _handle_end_of_turn(self):
if self.turn_analyzer:
state, prediction = await self.turn_analyzer.analyze_end_of_turn()