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/frameworks/langchain.py",
"src/pipecat/processors/frameworks/rtvi/observer.py",
"src/pipecat/processors/frameworks/rtvi/processor.py",
"src/pipecat/processors/frameworks/strands_agents.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/nova_sonic/llm.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/azure/stt.py",
"src/pipecat/services/azure/tts.py",
"src/pipecat/services/deepgram/flux/base.py",
"src/pipecat/services/deepgram/flux/sagemaker/stt.py",
@@ -42,8 +36,6 @@
"src/pipecat/services/google/llm.py",
"src/pipecat/services/google/stt.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/video.py",
"src/pipecat/services/inworld/realtime/llm.py",
@@ -57,7 +49,6 @@
"src/pipecat/services/openai/base_llm.py",
"src/pipecat/services/openai/image.py",
"src/pipecat/services/openai/realtime/llm.py",
"src/pipecat/services/openai/responses/llm.py",
"src/pipecat/services/openai/stt.py",
"src/pipecat/services/rime/tts.py",
"src/pipecat/services/sambanova/llm.py",
@@ -72,7 +63,6 @@
"src/pipecat/services/xai/realtime/llm.py",
"src/pipecat/transports/base_output.py",
"src/pipecat/transports/daily/transport.py",
"src/pipecat/transports/heygen/transport.py",
"src/pipecat/transports/lemonslice/transport.py",
"src/pipecat/transports/livekit/transport.py",
"src/pipecat/transports/smallwebrtc/connection.py",

View File

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

View File

@@ -102,7 +102,7 @@ class RTVIProcessor(FrameProcessor):
self._client_ready = True
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.
Args:
@@ -404,7 +404,7 @@ class RTVIProcessor(FrameProcessor):
)
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.
Args:

View File

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

View File

@@ -201,7 +201,11 @@ class AWSAgentCoreProcessor(FrameProcessor):
if not payload:
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
response = await client.invoke_agent_runtime(
agentRuntimeArn=self._agentArn, payload=payload.encode()

View File

@@ -16,7 +16,7 @@ import random
import string
from collections.abc import AsyncGenerator
from dataclasses import dataclass
from typing import Any
from typing import Any, cast
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
try:
from websockets import Subprotocol
from websockets.asyncio.client import connect as websocket_connect
from websockets.protocol import State
except ModuleNotFoundError as e:
@@ -314,7 +315,7 @@ class AWSTranscribeSTTService(WebsocketSTTService):
self._websocket = await websocket_connect(
presigned_url,
additional_headers=additional_headers,
subprotocols=["mqtt"],
subprotocols=[Subprotocol("mqtt")],
ping_interval=None,
ping_timeout=None,
compression=None,
@@ -534,7 +535,11 @@ class AWSTranscribeSTTService(WebsocketSTTService):
is_final = not result.get("IsPartial", True)
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:
await self.push_frame(
TranscriptionFrame(

View File

@@ -345,7 +345,11 @@ class AWSPollyTTSService(TTSService):
# Filter out None values
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)
if "AudioStream" in response:
# Get the streaming body and read it
@@ -353,7 +357,7 @@ class AWSPollyTTSService(TTSService):
audio_data = await stream.read()
else:
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)

View File

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

View File

@@ -15,7 +15,7 @@ import base64
import json
from collections.abc import AsyncGenerator
from dataclasses import dataclass, field
from typing import Any
from typing import Any, cast
from loguru import logger
from pydantic import BaseModel
@@ -401,8 +401,10 @@ class GradiumSTTService(WebsocketSTTService):
json_config = {}
if self._json_config:
json_config = json.loads(self._json_config)
language = assert_given(self._settings.language)
if 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 language is not None:
gradium_language = language_to_gradium_language(language)
if gradium_language:
json_config["language"] = gradium_language
@@ -482,12 +484,14 @@ class GradiumSTTService(WebsocketSTTService):
"""
self._accumulated_text.append(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(
InterimTranscriptionFrame(
text=accumulated,
user_id=self._user_id,
timestamp=time_now_iso8601(),
language=assert_given(self._settings.language),
language=cast("Language | None", assert_given(self._settings.language)),
)
)
await self.stop_processing_metrics()
@@ -519,7 +523,9 @@ class GradiumSTTService(WebsocketSTTService):
text = " ".join(self._accumulated_text)
self._accumulated_text.clear()
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(
TranscriptionFrame(
text,

View File

@@ -210,11 +210,11 @@ class HeyGenApi(BaseAvatarApi):
"quality": request_data.quality,
"avatar_id": request_data.avatar_id,
"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,
"emotion": request_data.voice.emotion if request_data.voice else None,
"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,

View File

@@ -12,7 +12,7 @@ Voxtral Realtime transcription API using the Mistral SDK's RealtimeConnection.
from collections.abc import AsyncGenerator
from dataclasses import dataclass
from typing import Any
from typing import Any, cast
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.stt_latency import MISTRAL_TTFS_P99
from pipecat.services.stt_service import STTService
from pipecat.transcriptions.language import Language
from pipecat.utils.time import time_now_iso8601
from pipecat.utils.tracing.service_decorators import traced_stt
@@ -132,7 +133,7 @@ class MistralSTTService(STTService):
self._connection: RealtimeConnection | None = None
self._receive_task = None
self._accumulated_text = ""
self._detected_language: str | None = None
self._detected_language: Language | None = None
def can_generate_metrics(self) -> bool:
"""Check if the service can generate processing metrics.
@@ -278,7 +279,9 @@ class MistralSTTService(STTService):
self._accumulated_text = ""
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):
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!")
self._event_id = str(frame.id)
elif isinstance(frame, BotStoppedSpeakingFrame):
await self._client.agent_speak_end(self._event_id)
self._event_id = None
if self._event_id is not None:
await self._client.agent_speak_end(self._event_id)
self._event_id = None
await super().push_frame(frame, direction)
async def process_frame(self, frame: Frame, direction: FrameDirection):
@@ -261,7 +262,8 @@ class HeyGenOutputTransport(BaseOutputTransport):
"""
await super().process_frame(frame, direction)
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)
if isinstance(frame, UserStartedSpeakingFrame):
await self._client.start_agent_listening()
@@ -281,6 +283,11 @@ class HeyGenOutputTransport(BaseOutputTransport):
audio = frame.audio
if 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)
return True