From 35919a84e3b1637d84ad5f519d46b20451a02947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Sun, 11 Jan 2026 20:14:59 +0100 Subject: [PATCH 01/50] aic-sdk-py v2. # Conflicts: # uv.lock --- .../07zd-interruptible-aicoustics.py | 2 +- pyproject.toml | 3 +- src/pipecat/audio/filters/aic_filter.py | 19 +- src/pipecat/audio/filters/aic_filter_v2.py | 296 ++++++++++++++++++ src/pipecat/audio/utils.py | 57 ++++ src/pipecat/audio/vad/aic_vad.py | 18 +- src/pipecat/audio/vad/aic_vad_v2.py | 163 ++++++++++ 7 files changed, 541 insertions(+), 17 deletions(-) create mode 100644 src/pipecat/audio/filters/aic_filter_v2.py create mode 100644 src/pipecat/audio/vad/aic_vad_v2.py diff --git a/examples/foundational/07zd-interruptible-aicoustics.py b/examples/foundational/07zd-interruptible-aicoustics.py index 8a49e734f..7970af4a6 100644 --- a/examples/foundational/07zd-interruptible-aicoustics.py +++ b/examples/foundational/07zd-interruptible-aicoustics.py @@ -12,7 +12,7 @@ import wave from dotenv import load_dotenv from loguru import logger -from pipecat.audio.filters.aic_filter import AICFilter +from pipecat.audio.filters.aic_filter_v2 import AICFilter from pipecat.audio.turn.smart_turn.local_smart_turn_v3 import LocalSmartTurnAnalyzerV3 from pipecat.frames.frames import LLMRunFrame from pipecat.pipeline.pipeline import Pipeline diff --git a/pyproject.toml b/pyproject.toml index e99ab62bc..0f8d8c473 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,8 @@ Issues = "https://github.com/pipecat-ai/pipecat/issues" Changelog = "https://github.com/pipecat-ai/pipecat/blob/main/CHANGELOG.md" [project.optional-dependencies] -aic = [ "aic-sdk~=1.2.0" ] +aic = [ "aic-sdk>=1.2.0" ] +#aic = [ "aic-sdk @ file:///Users/goedev/Workspace/aic/pipecat/aic_sdk-2.0.0-cp313-cp313-macosx_11_0_arm64.whl" ] anthropic = [ "anthropic~=0.49.0" ] assemblyai = [ "pipecat-ai[websockets-base]" ] asyncai = [ "pipecat-ai[websockets-base]" ] diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index e4242521b..f37543551 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -4,11 +4,15 @@ # SPDX-License-Identifier: BSD 2-Clause License # -"""ai-coustics AIC SDK audio filter for Pipecat. +"""ai-coustics AIC SDK audio filter for Pipecat (aic-sdk < 2.0.0). This module provides an audio filter implementation using ai-coustics' AIC SDK to enhance audio streams in real time. It mirrors the structure of other filters like the Koala filter and integrates with Pipecat's input transport pipeline. + +.. note:: + This module is compatible with aic-sdk versions < 2.0.0. + For aic-sdk >= 2.0.0, use :mod:`pipecat.audio.filters.aic_filter_v2` instead. """ from typing import List, Optional @@ -17,15 +21,14 @@ import numpy as np from loguru import logger from pipecat.audio.filters.base_audio_filter import BaseAudioFilter +from pipecat.audio.utils import check_aic_sdk_version from pipecat.frames.frames import FilterControlFrame, FilterEnableFrame -try: - # AIC SDK (https://ai-coustics.github.io/aic-sdk-py/api/) - from aic import AICModelType, AICParameter, Model -except ModuleNotFoundError as e: - logger.error(f"Exception: {e}") - logger.error("In order to use the AIC filter, you need to `pip install pipecat-ai[aic]`.") - raise Exception(f"Missing module: {e}") +# Check aic-sdk is installed and version is compatible (< 2.0.0) +check_aic_sdk_version("v1") + +# AIC SDK (https://ai-coustics.github.io/aic-sdk-py/api/) +from aic import AICModelType, AICParameter, Model class AICFilter(BaseAudioFilter): diff --git a/src/pipecat/audio/filters/aic_filter_v2.py b/src/pipecat/audio/filters/aic_filter_v2.py new file mode 100644 index 000000000..be2912107 --- /dev/null +++ b/src/pipecat/audio/filters/aic_filter_v2.py @@ -0,0 +1,296 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""ai-coustics AIC SDK audio filter for Pipecat (aic-sdk >= 2.0.0). + +This module provides an audio filter implementation using ai-coustics' AIC SDK to +enhance audio streams in real time. It mirrors the structure of other filters like +the Koala filter and integrates with Pipecat's input transport pipeline. + +.. note:: + This module is compatible with aic-sdk versions >= 2.0.0. + For aic-sdk < 2.0.0, use :mod:`pipecat.audio.filters.aic_filter` instead. +""" + +import os +from typing import List, Optional + +import numpy as np +from loguru import logger + +from pipecat.audio.filters.base_audio_filter import BaseAudioFilter +from pipecat.audio.utils import check_aic_sdk_version +from pipecat.frames.frames import FilterControlFrame, FilterEnableFrame + +# Check aic-sdk is installed and version is compatible (>= 2.0.0) +check_aic_sdk_version("v2") + +# AIC SDK v2 (https://ai-coustics.github.io/aic-sdk-py/api/) +import aic +from aic import Model, ProcessorAsync, ProcessorConfig, ProcessorParameter, VadParameter + + +class AICFilter(BaseAudioFilter): + """Audio filter using ai-coustics' AIC SDK v2 for real-time enhancement. + + Buffers incoming audio to the model's preferred block size and processes + planar frames in-place using float32 samples in the linear -1..+1 range. + + .. note:: + This class requires aic-sdk >= 2.0.0. + """ + + def __init__( + self, + *, + license_key: str = "", + model_id: Optional[str] = None, + model_path: Optional[str] = None, + model_download_dir: Optional[str] = None, + enhancement_level: Optional[float] = 1.0, + voice_gain: Optional[float] = 1.0, + ) -> None: + """Initialize the AIC filter. + + Args: + license_key: ai-coustics license key for authentication. + model_id: Model identifier to download from CDN. Required if model_path + is not provided. See https://artifacts.ai-coustics.io/ for available models. + model_path: Optional path to a local .aicmodel file. If provided, + model_id is ignored and no download occurs. + model_download_dir: Directory for downloading models. Defaults to + a cache directory in user's home folder. + enhancement_level: Optional overall enhancement strength (0.0..1.0). + voice_gain: Optional linear gain applied to detected speech (0.1..4.0). + + Raises: + ValueError: If neither model_id nor model_path is provided. + """ + if model_id is None and model_path is None: + raise ValueError( + "Either 'model_id' or 'model_path' must be provided. " + "See https://artifacts.ai-coustics.io/ for available models." + ) + + self._license_key = license_key + self._model_id = model_id + self._model_path = model_path + self._model_download_dir = model_download_dir or os.path.expanduser( + "~/.cache/pipecat/aic-models" + ) + + self._enhancement_level = enhancement_level + self._voice_gain = voice_gain + + self._enabled = True + self._sample_rate = 0 + self._aic_ready = False + self._frames_per_block = 0 + self._audio_buffer = bytearray() + + # v2 API objects + self._model: Optional[Model] = None + self._processor: Optional[ProcessorAsync] = None + self._processor_ctx = None + self._vad_ctx = None + + def get_vad_context(self): + """Return the VAD context once the processor exists. + + Returns: + The VadContext instance bound to the underlying processor. + Raises RuntimeError if the processor has not been initialized. + """ + if self._vad_ctx is None: + raise RuntimeError("AIC processor not initialized yet. Call start(sample_rate) first.") + return self._vad_ctx + + def create_vad_analyzer( + self, + *, + speech_hold_duration: Optional[float] = None, + sensitivity: Optional[float] = None, + ): + """Return an analyzer that will lazily instantiate the AIC VAD when ready. + + AIC VAD parameters (v2): + - speech_hold_duration: + How long VAD continues detecting after speech ends (in seconds). + Range: 0.0 .. 20x model window length, Default (SDK): 0.05s + - sensitivity: + Energy threshold sensitivity. Energy threshold = 10 ** (-sensitivity). + Range: 1.0 .. 15.0, Default (SDK): 6.0 + + Args: + speech_hold_duration: Optional speech hold duration to configure on the VAD. + If None, SDK default (0.05s) is used. + sensitivity: Optional sensitivity (energy threshold) to configure on the VAD. + Range: 1.0 .. 15.0. If None, SDK default (6.0) is used. + + Returns: + A lazily-initialized AICVADAnalyzer that will bind to the VAD context + once the filter's processor has been created (after start(sample_rate)). + """ + from pipecat.audio.vad.aic_vad_v2 import AICVADAnalyzer + + return AICVADAnalyzer( + vad_context_factory=lambda: self.get_vad_context(), + speech_hold_duration=speech_hold_duration, + sensitivity=sensitivity, + ) + + async def start(self, sample_rate: int): + """Initialize the filter with the transport's sample rate. + + Args: + sample_rate: The sample rate of the input transport in Hz. + + Returns: + None + """ + self._sample_rate = sample_rate + + try: + # Load or download model + if self._model_path: + logger.debug(f"Loading AIC model from: {self._model_path}") + self._model = Model.from_file(self._model_path) + else: + logger.debug(f"Downloading AIC model: {self._model_id}") + os.makedirs(self._model_download_dir, exist_ok=True) + model_path = await Model.download_async(self._model_id, self._model_download_dir) + logger.debug(f"Model downloaded to: {model_path}") + self._model = Model.from_file(model_path) + + # Create async processor + self._processor = ProcessorAsync(self._model, self._license_key or "") + + # Get optimal frames for this sample rate + self._frames_per_block = self._model.get_optimal_num_frames(self._sample_rate) + + # Create configuration + config = ProcessorConfig( + sample_rate=self._sample_rate, + num_channels=1, + num_frames=self._frames_per_block, + allow_variable_frames=False, + ) + + # Initialize processor + await self._processor.initialize_async(config) + + # Get contexts for parameter control and VAD + self._processor_ctx = self._processor.get_processor_context() + self._vad_ctx = self._processor.get_vad_context() + + # Apply initial parameters + if self._enhancement_level is not None: + level = float(self._enhancement_level if self._enabled else 0.0) + self._processor_ctx.set_parameter(ProcessorParameter.EnhancementLevel, level) + if self._voice_gain is not None: + self._processor_ctx.set_parameter( + ProcessorParameter.VoiceGain, float(self._voice_gain) + ) + + self._aic_ready = True + + # Log processor information + logger.debug(f"ai-coustics filter (v2) started:") + logger.debug(f" Model ID: {self._model.get_id()}") + logger.debug(f" Sample rate: {self._sample_rate} Hz") + logger.debug(f" Frames per chunk: {self._frames_per_block}") + logger.debug(f" Enhancement strength: {int((self._enhancement_level or 1.0) * 100)}%") + logger.debug(f" Optimal sample rate: {self._model.get_optimal_sample_rate()} Hz") + logger.debug( + f" Output delay: {self._processor_ctx.get_output_delay()} samples " + f"({self._processor_ctx.get_output_delay() / self._sample_rate * 1000:.2f}ms)" + ) + except Exception as e: # noqa: BLE001 - surfacing SDK initialization errors + logger.error(f"AIC model initialization failed: {e}") + self._aic_ready = False + + async def stop(self): + """Clean up the AIC processor when stopping. + + Returns: + None + """ + try: + if self._processor_ctx is not None: + self._processor_ctx.reset() + finally: + self._processor = None + self._processor_ctx = None + self._vad_ctx = None + self._model = None + self._aic_ready = False + self._audio_buffer.clear() + + async def process_frame(self, frame: FilterControlFrame): + """Process control frames to enable/disable filtering. + + Args: + frame: The control frame containing filter commands. + + Returns: + None + """ + if isinstance(frame, FilterEnableFrame): + self._enabled = frame.enable + if self._processor_ctx is not None: + try: + level = float(self._enhancement_level if self._enabled else 0.0) + self._processor_ctx.set_parameter(ProcessorParameter.EnhancementLevel, level) + except Exception as e: # noqa: BLE001 + logger.error(f"AIC set_parameter failed: {e}") + + async def filter(self, audio: bytes) -> bytes: + """Apply AIC enhancement to audio data. + + Buffers incoming audio and processes it in chunks that match the AIC + model's required block length. Returns enhanced audio data. + + Args: + audio: Raw audio data as bytes to be filtered (int16 PCM, planar). + + Returns: + Enhanced audio data as bytes (int16 PCM, planar). + """ + if not self._aic_ready or self._processor is None: + return audio + + self._audio_buffer.extend(audio) + + filtered_chunks: List[bytes] = [] + + # Number of int16 samples currently buffered + available_frames = len(self._audio_buffer) // 2 + + while available_frames >= self._frames_per_block: + # Consume exactly one block worth of frames + samples_to_consume = self._frames_per_block * 1 + bytes_to_consume = samples_to_consume * 2 + block_bytes = bytes(self._audio_buffer[:bytes_to_consume]) + + # Convert to float32 in -1..+1 range and reshape to (channels, frames) + block_i16 = np.frombuffer(block_bytes, dtype=np.int16) + block_f32 = (block_i16.astype(np.float32) / 32768.0).reshape( + (1, self._frames_per_block) + ) + + # Process via async processor; returns ndarray (same shape) + out_f32 = await self._processor.process_async(block_f32) + + # Convert back to int16 bytes + out_i16 = np.clip(out_f32 * 32768.0, -32768, 32767).astype(np.int16) + filtered_chunks.append(out_i16.reshape(-1).tobytes()) + + # Slide buffer + self._audio_buffer = self._audio_buffer[bytes_to_consume:] + available_frames = len(self._audio_buffer) // 2 + + # Do not flush incomplete frames; keep them buffered for the next call + return b"".join(filtered_chunks) diff --git a/src/pipecat/audio/utils.py b/src/pipecat/audio/utils.py index 65f451675..002375f35 100644 --- a/src/pipecat/audio/utils.py +++ b/src/pipecat/audio/utils.py @@ -12,6 +12,9 @@ various audio formats used in Pipecat pipelines. """ import audioop +from typing import Literal + +from loguru import logger import numpy as np import pyloudnorm as pyln @@ -311,3 +314,57 @@ def is_silence(pcm_bytes: bytes) -> bool: # If max value is lower than SPEAKING_THRESHOLD, consider it as silence return max_value <= SPEAKING_THRESHOLD + + +def check_aic_sdk_version(required_version: Literal["v1", "v2"]) -> None: + """Check if the aic-sdk is installed and compatible with the module. + + This function checks both that the aic-sdk is installed and that its version + is compatible with the module requirements. + + Args: + required_version: Either "v1" (for aic-sdk < 2.0.0) or "v2" (for aic-sdk >= 2.0.0). + + Raises: + ImportError: If aic-sdk is not installed or version is incompatible. + """ + try: + import aic # noqa: F401 - check if module is installed + except ModuleNotFoundError as e: + logger.error(f"Exception: {e}") + logger.error("In order to use the AIC filter, you need to `pip install pipecat-ai[aic]`.") + raise ImportError(f"Missing module: {e}") from e + + try: + from importlib.metadata import version as get_version + + aic_version = get_version("aic-sdk") + major_version = int(aic_version.split(".")[0]) + + if required_version == "v1" and major_version >= 2: + error_msg = ( + f"aic-sdk version {aic_version} detected, but aic-sdk < 2.0.0 is required. " + "Please use the v2 modules instead: " + "'from pipecat.audio.filters.aic_filter_v2 import AICFilter' or " + "'from pipecat.audio.vad.aic_vad_v2 import AICVADAnalyzer'." + ) + logger.error(error_msg) + raise ImportError(error_msg) + + if required_version == "v2" and major_version < 2: + error_msg = ( + f"aic-sdk version {aic_version} detected, but aic-sdk >= 2.0.0 is required. " + "Please update with 'pip install --upgrade aic-sdk>=2.0.0' " + "or use the v1 modules: " + "'from pipecat.audio.filters.aic_filter import AICFilter' or " + "'from pipecat.audio.vad.aic_vad import AICVADAnalyzer'." + ) + logger.error(error_msg) + raise ImportError(error_msg) + + except ImportError: + # Re-raise if it's our version mismatch error + raise + except Exception: + # If we can't determine version for other reasons, log warning and allow to proceed + logger.warning("Could not determine aic-sdk version. Proceeding anyway.") diff --git a/src/pipecat/audio/vad/aic_vad.py b/src/pipecat/audio/vad/aic_vad.py index 4907e4f55..780b33cd8 100644 --- a/src/pipecat/audio/vad/aic_vad.py +++ b/src/pipecat/audio/vad/aic_vad.py @@ -1,22 +1,26 @@ -"""AIC-integrated VAD analyzer that lazily binds to the AIC SDK backend. +"""AIC-integrated VAD analyzer that lazily binds to the AIC SDK backend (aic-sdk < 2.0.0). This analyzer queries the backend's is_speech_detected() and maps it to a float confidence (1.0/0.0). It uses 10 ms windows based on the sample rate and applies optional AIC VAD parameters (lookback_buffer_size, sensitivity) when available. + +.. note:: + This module is compatible with aic-sdk versions < 2.0.0. + For aic-sdk >= 2.0.0, use :mod:`pipecat.audio.vad.aic_vad_v2` instead. """ from typing import Any, Callable, Optional from loguru import logger +from pipecat.audio.utils import check_aic_sdk_version from pipecat.audio.vad.vad_analyzer import VADAnalyzer, VADParams -try: - from aic import AICVadParameter -except ModuleNotFoundError as e: - logger.error(f"Exception: {e}") - logger.error("In order to use the AIC filter, you need to `pip install pipecat-ai[aic]`.") - raise Exception(f"Missing module: {e}") +# Check aic-sdk is installed and version is compatible (< 2.0.0) +check_aic_sdk_version("v1") + +from aic import AICVadParameter + class AICVADAnalyzer(VADAnalyzer): diff --git a/src/pipecat/audio/vad/aic_vad_v2.py b/src/pipecat/audio/vad/aic_vad_v2.py new file mode 100644 index 000000000..2f8129a0d --- /dev/null +++ b/src/pipecat/audio/vad/aic_vad_v2.py @@ -0,0 +1,163 @@ +"""AIC-integrated VAD analyzer that lazily binds to the AIC SDK v2 backend. + +This analyzer queries the backend's is_speech_detected() and maps it to a float +confidence (1.0/0.0). It uses 10 ms windows based on the sample rate and applies +optional AIC VAD parameters (speech_hold_duration, sensitivity) when available. + +.. note:: + This module is compatible with aic-sdk versions >= 2.0.0. + For aic-sdk < 2.0.0, use :mod:`pipecat.audio.vad.aic_vad` instead. +""" + +from typing import Any, Callable, Optional + +from loguru import logger + +from pipecat.audio.utils import check_aic_sdk_version +from pipecat.audio.vad.vad_analyzer import VADAnalyzer, VADParams + +# Check aic-sdk is installed and version is compatible (>= 2.0.0) +check_aic_sdk_version("v2") + +from aic import VadParameter + + + +class AICVADAnalyzer(VADAnalyzer): + """VAD analyzer that lazily binds to the AIC VadContext via a factory. + + The analyzer can be constructed before the AIC Processor exists. Once the filter has + started and the Processor is available, the provided factory will succeed and the + VadContext will be obtained. We then use the context's is_speech_detected() state + to derive confidence values. + + AIC VAD runtime parameters (v2): + - speech_hold_duration: + Controls for how long the VAD continues to detect speech after the audio signal + no longer contains speech (in seconds). + Range: 0.0 .. 20x model window length + Default (SDK): 0.05s (50ms) + - sensitivity: + Controls the energy threshold sensitivity. Higher values make the detector + less sensitive (require more energy to count as speech). + Range: 1.0 .. 15.0 + Formula: Energy threshold = 10 ** (-sensitivity) + Default (SDK): 6.0 + + .. note:: + This class requires aic-sdk >= 2.0.0. + """ + + def __init__( + self, + *, + vad_context_factory: Optional[Callable[[], Any]] = None, + speech_hold_duration: Optional[float] = None, + sensitivity: Optional[float] = None, + ): + """Create an AIC VAD analyzer. + + Args: + vad_context_factory: + Zero-arg callable that returns the AIC VadContext. + This may raise until the filter's Processor has been created; the analyzer + will retry on set_sample_rate/first use. + speech_hold_duration: + Optional override for AIC VAD speech hold duration (in seconds). + Range: 0.0 .. 20x model window length. + If None, the SDK default (0.05s) is used. + sensitivity: + Optional override for AIC VAD sensitivity (energy threshold). + Range: 1.0 .. 15.0. Energy threshold = 10 ** (-sensitivity). + If None, the SDK default (6.0) is used. + """ + # Use fixed VAD parameters for AIC: no user override + fixed_params = VADParams(confidence=0.5, start_secs=0.0, stop_secs=0.0, min_volume=0.0) + super().__init__(sample_rate=None, params=fixed_params) + self._vad_context_factory = vad_context_factory + self._vad_ctx: Optional[Any] = None + self._pending_speech_hold_duration: Optional[float] = speech_hold_duration + self._pending_sensitivity: Optional[float] = sensitivity + + def bind_vad_context_factory(self, vad_context_factory: Callable[[], Any]): + """Attach or replace the factory post-construction.""" + self._vad_context_factory = vad_context_factory + self._ensure_vad_context_initialized() + + def _apply_vad_params(self): + """Apply optional AIC VAD parameters if available.""" + if self._vad_ctx is None or VadParameter is None: + return + try: + if self._pending_speech_hold_duration is not None: + self._vad_ctx.set_parameter( + VadParameter.SpeechHoldDuration, float(self._pending_speech_hold_duration) + ) + if self._pending_sensitivity is not None: + self._vad_ctx.set_parameter( + VadParameter.Sensitivity, float(self._pending_sensitivity) + ) + except Exception as e: # noqa: BLE001 + logger.debug(f"AIC VAD parameter application deferred/failed: {e}") + + def _ensure_vad_context_initialized(self): + if self._vad_ctx is not None: + return + if not self._vad_context_factory: + return + try: + self._vad_ctx = self._vad_context_factory() + self._apply_vad_params() + # With VAD context ready, recompute internal frame sizing + super().set_params(self._params) + logger.debug("AIC VAD context (v2) initialized in analyzer.") + except Exception as e: # noqa: BLE001 + # Filter may not be started yet; try again later + logger.debug(f"Deferring AIC VAD context initialization: {e}") + + def set_sample_rate(self, sample_rate: int): + """Set the sample rate for audio processing. + + Args: + sample_rate: Audio sample rate in Hz. + """ + # Set rate and attempt VAD context initialization once we know SR + self._sample_rate = self._init_sample_rate or sample_rate + self._ensure_vad_context_initialized() + # Ensure params are initialized even if VAD context not ready yet + try: + super().set_params(self._params) + except Exception: + pass + + def num_frames_required(self) -> int: + """Get the number of audio frames required for analysis. + + Returns: + Number of frames needed for VAD processing. + """ + # Use 10 ms windows based on sample rate + return int(self.sample_rate * 0.01) if self.sample_rate > 0 else 160 + + def voice_confidence(self, buffer: bytes) -> float: + """Calculate voice activity confidence for the given audio buffer. + + Args: + buffer: Audio buffer to analyze. + + Returns: + Voice confidence score is 0.0 or 1.0. + """ + # Ensure VAD context exists (filter might have started since last call) + self._ensure_vad_context_initialized() + if self._vad_ctx is None: + return 0.0 + + # We do not need to analyze 'buffer' here since the processor's VAD is updated + # as part of the enhancement pipeline. Simply query the boolean and map it. + try: + is_speech = self._vad_ctx.is_speech_detected() + return 1.0 if is_speech else 0.0 + except Exception as e: # noqa: BLE001 + logger.error(f"AIC VAD inference error: {e}") + return 0.0 From a0d801b65882bfe1a65ac76b953192719d3ef406 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Wed, 14 Jan 2026 17:22:38 +0100 Subject: [PATCH 02/50] Remove outdated AIC Filter and VAD v2 files, migrate to consolidated implementations. Added the new ACIFilter to the same module. --- .../07zd-interruptible-aicoustics.py | 13 +- pyproject.toml | 1 - src/pipecat/audio/filters/aic_filter.py | 311 +++++++++++++++++- src/pipecat/audio/filters/aic_filter_v2.py | 296 ----------------- src/pipecat/audio/utils.py | 90 ++--- src/pipecat/audio/vad/aic_vad.py | 178 +++++++++- src/pipecat/audio/vad/aic_vad_v2.py | 163 --------- uv.lock | 75 ++--- 8 files changed, 556 insertions(+), 571 deletions(-) delete mode 100644 src/pipecat/audio/filters/aic_filter_v2.py delete mode 100644 src/pipecat/audio/vad/aic_vad_v2.py diff --git a/examples/foundational/07zd-interruptible-aicoustics.py b/examples/foundational/07zd-interruptible-aicoustics.py index 7970af4a6..479ae36b5 100644 --- a/examples/foundational/07zd-interruptible-aicoustics.py +++ b/examples/foundational/07zd-interruptible-aicoustics.py @@ -12,7 +12,7 @@ import wave from dotenv import load_dotenv from loguru import logger -from pipecat.audio.filters.aic_filter_v2 import AICFilter +from pipecat.audio.filters.aic_filter import AICFilterV2 from pipecat.audio.turn.smart_turn.local_smart_turn_v3 import LocalSmartTurnAnalyzerV3 from pipecat.frames.frames import LLMRunFrame from pipecat.pipeline.pipeline import Pipeline @@ -45,11 +45,12 @@ audiobuffer = AudioBufferProcessor( ) -def _create_aic_filter() -> AICFilter: +def _create_aic_filter() -> AICFilterV2: license_key = os.getenv("AICOUSTICS_LICENSE_KEY", "") - return AICFilter( + return AICFilterV2( license_key=license_key, + model_id="quail-xxs-48khz", enhancement_level=0.5, ) @@ -62,7 +63,7 @@ transport_params = { lambda aic: DailyParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=aic.create_vad_analyzer(lookback_buffer_size=6.0, sensitivity=6.0), + vad_analyzer=aic.create_vad_analyzer(speech_hold_duration=0.05, sensitivity=6.0), audio_in_filter=aic, ) )(_create_aic_filter()), @@ -70,7 +71,7 @@ transport_params = { lambda aic: FastAPIWebsocketParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=aic.create_vad_analyzer(lookback_buffer_size=6.0, sensitivity=6.0), + vad_analyzer=aic.create_vad_analyzer(speech_hold_duration=0.05, sensitivity=6.0), audio_in_filter=aic, ) )(_create_aic_filter()), @@ -78,7 +79,7 @@ transport_params = { lambda aic: TransportParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=aic.create_vad_analyzer(lookback_buffer_size=6.0, sensitivity=6.0), + vad_analyzer=aic.create_vad_analyzer(speech_hold_duration=0.05, sensitivity=6.0), audio_in_filter=aic, ) )(_create_aic_filter()), diff --git a/pyproject.toml b/pyproject.toml index 0f8d8c473..f209a4c64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,6 @@ Changelog = "https://github.com/pipecat-ai/pipecat/blob/main/CHANGELOG.md" [project.optional-dependencies] aic = [ "aic-sdk>=1.2.0" ] -#aic = [ "aic-sdk @ file:///Users/goedev/Workspace/aic/pipecat/aic_sdk-2.0.0-cp313-cp313-macosx_11_0_arm64.whl" ] anthropic = [ "anthropic~=0.49.0" ] assemblyai = [ "pipecat-ai[websockets-base]" ] asyncai = [ "pipecat-ai[websockets-base]" ] diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index f37543551..f71c09700 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -4,45 +4,43 @@ # SPDX-License-Identifier: BSD 2-Clause License # -"""ai-coustics AIC SDK audio filter for Pipecat (aic-sdk < 2.0.0). +"""ai-coustics AIC SDK audio filter for Pipecat. -This module provides an audio filter implementation using ai-coustics' AIC SDK to +This module provides audio filter implementations using ai-coustics' AIC SDK to enhance audio streams in real time. It mirrors the structure of other filters like the Koala filter and integrates with Pipecat's input transport pipeline. -.. note:: - This module is compatible with aic-sdk versions < 2.0.0. - For aic-sdk >= 2.0.0, use :mod:`pipecat.audio.filters.aic_filter_v2` instead. +Classes: + AICFilter: For aic-sdk < 2.0.0 (uses 'aic' module) + AICFilterV2: For aic-sdk >= 2.0.0 (uses 'aic_sdk' module) """ +import os from typing import List, Optional import numpy as np from loguru import logger from pipecat.audio.filters.base_audio_filter import BaseAudioFilter -from pipecat.audio.utils import check_aic_sdk_version from pipecat.frames.frames import FilterControlFrame, FilterEnableFrame -# Check aic-sdk is installed and version is compatible (< 2.0.0) -check_aic_sdk_version("v1") - -# AIC SDK (https://ai-coustics.github.io/aic-sdk-py/api/) -from aic import AICModelType, AICParameter, Model - class AICFilter(BaseAudioFilter): """Audio filter using ai-coustics' AIC SDK for real-time enhancement. Buffers incoming audio to the model's preferred block size and processes planar frames in-place using float32 samples in the linear -1..+1 range. + + .. note:: + This class requires aic-sdk < 2.0.0 (uses 'aic' module). + For aic-sdk >= 2.0.0, use :class:`AICFilterV2` instead. """ def __init__( self, *, license_key: str = "", - model_type: AICModelType = AICModelType.QUAIL_STT, + model_type: Optional["AICModelType"] = None, enhancement_level: Optional[float] = 1.0, voice_gain: Optional[float] = 1.0, noise_gate_enable: Optional[bool] = True, @@ -51,7 +49,7 @@ class AICFilter(BaseAudioFilter): Args: license_key: ai-coustics license key for authentication. - model_type: Model variant to load. + model_type: Model variant to load. If None, defaults to AICModelType.QUAIL_STT. enhancement_level: Optional overall enhancement strength (0.0..1.0). voice_gain: Optional linear gain applied to detected speech (0.0..4.0). noise_gate_enable: Optional enable/disable noise gate (default: True). @@ -60,8 +58,15 @@ class AICFilter(BaseAudioFilter): The `noise_gate_enable` parameter is deprecated and no longer has any effect. It will be removed in a future version. """ + from pipecat.audio.utils import check_aic_sdk_version + + check_aic_sdk_version("v1") + + # Import AIC SDK v1 types + from aic import AICModelType + self._license_key = license_key - self._model_type = model_type + self._model_type = model_type if model_type is not None else AICModelType.QUAIL_STT self._enhancement_level = enhancement_level self._voice_gain = voice_gain @@ -147,6 +152,8 @@ class AICFilter(BaseAudioFilter): Returns: None """ + from aic import AICParameter, Model + self._sample_rate = sample_rate try: @@ -208,6 +215,8 @@ class AICFilter(BaseAudioFilter): None """ if isinstance(frame, FilterEnableFrame): + from aic import AICParameter + self._enabled = frame.enable if self._aic is not None: try: @@ -263,3 +272,275 @@ class AICFilter(BaseAudioFilter): # Do not flush incomplete frames; keep them buffered for the next call return b"".join(filtered_chunks) + + +class AICFilterV2(BaseAudioFilter): + """Audio filter using ai-coustics' AIC SDK v2 for real-time enhancement. + + Buffers incoming audio to the model's preferred block size and processes + planar frames in-place using float32 samples in the linear -1..+1 range. + + .. note:: + This class requires aic-sdk >= 2.0.0 (uses 'aic_sdk' module). + For aic-sdk < 2.0.0, use :class:`AICFilter` instead. + """ + + def __init__( + self, + *, + license_key: str = "", + model_id: Optional[str] = None, + model_path: Optional[str] = None, + model_download_dir: Optional[str] = None, + enhancement_level: Optional[float] = 1.0, + voice_gain: Optional[float] = 1.0, + ) -> None: + """Initialize the AIC filter. + + Args: + license_key: ai-coustics license key for authentication. + model_id: Model identifier to download from CDN. Required if model_path + is not provided. See https://artifacts.ai-coustics.io/ for available models. + model_path: Optional path to a local .aicmodel file. If provided, + model_id is ignored and no download occurs. + model_download_dir: Directory for downloading models. Defaults to + a cache directory in user's home folder. + enhancement_level: Optional overall enhancement strength (0.0..1.0). + voice_gain: Optional linear gain applied to detected speech (0.1..4.0). + + Raises: + ValueError: If neither model_id nor model_path is provided. + """ + from pipecat.audio.utils import check_aic_sdk_version + + check_aic_sdk_version("v2") + + if model_id is None and model_path is None: + raise ValueError( + "Either 'model_id' or 'model_path' must be provided. " + "See https://artifacts.ai-coustics.io/ for available models." + ) + + self._license_key = license_key + self._model_id = model_id + self._model_path = model_path + self._model_download_dir = model_download_dir or os.path.expanduser( + "~/.cache/pipecat/aic-models" + ) + + self._enhancement_level = enhancement_level + self._voice_gain = voice_gain + + self._enabled = True + self._sample_rate = 0 + self._aic_ready = False + self._frames_per_block = 0 + self._audio_buffer = bytearray() + + # v2 API objects + self._model = None + self._processor = None + self._processor_ctx = None + self._vad_ctx = None + + def get_vad_context(self): + """Return the VAD context once the processor exists. + + Returns: + The VadContext instance bound to the underlying processor. + Raises RuntimeError if the processor has not been initialized. + """ + if self._vad_ctx is None: + raise RuntimeError("AIC processor not initialized yet. Call start(sample_rate) first.") + return self._vad_ctx + + def create_vad_analyzer( + self, + *, + speech_hold_duration: Optional[float] = None, + sensitivity: Optional[float] = None, + ): + """Return an analyzer that will lazily instantiate the AIC VAD when ready. + + AIC VAD parameters (v2): + - speech_hold_duration: + How long VAD continues detecting after speech ends (in seconds). + Range: 0.0 .. 20x model window length, Default (SDK): 0.05s + - sensitivity: + Energy threshold sensitivity. Energy threshold = 10 ** (-sensitivity). + Range: 1.0 .. 15.0, Default (SDK): 6.0 + + Args: + speech_hold_duration: Optional speech hold duration to configure on the VAD. + If None, SDK default (0.05s) is used. + sensitivity: Optional sensitivity (energy threshold) to configure on the VAD. + Range: 1.0 .. 15.0. If None, SDK default (6.0) is used. + + Returns: + A lazily-initialized AICVADAnalyzerV2 that will bind to the VAD context + once the filter's processor has been created (after start(sample_rate)). + """ + from pipecat.audio.vad.aic_vad import AICVADAnalyzerV2 + + return AICVADAnalyzerV2( + vad_context_factory=lambda: self.get_vad_context(), + speech_hold_duration=speech_hold_duration, + sensitivity=sensitivity, + ) + + async def start(self, sample_rate: int): + """Initialize the filter with the transport's sample rate. + + Args: + sample_rate: The sample rate of the input transport in Hz. + + Returns: + None + """ + from aic_sdk import Model, ProcessorAsync, ProcessorConfig, ProcessorParameter + + self._sample_rate = sample_rate + + try: + # Load or download model + if self._model_path: + logger.debug(f"Loading AIC model from: {self._model_path}") + self._model = Model.from_file(self._model_path) + else: + logger.debug(f"Downloading AIC model: {self._model_id}") + os.makedirs(self._model_download_dir, exist_ok=True) + model_path = await Model.download_async(self._model_id, self._model_download_dir) + logger.debug(f"Model downloaded to: {model_path}") + self._model = Model.from_file(model_path) + + # Create async processor + self._processor = ProcessorAsync(self._model, self._license_key or "") + + # Get optimal frames for this sample rate + self._frames_per_block = self._model.get_optimal_num_frames(self._sample_rate) + + # Create configuration + config = ProcessorConfig( + sample_rate=self._sample_rate, + num_channels=1, + num_frames=self._frames_per_block, + allow_variable_frames=False, + ) + + # Initialize processor + await self._processor.initialize_async(config) + + # Get contexts for parameter control and VAD + self._processor_ctx = self._processor.get_processor_context() + self._vad_ctx = self._processor.get_vad_context() + + # Apply initial parameters + if self._enhancement_level is not None: + level = float(self._enhancement_level if self._enabled else 0.0) + self._processor_ctx.set_parameter(ProcessorParameter.EnhancementLevel, level) + if self._voice_gain is not None: + self._processor_ctx.set_parameter( + ProcessorParameter.VoiceGain, float(self._voice_gain) + ) + + self._aic_ready = True + + # Log processor information + logger.debug(f"ai-coustics filter (v2) started:") + logger.debug(f" Model ID: {self._model.get_id()}") + logger.debug(f" Sample rate: {self._sample_rate} Hz") + logger.debug(f" Frames per chunk: {self._frames_per_block}") + logger.debug(f" Enhancement strength: {int((self._enhancement_level or 1.0) * 100)}%") + logger.debug(f" Optimal sample rate: {self._model.get_optimal_sample_rate()} Hz") + logger.debug( + f" Output delay: {self._processor_ctx.get_output_delay()} samples " + f"({self._processor_ctx.get_output_delay() / self._sample_rate * 1000:.2f}ms)" + ) + except Exception as e: # noqa: BLE001 - surfacing SDK initialization errors + logger.error(f"AIC model initialization failed: {e}") + self._aic_ready = False + + async def stop(self): + """Clean up the AIC processor when stopping. + + Returns: + None + """ + try: + if self._processor_ctx is not None: + self._processor_ctx.reset() + finally: + self._processor = None + self._processor_ctx = None + self._vad_ctx = None + self._model = None + self._aic_ready = False + self._audio_buffer.clear() + + async def process_frame(self, frame: FilterControlFrame): + """Process control frames to enable/disable filtering. + + Args: + frame: The control frame containing filter commands. + + Returns: + None + """ + if isinstance(frame, FilterEnableFrame): + from aic_sdk import ProcessorParameter + + self._enabled = frame.enable + if self._processor_ctx is not None: + try: + level = float(self._enhancement_level if self._enabled else 0.0) + self._processor_ctx.set_parameter(ProcessorParameter.EnhancementLevel, level) + except Exception as e: # noqa: BLE001 + logger.error(f"AIC set_parameter failed: {e}") + + async def filter(self, audio: bytes) -> bytes: + """Apply AIC enhancement to audio data. + + Buffers incoming audio and processes it in chunks that match the AIC + model's required block length. Returns enhanced audio data. + + Args: + audio: Raw audio data as bytes to be filtered (int16 PCM, planar). + + Returns: + Enhanced audio data as bytes (int16 PCM, planar). + """ + if not self._aic_ready or self._processor is None: + return audio + + self._audio_buffer.extend(audio) + + filtered_chunks: List[bytes] = [] + + # Number of int16 samples currently buffered + available_frames = len(self._audio_buffer) // 2 + + while available_frames >= self._frames_per_block: + # Consume exactly one block worth of frames + samples_to_consume = self._frames_per_block * 1 + bytes_to_consume = samples_to_consume * 2 + block_bytes = bytes(self._audio_buffer[:bytes_to_consume]) + + # Convert to float32 in -1..+1 range and reshape to (channels, frames) + block_i16 = np.frombuffer(block_bytes, dtype=np.int16) + block_f32 = (block_i16.astype(np.float32) / 32768.0).reshape( + (1, self._frames_per_block) + ) + + # Process via async processor; returns ndarray (same shape) + out_f32 = await self._processor.process_async(block_f32) + + # Convert back to int16 bytes + out_i16 = np.clip(out_f32 * 32768.0, -32768, 32767).astype(np.int16) + filtered_chunks.append(out_i16.reshape(-1).tobytes()) + + # Slide buffer + self._audio_buffer = self._audio_buffer[bytes_to_consume:] + available_frames = len(self._audio_buffer) // 2 + + # Do not flush incomplete frames; keep them buffered for the next call + return b"".join(filtered_chunks) diff --git a/src/pipecat/audio/filters/aic_filter_v2.py b/src/pipecat/audio/filters/aic_filter_v2.py deleted file mode 100644 index be2912107..000000000 --- a/src/pipecat/audio/filters/aic_filter_v2.py +++ /dev/null @@ -1,296 +0,0 @@ -# -# Copyright (c) 2024-2026, Daily -# -# SPDX-License-Identifier: BSD 2-Clause License -# - -"""ai-coustics AIC SDK audio filter for Pipecat (aic-sdk >= 2.0.0). - -This module provides an audio filter implementation using ai-coustics' AIC SDK to -enhance audio streams in real time. It mirrors the structure of other filters like -the Koala filter and integrates with Pipecat's input transport pipeline. - -.. note:: - This module is compatible with aic-sdk versions >= 2.0.0. - For aic-sdk < 2.0.0, use :mod:`pipecat.audio.filters.aic_filter` instead. -""" - -import os -from typing import List, Optional - -import numpy as np -from loguru import logger - -from pipecat.audio.filters.base_audio_filter import BaseAudioFilter -from pipecat.audio.utils import check_aic_sdk_version -from pipecat.frames.frames import FilterControlFrame, FilterEnableFrame - -# Check aic-sdk is installed and version is compatible (>= 2.0.0) -check_aic_sdk_version("v2") - -# AIC SDK v2 (https://ai-coustics.github.io/aic-sdk-py/api/) -import aic -from aic import Model, ProcessorAsync, ProcessorConfig, ProcessorParameter, VadParameter - - -class AICFilter(BaseAudioFilter): - """Audio filter using ai-coustics' AIC SDK v2 for real-time enhancement. - - Buffers incoming audio to the model's preferred block size and processes - planar frames in-place using float32 samples in the linear -1..+1 range. - - .. note:: - This class requires aic-sdk >= 2.0.0. - """ - - def __init__( - self, - *, - license_key: str = "", - model_id: Optional[str] = None, - model_path: Optional[str] = None, - model_download_dir: Optional[str] = None, - enhancement_level: Optional[float] = 1.0, - voice_gain: Optional[float] = 1.0, - ) -> None: - """Initialize the AIC filter. - - Args: - license_key: ai-coustics license key for authentication. - model_id: Model identifier to download from CDN. Required if model_path - is not provided. See https://artifacts.ai-coustics.io/ for available models. - model_path: Optional path to a local .aicmodel file. If provided, - model_id is ignored and no download occurs. - model_download_dir: Directory for downloading models. Defaults to - a cache directory in user's home folder. - enhancement_level: Optional overall enhancement strength (0.0..1.0). - voice_gain: Optional linear gain applied to detected speech (0.1..4.0). - - Raises: - ValueError: If neither model_id nor model_path is provided. - """ - if model_id is None and model_path is None: - raise ValueError( - "Either 'model_id' or 'model_path' must be provided. " - "See https://artifacts.ai-coustics.io/ for available models." - ) - - self._license_key = license_key - self._model_id = model_id - self._model_path = model_path - self._model_download_dir = model_download_dir or os.path.expanduser( - "~/.cache/pipecat/aic-models" - ) - - self._enhancement_level = enhancement_level - self._voice_gain = voice_gain - - self._enabled = True - self._sample_rate = 0 - self._aic_ready = False - self._frames_per_block = 0 - self._audio_buffer = bytearray() - - # v2 API objects - self._model: Optional[Model] = None - self._processor: Optional[ProcessorAsync] = None - self._processor_ctx = None - self._vad_ctx = None - - def get_vad_context(self): - """Return the VAD context once the processor exists. - - Returns: - The VadContext instance bound to the underlying processor. - Raises RuntimeError if the processor has not been initialized. - """ - if self._vad_ctx is None: - raise RuntimeError("AIC processor not initialized yet. Call start(sample_rate) first.") - return self._vad_ctx - - def create_vad_analyzer( - self, - *, - speech_hold_duration: Optional[float] = None, - sensitivity: Optional[float] = None, - ): - """Return an analyzer that will lazily instantiate the AIC VAD when ready. - - AIC VAD parameters (v2): - - speech_hold_duration: - How long VAD continues detecting after speech ends (in seconds). - Range: 0.0 .. 20x model window length, Default (SDK): 0.05s - - sensitivity: - Energy threshold sensitivity. Energy threshold = 10 ** (-sensitivity). - Range: 1.0 .. 15.0, Default (SDK): 6.0 - - Args: - speech_hold_duration: Optional speech hold duration to configure on the VAD. - If None, SDK default (0.05s) is used. - sensitivity: Optional sensitivity (energy threshold) to configure on the VAD. - Range: 1.0 .. 15.0. If None, SDK default (6.0) is used. - - Returns: - A lazily-initialized AICVADAnalyzer that will bind to the VAD context - once the filter's processor has been created (after start(sample_rate)). - """ - from pipecat.audio.vad.aic_vad_v2 import AICVADAnalyzer - - return AICVADAnalyzer( - vad_context_factory=lambda: self.get_vad_context(), - speech_hold_duration=speech_hold_duration, - sensitivity=sensitivity, - ) - - async def start(self, sample_rate: int): - """Initialize the filter with the transport's sample rate. - - Args: - sample_rate: The sample rate of the input transport in Hz. - - Returns: - None - """ - self._sample_rate = sample_rate - - try: - # Load or download model - if self._model_path: - logger.debug(f"Loading AIC model from: {self._model_path}") - self._model = Model.from_file(self._model_path) - else: - logger.debug(f"Downloading AIC model: {self._model_id}") - os.makedirs(self._model_download_dir, exist_ok=True) - model_path = await Model.download_async(self._model_id, self._model_download_dir) - logger.debug(f"Model downloaded to: {model_path}") - self._model = Model.from_file(model_path) - - # Create async processor - self._processor = ProcessorAsync(self._model, self._license_key or "") - - # Get optimal frames for this sample rate - self._frames_per_block = self._model.get_optimal_num_frames(self._sample_rate) - - # Create configuration - config = ProcessorConfig( - sample_rate=self._sample_rate, - num_channels=1, - num_frames=self._frames_per_block, - allow_variable_frames=False, - ) - - # Initialize processor - await self._processor.initialize_async(config) - - # Get contexts for parameter control and VAD - self._processor_ctx = self._processor.get_processor_context() - self._vad_ctx = self._processor.get_vad_context() - - # Apply initial parameters - if self._enhancement_level is not None: - level = float(self._enhancement_level if self._enabled else 0.0) - self._processor_ctx.set_parameter(ProcessorParameter.EnhancementLevel, level) - if self._voice_gain is not None: - self._processor_ctx.set_parameter( - ProcessorParameter.VoiceGain, float(self._voice_gain) - ) - - self._aic_ready = True - - # Log processor information - logger.debug(f"ai-coustics filter (v2) started:") - logger.debug(f" Model ID: {self._model.get_id()}") - logger.debug(f" Sample rate: {self._sample_rate} Hz") - logger.debug(f" Frames per chunk: {self._frames_per_block}") - logger.debug(f" Enhancement strength: {int((self._enhancement_level or 1.0) * 100)}%") - logger.debug(f" Optimal sample rate: {self._model.get_optimal_sample_rate()} Hz") - logger.debug( - f" Output delay: {self._processor_ctx.get_output_delay()} samples " - f"({self._processor_ctx.get_output_delay() / self._sample_rate * 1000:.2f}ms)" - ) - except Exception as e: # noqa: BLE001 - surfacing SDK initialization errors - logger.error(f"AIC model initialization failed: {e}") - self._aic_ready = False - - async def stop(self): - """Clean up the AIC processor when stopping. - - Returns: - None - """ - try: - if self._processor_ctx is not None: - self._processor_ctx.reset() - finally: - self._processor = None - self._processor_ctx = None - self._vad_ctx = None - self._model = None - self._aic_ready = False - self._audio_buffer.clear() - - async def process_frame(self, frame: FilterControlFrame): - """Process control frames to enable/disable filtering. - - Args: - frame: The control frame containing filter commands. - - Returns: - None - """ - if isinstance(frame, FilterEnableFrame): - self._enabled = frame.enable - if self._processor_ctx is not None: - try: - level = float(self._enhancement_level if self._enabled else 0.0) - self._processor_ctx.set_parameter(ProcessorParameter.EnhancementLevel, level) - except Exception as e: # noqa: BLE001 - logger.error(f"AIC set_parameter failed: {e}") - - async def filter(self, audio: bytes) -> bytes: - """Apply AIC enhancement to audio data. - - Buffers incoming audio and processes it in chunks that match the AIC - model's required block length. Returns enhanced audio data. - - Args: - audio: Raw audio data as bytes to be filtered (int16 PCM, planar). - - Returns: - Enhanced audio data as bytes (int16 PCM, planar). - """ - if not self._aic_ready or self._processor is None: - return audio - - self._audio_buffer.extend(audio) - - filtered_chunks: List[bytes] = [] - - # Number of int16 samples currently buffered - available_frames = len(self._audio_buffer) // 2 - - while available_frames >= self._frames_per_block: - # Consume exactly one block worth of frames - samples_to_consume = self._frames_per_block * 1 - bytes_to_consume = samples_to_consume * 2 - block_bytes = bytes(self._audio_buffer[:bytes_to_consume]) - - # Convert to float32 in -1..+1 range and reshape to (channels, frames) - block_i16 = np.frombuffer(block_bytes, dtype=np.int16) - block_f32 = (block_i16.astype(np.float32) / 32768.0).reshape( - (1, self._frames_per_block) - ) - - # Process via async processor; returns ndarray (same shape) - out_f32 = await self._processor.process_async(block_f32) - - # Convert back to int16 bytes - out_i16 = np.clip(out_f32 * 32768.0, -32768, 32767).astype(np.int16) - filtered_chunks.append(out_i16.reshape(-1).tobytes()) - - # Slide buffer - self._audio_buffer = self._audio_buffer[bytes_to_consume:] - available_frames = len(self._audio_buffer) // 2 - - # Do not flush incomplete frames; keep them buffered for the next call - return b"".join(filtered_chunks) diff --git a/src/pipecat/audio/utils.py b/src/pipecat/audio/utils.py index 002375f35..c61aba4dd 100644 --- a/src/pipecat/audio/utils.py +++ b/src/pipecat/audio/utils.py @@ -316,11 +316,41 @@ def is_silence(pcm_bytes: bytes) -> bool: return max_value <= SPEAKING_THRESHOLD +def is_aic_sdk_v2() -> bool: + """Detect if aic-sdk v2 is installed by checking the module name. + + In v2, the module was renamed from 'aic' to 'aic_sdk'. + + Returns: + True if aic-sdk v2 (aic_sdk module) is installed, False if v1 (aic module). + + Raises: + ImportError: If neither aic nor aic_sdk module is installed. + """ + try: + import aic_sdk # noqa: F401 + + return True + except ModuleNotFoundError: + pass + + try: + import aic # noqa: F401 + + return False + except ModuleNotFoundError: + logger.error("In order to use the AIC filter, you need to `pip install pipecat-ai[aic]`.") + raise ImportError( + "aic-sdk is not installed. Install with 'pip install pipecat-ai[aic]'." + ) + + def check_aic_sdk_version(required_version: Literal["v1", "v2"]) -> None: """Check if the aic-sdk is installed and compatible with the module. This function checks both that the aic-sdk is installed and that its version - is compatible with the module requirements. + is compatible with the module requirements. Version detection is based on + the module name: v2 uses 'aic_sdk', v1 uses 'aic'. Args: required_version: Either "v1" (for aic-sdk < 2.0.0) or "v2" (for aic-sdk >= 2.0.0). @@ -328,43 +358,25 @@ def check_aic_sdk_version(required_version: Literal["v1", "v2"]) -> None: Raises: ImportError: If aic-sdk is not installed or version is incompatible. """ - try: - import aic # noqa: F401 - check if module is installed - except ModuleNotFoundError as e: - logger.error(f"Exception: {e}") - logger.error("In order to use the AIC filter, you need to `pip install pipecat-ai[aic]`.") - raise ImportError(f"Missing module: {e}") from e + is_v2 = is_aic_sdk_v2() - try: - from importlib.metadata import version as get_version + if required_version == "v1" and is_v2: + error_msg = ( + "aic-sdk v2 (aic_sdk module) detected, but v1 (aic module) is required. " + "Please use the v2 classes instead: " + "'from pipecat.audio.filters.aic_filter import AICFilterV2' or " + "'from pipecat.audio.vad.aic_vad import AICVADAnalyzerV2'." + ) + logger.error(error_msg) + raise ImportError(error_msg) - aic_version = get_version("aic-sdk") - major_version = int(aic_version.split(".")[0]) - - if required_version == "v1" and major_version >= 2: - error_msg = ( - f"aic-sdk version {aic_version} detected, but aic-sdk < 2.0.0 is required. " - "Please use the v2 modules instead: " - "'from pipecat.audio.filters.aic_filter_v2 import AICFilter' or " - "'from pipecat.audio.vad.aic_vad_v2 import AICVADAnalyzer'." - ) - logger.error(error_msg) - raise ImportError(error_msg) - - if required_version == "v2" and major_version < 2: - error_msg = ( - f"aic-sdk version {aic_version} detected, but aic-sdk >= 2.0.0 is required. " - "Please update with 'pip install --upgrade aic-sdk>=2.0.0' " - "or use the v1 modules: " - "'from pipecat.audio.filters.aic_filter import AICFilter' or " - "'from pipecat.audio.vad.aic_vad import AICVADAnalyzer'." - ) - logger.error(error_msg) - raise ImportError(error_msg) - - except ImportError: - # Re-raise if it's our version mismatch error - raise - except Exception: - # If we can't determine version for other reasons, log warning and allow to proceed - logger.warning("Could not determine aic-sdk version. Proceeding anyway.") + if required_version == "v2" and not is_v2: + error_msg = ( + "aic-sdk v1 (aic module) detected, but v2 (aic_sdk module) is required. " + "Please update with 'pip install --upgrade aic-sdk>=2.0.0' " + "or use the v1 classes: " + "'from pipecat.audio.filters.aic_filter import AICFilter' or " + "'from pipecat.audio.vad.aic_vad import AICVADAnalyzer'." + ) + logger.error(error_msg) + raise ImportError(error_msg) diff --git a/src/pipecat/audio/vad/aic_vad.py b/src/pipecat/audio/vad/aic_vad.py index 780b33cd8..9ff19feec 100644 --- a/src/pipecat/audio/vad/aic_vad.py +++ b/src/pipecat/audio/vad/aic_vad.py @@ -1,27 +1,20 @@ -"""AIC-integrated VAD analyzer that lazily binds to the AIC SDK backend (aic-sdk < 2.0.0). +"""AIC-integrated VAD analyzer that lazily binds to the AIC SDK backend. -This analyzer queries the backend's is_speech_detected() and maps it to a float -confidence (1.0/0.0). It uses 10 ms windows based on the sample rate and applies -optional AIC VAD parameters (lookback_buffer_size, sensitivity) when available. +This module provides VAD analyzer implementations that query the AIC SDK's +is_speech_detected() and map it to a float confidence (1.0/0.0). They use +10 ms windows based on the sample rate and apply optional AIC VAD parameters. -.. note:: - This module is compatible with aic-sdk versions < 2.0.0. - For aic-sdk >= 2.0.0, use :mod:`pipecat.audio.vad.aic_vad_v2` instead. +Classes: + AICVADAnalyzer: For aic-sdk < 2.0.0 (uses 'aic' module) + AICVADAnalyzerV2: For aic-sdk >= 2.0.0 (uses 'aic_sdk' module) """ from typing import Any, Callable, Optional from loguru import logger -from pipecat.audio.utils import check_aic_sdk_version from pipecat.audio.vad.vad_analyzer import VADAnalyzer, VADParams -# Check aic-sdk is installed and version is compatible (< 2.0.0) -check_aic_sdk_version("v1") - -from aic import AICVadParameter - - class AICVADAnalyzer(VADAnalyzer): """VAD analyzer that lazily instantiates the AIC VoiceActivityDetector via a factory. @@ -45,6 +38,10 @@ class AICVADAnalyzer(VADAnalyzer): Range: 1.0 .. 15.0 Formula: Energy threshold = 10 ** (-sensitivity) Default (SDK): 6.0 + + .. note:: + This class requires aic-sdk < 2.0.0 (uses 'aic' module). + For aic-sdk >= 2.0.0, use :class:`AICVADAnalyzerV2` instead. """ def __init__( @@ -70,6 +67,10 @@ class AICVADAnalyzer(VADAnalyzer): Range: 1.0 .. 15.0. Energy threshold = 10 ** (-sensitivity). If None, the SDK default (6.0) is used. """ + from pipecat.audio.utils import check_aic_sdk_version + + check_aic_sdk_version("v1") + # Use fixed VAD parameters for AIC: no user override fixed_params = VADParams(confidence=0.5, start_secs=0.0, stop_secs=0.0, min_volume=0.0) super().__init__(sample_rate=None, params=fixed_params) @@ -85,6 +86,8 @@ class AICVADAnalyzer(VADAnalyzer): def _apply_backend_params(self): """Apply optional AIC VAD parameters if available.""" + from aic import AICVadParameter + if self._backend_vad is None or AICVadParameter is None: return try: @@ -160,3 +163,150 @@ class AICVADAnalyzer(VADAnalyzer): except Exception as e: # noqa: BLE001 logger.error(f"AIC VAD inference error: {e}") return 0.0 + + +class AICVADAnalyzerV2(VADAnalyzer): + """VAD analyzer that lazily binds to the AIC VadContext via a factory. + + The analyzer can be constructed before the AIC Processor exists. Once the filter has + started and the Processor is available, the provided factory will succeed and the + VadContext will be obtained. We then use the context's is_speech_detected() state + to derive confidence values. + + AIC VAD runtime parameters (v2): + - speech_hold_duration: + Controls for how long the VAD continues to detect speech after the audio signal + no longer contains speech (in seconds). + Range: 0.0 .. 20x model window length + Default (SDK): 0.05s (50ms) + - sensitivity: + Controls the energy threshold sensitivity. Higher values make the detector + less sensitive (require more energy to count as speech). + Range: 1.0 .. 15.0 + Formula: Energy threshold = 10 ** (-sensitivity) + Default (SDK): 6.0 + + .. note:: + This class requires aic-sdk >= 2.0.0 (uses 'aic_sdk' module). + For aic-sdk < 2.0.0, use :class:`AICVADAnalyzer` instead. + """ + + def __init__( + self, + *, + vad_context_factory: Optional[Callable[[], Any]] = None, + speech_hold_duration: Optional[float] = None, + sensitivity: Optional[float] = None, + ): + """Create an AIC VAD analyzer. + + Args: + vad_context_factory: + Zero-arg callable that returns the AIC VadContext. + This may raise until the filter's Processor has been created; the analyzer + will retry on set_sample_rate/first use. + speech_hold_duration: + Optional override for AIC VAD speech hold duration (in seconds). + Range: 0.0 .. 20x model window length. + If None, the SDK default (0.05s) is used. + sensitivity: + Optional override for AIC VAD sensitivity (energy threshold). + Range: 1.0 .. 15.0. Energy threshold = 10 ** (-sensitivity). + If None, the SDK default (6.0) is used. + """ + from pipecat.audio.utils import check_aic_sdk_version + + check_aic_sdk_version("v2") + + # Use fixed VAD parameters for AIC: no user override + fixed_params = VADParams(confidence=0.5, start_secs=0.0, stop_secs=0.0, min_volume=0.0) + super().__init__(sample_rate=None, params=fixed_params) + self._vad_context_factory = vad_context_factory + self._vad_ctx: Optional[Any] = None + self._pending_speech_hold_duration: Optional[float] = speech_hold_duration + self._pending_sensitivity: Optional[float] = sensitivity + + def bind_vad_context_factory(self, vad_context_factory: Callable[[], Any]): + """Attach or replace the factory post-construction.""" + self._vad_context_factory = vad_context_factory + self._ensure_vad_context_initialized() + + def _apply_vad_params(self): + """Apply optional AIC VAD parameters if available.""" + from aic_sdk import VadParameter + + if self._vad_ctx is None or VadParameter is None: + return + try: + if self._pending_speech_hold_duration is not None: + self._vad_ctx.set_parameter( + VadParameter.SpeechHoldDuration, float(self._pending_speech_hold_duration) + ) + if self._pending_sensitivity is not None: + self._vad_ctx.set_parameter( + VadParameter.Sensitivity, float(self._pending_sensitivity) + ) + except Exception as e: # noqa: BLE001 + logger.debug(f"AIC VAD parameter application deferred/failed: {e}") + + def _ensure_vad_context_initialized(self): + if self._vad_ctx is not None: + return + if not self._vad_context_factory: + return + try: + self._vad_ctx = self._vad_context_factory() + self._apply_vad_params() + # With VAD context ready, recompute internal frame sizing + super().set_params(self._params) + logger.debug("AIC VAD context (v2) initialized in analyzer.") + except Exception as e: # noqa: BLE001 + # Filter may not be started yet; try again later + logger.debug(f"Deferring AIC VAD context initialization: {e}") + + def set_sample_rate(self, sample_rate: int): + """Set the sample rate for audio processing. + + Args: + sample_rate: Audio sample rate in Hz. + """ + # Set rate and attempt VAD context initialization once we know SR + self._sample_rate = self._init_sample_rate or sample_rate + self._ensure_vad_context_initialized() + # Ensure params are initialized even if VAD context not ready yet + try: + super().set_params(self._params) + except Exception: + pass + + def num_frames_required(self) -> int: + """Get the number of audio frames required for analysis. + + Returns: + Number of frames needed for VAD processing. + """ + # Use 10 ms windows based on sample rate + return int(self.sample_rate * 0.01) if self.sample_rate > 0 else 160 + + def voice_confidence(self, buffer: bytes) -> float: + """Calculate voice activity confidence for the given audio buffer. + + Args: + buffer: Audio buffer to analyze. + + Returns: + Voice confidence score is 0.0 or 1.0. + """ + # Ensure VAD context exists (filter might have started since last call) + self._ensure_vad_context_initialized() + if self._vad_ctx is None: + return 0.0 + + # We do not need to analyze 'buffer' here since the processor's VAD is updated + # as part of the enhancement pipeline. Simply query the boolean and map it. + try: + is_speech = self._vad_ctx.is_speech_detected() + return 1.0 if is_speech else 0.0 + except Exception as e: # noqa: BLE001 + logger.error(f"AIC VAD inference error: {e}") + return 0.0 diff --git a/src/pipecat/audio/vad/aic_vad_v2.py b/src/pipecat/audio/vad/aic_vad_v2.py deleted file mode 100644 index 2f8129a0d..000000000 --- a/src/pipecat/audio/vad/aic_vad_v2.py +++ /dev/null @@ -1,163 +0,0 @@ -"""AIC-integrated VAD analyzer that lazily binds to the AIC SDK v2 backend. - -This analyzer queries the backend's is_speech_detected() and maps it to a float -confidence (1.0/0.0). It uses 10 ms windows based on the sample rate and applies -optional AIC VAD parameters (speech_hold_duration, sensitivity) when available. - -.. note:: - This module is compatible with aic-sdk versions >= 2.0.0. - For aic-sdk < 2.0.0, use :mod:`pipecat.audio.vad.aic_vad` instead. -""" - -from typing import Any, Callable, Optional - -from loguru import logger - -from pipecat.audio.utils import check_aic_sdk_version -from pipecat.audio.vad.vad_analyzer import VADAnalyzer, VADParams - -# Check aic-sdk is installed and version is compatible (>= 2.0.0) -check_aic_sdk_version("v2") - -from aic import VadParameter - - - -class AICVADAnalyzer(VADAnalyzer): - """VAD analyzer that lazily binds to the AIC VadContext via a factory. - - The analyzer can be constructed before the AIC Processor exists. Once the filter has - started and the Processor is available, the provided factory will succeed and the - VadContext will be obtained. We then use the context's is_speech_detected() state - to derive confidence values. - - AIC VAD runtime parameters (v2): - - speech_hold_duration: - Controls for how long the VAD continues to detect speech after the audio signal - no longer contains speech (in seconds). - Range: 0.0 .. 20x model window length - Default (SDK): 0.05s (50ms) - - sensitivity: - Controls the energy threshold sensitivity. Higher values make the detector - less sensitive (require more energy to count as speech). - Range: 1.0 .. 15.0 - Formula: Energy threshold = 10 ** (-sensitivity) - Default (SDK): 6.0 - - .. note:: - This class requires aic-sdk >= 2.0.0. - """ - - def __init__( - self, - *, - vad_context_factory: Optional[Callable[[], Any]] = None, - speech_hold_duration: Optional[float] = None, - sensitivity: Optional[float] = None, - ): - """Create an AIC VAD analyzer. - - Args: - vad_context_factory: - Zero-arg callable that returns the AIC VadContext. - This may raise until the filter's Processor has been created; the analyzer - will retry on set_sample_rate/first use. - speech_hold_duration: - Optional override for AIC VAD speech hold duration (in seconds). - Range: 0.0 .. 20x model window length. - If None, the SDK default (0.05s) is used. - sensitivity: - Optional override for AIC VAD sensitivity (energy threshold). - Range: 1.0 .. 15.0. Energy threshold = 10 ** (-sensitivity). - If None, the SDK default (6.0) is used. - """ - # Use fixed VAD parameters for AIC: no user override - fixed_params = VADParams(confidence=0.5, start_secs=0.0, stop_secs=0.0, min_volume=0.0) - super().__init__(sample_rate=None, params=fixed_params) - self._vad_context_factory = vad_context_factory - self._vad_ctx: Optional[Any] = None - self._pending_speech_hold_duration: Optional[float] = speech_hold_duration - self._pending_sensitivity: Optional[float] = sensitivity - - def bind_vad_context_factory(self, vad_context_factory: Callable[[], Any]): - """Attach or replace the factory post-construction.""" - self._vad_context_factory = vad_context_factory - self._ensure_vad_context_initialized() - - def _apply_vad_params(self): - """Apply optional AIC VAD parameters if available.""" - if self._vad_ctx is None or VadParameter is None: - return - try: - if self._pending_speech_hold_duration is not None: - self._vad_ctx.set_parameter( - VadParameter.SpeechHoldDuration, float(self._pending_speech_hold_duration) - ) - if self._pending_sensitivity is not None: - self._vad_ctx.set_parameter( - VadParameter.Sensitivity, float(self._pending_sensitivity) - ) - except Exception as e: # noqa: BLE001 - logger.debug(f"AIC VAD parameter application deferred/failed: {e}") - - def _ensure_vad_context_initialized(self): - if self._vad_ctx is not None: - return - if not self._vad_context_factory: - return - try: - self._vad_ctx = self._vad_context_factory() - self._apply_vad_params() - # With VAD context ready, recompute internal frame sizing - super().set_params(self._params) - logger.debug("AIC VAD context (v2) initialized in analyzer.") - except Exception as e: # noqa: BLE001 - # Filter may not be started yet; try again later - logger.debug(f"Deferring AIC VAD context initialization: {e}") - - def set_sample_rate(self, sample_rate: int): - """Set the sample rate for audio processing. - - Args: - sample_rate: Audio sample rate in Hz. - """ - # Set rate and attempt VAD context initialization once we know SR - self._sample_rate = self._init_sample_rate or sample_rate - self._ensure_vad_context_initialized() - # Ensure params are initialized even if VAD context not ready yet - try: - super().set_params(self._params) - except Exception: - pass - - def num_frames_required(self) -> int: - """Get the number of audio frames required for analysis. - - Returns: - Number of frames needed for VAD processing. - """ - # Use 10 ms windows based on sample rate - return int(self.sample_rate * 0.01) if self.sample_rate > 0 else 160 - - def voice_confidence(self, buffer: bytes) -> float: - """Calculate voice activity confidence for the given audio buffer. - - Args: - buffer: Audio buffer to analyze. - - Returns: - Voice confidence score is 0.0 or 1.0. - """ - # Ensure VAD context exists (filter might have started since last call) - self._ensure_vad_context_initialized() - if self._vad_ctx is None: - return 0.0 - - # We do not need to analyze 'buffer' here since the processor's VAD is updated - # as part of the enhancement pipeline. Simply query the boolean and map it. - try: - is_speech = self._vad_ctx.is_speech_detected() - return 1.0 if is_speech else 0.0 - except Exception as e: # noqa: BLE001 - logger.error(f"AIC VAD inference error: {e}") - return 0.0 diff --git a/uv.lock b/uv.lock index 45c1f892f..4782f1c79 100644 --- a/uv.lock +++ b/uv.lock @@ -38,12 +38,12 @@ wheels = [ [[package]] name = "aic-sdk" -version = "1.2.0" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f9/ba/3ebe31b91e03d42437ec864e9d2af3a52b7ccc73a1a0c1026275956270b0/aic_sdk-1.2.0.tar.gz", hash = "sha256:eeda9a181c679f175dbe6f0efc0c67ec98ff3d84cfe01541fef7fa12ecd505ca", size = 35606, upload-time = "2025-11-20T14:42:14.333Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/e7/56b6074224dc26a1350b165fd0ef3c8ca8c115cc1d4aa3e4f38af9c5d3f1/aic_sdk-1.3.0.tar.gz", hash = "sha256:ccccf7c0c35fd0342a0cbcd1ed81bd3fd7f59df51fbb7c1a80fb438a94ee6ae9", size = 37700, upload-time = "2025-12-12T13:00:09.11Z" } [[package]] name = "aioboto3" @@ -3292,47 +3292,47 @@ wheels = [ [[package]] name = "mlx" -version = "0.30.1" +version = "0.30.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mlx-metal", marker = "sys_platform == 'darwin'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/8d/16a34feb957ac33525b9b787b5132053a44bc94d1bf40c18639f6e05cd2a/mlx-0.30.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:391c650f0578ce359c8cffddb204b42798b622f9ee2b57b865d87716c00db536", size = 592926, upload-time = "2025-12-18T01:55:28.757Z" }, - { url = "https://files.pythonhosted.org/packages/34/e6/0661455f5f4bd9de257874b28a96a33699d36a1e17ccde821341c0ac1c0e/mlx-0.30.1-cp310-cp310-macosx_15_0_arm64.whl", hash = "sha256:42fefcad72d7488c65649e152a1b28f00c2033d38121afa45ce65ae16ec6b988", size = 592926, upload-time = "2025-12-18T01:55:30.141Z" }, - { url = "https://files.pythonhosted.org/packages/d8/37/a322af7dba9101064b5e858d1208e0e66cd83be7d060d14fa03ace37d52e/mlx-0.30.1-cp310-cp310-macosx_26_0_arm64.whl", hash = "sha256:a9db94e7e080672cc0dda9a5f121aebe0d49f7a8cb46706ecfd8b8ce7d99d541", size = 566952, upload-time = "2025-12-18T00:15:50.075Z" }, - { url = "https://files.pythonhosted.org/packages/c9/46/f0005d07fe5687bbf4efc15b468d27f2923f486b07a625d35c7d3cbb4962/mlx-0.30.1-cp310-cp310-manylinux_2_35_aarch64.whl", hash = "sha256:44b2142896c8dd8ab057dd785faf92fa83f3697b4b6bb01ff7515df12b6de666", size = 658049, upload-time = "2025-12-18T01:55:31.748Z" }, - { url = "https://files.pythonhosted.org/packages/cb/95/cc47c4607cc78f55ce3081ade9161961795c15c049bf219f27a393f85767/mlx-0.30.1-cp310-cp310-manylinux_2_35_x86_64.whl", hash = "sha256:37ea97b3c4bd71b19d87c6ef2c9e681e11f37908d8381fc2b785d2509b0681df", size = 692336, upload-time = "2025-12-18T01:55:33.224Z" }, - { url = "https://files.pythonhosted.org/packages/07/14/74acbd677ececd17a44dafda1b472aebacef54f60ff9a41a801f711de9a7/mlx-0.30.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:acfd7d1b8e5b9fa1b7e9fab4cc5ba6a492c559fbb1c5aeab16c1d7a148ab4f1b", size = 593048, upload-time = "2025-12-18T01:55:34.9Z" }, - { url = "https://files.pythonhosted.org/packages/58/8c/5309848afb9c53d363f59b88ae5811de248e2817e91aeadf007e2ac8d22b/mlx-0.30.1-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:b62030471272d1835b8137164bd43d863cc93ff1d67ec4f1f87bb4c8613dd5a6", size = 593043, upload-time = "2025-12-18T01:55:36.839Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5a/0039815a930f0193e2cffb27c57dc6971004bce0086c2bbbdb10395c272c/mlx-0.30.1-cp311-cp311-macosx_26_0_arm64.whl", hash = "sha256:0489cd340f2d262cb3aaad4368e40e84b152e182e4cea37ba018e56c72e1d020", size = 567014, upload-time = "2025-12-18T00:15:51.731Z" }, - { url = "https://files.pythonhosted.org/packages/de/c7/6bdb5497c1f5ed3e33afa7785761ad87fd3436c071805d9a93c905943f04/mlx-0.30.1-cp311-cp311-manylinux_2_35_aarch64.whl", hash = "sha256:fbdcfc3ed556a7e701a8eb67da299e2a25f52615193833ca6374decca3be5bf4", size = 658930, upload-time = "2025-12-18T01:55:38.441Z" }, - { url = "https://files.pythonhosted.org/packages/91/02/2d86a1c116e951eb4d88fe313c321e23628ce7404712e1258cacf925a8b8/mlx-0.30.1-cp311-cp311-manylinux_2_35_x86_64.whl", hash = "sha256:68ec854e7b5f89454e67d6c2fa7bb416b8afb148003ccd775904ec6ec4744818", size = 692484, upload-time = "2025-12-18T01:55:40.254Z" }, - { url = "https://files.pythonhosted.org/packages/3a/4b/ad57b2f0ede3f0d009c0e3e1270c219bd18f9025388855ee149680cffa20/mlx-0.30.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:deaef3ecd2f99930867a29de748e3bffa9cc7e4dfa834f2501c37ed29aece1cc", size = 593397, upload-time = "2025-12-18T01:55:41.814Z" }, - { url = "https://files.pythonhosted.org/packages/ef/14/7fa03a0f66ac3cfb2fd6752178a1488f13c7233fff26eed0f832d961db35/mlx-0.30.1-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:86ccdcda0b5ea4768b87da25beae5b83ac7cc802506116b6845cea6f450e2377", size = 593397, upload-time = "2025-12-18T01:55:43Z" }, - { url = "https://files.pythonhosted.org/packages/9c/c8/9f1343dbe2381f9653df4e0a62dc8bf38f575a2553dc2aa6916de32d2a85/mlx-0.30.1-cp312-cp312-macosx_26_0_arm64.whl", hash = "sha256:a625cb434b2acc5674fe10683374641dab9671fb354ae7c2c67a1fb0405eeb37", size = 567576, upload-time = "2025-12-18T00:15:55.114Z" }, - { url = "https://files.pythonhosted.org/packages/15/ff/485ed9c99c18ef89ac987178c0a526cb4148ba38b14838d315311d9d76a8/mlx-0.30.1-cp312-cp312-manylinux_2_35_aarch64.whl", hash = "sha256:ccc1ff3aca8fb1073c7dcd1274cebe48ae75f852d14b16c7db8228fbbad594dd", size = 643654, upload-time = "2025-12-18T01:55:44.165Z" }, - { url = "https://files.pythonhosted.org/packages/8a/d3/54d3bf5e404c3b6424b49c505dc8b3c06c6bb498fe720195b1fafbd69b5e/mlx-0.30.1-cp312-cp312-manylinux_2_35_x86_64.whl", hash = "sha256:55ed7fc4b389d6e49dac6d34a97b41e61cbe3662ac29c3d29cf612e6b2ed9827", size = 687305, upload-time = "2025-12-18T01:55:45.526Z" }, - { url = "https://files.pythonhosted.org/packages/f9/fd/c6f56cd87d48763ed63655ace627c06db9819eae7d43d132f40d4965947a/mlx-0.30.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:743520758bc8261b2ed8f3b3dc96e4e9236769dd8f61fb17877c5e44037e2058", size = 593366, upload-time = "2025-12-18T01:55:46.786Z" }, - { url = "https://files.pythonhosted.org/packages/dc/53/96d8c48b21f91c4216b6d2ef6dfc10862e5fb0b811a2aaf02c96c78601de/mlx-0.30.1-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:fc9745bc1860ca60128e3a6d36157da06d936e2b4007a4dcba990b40202f598f", size = 593368, upload-time = "2025-12-18T01:55:48.363Z" }, - { url = "https://files.pythonhosted.org/packages/70/ce/476c3b7d3a4153bd0e1c5af1f1b6c09a804b652bbed34072404b322c22e0/mlx-0.30.1-cp313-cp313-macosx_26_0_arm64.whl", hash = "sha256:a1480399c67bb327a66c5527b73915132e3fcaae3bce9634e5c81ccad9f43229", size = 567561, upload-time = "2025-12-18T00:15:56.153Z" }, - { url = "https://files.pythonhosted.org/packages/33/41/7ad1e639fd7dd1cf01a62c1c5b051024a859888c27504996e9d8380e6754/mlx-0.30.1-cp313-cp313-manylinux_2_35_aarch64.whl", hash = "sha256:8e19850a4236a8e174f851f5789b8b62a8eb74f5a8fa49ad8ba286c5ddb5f9bf", size = 643122, upload-time = "2025-12-18T01:55:49.607Z" }, - { url = "https://files.pythonhosted.org/packages/d0/dc/72d3737c5b0662eb5e785d353dbc5e34d793d27b09b99e39993ee051bd19/mlx-0.30.1-cp313-cp313-manylinux_2_35_x86_64.whl", hash = "sha256:1c8ed5bcd9f1910fca209e95859ac737e60b3e1954181b820fa269158f81049a", size = 687254, upload-time = "2025-12-18T01:55:51.239Z" }, - { url = "https://files.pythonhosted.org/packages/9b/cc/523448996247bb05d9d68e23bccf3dafdda660befb9330f6bd5fa13361e8/mlx-0.30.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d34cc2c25b0ee41c1349f14650db760e282685339858e305453f62405c12bc1b", size = 596006, upload-time = "2025-12-18T01:55:52.463Z" }, - { url = "https://files.pythonhosted.org/packages/23/0e/f9f2f9659c34c87be8f4167f6a1d6ed7e826f4889d20eecd4c0d8122f0e9/mlx-0.30.1-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:4e47d301e9095b87f0bda8827bfd6ffe744223aba5cee8f28e25894d647f5823", size = 596008, upload-time = "2025-12-18T01:55:54.02Z" }, - { url = "https://files.pythonhosted.org/packages/56/a7/49e41fb141de95b6a376091a963c737839c9cda04e423c67f57460a50458/mlx-0.30.1-cp314-cp314-macosx_26_0_arm64.whl", hash = "sha256:cfba13e2a52255d663a1ad62f0f83eb3991e42147edf9a8d38cdd224e48ca49b", size = 570406, upload-time = "2025-12-18T00:15:57.177Z" }, - { url = "https://files.pythonhosted.org/packages/73/99/a43cb112167cf865c069f5e108ae42f5314663930ff3dd86c2d23d984191/mlx-0.30.1-cp314-cp314-manylinux_2_35_aarch64.whl", hash = "sha256:bebfec377208eb29cc88aa86c897c7446aa0984838669e138f273f9225d627ff", size = 646461, upload-time = "2025-12-18T01:55:55.285Z" }, - { url = "https://files.pythonhosted.org/packages/d4/ff/1e1968f107b4221a98dc26832586b1f646b27ddf3e55c95051c09d751f0a/mlx-0.30.1-cp314-cp314-manylinux_2_35_x86_64.whl", hash = "sha256:d18012d5cf0f013bc4a405cfd1e9d2d28e798f4d2dc4f15aa0fbffff73c02ba2", size = 687114, upload-time = "2025-12-18T01:55:56.506Z" }, + { url = "https://files.pythonhosted.org/packages/9c/9c/d6f72f04eeeeaeee8309397efcfa0e923189d0b720f4ac6b3887d0a2f40b/mlx-0.30.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:685051761e428336f8f19ae76a761ce99d29ff67c52738f15ce6409e2ff34e6b", size = 568453, upload-time = "2026-01-14T01:16:40.796Z" }, + { url = "https://files.pythonhosted.org/packages/db/59/505717fd63f62d766f054ab8770d08e98b10217c0995bd2555429863fd31/mlx-0.30.3-cp310-cp310-macosx_15_0_arm64.whl", hash = "sha256:e405e6575e3b0b00dd6bd02bdb415b638cd5c2e5faedb696df2b2c8fbe871240", size = 568451, upload-time = "2026-01-14T01:16:42.027Z" }, + { url = "https://files.pythonhosted.org/packages/86/9c/a5319ae8ed0baa76fde80def12391ae13acec1b88904d4ead9bbabc9a083/mlx-0.30.3-cp310-cp310-macosx_26_0_arm64.whl", hash = "sha256:46894eb528457483aec44227f61afdff424cb76d146a6e1727d03ea0f52be41b", size = 568309, upload-time = "2026-01-14T05:52:06.915Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7c/ac360b04c6b09acf11fcb54068909ca030325b248557930f6991d5601436/mlx-0.30.3-cp310-cp310-manylinux_2_35_aarch64.whl", hash = "sha256:a18e1b380130a77feda83b8bb8a7ff5a24f78e7263af484c6627f23e4210bdaf", size = 631435, upload-time = "2026-01-14T01:16:43.085Z" }, + { url = "https://files.pythonhosted.org/packages/01/b5/3becb2c93955d43539d9c916b33899a57c50099c29310dc2b5c68ff7a88d/mlx-0.30.3-cp310-cp310-manylinux_2_35_x86_64.whl", hash = "sha256:c27f8e78b89cf97411d740a2ca46accf6c6e3fcc43d1e906389abff1f0e00376", size = 664899, upload-time = "2026-01-14T01:16:44.623Z" }, + { url = "https://files.pythonhosted.org/packages/78/b6/dfcfffc41d832a86249715fab336dc8638c2237035287eb24af792484c53/mlx-0.30.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:794e79587a4906bdb3c5473ef936f45008eaaa609a3c498cc29a442b2c829621", size = 568664, upload-time = "2026-01-14T01:16:45.573Z" }, + { url = "https://files.pythonhosted.org/packages/22/9f/22d494b83b611380063da31c2b482db8c620f7ad6531cfcd1e11f7c35852/mlx-0.30.3-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:472cdc6eaca8610224621a1561e8c36477eab1a2f0dd3eb49b95484d739c4605", size = 568663, upload-time = "2026-01-14T01:16:46.588Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/b6fb0500aef8e9ed65d4730d8c34b13d7a770ca863b9af363b5713a16040/mlx-0.30.3-cp311-cp311-macosx_26_0_arm64.whl", hash = "sha256:a5d82be69c7e671dc4d5855d2f6aedcb507817e5985478903ab754b642d9ba01", size = 568522, upload-time = "2026-01-14T05:52:08.334Z" }, + { url = "https://files.pythonhosted.org/packages/6e/23/ea140c35419ec133e1037d34d94854474cdd72c89eedc3a90b8ec65fb0ff/mlx-0.30.3-cp311-cp311-manylinux_2_35_aarch64.whl", hash = "sha256:009a9a1d2e234b9b269f455729202feaf22eb1faf2c7b85818f2473f6c2f9cbe", size = 632235, upload-time = "2026-01-14T01:16:47.764Z" }, + { url = "https://files.pythonhosted.org/packages/af/18/335d2d455b1e15036e315c6b64de8e6b4b04ec60576e1b99a651a7487014/mlx-0.30.3-cp311-cp311-manylinux_2_35_x86_64.whl", hash = "sha256:ba7141b6c251207d26a5611a0038d121cd13e367a59589d8c827e6af06b1f406", size = 664821, upload-time = "2026-01-14T01:16:48.8Z" }, + { url = "https://files.pythonhosted.org/packages/11/b3/e24c3a69dad0cf4404bb174c6fed0d804022da64758cd815a254e1cd0627/mlx-0.30.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:0b275168b80645a155b456e1a457a37fb5ee2c251e8fbd8db9e153351a9e2d2f", size = 569398, upload-time = "2026-01-14T01:16:49.804Z" }, + { url = "https://files.pythonhosted.org/packages/0b/87/d0804443da97a06d3439f6efb0ceffa178f530a121f0f4a6c77b39f8bfd7/mlx-0.30.3-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:6e818de14864982e832344198240a1dafba7d3316c4eb6f1b8e43b4dd25dd2ef", size = 569396, upload-time = "2026-01-14T01:16:51.007Z" }, + { url = "https://files.pythonhosted.org/packages/cf/dc/7cdd95e4561b73fba8c86bf11293797076120400e472fe2a72ef483b6d8d/mlx-0.30.3-cp312-cp312-macosx_26_0_arm64.whl", hash = "sha256:d23b422209fd4b7ecacef59070321f8c6a122f906a5e9b6683a5fc9e1b8fcd5c", size = 569192, upload-time = "2026-01-14T05:52:09.715Z" }, + { url = "https://files.pythonhosted.org/packages/58/3b/6892f48dce949da7e1706cad45a1693857ef3adf23f849bf851c37e605eb/mlx-0.30.3-cp312-cp312-manylinux_2_35_aarch64.whl", hash = "sha256:f487461ffd5c2411c012dd8cd0d347dd807f05f223b1dec1c13bad0815cdcefd", size = 617390, upload-time = "2026-01-14T01:16:52.676Z" }, + { url = "https://files.pythonhosted.org/packages/66/ce/606e2111bc7c2ed1a2f2582caeb3e73b90e00d773d573fe9cd5dd36a0321/mlx-0.30.3-cp312-cp312-manylinux_2_35_x86_64.whl", hash = "sha256:b78627f324790fd0e06c4fa6e79b88094b342c5c425f8909de7c3f2fa5d01302", size = 659552, upload-time = "2026-01-14T01:16:53.888Z" }, + { url = "https://files.pythonhosted.org/packages/d0/22/42935d593fe82d3b98eb9d60e4620ed99703886635106f89d407c68f33bc/mlx-0.30.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:743fac1e4f9e8e46c8262943c643a31139c255cdb256c99ad496958215ccac1e", size = 569344, upload-time = "2026-01-14T01:16:54.847Z" }, + { url = "https://files.pythonhosted.org/packages/7d/27/f2e7a5236289d45315d0215e8553b4dd7e2faaba3bcb5025b34b25d5ab66/mlx-0.30.3-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:3b04ae81655aa0e63a6e8f2c749de3bbce64cf5b168ae10f39ed086dfa99e7f8", size = 569345, upload-time = "2026-01-14T01:16:56.564Z" }, + { url = "https://files.pythonhosted.org/packages/01/41/06b042457f51952456e9bb46b2c6e205ab3a28fc52d6751b5787fdb762b2/mlx-0.30.3-cp313-cp313-macosx_26_0_arm64.whl", hash = "sha256:ba9b5bdb1e929cc130af72efd7f73508c0f4e526d224489af7ec1c6419564659", size = 569213, upload-time = "2026-01-14T05:52:10.86Z" }, + { url = "https://files.pythonhosted.org/packages/ec/1e/f62c98fc0d2d878ee4235671f9d406b13cc9240493ba6fcfde2f72c2ff83/mlx-0.30.3-cp313-cp313-manylinux_2_35_aarch64.whl", hash = "sha256:dfe5c5b64e55398a22100804abbf9681996b03129e720e36b1727ed704db12b5", size = 617309, upload-time = "2026-01-14T01:16:57.58Z" }, + { url = "https://files.pythonhosted.org/packages/e9/62/811f064693449de740350d27793ce39343a460305ec8d878c318b80921d0/mlx-0.30.3-cp313-cp313-manylinux_2_35_x86_64.whl", hash = "sha256:a3364924610929936e6aaf13c71106161258e5a5d3f7813a64c07cc2435f9f55", size = 659521, upload-time = "2026-01-14T01:16:58.719Z" }, + { url = "https://files.pythonhosted.org/packages/82/e2/6e551bd48fb350fbf0ee4cc5cd09485437d260b8f4937f22d8623e14687a/mlx-0.30.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2c27fd8daaae14ca6cf407fcd236006a6e968f7708c8f61a2709116f2e754852", size = 571920, upload-time = "2026-01-14T01:16:59.683Z" }, + { url = "https://files.pythonhosted.org/packages/82/c0/561d1c9d3d12830b0e7fdcbd807585ef20909e398d4bcdbf25e4367543eb/mlx-0.30.3-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:b755fd4ed4b6a2ae4dee3766b5a2ea52fcbe83ebd1cf018458e18b74139409f3", size = 571921, upload-time = "2026-01-14T01:17:00.868Z" }, + { url = "https://files.pythonhosted.org/packages/42/1a/fb573fc2edc22a777fa254ff5c0c886ffd2c88aeb1f21c45778ef170f990/mlx-0.30.3-cp314-cp314-macosx_26_0_arm64.whl", hash = "sha256:7e352c0369a2f7e54d4f317b434eab3333918ea9edde1c43c61d36386b6f76bf", size = 571732, upload-time = "2026-01-14T05:52:11.893Z" }, + { url = "https://files.pythonhosted.org/packages/9e/db/d0083e8f2205b3b2dcd9670eb6f0d6c1b7cbfea6b01a1f8bff39142edf44/mlx-0.30.3-cp314-cp314-manylinux_2_35_aarch64.whl", hash = "sha256:00ac867f3d003c1477a66a579442c2040ba7ea43ce3c174490d1f8bf379606bd", size = 619635, upload-time = "2026-01-14T01:17:01.812Z" }, + { url = "https://files.pythonhosted.org/packages/ab/90/ab0b93ff0e76da4fe0e878722c76a308cfb950b044a4676e9617276d8ccd/mlx-0.30.3-cp314-cp314-manylinux_2_35_x86_64.whl", hash = "sha256:5be7d0329036f09c6ed003ea3e307e97e3144f20a3e4711b01810d7d5013cf2c", size = 659652, upload-time = "2026-01-14T01:17:02.915Z" }, ] [[package]] name = "mlx-metal" -version = "0.30.1" +version = "0.30.3" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/09/3f/0be35ddad7e13d8ecd33a9185895f9739bb00b96ef0cce36cf0405d4aec0/mlx_metal-0.30.1-py3-none-macosx_14_0_arm64.whl", hash = "sha256:e7e92c6bdbd7ac8083f528a4c6640552ae106a57bb3d99856ac10a32e93a4b5e", size = 36864966, upload-time = "2025-12-18T01:55:31.473Z" }, - { url = "https://files.pythonhosted.org/packages/1e/1f/c0bddd0d5bf3871411aabe32121e09e1b7cdbece8917a49d5a442310e3e5/mlx_metal-0.30.1-py3-none-macosx_15_0_arm64.whl", hash = "sha256:bb50f57418af7fc3c42a2da2c4bde0e7ab7ac0b997de1f6f642a6680ac65d626", size = 36859011, upload-time = "2025-12-18T01:55:34.541Z" }, - { url = "https://files.pythonhosted.org/packages/67/b3/73cc2f584ac612a476096d35a61eed75ee7ed8b4e320b0c36cf60a14d4eb/mlx_metal-0.30.1-py3-none-macosx_26_0_arm64.whl", hash = "sha256:e0b151a0053ac00b4226710bfb6dbf54b87283fb01e10fb3877f9ea969f680aa", size = 44981160, upload-time = "2025-12-18T00:15:47.518Z" }, + { url = "https://files.pythonhosted.org/packages/f6/63/4d8f6fefb507c028df4454dabfe8d8e0ad2961bb06510b6aca23d2d5b2be/mlx_metal-0.30.3-py3-none-macosx_14_0_arm64.whl", hash = "sha256:6276312b02353714c7c6515169569fe1c4bebe3229c8ecf1fdb375a13e78c966", size = 37716245, upload-time = "2026-01-14T01:16:34.838Z" }, + { url = "https://files.pythonhosted.org/packages/35/91/1d452e48a4bb4958844fd3bb28ae31b8de110549c009ebec5024ce27ebf3/mlx_metal-0.30.3-py3-none-macosx_15_0_arm64.whl", hash = "sha256:c096c0a3428f3f96a06220f97a36f9528b18bc05173f821eb05bc8458e723fa8", size = 37712125, upload-time = "2026-01-14T01:16:38.619Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/7a3cbca85542b5ca4faf871e35927f43aa0e3fc830ae5b699780fe723677/mlx_metal-0.30.3-py3-none-macosx_26_0_arm64.whl", hash = "sha256:69068533bd1ee8b0379ce5de57ed5fd313577a10ecab58e1332fd1ff7248a75e", size = 46488962, upload-time = "2026-01-14T05:52:04.523Z" }, ] [[package]] @@ -4495,7 +4495,7 @@ docs = [ [package.metadata] requires-dist = [ { name = "accelerate", marker = "extra == 'moondream'", specifier = "~=1.10.0" }, - { name = "aic-sdk", marker = "extra == 'aic'", specifier = "~=1.2.0" }, + { name = "aic-sdk", marker = "extra == 'aic'", specifier = ">=1.2.0" }, { name = "aioboto3", marker = "extra == 'aws'", specifier = "~=15.5.0" }, { name = "aiofiles", specifier = ">=24.1.0,<25" }, { name = "aiohttp", specifier = ">=3.11.12,<4" }, @@ -5280,7 +5280,7 @@ wheels = [ [[package]] name = "pyrnnoise" -version = "0.4.1" +version = "0.4.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "audiolab" }, @@ -5290,9 +5290,10 @@ dependencies = [ { name = "tqdm" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/59/49/7017ffa14230096e0271bd49dfd9ab60a32bfebe7e71399c2a0e38c6f859/pyrnnoise-0.4.1-py3-none-macosx_15_0_universal2.whl", hash = "sha256:c1fe407729190d0f84f3e3c9d9322ebbd33b27f3f5d9f7217379b71a4dd043e7", size = 13381833, upload-time = "2025-11-25T15:54:06.532Z" }, - { url = "https://files.pythonhosted.org/packages/8e/24/fb8b7bafb3dd9cbb46e134fa25c9597683c61b42c0133453fefeebeb0066/pyrnnoise-0.4.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:ddd39b45221b65fb235f882a0ce127513a1012d41c5b3ba9dc4e9e991b22c205", size = 13273307, upload-time = "2025-11-25T15:54:04.076Z" }, - { url = "https://files.pythonhosted.org/packages/7f/8e/eef9b2022fa5b9a111ba31d2f25ccd6e45da3daf16d20352e1fb18fd81dd/pyrnnoise-0.4.1-py3-none-win_amd64.whl", hash = "sha256:440e32359256eb7947e29fb080e800e984ba521fbe89a8b0b2f5dc196965e441", size = 13267076, upload-time = "2025-11-25T15:54:37.547Z" }, + { url = "https://files.pythonhosted.org/packages/1f/90/51bb94bcfd8aab186fd08902e0706a6eda5813485fb57eff011ce6ae4c51/pyrnnoise-0.4.3-py3-none-macosx_15_0_universal2.whl", hash = "sha256:bdd8e933d32457362e6f4e56831afa8155208825040ab075c4223baed755fa4f", size = 13381834, upload-time = "2026-01-14T08:44:28.263Z" }, + { url = "https://files.pythonhosted.org/packages/04/51/993a25a8b5220e23e0a31ff98747b8fce4685336e0fc4e8e156feab5c4f1/pyrnnoise-0.4.3-py3-none-manylinux1_x86_64.whl", hash = "sha256:1b094777e73797c5dd647782902c691ebb9a3c456c878e742597f5b55535a3db", size = 13273307, upload-time = "2026-01-14T08:44:27.801Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e4/9a13ede6521360341314bf90d5b687cd3f1bd4259bfea740dbc88340484a/pyrnnoise-0.4.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:161c57e05257e0b51f1b21675dcb2debb8cc86903c1fe2ccc3feb4322e545732", size = 13267247, upload-time = "2026-01-14T08:44:30.119Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/795f8504fa7f07fc16e99e82413a6fe997df1999e18bb6fab0b428431a92/pyrnnoise-0.4.3-py3-none-win_amd64.whl", hash = "sha256:25e7d8d63f251238a439e6e3d54ad8cb147c9f2b7c7c56fc9d9a496f682d8b06", size = 13267061, upload-time = "2026-01-14T08:45:03.444Z" }, ] [[package]] From 465ae4f706dd3981e7e958c5965c63e246fc4123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Fri, 16 Jan 2026 00:08:56 +0100 Subject: [PATCH 03/50] keep uv.lock as it is. --- uv.lock | 75 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/uv.lock b/uv.lock index 4782f1c79..45c1f892f 100644 --- a/uv.lock +++ b/uv.lock @@ -38,12 +38,12 @@ wheels = [ [[package]] name = "aic-sdk" -version = "1.3.0" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/e7/56b6074224dc26a1350b165fd0ef3c8ca8c115cc1d4aa3e4f38af9c5d3f1/aic_sdk-1.3.0.tar.gz", hash = "sha256:ccccf7c0c35fd0342a0cbcd1ed81bd3fd7f59df51fbb7c1a80fb438a94ee6ae9", size = 37700, upload-time = "2025-12-12T13:00:09.11Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/ba/3ebe31b91e03d42437ec864e9d2af3a52b7ccc73a1a0c1026275956270b0/aic_sdk-1.2.0.tar.gz", hash = "sha256:eeda9a181c679f175dbe6f0efc0c67ec98ff3d84cfe01541fef7fa12ecd505ca", size = 35606, upload-time = "2025-11-20T14:42:14.333Z" } [[package]] name = "aioboto3" @@ -3292,47 +3292,47 @@ wheels = [ [[package]] name = "mlx" -version = "0.30.3" +version = "0.30.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mlx-metal", marker = "sys_platform == 'darwin'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/9c/d6f72f04eeeeaeee8309397efcfa0e923189d0b720f4ac6b3887d0a2f40b/mlx-0.30.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:685051761e428336f8f19ae76a761ce99d29ff67c52738f15ce6409e2ff34e6b", size = 568453, upload-time = "2026-01-14T01:16:40.796Z" }, - { url = "https://files.pythonhosted.org/packages/db/59/505717fd63f62d766f054ab8770d08e98b10217c0995bd2555429863fd31/mlx-0.30.3-cp310-cp310-macosx_15_0_arm64.whl", hash = "sha256:e405e6575e3b0b00dd6bd02bdb415b638cd5c2e5faedb696df2b2c8fbe871240", size = 568451, upload-time = "2026-01-14T01:16:42.027Z" }, - { url = "https://files.pythonhosted.org/packages/86/9c/a5319ae8ed0baa76fde80def12391ae13acec1b88904d4ead9bbabc9a083/mlx-0.30.3-cp310-cp310-macosx_26_0_arm64.whl", hash = "sha256:46894eb528457483aec44227f61afdff424cb76d146a6e1727d03ea0f52be41b", size = 568309, upload-time = "2026-01-14T05:52:06.915Z" }, - { url = "https://files.pythonhosted.org/packages/8c/7c/ac360b04c6b09acf11fcb54068909ca030325b248557930f6991d5601436/mlx-0.30.3-cp310-cp310-manylinux_2_35_aarch64.whl", hash = "sha256:a18e1b380130a77feda83b8bb8a7ff5a24f78e7263af484c6627f23e4210bdaf", size = 631435, upload-time = "2026-01-14T01:16:43.085Z" }, - { url = "https://files.pythonhosted.org/packages/01/b5/3becb2c93955d43539d9c916b33899a57c50099c29310dc2b5c68ff7a88d/mlx-0.30.3-cp310-cp310-manylinux_2_35_x86_64.whl", hash = "sha256:c27f8e78b89cf97411d740a2ca46accf6c6e3fcc43d1e906389abff1f0e00376", size = 664899, upload-time = "2026-01-14T01:16:44.623Z" }, - { url = "https://files.pythonhosted.org/packages/78/b6/dfcfffc41d832a86249715fab336dc8638c2237035287eb24af792484c53/mlx-0.30.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:794e79587a4906bdb3c5473ef936f45008eaaa609a3c498cc29a442b2c829621", size = 568664, upload-time = "2026-01-14T01:16:45.573Z" }, - { url = "https://files.pythonhosted.org/packages/22/9f/22d494b83b611380063da31c2b482db8c620f7ad6531cfcd1e11f7c35852/mlx-0.30.3-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:472cdc6eaca8610224621a1561e8c36477eab1a2f0dd3eb49b95484d739c4605", size = 568663, upload-time = "2026-01-14T01:16:46.588Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/b6fb0500aef8e9ed65d4730d8c34b13d7a770ca863b9af363b5713a16040/mlx-0.30.3-cp311-cp311-macosx_26_0_arm64.whl", hash = "sha256:a5d82be69c7e671dc4d5855d2f6aedcb507817e5985478903ab754b642d9ba01", size = 568522, upload-time = "2026-01-14T05:52:08.334Z" }, - { url = "https://files.pythonhosted.org/packages/6e/23/ea140c35419ec133e1037d34d94854474cdd72c89eedc3a90b8ec65fb0ff/mlx-0.30.3-cp311-cp311-manylinux_2_35_aarch64.whl", hash = "sha256:009a9a1d2e234b9b269f455729202feaf22eb1faf2c7b85818f2473f6c2f9cbe", size = 632235, upload-time = "2026-01-14T01:16:47.764Z" }, - { url = "https://files.pythonhosted.org/packages/af/18/335d2d455b1e15036e315c6b64de8e6b4b04ec60576e1b99a651a7487014/mlx-0.30.3-cp311-cp311-manylinux_2_35_x86_64.whl", hash = "sha256:ba7141b6c251207d26a5611a0038d121cd13e367a59589d8c827e6af06b1f406", size = 664821, upload-time = "2026-01-14T01:16:48.8Z" }, - { url = "https://files.pythonhosted.org/packages/11/b3/e24c3a69dad0cf4404bb174c6fed0d804022da64758cd815a254e1cd0627/mlx-0.30.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:0b275168b80645a155b456e1a457a37fb5ee2c251e8fbd8db9e153351a9e2d2f", size = 569398, upload-time = "2026-01-14T01:16:49.804Z" }, - { url = "https://files.pythonhosted.org/packages/0b/87/d0804443da97a06d3439f6efb0ceffa178f530a121f0f4a6c77b39f8bfd7/mlx-0.30.3-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:6e818de14864982e832344198240a1dafba7d3316c4eb6f1b8e43b4dd25dd2ef", size = 569396, upload-time = "2026-01-14T01:16:51.007Z" }, - { url = "https://files.pythonhosted.org/packages/cf/dc/7cdd95e4561b73fba8c86bf11293797076120400e472fe2a72ef483b6d8d/mlx-0.30.3-cp312-cp312-macosx_26_0_arm64.whl", hash = "sha256:d23b422209fd4b7ecacef59070321f8c6a122f906a5e9b6683a5fc9e1b8fcd5c", size = 569192, upload-time = "2026-01-14T05:52:09.715Z" }, - { url = "https://files.pythonhosted.org/packages/58/3b/6892f48dce949da7e1706cad45a1693857ef3adf23f849bf851c37e605eb/mlx-0.30.3-cp312-cp312-manylinux_2_35_aarch64.whl", hash = "sha256:f487461ffd5c2411c012dd8cd0d347dd807f05f223b1dec1c13bad0815cdcefd", size = 617390, upload-time = "2026-01-14T01:16:52.676Z" }, - { url = "https://files.pythonhosted.org/packages/66/ce/606e2111bc7c2ed1a2f2582caeb3e73b90e00d773d573fe9cd5dd36a0321/mlx-0.30.3-cp312-cp312-manylinux_2_35_x86_64.whl", hash = "sha256:b78627f324790fd0e06c4fa6e79b88094b342c5c425f8909de7c3f2fa5d01302", size = 659552, upload-time = "2026-01-14T01:16:53.888Z" }, - { url = "https://files.pythonhosted.org/packages/d0/22/42935d593fe82d3b98eb9d60e4620ed99703886635106f89d407c68f33bc/mlx-0.30.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:743fac1e4f9e8e46c8262943c643a31139c255cdb256c99ad496958215ccac1e", size = 569344, upload-time = "2026-01-14T01:16:54.847Z" }, - { url = "https://files.pythonhosted.org/packages/7d/27/f2e7a5236289d45315d0215e8553b4dd7e2faaba3bcb5025b34b25d5ab66/mlx-0.30.3-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:3b04ae81655aa0e63a6e8f2c749de3bbce64cf5b168ae10f39ed086dfa99e7f8", size = 569345, upload-time = "2026-01-14T01:16:56.564Z" }, - { url = "https://files.pythonhosted.org/packages/01/41/06b042457f51952456e9bb46b2c6e205ab3a28fc52d6751b5787fdb762b2/mlx-0.30.3-cp313-cp313-macosx_26_0_arm64.whl", hash = "sha256:ba9b5bdb1e929cc130af72efd7f73508c0f4e526d224489af7ec1c6419564659", size = 569213, upload-time = "2026-01-14T05:52:10.86Z" }, - { url = "https://files.pythonhosted.org/packages/ec/1e/f62c98fc0d2d878ee4235671f9d406b13cc9240493ba6fcfde2f72c2ff83/mlx-0.30.3-cp313-cp313-manylinux_2_35_aarch64.whl", hash = "sha256:dfe5c5b64e55398a22100804abbf9681996b03129e720e36b1727ed704db12b5", size = 617309, upload-time = "2026-01-14T01:16:57.58Z" }, - { url = "https://files.pythonhosted.org/packages/e9/62/811f064693449de740350d27793ce39343a460305ec8d878c318b80921d0/mlx-0.30.3-cp313-cp313-manylinux_2_35_x86_64.whl", hash = "sha256:a3364924610929936e6aaf13c71106161258e5a5d3f7813a64c07cc2435f9f55", size = 659521, upload-time = "2026-01-14T01:16:58.719Z" }, - { url = "https://files.pythonhosted.org/packages/82/e2/6e551bd48fb350fbf0ee4cc5cd09485437d260b8f4937f22d8623e14687a/mlx-0.30.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2c27fd8daaae14ca6cf407fcd236006a6e968f7708c8f61a2709116f2e754852", size = 571920, upload-time = "2026-01-14T01:16:59.683Z" }, - { url = "https://files.pythonhosted.org/packages/82/c0/561d1c9d3d12830b0e7fdcbd807585ef20909e398d4bcdbf25e4367543eb/mlx-0.30.3-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:b755fd4ed4b6a2ae4dee3766b5a2ea52fcbe83ebd1cf018458e18b74139409f3", size = 571921, upload-time = "2026-01-14T01:17:00.868Z" }, - { url = "https://files.pythonhosted.org/packages/42/1a/fb573fc2edc22a777fa254ff5c0c886ffd2c88aeb1f21c45778ef170f990/mlx-0.30.3-cp314-cp314-macosx_26_0_arm64.whl", hash = "sha256:7e352c0369a2f7e54d4f317b434eab3333918ea9edde1c43c61d36386b6f76bf", size = 571732, upload-time = "2026-01-14T05:52:11.893Z" }, - { url = "https://files.pythonhosted.org/packages/9e/db/d0083e8f2205b3b2dcd9670eb6f0d6c1b7cbfea6b01a1f8bff39142edf44/mlx-0.30.3-cp314-cp314-manylinux_2_35_aarch64.whl", hash = "sha256:00ac867f3d003c1477a66a579442c2040ba7ea43ce3c174490d1f8bf379606bd", size = 619635, upload-time = "2026-01-14T01:17:01.812Z" }, - { url = "https://files.pythonhosted.org/packages/ab/90/ab0b93ff0e76da4fe0e878722c76a308cfb950b044a4676e9617276d8ccd/mlx-0.30.3-cp314-cp314-manylinux_2_35_x86_64.whl", hash = "sha256:5be7d0329036f09c6ed003ea3e307e97e3144f20a3e4711b01810d7d5013cf2c", size = 659652, upload-time = "2026-01-14T01:17:02.915Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8d/16a34feb957ac33525b9b787b5132053a44bc94d1bf40c18639f6e05cd2a/mlx-0.30.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:391c650f0578ce359c8cffddb204b42798b622f9ee2b57b865d87716c00db536", size = 592926, upload-time = "2025-12-18T01:55:28.757Z" }, + { url = "https://files.pythonhosted.org/packages/34/e6/0661455f5f4bd9de257874b28a96a33699d36a1e17ccde821341c0ac1c0e/mlx-0.30.1-cp310-cp310-macosx_15_0_arm64.whl", hash = "sha256:42fefcad72d7488c65649e152a1b28f00c2033d38121afa45ce65ae16ec6b988", size = 592926, upload-time = "2025-12-18T01:55:30.141Z" }, + { url = "https://files.pythonhosted.org/packages/d8/37/a322af7dba9101064b5e858d1208e0e66cd83be7d060d14fa03ace37d52e/mlx-0.30.1-cp310-cp310-macosx_26_0_arm64.whl", hash = "sha256:a9db94e7e080672cc0dda9a5f121aebe0d49f7a8cb46706ecfd8b8ce7d99d541", size = 566952, upload-time = "2025-12-18T00:15:50.075Z" }, + { url = "https://files.pythonhosted.org/packages/c9/46/f0005d07fe5687bbf4efc15b468d27f2923f486b07a625d35c7d3cbb4962/mlx-0.30.1-cp310-cp310-manylinux_2_35_aarch64.whl", hash = "sha256:44b2142896c8dd8ab057dd785faf92fa83f3697b4b6bb01ff7515df12b6de666", size = 658049, upload-time = "2025-12-18T01:55:31.748Z" }, + { url = "https://files.pythonhosted.org/packages/cb/95/cc47c4607cc78f55ce3081ade9161961795c15c049bf219f27a393f85767/mlx-0.30.1-cp310-cp310-manylinux_2_35_x86_64.whl", hash = "sha256:37ea97b3c4bd71b19d87c6ef2c9e681e11f37908d8381fc2b785d2509b0681df", size = 692336, upload-time = "2025-12-18T01:55:33.224Z" }, + { url = "https://files.pythonhosted.org/packages/07/14/74acbd677ececd17a44dafda1b472aebacef54f60ff9a41a801f711de9a7/mlx-0.30.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:acfd7d1b8e5b9fa1b7e9fab4cc5ba6a492c559fbb1c5aeab16c1d7a148ab4f1b", size = 593048, upload-time = "2025-12-18T01:55:34.9Z" }, + { url = "https://files.pythonhosted.org/packages/58/8c/5309848afb9c53d363f59b88ae5811de248e2817e91aeadf007e2ac8d22b/mlx-0.30.1-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:b62030471272d1835b8137164bd43d863cc93ff1d67ec4f1f87bb4c8613dd5a6", size = 593043, upload-time = "2025-12-18T01:55:36.839Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5a/0039815a930f0193e2cffb27c57dc6971004bce0086c2bbbdb10395c272c/mlx-0.30.1-cp311-cp311-macosx_26_0_arm64.whl", hash = "sha256:0489cd340f2d262cb3aaad4368e40e84b152e182e4cea37ba018e56c72e1d020", size = 567014, upload-time = "2025-12-18T00:15:51.731Z" }, + { url = "https://files.pythonhosted.org/packages/de/c7/6bdb5497c1f5ed3e33afa7785761ad87fd3436c071805d9a93c905943f04/mlx-0.30.1-cp311-cp311-manylinux_2_35_aarch64.whl", hash = "sha256:fbdcfc3ed556a7e701a8eb67da299e2a25f52615193833ca6374decca3be5bf4", size = 658930, upload-time = "2025-12-18T01:55:38.441Z" }, + { url = "https://files.pythonhosted.org/packages/91/02/2d86a1c116e951eb4d88fe313c321e23628ce7404712e1258cacf925a8b8/mlx-0.30.1-cp311-cp311-manylinux_2_35_x86_64.whl", hash = "sha256:68ec854e7b5f89454e67d6c2fa7bb416b8afb148003ccd775904ec6ec4744818", size = 692484, upload-time = "2025-12-18T01:55:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/3a/4b/ad57b2f0ede3f0d009c0e3e1270c219bd18f9025388855ee149680cffa20/mlx-0.30.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:deaef3ecd2f99930867a29de748e3bffa9cc7e4dfa834f2501c37ed29aece1cc", size = 593397, upload-time = "2025-12-18T01:55:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/ef/14/7fa03a0f66ac3cfb2fd6752178a1488f13c7233fff26eed0f832d961db35/mlx-0.30.1-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:86ccdcda0b5ea4768b87da25beae5b83ac7cc802506116b6845cea6f450e2377", size = 593397, upload-time = "2025-12-18T01:55:43Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c8/9f1343dbe2381f9653df4e0a62dc8bf38f575a2553dc2aa6916de32d2a85/mlx-0.30.1-cp312-cp312-macosx_26_0_arm64.whl", hash = "sha256:a625cb434b2acc5674fe10683374641dab9671fb354ae7c2c67a1fb0405eeb37", size = 567576, upload-time = "2025-12-18T00:15:55.114Z" }, + { url = "https://files.pythonhosted.org/packages/15/ff/485ed9c99c18ef89ac987178c0a526cb4148ba38b14838d315311d9d76a8/mlx-0.30.1-cp312-cp312-manylinux_2_35_aarch64.whl", hash = "sha256:ccc1ff3aca8fb1073c7dcd1274cebe48ae75f852d14b16c7db8228fbbad594dd", size = 643654, upload-time = "2025-12-18T01:55:44.165Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d3/54d3bf5e404c3b6424b49c505dc8b3c06c6bb498fe720195b1fafbd69b5e/mlx-0.30.1-cp312-cp312-manylinux_2_35_x86_64.whl", hash = "sha256:55ed7fc4b389d6e49dac6d34a97b41e61cbe3662ac29c3d29cf612e6b2ed9827", size = 687305, upload-time = "2025-12-18T01:55:45.526Z" }, + { url = "https://files.pythonhosted.org/packages/f9/fd/c6f56cd87d48763ed63655ace627c06db9819eae7d43d132f40d4965947a/mlx-0.30.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:743520758bc8261b2ed8f3b3dc96e4e9236769dd8f61fb17877c5e44037e2058", size = 593366, upload-time = "2025-12-18T01:55:46.786Z" }, + { url = "https://files.pythonhosted.org/packages/dc/53/96d8c48b21f91c4216b6d2ef6dfc10862e5fb0b811a2aaf02c96c78601de/mlx-0.30.1-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:fc9745bc1860ca60128e3a6d36157da06d936e2b4007a4dcba990b40202f598f", size = 593368, upload-time = "2025-12-18T01:55:48.363Z" }, + { url = "https://files.pythonhosted.org/packages/70/ce/476c3b7d3a4153bd0e1c5af1f1b6c09a804b652bbed34072404b322c22e0/mlx-0.30.1-cp313-cp313-macosx_26_0_arm64.whl", hash = "sha256:a1480399c67bb327a66c5527b73915132e3fcaae3bce9634e5c81ccad9f43229", size = 567561, upload-time = "2025-12-18T00:15:56.153Z" }, + { url = "https://files.pythonhosted.org/packages/33/41/7ad1e639fd7dd1cf01a62c1c5b051024a859888c27504996e9d8380e6754/mlx-0.30.1-cp313-cp313-manylinux_2_35_aarch64.whl", hash = "sha256:8e19850a4236a8e174f851f5789b8b62a8eb74f5a8fa49ad8ba286c5ddb5f9bf", size = 643122, upload-time = "2025-12-18T01:55:49.607Z" }, + { url = "https://files.pythonhosted.org/packages/d0/dc/72d3737c5b0662eb5e785d353dbc5e34d793d27b09b99e39993ee051bd19/mlx-0.30.1-cp313-cp313-manylinux_2_35_x86_64.whl", hash = "sha256:1c8ed5bcd9f1910fca209e95859ac737e60b3e1954181b820fa269158f81049a", size = 687254, upload-time = "2025-12-18T01:55:51.239Z" }, + { url = "https://files.pythonhosted.org/packages/9b/cc/523448996247bb05d9d68e23bccf3dafdda660befb9330f6bd5fa13361e8/mlx-0.30.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d34cc2c25b0ee41c1349f14650db760e282685339858e305453f62405c12bc1b", size = 596006, upload-time = "2025-12-18T01:55:52.463Z" }, + { url = "https://files.pythonhosted.org/packages/23/0e/f9f2f9659c34c87be8f4167f6a1d6ed7e826f4889d20eecd4c0d8122f0e9/mlx-0.30.1-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:4e47d301e9095b87f0bda8827bfd6ffe744223aba5cee8f28e25894d647f5823", size = 596008, upload-time = "2025-12-18T01:55:54.02Z" }, + { url = "https://files.pythonhosted.org/packages/56/a7/49e41fb141de95b6a376091a963c737839c9cda04e423c67f57460a50458/mlx-0.30.1-cp314-cp314-macosx_26_0_arm64.whl", hash = "sha256:cfba13e2a52255d663a1ad62f0f83eb3991e42147edf9a8d38cdd224e48ca49b", size = 570406, upload-time = "2025-12-18T00:15:57.177Z" }, + { url = "https://files.pythonhosted.org/packages/73/99/a43cb112167cf865c069f5e108ae42f5314663930ff3dd86c2d23d984191/mlx-0.30.1-cp314-cp314-manylinux_2_35_aarch64.whl", hash = "sha256:bebfec377208eb29cc88aa86c897c7446aa0984838669e138f273f9225d627ff", size = 646461, upload-time = "2025-12-18T01:55:55.285Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ff/1e1968f107b4221a98dc26832586b1f646b27ddf3e55c95051c09d751f0a/mlx-0.30.1-cp314-cp314-manylinux_2_35_x86_64.whl", hash = "sha256:d18012d5cf0f013bc4a405cfd1e9d2d28e798f4d2dc4f15aa0fbffff73c02ba2", size = 687114, upload-time = "2025-12-18T01:55:56.506Z" }, ] [[package]] name = "mlx-metal" -version = "0.30.3" +version = "0.30.1" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/63/4d8f6fefb507c028df4454dabfe8d8e0ad2961bb06510b6aca23d2d5b2be/mlx_metal-0.30.3-py3-none-macosx_14_0_arm64.whl", hash = "sha256:6276312b02353714c7c6515169569fe1c4bebe3229c8ecf1fdb375a13e78c966", size = 37716245, upload-time = "2026-01-14T01:16:34.838Z" }, - { url = "https://files.pythonhosted.org/packages/35/91/1d452e48a4bb4958844fd3bb28ae31b8de110549c009ebec5024ce27ebf3/mlx_metal-0.30.3-py3-none-macosx_15_0_arm64.whl", hash = "sha256:c096c0a3428f3f96a06220f97a36f9528b18bc05173f821eb05bc8458e723fa8", size = 37712125, upload-time = "2026-01-14T01:16:38.619Z" }, - { url = "https://files.pythonhosted.org/packages/fe/36/7a3cbca85542b5ca4faf871e35927f43aa0e3fc830ae5b699780fe723677/mlx_metal-0.30.3-py3-none-macosx_26_0_arm64.whl", hash = "sha256:69068533bd1ee8b0379ce5de57ed5fd313577a10ecab58e1332fd1ff7248a75e", size = 46488962, upload-time = "2026-01-14T05:52:04.523Z" }, + { url = "https://files.pythonhosted.org/packages/09/3f/0be35ddad7e13d8ecd33a9185895f9739bb00b96ef0cce36cf0405d4aec0/mlx_metal-0.30.1-py3-none-macosx_14_0_arm64.whl", hash = "sha256:e7e92c6bdbd7ac8083f528a4c6640552ae106a57bb3d99856ac10a32e93a4b5e", size = 36864966, upload-time = "2025-12-18T01:55:31.473Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1f/c0bddd0d5bf3871411aabe32121e09e1b7cdbece8917a49d5a442310e3e5/mlx_metal-0.30.1-py3-none-macosx_15_0_arm64.whl", hash = "sha256:bb50f57418af7fc3c42a2da2c4bde0e7ab7ac0b997de1f6f642a6680ac65d626", size = 36859011, upload-time = "2025-12-18T01:55:34.541Z" }, + { url = "https://files.pythonhosted.org/packages/67/b3/73cc2f584ac612a476096d35a61eed75ee7ed8b4e320b0c36cf60a14d4eb/mlx_metal-0.30.1-py3-none-macosx_26_0_arm64.whl", hash = "sha256:e0b151a0053ac00b4226710bfb6dbf54b87283fb01e10fb3877f9ea969f680aa", size = 44981160, upload-time = "2025-12-18T00:15:47.518Z" }, ] [[package]] @@ -4495,7 +4495,7 @@ docs = [ [package.metadata] requires-dist = [ { name = "accelerate", marker = "extra == 'moondream'", specifier = "~=1.10.0" }, - { name = "aic-sdk", marker = "extra == 'aic'", specifier = ">=1.2.0" }, + { name = "aic-sdk", marker = "extra == 'aic'", specifier = "~=1.2.0" }, { name = "aioboto3", marker = "extra == 'aws'", specifier = "~=15.5.0" }, { name = "aiofiles", specifier = ">=24.1.0,<25" }, { name = "aiohttp", specifier = ">=3.11.12,<4" }, @@ -5280,7 +5280,7 @@ wheels = [ [[package]] name = "pyrnnoise" -version = "0.4.3" +version = "0.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "audiolab" }, @@ -5290,10 +5290,9 @@ dependencies = [ { name = "tqdm" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/90/51bb94bcfd8aab186fd08902e0706a6eda5813485fb57eff011ce6ae4c51/pyrnnoise-0.4.3-py3-none-macosx_15_0_universal2.whl", hash = "sha256:bdd8e933d32457362e6f4e56831afa8155208825040ab075c4223baed755fa4f", size = 13381834, upload-time = "2026-01-14T08:44:28.263Z" }, - { url = "https://files.pythonhosted.org/packages/04/51/993a25a8b5220e23e0a31ff98747b8fce4685336e0fc4e8e156feab5c4f1/pyrnnoise-0.4.3-py3-none-manylinux1_x86_64.whl", hash = "sha256:1b094777e73797c5dd647782902c691ebb9a3c456c878e742597f5b55535a3db", size = 13273307, upload-time = "2026-01-14T08:44:27.801Z" }, - { url = "https://files.pythonhosted.org/packages/aa/e4/9a13ede6521360341314bf90d5b687cd3f1bd4259bfea740dbc88340484a/pyrnnoise-0.4.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:161c57e05257e0b51f1b21675dcb2debb8cc86903c1fe2ccc3feb4322e545732", size = 13267247, upload-time = "2026-01-14T08:44:30.119Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e9/795f8504fa7f07fc16e99e82413a6fe997df1999e18bb6fab0b428431a92/pyrnnoise-0.4.3-py3-none-win_amd64.whl", hash = "sha256:25e7d8d63f251238a439e6e3d54ad8cb147c9f2b7c7c56fc9d9a496f682d8b06", size = 13267061, upload-time = "2026-01-14T08:45:03.444Z" }, + { url = "https://files.pythonhosted.org/packages/59/49/7017ffa14230096e0271bd49dfd9ab60a32bfebe7e71399c2a0e38c6f859/pyrnnoise-0.4.1-py3-none-macosx_15_0_universal2.whl", hash = "sha256:c1fe407729190d0f84f3e3c9d9322ebbd33b27f3f5d9f7217379b71a4dd043e7", size = 13381833, upload-time = "2025-11-25T15:54:06.532Z" }, + { url = "https://files.pythonhosted.org/packages/8e/24/fb8b7bafb3dd9cbb46e134fa25c9597683c61b42c0133453fefeebeb0066/pyrnnoise-0.4.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:ddd39b45221b65fb235f882a0ce127513a1012d41c5b3ba9dc4e9e991b22c205", size = 13273307, upload-time = "2025-11-25T15:54:04.076Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8e/eef9b2022fa5b9a111ba31d2f25ccd6e45da3daf16d20352e1fb18fd81dd/pyrnnoise-0.4.1-py3-none-win_amd64.whl", hash = "sha256:440e32359256eb7947e29fb080e800e984ba521fbe89a8b0b2f5dc196965e441", size = 13267076, upload-time = "2025-11-25T15:54:37.547Z" }, ] [[package]] From d3bdd2d2469a608b13037239752e65077e3afcd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Fri, 16 Jan 2026 00:19:29 +0100 Subject: [PATCH 04/50] use new model id. --- examples/foundational/07zd-interruptible-aicoustics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/foundational/07zd-interruptible-aicoustics.py b/examples/foundational/07zd-interruptible-aicoustics.py index 479ae36b5..7a2d692e4 100644 --- a/examples/foundational/07zd-interruptible-aicoustics.py +++ b/examples/foundational/07zd-interruptible-aicoustics.py @@ -50,7 +50,7 @@ def _create_aic_filter() -> AICFilterV2: return AICFilterV2( license_key=license_key, - model_id="quail-xxs-48khz", + model_id="sparrow-xxs-48khz", enhancement_level=0.5, ) From a90c15362c3cd4de804bb3943f5f44226f7f755c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Fri, 16 Jan 2026 12:31:41 +0100 Subject: [PATCH 05/50] drop v1 support from aic. --- pyproject.toml | 2 +- src/pipecat/audio/filters/aic_filter.py | 269 +----------------------- src/pipecat/audio/utils.py | 69 +----- src/pipecat/audio/vad/aic_vad.py | 161 +------------- 4 files changed, 12 insertions(+), 489 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f209a4c64..65a2a4bf0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ Issues = "https://github.com/pipecat-ai/pipecat/issues" Changelog = "https://github.com/pipecat-ai/pipecat/blob/main/CHANGELOG.md" [project.optional-dependencies] -aic = [ "aic-sdk>=1.2.0" ] +aic = [ "aic-sdk>=2.0.0" ] anthropic = [ "anthropic~=0.49.0" ] assemblyai = [ "pipecat-ai[websockets-base]" ] asyncai = [ "pipecat-ai[websockets-base]" ] diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index f71c09700..d9d7a18a1 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -11,8 +11,7 @@ enhance audio streams in real time. It mirrors the structure of other filters li the Koala filter and integrates with Pipecat's input transport pipeline. Classes: - AICFilter: For aic-sdk < 2.0.0 (uses 'aic' module) - AICFilterV2: For aic-sdk >= 2.0.0 (uses 'aic_sdk' module) + AICFilter: For aic-sdk >= 2.0.0 (uses 'aic_sdk' module) """ import os @@ -31,258 +30,8 @@ class AICFilter(BaseAudioFilter): Buffers incoming audio to the model's preferred block size and processes planar frames in-place using float32 samples in the linear -1..+1 range. - .. note:: - This class requires aic-sdk < 2.0.0 (uses 'aic' module). - For aic-sdk >= 2.0.0, use :class:`AICFilterV2` instead. - """ - - def __init__( - self, - *, - license_key: str = "", - model_type: Optional["AICModelType"] = None, - enhancement_level: Optional[float] = 1.0, - voice_gain: Optional[float] = 1.0, - noise_gate_enable: Optional[bool] = True, - ) -> None: - """Initialize the AIC filter. - - Args: - license_key: ai-coustics license key for authentication. - model_type: Model variant to load. If None, defaults to AICModelType.QUAIL_STT. - enhancement_level: Optional overall enhancement strength (0.0..1.0). - voice_gain: Optional linear gain applied to detected speech (0.0..4.0). - noise_gate_enable: Optional enable/disable noise gate (default: True). - - .. deprecated:: 1.3.0 - The `noise_gate_enable` parameter is deprecated and no longer has any effect. - It will be removed in a future version. - """ - from pipecat.audio.utils import check_aic_sdk_version - - check_aic_sdk_version("v1") - - # Import AIC SDK v1 types - from aic import AICModelType - - self._license_key = license_key - self._model_type = model_type if model_type is not None else AICModelType.QUAIL_STT - - self._enhancement_level = enhancement_level - self._voice_gain = voice_gain - if noise_gate_enable is not None: - import warnings - - with warnings.catch_warnings(): - warnings.simplefilter("always") - warnings.warn( - "Parameter `noise_gate_enable` is deprecated and no longer has any effect. " - "It will be removed in a future version. Use AIC VAD instead (create_vad_analyzer()).", - DeprecationWarning, - ) - - self._noise_gate_enable = noise_gate_enable - - self._enabled = True - self._sample_rate = 0 - self._aic_ready = False - self._frames_per_block = 0 - self._audio_buffer = bytearray() - # Model will be created in start() since the API now requires sample_rate - self._aic = None - - def get_vad_factory(self): - """Return a zero-arg factory that will create the VAD once the model exists. - - Returns: - A zero-argument callable that, when invoked, returns an initialized - VoiceActivityDetector bound to the underlying AIC model. Raises a - RuntimeError if the model has not been initialized (i.e. start() - has not been called successfully). - """ - - def _factory(): - if self._aic is None: - raise RuntimeError("AIC model not initialized yet. Call start(sample_rate) first.") - return self._aic.create_vad() - - return _factory - - def create_vad_analyzer( - self, - *, - lookback_buffer_size: Optional[float] = None, - sensitivity: Optional[float] = None, - ): - """Return an analyzer that will lazily instantiate the AIC VAD when ready. - - AIC VAD parameters: - - lookback_buffer_size: - Number of window-length audio buffers used as a lookback buffer. - Higher values increase prediction stability but add latency. - Range: 1.0 .. 20.0, Default (SDK): 6.0 - - sensitivity: - Energy threshold sensitivity. Energy threshold = 10 ** (-sensitivity). - Range: 1.0 .. 15.0, Default (SDK): 6.0 - - Args: - lookback_buffer_size: Optional lookback buffer size to configure on the VAD. - Range: 1.0 .. 20.0. If None, SDK default is used. - sensitivity: Optional sensitivity (energy threshold) to configure on the VAD. - Range: 1.0 .. 15.0. If None, SDK default is used. - - Returns: - A lazily-initialized AICVADAnalyzer that will bind to the VAD backend - once the filter's model has been created (after start(sample_rate)). - """ - from pipecat.audio.vad.aic_vad import AICVADAnalyzer - - return AICVADAnalyzer( - vad_factory=self.get_vad_factory(), - lookback_buffer_size=lookback_buffer_size, - sensitivity=sensitivity, - ) - - async def start(self, sample_rate: int): - """Initialize the filter with the transport's sample rate. - - Args: - sample_rate: The sample rate of the input transport in Hz. - - Returns: - None - """ - from aic import AICParameter, Model - - self._sample_rate = sample_rate - - try: - # Create model with required runtime parameters - self._aic = Model( - model_type=self._model_type, - license_key=self._license_key or None, - sample_rate=self._sample_rate, - channels=1, - ) - self._frames_per_block = self._aic.optimal_num_frames() - - # Optional parameter configuration - if self._enhancement_level is not None: - self._aic.set_parameter( - AICParameter.ENHANCEMENT_LEVEL, - float(self._enhancement_level if self._enabled else 0.0), - ) - if self._voice_gain is not None: - self._aic.set_parameter(AICParameter.VOICE_GAIN, float(self._voice_gain)) - - self._aic_ready = True - - # Log processor information - logger.debug(f"ai-coustics filter started:") - logger.debug(f" Sample rate: {self._sample_rate} Hz") - logger.debug(f" Frames per chunk: {self._frames_per_block}") - logger.debug(f" Enhancement strength: {int(self._enhancement_level * 100)}%") - logger.debug(f" Optimal input buffer size: {self._aic.optimal_num_frames()} samples") - logger.debug(f" Optimal sample rate: {self._aic.optimal_sample_rate()} Hz") - logger.debug( - f" Current algorithmic latency: {self._aic.processing_latency() / self._sample_rate * 1000:.2f}ms" - ) - except Exception as e: # noqa: BLE001 - surfacing SDK initialization errors - logger.error(f"AIC model initialization failed: {e}") - self._aic_ready = False - - async def stop(self): - """Clean up the AIC model when stopping. - - Returns: - None - """ - try: - if self._aic is not None: - self._aic.close() - finally: - self._aic = None - self._aic_ready = False - self._audio_buffer.clear() - - async def process_frame(self, frame: FilterControlFrame): - """Process control frames to enable/disable filtering. - - Args: - frame: The control frame containing filter commands. - - Returns: - None - """ - if isinstance(frame, FilterEnableFrame): - from aic import AICParameter - - self._enabled = frame.enable - if self._aic is not None: - try: - level = float(self._enhancement_level if self._enabled else 0.0) - self._aic.set_parameter(AICParameter.ENHANCEMENT_LEVEL, level) - except Exception as e: # noqa: BLE001 - logger.error(f"AIC set_parameter failed: {e}") - - async def filter(self, audio: bytes) -> bytes: - """Apply AIC enhancement to audio data. - - Buffers incoming audio and processes it in chunks that match the AIC - model's required block length. Returns enhanced audio data. - - Args: - audio: Raw audio data as bytes to be filtered (int16 PCM, planar). - - Returns: - Enhanced audio data as bytes (int16 PCM, planar). - """ - if not self._aic_ready or self._aic is None: - return audio - - self._audio_buffer.extend(audio) - - filtered_chunks: List[bytes] = [] - - # Number of int16 samples currently buffered - available_frames = len(self._audio_buffer) // 2 - - while available_frames >= self._frames_per_block: - # Consume exactly one block worth of frames - samples_to_consume = self._frames_per_block * 1 - bytes_to_consume = samples_to_consume * 2 - block_bytes = bytes(self._audio_buffer[:bytes_to_consume]) - - # Convert to float32 in -1..+1 range and reshape to planar (channels, frames) - block_i16 = np.frombuffer(block_bytes, dtype=np.int16) - block_f32 = (block_i16.astype(np.float32) / 32768.0).reshape( - (1, self._frames_per_block) - ) - - # Process planar in-place; returns ndarray (same shape) - out_f32 = await self._aic.process_async(block_f32) - - # Convert back to int16 bytes, planar layout - out_i16 = np.clip(out_f32 * 32768.0, -32768, 32767).astype(np.int16) - filtered_chunks.append(out_i16.reshape(-1).tobytes()) - - # Slide buffer - self._audio_buffer = self._audio_buffer[bytes_to_consume:] - available_frames = len(self._audio_buffer) // 2 - - # Do not flush incomplete frames; keep them buffered for the next call - return b"".join(filtered_chunks) - - -class AICFilterV2(BaseAudioFilter): - """Audio filter using ai-coustics' AIC SDK v2 for real-time enhancement. - - Buffers incoming audio to the model's preferred block size and processes - planar frames in-place using float32 samples in the linear -1..+1 range. - .. note:: This class requires aic-sdk >= 2.0.0 (uses 'aic_sdk' module). - For aic-sdk < 2.0.0, use :class:`AICFilter` instead. """ def __init__( @@ -311,10 +60,6 @@ class AICFilterV2(BaseAudioFilter): Raises: ValueError: If neither model_id nor model_path is provided. """ - from pipecat.audio.utils import check_aic_sdk_version - - check_aic_sdk_version("v2") - if model_id is None and model_path is None: raise ValueError( "Either 'model_id' or 'model_path' must be provided. " @@ -337,7 +82,7 @@ class AICFilterV2(BaseAudioFilter): self._frames_per_block = 0 self._audio_buffer = bytearray() - # v2 API objects + # AIC SDK objects self._model = None self._processor = None self._processor_ctx = None @@ -362,7 +107,7 @@ class AICFilterV2(BaseAudioFilter): ): """Return an analyzer that will lazily instantiate the AIC VAD when ready. - AIC VAD parameters (v2): + AIC VAD parameters: - speech_hold_duration: How long VAD continues detecting after speech ends (in seconds). Range: 0.0 .. 20x model window length, Default (SDK): 0.05s @@ -377,12 +122,12 @@ class AICFilterV2(BaseAudioFilter): Range: 1.0 .. 15.0. If None, SDK default (6.0) is used. Returns: - A lazily-initialized AICVADAnalyzerV2 that will bind to the VAD context + A lazily-initialized AICVADAnalyzer that will bind to the VAD context once the filter's processor has been created (after start(sample_rate)). """ - from pipecat.audio.vad.aic_vad import AICVADAnalyzerV2 + from pipecat.audio.vad.aic_vad import AICVADAnalyzer - return AICVADAnalyzerV2( + return AICVADAnalyzer( vad_context_factory=lambda: self.get_vad_context(), speech_hold_duration=speech_hold_duration, sensitivity=sensitivity, @@ -446,7 +191,7 @@ class AICFilterV2(BaseAudioFilter): self._aic_ready = True # Log processor information - logger.debug(f"ai-coustics filter (v2) started:") + logger.debug(f"ai-coustics filter started:") logger.debug(f" Model ID: {self._model.get_id()}") logger.debug(f" Sample rate: {self._sample_rate} Hz") logger.debug(f" Frames per chunk: {self._frames_per_block}") diff --git a/src/pipecat/audio/utils.py b/src/pipecat/audio/utils.py index c61aba4dd..29fc44ea9 100644 --- a/src/pipecat/audio/utils.py +++ b/src/pipecat/audio/utils.py @@ -14,10 +14,9 @@ various audio formats used in Pipecat pipelines. import audioop from typing import Literal -from loguru import logger - import numpy as np import pyloudnorm as pyln +from loguru import logger from pipecat.audio.resamplers.base_audio_resampler import BaseAudioResampler from pipecat.audio.resamplers.soxr_resampler import SOXRAudioResampler @@ -314,69 +313,3 @@ def is_silence(pcm_bytes: bytes) -> bool: # If max value is lower than SPEAKING_THRESHOLD, consider it as silence return max_value <= SPEAKING_THRESHOLD - - -def is_aic_sdk_v2() -> bool: - """Detect if aic-sdk v2 is installed by checking the module name. - - In v2, the module was renamed from 'aic' to 'aic_sdk'. - - Returns: - True if aic-sdk v2 (aic_sdk module) is installed, False if v1 (aic module). - - Raises: - ImportError: If neither aic nor aic_sdk module is installed. - """ - try: - import aic_sdk # noqa: F401 - - return True - except ModuleNotFoundError: - pass - - try: - import aic # noqa: F401 - - return False - except ModuleNotFoundError: - logger.error("In order to use the AIC filter, you need to `pip install pipecat-ai[aic]`.") - raise ImportError( - "aic-sdk is not installed. Install with 'pip install pipecat-ai[aic]'." - ) - - -def check_aic_sdk_version(required_version: Literal["v1", "v2"]) -> None: - """Check if the aic-sdk is installed and compatible with the module. - - This function checks both that the aic-sdk is installed and that its version - is compatible with the module requirements. Version detection is based on - the module name: v2 uses 'aic_sdk', v1 uses 'aic'. - - Args: - required_version: Either "v1" (for aic-sdk < 2.0.0) or "v2" (for aic-sdk >= 2.0.0). - - Raises: - ImportError: If aic-sdk is not installed or version is incompatible. - """ - is_v2 = is_aic_sdk_v2() - - if required_version == "v1" and is_v2: - error_msg = ( - "aic-sdk v2 (aic_sdk module) detected, but v1 (aic module) is required. " - "Please use the v2 classes instead: " - "'from pipecat.audio.filters.aic_filter import AICFilterV2' or " - "'from pipecat.audio.vad.aic_vad import AICVADAnalyzerV2'." - ) - logger.error(error_msg) - raise ImportError(error_msg) - - if required_version == "v2" and not is_v2: - error_msg = ( - "aic-sdk v1 (aic module) detected, but v2 (aic_sdk module) is required. " - "Please update with 'pip install --upgrade aic-sdk>=2.0.0' " - "or use the v1 classes: " - "'from pipecat.audio.filters.aic_filter import AICFilter' or " - "'from pipecat.audio.vad.aic_vad import AICVADAnalyzer'." - ) - logger.error(error_msg) - raise ImportError(error_msg) diff --git a/src/pipecat/audio/vad/aic_vad.py b/src/pipecat/audio/vad/aic_vad.py index 9ff19feec..05b576ce8 100644 --- a/src/pipecat/audio/vad/aic_vad.py +++ b/src/pipecat/audio/vad/aic_vad.py @@ -5,8 +5,7 @@ is_speech_detected() and map it to a float confidence (1.0/0.0). They use 10 ms windows based on the sample rate and apply optional AIC VAD parameters. Classes: - AICVADAnalyzer: For aic-sdk < 2.0.0 (uses 'aic' module) - AICVADAnalyzerV2: For aic-sdk >= 2.0.0 (uses 'aic_sdk' module) + AICVADAnalyzer: For aic-sdk >= 2.0.0 (uses 'aic_sdk' module) """ from typing import Any, Callable, Optional @@ -17,155 +16,6 @@ from pipecat.audio.vad.vad_analyzer import VADAnalyzer, VADParams class AICVADAnalyzer(VADAnalyzer): - """VAD analyzer that lazily instantiates the AIC VoiceActivityDetector via a factory. - - The analyzer can be constructed before the AIC Model exists. Once the filter has - started and the Model is available, the provided factory will succeed and the - backend VAD will be created. We then switch to single-sample updates where - num_frames_required() returns 1 and confidence is derived from the backend's - boolean is_speech_detected() state. - - AIC VAD runtime parameters: - - lookback_buffer_size: - Controls the lookback buffer size used by the VAD, i.e. the number of - window-length audio buffers used as a lookback buffer. Larger values improve - stability but increase latency. - Range: 1.0 .. 20.0 - Default (SDK): 6.0 - - sensitivity: - Controls the energy threshold sensitivity. Higher values make the detector - less sensitive (require more energy to count as speech). - Range: 1.0 .. 15.0 - Formula: Energy threshold = 10 ** (-sensitivity) - Default (SDK): 6.0 - - .. note:: - This class requires aic-sdk < 2.0.0 (uses 'aic' module). - For aic-sdk >= 2.0.0, use :class:`AICVADAnalyzerV2` instead. - """ - - def __init__( - self, - *, - vad_factory: Optional[Callable[[], Any]] = None, - lookback_buffer_size: Optional[float] = None, - sensitivity: Optional[float] = None, - ): - """Create an AIC VAD analyzer. - - Args: - vad_factory: - Zero-arg callable that returns an initialized AIC VoiceActivityDetector. - This may raise until the filter's Model has been created; the analyzer - will retry on set_sample_rate/first use. - lookback_buffer_size: - Optional override for AIC VAD lookback buffer size. - Range: 1.0 .. 20.0. Larger values increase stability at the cost of latency. - If None, the SDK default (6.0) is used. - sensitivity: - Optional override for AIC VAD sensitivity (energy threshold). - Range: 1.0 .. 15.0. Energy threshold = 10 ** (-sensitivity). - If None, the SDK default (6.0) is used. - """ - from pipecat.audio.utils import check_aic_sdk_version - - check_aic_sdk_version("v1") - - # Use fixed VAD parameters for AIC: no user override - fixed_params = VADParams(confidence=0.5, start_secs=0.0, stop_secs=0.0, min_volume=0.0) - super().__init__(sample_rate=None, params=fixed_params) - self._vad_factory = vad_factory - self._backend_vad: Optional[Any] = None - self._pending_lookback: Optional[float] = lookback_buffer_size - self._pending_sensitivity: Optional[float] = sensitivity - - def bind_vad_factory(self, vad_factory: Callable[[], Any]): - """Attach or replace the factory post-construction.""" - self._vad_factory = vad_factory - self._ensure_backend_initialized() - - def _apply_backend_params(self): - """Apply optional AIC VAD parameters if available.""" - from aic import AICVadParameter - - if self._backend_vad is None or AICVadParameter is None: - return - try: - if self._pending_lookback is not None: - self._backend_vad.set_parameter( - AICVadParameter.LOOKBACK_BUFFER_SIZE, float(self._pending_lookback) - ) - if self._pending_sensitivity is not None: - self._backend_vad.set_parameter( - AICVadParameter.SENSITIVITY, float(self._pending_sensitivity) - ) - except Exception as e: # noqa: BLE001 - logger.debug(f"AIC VAD parameter application deferred/failed: {e}") - - def _ensure_backend_initialized(self): - if self._backend_vad is not None: - return - if not self._vad_factory: - return - try: - self._backend_vad = self._vad_factory() - self._apply_backend_params() - # With backend ready, recompute internal frame sizing - super().set_params(self._params) - logger.debug("AIC VAD backend initialized in analyzer.") - except Exception as e: # noqa: BLE001 - # Filter may not be started yet; try again later - logger.debug(f"Deferring AIC VAD backend initialization: {e}") - - def set_sample_rate(self, sample_rate: int): - """Set the sample rate for audio processing. - - Args: - sample_rate: Audio sample rate in Hz. - """ - # Set rate and attempt backend initialization once we know SR - self._sample_rate = self._init_sample_rate or sample_rate - self._ensure_backend_initialized() - # Ensure params are initialized even if backend not ready yet - try: - super().set_params(self._params) - except Exception: - pass - - def num_frames_required(self) -> int: - """Get the number of audio frames required for analysis. - - Returns: - Number of frames needed for VAD processing. - """ - # Use 10 ms windows based on sample rate - return int(self.sample_rate * 0.01) if self.sample_rate > 0 else 160 - - def voice_confidence(self, buffer: bytes) -> float: - """Calculate voice activity confidence for the given audio buffer. - - Args: - buffer: Audio buffer to analyze. - - Returns: - Voice confidence score is 0.0 or 1.0. - """ - # Ensure backend exists (filter might have started since last call) - self._ensure_backend_initialized() - if self._backend_vad is None: - return 0.0 - - # We do not need to analyze 'buffer' here since the model's VAD is updated - # as part of the enhancement pipeline. Simply query the boolean and map it. - try: - is_speech = self._backend_vad.is_speech_detected() - return 1.0 if is_speech else 0.0 - except Exception as e: # noqa: BLE001 - logger.error(f"AIC VAD inference error: {e}") - return 0.0 - - -class AICVADAnalyzerV2(VADAnalyzer): """VAD analyzer that lazily binds to the AIC VadContext via a factory. The analyzer can be constructed before the AIC Processor exists. Once the filter has @@ -173,7 +23,7 @@ class AICVADAnalyzerV2(VADAnalyzer): VadContext will be obtained. We then use the context's is_speech_detected() state to derive confidence values. - AIC VAD runtime parameters (v2): + AIC VAD runtime parameters: - speech_hold_duration: Controls for how long the VAD continues to detect speech after the audio signal no longer contains speech (in seconds). @@ -188,7 +38,6 @@ class AICVADAnalyzerV2(VADAnalyzer): .. note:: This class requires aic-sdk >= 2.0.0 (uses 'aic_sdk' module). - For aic-sdk < 2.0.0, use :class:`AICVADAnalyzer` instead. """ def __init__( @@ -214,10 +63,6 @@ class AICVADAnalyzerV2(VADAnalyzer): Range: 1.0 .. 15.0. Energy threshold = 10 ** (-sensitivity). If None, the SDK default (6.0) is used. """ - from pipecat.audio.utils import check_aic_sdk_version - - check_aic_sdk_version("v2") - # Use fixed VAD parameters for AIC: no user override fixed_params = VADParams(confidence=0.5, start_secs=0.0, stop_secs=0.0, min_volume=0.0) super().__init__(sample_rate=None, params=fixed_params) @@ -259,7 +104,7 @@ class AICVADAnalyzerV2(VADAnalyzer): self._apply_vad_params() # With VAD context ready, recompute internal frame sizing super().set_params(self._params) - logger.debug("AIC VAD context (v2) initialized in analyzer.") + logger.debug("AIC VAD context initialized in analyzer.") except Exception as e: # noqa: BLE001 # Filter may not be started yet; try again later logger.debug(f"Deferring AIC VAD context initialization: {e}") From 2a927189d93b0effd0d9b0946cd7af3ed9795587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Fri, 16 Jan 2026 12:37:59 +0100 Subject: [PATCH 06/50] reorganize imports. --- src/pipecat/audio/filters/aic_filter.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index d9d7a18a1..a65be0db9 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -18,9 +18,11 @@ import os from typing import List, Optional import numpy as np +from aic_sdk import Model, ProcessorAsync, ProcessorConfig, ProcessorParameter from loguru import logger from pipecat.audio.filters.base_audio_filter import BaseAudioFilter +from pipecat.audio.vad.aic_vad import AICVADAnalyzer from pipecat.frames.frames import FilterControlFrame, FilterEnableFrame @@ -125,8 +127,6 @@ class AICFilter(BaseAudioFilter): A lazily-initialized AICVADAnalyzer that will bind to the VAD context once the filter's processor has been created (after start(sample_rate)). """ - from pipecat.audio.vad.aic_vad import AICVADAnalyzer - return AICVADAnalyzer( vad_context_factory=lambda: self.get_vad_context(), speech_hold_duration=speech_hold_duration, @@ -142,8 +142,6 @@ class AICFilter(BaseAudioFilter): Returns: None """ - from aic_sdk import Model, ProcessorAsync, ProcessorConfig, ProcessorParameter - self._sample_rate = sample_rate try: @@ -232,8 +230,6 @@ class AICFilter(BaseAudioFilter): None """ if isinstance(frame, FilterEnableFrame): - from aic_sdk import ProcessorParameter - self._enabled = frame.enable if self._processor_ctx is not None: try: From a13380b5746e8d0d7f257915caae3cf459592653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Fri, 16 Jan 2026 12:53:15 +0100 Subject: [PATCH 07/50] clean up unused imports in audio utils. --- src/pipecat/audio/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pipecat/audio/utils.py b/src/pipecat/audio/utils.py index 29fc44ea9..65f451675 100644 --- a/src/pipecat/audio/utils.py +++ b/src/pipecat/audio/utils.py @@ -12,11 +12,9 @@ various audio formats used in Pipecat pipelines. """ import audioop -from typing import Literal import numpy as np import pyloudnorm as pyln -from loguru import logger from pipecat.audio.resamplers.base_audio_resampler import BaseAudioResampler from pipecat.audio.resamplers.soxr_resampler import SOXRAudioResampler From 61a230ec53cd26b37ad8543b8b5b78abc1899646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Mon, 19 Jan 2026 13:56:36 +0100 Subject: [PATCH 08/50] Update src/pipecat/audio/filters/aic_filter.py Co-authored-by: Stephan Eckes --- src/pipecat/audio/filters/aic_filter.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index a65be0db9..cb5b0e0ce 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -156,22 +156,14 @@ class AICFilter(BaseAudioFilter): logger.debug(f"Model downloaded to: {model_path}") self._model = Model.from_file(model_path) - # Create async processor - self._processor = ProcessorAsync(self._model, self._license_key or "") - - # Get optimal frames for this sample rate - self._frames_per_block = self._model.get_optimal_num_frames(self._sample_rate) - # Create configuration - config = ProcessorConfig( + config = ProcessorConfig.optimal( + self._model, sample_rate=self._sample_rate, - num_channels=1, - num_frames=self._frames_per_block, - allow_variable_frames=False, ) - # Initialize processor - await self._processor.initialize_async(config) + # Create async processor + self._processor = ProcessorAsync(self._model, self._license_key or "", config) # Get contexts for parameter control and VAD self._processor_ctx = self._processor.get_processor_context() From a1cc88a233d82838d6a394a9ee507e0e9aca816b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Mon, 19 Jan 2026 15:12:28 +0100 Subject: [PATCH 09/50] Update src/pipecat/audio/filters/aic_filter.py Co-authored-by: Tobias <76444201+Fl1tzi@users.noreply.github.com> --- src/pipecat/audio/filters/aic_filter.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index cb5b0e0ce..d4de454b2 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -260,9 +260,13 @@ class AICFilter(BaseAudioFilter): # Convert to float32 in -1..+1 range and reshape to (channels, frames) block_i16 = np.frombuffer(block_bytes, dtype=np.int16) - block_f32 = (block_i16.astype(np.float32) / 32768.0).reshape( - (1, self._frames_per_block) - ) + # Convert to float32 and normalize + block_f32 = block_i16.astype(np.float32) + + block_f32 *= (1.0 / 32768.0) + + # Reshape to (1, frames) for AIC SDK + block_f32 = block_f32.reshape((1, self._frames_per_block)) # Process via async processor; returns ndarray (same shape) out_f32 = await self._processor.process_async(block_f32) From 4e85e81d9b440cc3f7bbebfc1a6f105c4e4740e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Mon, 19 Jan 2026 15:12:37 +0100 Subject: [PATCH 10/50] Update src/pipecat/audio/filters/aic_filter.py Co-authored-by: Tobias <76444201+Fl1tzi@users.noreply.github.com> --- src/pipecat/audio/filters/aic_filter.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index d4de454b2..14b95c92d 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -272,7 +272,12 @@ class AICFilter(BaseAudioFilter): out_f32 = await self._processor.process_async(block_f32) # Convert back to int16 bytes - out_i16 = np.clip(out_f32 * 32768.0, -32768, 32767).astype(np.int16) + # Denormalize and convert back to int16 + out_f32 *= 32768.0 + + # In-place clip to valid int16 range (-32768 to 32767) + np.clip(out_f32, -32768.0, 32767, out=out_f32) + out_i16 = out_f32.astype(dtype) filtered_chunks.append(out_i16.reshape(-1).tobytes()) # Slide buffer From ec17dc66267c3a45d43853ae1f42c2465264a610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Sun, 11 Jan 2026 20:14:59 +0100 Subject: [PATCH 11/50] aic-sdk-py v2. # Conflicts: # uv.lock # Conflicts: # examples/foundational/07zd-interruptible-aicoustics.py # pyproject.toml # src/pipecat/audio/filters/aic_filter.py # src/pipecat/audio/vad/aic_vad.py --- src/pipecat/audio/filters/aic_filter_v2.py | 296 +++++++++++++++++++++ src/pipecat/audio/utils.py | 57 ++++ src/pipecat/audio/vad/aic_vad_v2.py | 163 ++++++++++++ 3 files changed, 516 insertions(+) create mode 100644 src/pipecat/audio/filters/aic_filter_v2.py create mode 100644 src/pipecat/audio/vad/aic_vad_v2.py diff --git a/src/pipecat/audio/filters/aic_filter_v2.py b/src/pipecat/audio/filters/aic_filter_v2.py new file mode 100644 index 000000000..be2912107 --- /dev/null +++ b/src/pipecat/audio/filters/aic_filter_v2.py @@ -0,0 +1,296 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""ai-coustics AIC SDK audio filter for Pipecat (aic-sdk >= 2.0.0). + +This module provides an audio filter implementation using ai-coustics' AIC SDK to +enhance audio streams in real time. It mirrors the structure of other filters like +the Koala filter and integrates with Pipecat's input transport pipeline. + +.. note:: + This module is compatible with aic-sdk versions >= 2.0.0. + For aic-sdk < 2.0.0, use :mod:`pipecat.audio.filters.aic_filter` instead. +""" + +import os +from typing import List, Optional + +import numpy as np +from loguru import logger + +from pipecat.audio.filters.base_audio_filter import BaseAudioFilter +from pipecat.audio.utils import check_aic_sdk_version +from pipecat.frames.frames import FilterControlFrame, FilterEnableFrame + +# Check aic-sdk is installed and version is compatible (>= 2.0.0) +check_aic_sdk_version("v2") + +# AIC SDK v2 (https://ai-coustics.github.io/aic-sdk-py/api/) +import aic +from aic import Model, ProcessorAsync, ProcessorConfig, ProcessorParameter, VadParameter + + +class AICFilter(BaseAudioFilter): + """Audio filter using ai-coustics' AIC SDK v2 for real-time enhancement. + + Buffers incoming audio to the model's preferred block size and processes + planar frames in-place using float32 samples in the linear -1..+1 range. + + .. note:: + This class requires aic-sdk >= 2.0.0. + """ + + def __init__( + self, + *, + license_key: str = "", + model_id: Optional[str] = None, + model_path: Optional[str] = None, + model_download_dir: Optional[str] = None, + enhancement_level: Optional[float] = 1.0, + voice_gain: Optional[float] = 1.0, + ) -> None: + """Initialize the AIC filter. + + Args: + license_key: ai-coustics license key for authentication. + model_id: Model identifier to download from CDN. Required if model_path + is not provided. See https://artifacts.ai-coustics.io/ for available models. + model_path: Optional path to a local .aicmodel file. If provided, + model_id is ignored and no download occurs. + model_download_dir: Directory for downloading models. Defaults to + a cache directory in user's home folder. + enhancement_level: Optional overall enhancement strength (0.0..1.0). + voice_gain: Optional linear gain applied to detected speech (0.1..4.0). + + Raises: + ValueError: If neither model_id nor model_path is provided. + """ + if model_id is None and model_path is None: + raise ValueError( + "Either 'model_id' or 'model_path' must be provided. " + "See https://artifacts.ai-coustics.io/ for available models." + ) + + self._license_key = license_key + self._model_id = model_id + self._model_path = model_path + self._model_download_dir = model_download_dir or os.path.expanduser( + "~/.cache/pipecat/aic-models" + ) + + self._enhancement_level = enhancement_level + self._voice_gain = voice_gain + + self._enabled = True + self._sample_rate = 0 + self._aic_ready = False + self._frames_per_block = 0 + self._audio_buffer = bytearray() + + # v2 API objects + self._model: Optional[Model] = None + self._processor: Optional[ProcessorAsync] = None + self._processor_ctx = None + self._vad_ctx = None + + def get_vad_context(self): + """Return the VAD context once the processor exists. + + Returns: + The VadContext instance bound to the underlying processor. + Raises RuntimeError if the processor has not been initialized. + """ + if self._vad_ctx is None: + raise RuntimeError("AIC processor not initialized yet. Call start(sample_rate) first.") + return self._vad_ctx + + def create_vad_analyzer( + self, + *, + speech_hold_duration: Optional[float] = None, + sensitivity: Optional[float] = None, + ): + """Return an analyzer that will lazily instantiate the AIC VAD when ready. + + AIC VAD parameters (v2): + - speech_hold_duration: + How long VAD continues detecting after speech ends (in seconds). + Range: 0.0 .. 20x model window length, Default (SDK): 0.05s + - sensitivity: + Energy threshold sensitivity. Energy threshold = 10 ** (-sensitivity). + Range: 1.0 .. 15.0, Default (SDK): 6.0 + + Args: + speech_hold_duration: Optional speech hold duration to configure on the VAD. + If None, SDK default (0.05s) is used. + sensitivity: Optional sensitivity (energy threshold) to configure on the VAD. + Range: 1.0 .. 15.0. If None, SDK default (6.0) is used. + + Returns: + A lazily-initialized AICVADAnalyzer that will bind to the VAD context + once the filter's processor has been created (after start(sample_rate)). + """ + from pipecat.audio.vad.aic_vad_v2 import AICVADAnalyzer + + return AICVADAnalyzer( + vad_context_factory=lambda: self.get_vad_context(), + speech_hold_duration=speech_hold_duration, + sensitivity=sensitivity, + ) + + async def start(self, sample_rate: int): + """Initialize the filter with the transport's sample rate. + + Args: + sample_rate: The sample rate of the input transport in Hz. + + Returns: + None + """ + self._sample_rate = sample_rate + + try: + # Load or download model + if self._model_path: + logger.debug(f"Loading AIC model from: {self._model_path}") + self._model = Model.from_file(self._model_path) + else: + logger.debug(f"Downloading AIC model: {self._model_id}") + os.makedirs(self._model_download_dir, exist_ok=True) + model_path = await Model.download_async(self._model_id, self._model_download_dir) + logger.debug(f"Model downloaded to: {model_path}") + self._model = Model.from_file(model_path) + + # Create async processor + self._processor = ProcessorAsync(self._model, self._license_key or "") + + # Get optimal frames for this sample rate + self._frames_per_block = self._model.get_optimal_num_frames(self._sample_rate) + + # Create configuration + config = ProcessorConfig( + sample_rate=self._sample_rate, + num_channels=1, + num_frames=self._frames_per_block, + allow_variable_frames=False, + ) + + # Initialize processor + await self._processor.initialize_async(config) + + # Get contexts for parameter control and VAD + self._processor_ctx = self._processor.get_processor_context() + self._vad_ctx = self._processor.get_vad_context() + + # Apply initial parameters + if self._enhancement_level is not None: + level = float(self._enhancement_level if self._enabled else 0.0) + self._processor_ctx.set_parameter(ProcessorParameter.EnhancementLevel, level) + if self._voice_gain is not None: + self._processor_ctx.set_parameter( + ProcessorParameter.VoiceGain, float(self._voice_gain) + ) + + self._aic_ready = True + + # Log processor information + logger.debug(f"ai-coustics filter (v2) started:") + logger.debug(f" Model ID: {self._model.get_id()}") + logger.debug(f" Sample rate: {self._sample_rate} Hz") + logger.debug(f" Frames per chunk: {self._frames_per_block}") + logger.debug(f" Enhancement strength: {int((self._enhancement_level or 1.0) * 100)}%") + logger.debug(f" Optimal sample rate: {self._model.get_optimal_sample_rate()} Hz") + logger.debug( + f" Output delay: {self._processor_ctx.get_output_delay()} samples " + f"({self._processor_ctx.get_output_delay() / self._sample_rate * 1000:.2f}ms)" + ) + except Exception as e: # noqa: BLE001 - surfacing SDK initialization errors + logger.error(f"AIC model initialization failed: {e}") + self._aic_ready = False + + async def stop(self): + """Clean up the AIC processor when stopping. + + Returns: + None + """ + try: + if self._processor_ctx is not None: + self._processor_ctx.reset() + finally: + self._processor = None + self._processor_ctx = None + self._vad_ctx = None + self._model = None + self._aic_ready = False + self._audio_buffer.clear() + + async def process_frame(self, frame: FilterControlFrame): + """Process control frames to enable/disable filtering. + + Args: + frame: The control frame containing filter commands. + + Returns: + None + """ + if isinstance(frame, FilterEnableFrame): + self._enabled = frame.enable + if self._processor_ctx is not None: + try: + level = float(self._enhancement_level if self._enabled else 0.0) + self._processor_ctx.set_parameter(ProcessorParameter.EnhancementLevel, level) + except Exception as e: # noqa: BLE001 + logger.error(f"AIC set_parameter failed: {e}") + + async def filter(self, audio: bytes) -> bytes: + """Apply AIC enhancement to audio data. + + Buffers incoming audio and processes it in chunks that match the AIC + model's required block length. Returns enhanced audio data. + + Args: + audio: Raw audio data as bytes to be filtered (int16 PCM, planar). + + Returns: + Enhanced audio data as bytes (int16 PCM, planar). + """ + if not self._aic_ready or self._processor is None: + return audio + + self._audio_buffer.extend(audio) + + filtered_chunks: List[bytes] = [] + + # Number of int16 samples currently buffered + available_frames = len(self._audio_buffer) // 2 + + while available_frames >= self._frames_per_block: + # Consume exactly one block worth of frames + samples_to_consume = self._frames_per_block * 1 + bytes_to_consume = samples_to_consume * 2 + block_bytes = bytes(self._audio_buffer[:bytes_to_consume]) + + # Convert to float32 in -1..+1 range and reshape to (channels, frames) + block_i16 = np.frombuffer(block_bytes, dtype=np.int16) + block_f32 = (block_i16.astype(np.float32) / 32768.0).reshape( + (1, self._frames_per_block) + ) + + # Process via async processor; returns ndarray (same shape) + out_f32 = await self._processor.process_async(block_f32) + + # Convert back to int16 bytes + out_i16 = np.clip(out_f32 * 32768.0, -32768, 32767).astype(np.int16) + filtered_chunks.append(out_i16.reshape(-1).tobytes()) + + # Slide buffer + self._audio_buffer = self._audio_buffer[bytes_to_consume:] + available_frames = len(self._audio_buffer) // 2 + + # Do not flush incomplete frames; keep them buffered for the next call + return b"".join(filtered_chunks) diff --git a/src/pipecat/audio/utils.py b/src/pipecat/audio/utils.py index 65f451675..002375f35 100644 --- a/src/pipecat/audio/utils.py +++ b/src/pipecat/audio/utils.py @@ -12,6 +12,9 @@ various audio formats used in Pipecat pipelines. """ import audioop +from typing import Literal + +from loguru import logger import numpy as np import pyloudnorm as pyln @@ -311,3 +314,57 @@ def is_silence(pcm_bytes: bytes) -> bool: # If max value is lower than SPEAKING_THRESHOLD, consider it as silence return max_value <= SPEAKING_THRESHOLD + + +def check_aic_sdk_version(required_version: Literal["v1", "v2"]) -> None: + """Check if the aic-sdk is installed and compatible with the module. + + This function checks both that the aic-sdk is installed and that its version + is compatible with the module requirements. + + Args: + required_version: Either "v1" (for aic-sdk < 2.0.0) or "v2" (for aic-sdk >= 2.0.0). + + Raises: + ImportError: If aic-sdk is not installed or version is incompatible. + """ + try: + import aic # noqa: F401 - check if module is installed + except ModuleNotFoundError as e: + logger.error(f"Exception: {e}") + logger.error("In order to use the AIC filter, you need to `pip install pipecat-ai[aic]`.") + raise ImportError(f"Missing module: {e}") from e + + try: + from importlib.metadata import version as get_version + + aic_version = get_version("aic-sdk") + major_version = int(aic_version.split(".")[0]) + + if required_version == "v1" and major_version >= 2: + error_msg = ( + f"aic-sdk version {aic_version} detected, but aic-sdk < 2.0.0 is required. " + "Please use the v2 modules instead: " + "'from pipecat.audio.filters.aic_filter_v2 import AICFilter' or " + "'from pipecat.audio.vad.aic_vad_v2 import AICVADAnalyzer'." + ) + logger.error(error_msg) + raise ImportError(error_msg) + + if required_version == "v2" and major_version < 2: + error_msg = ( + f"aic-sdk version {aic_version} detected, but aic-sdk >= 2.0.0 is required. " + "Please update with 'pip install --upgrade aic-sdk>=2.0.0' " + "or use the v1 modules: " + "'from pipecat.audio.filters.aic_filter import AICFilter' or " + "'from pipecat.audio.vad.aic_vad import AICVADAnalyzer'." + ) + logger.error(error_msg) + raise ImportError(error_msg) + + except ImportError: + # Re-raise if it's our version mismatch error + raise + except Exception: + # If we can't determine version for other reasons, log warning and allow to proceed + logger.warning("Could not determine aic-sdk version. Proceeding anyway.") diff --git a/src/pipecat/audio/vad/aic_vad_v2.py b/src/pipecat/audio/vad/aic_vad_v2.py new file mode 100644 index 000000000..2f8129a0d --- /dev/null +++ b/src/pipecat/audio/vad/aic_vad_v2.py @@ -0,0 +1,163 @@ +"""AIC-integrated VAD analyzer that lazily binds to the AIC SDK v2 backend. + +This analyzer queries the backend's is_speech_detected() and maps it to a float +confidence (1.0/0.0). It uses 10 ms windows based on the sample rate and applies +optional AIC VAD parameters (speech_hold_duration, sensitivity) when available. + +.. note:: + This module is compatible with aic-sdk versions >= 2.0.0. + For aic-sdk < 2.0.0, use :mod:`pipecat.audio.vad.aic_vad` instead. +""" + +from typing import Any, Callable, Optional + +from loguru import logger + +from pipecat.audio.utils import check_aic_sdk_version +from pipecat.audio.vad.vad_analyzer import VADAnalyzer, VADParams + +# Check aic-sdk is installed and version is compatible (>= 2.0.0) +check_aic_sdk_version("v2") + +from aic import VadParameter + + + +class AICVADAnalyzer(VADAnalyzer): + """VAD analyzer that lazily binds to the AIC VadContext via a factory. + + The analyzer can be constructed before the AIC Processor exists. Once the filter has + started and the Processor is available, the provided factory will succeed and the + VadContext will be obtained. We then use the context's is_speech_detected() state + to derive confidence values. + + AIC VAD runtime parameters (v2): + - speech_hold_duration: + Controls for how long the VAD continues to detect speech after the audio signal + no longer contains speech (in seconds). + Range: 0.0 .. 20x model window length + Default (SDK): 0.05s (50ms) + - sensitivity: + Controls the energy threshold sensitivity. Higher values make the detector + less sensitive (require more energy to count as speech). + Range: 1.0 .. 15.0 + Formula: Energy threshold = 10 ** (-sensitivity) + Default (SDK): 6.0 + + .. note:: + This class requires aic-sdk >= 2.0.0. + """ + + def __init__( + self, + *, + vad_context_factory: Optional[Callable[[], Any]] = None, + speech_hold_duration: Optional[float] = None, + sensitivity: Optional[float] = None, + ): + """Create an AIC VAD analyzer. + + Args: + vad_context_factory: + Zero-arg callable that returns the AIC VadContext. + This may raise until the filter's Processor has been created; the analyzer + will retry on set_sample_rate/first use. + speech_hold_duration: + Optional override for AIC VAD speech hold duration (in seconds). + Range: 0.0 .. 20x model window length. + If None, the SDK default (0.05s) is used. + sensitivity: + Optional override for AIC VAD sensitivity (energy threshold). + Range: 1.0 .. 15.0. Energy threshold = 10 ** (-sensitivity). + If None, the SDK default (6.0) is used. + """ + # Use fixed VAD parameters for AIC: no user override + fixed_params = VADParams(confidence=0.5, start_secs=0.0, stop_secs=0.0, min_volume=0.0) + super().__init__(sample_rate=None, params=fixed_params) + self._vad_context_factory = vad_context_factory + self._vad_ctx: Optional[Any] = None + self._pending_speech_hold_duration: Optional[float] = speech_hold_duration + self._pending_sensitivity: Optional[float] = sensitivity + + def bind_vad_context_factory(self, vad_context_factory: Callable[[], Any]): + """Attach or replace the factory post-construction.""" + self._vad_context_factory = vad_context_factory + self._ensure_vad_context_initialized() + + def _apply_vad_params(self): + """Apply optional AIC VAD parameters if available.""" + if self._vad_ctx is None or VadParameter is None: + return + try: + if self._pending_speech_hold_duration is not None: + self._vad_ctx.set_parameter( + VadParameter.SpeechHoldDuration, float(self._pending_speech_hold_duration) + ) + if self._pending_sensitivity is not None: + self._vad_ctx.set_parameter( + VadParameter.Sensitivity, float(self._pending_sensitivity) + ) + except Exception as e: # noqa: BLE001 + logger.debug(f"AIC VAD parameter application deferred/failed: {e}") + + def _ensure_vad_context_initialized(self): + if self._vad_ctx is not None: + return + if not self._vad_context_factory: + return + try: + self._vad_ctx = self._vad_context_factory() + self._apply_vad_params() + # With VAD context ready, recompute internal frame sizing + super().set_params(self._params) + logger.debug("AIC VAD context (v2) initialized in analyzer.") + except Exception as e: # noqa: BLE001 + # Filter may not be started yet; try again later + logger.debug(f"Deferring AIC VAD context initialization: {e}") + + def set_sample_rate(self, sample_rate: int): + """Set the sample rate for audio processing. + + Args: + sample_rate: Audio sample rate in Hz. + """ + # Set rate and attempt VAD context initialization once we know SR + self._sample_rate = self._init_sample_rate or sample_rate + self._ensure_vad_context_initialized() + # Ensure params are initialized even if VAD context not ready yet + try: + super().set_params(self._params) + except Exception: + pass + + def num_frames_required(self) -> int: + """Get the number of audio frames required for analysis. + + Returns: + Number of frames needed for VAD processing. + """ + # Use 10 ms windows based on sample rate + return int(self.sample_rate * 0.01) if self.sample_rate > 0 else 160 + + def voice_confidence(self, buffer: bytes) -> float: + """Calculate voice activity confidence for the given audio buffer. + + Args: + buffer: Audio buffer to analyze. + + Returns: + Voice confidence score is 0.0 or 1.0. + """ + # Ensure VAD context exists (filter might have started since last call) + self._ensure_vad_context_initialized() + if self._vad_ctx is None: + return 0.0 + + # We do not need to analyze 'buffer' here since the processor's VAD is updated + # as part of the enhancement pipeline. Simply query the boolean and map it. + try: + is_speech = self._vad_ctx.is_speech_detected() + return 1.0 if is_speech else 0.0 + except Exception as e: # noqa: BLE001 + logger.error(f"AIC VAD inference error: {e}") + return 0.0 From f05809520bd4698941f0e1ba971c591c147d7d2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Wed, 14 Jan 2026 17:22:38 +0100 Subject: [PATCH 12/50] Remove outdated AIC Filter and VAD v2 files, migrate to consolidated implementations. Added the new ACIFilter to the same module. --- src/pipecat/audio/filters/aic_filter_v2.py | 296 --------------------- src/pipecat/audio/utils.py | 90 ++++--- src/pipecat/audio/vad/aic_vad_v2.py | 163 ------------ uv.lock | 75 +++--- 4 files changed, 89 insertions(+), 535 deletions(-) delete mode 100644 src/pipecat/audio/filters/aic_filter_v2.py delete mode 100644 src/pipecat/audio/vad/aic_vad_v2.py diff --git a/src/pipecat/audio/filters/aic_filter_v2.py b/src/pipecat/audio/filters/aic_filter_v2.py deleted file mode 100644 index be2912107..000000000 --- a/src/pipecat/audio/filters/aic_filter_v2.py +++ /dev/null @@ -1,296 +0,0 @@ -# -# Copyright (c) 2024-2026, Daily -# -# SPDX-License-Identifier: BSD 2-Clause License -# - -"""ai-coustics AIC SDK audio filter for Pipecat (aic-sdk >= 2.0.0). - -This module provides an audio filter implementation using ai-coustics' AIC SDK to -enhance audio streams in real time. It mirrors the structure of other filters like -the Koala filter and integrates with Pipecat's input transport pipeline. - -.. note:: - This module is compatible with aic-sdk versions >= 2.0.0. - For aic-sdk < 2.0.0, use :mod:`pipecat.audio.filters.aic_filter` instead. -""" - -import os -from typing import List, Optional - -import numpy as np -from loguru import logger - -from pipecat.audio.filters.base_audio_filter import BaseAudioFilter -from pipecat.audio.utils import check_aic_sdk_version -from pipecat.frames.frames import FilterControlFrame, FilterEnableFrame - -# Check aic-sdk is installed and version is compatible (>= 2.0.0) -check_aic_sdk_version("v2") - -# AIC SDK v2 (https://ai-coustics.github.io/aic-sdk-py/api/) -import aic -from aic import Model, ProcessorAsync, ProcessorConfig, ProcessorParameter, VadParameter - - -class AICFilter(BaseAudioFilter): - """Audio filter using ai-coustics' AIC SDK v2 for real-time enhancement. - - Buffers incoming audio to the model's preferred block size and processes - planar frames in-place using float32 samples in the linear -1..+1 range. - - .. note:: - This class requires aic-sdk >= 2.0.0. - """ - - def __init__( - self, - *, - license_key: str = "", - model_id: Optional[str] = None, - model_path: Optional[str] = None, - model_download_dir: Optional[str] = None, - enhancement_level: Optional[float] = 1.0, - voice_gain: Optional[float] = 1.0, - ) -> None: - """Initialize the AIC filter. - - Args: - license_key: ai-coustics license key for authentication. - model_id: Model identifier to download from CDN. Required if model_path - is not provided. See https://artifacts.ai-coustics.io/ for available models. - model_path: Optional path to a local .aicmodel file. If provided, - model_id is ignored and no download occurs. - model_download_dir: Directory for downloading models. Defaults to - a cache directory in user's home folder. - enhancement_level: Optional overall enhancement strength (0.0..1.0). - voice_gain: Optional linear gain applied to detected speech (0.1..4.0). - - Raises: - ValueError: If neither model_id nor model_path is provided. - """ - if model_id is None and model_path is None: - raise ValueError( - "Either 'model_id' or 'model_path' must be provided. " - "See https://artifacts.ai-coustics.io/ for available models." - ) - - self._license_key = license_key - self._model_id = model_id - self._model_path = model_path - self._model_download_dir = model_download_dir or os.path.expanduser( - "~/.cache/pipecat/aic-models" - ) - - self._enhancement_level = enhancement_level - self._voice_gain = voice_gain - - self._enabled = True - self._sample_rate = 0 - self._aic_ready = False - self._frames_per_block = 0 - self._audio_buffer = bytearray() - - # v2 API objects - self._model: Optional[Model] = None - self._processor: Optional[ProcessorAsync] = None - self._processor_ctx = None - self._vad_ctx = None - - def get_vad_context(self): - """Return the VAD context once the processor exists. - - Returns: - The VadContext instance bound to the underlying processor. - Raises RuntimeError if the processor has not been initialized. - """ - if self._vad_ctx is None: - raise RuntimeError("AIC processor not initialized yet. Call start(sample_rate) first.") - return self._vad_ctx - - def create_vad_analyzer( - self, - *, - speech_hold_duration: Optional[float] = None, - sensitivity: Optional[float] = None, - ): - """Return an analyzer that will lazily instantiate the AIC VAD when ready. - - AIC VAD parameters (v2): - - speech_hold_duration: - How long VAD continues detecting after speech ends (in seconds). - Range: 0.0 .. 20x model window length, Default (SDK): 0.05s - - sensitivity: - Energy threshold sensitivity. Energy threshold = 10 ** (-sensitivity). - Range: 1.0 .. 15.0, Default (SDK): 6.0 - - Args: - speech_hold_duration: Optional speech hold duration to configure on the VAD. - If None, SDK default (0.05s) is used. - sensitivity: Optional sensitivity (energy threshold) to configure on the VAD. - Range: 1.0 .. 15.0. If None, SDK default (6.0) is used. - - Returns: - A lazily-initialized AICVADAnalyzer that will bind to the VAD context - once the filter's processor has been created (after start(sample_rate)). - """ - from pipecat.audio.vad.aic_vad_v2 import AICVADAnalyzer - - return AICVADAnalyzer( - vad_context_factory=lambda: self.get_vad_context(), - speech_hold_duration=speech_hold_duration, - sensitivity=sensitivity, - ) - - async def start(self, sample_rate: int): - """Initialize the filter with the transport's sample rate. - - Args: - sample_rate: The sample rate of the input transport in Hz. - - Returns: - None - """ - self._sample_rate = sample_rate - - try: - # Load or download model - if self._model_path: - logger.debug(f"Loading AIC model from: {self._model_path}") - self._model = Model.from_file(self._model_path) - else: - logger.debug(f"Downloading AIC model: {self._model_id}") - os.makedirs(self._model_download_dir, exist_ok=True) - model_path = await Model.download_async(self._model_id, self._model_download_dir) - logger.debug(f"Model downloaded to: {model_path}") - self._model = Model.from_file(model_path) - - # Create async processor - self._processor = ProcessorAsync(self._model, self._license_key or "") - - # Get optimal frames for this sample rate - self._frames_per_block = self._model.get_optimal_num_frames(self._sample_rate) - - # Create configuration - config = ProcessorConfig( - sample_rate=self._sample_rate, - num_channels=1, - num_frames=self._frames_per_block, - allow_variable_frames=False, - ) - - # Initialize processor - await self._processor.initialize_async(config) - - # Get contexts for parameter control and VAD - self._processor_ctx = self._processor.get_processor_context() - self._vad_ctx = self._processor.get_vad_context() - - # Apply initial parameters - if self._enhancement_level is not None: - level = float(self._enhancement_level if self._enabled else 0.0) - self._processor_ctx.set_parameter(ProcessorParameter.EnhancementLevel, level) - if self._voice_gain is not None: - self._processor_ctx.set_parameter( - ProcessorParameter.VoiceGain, float(self._voice_gain) - ) - - self._aic_ready = True - - # Log processor information - logger.debug(f"ai-coustics filter (v2) started:") - logger.debug(f" Model ID: {self._model.get_id()}") - logger.debug(f" Sample rate: {self._sample_rate} Hz") - logger.debug(f" Frames per chunk: {self._frames_per_block}") - logger.debug(f" Enhancement strength: {int((self._enhancement_level or 1.0) * 100)}%") - logger.debug(f" Optimal sample rate: {self._model.get_optimal_sample_rate()} Hz") - logger.debug( - f" Output delay: {self._processor_ctx.get_output_delay()} samples " - f"({self._processor_ctx.get_output_delay() / self._sample_rate * 1000:.2f}ms)" - ) - except Exception as e: # noqa: BLE001 - surfacing SDK initialization errors - logger.error(f"AIC model initialization failed: {e}") - self._aic_ready = False - - async def stop(self): - """Clean up the AIC processor when stopping. - - Returns: - None - """ - try: - if self._processor_ctx is not None: - self._processor_ctx.reset() - finally: - self._processor = None - self._processor_ctx = None - self._vad_ctx = None - self._model = None - self._aic_ready = False - self._audio_buffer.clear() - - async def process_frame(self, frame: FilterControlFrame): - """Process control frames to enable/disable filtering. - - Args: - frame: The control frame containing filter commands. - - Returns: - None - """ - if isinstance(frame, FilterEnableFrame): - self._enabled = frame.enable - if self._processor_ctx is not None: - try: - level = float(self._enhancement_level if self._enabled else 0.0) - self._processor_ctx.set_parameter(ProcessorParameter.EnhancementLevel, level) - except Exception as e: # noqa: BLE001 - logger.error(f"AIC set_parameter failed: {e}") - - async def filter(self, audio: bytes) -> bytes: - """Apply AIC enhancement to audio data. - - Buffers incoming audio and processes it in chunks that match the AIC - model's required block length. Returns enhanced audio data. - - Args: - audio: Raw audio data as bytes to be filtered (int16 PCM, planar). - - Returns: - Enhanced audio data as bytes (int16 PCM, planar). - """ - if not self._aic_ready or self._processor is None: - return audio - - self._audio_buffer.extend(audio) - - filtered_chunks: List[bytes] = [] - - # Number of int16 samples currently buffered - available_frames = len(self._audio_buffer) // 2 - - while available_frames >= self._frames_per_block: - # Consume exactly one block worth of frames - samples_to_consume = self._frames_per_block * 1 - bytes_to_consume = samples_to_consume * 2 - block_bytes = bytes(self._audio_buffer[:bytes_to_consume]) - - # Convert to float32 in -1..+1 range and reshape to (channels, frames) - block_i16 = np.frombuffer(block_bytes, dtype=np.int16) - block_f32 = (block_i16.astype(np.float32) / 32768.0).reshape( - (1, self._frames_per_block) - ) - - # Process via async processor; returns ndarray (same shape) - out_f32 = await self._processor.process_async(block_f32) - - # Convert back to int16 bytes - out_i16 = np.clip(out_f32 * 32768.0, -32768, 32767).astype(np.int16) - filtered_chunks.append(out_i16.reshape(-1).tobytes()) - - # Slide buffer - self._audio_buffer = self._audio_buffer[bytes_to_consume:] - available_frames = len(self._audio_buffer) // 2 - - # Do not flush incomplete frames; keep them buffered for the next call - return b"".join(filtered_chunks) diff --git a/src/pipecat/audio/utils.py b/src/pipecat/audio/utils.py index 002375f35..c61aba4dd 100644 --- a/src/pipecat/audio/utils.py +++ b/src/pipecat/audio/utils.py @@ -316,11 +316,41 @@ def is_silence(pcm_bytes: bytes) -> bool: return max_value <= SPEAKING_THRESHOLD +def is_aic_sdk_v2() -> bool: + """Detect if aic-sdk v2 is installed by checking the module name. + + In v2, the module was renamed from 'aic' to 'aic_sdk'. + + Returns: + True if aic-sdk v2 (aic_sdk module) is installed, False if v1 (aic module). + + Raises: + ImportError: If neither aic nor aic_sdk module is installed. + """ + try: + import aic_sdk # noqa: F401 + + return True + except ModuleNotFoundError: + pass + + try: + import aic # noqa: F401 + + return False + except ModuleNotFoundError: + logger.error("In order to use the AIC filter, you need to `pip install pipecat-ai[aic]`.") + raise ImportError( + "aic-sdk is not installed. Install with 'pip install pipecat-ai[aic]'." + ) + + def check_aic_sdk_version(required_version: Literal["v1", "v2"]) -> None: """Check if the aic-sdk is installed and compatible with the module. This function checks both that the aic-sdk is installed and that its version - is compatible with the module requirements. + is compatible with the module requirements. Version detection is based on + the module name: v2 uses 'aic_sdk', v1 uses 'aic'. Args: required_version: Either "v1" (for aic-sdk < 2.0.0) or "v2" (for aic-sdk >= 2.0.0). @@ -328,43 +358,25 @@ def check_aic_sdk_version(required_version: Literal["v1", "v2"]) -> None: Raises: ImportError: If aic-sdk is not installed or version is incompatible. """ - try: - import aic # noqa: F401 - check if module is installed - except ModuleNotFoundError as e: - logger.error(f"Exception: {e}") - logger.error("In order to use the AIC filter, you need to `pip install pipecat-ai[aic]`.") - raise ImportError(f"Missing module: {e}") from e + is_v2 = is_aic_sdk_v2() - try: - from importlib.metadata import version as get_version + if required_version == "v1" and is_v2: + error_msg = ( + "aic-sdk v2 (aic_sdk module) detected, but v1 (aic module) is required. " + "Please use the v2 classes instead: " + "'from pipecat.audio.filters.aic_filter import AICFilterV2' or " + "'from pipecat.audio.vad.aic_vad import AICVADAnalyzerV2'." + ) + logger.error(error_msg) + raise ImportError(error_msg) - aic_version = get_version("aic-sdk") - major_version = int(aic_version.split(".")[0]) - - if required_version == "v1" and major_version >= 2: - error_msg = ( - f"aic-sdk version {aic_version} detected, but aic-sdk < 2.0.0 is required. " - "Please use the v2 modules instead: " - "'from pipecat.audio.filters.aic_filter_v2 import AICFilter' or " - "'from pipecat.audio.vad.aic_vad_v2 import AICVADAnalyzer'." - ) - logger.error(error_msg) - raise ImportError(error_msg) - - if required_version == "v2" and major_version < 2: - error_msg = ( - f"aic-sdk version {aic_version} detected, but aic-sdk >= 2.0.0 is required. " - "Please update with 'pip install --upgrade aic-sdk>=2.0.0' " - "or use the v1 modules: " - "'from pipecat.audio.filters.aic_filter import AICFilter' or " - "'from pipecat.audio.vad.aic_vad import AICVADAnalyzer'." - ) - logger.error(error_msg) - raise ImportError(error_msg) - - except ImportError: - # Re-raise if it's our version mismatch error - raise - except Exception: - # If we can't determine version for other reasons, log warning and allow to proceed - logger.warning("Could not determine aic-sdk version. Proceeding anyway.") + if required_version == "v2" and not is_v2: + error_msg = ( + "aic-sdk v1 (aic module) detected, but v2 (aic_sdk module) is required. " + "Please update with 'pip install --upgrade aic-sdk>=2.0.0' " + "or use the v1 classes: " + "'from pipecat.audio.filters.aic_filter import AICFilter' or " + "'from pipecat.audio.vad.aic_vad import AICVADAnalyzer'." + ) + logger.error(error_msg) + raise ImportError(error_msg) diff --git a/src/pipecat/audio/vad/aic_vad_v2.py b/src/pipecat/audio/vad/aic_vad_v2.py deleted file mode 100644 index 2f8129a0d..000000000 --- a/src/pipecat/audio/vad/aic_vad_v2.py +++ /dev/null @@ -1,163 +0,0 @@ -"""AIC-integrated VAD analyzer that lazily binds to the AIC SDK v2 backend. - -This analyzer queries the backend's is_speech_detected() and maps it to a float -confidence (1.0/0.0). It uses 10 ms windows based on the sample rate and applies -optional AIC VAD parameters (speech_hold_duration, sensitivity) when available. - -.. note:: - This module is compatible with aic-sdk versions >= 2.0.0. - For aic-sdk < 2.0.0, use :mod:`pipecat.audio.vad.aic_vad` instead. -""" - -from typing import Any, Callable, Optional - -from loguru import logger - -from pipecat.audio.utils import check_aic_sdk_version -from pipecat.audio.vad.vad_analyzer import VADAnalyzer, VADParams - -# Check aic-sdk is installed and version is compatible (>= 2.0.0) -check_aic_sdk_version("v2") - -from aic import VadParameter - - - -class AICVADAnalyzer(VADAnalyzer): - """VAD analyzer that lazily binds to the AIC VadContext via a factory. - - The analyzer can be constructed before the AIC Processor exists. Once the filter has - started and the Processor is available, the provided factory will succeed and the - VadContext will be obtained. We then use the context's is_speech_detected() state - to derive confidence values. - - AIC VAD runtime parameters (v2): - - speech_hold_duration: - Controls for how long the VAD continues to detect speech after the audio signal - no longer contains speech (in seconds). - Range: 0.0 .. 20x model window length - Default (SDK): 0.05s (50ms) - - sensitivity: - Controls the energy threshold sensitivity. Higher values make the detector - less sensitive (require more energy to count as speech). - Range: 1.0 .. 15.0 - Formula: Energy threshold = 10 ** (-sensitivity) - Default (SDK): 6.0 - - .. note:: - This class requires aic-sdk >= 2.0.0. - """ - - def __init__( - self, - *, - vad_context_factory: Optional[Callable[[], Any]] = None, - speech_hold_duration: Optional[float] = None, - sensitivity: Optional[float] = None, - ): - """Create an AIC VAD analyzer. - - Args: - vad_context_factory: - Zero-arg callable that returns the AIC VadContext. - This may raise until the filter's Processor has been created; the analyzer - will retry on set_sample_rate/first use. - speech_hold_duration: - Optional override for AIC VAD speech hold duration (in seconds). - Range: 0.0 .. 20x model window length. - If None, the SDK default (0.05s) is used. - sensitivity: - Optional override for AIC VAD sensitivity (energy threshold). - Range: 1.0 .. 15.0. Energy threshold = 10 ** (-sensitivity). - If None, the SDK default (6.0) is used. - """ - # Use fixed VAD parameters for AIC: no user override - fixed_params = VADParams(confidence=0.5, start_secs=0.0, stop_secs=0.0, min_volume=0.0) - super().__init__(sample_rate=None, params=fixed_params) - self._vad_context_factory = vad_context_factory - self._vad_ctx: Optional[Any] = None - self._pending_speech_hold_duration: Optional[float] = speech_hold_duration - self._pending_sensitivity: Optional[float] = sensitivity - - def bind_vad_context_factory(self, vad_context_factory: Callable[[], Any]): - """Attach or replace the factory post-construction.""" - self._vad_context_factory = vad_context_factory - self._ensure_vad_context_initialized() - - def _apply_vad_params(self): - """Apply optional AIC VAD parameters if available.""" - if self._vad_ctx is None or VadParameter is None: - return - try: - if self._pending_speech_hold_duration is not None: - self._vad_ctx.set_parameter( - VadParameter.SpeechHoldDuration, float(self._pending_speech_hold_duration) - ) - if self._pending_sensitivity is not None: - self._vad_ctx.set_parameter( - VadParameter.Sensitivity, float(self._pending_sensitivity) - ) - except Exception as e: # noqa: BLE001 - logger.debug(f"AIC VAD parameter application deferred/failed: {e}") - - def _ensure_vad_context_initialized(self): - if self._vad_ctx is not None: - return - if not self._vad_context_factory: - return - try: - self._vad_ctx = self._vad_context_factory() - self._apply_vad_params() - # With VAD context ready, recompute internal frame sizing - super().set_params(self._params) - logger.debug("AIC VAD context (v2) initialized in analyzer.") - except Exception as e: # noqa: BLE001 - # Filter may not be started yet; try again later - logger.debug(f"Deferring AIC VAD context initialization: {e}") - - def set_sample_rate(self, sample_rate: int): - """Set the sample rate for audio processing. - - Args: - sample_rate: Audio sample rate in Hz. - """ - # Set rate and attempt VAD context initialization once we know SR - self._sample_rate = self._init_sample_rate or sample_rate - self._ensure_vad_context_initialized() - # Ensure params are initialized even if VAD context not ready yet - try: - super().set_params(self._params) - except Exception: - pass - - def num_frames_required(self) -> int: - """Get the number of audio frames required for analysis. - - Returns: - Number of frames needed for VAD processing. - """ - # Use 10 ms windows based on sample rate - return int(self.sample_rate * 0.01) if self.sample_rate > 0 else 160 - - def voice_confidence(self, buffer: bytes) -> float: - """Calculate voice activity confidence for the given audio buffer. - - Args: - buffer: Audio buffer to analyze. - - Returns: - Voice confidence score is 0.0 or 1.0. - """ - # Ensure VAD context exists (filter might have started since last call) - self._ensure_vad_context_initialized() - if self._vad_ctx is None: - return 0.0 - - # We do not need to analyze 'buffer' here since the processor's VAD is updated - # as part of the enhancement pipeline. Simply query the boolean and map it. - try: - is_speech = self._vad_ctx.is_speech_detected() - return 1.0 if is_speech else 0.0 - except Exception as e: # noqa: BLE001 - logger.error(f"AIC VAD inference error: {e}") - return 0.0 diff --git a/uv.lock b/uv.lock index 45c1f892f..4782f1c79 100644 --- a/uv.lock +++ b/uv.lock @@ -38,12 +38,12 @@ wheels = [ [[package]] name = "aic-sdk" -version = "1.2.0" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f9/ba/3ebe31b91e03d42437ec864e9d2af3a52b7ccc73a1a0c1026275956270b0/aic_sdk-1.2.0.tar.gz", hash = "sha256:eeda9a181c679f175dbe6f0efc0c67ec98ff3d84cfe01541fef7fa12ecd505ca", size = 35606, upload-time = "2025-11-20T14:42:14.333Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/e7/56b6074224dc26a1350b165fd0ef3c8ca8c115cc1d4aa3e4f38af9c5d3f1/aic_sdk-1.3.0.tar.gz", hash = "sha256:ccccf7c0c35fd0342a0cbcd1ed81bd3fd7f59df51fbb7c1a80fb438a94ee6ae9", size = 37700, upload-time = "2025-12-12T13:00:09.11Z" } [[package]] name = "aioboto3" @@ -3292,47 +3292,47 @@ wheels = [ [[package]] name = "mlx" -version = "0.30.1" +version = "0.30.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mlx-metal", marker = "sys_platform == 'darwin'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/8d/16a34feb957ac33525b9b787b5132053a44bc94d1bf40c18639f6e05cd2a/mlx-0.30.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:391c650f0578ce359c8cffddb204b42798b622f9ee2b57b865d87716c00db536", size = 592926, upload-time = "2025-12-18T01:55:28.757Z" }, - { url = "https://files.pythonhosted.org/packages/34/e6/0661455f5f4bd9de257874b28a96a33699d36a1e17ccde821341c0ac1c0e/mlx-0.30.1-cp310-cp310-macosx_15_0_arm64.whl", hash = "sha256:42fefcad72d7488c65649e152a1b28f00c2033d38121afa45ce65ae16ec6b988", size = 592926, upload-time = "2025-12-18T01:55:30.141Z" }, - { url = "https://files.pythonhosted.org/packages/d8/37/a322af7dba9101064b5e858d1208e0e66cd83be7d060d14fa03ace37d52e/mlx-0.30.1-cp310-cp310-macosx_26_0_arm64.whl", hash = "sha256:a9db94e7e080672cc0dda9a5f121aebe0d49f7a8cb46706ecfd8b8ce7d99d541", size = 566952, upload-time = "2025-12-18T00:15:50.075Z" }, - { url = "https://files.pythonhosted.org/packages/c9/46/f0005d07fe5687bbf4efc15b468d27f2923f486b07a625d35c7d3cbb4962/mlx-0.30.1-cp310-cp310-manylinux_2_35_aarch64.whl", hash = "sha256:44b2142896c8dd8ab057dd785faf92fa83f3697b4b6bb01ff7515df12b6de666", size = 658049, upload-time = "2025-12-18T01:55:31.748Z" }, - { url = "https://files.pythonhosted.org/packages/cb/95/cc47c4607cc78f55ce3081ade9161961795c15c049bf219f27a393f85767/mlx-0.30.1-cp310-cp310-manylinux_2_35_x86_64.whl", hash = "sha256:37ea97b3c4bd71b19d87c6ef2c9e681e11f37908d8381fc2b785d2509b0681df", size = 692336, upload-time = "2025-12-18T01:55:33.224Z" }, - { url = "https://files.pythonhosted.org/packages/07/14/74acbd677ececd17a44dafda1b472aebacef54f60ff9a41a801f711de9a7/mlx-0.30.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:acfd7d1b8e5b9fa1b7e9fab4cc5ba6a492c559fbb1c5aeab16c1d7a148ab4f1b", size = 593048, upload-time = "2025-12-18T01:55:34.9Z" }, - { url = "https://files.pythonhosted.org/packages/58/8c/5309848afb9c53d363f59b88ae5811de248e2817e91aeadf007e2ac8d22b/mlx-0.30.1-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:b62030471272d1835b8137164bd43d863cc93ff1d67ec4f1f87bb4c8613dd5a6", size = 593043, upload-time = "2025-12-18T01:55:36.839Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5a/0039815a930f0193e2cffb27c57dc6971004bce0086c2bbbdb10395c272c/mlx-0.30.1-cp311-cp311-macosx_26_0_arm64.whl", hash = "sha256:0489cd340f2d262cb3aaad4368e40e84b152e182e4cea37ba018e56c72e1d020", size = 567014, upload-time = "2025-12-18T00:15:51.731Z" }, - { url = "https://files.pythonhosted.org/packages/de/c7/6bdb5497c1f5ed3e33afa7785761ad87fd3436c071805d9a93c905943f04/mlx-0.30.1-cp311-cp311-manylinux_2_35_aarch64.whl", hash = "sha256:fbdcfc3ed556a7e701a8eb67da299e2a25f52615193833ca6374decca3be5bf4", size = 658930, upload-time = "2025-12-18T01:55:38.441Z" }, - { url = "https://files.pythonhosted.org/packages/91/02/2d86a1c116e951eb4d88fe313c321e23628ce7404712e1258cacf925a8b8/mlx-0.30.1-cp311-cp311-manylinux_2_35_x86_64.whl", hash = "sha256:68ec854e7b5f89454e67d6c2fa7bb416b8afb148003ccd775904ec6ec4744818", size = 692484, upload-time = "2025-12-18T01:55:40.254Z" }, - { url = "https://files.pythonhosted.org/packages/3a/4b/ad57b2f0ede3f0d009c0e3e1270c219bd18f9025388855ee149680cffa20/mlx-0.30.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:deaef3ecd2f99930867a29de748e3bffa9cc7e4dfa834f2501c37ed29aece1cc", size = 593397, upload-time = "2025-12-18T01:55:41.814Z" }, - { url = "https://files.pythonhosted.org/packages/ef/14/7fa03a0f66ac3cfb2fd6752178a1488f13c7233fff26eed0f832d961db35/mlx-0.30.1-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:86ccdcda0b5ea4768b87da25beae5b83ac7cc802506116b6845cea6f450e2377", size = 593397, upload-time = "2025-12-18T01:55:43Z" }, - { url = "https://files.pythonhosted.org/packages/9c/c8/9f1343dbe2381f9653df4e0a62dc8bf38f575a2553dc2aa6916de32d2a85/mlx-0.30.1-cp312-cp312-macosx_26_0_arm64.whl", hash = "sha256:a625cb434b2acc5674fe10683374641dab9671fb354ae7c2c67a1fb0405eeb37", size = 567576, upload-time = "2025-12-18T00:15:55.114Z" }, - { url = "https://files.pythonhosted.org/packages/15/ff/485ed9c99c18ef89ac987178c0a526cb4148ba38b14838d315311d9d76a8/mlx-0.30.1-cp312-cp312-manylinux_2_35_aarch64.whl", hash = "sha256:ccc1ff3aca8fb1073c7dcd1274cebe48ae75f852d14b16c7db8228fbbad594dd", size = 643654, upload-time = "2025-12-18T01:55:44.165Z" }, - { url = "https://files.pythonhosted.org/packages/8a/d3/54d3bf5e404c3b6424b49c505dc8b3c06c6bb498fe720195b1fafbd69b5e/mlx-0.30.1-cp312-cp312-manylinux_2_35_x86_64.whl", hash = "sha256:55ed7fc4b389d6e49dac6d34a97b41e61cbe3662ac29c3d29cf612e6b2ed9827", size = 687305, upload-time = "2025-12-18T01:55:45.526Z" }, - { url = "https://files.pythonhosted.org/packages/f9/fd/c6f56cd87d48763ed63655ace627c06db9819eae7d43d132f40d4965947a/mlx-0.30.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:743520758bc8261b2ed8f3b3dc96e4e9236769dd8f61fb17877c5e44037e2058", size = 593366, upload-time = "2025-12-18T01:55:46.786Z" }, - { url = "https://files.pythonhosted.org/packages/dc/53/96d8c48b21f91c4216b6d2ef6dfc10862e5fb0b811a2aaf02c96c78601de/mlx-0.30.1-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:fc9745bc1860ca60128e3a6d36157da06d936e2b4007a4dcba990b40202f598f", size = 593368, upload-time = "2025-12-18T01:55:48.363Z" }, - { url = "https://files.pythonhosted.org/packages/70/ce/476c3b7d3a4153bd0e1c5af1f1b6c09a804b652bbed34072404b322c22e0/mlx-0.30.1-cp313-cp313-macosx_26_0_arm64.whl", hash = "sha256:a1480399c67bb327a66c5527b73915132e3fcaae3bce9634e5c81ccad9f43229", size = 567561, upload-time = "2025-12-18T00:15:56.153Z" }, - { url = "https://files.pythonhosted.org/packages/33/41/7ad1e639fd7dd1cf01a62c1c5b051024a859888c27504996e9d8380e6754/mlx-0.30.1-cp313-cp313-manylinux_2_35_aarch64.whl", hash = "sha256:8e19850a4236a8e174f851f5789b8b62a8eb74f5a8fa49ad8ba286c5ddb5f9bf", size = 643122, upload-time = "2025-12-18T01:55:49.607Z" }, - { url = "https://files.pythonhosted.org/packages/d0/dc/72d3737c5b0662eb5e785d353dbc5e34d793d27b09b99e39993ee051bd19/mlx-0.30.1-cp313-cp313-manylinux_2_35_x86_64.whl", hash = "sha256:1c8ed5bcd9f1910fca209e95859ac737e60b3e1954181b820fa269158f81049a", size = 687254, upload-time = "2025-12-18T01:55:51.239Z" }, - { url = "https://files.pythonhosted.org/packages/9b/cc/523448996247bb05d9d68e23bccf3dafdda660befb9330f6bd5fa13361e8/mlx-0.30.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d34cc2c25b0ee41c1349f14650db760e282685339858e305453f62405c12bc1b", size = 596006, upload-time = "2025-12-18T01:55:52.463Z" }, - { url = "https://files.pythonhosted.org/packages/23/0e/f9f2f9659c34c87be8f4167f6a1d6ed7e826f4889d20eecd4c0d8122f0e9/mlx-0.30.1-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:4e47d301e9095b87f0bda8827bfd6ffe744223aba5cee8f28e25894d647f5823", size = 596008, upload-time = "2025-12-18T01:55:54.02Z" }, - { url = "https://files.pythonhosted.org/packages/56/a7/49e41fb141de95b6a376091a963c737839c9cda04e423c67f57460a50458/mlx-0.30.1-cp314-cp314-macosx_26_0_arm64.whl", hash = "sha256:cfba13e2a52255d663a1ad62f0f83eb3991e42147edf9a8d38cdd224e48ca49b", size = 570406, upload-time = "2025-12-18T00:15:57.177Z" }, - { url = "https://files.pythonhosted.org/packages/73/99/a43cb112167cf865c069f5e108ae42f5314663930ff3dd86c2d23d984191/mlx-0.30.1-cp314-cp314-manylinux_2_35_aarch64.whl", hash = "sha256:bebfec377208eb29cc88aa86c897c7446aa0984838669e138f273f9225d627ff", size = 646461, upload-time = "2025-12-18T01:55:55.285Z" }, - { url = "https://files.pythonhosted.org/packages/d4/ff/1e1968f107b4221a98dc26832586b1f646b27ddf3e55c95051c09d751f0a/mlx-0.30.1-cp314-cp314-manylinux_2_35_x86_64.whl", hash = "sha256:d18012d5cf0f013bc4a405cfd1e9d2d28e798f4d2dc4f15aa0fbffff73c02ba2", size = 687114, upload-time = "2025-12-18T01:55:56.506Z" }, + { url = "https://files.pythonhosted.org/packages/9c/9c/d6f72f04eeeeaeee8309397efcfa0e923189d0b720f4ac6b3887d0a2f40b/mlx-0.30.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:685051761e428336f8f19ae76a761ce99d29ff67c52738f15ce6409e2ff34e6b", size = 568453, upload-time = "2026-01-14T01:16:40.796Z" }, + { url = "https://files.pythonhosted.org/packages/db/59/505717fd63f62d766f054ab8770d08e98b10217c0995bd2555429863fd31/mlx-0.30.3-cp310-cp310-macosx_15_0_arm64.whl", hash = "sha256:e405e6575e3b0b00dd6bd02bdb415b638cd5c2e5faedb696df2b2c8fbe871240", size = 568451, upload-time = "2026-01-14T01:16:42.027Z" }, + { url = "https://files.pythonhosted.org/packages/86/9c/a5319ae8ed0baa76fde80def12391ae13acec1b88904d4ead9bbabc9a083/mlx-0.30.3-cp310-cp310-macosx_26_0_arm64.whl", hash = "sha256:46894eb528457483aec44227f61afdff424cb76d146a6e1727d03ea0f52be41b", size = 568309, upload-time = "2026-01-14T05:52:06.915Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7c/ac360b04c6b09acf11fcb54068909ca030325b248557930f6991d5601436/mlx-0.30.3-cp310-cp310-manylinux_2_35_aarch64.whl", hash = "sha256:a18e1b380130a77feda83b8bb8a7ff5a24f78e7263af484c6627f23e4210bdaf", size = 631435, upload-time = "2026-01-14T01:16:43.085Z" }, + { url = "https://files.pythonhosted.org/packages/01/b5/3becb2c93955d43539d9c916b33899a57c50099c29310dc2b5c68ff7a88d/mlx-0.30.3-cp310-cp310-manylinux_2_35_x86_64.whl", hash = "sha256:c27f8e78b89cf97411d740a2ca46accf6c6e3fcc43d1e906389abff1f0e00376", size = 664899, upload-time = "2026-01-14T01:16:44.623Z" }, + { url = "https://files.pythonhosted.org/packages/78/b6/dfcfffc41d832a86249715fab336dc8638c2237035287eb24af792484c53/mlx-0.30.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:794e79587a4906bdb3c5473ef936f45008eaaa609a3c498cc29a442b2c829621", size = 568664, upload-time = "2026-01-14T01:16:45.573Z" }, + { url = "https://files.pythonhosted.org/packages/22/9f/22d494b83b611380063da31c2b482db8c620f7ad6531cfcd1e11f7c35852/mlx-0.30.3-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:472cdc6eaca8610224621a1561e8c36477eab1a2f0dd3eb49b95484d739c4605", size = 568663, upload-time = "2026-01-14T01:16:46.588Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/b6fb0500aef8e9ed65d4730d8c34b13d7a770ca863b9af363b5713a16040/mlx-0.30.3-cp311-cp311-macosx_26_0_arm64.whl", hash = "sha256:a5d82be69c7e671dc4d5855d2f6aedcb507817e5985478903ab754b642d9ba01", size = 568522, upload-time = "2026-01-14T05:52:08.334Z" }, + { url = "https://files.pythonhosted.org/packages/6e/23/ea140c35419ec133e1037d34d94854474cdd72c89eedc3a90b8ec65fb0ff/mlx-0.30.3-cp311-cp311-manylinux_2_35_aarch64.whl", hash = "sha256:009a9a1d2e234b9b269f455729202feaf22eb1faf2c7b85818f2473f6c2f9cbe", size = 632235, upload-time = "2026-01-14T01:16:47.764Z" }, + { url = "https://files.pythonhosted.org/packages/af/18/335d2d455b1e15036e315c6b64de8e6b4b04ec60576e1b99a651a7487014/mlx-0.30.3-cp311-cp311-manylinux_2_35_x86_64.whl", hash = "sha256:ba7141b6c251207d26a5611a0038d121cd13e367a59589d8c827e6af06b1f406", size = 664821, upload-time = "2026-01-14T01:16:48.8Z" }, + { url = "https://files.pythonhosted.org/packages/11/b3/e24c3a69dad0cf4404bb174c6fed0d804022da64758cd815a254e1cd0627/mlx-0.30.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:0b275168b80645a155b456e1a457a37fb5ee2c251e8fbd8db9e153351a9e2d2f", size = 569398, upload-time = "2026-01-14T01:16:49.804Z" }, + { url = "https://files.pythonhosted.org/packages/0b/87/d0804443da97a06d3439f6efb0ceffa178f530a121f0f4a6c77b39f8bfd7/mlx-0.30.3-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:6e818de14864982e832344198240a1dafba7d3316c4eb6f1b8e43b4dd25dd2ef", size = 569396, upload-time = "2026-01-14T01:16:51.007Z" }, + { url = "https://files.pythonhosted.org/packages/cf/dc/7cdd95e4561b73fba8c86bf11293797076120400e472fe2a72ef483b6d8d/mlx-0.30.3-cp312-cp312-macosx_26_0_arm64.whl", hash = "sha256:d23b422209fd4b7ecacef59070321f8c6a122f906a5e9b6683a5fc9e1b8fcd5c", size = 569192, upload-time = "2026-01-14T05:52:09.715Z" }, + { url = "https://files.pythonhosted.org/packages/58/3b/6892f48dce949da7e1706cad45a1693857ef3adf23f849bf851c37e605eb/mlx-0.30.3-cp312-cp312-manylinux_2_35_aarch64.whl", hash = "sha256:f487461ffd5c2411c012dd8cd0d347dd807f05f223b1dec1c13bad0815cdcefd", size = 617390, upload-time = "2026-01-14T01:16:52.676Z" }, + { url = "https://files.pythonhosted.org/packages/66/ce/606e2111bc7c2ed1a2f2582caeb3e73b90e00d773d573fe9cd5dd36a0321/mlx-0.30.3-cp312-cp312-manylinux_2_35_x86_64.whl", hash = "sha256:b78627f324790fd0e06c4fa6e79b88094b342c5c425f8909de7c3f2fa5d01302", size = 659552, upload-time = "2026-01-14T01:16:53.888Z" }, + { url = "https://files.pythonhosted.org/packages/d0/22/42935d593fe82d3b98eb9d60e4620ed99703886635106f89d407c68f33bc/mlx-0.30.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:743fac1e4f9e8e46c8262943c643a31139c255cdb256c99ad496958215ccac1e", size = 569344, upload-time = "2026-01-14T01:16:54.847Z" }, + { url = "https://files.pythonhosted.org/packages/7d/27/f2e7a5236289d45315d0215e8553b4dd7e2faaba3bcb5025b34b25d5ab66/mlx-0.30.3-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:3b04ae81655aa0e63a6e8f2c749de3bbce64cf5b168ae10f39ed086dfa99e7f8", size = 569345, upload-time = "2026-01-14T01:16:56.564Z" }, + { url = "https://files.pythonhosted.org/packages/01/41/06b042457f51952456e9bb46b2c6e205ab3a28fc52d6751b5787fdb762b2/mlx-0.30.3-cp313-cp313-macosx_26_0_arm64.whl", hash = "sha256:ba9b5bdb1e929cc130af72efd7f73508c0f4e526d224489af7ec1c6419564659", size = 569213, upload-time = "2026-01-14T05:52:10.86Z" }, + { url = "https://files.pythonhosted.org/packages/ec/1e/f62c98fc0d2d878ee4235671f9d406b13cc9240493ba6fcfde2f72c2ff83/mlx-0.30.3-cp313-cp313-manylinux_2_35_aarch64.whl", hash = "sha256:dfe5c5b64e55398a22100804abbf9681996b03129e720e36b1727ed704db12b5", size = 617309, upload-time = "2026-01-14T01:16:57.58Z" }, + { url = "https://files.pythonhosted.org/packages/e9/62/811f064693449de740350d27793ce39343a460305ec8d878c318b80921d0/mlx-0.30.3-cp313-cp313-manylinux_2_35_x86_64.whl", hash = "sha256:a3364924610929936e6aaf13c71106161258e5a5d3f7813a64c07cc2435f9f55", size = 659521, upload-time = "2026-01-14T01:16:58.719Z" }, + { url = "https://files.pythonhosted.org/packages/82/e2/6e551bd48fb350fbf0ee4cc5cd09485437d260b8f4937f22d8623e14687a/mlx-0.30.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2c27fd8daaae14ca6cf407fcd236006a6e968f7708c8f61a2709116f2e754852", size = 571920, upload-time = "2026-01-14T01:16:59.683Z" }, + { url = "https://files.pythonhosted.org/packages/82/c0/561d1c9d3d12830b0e7fdcbd807585ef20909e398d4bcdbf25e4367543eb/mlx-0.30.3-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:b755fd4ed4b6a2ae4dee3766b5a2ea52fcbe83ebd1cf018458e18b74139409f3", size = 571921, upload-time = "2026-01-14T01:17:00.868Z" }, + { url = "https://files.pythonhosted.org/packages/42/1a/fb573fc2edc22a777fa254ff5c0c886ffd2c88aeb1f21c45778ef170f990/mlx-0.30.3-cp314-cp314-macosx_26_0_arm64.whl", hash = "sha256:7e352c0369a2f7e54d4f317b434eab3333918ea9edde1c43c61d36386b6f76bf", size = 571732, upload-time = "2026-01-14T05:52:11.893Z" }, + { url = "https://files.pythonhosted.org/packages/9e/db/d0083e8f2205b3b2dcd9670eb6f0d6c1b7cbfea6b01a1f8bff39142edf44/mlx-0.30.3-cp314-cp314-manylinux_2_35_aarch64.whl", hash = "sha256:00ac867f3d003c1477a66a579442c2040ba7ea43ce3c174490d1f8bf379606bd", size = 619635, upload-time = "2026-01-14T01:17:01.812Z" }, + { url = "https://files.pythonhosted.org/packages/ab/90/ab0b93ff0e76da4fe0e878722c76a308cfb950b044a4676e9617276d8ccd/mlx-0.30.3-cp314-cp314-manylinux_2_35_x86_64.whl", hash = "sha256:5be7d0329036f09c6ed003ea3e307e97e3144f20a3e4711b01810d7d5013cf2c", size = 659652, upload-time = "2026-01-14T01:17:02.915Z" }, ] [[package]] name = "mlx-metal" -version = "0.30.1" +version = "0.30.3" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/09/3f/0be35ddad7e13d8ecd33a9185895f9739bb00b96ef0cce36cf0405d4aec0/mlx_metal-0.30.1-py3-none-macosx_14_0_arm64.whl", hash = "sha256:e7e92c6bdbd7ac8083f528a4c6640552ae106a57bb3d99856ac10a32e93a4b5e", size = 36864966, upload-time = "2025-12-18T01:55:31.473Z" }, - { url = "https://files.pythonhosted.org/packages/1e/1f/c0bddd0d5bf3871411aabe32121e09e1b7cdbece8917a49d5a442310e3e5/mlx_metal-0.30.1-py3-none-macosx_15_0_arm64.whl", hash = "sha256:bb50f57418af7fc3c42a2da2c4bde0e7ab7ac0b997de1f6f642a6680ac65d626", size = 36859011, upload-time = "2025-12-18T01:55:34.541Z" }, - { url = "https://files.pythonhosted.org/packages/67/b3/73cc2f584ac612a476096d35a61eed75ee7ed8b4e320b0c36cf60a14d4eb/mlx_metal-0.30.1-py3-none-macosx_26_0_arm64.whl", hash = "sha256:e0b151a0053ac00b4226710bfb6dbf54b87283fb01e10fb3877f9ea969f680aa", size = 44981160, upload-time = "2025-12-18T00:15:47.518Z" }, + { url = "https://files.pythonhosted.org/packages/f6/63/4d8f6fefb507c028df4454dabfe8d8e0ad2961bb06510b6aca23d2d5b2be/mlx_metal-0.30.3-py3-none-macosx_14_0_arm64.whl", hash = "sha256:6276312b02353714c7c6515169569fe1c4bebe3229c8ecf1fdb375a13e78c966", size = 37716245, upload-time = "2026-01-14T01:16:34.838Z" }, + { url = "https://files.pythonhosted.org/packages/35/91/1d452e48a4bb4958844fd3bb28ae31b8de110549c009ebec5024ce27ebf3/mlx_metal-0.30.3-py3-none-macosx_15_0_arm64.whl", hash = "sha256:c096c0a3428f3f96a06220f97a36f9528b18bc05173f821eb05bc8458e723fa8", size = 37712125, upload-time = "2026-01-14T01:16:38.619Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/7a3cbca85542b5ca4faf871e35927f43aa0e3fc830ae5b699780fe723677/mlx_metal-0.30.3-py3-none-macosx_26_0_arm64.whl", hash = "sha256:69068533bd1ee8b0379ce5de57ed5fd313577a10ecab58e1332fd1ff7248a75e", size = 46488962, upload-time = "2026-01-14T05:52:04.523Z" }, ] [[package]] @@ -4495,7 +4495,7 @@ docs = [ [package.metadata] requires-dist = [ { name = "accelerate", marker = "extra == 'moondream'", specifier = "~=1.10.0" }, - { name = "aic-sdk", marker = "extra == 'aic'", specifier = "~=1.2.0" }, + { name = "aic-sdk", marker = "extra == 'aic'", specifier = ">=1.2.0" }, { name = "aioboto3", marker = "extra == 'aws'", specifier = "~=15.5.0" }, { name = "aiofiles", specifier = ">=24.1.0,<25" }, { name = "aiohttp", specifier = ">=3.11.12,<4" }, @@ -5280,7 +5280,7 @@ wheels = [ [[package]] name = "pyrnnoise" -version = "0.4.1" +version = "0.4.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "audiolab" }, @@ -5290,9 +5290,10 @@ dependencies = [ { name = "tqdm" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/59/49/7017ffa14230096e0271bd49dfd9ab60a32bfebe7e71399c2a0e38c6f859/pyrnnoise-0.4.1-py3-none-macosx_15_0_universal2.whl", hash = "sha256:c1fe407729190d0f84f3e3c9d9322ebbd33b27f3f5d9f7217379b71a4dd043e7", size = 13381833, upload-time = "2025-11-25T15:54:06.532Z" }, - { url = "https://files.pythonhosted.org/packages/8e/24/fb8b7bafb3dd9cbb46e134fa25c9597683c61b42c0133453fefeebeb0066/pyrnnoise-0.4.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:ddd39b45221b65fb235f882a0ce127513a1012d41c5b3ba9dc4e9e991b22c205", size = 13273307, upload-time = "2025-11-25T15:54:04.076Z" }, - { url = "https://files.pythonhosted.org/packages/7f/8e/eef9b2022fa5b9a111ba31d2f25ccd6e45da3daf16d20352e1fb18fd81dd/pyrnnoise-0.4.1-py3-none-win_amd64.whl", hash = "sha256:440e32359256eb7947e29fb080e800e984ba521fbe89a8b0b2f5dc196965e441", size = 13267076, upload-time = "2025-11-25T15:54:37.547Z" }, + { url = "https://files.pythonhosted.org/packages/1f/90/51bb94bcfd8aab186fd08902e0706a6eda5813485fb57eff011ce6ae4c51/pyrnnoise-0.4.3-py3-none-macosx_15_0_universal2.whl", hash = "sha256:bdd8e933d32457362e6f4e56831afa8155208825040ab075c4223baed755fa4f", size = 13381834, upload-time = "2026-01-14T08:44:28.263Z" }, + { url = "https://files.pythonhosted.org/packages/04/51/993a25a8b5220e23e0a31ff98747b8fce4685336e0fc4e8e156feab5c4f1/pyrnnoise-0.4.3-py3-none-manylinux1_x86_64.whl", hash = "sha256:1b094777e73797c5dd647782902c691ebb9a3c456c878e742597f5b55535a3db", size = 13273307, upload-time = "2026-01-14T08:44:27.801Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e4/9a13ede6521360341314bf90d5b687cd3f1bd4259bfea740dbc88340484a/pyrnnoise-0.4.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:161c57e05257e0b51f1b21675dcb2debb8cc86903c1fe2ccc3feb4322e545732", size = 13267247, upload-time = "2026-01-14T08:44:30.119Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/795f8504fa7f07fc16e99e82413a6fe997df1999e18bb6fab0b428431a92/pyrnnoise-0.4.3-py3-none-win_amd64.whl", hash = "sha256:25e7d8d63f251238a439e6e3d54ad8cb147c9f2b7c7c56fc9d9a496f682d8b06", size = 13267061, upload-time = "2026-01-14T08:45:03.444Z" }, ] [[package]] From c943ef9261f49fc6b67c2fb277dd4e1130d576b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Fri, 16 Jan 2026 00:08:56 +0100 Subject: [PATCH 13/50] keep uv.lock as it is. --- uv.lock | 75 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/uv.lock b/uv.lock index 4782f1c79..45c1f892f 100644 --- a/uv.lock +++ b/uv.lock @@ -38,12 +38,12 @@ wheels = [ [[package]] name = "aic-sdk" -version = "1.3.0" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/e7/56b6074224dc26a1350b165fd0ef3c8ca8c115cc1d4aa3e4f38af9c5d3f1/aic_sdk-1.3.0.tar.gz", hash = "sha256:ccccf7c0c35fd0342a0cbcd1ed81bd3fd7f59df51fbb7c1a80fb438a94ee6ae9", size = 37700, upload-time = "2025-12-12T13:00:09.11Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/ba/3ebe31b91e03d42437ec864e9d2af3a52b7ccc73a1a0c1026275956270b0/aic_sdk-1.2.0.tar.gz", hash = "sha256:eeda9a181c679f175dbe6f0efc0c67ec98ff3d84cfe01541fef7fa12ecd505ca", size = 35606, upload-time = "2025-11-20T14:42:14.333Z" } [[package]] name = "aioboto3" @@ -3292,47 +3292,47 @@ wheels = [ [[package]] name = "mlx" -version = "0.30.3" +version = "0.30.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mlx-metal", marker = "sys_platform == 'darwin'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/9c/d6f72f04eeeeaeee8309397efcfa0e923189d0b720f4ac6b3887d0a2f40b/mlx-0.30.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:685051761e428336f8f19ae76a761ce99d29ff67c52738f15ce6409e2ff34e6b", size = 568453, upload-time = "2026-01-14T01:16:40.796Z" }, - { url = "https://files.pythonhosted.org/packages/db/59/505717fd63f62d766f054ab8770d08e98b10217c0995bd2555429863fd31/mlx-0.30.3-cp310-cp310-macosx_15_0_arm64.whl", hash = "sha256:e405e6575e3b0b00dd6bd02bdb415b638cd5c2e5faedb696df2b2c8fbe871240", size = 568451, upload-time = "2026-01-14T01:16:42.027Z" }, - { url = "https://files.pythonhosted.org/packages/86/9c/a5319ae8ed0baa76fde80def12391ae13acec1b88904d4ead9bbabc9a083/mlx-0.30.3-cp310-cp310-macosx_26_0_arm64.whl", hash = "sha256:46894eb528457483aec44227f61afdff424cb76d146a6e1727d03ea0f52be41b", size = 568309, upload-time = "2026-01-14T05:52:06.915Z" }, - { url = "https://files.pythonhosted.org/packages/8c/7c/ac360b04c6b09acf11fcb54068909ca030325b248557930f6991d5601436/mlx-0.30.3-cp310-cp310-manylinux_2_35_aarch64.whl", hash = "sha256:a18e1b380130a77feda83b8bb8a7ff5a24f78e7263af484c6627f23e4210bdaf", size = 631435, upload-time = "2026-01-14T01:16:43.085Z" }, - { url = "https://files.pythonhosted.org/packages/01/b5/3becb2c93955d43539d9c916b33899a57c50099c29310dc2b5c68ff7a88d/mlx-0.30.3-cp310-cp310-manylinux_2_35_x86_64.whl", hash = "sha256:c27f8e78b89cf97411d740a2ca46accf6c6e3fcc43d1e906389abff1f0e00376", size = 664899, upload-time = "2026-01-14T01:16:44.623Z" }, - { url = "https://files.pythonhosted.org/packages/78/b6/dfcfffc41d832a86249715fab336dc8638c2237035287eb24af792484c53/mlx-0.30.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:794e79587a4906bdb3c5473ef936f45008eaaa609a3c498cc29a442b2c829621", size = 568664, upload-time = "2026-01-14T01:16:45.573Z" }, - { url = "https://files.pythonhosted.org/packages/22/9f/22d494b83b611380063da31c2b482db8c620f7ad6531cfcd1e11f7c35852/mlx-0.30.3-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:472cdc6eaca8610224621a1561e8c36477eab1a2f0dd3eb49b95484d739c4605", size = 568663, upload-time = "2026-01-14T01:16:46.588Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/b6fb0500aef8e9ed65d4730d8c34b13d7a770ca863b9af363b5713a16040/mlx-0.30.3-cp311-cp311-macosx_26_0_arm64.whl", hash = "sha256:a5d82be69c7e671dc4d5855d2f6aedcb507817e5985478903ab754b642d9ba01", size = 568522, upload-time = "2026-01-14T05:52:08.334Z" }, - { url = "https://files.pythonhosted.org/packages/6e/23/ea140c35419ec133e1037d34d94854474cdd72c89eedc3a90b8ec65fb0ff/mlx-0.30.3-cp311-cp311-manylinux_2_35_aarch64.whl", hash = "sha256:009a9a1d2e234b9b269f455729202feaf22eb1faf2c7b85818f2473f6c2f9cbe", size = 632235, upload-time = "2026-01-14T01:16:47.764Z" }, - { url = "https://files.pythonhosted.org/packages/af/18/335d2d455b1e15036e315c6b64de8e6b4b04ec60576e1b99a651a7487014/mlx-0.30.3-cp311-cp311-manylinux_2_35_x86_64.whl", hash = "sha256:ba7141b6c251207d26a5611a0038d121cd13e367a59589d8c827e6af06b1f406", size = 664821, upload-time = "2026-01-14T01:16:48.8Z" }, - { url = "https://files.pythonhosted.org/packages/11/b3/e24c3a69dad0cf4404bb174c6fed0d804022da64758cd815a254e1cd0627/mlx-0.30.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:0b275168b80645a155b456e1a457a37fb5ee2c251e8fbd8db9e153351a9e2d2f", size = 569398, upload-time = "2026-01-14T01:16:49.804Z" }, - { url = "https://files.pythonhosted.org/packages/0b/87/d0804443da97a06d3439f6efb0ceffa178f530a121f0f4a6c77b39f8bfd7/mlx-0.30.3-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:6e818de14864982e832344198240a1dafba7d3316c4eb6f1b8e43b4dd25dd2ef", size = 569396, upload-time = "2026-01-14T01:16:51.007Z" }, - { url = "https://files.pythonhosted.org/packages/cf/dc/7cdd95e4561b73fba8c86bf11293797076120400e472fe2a72ef483b6d8d/mlx-0.30.3-cp312-cp312-macosx_26_0_arm64.whl", hash = "sha256:d23b422209fd4b7ecacef59070321f8c6a122f906a5e9b6683a5fc9e1b8fcd5c", size = 569192, upload-time = "2026-01-14T05:52:09.715Z" }, - { url = "https://files.pythonhosted.org/packages/58/3b/6892f48dce949da7e1706cad45a1693857ef3adf23f849bf851c37e605eb/mlx-0.30.3-cp312-cp312-manylinux_2_35_aarch64.whl", hash = "sha256:f487461ffd5c2411c012dd8cd0d347dd807f05f223b1dec1c13bad0815cdcefd", size = 617390, upload-time = "2026-01-14T01:16:52.676Z" }, - { url = "https://files.pythonhosted.org/packages/66/ce/606e2111bc7c2ed1a2f2582caeb3e73b90e00d773d573fe9cd5dd36a0321/mlx-0.30.3-cp312-cp312-manylinux_2_35_x86_64.whl", hash = "sha256:b78627f324790fd0e06c4fa6e79b88094b342c5c425f8909de7c3f2fa5d01302", size = 659552, upload-time = "2026-01-14T01:16:53.888Z" }, - { url = "https://files.pythonhosted.org/packages/d0/22/42935d593fe82d3b98eb9d60e4620ed99703886635106f89d407c68f33bc/mlx-0.30.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:743fac1e4f9e8e46c8262943c643a31139c255cdb256c99ad496958215ccac1e", size = 569344, upload-time = "2026-01-14T01:16:54.847Z" }, - { url = "https://files.pythonhosted.org/packages/7d/27/f2e7a5236289d45315d0215e8553b4dd7e2faaba3bcb5025b34b25d5ab66/mlx-0.30.3-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:3b04ae81655aa0e63a6e8f2c749de3bbce64cf5b168ae10f39ed086dfa99e7f8", size = 569345, upload-time = "2026-01-14T01:16:56.564Z" }, - { url = "https://files.pythonhosted.org/packages/01/41/06b042457f51952456e9bb46b2c6e205ab3a28fc52d6751b5787fdb762b2/mlx-0.30.3-cp313-cp313-macosx_26_0_arm64.whl", hash = "sha256:ba9b5bdb1e929cc130af72efd7f73508c0f4e526d224489af7ec1c6419564659", size = 569213, upload-time = "2026-01-14T05:52:10.86Z" }, - { url = "https://files.pythonhosted.org/packages/ec/1e/f62c98fc0d2d878ee4235671f9d406b13cc9240493ba6fcfde2f72c2ff83/mlx-0.30.3-cp313-cp313-manylinux_2_35_aarch64.whl", hash = "sha256:dfe5c5b64e55398a22100804abbf9681996b03129e720e36b1727ed704db12b5", size = 617309, upload-time = "2026-01-14T01:16:57.58Z" }, - { url = "https://files.pythonhosted.org/packages/e9/62/811f064693449de740350d27793ce39343a460305ec8d878c318b80921d0/mlx-0.30.3-cp313-cp313-manylinux_2_35_x86_64.whl", hash = "sha256:a3364924610929936e6aaf13c71106161258e5a5d3f7813a64c07cc2435f9f55", size = 659521, upload-time = "2026-01-14T01:16:58.719Z" }, - { url = "https://files.pythonhosted.org/packages/82/e2/6e551bd48fb350fbf0ee4cc5cd09485437d260b8f4937f22d8623e14687a/mlx-0.30.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2c27fd8daaae14ca6cf407fcd236006a6e968f7708c8f61a2709116f2e754852", size = 571920, upload-time = "2026-01-14T01:16:59.683Z" }, - { url = "https://files.pythonhosted.org/packages/82/c0/561d1c9d3d12830b0e7fdcbd807585ef20909e398d4bcdbf25e4367543eb/mlx-0.30.3-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:b755fd4ed4b6a2ae4dee3766b5a2ea52fcbe83ebd1cf018458e18b74139409f3", size = 571921, upload-time = "2026-01-14T01:17:00.868Z" }, - { url = "https://files.pythonhosted.org/packages/42/1a/fb573fc2edc22a777fa254ff5c0c886ffd2c88aeb1f21c45778ef170f990/mlx-0.30.3-cp314-cp314-macosx_26_0_arm64.whl", hash = "sha256:7e352c0369a2f7e54d4f317b434eab3333918ea9edde1c43c61d36386b6f76bf", size = 571732, upload-time = "2026-01-14T05:52:11.893Z" }, - { url = "https://files.pythonhosted.org/packages/9e/db/d0083e8f2205b3b2dcd9670eb6f0d6c1b7cbfea6b01a1f8bff39142edf44/mlx-0.30.3-cp314-cp314-manylinux_2_35_aarch64.whl", hash = "sha256:00ac867f3d003c1477a66a579442c2040ba7ea43ce3c174490d1f8bf379606bd", size = 619635, upload-time = "2026-01-14T01:17:01.812Z" }, - { url = "https://files.pythonhosted.org/packages/ab/90/ab0b93ff0e76da4fe0e878722c76a308cfb950b044a4676e9617276d8ccd/mlx-0.30.3-cp314-cp314-manylinux_2_35_x86_64.whl", hash = "sha256:5be7d0329036f09c6ed003ea3e307e97e3144f20a3e4711b01810d7d5013cf2c", size = 659652, upload-time = "2026-01-14T01:17:02.915Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8d/16a34feb957ac33525b9b787b5132053a44bc94d1bf40c18639f6e05cd2a/mlx-0.30.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:391c650f0578ce359c8cffddb204b42798b622f9ee2b57b865d87716c00db536", size = 592926, upload-time = "2025-12-18T01:55:28.757Z" }, + { url = "https://files.pythonhosted.org/packages/34/e6/0661455f5f4bd9de257874b28a96a33699d36a1e17ccde821341c0ac1c0e/mlx-0.30.1-cp310-cp310-macosx_15_0_arm64.whl", hash = "sha256:42fefcad72d7488c65649e152a1b28f00c2033d38121afa45ce65ae16ec6b988", size = 592926, upload-time = "2025-12-18T01:55:30.141Z" }, + { url = "https://files.pythonhosted.org/packages/d8/37/a322af7dba9101064b5e858d1208e0e66cd83be7d060d14fa03ace37d52e/mlx-0.30.1-cp310-cp310-macosx_26_0_arm64.whl", hash = "sha256:a9db94e7e080672cc0dda9a5f121aebe0d49f7a8cb46706ecfd8b8ce7d99d541", size = 566952, upload-time = "2025-12-18T00:15:50.075Z" }, + { url = "https://files.pythonhosted.org/packages/c9/46/f0005d07fe5687bbf4efc15b468d27f2923f486b07a625d35c7d3cbb4962/mlx-0.30.1-cp310-cp310-manylinux_2_35_aarch64.whl", hash = "sha256:44b2142896c8dd8ab057dd785faf92fa83f3697b4b6bb01ff7515df12b6de666", size = 658049, upload-time = "2025-12-18T01:55:31.748Z" }, + { url = "https://files.pythonhosted.org/packages/cb/95/cc47c4607cc78f55ce3081ade9161961795c15c049bf219f27a393f85767/mlx-0.30.1-cp310-cp310-manylinux_2_35_x86_64.whl", hash = "sha256:37ea97b3c4bd71b19d87c6ef2c9e681e11f37908d8381fc2b785d2509b0681df", size = 692336, upload-time = "2025-12-18T01:55:33.224Z" }, + { url = "https://files.pythonhosted.org/packages/07/14/74acbd677ececd17a44dafda1b472aebacef54f60ff9a41a801f711de9a7/mlx-0.30.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:acfd7d1b8e5b9fa1b7e9fab4cc5ba6a492c559fbb1c5aeab16c1d7a148ab4f1b", size = 593048, upload-time = "2025-12-18T01:55:34.9Z" }, + { url = "https://files.pythonhosted.org/packages/58/8c/5309848afb9c53d363f59b88ae5811de248e2817e91aeadf007e2ac8d22b/mlx-0.30.1-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:b62030471272d1835b8137164bd43d863cc93ff1d67ec4f1f87bb4c8613dd5a6", size = 593043, upload-time = "2025-12-18T01:55:36.839Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5a/0039815a930f0193e2cffb27c57dc6971004bce0086c2bbbdb10395c272c/mlx-0.30.1-cp311-cp311-macosx_26_0_arm64.whl", hash = "sha256:0489cd340f2d262cb3aaad4368e40e84b152e182e4cea37ba018e56c72e1d020", size = 567014, upload-time = "2025-12-18T00:15:51.731Z" }, + { url = "https://files.pythonhosted.org/packages/de/c7/6bdb5497c1f5ed3e33afa7785761ad87fd3436c071805d9a93c905943f04/mlx-0.30.1-cp311-cp311-manylinux_2_35_aarch64.whl", hash = "sha256:fbdcfc3ed556a7e701a8eb67da299e2a25f52615193833ca6374decca3be5bf4", size = 658930, upload-time = "2025-12-18T01:55:38.441Z" }, + { url = "https://files.pythonhosted.org/packages/91/02/2d86a1c116e951eb4d88fe313c321e23628ce7404712e1258cacf925a8b8/mlx-0.30.1-cp311-cp311-manylinux_2_35_x86_64.whl", hash = "sha256:68ec854e7b5f89454e67d6c2fa7bb416b8afb148003ccd775904ec6ec4744818", size = 692484, upload-time = "2025-12-18T01:55:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/3a/4b/ad57b2f0ede3f0d009c0e3e1270c219bd18f9025388855ee149680cffa20/mlx-0.30.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:deaef3ecd2f99930867a29de748e3bffa9cc7e4dfa834f2501c37ed29aece1cc", size = 593397, upload-time = "2025-12-18T01:55:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/ef/14/7fa03a0f66ac3cfb2fd6752178a1488f13c7233fff26eed0f832d961db35/mlx-0.30.1-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:86ccdcda0b5ea4768b87da25beae5b83ac7cc802506116b6845cea6f450e2377", size = 593397, upload-time = "2025-12-18T01:55:43Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c8/9f1343dbe2381f9653df4e0a62dc8bf38f575a2553dc2aa6916de32d2a85/mlx-0.30.1-cp312-cp312-macosx_26_0_arm64.whl", hash = "sha256:a625cb434b2acc5674fe10683374641dab9671fb354ae7c2c67a1fb0405eeb37", size = 567576, upload-time = "2025-12-18T00:15:55.114Z" }, + { url = "https://files.pythonhosted.org/packages/15/ff/485ed9c99c18ef89ac987178c0a526cb4148ba38b14838d315311d9d76a8/mlx-0.30.1-cp312-cp312-manylinux_2_35_aarch64.whl", hash = "sha256:ccc1ff3aca8fb1073c7dcd1274cebe48ae75f852d14b16c7db8228fbbad594dd", size = 643654, upload-time = "2025-12-18T01:55:44.165Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d3/54d3bf5e404c3b6424b49c505dc8b3c06c6bb498fe720195b1fafbd69b5e/mlx-0.30.1-cp312-cp312-manylinux_2_35_x86_64.whl", hash = "sha256:55ed7fc4b389d6e49dac6d34a97b41e61cbe3662ac29c3d29cf612e6b2ed9827", size = 687305, upload-time = "2025-12-18T01:55:45.526Z" }, + { url = "https://files.pythonhosted.org/packages/f9/fd/c6f56cd87d48763ed63655ace627c06db9819eae7d43d132f40d4965947a/mlx-0.30.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:743520758bc8261b2ed8f3b3dc96e4e9236769dd8f61fb17877c5e44037e2058", size = 593366, upload-time = "2025-12-18T01:55:46.786Z" }, + { url = "https://files.pythonhosted.org/packages/dc/53/96d8c48b21f91c4216b6d2ef6dfc10862e5fb0b811a2aaf02c96c78601de/mlx-0.30.1-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:fc9745bc1860ca60128e3a6d36157da06d936e2b4007a4dcba990b40202f598f", size = 593368, upload-time = "2025-12-18T01:55:48.363Z" }, + { url = "https://files.pythonhosted.org/packages/70/ce/476c3b7d3a4153bd0e1c5af1f1b6c09a804b652bbed34072404b322c22e0/mlx-0.30.1-cp313-cp313-macosx_26_0_arm64.whl", hash = "sha256:a1480399c67bb327a66c5527b73915132e3fcaae3bce9634e5c81ccad9f43229", size = 567561, upload-time = "2025-12-18T00:15:56.153Z" }, + { url = "https://files.pythonhosted.org/packages/33/41/7ad1e639fd7dd1cf01a62c1c5b051024a859888c27504996e9d8380e6754/mlx-0.30.1-cp313-cp313-manylinux_2_35_aarch64.whl", hash = "sha256:8e19850a4236a8e174f851f5789b8b62a8eb74f5a8fa49ad8ba286c5ddb5f9bf", size = 643122, upload-time = "2025-12-18T01:55:49.607Z" }, + { url = "https://files.pythonhosted.org/packages/d0/dc/72d3737c5b0662eb5e785d353dbc5e34d793d27b09b99e39993ee051bd19/mlx-0.30.1-cp313-cp313-manylinux_2_35_x86_64.whl", hash = "sha256:1c8ed5bcd9f1910fca209e95859ac737e60b3e1954181b820fa269158f81049a", size = 687254, upload-time = "2025-12-18T01:55:51.239Z" }, + { url = "https://files.pythonhosted.org/packages/9b/cc/523448996247bb05d9d68e23bccf3dafdda660befb9330f6bd5fa13361e8/mlx-0.30.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d34cc2c25b0ee41c1349f14650db760e282685339858e305453f62405c12bc1b", size = 596006, upload-time = "2025-12-18T01:55:52.463Z" }, + { url = "https://files.pythonhosted.org/packages/23/0e/f9f2f9659c34c87be8f4167f6a1d6ed7e826f4889d20eecd4c0d8122f0e9/mlx-0.30.1-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:4e47d301e9095b87f0bda8827bfd6ffe744223aba5cee8f28e25894d647f5823", size = 596008, upload-time = "2025-12-18T01:55:54.02Z" }, + { url = "https://files.pythonhosted.org/packages/56/a7/49e41fb141de95b6a376091a963c737839c9cda04e423c67f57460a50458/mlx-0.30.1-cp314-cp314-macosx_26_0_arm64.whl", hash = "sha256:cfba13e2a52255d663a1ad62f0f83eb3991e42147edf9a8d38cdd224e48ca49b", size = 570406, upload-time = "2025-12-18T00:15:57.177Z" }, + { url = "https://files.pythonhosted.org/packages/73/99/a43cb112167cf865c069f5e108ae42f5314663930ff3dd86c2d23d984191/mlx-0.30.1-cp314-cp314-manylinux_2_35_aarch64.whl", hash = "sha256:bebfec377208eb29cc88aa86c897c7446aa0984838669e138f273f9225d627ff", size = 646461, upload-time = "2025-12-18T01:55:55.285Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ff/1e1968f107b4221a98dc26832586b1f646b27ddf3e55c95051c09d751f0a/mlx-0.30.1-cp314-cp314-manylinux_2_35_x86_64.whl", hash = "sha256:d18012d5cf0f013bc4a405cfd1e9d2d28e798f4d2dc4f15aa0fbffff73c02ba2", size = 687114, upload-time = "2025-12-18T01:55:56.506Z" }, ] [[package]] name = "mlx-metal" -version = "0.30.3" +version = "0.30.1" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/63/4d8f6fefb507c028df4454dabfe8d8e0ad2961bb06510b6aca23d2d5b2be/mlx_metal-0.30.3-py3-none-macosx_14_0_arm64.whl", hash = "sha256:6276312b02353714c7c6515169569fe1c4bebe3229c8ecf1fdb375a13e78c966", size = 37716245, upload-time = "2026-01-14T01:16:34.838Z" }, - { url = "https://files.pythonhosted.org/packages/35/91/1d452e48a4bb4958844fd3bb28ae31b8de110549c009ebec5024ce27ebf3/mlx_metal-0.30.3-py3-none-macosx_15_0_arm64.whl", hash = "sha256:c096c0a3428f3f96a06220f97a36f9528b18bc05173f821eb05bc8458e723fa8", size = 37712125, upload-time = "2026-01-14T01:16:38.619Z" }, - { url = "https://files.pythonhosted.org/packages/fe/36/7a3cbca85542b5ca4faf871e35927f43aa0e3fc830ae5b699780fe723677/mlx_metal-0.30.3-py3-none-macosx_26_0_arm64.whl", hash = "sha256:69068533bd1ee8b0379ce5de57ed5fd313577a10ecab58e1332fd1ff7248a75e", size = 46488962, upload-time = "2026-01-14T05:52:04.523Z" }, + { url = "https://files.pythonhosted.org/packages/09/3f/0be35ddad7e13d8ecd33a9185895f9739bb00b96ef0cce36cf0405d4aec0/mlx_metal-0.30.1-py3-none-macosx_14_0_arm64.whl", hash = "sha256:e7e92c6bdbd7ac8083f528a4c6640552ae106a57bb3d99856ac10a32e93a4b5e", size = 36864966, upload-time = "2025-12-18T01:55:31.473Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1f/c0bddd0d5bf3871411aabe32121e09e1b7cdbece8917a49d5a442310e3e5/mlx_metal-0.30.1-py3-none-macosx_15_0_arm64.whl", hash = "sha256:bb50f57418af7fc3c42a2da2c4bde0e7ab7ac0b997de1f6f642a6680ac65d626", size = 36859011, upload-time = "2025-12-18T01:55:34.541Z" }, + { url = "https://files.pythonhosted.org/packages/67/b3/73cc2f584ac612a476096d35a61eed75ee7ed8b4e320b0c36cf60a14d4eb/mlx_metal-0.30.1-py3-none-macosx_26_0_arm64.whl", hash = "sha256:e0b151a0053ac00b4226710bfb6dbf54b87283fb01e10fb3877f9ea969f680aa", size = 44981160, upload-time = "2025-12-18T00:15:47.518Z" }, ] [[package]] @@ -4495,7 +4495,7 @@ docs = [ [package.metadata] requires-dist = [ { name = "accelerate", marker = "extra == 'moondream'", specifier = "~=1.10.0" }, - { name = "aic-sdk", marker = "extra == 'aic'", specifier = ">=1.2.0" }, + { name = "aic-sdk", marker = "extra == 'aic'", specifier = "~=1.2.0" }, { name = "aioboto3", marker = "extra == 'aws'", specifier = "~=15.5.0" }, { name = "aiofiles", specifier = ">=24.1.0,<25" }, { name = "aiohttp", specifier = ">=3.11.12,<4" }, @@ -5280,7 +5280,7 @@ wheels = [ [[package]] name = "pyrnnoise" -version = "0.4.3" +version = "0.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "audiolab" }, @@ -5290,10 +5290,9 @@ dependencies = [ { name = "tqdm" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/90/51bb94bcfd8aab186fd08902e0706a6eda5813485fb57eff011ce6ae4c51/pyrnnoise-0.4.3-py3-none-macosx_15_0_universal2.whl", hash = "sha256:bdd8e933d32457362e6f4e56831afa8155208825040ab075c4223baed755fa4f", size = 13381834, upload-time = "2026-01-14T08:44:28.263Z" }, - { url = "https://files.pythonhosted.org/packages/04/51/993a25a8b5220e23e0a31ff98747b8fce4685336e0fc4e8e156feab5c4f1/pyrnnoise-0.4.3-py3-none-manylinux1_x86_64.whl", hash = "sha256:1b094777e73797c5dd647782902c691ebb9a3c456c878e742597f5b55535a3db", size = 13273307, upload-time = "2026-01-14T08:44:27.801Z" }, - { url = "https://files.pythonhosted.org/packages/aa/e4/9a13ede6521360341314bf90d5b687cd3f1bd4259bfea740dbc88340484a/pyrnnoise-0.4.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:161c57e05257e0b51f1b21675dcb2debb8cc86903c1fe2ccc3feb4322e545732", size = 13267247, upload-time = "2026-01-14T08:44:30.119Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e9/795f8504fa7f07fc16e99e82413a6fe997df1999e18bb6fab0b428431a92/pyrnnoise-0.4.3-py3-none-win_amd64.whl", hash = "sha256:25e7d8d63f251238a439e6e3d54ad8cb147c9f2b7c7c56fc9d9a496f682d8b06", size = 13267061, upload-time = "2026-01-14T08:45:03.444Z" }, + { url = "https://files.pythonhosted.org/packages/59/49/7017ffa14230096e0271bd49dfd9ab60a32bfebe7e71399c2a0e38c6f859/pyrnnoise-0.4.1-py3-none-macosx_15_0_universal2.whl", hash = "sha256:c1fe407729190d0f84f3e3c9d9322ebbd33b27f3f5d9f7217379b71a4dd043e7", size = 13381833, upload-time = "2025-11-25T15:54:06.532Z" }, + { url = "https://files.pythonhosted.org/packages/8e/24/fb8b7bafb3dd9cbb46e134fa25c9597683c61b42c0133453fefeebeb0066/pyrnnoise-0.4.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:ddd39b45221b65fb235f882a0ce127513a1012d41c5b3ba9dc4e9e991b22c205", size = 13273307, upload-time = "2025-11-25T15:54:04.076Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8e/eef9b2022fa5b9a111ba31d2f25ccd6e45da3daf16d20352e1fb18fd81dd/pyrnnoise-0.4.1-py3-none-win_amd64.whl", hash = "sha256:440e32359256eb7947e29fb080e800e984ba521fbe89a8b0b2f5dc196965e441", size = 13267076, upload-time = "2025-11-25T15:54:37.547Z" }, ] [[package]] From a4a9bae79e0f7f0cdb177f0c2ecb865b09b54ea1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Fri, 16 Jan 2026 12:31:41 +0100 Subject: [PATCH 14/50] drop v1 support from aic. --- src/pipecat/audio/utils.py | 69 +------------------------------------- 1 file changed, 1 insertion(+), 68 deletions(-) diff --git a/src/pipecat/audio/utils.py b/src/pipecat/audio/utils.py index c61aba4dd..29fc44ea9 100644 --- a/src/pipecat/audio/utils.py +++ b/src/pipecat/audio/utils.py @@ -14,10 +14,9 @@ various audio formats used in Pipecat pipelines. import audioop from typing import Literal -from loguru import logger - import numpy as np import pyloudnorm as pyln +from loguru import logger from pipecat.audio.resamplers.base_audio_resampler import BaseAudioResampler from pipecat.audio.resamplers.soxr_resampler import SOXRAudioResampler @@ -314,69 +313,3 @@ def is_silence(pcm_bytes: bytes) -> bool: # If max value is lower than SPEAKING_THRESHOLD, consider it as silence return max_value <= SPEAKING_THRESHOLD - - -def is_aic_sdk_v2() -> bool: - """Detect if aic-sdk v2 is installed by checking the module name. - - In v2, the module was renamed from 'aic' to 'aic_sdk'. - - Returns: - True if aic-sdk v2 (aic_sdk module) is installed, False if v1 (aic module). - - Raises: - ImportError: If neither aic nor aic_sdk module is installed. - """ - try: - import aic_sdk # noqa: F401 - - return True - except ModuleNotFoundError: - pass - - try: - import aic # noqa: F401 - - return False - except ModuleNotFoundError: - logger.error("In order to use the AIC filter, you need to `pip install pipecat-ai[aic]`.") - raise ImportError( - "aic-sdk is not installed. Install with 'pip install pipecat-ai[aic]'." - ) - - -def check_aic_sdk_version(required_version: Literal["v1", "v2"]) -> None: - """Check if the aic-sdk is installed and compatible with the module. - - This function checks both that the aic-sdk is installed and that its version - is compatible with the module requirements. Version detection is based on - the module name: v2 uses 'aic_sdk', v1 uses 'aic'. - - Args: - required_version: Either "v1" (for aic-sdk < 2.0.0) or "v2" (for aic-sdk >= 2.0.0). - - Raises: - ImportError: If aic-sdk is not installed or version is incompatible. - """ - is_v2 = is_aic_sdk_v2() - - if required_version == "v1" and is_v2: - error_msg = ( - "aic-sdk v2 (aic_sdk module) detected, but v1 (aic module) is required. " - "Please use the v2 classes instead: " - "'from pipecat.audio.filters.aic_filter import AICFilterV2' or " - "'from pipecat.audio.vad.aic_vad import AICVADAnalyzerV2'." - ) - logger.error(error_msg) - raise ImportError(error_msg) - - if required_version == "v2" and not is_v2: - error_msg = ( - "aic-sdk v1 (aic module) detected, but v2 (aic_sdk module) is required. " - "Please update with 'pip install --upgrade aic-sdk>=2.0.0' " - "or use the v1 classes: " - "'from pipecat.audio.filters.aic_filter import AICFilter' or " - "'from pipecat.audio.vad.aic_vad import AICVADAnalyzer'." - ) - logger.error(error_msg) - raise ImportError(error_msg) From effb6aa8f4d50edb3af486d4e5bb55a3b021ffba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Fri, 16 Jan 2026 12:53:15 +0100 Subject: [PATCH 15/50] clean up unused imports in audio utils. --- src/pipecat/audio/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pipecat/audio/utils.py b/src/pipecat/audio/utils.py index 29fc44ea9..65f451675 100644 --- a/src/pipecat/audio/utils.py +++ b/src/pipecat/audio/utils.py @@ -12,11 +12,9 @@ various audio formats used in Pipecat pipelines. """ import audioop -from typing import Literal import numpy as np import pyloudnorm as pyln -from loguru import logger from pipecat.audio.resamplers.base_audio_resampler import BaseAudioResampler from pipecat.audio.resamplers.soxr_resampler import SOXRAudioResampler From 1e1e275fea70cc47730c8049353e461263306550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Mon, 19 Jan 2026 15:51:28 +0100 Subject: [PATCH 16/50] address feedback. --- src/pipecat/audio/filters/aic_filter.py | 85 +++++++++++++------------ 1 file changed, 44 insertions(+), 41 deletions(-) diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index 14b95c92d..16062c6ea 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -11,7 +11,7 @@ enhance audio streams in real time. It mirrors the structure of other filters li the Koala filter and integrates with Pipecat's input transport pipeline. Classes: - AICFilter: For aic-sdk >= 2.0.0 (uses 'aic_sdk' module) + AICFilter: For aic-sdk (uses 'aic_sdk' module) """ import os @@ -30,7 +30,7 @@ class AICFilter(BaseAudioFilter): """Audio filter using ai-coustics' AIC SDK for real-time enhancement. Buffers incoming audio to the model's preferred block size and processes - planar frames in-place using float32 samples in the linear -1..+1 range. + frames using float32 samples normalized to the -1..+1 range. .. note:: This class requires aic-sdk >= 2.0.0 (uses 'aic_sdk' module). @@ -84,12 +84,21 @@ class AICFilter(BaseAudioFilter): self._frames_per_block = 0 self._audio_buffer = bytearray() + # Audio format constants + self._bytes_per_sample = 2 # int16 = 2 bytes + self._dtype = np.int16 + self._scale = 32768.0 # 2^15, for normalizing int16 (-32768..32767) to float32 (-1.0..1.0) + # AIC SDK objects self._model = None self._processor = None self._processor_ctx = None self._vad_ctx = None + # Pre-allocated buffers (resized in start() once frames_per_block is known) + self._in_f32 = None + self._out_i16 = None + def get_vad_context(self): """Return the VAD context once the processor exists. @@ -156,6 +165,13 @@ class AICFilter(BaseAudioFilter): logger.debug(f"Model downloaded to: {model_path}") self._model = Model.from_file(model_path) + # Get optimal frames for this sample rate + self._frames_per_block = self._model.get_optimal_num_frames(self._sample_rate) + + # Allocate processing buffers now that we know the block size + self._in_f32 = np.zeros((1, self._frames_per_block), dtype=np.float32) + self._out_i16 = np.zeros(self._frames_per_block, dtype=np.int16) + # Create configuration config = ProcessorConfig.optimal( self._model, @@ -163,7 +179,7 @@ class AICFilter(BaseAudioFilter): ) # Create async processor - self._processor = ProcessorAsync(self._model, self._license_key or "", config) + self._processor = ProcessorAsync(self._model, self._license_key, config) # Get contexts for parameter control and VAD self._processor_ctx = self._processor.get_processor_context() @@ -171,12 +187,10 @@ class AICFilter(BaseAudioFilter): # Apply initial parameters if self._enhancement_level is not None: - level = float(self._enhancement_level if self._enabled else 0.0) + level = self._enhancement_level if self._enabled else 0.0 self._processor_ctx.set_parameter(ProcessorParameter.EnhancementLevel, level) if self._voice_gain is not None: - self._processor_ctx.set_parameter( - ProcessorParameter.VoiceGain, float(self._voice_gain) - ) + self._processor_ctx.set_parameter(ProcessorParameter.VoiceGain, self._voice_gain) self._aic_ready = True @@ -225,7 +239,7 @@ class AICFilter(BaseAudioFilter): self._enabled = frame.enable if self._processor_ctx is not None: try: - level = float(self._enhancement_level if self._enabled else 0.0) + level = self._enhancement_level if self._enabled else 0.0 self._processor_ctx.set_parameter(ProcessorParameter.EnhancementLevel, level) except Exception as e: # noqa: BLE001 logger.error(f"AIC set_parameter failed: {e}") @@ -237,52 +251,41 @@ class AICFilter(BaseAudioFilter): model's required block length. Returns enhanced audio data. Args: - audio: Raw audio data as bytes to be filtered (int16 PCM, planar). + audio: Raw audio data as bytes (int16 PCM). Returns: - Enhanced audio data as bytes (int16 PCM, planar). + Enhanced audio data as bytes (int16 PCM). """ if not self._aic_ready or self._processor is None: return audio self._audio_buffer.extend(audio) + available_frames = len(self._audio_buffer) // self._bytes_per_sample + num_blocks = available_frames // self._frames_per_block + + if num_blocks == 0: + return b"" filtered_chunks: List[bytes] = [] + mv = memoryview(self._audio_buffer) + block_size = self._frames_per_block * self._bytes_per_sample - # Number of int16 samples currently buffered - available_frames = len(self._audio_buffer) // 2 + for i in range(num_blocks): + start = i * block_size + block_i16 = np.frombuffer(mv[start : start + block_size], dtype=self._dtype) - while available_frames >= self._frames_per_block: - # Consume exactly one block worth of frames - samples_to_consume = self._frames_per_block * 1 - bytes_to_consume = samples_to_consume * 2 - block_bytes = bytes(self._audio_buffer[:bytes_to_consume]) + # Reuse input buffer, in-place divide + np.copyto(self._in_f32[0], block_i16) + self._in_f32 /= self._scale - # Convert to float32 in -1..+1 range and reshape to (channels, frames) - block_i16 = np.frombuffer(block_bytes, dtype=np.int16) - # Convert to float32 and normalize - block_f32 = block_i16.astype(np.float32) - - block_f32 *= (1.0 / 32768.0) - - # Reshape to (1, frames) for AIC SDK - block_f32 = block_f32.reshape((1, self._frames_per_block)) + out_f32 = await self._processor.process_async(self._in_f32) - # Process via async processor; returns ndarray (same shape) - out_f32 = await self._processor.process_async(block_f32) + # Convert float32 output back to int16 + np.multiply(out_f32, self._scale, out=self._in_f32) # reuse in_f32 as temp + np.clip(self._in_f32, -self._scale, self._scale - 1, out=self._in_f32) + np.copyto(self._out_i16, self._in_f32[0].astype(self._dtype)) - # Convert back to int16 bytes - # Denormalize and convert back to int16 - out_f32 *= 32768.0 - - # In-place clip to valid int16 range (-32768 to 32767) - np.clip(out_f32, -32768.0, 32767, out=out_f32) - out_i16 = out_f32.astype(dtype) - filtered_chunks.append(out_i16.reshape(-1).tobytes()) + filtered_chunks.append(self._out_i16.tobytes()) - # Slide buffer - self._audio_buffer = self._audio_buffer[bytes_to_consume:] - available_frames = len(self._audio_buffer) // 2 - - # Do not flush incomplete frames; keep them buffered for the next call + self._audio_buffer = self._audio_buffer[num_blocks * block_size :] return b"".join(filtered_chunks) From bdded9b0264e588cd02a032754d30b5643fbdeff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Mon, 19 Jan 2026 16:04:26 +0100 Subject: [PATCH 17/50] set SDK ID for telemetry in AIC filter. --- src/pipecat/audio/filters/aic_filter.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index 16062c6ea..f40ed979a 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -18,7 +18,7 @@ import os from typing import List, Optional import numpy as np -from aic_sdk import Model, ProcessorAsync, ProcessorConfig, ProcessorParameter +from aic_sdk import Model, ProcessorAsync, ProcessorConfig, ProcessorParameter, set_sdk_id from loguru import logger from pipecat.audio.filters.base_audio_filter import BaseAudioFilter @@ -62,6 +62,9 @@ class AICFilter(BaseAudioFilter): Raises: ValueError: If neither model_id nor model_path is provided. """ + # Set SDK ID for telemetry identification (6 = pipecat) + set_sdk_id(6) + if model_id is None and model_path is None: raise ValueError( "Either 'model_id' or 'model_path' must be provided. " From dcab79753b5f9bc7787497fae883a4499b8bc4cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Mon, 19 Jan 2026 16:20:45 +0100 Subject: [PATCH 18/50] even the parameters are fixed, keep aic ready for processing. --- src/pipecat/audio/filters/aic_filter.py | 100 +++++++++++++----------- 1 file changed, 55 insertions(+), 45 deletions(-) diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index f40ed979a..eb4ee7f04 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -18,7 +18,14 @@ import os from typing import List, Optional import numpy as np -from aic_sdk import Model, ProcessorAsync, ProcessorConfig, ProcessorParameter, set_sdk_id +from aic_sdk import ( + Model, + ParameterFixedError, + ProcessorAsync, + ProcessorConfig, + ProcessorParameter, + set_sdk_id, +) from loguru import logger from pipecat.audio.filters.base_audio_filter import BaseAudioFilter @@ -156,61 +163,64 @@ class AICFilter(BaseAudioFilter): """ self._sample_rate = sample_rate + # Load or download model + if self._model_path: + logger.debug(f"Loading AIC model from: {self._model_path}") + self._model = Model.from_file(self._model_path) + else: + logger.debug(f"Downloading AIC model: {self._model_id}") + os.makedirs(self._model_download_dir, exist_ok=True) + model_path = await Model.download_async(self._model_id, self._model_download_dir) + logger.debug(f"Model downloaded to: {model_path}") + self._model = Model.from_file(model_path) + + # Get optimal frames for this sample rate + self._frames_per_block = self._model.get_optimal_num_frames(self._sample_rate) + + # Allocate processing buffers now that we know the block size + self._in_f32 = np.zeros((1, self._frames_per_block), dtype=np.float32) + self._out_i16 = np.zeros(self._frames_per_block, dtype=np.int16) + + # Create configuration + config = ProcessorConfig.optimal( + self._model, + sample_rate=self._sample_rate, + ) + + # Create async processor try: - # Load or download model - if self._model_path: - logger.debug(f"Loading AIC model from: {self._model_path}") - self._model = Model.from_file(self._model_path) - else: - logger.debug(f"Downloading AIC model: {self._model_id}") - os.makedirs(self._model_download_dir, exist_ok=True) - model_path = await Model.download_async(self._model_id, self._model_download_dir) - logger.debug(f"Model downloaded to: {model_path}") - self._model = Model.from_file(model_path) - - # Get optimal frames for this sample rate - self._frames_per_block = self._model.get_optimal_num_frames(self._sample_rate) - - # Allocate processing buffers now that we know the block size - self._in_f32 = np.zeros((1, self._frames_per_block), dtype=np.float32) - self._out_i16 = np.zeros(self._frames_per_block, dtype=np.int16) - - # Create configuration - config = ProcessorConfig.optimal( - self._model, - sample_rate=self._sample_rate, - ) - - # Create async processor self._processor = ProcessorAsync(self._model, self._license_key, config) + self._aic_ready = True + except Exception as e: # noqa: BLE001 - surfacing SDK initialization errors + logger.error(f"AIC model initialization failed: {e}") + self._aic_ready = False - # Get contexts for parameter control and VAD - self._processor_ctx = self._processor.get_processor_context() - self._vad_ctx = self._processor.get_vad_context() + # Get contexts for parameter control and VAD + self._processor_ctx = self._processor.get_processor_context() + self._vad_ctx = self._processor.get_vad_context() + try: # Apply initial parameters if self._enhancement_level is not None: level = self._enhancement_level if self._enabled else 0.0 self._processor_ctx.set_parameter(ProcessorParameter.EnhancementLevel, level) + if self._voice_gain is not None: self._processor_ctx.set_parameter(ProcessorParameter.VoiceGain, self._voice_gain) + except ParameterFixedError as e: + logger.error(f"AIC parameter update failed: {e}") - self._aic_ready = True - - # Log processor information - logger.debug(f"ai-coustics filter started:") - logger.debug(f" Model ID: {self._model.get_id()}") - logger.debug(f" Sample rate: {self._sample_rate} Hz") - logger.debug(f" Frames per chunk: {self._frames_per_block}") - logger.debug(f" Enhancement strength: {int((self._enhancement_level or 1.0) * 100)}%") - logger.debug(f" Optimal sample rate: {self._model.get_optimal_sample_rate()} Hz") - logger.debug( - f" Output delay: {self._processor_ctx.get_output_delay()} samples " - f"({self._processor_ctx.get_output_delay() / self._sample_rate * 1000:.2f}ms)" - ) - except Exception as e: # noqa: BLE001 - surfacing SDK initialization errors - logger.error(f"AIC model initialization failed: {e}") - self._aic_ready = False + # Log processor information + logger.debug(f"ai-coustics filter started:") + logger.debug(f" Model ID: {self._model.get_id()}") + logger.debug(f" Sample rate: {self._sample_rate} Hz") + logger.debug(f" Frames per chunk: {self._frames_per_block}") + logger.debug(f" Enhancement strength: {int((self._enhancement_level or 1.0) * 100)}%") + logger.debug(f" Optimal sample rate: {self._model.get_optimal_sample_rate()} Hz") + logger.debug( + f" Output delay: {self._processor_ctx.get_output_delay()} samples " + f"({self._processor_ctx.get_output_delay() / self._sample_rate * 1000:.2f}ms)" + ) async def stop(self): """Clean up the AIC processor when stopping. From 7f86f4ac273454582b4cfc3562fea87d59508e1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Mon, 19 Jan 2026 16:30:53 +0100 Subject: [PATCH 19/50] fix class name. --- examples/foundational/07zd-interruptible-aicoustics.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/foundational/07zd-interruptible-aicoustics.py b/examples/foundational/07zd-interruptible-aicoustics.py index 7a2d692e4..2259ef016 100644 --- a/examples/foundational/07zd-interruptible-aicoustics.py +++ b/examples/foundational/07zd-interruptible-aicoustics.py @@ -12,7 +12,7 @@ import wave from dotenv import load_dotenv from loguru import logger -from pipecat.audio.filters.aic_filter import AICFilterV2 +from pipecat.audio.filters.aic_filter import AICFilter from pipecat.audio.turn.smart_turn.local_smart_turn_v3 import LocalSmartTurnAnalyzerV3 from pipecat.frames.frames import LLMRunFrame from pipecat.pipeline.pipeline import Pipeline @@ -45,10 +45,10 @@ audiobuffer = AudioBufferProcessor( ) -def _create_aic_filter() -> AICFilterV2: +def _create_aic_filter() -> AICFilter: license_key = os.getenv("AICOUSTICS_LICENSE_KEY", "") - return AICFilterV2( + return AICFilter( license_key=license_key, model_id="sparrow-xxs-48khz", enhancement_level=0.5, From 22b9aac2ff39b92a1306de622ee471b73a7a2eaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Mon, 19 Jan 2026 16:32:05 +0100 Subject: [PATCH 20/50] use quail model in the example. --- examples/foundational/07zd-interruptible-aicoustics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/foundational/07zd-interruptible-aicoustics.py b/examples/foundational/07zd-interruptible-aicoustics.py index 2259ef016..74252341d 100644 --- a/examples/foundational/07zd-interruptible-aicoustics.py +++ b/examples/foundational/07zd-interruptible-aicoustics.py @@ -50,7 +50,7 @@ def _create_aic_filter() -> AICFilter: return AICFilter( license_key=license_key, - model_id="sparrow-xxs-48khz", + model_id="quail-vf-l-16khz", enhancement_level=0.5, ) From f0cc54589e5f9ac44e2023b992bda4518e286958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Mon, 19 Jan 2026 16:54:44 +0100 Subject: [PATCH 21/50] remove enhancement level parameter from AICFilter. --- .../foundational/07zd-interruptible-aicoustics.py | 1 - src/pipecat/audio/filters/aic_filter.py | 15 ++++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/examples/foundational/07zd-interruptible-aicoustics.py b/examples/foundational/07zd-interruptible-aicoustics.py index 74252341d..5978741b8 100644 --- a/examples/foundational/07zd-interruptible-aicoustics.py +++ b/examples/foundational/07zd-interruptible-aicoustics.py @@ -51,7 +51,6 @@ def _create_aic_filter() -> AICFilter: return AICFilter( license_key=license_key, model_id="quail-vf-l-16khz", - enhancement_level=0.5, ) diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index eb4ee7f04..438d870ee 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -50,7 +50,6 @@ class AICFilter(BaseAudioFilter): model_id: Optional[str] = None, model_path: Optional[str] = None, model_download_dir: Optional[str] = None, - enhancement_level: Optional[float] = 1.0, voice_gain: Optional[float] = 1.0, ) -> None: """Initialize the AIC filter. @@ -63,7 +62,6 @@ class AICFilter(BaseAudioFilter): model_id is ignored and no download occurs. model_download_dir: Directory for downloading models. Defaults to a cache directory in user's home folder. - enhancement_level: Optional overall enhancement strength (0.0..1.0). voice_gain: Optional linear gain applied to detected speech (0.1..4.0). Raises: @@ -85,7 +83,6 @@ class AICFilter(BaseAudioFilter): "~/.cache/pipecat/aic-models" ) - self._enhancement_level = enhancement_level self._voice_gain = voice_gain self._enabled = True @@ -201,9 +198,9 @@ class AICFilter(BaseAudioFilter): try: # Apply initial parameters - if self._enhancement_level is not None: - level = self._enhancement_level if self._enabled else 0.0 - self._processor_ctx.set_parameter(ProcessorParameter.EnhancementLevel, level) + self._processor_ctx.set_parameter( + ProcessorParameter.Bypass, 0.0 if self._enabled else 1.0 + ) if self._voice_gain is not None: self._processor_ctx.set_parameter(ProcessorParameter.VoiceGain, self._voice_gain) @@ -215,7 +212,6 @@ class AICFilter(BaseAudioFilter): logger.debug(f" Model ID: {self._model.get_id()}") logger.debug(f" Sample rate: {self._sample_rate} Hz") logger.debug(f" Frames per chunk: {self._frames_per_block}") - logger.debug(f" Enhancement strength: {int((self._enhancement_level or 1.0) * 100)}%") logger.debug(f" Optimal sample rate: {self._model.get_optimal_sample_rate()} Hz") logger.debug( f" Output delay: {self._processor_ctx.get_output_delay()} samples " @@ -252,8 +248,9 @@ class AICFilter(BaseAudioFilter): self._enabled = frame.enable if self._processor_ctx is not None: try: - level = self._enhancement_level if self._enabled else 0.0 - self._processor_ctx.set_parameter(ProcessorParameter.EnhancementLevel, level) + self._processor_ctx.set_parameter( + ProcessorParameter.Bypass, 0.0 if self._enabled else 1.0 + ) except Exception as e: # noqa: BLE001 logger.error(f"AIC set_parameter failed: {e}") From e8d1bec03bace22a2937c037d433b3918be8b2c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Mon, 19 Jan 2026 17:02:25 +0100 Subject: [PATCH 22/50] Update src/pipecat/audio/filters/aic_filter.py Co-authored-by: Andres O. Vela --- src/pipecat/audio/filters/aic_filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index 438d870ee..f0ce20c42 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -40,7 +40,7 @@ class AICFilter(BaseAudioFilter): frames using float32 samples normalized to the -1..+1 range. .. note:: - This class requires aic-sdk >= 2.0.0 (uses 'aic_sdk' module). + This class requires aic-sdk ~= 2.0.0 (uses 'aic_sdk' module). """ def __init__( From ca4e3c79f91d9c2a55c721ac2965072e3fe2b022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Mon, 19 Jan 2026 17:03:37 +0100 Subject: [PATCH 23/50] Update pyproject.toml Co-authored-by: Andres O. Vela --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 65a2a4bf0..7e1e381d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ Issues = "https://github.com/pipecat-ai/pipecat/issues" Changelog = "https://github.com/pipecat-ai/pipecat/blob/main/CHANGELOG.md" [project.optional-dependencies] -aic = [ "aic-sdk>=2.0.0" ] +aic = [ "aic-sdk~=2.0.0" ] anthropic = [ "anthropic~=0.49.0" ] assemblyai = [ "pipecat-ai[websockets-base]" ] asyncai = [ "pipecat-ai[websockets-base]" ] From abebcf37bd226e1bcf7e928b7600b5e277591c12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Mon, 19 Jan 2026 17:06:39 +0100 Subject: [PATCH 24/50] address feedback. --- src/pipecat/audio/filters/aic_filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index f0ce20c42..90bcc003c 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -6,7 +6,7 @@ """ai-coustics AIC SDK audio filter for Pipecat. -This module provides audio filter implementations using ai-coustics' AIC SDK to +This module provides an audio filter implementation using ai-coustics' AIC SDK to enhance audio streams in real time. It mirrors the structure of other filters like the Koala filter and integrates with Pipecat's input transport pipeline. From 5fd43faec3dd9413ec9f7ac31270efe4ab9b2e09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Mon, 19 Jan 2026 17:33:27 +0100 Subject: [PATCH 25/50] add min speech duration. --- .../07zd-interruptible-aicoustics.py | 12 ++++++-- src/pipecat/audio/filters/aic_filter.py | 7 +++++ src/pipecat/audio/vad/aic_vad.py | 28 ++++++++++++++----- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/examples/foundational/07zd-interruptible-aicoustics.py b/examples/foundational/07zd-interruptible-aicoustics.py index 5978741b8..ca205fc1d 100644 --- a/examples/foundational/07zd-interruptible-aicoustics.py +++ b/examples/foundational/07zd-interruptible-aicoustics.py @@ -62,7 +62,9 @@ transport_params = { lambda aic: DailyParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=aic.create_vad_analyzer(speech_hold_duration=0.05, sensitivity=6.0), + vad_analyzer=aic.create_vad_analyzer( + speech_hold_duration=0.05, minimum_speech_duration=0.0, sensitivity=6.0 + ), audio_in_filter=aic, ) )(_create_aic_filter()), @@ -70,7 +72,9 @@ transport_params = { lambda aic: FastAPIWebsocketParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=aic.create_vad_analyzer(speech_hold_duration=0.05, sensitivity=6.0), + vad_analyzer=aic.create_vad_analyzer( + speech_hold_duration=0.05, minimum_speech_duration=0.0, sensitivity=6.0 + ), audio_in_filter=aic, ) )(_create_aic_filter()), @@ -78,7 +82,9 @@ transport_params = { lambda aic: TransportParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=aic.create_vad_analyzer(speech_hold_duration=0.05, sensitivity=6.0), + vad_analyzer=aic.create_vad_analyzer( + speech_hold_duration=0.05, minimum_speech_duration=0.0, sensitivity=6.0 + ), audio_in_filter=aic, ) )(_create_aic_filter()), diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index 90bcc003c..63f269b27 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -121,6 +121,7 @@ class AICFilter(BaseAudioFilter): self, *, speech_hold_duration: Optional[float] = None, + minimum_speech_duration: Optional[float] = None, sensitivity: Optional[float] = None, ): """Return an analyzer that will lazily instantiate the AIC VAD when ready. @@ -129,6 +130,9 @@ class AICFilter(BaseAudioFilter): - speech_hold_duration: How long VAD continues detecting after speech ends (in seconds). Range: 0.0 .. 20x model window length, Default (SDK): 0.05s + - minimum_speech_duration: + Minimum duration of speech required before VAD reports speech detected + (in seconds). Range: 0.0 .. 20x model window length, Default (SDK): 0.0s - sensitivity: Energy threshold sensitivity. Energy threshold = 10 ** (-sensitivity). Range: 1.0 .. 15.0, Default (SDK): 6.0 @@ -136,6 +140,8 @@ class AICFilter(BaseAudioFilter): Args: speech_hold_duration: Optional speech hold duration to configure on the VAD. If None, SDK default (0.05s) is used. + minimum_speech_duration: Optional minimum speech duration before VAD reports + speech detected. If None, SDK default (0.0s) is used. sensitivity: Optional sensitivity (energy threshold) to configure on the VAD. Range: 1.0 .. 15.0. If None, SDK default (6.0) is used. @@ -146,6 +152,7 @@ class AICFilter(BaseAudioFilter): return AICVADAnalyzer( vad_context_factory=lambda: self.get_vad_context(), speech_hold_duration=speech_hold_duration, + minimum_speech_duration=minimum_speech_duration, sensitivity=sensitivity, ) diff --git a/src/pipecat/audio/vad/aic_vad.py b/src/pipecat/audio/vad/aic_vad.py index 05b576ce8..85d70979f 100644 --- a/src/pipecat/audio/vad/aic_vad.py +++ b/src/pipecat/audio/vad/aic_vad.py @@ -10,6 +10,7 @@ Classes: from typing import Any, Callable, Optional +from aic_sdk import VadParameter from loguru import logger from pipecat.audio.vad.vad_analyzer import VADAnalyzer, VADParams @@ -29,6 +30,10 @@ class AICVADAnalyzer(VADAnalyzer): no longer contains speech (in seconds). Range: 0.0 .. 20x model window length Default (SDK): 0.05s (50ms) + - minimum_speech_duration: + Minimum duration of speech required before VAD reports speech detected (in seconds). + Range: 0.0 .. 20x model window length + Default (SDK): 0.0s - sensitivity: Controls the energy threshold sensitivity. Higher values make the detector less sensitive (require more energy to count as speech). @@ -37,7 +42,7 @@ class AICVADAnalyzer(VADAnalyzer): Default (SDK): 6.0 .. note:: - This class requires aic-sdk >= 2.0.0 (uses 'aic_sdk' module). + This class requires aic-sdk ~= 2.0.0 (uses 'aic_sdk' module). """ def __init__( @@ -45,6 +50,7 @@ class AICVADAnalyzer(VADAnalyzer): *, vad_context_factory: Optional[Callable[[], Any]] = None, speech_hold_duration: Optional[float] = None, + minimum_speech_duration: Optional[float] = None, sensitivity: Optional[float] = None, ): """Create an AIC VAD analyzer. @@ -58,6 +64,11 @@ class AICVADAnalyzer(VADAnalyzer): Optional override for AIC VAD speech hold duration (in seconds). Range: 0.0 .. 20x model window length. If None, the SDK default (0.05s) is used. + minimum_speech_duration: + Optional override for minimum speech duration before VAD reports + speech detected (in seconds). + Range: 0.0 .. 20x model window length. + If None, the SDK default (0.0s) is used. sensitivity: Optional override for AIC VAD sensitivity (energy threshold). Range: 1.0 .. 15.0. Energy threshold = 10 ** (-sensitivity). @@ -66,9 +77,11 @@ class AICVADAnalyzer(VADAnalyzer): # Use fixed VAD parameters for AIC: no user override fixed_params = VADParams(confidence=0.5, start_secs=0.0, stop_secs=0.0, min_volume=0.0) super().__init__(sample_rate=None, params=fixed_params) + self._vad_context_factory = vad_context_factory self._vad_ctx: Optional[Any] = None self._pending_speech_hold_duration: Optional[float] = speech_hold_duration + self._pending_minimum_speech_duration: Optional[float] = minimum_speech_duration self._pending_sensitivity: Optional[float] = sensitivity def bind_vad_context_factory(self, vad_context_factory: Callable[[], Any]): @@ -78,19 +91,20 @@ class AICVADAnalyzer(VADAnalyzer): def _apply_vad_params(self): """Apply optional AIC VAD parameters if available.""" - from aic_sdk import VadParameter - if self._vad_ctx is None or VadParameter is None: return + try: if self._pending_speech_hold_duration is not None: self._vad_ctx.set_parameter( - VadParameter.SpeechHoldDuration, float(self._pending_speech_hold_duration) + VadParameter.SpeechHoldDuration, self._pending_speech_hold_duration + ) + if self._pending_minimum_speech_duration is not None: + self._vad_ctx.set_parameter( + VadParameter.MinimumSpeechDuration, self._pending_minimum_speech_duration ) if self._pending_sensitivity is not None: - self._vad_ctx.set_parameter( - VadParameter.Sensitivity, float(self._pending_sensitivity) - ) + self._vad_ctx.set_parameter(VadParameter.Sensitivity, self._pending_sensitivity) except Exception as e: # noqa: BLE001 logger.debug(f"AIC VAD parameter application deferred/failed: {e}") From b77f8b065f2a44443fb90f2df4a4d72e6a686ff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Mon, 19 Jan 2026 17:41:52 +0100 Subject: [PATCH 26/50] remove voice gain. --- src/pipecat/audio/filters/aic_filter.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index 63f269b27..3deca5788 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -50,7 +50,6 @@ class AICFilter(BaseAudioFilter): model_id: Optional[str] = None, model_path: Optional[str] = None, model_download_dir: Optional[str] = None, - voice_gain: Optional[float] = 1.0, ) -> None: """Initialize the AIC filter. @@ -62,7 +61,6 @@ class AICFilter(BaseAudioFilter): model_id is ignored and no download occurs. model_download_dir: Directory for downloading models. Defaults to a cache directory in user's home folder. - voice_gain: Optional linear gain applied to detected speech (0.1..4.0). Raises: ValueError: If neither model_id nor model_path is provided. @@ -83,8 +81,6 @@ class AICFilter(BaseAudioFilter): "~/.cache/pipecat/aic-models" ) - self._voice_gain = voice_gain - self._enabled = True self._sample_rate = 0 self._aic_ready = False @@ -203,14 +199,11 @@ class AICFilter(BaseAudioFilter): self._processor_ctx = self._processor.get_processor_context() self._vad_ctx = self._processor.get_vad_context() + # Apply initial parameters try: - # Apply initial parameters self._processor_ctx.set_parameter( ProcessorParameter.Bypass, 0.0 if self._enabled else 1.0 ) - - if self._voice_gain is not None: - self._processor_ctx.set_parameter(ProcessorParameter.VoiceGain, self._voice_gain) except ParameterFixedError as e: logger.error(f"AIC parameter update failed: {e}") From 65b8e0e89c550971c956cae7bf0cfe6cf08235bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Mon, 19 Jan 2026 17:51:31 +0100 Subject: [PATCH 27/50] rename `enabled` to `bypass` in AICFilter for clarity. --- src/pipecat/audio/filters/aic_filter.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index 3deca5788..6a18efffc 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -81,7 +81,7 @@ class AICFilter(BaseAudioFilter): "~/.cache/pipecat/aic-models" ) - self._enabled = True + self._bypass = False self._sample_rate = 0 self._aic_ready = False self._frames_per_block = 0 @@ -202,7 +202,7 @@ class AICFilter(BaseAudioFilter): # Apply initial parameters try: self._processor_ctx.set_parameter( - ProcessorParameter.Bypass, 0.0 if self._enabled else 1.0 + ProcessorParameter.Bypass, 1.0 if self._bypass else 0.0 ) except ParameterFixedError as e: logger.error(f"AIC parameter update failed: {e}") @@ -245,11 +245,11 @@ class AICFilter(BaseAudioFilter): None """ if isinstance(frame, FilterEnableFrame): - self._enabled = frame.enable + self._bypass = not frame.enable if self._processor_ctx is not None: try: self._processor_ctx.set_parameter( - ProcessorParameter.Bypass, 0.0 if self._enabled else 1.0 + ProcessorParameter.Bypass, 1.0 if self._bypass else 0.0 ) except Exception as e: # noqa: BLE001 logger.error(f"AIC set_parameter failed: {e}") From 18b3ee743b2c5bdf0ac746398e99ee72b846d05d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Tue, 20 Jan 2026 10:21:37 +0100 Subject: [PATCH 28/50] replace `os` with `pathlib.Path` in AICFilter for path handling consistency. --- src/pipecat/audio/filters/aic_filter.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index 6a18efffc..748064806 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -14,7 +14,7 @@ Classes: AICFilter: For aic-sdk (uses 'aic_sdk' module) """ -import os +from pathlib import Path from typing import List, Optional import numpy as np @@ -49,7 +49,7 @@ class AICFilter(BaseAudioFilter): license_key: str = "", model_id: Optional[str] = None, model_path: Optional[str] = None, - model_download_dir: Optional[str] = None, + model_download_dir: Optional[Path] = None, ) -> None: """Initialize the AIC filter. @@ -59,8 +59,8 @@ class AICFilter(BaseAudioFilter): is not provided. See https://artifacts.ai-coustics.io/ for available models. model_path: Optional path to a local .aicmodel file. If provided, model_id is ignored and no download occurs. - model_download_dir: Directory for downloading models. Defaults to - a cache directory in user's home folder. + model_download_dir: Directory for downloading models as a Path object. + Defaults to a cache directory in user's home folder. Raises: ValueError: If neither model_id nor model_path is provided. @@ -77,8 +77,8 @@ class AICFilter(BaseAudioFilter): self._license_key = license_key self._model_id = model_id self._model_path = model_path - self._model_download_dir = model_download_dir or os.path.expanduser( - "~/.cache/pipecat/aic-models" + self._model_download_dir = model_download_dir or ( + Path.home() / ".cache" / "pipecat" / "aic-models" ) self._bypass = False @@ -169,8 +169,8 @@ class AICFilter(BaseAudioFilter): self._model = Model.from_file(self._model_path) else: logger.debug(f"Downloading AIC model: {self._model_id}") - os.makedirs(self._model_download_dir, exist_ok=True) - model_path = await Model.download_async(self._model_id, self._model_download_dir) + self._model_download_dir.mkdir(parents=True, exist_ok=True) + model_path = await Model.download_async(self._model_id, str(self._model_download_dir)) logger.debug(f"Model downloaded to: {model_path}") self._model = Model.from_file(model_path) From e4e22319589cda95b2d198a9f8dec23332f41799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Tue, 20 Jan 2026 13:22:00 +0100 Subject: [PATCH 29/50] Update src/pipecat/audio/vad/aic_vad.py Co-authored-by: Andres O. Vela --- src/pipecat/audio/vad/aic_vad.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipecat/audio/vad/aic_vad.py b/src/pipecat/audio/vad/aic_vad.py index 85d70979f..306af08d3 100644 --- a/src/pipecat/audio/vad/aic_vad.py +++ b/src/pipecat/audio/vad/aic_vad.py @@ -32,7 +32,7 @@ class AICVADAnalyzer(VADAnalyzer): Default (SDK): 0.05s (50ms) - minimum_speech_duration: Minimum duration of speech required before VAD reports speech detected (in seconds). - Range: 0.0 .. 20x model window length + Range: 0.0 .. 1.0 Default (SDK): 0.0s - sensitivity: Controls the energy threshold sensitivity. Higher values make the detector From dc8972cd94fb6d28049f7e8785262ef3ac657f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Tue, 20 Jan 2026 13:44:25 +0100 Subject: [PATCH 30/50] log optimal number of frames for given sample rate in AICFilter. --- src/pipecat/audio/filters/aic_filter.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index 748064806..e1136e411 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -213,6 +213,9 @@ class AICFilter(BaseAudioFilter): logger.debug(f" Sample rate: {self._sample_rate} Hz") logger.debug(f" Frames per chunk: {self._frames_per_block}") logger.debug(f" Optimal sample rate: {self._model.get_optimal_sample_rate()} Hz") + logger.debug( + f" Optimal number of frames for {self._sample_rate} Hz: {self._model.get_optimal_num_frames(self._sample_rate)}" + ) logger.debug( f" Output delay: {self._processor_ctx.get_output_delay()} samples " f"({self._processor_ctx.get_output_delay() / self._sample_rate * 1000:.2f}ms)" From 0e6a42395582f6e77d8ceeaff11443f4be67edaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Tue, 20 Jan 2026 13:45:11 +0100 Subject: [PATCH 31/50] Update src/pipecat/audio/filters/aic_filter.py Co-authored-by: Andres O. Vela --- src/pipecat/audio/filters/aic_filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index e1136e411..79c2ed547 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -128,7 +128,7 @@ class AICFilter(BaseAudioFilter): Range: 0.0 .. 20x model window length, Default (SDK): 0.05s - minimum_speech_duration: Minimum duration of speech required before VAD reports speech detected - (in seconds). Range: 0.0 .. 20x model window length, Default (SDK): 0.0s + (in seconds). Range: 0.0 .. 1.0, Default (SDK): 0.0s - sensitivity: Energy threshold sensitivity. Energy threshold = 10 ** (-sensitivity). Range: 1.0 .. 15.0, Default (SDK): 6.0 From 09b5b6b12df43af36f0d57c1efad6f20a76389ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Tue, 20 Jan 2026 13:45:41 +0100 Subject: [PATCH 32/50] Update src/pipecat/audio/vad/aic_vad.py Co-authored-by: Andres O. Vela --- src/pipecat/audio/vad/aic_vad.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pipecat/audio/vad/aic_vad.py b/src/pipecat/audio/vad/aic_vad.py index 306af08d3..48c615c93 100644 --- a/src/pipecat/audio/vad/aic_vad.py +++ b/src/pipecat/audio/vad/aic_vad.py @@ -1,8 +1,7 @@ """AIC-integrated VAD analyzer that lazily binds to the AIC SDK backend. This module provides VAD analyzer implementations that query the AIC SDK's -is_speech_detected() and map it to a float confidence (1.0/0.0). They use -10 ms windows based on the sample rate and apply optional AIC VAD parameters. +is_speech_detected() and map it to a float confidence (1.0/0.0). Classes: AICVADAnalyzer: For aic-sdk >= 2.0.0 (uses 'aic_sdk' module) From 648f20db6d483a40eb3e0ae9019524f3044560cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Tue, 20 Jan 2026 13:49:38 +0100 Subject: [PATCH 33/50] Update src/pipecat/audio/vad/aic_vad.py Co-authored-by: Andres O. Vela --- src/pipecat/audio/vad/aic_vad.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipecat/audio/vad/aic_vad.py b/src/pipecat/audio/vad/aic_vad.py index 48c615c93..d263bf9d3 100644 --- a/src/pipecat/audio/vad/aic_vad.py +++ b/src/pipecat/audio/vad/aic_vad.py @@ -30,7 +30,7 @@ class AICVADAnalyzer(VADAnalyzer): Range: 0.0 .. 20x model window length Default (SDK): 0.05s (50ms) - minimum_speech_duration: - Minimum duration of speech required before VAD reports speech detected (in seconds). + Controls for how long speech needs to be present in the audio signal before the VAD considers it speech (in seconds). Range: 0.0 .. 1.0 Default (SDK): 0.0s - sensitivity: From 0e99400148210745fb66225ef520e0fde274be9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Tue, 20 Jan 2026 14:03:10 +0100 Subject: [PATCH 34/50] two dots are rust specific thinks, I'm not sure if it's familiar for Python developers. --- src/pipecat/audio/filters/aic_filter.py | 14 ++++++++------ src/pipecat/audio/vad/aic_vad.py | 14 +++++++------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index 79c2ed547..05536aebf 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -37,7 +37,7 @@ class AICFilter(BaseAudioFilter): """Audio filter using ai-coustics' AIC SDK for real-time enhancement. Buffers incoming audio to the model's preferred block size and processes - frames using float32 samples normalized to the -1..+1 range. + frames using float32 samples normalized to the range -1 to +1. .. note:: This class requires aic-sdk ~= 2.0.0 (uses 'aic_sdk' module). @@ -90,7 +90,9 @@ class AICFilter(BaseAudioFilter): # Audio format constants self._bytes_per_sample = 2 # int16 = 2 bytes self._dtype = np.int16 - self._scale = 32768.0 # 2^15, for normalizing int16 (-32768..32767) to float32 (-1.0..1.0) + self._scale = ( + 32768.0 # 2^15, for normalizing int16 (-32768 to 32767) to float32 (-1.0 to 1.0) + ) # AIC SDK objects self._model = None @@ -125,13 +127,13 @@ class AICFilter(BaseAudioFilter): AIC VAD parameters: - speech_hold_duration: How long VAD continues detecting after speech ends (in seconds). - Range: 0.0 .. 20x model window length, Default (SDK): 0.05s + Range: 0.0 to 20x model window length, Default (SDK): 0.05s - minimum_speech_duration: Minimum duration of speech required before VAD reports speech detected - (in seconds). Range: 0.0 .. 1.0, Default (SDK): 0.0s + (in seconds). Range: 0.0 to 1.0, Default (SDK): 0.0s - sensitivity: Energy threshold sensitivity. Energy threshold = 10 ** (-sensitivity). - Range: 1.0 .. 15.0, Default (SDK): 6.0 + Range: 1.0 to 15.0, Default (SDK): 6.0 Args: speech_hold_duration: Optional speech hold duration to configure on the VAD. @@ -139,7 +141,7 @@ class AICFilter(BaseAudioFilter): minimum_speech_duration: Optional minimum speech duration before VAD reports speech detected. If None, SDK default (0.0s) is used. sensitivity: Optional sensitivity (energy threshold) to configure on the VAD. - Range: 1.0 .. 15.0. If None, SDK default (6.0) is used. + Range: 1.0 to 15.0. If None, SDK default (6.0) is used. Returns: A lazily-initialized AICVADAnalyzer that will bind to the VAD context diff --git a/src/pipecat/audio/vad/aic_vad.py b/src/pipecat/audio/vad/aic_vad.py index d263bf9d3..17df6d094 100644 --- a/src/pipecat/audio/vad/aic_vad.py +++ b/src/pipecat/audio/vad/aic_vad.py @@ -4,7 +4,7 @@ This module provides VAD analyzer implementations that query the AIC SDK's is_speech_detected() and map it to a float confidence (1.0/0.0). Classes: - AICVADAnalyzer: For aic-sdk >= 2.0.0 (uses 'aic_sdk' module) + AICVADAnalyzer: For aic-sdk (uses 'aic_sdk' module) """ from typing import Any, Callable, Optional @@ -27,16 +27,16 @@ class AICVADAnalyzer(VADAnalyzer): - speech_hold_duration: Controls for how long the VAD continues to detect speech after the audio signal no longer contains speech (in seconds). - Range: 0.0 .. 20x model window length + Range: 0.0 to 20x model window length Default (SDK): 0.05s (50ms) - minimum_speech_duration: Controls for how long speech needs to be present in the audio signal before the VAD considers it speech (in seconds). - Range: 0.0 .. 1.0 + Range: 0.0 to 1.0 Default (SDK): 0.0s - sensitivity: Controls the energy threshold sensitivity. Higher values make the detector less sensitive (require more energy to count as speech). - Range: 1.0 .. 15.0 + Range: 1.0 to 15.0 Formula: Energy threshold = 10 ** (-sensitivity) Default (SDK): 6.0 @@ -61,16 +61,16 @@ class AICVADAnalyzer(VADAnalyzer): will retry on set_sample_rate/first use. speech_hold_duration: Optional override for AIC VAD speech hold duration (in seconds). - Range: 0.0 .. 20x model window length. + Range: 0.0 to 20x model window length. If None, the SDK default (0.05s) is used. minimum_speech_duration: Optional override for minimum speech duration before VAD reports speech detected (in seconds). - Range: 0.0 .. 20x model window length. + Range: 0.0 to 20x model window length. If None, the SDK default (0.0s) is used. sensitivity: Optional override for AIC VAD sensitivity (energy threshold). - Range: 1.0 .. 15.0. Energy threshold = 10 ** (-sensitivity). + Range: 1.0 to 15.0. Energy threshold = 10 ** (-sensitivity). If None, the SDK default (6.0) is used. """ # Use fixed VAD parameters for AIC: no user override From 0a8588669c3b36b88b04eead73ee99583e6e64b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Tue, 20 Jan 2026 14:11:00 +0100 Subject: [PATCH 35/50] address feedback. --- src/pipecat/audio/vad/aic_vad.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pipecat/audio/vad/aic_vad.py b/src/pipecat/audio/vad/aic_vad.py index 17df6d094..481db099a 100644 --- a/src/pipecat/audio/vad/aic_vad.py +++ b/src/pipecat/audio/vad/aic_vad.py @@ -30,12 +30,14 @@ class AICVADAnalyzer(VADAnalyzer): Range: 0.0 to 20x model window length Default (SDK): 0.05s (50ms) - minimum_speech_duration: - Controls for how long speech needs to be present in the audio signal before the VAD considers it speech (in seconds). + Controls for how long speech needs to be present in the audio signal before the + VAD considers it speech (in seconds). Range: 0.0 to 1.0 Default (SDK): 0.0s - sensitivity: - Controls the energy threshold sensitivity. Higher values make the detector - less sensitive (require more energy to count as speech). + Controls the sensitivity (energy threshold) of the VAD. This value is used by + the VAD as the threshold a speech audio signal's energy has to exceed in order + to be considered speech. Range: 1.0 to 15.0 Formula: Energy threshold = 10 ** (-sensitivity) Default (SDK): 6.0 From 91e86658b79fed77c16d436b1ba875968aeb68bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Tue, 20 Jan 2026 14:37:44 +0100 Subject: [PATCH 36/50] force developer to set a license key, it's required. --- src/pipecat/audio/filters/aic_filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index 05536aebf..94811479c 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -46,7 +46,7 @@ class AICFilter(BaseAudioFilter): def __init__( self, *, - license_key: str = "", + license_key: str, model_id: Optional[str] = None, model_path: Optional[str] = None, model_download_dir: Optional[Path] = None, From 70a85cd192aa7d33e0554d043630db445809a245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Tue, 20 Jan 2026 14:43:57 +0100 Subject: [PATCH 37/50] use path for keeping the consistency between the parameters. --- src/pipecat/audio/filters/aic_filter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index 94811479c..cd0841d40 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -48,7 +48,7 @@ class AICFilter(BaseAudioFilter): *, license_key: str, model_id: Optional[str] = None, - model_path: Optional[str] = None, + model_path: Optional[Path] = None, model_download_dir: Optional[Path] = None, ) -> None: """Initialize the AIC filter. @@ -168,7 +168,7 @@ class AICFilter(BaseAudioFilter): # Load or download model if self._model_path: logger.debug(f"Loading AIC model from: {self._model_path}") - self._model = Model.from_file(self._model_path) + self._model = Model.from_file(str(self._model_path)) else: logger.debug(f"Downloading AIC model: {self._model_id}") self._model_download_dir.mkdir(parents=True, exist_ok=True) From dca7f3b5b0da3a5d4e69bc6c115a6e4e6d49d786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Tue, 20 Jan 2026 15:04:07 +0100 Subject: [PATCH 38/50] add changelog. --- changelog/3408.added.md | 2 ++ changelog/3408.changed.md | 1 + changelog/3408.removed.md | 1 + 3 files changed, 4 insertions(+) create mode 100644 changelog/3408.added.md create mode 100644 changelog/3408.changed.md create mode 100644 changelog/3408.removed.md diff --git a/changelog/3408.added.md b/changelog/3408.added.md new file mode 100644 index 000000000..ea7c620e0 --- /dev/null +++ b/changelog/3408.added.md @@ -0,0 +1,2 @@ +- Added model downloading support to `AICFilter` with `model_id` and `model_download_dir` parameters. +- Added `model_path` parameter to `AICFilter` for loading local .aicmodel files. diff --git a/changelog/3408.changed.md b/changelog/3408.changed.md new file mode 100644 index 000000000..aeb563ea2 --- /dev/null +++ b/changelog/3408.changed.md @@ -0,0 +1 @@ +- Updated `AICFilter` and `AICVADAnalyzer` to use aic-sdk ~= 2.0.0. diff --git a/changelog/3408.removed.md b/changelog/3408.removed.md new file mode 100644 index 000000000..f578bf5d0 --- /dev/null +++ b/changelog/3408.removed.md @@ -0,0 +1 @@ +- Removed deprecated `AICFilter` parameters: `enhancement_level`, `voice_gain`, `noise_gate_enable`. From 34e9f224a86d9734b3437ffd2508f40236adfcdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Tue, 20 Jan 2026 15:27:03 +0100 Subject: [PATCH 39/50] Update src/pipecat/audio/vad/aic_vad.py Co-authored-by: Andres O. Vela --- src/pipecat/audio/vad/aic_vad.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipecat/audio/vad/aic_vad.py b/src/pipecat/audio/vad/aic_vad.py index 481db099a..2195c1310 100644 --- a/src/pipecat/audio/vad/aic_vad.py +++ b/src/pipecat/audio/vad/aic_vad.py @@ -68,7 +68,7 @@ class AICVADAnalyzer(VADAnalyzer): minimum_speech_duration: Optional override for minimum speech duration before VAD reports speech detected (in seconds). - Range: 0.0 to 20x model window length. + Range: 0.0 to 1.0. If None, the SDK default (0.0s) is used. sensitivity: Optional override for AIC VAD sensitivity (energy threshold). From bd92104fb3dbb554ea557ae9d827bb46de59aab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Fri, 23 Jan 2026 15:53:14 +0100 Subject: [PATCH 40/50] clarify voice confidence method behavior in AIC VAD. --- src/pipecat/audio/vad/aic_vad.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/pipecat/audio/vad/aic_vad.py b/src/pipecat/audio/vad/aic_vad.py index 2195c1310..6518d810c 100644 --- a/src/pipecat/audio/vad/aic_vad.py +++ b/src/pipecat/audio/vad/aic_vad.py @@ -149,13 +149,19 @@ class AICVADAnalyzer(VADAnalyzer): return int(self.sample_rate * 0.01) if self.sample_rate > 0 else 160 def voice_confidence(self, buffer: bytes) -> float: - """Calculate voice activity confidence for the given audio buffer. + """Return voice activity detection result for the given audio buffer. + + Note: + The AIC SDK provides binary speech detection (not a probability score). + This method returns 1.0 when speech is detected and 0.0 otherwise, + rather than a true confidence value. Args: - buffer: Audio buffer to analyze. + buffer: Audio buffer (unused - AIC VAD state is updated internally + by the enhancement pipeline). Returns: - Voice confidence score is 0.0 or 1.0. + 1.0 if speech is detected, 0.0 otherwise. """ # Ensure VAD context exists (filter might have started since last call) self._ensure_vad_context_initialized() From afcdef8c8103d3b61699b06c24503394cb3f2d08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Fri, 23 Jan 2026 15:57:40 +0100 Subject: [PATCH 41/50] docstring clarification. --- src/pipecat/audio/vad/aic_vad.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pipecat/audio/vad/aic_vad.py b/src/pipecat/audio/vad/aic_vad.py index 6518d810c..dd371636d 100644 --- a/src/pipecat/audio/vad/aic_vad.py +++ b/src/pipecat/audio/vad/aic_vad.py @@ -20,8 +20,8 @@ class AICVADAnalyzer(VADAnalyzer): The analyzer can be constructed before the AIC Processor exists. Once the filter has started and the Processor is available, the provided factory will succeed and the - VadContext will be obtained. We then use the context's is_speech_detected() state - to derive confidence values. + VadContext will be obtained. The context's is_speech_detected() boolean state is + then mapped to 1.0 (speech) or 0.0 (no speech) to satisfy the VADAnalyzer interface. AIC VAD runtime parameters: - speech_hold_duration: From 58b901985294c54ac9b55e223673ca67b9296ce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Mon, 26 Jan 2026 09:14:16 +0100 Subject: [PATCH 42/50] bump aic-sdk to 2.0.1 in optional dependencies. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7e1e381d3..4928ab3c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ Issues = "https://github.com/pipecat-ai/pipecat/issues" Changelog = "https://github.com/pipecat-ai/pipecat/blob/main/CHANGELOG.md" [project.optional-dependencies] -aic = [ "aic-sdk~=2.0.0" ] +aic = [ "aic-sdk~=2.0.1" ] anthropic = [ "anthropic~=0.49.0" ] assemblyai = [ "pipecat-ai[websockets-base]" ] asyncai = [ "pipecat-ai[websockets-base]" ] From a824660df7524e892afd60fe40215f053e4ab554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Mon, 26 Jan 2026 09:56:36 +0100 Subject: [PATCH 43/50] add unit tests for `AICVADAnalyzer` and `AICFilter`. --- tests/test_aic_filter.py | 471 +++++++++++++++++++++++++++++++++++++++ tests/test_aic_vad.py | 322 ++++++++++++++++++++++++++ 2 files changed, 793 insertions(+) create mode 100644 tests/test_aic_filter.py create mode 100644 tests/test_aic_vad.py diff --git a/tests/test_aic_filter.py b/tests/test_aic_filter.py new file mode 100644 index 000000000..6499084af --- /dev/null +++ b/tests/test_aic_filter.py @@ -0,0 +1,471 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import asyncio +import unittest +from pathlib import Path +from unittest.mock import AsyncMock, MagicMock, patch + +import numpy as np + +# Check if aic_sdk is available +try: + import aic_sdk + + HAS_AIC_SDK = True +except ImportError: + HAS_AIC_SDK = False + +# Module path for patching +AIC_FILTER_MODULE = "pipecat.audio.filters.aic_filter" + + +class MockProcessor: + """A lightweight mock for AIC ProcessorAsync that mimics real behavior.""" + + def __init__(self): + self.processor_ctx = MockProcessorContext() + self.vad_ctx = MockVadContext() + + def get_processor_context(self): + return self.processor_ctx + + def get_vad_context(self): + return self.vad_ctx + + async def process_async(self, audio_array): + # Return a copy of the input (simulating passthrough) + return audio_array.copy() + + +class MockProcessorContext: + """A lightweight mock for AIC ProcessorContext.""" + + def __init__(self): + self.parameters_set: list[tuple] = [] + self.reset_called = False + self._output_delay = 0 + + def get_output_delay(self): + return self._output_delay + + def set_parameter(self, param, value): + self.parameters_set.append((param, value)) + + def reset(self): + self.reset_called = True + + +class MockVadContext: + """A lightweight mock for AIC VadContext.""" + + def __init__(self, speech_detected: bool = False): + self.speech_detected = speech_detected + self.parameters_set: list[tuple] = [] + + def is_speech_detected(self) -> bool: + return self.speech_detected + + def set_parameter(self, param, value): + self.parameters_set.append((param, value)) + + +class MockModel: + """A lightweight mock for AIC Model.""" + + def __init__(self, model_id: str = "test-model"): + self._model_id = model_id + self._optimal_num_frames = 160 + self._optimal_sample_rate = 16000 + + def get_optimal_num_frames(self, sample_rate: int): + """Return optimal number of frames for the given sample rate.""" + return self._optimal_num_frames + + def get_id(self): + return self._model_id + + def get_optimal_sample_rate(self): + return self._optimal_sample_rate + + +@unittest.skipUnless(HAS_AIC_SDK, "aic-sdk not installed") +class TestAICFilter(unittest.IsolatedAsyncioTestCase): + """Test suite for AICFilter audio filter using real aic_sdk types.""" + + @classmethod + def setUpClass(cls): + """Import AICFilter after confirming aic_sdk is available.""" + from pipecat.audio.filters.aic_filter import AICFilter + from pipecat.frames.frames import FilterEnableFrame + + cls.AICFilter = AICFilter + cls.FilterEnableFrame = FilterEnableFrame + + def setUp(self): + """Set up test fixtures before each test method.""" + self.mock_model = MockModel() + self.mock_processor = MockProcessor() + + def _create_filter_with_mocks(self, **kwargs): + """Create an AICFilter with mocked SDK components.""" + filter_kwargs = { + "license_key": "test-key", + "model_id": "test-model", + } + filter_kwargs.update(kwargs) + with patch(f"{AIC_FILTER_MODULE}.set_sdk_id"): + return self.AICFilter(**filter_kwargs) + + async def _start_filter_with_mocks(self, filter_instance, sample_rate=16000): + """Start a filter with mocked SDK components.""" + with ( + patch(f"{AIC_FILTER_MODULE}.Model") as mock_model_cls, + patch(f"{AIC_FILTER_MODULE}.ProcessorConfig") as mock_config_cls, + patch(f"{AIC_FILTER_MODULE}.ProcessorAsync", return_value=self.mock_processor), + ): + mock_model_cls.from_file.return_value = self.mock_model + mock_model_cls.download_async = AsyncMock(return_value="/tmp/model") + mock_config_cls.optimal.return_value = MagicMock() + await filter_instance.start(sample_rate) + + async def test_initialization_requires_model_id_or_path(self): + """Test filter initialization fails without model_id or model_path.""" + with patch(f"{AIC_FILTER_MODULE}.set_sdk_id"): + with self.assertRaises(ValueError) as context: + self.AICFilter(license_key="test-key") + + self.assertIn("model_id", str(context.exception)) + self.assertIn("model_path", str(context.exception)) + + async def test_initialization_with_model_id(self): + """Test filter initialization with model_id.""" + filter_instance = self._create_filter_with_mocks() + + self.assertEqual(filter_instance._license_key, "test-key") + self.assertEqual(filter_instance._model_id, "test-model") + self.assertIsNone(filter_instance._model_path) + self.assertFalse(filter_instance._bypass) + + async def test_initialization_with_model_path(self): + """Test filter initialization with model_path.""" + model_path = Path("/tmp/test.aicmodel") + filter_instance = self._create_filter_with_mocks(model_id=None, model_path=model_path) + + self.assertEqual(filter_instance._model_path, model_path) + self.assertIsNone(filter_instance._model_id) + + async def test_initialization_with_custom_download_dir(self): + """Test filter initialization with custom model_download_dir.""" + download_dir = Path("/custom/cache") + filter_instance = self._create_filter_with_mocks(model_download_dir=download_dir) + + self.assertEqual(filter_instance._model_download_dir, download_dir) + + async def test_start_with_model_path(self): + """Test starting filter with a local model path.""" + model_path = Path("/tmp/test.aicmodel") + filter_instance = self._create_filter_with_mocks(model_id=None, model_path=model_path) + + with ( + patch(f"{AIC_FILTER_MODULE}.Model") as mock_model_cls, + patch(f"{AIC_FILTER_MODULE}.ProcessorConfig") as mock_config_cls, + patch(f"{AIC_FILTER_MODULE}.ProcessorAsync", return_value=self.mock_processor), + ): + mock_model_cls.from_file.return_value = self.mock_model + mock_config_cls.optimal.return_value = MagicMock() + + await filter_instance.start(16000) + + mock_model_cls.from_file.assert_called_once_with(str(model_path)) + self.assertTrue(filter_instance._aic_ready) + self.assertEqual(filter_instance._sample_rate, 16000) + self.assertEqual(filter_instance._frames_per_block, 160) + + async def test_start_with_model_id_downloads(self): + """Test starting filter with model_id triggers download.""" + filter_instance = self._create_filter_with_mocks() + + with ( + patch(f"{AIC_FILTER_MODULE}.Model") as mock_model_cls, + patch(f"{AIC_FILTER_MODULE}.ProcessorConfig") as mock_config_cls, + patch(f"{AIC_FILTER_MODULE}.ProcessorAsync", return_value=self.mock_processor), + ): + mock_model_cls.from_file.return_value = self.mock_model + mock_model_cls.download_async = AsyncMock(return_value="/tmp/model") + mock_config_cls.optimal.return_value = MagicMock() + + await filter_instance.start(16000) + + mock_model_cls.download_async.assert_called_once() + mock_model_cls.from_file.assert_called_once() + self.assertTrue(filter_instance._aic_ready) + + async def test_start_creates_processor(self): + """Test that start creates processor with correct config.""" + filter_instance = self._create_filter_with_mocks() + + with ( + patch(f"{AIC_FILTER_MODULE}.Model") as mock_model_cls, + patch(f"{AIC_FILTER_MODULE}.ProcessorConfig") as mock_config_cls, + patch( + f"{AIC_FILTER_MODULE}.ProcessorAsync", return_value=self.mock_processor + ) as mock_processor_cls, + ): + mock_model_cls.from_file.return_value = self.mock_model + mock_model_cls.download_async = AsyncMock(return_value="/tmp/model") + mock_config_cls.optimal.return_value = MagicMock() + + await filter_instance.start(16000) + + mock_config_cls.optimal.assert_called_once() + mock_processor_cls.assert_called_once() + self.assertIsNotNone(filter_instance._processor_ctx) + self.assertIsNotNone(filter_instance._vad_ctx) + + async def test_start_applies_initial_bypass_parameter(self): + """Test that start applies bypass parameter.""" + filter_instance = self._create_filter_with_mocks() + await self._start_filter_with_mocks(filter_instance) + + # Check that bypass was set to 0.0 (enabled) + bypass_params = [ + (p, v) + for p, v in self.mock_processor.processor_ctx.parameters_set + if p == aic_sdk.ProcessorParameter.Bypass + ] + self.assertTrue(len(bypass_params) > 0) + self.assertEqual(bypass_params[-1][1], 0.0) + + async def test_stop_cleans_up_resources(self): + """Test that stop properly cleans up resources.""" + filter_instance = self._create_filter_with_mocks() + await self._start_filter_with_mocks(filter_instance) + + await filter_instance.stop() + + self.assertTrue(self.mock_processor.processor_ctx.reset_called) + self.assertIsNone(filter_instance._processor) + self.assertIsNone(filter_instance._processor_ctx) + self.assertIsNone(filter_instance._vad_ctx) + self.assertIsNone(filter_instance._model) + self.assertFalse(filter_instance._aic_ready) + + async def test_stop_without_start(self): + """Test that stop can be called safely without start.""" + filter_instance = self._create_filter_with_mocks() + + # Should not raise + await filter_instance.stop() + + async def test_process_frame_enable(self): + """Test processing FilterEnableFrame to enable filtering.""" + filter_instance = self._create_filter_with_mocks() + await self._start_filter_with_mocks(filter_instance) + filter_instance._bypass = True + + enable_frame = self.FilterEnableFrame(enable=True) + await filter_instance.process_frame(enable_frame) + + self.assertFalse(filter_instance._bypass) + + async def test_process_frame_disable(self): + """Test processing FilterEnableFrame to disable filtering.""" + filter_instance = self._create_filter_with_mocks() + await self._start_filter_with_mocks(filter_instance) + + disable_frame = self.FilterEnableFrame(enable=False) + await filter_instance.process_frame(disable_frame) + + self.assertTrue(filter_instance._bypass) + + async def test_filter_when_not_ready(self): + """Test that filter returns audio unchanged when not ready.""" + filter_instance = self._create_filter_with_mocks() + # Don't call start() + + input_audio = b"\x00\x01\x02\x03" + output_audio = await filter_instance.filter(input_audio) + + self.assertEqual(output_audio, input_audio) + + async def test_filter_with_incomplete_frame(self): + """Test filtering audio with incomplete frame data.""" + filter_instance = self._create_filter_with_mocks() + await self._start_filter_with_mocks(filter_instance) + + # Create audio data for less than one frame (100 samples = 200 bytes) + samples = np.random.randint(-32768, 32767, size=100, dtype=np.int16) + input_audio = samples.tobytes() + + output_audio = await filter_instance.filter(input_audio) + + # Should return empty bytes since no complete frame + self.assertEqual(output_audio, b"") + + async def test_filter_with_complete_frame(self): + """Test filtering audio with exactly one complete frame.""" + filter_instance = self._create_filter_with_mocks() + await self._start_filter_with_mocks(filter_instance) + + # Create audio data for exactly one frame (160 samples = 320 bytes) + samples = np.random.randint(-32768, 32767, size=160, dtype=np.int16) + input_audio = samples.tobytes() + + output_audio = await filter_instance.filter(input_audio) + + self.assertIsInstance(output_audio, bytes) + self.assertEqual(len(output_audio), len(input_audio)) + + async def test_filter_with_multiple_frames(self): + """Test filtering audio with multiple complete frames.""" + filter_instance = self._create_filter_with_mocks() + await self._start_filter_with_mocks(filter_instance) + + # Create audio data for 3 complete frames (480 samples = 960 bytes) + samples = np.random.randint(-32768, 32767, size=480, dtype=np.int16) + input_audio = samples.tobytes() + + output_audio = await filter_instance.filter(input_audio) + + self.assertEqual(len(output_audio), len(input_audio)) + + async def test_filter_with_buffering(self): + """Test that filter properly buffers incomplete frames.""" + filter_instance = self._create_filter_with_mocks() + await self._start_filter_with_mocks(filter_instance) + + # First call: Send 100 samples (incomplete frame) + samples1 = np.random.randint(-32768, 32767, size=100, dtype=np.int16) + input_audio1 = samples1.tobytes() + output_audio1 = await filter_instance.filter(input_audio1) + + self.assertEqual(output_audio1, b"") + self.assertEqual(len(filter_instance._audio_buffer), 200) + + # Second call: Send 60 more samples (now we have 160 total = 1 complete frame) + samples2 = np.random.randint(-32768, 32767, size=60, dtype=np.int16) + input_audio2 = samples2.tobytes() + output_audio2 = await filter_instance.filter(input_audio2) + + self.assertEqual(len(output_audio2), 320) + self.assertEqual(len(filter_instance._audio_buffer), 0) + + async def test_filter_with_partial_buffering(self): + """Test that filter keeps remainder in buffer after processing.""" + filter_instance = self._create_filter_with_mocks() + await self._start_filter_with_mocks(filter_instance) + + # Send 250 samples (1 complete frame + 90 samples remainder) + samples = np.random.randint(-32768, 32767, size=250, dtype=np.int16) + input_audio = samples.tobytes() + + output_audio = await filter_instance.filter(input_audio) + + self.assertEqual(len(output_audio), 320) # 1 frame + self.assertEqual(len(filter_instance._audio_buffer), 180) # 90 samples * 2 bytes + + async def test_get_vad_context_before_start(self): + """Test that get_vad_context raises before start.""" + filter_instance = self._create_filter_with_mocks() + + with self.assertRaises(RuntimeError) as context: + filter_instance.get_vad_context() + + self.assertIn("not initialized", str(context.exception)) + + async def test_get_vad_context_after_start(self): + """Test that get_vad_context returns context after start.""" + filter_instance = self._create_filter_with_mocks() + await self._start_filter_with_mocks(filter_instance) + + vad_ctx = filter_instance.get_vad_context() + + self.assertEqual(vad_ctx, self.mock_processor.vad_ctx) + + async def test_create_vad_analyzer(self): + """Test create_vad_analyzer returns analyzer with factory.""" + filter_instance = self._create_filter_with_mocks() + + analyzer = filter_instance.create_vad_analyzer() + + self.assertIsNotNone(analyzer) + # Factory should be set + self.assertIsNotNone(analyzer._vad_context_factory) + + async def test_create_vad_analyzer_with_params(self): + """Test create_vad_analyzer with custom parameters.""" + filter_instance = self._create_filter_with_mocks() + + analyzer = filter_instance.create_vad_analyzer( + speech_hold_duration=0.1, + minimum_speech_duration=0.05, + sensitivity=8.0, + ) + + self.assertEqual(analyzer._pending_speech_hold_duration, 0.1) + self.assertEqual(analyzer._pending_minimum_speech_duration, 0.05) + self.assertEqual(analyzer._pending_sensitivity, 8.0) + + async def test_multiple_start_stop_cycles(self): + """Test multiple start/stop cycles.""" + filter_instance = self._create_filter_with_mocks() + + for sample_rate in [16000, 24000, 48000]: + # Create fresh mock processor for each cycle + self.mock_processor = MockProcessor() + await self._start_filter_with_mocks(filter_instance, sample_rate) + self.assertTrue(filter_instance._aic_ready) + self.assertEqual(filter_instance._sample_rate, sample_rate) + + await filter_instance.stop() + self.assertFalse(filter_instance._aic_ready) + + async def test_concurrent_filter_calls(self): + """Test that concurrent filter calls are handled safely.""" + filter_instance = self._create_filter_with_mocks() + await self._start_filter_with_mocks(filter_instance) + + samples = np.random.randint(-32768, 32767, size=160, dtype=np.int16) + input_audio = samples.tobytes() + + async def filter_audio(): + return await filter_instance.filter(input_audio) + + tasks = [filter_audio() for _ in range(10)] + results = await asyncio.gather(*tasks) + + self.assertEqual(len(results), 10) + for result in results: + self.assertIsInstance(result, bytes) + + async def test_buffer_cleared_on_stop(self): + """Test that audio buffer is cleared when stopping.""" + filter_instance = self._create_filter_with_mocks() + await self._start_filter_with_mocks(filter_instance) + + # Add incomplete frame to buffer + samples = np.random.randint(-32768, 32767, size=100, dtype=np.int16) + input_audio = samples.tobytes() + await filter_instance.filter(input_audio) + + # Verify buffer has data + self.assertGreater(len(filter_instance._audio_buffer), 0) + + # Stop should clear buffer + await filter_instance.stop() + self.assertEqual(len(filter_instance._audio_buffer), 0) + + async def test_set_sdk_id_called_on_init(self): + """Test that set_sdk_id is called during initialization.""" + with patch(f"{AIC_FILTER_MODULE}.set_sdk_id") as mock_set_sdk_id: + self.AICFilter(license_key="test-key", model_id="test-model") + + mock_set_sdk_id.assert_called_once_with(6) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_aic_vad.py b/tests/test_aic_vad.py new file mode 100644 index 000000000..5028da46b --- /dev/null +++ b/tests/test_aic_vad.py @@ -0,0 +1,322 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import unittest + +# Check if aic_sdk is available +try: + import aic_sdk + + HAS_AIC_SDK = True +except ImportError: + HAS_AIC_SDK = False + + +@unittest.skipUnless(HAS_AIC_SDK, "aic-sdk not installed") +class TestAICVADAnalyzer(unittest.IsolatedAsyncioTestCase): + """Test suite for AICVADAnalyzer using real aic_sdk.""" + + @classmethod + def setUpClass(cls): + """Import AICVADAnalyzer after confirming aic_sdk is available.""" + from pipecat.audio.vad.aic_vad import AICVADAnalyzer + + cls.AICVADAnalyzer = AICVADAnalyzer + + def test_initialization_without_factory(self): + """Test analyzer initialization without a factory.""" + analyzer = self.AICVADAnalyzer() + + self.assertIsNone(analyzer._vad_context_factory) + self.assertIsNone(analyzer._vad_ctx) + # Fixed params should be set + self.assertEqual(analyzer._params.confidence, 0.5) + self.assertEqual(analyzer._params.start_secs, 0.0) + self.assertEqual(analyzer._params.stop_secs, 0.0) + self.assertEqual(analyzer._params.min_volume, 0.0) + + def test_initialization_with_factory(self): + """Test analyzer initialization with a factory.""" + # Create a mock VAD context for testing + mock_vad_ctx = MockVadContext() + factory = lambda: mock_vad_ctx + analyzer = self.AICVADAnalyzer(vad_context_factory=factory) + + self.assertIsNotNone(analyzer._vad_context_factory) + + def test_initialization_with_vad_params(self): + """Test analyzer initialization with VAD parameters.""" + analyzer = self.AICVADAnalyzer( + speech_hold_duration=0.1, + minimum_speech_duration=0.05, + sensitivity=8.0, + ) + + self.assertEqual(analyzer._pending_speech_hold_duration, 0.1) + self.assertEqual(analyzer._pending_minimum_speech_duration, 0.05) + self.assertEqual(analyzer._pending_sensitivity, 8.0) + + def test_bind_vad_context_factory(self): + """Test binding a factory post-construction.""" + mock_vad_ctx = MockVadContext() + analyzer = self.AICVADAnalyzer() + factory = lambda: mock_vad_ctx + + analyzer.bind_vad_context_factory(factory) + + self.assertEqual(analyzer._vad_context_factory, factory) + # Should have attempted to initialize + self.assertEqual(analyzer._vad_ctx, mock_vad_ctx) + + def test_bind_vad_context_factory_applies_params(self): + """Test that binding factory applies pending VAD params.""" + mock_vad_ctx = MockVadContext() + analyzer = self.AICVADAnalyzer( + speech_hold_duration=0.1, + minimum_speech_duration=0.05, + sensitivity=8.0, + ) + factory = lambda: mock_vad_ctx + + analyzer.bind_vad_context_factory(factory) + + # Verify parameters were applied + self.assertIn( + (aic_sdk.VadParameter.SpeechHoldDuration, 0.1), + mock_vad_ctx.parameters_set, + ) + self.assertIn( + (aic_sdk.VadParameter.MinimumSpeechDuration, 0.05), + mock_vad_ctx.parameters_set, + ) + self.assertIn( + (aic_sdk.VadParameter.Sensitivity, 8.0), + mock_vad_ctx.parameters_set, + ) + + def test_set_sample_rate(self): + """Test setting sample rate.""" + analyzer = self.AICVADAnalyzer() + + analyzer.set_sample_rate(16000) + + self.assertEqual(analyzer._sample_rate, 16000) + + def test_set_sample_rate_with_init_sample_rate(self): + """Test that init_sample_rate takes precedence.""" + # Create analyzer and manually set _init_sample_rate + analyzer = self.AICVADAnalyzer() + analyzer._init_sample_rate = 48000 + + analyzer.set_sample_rate(16000) + + # init_sample_rate should take precedence + self.assertEqual(analyzer._sample_rate, 48000) + + def test_set_sample_rate_triggers_context_init(self): + """Test that set_sample_rate attempts context initialization.""" + mock_vad_ctx = MockVadContext() + factory = lambda: mock_vad_ctx + analyzer = self.AICVADAnalyzer(vad_context_factory=factory) + + analyzer.set_sample_rate(16000) + + self.assertEqual(analyzer._vad_ctx, mock_vad_ctx) + + def test_num_frames_required_with_sample_rate(self): + """Test num_frames_required returns correct value.""" + analyzer = self.AICVADAnalyzer() + analyzer.set_sample_rate(16000) + + frames = analyzer.num_frames_required() + + # 10ms at 16kHz = 160 frames + self.assertEqual(frames, 160) + + def test_num_frames_required_different_sample_rates(self): + """Test num_frames_required for different sample rates.""" + analyzer = self.AICVADAnalyzer() + + test_cases = [ + (8000, 80), # 10ms at 8kHz + (16000, 160), # 10ms at 16kHz + (24000, 240), # 10ms at 24kHz + (48000, 480), # 10ms at 48kHz + ] + + for sample_rate, expected_frames in test_cases: + analyzer.set_sample_rate(sample_rate) + frames = analyzer.num_frames_required() + self.assertEqual(frames, expected_frames, f"Failed for {sample_rate}Hz") + + def test_num_frames_required_no_sample_rate(self): + """Test num_frames_required returns default when no sample rate.""" + analyzer = self.AICVADAnalyzer() + + frames = analyzer.num_frames_required() + + # Default is 160 + self.assertEqual(frames, 160) + + def test_voice_confidence_no_context(self): + """Test voice_confidence returns 0.0 when no context.""" + analyzer = self.AICVADAnalyzer() + + confidence = analyzer.voice_confidence(b"\x00" * 320) + + self.assertEqual(confidence, 0.0) + + def test_voice_confidence_speech_detected(self): + """Test voice_confidence returns 1.0 when speech detected.""" + mock_vad_ctx = MockVadContext(speech_detected=True) + factory = lambda: mock_vad_ctx + analyzer = self.AICVADAnalyzer(vad_context_factory=factory) + analyzer.set_sample_rate(16000) + + confidence = analyzer.voice_confidence(b"\x00" * 320) + + self.assertEqual(confidence, 1.0) + + def test_voice_confidence_no_speech(self): + """Test voice_confidence returns 0.0 when no speech.""" + mock_vad_ctx = MockVadContext(speech_detected=False) + factory = lambda: mock_vad_ctx + analyzer = self.AICVADAnalyzer(vad_context_factory=factory) + analyzer.set_sample_rate(16000) + + confidence = analyzer.voice_confidence(b"\x00" * 320) + + self.assertEqual(confidence, 0.0) + + def test_voice_confidence_handles_exception(self): + """Test voice_confidence handles exceptions gracefully.""" + mock_vad_ctx = MockVadContext(raise_on_detect=True) + factory = lambda: mock_vad_ctx + analyzer = self.AICVADAnalyzer(vad_context_factory=factory) + analyzer.set_sample_rate(16000) + + confidence = analyzer.voice_confidence(b"\x00" * 320) + + self.assertEqual(confidence, 0.0) + + def test_lazy_initialization(self): + """Test that VAD context is lazily initialized.""" + call_count = 0 + mock_vad_ctx = MockVadContext() + + def counting_factory(): + nonlocal call_count + call_count += 1 + return mock_vad_ctx + + analyzer = self.AICVADAnalyzer(vad_context_factory=counting_factory) + + # Factory not called yet + self.assertEqual(call_count, 0) + + # First call to voice_confidence triggers initialization + analyzer.voice_confidence(b"\x00" * 320) + self.assertEqual(call_count, 1) + + # Subsequent calls don't re-initialize + analyzer.voice_confidence(b"\x00" * 320) + analyzer.voice_confidence(b"\x00" * 320) + self.assertEqual(call_count, 1) + + def test_deferred_initialization_on_factory_failure(self): + """Test that initialization is deferred when factory fails.""" + call_count = 0 + mock_vad_ctx = MockVadContext(speech_detected=True) + + def failing_then_succeeding_factory(): + nonlocal call_count + call_count += 1 + if call_count < 3: + raise RuntimeError("Not ready yet") + return mock_vad_ctx + + analyzer = self.AICVADAnalyzer(vad_context_factory=failing_then_succeeding_factory) + + # First two calls fail, should return 0.0 + self.assertEqual(analyzer.voice_confidence(b"\x00" * 320), 0.0) + self.assertEqual(analyzer.voice_confidence(b"\x00" * 320), 0.0) + + # Third call succeeds + self.assertEqual(analyzer.voice_confidence(b"\x00" * 320), 1.0) + + def test_apply_vad_params_deferred_on_failure(self): + """Test that VAD param application handles exceptions.""" + mock_vad_ctx = MockVadContext(raise_on_set_param=True) + factory = lambda: mock_vad_ctx + + analyzer = self.AICVADAnalyzer( + vad_context_factory=factory, + speech_hold_duration=0.1, + ) + + # Should not raise, just log debug message + analyzer.bind_vad_context_factory(factory) + + # Context should still be set despite param failure + self.assertEqual(analyzer._vad_ctx, mock_vad_ctx) + + def test_apply_vad_params_only_set_values(self): + """Test that only specified VAD params are applied.""" + mock_vad_ctx = MockVadContext() + factory = lambda: mock_vad_ctx + analyzer = self.AICVADAnalyzer( + vad_context_factory=factory, + speech_hold_duration=0.1, + # minimum_speech_duration and sensitivity not set + ) + + analyzer.bind_vad_context_factory(factory) + + # Only SpeechHoldDuration should be set + self.assertEqual(len(mock_vad_ctx.parameters_set), 1) + self.assertIn( + (aic_sdk.VadParameter.SpeechHoldDuration, 0.1), + mock_vad_ctx.parameters_set, + ) + + def test_fixed_vad_params(self): + """Test that VAD uses fixed parameters.""" + analyzer = self.AICVADAnalyzer() + + # These are the fixed params for AIC VAD + self.assertEqual(analyzer._params.confidence, 0.5) + self.assertEqual(analyzer._params.start_secs, 0.0) + self.assertEqual(analyzer._params.stop_secs, 0.0) + self.assertEqual(analyzer._params.min_volume, 0.0) + + +class MockVadContext: + """A lightweight mock for AIC VadContext that mimics real behavior.""" + + def __init__( + self, + speech_detected: bool = False, + raise_on_detect: bool = False, + raise_on_set_param: bool = False, + ): + self.speech_detected = speech_detected + self.raise_on_detect = raise_on_detect + self.raise_on_set_param = raise_on_set_param + self.parameters_set: list[tuple] = [] + + def is_speech_detected(self) -> bool: + if self.raise_on_detect: + raise RuntimeError("VAD error") + return self.speech_detected + + def set_parameter(self, param, value): + if self.raise_on_set_param: + raise RuntimeError("Param error") + self.parameters_set.append((param, value)) + + +if __name__ == "__main__": + unittest.main() From bd618d64e3f215dc08d878ce198d5a0adbf77e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Mon, 26 Jan 2026 10:06:16 +0100 Subject: [PATCH 44/50] Update src/pipecat/audio/filters/aic_filter.py Co-authored-by: Andres O. Vela --- src/pipecat/audio/filters/aic_filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index cd0841d40..66012c6c3 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -127,7 +127,7 @@ class AICFilter(BaseAudioFilter): AIC VAD parameters: - speech_hold_duration: How long VAD continues detecting after speech ends (in seconds). - Range: 0.0 to 20x model window length, Default (SDK): 0.05s + Range: 0.0 to 100x model window length, Default (SDK): 0.05s - minimum_speech_duration: Minimum duration of speech required before VAD reports speech detected (in seconds). Range: 0.0 to 1.0, Default (SDK): 0.0s From 3c463c9416d5dc68f7c26c8b779b14d21acb5f5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Mon, 26 Jan 2026 10:06:33 +0100 Subject: [PATCH 45/50] Update src/pipecat/audio/vad/aic_vad.py Co-authored-by: Andres O. Vela --- src/pipecat/audio/vad/aic_vad.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipecat/audio/vad/aic_vad.py b/src/pipecat/audio/vad/aic_vad.py index dd371636d..a608302bf 100644 --- a/src/pipecat/audio/vad/aic_vad.py +++ b/src/pipecat/audio/vad/aic_vad.py @@ -27,7 +27,7 @@ class AICVADAnalyzer(VADAnalyzer): - speech_hold_duration: Controls for how long the VAD continues to detect speech after the audio signal no longer contains speech (in seconds). - Range: 0.0 to 20x model window length + Range: 0.0 to 100x model window length Default (SDK): 0.05s (50ms) - minimum_speech_duration: Controls for how long speech needs to be present in the audio signal before the From 7572d63f8f1a1b381d869389b0a2cf5970305f5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Mon, 26 Jan 2026 10:06:40 +0100 Subject: [PATCH 46/50] Update src/pipecat/audio/vad/aic_vad.py Co-authored-by: Andres O. Vela --- src/pipecat/audio/vad/aic_vad.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipecat/audio/vad/aic_vad.py b/src/pipecat/audio/vad/aic_vad.py index a608302bf..959c6e35e 100644 --- a/src/pipecat/audio/vad/aic_vad.py +++ b/src/pipecat/audio/vad/aic_vad.py @@ -63,7 +63,7 @@ class AICVADAnalyzer(VADAnalyzer): will retry on set_sample_rate/first use. speech_hold_duration: Optional override for AIC VAD speech hold duration (in seconds). - Range: 0.0 to 20x model window length. + Range: 0.0 to 100x model window length. If None, the SDK default (0.05s) is used. minimum_speech_duration: Optional override for minimum speech duration before VAD reports From 4bce58f27046b06bc02912d55cd6105b588cf0bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Mon, 26 Jan 2026 10:15:15 +0100 Subject: [PATCH 47/50] update changelog and remove outdated dependency notes --- changelog/3408.added.md | 1 + changelog/3408.changed.md | 2 +- src/pipecat/audio/filters/aic_filter.py | 3 --- src/pipecat/audio/vad/aic_vad.py | 3 --- 4 files changed, 2 insertions(+), 7 deletions(-) diff --git a/changelog/3408.added.md b/changelog/3408.added.md index ea7c620e0..fb528c7a7 100644 --- a/changelog/3408.added.md +++ b/changelog/3408.added.md @@ -1,2 +1,3 @@ - Added model downloading support to `AICFilter` with `model_id` and `model_download_dir` parameters. - Added `model_path` parameter to `AICFilter` for loading local .aicmodel files. +- Added unit tests for `AICFilter` and `AICVADAnalyzer`. diff --git a/changelog/3408.changed.md b/changelog/3408.changed.md index aeb563ea2..9436b6074 100644 --- a/changelog/3408.changed.md +++ b/changelog/3408.changed.md @@ -1 +1 @@ -- Updated `AICFilter` and `AICVADAnalyzer` to use aic-sdk ~= 2.0.0. +- Updated `AICFilter` and `AICVADAnalyzer` to use aic-sdk ~= 2.0.1. diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index cd0841d40..cdb77b79e 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -38,9 +38,6 @@ class AICFilter(BaseAudioFilter): Buffers incoming audio to the model's preferred block size and processes frames using float32 samples normalized to the range -1 to +1. - - .. note:: - This class requires aic-sdk ~= 2.0.0 (uses 'aic_sdk' module). """ def __init__( diff --git a/src/pipecat/audio/vad/aic_vad.py b/src/pipecat/audio/vad/aic_vad.py index dd371636d..3cdd080bf 100644 --- a/src/pipecat/audio/vad/aic_vad.py +++ b/src/pipecat/audio/vad/aic_vad.py @@ -41,9 +41,6 @@ class AICVADAnalyzer(VADAnalyzer): Range: 1.0 to 15.0 Formula: Energy threshold = 10 ** (-sensitivity) Default (SDK): 6.0 - - .. note:: - This class requires aic-sdk ~= 2.0.0 (uses 'aic_sdk' module). """ def __init__( From 81a53c699c32a00700f63a4a92602c96c0777fee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Tue, 27 Jan 2026 11:28:05 +0100 Subject: [PATCH 48/50] handle AIC processor init errors gracefully and ensure `_aic_ready` reflects readiness --- src/pipecat/audio/filters/aic_filter.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index 8d198ca1c..7f0626776 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -189,10 +189,15 @@ class AICFilter(BaseAudioFilter): # Create async processor try: self._processor = ProcessorAsync(self._model, self._license_key, config) - self._aic_ready = True except Exception as e: # noqa: BLE001 - surfacing SDK initialization errors logger.error(f"AIC model initialization failed: {e}") - self._aic_ready = False + self._processor = None + + self._aic_ready = self._processor is not None + + if not self._aic_ready: + logger.debug(f"ai-coustics filter is not ready.") + return # Get contexts for parameter control and VAD self._processor_ctx = self._processor.get_processor_context() From 45b7ec4e2c7ae9da457b275550398fffddff91e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Tue, 27 Jan 2026 16:18:56 +0100 Subject: [PATCH 49/50] re-enable `07zd-interruptible-aicoustics.py` in release evals. --- scripts/evals/run-release-evals.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/evals/run-release-evals.py b/scripts/evals/run-release-evals.py index e544c732e..d4290be00 100644 --- a/scripts/evals/run-release-evals.py +++ b/scripts/evals/run-release-evals.py @@ -133,8 +133,7 @@ TESTS_07 = [ ("07zb-interruptible-inworld-http.py", EVAL_SIMPLE_MATH), ("07zc-interruptible-asyncai.py", EVAL_SIMPLE_MATH), ("07zc-interruptible-asyncai-http.py", EVAL_SIMPLE_MATH), - # Need license key to run - # ("07zd-interruptible-aicoustics.py", EVAL_SIMPLE_MATH), + ("07zd-interruptible-aicoustics.py", EVAL_SIMPLE_MATH), ("07ze-interruptible-hume.py", EVAL_SIMPLE_MATH), ("07zf-interruptible-gradium.py", EVAL_SIMPLE_MATH), ("07zg-interruptible-camb.py", EVAL_SIMPLE_MATH), From 6aa77ccc136b9fbc9402a1cac3d39edadcfc72bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Tue, 27 Jan 2026 16:22:54 +0100 Subject: [PATCH 50/50] group aic related changes in changelog. --- changelog/3408.added.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/changelog/3408.added.md b/changelog/3408.added.md index fb528c7a7..04f1311cc 100644 --- a/changelog/3408.added.md +++ b/changelog/3408.added.md @@ -1,3 +1,4 @@ -- Added model downloading support to `AICFilter` with `model_id` and `model_download_dir` parameters. -- Added `model_path` parameter to `AICFilter` for loading local .aicmodel files. -- Added unit tests for `AICFilter` and `AICVADAnalyzer`. +- Additions for `AICFilter` and `AICVADAnalyzer`: + - Added model downloading support to `AICFilter` with `model_id` and `model_download_dir` parameters. + - Added `model_path` parameter to `AICFilter` for loading local `.aicmodel` files. + - Added unit tests for `AICFilter` and `AICVADAnalyzer`.