diff --git a/CHANGELOG.md b/CHANGELOG.md index 39dd6c194..dfd42c6c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,483 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [0.0.106] - 2026-03-18 + +### Added + +- Added optional `service` field to `ServiceUpdateSettingsFrame` (and its + subclasses `LLMUpdateSettingsFrame`, `TTSUpdateSettingsFrame`, + `STTUpdateSettingsFrame`) to target a specific service instance. When + `service` is set, only the matching service applies the settings; others + forward the frame unchanged. This enables updating a single service when + multiple services of the same type exist in the pipeline. + (PR [#4004](https://github.com/pipecat-ai/pipecat/pull/4004)) + +- Added `sip_provider` and `room_geo` parameters to `configure()` in the Daily + runner. These convenience parameters let callers specify a SIP provider name + and geographic region directly without manually constructing + `DailyRoomProperties` and `DailyRoomSipParams`. + (PR [#4005](https://github.com/pipecat-ai/pipecat/pull/4005)) + +- Added `PerplexityLLMAdapter` that automatically transforms conversation + messages to satisfy Perplexity's stricter API constraints (strict role + alternation, no non-initial system messages, last message must be user/tool). + Previously, certain conversation histories could cause Perplexity API errors + that didn't occur with OpenAI (`PerplexityLLMService` subclasses + `OpenAILLMService` since Perplexity uses an OpenAI-compatible API). + (PR [#4009](https://github.com/pipecat-ai/pipecat/pull/4009)) + +- Added DTMF input event support to the Daily transport. Incoming DTMF tones + are now received via Daily's `on_dtmf_event` callback and pushed into the + pipeline as `InputDTMFFrame`, enabling bots to react to keypad presses from + phone callers. + (PR [#4047](https://github.com/pipecat-ai/pipecat/pull/4047)) + +- Added `WakePhraseUserTurnStartStrategy` for triggering user turns based on + wake phrases, with support for `single_activation` mode. Deprecates + `WakeCheckFilter`. + (PR [#4064](https://github.com/pipecat-ai/pipecat/pull/4064)) + +- Added `default_user_turn_start_strategies()` and + `default_user_turn_stop_strategies()` helper functions for composing custom + strategy lists. + (PR [#4064](https://github.com/pipecat-ai/pipecat/pull/4064)) + +### Changed + +- Changed tool result JSON serialization to use `ensure_ascii=False`, + preserving UTF-8 characters instead of escaping them. This reduces context + size and token usage for non-English languages. + (PR [#3457](https://github.com/pipecat-ai/pipecat/pull/3457)) + +- `OpenAIRealtimeSTTService`'s `noise_reduction` parameter is now part of + `OpenAIRealtimeSTTSettings`, making it runtime-updatable via + `STTUpdateSettingsFrame`. The direct `noise_reduction` init argument is + deprecated as of 0.0.106. + (PR [#3991](https://github.com/pipecat-ai/pipecat/pull/3991)) + +- Updated `sarvamai` dependency from `0.1.26a2` (alpha) to `0.1.26` (stable + release). + (PR [#3997](https://github.com/pipecat-ai/pipecat/pull/3997)) + +- `SimliVideoService` now extends `AIService` instead of `FrameProcessor`, + aligning it with the HeyGen and Tavus video services. It supports + `SimliVideoService.Settings(...)` for configuration and uses + `start()`/`stop()`/`cancel()` lifecycle methods. Existing constructor usage + (`api_key`, `face_id`, etc.) remains unchanged. + (PR [#4001](https://github.com/pipecat-ai/pipecat/pull/4001)) + +- Update `pipecat-ai-small-webrtc-prebuilt` to `2.4.0`. + (PR [#4023](https://github.com/pipecat-ai/pipecat/pull/4023)) + +- Nova Sonic assistant text transcripts are now delivered in real-time using + speculative text events instead of delayed final text events. Previously, + assistant text only arrived after all audio had finished playing, causing + laggy transcripts in client UIs. Speculative text arrives before each audio + chunk, providing text synchronized with what the bot is saying. This also + simplifies the internal text handling by removing the interruption re-push + hack and assistant text buffer. + (PR [#4042](https://github.com/pipecat-ai/pipecat/pull/4042)) + +- Updated `daily-python` dependency to 0.25.0. + (PR [#4047](https://github.com/pipecat-ai/pipecat/pull/4047)) + +- Added `enable_dialout` parameter to `configure()` in `pipecat.runner.daily` + to support dial-out rooms. Also narrowed misleading `Optional` type hints and + deduplicated token expiry calculation. + (PR [#4048](https://github.com/pipecat-ai/pipecat/pull/4048)) + +- Extended `ProcessFrameResult` to stop strategies, allowing a stop strategy to + short-circuit evaluation of subsequent strategies by returning `STOP`. + (PR [#4064](https://github.com/pipecat-ai/pipecat/pull/4064)) + +- `GradiumSTTService` now takes both an `encoding` and `sample_rate` + constructor argument which is assmebled in the class to form the + `input_format`. PCM accepts `8000`, `16000`, and `24000` Hz sample rates. + (PR [#4066](https://github.com/pipecat-ai/pipecat/pull/4066)) + +- Improved `GradiumSTTService` transcription accuracy by reworking how text + fragments are accumulated and finalized. Previously, trailing words could be + dropped when the server's `flushed` response arrived before all text tokens + were delivered. The service now uses a short aggregation delay after flush to + capture trailing tokens, producing complete utterances. + (PR [#4066](https://github.com/pipecat-ai/pipecat/pull/4066)) + +### Deprecated + +- `SimliVideoService.InputParams` is deprecated. Use the direct constructor + parameters `max_session_length`, `max_idle_time`, and `enable_logging` + instead. + (PR [#4001](https://github.com/pipecat-ai/pipecat/pull/4001)) + +- Deprecated `LocalSmartTurnAnalyzerV2` and `LocalCoreMLSmartTurnAnalyzer`. Use + `LocalSmartTurnAnalyzerV3` instead. Instantiating these analyzers will now + emit a `DeprecationWarning`. + (PR [#4012](https://github.com/pipecat-ai/pipecat/pull/4012)) + +- Deprecated `WakeCheckFilter` in favor of `WakePhraseUserTurnStartStrategy`. + (PR [#4064](https://github.com/pipecat-ai/pipecat/pull/4064)) + +### Fixed + +- Fixed an issue where the default model for `OpenAILLMService` and + `AzureLLMService` was mistakenly reverted to `gpt-4o`. The defaults are now + restored to `gpt-4.1`. + (PR [#4000](https://github.com/pipecat-ai/pipecat/pull/4000)) + +- Fixed a race condition where `EndTaskFrame` could cause the pipeline to shut + down before in-flight frames (e.g. LLM function call responses) finished + processing. `EndTaskFrame` and `StopTaskFrame` now flow through the pipeline + as `ControlFrame`s, ensuring all pending work is flushed before shutdown + begins. `CancelTaskFrame` and `InterruptionTaskFrame` remain immediate + (`SystemFrame`). + (PR [#4006](https://github.com/pipecat-ai/pipecat/pull/4006)) + +- Fixed `ParallelPipeline` dropping or misordering frames during lifecycle + synchronization. Buffered frames are now flushed in the correct order + relative to synchronization frames (`StartFrame` goes first, + `EndFrame`/`CancelFrame` go after), and frames added to the buffer during + flush are also drained. + (PR [#4007](https://github.com/pipecat-ai/pipecat/pull/4007)) + +- Fixed `TTSService` potentially canceling in-flight audio during shutdown. The + stop sequence now waits for all queued audio contexts to finish processing + before canceling the stop frame task. + (PR [#4007](https://github.com/pipecat-ai/pipecat/pull/4007)) + +- Fixed `Language` enum values (e.g. `Language.ES`) not being converted to + service-specific codes when passed via + `settings=Service.Settings(language=Language.ES)` at init time. This caused + API errors (e.g. 400 from Rime) because the raw enum was sent instead of the + expected language code (e.g. `"spa"`). Runtime updates via + `UpdateSettingsFrame` were unaffected. The fix centralizes conversion in the + base `TTSService` and `STTService` classes so all services handle this + consistently. + (PR [#4024](https://github.com/pipecat-ai/pipecat/pull/4024)) + +- Fixed `DeepgramSTTService` ignoring the `base_url` scheme when using `ws://` + or `http://`. Previously these were silently overwritten with `wss://` / + `https://`, breaking air-gapped or private deployments that don't use TLS. + All scheme choices (`wss://`, `https://`, `ws://`, `http://`, or bare + hostname) are now respected. + (PR [#4026](https://github.com/pipecat-ai/pipecat/pull/4026)) + +- Fixed `LLMSwitcher.register_function()` and `register_direct_function()` not + accepting or forwarding the `timeout_secs` parameter. + (PR [#4037](https://github.com/pipecat-ai/pipecat/pull/4037)) + +- Fixed empty user transcriptions in Nova Sonic causing spurious interruptions. + Previously, an empty transcription could trigger an interruption of the + assistant's response even though the user hadn't actually spoken. + (PR [#4042](https://github.com/pipecat-ai/pipecat/pull/4042)) + +- Fixed `SonioxSTTService` and `OpenAIRealtimeSTTService` crash when language + parameters contain plain strings instead of `Language` enum values. + (PR [#4046](https://github.com/pipecat-ai/pipecat/pull/4046)) + +- Fixed premature user turn stops caused by late transcriptions arriving + between turns. A stale transcript from the previous turn could persist into + the next turn and trigger a stop before the current turn's real transcript + arrived. Stop strategies are now reset at both turn start and turn stop to + prevent state from leaking across turn boundaries. + (PR [#4057](https://github.com/pipecat-ai/pipecat/pull/4057)) + +- Fixed raw language strings like `"de-DE"` silently failing when passed to + TTS/STT services (e.g. ElevenLabs producing no audio). Raw strings now go + through the same `Language` enum resolution as enum values, so regional codes + like `"de-DE"` are properly converted to service-expected formats like + `"de"`. Unrecognized strings log a warning instead of failing silently. + (PR [#4058](https://github.com/pipecat-ai/pipecat/pull/4058)) + +- Fixed Deepgram STT list-type settings (`keyterm`, `keywords`, `search`, + `redact`, `replace`) being stringified instead of passed as lists to the SDK, + which caused them to be sent as literal strings (e.g. `"['pipecat']"`) in the + WebSocket query params. + (PR [#4063](https://github.com/pipecat-ai/pipecat/pull/4063)) + +- Fixed `MinWordsUserTurnStartStrategy` including text below the word threshold + in the output by resetting aggregation when the minimum word count is not + met. + (PR [#4064](https://github.com/pipecat-ai/pipecat/pull/4064)) + +- Fixed audio overlap and potential dropped TTS content when multiple assistant + turns occur in quick succession. `TTSService` now flushes remaining text + before pausing frame processing on `LLMFullResponseEndFrame`/`EndFrame`, + instead of pausing first. + (PR [#4071](https://github.com/pipecat-ai/pipecat/pull/4071)) + +### Security + +- Bumped PyJWT minimum version from 2.10.1 to 2.12.0 in the `livekit` extra to + address CVE-2026-32597 (GHSA-752w-5fwx-jx9f), where PyJWT <= 2.11.0 accepted + unknown `crit` header extensions. + (PR [#4035](https://github.com/pipecat-ai/pipecat/pull/4035)) + +## [0.0.105] - 2026-03-10 + +### Added + +- Added concurrent audio context support: `CartesiaTTSService` can now + synthesize the next sentence while the previous one is still playing, by + setting `pause_frame_processing=False` and routing each sentence through its + own audio context queue. + (PR [#3804](https://github.com/pipecat-ai/pipecat/pull/3804)) + +- Added custom video track support to Daily transport. Use + `video_out_destinations` in `DailyParams` to publish multiple video tracks + simultaneously, mirroring the existing `audio_out_destinations` feature. + (PR [#3831](https://github.com/pipecat-ai/pipecat/pull/3831)) + +- Added `ServiceSwitcherStrategyFailover` that automatically switches to the + next service when the active service reports a non-fatal error. Recovery + policies can be implemented via the `on_service_switched` event handler. + (PR [#3861](https://github.com/pipecat-ai/pipecat/pull/3861)) + +- Added optional `timeout_secs` parameter to `register_function()` and + `register_direct_function()` for per-tool function call timeout control, + overriding the global `function_call_timeout_secs` default. + (PR [#3915](https://github.com/pipecat-ai/pipecat/pull/3915)) + +- Added `cloud-audio-only` recording option to Daily transport's + `enable_recording` property. + (PR [#3916](https://github.com/pipecat-ai/pipecat/pull/3916)) + +- Wired up `system_instruction` in `BaseOpenAILLMService`, + `AnthropicLLMService`, and `AWSBedrockLLMService` so it works as a default + system prompt, matching the behavior of the Google services. This enables + sharing a single `LLMContext` across multiple LLM services, where each + service provides its own system instruction independently. + + ```python + llm = OpenAILLMService( + api_key=os.getenv("OPENAI_API_KEY"), + system_instruction="You are a helpful assistant.", + ) + + context = LLMContext() + + @transport.event_handler("on_client_connected") + async def on_client_connected(transport, client): + context.add_message({"role": "user", "content": "Please introduce yourself."}) + await task.queue_frames([LLMRunFrame()]) + ``` + (PR [#3918](https://github.com/pipecat-ai/pipecat/pull/3918)) + +- Added `vad_threshold` parameter to `AssemblyAIConnectionParams` for + configuring voice activity detection sensitivity in U3 Pro. Aligning this + with external VAD thresholds (e.g., Silero VAD) prevents the "dead zone" + where AssemblyAI transcribes speech that VAD hasn't detected yet. + (PR [#3927](https://github.com/pipecat-ai/pipecat/pull/3927)) + +- Added `push_empty_transcripts` parameter to `BaseWhisperSTTService` and + `OpenAISTTService` to allow empty transcripts to be pushed downstream as + `TranscriptionFrame` instead of discarding them (the default behavior). This + is intended for situations where VAD fires even though the user did not + speak. In these cases, it is useful to know that nothing was transcribed so + that the agent can resume speaking, instead of waiting longer for a + transcription. + (PR [#3930](https://github.com/pipecat-ai/pipecat/pull/3930)) + +- LLM services (`BaseOpenAILLMService`, `AnthropicLLMService`, + `AWSBedrockLLMService`) now log a warning when both `system_instruction` and + a system message in the context are set. The constructor's + `system_instruction` takes precedence. + (PR [#3932](https://github.com/pipecat-ai/pipecat/pull/3932)) + +- Runtime settings updates (via `STTUpdateSettingsFrame`) now work for AWS + Transcribe, Azure, Cartesia, Deepgram, ElevenLabs Realtime, Gradium, and + Soniox STT services. Previously, changing settings at runtime only stored the + new values without reconnecting. + (PR [#3946](https://github.com/pipecat-ai/pipecat/pull/3946)) + +- Exposed `on_summary_applied` event on `LLMAssistantAggregator`, allowing + users to listen for context summarization events without accessing private + members. + (PR [#3947](https://github.com/pipecat-ai/pipecat/pull/3947)) + +- Deepgram Flux STT settings (`keyterm`, `eot_threshold`, + `eager_eot_threshold`, `eot_timeout_ms`) can now be updated mid-stream via + `STTUpdateSettingsFrame` without triggering a reconnect. The new values are + sent to Deepgram as a Configure WebSocket message on the existing connection. + (PR [#3953](https://github.com/pipecat-ai/pipecat/pull/3953)) + +- Added `system_instruction` parameter to `run_inference` across all LLM + services, allowing callers to override the system prompt for one-shot + inference calls. Used by `_generate_summary` to pass the summarization prompt + cleanly. + (PR [#3968](https://github.com/pipecat-ai/pipecat/pull/3968)) + +### Changed + +- Audio context management (previously in `AudioContextTTSService`) is now + built into `TTSService`. All WebSocket providers (`cartesia`, `elevenlabs`, + `asyncai`, `inworld`, `rime`, `gradium`, `resembleai`) now inherit from + `WebsocketTTSService` directly. Word-timestamp baseline is set automatically + on the first audio chunk of each context instead of requiring each provider + to call `start_word_timestamps()` in their receive loop. + (PR [#3804](https://github.com/pipecat-ai/pipecat/pull/3804)) + +- Daily transport now uses `CustomVideoSource`/`CustomVideoTrack` instead of + `VirtualCameraDevice` for the default camera output, mirroring how audio + already works with `CustomAudioSource`/`CustomAudioTrack`. + (PR [#3831](https://github.com/pipecat-ai/pipecat/pull/3831)) + +- ⚠️ Updated `DeepgramSTTService` to use `deepgram-sdk` v6. The `LiveOptions` + class was removed from the SDK and is now provided by pipecat directly; + import it from `pipecat.services.deepgram.stt` instead of `deepgram`. + (PR [#3848](https://github.com/pipecat-ai/pipecat/pull/3848)) + +- `ServiceSwitcherStrategy` base class now provides a `handle_error()` hook for + subclasses to implement error-based switching. `ServiceSwitcher` defaults to + `ServiceSwitcherStrategyManual` and `strategy_type` is now optional. + (PR [#3861](https://github.com/pipecat-ai/pipecat/pull/3861)) + +- Support for Voice Focus 2.0 models. + - Updated `aic-sdk` to `~=2.1.0` to support Voice Focus 2.0 models. + - Cleaned unused `ParameterFixedError` exception handling in `AICFilter` + parameter setup. + (PR [#3889](https://github.com/pipecat-ai/pipecat/pull/3889)) + +- `max_context_tokens` and `max_unsummarized_messages` in + `LLMAutoContextSummarizationConfig` (and deprecated + `LLMContextSummarizationConfig`) can now be set to `None` independently to + disable that summarization threshold. At least one must remain set. + (PR [#3914](https://github.com/pipecat-ai/pipecat/pull/3914)) + +- ⚠️ Removed `formatted_finals` and `word_finalization_max_wait_time` from + `AssemblyAIConnectionParams` as these were v2 API parameters not supported in + v3. Clarified that `format_turns` only applies to Universal-Streaming models; + U3 Pro has automatic formatting built-in. + (PR [#3927](https://github.com/pipecat-ai/pipecat/pull/3927)) + +- Changed `DeepgramTTSService` to send a Clear message on interruption instead + of disconnecting and reconnecting the WebSocket, allowing the connection to + persist throughout the session. + (PR [#3958](https://github.com/pipecat-ai/pipecat/pull/3958)) + +- Re-added `enhancement_level` support to `AICFilter` with runtime + `FilterEnableFrame` control, applying `ProcessorParameter.Bypass` and + `ProcessorParameter.EnhancementLevel` together. + (PR [#3961](https://github.com/pipecat-ai/pipecat/pull/3961)) + +- Updated `daily-python` dependency from `~=0.23.0` to `~=0.24.0`. + (PR [#3970](https://github.com/pipecat-ai/pipecat/pull/3970)) + +- Updated `FishAudioTTSService` default model from `s1` to `s2-pro`, matching + Fish Audio's latest recommended model for improved quality and speed. + (PR [#3973](https://github.com/pipecat-ai/pipecat/pull/3973)) + +- `AzureSTTService` `region` parameter is now optional when `private_endpoint` + is provided. A `ValueError` is raised if neither is given, and a warning is + logged if both are provided (`private_endpoint` takes priority). + (PR [#3974](https://github.com/pipecat-ai/pipecat/pull/3974)) + +### Deprecated + +- Deprecated `AudioContextTTSService` and `AudioContextWordTTSService`. + Subclass `WebsocketTTSService` directly instead; audio context management is + now part of the base `TTSService`. + - Deprecated `WordTTSService`, `WebsocketWordTTSService`, and + `InterruptibleWordTTSService`. Word timestamp logic is now always active in + `TTSService` and no longer needs to be opted into via a subclass. + (PR [#3804](https://github.com/pipecat-ai/pipecat/pull/3804)) + +- Deprecated `pipecat.services.google.llm_vertex`, + `pipecat.services.google.llm_openai`, and + `pipecat.services.google.gemini_live.llm_vertex` modules. Use + `pipecat.services.google.vertex.llm`, `pipecat.services.google.openai.llm`, + and `pipecat.services.google.gemini_live.vertex.llm` instead. The old import + paths still work but will emit a `DeprecationWarning`. + (PR [#3980](https://github.com/pipecat-ai/pipecat/pull/3980)) + +### Removed + +- ⚠️ Removed `supports_word_timestamps` parameter from `TTSService.__init__()`. + Word timestamp logic is now always active. Remove this argument from any + custom subclass `super().__init__()` calls. + (PR [#3804](https://github.com/pipecat-ai/pipecat/pull/3804)) + +### Fixed + +- Fixed `DeepgramSTTService` keepalive ping timeout disconnections. The + deepgram-sdk v6 removed automatic keepalive; pipecat now sends explicit + `KeepAlive` messages every 5 seconds, within the recommended 3–5 second + interval before Deepgram's 10-second inactivity timeout. + (PR [#3848](https://github.com/pipecat-ai/pipecat/pull/3848)) + +- Fixed `BufferError: Existing exports of data: object cannot be re-sized` in + `AICFilter` caused by holding a `memoryview` on the mutable audio buffer + across async yield points. + (PR [#3889](https://github.com/pipecat-ai/pipecat/pull/3889)) + +- Fixed TTS context not being appended to the assistant message history when + using `TTSSpeakFrame` with `append_to_context=True` with some TTS providers. + (PR [#3936](https://github.com/pipecat-ai/pipecat/pull/3936)) + +- Fixed context summarization leaving orphaned tool responses in the kept + context when tool calls were moved to the summarized portion. + (PR [#3937](https://github.com/pipecat-ai/pipecat/pull/3937)) + +- Fixed turn completion state not resetting at end of LLM responses. + `LLMFullResponseEndFrame` is pushed (not received) by the LLM service, so the + mixin now handles it in `push_frame` instead of `process_frame`. + (PR [#3956](https://github.com/pipecat-ai/pipecat/pull/3956)) + +- Fixed turn completion instructions being injected as a context system message + instead of using `system_instruction`. This caused warning spam when + `system_instruction` was also set and didn't persist across full context + updates. + (PR [#3957](https://github.com/pipecat-ai/pipecat/pull/3957)) + +- Fixed `TTSService` audio context queue getting blocked when + `append_to_audio_context()` was called with a `None` context ID, which + prevented subsequent audio from being delivered. + (PR [#3958](https://github.com/pipecat-ai/pipecat/pull/3958)) + +- Fixed `on_call_state_updated` event handler in LiveKit transport receiving + incorrect number of arguments due to redundant `self` passed to + `_call_event_handler`. + (PR [#3959](https://github.com/pipecat-ai/pipecat/pull/3959)) + +- Fixed OpenAI Realtime, OpenAI Realtime Beta, and Grok realtime services + treating `conversation_already_has_active_response` as a fatal error. These + services now log it as a non-fatal debug event when a response is already in + progress. + (PR [#3960](https://github.com/pipecat-ai/pipecat/pull/3960)) + +- Fixed `SmallWebRTCConnection` silently discarding messages sent before the + data channel is open by queuing them and flushing once the channel is ready. + A bounded queue (`MAX_MESSAGE_QUEUE_SIZE = 50`) prevents unbounded memory + growth, and a 10-second timeout after connection clears the queue and falls + back to discard mode if the data channel never opens. + (PR [#3962](https://github.com/pipecat-ai/pipecat/pull/3962)) + +- Fixed `AzureSTTService` failing to initialize when `private_endpoint` is + provided. The Azure Speech SDK's `SpeechConfig` does not accept both `region` + and `endpoint` simultaneously, so they are now passed conditionally. + (PR [#3967](https://github.com/pipecat-ai/pipecat/pull/3967)) + +- Fixed `GoogleLLMService` ignoring the `system_instruction` set via + constructor or `GoogleLLMSettings` when a system message was also present in + the context. The settings value now correctly takes priority, and a warning + is logged when both are set. + (PR [#3976](https://github.com/pipecat-ai/pipecat/pull/3976)) + +### Other + +- Updated foundational examples to use `system_instruction` on LLM services + instead of adding system messages to `LLMContext`. + (PR [#3918](https://github.com/pipecat-ai/pipecat/pull/3918)) + +- Updated AssemblyAI turn detection example to use `keyterms_prompt` list + format instead of `prompt` string for improved clarity. + (PR [#3929](https://github.com/pipecat-ai/pipecat/pull/3929)) + +- Updated foundational examples and eval scripts to use `"user"` role instead + of `"system"` when adding messages to `LLMContext`, since system prompts + should be set via `system_instruction` on the LLM service. + (PR [#3931](https://github.com/pipecat-ai/pipecat/pull/3931)) + ## [0.0.104] - 2026-03-02 ### Added diff --git a/COMMUNITY_INTEGRATIONS.md b/COMMUNITY_INTEGRATIONS.md index f6f92a5d3..c7e4c8e90 100644 --- a/COMMUNITY_INTEGRATIONS.md +++ b/COMMUNITY_INTEGRATIONS.md @@ -280,17 +280,17 @@ from typing import Optional class MyTTSService(TTSService): Settings = MyTTSSettings - _settings: MyTTSSettings + _settings: Settings def __init__( self, *, api_key: str, - settings: Optional[MyTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): # 1. Defaults — every field has a real value (store mode). - default_settings = MyTTSSettings( + default_settings = self.Settings( model="my-model-v1", voice="default-voice", language="en", diff --git a/changelog/3804.added.md b/changelog/3804.added.md deleted file mode 100644 index 0ad7676c9..000000000 --- a/changelog/3804.added.md +++ /dev/null @@ -1 +0,0 @@ -- Added concurrent audio context support: `CartesiaTTSService` can now synthesize the next sentence while the previous one is still playing, by setting `pause_frame_processing=False` and routing each sentence through its own audio context queue. diff --git a/changelog/3804.changed.md b/changelog/3804.changed.md deleted file mode 100644 index 9caae491f..000000000 --- a/changelog/3804.changed.md +++ /dev/null @@ -1 +0,0 @@ -- Audio context management (previously in `AudioContextTTSService`) is now built into `TTSService`. All WebSocket providers (`cartesia`, `elevenlabs`, `asyncai`, `inworld`, `rime`, `gradium`, `resembleai`) now inherit from `WebsocketTTSService` directly. Word-timestamp baseline is set automatically on the first audio chunk of each context instead of requiring each provider to call `start_word_timestamps()` in their receive loop. diff --git a/changelog/3804.deprecated.md b/changelog/3804.deprecated.md deleted file mode 100644 index 0babfe7d4..000000000 --- a/changelog/3804.deprecated.md +++ /dev/null @@ -1,2 +0,0 @@ -- Deprecated `AudioContextTTSService` and `AudioContextWordTTSService`. Subclass `WebsocketTTSService` directly instead; audio context management is now part of the base `TTSService`. -- Deprecated `WordTTSService`, `WebsocketWordTTSService`, and `InterruptibleWordTTSService`. Word timestamp logic is now always active in `TTSService` and no longer needs to be opted into via a subclass. diff --git a/changelog/3804.removed.md b/changelog/3804.removed.md deleted file mode 100644 index 1813b5999..000000000 --- a/changelog/3804.removed.md +++ /dev/null @@ -1 +0,0 @@ -- ⚠️ Removed `supports_word_timestamps` parameter from `TTSService.__init__()`. Word timestamp logic is now always active. Remove this argument from any custom subclass `super().__init__()` calls. diff --git a/changelog/3848.changed.md b/changelog/3848.changed.md deleted file mode 100644 index 1590e284a..000000000 --- a/changelog/3848.changed.md +++ /dev/null @@ -1 +0,0 @@ -- ⚠️ Updated `DeepgramSTTService` to use `deepgram-sdk` v6. The `LiveOptions` class was removed from the SDK and is now provided by pipecat directly; import it from `pipecat.services.deepgram.stt` instead of `deepgram`. diff --git a/changelog/3848.fixed.md b/changelog/3848.fixed.md deleted file mode 100644 index a6651b763..000000000 --- a/changelog/3848.fixed.md +++ /dev/null @@ -1 +0,0 @@ -- Fixed `DeepgramSTTService` keepalive ping timeout disconnections. The deepgram-sdk v6 removed automatic keepalive; pipecat now sends explicit `KeepAlive` messages every 5 seconds, within the recommended 3–5 second interval before Deepgram's 10-second inactivity timeout. diff --git a/changelog/3889.changed.md b/changelog/3889.changed.md deleted file mode 100644 index c04aa0080..000000000 --- a/changelog/3889.changed.md +++ /dev/null @@ -1,3 +0,0 @@ -- Support for Voice Focus 2.0 models. - - Updated `aic-sdk` to `~=2.1.0` to support Voice Focus 2.0 models. - - Cleaned unused `ParameterFixedError` exception handling in `AICFilter` parameter setup. diff --git a/changelog/3889.fixed.md b/changelog/3889.fixed.md deleted file mode 100644 index babe3eb35..000000000 --- a/changelog/3889.fixed.md +++ /dev/null @@ -1 +0,0 @@ -- Fixed `BufferError: Existing exports of data: object cannot be re-sized` in `AICFilter` caused by holding a `memoryview` on the mutable audio buffer across async yield points. diff --git a/changelog/3914.changed.md b/changelog/3914.changed.md deleted file mode 100644 index 22d4ff94e..000000000 --- a/changelog/3914.changed.md +++ /dev/null @@ -1 +0,0 @@ -- `max_context_tokens` and `max_unsummarized_messages` in `LLMAutoContextSummarizationConfig` (and deprecated `LLMContextSummarizationConfig`) can now be set to `None` independently to disable that summarization threshold. At least one must remain set. diff --git a/changelog/3915.added.md b/changelog/3915.added.md deleted file mode 100644 index 66d4fb383..000000000 --- a/changelog/3915.added.md +++ /dev/null @@ -1 +0,0 @@ -- Added optional `timeout_secs` parameter to `register_function()` and `register_direct_function()` for per-tool function call timeout control, overriding the global `function_call_timeout_secs` default. diff --git a/changelog/3916.added.md b/changelog/3916.added.md deleted file mode 100644 index 05c3f124f..000000000 --- a/changelog/3916.added.md +++ /dev/null @@ -1 +0,0 @@ -- Added `cloud-audio-only` recording option to Daily transport's `enable_recording` property. diff --git a/changelog/3918.added.md b/changelog/3918.added.md deleted file mode 100644 index 6b4d4446f..000000000 --- a/changelog/3918.added.md +++ /dev/null @@ -1,15 +0,0 @@ -- Wired up `system_instruction` in `BaseOpenAILLMService`, `AnthropicLLMService`, and `AWSBedrockLLMService` so it works as a default system prompt, matching the behavior of the Google services. This enables sharing a single `LLMContext` across multiple LLM services, where each service provides its own system instruction independently. - - ```python - llm = OpenAILLMService( - api_key=os.getenv("OPENAI_API_KEY"), - system_instruction="You are a helpful assistant.", - ) - - context = LLMContext() - - @transport.event_handler("on_client_connected") - async def on_client_connected(transport, client): - context.add_message({"role": "user", "content": "Please introduce yourself."}) - await task.queue_frames([LLMRunFrame()]) - ``` diff --git a/changelog/3918.other.md b/changelog/3918.other.md deleted file mode 100644 index 6caa1ba05..000000000 --- a/changelog/3918.other.md +++ /dev/null @@ -1 +0,0 @@ -- Updated foundational examples to use `system_instruction` on LLM services instead of adding system messages to `LLMContext`. diff --git a/changelog/3927.added.md b/changelog/3927.added.md deleted file mode 100644 index 378815fc3..000000000 --- a/changelog/3927.added.md +++ /dev/null @@ -1 +0,0 @@ -- Added `vad_threshold` parameter to `AssemblyAIConnectionParams` for configuring voice activity detection sensitivity in U3 Pro. Aligning this with external VAD thresholds (e.g., Silero VAD) prevents the "dead zone" where AssemblyAI transcribes speech that VAD hasn't detected yet. diff --git a/changelog/3927.changed.md b/changelog/3927.changed.md deleted file mode 100644 index b397e86a2..000000000 --- a/changelog/3927.changed.md +++ /dev/null @@ -1 +0,0 @@ -- ⚠️ Removed `formatted_finals` and `word_finalization_max_wait_time` from `AssemblyAIConnectionParams` as these were v2 API parameters not supported in v3. Clarified that `format_turns` only applies to Universal-Streaming models; U3 Pro has automatic formatting built-in. diff --git a/changelog/3929.other.md b/changelog/3929.other.md deleted file mode 100644 index c1d9f8dda..000000000 --- a/changelog/3929.other.md +++ /dev/null @@ -1 +0,0 @@ -- Updated AssemblyAI turn detection example to use `keyterms_prompt` list format instead of `prompt` string for improved clarity. diff --git a/changelog/3930.added.md b/changelog/3930.added.md deleted file mode 100644 index dd799f9ec..000000000 --- a/changelog/3930.added.md +++ /dev/null @@ -1 +0,0 @@ -- Added `push_empty_transcripts` parameter to `BaseWhisperSTTService` and `OpenAISTTService` to allow empty transcripts to be pushed downstream as `TranscriptionFrame` instead of discarding them (the default behavior). This is intended for situations where VAD fires even though the user did not speak. In these cases, it is useful to know that nothing was transcribed so that the agent can resume speaking, instead of waiting longer for a transcription. \ No newline at end of file diff --git a/changelog/3931.other.md b/changelog/3931.other.md deleted file mode 100644 index a1ebef414..000000000 --- a/changelog/3931.other.md +++ /dev/null @@ -1 +0,0 @@ -- Updated foundational examples and eval scripts to use `"user"` role instead of `"system"` when adding messages to `LLMContext`, since system prompts should be set via `system_instruction` on the LLM service. diff --git a/changelog/3932.added.md b/changelog/3932.added.md deleted file mode 100644 index e97690d44..000000000 --- a/changelog/3932.added.md +++ /dev/null @@ -1 +0,0 @@ -- LLM services (`BaseOpenAILLMService`, `AnthropicLLMService`, `AWSBedrockLLMService`) now log a warning when both `system_instruction` and a system message in the context are set. The constructor's `system_instruction` takes precedence. diff --git a/changelog/3936.fixed.md b/changelog/3936.fixed.md deleted file mode 100644 index 27e48815c..000000000 --- a/changelog/3936.fixed.md +++ /dev/null @@ -1 +0,0 @@ -- Fixed TTS context not being appended to the assistant message history when using `TTSSpeakFrame` with `append_to_context=True` with some TTS providers. diff --git a/changelog/3937.fixed.md b/changelog/3937.fixed.md deleted file mode 100644 index eda9d27f6..000000000 --- a/changelog/3937.fixed.md +++ /dev/null @@ -1 +0,0 @@ -- Fixed context summarization leaving orphaned tool responses in the kept context when tool calls were moved to the summarized portion. diff --git a/changelog/3946.added.md b/changelog/3946.added.md deleted file mode 100644 index 6aabfefb2..000000000 --- a/changelog/3946.added.md +++ /dev/null @@ -1 +0,0 @@ -- Runtime settings updates (via `STTUpdateSettingsFrame`) now work for AWS Transcribe, Azure, Cartesia, Deepgram, ElevenLabs Realtime, Gradium, and Soniox STT services. Previously, changing settings at runtime only stored the new values without reconnecting. diff --git a/changelog/3947.added.md b/changelog/3947.added.md deleted file mode 100644 index 175cb87bd..000000000 --- a/changelog/3947.added.md +++ /dev/null @@ -1 +0,0 @@ -- Exposed `on_summary_applied` event on `LLMAssistantAggregator`, allowing users to listen for context summarization events without accessing private members. diff --git a/changelog/3953.added.md b/changelog/3953.added.md deleted file mode 100644 index f30c31733..000000000 --- a/changelog/3953.added.md +++ /dev/null @@ -1 +0,0 @@ -- Deepgram Flux STT settings (`keyterm`, `eot_threshold`, `eager_eot_threshold`, `eot_timeout_ms`) can now be updated mid-stream via `STTUpdateSettingsFrame` without triggering a reconnect. The new values are sent to Deepgram as a Configure WebSocket message on the existing connection. diff --git a/changelog/3956.fixed.md b/changelog/3956.fixed.md deleted file mode 100644 index c8af8db91..000000000 --- a/changelog/3956.fixed.md +++ /dev/null @@ -1 +0,0 @@ -- Fixed turn completion state not resetting at end of LLM responses. `LLMFullResponseEndFrame` is pushed (not received) by the LLM service, so the mixin now handles it in `push_frame` instead of `process_frame`. diff --git a/changelog/3957.fixed.md b/changelog/3957.fixed.md deleted file mode 100644 index a501aecb1..000000000 --- a/changelog/3957.fixed.md +++ /dev/null @@ -1 +0,0 @@ -- Fixed turn completion instructions being injected as a context system message instead of using `system_instruction`. This caused warning spam when `system_instruction` was also set and didn't persist across full context updates. diff --git a/changelog/3959.fixed.md b/changelog/3959.fixed.md deleted file mode 100644 index a10521d92..000000000 --- a/changelog/3959.fixed.md +++ /dev/null @@ -1 +0,0 @@ -- Fixed `on_call_state_updated` event handler in LiveKit transport receiving incorrect number of arguments due to redundant `self` passed to `_call_event_handler`. diff --git a/changelog/3962.fixed.md b/changelog/3962.fixed.md deleted file mode 100644 index 1d326cd05..000000000 --- a/changelog/3962.fixed.md +++ /dev/null @@ -1 +0,0 @@ -- Fixed `SmallWebRTCConnection` silently discarding messages sent before the data channel is open by queuing them and flushing once the channel is ready. A bounded queue (`MAX_MESSAGE_QUEUE_SIZE = 50`) prevents unbounded memory growth, and a 10-second timeout after connection clears the queue and falls back to discard mode if the data channel never opens. diff --git a/changelog/3967.fixed.md b/changelog/3967.fixed.md deleted file mode 100644 index dff880c2c..000000000 --- a/changelog/3967.fixed.md +++ /dev/null @@ -1 +0,0 @@ -- Fixed `AzureSTTService` failing to initialize when `private_endpoint` is provided. The Azure Speech SDK's `SpeechConfig` does not accept both `region` and `endpoint` simultaneously, so they are now passed conditionally. diff --git a/changelog/4029.added.2.md b/changelog/4029.added.2.md new file mode 100644 index 000000000..1ae691442 --- /dev/null +++ b/changelog/4029.added.2.md @@ -0,0 +1 @@ +- Added `frame_order` parameter to `SyncParallelPipeline`. Set `frame_order=FrameOrder.PIPELINE` to push synchronized output frames in pipeline definition order (all frames from the first pipeline, then the second, etc.) instead of the default arrival order. diff --git a/changelog/4029.added.md b/changelog/4029.added.md new file mode 100644 index 000000000..ba3714483 --- /dev/null +++ b/changelog/4029.added.md @@ -0,0 +1 @@ +- Added `sync_with_audio` field to `OutputImageRawFrame`. When set to `True`, the output transport queues image frames with audio so they are displayed only after all preceding audio has been sent, enabling synchronized audio/image playback. diff --git a/changelog/4029.fixed.3.md b/changelog/4029.fixed.3.md new file mode 100644 index 000000000..3c812d590 --- /dev/null +++ b/changelog/4029.fixed.3.md @@ -0,0 +1 @@ +- Fixed `SyncParallelPipeline` breaking the Whisker debugger. diff --git a/changelog/4029.fixed.md b/changelog/4029.fixed.md new file mode 100644 index 000000000..57930a997 --- /dev/null +++ b/changelog/4029.fixed.md @@ -0,0 +1 @@ +- Fixed `SyncParallelPipeline` race condition where concurrent SystemFrame processing (e.g. from RTVI) could corrupt sink queues and cause deadlocks. SystemFrames now take a fast path that passes them through without draining queued output. diff --git a/changelog/4074.added.md b/changelog/4074.added.md new file mode 100644 index 000000000..c27a8e3cf --- /dev/null +++ b/changelog/4074.added.md @@ -0,0 +1 @@ +- Added `OpenAIResponsesLLMService`, a new LLM service that uses the OpenAI Responses API. Supports streaming text, function calling, usage metrics, and out-of-band inference. Works with the universal `LLMContext` and `LLMContextAggregatorPair`. See `examples/foundational/07-interruptible-openai-responses.py` and `14-function-calling-openai-responses.py`. diff --git a/changelog/4075.fixed.md b/changelog/4075.fixed.md new file mode 100644 index 000000000..97870d4bb --- /dev/null +++ b/changelog/4075.fixed.md @@ -0,0 +1 @@ +- Fixed TTS frame ordering so that non-system frames always arrive in correct order relative to the `TTSStartedFrame`/`TTSAudioRawFrame`/`TTSStoppedFrame` sequence. Previously these frames could race ahead of or behind audio context frames, producing out-of-order output downstream. diff --git a/examples/foundational/02-llm-say-one-thing.py b/examples/foundational/02-llm-say-one-thing.py index cebea9dcf..d699277ce 100644 --- a/examples/foundational/02-llm-say-one-thing.py +++ b/examples/foundational/02-llm-say-one-thing.py @@ -47,7 +47,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are an LLM in a WebRTC session, and this is a 'hello world' demo.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/04-transports-small-webrtc.py b/examples/foundational/04-transports-small-webrtc.py index ba4749919..cc768a751 100644 --- a/examples/foundational/04-transports-small-webrtc.py +++ b/examples/foundational/04-transports-small-webrtc.py @@ -75,7 +75,7 @@ async def run_example(webrtc_connection: SmallWebRTCConnection): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/04a-transports-daily.py b/examples/foundational/04a-transports-daily.py index ab986c22f..83bc27aca 100644 --- a/examples/foundational/04a-transports-daily.py +++ b/examples/foundational/04a-transports-daily.py @@ -58,8 +58,7 @@ async def main(): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - model="gpt-4o", - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/04b-transports-livekit.py b/examples/foundational/04b-transports-livekit.py index 09262f767..b2ec6b49c 100644 --- a/examples/foundational/04b-transports-livekit.py +++ b/examples/foundational/04b-transports-livekit.py @@ -58,7 +58,7 @@ async def main(): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/05-sync-speech-and-image.py b/examples/foundational/05-sync-speech-and-image.py index 1e4cf34a4..f0e2ff9c7 100644 --- a/examples/foundational/05-sync-speech-and-image.py +++ b/examples/foundational/05-sync-speech-and-image.py @@ -16,11 +16,12 @@ from pipecat.frames.frames import ( Frame, LLMContextFrame, LLMFullResponseStartFrame, + OutputImageRawFrame, TextFrame, ) from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner -from pipecat.pipeline.sync_parallel_pipeline import SyncParallelPipeline +from pipecat.pipeline.sync_parallel_pipeline import FrameOrder, SyncParallelPipeline from pipecat.pipeline.task import PipelineTask from pipecat.processors.aggregators.llm_context import LLMContext from pipecat.processors.aggregators.sentence import SentenceAggregator @@ -30,6 +31,7 @@ from pipecat.runner.utils import create_transport from pipecat.services.cartesia.tts import CartesiaHttpTTSService from pipecat.services.fal.image import FalImageGenService from pipecat.services.openai.llm import OpenAILLMService +from pipecat.services.tts_service import TextAggregationMode from pipecat.transports.base_transport import BaseTransport, TransportParams from pipecat.transports.daily.transport import DailyParams @@ -44,6 +46,18 @@ class MonthFrame(DataFrame): return f"{self.name}(month: {self.month})" +class MarkImageForPlaybackSync(FrameProcessor): + """Marks output image frames to be synchronized with audio playback.""" + + async def process_frame(self, frame: Frame, direction: FrameDirection): + await super().process_frame(frame, direction) + + if isinstance(frame, OutputImageRawFrame): + frame.sync_with_audio = True + + await self.push_frame(frame, direction) + + class MonthPrepender(FrameProcessor): def __init__(self): super().__init__() @@ -101,6 +115,10 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): settings=CartesiaHttpTTSService.Settings( voice="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady ), + # No need to aggregate by sentences (the default), as we already know we're getting full sentences + # (Otherwise the service will unnecessarily wait for follow-up input to confirm the sentence is complete, + # which, sadly, actually breaks the synchronization mechanism) + text_aggregation_mode=TextAggregationMode.TOKEN, ) imagegen = FalImageGenService( @@ -119,17 +137,26 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): # that, each pipeline runs concurrently and `SyncParallelPipeline` will # wait for the input frame to be processed. # + # We use `FrameOrder.PIPELINE` so that each synchronized batch of output + # frames is pushed in the order the pipelines are listed: image first, + # then audio. This ensures the transport receives the image before the + # audio frames it should accompany. + # # Note that `SyncParallelPipeline` requires the last processor in each # of the pipelines to be synchronous. In this case, we use - # `CartesiaHttpTTSService` and `FalImageGenService` which make HTTP + # `FalImageGenService` and `CartesiaHttpTTSService` which make HTTP # requests and wait for the response. pipeline = Pipeline( [ llm, # LLM sentence_aggregator, # Aggregates LLM output into full sentences SyncParallelPipeline( # Run pipelines in parallel aggregating the result + [ + imagegen, # Generate image + MarkImageForPlaybackSync(), # Mark image as needing sync w/audio during playback + ], [month_prepender, tts], # Create "Month: sentence" and output audio - [imagegen], # Generate image + frame_order=FrameOrder.PIPELINE, ), transport.output(), # Transport output ] diff --git a/examples/foundational/05a-local-sync-speech-and-image.py b/examples/foundational/05a-local-sync-speech-and-image.py deleted file mode 100644 index d0c5a103a..000000000 --- a/examples/foundational/05a-local-sync-speech-and-image.py +++ /dev/null @@ -1,202 +0,0 @@ -# -# Copyright (c) 2024-2026, Daily -# -# SPDX-License-Identifier: BSD 2-Clause License -# - -import asyncio -import os -import sys -import tkinter as tk - -import aiohttp -from dotenv import load_dotenv -from loguru import logger - -from pipecat.frames.frames import ( - Frame, - LLMContextFrame, - OutputAudioRawFrame, - TextFrame, - TTSAudioRawFrame, - URLImageRawFrame, -) -from pipecat.pipeline.pipeline import Pipeline -from pipecat.pipeline.runner import PipelineRunner -from pipecat.pipeline.sync_parallel_pipeline import SyncParallelPipeline -from pipecat.pipeline.task import PipelineTask -from pipecat.processors.aggregators.llm_context import LLMContext -from pipecat.processors.aggregators.sentence import SentenceAggregator -from pipecat.processors.frame_processor import FrameDirection, FrameProcessor -from pipecat.services.cartesia.tts import CartesiaHttpTTSService -from pipecat.services.fal.image import FalImageGenService -from pipecat.services.openai.llm import OpenAILLMService -from pipecat.transports.local.tk import TkLocalTransport, TkTransportParams - -load_dotenv(override=True) - -logger.remove(0) -logger.add(sys.stderr, level="DEBUG") - - -async def main(): - async with aiohttp.ClientSession() as session: - tk_root = tk.Tk() - tk_root.title("Calendar") - - runner = PipelineRunner() - - async def get_month_data(month): - messages = [ - { - "role": "user", - "content": f"Describe a nature photograph suitable for use in a calendar, for the month of {month}. Include only the image description with no preamble. Limit the description to one sentence, please.", - } - ] - - class ImageDescription(FrameProcessor): - def __init__(self): - super().__init__() - self.text = "" - - async def process_frame(self, frame: Frame, direction: FrameDirection): - await super().process_frame(frame, direction) - - if isinstance(frame, TextFrame): - self.text = frame.text - await self.push_frame(frame, direction) - - class AudioGrabber(FrameProcessor): - def __init__(self): - super().__init__() - self.audio = bytearray() - self.frame = None - - async def process_frame(self, frame: Frame, direction: FrameDirection): - await super().process_frame(frame, direction) - - if isinstance(frame, TTSAudioRawFrame): - self.audio.extend(frame.audio) - self.frame = OutputAudioRawFrame( - bytes(self.audio), frame.sample_rate, frame.num_channels - ) - await self.push_frame(frame, direction) - - class ImageGrabber(FrameProcessor): - def __init__(self): - super().__init__() - self.frame = None - - async def process_frame(self, frame: Frame, direction: FrameDirection): - await super().process_frame(frame, direction) - - if isinstance(frame, URLImageRawFrame): - self.frame = frame - await self.push_frame(frame, direction) - - llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY")) - - tts = CartesiaHttpTTSService( - api_key=os.getenv("CARTESIA_API_KEY"), - settings=CartesiaHttpTTSService.Settings( - voice="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady - ), - ) - - imagegen = FalImageGenService( - settings=FalImageGenService.Settings( - image_size="square_hd", - ), - aiohttp_session=session, - key=os.getenv("FAL_KEY"), - ) - - sentence_aggregator = SentenceAggregator() - - description = ImageDescription() - - audio_grabber = AudioGrabber() - - image_grabber = ImageGrabber() - - # With `SyncParallelPipeline` we synchronize audio and images by - # pushing them basically in order (e.g. I1 A1 A1 A1 I2 A2 A2 A2 A2 - # I3 A3). To do that, each pipeline runs concurrently and - # `SyncParallelPipeline` will wait for the input frame to be - # processed. - # - # Note that `SyncParallelPipeline` requires the last processor in - # each of the pipelines to be synchronous. In this case, we use - # `CartesiaHttpTTSService` and `FalImageGenService` which make HTTP - # requests and wait for the response. - pipeline = Pipeline( - [ - llm, # LLM - sentence_aggregator, # Aggregates LLM output into full sentences - description, # Store sentence - SyncParallelPipeline( - [tts, audio_grabber], # Generate and store audio for the given sentence - [imagegen, image_grabber], # Generate and storeimage for the given sentence - ), - ] - ) - - task = PipelineTask(pipeline) - await task.queue_frame(LLMContextFrame(LLMContext(messages))) - await task.stop_when_done() - - await runner.run(task) - - return { - "month": month, - "text": description.text, - "image": image_grabber.frame, - "audio": audio_grabber.frame, - } - - transport = TkLocalTransport( - tk_root, - TkTransportParams( - audio_out_enabled=True, - video_out_enabled=True, - video_out_width=1024, - video_out_height=1024, - ), - ) - - pipeline = Pipeline([transport.output()]) - - task = PipelineTask(pipeline) - - # We only specify a few months as we create tasks all at once and we - # might get rate limited otherwise. - months: list[str] = [ - "January", - "February", - ] - - # We create one task per month. This will be executed concurrently. - month_tasks = [asyncio.create_task(get_month_data(month)) for month in months] - - # Now we wait for each month task in the order they're completed. The - # benefit is we'll have as little delay as possible before the first - # month, and likely no delay between months, but the months won't - # display in order. - async def show_images(month_tasks): - for month_data_task in asyncio.as_completed(month_tasks): - data = await month_data_task - await task.queue_frames([data["image"], data["audio"]]) - - await runner.stop_when_done() - - async def run_tk(): - while not task.has_finished(): - tk_root.update() - tk_root.update_idletasks() - await asyncio.sleep(0.1) - - await asyncio.gather(runner.run(task), show_images(month_tasks), run_tk()) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/examples/foundational/06-listen-and-respond.py b/examples/foundational/06-listen-and-respond.py index 7a73ad876..769975b9a 100644 --- a/examples/foundational/06-listen-and-respond.py +++ b/examples/foundational/06-listen-and-respond.py @@ -91,7 +91,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/06a-image-sync.py b/examples/foundational/06a-image-sync.py index 3f80380dc..473441fac 100644 --- a/examples/foundational/06a-image-sync.py +++ b/examples/foundational/06a-image-sync.py @@ -108,7 +108,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07-interruptible-cartesia-http.py b/examples/foundational/07-interruptible-cartesia-http.py index be6bc07a6..6b3daecf0 100644 --- a/examples/foundational/07-interruptible-cartesia-http.py +++ b/examples/foundational/07-interruptible-cartesia-http.py @@ -67,7 +67,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07-interruptible-openai-responses.py b/examples/foundational/07-interruptible-openai-responses.py new file mode 100644 index 000000000..baae3754a --- /dev/null +++ b/examples/foundational/07-interruptible-openai-responses.py @@ -0,0 +1,125 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import os + +from dotenv import load_dotenv +from loguru import logger + +from pipecat.audio.vad.silero import SileroVADAnalyzer +from pipecat.frames.frames import LLMRunFrame +from pipecat.pipeline.pipeline import Pipeline +from pipecat.pipeline.runner import PipelineRunner +from pipecat.pipeline.task import PipelineParams, PipelineTask +from pipecat.processors.aggregators.llm_context import LLMContext +from pipecat.processors.aggregators.llm_response_universal import ( + LLMContextAggregatorPair, + LLMUserAggregatorParams, +) +from pipecat.runner.types import RunnerArguments +from pipecat.runner.utils import create_transport +from pipecat.services.cartesia.tts import CartesiaTTSService +from pipecat.services.deepgram.stt import DeepgramSTTService +from pipecat.services.openai.responses.llm import OpenAIResponsesLLMService +from pipecat.transports.base_transport import BaseTransport, TransportParams +from pipecat.transports.daily.transport import DailyParams +from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams + +load_dotenv(override=True) + +# We use lambdas to defer transport parameter creation until the transport +# type is selected at runtime. +transport_params = { + "daily": lambda: DailyParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), + "twilio": lambda: FastAPIWebsocketParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), + "webrtc": lambda: TransportParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), +} + + +async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): + logger.info(f"Starting bot") + + stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY")) + + tts = CartesiaTTSService( + api_key=os.getenv("CARTESIA_API_KEY"), + settings=CartesiaTTSService.Settings( + voice="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady + ), + ) + + llm = OpenAIResponsesLLMService( + api_key=os.getenv("OPENAI_API_KEY"), + settings=OpenAIResponsesLLMService.Settings( + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", + ), + ) + + context = LLMContext() + user_aggregator, assistant_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) + + pipeline = Pipeline( + [ + transport.input(), # Transport user input + stt, + user_aggregator, # User responses + llm, # LLM + tts, # TTS + transport.output(), # Transport bot output + assistant_aggregator, # Assistant spoken responses + ] + ) + + task = PipelineTask( + pipeline, + params=PipelineParams( + enable_metrics=True, + enable_usage_metrics=True, + ), + idle_timeout_secs=runner_args.pipeline_idle_timeout_secs, + ) + + @transport.event_handler("on_client_connected") + async def on_client_connected(transport, client): + logger.info(f"Client connected") + # Kick off the conversation. + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) + await task.queue_frames([LLMRunFrame()]) + + @transport.event_handler("on_client_disconnected") + async def on_client_disconnected(transport, client): + logger.info(f"Client disconnected") + await task.cancel() + + runner = PipelineRunner(handle_sigint=runner_args.handle_sigint) + + await runner.run(task) + + +async def bot(runner_args: RunnerArguments): + """Main bot entry point compatible with Pipecat Cloud.""" + transport = await create_transport(runner_args, transport_params) + await run_bot(transport, runner_args) + + +if __name__ == "__main__": + from pipecat.runner.run import main + + main() diff --git a/examples/foundational/07-interruptible.py b/examples/foundational/07-interruptible.py index 17aec31e1..c0b410f50 100644 --- a/examples/foundational/07-interruptible.py +++ b/examples/foundational/07-interruptible.py @@ -63,7 +63,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07a-interruptible-speechmatics-vad.py b/examples/foundational/07a-interruptible-speechmatics-vad.py index cece246d8..327701ff2 100644 --- a/examples/foundational/07a-interruptible-speechmatics-vad.py +++ b/examples/foundational/07a-interruptible-speechmatics-vad.py @@ -113,7 +113,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( temperature=0.75, - system_instruction="You are a helpful British assistant called Sarah. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Always include punctuation in your responses. Give very short replies - do not give longer replies unless strictly necessary. Respond to what the user said in a concise, funny, creative and helpful way. Use `` tags to identify different speakers - do not use tags in your replies. Do not respond to speakers within `` tags unless explicitly asked to.", + system_instruction="You are a helpful British assistant called Sarah in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Always include punctuation in your responses. Give very short replies - do not give longer replies unless strictly necessary. Respond to what the user said in a concise, funny, creative and helpful way. Use `` tags to identify different speakers - do not use tags in your replies. Do not respond to speakers within `` tags unless explicitly asked to.", ), ) diff --git a/examples/foundational/07a-interruptible-speechmatics.py b/examples/foundational/07a-interruptible-speechmatics.py index 3d6395f73..5181bf36f 100644 --- a/examples/foundational/07a-interruptible-speechmatics.py +++ b/examples/foundational/07a-interruptible-speechmatics.py @@ -93,7 +93,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( temperature=0.75, - system_instruction="You are a helpful British assistant called Sarah. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Always include punctuation in your responses. Give very short replies - do not give longer replies unless strictly necessary. Respond to what the user said in a concise, funny, creative and helpful way. Use `` tags to identify different speakers - do not use tags in your replies. Do not respond to speakers within `` tags unless explicitly asked to.", + system_instruction="You are a helpful British assistant called Sarah in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Always include punctuation in your responses. Give very short replies - do not give longer replies unless strictly necessary. Respond to what the user said in a concise, funny, creative and helpful way. Use `` tags to identify different speakers - do not use tags in your replies. Do not respond to speakers within `` tags unless explicitly asked to.", ), ) diff --git a/examples/foundational/07b-interruptible-langchain.py b/examples/foundational/07b-interruptible-langchain.py index 1a85bf7ad..d78e678a2 100644 --- a/examples/foundational/07b-interruptible-langchain.py +++ b/examples/foundational/07b-interruptible-langchain.py @@ -80,8 +80,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): [ ( "system", - "Be nice and helpful. Answer very briefly and without special characters like `#` or `*`. " - "Your response will be synthesized to voice and those characters will create unnatural sounds.", + "You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), MessagesPlaceholder("chat_history"), ("human", "{input}"), diff --git a/examples/foundational/07c-interruptible-deepgram-flux.py b/examples/foundational/07c-interruptible-deepgram-flux.py index da027b4f8..2c4e63bb7 100644 --- a/examples/foundational/07c-interruptible-deepgram-flux.py +++ b/examples/foundational/07c-interruptible-deepgram-flux.py @@ -71,7 +71,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07c-interruptible-deepgram-http.py b/examples/foundational/07c-interruptible-deepgram-http.py index ee6733165..cb1026d7f 100644 --- a/examples/foundational/07c-interruptible-deepgram-http.py +++ b/examples/foundational/07c-interruptible-deepgram-http.py @@ -68,7 +68,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07c-interruptible-deepgram-sagemaker.py b/examples/foundational/07c-interruptible-deepgram-sagemaker.py index 91b1c95aa..b3b53b3db 100644 --- a/examples/foundational/07c-interruptible-deepgram-sagemaker.py +++ b/examples/foundational/07c-interruptible-deepgram-sagemaker.py @@ -79,7 +79,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): settings=AWSBedrockLLMSettings( model="us.amazon.nova-pro-v1:0", temperature=0.8, - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07c-interruptible-deepgram-vad.py b/examples/foundational/07c-interruptible-deepgram-vad.py index 0a4dc0125..420ec795b 100644 --- a/examples/foundational/07c-interruptible-deepgram-vad.py +++ b/examples/foundational/07c-interruptible-deepgram-vad.py @@ -71,7 +71,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07c-interruptible-deepgram.py b/examples/foundational/07c-interruptible-deepgram.py index e00a9f191..a4d94f915 100644 --- a/examples/foundational/07c-interruptible-deepgram.py +++ b/examples/foundational/07c-interruptible-deepgram.py @@ -65,7 +65,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07d-interruptible-elevenlabs-http.py b/examples/foundational/07d-interruptible-elevenlabs-http.py index cf60ac3e7..a83df1465 100644 --- a/examples/foundational/07d-interruptible-elevenlabs-http.py +++ b/examples/foundational/07d-interruptible-elevenlabs-http.py @@ -72,7 +72,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07d-interruptible-elevenlabs.py b/examples/foundational/07d-interruptible-elevenlabs.py index d14bc71cd..ad1873788 100644 --- a/examples/foundational/07d-interruptible-elevenlabs.py +++ b/examples/foundational/07d-interruptible-elevenlabs.py @@ -65,7 +65,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07f-interruptible-azure-http.py b/examples/foundational/07f-interruptible-azure-http.py index 38a2410fc..c372c7c7e 100644 --- a/examples/foundational/07f-interruptible-azure-http.py +++ b/examples/foundational/07f-interruptible-azure-http.py @@ -67,7 +67,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): endpoint=os.getenv("AZURE_CHATGPT_ENDPOINT"), settings=AzureLLMService.Settings( model=os.getenv("AZURE_CHATGPT_MODEL"), - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07f-interruptible-azure.py b/examples/foundational/07f-interruptible-azure.py index b7ace8548..6a0b39bb5 100644 --- a/examples/foundational/07f-interruptible-azure.py +++ b/examples/foundational/07f-interruptible-azure.py @@ -67,7 +67,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): endpoint=os.getenv("AZURE_CHATGPT_ENDPOINT"), settings=AzureLLMService.Settings( model=os.getenv("AZURE_CHATGPT_MODEL"), - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07h-interruptible-openpipe.py b/examples/foundational/07h-interruptible-openpipe.py index eaaa6fedd..8d31125f2 100644 --- a/examples/foundational/07h-interruptible-openpipe.py +++ b/examples/foundational/07h-interruptible-openpipe.py @@ -68,7 +68,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): openpipe_api_key=os.getenv("OPENPIPE_API_KEY"), tags={"conversation_id": f"pipecat-{timestamp}"}, settings=OpenPipeLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07i-interruptible-xtts.py b/examples/foundational/07i-interruptible-xtts.py index 3246d5da9..0f51ff03e 100644 --- a/examples/foundational/07i-interruptible-xtts.py +++ b/examples/foundational/07i-interruptible-xtts.py @@ -68,7 +68,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07j-interruptible-gladia-vad.py b/examples/foundational/07j-interruptible-gladia-vad.py index b25ab7275..ec3cc80e1 100644 --- a/examples/foundational/07j-interruptible-gladia-vad.py +++ b/examples/foundational/07j-interruptible-gladia-vad.py @@ -76,7 +76,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY", ""), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07j-interruptible-gladia.py b/examples/foundational/07j-interruptible-gladia.py index d1fee759b..5ab2e16a3 100644 --- a/examples/foundational/07j-interruptible-gladia.py +++ b/examples/foundational/07j-interruptible-gladia.py @@ -74,7 +74,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY", ""), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07k-interruptible-lmnt.py b/examples/foundational/07k-interruptible-lmnt.py index a41f1ae2e..c6f931413 100644 --- a/examples/foundational/07k-interruptible-lmnt.py +++ b/examples/foundational/07k-interruptible-lmnt.py @@ -64,7 +64,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07l-interruptible-groq.py b/examples/foundational/07l-interruptible-groq.py index 208d39c2d..59e1a7fca 100644 --- a/examples/foundational/07l-interruptible-groq.py +++ b/examples/foundational/07l-interruptible-groq.py @@ -57,8 +57,8 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = GroqLLMService( api_key=os.getenv("GROQ_API_KEY"), settings=GroqLLMService.Settings( - model="meta-llama/llama-4-maverick-17b-128e-instruct", - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + model="llama-3.1-8b-instant", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07m-interruptible-aws.py b/examples/foundational/07m-interruptible-aws.py index ff3b6a789..ca44fb448 100644 --- a/examples/foundational/07m-interruptible-aws.py +++ b/examples/foundational/07m-interruptible-aws.py @@ -66,7 +66,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): settings=AWSBedrockLLMService.Settings( model="us.anthropic.claude-sonnet-4-6", temperature=0.8, - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07n-interruptible-gemini-image.py b/examples/foundational/07n-interruptible-gemini-image.py index ad292ab03..461e2d8fa 100644 --- a/examples/foundational/07n-interruptible-gemini-image.py +++ b/examples/foundational/07n-interruptible-gemini-image.py @@ -89,7 +89,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): settings=GoogleLLMService.Settings( model="gemini-2.5-flash-image", # model="gemini-3-pro-image-preview", # A more powerful model, but slower, - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07n-interruptible-gemini.py b/examples/foundational/07n-interruptible-gemini.py index 3d8857708..00290bd3a 100644 --- a/examples/foundational/07n-interruptible-gemini.py +++ b/examples/foundational/07n-interruptible-gemini.py @@ -74,7 +74,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): api_key=os.getenv("GOOGLE_API_KEY"), model="gemini-2.5-flash", settings=GoogleLLMService.Settings( - system_instruction="""You are a helpful AI assistant in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. + system_instruction="""You are a helpful assistant in a voice conversation. IMPORTANT: You're using Gemini TTS which supports expressive markup tags. You can use these tags in your responses: - [sigh] - Insert a sigh sound @@ -91,7 +91,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): - "[whispering] Let me tell you a secret." - "The answer is... [long pause] ...42!" - Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.""", + Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Keep responses concise. Respond to what the user said in a creative and helpful way.""", ), ) diff --git a/examples/foundational/07n-interruptible-google-http.py b/examples/foundational/07n-interruptible-google-http.py index 91e822ec0..d627f431f 100644 --- a/examples/foundational/07n-interruptible-google-http.py +++ b/examples/foundational/07n-interruptible-google-http.py @@ -73,11 +73,11 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = GoogleLLMService( api_key=os.getenv("GOOGLE_API_KEY"), - settings=GoogleLLMService.GoogleLLMSettings( + settings=GoogleLLMService.Settings( model="gemini-2.5-flash", # force a certain amount of thinking if you want it # thinking=GoogleLLMService.ThinkingConfig(thinking_budget=4096) - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07n-interruptible-google.py b/examples/foundational/07n-interruptible-google.py index 2f148bb13..f8ec7b037 100644 --- a/examples/foundational/07n-interruptible-google.py +++ b/examples/foundational/07n-interruptible-google.py @@ -77,7 +77,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): model="gemini-2.5-flash", # force a certain amount of thinking if you want it # thinking=GoogleLLMService.ThinkingConfig(thinking_budget=4096), - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07o-interruptible-assemblyai-turn-detection.py b/examples/foundational/07o-interruptible-assemblyai-turn-detection.py index 7f8c8169e..14ec1af21 100644 --- a/examples/foundational/07o-interruptible-assemblyai-turn-detection.py +++ b/examples/foundational/07o-interruptible-assemblyai-turn-detection.py @@ -115,7 +115,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07o-interruptible-assemblyai.py b/examples/foundational/07o-interruptible-assemblyai.py index 700ec404e..dc8ecee12 100644 --- a/examples/foundational/07o-interruptible-assemblyai.py +++ b/examples/foundational/07o-interruptible-assemblyai.py @@ -67,7 +67,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07p-interruptible-krisp-viva.py b/examples/foundational/07p-interruptible-krisp-viva.py index b9eda804c..2b8e7f2f0 100644 --- a/examples/foundational/07p-interruptible-krisp-viva.py +++ b/examples/foundational/07p-interruptible-krisp-viva.py @@ -93,7 +93,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07p-interruptible-krisp.py b/examples/foundational/07p-interruptible-krisp.py index 800bc6fbc..229d50d17 100644 --- a/examples/foundational/07p-interruptible-krisp.py +++ b/examples/foundational/07p-interruptible-krisp.py @@ -68,7 +68,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07q-interruptible-rime-http.py b/examples/foundational/07q-interruptible-rime-http.py index f143ee287..f661c112b 100644 --- a/examples/foundational/07q-interruptible-rime-http.py +++ b/examples/foundational/07q-interruptible-rime-http.py @@ -71,7 +71,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07q-interruptible-rime.py b/examples/foundational/07q-interruptible-rime.py index dde507ee1..694f25c25 100644 --- a/examples/foundational/07q-interruptible-rime.py +++ b/examples/foundational/07q-interruptible-rime.py @@ -64,7 +64,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07r-interruptible-nvidia.py b/examples/foundational/07r-interruptible-nvidia.py index 2e69dd50f..ed0918ec1 100644 --- a/examples/foundational/07r-interruptible-nvidia.py +++ b/examples/foundational/07r-interruptible-nvidia.py @@ -58,7 +58,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): api_key=os.getenv("NVIDIA_API_KEY"), settings=NvidiaLLMService.Settings( model="meta/llama-3.3-70b-instruct", - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07s-interruptible-google-audio-in.py b/examples/foundational/07s-interruptible-google-audio-in.py index 6de7e1966..3f92872f0 100644 --- a/examples/foundational/07s-interruptible-google-audio-in.py +++ b/examples/foundational/07s-interruptible-google-audio-in.py @@ -48,7 +48,7 @@ load_dotenv(override=True) marker = "|----|" system_message = f""" -You are a helpful LLM in a WebRTC call. Your goals are to be helpful and brief in your responses. +You are a helpful LLM in a voice call. Your goals are to be helpful and brief in your responses. You are expert at transcribing audio to text. You will receive a mixture of audio and text input. When asked to transcribe what the user said, output an exact, word-for-word transcription. diff --git a/examples/foundational/07t-interruptible-fish.py b/examples/foundational/07t-interruptible-fish.py index d51e3a71e..13612a887 100644 --- a/examples/foundational/07t-interruptible-fish.py +++ b/examples/foundational/07t-interruptible-fish.py @@ -65,7 +65,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07v-interruptible-neuphonic-http.py b/examples/foundational/07v-interruptible-neuphonic-http.py index bc2bbe983..ad5b7e996 100644 --- a/examples/foundational/07v-interruptible-neuphonic-http.py +++ b/examples/foundational/07v-interruptible-neuphonic-http.py @@ -69,7 +69,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07v-interruptible-neuphonic.py b/examples/foundational/07v-interruptible-neuphonic.py index b44632286..ba3350754 100644 --- a/examples/foundational/07v-interruptible-neuphonic.py +++ b/examples/foundational/07v-interruptible-neuphonic.py @@ -64,7 +64,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07w-interruptible-fal.py b/examples/foundational/07w-interruptible-fal.py index 89a38d23c..08f24fd79 100644 --- a/examples/foundational/07w-interruptible-fal.py +++ b/examples/foundational/07w-interruptible-fal.py @@ -70,7 +70,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07x-interruptible-local.py b/examples/foundational/07x-interruptible-local.py index e0ace854b..28e970403 100644 --- a/examples/foundational/07x-interruptible-local.py +++ b/examples/foundational/07x-interruptible-local.py @@ -52,7 +52,7 @@ async def main(): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07y-interruptible-minimax.py b/examples/foundational/07y-interruptible-minimax.py index d9bcef371..f8323369e 100644 --- a/examples/foundational/07y-interruptible-minimax.py +++ b/examples/foundational/07y-interruptible-minimax.py @@ -71,7 +71,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07z-interruptible-sarvam-http.py b/examples/foundational/07z-interruptible-sarvam-http.py index 76c892fd9..f8a806493 100644 --- a/examples/foundational/07z-interruptible-sarvam-http.py +++ b/examples/foundational/07z-interruptible-sarvam-http.py @@ -75,7 +75,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07z-interruptible-sarvam.py b/examples/foundational/07z-interruptible-sarvam.py index 915ea2c3d..5f144f73f 100644 --- a/examples/foundational/07z-interruptible-sarvam.py +++ b/examples/foundational/07z-interruptible-sarvam.py @@ -69,7 +69,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07za-interruptible-soniox.py b/examples/foundational/07za-interruptible-soniox.py index d425be966..c29fea9fb 100644 --- a/examples/foundational/07za-interruptible-soniox.py +++ b/examples/foundational/07za-interruptible-soniox.py @@ -71,7 +71,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07zc-interruptible-asyncai-http.py b/examples/foundational/07zc-interruptible-asyncai-http.py index e95bd60b2..96964f5e8 100644 --- a/examples/foundational/07zc-interruptible-asyncai-http.py +++ b/examples/foundational/07zc-interruptible-asyncai-http.py @@ -69,7 +69,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07zc-interruptible-asyncai.py b/examples/foundational/07zc-interruptible-asyncai.py index 40cdcf7ab..39052720c 100644 --- a/examples/foundational/07zc-interruptible-asyncai.py +++ b/examples/foundational/07zc-interruptible-asyncai.py @@ -65,7 +65,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07zd-interruptible-aicoustics.py b/examples/foundational/07zd-interruptible-aicoustics.py index 923530943..eeb45a4c7 100644 --- a/examples/foundational/07zd-interruptible-aicoustics.py +++ b/examples/foundational/07zd-interruptible-aicoustics.py @@ -85,7 +85,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07ze-interruptible-hume.py b/examples/foundational/07ze-interruptible-hume.py index ef19e3619..a5e4253f6 100644 --- a/examples/foundational/07ze-interruptible-hume.py +++ b/examples/foundational/07ze-interruptible-hume.py @@ -67,7 +67,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07zf-interruptible-gradium.py b/examples/foundational/07zf-interruptible-gradium.py index 4b7e2f32b..1a067d1b2 100644 --- a/examples/foundational/07zf-interruptible-gradium.py +++ b/examples/foundational/07zf-interruptible-gradium.py @@ -71,7 +71,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07zg-interruptible-camb.py b/examples/foundational/07zg-interruptible-camb.py index e0f764f4e..ff9b7162d 100644 --- a/examples/foundational/07zg-interruptible-camb.py +++ b/examples/foundational/07zg-interruptible-camb.py @@ -64,7 +64,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful voice assistant powered by Camb AI text-to-speech. ", + system_instruction="You are a helpful voice assistant powered by Camb AI text-to-speech. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Keep responses concise.", ), ) diff --git a/examples/foundational/07zi-interruptible-piper.py b/examples/foundational/07zi-interruptible-piper.py index 44b7d0efd..53f21811c 100644 --- a/examples/foundational/07zi-interruptible-piper.py +++ b/examples/foundational/07zi-interruptible-piper.py @@ -63,7 +63,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07zj-interruptible-kokoro.py b/examples/foundational/07zj-interruptible-kokoro.py index 0978ba720..5fb0ca55b 100644 --- a/examples/foundational/07zj-interruptible-kokoro.py +++ b/examples/foundational/07zj-interruptible-kokoro.py @@ -63,7 +63,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/07zk-interruptible-resemble.py b/examples/foundational/07zk-interruptible-resemble.py index 88bcb724e..60d1d8495 100644 --- a/examples/foundational/07zk-interruptible-resemble.py +++ b/examples/foundational/07zk-interruptible-resemble.py @@ -30,24 +30,20 @@ from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams load_dotenv(override=True) -# We store functions so objects (e.g. SileroVADAnalyzer) don't get -# instantiated. The function will be called when the desired transport gets -# selected. +# We use lambdas to defer transport parameter creation until the transport +# type is selected at runtime. transport_params = { "daily": lambda: DailyParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "twilio": lambda: FastAPIWebsocketParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "webrtc": lambda: TransportParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), } @@ -67,7 +63,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/08-custom-frame-processor.py b/examples/foundational/08-custom-frame-processor.py index b6b1ef42c..f07711657 100644 --- a/examples/foundational/08-custom-frame-processor.py +++ b/examples/foundational/08-custom-frame-processor.py @@ -103,7 +103,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/10-wake-phrase.py b/examples/foundational/10-wake-phrase.py index df74e4be4..5b93efb0e 100644 --- a/examples/foundational/10-wake-phrase.py +++ b/examples/foundational/10-wake-phrase.py @@ -19,7 +19,6 @@ from pipecat.processors.aggregators.llm_response_universal import ( LLMContextAggregatorPair, LLMUserAggregatorParams, ) -from pipecat.processors.filters.wake_check_filter import WakeCheckFilter from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport from pipecat.services.cartesia.tts import CartesiaTTSService @@ -28,6 +27,11 @@ from pipecat.services.openai.llm import OpenAILLMService from pipecat.transports.base_transport import BaseTransport, TransportParams from pipecat.transports.daily.transport import DailyParams from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams +from pipecat.turns.user_start import WakePhraseUserTurnStartStrategy +from pipecat.turns.user_turn_strategies import ( + UserTurnStrategies, + default_user_turn_start_strategies, +) load_dotenv(override=True) @@ -52,7 +56,12 @@ transport_params = { async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): logger.info(f"Starting bot") - stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY")) + stt = DeepgramSTTService( + api_key=os.getenv("DEEPGRAM_API_KEY"), + settings=DeepgramSTTService.Settings( + keyterm=["pipecat"], + ), + ) tts = CartesiaTTSService( api_key=os.getenv("CARTESIA_API_KEY"), @@ -64,23 +73,32 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful assistant. Respond to what the user said in a creative and helpful way. Keep your responses brief.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) - hey_robot_filter = WakeCheckFilter(["hey robot", "hey, robot"]) - context = LLMContext() user_aggregator, assistant_aggregator = LLMContextAggregatorPair( context, - user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + user_params=LLMUserAggregatorParams( + user_turn_strategies=UserTurnStrategies( + start=[ + WakePhraseUserTurnStartStrategy( + phrases=["pipecat"], + # Timeout before wake phrase must be spoken again + timeout=5.0, + ), + *default_user_turn_start_strategies(), + ] + ), + vad_analyzer=SileroVADAnalyzer(), + ), ) pipeline = Pipeline( [ transport.input(), # Transport user input - stt, # STT - hey_robot_filter, # Filter out speech not directed at the robot + stt, user_aggregator, # User responses llm, # LLM tts, # TTS @@ -102,12 +120,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - context.add_message( - { - "role": "user", - "content": "Please introduce yourself. Tell the user they should say 'Hey Robot' before talking to you.", - } - ) + context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/11-sound-effects.py b/examples/foundational/11-sound-effects.py index 87cb3f333..1f7fdd339 100644 --- a/examples/foundational/11-sound-effects.py +++ b/examples/foundational/11-sound-effects.py @@ -107,7 +107,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/12-describe-image-openai-responses.py b/examples/foundational/12-describe-image-openai-responses.py new file mode 100644 index 000000000..a3c113cb2 --- /dev/null +++ b/examples/foundational/12-describe-image-openai-responses.py @@ -0,0 +1,139 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + + +import os + +from dotenv import load_dotenv +from loguru import logger +from PIL import Image + +from pipecat.audio.vad.silero import SileroVADAnalyzer +from pipecat.frames.frames import LLMRunFrame +from pipecat.pipeline.pipeline import Pipeline +from pipecat.pipeline.runner import PipelineRunner +from pipecat.pipeline.task import PipelineParams, PipelineTask +from pipecat.processors.aggregators.llm_context import LLMContext +from pipecat.processors.aggregators.llm_response_universal import ( + LLMContextAggregatorPair, + LLMUserAggregatorParams, +) +from pipecat.runner.types import RunnerArguments +from pipecat.runner.utils import create_transport +from pipecat.services.cartesia.tts import CartesiaTTSService +from pipecat.services.deepgram.stt import DeepgramSTTService +from pipecat.services.openai.responses.llm import OpenAIResponsesLLMService +from pipecat.transports.base_transport import BaseTransport, TransportParams +from pipecat.transports.daily.transport import DailyParams + +load_dotenv(override=True) + + +# We use lambdas to defer transport parameter creation until the transport +# type is selected at runtime. +transport_params = { + "daily": lambda: DailyParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), + "webrtc": lambda: TransportParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), +} + + +async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): + logger.info(f"Starting bot") + + stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY")) + + tts = CartesiaTTSService( + api_key=os.getenv("CARTESIA_API_KEY"), + settings=CartesiaTTSService.Settings( + voice="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady + ), + ) + + llm = OpenAIResponsesLLMService( + api_key=os.getenv("OPENAI_API_KEY"), + settings=OpenAIResponsesLLMService.Settings( + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way. You are also able to describe images.", + ), + ) + + context = LLMContext() + user_aggregator, assistant_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) + + pipeline = Pipeline( + [ + transport.input(), # Transport user input + stt, # STT + user_aggregator, # User responses + llm, # LLM + tts, # TTS + transport.output(), # Transport bot output + assistant_aggregator, # Assistant spoken responses + ] + ) + + task = PipelineTask( + pipeline, + params=PipelineParams( + enable_metrics=True, + enable_usage_metrics=True, + ), + idle_timeout_secs=runner_args.pipeline_idle_timeout_secs, + ) + + @transport.event_handler("on_client_connected") + async def on_client_connected(transport, client): + logger.info(f"Client connected") + + if not runner_args.body: + script_dir = os.path.dirname(__file__) + runner_args.body = { + "image_path": os.path.join(script_dir, "assets", "cat.jpg"), + "question": "Describe this image", + } + + image_path = runner_args.body["image_path"] + question = runner_args.body["question"] + + # Kick off the conversation. + image = Image.open(image_path) + message = await LLMContext.create_image_message( + image=image.tobytes(), + format="RGB", + size=image.size, + text=question, + ) + context.add_message(message) + await task.queue_frames([LLMRunFrame()]) + + @transport.event_handler("on_client_disconnected") + async def on_client_disconnected(transport, client): + logger.info(f"Client disconnected") + await task.cancel() + + runner = PipelineRunner(handle_sigint=runner_args.handle_sigint) + + await runner.run(task) + + +async def bot(runner_args: RunnerArguments): + """Main bot entry point compatible with Pipecat Cloud.""" + transport = await create_transport(runner_args, transport_params) + await run_bot(transport, runner_args) + + +if __name__ == "__main__": + from pipecat.runner.run import main + + main() diff --git a/examples/foundational/12-describe-image-openai.py b/examples/foundational/12-describe-image-openai.py index 0888fa6bf..8c8af6352 100644 --- a/examples/foundational/12-describe-image-openai.py +++ b/examples/foundational/12-describe-image-openai.py @@ -61,7 +61,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way. You are also able to describe images.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way. You are also able to describe images.", ), ) diff --git a/examples/foundational/12a-describe-image-anthropic.py b/examples/foundational/12a-describe-image-anthropic.py index 086e282dc..642885dcf 100644 --- a/examples/foundational/12a-describe-image-anthropic.py +++ b/examples/foundational/12a-describe-image-anthropic.py @@ -61,7 +61,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = AnthropicLLMService( api_key=os.getenv("ANTHROPIC_API_KEY"), settings=AnthropicLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way. You are also able to describe images.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way. You are also able to describe images.", ), ) diff --git a/examples/foundational/12b-describe-image-aws.py b/examples/foundational/12b-describe-image-aws.py index d42bc6ef0..47d2b7970 100644 --- a/examples/foundational/12b-describe-image-aws.py +++ b/examples/foundational/12b-describe-image-aws.py @@ -63,7 +63,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): settings=AWSBedrockLLMService.Settings( model="us.anthropic.claude-sonnet-4-6", temperature=0.8, - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way. You are also able to describe images.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way. You are also able to describe images.", ), ) diff --git a/examples/foundational/12c-describe-image-gemini-flash.py b/examples/foundational/12c-describe-image-gemini-flash.py index acb481d3e..248bc08a7 100644 --- a/examples/foundational/12c-describe-image-gemini-flash.py +++ b/examples/foundational/12c-describe-image-gemini-flash.py @@ -61,7 +61,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = GoogleLLMService( api_key=os.getenv("GOOGLE_API_KEY"), settings=GoogleLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way. You are also able to describe images.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way. You are also able to describe images.", ), ) diff --git a/examples/foundational/13-whisper-transcription.py b/examples/foundational/13-whisper-transcription.py index e2f0f61e0..f61cec132 100644 --- a/examples/foundational/13-whisper-transcription.py +++ b/examples/foundational/13-whisper-transcription.py @@ -13,6 +13,7 @@ from pipecat.frames.frames import Frame, TranscriptionFrame from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.task import PipelineTask +from pipecat.processors.audio.vad_processor import VADProcessor from pipecat.processors.frame_processor import FrameDirection, FrameProcessor from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport @@ -40,15 +41,12 @@ class TranscriptionLogger(FrameProcessor): transport_params = { "daily": lambda: DailyParams( audio_in_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "twilio": lambda: FastAPIWebsocketParams( audio_in_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "webrtc": lambda: TransportParams( audio_in_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), } @@ -59,8 +57,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): stt = WhisperSTTService() tl = TranscriptionLogger() + vad_processor = VADProcessor(vad_analyzer=SileroVADAnalyzer()) - pipeline = Pipeline([transport.input(), stt, tl]) + pipeline = Pipeline([transport.input(), vad_processor, stt, tl]) task = PipelineTask( pipeline, diff --git a/examples/foundational/13a-whisper-local.py b/examples/foundational/13a-whisper-local.py index ec9ddb603..0882542fc 100644 --- a/examples/foundational/13a-whisper-local.py +++ b/examples/foundational/13a-whisper-local.py @@ -15,6 +15,7 @@ from pipecat.frames.frames import Frame, TranscriptionFrame from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.task import PipelineTask +from pipecat.processors.audio.vad_processor import VADProcessor from pipecat.processors.frame_processor import FrameDirection, FrameProcessor from pipecat.services.whisper.stt import WhisperSTTService from pipecat.transports.local.audio import LocalAudioTransport, LocalAudioTransportParams @@ -40,15 +41,15 @@ async def main(): transport = LocalAudioTransport( LocalAudioTransportParams( audio_in_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ) ) stt = WhisperSTTService() tl = TranscriptionLogger() + vad_processor = VADProcessor(vad_analyzer=SileroVADAnalyzer()) - pipeline = Pipeline([transport.input(), stt, tl]) + pipeline = Pipeline([transport.input(), vad_processor, stt, tl]) task = PipelineTask(pipeline) diff --git a/examples/foundational/13e-whisper-mlx.py b/examples/foundational/13e-whisper-mlx.py index f4721a86a..af7d2d79e 100644 --- a/examples/foundational/13e-whisper-mlx.py +++ b/examples/foundational/13e-whisper-mlx.py @@ -15,6 +15,7 @@ from pipecat.frames.frames import Frame, TranscriptionFrame, UserStoppedSpeaking from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.task import PipelineParams, PipelineTask +from pipecat.processors.audio.vad_processor import VADProcessor from pipecat.processors.frame_processor import FrameDirection, FrameProcessor from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport @@ -61,15 +62,12 @@ class TranscriptionLogger(FrameProcessor): transport_params = { "daily": lambda: DailyParams( audio_in_enabled=True, - vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=STOP_SECS)), ), "twilio": lambda: FastAPIWebsocketParams( audio_in_enabled=True, - vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=STOP_SECS)), ), "webrtc": lambda: TransportParams( audio_in_enabled=True, - vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=STOP_SECS)), ), } @@ -84,8 +82,11 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): ) tl = TranscriptionLogger() + vad_processor = VADProcessor( + vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=STOP_SECS)) + ) - pipeline = Pipeline([transport.input(), stt, tl]) + pipeline = Pipeline([transport.input(), vad_processor, stt, tl]) task = PipelineTask( pipeline, diff --git a/examples/foundational/13g-sambanova-transcription.py b/examples/foundational/13g-sambanova-transcription.py index 404c215fd..26e961c5e 100644 --- a/examples/foundational/13g-sambanova-transcription.py +++ b/examples/foundational/13g-sambanova-transcription.py @@ -16,6 +16,7 @@ from pipecat.frames.frames import Frame, TranscriptionFrame, UserStoppedSpeaking from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.task import PipelineParams, PipelineTask +from pipecat.processors.audio.vad_processor import VADProcessor from pipecat.processors.frame_processor import FrameDirection, FrameProcessor from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport @@ -62,15 +63,12 @@ class TranscriptionLogger(FrameProcessor): transport_params = { "daily": lambda: DailyParams( audio_in_enabled=True, - vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=STOP_SECS)), ), "twilio": lambda: FastAPIWebsocketParams( audio_in_enabled=True, - vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=STOP_SECS)), ), "webrtc": lambda: TransportParams( audio_in_enabled=True, - vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=STOP_SECS)), ), } @@ -86,8 +84,11 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): ) tl = TranscriptionLogger() + vad_processor = VADProcessor( + vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=STOP_SECS)) + ) - pipeline = Pipeline([transport.input(), stt, tl]) + pipeline = Pipeline([transport.input(), vad_processor, stt, tl]) task = PipelineTask( pipeline, diff --git a/examples/foundational/13i-soniox-transcription.py b/examples/foundational/13i-soniox-transcription.py index d8d46dfc3..9476e9441 100644 --- a/examples/foundational/13i-soniox-transcription.py +++ b/examples/foundational/13i-soniox-transcription.py @@ -14,6 +14,7 @@ from pipecat.frames.frames import Frame, TranscriptionFrame from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.task import PipelineTask +from pipecat.processors.audio.vad_processor import VADProcessor from pipecat.processors.frame_processor import FrameDirection, FrameProcessor from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport @@ -39,15 +40,12 @@ class TranscriptionLogger(FrameProcessor): transport_params = { "daily": lambda: DailyParams( audio_in_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "twilio": lambda: FastAPIWebsocketParams( audio_in_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "webrtc": lambda: TransportParams( audio_in_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), } @@ -60,8 +58,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): ) tl = TranscriptionLogger() + vad_processor = VADProcessor(vad_analyzer=SileroVADAnalyzer()) - pipeline = Pipeline([transport.input(), stt, tl]) + pipeline = Pipeline([transport.input(), vad_processor, stt, tl]) task = PipelineTask( pipeline, diff --git a/examples/foundational/13j-azure-transcription.py b/examples/foundational/13j-azure-transcription.py index d3df106bd..301c6effd 100644 --- a/examples/foundational/13j-azure-transcription.py +++ b/examples/foundational/13j-azure-transcription.py @@ -14,6 +14,7 @@ from pipecat.frames.frames import Frame, TranscriptionFrame from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.task import PipelineTask +from pipecat.processors.audio.vad_processor import VADProcessor from pipecat.processors.frame_processor import FrameDirection, FrameProcessor from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport @@ -39,15 +40,12 @@ class TranscriptionLogger(FrameProcessor): transport_params = { "daily": lambda: DailyParams( audio_in_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "twilio": lambda: FastAPIWebsocketParams( audio_in_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "webrtc": lambda: TransportParams( audio_in_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), } @@ -61,8 +59,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): ) tl = TranscriptionLogger() + vad_processor = VADProcessor(vad_analyzer=SileroVADAnalyzer()) - pipeline = Pipeline([transport.input(), stt, tl]) + pipeline = Pipeline([transport.input(), vad_processor, stt, tl]) task = PipelineTask( pipeline, diff --git a/examples/foundational/13k-elevenlabs-transcription.py b/examples/foundational/13k-elevenlabs-transcription.py index f801944e5..dbfd32ec6 100644 --- a/examples/foundational/13k-elevenlabs-transcription.py +++ b/examples/foundational/13k-elevenlabs-transcription.py @@ -14,6 +14,7 @@ from pipecat.frames.frames import Frame, TranscriptionFrame from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.task import PipelineTask +from pipecat.processors.audio.vad_processor import VADProcessor from pipecat.processors.frame_processor import FrameDirection, FrameProcessor from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport @@ -39,11 +40,9 @@ class TranscriptionLogger(FrameProcessor): # We use lambdas to defer transport parameter creation until the transport # type is selected at runtime. transport_params = { - "daily": lambda: DailyParams(audio_in_enabled=True, vad_analyzer=SileroVADAnalyzer()), - "twilio": lambda: FastAPIWebsocketParams( - audio_in_enabled=True, vad_analyzer=SileroVADAnalyzer() - ), - "webrtc": lambda: TransportParams(audio_in_enabled=True, vad_analyzer=SileroVADAnalyzer()), + "daily": lambda: DailyParams(audio_in_enabled=True), + "twilio": lambda: FastAPIWebsocketParams(audio_in_enabled=True), + "webrtc": lambda: TransportParams(audio_in_enabled=True), } @@ -53,8 +52,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): stt = ElevenLabsRealtimeSTTService(api_key=os.getenv("ELEVENLABS_API_KEY")) tl = TranscriptionLogger() + vad_processor = VADProcessor(vad_analyzer=SileroVADAnalyzer()) - pipeline = Pipeline([transport.input(), stt, tl]) + pipeline = Pipeline([transport.input(), vad_processor, stt, tl]) task = PipelineTask( pipeline, diff --git a/examples/foundational/13m-openai-transcription.py b/examples/foundational/13m-openai-transcription.py index 0fb595881..cbbf0e13d 100644 --- a/examples/foundational/13m-openai-transcription.py +++ b/examples/foundational/13m-openai-transcription.py @@ -14,6 +14,7 @@ from pipecat.frames.frames import Frame, TranscriptionFrame from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.task import PipelineTask +from pipecat.processors.audio.vad_processor import VADProcessor from pipecat.processors.frame_processor import FrameDirection, FrameProcessor from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport @@ -39,11 +40,9 @@ class TranscriptionLogger(FrameProcessor): # We use lambdas to defer transport parameter creation until the transport # type is selected at runtime. transport_params = { - "daily": lambda: DailyParams(audio_in_enabled=True, vad_analyzer=SileroVADAnalyzer()), - "twilio": lambda: FastAPIWebsocketParams( - audio_in_enabled=True, vad_analyzer=SileroVADAnalyzer() - ), - "webrtc": lambda: TransportParams(audio_in_enabled=True, vad_analyzer=SileroVADAnalyzer()), + "daily": lambda: DailyParams(audio_in_enabled=True), + "twilio": lambda: FastAPIWebsocketParams(audio_in_enabled=True), + "webrtc": lambda: TransportParams(audio_in_enabled=True), } @@ -59,8 +58,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): ) tl = TranscriptionLogger() + vad_processor = VADProcessor(vad_analyzer=SileroVADAnalyzer()) - pipeline = Pipeline([transport.input(), stt, tl]) + pipeline = Pipeline([transport.input(), vad_processor, stt, tl]) task = PipelineTask( pipeline, diff --git a/examples/foundational/14-function-calling-openai-responses.py b/examples/foundational/14-function-calling-openai-responses.py new file mode 100644 index 000000000..58cac774a --- /dev/null +++ b/examples/foundational/14-function-calling-openai-responses.py @@ -0,0 +1,175 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import os + +from dotenv import load_dotenv +from loguru import logger + +from pipecat.adapters.schemas.function_schema import FunctionSchema +from pipecat.adapters.schemas.tools_schema import ToolsSchema +from pipecat.audio.vad.silero import SileroVADAnalyzer +from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame +from pipecat.pipeline.pipeline import Pipeline +from pipecat.pipeline.runner import PipelineRunner +from pipecat.pipeline.task import PipelineParams, PipelineTask +from pipecat.processors.aggregators.llm_context import LLMContext +from pipecat.processors.aggregators.llm_response_universal import ( + LLMContextAggregatorPair, + LLMUserAggregatorParams, +) +from pipecat.runner.types import RunnerArguments +from pipecat.runner.utils import create_transport +from pipecat.services.cartesia.tts import CartesiaTTSService +from pipecat.services.deepgram.stt import DeepgramSTTService +from pipecat.services.llm_service import FunctionCallParams +from pipecat.services.openai.responses.llm import OpenAIResponsesLLMService +from pipecat.transports.base_transport import BaseTransport, TransportParams +from pipecat.transports.daily.transport import DailyParams +from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams + +load_dotenv(override=True) + + +async def fetch_weather_from_api(params: FunctionCallParams): + await params.result_callback({"conditions": "nice", "temperature": "75"}) + + +async def fetch_restaurant_recommendation(params: FunctionCallParams): + await params.result_callback({"name": "The Golden Dragon"}) + + +# We use lambdas to defer transport parameter creation until the transport +# type is selected at runtime. +transport_params = { + "daily": lambda: DailyParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), + "twilio": lambda: FastAPIWebsocketParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), + "webrtc": lambda: TransportParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), +} + + +async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): + logger.info(f"Starting bot") + + stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY")) + + tts = CartesiaTTSService( + api_key=os.getenv("CARTESIA_API_KEY"), + settings=CartesiaTTSService.Settings( + voice="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady + ), + ) + + llm = OpenAIResponsesLLMService( + api_key=os.getenv("OPENAI_API_KEY"), + settings=OpenAIResponsesLLMService.Settings( + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", + ), + ) + + # You can also register a function_name of None to get all functions + # sent to the same callback with an additional function_name parameter. + llm.register_function("get_current_weather", fetch_weather_from_api) + llm.register_function("get_restaurant_recommendation", fetch_restaurant_recommendation) + + @llm.event_handler("on_function_calls_started") + async def on_function_calls_started(service, function_calls): + await tts.queue_frame(TTSSpeakFrame("Let me check on that.")) + + weather_function = FunctionSchema( + name="get_current_weather", + description="Get the current weather", + properties={ + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA", + }, + "format": { + "type": "string", + "enum": ["celsius", "fahrenheit"], + "description": "The temperature unit to use. Infer this from the user's location.", + }, + }, + required=["location", "format"], + ) + restaurant_function = FunctionSchema( + name="get_restaurant_recommendation", + description="Get a restaurant recommendation", + properties={ + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA", + }, + }, + required=["location"], + ) + tools = ToolsSchema(standard_tools=[weather_function, restaurant_function]) + + context = LLMContext(tools=tools) + user_aggregator, assistant_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) + + pipeline = Pipeline( + [ + transport.input(), + stt, + user_aggregator, + llm, + tts, + transport.output(), + assistant_aggregator, + ] + ) + + task = PipelineTask( + pipeline, + params=PipelineParams( + enable_metrics=True, + enable_usage_metrics=True, + ), + idle_timeout_secs=runner_args.pipeline_idle_timeout_secs, + ) + + @transport.event_handler("on_client_connected") + async def on_client_connected(transport, client): + logger.info(f"Client connected") + # Kick off the conversation. + context.add_message( + {"role": "developer", "content": "Please introduce yourself to the user."} + ) + await task.queue_frames([LLMRunFrame()]) + + @transport.event_handler("on_client_disconnected") + async def on_client_disconnected(transport, client): + logger.info(f"Client disconnected") + await task.cancel() + + runner = PipelineRunner(handle_sigint=runner_args.handle_sigint) + + await runner.run(task) + + +async def bot(runner_args: RunnerArguments): + """Main bot entry point compatible with Pipecat Cloud.""" + transport = await create_transport(runner_args, transport_params) + await run_bot(transport, runner_args) + + +if __name__ == "__main__": + from pipecat.runner.run import main + + main() diff --git a/examples/foundational/14-function-calling.py b/examples/foundational/14-function-calling.py index 7b7e11edf..5001d5dad 100644 --- a/examples/foundational/14-function-calling.py +++ b/examples/foundational/14-function-calling.py @@ -75,7 +75,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/14a-function-calling-anthropic.py b/examples/foundational/14a-function-calling-anthropic.py index c0ad9102d..6cf1e228f 100644 --- a/examples/foundational/14a-function-calling-anthropic.py +++ b/examples/foundational/14a-function-calling-anthropic.py @@ -77,7 +77,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = AnthropicLLMService( api_key=os.getenv("ANTHROPIC_API_KEY"), settings=AnthropicLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) llm.register_function("get_weather", get_weather) diff --git a/examples/foundational/14c-function-calling-together.py b/examples/foundational/14c-function-calling-together.py index 3d2034cb4..5bd176237 100644 --- a/examples/foundational/14c-function-calling-together.py +++ b/examples/foundational/14c-function-calling-together.py @@ -73,7 +73,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): api_key=os.getenv("TOGETHER_API_KEY"), settings=TogetherLLMService.Settings( model="meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo", - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) # You can also register a function_name of None to get all functions diff --git a/examples/foundational/14d-function-calling-anthropic-video.py b/examples/foundational/14d-function-calling-anthropic-video.py index ce51ddb22..f2653101e 100644 --- a/examples/foundational/14d-function-calling-anthropic-video.py +++ b/examples/foundational/14d-function-calling-anthropic-video.py @@ -99,7 +99,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = AnthropicLLMService( api_key=os.getenv("ANTHROPIC_API_KEY"), settings=AnthropicLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way. You are able to describe images from the user camera.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way. You are able to describe images from the user camera.", ), ) llm.register_function("fetch_user_image", fetch_user_image) diff --git a/examples/foundational/14d-function-calling-aws-video.py b/examples/foundational/14d-function-calling-aws-video.py index 2c54e83f4..9fccdecdf 100644 --- a/examples/foundational/14d-function-calling-aws-video.py +++ b/examples/foundational/14d-function-calling-aws-video.py @@ -104,7 +104,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): # Here we can't because AWS Bedrock doesn't support it for Claude 3.7, # which we need for image input. temperature=0.8, - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way. You are able to describe images from the user camera.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way. You are able to describe images from the user camera.", ), ) llm.register_function("fetch_user_image", fetch_user_image) diff --git a/examples/foundational/14d-function-calling-gemini-flash-video.py b/examples/foundational/14d-function-calling-gemini-flash-video.py index 16d82e002..ba76de44b 100644 --- a/examples/foundational/14d-function-calling-gemini-flash-video.py +++ b/examples/foundational/14d-function-calling-gemini-flash-video.py @@ -99,7 +99,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = GoogleLLMService( api_key=os.getenv("GOOGLE_API_KEY"), settings=GoogleLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way. You are able to describe images from the user camera.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way. You are able to describe images from the user camera.", ), ) llm.register_function("fetch_user_image", fetch_user_image) diff --git a/examples/foundational/14d-function-calling-moondream-video.py b/examples/foundational/14d-function-calling-moondream-video.py index df7fc6014..2bbfbd426 100644 --- a/examples/foundational/14d-function-calling-moondream-video.py +++ b/examples/foundational/14d-function-calling-moondream-video.py @@ -129,7 +129,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way. You are able to describe images from the user camera.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way. You are able to describe images from the user camera.", ), ) llm.register_function("fetch_user_image", fetch_user_image) diff --git a/examples/foundational/14d-function-calling-openai-responses-video.py b/examples/foundational/14d-function-calling-openai-responses-video.py new file mode 100644 index 000000000..440c51cc1 --- /dev/null +++ b/examples/foundational/14d-function-calling-openai-responses-video.py @@ -0,0 +1,195 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + + +import os + +from dotenv import load_dotenv +from loguru import logger + +from pipecat.adapters.schemas.function_schema import FunctionSchema +from pipecat.adapters.schemas.tools_schema import ToolsSchema +from pipecat.audio.vad.silero import SileroVADAnalyzer +from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame, UserImageRequestFrame +from pipecat.pipeline.pipeline import Pipeline +from pipecat.pipeline.runner import PipelineRunner +from pipecat.pipeline.task import PipelineParams, PipelineTask +from pipecat.processors.aggregators.llm_context import LLMContext +from pipecat.processors.aggregators.llm_response_universal import ( + LLMContextAggregatorPair, + LLMUserAggregatorParams, +) +from pipecat.processors.frame_processor import FrameDirection +from pipecat.runner.types import RunnerArguments +from pipecat.runner.utils import ( + create_transport, + get_transport_client_id, + maybe_capture_participant_camera, +) +from pipecat.services.cartesia.tts import CartesiaTTSService +from pipecat.services.deepgram.stt import DeepgramSTTService +from pipecat.services.llm_service import FunctionCallParams +from pipecat.services.openai.responses.llm import OpenAIResponsesLLMService +from pipecat.transports.base_transport import BaseTransport, TransportParams +from pipecat.transports.daily.transport import DailyParams + +load_dotenv(override=True) + + +async def fetch_user_image(params: FunctionCallParams): + """Fetch the user image and push it to the LLM. + + When called, this function pushes a UserImageRequestFrame upstream to the + transport. As a result, the transport will request the user image and push a + UserImageRawFrame downstream which will be added to the context by the LLM + assistant aggregator. The result_callback will be invoked once the image is + retrieved and processed. + """ + user_id = params.arguments["user_id"] + question = params.arguments["question"] + logger.debug(f"Requesting image with user_id={user_id}, question={question}") + + # Request a user image frame and indicate that it should be added to the + # context. Also associate it to the function call. Pass the result_callback + # so it can be invoked when the image is actually retrieved. + await params.llm.push_frame( + UserImageRequestFrame( + user_id=user_id, + text=question, + append_to_context=True, + function_name=params.function_name, + tool_call_id=params.tool_call_id, + result_callback=params.result_callback, + ), + FrameDirection.UPSTREAM, + ) + + +# We use lambdas to defer transport parameter creation until the transport +# type is selected at runtime. +transport_params = { + "daily": lambda: DailyParams( + audio_in_enabled=True, + audio_out_enabled=True, + video_in_enabled=True, + ), + "webrtc": lambda: TransportParams( + audio_in_enabled=True, + audio_out_enabled=True, + video_in_enabled=True, + ), +} + + +async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): + logger.info(f"Starting bot") + + stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY")) + + tts = CartesiaTTSService( + api_key=os.getenv("CARTESIA_API_KEY"), + settings=CartesiaTTSService.Settings( + voice="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady + ), + ) + + llm = OpenAIResponsesLLMService( + api_key=os.getenv("OPENAI_API_KEY"), + settings=OpenAIResponsesLLMService.Settings( + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way. You are able to describe images from the user camera.", + ), + ) + llm.register_function("fetch_user_image", fetch_user_image) + + @llm.event_handler("on_function_calls_started") + async def on_function_calls_started(service, function_calls): + await tts.queue_frame(TTSSpeakFrame("Let me check on that.", append_to_context=False)) + + fetch_image_function = FunctionSchema( + name="fetch_user_image", + description="Called when the user requests a description of their camera feed", + properties={ + "user_id": { + "type": "string", + "description": "The ID of the user to grab the image from", + }, + "question": { + "type": "string", + "description": "The question that the user is asking about the image", + }, + }, + required=["user_id", "question"], + ) + tools = ToolsSchema(standard_tools=[fetch_image_function]) + + context = LLMContext(tools=tools) + user_aggregator, assistant_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) + + pipeline = Pipeline( + [ + transport.input(), # Transport user input + stt, # STT + user_aggregator, # User responses + llm, # LLM + tts, # TTS + transport.output(), # Transport bot output + assistant_aggregator, # Assistant spoken responses + ] + ) + + task = PipelineTask( + pipeline, + params=PipelineParams( + enable_metrics=True, + enable_usage_metrics=True, + ), + idle_timeout_secs=runner_args.pipeline_idle_timeout_secs, + ) + + @transport.event_handler("on_client_connected") + async def on_client_connected(transport, client): + logger.info(f"Client connected") + + await maybe_capture_participant_camera(transport, client) + + client_id = get_transport_client_id(transport, client) + + # Kick off the conversation. + context.add_message( + { + "role": "user", + "content": f"Please introduce yourself to the user. Use '{client_id}' as the user ID during function calls.", + } + ) + await task.queue_frames([LLMRunFrame()]) + + @transport.event_handler("on_client_disconnected") + async def on_client_disconnected(transport, client): + logger.info(f"Client disconnected") + await task.cancel() + + @tts.event_handler("on_tts_request") + async def on_tts_request(tts, context_id: str, text: str): + logger.debug(f"On TTS request: {context_id}: {text}") + + runner = PipelineRunner(handle_sigint=runner_args.handle_sigint) + + await runner.run(task) + + +async def bot(runner_args: RunnerArguments): + """Main bot entry point compatible with Pipecat Cloud.""" + transport = await create_transport(runner_args, transport_params) + await run_bot(transport, runner_args) + + +if __name__ == "__main__": + from pipecat.runner.run import main + + main() diff --git a/examples/foundational/14d-function-calling-openai-video.py b/examples/foundational/14d-function-calling-openai-video.py index 1eac522e8..59bef64b6 100644 --- a/examples/foundational/14d-function-calling-openai-video.py +++ b/examples/foundational/14d-function-calling-openai-video.py @@ -99,7 +99,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way. You are able to describe images from the user camera.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way. You are able to describe images from the user camera.", ), ) llm.register_function("fetch_user_image", fetch_user_image) diff --git a/examples/foundational/14f-function-calling-groq.py b/examples/foundational/14f-function-calling-groq.py index a6f383a1f..1e6e84338 100644 --- a/examples/foundational/14f-function-calling-groq.py +++ b/examples/foundational/14f-function-calling-groq.py @@ -72,7 +72,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = GroqLLMService( api_key=os.getenv("GROQ_API_KEY"), settings=GroqLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) # You can also register a function_name of None to get all functions diff --git a/examples/foundational/14g-function-calling-grok.py b/examples/foundational/14g-function-calling-grok.py index 283fd40fd..4de1f6528 100644 --- a/examples/foundational/14g-function-calling-grok.py +++ b/examples/foundational/14g-function-calling-grok.py @@ -72,7 +72,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = GrokLLMService( api_key=os.getenv("GROK_API_KEY"), settings=GrokLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) # You can also register a function_name of None to get all functions diff --git a/examples/foundational/14h-function-calling-azure.py b/examples/foundational/14h-function-calling-azure.py index 3aae0c490..23c156da4 100644 --- a/examples/foundational/14h-function-calling-azure.py +++ b/examples/foundational/14h-function-calling-azure.py @@ -74,7 +74,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): endpoint=os.getenv("AZURE_CHATGPT_ENDPOINT"), settings=AzureLLMService.Settings( model=os.getenv("AZURE_CHATGPT_MODEL"), - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) # You can also register a function_name of None to get all functions diff --git a/examples/foundational/14i-function-calling-fireworks.py b/examples/foundational/14i-function-calling-fireworks.py index c82165b17..f58ef73d9 100644 --- a/examples/foundational/14i-function-calling-fireworks.py +++ b/examples/foundational/14i-function-calling-fireworks.py @@ -73,7 +73,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): api_key=os.getenv("FIREWORKS_API_KEY"), settings=FireworksLLMService.Settings( model="accounts/fireworks/models/gpt-oss-20b", - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) # You can also register a function_name of None to get all functions diff --git a/examples/foundational/14j-function-calling-nvidia.py b/examples/foundational/14j-function-calling-nvidia.py index 39b75b7ac..e3db6db8d 100644 --- a/examples/foundational/14j-function-calling-nvidia.py +++ b/examples/foundational/14j-function-calling-nvidia.py @@ -75,7 +75,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): model="nvidia/llama-3.3-nemotron-super-49b-v1.5", # Recommended when turning thinking off temperature=0.0, - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="/no_think You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) # You can also register a function_name of None to get all functions diff --git a/examples/foundational/14k-function-calling-cerebras.py b/examples/foundational/14k-function-calling-cerebras.py index 51b9c9bf9..ad8475185 100644 --- a/examples/foundational/14k-function-calling-cerebras.py +++ b/examples/foundational/14k-function-calling-cerebras.py @@ -72,7 +72,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = CerebrasLLMService( api_key=os.getenv("CEREBRAS_API_KEY"), settings=CerebrasLLMService.Settings( - system_instruction="""You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. + system_instruction="""You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way. You have one functions available: diff --git a/examples/foundational/14l-function-calling-deepseek.py b/examples/foundational/14l-function-calling-deepseek.py index 4af091b7a..f115299e2 100644 --- a/examples/foundational/14l-function-calling-deepseek.py +++ b/examples/foundational/14l-function-calling-deepseek.py @@ -73,7 +73,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): api_key=os.getenv("DEEPSEEK_API_KEY"), settings=DeepSeekLLMService.Settings( model="deepseek-chat", - system_instruction="""You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. + system_instruction="""You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way. You have one functions available: diff --git a/examples/foundational/14m-function-calling-openrouter.py b/examples/foundational/14m-function-calling-openrouter.py index e23865435..b341ca71c 100644 --- a/examples/foundational/14m-function-calling-openrouter.py +++ b/examples/foundational/14m-function-calling-openrouter.py @@ -77,7 +77,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): api_key=os.getenv("OPENROUTER_API_KEY"), settings=OpenRouterLLMService.Settings( model="openai/gpt-4o-2024-11-20", - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) # You can also register a function_name of None to get all functions diff --git a/examples/foundational/14n-function-calling-perplexity.py b/examples/foundational/14n-function-calling-perplexity.py index 2c1ba9e00..a67e93ab8 100644 --- a/examples/foundational/14n-function-calling-perplexity.py +++ b/examples/foundational/14n-function-calling-perplexity.py @@ -70,7 +70,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = PerplexityLLMService( api_key=os.getenv("PERPLEXITY_API_KEY"), settings=PerplexityLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way, but try to be brief.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/14o-function-calling-gemini-openai-format.py b/examples/foundational/14o-function-calling-gemini-openai-format.py index 416e602bf..70e8af3d3 100644 --- a/examples/foundational/14o-function-calling-gemini-openai-format.py +++ b/examples/foundational/14o-function-calling-gemini-openai-format.py @@ -26,7 +26,7 @@ from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport from pipecat.services.deepgram.stt import DeepgramSTTService from pipecat.services.elevenlabs.tts import ElevenLabsTTSService -from pipecat.services.google.llm_openai import GoogleLLMOpenAIBetaService +from pipecat.services.google.openai.llm import GoogleLLMOpenAIBetaService from pipecat.services.llm_service import FunctionCallParams from pipecat.transports.base_transport import BaseTransport, TransportParams from pipecat.transports.daily.transport import DailyParams @@ -72,7 +72,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = GoogleLLMOpenAIBetaService( api_key=os.getenv("GOOGLE_API_KEY"), settings=GoogleLLMOpenAIBetaService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) # You can aslo register a function_name of None to get all functions diff --git a/examples/foundational/14p-function-calling-gemini-vertex-ai.py b/examples/foundational/14p-function-calling-gemini-vertex-ai.py index 1f33efacf..fb3910ed3 100644 --- a/examples/foundational/14p-function-calling-gemini-vertex-ai.py +++ b/examples/foundational/14p-function-calling-gemini-vertex-ai.py @@ -26,7 +26,7 @@ from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport from pipecat.services.deepgram.stt import DeepgramSTTService from pipecat.services.elevenlabs.tts import ElevenLabsTTSService -from pipecat.services.google.llm_vertex import GoogleVertexLLMService +from pipecat.services.google.vertex.llm import GoogleVertexLLMService from pipecat.services.llm_service import FunctionCallParams from pipecat.transports.base_transport import BaseTransport, TransportParams from pipecat.transports.daily.transport import DailyParams @@ -74,7 +74,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): project_id=os.getenv("GOOGLE_CLOUD_PROJECT_ID"), location=os.getenv("GOOGLE_CLOUD_LOCATION"), settings=GoogleVertexLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) # You can aslo register a function_name of None to get all functions diff --git a/examples/foundational/14q-function-calling-qwen.py b/examples/foundational/14q-function-calling-qwen.py index bf866a519..9a6bf6d8b 100644 --- a/examples/foundational/14q-function-calling-qwen.py +++ b/examples/foundational/14q-function-calling-qwen.py @@ -73,7 +73,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): api_key=os.getenv("QWEN_API_KEY"), model="qwen2.5-72b-instruct", settings=QwenLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/14r-function-calling-aws.py b/examples/foundational/14r-function-calling-aws.py index 29591c80a..86ee0f0dc 100644 --- a/examples/foundational/14r-function-calling-aws.py +++ b/examples/foundational/14r-function-calling-aws.py @@ -78,7 +78,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): settings=AWSBedrockLLMService.Settings( model="us.anthropic.claude-sonnet-4-6", temperature=0.8, - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/14s-function-calling-sambanova.py b/examples/foundational/14s-function-calling-sambanova.py index c329135f9..3854d2d6e 100644 --- a/examples/foundational/14s-function-calling-sambanova.py +++ b/examples/foundational/14s-function-calling-sambanova.py @@ -75,7 +75,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = SambaNovaLLMService( api_key=os.getenv("SAMBANOVA_API_KEY"), settings=SambaNovaLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) # You can also register a function_name of None to get all functions diff --git a/examples/foundational/14t-function-calling-direct.py b/examples/foundational/14t-function-calling-direct.py index 7b4485226..1c6ebe072 100644 --- a/examples/foundational/14t-function-calling-direct.py +++ b/examples/foundational/14t-function-calling-direct.py @@ -88,7 +88,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/14u-function-calling-ollama.py b/examples/foundational/14u-function-calling-ollama.py index a0c1ec33a..87491f3d6 100644 --- a/examples/foundational/14u-function-calling-ollama.py +++ b/examples/foundational/14u-function-calling-ollama.py @@ -76,7 +76,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OLLamaLLMService( settings=OLLamaLLMService.Settings( model="llama3.2", - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) # Update to the model you're running locally diff --git a/examples/foundational/14v-function-calling-openai.py b/examples/foundational/14v-function-calling-openai.py index 639d9bd23..2b59d7072 100644 --- a/examples/foundational/14v-function-calling-openai.py +++ b/examples/foundational/14v-function-calling-openai.py @@ -79,11 +79,10 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): instructions="Please speak clearly and at a moderate pace.", ) - # model choices: gpt-4o, gpt-4.1, etc. llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/14w-function-calling-mistral.py b/examples/foundational/14w-function-calling-mistral.py index 421a63b74..c52d46c03 100644 --- a/examples/foundational/14w-function-calling-mistral.py +++ b/examples/foundational/14w-function-calling-mistral.py @@ -75,7 +75,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = MistralLLMService( api_key=os.getenv("MISTRAL_API_KEY"), settings=MistralLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/14x-function-calling-openpipe.py b/examples/foundational/14x-function-calling-openpipe.py index e82062c16..5074bda2f 100644 --- a/examples/foundational/14x-function-calling-openpipe.py +++ b/examples/foundational/14x-function-calling-openpipe.py @@ -79,7 +79,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): openpipe_api_key=os.getenv("OPENPIPE_API_KEY"), tags={"conversation_id": f"pipecat-{timestamp}"}, settings=OpenPipeLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/15-switch-voices.py b/examples/foundational/15-switch-voices.py index 39918918b..daf77ecde 100644 --- a/examples/foundational/15-switch-voices.py +++ b/examples/foundational/15-switch-voices.py @@ -121,7 +121,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities. Respond to what the user said in a creative and helpful way. Your output should not include non-alphanumeric characters. You can do the following voices: 'News Lady', 'British Lady' and 'Barbershop Man'.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative and helpful way. You can do the following voices: 'News Lady', 'British Lady' and 'Barbershop Man'.", ), ) llm.register_function("switch_voice", tts.switch_voice) diff --git a/examples/foundational/15a-switch-languages.py b/examples/foundational/15a-switch-languages.py index 67d943a0a..0ff13df1a 100644 --- a/examples/foundational/15a-switch-languages.py +++ b/examples/foundational/15a-switch-languages.py @@ -112,7 +112,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities. Respond to what the user said in a creative and helpful way. Your output should not include non-alphanumeric characters. You can speak the following languages: 'English' and 'Spanish'.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way. You can speak the following languages: 'English' and 'Spanish'.", ), ) llm.register_function("switch_language", tts.switch_language) diff --git a/examples/foundational/16-gpu-container-local-bot.py b/examples/foundational/16-gpu-container-local-bot.py index 2b680eee1..b12d85dff 100644 --- a/examples/foundational/16-gpu-container-local-bot.py +++ b/examples/foundational/16-gpu-container-local-bot.py @@ -72,7 +72,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): # Or, to use a local vLLM (or similar) api server settings=OpenAILLMService.Settings( model="meta-llama/Meta-Llama-3-8B-Instruct", - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), base_url="http://0.0.0.0:8000/v1", ) diff --git a/examples/foundational/17-detect-user-idle.py b/examples/foundational/17-detect-user-idle.py index a1ac00501..1cd772af2 100644 --- a/examples/foundational/17-detect-user-idle.py +++ b/examples/foundational/17-detect-user-idle.py @@ -123,7 +123,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/19-openai-realtime.py b/examples/foundational/19-openai-realtime.py index 8eeea9e02..cb3abf953 100644 --- a/examples/foundational/19-openai-realtime.py +++ b/examples/foundational/19-openai-realtime.py @@ -24,6 +24,7 @@ from pipecat.processors.aggregators.llm_context import LLMContext from pipecat.processors.aggregators.llm_response_universal import ( AssistantTurnStoppedMessage, LLMContextAggregatorPair, + LLMUserAggregatorParams, UserTurnStoppedMessage, ) from pipecat.runner.types import RunnerArguments @@ -119,17 +120,14 @@ transport_params = { "daily": lambda: DailyParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "twilio": lambda: FastAPIWebsocketParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "webrtc": lambda: TransportParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), } @@ -187,7 +185,10 @@ Remember, your responses should be short. Just one or two sentences, usually. Re tools, ) - user_aggregator, assistant_aggregator = LLMContextAggregatorPair(context) + user_aggregator, assistant_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) pipeline = Pipeline( [ diff --git a/examples/foundational/19a-azure-realtime.py b/examples/foundational/19a-azure-realtime.py index c98dc167a..ffba20f5d 100644 --- a/examples/foundational/19a-azure-realtime.py +++ b/examples/foundational/19a-azure-realtime.py @@ -19,7 +19,10 @@ from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.task import PipelineParams, PipelineTask from pipecat.processors.aggregators.llm_context import LLMContext -from pipecat.processors.aggregators.llm_response_universal import LLMContextAggregatorPair +from pipecat.processors.aggregators.llm_response_universal import ( + LLMContextAggregatorPair, + LLMUserAggregatorParams, +) from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport from pipecat.services.azure.realtime.llm import AzureRealtimeLLMService @@ -93,17 +96,14 @@ transport_params = { "daily": lambda: DailyParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "twilio": lambda: FastAPIWebsocketParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "webrtc": lambda: TransportParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), } @@ -172,7 +172,10 @@ Remember, your responses should be short. Just one or two sentences, usually. Re tools, ) - user_aggregator, assistant_aggregator = LLMContextAggregatorPair(context) + user_aggregator, assistant_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) pipeline = Pipeline( [ diff --git a/examples/foundational/19b-openai-realtime-text.py b/examples/foundational/19b-openai-realtime-text.py index deb50e805..1fa6ea545 100644 --- a/examples/foundational/19b-openai-realtime-text.py +++ b/examples/foundational/19b-openai-realtime-text.py @@ -19,7 +19,10 @@ from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.task import PipelineParams, PipelineTask from pipecat.processors.aggregators.llm_context import LLMContext -from pipecat.processors.aggregators.llm_response_universal import LLMContextAggregatorPair +from pipecat.processors.aggregators.llm_response_universal import ( + LLMContextAggregatorPair, + LLMUserAggregatorParams, +) from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport from pipecat.services.cartesia.tts import CartesiaTTSService @@ -98,17 +101,14 @@ transport_params = { "daily": lambda: DailyParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "twilio": lambda: FastAPIWebsocketParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "webrtc": lambda: TransportParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), } @@ -175,7 +175,10 @@ Remember, your responses should be short. Just one or two sentences, usually. Re tools, ) - user_aggregator, assistant_aggregator = LLMContextAggregatorPair(context) + user_aggregator, assistant_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) pipeline = Pipeline( [ diff --git a/examples/foundational/19c-openai-realtime-live-video.py b/examples/foundational/19c-openai-realtime-live-video.py index 3038a57bc..f862b5511 100644 --- a/examples/foundational/19c-openai-realtime-live-video.py +++ b/examples/foundational/19c-openai-realtime-live-video.py @@ -17,7 +17,10 @@ from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.task import PipelineParams, PipelineTask from pipecat.processors.aggregators.llm_context import LLMContext -from pipecat.processors.aggregators.llm_response_universal import LLMContextAggregatorPair +from pipecat.processors.aggregators.llm_response_universal import ( + LLMContextAggregatorPair, + LLMUserAggregatorParams, +) from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import ( create_transport, @@ -46,13 +49,11 @@ transport_params = { audio_in_enabled=True, audio_out_enabled=True, video_in_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "webrtc": lambda: TransportParams( audio_in_enabled=True, audio_out_enabled=True, video_in_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), } @@ -103,7 +104,10 @@ Remember, your responses should be short. Just one or two sentences, usually. Re [{"role": "user", "content": "Say hello!"}], ) - user_aggregator, assistant_aggregator = LLMContextAggregatorPair(context) + user_aggregator, assistant_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) pipeline = Pipeline( [ diff --git a/examples/foundational/20a-persistent-context-openai-responses.py b/examples/foundational/20a-persistent-context-openai-responses.py new file mode 100644 index 000000000..5fd9c7657 --- /dev/null +++ b/examples/foundational/20a-persistent-context-openai-responses.py @@ -0,0 +1,249 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import glob +import json +import os +from datetime import datetime + +from dotenv import load_dotenv +from loguru import logger + +from pipecat.adapters.schemas.function_schema import FunctionSchema +from pipecat.adapters.schemas.tools_schema import ToolsSchema +from pipecat.audio.vad.silero import SileroVADAnalyzer +from pipecat.frames.frames import LLMRunFrame, TTSSpeakFrame +from pipecat.pipeline.pipeline import Pipeline +from pipecat.pipeline.runner import PipelineRunner +from pipecat.pipeline.task import PipelineParams, PipelineTask +from pipecat.processors.aggregators.llm_context import LLMContext +from pipecat.processors.aggregators.llm_response_universal import ( + LLMContextAggregatorPair, + LLMUserAggregatorParams, +) +from pipecat.runner.types import RunnerArguments +from pipecat.runner.utils import create_transport +from pipecat.services.cartesia.tts import CartesiaTTSService +from pipecat.services.deepgram.stt import DeepgramSTTService +from pipecat.services.llm_service import FunctionCallParams +from pipecat.services.openai.responses.llm import OpenAIResponsesLLMService +from pipecat.transports.base_transport import BaseTransport, TransportParams +from pipecat.transports.daily.transport import DailyParams +from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams + +load_dotenv(override=True) + + +BASE_FILENAME = "/tmp/pipecat_conversation_" + + +async def fetch_weather_from_api(params: FunctionCallParams): + temperature = 75 if params.arguments["format"] == "fahrenheit" else 24 + await params.result_callback( + { + "conditions": "nice", + "temperature": temperature, + "format": params.arguments["format"], + "timestamp": datetime.now().strftime("%Y%m%d_%H%M%S"), + } + ) + + +async def get_saved_conversation_filenames(params: FunctionCallParams): + # Construct the full pattern including the BASE_FILENAME + full_pattern = f"{BASE_FILENAME}*.json" + + # Use glob to find all matching files + matching_files = glob.glob(full_pattern) + logger.debug(f"matching files: {matching_files}") + + await params.result_callback({"filenames": matching_files}) + + +async def save_conversation(params: FunctionCallParams): + timestamp = datetime.now().strftime("%Y-%m-%d_%H:%M:%S") + filename = f"{BASE_FILENAME}{timestamp}.json" + logger.debug( + f"writing conversation to {filename}\n{json.dumps(params.context.get_messages(), indent=4)}" + ) + try: + with open(filename, "w") as file: + messages = params.context.get_messages() + # remove the last message, which is the instruction we just gave to save the conversation + messages.pop() + json.dump(messages, file, indent=2) + await params.result_callback({"success": True}) + except Exception as e: + await params.result_callback({"success": False, "error": str(e)}) + + +async def load_conversation(params: FunctionCallParams): + global tts + filename = params.arguments["filename"] + logger.debug(f"loading conversation from {filename}") + try: + with open(filename, "r") as file: + params.context.set_messages(json.load(file)) + logger.debug( + f"loaded conversation from {filename}\n{json.dumps(params.context.get_messages(), indent=4)}" + ) + await params.llm.queue_frame(TTSSpeakFrame("Ok, I've loaded that conversation.")) + except Exception as e: + await params.result_callback({"success": False, "error": str(e)}) + + +system_instruction = "You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way." + +weather_function = FunctionSchema( + name="get_current_weather", + description="Get the current weather", + properties={ + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA", + }, + "format": { + "type": "string", + "enum": ["celsius", "fahrenheit"], + "description": "The temperature unit to use. Infer this from the users location.", + }, + }, + required=["location", "format"], +) + +save_conversation_function = FunctionSchema( + name="save_conversation", + description="Save the current conversation. Use this function to persist the current conversation to external storage.", + properties={}, + required=[], +) + +get_filenames_function = FunctionSchema( + name="get_saved_conversation_filenames", + description="Get a list of saved conversation histories. Returns a list of filenames. Each filename includes a date and timestamp. Each file is conversation history that can be loaded into this session.", + properties={}, + required=[], +) + +load_conversation_function = FunctionSchema( + name="load_conversation", + description="Load a conversation history. Use this function to load a conversation history into the current session.", + properties={ + "filename": { + "type": "string", + "description": "The filename of the conversation history to load.", + } + }, + required=["filename"], +) + +tools = ToolsSchema( + standard_tools=[ + weather_function, + save_conversation_function, + get_filenames_function, + load_conversation_function, + ] +) + + +# We use lambdas to defer transport parameter creation until the transport +# type is selected at runtime. +transport_params = { + "daily": lambda: DailyParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), + "twilio": lambda: FastAPIWebsocketParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), + "webrtc": lambda: TransportParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), +} + + +async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): + logger.info(f"Starting bot") + + stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY")) + + tts = CartesiaTTSService( + api_key=os.getenv("CARTESIA_API_KEY"), + settings=CartesiaTTSService.Settings( + voice="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady + ), + ) + + llm = OpenAIResponsesLLMService( + api_key=os.getenv("OPENAI_API_KEY"), + settings=OpenAIResponsesLLMService.Settings( + system_instruction=system_instruction, + ), + ) + + # you can either register a single function for all function calls, or specific functions + # llm.register_function(None, fetch_weather_from_api) + llm.register_function("get_current_weather", fetch_weather_from_api) + llm.register_function("save_conversation", save_conversation) + llm.register_function("get_saved_conversation_filenames", get_saved_conversation_filenames) + llm.register_function("load_conversation", load_conversation) + + context = LLMContext(tools=tools) + user_aggregator, assistant_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) + + pipeline = Pipeline( + [ + transport.input(), # Transport user input + stt, # STT + user_aggregator, + llm, # LLM + tts, + transport.output(), # Transport bot output + assistant_aggregator, + ] + ) + + task = PipelineTask( + pipeline, + params=PipelineParams( + enable_metrics=True, + enable_usage_metrics=True, + ), + idle_timeout_secs=runner_args.pipeline_idle_timeout_secs, + ) + + @transport.event_handler("on_client_connected") + async def on_client_connected(transport, client): + logger.info(f"Client connected") + # Kick off the conversation. + await task.queue_frames([LLMRunFrame()]) + + @transport.event_handler("on_client_disconnected") + async def on_client_disconnected(transport, client): + logger.info(f"Client disconnected") + await task.cancel() + + runner = PipelineRunner(handle_sigint=runner_args.handle_sigint) + + await runner.run(task) + + +async def bot(runner_args: RunnerArguments): + """Main bot entry point compatible with Pipecat Cloud.""" + transport = await create_transport(runner_args, transport_params) + await run_bot(transport, runner_args) + + +if __name__ == "__main__": + from pipecat.runner.run import main + + main() diff --git a/examples/foundational/20a-persistent-context-openai.py b/examples/foundational/20a-persistent-context-openai.py index d487133f5..7f744fd46 100644 --- a/examples/foundational/20a-persistent-context-openai.py +++ b/examples/foundational/20a-persistent-context-openai.py @@ -95,7 +95,7 @@ async def load_conversation(params: FunctionCallParams): await params.result_callback({"success": False, "error": str(e)}) -system_instruction = "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way." +system_instruction = "You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way." weather_function = FunctionSchema( name="get_current_weather", @@ -116,7 +116,7 @@ weather_function = FunctionSchema( save_conversation_function = FunctionSchema( name="save_conversation", - description="Save the current conversatione. Use this function to persist the current conversation to external storage.", + description="Save the current conversation. Use this function to persist the current conversation to external storage.", properties={}, required=[], ) diff --git a/examples/foundational/20b-persistent-context-openai-realtime-beta.py b/examples/foundational/20b-persistent-context-openai-realtime-beta.py index fa59e1674..4b05db618 100644 --- a/examples/foundational/20b-persistent-context-openai-realtime-beta.py +++ b/examples/foundational/20b-persistent-context-openai-realtime-beta.py @@ -119,7 +119,7 @@ tools = [ { "type": "function", "name": "save_conversation", - "description": "Save the current conversatione. Use this function to persist the current conversation to external storage.", + "description": "Save the current conversation. Use this function to persist the current conversation to external storage.", "parameters": { "type": "object", "properties": {}, diff --git a/examples/foundational/20b-persistent-context-openai-realtime.py b/examples/foundational/20b-persistent-context-openai-realtime.py index 2b9a8b6bf..bceca410d 100644 --- a/examples/foundational/20b-persistent-context-openai-realtime.py +++ b/examples/foundational/20b-persistent-context-openai-realtime.py @@ -21,7 +21,10 @@ from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.task import PipelineParams, PipelineTask from pipecat.processors.aggregators.llm_context import LLMContext -from pipecat.processors.aggregators.llm_response_universal import LLMContextAggregatorPair +from pipecat.processors.aggregators.llm_response_universal import ( + LLMContextAggregatorPair, + LLMUserAggregatorParams, +) from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport from pipecat.services.deepgram.stt import DeepgramSTTService @@ -122,7 +125,7 @@ tools = ToolsSchema( ), FunctionSchema( name="save_conversation", - description="Save the current conversatione. Use this function to persist the current conversation to external storage.", + description="Save the current conversation. Use this function to persist the current conversation to external storage.", properties={}, required=[], ), @@ -153,17 +156,14 @@ transport_params = { "daily": lambda: DailyParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "twilio": lambda: FastAPIWebsocketParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "webrtc": lambda: TransportParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), } @@ -214,7 +214,10 @@ Remember, your responses should be short. Just one or two sentences, usually.""" llm.register_function("load_conversation", load_conversation) context = LLMContext([{"role": "user", "content": "Say hello!"}], tools) - user_aggregator, assistant_aggregator = LLMContextAggregatorPair(context) + user_aggregator, assistant_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) pipeline = Pipeline( [ diff --git a/examples/foundational/20c-persistent-context-anthropic.py b/examples/foundational/20c-persistent-context-anthropic.py index e7a8335ba..1b74c87e1 100644 --- a/examples/foundational/20c-persistent-context-anthropic.py +++ b/examples/foundational/20c-persistent-context-anthropic.py @@ -94,7 +94,7 @@ async def load_conversation(params: FunctionCallParams): await params.result_callback({"success": False, "error": str(e)}) -system_instruction = "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a succinct, creative and helpful way. Prefer responses that are one sentence long unless you are asked for a longer or more detailed response." +system_instruction = "You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a succinct, creative and helpful way. Prefer responses that are one sentence long unless you are asked for a longer or more detailed response." weather_function = FunctionSchema( name="get_current_weather", diff --git a/examples/foundational/20d-persistent-context-gemini.py b/examples/foundational/20d-persistent-context-gemini.py index 638728331..ae37e19ce 100644 --- a/examples/foundational/20d-persistent-context-gemini.py +++ b/examples/foundational/20d-persistent-context-gemini.py @@ -122,8 +122,8 @@ async def load_conversation(params: FunctionCallParams): await params.result_callback({"success": False, "error": str(e)}) -system_instruction = """You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your -capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that +system_instruction = """You are a helpful assistant in a voice conversation. Your goal is to demonstrate your +capabilities in a succinct way. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Keep responses concise. Respond to what the user said in a creative can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way. diff --git a/examples/foundational/20e-persistent-context-aws-nova-sonic.py b/examples/foundational/20e-persistent-context-aws-nova-sonic.py index 9614328f2..3d1043d18 100644 --- a/examples/foundational/20e-persistent-context-aws-nova-sonic.py +++ b/examples/foundational/20e-persistent-context-aws-nova-sonic.py @@ -21,7 +21,10 @@ from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.task import PipelineParams, PipelineTask from pipecat.processors.aggregators.llm_context import LLMContext -from pipecat.processors.aggregators.llm_response_universal import LLMContextAggregatorPair +from pipecat.processors.aggregators.llm_response_universal import ( + LLMContextAggregatorPair, + LLMUserAggregatorParams, +) from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport from pipecat.services.aws.nova_sonic.llm import AWSNovaSonicLLMService @@ -189,17 +192,14 @@ transport_params = { "daily": lambda: DailyParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "twilio": lambda: FastAPIWebsocketParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "webrtc": lambda: TransportParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), } @@ -236,7 +236,10 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm.register_function("load_conversation", load_conversation) context = LLMContext(tools=tools) - user_aggregator, assistant_aggregator = LLMContextAggregatorPair(context) + user_aggregator, assistant_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) pipeline = Pipeline( [ diff --git a/examples/foundational/21-tavus-transport.py b/examples/foundational/21-tavus-transport.py index 1ac54ff19..f09902fcd 100644 --- a/examples/foundational/21-tavus-transport.py +++ b/examples/foundational/21-tavus-transport.py @@ -59,7 +59,7 @@ async def main(): llm = GoogleLLMService( api_key=os.getenv("GOOGLE_API_KEY"), settings=GoogleLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/21a-tavus-video-service.py b/examples/foundational/21a-tavus-video-service.py index f716d88d8..6e03a2418 100644 --- a/examples/foundational/21a-tavus-video-service.py +++ b/examples/foundational/21a-tavus-video-service.py @@ -69,7 +69,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = GoogleLLMService( api_key=os.getenv("GOOGLE_API_KEY"), settings=GoogleLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/22-filter-incomplete-turns.py b/examples/foundational/22-filter-incomplete-turns.py index 454e22722..8a3001366 100644 --- a/examples/foundational/22-filter-incomplete-turns.py +++ b/examples/foundational/22-filter-incomplete-turns.py @@ -71,7 +71,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/23-bot-background-sound.py b/examples/foundational/23-bot-background-sound.py index 6f6cf8eb7..0229db419 100644 --- a/examples/foundational/23-bot-background-sound.py +++ b/examples/foundational/23-bot-background-sound.py @@ -83,7 +83,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/25-google-audio-in.py b/examples/foundational/25-google-audio-in.py index 572e0e213..7a6c910e7 100644 --- a/examples/foundational/25-google-audio-in.py +++ b/examples/foundational/25-google-audio-in.py @@ -28,7 +28,10 @@ from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.task import PipelineParams, PipelineTask from pipecat.processors.aggregators.llm_context import LLMContext -from pipecat.processors.aggregators.llm_response_universal import LLMContextAggregatorPair +from pipecat.processors.aggregators.llm_response_universal import ( + LLMContextAggregatorPair, + LLMUserAggregatorParams, +) from pipecat.processors.frame_processor import FrameProcessor from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport @@ -44,7 +47,7 @@ load_dotenv(override=True) # The system prompt for the main conversation. # conversation_system_message = """ -You are a helpful LLM in a WebRTC call. Your goals are to be helpful and brief in your responses. Respond with one or two sentences at most, unless you are asked to +You are a helpful LLM in a voice call. Your goals are to be helpful and brief in your responses. Respond with one or two sentences at most, unless you are asked to respond at more length. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. """ @@ -270,17 +273,14 @@ transport_params = { "daily": lambda: DailyParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "twilio": lambda: FastAPIWebsocketParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "webrtc": lambda: TransportParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), } @@ -324,7 +324,10 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): ] context = LLMContext(messages) - context_aggregator = LLMContextAggregatorPair(context) + context_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) audio_collector = UserAudioCollector(context, context_aggregator.user()) input_transcription_context_filter = InputTranscriptionContextFilter() transcription_frames_emitter = InputTranscriptionFrameEmitter() diff --git a/examples/foundational/26h-gemini-live-vertex-function-calling.py b/examples/foundational/26h-gemini-live-vertex-function-calling.py index 18e417f22..7dd894fd1 100644 --- a/examples/foundational/26h-gemini-live-vertex-function-calling.py +++ b/examples/foundational/26h-gemini-live-vertex-function-calling.py @@ -26,7 +26,7 @@ from pipecat.processors.aggregators.llm_response_universal import ( ) from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport -from pipecat.services.google.gemini_live.llm_vertex import GeminiLiveVertexLLMService +from pipecat.services.google.gemini_live.vertex.llm import GeminiLiveVertexLLMService from pipecat.services.llm_service import FunctionCallParams from pipecat.transports.base_transport import BaseTransport, TransportParams from pipecat.transports.daily.transport import DailyParams diff --git a/examples/foundational/27-simli-layer.py b/examples/foundational/27-simli-layer.py index c8a9ad663..76c2dd99d 100644 --- a/examples/foundational/27-simli-layer.py +++ b/examples/foundational/27-simli-layer.py @@ -73,8 +73,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - model="gpt-4o-mini", - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/28-user-assistant-turns.py b/examples/foundational/28-user-assistant-turns.py index b439794b5..d1c053710 100644 --- a/examples/foundational/28-user-assistant-turns.py +++ b/examples/foundational/28-user-assistant-turns.py @@ -130,7 +130,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative, helpful, and brief way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/29-turn-tracking-observer.py b/examples/foundational/29-turn-tracking-observer.py index f17dd0260..e4c33a379 100644 --- a/examples/foundational/29-turn-tracking-observer.py +++ b/examples/foundational/29-turn-tracking-observer.py @@ -81,7 +81,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/30-observer.py b/examples/foundational/30-observer.py index 227a8978b..523e09583 100644 --- a/examples/foundational/30-observer.py +++ b/examples/foundational/30-observer.py @@ -112,7 +112,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/34-audio-recording.py b/examples/foundational/34-audio-recording.py index 6ddfa6033..68354e0dd 100644 --- a/examples/foundational/34-audio-recording.py +++ b/examples/foundational/34-audio-recording.py @@ -120,7 +120,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful assistant demonstrating audio recording capabilities. Keep your responses brief and clear.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/35-pattern-pair-voice-switching.py b/examples/foundational/35-pattern-pair-voice-switching.py index ed9bb3973..c246ce599 100644 --- a/examples/foundational/35-pattern-pair-voice-switching.py +++ b/examples/foundational/35-pattern-pair-voice-switching.py @@ -24,7 +24,7 @@ The PatternPairAggregator: - Returns processed text at sentence boundaries Requirements: - - OpenAI API key (for GPT-4o) + - OpenAI API key - Cartesia API key (for text-to-speech) - Daily API key (for video/audio transport) diff --git a/examples/foundational/37-mem0.py b/examples/foundational/37-mem0.py index 06883a43e..e572d2dbe 100644 --- a/examples/foundational/37-mem0.py +++ b/examples/foundational/37-mem0.py @@ -24,7 +24,7 @@ Example usage (run from pipecat root directory): $ python examples/foundational/37-mem0.py Requirements: - - OpenAI API key (for GPT-4o-mini) + - OpenAI API key - ElevenLabs API key (for text-to-speech) - Daily API key (for video/audio transport) - Mem0 API key (for cloud-based memory storage) @@ -226,7 +226,6 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - model="gpt-4o-mini", system_instruction="""You are a personal assistant. You can remember things about the person you are talking to. Some Guidelines: - Make sure your responses are friendly yet short and concise. diff --git a/examples/foundational/38-smart-turn-fal.py b/examples/foundational/38-smart-turn-fal.py index b2de247d7..b8a62626a 100644 --- a/examples/foundational/38-smart-turn-fal.py +++ b/examples/foundational/38-smart-turn-fal.py @@ -69,7 +69,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/38a-smart-turn-local-coreml.py b/examples/foundational/38a-smart-turn-local-coreml.py index 3073ba0dd..fc2f1d14d 100644 --- a/examples/foundational/38a-smart-turn-local-coreml.py +++ b/examples/foundational/38a-smart-turn-local-coreml.py @@ -84,7 +84,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/38b-smart-turn-local.py b/examples/foundational/38b-smart-turn-local.py index de8b214ea..7e4a0abd8 100644 --- a/examples/foundational/38b-smart-turn-local.py +++ b/examples/foundational/38b-smart-turn-local.py @@ -66,7 +66,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/39-mcp-stdio.py b/examples/foundational/39-mcp-stdio.py index d46c034d1..48382046d 100644 --- a/examples/foundational/39-mcp-stdio.py +++ b/examples/foundational/39-mcp-stdio.py @@ -143,7 +143,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): ) system_prompt = f""" - You are a helpful LLM in a WebRTC call. + You are a helpful LLM in a voice call. Your goal is to demonstrate your capabilities in a succinct way. You have access to tools to search the Rijksmuseum collection. Offer, for example, to show a floral still life, use the `search_artwork` tool. diff --git a/examples/foundational/39a-mcp-streamable-http.py b/examples/foundational/39a-mcp-streamable-http.py index b71938e43..5ddf53264 100644 --- a/examples/foundational/39a-mcp-streamable-http.py +++ b/examples/foundational/39a-mcp-streamable-http.py @@ -64,7 +64,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): ) system_prompt = f""" - You are a helpful LLM in a WebRTC call. + You are a helpful LLM in a voice call. Your goal is to answer questions about the user's GitHub repositories and account. You have access to a number of tools provided by Github. Use any and all tools to help users. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. diff --git a/examples/foundational/39b-mcp-streamable-http-gemini-live.py b/examples/foundational/39b-mcp-streamable-http-gemini-live.py index 19a5aa1b6..eb276a91a 100644 --- a/examples/foundational/39b-mcp-streamable-http-gemini-live.py +++ b/examples/foundational/39b-mcp-streamable-http-gemini-live.py @@ -86,7 +86,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): logger.exception("error trace:") system = f""" - You are a helpful LLM in a WebRTC call. + You are a helpful LLM in a voice call. Your goal is to answer questions about the user's GitHub repositories and account. You have access to a number of tools provided by Github. Use any and all tools to help users. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. diff --git a/examples/foundational/39c-multiple-mcp.py b/examples/foundational/39c-multiple-mcp.py index eabfeccab..9d449251d 100644 --- a/examples/foundational/39c-multiple-mcp.py +++ b/examples/foundational/39c-multiple-mcp.py @@ -126,7 +126,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): ) system_prompt = f""" - You are a helpful LLM in a WebRTC call. + You are a helpful LLM in a voice call. Your goal is to demonstrate your capabilities in a succinct way. You have access to tools to search the Rijksmuseum collection and the user's GitHub repositories and account. Offer, for example, to show a floral still life, use the `search_artwork` tool. diff --git a/examples/foundational/40-aws-nova-sonic.py b/examples/foundational/40-aws-nova-sonic.py index 79a3d168c..6f7c745ce 100644 --- a/examples/foundational/40-aws-nova-sonic.py +++ b/examples/foundational/40-aws-nova-sonic.py @@ -24,6 +24,7 @@ from pipecat.processors.aggregators.llm_context import LLMContext from pipecat.processors.aggregators.llm_response_universal import ( AssistantTurnStoppedMessage, LLMContextAggregatorPair, + LLMUserAggregatorParams, UserTurnStoppedMessage, ) from pipecat.runner.types import RunnerArguments @@ -87,17 +88,14 @@ transport_params = { "daily": lambda: DailyParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "twilio": lambda: FastAPIWebsocketParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "webrtc": lambda: TransportParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), } @@ -147,7 +145,10 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): # Set up context and context management. context = LLMContext(tools=tools) - user_aggregator, assistant_aggregator = LLMContextAggregatorPair(context) + user_aggregator, assistant_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) # Build the pipeline pipeline = Pipeline( diff --git a/examples/foundational/42-interruption-config.py b/examples/foundational/42-interruption-config.py index d690f60f0..64ea2cee0 100644 --- a/examples/foundational/42-interruption-config.py +++ b/examples/foundational/42-interruption-config.py @@ -67,7 +67,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/44-voicemail-detection.py b/examples/foundational/44-voicemail-detection.py index df47bb945..854e546ec 100644 --- a/examples/foundational/44-voicemail-detection.py +++ b/examples/foundational/44-voicemail-detection.py @@ -64,7 +64,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) classifier_llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY")) diff --git a/examples/foundational/45-before-and-after-events.py b/examples/foundational/45-before-and-after-events.py index 3c583546f..3a36a626a 100644 --- a/examples/foundational/45-before-and-after-events.py +++ b/examples/foundational/45-before-and-after-events.py @@ -75,7 +75,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/47-sentry-metrics.py b/examples/foundational/47-sentry-metrics.py index 9fe35d4c4..3294727dc 100644 --- a/examples/foundational/47-sentry-metrics.py +++ b/examples/foundational/47-sentry-metrics.py @@ -76,7 +76,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): api_key=os.getenv("OPENAI_API_KEY"), metrics=SentryMetrics(), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/48-service-switcher.py b/examples/foundational/48-service-switcher.py index e37925006..9225963f7 100644 --- a/examples/foundational/48-service-switcher.py +++ b/examples/foundational/48-service-switcher.py @@ -17,7 +17,7 @@ from pipecat.frames.frames import LLMRunFrame, ManuallySwitchServiceFrame from pipecat.pipeline.llm_switcher import LLMSwitcher from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner -from pipecat.pipeline.service_switcher import ServiceSwitcher, ServiceSwitcherStrategyManual +from pipecat.pipeline.service_switcher import ServiceSwitcher from pipecat.pipeline.task import PipelineParams, PipelineTask from pipecat.processors.aggregators.llm_context import LLMContext from pipecat.processors.aggregators.llm_response_universal import ( @@ -96,9 +96,8 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): stt_cartesia = CartesiaSTTService(api_key=os.getenv("CARTESIA_API_KEY")) stt_deepgram = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY")) - stt_switcher = ServiceSwitcher( - services=[stt_cartesia, stt_deepgram], strategy_type=ServiceSwitcherStrategyManual - ) + # Uses ServiceSwitcherStrategyManual by default + stt_switcher = ServiceSwitcher(services=[stt_cartesia, stt_deepgram]) tts_cartesia = CartesiaTTSService( api_key=os.getenv("CARTESIA_API_KEY"), @@ -112,11 +111,10 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): voice="aura-2-helena-en", ), ) - tts_switcher = ServiceSwitcher( - services=[tts_cartesia, tts_deepgram], strategy_type=ServiceSwitcherStrategyManual - ) + # Uses ServiceSwitcherStrategyManual by default + tts_switcher = ServiceSwitcher(services=[tts_cartesia, tts_deepgram]) - system_prompt = "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way." + system_prompt = "You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way." llm_openai = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), @@ -126,9 +124,8 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): api_key=os.getenv("GOOGLE_API_KEY"), settings=GoogleLLMService.Settings(system_instruction=system_prompt), ) - llm_switcher = LLMSwitcher( - llms=[llm_openai, llm_google], strategy_type=ServiceSwitcherStrategyManual - ) + # Uses ServiceSwitcherStrategyManual by default + llm_switcher = LLMSwitcher(llms=[llm_openai, llm_google]) # Register a "classic" function llm_switcher.register_function("get_current_weather", fetch_weather_from_api) # Register a "direct" function diff --git a/examples/foundational/49a-thinking-anthropic.py b/examples/foundational/49a-thinking-anthropic.py index 8825ce7c5..0495d5e97 100644 --- a/examples/foundational/49a-thinking-anthropic.py +++ b/examples/foundational/49a-thinking-anthropic.py @@ -22,7 +22,7 @@ from pipecat.processors.aggregators.llm_response_universal import ( ) from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport -from pipecat.services.anthropic.llm import AnthropicLLMService, AnthropicThinkingConfig +from pipecat.services.anthropic.llm import AnthropicLLMService from pipecat.services.cartesia.tts import CartesiaTTSService from pipecat.services.deepgram.stt import DeepgramSTTService from pipecat.transports.base_transport import BaseTransport, TransportParams @@ -64,11 +64,11 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = AnthropicLLMService( api_key=os.getenv("ANTHROPIC_API_KEY"), settings=AnthropicLLMService.Settings( - thinking=AnthropicThinkingConfig( + thinking=AnthropicLLMService.ThinkingConfig( type="enabled", budget_tokens=2048, ), - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/49b-thinking-google.py b/examples/foundational/49b-thinking-google.py index ed7a393b0..1cfeba456 100644 --- a/examples/foundational/49b-thinking-google.py +++ b/examples/foundational/49b-thinking-google.py @@ -24,7 +24,7 @@ from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport from pipecat.services.cartesia.tts import CartesiaTTSService from pipecat.services.deepgram.stt import DeepgramSTTService -from pipecat.services.google.llm import GoogleLLMService, GoogleThinkingConfig +from pipecat.services.google.llm import GoogleLLMService from pipecat.transports.base_transport import BaseTransport, TransportParams from pipecat.transports.daily.transport import DailyParams from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams @@ -65,11 +65,11 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): api_key=os.getenv("GOOGLE_API_KEY"), # model="gemini-3-pro-preview", # A more powerful reasoning model, but slower settings=GoogleLLMService.Settings( - thinking=GoogleThinkingConfig( + thinking=GoogleLLMService.ThinkingConfig( thinking_budget=-1, # Dynamic thinking include_thoughts=True, ), - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/49c-thinking-functions-anthropic.py b/examples/foundational/49c-thinking-functions-anthropic.py index bc9f334c8..1ce5832fc 100644 --- a/examples/foundational/49c-thinking-functions-anthropic.py +++ b/examples/foundational/49c-thinking-functions-anthropic.py @@ -23,7 +23,7 @@ from pipecat.processors.aggregators.llm_response_universal import ( ) from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport -from pipecat.services.anthropic.llm import AnthropicLLMService, AnthropicThinkingConfig +from pipecat.services.anthropic.llm import AnthropicLLMService from pipecat.services.cartesia.tts import CartesiaTTSService from pipecat.services.deepgram.stt import DeepgramSTTService from pipecat.services.llm_service import FunctionCallParams @@ -85,11 +85,11 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = AnthropicLLMService( api_key=os.getenv("ANTHROPIC_API_KEY"), settings=AnthropicLLMService.Settings( - thinking=AnthropicThinkingConfig( + thinking=AnthropicLLMService.ThinkingConfig( type="enabled", budget_tokens=2048, ), - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/49d-thinking-functions-google.py b/examples/foundational/49d-thinking-functions-google.py index e8ecefcdd..0b62fbf2e 100644 --- a/examples/foundational/49d-thinking-functions-google.py +++ b/examples/foundational/49d-thinking-functions-google.py @@ -25,7 +25,7 @@ from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport from pipecat.services.cartesia.tts import CartesiaTTSService from pipecat.services.deepgram.stt import DeepgramSTTService -from pipecat.services.google.llm import GoogleLLMService, GoogleThinkingConfig +from pipecat.services.google.llm import GoogleLLMService from pipecat.services.llm_service import FunctionCallParams from pipecat.transports.base_transport import BaseTransport, TransportParams from pipecat.transports.daily.transport import DailyParams @@ -86,11 +86,11 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): api_key=os.getenv("GOOGLE_API_KEY"), # model="gemini-3-pro-preview", # A more powerful reasoning model, but slower settings=GoogleLLMService.Settings( - thinking=GoogleThinkingConfig( + thinking=GoogleLLMService.ThinkingConfig( thinking_budget=-1, # Dynamic thinking include_thoughts=True, ), - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/53-concurrent-llm-evaluation.py b/examples/foundational/53-concurrent-llm-evaluation.py index 46b2847e9..69cebd0ac 100644 --- a/examples/foundational/53-concurrent-llm-evaluation.py +++ b/examples/foundational/53-concurrent-llm-evaluation.py @@ -70,7 +70,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): openai_llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/53-concurrent-llm-rtvi-ignored-sources.py b/examples/foundational/53-concurrent-llm-rtvi-ignored-sources.py index d27bd86a1..481d865e1 100644 --- a/examples/foundational/53-concurrent-llm-rtvi-ignored-sources.py +++ b/examples/foundational/53-concurrent-llm-rtvi-ignored-sources.py @@ -76,7 +76,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): main_llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/54-context-summarization-openai.py b/examples/foundational/54-context-summarization-openai.py index a536fc813..060eda8f7 100644 --- a/examples/foundational/54-context-summarization-openai.py +++ b/examples/foundational/54-context-summarization-openai.py @@ -89,7 +89,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way. You have access to tools to get the current weather - use them when relevant.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way. You have access to tools to get the current weather - use them when relevant.", ), ) diff --git a/examples/foundational/54a-context-summarization-google.py b/examples/foundational/54a-context-summarization-google.py index 942567c83..2727d34a8 100644 --- a/examples/foundational/54a-context-summarization-google.py +++ b/examples/foundational/54a-context-summarization-google.py @@ -89,7 +89,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = GoogleLLMService( api_key=os.getenv("GOOGLE_API_KEY"), settings=GoogleLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way. You have access to tools to get the current weather - use them when relevant.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way. You have access to tools to get the current weather - use them when relevant.", ), ) diff --git a/examples/foundational/54b-context-summarization-manual-openai.py b/examples/foundational/54b-context-summarization-manual-openai.py index 520e1c996..e6bdcaf00 100644 --- a/examples/foundational/54b-context-summarization-manual-openai.py +++ b/examples/foundational/54b-context-summarization-manual-openai.py @@ -81,7 +81,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): ), ) - system_prompt = """You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your + system_prompt = """You are a helpful LLM in a voice call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way. diff --git a/examples/foundational/54c-context-summarization-dedicated-llm.py b/examples/foundational/54c-context-summarization-dedicated-llm.py index 9f8f0b35d..ed56e343e 100644 --- a/examples/foundational/54c-context-summarization-dedicated-llm.py +++ b/examples/foundational/54c-context-summarization-dedicated-llm.py @@ -98,7 +98,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): ), ) - system_prompt = """You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your + system_prompt = """You are a helpful LLM in a voice call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way. diff --git a/examples/foundational/55a-update-settings-deepgram-flux-stt.py b/examples/foundational/55a-update-settings-deepgram-flux-stt.py index 237e5c3f7..d710015bd 100644 --- a/examples/foundational/55a-update-settings-deepgram-flux-stt.py +++ b/examples/foundational/55a-update-settings-deepgram-flux-stt.py @@ -63,7 +63,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55a-update-settings-deepgram-sagemaker-stt.py b/examples/foundational/55a-update-settings-deepgram-sagemaker-stt.py index 4b071d733..ebe3d4cce 100644 --- a/examples/foundational/55a-update-settings-deepgram-sagemaker-stt.py +++ b/examples/foundational/55a-update-settings-deepgram-sagemaker-stt.py @@ -66,7 +66,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55a-update-settings-deepgram-stt.py b/examples/foundational/55a-update-settings-deepgram-stt.py index 3c39c8cd5..46adaad6b 100644 --- a/examples/foundational/55a-update-settings-deepgram-stt.py +++ b/examples/foundational/55a-update-settings-deepgram-stt.py @@ -63,7 +63,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55b-update-settings-azure-stt.py b/examples/foundational/55b-update-settings-azure-stt.py index 59a411caf..8e5d3bfe3 100644 --- a/examples/foundational/55b-update-settings-azure-stt.py +++ b/examples/foundational/55b-update-settings-azure-stt.py @@ -66,7 +66,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55c-update-settings-google-stt.py b/examples/foundational/55c-update-settings-google-stt.py index c6d44da12..be62d90ad 100644 --- a/examples/foundational/55c-update-settings-google-stt.py +++ b/examples/foundational/55c-update-settings-google-stt.py @@ -63,7 +63,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55d-update-settings-assemblyai-stt.py b/examples/foundational/55d-update-settings-assemblyai-stt.py index f62a968eb..213873ab5 100644 --- a/examples/foundational/55d-update-settings-assemblyai-stt.py +++ b/examples/foundational/55d-update-settings-assemblyai-stt.py @@ -67,7 +67,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call demonstrating dynamic keyterms updates. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Try saying difficult names like 'Xiomara', 'Saoirse', or 'Krzystof' to test transcription accuracy.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Try saying difficult names like 'Xiomara', 'Saoirse', or 'Krzystof' to test transcription accuracy.", ), ) diff --git a/examples/foundational/55e-update-settings-gladia-stt.py b/examples/foundational/55e-update-settings-gladia-stt.py index 6ca8fa725..307ea2954 100644 --- a/examples/foundational/55e-update-settings-gladia-stt.py +++ b/examples/foundational/55e-update-settings-gladia-stt.py @@ -63,7 +63,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55f-update-settings-elevenlabs-realtime-stt.py b/examples/foundational/55f-update-settings-elevenlabs-realtime-stt.py index ee3244ffe..4fc351de0 100644 --- a/examples/foundational/55f-update-settings-elevenlabs-realtime-stt.py +++ b/examples/foundational/55f-update-settings-elevenlabs-realtime-stt.py @@ -63,7 +63,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55g-update-settings-elevenlabs-stt.py b/examples/foundational/55g-update-settings-elevenlabs-stt.py index 6307264fc..d610acea6 100644 --- a/examples/foundational/55g-update-settings-elevenlabs-stt.py +++ b/examples/foundational/55g-update-settings-elevenlabs-stt.py @@ -68,7 +68,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55h-update-settings-speechmatics-stt.py b/examples/foundational/55h-update-settings-speechmatics-stt.py index df6d9575b..eaa1874f6 100644 --- a/examples/foundational/55h-update-settings-speechmatics-stt.py +++ b/examples/foundational/55h-update-settings-speechmatics-stt.py @@ -70,7 +70,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55i-update-settings-whisper-api-stt.py b/examples/foundational/55i-update-settings-whisper-api-stt.py index e9ef61cce..179a65f83 100644 --- a/examples/foundational/55i-update-settings-whisper-api-stt.py +++ b/examples/foundational/55i-update-settings-whisper-api-stt.py @@ -69,7 +69,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55j-update-settings-sarvam-stt.py b/examples/foundational/55j-update-settings-sarvam-stt.py index 529cf461d..b65401dc5 100644 --- a/examples/foundational/55j-update-settings-sarvam-stt.py +++ b/examples/foundational/55j-update-settings-sarvam-stt.py @@ -63,7 +63,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55k-update-settings-soniox-stt.py b/examples/foundational/55k-update-settings-soniox-stt.py index 07e33b4cd..2a2548888 100644 --- a/examples/foundational/55k-update-settings-soniox-stt.py +++ b/examples/foundational/55k-update-settings-soniox-stt.py @@ -63,7 +63,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55l-update-settings-aws-transcribe-stt.py b/examples/foundational/55l-update-settings-aws-transcribe-stt.py index e18833d8d..cd9cfec11 100644 --- a/examples/foundational/55l-update-settings-aws-transcribe-stt.py +++ b/examples/foundational/55l-update-settings-aws-transcribe-stt.py @@ -63,7 +63,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55m-update-settings-cartesia-stt.py b/examples/foundational/55m-update-settings-cartesia-stt.py index 0599c9b56..acd7fb972 100644 --- a/examples/foundational/55m-update-settings-cartesia-stt.py +++ b/examples/foundational/55m-update-settings-cartesia-stt.py @@ -63,7 +63,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55n-update-settings-cartesia-http-tts.py b/examples/foundational/55n-update-settings-cartesia-http-tts.py index af4e8d5c1..328940bb5 100644 --- a/examples/foundational/55n-update-settings-cartesia-http-tts.py +++ b/examples/foundational/55n-update-settings-cartesia-http-tts.py @@ -62,7 +62,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55n-update-settings-cartesia-tts.py b/examples/foundational/55n-update-settings-cartesia-tts.py index 64afeffc2..67e0c599b 100644 --- a/examples/foundational/55n-update-settings-cartesia-tts.py +++ b/examples/foundational/55n-update-settings-cartesia-tts.py @@ -64,7 +64,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55o-update-settings-elevenlabs-http-tts.py b/examples/foundational/55o-update-settings-elevenlabs-http-tts.py index 8b6479edf..d819f2bdf 100644 --- a/examples/foundational/55o-update-settings-elevenlabs-http-tts.py +++ b/examples/foundational/55o-update-settings-elevenlabs-http-tts.py @@ -65,7 +65,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55o-update-settings-elevenlabs-tts.py b/examples/foundational/55o-update-settings-elevenlabs-tts.py index 03a692324..9afa33252 100644 --- a/examples/foundational/55o-update-settings-elevenlabs-tts.py +++ b/examples/foundational/55o-update-settings-elevenlabs-tts.py @@ -60,7 +60,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55p-update-settings-openai-tts.py b/examples/foundational/55p-update-settings-openai-tts.py index 110ee435b..a6f2b40c7 100644 --- a/examples/foundational/55p-update-settings-openai-tts.py +++ b/examples/foundational/55p-update-settings-openai-tts.py @@ -57,7 +57,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55q-update-settings-deepgram-http-tts.py b/examples/foundational/55q-update-settings-deepgram-http-tts.py index 877ec3f65..706dcc357 100644 --- a/examples/foundational/55q-update-settings-deepgram-http-tts.py +++ b/examples/foundational/55q-update-settings-deepgram-http-tts.py @@ -64,7 +64,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55q-update-settings-deepgram-sagemaker-tts.py b/examples/foundational/55q-update-settings-deepgram-sagemaker-tts.py index 270e00747..76c67095f 100644 --- a/examples/foundational/55q-update-settings-deepgram-sagemaker-tts.py +++ b/examples/foundational/55q-update-settings-deepgram-sagemaker-tts.py @@ -61,7 +61,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55q-update-settings-deepgram-tts.py b/examples/foundational/55q-update-settings-deepgram-tts.py index 335526768..39a6ea7b7 100644 --- a/examples/foundational/55q-update-settings-deepgram-tts.py +++ b/examples/foundational/55q-update-settings-deepgram-tts.py @@ -57,7 +57,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55r-update-settings-azure-http-tts.py b/examples/foundational/55r-update-settings-azure-http-tts.py index 56cb23854..c75ba5ff4 100644 --- a/examples/foundational/55r-update-settings-azure-http-tts.py +++ b/examples/foundational/55r-update-settings-azure-http-tts.py @@ -60,7 +60,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55r-update-settings-azure-tts.py b/examples/foundational/55r-update-settings-azure-tts.py index 5ccaa99bc..851e16de3 100644 --- a/examples/foundational/55r-update-settings-azure-tts.py +++ b/examples/foundational/55r-update-settings-azure-tts.py @@ -60,7 +60,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55s-update-settings-google-http-tts.py b/examples/foundational/55s-update-settings-google-http-tts.py index fbe44ff07..bb9b1862b 100644 --- a/examples/foundational/55s-update-settings-google-http-tts.py +++ b/examples/foundational/55s-update-settings-google-http-tts.py @@ -57,7 +57,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55s-update-settings-google-stream-tts.py b/examples/foundational/55s-update-settings-google-stream-tts.py index b3310975a..68cfd2aaa 100644 --- a/examples/foundational/55s-update-settings-google-stream-tts.py +++ b/examples/foundational/55s-update-settings-google-stream-tts.py @@ -57,7 +57,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55t-update-settings-piper-http-tts.py b/examples/foundational/55t-update-settings-piper-http-tts.py index 0b208ccbc..57770cc91 100644 --- a/examples/foundational/55t-update-settings-piper-http-tts.py +++ b/examples/foundational/55t-update-settings-piper-http-tts.py @@ -64,7 +64,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55t-update-settings-piper-tts.py b/examples/foundational/55t-update-settings-piper-tts.py index 6e9d79a72..5416b7e58 100644 --- a/examples/foundational/55t-update-settings-piper-tts.py +++ b/examples/foundational/55t-update-settings-piper-tts.py @@ -61,7 +61,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55u-update-settings-rime-http-tts.py b/examples/foundational/55u-update-settings-rime-http-tts.py index 07bb7e9a9..1d7154107 100644 --- a/examples/foundational/55u-update-settings-rime-http-tts.py +++ b/examples/foundational/55u-update-settings-rime-http-tts.py @@ -65,7 +65,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55u-update-settings-rime-tts.py b/examples/foundational/55u-update-settings-rime-tts.py index bbbdf534d..33a811228 100644 --- a/examples/foundational/55u-update-settings-rime-tts.py +++ b/examples/foundational/55u-update-settings-rime-tts.py @@ -60,7 +60,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55v-update-settings-lmnt-tts.py b/examples/foundational/55v-update-settings-lmnt-tts.py index 30f8135a0..1a6c9bd07 100644 --- a/examples/foundational/55v-update-settings-lmnt-tts.py +++ b/examples/foundational/55v-update-settings-lmnt-tts.py @@ -60,7 +60,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55w-update-settings-fish-tts.py b/examples/foundational/55w-update-settings-fish-tts.py index f54d97684..6ba018d0e 100644 --- a/examples/foundational/55w-update-settings-fish-tts.py +++ b/examples/foundational/55w-update-settings-fish-tts.py @@ -62,7 +62,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55x-update-settings-minimax-tts.py b/examples/foundational/55x-update-settings-minimax-tts.py index 53a120811..c91e728a4 100644 --- a/examples/foundational/55x-update-settings-minimax-tts.py +++ b/examples/foundational/55x-update-settings-minimax-tts.py @@ -63,7 +63,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55y-update-settings-groq-tts.py b/examples/foundational/55y-update-settings-groq-tts.py index c58b672c8..2e1a7a09d 100644 --- a/examples/foundational/55y-update-settings-groq-tts.py +++ b/examples/foundational/55y-update-settings-groq-tts.py @@ -57,7 +57,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55z-update-settings-hume-tts.py b/examples/foundational/55z-update-settings-hume-tts.py index 3af8b5fe7..4a1fc061b 100644 --- a/examples/foundational/55z-update-settings-hume-tts.py +++ b/examples/foundational/55z-update-settings-hume-tts.py @@ -60,7 +60,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55za-update-settings-neuphonic-http-tts.py b/examples/foundational/55za-update-settings-neuphonic-http-tts.py index daaef4750..12d44aa02 100644 --- a/examples/foundational/55za-update-settings-neuphonic-http-tts.py +++ b/examples/foundational/55za-update-settings-neuphonic-http-tts.py @@ -62,7 +62,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55za-update-settings-neuphonic-tts.py b/examples/foundational/55za-update-settings-neuphonic-tts.py index dc9d3cb6f..f3b2970a6 100644 --- a/examples/foundational/55za-update-settings-neuphonic-tts.py +++ b/examples/foundational/55za-update-settings-neuphonic-tts.py @@ -57,7 +57,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zb-update-settings-inworld-http-tts.py b/examples/foundational/55zb-update-settings-inworld-http-tts.py index bb031c231..fb96a8eeb 100644 --- a/examples/foundational/55zb-update-settings-inworld-http-tts.py +++ b/examples/foundational/55zb-update-settings-inworld-http-tts.py @@ -61,7 +61,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zb-update-settings-inworld-tts.py b/examples/foundational/55zb-update-settings-inworld-tts.py index f1581bd44..07ee4d674 100644 --- a/examples/foundational/55zb-update-settings-inworld-tts.py +++ b/examples/foundational/55zb-update-settings-inworld-tts.py @@ -57,7 +57,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zc-update-settings-gemini-tts.py b/examples/foundational/55zc-update-settings-gemini-tts.py index 0bf027f20..fd7a8a5f7 100644 --- a/examples/foundational/55zc-update-settings-gemini-tts.py +++ b/examples/foundational/55zc-update-settings-gemini-tts.py @@ -66,7 +66,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zd-update-settings-aws-polly-tts.py b/examples/foundational/55zd-update-settings-aws-polly-tts.py index 38a2544f2..d293e12ed 100644 --- a/examples/foundational/55zd-update-settings-aws-polly-tts.py +++ b/examples/foundational/55zd-update-settings-aws-polly-tts.py @@ -57,7 +57,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55ze-update-settings-sarvam-http-tts.py b/examples/foundational/55ze-update-settings-sarvam-http-tts.py index e00dfc621..5bc5da404 100644 --- a/examples/foundational/55ze-update-settings-sarvam-http-tts.py +++ b/examples/foundational/55ze-update-settings-sarvam-http-tts.py @@ -61,7 +61,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55ze-update-settings-sarvam-tts.py b/examples/foundational/55ze-update-settings-sarvam-tts.py index 6b6a0709a..8df7f781b 100644 --- a/examples/foundational/55ze-update-settings-sarvam-tts.py +++ b/examples/foundational/55ze-update-settings-sarvam-tts.py @@ -57,7 +57,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zf-update-settings-camb-tts.py b/examples/foundational/55zf-update-settings-camb-tts.py index 5f0840857..66a90b6a8 100644 --- a/examples/foundational/55zf-update-settings-camb-tts.py +++ b/examples/foundational/55zf-update-settings-camb-tts.py @@ -58,7 +58,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) @@ -96,9 +96,11 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): await task.queue_frames([LLMRunFrame()]) await asyncio.sleep(10) - logger.info("Updating Camb TTS settings: language -> Spanish") + logger.info("Updating Camb TTS settings: language -> Spanish, voice -> Pirate Captain") await task.queue_frame( - TTSUpdateSettingsFrame(delta=CambTTSService.Settings(language=Language.ES)) + TTSUpdateSettingsFrame( + delta=CambTTSService.Settings(language=Language.ES, voice=147319) + ) ) @transport.event_handler("on_client_disconnected") diff --git a/examples/foundational/55zg-update-settings-kokoro-tts.py b/examples/foundational/55zg-update-settings-kokoro-tts.py index 69fb14512..2ef247f66 100644 --- a/examples/foundational/55zg-update-settings-kokoro-tts.py +++ b/examples/foundational/55zg-update-settings-kokoro-tts.py @@ -61,7 +61,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zh-update-settings-resembleai-tts.py b/examples/foundational/55zh-update-settings-resembleai-tts.py index befa0ce65..45f8785a0 100644 --- a/examples/foundational/55zh-update-settings-resembleai-tts.py +++ b/examples/foundational/55zh-update-settings-resembleai-tts.py @@ -60,7 +60,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zi-update-settings-azure-llm.py b/examples/foundational/55zi-update-settings-azure-llm.py index 5fbd796ca..20bae3aac 100644 --- a/examples/foundational/55zi-update-settings-azure-llm.py +++ b/examples/foundational/55zi-update-settings-azure-llm.py @@ -64,7 +64,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): endpoint=os.getenv("AZURE_CHATGPT_ENDPOINT"), settings=AzureLLMService.Settings( model=os.getenv("AZURE_CHATGPT_MODEL"), - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zi-update-settings-openai-llm.py b/examples/foundational/55zi-update-settings-openai-llm.py index 76a14a3f0..5862e3cab 100644 --- a/examples/foundational/55zi-update-settings-openai-llm.py +++ b/examples/foundational/55zi-update-settings-openai-llm.py @@ -62,7 +62,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zi-update-settings-openai-responses-llm.py b/examples/foundational/55zi-update-settings-openai-responses-llm.py new file mode 100644 index 000000000..61bd8329e --- /dev/null +++ b/examples/foundational/55zi-update-settings-openai-responses-llm.py @@ -0,0 +1,127 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import asyncio +import os + +from dotenv import load_dotenv +from loguru import logger + +from pipecat.audio.vad.silero import SileroVADAnalyzer +from pipecat.frames.frames import LLMRunFrame, LLMUpdateSettingsFrame +from pipecat.pipeline.pipeline import Pipeline +from pipecat.pipeline.runner import PipelineRunner +from pipecat.pipeline.task import PipelineParams, PipelineTask +from pipecat.processors.aggregators.llm_context import LLMContext +from pipecat.processors.aggregators.llm_response_universal import ( + LLMContextAggregatorPair, + LLMUserAggregatorParams, +) +from pipecat.runner.types import RunnerArguments +from pipecat.runner.utils import create_transport +from pipecat.services.cartesia.tts import CartesiaTTSService +from pipecat.services.deepgram.stt import DeepgramSTTService +from pipecat.services.openai.responses.llm import OpenAIResponsesLLMService +from pipecat.transports.base_transport import BaseTransport, TransportParams +from pipecat.transports.daily.transport import DailyParams +from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams + +load_dotenv(override=True) + +transport_params = { + "daily": lambda: DailyParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), + "twilio": lambda: FastAPIWebsocketParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), + "webrtc": lambda: TransportParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), +} + + +async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): + logger.info(f"Starting bot") + + stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY")) + + tts = CartesiaTTSService( + api_key=os.getenv("CARTESIA_API_KEY"), + settings=CartesiaTTSService.Settings( + voice="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady + ), + ) + + llm = OpenAIResponsesLLMService( + api_key=os.getenv("OPENAI_API_KEY"), + settings=OpenAIResponsesLLMService.Settings( + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", + ), + ) + + context = LLMContext() + user_aggregator, assistant_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) + + pipeline = Pipeline( + [ + transport.input(), + stt, + user_aggregator, + llm, + tts, + transport.output(), + assistant_aggregator, + ] + ) + + task = PipelineTask( + pipeline, + params=PipelineParams( + enable_metrics=True, + enable_usage_metrics=True, + ), + idle_timeout_secs=runner_args.pipeline_idle_timeout_secs, + ) + + @transport.event_handler("on_client_connected") + async def on_client_connected(transport, client): + logger.info(f"Client connected") + context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + await task.queue_frames([LLMRunFrame()]) + + await asyncio.sleep(10) + logger.info("Updating OpenAI LLM settings: temperature=0.1") + await task.queue_frame( + LLMUpdateSettingsFrame(delta=OpenAIResponsesLLMService.Settings(temperature=0.1)) + ) + + @transport.event_handler("on_client_disconnected") + async def on_client_disconnected(transport, client): + logger.info(f"Client disconnected") + await task.cancel() + + runner = PipelineRunner(handle_sigint=runner_args.handle_sigint) + + await runner.run(task) + + +async def bot(runner_args: RunnerArguments): + """Main bot entry point compatible with Pipecat Cloud.""" + transport = await create_transport(runner_args, transport_params) + await run_bot(transport, runner_args) + + +if __name__ == "__main__": + from pipecat.runner.run import main + + main() diff --git a/examples/foundational/55zj-update-settings-anthropic-llm.py b/examples/foundational/55zj-update-settings-anthropic-llm.py index ba729bf4c..437769643 100644 --- a/examples/foundational/55zj-update-settings-anthropic-llm.py +++ b/examples/foundational/55zj-update-settings-anthropic-llm.py @@ -62,7 +62,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = AnthropicLLMService( api_key=os.getenv("ANTHROPIC_API_KEY"), settings=AnthropicLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zk-update-settings-google-llm.py b/examples/foundational/55zk-update-settings-google-llm.py index dc3e5917a..a9fbfd093 100644 --- a/examples/foundational/55zk-update-settings-google-llm.py +++ b/examples/foundational/55zk-update-settings-google-llm.py @@ -62,7 +62,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = GoogleLLMService( api_key=os.getenv("GOOGLE_API_KEY"), settings=GoogleLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zk-update-settings-google-vertex-llm.py b/examples/foundational/55zk-update-settings-google-vertex-llm.py index 6b2babb7f..c2c8ea2bf 100644 --- a/examples/foundational/55zk-update-settings-google-vertex-llm.py +++ b/examples/foundational/55zk-update-settings-google-vertex-llm.py @@ -24,7 +24,7 @@ from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport from pipecat.services.cartesia.tts import CartesiaTTSService from pipecat.services.deepgram.stt import DeepgramSTTService -from pipecat.services.google.llm_vertex import GoogleVertexLLMService +from pipecat.services.google.vertex.llm import GoogleVertexLLMService from pipecat.transports.base_transport import BaseTransport, TransportParams from pipecat.transports.daily.transport import DailyParams from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams @@ -64,7 +64,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): project_id=os.getenv("GOOGLE_CLOUD_PROJECT_ID"), location=os.getenv("GOOGLE_CLOUD_LOCATION"), settings=GoogleVertexLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zl-update-settings-azure-realtime.py b/examples/foundational/55zl-update-settings-azure-realtime.py index fcfcdcfdd..fde633912 100644 --- a/examples/foundational/55zl-update-settings-azure-realtime.py +++ b/examples/foundational/55zl-update-settings-azure-realtime.py @@ -19,6 +19,7 @@ from pipecat.processors.aggregators.llm_context import LLMContext from pipecat.processors.aggregators.llm_response_universal import ( AssistantTurnStoppedMessage, LLMContextAggregatorPair, + LLMUserAggregatorParams, ) from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport @@ -34,17 +35,14 @@ transport_params = { "daily": lambda: DailyParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "twilio": lambda: FastAPIWebsocketParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "webrtc": lambda: TransportParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), } @@ -60,12 +58,15 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): messages = [ { "role": "system", - "content": "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + "content": "You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", }, ] context = LLMContext(messages) - user_aggregator, assistant_aggregator = LLMContextAggregatorPair(context) + user_aggregator, assistant_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) pipeline = Pipeline( [ diff --git a/examples/foundational/55zl-update-settings-openai-realtime.py b/examples/foundational/55zl-update-settings-openai-realtime.py index 372b1f1e6..83cdb9fa7 100644 --- a/examples/foundational/55zl-update-settings-openai-realtime.py +++ b/examples/foundational/55zl-update-settings-openai-realtime.py @@ -19,6 +19,7 @@ from pipecat.processors.aggregators.llm_context import LLMContext from pipecat.processors.aggregators.llm_response_universal import ( AssistantTurnStoppedMessage, LLMContextAggregatorPair, + LLMUserAggregatorParams, ) from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport @@ -34,17 +35,14 @@ transport_params = { "daily": lambda: DailyParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "twilio": lambda: FastAPIWebsocketParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "webrtc": lambda: TransportParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), } @@ -57,12 +55,15 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): messages = [ { "role": "system", - "content": "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + "content": "You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", }, ] context = LLMContext(messages) - user_aggregator, assistant_aggregator = LLMContextAggregatorPair(context) + user_aggregator, assistant_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) pipeline = Pipeline( [ diff --git a/examples/foundational/55zm-update-settings-gemini-live-vertex.py b/examples/foundational/55zm-update-settings-gemini-live-vertex.py index f69b94557..59b9a5a41 100644 --- a/examples/foundational/55zm-update-settings-gemini-live-vertex.py +++ b/examples/foundational/55zm-update-settings-gemini-live-vertex.py @@ -16,10 +16,13 @@ from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.task import PipelineParams, PipelineTask from pipecat.processors.aggregators.llm_context import LLMContext -from pipecat.processors.aggregators.llm_response_universal import LLMContextAggregatorPair +from pipecat.processors.aggregators.llm_response_universal import ( + LLMContextAggregatorPair, + LLMUserAggregatorParams, +) from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport -from pipecat.services.google.gemini_live.llm_vertex import GeminiLiveVertexLLMService +from pipecat.services.google.gemini_live.vertex.llm import GeminiLiveVertexLLMService from pipecat.transports.base_transport import BaseTransport, TransportParams from pipecat.transports.daily.transport import DailyParams from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams @@ -30,17 +33,14 @@ transport_params = { "daily": lambda: DailyParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "twilio": lambda: FastAPIWebsocketParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "webrtc": lambda: TransportParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), } @@ -53,12 +53,15 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): project_id=os.getenv("GOOGLE_CLOUD_PROJECT_ID"), location=os.getenv("GOOGLE_CLOUD_LOCATION"), settings=GeminiLiveVertexLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) context = LLMContext() - user_aggregator, assistant_aggregator = LLMContextAggregatorPair(context) + user_aggregator, assistant_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) pipeline = Pipeline( [ diff --git a/examples/foundational/55zm-update-settings-gemini-live.py b/examples/foundational/55zm-update-settings-gemini-live.py index 0b88c45da..fd5ef213a 100644 --- a/examples/foundational/55zm-update-settings-gemini-live.py +++ b/examples/foundational/55zm-update-settings-gemini-live.py @@ -16,7 +16,10 @@ from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.task import PipelineParams, PipelineTask from pipecat.processors.aggregators.llm_context import LLMContext -from pipecat.processors.aggregators.llm_response_universal import LLMContextAggregatorPair +from pipecat.processors.aggregators.llm_response_universal import ( + LLMContextAggregatorPair, + LLMUserAggregatorParams, +) from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport from pipecat.services.google.gemini_live.llm import GeminiLiveLLMService @@ -30,17 +33,14 @@ transport_params = { "daily": lambda: DailyParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "twilio": lambda: FastAPIWebsocketParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "webrtc": lambda: TransportParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), } @@ -51,12 +51,15 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = GeminiLiveLLMService( api_key=os.getenv("GOOGLE_API_KEY"), settings=GeminiLiveLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) context = LLMContext() - user_aggregator, assistant_aggregator = LLMContextAggregatorPair(context) + user_aggregator, assistant_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) pipeline = Pipeline( [ diff --git a/examples/foundational/55zn-update-settings-ultravox-realtime.py b/examples/foundational/55zn-update-settings-ultravox-realtime.py index 13ce6c1ae..f77aa4252 100644 --- a/examples/foundational/55zn-update-settings-ultravox-realtime.py +++ b/examples/foundational/55zn-update-settings-ultravox-realtime.py @@ -21,6 +21,7 @@ from pipecat.processors.aggregators.llm_context import LLMContext from pipecat.processors.aggregators.llm_response_universal import ( AssistantTurnStoppedMessage, LLMContextAggregatorPair, + LLMUserAggregatorParams, ) from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport @@ -35,17 +36,14 @@ transport_params = { "daily": lambda: DailyParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "twilio": lambda: FastAPIWebsocketParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "webrtc": lambda: TransportParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), } @@ -53,7 +51,7 @@ transport_params = { async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): logger.info(f"Starting bot") - system_prompt = "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way." + system_prompt = "You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way." llm = UltravoxRealtimeLLMService( params=OneShotInputParams( @@ -73,7 +71,10 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): ] context = LLMContext(messages) - user_aggregator, assistant_aggregator = LLMContextAggregatorPair(context) + user_aggregator, assistant_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) pipeline = Pipeline( [ diff --git a/examples/foundational/55zo-update-settings-grok-realtime.py b/examples/foundational/55zo-update-settings-grok-realtime.py index 5ac425de4..0d44470e5 100644 --- a/examples/foundational/55zo-update-settings-grok-realtime.py +++ b/examples/foundational/55zo-update-settings-grok-realtime.py @@ -19,6 +19,7 @@ from pipecat.processors.aggregators.llm_context import LLMContext from pipecat.processors.aggregators.llm_response_universal import ( AssistantTurnStoppedMessage, LLMContextAggregatorPair, + LLMUserAggregatorParams, ) from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport @@ -34,17 +35,14 @@ transport_params = { "daily": lambda: DailyParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "twilio": lambda: FastAPIWebsocketParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), "webrtc": lambda: TransportParams( audio_in_enabled=True, audio_out_enabled=True, - vad_analyzer=SileroVADAnalyzer(), ), } @@ -57,12 +55,15 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): messages = [ { "role": "system", - "content": "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + "content": "You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", }, ] context = LLMContext(messages) - user_aggregator, assistant_aggregator = LLMContextAggregatorPair(context) + user_aggregator, assistant_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) pipeline = Pipeline( [ diff --git a/examples/foundational/55zp-update-settings-aws-bedrock-llm.py b/examples/foundational/55zp-update-settings-aws-bedrock-llm.py index 2452ceada..96f151463 100644 --- a/examples/foundational/55zp-update-settings-aws-bedrock-llm.py +++ b/examples/foundational/55zp-update-settings-aws-bedrock-llm.py @@ -64,7 +64,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): settings=AWSBedrockLLMService.Settings( model="us.anthropic.claude-sonnet-4-6", temperature=0.8, - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zq-update-settings-fal-stt.py b/examples/foundational/55zq-update-settings-fal-stt.py index 77d9bfd85..8047c04e3 100644 --- a/examples/foundational/55zq-update-settings-fal-stt.py +++ b/examples/foundational/55zq-update-settings-fal-stt.py @@ -62,7 +62,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zr-update-settings-gradium-stt.py b/examples/foundational/55zr-update-settings-gradium-stt.py index e621cc89b..b906306b4 100644 --- a/examples/foundational/55zr-update-settings-gradium-stt.py +++ b/examples/foundational/55zr-update-settings-gradium-stt.py @@ -65,7 +65,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zs-update-settings-whisper-mlx-stt.py b/examples/foundational/55zs-update-settings-whisper-mlx-stt.py index a3ca78f8c..c60cd072b 100644 --- a/examples/foundational/55zs-update-settings-whisper-mlx-stt.py +++ b/examples/foundational/55zs-update-settings-whisper-mlx-stt.py @@ -67,7 +67,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zs-update-settings-whisper-stt.py b/examples/foundational/55zs-update-settings-whisper-stt.py index b58e86dbf..5af0049ea 100644 --- a/examples/foundational/55zs-update-settings-whisper-stt.py +++ b/examples/foundational/55zs-update-settings-whisper-stt.py @@ -67,7 +67,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zt-update-settings-nvidia-segmented-stt.py b/examples/foundational/55zt-update-settings-nvidia-segmented-stt.py index 62d10d455..28717e035 100644 --- a/examples/foundational/55zt-update-settings-nvidia-segmented-stt.py +++ b/examples/foundational/55zt-update-settings-nvidia-segmented-stt.py @@ -62,7 +62,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zt-update-settings-nvidia-stt.py b/examples/foundational/55zt-update-settings-nvidia-stt.py index 612f12acf..c2ad7c813 100644 --- a/examples/foundational/55zt-update-settings-nvidia-stt.py +++ b/examples/foundational/55zt-update-settings-nvidia-stt.py @@ -63,7 +63,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zu-update-settings-openai-realtime-stt.py b/examples/foundational/55zu-update-settings-openai-realtime-stt.py index 62cf902ed..5ddea6181 100644 --- a/examples/foundational/55zu-update-settings-openai-realtime-stt.py +++ b/examples/foundational/55zu-update-settings-openai-realtime-stt.py @@ -63,7 +63,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zv-update-settings-asyncai-http-tts.py b/examples/foundational/55zv-update-settings-asyncai-http-tts.py index 639a28d7f..caaba5ff2 100644 --- a/examples/foundational/55zv-update-settings-asyncai-http-tts.py +++ b/examples/foundational/55zv-update-settings-asyncai-http-tts.py @@ -68,7 +68,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zv-update-settings-asyncai-tts.py b/examples/foundational/55zv-update-settings-asyncai-tts.py index cac3fde19..6aa678df7 100644 --- a/examples/foundational/55zv-update-settings-asyncai-tts.py +++ b/examples/foundational/55zv-update-settings-asyncai-tts.py @@ -63,7 +63,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zw-update-settings-gradium-tts.py b/examples/foundational/55zw-update-settings-gradium-tts.py index d2b3535c4..71f19dc98 100644 --- a/examples/foundational/55zw-update-settings-gradium-tts.py +++ b/examples/foundational/55zw-update-settings-gradium-tts.py @@ -61,7 +61,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zx-update-settings-cerebras-llm.py b/examples/foundational/55zx-update-settings-cerebras-llm.py index 2d8e5a3d9..6e03a1d8b 100644 --- a/examples/foundational/55zx-update-settings-cerebras-llm.py +++ b/examples/foundational/55zx-update-settings-cerebras-llm.py @@ -62,7 +62,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = CerebrasLLMService( api_key=os.getenv("CEREBRAS_API_KEY"), settings=CerebrasLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zy-update-settings-deepseek-llm.py b/examples/foundational/55zy-update-settings-deepseek-llm.py index 2a439f76d..4d34cf67b 100644 --- a/examples/foundational/55zy-update-settings-deepseek-llm.py +++ b/examples/foundational/55zy-update-settings-deepseek-llm.py @@ -62,7 +62,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = DeepSeekLLMService( api_key=os.getenv("DEEPSEEK_API_KEY"), settings=DeepSeekLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zz-update-settings-fireworks-llm.py b/examples/foundational/55zz-update-settings-fireworks-llm.py index 0b184974d..85d83097e 100644 --- a/examples/foundational/55zz-update-settings-fireworks-llm.py +++ b/examples/foundational/55zz-update-settings-fireworks-llm.py @@ -63,7 +63,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): api_key=os.getenv("FIREWORKS_API_KEY"), settings=FireworksLLMService.Settings( model="accounts/fireworks/models/gpt-oss-20b", - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zza-update-settings-grok-llm.py b/examples/foundational/55zza-update-settings-grok-llm.py index c5caef791..0a793d58e 100644 --- a/examples/foundational/55zza-update-settings-grok-llm.py +++ b/examples/foundational/55zza-update-settings-grok-llm.py @@ -62,7 +62,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = GrokLLMService( api_key=os.getenv("GROK_API_KEY"), settings=GrokLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zzb-update-settings-groq-llm.py b/examples/foundational/55zzb-update-settings-groq-llm.py index 25d1b4019..896f5d6ab 100644 --- a/examples/foundational/55zzb-update-settings-groq-llm.py +++ b/examples/foundational/55zzb-update-settings-groq-llm.py @@ -63,7 +63,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): api_key=os.getenv("GROQ_API_KEY"), settings=GroqLLMService.Settings( model="meta-llama/llama-4-maverick-17b-128e-instruct", - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zzc-update-settings-mistral-llm.py b/examples/foundational/55zzc-update-settings-mistral-llm.py index 94ee3f29a..6a03b9e8c 100644 --- a/examples/foundational/55zzc-update-settings-mistral-llm.py +++ b/examples/foundational/55zzc-update-settings-mistral-llm.py @@ -62,7 +62,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = MistralLLMService( api_key=os.getenv("MISTRAL_API_KEY"), settings=MistralLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zzd-update-settings-nvidia-llm.py b/examples/foundational/55zzd-update-settings-nvidia-llm.py index eb099e50c..c34d45a2a 100644 --- a/examples/foundational/55zzd-update-settings-nvidia-llm.py +++ b/examples/foundational/55zzd-update-settings-nvidia-llm.py @@ -63,7 +63,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): api_key=os.getenv("NVIDIA_API_KEY"), settings=NvidiaLLMService.Settings( model="meta/llama-3.1-405b-instruct", - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zze-update-settings-ollama-llm.py b/examples/foundational/55zze-update-settings-ollama-llm.py index 5625e188c..666f96e6b 100644 --- a/examples/foundational/55zze-update-settings-ollama-llm.py +++ b/examples/foundational/55zze-update-settings-ollama-llm.py @@ -62,7 +62,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OLLamaLLMService( settings=OLLamaLLMService.Settings( model="llama3.2", - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) # Update to the model you're running locally diff --git a/examples/foundational/55zzf-update-settings-openrouter-llm.py b/examples/foundational/55zzf-update-settings-openrouter-llm.py index 4745ae537..2647848bc 100644 --- a/examples/foundational/55zzf-update-settings-openrouter-llm.py +++ b/examples/foundational/55zzf-update-settings-openrouter-llm.py @@ -62,7 +62,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenRouterLLMService( api_key=os.getenv("OPENROUTER_API_KEY"), settings=OpenRouterLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zzg-update-settings-perplexity-llm.py b/examples/foundational/55zzg-update-settings-perplexity-llm.py index b764e0ff7..c495830dd 100644 --- a/examples/foundational/55zzg-update-settings-perplexity-llm.py +++ b/examples/foundational/55zzg-update-settings-perplexity-llm.py @@ -64,7 +64,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): messages = [ { "role": "user", - "content": "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way. Start by introducing yourself.", + "content": "You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way. Start by introducing yourself.", }, ] diff --git a/examples/foundational/55zzh-update-settings-qwen-llm.py b/examples/foundational/55zzh-update-settings-qwen-llm.py index 2f56886b9..6cfcfc97d 100644 --- a/examples/foundational/55zzh-update-settings-qwen-llm.py +++ b/examples/foundational/55zzh-update-settings-qwen-llm.py @@ -63,7 +63,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): api_key=os.getenv("QWEN_API_KEY"), settings=QwenLLMService.Settings( model="qwen2.5-72b-instruct", - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zzi-update-settings-sambanova-llm.py b/examples/foundational/55zzi-update-settings-sambanova-llm.py index 46db2099f..46a6561ae 100644 --- a/examples/foundational/55zzi-update-settings-sambanova-llm.py +++ b/examples/foundational/55zzi-update-settings-sambanova-llm.py @@ -62,7 +62,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = SambaNovaLLMService( api_key=os.getenv("SAMBANOVA_API_KEY"), settings=SambaNovaLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zzj-update-settings-together-llm.py b/examples/foundational/55zzj-update-settings-together-llm.py index e293613b1..3ffbe9c3e 100644 --- a/examples/foundational/55zzj-update-settings-together-llm.py +++ b/examples/foundational/55zzj-update-settings-together-llm.py @@ -63,7 +63,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): api_key=os.getenv("TOGETHER_API_KEY"), settings=TogetherLLMService.Settings( model="meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo", - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zzk-update-settings-aws-nova-sonic-llm.py b/examples/foundational/55zzk-update-settings-aws-nova-sonic-llm.py index bec1c4d86..0d03efe58 100644 --- a/examples/foundational/55zzk-update-settings-aws-nova-sonic-llm.py +++ b/examples/foundational/55zzk-update-settings-aws-nova-sonic-llm.py @@ -53,7 +53,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): access_key_id=os.getenv("AWS_ACCESS_KEY_ID"), region=os.getenv("AWS_REGION"), settings=AWSNovaSonicLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zzl-update-settings-nvidia-tts.py b/examples/foundational/55zzl-update-settings-nvidia-tts.py index 5caaad3b4..3282c981d 100644 --- a/examples/foundational/55zzl-update-settings-nvidia-tts.py +++ b/examples/foundational/55zzl-update-settings-nvidia-tts.py @@ -58,7 +58,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zzm-update-settings-speechmatics-tts.py b/examples/foundational/55zzm-update-settings-speechmatics-tts.py index 80a6c694c..908ff9918 100644 --- a/examples/foundational/55zzm-update-settings-speechmatics-tts.py +++ b/examples/foundational/55zzm-update-settings-speechmatics-tts.py @@ -62,7 +62,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zzn-update-settings-groq-stt.py b/examples/foundational/55zzn-update-settings-groq-stt.py index 1b4e9b8ed..e5c903753 100644 --- a/examples/foundational/55zzn-update-settings-groq-stt.py +++ b/examples/foundational/55zzn-update-settings-groq-stt.py @@ -64,7 +64,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), settings=OpenAILLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/foundational/55zzo-update-settings-openpipe-llm.py b/examples/foundational/55zzo-update-settings-openpipe-llm.py new file mode 100644 index 000000000..89a94d61f --- /dev/null +++ b/examples/foundational/55zzo-update-settings-openpipe-llm.py @@ -0,0 +1,131 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import asyncio +import os +import time + +from dotenv import load_dotenv +from loguru import logger + +from pipecat.audio.vad.silero import SileroVADAnalyzer +from pipecat.frames.frames import LLMRunFrame, LLMUpdateSettingsFrame +from pipecat.pipeline.pipeline import Pipeline +from pipecat.pipeline.runner import PipelineRunner +from pipecat.pipeline.task import PipelineParams, PipelineTask +from pipecat.processors.aggregators.llm_context import LLMContext +from pipecat.processors.aggregators.llm_response_universal import ( + LLMContextAggregatorPair, + LLMUserAggregatorParams, +) +from pipecat.runner.types import RunnerArguments +from pipecat.runner.utils import create_transport +from pipecat.services.cartesia.tts import CartesiaTTSService +from pipecat.services.deepgram.stt import DeepgramSTTService +from pipecat.services.openpipe.llm import OpenPipeLLMService +from pipecat.transports.base_transport import BaseTransport, TransportParams +from pipecat.transports.daily.transport import DailyParams +from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams + +load_dotenv(override=True) + +transport_params = { + "daily": lambda: DailyParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), + "twilio": lambda: FastAPIWebsocketParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), + "webrtc": lambda: TransportParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), +} + + +async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): + logger.info(f"Starting bot") + + stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY")) + + tts = CartesiaTTSService( + api_key=os.getenv("CARTESIA_API_KEY"), + settings=CartesiaTTSService.Settings( + voice="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady + ), + ) + + timestamp = int(time.time()) + llm = OpenPipeLLMService( + api_key=os.getenv("OPENAI_API_KEY"), + openpipe_api_key=os.getenv("OPENPIPE_API_KEY"), + tags={"conversation_id": f"pipecat-{timestamp}"}, + settings=OpenPipeLLMService.Settings( + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", + ), + ) + + context = LLMContext() + user_aggregator, assistant_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) + + pipeline = Pipeline( + [ + transport.input(), + stt, + user_aggregator, + llm, + tts, + transport.output(), + assistant_aggregator, + ] + ) + + task = PipelineTask( + pipeline, + params=PipelineParams( + enable_metrics=True, + enable_usage_metrics=True, + ), + idle_timeout_secs=runner_args.pipeline_idle_timeout_secs, + ) + + @transport.event_handler("on_client_connected") + async def on_client_connected(transport, client): + logger.info(f"Client connected") + context.add_message({"role": "user", "content": "Please introduce yourself to the user."}) + await task.queue_frames([LLMRunFrame()]) + + await asyncio.sleep(10) + logger.info("Updating OpenPipe LLM settings: temperature=0.1") + await task.queue_frame( + LLMUpdateSettingsFrame(delta=OpenPipeLLMService.Settings(temperature=0.1)) + ) + + @transport.event_handler("on_client_disconnected") + async def on_client_disconnected(transport, client): + logger.info(f"Client disconnected") + await task.cancel() + + runner = PipelineRunner(handle_sigint=runner_args.handle_sigint) + + await runner.run(task) + + +async def bot(runner_args: RunnerArguments): + """Main bot entry point compatible with Pipecat Cloud.""" + transport = await create_transport(runner_args, transport_params) + await run_bot(transport, runner_args) + + +if __name__ == "__main__": + from pipecat.runner.run import main + + main() diff --git a/examples/foundational/55zzp-update-settings-xtts-tts.py b/examples/foundational/55zzp-update-settings-xtts-tts.py new file mode 100644 index 000000000..adb90247b --- /dev/null +++ b/examples/foundational/55zzp-update-settings-xtts-tts.py @@ -0,0 +1,133 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import asyncio +import os + +import aiohttp +from dotenv import load_dotenv +from loguru import logger + +from pipecat.audio.vad.silero import SileroVADAnalyzer +from pipecat.frames.frames import LLMRunFrame, TTSUpdateSettingsFrame +from pipecat.pipeline.pipeline import Pipeline +from pipecat.pipeline.runner import PipelineRunner +from pipecat.pipeline.task import PipelineParams, PipelineTask +from pipecat.processors.aggregators.llm_context import LLMContext +from pipecat.processors.aggregators.llm_response_universal import ( + LLMContextAggregatorPair, + LLMUserAggregatorParams, +) +from pipecat.runner.types import RunnerArguments +from pipecat.runner.utils import create_transport +from pipecat.services.deepgram.stt import DeepgramSTTService +from pipecat.services.openai.llm import OpenAILLMService +from pipecat.services.xtts.tts import XTTSService +from pipecat.transports.base_transport import BaseTransport, TransportParams +from pipecat.transports.daily.transport import DailyParams +from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams + +load_dotenv(override=True) + + +transport_params = { + "daily": lambda: DailyParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), + "twilio": lambda: FastAPIWebsocketParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), + "webrtc": lambda: TransportParams( + audio_in_enabled=True, + audio_out_enabled=True, + ), +} + + +async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): + logger.info(f"Starting bot") + + async with aiohttp.ClientSession() as session: + stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY")) + + tts = XTTSService( + aiohttp_session=session, + settings=XTTSService.Settings( + voice="Claribel Dervla", + ), + base_url="http://localhost:8000", + ) + + llm = OpenAILLMService( + api_key=os.getenv("OPENAI_API_KEY"), + settings=OpenAILLMService.Settings( + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", + ), + ) + + context = LLMContext() + user_aggregator, assistant_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), + ) + + pipeline = Pipeline( + [ + transport.input(), + stt, + user_aggregator, + llm, + tts, + transport.output(), + assistant_aggregator, + ] + ) + + task = PipelineTask( + pipeline, + params=PipelineParams( + enable_metrics=True, + enable_usage_metrics=True, + ), + idle_timeout_secs=runner_args.pipeline_idle_timeout_secs, + ) + + @transport.event_handler("on_client_connected") + async def on_client_connected(transport, client): + logger.info(f"Client connected") + context.add_message( + {"role": "user", "content": "Please introduce yourself to the user."} + ) + await task.queue_frames([LLMRunFrame()]) + + await asyncio.sleep(10) + logger.info('Updating XTTS TTS settings: voice="Ana Florence"') + await task.queue_frame( + TTSUpdateSettingsFrame(delta=XTTSService.Settings(voice="Ana Florence")) + ) + + @transport.event_handler("on_client_disconnected") + async def on_client_disconnected(transport, client): + logger.info(f"Client disconnected") + await task.cancel() + + runner = PipelineRunner(handle_sigint=runner_args.handle_sigint) + + await runner.run(task) + + +async def bot(runner_args: RunnerArguments): + """Main bot entry point compatible with Pipecat Cloud.""" + transport = await create_transport(runner_args, transport_params) + await run_bot(transport, runner_args) + + +if __name__ == "__main__": + from pipecat.runner.run import main + + main() diff --git a/examples/foundational/56-custom-video-track.py b/examples/foundational/56-custom-video-track.py new file mode 100644 index 000000000..274ab6a5e --- /dev/null +++ b/examples/foundational/56-custom-video-track.py @@ -0,0 +1,210 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""Example demonstrating custom video tracks output with Daily transport. + +This example outputs two video track simultaneously: + - The default camera track with an animated color gradient pattern. + - A custom "blue" track with the same pattern but with a blue tint applied. + +The pattern generator pushes frames to the default camera. A second processor +(BlueTintProcessor) duplicates each frame, applies a blue tint, and pushes it +to the "blue" custom video destination. + +Run with: python examples/foundational/56-custom-video-track.py -t daily +""" + +import asyncio +import math +import time + +import numpy as np +from loguru import logger + +from pipecat.frames.frames import ( + CancelFrame, + EndFrame, + Frame, + OutputImageRawFrame, + StartFrame, + SystemFrame, +) +from pipecat.pipeline.pipeline import Pipeline +from pipecat.pipeline.runner import PipelineRunner +from pipecat.pipeline.task import PipelineTask +from pipecat.processors.frame_processor import FrameDirection, FrameProcessor +from pipecat.runner.types import RunnerArguments +from pipecat.runner.utils import create_transport +from pipecat.transports.base_transport import BaseTransport +from pipecat.transports.daily.transport import DailyCustomVideoTrackParams, DailyParams + +WIDTH = 320 +HEIGHT = 240 +FPS = 30 + +transport_params = { + "daily": lambda: DailyParams( + video_out_enabled=True, + video_out_width=WIDTH, + video_out_height=HEIGHT, + video_out_framerate=FPS, + video_out_destinations=["blue"], + custom_video_track_params={ + "blue": DailyCustomVideoTrackParams( + width=WIDTH, + height=HEIGHT, + send_settings={ + "maxQuality": "low", + "encodings": { + "low": { + "maxBitrate": 500_000, + "maxFramerate": FPS, + } + }, + }, + ), + }, + ), +} + + +def generate_gradient_frame(width: int, height: int, t: float) -> np.ndarray: + """Generate an animated gradient pattern. + + Creates a smooth color gradient that shifts over time using sine waves + for each RGB channel at different frequencies. + """ + x = np.linspace(0, 1, width) + y = np.linspace(0, 1, height) + xv, yv = np.meshgrid(x, y) + + r = ((np.sin(2 * math.pi * (xv + t * 0.3)) + 1) / 2 * 255).astype(np.uint8) + g = ((np.sin(2 * math.pi * (yv + t * 0.5)) + 1) / 2 * 255).astype(np.uint8) + b = ((np.sin(2 * math.pi * (xv + yv + t * 0.7)) + 1) / 2 * 255).astype(np.uint8) + + return np.stack([r, g, b], axis=-1) + + +class VideoPatternGenerator(FrameProcessor): + """Generates an animated gradient pattern and pushes it as video frames.""" + + def __init__(self, width: int, height: int, fps: int): + super().__init__() + self._width = width + self._height = height + self._fps = fps + self._generate_task = None + + async def process_frame(self, frame: Frame, direction: FrameDirection): + await super().process_frame(frame, direction) + + if isinstance(frame, StartFrame): + await self.push_frame(frame, direction) + await self._start() + elif isinstance(frame, (EndFrame, CancelFrame)): + await self._stop() + await self.push_frame(frame, direction) + else: + await self.push_frame(frame, direction) + + async def _start(self): + self._generate_task = self.create_task(self._generate_loop(), "video_generate_loop") + + async def _stop(self): + if self._generate_task: + await self.cancel_task(self._generate_task) + self._generate_task = None + + async def _generate_loop(self): + interval = 1.0 / self._fps + start = time.monotonic() + + while True: + t = time.monotonic() - start + + pattern = generate_gradient_frame(self._width, self._height, t) + + frame = OutputImageRawFrame( + image=pattern.tobytes(), + size=(self._width, self._height), + format="RGB", + ) + await self.push_frame(frame) + + elapsed = time.monotonic() - start - t + await asyncio.sleep(max(0, interval - elapsed)) + + +class BlueTintProcessor(FrameProcessor): + """Duplicates OutputImageRawFrames with a blue tint for a custom video destination.""" + + def __init__(self, destination: str): + super().__init__() + self._destination = destination + + async def process_frame(self, frame: Frame, direction: FrameDirection): + await super().process_frame(frame, direction) + + if isinstance(frame, OutputImageRawFrame): + # Pass through the original frame. + await self.push_frame(frame, direction) + + # Create a blue-tinted copy for the custom destination. + img = np.frombuffer(frame.image, dtype=np.uint8).reshape( + (frame.size[1], frame.size[0], 3) + ) + tinted = img.copy() + tinted[:, :, 0] = (tinted[:, :, 0] * 0.3).astype(np.uint8) # R + tinted[:, :, 1] = (tinted[:, :, 1] * 0.3).astype(np.uint8) # G + tinted[:, :, 2] = np.clip(tinted[:, :, 2].astype(np.uint16) + 80, 0, 255).astype( + np.uint8 + ) # B + + blue_frame = OutputImageRawFrame( + image=tinted.tobytes(), + size=frame.size, + format=frame.format, + ) + blue_frame.transport_destination = self._destination + await self.push_frame(blue_frame) + else: + await self.push_frame(frame, direction) + + +async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): + logger.info("Starting dual video track bot") + + generator = VideoPatternGenerator(WIDTH, HEIGHT, FPS) + blue_tint = BlueTintProcessor(destination="blue") + + task = PipelineTask( + Pipeline([generator, blue_tint, transport.output()]), + idle_timeout_secs=runner_args.pipeline_idle_timeout_secs, + ) + + @transport.event_handler("on_client_connected") + async def on_client_connected(transport, client): + logger.info("Client connected") + + @transport.event_handler("on_client_disconnected") + async def on_client_disconnected(transport, client): + logger.info("Client disconnected") + await task.queue_frame(EndFrame()) + + runner = PipelineRunner(handle_sigint=runner_args.handle_sigint) + await runner.run(task) + + +async def bot(runner_args: RunnerArguments): + """Main bot entry point compatible with Pipecat Cloud.""" + transport = await create_transport(runner_args, transport_params) + await run_bot(transport, runner_args) + + +if __name__ == "__main__": + from pipecat.runner.run import main + + main() diff --git a/examples/foundational/56-lemonslice-transport.py b/examples/foundational/56-lemonslice-transport.py index 67f4e8ad1..667b317f8 100644 --- a/examples/foundational/56-lemonslice-transport.py +++ b/examples/foundational/56-lemonslice-transport.py @@ -58,7 +58,7 @@ async def main(): llm = GroqLLMService( api_key=os.getenv("GROQ_API_KEY"), settings=GroqLLMService.Settings( - system_instruction="You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points. Respond to what the user said in a creative and helpful way.", + system_instruction="You are a helpful assistant in a voice conversation. Your responses will be spoken aloud, so avoid emojis, bullet points, or other formatting that can't be spoken. Respond to what the user said in a creative, helpful, and brief way.", ), ) diff --git a/examples/quickstart/README.md b/examples/quickstart/README.md index 91a3fd888..6374d622c 100644 --- a/examples/quickstart/README.md +++ b/examples/quickstart/README.md @@ -81,23 +81,12 @@ Transform your local bot into a production-ready service. Pipecat Cloud handles > 💡 Tip: You can run the `pipecat` CLI using the `pc` alias. -3. Set up Docker for building your bot image: - - - **Install [Docker](https://www.docker.com/)** on your system - - **Create a [Docker Hub](https://hub.docker.com/) account** - - **Login to Docker Hub:** - - ```bash - docker login - ``` - ### Configure your deployment -The `pcc-deploy.toml` file tells Pipecat Cloud how to run your bot. **Update the image field** with your Docker Hub username by editing `pcc-deploy.toml`. +The `pcc-deploy.toml` file tells Pipecat Cloud how to run your bot. ```ini agent_name = "quickstart" -image = "YOUR_DOCKERHUB_USERNAME/quickstart:0.1" # 👈 Update this line secret_set = "quickstart-secrets" [scaling] @@ -107,12 +96,9 @@ secret_set = "quickstart-secrets" **Understanding the TOML file settings:** - `agent_name`: Your bot's name in Pipecat Cloud -- `image`: The Docker image to deploy (format: `username/image:version`) - `secret_set`: Where your API keys are stored securely - `min_agents`: Number of bot instances to keep ready (1 = instant start) -> 💡 Tip: [Set up `image_credentials`](https://docs.pipecat.ai/deployment/pipecat-cloud/fundamentals/secrets#image-pull-secrets) in your TOML file for authenticated image pulls - ### Log in to Pipecat Cloud To start using the CLI, authenticate to Pipecat Cloud: @@ -121,7 +107,7 @@ To start using the CLI, authenticate to Pipecat Cloud: pipecat cloud auth login ``` -You'll be presented with a link that you can click to authenticate your client. +You'll be presented with a link and six-digit code that you can click to authenticate your client. ### Configure secrets @@ -133,13 +119,7 @@ pipecat cloud secrets set quickstart-secrets --file .env This creates a secret set called `quickstart-secrets` (matching your TOML file) and uploads all your API keys from `.env`. -### Build and deploy - -Build your Docker image and push to Docker Hub: - -```bash -pipecat cloud docker build-push -``` +### Deploy Deploy to Pipecat Cloud: @@ -147,6 +127,8 @@ Deploy to Pipecat Cloud: pipecat cloud deploy ``` +This pushes your project files to Pipecat Cloud where a docker image is built and deployed into production. + ### Connect to your agent 1. Open your [Pipecat Cloud dashboard](https://pipecat.daily.co/) diff --git a/examples/quickstart/bot.py b/examples/quickstart/bot.py index fbe27d783..c201edeb3 100644 --- a/examples/quickstart/bot.py +++ b/examples/quickstart/bot.py @@ -63,19 +63,19 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): tts = CartesiaTTSService( api_key=os.getenv("CARTESIA_API_KEY"), - voice_id="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady + settings=CartesiaTTSService.Settings( + voice="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady + ), ) - llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY")) + llm = OpenAILLMService( + api_key=os.getenv("OPENAI_API_KEY"), + settings=OpenAILLMService.Settings( + system_instruction="You are a friendly AI assistant. Respond naturally and keep your answers conversational.", + ), + ) - messages = [ - { - "role": "system", - "content": "You are a friendly AI assistant. Respond naturally and keep your answers conversational.", - }, - ] - - context = LLMContext(messages) + context = LLMContext() user_aggregator, assistant_aggregator = LLMContextAggregatorPair( context, user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), @@ -105,7 +105,9 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def on_client_connected(transport, client): logger.info(f"Client connected") # Kick off the conversation. - messages.append({"role": "system", "content": "Say hello and briefly introduce yourself."}) + context.add_message( + {"role": "user", "content": "Say hello and briefly introduce yourself."} + ) await task.queue_frames([LLMRunFrame()]) @transport.event_handler("on_client_disconnected") diff --git a/examples/quickstart/pcc-deploy.toml b/examples/quickstart/pcc-deploy.toml index f08e8a624..5562f117e 100644 --- a/examples/quickstart/pcc-deploy.toml +++ b/examples/quickstart/pcc-deploy.toml @@ -1,11 +1,6 @@ -agent_name = "quickstart-test" -image = "your_username/quickstart-test:latest" -secret_set = "quickstart-test-secrets" +agent_name = "quickstart" +secret_set = "quickstart-secrets" agent_profile = "agent-1x" -# RECOMMENDED: Set an image pull secret: -# https://docs.pipecat.ai/deployment/pipecat-cloud/fundamentals/secrets#image-pull-secrets -image_credentials = "dockerhub-access" - [scaling] min_agents = 1 diff --git a/pyproject.toml b/pyproject.toml index d52e8af07..91afcc794 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ azure = [ "azure-cognitiveservices-speech>=1.47.0,<2"] cartesia = [ "pipecat-ai[websockets-base]" ] camb = [ "camb-sdk>=1.5.4,<2" ] cerebras = [] -daily = [ "daily-python~=0.23.0" ] +daily = [ "daily-python~=0.25.0" ] deepgram = [ "deepgram-sdk>=6.0.1,<7", "pipecat-ai[websockets-base]" ] deepseek = [] elevenlabs = [ "pipecat-ai[websockets-base]" ] @@ -83,7 +83,7 @@ kokoro = [ "kokoro-onnx>=0.5.0,<1", "requests>=2.32.5,<3" ] krisp = [ "pipecat-ai-krisp~=0.4.0" ] langchain = [ "langchain~=0.3.20", "langchain-community~=0.3.20", "langchain-openai~=0.3.9" ] lemonslice = [ "pipecat-ai[daily]" ] -livekit = [ "livekit>=1.0.13,<2", "livekit-api>=1.0.5,<2", "tenacity>=8.2.3,<10.0.0", "pyjwt>=2.10.1,<3" ] +livekit = [ "livekit>=1.0.13,<2", "livekit-api>=1.0.5,<2", "tenacity>=8.2.3,<10.0.0", "pyjwt>=2.12.0,<3" ] lmnt = [ "pipecat-ai[websockets-base]" ] local = [ "pyaudio~=0.2.14" ] local-smart-turn = [ "coremltools>=8.0", "transformers>=4.48.0,<6", "torch>=2.5.0,<3", "torchaudio>=2.5.0,<3" ] @@ -106,10 +106,10 @@ remote-smart-turn = [] resembleai = [ "pipecat-ai[websockets-base]" ] rime = [ "pipecat-ai[websockets-base]" ] riva = [ "pipecat-ai[nvidia]" ] -runner = [ "python-dotenv>=1.0.0,<2.0.0", "uvicorn>=0.32.0,<1.0.0", "fastapi>=0.115.6,<1", "pipecat-ai-small-webrtc-prebuilt>=2.3.0"] +runner = [ "python-dotenv>=1.0.0,<2.0.0", "uvicorn>=0.32.0,<1.0.0", "fastapi>=0.115.6,<1", "pipecat-ai-small-webrtc-prebuilt>=2.4.0"] sagemaker = ["aws_sdk_sagemaker_runtime_http2; python_version>='3.12'"] sambanova = [] -sarvam = [ "sarvamai==0.1.26a2", "pipecat-ai[websockets-base]" ] +sarvam = [ "sarvamai==0.1.26", "pipecat-ai[websockets-base]" ] sentry = [ "sentry-sdk>=2.28.0,<3" ] silero = [] simli = [ "simli-ai~=2.0.1"] diff --git a/scripts/evals/run-release-evals.py b/scripts/evals/run-release-evals.py index 625f33564..19492ba62 100644 --- a/scripts/evals/run-release-evals.py +++ b/scripts/evals/run-release-evals.py @@ -147,12 +147,14 @@ TESTS_07 = [ ("07zi-interruptible-piper.py", EVAL_SIMPLE_MATH), ("07zj-interruptible-kokoro.py", EVAL_SIMPLE_MATH), ("07zk-interruptible-resembleai.py", EVAL_SIMPLE_MATH), + ("07-interruptible-openai-responses.py", EVAL_SIMPLE_MATH), # Needs a local XTTS docker instance running. # ("07i-interruptible-xtts.py", EVAL_SIMPLE_MATH), ] TESTS_12 = [ ("12-describe-image-openai.py", EVAL_VISION_IMAGE(eval_speaks_first=True)), + ("12-describe-image-openai-responses.py", EVAL_VISION_IMAGE(eval_speaks_first=True)), ("12a-describe-image-anthropic.py", EVAL_VISION_IMAGE(eval_speaks_first=True)), ("12b-describe-image-aws.py", EVAL_VISION_IMAGE(eval_speaks_first=True)), ("12c-describe-image-gemini-flash.py", EVAL_VISION_IMAGE(eval_speaks_first=True)), @@ -184,12 +186,15 @@ TESTS_14 = [ ("14v-function-calling-openai.py", EVAL_WEATHER), ("14w-function-calling-mistral.py", EVAL_WEATHER), ("14x-function-calling-openpipe.py", EVAL_WEATHER), + ("14-function-calling-openai-responses.py", EVAL_WEATHER), + ("14-function-calling-openai-responses.py", EVAL_WEATHER_AND_RESTAURANT), # Video ("14d-function-calling-anthropic-video.py", EVAL_VISION_CAMERA), ("14d-function-calling-aws-video.py", EVAL_VISION_CAMERA), ("14d-function-calling-gemini-flash-video.py", EVAL_VISION_CAMERA), ("14d-function-calling-moondream-video.py", EVAL_VISION_CAMERA), ("14d-function-calling-openai-video.py", EVAL_VISION_CAMERA), + ("14d-function-calling-openai-responses-video.py", EVAL_VISION_CAMERA), # Currently not working. # ("14c-function-calling-together.py", EVAL_WEATHER), # ("14l-function-calling-deepseek.py", EVAL_WEATHER), diff --git a/src/pipecat/adapters/services/open_ai_responses_adapter.py b/src/pipecat/adapters/services/open_ai_responses_adapter.py new file mode 100644 index 000000000..70627fe5d --- /dev/null +++ b/src/pipecat/adapters/services/open_ai_responses_adapter.py @@ -0,0 +1,254 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""OpenAI Responses API adapter for Pipecat.""" + +import copy +from typing import Any, Dict, List, Optional, TypedDict + +from loguru import logger +from openai._types import NotGiven as OpenAINotGiven +from openai.types.responses import FunctionToolParam, ResponseInputItemParam + +from pipecat.adapters.base_llm_adapter import BaseLLMAdapter +from pipecat.adapters.schemas.tools_schema import ToolsSchema +from pipecat.processors.aggregators.llm_context import ( + LLMContext, + LLMContextMessage, + LLMSpecificMessage, + NotGiven, +) + + +class OpenAIResponsesLLMInvocationParams(TypedDict, total=False): + """Context-based parameters for invoking OpenAI Responses API.""" + + input: List[ResponseInputItemParam] + tools: List[FunctionToolParam] | OpenAINotGiven + instructions: str + + +class OpenAIResponsesLLMAdapter(BaseLLMAdapter[OpenAIResponsesLLMInvocationParams]): + """OpenAI Responses API adapter for Pipecat. + + Handles: + + - Converting LLMContext messages to Responses API input items + - Converting Pipecat's standardized tools schema to Responses API function tool format + - Extracting and sanitizing messages from the LLM context for logging + """ + + def __init__(self): + """Initialize the adapter.""" + super().__init__() + self._warned_system_instruction = False + + @property + def id_for_llm_specific_messages(self) -> str: + """Get the identifier used in LLMSpecificMessage instances.""" + return "openai_responses" + + def get_llm_invocation_params( + self, + context: LLMContext, + *, + system_instruction: Optional[str] = None, + ) -> OpenAIResponsesLLMInvocationParams: + """Get Responses API invocation parameters from a universal LLM context. + + Args: + context: The LLM context containing messages, tools, etc. + system_instruction: Optional system instruction from service settings. + + Returns: + Dictionary of parameters for the Responses API. + """ + messages = self.get_messages(context) + input_items = self._convert_messages_to_input(messages) + + params: OpenAIResponsesLLMInvocationParams = { + "input": input_items, + "tools": self.from_standard_tools(context.tools), + } + + if system_instruction: + # Compatibility: The Responses API requires at least one input + # message when instructions are provided. Contexts that worked with + # OpenAILLMService (system_instruction + empty messages) need the + # instructions converted to an initial developer message. + # + # NOTE: if/when we support `previous_response_id` and/or + # `conversation_id`, we'll need to revisit this logic, as it'll + # be legit to provide instructions without input items. Worth + # noting that OpenAI's docs suggest these parameters are primarily + # for development convenience rather than performance (the model + # still processes the full context), and come with the tradeoff + # of requiring OpenAI-side 30-day conversation storage, which may + # not be desirable for many users. But it could give folks an easy + # way to store/switch between conversations without needing to + # manage that storage themselves. + if not input_items: + params["input"] = [{"role": "developer", "content": system_instruction}] + else: + params["instructions"] = system_instruction + + return params + + def to_provider_tools_format(self, tools_schema: ToolsSchema) -> List[FunctionToolParam]: + """Convert function schemas to Responses API function tool format. + + Args: + tools_schema: The Pipecat tools schema to convert. + + Returns: + List of Responses API function tool definitions. + """ + functions_schema = tools_schema.standard_tools + result = [] + for func in functions_schema: + d = func.to_default_dict() + tool: FunctionToolParam = { + "type": "function", + "name": d["name"], + "parameters": d.get("parameters", {}), + "strict": d.get("strict", None), + } + if "description" in d: + tool["description"] = d["description"] + result.append(tool) + return result + + def get_messages_for_logging(self, context: LLMContext) -> List[Dict[str, Any]]: + """Get messages from context in a format ready for logging. + + Removes or truncates sensitive data like image content for safe logging. + + Args: + context: The LLM context containing messages. + + Returns: + List of messages in a format ready for logging. + """ + msgs = [] + for message in self.get_messages(context): + msg = copy.deepcopy(message) + if "content" in msg: + if isinstance(msg["content"], list): + for item in msg["content"]: + if item.get("type") == "image_url": + if item["image_url"]["url"].startswith("data:image/"): + item["image_url"]["url"] = "data:image/..." + if item.get("type") == "input_audio": + item["input_audio"]["data"] = "..." + msgs.append(msg) + return msgs + + def _convert_messages_to_input( + self, messages: List[LLMContextMessage] + ) -> List[ResponseInputItemParam]: + """Convert LLMContext messages to Responses API input items. + + Args: + messages: Messages from the LLMContext. + + Returns: + List of Responses API input items. + """ + result: List[ResponseInputItemParam] = [] + is_first = True + + for message in messages: + if isinstance(message, LLMSpecificMessage): + result.append(message.message) + is_first = False + continue + + role = message.get("role") + + if role == "system": + if is_first and not self._warned_system_instruction: + logger.warning( + "System messages in LLMContext are converted to 'developer' role for the " + "Responses API. Consider using settings.system_instruction instead, which " + "maps to the 'instructions' parameter." + ) + self._warned_system_instruction = True + content = message.get("content", "") + if isinstance(content, list): + content = self._convert_multimodal_content(content) + result.append({"role": "developer", "content": content}) + + elif role == "user": + content = message.get("content", "") + if isinstance(content, list): + content = self._convert_multimodal_content(content) + result.append({"role": "user", "content": content}) + + elif role == "assistant": + tool_calls = message.get("tool_calls") + if tool_calls: + for tc in tool_calls: + func = tc.get("function", {}) + result.append( + { + "type": "function_call", + "call_id": tc.get("id", ""), + "name": func.get("name", ""), + "arguments": func.get("arguments", ""), + } + ) + else: + content = message.get("content", "") + if isinstance(content, list): + content = self._convert_multimodal_content(content) + result.append({"role": "assistant", "content": content}) + + elif role == "tool": + content = message.get("content", "") + if not isinstance(content, str): + content = str(content) + result.append( + { + "type": "function_call_output", + "call_id": message.get("tool_call_id", ""), + "output": content, + } + ) + + is_first = False + + return result + + def _convert_multimodal_content(self, content: list) -> list: + """Convert multimodal content parts to Responses API format. + + Args: + content: List of content parts from the LLMContext message. + + Returns: + List of content parts in Responses API format. + """ + result = [] + for part in content: + part_type = part.get("type") + if part_type == "text": + result.append({"type": "input_text", "text": part.get("text", "")}) + elif part_type == "image_url": + image_url_obj = part.get("image_url", {}) + result.append( + { + "type": "input_image", + "image_url": image_url_obj.get("url", ""), + "detail": image_url_obj.get("detail", "auto"), + } + ) + else: + # Pass through other types as-is. Note: "input_audio" is not + # yet supported by the Responses API (coming soon per OpenAI + # docs) but the LLMContext format already matches the expected + # shape, so it should work once support is enabled. + result.append(part) + return result diff --git a/src/pipecat/adapters/services/perplexity_adapter.py b/src/pipecat/adapters/services/perplexity_adapter.py new file mode 100644 index 000000000..a8fbe3c18 --- /dev/null +++ b/src/pipecat/adapters/services/perplexity_adapter.py @@ -0,0 +1,152 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""Perplexity LLM adapter for Pipecat. + +Perplexity's API uses an OpenAI-compatible interface but enforces stricter +constraints on conversation history structure: + +1. **Strict role alternation** — Messages must alternate between "user"/"tool" + and "assistant" roles. Consecutive messages with the same role (e.g. two + "user" messages in a row) are rejected with: + ``"messages must be an alternating sequence of user/tool and assistant messages"`` + +2. **No non-initial system messages** — "system" messages are only allowed at + the start of the conversation. A system message after a non-system message + causes: + ``"only the initial message can have the system role"`` + +3. **Last message must be user/tool** — The final message in the conversation + must have role "user" or "tool". A trailing "assistant" message causes: + ``"the last message must have the user or tool role"`` + +This adapter transforms the message list to satisfy all three constraints before +the messages are sent to Perplexity's API. +""" + +import copy +from typing import List + +from openai.types.chat import ChatCompletionMessageParam + +from pipecat.adapters.services.open_ai_adapter import OpenAILLMAdapter, OpenAILLMInvocationParams +from pipecat.processors.aggregators.llm_context import LLMContext + + +class PerplexityLLMAdapter(OpenAILLMAdapter): + """Adapter that transforms messages to satisfy Perplexity's API constraints. + + Perplexity's API is stricter than OpenAI about message structure. This + adapter extends ``OpenAILLMAdapter`` and applies message transformations + to ensure compliance with Perplexity's constraints (role alternation, + no non-initial system messages, last message must be user/tool). + + The transformations are applied in ``get_llm_invocation_params`` after the + parent adapter extracts messages from the LLM context, and before + ``build_chat_completion_params`` prepends ``system_instruction``. + """ + + def get_llm_invocation_params(self, context: LLMContext) -> OpenAILLMInvocationParams: + """Get OpenAI-compatible invocation parameters with Perplexity message fixes applied. + + Args: + context: The LLM context containing messages, tools, etc. + + Returns: + Dictionary of parameters for Perplexity's ChatCompletion API, with + messages transformed to satisfy Perplexity's constraints. + """ + params = super().get_llm_invocation_params(context) + params["messages"] = self._transform_messages(list(params["messages"])) + return params + + def _transform_messages( + self, messages: List[ChatCompletionMessageParam] + ) -> List[ChatCompletionMessageParam]: + """Transform messages to satisfy Perplexity's API constraints. + + Applies three transformation steps in order: + + 1. **Convert non-initial system messages to user** — Any system message + after the initial system message block is converted to role "user", + since Perplexity rejects system messages after a non-system message. + + 2. **Merge consecutive same-role messages** — After the above + conversions, adjacent messages with the same role are merged using + list-of-dicts content format. This ensures strict role alternation + (e.g. a converted system→user message adjacent to an existing user + message gets merged). + + 3. **Remove trailing assistant messages** — If the last message is + "assistant", remove it. OpenAI appears to silently ignore trailing + assistant messages server-side, so removing them preserves equivalent + behavior while satisfying Perplexity's "last message must be + user/tool" constraint. + + Note: we intentionally do *not* convert a trailing system message to + "user". That would make the transformation unstable across calls — + Perplexity appears to have statefulness/caching within a conversation, + so a message that was sent as "user" in one call but becomes "system" + in the next (once more messages are appended) causes errors. If the + context consists entirely of system messages, the Perplexity API call + will fail, but that mistake will be caught right away. + + Args: + messages: List of message dicts with "role" and "content" keys. + + Returns: + Transformed list of message dicts satisfying Perplexity's constraints. + """ + if not messages: + return messages + + messages = copy.deepcopy(messages) + + # Step 1: Convert non-initial system messages to "user". + # Perplexity allows system messages at the start, but rejects them + # after any non-system message. + in_initial_system_block = True + for i in range(len(messages)): + if messages[i].get("role") == "system": + if not in_initial_system_block: + messages[i]["role"] = "user" + else: + in_initial_system_block = False + + # Step 2: Merge consecutive same-role messages. + # After system→user conversions above, we may have adjacent same-role + # messages that violate Perplexity's strict alternation requirement. + # Skip consecutive system messages at the start — Perplexity allows those. + i = 0 + while i < len(messages) - 1: + current = messages[i] + next_msg = messages[i + 1] + if current["role"] == next_msg["role"] == "system": + # Perplexity allows multiple initial system messages, don't merge + i += 1 + elif current["role"] == next_msg["role"]: + # Convert string content to list-of-dicts format for merging + if isinstance(current.get("content"), str): + current["content"] = [{"type": "text", "text": current["content"]}] + if isinstance(next_msg.get("content"), str): + next_msg["content"] = [{"type": "text", "text": next_msg["content"]}] + # Merge content from next message into current + if isinstance(current.get("content"), list) and isinstance( + next_msg.get("content"), list + ): + current["content"].extend(next_msg["content"]) + messages.pop(i + 1) + else: + i += 1 + + # Step 3: Remove trailing assistant messages. + # Perplexity requires the last message to be "user" or "tool". + # OpenAI appears to silently ignore trailing assistant messages + # server-side, so removing them preserves equivalent behavior. + while messages and messages[-1].get("role") == "assistant": + messages.pop() + + return messages diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index e33899aae..752f6f3fa 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -23,6 +23,7 @@ from typing import List, Optional, Tuple import numpy as np from aic_sdk import ( Model, + ParameterOutOfRangeError, ProcessorAsync, ProcessorConfig, ProcessorParameter, @@ -220,6 +221,7 @@ class AICFilter(BaseAudioFilter): model_id: Optional[str] = None, model_path: Optional[Path] = None, model_download_dir: Optional[Path] = None, + enhancement_level: Optional[float] = None, ) -> None: """Initialize the AIC filter. @@ -231,9 +233,12 @@ class AICFilter(BaseAudioFilter): model_id is ignored and no download occurs. model_download_dir: Directory for downloading models as a Path object. Defaults to a cache directory in user's home folder. + enhancement_level: Optional overall enhancement strength (0.0..1.0). + If None, the model default is used. Raises: - ValueError: If neither model_id nor model_path is provided. + ValueError: If neither model_id nor model_path is provided, or if + enhancement_level is out of range. """ # Set SDK ID for telemetry identification (6 = pipecat) set_sdk_id(6) @@ -244,14 +249,18 @@ class AICFilter(BaseAudioFilter): "See https://artifacts.ai-coustics.io/ for available models." ) + if enhancement_level is not None and not 0.0 <= enhancement_level <= 1.0: + raise ValueError("'enhancement_level' must be between 0.0 and 1.0.") + self._license_key = license_key self._model_id = model_id self._model_path = model_path self._model_download_dir = model_download_dir or ( Path.home() / ".cache" / "pipecat" / "aic-models" ) - + self._enhancement_level = enhancement_level self._bypass = False + self._sample_rate = 0 self._aic_ready = False self._frames_per_block = 0 @@ -325,6 +334,26 @@ class AICFilter(BaseAudioFilter): sensitivity=sensitivity, ) + def _apply_enhancement_level(self): + """Apply enhancement_level if configured and supported by the active model.""" + if self._processor_ctx is None or self._enhancement_level is None: + return + + try: + self._processor_ctx.set_parameter( + ProcessorParameter.EnhancementLevel, self._enhancement_level + ) + except ParameterOutOfRangeError as e: + logger.warning(f"AIC EnhancementLevel set_parameter out-of-range: {e}") + self._enhancement_level = None + + def _apply_bypass(self): + """Apply bypass parameter to the active processor.""" + if self._processor_ctx is None: + return + + self._processor_ctx.set_parameter(ProcessorParameter.Bypass, 1.0 if self._bypass else 0.0) + async def start(self, sample_rate: int): """Initialize the filter with the transport's sample rate. @@ -373,14 +402,19 @@ class AICFilter(BaseAudioFilter): self._processor_ctx = self._processor.get_processor_context() self._vad_ctx = self._processor.get_vad_context() - # Apply initial parameters - self._processor_ctx.set_parameter(ProcessorParameter.Bypass, 1.0 if self._bypass else 0.0) + # Apply initial control parameters + self._apply_bypass() + self._apply_enhancement_level() # 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}") + if self._enhancement_level is not None: + logger.debug(f" Enhancement level: {self._enhancement_level}") + else: + logger.debug(" Enhancement level not configured; using the model's default behavior.") 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: " @@ -425,9 +459,8 @@ class AICFilter(BaseAudioFilter): self._bypass = not frame.enable if self._processor_ctx is not None: try: - self._processor_ctx.set_parameter( - ProcessorParameter.Bypass, 1.0 if self._bypass else 0.0 - ) + self._apply_bypass() + self._apply_enhancement_level() except Exception as e: # noqa: BLE001 logger.error(f"AIC set_parameter failed: {e}") diff --git a/src/pipecat/audio/turn/smart_turn/local_coreml_smart_turn.py b/src/pipecat/audio/turn/smart_turn/local_coreml_smart_turn.py index be4744c27..18310c386 100644 --- a/src/pipecat/audio/turn/smart_turn/local_coreml_smart_turn.py +++ b/src/pipecat/audio/turn/smart_turn/local_coreml_smart_turn.py @@ -10,6 +10,7 @@ This module provides a smart turn analyzer that uses CoreML models for local end-of-turn detection without requiring network connectivity. """ +import warnings from typing import Any, Dict import numpy as np @@ -35,6 +36,10 @@ class LocalCoreMLSmartTurnAnalyzer(BaseSmartTurn): Provides end-of-turn detection using locally-stored CoreML models, enabling offline operation without network dependencies. Optimized for Apple Silicon and other CoreML-compatible hardware. + + .. deprecated:: 0.0.106 + LocalCoreMLSmartTurnAnalyzer is deprecated and will be removed in a future version. + Use LocalSmartTurnAnalyzerV3 instead. """ def __init__(self, *, smart_turn_model_path: str, **kwargs): @@ -50,6 +55,15 @@ class LocalCoreMLSmartTurnAnalyzer(BaseSmartTurn): """ super().__init__(**kwargs) + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "LocalCoreMLSmartTurnAnalyzer is deprecated and will be removed in a future " + "version. Use LocalSmartTurnAnalyzerV3 instead.", + DeprecationWarning, + stacklevel=2, + ) + if not smart_turn_model_path: logger.error("smart_turn_model_path is not set.") raise Exception("smart_turn_model_path must be provided.") diff --git a/src/pipecat/audio/turn/smart_turn/local_smart_turn.py b/src/pipecat/audio/turn/smart_turn/local_smart_turn.py index e98c345a1..791b63af1 100644 --- a/src/pipecat/audio/turn/smart_turn/local_smart_turn.py +++ b/src/pipecat/audio/turn/smart_turn/local_smart_turn.py @@ -36,7 +36,7 @@ class LocalSmartTurnAnalyzer(BaseSmartTurn): enabling offline operation without network dependencies. Uses Wav2Vec2-BERT architecture for audio sequence classification. - .. deprecated:: 0.98.0 + .. deprecated:: 0.0.98 LocalSmartTurnAnalyzer is deprecated and will be removed in a future version. Use LocalSmartTurnAnalyzerV3 instead. """ diff --git a/src/pipecat/audio/turn/smart_turn/local_smart_turn_v2.py b/src/pipecat/audio/turn/smart_turn/local_smart_turn_v2.py index 0b2f21cba..8d584ecd2 100644 --- a/src/pipecat/audio/turn/smart_turn/local_smart_turn_v2.py +++ b/src/pipecat/audio/turn/smart_turn/local_smart_turn_v2.py @@ -10,6 +10,7 @@ This module provides a smart turn analyzer that uses PyTorch models for local end-of-turn detection without requiring network connectivity. """ +import warnings from typing import Any, Dict import numpy as np @@ -41,6 +42,10 @@ class LocalSmartTurnAnalyzerV2(BaseSmartTurn): Provides end-of-turn detection using locally-stored PyTorch models, enabling offline operation without network dependencies. Uses Wav2Vec2 architecture for audio sequence classification. + + .. deprecated:: 0.0.106 + LocalSmartTurnAnalyzerV2 is deprecated and will be removed in a future version. + Use LocalSmartTurnAnalyzerV3 instead. """ def __init__(self, *, smart_turn_model_path: str, **kwargs): @@ -53,6 +58,15 @@ class LocalSmartTurnAnalyzerV2(BaseSmartTurn): """ super().__init__(**kwargs) + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "LocalSmartTurnAnalyzerV2 is deprecated and will be removed in a future version. " + "Use LocalSmartTurnAnalyzerV3 instead.", + DeprecationWarning, + stacklevel=2, + ) + if not smart_turn_model_path: # Define the path to the pretrained model on Hugging Face smart_turn_model_path = "pipecat-ai/smart-turn-v2" diff --git a/src/pipecat/frames/frames.py b/src/pipecat/frames/frames.py index 390eb93dd..7107cfd97 100644 --- a/src/pipecat/frames/frames.py +++ b/src/pipecat/frames/frames.py @@ -274,8 +274,16 @@ class OutputImageRawFrame(DataFrame, ImageRawFrame): An image that will be shown by the transport. If the transport supports multiple video destinations (e.g. multiple video tracks) the destination name can be specified in transport_destination. + + Parameters: + sync_with_audio: If True, the image is queued with audio frames so + it is only displayed after all preceding audio has been sent. + Defaults to False (image is displayed immediately when the output + transport receives it). """ + sync_with_audio: bool = field(default=False, init=False) + def __str__(self): pts = format_pts(self.pts) return f"{self.name}(pts: {pts}, destination: {self.transport_destination}, size: {self.size}, format: {self.format})" @@ -1001,7 +1009,8 @@ class OutputDTMFFrame(DTMFFrame, DataFrame): specify where the DTMF keypress should be sent. """ - pass + def __str__(self): + return f"{self.name}(tone: {self.button})" # @@ -1658,7 +1667,8 @@ class AssistantImageRawFrame(OutputImageRawFrame): class InputDTMFFrame(DTMFFrame, SystemFrame): """DTMF keypress input frame from transport.""" - pass + def __str__(self): + return f"{self.name}(tone: {self.button.value})" @dataclass @@ -1742,7 +1752,7 @@ class ServiceSwitcherRequestMetadataFrame(ControlFrame): @dataclass -class TaskFrame(SystemFrame): +class TaskFrame(ControlFrame): """Base frame for task frames. This is a base class for frames that are meant to be sent and handled @@ -1756,7 +1766,21 @@ class TaskFrame(SystemFrame): @dataclass -class EndTaskFrame(TaskFrame): +class TaskSystemFrame(SystemFrame): + """Base frame for task system frames. + + This is a base class for frames that are meant to be sent and handled + upstream by the pipeline task. This might result in a corresponding frame + sent downstream (e.g. `InterruptionTaskFrame` / `InterruptionFrame` or + `EndTaskFrame` / `EndFrame`). + + """ + + pass + + +@dataclass +class EndTaskFrame(TaskFrame, UninterruptibleFrame): """Frame to request graceful pipeline task closure. This is used to notify the pipeline task that the pipeline should be @@ -1774,7 +1798,20 @@ class EndTaskFrame(TaskFrame): @dataclass -class CancelTaskFrame(TaskFrame): +class StopTaskFrame(TaskFrame, UninterruptibleFrame): + """Frame to request pipeline task stop while keeping processors running. + + This is used to notify the pipeline task that it should be stopped as + soon as possible (flushing all the queued frames) but that the pipeline + processors should be kept in a running state. This frame should be pushed + upstream. + """ + + pass + + +@dataclass +class CancelTaskFrame(TaskSystemFrame): """Frame to request immediate pipeline task cancellation. This is used to notify the pipeline task that the pipeline should be @@ -1792,20 +1829,7 @@ class CancelTaskFrame(TaskFrame): @dataclass -class StopTaskFrame(TaskFrame): - """Frame to request pipeline task stop while keeping processors running. - - This is used to notify the pipeline task that it should be stopped as - soon as possible (flushing all the queued frames) but that the pipeline - processors should be kept in a running state. This frame should be pushed - upstream. - """ - - pass - - -@dataclass -class InterruptionTaskFrame(TaskFrame): +class InterruptionTaskFrame(TaskSystemFrame): """Frame indicating the pipeline should be interrupted. This frame should be pushed upstream to indicate the pipeline should be @@ -2154,10 +2178,15 @@ class ServiceUpdateSettingsFrame(ControlFrame, UninterruptibleFrame): delta: :class:`~pipecat.services.settings.ServiceSettings` delta-mode object describing the fields to change. + + service: Optional target service instance. When provided, only that + service will apply the settings; other services will forward the + frame unchanged. """ settings: Mapping[str, Any] = field(default_factory=dict) delta: Optional["ServiceSettings"] = None + service: Optional["FrameProcessor"] = None @dataclass diff --git a/src/pipecat/pipeline/llm_switcher.py b/src/pipecat/pipeline/llm_switcher.py index f9f53c066..cfee6b2b0 100644 --- a/src/pipecat/pipeline/llm_switcher.py +++ b/src/pipecat/pipeline/llm_switcher.py @@ -9,7 +9,11 @@ from typing import Any, List, Optional, Type from pipecat.adapters.schemas.direct_function import DirectFunction -from pipecat.pipeline.service_switcher import ServiceSwitcher, StrategyType +from pipecat.pipeline.service_switcher import ( + ServiceSwitcher, + ServiceSwitcherStrategyManual, + StrategyType, +) from pipecat.processors.aggregators.llm_context import LLMContext from pipecat.services.llm_service import LLMService @@ -19,18 +23,20 @@ class LLMSwitcher(ServiceSwitcher[StrategyType]): Example:: - llm_switcher = LLMSwitcher( - llms=[openai_llm, anthropic_llm], - strategy_type=ServiceSwitcherStrategyManual - ) + llm_switcher = LLMSwitcher(llms=[openai_llm, anthropic_llm]) """ - def __init__(self, llms: List[LLMService], strategy_type: Type[StrategyType]): + def __init__( + self, + llms: List[LLMService], + strategy_type: Type[StrategyType] = ServiceSwitcherStrategyManual, + ): """Initialize the service switcher with a list of LLMs and a switching strategy. Args: llms: List of LLM services to switch between. strategy_type: The strategy class to use for switching between LLMs. + Defaults to ``ServiceSwitcherStrategyManual``. """ super().__init__(llms, strategy_type) @@ -52,17 +58,19 @@ class LLMSwitcher(ServiceSwitcher[StrategyType]): """ return self.strategy.active_service - async def run_inference(self, context: LLMContext) -> Optional[str]: + async def run_inference(self, context: LLMContext, **kwargs) -> Optional[str]: """Run a one-shot, out-of-band (i.e. out-of-pipeline) inference with the given LLM context, using the currently active LLM. Args: context: The LLM context containing conversation history. + **kwargs: Additional arguments forwarded to the active LLM's run_inference + (e.g. max_tokens, system_instruction). Returns: The LLM's response as a string, or None if no response is generated. """ if self.active_llm: - return await self.active_llm.run_inference(context=context) + return await self.active_llm.run_inference(context=context, **kwargs) return None def register_function( @@ -72,6 +80,7 @@ class LLMSwitcher(ServiceSwitcher[StrategyType]): start_callback=None, *, cancel_on_interruption: bool = True, + timeout_secs: Optional[float] = None, ): """Register a function handler for LLM function calls, on all LLMs, active or not. @@ -88,6 +97,7 @@ class LLMSwitcher(ServiceSwitcher[StrategyType]): cancel_on_interruption: Whether to cancel this function call when an interruption occurs. Defaults to True. + timeout_secs: Optional timeout in seconds for the function call. """ for llm in self.llms: llm.register_function( @@ -95,6 +105,7 @@ class LLMSwitcher(ServiceSwitcher[StrategyType]): handler=handler, start_callback=start_callback, cancel_on_interruption=cancel_on_interruption, + timeout_secs=timeout_secs, ) def register_direct_function( @@ -102,6 +113,7 @@ class LLMSwitcher(ServiceSwitcher[StrategyType]): handler: DirectFunction, *, cancel_on_interruption: bool = True, + timeout_secs: Optional[float] = None, ): """Register a direct function handler for LLM function calls, on all LLMs, active or not. @@ -109,9 +121,11 @@ class LLMSwitcher(ServiceSwitcher[StrategyType]): handler: The direct function to register. Must follow DirectFunction protocol. cancel_on_interruption: Whether to cancel this function call when an interruption occurs. Defaults to True. + timeout_secs: Optional timeout in seconds for the function call. """ for llm in self.llms: llm.register_direct_function( handler=handler, cancel_on_interruption=cancel_on_interruption, + timeout_secs=timeout_secs, ) diff --git a/src/pipecat/pipeline/parallel_pipeline.py b/src/pipecat/pipeline/parallel_pipeline.py index 88ea04638..1e2e03a8f 100644 --- a/src/pipecat/pipeline/parallel_pipeline.py +++ b/src/pipecat/pipeline/parallel_pipeline.py @@ -143,6 +143,19 @@ class ParallelPipeline(BasePipeline): await super().process_frame(frame, direction) # Parallel pipeline synchronized frames. + # + # - StartFrame: If a fast branch completes first, processors in + # other branches that haven't received StartFrame yet could + # receive other frames before it, causing errors. + # + # - EndFrame: If EndFrame escapes from a fast branch, downstream + # processors (e.g. output transport) begin shutting down while + # other branches still have frames to flush, causing lost output. + # + # - CancelFrame: PipelineTask waits for CancelFrame to reach the + # pipeline sink. If it escapes from a fast branch while slower + # branches are still running, the task considers cancellation + # complete prematurely. if isinstance(frame, (StartFrame, EndFrame, CancelFrame)): self._frame_counter[frame.id] = len(self._pipelines) self._synchronizing = True @@ -179,8 +192,13 @@ class ParallelPipeline(BasePipeline): # Only push the frame when all pipelines have processed it. if frame_counter == 0: self._synchronizing = False - await self._parallel_push_frame(frame, direction) - await self._flush_buffered_frames() + # StartFrame should always go before any other frame. + if isinstance(frame, StartFrame): + await self._parallel_push_frame(frame, direction) + await self._flush_buffered_frames() + else: + await self._flush_buffered_frames() + await self._parallel_push_frame(frame, direction) await self.resume_processing_system_frames() await self.resume_processing_frames() else: @@ -188,7 +206,6 @@ class ParallelPipeline(BasePipeline): async def _flush_buffered_frames(self): """Flush frames that were buffered during lifecycle frame synchronization.""" - frames = self._buffered_frames - self._buffered_frames = [] - for frame, direction in frames: + while len(self._buffered_frames) > 0: + frame, direction = self._buffered_frames.pop(0) await self.push_frame(frame, direction) diff --git a/src/pipecat/pipeline/service_switcher.py b/src/pipecat/pipeline/service_switcher.py index d18f00e7c..76b703681 100644 --- a/src/pipecat/pipeline/service_switcher.py +++ b/src/pipecat/pipeline/service_switcher.py @@ -6,10 +6,12 @@ """Service switcher for switching between different services at runtime, with different switching strategies.""" -from abc import abstractmethod from typing import Any, Generic, List, Optional, Type, TypeVar +from loguru import logger + from pipecat.frames.frames import ( + ErrorFrame, Frame, ManuallySwitchServiceFrame, ServiceMetadataFrame, @@ -69,13 +71,13 @@ class ServiceSwitcherStrategy(BaseObject): """Return the currently active service.""" return self._active_service - @abstractmethod async def handle_frame( self, frame: ServiceSwitcherFrame, direction: FrameDirection ) -> Optional[FrameProcessor]: """Handle a frame that controls service switching. - Subclasses implement this to decide whether a switch should occur. + The base implementation returns ``None`` for all frames. Subclasses + override this to implement specific switching behaviors. Args: frame: The frame to handle. @@ -84,7 +86,41 @@ class ServiceSwitcherStrategy(BaseObject): Returns: The newly active service if a switch occurred, or None otherwise. """ - pass + return None + + async def handle_error(self, error: ErrorFrame) -> Optional[FrameProcessor]: + """Handle an error from the active service. + + Called by ``ServiceSwitcher`` when a non-fatal ``ErrorFrame`` is pushed + upstream by the currently active service. Subclasses can override this + to implement automatic failover. + + Args: + error: The error frame pushed by the active service. + + Returns: + The newly active service if a switch occurred, or None otherwise. + """ + return None + + async def _set_active_if_available(self, service: FrameProcessor) -> Optional[FrameProcessor]: + """Set the active service to the given one, if it is in the list of available services. + + If it's not in the list, the request is ignored, as it may have been + intended for another ServiceSwitcher in the pipeline. + + Args: + service: The service to set as active. + + Returns: + The newly active service, or None if the service was not found. + """ + if service in self.services: + self._active_service = service + await service.queue_frame(ServiceSwitcherRequestMetadataFrame(service=service)) + await self._call_event_handler("on_service_switched", service) + return service + return None class ServiceSwitcherStrategyManual(ServiceSwitcherStrategy): @@ -118,23 +154,54 @@ class ServiceSwitcherStrategyManual(ServiceSwitcherStrategy): return None - async def _set_active_if_available(self, service: FrameProcessor) -> Optional[FrameProcessor]: - """Set the active service to the given one, if it is in the list of available services. - If it's not in the list, the request is ignored, as it may have been - intended for another ServiceSwitcher in the pipeline. +class ServiceSwitcherStrategyFailover(ServiceSwitcherStrategyManual): + """A strategy that automatically switches to a backup service on failure. + + When the active service produces a non-fatal error, this strategy switches + to the next available service in the list. Recovery and fallback policies + are left to application code via the ``on_service_switched`` event. + + Event handlers available: + + - on_service_switched: Called when the active service changes. + + Example:: + + switcher = ServiceSwitcher( + services=[primary_stt, backup_stt], + strategy_type=ServiceSwitcherStrategyFailover, + ) + + @switcher.strategy.event_handler("on_service_switched") + async def on_switched(strategy, service): + # App decides when/how to recover the failed service + ... + """ + + async def handle_error(self, error: ErrorFrame) -> Optional[FrameProcessor]: + """Handle an error from the active service by failing over. + + Switches to the next service in the list. The failed service remains + in the list and can be switched back to manually or via application + logic in the ``on_service_switched`` event handler. Args: - service: The service to set as active. + error: The error frame pushed by the active service. Returns: - The newly active service, or None if the service was not found. + The newly active service if a switch occurred, or None if no + other service is available. """ - if service in self.services: - self._active_service = service - await self._call_event_handler("on_service_switched", service) - return service - return None + logger.warning(f"Service {self._active_service.name} reported an error: {error.error}") + + if len(self._services) <= 1: + logger.error("No other service available to switch to") + return None + + current_idx = self._services.index(self._active_service) + next_idx = (current_idx + 1) % len(self._services) + return await self._set_active_if_available(self._services[next_idx]) StrategyType = TypeVar("StrategyType", bound=ServiceSwitcherStrategy) @@ -150,18 +217,20 @@ class ServiceSwitcher(ParallelPipeline, Generic[StrategyType]): Example:: - switcher = ServiceSwitcher( - services=[stt_1, stt_2], - strategy_type=ServiceSwitcherStrategyManual, - ) + switcher = ServiceSwitcher(services=[stt_1, stt_2]) """ - def __init__(self, services: List[FrameProcessor], strategy_type: Type[StrategyType]): + def __init__( + self, + services: List[FrameProcessor], + strategy_type: Type[StrategyType] = ServiceSwitcherStrategyManual, + ): """Initialize the service switcher with a list of services and a switching strategy. Args: services: List of frame processors to switch between. strategy_type: The strategy class to use for switching between services. + Defaults to ``ServiceSwitcherStrategyManual``. """ _strategy = strategy_type(services) super().__init__(*self._make_pipeline_definitions(services, _strategy)) @@ -227,6 +296,10 @@ class ServiceSwitcher(ParallelPipeline, Generic[StrategyType]): all the filters let it pass, and `StartFrame` causes the service to generate `ServiceMetadataFrame`. + Non-fatal ``ErrorFrame`` instances are forwarded to the strategy via + ``handle_error`` so strategies like ``ServiceSwitcherStrategyFailover`` + can perform failover. The error frame is still propagated upstream so + that application-level error handlers can observe it. """ # Consume ServiceSwitcherRequestMetadataFrame once the targeted service # has handled it (i.e. the active service). @@ -239,6 +312,10 @@ class ServiceSwitcher(ParallelPipeline, Generic[StrategyType]): if frame.service_name != self.strategy.active_service.name: return + # Let the strategy react to non-fatal errors from the active service. + if isinstance(frame, ErrorFrame) and not frame.fatal: + await self.strategy.handle_error(frame) + await super().push_frame(frame, direction) async def process_frame(self, frame: Frame, direction: FrameDirection): @@ -255,9 +332,5 @@ class ServiceSwitcher(ParallelPipeline, Generic[StrategyType]): # frame. If we switched, we just swallow the frame. if not service: await super().process_frame(frame, direction) - - # If we switched to a new service, request its metadata. - if service: - await service.queue_frame(ServiceSwitcherRequestMetadataFrame(service=service)) else: await super().process_frame(frame, direction) diff --git a/src/pipecat/pipeline/sync_parallel_pipeline.py b/src/pipecat/pipeline/sync_parallel_pipeline.py index cb3f1bbe0..148d29b25 100644 --- a/src/pipecat/pipeline/sync_parallel_pipeline.py +++ b/src/pipecat/pipeline/sync_parallel_pipeline.py @@ -4,15 +4,21 @@ # SPDX-License-Identifier: BSD 2-Clause License # -"""Synchronous parallel pipeline implementation for concurrent frame processing. +"""Synchronized parallel pipeline that holds output until all branches finish. -This module provides a pipeline that processes frames through multiple parallel -pipelines simultaneously, synchronizing their output to maintain frame ordering -and prevent duplicate processing. +A SyncParallelPipeline fans each inbound frame out to multiple parallel pipelines +and waits for every pipeline to finish processing before releasing any of the +resulting output frames. This ensures that all frames produced in response to a +single input frame are emitted together. + +System frames (except EndFrame) are exempt from this synchronization — they pass +straight through without waiting, since they are expected to race ahead of +regular data frames. """ import asyncio from dataclasses import dataclass +from enum import Enum from itertools import chain from typing import List @@ -24,22 +30,42 @@ from pipecat.pipeline.pipeline import Pipeline from pipecat.processors.frame_processor import FrameDirection, FrameProcessor, FrameProcessorSetup +class FrameOrder(Enum): + """Controls the order in which synchronized frames are pushed downstream. + + When multiple parallel pipelines produce output for the same input frame, + this setting determines the order in which those output frames are pushed. + + Attributes: + ARRIVAL: Frames are pushed in the order they arrive from any pipeline. + This is the default and matches the behavior of prior versions. + PIPELINE: Frames are pushed in pipeline definition order — all frames + from the first pipeline are pushed, then all frames from the second + pipeline, and so on. Useful when the relative ordering between + pipelines matters (e.g. ensuring image frames precede audio frames). + """ + + ARRIVAL = "arrival" + PIPELINE = "pipeline" + + @dataclass class SyncFrame(ControlFrame): - """Control frame used to synchronize parallel pipeline processing. + """Sentinel frame used to detect when a parallel pipeline has finished processing. - This frame is sent through parallel pipelines to determine when the - internal pipelines have finished processing a batch of frames. + After sending a real frame into a parallel pipeline, a SyncFrame is sent + behind it. When the SyncFrame emerges from the pipeline's output, we know + all output frames for the preceding input have been produced. """ pass class SyncParallelPipelineSource(FrameProcessor): - """Source processor for synchronous parallel pipeline processing. + """Bookend processor placed at the start of each parallel pipeline. - Routes frames to parallel pipelines and collects upstream responses - for synchronization purposes. + Forwards downstream frames into the pipeline and captures upstream frames + into a queue so the parent SyncParallelPipeline can release them later. """ def __init__(self, upstream_queue: asyncio.Queue): @@ -68,10 +94,11 @@ class SyncParallelPipelineSource(FrameProcessor): class SyncParallelPipelineSink(FrameProcessor): - """Sink processor for synchronous parallel pipeline processing. + """Bookend processor placed at the end of each parallel pipeline. - Collects downstream frames from parallel pipelines and routes - upstream frames back through the pipeline. + Captures downstream output frames into a queue so the parent + SyncParallelPipeline can release them later, and forwards upstream + frames back through the pipeline. """ def __init__(self, downstream_queue: asyncio.Queue): @@ -100,29 +127,44 @@ class SyncParallelPipelineSink(FrameProcessor): class SyncParallelPipeline(BasePipeline): - """Pipeline that processes frames through multiple parallel pipelines synchronously. + """Fans each input frame to parallel pipelines then holds output until every pipeline finishes. - Creates multiple parallel processing paths that all receive the same input frames - and produces synchronized output. Each parallel path is a separate pipeline that - processes frames independently, with synchronization points to ensure consistent - ordering and prevent duplicate frame processing. + For each inbound frame the pipeline: - The pipeline uses SyncFrame control frames to coordinate between parallel paths - and ensure all paths have completed processing before moving to the next frame. + 1. Sends the frame into every parallel pipeline. + 2. Sends a ``SyncFrame`` sentinel behind it in each pipeline. + 3. Waits until every pipeline has produced its ``SyncFrame``, meaning all + output for that input is ready. + 4. Releases the collected output frames (deduplicating by frame id, since + the same frame may emerge from more than one branch). + + System frames (except ``EndFrame``) bypass this mechanism entirely — they are + forwarded through each pipeline and pushed immediately, since system frames + are expected to race ahead of regular data frames. + + By default, output frames are pushed in the order they arrive from any pipeline + (``FrameOrder.ARRIVAL``). Set ``frame_order=FrameOrder.PIPELINE`` to push frames + in pipeline definition order instead — all output from the first pipeline, then + the second, and so on. """ - def __init__(self, *args): + def __init__(self, *args, frame_order: FrameOrder = FrameOrder.ARRIVAL): """Initialize the synchronous parallel pipeline. Args: - *args: Variable number of processor lists, each representing a parallel pipeline path. - Each argument should be a list of FrameProcessor instances. + *args: Variable number of processor lists, each representing a parallel + pipeline path. Each argument should be a list of FrameProcessor instances. + frame_order: Controls the order in which synchronized output frames are + pushed. ``FrameOrder.ARRIVAL`` (default) pushes frames in the order they arrive. + ``FrameOrder.PIPELINE`` pushes all frames from the first pipeline + before the second, and so on. Raises: Exception: If no arguments are provided. TypeError: If any argument is not a list of processors. """ super().__init__() + self._frame_order = frame_order if len(args) == 0: raise Exception(f"SyncParallelPipeline needs at least one argument") @@ -184,7 +226,7 @@ class SyncParallelPipeline(BasePipeline): Returns: The list of entry processors. """ - return self._sources + return [s["processor"] for s in self._sources] def processors_with_metrics(self) -> List[FrameProcessor]: """Collect processors that can generate metrics from all parallel pipelines. @@ -209,11 +251,11 @@ class SyncParallelPipeline(BasePipeline): await asyncio.gather(*[p.cleanup() for p in self._pipelines]) async def process_frame(self, frame: Frame, direction: FrameDirection): - """Process frames through all parallel pipelines with synchronization. + """Send a frame through all parallel pipelines and release output once all finish. - Distributes frames to all parallel pipelines and synchronizes their output - to maintain proper ordering and prevent duplicate processing. Uses SyncFrame - control frames to coordinate between parallel paths. + System frames (except EndFrame) skip synchronization and pass straight + through. All other frames are fanned out to every pipeline, and output is + held until every pipeline signals completion (via SyncFrame). Args: frame: The frame to process. @@ -221,60 +263,102 @@ class SyncParallelPipeline(BasePipeline): """ await super().process_frame(frame, direction) + # SystemFrames (but not EndFrame) are simply passed through all + # internal pipelines without draining queued output. This avoids + # the race condition where a SystemFrame's wait_for_sync steals + # frames from a concurrent non-SystemFrame's wait_for_sync. + if isinstance(frame, SystemFrame) and not isinstance(frame, EndFrame): + if direction == FrameDirection.UPSTREAM: + for s in self._sinks: + await s["processor"].process_frame(frame, direction) + elif direction == FrameDirection.DOWNSTREAM: + for s in self._sources: + await s["processor"].process_frame(frame, direction) + await self.push_frame(frame, direction) + return + + use_pipeline_order = self._frame_order == FrameOrder.PIPELINE + # The last processor of each pipeline needs to be synchronous otherwise - # this element won't work. Since, we know it should be synchronous we + # this element won't work. Since we know it should be synchronous we # push a SyncFrame. Since frames are ordered we know this frame will be # pushed after the synchronous processor has pushed its data allowing us - # to synchrnonize all the internal pipelines by waiting for the + # to synchronize all the internal pipelines by waiting for the # SyncFrame in all of them. + # + # In ARRIVAL mode, output frames are put onto a shared main_queue as + # they arrive. In PIPELINE mode, they are accumulated in a per-pipeline + # list and returned so the caller can drain them in definition order. async def wait_for_sync( obj, main_queue: asyncio.Queue, frame: Frame, direction: FrameDirection - ): + ) -> list[Frame]: processor = obj["processor"] queue = obj["queue"] + output_frames: list[Frame] = [] await processor.process_frame(frame, direction) - if isinstance(frame, (SystemFrame, EndFrame)): + if isinstance(frame, EndFrame): new_frame = await queue.get() - if isinstance(new_frame, (SystemFrame, EndFrame)): - await main_queue.put(new_frame) - else: - while not isinstance(new_frame, (SystemFrame, EndFrame)): + if isinstance(new_frame, EndFrame): + if use_pipeline_order: + output_frames.append(new_frame) + else: await main_queue.put(new_frame) + else: + while not isinstance(new_frame, EndFrame): + if use_pipeline_order: + output_frames.append(new_frame) + else: + await main_queue.put(new_frame) queue.task_done() new_frame = await queue.get() else: await processor.process_frame(SyncFrame(), direction) new_frame = await queue.get() while not isinstance(new_frame, SyncFrame): - await main_queue.put(new_frame) + if use_pipeline_order: + output_frames.append(new_frame) + else: + await main_queue.put(new_frame) queue.task_done() new_frame = await queue.get() + return output_frames + if direction == FrameDirection.UPSTREAM: # If we get an upstream frame we process it in each sink. - await asyncio.gather( + frames_per_pipeline = await asyncio.gather( *[wait_for_sync(s, self._up_queue, frame, direction) for s in self._sinks] ) elif direction == FrameDirection.DOWNSTREAM: # If we get a downstream frame we process it in each source. - await asyncio.gather( + frames_per_pipeline = await asyncio.gather( *[wait_for_sync(s, self._down_queue, frame, direction) for s in self._sources] ) - seen_ids = set() - while not self._up_queue.empty(): - frame = await self._up_queue.get() - if frame.id not in seen_ids: - await self.push_frame(frame, FrameDirection.UPSTREAM) - seen_ids.add(frame.id) - self._up_queue.task_done() + if use_pipeline_order: + # Push frames in pipeline definition order, deduplicating by id. + seen_ids = set() + for pipeline_frames in frames_per_pipeline: + for f in pipeline_frames: + if f.id not in seen_ids: + await self.push_frame(f, direction) + seen_ids.add(f.id) + else: + # ARRIVAL mode: drain the shared queues in the order frames arrived. + seen_ids = set() + while not self._up_queue.empty(): + frame = await self._up_queue.get() + if frame.id not in seen_ids: + await self.push_frame(frame, FrameDirection.UPSTREAM) + seen_ids.add(frame.id) + self._up_queue.task_done() - seen_ids = set() - while not self._down_queue.empty(): - frame = await self._down_queue.get() - if frame.id not in seen_ids: - await self.push_frame(frame, FrameDirection.DOWNSTREAM) - seen_ids.add(frame.id) - self._down_queue.task_done() + seen_ids = set() + while not self._down_queue.empty(): + frame = await self._down_queue.get() + if frame.id not in seen_ids: + await self.push_frame(frame, FrameDirection.DOWNSTREAM) + seen_ids.add(frame.id) + self._down_queue.task_done() diff --git a/src/pipecat/pipeline/task.py b/src/pipecat/pipeline/task.py index e795961a1..56df719d5 100644 --- a/src/pipecat/pipeline/task.py +++ b/src/pipecat/pipeline/task.py @@ -876,22 +876,22 @@ class PipelineTask(BasePipelineTask): if isinstance(frame, EndTaskFrame): # Tell the task we should end nicely. - logger.debug(f"{self}: received end task frame {frame}") + logger.debug(f"{self}: received end task frame upstream {frame}") await self.queue_frame(EndFrame(reason=frame.reason)) elif isinstance(frame, CancelTaskFrame): # Tell the task we should end right away. - logger.debug(f"{self}: received cancel task frame {frame}") + logger.debug(f"{self}: received cancel task frame upstream {frame}") await self.queue_frame(CancelFrame(reason=frame.reason)) elif isinstance(frame, StopTaskFrame): # Tell the task we should stop nicely. - logger.debug(f"{self}: received stop task frame {frame}") + logger.debug(f"{self}: received stop task frame upstream {frame}") await self.queue_frame(StopFrame()) elif isinstance(frame, InterruptionTaskFrame): # Tell the task we should interrupt the pipeline. Note that we are # bypassing the push queue and directly queue into the # pipeline. This is in case the push task is blocked waiting for a # pipeline-ending frame to finish traversing the pipeline. - logger.debug(f"{self}: received interruption task frame {frame}") + logger.debug(f"{self}: received interruption task frame upstream {frame}") await self._pipeline.queue_frame(InterruptionFrame()) elif isinstance(frame, ErrorFrame): await self._call_event_handler("on_pipeline_error", frame) @@ -934,6 +934,18 @@ class PipelineTask(BasePipelineTask): self._pipeline_end_event.set() elif isinstance(frame, HeartbeatFrame): await self._heartbeat_queue.put(frame) + elif isinstance(frame, EndTaskFrame): + logger.debug(f"{self}: received end task frame downstream {frame}") + await self.queue_frame(EndTaskFrame(reason=frame.reason), FrameDirection.UPSTREAM) + elif isinstance(frame, StopTaskFrame): + logger.debug(f"{self}: received stop task frame downstream {frame}") + await self.queue_frame(StopTaskFrame(), FrameDirection.UPSTREAM) + elif isinstance(frame, CancelTaskFrame): + logger.debug(f"{self}: received cancel task frame downstream {frame}") + await self.queue_frame(CancelTaskFrame(reason=frame.reason), FrameDirection.UPSTREAM) + elif isinstance(frame, InterruptionTaskFrame): + logger.debug(f"{self}: received interruption task frame downstream {frame}") + await self.queue_frame(InterruptionTaskFrame(), FrameDirection.UPSTREAM) async def _heartbeat_push_handler(self): """Push heartbeat frames at regular intervals.""" diff --git a/src/pipecat/processors/aggregators/llm_response_universal.py b/src/pipecat/processors/aggregators/llm_response_universal.py index e1e356007..605db31f6 100644 --- a/src/pipecat/processors/aggregators/llm_response_universal.py +++ b/src/pipecat/processors/aggregators/llm_response_universal.py @@ -75,6 +75,7 @@ from pipecat.processors.aggregators.llm_context_summarizer import ( SummaryAppliedEvent, ) from pipecat.processors.frame_processor import FrameCallback, FrameDirection, FrameProcessor +from pipecat.services.settings import LLMSettings from pipecat.turns.user_idle_controller import UserIdleController from pipecat.turns.user_mute import BaseUserMuteStrategy from pipecat.turns.user_start import BaseUserTurnStartStrategy, UserTurnStartedParams @@ -446,6 +447,9 @@ class LLMUserAggregator(LLMContextAggregator): self._user_turn_controller.add_event_handler( "on_user_turn_stop_timeout", self._on_user_turn_stop_timeout ) + self._user_turn_controller.add_event_handler( + "on_reset_aggregation", self._on_reset_aggregation + ) self._user_idle_controller = UserIdleController( user_idle_timeout=self._params.user_idle_timeout @@ -561,10 +565,10 @@ class LLMUserAggregator(LLMContextAggregator): # Enable the feature on the LLM with config await self.push_frame( LLMUpdateSettingsFrame( - settings={ - "filter_incomplete_user_turns": True, - "user_turn_completion_config": config, - } + delta=LLMSettings( + filter_incomplete_user_turns=True, + user_turn_completion_config=config, + ) ) ) @@ -747,6 +751,12 @@ class LLMUserAggregator(LLMContextAggregator): await self._maybe_emit_user_turn_stopped(strategy) + async def _on_reset_aggregation( + self, controller: UserTurnController, strategy: BaseUserTurnStartStrategy + ): + logger.debug(f"{self}: Resetting aggregation (strategy: {strategy})") + await self.reset() + async def _on_user_turn_stop_timeout(self, controller): await self._call_event_handler("on_user_turn_stop_timeout") diff --git a/src/pipecat/processors/filters/wake_check_filter.py b/src/pipecat/processors/filters/wake_check_filter.py index ec8f31f53..6a9e524e6 100644 --- a/src/pipecat/processors/filters/wake_check_filter.py +++ b/src/pipecat/processors/filters/wake_check_filter.py @@ -6,6 +6,9 @@ """Wake phrase detection filter for Pipecat transcription processing. +.. deprecated:: 0.0.106 + Use :class:`~pipecat.turns.user_start.WakePhraseUserTurnStartStrategy` instead. + This module provides a frame processor that filters transcription frames, only allowing them through after wake phrases have been detected. Includes keepalive functionality to maintain conversation flow after wake detection. @@ -13,6 +16,7 @@ keepalive functionality to maintain conversation flow after wake detection. import re import time +import warnings from enum import Enum from typing import List @@ -25,6 +29,11 @@ from pipecat.processors.frame_processor import FrameDirection, FrameProcessor class WakeCheckFilter(FrameProcessor): """Frame processor that filters transcription frames based on wake phrase detection. + .. deprecated:: 0.0.106 + Use :class:`~pipecat.turns.user_start.WakePhraseUserTurnStartStrategy` instead, + which integrates with the user turn strategy system and supports configurable + timeouts and single-activation mode. + This filter monitors transcription frames for configured wake phrases and only passes frames through after a wake phrase has been detected. Maintains a keepalive timeout to allow continued conversation after wake detection. @@ -65,12 +74,21 @@ class WakeCheckFilter(FrameProcessor): def __init__(self, wake_phrases: List[str], keepalive_timeout: float = 3): """Initialize the wake phrase filter. + .. deprecated:: 0.0.106 + Use :class:`~pipecat.turns.user_start.WakePhraseUserTurnStartStrategy` instead. + Args: wake_phrases: List of wake phrases to detect in transcriptions. keepalive_timeout: Duration in seconds to keep passing frames after wake detection. Defaults to 3 seconds. """ super().__init__() + warnings.warn( + "WakeCheckFilter is deprecated since v0.0.106. " + "Use WakePhraseUserTurnStartStrategy instead.", + DeprecationWarning, + stacklevel=2, + ) self._participant_states = {} self._keepalive_timeout = keepalive_timeout self._wake_patterns = [] diff --git a/src/pipecat/runner/daily.py b/src/pipecat/runner/daily.py index b2090b094..b9bcb6e3d 100644 --- a/src/pipecat/runner/daily.py +++ b/src/pipecat/runner/daily.py @@ -79,14 +79,17 @@ async def configure( aiohttp_session: aiohttp.ClientSession, *, api_key: Optional[str] = None, - room_exp_duration: Optional[float] = 2.0, - token_exp_duration: Optional[float] = 2.0, + room_exp_duration: float = 2.0, + token_exp_duration: float = 2.0, sip_caller_phone: Optional[str] = None, - sip_enable_video: Optional[bool] = False, - sip_num_endpoints: Optional[int] = 1, + sip_enable_video: bool = False, + sip_num_endpoints: int = 1, + enable_dialout: bool = False, sip_codecs: Optional[Dict[str, List[str]]] = None, + sip_provider: Optional[str] = None, + room_geo: Optional[str] = None, room_properties: Optional[DailyRoomProperties] = None, - token_properties: Optional["DailyMeetingTokenProperties"] = None, + token_properties: Optional[DailyMeetingTokenProperties] = None, ) -> DailyRoomConfig: """Configure Daily room URL and token with optional SIP capabilities. @@ -103,8 +106,14 @@ async def configure( When provided, enables SIP functionality and returns SipRoomConfig. sip_enable_video: Whether video is enabled for SIP. sip_num_endpoints: Number of allowed SIP endpoints. + enable_dialout: Whether to enable outbound dialing (PSTN or SIP) on the room. + Requires dial-out entitlement on your Daily account. sip_codecs: Codecs to support for audio and video. If None, uses Daily defaults. Example: {"audio": ["OPUS"], "video": ["H264"]} + sip_provider: SIP provider name (e.g., "daily"). Only used when + sip_caller_phone is provided and room_properties is not. + room_geo: Daily room geographic region (e.g., "us-east-1"). Only used + when room_properties is not provided. room_properties: Optional DailyRoomProperties to use instead of building from individual parameters. When provided, this overrides room_exp_duration and SIP-related parameters. If not provided, properties are built from the @@ -153,7 +162,10 @@ async def configure( sip_caller_phone is not None, sip_enable_video is not False, sip_num_endpoints != 1, + enable_dialout is not False, sip_codecs is not None, + sip_provider is not None, + room_geo is not None, ] ) if individual_params_provided: @@ -176,6 +188,8 @@ async def configure( aiohttp_session=aiohttp_session, ) + token_expiry_seconds: float = token_exp_duration * 60 * 60 + # Check for existing room URL (only in standard mode) existing_room_url = os.getenv("DAILY_ROOM_URL") if existing_room_url and not sip_enabled: @@ -184,11 +198,12 @@ async def configure( room_url = existing_room_url # Create token and return standard format - expiry_time: float = token_exp_duration * 60 * 60 token_params = None if token_properties: token_params = DailyMeetingTokenParams(properties=token_properties) - token = await daily_rest_helper.get_token(room_url, expiry_time, params=token_params) + token = await daily_rest_helper.get_token( + room_url, token_expiry_seconds, params=token_params + ) return DailyRoomConfig(room_url=room_url, token=token) # Create a new room @@ -207,6 +222,9 @@ async def configure( eject_at_room_exp=True, ) + if room_geo: + room_properties.geo = room_geo + # Add SIP configuration if enabled if sip_enabled: sip_params = DailyRoomSipParams( @@ -215,9 +233,10 @@ async def configure( sip_mode="dial-in", num_endpoints=sip_num_endpoints, codecs=sip_codecs, + provider=sip_provider, ) room_properties.sip = sip_params - room_properties.enable_dialout = True # Enable outbound calls if needed + room_properties.enable_dialout = enable_dialout room_properties.start_video_off = not sip_enable_video # Voice-only by default # Create room parameters @@ -229,7 +248,6 @@ async def configure( logger.info(f"Created Daily room: {room_url}") # Create meeting token - token_expiry_seconds = token_exp_duration * 60 * 60 token_params = None if token_properties: token_params = DailyMeetingTokenParams(properties=token_properties) diff --git a/src/pipecat/services/ai_service.py b/src/pipecat/services/ai_service.py index c4e45a417..dd9ef1dba 100644 --- a/src/pipecat/services/ai_service.py +++ b/src/pipecat/services/ai_service.py @@ -10,6 +10,7 @@ Provides the foundation for all AI services in the Pipecat framework, including model management, settings handling, and frame processing lifecycle methods. """ +import warnings from typing import Any, AsyncGenerator, Dict from loguru import logger @@ -130,6 +131,43 @@ class AIService(FrameProcessor): return changed + def _warn_init_param_moved_to_settings( + self, + param_name: str, + settings_field: str | None = None, + stacklevel: int = 3, + ): + """Warn that an ``__init__`` param has moved to ``Settings``. + + Emits a ``DeprecationWarning`` directing users to the canonical + ``settings=ServiceClass.Settings(field=...)`` API. + + Args: + param_name: Name of the deprecated ``__init__`` parameter. + settings_field: The corresponding field on the ``Settings`` + dataclass, if different from *param_name*. When ``None`` + the message omits the field hint. + stacklevel: Stack depth for the warning. Default ``3`` targets + the caller's caller (i.e. user code that instantiated the + service). + """ + label = f"{type(self).__name__}.Settings" + if settings_field: + msg = ( + f"The `{param_name}` parameter is deprecated. " + f"Use `settings={label}({settings_field}=...)` instead. " + f"If both are provided, `settings` takes precedence." + ) + else: + msg = ( + f"The `{param_name}` parameter is deprecated. " + f"Use `settings={label}(...)` instead. " + f"If both are provided, `settings` takes precedence." + ) + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn(msg, DeprecationWarning, stacklevel=stacklevel) + def _warn_unhandled_updated_settings(self, unhandled): """Log a warning for settings changes that won't take effect at runtime. diff --git a/src/pipecat/services/anthropic/llm.py b/src/pipecat/services/anthropic/llm.py index 7abcec3e0..2f375df82 100644 --- a/src/pipecat/services/anthropic/llm.py +++ b/src/pipecat/services/anthropic/llm.py @@ -58,7 +58,7 @@ from pipecat.processors.aggregators.openai_llm_context import ( from pipecat.processors.frame_processor import FrameDirection from pipecat.services.llm_service import FunctionCallFromLLM, LLMService from pipecat.services.settings import NOT_GIVEN as _NOT_GIVEN -from pipecat.services.settings import LLMSettings, _NotGiven, _warn_deprecated_param, is_given +from pipecat.services.settings import LLMSettings, _NotGiven, is_given from pipecat.utils.tracing.service_decorators import traced_llm try: @@ -98,18 +98,20 @@ class AnthropicLLMSettings(LLMSettings): """ enable_prompt_caching: bool | _NotGiven = field(default_factory=lambda: _NOT_GIVEN) - thinking: AnthropicThinkingConfig | _NotGiven = field(default_factory=lambda: _NOT_GIVEN) + thinking: Union["AnthropicLLMService.ThinkingConfig", _NotGiven] = field( + default_factory=lambda: _NOT_GIVEN + ) @classmethod def from_mapping(cls, settings): """Convert a plain dict to settings, coercing thinking dicts. For backward compatibility, a ``thinking`` value that is a plain dict - is converted to a :class:`AnthropicThinkingConfig`. + is converted to a :class:`AnthropicLLMService.ThinkingConfig`. """ instance = super().from_mapping(settings) if is_given(instance.thinking) and isinstance(instance.thinking, dict): - instance.thinking = AnthropicThinkingConfig(**instance.thinking) + instance.thinking = AnthropicLLMService.ThinkingConfig(**instance.thinking) return instance @@ -160,7 +162,7 @@ class AnthropicLLMService(LLMService): """ Settings = AnthropicLLMSettings - _settings: AnthropicLLMSettings + _settings: Settings # Overriding the default adapter to use the Anthropic one. adapter_class = AnthropicLLMAdapter @@ -172,7 +174,7 @@ class AnthropicLLMService(LLMService): """Input parameters for Anthropic model inference. .. deprecated:: 0.0.105 - Use ``AnthropicLLMSettings`` instead. Pass settings directly via the + Use ``AnthropicLLMService.Settings`` instead. Pass settings directly via the ``settings`` parameter of :class:`AnthropicLLMService`. Parameters: @@ -199,7 +201,9 @@ class AnthropicLLMService(LLMService): temperature: Optional[float] = Field(default_factory=lambda: NOT_GIVEN, ge=0.0, le=1.0) top_k: Optional[int] = Field(default_factory=lambda: NOT_GIVEN, ge=0) top_p: Optional[float] = Field(default_factory=lambda: NOT_GIVEN, ge=0.0, le=1.0) - thinking: Optional[AnthropicThinkingConfig] = Field(default_factory=lambda: NOT_GIVEN) + thinking: Optional["AnthropicLLMService.ThinkingConfig"] = Field( + default_factory=lambda: NOT_GIVEN + ) extra: Optional[Dict[str, Any]] = Field(default_factory=dict) def model_post_init(self, __context): @@ -220,7 +224,7 @@ class AnthropicLLMService(LLMService): api_key: str, model: Optional[str] = None, params: Optional[InputParams] = None, - settings: Optional[AnthropicLLMSettings] = None, + settings: Optional[Settings] = None, client=None, retry_timeout_secs: Optional[float] = 5.0, retry_on_timeout: Optional[bool] = False, @@ -233,12 +237,12 @@ class AnthropicLLMService(LLMService): model: Model name to use. .. deprecated:: 0.0.105 - Use ``settings=AnthropicLLMSettings(model=...)`` instead. + Use ``settings=AnthropicLLMService.Settings(model=...)`` instead. params: Optional model parameters for inference. .. deprecated:: 0.0.105 - Use ``settings=AnthropicLLMSettings(...)`` instead. + Use ``settings=AnthropicLLMService.Settings(...)`` instead. settings: Runtime-updatable settings for this service. When both deprecated parameters and *settings* are provided, *settings* @@ -249,7 +253,7 @@ class AnthropicLLMService(LLMService): **kwargs: Additional arguments passed to parent LLMService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = AnthropicLLMSettings( + default_settings = self.Settings( model="claude-sonnet-4-6", system_instruction=None, max_tokens=4096, @@ -268,12 +272,12 @@ class AnthropicLLMService(LLMService): # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", AnthropicLLMSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", AnthropicLLMSettings) + self._warn_init_param_moved_to_settings("params") if not settings: default_settings.max_tokens = params.max_tokens default_settings.temperature = params.temperature @@ -346,7 +350,10 @@ class AnthropicLLMService(LLMService): return response async def run_inference( - self, context: LLMContext | OpenAILLMContext, max_tokens: Optional[int] = None + self, + context: LLMContext | OpenAILLMContext, + max_tokens: Optional[int] = None, + system_instruction: Optional[str] = None, ) -> Optional[str]: """Run a one-shot, out-of-band (i.e. out-of-pipeline) inference with the given LLM context. @@ -354,6 +361,8 @@ class AnthropicLLMService(LLMService): context: The LLM context containing conversation history. max_tokens: Optional maximum number of tokens to generate. If provided, overrides the service's default max_tokens setting. + system_instruction: Optional system instruction to use for this inference. + If provided, overrides any system instruction in the context. Returns: The LLM's response as a string, or None if no response is generated. @@ -375,6 +384,15 @@ class AnthropicLLMService(LLMService): system = getattr(context, "system", NOT_GIVEN) tools = context.tools or [] + # Override system instruction if provided + if system_instruction is not None: + if system and system is not NOT_GIVEN: + logger.warning( + f"{self}: Both system_instruction and a system message in context are set." + " Using system_instruction." + ) + system = system_instruction + # Build params using the same method as streaming completions params = { "model": self._settings.model, @@ -1228,7 +1246,7 @@ class AnthropicAssistantContextAggregator(LLMAssistantContextAggregator): frame: Frame containing function call result. """ if frame.result: - result = json.dumps(frame.result) + result = json.dumps(frame.result, ensure_ascii=False) await self._update_function_call_result(frame.function_name, frame.tool_call_id, result) else: await self._update_function_call_result( diff --git a/src/pipecat/services/assemblyai/models.py b/src/pipecat/services/assemblyai/models.py index efc13d482..cffebcf06 100644 --- a/src/pipecat/services/assemblyai/models.py +++ b/src/pipecat/services/assemblyai/models.py @@ -125,7 +125,7 @@ class AssemblyAIConnectionParams(BaseModel): """Configuration parameters for AssemblyAI WebSocket connection. .. deprecated:: 0.0.105 - Use ``settings=AssemblyAISTTSettings(foo=...)`` instead. + Use ``settings=AssemblyAISTTService.Settings(foo=...)`` instead. Parameters: sample_rate: Audio sample rate in Hz. Defaults to 16000. diff --git a/src/pipecat/services/assemblyai/stt.py b/src/pipecat/services/assemblyai/stt.py index ddfcb4a47..f52c1d935 100644 --- a/src/pipecat/services/assemblyai/stt.py +++ b/src/pipecat/services/assemblyai/stt.py @@ -32,7 +32,7 @@ from pipecat.frames.frames import ( VADUserStoppedSpeakingFrame, ) from pipecat.processors.frame_processor import FrameDirection -from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven from pipecat.services.stt_latency import ASSEMBLYAI_TTFS_P99 from pipecat.services.stt_service import WebsocketSTTService from pipecat.transcriptions.language import Language @@ -129,7 +129,7 @@ class AssemblyAISTTService(WebsocketSTTService): """ Settings = AssemblyAISTTSettings - _settings: AssemblyAISTTSettings + _settings: Settings def __init__( self, @@ -143,7 +143,7 @@ class AssemblyAISTTService(WebsocketSTTService): vad_force_turn_endpoint: bool = True, should_interrupt: bool = True, speaker_format: Optional[str] = None, - settings: Optional[AssemblyAISTTSettings] = None, + settings: Optional[Settings] = None, ttfs_p99_latency: Optional[float] = ASSEMBLYAI_TTFS_P99, **kwargs, ): @@ -154,7 +154,7 @@ class AssemblyAISTTService(WebsocketSTTService): language: Language code for transcription. Defaults to English (Language.EN). .. deprecated:: 0.0.105 - Use ``settings=AssemblyAISTTSettings(language=...)`` instead. + Use ``settings=AssemblyAISTTService.Settings(language=...)`` instead. api_endpoint_base_url: WebSocket endpoint URL. Defaults to AssemblyAI's streaming endpoint. sample_rate: Audio sample rate in Hz. Defaults to 16000. @@ -162,7 +162,7 @@ class AssemblyAISTTService(WebsocketSTTService): connection_params: Connection configuration parameters. .. deprecated:: 0.0.105 - Use ``settings=AssemblyAISTTSettings(...)`` instead. + Use ``settings=AssemblyAISTTService.Settings(...)`` instead. vad_force_turn_endpoint: Controls turn detection mode. When True (Pipecat mode, default): Forces AssemblyAI to return finals ASAP @@ -190,7 +190,7 @@ class AssemblyAISTTService(WebsocketSTTService): **kwargs: Additional arguments passed to parent STTService class. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = AssemblyAISTTSettings( + default_settings = self.Settings( model="u3-rt-pro", language=Language.EN, formatted_finals=True, @@ -208,12 +208,12 @@ class AssemblyAISTTService(WebsocketSTTService): # 2. Apply direct init arg overrides (deprecated) if language is not None: - _warn_deprecated_param("language", AssemblyAISTTSettings, "language") + self._warn_init_param_moved_to_settings("language", "language") default_settings.language = language # 3. Apply connection_params overrides (deprecated) — only if settings not provided if connection_params is not None: - _warn_deprecated_param("connection_params", AssemblyAISTTSettings) + self._warn_init_param_moved_to_settings("connection_params") if not settings: sample_rate = connection_params.sample_rate encoding = connection_params.encoding @@ -299,7 +299,7 @@ class AssemblyAISTTService(WebsocketSTTService): self._user_speaking = False - def _configure_pipecat_turn_mode(self, settings: AssemblyAISTTSettings, is_u3_pro: bool): + def _configure_pipecat_turn_mode(self, settings: Settings, is_u3_pro: bool): """Configure settings for Pipecat turn detection mode. When vad_force_turn_endpoint is enabled, force AssemblyAI to return @@ -353,7 +353,7 @@ class AssemblyAISTTService(WebsocketSTTService): """ return True - async def _update_settings(self, delta: AssemblyAISTTSettings) -> dict[str, Any]: + async def _update_settings(self, delta: Settings) -> dict[str, Any]: """Apply a settings delta and reconnect to apply changes. Args: diff --git a/src/pipecat/services/asyncai/tts.py b/src/pipecat/services/asyncai/tts.py index 67f79dbfd..d2ac74445 100644 --- a/src/pipecat/services/asyncai/tts.py +++ b/src/pipecat/services/asyncai/tts.py @@ -26,7 +26,7 @@ from pipecat.frames.frames import ( TTSStoppedFrame, ) from pipecat.processors.frame_processor import FrameDirection -from pipecat.services.settings import TTSSettings, _warn_deprecated_param +from pipecat.services.settings import TTSSettings from pipecat.services.tts_service import TextAggregationMode, TTSService, WebsocketTTSService from pipecat.transcriptions.language import Language, resolve_language from pipecat.utils.tracing.service_decorators import traced_tts @@ -86,13 +86,13 @@ class AsyncAITTSService(WebsocketTTSService): """ Settings = AsyncAITTSSettings - _settings: AsyncAITTSSettings + _settings: Settings class InputParams(BaseModel): """Input parameters for Async TTS configuration. .. deprecated:: 0.0.105 - Use ``AsyncAITTSSettings`` directly via the ``settings`` parameter instead. + Use ``AsyncAITTSService.Settings`` directly via the ``settings`` parameter instead. Parameters: language: Language to use for synthesis. @@ -112,7 +112,7 @@ class AsyncAITTSService(WebsocketTTSService): encoding: str = "pcm_s16le", container: str = "raw", params: Optional[InputParams] = None, - settings: Optional[AsyncAITTSSettings] = None, + settings: Optional[Settings] = None, aggregate_sentences: Optional[bool] = None, text_aggregation_mode: Optional[TextAggregationMode] = None, **kwargs, @@ -125,14 +125,14 @@ class AsyncAITTSService(WebsocketTTSService): https://docs.async.com/list-voices-16699698e0 .. deprecated:: 0.0.105 - Use ``settings=AsyncAITTSSettings(voice=...)`` instead. + Use ``settings=AsyncAITTSService.Settings(voice=...)`` instead. version: Async API version. url: WebSocket URL for Async TTS API. model: TTS model to use (e.g., "async_flash_v1.0"). .. deprecated:: 0.0.105 - Use ``settings=AsyncAITTSSettings(model=...)`` instead. + Use ``settings=AsyncAITTSService.Settings(model=...)`` instead. sample_rate: Audio sample rate. encoding: Audio encoding format. @@ -140,7 +140,7 @@ class AsyncAITTSService(WebsocketTTSService): params: Additional input parameters for voice customization. .. deprecated:: 0.0.105 - Use ``settings=AsyncAITTSSettings(...)`` instead. + Use ``settings=AsyncAITTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -153,7 +153,7 @@ class AsyncAITTSService(WebsocketTTSService): **kwargs: Additional arguments passed to the parent service. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = AsyncAITTSSettings( + default_settings = self.Settings( model="async_flash_v1.0", voice=None, language=None, @@ -161,19 +161,17 @@ class AsyncAITTSService(WebsocketTTSService): # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: - _warn_deprecated_param("voice_id", AsyncAITTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id if model is not None: - _warn_deprecated_param("model", AsyncAITTSSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", AsyncAITTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: - default_settings.language = ( - self.language_to_service_language(params.language) if params.language else None - ) + default_settings.language = params.language # 4. Apply settings delta (canonical API, always wins) if settings is not None: @@ -487,13 +485,13 @@ class AsyncAIHttpTTSService(TTSService): """ Settings = AsyncAITTSSettings - _settings: AsyncAITTSSettings + _settings: Settings class InputParams(BaseModel): """Input parameters for Async API. .. deprecated:: 0.0.105 - Use ``AsyncAITTSSettings`` directly via the ``settings`` parameter instead. + Use ``AsyncAIHttpTTSService.Settings`` directly via the ``settings`` parameter instead. Parameters: language: Language to use for synthesis. @@ -514,7 +512,7 @@ class AsyncAIHttpTTSService(TTSService): encoding: str = "pcm_s16le", container: str = "raw", params: Optional[InputParams] = None, - settings: Optional[AsyncAITTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Async TTS service. @@ -524,13 +522,13 @@ class AsyncAIHttpTTSService(TTSService): voice_id: ID of the voice to use for synthesis. .. deprecated:: 0.0.105 - Use ``settings=AsyncAITTSSettings(voice=...)`` instead. + Use ``settings=AsyncAIHttpTTSService.Settings(voice=...)`` instead. aiohttp_session: An aiohttp session for making HTTP requests. model: TTS model to use (e.g., "async_flash_v1.0"). .. deprecated:: 0.0.105 - Use ``settings=AsyncAITTSSettings(model=...)`` instead. + Use ``settings=AsyncAIHttpTTSService.Settings(model=...)`` instead. url: Base URL for Async API. version: API version string for Async API. @@ -540,14 +538,14 @@ class AsyncAIHttpTTSService(TTSService): params: Additional input parameters for voice customization. .. deprecated:: 0.0.105 - Use ``settings=AsyncAITTSSettings(...)`` instead. + Use ``settings=AsyncAIHttpTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional arguments passed to the parent TTSService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = AsyncAITTSSettings( + default_settings = self.Settings( model="async_flash_v1.0", voice=None, language=None, @@ -555,19 +553,17 @@ class AsyncAIHttpTTSService(TTSService): # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: - _warn_deprecated_param("voice_id", AsyncAITTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id if model is not None: - _warn_deprecated_param("model", AsyncAITTSSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", AsyncAITTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: - default_settings.language = ( - self.language_to_service_language(params.language) if params.language else None - ) + default_settings.language = params.language # 4. Apply settings delta (canonical API, always wins) if settings is not None: diff --git a/src/pipecat/services/aws/llm.py b/src/pipecat/services/aws/llm.py index 434011f1f..92049dffb 100644 --- a/src/pipecat/services/aws/llm.py +++ b/src/pipecat/services/aws/llm.py @@ -55,7 +55,7 @@ from pipecat.processors.aggregators.openai_llm_context import ( ) from pipecat.processors.frame_processor import FrameDirection from pipecat.services.llm_service import LLMService -from pipecat.services.settings import NOT_GIVEN, LLMSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, LLMSettings, _NotGiven from pipecat.utils.tracing.service_decorators import traced_llm try: @@ -691,7 +691,7 @@ class AWSBedrockAssistantContextAggregator(LLMAssistantContextAggregator): frame: The function call result frame to handle. """ if frame.result: - result = json.dumps(frame.result) + result = json.dumps(frame.result, ensure_ascii=False) await self._update_function_call_result(frame.function_name, frame.tool_call_id, result) else: await self._update_function_call_result( @@ -747,7 +747,7 @@ class AWSBedrockLLMService(LLMService): """ Settings = AWSBedrockLLMSettings - _settings: AWSBedrockLLMSettings + _settings: Settings # Overriding the default adapter to use the Anthropic one. adapter_class = AWSBedrockLLMAdapter @@ -756,7 +756,7 @@ class AWSBedrockLLMService(LLMService): """Input parameters for AWS Bedrock LLM service. .. deprecated:: 0.0.105 - Use ``AWSBedrockLLMSettings`` instead. Pass settings directly via the + Use ``AWSBedrockLLMService.Settings`` instead. Pass settings directly via the ``settings`` parameter of :class:`AWSBedrockLLMService`. Parameters: @@ -784,7 +784,7 @@ class AWSBedrockLLMService(LLMService): aws_session_token: Optional[str] = None, aws_region: Optional[str] = None, params: Optional[InputParams] = None, - settings: Optional[AWSBedrockLLMSettings] = None, + settings: Optional[Settings] = None, stop_sequences: Optional[List[str]] = None, client_config: Optional[Config] = None, retry_timeout_secs: Optional[float] = 5.0, @@ -797,7 +797,7 @@ class AWSBedrockLLMService(LLMService): model: The AWS Bedrock model identifier to use. .. deprecated:: 0.0.105 - Use ``settings=AWSBedrockLLMSettings(model=...)`` instead. + Use ``settings=AWSBedrockLLMService.Settings(model=...)`` instead. aws_access_key: AWS access key ID. If None, uses default credentials. aws_secret_key: AWS secret access key. If None, uses default credentials. @@ -806,7 +806,7 @@ class AWSBedrockLLMService(LLMService): params: Model parameters and configuration. .. deprecated:: 0.0.105 - Use ``settings=AWSBedrockLLMSettings(...)`` instead. + Use ``settings=AWSBedrockLLMService.Settings(...)`` instead. settings: Runtime-updatable settings for this service. When both deprecated parameters and *settings* are provided, *settings* @@ -814,7 +814,7 @@ class AWSBedrockLLMService(LLMService): stop_sequences: List of strings that stop generation. .. deprecated:: 0.0.105 - Use ``settings=AWSBedrockLLMSettings(stop_sequences=...)`` instead. + Use ``settings=AWSBedrockLLMService.Settings(stop_sequences=...)`` instead. client_config: Custom boto3 client configuration. retry_timeout_secs: Request timeout in seconds for retry logic. @@ -822,7 +822,7 @@ class AWSBedrockLLMService(LLMService): **kwargs: Additional arguments passed to parent LLMService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = AWSBedrockLLMSettings( + default_settings = self.Settings( model="us.amazon.nova-lite-v1:0", system_instruction=None, max_tokens=None, @@ -841,15 +841,15 @@ class AWSBedrockLLMService(LLMService): # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", AWSBedrockLLMSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model if stop_sequences is not None: - _warn_deprecated_param("stop_sequences", AWSBedrockLLMSettings, "stop_sequences") + self._warn_init_param_moved_to_settings("stop_sequences", "stop_sequences") default_settings.stop_sequences = stop_sequences # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", AWSBedrockLLMSettings) + self._warn_init_param_moved_to_settings("params") if not settings: default_settings.max_tokens = params.max_tokens default_settings.temperature = params.temperature @@ -923,7 +923,10 @@ class AWSBedrockLLMService(LLMService): return inference_config async def run_inference( - self, context: LLMContext | OpenAILLMContext, max_tokens: Optional[int] = None + self, + context: LLMContext | OpenAILLMContext, + max_tokens: Optional[int] = None, + system_instruction: Optional[str] = None, ) -> Optional[str]: """Run a one-shot, out-of-band (i.e. out-of-pipeline) inference with the given LLM context. @@ -931,6 +934,8 @@ class AWSBedrockLLMService(LLMService): context: The LLM context containing conversation history. max_tokens: Optional maximum number of tokens to generate. If provided, overrides the service's default max_tokens setting. + system_instruction: Optional system instruction to use for this inference. + If provided, overrides any system instruction in the context. Returns: The LLM's response as a string, or None if no response is generated. @@ -947,6 +952,15 @@ class AWSBedrockLLMService(LLMService): messages = context.messages system = getattr(context, "system", None) # [{"text": "system message"}] + # Override system instruction if provided + if system_instruction is not None: + if system: + logger.warning( + f"{self}: Both system_instruction and a system message in context are set." + " Using system_instruction." + ) + system = [{"text": system_instruction}] + # Prepare request parameters using the same method as streaming inference_config = self._build_inference_config() diff --git a/src/pipecat/services/aws/nova_sonic/llm.py b/src/pipecat/services/aws/nova_sonic/llm.py index 8e4633544..ffcbb5e5d 100644 --- a/src/pipecat/services/aws/nova_sonic/llm.py +++ b/src/pipecat/services/aws/nova_sonic/llm.py @@ -27,8 +27,8 @@ from pydantic import BaseModel, Field from pipecat.adapters.schemas.tools_schema import ToolsSchema from pipecat.adapters.services.aws_nova_sonic_adapter import AWSNovaSonicLLMAdapter, Role from pipecat.frames.frames import ( + AggregatedTextFrame, AggregationType, - BotStoppedSpeakingFrame, CancelFrame, EndFrame, Frame, @@ -60,7 +60,7 @@ from pipecat.processors.aggregators.openai_llm_context import ( ) from pipecat.processors.frame_processor import FrameDirection from pipecat.services.llm_service import LLMService -from pipecat.services.settings import NOT_GIVEN, LLMSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, LLMSettings, _NotGiven from pipecat.utils.time import time_now_iso8601 try: @@ -150,7 +150,7 @@ class Params(BaseModel): """Configuration parameters for AWS Nova Sonic. .. deprecated:: 0.0.105 - Use ``settings=AWSNovaSonicLLMSettings(...)`` for inference settings + Use ``settings=AWSNovaSonicLLMService.Settings(...)`` for inference settings and ``audio_config=AudioConfig(...)`` for audio configuration. Parameters: @@ -247,7 +247,7 @@ class AWSNovaSonicLLMService(LLMService): """ Settings = AWSNovaSonicLLMSettings - _settings: AWSNovaSonicLLMSettings + _settings: Settings # Override the default adapter to use the AWSNovaSonicLLMAdapter one adapter_class = AWSNovaSonicLLMAdapter @@ -263,7 +263,7 @@ class AWSNovaSonicLLMService(LLMService): voice_id: str = "matthew", params: Optional[Params] = None, audio_config: Optional[AudioConfig] = None, - settings: Optional[AWSNovaSonicLLMSettings] = None, + settings: Optional[Settings] = None, system_instruction: Optional[str] = None, tools: Optional[ToolsSchema] = None, send_transcription_frames: bool = True, @@ -282,7 +282,7 @@ class AWSNovaSonicLLMService(LLMService): model: Model identifier. Defaults to "amazon.nova-2-sonic-v1:0". .. deprecated:: 0.0.105 - Use ``settings=AWSNovaSonicLLMSettings(model=...)`` instead. + Use ``settings=AWSNovaSonicLLMService.Settings(model=...)`` instead. voice_id: Voice ID for speech synthesis. Note that some voices are designed for use with a specific language. @@ -291,12 +291,12 @@ class AWSNovaSonicLLMService(LLMService): - Nova Sonic (the older model): see https://docs.aws.amazon.com/nova/latest/userguide/available-voices.html. .. deprecated:: 0.0.105 - Use ``settings=AWSNovaSonicLLMSettings(voice=...)`` instead. + Use ``settings=AWSNovaSonicLLMService.Settings(voice=...)`` instead. params: Model parameters for audio configuration and inference. .. deprecated:: 0.0.105 - Use ``settings=AWSNovaSonicLLMSettings(...)`` for inference + Use ``settings=AWSNovaSonicLLMService.Settings(...)`` for inference settings and ``audio_config=AudioConfig(...)`` for audio configuration. @@ -308,7 +308,7 @@ class AWSNovaSonicLLMService(LLMService): system_instruction: System-level instruction for the model. .. deprecated:: 0.0.105 - Use ``settings=AWSNovaSonicLLMSettings(system_instruction=...)`` instead. + Use ``settings=AWSNovaSonicLLMService.Settings(system_instruction=...)`` instead. tools: Available tools/functions for the model to use. send_transcription_frames: Whether to emit transcription frames. @@ -319,7 +319,7 @@ class AWSNovaSonicLLMService(LLMService): **kwargs: Additional arguments passed to the parent LLMService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = AWSNovaSonicLLMSettings( + default_settings = self.Settings( model="amazon.nova-2-sonic-v1:0", system_instruction=None, voice="matthew", @@ -337,15 +337,13 @@ class AWSNovaSonicLLMService(LLMService): # 2. Apply direct init arg overrides (deprecated) if model != "amazon.nova-2-sonic-v1:0": - _warn_deprecated_param("model", AWSNovaSonicLLMSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model if voice_id != "matthew": - _warn_deprecated_param("voice_id", AWSNovaSonicLLMSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id if system_instruction is not None: - _warn_deprecated_param( - "system_instruction", AWSNovaSonicLLMSettings, "system_instruction" - ) + self._warn_init_param_moved_to_settings("system_instruction", "system_instruction") default_settings.system_instruction = system_instruction # 3. Apply params overrides — only if settings not provided @@ -356,7 +354,7 @@ class AWSNovaSonicLLMService(LLMService): warnings.simplefilter("always") warnings.warn( "The `params` parameter is deprecated. " - "Use `settings=AWSNovaSonicLLMSettings(...)` for inference settings " + "Use `settings=self.Settings(...)` for inference settings " "(temperature, max_tokens, top_p, endpointing_sensitivity) " "and `audio_config=AudioConfig(...)` for audio configuration " "(sample rates, sample sizes, channel counts).", @@ -426,18 +424,16 @@ class AWSNovaSonicLLMService(LLMService): self._input_audio_content_name: Optional[str] = None self._content_being_received: Optional[CurrentContent] = None self._assistant_is_responding = False - self._may_need_repush_assistant_text = False self._ready_to_send_context = False - self._handling_bot_stopped_speaking = False self._triggering_assistant_response = False self._waiting_for_trigger_transcription = False self._disconnecting = False self._connected_time: Optional[float] = None self._wants_connection = False self._user_text_buffer = "" - self._assistant_text_buffer = "" self._completed_tool_calls = set() self._audio_input_started = False + self._pending_speculative_text: Optional[str] = None file_path = files("pipecat.services.aws.nova_sonic").joinpath("ready.wav") with wave.open(file_path.open("rb"), "rb") as wav_file: @@ -447,7 +443,7 @@ class AWSNovaSonicLLMService(LLMService): # settings # - async def _update_settings(self, delta: AWSNovaSonicLLMSettings) -> dict[str, Any]: + async def _update_settings(self, delta: Settings) -> dict[str, Any]: """Apply a settings delta. Settings are stored but not applied to the active connection. @@ -507,11 +503,13 @@ class AWSNovaSonicLLMService(LLMService): async def reset_conversation(self): """Reset the conversation state while preserving context. - Handles bot stopped speaking event, disconnects from the service, - and reconnects with the preserved context. + Cleans up any in-progress assistant response, disconnects from the + service, and reconnects with the preserved context. """ logger.debug("Resetting conversation") - await self._handle_bot_stopped_speaking(delay_to_catch_trailing_assistant_text=False) + if self._assistant_is_responding: + self._assistant_is_responding = False + await self._report_assistant_response_ended() # Grab context to carry through disconnect/reconnect context = self._context @@ -542,8 +540,6 @@ class AWSNovaSonicLLMService(LLMService): await self._handle_context(context) elif isinstance(frame, InputAudioRawFrame): await self._handle_input_audio_frame(frame) - elif isinstance(frame, BotStoppedSpeakingFrame): - await self._handle_bot_stopped_speaking(delay_to_catch_trailing_assistant_text=True) elif isinstance(frame, InterruptionFrame): await self._handle_interruption_frame() @@ -571,49 +567,8 @@ class AWSNovaSonicLLMService(LLMService): await self._send_user_audio_event(frame.audio) - async def _handle_bot_stopped_speaking(self, delay_to_catch_trailing_assistant_text: bool): - # Protect against back-to-back BotStoppedSpeaking calls, which I've observed - if self._handling_bot_stopped_speaking: - return - self._handling_bot_stopped_speaking = True - - async def finalize_assistant_response(): - if self._assistant_is_responding: - # Consider the assistant finished with their response (possibly after a short delay, - # to allow for any trailing FINAL assistant text block to come in that need to make - # it into context). - # - # TODO: ideally we could base this solely on the LLM output events, but I couldn't - # figure out a reliable way to determine when we've gotten our last FINAL text block - # after the LLM is done talking. - # - # First I looked at stopReason, but it doesn't seem like the last FINAL text block - # is reliably marked END_TURN (sometimes the *first* one is, but not the last... - # bug?) - # - # Then I considered schemes where we tally or match up SPECULATIVE text blocks with - # FINAL text blocks to know how many or which FINAL blocks to expect, but user - # interruptions throw a wrench in these schemes: depending on the exact timing of - # the interruption, we should or shouldn't expect some FINAL blocks. - if delay_to_catch_trailing_assistant_text: - # This delay length is a balancing act between "catching" trailing assistant - # text that is quite delayed but not waiting so long that user text comes in - # first and results in a bit of context message order scrambling. - await asyncio.sleep(1.25) - self._assistant_is_responding = False - await self._report_assistant_response_ended() - - self._handling_bot_stopped_speaking = False - - # Finalize the assistant response, either now or after a delay - if delay_to_catch_trailing_assistant_text: - self.create_task(finalize_assistant_response()) - else: - await finalize_assistant_response() - async def _handle_interruption_frame(self): - if self._assistant_is_responding: - self._may_need_repush_assistant_text = True + pass # # LLM communication: lifecycle @@ -773,17 +728,15 @@ class AWSNovaSonicLLMService(LLMService): self._input_audio_content_name = None self._content_being_received = None self._assistant_is_responding = False - self._may_need_repush_assistant_text = False self._ready_to_send_context = False - self._handling_bot_stopped_speaking = False self._triggering_assistant_response = False self._waiting_for_trigger_transcription = False self._disconnecting = False self._connected_time = None self._user_text_buffer = "" - self._assistant_text_buffer = "" self._completed_tool_calls = set() self._audio_input_started = False + self._pending_speculative_text = None logger.info("Finished disconnecting") except Exception as e: @@ -1046,7 +999,9 @@ class AWSNovaSonicLLMService(LLMService): "toolResult": { "promptName": self._prompt_name, "contentName": content_name, - "content": json.dumps(result) if isinstance(result, dict) else result, + "content": json.dumps(result, ensure_ascii=False) + if isinstance(result, dict) + else result, } } } @@ -1153,10 +1108,11 @@ class AWSNovaSonicLLMService(LLMService): self._content_being_received = content if content.role == Role.ASSISTANT: - if content.type == ContentType.AUDIO: - # Note that an assistant response can comprise of multiple audio blocks - if not self._assistant_is_responding: - # The assistant has started responding. + if content.type == ContentType.TEXT: + if ( + content.text_stage == TextStage.SPECULATIVE + and not self._assistant_is_responding + ): self._assistant_is_responding = True await self._report_user_transcription_ended() # Consider user turn over await self._report_assistant_response_started() @@ -1232,18 +1188,30 @@ class AWSNovaSonicLLMService(LLMService): if content.role == Role.ASSISTANT: if content.type == ContentType.TEXT: - # Ignore non-final text, and the "interrupted" message (which isn't meaningful text) - if content.text_stage == TextStage.FINAL and stop_reason != "INTERRUPTED": - if self._assistant_is_responding: - # Text added to the ongoing assistant response - await self._report_assistant_response_text_added(content.text_content) + if stop_reason != "INTERRUPTED": + if content.text_stage == TextStage.SPECULATIVE: + await self._report_llm_text(content.text_content) + elif self._assistant_is_responding: + # TEXT INTERRUPTED with no audio means the user interrupted + # before audio started. End the response here since no AUDIO + # contentEnd will arrive. + self._assistant_is_responding = False + await self._report_assistant_response_ended() + elif content.type == ContentType.AUDIO: + # Emit deferred TTSTextFrame after all audio chunks have been sent + await self._report_tts_text() + if stop_reason in ("END_TURN", "INTERRUPTED"): + # END_TURN: normal completion. INTERRUPTED: user interrupted + # mid-audio. Both mean no more audio for this turn. + self._assistant_is_responding = False + await self._report_assistant_response_ended() elif content.role == Role.USER: if content.type == ContentType.TEXT: if content.text_stage == TextStage.FINAL: # User transcription text added await self._report_user_transcription_text_added(content.text_content) - async def _handle_completion_end_event(self, event_json): + async def _handle_completion_end_event(self, _): pass # @@ -1256,29 +1224,40 @@ class AWSNovaSonicLLMService(LLMService): async def _report_assistant_response_started(self): logger.debug("Assistant response started") - - # Report the start of the assistant response. await self.push_frame(LLMFullResponseStartFrame()) # Report that equivalent of TTS (this is a speech-to-speech model) started await self.push_frame(TTSStartedFrame()) - async def _report_assistant_response_text_added(self, text): - if not self._context: # should never happen - return + async def _report_llm_text(self, text): + """Push speculative assistant text and defer TTSTextFrame. - logger.debug(f"Assistant response text added: {text}") + Speculative text arrives before each audio chunk, providing real-time + text that is synchronized with what the bot is saying. LLMTextFrame and + AggregatedTextFrame are pushed immediately for real-time text display. + TTSTextFrame emission is deferred to audio contentEnd so it aligns with + audio playout timing. + """ + logger.debug(f"Assistant speculative text: {text}") - # Report the text of the assistant response. - await self._push_assistant_response_text_frames(text) + llm_text_frame = LLMTextFrame(text) + llm_text_frame.append_to_context = False + await self.push_frame(llm_text_frame) - # HACK: here we're also buffering the assistant text ourselves as a - # backup rather than relying solely on the assistant context aggregator - # to do it, because the text arrives from Nova Sonic only after all the - # assistant audio frames have been pushed, meaning that if an - # interruption frame were to arrive we would lose all of it (the text - # frames sitting in the queue would be wiped). - self._assistant_text_buffer += text + aggregated_text_frame = AggregatedTextFrame(text, aggregated_by=AggregationType.SENTENCE) + aggregated_text_frame.append_to_context = False + await self.push_frame(aggregated_text_frame) + + self._pending_speculative_text = text + + async def _report_tts_text(self): + if self._pending_speculative_text: + tts_text_frame = TTSTextFrame( + self._pending_speculative_text, aggregated_by=AggregationType.SENTENCE + ) + tts_text_frame.includes_inter_frame_spaces = True + await self.push_frame(tts_text_frame) + self._pending_speculative_text = None async def _report_assistant_response_ended(self): if not self._context: # should never happen @@ -1286,54 +1265,12 @@ class AWSNovaSonicLLMService(LLMService): logger.debug("Assistant response ended") - # If an interruption frame arrived while the assistant was responding - # we may have lost all of the assistant text (see HACK, above), so - # re-push it downstream to the aggregator now. - if self._may_need_repush_assistant_text: - # Just in case, check that assistant text hasn't already made it - # into the context (sometimes it does, despite the interruption). - messages = self._context.get_messages() - last_message = messages[-1] if messages else None - if ( - not last_message - or last_message.get("role") != "assistant" - or last_message.get("content") != self._assistant_text_buffer - ): - # We also need to re-push the LLMFullResponseStartFrame since the - # TTSTextFrame would be ignored otherwise (the interruption frame - # would have cleared the assistant aggregator state). - await self.push_frame(LLMFullResponseStartFrame()) - await self._push_assistant_response_text_frames(self._assistant_text_buffer) - self._may_need_repush_assistant_text = False - # Report the end of the assistant response. await self.push_frame(LLMFullResponseEndFrame()) # Report that equivalent of TTS (this is a speech-to-speech model) stopped. await self.push_frame(TTSStoppedFrame()) - # Clear out the buffered assistant text - self._assistant_text_buffer = "" - - async def _push_assistant_response_text_frames(self, text: str): - # In a typical "cascade" LLM + TTS setup, LLMTextFrames would not - # proceed beyond the TTS service. Therefore, since a speech-to-speech - # service like Nova Sonic combines both LLM and TTS functionality, you - # would think we wouldn't need to push LLMTextFrames at all. However, - # RTVI relies on LLMTextFrames being pushed to trigger its - # "bot-llm-text" event. So here we push an LLMTextFrame, too, but avoid - # appending it to context to avoid context message duplication. - - # Push LLMTextFrame - llm_text_frame = LLMTextFrame(text) - llm_text_frame.append_to_context = False - await self.push_frame(llm_text_frame) - - # Push TTSTextFrame - tts_text_frame = TTSTextFrame(text, aggregated_by=AggregationType.SENTENCE) - tts_text_frame.includes_inter_frame_spaces = True - await self.push_frame(tts_text_frame) - # # user transcription reporting # @@ -1363,6 +1300,12 @@ class AWSNovaSonicLLMService(LLMService): if not self._context: # should never happen return + # Nothing to report if no user speech was transcribed (e.g. the prompt + # was text-only, which is the case on the first user turn when the bot + # starts the conversation). + if not self._user_text_buffer: + return + logger.debug(f"User transcription ended") # Report to the upstream user context aggregator that some new user diff --git a/src/pipecat/services/aws/stt.py b/src/pipecat/services/aws/stt.py index 8b28c006a..ace05090d 100644 --- a/src/pipecat/services/aws/stt.py +++ b/src/pipecat/services/aws/stt.py @@ -29,7 +29,7 @@ from pipecat.frames.frames import ( TranscriptionFrame, ) from pipecat.services.aws.utils import build_event_message, decode_event, get_presigned_url -from pipecat.services.settings import STTSettings, _warn_deprecated_param +from pipecat.services.settings import STTSettings from pipecat.services.stt_latency import AWS_TRANSCRIBE_TTFS_P99 from pipecat.services.stt_service import WebsocketSTTService from pipecat.transcriptions.language import Language, resolve_language @@ -61,7 +61,7 @@ class AWSTranscribeSTTService(WebsocketSTTService): """ Settings = AWSTranscribeSTTSettings - _settings: AWSTranscribeSTTSettings + _settings: Settings def __init__( self, @@ -72,7 +72,7 @@ class AWSTranscribeSTTService(WebsocketSTTService): region: Optional[str] = None, sample_rate: Optional[int] = None, language: Optional[Language] = None, - settings: Optional[AWSTranscribeSTTSettings] = None, + settings: Optional[Settings] = None, ttfs_p99_latency: Optional[float] = AWS_TRANSCRIBE_TTFS_P99, **kwargs, ): @@ -89,7 +89,7 @@ class AWSTranscribeSTTService(WebsocketSTTService): language: Language for transcription. .. deprecated:: 0.0.105 - Use ``settings=AWSTranscribeSTTSettings(language=...)`` instead. + Use ``settings=AWSTranscribeSTTService.Settings(language=...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -98,15 +98,15 @@ class AWSTranscribeSTTService(WebsocketSTTService): **kwargs: Additional arguments passed to parent STTService class. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = AWSTranscribeSTTSettings( + default_settings = self.Settings( model=None, - language=self.language_to_service_language(Language.EN), + language=Language.EN, ) # 2. Apply direct init arg overrides (deprecated) if language is not None: - _warn_deprecated_param("language", AWSTranscribeSTTSettings, "language") - default_settings.language = self.language_to_service_language(language) + self._warn_init_param_moved_to_settings("language", "language") + default_settings.language = language # 3. (No step 3, as there's no params object to apply) diff --git a/src/pipecat/services/aws/tts.py b/src/pipecat/services/aws/tts.py index 47b12429a..32266886e 100644 --- a/src/pipecat/services/aws/tts.py +++ b/src/pipecat/services/aws/tts.py @@ -23,7 +23,7 @@ from pipecat.frames.frames import ( Frame, TTSAudioRawFrame, ) -from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven from pipecat.services.tts_service import TTSService from pipecat.transcriptions.language import Language, resolve_language from pipecat.utils.tracing.service_decorators import traced_tts @@ -149,13 +149,13 @@ class AWSPollyTTSService(TTSService): """ Settings = AWSPollyTTSSettings - _settings: AWSPollyTTSSettings + _settings: Settings class InputParams(BaseModel): """Input parameters for AWS Polly TTS configuration. .. deprecated:: 0.0.105 - Use ``AWSPollyTTSSettings`` directly via the ``settings`` parameter instead. + Use ``AWSPollyTTSService.Settings`` directly via the ``settings`` parameter instead. Parameters: engine: TTS engine to use ('standard', 'neural', etc.). @@ -183,7 +183,7 @@ class AWSPollyTTSService(TTSService): voice_id: Optional[str] = None, sample_rate: Optional[int] = None, params: Optional[InputParams] = None, - settings: Optional[AWSPollyTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initializes the AWS Polly TTS service. @@ -196,20 +196,20 @@ class AWSPollyTTSService(TTSService): voice_id: Voice ID to use for synthesis. Defaults to 'Joanna'. .. deprecated:: 0.0.105 - Use ``settings=AWSPollyTTSSettings(voice=...)`` instead. + Use ``settings=AWSPollyTTSService.Settings(voice=...)`` instead. sample_rate: Audio sample rate. If None, uses service default. params: Additional input parameters for voice customization. .. deprecated:: 0.0.105 - Use ``settings=AWSPollyTTSSettings(...)`` instead. + Use ``settings=AWSPollyTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional arguments passed to parent TTSService class. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = AWSPollyTTSSettings( + default_settings = self.Settings( model=None, voice="Joanna", language="en-US", @@ -222,19 +222,15 @@ class AWSPollyTTSService(TTSService): # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: - _warn_deprecated_param("voice_id", AWSPollyTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", AWSPollyTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: default_settings.engine = params.engine - default_settings.language = ( - self.language_to_service_language(params.language) - if params.language - else "en-US" - ) + default_settings.language = params.language if params.language else "en-US" default_settings.pitch = params.pitch default_settings.rate = params.rate default_settings.volume = params.volume diff --git a/src/pipecat/services/azure/image.py b/src/pipecat/services/azure/image.py index 1b62fbfe4..fc50d710a 100644 --- a/src/pipecat/services/azure/image.py +++ b/src/pipecat/services/azure/image.py @@ -20,7 +20,7 @@ from PIL import Image from pipecat.frames.frames import ErrorFrame, Frame, URLImageRawFrame from pipecat.services.image_service import ImageGenService -from pipecat.services.settings import NOT_GIVEN, ImageGenSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, ImageGenSettings, _NotGiven @dataclass @@ -44,7 +44,7 @@ class AzureImageGenServiceREST(ImageGenService): """ Settings = AzureImageGenSettings - _settings: AzureImageGenSettings + _settings: Settings def __init__( self, @@ -55,7 +55,7 @@ class AzureImageGenServiceREST(ImageGenService): model: Optional[str] = None, aiohttp_session: aiohttp.ClientSession, api_version="2023-06-01-preview", - settings: Optional[AzureImageGenSettings] = None, + settings: Optional[Settings] = None, ): """Initialize the AzureImageGenServiceREST. @@ -63,14 +63,14 @@ class AzureImageGenServiceREST(ImageGenService): image_size: Size specification for generated images (e.g., "1024x1024"). .. deprecated:: 0.0.105 - Use ``settings=AzureImageGenSettings(image_size=...)`` instead. + Use ``settings=AzureImageGenServiceREST.Settings(image_size=...)`` instead. api_key: Azure OpenAI API key for authentication. endpoint: Azure OpenAI endpoint URL. model: The image generation model to use. .. deprecated:: 0.0.105 - Use ``settings=AzureImageGenSettings(model=...)`` instead. + Use ``settings=AzureImageGenServiceREST.Settings(model=...)`` instead. aiohttp_session: Shared aiohttp session for HTTP requests. api_version: Azure API version string. Defaults to "2023-06-01-preview". @@ -78,18 +78,18 @@ class AzureImageGenServiceREST(ImageGenService): parameters, ``settings`` values take precedence. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = AzureImageGenSettings( + default_settings = self.Settings( model=None, image_size=None, ) # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", AzureImageGenSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model if image_size is not None: - _warn_deprecated_param("image_size", AzureImageGenSettings, "image_size") + self._warn_init_param_moved_to_settings("image_size", "image_size") default_settings.image_size = image_size # 4. Apply settings delta (canonical API, always wins) diff --git a/src/pipecat/services/azure/llm.py b/src/pipecat/services/azure/llm.py index 322ff0b3e..8b5050e5b 100644 --- a/src/pipecat/services/azure/llm.py +++ b/src/pipecat/services/azure/llm.py @@ -12,13 +12,12 @@ from typing import Optional from loguru import logger from openai import AsyncAzureOpenAI -from pipecat.services.openai.base_llm import OpenAILLMSettings +from pipecat.services.openai.base_llm import BaseOpenAILLMService from pipecat.services.openai.llm import OpenAILLMService -from pipecat.services.settings import _warn_deprecated_param @dataclass -class AzureLLMSettings(OpenAILLMSettings): +class AzureLLMSettings(BaseOpenAILLMService.Settings): """Settings for AzureLLMService.""" pass @@ -40,7 +39,7 @@ class AzureLLMService(OpenAILLMService): endpoint: str, model: Optional[str] = None, api_version: str = "2024-09-01-preview", - settings: Optional[AzureLLMSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Azure LLM service. @@ -48,10 +47,10 @@ class AzureLLMService(OpenAILLMService): Args: api_key: The API key for accessing Azure OpenAI. endpoint: The Azure endpoint URL. - model: The model identifier to use. Defaults to "gpt-4o". + model: The model identifier to use. Defaults to "gpt-4.1". .. deprecated:: 0.0.105 - Use ``settings=OpenAILLMSettings(model=...)`` instead. + Use ``settings=AzureLLMService.Settings(model=...)`` instead. api_version: Azure API version. Defaults to "2024-09-01-preview". settings: Runtime-updatable settings. When provided alongside deprecated @@ -59,11 +58,11 @@ class AzureLLMService(OpenAILLMService): **kwargs: Additional keyword arguments passed to OpenAILLMService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = AzureLLMSettings(model="gpt-4o") + default_settings = self.Settings(model="gpt-4.1") # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", AzureLLMSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. (No step 3, as there's no params object to apply) diff --git a/src/pipecat/services/azure/realtime/llm.py b/src/pipecat/services/azure/realtime/llm.py index c791af94d..e6bc05478 100644 --- a/src/pipecat/services/azure/realtime/llm.py +++ b/src/pipecat/services/azure/realtime/llm.py @@ -10,7 +10,7 @@ from dataclasses import dataclass from loguru import logger -from pipecat.services.openai.realtime.llm import OpenAIRealtimeLLMService, OpenAIRealtimeLLMSettings +from pipecat.services.openai.realtime.llm import OpenAIRealtimeLLMService try: from websockets.asyncio.client import connect as websocket_connect @@ -21,7 +21,7 @@ except ModuleNotFoundError as e: @dataclass -class AzureRealtimeLLMSettings(OpenAIRealtimeLLMSettings): +class AzureRealtimeLLMSettings(OpenAIRealtimeLLMService.Settings): """Settings for AzureRealtimeLLMService.""" pass @@ -36,7 +36,7 @@ class AzureRealtimeLLMService(OpenAIRealtimeLLMService): """ Settings = AzureRealtimeLLMSettings - _settings: AzureRealtimeLLMSettings + _settings: Settings def __init__( self, diff --git a/src/pipecat/services/azure/stt.py b/src/pipecat/services/azure/stt.py index 72388b370..57306e06a 100644 --- a/src/pipecat/services/azure/stt.py +++ b/src/pipecat/services/azure/stt.py @@ -26,7 +26,7 @@ from pipecat.frames.frames import ( TranscriptionFrame, ) from pipecat.services.azure.common import language_to_azure_language -from pipecat.services.settings import STTSettings, _warn_deprecated_param +from pipecat.services.settings import STTSettings from pipecat.services.stt_latency import AZURE_TTFS_P99 from pipecat.services.stt_service import STTService from pipecat.transcriptions.language import Language @@ -67,18 +67,18 @@ class AzureSTTService(STTService): """ Settings = AzureSTTSettings - _settings: AzureSTTSettings + _settings: Settings def __init__( self, *, api_key: str, - region: str, + region: Optional[str] = None, language: Optional[Language] = Language.EN_US, sample_rate: Optional[int] = None, private_endpoint: Optional[str] = None, endpoint_id: Optional[str] = None, - settings: Optional[AzureSTTSettings] = None, + settings: Optional[Settings] = None, ttfs_p99_latency: Optional[float] = AZURE_TTFS_P99, **kwargs, ): @@ -87,10 +87,11 @@ class AzureSTTService(STTService): Args: api_key: Azure Cognitive Services subscription key. region: Azure region for the Speech service (e.g., 'eastus'). + Required unless ``private_endpoint`` is provided. language: Language for speech recognition. Defaults to English (US). .. deprecated:: 0.0.105 - Use ``settings=AzureSTTSettings(language=...)`` instead. + Use ``settings=AzureSTTService.Settings(language=...)`` instead. sample_rate: Audio sample rate in Hz. If None, uses service default. private_endpoint: Private endpoint for STT behind firewall. @@ -103,15 +104,15 @@ class AzureSTTService(STTService): **kwargs: Additional arguments passed to parent STTService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = AzureSTTSettings( + default_settings = self.Settings( model=None, - language=language_to_azure_language(Language.EN_US), + language=Language.EN_US, ) # 2. Apply direct init arg overrides (deprecated) if language is not None and language != Language.EN_US: - _warn_deprecated_param("language", AzureSTTSettings, "language") - default_settings.language = language_to_azure_language(language) + self._warn_init_param_moved_to_settings("language", "language") + default_settings.language = language # 3. (No step 3, as there's no params object to apply) @@ -126,21 +127,29 @@ class AzureSTTService(STTService): **kwargs, ) - speech_config_kwargs: dict[str, Any] = { - "subscription": api_key, - "speech_recognition_language": default_settings.language - or language_to_azure_language(Language.EN_US), - } + recognition_language = default_settings.language or language_to_azure_language( + Language.EN_US + ) + + if not region and not private_endpoint: + raise ValueError("Either 'region' or 'private_endpoint' must be provided.") + if private_endpoint: if region: logger.warning( "Both 'region' and 'private_endpoint' provided; 'region' will be ignored." ) - speech_config_kwargs["endpoint"] = private_endpoint + self._speech_config = SpeechConfig( + subscription=api_key, + endpoint=private_endpoint, + speech_recognition_language=recognition_language, + ) else: - speech_config_kwargs["region"] = region - - self._speech_config = SpeechConfig(**speech_config_kwargs) + self._speech_config = SpeechConfig( + subscription=api_key, + region=region, + speech_recognition_language=recognition_language, + ) if endpoint_id: self._speech_config.endpoint_id = endpoint_id diff --git a/src/pipecat/services/azure/tts.py b/src/pipecat/services/azure/tts.py index 1c74c7655..a9491e9aa 100644 --- a/src/pipecat/services/azure/tts.py +++ b/src/pipecat/services/azure/tts.py @@ -25,7 +25,7 @@ from pipecat.frames.frames import ( ) from pipecat.processors.frame_processor import FrameDirection from pipecat.services.azure.common import language_to_azure_language -from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven from pipecat.services.tts_service import TextAggregationMode, TTSService from pipecat.transcriptions.language import Language from pipecat.utils.tracing.service_decorators import traced_tts @@ -97,7 +97,8 @@ class AzureBaseTTSService: This is a mixin class and should be used alongside TTSService or its subclasses. """ - _settings: AzureTTSSettings + Settings = AzureTTSSettings + _settings: Settings # Define SSML escape mappings based on SSML reserved characters # See - https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-structure @@ -113,7 +114,7 @@ class AzureBaseTTSService: """Input parameters for Azure TTS voice configuration. .. deprecated:: 0.0.105 - Use ``settings=AzureTTSSettings(...)`` instead. + Use ``settings=AzureBaseTTSService.Settings(...)`` instead. Parameters: emphasis: Emphasis level for speech ("strong", "moderate", "reduced"). @@ -256,7 +257,7 @@ class AzureTTSService(TTSService, AzureBaseTTSService): voice: Optional[str] = None, sample_rate: Optional[int] = None, params: Optional[AzureBaseTTSService.InputParams] = None, - settings: Optional[AzureTTSSettings] = None, + settings: Optional[Settings] = None, aggregate_sentences: Optional[bool] = None, text_aggregation_mode: Optional[TextAggregationMode] = None, **kwargs, @@ -269,13 +270,13 @@ class AzureTTSService(TTSService, AzureBaseTTSService): voice: Voice name to use for synthesis. .. deprecated:: 0.0.105 - Use ``settings=AzureTTSSettings(voice=...)`` instead. + Use ``settings=AzureTTSService.Settings(voice=...)`` instead. sample_rate: Audio sample rate in Hz. If None, uses service default. params: Voice and synthesis parameters configuration. .. deprecated:: 0.0.105 - Use ``settings=AzureTTSSettings(...)`` instead. + Use ``settings=AzureTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -288,7 +289,7 @@ class AzureTTSService(TTSService, AzureBaseTTSService): **kwargs: Additional arguments passed to parent WordTTSService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = AzureTTSSettings( + default_settings = self.Settings( model=None, voice="en-US-SaraNeural", language="en-US", @@ -303,19 +304,15 @@ class AzureTTSService(TTSService, AzureBaseTTSService): # 2. Apply direct init arg overrides (deprecated) if voice is not None: - _warn_deprecated_param("voice", AzureTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice", "voice") default_settings.voice = voice # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", AzureTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: default_settings.emphasis = params.emphasis - default_settings.language = ( - self.language_to_service_language(params.language) - if params.language - else "en-US" - ) + default_settings.language = params.language if params.language else "en-US" default_settings.pitch = params.pitch default_settings.rate = params.rate default_settings.role = params.role @@ -761,7 +758,7 @@ class AzureHttpTTSService(TTSService, AzureBaseTTSService): voice: Optional[str] = None, sample_rate: Optional[int] = None, params: Optional[AzureBaseTTSService.InputParams] = None, - settings: Optional[AzureTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Azure HTTP TTS service. @@ -772,20 +769,20 @@ class AzureHttpTTSService(TTSService, AzureBaseTTSService): voice: Voice name to use for synthesis. .. deprecated:: 0.0.105 - Use ``settings=AzureTTSSettings(voice=...)`` instead. + Use ``settings=AzureHttpTTSService.Settings(voice=...)`` instead. sample_rate: Audio sample rate in Hz. If None, uses service default. params: Voice and synthesis parameters configuration. .. deprecated:: 0.0.105 - Use ``settings=AzureTTSSettings(...)`` instead. + Use ``settings=AzureHttpTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional arguments passed to parent TTSService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = AzureTTSSettings( + default_settings = self.Settings( model=None, voice="en-US-SaraNeural", language="en-US", @@ -800,19 +797,15 @@ class AzureHttpTTSService(TTSService, AzureBaseTTSService): # 2. Apply direct init arg overrides (deprecated) if voice is not None: - _warn_deprecated_param("voice", AzureTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice", "voice") default_settings.voice = voice # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", AzureTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: default_settings.emphasis = params.emphasis - default_settings.language = ( - self.language_to_service_language(params.language) - if params.language - else "en-US" - ) + default_settings.language = params.language if params.language else "en-US" default_settings.pitch = params.pitch default_settings.rate = params.rate default_settings.role = params.role diff --git a/src/pipecat/services/camb/tts.py b/src/pipecat/services/camb/tts.py index 9918b2320..b6b83a928 100644 --- a/src/pipecat/services/camb/tts.py +++ b/src/pipecat/services/camb/tts.py @@ -30,7 +30,7 @@ from pipecat.frames.frames import ( StartFrame, TTSAudioRawFrame, ) -from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven from pipecat.services.tts_service import TTSService from pipecat.transcriptions.language import Language, resolve_language from pipecat.utils.tracing.service_decorators import traced_tts @@ -138,10 +138,13 @@ class CambTTSSettings(TTSSettings): """Settings for CambTTSService. Parameters: + voice: Camb.ai voice ID. Overrides ``TTSSettings.voice`` (str) because + Camb.ai uses integer voice IDs. user_instructions: Custom instructions for mars-instruct model only. Ignored for other models. Max 1000 characters. """ + voice: int | _NotGiven = field(default_factory=lambda: NOT_GIVEN) user_instructions: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) @@ -158,24 +161,31 @@ class CambTTSService(TTSService): Example:: # Basic usage with mars-flash (fast) - tts = CambTTSService(api_key="your-api-key", model="mars-flash") + tts = CambTTSService( + api_key="your-api-key", + settings=CambTTSService.Settings( + model="mars-flash" + ) + ) # High quality with mars-pro tts = CambTTSService( api_key="your-api-key", - voice_id=12345, - model="mars-pro", + settings=CambTTSService.Settings( + voice=12345, + model="mars-pro", + ) ) """ Settings = CambTTSSettings - _settings: CambTTSSettings + _settings: Settings class InputParams(BaseModel): """Input parameters for Camb.ai TTS configuration. .. deprecated:: 0.0.105 - Use ``settings=CambTTSSettings(...)`` instead. + Use ``settings=CambTTSService.Settings(...)`` instead. Parameters: language: Language for synthesis (BCP-47 format). Defaults to English. @@ -200,7 +210,7 @@ class CambTTSService(TTSService): timeout: float = 60.0, sample_rate: Optional[int] = None, params: Optional[InputParams] = None, - settings: Optional[CambTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Camb.ai TTS service. @@ -210,12 +220,12 @@ class CambTTSService(TTSService): voice_id: Voice ID to use. .. deprecated:: 0.0.105 - Use ``settings=CambTTSSettings(voice=...)`` instead. + Use ``settings=CambTTSService.Settings(voice=...)`` instead. model: TTS model to use. Options: "mars-flash" (fast), "mars-pro" (high quality). .. deprecated:: 0.0.105 - Use ``settings=CambTTSSettings(model=...)`` instead. + Use ``settings=CambTTSService.Settings(model=...)`` instead. timeout: Request timeout in seconds. Defaults to 60.0 (minimum recommended by Camb.ai). @@ -223,14 +233,14 @@ class CambTTSService(TTSService): params: Additional voice parameters. If None, uses defaults. .. deprecated:: 0.0.105 - Use ``settings=CambTTSSettings(...)`` instead. + Use ``settings=CambTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional arguments passed to parent TTSService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = CambTTSSettings( + default_settings = self.Settings( model="mars-flash", voice=147320, language="en-us", @@ -239,20 +249,18 @@ class CambTTSService(TTSService): # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", CambTTSSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model if voice_id is not None: - _warn_deprecated_param("voice_id", CambTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", CambTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: if params.language is not None: - default_settings.language = ( - self.language_to_service_language(params.language) or "en-us" - ) + default_settings.language = params.language if params.user_instructions is not None: default_settings.user_instructions = params.user_instructions diff --git a/src/pipecat/services/cartesia/stt.py b/src/pipecat/services/cartesia/stt.py index 9c0924827..85d43bcd3 100644 --- a/src/pipecat/services/cartesia/stt.py +++ b/src/pipecat/services/cartesia/stt.py @@ -28,7 +28,7 @@ from pipecat.frames.frames import ( VADUserStoppedSpeakingFrame, ) from pipecat.processors.frame_processor import FrameDirection -from pipecat.services.settings import STTSettings, _warn_deprecated_param +from pipecat.services.settings import STTSettings from pipecat.services.stt_latency import CARTESIA_TTFS_P99 from pipecat.services.stt_service import WebsocketSTTService from pipecat.transcriptions.language import Language @@ -55,7 +55,7 @@ class CartesiaLiveOptions: """Configuration options for Cartesia Live STT service. .. deprecated:: 0.0.105 - Use ``settings=CartesiaSTTSettings(...)`` for model/language and + Use ``settings=CartesiaSTTService.Settings(...)`` for model/language and direct ``__init__`` parameters for encoding/sample_rate instead. """ @@ -147,7 +147,7 @@ class CartesiaSTTService(WebsocketSTTService): """ Settings = CartesiaSTTSettings - _settings: CartesiaSTTSettings + _settings: Settings def __init__( self, @@ -157,7 +157,7 @@ class CartesiaSTTService(WebsocketSTTService): encoding: str = "pcm_s16le", sample_rate: Optional[int] = None, live_options: Optional[CartesiaLiveOptions] = None, - settings: Optional[CartesiaSTTSettings] = None, + settings: Optional[Settings] = None, ttfs_p99_latency: Optional[float] = CARTESIA_TTFS_P99, **kwargs, ): @@ -172,7 +172,7 @@ class CartesiaSTTService(WebsocketSTTService): live_options: Configuration options for transcription service. .. deprecated:: 0.0.105 - Use ``settings=CartesiaSTTSettings(...)`` for model/language + Use ``settings=CartesiaSTTService.Settings(...)`` for model/language and direct init parameters for encoding/sample_rate instead. settings: Runtime-updatable settings. When provided alongside deprecated @@ -182,14 +182,14 @@ class CartesiaSTTService(WebsocketSTTService): **kwargs: Additional arguments passed to parent STTService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = CartesiaSTTSettings( + default_settings = self.Settings( model="ink-whisper", language=Language.EN.value, ) # 2. Apply live_options overrides — only if settings not provided if live_options is not None: - _warn_deprecated_param("live_options", CartesiaSTTSettings) + self._warn_init_param_moved_to_settings("live_options") if not settings: if live_options.sample_rate and sample_rate is None: sample_rate = live_options.sample_rate @@ -313,7 +313,7 @@ class CartesiaSTTService(WebsocketSTTService): """Apply a settings delta. Args: - delta: A :class:`STTSettings` (or ``CartesiaSTTSettings``) delta. + delta: A :class:`STTSettings` (or ``CartesiaSTTService.Settings``) delta. Returns: Dict mapping changed field names to their previous values. diff --git a/src/pipecat/services/cartesia/tts.py b/src/pipecat/services/cartesia/tts.py index d41f341ca..b713a0d9a 100644 --- a/src/pipecat/services/cartesia/tts.py +++ b/src/pipecat/services/cartesia/tts.py @@ -25,7 +25,7 @@ from pipecat.frames.frames import ( TTSAudioRawFrame, TTSStoppedFrame, ) -from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven from pipecat.services.tts_service import TextAggregationMode, TTSService, WebsocketTTSService from pipecat.transcriptions.language import Language, resolve_language from pipecat.utils.text.base_text_aggregator import BaseTextAggregator @@ -211,7 +211,7 @@ class CartesiaTTSService(WebsocketTTSService): """ Settings = CartesiaTTSSettings - _settings: CartesiaTTSSettings + _settings: Settings class InputParams(BaseModel): """Input parameters for Cartesia TTS configuration. @@ -239,7 +239,7 @@ class CartesiaTTSService(WebsocketTTSService): encoding: str = "pcm_s16le", container: str = "raw", params: Optional[InputParams] = None, - settings: Optional[CartesiaTTSSettings] = None, + settings: Optional[Settings] = None, text_aggregator: Optional[BaseTextAggregator] = None, text_aggregation_mode: Optional[TextAggregationMode] = None, aggregate_sentences: Optional[bool] = None, @@ -252,14 +252,14 @@ class CartesiaTTSService(WebsocketTTSService): voice_id: ID of the voice to use for synthesis. .. deprecated:: 0.0.105 - Use ``settings=CartesiaTTSSettings(voice=...)`` instead. + Use ``settings=CartesiaTTSService.Settings(voice=...)`` instead. cartesia_version: API version string for Cartesia service. url: WebSocket URL for Cartesia TTS API. model: TTS model to use (e.g., "sonic-3"). .. deprecated:: 0.0.105 - Use ``settings=CartesiaTTSSettings(model=...)`` instead. + Use ``settings=CartesiaTTSService.Settings(model=...)`` instead. sample_rate: Audio sample rate. If None, uses default. encoding: Audio encoding format. @@ -267,7 +267,7 @@ class CartesiaTTSService(WebsocketTTSService): params: Additional input parameters for voice customization. .. deprecated:: 0.0.105 - Use ``settings=CartesiaTTSSettings(...)`` instead. + Use ``settings=CartesiaTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -299,28 +299,28 @@ class CartesiaTTSService(WebsocketTTSService): # playout timing of the audio! # 1. Initialize default_settings with hardcoded defaults - default_settings = CartesiaTTSSettings( + default_settings = self.Settings( model="sonic-3", voice=None, - language=language_to_cartesia_language(Language.EN), + language=Language.EN, generation_config=None, pronunciation_dict_id=None, ) # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: - _warn_deprecated_param("voice_id", CartesiaTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id if model is not None: - _warn_deprecated_param("model", CartesiaTTSSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", CartesiaTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: if params.language is not None: - default_settings.language = self.language_to_service_language(params.language) + default_settings.language = params.language if params.generation_config is not None: default_settings.generation_config = params.generation_config if params.pronunciation_dict_id is not None: @@ -683,7 +683,7 @@ class CartesiaHttpTTSService(TTSService): """ Settings = CartesiaTTSSettings - _settings: CartesiaTTSSettings + _settings: Settings class InputParams(BaseModel): """Input parameters for Cartesia HTTP TTS configuration. @@ -712,7 +712,7 @@ class CartesiaHttpTTSService(TTSService): encoding: str = "pcm_s16le", container: str = "raw", params: Optional[InputParams] = None, - settings: Optional[CartesiaTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Cartesia HTTP TTS service. @@ -722,12 +722,12 @@ class CartesiaHttpTTSService(TTSService): voice_id: ID of the voice to use for synthesis. .. deprecated:: 0.0.105 - Use ``settings=CartesiaTTSSettings(voice=...)`` instead. + Use ``settings=CartesiaHttpTTSService.Settings(voice=...)`` instead. model: TTS model to use (e.g., "sonic-3"). .. deprecated:: 0.0.105 - Use ``settings=CartesiaTTSSettings(model=...)`` instead. + Use ``settings=CartesiaHttpTTSService.Settings(model=...)`` instead. base_url: Base URL for Cartesia HTTP API. cartesia_version: API version string for Cartesia service. @@ -739,35 +739,35 @@ class CartesiaHttpTTSService(TTSService): params: Additional input parameters for voice customization. .. deprecated:: 0.0.105 - Use ``settings=CartesiaTTSSettings(...)`` instead. + Use ``settings=CartesiaHttpTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional arguments passed to the parent TTSService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = CartesiaTTSSettings( + default_settings = self.Settings( model="sonic-3", voice=None, - language=language_to_cartesia_language(Language.EN), + language=Language.EN, generation_config=None, pronunciation_dict_id=None, ) # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: - _warn_deprecated_param("voice_id", CartesiaTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id if model is not None: - _warn_deprecated_param("model", CartesiaTTSSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", CartesiaTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: if params.language is not None: - default_settings.language = self.language_to_service_language(params.language) + default_settings.language = params.language if params.generation_config is not None: default_settings.generation_config = params.generation_config if params.pronunciation_dict_id is not None: diff --git a/src/pipecat/services/cerebras/llm.py b/src/pipecat/services/cerebras/llm.py index 5d4bc2d14..dfb62baf8 100644 --- a/src/pipecat/services/cerebras/llm.py +++ b/src/pipecat/services/cerebras/llm.py @@ -12,13 +12,12 @@ from typing import Optional from loguru import logger from pipecat.adapters.services.open_ai_adapter import OpenAILLMInvocationParams -from pipecat.services.openai.base_llm import OpenAILLMSettings +from pipecat.services.openai.base_llm import BaseOpenAILLMService from pipecat.services.openai.llm import OpenAILLMService -from pipecat.services.settings import _warn_deprecated_param @dataclass -class CerebrasLLMSettings(OpenAILLMSettings): +class CerebrasLLMSettings(BaseOpenAILLMService.Settings): """Settings for CerebrasLLMService.""" pass @@ -32,7 +31,7 @@ class CerebrasLLMService(OpenAILLMService): """ Settings = CerebrasLLMSettings - _settings: CerebrasLLMSettings + _settings: Settings def __init__( self, @@ -40,7 +39,7 @@ class CerebrasLLMService(OpenAILLMService): api_key: str, base_url: str = "https://api.cerebras.ai/v1", model: Optional[str] = None, - settings: Optional[CerebrasLLMSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Cerebras LLM service. @@ -51,18 +50,18 @@ class CerebrasLLMService(OpenAILLMService): model: The model identifier to use. Defaults to "gpt-oss-120b". .. deprecated:: 0.0.105 - Use ``settings=OpenAILLMSettings(model=...)`` instead. + Use ``settings=CerebrasLLMService.Settings(model=...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional keyword arguments passed to OpenAILLMService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = CerebrasLLMSettings(model="gpt-oss-120b") + default_settings = self.Settings(model="gpt-oss-120b") # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", CerebrasLLMSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. (No step 3, as there's no params object to apply) @@ -118,6 +117,10 @@ class CerebrasLLMService(OpenAILLMService): # Prepend system instruction if set if self._settings.system_instruction: messages = params.get("messages", []) + if messages and messages[0].get("role") == "system": + logger.warning( + f"{self}: Both system_instruction and an initial system message in context are set. This may be unintended." + ) params["messages"] = [ {"role": "system", "content": self._settings.system_instruction} ] + messages diff --git a/src/pipecat/services/deepgram/flux/stt.py b/src/pipecat/services/deepgram/flux/stt.py index 3227bd0a6..64d4e87b9 100644 --- a/src/pipecat/services/deepgram/flux/stt.py +++ b/src/pipecat/services/deepgram/flux/stt.py @@ -28,7 +28,7 @@ from pipecat.frames.frames import ( UserStartedSpeakingFrame, UserStoppedSpeakingFrame, ) -from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven from pipecat.services.stt_service import WebsocketSTTService from pipecat.transcriptions.language import Language from pipecat.utils.time import time_now_iso8601 @@ -116,14 +116,14 @@ class DeepgramFluxSTTService(WebsocketSTTService): """ Settings = DeepgramFluxSTTSettings - _settings: DeepgramFluxSTTSettings + _settings: Settings _CONFIGURE_FIELDS = {"keyterm", "eot_threshold", "eager_eot_threshold", "eot_timeout_ms"} class InputParams(BaseModel): """Configuration parameters for Deepgram Flux API. .. deprecated:: 0.0.105 - Use ``settings=DeepgramFluxSTTSettings(...)`` instead. + Use ``settings=DeepgramFluxSTTService.Settings(...)`` instead. Parameters: eager_eot_threshold: Optional. EagerEndOfTurn/TurnResumed are off by default. @@ -162,7 +162,7 @@ class DeepgramFluxSTTService(WebsocketSTTService): tag: Optional[list] = None, params: Optional[InputParams] = None, should_interrupt: bool = True, - settings: Optional[DeepgramFluxSTTSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Deepgram Flux STT service. @@ -176,7 +176,7 @@ class DeepgramFluxSTTService(WebsocketSTTService): model: Deepgram Flux model to use for transcription. .. deprecated:: 0.0.105 - Use ``settings=DeepgramFluxSTTSettings(model=...)`` instead. + Use ``settings=DeepgramFluxSTTService.Settings(model=...)`` instead. flux_encoding: Audio encoding format required by Flux API. Must be "linear16". Raw signed little-endian 16-bit PCM encoding. @@ -184,7 +184,7 @@ class DeepgramFluxSTTService(WebsocketSTTService): params: InputParams instance containing detailed API configuration options. .. deprecated:: 0.0.105 - Use ``settings=DeepgramFluxSTTSettings(...)`` instead. + Use ``settings=DeepgramFluxSTTService.Settings(...)`` instead. should_interrupt: Determine whether the bot should be interrupted when Flux detects that the user is speaking. settings: Runtime-updatable settings. When provided alongside deprecated @@ -200,7 +200,7 @@ class DeepgramFluxSTTService(WebsocketSTTService): stt = DeepgramFluxSTTService( api_key="your-api-key", - settings=DeepgramFluxSTTSettings( + settings=DeepgramFluxSTTService.Settings( model="flux-general-en", eager_eot_threshold=0.5, eot_threshold=0.8, @@ -221,7 +221,7 @@ class DeepgramFluxSTTService(WebsocketSTTService): # already try to reconnect if needed. # 1. Initialize default_settings with hardcoded defaults - default_settings = DeepgramFluxSTTSettings( + default_settings = self.Settings( model="flux-general-en", language=Language.EN, eager_eot_threshold=None, @@ -233,12 +233,12 @@ class DeepgramFluxSTTService(WebsocketSTTService): # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", DeepgramFluxSTTSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", DeepgramFluxSTTSettings) + self._warn_init_param_moved_to_settings("params") if not settings: default_settings.eager_eot_threshold = params.eager_eot_threshold default_settings.eot_threshold = params.eot_threshold @@ -448,7 +448,7 @@ class DeepgramFluxSTTService(WebsocketSTTService): """ return True - async def _update_settings(self, delta: DeepgramFluxSTTSettings) -> dict[str, Any]: + async def _update_settings(self, delta: Settings) -> dict[str, Any]: """Apply a settings delta. Configure-able fields (keyterm, eot_threshold, eager_eot_threshold, diff --git a/src/pipecat/services/deepgram/sagemaker/stt.py b/src/pipecat/services/deepgram/sagemaker/stt.py index 812a2701e..1087b124f 100644 --- a/src/pipecat/services/deepgram/sagemaker/stt.py +++ b/src/pipecat/services/deepgram/sagemaker/stt.py @@ -32,8 +32,8 @@ from pipecat.frames.frames import ( ) from pipecat.processors.frame_processor import FrameDirection from pipecat.services.aws.sagemaker.bidi_client import SageMakerBidiClient -from pipecat.services.deepgram.stt import DeepgramSTTSettings, LiveOptions -from pipecat.services.settings import STTSettings, _warn_deprecated_param, is_given +from pipecat.services.deepgram.stt import DeepgramSTTService, LiveOptions +from pipecat.services.settings import STTSettings, is_given from pipecat.services.stt_latency import DEEPGRAM_SAGEMAKER_TTFS_P99 from pipecat.services.stt_service import STTService from pipecat.transcriptions.language import Language @@ -42,10 +42,10 @@ from pipecat.utils.tracing.service_decorators import traced_stt @dataclass -class DeepgramSageMakerSTTSettings(DeepgramSTTSettings): +class DeepgramSageMakerSTTSettings(DeepgramSTTService.Settings): """Settings for the Deepgram SageMaker STT service. - Inherits all fields from :class:`DeepgramSTTSettings`. + Inherits all fields from :class:`DeepgramSTTService.Settings`. """ pass @@ -69,7 +69,7 @@ class DeepgramSageMakerSTTService(STTService): stt = DeepgramSageMakerSTTService( endpoint_name="my-deepgram-endpoint", region="us-east-2", - settings=DeepgramSageMakerSTTSettings( + settings=DeepgramSageMakerSTTService.Settings( model="nova-3", language="en", interim_results=True, @@ -79,7 +79,7 @@ class DeepgramSageMakerSTTService(STTService): """ Settings = DeepgramSageMakerSTTSettings - _settings: DeepgramSageMakerSTTSettings + _settings: Settings def __init__( self, @@ -92,7 +92,7 @@ class DeepgramSageMakerSTTService(STTService): sample_rate: Optional[int] = None, mip_opt_out: Optional[bool] = None, live_options: Optional[LiveOptions] = None, - settings: Optional[DeepgramSageMakerSTTSettings] = None, + settings: Optional[Settings] = None, ttfs_p99_latency: Optional[float] = DEEPGRAM_SAGEMAKER_TTFS_P99, **kwargs, ): @@ -112,7 +112,7 @@ class DeepgramSageMakerSTTService(STTService): live_options: Legacy configuration options. .. deprecated:: 0.0.105 - Use ``settings=DeepgramSageMakerSTTSettings(...)`` for + Use ``settings=DeepgramSageMakerSTTService.Settings(...)`` for runtime-updatable fields and direct init parameters for connection-level config. @@ -124,7 +124,7 @@ class DeepgramSageMakerSTTService(STTService): **kwargs: Additional arguments passed to the parent STTService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = DeepgramSageMakerSTTSettings( + default_settings = self.Settings( model="nova-3", language=Language.EN, detect_entities=False, @@ -147,7 +147,7 @@ class DeepgramSageMakerSTTService(STTService): # 2. Apply live_options overrides — only if settings not provided if live_options is not None: - _warn_deprecated_param("live_options", DeepgramSageMakerSTTSettings) + self._warn_init_param_moved_to_settings("live_options") if not settings: # Extract init-only fields from live_options if live_options.sample_rate is not None and sample_rate is None: @@ -170,7 +170,7 @@ class DeepgramSageMakerSTTService(STTService): "mip_opt_out", } lo_dict = {k: v for k, v in live_options.to_dict().items() if k not in init_only} - delta = DeepgramSageMakerSTTSettings.from_mapping(lo_dict) + delta = self.Settings.from_mapping(lo_dict) default_settings.apply_update(delta) # 3. Apply settings delta (canonical API, always wins) @@ -216,7 +216,7 @@ class DeepgramSageMakerSTTService(STTService): return changed # Sync extra to fields after the update so self._settings stays unambiguous - if isinstance(self._settings, DeepgramSTTSettings): + if isinstance(self._settings, self.Settings): self._settings._sync_extra_to_fields() # TODO: someday we could reconnect here to apply updated settings. diff --git a/src/pipecat/services/deepgram/sagemaker/tts.py b/src/pipecat/services/deepgram/sagemaker/tts.py index a40f56713..70be4a00e 100644 --- a/src/pipecat/services/deepgram/sagemaker/tts.py +++ b/src/pipecat/services/deepgram/sagemaker/tts.py @@ -33,7 +33,7 @@ from pipecat.frames.frames import ( ) from pipecat.processors.frame_processor import FrameDirection from pipecat.services.aws.sagemaker.bidi_client import SageMakerBidiClient -from pipecat.services.settings import TTSSettings, _warn_deprecated_param +from pipecat.services.settings import TTSSettings from pipecat.services.tts_service import TTSService from pipecat.utils.tracing.service_decorators import traced_tts @@ -63,12 +63,14 @@ class DeepgramSageMakerTTSService(TTSService): tts = DeepgramSageMakerTTSService( endpoint_name="my-deepgram-tts-endpoint", region="us-east-2", - voice="aura-2-helena-en", + settings=DeepgramSageMakerTTSService.Settings( + voice="aura-2-helena-en", + ) ) """ Settings = DeepgramSageMakerTTSSettings - _settings: DeepgramSageMakerTTSSettings + _settings: Settings def __init__( self, @@ -78,7 +80,7 @@ class DeepgramSageMakerTTSService(TTSService): voice: Optional[str] = None, sample_rate: Optional[int] = None, encoding: str = "linear16", - settings: Optional[DeepgramSageMakerTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Deepgram SageMaker TTS service. @@ -90,7 +92,7 @@ class DeepgramSageMakerTTSService(TTSService): voice: Voice model to use for synthesis. Defaults to "aura-2-helena-en". .. deprecated:: 0.0.105 - Use ``settings=DeepgramSageMakerTTSSettings(voice=...)`` instead. + Use ``settings=DeepgramSageMakerTTSService.Settings(voice=...)`` instead. sample_rate: Audio sample rate in Hz. If None, uses the value from StartFrame. encoding: Audio encoding format. Defaults to "linear16". @@ -99,11 +101,11 @@ class DeepgramSageMakerTTSService(TTSService): **kwargs: Additional arguments passed to the parent TTSService. """ if voice is not None: - _warn_deprecated_param("voice", DeepgramSageMakerTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice", "voice") voice = voice or "aura-2-helena-en" - default_settings = DeepgramSageMakerTTSSettings( + default_settings = self.Settings( model=None, voice=voice, language=None, diff --git a/src/pipecat/services/deepgram/stt.py b/src/pipecat/services/deepgram/stt.py index d8f782f5d..0fb891d61 100644 --- a/src/pipecat/services/deepgram/stt.py +++ b/src/pipecat/services/deepgram/stt.py @@ -29,7 +29,6 @@ from pipecat.services.settings import ( NOT_GIVEN, STTSettings, _NotGiven, - _warn_deprecated_param, is_given, ) from pipecat.services.stt_latency import DEEPGRAM_TTFS_P99 @@ -59,7 +58,7 @@ class LiveOptions: deepgram-sdk v6. .. deprecated:: 0.0.105 - Use ``settings=DeepgramSTTSettings(...)`` for runtime-updatable fields + Use ``settings=DeepgramSTTService.Settings(...)`` for runtime-updatable fields and direct ``__init__`` parameters for connection-level config instead. """ @@ -248,6 +247,45 @@ class DeepgramSTTSettings(STTSettings): del self.extra[key] +def _derive_deepgram_urls(base_url: str) -> tuple[str, str]: + """Derive paired WebSocket and HTTP URLs from a single base URL. + + The Deepgram SDK client requires both a WebSocket URL (for streaming) + and an HTTP URL (for REST calls). This helper lets developers provide + a single ``base_url`` and consistently derives both, preserving the + security level they chose. Useful for air-gapped or private deployments + where insecure schemes (ws:// / http://) are acceptable. + + Accepted inputs: + - ``wss://`` or ``https://`` — secure (paired as wss + https) + - ``ws://`` or ``http://`` — insecure (paired as ws + http) + - Bare hostname (no scheme) — defaults to secure + - Unrecognized scheme — logs a warning, defaults to secure + + Args: + base_url: Host with optional scheme, port, and path. + + Returns: + A (ws_url, http_url) tuple with consistent schemes. + """ + known_schemes = ("wss://", "https://", "ws://", "http://") + if "://" in base_url: + scheme, host = base_url.split("://", 1) + scheme += "://" + if scheme not in known_schemes: + logger.warning( + f"Unrecognized scheme in base_url '{base_url}', defaulting to wss:// / https://" + ) + else: + scheme = "" + host = base_url + + insecure = scheme in ("ws://", "http://") + ws_url = f"{'ws' if insecure else 'wss'}://{host}" + http_url = f"{'http' if insecure else 'https'}://{host}" + return ws_url, http_url + + class DeepgramSTTService(STTService): """Deepgram speech-to-text service. @@ -267,7 +305,7 @@ class DeepgramSTTService(STTService): """ Settings = DeepgramSTTSettings - _settings: DeepgramSTTSettings + _settings: Settings def __init__( self, @@ -286,7 +324,7 @@ class DeepgramSTTService(STTService): live_options: Optional[LiveOptions] = None, addons: Optional[dict] = None, should_interrupt: bool = True, - settings: Optional[DeepgramSTTSettings] = None, + settings: Optional[Settings] = None, ttfs_p99_latency: Optional[float] = DEEPGRAM_TTFS_P99, **kwargs, ): @@ -313,7 +351,7 @@ class DeepgramSTTService(STTService): live_options: Legacy configuration options. .. deprecated:: 0.0.105 - Use ``settings=DeepgramSTTSettings(...)`` for runtime-updatable + Use ``settings=DeepgramSTTService.Settings(...)`` for runtime-updatable fields and direct init parameters for connection-level config. addons: Additional Deepgram features to enable. @@ -345,7 +383,7 @@ class DeepgramSTTService(STTService): base_url = url # 1. Initialize default_settings with hardcoded defaults - default_settings = DeepgramSTTSettings( + default_settings = self.Settings( model="nova-3-general", language=Language.EN, detect_entities=False, @@ -370,7 +408,7 @@ class DeepgramSTTService(STTService): # 3. Apply live_options overrides — only if settings not provided if live_options is not None: - _warn_deprecated_param("live_options", DeepgramSTTSettings) + self._warn_init_param_moved_to_settings("live_options") if not settings: # Extract init-only fields from live_options if live_options.sample_rate is not None and sample_rate is None: @@ -402,7 +440,7 @@ class DeepgramSTTService(STTService): "mip_opt_out", } lo_dict = {k: v for k, v in live_options.to_dict().items() if k not in init_only} - delta = DeepgramSTTSettings.from_mapping(lo_dict) + delta = self.Settings.from_mapping(lo_dict) default_settings.apply_update(delta) # 4. Apply settings delta (canonical API, always wins) @@ -446,8 +484,7 @@ class DeepgramSTTService(STTService): try: from deepgram import DeepgramClientEnvironment - ws_url = base_url if base_url.startswith("wss://") else f"wss://{base_url}" - http_url = base_url if base_url.startswith("https://") else f"https://{base_url}" + ws_url, http_url = _derive_deepgram_urls(base_url) environment = DeepgramClientEnvironment( base=http_url, production=ws_url, @@ -494,7 +531,7 @@ class DeepgramSTTService(STTService): return changed # Sync extra to fields after the update so self._settings stays unambiguous - if isinstance(self._settings, DeepgramSTTSettings): + if isinstance(self._settings, self.Settings): self._settings._sync_extra_to_fields() if self._connection: @@ -555,7 +592,15 @@ class DeepgramSTTService(STTService): value = getattr(s, f.name) if not is_given(value) or value is None: continue - kwargs[f.name] = str(value).lower() if isinstance(value, bool) else str(value) + # Lists (e.g. keyterm, keywords, search, redact, replace) must be + # passed through as-is so the SDK's encode_query produces repeated + # query params (keyterm=a&keyterm=b) instead of a stringified list. + if isinstance(value, list): + kwargs[f.name] = value + elif isinstance(value, bool): + kwargs[f.name] = str(value).lower() + else: + kwargs[f.name] = str(value) # model and language if is_given(s.model) and s.model is not None: @@ -581,7 +626,12 @@ class DeepgramSTTService(STTService): # Any remaining values in extra (that didn't map to declared fields) for key, value in s.extra.items(): if value is not None: - kwargs[key] = str(value).lower() if isinstance(value, bool) else str(value) + if isinstance(value, list): + kwargs[key] = value + elif isinstance(value, bool): + kwargs[key] = str(value).lower() + else: + kwargs[key] = str(value) if self._addons: for key, value in self._addons.items(): diff --git a/src/pipecat/services/deepgram/tts.py b/src/pipecat/services/deepgram/tts.py index 89bc9ae97..9f2dc3976 100644 --- a/src/pipecat/services/deepgram/tts.py +++ b/src/pipecat/services/deepgram/tts.py @@ -22,13 +22,11 @@ from pipecat.frames.frames import ( EndFrame, ErrorFrame, Frame, - InterruptionFrame, - LLMFullResponseEndFrame, StartFrame, TTSAudioRawFrame, + TTSStoppedFrame, ) -from pipecat.processors.frame_processor import FrameDirection -from pipecat.services.settings import TTSSettings, _warn_deprecated_param +from pipecat.services.settings import TTSSettings from pipecat.services.tts_service import TTSService, WebsocketTTSService from pipecat.utils.tracing.service_decorators import traced_tts @@ -59,7 +57,7 @@ class DeepgramTTSService(WebsocketTTSService): """ Settings = DeepgramTTSSettings - _settings: DeepgramTTSSettings + _settings: Settings SUPPORTED_ENCODINGS = ("linear16", "mulaw", "alaw") @@ -71,7 +69,7 @@ class DeepgramTTSService(WebsocketTTSService): base_url: str = "wss://api.deepgram.com", sample_rate: Optional[int] = None, encoding: str = "linear16", - settings: Optional[DeepgramTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Deepgram WebSocket TTS service. @@ -81,7 +79,7 @@ class DeepgramTTSService(WebsocketTTSService): voice: Voice model to use for synthesis. .. deprecated:: 0.0.105 - Use ``settings=DeepgramTTSSettings(voice=...)`` instead. + Use ``settings=DeepgramTTSService.Settings(voice=...)`` instead. base_url: WebSocket base URL for Deepgram API. Defaults to "wss://api.deepgram.com". sample_rate: Audio sample rate in Hz. If None, uses service default. @@ -99,7 +97,7 @@ class DeepgramTTSService(WebsocketTTSService): ) # 1. Initialize default_settings with hardcoded defaults - default_settings = DeepgramTTSSettings( + default_settings = self.Settings( model=None, voice="aura-2-helena-en", language=None, @@ -107,7 +105,7 @@ class DeepgramTTSService(WebsocketTTSService): # 2. Apply direct init arg overrides (deprecated) if voice is not None: - _warn_deprecated_param("voice", DeepgramTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice", "voice") default_settings.model = voice default_settings.voice = voice @@ -120,7 +118,7 @@ class DeepgramTTSService(WebsocketTTSService): super().__init__( sample_rate=sample_rate, pause_frame_processing=True, - push_stop_frames=True, + push_stop_frames=False, push_start_frame=True, append_trailing_space=True, settings=default_settings, @@ -168,19 +166,6 @@ class DeepgramTTSService(WebsocketTTSService): await super().cancel(frame) await self._disconnect() - async def process_frame(self, frame: Frame, direction: FrameDirection): - """Process frames with special handling for LLM response end. - - Args: - frame: The frame to process. - direction: The direction of frame processing. - """ - await super().process_frame(frame, direction) - - # When the LLM finishes responding, flush any remaining text in Deepgram's buffer - if isinstance(frame, (LLMFullResponseEndFrame, EndFrame)): - await self.flush_audio() - async def _connect(self): """Connect to Deepgram WebSocket and start receive task.""" await super()._connect() @@ -204,7 +189,7 @@ class DeepgramTTSService(WebsocketTTSService): """Apply a settings delta. Args: - delta: A :class:`TTSSettings` (or ``DeepgramTTSSettings``) delta. + delta: A :class:`TTSSettings` (or ``DeepgramTTSService.Settings``) delta. Returns: Dict mapping changed field names to their previous values. @@ -277,19 +262,19 @@ class DeepgramTTSService(WebsocketTTSService): return self._websocket raise Exception("Websocket not connected") - async def _handle_interruption(self, frame: InterruptionFrame, direction: FrameDirection): - """Handle interruption by sending Clear message to Deepgram. + async def on_audio_context_interrupted(self, context_id: str): + """Send Clear message to Deepgram when an audio context is interrupted. The Clear message will clear Deepgram's internal text buffer and stop sending audio, allowing for a new response to be generated. - """ - await super()._handle_interruption(frame, direction) - # Send Clear message to stop current audio generation + Args: + context_id: The ID of the audio context that was interrupted. + """ + await self.stop_all_metrics() if self._websocket: try: - clear_msg = {"type": "Clear"} - await self._websocket.send(json.dumps(clear_msg)) + await self._websocket.send(json.dumps({"type": "Clear"})) except Exception as e: logger.error(f"{self} error sending Clear message: {e}") @@ -298,11 +283,9 @@ class DeepgramTTSService(WebsocketTTSService): async for message in self._get_websocket(): if isinstance(message, bytes): # Binary message contains audio data - await self.stop_ttfb_metrics() - frame = TTSAudioRawFrame( - message, self.sample_rate, 1, context_id=self.get_active_audio_context_id() - ) - await self.push_frame(frame) + ctx_id = self.get_active_audio_context_id() + frame = TTSAudioRawFrame(message, self.sample_rate, 1, context_id=ctx_id) + await self.append_to_audio_context(ctx_id, frame) elif isinstance(message, str): # Text message contains metadata or control messages try: @@ -313,12 +296,15 @@ class DeepgramTTSService(WebsocketTTSService): logger.trace(f"Received metadata: {msg}") elif msg_type == "Flushed": logger.trace(f"Received Flushed: {msg}") - # Flushed indicates the end of audio generation for the current buffer - # This happens after flush_audio() is called + ctx_id = self.get_active_audio_context_id() + await self.append_to_audio_context( + ctx_id, TTSStoppedFrame(context_id=ctx_id) + ) + await self.remove_audio_context(ctx_id) elif msg_type == "Cleared": logger.trace(f"Received Cleared: {msg}") - # Buffer has been cleared after interruption - # TTSStoppedFrame will be sent by the interruption handler + # Buffer has been cleared after interruption. + # The on_audio_context_interrupted handler already cleaned up. elif msg_type == "Warning": logger.warning( f"{self} warning: {msg.get('description', 'Unknown warning')}" @@ -359,8 +345,6 @@ class DeepgramTTSService(WebsocketTTSService): if not self._websocket or self._websocket.state is State.CLOSED: await self._connect() - await self.start_tts_usage_metrics(text) - # Send text message to Deepgram # Note: We don't send Flush here - that should only be sent when the # LLM finishes a complete response via flush_audio() @@ -383,7 +367,7 @@ class DeepgramHttpTTSService(TTSService): """ Settings = DeepgramTTSSettings - _settings: DeepgramTTSSettings + _settings: Settings def __init__( self, @@ -394,7 +378,7 @@ class DeepgramHttpTTSService(TTSService): base_url: str = "https://api.deepgram.com", sample_rate: Optional[int] = None, encoding: str = "linear16", - settings: Optional[DeepgramTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Deepgram TTS service. @@ -404,7 +388,7 @@ class DeepgramHttpTTSService(TTSService): voice: Voice model to use for synthesis. .. deprecated:: 0.0.105 - Use ``settings=DeepgramTTSSettings(voice=...)`` instead. + Use ``settings=DeepgramHttpTTSService.Settings(voice=...)`` instead. aiohttp_session: Shared aiohttp session for HTTP requests with connection pooling. base_url: Custom base URL for Deepgram API. Defaults to "https://api.deepgram.com". @@ -415,7 +399,7 @@ class DeepgramHttpTTSService(TTSService): **kwargs: Additional arguments passed to parent TTSService class. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = DeepgramTTSSettings( + default_settings = self.Settings( model=None, voice="aura-2-helena-en", language=None, @@ -423,7 +407,7 @@ class DeepgramHttpTTSService(TTSService): # 2. Apply direct init arg overrides (deprecated) if voice is not None: - _warn_deprecated_param("voice", DeepgramTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice", "voice") default_settings.model = voice default_settings.voice = voice diff --git a/src/pipecat/services/deepseek/llm.py b/src/pipecat/services/deepseek/llm.py index 8fc4b5db8..cfb69cb9a 100644 --- a/src/pipecat/services/deepseek/llm.py +++ b/src/pipecat/services/deepseek/llm.py @@ -12,13 +12,12 @@ from typing import Optional from loguru import logger from pipecat.adapters.services.open_ai_adapter import OpenAILLMInvocationParams -from pipecat.services.openai.base_llm import OpenAILLMSettings +from pipecat.services.openai.base_llm import BaseOpenAILLMService from pipecat.services.openai.llm import OpenAILLMService -from pipecat.services.settings import _warn_deprecated_param @dataclass -class DeepSeekLLMSettings(OpenAILLMSettings): +class DeepSeekLLMSettings(BaseOpenAILLMService.Settings): """Settings for DeepSeekLLMService.""" pass @@ -32,7 +31,7 @@ class DeepSeekLLMService(OpenAILLMService): """ Settings = DeepSeekLLMSettings - _settings: DeepSeekLLMSettings + _settings: Settings def __init__( self, @@ -40,7 +39,7 @@ class DeepSeekLLMService(OpenAILLMService): api_key: str, base_url: str = "https://api.deepseek.com/v1", model: Optional[str] = None, - settings: Optional[DeepSeekLLMSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the DeepSeek LLM service. @@ -51,18 +50,18 @@ class DeepSeekLLMService(OpenAILLMService): model: The model identifier to use. Defaults to "deepseek-chat". .. deprecated:: 0.0.105 - Use ``settings=OpenAILLMSettings(model=...)`` instead. + Use ``settings=DeepSeekLLMService.Settings(model=...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional keyword arguments passed to OpenAILLMService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = DeepSeekLLMSettings(model="deepseek-chat") + default_settings = self.Settings(model="deepseek-chat") # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", DeepSeekLLMSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. (No step 3, as there's no params object to apply) diff --git a/src/pipecat/services/elevenlabs/stt.py b/src/pipecat/services/elevenlabs/stt.py index 76e854ade..aa7fd0659 100644 --- a/src/pipecat/services/elevenlabs/stt.py +++ b/src/pipecat/services/elevenlabs/stt.py @@ -35,7 +35,7 @@ from pipecat.frames.frames import ( VADUserStoppedSpeakingFrame, ) from pipecat.processors.frame_processor import FrameDirection -from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven from pipecat.services.stt_latency import ELEVENLABS_REALTIME_TTFS_P99, ELEVENLABS_TTFS_P99 from pipecat.services.stt_service import SegmentedSTTService, WebsocketSTTService from pipecat.transcriptions.language import Language, resolve_language @@ -217,13 +217,13 @@ class ElevenLabsSTTService(SegmentedSTTService): """ Settings = ElevenLabsSTTSettings - _settings: ElevenLabsSTTSettings + _settings: Settings class InputParams(BaseModel): """Configuration parameters for ElevenLabs STT API. .. deprecated:: 0.0.105 - Use ``settings=ElevenLabsSTTSettings(...)`` instead. + Use ``settings=ElevenLabsSTTService.Settings(...)`` instead. Parameters: language: Target language for transcription. @@ -242,7 +242,7 @@ class ElevenLabsSTTService(SegmentedSTTService): model: Optional[str] = None, sample_rate: Optional[int] = None, params: Optional[InputParams] = None, - settings: Optional[ElevenLabsSTTSettings] = None, + settings: Optional[Settings] = None, ttfs_p99_latency: Optional[float] = ELEVENLABS_TTFS_P99, **kwargs, ): @@ -255,13 +255,13 @@ class ElevenLabsSTTService(SegmentedSTTService): model: Model ID for transcription. .. deprecated:: 0.0.105 - Use ``settings=ElevenLabsSTTSettings(model=...)`` instead. + Use ``settings=ElevenLabsSTTService.Settings(model=...)`` instead. sample_rate: Audio sample rate in Hz. If not provided, uses the pipeline's rate. params: Configuration parameters for the STT service. .. deprecated:: 0.0.105 - Use ``settings=ElevenLabsSTTSettings(...)`` instead. + Use ``settings=ElevenLabsSTTService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -270,23 +270,23 @@ class ElevenLabsSTTService(SegmentedSTTService): **kwargs: Additional arguments passed to SegmentedSTTService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = ElevenLabsSTTSettings( + default_settings = self.Settings( model="scribe_v2", - language=language_to_elevenlabs_language(Language.EN), + language=Language.EN, tag_audio_events=None, ) # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", ElevenLabsSTTSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", ElevenLabsSTTSettings) + self._warn_init_param_moved_to_settings("params") if not settings: if params.language is not None: - default_settings.language = language_to_elevenlabs_language(params.language) + default_settings.language = params.language default_settings.tag_audio_events = params.tag_audio_events # 4. Apply settings delta (canonical API, always wins) @@ -450,13 +450,13 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService): """ Settings = ElevenLabsRealtimeSTTSettings - _settings: ElevenLabsRealtimeSTTSettings + _settings: Settings class InputParams(BaseModel): """Configuration parameters for ElevenLabs Realtime STT API. .. deprecated:: 0.0.105 - Use ``settings=ElevenLabsRealtimeSTTSettings(...)`` instead. + Use ``settings=ElevenLabsRealtimeSTTService.Settings(...)`` instead. Parameters: language_code: ISO-639-1 or ISO-639-3 language code. Leave None for auto-detection. @@ -496,7 +496,7 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService): enable_logging: bool = False, include_language_detection: bool = False, params: Optional[InputParams] = None, - settings: Optional[ElevenLabsRealtimeSTTSettings] = None, + settings: Optional[Settings] = None, ttfs_p99_latency: Optional[float] = ELEVENLABS_REALTIME_TTFS_P99, **kwargs, ): @@ -511,7 +511,7 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService): model: Model ID for transcription. .. deprecated:: 0.0.105 - Use ``settings=ElevenLabsRealtimeSTTSettings(model=...)`` instead. + Use ``settings=ElevenLabsRealtimeSTTService.Settings(model=...)`` instead. sample_rate: Audio sample rate in Hz. If not provided, uses the pipeline's rate. include_timestamps: Whether to include word-level timestamps in transcripts. @@ -520,7 +520,7 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService): params: Configuration parameters for the STT service. .. deprecated:: 0.0.105 - Use ``settings=ElevenLabsRealtimeSTTSettings(...)`` instead. + Use ``settings=ElevenLabsRealtimeSTTService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -529,7 +529,7 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService): **kwargs: Additional arguments passed to WebsocketSTTService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = ElevenLabsRealtimeSTTSettings( + default_settings = self.Settings( model="scribe_v2_realtime", language=None, vad_silence_threshold_secs=None, @@ -540,12 +540,12 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService): # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", ElevenLabsRealtimeSTTSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", ElevenLabsRealtimeSTTSettings) + self._warn_init_param_moved_to_settings("params") if not settings: default_settings.language = params.language_code if params.commit_strategy != CommitStrategy.MANUAL: @@ -597,7 +597,7 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService): """Apply a settings delta and reconnect if anything changed. Args: - delta: A :class:`STTSettings` (or ``ElevenLabsRealtimeSTTSettings``) delta. + delta: A :class:`STTSettings` (or ``ElevenLabsRealtimeSTTService.Settings``) delta. Returns: Dict mapping changed field names to their previous values. diff --git a/src/pipecat/services/elevenlabs/tts.py b/src/pipecat/services/elevenlabs/tts.py index 01f3cd516..866d0405f 100644 --- a/src/pipecat/services/elevenlabs/tts.py +++ b/src/pipecat/services/elevenlabs/tts.py @@ -44,7 +44,7 @@ from pipecat.frames.frames import ( TTSStoppedFrame, ) from pipecat.processors.frame_processor import FrameDirection -from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven from pipecat.services.tts_service import ( TextAggregationMode, TTSService, @@ -317,13 +317,13 @@ class ElevenLabsTTSService(WebsocketTTSService): """ Settings = ElevenLabsTTSSettings - _settings: ElevenLabsTTSSettings + _settings: Settings class InputParams(BaseModel): """Input parameters for ElevenLabs TTS configuration. .. deprecated:: 0.0.105 - Use ``settings=ElevenLabsTTSSettings(...)`` instead. + Use ``settings=ElevenLabsTTSService.Settings(...)`` instead. Parameters: language: Language to use for synthesis. @@ -364,7 +364,7 @@ class ElevenLabsTTSService(WebsocketTTSService): enable_logging: Optional[bool] = None, pronunciation_dictionary_locators: Optional[List[PronunciationDictionaryLocator]] = None, params: Optional[InputParams] = None, - settings: Optional[ElevenLabsTTSSettings] = None, + settings: Optional[Settings] = None, text_aggregation_mode: Optional[TextAggregationMode] = None, aggregate_sentences: Optional[bool] = None, **kwargs, @@ -376,12 +376,12 @@ class ElevenLabsTTSService(WebsocketTTSService): voice_id: ID of the voice to use for synthesis. .. deprecated:: 0.0.105 - Use ``settings=ElevenLabsTTSSettings(voice=...)`` instead. + Use ``settings=ElevenLabsTTSService.Settings(voice=...)`` instead. model: TTS model to use (e.g., "eleven_turbo_v2_5"). .. deprecated:: 0.0.105 - Use ``settings=ElevenLabsTTSSettings(model=...)`` instead. + Use ``settings=ElevenLabsTTSService.Settings(model=...)`` instead. url: WebSocket URL for ElevenLabs TTS API. sample_rate: Audio sample rate. If None, uses default. @@ -393,7 +393,7 @@ class ElevenLabsTTSService(WebsocketTTSService): params: Additional input parameters for voice customization. .. deprecated:: 0.0.105 - Use ``settings=ElevenLabsTTSSettings(...)`` instead. + Use ``settings=ElevenLabsTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -423,7 +423,7 @@ class ElevenLabsTTSService(WebsocketTTSService): # after a short period not receiving any audio. # 1. Initialize default_settings with hardcoded defaults - default_settings = ElevenLabsTTSSettings( + default_settings = self.Settings( model="eleven_turbo_v2_5", voice=None, language=None, @@ -437,19 +437,19 @@ class ElevenLabsTTSService(WebsocketTTSService): # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: - _warn_deprecated_param("voice_id", ElevenLabsTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id if model is not None: - _warn_deprecated_param("model", ElevenLabsTTSSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. Apply params overrides — only if settings not provided _pronunciation_dictionary_locators = pronunciation_dictionary_locators if params is not None: - _warn_deprecated_param("params", ElevenLabsTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: if params.language is not None: - default_settings.language = self.language_to_service_language(params.language) + default_settings.language = params.language if params.stability is not None: default_settings.stability = params.stability if params.similarity_boost is not None: @@ -533,11 +533,11 @@ class ElevenLabsTTSService(WebsocketTTSService): """Apply a settings delta, reconnecting as needed. Uses the declarative ``URL_FIELDS`` and ``VOICE_SETTINGS_FIELDS`` - sets on :class:`ElevenLabsTTSSettings` to decide whether to + sets on :class:`ElevenLabsTTSService.Settings` to decide whether to reconnect the WebSocket or close the current audio context. Args: - delta: A :class:`TTSSettings` (or ``ElevenLabsTTSSettings``) delta. + delta: A :class:`TTSSettings` (or ``ElevenLabsTTSService.Settings``) delta. Returns: Dict mapping changed field names to their previous values. @@ -550,19 +550,19 @@ class ElevenLabsTTSService(WebsocketTTSService): # Rebuild voice settings for next context self._voice_settings = self._set_voice_settings() - url_changed = bool(changed.keys() & ElevenLabsTTSSettings.URL_FIELDS) - voice_settings_changed = bool(changed.keys() & ElevenLabsTTSSettings.VOICE_SETTINGS_FIELDS) + url_changed = bool(changed.keys() & self.Settings.URL_FIELDS) + voice_settings_changed = bool(changed.keys() & self.Settings.VOICE_SETTINGS_FIELDS) if url_changed: logger.debug( - f"URL-level setting changed ({changed.keys() & ElevenLabsTTSSettings.URL_FIELDS}), " + f"URL-level setting changed ({changed.keys() & self.Settings.URL_FIELDS}), " f"reconnecting WebSocket" ) await self._disconnect() await self._connect() elif voice_settings_changed: logger.debug( - f"Voice settings changed ({changed.keys() & ElevenLabsTTSSettings.VOICE_SETTINGS_FIELDS}), " + f"Voice settings changed ({changed.keys() & self.Settings.VOICE_SETTINGS_FIELDS}), " f"closing current context to apply changes" ) audio_contexts = self.get_audio_contexts() @@ -573,7 +573,7 @@ class ElevenLabsTTSService(WebsocketTTSService): if not url_changed: # Reconnect applies all settings; only warn about fields not handled # by voice settings or URL changes. - handled = ElevenLabsTTSSettings.URL_FIELDS | ElevenLabsTTSSettings.VOICE_SETTINGS_FIELDS + handled = self.Settings.URL_FIELDS | self.Settings.VOICE_SETTINGS_FIELDS self._warn_unhandled_updated_settings(changed.keys() - handled) return changed @@ -906,13 +906,13 @@ class ElevenLabsHttpTTSService(TTSService): """ Settings = ElevenLabsHttpTTSSettings - _settings: ElevenLabsHttpTTSSettings + _settings: Settings class InputParams(BaseModel): """Input parameters for ElevenLabs HTTP TTS configuration. .. deprecated:: 0.0.105 - Use ``settings=ElevenLabsHttpTTSSettings(...)`` instead. + Use ``settings=ElevenLabsHttpTTSService.Settings(...)`` instead. Parameters: language: Language to use for synthesis. @@ -947,7 +947,7 @@ class ElevenLabsHttpTTSService(TTSService): sample_rate: Optional[int] = None, pronunciation_dictionary_locators: Optional[List[PronunciationDictionaryLocator]] = None, params: Optional[InputParams] = None, - settings: Optional[ElevenLabsHttpTTSSettings] = None, + settings: Optional[Settings] = None, text_aggregation_mode: Optional[TextAggregationMode] = None, aggregate_sentences: Optional[bool] = None, **kwargs, @@ -959,13 +959,13 @@ class ElevenLabsHttpTTSService(TTSService): voice_id: ID of the voice to use for synthesis. .. deprecated:: 0.0.105 - Use ``settings=ElevenLabsHttpTTSSettings(voice=...)`` instead. + Use ``settings=ElevenLabsHttpTTSService.Settings(voice=...)`` instead. aiohttp_session: aiohttp ClientSession for HTTP requests. model: TTS model to use (e.g., "eleven_turbo_v2_5"). .. deprecated:: 0.0.105 - Use ``settings=ElevenLabsHttpTTSSettings(model=...)`` instead. + Use ``settings=ElevenLabsHttpTTSService.Settings(model=...)`` instead. base_url: Base URL for ElevenLabs HTTP API. sample_rate: Audio sample rate. If None, uses default. @@ -974,7 +974,7 @@ class ElevenLabsHttpTTSService(TTSService): params: Additional input parameters for voice customization. .. deprecated:: 0.0.105 - Use ``settings=ElevenLabsHttpTTSSettings(...)`` instead. + Use ``settings=ElevenLabsHttpTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -987,7 +987,7 @@ class ElevenLabsHttpTTSService(TTSService): **kwargs: Additional arguments passed to the parent service. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = ElevenLabsHttpTTSSettings( + default_settings = self.Settings( model="eleven_turbo_v2_5", voice=None, language=None, @@ -1002,19 +1002,19 @@ class ElevenLabsHttpTTSService(TTSService): # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: - _warn_deprecated_param("voice_id", ElevenLabsHttpTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id if model is not None: - _warn_deprecated_param("model", ElevenLabsHttpTTSSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. Apply params overrides — only if settings not provided _pronunciation_dictionary_locators = pronunciation_dictionary_locators if params is not None: - _warn_deprecated_param("params", ElevenLabsHttpTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: if params.language is not None: - default_settings.language = self.language_to_service_language(params.language) + default_settings.language = params.language if params.optimize_streaming_latency is not None: default_settings.optimize_streaming_latency = params.optimize_streaming_latency if params.stability is not None: @@ -1091,7 +1091,7 @@ class ElevenLabsHttpTTSService(TTSService): """Apply a settings delta and rebuild voice settings. Args: - delta: A :class:`TTSSettings` (or ``ElevenLabsHttpTTSSettings``) delta. + delta: A :class:`TTSSettings` (or ``ElevenLabsHttpTTSService.Settings``) delta. Returns: Dict mapping changed field names to their previous values. diff --git a/src/pipecat/services/fal/image.py b/src/pipecat/services/fal/image.py index cb4d646ba..31af55440 100644 --- a/src/pipecat/services/fal/image.py +++ b/src/pipecat/services/fal/image.py @@ -23,7 +23,7 @@ from pydantic import BaseModel from pipecat.frames.frames import ErrorFrame, Frame, URLImageRawFrame from pipecat.services.image_service import ImageGenService -from pipecat.services.settings import NOT_GIVEN, ImageGenSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, ImageGenSettings, _NotGiven @dataclass @@ -71,13 +71,13 @@ class FalImageGenService(ImageGenService): """ Settings = FalImageGenSettings - _settings: FalImageGenSettings + _settings: Settings class InputParams(BaseModel): """Input parameters for Fal.ai image generation. .. deprecated:: 0.0.105 - Use ``settings=FalImageGenSettings(...)`` instead. + Use ``settings=FalImageGenService.Settings(...)`` instead. Parameters: seed: Random seed for reproducible generation. If None, uses random seed. @@ -97,7 +97,7 @@ class FalImageGenService(ImageGenService): enable_safety_checker: bool = True format: str = "png" - _settings: FalImageGenSettings + _settings: Settings def __init__( self, @@ -106,7 +106,7 @@ class FalImageGenService(ImageGenService): aiohttp_session: aiohttp.ClientSession, model: Optional[str] = None, key: Optional[str] = None, - settings: Optional[FalImageGenSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the FalImageGenService. @@ -115,13 +115,13 @@ class FalImageGenService(ImageGenService): params: Input parameters for image generation configuration. .. deprecated:: 0.0.105 - Use ``settings=FalImageGenSettings(...)`` instead. + Use ``settings=FalImageGenService.Settings(...)`` instead. aiohttp_session: HTTP client session for downloading generated images. model: The Fal.ai model to use for generation. Defaults to "fal-ai/fast-sdxl". .. deprecated:: 0.0.105 - Use ``settings=FalImageGenSettings(model=...)`` instead. + Use ``settings=FalImageGenService.Settings(model=...)`` instead. key: Optional API key for Fal.ai. If provided, sets FAL_KEY environment variable. settings: Runtime-updatable settings. When provided alongside deprecated @@ -129,7 +129,7 @@ class FalImageGenService(ImageGenService): **kwargs: Additional arguments passed to parent ImageGenService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = FalImageGenSettings( + default_settings = self.Settings( model="fal-ai/fast-sdxl", seed=None, num_inference_steps=8, @@ -142,11 +142,11 @@ class FalImageGenService(ImageGenService): # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", FalImageGenSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model if params is not None: - _warn_deprecated_param("params", FalImageGenSettings) + self._warn_init_param_moved_to_settings("params") if not settings: default_settings.seed = params.seed default_settings.num_inference_steps = params.num_inference_steps diff --git a/src/pipecat/services/fal/stt.py b/src/pipecat/services/fal/stt.py index 692878bf6..65df7e3ab 100644 --- a/src/pipecat/services/fal/stt.py +++ b/src/pipecat/services/fal/stt.py @@ -20,7 +20,7 @@ from loguru import logger from pydantic import BaseModel from pipecat.frames.frames import ErrorFrame, Frame, TranscriptionFrame -from pipecat.services.settings import STTSettings, _warn_deprecated_param +from pipecat.services.settings import STTSettings from pipecat.services.stt_latency import FAL_TTFS_P99 from pipecat.services.stt_service import SegmentedSTTService from pipecat.transcriptions.language import Language, resolve_language @@ -156,13 +156,13 @@ class FalSTTService(SegmentedSTTService): """ Settings = FalSTTSettings - _settings: FalSTTSettings + _settings: Settings class InputParams(BaseModel): """Configuration parameters for Fal's Wizper API. .. deprecated:: 0.0.105 - Use ``settings=FalSTTSettings(...)`` instead. + Use ``settings=FalSTTService.Settings(...)`` instead. Parameters: language: Language of the audio input. Defaults to English. @@ -186,7 +186,7 @@ class FalSTTService(SegmentedSTTService): version: str = "3", sample_rate: Optional[int] = None, params: Optional[InputParams] = None, - settings: Optional[FalSTTSettings] = None, + settings: Optional[Settings] = None, ttfs_p99_latency: Optional[float] = FAL_TTFS_P99, **kwargs, ): @@ -204,7 +204,7 @@ class FalSTTService(SegmentedSTTService): params: Configuration parameters for the Wizper API. .. deprecated:: 0.0.105 - Use ``settings=FalSTTSettings(...)`` for model/language and + Use ``settings=FalSTTService.Settings(...)`` for model/language and direct init parameters for task/chunk_level/version instead. settings: Runtime-updatable settings. When provided alongside deprecated @@ -214,19 +214,19 @@ class FalSTTService(SegmentedSTTService): **kwargs: Additional arguments passed to SegmentedSTTService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = FalSTTSettings( + default_settings = self.Settings( model=None, - language=language_to_fal_language(Language.EN), + language=Language.EN, ) # 2. (no deprecated direct args for this service) # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", FalSTTSettings) + self._warn_init_param_moved_to_settings("params") if not settings: if params.language is not None: - default_settings.language = language_to_fal_language(params.language) + default_settings.language = params.language if params.task != "transcribe": task = params.task if params.chunk_level != "segment": diff --git a/src/pipecat/services/fireworks/llm.py b/src/pipecat/services/fireworks/llm.py index 118090be9..5efa60793 100644 --- a/src/pipecat/services/fireworks/llm.py +++ b/src/pipecat/services/fireworks/llm.py @@ -12,13 +12,12 @@ from typing import Optional from loguru import logger from pipecat.adapters.services.open_ai_adapter import OpenAILLMInvocationParams -from pipecat.services.openai.base_llm import OpenAILLMSettings +from pipecat.services.openai.base_llm import BaseOpenAILLMService from pipecat.services.openai.llm import OpenAILLMService -from pipecat.services.settings import _warn_deprecated_param @dataclass -class FireworksLLMSettings(OpenAILLMSettings): +class FireworksLLMSettings(BaseOpenAILLMService.Settings): """Settings for FireworksLLMService.""" pass @@ -32,7 +31,7 @@ class FireworksLLMService(OpenAILLMService): """ Settings = FireworksLLMSettings - _settings: FireworksLLMSettings + _settings: Settings def __init__( self, @@ -40,7 +39,7 @@ class FireworksLLMService(OpenAILLMService): api_key: str, model: Optional[str] = None, base_url: str = "https://api.fireworks.ai/inference/v1", - settings: Optional[FireworksLLMSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Fireworks LLM service. @@ -50,7 +49,7 @@ class FireworksLLMService(OpenAILLMService): model: The model identifier to use. Defaults to "accounts/fireworks/models/firefunction-v2". .. deprecated:: 0.0.105 - Use ``settings=OpenAILLMSettings(model=...)`` instead. + Use ``settings=FireworksLLMService.Settings(model=...)`` instead. base_url: The base URL for Fireworks API. Defaults to "https://api.fireworks.ai/inference/v1". settings: Runtime-updatable settings. When provided alongside deprecated @@ -58,11 +57,11 @@ class FireworksLLMService(OpenAILLMService): **kwargs: Additional keyword arguments passed to OpenAILLMService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = FireworksLLMSettings(model="accounts/fireworks/models/firefunction-v2") + default_settings = self.Settings(model="accounts/fireworks/models/firefunction-v2") # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", FireworksLLMSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. (No step 3, as there's no params object to apply) @@ -119,6 +118,10 @@ class FireworksLLMService(OpenAILLMService): # Prepend system instruction if set if self._settings.system_instruction: messages = params.get("messages", []) + if messages and messages[0].get("role") == "system": + logger.warning( + f"{self}: Both system_instruction and an initial system message in context are set. This may be unintended." + ) params["messages"] = [ {"role": "system", "content": self._settings.system_instruction} ] + messages diff --git a/src/pipecat/services/fish/tts.py b/src/pipecat/services/fish/tts.py index 4974dfa37..92cb54701 100644 --- a/src/pipecat/services/fish/tts.py +++ b/src/pipecat/services/fish/tts.py @@ -27,7 +27,7 @@ from pipecat.frames.frames import ( TTSStoppedFrame, ) from pipecat.processors.frame_processor import FrameDirection -from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven from pipecat.services.tts_service import InterruptibleTTSService from pipecat.transcriptions.language import Language from pipecat.utils.tracing.service_decorators import traced_tts @@ -85,13 +85,13 @@ class FishAudioTTSService(InterruptibleTTSService): """ Settings = FishAudioTTSSettings - _settings: FishAudioTTSSettings + _settings: Settings class InputParams(BaseModel): """Input parameters for Fish Audio TTS configuration. .. deprecated:: 0.0.105 - Use ``settings=FishAudioTTSSettings(...)`` instead. + Use ``settings=FishAudioTTSService.Settings(...)`` instead. Parameters: language: Language for synthesis. Defaults to English. @@ -117,7 +117,7 @@ class FishAudioTTSService(InterruptibleTTSService): output_format: FishAudioOutputFormat = "pcm", sample_rate: Optional[int] = None, params: Optional[InputParams] = None, - settings: Optional[FishAudioTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Fish Audio TTS service. @@ -127,7 +127,7 @@ class FishAudioTTSService(InterruptibleTTSService): reference_id: Reference ID of the voice model to use for synthesis. .. deprecated:: 0.0.105 - Use ``settings=FishAudioTTSSettings(voice=...)`` instead. + Use ``settings=FishAudioTTSService.Settings(voice=...)`` instead. model: Deprecated. Reference ID of the voice model to use for synthesis. @@ -138,14 +138,14 @@ class FishAudioTTSService(InterruptibleTTSService): model_id: Specify which Fish Audio TTS model to use (e.g. "s1"). .. deprecated:: 0.0.105 - Use ``settings=FishAudioTTSSettings(model=...)`` instead. + Use ``settings=FishAudioTTSService.Settings(model=...)`` instead. output_format: Audio output format. Defaults to "pcm". sample_rate: Audio sample rate. If None, uses default. params: Additional input parameters for voice customization. .. deprecated:: 0.0.105 - Use ``settings=FishAudioTTSSettings(...)`` instead. + Use ``settings=FishAudioTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -171,8 +171,8 @@ class FishAudioTTSService(InterruptibleTTSService): reference_id = model # 1. Initialize default_settings with hardcoded defaults - default_settings = FishAudioTTSSettings( - model="s1", + default_settings = self.Settings( + model="s2-pro", voice=None, language=None, latency="balanced", @@ -185,15 +185,15 @@ class FishAudioTTSService(InterruptibleTTSService): # 2. Apply direct init arg overrides (deprecated) if reference_id is not None: - _warn_deprecated_param("reference_id", FishAudioTTSSettings, "voice") + self._warn_init_param_moved_to_settings("reference_id", "voice") default_settings.voice = reference_id if model_id is not None: - _warn_deprecated_param("model_id", FishAudioTTSSettings, "model") + self._warn_init_param_moved_to_settings("model_id", "model") default_settings.model = model_id # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", FishAudioTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: if params.latency is not None: default_settings.latency = params.latency @@ -240,7 +240,7 @@ class FishAudioTTSService(InterruptibleTTSService): Any change to voice or model triggers a WebSocket reconnect. Args: - delta: A :class:`TTSSettings` (or ``FishAudioTTSSettings``) delta. + delta: A :class:`TTSSettings` (or ``FishAudioTTSService.Settings``) delta. Returns: Dict mapping changed field names to their previous values. diff --git a/src/pipecat/services/gladia/config.py b/src/pipecat/services/gladia/config.py index ec8997d7c..c492a04a1 100644 --- a/src/pipecat/services/gladia/config.py +++ b/src/pipecat/services/gladia/config.py @@ -153,7 +153,7 @@ class GladiaInputParams(BaseModel): """Configuration parameters for the Gladia STT service. .. deprecated:: 0.0.105 - Use ``settings=GladiaSTTSettings(...)`` for runtime-updatable + Use ``settings=GladiaSTTService.Settings(...)`` for runtime-updatable fields and direct init parameters for encoding/bit_depth/channels. Parameters: diff --git a/src/pipecat/services/gladia/stt.py b/src/pipecat/services/gladia/stt.py index fc09d94fc..2ce2a15b5 100644 --- a/src/pipecat/services/gladia/stt.py +++ b/src/pipecat/services/gladia/stt.py @@ -39,7 +39,7 @@ from pipecat.services.gladia.config import ( PreProcessingConfig, RealtimeProcessingConfig, ) -from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven from pipecat.services.stt_latency import GLADIA_TTFS_P99 from pipecat.services.stt_service import WebsocketSTTService from pipecat.transcriptions.language import Language, resolve_language @@ -231,7 +231,7 @@ class GladiaSTTService(WebsocketSTTService): """ Settings = GladiaSTTSettings - _settings: GladiaSTTSettings + _settings: Settings # Maintain backward compatibility InputParams = _InputParamsDescriptor() @@ -251,7 +251,7 @@ class GladiaSTTService(WebsocketSTTService): params: Optional[GladiaInputParams] = None, max_buffer_size: int = 1024 * 1024 * 20, # 20MB default buffer should_interrupt: bool = True, - settings: Optional[GladiaSTTSettings] = None, + settings: Optional[Settings] = None, ttfs_p99_latency: Optional[float] = GLADIA_TTFS_P99, **kwargs, ): @@ -274,12 +274,12 @@ class GladiaSTTService(WebsocketSTTService): model: Model to use for transcription. .. deprecated:: 0.0.105 - Use ``settings=GladiaSTTSettings(model=...)`` instead. + Use ``settings=GladiaSTTService.Settings(model=...)`` instead. params: Additional configuration parameters for Gladia service. .. deprecated:: 0.0.105 - Use ``settings=GladiaSTTSettings(...)`` for runtime-updatable + Use ``settings=GladiaSTTService.Settings(...)`` for runtime-updatable fields and direct init parameters for encoding/bit_depth/channels. max_buffer_size: Maximum size of audio buffer in bytes. Defaults to 20MB. @@ -302,7 +302,7 @@ class GladiaSTTService(WebsocketSTTService): ) # 1. Initialize default_settings with hardcoded defaults - default_settings = GladiaSTTSettings( + default_settings = self.Settings( model="solaria-1", language=None, language_config=None, @@ -317,12 +317,12 @@ class GladiaSTTService(WebsocketSTTService): # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", GladiaSTTSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", GladiaSTTSettings) + self._warn_init_param_moved_to_settings("params") if params.language is not None: with warnings.catch_warnings(): warnings.simplefilter("always") @@ -469,7 +469,7 @@ class GladiaSTTService(WebsocketSTTService): await super().start(frame) await self._connect() - async def _update_settings(self, delta: GladiaSTTSettings) -> dict[str, Any]: + async def _update_settings(self, delta: Settings) -> dict[str, Any]: """Apply settings delta. Settings are stored but not applied to the active session. diff --git a/src/pipecat/services/google/__init__.py b/src/pipecat/services/google/__init__.py index 032cf0eb8..32b12e367 100644 --- a/src/pipecat/services/google/__init__.py +++ b/src/pipecat/services/google/__init__.py @@ -12,12 +12,12 @@ from .frames import * from .gemini_live import * from .image import * from .llm import * -from .llm_openai import * -from .llm_vertex import * +from .openai import * from .rtvi import * from .stt import * from .tts import * +from .vertex import * sys.modules[__name__] = DeprecatedModuleProxy( - globals(), "google", "google.[frames,image,llm,llm_openai,llm_vertex,rtvi,stt,tts]" + globals(), "google", "google.[frames,image,llm,openai,vertex,rtvi,stt,tts]" ) diff --git a/src/pipecat/services/google/gemini_live/__init__.py b/src/pipecat/services/google/gemini_live/__init__.py index f4bfbb5c8..4afeb99ce 100644 --- a/src/pipecat/services/google/gemini_live/__init__.py +++ b/src/pipecat/services/google/gemini_live/__init__.py @@ -1,6 +1,6 @@ from .file_api import GeminiFileAPI from .llm import GeminiLiveLLMService -from .llm_vertex import GeminiLiveVertexLLMService +from .vertex.llm import GeminiLiveVertexLLMService __all__ = [ "GeminiFileAPI", diff --git a/src/pipecat/services/google/gemini_live/llm.py b/src/pipecat/services/google/gemini_live/llm.py index 07f185f99..5c9e5f8b3 100644 --- a/src/pipecat/services/google/gemini_live/llm.py +++ b/src/pipecat/services/google/gemini_live/llm.py @@ -76,7 +76,7 @@ from pipecat.services.openai.llm import ( OpenAIAssistantContextAggregator, OpenAIUserContextAggregator, ) -from pipecat.services.settings import NOT_GIVEN, LLMSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, LLMSettings, _NotGiven from pipecat.transcriptions.language import Language, resolve_language from pipecat.utils.string import match_endofsentence from pipecat.utils.time import time_now_iso8601 @@ -553,7 +553,7 @@ class InputParams(BaseModel): """Input parameters for Gemini Live generation. .. deprecated:: 0.0.105 - Use ``GeminiLiveLLMSettings`` instead. + Use ``GeminiLiveLLMService.Settings`` instead. Parameters: frequency_penalty: Frequency penalty for generation (0.0-2.0). Defaults to None. @@ -643,7 +643,7 @@ class GeminiLiveLLMService(LLMService): """ Settings = GeminiLiveLLMSettings - _settings: GeminiLiveLLMSettings + _settings: Settings # Overriding the default adapter to use the Gemini one. adapter_class = GeminiLLMAdapter @@ -660,7 +660,7 @@ class GeminiLiveLLMService(LLMService): system_instruction: Optional[str] = None, tools: Optional[Union[List[dict], ToolsSchema]] = None, params: Optional[InputParams] = None, - settings: Optional[GeminiLiveLLMSettings] = None, + settings: Optional[Settings] = None, inference_on_context_initialization: bool = True, file_api_base_url: str = "https://generativelanguage.googleapis.com/v1beta/files", http_options: Optional[HttpOptions] = None, @@ -680,12 +680,12 @@ class GeminiLiveLLMService(LLMService): model: Model identifier to use. .. deprecated:: 0.0.105 - Use ``settings=GeminiLiveLLMSettings(model=...)`` instead. + Use ``settings=GeminiLiveLLMService.Settings(model=...)`` instead. voice_id: TTS voice identifier. Defaults to "Charon". .. deprecated:: 0.0.105 - Use ``settings=GeminiLiveLLMSettings(voice=...)`` instead. + Use ``settings=GeminiLiveLLMService.Settings(voice=...)`` instead. start_audio_paused: Whether to start with audio input paused. Defaults to False. start_video_paused: Whether to start with video input paused. Defaults to False. system_instruction: System prompt for the model. Defaults to None. @@ -693,7 +693,7 @@ class GeminiLiveLLMService(LLMService): params: Configuration parameters for the model. .. deprecated:: 0.0.105 - Use ``settings=GeminiLiveLLMSettings(...)`` instead. + Use ``settings=GeminiLiveLLMService.Settings(...)`` instead. settings: Gemini Live LLM settings. If provided together with deprecated top-level parameters, the ``settings`` values take precedence. @@ -716,7 +716,7 @@ class GeminiLiveLLMService(LLMService): ) # 1. Initialize default_settings with hardcoded defaults - default_settings = GeminiLiveLLMSettings( + default_settings = self.Settings( model="models/gemini-2.5-flash-native-audio-preview-12-2025", system_instruction=system_instruction, voice="Charon", @@ -742,15 +742,15 @@ class GeminiLiveLLMService(LLMService): # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", GeminiLiveLLMSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model if voice_id != "Charon": - _warn_deprecated_param("voice_id", GeminiLiveLLMSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", GeminiLiveLLMSettings) + self._warn_init_param_moved_to_settings("params") if not settings: default_settings.frequency_penalty = params.frequency_penalty default_settings.max_tokens = params.max_tokens diff --git a/src/pipecat/services/google/gemini_live/llm_vertex.py b/src/pipecat/services/google/gemini_live/llm_vertex.py index cdfde1660..038d72e57 100644 --- a/src/pipecat/services/google/gemini_live/llm_vertex.py +++ b/src/pipecat/services/google/gemini_live/llm_vertex.py @@ -4,277 +4,15 @@ # SPDX-License-Identifier: BSD 2-Clause License # -"""Service for accessing Gemini Live via Google Vertex AI. +"""Deprecated: use ``pipecat.services.google.gemini_live.vertex.llm`` instead.""" -This module provides integration with Google's Gemini Live model via -Vertex AI, supporting both text and audio modalities with voice transcription, -streaming responses, and tool usage. -""" +import warnings -import json -from dataclasses import dataclass -from typing import List, Optional, Union - -from loguru import logger - -from pipecat.adapters.schemas.tools_schema import ToolsSchema -from pipecat.services.google.gemini_live.llm import ( - GeminiLiveLLMService, - GeminiLiveLLMSettings, - GeminiMediaResolution, - GeminiModalities, - HttpOptions, - InputParams, - language_to_gemini_language, +warnings.warn( + "Module `pipecat.services.google.gemini_live.llm_vertex` is deprecated, " + "use `pipecat.services.google.gemini_live.vertex.llm` instead.", + DeprecationWarning, + stacklevel=2, ) -from pipecat.services.settings import _warn_deprecated_param -try: - from google.auth import default - from google.auth.exceptions import GoogleAuthError - from google.auth.transport.requests import Request - from google.genai import Client - from google.oauth2 import service_account - -except ModuleNotFoundError as e: - logger.error(f"Exception: {e}") - logger.error("In order to use Google Vertex AI, you need to `pip install pipecat-ai[google]`.") - raise Exception(f"Missing module: {e}") - - -@dataclass -class GeminiLiveVertexLLMSettings(GeminiLiveLLMSettings): - """Settings for GeminiLiveVertexLLMService.""" - - pass - - -class GeminiLiveVertexLLMService(GeminiLiveLLMService): - """Provides access to Google's Gemini Live model via Vertex AI. - - This service enables real-time conversations with Gemini, supporting both - text and audio modalities. It handles voice transcription, streaming audio - responses, and tool usage. - """ - - Settings = GeminiLiveVertexLLMSettings - _settings: GeminiLiveVertexLLMSettings - - def __init__( - self, - *, - credentials: Optional[str] = None, - credentials_path: Optional[str] = None, - location: str, - project_id: str, - model: Optional[str] = None, - voice_id: str = "Charon", - start_audio_paused: bool = False, - start_video_paused: bool = False, - system_instruction: Optional[str] = None, - tools: Optional[Union[List[dict], ToolsSchema]] = None, - params: Optional[InputParams] = None, - settings: Optional[GeminiLiveVertexLLMSettings] = None, - inference_on_context_initialization: bool = True, - file_api_base_url: str = "https://generativelanguage.googleapis.com/v1beta/files", - http_options: Optional[HttpOptions] = None, - **kwargs, - ): - """Initialize the service for accessing Gemini Live via Google Vertex AI. - - Args: - credentials: JSON string of service account credentials. - credentials_path: Path to the service account JSON file. - location: GCP region for Vertex AI endpoint (e.g., "us-east4"). - project_id: Google Cloud project ID. - model: Model identifier to use. - - .. deprecated:: 0.0.105 - Use ``settings=GeminiLiveLLMSettings(model=...)`` instead. - - voice_id: TTS voice identifier. Defaults to "Charon". - - .. deprecated:: 0.0.105 - Use ``settings=GeminiLiveVertexLLMSettings(voice=...)`` instead. - start_audio_paused: Whether to start with audio input paused. Defaults to False. - start_video_paused: Whether to start with video input paused. Defaults to False. - system_instruction: System prompt for the model. Defaults to None. - tools: Tools/functions available to the model. Defaults to None. - params: Configuration parameters for the model along with Vertex AI - location and project ID. - - .. deprecated:: 0.0.105 - Use ``settings=GeminiLiveLLMSettings(...)`` instead. - - settings: Gemini Live LLM settings. If provided together with deprecated - top-level parameters, the ``settings`` values take precedence. - inference_on_context_initialization: Whether to generate a response when context - is first set. Defaults to True. - file_api_base_url: Base URL for the Gemini File API. Defaults to the official endpoint. - http_options: HTTP options for the client. - **kwargs: Additional arguments passed to parent GeminiLiveLLMService. - """ - # Check if user incorrectly passed api_key, which is used by parent - # class but not here. - if "api_key" in kwargs: - logger.error( - "GeminiLiveVertexLLMService does not accept 'api_key' parameter. " - "Use 'credentials' or 'credentials_path' instead for Vertex AI authentication." - ) - raise ValueError( - "Invalid parameter 'api_key'. Use 'credentials' or 'credentials_path' for Vertex AI authentication." - ) - - # These need to be set before calling super().__init__() because - # super().__init__() invokes create_client(), which needs these. - self._credentials = self._get_credentials(credentials, credentials_path) - self._project_id = project_id - self._location = location - - # Build default_settings from deprecated args, then apply settings delta. - # We pass settings= to super() instead of model=/params= to avoid - # double deprecation warnings from the parent. - - # 1. Initialize default_settings with hardcoded defaults - default_settings = GeminiLiveVertexLLMSettings( - model="google/gemini-live-2.5-flash-native-audio", - voice="Charon", - frequency_penalty=None, - max_tokens=4096, - presence_penalty=None, - temperature=None, - top_k=None, - top_p=None, - seed=None, - filter_incomplete_user_turns=False, - user_turn_completion_config=None, - modalities=GeminiModalities.AUDIO, - language="en-US", - media_resolution=GeminiMediaResolution.UNSPECIFIED, - vad=None, - context_window_compression={}, - thinking={}, - enable_affective_dialog=False, - proactivity={}, - extra={}, - ) - - # 2. Apply direct init arg overrides (deprecated) - if model is not None: - _warn_deprecated_param("model", GeminiLiveVertexLLMSettings, "model") - default_settings.model = model - if voice_id != "Charon": - _warn_deprecated_param("voice_id", GeminiLiveVertexLLMSettings, "voice") - default_settings.voice = voice_id - - # 3. Apply params overrides — only if settings not provided - if params is not None: - _warn_deprecated_param("params", GeminiLiveVertexLLMSettings) - if not settings: - default_settings.frequency_penalty = params.frequency_penalty - default_settings.max_tokens = params.max_tokens - default_settings.presence_penalty = params.presence_penalty - default_settings.temperature = params.temperature - default_settings.top_k = params.top_k - default_settings.top_p = params.top_p - default_settings.modalities = params.modalities - default_settings.language = ( - language_to_gemini_language(params.language) if params.language else "en-US" - ) - default_settings.media_resolution = params.media_resolution - default_settings.vad = params.vad - default_settings.context_window_compression = ( - params.context_window_compression.model_dump() - if params.context_window_compression - else {} - ) - default_settings.thinking = params.thinking or {} - default_settings.enable_affective_dialog = params.enable_affective_dialog or False - default_settings.proactivity = params.proactivity or {} - if isinstance(params.extra, dict): - default_settings.extra = params.extra - - # 4. Apply settings delta (canonical API, always wins) - if settings is not None: - default_settings.apply_update(settings) - - # Call parent constructor with the obtained settings - super().__init__( - # api_key is required by parent class, but actually not used with - # Vertex - api_key="dummy", - start_audio_paused=start_audio_paused, - start_video_paused=start_video_paused, - system_instruction=system_instruction, - tools=tools, - settings=default_settings, - inference_on_context_initialization=inference_on_context_initialization, - file_api_base_url=file_api_base_url, - http_options=http_options, - **kwargs, - ) - - def create_client(self): - """Create the Gemini client instance.""" - self._client = Client( - vertexai=True, - credentials=self._credentials, - project=self._project_id, - location=self._location, - http_options=self._http_options, - ) - - @property - def file_api(self): - """Gemini File API is not supported with Vertex AI.""" - raise NotImplementedError( - "When using Vertex AI, the recommended approach is to use Google Cloud Storage for file handling. The Gemini File API is not directly supported in this context." - ) - - @staticmethod - def _get_credentials(credentials: Optional[str], credentials_path: Optional[str]) -> str: - """Retrieve Credentials using Google service account credentials JSON. - - Supports multiple authentication methods: - 1. Direct JSON credentials string - 2. Path to service account JSON file - 3. Default application credentials (ADC) - - Args: - credentials: JSON string of service account credentials. - credentials_path: Path to the service account JSON file. - - Returns: - OAuth token for API authentication. - - Raises: - ValueError: If no valid credentials are provided or found. - """ - creds: Optional[service_account.Credentials] = None - - if credentials: - # Parse and load credentials from JSON string - creds = service_account.Credentials.from_service_account_info( - json.loads(credentials), - scopes=["https://www.googleapis.com/auth/cloud-platform"], - ) - elif credentials_path: - # Load credentials from JSON file - creds = service_account.Credentials.from_service_account_file( - credentials_path, - scopes=["https://www.googleapis.com/auth/cloud-platform"], - ) - else: - try: - creds, project_id = default( - scopes=["https://www.googleapis.com/auth/cloud-platform"] - ) - except GoogleAuthError: - pass - - if not creds: - raise ValueError("No valid credentials provided.") - - creds.refresh(Request()) # Ensure token is up-to-date, lifetime is 1 hour. - - return creds +from pipecat.services.google.gemini_live.vertex.llm import * # noqa: E402, F401, F403 diff --git a/src/pipecat/services/google/gemini_live/vertex/__init__.py b/src/pipecat/services/google/gemini_live/vertex/__init__.py new file mode 100644 index 000000000..c4d243b97 --- /dev/null +++ b/src/pipecat/services/google/gemini_live/vertex/__init__.py @@ -0,0 +1,5 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# diff --git a/src/pipecat/services/google/gemini_live/vertex/llm.py b/src/pipecat/services/google/gemini_live/vertex/llm.py new file mode 100644 index 000000000..cb9d74c62 --- /dev/null +++ b/src/pipecat/services/google/gemini_live/vertex/llm.py @@ -0,0 +1,278 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""Service for accessing Gemini Live via Google Vertex AI. + +This module provides integration with Google's Gemini Live model via +Vertex AI, supporting both text and audio modalities with voice transcription, +streaming responses, and tool usage. +""" + +import json +from dataclasses import dataclass +from typing import List, Optional, Union + +from loguru import logger + +from pipecat.adapters.schemas.tools_schema import ToolsSchema +from pipecat.services.google.gemini_live.llm import ( + GeminiLiveLLMService, + GeminiMediaResolution, + GeminiModalities, + HttpOptions, + InputParams, + language_to_gemini_language, +) + +try: + from google.auth import default + from google.auth.exceptions import GoogleAuthError + from google.auth.transport.requests import Request + from google.genai import Client + from google.oauth2 import service_account + +except ModuleNotFoundError as e: + logger.error(f"Exception: {e}") + logger.error("In order to use Google Vertex AI, you need to `pip install pipecat-ai[google]`.") + raise Exception(f"Missing module: {e}") + + +@dataclass +class GeminiLiveVertexLLMSettings(GeminiLiveLLMService.Settings): + """Settings for GeminiLiveVertexLLMService.""" + + pass + + +class GeminiLiveVertexLLMService(GeminiLiveLLMService): + """Provides access to Google's Gemini Live model via Vertex AI. + + This service enables real-time conversations with Gemini, supporting both + text and audio modalities. It handles voice transcription, streaming audio + responses, and tool usage. + """ + + Settings = GeminiLiveVertexLLMSettings + _settings: Settings + + def __init__( + self, + *, + credentials: Optional[str] = None, + credentials_path: Optional[str] = None, + location: str, + project_id: str, + model: Optional[str] = None, + voice_id: str = "Charon", + start_audio_paused: bool = False, + start_video_paused: bool = False, + system_instruction: Optional[str] = None, + tools: Optional[Union[List[dict], ToolsSchema]] = None, + params: Optional[InputParams] = None, + settings: Optional[Settings] = None, + inference_on_context_initialization: bool = True, + file_api_base_url: str = "https://generativelanguage.googleapis.com/v1beta/files", + http_options: Optional[HttpOptions] = None, + **kwargs, + ): + """Initialize the service for accessing Gemini Live via Google Vertex AI. + + Args: + credentials: JSON string of service account credentials. + credentials_path: Path to the service account JSON file. + location: GCP region for Vertex AI endpoint (e.g., "us-east4"). + project_id: Google Cloud project ID. + model: Model identifier to use. + + .. deprecated:: 0.0.105 + Use ``settings=GeminiLiveVertexLLMService.Settings(model=...)`` instead. + + voice_id: TTS voice identifier. Defaults to "Charon". + + .. deprecated:: 0.0.105 + Use ``settings=GeminiLiveVertexLLMService.Settings(voice=...)`` instead. + start_audio_paused: Whether to start with audio input paused. Defaults to False. + start_video_paused: Whether to start with video input paused. Defaults to False. + system_instruction: System prompt for the model. Defaults to None. + tools: Tools/functions available to the model. Defaults to None. + params: Configuration parameters for the model along with Vertex AI + location and project ID. + + .. deprecated:: 0.0.105 + Use ``settings=GeminiLiveVertexLLMService.Settings(...)`` instead. + + settings: Gemini Live LLM settings. If provided together with deprecated + top-level parameters, the ``settings`` values take precedence. + inference_on_context_initialization: Whether to generate a response when context + is first set. Defaults to True. + file_api_base_url: Base URL for the Gemini File API. Defaults to the official endpoint. + http_options: HTTP options for the client. + **kwargs: Additional arguments passed to parent GeminiLiveLLMService. + """ + # Check if user incorrectly passed api_key, which is used by parent + # class but not here. + if "api_key" in kwargs: + logger.error( + "GeminiLiveVertexLLMService does not accept 'api_key' parameter. " + "Use 'credentials' or 'credentials_path' instead for Vertex AI authentication." + ) + raise ValueError( + "Invalid parameter 'api_key'. Use 'credentials' or 'credentials_path' for Vertex AI authentication." + ) + + # These need to be set before calling super().__init__() because + # super().__init__() invokes create_client(), which needs these. + self._credentials = self._get_credentials(credentials, credentials_path) + self._project_id = project_id + self._location = location + + # Build default_settings from deprecated args, then apply settings delta. + # We pass settings= to super() instead of model=/params= to avoid + # double deprecation warnings from the parent. + + # 1. Initialize default_settings with hardcoded defaults + default_settings = self.Settings( + model="google/gemini-live-2.5-flash-native-audio", + voice="Charon", + frequency_penalty=None, + max_tokens=4096, + presence_penalty=None, + temperature=None, + top_k=None, + top_p=None, + seed=None, + filter_incomplete_user_turns=False, + user_turn_completion_config=None, + modalities=GeminiModalities.AUDIO, + language="en-US", + media_resolution=GeminiMediaResolution.UNSPECIFIED, + vad=None, + context_window_compression={}, + thinking={}, + enable_affective_dialog=False, + proactivity={}, + extra={}, + ) + + # 2. Apply direct init arg overrides (deprecated) + if model is not None: + self._warn_init_param_moved_to_settings("model", "model") + default_settings.model = model + if voice_id != "Charon": + self._warn_init_param_moved_to_settings("voice_id", "voice") + default_settings.voice = voice_id + + # 3. Apply params overrides — only if settings not provided + if params is not None: + self._warn_init_param_moved_to_settings("params") + if not settings: + default_settings.frequency_penalty = params.frequency_penalty + default_settings.max_tokens = params.max_tokens + default_settings.presence_penalty = params.presence_penalty + default_settings.temperature = params.temperature + default_settings.top_k = params.top_k + default_settings.top_p = params.top_p + default_settings.modalities = params.modalities + default_settings.language = ( + language_to_gemini_language(params.language) if params.language else "en-US" + ) + default_settings.media_resolution = params.media_resolution + default_settings.vad = params.vad + default_settings.context_window_compression = ( + params.context_window_compression.model_dump() + if params.context_window_compression + else {} + ) + default_settings.thinking = params.thinking or {} + default_settings.enable_affective_dialog = params.enable_affective_dialog or False + default_settings.proactivity = params.proactivity or {} + if isinstance(params.extra, dict): + default_settings.extra = params.extra + + # 4. Apply settings delta (canonical API, always wins) + if settings is not None: + default_settings.apply_update(settings) + + # Call parent constructor with the obtained settings + super().__init__( + # api_key is required by parent class, but actually not used with + # Vertex + api_key="dummy", + start_audio_paused=start_audio_paused, + start_video_paused=start_video_paused, + system_instruction=system_instruction, + tools=tools, + settings=default_settings, + inference_on_context_initialization=inference_on_context_initialization, + file_api_base_url=file_api_base_url, + http_options=http_options, + **kwargs, + ) + + def create_client(self): + """Create the Gemini client instance.""" + self._client = Client( + vertexai=True, + credentials=self._credentials, + project=self._project_id, + location=self._location, + http_options=self._http_options, + ) + + @property + def file_api(self): + """Gemini File API is not supported with Vertex AI.""" + raise NotImplementedError( + "When using Vertex AI, the recommended approach is to use Google Cloud Storage for file handling. The Gemini File API is not directly supported in this context." + ) + + @staticmethod + def _get_credentials(credentials: Optional[str], credentials_path: Optional[str]) -> str: + """Retrieve Credentials using Google service account credentials JSON. + + Supports multiple authentication methods: + 1. Direct JSON credentials string + 2. Path to service account JSON file + 3. Default application credentials (ADC) + + Args: + credentials: JSON string of service account credentials. + credentials_path: Path to the service account JSON file. + + Returns: + OAuth token for API authentication. + + Raises: + ValueError: If no valid credentials are provided or found. + """ + creds: Optional[service_account.Credentials] = None + + if credentials: + # Parse and load credentials from JSON string + creds = service_account.Credentials.from_service_account_info( + json.loads(credentials), + scopes=["https://www.googleapis.com/auth/cloud-platform"], + ) + elif credentials_path: + # Load credentials from JSON file + creds = service_account.Credentials.from_service_account_file( + credentials_path, + scopes=["https://www.googleapis.com/auth/cloud-platform"], + ) + else: + try: + creds, project_id = default( + scopes=["https://www.googleapis.com/auth/cloud-platform"] + ) + except GoogleAuthError: + pass + + if not creds: + raise ValueError("No valid credentials provided.") + + creds.refresh(Request()) # Ensure token is up-to-date, lifetime is 1 hour. + + return creds diff --git a/src/pipecat/services/google/google.py b/src/pipecat/services/google/google.py index 3d1814cf0..b2fc88b23 100644 --- a/src/pipecat/services/google/google.py +++ b/src/pipecat/services/google/google.py @@ -13,12 +13,12 @@ from pipecat.services import DeprecatedModuleProxy from .frames import * from .image import * from .llm import * -from .llm_openai import * -from .llm_vertex import * +from .openai import * from .rtvi import * from .stt import * from .tts import * +from .vertex import * sys.modules[__name__] = DeprecatedModuleProxy( - globals(), "google", "google.[frames,image,llm,llm_openai,llm_vertex,rtvi,stt,tts]" + globals(), "google", "google.[frames,image,llm,openai,vertex,rtvi,stt,tts]" ) diff --git a/src/pipecat/services/google/image.py b/src/pipecat/services/google/image.py index 5cf37aa4e..6a2919986 100644 --- a/src/pipecat/services/google/image.py +++ b/src/pipecat/services/google/image.py @@ -26,7 +26,7 @@ from pydantic import BaseModel, Field from pipecat.frames.frames import ErrorFrame, Frame, URLImageRawFrame from pipecat.services.google.utils import update_google_client_http_options from pipecat.services.image_service import ImageGenService -from pipecat.services.settings import NOT_GIVEN, ImageGenSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, ImageGenSettings, _NotGiven try: from google import genai @@ -60,13 +60,13 @@ class GoogleImageGenService(ImageGenService): """ Settings = GoogleImageGenSettings - _settings: GoogleImageGenSettings + _settings: Settings class InputParams(BaseModel): """Configuration parameters for Google image generation. .. deprecated:: 0.0.105 - Use ``settings=GoogleImageGenSettings(...)`` instead. + Use ``settings=GoogleImageGenService.Settings(...)`` instead. Parameters: number_of_images: Number of images to generate (1-8). Defaults to 1. @@ -84,7 +84,7 @@ class GoogleImageGenService(ImageGenService): api_key: str, params: Optional[InputParams] = None, http_options: Optional[Any] = None, - settings: Optional[GoogleImageGenSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the GoogleImageGenService with API key and parameters. @@ -94,7 +94,7 @@ class GoogleImageGenService(ImageGenService): params: Configuration parameters for image generation. .. deprecated:: 0.0.105 - Use ``settings=GoogleImageGenSettings(...)`` instead. + Use ``settings=GoogleImageGenService.Settings(...)`` instead. http_options: HTTP options for the client. settings: Runtime-updatable settings. When provided alongside deprecated @@ -102,7 +102,7 @@ class GoogleImageGenService(ImageGenService): **kwargs: Additional arguments passed to the parent ImageGenService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = GoogleImageGenSettings( + default_settings = self.Settings( model="imagen-3.0-generate-002", number_of_images=1, negative_prompt=None, @@ -110,7 +110,7 @@ class GoogleImageGenService(ImageGenService): # 2. Apply params overrides (deprecated) if params is not None: - _warn_deprecated_param("params", GoogleImageGenSettings) + self._warn_init_param_moved_to_settings("params") if not settings: default_settings.model = params.model default_settings.number_of_images = params.number_of_images diff --git a/src/pipecat/services/google/llm.py b/src/pipecat/services/google/llm.py index 7f4f8caae..26ad46311 100644 --- a/src/pipecat/services/google/llm.py +++ b/src/pipecat/services/google/llm.py @@ -16,7 +16,7 @@ import json import os import uuid from dataclasses import dataclass, field -from typing import Any, AsyncIterator, Dict, List, Literal, Optional +from typing import Any, AsyncIterator, Dict, List, Literal, Optional, Union from loguru import logger from PIL import Image @@ -61,7 +61,6 @@ from pipecat.services.settings import ( NOT_GIVEN, LLMSettings, _NotGiven, - _warn_deprecated_param, is_given, ) from pipecat.utils.tracing.service_decorators import traced_llm @@ -201,7 +200,9 @@ class GoogleAssistantContextAggregator(OpenAIAssistantContextAggregator): if message.role == "user": for part in message.parts: if part.function_response and part.function_response.id == tool_call_id: - part.function_response.response = {"value": json.dumps(result)} + part.function_response.response = { + "value": json.dumps(result, ensure_ascii=False) + } @dataclass @@ -719,18 +720,20 @@ class GoogleLLMSettings(LLMSettings): thinking: Thinking configuration. """ - thinking: GoogleThinkingConfig | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + thinking: Union["GoogleLLMService.ThinkingConfig", _NotGiven] = field( + default_factory=lambda: NOT_GIVEN + ) @classmethod def from_mapping(cls, settings): """Convert a plain dict to settings, coercing thinking dicts. For backward compatibility, a ``thinking`` value that is a plain dict - is converted to a :class:`GoogleThinkingConfig`. + is converted to a :class:`GoogleLLMService.ThinkingConfig`. """ instance = super().from_mapping(settings) if is_given(instance.thinking) and isinstance(instance.thinking, dict): - instance.thinking = GoogleThinkingConfig(**instance.thinking) + instance.thinking = GoogleLLMService.ThinkingConfig(**instance.thinking) return instance @@ -743,7 +746,7 @@ class GoogleLLMService(LLMService): """ Settings = GoogleLLMSettings - _settings: GoogleLLMSettings + _settings: Settings # Overriding the default adapter to use the Gemini one. adapter_class = GeminiLLMAdapter @@ -755,7 +758,7 @@ class GoogleLLMService(LLMService): """Input parameters for Google AI models. .. deprecated:: 0.0.105 - Use ``settings=GoogleLLMSettings(...)`` instead. + Use ``settings=GoogleLLMService.Settings(...)`` instead. Parameters: max_tokens: Maximum number of tokens to generate. @@ -775,7 +778,7 @@ class GoogleLLMService(LLMService): temperature: Optional[float] = Field(default=None, ge=0.0, le=2.0) top_k: Optional[int] = Field(default=None, ge=0) top_p: Optional[float] = Field(default=None, ge=0.0, le=1.0) - thinking: Optional[GoogleThinkingConfig] = Field(default=None) + thinking: Optional["GoogleLLMService.ThinkingConfig"] = Field(default=None) extra: Optional[Dict[str, Any]] = Field(default_factory=dict) def __init__( @@ -784,7 +787,7 @@ class GoogleLLMService(LLMService): api_key: str, model: Optional[str] = None, params: Optional[InputParams] = None, - settings: Optional[GoogleLLMSettings] = None, + settings: Optional[Settings] = None, system_instruction: Optional[str] = None, tools: Optional[List[Dict[str, Any]]] = None, tool_config: Optional[Dict[str, Any]] = None, @@ -798,12 +801,12 @@ class GoogleLLMService(LLMService): model: Model name to use. .. deprecated:: 0.0.105 - Use ``settings=GoogleLLMSettings(model=...)`` instead. + Use ``settings=GoogleLLMService.Settings(model=...)`` instead. params: Optional model parameters for inference. .. deprecated:: 0.0.105 - Use ``settings=GoogleLLMSettings(...)`` instead. + Use ``settings=GoogleLLMService.Settings(...)`` instead. settings: Runtime-updatable settings for this service. When both deprecated parameters and *settings* are provided, *settings* @@ -811,14 +814,14 @@ class GoogleLLMService(LLMService): system_instruction: System instruction/prompt for the model. .. deprecated:: 0.0.105 - Use ``settings=GoogleLLMSettings(system_instruction=...)`` instead. + Use ``settings=GoogleLLMService.Settings(system_instruction=...)`` instead. tools: List of available tools/functions. tool_config: Configuration for tool usage. http_options: HTTP options for the client. **kwargs: Additional arguments passed to parent class. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = GoogleLLMSettings( + default_settings = self.Settings( model="gemini-2.5-flash", system_instruction=None, max_tokens=4096, @@ -836,15 +839,15 @@ class GoogleLLMService(LLMService): # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", GoogleLLMSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model if system_instruction is not None: - _warn_deprecated_param("system_instruction", GoogleLLMSettings, "system_instruction") + self._warn_init_param_moved_to_settings("system_instruction", "system_instruction") default_settings.system_instruction = system_instruction # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", GoogleLLMSettings) + self._warn_init_param_moved_to_settings("params") if not settings: default_settings.max_tokens = params.max_tokens default_settings.temperature = params.temperature @@ -881,7 +884,10 @@ class GoogleLLMService(LLMService): self._client = genai.Client(api_key=self._api_key, http_options=self._http_options) async def run_inference( - self, context: LLMContext | OpenAILLMContext, max_tokens: Optional[int] = None + self, + context: LLMContext | OpenAILLMContext, + max_tokens: Optional[int] = None, + system_instruction: Optional[str] = None, ) -> Optional[str]: """Run a one-shot, out-of-band (i.e. out-of-pipeline) inference with the given LLM context. @@ -889,6 +895,8 @@ class GoogleLLMService(LLMService): context: The LLM context containing conversation history. max_tokens: Optional maximum number of tokens to generate. If provided, overrides the service's default max_tokens setting. + system_instruction: Optional system instruction to use for this inference. + If provided, overrides any system instruction in the context. Returns: The LLM's response as a string, or None if no response is generated. @@ -908,6 +916,15 @@ class GoogleLLMService(LLMService): system = getattr(context, "system_message", None) tools = context.tools or [] + # Override system instruction if provided + if system_instruction is not None: + if system: + logger.warning( + f"{self}: Both system_instruction and a system message in context are set." + " Using system_instruction." + ) + system = system_instruction + # Build generation config using the same method as streaming generation_params = self._build_generation_params( system_instruction=system, tools=tools if tools else None @@ -997,12 +1014,16 @@ class GoogleLLMService(LLMService): self, params_from_context: GeminiLLMInvocationParams ) -> AsyncIterator[GenerateContentResponse]: messages = params_from_context["messages"] - if ( - params_from_context["system_instruction"] - and self._settings.system_instruction != params_from_context["system_instruction"] - ): - logger.debug(f"System instruction changed: {params_from_context['system_instruction']}") - self._settings.system_instruction = params_from_context["system_instruction"] + + # Constructor/settings system instruction takes priority over context. + if self._settings.system_instruction and params_from_context["system_instruction"]: + logger.warning( + f"{self}: Both system_instruction and a system message in context are" + " set. Using system_instruction." + ) + system_instruction = ( + self._settings.system_instruction or params_from_context["system_instruction"] + ) tools = [] if params_from_context["tools"]: @@ -1015,7 +1036,7 @@ class GoogleLLMService(LLMService): # Build generation parameters generation_params = self._build_generation_params( - system_instruction=self._settings.system_instruction, + system_instruction=system_instruction, tools=tools, tool_config=tool_config, ) diff --git a/src/pipecat/services/google/llm_openai.py b/src/pipecat/services/google/llm_openai.py index cd5e0f060..f9d182e78 100644 --- a/src/pipecat/services/google/llm_openai.py +++ b/src/pipecat/services/google/llm_openai.py @@ -4,211 +4,15 @@ # SPDX-License-Identifier: BSD 2-Clause License # -"""Google LLM service using OpenAI-compatible API format. +"""Deprecated: use ``pipecat.services.google.openai.llm`` instead.""" -This module provides integration with Google's AI LLM models using the OpenAI -API format through Google's Gemini API OpenAI compatibility layer. -""" +import warnings -import json -import os -from dataclasses import dataclass -from typing import Optional +warnings.warn( + "Module `pipecat.services.google.llm_openai` is deprecated, " + "use `pipecat.services.google.openai.llm` instead.", + DeprecationWarning, + stacklevel=2, +) -from openai import AsyncStream -from openai.types.chat import ChatCompletionChunk - -from pipecat.services.llm_service import FunctionCallFromLLM - -# Suppress gRPC fork warnings -os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false" - -from loguru import logger - -from pipecat.frames.frames import LLMTextFrame -from pipecat.metrics.metrics import LLMTokenUsage -from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext -from pipecat.services.openai.base_llm import OpenAILLMSettings -from pipecat.services.openai.llm import OpenAILLMService -from pipecat.services.settings import _warn_deprecated_param - - -@dataclass -class GoogleOpenAILLMSettings(OpenAILLMSettings): - """Settings for GoogleLLMOpenAIBetaService.""" - - pass - - -class GoogleLLMOpenAIBetaService(OpenAILLMService): - """Google LLM service using OpenAI-compatible API format. - - This service provides access to Google's AI LLM models (like Gemini) through - the OpenAI API format. It handles streaming responses, function calls, and - tool usage while maintaining compatibility with OpenAI's interface. - - Note: This service includes a workaround for a Google API bug where function - call indices may be incorrectly set to None, resulting in empty function names. - - .. deprecated:: 0.0.82 - GoogleLLMOpenAIBetaService is deprecated and will be removed in a future version. - Use GoogleLLMService instead for better integration with Google's native API. - - Reference: - https://ai.google.dev/gemini-api/docs/openai - """ - - Settings = GoogleOpenAILLMSettings - _settings: GoogleOpenAILLMSettings - - def __init__( - self, - *, - api_key: str, - base_url: str = "https://generativelanguage.googleapis.com/v1beta/openai/", - model: Optional[str] = None, - settings: Optional[GoogleOpenAILLMSettings] = None, - **kwargs, - ): - """Initialize the Google LLM service. - - Args: - api_key: Google API key for authentication. - base_url: Base URL for Google's OpenAI-compatible API. - model: Google model name to use (e.g., "gemini-2.0-flash"). - - .. deprecated:: 0.0.105 - Use ``settings=OpenAILLMSettings(model=...)`` instead. - - settings: Runtime-updatable settings. When provided alongside deprecated - parameters, ``settings`` values take precedence. - **kwargs: Additional arguments passed to the parent OpenAILLMService. - """ - import warnings - - with warnings.catch_warnings(): - warnings.simplefilter("always") - warnings.warn( - "GoogleLLMOpenAIBetaService is deprecated and will be removed in a future version. " - "Use GoogleLLMService instead for better integration with Google's native API.", - DeprecationWarning, - stacklevel=2, - ) - - # 1. Initialize default_settings with hardcoded defaults - default_settings = GoogleOpenAILLMSettings(model="gemini-2.0-flash") - - # 2. Apply direct init arg overrides (deprecated) - if model is not None: - _warn_deprecated_param("model", GoogleOpenAILLMSettings, "model") - default_settings.model = model - - # 3. (No step 3, as there's no params object to apply) - - # 4. Apply settings delta (canonical API, always wins) - if settings is not None: - default_settings.apply_update(settings) - - super().__init__(api_key=api_key, base_url=base_url, settings=default_settings, **kwargs) - - async def _process_context(self, context: OpenAILLMContext): - functions_list = [] - arguments_list = [] - tool_id_list = [] - func_idx = 0 - function_name = "" - arguments = "" - tool_call_id = "" - - await self.start_ttfb_metrics() - - chunk_stream: AsyncStream[ - ChatCompletionChunk - ] = await self._stream_chat_completions_specific_context(context) - - # Use context manager to ensure stream is closed on cancellation/exception. - # Without this, CancelledError during iteration leaves the underlying socket open. - async with chunk_stream: - async for chunk in chunk_stream: - if chunk.usage: - tokens = LLMTokenUsage( - prompt_tokens=chunk.usage.prompt_tokens or 0, - completion_tokens=chunk.usage.completion_tokens or 0, - total_tokens=chunk.usage.total_tokens or 0, - ) - await self.start_llm_usage_metrics(tokens) - - if chunk.choices is None or len(chunk.choices) == 0: - continue - - await self.stop_ttfb_metrics() - - if not chunk.choices[0].delta: - continue - - if chunk.choices[0].delta.tool_calls: - # We're streaming the LLM response to enable the fastest response times. - # For text, we just yield each chunk as we receive it and count on consumers - # to do whatever coalescing they need (eg. to pass full sentences to TTS) - # - # If the LLM is a function call, we'll do some coalescing here. - # If the response contains a function name, we'll yield a frame to tell consumers - # that they can start preparing to call the function with that name. - # We accumulate all the arguments for the rest of the streamed response, then when - # the response is done, we package up all the arguments and the function name and - # yield a frame containing the function name and the arguments. - logger.debug(f"Tool call: {chunk.choices[0].delta.tool_calls}") - tool_call = chunk.choices[0].delta.tool_calls[0] - if tool_call.index != func_idx: - functions_list.append(function_name) - arguments_list.append(arguments) - tool_id_list.append(tool_call_id) - function_name = "" - arguments = "" - tool_call_id = "" - func_idx += 1 - if tool_call.function and tool_call.function.name: - function_name += tool_call.function.name - tool_call_id = tool_call.id - if tool_call.function and tool_call.function.arguments: - # Keep iterating through the response to collect all the argument fragments - arguments += tool_call.function.arguments - elif chunk.choices[0].delta.content: - await self.push_frame(LLMTextFrame(chunk.choices[0].delta.content)) - - # if we got a function name and arguments, check to see if it's a function with - # a registered handler. If so, run the registered callback, save the result to - # the context, and re-prompt to get a chat answer. If we don't have a registered - # handler, raise an exception. - if function_name and arguments: - # added to the list as last function name and arguments not added to the list - functions_list.append(function_name) - arguments_list.append(arguments) - tool_id_list.append(tool_call_id) - - logger.debug( - f"Function list: {functions_list}, Arguments list: {arguments_list}, Tool ID list: {tool_id_list}" - ) - - function_calls = [] - for function_name, arguments, tool_id in zip( - functions_list, arguments_list, tool_id_list - ): - if function_name == "": - # TODO: Remove the _process_context method once Google resolves the bug - # where the index is incorrectly set to None instead of returning the actual index, - # which currently results in an empty function name(''). - continue - - arguments = json.loads(arguments) - - function_calls.append( - FunctionCallFromLLM( - context=context, - tool_call_id=tool_id, - function_name=function_name, - arguments=arguments, - ) - ) - - await self.run_function_calls(function_calls) +from pipecat.services.google.openai.llm import * # noqa: E402, F401, F403 diff --git a/src/pipecat/services/google/llm_vertex.py b/src/pipecat/services/google/llm_vertex.py index 3901b5d50..54d338ad7 100644 --- a/src/pipecat/services/google/llm_vertex.py +++ b/src/pipecat/services/google/llm_vertex.py @@ -4,306 +4,15 @@ # SPDX-License-Identifier: BSD 2-Clause License # -"""Google Vertex AI LLM service implementation. +"""Deprecated: use ``pipecat.services.google.vertex.llm`` instead.""" -This module provides integration with Google's AI models via Vertex AI, -extending the GoogleLLMService with Vertex AI authentication. -""" +import warnings -import json -import os -from dataclasses import dataclass +warnings.warn( + "Module `pipecat.services.google.llm_vertex` is deprecated, " + "use `pipecat.services.google.vertex.llm` instead.", + DeprecationWarning, + stacklevel=2, +) -# Suppress gRPC fork warnings -os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false" - -from typing import Optional - -from loguru import logger - -from pipecat.services.google.llm import GoogleLLMService, GoogleLLMSettings -from pipecat.services.settings import _warn_deprecated_param - -try: - from google.auth import default - from google.auth.exceptions import GoogleAuthError - from google.auth.transport.requests import Request - from google.genai import Client - from google.genai.types import HttpOptions - from google.oauth2 import service_account - -except ModuleNotFoundError as e: - logger.error(f"Exception: {e}") - logger.error( - "In order to use Google AI, you need to `pip install pipecat-ai[google]`. Also, set `GOOGLE_APPLICATION_CREDENTIALS` environment variable." - ) - raise Exception(f"Missing module: {e}") - - -@dataclass -class GoogleVertexLLMSettings(GoogleLLMSettings): - """Settings for GoogleVertexLLMService.""" - - pass - - -class GoogleVertexLLMService(GoogleLLMService): - """Google Vertex AI LLM service extending GoogleLLMService. - - Provides access to Google's AI models via Vertex AI while using the same - Google AI client and message format as GoogleLLMService. Handles authentication - using Google service account credentials and configures the client for - Vertex AI endpoints. - - Reference: - https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference - """ - - Settings = GoogleVertexLLMSettings - _settings: GoogleVertexLLMSettings - - class InputParams(GoogleLLMService.InputParams): - """Input parameters specific to Vertex AI. - - Parameters: - location: GCP region for Vertex AI endpoint (e.g., "us-east4"). - - .. deprecated:: 0.0.90 - Use `location` as a direct argument to - `GoogleVertexLLMService.__init__()` instead. - - project_id: Google Cloud project ID. - - .. deprecated:: 0.0.90 - Use `project_id` as a direct argument to - `GoogleVertexLLMService.__init__()` instead. - """ - - # https://cloud.google.com/vertex-ai/generative-ai/docs/learn/locations - location: Optional[str] = None - project_id: Optional[str] = None - - def __init__(self, **kwargs): - """Initializes the InputParams.""" - import warnings - - with warnings.catch_warnings(): - warnings.simplefilter("always") - if "location" in kwargs and kwargs["location"] is not None: - warnings.warn( - "GoogleVertexLLMService.InputParams.location is deprecated. " - "Please provide 'location' as a direct argument to GoogleVertexLLMService.__init__() instead.", - DeprecationWarning, - stacklevel=2, - ) - - if "project_id" in kwargs and kwargs["project_id"] is not None: - warnings.warn( - "GoogleVertexLLMService.InputParams.project_id is deprecated. " - "Please provide 'project_id' as a direct argument to GoogleVertexLLMService.__init__() instead.", - DeprecationWarning, - stacklevel=2, - ) - super().__init__(**kwargs) - - def __init__( - self, - *, - credentials: Optional[str] = None, - credentials_path: Optional[str] = None, - model: Optional[str] = None, - location: Optional[str] = None, - project_id: Optional[str] = None, - params: Optional[GoogleLLMService.InputParams] = None, - settings: Optional[GoogleVertexLLMSettings] = None, - system_instruction: Optional[str] = None, - tools: Optional[list] = None, - tool_config: Optional[dict] = None, - http_options: Optional[HttpOptions] = None, - **kwargs, - ): - """Initializes the VertexLLMService. - - Args: - credentials: JSON string of service account credentials. - credentials_path: Path to the service account JSON file. - model: Model identifier (e.g., "gemini-2.5-flash"). - - .. deprecated:: 0.0.105 - Use ``settings=GoogleLLMSettings(model=...)`` instead. - - location: GCP region for Vertex AI endpoint (e.g., "us-east4"). - project_id: Google Cloud project ID. - params: Input parameters for the model. - - .. deprecated:: 0.0.105 - Use ``settings=GoogleLLMSettings(...)`` instead. - - settings: Runtime-updatable settings for this service. When both - deprecated parameters and *settings* are provided, *settings* - values take precedence. - system_instruction: System instruction/prompt for the model. - - .. deprecated:: 0.0.105 - Use ``settings=GoogleVertexLLMSettings(system_instruction=...)`` instead. - tools: List of available tools/functions. - tool_config: Configuration for tool usage. - http_options: HTTP options for the client. - **kwargs: Additional arguments passed to GoogleLLMService. - """ - # Check if user incorrectly passed api_key, which is used by parent - # class but not here. - if "api_key" in kwargs: - logger.error( - "GoogleVertexLLMService does not accept 'api_key' parameter. " - "Use 'credentials' or 'credentials_path' instead for Vertex AI authentication." - ) - raise ValueError( - "Invalid parameter 'api_key'. Use 'credentials' or 'credentials_path' for Vertex AI authentication." - ) - - # Handle deprecated InputParams fields (location/project_id extraction - # must happen before validation, regardless of settings) - if params and isinstance(params, GoogleVertexLLMService.InputParams): - if project_id is None: - project_id = params.project_id - if location is None: - location = params.location - # Convert to base InputParams - params = GoogleLLMService.InputParams( - **params.model_dump(exclude={"location", "project_id"}, exclude_unset=True) - ) - - # Validate project_id and location parameters - # NOTE: once we remove Vertex-specific InputParams class, we can update - # __init__() signature as follows: - # - location: str = "us-east4", - # - project_id: str, - # But for now, we need them as-is to maintain proper backward - # compatibility. - if project_id is None: - raise ValueError("project_id is required") - if location is None: - # If location is not provided, default to "us-east4". - # Note: this is legacy behavior; ideally location would be - # required. - logger.warning("location is not provided. Defaulting to 'us-east4'.") - location = "us-east4" # Default location if not provided - - # These need to be set before calling super().__init__() because - # super().__init__() invokes _create_client(), which needs these. - self._credentials = self._get_credentials(credentials, credentials_path) - self._project_id = project_id - self._location = location - - # 1. Initialize default_settings with hardcoded defaults - default_settings = GoogleVertexLLMSettings( - model="gemini-2.5-flash", - system_instruction=None, - max_tokens=4096, - temperature=None, - top_k=None, - top_p=None, - frequency_penalty=None, - presence_penalty=None, - seed=None, - filter_incomplete_user_turns=False, - user_turn_completion_config=None, - thinking=None, - extra={}, - ) - - # 2. Apply direct init arg overrides (deprecated) - if model is not None: - _warn_deprecated_param("model", GoogleVertexLLMSettings, "model") - default_settings.model = model - if system_instruction is not None: - _warn_deprecated_param( - "system_instruction", GoogleVertexLLMSettings, "system_instruction" - ) - default_settings.system_instruction = system_instruction - - # 3. Apply params overrides — only if settings not provided - if params is not None: - _warn_deprecated_param("params", GoogleVertexLLMSettings) - if not settings: - default_settings.max_tokens = params.max_tokens - default_settings.temperature = params.temperature - default_settings.top_k = params.top_k - default_settings.top_p = params.top_p - default_settings.thinking = params.thinking - if isinstance(params.extra, dict): - default_settings.extra = params.extra - - # 4. Apply settings delta (canonical API, always wins) - if settings is not None: - default_settings.apply_update(settings) - - # Call parent constructor with dummy api_key - # (api_key is required by parent class, but not actually used with Vertex) - super().__init__( - api_key="dummy", - settings=default_settings, - tools=tools, - tool_config=tool_config, - http_options=http_options, - **kwargs, - ) - - def create_client(self): - """Create the Gemini client instance configured for Vertex AI.""" - self._client = Client( - vertexai=True, - credentials=self._credentials, - project=self._project_id, - location=self._location, - http_options=self._http_options, - ) - - @staticmethod - def _get_credentials(credentials: Optional[str], credentials_path: Optional[str]): - """Retrieve Credentials using Google service account credentials. - - Supports multiple authentication methods: - 1. Direct JSON credentials string - 2. Path to service account JSON file - 3. Default application credentials (ADC) - - Args: - credentials: JSON string of service account credentials. - credentials_path: Path to the service account JSON file. - - Returns: - Google credentials object for API authentication. - - Raises: - ValueError: If no valid credentials are provided or found. - """ - creds: Optional[service_account.Credentials] = None - - if credentials: - # Parse and load credentials from JSON string - creds = service_account.Credentials.from_service_account_info( - json.loads(credentials), - scopes=["https://www.googleapis.com/auth/cloud-platform"], - ) - elif credentials_path: - # Load credentials from JSON file - creds = service_account.Credentials.from_service_account_file( - credentials_path, - scopes=["https://www.googleapis.com/auth/cloud-platform"], - ) - else: - try: - creds, project_id = default( - scopes=["https://www.googleapis.com/auth/cloud-platform"] - ) - except GoogleAuthError: - pass - - if not creds: - raise ValueError("No valid credentials provided.") - - creds.refresh(Request()) # Ensure token is up-to-date, lifetime is 1 hour. - - return creds +from pipecat.services.google.vertex.llm import * # noqa: E402, F401, F403 diff --git a/src/pipecat/services/google/openai/__init__.py b/src/pipecat/services/google/openai/__init__.py new file mode 100644 index 000000000..c4d243b97 --- /dev/null +++ b/src/pipecat/services/google/openai/__init__.py @@ -0,0 +1,5 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# diff --git a/src/pipecat/services/google/openai/llm.py b/src/pipecat/services/google/openai/llm.py new file mode 100644 index 000000000..da5d1be7a --- /dev/null +++ b/src/pipecat/services/google/openai/llm.py @@ -0,0 +1,213 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""Google LLM service using OpenAI-compatible API format. + +This module provides integration with Google's AI LLM models using the OpenAI +API format through Google's Gemini API OpenAI compatibility layer. +""" + +import json +import os +from dataclasses import dataclass +from typing import Optional + +from openai import AsyncStream +from openai.types.chat import ChatCompletionChunk + +from pipecat.services.llm_service import FunctionCallFromLLM + +# Suppress gRPC fork warnings +os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false" + +from loguru import logger + +from pipecat.frames.frames import LLMTextFrame +from pipecat.metrics.metrics import LLMTokenUsage +from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext +from pipecat.services.openai.base_llm import BaseOpenAILLMService +from pipecat.services.openai.llm import OpenAILLMService + + +@dataclass +class GoogleOpenAILLMSettings(BaseOpenAILLMService.Settings): + """Settings for GoogleLLMOpenAIBetaService.""" + + pass + + +class GoogleLLMOpenAIBetaService(OpenAILLMService): + """Google LLM service using OpenAI-compatible API format. + + This service provides access to Google's AI LLM models (like Gemini) through + the OpenAI API format. It handles streaming responses, function calls, and + tool usage while maintaining compatibility with OpenAI's interface. + + Note: This service includes a workaround for a Google API bug where function + call indices may be incorrectly set to None, resulting in empty function names. + + .. deprecated:: 0.0.82 + GoogleLLMOpenAIBetaService is deprecated and will be removed in a future version. + Use GoogleLLMService instead for better integration with Google's native API. + + Reference: + https://ai.google.dev/gemini-api/docs/openai + """ + + Settings = GoogleOpenAILLMSettings + _settings: Settings + + def __init__( + self, + *, + api_key: str, + base_url: str = "https://generativelanguage.googleapis.com/v1beta/openai/", + model: Optional[str] = None, + settings: Optional[Settings] = None, + **kwargs, + ): + """Initialize the Google LLM service. + + Args: + api_key: Google API key for authentication. + base_url: Base URL for Google's OpenAI-compatible API. + model: Google model name to use (e.g., "gemini-2.0-flash"). + + .. deprecated:: 0.0.105 + Use ``settings=GoogleLLMOpenAIBetaService.Settings(model=...)`` instead. + + settings: Runtime-updatable settings. When provided alongside deprecated + parameters, ``settings`` values take precedence. + **kwargs: Additional arguments passed to the parent OpenAILLMService. + """ + import warnings + + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "GoogleLLMOpenAIBetaService is deprecated and will be removed in a future version. " + "Use GoogleLLMService instead for better integration with Google's native API.", + DeprecationWarning, + stacklevel=2, + ) + + # 1. Initialize default_settings with hardcoded defaults + default_settings = self.Settings(model="gemini-2.0-flash") + + # 2. Apply direct init arg overrides (deprecated) + if model is not None: + self._warn_init_param_moved_to_settings("model", "model") + default_settings.model = model + + # 3. (No step 3, as there's no params object to apply) + + # 4. Apply settings delta (canonical API, always wins) + if settings is not None: + default_settings.apply_update(settings) + + super().__init__(api_key=api_key, base_url=base_url, settings=default_settings, **kwargs) + + async def _process_context(self, context: OpenAILLMContext): + functions_list = [] + arguments_list = [] + tool_id_list = [] + func_idx = 0 + function_name = "" + arguments = "" + tool_call_id = "" + + await self.start_ttfb_metrics() + + chunk_stream: AsyncStream[ + ChatCompletionChunk + ] = await self._stream_chat_completions_specific_context(context) + + # Use context manager to ensure stream is closed on cancellation/exception. + # Without this, CancelledError during iteration leaves the underlying socket open. + async with chunk_stream: + async for chunk in chunk_stream: + if chunk.usage: + tokens = LLMTokenUsage( + prompt_tokens=chunk.usage.prompt_tokens or 0, + completion_tokens=chunk.usage.completion_tokens or 0, + total_tokens=chunk.usage.total_tokens or 0, + ) + await self.start_llm_usage_metrics(tokens) + + if chunk.choices is None or len(chunk.choices) == 0: + continue + + await self.stop_ttfb_metrics() + + if not chunk.choices[0].delta: + continue + + if chunk.choices[0].delta.tool_calls: + # We're streaming the LLM response to enable the fastest response times. + # For text, we just yield each chunk as we receive it and count on consumers + # to do whatever coalescing they need (eg. to pass full sentences to TTS) + # + # If the LLM is a function call, we'll do some coalescing here. + # If the response contains a function name, we'll yield a frame to tell consumers + # that they can start preparing to call the function with that name. + # We accumulate all the arguments for the rest of the streamed response, then when + # the response is done, we package up all the arguments and the function name and + # yield a frame containing the function name and the arguments. + logger.debug(f"Tool call: {chunk.choices[0].delta.tool_calls}") + tool_call = chunk.choices[0].delta.tool_calls[0] + if tool_call.index != func_idx: + functions_list.append(function_name) + arguments_list.append(arguments) + tool_id_list.append(tool_call_id) + function_name = "" + arguments = "" + tool_call_id = "" + func_idx += 1 + if tool_call.function and tool_call.function.name: + function_name += tool_call.function.name + tool_call_id = tool_call.id + if tool_call.function and tool_call.function.arguments: + # Keep iterating through the response to collect all the argument fragments + arguments += tool_call.function.arguments + elif chunk.choices[0].delta.content: + await self.push_frame(LLMTextFrame(chunk.choices[0].delta.content)) + + # if we got a function name and arguments, check to see if it's a function with + # a registered handler. If so, run the registered callback, save the result to + # the context, and re-prompt to get a chat answer. If we don't have a registered + # handler, raise an exception. + if function_name and arguments: + # added to the list as last function name and arguments not added to the list + functions_list.append(function_name) + arguments_list.append(arguments) + tool_id_list.append(tool_call_id) + + logger.debug( + f"Function list: {functions_list}, Arguments list: {arguments_list}, Tool ID list: {tool_id_list}" + ) + + function_calls = [] + for function_name, arguments, tool_id in zip( + functions_list, arguments_list, tool_id_list + ): + if function_name == "": + # TODO: Remove the _process_context method once Google resolves the bug + # where the index is incorrectly set to None instead of returning the actual index, + # which currently results in an empty function name(''). + continue + + arguments = json.loads(arguments) + + function_calls.append( + FunctionCallFromLLM( + context=context, + tool_call_id=tool_id, + function_name=function_name, + arguments=arguments, + ) + ) + + await self.run_function_calls(function_calls) diff --git a/src/pipecat/services/google/stt.py b/src/pipecat/services/google/stt.py index 70e51ac3c..173b201fe 100644 --- a/src/pipecat/services/google/stt.py +++ b/src/pipecat/services/google/stt.py @@ -36,7 +36,7 @@ from pipecat.frames.frames import ( StartFrame, TranscriptionFrame, ) -from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven from pipecat.services.stt_latency import GOOGLE_TTFS_P99 from pipecat.services.stt_service import STTService from pipecat.transcriptions.language import Language, resolve_language @@ -415,7 +415,7 @@ class GoogleSTTService(STTService): """ Settings = GoogleSTTSettings - _settings: GoogleSTTSettings + _settings: Settings # Google Cloud's STT service has a connection time limit of 5 minutes per stream. # They've shared an "endless streaming" example that guided this implementation: @@ -427,7 +427,7 @@ class GoogleSTTService(STTService): """Configuration parameters for Google Speech-to-Text. .. deprecated:: 0.0.105 - Use ``settings=GoogleSTTSettings(...)`` instead. + Use ``settings=GoogleSTTService.Settings(...)`` instead. Parameters: languages: Single language or list of recognition languages. First language is primary. @@ -488,7 +488,7 @@ class GoogleSTTService(STTService): location: str = "global", sample_rate: Optional[int] = None, params: Optional[InputParams] = None, - settings: Optional[GoogleSTTSettings] = None, + settings: Optional[Settings] = None, ttfs_p99_latency: Optional[float] = GOOGLE_TTFS_P99, **kwargs, ): @@ -502,7 +502,7 @@ class GoogleSTTService(STTService): params: Configuration parameters for the service. .. deprecated:: 0.0.105 - Use ``settings=GoogleSTTSettings(...)`` instead. + Use ``settings=GoogleSTTService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated ``params``, ``settings`` values take precedence. @@ -511,7 +511,7 @@ class GoogleSTTService(STTService): **kwargs: Additional arguments passed to STTService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = GoogleSTTSettings( + default_settings = self.Settings( language=None, languages=[Language.EN_US], language_codes=None, @@ -531,7 +531,7 @@ class GoogleSTTService(STTService): # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", GoogleSTTSettings) + self._warn_init_param_moved_to_settings("params") if not settings: default_settings.languages = list(params.language_list) default_settings.model = params.model @@ -655,7 +655,7 @@ class GoogleSTTService(STTService): """Update the service's recognition languages. .. deprecated:: 0.0.104 - Use ``STTUpdateSettingsFrame`` with ``GoogleSTTSettings(languages=...)`` + Use ``STTUpdateSettingsFrame`` with ``GoogleSTTService.Settings(languages=...)`` instead. Args: @@ -665,13 +665,13 @@ class GoogleSTTService(STTService): warnings.simplefilter("always") warnings.warn( "set_languages() is deprecated. Use STTUpdateSettingsFrame with " - "GoogleSTTSettings(languages=...) instead.", + "self.Settings(languages=...) instead.", DeprecationWarning, ) logger.debug(f"Switching STT languages to: {languages}") - await self._update_settings(GoogleSTTSettings(languages=list(languages))) + await self._update_settings(self.Settings(languages=list(languages))) - async def _update_settings(self, delta: GoogleSTTSettings) -> dict[str, Any]: + async def _update_settings(self, delta: Settings) -> dict[str, Any]: """Apply settings delta and reconnect if anything changed. Handles ``language`` from base ``set_language`` by converting it to @@ -698,8 +698,8 @@ class GoogleSTTService(STTService): with warnings.catch_warnings(): warnings.simplefilter("always") warnings.warn( - "GoogleSTTSettings.language_codes is deprecated. " - "Use GoogleSTTSettings.languages (List[Language]) instead.", + "self.Settings.language_codes is deprecated. " + "Use self.Settings.languages (List[Language]) instead.", DeprecationWarning, stacklevel=2, ) @@ -756,7 +756,7 @@ class GoogleSTTService(STTService): """Update service options dynamically. .. deprecated:: - Use ``STTUpdateSettingsFrame`` with ``GoogleSTTSettings(...)`` + Use ``STTUpdateSettingsFrame`` with ``GoogleSTTService.Settings(...)`` instead. Args: @@ -780,11 +780,11 @@ class GoogleSTTService(STTService): warnings.simplefilter("always") warnings.warn( "update_options() is deprecated. Use STTUpdateSettingsFrame with " - "GoogleSTTSettings(...) instead.", + "self.Settings(...) instead.", DeprecationWarning, ) # Build a settings delta from the provided options - delta = GoogleSTTSettings() + delta = self.Settings() if languages is not None: delta.languages = list(languages) diff --git a/src/pipecat/services/google/tts.py b/src/pipecat/services/google/tts.py index a9394028b..93053cc94 100644 --- a/src/pipecat/services/google/tts.py +++ b/src/pipecat/services/google/tts.py @@ -39,7 +39,6 @@ from pipecat.services.settings import ( NOT_GIVEN, TTSSettings, _NotGiven, - _warn_deprecated_param, is_given, ) from pipecat.services.tts_service import TTSService @@ -523,7 +522,7 @@ class GoogleTTSSettings(TTSSettings): #: .. deprecated:: 0.0.105 -#: Use ``GoogleTTSSettings`` instead. +#: Use ``GoogleTTSService.Settings`` instead. GoogleStreamTTSSettings = GoogleTTSSettings @@ -559,13 +558,13 @@ class GoogleHttpTTSService(TTSService): """ Settings = GoogleHttpTTSSettings - _settings: GoogleHttpTTSSettings + _settings: Settings class InputParams(BaseModel): """Input parameters for Google HTTP TTS voice customization. .. deprecated:: 0.0.105 - Use ``GoogleHttpTTSSettings`` directly via the ``settings`` parameter instead. + Use ``GoogleHttpTTSService.Settings`` directly via the ``settings`` parameter instead. Parameters: pitch: Voice pitch adjustment (e.g., "+2st", "-50%"). @@ -596,7 +595,7 @@ class GoogleHttpTTSService(TTSService): voice_id: Optional[str] = None, sample_rate: Optional[int] = None, params: Optional[InputParams] = None, - settings: Optional[GoogleHttpTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initializes the Google HTTP TTS service. @@ -608,20 +607,20 @@ class GoogleHttpTTSService(TTSService): voice_id: Google TTS voice identifier (e.g., "en-US-Standard-A"). .. deprecated:: 0.0.105 - Use ``settings=GoogleHttpTTSSettings(voice=...)`` instead. + Use ``settings=GoogleHttpTTSService.Settings(voice=...)`` instead. sample_rate: Audio sample rate in Hz. If None, uses default. params: Voice customization parameters including pitch, rate, volume, etc. .. deprecated:: 0.0.105 - Use ``settings=GoogleHttpTTSSettings(...)`` instead. + Use ``settings=GoogleHttpTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional arguments passed to parent TTSService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = GoogleHttpTTSSettings( + default_settings = self.Settings( model=None, voice="en-US-Chirp3-HD-Charon", language="en-US", @@ -636,12 +635,12 @@ class GoogleHttpTTSService(TTSService): # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: - _warn_deprecated_param("voice_id", GoogleHttpTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", GoogleHttpTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: if params.pitch is not None: default_settings.pitch = params.pitch @@ -654,7 +653,7 @@ class GoogleHttpTTSService(TTSService): if params.emphasis is not None: default_settings.emphasis = params.emphasis if params.language is not None: - default_settings.language = self.language_to_service_language(params.language) + default_settings.language = params.language if params.gender is not None: default_settings.gender = params.gender if params.google_style is not None: @@ -747,7 +746,7 @@ class GoogleHttpTTSService(TTSService): Args: delta: Settings delta. Can include 'speaking_rate' (float). """ - if isinstance(delta, GoogleHttpTTSSettings) and is_given(delta.speaking_rate): + if isinstance(delta, self.Settings) and is_given(delta.speaking_rate): rate_value = float(delta.speaking_rate) if not (0.25 <= rate_value <= 2.0): logger.warning( @@ -1014,21 +1013,21 @@ class GoogleTTSService(GoogleBaseTTSService): tts = GoogleTTSService( credentials_path="/path/to/service-account.json", - voice_id="en-US-Chirp3-HD-Charon", - params=GoogleTTSService.InputParams( + settings=GoogleTTSService.Settings( + voice="en-US-Chirp3-HD-Charon", language=Language.EN_US, ) ) """ Settings = GoogleTTSSettings - _settings: GoogleTTSSettings + _settings: Settings class InputParams(BaseModel): """Input parameters for Google streaming TTS configuration. .. deprecated:: 0.0.105 - Use ``GoogleTTSSettings`` directly via the ``settings`` parameter instead. + Use ``GoogleTTSService.Settings`` directly via the ``settings`` parameter instead. Parameters: language: Language for synthesis. Defaults to English. @@ -1048,7 +1047,7 @@ class GoogleTTSService(GoogleBaseTTSService): voice_cloning_key: Optional[str] = None, sample_rate: Optional[int] = None, params: Optional[InputParams] = None, - settings: Optional[GoogleTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initializes the Google streaming TTS service. @@ -1060,21 +1059,21 @@ class GoogleTTSService(GoogleBaseTTSService): voice_id: Google TTS voice identifier (e.g., "en-US-Chirp3-HD-Charon"). .. deprecated:: 0.0.105 - Use ``settings=GoogleTTSSettings(voice=...)`` instead. + Use ``settings=GoogleTTSService.Settings(voice=...)`` instead. voice_cloning_key: The voice cloning key for Chirp 3 custom voices. sample_rate: Audio sample rate in Hz. If None, uses default. params: Language configuration parameters. .. deprecated:: 0.0.105 - Use ``settings=GoogleTTSSettings(...)`` instead. + Use ``settings=GoogleTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional arguments passed to parent TTSService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = GoogleTTSSettings( + default_settings = self.Settings( model=None, voice="en-US-Chirp3-HD-Charon", language="en-US", @@ -1083,15 +1082,15 @@ class GoogleTTSService(GoogleBaseTTSService): # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: - _warn_deprecated_param("voice_id", GoogleTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", GoogleTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: if params.language is not None: - default_settings.language = self.language_to_service_language(params.language) + default_settings.language = params.language if params.speaking_rate is not None: default_settings.speaking_rate = params.speaking_rate @@ -1119,7 +1118,7 @@ class GoogleTTSService(GoogleBaseTTSService): Args: delta: Settings delta. Can include 'speaking_rate' (float). """ - if isinstance(delta, GoogleTTSSettings) and is_given(delta.speaking_rate): + if isinstance(delta, self.Settings) and is_given(delta.speaking_rate): rate_value = float(delta.speaking_rate) if not (0.25 <= rate_value <= 2.0): logger.warning( @@ -1191,9 +1190,9 @@ class GeminiTTSService(GoogleBaseTTSService): tts = GeminiTTSService( credentials_path="/path/to/service-account.json", - model="gemini-2.5-flash-tts", - voice_id="Kore", - params=GeminiTTSService.InputParams( + settings=GeminiTTSService.Settings( + model="gemini-2.5-flash-tts", + voice="Kore", language=Language.EN_US, prompt="Say this in a friendly and helpful tone" ) @@ -1201,7 +1200,7 @@ class GeminiTTSService(GoogleBaseTTSService): """ Settings = GeminiTTSSettings - _settings: GeminiTTSSettings + _settings: Settings GOOGLE_SAMPLE_RATE = 24000 # Google TTS always outputs at 24kHz @@ -1243,7 +1242,7 @@ class GeminiTTSService(GoogleBaseTTSService): """Input parameters for Gemini TTS configuration. .. deprecated:: 0.0.105 - Use ``GeminiTTSSettings`` directly via the ``settings`` parameter instead. + Use ``GeminiTTSService.Settings`` directly via the ``settings`` parameter instead. Parameters: language: Language for synthesis. Defaults to English. @@ -1268,7 +1267,7 @@ class GeminiTTSService(GoogleBaseTTSService): voice_id: Optional[str] = None, sample_rate: Optional[int] = None, params: Optional[InputParams] = None, - settings: Optional[GeminiTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initializes the Gemini TTS service. @@ -1284,7 +1283,7 @@ class GeminiTTSService(GoogleBaseTTSService): "gemini-2.5-flash-tts" or "gemini-2.5-pro-tts". .. deprecated:: 0.0.105 - Use ``settings=GeminiTTSSettings(model=...)`` instead. + Use ``settings=GeminiTTSService.Settings(model=...)`` instead. credentials: JSON string containing Google Cloud service account credentials. credentials_path: Path to Google Cloud service account JSON file. @@ -1292,13 +1291,13 @@ class GeminiTTSService(GoogleBaseTTSService): voice_id: Voice name from the available Gemini voices. .. deprecated:: 0.0.105 - Use ``settings=GeminiTTSSettings(voice=...)`` instead. + Use ``settings=GeminiTTSService.Settings(voice=...)`` instead. sample_rate: Audio sample rate in Hz. If None, uses Google's default 24kHz. params: TTS configuration parameters. .. deprecated:: 0.0.105 - Use ``settings=GeminiTTSSettings(...)`` instead. + Use ``settings=GeminiTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -1320,7 +1319,7 @@ class GeminiTTSService(GoogleBaseTTSService): ) # 1. Initialize default_settings with hardcoded defaults - default_settings = GeminiTTSSettings( + default_settings = self.Settings( model="gemini-2.5-flash-tts", voice="Kore", language="en-US", @@ -1331,10 +1330,10 @@ class GeminiTTSService(GoogleBaseTTSService): # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", GeminiTTSSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model if voice_id is not None: - _warn_deprecated_param("voice_id", GeminiTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id if default_settings.voice not in self.AVAILABLE_VOICES: @@ -1344,10 +1343,10 @@ class GeminiTTSService(GoogleBaseTTSService): # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", GeminiTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: if params.language is not None: - default_settings.language = self.language_to_service_language(params.language) + default_settings.language = params.language if params.prompt is not None: default_settings.prompt = params.prompt if params.multi_speaker is not None: diff --git a/src/pipecat/services/google/vertex/__init__.py b/src/pipecat/services/google/vertex/__init__.py new file mode 100644 index 000000000..c4d243b97 --- /dev/null +++ b/src/pipecat/services/google/vertex/__init__.py @@ -0,0 +1,5 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# diff --git a/src/pipecat/services/google/vertex/llm.py b/src/pipecat/services/google/vertex/llm.py new file mode 100644 index 000000000..946a8f1bb --- /dev/null +++ b/src/pipecat/services/google/vertex/llm.py @@ -0,0 +1,306 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""Google Vertex AI LLM service implementation. + +This module provides integration with Google's AI models via Vertex AI, +extending the GoogleLLMService with Vertex AI authentication. +""" + +import json +import os +from dataclasses import dataclass + +# Suppress gRPC fork warnings +os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false" + +from typing import Optional + +from loguru import logger + +from pipecat.services.google.llm import GoogleLLMService + +try: + from google.auth import default + from google.auth.exceptions import GoogleAuthError + from google.auth.transport.requests import Request + from google.genai import Client + from google.genai.types import HttpOptions + from google.oauth2 import service_account + +except ModuleNotFoundError as e: + logger.error(f"Exception: {e}") + logger.error( + "In order to use Google AI, you need to `pip install pipecat-ai[google]`. Also, set `GOOGLE_APPLICATION_CREDENTIALS` environment variable." + ) + raise Exception(f"Missing module: {e}") + + +@dataclass +class GoogleVertexLLMSettings(GoogleLLMService.Settings): + """Settings for GoogleVertexLLMService.""" + + pass + + +class GoogleVertexLLMService(GoogleLLMService): + """Google Vertex AI LLM service extending GoogleLLMService. + + Provides access to Google's AI models via Vertex AI while using the same + Google AI client and message format as GoogleLLMService. Handles authentication + using Google service account credentials and configures the client for + Vertex AI endpoints. + + Reference: + https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference + """ + + Settings = GoogleVertexLLMSettings + _settings: Settings + + class InputParams(GoogleLLMService.InputParams): + """Input parameters specific to Vertex AI. + + Parameters: + location: GCP region for Vertex AI endpoint (e.g., "us-east4"). + + .. deprecated:: 0.0.90 + Use `location` as a direct argument to + `GoogleVertexLLMService.__init__()` instead. + + project_id: Google Cloud project ID. + + .. deprecated:: 0.0.90 + Use `project_id` as a direct argument to + `GoogleVertexLLMService.__init__()` instead. + """ + + # https://cloud.google.com/vertex-ai/generative-ai/docs/learn/locations + location: Optional[str] = None + project_id: Optional[str] = None + + def __init__(self, **kwargs): + """Initializes the InputParams.""" + import warnings + + with warnings.catch_warnings(): + warnings.simplefilter("always") + if "location" in kwargs and kwargs["location"] is not None: + warnings.warn( + "GoogleVertexLLMService.InputParams.location is deprecated. " + "Please provide 'location' as a direct argument to GoogleVertexLLMService.__init__() instead.", + DeprecationWarning, + stacklevel=2, + ) + + if "project_id" in kwargs and kwargs["project_id"] is not None: + warnings.warn( + "GoogleVertexLLMService.InputParams.project_id is deprecated. " + "Please provide 'project_id' as a direct argument to GoogleVertexLLMService.__init__() instead.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(**kwargs) + + def __init__( + self, + *, + credentials: Optional[str] = None, + credentials_path: Optional[str] = None, + model: Optional[str] = None, + location: Optional[str] = None, + project_id: Optional[str] = None, + params: Optional[GoogleLLMService.InputParams] = None, + settings: Optional[Settings] = None, + system_instruction: Optional[str] = None, + tools: Optional[list] = None, + tool_config: Optional[dict] = None, + http_options: Optional[HttpOptions] = None, + **kwargs, + ): + """Initializes the VertexLLMService. + + Args: + credentials: JSON string of service account credentials. + credentials_path: Path to the service account JSON file. + model: Model identifier (e.g., "gemini-2.5-flash"). + + .. deprecated:: 0.0.105 + Use ``settings=GoogleVertexLLMService.Settings(model=...)`` instead. + + location: GCP region for Vertex AI endpoint (e.g., "us-east4"). + project_id: Google Cloud project ID. + params: Input parameters for the model. + + .. deprecated:: 0.0.105 + Use ``settings=GoogleVertexLLMService.Settings(...)`` instead. + + settings: Runtime-updatable settings for this service. When both + deprecated parameters and *settings* are provided, *settings* + values take precedence. + system_instruction: System instruction/prompt for the model. + + .. deprecated:: 0.0.105 + Use ``settings=GoogleVertexLLMService.Settings(system_instruction=...)`` instead. + tools: List of available tools/functions. + tool_config: Configuration for tool usage. + http_options: HTTP options for the client. + **kwargs: Additional arguments passed to GoogleLLMService. + """ + # Check if user incorrectly passed api_key, which is used by parent + # class but not here. + if "api_key" in kwargs: + logger.error( + "GoogleVertexLLMService does not accept 'api_key' parameter. " + "Use 'credentials' or 'credentials_path' instead for Vertex AI authentication." + ) + raise ValueError( + "Invalid parameter 'api_key'. Use 'credentials' or 'credentials_path' for Vertex AI authentication." + ) + + # Handle deprecated InputParams fields (location/project_id extraction + # must happen before validation, regardless of settings) + if params and isinstance(params, GoogleVertexLLMService.InputParams): + if project_id is None: + project_id = params.project_id + if location is None: + location = params.location + # Convert to base InputParams + params = GoogleLLMService.InputParams( + **params.model_dump(exclude={"location", "project_id"}, exclude_unset=True) + ) + + # Validate project_id and location parameters + # NOTE: once we remove Vertex-specific InputParams class, we can update + # __init__() signature as follows: + # - location: str = "us-east4", + # - project_id: str, + # But for now, we need them as-is to maintain proper backward + # compatibility. + if project_id is None: + raise ValueError("project_id is required") + if location is None: + # If location is not provided, default to "us-east4". + # Note: this is legacy behavior; ideally location would be + # required. + logger.warning("location is not provided. Defaulting to 'us-east4'.") + location = "us-east4" # Default location if not provided + + # These need to be set before calling super().__init__() because + # super().__init__() invokes _create_client(), which needs these. + self._credentials = self._get_credentials(credentials, credentials_path) + self._project_id = project_id + self._location = location + + # 1. Initialize default_settings with hardcoded defaults + default_settings = self.Settings( + model="gemini-2.5-flash", + system_instruction=None, + max_tokens=4096, + temperature=None, + top_k=None, + top_p=None, + frequency_penalty=None, + presence_penalty=None, + seed=None, + filter_incomplete_user_turns=False, + user_turn_completion_config=None, + thinking=None, + extra={}, + ) + + # 2. Apply direct init arg overrides (deprecated) + if model is not None: + self._warn_init_param_moved_to_settings("model", "model") + default_settings.model = model + if system_instruction is not None: + self._warn_init_param_moved_to_settings("system_instruction", "system_instruction") + default_settings.system_instruction = system_instruction + + # 3. Apply params overrides — only if settings not provided + if params is not None: + self._warn_init_param_moved_to_settings("params") + if not settings: + default_settings.max_tokens = params.max_tokens + default_settings.temperature = params.temperature + default_settings.top_k = params.top_k + default_settings.top_p = params.top_p + default_settings.thinking = params.thinking + if isinstance(params.extra, dict): + default_settings.extra = params.extra + + # 4. Apply settings delta (canonical API, always wins) + if settings is not None: + default_settings.apply_update(settings) + + # Call parent constructor with dummy api_key + # (api_key is required by parent class, but not actually used with Vertex) + super().__init__( + api_key="dummy", + settings=default_settings, + tools=tools, + tool_config=tool_config, + http_options=http_options, + **kwargs, + ) + + def create_client(self): + """Create the Gemini client instance configured for Vertex AI.""" + self._client = Client( + vertexai=True, + credentials=self._credentials, + project=self._project_id, + location=self._location, + http_options=self._http_options, + ) + + @staticmethod + def _get_credentials(credentials: Optional[str], credentials_path: Optional[str]): + """Retrieve Credentials using Google service account credentials. + + Supports multiple authentication methods: + 1. Direct JSON credentials string + 2. Path to service account JSON file + 3. Default application credentials (ADC) + + Args: + credentials: JSON string of service account credentials. + credentials_path: Path to the service account JSON file. + + Returns: + Google credentials object for API authentication. + + Raises: + ValueError: If no valid credentials are provided or found. + """ + creds: Optional[service_account.Credentials] = None + + if credentials: + # Parse and load credentials from JSON string + creds = service_account.Credentials.from_service_account_info( + json.loads(credentials), + scopes=["https://www.googleapis.com/auth/cloud-platform"], + ) + elif credentials_path: + # Load credentials from JSON file + creds = service_account.Credentials.from_service_account_file( + credentials_path, + scopes=["https://www.googleapis.com/auth/cloud-platform"], + ) + else: + try: + creds, project_id = default( + scopes=["https://www.googleapis.com/auth/cloud-platform"] + ) + except GoogleAuthError: + pass + + if not creds: + raise ValueError("No valid credentials provided.") + + creds.refresh(Request()) # Ensure token is up-to-date, lifetime is 1 hour. + + return creds diff --git a/src/pipecat/services/gradium/stt.py b/src/pipecat/services/gradium/stt.py index 3fddf0470..5dea2c824 100644 --- a/src/pipecat/services/gradium/stt.py +++ b/src/pipecat/services/gradium/stt.py @@ -10,6 +10,7 @@ This module provides integration with Gradium's real-time speech-to-text WebSocket API for streaming audio transcription. """ +import asyncio import base64 import json from dataclasses import dataclass, field @@ -22,13 +23,14 @@ from pipecat.frames.frames import ( CancelFrame, EndFrame, Frame, + InterimTranscriptionFrame, StartFrame, TranscriptionFrame, VADUserStartedSpeakingFrame, VADUserStoppedSpeakingFrame, ) from pipecat.processors.frame_processor import FrameDirection -from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven from pipecat.services.stt_latency import GRADIUM_TTFS_P99 from pipecat.services.stt_service import WebsocketSTTService from pipecat.transcriptions.language import Language, resolve_language @@ -43,7 +45,37 @@ except ModuleNotFoundError as e: logger.error('In order to use Gradium, you need to `pip install "pipecat-ai[gradium]"`.') raise Exception(f"Missing module: {e}") -SAMPLE_RATE = 24000 +# Seconds to wait after a "flushed" message for trailing text tokens to arrive +# before finalizing the transcription. +TRANSCRIPT_AGGREGATION_DELAY = 0.1 + + +def _input_format_from_encoding(encoding: str, sample_rate: int) -> str: + """Build Gradium input_format from encoding type and sample rate. + + For PCM encoding, appends the sample rate (e.g., "pcm_16000"). + For other encodings (wav, opus), returns the encoding as-is. + + Args: + encoding: Base encoding type ("pcm", "wav", or "opus"). + sample_rate: Audio sample rate in Hz. + + Returns: + The full input_format string for the Gradium API. + """ + if encoding == "pcm": + match sample_rate: + case 8000: + return "pcm_8000" + case 16000: + return "pcm_16000" + case 24000: + return "pcm_24000" + logger.warning( + f"GradiumSTTService: unsupported sample rate {sample_rate} for PCM encoding, using pcm_16000" + ) + return "pcm_16000" + return encoding def language_to_gradium_language(language: Language) -> Optional[str]: @@ -89,13 +121,13 @@ class GradiumSTTService(WebsocketSTTService): """ Settings = GradiumSTTSettings - _settings: GradiumSTTSettings + _settings: Settings class InputParams(BaseModel): """Configuration parameters for Gradium STT API. .. deprecated:: 0.0.105 - Use ``settings=GradiumSTTSettings(...)`` instead. + Use ``settings=GradiumSTTService.Settings(...)`` instead. Parameters: language: Expected language of the audio (e.g., "en", "es", "fr"). @@ -115,9 +147,11 @@ class GradiumSTTService(WebsocketSTTService): *, api_key: str, api_endpoint_base_url: str = "wss://eu.api.gradium.ai/api/speech/asr", + encoding: str = "pcm", + sample_rate: Optional[int] = None, params: Optional[InputParams] = None, json_config: Optional[str] = None, - settings: Optional[GradiumSTTSettings] = None, + settings: Optional[Settings] = None, ttfs_p99_latency: Optional[float] = GRADIUM_TTFS_P99, **kwargs, ): @@ -126,10 +160,16 @@ class GradiumSTTService(WebsocketSTTService): Args: api_key: Gradium API key for authentication. api_endpoint_base_url: WebSocket endpoint URL. Defaults to Gradium's streaming endpoint. + encoding: Base audio encoding type. One of "pcm", "wav", or "opus". + For PCM, the sample rate is appended automatically from the + pipeline's audio_in_sample_rate (e.g., "pcm" becomes "pcm_16000"). + Defaults to "pcm". + sample_rate: Audio sample rate in Hz. If None, uses the pipeline + sample rate. params: Configuration parameters for language and delay settings. .. deprecated:: 0.0.105 - Use ``settings=GradiumSTTSettings(...)`` instead. + Use ``settings=GradiumSTTService.Settings(...)`` instead. json_config: Optional JSON configuration string for additional model settings. @@ -152,8 +192,8 @@ class GradiumSTTService(WebsocketSTTService): ) # 1. Initialize default_settings with hardcoded defaults - default_settings = GradiumSTTSettings( - model=None, + default_settings = self.Settings( + model="default", language=None, delay_in_frames=None, ) @@ -162,7 +202,7 @@ class GradiumSTTService(WebsocketSTTService): # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", GradiumSTTSettings) + self._warn_init_param_moved_to_settings("params") if not settings: default_settings.language = params.language if params.delay_in_frames is not None: @@ -173,7 +213,7 @@ class GradiumSTTService(WebsocketSTTService): default_settings.apply_update(settings) super().__init__( - sample_rate=SAMPLE_RATE, + sample_rate=sample_rate, ttfs_p99_latency=ttfs_p99_latency, settings=default_settings, **kwargs, @@ -181,19 +221,25 @@ class GradiumSTTService(WebsocketSTTService): self._api_key = api_key self._api_endpoint_base_url = api_endpoint_base_url + self._encoding = encoding self._websocket = None self._json_config = json_config self._receive_task = None + self._input_format = "" + self._audio_buffer = bytearray() self._chunk_size_ms = 80 self._chunk_size_bytes = 0 - # Set from the ready message when connecting to the service. - # These values are used for flushing transcription. - self._delay_in_frames = 0 - self._frame_size = 0 + # Accumulates text fragments within a turn. Each "text" message + # appends to this list. On "flushed" a short aggregation delay + # allows trailing tokens to arrive before the full text is joined + # and pushed as a TranscriptionFrame. + self._accumulated_text: list[str] = [] + self._flush_counter = 0 + self._transcript_aggregation_task: Optional[asyncio.Task] = None def can_generate_metrics(self) -> bool: """Check if the service can generate metrics. @@ -207,7 +253,7 @@ class GradiumSTTService(WebsocketSTTService): """Apply a settings delta, sync params, and reconnect. Args: - delta: A :class:`STTSettings` (or ``GradiumSTTSettings``) delta. + delta: A :class:`STTSettings` (or ``GradiumSTTService.Settings``) delta. Returns: Dict mapping changed field names to their previous values. @@ -228,6 +274,7 @@ class GradiumSTTService(WebsocketSTTService): frame: Start frame to begin processing. """ await super().start(frame) + self._input_format = _input_format_from_encoding(self._encoding, self.sample_rate) self._chunk_size_bytes = int(self._chunk_size_ms * self.sample_rate * 2 / 1000) await self._connect() @@ -249,56 +296,41 @@ class GradiumSTTService(WebsocketSTTService): await super().cancel(frame) await self._disconnect() - async def process_frame(self, frame: Frame, direction: FrameDirection): - """Process frames with VAD-specific handling. + async def _start_metrics(self): + """Start performance metrics collection for transcription processing.""" + await self.start_processing_metrics() - When VAD detects the user has stopped speaking, we flush the transcription - by sending silence frames. This makes the system more reactive by getting - the final transcription faster without closing the connection. + async def process_frame(self, frame: Frame, direction: FrameDirection): + """Process incoming frames and handle speech events. Args: frame: The frame to process. - direction: The direction of frame processing. + direction: Direction of frame flow in the pipeline. """ await super().process_frame(frame, direction) if isinstance(frame, VADUserStartedSpeakingFrame): - await self.start_processing_metrics() + await self._start_metrics() elif isinstance(frame, VADUserStoppedSpeakingFrame): - await self._flush_transcription() + await self._send_flush() - async def _flush_transcription(self): - """Flush the transcription by sending silence frames. + async def _send_flush(self): + """Send a flush request to process any buffered audio immediately. - When VAD detects the user stopped speaking, we send delay_in_frames - chunks of silence (zeros) to flush the remaining audio from the model's - buffer. This allows for faster turn-around without closing the connection. - - From Gradium docs: "feed in delay_in_frames chunks of silence (vectors - of zeros). If those are fed in faster than realtime, the API also has - a possibility to process them faster." + Sends a flush message to tell the server to process buffered audio. + The server responds with text fragments followed by a "flushed" + acknowledgment, which triggers finalization. """ if not self._websocket or self._websocket.state is not State.OPEN: return - if self._delay_in_frames <= 0: - logger.debug("No delay_in_frames set, skipping flush") - return - - # Create a silence chunk (zeros) of frame_size samples - # Each sample is 2 bytes (16-bit PCM) - silence_bytes = bytes(self._frame_size * 2) - silence_b64 = base64.b64encode(silence_bytes).decode("utf-8") - - logger.debug(f"Flushing Gradium STT with {self._delay_in_frames} silence frames") - - for _ in range(self._delay_in_frames): - msg = {"type": "audio", "audio": silence_b64} - try: - await self._websocket.send(json.dumps(msg)) - except Exception as e: - logger.warning(f"Failed to send silence frame: {e}") - break + self._flush_counter += 1 + flush_id = str(self._flush_counter) + msg = {"type": "flush", "flush_id": flush_id} + try: + await self._websocket.send(json.dumps(msg)) + except Exception as e: + logger.warning(f"Failed to send flush: {e}") async def run_stt(self, audio: bytes) -> AsyncGenerator[Frame, None]: """Process audio data for speech-to-text conversion. @@ -353,7 +385,8 @@ class GradiumSTTService(WebsocketSTTService): await self._call_event_handler("on_connected") setup_msg = { "type": "setup", - "input_format": "pcm", + "model_name": self._settings.model, + "input_format": self._input_format, } # Build json_config: start with deprecated json_config, then override with params json_config = {} @@ -375,13 +408,7 @@ class GradiumSTTService(WebsocketSTTService): if ready_msg["type"] != "ready": raise Exception(f"unexpected first message type {ready_msg['type']}") - # Store delay_in_frames and frame_size for silence flushing - self._delay_in_frames = ready_msg.get("delay_in_frames", 0) - self._frame_size = ready_msg.get("frame_size", 1920) - logger.debug( - f"Connected to Gradium STT (delay_in_frames={self._delay_in_frames}, " - f"frame_size={self._frame_size})" - ) + logger.debug("Connected to Gradium STT") except Exception as e: await self.push_error(error_msg=f"Unknown error occurred: {e}", exception=e) @@ -390,6 +417,13 @@ class GradiumSTTService(WebsocketSTTService): async def _disconnect(self): await super()._disconnect() + if self._transcript_aggregation_task: + await self.cancel_task(self._transcript_aggregation_task) + self._transcript_aggregation_task = None + + self._accumulated_text.clear() + self._flush_counter = 0 + if self._receive_task: await self.cancel_task(self._receive_task) self._receive_task = None @@ -412,41 +446,75 @@ class GradiumSTTService(WebsocketSTTService): return self._websocket raise Exception("Websocket not connected") - async def _process_messages(self): + async def _receive_messages(self): async for message in self._get_websocket(): try: - data = json.loads(message) - await self._process_response(data) + msg = json.loads(message) except json.JSONDecodeError: logger.warning(f"Received non-JSON message: {message}") + continue - async def _receive_messages(self): - while True: - await self._process_messages() - logger.debug(f"{self} Gradium connection was disconnected (timeout?), reconnecting") - await self._connect_websocket() - - async def _process_response(self, msg): - type_ = msg.get("type", "") - if type_ == "text": - await self._handle_text(msg["text"]) - elif type_ == "end_of_stream": - await self._handle_end_of_stream() - elif type_ == "error": - await self.push_error(error_msg=f"Error: {msg}") - - async def _handle_end_of_stream(self): - """Handle termination message.""" - logger.debug("Received end_of_stream message from server") + type_ = msg.get("type", "") + if type_ == "text": + await self._handle_text(msg["text"]) + elif type_ == "flushed": + await self._handle_flushed() + elif type_ == "end_of_stream": + logger.debug("Received end_of_stream message from server") + elif type_ == "error": + await self.push_error(error_msg=f"Error: {msg}") async def _handle_text(self, text: str): - """Handle transcription results.""" + """Handle streaming transcription fragment. + + Accumulates text and pushes an InterimTranscriptionFrame with the + full accumulated text so far. + """ + self._accumulated_text.append(text) + accumulated = " ".join(self._accumulated_text) + await self.push_frame( + InterimTranscriptionFrame( + text=accumulated, + user_id=self._user_id, + timestamp=time_now_iso8601(), + language=self._settings.language, + ) + ) + await self.stop_processing_metrics() + + async def _handle_flushed(self): + """Handle flush completion by starting a transcript aggregation timer. + + The "flushed" message confirms that buffered audio has been processed, + but text tokens may still arrive after this point. A short timer allows + trailing tokens to accumulate before finalizing the transcription. + """ + if self._transcript_aggregation_task: + await self.cancel_task(self._transcript_aggregation_task) + self._transcript_aggregation_task = self.create_task( + self._transcript_aggregation_handler(), "transcript_aggregation" + ) + + async def _transcript_aggregation_handler(self): + """Wait for trailing tokens then finalize the accumulated transcription.""" + await asyncio.sleep(TRANSCRIPT_AGGREGATION_DELAY) + await self._finalize_accumulated_text() + + async def _finalize_accumulated_text(self): + """Join accumulated text, push TranscriptionFrame, and clear state.""" + if not self._accumulated_text: + return + self._transcript_aggregation_task = None + + text = " ".join(self._accumulated_text) + self._accumulated_text.clear() + logger.debug(f"Final transcription: [{text}]") await self.push_frame( TranscriptionFrame( text, self._user_id, time_now_iso8601(), + self._settings.language, ) ) - await self._trace_transcription(text, is_final=True, language=None) - await self.stop_processing_metrics() + await self._trace_transcription(text, is_final=True, language=self._settings.language) diff --git a/src/pipecat/services/gradium/tts.py b/src/pipecat/services/gradium/tts.py index 03b4d7008..f63f931ad 100644 --- a/src/pipecat/services/gradium/tts.py +++ b/src/pipecat/services/gradium/tts.py @@ -21,7 +21,7 @@ from pipecat.frames.frames import ( TTSAudioRawFrame, TTSStoppedFrame, ) -from pipecat.services.settings import TTSSettings, _warn_deprecated_param +from pipecat.services.settings import TTSSettings from pipecat.services.tts_service import WebsocketTTSService from pipecat.utils.tracing.service_decorators import traced_tts @@ -48,13 +48,13 @@ class GradiumTTSService(WebsocketTTSService): """Text-to-Speech service using Gradium's websocket API.""" Settings = GradiumTTSSettings - _settings: GradiumTTSSettings + _settings: Settings class InputParams(BaseModel): """Configuration parameters for Gradium TTS service. .. deprecated:: 0.0.105 - Use ``GradiumTTSSettings`` directly via the ``settings`` parameter instead. + Use ``GradiumTTSService.Settings`` directly via the ``settings`` parameter instead. Parameters: temp: Temperature to be used for generation, defaults to 0.6. @@ -71,7 +71,7 @@ class GradiumTTSService(WebsocketTTSService): model: Optional[str] = None, json_config: Optional[str] = None, params: Optional[InputParams] = None, - settings: Optional[GradiumTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Gradium TTS service. @@ -81,26 +81,26 @@ class GradiumTTSService(WebsocketTTSService): voice_id: the voice identifier. .. deprecated:: 0.0.105 - Use ``settings=GradiumTTSSettings(voice=...)`` instead. + Use ``settings=GradiumTTSService.Settings(voice=...)`` instead. url: Gradium websocket API endpoint. model: Model ID to use for synthesis. .. deprecated:: 0.0.105 - Use ``settings=GradiumTTSSettings(model=...)`` instead. + Use ``settings=GradiumTTSService.Settings(model=...)`` instead. json_config: Optional JSON configuration string for additional model settings. params: Additional configuration parameters. .. deprecated:: 0.0.105 - Use ``settings=GradiumTTSSettings(...)`` instead. + Use ``settings=GradiumTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional arguments passed to parent class. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = GradiumTTSSettings( + default_settings = self.Settings( model="default", voice="YTpq7expH9539ERJ", language=None, @@ -108,15 +108,15 @@ class GradiumTTSService(WebsocketTTSService): # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", GradiumTTSSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model if voice_id is not None: - _warn_deprecated_param("voice_id", GradiumTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", GradiumTTSSettings) + self._warn_init_param_moved_to_settings("params") # Note: params.temp has no corresponding settings field # 4. Apply settings delta (canonical API, always wins) @@ -153,7 +153,7 @@ class GradiumTTSService(WebsocketTTSService): """Apply a settings delta and reconnect if voice changed. Args: - delta: A :class:`TTSSettings` (or ``GradiumTTSSettings``) delta. + delta: A :class:`TTSSettings` (or ``GradiumTTSService.Settings``) delta. Returns: Dict mapping changed field names to their previous values. diff --git a/src/pipecat/services/grok/llm.py b/src/pipecat/services/grok/llm.py index 255ee2cf5..160ad3331 100644 --- a/src/pipecat/services/grok/llm.py +++ b/src/pipecat/services/grok/llm.py @@ -23,13 +23,12 @@ from pipecat.processors.aggregators.llm_response import ( LLMUserAggregatorParams, ) from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext -from pipecat.services.openai.base_llm import OpenAILLMSettings +from pipecat.services.openai.base_llm import BaseOpenAILLMService from pipecat.services.openai.llm import ( OpenAIAssistantContextAggregator, OpenAILLMService, OpenAIUserContextAggregator, ) -from pipecat.services.settings import _warn_deprecated_param @dataclass @@ -71,7 +70,7 @@ class GrokContextAggregatorPair: @dataclass -class GrokLLMSettings(OpenAILLMSettings): +class GrokLLMSettings(BaseOpenAILLMService.Settings): """Settings for GrokLLMService.""" pass @@ -87,7 +86,7 @@ class GrokLLMService(OpenAILLMService): """ Settings = GrokLLMSettings - _settings: GrokLLMSettings + _settings: Settings def __init__( self, @@ -95,7 +94,7 @@ class GrokLLMService(OpenAILLMService): api_key: str, base_url: str = "https://api.x.ai/v1", model: Optional[str] = None, - settings: Optional[GrokLLMSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the GrokLLMService with API key and model. @@ -106,18 +105,18 @@ class GrokLLMService(OpenAILLMService): model: The model identifier to use. Defaults to "grok-3-beta". .. deprecated:: 0.0.105 - Use ``settings=OpenAILLMSettings(model=...)`` instead. + Use ``settings=GrokLLMService.Settings(model=...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional keyword arguments passed to OpenAILLMService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = GrokLLMSettings(model="grok-3-beta") + default_settings = self.Settings(model="grok-3-beta") # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", GrokLLMSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. (No step 3, as there's no params object to apply) diff --git a/src/pipecat/services/grok/realtime/llm.py b/src/pipecat/services/grok/realtime/llm.py index 30061af4a..6e37d21d2 100644 --- a/src/pipecat/services/grok/realtime/llm.py +++ b/src/pipecat/services/grok/realtime/llm.py @@ -60,7 +60,6 @@ from pipecat.services.settings import ( NOT_GIVEN, LLMSettings, _NotGiven, - _warn_deprecated_param, is_given, ) from pipecat.utils.time import time_now_iso8601 @@ -109,7 +108,7 @@ class GrokRealtimeLLMSettings(LLMSettings): # -- Bidirectional sync helpers ------------------------------------------ @staticmethod - def _sync_top_level_to_sp(settings: "GrokRealtimeLLMSettings"): + def _sync_top_level_to_sp(settings: "GrokRealtimeLLMService.Settings"): """Push top-level ``system_instruction`` into ``session_properties``.""" if not is_given(settings.session_properties): return @@ -119,7 +118,7 @@ class GrokRealtimeLLMSettings(LLMSettings): # -- apply_update override ----------------------------------------------- - def apply_update(self, delta: "GrokRealtimeLLMSettings") -> Dict[str, Any]: + def apply_update(self, delta: "GrokRealtimeLLMService.Settings") -> Dict[str, Any]: """Merge a delta, keeping ``system_instruction`` in sync with SP. When the delta contains ``session_properties``, it **replaces** the @@ -151,8 +150,8 @@ class GrokRealtimeLLMSettings(LLMSettings): @classmethod def from_mapping( - cls: Type["GrokRealtimeLLMSettings"], settings: Mapping[str, Any] - ) -> "GrokRealtimeLLMSettings": + cls: Type["GrokRealtimeLLMService.Settings"], settings: Mapping[str, Any] + ) -> "GrokRealtimeLLMService.Settings": """Build a delta from a plain dict, routing SP keys into ``session_properties``. Keys that correspond to ``SessionProperties`` fields are collected into @@ -203,7 +202,7 @@ class GrokRealtimeLLMService(LLMService): """ Settings = GrokRealtimeLLMSettings - _settings: GrokRealtimeLLMSettings + _settings: Settings # Use the Grok-specific adapter adapter_class = GrokRealtimeLLMAdapter @@ -214,7 +213,7 @@ class GrokRealtimeLLMService(LLMService): api_key: str, base_url: str = "wss://api.x.ai/v1/realtime", session_properties: Optional[events.SessionProperties] = None, - settings: Optional[GrokRealtimeLLMSettings] = None, + settings: Optional[Settings] = None, start_audio_paused: bool = False, **kwargs, ): @@ -228,7 +227,7 @@ class GrokRealtimeLLMService(LLMService): If None, uses default SessionProperties with voice "Ara". .. deprecated:: 0.0.105 - Use ``settings=GrokRealtimeLLMSettings(session_properties=...)`` + Use ``settings=GrokRealtimeLLMService.Settings(session_properties=...)`` instead. To set a different voice, configure it in session_properties: @@ -241,7 +240,7 @@ class GrokRealtimeLLMService(LLMService): **kwargs: Additional arguments passed to parent LLMService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = GrokRealtimeLLMSettings( + default_settings = self.Settings( model=None, system_instruction=None, temperature=None, @@ -260,7 +259,7 @@ class GrokRealtimeLLMService(LLMService): if session_properties is not None: _warn_deprecated_param( "session_properties", - GrokRealtimeLLMSettings, + self.Settings, "session_properties", ) default_settings.session_properties = session_properties @@ -269,7 +268,7 @@ class GrokRealtimeLLMService(LLMService): default_settings.system_instruction = session_properties.instructions # Sync top-level system_instruction back into session_properties - GrokRealtimeLLMSettings._sync_top_level_to_sp(default_settings) + self.Settings._sync_top_level_to_sp(default_settings) # 3. Apply settings delta (canonical API, always wins) if settings is not None: @@ -677,7 +676,10 @@ class GrokRealtimeLLMService(LLMService): elif evt.type == "response.function_call_arguments.done": await self._handle_evt_function_call_arguments_done(evt) elif evt.type == "error": - if evt.error.code == "response_cancel_not_active": + if evt.error.code in ( + "response_cancel_not_active", + "conversation_already_has_active_response", + ): logger.debug(f"{self} {evt.error.message}") else: await self._handle_evt_error(evt) @@ -937,7 +939,7 @@ class GrokRealtimeLLMService(LLMService): item = events.ConversationItem( type="function_call_output", call_id=tool_call_id, - output=json.dumps(result), + output=json.dumps(result, ensure_ascii=False), ) await self.send_client_event(events.ConversationItemCreateEvent(item=item)) diff --git a/src/pipecat/services/groq/llm.py b/src/pipecat/services/groq/llm.py index 6669c385a..d36b52ab8 100644 --- a/src/pipecat/services/groq/llm.py +++ b/src/pipecat/services/groq/llm.py @@ -11,13 +11,12 @@ from typing import Optional from loguru import logger -from pipecat.services.openai.base_llm import OpenAILLMSettings +from pipecat.services.openai.base_llm import BaseOpenAILLMService from pipecat.services.openai.llm import OpenAILLMService -from pipecat.services.settings import _warn_deprecated_param @dataclass -class GroqLLMSettings(OpenAILLMSettings): +class GroqLLMSettings(BaseOpenAILLMService.Settings): """Settings for GroqLLMService.""" pass @@ -31,7 +30,7 @@ class GroqLLMService(OpenAILLMService): """ Settings = GroqLLMSettings - _settings: GroqLLMSettings + _settings: Settings def __init__( self, @@ -39,7 +38,7 @@ class GroqLLMService(OpenAILLMService): api_key: str, base_url: str = "https://api.groq.com/openai/v1", model: Optional[str] = None, - settings: Optional[GroqLLMSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize Groq LLM service. @@ -50,18 +49,18 @@ class GroqLLMService(OpenAILLMService): model: The model identifier to use. Defaults to "llama-3.3-70b-versatile". .. deprecated:: 0.0.105 - Use ``settings=OpenAILLMSettings(model=...)`` instead. + Use ``settings=GroqLLMService.Settings(model=...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional keyword arguments passed to OpenAILLMService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = GroqLLMSettings(model="llama-3.3-70b-versatile") + default_settings = self.Settings(model="llama-3.3-70b-versatile") # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", GroqLLMSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. (No step 3, as there's no params object to apply) diff --git a/src/pipecat/services/groq/stt.py b/src/pipecat/services/groq/stt.py index b5d59181a..3f6c23774 100644 --- a/src/pipecat/services/groq/stt.py +++ b/src/pipecat/services/groq/stt.py @@ -9,18 +9,16 @@ from dataclasses import dataclass from typing import Optional -from pipecat.services.settings import _warn_deprecated_param from pipecat.services.stt_latency import GROQ_TTFS_P99 from pipecat.services.whisper.base_stt import ( BaseWhisperSTTService, - BaseWhisperSTTSettings, Transcription, ) from pipecat.transcriptions.language import Language @dataclass -class GroqSTTSettings(BaseWhisperSTTSettings): +class GroqSTTSettings(BaseWhisperSTTService.Settings): """Settings for the Groq STT service. Parameters: @@ -38,7 +36,7 @@ class GroqSTTService(BaseWhisperSTTService): """ Settings = GroqSTTSettings - _settings: GroqSTTSettings + _settings: Settings def __init__( self, @@ -49,7 +47,7 @@ class GroqSTTService(BaseWhisperSTTService): language: Optional[Language] = None, prompt: Optional[str] = None, temperature: Optional[float] = None, - settings: Optional[GroqSTTSettings] = None, + settings: Optional[Settings] = None, ttfs_p99_latency: Optional[float] = GROQ_TTFS_P99, **kwargs, ): @@ -59,24 +57,24 @@ class GroqSTTService(BaseWhisperSTTService): model: Whisper model to use. .. deprecated:: 0.0.105 - Use ``settings=GroqSTTSettings(model=...)`` instead. + Use ``settings=GroqSTTService.Settings(model=...)`` instead. api_key: Groq API key. Defaults to None. base_url: API base URL. Defaults to "https://api.groq.com/openai/v1". language: Language of the audio input. .. deprecated:: 0.0.105 - Use ``settings=GroqSTTSettings(language=...)`` instead. + Use ``settings=GroqSTTService.Settings(language=...)`` instead. prompt: Optional text to guide the model's style or continue a previous segment. .. deprecated:: 0.0.105 - Use ``settings=GroqSTTSettings(prompt=...)`` instead. + Use ``settings=GroqSTTService.Settings(prompt=...)`` instead. temperature: Optional sampling temperature between 0 and 1. .. deprecated:: 0.0.105 - Use ``settings=GroqSTTSettings(temperature=...)`` instead. + Use ``settings=GroqSTTService.Settings(temperature=...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -85,25 +83,25 @@ class GroqSTTService(BaseWhisperSTTService): **kwargs: Additional arguments passed to BaseWhisperSTTService. """ # --- 1. Hardcoded defaults --- - default_settings = GroqSTTSettings( + default_settings = self.Settings( model="whisper-large-v3-turbo", - language=self.language_to_service_language(Language.EN), + language=Language.EN, prompt=None, temperature=None, ) # --- 2. Deprecated direct-arg overrides --- if model is not None: - _warn_deprecated_param("model", GroqSTTSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model if language is not None: - _warn_deprecated_param("language", GroqSTTSettings, "language") - default_settings.language = self.language_to_service_language(language) + self._warn_init_param_moved_to_settings("language", "language") + default_settings.language = language if prompt is not None: - _warn_deprecated_param("prompt", GroqSTTSettings, "prompt") + self._warn_init_param_moved_to_settings("prompt", "prompt") default_settings.prompt = prompt if temperature is not None: - _warn_deprecated_param("temperature", GroqSTTSettings, "temperature") + self._warn_init_param_moved_to_settings("temperature", "temperature") default_settings.temperature = temperature # --- 3. (no params object for this service) --- diff --git a/src/pipecat/services/groq/tts.py b/src/pipecat/services/groq/tts.py index bb39a9067..00ff3ef84 100644 --- a/src/pipecat/services/groq/tts.py +++ b/src/pipecat/services/groq/tts.py @@ -19,7 +19,7 @@ from pipecat.frames.frames import ( Frame, TTSAudioRawFrame, ) -from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven from pipecat.services.tts_service import TTSService from pipecat.transcriptions.language import Language from pipecat.utils.tracing.service_decorators import traced_tts @@ -52,13 +52,13 @@ class GroqTTSService(TTSService): """ Settings = GroqTTSSettings - _settings: GroqTTSSettings + _settings: Settings class InputParams(BaseModel): """Input parameters for Groq TTS configuration. .. deprecated:: 0.0.105 - Use ``settings=GroqTTSSettings(...)`` instead. + Use ``settings=GroqTTSService.Settings(...)`` instead. Parameters: language: Language for speech synthesis. Defaults to English. @@ -79,7 +79,7 @@ class GroqTTSService(TTSService): model_name: Optional[str] = None, voice_id: Optional[str] = None, sample_rate: Optional[int] = GROQ_SAMPLE_RATE, - settings: Optional[GroqTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize Groq TTS service. @@ -90,17 +90,17 @@ class GroqTTSService(TTSService): params: Additional input parameters for voice customization. .. deprecated:: 0.0.105 - Use ``settings=GroqTTSSettings(...)`` instead. + Use ``settings=GroqTTSService.Settings(...)`` instead. model_name: TTS model to use. .. deprecated:: 0.0.105 - Use ``settings=GroqTTSSettings(model=...)`` instead. + Use ``settings=GroqTTSService.Settings(model=...)`` instead. voice_id: Voice identifier to use. .. deprecated:: 0.0.105 - Use ``settings=GroqTTSSettings(voice=...)`` instead. + Use ``settings=GroqTTSService.Settings(voice=...)`` instead. sample_rate: Audio sample rate. Must be 48000 Hz for Groq TTS. settings: Runtime-updatable settings. When provided alongside deprecated @@ -111,7 +111,7 @@ class GroqTTSService(TTSService): logger.warning(f"Groq TTS only supports {self.GROQ_SAMPLE_RATE}Hz sample rate. ") # 1. Initialize default_settings with hardcoded defaults - default_settings = GroqTTSSettings( + default_settings = self.Settings( model="canopylabs/orpheus-v1-english", voice="autumn", language="en", @@ -120,15 +120,15 @@ class GroqTTSService(TTSService): # 2. Apply direct init arg overrides (deprecated) if model_name is not None: - _warn_deprecated_param("model_name", GroqTTSSettings, "model") + self._warn_init_param_moved_to_settings("model_name", "model") default_settings.model = model_name if voice_id is not None: - _warn_deprecated_param("voice_id", GroqTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", GroqTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: default_settings.language = str(params.language) if params.language else "en" default_settings.speed = params.speed diff --git a/src/pipecat/services/heygen/video.py b/src/pipecat/services/heygen/video.py index 2c42dfc6b..9a20f35ef 100644 --- a/src/pipecat/services/heygen/video.py +++ b/src/pipecat/services/heygen/video.py @@ -83,7 +83,7 @@ class HeyGenVideoService(AIService): """ Settings = HeyGenVideoSettings - _settings: HeyGenVideoSettings + _settings: Settings def __init__( self, @@ -92,7 +92,7 @@ class HeyGenVideoService(AIService): session: aiohttp.ClientSession, session_request: Optional[Union[LiveAvatarNewSessionRequest, NewSessionRequest]] = None, service_type: Optional[ServiceType] = None, - settings: Optional[HeyGenVideoSettings] = None, + settings: Optional[Settings] = None, **kwargs, ) -> None: """Initialize the HeyGen video service. diff --git a/src/pipecat/services/hume/tts.py b/src/pipecat/services/hume/tts.py index 5eb2db646..e591ebec2 100644 --- a/src/pipecat/services/hume/tts.py +++ b/src/pipecat/services/hume/tts.py @@ -25,7 +25,7 @@ from pipecat.frames.frames import ( TTSStoppedFrame, ) from pipecat.processors.frame_processor import FrameDirection -from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven from pipecat.services.tts_service import TTSService from pipecat.utils.tracing.service_decorators import traced_tts @@ -79,13 +79,13 @@ class HumeTTSService(TTSService): """ Settings = HumeTTSSettings - _settings: HumeTTSSettings + _settings: Settings class InputParams(BaseModel): """Optional synthesis parameters for Hume TTS. .. deprecated:: 0.0.105 - Use ``settings=HumeTTSSettings(...)`` instead. + Use ``settings=HumeTTSService.Settings(...)`` instead. Parameters: description: Natural-language acting directions (up to 100 characters). @@ -104,7 +104,7 @@ class HumeTTSService(TTSService): voice_id: Optional[str] = None, params: Optional[InputParams] = None, sample_rate: Optional[int] = HUME_SAMPLE_RATE, - settings: Optional[HumeTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ) -> None: """Initialize the HumeTTSService. @@ -114,12 +114,12 @@ class HumeTTSService(TTSService): voice_id: ID of the voice to use. Only voice IDs are supported; voice names are not. .. deprecated:: 0.0.105 - Use ``settings=HumeTTSSettings(voice=...)`` instead. + Use ``settings=HumeTTSService.Settings(voice=...)`` instead. params: Optional synthesis controls (acting instructions, speed, trailing silence). .. deprecated:: 0.0.105 - Use ``settings=HumeTTSSettings(...)`` instead. + Use ``settings=HumeTTSService.Settings(...)`` instead. sample_rate: Output sample rate for emitted PCM frames. Defaults to 48_000 (Hume). settings: Runtime-updatable settings. When provided alongside deprecated @@ -136,7 +136,7 @@ class HumeTTSService(TTSService): ) # 1. Initialize default_settings with hardcoded defaults - default_settings = HumeTTSSettings( + default_settings = self.Settings( model=None, voice=None, language=None, # Not applicable here @@ -147,12 +147,12 @@ class HumeTTSService(TTSService): # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: - _warn_deprecated_param("voice_id", HumeTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", HumeTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: default_settings.description = params.description default_settings.speed = params.speed @@ -242,7 +242,7 @@ class HumeTTSService(TTSService): """Runtime updates via key/value pair. .. deprecated:: 0.0.104 - Use ``TTSUpdateSettingsFrame(delta=HumeTTSSettings(...))`` instead. + Use ``TTSUpdateSettingsFrame(delta=HumeTTSService.Settings(...))`` instead. Args: key: The name of the setting to update. Recognized keys are: @@ -256,7 +256,7 @@ class HumeTTSService(TTSService): warnings.simplefilter("always") warnings.warn( "'update_setting' is deprecated, use " - "'TTSUpdateSettingsFrame(delta=HumeTTSSettings(...))' instead.", + "'TTSUpdateSettingsFrame(delta=self.Settings(...))' instead.", DeprecationWarning, stacklevel=2, ) @@ -274,7 +274,7 @@ class HumeTTSService(TTSService): kwargs["speed"] = None if value is None else float(value) elif key_l == "trailing_silence": kwargs["trailing_silence"] = None if value is None else float(value) - await self._update_settings(HumeTTSSettings(**kwargs)) + await self._update_settings(self.Settings(**kwargs)) @traced_tts async def run_tts(self, text: str, context_id: str) -> AsyncGenerator[Frame, None]: diff --git a/src/pipecat/services/inworld/tts.py b/src/pipecat/services/inworld/tts.py index dc09c0481..334c3c617 100644 --- a/src/pipecat/services/inworld/tts.py +++ b/src/pipecat/services/inworld/tts.py @@ -40,7 +40,7 @@ from pipecat import version as pipecat_version USER_AGENT = f"pipecat/{pipecat_version()}" from pydantic import BaseModel -from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven try: from websockets.asyncio.client import connect as websocket_connect @@ -101,13 +101,13 @@ class InworldHttpTTSService(TTSService): """ Settings = InworldTTSSettings - _settings: InworldTTSSettings + _settings: Settings class InputParams(BaseModel): """Input parameters for Inworld TTS configuration. .. deprecated:: 0.0.105 - Use ``InworldTTSSettings`` directly via the ``settings`` parameter instead. + Use ``InworldHttpTTSService.Settings`` directly via the ``settings`` parameter instead. Parameters: temperature: Temperature for speech synthesis. @@ -131,7 +131,7 @@ class InworldHttpTTSService(TTSService): encoding: str = "LINEAR16", timestamp_transport_strategy: Optional[Literal["ASYNC", "SYNC"]] = "ASYNC", params: Optional[InputParams] = None, - settings: Optional[InworldTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Inworld TTS service. @@ -142,12 +142,12 @@ class InworldHttpTTSService(TTSService): voice_id: ID of the voice to use for synthesis. .. deprecated:: 0.0.105 - Use ``settings=InworldTTSSettings(voice=...)`` instead. + Use ``settings=InworldHttpTTSService.Settings(voice=...)`` instead. model: ID of the model to use for synthesis. .. deprecated:: 0.0.105 - Use ``settings=InworldTTSSettings(model=...)`` instead. + Use ``settings=InworldHttpTTSService.Settings(model=...)`` instead. streaming: Whether to use streaming mode. sample_rate: Audio sample rate in Hz. @@ -157,14 +157,14 @@ class InworldHttpTTSService(TTSService): params: Input parameters for Inworld TTS configuration. .. deprecated:: 0.0.105 - Use ``settings=InworldTTSSettings(...)`` instead. + Use ``settings=InworldHttpTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional arguments passed to the parent class. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = InworldTTSSettings( + default_settings = self.Settings( model="inworld-tts-1.5-max", voice="Ashley", language=None, @@ -174,15 +174,15 @@ class InworldHttpTTSService(TTSService): # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: - _warn_deprecated_param("voice_id", InworldTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id if model is not None: - _warn_deprecated_param("model", InworldTTSSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", InworldTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: if params.speaking_rate is not None: default_settings.speaking_rate = params.speaking_rate @@ -489,13 +489,13 @@ class InworldTTSService(WebsocketTTSService): """ Settings = InworldTTSSettings - _settings: InworldTTSSettings + _settings: Settings class InputParams(BaseModel): """Input parameters for Inworld WebSocket TTS configuration. .. deprecated:: 0.0.105 - Use ``InworldTTSSettings`` directly via the ``settings`` parameter instead. + Use ``InworldTTSService.Settings`` directly via the ``settings`` parameter instead. Parameters: temperature: Temperature for speech synthesis. @@ -532,7 +532,7 @@ class InworldTTSService(WebsocketTTSService): apply_text_normalization: Optional[str] = None, timestamp_transport_strategy: Optional[Literal["ASYNC", "SYNC"]] = "ASYNC", params: Optional[InputParams] = None, - settings: Optional[InworldTTSSettings] = None, + settings: Optional[Settings] = None, aggregate_sentences: Optional[bool] = None, text_aggregation_mode: Optional[TextAggregationMode] = None, append_trailing_space: bool = True, @@ -545,12 +545,12 @@ class InworldTTSService(WebsocketTTSService): voice_id: ID of the voice to use for synthesis. .. deprecated:: 0.0.105 - Use ``settings=InworldTTSSettings(voice=...)`` instead. + Use ``settings=InworldTTSService.Settings(voice=...)`` instead. model: ID of the model to use for synthesis. .. deprecated:: 0.0.105 - Use ``settings=InworldTTSSettings(model=...)`` instead. + Use ``settings=InworldTTSService.Settings(model=...)`` instead. url: URL of the Inworld WebSocket API. sample_rate: Audio sample rate in Hz. @@ -564,7 +564,7 @@ class InworldTTSService(WebsocketTTSService): params: Input parameters for Inworld WebSocket TTS configuration. .. deprecated:: 0.0.105 - Use ``settings=InworldTTSSettings(...)`` instead. + Use ``settings=InworldTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -582,7 +582,7 @@ class InworldTTSService(WebsocketTTSService): auto_mode = True if aggregate_sentences is None else aggregate_sentences # 1. Initialize default_settings with hardcoded defaults - default_settings = InworldTTSSettings( + default_settings = self.Settings( model="inworld-tts-1.5-max", voice="Ashley", language=None, @@ -592,17 +592,17 @@ class InworldTTSService(WebsocketTTSService): # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: - _warn_deprecated_param("voice_id", InworldTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id if model is not None: - _warn_deprecated_param("model", InworldTTSSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. Apply params overrides — only if settings not provided _buffer_max_delay_ms = None _buffer_char_threshold = None if params is not None: - _warn_deprecated_param("params", InworldTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: if params.speaking_rate is not None: default_settings.speaking_rate = params.speaking_rate diff --git a/src/pipecat/services/kokoro/tts.py b/src/pipecat/services/kokoro/tts.py index 98f181c6f..4756d4e74 100644 --- a/src/pipecat/services/kokoro/tts.py +++ b/src/pipecat/services/kokoro/tts.py @@ -21,7 +21,7 @@ from pipecat.frames.frames import ( Frame, TTSAudioRawFrame, ) -from pipecat.services.settings import TTSSettings, _warn_deprecated_param +from pipecat.services.settings import TTSSettings from pipecat.services.tts_service import TTSService from pipecat.transcriptions.language import Language, resolve_language from pipecat.utils.tracing.service_decorators import traced_tts @@ -102,13 +102,13 @@ class KokoroTTSService(TTSService): """ Settings = KokoroTTSSettings - _settings: KokoroTTSSettings + _settings: Settings class InputParams(BaseModel): """Input parameters for Kokoro TTS configuration. .. deprecated:: 0.0.105 - Use ``KokoroTTSSettings`` directly via the ``settings`` parameter instead. + Use ``KokoroTTSService.Settings`` directly via the ``settings`` parameter instead. Parameters: language: Language to use for synthesis. @@ -123,7 +123,7 @@ class KokoroTTSService(TTSService): model_path: Optional[str] = None, voices_path: Optional[str] = None, params: Optional[InputParams] = None, - settings: Optional[KokoroTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Kokoro TTS service. @@ -132,14 +132,14 @@ class KokoroTTSService(TTSService): voice_id: Voice identifier to use for synthesis. .. deprecated:: 0.0.105 - Use ``settings=KokoroTTSSettings(voice=...)`` instead. + Use ``settings=KokoroTTSService.Settings(voice=...)`` instead. model_path: Path to the kokoro ONNX model file. Defaults to auto-downloaded file. voices_path: Path to the voices binary file. Defaults to auto-downloaded file. params: Configuration parameters for synthesis. .. deprecated:: 0.0.105 - Use ``settings=KokoroTTSSettings(...)`` instead. + Use ``settings=KokoroTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -147,22 +147,22 @@ class KokoroTTSService(TTSService): """ # 1. Initialize default_settings with hardcoded defaults - default_settings = KokoroTTSSettings( + default_settings = self.Settings( model=None, voice=None, - language=language_to_kokoro_language(Language.EN), + language=Language.EN, ) # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: - _warn_deprecated_param("voice_id", KokoroTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", KokoroTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: - default_settings.language = language_to_kokoro_language(params.language) + default_settings.language = params.language # 4. Apply settings delta (canonical API, always wins) if settings is not None: diff --git a/src/pipecat/services/llm_service.py b/src/pipecat/services/llm_service.py index 44858038d..a479fcfc6 100644 --- a/src/pipecat/services/llm_service.py +++ b/src/pipecat/services/llm_service.py @@ -244,7 +244,10 @@ class LLMService(UserTurnCompletionLLMServiceMixin, AIService): return self.get_llm_adapter().create_llm_specific_message(message) async def run_inference( - self, context: LLMContext | OpenAILLMContext, max_tokens: Optional[int] = None + self, + context: LLMContext | OpenAILLMContext, + max_tokens: Optional[int] = None, + system_instruction: Optional[str] = None, ) -> Optional[str]: """Run a one-shot, out-of-band (i.e. out-of-pipeline) inference with the given LLM context. @@ -254,6 +257,8 @@ class LLMService(UserTurnCompletionLLMServiceMixin, AIService): context: The LLM context containing conversation history. max_tokens: Optional maximum number of tokens to generate. If provided, overrides the service's default max_tokens/max_completion_tokens setting. + system_instruction: Optional system instruction to use for this inference. + If provided, overrides any system instruction in the context. Returns: The LLM's response as a string, or None if no response is generated. @@ -398,7 +403,9 @@ class LLMService(UserTurnCompletionLLMServiceMixin, AIService): elif isinstance(frame, LLMConfigureOutputFrame): self._skip_tts = frame.skip_tts elif isinstance(frame, LLMUpdateSettingsFrame): - if frame.delta is not None: + if frame.service is not None and frame.service is not self: + await self.push_frame(frame, direction) + elif frame.delta is not None: await self._update_settings(frame.delta) elif frame.settings: # Backward-compatible path: convert legacy dict to settings object. @@ -535,23 +542,17 @@ class LLMService(UserTurnCompletionLLMServiceMixin, AIService): # Create summary context transcript = LLMContextSummarizationUtil.format_messages_for_summary(result.messages) - prompt_messages = [ - { - "role": "system", - "content": frame.summarization_prompt, - }, - { - "role": "user", - "content": f"Conversation history:\n{transcript}", - }, - ] - summary_context = LLMContext(messages=prompt_messages) + summary_context = LLMContext( + messages=[{"role": "user", "content": f"Conversation history:\n{transcript}"}] + ) # Generate summary using run_inference # This will be overridden by each LLM service implementation try: summary_text = await self.run_inference( - summary_context, max_tokens=frame.target_context_tokens + summary_context, + max_tokens=frame.target_context_tokens, + system_instruction=frame.summarization_prompt, ) except NotImplementedError: raise RuntimeError( diff --git a/src/pipecat/services/lmnt/tts.py b/src/pipecat/services/lmnt/tts.py index 46e5e4697..29d0b60ca 100644 --- a/src/pipecat/services/lmnt/tts.py +++ b/src/pipecat/services/lmnt/tts.py @@ -22,7 +22,7 @@ from pipecat.frames.frames import ( TTSStoppedFrame, ) from pipecat.processors.frame_processor import FrameDirection -from pipecat.services.settings import TTSSettings, _warn_deprecated_param +from pipecat.services.settings import TTSSettings from pipecat.services.tts_service import InterruptibleTTSService from pipecat.transcriptions.language import Language, resolve_language from pipecat.utils.tracing.service_decorators import traced_tts @@ -89,7 +89,7 @@ class LmntTTSService(InterruptibleTTSService): """ Settings = LmntTTSSettings - _settings: LmntTTSSettings + _settings: Settings def __init__( self, @@ -100,7 +100,7 @@ class LmntTTSService(InterruptibleTTSService): language: Language = Language.EN, output_format: str = "pcm_s16le", model: Optional[str] = None, - settings: Optional[LmntTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the LMNT TTS service. @@ -110,34 +110,41 @@ class LmntTTSService(InterruptibleTTSService): voice_id: ID of the voice to use for synthesis. .. deprecated:: 0.0.105 - Use ``settings=LmntTTSSettings(voice=...)`` instead. + Use ``settings=LmntTTSService.Settings(voice=...)`` instead. sample_rate: Audio sample rate. If None, uses default. language: Language for synthesis. Defaults to English. + + .. deprecated:: 0.0.106 + Use ``settings=LmntTTSService.Settings(language=...)`` instead. + output_format: Audio output format. One of "pcm_s16le", "pcm_f32le", "mp3", "ulaw", "webm". Defaults to "pcm_s16le". model: TTS model to use. .. deprecated:: 0.0.105 - Use ``settings=LmntTTSSettings(model=...)`` instead. + Use ``settings=LmntTTSService.Settings(model=...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional arguments passed to parent InterruptibleTTSService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = LmntTTSSettings( + default_settings = self.Settings( model="aurora", voice=None, - language=self.language_to_service_language(language), + language=Language.EN, ) # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: - _warn_deprecated_param("voice_id", LmntTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id + if language is not None: + self._warn_init_param_moved_to_settings("language", "language") + default_settings.language = language if model is not None: - _warn_deprecated_param("model", LmntTTSSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. (No step 3, as there's no params object to apply) @@ -237,7 +244,7 @@ class LmntTTSService(InterruptibleTTSService): """Apply a settings delta. Args: - delta: A :class:`TTSSettings` (or ``LmntTTSSettings``) delta. + delta: A :class:`TTSSettings` (or ``LmntTTSService.Settings``) delta. Returns: Dict mapping changed field names to their previous values. diff --git a/src/pipecat/services/minimax/tts.py b/src/pipecat/services/minimax/tts.py index f62e8d46b..d41fb6255 100644 --- a/src/pipecat/services/minimax/tts.py +++ b/src/pipecat/services/minimax/tts.py @@ -24,7 +24,7 @@ from pipecat.frames.frames import ( StartFrame, TTSAudioRawFrame, ) -from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven from pipecat.services.tts_service import TTSService from pipecat.transcriptions.language import Language, resolve_language from pipecat.utils.tracing.service_decorators import traced_tts @@ -141,13 +141,13 @@ class MiniMaxHttpTTSService(TTSService): """ Settings = MiniMaxTTSSettings - _settings: MiniMaxTTSSettings + _settings: Settings class InputParams(BaseModel): """Configuration parameters for MiniMax TTS. .. deprecated:: 0.0.105 - Use ``MiniMaxTTSSettings`` directly via the ``settings`` parameter instead. + Use ``MiniMaxHttpTTSService.Settings`` directly via the ``settings`` parameter instead. Parameters: language: Language for TTS generation. Supports 40 languages. @@ -190,7 +190,7 @@ class MiniMaxHttpTTSService(TTSService): sample_rate: Optional[int] = None, stream: bool = True, params: Optional[InputParams] = None, - settings: Optional[MiniMaxTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the MiniMax TTS service. @@ -208,12 +208,12 @@ class MiniMaxHttpTTSService(TTSService): "speech-01-hd", "speech-01-turbo". .. deprecated:: 0.0.105 - Use ``settings=MiniMaxTTSSettings(model=...)`` instead. + Use ``settings=MiniMaxHttpTTSService.Settings(model=...)`` instead. voice_id: Voice identifier. Defaults to "Calm_Woman". .. deprecated:: 0.0.105 - Use ``settings=MiniMaxTTSSettings(voice=...)`` instead. + Use ``settings=MiniMaxHttpTTSService.Settings(voice=...)`` instead. aiohttp_session: aiohttp.ClientSession for API communication. sample_rate: Output audio sample rate in Hz. If None, uses pipeline default. @@ -221,14 +221,14 @@ class MiniMaxHttpTTSService(TTSService): params: Additional configuration parameters. .. deprecated:: 0.0.105 - Use ``settings=MiniMaxTTSSettings(...)`` instead. + Use ``settings=MiniMaxHttpTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional arguments passed to parent TTSService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = MiniMaxTTSSettings( + default_settings = self.Settings( model="speech-02-turbo", voice="Calm_Woman", language=None, @@ -243,15 +243,15 @@ class MiniMaxHttpTTSService(TTSService): # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", MiniMaxTTSSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model if voice_id is not None: - _warn_deprecated_param("voice_id", MiniMaxTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", MiniMaxTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: default_settings.speed = params.speed default_settings.volume = params.volume diff --git a/src/pipecat/services/mistral/llm.py b/src/pipecat/services/mistral/llm.py index 9e01a76b5..063dac3aa 100644 --- a/src/pipecat/services/mistral/llm.py +++ b/src/pipecat/services/mistral/llm.py @@ -14,13 +14,12 @@ from openai.types.chat import ChatCompletionMessageParam from pipecat.adapters.services.open_ai_adapter import OpenAILLMInvocationParams from pipecat.frames.frames import FunctionCallFromLLM -from pipecat.services.openai.base_llm import OpenAILLMSettings +from pipecat.services.openai.base_llm import BaseOpenAILLMService from pipecat.services.openai.llm import OpenAILLMService -from pipecat.services.settings import _warn_deprecated_param @dataclass -class MistralLLMSettings(OpenAILLMSettings): +class MistralLLMSettings(BaseOpenAILLMService.Settings): """Settings for MistralLLMService.""" pass @@ -34,7 +33,7 @@ class MistralLLMService(OpenAILLMService): """ Settings = MistralLLMSettings - _settings: MistralLLMSettings + _settings: Settings def __init__( self, @@ -42,7 +41,7 @@ class MistralLLMService(OpenAILLMService): api_key: str, base_url: str = "https://api.mistral.ai/v1", model: Optional[str] = None, - settings: Optional[MistralLLMSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Mistral LLM service. @@ -53,18 +52,18 @@ class MistralLLMService(OpenAILLMService): model: The model identifier to use. Defaults to "mistral-small-latest". .. deprecated:: 0.0.105 - Use ``settings=OpenAILLMSettings(model=...)`` instead. + Use ``settings=MistralLLMService.Settings(model=...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional keyword arguments passed to OpenAILLMService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = MistralLLMSettings(model="mistral-small-latest") + default_settings = self.Settings(model="mistral-small-latest") # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", MistralLLMSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. (No step 3, as there's no params object to apply) @@ -237,6 +236,10 @@ class MistralLLMService(OpenAILLMService): # Prepend system instruction if set if self._settings.system_instruction: messages = params.get("messages", []) + if messages and messages[0].get("role") == "system": + logger.warning( + f"{self}: Both system_instruction and an initial system message in context are set. This may be unintended." + ) params["messages"] = [ {"role": "system", "content": self._settings.system_instruction} ] + messages diff --git a/src/pipecat/services/moondream/vision.py b/src/pipecat/services/moondream/vision.py index abc344fc5..6eeff19cd 100644 --- a/src/pipecat/services/moondream/vision.py +++ b/src/pipecat/services/moondream/vision.py @@ -25,7 +25,7 @@ from pipecat.frames.frames import ( VisionFullResponseStartFrame, VisionTextFrame, ) -from pipecat.services.settings import VisionSettings, _warn_deprecated_param +from pipecat.services.settings import VisionSettings from pipecat.services.vision_service import VisionService try: @@ -80,7 +80,7 @@ class MoondreamService(VisionService): """ Settings = MoondreamSettings - _settings: MoondreamSettings + _settings: Settings def __init__( self, @@ -88,7 +88,7 @@ class MoondreamService(VisionService): model: Optional[str] = None, revision="2025-01-09", use_cpu=False, - settings: Optional[MoondreamSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Moondream service. @@ -97,7 +97,7 @@ class MoondreamService(VisionService): model: Hugging Face model identifier for the Moondream model. .. deprecated:: 0.0.105 - Use ``settings=MoondreamSettings(model=...)`` instead. + Use ``settings=MoondreamService.Settings(model=...)`` instead. revision: Specific model revision to use. use_cpu: Whether to force CPU usage instead of hardware acceleration. @@ -106,11 +106,11 @@ class MoondreamService(VisionService): **kwargs: Additional arguments passed to the parent VisionService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = MoondreamSettings(model="vikhyatk/moondream2") + default_settings = self.Settings(model="vikhyatk/moondream2") # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", MoondreamSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 4. Apply settings delta (canonical API, always wins) diff --git a/src/pipecat/services/neuphonic/tts.py b/src/pipecat/services/neuphonic/tts.py index abc33c37e..6fb9d3e28 100644 --- a/src/pipecat/services/neuphonic/tts.py +++ b/src/pipecat/services/neuphonic/tts.py @@ -33,7 +33,7 @@ from pipecat.frames.frames import ( TTSStoppedFrame, ) from pipecat.processors.frame_processor import FrameDirection -from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven from pipecat.services.tts_service import InterruptibleTTSService, TextAggregationMode, TTSService from pipecat.transcriptions.language import Language, resolve_language from pipecat.utils.tracing.service_decorators import traced_tts @@ -92,13 +92,13 @@ class NeuphonicTTSService(InterruptibleTTSService): """ Settings = NeuphonicTTSSettings - _settings: NeuphonicTTSSettings + _settings: Settings class InputParams(BaseModel): """Input parameters for Neuphonic TTS configuration. .. deprecated:: 0.0.105 - Use ``settings=NeuphonicTTSSettings(...)`` instead. + Use ``settings=NeuphonicTTSService.Settings(...)`` instead. Parameters: language: Language for synthesis. Defaults to English. @@ -117,7 +117,7 @@ class NeuphonicTTSService(InterruptibleTTSService): sample_rate: Optional[int] = 22050, encoding: str = "pcm_linear", params: Optional[InputParams] = None, - settings: Optional[NeuphonicTTSSettings] = None, + settings: Optional[Settings] = None, aggregate_sentences: Optional[bool] = None, text_aggregation_mode: Optional[TextAggregationMode] = None, **kwargs, @@ -129,7 +129,7 @@ class NeuphonicTTSService(InterruptibleTTSService): voice_id: ID of the voice to use for synthesis. .. deprecated:: 0.0.105 - Use ``settings=NeuphonicTTSSettings(voice=...)`` instead. + Use ``settings=NeuphonicTTSService.Settings(voice=...)`` instead. url: WebSocket URL for the Neuphonic API. sample_rate: Audio sample rate in Hz. Defaults to 22050. @@ -137,7 +137,7 @@ class NeuphonicTTSService(InterruptibleTTSService): params: Additional input parameters for TTS configuration. .. deprecated:: 0.0.105 - Use ``settings=NeuphonicTTSSettings(...)`` instead. + Use ``settings=NeuphonicTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -150,24 +150,24 @@ class NeuphonicTTSService(InterruptibleTTSService): **kwargs: Additional arguments passed to parent InterruptibleTTSService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = NeuphonicTTSSettings( + default_settings = self.Settings( model=None, voice=None, - language=self.language_to_service_language(Language.EN), + language=Language.EN, speed=1.0, ) # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: - _warn_deprecated_param("voice_id", NeuphonicTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", NeuphonicTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: if params.language is not None: - default_settings.language = self.language_to_service_language(params.language) + default_settings.language = params.language if params.speed is not None: default_settings.speed = params.speed @@ -432,13 +432,13 @@ class NeuphonicHttpTTSService(TTSService): """ Settings = NeuphonicTTSSettings - _settings: NeuphonicTTSSettings + _settings: Settings class InputParams(BaseModel): """Input parameters for Neuphonic HTTP TTS configuration. .. deprecated:: 0.0.105 - Use ``settings=NeuphonicTTSSettings(...)`` instead. + Use ``settings=NeuphonicHttpTTSService.Settings(...)`` instead. Parameters: language: Language for synthesis. Defaults to English. @@ -458,7 +458,7 @@ class NeuphonicHttpTTSService(TTSService): sample_rate: Optional[int] = 22050, encoding: Optional[str] = "pcm_linear", params: Optional[InputParams] = None, - settings: Optional[NeuphonicTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Neuphonic HTTP TTS service. @@ -468,7 +468,7 @@ class NeuphonicHttpTTSService(TTSService): voice_id: ID of the voice to use for synthesis. .. deprecated:: 0.0.105 - Use ``settings=NeuphonicTTSSettings(voice=...)`` instead. + Use ``settings=NeuphonicHttpTTSService.Settings(voice=...)`` instead. aiohttp_session: Shared aiohttp session for HTTP requests. url: Base URL for the Neuphonic HTTP API. @@ -477,31 +477,31 @@ class NeuphonicHttpTTSService(TTSService): params: Additional input parameters for TTS configuration. .. deprecated:: 0.0.105 - Use ``settings=NeuphonicTTSSettings(...)`` instead. + Use ``settings=NeuphonicHttpTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional arguments passed to parent TTSService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = NeuphonicTTSSettings( + default_settings = self.Settings( model=None, voice=None, - language=self.language_to_service_language(Language.EN), + language=Language.EN, speed=1.0, ) # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: - _warn_deprecated_param("voice_id", NeuphonicTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", NeuphonicTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: if params.language is not None: - default_settings.language = self.language_to_service_language(params.language) + default_settings.language = params.language if params.speed is not None: default_settings.speed = params.speed diff --git a/src/pipecat/services/nvidia/llm.py b/src/pipecat/services/nvidia/llm.py index 17490f513..66bbd4402 100644 --- a/src/pipecat/services/nvidia/llm.py +++ b/src/pipecat/services/nvidia/llm.py @@ -16,13 +16,12 @@ from typing import Optional from pipecat.metrics.metrics import LLMTokenUsage from pipecat.processors.aggregators.llm_context import LLMContext from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext -from pipecat.services.openai.base_llm import OpenAILLMSettings +from pipecat.services.openai.base_llm import BaseOpenAILLMService from pipecat.services.openai.llm import OpenAILLMService -from pipecat.services.settings import _warn_deprecated_param @dataclass -class NvidiaLLMSettings(OpenAILLMSettings): +class NvidiaLLMSettings(BaseOpenAILLMService.Settings): """Settings for NvidiaLLMService.""" pass @@ -37,7 +36,7 @@ class NvidiaLLMService(OpenAILLMService): """ Settings = NvidiaLLMSettings - _settings: NvidiaLLMSettings + _settings: Settings def __init__( self, @@ -45,7 +44,7 @@ class NvidiaLLMService(OpenAILLMService): api_key: str, base_url: str = "https://integrate.api.nvidia.com/v1", model: Optional[str] = None, - settings: Optional[NvidiaLLMSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the NvidiaLLMService. @@ -57,18 +56,18 @@ class NvidiaLLMService(OpenAILLMService): "nvidia/llama-3.1-nemotron-70b-instruct". .. deprecated:: 0.0.105 - Use ``settings=OpenAILLMSettings(model=...)`` instead. + Use ``settings=NvidiaLLMService.Settings(model=...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional keyword arguments passed to OpenAILLMService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = NvidiaLLMSettings(model="nvidia/llama-3.1-nemotron-70b-instruct") + default_settings = self.Settings(model="nvidia/llama-3.1-nemotron-70b-instruct") # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", NvidiaLLMSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. (No step 3, as there's no params object to apply) diff --git a/src/pipecat/services/nvidia/stt.py b/src/pipecat/services/nvidia/stt.py index 5935e7846..50a654191 100644 --- a/src/pipecat/services/nvidia/stt.py +++ b/src/pipecat/services/nvidia/stt.py @@ -23,7 +23,7 @@ from pipecat.frames.frames import ( StartFrame, TranscriptionFrame, ) -from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven from pipecat.services.stt_latency import NVIDIA_TTFS_P99 from pipecat.services.stt_service import SegmentedSTTService, STTService from pipecat.transcriptions.language import Language, resolve_language @@ -126,13 +126,13 @@ class NvidiaSTTService(STTService): """ Settings = NvidiaSTTSettings - _settings: NvidiaSTTSettings + _settings: Settings class InputParams(BaseModel): """Configuration parameters for NVIDIA Riva STT service. .. deprecated:: 0.0.105 - Use ``settings=NvidiaSTTSettings(...)`` instead. + Use ``settings=NvidiaSTTService.Settings(...)`` instead. Parameters: language: Target language for transcription. Defaults to EN_US. @@ -152,7 +152,7 @@ class NvidiaSTTService(STTService): sample_rate: Optional[int] = None, params: Optional[InputParams] = None, use_ssl: bool = True, - settings: Optional[NvidiaSTTSettings] = None, + settings: Optional[Settings] = None, ttfs_p99_latency: Optional[float] = NVIDIA_TTFS_P99, **kwargs, ): @@ -166,7 +166,7 @@ class NvidiaSTTService(STTService): params: Additional configuration parameters for NVIDIA Riva. .. deprecated:: 0.0.105 - Use ``settings=NvidiaSTTSettings(...)`` instead. + Use ``settings=NvidiaSTTService.Settings(...)`` instead. use_ssl: Whether to use SSL for the NVIDIA Riva server. Defaults to True. settings: Runtime-updatable settings. When provided alongside deprecated @@ -176,7 +176,7 @@ class NvidiaSTTService(STTService): **kwargs: Additional arguments passed to STTService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = NvidiaSTTSettings( + default_settings = self.Settings( model=model_function_map.get("model_name"), language=Language.EN_US, ) @@ -185,7 +185,7 @@ class NvidiaSTTService(STTService): # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", NvidiaSTTSettings) + self._warn_init_param_moved_to_settings("params") if not settings: default_settings.language = params.language @@ -441,13 +441,13 @@ class NvidiaSegmentedSTTService(SegmentedSTTService): """ Settings = NvidiaSegmentedSTTSettings - _settings: NvidiaSegmentedSTTSettings + _settings: Settings class InputParams(BaseModel): """Configuration parameters for NVIDIA Riva segmented STT service. .. deprecated:: 0.0.105 - Use ``settings=NvidiaSegmentedSTTSettings(...)`` instead. + Use ``settings=NvidiaSegmentedSTTService.Settings(...)`` instead. Parameters: language: Target language for transcription. Defaults to EN_US. @@ -477,7 +477,7 @@ class NvidiaSegmentedSTTService(SegmentedSTTService): sample_rate: Optional[int] = None, params: Optional[InputParams] = None, use_ssl: bool = True, - settings: Optional[NvidiaSegmentedSTTSettings] = None, + settings: Optional[Settings] = None, ttfs_p99_latency: Optional[float] = NVIDIA_TTFS_P99, **kwargs, ): @@ -491,7 +491,7 @@ class NvidiaSegmentedSTTService(SegmentedSTTService): params: Additional configuration parameters for NVIDIA Riva .. deprecated:: 0.0.105 - Use ``settings=NvidiaSegmentedSTTSettings(...)`` instead. + Use ``settings=NvidiaSegmentedSTTService.Settings(...)`` instead. use_ssl: Whether to use SSL for the NVIDIA Riva server. Defaults to True. settings: Runtime-updatable settings. When provided alongside deprecated @@ -501,9 +501,9 @@ class NvidiaSegmentedSTTService(SegmentedSTTService): **kwargs: Additional arguments passed to SegmentedSTTService """ # 1. Initialize default_settings with hardcoded defaults - default_settings = NvidiaSegmentedSTTSettings( + default_settings = self.Settings( model=model_function_map.get("model_name"), - language=language_to_nvidia_riva_language(Language.EN_US) or "en-US", + language=Language.EN_US, profanity_filter=False, automatic_punctuation=True, verbatim_transcripts=False, @@ -515,11 +515,9 @@ class NvidiaSegmentedSTTService(SegmentedSTTService): # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", NvidiaSegmentedSTTSettings) + self._warn_init_param_moved_to_settings("params") if not settings: - default_settings.language = ( - language_to_nvidia_riva_language(params.language or Language.EN_US) or "en-US" - ) + default_settings.language = params.language or Language.EN_US default_settings.profanity_filter = params.profanity_filter default_settings.automatic_punctuation = params.automatic_punctuation default_settings.verbatim_transcripts = params.verbatim_transcripts @@ -641,7 +639,7 @@ class NvidiaSegmentedSTTService(SegmentedSTTService): """Apply a settings delta and sync internal state. Args: - delta: A :class:`STTSettings` (or ``NvidiaSegmentedSTTSettings``) delta. + delta: A :class:`STTSettings` (or ``NvidiaSegmentedSTTService.Settings``) delta. Returns: Dict mapping changed field names to their previous values. diff --git a/src/pipecat/services/nvidia/tts.py b/src/pipecat/services/nvidia/tts.py index ad39d505b..5e7a20a3c 100644 --- a/src/pipecat/services/nvidia/tts.py +++ b/src/pipecat/services/nvidia/tts.py @@ -29,7 +29,7 @@ from pipecat.frames.frames import ( StartFrame, TTSAudioRawFrame, ) -from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven from pipecat.services.tts_service import TTSService from pipecat.transcriptions.language import Language @@ -62,13 +62,13 @@ class NvidiaTTSService(TTSService): """ Settings = NvidiaTTSSettings - _settings: NvidiaTTSSettings + _settings: Settings class InputParams(BaseModel): """Input parameters for Riva TTS configuration. .. deprecated:: 0.0.105 - Use ``NvidiaTTSSettings`` directly via the ``settings`` parameter instead. + Use ``NvidiaTTSService.Settings`` directly via the ``settings`` parameter instead. Parameters: language: Language code for synthesis. Defaults to US English. @@ -90,7 +90,7 @@ class NvidiaTTSService(TTSService): "model_name": "magpie-tts-multilingual", }, params: Optional[InputParams] = None, - settings: Optional[NvidiaTTSSettings] = None, + settings: Optional[Settings] = None, use_ssl: bool = True, **kwargs, ): @@ -102,14 +102,14 @@ class NvidiaTTSService(TTSService): voice_id: Voice model identifier. Defaults to multilingual Aria voice. .. deprecated:: 0.0.105 - Use ``settings=NvidiaTTSSettings(voice=...)`` instead. + Use ``settings=NvidiaTTSService.Settings(voice=...)`` instead. sample_rate: Audio sample rate. If None, uses service default. model_function_map: Dictionary containing function_id and model_name for the TTS model. params: Additional configuration parameters for TTS synthesis. .. deprecated:: 0.0.105 - Use ``settings=NvidiaTTSSettings(...)`` instead. + Use ``settings=NvidiaTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -117,7 +117,7 @@ class NvidiaTTSService(TTSService): **kwargs: Additional arguments passed to parent TTSService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = NvidiaTTSSettings( + default_settings = self.Settings( model=model_function_map.get("model_name"), voice="Magpie-Multilingual.EN-US.Aria", language=Language.EN_US, @@ -126,12 +126,12 @@ class NvidiaTTSService(TTSService): # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: - _warn_deprecated_param("voice_id", NvidiaTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", NvidiaTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: if params.language is not None: default_settings.language = params.language @@ -186,7 +186,7 @@ class NvidiaTTSService(TTSService): stacklevel=2, ) - async def _update_settings(self, delta: NvidiaTTSSettings) -> dict[str, Any]: + async def _update_settings(self, delta: Settings) -> dict[str, Any]: """Apply a settings delta. Settings are stored but not applied to the active connection. diff --git a/src/pipecat/services/ollama/llm.py b/src/pipecat/services/ollama/llm.py index f4d138d78..a24ebfcaf 100644 --- a/src/pipecat/services/ollama/llm.py +++ b/src/pipecat/services/ollama/llm.py @@ -11,13 +11,12 @@ from typing import Optional from loguru import logger -from pipecat.services.openai.base_llm import OpenAILLMSettings +from pipecat.services.openai.base_llm import BaseOpenAILLMService from pipecat.services.openai.llm import OpenAILLMService -from pipecat.services.settings import _warn_deprecated_param @dataclass -class OllamaLLMSettings(OpenAILLMSettings): +class OllamaLLMSettings(BaseOpenAILLMService.Settings): """Settings for OLLamaLLMService.""" pass @@ -31,14 +30,14 @@ class OLLamaLLMService(OpenAILLMService): """ Settings = OllamaLLMSettings - _settings: OllamaLLMSettings + _settings: Settings def __init__( self, *, model: Optional[str] = None, base_url: str = "http://localhost:11434/v1", - settings: Optional[OllamaLLMSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize OLLama LLM service. @@ -47,7 +46,7 @@ class OLLamaLLMService(OpenAILLMService): model: The OLLama model to use. Defaults to "llama2". .. deprecated:: 0.0.105 - Use ``settings=OpenAILLMSettings(model=...)`` instead. + Use ``settings=OLLamaLLMService.Settings(model=...)`` instead. base_url: The base URL for the OLLama API endpoint. Defaults to "http://localhost:11434/v1". @@ -56,11 +55,11 @@ class OLLamaLLMService(OpenAILLMService): **kwargs: Additional keyword arguments passed to OpenAILLMService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = OllamaLLMSettings(model="llama2") + default_settings = self.Settings(model="llama2") # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", OllamaLLMSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. (No step 3, as there's no params object to apply) diff --git a/src/pipecat/services/openai/__init__.py b/src/pipecat/services/openai/__init__.py index e182264b1..3caa3c3cb 100644 --- a/src/pipecat/services/openai/__init__.py +++ b/src/pipecat/services/openai/__init__.py @@ -11,6 +11,7 @@ from pipecat.services import DeprecatedModuleProxy from .image import * from .llm import * from .realtime import * +from .responses.llm import * from .stt import * from .tts import * diff --git a/src/pipecat/services/openai/base_llm.py b/src/pipecat/services/openai/base_llm.py index 5466c75a5..eb8ce3cc6 100644 --- a/src/pipecat/services/openai/base_llm.py +++ b/src/pipecat/services/openai/base_llm.py @@ -69,13 +69,13 @@ class BaseOpenAILLMService(LLMService): """ Settings = OpenAILLMSettings - _settings: OpenAILLMSettings + _settings: Settings class InputParams(BaseModel): """Input parameters for OpenAI model configuration. .. deprecated:: 0.0.105 - Use ``settings=OpenAILLMSettings(...)`` instead of + Use ``settings=BaseOpenAILLMService.Settings(...)`` instead of ``params=InputParams(...)``. Parameters: @@ -119,7 +119,7 @@ class BaseOpenAILLMService(LLMService): default_headers: Optional[Mapping[str, str]] = None, service_tier: Optional[str] = None, params: Optional[InputParams] = None, - settings: Optional[OpenAILLMSettings] = None, + settings: Optional[Settings] = None, retry_timeout_secs: Optional[float] = 5.0, retry_on_timeout: Optional[bool] = False, **kwargs, @@ -130,7 +130,7 @@ class BaseOpenAILLMService(LLMService): model: The OpenAI model name to use (e.g., "gpt-4.1", "gpt-4o"). .. deprecated:: 0.0.105 - Use ``settings=OpenAILLMSettings(model=...)`` instead. + Use ``settings=BaseOpenAILLMService.Settings(model=...)`` instead. api_key: OpenAI API key. If None, uses environment variable. base_url: Custom base URL for OpenAI API. If None, uses default. @@ -141,7 +141,7 @@ class BaseOpenAILLMService(LLMService): params: Input parameters for model configuration and behavior. .. deprecated:: 0.0.105 - Use ``settings=OpenAILLMSettings(...)`` instead. + Use ``settings=BaseOpenAILLMService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -150,8 +150,8 @@ class BaseOpenAILLMService(LLMService): **kwargs: Additional arguments passed to the parent LLMService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = OpenAILLMSettings( - model="gpt-4o", + default_settings = self.Settings( + model="gpt-4.1", system_instruction=None, frequency_penalty=NOT_GIVEN, presence_penalty=NOT_GIVEN, @@ -327,13 +327,12 @@ class BaseOpenAILLMService(LLMService): params.update(self._settings.extra) - # Prepend system instruction from constructor, replacing any context system message + # Prepend system instruction from constructor if self._settings.system_instruction: messages = params.get("messages", []) if messages and messages[0].get("role") == "system": logger.warning( - f"{self}: Both system_instruction and a system message in context are set." - " Using system_instruction." + f"{self}: Both system_instruction and an initial system message in context are set. This may be unintended." ) params["messages"] = [ {"role": "system", "content": self._settings.system_instruction} @@ -342,7 +341,10 @@ class BaseOpenAILLMService(LLMService): return params async def run_inference( - self, context: LLMContext | OpenAILLMContext, max_tokens: Optional[int] = None + self, + context: LLMContext | OpenAILLMContext, + max_tokens: Optional[int] = None, + system_instruction: Optional[str] = None, ) -> Optional[str]: """Run a one-shot, out-of-band (i.e. out-of-pipeline) inference with the given LLM context. @@ -350,6 +352,8 @@ class BaseOpenAILLMService(LLMService): context: The LLM context containing conversation history. max_tokens: Optional maximum number of tokens to generate. If provided, overrides the service's default max_tokens/max_completion_tokens setting. + system_instruction: Optional system instruction to use for this inference. + If provided, overrides any system instruction in the context. Returns: The LLM's response as a string, or None if no response is generated. @@ -371,6 +375,15 @@ class BaseOpenAILLMService(LLMService): params["stream"] = False params.pop("stream_options", None) + # Prepend system instruction if provided + if system_instruction is not None: + messages = params.get("messages", []) + if messages and messages[0].get("role") == "system": + logger.warning( + f"{self}: Both system_instruction and an initial system message in context are set. This may be unintended." + ) + params["messages"] = [{"role": "system", "content": system_instruction}] + messages + # Override max_tokens if provided if max_tokens is not None: # Use max_completion_tokens for newer models, fallback to max_tokens diff --git a/src/pipecat/services/openai/image.py b/src/pipecat/services/openai/image.py index 6091863f3..de010247b 100644 --- a/src/pipecat/services/openai/image.py +++ b/src/pipecat/services/openai/image.py @@ -25,7 +25,7 @@ from pipecat.frames.frames import ( URLImageRawFrame, ) from pipecat.services.image_service import ImageGenService -from pipecat.services.settings import NOT_GIVEN, ImageGenSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, ImageGenSettings, _NotGiven @dataclass @@ -49,7 +49,7 @@ class OpenAIImageGenService(ImageGenService): """ Settings = OpenAIImageGenSettings - _settings: OpenAIImageGenSettings + _settings: Settings def __init__( self, @@ -61,7 +61,7 @@ class OpenAIImageGenService(ImageGenService): Literal["256x256", "512x512", "1024x1024", "1792x1024", "1024x1792"] ] = None, model: Optional[str] = None, - settings: Optional[OpenAIImageGenSettings] = None, + settings: Optional[Settings] = None, ): """Initialize the OpenAI image generation service. @@ -72,29 +72,29 @@ class OpenAIImageGenService(ImageGenService): image_size: Target size for generated images. Defaults to "1024x1024". .. deprecated:: 0.0.105 - Use ``settings=OpenAIImageGenSettings(image_size=...)`` instead. + Use ``settings=OpenAIImageGenService.Settings(image_size=...)`` instead. model: DALL-E model to use for generation. Defaults to "dall-e-3". .. deprecated:: 0.0.105 - Use ``settings=OpenAIImageGenSettings(model=...)`` instead. + Use ``settings=OpenAIImageGenService.Settings(model=...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = OpenAIImageGenSettings( + default_settings = self.Settings( model="dall-e-3", image_size=None, ) # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", OpenAIImageGenSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model if image_size is not None: - _warn_deprecated_param("image_size", OpenAIImageGenSettings, "image_size") + self._warn_init_param_moved_to_settings("image_size", "image_size") default_settings.image_size = image_size # 4. Apply settings delta (canonical API, always wins) diff --git a/src/pipecat/services/openai/llm.py b/src/pipecat/services/openai/llm.py index ab1384a4c..553733922 100644 --- a/src/pipecat/services/openai/llm.py +++ b/src/pipecat/services/openai/llm.py @@ -25,8 +25,7 @@ from pipecat.processors.aggregators.llm_response import ( LLMUserContextAggregator, ) from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext -from pipecat.services.openai.base_llm import BaseOpenAILLMService, OpenAILLMSettings -from pipecat.services.settings import _warn_deprecated_param +from pipecat.services.openai.base_llm import BaseOpenAILLMService @dataclass @@ -72,13 +71,15 @@ class OpenAILLMService(BaseOpenAILLMService): context aggregator creation. """ + Settings = BaseOpenAILLMService.Settings + def __init__( self, *, model: Optional[str] = None, service_tier: Optional[str] = None, params: Optional[BaseOpenAILLMService.InputParams] = None, - settings: Optional[OpenAILLMSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize OpenAI LLM service. @@ -87,20 +88,20 @@ class OpenAILLMService(BaseOpenAILLMService): model: The OpenAI model name to use. Defaults to "gpt-4.1". .. deprecated:: 0.0.105 - Use ``settings=OpenAILLMSettings(model=...)`` instead. + Use ``settings=OpenAILLMService.Settings(model=...)`` instead. service_tier: Service tier to use (e.g., "auto", "flex", "priority"). params: Input parameters for model configuration. .. deprecated:: 0.0.105 - Use ``settings=OpenAILLMSettings(...)`` instead. + Use ``settings=OpenAILLMService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional arguments passed to the parent BaseOpenAILLMService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = OpenAILLMSettings( + default_settings = self.Settings( model="gpt-4.1", system_instruction=None, frequency_penalty=NOT_GIVEN, @@ -118,7 +119,7 @@ class OpenAILLMService(BaseOpenAILLMService): # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", OpenAILLMSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # Handle service_tier from deprecated params @@ -127,7 +128,7 @@ class OpenAILLMService(BaseOpenAILLMService): # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", OpenAILLMSettings) + self._warn_init_param_moved_to_settings("params") if not settings: default_settings.frequency_penalty = params.frequency_penalty default_settings.presence_penalty = params.presence_penalty @@ -254,7 +255,7 @@ class OpenAIAssistantContextAggregator(LLMAssistantContextAggregator): frame: Frame containing the function call result. """ if frame.result: - result = json.dumps(frame.result) + result = json.dumps(frame.result, ensure_ascii=False) await self._update_function_call_result(frame.function_name, frame.tool_call_id, result) else: await self._update_function_call_result( diff --git a/src/pipecat/services/openai/realtime/llm.py b/src/pipecat/services/openai/realtime/llm.py index d1a90779e..bd31369ae 100644 --- a/src/pipecat/services/openai/realtime/llm.py +++ b/src/pipecat/services/openai/realtime/llm.py @@ -63,7 +63,6 @@ from pipecat.services.settings import ( NOT_GIVEN, LLMSettings, _NotGiven, - _warn_deprecated_param, is_given, ) from pipecat.transcriptions.language import Language @@ -115,7 +114,7 @@ class OpenAIRealtimeLLMSettings(LLMSettings): # -- Bidirectional sync helpers ------------------------------------------ @staticmethod - def _sync_top_level_to_sp(settings: "OpenAIRealtimeLLMSettings"): + def _sync_top_level_to_sp(settings: "OpenAIRealtimeLLMService.Settings"): """Push top-level ``model``/``system_instruction`` into ``session_properties``.""" if not is_given(settings.session_properties): return @@ -127,7 +126,7 @@ class OpenAIRealtimeLLMSettings(LLMSettings): # -- apply_update override ----------------------------------------------- - def apply_update(self, delta: "OpenAIRealtimeLLMSettings") -> Dict[str, Any]: + def apply_update(self, delta: "OpenAIRealtimeLLMService.Settings") -> Dict[str, Any]: """Merge a delta, keeping ``model``/``system_instruction`` in sync with SP. When the delta contains ``session_properties``, it **replaces** the @@ -165,8 +164,8 @@ class OpenAIRealtimeLLMSettings(LLMSettings): @classmethod def from_mapping( - cls: Type["OpenAIRealtimeLLMSettings"], settings: Mapping[str, Any] - ) -> "OpenAIRealtimeLLMSettings": + cls: Type["OpenAIRealtimeLLMService.Settings"], settings: Mapping[str, Any] + ) -> "OpenAIRealtimeLLMService.Settings": """Build a delta from a plain dict, routing SP keys into ``session_properties``. Keys that correspond to ``SessionProperties`` fields (except ``model``) @@ -211,7 +210,7 @@ class OpenAIRealtimeLLMService(LLMService): """ Settings = OpenAIRealtimeLLMSettings - _settings: OpenAIRealtimeLLMSettings + _settings: Settings # Overriding the default adapter to use the OpenAIRealtimeLLMAdapter one. adapter_class = OpenAIRealtimeLLMAdapter @@ -223,7 +222,7 @@ class OpenAIRealtimeLLMService(LLMService): model: Optional[str] = None, base_url: str = "wss://api.openai.com/v1/realtime", session_properties: Optional[events.SessionProperties] = None, - settings: Optional[OpenAIRealtimeLLMSettings] = None, + settings: Optional[Settings] = None, start_audio_paused: bool = False, start_video_paused: bool = False, video_frame_detail: str = "auto", @@ -237,7 +236,7 @@ class OpenAIRealtimeLLMService(LLMService): model: OpenAI model name. .. deprecated:: 0.0.105 - Use ``settings=OpenAIRealtimeLLMSettings(model=...)`` instead. + Use ``settings=OpenAIRealtimeLLMService.Settings(model=...)`` instead. This is a connection-level parameter set via the WebSocket URL query parameter and cannot be changed during the session. @@ -247,7 +246,7 @@ class OpenAIRealtimeLLMService(LLMService): If None, uses default SessionProperties. .. deprecated:: 0.0.105 - Use ``settings=OpenAIRealtimeLLMSettings(session_properties=...)`` + Use ``settings=OpenAIRealtimeLLMService.Settings(session_properties=...)`` instead. settings: Runtime-updatable settings for this service. start_audio_paused: Whether to start with audio input paused. Defaults to False. @@ -277,7 +276,7 @@ class OpenAIRealtimeLLMService(LLMService): ) # 1. Initialize default_settings with hardcoded defaults - default_settings = OpenAIRealtimeLLMSettings( + default_settings = self.Settings( model="gpt-realtime-1.5", system_instruction=None, temperature=None, @@ -294,13 +293,13 @@ class OpenAIRealtimeLLMService(LLMService): # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", OpenAIRealtimeLLMSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model if session_properties is not None: _warn_deprecated_param( "session_properties", - OpenAIRealtimeLLMSettings, + self.Settings, "session_properties", ) default_settings.session_properties = session_properties @@ -312,7 +311,7 @@ class OpenAIRealtimeLLMService(LLMService): default_settings.system_instruction = session_properties.instructions # Sync top-level model back into session_properties - OpenAIRealtimeLLMSettings._sync_top_level_to_sp(default_settings) + self.Settings._sync_top_level_to_sp(default_settings) # 3. Apply settings delta (canonical API, always wins) if settings is not None: @@ -747,7 +746,10 @@ class OpenAIRealtimeLLMService(LLMService): await self._handle_evt_function_call_arguments_done(evt) elif evt.type == "error": if not await self._maybe_handle_evt_retrieve_conversation_item_error(evt): - if evt.error.code == "response_cancel_not_active": + if evt.error.code in ( + "response_cancel_not_active", + "conversation_already_has_active_response", + ): logger.debug(f"{self} {evt.error.message}") else: await self._handle_evt_error(evt) @@ -1126,7 +1128,7 @@ class OpenAIRealtimeLLMService(LLMService): item = events.ConversationItem( type="function_call_output", call_id=tool_call_id, - output=json.dumps(result), + output=json.dumps(result, ensure_ascii=False), ) await self.send_client_event(events.ConversationItemCreateEvent(item=item)) diff --git a/src/pipecat/services/openai/responses/__init__.py b/src/pipecat/services/openai/responses/__init__.py new file mode 100644 index 000000000..c4d243b97 --- /dev/null +++ b/src/pipecat/services/openai/responses/__init__.py @@ -0,0 +1,5 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# diff --git a/src/pipecat/services/openai/responses/llm.py b/src/pipecat/services/openai/responses/llm.py new file mode 100644 index 000000000..e9e5d3a1f --- /dev/null +++ b/src/pipecat/services/openai/responses/llm.py @@ -0,0 +1,400 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""OpenAI Responses API LLM service implementation.""" + +import json +from contextlib import asynccontextmanager +from dataclasses import dataclass, field +from typing import Any, Dict, List, Mapping, Optional + +import httpx +from loguru import logger +from openai import NOT_GIVEN, AsyncOpenAI, AsyncStream, DefaultAsyncHttpxClient +from openai.types.responses import ( + ResponseCompletedEvent, + ResponseFunctionCallArgumentsDeltaEvent, + ResponseFunctionCallArgumentsDoneEvent, + ResponseFunctionToolCall, + ResponseOutputItemAddedEvent, + ResponseOutputItemDoneEvent, + ResponseStreamEvent, + ResponseTextDeltaEvent, +) + +from pipecat.adapters.services.open_ai_responses_adapter import ( + OpenAIResponsesLLMAdapter, + OpenAIResponsesLLMInvocationParams, +) +from pipecat.frames.frames import ( + Frame, + LLMContextFrame, + LLMFullResponseEndFrame, + LLMFullResponseStartFrame, +) +from pipecat.metrics.metrics import LLMTokenUsage +from pipecat.processors.aggregators.llm_context import LLMContext +from pipecat.processors.frame_processor import FrameDirection +from pipecat.services.llm_service import FunctionCallFromLLM, LLMService +from pipecat.services.settings import NOT_GIVEN as _NOT_GIVEN +from pipecat.services.settings import LLMSettings, _NotGiven +from pipecat.utils.tracing.service_decorators import traced_llm + + +@dataclass +class OpenAIResponsesLLMSettings(LLMSettings): + """Settings for OpenAIResponsesLLMService. + + Parameters: + max_completion_tokens: Maximum completion tokens to generate. + """ + + max_completion_tokens: int | _NotGiven = field(default_factory=lambda: _NOT_GIVEN) + + +class OpenAIResponsesLLMService(LLMService): + """OpenAI Responses API LLM service. + + This service works with the universal LLMContext and LLMContextAggregatorPair. + + Example:: + + llm = OpenAIResponsesLLMService( + api_key=os.getenv("OPENAI_API_KEY"), + settings=OpenAIResponsesLLMService.Settings( + model="gpt-4.1", + system_instruction="You are a helpful assistant.", + ), + ) + """ + + Settings = OpenAIResponsesLLMSettings + _settings: Settings + + adapter_class = OpenAIResponsesLLMAdapter + + def __init__( + self, + *, + api_key=None, + base_url=None, + organization=None, + project=None, + default_headers: Optional[Mapping[str, str]] = None, + service_tier: Optional[str] = None, + settings: Optional[Settings] = None, + **kwargs, + ): + """Initialize the OpenAI Responses API LLM service. + + Args: + api_key: OpenAI API key. If None, uses environment variable. + base_url: Custom base URL for OpenAI API. If None, uses default. + organization: OpenAI organization ID. + project: OpenAI project ID. + default_headers: Additional HTTP headers to include in requests. + service_tier: Service tier to use (e.g., "auto", "flex", "priority"). + settings: Runtime-updatable settings. + **kwargs: Additional arguments passed to the parent LLMService. + """ + default_settings = self.Settings( + model="gpt-4.1", + system_instruction=None, + frequency_penalty=None, + presence_penalty=None, + seed=None, + temperature=NOT_GIVEN, + top_p=NOT_GIVEN, + top_k=None, + max_tokens=None, + max_completion_tokens=NOT_GIVEN, + filter_incomplete_user_turns=False, + user_turn_completion_config=None, + extra={}, + ) + + if settings is not None: + default_settings.apply_update(settings) + + super().__init__( + settings=default_settings, + **kwargs, + ) + + self._service_tier = service_tier + self._client = self._create_client( + api_key=api_key, + base_url=base_url, + organization=organization, + project=project, + default_headers=default_headers, + ) + + if self._settings.system_instruction: + logger.debug(f"{self}: Using system instruction: {self._settings.system_instruction}") + + def _create_client( + self, + api_key=None, + base_url=None, + organization=None, + project=None, + default_headers=None, + ) -> AsyncOpenAI: + """Create an AsyncOpenAI client instance. + + Args: + api_key: OpenAI API key. + base_url: Custom base URL for the API. + organization: OpenAI organization ID. + project: OpenAI project ID. + default_headers: Additional HTTP headers. + + Returns: + Configured AsyncOpenAI client instance. + """ + return AsyncOpenAI( + api_key=api_key, + base_url=base_url, + organization=organization, + project=project, + http_client=DefaultAsyncHttpxClient( + limits=httpx.Limits( + max_keepalive_connections=100, max_connections=1000, keepalive_expiry=None + ) + ), + default_headers=default_headers, + ) + + def can_generate_metrics(self) -> bool: + """Check if this service can generate processing metrics.""" + return True + + async def process_frame(self, frame: Frame, direction: FrameDirection): + """Process frames for LLM completion requests. + + Args: + frame: The frame to process. + direction: The direction of frame processing. + """ + await super().process_frame(frame, direction) + + context = None + if isinstance(frame, LLMContextFrame): + context = frame.context + else: + await self.push_frame(frame, direction) + + if context: + try: + await self.push_frame(LLMFullResponseStartFrame()) + await self.start_processing_metrics() + await self._process_context(context) + except httpx.TimeoutException as e: + await self._call_event_handler("on_completion_timeout") + await self.push_error(error_msg="LLM completion timeout", exception=e) + except Exception as e: + await self.push_error(error_msg=f"Error during completion: {e}", exception=e) + finally: + await self.stop_processing_metrics() + await self.push_frame(LLMFullResponseEndFrame()) + + @traced_llm + async def _process_context(self, context: LLMContext): + adapter: OpenAIResponsesLLMAdapter = self.get_llm_adapter() + logger.debug( + f"{self}: Generating response from universal context " + f"{adapter.get_messages_for_logging(context)}" + ) + + invocation_params = adapter.get_llm_invocation_params( + context, system_instruction=self._settings.system_instruction + ) + + params = self._build_response_params(invocation_params) + + await self.start_ttfb_metrics() + + stream: AsyncStream[ResponseStreamEvent] = await self._client.responses.create(**params) + + # Track function calls across stream events + function_calls: Dict[str, Dict[str, str]] = {} # item_id -> {name, call_id, arguments} + current_arguments: Dict[str, str] = {} # item_id -> accumulated arguments + + # Ensure stream and its async iterator are closed on cancellation/exception + # to prevent socket leaks and uvloop crashes. Closing the iterator first + # cascades cleanup through nested async generators (httpx/httpcore internals), + # preventing uvloop's broken asyncgen finalizer from firing on Python 3.12+ + # (MagicStack/uvloop#699). + @asynccontextmanager + async def _closing(stream): + chunk_iter = stream.__aiter__() + try: + yield chunk_iter + finally: + # Close the iterator first to cascade cleanup through + # nested async generators (httpx/httpcore internals). + if hasattr(chunk_iter, "aclose"): + await chunk_iter.aclose() + # Then close the stream to release HTTP resources. + if hasattr(stream, "close"): + await stream.close() + elif hasattr(stream, "aclose"): + await stream.aclose() + + async with _closing(stream) as event_iter: + async for event in event_iter: + if isinstance(event, ResponseTextDeltaEvent): + await self.stop_ttfb_metrics() + await self._push_llm_text(event.delta) + + elif isinstance(event, ResponseOutputItemAddedEvent): + await self.stop_ttfb_metrics() + item = event.item + if isinstance(item, ResponseFunctionToolCall): + item_id = item.id or "" + function_calls[item_id] = { + "name": item.name, + "call_id": item.call_id, + "arguments": "", + } + current_arguments[item_id] = "" + + elif isinstance(event, ResponseFunctionCallArgumentsDeltaEvent): + item_id = event.item_id + if item_id in current_arguments: + current_arguments[item_id] += event.delta + + elif isinstance(event, ResponseFunctionCallArgumentsDoneEvent): + item_id = event.item_id + if item_id in function_calls: + function_calls[item_id]["arguments"] = event.arguments + + elif isinstance(event, ResponseOutputItemDoneEvent): + item = event.item + if isinstance(item, ResponseFunctionToolCall): + item_id = item.id or "" + if item_id in function_calls: + function_calls[item_id]["name"] = item.name + function_calls[item_id]["call_id"] = item.call_id + function_calls[item_id]["arguments"] = item.arguments + + elif isinstance(event, ResponseCompletedEvent): + response = event.response + if response.usage: + tokens = LLMTokenUsage( + prompt_tokens=response.usage.input_tokens, + completion_tokens=response.usage.output_tokens, + total_tokens=response.usage.total_tokens, + cache_read_input_tokens=response.usage.input_tokens_details.cached_tokens, + reasoning_tokens=response.usage.output_tokens_details.reasoning_tokens, + ) + await self.start_llm_usage_metrics(tokens) + + # This field is used by @traced_llm for more detailed + # model name in tracing spans + self._full_model_name = response.model + + # Process any function calls + if function_calls: + fc_list: List[FunctionCallFromLLM] = [] + for item_id, fc in function_calls.items(): + try: + arguments = json.loads(fc["arguments"]) if fc["arguments"] else {} + except json.JSONDecodeError: + logger.warning( + f"{self}: Failed to parse function call arguments: {fc['arguments']}" + ) + arguments = {} + fc_list.append( + FunctionCallFromLLM( + context=context, + tool_call_id=fc["call_id"], + function_name=fc["name"], + arguments=arguments, + ) + ) + await self.run_function_calls(fc_list) + + def _build_response_params(self, invocation_params: OpenAIResponsesLLMInvocationParams) -> dict: + """Build parameters for the responses.create() call. + + Args: + invocation_params: Parameters derived from the LLM context. + + Returns: + Dictionary of parameters for the Responses API call. + """ + params: Dict[str, Any] = { + "model": self._settings.model, + "stream": True, + "store": False, + "input": invocation_params["input"], + } + + # instructions (set by the adapter when input is non-empty) + if "instructions" in invocation_params: + params["instructions"] = invocation_params["instructions"] + + # Optional parameters - only include if given + if isinstance(self._settings.temperature, (int, float)): + params["temperature"] = self._settings.temperature + + if isinstance(self._settings.top_p, (int, float)): + params["top_p"] = self._settings.top_p + + if isinstance(self._settings.max_completion_tokens, int): + params["max_output_tokens"] = self._settings.max_completion_tokens + + if self._service_tier is not None: + params["service_tier"] = self._service_tier + + # Tools + tools = invocation_params.get("tools") + if tools is not None and not isinstance(tools, type(NOT_GIVEN)): + params["tools"] = tools + + # Extra settings + params.update(self._settings.extra) + + return params + + async def run_inference( + self, + context: LLMContext, + max_tokens: Optional[int] = None, + system_instruction: Optional[str] = None, + ) -> Optional[str]: + """Run a one-shot, out-of-band inference with the given LLM context. + + Args: + context: The LLM context containing conversation history. + max_tokens: Optional maximum number of tokens to generate. + system_instruction: Optional system instruction for this inference. + + Returns: + The LLM's response as a string, or None if no response is generated. + """ + adapter: OpenAIResponsesLLMAdapter = self.get_llm_adapter() + effective_instruction = system_instruction or self._settings.system_instruction + invocation_params = adapter.get_llm_invocation_params( + context, system_instruction=effective_instruction + ) + + params = self._build_response_params(invocation_params) + + # Override for non-streaming + params["stream"] = False + + if max_tokens is not None: + params["max_output_tokens"] = max_tokens + + response = await self._client.responses.create(**params) + + return response.output_text + + +__all__ = ["OpenAIResponsesLLMService", "OpenAIResponsesLLMSettings"] diff --git a/src/pipecat/services/openai/stt.py b/src/pipecat/services/openai/stt.py index e2f9ec0ee..fd98dbf49 100644 --- a/src/pipecat/services/openai/stt.py +++ b/src/pipecat/services/openai/stt.py @@ -35,12 +35,11 @@ from pipecat.frames.frames import ( VADUserStoppedSpeakingFrame, ) from pipecat.processors.frame_processor import FrameDirection -from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven from pipecat.services.stt_latency import OPENAI_REALTIME_TTFS_P99, OPENAI_TTFS_P99 from pipecat.services.stt_service import WebsocketSTTService from pipecat.services.whisper.base_stt import ( BaseWhisperSTTService, - BaseWhisperSTTSettings, Transcription, ) from pipecat.transcriptions.language import Language @@ -56,7 +55,7 @@ except ModuleNotFoundError: @dataclass -class OpenAISTTSettings(BaseWhisperSTTSettings): +class OpenAISTTSettings(BaseWhisperSTTService.Settings): """Settings for the OpenAI STT service.""" pass @@ -70,7 +69,7 @@ class OpenAISTTService(BaseWhisperSTTService): """ Settings = OpenAISTTSettings - _settings: OpenAISTTSettings + _settings: Settings def __init__( self, @@ -81,7 +80,7 @@ class OpenAISTTService(BaseWhisperSTTService): language: Optional[Language] = Language.EN, prompt: Optional[str] = None, temperature: Optional[float] = None, - settings: Optional[OpenAISTTSettings] = None, + settings: Optional[Settings] = None, ttfs_p99_latency: Optional[float] = OPENAI_TTFS_P99, **kwargs, ): @@ -91,24 +90,24 @@ class OpenAISTTService(BaseWhisperSTTService): model: Model to use — either gpt-4o or Whisper. .. deprecated:: 0.0.105 - Use ``settings=OpenAISTTSettings(model=...)`` instead. + Use ``settings=OpenAISTTService.Settings(model=...)`` instead. api_key: OpenAI API key. Defaults to None. base_url: API base URL. Defaults to None. language: Language of the audio input. Defaults to English. .. deprecated:: 0.0.105 - Use ``settings=OpenAISTTSettings(language=...)`` instead. + Use ``settings=OpenAISTTService.Settings(language=...)`` instead. prompt: Optional text to guide the model's style or continue a previous segment. .. deprecated:: 0.0.105 - Use ``settings=OpenAISTTSettings(prompt=...)`` instead. + Use ``settings=OpenAISTTService.Settings(prompt=...)`` instead. temperature: Optional sampling temperature between 0 and 1. Defaults to 0.0. .. deprecated:: 0.0.105 - Use ``settings=OpenAISTTSettings(temperature=...)`` instead. + Use ``settings=OpenAISTTService.Settings(temperature=...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -118,22 +117,22 @@ class OpenAISTTService(BaseWhisperSTTService): """ # --- 1. Hardcoded defaults --- _language = language or Language.EN - default_settings = OpenAISTTSettings( + default_settings = self.Settings( model="gpt-4o-transcribe", - language=self.language_to_service_language(_language), + language=_language, prompt=None, temperature=None, ) # --- 2. Deprecated direct-arg overrides --- if model is not None: - _warn_deprecated_param("model", OpenAISTTSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model if prompt is not None: - _warn_deprecated_param("prompt", OpenAISTTSettings, "prompt") + self._warn_init_param_moved_to_settings("prompt", "prompt") default_settings.prompt = prompt if temperature is not None: - _warn_deprecated_param("temperature", OpenAISTTSettings, "temperature") + self._warn_init_param_moved_to_settings("temperature", "temperature") default_settings.temperature = temperature # --- 3. (no params object for this service) --- @@ -187,9 +186,15 @@ class OpenAIRealtimeSTTSettings(STTSettings): Parameters: prompt: Optional prompt text to guide transcription style. + noise_reduction: Noise reduction mode. ``"near_field"`` for close + microphones, ``"far_field"`` for distant microphones, or ``None`` + to disable. """ prompt: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN) + noise_reduction: Literal["near_field", "far_field"] | None | _NotGiven = field( + default_factory=lambda: NOT_GIVEN + ) class OpenAIRealtimeSTTService(WebsocketSTTService): @@ -220,13 +225,15 @@ class OpenAIRealtimeSTTService(WebsocketSTTService): stt = OpenAIRealtimeSTTService( api_key="sk-...", - model="gpt-4o-transcribe", - noise_reduction="near_field", + settings=OpenAIRealtimeSTTService.Settings( + model="gpt-4o-transcribe", + noise_reduction="near_field", + ), ) """ Settings = OpenAIRealtimeSTTSettings - _settings: OpenAIRealtimeSTTSettings + _settings: Settings def __init__( self, @@ -239,7 +246,7 @@ class OpenAIRealtimeSTTService(WebsocketSTTService): turn_detection: Optional[Union[dict, Literal[False]]] = False, noise_reduction: Optional[Literal["near_field", "far_field"]] = None, should_interrupt: bool = True, - settings: Optional[OpenAIRealtimeSTTSettings] = None, + settings: Optional[Settings] = None, ttfs_p99_latency: Optional[float] = OPENAI_REALTIME_TTFS_P99, **kwargs, ): @@ -251,20 +258,20 @@ class OpenAIRealtimeSTTService(WebsocketSTTService): ``"gpt-4o-transcribe"`` and ``"gpt-4o-mini-transcribe"``. .. deprecated:: 0.0.105 - Use ``settings=OpenAIRealtimeSTTSettings(model=...)`` instead. + Use ``settings=OpenAIRealtimeSTTService.Settings(model=...)`` instead. base_url: WebSocket base URL for the Realtime API. Defaults to ``"wss://api.openai.com/v1/realtime"``. language: Language of the audio input. Defaults to English. .. deprecated:: 0.0.105 - Use ``settings=OpenAIRealtimeSTTSettings(language=...)`` instead. + Use ``settings=OpenAIRealtimeSTTService.Settings(language=...)`` instead. prompt: Optional prompt text to guide transcription style or provide keyword hints. .. deprecated:: 0.0.105 - Use ``settings=OpenAIRealtimeSTTSettings(prompt=...)`` instead. + Use ``settings=OpenAIRealtimeSTTService.Settings(prompt=...)`` instead. turn_detection: Server-side VAD configuration. Defaults to ``False`` (disabled), which relies on a local VAD @@ -274,6 +281,9 @@ class OpenAIRealtimeSTTService(WebsocketSTTService): noise_reduction: Noise reduction mode. ``"near_field"`` for close microphones, ``"far_field"`` for distant microphones, or ``None`` to disable. + + .. deprecated:: 0.0.106 + Use ``settings=OpenAIRealtimeSTTService.Settings(noise_reduction=...)`` instead. should_interrupt: Whether to interrupt bot output when speech is detected by server-side VAD. Only applies when turn detection is enabled. Defaults to True. @@ -291,22 +301,26 @@ class OpenAIRealtimeSTTService(WebsocketSTTService): ) # --- 1. Hardcoded defaults --- - default_settings = OpenAIRealtimeSTTSettings( + default_settings = self.Settings( model="gpt-4o-transcribe", language=Language.EN, prompt=None, + noise_reduction=None, ) # --- 2. Deprecated direct-arg overrides --- if model is not None: - _warn_deprecated_param("model", OpenAIRealtimeSTTSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model if language is not None and language != Language.EN: - _warn_deprecated_param("language", OpenAIRealtimeSTTSettings, "language") + self._warn_init_param_moved_to_settings("language", "language") default_settings.language = language if prompt is not None: - _warn_deprecated_param("prompt", OpenAIRealtimeSTTSettings, "prompt") + self._warn_init_param_moved_to_settings("prompt", "prompt") default_settings.prompt = prompt + if noise_reduction is not None: + self._warn_init_param_moved_to_settings("noise_reduction", "noise_reduction") + default_settings.noise_reduction = noise_reduction # --- 3. (no params object for this service) --- @@ -324,7 +338,6 @@ class OpenAIRealtimeSTTService(WebsocketSTTService): self._base_url = base_url self._turn_detection = turn_detection - self._noise_reduction = noise_reduction self._should_interrupt = should_interrupt self._receive_task = None @@ -345,8 +358,8 @@ class OpenAIRealtimeSTTService(WebsocketSTTService): Returns: Two-letter ISO-639-1 language code. """ - # Language.value is e.g. "en", "en-US", "fr", "zh". - return language.value.split("-")[0].lower() + # Language value is e.g. "en", "en-US", "fr", "zh". + return str(language).split("-")[0].lower() def can_generate_metrics(self) -> bool: """Check if the service can generate processing metrics. @@ -362,7 +375,7 @@ class OpenAIRealtimeSTTService(WebsocketSTTService): Sends a ``session.update`` to the server when the session is active. Args: - delta: A :class:`STTSettings` (or ``OpenAIRealtimeSTTSettings``) delta. + delta: A :class:`STTSettings` (or ``OpenAIRealtimeSTTService.Settings``) delta. Returns: Dict mapping changed field names to their previous values. @@ -544,9 +557,9 @@ class OpenAIRealtimeSTTService(WebsocketSTTService): input_audio["turn_detection"] = self._turn_detection # Noise reduction - if self._noise_reduction: + if self._settings.noise_reduction: input_audio["noise_reduction"] = { - "type": self._noise_reduction, + "type": self._settings.noise_reduction, } await self._ws_send( diff --git a/src/pipecat/services/openai/tts.py b/src/pipecat/services/openai/tts.py index 264475113..074792b33 100644 --- a/src/pipecat/services/openai/tts.py +++ b/src/pipecat/services/openai/tts.py @@ -23,7 +23,7 @@ from pipecat.frames.frames import ( StartFrame, TTSAudioRawFrame, ) -from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven from pipecat.services.tts_service import TTSService from pipecat.utils.tracing.service_decorators import traced_tts @@ -82,7 +82,7 @@ class OpenAITTSService(TTSService): """ Settings = OpenAITTSSettings - _settings: OpenAITTSSettings + _settings: Settings OPENAI_SAMPLE_RATE = 24000 # OpenAI TTS always outputs at 24kHz @@ -90,7 +90,7 @@ class OpenAITTSService(TTSService): """Input parameters for OpenAI TTS configuration. .. deprecated:: 0.0.105 - Use ``settings=OpenAITTSSettings(...)`` instead. + Use ``settings=OpenAITTSService.Settings(...)`` instead. Parameters: instructions: Instructions to guide voice synthesis behavior. @@ -111,7 +111,7 @@ class OpenAITTSService(TTSService): instructions: Optional[str] = None, speed: Optional[float] = None, params: Optional[InputParams] = None, - settings: Optional[OpenAITTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize OpenAI TTS service. @@ -122,28 +122,28 @@ class OpenAITTSService(TTSService): voice: Voice ID to use for synthesis. Defaults to "alloy". .. deprecated:: 0.0.105 - Use ``settings=OpenAITTSSettings(voice=...)`` instead. + Use ``settings=OpenAITTSService.Settings(voice=...)`` instead. model: TTS model to use. Defaults to "gpt-4o-mini-tts". .. deprecated:: 0.0.105 - Use ``settings=OpenAITTSSettings(model=...)`` instead. + Use ``settings=OpenAITTSService.Settings(model=...)`` instead. sample_rate: Output audio sample rate in Hz. If None, uses OpenAI's default 24kHz. instructions: Optional instructions to guide voice synthesis behavior. .. deprecated:: 0.0.105 - Use ``settings=OpenAITTSSettings(instructions=...)`` instead. + Use ``settings=OpenAITTSService.Settings(instructions=...)`` instead. speed: Voice speed control (0.25 to 4.0, default 1.0). .. deprecated:: 0.0.105 - Use ``settings=OpenAITTSSettings(speed=...)`` instead. + Use ``settings=OpenAITTSService.Settings(speed=...)`` instead. params: Optional synthesis controls (acting instructions, speed, ...). .. deprecated:: 0.0.105 - Use ``settings=OpenAITTSSettings(...)`` instead. + Use ``settings=OpenAITTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -156,7 +156,7 @@ class OpenAITTSService(TTSService): ) # 1. Initialize default_settings with hardcoded defaults - default_settings = OpenAITTSSettings( + default_settings = self.Settings( model="gpt-4o-mini-tts", voice="alloy", language=None, @@ -166,21 +166,21 @@ class OpenAITTSService(TTSService): # 2. Apply direct init arg overrides (deprecated) if voice is not None: - _warn_deprecated_param("voice", OpenAITTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice", "voice") default_settings.voice = voice if model is not None: - _warn_deprecated_param("model", OpenAITTSSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model if instructions is not None: - _warn_deprecated_param("instructions", OpenAITTSSettings, "instructions") + self._warn_init_param_moved_to_settings("instructions", "instructions") default_settings.instructions = instructions if speed is not None: - _warn_deprecated_param("speed", OpenAITTSSettings, "speed") + self._warn_init_param_moved_to_settings("speed", "speed") default_settings.speed = speed # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", OpenAITTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: if params.instructions is not None: default_settings.instructions = params.instructions diff --git a/src/pipecat/services/openai_realtime_beta/azure.py b/src/pipecat/services/openai_realtime_beta/azure.py index 6497e1266..b590f2c29 100644 --- a/src/pipecat/services/openai_realtime_beta/azure.py +++ b/src/pipecat/services/openai_realtime_beta/azure.py @@ -11,7 +11,7 @@ from dataclasses import dataclass from loguru import logger -from .openai import OpenAIRealtimeBetaLLMService, OpenAIRealtimeBetaLLMSettings +from .openai import OpenAIRealtimeBetaLLMService try: from websockets.asyncio.client import connect as websocket_connect @@ -24,7 +24,7 @@ except ModuleNotFoundError as e: @dataclass -class AzureRealtimeBetaLLMSettings(OpenAIRealtimeBetaLLMSettings): +class AzureRealtimeBetaLLMSettings(OpenAIRealtimeBetaLLMService.Settings): """Settings for AzureRealtimeBetaLLMService.""" pass @@ -43,7 +43,7 @@ class AzureRealtimeBetaLLMService(OpenAIRealtimeBetaLLMService): """ Settings = AzureRealtimeBetaLLMSettings - _settings: AzureRealtimeBetaLLMSettings + _settings: Settings def __init__( self, diff --git a/src/pipecat/services/openai_realtime_beta/openai.py b/src/pipecat/services/openai_realtime_beta/openai.py index 51fcc2720..0d20039b1 100644 --- a/src/pipecat/services/openai_realtime_beta/openai.py +++ b/src/pipecat/services/openai_realtime_beta/openai.py @@ -54,7 +54,7 @@ from pipecat.processors.aggregators.openai_llm_context import ( from pipecat.processors.frame_processor import FrameDirection from pipecat.services.llm_service import FunctionCallFromLLM, LLMService from pipecat.services.openai.llm import OpenAIContextAggregatorPair -from pipecat.services.settings import LLMSettings, _warn_deprecated_param +from pipecat.services.settings import LLMSettings from pipecat.transcriptions.language import Language from pipecat.utils.time import time_now_iso8601 from pipecat.utils.tracing.service_decorators import traced_openai_realtime, traced_stt @@ -112,7 +112,7 @@ class OpenAIRealtimeBetaLLMService(LLMService): """ Settings = OpenAIRealtimeBetaLLMSettings - _settings: OpenAIRealtimeBetaLLMSettings + _settings: Settings # Overriding the default adapter to use the OpenAIRealtimeLLMAdapter one. adapter_class = OpenAIRealtimeLLMAdapter @@ -124,7 +124,7 @@ class OpenAIRealtimeBetaLLMService(LLMService): model: Optional[str] = None, base_url: str = "wss://api.openai.com/v1/realtime", session_properties: Optional[events.SessionProperties] = None, - settings: Optional[OpenAIRealtimeBetaLLMSettings] = None, + settings: Optional[Settings] = None, start_audio_paused: bool = False, send_transcription_frames: bool = True, **kwargs, @@ -136,7 +136,7 @@ class OpenAIRealtimeBetaLLMService(LLMService): model: OpenAI model name. .. deprecated:: 0.0.105 - Use ``settings=OpenAIRealtimeBetaLLMSettings(model=...)`` instead. + Use ``settings=OpenAIRealtimeBetaLLMService.Settings(model=...)`` instead. base_url: WebSocket base URL for the realtime API. Defaults to "wss://api.openai.com/v1/realtime". @@ -157,7 +157,7 @@ class OpenAIRealtimeBetaLLMService(LLMService): ) # 1. Initialize default_settings with hardcoded defaults - default_settings = OpenAIRealtimeBetaLLMSettings( + default_settings = self.Settings( model="gpt-4o-realtime-preview-2025-06-03", system_instruction=None, temperature=None, @@ -173,7 +173,7 @@ class OpenAIRealtimeBetaLLMService(LLMService): # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", OpenAIRealtimeBetaLLMSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. Apply settings delta (canonical API, always wins) if settings is not None: @@ -441,7 +441,7 @@ class OpenAIRealtimeBetaLLMService(LLMService): item = events.ConversationItem( type="function_call_output", call_id=frame.tool_call_id, - output=json.dumps(frame.result), + output=json.dumps(frame.result, ensure_ascii=False), ) await self.send_client_event(events.ConversationItemCreateEvent(item=item)) @@ -556,7 +556,10 @@ class OpenAIRealtimeBetaLLMService(LLMService): await self._handle_evt_audio_transcript_delta(evt) elif evt.type == "error": if not await self._maybe_handle_evt_retrieve_conversation_item_error(evt): - if evt.error.code == "response_cancel_not_active": + if evt.error.code in ( + "response_cancel_not_active", + "conversation_already_has_active_response", + ): logger.debug(f"{self} {evt.error.message}") else: await self._handle_evt_error(evt) diff --git a/src/pipecat/services/openpipe/llm.py b/src/pipecat/services/openpipe/llm.py index 3fef1e3af..0a8fd9044 100644 --- a/src/pipecat/services/openpipe/llm.py +++ b/src/pipecat/services/openpipe/llm.py @@ -16,9 +16,8 @@ from typing import Dict, Optional from loguru import logger from pipecat.adapters.services.open_ai_adapter import OpenAILLMInvocationParams -from pipecat.services.openai.base_llm import OpenAILLMSettings +from pipecat.services.openai.base_llm import BaseOpenAILLMService from pipecat.services.openai.llm import OpenAILLMService -from pipecat.services.settings import _warn_deprecated_param try: from openpipe import AsyncOpenAI as OpenPipeAI @@ -29,7 +28,7 @@ except ModuleNotFoundError as e: @dataclass -class OpenPipeLLMSettings(OpenAILLMSettings): +class OpenPipeLLMSettings(BaseOpenAILLMService.Settings): """Settings for OpenPipeLLMService.""" pass @@ -44,7 +43,7 @@ class OpenPipeLLMService(OpenAILLMService): """ Settings = OpenPipeLLMSettings - _settings: OpenPipeLLMSettings + _settings: Settings def __init__( self, @@ -55,7 +54,7 @@ class OpenPipeLLMService(OpenAILLMService): openpipe_api_key: Optional[str] = None, openpipe_base_url: str = "https://app.openpipe.ai/api/v1", tags: Optional[Dict[str, str]] = None, - settings: Optional[OpenPipeLLMSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize OpenPipe LLM service. @@ -64,7 +63,7 @@ class OpenPipeLLMService(OpenAILLMService): model: The model name to use. Defaults to "gpt-4.1". .. deprecated:: 0.0.105 - Use ``settings=OpenAILLMSettings(model=...)`` instead. + Use ``settings=OpenPipeLLMService.Settings(model=...)`` instead. api_key: OpenAI API key for authentication. If None, reads from environment. base_url: Custom OpenAI API endpoint URL. Uses default if None. @@ -76,11 +75,11 @@ class OpenPipeLLMService(OpenAILLMService): **kwargs: Additional arguments passed to parent OpenAILLMService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = OpenPipeLLMSettings(model="gpt-4.1") + default_settings = self.Settings(model="gpt-4.1") # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", OpenPipeLLMSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. (No step 3, as there's no params object to apply) diff --git a/src/pipecat/services/openrouter/llm.py b/src/pipecat/services/openrouter/llm.py index d57c5cf24..f92fb5e3b 100644 --- a/src/pipecat/services/openrouter/llm.py +++ b/src/pipecat/services/openrouter/llm.py @@ -15,13 +15,12 @@ from typing import Any, Dict, Optional from loguru import logger -from pipecat.services.openai.base_llm import OpenAILLMSettings +from pipecat.services.openai.base_llm import BaseOpenAILLMService from pipecat.services.openai.llm import OpenAILLMService -from pipecat.services.settings import _warn_deprecated_param @dataclass -class OpenRouterLLMSettings(OpenAILLMSettings): +class OpenRouterLLMSettings(BaseOpenAILLMService.Settings): """Settings for OpenRouterLLMService.""" pass @@ -35,7 +34,7 @@ class OpenRouterLLMService(OpenAILLMService): """ Settings = OpenRouterLLMSettings - _settings: OpenRouterLLMSettings + _settings: Settings def __init__( self, @@ -43,7 +42,7 @@ class OpenRouterLLMService(OpenAILLMService): api_key: Optional[str] = None, model: Optional[str] = None, base_url: str = "https://openrouter.ai/api/v1", - settings: Optional[OpenRouterLLMSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the OpenRouter LLM service. @@ -54,7 +53,7 @@ class OpenRouterLLMService(OpenAILLMService): model: The model identifier to use. Defaults to "openai/gpt-4o-2024-11-20". .. deprecated:: 0.0.105 - Use ``settings=OpenAILLMSettings(model=...)`` instead. + Use ``settings=OpenRouterLLMService.Settings(model=...)`` instead. base_url: The base URL for OpenRouter API. Defaults to "https://openrouter.ai/api/v1". settings: Runtime-updatable settings. When provided alongside deprecated @@ -62,11 +61,11 @@ class OpenRouterLLMService(OpenAILLMService): **kwargs: Additional keyword arguments passed to OpenAILLMService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = OpenRouterLLMSettings(model="openai/gpt-4o-2024-11-20") + default_settings = self.Settings(model="openai/gpt-4o-2024-11-20") # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", OpenRouterLLMSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. (No step 3, as there's no params object to apply) diff --git a/src/pipecat/services/perplexity/llm.py b/src/pipecat/services/perplexity/llm.py index b13fb20f0..9ea323c5d 100644 --- a/src/pipecat/services/perplexity/llm.py +++ b/src/pipecat/services/perplexity/llm.py @@ -14,17 +14,19 @@ reporting patterns while maintaining compatibility with the Pipecat framework. from dataclasses import dataclass from typing import Optional +from loguru import logger + from pipecat.adapters.services.open_ai_adapter import OpenAILLMInvocationParams +from pipecat.adapters.services.perplexity_adapter import PerplexityLLMAdapter from pipecat.metrics.metrics import LLMTokenUsage from pipecat.processors.aggregators.llm_context import LLMContext from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext -from pipecat.services.openai.base_llm import OpenAILLMSettings +from pipecat.services.openai.base_llm import BaseOpenAILLMService from pipecat.services.openai.llm import OpenAILLMService -from pipecat.services.settings import _warn_deprecated_param @dataclass -class PerplexityLLMSettings(OpenAILLMSettings): +class PerplexityLLMSettings(BaseOpenAILLMService.Settings): """Settings for PerplexityLLMService.""" pass @@ -38,8 +40,10 @@ class PerplexityLLMService(OpenAILLMService): in token usage reporting between Perplexity (incremental) and OpenAI (final summary). """ + adapter_class = PerplexityLLMAdapter + Settings = PerplexityLLMSettings - _settings: PerplexityLLMSettings + _settings: Settings def __init__( self, @@ -47,7 +51,7 @@ class PerplexityLLMService(OpenAILLMService): api_key: str, base_url: str = "https://api.perplexity.ai", model: Optional[str] = None, - settings: Optional[PerplexityLLMSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Perplexity LLM service. @@ -58,18 +62,18 @@ class PerplexityLLMService(OpenAILLMService): model: The model identifier to use. Defaults to "sonar". .. deprecated:: 0.0.105 - Use ``settings=OpenAILLMSettings(model=...)`` instead. + Use ``settings=PerplexityLLMService.Settings(model=...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional keyword arguments passed to OpenAILLMService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = PerplexityLLMSettings(model="sonar") + default_settings = self.Settings(model="sonar") # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", PerplexityLLMSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. (No step 3, as there's no params object to apply) @@ -120,6 +124,10 @@ class PerplexityLLMService(OpenAILLMService): # Prepend system instruction if set if self._settings.system_instruction: messages = params.get("messages", []) + if messages and messages[0].get("role") == "system": + logger.warning( + f"{self}: Both system_instruction and an initial system message in context are set. This may be unintended." + ) params["messages"] = [ {"role": "system", "content": self._settings.system_instruction} ] + messages diff --git a/src/pipecat/services/piper/tts.py b/src/pipecat/services/piper/tts.py index 3cafd2dcd..1b0037abb 100644 --- a/src/pipecat/services/piper/tts.py +++ b/src/pipecat/services/piper/tts.py @@ -19,7 +19,7 @@ from pipecat.frames.frames import ( Frame, TTSStoppedFrame, ) -from pipecat.services.settings import TTSSettings, _warn_deprecated_param +from pipecat.services.settings import TTSSettings from pipecat.services.tts_service import TTSService from pipecat.utils.tracing.service_decorators import traced_tts @@ -48,7 +48,7 @@ class PiperTTSService(TTSService): """ Settings = PiperTTSSettings - _settings: PiperTTSSettings + _settings: Settings def __init__( self, @@ -57,7 +57,7 @@ class PiperTTSService(TTSService): download_dir: Optional[Path] = None, force_redownload: bool = False, use_cuda: bool = False, - settings: Optional[PiperTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Piper TTS service. @@ -66,7 +66,7 @@ class PiperTTSService(TTSService): voice_id: Piper voice model identifier (e.g. `en_US-ryan-high`). .. deprecated:: 0.0.105 - Use ``settings=PiperTTSSettings(voice=...)`` instead. + Use ``settings=PiperTTSService.Settings(voice=...)`` instead. download_dir: Directory for storing voice model files. Defaults to the current working directory. @@ -77,11 +77,11 @@ class PiperTTSService(TTSService): **kwargs: Additional arguments passed to the parent `TTSService`. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = PiperTTSSettings(model=None, voice=None, language=None) + default_settings = self.Settings(model=None, voice=None, language=None) # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: - _warn_deprecated_param("voice_id", PiperTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id # 3. (No step 3, as there's no params object to apply) @@ -121,7 +121,7 @@ class PiperTTSService(TTSService): """ return True - async def _update_settings(self, delta: PiperTTSSettings) -> dict[str, Any]: + async def _update_settings(self, delta: Settings) -> dict[str, Any]: """Apply a settings delta. Settings are stored but not applied to the active connection. @@ -202,7 +202,7 @@ class PiperHttpTTSService(TTSService): """ Settings = PiperHttpTTSSettings - _settings: PiperHttpTTSSettings + _settings: Settings def __init__( self, @@ -210,7 +210,7 @@ class PiperHttpTTSService(TTSService): base_url: str, aiohttp_session: aiohttp.ClientSession, voice_id: Optional[str] = None, - settings: Optional[PiperHttpTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Piper TTS service. @@ -221,18 +221,18 @@ class PiperHttpTTSService(TTSService): voice_id: Piper voice model identifier (e.g. `en_US-ryan-high`). .. deprecated:: 0.0.105 - Use ``settings=PiperHttpTTSSettings(voice=...)`` instead. + Use ``settings=PiperHttpTTSService.Settings(voice=...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional arguments passed to the parent TTSService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = PiperHttpTTSSettings(model=None, voice=None, language=None) + default_settings = self.Settings(model=None, voice=None, language=None) # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: - _warn_deprecated_param("voice_id", PiperHttpTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id # 3. (No step 3, as there's no params object to apply) diff --git a/src/pipecat/services/qwen/llm.py b/src/pipecat/services/qwen/llm.py index d145f8339..857c89bea 100644 --- a/src/pipecat/services/qwen/llm.py +++ b/src/pipecat/services/qwen/llm.py @@ -11,13 +11,12 @@ from typing import Optional from loguru import logger -from pipecat.services.openai.base_llm import OpenAILLMSettings +from pipecat.services.openai.base_llm import BaseOpenAILLMService from pipecat.services.openai.llm import OpenAILLMService -from pipecat.services.settings import _warn_deprecated_param @dataclass -class QwenLLMSettings(OpenAILLMSettings): +class QwenLLMSettings(BaseOpenAILLMService.Settings): """Settings for QwenLLMService.""" pass @@ -31,7 +30,7 @@ class QwenLLMService(OpenAILLMService): """ Settings = QwenLLMSettings - _settings: QwenLLMSettings + _settings: Settings def __init__( self, @@ -39,7 +38,7 @@ class QwenLLMService(OpenAILLMService): api_key: str, base_url: str = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", model: Optional[str] = None, - settings: Optional[QwenLLMSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Qwen LLM service. @@ -50,18 +49,18 @@ class QwenLLMService(OpenAILLMService): model: The model identifier to use. Defaults to "qwen-plus". .. deprecated:: 0.0.105 - Use ``settings=OpenAILLMSettings(model=...)`` instead. + Use ``settings=QwenLLMService.Settings(model=...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional keyword arguments passed to OpenAILLMService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = QwenLLMSettings(model="qwen-plus") + default_settings = self.Settings(model="qwen-plus") # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", QwenLLMSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. (No step 3, as there's no params object to apply) diff --git a/src/pipecat/services/resembleai/tts.py b/src/pipecat/services/resembleai/tts.py index 31977ca0b..fc70d814b 100644 --- a/src/pipecat/services/resembleai/tts.py +++ b/src/pipecat/services/resembleai/tts.py @@ -23,7 +23,7 @@ from pipecat.frames.frames import ( TTSStartedFrame, TTSStoppedFrame, ) -from pipecat.services.settings import TTSSettings, _warn_deprecated_param +from pipecat.services.settings import TTSSettings from pipecat.services.tts_service import WebsocketTTSService from pipecat.utils.tracing.service_decorators import traced_tts @@ -52,7 +52,7 @@ class ResembleAITTSService(WebsocketTTSService): """ Settings = ResembleAITTSSettings - _settings: ResembleAITTSSettings + _settings: Settings def __init__( self, @@ -63,7 +63,7 @@ class ResembleAITTSService(WebsocketTTSService): precision: Optional[str] = "PCM_16", output_format: Optional[str] = "wav", sample_rate: Optional[int] = 22050, - settings: Optional[ResembleAITTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Resemble AI TTS service. @@ -73,7 +73,7 @@ class ResembleAITTSService(WebsocketTTSService): voice_id: Voice UUID to use for synthesis. .. deprecated:: 0.0.105 - Use ``settings=ResembleAITTSSettings(voice=...)`` instead. + Use ``settings=ResembleAITTSService.Settings(voice=...)`` instead. url: WebSocket URL for Resemble AI TTS API. precision: PCM bit depth (PCM_32, PCM_24, PCM_16, or MULAW). @@ -84,7 +84,7 @@ class ResembleAITTSService(WebsocketTTSService): **kwargs: Additional arguments passed to the parent service. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = ResembleAITTSSettings( + default_settings = self.Settings( model=None, voice=None, language=None, @@ -92,7 +92,7 @@ class ResembleAITTSService(WebsocketTTSService): # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: - _warn_deprecated_param("voice_id", ResembleAITTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id # 3. (No step 3, as there's no params object to apply) diff --git a/src/pipecat/services/rime/tts.py b/src/pipecat/services/rime/tts.py index 15cc8b88a..aec5630b6 100644 --- a/src/pipecat/services/rime/tts.py +++ b/src/pipecat/services/rime/tts.py @@ -31,7 +31,7 @@ from pipecat.frames.frames import ( TTSStoppedFrame, ) from pipecat.processors.frame_processor import FrameDirection -from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven from pipecat.services.tts_service import ( InterruptibleTTSService, TextAggregationMode, @@ -132,13 +132,13 @@ class RimeTTSService(WebsocketTTSService): """ Settings = RimeTTSSettings - _settings: RimeTTSSettings + _settings: Settings class InputParams(BaseModel): """Configuration parameters for Rime TTS service. .. deprecated:: 0.0.105 - Use ``settings=RimeTTSSettings(...)`` instead. + Use ``settings=RimeTTSService.Settings(...)`` instead. Parameters: language: Language for synthesis. Defaults to English. @@ -177,7 +177,7 @@ class RimeTTSService(WebsocketTTSService): model: Optional[str] = None, sample_rate: Optional[int] = None, params: Optional[InputParams] = None, - settings: Optional[RimeTTSSettings] = None, + settings: Optional[Settings] = None, text_aggregator: Optional[BaseTextAggregator] = None, text_aggregation_mode: Optional[TextAggregationMode] = None, aggregate_sentences: Optional[bool] = None, @@ -190,19 +190,19 @@ class RimeTTSService(WebsocketTTSService): voice_id: ID of the voice to use. .. deprecated:: 0.0.105 - Use ``settings=RimeTTSSettings(voice=...)`` instead. + Use ``settings=RimeTTSService.Settings(voice=...)`` instead. url: Rime websocket API endpoint. model: Model ID to use for synthesis. .. deprecated:: 0.0.105 - Use ``settings=RimeTTSSettings(model=...)`` instead. + Use ``settings=RimeTTSService.Settings(model=...)`` instead. sample_rate: Audio sample rate in Hz. params: Additional configuration parameters. .. deprecated:: 0.0.105 - Use ``settings=RimeTTSSettings(...)`` instead. + Use ``settings=RimeTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -220,7 +220,7 @@ class RimeTTSService(WebsocketTTSService): **kwargs: Additional arguments passed to parent class. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = RimeTTSSettings( + default_settings = self.Settings( model="arcana", voice=None, language=None, @@ -241,19 +241,17 @@ class RimeTTSService(WebsocketTTSService): # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: - _warn_deprecated_param("voice_id", RimeTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id if model is not None: - _warn_deprecated_param("model", RimeTTSSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", RimeTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: - default_settings.language = ( - self.language_to_service_language(params.language) if params.language else None - ) + default_settings.language = params.language default_settings.segment = params.segment default_settings.speedAlpha = params.speed_alpha # Arcana params @@ -663,13 +661,13 @@ class RimeHttpTTSService(TTSService): """ Settings = RimeTTSSettings - _settings: RimeTTSSettings + _settings: Settings class InputParams(BaseModel): """Configuration parameters for Rime HTTP TTS service. .. deprecated:: 0.0.105 - Use ``settings=RimeTTSSettings(...)`` instead. + Use ``settings=RimeHttpTTSService.Settings(...)`` instead. Parameters: language: Language for synthesis. Defaults to English. @@ -696,7 +694,7 @@ class RimeHttpTTSService(TTSService): model: Optional[str] = None, sample_rate: Optional[int] = None, params: Optional[InputParams] = None, - settings: Optional[RimeTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize Rime HTTP TTS service. @@ -706,26 +704,26 @@ class RimeHttpTTSService(TTSService): voice_id: ID of the voice to use. .. deprecated:: 0.0.105 - Use ``settings=RimeTTSSettings(voice=...)`` instead. + Use ``settings=RimeHttpTTSService.Settings(voice=...)`` instead. aiohttp_session: Shared aiohttp session for HTTP requests. model: Model ID to use for synthesis. .. deprecated:: 0.0.105 - Use ``settings=RimeTTSSettings(model=...)`` instead. + Use ``settings=RimeHttpTTSService.Settings(model=...)`` instead. sample_rate: Audio sample rate in Hz. params: Additional configuration parameters. .. deprecated:: 0.0.105 - Use ``settings=RimeTTSSettings(...)`` instead. + Use ``settings=RimeHttpTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional arguments passed to parent TTSService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = RimeTTSSettings( + default_settings = self.Settings( model="mistv2", voice=None, language="eng", @@ -744,19 +742,17 @@ class RimeHttpTTSService(TTSService): # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: - _warn_deprecated_param("voice_id", RimeTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id if model is not None: - _warn_deprecated_param("model", RimeTTSSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", RimeTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: - default_settings.language = ( - self.language_to_service_language(params.language) if params.language else "eng" - ) + default_settings.language = params.language default_settings.speedAlpha = params.speed_alpha default_settings.reduceLatency = params.reduce_latency default_settings.pauseBetweenBrackets = params.pause_between_brackets @@ -888,13 +884,13 @@ class RimeNonJsonTTSService(InterruptibleTTSService): """ Settings = RimeNonJsonTTSSettings - _settings: RimeNonJsonTTSSettings + _settings: Settings class InputParams(BaseModel): """Configuration parameters for Rime Non-JSON WebSocket TTS service. .. deprecated:: 0.0.105 - Use ``settings=RimeNonJsonTTSSettings(...)`` instead. + Use ``settings=RimeNonJsonTTSService.Settings(...)`` instead. Args: language: Language for synthesis. Defaults to English. @@ -922,7 +918,7 @@ class RimeNonJsonTTSService(InterruptibleTTSService): audio_format: str = "pcm", sample_rate: Optional[int] = None, params: Optional[InputParams] = None, - settings: Optional[RimeNonJsonTTSSettings] = None, + settings: Optional[Settings] = None, aggregate_sentences: Optional[bool] = None, text_aggregation_mode: Optional[TextAggregationMode] = None, **kwargs, @@ -934,20 +930,20 @@ class RimeNonJsonTTSService(InterruptibleTTSService): voice_id: ID of the voice to use. .. deprecated:: 0.0.105 - Use ``settings=RimeNonJsonTTSSettings(voice=...)`` instead. + Use ``settings=RimeNonJsonTTSService.Settings(voice=...)`` instead. url: Rime websocket API endpoint. model: Model ID to use for synthesis. .. deprecated:: 0.0.105 - Use ``settings=RimeNonJsonTTSSettings(model=...)`` instead. + Use ``settings=RimeNonJsonTTSService.Settings(model=...)`` instead. audio_format: Audio format to use. sample_rate: Audio sample rate in Hz. params: Additional configuration parameters. .. deprecated:: 0.0.105 - Use ``settings=RimeNonJsonTTSSettings(...)`` instead. + Use ``settings=RimeNonJsonTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -962,7 +958,7 @@ class RimeNonJsonTTSService(InterruptibleTTSService): **kwargs: Additional arguments passed to parent class. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = RimeNonJsonTTSSettings( + default_settings = self.Settings( voice=None, model="arcana", language=None, @@ -974,19 +970,17 @@ class RimeNonJsonTTSService(InterruptibleTTSService): # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: - _warn_deprecated_param("voice_id", RimeNonJsonTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id if model is not None: - _warn_deprecated_param("model", RimeNonJsonTTSSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", RimeNonJsonTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: - default_settings.language = ( - self.language_to_service_language(params.language) if params.language else None - ) + default_settings.language = params.language default_settings.segment = params.segment default_settings.repetition_penalty = params.repetition_penalty default_settings.temperature = params.temperature diff --git a/src/pipecat/services/sambanova/llm.py b/src/pipecat/services/sambanova/llm.py index 629c3d4c5..710a22db2 100644 --- a/src/pipecat/services/sambanova/llm.py +++ b/src/pipecat/services/sambanova/llm.py @@ -22,14 +22,13 @@ from pipecat.metrics.metrics import LLMTokenUsage from pipecat.processors.aggregators.llm_context import LLMContext from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext from pipecat.services.llm_service import FunctionCallFromLLM -from pipecat.services.openai.base_llm import OpenAILLMSettings +from pipecat.services.openai.base_llm import BaseOpenAILLMService from pipecat.services.openai.llm import OpenAILLMService -from pipecat.services.settings import _warn_deprecated_param from pipecat.utils.tracing.service_decorators import traced_llm @dataclass -class SambaNovaLLMSettings(OpenAILLMSettings): +class SambaNovaLLMSettings(BaseOpenAILLMService.Settings): """Settings for SambaNovaLLMService.""" pass @@ -43,7 +42,7 @@ class SambaNovaLLMService(OpenAILLMService): # type: ignore """ Settings = SambaNovaLLMSettings - _settings: SambaNovaLLMSettings + _settings: Settings def __init__( self, @@ -51,7 +50,7 @@ class SambaNovaLLMService(OpenAILLMService): # type: ignore api_key: str, model: Optional[str] = None, base_url: str = "https://api.sambanova.ai/v1", - settings: Optional[SambaNovaLLMSettings] = None, + settings: Optional[Settings] = None, **kwargs: Dict[Any, Any], ) -> None: """Initialize SambaNova LLM service. @@ -61,7 +60,7 @@ class SambaNovaLLMService(OpenAILLMService): # type: ignore model: The model identifier to use. Defaults to "Llama-4-Maverick-17B-128E-Instruct". .. deprecated:: 0.0.105 - Use ``settings=OpenAILLMSettings(model=...)`` instead. + Use ``settings=SambaNovaLLMService.Settings(model=...)`` instead. base_url: The base URL for SambaNova API. Defaults to "https://api.sambanova.ai/v1". settings: Runtime-updatable settings. When provided alongside deprecated @@ -69,11 +68,11 @@ class SambaNovaLLMService(OpenAILLMService): # type: ignore **kwargs: Additional keyword arguments passed to OpenAILLMService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = SambaNovaLLMSettings(model="Llama-4-Maverick-17B-128E-Instruct") + default_settings = self.Settings(model="Llama-4-Maverick-17B-128E-Instruct") # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", SambaNovaLLMSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. (No step 3, as there's no params object to apply) @@ -135,6 +134,10 @@ class SambaNovaLLMService(OpenAILLMService): # type: ignore # Prepend system instruction if set if self._settings.system_instruction: messages = params.get("messages", []) + if messages and messages[0].get("role") == "system": + logger.warning( + f"{self}: Both system_instruction and an initial system message in context are set. This may be unintended." + ) params["messages"] = [ {"role": "system", "content": self._settings.system_instruction} ] + messages diff --git a/src/pipecat/services/sambanova/stt.py b/src/pipecat/services/sambanova/stt.py index c273b67eb..5cf12d771 100644 --- a/src/pipecat/services/sambanova/stt.py +++ b/src/pipecat/services/sambanova/stt.py @@ -11,18 +11,16 @@ from typing import Any, Optional from loguru import logger -from pipecat.services.settings import _warn_deprecated_param from pipecat.services.stt_latency import SAMBANOVA_TTFS_P99 from pipecat.services.whisper.base_stt import ( BaseWhisperSTTService, - BaseWhisperSTTSettings, Transcription, ) from pipecat.transcriptions.language import Language @dataclass -class SambaNovaSTTSettings(BaseWhisperSTTSettings): +class SambaNovaSTTSettings(BaseWhisperSTTService.Settings): """Settings for the SambaNova STT service.""" pass @@ -46,7 +44,7 @@ class SambaNovaSTTService(BaseWhisperSTTService): # type: ignore language: Optional[Language] = None, prompt: Optional[str] = None, temperature: Optional[float] = None, - settings: Optional[SambaNovaSTTSettings] = None, + settings: Optional[Settings] = None, ttfs_p99_latency: Optional[float] = SAMBANOVA_TTFS_P99, **kwargs: Any, ) -> None: @@ -56,24 +54,24 @@ class SambaNovaSTTService(BaseWhisperSTTService): # type: ignore model: Whisper model to use. .. deprecated:: 0.0.105 - Use ``settings=SambaNovaSTTSettings(model=...)`` instead. + Use ``settings=SambaNovaSTTService.Settings(model=...)`` instead. api_key: SambaNova API key. Defaults to None. base_url: API base URL. Defaults to "https://api.sambanova.ai/v1". language: Language of the audio input. .. deprecated:: 0.0.105 - Use ``settings=SambaNovaSTTSettings(language=...)`` instead. + Use ``settings=SambaNovaSTTService.Settings(language=...)`` instead. prompt: Optional text to guide the model's style or continue a previous segment. .. deprecated:: 0.0.105 - Use ``settings=SambaNovaSTTSettings(prompt=...)`` instead. + Use ``settings=SambaNovaSTTService.Settings(prompt=...)`` instead. temperature: Optional sampling temperature between 0 and 1. .. deprecated:: 0.0.105 - Use ``settings=SambaNovaSTTSettings(temperature=...)`` instead. + Use ``settings=SambaNovaSTTService.Settings(temperature=...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -82,25 +80,25 @@ class SambaNovaSTTService(BaseWhisperSTTService): # type: ignore **kwargs: Additional arguments passed to `pipecat.services.whisper.base_stt.BaseWhisperSTTService`. """ # --- 1. Hardcoded defaults --- - default_settings = SambaNovaSTTSettings( + default_settings = self.Settings( model="Whisper-Large-v3", - language=self.language_to_service_language(Language.EN), + language=Language.EN, prompt=None, temperature=None, ) # --- 2. Deprecated direct-arg overrides --- if model is not None: - _warn_deprecated_param("model", SambaNovaSTTSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model if language is not None: - _warn_deprecated_param("language", SambaNovaSTTSettings, "language") - default_settings.language = self.language_to_service_language(language) + self._warn_init_param_moved_to_settings("language", "language") + default_settings.language = language if prompt is not None: - _warn_deprecated_param("prompt", SambaNovaSTTSettings, "prompt") + self._warn_init_param_moved_to_settings("prompt", "prompt") default_settings.prompt = prompt if temperature is not None: - _warn_deprecated_param("temperature", SambaNovaSTTSettings, "temperature") + self._warn_init_param_moved_to_settings("temperature", "temperature") default_settings.temperature = temperature # --- 3. (no params object for this service) --- diff --git a/src/pipecat/services/sarvam/stt.py b/src/pipecat/services/sarvam/stt.py index 429973838..8d0a38810 100644 --- a/src/pipecat/services/sarvam/stt.py +++ b/src/pipecat/services/sarvam/stt.py @@ -36,7 +36,6 @@ from pipecat.services.settings import ( NOT_GIVEN, STTSettings, _NotGiven, - _warn_deprecated_param, is_given, ) from pipecat.services.stt_latency import SARVAM_TTFS_P99 @@ -172,13 +171,13 @@ class SarvamSTTService(STTService): """ Settings = SarvamSTTSettings - _settings: SarvamSTTSettings + _settings: Settings class InputParams(BaseModel): """Configuration parameters for Sarvam STT service. .. deprecated:: 0.0.105 - Use ``settings=SarvamSTTSettings(...)`` instead. + Use ``settings=SarvamSTTService.Settings(...)`` instead. Parameters: language: Target language for transcription. @@ -210,7 +209,7 @@ class SarvamSTTService(STTService): sample_rate: Optional[int] = None, input_audio_codec: str = "wav", params: Optional[InputParams] = None, - settings: Optional[SarvamSTTSettings] = None, + settings: Optional[Settings] = None, ttfs_p99_latency: Optional[float] = SARVAM_TTFS_P99, keepalive_timeout: Optional[float] = None, keepalive_interval: float = 5.0, @@ -223,7 +222,7 @@ class SarvamSTTService(STTService): model: Sarvam model to use for transcription. .. deprecated:: 0.0.105 - Use ``settings=SarvamSTTSettings(model=...)`` instead. + Use ``settings=SarvamSTTService.Settings(model=...)`` instead. mode: Mode of operation. Options: transcribe, translate, verbatim, translit, codemix. Only applicable to models that support it @@ -233,7 +232,7 @@ class SarvamSTTService(STTService): params: Configuration parameters for Sarvam STT service. .. deprecated:: 0.0.105 - Use ``settings=SarvamSTTSettings(...)`` instead. + Use ``settings=SarvamSTTService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -245,7 +244,7 @@ class SarvamSTTService(STTService): **kwargs: Additional arguments passed to the parent STTService. """ # --- 1. Hardcoded defaults --- - default_settings = SarvamSTTSettings( + default_settings = self.Settings( model="saarika:v2.5", language=None, prompt=None, @@ -255,12 +254,12 @@ class SarvamSTTService(STTService): # --- 2. Deprecated direct-arg overrides --- if model is not None: - _warn_deprecated_param("model", SarvamSTTSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # --- 3. Deprecated params overrides --- if params is not None: - _warn_deprecated_param("params", SarvamSTTSettings) + self._warn_init_param_moved_to_settings("params") if not settings: default_settings.language = params.language default_settings.prompt = params.prompt @@ -374,7 +373,7 @@ class SarvamSTTService(STTService): """Apply a settings delta, validate, sync state, and reconnect. Args: - delta: A :class:`STTSettings` (or ``SarvamSTTSettings``) delta. + delta: A :class:`STTSettings` (or ``SarvamSTTService.Settings``) delta. Returns: Dict mapping changed field names to their previous values. @@ -389,11 +388,7 @@ class SarvamSTTService(STTService): f"Model '{self._settings.model}' does not support language parameter " "(auto-detects language)." ) - if ( - isinstance(delta, SarvamSTTSettings) - and is_given(delta.prompt) - and delta.prompt is not None - ): + if isinstance(delta, self.Settings) and is_given(delta.prompt) and delta.prompt is not None: if not self._config.supports_prompt: raise ValueError( f"Model '{self._settings.model}' does not support prompt parameter." @@ -417,7 +412,7 @@ class SarvamSTTService(STTService): """Set the transcription/translation prompt and reconnect. .. deprecated:: 0.0.104 - Use ``STTUpdateSettingsFrame(SarvamSTTSettings(prompt=...))`` instead. + Use ``STTUpdateSettingsFrame(SarvamSTTService.Settings(prompt=...))`` instead. Args: prompt: Prompt text to guide transcription/translation style/context. @@ -430,7 +425,7 @@ class SarvamSTTService(STTService): warnings.simplefilter("always") warnings.warn( f"{self.__class__.__name__}.set_prompt() is deprecated. " - "Use STTUpdateSettingsFrame(SarvamSTTSettings(prompt=...)) instead.", + "Use STTUpdateSettingsFrame(self.Settings(prompt=...)) instead.", DeprecationWarning, stacklevel=2, ) diff --git a/src/pipecat/services/sarvam/tts.py b/src/pipecat/services/sarvam/tts.py index 639a860ac..c926270de 100644 --- a/src/pipecat/services/sarvam/tts.py +++ b/src/pipecat/services/sarvam/tts.py @@ -60,7 +60,7 @@ from pipecat.frames.frames import ( ) from pipecat.processors.frame_processor import FrameDirection from pipecat.services.sarvam._sdk import sdk_headers -from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven from pipecat.services.tts_service import InterruptibleTTSService, TextAggregationMode, TTSService from pipecat.transcriptions.language import Language, resolve_language from pipecat.utils.tracing.service_decorators import traced_tts @@ -284,7 +284,7 @@ class SarvamHttpTTSSettings(TTSSettings): class SarvamTTSSettings(SarvamHttpTTSSettings): """Settings for SarvamTTSService. - Extends :class:`SarvamHttpTTSSettings` with WebSocket-specific buffering parameters. + Extends :class:`SarvamHttpTTSService.Settings` with WebSocket-specific buffering parameters. Parameters: min_buffer_size: Minimum characters to buffer before generating audio. @@ -326,39 +326,39 @@ class SarvamHttpTTSService(TTSService): # Using bulbul:v2 (default) tts = SarvamHttpTTSService( api_key="your-api-key", - voice_id="anushka", - model="bulbul:v2", aiohttp_session=session, - params=SarvamHttpTTSService.InputParams( + settings=SarvamHttpTTSService.Settings( + voice="anushka", + model="bulbul:v2", language=Language.HI, pitch=0.1, pace=1.2, - loudness=1.5 - ) + loudness=1.5, + ), ) # Using bulbul:v3-beta with temperature control tts_v3 = SarvamHttpTTSService( api_key="your-api-key", - voice_id="aditya", # Use v3 speaker - model="bulbul:v3-beta", aiohttp_session=session, - params=SarvamHttpTTSService.InputParams( + settings=SarvamHttpTTSService.Settings( + voice="aditya", # Use v3 speaker + model="bulbul:v3-beta", language=Language.HI, pace=1.2, # Range: 0.5-2.0 for v3 - temperature=0.8 - ) + temperature=0.8, + ), ) """ Settings = SarvamHttpTTSSettings - _settings: SarvamHttpTTSSettings + _settings: Settings class InputParams(BaseModel): """Input parameters for Sarvam TTS configuration. .. deprecated:: 0.0.105 - Use ``SarvamHttpTTSSettings`` directly via the ``settings`` parameter instead. + Use ``SarvamHttpTTSService.Settings`` directly via the ``settings`` parameter instead. Parameters: language: Language for synthesis. Defaults to English (India). @@ -416,7 +416,7 @@ class SarvamHttpTTSService(TTSService): base_url: str = "https://api.sarvam.ai", sample_rate: Optional[int] = None, params: Optional[InputParams] = None, - settings: Optional[SarvamHttpTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Sarvam TTS service. @@ -427,14 +427,14 @@ class SarvamHttpTTSService(TTSService): voice_id: Speaker voice ID. If None, uses model-appropriate default. .. deprecated:: 0.0.105 - Use ``settings=SarvamHttpTTSSettings(voice=...)`` instead. + Use ``settings=SarvamHttpTTSService.Settings(voice=...)`` instead. model: TTS model to use. Options: - "bulbul:v2" (default): Standard model with pitch/loudness support - "bulbul:v3-beta": Advanced model with temperature control .. deprecated:: 0.0.105 - Use ``settings=SarvamHttpTTSSettings(model=...)`` instead. + Use ``settings=SarvamHttpTTSService.Settings(model=...)`` instead. base_url: Sarvam AI API base URL. Defaults to "https://api.sarvam.ai". sample_rate: Audio sample rate in Hz (8000, 16000, 22050, 24000). @@ -442,14 +442,14 @@ class SarvamHttpTTSService(TTSService): params: Additional voice and preprocessing parameters. If None, uses defaults. .. deprecated:: 0.0.105 - Use ``settings=SarvamHttpTTSSettings(...)`` instead. + Use ``settings=SarvamHttpTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional arguments passed to parent TTSService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = SarvamHttpTTSSettings( + default_settings = self.Settings( model="bulbul:v2", voice="anushka", language="en-IN", @@ -462,20 +462,18 @@ class SarvamHttpTTSService(TTSService): # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", SarvamHttpTTSSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model if voice_id is not None: - _warn_deprecated_param("voice_id", SarvamHttpTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", SarvamHttpTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: if params.language is not None: - default_settings.language = ( - self.language_to_service_language(params.language) or "en-IN" - ) + default_settings.language = params.language if params.enable_preprocessing is not None: default_settings.enable_preprocessing = params.enable_preprocessing if params.pace is not None: @@ -491,10 +489,6 @@ class SarvamHttpTTSService(TTSService): if settings is not None: default_settings.apply_update(settings) - # Convert Language enum to service-specific string - if isinstance(default_settings.language, Language): - default_settings.language = self.language_to_service_language(default_settings.language) - # Get model configuration (validates model exists) resolved_model = default_settings.model if resolved_model not in TTS_MODEL_CONFIGS: @@ -693,39 +687,39 @@ class SarvamTTSService(InterruptibleTTSService): # Using bulbul:v2 (default) tts = SarvamTTSService( api_key="your-api-key", - voice_id="anushka", - model="bulbul:v2", - params=SarvamTTSService.InputParams( + settings=SarvamTTSService.Settings( + voice="anushka", + model="bulbul:v2", language=Language.HI, pitch=0.1, pace=1.2, - loudness=1.5 - ) + loudness=1.5, + ), ) # Using bulbul:v3-beta with temperature control tts_v3 = SarvamTTSService( api_key="your-api-key", - voice_id="aditya", # Use v3 speaker - model="bulbul:v3-beta", - params=SarvamTTSService.InputParams( + settings=SarvamTTSService.Settings( + voice="aditya", # Use v3 speaker + model="bulbul:v3-beta", language=Language.HI, pace=1.2, # Range: 0.5-2.0 for v3 - temperature=0.8 - ) + temperature=0.8, + ), ) See https://docs.sarvam.ai/api-reference-docs/text-to-speech/stream for API details. """ Settings = SarvamTTSSettings - _settings: SarvamTTSSettings + _settings: Settings class InputParams(BaseModel): """Configuration parameters for Sarvam TTS WebSocket service. .. deprecated:: 0.0.105 - Use ``SarvamTTSSettings`` directly via the ``settings`` parameter instead. + Use ``SarvamTTSService.Settings`` directly via the ``settings`` parameter instead. Parameters: pitch: Voice pitch adjustment (-0.75 to 0.75). Defaults to 0.0. @@ -819,7 +813,7 @@ class SarvamTTSService(InterruptibleTTSService): text_aggregation_mode: Optional[TextAggregationMode] = None, sample_rate: Optional[int] = None, params: Optional[InputParams] = None, - settings: Optional[SarvamTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Sarvam TTS service with voice and transport configuration. @@ -831,12 +825,12 @@ class SarvamTTSService(InterruptibleTTSService): - "bulbul:v3-beta": Advanced model with temperature control .. deprecated:: 0.0.105 - Use ``settings=SarvamTTSSettings(model=...)`` instead. + Use ``settings=SarvamTTSService.Settings(model=...)`` instead. voice_id: Speaker voice ID. If None, uses model-appropriate default. .. deprecated:: 0.0.105 - Use ``settings=SarvamTTSSettings(voice=...)`` instead. + Use ``settings=SarvamTTSService.Settings(voice=...)`` instead. url: WebSocket URL for the TTS backend (default production URL). aggregate_sentences: Deprecated. Use text_aggregation_mode instead. @@ -850,7 +844,7 @@ class SarvamTTSService(InterruptibleTTSService): params: Optional input parameters to override defaults. .. deprecated:: 0.0.105 - Use ``settings=SarvamTTSSettings(...)`` instead. + Use ``settings=SarvamTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -859,7 +853,7 @@ class SarvamTTSService(InterruptibleTTSService): See https://docs.sarvam.ai/api-reference-docs/text-to-speech/stream """ # 1. Initialize default_settings with hardcoded defaults - default_settings = SarvamTTSSettings( + default_settings = self.Settings( model="bulbul:v2", voice="anushka", language="en-IN", @@ -874,10 +868,10 @@ class SarvamTTSService(InterruptibleTTSService): # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", SarvamTTSSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model if voice_id is not None: - _warn_deprecated_param("voice_id", SarvamTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id # Init-only audio format fields (not runtime-updatable) @@ -886,12 +880,10 @@ class SarvamTTSService(InterruptibleTTSService): # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", SarvamTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: if params.language is not None: - default_settings.language = ( - self.language_to_service_language(params.language) or "en-IN" - ) + default_settings.language = params.language if params.enable_preprocessing is not None: default_settings.enable_preprocessing = params.enable_preprocessing if params.min_buffer_size is not None: @@ -915,10 +907,6 @@ class SarvamTTSService(InterruptibleTTSService): if settings is not None: default_settings.apply_update(settings) - # Convert Language enum to service-specific string - if isinstance(default_settings.language, Language): - default_settings.language = self.language_to_service_language(default_settings.language) - # Get model configuration (validates model exists) resolved_model = default_settings.model if resolved_model not in TTS_MODEL_CONFIGS: diff --git a/src/pipecat/services/settings.py b/src/pipecat/services/settings.py index c9a120972..a0bf3cd58 100644 --- a/src/pipecat/services/settings.py +++ b/src/pipecat/services/settings.py @@ -37,7 +37,6 @@ Key helpers: from __future__ import annotations import copy -import warnings from dataclasses import dataclass, field, fields from typing import TYPE_CHECKING, Any, ClassVar, Dict, Mapping, Optional, Type, TypeVar @@ -49,45 +48,6 @@ if TYPE_CHECKING: from pipecat.turns.user_turn_completion_mixin import UserTurnCompletionConfig -# --------------------------------------------------------------------------- -# Deprecation helper -# --------------------------------------------------------------------------- - - -def _warn_deprecated_param( - param_name: str, - settings_class: type, - settings_field: str | None = None, - stacklevel: int = 3, -): - """Emit DeprecationWarning for a deprecated init parameter. - - Args: - param_name: Name of the deprecated parameter. - settings_class: The settings class to use instead. - settings_field: Specific field on the settings class, if different - from *param_name*. - stacklevel: Stack depth for the warning. Default ``3`` targets - the caller's caller (i.e. user code that instantiated the service). - """ - settings_class_name = settings_class.__name__ - if settings_field: - msg = ( - f"The `{param_name}` parameter is deprecated. " - f"Use `settings={settings_class_name}({settings_field}=...)` instead. " - f"If both are provided, `settings` takes precedence." - ) - else: - msg = ( - f"The `{param_name}` parameter is deprecated. " - f"Use `settings={settings_class_name}(...)` instead. " - f"If both are provided, `settings` takes precedence." - ) - with warnings.catch_warnings(): - warnings.simplefilter("always") - warnings.warn(msg, DeprecationWarning, stacklevel=stacklevel) - - # --------------------------------------------------------------------------- # NOT_GIVEN sentinel # --------------------------------------------------------------------------- diff --git a/src/pipecat/services/simli/video.py b/src/pipecat/services/simli/video.py index 880b40428..f994ef0dc 100644 --- a/src/pipecat/services/simli/video.py +++ b/src/pipecat/services/simli/video.py @@ -8,6 +8,7 @@ import asyncio import warnings +from dataclasses import dataclass from typing import Optional import numpy as np @@ -20,11 +21,14 @@ from pipecat.frames.frames import ( Frame, InterruptionFrame, OutputImageRawFrame, + StartFrame, TTSAudioRawFrame, TTSStoppedFrame, UserStartedSpeakingFrame, ) -from pipecat.processors.frame_processor import FrameDirection, FrameProcessor, StartFrame +from pipecat.processors.frame_processor import FrameDirection +from pipecat.services.ai_service import AIService +from pipecat.services.settings import ServiceSettings try: from av.audio.frame import AudioFrame @@ -36,7 +40,14 @@ except ModuleNotFoundError as e: raise Exception(f"Missing module: {e}") -class SimliVideoService(FrameProcessor): +@dataclass +class SimliVideoSettings(ServiceSettings): + """Settings for the Simli video service.""" + + pass + + +class SimliVideoService(AIService): """Simli video service for real-time avatar generation. Provides real-time avatar video generation by processing audio frames @@ -44,9 +55,15 @@ class SimliVideoService(FrameProcessor): audio resampling, video frame processing, and connection management. """ + Settings = SimliVideoSettings + _settings: Settings + class InputParams(BaseModel): """Input parameters for Simli video configuration. + .. deprecated:: 0.0.106 + Use ``SimliVideoService.Settings(...)`` instead. + Parameters: enable_logging: Whether to enable Simli logging. max_session_length: Absolute maximum session duration in seconds. @@ -66,10 +83,13 @@ class SimliVideoService(FrameProcessor): face_id: Optional[str] = None, simli_config: Optional[SimliConfig] = None, use_turn_server: bool = False, - latency_interval: int = 0, simli_url: str = "https://api.simli.ai", is_trinity_avatar: bool = False, params: Optional[InputParams] = None, + max_session_length: Optional[int] = None, + max_idle_time: Optional[int] = None, + enable_logging: Optional[bool] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Simli video service. @@ -90,18 +110,42 @@ class SimliVideoService(FrameProcessor): .. deprecated:: 0.0.95 The 'use_turn_server' parameter is deprecated and will be removed in a future version. - latency_interval: Latency interval setting for sending health checks to check - the latency to Simli Servers. Defaults to 0. simli_url: URL of the simli servers. Can be changed for custom deployments of enterprise users. is_trinity_avatar: Boolean to tell simli client that this is a Trinity avatar which reduces latency when using Trinity. params: Additional input parameters for session configuration. - **kwargs: Additional arguments passed to the parent FrameProcessor. - """ - super().__init__(**kwargs) - params = params or SimliVideoService.InputParams() + .. deprecated:: 0.0.106 + Use ``settings=SimliVideoService.Settings(...)`` instead. + + max_session_length: Absolute maximum session duration in seconds. + Avatar will disconnect after this time even if it's speaking. + max_idle_time: Maximum duration in seconds the avatar is not speaking + before the avatar disconnects. + enable_logging: Whether to enable Simli logging. + settings: Service settings. + **kwargs: Additional arguments passed to the parent AIService. + """ + # 1. Default settings + default_settings = ServiceSettings(model=None) + + # 2. Apply deprecated params overrides + if params is not None: + self._warn_init_param_moved_to_settings("params") + if max_session_length is None and hasattr(params, "max_session_length"): + max_session_length = params.max_session_length + if max_idle_time is None and hasattr(params, "max_idle_time"): + max_idle_time = params.max_idle_time + if enable_logging is None and hasattr(params, "enable_logging"): + enable_logging = params.enable_logging + + # 3. Apply settings delta + if settings is not None: + default_settings.apply_update(settings) + + # 4. Call super + super().__init__(settings=default_settings, **kwargs) # Handle deprecated simli_config parameter if simli_config is not None: @@ -133,10 +177,10 @@ class SimliVideoService(FrameProcessor): config_kwargs = { "faceId": face_id, } - if params.max_session_length is not None: - config_kwargs["maxSessionLength"] = params.max_session_length - if params.max_idle_time is not None: - config_kwargs["maxIdleTime"] = params.max_idle_time + if max_session_length is not None: + config_kwargs["maxSessionLength"] = max_session_length + if max_idle_time is not None: + config_kwargs["maxIdleTime"] = max_idle_time config = SimliConfig(**config_kwargs) @@ -168,6 +212,33 @@ class SimliVideoService(FrameProcessor): self._previously_interrupted = is_trinity_avatar self._audio_buffer = bytearray() + async def start(self, frame: StartFrame): + """Start the Simli video service. + + Args: + frame: The start frame containing initialization parameters. + """ + await super().start(frame) + await self._start_connection() + + async def stop(self, frame: EndFrame): + """Stop the Simli video service. + + Args: + frame: The end frame. + """ + await super().stop(frame) + await self._stop_connection() + + async def cancel(self, frame: CancelFrame): + """Cancel the Simli video service. + + Args: + frame: The cancel frame. + """ + await super().cancel(frame) + await self._stop_connection() + async def _start_connection(self): """Start the connection to Simli service and begin processing tasks.""" try: @@ -222,9 +293,7 @@ class SimliVideoService(FrameProcessor): direction: The direction of frame processing. """ await super().process_frame(frame, direction) - if isinstance(frame, StartFrame): - await self._start_connection() - elif isinstance(frame, TTSAudioRawFrame): + if isinstance(frame, TTSAudioRawFrame): # Send audio frame to Simli try: old_frame = AudioFrame.from_ndarray( @@ -268,8 +337,6 @@ class SimliVideoService(FrameProcessor): except Exception as e: await self.push_error(error_msg=f"Error stopping TTS: {e}", exception=e) return - elif isinstance(frame, (EndFrame, CancelFrame)): - await self._stop() elif isinstance(frame, (InterruptionFrame, UserStartedSpeakingFrame)): if not self._previously_interrupted: await self._simli_client.clearBuffer() @@ -277,7 +344,7 @@ class SimliVideoService(FrameProcessor): await self.push_frame(frame, direction) - async def _stop(self): + async def _stop_connection(self): """Stop the Simli client and cancel processing tasks.""" await self._simli_client.stop() if self._audio_task: diff --git a/src/pipecat/services/soniox/stt.py b/src/pipecat/services/soniox/stt.py index 6e5a8f62c..5163ef113 100644 --- a/src/pipecat/services/soniox/stt.py +++ b/src/pipecat/services/soniox/stt.py @@ -24,10 +24,10 @@ from pipecat.frames.frames import ( VADUserStoppedSpeakingFrame, ) from pipecat.processors.frame_processor import FrameDirection -from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven from pipecat.services.stt_latency import SONIOX_TTFS_P99 from pipecat.services.stt_service import WebsocketSTTService -from pipecat.transcriptions.language import Language +from pipecat.transcriptions.language import Language, resolve_language from pipecat.utils.time import time_now_iso8601 from pipecat.utils.tracing.service_decorators import traced_stt @@ -80,7 +80,7 @@ class SonioxInputParams(BaseModel): """Real-time transcription settings. .. deprecated:: 0.0.105 - Use ``settings=SonioxSTTSettings(...)`` instead. + Use ``settings=SonioxSTTService.Settings(...)`` instead. See Soniox WebSocket API documentation for more details: https://soniox.com/docs/speech-to-text/api-reference/websocket-api#configuration-parameters @@ -118,14 +118,75 @@ def is_end_token(token: dict) -> bool: def language_to_soniox_language(language: Language) -> str: - """Pipecat Language enum uses same ISO 2-letter codes as Soniox, except with added regional variants. + """Convert a Pipecat Language to a Soniox language code. - For a list of all supported languages, see: https://soniox.com/docs/speech-to-text/core-concepts/supported-languages + For a list of all supported languages, see: + https://soniox.com/docs/speech-to-text/core-concepts/supported-languages """ - lang_str = str(language.value).lower() - if "-" in lang_str: - return lang_str.split("-")[0] - return lang_str + LANGUAGE_MAP = { + Language.AF: "af", + Language.AR: "ar", + Language.AZ: "az", + Language.BE: "be", + Language.BG: "bg", + Language.BN: "bn", + Language.BS: "bs", + Language.CA: "ca", + Language.CS: "cs", + Language.CY: "cy", + Language.DA: "da", + Language.DE: "de", + Language.EL: "el", + Language.EN: "en", + Language.ES: "es", + Language.ET: "et", + Language.EU: "eu", + Language.FA: "fa", + Language.FI: "fi", + Language.FR: "fr", + Language.GL: "gl", + Language.GU: "gu", + Language.HE: "he", + Language.HI: "hi", + Language.HR: "hr", + Language.HU: "hu", + Language.ID: "id", + Language.IT: "it", + Language.JA: "ja", + Language.KA: "ka", + Language.KK: "kk", + Language.KN: "kn", + Language.KO: "ko", + Language.LT: "lt", + Language.LV: "lv", + Language.MK: "mk", + Language.ML: "ml", + Language.MR: "mr", + Language.MS: "ms", + Language.NL: "nl", + Language.NO: "no", + Language.PA: "pa", + Language.PL: "pl", + Language.PT: "pt", + Language.RO: "ro", + Language.RU: "ru", + Language.SK: "sk", + Language.SL: "sl", + Language.SQ: "sq", + Language.SR: "sr", + Language.SV: "sv", + Language.SW: "sw", + Language.TA: "ta", + Language.TE: "te", + Language.TH: "th", + Language.TL: "tl", + Language.TR: "tr", + Language.UK: "uk", + Language.UR: "ur", + Language.VI: "vi", + Language.ZH: "zh", + } + return resolve_language(language, LANGUAGE_MAP, use_base_code=True) def _prepare_language_hints( @@ -175,7 +236,7 @@ class SonioxSTTService(WebsocketSTTService): """ Settings = SonioxSTTSettings - _settings: SonioxSTTSettings + _settings: Settings def __init__( self, @@ -188,7 +249,7 @@ class SonioxSTTService(WebsocketSTTService): num_channels: int = 1, params: Optional[SonioxInputParams] = None, vad_force_turn_endpoint: bool = True, - settings: Optional[SonioxSTTSettings] = None, + settings: Optional[Settings] = None, ttfs_p99_latency: Optional[float] = SONIOX_TTFS_P99, **kwargs, ): @@ -201,7 +262,7 @@ class SonioxSTTService(WebsocketSTTService): model: Soniox model to use for transcription. .. deprecated:: 0.0.105 - Use ``settings=SonioxSTTSettings(model=...)`` instead. + Use ``settings=SonioxSTTService.Settings(model=...)`` instead. audio_format: Audio format for transcription. Defaults to ``"pcm_s16le"``. num_channels: Number of audio channels. Defaults to 1. @@ -209,7 +270,7 @@ class SonioxSTTService(WebsocketSTTService): speaker diarization. .. deprecated:: 0.0.105 - Use ``settings=SonioxSTTSettings(...)`` instead. + Use ``settings=SonioxSTTService.Settings(...)`` instead. vad_force_turn_endpoint: Listen to `VADUserStoppedSpeakingFrame` to send finalize message to Soniox. If disabled, Soniox will detect the end of the speech. Defaults to True. @@ -220,7 +281,7 @@ class SonioxSTTService(WebsocketSTTService): **kwargs: Additional arguments passed to the STTService. """ # --- 1. Hardcoded defaults --- - default_settings = SonioxSTTSettings( + default_settings = self.Settings( model="stt-rt-v4", language=None, language_hints=None, @@ -233,12 +294,12 @@ class SonioxSTTService(WebsocketSTTService): # --- 2. Deprecated direct-arg overrides --- if model is not None: - _warn_deprecated_param("model", SonioxSTTSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # --- 3. Deprecated params overrides --- if params is not None: - _warn_deprecated_param("params", SonioxSTTSettings) + self._warn_init_param_moved_to_settings("params") if not settings: default_settings.model = params.model if params.audio_format is not None: @@ -297,7 +358,7 @@ class SonioxSTTService(WebsocketSTTService): await super().start(frame) await self._connect() - async def _update_settings(self, delta: SonioxSTTSettings) -> dict[str, Any]: + async def _update_settings(self, delta: Settings) -> dict[str, Any]: """Apply settings delta and reconnect if anything changed. Args: diff --git a/src/pipecat/services/speechmatics/stt.py b/src/pipecat/services/speechmatics/stt.py index aaff90900..ae8e35850 100644 --- a/src/pipecat/services/speechmatics/stt.py +++ b/src/pipecat/services/speechmatics/stt.py @@ -33,7 +33,7 @@ from pipecat.frames.frames import ( VADUserStoppedSpeakingFrame, ) from pipecat.processors.frame_processor import FrameDirection -from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven from pipecat.services.stt_latency import SPEECHMATICS_TTFS_P99 from pipecat.services.stt_service import STTService from pipecat.transcriptions.language import Language, resolve_language @@ -176,7 +176,7 @@ class SpeechmaticsSTTService(STTService): """ Settings = SpeechmaticsSTTSettings - _settings: SpeechmaticsSTTSettings + _settings: Settings # Export related classes as class attributes TurnDetectionMode = TurnDetectionMode @@ -343,7 +343,7 @@ class SpeechmaticsSTTService(STTService): """Update parameters for Speechmatics STT service. .. deprecated:: 0.0.104 - Use ``SpeechmaticsSTTSettings`` with ``STTUpdateSettingsFrame`` instead. + Use ``SpeechmaticsSTTService.Settings`` with ``STTUpdateSettingsFrame`` instead. Parameters: focus_speakers: List of speaker IDs to focus on. When enabled, only these speakers are @@ -380,7 +380,7 @@ class SpeechmaticsSTTService(STTService): encoding: AudioEncoding = AudioEncoding.PCM_S16LE, params: InputParams | None = None, should_interrupt: bool = True, - settings: SpeechmaticsSTTSettings | None = None, + settings: Settings | None = None, ttfs_p99_latency: float | None = SPEECHMATICS_TTFS_P99, **kwargs, ): @@ -396,7 +396,7 @@ class SpeechmaticsSTTService(STTService): params: Input parameters for the service. .. deprecated:: 0.0.105 - Use ``settings=SpeechmaticsSTTSettings(...)`` instead. + Use ``settings=SpeechmaticsSTTService.Settings(...)`` instead. should_interrupt: Determine whether the bot should be interrupted when Speechmatics turn_detection_mode is configured to detect user speech. settings: Runtime-updatable settings. When provided alongside deprecated @@ -424,7 +424,7 @@ class SpeechmaticsSTTService(STTService): self._check_deprecated_args(kwargs, _params) # --- 1. Hardcoded defaults --- - default_settings = SpeechmaticsSTTSettings( + default_settings = self.Settings( model=None, # Will be resolved from operating_point after config is built language=Language.EN, domain=None, @@ -454,7 +454,7 @@ class SpeechmaticsSTTService(STTService): # --- 3. Deprecated params overrides --- if params is not None: - _warn_deprecated_param("params", SpeechmaticsSTTSettings) + self._warn_init_param_moved_to_settings("params") if not settings: default_settings.language = _params.language default_settings.domain = _params.domain @@ -537,11 +537,11 @@ class SpeechmaticsSTTService(STTService): await super().start(frame) await self._connect() - async def _update_settings(self, delta: SpeechmaticsSTTSettings) -> dict[str, Any]: + async def _update_settings(self, delta: Settings) -> dict[str, Any]: """Apply settings delta, reconnecting only when necessary. Fields are classified into three categories (see - ``SpeechmaticsSTTSettings``): + ``SpeechmaticsSTTService.Settings``): * **HOT_FIELDS** – diarization speaker settings that can be pushed to a live Speechmatics connection without reconnecting. @@ -561,7 +561,7 @@ class SpeechmaticsSTTService(STTService): if not changed: return changed - no_reconnect = SpeechmaticsSTTSettings.HOT_FIELDS | SpeechmaticsSTTSettings.LOCAL_FIELDS + no_reconnect = self.Settings.HOT_FIELDS | self.Settings.LOCAL_FIELDS needs_reconnect = bool(changed.keys() - no_reconnect) if needs_reconnect: @@ -571,7 +571,7 @@ class SpeechmaticsSTTService(STTService): self._config = self._build_config(self._settings) await self._disconnect() await self._connect() - elif changed.keys() & SpeechmaticsSTTSettings.HOT_FIELDS: + elif changed.keys() & self.Settings.HOT_FIELDS: logger.debug(f"{self} applying hot settings update: {changed.keys()}") if self._config.enable_diarization: # Only hot-updatable fields changed — push to the live session. @@ -586,7 +586,7 @@ class SpeechmaticsSTTService(STTService): ) # Diarization not enabled — the new settings will take effect # if/when diarization is enabled, which does require a reconnect. - elif changed.keys() & SpeechmaticsSTTSettings.LOCAL_FIELDS: + elif changed.keys() & self.Settings.LOCAL_FIELDS: logger.debug( f"{self} local settings update, no special action required: {changed.keys()}" ) @@ -705,7 +705,7 @@ class SpeechmaticsSTTService(STTService): # CONFIGURATION # ============================================================================ - def _build_config(self, settings: SpeechmaticsSTTSettings) -> VoiceAgentConfig: + def _build_config(self, settings: Settings) -> VoiceAgentConfig: """Build a ``VoiceAgentConfig`` from the given settings. Used both at init time (with explicit settings, before @@ -778,7 +778,7 @@ class SpeechmaticsSTTService(STTService): .. deprecated:: 0.0.104 Use ``STTUpdateSettingsFrame`` with - ``SpeechmaticsSTTSettings(...)`` instead. + ``SpeechmaticsSTTService.Settings(...)`` instead. This can update the speakers to listen to or ignore during an in-flight transcription. Only available if diarization is enabled. @@ -790,7 +790,7 @@ class SpeechmaticsSTTService(STTService): warnings.simplefilter("always") warnings.warn( "update_params() is deprecated. Use STTUpdateSettingsFrame with " - "SpeechmaticsSTTSettings(...) instead.", + "self.Settings(...) instead.", DeprecationWarning, ) # Check possible diff --git a/src/pipecat/services/speechmatics/tts.py b/src/pipecat/services/speechmatics/tts.py index 20a3212ff..64f64378a 100644 --- a/src/pipecat/services/speechmatics/tts.py +++ b/src/pipecat/services/speechmatics/tts.py @@ -20,7 +20,7 @@ from pipecat.frames.frames import ( Frame, TTSAudioRawFrame, ) -from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven from pipecat.services.tts_service import TTSService from pipecat.utils.network import exponential_backoff_time from pipecat.utils.tracing.service_decorators import traced_tts @@ -54,7 +54,7 @@ class SpeechmaticsTTSService(TTSService): """ Settings = SpeechmaticsTTSSettings - _settings: SpeechmaticsTTSSettings + _settings: Settings SPEECHMATICS_SAMPLE_RATE = 16000 @@ -62,7 +62,7 @@ class SpeechmaticsTTSService(TTSService): """Optional input parameters for Speechmatics TTS configuration. .. deprecated:: 0.0.105 - Use ``settings=SpeechmaticsTTSSettings(...)`` instead. + Use ``settings=SpeechmaticsTTSService.Settings(...)`` instead. Parameters: max_retries: Maximum number of retries for TTS requests. Defaults to 5. @@ -79,7 +79,7 @@ class SpeechmaticsTTSService(TTSService): aiohttp_session: aiohttp.ClientSession, sample_rate: Optional[int] = SPEECHMATICS_SAMPLE_RATE, params: Optional[InputParams] = None, - settings: Optional[SpeechmaticsTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Speechmatics TTS service. @@ -90,14 +90,14 @@ class SpeechmaticsTTSService(TTSService): voice_id: Voice model to use for synthesis. .. deprecated:: 0.0.105 - Use ``settings=SpeechmaticsTTSSettings(voice=...)`` instead. + Use ``settings=SpeechmaticsTTSService.Settings(voice=...)`` instead. aiohttp_session: Shared aiohttp session for HTTP requests. sample_rate: Audio sample rate in Hz. params: Input parameters for the service. .. deprecated:: 0.0.105 - Use ``settings=SpeechmaticsTTSSettings(...)`` instead. + Use ``settings=SpeechmaticsTTSService.Settings(...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. @@ -110,7 +110,7 @@ class SpeechmaticsTTSService(TTSService): ) # 1. Initialize default_settings with hardcoded defaults - default_settings = SpeechmaticsTTSSettings( + default_settings = self.Settings( model=None, voice="sarah", language=None, @@ -119,12 +119,12 @@ class SpeechmaticsTTSService(TTSService): # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: - _warn_deprecated_param("voice_id", SpeechmaticsTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id # 3. Apply params overrides — only if settings not provided if params is not None: - _warn_deprecated_param("params", SpeechmaticsTTSSettings) + self._warn_init_param_moved_to_settings("params") if not settings: default_settings.max_retries = params.max_retries diff --git a/src/pipecat/services/stt_service.py b/src/pipecat/services/stt_service.py index ebf007f6f..ecfec7b9b 100644 --- a/src/pipecat/services/stt_service.py +++ b/src/pipecat/services/stt_service.py @@ -120,6 +120,27 @@ class STTService(AIService): or STTSettings(), **kwargs, ) + + # Convert Language enum to service-specific format at init time. + # Runtime updates are handled by _update_settings(), but init-time + # settings bypass that path and need explicit conversion. + # Raw strings (e.g. "de-DE") are first converted to Language enums + # so they go through the same resolution logic. + if isinstance(self._settings.language, str) and not isinstance( + self._settings.language, Language + ): + try: + self._settings.language = Language(self._settings.language) + except ValueError: + logger.warning( + f"Language string '{self._settings.language}' is not a recognized " + f"Language code. It will be passed to the service as-is." + ) + if isinstance(self._settings.language, Language): + converted = self.language_to_service_language(self._settings.language) + if converted is not None: + self._settings.language = converted + self._audio_passthrough = audio_passthrough self._init_sample_rate = sample_rate self._sample_rate = 0 @@ -285,7 +306,20 @@ class STTService(AIService): Returns: Dict mapping changed field names to their previous values. """ - # Translate language *before* applying so the stored value is canonical + # Translate language *before* applying so the stored value is canonical. + # Raw strings are first converted to Language enums for proper resolution. + if ( + is_given(delta.language) + and isinstance(delta.language, str) + and not isinstance(delta.language, Language) + ): + try: + delta.language = Language(delta.language) + except ValueError: + logger.warning( + f"Language string '{delta.language}' is not a recognized " + f"Language code. It will be passed to the service as-is." + ) if is_given(delta.language) and isinstance(delta.language, Language): converted = self.language_to_service_language(delta.language) if converted is not None: @@ -357,7 +391,9 @@ class STTService(AIService): await self._handle_vad_user_stopped_speaking(frame) await self.push_frame(frame, direction) elif isinstance(frame, STTUpdateSettingsFrame): - if frame.delta is not None: + if frame.service is not None and frame.service is not self: + await self.push_frame(frame, direction) + elif frame.delta is not None: await self._update_settings(frame.delta) elif frame.settings: # Backward-compatible path: convert legacy dict to settings object. diff --git a/src/pipecat/services/tavus/video.py b/src/pipecat/services/tavus/video.py index 3d5946e54..9043710c8 100644 --- a/src/pipecat/services/tavus/video.py +++ b/src/pipecat/services/tavus/video.py @@ -60,7 +60,7 @@ class TavusVideoService(AIService): """ Settings = TavusVideoSettings - _settings: TavusVideoSettings + _settings: Settings def __init__( self, @@ -69,7 +69,7 @@ class TavusVideoService(AIService): replica_id: str, persona_id: str = "pipecat-stream", session: aiohttp.ClientSession, - settings: Optional[TavusVideoSettings] = None, + settings: Optional[Settings] = None, **kwargs, ) -> None: """Initialize the Tavus video service. diff --git a/src/pipecat/services/together/llm.py b/src/pipecat/services/together/llm.py index 21663206e..4ec2f8244 100644 --- a/src/pipecat/services/together/llm.py +++ b/src/pipecat/services/together/llm.py @@ -11,13 +11,12 @@ from typing import Optional from loguru import logger -from pipecat.services.openai.base_llm import OpenAILLMSettings +from pipecat.services.openai.base_llm import BaseOpenAILLMService from pipecat.services.openai.llm import OpenAILLMService -from pipecat.services.settings import _warn_deprecated_param @dataclass -class TogetherLLMSettings(OpenAILLMSettings): +class TogetherLLMSettings(BaseOpenAILLMService.Settings): """Settings for TogetherLLMService.""" pass @@ -31,7 +30,7 @@ class TogetherLLMService(OpenAILLMService): """ Settings = TogetherLLMSettings - _settings: TogetherLLMSettings + _settings: Settings def __init__( self, @@ -39,7 +38,7 @@ class TogetherLLMService(OpenAILLMService): api_key: str, base_url: str = "https://api.together.xyz/v1", model: Optional[str] = None, - settings: Optional[TogetherLLMSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize Together.ai LLM service. @@ -50,18 +49,18 @@ class TogetherLLMService(OpenAILLMService): model: The model identifier to use. Defaults to "meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo". .. deprecated:: 0.0.105 - Use ``settings=OpenAILLMSettings(model=...)`` instead. + Use ``settings=TogetherLLMService.Settings(model=...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional keyword arguments passed to OpenAILLMService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = TogetherLLMSettings(model="meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo") + default_settings = self.Settings(model="meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo") # 2. Apply direct init arg overrides (deprecated) if model is not None: - _warn_deprecated_param("model", TogetherLLMSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model # 3. (No step 3, as there's no params object to apply) diff --git a/src/pipecat/services/tts_service.py b/src/pipecat/services/tts_service.py index 6a04062e5..f16c66191 100644 --- a/src/pipecat/services/tts_service.py +++ b/src/pipecat/services/tts_service.py @@ -43,6 +43,7 @@ from pipecat.frames.frames import ( LLMFullResponseEndFrame, LLMFullResponseStartFrame, StartFrame, + SystemFrame, TextFrame, TranscriptionFrame, TTSAudioRawFrame, @@ -245,6 +246,26 @@ class TTSService(AIService): **kwargs, ) + # Convert Language enum to service-specific format at init time. + # Runtime updates are handled by _update_settings(), but init-time + # settings bypass that path and need explicit conversion. + # Raw strings (e.g. "de-DE") are first converted to Language enums + # so they go through the same resolution logic. + if isinstance(self._settings.language, str) and not isinstance( + self._settings.language, Language + ): + try: + self._settings.language = Language(self._settings.language) + except ValueError: + logger.warning( + f"Language string '{self._settings.language}' is not a recognized " + f"Language code. It will be passed to the service as-is." + ) + if isinstance(self._settings.language, Language): + converted = self.language_to_service_language(self._settings.language) + if converted is not None: + self._settings.language = converted + # Resolve text_aggregation_mode from the new param or deprecated aggregate_sentences if aggregate_sentences is not None: import warnings @@ -536,15 +557,15 @@ class TTSService(AIService): frame: The end frame. """ await super().stop(frame) + if self._audio_context_task: + # Sentinel None shuts down the serialization queue once all + # pending contexts and frames have been processed. + await self._serialization_queue.put(None) + await self._audio_context_task + self._audio_context_task = None if self._stop_frame_task: await self.cancel_task(self._stop_frame_task) self._stop_frame_task = None - if self._audio_context_task: - # Indicate no more audio contexts are available; this will end the - # task cleanly after all contexts have been processed. - await self._contexts_queue.put(None) - await self._audio_context_task - self._audio_context_task = None async def cancel(self, frame: CancelFrame): """Cancel the TTS service. @@ -602,7 +623,20 @@ class TTSService(AIService): Returns: Dict mapping changed field names to their previous values. """ - # Translate language *before* applying so the stored value is canonical + # Translate language *before* applying so the stored value is canonical. + # Raw strings are first converted to Language enums for proper resolution. + if ( + is_given(delta.language) + and isinstance(delta.language, str) + and not isinstance(delta.language, Language) + ): + try: + delta.language = Language(delta.language) + except ValueError: + logger.warning( + f"Language string '{delta.language}' is not a recognized " + f"Language code. It will be passed to the service as-is." + ) if is_given(delta.language) and isinstance(delta.language, Language): converted = self.language_to_service_language(delta.language) if converted is not None: @@ -687,11 +721,6 @@ class TTSService(AIService): self._turn_context_id = self.create_context_id() await self.push_frame(frame, direction) elif isinstance(frame, (LLMFullResponseEndFrame, EndFrame)): - # We pause processing incoming frames if the LLM response included - # text (it might be that it's only a function calling response). We - # pause to avoid audio overlapping. - await self._maybe_pause_frame_processing() - # Flush any remaining text (including text waiting for lookahead) remaining = await self._text_aggregator.flush() # Stop the aggregation metric (no-op if already stopped on first sentence). @@ -699,6 +728,11 @@ class TTSService(AIService): if remaining: await self._push_tts_frames(AggregatedTextFrame(remaining.text, remaining.type)) + # We pause processing incoming frames if the LLM response included + # text (it might be that it's only a function calling response). We + # pause to avoid audio overlapping. + await self._maybe_pause_frame_processing() + # Log accumulated streamed text and emit aggregated usage metric. if self._streamed_text: logger.debug(f"{self}: Generating TTS [{self._streamed_text}]") @@ -738,7 +772,9 @@ class TTSService(AIService): self._turn_context_id = saved_turn_context_id self._processing_text = processing_text elif isinstance(frame, TTSUpdateSettingsFrame): - if frame.delta is not None: + if frame.service is not None and frame.service is not self: + await self.push_frame(frame, direction) + elif frame.delta is not None: await self._update_settings(frame.delta) elif frame.settings: # Backward-compatible path: convert legacy dict to settings object. @@ -756,7 +792,15 @@ class TTSService(AIService): await self._maybe_resume_frame_processing() await self.push_frame(frame, direction) else: - await self.push_frame(frame, direction) + if direction == FrameDirection.DOWNSTREAM and not isinstance(frame, SystemFrame): + # Route non-system downstream frames through the serialization queue so they + # are emitted in the same order they arrive relative to any audio contexts that + # are already queued (e.g. a FooFrame sent right after a TTSSpeakFrame must + # not overtake the TTSStartedFrame / TTSAudioRawFrame / TTSStoppedFrame + # sequence from that speak frame). + await self._serialization_queue.put(frame) + else: + await self.push_frame(frame, direction) async def push_frame(self, frame: Frame, direction: FrameDirection = FrameDirection.DOWNSTREAM): """Push a frame downstream with TTS-specific handling. @@ -959,7 +1003,15 @@ class TTSService(AIService): # is spoken, so we set append_to_context to False. src_frame.append_to_context = False src_frame.context_id = context_id - await self.push_frame(src_frame) + # Route AggregatedTextFrame through the serialization queue so it is emitted + # immediately before the TTSStartedFrame of the audio context it describes, + # rather than racing ahead of audio frames from a previous context. + if not self.audio_context_available(context_id): + await self._serialization_queue.put(src_frame) + # Otherwise, if the context already exists, we append the AggregatedTextFrame + # to the existing context queue. + else: + await self.append_to_audio_context(context_id, src_frame) # Note: Text transformations are meant to only affect the text sent to the TTS for # TTS-specific purposes. This allows for explicit TTS modifications (e.g., inserting @@ -1008,11 +1060,8 @@ class TTSService(AIService): # Only override append_to_context if explicitly set if append_tts_text_to_context is not None: frame.append_to_context = append_tts_text_to_context - # For services using the audio context we are appending to the context, so it preserves the ordering. - if self.audio_context_available(context_id): - await self.append_to_audio_context(context_id, frame) - else: - await self.push_frame(frame) + # Appending to the context, so it preserves the ordering. + await self.append_to_audio_context(context_id, frame) async def tts_process_generator( self, context_id: str, generator: AsyncGenerator[Frame | None, None] @@ -1168,7 +1217,7 @@ class TTSService(AIService): Args: context_id: Unique identifier for the audio context. """ - await self._contexts_queue.put(context_id) + await self._serialization_queue.put(context_id) self._audio_contexts[context_id] = asyncio.Queue() logger.trace(f"{self} created audio context {context_id}") @@ -1179,6 +1228,9 @@ class TTSService(AIService): context_id: The context to append audio to. frame: The audio or control frame to append. """ + if not context_id: + logger.debug(f"{self} unable to append audio to context: no context ID provided") + return if self.audio_context_available(context_id): logger.trace(f"{self} appending audio {frame} to audio context {context_id}") await self._audio_contexts[context_id].put(frame) @@ -1257,7 +1309,14 @@ class TTSService(AIService): def _create_audio_context_task(self): if not self._audio_context_task: - self._contexts_queue: asyncio.Queue = asyncio.Queue() + # Single FIFO queue that serializes everything the TTS service emits downstream. + # Items can be: + # str – an audio context ID: process the per-context audio queue in full before + # moving on (see _handle_audio_context). + # Frame – a non-system downstream frame (e.g. AggregatedTextFrame, FooFrame) that + # must be emitted in-order relative to surrounding audio contexts. + # None – shutdown sentinel (sent by stop()). + self._serialization_queue: asyncio.Queue = asyncio.Queue() self._audio_contexts: Dict[str, asyncio.Queue] = {} self._audio_context_task = self.create_task(self._audio_context_task_handler()) @@ -1267,13 +1326,26 @@ class TTSService(AIService): self._audio_context_task = None async def _audio_context_task_handler(self): - """In this task we process audio contexts in order.""" + """Drain the serialization queue, preserving downstream frame order. + + The queue carries three kinds of items (see _create_audio_context_task): + + * str – audio context ID: block until all audio for that context has been + pushed downstream, then call on_audio_context_completed(). + * Frame – a non-system downstream frame that must be emitted at this exact + position in the output stream (e.g. AggregatedTextFrame preceding + its audio, or an arbitrary frame that arrived between two speak frames). + * None – shutdown sentinel; exit the loop once reached. + """ running = True while running: - context_id = await self._contexts_queue.get() - self._playing_context_id = context_id + context_value = await self._serialization_queue.get() + if isinstance(context_value, Frame): + await self.push_frame(context_value) + elif isinstance(context_value, str): + context_id = context_value + self._playing_context_id = context_id - if context_id: # Process the audio context until the context doesn't have more # audio available (i.e. we find None). await self._handle_audio_context(context_id) @@ -1285,7 +1357,7 @@ class TTSService(AIService): else: running = False - self._contexts_queue.task_done() + self._serialization_queue.task_done() async def _handle_audio_context(self, context_id: str): """Process items from an audio context queue until it is exhausted.""" diff --git a/src/pipecat/services/ultravox/llm.py b/src/pipecat/services/ultravox/llm.py index bfd34ceae..fe8a97549 100644 --- a/src/pipecat/services/ultravox/llm.py +++ b/src/pipecat/services/ultravox/llm.py @@ -167,13 +167,13 @@ class UltravoxRealtimeLLMService(LLMService): """ Settings = UltravoxRealtimeLLMSettings - _settings: UltravoxRealtimeLLMSettings + _settings: Settings def __init__( self, *, params: Union[AgentInputParams, OneShotInputParams, JoinUrlInputParams], - settings: Optional[UltravoxRealtimeLLMSettings] = None, + settings: Optional[Settings] = None, one_shot_selected_tools: Optional[ToolsSchema] = None, **kwargs, ): @@ -188,7 +188,7 @@ class UltravoxRealtimeLLMService(LLMService): **kwargs: Additional arguments passed to parent LLMService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = UltravoxRealtimeLLMSettings( + default_settings = self.Settings( model=None, system_instruction=None, temperature=None, @@ -383,7 +383,7 @@ class UltravoxRealtimeLLMService(LLMService): await self.cancel_task(self._receive_task, timeout=1.0) self._receive_task = None - async def _update_settings(self, delta: UltravoxRealtimeLLMSettings): + async def _update_settings(self, delta: Settings): changed = await super()._update_settings(delta) if "output_medium" in changed: await self._update_output_medium(self._settings.output_medium) diff --git a/src/pipecat/services/whisper/base_stt.py b/src/pipecat/services/whisper/base_stt.py index 869f92a55..33d19b0aa 100644 --- a/src/pipecat/services/whisper/base_stt.py +++ b/src/pipecat/services/whisper/base_stt.py @@ -18,7 +18,7 @@ from openai import AsyncOpenAI from openai.types.audio import Transcription from pipecat.frames.frames import ErrorFrame, Frame, TranscriptionFrame -from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven from pipecat.services.stt_latency import WHISPER_TTFS_P99 from pipecat.services.stt_service import SegmentedSTTService from pipecat.transcriptions.language import Language, resolve_language @@ -123,7 +123,7 @@ class BaseWhisperSTTService(SegmentedSTTService): """ Settings = BaseWhisperSTTSettings - _settings: BaseWhisperSTTSettings + _settings: Settings def __init__( self, @@ -136,7 +136,7 @@ class BaseWhisperSTTService(SegmentedSTTService): temperature: Optional[float] = None, include_prob_metrics: bool = False, push_empty_transcripts: bool = False, - settings: Optional[BaseWhisperSTTSettings] = None, + settings: Optional[Settings] = None, ttfs_p99_latency: Optional[float] = WHISPER_TTFS_P99, **kwargs, ): @@ -146,24 +146,24 @@ class BaseWhisperSTTService(SegmentedSTTService): model: Name of the Whisper model to use. .. deprecated:: 0.0.105 - Use ``settings=BaseWhisperSTTSettings(model=...)`` instead. + Use ``settings=BaseWhisperSTTService.Settings(model=...)`` instead. api_key: Service API key. Defaults to None. base_url: Service API base URL. Defaults to None. language: Language of the audio input. .. deprecated:: 0.0.105 - Use ``settings=BaseWhisperSTTSettings(language=...)`` instead. + Use ``settings=BaseWhisperSTTService.Settings(language=...)`` instead. prompt: Optional text to guide the model's style or continue a previous segment. .. deprecated:: 0.0.105 - Use ``settings=BaseWhisperSTTSettings(prompt=...)`` instead. + Use ``settings=BaseWhisperSTTService.Settings(prompt=...)`` instead. temperature: Sampling temperature between 0 and 1. .. deprecated:: 0.0.105 - Use ``settings=BaseWhisperSTTSettings(temperature=...)`` instead. + Use ``settings=BaseWhisperSTTService.Settings(temperature=...)`` instead. include_prob_metrics: If True, enables probability metrics in API response. Each service implements this differently (see child classes). @@ -181,7 +181,7 @@ class BaseWhisperSTTService(SegmentedSTTService): **kwargs: Additional arguments passed to SegmentedSTTService. """ # --- 1. Hardcoded defaults --- - default_settings = BaseWhisperSTTSettings( + default_settings = self.Settings( model=None, language=None, prompt=None, @@ -190,16 +190,16 @@ class BaseWhisperSTTService(SegmentedSTTService): # --- 2. Deprecated direct-arg overrides --- if model is not None: - _warn_deprecated_param("model", BaseWhisperSTTSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model if language is not None: - _warn_deprecated_param("language", BaseWhisperSTTSettings, "language") - default_settings.language = self.language_to_service_language(language) + self._warn_init_param_moved_to_settings("language", "language") + default_settings.language = language if prompt is not None: - _warn_deprecated_param("prompt", BaseWhisperSTTSettings, "prompt") + self._warn_init_param_moved_to_settings("prompt", "prompt") default_settings.prompt = prompt if temperature is not None: - _warn_deprecated_param("temperature", BaseWhisperSTTSettings, "temperature") + self._warn_init_param_moved_to_settings("temperature", "temperature") default_settings.temperature = temperature # --- 3. (no params object for this service) --- diff --git a/src/pipecat/services/whisper/stt.py b/src/pipecat/services/whisper/stt.py index b5971ce9a..ac5d90c30 100644 --- a/src/pipecat/services/whisper/stt.py +++ b/src/pipecat/services/whisper/stt.py @@ -20,7 +20,7 @@ from loguru import logger from typing_extensions import TYPE_CHECKING, override from pipecat.frames.frames import ErrorFrame, Frame, TranscriptionFrame -from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param +from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven from pipecat.services.stt_service import SegmentedSTTService from pipecat.transcriptions.language import Language, resolve_language from pipecat.utils.time import time_now_iso8601 @@ -208,7 +208,7 @@ class WhisperSTTService(SegmentedSTTService): """ Settings = WhisperSTTSettings - _settings: WhisperSTTSettings + _settings: Settings def __init__( self, @@ -218,7 +218,7 @@ class WhisperSTTService(SegmentedSTTService): compute_type: str = "default", no_speech_prob: Optional[float] = None, language: Optional[Language] = None, - settings: Optional[WhisperSTTSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the Whisper STT service. @@ -227,7 +227,7 @@ class WhisperSTTService(SegmentedSTTService): model: The Whisper model to use for transcription. Can be a Model enum or string. .. deprecated:: 0.0.105 - Use ``settings=WhisperSTTSettings(model=...)`` instead. + Use ``settings=WhisperSTTService.Settings(model=...)`` instead. device: The device to run inference on ('cpu', 'cuda', or 'auto'). Defaults to ``"auto"``. @@ -236,19 +236,19 @@ class WhisperSTTService(SegmentedSTTService): no_speech_prob: Probability threshold for filtering out non-speech segments. .. deprecated:: 0.0.105 - Use ``settings=WhisperSTTSettings(no_speech_prob=...)`` instead. + Use ``settings=WhisperSTTService.Settings(no_speech_prob=...)`` instead. language: The default language for transcription. .. deprecated:: 0.0.105 - Use ``settings=WhisperSTTSettings(language=...)`` instead. + Use ``settings=WhisperSTTService.Settings(language=...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional arguments passed to SegmentedSTTService. """ # --- 1. Hardcoded defaults --- - default_settings = WhisperSTTSettings( + default_settings = self.Settings( model=Model.DISTIL_MEDIUM_EN.value, language=Language.EN, no_speech_prob=0.4, @@ -256,13 +256,13 @@ class WhisperSTTService(SegmentedSTTService): # --- 2. Deprecated direct-arg overrides --- if model is not None: - _warn_deprecated_param("model", WhisperSTTSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model if isinstance(model, str) else model.value if no_speech_prob is not None: - _warn_deprecated_param("no_speech_prob", WhisperSTTSettings, "no_speech_prob") + self._warn_init_param_moved_to_settings("no_speech_prob", "no_speech_prob") default_settings.no_speech_prob = no_speech_prob if language is not None: - _warn_deprecated_param("language", WhisperSTTSettings, "language") + self._warn_init_param_moved_to_settings("language", "language") default_settings.language = language # --- 3. (no params object for this service) --- @@ -382,7 +382,7 @@ class WhisperSTTServiceMLX(WhisperSTTService): """ Settings = WhisperMLXSTTSettings - _settings: WhisperMLXSTTSettings + _settings: Settings def __init__( self, @@ -391,7 +391,7 @@ class WhisperSTTServiceMLX(WhisperSTTService): no_speech_prob: Optional[float] = None, language: Optional[Language] = None, temperature: Optional[float] = None, - settings: Optional[WhisperMLXSTTSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the MLX Whisper STT service. @@ -400,29 +400,29 @@ class WhisperSTTServiceMLX(WhisperSTTService): model: The MLX Whisper model to use for transcription. Can be an MLXModel enum or string. .. deprecated:: 0.0.105 - Use ``settings=WhisperMLXSTTSettings(model=...)`` instead. + Use ``settings=WhisperSTTServiceMLX.Settings(model=...)`` instead. no_speech_prob: Probability threshold for filtering out non-speech segments. .. deprecated:: 0.0.105 - Use ``settings=WhisperMLXSTTSettings(no_speech_prob=...)`` instead. + Use ``settings=WhisperSTTServiceMLX.Settings(no_speech_prob=...)`` instead. language: The default language for transcription. .. deprecated:: 0.0.105 - Use ``settings=WhisperMLXSTTSettings(language=...)`` instead. + Use ``settings=WhisperSTTServiceMLX.Settings(language=...)`` instead. temperature: Temperature for sampling. Can be a float or tuple of floats. .. deprecated:: 0.0.105 - Use ``settings=WhisperMLXSTTSettings(temperature=...)`` instead. + Use ``settings=WhisperSTTServiceMLX.Settings(temperature=...)`` instead. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional arguments passed to SegmentedSTTService. """ # --- 1. Hardcoded defaults --- - default_settings = WhisperMLXSTTSettings( + default_settings = self.Settings( model=MLXModel.TINY.value, language=Language.EN, no_speech_prob=0.6, @@ -432,16 +432,16 @@ class WhisperSTTServiceMLX(WhisperSTTService): # --- 2. Deprecated direct-arg overrides --- if model is not None: - _warn_deprecated_param("model", WhisperMLXSTTSettings, "model") + self._warn_init_param_moved_to_settings("model", "model") default_settings.model = model if isinstance(model, str) else model.value if no_speech_prob is not None: - _warn_deprecated_param("no_speech_prob", WhisperMLXSTTSettings, "no_speech_prob") + self._warn_init_param_moved_to_settings("no_speech_prob", "no_speech_prob") default_settings.no_speech_prob = no_speech_prob if language is not None: - _warn_deprecated_param("language", WhisperMLXSTTSettings, "language") + self._warn_init_param_moved_to_settings("language", "language") default_settings.language = language if temperature is not None: - _warn_deprecated_param("temperature", WhisperMLXSTTSettings, "temperature") + self._warn_init_param_moved_to_settings("temperature", "temperature") default_settings.temperature = temperature # --- 3. (no params object for this service) --- diff --git a/src/pipecat/services/xtts/tts.py b/src/pipecat/services/xtts/tts.py index 32c2781f2..b164f8945 100644 --- a/src/pipecat/services/xtts/tts.py +++ b/src/pipecat/services/xtts/tts.py @@ -23,7 +23,7 @@ from pipecat.frames.frames import ( StartFrame, TTSAudioRawFrame, ) -from pipecat.services.settings import TTSSettings, _warn_deprecated_param +from pipecat.services.settings import TTSSettings from pipecat.services.tts_service import TTSService from pipecat.transcriptions.language import Language, resolve_language from pipecat.utils.tracing.service_decorators import traced_tts @@ -84,7 +84,7 @@ class XTTSService(TTSService): """ Settings = XTTSTTSSettings - _settings: XTTSTTSSettings + _settings: Settings def __init__( self, @@ -94,7 +94,7 @@ class XTTSService(TTSService): aiohttp_session: aiohttp.ClientSession, language: Language = Language.EN, sample_rate: Optional[int] = None, - settings: Optional[XTTSTTSSettings] = None, + settings: Optional[Settings] = None, **kwargs, ): """Initialize the XTTS service. @@ -103,27 +103,34 @@ class XTTSService(TTSService): voice_id: ID of the voice/speaker to use for synthesis. .. deprecated:: 0.0.105 - Use ``settings=XTTSTTSSettings(voice=...)`` instead. + Use ``settings=XTTSService.Settings(voice=...)`` instead. base_url: Base URL of the XTTS streaming server. aiohttp_session: HTTP session for making requests to the server. language: Language for synthesis. Defaults to English. + + .. deprecated:: 0.0.106 + Use ``settings=XTTSService.Settings(language=...)`` instead. + sample_rate: Audio sample rate. If None, uses default. settings: Runtime-updatable settings. When provided alongside deprecated parameters, ``settings`` values take precedence. **kwargs: Additional arguments passed to parent TTSService. """ # 1. Initialize default_settings with hardcoded defaults - default_settings = XTTSTTSSettings( + default_settings = self.Settings( model=None, voice=None, - language=self.language_to_service_language(language), + language=Language.EN, ) # 2. Apply direct init arg overrides (deprecated) if voice_id is not None: - _warn_deprecated_param("voice_id", XTTSTTSSettings, "voice") + self._warn_init_param_moved_to_settings("voice_id", "voice") default_settings.voice = voice_id + if language is not None: + self._warn_init_param_moved_to_settings("language", "language") + default_settings.language = language # 3. (No step 3, as there's no params object to apply) diff --git a/src/pipecat/transcriptions/language.py b/src/pipecat/transcriptions/language.py index a79a85166..1980590e3 100644 --- a/src/pipecat/transcriptions/language.py +++ b/src/pipecat/transcriptions/language.py @@ -631,13 +631,13 @@ def resolve_language( return result # Not in map - fall back with warning - lang_str = str(language.value) + lang_str = str(language) if use_base_code: # Extract base code (e.g., "en" from "en-US") base_code = lang_str.split("-")[0].lower() - logger.warning(f"Language {language.value} not verified. Using base code '{base_code}'.") + logger.warning(f"Language {language} not verified. Using base code '{base_code}'.") return base_code else: - logger.warning(f"Language {language.value} not verified. Using '{lang_str}'.") + logger.warning(f"Language {language} not verified. Using '{lang_str}'.") return lang_str diff --git a/src/pipecat/transports/base_input.py b/src/pipecat/transports/base_input.py index 1da672ab7..bcd457564 100644 --- a/src/pipecat/transports/base_input.py +++ b/src/pipecat/transports/base_input.py @@ -150,8 +150,9 @@ class BaseInputTransport(FrameProcessor): with warnings.catch_warnings(): warnings.simplefilter("always") warnings.warn( - "Parameter 'vad_analyzer' is deprecated, use `LLMUserAggregator`'s new " - "`vad_analyzer` parameter instead.", + "Parameter 'vad_analyzer' is deprecated. Use `LLMUserAggregator`'s " + "`vad_analyzer` parameter, or `VADProcessor` if no `LLMUserAggregator` " + "is needed.", DeprecationWarning, ) diff --git a/src/pipecat/transports/base_output.py b/src/pipecat/transports/base_output.py index e14ae3828..01af97be8 100644 --- a/src/pipecat/transports/base_output.py +++ b/src/pipecat/transports/base_output.py @@ -569,7 +569,11 @@ class BaseOutputTransport(FrameProcessor): if not self._params.video_out_enabled: return - if self._params.video_out_is_live and isinstance(frame, OutputImageRawFrame): + if isinstance(frame, OutputImageRawFrame) and frame.sync_with_audio: + # Route through the audio queue so the image is only + # displayed after all preceding audio has been sent. + await self._audio_queue.put(frame) + elif self._params.video_out_is_live and isinstance(frame, OutputImageRawFrame): await self._video_queue.put(frame) elif isinstance(frame, OutputImageRawFrame): await self._set_video_image(frame) diff --git a/src/pipecat/transports/base_transport.py b/src/pipecat/transports/base_transport.py index 7879976ba..a78e1046c 100644 --- a/src/pipecat/transports/base_transport.py +++ b/src/pipecat/transports/base_transport.py @@ -115,8 +115,9 @@ class TransportParams(BaseModel): vad_analyzer: Voice Activity Detection analyzer instance. .. deprecated:: 0.0.101 - The `vad_analyzer` parameter is deprecated, use `LLMUSerAggregator`'s - new `vad_analyzer` parameter instead. + The `vad_analyzer` parameter is deprecated. Use `LLMUserAggregator`'s + `vad_analyzer` parameter, or `VADProcessor` if no `LLMUserAggregator` + is needed. turn_analyzer: Turn-taking analyzer instance for conversation management. diff --git a/src/pipecat/transports/daily/transport.py b/src/pipecat/transports/daily/transport.py index 421e5db8e..2798f6d5e 100644 --- a/src/pipecat/transports/daily/transport.py +++ b/src/pipecat/transports/daily/transport.py @@ -22,6 +22,7 @@ import aiohttp from loguru import logger from pydantic import BaseModel +from pipecat.audio.dtmf.types import KeypadEntry from pipecat.audio.vad.vad_analyzer import VADAnalyzer, VADParams from pipecat.frames.frames import ( BotConnectedFrame, @@ -31,6 +32,7 @@ from pipecat.frames.frames import ( EndFrame, Frame, InputAudioRawFrame, + InputDTMFFrame, InputTransportMessageFrame, InterimTranscriptionFrame, OutputAudioRawFrame, @@ -57,10 +59,11 @@ try: CallClient, CustomAudioSource, CustomAudioTrack, + CustomVideoSource, + CustomVideoTrack, Daily, EventHandler, VideoFrame, - VirtualCameraDevice, VirtualSpeakerDevice, ) from daily import LogLevel as DailyLogLevel @@ -304,6 +307,44 @@ class DailyTranscriptionSettings(BaseModel): extra: Mapping[str, Any] = {"interim_results": True} +class DailyCustomVideoTrackParams(BaseModel): + """Configuration for a custom video track. + + If ``send_settings`` is not provided, the track will use the default video + publishing settings (framerate, bitrate, codec, etc.). + + Parameters: + width: Video width in pixels. + height: Video height in pixels. + color_format: Video color format (e.g., "RGB", "RGBA", "BGRA"). + send_settings: Optional Daily sendSettings dict for this track. + See https://reference-python.daily.co/types.html#videopublishingsettings + """ + + width: int = 1024 + height: int = 768 + color_format: str = "RGB" + send_settings: Optional[Dict[str, Any]] = None + + +class DailyCustomAudioTrackParams(BaseModel): + """Configuration for a custom audio track. + + If ``send_settings`` is not provided, the track will use the default audio + publishing settings (bitrate, channel config, etc.). + + Parameters: + sample_rate: Audio sample rate in Hz. Defaults to transport's output sample rate. + channels: Number of audio channels. + send_settings: Optional Daily sendSettings dict for this track. + See https://reference-python.daily.co/types.html#audiopublishingsettings + """ + + sample_rate: Optional[int] = None + channels: int = 1 + send_settings: Optional[Dict[str, Any]] = None + + class DailyParams(TransportParams): """Configuration parameters for Daily transport. @@ -311,8 +352,10 @@ class DailyParams(TransportParams): api_url: Daily API base URL. api_key: Daily API authentication key. audio_in_user_tracks: Receive users' audio in separate tracks - dialin_settings: Optional settings for dial-in functionality. camera_out_enabled: Whether to enable the main camera output track. + custom_audio_track_params: Per-destination configuration for custom audio tracks. + custom_video_track_params: Per-destination configuration for custom video tracks. + dialin_settings: Optional settings for dial-in functionality. microphone_out_enabled: Whether to enable the main microphone track. transcription_enabled: Whether to enable speech transcription. transcription_settings: Configuration for transcription service. @@ -321,8 +364,10 @@ class DailyParams(TransportParams): api_url: str = "https://api.daily.co/v1" api_key: str = "" audio_in_user_tracks: bool = True - dialin_settings: Optional[DailyDialinSettings] = None camera_out_enabled: bool = True + custom_audio_track_params: Optional[Mapping[str, DailyCustomAudioTrackParams]] = None + custom_video_track_params: Optional[Mapping[str, DailyCustomVideoTrackParams]] = None + dialin_settings: Optional[DailyDialinSettings] = None microphone_out_enabled: bool = True transcription_enabled: bool = False transcription_settings: DailyTranscriptionSettings = DailyTranscriptionSettings() @@ -351,6 +396,7 @@ class DailyCallbacks(BaseModel): on_dialout_stopped: Called when dial-out is stopped. on_dialout_error: Called when dial-out encounters an error. on_dialout_warning: Called when dial-out has a warning. + on_dtmf_event: Called when a DTMF tone happens. on_participant_joined: Called when a participant joins. on_participant_left: Called when a participant leaves. on_participant_updated: Called when participant info is updated. @@ -381,6 +427,7 @@ class DailyCallbacks(BaseModel): on_dialout_stopped: Callable[[Any], Awaitable[None]] on_dialout_error: Callable[[Any], Awaitable[None]] on_dialout_warning: Callable[[Any], Awaitable[None]] + on_dtmf_event: Callable[[Any], Awaitable[None]] on_participant_joined: Callable[[Mapping[str, Any]], Awaitable[None]] on_participant_left: Callable[[Mapping[str, Any], str], Awaitable[None]] on_participant_updated: Callable[[Mapping[str, Any]], Awaitable[None]] @@ -430,6 +477,19 @@ class DailyAudioTrack: track: CustomAudioTrack +@dataclass +class DailyVideoTrack: + """Container for Daily video track components. + + Parameters: + source: The custom video source for the track. + track: The custom video track instance. + """ + + source: CustomVideoSource + track: CustomVideoTrack + + # This is just a type alias for the errors returned by daily-python. Right now # they are just a string. CallClientError = str @@ -519,14 +579,11 @@ class DailyTransportClient(EventHandler): self._in_sample_rate = 0 self._out_sample_rate = 0 - self._camera: Optional[VirtualCameraDevice] = None self._speaker: Optional[VirtualSpeakerDevice] = None + self._camera_track: Optional[DailyVideoTrack] = None self._microphone_track: Optional[DailyAudioTrack] = None self._custom_audio_tracks: Dict[str, DailyAudioTrack] = {} - - def _camera_name(self): - """Generate a unique virtual camera name for this client instance.""" - return f"camera-{self}" + self._custom_video_tracks: Dict[str, DailyVideoTrack] = {} def _speaker_name(self): """Generate a unique virtual speaker name for this client instance.""" @@ -625,8 +682,29 @@ class DailyTransportClient(EventHandler): Args: destination: The destination identifier to register. """ - self._custom_audio_tracks[destination] = await self.add_custom_audio_track(destination) - self._client.update_publishing({"customAudio": {destination: True}}) + params = (self._params.custom_audio_track_params or {}).get(destination) + self._custom_audio_tracks[destination] = await self.add_custom_audio_track( + destination, params=params + ) + publishing: Dict[str, Any] = {"customAudio": {destination: True}} + if params and params.send_settings: + publishing["customAudio"][destination] = {"sendSettings": params.send_settings} + self._client.update_publishing(publishing) + + async def register_video_destination(self, destination: str): + """Register a custom video destination for multi-track output. + + Args: + destination: The destination identifier to register. + """ + params = (self._params.custom_video_track_params or {}).get(destination) + self._custom_video_tracks[destination] = await self.add_custom_video_track( + destination, params=params + ) + publishing: Dict[str, Any] = {"customVideo": {destination: True}} + if params and params.send_settings: + publishing["customVideo"][destination] = {"sendSettings": params.send_settings} + self._client.update_publishing(publishing) async def write_audio_frame(self, frame: OutputAudioRawFrame) -> bool: """Write an audio frame to the appropriate audio track. @@ -657,7 +735,7 @@ class DailyTransportClient(EventHandler): return num_frames > 0 async def write_video_frame(self, frame: OutputImageRawFrame) -> bool: - """Write a video frame to the camera device. + """Write a video frame to the appropriate video track. Args: frame: The image frame to write. @@ -665,10 +743,20 @@ class DailyTransportClient(EventHandler): Returns: True if the video frame was written successfully, False otherwise. """ - if not frame.transport_destination and self._camera: - self._camera.write_frame(frame.image) + destination = frame.transport_destination + video_source: Optional[CustomVideoSource] = None + if not destination and self._camera_track: + video_source = self._camera_track.source + elif destination and destination in self._custom_video_tracks: + track = self._custom_video_tracks[destination] + video_source = track.source + + if video_source: + video_source.write_frame(frame.image) return True - return False + else: + logger.warning(f"{self} unable to write video frames to destination [{destination}]") + return False async def setup(self, setup: FrameProcessorSetup): """Setup the client with task manager and event queues. @@ -733,13 +821,14 @@ class DailyTransportClient(EventHandler): self._callback_task_handler(self._video_queue), f"{self}::video_callback_task", ) - if self._params.video_out_enabled and not self._camera: - self._camera = Daily.create_camera_device( - self._camera_name(), - width=self._params.video_out_width, - height=self._params.video_out_height, - color_format=self._params.video_out_color_format, + if self._params.video_out_enabled and not self._camera_track: + video_source = CustomVideoSource( + self._params.video_out_width, + self._params.video_out_height, + self._params.video_out_color_format, ) + video_track = CustomVideoTrack(video_source) + self._camera_track = DailyVideoTrack(source=video_source, track=video_track) if self._params.audio_out_enabled and not self._microphone_track: audio_source = CustomAudioSource(self._out_sample_rate, self._params.audio_out_channels) @@ -809,7 +898,11 @@ class DailyTransportClient(EventHandler): "camera": { "isEnabled": camera_enabled, "settings": { - "deviceId": self._camera_name(), + "customTrack": { + "id": self._camera_track.track.id + if self._camera_track + else "no-camera-track" + } }, }, "microphone": { @@ -874,6 +967,8 @@ class DailyTransportClient(EventHandler): # Remove any custom tracks, if any. for track_name, _ in self._custom_audio_tracks.items(): await self.remove_custom_audio_track(track_name) + for track_name, _ in self._custom_video_tracks.items(): + await self.remove_custom_video_track(track_name) error = await self._leave() if not error: @@ -1043,10 +1138,7 @@ class DailyTransportClient(EventHandler): return "Transcription can't be started without a room token" future = self._get_event_loop().create_future() - self._client.start_transcription( - settings=self._params.transcription_settings.model_dump(exclude_none=True), - completion=completion_callback(future), - ) + self._client.start_transcription(settings=settings, completion=completion_callback(future)) return await future async def stop_transcription(self) -> Optional[CallClientError]: @@ -1173,18 +1265,26 @@ class DailyTransportClient(EventHandler): color_format=color_format, ) - async def add_custom_audio_track(self, track_name: str) -> DailyAudioTrack: + async def add_custom_audio_track( + self, + track_name: str, + params: Optional[DailyCustomAudioTrackParams] = None, + ) -> DailyAudioTrack: """Add a custom audio track for multi-stream output. Args: track_name: Name for the custom audio track. + params: Optional per-track configuration for sample rate, channels, and sendSettings. Returns: The created DailyAudioTrack instance. """ future = self._get_event_loop().create_future() - audio_source = CustomAudioSource(self._out_sample_rate, 1) + sample_rate = params.sample_rate if params and params.sample_rate else self._out_sample_rate + channels = params.channels if params else 1 + + audio_source = CustomAudioSource(sample_rate, channels) audio_track = CustomAudioTrack(audio_source) @@ -1217,6 +1317,56 @@ class DailyTransportClient(EventHandler): ) return await future + async def add_custom_video_track( + self, + track_name: str, + params: Optional[DailyCustomVideoTrackParams] = None, + ) -> DailyVideoTrack: + """Add a custom video track for multi-stream output. + + Args: + track_name: Name for the custom video track. + params: Optional per-track configuration for dimensions, color format, and sendSettings. + + Returns: + The created DailyVideoTrack instance. + """ + future = self._get_event_loop().create_future() + + width = params.width if params else self._params.video_out_width + height = params.height if params else self._params.video_out_height + color_format = params.color_format if params else self._params.video_out_color_format + + video_source = CustomVideoSource(width, height, color_format) + + video_track = CustomVideoTrack(video_source) + + self._client.add_custom_video_track( + track_name=track_name, + video_track=video_track, + completion=completion_callback(future), + ) + + await future + + return DailyVideoTrack(source=video_source, track=video_track) + + async def remove_custom_video_track(self, track_name: str) -> Optional[CallClientError]: + """Remove a custom video track. + + Args: + track_name: Name of the custom video track to remove. + + Returns: + error: An error description or None. + """ + future = self._get_event_loop().create_future() + self._client.remove_custom_video_track( + track_name=track_name, + completion=completion_callback(future), + ) + return await future + async def update_transcription( self, participants=None, instance_id=None ) -> Optional[CallClientError]: @@ -1414,6 +1564,14 @@ class DailyTransportClient(EventHandler): """ self._call_event_callback(self._callbacks.on_dialout_warning, data) + def on_dtmf_event(self, data: Any): + """Handle incoming DTMF events. + + Args: + data: DTMF data. + """ + self._call_event_callback(self._callbacks.on_dtmf_event, data) + def on_participant_joined(self, participant): """Handle participant joined events. @@ -1985,7 +2143,7 @@ class DailyOutputTransport(BaseOutputTransport): Args: destination: The destination identifier to register. """ - logger.warning(f"{self} registering video destinations is not supported yet") + await self._client.register_video_destination(destination) async def register_audio_destination(self, destination: str): """Register an audio output destination. @@ -2167,6 +2325,7 @@ class DailyTransport(BaseTransport): on_dialout_stopped=self._on_dialout_stopped, on_dialout_error=self._on_dialout_error, on_dialout_warning=self._on_dialout_warning, + on_dtmf_event=self._on_dtmf_event, on_participant_joined=self._on_participant_joined, on_participant_left=self._on_participant_left, on_participant_updated=self._on_participant_updated, @@ -2208,6 +2367,7 @@ class DailyTransport(BaseTransport): self._register_event_handler("on_dialout_stopped") self._register_event_handler("on_dialout_error") self._register_event_handler("on_dialout_warning") + self._register_event_handler("on_dtmf_event") self._register_event_handler("on_first_participant_joined") self._register_event_handler("on_participant_joined") self._register_event_handler("on_participant_left") @@ -2578,7 +2738,8 @@ class DailyTransport(BaseTransport): if self._params.transcription_enabled: # We report an error because we are starting transcription # internally and if it fails we need to know. - error = await self.start_transcription(self._params.transcription_settings) + settings = self._params.transcription_settings.model_dump(exclude_none=True) + error = await self.start_transcription(settings) if error: await self._on_error(f"Unable to start transcription: {error}") await self._call_event_handler("on_joined", data) @@ -2717,6 +2878,15 @@ class DailyTransport(BaseTransport): logger.warning(f"{self} dial-out warning: {data}") await self._call_event_handler("on_dialout_warning", data) + async def _on_dtmf_event(self, data): + """Handle incoming DTMF events.""" + logger.debug(f"{self} DTMF event: {data}") + await self._call_event_handler("on_dtmf_event", data) + + if self._input: + frame = InputDTMFFrame(button=KeypadEntry(data["tone"])) + await self._input.push_frame(frame) + async def _on_participant_joined(self, participant): """Handle participant joined events.""" id = participant["id"] diff --git a/src/pipecat/transports/daily/utils.py b/src/pipecat/transports/daily/utils.py index 8719cfe7d..8c7526357 100644 --- a/src/pipecat/transports/daily/utils.py +++ b/src/pipecat/transports/daily/utils.py @@ -89,7 +89,7 @@ class DailyRoomProperties(BaseModel): enable_emoji_reactions: Whether emoji reactions are enabled. eject_at_room_exp: Whether to remove participants when room expires. enable_dialout: Whether SIP dial-out is enabled. - enable_recording: Recording settings ('cloud', 'local', 'raw-tracks'). + enable_recording: Recording settings ('cloud', 'cloud-audio-only', 'local', 'raw-tracks'). enable_transcription_storage: Whether transcription storage is enabled. geo: Geographic region for room. max_participants: Maximum number of participants allowed in the room. @@ -185,7 +185,7 @@ class DailyMeetingTokenProperties(BaseModel): enable_screenshare: If True, the user will be able to share their screen. start_video_off: If True, the user's video will be turned off when they join the room. start_audio_off: If True, the user's audio will be turned off when they join the room. - enable_recording: Recording settings for the token. Must be one of 'cloud', 'local' or 'raw-tracks'. + enable_recording: Recording settings for the token. Must be one of 'cloud', 'cloud-audio-only', 'local' or 'raw-tracks'. enable_prejoin_ui: If True, the user will see the prejoin UI before joining the room. start_cloud_recording: Start cloud recording when the user joins the room. permissions: Specifies the initial default permissions for a non-meeting-owner participant. diff --git a/src/pipecat/transports/livekit/transport.py b/src/pipecat/transports/livekit/transport.py index 3fb3d1694..18163cf8e 100644 --- a/src/pipecat/transports/livekit/transport.py +++ b/src/pipecat/transports/livekit/transport.py @@ -388,7 +388,16 @@ class LiveKitTransportClient: await self._audio_source.capture_frame(audio_frame) return True except Exception as e: - logger.error(f"Error publishing audio: {e}") + # When using an audio mixer, the base output transport's + # with_mixer() generator continuously yields frames (mixed with + # background audio) even when no TTS audio is queued. During + # interruptions, the audio task is cancelled and recreated, but + # there is a brief window where the native LiveKit AudioSource + # rejects capture_frame() with an InvalidState error. This is a + # transient condition — the mixer will produce a new frame within + # milliseconds, so we silently drop these frames. + if "InvalidState" not in str(e): + logger.error(f"Error publishing audio: {e}") return False def get_participants(self) -> List[str]: diff --git a/src/pipecat/transports/tavus/transport.py b/src/pipecat/transports/tavus/transport.py index 79be070c5..872e6eefc 100644 --- a/src/pipecat/transports/tavus/transport.py +++ b/src/pipecat/transports/tavus/transport.py @@ -241,6 +241,7 @@ class TavusTransportClient: on_dialout_stopped=partial(self._on_handle_callback, "on_dialout_stopped"), on_dialout_error=partial(self._on_handle_callback, "on_dialout_error"), on_dialout_warning=partial(self._on_handle_callback, "on_dialout_warning"), + on_dtmf_event=partial(self._on_handle_callback, "on_dtmf_event"), on_participant_joined=self._callbacks.on_participant_joined, on_participant_left=self._callbacks.on_participant_left, on_participant_updated=partial(self._on_handle_callback, "on_participant_updated"), diff --git a/src/pipecat/turns/types.py b/src/pipecat/turns/types.py new file mode 100644 index 000000000..5ebdf20a3 --- /dev/null +++ b/src/pipecat/turns/types.py @@ -0,0 +1,24 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""Shared result type for user turn strategy frame processing.""" + +from enum import Enum + + +class ProcessFrameResult(Enum): + """Result of processing a frame in a user turn strategy. + + Controls whether the strategy loop in the controller continues to the + next strategy or stops early. + + Attributes: + CONTINUE: Continue to the next strategy in the loop. + STOP: Stop evaluating further strategies for this frame. + """ + + CONTINUE = "continue" + STOP = "stop" diff --git a/src/pipecat/turns/user_start/__init__.py b/src/pipecat/turns/user_start/__init__.py index fb84f62ed..94d12708d 100644 --- a/src/pipecat/turns/user_start/__init__.py +++ b/src/pipecat/turns/user_start/__init__.py @@ -9,6 +9,7 @@ from .external_user_turn_start_strategy import ExternalUserTurnStartStrategy from .min_words_user_turn_start_strategy import MinWordsUserTurnStartStrategy from .transcription_user_turn_start_strategy import TranscriptionUserTurnStartStrategy from .vad_user_turn_start_strategy import VADUserTurnStartStrategy +from .wake_phrase_user_turn_start_strategy import WakePhraseUserTurnStartStrategy __all__ = [ "BaseUserTurnStartStrategy", @@ -17,4 +18,5 @@ __all__ = [ "TranscriptionUserTurnStartStrategy", "UserTurnStartedParams", "VADUserTurnStartStrategy", + "WakePhraseUserTurnStartStrategy", ] diff --git a/src/pipecat/turns/user_start/base_user_turn_start_strategy.py b/src/pipecat/turns/user_start/base_user_turn_start_strategy.py index 25f5c8303..fd928a8a7 100644 --- a/src/pipecat/turns/user_start/base_user_turn_start_strategy.py +++ b/src/pipecat/turns/user_start/base_user_turn_start_strategy.py @@ -11,6 +11,7 @@ from typing import Optional, Type from pipecat.frames.frames import Frame from pipecat.processors.frame_processor import FrameDirection +from pipecat.turns.types import ProcessFrameResult from pipecat.utils.asyncio.task_manager import BaseTaskManager from pipecat.utils.base_object import BaseObject @@ -76,6 +77,7 @@ class BaseUserTurnStartStrategy(BaseObject): self._register_event_handler("on_push_frame", sync=True) self._register_event_handler("on_broadcast_frame", sync=True) self._register_event_handler("on_user_turn_started", sync=True) + self._register_event_handler("on_reset_aggregation", sync=True) @property def task_manager(self) -> BaseTaskManager: @@ -100,7 +102,7 @@ class BaseUserTurnStartStrategy(BaseObject): """Reset the strategy to its initial state.""" pass - async def process_frame(self, frame: Frame): + async def process_frame(self, frame: Frame) -> ProcessFrameResult: """Process an incoming frame. Subclasses should override this to implement logic that decides whether @@ -108,6 +110,10 @@ class BaseUserTurnStartStrategy(BaseObject): Args: frame: The frame to be processed. + + Returns: + A ProcessFrameResult indicating the outcome. Subclasses that return + None are treated as CONTINUE for backward compatibility. """ pass @@ -138,3 +144,7 @@ class BaseUserTurnStartStrategy(BaseObject): enable_user_speaking_frames=self._enable_user_speaking_frames, ), ) + + async def trigger_reset_aggregation(self): + """Trigger the `on_reset_aggregation` event.""" + await self._call_event_handler("on_reset_aggregation") diff --git a/src/pipecat/turns/user_start/external_user_turn_start_strategy.py b/src/pipecat/turns/user_start/external_user_turn_start_strategy.py index 9a85cad6d..1031a6b46 100644 --- a/src/pipecat/turns/user_start/external_user_turn_start_strategy.py +++ b/src/pipecat/turns/user_start/external_user_turn_start_strategy.py @@ -7,6 +7,7 @@ """User turn start strategy triggered by externally emitted frames.""" from pipecat.frames.frames import Frame, UserStartedSpeakingFrame +from pipecat.turns.types import ProcessFrameResult from pipecat.turns.user_start.base_user_turn_start_strategy import BaseUserTurnStartStrategy @@ -27,13 +28,17 @@ class ExternalUserTurnStartStrategy(BaseUserTurnStartStrategy): """ super().__init__(enable_interruptions=False, enable_user_speaking_frames=False, **kwargs) - async def process_frame(self, frame: Frame): + async def process_frame(self, frame: Frame) -> ProcessFrameResult: """Process an incoming frame to detect user turn start. Args: frame: The frame to be analyzed. - """ - await super().process_frame(frame) + Returns: + STOP if a user started speaking frame was received, CONTINUE otherwise. + """ if isinstance(frame, UserStartedSpeakingFrame): await self.trigger_user_turn_started() + return ProcessFrameResult.STOP + + return ProcessFrameResult.CONTINUE diff --git a/src/pipecat/turns/user_start/min_words_user_turn_start_strategy.py b/src/pipecat/turns/user_start/min_words_user_turn_start_strategy.py index 57a0ca0d8..10bcfcdad 100644 --- a/src/pipecat/turns/user_start/min_words_user_turn_start_strategy.py +++ b/src/pipecat/turns/user_start/min_words_user_turn_start_strategy.py @@ -15,6 +15,7 @@ from pipecat.frames.frames import ( InterimTranscriptionFrame, TranscriptionFrame, ) +from pipecat.turns.types import ProcessFrameResult from pipecat.turns.user_start.base_user_turn_start_strategy import BaseUserTurnStartStrategy @@ -47,7 +48,7 @@ class MinWordsUserTurnStartStrategy(BaseUserTurnStartStrategy): await super().reset() self._bot_speaking = False - async def process_frame(self, frame: Frame): + async def process_frame(self, frame: Frame) -> ProcessFrameResult: """Process an incoming frame to detect the start of a user turn. This method updates internal state based on transcription frames and @@ -55,17 +56,20 @@ class MinWordsUserTurnStartStrategy(BaseUserTurnStartStrategy): Args: frame: The frame to be analyzed. - """ - await super().process_frame(frame) + Returns: + STOP if the minimum word count was reached, CONTINUE otherwise. + """ if isinstance(frame, BotStartedSpeakingFrame): await self._handle_bot_started_speaking(frame) elif isinstance(frame, BotStoppedSpeakingFrame): await self._handle_bot_stopped_speaking(frame) elif isinstance(frame, TranscriptionFrame): - await self._handle_transcription(frame) + return await self._handle_transcription(frame) elif isinstance(frame, InterimTranscriptionFrame) and self._use_interim: - await self._handle_transcription(frame) + return await self._handle_transcription(frame) + + return ProcessFrameResult.CONTINUE async def _handle_bot_started_speaking(self, frame: BotStartedSpeakingFrame): """Handle bot started speaking frame. @@ -87,11 +91,16 @@ class MinWordsUserTurnStartStrategy(BaseUserTurnStartStrategy): """ self._bot_speaking = False - async def _handle_transcription(self, frame: TranscriptionFrame | InterimTranscriptionFrame): - """Handle a completed transcription frame and check word count. + async def _handle_transcription( + self, frame: TranscriptionFrame | InterimTranscriptionFrame + ) -> ProcessFrameResult: + """Handle a transcription frame and check word count. Args: frame: The transcription frame to be processed. + + Returns: + STOP if the minimum word count was reached, CONTINUE otherwise. """ min_words = self._min_words if self._bot_speaking else 1 @@ -106,3 +115,7 @@ class MinWordsUserTurnStartStrategy(BaseUserTurnStartStrategy): if should_trigger: await self.trigger_user_turn_started() + return ProcessFrameResult.STOP + await self.trigger_reset_aggregation() + + return ProcessFrameResult.CONTINUE diff --git a/src/pipecat/turns/user_start/transcription_user_turn_start_strategy.py b/src/pipecat/turns/user_start/transcription_user_turn_start_strategy.py index b69b127ea..34a3e83e6 100644 --- a/src/pipecat/turns/user_start/transcription_user_turn_start_strategy.py +++ b/src/pipecat/turns/user_start/transcription_user_turn_start_strategy.py @@ -7,6 +7,7 @@ """User turn start strategy based on transcriptions.""" from pipecat.frames.frames import Frame, InterimTranscriptionFrame, TranscriptionFrame +from pipecat.turns.types import ProcessFrameResult from pipecat.turns.user_start.base_user_turn_start_strategy import BaseUserTurnStartStrategy @@ -25,15 +26,20 @@ class TranscriptionUserTurnStartStrategy(BaseUserTurnStartStrategy): super().__init__(**kwargs) self._use_interim = use_interim - async def process_frame(self, frame: Frame): + async def process_frame(self, frame: Frame) -> ProcessFrameResult: """Process an incoming frame to detect the start of a user turn. Args: frame: The frame to be processed. - """ - await super().process_frame(frame) + Returns: + STOP if a transcription was received, CONTINUE otherwise. + """ if isinstance(frame, InterimTranscriptionFrame) and self._use_interim: await self.trigger_user_turn_started() + return ProcessFrameResult.STOP elif isinstance(frame, TranscriptionFrame): await self.trigger_user_turn_started() + return ProcessFrameResult.STOP + + return ProcessFrameResult.CONTINUE diff --git a/src/pipecat/turns/user_start/vad_user_turn_start_strategy.py b/src/pipecat/turns/user_start/vad_user_turn_start_strategy.py index 4bdf48594..2bc3875e0 100644 --- a/src/pipecat/turns/user_start/vad_user_turn_start_strategy.py +++ b/src/pipecat/turns/user_start/vad_user_turn_start_strategy.py @@ -7,6 +7,7 @@ """User turn start strategy based on VAD events.""" from pipecat.frames.frames import Frame, VADUserStartedSpeakingFrame +from pipecat.turns.types import ProcessFrameResult from pipecat.turns.user_start.base_user_turn_start_strategy import BaseUserTurnStartStrategy @@ -18,13 +19,17 @@ class VADUserTurnStartStrategy(BaseUserTurnStartStrategy): """ - async def process_frame(self, frame: Frame): + async def process_frame(self, frame: Frame) -> ProcessFrameResult: """Process an incoming frame to detect user turn start. Args: frame: The frame to be analyzed. - """ - await super().process_frame(frame) + Returns: + STOP if the user started speaking, CONTINUE otherwise. + """ if isinstance(frame, VADUserStartedSpeakingFrame): await self.trigger_user_turn_started() + return ProcessFrameResult.STOP + + return ProcessFrameResult.CONTINUE diff --git a/src/pipecat/turns/user_start/wake_phrase_user_turn_start_strategy.py b/src/pipecat/turns/user_start/wake_phrase_user_turn_start_strategy.py new file mode 100644 index 000000000..bcc069ad7 --- /dev/null +++ b/src/pipecat/turns/user_start/wake_phrase_user_turn_start_strategy.py @@ -0,0 +1,281 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""User turn start strategy that gates interaction behind wake phrase detection.""" + +import asyncio +import enum +import re +from typing import List, Optional + +from loguru import logger + +from pipecat.frames.frames import ( + BotSpeakingFrame, + Frame, + TranscriptionFrame, + UserSpeakingFrame, + VADUserStartedSpeakingFrame, +) +from pipecat.turns.types import ProcessFrameResult +from pipecat.turns.user_start.base_user_turn_start_strategy import BaseUserTurnStartStrategy +from pipecat.utils.asyncio.task_manager import BaseTaskManager + + +class _WakeState(enum.Enum): + """Internal state for wake phrase detection.""" + + IDLE = "idle" + AWAKE = "awake" + + +class WakePhraseUserTurnStartStrategy(BaseUserTurnStartStrategy): + """User turn start strategy that requires a wake phrase before interaction. + + Blocks subsequent strategies until a wake phrase is detected in a final + transcription. After detection, allows interaction for a configurable + timeout period before requiring the wake phrase again. Use + ``single_activation=True`` to require the wake phrase before every turn. + + This strategy should be placed first in the start strategies list. + + Event handlers available: + + - on_wake_phrase_detected: Called when a wake phrase is matched. + - on_wake_phrase_timeout: Called when the inactivity timeout expires + (timeout mode only). + + Example:: + + # Timeout mode (default): wake phrase unlocks interaction for 10s + strategy = WakePhraseUserTurnStartStrategy( + phrases=["hey pipecat", "ok pipecat"], + timeout=10.0, + ) + + # Single activation: wake phrase required before every turn + strategy = WakePhraseUserTurnStartStrategy( + phrases=["hey pipecat"], + single_activation=True, + ) + + @strategy.event_handler("on_wake_phrase_detected") + async def on_wake_phrase_detected(strategy, phrase): + ... + + @strategy.event_handler("on_wake_phrase_timeout") + async def on_wake_phrase_timeout(strategy): + ... + + Args: + phrases: List of wake phrases to detect. + timeout: Inactivity timeout in seconds before returning to IDLE. + In timeout mode, the timer resets on activity (user, bot speech). + In single activation mode, acts as a keepalive window — the strategy + stays AWAKE for this duration after wake phrase detection, allowing + the current turn to complete before returning to IDLE. + single_activation: If True, the wake phrase is required before every + turn. The strategy returns to IDLE after each turn completes. + **kwargs: Additional keyword arguments passed to parent. + """ + + def __init__( + self, + *, + phrases: List[str], + timeout: float = 10.0, + single_activation: bool = False, + **kwargs, + ): + """Initialize the wake phrase user turn start strategy. + + Args: + phrases: List of wake phrases to detect. + timeout: Inactivity timeout in seconds before returning to IDLE. + In timeout mode, the timer resets on activity. In single activation + mode, acts as a keepalive window after wake phrase detection. + single_activation: If True, the wake phrase is required before every + turn. The strategy returns to IDLE after each turn completes. + **kwargs: Additional keyword arguments passed to parent. + """ + super().__init__(**kwargs) + self._phrases = phrases + self._timeout = timeout + self._single_activation = single_activation + + self._patterns: List[re.Pattern] = [] + for phrase in phrases: + pattern = re.compile( + r"\b" + r"\s*".join(re.escape(word) for word in phrase.split()) + r"\b", + re.IGNORECASE, + ) + self._patterns.append(pattern) + + self._state = _WakeState.IDLE + self._accumulated_text = "" + + self._timeout_event = asyncio.Event() + self._timeout_task: Optional[asyncio.Task] = None + + self._register_event_handler("on_wake_phrase_detected") + self._register_event_handler("on_wake_phrase_timeout") + + @property + def state(self) -> _WakeState: + """Returns the current wake state.""" + return self._state + + async def setup(self, task_manager: BaseTaskManager): + """Initialize the strategy with the given task manager. + + Args: + task_manager: The task manager to be associated with this instance. + """ + await super().setup(task_manager) + if not self._timeout_task: + self._timeout_task = self.task_manager.create_task( + self._timeout_task_handler(), + f"{self}::_timeout_task_handler", + ) + + async def cleanup(self): + """Cleanup the strategy.""" + await super().cleanup() + if self._timeout_task: + await self.task_manager.cancel_task(self._timeout_task) + self._timeout_task = None + + async def reset(self): + """Reset the strategy. + + In timeout mode, preserves state and refreshes timeout since reset + means a turn started (activity). In single activation mode, does + nothing — the keepalive timeout (started when the wake phrase was + detected) handles the transition back to IDLE. + """ + await super().reset() + if self._state == _WakeState.AWAKE: + if not self._single_activation: + self._refresh_timeout() + + async def process_frame(self, frame: Frame) -> ProcessFrameResult: + """Process an incoming frame for wake phrase detection or passthrough. + + Args: + frame: The frame to be processed. + + Returns: + STOP when the wake phrase is detected or when in IDLE state + (blocks subsequent strategies), CONTINUE when in AWAKE state + (allows subsequent strategies to proceed). + """ + await super().process_frame(frame) + + if self._state == _WakeState.IDLE: + return await self._process_idle(frame) + else: + return await self._process_awake(frame) + + async def _process_idle(self, frame: Frame) -> ProcessFrameResult: + """Process a frame while in IDLE state. + + Only final ``TranscriptionFrame`` instances are checked for wake phrase + matches. When a match is found, a user turn start is triggered. + Transcription frames that don't match have their text cleared so that + pre-wake-phrase speech is not added to the LLM context. All frames + return STOP to block subsequent strategies. + """ + if isinstance(frame, TranscriptionFrame): + if self._check_wake_phrase(frame.text): + await self.trigger_user_turn_started() + return ProcessFrameResult.STOP + await self.trigger_reset_aggregation() + + return ProcessFrameResult.STOP + + async def _process_awake(self, frame: Frame) -> ProcessFrameResult: + """Process a frame while in AWAKE state. + + Refreshes the timeout on activity frames (timeout mode only). Returns + CONTINUE so subsequent strategies can process the frame. + """ + if not self._single_activation: + if isinstance(frame, (UserSpeakingFrame, BotSpeakingFrame)): + self._refresh_timeout() + elif isinstance(frame, TranscriptionFrame): + self._refresh_timeout() + elif isinstance(frame, VADUserStartedSpeakingFrame): + self._refresh_timeout() + + return ProcessFrameResult.CONTINUE + + @staticmethod + def _strip_punctuation(text: str) -> str: + """Strip punctuation from text, keeping only letters, digits, and whitespace.""" + return re.sub(r"[^\w\s]", "", text) + + def _check_wake_phrase(self, text: str) -> bool: + """Check if the accumulated text contains a wake phrase. + + Punctuation is stripped before matching so that STT output like + "Hey, Pipecat!" still matches the phrase "hey pipecat". + + Args: + text: New transcription text to append and check. + + Returns: + True if a wake phrase was found, False otherwise. + """ + self._accumulated_text += " " + self._strip_punctuation(text) + # Cap accumulated text to prevent unbounded growth. + if len(self._accumulated_text) > 250: + self._accumulated_text = self._accumulated_text[-250:] + + for i, pattern in enumerate(self._patterns): + if pattern.search(self._accumulated_text): + phrase = self._phrases[i] + logger.debug(f"{self} wake phrase detected: {phrase!r}") + self._transition_to_awake(phrase) + return True + + return False + + def _transition_to_awake(self, phrase: str): + """Transition from IDLE to AWAKE state.""" + self._state = _WakeState.AWAKE + self._accumulated_text = "" + self._refresh_timeout() + self.task_manager.create_task( + self._call_event_handler("on_wake_phrase_detected", phrase), + f"{self}::on_wake_phrase_detected", + ) + + def _transition_to_idle(self): + """Transition from AWAKE to IDLE state.""" + logger.debug(f"{self} wake phrase timeout, returning to IDLE") + self._state = _WakeState.IDLE + self._accumulated_text = "" + self.task_manager.create_task( + self._call_event_handler("on_wake_phrase_timeout"), + f"{self}::on_wake_phrase_timeout", + ) + + def _refresh_timeout(self): + """Refresh the inactivity timeout.""" + self._timeout_event.set() + + async def _timeout_task_handler(self): + """Background task that monitors inactivity timeout.""" + while True: + try: + await asyncio.wait_for( + self._timeout_event.wait(), + timeout=self._timeout, + ) + self._timeout_event.clear() + except asyncio.TimeoutError: + if self._state == _WakeState.AWAKE: + self._transition_to_idle() diff --git a/src/pipecat/turns/user_stop/base_user_turn_stop_strategy.py b/src/pipecat/turns/user_stop/base_user_turn_stop_strategy.py index c0042f902..7c493475e 100644 --- a/src/pipecat/turns/user_stop/base_user_turn_stop_strategy.py +++ b/src/pipecat/turns/user_stop/base_user_turn_stop_strategy.py @@ -11,6 +11,7 @@ from typing import Optional, Type from pipecat.frames.frames import Frame from pipecat.processors.frame_processor import FrameDirection +from pipecat.turns.types import ProcessFrameResult from pipecat.utils.asyncio.task_manager import BaseTaskManager from pipecat.utils.base_object import BaseObject @@ -89,7 +90,7 @@ class BaseUserTurnStopStrategy(BaseObject): """Reset the strategy to its initial state.""" pass - async def process_frame(self, frame: Frame): + async def process_frame(self, frame: Frame) -> ProcessFrameResult: """Process an incoming frame to decide whether the user stopped speaking. Subclasses should override this to implement logic that decides whether @@ -97,6 +98,10 @@ class BaseUserTurnStopStrategy(BaseObject): Args: frame: The frame to be analyzed. + + Returns: + A ProcessFrameResult indicating the outcome. Subclasses that return + None are treated as CONTINUE for backward compatibility. """ pass diff --git a/src/pipecat/turns/user_stop/external_user_turn_stop_strategy.py b/src/pipecat/turns/user_stop/external_user_turn_stop_strategy.py index 58e037fbf..4ffa3360b 100644 --- a/src/pipecat/turns/user_stop/external_user_turn_stop_strategy.py +++ b/src/pipecat/turns/user_stop/external_user_turn_stop_strategy.py @@ -16,6 +16,7 @@ from pipecat.frames.frames import ( UserStartedSpeakingFrame, UserStoppedSpeakingFrame, ) +from pipecat.turns.types import ProcessFrameResult from pipecat.turns.user_stop.base_user_turn_stop_strategy import BaseUserTurnStopStrategy from pipecat.utils.asyncio.task_manager import BaseTaskManager @@ -69,7 +70,7 @@ class ExternalUserTurnStopStrategy(BaseUserTurnStopStrategy): await self.task_manager.cancel_task(self._task) self._task = None - async def process_frame(self, frame: Frame): + async def process_frame(self, frame: Frame) -> ProcessFrameResult: """Process an incoming frame to update strategy state. Updates internal transcription text and VAD state. The user end turn @@ -78,6 +79,8 @@ class ExternalUserTurnStopStrategy(BaseUserTurnStopStrategy): Args: frame: The frame to be analyzed. + Returns: + Always returns CONTINUE so subsequent stop strategies are evaluated. """ if isinstance(frame, UserStartedSpeakingFrame): await self._handle_user_started_speaking(frame) @@ -88,6 +91,8 @@ class ExternalUserTurnStopStrategy(BaseUserTurnStopStrategy): elif isinstance(frame, TranscriptionFrame): await self._handle_transcription(frame) + return ProcessFrameResult.CONTINUE + async def _handle_user_started_speaking(self, _: UserStartedSpeakingFrame): """Handle when the external service indicates the user is speaking.""" self._user_speaking = True diff --git a/src/pipecat/turns/user_stop/speech_timeout_user_turn_stop_strategy.py b/src/pipecat/turns/user_stop/speech_timeout_user_turn_stop_strategy.py index 66d6fa703..341a4c1e3 100644 --- a/src/pipecat/turns/user_stop/speech_timeout_user_turn_stop_strategy.py +++ b/src/pipecat/turns/user_stop/speech_timeout_user_turn_stop_strategy.py @@ -17,6 +17,7 @@ from pipecat.frames.frames import ( VADUserStartedSpeakingFrame, VADUserStoppedSpeakingFrame, ) +from pipecat.turns.types import ProcessFrameResult from pipecat.turns.user_stop.base_user_turn_stop_strategy import BaseUserTurnStopStrategy from pipecat.utils.asyncio.task_manager import BaseTaskManager @@ -64,6 +65,9 @@ class SpeechTimeoutUserTurnStopStrategy(BaseUserTurnStopStrategy): self._vad_user_speaking = False self._transcript_finalized = False self._vad_stopped_time = None + if self._timeout_task: + await self.task_manager.cancel_task(self._timeout_task) + self._timeout_task = None async def setup(self, task_manager: BaseTaskManager): """Initialize the strategy with the given task manager. @@ -80,7 +84,7 @@ class SpeechTimeoutUserTurnStopStrategy(BaseUserTurnStopStrategy): await self.task_manager.cancel_task(self._timeout_task) self._timeout_task = None - async def process_frame(self, frame: Frame): + async def process_frame(self, frame: Frame) -> ProcessFrameResult: """Process an incoming frame to update strategy state. Updates internal transcription text and VAD state. The user end turn @@ -89,6 +93,8 @@ class SpeechTimeoutUserTurnStopStrategy(BaseUserTurnStopStrategy): Args: frame: The frame to be analyzed. + Returns: + Always returns CONTINUE so subsequent stop strategies are evaluated. """ if isinstance(frame, STTMetadataFrame): self._stt_timeout = frame.ttfs_p99_latency @@ -99,6 +105,8 @@ class SpeechTimeoutUserTurnStopStrategy(BaseUserTurnStopStrategy): elif isinstance(frame, TranscriptionFrame): await self._handle_transcription(frame) + return ProcessFrameResult.CONTINUE + async def _handle_vad_user_started_speaking(self, _: VADUserStartedSpeakingFrame): """Handle when the VAD indicates the user is speaking.""" self._vad_user_speaking = True diff --git a/src/pipecat/turns/user_stop/turn_analyzer_user_turn_stop_strategy.py b/src/pipecat/turns/user_stop/turn_analyzer_user_turn_stop_strategy.py index f141a75b7..a0df2efbb 100644 --- a/src/pipecat/turns/user_stop/turn_analyzer_user_turn_stop_strategy.py +++ b/src/pipecat/turns/user_stop/turn_analyzer_user_turn_stop_strategy.py @@ -22,6 +22,7 @@ from pipecat.frames.frames import ( VADUserStoppedSpeakingFrame, ) from pipecat.metrics.metrics import MetricsData +from pipecat.turns.types import ProcessFrameResult from pipecat.turns.user_stop.base_user_turn_stop_strategy import BaseUserTurnStopStrategy from pipecat.utils.asyncio.task_manager import BaseTaskManager @@ -68,6 +69,9 @@ class TurnAnalyzerUserTurnStopStrategy(BaseUserTurnStopStrategy): self._vad_user_speaking = False self._vad_stopped_time = None self._transcript_finalized = False + if self._timeout_task: + await self.task_manager.cancel_task(self._timeout_task) + self._timeout_task = None async def setup(self, task_manager: BaseTaskManager): """Initialize the strategy with the given task manager. @@ -85,11 +89,14 @@ class TurnAnalyzerUserTurnStopStrategy(BaseUserTurnStopStrategy): await self.task_manager.cancel_task(self._timeout_task) self._timeout_task = None - async def process_frame(self, frame: Frame): + async def process_frame(self, frame: Frame) -> ProcessFrameResult: """Process an incoming frame to update the turn analyzer and strategy state. Args: frame: The frame to be analyzed. + + Returns: + Always returns CONTINUE so subsequent stop strategies are evaluated. """ await super().process_frame(frame) @@ -106,6 +113,8 @@ class TurnAnalyzerUserTurnStopStrategy(BaseUserTurnStopStrategy): elif isinstance(frame, TranscriptionFrame): await self._handle_transcription(frame) + return ProcessFrameResult.CONTINUE + async def _start(self, frame: StartFrame): """Process the start frame to configure the turn analyzer.""" self._turn_analyzer.set_sample_rate(frame.audio_in_sample_rate) diff --git a/src/pipecat/turns/user_turn_controller.py b/src/pipecat/turns/user_turn_controller.py index adaafd298..9abed3932 100644 --- a/src/pipecat/turns/user_turn_controller.py +++ b/src/pipecat/turns/user_turn_controller.py @@ -19,7 +19,11 @@ from pipecat.frames.frames import ( VADUserStoppedSpeakingFrame, ) from pipecat.processors.frame_processor import FrameDirection -from pipecat.turns.user_start import BaseUserTurnStartStrategy, UserTurnStartedParams +from pipecat.turns.types import ProcessFrameResult +from pipecat.turns.user_start import ( + BaseUserTurnStartStrategy, + UserTurnStartedParams, +) from pipecat.turns.user_stop import BaseUserTurnStopStrategy, UserTurnStoppedParams from pipecat.turns.user_turn_strategies import UserTurnStrategies from pipecat.utils.asyncio.task_manager import BaseTaskManager @@ -94,6 +98,7 @@ class UserTurnController(BaseObject): self._register_event_handler("on_user_turn_started", sync=True) self._register_event_handler("on_user_turn_stopped", sync=True) self._register_event_handler("on_user_turn_stop_timeout", sync=True) + self._register_event_handler("on_reset_aggregation", sync=True) @property def task_manager(self) -> BaseTaskManager: @@ -161,10 +166,14 @@ class UserTurnController(BaseObject): await self._handle_transcription(frame) for strategy in self._user_turn_strategies.start or []: - await strategy.process_frame(frame) + result = await strategy.process_frame(frame) + if result == ProcessFrameResult.STOP: + break for strategy in self._user_turn_strategies.stop or []: - await strategy.process_frame(frame) + result = await strategy.process_frame(frame) + if result == ProcessFrameResult.STOP: + break async def _setup_strategies(self): for s in self._user_turn_strategies.start or []: @@ -172,6 +181,7 @@ class UserTurnController(BaseObject): s.add_event_handler("on_push_frame", self._on_push_frame) s.add_event_handler("on_broadcast_frame", self._on_broadcast_frame) s.add_event_handler("on_user_turn_started", self._on_user_turn_started) + s.add_event_handler("on_reset_aggregation", self._on_reset_aggregation) for s in self._user_turn_strategies.stop or []: await s.setup(self.task_manager) @@ -242,6 +252,9 @@ class UserTurnController(BaseObject): ): await self._trigger_user_turn_stop(strategy, params) + async def _on_reset_aggregation(self, strategy: BaseUserTurnStartStrategy): + await self._call_event_handler("on_reset_aggregation", strategy) + async def _trigger_user_turn_start( self, strategy: Optional[BaseUserTurnStartStrategy], params: UserTurnStartedParams ): @@ -256,6 +269,10 @@ class UserTurnController(BaseObject): for s in self._user_turn_strategies.start or []: await s.reset() + # Reset all user turn stop strategies to start fresh for the new turn. + for s in self._user_turn_strategies.stop or []: + await s.reset() + await self._call_event_handler("on_user_turn_started", strategy, params) async def _trigger_user_turn_stop( diff --git a/src/pipecat/turns/user_turn_strategies.py b/src/pipecat/turns/user_turn_strategies.py index 0435f141c..5789c6328 100644 --- a/src/pipecat/turns/user_turn_strategies.py +++ b/src/pipecat/turns/user_turn_strategies.py @@ -23,6 +23,31 @@ from pipecat.turns.user_stop import ( ) +def default_user_turn_start_strategies() -> List[BaseUserTurnStartStrategy]: + """Return the default user turn start strategies. + + Returns ``[VADUserTurnStartStrategy, TranscriptionUserTurnStartStrategy]``. + Useful when building a custom strategy list that extends the defaults. + + Example:: + + start_strategies = [ + WakePhraseUserTurnStartStrategy(phrases=["hey pipecat"]), + *default_user_turn_start_strategies(), + ] + """ + return [VADUserTurnStartStrategy(), TranscriptionUserTurnStartStrategy()] + + +def default_user_turn_stop_strategies() -> List[BaseUserTurnStopStrategy]: + """Return the default user turn stop strategies. + + Returns ``[TurnAnalyzerUserTurnStopStrategy(LocalSmartTurnAnalyzerV3)]``. + Useful when building a custom strategy list that extends the defaults. + """ + return [TurnAnalyzerUserTurnStopStrategy(turn_analyzer=LocalSmartTurnAnalyzerV3())] + + @dataclass class UserTurnStrategies: """Container for user turn start and stop strategies. @@ -45,9 +70,9 @@ class UserTurnStrategies: def __post_init__(self): if not self.start: - self.start = [VADUserTurnStartStrategy(), TranscriptionUserTurnStartStrategy()] + self.start = default_user_turn_start_strategies() if not self.stop: - self.stop = [TurnAnalyzerUserTurnStopStrategy(turn_analyzer=LocalSmartTurnAnalyzerV3())] + self.stop = default_user_turn_stop_strategies() @dataclass diff --git a/src/pipecat/utils/tracing/service_attributes.py b/src/pipecat/utils/tracing/service_attributes.py index 97ac49d87..21a22341f 100644 --- a/src/pipecat/utils/tracing/service_attributes.py +++ b/src/pipecat/utils/tracing/service_attributes.py @@ -107,7 +107,7 @@ def add_tts_span_attributes( if ttfb is not None: span.set_attribute("metrics.ttfb", ttfb) - # Add settings if provided + # Use given_fields() defensively in case a service doesn't initialize all settings. if settings: for key, value in settings.given_fields().items(): if isinstance(value, (str, int, float, bool)): @@ -171,7 +171,7 @@ def add_stt_span_attributes( if ttfb is not None: span.set_attribute("metrics.ttfb", ttfb) - # Add settings if provided + # Use given_fields() defensively in case a service doesn't initialize all settings. if settings: for key, value in settings.given_fields().items(): if isinstance(value, (str, int, float, bool)): @@ -359,7 +359,7 @@ def add_gemini_live_span_attributes( if tools_serialized: span.set_attribute("tools.definitions", tools_serialized) - # Add settings if provided + # Use given_fields() defensively in case a service doesn't initialize all settings. if settings: for key, value in settings.given_fields().items(): if isinstance(value, (str, int, float, bool)): diff --git a/src/pipecat/utils/tracing/service_decorators.py b/src/pipecat/utils/tracing/service_decorators.py index 6f772aa88..0b63989ea 100644 --- a/src/pipecat/utils/tracing/service_decorators.py +++ b/src/pipecat/utils/tracing/service_decorators.py @@ -51,8 +51,10 @@ def _get_model_name(service) -> str: check all the places we used to store it. """ return ( - getattr(getattr(service, "_settings", None), "model", None) - or getattr(service, "_full_model_name", None) + # Some services store an API-response-provided detailed "full" name, + # which is distinct from the user-provided model name + getattr(service, "_full_model_name", None) + or getattr(getattr(service, "_settings", None), "model", None) or getattr(service, "model_name", None) or getattr(service, "_model_name", None) or "unknown" @@ -524,7 +526,8 @@ def traced_llm(func: Optional[Callable] = None, *, name: Optional[str] = None) - elif hasattr(self, "_system_instruction"): system_message = self._system_instruction - # Get settings from the service + # Use given_fields() defensively in case a service doesn't + # initialize all settings. params = {} if hasattr(self, "_settings"): for key, value in self._settings.given_fields().items(): diff --git a/tests/test_aic_filter.py b/tests/test_aic_filter.py index 08c702986..b04054180 100644 --- a/tests/test_aic_filter.py +++ b/tests/test_aic_filter.py @@ -8,16 +8,19 @@ import asyncio import time import unittest from pathlib import Path +from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import numpy as np # Check if aic_sdk is available +aic_sdk: Any try: import aic_sdk HAS_AIC_SDK = True except ImportError: + aic_sdk = None HAS_AIC_SDK = False # Module path for patching @@ -67,6 +70,22 @@ class MockProcessorContext: self.reset_called = True +class UnsupportedEnhancementProcessorContext(MockProcessorContext): + """Processor context mock that rejects EnhancementLevel updates.""" + + def __init__(self, enhancement_parameter, error_type): + super().__init__() + self._enhancement_parameter = enhancement_parameter + self._error_type = error_type + self.enhancement_attempts = 0 + + def set_parameter(self, param, value): + if param == self._enhancement_parameter: + self.enhancement_attempts += 1 + raise self._error_type("EnhancementLevel out of range") + super().set_parameter(param, value) + + class MockVadContext: """A lightweight mock for AIC VadContext.""" @@ -157,6 +176,7 @@ class TestAICFilter(unittest.IsolatedAsyncioTestCase): self.assertEqual(filter_instance._license_key, "test-key") self.assertEqual(filter_instance._model_id, "test-model") self.assertIsNone(filter_instance._model_path) + self.assertIsNone(filter_instance._enhancement_level) self.assertFalse(filter_instance._bypass) async def test_initialization_with_model_path(self): @@ -174,6 +194,29 @@ class TestAICFilter(unittest.IsolatedAsyncioTestCase): self.assertEqual(filter_instance._model_download_dir, download_dir) + async def test_initialization_with_valid_enhancement_level(self): + """Test filter initialization with a valid enhancement_level.""" + filter_instance = self._create_filter_with_mocks(enhancement_level=0.75) + + self.assertEqual(filter_instance._enhancement_level, 0.75) + + async def test_initialization_with_none_enhancement_level(self): + """Test filter initialization with enhancement_level set to None.""" + filter_instance = self._create_filter_with_mocks(enhancement_level=None) + + self.assertIsNone(filter_instance._enhancement_level) + + async def test_initialization_invalid_enhancement_level_raises(self): + """Test initialization rejects enhancement_level outside 0.0..1.0.""" + with patch(f"{AIC_FILTER_MODULE}.set_sdk_id"): + with self.assertRaises(ValueError) as context: + self.AICFilter( + license_key="test-key", + model_id="test-model", + enhancement_level=1.5, + ) + self.assertIn("enhancement_level", str(context.exception)) + async def test_start_with_model_path(self): """Test starting filter with a local model path.""" model_path = Path("/tmp/test.aicmodel") @@ -246,7 +289,6 @@ class TestAICFilter(unittest.IsolatedAsyncioTestCase): 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 @@ -255,6 +297,50 @@ class TestAICFilter(unittest.IsolatedAsyncioTestCase): self.assertTrue(len(bypass_params) > 0) self.assertEqual(bypass_params[-1][1], 0.0) + async def test_start_applies_enhancement_level_when_supported(self): + """Test that start applies enhancement_level when supported by model.""" + filter_instance = self._create_filter_with_mocks(enhancement_level=0.65) + await self._start_filter_with_mocks(filter_instance) + + enhancement_params = [ + (p, v) + for p, v in self.mock_processor.processor_ctx.parameters_set + if p == aic_sdk.ProcessorParameter.EnhancementLevel + ] + self.assertTrue(len(enhancement_params) > 0) + self.assertEqual(enhancement_params[-1][1], 0.65) + + async def test_start_ignores_enhancement_level_when_unsupported(self): + """Test unsupported enhancement_level logs warning and keeps filter ready.""" + filter_instance = self._create_filter_with_mocks(enhancement_level=0.65) + + with patch(f"{AIC_FILTER_MODULE}.ParameterOutOfRangeError", ValueError): + unsupported_ctx = UnsupportedEnhancementProcessorContext( + aic_sdk.ProcessorParameter.EnhancementLevel, ValueError + ) + self.mock_processor.processor_ctx = unsupported_ctx + await self._start_filter_with_mocks(filter_instance) + + self.assertTrue(filter_instance._aic_ready) + self.assertIsNone(filter_instance._enhancement_level) + self.assertEqual(unsupported_ctx.enhancement_attempts, 1) + + async def test_start_does_not_set_enhancement_level_when_none(self): + """Test start does not attempt enhancement_level when not configured.""" + filter_instance = self._create_filter_with_mocks(enhancement_level=None) + with patch(f"{AIC_FILTER_MODULE}.logger.debug") as mock_debug: + await self._start_filter_with_mocks(filter_instance) + + enhancement_params = [ + (p, v) + for p, v in self.mock_processor.processor_ctx.parameters_set + if p == aic_sdk.ProcessorParameter.EnhancementLevel + ] + self.assertEqual(enhancement_params, []) + self.assertTrue( + any("default behavior" in str(call.args[0]) for call in mock_debug.call_args_list) + ) + async def test_stop_cleans_up_resources(self): """Test that stop properly cleans up resources and releases model reference.""" filter_instance = self._create_filter_with_mocks() @@ -420,57 +506,64 @@ class TestAICFilter(unittest.IsolatedAsyncioTestCase): self.assertIsNone(filter_instance._processor) self.assertFalse(filter_instance._aic_ready) - async def test_start_parameter_fixed_error_logged(self): - """Test start() when set_parameter raises ParameterFixedError: logged, no raise.""" - filter_instance = self._create_filter_with_mocks() - self.mock_processor.processor_ctx.set_parameter = MagicMock( - side_effect=aic_sdk.ParameterFixedError("fixed") - ) + async def test_start_skips_unsupported_enhancement_level_after_first_attempt(self): + """Test unsupported enhancement_level is attempted once and then skipped.""" + filter_instance = self._create_filter_with_mocks(enhancement_level=0.9) - with ( - patch(f"{AIC_FILTER_MODULE}.AICModelManager") as mock_manager_cls, - patch(f"{AIC_FILTER_MODULE}.ProcessorConfig") as mock_config_cls, - patch(f"{AIC_FILTER_MODULE}.ProcessorAsync", return_value=self.mock_processor), - ): - mock_manager_cls.acquire = AsyncMock(return_value=(self.mock_model, "test-key")) - mock_config_cls.optimal.return_value = MagicMock() + with patch(f"{AIC_FILTER_MODULE}.ParameterOutOfRangeError", ValueError): + unsupported_ctx = UnsupportedEnhancementProcessorContext( + aic_sdk.ProcessorParameter.EnhancementLevel, ValueError + ) + self.mock_processor.processor_ctx = unsupported_ctx - await filter_instance.start(16000) + await self._start_filter_with_mocks(filter_instance) + await filter_instance.stop() + await self._start_filter_with_mocks(filter_instance) - self.assertTrue(filter_instance._aic_ready) + self.assertEqual(unsupported_ctx.enhancement_attempts, 1) - async def test_process_frame_set_parameter_exception_logged(self): - """Test process_frame when set_parameter raises: exception logged, no raise.""" - filter_instance = self._create_filter_with_mocks() + async def test_process_frame_disable_sets_bypass(self): + """Test disable frame toggles bypass.""" + filter_instance = self._create_filter_with_mocks(enhancement_level=0.7) await self._start_filter_with_mocks(filter_instance) - filter_instance._processor_ctx.set_parameter = MagicMock( - side_effect=ValueError("param error") - ) + await filter_instance.process_frame(self.FilterEnableFrame(enable=False)) - await filter_instance.process_frame(self.FilterEnableFrame(enable=True)) - - self.assertFalse(filter_instance._bypass) - - 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) + enhancement_params = [ + (p, v) + for p, v in self.mock_processor.processor_ctx.parameters_set + if p == aic_sdk.ProcessorParameter.EnhancementLevel + ] + bypass_params = [ + (p, v) + for p, v in self.mock_processor.processor_ctx.parameters_set + if p == aic_sdk.ProcessorParameter.Bypass + ] self.assertTrue(filter_instance._bypass) + self.assertEqual(enhancement_params[-1][1], 0.7) + self.assertEqual(bypass_params[-1][1], 1.0) + + async def test_process_frame_enable_restores_configured_enhancement(self): + """Test enable frame restores configured enhancement level.""" + filter_instance = self._create_filter_with_mocks(enhancement_level=0.7) + await self._start_filter_with_mocks(filter_instance) + + await filter_instance.process_frame(self.FilterEnableFrame(enable=False)) + await filter_instance.process_frame(self.FilterEnableFrame(enable=True)) + + enhancement_params = [ + (p, v) + for p, v in self.mock_processor.processor_ctx.parameters_set + if p == aic_sdk.ProcessorParameter.EnhancementLevel + ] + bypass_params = [ + (p, v) + for p, v in self.mock_processor.processor_ctx.parameters_set + if p == aic_sdk.ProcessorParameter.Bypass + ] + self.assertFalse(filter_instance._bypass) + self.assertEqual(enhancement_params[-1][1], 0.7) + self.assertEqual(bypass_params[-1][1], 0.0) async def test_filter_when_not_ready(self): """Test that filter returns audio unchanged when not ready.""" @@ -678,7 +771,6 @@ class TestAICFilter(unittest.IsolatedAsyncioTestCase): BufferError: Existing exports of data: object cannot be re-sized - This is the exact error path reported in production (line 414). The fix removes the memoryview by snapshotting data into immutable bytes before any await. """ diff --git a/tests/test_deepgram_stt.py b/tests/test_deepgram_stt.py new file mode 100644 index 000000000..eb8036237 --- /dev/null +++ b/tests/test_deepgram_stt.py @@ -0,0 +1,51 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import io + +import pytest +from loguru import logger + +from pipecat.services.deepgram.stt import _derive_deepgram_urls + + +@pytest.mark.parametrize( + "base_url, expected_ws, expected_http", + [ + # Secure schemes + ("wss://mydeepgram.com", "wss://mydeepgram.com", "https://mydeepgram.com"), + ("https://mydeepgram.com", "wss://mydeepgram.com", "https://mydeepgram.com"), + # Insecure schemes (air-gapped deployments) + ("ws://mydeepgram.com", "ws://mydeepgram.com", "http://mydeepgram.com"), + ("http://mydeepgram.com", "ws://mydeepgram.com", "http://mydeepgram.com"), + # Bare hostname defaults to secure + ("mydeepgram.com", "wss://mydeepgram.com", "https://mydeepgram.com"), + # With port + ("ws://localhost:8080", "ws://localhost:8080", "http://localhost:8080"), + ("wss://localhost:443", "wss://localhost:443", "https://localhost:443"), + ("localhost:8080", "wss://localhost:8080", "https://localhost:8080"), + # With path + ("wss://host/v1/listen", "wss://host/v1/listen", "https://host/v1/listen"), + ("http://host/v1/listen", "ws://host/v1/listen", "http://host/v1/listen"), + ], +) +def test_derive_deepgram_urls(base_url, expected_ws, expected_http): + ws_url, http_url = _derive_deepgram_urls(base_url) + assert ws_url == expected_ws + assert http_url == expected_http + + +def test_derive_deepgram_urls_unknown_scheme_warns(): + sink = io.StringIO() + handler_id = logger.add(sink, format="{message}") + try: + ws_url, http_url = _derive_deepgram_urls("ftp://mydeepgram.com") + # Falls back to secure + assert ws_url == "wss://mydeepgram.com" + assert http_url == "https://mydeepgram.com" + assert "Unrecognized scheme" in sink.getvalue() + finally: + logger.remove(handler_id) diff --git a/tests/test_get_llm_invocation_params.py b/tests/test_get_llm_invocation_params.py index c93275b67..9cfeb8933 100644 --- a/tests/test_get_llm_invocation_params.py +++ b/tests/test_get_llm_invocation_params.py @@ -48,6 +48,7 @@ from pipecat.adapters.services.anthropic_adapter import AnthropicLLMAdapter from pipecat.adapters.services.bedrock_adapter import AWSBedrockLLMAdapter from pipecat.adapters.services.gemini_adapter import GeminiLLMAdapter from pipecat.adapters.services.open_ai_adapter import OpenAILLMAdapter +from pipecat.adapters.services.perplexity_adapter import PerplexityLLMAdapter from pipecat.processors.aggregators.llm_context import ( LLMContext, LLMStandardMessage, @@ -992,5 +993,222 @@ class TestAWSBedrockGetLLMInvocationParams(unittest.TestCase): self.assertEqual(len(params["messages"]), 0) +class TestPerplexityGetLLMInvocationParams(unittest.TestCase): + def setUp(self) -> None: + """Sets up a common adapter instance for all tests.""" + self.adapter = PerplexityLLMAdapter() + + def test_standard_messages_pass_through(self): + """Test that a valid [user, assistant, user] sequence passes through unchanged.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi there!"}, + {"role": "user", "content": "How are you?"}, + ] + + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(len(params["messages"]), 3) + self.assertEqual(params["messages"][0]["role"], "user") + self.assertEqual(params["messages"][0]["content"], "Hello") + self.assertEqual(params["messages"][1]["role"], "assistant") + self.assertEqual(params["messages"][1]["content"], "Hi there!") + self.assertEqual(params["messages"][2]["role"], "user") + self.assertEqual(params["messages"][2]["content"], "How are you?") + + def test_initial_system_message_preserved(self): + """Test that a valid [system, user, assistant, user] sequence passes through unchanged.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi!"}, + {"role": "user", "content": "Bye"}, + ] + + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(len(params["messages"]), 4) + self.assertEqual(params["messages"][0]["role"], "system") + self.assertEqual(params["messages"][0]["content"], "You are a helpful assistant.") + self.assertEqual(params["messages"][1]["role"], "user") + self.assertEqual(params["messages"][2]["role"], "assistant") + self.assertEqual(params["messages"][3]["role"], "user") + + def test_consecutive_same_role_messages_merged(self): + """Test that consecutive user messages are merged into list-of-dicts content.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "First message"}, + {"role": "user", "content": "Second message"}, + {"role": "assistant", "content": "Response"}, + {"role": "user", "content": "Third message"}, + ] + + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(len(params["messages"]), 3) + + # First message should be merged users + merged = params["messages"][0] + self.assertEqual(merged["role"], "user") + self.assertIsInstance(merged["content"], list) + self.assertEqual(len(merged["content"]), 2) + self.assertEqual(merged["content"][0]["type"], "text") + self.assertEqual(merged["content"][0]["text"], "First message") + self.assertEqual(merged["content"][1]["type"], "text") + self.assertEqual(merged["content"][1]["text"], "Second message") + + self.assertEqual(params["messages"][1]["role"], "assistant") + self.assertEqual(params["messages"][2]["role"], "user") + + def test_non_initial_system_converted_to_user(self): + """Test that non-initial system messages are converted to user and merged with adjacent user.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi!"}, + {"role": "system", "content": "Be concise."}, + {"role": "user", "content": "Tell me about Python."}, + ] + + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + # system(initial), user, assistant, merged(system→user + user) + self.assertEqual(len(params["messages"]), 4) + self.assertEqual(params["messages"][0]["role"], "system") + self.assertEqual(params["messages"][1]["role"], "user") + self.assertEqual(params["messages"][2]["role"], "assistant") + + # The converted system→user and the following user should be merged + merged = params["messages"][3] + self.assertEqual(merged["role"], "user") + self.assertIsInstance(merged["content"], list) + self.assertEqual(len(merged["content"]), 2) + self.assertEqual(merged["content"][0]["text"], "Be concise.") + self.assertEqual(merged["content"][1]["text"], "Tell me about Python.") + + def test_multiple_system_messages_at_start_preserved(self): + """Test that multiple consecutive system messages at start pass through unchanged.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "system", "content": "Always be polite."}, + {"role": "user", "content": "Hello"}, + ] + + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(len(params["messages"]), 3) + self.assertEqual(params["messages"][0]["role"], "system") + self.assertEqual(params["messages"][0]["content"], "You are a helpful assistant.") + self.assertEqual(params["messages"][1]["role"], "system") + self.assertEqual(params["messages"][1]["content"], "Always be polite.") + self.assertEqual(params["messages"][2]["role"], "user") + self.assertEqual(params["messages"][2]["content"], "Hello") + + def test_trailing_assistant_removed(self): + """Test that a trailing assistant message is removed.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi there!"}, + ] + + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(len(params["messages"]), 1) + self.assertEqual(params["messages"][0]["role"], "user") + self.assertEqual(params["messages"][0]["content"], "Hello") + + def test_only_system_messages_preserved(self): + """Test that system-only contexts are left unchanged (no system→user conversion). + + We intentionally do not convert trailing system messages to "user" + because that would make the transformation unstable across calls — + Perplexity has statefulness within a conversation, so a message that + was "user" in one call but becomes "system" in the next causes errors. + """ + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are a helpful assistant."}, + ] + + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(len(params["messages"]), 1) + self.assertEqual(params["messages"][0]["role"], "system") + + def test_system_exposed_after_trailing_assistant_removed(self): + """Test that a system message exposed by trailing assistant removal stays system. + + It's important that initial system messages are never converted to + "user", because Perplexity has statefulness within a conversation — if + a message was sent as "system" in one call and then becomes "user" in a + later call (after more messages are appended), the API rejects it. + """ + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "assistant", "content": "Sure thing."}, + ] + + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + # Trailing assistant removed → [system], system stays as-is + self.assertEqual(len(params["messages"]), 1) + self.assertEqual(params["messages"][0]["role"], "system") + self.assertEqual(params["messages"][0]["content"], "You are helpful.") + + def test_consecutive_assistants_merged_then_trailing_removed(self): + """Test that consecutive assistant messages are merged, then trailing assistant is removed.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "First response"}, + {"role": "assistant", "content": "Second response"}, + ] + + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + # After merging assistants we get [user, assistant(merged)], then trailing + # assistant is removed, leaving just [user] + self.assertEqual(len(params["messages"]), 1) + self.assertEqual(params["messages"][0]["role"], "user") + self.assertEqual(params["messages"][0]["content"], "Hello") + + def test_tool_messages_preserved(self): + """Test that tool messages pass through without modification.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "What's the weather?"}, + { + "role": "assistant", + "content": "Let me check.", + "tool_calls": [{"id": "1", "function": {"name": "get_weather", "arguments": "{}"}}], + }, + {"role": "tool", "content": "Sunny, 72F", "tool_call_id": "1"}, + {"role": "user", "content": "Thanks!"}, + ] + + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(len(params["messages"]), 4) + self.assertEqual(params["messages"][0]["role"], "user") + self.assertEqual(params["messages"][1]["role"], "assistant") + self.assertEqual(params["messages"][2]["role"], "tool") + self.assertEqual(params["messages"][2]["content"], "Sunny, 72F") + self.assertEqual(params["messages"][3]["role"], "user") + + def test_empty_messages(self): + """Test that empty messages list returns empty.""" + context = LLMContext(messages=[]) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(params["messages"], []) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_google_llm_openai.py b/tests/test_google_llm_openai.py index 2940bf7d7..5e6cee6f8 100644 --- a/tests/test_google_llm_openai.py +++ b/tests/test_google_llm_openai.py @@ -15,7 +15,7 @@ import pytest from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext try: - from pipecat.services.google.llm_openai import GoogleLLMOpenAIBetaService + from pipecat.services.google.openai.llm import GoogleLLMOpenAIBetaService google_available = True except Exception: diff --git a/tests/test_openai_responses_adapter.py b/tests/test_openai_responses_adapter.py new file mode 100644 index 000000000..973c05c8c --- /dev/null +++ b/tests/test_openai_responses_adapter.py @@ -0,0 +1,349 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""Unit tests for the OpenAI Responses API adapter. + +Tests the conversion from LLMContext messages to Responses API input items, including: + +1. Simple user/assistant text messages pass through (with correct role) +2. System role converted to developer role +3. First-message system role triggers a warning +4. Assistant messages with tool_calls produce function_call input items +5. Tool messages produce function_call_output input items +6. Mixed conversations with text + function calls convert correctly +7. Multimodal content conversion (text -> input_text, image_url -> input_image) +8. Tools schema flattening (nested function dict -> flat format) +9. Empty messages list +10. LLMSpecificMessage with llm="openai_responses" passes through +""" + +import unittest +from unittest.mock import patch + +from pipecat.adapters.schemas.function_schema import FunctionSchema +from pipecat.adapters.schemas.tools_schema import ToolsSchema +from pipecat.adapters.services.open_ai_responses_adapter import OpenAIResponsesLLMAdapter +from pipecat.processors.aggregators.llm_context import LLMContext, LLMStandardMessage + + +class TestOpenAIResponsesAdapter(unittest.TestCase): + def setUp(self): + self.adapter = OpenAIResponsesLLMAdapter() + + def test_simple_user_assistant_messages(self): + """Simple user/assistant text messages are converted correctly.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi there!"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(len(params["input"]), 2) + self.assertEqual(params["input"][0], {"role": "user", "content": "Hello"}) + self.assertEqual(params["input"][1], {"role": "assistant", "content": "Hi there!"}) + + def test_system_role_converted_to_developer(self): + """System role messages are converted to developer role.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(params["input"][0]["role"], "developer") + self.assertEqual(params["input"][0]["content"], "You are helpful.") + + def test_first_system_message_triggers_warning(self): + """First system message triggers a warning about using system_instruction.""" + # Use a fresh adapter so the warning hasn't been emitted yet + adapter = OpenAIResponsesLLMAdapter() + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + with patch("pipecat.adapters.services.open_ai_responses_adapter.logger") as mock_logger: + adapter.get_llm_invocation_params(context) + mock_logger.warning.assert_called_once() + warning_msg = mock_logger.warning.call_args[0][0] + self.assertIn("system_instruction", warning_msg) + + def test_non_initial_system_message_no_warning(self): + """Non-initial system messages are converted without a warning.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "Hello"}, + {"role": "system", "content": "New instruction"}, + ] + context = LLMContext(messages=messages) + + adapter = OpenAIResponsesLLMAdapter() + with patch("pipecat.adapters.services.open_ai_responses_adapter.logger") as mock_logger: + params = adapter.get_llm_invocation_params(context) + mock_logger.warning.assert_not_called() + + self.assertEqual(params["input"][1]["role"], "developer") + self.assertEqual(params["input"][1]["content"], "New instruction") + + def test_first_system_message_warning_fires_only_once(self): + """The first-system-message warning fires only once per adapter instance.""" + messages: list[LLMStandardMessage] = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + + adapter = OpenAIResponsesLLMAdapter() + with patch("pipecat.adapters.services.open_ai_responses_adapter.logger") as mock_logger: + adapter.get_llm_invocation_params(context) + adapter.get_llm_invocation_params(context) + # Warning should have been emitted exactly once, not twice + mock_logger.warning.assert_called_once() + + def test_assistant_tool_calls_to_function_call(self): + """Assistant messages with tool_calls produce function_call input items.""" + messages = [ + { + "role": "assistant", + "tool_calls": [ + { + "id": "call_123", + "function": { + "name": "get_weather", + "arguments": '{"location": "SF"}', + }, + "type": "function", + } + ], + } + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(len(params["input"]), 1) + fc = params["input"][0] + self.assertEqual(fc["type"], "function_call") + self.assertEqual(fc["call_id"], "call_123") + self.assertEqual(fc["name"], "get_weather") + self.assertEqual(fc["arguments"], '{"location": "SF"}') + + def test_multiple_tool_calls(self): + """Multiple tool calls in one assistant message produce multiple function_call items.""" + messages = [ + { + "role": "assistant", + "tool_calls": [ + { + "id": "call_1", + "function": {"name": "get_weather", "arguments": '{"location": "SF"}'}, + "type": "function", + }, + { + "id": "call_2", + "function": {"name": "get_restaurant", "arguments": '{"location": "SF"}'}, + "type": "function", + }, + ], + } + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(len(params["input"]), 2) + self.assertEqual(params["input"][0]["name"], "get_weather") + self.assertEqual(params["input"][1]["name"], "get_restaurant") + + def test_tool_message_to_function_call_output(self): + """Tool role messages produce function_call_output input items.""" + messages = [ + { + "role": "tool", + "content": '{"temperature": "72"}', + "tool_call_id": "call_123", + } + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(len(params["input"]), 1) + fco = params["input"][0] + self.assertEqual(fco["type"], "function_call_output") + self.assertEqual(fco["call_id"], "call_123") + self.assertEqual(fco["output"], '{"temperature": "72"}') + + def test_mixed_conversation(self): + """Mixed conversation with text + function calls converts correctly.""" + messages = [ + {"role": "user", "content": "What's the weather in SF?"}, + { + "role": "assistant", + "tool_calls": [ + { + "id": "call_abc", + "function": {"name": "get_weather", "arguments": '{"location": "SF"}'}, + "type": "function", + } + ], + }, + { + "role": "tool", + "content": '{"temp": "72"}', + "tool_call_id": "call_abc", + }, + {"role": "assistant", "content": "It's 72 degrees in SF."}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(len(params["input"]), 4) + self.assertEqual(params["input"][0]["role"], "user") + self.assertEqual(params["input"][1]["type"], "function_call") + self.assertEqual(params["input"][2]["type"], "function_call_output") + self.assertEqual(params["input"][3]["role"], "assistant") + + def test_multimodal_text_conversion(self): + """Multimodal text content parts are converted to input_text.""" + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "What's in this image?"}, + ], + } + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + content = params["input"][0]["content"] + self.assertEqual(len(content), 1) + self.assertEqual(content[0]["type"], "input_text") + self.assertEqual(content[0]["text"], "What's in this image?") + + def test_multimodal_image_conversion(self): + """Multimodal image_url content parts are converted to input_image.""" + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "Describe this:"}, + { + "type": "image_url", + "image_url": {"url": "data:image/jpeg;base64,abc123"}, + }, + ], + } + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + content = params["input"][0]["content"] + self.assertEqual(len(content), 2) + self.assertEqual(content[0]["type"], "input_text") + self.assertEqual(content[1]["type"], "input_image") + self.assertEqual(content[1]["image_url"], "data:image/jpeg;base64,abc123") + self.assertEqual(content[1]["detail"], "auto") + + def test_multimodal_image_with_detail(self): + """Image content parts preserve the detail setting when provided.""" + messages = [ + { + "role": "user", + "content": [ + { + "type": "image_url", + "image_url": {"url": "https://example.com/img.png", "detail": "high"}, + }, + ], + } + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + content = params["input"][0]["content"] + self.assertEqual(content[0]["detail"], "high") + + def test_tools_schema_flattening(self): + """Tools schema with nested function dict is flattened to Responses API format.""" + weather_fn = FunctionSchema( + name="get_weather", + description="Get the current weather", + properties={ + "location": {"type": "string", "description": "The city"}, + }, + required=["location"], + ) + tools = ToolsSchema(standard_tools=[weather_fn]) + context = LLMContext(tools=tools) + params = self.adapter.get_llm_invocation_params(context) + + tool_list = params["tools"] + self.assertEqual(len(tool_list), 1) + tool = tool_list[0] + self.assertEqual(tool["type"], "function") + self.assertEqual(tool["name"], "get_weather") + self.assertEqual(tool["description"], "Get the current weather") + self.assertIn("properties", tool["parameters"]) + + def test_empty_messages(self): + """Empty messages list produces empty input list.""" + context = LLMContext(messages=[]) + params = self.adapter.get_llm_invocation_params(context) + self.assertEqual(params["input"], []) + + def test_llm_specific_message_passthrough(self): + """LLMSpecificMessage with llm='openai_responses' passes through.""" + specific_msg = self.adapter.create_llm_specific_message( + {"type": "function_call", "call_id": "x", "name": "foo", "arguments": "{}"} + ) + messages = [ + {"role": "user", "content": "Hello"}, + specific_msg, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context) + + self.assertEqual(len(params["input"]), 2) + self.assertEqual(params["input"][0]["role"], "user") + self.assertEqual(params["input"][1]["type"], "function_call") + + def test_id_for_llm_specific_messages(self): + """Adapter identifier is 'openai_responses'.""" + self.assertEqual(self.adapter.id_for_llm_specific_messages, "openai_responses") + + def test_system_instruction_with_messages_sets_instructions(self): + """When system_instruction is provided and input is non-empty, sets instructions.""" + messages: list[LLMStandardMessage] = [ + {"role": "user", "content": "Hello"}, + ] + context = LLMContext(messages=messages) + params = self.adapter.get_llm_invocation_params(context, system_instruction="Be helpful.") + + self.assertEqual(params["instructions"], "Be helpful.") + self.assertEqual(len(params["input"]), 1) + self.assertEqual(params["input"][0]["role"], "user") + + def test_system_instruction_with_empty_input_becomes_developer_message(self): + """When system_instruction is provided but input is empty, it becomes a developer message.""" + context = LLMContext(messages=[]) + params = self.adapter.get_llm_invocation_params(context, system_instruction="Be helpful.") + + self.assertNotIn("instructions", params) + self.assertEqual(len(params["input"]), 1) + self.assertEqual(params["input"][0]["role"], "developer") + self.assertEqual(params["input"][0]["content"], "Be helpful.") + + def test_no_system_instruction_omits_instructions(self): + """When no system_instruction is provided, instructions key is absent.""" + context = LLMContext(messages=[{"role": "user", "content": "Hi"}]) + params = self.adapter.get_llm_invocation_params(context) + + self.assertNotIn("instructions", params) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_run_inference.py b/tests/test_run_inference.py index eb18a5fda..cef13fb27 100644 --- a/tests/test_run_inference.py +++ b/tests/test_run_inference.py @@ -20,6 +20,7 @@ from pipecat.services.anthropic.llm import AnthropicLLMService from pipecat.services.aws.llm import AWSBedrockLLMService from pipecat.services.google.llm import GoogleLLMService from pipecat.services.openai.llm import OpenAILLMService +from pipecat.services.openai.responses.llm import OpenAIResponsesLLMService @pytest.mark.asyncio @@ -511,5 +512,426 @@ async def test_aws_bedrock_run_inference_client_exception(): await service.run_inference(mock_context) -if __name__ == "__main__": - unittest.main() +# --- system_instruction parameter tests --- + + +@pytest.mark.asyncio +async def test_openai_run_inference_system_instruction_overrides_context(): + """Test that system_instruction overrides the system message from context.""" + with patch.object(OpenAILLMService, "create_client"): + service = OpenAILLMService(model="gpt-4") + service._client = AsyncMock() + + mock_context = MagicMock(spec=LLMContext) + mock_adapter = MagicMock() + test_messages = [ + {"role": "system", "content": "Original system message"}, + {"role": "user", "content": "Hello"}, + ] + mock_adapter.get_llm_invocation_params.return_value = OpenAILLMInvocationParams( + messages=test_messages, tools=OPENAI_NOT_GIVEN, tool_choice=OPENAI_NOT_GIVEN + ) + service.get_llm_adapter = MagicMock(return_value=mock_adapter) + + mock_response = MagicMock() + mock_response.choices = [MagicMock()] + mock_response.choices[0].message.content = "Response" + service._client.chat.completions.create.return_value = mock_response + + result = await service.run_inference( + mock_context, system_instruction="New system instruction" + ) + + assert result == "Response" + call_kwargs = service._client.chat.completions.create.call_args.kwargs + messages = call_kwargs["messages"] + # system_instruction should be prepended as the first message + assert messages[0] == {"role": "system", "content": "New system instruction"} + # Original system message should still be present + assert messages[1] == {"role": "system", "content": "Original system message"} + # User message should still be present + assert messages[2] == {"role": "user", "content": "Hello"} + assert len(messages) == 3 + + +@pytest.mark.asyncio +async def test_openai_run_inference_system_instruction_none_unchanged(): + """Test that when system_instruction is None, behavior is unchanged.""" + with patch.object(OpenAILLMService, "create_client"): + service = OpenAILLMService(model="gpt-4") + service._client = AsyncMock() + + mock_context = MagicMock(spec=LLMContext) + mock_adapter = MagicMock() + test_messages = [ + {"role": "system", "content": "Original system message"}, + {"role": "user", "content": "Hello"}, + ] + mock_adapter.get_llm_invocation_params.return_value = OpenAILLMInvocationParams( + messages=test_messages, tools=OPENAI_NOT_GIVEN, tool_choice=OPENAI_NOT_GIVEN + ) + service.get_llm_adapter = MagicMock(return_value=mock_adapter) + + mock_response = MagicMock() + mock_response.choices = [MagicMock()] + mock_response.choices[0].message.content = "Response" + service._client.chat.completions.create.return_value = mock_response + + result = await service.run_inference(mock_context) + + assert result == "Response" + call_kwargs = service._client.chat.completions.create.call_args.kwargs + messages = call_kwargs["messages"] + assert messages[0] == {"role": "system", "content": "Original system message"} + assert messages[1] == {"role": "user", "content": "Hello"} + + +@pytest.mark.asyncio +async def test_anthropic_run_inference_system_instruction_overrides_context(): + """Test that system_instruction overrides the system message for Anthropic.""" + service = AnthropicLLMService(api_key="test-key", model="claude-3-sonnet-20240229") + service._client = AsyncMock() + + mock_context = MagicMock(spec=LLMContext) + mock_adapter = MagicMock() + test_messages = [{"role": "user", "content": "Hello"}] + mock_adapter.get_llm_invocation_params.return_value = AnthropicLLMInvocationParams( + messages=test_messages, system="Original system", tools=[] + ) + service.get_llm_adapter = MagicMock(return_value=mock_adapter) + + mock_response = MagicMock() + mock_response.content = [MagicMock()] + mock_response.content[0].text = "Response" + service._client.beta.messages.create.return_value = mock_response + + result = await service.run_inference(mock_context, system_instruction="New system instruction") + + assert result == "Response" + call_kwargs = service._client.beta.messages.create.call_args.kwargs + assert call_kwargs["system"] == "New system instruction" + assert call_kwargs["messages"] == test_messages + + +@pytest.mark.asyncio +async def test_anthropic_run_inference_system_instruction_none_unchanged(): + """Test that when system_instruction is None, Anthropic behavior is unchanged.""" + service = AnthropicLLMService(api_key="test-key", model="claude-3-sonnet-20240229") + service._client = AsyncMock() + + mock_context = MagicMock(spec=LLMContext) + mock_adapter = MagicMock() + test_messages = [{"role": "user", "content": "Hello"}] + mock_adapter.get_llm_invocation_params.return_value = AnthropicLLMInvocationParams( + messages=test_messages, system="Original system", tools=[] + ) + service.get_llm_adapter = MagicMock(return_value=mock_adapter) + + mock_response = MagicMock() + mock_response.content = [MagicMock()] + mock_response.content[0].text = "Response" + service._client.beta.messages.create.return_value = mock_response + + result = await service.run_inference(mock_context) + + assert result == "Response" + call_kwargs = service._client.beta.messages.create.call_args.kwargs + assert call_kwargs["system"] == "Original system" + + +@pytest.mark.asyncio +async def test_google_run_inference_system_instruction_overrides_context(): + """Test that system_instruction overrides the system message for Google.""" + service = GoogleLLMService(api_key="test-key", model="gemini-2.0-flash") + service._client = AsyncMock() + + mock_context = MagicMock(spec=LLMContext) + mock_adapter = MagicMock() + test_messages = [{"role": "user", "content": "Hello"}] + mock_adapter.get_llm_invocation_params.return_value = GeminiLLMInvocationParams( + messages=test_messages, system_instruction="Original system", tools=NotGiven() + ) + service.get_llm_adapter = MagicMock(return_value=mock_adapter) + + mock_response = MagicMock() + mock_response.candidates = [MagicMock()] + mock_response.candidates[0].content = MagicMock() + mock_response.candidates[0].content.parts = [MagicMock()] + mock_response.candidates[0].content.parts[0].text = "Response" + service._client.aio = AsyncMock() + service._client.aio.models = AsyncMock() + service._client.aio.models.generate_content = AsyncMock(return_value=mock_response) + + result = await service.run_inference(mock_context, system_instruction="New system instruction") + + assert result == "Response" + call_kwargs = service._client.aio.models.generate_content.call_args.kwargs + config = call_kwargs["config"] + assert config.system_instruction == "New system instruction" + + +@pytest.mark.asyncio +async def test_google_run_inference_system_instruction_none_unchanged(): + """Test that when system_instruction is None, Google behavior is unchanged.""" + service = GoogleLLMService(api_key="test-key", model="gemini-2.0-flash") + service._client = AsyncMock() + + mock_context = MagicMock(spec=LLMContext) + mock_adapter = MagicMock() + test_messages = [{"role": "user", "content": "Hello"}] + mock_adapter.get_llm_invocation_params.return_value = GeminiLLMInvocationParams( + messages=test_messages, system_instruction="Original system", tools=NotGiven() + ) + service.get_llm_adapter = MagicMock(return_value=mock_adapter) + + mock_response = MagicMock() + mock_response.candidates = [MagicMock()] + mock_response.candidates[0].content = MagicMock() + mock_response.candidates[0].content.parts = [MagicMock()] + mock_response.candidates[0].content.parts[0].text = "Response" + service._client.aio = AsyncMock() + service._client.aio.models = AsyncMock() + service._client.aio.models.generate_content = AsyncMock(return_value=mock_response) + + result = await service.run_inference(mock_context) + + assert result == "Response" + call_kwargs = service._client.aio.models.generate_content.call_args.kwargs + config = call_kwargs["config"] + assert config.system_instruction == "Original system" + + +@pytest.mark.asyncio +async def test_aws_bedrock_run_inference_system_instruction_overrides_context(): + """Test that system_instruction overrides the system message for AWS Bedrock.""" + service = AWSBedrockLLMService(model="anthropic.claude-3-sonnet-20240229-v1:0") + + mock_context = MagicMock(spec=LLMContext) + mock_adapter = MagicMock() + test_messages = [{"role": "user", "content": [{"text": "Hello"}]}] + mock_adapter.get_llm_invocation_params.return_value = AWSBedrockLLMInvocationParams( + messages=test_messages, + system=[{"text": "Original system"}], + tools=[], + tool_choice=None, + ) + service.get_llm_adapter = MagicMock(return_value=mock_adapter) + + mock_client = AsyncMock() + mock_response = {"output": {"message": {"content": [{"text": "Response"}]}}} + mock_client.converse.return_value = mock_response + + mock_context_manager = AsyncMock() + mock_context_manager.__aenter__ = AsyncMock(return_value=mock_client) + mock_context_manager.__aexit__ = AsyncMock(return_value=None) + + with patch.object(service._aws_session, "client", return_value=mock_context_manager): + result = await service.run_inference( + mock_context, system_instruction="New system instruction" + ) + + assert result == "Response" + call_kwargs = mock_client.converse.call_args.kwargs + assert call_kwargs["system"] == [{"text": "New system instruction"}] + assert call_kwargs["messages"] == test_messages + + +@pytest.mark.asyncio +async def test_aws_bedrock_run_inference_system_instruction_none_unchanged(): + """Test that when system_instruction is None, AWS Bedrock behavior is unchanged.""" + service = AWSBedrockLLMService(model="anthropic.claude-3-sonnet-20240229-v1:0") + + mock_context = MagicMock(spec=LLMContext) + mock_adapter = MagicMock() + test_messages = [{"role": "user", "content": [{"text": "Hello"}]}] + mock_adapter.get_llm_invocation_params.return_value = AWSBedrockLLMInvocationParams( + messages=test_messages, + system=[{"text": "Original system"}], + tools=[], + tool_choice=None, + ) + service.get_llm_adapter = MagicMock(return_value=mock_adapter) + + mock_client = AsyncMock() + mock_response = {"output": {"message": {"content": [{"text": "Response"}]}}} + mock_client.converse.return_value = mock_response + + mock_context_manager = AsyncMock() + mock_context_manager.__aenter__ = AsyncMock(return_value=mock_client) + mock_context_manager.__aexit__ = AsyncMock(return_value=None) + + with patch.object(service._aws_session, "client", return_value=mock_context_manager): + result = await service.run_inference(mock_context) + + assert result == "Response" + call_kwargs = mock_client.converse.call_args.kwargs + assert call_kwargs["system"] == [{"text": "Original system"}] + + +# --- OpenAI Responses API tests --- + + +@pytest.mark.asyncio +async def test_openai_responses_run_inference_with_llm_context(): + """Test run_inference with LLMContext returns expected response.""" + with patch.object(OpenAIResponsesLLMService, "_create_client"): + service = OpenAIResponsesLLMService( + settings=OpenAIResponsesLLMService.Settings( + model="gpt-4.1", + system_instruction="You are a helpful assistant", + temperature=0.7, + max_completion_tokens=100, + ), + ) + service._client = AsyncMock() + + context = LLMContext( + messages=[ + {"role": "user", "content": "Hello, world!"}, + ] + ) + + mock_response = MagicMock() + mock_response.output_text = "Hello! How can I help you today?" + service._client.responses.create = AsyncMock(return_value=mock_response) + + result = await service.run_inference(context) + + assert result == "Hello! How can I help you today?" + call_kwargs = service._client.responses.create.call_args.kwargs + assert call_kwargs["model"] == "gpt-4.1" + assert call_kwargs["stream"] is False + assert call_kwargs["store"] is False + assert call_kwargs["input"] == [{"role": "user", "content": "Hello, world!"}] + assert call_kwargs["instructions"] == "You are a helpful assistant" + assert call_kwargs["temperature"] == 0.7 + assert call_kwargs["max_output_tokens"] == 100 + + +@pytest.mark.asyncio +async def test_openai_responses_run_inference_client_exception(): + """Test that exceptions from the client are propagated.""" + with patch.object(OpenAIResponsesLLMService, "_create_client"): + service = OpenAIResponsesLLMService() + service._client = AsyncMock() + + context = LLMContext(messages=[{"role": "user", "content": "Hello"}]) + service._client.responses.create = AsyncMock(side_effect=Exception("API Error")) + + with pytest.raises(Exception, match="API Error"): + await service.run_inference(context) + + +@pytest.mark.asyncio +async def test_openai_responses_run_inference_system_instruction_overrides(): + """Test that system_instruction parameter overrides the settings instruction.""" + with patch.object(OpenAIResponsesLLMService, "_create_client"): + service = OpenAIResponsesLLMService( + settings=OpenAIResponsesLLMService.Settings( + model="gpt-4.1", + system_instruction="Original instruction", + ), + ) + service._client = AsyncMock() + + context = LLMContext( + messages=[{"role": "user", "content": "Hello"}], + ) + + mock_response = MagicMock() + mock_response.output_text = "Response" + service._client.responses.create = AsyncMock(return_value=mock_response) + + result = await service.run_inference(context, system_instruction="New system instruction") + + assert result == "Response" + call_kwargs = service._client.responses.create.call_args.kwargs + assert call_kwargs["instructions"] == "New system instruction" + assert call_kwargs["input"] == [{"role": "user", "content": "Hello"}] + + +@pytest.mark.asyncio +async def test_openai_responses_run_inference_empty_context_with_instruction(): + """Test that system_instruction becomes a developer message when context is empty.""" + with patch.object(OpenAIResponsesLLMService, "_create_client"): + service = OpenAIResponsesLLMService( + settings=OpenAIResponsesLLMService.Settings( + model="gpt-4.1", + system_instruction="You are helpful", + ), + ) + service._client = AsyncMock() + + context = LLMContext(messages=[]) + + mock_response = MagicMock() + mock_response.output_text = "Response" + service._client.responses.create = AsyncMock(return_value=mock_response) + + result = await service.run_inference(context) + + assert result == "Response" + call_kwargs = service._client.responses.create.call_args.kwargs + # With empty context, instruction should become a developer message + assert call_kwargs["input"] == [{"role": "developer", "content": "You are helpful"}] + assert "instructions" not in call_kwargs + + +@pytest.mark.asyncio +async def test_openai_responses_run_inference_max_tokens_override(): + """Test that max_tokens parameter overrides max_output_tokens.""" + with patch.object(OpenAIResponsesLLMService, "_create_client"): + service = OpenAIResponsesLLMService( + settings=OpenAIResponsesLLMService.Settings( + model="gpt-4.1", + max_completion_tokens=500, + ), + ) + service._client = AsyncMock() + + context = LLMContext( + messages=[{"role": "user", "content": "Summarize this"}], + ) + + mock_response = MagicMock() + mock_response.output_text = "Summary" + service._client.responses.create = AsyncMock(return_value=mock_response) + + result = await service.run_inference(context, max_tokens=200) + + assert result == "Summary" + call_kwargs = service._client.responses.create.call_args.kwargs + assert call_kwargs["max_output_tokens"] == 200 + + +@pytest.mark.asyncio +async def test_openai_responses_run_inference_system_instruction_param_with_empty_context(): + """Test that system_instruction param becomes a developer message when context is empty. + + The Responses API rejects requests with instructions but no input items. + When run_inference is called with an explicit system_instruction and an + empty context, the instruction must become a developer message — not be + sent as the instructions parameter. + """ + with patch.object(OpenAIResponsesLLMService, "_create_client"): + service = OpenAIResponsesLLMService( + settings=OpenAIResponsesLLMService.Settings(model="gpt-4.1"), + ) + service._client = AsyncMock() + + context = LLMContext(messages=[]) + + mock_response = MagicMock() + mock_response.output_text = "Response" + service._client.responses.create = AsyncMock(return_value=mock_response) + + result = await service.run_inference( + context, system_instruction="Summarize the conversation" + ) + + assert result == "Response" + call_kwargs = service._client.responses.create.call_args.kwargs + assert call_kwargs["input"] == [ + {"role": "developer", "content": "Summarize the conversation"} + ] + assert "instructions" not in call_kwargs diff --git a/tests/test_service_language.py b/tests/test_service_language.py new file mode 100644 index 000000000..6ac23c823 --- /dev/null +++ b/tests/test_service_language.py @@ -0,0 +1,251 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""Tests for language parameter handling in TTS and STT services. + +Verifies that Language enums, raw strings (e.g. "de-DE"), and unrecognized +strings are all resolved correctly at both init time and runtime update time. +""" + +from typing import AsyncGenerator, Optional +from unittest.mock import patch + +import pytest + +from pipecat.frames.frames import Frame +from pipecat.services.settings import STTSettings, TTSSettings +from pipecat.services.stt_service import STTService +from pipecat.services.tts_service import TTSService +from pipecat.transcriptions.language import Language, resolve_language + +# --------------------------------------------------------------------------- +# Minimal concrete subclasses for testing +# --------------------------------------------------------------------------- + +# A simple language map using only base codes (like ElevenLabs does). +_LANGUAGE_MAP = { + Language.DE: "de", + Language.EN: "en", + Language.FR: "fr", +} + + +class _TestTTSService(TTSService): + """Minimal concrete TTS service for testing language resolution.""" + + class Settings(TTSSettings): + pass + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + async def run_tts(self, text: str, context_id: str) -> AsyncGenerator[Frame, None]: + yield # pragma: no cover + + def language_to_service_language(self, language: Language) -> Optional[str]: + return resolve_language(language, _LANGUAGE_MAP, use_base_code=True) + + +class _TestSTTService(STTService): + """Minimal concrete STT service for testing language resolution.""" + + class Settings(STTSettings): + pass + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + async def run_stt(self, audio: bytes) -> AsyncGenerator[Frame, None]: + yield # pragma: no cover + + async def process_audio_frame(self, frame, direction): + pass # pragma: no cover + + def language_to_service_language(self, language: Language) -> Optional[str]: + return resolve_language(language, _LANGUAGE_MAP, use_base_code=True) + + +# --------------------------------------------------------------------------- +# TTS init tests +# --------------------------------------------------------------------------- + + +class TestTTSLanguageInit: + """Test language resolution at TTS service init time.""" + + def test_language_enum_base_code(self): + """Language.DE (base code in map) resolves to 'de'.""" + svc = _TestTTSService(settings=_TestTTSService.Settings(language=Language.DE)) + assert svc._settings.language == "de" + + def test_language_enum_regional_code(self): + """Language.DE_DE (regional, not in map) falls back to base code 'de'.""" + svc = _TestTTSService(settings=_TestTTSService.Settings(language=Language.DE_DE)) + assert svc._settings.language == "de" + + def test_raw_string_base_code(self): + """Raw string 'de' is converted to Language.DE then resolved to 'de'.""" + svc = _TestTTSService(settings=_TestTTSService.Settings(language="de")) + assert svc._settings.language == "de" + + def test_raw_string_regional_code(self): + """Raw string 'de-DE' is converted to Language.DE_DE then resolved to 'de'.""" + svc = _TestTTSService(settings=_TestTTSService.Settings(language="de-DE")) + assert svc._settings.language == "de" + + def test_raw_string_other_regional(self): + """Raw string 'en-US' is converted to Language.EN_US then resolved to 'en'.""" + svc = _TestTTSService(settings=_TestTTSService.Settings(language="en-US")) + assert svc._settings.language == "en" + + def test_raw_string_unrecognized(self): + """Unrecognized raw string logs a warning and is passed through as-is.""" + with patch("pipecat.services.tts_service.logger") as mock_logger: + svc = _TestTTSService(settings=_TestTTSService.Settings(language="klingon")) + assert svc._settings.language == "klingon" + mock_logger.warning.assert_called_once() + assert "klingon" in mock_logger.warning.call_args[0][0] + + def test_language_none(self): + """None language is left as None.""" + svc = _TestTTSService(settings=_TestTTSService.Settings(language=None)) + assert svc._settings.language is None + + +# --------------------------------------------------------------------------- +# STT init tests +# --------------------------------------------------------------------------- + + +class TestSTTLanguageInit: + """Test language resolution at STT service init time.""" + + def test_language_enum_base_code(self): + """Language.FR (base code in map) resolves to 'fr'.""" + svc = _TestSTTService(settings=_TestSTTService.Settings(language=Language.FR)) + assert svc._settings.language == "fr" + + def test_language_enum_regional_code(self): + """Language.FR_FR (regional, not in map) falls back to base code 'fr'.""" + svc = _TestSTTService(settings=_TestSTTService.Settings(language=Language.FR_FR)) + assert svc._settings.language == "fr" + + def test_raw_string_base_code(self): + """Raw string 'fr' is converted to Language.FR then resolved to 'fr'.""" + svc = _TestSTTService(settings=_TestSTTService.Settings(language="fr")) + assert svc._settings.language == "fr" + + def test_raw_string_regional_code(self): + """Raw string 'de-DE' is converted to Language.DE_DE then resolved to 'de'.""" + svc = _TestSTTService(settings=_TestSTTService.Settings(language="de-DE")) + assert svc._settings.language == "de" + + def test_raw_string_unrecognized(self): + """Unrecognized raw string logs a warning and is passed through as-is.""" + with patch("pipecat.services.stt_service.logger") as mock_logger: + svc = _TestSTTService(settings=_TestSTTService.Settings(language="klingon")) + assert svc._settings.language == "klingon" + mock_logger.warning.assert_called_once() + assert "klingon" in mock_logger.warning.call_args[0][0] + + def test_language_none(self): + """None language is left as None.""" + svc = _TestSTTService(settings=_TestSTTService.Settings(language=None)) + assert svc._settings.language is None + + +# --------------------------------------------------------------------------- +# TTS runtime update tests +# --------------------------------------------------------------------------- + + +class TestTTSLanguageUpdate: + """Test language resolution during runtime settings updates.""" + + @pytest.mark.asyncio + async def test_update_language_enum_base_code(self): + """Updating with Language.EN resolves to 'en'.""" + svc = _TestTTSService(settings=_TestTTSService.Settings(language=None)) + await svc._update_settings(_TestTTSService.Settings(language=Language.EN)) + assert svc._settings.language == "en" + + @pytest.mark.asyncio + async def test_update_language_enum_regional_code(self): + """Updating with Language.DE_DE falls back to base code 'de'.""" + svc = _TestTTSService(settings=_TestTTSService.Settings(language=None)) + await svc._update_settings(_TestTTSService.Settings(language=Language.DE_DE)) + assert svc._settings.language == "de" + + @pytest.mark.asyncio + async def test_update_raw_string_base_code(self): + """Updating with raw string 'de' resolves to 'de'.""" + svc = _TestTTSService(settings=_TestTTSService.Settings(language=None)) + await svc._update_settings(_TestTTSService.Settings(language="de")) + assert svc._settings.language == "de" + + @pytest.mark.asyncio + async def test_update_raw_string_regional_code(self): + """Updating with raw string 'de-DE' resolves to 'de'.""" + svc = _TestTTSService(settings=_TestTTSService.Settings(language=None)) + await svc._update_settings(_TestTTSService.Settings(language="de-DE")) + assert svc._settings.language == "de" + + @pytest.mark.asyncio + async def test_update_raw_string_unrecognized(self): + """Updating with unrecognized string logs warning and passes through.""" + svc = _TestTTSService(settings=_TestTTSService.Settings(language=None)) + with patch("pipecat.services.tts_service.logger") as mock_logger: + await svc._update_settings(_TestTTSService.Settings(language="klingon")) + assert svc._settings.language == "klingon" + mock_logger.warning.assert_called_once() + assert "klingon" in mock_logger.warning.call_args[0][0] + + +# --------------------------------------------------------------------------- +# STT runtime update tests +# --------------------------------------------------------------------------- + + +class TestSTTLanguageUpdate: + """Test language resolution during runtime settings updates.""" + + @pytest.mark.asyncio + async def test_update_language_enum_base_code(self): + """Updating with Language.EN resolves to 'en'.""" + svc = _TestSTTService(settings=_TestSTTService.Settings(language=None)) + await svc._update_settings(_TestSTTService.Settings(language=Language.EN)) + assert svc._settings.language == "en" + + @pytest.mark.asyncio + async def test_update_language_enum_regional_code(self): + """Updating with Language.FR_FR falls back to base code 'fr'.""" + svc = _TestSTTService(settings=_TestSTTService.Settings(language=None)) + await svc._update_settings(_TestSTTService.Settings(language=Language.FR_FR)) + assert svc._settings.language == "fr" + + @pytest.mark.asyncio + async def test_update_raw_string_base_code(self): + """Updating with raw string 'fr' resolves to 'fr'.""" + svc = _TestSTTService(settings=_TestSTTService.Settings(language=None)) + await svc._update_settings(_TestSTTService.Settings(language="fr")) + assert svc._settings.language == "fr" + + @pytest.mark.asyncio + async def test_update_raw_string_regional_code(self): + """Updating with raw string 'fr-FR' resolves to 'fr'.""" + svc = _TestSTTService(settings=_TestSTTService.Settings(language=None)) + await svc._update_settings(_TestSTTService.Settings(language="fr-FR")) + assert svc._settings.language == "fr" + + @pytest.mark.asyncio + async def test_update_raw_string_unrecognized(self): + """Updating with unrecognized string logs warning and passes through.""" + svc = _TestSTTService(settings=_TestSTTService.Settings(language=None)) + with patch("pipecat.services.stt_service.logger") as mock_logger: + await svc._update_settings(_TestSTTService.Settings(language="klingon")) + assert svc._settings.language == "klingon" + mock_logger.warning.assert_called_once() + assert "klingon" in mock_logger.warning.call_args[0][0] diff --git a/tests/test_service_switcher.py b/tests/test_service_switcher.py index 4df6696af..4cdf38f1a 100644 --- a/tests/test_service_switcher.py +++ b/tests/test_service_switcher.py @@ -11,6 +11,7 @@ import unittest from dataclasses import dataclass from pipecat.frames.frames import ( + ErrorFrame, Frame, ManuallySwitchServiceFrame, ServiceMetadataFrame, @@ -20,7 +21,12 @@ from pipecat.frames.frames import ( TextFrame, ) from pipecat.pipeline.pipeline import Pipeline -from pipecat.pipeline.service_switcher import ServiceSwitcher, ServiceSwitcherStrategyManual +from pipecat.pipeline.service_switcher import ( + ServiceSwitcher, + ServiceSwitcherStrategy, + ServiceSwitcherStrategyFailover, + ServiceSwitcherStrategyManual, +) from pipecat.processors.frame_processor import FrameDirection, FrameProcessor from pipecat.tests.utils import run_test @@ -106,8 +112,8 @@ class DummySystemFrame(SystemFrame): text: str = "" -class TestServiceSwitcherStrategyManual(unittest.IsolatedAsyncioTestCase): - """Test cases for ServiceSwitcherStrategyManual.""" +class TestServiceSwitcherStrategy(unittest.IsolatedAsyncioTestCase): + """Test cases for the base ServiceSwitcherStrategy.""" def setUp(self): """Set up test fixtures.""" @@ -118,10 +124,54 @@ class TestServiceSwitcherStrategyManual(unittest.IsolatedAsyncioTestCase): def test_init_with_services(self): """Test initialization with a list of services.""" - strategy = ServiceSwitcherStrategyManual(self.services) + strategy = ServiceSwitcherStrategy(self.services) self.assertEqual(strategy.services, self.services) - self.assertEqual(strategy.active_service, self.service1) # First service should be active + self.assertEqual(strategy.active_service, self.service1) + + async def test_handle_frame_returns_none_for_manual_switch(self): + """Test that base strategy does not handle ManuallySwitchServiceFrame.""" + strategy = ServiceSwitcherStrategy(self.services) + + switch_frame = ManuallySwitchServiceFrame(service=self.service2) + result = await strategy.handle_frame(switch_frame, FrameDirection.DOWNSTREAM) + + self.assertIsNone(result) + self.assertEqual(strategy.active_service, self.service1) + + async def test_handle_frame_returns_none_for_unsupported_frame(self): + """Test that unsupported frame types return None.""" + strategy = ServiceSwitcherStrategy(self.services) + unsupported_frame = TextFrame(text="test") + + result = await strategy.handle_frame(unsupported_frame, FrameDirection.DOWNSTREAM) + + self.assertIsNone(result) + + async def test_handle_error_returns_none(self): + """Test that handle_error returns None by default.""" + strategy = ServiceSwitcherStrategy(self.services) + + result = await strategy.handle_error(ErrorFrame(error="error")) + + self.assertIsNone(result) + self.assertEqual(strategy.active_service, self.service1) + + +class TestServiceSwitcherStrategyManual(unittest.IsolatedAsyncioTestCase): + """Test cases for ServiceSwitcherStrategyManual.""" + + def setUp(self): + """Set up test fixtures.""" + self.service1 = MockFrameProcessor("service1") + self.service2 = MockFrameProcessor("service2") + self.service3 = MockFrameProcessor("service3") + self.services = [self.service1, self.service2, self.service3] + + def test_is_subclass_of_base_strategy(self): + """Test that ServiceSwitcherStrategyManual is a subclass of ServiceSwitcherStrategy.""" + strategy = ServiceSwitcherStrategyManual(self.services) + self.assertIsInstance(strategy, ServiceSwitcherStrategy) async def test_handle_manually_switch_service_frame(self): """Test manual service switching with ManuallySwitchServiceFrame.""" @@ -129,22 +179,15 @@ class TestServiceSwitcherStrategyManual(unittest.IsolatedAsyncioTestCase): # Initially service1 should be active self.assertEqual(strategy.active_service, self.service1) - self.assertNotEqual(strategy.active_service, self.service2) # Switch to service2 switch_frame = ManuallySwitchServiceFrame(service=self.service2) await strategy.handle_frame(switch_frame, FrameDirection.DOWNSTREAM) - - self.assertNotEqual(strategy.active_service, self.service1) self.assertEqual(strategy.active_service, self.service2) - self.assertNotEqual(strategy.active_service, self.service3) # Switch to service3 switch_frame = ManuallySwitchServiceFrame(service=self.service3) await strategy.handle_frame(switch_frame, FrameDirection.DOWNSTREAM) - - self.assertNotEqual(strategy.active_service, self.service1) - self.assertNotEqual(strategy.active_service, self.service2) self.assertEqual(strategy.active_service, self.service3) async def test_on_service_switched_event(self): @@ -157,25 +200,16 @@ class TestServiceSwitcherStrategyManual(unittest.IsolatedAsyncioTestCase): async def on_service_switched(strategy, service): switched_events.append((strategy, service)) - # Switch to service2 switch_frame = ManuallySwitchServiceFrame(service=self.service2) await strategy.handle_frame(switch_frame, FrameDirection.DOWNSTREAM) - await asyncio.sleep(0) # Let async event task run + await asyncio.sleep(0) self.assertEqual(len(switched_events), 1) self.assertIsInstance(switched_events[0][0], ServiceSwitcherStrategyManual) self.assertEqual(switched_events[0][1], self.service2) - # Switch to service3 - switch_frame = ManuallySwitchServiceFrame(service=self.service3) - await strategy.handle_frame(switch_frame, FrameDirection.DOWNSTREAM) - await asyncio.sleep(0) - - self.assertEqual(len(switched_events), 2) - self.assertEqual(switched_events[1][1], self.service3) - - async def test_on_service_switched_event_not_fired_for_unknown_service(self): - """Test that on_service_switched event does not fire for services not in the list.""" + async def test_unknown_service_ignored(self): + """Test that switching to an unknown service is ignored.""" strategy = ServiceSwitcherStrategyManual(self.services) switched_events = [] @@ -184,23 +218,14 @@ class TestServiceSwitcherStrategyManual(unittest.IsolatedAsyncioTestCase): async def on_service_switched(strategy, service): switched_events.append(service) - # Try switching to a service not in the list unknown_service = MockFrameProcessor("unknown") switch_frame = ManuallySwitchServiceFrame(service=unknown_service) - await strategy.handle_frame(switch_frame, FrameDirection.DOWNSTREAM) + result = await strategy.handle_frame(switch_frame, FrameDirection.DOWNSTREAM) await asyncio.sleep(0) - self.assertEqual(len(switched_events), 0) - self.assertEqual(strategy.active_service, self.service1) # Unchanged - - async def test_handle_frame_unsupported_frame_type(self): - """Test that unsupported frame types raise an error.""" - strategy = ServiceSwitcherStrategyManual(self.services) - unsupported_frame = TextFrame(text="test") # Not a ServiceSwitcherFrame - - result = await strategy.handle_frame(unsupported_frame, FrameDirection.DOWNSTREAM) - self.assertIsNone(result) + self.assertEqual(len(switched_events), 0) + self.assertEqual(strategy.active_service, self.service1) class TestServiceSwitcher(unittest.IsolatedAsyncioTestCase): @@ -213,9 +238,9 @@ class TestServiceSwitcher(unittest.IsolatedAsyncioTestCase): self.service3 = MockFrameProcessor("service3") self.services = [self.service1, self.service2, self.service3] - def test_init_with_manual_strategy(self): - """Test initialization with manual strategy.""" - switcher = ServiceSwitcher(self.services, ServiceSwitcherStrategyManual) + def test_init_with_default_strategy(self): + """Test initialization with default strategy.""" + switcher = ServiceSwitcher(self.services) self.assertEqual(switcher.services, self.services) self.assertIsInstance(switcher.strategy, ServiceSwitcherStrategyManual) @@ -223,7 +248,7 @@ class TestServiceSwitcher(unittest.IsolatedAsyncioTestCase): async def test_default_active_service(self): """Test that the initially-active service receives frames while others don't.""" - switcher = ServiceSwitcher(self.services, ServiceSwitcherStrategyManual) + switcher = ServiceSwitcher(self.services) # Reset counters for service in self.services: @@ -292,7 +317,7 @@ class TestServiceSwitcher(unittest.IsolatedAsyncioTestCase): async def test_service_switching(self): """Test that after service switching using ManuallySwitchServiceFrame, the new active service receives frames while others don't.""" - switcher = ServiceSwitcher(self.services, ServiceSwitcherStrategyManual) + switcher = ServiceSwitcher(self.services) # Reset counters for service in self.services: @@ -341,8 +366,8 @@ class TestServiceSwitcher(unittest.IsolatedAsyncioTestCase): switcher2_services = [switcher2_service1, switcher2_service2] # Create two service switchers - switcher1 = ServiceSwitcher(switcher1_services, ServiceSwitcherStrategyManual) - switcher2 = ServiceSwitcher(switcher2_services, ServiceSwitcherStrategyManual) + switcher1 = ServiceSwitcher(switcher1_services) + switcher2 = ServiceSwitcher(switcher2_services) # Create a pipeline with both switchers: switcher1 -> switcher2 pipeline = Pipeline([switcher1, switcher2]) @@ -428,7 +453,7 @@ class TestServiceSwitcherMetadata(unittest.IsolatedAsyncioTestCase): async def test_only_active_service_metadata_at_startup(self): """Test that only the active service's metadata leaves the ServiceSwitcher at startup.""" - switcher = ServiceSwitcher(self.services, ServiceSwitcherStrategyManual) + switcher = ServiceSwitcher(self.services) # Run the pipeline (StartFrame triggers metadata emission) output_frames = [] @@ -450,7 +475,7 @@ class TestServiceSwitcherMetadata(unittest.IsolatedAsyncioTestCase): async def test_metadata_emitted_on_service_switch(self): """Test that switching services triggers metadata emission from the new active service.""" - switcher = ServiceSwitcher(self.services, ServiceSwitcherStrategyManual) + switcher = ServiceSwitcher(self.services) # Reset counters after startup self.service1.reset_counters() @@ -482,7 +507,7 @@ class TestServiceSwitcherMetadata(unittest.IsolatedAsyncioTestCase): async def test_inactive_service_metadata_blocked(self): """Test that metadata from inactive services is blocked.""" - switcher = ServiceSwitcher(self.services, ServiceSwitcherStrategyManual) + switcher = ServiceSwitcher(self.services) # Run and collect output frames await run_test( @@ -497,5 +522,80 @@ class TestServiceSwitcherMetadata(unittest.IsolatedAsyncioTestCase): # Only one MockMetadataFrame should have left (from service1) +class TestServiceSwitcherStrategyFailover(unittest.IsolatedAsyncioTestCase): + """Test cases for ServiceSwitcherStrategyFailover.""" + + def setUp(self): + """Set up test fixtures.""" + self.service1 = MockFrameProcessor("service1") + self.service2 = MockFrameProcessor("service2") + self.service3 = MockFrameProcessor("service3") + self.services = [self.service1, self.service2, self.service3] + + def test_init_defaults(self): + """Test that default values are set correctly.""" + strategy = ServiceSwitcherStrategyFailover(self.services) + self.assertEqual(strategy.active_service, self.service1) + + async def test_error_switches_to_next_service(self): + """Test that an error on the active service switches to the next one.""" + strategy = ServiceSwitcherStrategyFailover(self.services) + + error = ErrorFrame(error="connection lost") + result = await strategy.handle_error(error) + + self.assertEqual(result, self.service2) + self.assertEqual(strategy.active_service, self.service2) + + async def test_consecutive_errors_cycle_through_services(self): + """Test that repeated errors cycle through all services.""" + strategy = ServiceSwitcherStrategyFailover(self.services) + + # First error: service1 -> service2 + await strategy.handle_error(ErrorFrame(error="error 1")) + self.assertEqual(strategy.active_service, self.service2) + + # Second error: service2 -> service3 + await strategy.handle_error(ErrorFrame(error="error 2")) + self.assertEqual(strategy.active_service, self.service3) + + # Third error: service3 -> service1 (wraps around) + await strategy.handle_error(ErrorFrame(error="error 3")) + self.assertEqual(strategy.active_service, self.service1) + + async def test_single_service_returns_none(self): + """Test that handle_error returns None with only one service.""" + strategy = ServiceSwitcherStrategyFailover([self.service1]) + + result = await strategy.handle_error(ErrorFrame(error="error")) + self.assertIsNone(result) + + async def test_manual_switch_still_works(self): + """Test that ManuallySwitchServiceFrame is still handled.""" + strategy = ServiceSwitcherStrategyFailover(self.services) + + frame = ManuallySwitchServiceFrame(service=self.service3) + result = await strategy.handle_frame(frame, FrameDirection.DOWNSTREAM) + + self.assertEqual(result, self.service3) + self.assertEqual(strategy.active_service, self.service3) + + async def test_on_service_switched_event_fires_on_error(self): + """Test that on_service_switched event fires when an error triggers a switch.""" + strategy = ServiceSwitcherStrategyFailover(self.services) + + switched_events = [] + + @strategy.event_handler("on_service_switched") + async def on_service_switched(strategy, service): + switched_events.append(service) + + await strategy.handle_error(ErrorFrame(error="error")) + await asyncio.sleep(0) + + self.assertEqual(len(switched_events), 1) + self.assertEqual(switched_events[0], self.service2) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_sync_parallel_pipeline.py b/tests/test_sync_parallel_pipeline.py new file mode 100644 index 000000000..6c6faf7c7 --- /dev/null +++ b/tests/test_sync_parallel_pipeline.py @@ -0,0 +1,117 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import asyncio +import unittest +from dataclasses import dataclass + +from pipecat.frames.frames import Frame, TextFrame +from pipecat.pipeline.sync_parallel_pipeline import FrameOrder, SyncParallelPipeline +from pipecat.processors.filters.identity_filter import IdentityFilter +from pipecat.processors.frame_processor import FrameDirection, FrameProcessor +from pipecat.tests.utils import run_test + + +@dataclass +class TaggedFrame(Frame): + """A simple tagged frame for testing pipeline ordering.""" + + tag: str = "" + + def __str__(self): + return f"{self.name}(tag: {self.tag})" + + +class EmitTaggedFrameProcessor(FrameProcessor): + """Emits a TaggedFrame for every TextFrame it receives. + + Used to produce distinguishable output from different pipelines so tests + can verify ordering. + """ + + def __init__(self, tag: str, *, delay: float = 0, **kwargs): + super().__init__(**kwargs) + self._tag = tag + self._delay = delay + + async def process_frame(self, frame: Frame, direction: FrameDirection): + await super().process_frame(frame, direction) + + if isinstance(frame, TextFrame): + if self._delay > 0: + await asyncio.sleep(self._delay) + await self.push_frame(TaggedFrame(tag=self._tag)) + else: + await self.push_frame(frame, direction) + + +class TestSyncParallelPipeline(unittest.IsolatedAsyncioTestCase): + async def test_dedup_multiple_frames(self): + """Identical frames from multiple paths should be deduplicated.""" + pipeline = SyncParallelPipeline([IdentityFilter()], [IdentityFilter()]) + + frames_to_send = [TextFrame(text="one"), TextFrame(text="two")] + expected_down_frames = [TextFrame, TextFrame] + await run_test( + pipeline, + frames_to_send=frames_to_send, + expected_down_frames=expected_down_frames, + ) + + async def test_arrival_order(self): + """With FrameOrder.ARRIVAL, a slow first pipeline's frames should + arrive after a fast second pipeline's frames.""" + pipeline = SyncParallelPipeline( + [EmitTaggedFrameProcessor("slow", delay=0.05)], + [EmitTaggedFrameProcessor("fast")], + frame_order=FrameOrder.ARRIVAL, + ) + + frames_to_send = [TextFrame(text="one"), TextFrame(text="two")] + (down_frames, _) = await run_test( + pipeline, + frames_to_send=frames_to_send, + ) + + tags = [f.tag for f in down_frames if isinstance(f, TaggedFrame)] + assert tags == [ + "fast", + "slow", + "fast", + "slow", + ], f"Expected fast before slow in each batch, got {tags}" + + async def test_pipeline_order(self): + """With FrameOrder.PIPELINE and multiple input frames, each batch + should follow pipeline definition order regardless of processing speed.""" + pipeline = SyncParallelPipeline( + [EmitTaggedFrameProcessor("slow", delay=0.05)], + [EmitTaggedFrameProcessor("fast")], + frame_order=FrameOrder.PIPELINE, + ) + + frames_to_send = [TextFrame(text="one"), TextFrame(text="two")] + (down_frames, _) = await run_test( + pipeline, + frames_to_send=frames_to_send, + ) + + tags = [f.tag for f in down_frames if isinstance(f, TaggedFrame)] + assert tags == [ + "slow", + "fast", + "slow", + "fast", + ], f"Expected pipeline definition order (slow, fast) in each batch, got {tags}" + + async def test_default_is_arrival(self): + """The default frame_order should be ARRIVAL.""" + pipeline = SyncParallelPipeline([IdentityFilter()]) + assert pipeline._frame_order == FrameOrder.ARRIVAL + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_tts_frame_ordering.py b/tests/test_tts_frame_ordering.py new file mode 100644 index 000000000..52d3df4e7 --- /dev/null +++ b/tests/test_tts_frame_ordering.py @@ -0,0 +1,315 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""Tests for frame ordering across TTS service types. + +Covers three patterns: +- HTTP TTS services (e.g. CartesiaHttpTTSService): yield audio frames synchronously. +- WebSocket TTS services without pause (e.g. CartesiaTTSService): deliver audio via + append_to_audio_context from a background receive loop, no frame-processing pause. +- WebSocket TTS services with pause (e.g. ElevenLabsTTSService): same delivery + mechanism, but pause downstream frame processing while audio is in flight. + +For all three patterns we verify: + AggregatedTextFrame → TTSStartedFrame → TTSAudioRawFrame (1+) → TTSStoppedFrame → FooFrame + +repeated for each TTSSpeakFrame, with no cross-group contamination. +""" + +import asyncio +import unittest +from dataclasses import dataclass +from typing import AsyncGenerator, List, Sequence, Tuple + +import pytest + +from pipecat.frames.frames import ( + AggregatedTextFrame, + DataFrame, + Frame, + TTSAudioRawFrame, + TTSSpeakFrame, + TTSStartedFrame, + TTSStoppedFrame, +) +from pipecat.services.tts_service import TTSService +from pipecat.tests.utils import run_test + +# --------------------------------------------------------------------------- +# Test-only frame +# --------------------------------------------------------------------------- + +_FAKE_AUDIO = b"\x00\x01" * 320 # 320 bytes of silence +_SAMPLE_RATE = 16000 + + +@dataclass +class FooFrame(DataFrame): + """Marker frame used to verify relative ordering against TTS audio frames.""" + + label: str = "" + + +# --------------------------------------------------------------------------- +# Mock TTS services +# --------------------------------------------------------------------------- + + +class MockHttpTTSService(TTSService): + """Simulates an HTTP TTS service (e.g. CartesiaHttpTTSService). + + Audio frames are yielded synchronously from run_tts(), so the audio context + is fully populated before the next downstream frame is processed. + TTSStoppedFrame is appended by the base class in on_turn_context_completed() + once it detects _is_yielding_frames_synchronously is True. + """ + + def __init__(self, **kwargs): + super().__init__( + push_start_frame=True, + push_stop_frames=True, + push_text_frames=False, + sample_rate=_SAMPLE_RATE, + **kwargs, + ) + + def can_generate_metrics(self) -> bool: + return False + + async def run_tts(self, text: str, context_id: str) -> AsyncGenerator[Frame, None]: + yield TTSAudioRawFrame( + audio=_FAKE_AUDIO, + sample_rate=_SAMPLE_RATE, + num_channels=1, + context_id=context_id, + ) + + +class MockWebSocketTTSService(TTSService): + """Simulates a WebSocket TTS service without frame-processing pause (e.g. CartesiaTTSService). + + run_tts() is an empty async generator (signals async delivery). A background + task appends audio frames and the TTSStoppedFrame to the audio context after a + short delay, mimicking real WebSocket receive-loop behaviour. + pause_frame_processing=False means downstream frames (FooFrame) are NOT held. + """ + + def __init__(self, **kwargs): + super().__init__( + push_start_frame=True, + push_text_frames=False, + pause_frame_processing=False, + sample_rate=_SAMPLE_RATE, + **kwargs, + ) + + def can_generate_metrics(self) -> bool: + return False + + async def run_tts(self, text: str, context_id: str) -> AsyncGenerator[Frame, None]: + async def _deliver_audio(): + await asyncio.sleep(0.01) + await self.append_to_audio_context( + context_id, + TTSAudioRawFrame( + audio=_FAKE_AUDIO, + sample_rate=_SAMPLE_RATE, + num_channels=1, + context_id=context_id, + ), + ) + await self.append_to_audio_context(context_id, TTSStoppedFrame(context_id=context_id)) + await self.remove_audio_context(context_id) + + self.create_task(_deliver_audio(), name=f"mock_ws_deliver_{context_id}") + if False: + yield # make this an async generator without yielding anything + + +class MockWebSocketPauseTTSService(TTSService): + """Simulates a WebSocket TTS service WITH frame-processing pause (e.g. ElevenLabsTTSService). + + Identical to MockWebSocketTTSService except pause_frame_processing=True. + on_audio_context_completed() resumes downstream processing once the full + audio context has been pushed, guaranteeing FooFrame arrives after TTSStoppedFrame. + """ + + def __init__(self, **kwargs): + super().__init__( + push_start_frame=True, + push_text_frames=False, + pause_frame_processing=True, + sample_rate=_SAMPLE_RATE, + **kwargs, + ) + + def can_generate_metrics(self) -> bool: + return False + + async def on_audio_context_completed(self, context_id: str): + # Resume frame processing after the audio context is fully played out. + await self._maybe_resume_frame_processing() + + async def run_tts(self, text: str, context_id: str) -> AsyncGenerator[Frame, None]: + async def _deliver_audio(): + await asyncio.sleep(0.01) + await self.append_to_audio_context( + context_id, + TTSAudioRawFrame( + audio=_FAKE_AUDIO, + sample_rate=_SAMPLE_RATE, + num_channels=1, + context_id=context_id, + ), + ) + await self.append_to_audio_context(context_id, TTSStoppedFrame(context_id=context_id)) + await self.remove_audio_context(context_id) + + self.create_task(_deliver_audio(), name=f"mock_ws_pause_deliver_{context_id}") + if False: + yield + + +# --------------------------------------------------------------------------- +# Assertion helper +# --------------------------------------------------------------------------- + + +def _assert_group_ordering( + down_frames: Sequence[Frame], + expected_groups: List[Tuple[str, str]], +) -> None: + """Assert two (or more) TTS+FooFrame groups are in strict order. + + Args: + down_frames: All downstream frames received by the test sink. + expected_groups: List of (tts_text, foo_label) pairs, one per TTSSpeakFrame. + tts_text is unused in assertions today but included for readability. + """ + relevant = [ + f + for f in down_frames + if isinstance( + f, (AggregatedTextFrame, TTSStartedFrame, TTSAudioRawFrame, TTSStoppedFrame, FooFrame) + ) + ] + + # Locate the FooFrames that delimit groups. + foo_indices = [i for i, f in enumerate(relevant) if isinstance(f, FooFrame)] + assert len(foo_indices) == len(expected_groups), ( + f"Expected {len(expected_groups)} FooFrames, got {len(foo_indices)}.\n" + f"Relevant frames: {[type(f).__name__ for f in relevant]}" + ) + + # Build groups: everything up to and including each FooFrame. + groups: List[List[Frame]] = [] + prev = 0 + for idx in foo_indices: + groups.append(relevant[prev : idx + 1]) + prev = idx + 1 + + for group, (_, foo_label) in zip(groups, expected_groups): + types = [type(f) for f in group] + type_names = [t.__name__ for t in types] + + assert AggregatedTextFrame in types, ( + f"Group {foo_label!r}: missing AggregatedTextFrame. Got: {type_names}" + ) + assert TTSStartedFrame in types, ( + f"Group {foo_label!r}: missing TTSStartedFrame. Got: {type_names}" + ) + assert TTSAudioRawFrame in types, ( + f"Group {foo_label!r}: missing TTSAudioRawFrame. Got: {type_names}" + ) + assert TTSStoppedFrame in types, ( + f"Group {foo_label!r}: missing TTSStoppedFrame. Got: {type_names}" + ) + + started_idx = types.index(TTSStartedFrame) + stopped_idx = types.index(TTSStoppedFrame) + foo_idx = types.index(FooFrame) + + assert started_idx < stopped_idx, ( + f"Group {foo_label!r}: TTSStartedFrame (pos {started_idx}) must precede " + f"TTSStoppedFrame (pos {stopped_idx}). Got: {type_names}" + ) + assert stopped_idx < foo_idx, ( + f"Group {foo_label!r}: TTSStoppedFrame (pos {stopped_idx}) must precede " + f"FooFrame (pos {foo_idx}). Got: {type_names}" + ) + + # All frames between TTSStartedFrame and TTSStoppedFrame must be audio. + mid_types = types[started_idx + 1 : stopped_idx] + for t in mid_types: + assert t is TTSAudioRawFrame, ( + f"Group {foo_label!r}: unexpected frame {t.__name__!r} between " + f"TTSStartedFrame and TTSStoppedFrame. Got: {type_names}" + ) + + # Check the FooFrame label. + actual_label = group[foo_idx].label + assert actual_label == foo_label, ( + f"Expected FooFrame(label={foo_label!r}), got label={actual_label!r}" + ) + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + +_GROUPS = [("test 1", "1"), ("test 2", "2")] + + +def _make_frames_no_sleep() -> List[Frame]: + """Return two TTSSpeakFrame+FooFrame pairs sent back-to-back. + + Only correct for services that pause downstream processing until the audio + context is fully consumed (pause_frame_processing=True + on_audio_context_completed). + """ + return [ + TTSSpeakFrame(text="test 1", append_to_context=False), + FooFrame(label="1"), + TTSSpeakFrame(text="test 2", append_to_context=False), + FooFrame(label="2"), + ] + + +def _print_frames_received(frames_received) -> None: + print("FRAMES RECEIVED:") + for frame in frames_received[0]: + print(frame.name) + + +@pytest.mark.asyncio +async def test_http_tts_frame_ordering(): + """HTTP TTS services yield audio synchronously.""" + tts = MockHttpTTSService() + frames_received = await run_test(tts, frames_to_send=_make_frames_no_sleep()) + + # only for debugging + _print_frames_received(frames_received) + + _assert_group_ordering(frames_received[0], _GROUPS) + + +@pytest.mark.asyncio +async def test_websocket_tts_no_pause_frame_ordering(): + """WebSocket TTS services without pause_frame_processing.""" + tts = MockWebSocketTTSService() + frames_received = await run_test(tts, frames_to_send=_make_frames_no_sleep()) + _assert_group_ordering(frames_received[0], _GROUPS) + + +@pytest.mark.asyncio +async def test_websocket_tts_with_pause_frame_ordering(): + """WebSocket TTS services with pause_frame_processing=True.""" + tts = MockWebSocketPauseTTSService() + frames_received = await run_test(tts, frames_to_send=_make_frames_no_sleep()) + _assert_group_ordering(frames_received[0], _GROUPS) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_user_turn_controller.py b/tests/test_user_turn_controller.py index 72a04a519..2883d39bd 100644 --- a/tests/test_user_turn_controller.py +++ b/tests/test_user_turn_controller.py @@ -15,6 +15,7 @@ from pipecat.frames.frames import ( VADUserStartedSpeakingFrame, VADUserStoppedSpeakingFrame, ) +from pipecat.turns.user_start import VADUserTurnStartStrategy from pipecat.turns.user_start.min_words_user_turn_start_strategy import ( MinWordsUserTurnStartStrategy, ) @@ -199,6 +200,73 @@ class TestUserTurnController(unittest.IsolatedAsyncioTestCase): self.assertTrue(should_stop) self.assertTrue(timeout) + async def test_late_transcription_between_turns_no_premature_stop(self): + """Test that a late transcription arriving between turns does not cause a premature stop. + + Reproduces the bug from issue #4053: after turn 1 completes and reset() + clears state, a late TranscriptionFrame sets _text to stale content. On + the next turn, that stale _text gates a premature turn stop via timeout(0) + before the current turn's transcript arrives. + + Uses only VADUserTurnStartStrategy (no TranscriptionUserTurnStartStrategy) + so the late transcription doesn't trigger a spurious turn start. + """ + controller = UserTurnController( + user_turn_strategies=UserTurnStrategies( + start=[VADUserTurnStartStrategy()], + stop=[SpeechTimeoutUserTurnStopStrategy(user_speech_timeout=TRANSCRIPTION_TIMEOUT)], + ), + user_turn_stop_timeout=USER_TURN_STOP_TIMEOUT, + ) + + await controller.setup(self.task_manager) + + start_count = 0 + stop_count = 0 + + @controller.event_handler("on_user_turn_started") + async def on_user_turn_started(controller, strategy, params): + nonlocal start_count + start_count += 1 + + @controller.event_handler("on_user_turn_stopped") + async def on_user_turn_stopped(controller, strategy, params): + nonlocal stop_count + stop_count += 1 + + # === Turn 1: S-T-E === + await controller.process_frame(VADUserStartedSpeakingFrame()) + self.assertEqual(start_count, 1) + + await controller.process_frame( + TranscriptionFrame(text="Hello!", user_id="", timestamp="now") + ) + + await controller.process_frame(VADUserStoppedSpeakingFrame()) + await asyncio.sleep(TRANSCRIPTION_TIMEOUT + 0.1) + self.assertEqual(stop_count, 1) + + # === Between turns: late transcription arrives === + # This sets _text on the stop strategy while _user_turn is False. + await controller.process_frame( + TranscriptionFrame(text="Hello!", user_id="", timestamp="now") + ) + + # === Turn 2: S-T-E (transcription arrives during turn) === + # The fix resets stop strategies at turn start, clearing stale _text. + await controller.process_frame(VADUserStartedSpeakingFrame()) + self.assertEqual(start_count, 2) + + await controller.process_frame( + TranscriptionFrame(text="How are you?", user_id="", timestamp="now") + ) + + await controller.process_frame(VADUserStoppedSpeakingFrame()) + + # Wait for user_speech_timeout to elapse — should get turn 2 stop + await asyncio.sleep(TRANSCRIPTION_TIMEOUT + 0.1) + self.assertEqual(stop_count, 2) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_user_turn_stop_strategy.py b/tests/test_user_turn_stop_strategy.py index 80fb98efc..85f9f2752 100644 --- a/tests/test_user_turn_stop_strategy.py +++ b/tests/test_user_turn_stop_strategy.py @@ -493,6 +493,50 @@ class TestSpeechTimeoutUserTurnStopStrategy(unittest.IsolatedAsyncioTestCase): # Finalized transcript received after timeout, triggers immediately self.assertTrue(should_start) + async def test_reset_clears_stale_text_no_premature_stop(self): + """Test that reset() clears stale text and cancels timeout, preventing premature stop. + + Reproduces the bug from issue #4053: after turn 1 completes and + reset() is called, a late transcription sets _text. If reset() is + called again at turn 2 start, the stale _text should be cleared + so no premature stop occurs on VAD stop. + """ + strategy = await self._create_strategy() + + stop_count = 0 + + @strategy.event_handler("on_user_turn_stopped") + async def on_user_turn_stopped(strategy, params): + nonlocal stop_count + stop_count += 1 + + # === Turn 1: S-T-E === + await strategy.process_frame(VADUserStartedSpeakingFrame()) + await strategy.process_frame(TranscriptionFrame(text="Hello!", user_id="cat", timestamp="")) + await strategy.process_frame(VADUserStoppedSpeakingFrame()) + await asyncio.sleep(AGGREGATION_TIMEOUT + 0.1) + self.assertEqual(stop_count, 1) + + # Reset after turn 1 (as controller would do at turn stop) + await strategy.reset() + + # === Late transcription arrives between turns === + await strategy.process_frame(TranscriptionFrame(text="Hello!", user_id="cat", timestamp="")) + + # Reset at turn 2 start (the fix: controller now resets stop strategies at turn start) + await strategy.reset() + + # === Turn 2: S-T-E (transcription arrives during turn) === + await strategy.process_frame(VADUserStartedSpeakingFrame()) + await strategy.process_frame( + TranscriptionFrame(text="How are you?", user_id="cat", timestamp="") + ) + await strategy.process_frame(VADUserStoppedSpeakingFrame()) + + # Wait for timeout — should get turn 2 stop with the real transcription + await asyncio.sleep(AGGREGATION_TIMEOUT + 0.1) + self.assertEqual(stop_count, 2) + class TestExternalUserTurnStopStrategy(unittest.IsolatedAsyncioTestCase): async def test_external_strategy(self): diff --git a/tests/test_wake_phrase_user_turn_start_strategy.py b/tests/test_wake_phrase_user_turn_start_strategy.py new file mode 100644 index 000000000..e79b9ab99 --- /dev/null +++ b/tests/test_wake_phrase_user_turn_start_strategy.py @@ -0,0 +1,346 @@ +# +# Copyright (c) 2024-2026, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import asyncio +import unittest + +from pipecat.frames.frames import ( + BotSpeakingFrame, + InterimTranscriptionFrame, + TranscriptionFrame, + UserSpeakingFrame, + VADUserStartedSpeakingFrame, +) +from pipecat.turns.types import ProcessFrameResult +from pipecat.turns.user_start.wake_phrase_user_turn_start_strategy import ( + WakePhraseUserTurnStartStrategy, + _WakeState, +) +from pipecat.utils.asyncio.task_manager import TaskManager, TaskManagerParams + + +class TestWakePhraseUserTurnStartStrategy(unittest.IsolatedAsyncioTestCase): + def _create_strategy(self, **kwargs) -> WakePhraseUserTurnStartStrategy: + kwargs.setdefault("phrases", ["hey pipecat"]) + kwargs.setdefault("timeout", 10.0) + return WakePhraseUserTurnStartStrategy(**kwargs) + + async def _setup_strategy(self, strategy: WakePhraseUserTurnStartStrategy): + task_manager = TaskManager() + loop = asyncio.get_running_loop() + task_manager.setup(TaskManagerParams(loop=loop)) + await strategy.setup(task_manager) + return task_manager + + async def test_wake_phrase_in_final_transcription(self): + strategy = self._create_strategy() + await self._setup_strategy(strategy) + + result = await strategy.process_frame( + TranscriptionFrame(text="hey pipecat", user_id="user1", timestamp="") + ) + self.assertEqual(result, ProcessFrameResult.STOP) + self.assertEqual(strategy.state, _WakeState.AWAKE) + + await strategy.cleanup() + + async def test_interim_transcription_ignored(self): + """Interim transcriptions are never used for wake phrase matching.""" + strategy = self._create_strategy() + await self._setup_strategy(strategy) + + result = await strategy.process_frame( + InterimTranscriptionFrame(text="hey pipecat", user_id="user1", timestamp="") + ) + self.assertEqual(result, ProcessFrameResult.STOP) + self.assertEqual(strategy.state, _WakeState.IDLE) + + await strategy.cleanup() + + async def test_no_wake_phrase_returns_stop(self): + strategy = self._create_strategy() + await self._setup_strategy(strategy) + + result = await strategy.process_frame( + TranscriptionFrame(text="hello world", user_id="user1", timestamp="") + ) + self.assertEqual(result, ProcessFrameResult.STOP) + self.assertEqual(strategy.state, _WakeState.IDLE) + + await strategy.cleanup() + + async def test_non_matching_text_resets_aggregation(self): + """Non-matching transcription triggers aggregation reset to prevent LLM context pollution.""" + strategy = self._create_strategy() + await self._setup_strategy(strategy) + + reset_called = False + + @strategy.event_handler("on_reset_aggregation") + async def on_reset_aggregation(strategy): + nonlocal reset_called + reset_called = True + + await strategy.process_frame( + TranscriptionFrame(text="hello world", user_id="user1", timestamp="") + ) + self.assertTrue(reset_called) + + await strategy.cleanup() + + async def test_vad_frame_returns_stop_in_listening(self): + strategy = self._create_strategy() + await self._setup_strategy(strategy) + + result = await strategy.process_frame(VADUserStartedSpeakingFrame()) + self.assertEqual(result, ProcessFrameResult.STOP) + self.assertEqual(strategy.state, _WakeState.IDLE) + + await strategy.cleanup() + + async def test_inactive_returns_continue(self): + strategy = self._create_strategy() + await self._setup_strategy(strategy) + + # Trigger wake phrase first. + await strategy.process_frame( + TranscriptionFrame(text="hey pipecat", user_id="user1", timestamp="") + ) + self.assertEqual(strategy.state, _WakeState.AWAKE) + + # Subsequent frames should return CONTINUE. + result = await strategy.process_frame(VADUserStartedSpeakingFrame()) + self.assertEqual(result, ProcessFrameResult.CONTINUE) + + result = await strategy.process_frame( + TranscriptionFrame(text="what is the weather", user_id="user1", timestamp="") + ) + self.assertEqual(result, ProcessFrameResult.CONTINUE) + + await strategy.cleanup() + + async def test_accumulation_across_frames(self): + strategy = self._create_strategy() + await self._setup_strategy(strategy) + + result = await strategy.process_frame( + TranscriptionFrame(text="hey", user_id="user1", timestamp="") + ) + self.assertEqual(result, ProcessFrameResult.STOP) + self.assertEqual(strategy.state, _WakeState.IDLE) + + result = await strategy.process_frame( + TranscriptionFrame(text="pipecat", user_id="user1", timestamp="") + ) + self.assertEqual(result, ProcessFrameResult.STOP) + self.assertEqual(strategy.state, _WakeState.AWAKE) + + await strategy.cleanup() + + async def test_multiple_phrases(self): + strategy = self._create_strategy(phrases=["hey pipecat", "ok computer"]) + await self._setup_strategy(strategy) + + result = await strategy.process_frame( + TranscriptionFrame(text="ok computer", user_id="user1", timestamp="") + ) + self.assertEqual(result, ProcessFrameResult.STOP) + self.assertEqual(strategy.state, _WakeState.AWAKE) + + await strategy.cleanup() + + async def test_punctuation_stripped(self): + """STT punctuation like 'Hey, Pipecat!' should still match.""" + strategy = self._create_strategy() + await self._setup_strategy(strategy) + + result = await strategy.process_frame( + TranscriptionFrame(text="Hey, Pipecat!", user_id="user1", timestamp="") + ) + self.assertEqual(result, ProcessFrameResult.STOP) + self.assertEqual(strategy.state, _WakeState.AWAKE) + + await strategy.cleanup() + + async def test_reset_preserves_inactive_state(self): + strategy = self._create_strategy() + await self._setup_strategy(strategy) + + await strategy.process_frame( + TranscriptionFrame(text="hey pipecat", user_id="user1", timestamp="") + ) + self.assertEqual(strategy.state, _WakeState.AWAKE) + + await strategy.reset() + self.assertEqual(strategy.state, _WakeState.AWAKE) + + await strategy.cleanup() + + async def test_timeout_returns_to_listening(self): + strategy = self._create_strategy(timeout=0.1) + await self._setup_strategy(strategy) + + # Trigger wake phrase. + await strategy.process_frame( + TranscriptionFrame(text="hey pipecat", user_id="user1", timestamp="") + ) + self.assertEqual(strategy.state, _WakeState.AWAKE) + + # Wait for timeout to expire. + await asyncio.sleep(0.3) + + self.assertEqual(strategy.state, _WakeState.IDLE) + + await strategy.cleanup() + + async def test_activity_refreshes_timeout(self): + strategy = self._create_strategy(timeout=0.2) + await self._setup_strategy(strategy) + + # Trigger wake phrase. + await strategy.process_frame( + TranscriptionFrame(text="hey pipecat", user_id="user1", timestamp="") + ) + self.assertEqual(strategy.state, _WakeState.AWAKE) + + # Send activity before timeout. + await asyncio.sleep(0.1) + await strategy.process_frame(UserSpeakingFrame()) + self.assertEqual(strategy.state, _WakeState.AWAKE) + + # Send more activity. + await asyncio.sleep(0.1) + await strategy.process_frame(BotSpeakingFrame()) + self.assertEqual(strategy.state, _WakeState.AWAKE) + + # Wait for timeout to expire after last activity. + await asyncio.sleep(0.3) + self.assertEqual(strategy.state, _WakeState.IDLE) + + await strategy.cleanup() + + async def test_wake_phrase_detected_event(self): + strategy = self._create_strategy() + await self._setup_strategy(strategy) + + detected_phrase = None + + @strategy.event_handler("on_wake_phrase_detected") + async def on_wake_phrase_detected(strategy, phrase): + nonlocal detected_phrase + detected_phrase = phrase + + await strategy.process_frame( + TranscriptionFrame(text="hey pipecat", user_id="user1", timestamp="") + ) + + # Event fires in a background task, give it a moment. + await asyncio.sleep(0.05) + self.assertEqual(detected_phrase, "hey pipecat") + + await strategy.cleanup() + + async def test_wake_phrase_timeout_event(self): + strategy = self._create_strategy(timeout=0.1) + await self._setup_strategy(strategy) + + timeout_fired = False + + @strategy.event_handler("on_wake_phrase_timeout") + async def on_wake_phrase_timeout(strategy): + nonlocal timeout_fired + timeout_fired = True + + await strategy.process_frame( + TranscriptionFrame(text="hey pipecat", user_id="user1", timestamp="") + ) + + # Wait for timeout. + await asyncio.sleep(0.3) + self.assertTrue(timeout_fired) + + await strategy.cleanup() + + async def test_single_activation_stays_inactive_after_reset(self): + """In single activation mode, reset() keeps INACTIVE so the current turn can finish.""" + strategy = self._create_strategy(single_activation=True, timeout=0.5) + await self._setup_strategy(strategy) + + # Trigger wake phrase. + result = await strategy.process_frame( + TranscriptionFrame(text="hey pipecat", user_id="user1", timestamp="") + ) + self.assertEqual(result, ProcessFrameResult.STOP) + self.assertEqual(strategy.state, _WakeState.AWAKE) + + # Simulate turn start (controller calls reset on all start strategies). + await strategy.reset() + # State remains INACTIVE so frames continue to flow. + self.assertEqual(strategy.state, _WakeState.AWAKE) + + # Subsequent frames should pass through (CONTINUE). + result = await strategy.process_frame(VADUserStartedSpeakingFrame()) + self.assertEqual(result, ProcessFrameResult.CONTINUE) + + result = await strategy.process_frame( + TranscriptionFrame(text="what is the weather", user_id="user1", timestamp="") + ) + self.assertEqual(result, ProcessFrameResult.CONTINUE) + + await strategy.cleanup() + + async def test_single_activation_timeout_returns_to_listening(self): + """In single activation mode, the keepalive timeout returns to LISTENING.""" + strategy = self._create_strategy(single_activation=True, timeout=0.1) + await self._setup_strategy(strategy) + + # Trigger wake phrase. + await strategy.process_frame( + TranscriptionFrame(text="hey pipecat", user_id="user1", timestamp="") + ) + self.assertEqual(strategy.state, _WakeState.AWAKE) + + # Wait for keepalive timeout to expire. + await asyncio.sleep(0.3) + self.assertEqual(strategy.state, _WakeState.IDLE) + + # Frames should now be blocked again. + result = await strategy.process_frame(VADUserStartedSpeakingFrame()) + self.assertEqual(result, ProcessFrameResult.STOP) + + await strategy.cleanup() + + async def test_single_activation_requires_wake_phrase_after_timeout(self): + """Single activation mode requires wake phrase again after keepalive timeout.""" + strategy = self._create_strategy(single_activation=True, timeout=0.1) + await self._setup_strategy(strategy) + + # First turn: wake phrase -> INACTIVE -> timeout -> LISTENING. + await strategy.process_frame( + TranscriptionFrame(text="hey pipecat", user_id="user1", timestamp="") + ) + self.assertEqual(strategy.state, _WakeState.AWAKE) + await asyncio.sleep(0.3) + self.assertEqual(strategy.state, _WakeState.IDLE) + + # Without wake phrase, frames are blocked. + result = await strategy.process_frame( + TranscriptionFrame(text="what is the weather", user_id="user1", timestamp="") + ) + self.assertEqual(result, ProcessFrameResult.STOP) + + # Second turn: wake phrase again. + result = await strategy.process_frame( + TranscriptionFrame(text="hey pipecat", user_id="user1", timestamp="") + ) + self.assertEqual(result, ProcessFrameResult.STOP) + self.assertEqual(strategy.state, _WakeState.AWAKE) + + await strategy.cleanup() + + +if __name__ == "__main__": + unittest.main() diff --git a/uv.lock b/uv.lock index 9d27b1c3a..1263097a8 100644 --- a/uv.lock +++ b/uv.lock @@ -16,7 +16,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "packaging" }, { name = "psutil" }, { name = "pyyaml" }, @@ -34,7 +34,7 @@ version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ea/d1/faca6596c0d598063b4b9e879f4110fde9dee1496273d6410505bc81fdcc/aic_sdk-2.1.0.tar.gz", hash = "sha256:b661743dce36413ddd264b909d818dfc997c3a189e4c52fed263f2177ee3bb17", size = 5315216, upload-time = "2026-02-27T23:04:43.644Z" } wheels = [ @@ -376,19 +376,20 @@ wheels = [ [[package]] name = "audiolab" -version = "0.4.8" +version = "0.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "av" }, { name = "click" }, { name = "humanize" }, { name = "jinja2" }, + { name = "requests" }, { name = "smart-open" }, { name = "soundfile" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/37/4321edad0813a7779472db47dacd9884c3ce00e381215e5a7fae8aad5ac6/audiolab-0.4.8.tar.gz", hash = "sha256:b6cd0e3e0bdee45e2df30b875f8577a3a101b2d0632a854465862e05602a0a6b", size = 32490, upload-time = "2026-01-15T14:14:04.095Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/ba/c0fab2cff574cfef0fb81071c1a0bc9f021b477a341adcf79e3220447b3f/audiolab-0.5.0.tar.gz", hash = "sha256:12f33c3cbbd09a9b6089f78fa8dd3f6472af345e8cdd6524009e5b2409b46c6a", size = 32970, upload-time = "2026-03-16T11:43:44.544Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/28/e9cd5a5b8838cff665f4e176d381d87d395641955615a94e8f19d7c0d361/audiolab-0.4.8-py3-none-any.whl", hash = "sha256:bddc5be8c7abbea292416fd67532c33112a677084086fa060bb7155d3fff9274", size = 51326, upload-time = "2026-01-15T14:14:02.813Z" }, + { url = "https://files.pythonhosted.org/packages/84/0f/c96512dedf6dcb06bf3647460d4bbd0f119853f9bb60115703cee1cc1711/audiolab-0.5.0-py3-none-any.whl", hash = "sha256:9670e6253cec87cca8e6f37c32abc93aadfe5e185b6743e05c83bdee2acd310e", size = 51682, upload-time = "2026-03-16T11:43:43.351Z" }, ] [[package]] @@ -588,15 +589,15 @@ wheels = [ [[package]] name = "azure-core" -version = "1.38.2" +version = "1.38.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/fe/5c7710bc611a4070d06ba801de9a935cc87c3d4b689c644958047bdf2cba/azure_core-1.38.2.tar.gz", hash = "sha256:67562857cb979217e48dc60980243b61ea115b77326fa93d83b729e7ff0482e7", size = 363734, upload-time = "2026-02-18T19:33:05.6Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/29/9641b73248745774a52c7ce7f965ed1febbdea787ec21caad3ae6891d18a/azure_core-1.38.3.tar.gz", hash = "sha256:a7931fd445cb4af8802c6f39c6a326bbd1e34b115846550a8245fa656ead6f8e", size = 367267, upload-time = "2026-03-12T20:28:21.122Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/23/6371a551800d3812d6019cd813acd985f9fac0fedc1290129211a73da4ae/azure_core-1.38.2-py3-none-any.whl", hash = "sha256:074806c75cf239ea284a33a66827695ef7aeddac0b4e19dda266a93e4665ead9", size = 217957, upload-time = "2026-02-18T19:33:07.696Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3d/ac86083efa45a439d0bbfb7947615227813d368b9e1e93d23fd30de6fec0/azure_core-1.38.3-py3-none-any.whl", hash = "sha256:bf59d29765bf4748ab9edf25f98a30b7ea9797f43e367c06d846a30b29c1f845", size = 218231, upload-time = "2026-03-12T20:28:22.462Z" }, ] [[package]] @@ -672,7 +673,7 @@ wheels = [ [[package]] name = "camb-sdk" -version = "1.5.9" +version = "1.5.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -681,9 +682,9 @@ dependencies = [ { name = "websocket-client" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/86/29/17527519a72ed1592f28a4d380fd50ed72978ac38148efc0f9e796504496/camb_sdk-1.5.9.tar.gz", hash = "sha256:c8daaa8eea20c94523ffddd2aa630a902932f78ea8af37e140603e52ff0025ad", size = 83521, upload-time = "2026-02-27T22:57:18.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/50/38/31fcc633963804a150175078c167f237bbd56dedba9903e4800a630ab944/camb_sdk-1.5.10.tar.gz", hash = "sha256:e1d23cbccea5aef5944612740b5c2e109a3c3ce778caa9664678a23b3a254419", size = 83643, upload-time = "2026-03-12T11:40:36.822Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/2a/b759c32c60c51f33ceb299b52f8f73348773cd75d3177a15eefc25b2dee9/camb_sdk-1.5.9-py3-none-any.whl", hash = "sha256:8c3fe9d05adee1d8de121eb6f1ee0a37e913f072d89c11ed3399746a9b69adbc", size = 152395, upload-time = "2026-02-27T22:57:14.137Z" }, + { url = "https://files.pythonhosted.org/packages/94/12/f294bf4f343b663dced6d8ea840985d58b0afd66cd40e221fc19dc6639f4/camb_sdk-1.5.10-py3-none-any.whl", hash = "sha256:5ec6014af18cf108041c921f743fbcabc17015df2fc4b7a17793ef17d0fe593a", size = 152463, upload-time = "2026-03-12T11:38:37.934Z" }, ] [[package]] @@ -802,91 +803,107 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.4" +version = "3.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, - { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, - { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, - { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, - { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, - { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, - { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, - { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, - { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, - { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, - { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, - { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, - { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, - { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, - { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, - { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, - { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, - { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, - { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, - { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, - { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, - { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, - { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, - { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, - { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, - { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, - { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, - { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, - { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, - { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, - { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, - { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, - { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, - { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, - { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, - { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, - { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, - { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, - { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, - { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, - { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, - { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, - { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, - { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, - { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, - { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, - { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, - { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, - { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, - { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, - { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, - { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, - { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, - { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, - { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, - { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, - { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, - { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, - { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, - { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, - { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/2c56124c6dc53a774d435f985b5973bc592f42d437be58c0c92d65ae7296/charset_normalizer-3.4.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95", size = 298751, upload-time = "2026-03-15T18:50:00.003Z" }, + { url = "https://files.pythonhosted.org/packages/86/2a/2a7db6b314b966a3bcad8c731c0719c60b931b931de7ae9f34b2839289ee/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd", size = 200027, upload-time = "2026-03-15T18:50:01.702Z" }, + { url = "https://files.pythonhosted.org/packages/68/f2/0fe775c74ae25e2a3b07b01538fc162737b3e3f795bada3bc26f4d4d495c/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4", size = 220741, upload-time = "2026-03-15T18:50:03.194Z" }, + { url = "https://files.pythonhosted.org/packages/10/98/8085596e41f00b27dd6aa1e68413d1ddda7e605f34dd546833c61fddd709/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db", size = 215802, upload-time = "2026-03-15T18:50:05.859Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ce/865e4e09b041bad659d682bbd98b47fb490b8e124f9398c9448065f64fee/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89", size = 207908, upload-time = "2026-03-15T18:50:07.676Z" }, + { url = "https://files.pythonhosted.org/packages/a8/54/8c757f1f7349262898c2f169e0d562b39dcb977503f18fdf0814e923db78/charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565", size = 194357, upload-time = "2026-03-15T18:50:09.327Z" }, + { url = "https://files.pythonhosted.org/packages/6f/29/e88f2fac9218907fc7a70722b393d1bbe8334c61fe9c46640dba349b6e66/charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9", size = 205610, upload-time = "2026-03-15T18:50:10.732Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c5/21d7bb0cb415287178450171d130bed9d664211fdd59731ed2c34267b07d/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7", size = 203512, upload-time = "2026-03-15T18:50:12.535Z" }, + { url = "https://files.pythonhosted.org/packages/a4/be/ce52f3c7fdb35cc987ad38a53ebcef52eec498f4fb6c66ecfe62cfe57ba2/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550", size = 195398, upload-time = "2026-03-15T18:50:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/81/a0/3ab5dd39d4859a3555e5dadfc8a9fa7f8352f8c183d1a65c90264517da0e/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0", size = 221772, upload-time = "2026-03-15T18:50:15.581Z" }, + { url = "https://files.pythonhosted.org/packages/04/6e/6a4e41a97ba6b2fa87f849c41e4d229449a586be85053c4d90135fe82d26/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8", size = 205759, upload-time = "2026-03-15T18:50:17.047Z" }, + { url = "https://files.pythonhosted.org/packages/db/3b/34a712a5ee64a6957bf355b01dc17b12de457638d436fdb05d01e463cd1c/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0", size = 216938, upload-time = "2026-03-15T18:50:18.44Z" }, + { url = "https://files.pythonhosted.org/packages/cb/05/5bd1e12da9ab18790af05c61aafd01a60f489778179b621ac2a305243c62/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b", size = 210138, upload-time = "2026-03-15T18:50:19.852Z" }, + { url = "https://files.pythonhosted.org/packages/bd/8e/3cb9e2d998ff6b21c0a1860343cb7b83eba9cdb66b91410e18fc4969d6ab/charset_normalizer-3.4.6-cp310-cp310-win32.whl", hash = "sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557", size = 144137, upload-time = "2026-03-15T18:50:21.505Z" }, + { url = "https://files.pythonhosted.org/packages/d8/8f/78f5489ffadb0db3eb7aff53d31c24531d33eb545f0c6f6567c25f49a5ff/charset_normalizer-3.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6", size = 154244, upload-time = "2026-03-15T18:50:22.81Z" }, + { url = "https://files.pythonhosted.org/packages/e4/74/e472659dffb0cadb2f411282d2d76c60da1fc94076d7fffed4ae8a93ec01/charset_normalizer-3.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058", size = 143312, upload-time = "2026-03-15T18:50:24.074Z" }, + { url = "https://files.pythonhosted.org/packages/62/28/ff6f234e628a2de61c458be2779cb182bc03f6eec12200d4a525bbfc9741/charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e", size = 293582, upload-time = "2026-03-15T18:50:25.454Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b7/b1a117e5385cbdb3205f6055403c2a2a220c5ea80b8716c324eaf75c5c95/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9", size = 197240, upload-time = "2026-03-15T18:50:27.196Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5f/2574f0f09f3c3bc1b2f992e20bce6546cb1f17e111c5be07308dc5427956/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d", size = 217363, upload-time = "2026-03-15T18:50:28.601Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d1/0ae20ad77bc949ddd39b51bf383b6ca932f2916074c95cad34ae465ab71f/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de", size = 212994, upload-time = "2026-03-15T18:50:30.102Z" }, + { url = "https://files.pythonhosted.org/packages/60/ac/3233d262a310c1b12633536a07cde5ddd16985e6e7e238e9f3f9423d8eb9/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73", size = 204697, upload-time = "2026-03-15T18:50:31.654Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/8a18fc411f085b82303cfb7154eed5bd49c77035eb7608d049468b53f87c/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c", size = 191673, upload-time = "2026-03-15T18:50:33.433Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a7/11cfe61d6c5c5c7438d6ba40919d0306ed83c9ab957f3d4da2277ff67836/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc", size = 201120, upload-time = "2026-03-15T18:50:35.105Z" }, + { url = "https://files.pythonhosted.org/packages/b5/10/cf491fa1abd47c02f69687046b896c950b92b6cd7337a27e6548adbec8e4/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f", size = 200911, upload-time = "2026-03-15T18:50:36.819Z" }, + { url = "https://files.pythonhosted.org/packages/28/70/039796160b48b18ed466fde0af84c1b090c4e288fae26cd674ad04a2d703/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef", size = 192516, upload-time = "2026-03-15T18:50:38.228Z" }, + { url = "https://files.pythonhosted.org/packages/ff/34/c56f3223393d6ff3124b9e78f7de738047c2d6bc40a4f16ac0c9d7a1cb3c/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398", size = 218795, upload-time = "2026-03-15T18:50:39.664Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3b/ce2d4f86c5282191a041fdc5a4ce18f1c6bd40a5bd1f74cf8625f08d51c1/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e", size = 201833, upload-time = "2026-03-15T18:50:41.552Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9b/b6a9f76b0fd7c5b5ec58b228ff7e85095370282150f0bd50b3126f5506d6/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed", size = 213920, upload-time = "2026-03-15T18:50:43.33Z" }, + { url = "https://files.pythonhosted.org/packages/ae/98/7bc23513a33d8172365ed30ee3a3b3fe1ece14a395e5fc94129541fc6003/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021", size = 206951, upload-time = "2026-03-15T18:50:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/32/73/c0b86f3d1458468e11aec870e6b3feac931facbe105a894b552b0e518e79/charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e", size = 143703, upload-time = "2026-03-15T18:50:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e3/76f2facfe8eddee0bbd38d2594e709033338eae44ebf1738bcefe0a06185/charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4", size = 153857, upload-time = "2026-03-15T18:50:47.563Z" }, + { url = "https://files.pythonhosted.org/packages/e2/dc/9abe19c9b27e6cd3636036b9d1b387b78c40dedbf0b47f9366737684b4b0/charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316", size = 142751, upload-time = "2026-03-15T18:50:49.234Z" }, + { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" }, + { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" }, + { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" }, + { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" }, + { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" }, + { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" }, + { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" }, + { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" }, + { url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" }, + { url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" }, + { url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, + { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, + { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, + { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, + { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, + { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, + { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, + { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" }, + { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" }, + { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" }, + { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" }, + { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" }, + { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" }, + { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" }, + { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" }, + { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" }, + { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" }, + { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" }, + { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" }, + { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" }, + { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" }, + { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" }, + { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" }, + { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" }, + { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" }, + { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, ] [[package]] @@ -1003,7 +1020,7 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } wheels = [ @@ -1088,7 +1105,7 @@ dependencies = [ { name = "attrs" }, { name = "cattrs" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "packaging" }, { name = "protobuf" }, { name = "pyaml" }, @@ -1312,7 +1329,7 @@ version = "4.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pyyaml" }, { name = "setuptools" }, ] @@ -1368,10 +1385,10 @@ wheels = [ [[package]] name = "cuda-pathfinder" -version = "1.4.0" +version = "1.4.3" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/60/d8f1dbfb7f06b94c662e98c95189e6f39b817da638bc8fcea0d003f89e5d/cuda_pathfinder-1.4.0-py3-none-any.whl", hash = "sha256:437079ca59e7b61ae439ecc501d69ed87b3accc34d58153ef1e54815e2c2e118", size = 38406, upload-time = "2026-02-25T22:13:00.807Z" }, + { url = "https://files.pythonhosted.org/packages/c0/59/911a1a597264f1fb7ac176995a0f0b6062e37f8c1b6e0f23071a76838507/cuda_pathfinder-1.4.3-py3-none-any.whl", hash = "sha256:4345d8ead1f701c4fb8a99be6bc1843a7348b6ba0ef3b031f5a2d66fb128ae4c", size = 47951, upload-time = "2026-03-16T21:31:25.526Z" }, ] [[package]] @@ -1385,13 +1402,13 @@ wheels = [ [[package]] name = "daily-python" -version = "0.23.0" +version = "0.25.0" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/3b/0f1465833787db21933caf9db0c4e45d10a31f477c2ae08ebe3edf31764d/daily_python-0.23.0-cp37-abi3-macosx_10_15_x86_64.whl", hash = "sha256:74998c9aabeccf8306def27219eb943df5a268f26ba7e25e0a105ced9425591a", size = 13450473, upload-time = "2025-12-18T03:21:50.488Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ca/7a9b581d411101454855c21559b1c9d9d58b6c527480548bba03e00a5641/daily_python-0.23.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:ce80746f5cba434371a9cb9995d8c4ec19d1490bd993b7dfd35356d81bd68425", size = 11988100, upload-time = "2025-12-18T03:21:53.265Z" }, - { url = "https://files.pythonhosted.org/packages/40/eb/15303b6ae2cdc5c591d6629d162085037697ca519d57751408c66f775991/daily_python-0.23.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:21a59b1730ae047f2335b9fdcf8ebb9fe05c3768dc4239ec955461f3f78476b0", size = 14094021, upload-time = "2025-12-18T03:21:55.606Z" }, - { url = "https://files.pythonhosted.org/packages/65/b9/f063e4854ba6e4edeea46e082720b7af82fb3cbf27e60efce63dbe34fe6d/daily_python-0.23.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:00b22041ef85556cc06262f9b8e39b686bdf37d50219d07542d7a2f9c3d413d4", size = 14616886, upload-time = "2025-12-18T03:21:58.084Z" }, + { url = "https://files.pythonhosted.org/packages/1b/58/074c6fca866fa13006b880eab521985d39300ea0d1df75a60d6dac4b2d47/daily_python-0.25.0-cp37-abi3-macosx_10_15_x86_64.whl", hash = "sha256:bff92b598863201bdffeea17819b7418d5c03a7ffc665a02be0333237fb3e4be", size = 13312157, upload-time = "2026-03-17T00:10:03.273Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d1/0b623e8e6d06713e8d193335db1e5ec46760af276f08d8b378a0a8e4696b/daily_python-0.25.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:733c70e77bb1c8933a7fa779722592a109617b2d098c192399c2bfffcb210847", size = 11831685, upload-time = "2026-03-17T00:10:05.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/36/45c3a59e92a37e3a51dbe2603f9e855408ecdb48f1b4cd396de83839e6f9/daily_python-0.25.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a6da5f8c99ccbdb59042a4c5e48f9a85f68a32f65f16b3778da5f319fd6d7e17", size = 13865923, upload-time = "2026-03-17T00:10:07.391Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5e/4574dedb8faa6a578079c89825f17c07a0bd4e1b4c2d2161966e62a6c6aa/daily_python-0.25.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0258ce43b92682379ecfcee1fa276799ccf41db25410f86395ae9927c2661cba", size = 14439736, upload-time = "2026-03-17T00:10:09.51Z" }, ] [[package]] @@ -1604,7 +1621,7 @@ standard = [ [[package]] name = "fastapi-cloud-cli" -version = "0.14.0" +version = "0.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastar" }, @@ -1616,9 +1633,9 @@ dependencies = [ { name = "typer" }, { name = "uvicorn", extra = ["standard"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2b/eb/e78ebd05a714c62a0578cdce4339cb6cd138421a7d865fbddedd7242420b/fastapi_cloud_cli-0.14.0.tar.gz", hash = "sha256:d3ecb8c942685a71df0af7bd59f463b5eff76f5818b48e5a03c6159726831e68", size = 39822, upload-time = "2026-02-25T14:19:53.535Z" } +sdist = { url = "https://files.pythonhosted.org/packages/63/e1/05c44e7bbc619e980fab0236cff9f5f323ac1aaa79434b4906febf98b1d3/fastapi_cloud_cli-0.15.0.tar.gz", hash = "sha256:d02515231f3f505f7669c20920343934570a88a08af9f9a6463ca2807f27ffe5", size = 45309, upload-time = "2026-03-11T22:31:32.455Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/18/7bf922ee0b6a737a9d88cf613182ecd6031f52298da893556f158eba763f/fastapi_cloud_cli-0.14.0-py3-none-any.whl", hash = "sha256:325fcb4b45e661184152da6db861d9fb718739fbcd561a4d334dbe78c026586f", size = 28350, upload-time = "2026-02-25T14:19:52.416Z" }, + { url = "https://files.pythonhosted.org/packages/40/cc/1ccca747f5609be27186ea8c9219449142f40e3eded2c6089bba6a6ecc82/fastapi_cloud_cli-0.15.0-py3-none-any.whl", hash = "sha256:9ffcf90bd713747efa65447620d29cfbb7b3f7de38d97467952ca6346e418d70", size = 32267, upload-time = "2026-03-11T22:31:33.499Z" }, ] [[package]] @@ -1760,11 +1777,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.25.0" +version = "3.25.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/77/18/a1fd2231c679dcb9726204645721b12498aeac28e1ad0601038f94b42556/filelock-3.25.0.tar.gz", hash = "sha256:8f00faf3abf9dc730a1ffe9c354ae5c04e079ab7d3a683b7c32da5dd05f26af3", size = 40158, upload-time = "2026-03-01T15:08:45.916Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/0b/de6f54d4a8bedfe8645c41497f3c18d749f0bd3218170c667bf4b81d0cdd/filelock-3.25.0-py3-none-any.whl", hash = "sha256:5ccf8069f7948f494968fc0713c10e5c182a9c9d9eef3a636307a20c2490f047", size = 26427, upload-time = "2026-03-01T15:08:44.593Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, ] [[package]] @@ -1777,59 +1794,59 @@ wheels = [ [[package]] name = "fonttools" -version = "4.61.1" +version = "4.62.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/94/8a28707adb00bed1bf22dac16ccafe60faf2ade353dcb32c3617ee917307/fonttools-4.61.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24", size = 2854799, upload-time = "2025-12-12T17:29:27.5Z" }, - { url = "https://files.pythonhosted.org/packages/94/93/c2e682faaa5ee92034818d8f8a8145ae73eb83619600495dcf8503fa7771/fonttools-4.61.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5fe9fd43882620017add5eabb781ebfbc6998ee49b35bd7f8f79af1f9f99a958", size = 2403032, upload-time = "2025-12-12T17:29:30.115Z" }, - { url = "https://files.pythonhosted.org/packages/f1/62/1748f7e7e1ee41aa52279fd2e3a6d0733dc42a673b16932bad8e5d0c8b28/fonttools-4.61.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8db08051fc9e7d8bc622f2112511b8107d8f27cd89e2f64ec45e9825e8288da", size = 4897863, upload-time = "2025-12-12T17:29:32.535Z" }, - { url = "https://files.pythonhosted.org/packages/69/69/4ca02ee367d2c98edcaeb83fc278d20972502ee071214ad9d8ca85e06080/fonttools-4.61.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a76d4cb80f41ba94a6691264be76435e5f72f2cb3cab0b092a6212855f71c2f6", size = 4859076, upload-time = "2025-12-12T17:29:34.907Z" }, - { url = "https://files.pythonhosted.org/packages/8c/f5/660f9e3cefa078861a7f099107c6d203b568a6227eef163dd173bfc56bdc/fonttools-4.61.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a13fc8aeb24bad755eea8f7f9d409438eb94e82cf86b08fe77a03fbc8f6a96b1", size = 4875623, upload-time = "2025-12-12T17:29:37.33Z" }, - { url = "https://files.pythonhosted.org/packages/63/d1/9d7c5091d2276ed47795c131c1bf9316c3c1ab2789c22e2f59e0572ccd38/fonttools-4.61.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b846a1fcf8beadeb9ea4f44ec5bdde393e2f1569e17d700bfc49cd69bde75881", size = 4993327, upload-time = "2025-12-12T17:29:39.781Z" }, - { url = "https://files.pythonhosted.org/packages/6f/2d/28def73837885ae32260d07660a052b99f0aa00454867d33745dfe49dbf0/fonttools-4.61.1-cp310-cp310-win32.whl", hash = "sha256:78a7d3ab09dc47ac1a363a493e6112d8cabed7ba7caad5f54dbe2f08676d1b47", size = 1502180, upload-time = "2025-12-12T17:29:42.217Z" }, - { url = "https://files.pythonhosted.org/packages/63/fa/bfdc98abb4dd2bd491033e85e3ba69a2313c850e759a6daa014bc9433b0f/fonttools-4.61.1-cp310-cp310-win_amd64.whl", hash = "sha256:eff1ac3cc66c2ac7cda1e64b4e2f3ffef474b7335f92fc3833fc632d595fcee6", size = 1550654, upload-time = "2025-12-12T17:29:44.564Z" }, - { url = "https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09", size = 2852213, upload-time = "2025-12-12T17:29:46.675Z" }, - { url = "https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37", size = 2401689, upload-time = "2025-12-12T17:29:48.769Z" }, - { url = "https://files.pythonhosted.org/packages/e5/fe/e6ce0fe20a40e03aef906af60aa87668696f9e4802fa283627d0b5ed777f/fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb", size = 5058809, upload-time = "2025-12-12T17:29:51.701Z" }, - { url = "https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9", size = 5036039, upload-time = "2025-12-12T17:29:53.659Z" }, - { url = "https://files.pythonhosted.org/packages/99/cc/fa1801e408586b5fce4da9f5455af8d770f4fc57391cd5da7256bb364d38/fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87", size = 5034714, upload-time = "2025-12-12T17:29:55.592Z" }, - { url = "https://files.pythonhosted.org/packages/bf/aa/b7aeafe65adb1b0a925f8f25725e09f078c635bc22754f3fecb7456955b0/fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56", size = 5158648, upload-time = "2025-12-12T17:29:57.861Z" }, - { url = "https://files.pythonhosted.org/packages/99/f9/08ea7a38663328881384c6e7777bbefc46fd7d282adfd87a7d2b84ec9d50/fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a", size = 2280681, upload-time = "2025-12-12T17:29:59.943Z" }, - { url = "https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7", size = 2331951, upload-time = "2025-12-12T17:30:02.254Z" }, - { url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593, upload-time = "2025-12-12T17:30:04.225Z" }, - { url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231, upload-time = "2025-12-12T17:30:06.47Z" }, - { url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103, upload-time = "2025-12-12T17:30:08.432Z" }, - { url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295, upload-time = "2025-12-12T17:30:10.56Z" }, - { url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109, upload-time = "2025-12-12T17:30:12.874Z" }, - { url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598, upload-time = "2025-12-12T17:30:15.79Z" }, - { url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060, upload-time = "2025-12-12T17:30:18.058Z" }, - { url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078, upload-time = "2025-12-12T17:30:22.862Z" }, - { url = "https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c", size = 2846454, upload-time = "2025-12-12T17:30:24.938Z" }, - { url = "https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e", size = 2398191, upload-time = "2025-12-12T17:30:27.343Z" }, - { url = "https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5", size = 4928410, upload-time = "2025-12-12T17:30:29.771Z" }, - { url = "https://files.pythonhosted.org/packages/b0/8d/6fb3494dfe61a46258cd93d979cf4725ded4eb46c2a4ca35e4490d84daea/fonttools-4.61.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd", size = 4984460, upload-time = "2025-12-12T17:30:32.073Z" }, - { url = "https://files.pythonhosted.org/packages/f7/f1/a47f1d30b3dc00d75e7af762652d4cbc3dff5c2697a0dbd5203c81afd9c3/fonttools-4.61.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3", size = 4925800, upload-time = "2025-12-12T17:30:34.339Z" }, - { url = "https://files.pythonhosted.org/packages/a7/01/e6ae64a0981076e8a66906fab01539799546181e32a37a0257b77e4aa88b/fonttools-4.61.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d", size = 5067859, upload-time = "2025-12-12T17:30:36.593Z" }, - { url = "https://files.pythonhosted.org/packages/73/aa/28e40b8d6809a9b5075350a86779163f074d2b617c15d22343fce81918db/fonttools-4.61.1-cp313-cp313-win32.whl", hash = "sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c", size = 2267821, upload-time = "2025-12-12T17:30:38.478Z" }, - { url = "https://files.pythonhosted.org/packages/1a/59/453c06d1d83dc0951b69ef692d6b9f1846680342927df54e9a1ca91c6f90/fonttools-4.61.1-cp313-cp313-win_amd64.whl", hash = "sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b", size = 2318169, upload-time = "2025-12-12T17:30:40.951Z" }, - { url = "https://files.pythonhosted.org/packages/32/8f/4e7bf82c0cbb738d3c2206c920ca34ca74ef9dabde779030145d28665104/fonttools-4.61.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd", size = 2846094, upload-time = "2025-12-12T17:30:43.511Z" }, - { url = "https://files.pythonhosted.org/packages/71/09/d44e45d0a4f3a651f23a1e9d42de43bc643cce2971b19e784cc67d823676/fonttools-4.61.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e", size = 2396589, upload-time = "2025-12-12T17:30:45.681Z" }, - { url = "https://files.pythonhosted.org/packages/89/18/58c64cafcf8eb677a99ef593121f719e6dcbdb7d1c594ae5a10d4997ca8a/fonttools-4.61.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c", size = 4877892, upload-time = "2025-12-12T17:30:47.709Z" }, - { url = "https://files.pythonhosted.org/packages/8a/ec/9e6b38c7ba1e09eb51db849d5450f4c05b7e78481f662c3b79dbde6f3d04/fonttools-4.61.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75", size = 4972884, upload-time = "2025-12-12T17:30:49.656Z" }, - { url = "https://files.pythonhosted.org/packages/5e/87/b5339da8e0256734ba0dbbf5b6cdebb1dd79b01dc8c270989b7bcd465541/fonttools-4.61.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063", size = 4924405, upload-time = "2025-12-12T17:30:51.735Z" }, - { url = "https://files.pythonhosted.org/packages/0b/47/e3409f1e1e69c073a3a6fd8cb886eb18c0bae0ee13db2c8d5e7f8495e8b7/fonttools-4.61.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2", size = 5035553, upload-time = "2025-12-12T17:30:54.823Z" }, - { url = "https://files.pythonhosted.org/packages/bf/b6/1f6600161b1073a984294c6c031e1a56ebf95b6164249eecf30012bb2e38/fonttools-4.61.1-cp314-cp314-win32.whl", hash = "sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c", size = 2271915, upload-time = "2025-12-12T17:30:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/52/7b/91e7b01e37cc8eb0e1f770d08305b3655e4f002fc160fb82b3390eabacf5/fonttools-4.61.1-cp314-cp314-win_amd64.whl", hash = "sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c", size = 2323487, upload-time = "2025-12-12T17:30:59.804Z" }, - { url = "https://files.pythonhosted.org/packages/39/5c/908ad78e46c61c3e3ed70c3b58ff82ab48437faf84ec84f109592cabbd9f/fonttools-4.61.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa", size = 2929571, upload-time = "2025-12-12T17:31:02.574Z" }, - { url = "https://files.pythonhosted.org/packages/bd/41/975804132c6dea64cdbfbaa59f3518a21c137a10cccf962805b301ac6ab2/fonttools-4.61.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91", size = 2435317, upload-time = "2025-12-12T17:31:04.974Z" }, - { url = "https://files.pythonhosted.org/packages/b0/5a/aef2a0a8daf1ebaae4cfd83f84186d4a72ee08fd6a8451289fcd03ffa8a4/fonttools-4.61.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19", size = 4882124, upload-time = "2025-12-12T17:31:07.456Z" }, - { url = "https://files.pythonhosted.org/packages/80/33/d6db3485b645b81cea538c9d1c9219d5805f0877fda18777add4671c5240/fonttools-4.61.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba", size = 5100391, upload-time = "2025-12-12T17:31:09.732Z" }, - { url = "https://files.pythonhosted.org/packages/6c/d6/675ba631454043c75fcf76f0ca5463eac8eb0666ea1d7badae5fea001155/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7", size = 4978800, upload-time = "2025-12-12T17:31:11.681Z" }, - { url = "https://files.pythonhosted.org/packages/7f/33/d3ec753d547a8d2bdaedd390d4a814e8d5b45a093d558f025c6b990b554c/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118", size = 5006426, upload-time = "2025-12-12T17:31:13.764Z" }, - { url = "https://files.pythonhosted.org/packages/b4/40/cc11f378b561a67bea850ab50063366a0d1dd3f6d0a30ce0f874b0ad5664/fonttools-4.61.1-cp314-cp314t-win32.whl", hash = "sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5", size = 2335377, upload-time = "2025-12-12T17:31:16.49Z" }, - { url = "https://files.pythonhosted.org/packages/e4/ff/c9a2b66b39f8628531ea58b320d66d951267c98c6a38684daa8f50fb02f8/fonttools-4.61.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b", size = 2400613, upload-time = "2025-12-12T17:31:18.769Z" }, - { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ff/532ed43808b469c807e8cb6b21358da3fe6fd51486b3a8c93db0bb5d957f/fonttools-4.62.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ad5cca75776cd453b1b035b530e943334957ae152a36a88a320e779d61fc980c", size = 2873740, upload-time = "2026-03-13T13:52:11.822Z" }, + { url = "https://files.pythonhosted.org/packages/85/e4/2318d2b430562da7227010fb2bb029d2fa54d7b46443ae8942bab224e2a0/fonttools-4.62.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b3ae47e8636156a9accff64c02c0924cbebad62854c4a6dbdc110cd5b4b341a", size = 2417649, upload-time = "2026-03-13T13:52:14.605Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/40f15523b5188598018e7956899fed94eb7debec89e2dd70cb4a8df90492/fonttools-4.62.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b9e288b4da2f64fd6180644221749de651703e8d0c16bd4b719533a3a7d6e3", size = 4935213, upload-time = "2026-03-13T13:52:17.399Z" }, + { url = "https://files.pythonhosted.org/packages/42/09/7dbe3d7023f57d9b580cfa832109d521988112fd59dddfda3fddda8218f9/fonttools-4.62.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bca7a1c1faf235ffe25d4f2e555246b4750220b38de8261d94ebc5ce8a23c23", size = 4892374, upload-time = "2026-03-13T13:52:20.175Z" }, + { url = "https://files.pythonhosted.org/packages/d1/2d/84509a2e32cb925371560ef5431365d8da2183c11d98e5b4b8b4e42426a5/fonttools-4.62.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4e0fcf265ad26e487c56cb12a42dffe7162de708762db951e1b3f755319507d", size = 4911856, upload-time = "2026-03-13T13:52:22.777Z" }, + { url = "https://files.pythonhosted.org/packages/a5/80/df28131379eed93d9e6e6fccd3bf6e3d077bebbfe98cc83f21bbcd83ed02/fonttools-4.62.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2d850f66830a27b0d498ee05adb13a3781637b1826982cd7e2b3789ef0cc71ae", size = 5031712, upload-time = "2026-03-13T13:52:25.14Z" }, + { url = "https://files.pythonhosted.org/packages/3d/03/3c8f09aad64230cd6d921ae7a19f9603c36f70930b00459f112706f6769a/fonttools-4.62.1-cp310-cp310-win32.whl", hash = "sha256:486f32c8047ccd05652aba17e4a8819a3a9d78570eb8a0e3b4503142947880ed", size = 1507878, upload-time = "2026-03-13T13:52:28.149Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ec/f53f626f8f3e89f4cadd8fc08f3452c8fd182c951ad5caa35efac22b29ab/fonttools-4.62.1-cp310-cp310-win_amd64.whl", hash = "sha256:5a648bde915fba9da05ae98856987ca91ba832949a9e2888b48c47ef8b96c5a9", size = 1556766, upload-time = "2026-03-13T13:52:30.814Z" }, + { url = "https://files.pythonhosted.org/packages/88/39/23ff32561ec8d45a4d48578b4d241369d9270dc50926c017570e60893701/fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7", size = 2871039, upload-time = "2026-03-13T13:52:33.127Z" }, + { url = "https://files.pythonhosted.org/packages/24/7f/66d3f8a9338a9b67fe6e1739f47e1cd5cee78bd3bc1206ef9b0b982289a5/fonttools-4.62.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14", size = 2416346, upload-time = "2026-03-13T13:52:35.676Z" }, + { url = "https://files.pythonhosted.org/packages/aa/53/5276ceba7bff95da7793a07c5284e1da901cf00341ce5e2f3273056c0cca/fonttools-4.62.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7", size = 5100897, upload-time = "2026-03-13T13:52:38.102Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a1/40a5c4d8e28b0851d53a8eeeb46fbd73c325a2a9a165f290a5ed90e6c597/fonttools-4.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b", size = 5071078, upload-time = "2026-03-13T13:52:41.305Z" }, + { url = "https://files.pythonhosted.org/packages/e3/be/d378fca4c65ea1956fee6d90ace6e861776809cbbc5af22388a090c3c092/fonttools-4.62.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1", size = 5076908, upload-time = "2026-03-13T13:52:44.122Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d9/ae6a1d0693a4185a84605679c8a1f719a55df87b9c6e8e817bfdd9ef5936/fonttools-4.62.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416", size = 5202275, upload-time = "2026-03-13T13:52:46.591Z" }, + { url = "https://files.pythonhosted.org/packages/54/6c/af95d9c4efb15cabff22642b608342f2bd67137eea6107202d91b5b03184/fonttools-4.62.1-cp311-cp311-win32.whl", hash = "sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53", size = 2293075, upload-time = "2026-03-13T13:52:48.711Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/bf54c5b3f2be34e1f143e6db838dfdc54f2ffa3e68c738934c82f3b2a08d/fonttools-4.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2", size = 2344593, upload-time = "2026-03-13T13:52:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974", size = 2870219, upload-time = "2026-03-13T13:52:53.664Z" }, + { url = "https://files.pythonhosted.org/packages/66/9e/a769c8e99b81e5a87ab7e5e7236684de4e96246aae17274e5347d11ebd78/fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9", size = 2414891, upload-time = "2026-03-13T13:52:56.493Z" }, + { url = "https://files.pythonhosted.org/packages/69/64/f19a9e3911968c37e1e620e14dfc5778299e1474f72f4e57c5ec771d9489/fonttools-4.62.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936", size = 5033197, upload-time = "2026-03-13T13:52:59.179Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392", size = 4988768, upload-time = "2026-03-13T13:53:02.761Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/0f904540d3e6ab463c1243a0d803504826a11604c72dd58c2949796a1762/fonttools-4.62.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04", size = 4971512, upload-time = "2026-03-13T13:53:05.678Z" }, + { url = "https://files.pythonhosted.org/packages/29/0b/5cbef6588dc9bd6b5c9ad6a4d5a8ca384d0cea089da31711bbeb4f9654a6/fonttools-4.62.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d", size = 5122723, upload-time = "2026-03-13T13:53:08.662Z" }, + { url = "https://files.pythonhosted.org/packages/4a/47/b3a5342d381595ef439adec67848bed561ab7fdb1019fa522e82101b7d9c/fonttools-4.62.1-cp312-cp312-win32.whl", hash = "sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c", size = 2281278, upload-time = "2026-03-13T13:53:10.998Z" }, + { url = "https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42", size = 2331414, upload-time = "2026-03-13T13:53:13.992Z" }, + { url = "https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79", size = 2865155, upload-time = "2026-03-13T13:53:16.132Z" }, + { url = "https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe", size = 2412802, upload-time = "2026-03-13T13:53:18.878Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/e6ac4b44026de7786fe46e3bfa0c87e51d5d70a841054065d49cd62bb909/fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68", size = 5013926, upload-time = "2026-03-13T13:53:21.379Z" }, + { url = "https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1", size = 4964575, upload-time = "2026-03-13T13:53:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/46/76/7d051671e938b1881670528fec69cc4044315edd71a229c7fd712eaa5119/fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069", size = 4953693, upload-time = "2026-03-13T13:53:26.569Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ae/b41f8628ec0be3c1b934fc12b84f4576a5c646119db4d3bdd76a217c90b5/fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9", size = 5094920, upload-time = "2026-03-13T13:53:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/53a1e9469331a23dcc400970a27a4caa3d9f6edbf5baab0260285238b884/fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24", size = 2279928, upload-time = "2026-03-13T13:53:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056", size = 2330514, upload-time = "2026-03-13T13:53:34.991Z" }, + { url = "https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca", size = 2864442, upload-time = "2026-03-13T13:53:37.509Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b2/e521803081f8dc35990816b82da6360fa668a21b44da4b53fc9e77efcd62/fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca", size = 2410901, upload-time = "2026-03-13T13:53:40.55Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/8c3511ff06e53110039358dbbdc1a65d72157a054638387aa2ada300a8b8/fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782", size = 4999608, upload-time = "2026-03-13T13:53:42.798Z" }, + { url = "https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae", size = 4912726, upload-time = "2026-03-13T13:53:45.405Z" }, + { url = "https://files.pythonhosted.org/packages/70/b9/ac677cb07c24c685cf34f64e140617d58789d67a3dd524164b63648c6114/fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7", size = 4951422, upload-time = "2026-03-13T13:53:48.326Z" }, + { url = "https://files.pythonhosted.org/packages/e6/10/11c08419a14b85b7ca9a9faca321accccc8842dd9e0b1c8a72908de05945/fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a", size = 5060979, upload-time = "2026-03-13T13:53:51.366Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3c/12eea4a4cf054e7ab058ed5ceada43b46809fce2bf319017c4d63ae55bb4/fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800", size = 2283733, upload-time = "2026-03-13T13:53:53.606Z" }, + { url = "https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e", size = 2335663, upload-time = "2026-03-13T13:53:56.23Z" }, + { url = "https://files.pythonhosted.org/packages/42/c5/4d2ed3ca6e33617fc5624467da353337f06e7f637707478903c785bd8e20/fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82", size = 2947288, upload-time = "2026-03-13T13:53:59.397Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e9/7ab11ddfda48ed0f89b13380e5595ba572619c27077be0b2c447a63ff351/fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260", size = 2449023, upload-time = "2026-03-13T13:54:01.642Z" }, + { url = "https://files.pythonhosted.org/packages/b2/10/a800fa090b5e8819942e54e19b55fc7c21fe14a08757c3aa3ca8db358939/fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4", size = 5137599, upload-time = "2026-03-13T13:54:04.495Z" }, + { url = "https://files.pythonhosted.org/packages/37/dc/8ccd45033fffd74deb6912fa1ca524643f584b94c87a16036855b498a1ed/fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b", size = 4920933, upload-time = "2026-03-13T13:54:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/99/eb/e618adefb839598d25ac8136cd577925d6c513dc0d931d93b8af956210f0/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87", size = 5016232, upload-time = "2026-03-13T13:54:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5f/9b5c9bfaa8ec82def8d8168c4f13615990d6ce5996fe52bd49bfb5e05134/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c", size = 5042987, upload-time = "2026-03-13T13:54:13.569Z" }, + { url = "https://files.pythonhosted.org/packages/90/aa/dfbbe24c6a6afc5c203d90cc0343e24bcbb09e76d67c4d6eef8c2558d7ba/fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a", size = 2348021, upload-time = "2026-03-13T13:54:16.98Z" }, + { url = "https://files.pythonhosted.org/packages/13/6f/ae9c4e4dd417948407b680855c2c7790efb52add6009aaecff1e3bc50e8e/fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e", size = 2414147, upload-time = "2026-03-13T13:54:19.416Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, ] [[package]] @@ -1986,16 +2003,15 @@ grpc = [ [[package]] name = "google-auth" -version = "2.48.0" +version = "2.49.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, - { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0c/41/242044323fbd746615884b1c16639749e73665b718209946ebad7ba8a813/google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce", size = 326522, upload-time = "2026-01-26T19:22:47.157Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/80/6a696a07d3d3b0a92488933532f03dbefa4a24ab80fb231395b9a2a1be77/google_auth-2.49.1.tar.gz", hash = "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64", size = 333825, upload-time = "2026-03-12T19:30:58.135Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f", size = 236499, upload-time = "2026-01-26T19:22:45.099Z" }, + { url = "https://files.pythonhosted.org/packages/e9/eb/c6c2478d8a8d633460be40e2a8a6f8f429171997a35a96f81d3b680dec83/google_auth-2.49.1-py3-none-any.whl", hash = "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7", size = 240737, upload-time = "2026-03-12T19:30:53.159Z" }, ] [package.optional-dependencies] @@ -2070,7 +2086,7 @@ wheels = [ [[package]] name = "google-genai" -version = "1.66.0" +version = "1.67.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2084,21 +2100,21 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9b/ba/0b343b0770d4710ad2979fd9301d7caa56c940174d5361ed4a7cc4979241/google_genai-1.66.0.tar.gz", hash = "sha256:ffc01647b65046bca6387320057aa51db0ad64bcc72c8e3e914062acfa5f7c49", size = 504386, upload-time = "2026-03-04T22:15:28.156Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/07/59a498f81f2c7b0649eacda2ea470b7fd8bd7149f20caba22962081bdd51/google_genai-1.67.0.tar.gz", hash = "sha256:897195a6a9742deb6de240b99227189ada8b2d901d61bdfba836c3092021eab6", size = 506972, upload-time = "2026-03-12T20:39:16.241Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/dd/403949d922d4e261b08b64aaa132af4e456c3b15c8e2a2d9e6ef693f66e2/google_genai-1.66.0-py3-none-any.whl", hash = "sha256:7f127a39cf695277104ce4091bb26e417c59bb46e952ff3699c3a982d9c474ee", size = 732174, upload-time = "2026-03-04T22:15:26.63Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/562aa1f086e53529ffbeb5b43d5d8bc42c1b968102b5e2163fad005ce298/google_genai-1.67.0-py3-none-any.whl", hash = "sha256:58b0484ff2d4335fa53c724b489e9f807fcca8115d9cdbd8fdf341121fbd6d2d", size = 733542, upload-time = "2026-03-12T20:39:14.615Z" }, ] [[package]] name = "googleapis-common-protos" -version = "1.72.0" +version = "1.73.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } +sdist = { url = "https://files.pythonhosted.org/packages/99/96/a0205167fa0154f4a542fd6925bdc63d039d88dab3588b875078107e6f06/googleapis_common_protos-1.73.0.tar.gz", hash = "sha256:778d07cd4fbeff84c6f7c72102f0daf98fa2bfd3fa8bea426edc545588da0b5a", size = 147323, upload-time = "2026-03-06T21:53:09.727Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, + { url = "https://files.pythonhosted.org/packages/69/28/23eea8acd65972bbfe295ce3666b28ac510dfcb115fac089d3edb0feb00a/googleapis_common_protos-1.73.0-py3-none-any.whl", hash = "sha256:dfdaaa2e860f242046be561e6d6cb5c5f1541ae02cfbcb034371aadb2942b4e8", size = 297578, upload-time = "2026-03-06T21:52:33.933Z" }, ] [[package]] @@ -2110,6 +2126,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/3f/9859f655d11901e7b2996c6e3d33e0caa9a1d4572c3bc61ed0faa64b2f4c/greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d", size = 277747, upload-time = "2026-02-20T20:16:21.325Z" }, { url = "https://files.pythonhosted.org/packages/fb/07/cb284a8b5c6498dbd7cba35d31380bb123d7dceaa7907f606c8ff5993cbf/greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13", size = 579202, upload-time = "2026-02-20T20:47:28.955Z" }, { url = "https://files.pythonhosted.org/packages/ed/45/67922992b3a152f726163b19f890a85129a992f39607a2a53155de3448b8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e", size = 590620, upload-time = "2026-02-20T20:55:55.581Z" }, + { url = "https://files.pythonhosted.org/packages/03/5f/6e2a7d80c353587751ef3d44bb947f0565ec008a2e0927821c007e96d3a7/greenlet-3.3.2-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508c7f01f1791fbc8e011bd508f6794cb95397fdb198a46cb6635eb5b78d85a7", size = 602132, upload-time = "2026-02-20T21:02:43.261Z" }, { url = "https://files.pythonhosted.org/packages/ad/55/9f1ebb5a825215fadcc0f7d5073f6e79e3007e3282b14b22d6aba7ca6cb8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f", size = 591729, upload-time = "2026-02-20T20:20:58.395Z" }, { url = "https://files.pythonhosted.org/packages/24/b4/21f5455773d37f94b866eb3cf5caed88d6cea6dd2c6e1f9c34f463cba3ec/greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef", size = 1551946, upload-time = "2026-02-20T20:49:31.102Z" }, { url = "https://files.pythonhosted.org/packages/00/68/91f061a926abead128fe1a87f0b453ccf07368666bd59ffa46016627a930/greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca", size = 1618494, upload-time = "2026-02-20T20:21:06.541Z" }, @@ -2117,6 +2134,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890, upload-time = "2026-02-20T20:19:39.263Z" }, { url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120, upload-time = "2026-02-20T20:47:30.161Z" }, { url = "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363, upload-time = "2026-02-20T20:55:56.965Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8b/1430a04657735a3f23116c2e0d5eb10220928846e4537a938a41b350bed6/greenlet-3.3.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2", size = 605046, upload-time = "2026-02-20T21:02:45.234Z" }, { url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156, upload-time = "2026-02-20T20:20:59.955Z" }, { url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649, upload-time = "2026-02-20T20:49:32.293Z" }, { url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472, upload-time = "2026-02-20T20:21:07.966Z" }, @@ -2125,6 +2143,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" }, { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, @@ -2133,6 +2152,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268, upload-time = "2026-02-20T21:02:48.024Z" }, { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, @@ -2141,6 +2161,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ac/85804f74f1ccea31ba518dcc8ee6f14c79f73fe36fa1beba38930806df09/greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9", size = 675371, upload-time = "2026-02-20T21:02:49.664Z" }, { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, @@ -2149,6 +2170,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/d1/67/8197b7e7e602150938049d8e7f30de1660cfb87e4c8ee349b42b67bdb2e1/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", size = 666581, upload-time = "2026-02-20T21:02:51.526Z" }, { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, @@ -2157,7 +2179,7 @@ wheels = [ [[package]] name = "groq" -version = "1.0.0" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2167,9 +2189,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3f/12/f4099a141677fcd2ed79dcc1fcec431e60c52e0e90c9c5d935f0ffaf8c0e/groq-1.0.0.tar.gz", hash = "sha256:66cb7bb729e6eb644daac7ce8efe945e99e4eb33657f733ee6f13059ef0c25a9", size = 146068, upload-time = "2025-12-17T23:34:23.115Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/bc/7ad1d9967c58b21cdec0c94f26f40fc37b07ba60715d6cbc7c7ef775d927/groq-1.1.1.tar.gz", hash = "sha256:ea971eca72d88e875a78567904bfb46a2f2e43907bfe400fc36a81150a4066d8", size = 150783, upload-time = "2026-03-11T09:11:32.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/88/3175759d2ef30406ea721f4d837bfa1ba4339fde3b81ba8c5640a96ed231/groq-1.0.0-py3-none-any.whl", hash = "sha256:6e22bf92ffad988f01d2d4df7729add66b8fd5dbfb2154b5bbf3af245b72c731", size = 138292, upload-time = "2025-12-17T23:34:21.957Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1d/0749c5f0ed76693f6a3a40e2b0c40201fa23e1ccb00e69d5aa63e3f5b0ff/groq-1.1.1-py3-none-any.whl", hash = "sha256:6b7932c0fd3189ad1842fbc294f57fbf014713e01f72037451cb60a138c4b846", size = 139650, upload-time = "2026-03-11T09:11:29.87Z" }, ] [[package]] @@ -2303,34 +2325,34 @@ wheels = [ [[package]] name = "hf-xet" -version = "1.3.2" +version = "1.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8b/cb/9bb543bd987ffa1ee48202cc96a756951b734b79a542335c566148ade36c/hf_xet-1.3.2.tar.gz", hash = "sha256:e130ee08984783d12717444e538587fa2119385e5bd8fc2bb9f930419b73a7af", size = 643646, upload-time = "2026-02-27T17:26:08.051Z" } +sdist = { url = "https://files.pythonhosted.org/packages/09/08/23c84a26716382c89151b5b447b4beb19e3345f3a93d3b73009a71a57ad3/hf_xet-1.4.2.tar.gz", hash = "sha256:b7457b6b482d9e0743bd116363239b1fa904a5e65deede350fbc0c4ea67c71ea", size = 672357, upload-time = "2026-03-13T06:58:51.077Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/75/462285971954269432aad2e7938c5c7ff9ec7d60129cec542ab37121e3d6/hf_xet-1.3.2-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:335a8f36c55fd35a92d0062f4e9201b4015057e62747b7e7001ffb203c0ee1d2", size = 3761019, upload-time = "2026-02-27T17:25:49.441Z" }, - { url = "https://files.pythonhosted.org/packages/35/56/987b0537ddaf88e17192ea09afa8eca853e55f39a4721578be436f8409df/hf_xet-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c1ae4d3a716afc774e66922f3cac8206bfa707db13f6a7e62dfff74bfc95c9a8", size = 3521565, upload-time = "2026-02-27T17:25:47.469Z" }, - { url = "https://files.pythonhosted.org/packages/a8/5c/7e4a33a3d689f77761156cc34558047569e54af92e4d15a8f493229f6767/hf_xet-1.3.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6dbdf231efac0b9b39adcf12a07f0c030498f9212a18e8c50224d0e84ab803d", size = 4176494, upload-time = "2026-02-27T17:25:40.247Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b3/71e856bf9d9a69b3931837e8bf22e095775f268c8edcd4a9e8c355f92484/hf_xet-1.3.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c1980abfb68ecf6c1c7983379ed7b1e2b49a1aaf1a5aca9acc7d48e5e2e0a961", size = 3955601, upload-time = "2026-02-27T17:25:38.376Z" }, - { url = "https://files.pythonhosted.org/packages/63/d7/aecf97b3f0a981600a67ff4db15e2d433389d698a284bb0ea5d8fcdd6f7f/hf_xet-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1c88fbd90ad0d27c46b77a445f0a436ebaa94e14965c581123b68b1c52f5fd30", size = 4154770, upload-time = "2026-02-27T17:25:56.756Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e1/3af961f71a40e09bf5ee909842127b6b00f5ab4ee3817599dc0771b79893/hf_xet-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:35b855024ca37f2dd113ac1c08993e997fbe167b9d61f9ef66d3d4f84015e508", size = 4394161, upload-time = "2026-02-27T17:25:58.111Z" }, - { url = "https://files.pythonhosted.org/packages/a1/c3/859509bade9178e21b8b1db867b8e10e9f817ab9ac1de77cb9f461ced765/hf_xet-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:31612ba0629046e425ba50375685a2586e11fb9144270ebabd75878c3eaf6378", size = 3637377, upload-time = "2026-02-27T17:26:10.611Z" }, - { url = "https://files.pythonhosted.org/packages/05/7f/724cfbef4da92d577b71f68bf832961c8919f36c60d28d289a9fc9d024d4/hf_xet-1.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:433c77c9f4e132b562f37d66c9b22c05b5479f243a1f06a120c1c06ce8b1502a", size = 3497875, upload-time = "2026-02-27T17:26:09.034Z" }, - { url = "https://files.pythonhosted.org/packages/ba/75/9d54c1ae1d05fb704f977eca1671747babf1957f19f38ae75c5933bc2dc1/hf_xet-1.3.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:c34e2c7aefad15792d57067c1c89b2b02c1bbaeabd7f8456ae3d07b4bbaf4094", size = 3761076, upload-time = "2026-02-27T17:25:55.42Z" }, - { url = "https://files.pythonhosted.org/packages/f2/8a/08a24b6c6f52b5d26848c16e4b6d790bb810d1bf62c3505bed179f7032d3/hf_xet-1.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4bc995d6c41992831f762096020dc14a65fdf3963f86ffed580b596d04de32e3", size = 3521745, upload-time = "2026-02-27T17:25:54.217Z" }, - { url = "https://files.pythonhosted.org/packages/b5/db/a75cf400dd8a1a8acf226a12955ff6ee999f272dfc0505bafd8079a61267/hf_xet-1.3.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:959083c89dee30f7d6f890b36cdadda823386c4de63b1a30384a75bfd2ae995d", size = 4176301, upload-time = "2026-02-27T17:25:46.044Z" }, - { url = "https://files.pythonhosted.org/packages/01/40/6c4c798ffdd83e740dd3925c4e47793b07442a9efa3bc3866ba141a82365/hf_xet-1.3.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cfa760888633b08c01b398d212ce7e8c0d7adac6c86e4b20dfb2397d8acd78ee", size = 3955437, upload-time = "2026-02-27T17:25:44.703Z" }, - { url = "https://files.pythonhosted.org/packages/0c/09/9a3aa7c5f07d3e5cc57bb750d12a124ffa72c273a87164bd848f9ac5cc14/hf_xet-1.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3155a02e083aa21fd733a7485c7c36025e49d5975c8d6bda0453d224dd0b0ac4", size = 4154535, upload-time = "2026-02-27T17:26:05.207Z" }, - { url = "https://files.pythonhosted.org/packages/ae/e0/831f7fa6d90cb47a230bc23284b502c700e1483bbe459437b3844cdc0776/hf_xet-1.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:91b1dc03c31cbf733d35dc03df7c5353686233d86af045e716f1e0ea4a2673cf", size = 4393891, upload-time = "2026-02-27T17:26:06.607Z" }, - { url = "https://files.pythonhosted.org/packages/ab/96/6ed472fdce7f8b70f5da6e3f05be76816a610063003bfd6d9cea0bbb58a3/hf_xet-1.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:211f30098512d95e85ad03ae63bd7dd2c4df476558a5095d09f9e38e78cbf674", size = 3637583, upload-time = "2026-02-27T17:26:17.349Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e8/a069edc4570b3f8e123c0b80fadc94530f3d7b01394e1fc1bb223339366c/hf_xet-1.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:4a6817c41de7c48ed9270da0b02849347e089c5ece9a0e72ae4f4b3a57617f82", size = 3497977, upload-time = "2026-02-27T17:26:14.966Z" }, - { url = "https://files.pythonhosted.org/packages/d8/28/dbb024e2e3907f6f3052847ca7d1a2f7a3972fafcd53ff79018977fcb3e4/hf_xet-1.3.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f93b7595f1d8fefddfede775c18b5c9256757824f7f6832930b49858483cd56f", size = 3763961, upload-time = "2026-02-27T17:25:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/e4/71/b99aed3823c9d1795e4865cf437d651097356a3f38c7d5877e4ac544b8e4/hf_xet-1.3.2-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:a85d3d43743174393afe27835bde0cd146e652b5fcfdbcd624602daef2ef3259", size = 3526171, upload-time = "2026-02-27T17:25:50.968Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ca/907890ce6ef5598b5920514f255ed0a65f558f820515b18db75a51b2f878/hf_xet-1.3.2-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7c2a054a97c44e136b1f7f5a78f12b3efffdf2eed3abc6746fc5ea4b39511633", size = 4180750, upload-time = "2026-02-27T17:25:43.125Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ad/bc7f41f87173d51d0bce497b171c4ee0cbde1eed2d7b4216db5d0ada9f50/hf_xet-1.3.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:06b724a361f670ae557836e57801b82c75b534812e351a87a2c739f77d1e0635", size = 3961035, upload-time = "2026-02-27T17:25:41.837Z" }, - { url = "https://files.pythonhosted.org/packages/73/38/600f4dda40c4a33133404d9fe644f1d35ff2d9babb4d0435c646c63dd107/hf_xet-1.3.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:305f5489d7241a47e0458ef49334be02411d1d0f480846363c1c8084ed9916f7", size = 4161378, upload-time = "2026-02-27T17:26:00.365Z" }, - { url = "https://files.pythonhosted.org/packages/00/b3/7bc1ff91d1ac18420b7ad1e169b618b27c00001b96310a89f8a9294fe509/hf_xet-1.3.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:06cdbde243c85f39a63b28e9034321399c507bcd5e7befdd17ed2ccc06dfe14e", size = 4398020, upload-time = "2026-02-27T17:26:03.977Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0b/99bfd948a3ed3620ab709276df3ad3710dcea61976918cce8706502927af/hf_xet-1.3.2-cp37-abi3-win_amd64.whl", hash = "sha256:9298b47cce6037b7045ae41482e703c471ce36b52e73e49f71226d2e8e5685a1", size = 3641624, upload-time = "2026-02-27T17:26:13.542Z" }, - { url = "https://files.pythonhosted.org/packages/cc/02/9a6e4ca1f3f73a164c0cd48e41b3cc56585dcc37e809250de443d673266f/hf_xet-1.3.2-cp37-abi3-win_arm64.whl", hash = "sha256:83d8ec273136171431833a6957e8f3af496bee227a0fe47c7b8b39c106d1749a", size = 3503976, upload-time = "2026-02-27T17:26:12.123Z" }, + { url = "https://files.pythonhosted.org/packages/18/06/e8cf74c3c48e5485c7acc5a990d0d8516cdfb5fdf80f799174f1287cc1b5/hf_xet-1.4.2-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ac8202ae1e664b2c15cdfc7298cbb25e80301ae596d602ef7870099a126fcad4", size = 3796125, upload-time = "2026-03-13T06:58:33.177Z" }, + { url = "https://files.pythonhosted.org/packages/66/d4/b73ebab01cbf60777323b7de9ef05550790451eb5172a220d6b9845385ec/hf_xet-1.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6d2f8ee39fa9fba9af929f8c0d0482f8ee6e209179ad14a909b6ad78ffcb7c81", size = 3555985, upload-time = "2026-03-13T06:58:31.797Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e7/ded6d1bd041c3f2bca9e913a0091adfe32371988e047dd3a68a2463c15a2/hf_xet-1.4.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4642a6cf249c09da8c1f87fe50b24b2a3450b235bf8adb55700b52f0ea6e2eb6", size = 4212085, upload-time = "2026-03-13T06:58:24.323Z" }, + { url = "https://files.pythonhosted.org/packages/97/c1/a0a44d1f98934f7bdf17f7a915b934f9fca44bb826628c553589900f6df8/hf_xet-1.4.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:769431385e746c92dc05492dde6f687d304584b89c33d79def8367ace06cb555", size = 3988266, upload-time = "2026-03-13T06:58:22.887Z" }, + { url = "https://files.pythonhosted.org/packages/7a/82/be713b439060e7d1f1d93543c8053d4ef2fe7e6922c5b31642eaa26f3c4b/hf_xet-1.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c9dd1c1bc4cc56168f81939b0e05b4c36dd2d28c13dc1364b17af89aa0082496", size = 4188513, upload-time = "2026-03-13T06:58:40.858Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/cbd4188b22abd80ebd0edbb2b3e87f2633e958983519980815fb8314eae5/hf_xet-1.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fca58a2ae4e6f6755cc971ac6fcdf777ea9284d7e540e350bb000813b9a3008d", size = 4428287, upload-time = "2026-03-13T06:58:42.601Z" }, + { url = "https://files.pythonhosted.org/packages/b2/4e/84e45b25e2e3e903ed3db68d7eafa96dae9a1d1f6d0e7fc85120347a852f/hf_xet-1.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:163aab46854ccae0ab6a786f8edecbbfbaa38fcaa0184db6feceebf7000c93c0", size = 3665574, upload-time = "2026-03-13T06:58:53.881Z" }, + { url = "https://files.pythonhosted.org/packages/ee/71/c5ac2b9a7ae39c14e91973035286e73911c31980fe44e7b1d03730c00adc/hf_xet-1.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:09b138422ecbe50fd0c84d4da5ff537d27d487d3607183cd10e3e53f05188e82", size = 3528760, upload-time = "2026-03-13T06:58:52.187Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0f/fcd2504015eab26358d8f0f232a1aed6b8d363a011adef83fe130bff88f7/hf_xet-1.4.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:949dcf88b484bb9d9276ca83f6599e4aa03d493c08fc168c124ad10b2e6f75d7", size = 3796493, upload-time = "2026-03-13T06:58:39.267Z" }, + { url = "https://files.pythonhosted.org/packages/82/56/19c25105ff81731ca6d55a188b5de2aa99d7a2644c7aa9de1810d5d3b726/hf_xet-1.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:41659966020d59eb9559c57de2cde8128b706a26a64c60f0531fa2318f409418", size = 3555797, upload-time = "2026-03-13T06:58:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/8933c073186849b5e06762aa89847991d913d10a95d1603eb7f2c3834086/hf_xet-1.4.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c588e21d80010119458dd5d02a69093f0d115d84e3467efe71ffb2c67c19146", size = 4212127, upload-time = "2026-03-13T06:58:30.539Z" }, + { url = "https://files.pythonhosted.org/packages/eb/01/f89ebba4e369b4ed699dcb60d3152753870996f41c6d22d3d7cac01310e1/hf_xet-1.4.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a296744d771a8621ad1d50c098d7ab975d599800dae6d48528ba3944e5001ba0", size = 3987788, upload-time = "2026-03-13T06:58:29.139Z" }, + { url = "https://files.pythonhosted.org/packages/84/4d/8a53e5ffbc2cc33bbf755382ac1552c6d9af13f623ed125fe67cc3e6772f/hf_xet-1.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f563f7efe49588b7d0629d18d36f46d1658fe7e08dce3fa3d6526e1c98315e2d", size = 4188315, upload-time = "2026-03-13T06:58:48.017Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b8/b7a1c1b5592254bd67050632ebbc1b42cc48588bf4757cb03c2ef87e704a/hf_xet-1.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5b2e0132c56d7ee1bf55bdb638c4b62e7106f6ac74f0b786fed499d5548c5570", size = 4428306, upload-time = "2026-03-13T06:58:49.502Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0c/40779e45b20e11c7c5821a94135e0207080d6b3d76e7b78ccb413c6f839b/hf_xet-1.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:2f45c712c2fa1215713db10df6ac84b49d0e1c393465440e9cb1de73ecf7bbf6", size = 3665826, upload-time = "2026-03-13T06:58:59.88Z" }, + { url = "https://files.pythonhosted.org/packages/51/4c/e2688c8ad1760d7c30f7c429c79f35f825932581bc7c9ec811436d2f21a0/hf_xet-1.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6d53df40616f7168abfccff100d232e9d460583b9d86fa4912c24845f192f2b8", size = 3529113, upload-time = "2026-03-13T06:58:58.491Z" }, + { url = "https://files.pythonhosted.org/packages/b4/86/b40b83a2ff03ef05c4478d2672b1fc2b9683ff870e2b25f4f3af240f2e7b/hf_xet-1.4.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:71f02d6e4cdd07f344f6844845d78518cc7186bd2bc52d37c3b73dc26a3b0bc5", size = 3800339, upload-time = "2026-03-13T06:58:36.245Z" }, + { url = "https://files.pythonhosted.org/packages/64/2e/af4475c32b4378b0e92a587adb1aa3ec53e3450fd3e5fe0372a874531c00/hf_xet-1.4.2-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e9b38d876e94d4bdcf650778d6ebbaa791dd28de08db9736c43faff06ede1b5a", size = 3559664, upload-time = "2026-03-13T06:58:34.787Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4c/781267da3188db679e601de18112021a5cb16506fe86b246e22c5401a9c4/hf_xet-1.4.2-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:77e8c180b7ef12d8a96739a4e1e558847002afe9ea63b6f6358b2271a8bdda1c", size = 4217422, upload-time = "2026-03-13T06:58:27.472Z" }, + { url = "https://files.pythonhosted.org/packages/68/47/d6cf4a39ecf6c7705f887a46f6ef5c8455b44ad9eb0d391aa7e8a2ff7fea/hf_xet-1.4.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c3b3c6a882016b94b6c210957502ff7877802d0dbda8ad142c8595db8b944271", size = 3992847, upload-time = "2026-03-13T06:58:25.989Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ef/e80815061abff54697239803948abc665c6b1d237102c174f4f7a9a5ffc5/hf_xet-1.4.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d9a634cc929cfbaf2e1a50c0e532ae8c78fa98618426769480c58501e8c8ac2", size = 4193843, upload-time = "2026-03-13T06:58:44.59Z" }, + { url = "https://files.pythonhosted.org/packages/54/75/07f6aa680575d9646c4167db6407c41340cbe2357f5654c4e72a1b01ca14/hf_xet-1.4.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6b0932eb8b10317ea78b7da6bab172b17be03bbcd7809383d8d5abd6a2233e04", size = 4432751, upload-time = "2026-03-13T06:58:46.533Z" }, + { url = "https://files.pythonhosted.org/packages/cd/71/193eabd7e7d4b903c4aa983a215509c6114915a5a237525ec562baddb868/hf_xet-1.4.2-cp37-abi3-win_amd64.whl", hash = "sha256:ad185719fb2e8ac26f88c8100562dbf9dbdcc3d9d2add00faa94b5f106aea53f", size = 3671149, upload-time = "2026-03-13T06:58:57.07Z" }, + { url = "https://files.pythonhosted.org/packages/b4/7e/ccf239da366b37ba7f0b36095450efae4a64980bdc7ec2f51354205fdf39/hf_xet-1.4.2-cp37-abi3-win_arm64.whl", hash = "sha256:32c012286b581f783653e718c1862aea5b9eb140631685bb0c5e7012c8719a87", size = 3533426, upload-time = "2026-03-13T06:58:55.46Z" }, ] [[package]] @@ -2497,11 +2519,11 @@ wheels = [ [[package]] name = "identify" -version = "2.6.17" +version = "2.6.18" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/84/376a3b96e5a8d33a7aa2c5b3b31a4b3c364117184bf0b17418055f6ace66/identify-2.6.17.tar.gz", hash = "sha256:f816b0b596b204c9fdf076ded172322f2723cf958d02f9c3587504834c8ff04d", size = 99579, upload-time = "2026-03-01T20:04:12.702Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/c4/7fb4db12296cdb11893d61c92048fe617ee853f8523b9b296ac03b43757e/identify-2.6.18.tar.gz", hash = "sha256:873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd", size = 99580, upload-time = "2026-03-15T18:39:50.319Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/66/71c1227dff78aaeb942fed29dd5651f2aec166cc7c9aeea3e8b26a539b7d/identify-2.6.17-py2.py3-none-any.whl", hash = "sha256:be5f8412d5ed4b20f2bd41a65f920990bdccaa6a4a18a08f1eefdcd0bdd885f0", size = 99382, upload-time = "2026-03-01T20:04:11.439Z" }, + { url = "https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl", hash = "sha256:8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737", size = 99394, upload-time = "2026-03-15T18:39:48.915Z" }, ] [[package]] @@ -2838,110 +2860,126 @@ wheels = [ [[package]] name = "kiwisolver" -version = "1.4.9" +version = "1.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/5d/8ce64e36d4e3aac5ca96996457dcf33e34e6051492399a3f1fec5657f30b/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b", size = 124159, upload-time = "2025-08-10T21:25:35.472Z" }, - { url = "https://files.pythonhosted.org/packages/96/1e/22f63ec454874378175a5f435d6ea1363dd33fb2af832c6643e4ccea0dc8/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f", size = 66578, upload-time = "2025-08-10T21:25:36.73Z" }, - { url = "https://files.pythonhosted.org/packages/41/4c/1925dcfff47a02d465121967b95151c82d11027d5ec5242771e580e731bd/kiwisolver-1.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf", size = 65312, upload-time = "2025-08-10T21:25:37.658Z" }, - { url = "https://files.pythonhosted.org/packages/d4/42/0f333164e6307a0687d1eb9ad256215aae2f4bd5d28f4653d6cd319a3ba3/kiwisolver-1.4.9-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9", size = 1628458, upload-time = "2025-08-10T21:25:39.067Z" }, - { url = "https://files.pythonhosted.org/packages/86/b6/2dccb977d651943995a90bfe3495c2ab2ba5cd77093d9f2318a20c9a6f59/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415", size = 1225640, upload-time = "2025-08-10T21:25:40.489Z" }, - { url = "https://files.pythonhosted.org/packages/50/2b/362ebd3eec46c850ccf2bfe3e30f2fc4c008750011f38a850f088c56a1c6/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b", size = 1244074, upload-time = "2025-08-10T21:25:42.221Z" }, - { url = "https://files.pythonhosted.org/packages/6f/bb/f09a1e66dab8984773d13184a10a29fe67125337649d26bdef547024ed6b/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154", size = 1293036, upload-time = "2025-08-10T21:25:43.801Z" }, - { url = "https://files.pythonhosted.org/packages/ea/01/11ecf892f201cafda0f68fa59212edaea93e96c37884b747c181303fccd1/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48", size = 2175310, upload-time = "2025-08-10T21:25:45.045Z" }, - { url = "https://files.pythonhosted.org/packages/7f/5f/bfe11d5b934f500cc004314819ea92427e6e5462706a498c1d4fc052e08f/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220", size = 2270943, upload-time = "2025-08-10T21:25:46.393Z" }, - { url = "https://files.pythonhosted.org/packages/3d/de/259f786bf71f1e03e73d87e2db1a9a3bcab64d7b4fd780167123161630ad/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586", size = 2440488, upload-time = "2025-08-10T21:25:48.074Z" }, - { url = "https://files.pythonhosted.org/packages/1b/76/c989c278faf037c4d3421ec07a5c452cd3e09545d6dae7f87c15f54e4edf/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634", size = 2246787, upload-time = "2025-08-10T21:25:49.442Z" }, - { url = "https://files.pythonhosted.org/packages/a2/55/c2898d84ca440852e560ca9f2a0d28e6e931ac0849b896d77231929900e7/kiwisolver-1.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611", size = 73730, upload-time = "2025-08-10T21:25:51.102Z" }, - { url = "https://files.pythonhosted.org/packages/e8/09/486d6ac523dd33b80b368247f238125d027964cfacb45c654841e88fb2ae/kiwisolver-1.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536", size = 65036, upload-time = "2025-08-10T21:25:52.063Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, - { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, - { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, - { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, - { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, - { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, - { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, - { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, - { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, - { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, - { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, - { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, - { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, - { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, - { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, - { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, - { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, - { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, - { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, - { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, - { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, - { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, - { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, - { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" }, - { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" }, - { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" }, - { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" }, - { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" }, - { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" }, - { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" }, - { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" }, - { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" }, - { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" }, - { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" }, - { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" }, - { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" }, - { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" }, - { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" }, - { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" }, - { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" }, - { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" }, - { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" }, - { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" }, - { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" }, - { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" }, - { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" }, - { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" }, - { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" }, - { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" }, - { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" }, - { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" }, - { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" }, - { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" }, - { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" }, - { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" }, - { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" }, - { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" }, - { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" }, - { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" }, - { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" }, - { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" }, - { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" }, - { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" }, - { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" }, - { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, - { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, - { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, - { url = "https://files.pythonhosted.org/packages/a2/63/fde392691690f55b38d5dd7b3710f5353bf7a8e52de93a22968801ab8978/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527", size = 60183, upload-time = "2025-08-10T21:27:37.669Z" }, - { url = "https://files.pythonhosted.org/packages/27/b1/6aad34edfdb7cced27f371866f211332bba215bfd918ad3322a58f480d8b/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771", size = 58675, upload-time = "2025-08-10T21:27:39.031Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1a/23d855a702bb35a76faed5ae2ba3de57d323f48b1f6b17ee2176c4849463/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e", size = 80277, upload-time = "2025-08-10T21:27:40.129Z" }, - { url = "https://files.pythonhosted.org/packages/5a/5b/5239e3c2b8fb5afa1e8508f721bb77325f740ab6994d963e61b2b7abcc1e/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9", size = 77994, upload-time = "2025-08-10T21:27:41.181Z" }, - { url = "https://files.pythonhosted.org/packages/f9/1c/5d4d468fb16f8410e596ed0eac02d2c68752aa7dc92997fe9d60a7147665/kiwisolver-1.4.9-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb", size = 73744, upload-time = "2025-08-10T21:27:42.254Z" }, - { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, - { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, - { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, - { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f8/06549565caa026e540b7e7bab5c5a90eb7ca986015f4c48dace243cd24d9/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374", size = 122802, upload-time = "2026-03-09T13:12:37.515Z" }, + { url = "https://files.pythonhosted.org/packages/84/eb/8476a0818850c563ff343ea7c9c05dcdcbd689a38e01aa31657df01f91fa/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd", size = 66216, upload-time = "2026-03-09T13:12:38.812Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/f9c8a6b4c21aed4198566e45923512986d6cef530e7263b3a5f823546561/kiwisolver-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476", size = 63917, upload-time = "2026-03-09T13:12:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0e/ba4ae25d03722f64de8b2c13e80d82ab537a06b30fc7065183c6439357e3/kiwisolver-1.5.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22", size = 1628776, upload-time = "2026-03-09T13:12:41.976Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e4/3f43a011bc8a0860d1c96f84d32fa87439d3feedf66e672fef03bf5e8bac/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b", size = 1228164, upload-time = "2026-03-09T13:12:44.002Z" }, + { url = "https://files.pythonhosted.org/packages/4b/34/3a901559a1e0c218404f9a61a93be82d45cb8f44453ba43088644980f033/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e", size = 1246656, upload-time = "2026-03-09T13:12:45.557Z" }, + { url = "https://files.pythonhosted.org/packages/87/9e/f78c466ea20527822b95ad38f141f2de1dcd7f23fb8716b002b0d91bbe59/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb", size = 1295562, upload-time = "2026-03-09T13:12:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/0a/66/fd0e4a612e3a286c24e6d6f3a5428d11258ed1909bc530ba3b59807fd980/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537", size = 2178473, upload-time = "2026-03-09T13:12:50.254Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8e/6cac929e0049539e5ee25c1ee937556f379ba5204840d03008363ced662d/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4", size = 2274035, upload-time = "2026-03-09T13:12:51.785Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d3/9d0c18f1b52ea8074b792452cf17f1f5a56bd0302a85191f405cfbf9da16/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c", size = 2443217, upload-time = "2026-03-09T13:12:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/45/2a/6e19368803a038b2a90857bf4ee9e3c7b667216d045866bf22d3439fd75e/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede", size = 2249196, upload-time = "2026-03-09T13:12:55.057Z" }, + { url = "https://files.pythonhosted.org/packages/75/2b/3f641dfcbe72e222175d626bacf2f72c3b34312afec949dd1c50afa400f5/kiwisolver-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2", size = 73389, upload-time = "2026-03-09T13:12:56.496Z" }, + { url = "https://files.pythonhosted.org/packages/da/88/299b137b9e0025d8982e03d2d52c123b0a2b159e84b0ef1501ef446339cf/kiwisolver-1.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875", size = 64782, upload-time = "2026-03-09T13:12:57.609Z" }, + { url = "https://files.pythonhosted.org/packages/12/dd/a495a9c104be1c476f0386e714252caf2b7eca883915422a64c50b88c6f5/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c", size = 122798, upload-time = "2026-03-09T13:12:58.963Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/37b4047a2af0cf5ef6d8b4b26e91829ae6fc6a2d1f74524bcb0e7cd28a32/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb", size = 66216, upload-time = "2026-03-09T13:13:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac", size = 63911, upload-time = "2026-03-09T13:13:01.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27", size = 1438209, upload-time = "2026-03-09T13:13:03.385Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/76621246f5165e5372f02f5e6f3f48ea336a8f9e96e43997d45b240ed8cd/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398", size = 1248888, upload-time = "2026-03-09T13:13:05.231Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c1/31559ec6fb39a5b48035ce29bb63ade628f321785f38c384dee3e2c08bc1/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db", size = 1266304, upload-time = "2026-03-09T13:13:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ef/1cb8276f2d29cc6a41e0a042f27946ca347d3a4a75acf85d0a16aa6dcc82/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc", size = 1319650, upload-time = "2026-03-09T13:13:08.607Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e4/5ba3cecd7ce6236ae4a80f67e5d5531287337d0e1f076ca87a5abe4cd5d0/kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679", size = 970949, upload-time = "2026-03-09T13:13:10.299Z" }, + { url = "https://files.pythonhosted.org/packages/5a/69/dc61f7ae9a2f071f26004ced87f078235b5507ab6e5acd78f40365655034/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309", size = 2199125, upload-time = "2026-03-09T13:13:11.841Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7b/abbe0f1b5afa85f8d084b73e90e5f801c0939eba16ac2e49af7c61a6c28d/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2", size = 2293783, upload-time = "2026-03-09T13:13:14.399Z" }, + { url = "https://files.pythonhosted.org/packages/8a/80/5908ae149d96d81580d604c7f8aefd0e98f4fd728cf172f477e9f2a81744/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c", size = 1960726, upload-time = "2026-03-09T13:13:16.047Z" }, + { url = "https://files.pythonhosted.org/packages/84/08/a78cb776f8c085b7143142ce479859cfec086bd09ee638a317040b6ef420/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08", size = 2464738, upload-time = "2026-03-09T13:13:17.897Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e1/65584da5356ed6cb12c63791a10b208860ac40a83de165cb6a6751a686e3/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4", size = 2270718, upload-time = "2026-03-09T13:13:19.421Z" }, + { url = "https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b", size = 73480, upload-time = "2026-03-09T13:13:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0e/2ee5debc4f77a625778fec5501ff3e8036fe361b7ee28ae402a485bb9694/kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac", size = 64930, upload-time = "2026-03-09T13:13:21.997Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" }, + { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" }, + { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" }, + { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" }, + { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" }, + { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" }, + { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" }, + { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" }, + { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" }, + { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" }, + { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" }, + { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" }, + { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" }, + { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" }, + { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" }, + { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" }, + { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" }, + { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" }, + { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, + { url = "https://files.pythonhosted.org/packages/17/6f/6fd4f690a40c2582fa34b97d2678f718acf3706b91d270c65ecb455d0a06/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4", size = 59606, upload-time = "2026-03-09T13:15:40.81Z" }, + { url = "https://files.pythonhosted.org/packages/82/a0/2355d5e3b338f13ce63f361abb181e3b6ea5fffdb73f739b3e80efa76159/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca", size = 57537, upload-time = "2026-03-09T13:15:42.071Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b9/1d50e610ecadebe205b71d6728fd224ce0e0ca6aba7b9cbe1da049203ac5/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f", size = 79888, upload-time = "2026-03-09T13:15:43.317Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ee/b85ffcd75afed0357d74f0e6fc02a4507da441165de1ca4760b9f496390d/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed", size = 77584, upload-time = "2026-03-09T13:15:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/6b/dd/644d0dde6010a8583b4cd66dd41c5f83f5325464d15c4f490b3340ab73b4/kiwisolver-1.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc", size = 73390, upload-time = "2026-03-09T13:15:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/e9/eb/5fcbbbf9a0e2c3a35effb88831a483345326bbc3a030a3b5b69aee647f84/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232", size = 59532, upload-time = "2026-03-09T13:15:47.047Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9b/e17104555bb4db148fd52327feea1e96be4b88e8e008b029002c281a21ab/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a", size = 57420, upload-time = "2026-03-09T13:15:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/48/44/2b5b95b7aa39fb2d8d9d956e0f3d5d45aef2ae1d942d4c3ffac2f9cfed1a/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737", size = 79892, upload-time = "2026-03-09T13:15:49.694Z" }, + { url = "https://files.pythonhosted.org/packages/52/7d/7157f9bba6b455cfb4632ed411e199fc8b8977642c2b12082e1bd9e6d173/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16", size = 77603, upload-time = "2026-03-09T13:15:50.945Z" }, + { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" }, ] [[package]] @@ -2951,7 +2989,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "espeakng-loader" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "onnxruntime" }, { name = "phonemizer-fork" }, ] @@ -2962,7 +3000,7 @@ wheels = [ [[package]] name = "langchain" -version = "0.3.27" +version = "0.3.28" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-timeout", marker = "python_full_version < '3.11'" }, @@ -2974,9 +3012,9 @@ dependencies = [ { name = "requests" }, { name = "sqlalchemy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/83/f6/f4f7f3a56626fe07e2bb330feb61254dbdf06c506e6b59a536a337da51cf/langchain-0.3.27.tar.gz", hash = "sha256:aa6f1e6274ff055d0fd36254176770f356ed0a8994297d1df47df341953cec62", size = 10233809, upload-time = "2025-07-24T14:42:32.959Z" } +sdist = { url = "https://files.pythonhosted.org/packages/87/bb/a65e29c8e4aaf0348c2617962e427c8e760d82a67adbd197019e49c7769d/langchain-0.3.28.tar.gz", hash = "sha256:30a32f44cc6690bcc6a6fb7c14d61a15406d5eda1a0e7eab60b3660944888741", size = 10242473, upload-time = "2026-03-06T22:45:17.911Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/d5/4861816a95b2f6993f1360cfb605aacb015506ee2090433a71de9cca8477/langchain-0.3.27-py3-none-any.whl", hash = "sha256:7b20c4f338826acb148d885b20a73a16e410ede9ee4f19bb02011852d5f98798", size = 1018194, upload-time = "2025-07-24T14:42:30.23Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f5/ecd71e5b78e67944b2600a155ef63000bc00148e6794e8e7809b2453887a/langchain-0.3.28-py3-none-any.whl", hash = "sha256:1ba1244477b67b812b775f346209fa596e78bf055a34e45ce22acb7a45842a32", size = 1024717, upload-time = "2026-03-06T22:45:15.545Z" }, ] [[package]] @@ -2991,7 +3029,7 @@ dependencies = [ { name = "langchain-core" }, { name = "langsmith" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pydantic-settings" }, { name = "pyyaml" }, { name = "requests" }, @@ -3050,7 +3088,7 @@ wheels = [ [[package]] name = "langsmith" -version = "0.7.13" +version = "0.7.18" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -3063,9 +3101,9 @@ dependencies = [ { name = "xxhash" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/21/72/89101642611def08758a2b7b82dbfb88e96cb905e1f3a7afb1d22d69ddd1/langsmith-0.7.13.tar.gz", hash = "sha256:9a9223e683158216d158f5a2f2ed6a9a5cf9e40bc66677e8a1402f48f1094013", size = 1112874, upload-time = "2026-03-06T00:13:00.947Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/a8/31e9e7b42089cf194a0d873cbfc1ecec6d6dd0cc693808f1cb64494cbd0c/langsmith-0.7.18.tar.gz", hash = "sha256:d7e6e1f9c9300ee83b9f201c9254b4a32799218de102a5b1d2b217e00be2dfa2", size = 1134635, upload-time = "2026-03-16T18:54:19.131Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/ae/b17097acc75e9f767d36260d84e6be5c3d7366a0476452b9d4f6ac77ffe3/langsmith-0.7.13-py3-none-any.whl", hash = "sha256:0aeba8dff8b02476893ab37108d79af94b268bbaa40505f84fc9a5ebd326550f", size = 347173, upload-time = "2026-03-06T00:12:58.938Z" }, + { url = "https://files.pythonhosted.org/packages/29/58/244a14e29c7feccf06ed3929c9ab65a747a9ee94d5ac43d40862053b2f54/langsmith-0.7.18-py3-none-any.whl", hash = "sha256:3253c171fe2f6506056a42f9077983a34749b7a1629e41d8fb8e2005d8960886", size = 359268, upload-time = "2026-03-16T18:54:17.397Z" }, ] [[package]] @@ -3084,7 +3122,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiofiles" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "protobuf" }, { name = "types-protobuf" }, ] @@ -3296,7 +3334,7 @@ dependencies = [ { name = "fonttools" }, { name = "kiwisolver" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "packaging" }, { name = "pillow" }, { name = "pyparsing" }, @@ -3420,47 +3458,47 @@ wheels = [ [[package]] name = "mlx" -version = "0.31.0" +version = "0.31.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mlx-metal", marker = "sys_platform == 'darwin'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/73/54/269d13847b04b07523d44cf903e1d3c6d48f56e6e89dda7e16418b411629/mlx-0.31.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:38680838e0dd9a621ed4adc5a9ed8b94aeb6a4798142fbe215b821b8c6b8fc36", size = 575395, upload-time = "2026-02-27T23:49:11.886Z" }, - { url = "https://files.pythonhosted.org/packages/3d/86/1fbe1f8f3a23c92c821c235ab7a28395c86c900b0a2b2425f3c8862bbeb6/mlx-0.31.0-cp310-cp310-macosx_15_0_arm64.whl", hash = "sha256:7aded590bcf6839307c3acc899e196936991f97b499ddbdd0cd3b228bf10792f", size = 575394, upload-time = "2026-02-27T23:49:13.738Z" }, - { url = "https://files.pythonhosted.org/packages/20/01/02b79132e91182c779bb6c4f586c5fb86d49c32e8f07f307d2d4ca64cca6/mlx-0.31.0-cp310-cp310-macosx_26_0_arm64.whl", hash = "sha256:6e3ae83607b798b44cb3e44437095cfd26886fecc15f90f29f9eafd206d4d170", size = 575411, upload-time = "2026-02-27T23:49:15.374Z" }, - { url = "https://files.pythonhosted.org/packages/13/86/c501ddb496a185b69f3181d77276907f43a847eaa4d9fff86bc0616d1dcc/mlx-0.31.0-cp310-cp310-manylinux_2_35_aarch64.whl", hash = "sha256:b25f785c94eb47d8104604a5de0e7d749b801e7a40073cbf457aa94c372e5593", size = 639542, upload-time = "2026-02-27T23:49:16.822Z" }, - { url = "https://files.pythonhosted.org/packages/86/7c/508bfc140cf777dbe61fc2be0fbfca56e3f0ceed233cd7a8ef4add84262e/mlx-0.31.0-cp310-cp310-manylinux_2_35_x86_64.whl", hash = "sha256:6a4342027e6608ce69807a8f079c750a7c6161f543ebb49e55654edd03c178d6", size = 672721, upload-time = "2026-02-27T23:49:17.978Z" }, - { url = "https://files.pythonhosted.org/packages/1e/d3/fcb8b9f645ae70b3295a353999c3c6c7a66fd43ed8aa716b13da12bf40d4/mlx-0.31.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:285313eaeba425e58cbb3238c2d1a3894e6252d58f243ce56681d5419a568d6c", size = 575602, upload-time = "2026-02-27T23:49:19.314Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2a/d35072e8dc31d9550f8218cfc388c1cd12c7fd89e8246540a9c7b873d958/mlx-0.31.0-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:acf4f04ff33a80784a0f15c492166dc889e65659b41c410ca5a7c2d78bee2a3a", size = 575603, upload-time = "2026-02-27T23:49:20.651Z" }, - { url = "https://files.pythonhosted.org/packages/43/fa/eca64a514cd50a4a38cc9b8827db85d9e554c3fe407ede043d061055b1ab/mlx-0.31.0-cp311-cp311-macosx_26_0_arm64.whl", hash = "sha256:f624571e23a86654496c42a507b4bb42ded0edb91f33161fabafdbf6b81ba024", size = 575637, upload-time = "2026-02-27T23:49:22.02Z" }, - { url = "https://files.pythonhosted.org/packages/72/cd/0ee01b646010c7a22872d2b849b766941f813c4fd777602306d01af3915f/mlx-0.31.0-cp311-cp311-manylinux_2_35_aarch64.whl", hash = "sha256:5b5306a0934b15c4e3a1088a10066bdde3966c21b95006c63ecc38ca8e3891e0", size = 639267, upload-time = "2026-02-27T23:49:23.265Z" }, - { url = "https://files.pythonhosted.org/packages/73/50/c72e2cabdeefc2bf51ae5c1111bdaa9055a0c2d18bc87314ef965ffff422/mlx-0.31.0-cp311-cp311-manylinux_2_35_x86_64.whl", hash = "sha256:18078bc67dfb7ed602fca233d00ce93e23d590d9347da5009472455a92831066", size = 672858, upload-time = "2026-02-27T23:49:24.627Z" }, - { url = "https://files.pythonhosted.org/packages/1a/7d/87fb0daa006dbbbd8894c3d496c7d9dfc52e4ade260482276d3eca137a15/mlx-0.31.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:de6c0a3e8aa0e7d1365d46634fdbb3f835c164fbdb6ba8a239e039a4efa07fe2", size = 575834, upload-time = "2026-02-27T23:49:26.61Z" }, - { url = "https://files.pythonhosted.org/packages/d4/e3/aa0fac5a9d52b1a4686c7097e56775c1a96dee3084f9c587b74e4c2cd284/mlx-0.31.0-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:d6af01b15177da995336a6fd9878e7c5994720a9f1614d8f4d1dbe9293167c30", size = 575836, upload-time = "2026-02-27T23:49:28.505Z" }, - { url = "https://files.pythonhosted.org/packages/8d/15/6aa3edaa34aeef370634756b7d131b8dc1cdb0002ddecdd3d876b5f9fa0c/mlx-0.31.0-cp312-cp312-macosx_26_0_arm64.whl", hash = "sha256:1ad14ddc3a15818f5bba0de35e88559ed8dcb93ccff2ef879ff604d02d663b25", size = 575828, upload-time = "2026-02-27T23:49:29.684Z" }, - { url = "https://files.pythonhosted.org/packages/6d/d3/53ac650a569f5f5111c0280611acf0dcbdfa5fd0da2d433bad0f5575de73/mlx-0.31.0-cp312-cp312-manylinux_2_35_aarch64.whl", hash = "sha256:a80754ecf64191f71da1946dc5de6cf903344cc90dd286c589792ee9d3fc62f9", size = 624405, upload-time = "2026-02-27T23:49:31.687Z" }, - { url = "https://files.pythonhosted.org/packages/9c/fe/a0c0b73c04f7673a50c505e155dd0088cc7a116d7b8d4eb4d1d9fdcd2c8f/mlx-0.31.0-cp312-cp312-manylinux_2_35_x86_64.whl", hash = "sha256:363282eb094785f6aba27810ff89331c0f7829c6961f571cd0feaad09d2c809f", size = 666952, upload-time = "2026-02-27T23:49:33.262Z" }, - { url = "https://files.pythonhosted.org/packages/4a/09/35d1192cf1f655438213d8baa2264a8bc2426b44d93802dabfc177fd8e81/mlx-0.31.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4f33e9aafc6d3ad29e72743dfb786c4ce67397414f0a091469058626381fc1bc", size = 575815, upload-time = "2026-02-27T23:49:34.607Z" }, - { url = "https://files.pythonhosted.org/packages/59/9d/29e0cb154a31ed05c9d24c776513bf1ec506b8570e214b4563b55bb19ef6/mlx-0.31.0-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:242806b8ad6a4d3ce86cdff513f86520552de7592786712770b2e1ebd178816a", size = 575821, upload-time = "2026-02-27T23:49:35.947Z" }, - { url = "https://files.pythonhosted.org/packages/5f/6c/437aefdca17216aab02d0fb7528cd63e2c3d8d9c1b079c07d579a770645f/mlx-0.31.0-cp313-cp313-macosx_26_0_arm64.whl", hash = "sha256:7f0bdbac084017820ce513a12318771a06c7ec10fad159839e27c998bc5dad89", size = 575810, upload-time = "2026-02-27T23:49:37.165Z" }, - { url = "https://files.pythonhosted.org/packages/a6/d5/986777b53e2c3eff709ee5a275b41ed84a9c04f60071e97f9d3b60dec845/mlx-0.31.0-cp313-cp313-manylinux_2_35_aarch64.whl", hash = "sha256:8642dda2b23195d9921973749ae9bf764e2c7d70bfc0e60b23b6335e660cc610", size = 624713, upload-time = "2026-02-27T23:49:38.672Z" }, - { url = "https://files.pythonhosted.org/packages/2d/29/da0875739d08760461a5b21207c34d959bc7572b27e46ccc0f48badae078/mlx-0.31.0-cp313-cp313-manylinux_2_35_x86_64.whl", hash = "sha256:c6daa671cfa3c194951d742aa09030c5008d9d9657034b2903389fa090b3ba92", size = 666888, upload-time = "2026-02-27T23:49:40.222Z" }, - { url = "https://files.pythonhosted.org/packages/66/60/0152a44ed737c3b16e9044909d01212b99e216c6ab4b2f76faa054ae8172/mlx-0.31.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:cce3e15cf11c608c9e721502fe56e54f9f48b897e9b80f1204a48643d68710c0", size = 577579, upload-time = "2026-02-27T23:49:41.723Z" }, - { url = "https://files.pythonhosted.org/packages/e3/6b/70f0a254d7ace58a030547a99219f1342c3cf383029e1af90eee3efaeb85/mlx-0.31.0-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:ba330fe40d73b202880bbb5cac62de0b639cf4c44a12853bcadb34a9e3ffe880", size = 577582, upload-time = "2026-02-27T23:49:42.998Z" }, - { url = "https://files.pythonhosted.org/packages/63/5a/81cf057dbc005a43d27b7dfaff88198c61bbfe76cb8da3499821083c3fca/mlx-0.31.0-cp314-cp314-macosx_26_0_arm64.whl", hash = "sha256:d2014d113070846c6cdee980653f561c92a4a663a449f64e70c15bbf74d637e1", size = 577535, upload-time = "2026-02-27T23:49:44.475Z" }, - { url = "https://files.pythonhosted.org/packages/75/22/1b2bddb2774c7951aa620d286157439f288186215ff6ce18d9a9a45e608e/mlx-0.31.0-cp314-cp314-manylinux_2_35_aarch64.whl", hash = "sha256:994fab25ff521621e03001177a8f0f1a7bf8294ff340f89910ec074f9f681ed9", size = 627410, upload-time = "2026-02-27T23:49:45.654Z" }, - { url = "https://files.pythonhosted.org/packages/46/f4/e9256326912ac21a9853b3a9856da19292b908270ff96cb27abb8421c8c6/mlx-0.31.0-cp314-cp314-manylinux_2_35_x86_64.whl", hash = "sha256:c3bb9961f40d098659326b0edb96e2a16adecfaf3c1f2518cad5a0b7e55a3a5d", size = 667351, upload-time = "2026-02-27T23:49:46.868Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f9/f1663dafd45af02467f4f41777c13ec34b9104b2b0450d870c3f906285cd/mlx-0.31.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:bc46c911cc060d2eaf21b9e24a1712dc56763b660b53631b9057a32ab1c0271a", size = 574137, upload-time = "2026-03-12T02:15:54.996Z" }, + { url = "https://files.pythonhosted.org/packages/c6/26/1fd632f537a5160a21475a70aaef252090c62f9629f45ad20f5acfe810f3/mlx-0.31.1-cp310-cp310-macosx_15_0_arm64.whl", hash = "sha256:fa132def5b3d959362077521c80f1fc80f64c45060d2940dc1d66a1aa19ce5f6", size = 574140, upload-time = "2026-03-12T02:15:56.709Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c9/e790fa8ddc1b27fea7ba749699883f31c65e166b18e4598beab4574e4686/mlx-0.31.1-cp310-cp310-macosx_26_0_arm64.whl", hash = "sha256:877ff2f98debd035b922825a0d7e7e1be0959fc5ca1d24cb5020a23e510ff16d", size = 574124, upload-time = "2026-03-12T02:15:58.323Z" }, + { url = "https://files.pythonhosted.org/packages/b4/da/f7375fc2be05d026640c5ced085a9e71066a33100638e5762347dae5d680/mlx-0.31.1-cp310-cp310-manylinux_2_35_aarch64.whl", hash = "sha256:931c9316ec47b45ec0e737519f4f4c90eb69cbbdaaecadd6dd2ccdf1a85d4e61", size = 641428, upload-time = "2026-03-12T02:15:59.743Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3f/ab060661d966d435e41212d4f6d6e9d1202da8b9043b1c18c343ab7d1b08/mlx-0.31.1-cp310-cp310-manylinux_2_35_x86_64.whl", hash = "sha256:dec00ce7b094d6bc2876996291fd76c9e28326bc1a9853440903f2a06946ce1f", size = 674521, upload-time = "2026-03-12T02:16:01.057Z" }, + { url = "https://files.pythonhosted.org/packages/75/32/25dc2eae1d6f867224ef2bca2c644e3e913fe8067991f8394c090b720e3e/mlx-0.31.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:8863835fb36c7c4f65008b1426ddb9ff7931a13c975e0ef58a40002ae8048922", size = 574311, upload-time = "2026-03-12T02:16:02.651Z" }, + { url = "https://files.pythonhosted.org/packages/9b/bf/c5aa1d1154f5a216139c8162cd3e6568b7eb427390d655f7f5ae3a1a61e7/mlx-0.31.1-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:0de504c1f1fe73b32fc3cf457b8eac30d1f7ce22440ef075c1970f96712e6fff", size = 574312, upload-time = "2026-03-12T02:16:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/3a/88/ef57747552c9e9da0c28465d9266c05a0009b698d90fb0bc63eb81840b8d/mlx-0.31.1-cp311-cp311-macosx_26_0_arm64.whl", hash = "sha256:10715b895e1f3e984c2c54257b7db956ff8af1fa93255412794a3724fe2dd3b1", size = 574385, upload-time = "2026-03-12T02:16:05.528Z" }, + { url = "https://files.pythonhosted.org/packages/ac/51/dbea4bbe7a2e4cd05226965b34198d49459cfaef8b9b37b72f006a9811ab/mlx-0.31.1-cp311-cp311-manylinux_2_35_aarch64.whl", hash = "sha256:d065625ab3101adcd7f5824297243fe40a0615099a06f5597ab67284483aa2f8", size = 641347, upload-time = "2026-03-12T02:16:07.013Z" }, + { url = "https://files.pythonhosted.org/packages/c5/86/3db98e8805637fb56f078311d622e9500f5c9088f6d79a6e304ec8235b47/mlx-0.31.1-cp311-cp311-manylinux_2_35_x86_64.whl", hash = "sha256:b2cf8502d9d64dc6851034fcd4a656cbb26be20c36f190f2971f4ac0caed89cb", size = 674769, upload-time = "2026-03-12T02:16:08.51Z" }, + { url = "https://files.pythonhosted.org/packages/38/29/71fe1f68756f515856e6930973c23245810d4aa3cd22fddd719d86a709dc/mlx-0.31.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8a63b31a398c9519f2bb0c81cf3865d9baca4ff573ffc31ead465d18286184e8", size = 574308, upload-time = "2026-03-12T02:16:10.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/be/70654a2cee0d71fd10bd237a50a79d06ae51679a194db6a3b16c0c84e6a5/mlx-0.31.1-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:a7a9347df4dcc41f0d16ff70b65650820af4879f686534b233b16826a22afa00", size = 574309, upload-time = "2026-03-12T02:16:11.577Z" }, + { url = "https://files.pythonhosted.org/packages/ad/69/c7bc7b04f76b0cbd678f328011d1634bd0bcfc2da45aba06e084cb031127/mlx-0.31.1-cp312-cp312-macosx_26_0_arm64.whl", hash = "sha256:6cdb797ea31787d1ce9e5be77991c4bd5cbf129ab15f7253b78e09737f535fce", size = 574289, upload-time = "2026-03-12T02:16:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/55/f7/dcc129228faab4d406041d91413c5999250ab79da6fe5417ac84f1616ff1/mlx-0.31.1-cp312-cp312-manylinux_2_35_aarch64.whl", hash = "sha256:1ed1991c8e39f841d5756c0c543beb819763a2f80fba3f4b150bc6cad4d973de", size = 626439, upload-time = "2026-03-12T02:16:14.741Z" }, + { url = "https://files.pythonhosted.org/packages/90/1d/8b32e46ea98ab5c1c15cf1b37ac97af651977f84e72e1800412a700c51d9/mlx-0.31.1-cp312-cp312-manylinux_2_35_x86_64.whl", hash = "sha256:195c5cb27328380287c0ffe9ef48f860ab75ec5d3dfce153d475dc2c99369708", size = 668679, upload-time = "2026-03-12T02:16:16.012Z" }, + { url = "https://files.pythonhosted.org/packages/44/45/04465da443634b23fb11670bbd2f7538b1ed43ffc5e0de44a95b3c29e9c1/mlx-0.31.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9a6d3410fc951bd28508fed9c1ab5d9903f6f6bb101c3a5d63d4191d49a384a1", size = 574268, upload-time = "2026-03-12T02:16:17.27Z" }, + { url = "https://files.pythonhosted.org/packages/85/7b/84956960356ff36e8c1bbed68fac96709e98e6a1adbc8e3d0ff71022d84e/mlx-0.31.1-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:20bd7ba19882603ac22711092d0e799f1ff7b5183c2c641d417dab4d2423d99e", size = 574265, upload-time = "2026-03-12T02:16:18.479Z" }, + { url = "https://files.pythonhosted.org/packages/86/01/d6f0ef5b8c0b390af08246d1301e9717dfb076b3920012b53105a888ed8c/mlx-0.31.1-cp313-cp313-macosx_26_0_arm64.whl", hash = "sha256:4c4565d6f4f8ce295613ee342d313ee5ab0b0eab9a6272954450f8343f7876bc", size = 574172, upload-time = "2026-03-12T02:16:19.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/05/eb29e9eb0cff9c7dfd872e26663e6e9512629730740e1db629086c80ac5a/mlx-0.31.1-cp313-cp313-manylinux_2_35_aarch64.whl", hash = "sha256:9dc564a8b38b9aec279a1c7d34551068b1cc1f8e43b5ac044b56b2a9a4205195", size = 626558, upload-time = "2026-03-12T02:16:21.652Z" }, + { url = "https://files.pythonhosted.org/packages/25/45/ecb746fbb6acb75d03760e41cc7bd21c2e2b544528b3033f7d70402334ac/mlx-0.31.1-cp313-cp313-manylinux_2_35_x86_64.whl", hash = "sha256:78f51ab929278366006ee7793dbb5c942b121542c793c33eb9b894a2ce8e27e1", size = 668625, upload-time = "2026-03-12T02:16:23.103Z" }, + { url = "https://files.pythonhosted.org/packages/99/65/208f511acd5fb1ed0b08f047bd6229583845cc6f4b5aa6547a3219332dbb/mlx-0.31.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:bba9d471ba20e050676292b1089a355c8042d3fc9462e4c1738a9735d7d40cfa", size = 576300, upload-time = "2026-03-12T02:16:24.545Z" }, + { url = "https://files.pythonhosted.org/packages/98/58/2d925cb3fa3cd28d279ed6f44508ab7fbbf7359b17359914aa3652a7d734/mlx-0.31.1-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:d90b0529b22553eb1353b113b7233aa391ca55e24b1ba69024c732fcc21c5c49", size = 576303, upload-time = "2026-03-12T02:16:26.283Z" }, + { url = "https://files.pythonhosted.org/packages/e1/17/abec0bd0f9347dae13e60b33325cb199312798842901953495e19f3bb3c8/mlx-0.31.1-cp314-cp314-macosx_26_0_arm64.whl", hash = "sha256:69bc88b41ddd61b44cd6a4d417790f9971ba3fdf58d824934cea95a95b9b4031", size = 576275, upload-time = "2026-03-12T02:16:27.57Z" }, + { url = "https://files.pythonhosted.org/packages/a2/91/85c73f7cc3a661416d05315623458c719eda7de958b05f4e10ba40c52d07/mlx-0.31.1-cp314-cp314-manylinux_2_35_aarch64.whl", hash = "sha256:b973506fd49ba39df6dc4ff655b77bd35ea193cee878e71d6ee3d1a951d2b3a6", size = 628701, upload-time = "2026-03-12T02:16:28.949Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e9/d87638e00a44dcf346fe838caaf1e2dae96a88d5779edbd66ce27d4bbdcc/mlx-0.31.1-cp314-cp314-manylinux_2_35_x86_64.whl", hash = "sha256:3987282a1e63252bdd7c636138812c67316c3f7c7a7acad08e76c8843648a056", size = 668959, upload-time = "2026-03-12T02:16:30.41Z" }, ] [[package]] name = "mlx-metal" -version = "0.31.0" +version = "0.31.1" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/4f/0a0671dfa62b59bf429edab0e2c9c7f9bc77865aa4218cd46f2f41d7d11a/mlx_metal-0.31.0-py3-none-macosx_14_0_arm64.whl", hash = "sha256:1c572a6e3634a63060c103b0c38ac309e2d217be15519e3d8f0d6b452bb015f5", size = 38596752, upload-time = "2026-02-27T23:29:39.52Z" }, - { url = "https://files.pythonhosted.org/packages/8d/42/c6d7bfd097b777f932d6cf8c79e41b565070b63cc452a069b8804e505140/mlx_metal-0.31.0-py3-none-macosx_15_0_arm64.whl", hash = "sha256:554dc7cb29e0ea5fb6941df42f11a1de385b095848e6183c7a99d7c1f1a11f5d", size = 38595434, upload-time = "2026-02-27T23:29:43.285Z" }, - { url = "https://files.pythonhosted.org/packages/ed/8f/cdaffd759b4c71e74c294e773daacad8aafabac103b93e0aa56d4468d279/mlx_metal-0.31.0-py3-none-macosx_26_0_arm64.whl", hash = "sha256:7fd412f55ddf9f1d90c2cd86ce281d19e8eb93d093c6dbd784a49f8bd7d0a22c", size = 47879607, upload-time = "2026-02-27T23:29:46.571Z" }, + { url = "https://files.pythonhosted.org/packages/39/66/2313497fdbc7fbadf8e026c09366e3f049f9114e65ca4edc23cdb8699186/mlx_metal-0.31.1-py3-none-macosx_14_0_arm64.whl", hash = "sha256:70741174131dbf7fdd479cb730e06e08c358eac3bf7905d9e884e7960cfdd5b8", size = 38624074, upload-time = "2026-03-12T02:15:48.036Z" }, + { url = "https://files.pythonhosted.org/packages/c7/34/4c3c6890ce6095b2ab2ba2f5f15c9a7ba17208d47f8cacb572885a2dc0eb/mlx_metal-0.31.1-py3-none-macosx_15_0_arm64.whl", hash = "sha256:6c56bd8cd27743e635f5a90a22535af7c31bd22b4b126d46b6da2da52d72e413", size = 38618950, upload-time = "2026-03-12T02:15:51.908Z" }, + { url = "https://files.pythonhosted.org/packages/51/bc/987cb99e3aafb296aa11ce5133838a10eae8447edd53168d0804d4fb3a14/mlx_metal-0.31.1-py3-none-macosx_26_0_arm64.whl", hash = "sha256:e7324b7c56b519ae67c025d3ced07e5d35bc3a9f19d4c45fe4927f385148c59e", size = 49256543, upload-time = "2026-03-12T02:15:54.851Z" }, ] [[package]] @@ -3473,7 +3511,7 @@ dependencies = [ { name = "more-itertools" }, { name = "numba" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "tiktoken" }, @@ -3708,7 +3746,7 @@ dependencies = [ { name = "joblib" }, { name = "matplotlib" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "tqdm" }, @@ -3725,7 +3763,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "llvmlite" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/23/c9/a0fb41787d01d621046138da30f6c2100d80857bf34b3390dd68040f27a3/numba-0.64.0.tar.gz", hash = "sha256:95e7300af648baa3308127b1955b52ce6d11889d16e8cfe637b4f85d2fca52b1", size = 2765679, upload-time = "2026-02-18T18:41:20.974Z" } wheels = [ @@ -3818,7 +3856,7 @@ wheels = [ [[package]] name = "numpy" -version = "2.4.2" +version = "2.4.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", @@ -3826,79 +3864,79 @@ resolution-markers = [ "python_full_version == '3.12.*'", "python_full_version == '3.11.*'", ] -sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } +sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478, upload-time = "2026-01-31T23:10:25.623Z" }, - { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467, upload-time = "2026-01-31T23:10:28.186Z" }, - { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172, upload-time = "2026-01-31T23:10:30.848Z" }, - { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145, upload-time = "2026-01-31T23:10:32.352Z" }, - { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084, upload-time = "2026-01-31T23:10:34.502Z" }, - { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477, upload-time = "2026-01-31T23:10:37.075Z" }, - { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429, upload-time = "2026-01-31T23:10:39.704Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109, upload-time = "2026-01-31T23:10:41.924Z" }, - { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915, upload-time = "2026-01-31T23:10:45.26Z" }, - { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972, upload-time = "2026-01-31T23:10:47.021Z" }, - { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763, upload-time = "2026-01-31T23:10:50.087Z" }, - { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, - { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, - { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, - { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, - { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, - { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, - { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, - { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, - { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, - { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, - { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, - { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, - { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" }, - { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" }, - { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" }, - { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" }, - { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" }, - { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" }, - { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" }, - { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" }, - { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" }, - { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" }, - { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" }, - { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" }, - { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" }, - { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" }, - { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" }, - { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" }, - { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" }, - { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" }, - { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" }, - { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" }, - { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" }, - { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" }, - { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" }, - { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" }, - { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" }, - { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" }, - { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" }, - { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" }, - { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" }, - { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" }, - { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" }, - { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" }, - { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" }, - { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179, upload-time = "2026-01-31T23:12:53.5Z" }, - { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755, upload-time = "2026-01-31T23:12:55.933Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500, upload-time = "2026-01-31T23:12:58.671Z" }, - { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252, upload-time = "2026-01-31T23:13:00.518Z" }, - { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142, upload-time = "2026-01-31T23:13:02.219Z" }, - { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979, upload-time = "2026-01-31T23:13:04.62Z" }, - { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" }, + { url = "https://files.pythonhosted.org/packages/f9/51/5093a2df15c4dc19da3f79d1021e891f5dcf1d9d1db6ba38891d5590f3fe/numpy-2.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33b3bf58ee84b172c067f56aeadc7ee9ab6de69c5e800ab5b10295d54c581adb", size = 16957183, upload-time = "2026-03-09T07:55:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/b5/7c/c061f3de0630941073d2598dc271ac2f6cbcf5c83c74a5870fea07488333/numpy-2.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ba7b51e71c05aa1f9bc3641463cd82308eab40ce0d5c7e1fd4038cbf9938147", size = 14968734, upload-time = "2026-03-09T07:56:00.494Z" }, + { url = "https://files.pythonhosted.org/packages/ef/27/d26c85cbcd86b26e4f125b0668e7a7c0542d19dd7d23ee12e87b550e95b5/numpy-2.4.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1988292870c7cb9d0ebb4cc96b4d447513a9644801de54606dc7aabf2b7d920", size = 5475288, upload-time = "2026-03-09T07:56:02.857Z" }, + { url = "https://files.pythonhosted.org/packages/2b/09/3c4abbc1dcd8010bf1a611d174c7aa689fc505585ec806111b4406f6f1b1/numpy-2.4.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:23b46bb6d8ecb68b58c09944483c135ae5f0e9b8d8858ece5e4ead783771d2a9", size = 6805253, upload-time = "2026-03-09T07:56:04.53Z" }, + { url = "https://files.pythonhosted.org/packages/21/bc/e7aa3f6817e40c3f517d407742337cbb8e6fc4b83ce0b55ab780c829243b/numpy-2.4.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a016db5c5dba78fa8fe9f5d80d6708f9c42ab087a739803c0ac83a43d686a470", size = 15969479, upload-time = "2026-03-09T07:56:06.638Z" }, + { url = "https://files.pythonhosted.org/packages/78/51/9f5d7a41f0b51649ddf2f2320595e15e122a40610b233d51928dd6c92353/numpy-2.4.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:715de7f82e192e8cae5a507a347d97ad17598f8e026152ca97233e3666daaa71", size = 16901035, upload-time = "2026-03-09T07:56:09.405Z" }, + { url = "https://files.pythonhosted.org/packages/64/6e/b221dd847d7181bc5ee4857bfb026182ef69499f9305eb1371cbb1aea626/numpy-2.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ddb7919366ee468342b91dea2352824c25b55814a987847b6c52003a7c97f15", size = 17325657, upload-time = "2026-03-09T07:56:12.067Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b8/8f3fd2da596e1063964b758b5e3c970aed1949a05200d7e3d46a9d46d643/numpy-2.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a315e5234d88067f2d97e1f2ef670a7569df445d55400f1e33d117418d008d52", size = 18635512, upload-time = "2026-03-09T07:56:14.629Z" }, + { url = "https://files.pythonhosted.org/packages/5c/24/2993b775c37e39d2f8ab4125b44337ab0b2ba106c100980b7c274a22bee7/numpy-2.4.3-cp311-cp311-win32.whl", hash = "sha256:2b3f8d2c4589b1a2028d2a770b0fc4d1f332fb5e01521f4de3199a896d158ddd", size = 6238100, upload-time = "2026-03-09T07:56:17.243Z" }, + { url = "https://files.pythonhosted.org/packages/76/1d/edccf27adedb754db7c4511d5eac8b83f004ae948fe2d3509e8b78097d4c/numpy-2.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:77e76d932c49a75617c6d13464e41203cd410956614d0a0e999b25e9e8d27eec", size = 12609816, upload-time = "2026-03-09T07:56:19.089Z" }, + { url = "https://files.pythonhosted.org/packages/92/82/190b99153480076c8dce85f4cfe7d53ea84444145ffa54cb58dcd460d66b/numpy-2.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:eb610595dd91560905c132c709412b512135a60f1851ccbd2c959e136431ff67", size = 10485757, upload-time = "2026-03-09T07:56:21.753Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ed/6388632536f9788cea23a3a1b629f25b43eaacd7d7377e5d6bc7b9deb69b/numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef", size = 16669628, upload-time = "2026-03-09T07:56:24.252Z" }, + { url = "https://files.pythonhosted.org/packages/74/1b/ee2abfc68e1ce728b2958b6ba831d65c62e1b13ce3017c13943f8f9b5b2e/numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e", size = 14696872, upload-time = "2026-03-09T07:56:26.991Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d1/780400e915ff5638166f11ca9dc2c5815189f3d7cf6f8759a1685e586413/numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4", size = 5203489, upload-time = "2026-03-09T07:56:29.414Z" }, + { url = "https://files.pythonhosted.org/packages/0b/bb/baffa907e9da4cc34a6e556d6d90e032f6d7a75ea47968ea92b4858826c4/numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18", size = 6550814, upload-time = "2026-03-09T07:56:32.225Z" }, + { url = "https://files.pythonhosted.org/packages/7b/12/8c9f0c6c95f76aeb20fc4a699c33e9f827fa0d0f857747c73bb7b17af945/numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5", size = 15666601, upload-time = "2026-03-09T07:56:34.461Z" }, + { url = "https://files.pythonhosted.org/packages/bd/79/cc665495e4d57d0aa6fbcc0aa57aa82671dfc78fbf95fe733ed86d98f52a/numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97", size = 16621358, upload-time = "2026-03-09T07:56:36.852Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/b4ecb7224af1065c3539f5ecfff879d090de09608ad1008f02c05c770cb3/numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c", size = 17016135, upload-time = "2026-03-09T07:56:39.337Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b1/6a88e888052eed951afed7a142dcdf3b149a030ca59b4c71eef085858e43/numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc", size = 18345816, upload-time = "2026-03-09T07:56:42.31Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8f/103a60c5f8c3d7fc678c19cd7b2476110da689ccb80bc18050efbaeae183/numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9", size = 5960132, upload-time = "2026-03-09T07:56:44.851Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7c/f5ee1bf6ed888494978046a809df2882aad35d414b622893322df7286879/numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5", size = 12316144, upload-time = "2026-03-09T07:56:47.057Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/8d1cb3f7a00f2fb6394140e7e6623696e54c6318a9d9691bb4904672cf42/numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e", size = 10220364, upload-time = "2026-03-09T07:56:49.849Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297, upload-time = "2026-03-09T07:56:52.296Z" }, + { url = "https://files.pythonhosted.org/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853, upload-time = "2026-03-09T07:56:54.992Z" }, + { url = "https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435, upload-time = "2026-03-09T07:56:57.184Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f", size = 6546347, upload-time = "2026-03-09T07:56:59.531Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ec/7971c4e98d86c564750393fab8d7d83d0a9432a9d78bb8a163a6dc59967a/numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f", size = 15664626, upload-time = "2026-03-09T07:57:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc", size = 16608916, upload-time = "2026-03-09T07:57:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/2a2b4a817ffd7472dca4421d9f0776898b364154e30c95f42195041dc03b/numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476", size = 17015824, upload-time = "2026-03-09T07:57:06.347Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ca/627a828d44e78a418c55f82dd4caea8ea4a8ef24e5144d9e71016e52fb40/numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92", size = 18334581, upload-time = "2026-03-09T07:57:09.114Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c0/76f93962fc79955fcba30a429b62304332345f22d4daec1cb33653425643/numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687", size = 5958618, upload-time = "2026-03-09T07:57:11.432Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd", size = 12312824, upload-time = "2026-03-09T07:57:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/58/ce/3d07743aced3d173f877c3ef6a454c2174ba42b584ab0b7e6d99374f51ed/numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d", size = 10221218, upload-time = "2026-03-09T07:57:16.183Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/d96b02a91d09e9d97862f4fc8bfebf5400f567d8eb1fe4b0cc4795679c15/numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875", size = 14819570, upload-time = "2026-03-09T07:57:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ca/0b1aba3905fdfa3373d523b2b15b19029f4f3031c87f4066bd9d20ef6c6b/numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070", size = 5326113, upload-time = "2026-03-09T07:57:21.052Z" }, + { url = "https://files.pythonhosted.org/packages/c0/63/406e0fd32fcaeb94180fd6a4c41e55736d676c54346b7efbce548b94a914/numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73", size = 6646370, upload-time = "2026-03-09T07:57:22.804Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d0/10f7dc157d4b37af92720a196be6f54f889e90dcd30dce9dc657ed92c257/numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368", size = 15723499, upload-time = "2026-03-09T07:57:24.693Z" }, + { url = "https://files.pythonhosted.org/packages/66/f1/d1c2bf1161396629701bc284d958dc1efa3a5a542aab83cf11ee6eb4cba5/numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22", size = 16657164, upload-time = "2026-03-09T07:57:27.676Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/cca19230b740af199ac47331a21c71e7a3d0ba59661350483c1600d28c37/numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a", size = 17081544, upload-time = "2026-03-09T07:57:30.664Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c5/9602b0cbb703a0936fb40f8a95407e8171935b15846de2f0776e08af04c7/numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349", size = 18380290, upload-time = "2026-03-09T07:57:33.763Z" }, + { url = "https://files.pythonhosted.org/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814, upload-time = "2026-03-09T07:57:36.491Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673, upload-time = "2026-03-09T07:57:38.281Z" }, + { url = "https://files.pythonhosted.org/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907, upload-time = "2026-03-09T07:57:40.747Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563, upload-time = "2026-03-09T07:57:43.817Z" }, + { url = "https://files.pythonhosted.org/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161, upload-time = "2026-03-09T07:57:46.169Z" }, + { url = "https://files.pythonhosted.org/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738, upload-time = "2026-03-09T07:57:48.506Z" }, + { url = "https://files.pythonhosted.org/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618, upload-time = "2026-03-09T07:57:50.591Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676, upload-time = "2026-03-09T07:57:52.34Z" }, + { url = "https://files.pythonhosted.org/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492, upload-time = "2026-03-09T07:57:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789, upload-time = "2026-03-09T07:57:57.641Z" }, + { url = "https://files.pythonhosted.org/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941, upload-time = "2026-03-09T07:58:00.577Z" }, + { url = "https://files.pythonhosted.org/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503, upload-time = "2026-03-09T07:58:03.331Z" }, + { url = "https://files.pythonhosted.org/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915, upload-time = "2026-03-09T07:58:06.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875, upload-time = "2026-03-09T07:58:08.734Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225, upload-time = "2026-03-09T07:58:11.034Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769, upload-time = "2026-03-09T07:58:13.67Z" }, + { url = "https://files.pythonhosted.org/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461, upload-time = "2026-03-09T07:58:15.912Z" }, + { url = "https://files.pythonhosted.org/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809, upload-time = "2026-03-09T07:58:17.787Z" }, + { url = "https://files.pythonhosted.org/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242, upload-time = "2026-03-09T07:58:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660, upload-time = "2026-03-09T07:58:23.067Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384, upload-time = "2026-03-09T07:58:25.839Z" }, + { url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" }, + { url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" }, + { url = "https://files.pythonhosted.org/packages/64/e4/4dab9fb43c83719c29241c535d9e07be73bea4bc0c6686c5816d8e1b6689/numpy-2.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c6b124bfcafb9e8d3ed09130dbee44848c20b3e758b6bbf006e641778927c028", size = 16834892, upload-time = "2026-03-09T07:58:35.334Z" }, + { url = "https://files.pythonhosted.org/packages/c9/29/f8b6d4af90fed3dfda84ebc0df06c9833d38880c79ce954e5b661758aa31/numpy-2.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:76dbb9d4e43c16cf9aa711fcd8de1e2eeb27539dcefb60a1d5e9f12fae1d1ed8", size = 14893070, upload-time = "2026-03-09T07:58:37.7Z" }, + { url = "https://files.pythonhosted.org/packages/9a/04/a19b3c91dbec0a49269407f15d5753673a09832daed40c45e8150e6fa558/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:29363fbfa6f8ee855d7569c96ce524845e3d726d6c19b29eceec7dd555dab152", size = 5399609, upload-time = "2026-03-09T07:58:39.853Z" }, + { url = "https://files.pythonhosted.org/packages/79/34/4d73603f5420eab89ea8a67097b31364bf7c30f811d4dd84b1659c7476d9/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:bc71942c789ef415a37f0d4eab90341425a00d538cd0642445d30b41023d3395", size = 6714355, upload-time = "2026-03-09T07:58:42.365Z" }, + { url = "https://files.pythonhosted.org/packages/58/ad/1100d7229bb248394939a12a8074d485b655e8ed44207d328fdd7fcebc7b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e58765ad74dcebd3ef0208a5078fba32dc8ec3578fe84a604432950cd043d79", size = 15800434, upload-time = "2026-03-09T07:58:44.837Z" }, + { url = "https://files.pythonhosted.org/packages/0c/fd/16d710c085d28ba4feaf29ac60c936c9d662e390344f94a6beaa2ac9899b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e236dbda4e1d319d681afcbb136c0c4a8e0f1a5c58ceec2adebb547357fe857", size = 16729409, upload-time = "2026-03-09T07:58:47.972Z" }, + { url = "https://files.pythonhosted.org/packages/57/a7/b35835e278c18b85206834b3aa3abe68e77a98769c59233d1f6300284781/numpy-2.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b42639cdde6d24e732ff823a3fa5b701d8acad89c4142bc1d0bd6dc85200ba5", size = 12504685, upload-time = "2026-03-09T07:58:50.525Z" }, ] [[package]] @@ -4057,7 +4095,7 @@ dependencies = [ { name = "coloredlogs" }, { name = "flatbuffers" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "packaging" }, { name = "protobuf" }, { name = "sympy" }, @@ -4112,7 +4150,7 @@ version = "4.13.0.92" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/fc/6f/5a28fef4c4a382be06afe3938c64cc168223016fa520c5abaf37e8862aa5/opencv_python-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:caf60c071ec391ba51ed00a4a920f996d0b64e3e46068aac1f646b5de0326a19", size = 46247052, upload-time = "2026-02-05T07:01:25.046Z" }, @@ -4510,7 +4548,7 @@ dependencies = [ { name = "nltk" }, { name = "numba" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "onnxruntime" }, { name = "openai" }, { name = "pillow" }, @@ -4751,7 +4789,7 @@ docs = [ { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx-autodoc-typehints", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx-autodoc-typehints", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "sphinx-autodoc-typehints", version = "3.9.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinx-autodoc-typehints", version = "3.9.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx-markdown-builder" }, { name = "sphinx-rtd-theme" }, { name = "toml" }, @@ -4772,7 +4810,7 @@ requires-dist = [ { name = "azure-cognitiveservices-speech", marker = "extra == 'azure'", specifier = ">=1.47.0,<2" }, { name = "camb-sdk", marker = "extra == 'camb'", specifier = ">=1.5.4,<2" }, { name = "coremltools", marker = "extra == 'local-smart-turn'", specifier = ">=8.0" }, - { name = "daily-python", marker = "extra == 'daily'", specifier = "~=0.23.0" }, + { name = "daily-python", marker = "extra == 'daily'", specifier = "~=0.25.0" }, { name = "deepgram-sdk", marker = "extra == 'deepgram'", specifier = ">=6.0.1,<7" }, { name = "docstring-parser", specifier = ">=0.16,<1" }, { name = "einops", marker = "extra == 'moondream'", specifier = "~=0.8.0" }, @@ -4833,14 +4871,14 @@ requires-dist = [ { name = "pipecat-ai", extras = ["websockets-base"], marker = "extra == 'ultravox'" }, { name = "pipecat-ai", extras = ["websockets-base"], marker = "extra == 'websocket'" }, { name = "pipecat-ai-krisp", marker = "extra == 'krisp'", specifier = "~=0.4.0" }, - { name = "pipecat-ai-small-webrtc-prebuilt", marker = "extra == 'runner'", specifier = ">=2.3.0" }, + { name = "pipecat-ai-small-webrtc-prebuilt", marker = "extra == 'runner'", specifier = ">=2.4.0" }, { name = "piper-tts", marker = "extra == 'piper'", specifier = ">=1.3.0,<2" }, { name = "protobuf", specifier = "~=5.29.6" }, { name = "pvkoala", marker = "extra == 'koala'", specifier = "~=2.0.3" }, { name = "pyaudio", marker = "extra == 'local'", specifier = "~=0.2.14" }, { name = "pydantic", specifier = ">=2.10.6,<3" }, { name = "pygobject", marker = "extra == 'gstreamer'", specifier = "~=3.50.0" }, - { name = "pyjwt", marker = "extra == 'livekit'", specifier = ">=2.10.1,<3" }, + { name = "pyjwt", marker = "extra == 'livekit'", specifier = ">=2.12.0,<3" }, { name = "pyloudnorm", specifier = "~=0.2.0" }, { name = "pyrnnoise", marker = "extra == 'rnnoise'", specifier = "~=0.4.1" }, { name = "python-dotenv", marker = "extra == 'runner'", specifier = ">=1.0.0,<2.0.0" }, @@ -4848,7 +4886,7 @@ requires-dist = [ { name = "requests", marker = "extra == 'kokoro'", specifier = ">=2.32.5,<3" }, { name = "requests", marker = "extra == 'piper'", specifier = ">=2.32.5,<3" }, { name = "resampy", specifier = "~=0.4.3" }, - { name = "sarvamai", marker = "extra == 'sarvam'", specifier = "==0.1.26a2" }, + { name = "sarvamai", marker = "extra == 'sarvam'", specifier = "==0.1.26" }, { name = "sentry-sdk", marker = "extra == 'sentry'", specifier = ">=2.28.0,<3" }, { name = "simli-ai", marker = "extra == 'simli'", specifier = "~=2.0.1" }, { name = "soundfile", marker = "extra == 'soundfile'", specifier = "~=0.13.1" }, @@ -4899,20 +4937,20 @@ version = "0.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1d/37/0f1d11d1dc33234a36de01992a9e5adc3c5e1dce71cc87b2bf909fa2f698/pipecat_ai_krisp-0.4.0.tar.gz", hash = "sha256:4f0e05e218dcf15874957e9851299e219c713a0aa8353d2fd811f1b54001a602", size = 13338, upload-time = "2025-06-09T16:13:08.209Z" } [[package]] name = "pipecat-ai-small-webrtc-prebuilt" -version = "2.3.0" +version = "2.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastapi", extra = ["all"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/5f/b0f73bbc6997c22655f0495ce21a4cb176e192df1b5407f66fad8101c697/pipecat_ai_small_webrtc_prebuilt-2.3.0.tar.gz", hash = "sha256:10dc31db9978d68001ae941066fe460c533412a8984df71e5416d4ebeb9c0371", size = 469001, upload-time = "2026-02-25T17:18:43.316Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/02/1e6e90f084ebb1fc954f37661c4614219e4c9fec3d305c8abe5141707b0c/pipecat_ai_small_webrtc_prebuilt-2.4.0.tar.gz", hash = "sha256:c5eddca4e061afb7c5f98cf52ccb85511978a8c834447f6c6d662029e02950c4", size = 472449, upload-time = "2026-03-13T14:17:08.164Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/bc/6193b639a53f4bac1c0fe29b1f8e0d49085c60e457b02a01e725eb7c093f/pipecat_ai_small_webrtc_prebuilt-2.3.0-py3-none-any.whl", hash = "sha256:b3ddaff8bbd56746fe3c58a2d721d3ccc94d17a33c16d78dcbce73d7526c1a05", size = 468881, upload-time = "2026-02-25T17:18:41.869Z" }, + { url = "https://files.pythonhosted.org/packages/25/77/8f6f67142a153943fff31530d51dcf7a2374c39dfa9aba6ef163bf0c622f/pipecat_ai_small_webrtc_prebuilt-2.4.0-py3-none-any.whl", hash = "sha256:9e9a3aa24231b1bf4101a6a2b42c4164a186c0c3d3e49bd51f77280eaa402d12", size = 472792, upload-time = "2026-03-13T14:17:06.556Z" }, ] [[package]] @@ -4963,7 +5001,7 @@ wheels = [ [[package]] name = "posthog" -version = "7.9.7" +version = "7.9.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backoff" }, @@ -4973,9 +5011,9 @@ dependencies = [ { name = "six" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/16/08/e5064ae25749367f38f6d204ce876a045ecf4fd01ed0e66477364925416c/posthog-7.9.7.tar.gz", hash = "sha256:35dcaf4acc37b386b5ebcd6037cc80821e88d359627c0f61537c667c52359483", size = 175634, upload-time = "2026-03-05T22:09:51.979Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/a7/2865487853061fbd62383492237b546d2d8f7c1846272350d2b9e14138cd/posthog-7.9.12.tar.gz", hash = "sha256:ebabf2eb2e1c1fbf22b0759df4644623fa43cc6c9dcbe9fd429b7937d14251ec", size = 176828, upload-time = "2026-03-12T09:01:15.184Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/8a/3e4dd145d7d5aaad856d522c61475c51ee80b512b6446bfb3966b2dedf66/posthog-7.9.7-py3-none-any.whl", hash = "sha256:204e47c27dcc230d0bc9b323709c36f98f86e79fa8190caea3b1fbc3c999b1a0", size = 201316, upload-time = "2026-03-05T22:09:50.18Z" }, + { url = "https://files.pythonhosted.org/packages/65/a9/7a803aed5a5649cf78ea7b31e90d0080181ba21f739243e1741a1e607f1f/posthog-7.9.12-py3-none-any.whl", hash = "sha256:7175bd1698a566bfea98a016c64e3456399f8046aeeca8f1d04ae5bf6c5a38d0", size = 202469, upload-time = "2026-03-12T09:01:13.38Z" }, ] [[package]] @@ -5185,11 +5223,11 @@ wheels = [ [[package]] name = "pyasn1" -version = "0.6.2" +version = "0.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, ] [[package]] @@ -5394,15 +5432,15 @@ wheels = [ [[package]] name = "pydantic-extra-types" -version = "2.11.0" +version = "2.11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/35/2fee58b1316a73e025728583d3b1447218a97e621933fc776fb8c0f2ebdd/pydantic_extra_types-2.11.0.tar.gz", hash = "sha256:4e9991959d045b75feb775683437a97991d02c138e00b59176571db9ce634f0e", size = 157226, upload-time = "2025-12-31T16:18:27.944Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/71/dba38ee2651f84f7842206adbd2233d8bbdb59fb85e9fa14232486a8c471/pydantic_extra_types-2.11.1.tar.gz", hash = "sha256:46792d2307383859e923d8fcefa82108b1a141f8a9c0198982b3832ab5ef1049", size = 172002, upload-time = "2026-03-16T08:08:03.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/17/fabd56da47096d240dd45ba627bead0333b0cf0ee8ada9bec579287dadf3/pydantic_extra_types-2.11.0-py3-none-any.whl", hash = "sha256:84b864d250a0fc62535b7ec591e36f2c5b4d1325fa0017eb8cda9aeb63b374a6", size = 74296, upload-time = "2025-12-31T16:18:26.38Z" }, + { url = "https://files.pythonhosted.org/packages/17/c1/3226e6d7f5a4f736f38ac11a6fbb262d701889802595cdb0f53a885ac2e0/pydantic_extra_types-2.11.1-py3-none-any.whl", hash = "sha256:1722ea2bddae5628ace25f2aa685b69978ef533123e5638cfbddb999e0100ec1", size = 79526, upload-time = "2026-03-16T08:08:02.533Z" }, ] [[package]] @@ -5451,11 +5489,14 @@ sdist = { url = "https://files.pythonhosted.org/packages/a7/5d/f2946cc6c1baf56de [[package]] name = "pyjwt" -version = "2.11.0" +version = "2.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, ] [package.optional-dependencies] @@ -5491,7 +5532,7 @@ version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] @@ -5502,15 +5543,15 @@ wheels = [ [[package]] name = "pyopenssl" -version = "25.3.0" +version = "26.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/80/be/97b83a464498a79103036bc74d1038df4a7ef0e402cfaf4d5e113fb14759/pyopenssl-25.3.0.tar.gz", hash = "sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329", size = 184073, upload-time = "2025-09-17T00:32:21.037Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/11/a62e1d33b373da2b2c2cd9eb508147871c80f12b1cacde3c5d314922afdd/pyopenssl-26.0.0.tar.gz", hash = "sha256:f293934e52936f2e3413b89c6ce36df66a0b34ae1ea3a053b8c5020ff2f513fc", size = 185534, upload-time = "2026-03-15T14:28:26.353Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/81/ef2b1dfd1862567d573a4fdbc9f969067621764fbb74338496840a1d2977/pyopenssl-25.3.0-py3-none-any.whl", hash = "sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6", size = 57268, upload-time = "2025-09-17T00:32:19.474Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7d/d4f7d908fa8415571771b30669251d57c3cf313b36a856e6d7548ae01619/pyopenssl-26.0.0-py3-none-any.whl", hash = "sha256:df94d28498848b98cc1c0ffb8ef1e71e40210d3b0a8064c9d29571ed2904bf81", size = 57969, upload-time = "2026-03-15T14:28:24.864Z" }, ] [[package]] @@ -5562,7 +5603,7 @@ dependencies = [ { name = "click" }, { name = "matplotlib" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "tqdm" }, ] wheels = [ @@ -5632,15 +5673,15 @@ wheels = [ [[package]] name = "python-discovery" -version = "1.1.0" +version = "1.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/82/bb/93a3e83bdf9322c7e21cafd092e56a4a17c4d8ef4277b6eb01af1a540a6f/python_discovery-1.1.0.tar.gz", hash = "sha256:447941ba1aed8cc2ab7ee3cb91be5fc137c5bdbb05b7e6ea62fbdcb66e50b268", size = 55674, upload-time = "2026-02-26T09:42:49.668Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/7e/9f3b0dd3a074a6c3e1e79f35e465b1f2ee4b262d619de00cfce523cc9b24/python_discovery-1.1.3.tar.gz", hash = "sha256:7acca36e818cd88e9b2ba03e045ad7e93e1713e29c6bbfba5d90202310b7baa5", size = 56945, upload-time = "2026-03-10T15:08:15.038Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/54/82a6e2ef37f0f23dccac604b9585bdcbd0698604feb64807dcb72853693e/python_discovery-1.1.0-py3-none-any.whl", hash = "sha256:a162893b8809727f54594a99ad2179d2ede4bf953e12d4c7abc3cc9cdbd1437b", size = 30687, upload-time = "2026-02-26T09:42:48.548Z" }, + { url = "https://files.pythonhosted.org/packages/e7/80/73211fc5bfbfc562369b4aa61dc1e4bf07dc7b34df7b317e4539316b809c/python_discovery-1.1.3-py3-none-any.whl", hash = "sha256:90e795f0121bc84572e737c9aa9966311b9fde44ffb88a5953b3ec9b31c6945e", size = 31485, upload-time = "2026-03-10T15:08:13.06Z" }, ] [[package]] @@ -5793,21 +5834,21 @@ wheels = [ [[package]] name = "qdrant-client" -version = "1.17.0" +version = "1.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "grpcio" }, { name = "httpx", extra = ["http2"] }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "portalocker" }, { name = "protobuf" }, { name = "pydantic" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/fb/c9c4cecf6e7fdff2dbaeee0de40e93fe495379eb5fe2775b184ea45315da/qdrant_client-1.17.0.tar.gz", hash = "sha256:47eb033edb9be33a4babb4d87b0d8d5eaf03d52112dca0218db7f2030bf41ba9", size = 344839, upload-time = "2026-02-19T16:03:17.069Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/dd/f8a8261b83946af3cd65943c93c4f83e044f01184e8525404989d22a81a5/qdrant_client-1.17.1.tar.gz", hash = "sha256:22f990bbd63485ed97ba551a4c498181fcb723f71dcab5d6e4e43fe1050a2bc0", size = 344979, upload-time = "2026-03-13T17:13:44.678Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/15/dfadbc9d8c9872e8ac45fa96f5099bb2855f23426bfea1bbcdc85e64ef6e/qdrant_client-1.17.0-py3-none-any.whl", hash = "sha256:f5b452c68c42b3580d3d266446fb00d3c6e3aae89c916e16585b3c704e108438", size = 390381, upload-time = "2026-02-19T16:03:15.486Z" }, + { url = "https://files.pythonhosted.org/packages/68/69/77d1a971c4b933e8c79403e99bcbb790463da5e48333cc4fd5d412c63c98/qdrant_client-1.17.1-py3-none-any.whl", hash = "sha256:6cda4064adfeaf211c751f3fbc00edbbdb499850918c7aff4855a9a759d56cbd", size = 389947, upload-time = "2026-03-13T17:13:43.156Z" }, ] [[package]] @@ -5992,7 +6033,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numba" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/29/f1/34be702a69a5d272e844c98cee82351f880985cfbca0cc86378011078497/resampy-0.4.3.tar.gz", hash = "sha256:a0d1c28398f0e55994b739650afef4e3974115edbe96cd4bb81968425e916e47", size = 3080604, upload-time = "2024-03-05T20:36:08.119Z" } wheels = [ @@ -6287,41 +6328,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, ] -[[package]] -name = "rsa" -version = "4.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, -] - [[package]] name = "ruff" -version = "0.15.5" +version = "0.15.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/77/9b/840e0039e65fcf12758adf684d2289024d6140cde9268cc59887dc55189c/ruff-0.15.5.tar.gz", hash = "sha256:7c3601d3b6d76dce18c5c824fc8d06f4eef33d6df0c21ec7799510cde0f159a2", size = 4574214, upload-time = "2026-03-05T20:06:34.946Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/df/f8629c19c5318601d3121e230f74cbee7a3732339c52b21daa2b82ef9c7d/ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4", size = 4597916, upload-time = "2026-03-12T23:05:47.51Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/20/5369c3ce21588c708bcbe517a8fbe1a8dfdb5dfd5137e14790b1da71612c/ruff-0.15.5-py3-none-linux_armv6l.whl", hash = "sha256:4ae44c42281f42e3b06b988e442d344a5b9b72450ff3c892e30d11b29a96a57c", size = 10478185, upload-time = "2026-03-05T20:06:29.093Z" }, - { url = "https://files.pythonhosted.org/packages/44/ed/e81dd668547da281e5dce710cf0bc60193f8d3d43833e8241d006720e42b/ruff-0.15.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6edd3792d408ebcf61adabc01822da687579a1a023f297618ac27a5b51ef0080", size = 10859201, upload-time = "2026-03-05T20:06:32.632Z" }, - { url = "https://files.pythonhosted.org/packages/c4/8f/533075f00aaf19b07c5cd6aa6e5d89424b06b3b3f4583bfa9c640a079059/ruff-0.15.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:89f463f7c8205a9f8dea9d658d59eff49db05f88f89cc3047fb1a02d9f344010", size = 10184752, upload-time = "2026-03-05T20:06:40.312Z" }, - { url = "https://files.pythonhosted.org/packages/66/0e/ba49e2c3fa0395b3152bad634c7432f7edfc509c133b8f4529053ff024fb/ruff-0.15.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba786a8295c6574c1116704cf0b9e6563de3432ac888d8f83685654fe528fd65", size = 10534857, upload-time = "2026-03-05T20:06:19.581Z" }, - { url = "https://files.pythonhosted.org/packages/59/71/39234440f27a226475a0659561adb0d784b4d247dfe7f43ffc12dd02e288/ruff-0.15.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd4b801e57955fe9f02b31d20375ab3a5c4415f2e5105b79fb94cf2642c91440", size = 10309120, upload-time = "2026-03-05T20:06:00.435Z" }, - { url = "https://files.pythonhosted.org/packages/f5/87/4140aa86a93df032156982b726f4952aaec4a883bb98cb6ef73c347da253/ruff-0.15.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391f7c73388f3d8c11b794dbbc2959a5b5afe66642c142a6effa90b45f6f5204", size = 11047428, upload-time = "2026-03-05T20:05:51.867Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f7/4953e7e3287676f78fbe85e3a0ca414c5ca81237b7575bdadc00229ac240/ruff-0.15.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dc18f30302e379fe1e998548b0f5e9f4dff907f52f73ad6da419ea9c19d66c8", size = 11914251, upload-time = "2026-03-05T20:06:22.887Z" }, - { url = "https://files.pythonhosted.org/packages/77/46/0f7c865c10cf896ccf5a939c3e84e1cfaeed608ff5249584799a74d33835/ruff-0.15.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc6e7f90087e2d27f98dc34ed1b3ab7c8f0d273cc5431415454e22c0bd2a681", size = 11333801, upload-time = "2026-03-05T20:05:57.168Z" }, - { url = "https://files.pythonhosted.org/packages/d3/01/a10fe54b653061585e655f5286c2662ebddb68831ed3eaebfb0eb08c0a16/ruff-0.15.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1cb7169f53c1ddb06e71a9aebd7e98fc0fea936b39afb36d8e86d36ecc2636a", size = 11206821, upload-time = "2026-03-05T20:06:03.441Z" }, - { url = "https://files.pythonhosted.org/packages/7a/0d/2132ceaf20c5e8699aa83da2706ecb5c5dcdf78b453f77edca7fb70f8a93/ruff-0.15.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9b037924500a31ee17389b5c8c4d88874cc6ea8e42f12e9c61a3d754ff72f1ca", size = 11133326, upload-time = "2026-03-05T20:06:25.655Z" }, - { url = "https://files.pythonhosted.org/packages/72/cb/2e5259a7eb2a0f87c08c0fe5bf5825a1e4b90883a52685524596bfc93072/ruff-0.15.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65bb414e5b4eadd95a8c1e4804f6772bbe8995889f203a01f77ddf2d790929dd", size = 10510820, upload-time = "2026-03-05T20:06:37.79Z" }, - { url = "https://files.pythonhosted.org/packages/ff/20/b67ce78f9e6c59ffbdb5b4503d0090e749b5f2d31b599b554698a80d861c/ruff-0.15.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d20aa469ae3b57033519c559e9bc9cd9e782842e39be05b50e852c7c981fa01d", size = 10302395, upload-time = "2026-03-05T20:05:54.504Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e5/719f1acccd31b720d477751558ed74e9c88134adcc377e5e886af89d3072/ruff-0.15.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:15388dd28c9161cdb8eda68993533acc870aa4e646a0a277aa166de9ad5a8752", size = 10754069, upload-time = "2026-03-05T20:06:06.422Z" }, - { url = "https://files.pythonhosted.org/packages/c3/9c/d1db14469e32d98f3ca27079dbd30b7b44dbb5317d06ab36718dee3baf03/ruff-0.15.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b30da330cbd03bed0c21420b6b953158f60c74c54c5f4c1dabbdf3a57bf355d2", size = 11304315, upload-time = "2026-03-05T20:06:10.867Z" }, - { url = "https://files.pythonhosted.org/packages/28/3a/950367aee7c69027f4f422059227b290ed780366b6aecee5de5039d50fa8/ruff-0.15.5-py3-none-win32.whl", hash = "sha256:732e5ee1f98ba5b3679029989a06ca39a950cced52143a0ea82a2102cb592b74", size = 10551676, upload-time = "2026-03-05T20:06:13.705Z" }, - { url = "https://files.pythonhosted.org/packages/b8/00/bf077a505b4e649bdd3c47ff8ec967735ce2544c8e4a43aba42ee9bf935d/ruff-0.15.5-py3-none-win_amd64.whl", hash = "sha256:821d41c5fa9e19117616c35eaa3f4b75046ec76c65e7ae20a333e9a8696bc7fe", size = 11678972, upload-time = "2026-03-05T20:06:45.379Z" }, - { url = "https://files.pythonhosted.org/packages/fe/4e/cd76eca6db6115604b7626668e891c9dd03330384082e33662fb0f113614/ruff-0.15.5-py3-none-win_arm64.whl", hash = "sha256:b498d1c60d2fe5c10c45ec3f698901065772730b411f164ae270bb6bfcc4740b", size = 10965572, upload-time = "2026-03-05T20:06:16.984Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2f/4e03a7e5ce99b517e98d3b4951f411de2b0fa8348d39cf446671adcce9a2/ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff", size = 10508953, upload-time = "2026-03-12T23:05:17.246Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/55bcdc3e9f80bcf39edf0cd272da6fa511a3d94d5a0dd9e0adf76ceebdb4/ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3", size = 10942257, upload-time = "2026-03-12T23:05:23.076Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/005c29bd1726c0f492bfa215e95154cf480574140cb5f867c797c18c790b/ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb", size = 10322683, upload-time = "2026-03-12T23:05:33.738Z" }, + { url = "https://files.pythonhosted.org/packages/5f/74/2f861f5fd7cbb2146bddb5501450300ce41562da36d21868c69b7a828169/ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8", size = 10660986, upload-time = "2026-03-12T23:05:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a1/309f2364a424eccb763cdafc49df843c282609f47fe53aa83f38272389e0/ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e", size = 10332177, upload-time = "2026-03-12T23:05:56.145Z" }, + { url = "https://files.pythonhosted.org/packages/30/41/7ebf1d32658b4bab20f8ac80972fb19cd4e2c6b78552be263a680edc55ac/ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15", size = 11170783, upload-time = "2026-03-12T23:06:01.742Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/6d488f6adca047df82cd62c304638bcb00821c36bd4881cfca221561fdfc/ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9", size = 12044201, upload-time = "2026-03-12T23:05:28.697Z" }, + { url = "https://files.pythonhosted.org/packages/71/68/e6f125df4af7e6d0b498f8d373274794bc5156b324e8ab4bf5c1b4fc0ec7/ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab", size = 11421561, upload-time = "2026-03-12T23:05:31.236Z" }, + { url = "https://files.pythonhosted.org/packages/f1/9f/f85ef5fd01a52e0b472b26dc1b4bd228b8f6f0435975442ffa4741278703/ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e", size = 11310928, upload-time = "2026-03-12T23:05:45.288Z" }, + { url = "https://files.pythonhosted.org/packages/8c/26/b75f8c421f5654304b89471ed384ae8c7f42b4dff58fa6ce1626d7f2b59a/ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c", size = 11235186, upload-time = "2026-03-12T23:05:50.677Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d4/d5a6d065962ff7a68a86c9b4f5500f7d101a0792078de636526c0edd40da/ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512", size = 10635231, upload-time = "2026-03-12T23:05:37.044Z" }, + { url = "https://files.pythonhosted.org/packages/d6/56/7c3acf3d50910375349016cf33de24be021532042afbed87942858992491/ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0", size = 10340357, upload-time = "2026-03-12T23:06:04.748Z" }, + { url = "https://files.pythonhosted.org/packages/06/54/6faa39e9c1033ff6a3b6e76b5df536931cd30caf64988e112bbf91ef5ce5/ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb", size = 10860583, upload-time = "2026-03-12T23:05:58.978Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/509a201b843b4dfb0b32acdedf68d951d3377988cae43949ba4c4133a96a/ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0", size = 11410976, upload-time = "2026-03-12T23:05:39.955Z" }, + { url = "https://files.pythonhosted.org/packages/6c/25/3fc9114abf979a41673ce877c08016f8e660ad6cf508c3957f537d2e9fa9/ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c", size = 10616872, upload-time = "2026-03-12T23:05:42.451Z" }, + { url = "https://files.pythonhosted.org/packages/89/7a/09ece68445ceac348df06e08bf75db72d0e8427765b96c9c0ffabc1be1d9/ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406", size = 11787271, upload-time = "2026-03-12T23:05:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d0/578c47dd68152ddddddf31cd7fc67dc30b7cdf639a86275fda821b0d9d98/ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837", size = 11060497, upload-time = "2026-03-12T23:05:25.968Z" }, ] [[package]] @@ -6364,7 +6393,7 @@ wheels = [ [[package]] name = "sarvamai" -version = "0.1.26a2" +version = "0.1.26" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -6373,9 +6402,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7a/6c/80ab26743586532a3e9d68385549b0992e5318b5499db815889c8527cce5/sarvamai-0.1.26a2.tar.gz", hash = "sha256:0cbd1a95d13c1f8f0d1bf8fbeb37e86d3c2dc75a7ac402743bf0e571378f79e4", size = 112445, upload-time = "2026-02-16T13:16:28.392Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/31/13f65e8533b667514e1cfe838d12a14494cbc5943fd8f0c101305127459b/sarvamai-0.1.26.tar.gz", hash = "sha256:d51a213c27feb33d65f5b71e4882dcdb873dc5e0d720390b7ba18d1bdeec2471", size = 113050, upload-time = "2026-03-06T16:40:36.647Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/3e/76c8ea81e790a5dab2ec9cd9fdb02cd600f90b1dfd9c895bea2fb5e6aa7f/sarvamai-0.1.26a2-py3-none-any.whl", hash = "sha256:2b0549a18e093ea382725240035a0bea18fff2a0d5207ad6c95ff7189e03264a", size = 227413, upload-time = "2026-02-16T13:16:27.045Z" }, + { url = "https://files.pythonhosted.org/packages/76/c9/c03a807ace9cafbfe26418be995e4959142a55313c9f26564586e111f31d/sarvamai-0.1.26-py3-none-any.whl", hash = "sha256:39e79ba0932f4501a2aa28f84fd2de64d34fc9a7af2b0d4ead1efa617517b3bd", size = 229057, upload-time = "2026-03-06T16:40:35.584Z" }, ] [[package]] @@ -6448,7 +6477,7 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -6516,15 +6545,15 @@ wheels = [ [[package]] name = "segments" -version = "2.3.0" +version = "2.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "csvw" }, { name = "regex" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9b/4c/25e499df952528004ff3f7f8e1e63d20773ed30141ed17c285adb5446f55/segments-2.3.0.tar.gz", hash = "sha256:381143f66f59eaf45398f5bb57f899d6501be011048ec5f92754c9b24b181615", size = 18193, upload-time = "2025-02-20T07:55:42.273Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/57/85cac3a8e32370e88fa5fa92812edb6025db7fcbed51452bd56ee1524957/segments-2.4.0.tar.gz", hash = "sha256:bba71f5520ddd54c8aa2f4d765a60618c6862162d6e7356a4a097f2223166f5b", size = 18662, upload-time = "2026-03-07T10:01:28.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/18/cb614939ccd46d336013cab705f1e11540ec9c68b08ecbb854ab893fc480/segments-2.3.0-py2.py3-none-any.whl", hash = "sha256:30a5656787071430cd22422e04713b2a9beabe1a97d2ebf37f716a56f90577a3", size = 15705, upload-time = "2025-02-20T07:55:39.755Z" }, + { url = "https://files.pythonhosted.org/packages/be/60/eef9acce946177f92c9aabf432224d87ab908bafafac516a36ab924199f3/segments-2.4.0-py2.py3-none-any.whl", hash = "sha256:4021dc67f201cc03c864c74c618bdb163b1af629da3040babbaa37d8813f3db0", size = 16321, upload-time = "2026-03-07T10:01:27.885Z" }, ] [[package]] @@ -6581,7 +6610,7 @@ dependencies = [ { name = "av" }, { name = "httpx" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "websockets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/aa/8c/fe0697cd371a0f203b915f59e376e1807e4ad79bd53e20ceea57a161f242/simli_ai-2.0.2.tar.gz", hash = "sha256:53b99901fe4c5eeb7637492f70dde34c131ee9e5589bf8781a75494c0469ca03", size = 16422, upload-time = "2026-02-25T11:13:16.854Z" } @@ -6708,7 +6737,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e1/41/9b873a8c055582859b239be17902a85339bec6a30ad162f98c9b0288a2cc/soundfile-0.13.1.tar.gz", hash = "sha256:b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b", size = 46156, upload-time = "2025-01-25T09:17:04.831Z" } wheels = [ @@ -6727,7 +6756,7 @@ version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/7e/f4b461944662ad75036df65277d6130f9411002bfb79e9df7dff40a31db9/soxr-1.0.0.tar.gz", hash = "sha256:e07ee6c1d659bc6957034f4800c60cb8b98de798823e34d2a2bba1caa85a4509", size = 171415, upload-time = "2025-09-07T13:22:21.317Z" } wheels = [ @@ -6771,7 +6800,7 @@ version = "0.2.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pydantic" }, { name = "speechmatics-rt" }, ] @@ -6914,7 +6943,7 @@ wheels = [ [[package]] name = "sphinx-autodoc-typehints" -version = "3.9.7" +version = "3.9.8" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", @@ -6924,14 +6953,14 @@ resolution-markers = [ dependencies = [ { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/06/da2d9e98b3f7f0df144496e62f453e0025f129bccc7a6076b8ceae6047b1/sphinx_autodoc_typehints-3.9.7.tar.gz", hash = "sha256:70f3dd4e4dd815ae30e5d3848a26dca71fb5e7fcf8f37cf8b840dc8afdf07e82", size = 68689, upload-time = "2026-03-05T18:33:40.829Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/4d/02216afa475c838c123b41f30e3a2875ad27c14fae3f5bbed4ba4e7fd894/sphinx_autodoc_typehints-3.9.8.tar.gz", hash = "sha256:1e36b31ee593b7e838988045918b7fa965f5062abbd6800af96d5e2c3f17130e", size = 68763, upload-time = "2026-03-09T15:40:01.02Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/a0/e7d3365dabfa79a1b2ac7d3122b5b22b401a9c4d5e4eadc5e13b88c63a2c/sphinx_autodoc_typehints-3.9.7-py3-none-any.whl", hash = "sha256:dd73f6a32adef0d8208f6f7d99254e1880259c77db7b4a91648345d45202d48e", size = 36691, upload-time = "2026-03-05T18:33:38.983Z" }, + { url = "https://files.pythonhosted.org/packages/a8/6c/f275f59095b2fec6627c3ce2caba4e18f55a3925718cf0547cde04821a37/sphinx_autodoc_typehints-3.9.8-py3-none-any.whl", hash = "sha256:df123ec82479934fed27e31d4ccdcf382901c5d9481450fc224054496e574466", size = 36685, upload-time = "2026-03-09T15:39:59.567Z" }, ] [[package]] name = "sphinx-markdown-builder" -version = "0.6.9" +version = "0.6.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -6941,9 +6970,9 @@ dependencies = [ { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "tabulate" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d2/f6/7566ba54c8b9744192bdf19ba01e62e1bb6cb1e8526447cdb29feb7cac7c/sphinx_markdown_builder-0.6.9.tar.gz", hash = "sha256:e89dc1b9eb837da430c2c230011fad95a3dfab0345ad503a32e35a31d284a722", size = 22707, upload-time = "2025-12-07T14:36:14.088Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/58/0b7b9a7d071140b3705885d51932e8b62f520388c2772e4952189971727b/sphinx_markdown_builder-0.6.10.tar.gz", hash = "sha256:cd5acf88d52ea0146a712fd557404f10326dff3428a78ba928e59b1727fd4a86", size = 22688, upload-time = "2026-03-11T10:56:57.639Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/ee/02f9986d7818be2ccc5bce76d388e73f5a163f604f682d1ad69e6bc0df7c/sphinx_markdown_builder-0.6.9-py3-none-any.whl", hash = "sha256:35b555760c48d4a38fe4b27813cb5ca636bbd22d8ef0742ac6959043f8000840", size = 16717, upload-time = "2025-12-07T14:36:12.646Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8f/9fecf3d081d5cd49eff83a17b9fef50ed741e6223ab3bb906de4ab0068f9/sphinx_markdown_builder-0.6.10-py3-none-any.whl", hash = "sha256:16d86738b9ac69fcbc86e373c31c6402c30af1fa8d98d0f62cc5f38bfe5fc26e", size = 16700, upload-time = "2026-03-11T10:56:56.135Z" }, ] [[package]] @@ -7119,7 +7148,7 @@ wheels = [ [[package]] name = "strands-agents" -version = "1.29.0" +version = "1.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "boto3" }, @@ -7131,12 +7160,13 @@ dependencies = [ { name = "opentelemetry-instrumentation-threading" }, { name = "opentelemetry-sdk" }, { name = "pydantic" }, + { name = "pyyaml" }, { name = "typing-extensions" }, { name = "watchdog" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/51/16241b671a7b8c1970775ee24a50ca8a1a3a319f0652a3b7336989baa245/strands_agents-1.29.0.tar.gz", hash = "sha256:2d07dbbd5af552460f43c764c5a34cc34d90638578ec420999b5dce683e431d9", size = 737805, upload-time = "2026-03-04T21:24:48.911Z" } +sdist = { url = "https://files.pythonhosted.org/packages/11/82/6c193a8ea19ed91a368a4cf7d20c87457793e1286dac5811a5c2a60a5cc2/strands_agents-1.30.0.tar.gz", hash = "sha256:358db9d78304fc1fe324763be545243e3f9cb030ed0f6f51d0c91d37caff7746", size = 773031, upload-time = "2026-03-11T18:38:32.257Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/19/468605511e596f838455e6a16ccf380c2d57c4b21f5658cf740b01917753/strands_agents-1.29.0-py3-none-any.whl", hash = "sha256:9cab6ce14292c450d2f0996a06f647754d772c9f1431170963921fe8f5eaaff8", size = 366508, upload-time = "2026-03-04T21:24:47.184Z" }, + { url = "https://files.pythonhosted.org/packages/6e/94/ecc2df8100fdf745d41d10ac2de4c9cb0325384d0e28b4bb90c82a6ec63b/strands_agents-1.30.0-py3-none-any.whl", hash = "sha256:457ba7b063df61d00f122c913b6b85ba6431d17741b9e34484a7e16fb7e00430", size = 386493, upload-time = "2026-03-11T18:38:30.503Z" }, ] [[package]] @@ -7384,6 +7414,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0f/8b/4b61d6e13f7108f36910df9ab4b58fd389cc2520d54d81b88660804aad99/torch-2.10.0-2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:418997cb02d0a0f1497cf6a09f63166f9f5df9f3e16c8a716ab76a72127c714f", size = 79423467, upload-time = "2026-02-10T21:44:48.711Z" }, { url = "https://files.pythonhosted.org/packages/d3/54/a2ba279afcca44bbd320d4e73675b282fcee3d81400ea1b53934efca6462/torch-2.10.0-2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:13ec4add8c3faaed8d13e0574f5cd4a323c11655546f91fbe6afa77b57423574", size = 79498202, upload-time = "2026-02-10T21:44:52.603Z" }, { url = "https://files.pythonhosted.org/packages/ec/23/2c9fe0c9c27f7f6cb865abcea8a4568f29f00acaeadfc6a37f6801f84cb4/torch-2.10.0-2-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:e521c9f030a3774ed770a9c011751fb47c4d12029a3d6522116e48431f2ff89e", size = 79498254, upload-time = "2026-02-10T21:44:44.095Z" }, + { url = "https://files.pythonhosted.org/packages/16/ee/efbd56687be60ef9af0c9c0ebe106964c07400eade5b0af8902a1d8cd58c/torch-2.10.0-3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a1ff626b884f8c4e897c4c33782bdacdff842a165fee79817b1dd549fdda1321", size = 915510070, upload-time = "2026-03-11T14:16:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/36/ab/7b562f1808d3f65414cd80a4f7d4bb00979d9355616c034c171249e1a303/torch-2.10.0-3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ac5bdcbb074384c66fa160c15b1ead77839e3fe7ed117d667249afce0acabfac", size = 915518691, upload-time = "2026-03-11T14:15:43.147Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7a/abada41517ce0011775f0f4eacc79659bc9bc6c361e6bfe6f7052a6b9363/torch-2.10.0-3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:98c01b8bb5e3240426dcde1446eed6f40c778091c8544767ef1168fc663a05a6", size = 915622781, upload-time = "2026-03-11T14:17:11.354Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c6/4dfe238342ffdcec5aef1c96c457548762d33c40b45a1ab7033bb26d2ff2/torch-2.10.0-3-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:80b1b5bfe38eb0e9f5ff09f206dcac0a87aadd084230d4a36eea5ec5232c115b", size = 915627275, upload-time = "2026-03-11T14:16:11.325Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/72bf18847f58f877a6a8acf60614b14935e2f156d942483af1ffc081aea0/torch-2.10.0-3-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:46b3574d93a2a8134b3f5475cfb98e2eb46771794c57015f6ad1fb795ec25e49", size = 915523474, upload-time = "2026-03-11T14:17:44.422Z" }, + { url = "https://files.pythonhosted.org/packages/f4/39/590742415c3030551944edc2ddc273ea1fdfe8ffb2780992e824f1ebee98/torch-2.10.0-3-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b1d5e2aba4eb7f8e87fbe04f86442887f9167a35f092afe4c237dfcaaef6e328", size = 915632474, upload-time = "2026-03-11T14:15:13.666Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8e/34949484f764dde5b222b7fe3fede43e4a6f0da9d7f8c370bb617d629ee2/torch-2.10.0-3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:0228d20b06701c05a8f978357f657817a4a63984b0c90745def81c18aedfa591", size = 915523882, upload-time = "2026-03-11T14:14:46.311Z" }, { url = "https://files.pythonhosted.org/packages/0c/1a/c61f36cfd446170ec27b3a4984f072fd06dab6b5d7ce27e11adb35d6c838/torch-2.10.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:5276fa790a666ee8becaffff8acb711922252521b28fbce5db7db5cf9cb2026d", size = 145992962, upload-time = "2026-01-21T16:24:14.04Z" }, { url = "https://files.pythonhosted.org/packages/b5/60/6662535354191e2d1555296045b63e4279e5a9dbad49acf55a5d38655a39/torch-2.10.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:aaf663927bcd490ae971469a624c322202a2a1e68936eb952535ca4cd3b90444", size = 915599237, upload-time = "2026-01-21T16:23:25.497Z" }, { url = "https://files.pythonhosted.org/packages/40/b8/66bbe96f0d79be2b5c697b2e0b187ed792a15c6c4b8904613454651db848/torch-2.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:a4be6a2a190b32ff5c8002a0977a25ea60e64f7ba46b1be37093c141d9c49aeb", size = 113720931, upload-time = "2026-01-21T16:24:23.743Z" }, @@ -7458,7 +7495,7 @@ version = "0.25.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pillow" }, { name = "torch" }, ] @@ -7527,7 +7564,7 @@ dependencies = [ { name = "filelock" }, { name = "huggingface-hub" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "packaging" }, { name = "pyyaml" }, { name = "regex" }, @@ -7662,16 +7699,16 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.41.0" +version = "0.42.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/ad/4a96c425be6fb67e0621e62d86c402b4a17ab2be7f7c055d9bd2f638b9e2/uvicorn-0.42.0.tar.gz", hash = "sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775", size = 85393, upload-time = "2026-03-16T06:19:50.077Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" }, + { url = "https://files.pythonhosted.org/packages/0a/89/f8827ccff89c1586027a105e5630ff6139a64da2515e24dafe860bd9ae4d/uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359", size = 68830, upload-time = "2026-03-16T06:19:48.325Z" }, ] [package.optional-dependencies] @@ -7731,7 +7768,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "21.1.0" +version = "21.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -7740,9 +7777,9 @@ dependencies = [ { name = "python-discovery" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/c9/18d4b36606d6091844daa3bd93cf7dc78e6f5da21d9f21d06c221104b684/virtualenv-21.1.0.tar.gz", hash = "sha256:1990a0188c8f16b6b9cf65c9183049007375b26aad415514d377ccacf1e4fb44", size = 5840471, upload-time = "2026-02-27T08:49:29.702Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703c2666e809c4f686c54ef0a68b0f6afccf518c0b1eb9/virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098", size = 5840618, upload-time = "2026-03-09T17:24:38.013Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/55/896b06bf93a49bec0f4ae2a6f1ed12bd05c8860744ac3a70eda041064e4d/virtualenv-21.1.0-py3-none-any.whl", hash = "sha256:164f5e14c5587d170cf98e60378eb91ea35bf037be313811905d3a24ea33cc07", size = 5825072, upload-time = "2026-02-27T08:49:27.516Z" }, + { url = "https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f", size = 5825084, upload-time = "2026-03-09T17:24:35.378Z" }, ] [[package]]