3131from pipecat .services .stt_service import STTService
3232from pipecat .transcriptions .language import Language
3333from pipecat .utils .time import time_now_iso8601
34- from pipecat .utils .tracing .service_decorators import traced_stt
34+ from pipecat .utils .tracing .service_decorators import trace_stt_cancellation , traced_stt
3535
3636try :
3737 from azure .cognitiveservices .speech import (
@@ -123,6 +123,10 @@ def __init__(
123123
124124 self ._audio_stream = None
125125 self ._speech_recognizer = None
126+ self ._audio_sent = False
127+ self ._recognition_active = False
128+ self ._recognition_terminated = False
129+ self ._shutdown_requested = False
126130
127131 def can_generate_metrics (self ) -> bool :
128132 """Check if this service can generate performance metrics.
@@ -179,7 +183,12 @@ async def run_stt(self, audio: bytes) -> AsyncGenerator[Frame, None]:
179183 try :
180184 await self .start_processing_metrics ()
181185 if self ._audio_stream :
186+ if self ._recognition_terminated and not self ._shutdown_requested :
187+ logger .warning ("Azure STT recognition terminated, dropping audio chunk" )
188+ yield None
189+ return
182190 self ._audio_stream .write (audio )
191+ self ._audio_sent = True
183192 yield None
184193 except Exception as e :
185194 yield ErrorFrame (error = f"Unknown error occurred: { e } " )
@@ -198,6 +207,11 @@ async def start(self, frame: StartFrame):
198207 if self ._audio_stream :
199208 return
200209
210+ self ._audio_sent = False
211+ self ._recognition_active = False
212+ self ._recognition_terminated = False
213+ self ._shutdown_requested = False
214+
201215 try :
202216 stream_format = AudioStreamFormat (samples_per_second = self .sample_rate , channels = 1 )
203217 self ._audio_stream = PushAudioInputStream (stream_format )
@@ -230,6 +244,10 @@ async def stop(self, frame: EndFrame):
230244 """
231245 await super ().stop (frame )
232246
247+ self ._shutdown_requested = True
248+ self ._recognition_active = False
249+ self ._recognition_terminated = True
250+
233251 if self ._speech_recognizer :
234252 self ._speech_recognizer .stop_continuous_recognition_async ()
235253
@@ -246,6 +264,10 @@ async def cancel(self, frame: CancelFrame):
246264 """
247265 await super ().cancel (frame )
248266
267+ self ._shutdown_requested = True
268+ self ._recognition_active = False
269+ self ._recognition_terminated = True
270+
249271 if self ._speech_recognizer :
250272 self ._speech_recognizer .stop_continuous_recognition_async ()
251273
@@ -259,6 +281,25 @@ async def _handle_transcription(
259281 """Handle a transcription result with tracing."""
260282 await self .stop_processing_metrics ()
261283
284+ async def _trace_cancellation (
285+ self ,
286+ * ,
287+ reason : str ,
288+ code : str ,
289+ recoverable : bool ,
290+ phase : str ,
291+ ):
292+ """Record a trace span for a canceled Azure STT recognition."""
293+ trace_stt_cancellation (
294+ self ,
295+ error_type = "azure.stt.canceled" ,
296+ cancel_reason = reason ,
297+ cancel_code = code ,
298+ recoverable = recoverable ,
299+ phase = phase ,
300+ region = self ._settings .region if isinstance (self ._settings .region , str ) else None ,
301+ )
302+
262303 def _on_handle_recognized (self , event ):
263304 if event .result .reason == ResultReason .RecognizedSpeech and len (event .result .text ) > 0 :
264305 language = getattr (event .result , "language" , None ) or self ._settings .language
@@ -288,30 +329,87 @@ def _on_handle_recognizing(self, event):
288329
289330 def _on_handle_canceled (self , event ):
290331 details = getattr (event , "cancellation_details" , None )
291- reason = getattr (details , "reason" , "UNKNOWN" )
292- code = getattr (details , "code" , "UNKNOWN" )
332+ reason = self . _normalize_cancellation_value ( getattr (details , "reason" , "UNKNOWN" ) )
333+ code = self . _normalize_cancellation_value ( getattr (details , "code" , "UNKNOWN" ) )
293334 error_details = getattr (details , "error_details" , "" )
335+ phase = self ._get_cancellation_phase ()
336+ recoverable = self ._is_cancellation_recoverable (reason , code )
337+
338+ self ._recognition_active = False
339+ self ._recognition_terminated = True
294340
295341 logger .error (
296- "Azure STT recognition canceled: reason={}, code={}, details={}" ,
342+ "Azure STT recognition canceled: reason={}, code={}, phase={}, recoverable={}, details={}" ,
297343 reason ,
298344 code ,
345+ phase ,
346+ recoverable ,
299347 error_details ,
300348 )
301349
302- error_message = f"Azure STT recognition canceled: { code } - { error_details } "
350+ asyncio .run_coroutine_threadsafe (
351+ self ._trace_cancellation (
352+ reason = reason ,
353+ code = code ,
354+ recoverable = recoverable ,
355+ phase = phase ,
356+ ),
357+ self .get_event_loop (),
358+ )
359+
360+ error_message = f"Azure STT recognition canceled: { reason } ({ code } )"
303361 asyncio .run_coroutine_threadsafe (
304362 self .push_error (error_msg = error_message ), self .get_event_loop ()
305363 )
306364
307365 def _on_handle_session_started (self , event ):
366+ self ._recognition_active = True
367+ self ._recognition_terminated = False
308368 logger .info (
309369 "Azure STT session started: session_id={}" ,
310370 getattr (event , "session_id" , "unknown" ),
311371 )
312372
313373 def _on_handle_session_stopped (self , event ):
314- logger .warning (
315- "Azure STT session stopped: session_id={}" ,
316- getattr (event , "session_id" , "unknown" ),
317- )
374+ self ._recognition_active = False
375+ self ._recognition_terminated = True
376+ if self ._shutdown_requested :
377+ logger .info (
378+ "Azure STT session stopped during shutdown: session_id={}" ,
379+ getattr (event , "session_id" , "unknown" ),
380+ )
381+ else :
382+ logger .warning (
383+ "Azure STT session stopped: session_id={}" ,
384+ getattr (event , "session_id" , "unknown" ),
385+ )
386+
387+ @staticmethod
388+ def _normalize_cancellation_value (value : Any ) -> str :
389+ normalized = getattr (value , "name" , None )
390+ if normalized :
391+ return normalized
392+ return str (value )
393+
394+ def _get_cancellation_phase (self ) -> str :
395+ if self ._shutdown_requested :
396+ return "shutdown"
397+ if not self ._recognition_active and not self ._audio_sent :
398+ return "startup"
399+ return "streaming"
400+
401+ @staticmethod
402+ def _is_cancellation_recoverable (reason : str , code : str ) -> bool :
403+ if reason == "CancelledByUser" :
404+ return True
405+ if reason != "Error" :
406+ return False
407+
408+ return code in {
409+ "ConnectionFailure" ,
410+ "ServiceRedirectPermanent" ,
411+ "ServiceRedirectTemporary" ,
412+ "ServiceTimeout" ,
413+ "ServiceUnavailable" ,
414+ "TooManyRequests" ,
415+ }
0 commit comments