Merge pull request #3893 from pipecat-ai/mb/fix-azure-error-propagation

Propagate Azure TTS/STT cancellation errors to the pipeline
This commit is contained in:
Mark Backman
2026-03-02 13:04:54 -05:00
committed by GitHub
3 changed files with 22 additions and 2 deletions

1
changelog/3893.fixed.md Normal file
View File

@@ -0,0 +1 @@
- Fixed Azure TTS and STT services silently swallowing cancellation errors (invalid API key, network failures, rate limiting) instead of propagating them as `ErrorFrame`s to the pipeline.

View File

@@ -35,6 +35,7 @@ from pipecat.utils.tracing.service_decorators import traced_stt
try: try:
from azure.cognitiveservices.speech import ( from azure.cognitiveservices.speech import (
CancellationReason,
ResultReason, ResultReason,
SpeechConfig, SpeechConfig,
SpeechRecognizer, SpeechRecognizer,
@@ -209,6 +210,7 @@ class AzureSTTService(STTService):
) )
self._speech_recognizer.recognizing.connect(self._on_handle_recognizing) self._speech_recognizer.recognizing.connect(self._on_handle_recognizing)
self._speech_recognizer.recognized.connect(self._on_handle_recognized) self._speech_recognizer.recognized.connect(self._on_handle_recognized)
self._speech_recognizer.canceled.connect(self._on_handle_canceled)
self._speech_recognizer.start_continuous_recognition_async() self._speech_recognizer.start_continuous_recognition_async()
except Exception as e: except Exception as e:
await self.push_error( await self.push_error(
@@ -280,3 +282,13 @@ class AzureSTTService(STTService):
result=event, result=event,
) )
asyncio.run_coroutine_threadsafe(self.push_frame(frame), self.get_event_loop()) asyncio.run_coroutine_threadsafe(self.push_frame(frame), self.get_event_loop())
def _on_handle_canceled(self, event):
details = event.result.cancellation_details
if details.reason == CancellationReason.Error:
error_msg = f"Azure STT recognition canceled: {details.reason}"
if details.error_details:
error_msg += f" - {details.error_details}"
asyncio.run_coroutine_threadsafe(
self.push_error(error_msg=error_msg), self.get_event_loop()
)

View File

@@ -561,9 +561,13 @@ class AzureTTSService(TTSService, AzureBaseTTSService):
# User cancellation (from interruption) is expected, not an error # User cancellation (from interruption) is expected, not an error
if reason == CancellationReason.CancelledByUser: if reason == CancellationReason.CancelledByUser:
logger.debug(f"{self}: Speech synthesis canceled by user (interruption)") logger.debug(f"{self}: Speech synthesis canceled by user (interruption)")
self._audio_queue.put_nowait(None)
else: else:
logger.warning(f"{self}: Speech synthesis canceled: {reason}") details = evt.result.cancellation_details
self._audio_queue.put_nowait(None) error_msg = f"Azure TTS synthesis canceled: {reason}"
if details.error_details:
error_msg += f" - {details.error_details}"
self._audio_queue.put_nowait(Exception(error_msg))
async def push_frame(self, frame: Frame, direction: FrameDirection = FrameDirection.DOWNSTREAM): async def push_frame(self, frame: Frame, direction: FrameDirection = FrameDirection.DOWNSTREAM):
"""Push a frame and handle state changes. """Push a frame and handle state changes.
@@ -676,6 +680,9 @@ class AzureTTSService(TTSService, AzureBaseTTSService):
chunk = await self._audio_queue.get() chunk = await self._audio_queue.get()
if chunk is None: # End of stream if chunk is None: # End of stream
break break
if isinstance(chunk, Exception): # Error from _handle_canceled
yield ErrorFrame(error=str(chunk))
break
if self._first_chunk: if self._first_chunk:
await self.stop_ttfb_metrics() await self.stop_ttfb_metrics()