nifty-coder/stemsplit-backend
0
1"""2Azure Speech Services provider implementation.3 4This module implements a provider that integrates with Microsoft Azure Speech Services5for high-quality speech recognition. It supports both streaming and batch transcription6with comprehensive quota management for the free tier (5 hours/month).7"""8 9import asyncio10import json11import logging12import base6413from typing import Dict, List, Optional, AsyncIterator, Any14from datetime import datetime, timezone, timedelta15import hashlib16import io17 18from ..interfaces import STTProvider19from ..models import (20 ProviderConfig, 21 TranscriptionResult, 22 QuotaStatus, 23 QuotaType,24 WordTimestamp,25 ProviderType26)27from ..exceptions import (28 ProviderError,29 ProviderUnavailableError,30 ProviderAuthenticationError,31 ProviderTimeoutError,32 UnsupportedFormatError,33 UnsupportedLanguageError,34 QuotaExceededError,35 RateLimitExceededError,36 TranscriptionError37)38 39logger = logging.getLogger(__name__)40 41try:42 import azure.cognitiveservices.speech as speechsdk43 from azure.cognitiveservices.speech import (44 SpeechConfig, 45 AudioConfig, 46 SpeechRecognizer,47 ResultReason,48 CancellationReason49 )50 AZURE_SPEECH_AVAILABLE = True51except ImportError:52 logger.warning("Azure Speech SDK not available. Install with: pip install azure-cognitiveservices-speech")53 AZURE_SPEECH_AVAILABLE = False54 # Create mock classes for type hints55 speechsdk = None56 SpeechConfig = None57 AudioConfig = None58 SpeechRecognizer = None59 ResultReason = None60 CancellationReason = None61 62 63class AzureSpeechProvider(STTProvider):64 """65 Azure Speech Services provider implementation.66 67 This provider integrates with Microsoft Azure Speech Services to provide68 high-quality speech recognition with support for neural voice models,69 multiple languages, streaming transcription, and advanced features.70 71 Key features:72 - Streaming and batch transcription73 - Neural voice model support74 - Free tier quota management (5 hours/month)75 - Multiple audio format support76 - Word-level timestamps77 - Confidence scoring78 - Language detection79 - Speaker diarization80 - Custom speech models81 """82 83 # Supported audio formats for Azure Speech Services84 SUPPORTED_FORMATS = [85 "wav", "flac", "mp3", "ogg", "webm", "amr", "alaw", "mulaw"86 ]87 88 # Supported languages (subset of Azure's extensive language support)89 SUPPORTED_LANGUAGES = [90 "en-US", "en-GB", "en-AU", "en-CA", "en-IN", "en-NZ", "en-ZA", "en-IE",91 "es-ES", "es-MX", "es-AR", "es-CO", "es-CL", "es-PE", "es-VE", "es-US",92 "fr-FR", "fr-CA", "fr-BE", "fr-CH",93 "de-DE", "de-AT", "de-CH",94 "it-IT", "it-CH",95 "pt-BR", "pt-PT",96 "ru-RU",97 "ja-JP",98 "ko-KR",99 "zh-CN", "zh-TW", "zh-HK",100 "ar-SA", "ar-EG", "ar-AE", "ar-BH", "ar-DZ", "ar-IQ", "ar-JO", "ar-KW", "ar-LB", "ar-LY", "ar-MA", "ar-OM", "ar-QA", "ar-SY", "ar-TN", "ar-YE",101 "hi-IN",102 "th-TH",103 "tr-TR",104 "pl-PL",105 "nl-NL", "nl-BE",106 "sv-SE",107 "da-DK",108 "no-NO",109 "fi-FI",110 "cs-CZ",111 "hu-HU",112 "ro-RO",113 "sk-SK",114 "sl-SI",115 "bg-BG",116 "hr-HR",117 "et-EE",118 "lv-LV",119 "lt-LT",120 "mt-MT",121 "uk-UA",122 "vi-VN",123 "id-ID",124 "ms-MY",125 "ta-IN",126 "te-IN",127 "bn-IN",128 "gu-IN",129 "kn-IN",130 "ml-IN",131 "mr-IN",132 "pa-IN",133 "ur-IN"134 ]135 136 # Free tier limits (as of 2024)137 FREE_TIER_LIMITS = {138 "audio_hours_per_month": 5.0, # 5 hours per month139 "requests_per_minute": 20,140 "requests_per_day": 5000141 }142 143 # Pricing (per hour, in USD)144 PRICING = {145 "standard": 1.0, # $1.00 per hour for standard models146 "neural": 2.5 # $2.50 per hour for neural models147 }148 149 def __init__(self, config: ProviderConfig):150 """151 Initialize the Azure Speech Services provider.152 153 Args:154 config: Provider configuration155 156 Raises:157 ImportError: If Azure Speech SDK is not available158 ProviderAuthenticationError: If credentials are invalid159 """160 super().__init__(config)161 162 if not AZURE_SPEECH_AVAILABLE:163 raise ImportError(164 "Azure Speech SDK not available. "165 "Install with: pip install azure-cognitiveservices-speech"166 )167 168 # Validate configuration169 if config.provider_type != ProviderType.AZURE_SPEECH:170 raise ValueError(f"Invalid provider type: {config.provider_type}")171 172 # Initialize speech config173 self._speech_config = None174 self._recognizer = None175 176 # Usage tracking for quota management177 self._usage_stats = {178 "requests_today": 0,179 "requests_this_minute": 0,180 "audio_hours_this_month": 0.0,181 "last_minute_reset": datetime.now(timezone.utc).replace(second=0, microsecond=0),182 "last_daily_reset": datetime.now(timezone.utc).date(),183 "last_monthly_reset": datetime.now(timezone.utc).replace(day=1, hour=0, minute=0, second=0, microsecond=0)184 }185 186 # Streaming session management187 self._active_streams = {}188 189 # Initialize the client (will be done lazily when needed)190 self._client_initialized = False191 192 logger.info(f"Initialized AzureSpeechProvider: {self.name}")193 194 async def _initialize_client(self) -> None:195 """Initialize the Azure Speech client with credentials."""196 try:197 # Get credentials from config198 credentials_info = self.config.api_credentials199 200 if "subscription_key" in credentials_info and "region" in credentials_info:201 # Use subscription key and region202 subscription_key = credentials_info["subscription_key"]203 region = credentials_info["region"]204 205 self._speech_config = SpeechConfig(206 subscription=subscription_key,207 region=region208 )209 elif "endpoint" in credentials_info and "subscription_key" in credentials_info:210 # Use custom endpoint211 endpoint = credentials_info["endpoint"]212 subscription_key = credentials_info["subscription_key"]213 214 self._speech_config = SpeechConfig(215 endpoint=endpoint,216 subscription=subscription_key217 )218 else:219 raise ValueError(220 "Azure Speech credentials must include either "221 "(subscription_key + region) or (endpoint + subscription_key)"222 )223 224 # Configure speech settings225 self._speech_config.speech_recognition_language = "en-US" # Default language226 self._speech_config.output_format = speechsdk.OutputFormat.Detailed227 228 # Enable detailed results for word timestamps229 self._speech_config.request_word_level_timestamps()230 231 # Configure neural voice models if available232 if credentials_info.get("use_neural_models", True):233 # Neural models provide better accuracy but cost more234 pass # Neural models are enabled by default in newer SDK versions235 236 self._client_initialized = True237 logger.info("Azure Speech client initialized successfully")238 239 except Exception as e:240 logger.error(f"Failed to initialize Azure Speech client: {e}")241 raise ProviderAuthenticationError(242 self.name,243 f"Failed to initialize client: {str(e)}"244 )245 246 async def transcribe_audio(247 self, 248 audio_data: bytes, 249 format: str,250 language: str = "en-US",251 **kwargs252 ) -> TranscriptionResult:253 """254 Transcribe audio data using Azure Speech Services.255 256 Args:257 audio_data: Raw audio bytes258 format: Audio format (wav, flac, mp3, etc.)259 language: Language code for transcription260 **kwargs: Additional options (model, enable_word_timestamps, etc.)261 262 Returns:263 TranscriptionResult with transcribed text and metadata264 265 Raises:266 ProviderUnavailableError: If Azure Speech API is unavailable267 UnsupportedFormatError: If audio format is not supported268 UnsupportedLanguageError: If language is not supported269 QuotaExceededError: If quota limits are exceeded270 TranscriptionError: If transcription fails271 """272 start_time = datetime.now(timezone.utc)273 274 # Ensure client is initialized275 if not self._client_initialized:276 await self._initialize_client()277 278 # Validate format and language279 if not self.supports_format(format):280 raise UnsupportedFormatError(self.name, format, self.SUPPORTED_FORMATS)281 282 if not self.supports_language(language):283 raise UnsupportedLanguageError(self.name, language, self.SUPPORTED_LANGUAGES)284 285 # Estimate audio duration and check quota286 audio_duration = kwargs.get('audio_duration', self._estimate_audio_duration(audio_data, format))287 await self._check_quota(audio_duration)288 289 try:290 # Configure speech recognition for this request291 speech_config = self._create_speech_config(language, **kwargs)292 293 # Create audio config from bytes294 audio_config = self._create_audio_config_from_bytes(audio_data, format)295 296 # Create recognizer297 recognizer = SpeechRecognizer(298 speech_config=speech_config,299 audio_config=audio_config300 )301 302 logger.debug(f"Starting Azure Speech transcription for {audio_duration:.2f}s audio")303 304 # Perform recognition305 result = await asyncio.get_event_loop().run_in_executor(306 None,307 lambda: recognizer.recognize_once()308 )309 310 # Process results311 transcription_result = self._process_recognition_result(result, start_time, audio_duration, language)312 313 # Update usage statistics314 await self._update_usage_stats(audio_duration, transcription_result.processing_time)315 316 logger.info(317 f"Azure Speech transcription completed: {len(transcription_result.text)} chars, "318 f"confidence: {transcription_result.confidence:.2f}, time: {transcription_result.processing_time:.2f}s"319 )320 321 return transcription_result322 323 except Exception as e:324 await self._handle_azure_error(e)325 326 async def transcribe_streaming(327 self,328 audio_stream: AsyncIterator[bytes],329 format: str,330 language: str = "en-US",331 **kwargs332 ) -> AsyncIterator[TranscriptionResult]:333 """334 Transcribe streaming audio data using Azure Speech Services.335 336 Args:337 audio_stream: Async iterator of audio chunks338 format: Audio format339 language: Language code for transcription340 **kwargs: Additional options341 342 Yields:343 TranscriptionResult for each processed chunk344 """345 # Ensure client is initialized346 if not self._client_initialized:347 await self._initialize_client()348 349 # Validate format and language350 if not self.supports_format(format):351 raise UnsupportedFormatError(self.name, format, self.SUPPORTED_FORMATS)352 353 if not self.supports_language(language):354 raise UnsupportedLanguageError(self.name, language, self.SUPPORTED_LANGUAGES)355 356 session_id = kwargs.get('session_id', f"azure_stream_{int(datetime.now().timestamp())}")357 358 try:359 # Initialize streaming session360 self._active_streams[session_id] = {361 "start_time": datetime.now(timezone.utc),362 "language": language,363 "format": format,364 "chunk_count": 0,365 "total_duration": 0.0366 }367 368 logger.info(f"Started Azure Speech streaming session: {session_id}")369 370 # Configure speech recognition for streaming371 speech_config = self._create_speech_config(language, **kwargs)372 373 # Create push audio input stream374 push_stream = speechsdk.audio.PushAudioInputStream()375 audio_config = speechsdk.audio.AudioConfig(stream=push_stream)376 377 # Create recognizer378 recognizer = SpeechRecognizer(379 speech_config=speech_config,380 audio_config=audio_config381 )382 383 # Set up event handlers for streaming results384 results_queue = asyncio.Queue()385 386 def recognized_handler(evt):387 """Handle recognized speech events."""388 if evt.result.reason == ResultReason.RecognizedSpeech:389 asyncio.create_task(results_queue.put(evt.result))390 391 def recognizing_handler(evt):392 """Handle interim recognition results."""393 if evt.result.reason == ResultReason.RecognizingSpeech:394 asyncio.create_task(results_queue.put(evt.result))395 396 recognizer.recognized.connect(recognized_handler)397 recognizer.recognizing.connect(recognizing_handler)398 399 # Start continuous recognition400 recognizer.start_continuous_recognition()401 402 # Process audio chunks403 async for audio_chunk in audio_stream:404 try:405 # Estimate chunk duration406 chunk_duration = self._estimate_audio_duration(audio_chunk, format)407 408 # Check quota for this chunk409 await self._check_quota(chunk_duration)410 411 # Push audio data to stream412 push_stream.write(audio_chunk)413 414 # Update session stats415 session = self._active_streams[session_id]416 session["chunk_count"] += 1417 session["total_duration"] += chunk_duration418 419 # Check for results (non-blocking)420 try:421 while True:422 result = results_queue.get_nowait()423 transcription_result = self._process_streaming_result(424 result, session_id, language425 )426 if transcription_result:427 yield transcription_result428 except asyncio.QueueEmpty:429 pass430 431 except Exception as e:432 logger.error(f"Error processing streaming chunk: {e}")433 continue434 435 # Close the stream and stop recognition436 push_stream.close()437 recognizer.stop_continuous_recognition()438 439 # Process any remaining results440 await asyncio.sleep(0.5) # Allow time for final results441 try:442 while True:443 result = results_queue.get_nowait()444 transcription_result = self._process_streaming_result(445 result, session_id, language446 )447 if transcription_result:448 yield transcription_result449 except asyncio.QueueEmpty:450 pass451 452 except Exception as e:453 await self._handle_azure_error(e)454 finally:455 # Clean up session456 if session_id in self._active_streams:457 session = self._active_streams.pop(session_id)458 total_duration = session["total_duration"]459 processing_time = (datetime.now(timezone.utc) - session["start_time"]).total_seconds()460 461 await self._update_usage_stats(total_duration, processing_time)462 logger.info(f"Completed Azure Speech streaming session: {session_id}, duration: {total_duration:.2f}s")463 464 async def check_health(self) -> bool:465 """466 Check if the Azure Speech Services are available and healthy.467 468 Returns:469 True if provider is healthy, False otherwise470 """471 try:472 if not self._client_initialized:473 await self._initialize_client()474 475 # Perform a simple health check with minimal audio476 test_audio = b'\x00' * 1000 # Silent audio477 478 # Create a simple speech config for health check479 speech_config = SpeechConfig(480 subscription=self.config.api_credentials["subscription_key"],481 region=self.config.api_credentials["region"]482 )483 speech_config.speech_recognition_language = "en-US"484 485 # Create audio config from test data486 audio_config = self._create_audio_config_from_bytes(test_audio, "wav")487 488 # Create recognizer and test489 recognizer = SpeechRecognizer(490 speech_config=speech_config,491 audio_config=audio_config492 )493 494 # This should complete quickly even if it doesn't recognize anything495 result = await asyncio.get_event_loop().run_in_executor(496 None,497 lambda: recognizer.recognize_once()498 )499 500 # Any result (including no speech) indicates the service is working501 return True502 503 except Exception as e:504 logger.error(f"Azure Speech health check failed: {e}")505 return False506 507 async def get_quota_status(self) -> Dict[str, QuotaStatus]:508 """509 Get current quota usage status for Azure Speech Services.510 511 Returns:512 Dictionary mapping quota types to their status513 """514 await self._reset_counters_if_needed()515 516 now = datetime.now(timezone.utc)517 518 return {519 "requests_per_minute": QuotaStatus(520 provider=self.name,521 quota_type=QuotaType.REQUESTS_PER_MINUTE,522 current_usage=self._usage_stats["requests_this_minute"],523 limit=self.FREE_TIER_LIMITS["requests_per_minute"],524 remaining=max(0, self.FREE_TIER_LIMITS["requests_per_minute"] - self._usage_stats["requests_this_minute"]),525 reset_time=now.replace(second=0, microsecond=0) + timedelta(minutes=1),526 percentage_used=min(1.0, self._usage_stats["requests_this_minute"] / self.FREE_TIER_LIMITS["requests_per_minute"])527 ),528 "requests_per_day": QuotaStatus(529 provider=self.name,530 quota_type=QuotaType.REQUESTS_PER_DAY,531 current_usage=self._usage_stats["requests_today"],532 limit=self.FREE_TIER_LIMITS["requests_per_day"],533 remaining=max(0, self.FREE_TIER_LIMITS["requests_per_day"] - self._usage_stats["requests_today"]),534 reset_time=now.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1),535 percentage_used=min(1.0, self._usage_stats["requests_today"] / self.FREE_TIER_LIMITS["requests_per_day"])536 ),537 "audio_hours_per_month": QuotaStatus(538 provider=self.name,539 quota_type=QuotaType.AUDIO_MINUTES_PER_MONTH, # Using minutes enum for hours540 current_usage=self._usage_stats["audio_hours_this_month"],541 limit=self.FREE_TIER_LIMITS["audio_hours_per_month"],542 remaining=max(0, self.FREE_TIER_LIMITS["audio_hours_per_month"] - self._usage_stats["audio_hours_this_month"]),543 reset_time=self._get_next_month_start(),544 percentage_used=min(1.0, self._usage_stats["audio_hours_this_month"] / self.FREE_TIER_LIMITS["audio_hours_per_month"])545 )546 }547 548 def supports_format(self, format: str) -> bool:549 """550 Check if the provider supports a given audio format.551 552 Args:553 format: Audio format to check554 555 Returns:556 True if format is supported, False otherwise557 """558 return format.lower() in [f.lower() for f in self.SUPPORTED_FORMATS]559 560 def supports_language(self, language: str) -> bool:561 """562 Check if the provider supports a given language.563 564 Args:565 language: Language code to check566 567 Returns:568 True if language is supported, False otherwise569 """570 return language in self.SUPPORTED_LANGUAGES571 572 async def estimate_cost(self, audio_duration: float) -> float:573 """574 Estimate the cost for transcribing audio of given duration.575 576 Args:577 audio_duration: Duration in seconds578 579 Returns:580 Estimated cost in USD581 """582 hours = audio_duration / 3600.0583 584 # Check if within free tier585 current_usage = self._usage_stats["audio_hours_this_month"]586 free_remaining = max(0, self.FREE_TIER_LIMITS["audio_hours_per_month"] - current_usage)587 588 if hours <= free_remaining:589 return 0.0 # Within free tier590 591 # Calculate cost for paid usage592 paid_hours = hours - free_remaining593 model_type = "neural" # Default to neural pricing (higher quality)594 595 return paid_hours * self.PRICING[model_type]596 597 # Private methods598 599 def _create_speech_config(self, language: str, **kwargs) -> 'SpeechConfig':600 """Create speech configuration for recognition."""601 # Clone the base config602 speech_config = SpeechConfig(603 subscription=self.config.api_credentials["subscription_key"],604 region=self.config.api_credentials["region"]605 )606 607 # Set language608 speech_config.speech_recognition_language = language609 610 # Configure output format for detailed results611 speech_config.output_format = speechsdk.OutputFormat.Detailed612 613 # Enable word-level timestamps if requested614 if kwargs.get('enable_word_timestamps', True):615 speech_config.request_word_level_timestamps()616 617 # Configure profanity filter618 if kwargs.get('profanity_filter', False):619 speech_config.set_profanity(speechsdk.ProfanityOption.Masked)620 else:621 speech_config.set_profanity(speechsdk.ProfanityOption.Raw)622 623 # Configure custom models if specified624 if 'custom_model_id' in kwargs:625 speech_config.endpoint_id = kwargs['custom_model_id']626 627 return speech_config628 629 def _create_audio_config_from_bytes(self, audio_data: bytes, format: str) -> 'AudioConfig':630 """Create audio configuration from audio bytes."""631 # Create a stream from the audio data632 stream = speechsdk.audio.PushAudioInputStream()633 634 # Write the audio data to the stream635 stream.write(audio_data)636 stream.close()637 638 # Create audio config from the stream639 audio_config = speechsdk.audio.AudioConfig(stream=stream)640 641 return audio_config642 643 def _process_recognition_result(644 self, 645 result, 646 start_time: datetime, 647 audio_duration: float, 648 language: str649 ) -> TranscriptionResult:650 """Process batch recognition result."""651 processing_time = (datetime.now(timezone.utc) - start_time).total_seconds()652 653 if result.reason == ResultReason.RecognizedSpeech:654 # Parse detailed results if available655 detailed_result = json.loads(result.json) if hasattr(result, 'json') and result.json else {}656 657 # Extract confidence score658 confidence = 0.0659 if 'NBest' in detailed_result and detailed_result['NBest']:660 confidence = detailed_result['NBest'][0].get('Confidence', 0.0)661 662 # Extract word timestamps663 word_timestamps = self._extract_word_timestamps(detailed_result)664 665 # Extract alternatives666 alternatives = []667 if 'NBest' in detailed_result and len(detailed_result['NBest']) > 1:668 alternatives = [item.get('Display', '') for item in detailed_result['NBest'][1:]]669 670 return TranscriptionResult(671 text=result.text,672 confidence=confidence,673 provider=self.name,674 processing_time=processing_time,675 audio_duration=audio_duration,676 language=language,677 alternatives=alternatives,678 word_timestamps=word_timestamps,679 is_final=True680 )681 682 elif result.reason == ResultReason.NoMatch:683 return TranscriptionResult(684 text="",685 confidence=0.0,686 provider=self.name,687 processing_time=processing_time,688 audio_duration=audio_duration,689 language=language,690 alternatives=[],691 word_timestamps=None,692 is_final=True693 )694 695 else:696 # Handle cancellation or error697 error_details = result.cancellation_details if hasattr(result, 'cancellation_details') else None698 error_msg = f"Recognition failed: {error_details.reason if error_details else 'Unknown error'}"699 700 if error_details and error_details.reason == CancellationReason.Error:701 error_msg += f" - {error_details.error_details}"702 703 raise TranscriptionError(error_msg)704 705 def _process_streaming_result(706 self, 707 result, 708 session_id: str, 709 language: str710 ) -> Optional[TranscriptionResult]:711 """Process streaming recognition result."""712 if result.reason in [ResultReason.RecognizedSpeech, ResultReason.RecognizingSpeech]:713 is_final = result.reason == ResultReason.RecognizedSpeech714 715 # Parse detailed results if available716 detailed_result = json.loads(result.json) if hasattr(result, 'json') and result.json else {}717 718 # Extract confidence score719 confidence = 0.0720 if 'NBest' in detailed_result and detailed_result['NBest']:721 confidence = detailed_result['NBest'][0].get('Confidence', 0.0)722 723 # For interim results, confidence is typically lower724 if not is_final:725 confidence *= 0.8 # Reduce confidence for interim results726 727 return TranscriptionResult(728 text=result.text,729 confidence=confidence,730 provider=self.name,731 processing_time=0.1, # Streaming has minimal processing time732 audio_duration=0.0, # Will be updated by session tracking733 language=language,734 alternatives=[],735 word_timestamps=self._extract_word_timestamps(detailed_result) if is_final else None,736 is_final=is_final,737 session_id=session_id738 )739 740 return None741 742 def _extract_word_timestamps(self, detailed_result: Dict[str, Any]) -> List[WordTimestamp]:743 """Extract word timestamps from detailed recognition result."""744 if not detailed_result or 'NBest' not in detailed_result or not detailed_result['NBest']:745 return []746 747 best_result = detailed_result['NBest'][0]748 if 'Words' not in best_result:749 return []750 751 timestamps = []752 try:753 for word_info in best_result['Words']:754 # Azure returns timestamps in 100-nanosecond units755 start_time = word_info.get('Offset', 0) / 10_000_000.0 # Convert to seconds756 duration = word_info.get('Duration', 0) / 10_000_000.0 # Convert to seconds757 end_time = start_time + duration758 759 timestamps.append(WordTimestamp(760 word=word_info.get('Word', ''),761 start_time=start_time,762 end_time=end_time,763 confidence=word_info.get('Confidence', 0.0)764 ))765 except (TypeError, KeyError, ValueError) as e:766 logger.warning(f"Error extracting word timestamps: {e}")767 return []768 769 return timestamps770 771 def _estimate_audio_duration(self, audio_data: bytes, format: str) -> float:772 """Estimate audio duration from data size and format."""773 # This is a rough estimation - in production, you'd want more accurate duration detection774 if format.lower() in ["wav", "flac"]:775 # Assume 16-bit, 16kHz mono for estimation776 return len(audio_data) / (16000 * 2)777 elif format.lower() in ["mp3", "ogg", "webm"]:778 # Compressed formats - rough estimation779 return len(audio_data) / 8000 # Assume ~64kbps compression780 else:781 # Default estimation782 return len(audio_data) / 16000783 784 async def _check_quota(self, audio_duration: float) -> None:785 """Check if request would exceed quota limits."""786 await self._reset_counters_if_needed()787 788 # Check requests per minute789 if self._usage_stats["requests_this_minute"] >= self.FREE_TIER_LIMITS["requests_per_minute"]:790 raise RateLimitExceededError(791 self.name,792 "requests_per_minute",793 retry_after=60.0794 )795 796 # Check requests per day797 if self._usage_stats["requests_today"] >= self.FREE_TIER_LIMITS["requests_per_day"]:798 raise RateLimitExceededError(799 self.name,800 "requests_per_day",801 retry_after=86400.0 # 24 hours802 )803 804 # Check audio hours per month805 audio_hours = audio_duration / 3600.0806 if self._usage_stats["audio_hours_this_month"] + audio_hours > self.FREE_TIER_LIMITS["audio_hours_per_month"]:807 raise QuotaExceededError(808 self.name,809 "audio_hours_per_month",810 self._usage_stats["audio_hours_this_month"] + audio_hours,811 self.FREE_TIER_LIMITS["audio_hours_per_month"]812 )813 814 async def _update_usage_stats(self, audio_duration: float, processing_time: float) -> None:815 """Update usage statistics after successful request."""816 await self._reset_counters_if_needed()817 818 self._usage_stats["requests_this_minute"] += 1819 self._usage_stats["requests_today"] += 1820 self._usage_stats["audio_hours_this_month"] += audio_duration / 3600.0821 822 logger.debug(823 f"Updated Azure Speech usage: {self._usage_stats['requests_today']} requests today, "824 f"{self._usage_stats['audio_hours_this_month']:.2f} hours this month"825 )826 827 async def _reset_counters_if_needed(self) -> None:828 """Reset usage counters based on time windows."""829 now = datetime.now(timezone.utc)830 831 # Reset minute counter832 current_minute = now.replace(second=0, microsecond=0)833 if current_minute > self._usage_stats["last_minute_reset"]:834 self._usage_stats["requests_this_minute"] = 0835 self._usage_stats["last_minute_reset"] = current_minute836 logger.debug("Reset Azure Speech minute counter")837 838 # Reset daily counter839 current_date = now.date()840 if current_date > self._usage_stats["last_daily_reset"]:841 self._usage_stats["requests_today"] = 0842 self._usage_stats["last_daily_reset"] = current_date843 logger.info("Reset Azure Speech daily counter")844 845 # Reset monthly counter846 current_month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)847 if current_month_start > self._usage_stats["last_monthly_reset"]:848 self._usage_stats["audio_hours_this_month"] = 0.0849 self._usage_stats["last_monthly_reset"] = current_month_start850 logger.info("Reset Azure Speech monthly counter")851 852 def _get_next_month_start(self) -> datetime:853 """Get the start of next month."""854 now = datetime.now(timezone.utc)855 if now.month == 12:856 return now.replace(year=now.year + 1, month=1, day=1, hour=0, minute=0, second=0, microsecond=0)857 else:858 return now.replace(month=now.month + 1, day=1, hour=0, minute=0, second=0, microsecond=0)859 860 async def _handle_azure_error(self, error: Exception) -> None:861 """Handle Azure Speech Services specific errors."""862 error_str = str(error).lower()863 864 if "unauthorized" in error_str or "authentication" in error_str:865 raise ProviderAuthenticationError(866 self.name,867 f"Authentication failed: {str(error)}"868 )869 elif "quota" in error_str or "rate limit" in error_str or "throttl" in error_str:870 raise RateLimitExceededError(871 self.name,872 retry_after=60.0873 )874 elif "timeout" in error_str or "timed out" in error_str:875 raise ProviderTimeoutError(876 self.name,877 f"Request timeout: {str(error)}"878 )879 elif "unavailable" in error_str or "service" in error_str:880 raise ProviderUnavailableError(881 self.name,882 f"Service unavailable: {str(error)}"883 )884 else:885 # Generic provider error886 logger.error(f"Azure Speech error: {error}")887 raise TranscriptionError(f"Azure Speech transcription failed: {str(error)}")888 889 890def create_azure_speech_provider(891 name: str = "azure_speech",892 priority: int = 3,893 **config_overrides894) -> AzureSpeechProvider:895 """896 Factory function to create an AzureSpeechProvider with default configuration.897 898 Args:899 name: Provider name900 priority: Provider priority (lower = higher priority)901 **config_overrides: Configuration overrides902 903 Returns:904 Configured AzureSpeechProvider instance905 """906 default_config = {907 "name": name,908 "provider_type": ProviderType.AZURE_SPEECH,909 "enabled": True,910 "priority": priority,911 "free_tier_limits": AzureSpeechProvider.FREE_TIER_LIMITS,912 "rate_limits": {913 "requests_per_minute": AzureSpeechProvider.FREE_TIER_LIMITS["requests_per_minute"],914 "requests_per_day": AzureSpeechProvider.FREE_TIER_LIMITS["requests_per_day"]915 },916 "supported_formats": AzureSpeechProvider.SUPPORTED_FORMATS,917 "supported_languages": AzureSpeechProvider.SUPPORTED_LANGUAGES,918 "cost_per_minute": AzureSpeechProvider.PRICING["neural"] / 60.0, # Convert hourly to per-minute919 "api_credentials": {}, # Must be provided by user920 "endpoint_url": None, # Uses default Azure endpoint921 "timeout_seconds": 60,922 "max_retries": 3923 }924 925 # Apply overrides926 default_config.update(config_overrides)927 928 # Create configuration object929 config = ProviderConfig(**default_config)930 931 return AzureSpeechProvider(config)