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",
|
"typeCheckingMode": "basic",
|
||||||
"pythonVersion": "3.11",
|
"pythonVersion": "3.11",
|
||||||
"pythonPlatform": "All",
|
"pythonPlatform": "All",
|
||||||
"include": ["scripts", "src/pipecat"],
|
"include": [
|
||||||
"exclude": ["**/*_pb2.py", "**/__pycache__"],
|
"scripts",
|
||||||
|
"src/pipecat"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"**/*_pb2.py",
|
||||||
|
"**/__pycache__"
|
||||||
|
],
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"tests",
|
"tests",
|
||||||
"src/pipecat/adapters/base_llm_adapter.py",
|
"src/pipecat/adapters/base_llm_adapter.py",
|
||||||
@@ -23,7 +29,6 @@
|
|||||||
"src/pipecat/audio/filters/aic_filter.py",
|
"src/pipecat/audio/filters/aic_filter.py",
|
||||||
"src/pipecat/audio/filters/krisp_viva_filter.py",
|
"src/pipecat/audio/filters/krisp_viva_filter.py",
|
||||||
"src/pipecat/audio/filters/rnnoise_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_v2.py",
|
||||||
"src/pipecat/audio/turn/smart_turn/local_smart_turn_v3.py",
|
"src/pipecat/audio/turn/smart_turn/local_smart_turn_v3.py",
|
||||||
"src/pipecat/audio/vad/silero.py",
|
"src/pipecat/audio/vad/silero.py",
|
||||||
@@ -35,7 +40,6 @@
|
|||||||
"src/pipecat/processors/frameworks/rtvi/processor.py",
|
"src/pipecat/processors/frameworks/rtvi/processor.py",
|
||||||
"src/pipecat/processors/frameworks/strands_agents.py",
|
"src/pipecat/processors/frameworks/strands_agents.py",
|
||||||
"src/pipecat/processors/gstreamer/pipeline_source.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/ai_service.py",
|
||||||
"src/pipecat/services/anthropic/llm.py",
|
"src/pipecat/services/anthropic/llm.py",
|
||||||
"src/pipecat/services/assemblyai/stt.py",
|
"src/pipecat/services/assemblyai/stt.py",
|
||||||
@@ -130,9 +134,8 @@
|
|||||||
"src/pipecat/transports/smallwebrtc/transport.py",
|
"src/pipecat/transports/smallwebrtc/transport.py",
|
||||||
"src/pipecat/transports/tavus/transport.py",
|
"src/pipecat/transports/tavus/transport.py",
|
||||||
"src/pipecat/transports/websocket/client.py",
|
"src/pipecat/transports/websocket/client.py",
|
||||||
"src/pipecat/transports/websocket/fastapi.py",
|
|
||||||
"src/pipecat/transports/websocket/server.py",
|
"src/pipecat/transports/websocket/server.py",
|
||||||
"src/pipecat/transports/whatsapp/client.py"
|
"src/pipecat/transports/whatsapp/client.py"
|
||||||
],
|
],
|
||||||
"reportMissingImports": false
|
"reportMissingImports": false
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,7 @@ class SOXRStreamAudioResampler(BaseAudioResampler):
|
|||||||
self._soxr_stream.clear()
|
self._soxr_stream.clear()
|
||||||
self._last_resample_time = current_time
|
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:
|
if self._soxr_stream is None:
|
||||||
self._initialize(in_rate, out_rate)
|
self._initialize(in_rate, out_rate)
|
||||||
else:
|
else:
|
||||||
@@ -80,6 +80,9 @@ class SOXRStreamAudioResampler(BaseAudioResampler):
|
|||||||
f"expected {self._in_rate}->{self._out_rate}, got {in_rate}->{out_rate}"
|
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:
|
async def resample(self, audio: bytes, in_rate: int, out_rate: int) -> bytes:
|
||||||
"""Resample audio data using soxr.ResampleStream resampler library.
|
"""Resample audio data using soxr.ResampleStream resampler library.
|
||||||
|
|
||||||
@@ -94,8 +97,8 @@ class SOXRStreamAudioResampler(BaseAudioResampler):
|
|||||||
if in_rate == out_rate:
|
if in_rate == out_rate:
|
||||||
return audio
|
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)
|
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()
|
result = resampled_audio.astype(np.int16).tobytes()
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ class FrameProcessorMetrics(BaseObject):
|
|||||||
Returns:
|
Returns:
|
||||||
The task manager instance for async operations.
|
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
|
return self._task_manager
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -255,7 +255,9 @@ class FastAPIWebsocketInputTransport(BaseInputTransport):
|
|||||||
if self._params.serializer:
|
if self._params.serializer:
|
||||||
await self._params.serializer.setup(frame)
|
await self._params.serializer.setup(frame)
|
||||||
if not self._monitor_websocket_task and self._params.session_timeout:
|
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._client.trigger_client_connected()
|
||||||
await self.push_frame(ClientConnectedFrame())
|
await self.push_frame(ClientConnectedFrame())
|
||||||
if not self._receive_task:
|
if not self._receive_task:
|
||||||
@@ -322,9 +324,9 @@ class FastAPIWebsocketInputTransport(BaseInputTransport):
|
|||||||
if not self._client.is_closing:
|
if not self._client.is_closing:
|
||||||
await self._client.trigger_client_disconnected()
|
await self._client.trigger_client_disconnected()
|
||||||
|
|
||||||
async def _monitor_websocket(self):
|
async def _monitor_websocket(self, timeout: int):
|
||||||
"""Wait for self._params.session_timeout seconds, if the websocket is still open, trigger timeout event."""
|
"""Wait for ``timeout`` seconds, then trigger the client-timeout event if still open."""
|
||||||
await asyncio.sleep(self._params.session_timeout)
|
await asyncio.sleep(timeout)
|
||||||
await self._client.trigger_client_timeout()
|
await self._client.trigger_client_timeout()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user