From f733e774967d4e6e2267b39aa25e3f35d9a0716f Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Mon, 19 Jan 2026 09:13:41 -0500 Subject: [PATCH 01/77] AzureTTS: work around word ordering issue at 8khz sample rate --- src/pipecat/services/azure/tts.py | 38 +++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/pipecat/services/azure/tts.py b/src/pipecat/services/azure/tts.py index 93c421c1e..b9733db07 100644 --- a/src/pipecat/services/azure/tts.py +++ b/src/pipecat/services/azure/tts.py @@ -277,6 +277,11 @@ class AzureTTSService(WordTTSService, AzureBaseTTSService): self._started = False self._first_chunk = True self._cumulative_audio_offset: float = 0.0 # Cumulative audio duration in seconds + self._current_sentence_base_offset: float = 0.0 # Base offset for current sentence + self._current_sentence_duration: float = 0.0 # Duration from Azure callback + self._current_sentence_max_word_offset: float = ( + 0.0 # Max word boundary offset seen in current sentence (for 8kHz workaround) + ) self._last_word: Optional[str] = None # Track last word for punctuation merging self._last_timestamp: Optional[float] = None # Track last timestamp @@ -386,8 +391,14 @@ class AzureTTSService(WordTTSService, AzureBaseTTSService): word = evt.text sentence_relative_seconds = evt.audio_offset / 10_000_000.0 - # Add cumulative offset to get absolute timestamp across sentences - absolute_seconds = self._cumulative_audio_offset + sentence_relative_seconds + # Use base offset captured at start of run_tts to avoid race conditions + # with callbacks from overlapping TTS requests + absolute_seconds = self._current_sentence_base_offset + sentence_relative_seconds + + # Track max word offset for accurate cumulative timing + # (audio_duration from Azure doesn't always match word boundary offsets at 8kHz) + if sentence_relative_seconds > self._current_sentence_max_word_offset: + self._current_sentence_max_word_offset = sentence_relative_seconds if not word: return @@ -492,9 +503,9 @@ class AzureTTSService(WordTTSService, AzureBaseTTSService): self._last_word = None self._last_timestamp = None - # Update cumulative audio offset for next sentence + # Store duration for cumulative offset calculation if evt.result and evt.result.audio_duration: - self._cumulative_audio_offset += evt.result.audio_duration.total_seconds() + self._current_sentence_duration = evt.result.audio_duration.total_seconds() self._audio_queue.put_nowait(None) # Signal completion @@ -530,6 +541,9 @@ class AzureTTSService(WordTTSService, AzureBaseTTSService): self._started = False self._first_chunk = True self._cumulative_audio_offset = 0.0 + self._current_sentence_base_offset = 0.0 + self._current_sentence_duration = 0.0 + self._current_sentence_max_word_offset = 0.0 self._last_word = None self._last_timestamp = None @@ -604,6 +618,12 @@ class AzureTTSService(WordTTSService, AzureBaseTTSService): self._started = True self._first_chunk = True + # Capture base offset BEFORE starting synthesis to avoid race conditions + # Word boundary callbacks will use this value + self._current_sentence_base_offset = self._cumulative_audio_offset + self._current_sentence_duration = 0.0 + self._current_sentence_max_word_offset = 0.0 + ssml = self._construct_ssml(text) self._speech_synthesizer.speak_ssml_async(ssml) await self.start_tts_usage_metrics(text) @@ -627,6 +647,16 @@ class AzureTTSService(WordTTSService, AzureBaseTTSService): ) yield frame + # Update cumulative offset for next sentence + # At 8kHz, Azure's audio_duration doesn't match word boundary offsets, + # so we use max_word_offset as a workaround. At other sample rates, + # audio_duration is accurate. + # TODO: Remove after Azure fixes word boundary timing at 8kHz + if self.sample_rate == 8000: + self._cumulative_audio_offset += self._current_sentence_max_word_offset + else: + self._cumulative_audio_offset += self._current_sentence_duration + except Exception as e: yield ErrorFrame(error=f"Unknown error occurred: {e}") yield TTSStoppedFrame() From 14bd3b1b32e4486a1eadf1d92e290916b20ef867 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Mon, 19 Jan 2026 09:19:57 -0500 Subject: [PATCH 02/77] Set Azure TTS default prosody rate to None --- src/pipecat/services/azure/tts.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/pipecat/services/azure/tts.py b/src/pipecat/services/azure/tts.py index b9733db07..dc0c56e7b 100644 --- a/src/pipecat/services/azure/tts.py +++ b/src/pipecat/services/azure/tts.py @@ -90,7 +90,7 @@ class AzureBaseTTSService: emphasis: Emphasis level for speech ("strong", "moderate", "reduced"). language: Language for synthesis. Defaults to English (US). pitch: Voice pitch adjustment (e.g., "+10%", "-5Hz", "high"). - rate: Speech rate multiplier. Defaults to "1.05". + rate: Speech rate adjustment (e.g., "1.0", "1.25", "slow", "fast"). role: Voice role for expression (e.g., "YoungAdultFemale"). style: Speaking style (e.g., "cheerful", "sad", "excited"). style_degree: Intensity of the speaking style (0.01 to 2.0). @@ -100,7 +100,7 @@ class AzureBaseTTSService: emphasis: Optional[str] = None language: Optional[Language] = Language.EN_US pitch: Optional[str] = None - rate: Optional[str] = "1.05" + rate: Optional[str] = None role: Optional[str] = None style: Optional[str] = None style_degree: Optional[str] = None @@ -185,7 +185,9 @@ class AzureBaseTTSService: if self._settings["volume"]: prosody_attrs.append(f"volume='{self._settings['volume']}'") - ssml += f"" + # Only wrap in prosody tag if there are prosody attributes + if prosody_attrs: + ssml += f"" if self._settings["emphasis"]: ssml += f"" @@ -195,7 +197,8 @@ class AzureBaseTTSService: if self._settings["emphasis"]: ssml += "" - ssml += "" + if prosody_attrs: + ssml += "" if self._settings["style"]: ssml += "" From 0b1a4792b8d2971e8fa97576e35a413e48ceff27 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Mon, 19 Jan 2026 09:51:39 -0500 Subject: [PATCH 03/77] Bump to latest azure-cognitiveservices-speech version, 1.47.0 --- pyproject.toml | 2 +- uv.lock | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e99ab62bc..0cb985f34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ assemblyai = [ "pipecat-ai[websockets-base]" ] asyncai = [ "pipecat-ai[websockets-base]" ] aws = [ "aioboto3~=15.5.0", "pipecat-ai[websockets-base]" ] aws-nova-sonic = [ "aws_sdk_bedrock_runtime~=0.2.0; python_version>='3.12'" ] -azure = [ "azure-cognitiveservices-speech~=1.44.0"] +azure = [ "azure-cognitiveservices-speech~=1.47.0"] cartesia = [ "cartesia~=2.0.3", "pipecat-ai[websockets-base]" ] camb = [ "camb-sdk>=1.5.4" ] cerebras = [] diff --git a/uv.lock b/uv.lock index 45c1f892f..0ca04667a 100644 --- a/uv.lock +++ b/uv.lock @@ -531,18 +531,18 @@ wheels = [ [[package]] name = "azure-cognitiveservices-speech" -version = "1.44.0" +version = "1.47.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/0d/0752835f079e8d2cc42bb634f3ccd761c8d6e9d0d46a2d6cf7b3ed8e714c/azure_cognitiveservices_speech-1.44.0-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:78037a147ba72abb57e8c10b693d43a1bb029986fae0918f1f9b7d6342737bfe", size = 7492396, upload-time = "2025-05-19T15:46:11.318Z" }, - { url = "https://files.pythonhosted.org/packages/76/1d/d0ed4ec0f51303a2a532dc845eeb72c7729a3c8639b08050f3c1cd96db79/azure_cognitiveservices_speech-1.44.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2c9b436326cd8dd82dfa88454b7b68359dfc7149e2ac9029f9bcff155ebd5c95", size = 7347577, upload-time = "2025-05-19T15:46:13.644Z" }, - { url = "https://files.pythonhosted.org/packages/89/c8/f0a4ea8bea014b912046f737e429378ceadad68258395454d62acf7f65bb/azure_cognitiveservices_speech-1.44.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:e5f07fc0587067850288c17aebf33d307d2c1ef9e0b2d11d9f44bff2af400568", size = 40977193, upload-time = "2025-05-19T15:46:15.878Z" }, - { url = "https://files.pythonhosted.org/packages/6a/0d/0a0394e8102d6660afeec6b780c451401f6074b1e19f00e90785529e459e/azure_cognitiveservices_speech-1.44.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:3461e22cf04816f69a964d936218d920240f987c0656fdaaf46571529ff0f7e6", size = 40747860, upload-time = "2025-05-19T15:46:19.316Z" }, - { url = "https://files.pythonhosted.org/packages/55/ad/3b7f6eca73040821358ce01f22067446a03d876bfed41cd784291706db4c/azure_cognitiveservices_speech-1.44.0-py3-none-win32.whl", hash = "sha256:a3fe7fd67ba7db281ae490de3d71b5a22648454ec2630eb6a70797f666330586", size = 2164045, upload-time = "2025-05-19T15:46:22.373Z" }, - { url = "https://files.pythonhosted.org/packages/83/ac/f491487d7d0e25ae2929b4f07e7f9b7456feb38e65b36fb605b2c9685b10/azure_cognitiveservices_speech-1.44.0-py3-none-win_amd64.whl", hash = "sha256:77cfb5dd40733b7ccc21edc427e9fb4720997832ea8a1ba460dc94345f3588ae", size = 2422937, upload-time = "2025-05-19T15:46:23.657Z" }, + { url = "https://files.pythonhosted.org/packages/cc/e3/b6a3d1ef4f135f8ef00ed084b9284e65409e9cd52bc96cd0453a5c6637c6/azure_cognitiveservices_speech-1.47.0-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:656577ed01ed4b8cd7c70fab2c921b300181b906f101758a16406bc99b133681", size = 3574346, upload-time = "2025-11-11T21:13:37.717Z" }, + { url = "https://files.pythonhosted.org/packages/82/fa/9cc0c5400e9d433bd98a1239bedf97b34abf410dbc8932a50886ae43e115/azure_cognitiveservices_speech-1.47.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:afd91653ceca482ccea5459eedda1ec9aa95ee07df12a15fc588c42d4f90f0a9", size = 3506219, upload-time = "2025-11-11T21:13:39.702Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d6/b8f55421b8cb40b478f4fb793c52b1bb0ed794263a5475ae2a6490a4cd53/azure_cognitiveservices_speech-1.47.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:577b702ee30d35ecc581e7e2ac23f4387782f93c241d7f8f3c86f72bb883d02d", size = 35399363, upload-time = "2025-11-11T21:13:41.915Z" }, + { url = "https://files.pythonhosted.org/packages/98/91/c36be146824797f57b194128a173baf289a260c2540c86c166f8c7fbebe3/azure_cognitiveservices_speech-1.47.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:ff72c74abe4b4c0f5a527eabf8511a8c0e689d884a95c54a46495b293e302e73", size = 35196906, upload-time = "2025-11-11T21:13:45.31Z" }, + { url = "https://files.pythonhosted.org/packages/fb/19/dd6f08dc623f2b336cc9cd5cf765712df5262fd675583e701922491e455d/azure_cognitiveservices_speech-1.47.0-py3-none-win_amd64.whl", hash = "sha256:ecfce57d66907afe305fb2950cc781ea8f327274facd2db66950e701b6cfd715", size = 2182376, upload-time = "2025-11-11T21:13:47.753Z" }, + { url = "https://files.pythonhosted.org/packages/1b/16/a6d1f7ab7eae21b00da2eee7186a7db9c9a2434e0ef833f071ff686b833f/azure_cognitiveservices_speech-1.47.0-py3-none-win_arm64.whl", hash = "sha256:4351734cf240d11340a057ecb388397e5ecf40e97e4b67a6a990fffe2791b56c", size = 1978493, upload-time = "2025-11-11T21:13:49.445Z" }, ] [[package]] @@ -4504,7 +4504,7 @@ requires-dist = [ { name = "audioop-lts", marker = "python_full_version >= '3.13'", specifier = "~=0.2.1" }, { name = "aws-sdk-bedrock-runtime", marker = "python_full_version >= '3.12' and extra == 'aws-nova-sonic'", specifier = "~=0.2.0" }, { name = "aws-sdk-sagemaker-runtime-http2", marker = "python_full_version >= '3.12' and extra == 'sagemaker'" }, - { name = "azure-cognitiveservices-speech", marker = "extra == 'azure'", specifier = "~=1.44.0" }, + { name = "azure-cognitiveservices-speech", marker = "extra == 'azure'", specifier = "~=1.47.0" }, { name = "camb-sdk", marker = "extra == 'camb'", specifier = ">=1.5.4" }, { name = "cartesia", marker = "extra == 'cartesia'", specifier = "~=2.0.3" }, { name = "coremltools", marker = "extra == 'local-smart-turn'", specifier = ">=8.0" }, From 3b3c7aa8cc9c84f4c632c128137d5f4b30e59516 Mon Sep 17 00:00:00 2001 From: Akhil Date: Thu, 22 Jan 2026 15:37:44 -0600 Subject: [PATCH 04/77] LLMAssistantAggregator: preserve non-ASCII characters in JSON output Add ensure_ascii=False to json.dumps() calls for tool call arguments and function call results to prevent unnecessary unicode escaping. --- src/pipecat/processors/aggregators/llm_response_universal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pipecat/processors/aggregators/llm_response_universal.py b/src/pipecat/processors/aggregators/llm_response_universal.py index 81dbfe844..6690a6e1e 100644 --- a/src/pipecat/processors/aggregators/llm_response_universal.py +++ b/src/pipecat/processors/aggregators/llm_response_universal.py @@ -833,7 +833,7 @@ class LLMAssistantAggregator(LLMContextAggregator): "id": frame.tool_call_id, "function": { "name": frame.function_name, - "arguments": json.dumps(frame.arguments), + "arguments": json.dumps(frame.arguments, ensure_ascii=False), }, "type": "function", } @@ -866,7 +866,7 @@ class LLMAssistantAggregator(LLMContextAggregator): # Update context with the function call result if frame.result: - result = json.dumps(frame.result) + result = json.dumps(frame.result, ensure_ascii=False) self._update_function_call_result(frame.function_name, frame.tool_call_id, result) else: self._update_function_call_result(frame.function_name, frame.tool_call_id, "COMPLETED") From 7921bce4af3044f667c681c443d73b6c598fdc36 Mon Sep 17 00:00:00 2001 From: filipi87 Date: Fri, 23 Jan 2026 16:15:48 -0300 Subject: [PATCH 05/77] Refactoring AudioBufferProcessor to fix audio track synchronization. --- .../audio/audio_buffer_processor.py | 80 +++++++++---------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/src/pipecat/processors/audio/audio_buffer_processor.py b/src/pipecat/processors/audio/audio_buffer_processor.py index 0d3ed76d4..ceadddf95 100644 --- a/src/pipecat/processors/audio/audio_buffer_processor.py +++ b/src/pipecat/processors/audio/audio_buffer_processor.py @@ -11,7 +11,6 @@ of audio from both user input and bot output sources, with support for various a configurations and event-driven processing. """ -import time from typing import Optional from pipecat.audio.utils import create_stream_resampler, interleave_stereo_audio, mix_audio @@ -104,10 +103,6 @@ class AudioBufferProcessor(FrameProcessor): self._user_turn_audio_buffer = bytearray() self._bot_turn_audio_buffer = bytearray() - # Intermittent (non continous user stream variables) - self._last_user_frame_at = 0 - self._last_bot_frame_at = 0 - self._recording = False self._input_resampler = create_stream_resampler() @@ -211,23 +206,31 @@ class AudioBufferProcessor(FrameProcessor): """Process audio frames for recording.""" resampled = None if isinstance(frame, InputAudioRawFrame): - # Add silence if we need to. - silence = self._compute_silence(self._last_user_frame_at) - self._user_audio_buffer.extend(silence) - # Add user audio. resampled = await self._resample_input_audio(frame) - self._user_audio_buffer.extend(resampled) - # Save time of frame so we can compute silence. - self._last_user_frame_at = time.time() + # Ignoring in case we don't have audio + if len(resampled) > 0: + # Sync bot buffer to current user position before adding user audio. + # We sync BEFORE extending to align both buffers at the same starting timestamp. + # For example, user buffer is at 100 bytes, and you receive 20 bytes of new audio + # - Bot buffer sees User is at 100. Bot pads itself to 100. + # - User buffer adds 20. User is now at 120. + # - Outcome: At index 100-120, we have User Audio and (potentially) Bot Audio or silence. They are aligned + # This gives the opportunity to the bot to send audio. + # + # If we synced AFTER, we'd pad the bot buffer with silence for the same + # window we just gave to the user, effectively "overwriting" that time slot + # with silence and causing the bot's audio to flicker or cut out. + self._sync_buffer_to_position(self._bot_audio_buffer, len(self._user_audio_buffer)) + # Add user audio. + self._user_audio_buffer.extend(resampled) elif self._recording and isinstance(frame, OutputAudioRawFrame): - # Add silence if we need to. - silence = self._compute_silence(self._last_bot_frame_at) - self._bot_audio_buffer.extend(silence) - # Add bot audio. resampled = await self._resample_output_audio(frame) - self._bot_audio_buffer.extend(resampled) - # Save time of frame so we can compute silence. - self._last_bot_frame_at = time.time() + # Ignoring in case we don't have audio + if len(resampled) > 0: + # Sync user buffer to current bot position before adding bot audio + self._sync_buffer_to_position(self._user_audio_buffer, len(self._bot_audio_buffer)) + # Add bot audio. + self._bot_audio_buffer.extend(resampled) if self._buffer_size > 0 and ( len(self._user_audio_buffer) >= self._buffer_size @@ -240,6 +243,21 @@ class AudioBufferProcessor(FrameProcessor): if self._enable_turn_audio: await self._process_turn_recording(frame, resampled) + def _sync_buffer_to_position(self, buffer: bytearray, target_position: int): + """Pad buffer with silence if it's behind the target position. + + This ensures both buffers stay synchronized by padding the lagging + buffer before new audio is added to the other buffer. + + Args: + buffer: The buffer to potentially pad. + target_position: The position (in bytes) the buffer should reach. + """ + current_len = len(buffer) + if current_len < target_position: + silence_needed = target_position - current_len + buffer.extend(b"\x00" * silence_needed) + async def _process_turn_recording(self, frame: Frame, resampled_audio: Optional[bytes] = None): """Process frames for turn-based audio recording.""" if isinstance(frame, UserStartedSpeakingFrame): @@ -281,8 +299,8 @@ class AudioBufferProcessor(FrameProcessor): if len(self._user_audio_buffer) == 0 and len(self._bot_audio_buffer) == 0: return + # Final alignment before we send the audio self._align_track_buffers() - flush_time = time.time() # Call original handler with merged audio merged_audio = self.merge_audio_buffers() @@ -299,9 +317,6 @@ class AudioBufferProcessor(FrameProcessor): self._num_channels, ) - self._last_user_frame_at = flush_time - self._last_bot_frame_at = flush_time - def _buffer_has_audio(self, buffer: bytearray) -> bool: """Check if a buffer contains audio data.""" return buffer is not None and len(buffer) > 0 @@ -309,8 +324,6 @@ class AudioBufferProcessor(FrameProcessor): def _reset_recording(self): """Reset recording state and buffers.""" self._reset_all_audio_buffers() - self._last_user_frame_at = time.time() - self._last_bot_frame_at = time.time() def _reset_all_audio_buffers(self): """Reset all audio buffers to empty state.""" @@ -336,11 +349,9 @@ class AudioBufferProcessor(FrameProcessor): target_len = max(user_len, bot_len) if user_len < target_len: - self._user_audio_buffer.extend(b"\x00" * (target_len - user_len)) - self._last_user_frame_at = max(self._last_user_frame_at, self._last_bot_frame_at) + self._sync_buffer_to_position(self._user_audio_buffer, target_len) if bot_len < target_len: - self._bot_audio_buffer.extend(b"\x00" * (target_len - bot_len)) - self._last_bot_frame_at = max(self._last_bot_frame_at, self._last_user_frame_at) + self._sync_buffer_to_position(self._bot_audio_buffer, target_len) async def _resample_input_audio(self, frame: InputAudioRawFrame) -> bytes: """Resample audio frame to the target sample rate.""" @@ -353,14 +364,3 @@ class AudioBufferProcessor(FrameProcessor): return await self._output_resampler.resample( frame.audio, frame.sample_rate, self._sample_rate ) - - def _compute_silence(self, from_time: float) -> bytes: - """Compute silence to insert based on time gap.""" - quiet_time = time.time() - from_time - # We should get audio frames very frequently. We introduce silence only - # if there's a big enough gap of 1s. - if from_time == 0 or quiet_time < 1.0: - return b"" - num_bytes = int(quiet_time * self._sample_rate) * 2 - silence = b"\x00" * num_bytes - return silence From f128cdd19a207148db1269569f030cb724bfd073 Mon Sep 17 00:00:00 2001 From: filipi87 Date: Fri, 23 Jan 2026 16:16:01 -0300 Subject: [PATCH 06/77] Adding a changelog entry to the AudioBufferProcessor fix. --- changelog/3541.fixed.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/3541.fixed.md diff --git a/changelog/3541.fixed.md b/changelog/3541.fixed.md new file mode 100644 index 000000000..ac5b59529 --- /dev/null +++ b/changelog/3541.fixed.md @@ -0,0 +1 @@ +- Fixed how audio tracks are synchronized inside the `AudioBufferProcessor` to fix timing issues where silence and audio were misaligned between user and bot buffers. From e93112e76e3ff89bbc559eceb7add64ad2436ab6 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Sat, 24 Jan 2026 14:54:58 -0500 Subject: [PATCH 07/77] Simplify STT finalize handling --- src/pipecat/services/cartesia/stt.py | 3 --- src/pipecat/services/elevenlabs/stt.py | 4 ---- src/pipecat/services/sarvam/stt.py | 2 -- src/pipecat/services/stt_service.py | 27 ++++++++------------------ 4 files changed, 8 insertions(+), 28 deletions(-) diff --git a/src/pipecat/services/cartesia/stt.py b/src/pipecat/services/cartesia/stt.py index 2fb08b981..c0bfb3e8d 100644 --- a/src/pipecat/services/cartesia/stt.py +++ b/src/pipecat/services/cartesia/stt.py @@ -221,13 +221,10 @@ class CartesiaSTTService(WebsocketSTTService): await super().process_frame(frame, direction) if isinstance(frame, VADUserStartedSpeakingFrame): - # Reset finalize state for new utterance - self.set_finalize_pending(False) await self._start_metrics() elif isinstance(frame, VADUserStoppedSpeakingFrame): # Send finalize command to flush the transcription session if self._websocket and self._websocket.state is State.OPEN: - self.set_finalize_pending(True) await self._websocket.send("finalize") async def run_stt(self, audio: bytes) -> AsyncGenerator[Frame, None]: diff --git a/src/pipecat/services/elevenlabs/stt.py b/src/pipecat/services/elevenlabs/stt.py index a1be56a0d..469d2c4fa 100644 --- a/src/pipecat/services/elevenlabs/stt.py +++ b/src/pipecat/services/elevenlabs/stt.py @@ -551,8 +551,6 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService): await super().process_frame(frame, direction) if isinstance(frame, VADUserStartedSpeakingFrame): - # Reset finalize state for new utterance - self.set_finalize_pending(False) # Start metrics when user starts speaking await self._start_metrics() elif isinstance(frame, VADUserStoppedSpeakingFrame): @@ -560,8 +558,6 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService): if self._params.commit_strategy == CommitStrategy.MANUAL: if self._websocket and self._websocket.state is State.OPEN: try: - # Mark that the next committed transcript should be finalized - self.set_finalize_pending(True) commit_message = { "message_type": "input_audio_chunk", "audio_base_64": "", diff --git a/src/pipecat/services/sarvam/stt.py b/src/pipecat/services/sarvam/stt.py index 96447dce9..164ad289e 100644 --- a/src/pipecat/services/sarvam/stt.py +++ b/src/pipecat/services/sarvam/stt.py @@ -193,11 +193,9 @@ class SarvamSTTService(STTService): # Only handle VAD frames when not using Sarvam's VAD signals if not self._vad_signals: if isinstance(frame, VADUserStartedSpeakingFrame): - self.set_finalize_pending(False) await self._start_metrics() elif isinstance(frame, VADUserStoppedSpeakingFrame): if self._socket_client: - self.set_finalize_pending(True) await self._socket_client.flush() async def set_language(self, language: Language): diff --git a/src/pipecat/services/stt_service.py b/src/pipecat/services/stt_service.py index 24b1aafcf..44c7af3b4 100644 --- a/src/pipecat/services/stt_service.py +++ b/src/pipecat/services/stt_service.py @@ -119,28 +119,15 @@ class STTService(AIService): """ return self._muted - def set_finalize_pending(self, value: bool): - """Set whether the next TranscriptionFrame should be marked as finalized. - - When True, the next TranscriptionFrame pushed will have its `finalized` - field set to True, and this flag will automatically reset to False. - This is used to signal that a transcript is the final result for an - utterance, enabling immediate TTFB reporting. - - Args: - value: True to mark the next transcription as finalized. - """ - self._finalize_pending = value - def request_finalize(self): """Mark that a finalize request has been sent, awaiting server confirmation. - For providers that require server confirmation before marking transcripts - as finalized (e.g., Deepgram's from_finalize field), call this when sending - the finalize request. Then call confirm_finalize() when the server confirms. + For providers that have explicit server confirmation of finalization + (e.g., Deepgram's from_finalize field), call this when sending the finalize + request. Then call confirm_finalize() when the server confirms. - This is an alternative to set_finalize_pending() for providers that need - two-step finalization. + For providers without server confirmation, don't call this method - just + send the finalize/flush/commit command and rely on the TTFB timeout. """ self._finalize_requested = True @@ -298,7 +285,7 @@ class STTService(AIService): """Push a frame downstream, tracking TranscriptionFrame timestamps for TTFB. Stores the timestamp of each TranscriptionFrame for TTFB calculation. - If the frame is marked as finalized (either directly or via set_finalize_pending), + If the frame is marked as finalized (via request_finalize/confirm_finalize), reports TTFB immediately and cancels any pending timeout. Otherwise, TTFB is reported after a timeout. @@ -361,6 +348,7 @@ class STTService(AIService): """Handle VAD user started speaking frame to start tracking transcriptions. Cancels any pending TTFB timeout, resets TTFB tracking state, and marks user as speaking. + Also resets finalization state to prevent stale finalization from a previous utterance. Args: frame: The VAD user started speaking frame. @@ -368,6 +356,7 @@ class STTService(AIService): await self._reset_stt_ttfb_state() self._user_speaking = True self._finalize_requested = False + self._finalize_pending = False async def _handle_vad_user_stopped_speaking(self, frame: VADUserStoppedSpeakingFrame): """Handle VAD user stopped speaking frame. From a4acc12f91d6ece7c3ec608973a311609d1d393f Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Sat, 24 Jan 2026 18:26:34 -0500 Subject: [PATCH 08/77] Align Speechmatics STT TTFB metrics with STT classes --- src/pipecat/services/speechmatics/stt.py | 26 ------------------------ 1 file changed, 26 deletions(-) diff --git a/src/pipecat/services/speechmatics/stt.py b/src/pipecat/services/speechmatics/stt.py index 457388c31..d12b2a7d3 100644 --- a/src/pipecat/services/speechmatics/stt.py +++ b/src/pipecat/services/speechmatics/stt.py @@ -8,7 +8,6 @@ import asyncio import os -import time from enum import Enum from typing import Any, AsyncGenerator @@ -598,9 +597,6 @@ class SpeechmaticsSTTService(STTService): if segments: await self._send_frames(segments) - # Update metrics - await self._emit_metrics(message.get("metadata", {}).get("processing_time", 0.0)) - async def _handle_segment(self, message: dict[str, Any]) -> None: """Handle AddSegment events. @@ -804,28 +800,6 @@ class SpeechmaticsSTTService(STTService): yield ErrorFrame(f"Speechmatics error: {e}") await self._disconnect() - async def _emit_metrics(self, processing_time: float) -> None: - """Create TTFB metrics. - - The TTFB is the seconds between the person speaking and the STT - engine emitting the first partial. This is only calculated at the - start of an utterance. - """ - # Skip if metrics not available - if not self._metrics or processing_time == 0.0: - return - - # Calculate time as time.time() - ttfb (which is seconds) - start_time = time.time() - processing_time - - # Update internal metrics - self._metrics._start_ttfb_time = start_time - self._metrics._start_processing_time = start_time - - # Stop TTFB metrics - await self.stop_ttfb_metrics() - await self.stop_processing_metrics() - # ============================================================================ # HELPERS # ============================================================================ From f94a60f38198b7ac99a9f5619562dd8ad02a361e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Sun, 25 Jan 2026 15:42:09 -0800 Subject: [PATCH 09/77] transports: fix broadcast_frame_class reference --- src/pipecat/transports/daily/transport.py | 2 +- src/pipecat/transports/smallwebrtc/transport.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pipecat/transports/daily/transport.py b/src/pipecat/transports/daily/transport.py index fe106ebeb..20a0be29f 100644 --- a/src/pipecat/transports/daily/transport.py +++ b/src/pipecat/transports/daily/transport.py @@ -1733,7 +1733,7 @@ class DailyInputTransport(BaseInputTransport): message: The message data to send. sender: ID of the message sender. """ - await self.broadcast_frame_class( + await self.broadcast_frame( DailyInputTransportMessageFrame, message=message, participant_id=sender ) diff --git a/src/pipecat/transports/smallwebrtc/transport.py b/src/pipecat/transports/smallwebrtc/transport.py index bcc3c8b79..7e76771c4 100644 --- a/src/pipecat/transports/smallwebrtc/transport.py +++ b/src/pipecat/transports/smallwebrtc/transport.py @@ -698,7 +698,7 @@ class SmallWebRTCInputTransport(BaseInputTransport): message: The application message to process. """ logger.debug(f"Received app message inside SmallWebRTCInputTransport {message}") - await self.broadcast_frame_class(InputTransportMessageFrame, message=message) + await self.broadcast_frame(InputTransportMessageFrame, message=message) # Add this method similar to DailyInputTransport.request_participant_image async def request_participant_image(self, frame: UserImageRequestFrame): 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 10/77] 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 11/77] 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 12/77] 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 13/77] 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 14/77] 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 15/77] 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 16/77] 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 17/77] 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 18/77] 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 19/77] 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 20/77] 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 21/77] 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 22/77] 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 23/77] 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 24/77] 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 25/77] 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 26/77] 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 27/77] 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 28/77] 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 29/77] 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 30/77] 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 31/77] 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 32/77] 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 33/77] 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 34/77] 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 35/77] 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 36/77] 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 37/77] 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 38/77] 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 39/77] 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 40/77] 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 41/77] 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 42/77] 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 43/77] 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 44/77] 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 45/77] 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 46/77] 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 47/77] 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 48/77] 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 49/77] 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 50/77] 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 51/77] 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 52/77] 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 53/77] 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 54/77] 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 55/77] 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 56/77] 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 960d0faea5f39ff2d2d0ede3f6d28950bb783ba0 Mon Sep 17 00:00:00 2001 From: Sam Sykes Date: Mon, 26 Jan 2026 15:48:04 +0000 Subject: [PATCH 57/77] support `is_eou` for final segment in utterance --- src/pipecat/services/speechmatics/stt.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pipecat/services/speechmatics/stt.py b/src/pipecat/services/speechmatics/stt.py index 457388c31..8d45b4e9d 100644 --- a/src/pipecat/services/speechmatics/stt.py +++ b/src/pipecat/services/speechmatics/stt.py @@ -738,9 +738,16 @@ class SpeechmaticsSTTService(STTService): # If final, then re-parse into TranscriptionFrame if finalized: - frames += [TranscriptionFrame(**attr_from_segment(segment)) for segment in segments] + frames += [ + TranscriptionFrame( + **attr_from_segment(segment), finalized=segment.get("is_eou", False) + ) + for segment in segments + ] finalized_text = "|".join([s["text"] for s in segments]) - await self._handle_transcription(finalized_text, True, segments[0]["language"]) + await self._handle_transcription( + finalized_text, is_final=True, language=segments[0]["language"] + ) logger.debug(f"{self} finalized transcript: {[f.text for f in frames]}") # Return as interim results (unformatted) From 8b88280bb10a5bc1fb22e00fe39f358669df25b2 Mon Sep 17 00:00:00 2001 From: Sam Sykes Date: Mon, 26 Jan 2026 15:52:42 +0000 Subject: [PATCH 58/77] Default to using `EXTERNAL` mode. --- src/pipecat/services/speechmatics/stt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pipecat/services/speechmatics/stt.py b/src/pipecat/services/speechmatics/stt.py index 8d45b4e9d..ed74c50d4 100644 --- a/src/pipecat/services/speechmatics/stt.py +++ b/src/pipecat/services/speechmatics/stt.py @@ -67,7 +67,7 @@ class TurnDetectionMode(str, Enum): """Endpoint and turn detection handling mode. How the STT engine handles the endpointing of speech. If using Pipecat's built-in endpointing, - then use `TurnDetectionMode.FIXED` (default). + then use `TurnDetectionMode.EXTERNAL` (default). To use the STT engine's built-in endpointing, then use `TurnDetectionMode.ADAPTIVE` for simple voice activity detection or `TurnDetectionMode.SMART_TURN` for more advanced ML-based @@ -107,7 +107,7 @@ class SpeechmaticsSTTService(STTService): turn_detection_mode: Endpoint handling, one of `TurnDetectionMode.FIXED`, `TurnDetectionMode.EXTERNAL`, `TurnDetectionMode.ADAPTIVE` and - `TurnDetectionMode.SMART_TURN`. Defaults to `TurnDetectionMode.FIXED`. + `TurnDetectionMode.SMART_TURN`. Defaults to `TurnDetectionMode.EXTERNAL`. speaker_active_format: Formatter for active speaker ID. This formatter is used to format the text output for individual speakers and ensures that the context is clear for @@ -208,7 +208,7 @@ class SpeechmaticsSTTService(STTService): language: Language | str = Language.EN # Endpointing mode - turn_detection_mode: TurnDetectionMode = TurnDetectionMode.FIXED + turn_detection_mode: TurnDetectionMode = TurnDetectionMode.EXTERNAL # Output formatting speaker_active_format: str | None = None From 0c69ae63710d426b3d372dd7a6b5868c0c689ae5 Mon Sep 17 00:00:00 2001 From: Sam Sykes Date: Mon, 26 Jan 2026 16:07:59 +0000 Subject: [PATCH 59/77] Changelog entry. --- changelog/3562.changed.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/3562.changed.md diff --git a/changelog/3562.changed.md b/changelog/3562.changed.md new file mode 100644 index 000000000..491e466ac --- /dev/null +++ b/changelog/3562.changed.md @@ -0,0 +1 @@ +- Added support for TTFS `finalize` attribute in `SpeechmaticsSTTServce` and set the default mode to `EXTERNAL` to support Pipecat-controlled VAD. From ea94939add121c54039c1cba07314a686a959a78 Mon Sep 17 00:00:00 2001 From: Sam Sykes Date: Mon, 26 Jan 2026 16:24:56 +0000 Subject: [PATCH 60/77] update dependency --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e99ab62bc..e2668d65d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,7 +109,7 @@ silero = [ "onnxruntime>=1.20.1,<2" ] simli = [ "simli-ai~=1.0.3"] soniox = [ "pipecat-ai[websockets-base]" ] soundfile = [ "soundfile~=0.13.1" ] -speechmatics = [ "speechmatics-voice[smart]>=0.2.6" ] +speechmatics = [ "speechmatics-voice[smart]>=0.2.8" ] strands = [ "strands-agents>=1.9.1,<2" ] tavus=[] together = [] From fc1444c9d628fbcbc492bde1e4642c666ba926b4 Mon Sep 17 00:00:00 2001 From: Sam Sykes Date: Mon, 26 Jan 2026 16:25:37 +0000 Subject: [PATCH 61/77] Updated changelog --- changelog/3562.changed.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog/3562.changed.md b/changelog/3562.changed.md index 491e466ac..09090672b 100644 --- a/changelog/3562.changed.md +++ b/changelog/3562.changed.md @@ -1 +1,2 @@ - Added support for TTFS `finalize` attribute in `SpeechmaticsSTTServce` and set the default mode to `EXTERNAL` to support Pipecat-controlled VAD. +- Changed dependency to `speechmatics-voice[smart]>=0.2.8` From 9c6b11cecfd45fef91181c6a74f786db67eed14e Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Mon, 26 Jan 2026 13:03:39 -0500 Subject: [PATCH 62/77] Update README links to use absolute URLs --- examples/foundational/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/foundational/README.md b/examples/foundational/README.md index bc25d42ca..5800efff3 100644 --- a/examples/foundational/README.md +++ b/examples/foundational/README.md @@ -4,7 +4,7 @@ This directory contains examples showing how to build voice and multimodal agent ## Setup -1. Follow the [README](../../README.md#%EF%B8%8F-contributing-to-the-framework) steps to get your local environment configured. +1. Follow the [README](https://github.com/pipecat-ai/pipecat/blob/main/README.md#%EF%B8%8F-contributing-to-the-framework) steps to get your local environment configured. > **Run from root directory**: Make sure you are running the steps from the root directory. @@ -140,4 +140,4 @@ uv run python --host 0.0.0.0 --port 8080 - **Connection errors**: Verify API keys in `.env` file - **Port conflicts**: Use `--port` to change the port -For more examples, visit our the [`pipecat-examples repository](https://github.com/pipecat-ai/pipecat-examples). +For more examples, visit our the [pipecat-examples repository](https://github.com/pipecat-ai/pipecat-examples). From 2f23f2e39cf1469ac0fa2f85a1a1fd3eb19ffb4f Mon Sep 17 00:00:00 2001 From: filipi87 Date: Mon, 26 Jan 2026 17:08:27 -0300 Subject: [PATCH 63/77] Fixed race condition in OpenAIRealtimeBetaLLMService that could cause an error when truncating the conversation. --- src/pipecat/services/openai_realtime_beta/openai.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/pipecat/services/openai_realtime_beta/openai.py b/src/pipecat/services/openai_realtime_beta/openai.py index 128bcc93a..1199d8556 100644 --- a/src/pipecat/services/openai_realtime_beta/openai.py +++ b/src/pipecat/services/openai_realtime_beta/openai.py @@ -525,6 +525,14 @@ class OpenAIRealtimeBetaLLMService(LLMService): # note: ttfb is faster by 1/2 RTT than ttfb as measured for other services, since we're getting # this event from the server await self.stop_ttfb_metrics() + + if self._current_audio_response and self._current_audio_response.item_id != evt.item_id: + logger.warning( + f"Received a new audio delta for an already completed audio response before receiving the BotStoppedSpeakingFrame." + ) + logger.debug("Forcing previous audio response to None") + self._current_audio_response = None + if not self._current_audio_response: self._current_audio_response = CurrentAudioResponse( item_id=evt.item_id, From 98fcfd7c91d6380436d4bacd89ed8c51922b343f Mon Sep 17 00:00:00 2001 From: filipi87 Date: Mon, 26 Jan 2026 17:19:08 -0300 Subject: [PATCH 64/77] Adding changelog entry for the OpenAiRealtimeBetaLLMService fix. --- changelog/3567.fixed.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/3567.fixed.md diff --git a/changelog/3567.fixed.md b/changelog/3567.fixed.md new file mode 100644 index 000000000..92ecabeca --- /dev/null +++ b/changelog/3567.fixed.md @@ -0,0 +1 @@ +- Fixed race condition in `OpenAIRealtimeBetaLLMService` that could cause an error when truncating the conversation. From 15d5d1159e0e771178a1f6eca9732db2e91b620a Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Fri, 23 Jan 2026 14:55:53 -0500 Subject: [PATCH 65/77] Added a changelog fragment for PR 3406 --- changelog/3406.fixed.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/3406.fixed.md diff --git a/changelog/3406.fixed.md b/changelog/3406.fixed.md new file mode 100644 index 000000000..b4eae4548 --- /dev/null +++ b/changelog/3406.fixed.md @@ -0,0 +1 @@ +- Fixed an issue where if you were using `OpenRouterLLMService` with a Gemini model, it wouldn't handle multiple `"system"` messages as expected (and as we do in `GoogleLLMService`), which is to convert subsequent ones into `"user"` messages. Instead, the latest `"system"` message would overwrite the previous ones. From febd52274d1d7d776c5ab6bc4ca27c4c06b07139 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Mon, 26 Jan 2026 16:42:00 -0500 Subject: [PATCH 66/77] Add changelog fragment for PR 3536 --- changelog/3536.fixed.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/3536.fixed.md diff --git a/changelog/3536.fixed.md b/changelog/3536.fixed.md new file mode 100644 index 000000000..f70bd62b6 --- /dev/null +++ b/changelog/3536.fixed.md @@ -0,0 +1 @@ +- Fixed a logging issue where non-ASCII characters (e.g., Japanese, Chinese, etc.) were being unnecessarily escaped to Unicode sequences when function call occurred. From 3a71865cf4c250b4bbf5c9bc8b3c4e618cfaadeb Mon Sep 17 00:00:00 2001 From: Sam Sykes Date: Mon, 26 Jan 2026 23:11:25 +0000 Subject: [PATCH 67/77] removed old metrics --- src/pipecat/services/speechmatics/stt.py | 47 ++++++++---------------- 1 file changed, 16 insertions(+), 31 deletions(-) diff --git a/src/pipecat/services/speechmatics/stt.py b/src/pipecat/services/speechmatics/stt.py index ed74c50d4..d02b4360b 100644 --- a/src/pipecat/services/speechmatics/stt.py +++ b/src/pipecat/services/speechmatics/stt.py @@ -598,9 +598,6 @@ class SpeechmaticsSTTService(STTService): if segments: await self._send_frames(segments) - # Update metrics - await self._emit_metrics(message.get("metadata", {}).get("processing_time", 0.0)) - async def _handle_segment(self, message: dict[str, Any]) -> None: """Handle AddSegment events. @@ -738,23 +735,33 @@ class SpeechmaticsSTTService(STTService): # If final, then re-parse into TranscriptionFrame if finalized: - frames += [ - TranscriptionFrame( - **attr_from_segment(segment), finalized=segment.get("is_eou", False) - ) - for segment in segments - ] + # Do any segments have `is_eou` set to True? + if ( + any(segment.get("is_eou", False) for segment in segments) + and self._finalize_requested + ): + self.confirm_finalize() + + # Add the finalized frames + frames += [TranscriptionFrame(**attr_from_segment(segment)) for segment in segments] + + # Handle the text (for metrics reporting) finalized_text = "|".join([s["text"] for s in segments]) await self._handle_transcription( finalized_text, is_final=True, language=segments[0]["language"] ) + + # Log the frames logger.debug(f"{self} finalized transcript: {[f.text for f in frames]}") # Return as interim results (unformatted) else: + # Add the interim frames frames += [ InterimTranscriptionFrame(**attr_from_segment(segment)) for segment in segments ] + + # Log the frames logger.debug(f"{self} interim transcript: {[f.text for f in frames]}") # Send the frames @@ -811,28 +818,6 @@ class SpeechmaticsSTTService(STTService): yield ErrorFrame(f"Speechmatics error: {e}") await self._disconnect() - async def _emit_metrics(self, processing_time: float) -> None: - """Create TTFB metrics. - - The TTFB is the seconds between the person speaking and the STT - engine emitting the first partial. This is only calculated at the - start of an utterance. - """ - # Skip if metrics not available - if not self._metrics or processing_time == 0.0: - return - - # Calculate time as time.time() - ttfb (which is seconds) - start_time = time.time() - processing_time - - # Update internal metrics - self._metrics._start_ttfb_time = start_time - self._metrics._start_processing_time = start_time - - # Stop TTFB metrics - await self.stop_ttfb_metrics() - await self.stop_processing_metrics() - # ============================================================================ # HELPERS # ============================================================================ From 99242c0a937762fd605939d350b604a7eed23f40 Mon Sep 17 00:00:00 2001 From: Sam Sykes Date: Mon, 26 Jan 2026 23:14:40 +0000 Subject: [PATCH 68/77] linting updates --- src/pipecat/services/speechmatics/stt.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pipecat/services/speechmatics/stt.py b/src/pipecat/services/speechmatics/stt.py index d02b4360b..b194e1b5b 100644 --- a/src/pipecat/services/speechmatics/stt.py +++ b/src/pipecat/services/speechmatics/stt.py @@ -201,6 +201,7 @@ class SpeechmaticsSTTService(STTService): extra_params: Extra parameters to pass to the STT engine. This is a dictionary of additional parameters that can be used to configure the STT engine. Default to None. + """ # Service configuration @@ -346,7 +347,7 @@ class SpeechmaticsSTTService(STTService): params.speaker_passive_format or params.speaker_active_format ) - # Metrics + # Model + metrics self.set_model_name(self._config.operating_point.value) # Message queue @@ -693,6 +694,7 @@ class SpeechmaticsSTTService(STTService): ) elif not self._enable_vad and self._client is not None: self._client.finalize() + # self.confirm_finalize() async def _send_frames(self, segments: list[dict[str, Any]], finalized: bool = False) -> None: """Send frames to the pipeline. From 23d7608e5f5895625cb6238db444eb7e164e2b96 Mon Sep 17 00:00:00 2001 From: Sam Sykes Date: Mon, 26 Jan 2026 23:15:30 +0000 Subject: [PATCH 69/77] changelog update --- changelog/3562.changed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/3562.changed.md b/changelog/3562.changed.md index 09090672b..533f37b2f 100644 --- a/changelog/3562.changed.md +++ b/changelog/3562.changed.md @@ -1,2 +1,2 @@ -- Added support for TTFS `finalize` attribute in `SpeechmaticsSTTServce` and set the default mode to `EXTERNAL` to support Pipecat-controlled VAD. +- Added support for TTFS in `SpeechmaticsSTTService` and set the default mode to `EXTERNAL` to support Pipecat-controlled VAD. - Changed dependency to `speechmatics-voice[smart]>=0.2.8` From 60168f7f69ea9388f3ea3917a11f67658570cb17 Mon Sep 17 00:00:00 2001 From: Sam Sykes Date: Mon, 26 Jan 2026 23:16:43 +0000 Subject: [PATCH 70/77] remove comment --- src/pipecat/services/speechmatics/stt.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pipecat/services/speechmatics/stt.py b/src/pipecat/services/speechmatics/stt.py index b194e1b5b..1d25d6af6 100644 --- a/src/pipecat/services/speechmatics/stt.py +++ b/src/pipecat/services/speechmatics/stt.py @@ -694,7 +694,6 @@ class SpeechmaticsSTTService(STTService): ) elif not self._enable_vad and self._client is not None: self._client.finalize() - # self.confirm_finalize() async def _send_frames(self, segments: list[dict[str, Any]], finalized: bool = False) -> None: """Send frames to the pipeline. 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 71/77] 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 91346f5f37655d96e8886af8aa5ee546f49a3e79 Mon Sep 17 00:00:00 2001 From: Sam Sykes Date: Tue, 27 Jan 2026 10:44:35 +0000 Subject: [PATCH 72/77] Add support for `self.request_finalize()` for Pipecat-based VAD. --- src/pipecat/services/speechmatics/stt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pipecat/services/speechmatics/stt.py b/src/pipecat/services/speechmatics/stt.py index 1d25d6af6..752edd98b 100644 --- a/src/pipecat/services/speechmatics/stt.py +++ b/src/pipecat/services/speechmatics/stt.py @@ -693,6 +693,7 @@ class SpeechmaticsSTTService(STTService): f"{self} VADUserStoppedSpeakingFrame received but internal VAD is being used" ) elif not self._enable_vad and self._client is not None: + self.request_finalize() self._client.finalize() async def _send_frames(self, segments: list[dict[str, Any]], finalized: bool = False) -> None: 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 73/77] 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 74/77] 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`. From 52012b0fb2c24bb0388a345f8cd6dad312cfa476 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Tue, 27 Jan 2026 12:33:44 -0500 Subject: [PATCH 75/77] Fix WebsocketService infinite loop on graceful server disconnect --- changelog/3574.fixed.md | 1 + src/pipecat/services/websocket_service.py | 23 +++++++++--- uv.lock | 46 +++++++++++++++++++---- 3 files changed, 57 insertions(+), 13 deletions(-) create mode 100644 changelog/3574.fixed.md diff --git a/changelog/3574.fixed.md b/changelog/3574.fixed.md new file mode 100644 index 000000000..187f172b7 --- /dev/null +++ b/changelog/3574.fixed.md @@ -0,0 +1 @@ +- Fixed an infinite loop in `WebsocketService` that blocked the event loop when a remote server closed the connection gracefully. \ No newline at end of file diff --git a/src/pipecat/services/websocket_service.py b/src/pipecat/services/websocket_service.py index e86dee73f..85e5b2db7 100644 --- a/src/pipecat/services/websocket_service.py +++ b/src/pipecat/services/websocket_service.py @@ -123,26 +123,29 @@ class WebsocketService(ABC): async def _maybe_try_reconnect( self, - error: Exception, error_message: str, report_error: Callable[[ErrorFrame], Awaitable[None]], + error: Optional[Exception] = None, ) -> bool: """Check if reconnection should be attempted and try if appropriate. Args: - error: The exception that occurred. error_message: Human-readable error message for logging. report_error: Callback function to report connection errors. + error: The exception that occurred (optional, may be None for graceful closes). Returns: True if should continue the receive loop, False if should break. """ # Don't reconnect if we're intentionally disconnecting if self._disconnecting: - logger.warning(f"{self} error during disconnect: {error}") + if error: + logger.warning(f"{self} error during disconnect: {error}") + else: + logger.debug(f"{self} receive loop ended during disconnect") return False - # Log the error + # Log the message logger.warning(error_message) # Try to reconnect if enabled @@ -167,6 +170,14 @@ class WebsocketService(ABC): while True: try: await self._receive_messages() + # _receive_messages() returned normally. This happens when the websocket + # closes gracefully (server sent close frame). The async for loop over + # the websocket exits without raising an exception in this case. + # We must handle this to avoid an infinite loop. + message = f"{self} connection closed by server" + should_continue = await self._maybe_try_reconnect(message, report_error) + if not should_continue: + break except ConnectionClosedOK as e: # Normal closure, don't retry logger.debug(f"{self} connection closed normally: {e}") @@ -175,13 +186,13 @@ class WebsocketService(ABC): # Connection closed with error (e.g., no close frame received/sent) # This often indicates network issues, server problems, or abrupt disconnection message = f"{self} connection closed, but with an error: {e}" - should_continue = await self._maybe_try_reconnect(e, message, report_error) + should_continue = await self._maybe_try_reconnect(message, report_error, e) if not should_continue: break except Exception as e: # General error during message receiving message = f"{self} error receiving messages: {e}" - should_continue = await self._maybe_try_reconnect(e, message, report_error) + should_continue = await self._maybe_try_reconnect(message, report_error, e) if not should_continue: break diff --git a/uv.lock b/uv.lock index 0ca04667a..b35c9f25d 100644 --- a/uv.lock +++ b/uv.lock @@ -38,12 +38,44 @@ wheels = [ [[package]] name = "aic-sdk" -version = "1.2.0" +version = "2.0.1" 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/68/c6/1f0b3d3d226c6d19ec654fdaea7859ee9931e0286735385b1f9ea4bcfba1/aic_sdk-2.0.1.tar.gz", hash = "sha256:2480d8398a26639ed7fb5175c37da82cf5e6b1138a1a301938cd8491fe461c20", size = 73091, upload-time = "2026-01-23T23:38:15.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/cf/b2f56f3129b8e393362487b6828a6811cc2f252d438bbf53dc917fd53f23/aic_sdk-2.0.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:583e0b51d236d02396b9d13fce112bb63aa2b6953e42c925af093beea2b82edb", size = 4892239, upload-time = "2026-01-23T23:36:15.832Z" }, + { url = "https://files.pythonhosted.org/packages/92/bc/300366b9a64c97ca40db4d54a0ab8390f4c6860bf6cb5e1e0c55988aca1f/aic_sdk-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ef80b2ef5d1f43ef28e117c7db3503e4877d532e12ebac79dd0c0a1944bc6a0a", size = 4449896, upload-time = "2026-01-23T23:36:20.784Z" }, + { url = "https://files.pythonhosted.org/packages/52/76/57e365ede8d4f88dbdce119ec6d8910d76c5e85e506ee3062a4a1222ea97/aic_sdk-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ee7b00bcf7eb870ef05bdefcb65eaf4894285155d454e85187c49f313978152", size = 3595181, upload-time = "2026-01-23T23:36:25.641Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/f6d92c34469ab54c74cbd59590d2f0f8247d2e576f0f97723e11004708ff/aic_sdk-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb0b74a457f3749a90414304e1291fcb6ffb8019f3c59f39c2f395eabf902b", size = 4111674, upload-time = "2026-01-23T23:36:31.985Z" }, + { url = "https://files.pythonhosted.org/packages/ad/78/2fe743d9194f4a187ca72dd9e24d96c9f3687e11990f2ebb2f900719303e/aic_sdk-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:559307bded02c0b64a00595ec8e5383bb7fe5e9b0865cd9b49e2b15411057f1a", size = 3663836, upload-time = "2026-01-23T23:36:36.322Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e4/2f6bdd665b4d4da43e890f8849daf9661ef36c7304a4c675f3cbf617cb14/aic_sdk-2.0.1-cp310-cp310-win_arm64.whl", hash = "sha256:e4b64f289416779711cd083905abdd80fdb4f8a6802480b958951ded1517c6a5", size = 3275160, upload-time = "2026-01-23T23:36:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/57/6f/2a065d61ed333e46a704f6592b33a88ffd0848b2efa99b039c8e427b21a3/aic_sdk-2.0.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:06aa50a7f014c8b06387cdea6fb37c53c9697490eab98959039aeccc8d51e360", size = 4892089, upload-time = "2026-01-23T23:36:41.249Z" }, + { url = "https://files.pythonhosted.org/packages/de/f7/cd0c82cec01a94d7e121d411780f43cb8e6611bd797a10c02fd02c858f49/aic_sdk-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1f1ade29783354f09f270ce38649dd6aed57c237c1b090b2ddc0fb61bc651d47", size = 4449813, upload-time = "2026-01-23T23:36:43.845Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/d90d39716cf487f0a41fd5bd01670884f9d0901902d6616595ad3ea17464/aic_sdk-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17040ea4d6a686429a214a5673c362890ad10cefb265b6f878a240763e6f39ef", size = 3594996, upload-time = "2026-01-23T23:36:46.334Z" }, + { url = "https://files.pythonhosted.org/packages/21/5d/8852484f85fa60a8ec2e696f6de8363301cd6100b2e5a68289ccc36d02ca/aic_sdk-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f13f9b2211136dd6f46fa2f148a55aabf5b9c3cb40fc0beed9e435b0df60d34c", size = 4111589, upload-time = "2026-01-23T23:36:50.448Z" }, + { url = "https://files.pythonhosted.org/packages/71/ca/22c99be2aca92f77d4f0fe742827cd2db5f0c761797ebe0e5bd43872259a/aic_sdk-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:f4c3556bad0b74f2c0a5c2a253f14ca58b7129ce5b17848b8b0948f68286639d", size = 3663706, upload-time = "2026-01-23T23:36:54.581Z" }, + { url = "https://files.pythonhosted.org/packages/19/fc/fbd7ee793cf15ef3319d12399ee9300c21c09acf654e1d8d1f64f682d750/aic_sdk-2.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:11de01064d028adeb2d2edda4546e86002d5b43710fcdc00a33ee2403a1676d4", size = 3274994, upload-time = "2026-01-23T23:36:58.177Z" }, + { url = "https://files.pythonhosted.org/packages/8b/04/07ed2ae4b4dc9f31522fa971791fd7d7e38feac8ce2b9d3316394b2e5fe5/aic_sdk-2.0.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f48dde209a704a51e65a44c7846c033dc860003467cef0fc2d15d7f8aa137dbc", size = 4893276, upload-time = "2026-01-23T23:37:03.097Z" }, + { url = "https://files.pythonhosted.org/packages/58/87/6328bcf58e633acdf65fd72c4dee61f468fef399c0868e5c446b99166bf5/aic_sdk-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2017ea843fc9e38612a13f1b0a668428a3f6862792baf230ac79a65d9c0633d9", size = 4450341, upload-time = "2026-01-23T23:37:08.648Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/da5138346944ac7dc61ed70e66c1fb2fddef815dc2bab561316db5aef252/aic_sdk-2.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:065954c17116b96408ebfbff29152ca458bb083a9e56178c70adbafdda08218a", size = 3594974, upload-time = "2026-01-23T23:37:13.83Z" }, + { url = "https://files.pythonhosted.org/packages/06/d8/17e1a77820a6848efb7c97751bc6022f65c5ca6436dc3caf3a9da356def1/aic_sdk-2.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa7a196160d6eaf2b856c542bc967c2e08e11a5d93ac4e632f843a01b2872274", size = 4113591, upload-time = "2026-01-23T23:37:19.067Z" }, + { url = "https://files.pythonhosted.org/packages/38/3b/04b70a75364c2ef1717018a81963a8e16bffc3f9f064f125cb111870b6a4/aic_sdk-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:dbd007a683ebff4def95fa5a7ace1602aa2d150fa80761231b044edd57e98bbb", size = 3661883, upload-time = "2026-01-23T23:37:25.242Z" }, + { url = "https://files.pythonhosted.org/packages/2a/bd/64bc3ce090cc110f3721c5e54f97f9fcb67dc50bd8dc6408896650d1d68e/aic_sdk-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:f776c5f0425b39073d4caca2f0bdea036647c4162d4673ef498e1306d41bb39e", size = 3271232, upload-time = "2026-01-23T23:37:30.005Z" }, + { url = "https://files.pythonhosted.org/packages/6e/72/8445a7201aa5969216b5d4ab60bb2ebefa2ac07f557e9ebca27172be2f00/aic_sdk-2.0.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a7ff35422ffb813e8a5b4afed6eb56d4e8abc1ecabf464084d4c7b5b8aff0e43", size = 4892624, upload-time = "2026-01-23T23:37:33.229Z" }, + { url = "https://files.pythonhosted.org/packages/c7/9c/5b060cbd9e9bcea5e62df13cf3e722f4355286e2174c298ffcfe337c680d/aic_sdk-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6cda0b6db664712099da483f803128e4e5256625aed2d85e65e5cc823a0e873b", size = 4449490, upload-time = "2026-01-23T23:37:35.938Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f8/ac61d007dc8d158a8f516327db74b6f3b1cf78b16be43acd29775197533d/aic_sdk-2.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ade8754bd878da0509e70636a9b4eaba0280741db5afabf752102ef605ffaac", size = 3594360, upload-time = "2026-01-23T23:37:38.943Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c4/af0c00055450b060b23e8dd5f3c1a208ea1444c6b497eaf29d3de6e215fa/aic_sdk-2.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b2f44c660aa29613be05c576da25092b3570c8fb3dddc09b70625789d066202", size = 4112325, upload-time = "2026-01-23T23:37:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/86/8e/62a7c53cc1bf2345ea20b554d1d8a61058301cd8088ff94ba95809b04a02/aic_sdk-2.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:ce1656991fc4dbbb40257c72a4b8fc4d4839363ca4b7b25a84ae5a83914ae90c", size = 3661441, upload-time = "2026-01-23T23:37:45.025Z" }, + { url = "https://files.pythonhosted.org/packages/39/98/aa9d6ccba0a1902f8480544fcf468dd3696ecb5392f02c2770f9020e6f9a/aic_sdk-2.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:0138e964feb15d9fb5d2c9c64a8d45a807171900f53351e5525c26869237bd1c", size = 3270753, upload-time = "2026-01-23T23:37:47.882Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3e/6a693ba223e2e55e142983c6243968222070405c6a90ec4c5a61b46652c1/aic_sdk-2.0.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:11eb0c3686ff83f340c875b864840fad19e3a98cd6e59815f83e9248a3ffb397", size = 4893527, upload-time = "2026-01-23T23:37:52.021Z" }, + { url = "https://files.pythonhosted.org/packages/35/9c/f149870d75f28c851de439d4039f85aa590f47499272f932841e4dc0a9a5/aic_sdk-2.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:27844521dc1ae3e1226e7371ec3e68fc4726515f14c5e82a6030f276b612c1a8", size = 4450169, upload-time = "2026-01-23T23:37:55.964Z" }, + { url = "https://files.pythonhosted.org/packages/92/87/8ee4e1763b603ad3d6d535d7ecfa7a2943145bcc18f2db4600279aa37af3/aic_sdk-2.0.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:adf617d4e4e8910764118d1baf1521c752218fd304c75d7e22352d4755cabd50", size = 3595300, upload-time = "2026-01-23T23:37:59.494Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b4/06d6f5c1b45d839d4d8ad4fbcb45dc224e980c69976c48e39d5e32850c51/aic_sdk-2.0.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2bc9071599b9703783b6b100758cd7621b30a64abddc8072f6872932b74c21", size = 4113837, upload-time = "2026-01-23T23:38:03.565Z" }, + { url = "https://files.pythonhosted.org/packages/1e/35/d7a2f7b37183b08b2e8969c3d6c6d1824253cd32894d72f250075edee654/aic_sdk-2.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:9db2bfb4f1ab40a4b130d8d0e158277461b25c7b78bffffba90f816766cb28e9", size = 3663055, upload-time = "2026-01-23T23:38:07.741Z" }, + { url = "https://files.pythonhosted.org/packages/90/97/9ed859e70b1d0c68edc9748c4e69d251e89a3faa462f36ce26c1f8aa7844/aic_sdk-2.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:3c6ed1bfda589970e6c6b96ae29f112baa430ad91e149e76004825870198a5c7", size = 3272737, upload-time = "2026-01-23T23:38:13.966Z" }, +] [[package]] name = "aioboto3" @@ -4495,7 +4527,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 = "~=2.0.1" }, { name = "aioboto3", marker = "extra == 'aws'", specifier = "~=15.5.0" }, { name = "aiofiles", specifier = ">=24.1.0,<25" }, { name = "aiohttp", specifier = ">=3.11.12,<4" }, @@ -4586,7 +4618,7 @@ requires-dist = [ { name = "simli-ai", marker = "extra == 'simli'", specifier = "~=1.0.3" }, { name = "soundfile", marker = "extra == 'soundfile'", specifier = "~=0.13.1" }, { name = "soxr", specifier = "~=0.5.0" }, - { name = "speechmatics-voice", extras = ["smart"], marker = "extra == 'speechmatics'", specifier = ">=0.2.6" }, + { name = "speechmatics-voice", extras = ["smart"], marker = "extra == 'speechmatics'", specifier = ">=0.2.8" }, { name = "strands-agents", marker = "extra == 'strands'", specifier = ">=1.9.1,<2" }, { name = "tenacity", marker = "extra == 'livekit'", specifier = ">=8.2.3,<10.0.0" }, { name = "timm", marker = "extra == 'moondream'", specifier = "~=1.0.13" }, @@ -6420,16 +6452,16 @@ wheels = [ [[package]] name = "speechmatics-voice" -version = "0.2.7" +version = "0.2.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "pydantic" }, { name = "speechmatics-rt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4a/94/47280c7fa5264676bfd6a2373c5cbfa562d5f1aefd77d7f241641a4889a6/speechmatics_voice-0.2.7.tar.gz", hash = "sha256:392b5129d2cbc0059f122fdf960d88dc59df5f26808992ef031f2eb40713c936", size = 61137, upload-time = "2026-01-12T14:21:17.672Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/b2/72b5b2203bbefbd22e7692adaca0dd7c2feebed1aaea5599ec579f74fbbf/speechmatics_voice-0.2.8.tar.gz", hash = "sha256:b2d9cbf773fd94400c744734662e2b16b5bdc4271d0dafde46ac032c438fe000", size = 61419, upload-time = "2026-01-26T16:26:09.082Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/72/e74dbcd42935b31b1d188b8f9d932d9d4078ea5edf303bb0ba0af4203ba2/speechmatics_voice-0.2.7-py3-none-any.whl", hash = "sha256:79c6072a5bf21cfa75770b5e3855cff5747222b024c417a276d0b9c2ae83cd0c", size = 57323, upload-time = "2026-01-12T14:21:16.679Z" }, + { url = "https://files.pythonhosted.org/packages/89/2d/a2ab215a7a31fad5ef9267420dc9ced96d6d52e5b80b131ef41424607849/speechmatics_voice-0.2.8-py3-none-any.whl", hash = "sha256:423ac7620ae8c98f175faace2184ac4ab1fe448ffb41af57aae05ec655326f79", size = 57629, upload-time = "2026-01-26T16:26:07.59Z" }, ] [package.optional-dependencies] From ee695ae9fe8d0dcc2eeca60cba5e7d90323217d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Sun, 25 Jan 2026 15:44:21 -0800 Subject: [PATCH 76/77] FrameSerializer: subclass from BaseObject so we can add events --- changelog/3560.changed.md | 1 + src/pipecat/serializers/base_serializer.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 changelog/3560.changed.md diff --git a/changelog/3560.changed.md b/changelog/3560.changed.md new file mode 100644 index 000000000..b4ba6aa8b --- /dev/null +++ b/changelog/3560.changed.md @@ -0,0 +1 @@ +- `FrameSerializer` now subclasses from `BaseObject` to enable event support. diff --git a/src/pipecat/serializers/base_serializer.py b/src/pipecat/serializers/base_serializer.py index 490951a69..9cc38cdc6 100644 --- a/src/pipecat/serializers/base_serializer.py +++ b/src/pipecat/serializers/base_serializer.py @@ -9,9 +9,10 @@ from abc import ABC, abstractmethod from pipecat.frames.frames import Frame, StartFrame +from pipecat.utils.base_object import BaseObject -class FrameSerializer(ABC): +class FrameSerializer(BaseObject): """Abstract base class for frame serialization implementations. Defines the interface for converting frames to/from serialized formats From e80e0eab297eca30a56ae4b39b837904df670683 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Tue, 27 Jan 2026 12:59:20 -0500 Subject: [PATCH 77/77] Emit on_assistant_turn_stopped and on_user_turn_stopped from EndFrame or CancelFrame --- changelog/3575.fixed.md | 1 + .../aggregators/llm_response_universal.py | 38 ++++++++++--- tests/test_context_aggregators_universal.py | 54 +++++++++++++++++++ uv.lock | 46 +++++++++++++--- 4 files changed, 124 insertions(+), 15 deletions(-) create mode 100644 changelog/3575.fixed.md diff --git a/changelog/3575.fixed.md b/changelog/3575.fixed.md new file mode 100644 index 000000000..03c42b1ad --- /dev/null +++ b/changelog/3575.fixed.md @@ -0,0 +1 @@ +- Fixed `LLMUserAggregator` and `LLMAssistantAggregator` not emitting pending transcripts via `on_user_turn_stopped` and `on_assistant_turn_stopped` events when the conversation ends (`EndFrame`) or is cancelled (`CancelFrame`). diff --git a/src/pipecat/processors/aggregators/llm_response_universal.py b/src/pipecat/processors/aggregators/llm_response_universal.py index 6690a6e1e..3a8bbb542 100644 --- a/src/pipecat/processors/aggregators/llm_response_universal.py +++ b/src/pipecat/processors/aggregators/llm_response_universal.py @@ -464,9 +464,11 @@ class LLMUserAggregator(LLMContextAggregator): await s.setup(self.task_manager) async def _stop(self, frame: EndFrame): + await self._maybe_emit_user_turn_stopped(on_session_end=True) await self._cleanup() async def _cancel(self, frame: CancelFrame): + await self._maybe_emit_user_turn_stopped(on_session_end=True) await self._cleanup() async def _cleanup(self): @@ -602,14 +604,7 @@ class LLMUserAggregator(LLMContextAggregator): if params.enable_user_speaking_frames: await self.broadcast_frame(UserStoppedSpeakingFrame) - # Always push context frame. - aggregation = await self.push_aggregation() - - message = UserTurnStoppedMessage( - content=aggregation, timestamp=self._user_turn_start_timestamp - ) - await self._call_event_handler("on_user_turn_stopped", strategy, message) - self._user_turn_start_timestamp = "" + await self._maybe_emit_user_turn_stopped(strategy) async def _on_user_turn_stop_timeout(self, controller): await self._call_event_handler("on_user_turn_stop_timeout") @@ -617,6 +612,26 @@ class LLMUserAggregator(LLMContextAggregator): async def _on_user_turn_idle(self, controller): await self._call_event_handler("on_user_turn_idle") + async def _maybe_emit_user_turn_stopped( + self, + strategy: Optional[BaseUserTurnStopStrategy] = None, + on_session_end: bool = False, + ): + """Maybe emit user turn stopped event. + + Args: + strategy: The strategy that triggered the turn stop. + on_session_end: If True, only emit if there's unemitted content + (avoids duplicate events when session ends). + """ + aggregation = await self.push_aggregation() + if not on_session_end or aggregation: + message = UserTurnStoppedMessage( + content=aggregation, timestamp=self._user_turn_start_timestamp + ) + await self._call_event_handler("on_user_turn_stopped", strategy, message) + self._user_turn_start_timestamp = "" + class LLMAssistantAggregator(LLMContextAggregator): """Assistant LLM aggregator that processes bot responses and function calls. @@ -739,6 +754,9 @@ class LLMAssistantAggregator(LLMContextAggregator): if isinstance(frame, InterruptionFrame): await self._handle_interruptions(frame) await self.push_frame(frame, direction) + elif isinstance(frame, (EndFrame, CancelFrame)): + await self._handle_end_or_cancel(frame) + await self.push_frame(frame, direction) elif isinstance(frame, LLMFullResponseStartFrame): await self._handle_llm_start(frame) elif isinstance(frame, LLMFullResponseEndFrame): @@ -813,6 +831,10 @@ class LLMAssistantAggregator(LLMContextAggregator): self._started = 0 await self.reset() + async def _handle_end_or_cancel(self, frame: Frame): + await self._trigger_assistant_turn_stopped() + self._started = 0 + async def _handle_function_calls_started(self, frame: FunctionCallsStartedFrame): function_names = [f"{f.function_name}:{f.tool_call_id}" for f in frame.function_calls] logger.debug(f"{self} FunctionCallsStartedFrame: {function_names}") diff --git a/tests/test_context_aggregators_universal.py b/tests/test_context_aggregators_universal.py index 3cb1407f1..63f1fef9a 100644 --- a/tests/test_context_aggregators_universal.py +++ b/tests/test_context_aggregators_universal.py @@ -344,6 +344,35 @@ class TestLLMUserAggregator(unittest.IsolatedAsyncioTestCase): # The user mute strategies should have muted the user. self.assertFalse(user_turn) + async def test_pending_transcription_emitted_on_end_frame(self): + """Pending user transcription should be emitted when EndFrame arrives.""" + context = LLMContext() + + user_aggregator = LLMUserAggregator(context) + + stop_messages = [] + + @user_aggregator.event_handler("on_user_turn_stopped") + async def on_user_turn_stopped(aggregator, strategy, message): + stop_messages.append((strategy, message)) + + pipeline = Pipeline([user_aggregator]) + + # Start turn and send transcription, but don't trigger normal turn stop + frames_to_send = [ + VADUserStartedSpeakingFrame(), + TranscriptionFrame(text="Hello!", user_id="", timestamp="now"), + # No VADUserStoppedSpeakingFrame - turn doesn't stop normally + # EndFrame will be sent by run_test, triggering emission + ] + await run_test(pipeline, frames_to_send=frames_to_send) + + # The pending transcription should be emitted on EndFrame + self.assertEqual(len(stop_messages), 1) + strategy, message = stop_messages[0] + self.assertIsNone(strategy) # strategy is None for end/cancel + self.assertEqual(message.content, "Hello!") + class TestLLMAssistantAggregator(unittest.IsolatedAsyncioTestCase): async def test_empty(self): @@ -512,3 +541,28 @@ class TestLLMAssistantAggregator(unittest.IsolatedAsyncioTestCase): ] await run_test(aggregator, frames_to_send=frames_to_send) self.assertEqual(thought_message.content, "I'm thinking!") + + async def test_pending_text_emitted_on_end_frame(self): + """Pending assistant text should be emitted when EndFrame arrives.""" + context = LLMContext() + + aggregator = LLMAssistantAggregator(context) + + stop_messages = [] + + @aggregator.event_handler("on_assistant_turn_stopped") + async def on_assistant_turn_stopped(aggregator, message: AssistantTurnStoppedMessage): + stop_messages.append(message) + + # Start response and send text, but don't send LLMFullResponseEndFrame + frames_to_send = [ + LLMFullResponseStartFrame(), + LLMTextFrame("Hello from Pipecat!"), + # No LLMFullResponseEndFrame - response doesn't end normally + # EndFrame will be sent by run_test, triggering emission + ] + await run_test(aggregator, frames_to_send=frames_to_send) + + # The pending text should be emitted on EndFrame + self.assertEqual(len(stop_messages), 1) + self.assertEqual(stop_messages[0].content, "Hello from Pipecat!") diff --git a/uv.lock b/uv.lock index 0ca04667a..b35c9f25d 100644 --- a/uv.lock +++ b/uv.lock @@ -38,12 +38,44 @@ wheels = [ [[package]] name = "aic-sdk" -version = "1.2.0" +version = "2.0.1" 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/68/c6/1f0b3d3d226c6d19ec654fdaea7859ee9931e0286735385b1f9ea4bcfba1/aic_sdk-2.0.1.tar.gz", hash = "sha256:2480d8398a26639ed7fb5175c37da82cf5e6b1138a1a301938cd8491fe461c20", size = 73091, upload-time = "2026-01-23T23:38:15.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/cf/b2f56f3129b8e393362487b6828a6811cc2f252d438bbf53dc917fd53f23/aic_sdk-2.0.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:583e0b51d236d02396b9d13fce112bb63aa2b6953e42c925af093beea2b82edb", size = 4892239, upload-time = "2026-01-23T23:36:15.832Z" }, + { url = "https://files.pythonhosted.org/packages/92/bc/300366b9a64c97ca40db4d54a0ab8390f4c6860bf6cb5e1e0c55988aca1f/aic_sdk-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ef80b2ef5d1f43ef28e117c7db3503e4877d532e12ebac79dd0c0a1944bc6a0a", size = 4449896, upload-time = "2026-01-23T23:36:20.784Z" }, + { url = "https://files.pythonhosted.org/packages/52/76/57e365ede8d4f88dbdce119ec6d8910d76c5e85e506ee3062a4a1222ea97/aic_sdk-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ee7b00bcf7eb870ef05bdefcb65eaf4894285155d454e85187c49f313978152", size = 3595181, upload-time = "2026-01-23T23:36:25.641Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/f6d92c34469ab54c74cbd59590d2f0f8247d2e576f0f97723e11004708ff/aic_sdk-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb0b74a457f3749a90414304e1291fcb6ffb8019f3c59f39c2f395eabf902b", size = 4111674, upload-time = "2026-01-23T23:36:31.985Z" }, + { url = "https://files.pythonhosted.org/packages/ad/78/2fe743d9194f4a187ca72dd9e24d96c9f3687e11990f2ebb2f900719303e/aic_sdk-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:559307bded02c0b64a00595ec8e5383bb7fe5e9b0865cd9b49e2b15411057f1a", size = 3663836, upload-time = "2026-01-23T23:36:36.322Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e4/2f6bdd665b4d4da43e890f8849daf9661ef36c7304a4c675f3cbf617cb14/aic_sdk-2.0.1-cp310-cp310-win_arm64.whl", hash = "sha256:e4b64f289416779711cd083905abdd80fdb4f8a6802480b958951ded1517c6a5", size = 3275160, upload-time = "2026-01-23T23:36:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/57/6f/2a065d61ed333e46a704f6592b33a88ffd0848b2efa99b039c8e427b21a3/aic_sdk-2.0.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:06aa50a7f014c8b06387cdea6fb37c53c9697490eab98959039aeccc8d51e360", size = 4892089, upload-time = "2026-01-23T23:36:41.249Z" }, + { url = "https://files.pythonhosted.org/packages/de/f7/cd0c82cec01a94d7e121d411780f43cb8e6611bd797a10c02fd02c858f49/aic_sdk-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1f1ade29783354f09f270ce38649dd6aed57c237c1b090b2ddc0fb61bc651d47", size = 4449813, upload-time = "2026-01-23T23:36:43.845Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/d90d39716cf487f0a41fd5bd01670884f9d0901902d6616595ad3ea17464/aic_sdk-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17040ea4d6a686429a214a5673c362890ad10cefb265b6f878a240763e6f39ef", size = 3594996, upload-time = "2026-01-23T23:36:46.334Z" }, + { url = "https://files.pythonhosted.org/packages/21/5d/8852484f85fa60a8ec2e696f6de8363301cd6100b2e5a68289ccc36d02ca/aic_sdk-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f13f9b2211136dd6f46fa2f148a55aabf5b9c3cb40fc0beed9e435b0df60d34c", size = 4111589, upload-time = "2026-01-23T23:36:50.448Z" }, + { url = "https://files.pythonhosted.org/packages/71/ca/22c99be2aca92f77d4f0fe742827cd2db5f0c761797ebe0e5bd43872259a/aic_sdk-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:f4c3556bad0b74f2c0a5c2a253f14ca58b7129ce5b17848b8b0948f68286639d", size = 3663706, upload-time = "2026-01-23T23:36:54.581Z" }, + { url = "https://files.pythonhosted.org/packages/19/fc/fbd7ee793cf15ef3319d12399ee9300c21c09acf654e1d8d1f64f682d750/aic_sdk-2.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:11de01064d028adeb2d2edda4546e86002d5b43710fcdc00a33ee2403a1676d4", size = 3274994, upload-time = "2026-01-23T23:36:58.177Z" }, + { url = "https://files.pythonhosted.org/packages/8b/04/07ed2ae4b4dc9f31522fa971791fd7d7e38feac8ce2b9d3316394b2e5fe5/aic_sdk-2.0.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f48dde209a704a51e65a44c7846c033dc860003467cef0fc2d15d7f8aa137dbc", size = 4893276, upload-time = "2026-01-23T23:37:03.097Z" }, + { url = "https://files.pythonhosted.org/packages/58/87/6328bcf58e633acdf65fd72c4dee61f468fef399c0868e5c446b99166bf5/aic_sdk-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2017ea843fc9e38612a13f1b0a668428a3f6862792baf230ac79a65d9c0633d9", size = 4450341, upload-time = "2026-01-23T23:37:08.648Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/da5138346944ac7dc61ed70e66c1fb2fddef815dc2bab561316db5aef252/aic_sdk-2.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:065954c17116b96408ebfbff29152ca458bb083a9e56178c70adbafdda08218a", size = 3594974, upload-time = "2026-01-23T23:37:13.83Z" }, + { url = "https://files.pythonhosted.org/packages/06/d8/17e1a77820a6848efb7c97751bc6022f65c5ca6436dc3caf3a9da356def1/aic_sdk-2.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa7a196160d6eaf2b856c542bc967c2e08e11a5d93ac4e632f843a01b2872274", size = 4113591, upload-time = "2026-01-23T23:37:19.067Z" }, + { url = "https://files.pythonhosted.org/packages/38/3b/04b70a75364c2ef1717018a81963a8e16bffc3f9f064f125cb111870b6a4/aic_sdk-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:dbd007a683ebff4def95fa5a7ace1602aa2d150fa80761231b044edd57e98bbb", size = 3661883, upload-time = "2026-01-23T23:37:25.242Z" }, + { url = "https://files.pythonhosted.org/packages/2a/bd/64bc3ce090cc110f3721c5e54f97f9fcb67dc50bd8dc6408896650d1d68e/aic_sdk-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:f776c5f0425b39073d4caca2f0bdea036647c4162d4673ef498e1306d41bb39e", size = 3271232, upload-time = "2026-01-23T23:37:30.005Z" }, + { url = "https://files.pythonhosted.org/packages/6e/72/8445a7201aa5969216b5d4ab60bb2ebefa2ac07f557e9ebca27172be2f00/aic_sdk-2.0.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a7ff35422ffb813e8a5b4afed6eb56d4e8abc1ecabf464084d4c7b5b8aff0e43", size = 4892624, upload-time = "2026-01-23T23:37:33.229Z" }, + { url = "https://files.pythonhosted.org/packages/c7/9c/5b060cbd9e9bcea5e62df13cf3e722f4355286e2174c298ffcfe337c680d/aic_sdk-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6cda0b6db664712099da483f803128e4e5256625aed2d85e65e5cc823a0e873b", size = 4449490, upload-time = "2026-01-23T23:37:35.938Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f8/ac61d007dc8d158a8f516327db74b6f3b1cf78b16be43acd29775197533d/aic_sdk-2.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ade8754bd878da0509e70636a9b4eaba0280741db5afabf752102ef605ffaac", size = 3594360, upload-time = "2026-01-23T23:37:38.943Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c4/af0c00055450b060b23e8dd5f3c1a208ea1444c6b497eaf29d3de6e215fa/aic_sdk-2.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b2f44c660aa29613be05c576da25092b3570c8fb3dddc09b70625789d066202", size = 4112325, upload-time = "2026-01-23T23:37:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/86/8e/62a7c53cc1bf2345ea20b554d1d8a61058301cd8088ff94ba95809b04a02/aic_sdk-2.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:ce1656991fc4dbbb40257c72a4b8fc4d4839363ca4b7b25a84ae5a83914ae90c", size = 3661441, upload-time = "2026-01-23T23:37:45.025Z" }, + { url = "https://files.pythonhosted.org/packages/39/98/aa9d6ccba0a1902f8480544fcf468dd3696ecb5392f02c2770f9020e6f9a/aic_sdk-2.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:0138e964feb15d9fb5d2c9c64a8d45a807171900f53351e5525c26869237bd1c", size = 3270753, upload-time = "2026-01-23T23:37:47.882Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3e/6a693ba223e2e55e142983c6243968222070405c6a90ec4c5a61b46652c1/aic_sdk-2.0.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:11eb0c3686ff83f340c875b864840fad19e3a98cd6e59815f83e9248a3ffb397", size = 4893527, upload-time = "2026-01-23T23:37:52.021Z" }, + { url = "https://files.pythonhosted.org/packages/35/9c/f149870d75f28c851de439d4039f85aa590f47499272f932841e4dc0a9a5/aic_sdk-2.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:27844521dc1ae3e1226e7371ec3e68fc4726515f14c5e82a6030f276b612c1a8", size = 4450169, upload-time = "2026-01-23T23:37:55.964Z" }, + { url = "https://files.pythonhosted.org/packages/92/87/8ee4e1763b603ad3d6d535d7ecfa7a2943145bcc18f2db4600279aa37af3/aic_sdk-2.0.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:adf617d4e4e8910764118d1baf1521c752218fd304c75d7e22352d4755cabd50", size = 3595300, upload-time = "2026-01-23T23:37:59.494Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b4/06d6f5c1b45d839d4d8ad4fbcb45dc224e980c69976c48e39d5e32850c51/aic_sdk-2.0.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2bc9071599b9703783b6b100758cd7621b30a64abddc8072f6872932b74c21", size = 4113837, upload-time = "2026-01-23T23:38:03.565Z" }, + { url = "https://files.pythonhosted.org/packages/1e/35/d7a2f7b37183b08b2e8969c3d6c6d1824253cd32894d72f250075edee654/aic_sdk-2.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:9db2bfb4f1ab40a4b130d8d0e158277461b25c7b78bffffba90f816766cb28e9", size = 3663055, upload-time = "2026-01-23T23:38:07.741Z" }, + { url = "https://files.pythonhosted.org/packages/90/97/9ed859e70b1d0c68edc9748c4e69d251e89a3faa462f36ce26c1f8aa7844/aic_sdk-2.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:3c6ed1bfda589970e6c6b96ae29f112baa430ad91e149e76004825870198a5c7", size = 3272737, upload-time = "2026-01-23T23:38:13.966Z" }, +] [[package]] name = "aioboto3" @@ -4495,7 +4527,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 = "~=2.0.1" }, { name = "aioboto3", marker = "extra == 'aws'", specifier = "~=15.5.0" }, { name = "aiofiles", specifier = ">=24.1.0,<25" }, { name = "aiohttp", specifier = ">=3.11.12,<4" }, @@ -4586,7 +4618,7 @@ requires-dist = [ { name = "simli-ai", marker = "extra == 'simli'", specifier = "~=1.0.3" }, { name = "soundfile", marker = "extra == 'soundfile'", specifier = "~=0.13.1" }, { name = "soxr", specifier = "~=0.5.0" }, - { name = "speechmatics-voice", extras = ["smart"], marker = "extra == 'speechmatics'", specifier = ">=0.2.6" }, + { name = "speechmatics-voice", extras = ["smart"], marker = "extra == 'speechmatics'", specifier = ">=0.2.8" }, { name = "strands-agents", marker = "extra == 'strands'", specifier = ">=1.9.1,<2" }, { name = "tenacity", marker = "extra == 'livekit'", specifier = ">=8.2.3,<10.0.0" }, { name = "timm", marker = "extra == 'moondream'", specifier = "~=1.0.13" }, @@ -6420,16 +6452,16 @@ wheels = [ [[package]] name = "speechmatics-voice" -version = "0.2.7" +version = "0.2.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "pydantic" }, { name = "speechmatics-rt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4a/94/47280c7fa5264676bfd6a2373c5cbfa562d5f1aefd77d7f241641a4889a6/speechmatics_voice-0.2.7.tar.gz", hash = "sha256:392b5129d2cbc0059f122fdf960d88dc59df5f26808992ef031f2eb40713c936", size = 61137, upload-time = "2026-01-12T14:21:17.672Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/b2/72b5b2203bbefbd22e7692adaca0dd7c2feebed1aaea5599ec579f74fbbf/speechmatics_voice-0.2.8.tar.gz", hash = "sha256:b2d9cbf773fd94400c744734662e2b16b5bdc4271d0dafde46ac032c438fe000", size = 61419, upload-time = "2026-01-26T16:26:09.082Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/72/e74dbcd42935b31b1d188b8f9d932d9d4078ea5edf303bb0ba0af4203ba2/speechmatics_voice-0.2.7-py3-none-any.whl", hash = "sha256:79c6072a5bf21cfa75770b5e3855cff5747222b024c417a276d0b9c2ae83cd0c", size = 57323, upload-time = "2026-01-12T14:21:16.679Z" }, + { url = "https://files.pythonhosted.org/packages/89/2d/a2ab215a7a31fad5ef9267420dc9ced96d6d52e5b80b131ef41424607849/speechmatics_voice-0.2.8-py3-none-any.whl", hash = "sha256:423ac7620ae8c98f175faace2184ac4ab1fe448ffb41af57aae05ec655326f79", size = 57629, upload-time = "2026-01-26T16:26:07.59Z" }, ] [package.optional-dependencies]