From 587cb4b8058917ba26d070d3f363b95578c73f37 Mon Sep 17 00:00:00 2001 From: Admin Date: Fri, 4 Sep 2026 17:03:10 +0400 Subject: [PATCH] fix(stt): add primary local Whisper M4 pipeline with ffmpeg audio extraction and working Gemini fallback models --- handlers/voice.py | 96 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 72 insertions(+), 24 deletions(-) diff --git a/handlers/voice.py b/handlers/voice.py index 791aac4..b54e4e6 100644 --- a/handlers/voice.py +++ b/handlers/voice.py @@ -163,29 +163,77 @@ async def text_to_speech_bytes(text: str) -> bytes: logger.error(f"Ошибка Edge TTS: {e}") return b"" -def process_audio_with_gemini(audio_path: str) -> str: - """Прямое транскрибирование аудио через Gemini Multimodal Audio API""" - try: - logger.info(f"Выгрузка {audio_path} в Gemini Multimodal Audio API...") - audio_file = genai.upload_file(path=audio_path) - - prompt = ( - "Точно транскрибируй эту аудиозапись на русском языке в текст. " - "Не добавляй никаких вводных слов, пояснений и комментариев, отдай исключительно чистый текст произнесенных слов." - ) - - model = genai.GenerativeModel('gemini-2.5-flash') - response = model.generate_content([audio_file, prompt]) - - try: - genai.delete_file(name=audio_file.name) - except Exception: - pass +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 с перебором рабочих моделей. + """ + clean_path = audio_path + tmp_wav = None - return response.text.strip() if response.text else "" + # Нормализация для видео/нестандартных форматов через ffmpeg + if audio_path.endswith('.mp4') or audio_path.endswith('.ogg'): + 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 + ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + 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.error(f"Ошибка Gemini Audio API: {e}") - return "" + logger.warning(f"⚠️ Whisper M4 недоступен ({e}), переход на Gemini...") + + # 2. Резерв: Gemini Audio API + for model_name in ['gemini-2.5-flash', 'gemini-3.5-transcribe', 'gemini-flash-latest', 'gemini-2.5-flash-lite']: + try: + logger.info(f"Выгрузка {clean_path} в Gemini Audio API ({model_name})...") + audio_file = genai.upload_file(path=clean_path) + + prompt = ( + "Точно транскрибируй эту аудиозапись на русском языке в текст. " + "Не добавляй никаких вводных слов, пояснений и комментариев, отдай исключительно чистый текст произнесенных слов." + ) + + model = genai.GenerativeModel(model_name) + response = model.generate_content([audio_file, prompt]) + + try: + genai.delete_file(name=audio_file.name) + except Exception: + pass + + if response and response.text and response.text.strip(): + text = response.text.strip() + logger.info(f"🎯 [Gemini {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}") + + if tmp_wav and os.path.exists(tmp_wav): + os.remove(tmp_wav) + return "" async def execute_antigravity_agent(user_text: str) -> str: """Обработка команды и мыслей агентом Майа с систематизацией, Битрикс24 и правом исполнения bash-команд""" @@ -215,7 +263,7 @@ async def execute_antigravity_agent(user_text: str) -> str: " Внутри пиши только чистый русский текст без звездочек, эмодзи и Markdown." ) - models_to_try = ['gemini-2.5-flash', 'gemini-3.5-flash', 'gemini-3.5-flash-lite'] + models_to_try = ['gemini-2.5-flash', 'gemini-flash-latest', 'gemini-2.5-flash-lite', 'gemini-3.7-flash', 'gemini-2.5-pro'] last_error = None for model_name in models_to_try: @@ -287,8 +335,8 @@ async def handle_voice_message(message: Message, bot: Bot): file_info = await bot.get_file(file_id) await bot.download_file(file_info.file_path, local_audio_path) - # 1. Прямая расшифровка через Gemini Audio - transcribed_text = process_audio_with_gemini(local_audio_path) + # 1. Прямая расшифровка через локальный Whisper M4 (с фоллбэком на Gemini) + transcribed_text = await transcribe_audio(local_audio_path) if os.path.exists(local_audio_path): os.remove(local_audio_path)