nifty-coder/stemsplit-backend
0
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)