Merge branch 'pipecat-ai:main' into telemetry-fix-system-message
This commit is contained in:
254
src/pipecat/adapters/services/open_ai_responses_adapter.py
Normal file
254
src/pipecat/adapters/services/open_ai_responses_adapter.py
Normal file
@@ -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
|
||||
152
src/pipecat/adapters/services/perplexity_adapter.py
Normal file
152
src/pipecat/adapters/services/perplexity_adapter.py
Normal file
@@ -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
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]"
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
#
|
||||
# Copyright (c) 2024-2026, Daily
|
||||
#
|
||||
# SPDX-License-Identifier: BSD 2-Clause License
|
||||
#
|
||||
278
src/pipecat/services/google/gemini_live/vertex/llm.py
Normal file
278
src/pipecat/services/google/gemini_live/vertex/llm.py
Normal file
@@ -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
|
||||
@@ -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]"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
5
src/pipecat/services/google/openai/__init__.py
Normal file
5
src/pipecat/services/google/openai/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
#
|
||||
# Copyright (c) 2024-2026, Daily
|
||||
#
|
||||
# SPDX-License-Identifier: BSD 2-Clause License
|
||||
#
|
||||
213
src/pipecat/services/google/openai/llm.py
Normal file
213
src/pipecat/services/google/openai/llm.py
Normal file
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
5
src/pipecat/services/google/vertex/__init__.py
Normal file
5
src/pipecat/services/google/vertex/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
#
|
||||
# Copyright (c) 2024-2026, Daily
|
||||
#
|
||||
# SPDX-License-Identifier: BSD 2-Clause License
|
||||
#
|
||||
306
src/pipecat/services/google/vertex/llm.py
Normal file
306
src/pipecat/services/google/vertex/llm.py
Normal file
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) ---
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 *
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
5
src/pipecat/services/openai/responses/__init__.py
Normal file
5
src/pipecat/services/openai/responses/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
#
|
||||
# Copyright (c) 2024-2026, Daily
|
||||
#
|
||||
# SPDX-License-Identifier: BSD 2-Clause License
|
||||
#
|
||||
400
src/pipecat/services/openai/responses/llm.py
Normal file
400
src/pipecat/services/openai/responses/llm.py
Normal file
@@ -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"]
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user