fix: clear 10 more services from pyright ignore list

A second pass over the low-error-count files in the ignore list. Drops
10 files (77 → 67) and full-pyright errors from 580 → 555. Default
pyright stays clean.

Three coherent shapes plus a handful of one-offs:

`Language | str | None` → `Language | None` at STT frame boundaries.
`assert_given(self._settings.language)` returns `Language | str | None`
(strips `_NotGiven`, keeps the rest), but `TranscriptionFrame.language`
expects `Language | None`. In practice both `_settings.language` and
SDK-supplied codes resolve to a `Language` enum value, but technically
they could be raw strings — and `Language` is a StrEnum, so downstream
consumers (which mostly compare/serialize as strings) handle either.
Used `cast("Language | None", ...)` at each call site rather than a
runtime-validating helper, so an unrecognised code (e.g. one we
haven't added to the enum yet) still flows through unchanged. Cleared
azure/stt.py, aws/stt.py, gradium/stt.py; mistral/stt.py keeps the
cast at the SDK boundary (storing under `_detected_language: Language
| None`) but stays in the ignore list because of two unrelated
Optional-access errors.

aiobotocore `async with` stub gap. `aioboto3.Session().client(...)`
is an async context manager at runtime but its stubs don't advertise
`__aenter__`/`__aexit__` to pyright. Scoped
`# pyright: ignore[reportGeneralTypeIssues]` on the two affected
sites: aws/agent_core.py and aws/tts.py. aws/tts.py also had a latent
bug on the no-`AudioStream` path: the original code set
`audio_data = None` and then crashed in `resample(...)` and
`len(audio_data)` below; replaced with an early `return` after
logging — matches the convention elsewhere (OpenAI TTS, etc.) of not
recording usage metrics on the error path.

heygen `event_id: str | None` → `str` at transport→client boundary.
Three call sites in transports/heygen/transport.py passed `self._event_id`
(`str | None`) into client methods that take `str`. Added a guard at
each: `agent_speak_end` and `interrupt` only fire when `_event_id` is
set; `write_audio_frame` warn-and-drops when there's no active bot
event rather than sending a malformed message.

`OpenAIResponsesLLMInvocationParams` TypedDict.
`get_llm_invocation_params` always sets both `input` and `tools` in
the same dict literal, but the TypedDict was `total=False` so direct
subscript access (`invocation_params["input"]`) tripped
`reportTypedDictNotRequiredAccess` in services/openai/responses/llm.py.
Marked both keys `Required[...]`; `instructions` stays non-required
since it's only added when a system instruction is present.

Latent bug in heygen/api_interactive_avatar.py: the code accessed
`request_data.voice.voiceId` and `request_data.voice.elevenlabsSettings`,
but those names are Pydantic *aliases*; the actual attribute names
(used for attribute access) are `voice_id` and `elevenlabs_settings`.
Switched to the field names — those camelCase accesses would have
raised AttributeError at runtime if `voice` was set.

Other small fixes:

- assemblyai/stt.py: the deprecated `connection_params=` init path
  was reading `formatted_finals` and `word_finalization_max_wait_time`
  off `AssemblyAIConnectionParams`, but those fields were never on
  the deprecated input model — they were added to Settings later.
  Removed the reads (with a comment noting they're only available
  via the canonical `settings=...` API); the deprecated input model
  is unchanged.
- rtvi/processor.py: two `about: Mapping[str, Any] = None` parameter
  signatures — declared `Mapping`, defaulted to `None`, and both
  function bodies already handled the None case. Widened to
  `Mapping[str, Any] | None = None`.
- aws/stt.py: `subprotocols=["mqtt"]` failed against websockets'
  `Sequence[Subprotocol] | None` (Subprotocol is a NewType wrapper).
  Wrapped: `subprotocols=[Subprotocol("mqtt")]`.

Files dropped from the ignore list (77 → 67):
processors/frameworks/rtvi/processor.py, services/assemblyai/stt.py,
services/aws/agent_core.py, services/aws/stt.py, services/aws/tts.py,
services/azure/stt.py, services/gradium/stt.py,
services/heygen/api_interactive_avatar.py,
services/openai/responses/llm.py, transports/heygen/transport.py.
This commit is contained in:
Paul Kompfner
2026-04-29 12:48:16 -04:00
parent 814f00ce41
commit 31ff07916f
12 changed files with 71 additions and 43 deletions

View File

@@ -17,18 +17,12 @@
"src/pipecat/processors/frame_processor.py", "src/pipecat/processors/frame_processor.py",
"src/pipecat/processors/frameworks/langchain.py", "src/pipecat/processors/frameworks/langchain.py",
"src/pipecat/processors/frameworks/rtvi/observer.py", "src/pipecat/processors/frameworks/rtvi/observer.py",
"src/pipecat/processors/frameworks/rtvi/processor.py",
"src/pipecat/processors/frameworks/strands_agents.py", "src/pipecat/processors/frameworks/strands_agents.py",
"src/pipecat/services/anthropic/llm.py", "src/pipecat/services/anthropic/llm.py",
"src/pipecat/services/assemblyai/stt.py",
"src/pipecat/services/aws/agent_core.py",
"src/pipecat/services/aws/llm.py", "src/pipecat/services/aws/llm.py",
"src/pipecat/services/aws/nova_sonic/llm.py", "src/pipecat/services/aws/nova_sonic/llm.py",
"src/pipecat/services/aws/sagemaker/bidi_client.py", "src/pipecat/services/aws/sagemaker/bidi_client.py",
"src/pipecat/services/aws/stt.py",
"src/pipecat/services/aws/tts.py",
"src/pipecat/services/aws/utils.py", "src/pipecat/services/aws/utils.py",
"src/pipecat/services/azure/stt.py",
"src/pipecat/services/azure/tts.py", "src/pipecat/services/azure/tts.py",
"src/pipecat/services/deepgram/flux/base.py", "src/pipecat/services/deepgram/flux/base.py",
"src/pipecat/services/deepgram/flux/sagemaker/stt.py", "src/pipecat/services/deepgram/flux/sagemaker/stt.py",
@@ -42,8 +36,6 @@
"src/pipecat/services/google/llm.py", "src/pipecat/services/google/llm.py",
"src/pipecat/services/google/stt.py", "src/pipecat/services/google/stt.py",
"src/pipecat/services/google/tts.py", "src/pipecat/services/google/tts.py",
"src/pipecat/services/gradium/stt.py",
"src/pipecat/services/heygen/api_interactive_avatar.py",
"src/pipecat/services/heygen/client.py", "src/pipecat/services/heygen/client.py",
"src/pipecat/services/heygen/video.py", "src/pipecat/services/heygen/video.py",
"src/pipecat/services/inworld/realtime/llm.py", "src/pipecat/services/inworld/realtime/llm.py",
@@ -57,7 +49,6 @@
"src/pipecat/services/openai/base_llm.py", "src/pipecat/services/openai/base_llm.py",
"src/pipecat/services/openai/image.py", "src/pipecat/services/openai/image.py",
"src/pipecat/services/openai/realtime/llm.py", "src/pipecat/services/openai/realtime/llm.py",
"src/pipecat/services/openai/responses/llm.py",
"src/pipecat/services/openai/stt.py", "src/pipecat/services/openai/stt.py",
"src/pipecat/services/rime/tts.py", "src/pipecat/services/rime/tts.py",
"src/pipecat/services/sambanova/llm.py", "src/pipecat/services/sambanova/llm.py",
@@ -72,7 +63,6 @@
"src/pipecat/services/xai/realtime/llm.py", "src/pipecat/services/xai/realtime/llm.py",
"src/pipecat/transports/base_output.py", "src/pipecat/transports/base_output.py",
"src/pipecat/transports/daily/transport.py", "src/pipecat/transports/daily/transport.py",
"src/pipecat/transports/heygen/transport.py",
"src/pipecat/transports/lemonslice/transport.py", "src/pipecat/transports/lemonslice/transport.py",
"src/pipecat/transports/livekit/transport.py", "src/pipecat/transports/livekit/transport.py",
"src/pipecat/transports/smallwebrtc/connection.py", "src/pipecat/transports/smallwebrtc/connection.py",

View File

@@ -6,7 +6,7 @@
"""OpenAI Responses API adapter for Pipecat.""" """OpenAI Responses API adapter for Pipecat."""
from typing import Any, TypedDict, cast from typing import Any, Required, TypedDict, cast
from openai._types import NotGiven as OpenAINotGiven from openai._types import NotGiven as OpenAINotGiven
from openai.types.responses import FunctionToolParam, ResponseInputItemParam, ToolParam from openai.types.responses import FunctionToolParam, ResponseInputItemParam, ToolParam
@@ -23,8 +23,10 @@ from pipecat.processors.aggregators.llm_context import (
class OpenAIResponsesLLMInvocationParams(TypedDict, total=False): class OpenAIResponsesLLMInvocationParams(TypedDict, total=False):
"""Context-based parameters for invoking OpenAI Responses API.""" """Context-based parameters for invoking OpenAI Responses API."""
input: list[ResponseInputItemParam] # `input` and `tools` are always populated by `get_llm_invocation_params`;
tools: list[ToolParam] | OpenAINotGiven # `instructions` is only set when a system instruction is present.
input: Required[list[ResponseInputItemParam]]
tools: Required[list[ToolParam] | OpenAINotGiven]
instructions: str instructions: str

View File

@@ -102,7 +102,7 @@ class RTVIProcessor(FrameProcessor):
self._client_ready = True self._client_ready = True
await self._call_event_handler("on_client_ready") await self._call_event_handler("on_client_ready")
async def set_bot_ready(self, about: Mapping[str, Any] = None): async def set_bot_ready(self, about: Mapping[str, Any] | None = None):
"""Mark the bot as ready and send the bot-ready message. """Mark the bot as ready and send the bot-ready message.
Args: Args:
@@ -404,7 +404,7 @@ class RTVIProcessor(FrameProcessor):
) )
await self.push_frame(frame) await self.push_frame(frame)
async def _send_bot_ready(self, about: Mapping[str, Any] = None): async def _send_bot_ready(self, about: Mapping[str, Any] | None = None):
"""Send the bot-ready message to the client. """Send the bot-ready message to the client.
Args: Args:

View File

@@ -233,10 +233,11 @@ class AssemblyAISTTService(WebsocketSTTService):
sample_rate = connection_params.sample_rate sample_rate = connection_params.sample_rate
encoding = connection_params.encoding encoding = connection_params.encoding
default_settings.model = connection_params.speech_model default_settings.model = connection_params.speech_model
default_settings.formatted_finals = connection_params.formatted_finals # Note: `formatted_finals` and `word_finalization_max_wait_time`
default_settings.word_finalization_max_wait_time = ( # were added to Settings after this deprecated input model
connection_params.word_finalization_max_wait_time # was frozen and have no equivalent on
) # AssemblyAIConnectionParams; they are only configurable via
# the canonical `settings=...` API.
default_settings.end_of_turn_confidence_threshold = ( default_settings.end_of_turn_confidence_threshold = (
connection_params.end_of_turn_confidence_threshold connection_params.end_of_turn_confidence_threshold
) )

View File

@@ -201,7 +201,11 @@ class AWSAgentCoreProcessor(FrameProcessor):
if not payload: if not payload:
return return
async with self._aws_session.client("bedrock-agentcore", **self._aws_params) as client: # aioboto3's `client()` is an async context manager but its stubs don't
# advertise `__aenter__` / `__aexit__` in a way pyright can see.
async with self._aws_session.client( # pyright: ignore[reportGeneralTypeIssues]
"bedrock-agentcore", **self._aws_params
) as client:
# Invoke the AgentCore agent # Invoke the AgentCore agent
response = await client.invoke_agent_runtime( response = await client.invoke_agent_runtime(
agentRuntimeArn=self._agentArn, payload=payload.encode() agentRuntimeArn=self._agentArn, payload=payload.encode()

View File

@@ -16,7 +16,7 @@ import random
import string import string
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any, cast
from loguru import logger from loguru import logger
@@ -38,6 +38,7 @@ from pipecat.utils.time import time_now_iso8601
from pipecat.utils.tracing.service_decorators import traced_stt from pipecat.utils.tracing.service_decorators import traced_stt
try: try:
from websockets import Subprotocol
from websockets.asyncio.client import connect as websocket_connect from websockets.asyncio.client import connect as websocket_connect
from websockets.protocol import State from websockets.protocol import State
except ModuleNotFoundError as e: except ModuleNotFoundError as e:
@@ -314,7 +315,7 @@ class AWSTranscribeSTTService(WebsocketSTTService):
self._websocket = await websocket_connect( self._websocket = await websocket_connect(
presigned_url, presigned_url,
additional_headers=additional_headers, additional_headers=additional_headers,
subprotocols=["mqtt"], subprotocols=[Subprotocol("mqtt")],
ping_interval=None, ping_interval=None,
ping_timeout=None, ping_timeout=None,
compression=None, compression=None,
@@ -534,7 +535,11 @@ class AWSTranscribeSTTService(WebsocketSTTService):
is_final = not result.get("IsPartial", True) is_final = not result.get("IsPartial", True)
if transcript: if transcript:
language = assert_given(self._settings.language) # Technically `_settings.language` could be a raw string, but
# Language is a StrEnum so downstream handles either.
language = cast(
"Language | None", assert_given(self._settings.language)
)
if is_final: if is_final:
await self.push_frame( await self.push_frame(
TranscriptionFrame( TranscriptionFrame(

View File

@@ -345,7 +345,11 @@ class AWSPollyTTSService(TTSService):
# Filter out None values # Filter out None values
filtered_params = {k: v for k, v in params.items() if v is not None} filtered_params = {k: v for k, v in params.items() if v is not None}
async with self._aws_session.client("polly", **self._aws_params) as polly: # aioboto3's `client()` is an async context manager but its stubs
# don't advertise `__aenter__` / `__aexit__` to pyright.
async with self._aws_session.client( # pyright: ignore[reportGeneralTypeIssues]
"polly", **self._aws_params
) as polly:
response = await polly.synthesize_speech(**filtered_params) response = await polly.synthesize_speech(**filtered_params)
if "AudioStream" in response: if "AudioStream" in response:
# Get the streaming body and read it # Get the streaming body and read it
@@ -353,7 +357,7 @@ class AWSPollyTTSService(TTSService):
audio_data = await stream.read() audio_data = await stream.read()
else: else:
logger.error(f"{self} No audio stream in response") logger.error(f"{self} No audio stream in response")
audio_data = None return
audio_data = await self._resampler.resample(audio_data, 16000, self.sample_rate) audio_data = await self._resampler.resample(audio_data, 16000, self.sample_rate)

View File

@@ -13,7 +13,7 @@ Speech SDK for real-time audio transcription.
import asyncio import asyncio
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any, cast
from loguru import logger from loguru import logger
@@ -280,8 +280,11 @@ class AzureSTTService(STTService):
def _on_handle_recognized(self, event): def _on_handle_recognized(self, event):
if event.result.reason == ResultReason.RecognizedSpeech and len(event.result.text) > 0: if event.result.reason == ResultReason.RecognizedSpeech and len(event.result.text) > 0:
language = getattr(event.result, "language", None) or assert_given( # Technically either source could be a raw string, but Language is
self._settings.language # a StrEnum so downstream handles either.
language = cast(
"Language | None",
getattr(event.result, "language", None) or assert_given(self._settings.language),
) )
frame = TranscriptionFrame( frame = TranscriptionFrame(
event.result.text, event.result.text,
@@ -297,8 +300,11 @@ class AzureSTTService(STTService):
def _on_handle_recognizing(self, event): def _on_handle_recognizing(self, event):
if event.result.reason == ResultReason.RecognizingSpeech and len(event.result.text) > 0: if event.result.reason == ResultReason.RecognizingSpeech and len(event.result.text) > 0:
language = getattr(event.result, "language", None) or assert_given( # Technically either source could be a raw string, but Language is
self._settings.language # a StrEnum so downstream handles either.
language = cast(
"Language | None",
getattr(event.result, "language", None) or assert_given(self._settings.language),
) )
frame = InterimTranscriptionFrame( frame = InterimTranscriptionFrame(
event.result.text, event.result.text,

View File

@@ -15,7 +15,7 @@ import base64
import json import json
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any from typing import Any, cast
from loguru import logger from loguru import logger
from pydantic import BaseModel from pydantic import BaseModel
@@ -401,8 +401,10 @@ class GradiumSTTService(WebsocketSTTService):
json_config = {} json_config = {}
if self._json_config: if self._json_config:
json_config = json.loads(self._json_config) json_config = json.loads(self._json_config)
language = assert_given(self._settings.language) # Technically `_settings.language` could be a raw string, but
if language: # Language is a StrEnum so downstream handles either.
language = cast("Language | None", assert_given(self._settings.language))
if language is not None:
gradium_language = language_to_gradium_language(language) gradium_language = language_to_gradium_language(language)
if gradium_language: if gradium_language:
json_config["language"] = gradium_language json_config["language"] = gradium_language
@@ -482,12 +484,14 @@ class GradiumSTTService(WebsocketSTTService):
""" """
self._accumulated_text.append(text) self._accumulated_text.append(text)
accumulated = " ".join(self._accumulated_text) accumulated = " ".join(self._accumulated_text)
# Technically `_settings.language` could be a raw string, but Language
# is a StrEnum so downstream handles either.
await self.push_frame( await self.push_frame(
InterimTranscriptionFrame( InterimTranscriptionFrame(
text=accumulated, text=accumulated,
user_id=self._user_id, user_id=self._user_id,
timestamp=time_now_iso8601(), timestamp=time_now_iso8601(),
language=assert_given(self._settings.language), language=cast("Language | None", assert_given(self._settings.language)),
) )
) )
await self.stop_processing_metrics() await self.stop_processing_metrics()
@@ -519,7 +523,9 @@ class GradiumSTTService(WebsocketSTTService):
text = " ".join(self._accumulated_text) text = " ".join(self._accumulated_text)
self._accumulated_text.clear() self._accumulated_text.clear()
logger.debug(f"Final transcription: [{text}]") logger.debug(f"Final transcription: [{text}]")
language = assert_given(self._settings.language) # Technically `_settings.language` could be a raw string, but Language
# is a StrEnum so downstream handles either.
language = cast("Language | None", assert_given(self._settings.language))
await self.push_frame( await self.push_frame(
TranscriptionFrame( TranscriptionFrame(
text, text,

View File

@@ -210,11 +210,11 @@ class HeyGenApi(BaseAvatarApi):
"quality": request_data.quality, "quality": request_data.quality,
"avatar_id": request_data.avatar_id, "avatar_id": request_data.avatar_id,
"voice": { "voice": {
"voice_id": request_data.voice.voiceId if request_data.voice else None, "voice_id": request_data.voice.voice_id if request_data.voice else None,
"rate": request_data.voice.rate if request_data.voice else None, "rate": request_data.voice.rate if request_data.voice else None,
"emotion": request_data.voice.emotion if request_data.voice else None, "emotion": request_data.voice.emotion if request_data.voice else None,
"elevenlabs_settings": ( "elevenlabs_settings": (
request_data.voice.elevenlabsSettings if request_data.voice else None request_data.voice.elevenlabs_settings if request_data.voice else None
), ),
}, },
"knowledge_id": request_data.knowledge_id, "knowledge_id": request_data.knowledge_id,

View File

@@ -12,7 +12,7 @@ Voxtral Realtime transcription API using the Mistral SDK's RealtimeConnection.
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any, cast
from loguru import logger from loguru import logger
@@ -30,6 +30,7 @@ from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.settings import STTSettings, assert_given from pipecat.services.settings import STTSettings, assert_given
from pipecat.services.stt_latency import MISTRAL_TTFS_P99 from pipecat.services.stt_latency import MISTRAL_TTFS_P99
from pipecat.services.stt_service import STTService from pipecat.services.stt_service import STTService
from pipecat.transcriptions.language import Language
from pipecat.utils.time import time_now_iso8601 from pipecat.utils.time import time_now_iso8601
from pipecat.utils.tracing.service_decorators import traced_stt from pipecat.utils.tracing.service_decorators import traced_stt
@@ -132,7 +133,7 @@ class MistralSTTService(STTService):
self._connection: RealtimeConnection | None = None self._connection: RealtimeConnection | None = None
self._receive_task = None self._receive_task = None
self._accumulated_text = "" self._accumulated_text = ""
self._detected_language: str | None = None self._detected_language: Language | None = None
def can_generate_metrics(self) -> bool: def can_generate_metrics(self) -> bool:
"""Check if the service can generate processing metrics. """Check if the service can generate processing metrics.
@@ -278,7 +279,9 @@ class MistralSTTService(STTService):
self._accumulated_text = "" self._accumulated_text = ""
elif isinstance(event, TranscriptionStreamLanguage): elif isinstance(event, TranscriptionStreamLanguage):
self._detected_language = event.audio_language # Technically the SDK could emit a code we haven't added yet,
# but Language is a StrEnum so downstream handles either.
self._detected_language = cast("Language | None", event.audio_language)
elif isinstance(event, RealtimeTranscriptionError): elif isinstance(event, RealtimeTranscriptionError):
error_msg = event.error.message if event.error else "Unknown error" error_msg = event.error.message if event.error else "Unknown error"

View File

@@ -239,8 +239,9 @@ class HeyGenOutputTransport(BaseOutputTransport):
logger.warning("self._event_id is already defined!") logger.warning("self._event_id is already defined!")
self._event_id = str(frame.id) self._event_id = str(frame.id)
elif isinstance(frame, BotStoppedSpeakingFrame): elif isinstance(frame, BotStoppedSpeakingFrame):
await self._client.agent_speak_end(self._event_id) if self._event_id is not None:
self._event_id = None await self._client.agent_speak_end(self._event_id)
self._event_id = None
await super().push_frame(frame, direction) await super().push_frame(frame, direction)
async def process_frame(self, frame: Frame, direction: FrameDirection): async def process_frame(self, frame: Frame, direction: FrameDirection):
@@ -261,7 +262,8 @@ class HeyGenOutputTransport(BaseOutputTransport):
""" """
await super().process_frame(frame, direction) await super().process_frame(frame, direction)
if isinstance(frame, InterruptionFrame): if isinstance(frame, InterruptionFrame):
await self._client.interrupt(self._event_id) if self._event_id is not None:
await self._client.interrupt(self._event_id)
await self.push_frame(frame, direction) await self.push_frame(frame, direction)
if isinstance(frame, UserStartedSpeakingFrame): if isinstance(frame, UserStartedSpeakingFrame):
await self._client.start_agent_listening() await self._client.start_agent_listening()
@@ -281,6 +283,11 @@ class HeyGenOutputTransport(BaseOutputTransport):
audio = frame.audio audio = frame.audio
if frame.sample_rate != HEY_GEN_SAMPLE_RATE: if frame.sample_rate != HEY_GEN_SAMPLE_RATE:
audio = await self._resampler.resample(audio, frame.sample_rate, HEY_GEN_SAMPLE_RATE) audio = await self._resampler.resample(audio, frame.sample_rate, HEY_GEN_SAMPLE_RATE)
if self._event_id is None:
# No active bot-speech event — drop the chunk rather than send a
# message the HeyGen API will reject.
logger.warning(f"{self}: dropping audio frame because no event_id is set")
return False
await self._client.agent_speak(bytes(audio), self._event_id) await self._client.agent_speak(bytes(audio), self._event_id)
return True return True