Skip to content
Open
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
127 changes: 127 additions & 0 deletions docs/transport/krisp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Krisp Integration Guide

## 1. Download the Krisp SDK and Model

Download two files:

1. The Krisp Python SDK ZIP file
Example:

```
https://xxxxxxx-xrxxx-sdks.xxxx.xxxxxxxxxxs.com/krisp-audio-sdk-python-1.4.0.zip
```

2. The Krisp model ZIP file
Example:

```
https://xxxxxxx-xxxx-sdks.xxxx.xxxxxxxxxxs.xxx/krisp-viva-modelsxxxxx.zip
```

Create a directory to store the model:

```
/home/user/xxxxx/agents/src/piopiy/audio/krisp
```

Extract both the SDK and the model into this directory. You can do it somewhere else as well.

---

## 2. Install the Krisp Python Wheel

Inside the extracted SDK directory, navigate into `dist/` and locate the wheel file that matches your `platform` and `Python` version.

For example:

```
krisp_audio-1.4.0-cp311-cp311-linux_x86_64.whl
```

Install it locally:

```
pip install krisp_audio-1.4.0-cp311-cp311-linux_x86_64.whl
```

---

## 3. Set the Model Path in .env

Add the following entry inside your `.env` file:

```
KRISP_MODEL_PATH=<path-of-the-viva-model>
```

This path must point to the `.kef` model file inside the extracted Krisp model directory, as in `telecmi.py` it is taking the path from env file.

---

## 4. Code Changes Required

### 4.1 Add `krisp_viva_filter.py`

Create a new file at:

```
/home/user/xxxxx/agents/src/piopiy/audio/filters/krisp_viva_filter.py
```

Place the Krisp Viva filter implementation inside it.
This file defines a filter class implementing the Piopiy `BaseAudioFilter` interface so it can be used by the transport.

---

### 4.2 Modified VoiceAgent (enable Krisp)

The VoiceAgent is updated to:

* Accept new parameters:

* `enable_krisp`
* `krisp_suppression_level`
* Create a Krisp filter only when `enable_krisp=True`
* Insert the filter into `telecmi_params` before building the TeleCMI transport

This makes the filter optional and fully configurable from the application side.

---

### 4.3 Modified telecmi.py (TeleCMI Transport)

The TeleCMI Input Transport is updated to support a generic audio filter:

* Added support for `krisp_model_path` and initialising `krisp viva filter`
* The filter is started when the transport starts
* Incoming audio frames are run through the filter before being passed to the next step
* The filter is stopped on transport stop or cancel

---

### 4.3 Modify base_transport.py (BaseTransport)

The BaseTransport is updated to support a generic audio filter:

* Added support for `enable_krisp: bool = False` and `krisp_suppression_level: int = 30`
---


## 5. Enabling Krisp When Creating the Agent

When calling `voice_agent.Action(...)`, include the new parameters:

```
await voice_agent.Action(
stt=...,
llm=...,
tts=...,
vad=True,
allow_interruptions=True,
interruption_strategy=MinWordsInterruptionStrategy(min_words=1),
enable_krisp=True,
krisp_suppression_level=30
)
```

This enables Krisp, loads the model, and passes the filter into TeleCMI for live noise suppression.
193 changes: 193 additions & 0 deletions src/piopiy/audio/filters/krisp_viva_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
#
# Copyright (c) 2024–2025, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#

"""Krisp noise reduction audio filter for Pipecat.

This module provides an audio filter implementation using Krisp VIVA SDK.
"""

import os

import numpy as np
from loguru import logger

from piopiy.audio.filters.base_audio_filter import BaseAudioFilter
from piopiy.frames.frames import FilterControlFrame, FilterEnableFrame

try:
import krisp_audio
except ModuleNotFoundError as e:
logger.error(f"Exception: {e}")
logger.error("In order to use the Krisp filter, you need to install krisp_audio.")
raise Exception(f"Missing module: {e}")


def _log_callback(log_message, log_level):
logger.info(f"[{log_level}] {log_message}")


class KrispVivaFilter(BaseAudioFilter):
"""Audio filter using the Krisp VIVA SDK.

Provides real-time noise reduction for audio streams using Krisp's
proprietary noise suppression algorithms. This filter requires a
valid Krisp model file to operate.

Supported sample rates:
- 8000 Hz
- 16000 Hz
- 24000 Hz
- 32000 Hz
- 44100 Hz
- 48000 Hz
"""

# Initialize Krisp Audio SDK globally
krisp_audio.globalInit("", _log_callback, krisp_audio.LogLevel.Off)
SDK_VERSION = krisp_audio.getVersion()
logger.debug(
f"Krisp Audio Python SDK Version: {SDK_VERSION.major}."
f"{SDK_VERSION.minor}.{SDK_VERSION.patch}"
)

SAMPLE_RATES = {
8000: krisp_audio.SamplingRate.Sr8000Hz,
16000: krisp_audio.SamplingRate.Sr16000Hz,
24000: krisp_audio.SamplingRate.Sr24000Hz,
32000: krisp_audio.SamplingRate.Sr32000Hz,
44100: krisp_audio.SamplingRate.Sr44100Hz,
48000: krisp_audio.SamplingRate.Sr48000Hz,
}

FRAME_SIZE_MS = 10 # Krisp requires audio frames of 10ms duration for processing.

def __init__(self, model_path: str = None, noise_suppression_level: int = 10) -> None:
"""Initialize the Krisp noise reduction filter.

Args:
model_path: Path to the Krisp model file (.kef extension).
If None, uses KRISP_VIVA_MODEL_PATH environment variable.
noise_suppression_level: Noise suppression level.

Raises:
ValueError: If model_path is not provided and KRISP_VIVA_MODEL_PATH is not set.
Exception: If model file doesn't have .kef extension.
FileNotFoundError: If model file doesn't exist.
"""
super().__init__()

# Set model path, checking environment if not specified
self._model_path = model_path or os.getenv("KRISP_VIVA_MODEL_PATH")
if not self._model_path:
logger.error("Model path is not provided and KRISP_VIVA_MODEL_PATH is not set.")
raise ValueError("Model path for KrispAudioProcessor 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._filtering = True
self._session = None
self._samples_per_frame = None
self._noise_suppression_level = noise_suppression_level

# Audio buffer to accumulate samples for complete frames
self._audio_buffer = bytearray()

def _int_to_sample_rate(self, sample_rate):
"""Convert integer sample rate to krisp_audio SamplingRate enum.

Args:
sample_rate: Sample rate as integer

Returns:
krisp_audio.SamplingRate enum value

Raises:
ValueError: If sample rate is not supported
"""
if sample_rate not in self.SAMPLE_RATES:
raise ValueError("Unsupported sample rate")
return self.SAMPLE_RATES[sample_rate]

async def start(self, sample_rate: int):
"""Initialize the Krisp processor with the transport's sample rate.

Args:
sample_rate: The sample rate of the input transport in Hz.
"""
model_info = krisp_audio.ModelInfo()
model_info.path = self._model_path

nc_cfg = krisp_audio.NcSessionConfig()
nc_cfg.inputSampleRate = self._int_to_sample_rate(sample_rate)
nc_cfg.inputFrameDuration = krisp_audio.FrameDuration.Fd10ms
nc_cfg.outputSampleRate = nc_cfg.inputSampleRate
nc_cfg.modelInfo = model_info

self._samples_per_frame = int((sample_rate * self.FRAME_SIZE_MS) / 1000)
self._session = krisp_audio.NcInt16.create(nc_cfg)

async def stop(self):
"""Clean up the Krisp processor when stopping."""
self._session = None

async def process_frame(self, frame: FilterControlFrame):
"""Process control frames to enable/disable filtering.

Args:
frame: The control frame containing filter commands.
"""
if isinstance(frame, FilterEnableFrame):
self._filtering = frame.enable

async def filter(self, audio: bytes) -> bytes:
"""Apply Krisp noise reduction to audio data.

Args:
audio: Raw audio data as bytes to be filtered.

Returns:
Noise-reduced audio data as bytes.
"""
if not self._filtering:
return audio

# Add incoming audio to our buffer
self._audio_buffer.extend(audio)

# Calculate how many complete frames we can process
total_samples = len(self._audio_buffer) // 2 # 2 bytes per int16 sample
num_complete_frames = total_samples // self._samples_per_frame

if num_complete_frames == 0:
# Not enough samples for a complete frame yet, return empty
return b""

# Calculate how many bytes we need for complete frames
complete_samples_count = num_complete_frames * self._samples_per_frame
bytes_to_process = complete_samples_count * 2 # 2 bytes per sample

# Extract the bytes we can process
audio_to_process = bytes(self._audio_buffer[:bytes_to_process])

# Remove processed bytes from buffer, keep the remainder
self._audio_buffer = self._audio_buffer[bytes_to_process:]

# Process the complete frames
samples = np.frombuffer(audio_to_process, dtype=np.int16)
frames = samples.reshape(-1, self._samples_per_frame)
processed_samples = np.empty_like(samples)

for i, frame in enumerate(frames):
cleaned_frame = self._session.process(frame, self._noise_suppression_level)
processed_samples[i * self._samples_per_frame : (i + 1) * self._samples_per_frame] = (
cleaned_frame
)

return processed_samples.tobytes()
7 changes: 6 additions & 1 deletion src/piopiy/transports/base_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ class TransportParams(BaseModel):
"""Configuration parameters for transport implementations.

Parameters:
enable_krisp: Enable the krisp filter

krisp_suppression_level: How much suppression you need from krisp

camera_in_enabled: Enable camera input (deprecated, use video_in_enabled).

.. deprecated:: 0.0.66
Expand Down Expand Up @@ -115,7 +119,8 @@ class TransportParams(BaseModel):
"""

model_config = ConfigDict(arbitrary_types_allowed=True)

enable_krisp: bool = False
krisp_suppression_level: int = 30
camera_in_enabled: bool = False
camera_out_enabled: bool = False
camera_out_is_live: bool = False
Expand Down
Loading