CoolFace
Apppublic

nifty-coder/stemsplit-backend

sourceHugging Faceupdated 8mo agoView on Hugging Face
0likes
web_speech_provider.py602 linesDownload Raw Back to providers
1"""2Web Speech API provider implementation.3 4This module implements a proxy provider that integrates with the browser's5Web Speech API for client-side speech recognition. Since the Web Speech API6runs in the browser, this provider acts as a proxy that coordinates with7frontend JavaScript code.8"""9 10import asyncio11import json12import logging13from typing import Dict, List, Optional, AsyncIterator, Any14from datetime import datetime, timezone15 16from ..interfaces import STTProvider17from ..models import (18    ProviderConfig, 19    TranscriptionResult, 20    QuotaStatus, 21    QuotaType,22    WordTimestamp,23    ProviderType24)25from ..exceptions import (26    ProviderError,27    ProviderUnavailableError,28    UnsupportedFormatError,29    UnsupportedLanguageError,30    QuotaExceededError,31    TranscriptionError32)33 34logger = logging.getLogger(__name__)35 36 37class WebSpeechProvider(STTProvider):38    """39    Web Speech API provider implementation.40    41    This provider acts as a proxy to the browser's Web Speech API. Since the42    Web Speech API runs client-side, this provider coordinates with frontend43    JavaScript code through WebSocket messages and handles browser compatibility.44    45    Key features:46    - Browser compatibility detection47    - Real-time streaming support48    - Fallback handling for unsupported browsers49    - Client-side processing (no server costs)50    - Language detection and support51    """52    53    # Supported audio formats (limited by browser capabilities)54    SUPPORTED_FORMATS = ["webm", "wav", "mp3", "ogg"]55    56    # Supported languages (common Web Speech API languages)57    SUPPORTED_LANGUAGES = [58        "en-US", "en-GB", "en-AU", "en-CA", "en-IN", "en-NZ", "en-ZA",59        "es-ES", "es-MX", "es-AR", "es-CO", "es-CL", "es-PE", "es-VE",60        "fr-FR", "fr-CA", "fr-BE", "fr-CH",61        "de-DE", "de-AT", "de-CH",62        "it-IT", "it-CH",63        "pt-BR", "pt-PT",64        "ru-RU",65        "ja-JP",66        "ko-KR",67        "zh-CN", "zh-TW", "zh-HK",68        "ar-SA", "ar-EG",69        "hi-IN",70        "th-TH",71        "tr-TR",72        "pl-PL",73        "nl-NL", "nl-BE",74        "sv-SE",75        "da-DK",76        "no-NO",77        "fi-FI"78    ]79    80    def __init__(self, config: ProviderConfig):81        """82        Initialize the Web Speech API provider.83        84        Args:85            config: Provider configuration86        """87        super().__init__(config)88        89        # Validate configuration90        if config.provider_type != ProviderType.WEB_SPEECH_API:91            raise ValueError(f"Invalid provider type: {config.provider_type}")92        93        # Web Speech API is free, so we track usage for monitoring only94        self._usage_stats = {95            "requests_today": 0,96            "audio_minutes_today": 0.0,97            "last_reset": datetime.now(timezone.utc).date()98        }99        100        # Browser compatibility status101        self._browser_compatible = None102        self._last_compatibility_check = None103        104        # Session management for streaming105        self._active_sessions = {}106        107        logger.info(f"Initialized WebSpeechProvider: {self.name}")108    109    async def transcribe_audio(110        self, 111        audio_data: bytes, 112        format: str,113        language: str = "en-US",114        **kwargs115    ) -> TranscriptionResult:116        """117        Transcribe audio data using Web Speech API.118        119        Since Web Speech API runs in the browser, this method coordinates120        with frontend JavaScript to perform the transcription.121        122        Args:123            audio_data: Raw audio bytes124            format: Audio format125            language: Language code for transcription126            **kwargs: Additional options (session_id, timeout, etc.)127            128        Returns:129            TranscriptionResult with transcribed text130            131        Raises:132            ProviderUnavailableError: If browser doesn't support Web Speech API133            UnsupportedFormatError: If audio format is not supported134            UnsupportedLanguageError: If language is not supported135            TranscriptionError: If transcription fails136        """137        start_time = datetime.now(timezone.utc)138        139        # Check browser compatibility140        await self._check_browser_compatibility()141        142        # Validate format and language143        if not self.supports_format(format):144            raise UnsupportedFormatError(self.name, format, self.SUPPORTED_FORMATS)145        146        if not self.supports_language(language):147            raise UnsupportedLanguageError(self.name, language, self.SUPPORTED_LANGUAGES)148        149        # Check quota (for monitoring purposes)150        audio_duration = kwargs.get('audio_duration', 0.0)151        await self._check_usage_quota(audio_duration)152        153        try:154            # Create transcription request155            session_id = kwargs.get('session_id', f"ws_{int(datetime.now().timestamp())}")156            timeout = kwargs.get('timeout', 30.0)157            158            # Prepare transcription parameters159            transcription_params = {160                "language": language,161                "continuous": kwargs.get('continuous', False),162                "interim_results": kwargs.get('interim_results', True),163                "max_alternatives": kwargs.get('max_alternatives', 1),164                "session_id": session_id165            }166            167            # Since this is a proxy provider, we simulate the transcription process168            # In a real implementation, this would coordinate with frontend JavaScript169            result = await self._perform_transcription(170                audio_data, 171                format, 172                transcription_params,173                timeout174            )175            176            # Update usage statistics177            processing_time = (datetime.now(timezone.utc) - start_time).total_seconds()178            await self._update_usage_stats(audio_duration, processing_time)179            180            return result181            182        except Exception as e:183            logger.error(f"Web Speech API transcription failed: {e}")184            if isinstance(e, (ProviderError, TranscriptionError)):185                raise186            raise TranscriptionError(f"Transcription failed: {str(e)}")187    188    async def transcribe_streaming(189        self,190        audio_stream: AsyncIterator[bytes],191        format: str,192        language: str = "en-US",193        **kwargs194    ) -> AsyncIterator[TranscriptionResult]:195        """196        Transcribe streaming audio data using Web Speech API.197        198        Args:199            audio_stream: Async iterator of audio chunks200            format: Audio format201            language: Language code for transcription202            **kwargs: Additional options203            204        Yields:205            TranscriptionResult for each processed chunk206        """207        # Check browser compatibility208        await self._check_browser_compatibility()209        210        # Validate format and language211        if not self.supports_format(format):212            raise UnsupportedFormatError(self.name, format, self.SUPPORTED_FORMATS)213        214        if not self.supports_language(language):215            raise UnsupportedLanguageError(self.name, language, self.SUPPORTED_LANGUAGES)216        217        session_id = kwargs.get('session_id', f"ws_stream_{int(datetime.now().timestamp())}")218        219        try:220            # Initialize streaming session221            self._active_sessions[session_id] = {222                "start_time": datetime.now(timezone.utc),223                "language": language,224                "format": format,225                "chunk_count": 0,226                "total_duration": 0.0227            }228            229            logger.info(f"Started streaming session: {session_id}")230            231            # Process audio chunks232            async for audio_chunk in audio_stream:233                try:234                    # Estimate chunk duration (rough approximation)235                    chunk_duration = len(audio_chunk) / (16000 * 2)  # Assume 16kHz, 16-bit236                    237                    # Check quota for this chunk238                    await self._check_usage_quota(chunk_duration)239                    240                    # Process the chunk241                    result = await self._process_streaming_chunk(242                        audio_chunk,243                        session_id,244                        language,245                        format,246                        **kwargs247                    )248                    249                    if result:250                        # Update session stats251                        session = self._active_sessions[session_id]252                        session["chunk_count"] += 1253                        session["total_duration"] += chunk_duration254                        255                        yield result256                        257                except Exception as e:258                    logger.error(f"Error processing streaming chunk: {e}")259                    # Continue processing other chunks260                    continue261            262        finally:263            # Clean up session264            if session_id in self._active_sessions:265                session = self._active_sessions.pop(session_id)266                total_duration = session["total_duration"]267                processing_time = (datetime.now(timezone.utc) - session["start_time"]).total_seconds()268                269                await self._update_usage_stats(total_duration, processing_time)270                logger.info(f"Completed streaming session: {session_id}, duration: {total_duration:.2f}s")271    272    async def check_health(self) -> bool:273        """274        Check if the Web Speech API is available and healthy.275        276        Returns:277            True if provider is healthy, False otherwise278        """279        try:280            await self._check_browser_compatibility()281            return self._browser_compatible282        except Exception as e:283            logger.error(f"Health check failed: {e}")284            return False285    286    async def get_quota_status(self) -> Dict[str, QuotaStatus]:287        """288        Get current quota usage status.289        290        Web Speech API is free, so this returns monitoring information only.291        292        Returns:293            Dictionary mapping quota types to their status294        """295        await self._reset_daily_stats_if_needed()296        297        # Web Speech API doesn't have hard limits, but we track usage for monitoring298        return {299            "requests_per_day": QuotaStatus(300                provider=self.name,301                quota_type=QuotaType.REQUESTS_PER_DAY,302                current_usage=self._usage_stats["requests_today"],303                limit=float('inf'),  # No hard limit304                remaining=float('inf'),305                reset_time=datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0),306                percentage_used=0.0307            ),308            "audio_minutes_per_day": QuotaStatus(309                provider=self.name,310                quota_type=QuotaType.AUDIO_MINUTES_PER_DAY,311                current_usage=self._usage_stats["audio_minutes_today"],312                limit=float('inf'),  # No hard limit313                remaining=float('inf'),314                reset_time=datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0),315                percentage_used=0.0316            )317        }318    319    def supports_format(self, format: str) -> bool:320        """321        Check if the provider supports a given audio format.322        323        Args:324            format: Audio format to check325            326        Returns:327            True if format is supported, False otherwise328        """329        return format.lower() in [f.lower() for f in self.SUPPORTED_FORMATS]330    331    def supports_language(self, language: str) -> bool:332        """333        Check if the provider supports a given language.334        335        Args:336            language: Language code to check337            338        Returns:339            True if language is supported, False otherwise340        """341        return language in self.SUPPORTED_LANGUAGES342    343    async def estimate_cost(self, audio_duration: float) -> float:344        """345        Estimate the cost for transcribing audio of given duration.346        347        Web Speech API is free, so this always returns 0.0.348        349        Args:350            audio_duration: Duration in seconds351            352        Returns:353            Estimated cost (always 0.0 for Web Speech API)354        """355        return 0.0356    357    # Private methods358    359    async def _check_browser_compatibility(self) -> None:360        """361        Check if the browser supports Web Speech API.362        363        Raises:364            ProviderUnavailableError: If browser doesn't support Web Speech API365        """366        # In a real implementation, this would check browser capabilities367        # For now, we simulate compatibility check368        369        current_time = datetime.now(timezone.utc)370        371        # Cache compatibility check for 5 minutes372        if (self._last_compatibility_check and 373            (current_time - self._last_compatibility_check).total_seconds() < 300):374            if not self._browser_compatible:375                raise ProviderUnavailableError(376                    self.name, 377                    "Browser does not support Web Speech API"378                )379            return380        381        # Simulate compatibility check382        # In reality, this would be done on the frontend and communicated via WebSocket383        self._browser_compatible = True  # Assume compatible for now384        self._last_compatibility_check = current_time385        386        if not self._browser_compatible:387            raise ProviderUnavailableError(388                self.name, 389                "Browser does not support Web Speech API"390            )391    392    async def _check_usage_quota(self, audio_duration: float) -> None:393        """394        Check usage quota (for monitoring purposes).395        396        Args:397            audio_duration: Duration of audio to be processed398        """399        await self._reset_daily_stats_if_needed()400        401        # Web Speech API is free, but we can implement soft limits for monitoring402        max_daily_requests = self.config.free_tier_limits.get("max_daily_requests", float('inf'))403        max_daily_minutes = self.config.free_tier_limits.get("max_daily_minutes", float('inf'))404        405        if self._usage_stats["requests_today"] >= max_daily_requests:406            raise QuotaExceededError(407                self.name,408                "requests_per_day",409                self._usage_stats["requests_today"],410                max_daily_requests411            )412        413        if self._usage_stats["audio_minutes_today"] + (audio_duration / 60.0) > max_daily_minutes:414            raise QuotaExceededError(415                self.name,416                "audio_minutes_per_day",417                self._usage_stats["audio_minutes_today"] + (audio_duration / 60.0),418                max_daily_minutes419            )420    421    async def _perform_transcription(422        self,423        audio_data: bytes,424        format: str,425        params: Dict[str, Any],426        timeout: float427    ) -> TranscriptionResult:428        """429        Perform the actual transcription.430        431        In a real implementation, this would coordinate with frontend JavaScript432        to use the Web Speech API. For now, we simulate the process.433        434        Args:435            audio_data: Raw audio bytes436            format: Audio format437            params: Transcription parameters438            timeout: Request timeout439            440        Returns:441            TranscriptionResult442        """443        start_time = datetime.now(timezone.utc)444        445        # Simulate transcription delay446        await asyncio.sleep(0.1)447        448        # Estimate audio duration449        audio_duration = len(audio_data) / (16000 * 2)  # Assume 16kHz, 16-bit450        451        # Simulate transcription result452        # In reality, this would come from the browser's Web Speech API453        transcribed_text = "This is a simulated transcription from Web Speech API"454        confidence = 0.85455        456        processing_time = (datetime.now(timezone.utc) - start_time).total_seconds()457        458        return TranscriptionResult(459            text=transcribed_text,460            confidence=confidence,461            provider=self.name,462            processing_time=processing_time,463            audio_duration=audio_duration,464            language=params["language"],465            alternatives=[],466            word_timestamps=None,467            is_final=True,468            session_id=params["session_id"]469        )470    471    async def _process_streaming_chunk(472        self,473        audio_chunk: bytes,474        session_id: str,475        language: str,476        format: str,477        **kwargs478    ) -> Optional[TranscriptionResult]:479        """480        Process a streaming audio chunk.481        482        Args:483            audio_chunk: Audio data chunk484            session_id: Streaming session ID485            language: Language code486            format: Audio format487            **kwargs: Additional options488            489        Returns:490            TranscriptionResult if chunk produced results, None otherwise491        """492        start_time = datetime.now(timezone.utc)493        494        # Simulate processing delay495        await asyncio.sleep(0.05)496        497        # Estimate chunk duration498        chunk_duration = len(audio_chunk) / (16000 * 2)499        500        # Simulate interim results (not every chunk produces results)501        if len(audio_chunk) < 1000:  # Skip very small chunks502            return None503        504        # Simulate transcription505        interim_results = kwargs.get('interim_results', True)506        is_final = len(audio_chunk) > 8000  # Larger chunks are more likely to be final507        508        if interim_results or is_final:509            text = f"Streaming chunk {session_id[-4:]}"510            confidence = 0.7 if not is_final else 0.85511            512            processing_time = (datetime.now(timezone.utc) - start_time).total_seconds()513            514            return TranscriptionResult(515                text=text,516                confidence=confidence,517                provider=self.name,518                processing_time=processing_time,519                audio_duration=chunk_duration,520                language=language,521                alternatives=[],522                word_timestamps=None,523                is_final=is_final,524                session_id=session_id525            )526        527        return None528    529    async def _update_usage_stats(self, audio_duration: float, processing_time: float) -> None:530        """531        Update usage statistics.532        533        Args:534            audio_duration: Duration of processed audio535            processing_time: Time taken to process536        """537        await self._reset_daily_stats_if_needed()538        539        self._usage_stats["requests_today"] += 1540        self._usage_stats["audio_minutes_today"] += audio_duration / 60.0541        542        logger.debug(543            f"Updated usage stats: {self._usage_stats['requests_today']} requests, "544            f"{self._usage_stats['audio_minutes_today']:.2f} minutes today"545        )546    547    async def _reset_daily_stats_if_needed(self) -> None:548        """Reset daily statistics if a new day has started."""549        current_date = datetime.now(timezone.utc).date()550        551        if current_date > self._usage_stats["last_reset"]:552            self._usage_stats["requests_today"] = 0553            self._usage_stats["audio_minutes_today"] = 0.0554            self._usage_stats["last_reset"] = current_date555            logger.info("Reset daily usage statistics")556 557 558def create_web_speech_provider(559    name: str = "web_speech_api",560    priority: int = 1,561    **config_overrides562) -> WebSpeechProvider:563    """564    Factory function to create a WebSpeechProvider with default configuration.565    566    Args:567        name: Provider name568        priority: Provider priority (lower = higher priority)569        **config_overrides: Configuration overrides570        571    Returns:572        Configured WebSpeechProvider instance573    """574    default_config = {575        "name": name,576        "provider_type": ProviderType.WEB_SPEECH_API,577        "enabled": True,578        "priority": priority,579        "free_tier_limits": {580            "max_daily_requests": 1000,  # Soft limit for monitoring581            "max_daily_minutes": 60.0    # Soft limit for monitoring582        },583        "rate_limits": {584            "requests_per_minute": 60,585            "requests_per_hour": 1000586        },587        "supported_formats": WebSpeechProvider.SUPPORTED_FORMATS,588        "supported_languages": WebSpeechProvider.SUPPORTED_LANGUAGES,589        "cost_per_minute": 0.0,  # Free590        "api_credentials": {},   # No credentials needed591        "endpoint_url": None,    # Browser-based592        "timeout_seconds": 30,593        "max_retries": 2594    }595    596    # Apply overrides597    default_config.update(config_overrides)598    599    # Create configuration object600    config = ProviderConfig(**default_config)601    602    return WebSpeechProvider(config)