Draft Implementation for Krisp VIVA VAD.
This commit is contained in:
189
src/pipecat/audio/vad/krisp_viva_vad.py
Normal file
189
src/pipecat/audio/vad/krisp_viva_vad.py
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2024-2026, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
"""Krisp Voice Activity Detection (VAD) implementation for Pipecat.
|
||||||
|
|
||||||
|
This module provides a VAD analyzer based on the Krisp VIVA SDK,
|
||||||
|
which can detect voice activity in audio streams with high accuracy.
|
||||||
|
Supports 8kHz, 16kHz, 32kHz, 44.1kHz and 48kHz sample rates.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from pipecat.audio.krisp_instance import (
|
||||||
|
KrispVivaSDKManager,
|
||||||
|
int_to_krisp_frame_duration,
|
||||||
|
int_to_krisp_sample_rate,
|
||||||
|
)
|
||||||
|
from pipecat.audio.vad.vad_analyzer import VADAnalyzer, VADParams
|
||||||
|
|
||||||
|
try:
|
||||||
|
import krisp_audio
|
||||||
|
except ModuleNotFoundError as e:
|
||||||
|
logger.error(f"Exception: {e}")
|
||||||
|
logger.error("In order to use KrispVivaVADAnalyzer, you need to install krisp_audio.")
|
||||||
|
raise Exception(f"Missing module: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
class KrispVivaVadAnalyzer(VADAnalyzer):
|
||||||
|
"""Voice Activity Detection analyzer using the Krisp VIVA SDK."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
model_path: Optional[str] = None,
|
||||||
|
frame_duration: int = 10,
|
||||||
|
sample_rate: Optional[int] = None,
|
||||||
|
params: Optional[VADParams] = None,
|
||||||
|
):
|
||||||
|
"""Initialize the Krisp VIVA VAD analyzer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_path: Path to the Krisp model file (.kef extension).
|
||||||
|
If None, uses KRISP_VIVA_VAD_MODEL_PATH environment variable.
|
||||||
|
frame_duration: Frame duration in milliseconds (default: 10ms).
|
||||||
|
sample_rate: Audio sample rate (must be 8000, 16000, 32000, 44100 or 48000 Hz).
|
||||||
|
If None, will be set later.
|
||||||
|
params: VAD parameters for detection configuration.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If model_path is not provided and KRISP_VIVA_VAD_MODEL_PATH is not set.
|
||||||
|
Exception: If model file doesn't have .kef extension.
|
||||||
|
FileNotFoundError: If model file doesn't exist.
|
||||||
|
"""
|
||||||
|
super().__init__(sample_rate=sample_rate, params=params)
|
||||||
|
|
||||||
|
logger.debug("Loading Krisp VIVA VAD model...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Set model path, checking environment if not specified
|
||||||
|
if model_path:
|
||||||
|
self._model_path = model_path
|
||||||
|
else:
|
||||||
|
self._model_path = os.getenv("KRISP_VIVA_VAD_MODEL_PATH")
|
||||||
|
if not self._model_path:
|
||||||
|
logger.error(
|
||||||
|
"Model path is not provided and KRISP_VIVA_VAD_MODEL_PATH is not set."
|
||||||
|
)
|
||||||
|
raise ValueError("Model path for KrispVivaVADAnalyzer must be provided.")
|
||||||
|
|
||||||
|
if not self._model_path.endswith(".kef"):
|
||||||
|
raise Exception("Model is expected with .kef extension")
|
||||||
|
|
||||||
|
if not os.path.isfile(self._model_path):
|
||||||
|
raise FileNotFoundError(f"Model file not found: {self._model_path}")
|
||||||
|
|
||||||
|
self._session = None
|
||||||
|
self._frame_duration_ms = frame_duration
|
||||||
|
self._samples_per_frame = frame_duration * sample_rate / 1000
|
||||||
|
|
||||||
|
# Acquire SDK reference (will initialize on first call)
|
||||||
|
KrispVivaSDKManager.acquire()
|
||||||
|
|
||||||
|
logger.debug("Loaded Krisp VIVA VAD")
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
# If initialization fails, release the SDK reference
|
||||||
|
KrispVivaSDKManager.release()
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _create_session(self, sample_rate: int, frame_duration: int):
|
||||||
|
"""Create a Krisp VAD session with a specific sample rate.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sample_rate: Sample rate for the session
|
||||||
|
frame_duration: Frame duration in milliseconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Krisp VAD session instance
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If session creation fails
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
model_info = krisp_audio.ModelInfo()
|
||||||
|
model_info.path = self._model_path
|
||||||
|
|
||||||
|
vad_cfg = krisp_audio.VadSessionConfig()
|
||||||
|
vad_cfg.inputSampleRate = int_to_krisp_sample_rate(sample_rate)
|
||||||
|
vad_cfg.inputFrameDuration = int_to_krisp_frame_duration(frame_duration)
|
||||||
|
vad_cfg.modelInfo = model_info
|
||||||
|
|
||||||
|
self._samples_per_frame = int((sample_rate * frame_duration) / 1000)
|
||||||
|
session = krisp_audio.VadFloat.create(vad_cfg)
|
||||||
|
return session
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to create Krisp VAD session: {e}", exc_info=True)
|
||||||
|
raise RuntimeError(f"Failed to create Krisp VAD session: {e}") from e
|
||||||
|
|
||||||
|
def set_sample_rate(self, sample_rate: int):
|
||||||
|
"""Set the sample rate for audio processing.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sample_rate: Audio sample rate (must be 8000, 16000, 32000 or 48000 Hz).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If sample rate is not 8000, 16000, 32000 or 48000 Hz.
|
||||||
|
RuntimeError: If VAD session creation fails.
|
||||||
|
"""
|
||||||
|
if sample_rate != 48000 and sample_rate != 44100 and sample_rate != 32000 and sample_rate != 16000 and sample_rate != 8000:
|
||||||
|
raise ValueError(
|
||||||
|
f"Krisp VIVA VAD sample rate needs to be 8000, 16000, 32000, 44100 or 48000 (sample rate: {sample_rate})"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create or recreate session with new sample rate
|
||||||
|
try:
|
||||||
|
self._session = self._create_session(sample_rate, self._frame_duration_ms)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to set sample rate: {e}", exc_info=True)
|
||||||
|
raise RuntimeError(f"Failed to create Krisp VAD session: {e}") from e
|
||||||
|
|
||||||
|
super().set_sample_rate(sample_rate)
|
||||||
|
|
||||||
|
def num_frames_required(self) -> int:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def voice_confidence(self, buffer) -> float:
|
||||||
|
"""Calculate voice activity confidence for the given audio buffer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
buffer: Audio buffer to analyze (bytes, int16 format).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Voice confidence score between 0.0 and 1.0.
|
||||||
|
"""
|
||||||
|
if self._session is None:
|
||||||
|
logger.warning("VAD session not initialized. Cannot process audio.")
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Convert bytes buffer to float32 numpy array
|
||||||
|
# Buffer is int16 (2 bytes per sample), need to convert to float32
|
||||||
|
audio_int16 = np.frombuffer(buffer, dtype=np.int16)
|
||||||
|
# Normalize to [-1.0, 1.0] range
|
||||||
|
audio_float32 = audio_int16.astype(np.float32) / 32768.0
|
||||||
|
|
||||||
|
# Process through VAD session
|
||||||
|
voice_probability = self._session.process(audio_float32)
|
||||||
|
|
||||||
|
return voice_probability
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error analyzing audio with Krisp VIVA VAD: {e}", exc_info=True)
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
"""Cleanup when the analyzer is destroyed."""
|
||||||
|
try:
|
||||||
|
self._session = None
|
||||||
|
KrispVivaSDKManager.release()
|
||||||
|
except Exception:
|
||||||
|
# Ignore errors during cleanup
|
||||||
|
pass
|
||||||
Reference in New Issue
Block a user