CoolFace
Apppublic

nifty-coder/stemsplit-backend

sourceHugging Faceupdated 8mo agoView on Hugging Face
0likes
azure_speech_provider.py931 linesDownload Raw Back to providers
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)