""" ============================================================ Speech-to-Text Module ============================================================ ميزة جديدة - مش موجودة في الشغل القديم. دعم لمصدرين: 1. Groq Whisper API (سريع، cloud-based) - الأساسي 2. Local Whisper (fallback) - لو الـ API فشل السيستم بيقبل: - ملف صوت مرفوع (.wav, .mp3, .m4a, ...) - بيرجع text mp transcription """ import os from typing import Optional from config import STT_CONFIG, GROQ_API_KEY class SpeechTranscriber: """مكون الـ STT""" def __init__(self): self.groq_client = None self.local_model = None self._init_groq() def _init_groq(self): """يحاول يحضّر Groq client""" if not GROQ_API_KEY: print("⚠ No GROQ_API_KEY - STT via Groq disabled") return try: from groq import Groq self.groq_client = Groq(api_key=GROQ_API_KEY) print("✓ Groq STT client initialized") except ImportError: print("⚠ groq package not installed - install with: pip install groq") except Exception as e: print(f"⚠ Groq init failed: {e}") def _init_local_whisper(self): """تحميل local whisper - lazy""" if self.local_model is not None: return try: import whisper print(f"⏳ Loading local Whisper '{STT_CONFIG['local_model_size']}'...") self.local_model = whisper.load_model(STT_CONFIG['local_model_size']) print("✓ Local Whisper loaded") except ImportError: print("⚠ openai-whisper not installed - install with: pip install openai-whisper") except Exception as e: print(f"⚠ Local Whisper load failed: {e}") def transcribe_groq(self, audio_path: str, language: str = None) -> Optional[dict]: """التحويل عبر Groq""" if self.groq_client is None: return None try: with open(audio_path, "rb") as f: kwargs = { "file": (os.path.basename(audio_path), f.read()), "model": STT_CONFIG['model'], "response_format": "verbose_json", } if language: kwargs["language"] = language transcript = self.groq_client.audio.transcriptions.create(**kwargs) return { "text": transcript.text, "language": getattr(transcript, 'language', 'unknown'), "duration": getattr(transcript, 'duration', None), "source": "groq" } except Exception as e: print(f"⚠ Groq transcription error: {e}") return None def transcribe_local(self, audio_path: str, language: str = None) -> Optional[dict]: """التحويل عبر local Whisper""" self._init_local_whisper() if self.local_model is None: return None try: kwargs = {} if language: kwargs["language"] = language result = self.local_model.transcribe(audio_path, **kwargs) return { "text": result.get("text", "").strip(), "language": result.get("language", "unknown"), "source": "local_whisper" } except Exception as e: print(f"⚠ Local transcription error: {e}") return None def transcribe(self, audio_path: str, language: str = None) -> dict: """ الـ method الرئيسي - يجرب Groq الأول، يـ fallback للـ local. """ if not os.path.exists(audio_path): return {"text": "", "error": f"File not found: {audio_path}"} # Try Groq first result = self.transcribe_groq(audio_path, language) if result and result.get("text"): return result # Fallback to local if STT_CONFIG['fallback_to_local']: print("↻ Falling back to local Whisper...") result = self.transcribe_local(audio_path, language) if result and result.get("text"): return result return { "text": "", "error": "Transcription failed - both Groq and local Whisper unavailable" } # Singleton _transcriber = None def get_transcriber() -> SpeechTranscriber: global _transcriber if _transcriber is None: _transcriber = SpeechTranscriber() return _transcriber