diff --git a/handlers/voice.py b/handlers/voice.py index b54e4e6..c16d91c 100644 --- a/handlers/voice.py +++ b/handlers/voice.py @@ -164,17 +164,16 @@ async def text_to_speech_bytes(text: str) -> bytes: return b"" async def transcribe_audio(audio_path: str) -> str: - """Двухконтурная транскрибация аудио: - 1. Основной контур: локальный Faster-Whisper (Large-v3) на MacMini M4 (порт 9001) - - Быстро, приватно, без квот и без rate-limits от Google. - - Поддерживает .ogg, .mp3, .m4a, .mp4, .wav. - 2. Резервный контур: Gemini Multimodal Audio API с перебором рабочих моделей. + """Распознавание речи через Gemini Multimodal Audio API: + - Идеальное распознавание Google (длинные записи, шумы, речь на ходу, естественные мысли). + - Автоматическая нормализация через ffmpeg для видеосообщений (.mp4) и голосовых (.ogg). + - Умный перебор моделей при секундных задержках/лимитах сети. """ clean_path = audio_path tmp_wav = None - # Нормализация для видео/нестандартных форматов через ffmpeg - if audio_path.endswith('.mp4') or audio_path.endswith('.ogg'): + # Нормализация для видеосообщений и нестандартных контейнеров + if audio_path.endswith(('.mp4', '.ogg', '.m4a', '.3gp')): tmp_wav = f"/tmp/norm_{os.getpid()}_{int(datetime.now().timestamp())}.wav" res = subprocess.run([ 'ffmpeg', '-y', '-i', audio_path, '-vn', '-ar', '16000', '-ac', '1', tmp_wav @@ -182,31 +181,16 @@ async def transcribe_audio(audio_path: str) -> str: if res.returncode == 0 and os.path.exists(tmp_wav): clean_path = tmp_wav - # 1. Приоритет: локальный Whisper M4 - try: - timeout = aiohttp.ClientTimeout(total=25) - async with aiohttp.ClientSession(timeout=timeout) as session: - with open(clean_path, 'rb') as f: - form = aiohttp.FormData() - form.add_field('file', f, filename=os.path.basename(clean_path), content_type='audio/wav') - async with session.post("http://192.168.10.167:9001/transcribe", data=form) as resp: - if resp.status == 200: - data = await resp.json() - text = (data.get('text') or "").strip() - if text: - logger.info(f"🎯 [Whisper M4] Распознано ({len(text)} симв): {text[:70]}...") - if tmp_wav and os.path.exists(tmp_wav): - os.remove(tmp_wav) - return text - else: - logger.warning(f"Whisper M4 вернул HTTP {resp.status}, переход на Gemini...") - except Exception as e: - logger.warning(f"⚠️ Whisper M4 недоступен ({e}), переход на Gemini...") + models_to_try = [ + 'gemini-2.5-flash', + 'gemini-flash-latest', + 'gemini-2.5-flash-lite', + 'gemini-3.5-transcribe' + ] - # 2. Резерв: Gemini Audio API - for model_name in ['gemini-2.5-flash', 'gemini-3.5-transcribe', 'gemini-flash-latest', 'gemini-2.5-flash-lite']: + for model_name in models_to_try: try: - logger.info(f"Выгрузка {clean_path} в Gemini Audio API ({model_name})...") + logger.info(f"🎙 [Gemini STT] Распознавание {clean_path} через {model_name}...") audio_file = genai.upload_file(path=clean_path) prompt = ( @@ -224,12 +208,13 @@ async def transcribe_audio(audio_path: str) -> str: if response and response.text and response.text.strip(): text = response.text.strip() - logger.info(f"🎯 [Gemini {model_name}] Распознано: {text[:70]}...") + logger.info(f"🎯 [Gemini STT] Успешно распознано ({model_name}): {text[:70]}...") if tmp_wav and os.path.exists(tmp_wav): os.remove(tmp_wav) return text except Exception as e: - logger.warning(f"⚠️ Gemini Audio API ({model_name}) сбой: {e}") + logger.warning(f"⚠️ [Gemini STT] Ошибка на модели {model_name}: {e}") + await asyncio.sleep(1) if tmp_wav and os.path.exists(tmp_wav): os.remove(tmp_wav)