Encode lazy-init invariants at the right site, not at read sites
Three spots had the same shape: a field starts None, a later method populates it, a read site later reads it. Pyright can't track the cross-method invariant. Rather than spray assertions at the read sites, fix each site at the structural level: - `FastAPIWebsocketInputTransport._monitor_websocket` now takes the session timeout as an argument. The task-creation site already guards on truthiness, so the call can pass the non-None value directly and the method's signature tells the truth. - `FrameProcessorMetrics.task_manager` raises `RuntimeError` instead of asserting. Asserts are stripped under `python -O`; a real raise keeps the runtime safety net and still narrows the type for pyright. - `SOXRStreamAudioResampler._maybe_initialize_sox_stream` returns the initialized stream. Callers use the return value and never touch the Optional `_soxr_stream` attribute, so narrowing stays inside the init method where the invariant is established.
This commit is contained in:
@@ -2,8 +2,14 @@
|
||||
"typeCheckingMode": "basic",
|
||||
"pythonVersion": "3.11",
|
||||
"pythonPlatform": "All",
|
||||
"include": ["scripts", "src/pipecat"],
|
||||
"exclude": ["**/*_pb2.py", "**/__pycache__"],
|
||||
"include": [
|
||||
"scripts",
|
||||
"src/pipecat"
|
||||
],
|
||||
"exclude": [
|
||||
"**/*_pb2.py",
|
||||
"**/__pycache__"
|
||||
],
|
||||
"ignore": [
|
||||
"tests",
|
||||
"src/pipecat/adapters/base_llm_adapter.py",
|
||||
@@ -23,7 +29,6 @@
|
||||
"src/pipecat/audio/filters/aic_filter.py",
|
||||
"src/pipecat/audio/filters/krisp_viva_filter.py",
|
||||
"src/pipecat/audio/filters/rnnoise_filter.py",
|
||||
"src/pipecat/audio/resamplers/soxr_stream_resampler.py",
|
||||
"src/pipecat/audio/turn/smart_turn/local_smart_turn_v2.py",
|
||||
"src/pipecat/audio/turn/smart_turn/local_smart_turn_v3.py",
|
||||
"src/pipecat/audio/vad/silero.py",
|
||||
@@ -35,7 +40,6 @@
|
||||
"src/pipecat/processors/frameworks/rtvi/processor.py",
|
||||
"src/pipecat/processors/frameworks/strands_agents.py",
|
||||
"src/pipecat/processors/gstreamer/pipeline_source.py",
|
||||
"src/pipecat/processors/metrics/frame_processor_metrics.py",
|
||||
"src/pipecat/services/ai_service.py",
|
||||
"src/pipecat/services/anthropic/llm.py",
|
||||
"src/pipecat/services/assemblyai/stt.py",
|
||||
@@ -130,9 +134,8 @@
|
||||
"src/pipecat/transports/smallwebrtc/transport.py",
|
||||
"src/pipecat/transports/tavus/transport.py",
|
||||
"src/pipecat/transports/websocket/client.py",
|
||||
"src/pipecat/transports/websocket/fastapi.py",
|
||||
"src/pipecat/transports/websocket/server.py",
|
||||
"src/pipecat/transports/whatsapp/client.py"
|
||||
],
|
||||
"reportMissingImports": false
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ class SOXRStreamAudioResampler(BaseAudioResampler):
|
||||
self._soxr_stream.clear()
|
||||
self._last_resample_time = current_time
|
||||
|
||||
def _maybe_initialize_sox_stream(self, in_rate: int, out_rate: int):
|
||||
def _maybe_initialize_sox_stream(self, in_rate: int, out_rate: int) -> "soxr.ResampleStream":
|
||||
if self._soxr_stream is None:
|
||||
self._initialize(in_rate, out_rate)
|
||||
else:
|
||||
@@ -80,6 +80,9 @@ class SOXRStreamAudioResampler(BaseAudioResampler):
|
||||
f"expected {self._in_rate}->{self._out_rate}, got {in_rate}->{out_rate}"
|
||||
)
|
||||
|
||||
assert self._soxr_stream is not None
|
||||
return self._soxr_stream
|
||||
|
||||
async def resample(self, audio: bytes, in_rate: int, out_rate: int) -> bytes:
|
||||
"""Resample audio data using soxr.ResampleStream resampler library.
|
||||
|
||||
@@ -94,8 +97,8 @@ class SOXRStreamAudioResampler(BaseAudioResampler):
|
||||
if in_rate == out_rate:
|
||||
return audio
|
||||
|
||||
self._maybe_initialize_sox_stream(in_rate, out_rate)
|
||||
stream = self._maybe_initialize_sox_stream(in_rate, out_rate)
|
||||
audio_data = np.frombuffer(audio, dtype=np.int16)
|
||||
resampled_audio = self._soxr_stream.resample_chunk(audio_data)
|
||||
resampled_audio = stream.resample_chunk(audio_data)
|
||||
result = resampled_audio.astype(np.int16).tobytes()
|
||||
return result
|
||||
|
||||
@@ -66,6 +66,8 @@ class FrameProcessorMetrics(BaseObject):
|
||||
Returns:
|
||||
The task manager instance for async operations.
|
||||
"""
|
||||
if self._task_manager is None:
|
||||
raise RuntimeError("task_manager not set; call setup() first")
|
||||
return self._task_manager
|
||||
|
||||
@property
|
||||
|
||||
@@ -255,7 +255,9 @@ class FastAPIWebsocketInputTransport(BaseInputTransport):
|
||||
if self._params.serializer:
|
||||
await self._params.serializer.setup(frame)
|
||||
if not self._monitor_websocket_task and self._params.session_timeout:
|
||||
self._monitor_websocket_task = self.create_task(self._monitor_websocket())
|
||||
self._monitor_websocket_task = self.create_task(
|
||||
self._monitor_websocket(self._params.session_timeout)
|
||||
)
|
||||
await self._client.trigger_client_connected()
|
||||
await self.push_frame(ClientConnectedFrame())
|
||||
if not self._receive_task:
|
||||
@@ -322,9 +324,9 @@ class FastAPIWebsocketInputTransport(BaseInputTransport):
|
||||
if not self._client.is_closing:
|
||||
await self._client.trigger_client_disconnected()
|
||||
|
||||
async def _monitor_websocket(self):
|
||||
"""Wait for self._params.session_timeout seconds, if the websocket is still open, trigger timeout event."""
|
||||
await asyncio.sleep(self._params.session_timeout)
|
||||
async def _monitor_websocket(self, timeout: int):
|
||||
"""Wait for ``timeout`` seconds, then trigger the client-timeout event if still open."""
|
||||
await asyncio.sleep(timeout)
|
||||
await self._client.trigger_client_timeout()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user