CoolFace
Apppublic

nifty-coder/stemsplit-backend

sourceHugging Faceupdated 8mo agoView on Hugging Face
0likes
google_speech_provider.py838 linesDownload Raw Back to providers
1"""2Google Speech-to-Text provider implementation.3 4This module implements a provider that integrates with Google Cloud Speech-to-Text API5for high-quality speech recognition. It supports both streaming and batch transcription6with comprehensive quota management for the free tier (60 minutes/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    from google.cloud import speech43    from google.cloud.speech import RecognitionConfig, StreamingRecognitionConfig44    from google.oauth2 import service_account45    from google.api_core import exceptions as google_exceptions46    GOOGLE_SPEECH_AVAILABLE = True47except ImportError:48    logger.warning("Google Cloud Speech library not available. Install with: pip install google-cloud-speech")49    GOOGLE_SPEECH_AVAILABLE = False50    # Create mock classes for type hints51    speech = None52    RecognitionConfig = None53    StreamingRecognitionConfig = None54    service_account = None55    google_exceptions = None56 57 58class GoogleSpeechProvider(STTProvider):59    """60    Google Cloud Speech-to-Text provider implementation.61    62    This provider integrates with Google Cloud Speech-to-Text API to provide63    high-quality speech recognition with support for multiple languages,64    streaming transcription, and advanced features like word timestamps.65    66    Key features:67    - Streaming and batch transcription68    - Free tier quota management (60 minutes/month)69    - Multiple audio format support70    - Word-level timestamps71    - Confidence scoring72    - Language detection73    - Noise robustness74    """75    76    # Supported audio formats for Google Speech-to-Text77    SUPPORTED_FORMATS = [78        "wav", "flac", "mp3", "ogg", "webm", "amr", "amr-wb"79    ]80    81    # Supported languages (subset of Google's extensive language support)82    SUPPORTED_LANGUAGES = [83        "en-US", "en-GB", "en-AU", "en-CA", "en-IN", "en-NZ", "en-ZA",84        "es-ES", "es-MX", "es-AR", "es-CO", "es-CL", "es-PE", "es-VE",85        "fr-FR", "fr-CA", "fr-BE", "fr-CH",86        "de-DE", "de-AT", "de-CH",87        "it-IT", "it-CH",88        "pt-BR", "pt-PT",89        "ru-RU",90        "ja-JP",91        "ko-KR",92        "zh-CN", "zh-TW", "zh-HK",93        "ar-SA", "ar-EG",94        "hi-IN",95        "th-TH",96        "tr-TR",97        "pl-PL",98        "nl-NL", "nl-BE",99        "sv-SE",100        "da-DK",101        "no-NO",102        "fi-FI",103        "cs-CZ",104        "hu-HU",105        "ro-RO",106        "sk-SK",107        "sl-SI",108        "bg-BG",109        "hr-HR",110        "et-EE",111        "lv-LV",112        "lt-LT",113        "mt-MT"114    ]115    116    # Free tier limits (as of 2024)117    FREE_TIER_LIMITS = {118        "audio_minutes_per_month": 60.0,119        "requests_per_minute": 1000,120        "requests_per_day": 50000121    }122    123    # Pricing (per minute, in USD)124    PRICING = {125        "standard": 0.006,  # $0.006 per minute for standard models126        "enhanced": 0.009   # $0.009 per minute for enhanced models127    }128    129    def __init__(self, config: ProviderConfig):130        """131        Initialize the Google Speech-to-Text provider.132        133        Args:134            config: Provider configuration135            136        Raises:137            ImportError: If Google Cloud Speech library is not available138            ProviderAuthenticationError: If credentials are invalid139        """140        super().__init__(config)141        142        if not GOOGLE_SPEECH_AVAILABLE:143            raise ImportError(144                "Google Cloud Speech library not available. "145                "Install with: pip install google-cloud-speech"146            )147        148        # Validate configuration149        if config.provider_type != ProviderType.GOOGLE_SPEECH:150            raise ValueError(f"Invalid provider type: {config.provider_type}")151        152        # Initialize client153        self._client = None154        self._streaming_client = None155        self._credentials = None156        157        # Usage tracking for quota management158        self._usage_stats = {159            "requests_today": 0,160            "requests_this_minute": 0,161            "audio_minutes_this_month": 0.0,162            "last_minute_reset": datetime.now(timezone.utc).replace(second=0, microsecond=0),163            "last_daily_reset": datetime.now(timezone.utc).date(),164            "last_monthly_reset": datetime.now(timezone.utc).replace(day=1, hour=0, minute=0, second=0, microsecond=0)165        }166        167        # Streaming session management168        self._active_streams = {}169        170        # Initialize the client (will be done lazily when needed)171        self._client_initialized = False172        173        logger.info(f"Initialized GoogleSpeechProvider: {self.name}")174    175    async def _initialize_client(self) -> None:176        """Initialize the Google Speech client with credentials."""177        try:178            # Get credentials from config179            credentials_info = self.config.api_credentials180            181            if "service_account_key" in credentials_info:182                # Use service account key (JSON string or file path)183                key_data = credentials_info["service_account_key"]184                if isinstance(key_data, str) and key_data.startswith("{"):185                    # JSON string186                    import json187                    key_info = json.loads(key_data)188                    self._credentials = service_account.Credentials.from_service_account_info(key_info)189                else:190                    # File path191                    self._credentials = service_account.Credentials.from_service_account_file(key_data)192            elif "api_key" in credentials_info:193                # Use API key (less secure, not recommended for production)194                logger.warning("Using API key authentication. Service account is recommended for production.")195                # For API key, we'll use default credentials and set the key in environment196                import os197                os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = credentials_info["api_key"]198                self._credentials = None199            else:200                # Use default credentials (from environment or metadata server)201                logger.info("Using default Google Cloud credentials")202                self._credentials = None203            204            # Create clients205            if self._credentials:206                self._client = speech.SpeechClient(credentials=self._credentials)207                self._streaming_client = speech.SpeechClient(credentials=self._credentials)208            else:209                self._client = speech.SpeechClient()210                self._streaming_client = speech.SpeechClient()211            212            self._client_initialized = True213            logger.info("Google Speech client initialized successfully")214            215        except Exception as e:216            logger.error(f"Failed to initialize Google Speech client: {e}")217            raise ProviderAuthenticationError(218                self.name,219                f"Failed to initialize client: {str(e)}"220            )221    222    async def transcribe_audio(223        self, 224        audio_data: bytes, 225        format: str,226        language: str = "en-US",227        **kwargs228    ) -> TranscriptionResult:229        """230        Transcribe audio data using Google Speech-to-Text API.231        232        Args:233            audio_data: Raw audio bytes234            format: Audio format (wav, flac, mp3, etc.)235            language: Language code for transcription236            **kwargs: Additional options (model, enable_word_time_offsets, etc.)237            238        Returns:239            TranscriptionResult with transcribed text and metadata240            241        Raises:242            ProviderUnavailableError: If Google Speech API is unavailable243            UnsupportedFormatError: If audio format is not supported244            UnsupportedLanguageError: If language is not supported245            QuotaExceededError: If quota limits are exceeded246            TranscriptionError: If transcription fails247        """248        start_time = datetime.now(timezone.utc)249        250        # Ensure client is initialized251        if not self._client_initialized:252            await self._initialize_client()253        254        # Validate format and language255        if not self.supports_format(format):256            raise UnsupportedFormatError(self.name, format, self.SUPPORTED_FORMATS)257        258        if not self.supports_language(language):259            raise UnsupportedLanguageError(self.name, language, self.SUPPORTED_LANGUAGES)260        261        # Estimate audio duration and check quota262        audio_duration = kwargs.get('audio_duration', self._estimate_audio_duration(audio_data, format))263        await self._check_quota(audio_duration)264        265        try:266            # Prepare recognition config267            config = self._create_recognition_config(format, language, **kwargs)268            269            # Prepare audio270            audio = speech.RecognitionAudio(content=audio_data)271            272            # Perform transcription273            logger.debug(f"Starting Google Speech transcription for {audio_duration:.2f}s audio")274            275            response = await asyncio.get_event_loop().run_in_executor(276                None,277                lambda: self._client.recognize(config=config, audio=audio)278            )279            280            # Process results281            result = self._process_recognition_response(response, start_time, audio_duration, language)282            283            # Update usage statistics284            await self._update_usage_stats(audio_duration, result.processing_time)285            286            logger.info(287                f"Google Speech transcription completed: {len(result.text)} chars, "288                f"confidence: {result.confidence:.2f}, time: {result.processing_time:.2f}s"289            )290            291            return result292            293        except Exception as e:294            # Check if it's a Google API error by checking attributes295            if hasattr(e, 'code'):296                await self._handle_google_api_error(e)297            else:298                logger.error(f"Google Speech transcription failed: {e}")299                raise TranscriptionError(f"Transcription failed: {str(e)}")300    301    async def transcribe_streaming(302        self,303        audio_stream: AsyncIterator[bytes],304        format: str,305        language: str = "en-US",306        **kwargs307    ) -> AsyncIterator[TranscriptionResult]:308        """309        Transcribe streaming audio data using Google Speech-to-Text API.310        311        Args:312            audio_stream: Async iterator of audio chunks313            format: Audio format314            language: Language code for transcription315            **kwargs: Additional options316            317        Yields:318            TranscriptionResult for each processed chunk319        """320        # Ensure client is initialized321        if not self._client_initialized:322            await self._initialize_client()323        324        # Validate format and language325        if not self.supports_format(format):326            raise UnsupportedFormatError(self.name, format, self.SUPPORTED_FORMATS)327        328        if not self.supports_language(language):329            raise UnsupportedLanguageError(self.name, language, self.SUPPORTED_LANGUAGES)330        331        session_id = kwargs.get('session_id', f"gs_stream_{int(datetime.now().timestamp())}")332        333        try:334            # Initialize streaming session335            self._active_streams[session_id] = {336                "start_time": datetime.now(timezone.utc),337                "language": language,338                "format": format,339                "chunk_count": 0,340                "total_duration": 0.0341            }342            343            logger.info(f"Started Google Speech streaming session: {session_id}")344            345            # Create streaming config346            config = self._create_streaming_config(format, language, **kwargs)347            348            # Create streaming request generator349            requests = self._create_streaming_requests(audio_stream, config, session_id)350            351            # Start streaming recognition352            responses = await asyncio.get_event_loop().run_in_executor(353                None,354                lambda: self._streaming_client.streaming_recognize(requests)355            )356            357            # Process streaming responses358            async for response in self._process_streaming_responses(responses, session_id):359                yield response360                361        except Exception as e:362            # Check if it's a Google API error by checking attributes363            if hasattr(e, 'code'):364                await self._handle_google_api_error(e)365            else:366                logger.error(f"Google Speech streaming failed: {e}")367                raise TranscriptionError(f"Streaming transcription failed: {str(e)}")368        finally:369            # Clean up session370            if session_id in self._active_streams:371                session = self._active_streams.pop(session_id)372                total_duration = session["total_duration"]373                processing_time = (datetime.now(timezone.utc) - session["start_time"]).total_seconds()374                375                await self._update_usage_stats(total_duration, processing_time)376                logger.info(f"Completed Google Speech streaming session: {session_id}, duration: {total_duration:.2f}s")377    378    async def check_health(self) -> bool:379        """380        Check if the Google Speech API is available and healthy.381        382        Returns:383            True if provider is healthy, False otherwise384        """385        try:386            if not self._client_initialized:387                await self._initialize_client()388            389            # Perform a simple health check with minimal audio390            test_audio = b'\x00' * 1000  # Silent audio391            config = speech.RecognitionConfig(392                encoding=speech.RecognitionConfig.AudioEncoding.LINEAR16,393                sample_rate_hertz=16000,394                language_code="en-US"395            )396            audio = speech.RecognitionAudio(content=test_audio)397            398            # This should complete quickly even if it doesn't recognize anything399            await asyncio.get_event_loop().run_in_executor(400                None,401                lambda: self._client.recognize(config=config, audio=audio)402            )403            404            return True405            406        except Exception as e:407            logger.error(f"Google Speech health check failed: {e}")408            return False409    410    async def get_quota_status(self) -> Dict[str, QuotaStatus]:411        """412        Get current quota usage status for Google Speech API.413        414        Returns:415            Dictionary mapping quota types to their status416        """417        await self._reset_counters_if_needed()418        419        now = datetime.now(timezone.utc)420        421        return {422            "requests_per_minute": QuotaStatus(423                provider=self.name,424                quota_type=QuotaType.REQUESTS_PER_MINUTE,425                current_usage=self._usage_stats["requests_this_minute"],426                limit=self.FREE_TIER_LIMITS["requests_per_minute"],427                remaining=max(0, self.FREE_TIER_LIMITS["requests_per_minute"] - self._usage_stats["requests_this_minute"]),428                reset_time=now.replace(second=0, microsecond=0) + timedelta(minutes=1),429                percentage_used=min(1.0, self._usage_stats["requests_this_minute"] / self.FREE_TIER_LIMITS["requests_per_minute"])430            ),431            "requests_per_day": QuotaStatus(432                provider=self.name,433                quota_type=QuotaType.REQUESTS_PER_DAY,434                current_usage=self._usage_stats["requests_today"],435                limit=self.FREE_TIER_LIMITS["requests_per_day"],436                remaining=max(0, self.FREE_TIER_LIMITS["requests_per_day"] - self._usage_stats["requests_today"]),437                reset_time=now.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1),438                percentage_used=min(1.0, self._usage_stats["requests_today"] / self.FREE_TIER_LIMITS["requests_per_day"])439            ),440            "audio_minutes_per_month": QuotaStatus(441                provider=self.name,442                quota_type=QuotaType.AUDIO_MINUTES_PER_MONTH,443                current_usage=self._usage_stats["audio_minutes_this_month"],444                limit=self.FREE_TIER_LIMITS["audio_minutes_per_month"],445                remaining=max(0, self.FREE_TIER_LIMITS["audio_minutes_per_month"] - self._usage_stats["audio_minutes_this_month"]),446                reset_time=self._get_next_month_start(),447                percentage_used=min(1.0, self._usage_stats["audio_minutes_this_month"] / self.FREE_TIER_LIMITS["audio_minutes_per_month"])448            )449        }450    451    def supports_format(self, format: str) -> bool:452        """453        Check if the provider supports a given audio format.454        455        Args:456            format: Audio format to check457            458        Returns:459            True if format is supported, False otherwise460        """461        return format.lower() in [f.lower() for f in self.SUPPORTED_FORMATS]462    463    def supports_language(self, language: str) -> bool:464        """465        Check if the provider supports a given language.466        467        Args:468            language: Language code to check469            470        Returns:471            True if language is supported, False otherwise472        """473        return language in self.SUPPORTED_LANGUAGES474    475    async def estimate_cost(self, audio_duration: float) -> float:476        """477        Estimate the cost for transcribing audio of given duration.478        479        Args:480            audio_duration: Duration in seconds481            482        Returns:483            Estimated cost in USD484        """485        minutes = audio_duration / 60.0486        487        # Check if within free tier488        current_usage = self._usage_stats["audio_minutes_this_month"]489        free_remaining = max(0, self.FREE_TIER_LIMITS["audio_minutes_per_month"] - current_usage)490        491        if minutes <= free_remaining:492            return 0.0  # Within free tier493        494        # Calculate cost for paid usage495        paid_minutes = minutes - free_remaining496        model_type = "standard"  # Default to standard pricing497        498        return paid_minutes * self.PRICING[model_type]499    500    # Private methods501    502    def _create_recognition_config(self, format: str, language: str, **kwargs) -> 'RecognitionConfig':503        """Create recognition configuration for batch transcription."""504        # Map format to Google Speech encoding505        encoding_map = {506            "wav": speech.RecognitionConfig.AudioEncoding.LINEAR16,507            "flac": speech.RecognitionConfig.AudioEncoding.FLAC,508            "mp3": speech.RecognitionConfig.AudioEncoding.MP3,509            "ogg": speech.RecognitionConfig.AudioEncoding.OGG_OPUS,510            "webm": speech.RecognitionConfig.AudioEncoding.WEBM_OPUS,511            "amr": speech.RecognitionConfig.AudioEncoding.AMR,512            "amr-wb": speech.RecognitionConfig.AudioEncoding.AMR_WB513        }514        515        encoding = encoding_map.get(format.lower(), speech.RecognitionConfig.AudioEncoding.LINEAR16)516        517        config = speech.RecognitionConfig(518            encoding=encoding,519            sample_rate_hertz=kwargs.get('sample_rate', 16000),520            language_code=language,521            enable_word_time_offsets=kwargs.get('enable_word_time_offsets', True),522            enable_automatic_punctuation=kwargs.get('enable_automatic_punctuation', True),523            max_alternatives=kwargs.get('max_alternatives', 1),524            profanity_filter=kwargs.get('profanity_filter', False),525            speech_contexts=kwargs.get('speech_contexts', []),526            enable_speaker_diarization=kwargs.get('enable_speaker_diarization', False),527            diarization_speaker_count=kwargs.get('diarization_speaker_count', 2),528            model=kwargs.get('model', 'latest_long')529        )530        531        return config532    533    def _create_streaming_config(self, format: str, language: str, **kwargs) -> 'StreamingRecognitionConfig':534        """Create streaming recognition configuration."""535        recognition_config = self._create_recognition_config(format, language, **kwargs)536        537        streaming_config = speech.StreamingRecognitionConfig(538            config=recognition_config,539            interim_results=kwargs.get('interim_results', True),540            single_utterance=kwargs.get('single_utterance', False)541        )542        543        return streaming_config544    545    async def _create_streaming_requests(546        self, 547        audio_stream: AsyncIterator[bytes], 548        config: 'StreamingRecognitionConfig',549        session_id: str550    ) -> AsyncIterator:551        """Create streaming requests from audio stream."""552        # First request with config553        yield speech.StreamingRecognizeRequest(streaming_config=config)554        555        # Subsequent requests with audio data556        async for audio_chunk in audio_stream:557            if session_id in self._active_streams:558                session = self._active_streams[session_id]559                session["chunk_count"] += 1560                561                # Estimate chunk duration562                chunk_duration = self._estimate_audio_duration(audio_chunk, session["format"])563                session["total_duration"] += chunk_duration564                565                # Check quota for this chunk566                await self._check_quota(chunk_duration)567                568                yield speech.StreamingRecognizeRequest(audio_content=audio_chunk)569    570    async def _process_streaming_responses(571        self, 572        responses, 573        session_id: str574    ) -> AsyncIterator[TranscriptionResult]:575        """Process streaming recognition responses."""576        for response in responses:577            if not response.results:578                continue579            580            for result in response.results:581                if not result.alternatives:582                    continue583                584                alternative = result.alternatives[0]585                586                # Create transcription result587                transcription_result = TranscriptionResult(588                    text=alternative.transcript,589                    confidence=alternative.confidence if hasattr(alternative, 'confidence') else 0.0,590                    provider=self.name,591                    processing_time=0.1,  # Streaming has minimal processing time592                    audio_duration=0.0,   # Will be updated by session tracking593                    language=self._active_streams[session_id]["language"],594                    alternatives=[alt.transcript for alt in result.alternatives[1:]] if len(result.alternatives) > 1 else [],595                    word_timestamps=self._extract_word_timestamps(alternative) if hasattr(alternative, 'words') else None,596                    is_final=result.is_final,597                    session_id=session_id598                )599                600                yield transcription_result601    602    def _process_recognition_response(603        self, 604        response, 605        start_time: datetime, 606        audio_duration: float, 607        language: str608    ) -> TranscriptionResult:609        """Process batch recognition response."""610        processing_time = (datetime.now(timezone.utc) - start_time).total_seconds()611        612        if not hasattr(response, 'results') or not response.results:613            return TranscriptionResult(614                text="",615                confidence=0.0,616                provider=self.name,617                processing_time=processing_time,618                audio_duration=audio_duration,619                language=language,620                alternatives=[],621                word_timestamps=None,622                is_final=True623            )624        625        # Get the best result626        result = response.results[0]627        if not hasattr(result, 'alternatives') or not result.alternatives:628            return TranscriptionResult(629                text="",630                confidence=0.0,631                provider=self.name,632                processing_time=processing_time,633                audio_duration=audio_duration,634                language=language,635                alternatives=[],636                word_timestamps=None,637                is_final=True638            )639        640        best_alternative = result.alternatives[0]641        642        return TranscriptionResult(643            text=best_alternative.transcript,644            confidence=getattr(best_alternative, 'confidence', 0.0),645            provider=self.name,646            processing_time=processing_time,647            audio_duration=audio_duration,648            language=language,649            alternatives=[alt.transcript for alt in result.alternatives[1:]] if len(result.alternatives) > 1 else [],650            word_timestamps=self._extract_word_timestamps(best_alternative) if hasattr(best_alternative, 'words') else None,651            is_final=True652        )653    654    def _extract_word_timestamps(self, alternative) -> List[WordTimestamp]:655        """Extract word timestamps from recognition alternative."""656        if not hasattr(alternative, 'words') or not alternative.words:657            return []658        659        timestamps = []660        try:661            for word_info in alternative.words:662                start_time = word_info.start_time.total_seconds() if hasattr(word_info.start_time, 'total_seconds') else 0.0663                end_time = word_info.end_time.total_seconds() if hasattr(word_info.end_time, 'total_seconds') else 0.0664                665                timestamps.append(WordTimestamp(666                    word=word_info.word,667                    start_time=start_time,668                    end_time=end_time,669                    confidence=getattr(word_info, 'confidence', 0.0)670                ))671        except (TypeError, AttributeError):672            # Handle mock objects or malformed data673            return []674        675        return timestamps676    677    def _estimate_audio_duration(self, audio_data: bytes, format: str) -> float:678        """Estimate audio duration from data size and format."""679        # This is a rough estimation - in production, you'd want more accurate duration detection680        if format.lower() in ["wav", "flac"]:681            # Assume 16-bit, 16kHz mono for estimation682            return len(audio_data) / (16000 * 2)683        elif format.lower() in ["mp3", "ogg", "webm"]:684            # Compressed formats - rough estimation685            return len(audio_data) / 8000  # Assume ~64kbps compression686        else:687            # Default estimation688            return len(audio_data) / 16000689    690    async def _check_quota(self, audio_duration: float) -> None:691        """Check if request would exceed quota limits."""692        await self._reset_counters_if_needed()693        694        # Check requests per minute695        if self._usage_stats["requests_this_minute"] >= self.FREE_TIER_LIMITS["requests_per_minute"]:696            raise RateLimitExceededError(697                self.name,698                "requests_per_minute",699                retry_after=60.0700            )701        702        # Check requests per day703        if self._usage_stats["requests_today"] >= self.FREE_TIER_LIMITS["requests_per_day"]:704            raise RateLimitExceededError(705                self.name,706                "requests_per_day",707                retry_after=86400.0  # 24 hours708            )709        710        # Check audio minutes per month711        audio_minutes = audio_duration / 60.0712        if self._usage_stats["audio_minutes_this_month"] + audio_minutes > self.FREE_TIER_LIMITS["audio_minutes_per_month"]:713            raise QuotaExceededError(714                self.name,715                "audio_minutes_per_month",716                self._usage_stats["audio_minutes_this_month"] + audio_minutes,717                self.FREE_TIER_LIMITS["audio_minutes_per_month"]718            )719    720    async def _update_usage_stats(self, audio_duration: float, processing_time: float) -> None:721        """Update usage statistics after successful request."""722        await self._reset_counters_if_needed()723        724        self._usage_stats["requests_this_minute"] += 1725        self._usage_stats["requests_today"] += 1726        self._usage_stats["audio_minutes_this_month"] += audio_duration / 60.0727        728        logger.debug(729            f"Updated Google Speech usage: {self._usage_stats['requests_today']} requests today, "730            f"{self._usage_stats['audio_minutes_this_month']:.2f} minutes this month"731        )732    733    async def _reset_counters_if_needed(self) -> None:734        """Reset usage counters based on time windows."""735        now = datetime.now(timezone.utc)736        737        # Reset minute counter738        current_minute = now.replace(second=0, microsecond=0)739        if current_minute > self._usage_stats["last_minute_reset"]:740            self._usage_stats["requests_this_minute"] = 0741            self._usage_stats["last_minute_reset"] = current_minute742            logger.debug("Reset Google Speech minute counter")743        744        # Reset daily counter745        current_date = now.date()746        if current_date > self._usage_stats["last_daily_reset"]:747            self._usage_stats["requests_today"] = 0748            self._usage_stats["last_daily_reset"] = current_date749            logger.info("Reset Google Speech daily counter")750        751        # Reset monthly counter752        current_month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)753        if current_month_start > self._usage_stats["last_monthly_reset"]:754            self._usage_stats["audio_minutes_this_month"] = 0.0755            self._usage_stats["last_monthly_reset"] = current_month_start756            logger.info("Reset Google Speech monthly counter")757    758    def _get_next_month_start(self) -> datetime:759        """Get the start of next month."""760        now = datetime.now(timezone.utc)761        if now.month == 12:762            return now.replace(year=now.year + 1, month=1, day=1, hour=0, minute=0, second=0, microsecond=0)763        else:764            return now.replace(month=now.month + 1, day=1, hour=0, minute=0, second=0, microsecond=0)765    766    async def _handle_google_api_error(self, error: Exception) -> None:767        """Handle Google API specific errors."""768        if hasattr(error, 'code'):769            if error.code == 401:770                raise ProviderAuthenticationError(771                    self.name,772                    f"Authentication failed: {str(error)}"773                )774            elif error.code == 429:775                raise RateLimitExceededError(776                    self.name,777                    retry_after=60.0778                )779            elif error.code == 503:780                raise ProviderUnavailableError(781                    self.name,782                    f"Service unavailable: {str(error)}"783                )784            elif error.code in [408, 504]:785                raise ProviderTimeoutError(786                    self.name,787                    f"Request timeout: {str(error)}"788                )789        790        # Generic provider error791        raise ProviderError(792            self.name,793            f"Google API error: {str(error)}"794        )795 796 797def create_google_speech_provider(798    name: str = "google_speech",799    priority: int = 2,800    **config_overrides801) -> GoogleSpeechProvider:802    """803    Factory function to create a GoogleSpeechProvider with default configuration.804    805    Args:806        name: Provider name807        priority: Provider priority (lower = higher priority)808        **config_overrides: Configuration overrides809        810    Returns:811        Configured GoogleSpeechProvider instance812    """813    default_config = {814        "name": name,815        "provider_type": ProviderType.GOOGLE_SPEECH,816        "enabled": True,817        "priority": priority,818        "free_tier_limits": GoogleSpeechProvider.FREE_TIER_LIMITS,819        "rate_limits": {820            "requests_per_minute": GoogleSpeechProvider.FREE_TIER_LIMITS["requests_per_minute"],821            "requests_per_day": GoogleSpeechProvider.FREE_TIER_LIMITS["requests_per_day"]822        },823        "supported_formats": GoogleSpeechProvider.SUPPORTED_FORMATS,824        "supported_languages": GoogleSpeechProvider.SUPPORTED_LANGUAGES,825        "cost_per_minute": GoogleSpeechProvider.PRICING["standard"],826        "api_credentials": {},  # Must be provided by user827        "endpoint_url": None,   # Uses default Google Cloud endpoint828        "timeout_seconds": 60,829        "max_retries": 3830    }831    832    # Apply overrides833    default_config.update(config_overrides)834    835    # Create configuration object836    config = ProviderConfig(**default_config)837    838    return GoogleSpeechProvider(config)