From 177070ab5abf96a5d9391d4f2f94c2606082a69b Mon Sep 17 00:00:00 2001 From: Admin Date: Mon, 17 Aug 2026 10:26:29 +0400 Subject: [PATCH] feat(voice): enable full Antigravity Agent shell command execution and model fallback for Telegram voice bridge --- handlers/voice.py | 190 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 143 insertions(+), 47 deletions(-) diff --git a/handlers/voice.py b/handlers/voice.py index 7e1c0f2..d04a9da 100644 --- a/handlers/voice.py +++ b/handlers/voice.py @@ -1,18 +1,41 @@ import os import logging -import aiohttp -import json +import asyncio import sqlite3 +import subprocess from datetime import datetime from aiogram import Router, F, Bot -from aiogram.types import Message +from aiogram.types import Message, BufferedInputFile from config import ADMIN_USER_ID, DATABASE_PATH -from pathlib import Path +import google.generativeai as genai +import edge_tts logger = logging.getLogger(__name__) router = Router() -WHISPER_API_URL = "http://192.168.10.168:9001/transcribe" +# Валидный проверенный ключ Gemini API +WORKING_GEMINI_KEY = "AIzaSyAJMfQ_mRZ1picRXVFNaeFJ7fpAkZxUKyk" +genai.configure(api_key=WORKING_GEMINI_KEY) + +def execute_bash_command(command: str) -> str: + """Выполняет bash-команду на локальном Linux хосте (DockerServer) или по SSH на узлах сети (MacMini 192.168.10.167, Synology 192.168.10.105, MikroTik 192.168.10.215, NanoPi 192.168.10.1) и возвращает результат вывода.""" + logger.info(f"⚡️ [Voice Agent Command] Executing bash: {command}") + try: + res = subprocess.run( + command, + shell=True, + capture_output=True, + text=True, + timeout=45 + ) + out = (res.stdout or "") + ("\n" + res.stderr if res.stderr else "") + output_str = out.strip() + if len(output_str) > 3000: + output_str = output_str[:3000] + "\n... [вывод обрезан]" + return output_str if output_str else "Команда выполнена успешно (вывод пуст)." + except Exception as e: + logger.error(f"❌ Bash Execution Error: {e}") + return f"Ошибка выполнения bash-команды: {e}" def log_voice_to_db(user_id, username, msg_type, duration, text): """Фиксация статистики в основную базу Домового""" @@ -39,68 +62,141 @@ def log_voice_to_db(user_id, username, msg_type, duration, text): except Exception as e: logger.error(f"❌ DB Log error: {e}") +async def text_to_speech_bytes(text: str) -> bytes: + """Синтез качественного голосового ответа на русском через Edge TTS""" + # Обрезаем длинный текст для аудио-резюме + short_text = text[:400] if len(text) > 400 else text + communicate = edge_tts.Communicate(short_text, "ru-RU-DmitryNeural") + out_path = f"/tmp/tts_{os.getpid()}_{int(datetime.now().timestamp())}.mp3" + await communicate.save(out_path) + audio_bytes = b"" + if os.path.exists(out_path): + with open(out_path, "rb") as f: + audio_bytes = f.read() + os.remove(out_path) + return audio_bytes + +def process_audio_with_gemini(audio_path: str) -> str: + """Прямое транскрибирование аудио через Gemini Multimodal 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 + + return response.text.strip() if response.text else "" + except Exception as e: + logger.error(f"Ошибка Gemini Audio API: {e}") + return "" + +async def execute_antigravity_agent(user_text: str) -> str: + """Обработка команды агентом Antigravity Lead Agent с правом выполнения bash-команд на хосте""" + system_instruction = ( + "Ты — Antigravity Lead Agent, суперинтеллектуальный помощник Александра с абсолютными правами управления инфраструктурой. " + "Пользователь Александр обращается к тебе голосом в Телеграмме. Ты обладаешь ТЕМИ ЖЕ ПОЛНОМОЧИЯМИ И ВОЗМОЖНОСТЯМИ, что и в консоли Antigravity CLI! " + "Если пользователь просит проверить системные ресурсы, диски, процессы, файлы, контейнеры, сеть, провести чистку или выполнить любые действия на хосте или соседних серверах (MacMini, Synology, MikroTik, NanoPi) — " + "ОБЯЗАТЕЛЬНО используй функцию execute_bash_command для выполнения консольных команд! " + "Пример команд: 'df -h', 'du -d 1 -h /home/matrixhasyou', 'docker ps', 'docker system df', 'ssh matrixhasyou@192.168.10.167 ...'. " + "Всегда последовательно выполняй необходимые команды, анализируй выхлоп и давай развернутый, четкий, табличный и структурированный ответ." + ) + + models_to_try = ['gemini-3.5-flash-lite', 'gemini-3.5-flash', 'gemini-2.5-flash'] + last_error = None + + for model_name in models_to_try: + try: + logger.info(f"🚀 [Antigravity Agent] Попытка запуска с моделью: {model_name}") + model = genai.GenerativeModel( + model_name=model_name, + system_instruction=system_instruction, + tools=[execute_bash_command] + ) + chat = model.start_chat(enable_automatic_function_calling=True) + response = chat.send_message(user_text) + if response and response.text: + return response.text.strip() + except Exception as e: + logger.warning(f"⚠️ [Antigravity Agent] Сбой на модели {model_name}: {e}") + last_error = e + await asyncio.sleep(2) + + return f"❌ Ошибка исполнения Antigravity Agent: {last_error}" + @router.message(F.voice | F.video_note) async def handle_voice_message(message: Message, bot: Bot): - # ПЕРЕВОДИМ ТОЛЬКО ДЛЯ ТЕБЯ (СУПЕРАДМИНА) + # ПЕРЕВОДИМ И ИСПОЛНЯЕМ ТОЛЬКО ДЛЯ ТЕБЯ (СУПЕРАДМИНА) if int(message.from_user.id) != int(ADMIN_USER_ID): return - status_msg = await message.reply("⏳ Распознаю голос (Whisper AI)...", parse_mode="HTML") + status_msg = await message.reply("🎙 Слушаю и расшифровываю через Gemini Multimodal API...", parse_mode="HTML") await bot.send_chat_action(message.chat.id, "typing") try: if message.voice: file_id = message.voice.file_id - filename = f"{file_id}.oga" + filename = f"voice_{file_id}.ogg" + duration = message.voice.duration msg_type = "VOICE" else: file_id = message.video_note.file_id - filename = f"{file_id}.mp4" + filename = f"note_{file_id}.mp4" + duration = message.video_note.duration msg_type = "VIDEO_NOTE" - file = await bot.get_file(file_id) - - # Используем aiohttp для отправки файла в микросервис - async with aiohttp.ClientSession() as session: - # Скачиваем файл во временный буфер - file_url = f"https://api.telegram.org/file/bot{bot.token}/{file.file_path}" - - # Прокси если нужно (подтянется из конфига если настроено, но тут идем напрямую) - async with session.get(file_url) as file_resp: - if file_resp.status != 200: - raise Exception(f"Не удалось скачать файл из TG: {file_resp.status}") - - file_content = await file_resp.read() - - # Отправляем в наш Whisper-Service - data = aiohttp.FormData() - data.add_field('file', file_content, filename=filename) - - async with session.post(WHISPER_API_URL, data=data) as whisper_resp: - if whisper_resp.status != 200: - err_text = await whisper_resp.text() - raise Exception(f"Ошибка Whisper API: {err_text}") - - result = await whisper_resp.json() - text = result.get("text", "") - duration = result.get("duration", 0) + local_audio_path = f"/tmp/{filename}" + file_info = await bot.get_file(file_id) + await bot.download_file(file_info.file_path, local_audio_path) - if text: - # Логируем в базу - log_voice_to_db(message.from_user.id, message.from_user.username, msg_type, int(duration), text) + # 1. Прямая расшифровка через Gemini Audio + transcribed_text = process_audio_with_gemini(local_audio_path) + + if os.path.exists(local_audio_path): + os.remove(local_audio_path) + + if transcribed_text: + log_voice_to_db(message.from_user.id, message.from_user.username, msg_type, int(duration), transcribed_text) - # Формируем красивый ответ - response = f"📝 РАСШИФРОВКА ({msg_type})\n" - response += f"⏱ Длительность: {int(duration)}с\n" - response += f"━━━━━━━━━━━━━━\n" - response += f"{text}" + await status_msg.edit_text(f"🗣 Команда принята: «{transcribed_text}»\n\n⚙️ Агент выполняет системную команду...", parse_mode="HTML") + await bot.send_chat_action(message.chat.id, "record_voice") + + # 2. Исполнение Antigravity Agent с вызовом функций хоста + agent_response = await execute_antigravity_agent(transcribed_text) + + # 3. Отправка текстового ответа + formatted_text = f"🎯 Ответ Antigravity Agent:\n\n{agent_response}" - await status_msg.edit_text(response, parse_mode="HTML") + # Разделяем длинный текст Telegram если нужно (TG limit 4096) + if len(formatted_text) > 4000: + await status_msg.edit_text(formatted_text[:4000], parse_mode="HTML") + await message.reply(formatted_text[4000:], parse_mode="HTML") + else: + await status_msg.edit_text(formatted_text, parse_mode="HTML") + + # 4. Синтез и отправка голосовой заметки (Voice Note) + try: + voice_bytes = await text_to_speech_bytes(agent_response) + if voice_bytes: + voice_file = BufferedInputFile(voice_bytes, filename="reply_voice.mp3") + await message.reply_voice(voice=voice_file, caption="🔊 Голосовой отклик Antigravity Agent") + except Exception as tts_err: + logger.error(f"TTS Send error: {tts_err}") + else: - await status_msg.edit_text("🔇 Голос обнаружен, но слов не разобрал.") + await status_msg.edit_text("🔇 Голос принят, но Gemini не разобрал слов. Попробуй наговорить еще раз.") except Exception as e: - logger.error(f"❌ Voice Integration Error: {e}") - await status_msg.edit_text(f"❌ Ошибка модуля Whisper: {e}") + logger.error(f"❌ Gemini Voice Integration Error: {e}") + await status_msg.edit_text(f"❌ Ошибка голосового моста: {e}", parse_mode="HTML") voice_router = router