diff --git a/handlers/voice.py b/handlers/voice.py index 56ae968..ef6e18e 100644 --- a/handlers/voice.py +++ b/handlers/voice.py @@ -1,4 +1,5 @@ import os +import re import logging import asyncio import sqlite3 @@ -63,10 +64,24 @@ def log_voice_to_db(user_id, username, msg_type, duration, text): 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") + """Синтез качественного женского голосового ответа на русском через Edge TTS с глубоким тембром""" + # Если в тексте есть тег ..., озвучиваем именно его + match = re.search(r"(.*?)", text, re.DOTALL | re.IGNORECASE) + if match: + speech_text = match.group(1).strip() + else: + # Иначе берем выжимку до 350 символов + speech_text = text[:350] if len(text) > 350 else text + + # Очищаем от Markdown и спецсимволов для чистоты звучания + for ch in ["*", "#", "_", "`", "~", ">", "•", "🎯", "⚙️", "📋", "💡", "🗄️", "🚨", "⚠️", "✅", "❌"]: + speech_text = speech_text.replace(ch, "") + speech_text = re.sub(r"\[.*?\]\(.*?\)", "", speech_text).strip() + + if not speech_text: + speech_text = "Приняла, Александр. Всё структурировано и зафиксировано." + + communicate = edge_tts.Communicate(speech_text, "ru-RU-SvetlanaNeural", pitch="-8Hz", rate="-3%") out_path = f"/tmp/tts_{os.getpid()}_{int(datetime.now().timestamp())}.mp3" await communicate.save(out_path) audio_bytes = b"" @@ -83,8 +98,8 @@ def process_audio_with_gemini(audio_path: str) -> str: audio_file = genai.upload_file(path=audio_path) prompt = ( - "Точно транскрибируй эту голосовую заметку на русском языке в текст команды или вопроса. " - "Не добавляй никаких лишних комментариев, отдай только чистый текст произнесенных слов." + "Точно транскрибируй эту аудиозапись на русском языке в текст. " + "Не добавляй никаких вводных слов, пояснений и комментариев, отдай исключительно чистый текст произнесенных слов." ) model = genai.GenerativeModel('gemini-2.5-flash') @@ -101,28 +116,32 @@ def process_audio_with_gemini(audio_path: str) -> str: return "" async def execute_antigravity_agent(user_text: str) -> str: - """Обработка команды агентом Antigravity Lead Agent с правом выполнения bash-команд на хосте""" + """Обработка команды и мыслей агентом Майа с систематизацией и правом исполнения bash-команд""" system_instruction = ( - "Ты — Antigravity Lead Agent, суперинтеллектуальный помощник Александра с абсолютными правами управления инфраструктурой. " - "Пользователь Александр обращается к тебе голосом в Телеграмме. Ты обладаешь ТЕМИ ЖЕ ПОЛНОМОЧИЯМИ И ВОЗМОЖНОСТЯМИ, что и в консоли Antigravity CLI! " - "Если пользователь просит проверить системные ресурсы, диски, процессы, файлы, контейнеры, сеть, проверить туннели, провести чистку или выполнить любые действия на хосте или соседних серверах — " - "ОБЯЗАТЕЛЬНО используй функцию execute_bash_command для выполнения консольных команд! " - "ВАЖНАЯ СПРАВКА ПО УЗЛАМ СЕТИ И SSH-ДОСТУПАМ:\n" - "- NanoPi R3S главный роутер: 'ssh -o StrictHostKeyChecking=no root@192.168.10.1 ...' (ПОЛЬЗОВАТЕЛЬ ТОЛЬКО root! НЕ matrixhasyou!)\n" - "- Synology NAS: 'ssh -o StrictHostKeyChecking=no matrixhasyou@192.168.10.105 ...'\n" - "- MacMini M4: 'ssh -o StrictHostKeyChecking=no matrixhasyou@192.168.10.167 ...'\n" - "- MikroTik Home: 'ssh -o StrictHostKeyChecking=no matrixhasyou@192.168.10.215 ...'\n" - "- MikroTik Office: 'ssh -o StrictHostKeyChecking=no atyrin@10.7.0.1 ...'\n" - "- Office Server simtech: 'ssh -o StrictHostKeyChecking=no ops@10.7.4.173 ...'\n" - "Всегда последовательно выполняй необходимые команды, анализируй выхлоп и давай развернутый, четкий, табличный и структурированный ответ." + "Ты — Майа (Maya / Antigravity Lead Agent), суперинтеллектуальный помощник Александра. " + "Александр обращается к тебе голосом в Телеграмме или надиктованной аудиозаписью с диктофона. " + "Твоя задача — быть идеальным личным инженером и аналитиком:\n\n" + "1. СТРУКТУРИРОВАНИЕ МЫСЛЕЙ (если Александр надиктовал идеи, мысли, список дел или сумбурный поток):\n" + " Разложи всё по четким блокам:\n" + " 🎯 Суть: краткая главная мысль в 1-2 предложениях.\n" + " 📋 Задачи к выполнению (Action Items): конкретные пункты с галочками [ ].\n" + " 💡 Идеи в копилку / Бэклог: новые задумки, архитектурные гипотезы.\n" + " 🗄️ Куда зафиксировать: в какую заметку Obsidian (HomeLab или Work) это стоит сохранить.\n\n" + "2. СИСТЕМНЫЕ КОМАНДЫ (если Александр просит проверить ресурсы, диски, сеть, контейнеры, запустить что-то):\n" + " ОБЯЗАТЕЛЬНО используй execute_bash_command! Выполни команду, проанализируй вывод и дай четкий отчет.\n" + " Узлы сети: NanoPi R3S (root@192.168.10.1), Synology (matrixhasyou@192.168.10.105), MacMini (matrixhasyou@192.168.10.167), MikroTik (matrixhasyou@192.168.10.215).\n\n" + "3. ГОЛОСОВОЙ ОТКЛИК ДЛЯ ОЗВУЧКИ:\n" + " В САМОМ КОНЦЕ ответа ОБЯЗАТЕЛЬНО добавь тег ....\n" + " Внутри него напиши короткое, живое, теплое и емкое резюме от Майи (1-3 предложения), которое будет озвучено вслух! " + " Внутри пиши только чистый русский текст без звездочек, эмодзи, списков и Markdown." ) - models_to_try = ['gemini-3.5-flash-lite', 'gemini-3.5-flash', 'gemini-2.5-flash'] + models_to_try = ['gemini-2.5-flash', 'gemini-3.5-flash', 'gemini-3.5-flash-lite'] last_error = None for model_name in models_to_try: try: - logger.info(f"🚀 [Antigravity Agent] Попытка запуска с моделью: {model_name}") + logger.info(f"🚀 [Maya Agent] Запуск с моделью: {model_name}") model = genai.GenerativeModel( model_name=model_name, system_instruction=system_instruction, @@ -133,33 +152,59 @@ async def execute_antigravity_agent(user_text: str) -> str: if response and response.text: return response.text.strip() except Exception as e: - logger.warning(f"⚠️ [Antigravity Agent] Сбой на модели {model_name}: {e}") + logger.warning(f"⚠️ [Maya Agent] Сбой на модели {model_name}: {e}") last_error = e - await asyncio.sleep(2) + await asyncio.sleep(1) - return f"❌ Ошибка исполнения Antigravity Agent: {last_error}" + return f"❌ Ошибка исполнения Maya Agent: {last_error}" -@router.message(F.voice | F.video_note) +@router.message(F.voice | F.video_note | F.audio | F.document) 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("🎙 Слушаю и расшифровываю через Gemini Multimodal API...", parse_mode="HTML") + # Определяем тип медиафайла + msg_type = "VOICE" + duration = 0 + filename = "audio.ogg" + file_id = None + + if message.voice: + file_id = message.voice.file_id + filename = f"voice_{file_id}.ogg" + duration = message.voice.duration + msg_type = "VOICE" + elif message.video_note: + file_id = message.video_note.file_id + filename = f"note_{file_id}.mp4" + duration = message.video_note.duration + msg_type = "VIDEO_NOTE" + elif message.audio: + file_id = message.audio.file_id + ext = os.path.splitext(message.audio.file_name or "audio.mp3")[1] or ".mp3" + filename = f"audio_{file_id}{ext}" + duration = message.audio.duration or 0 + msg_type = "AUDIO_RECORDING" + elif message.document: + doc = message.document + fn = (doc.file_name or "").lower() + mime = (doc.mime_type or "").lower() + allowed_exts = ('.mp3', '.m4a', '.ogg', '.wav', '.aac', '.flac', '.3gp', '.oga', '.wma') + if not (mime.startswith("audio/") or fn.endswith(allowed_exts)): + return # Не аудиозапись, пропускаем для других хэндлеров + file_id = doc.file_id + ext = os.path.splitext(doc.file_name or "rec.m4a")[1] or ".m4a" + filename = f"doc_{file_id}{ext}" + duration = 0 + msg_type = "RECORDER_FILE" + else: + return + + status_msg = await message.reply("🎙 Слушаю и анализирую аудиозапись...", parse_mode="HTML") await bot.send_chat_action(message.chat.id, "typing") try: - if message.voice: - file_id = message.voice.file_id - filename = f"voice_{file_id}.ogg" - duration = message.voice.duration - msg_type = "VOICE" - else: - file_id = message.video_note.file_id - filename = f"note_{file_id}.mp4" - duration = message.video_note.duration - msg_type = "VIDEO_NOTE" - 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) @@ -173,14 +218,15 @@ async def handle_voice_message(message: Message, bot: Bot): if transcribed_text: log_voice_to_db(message.from_user.id, message.from_user.username, msg_type, int(duration), transcribed_text) - await status_msg.edit_text(f"🗣 Команда принята: «{transcribed_text}»\n\n⚙️ Агент выполняет системную команду...", parse_mode="HTML") + 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 с вызовом функций хоста + # 2. Исполнение Maya Lead Agent (систематизация + команды хоста) agent_response = await execute_antigravity_agent(transcribed_text) - # 3. Отправка текстового ответа - formatted_text = f"🎯 Ответ Antigravity Agent:\n\n{agent_response}" + # 3. Подготовка текста для отправки (вырезаем тег из визуального сообщения) + clean_text = re.sub(r".*?", "", agent_response, flags=re.DOTALL).strip() + formatted_text = f"🎯 Майа // Структурированный отклик:\n\n{clean_text}" # Разделяем длинный текст Telegram если нужно (TG limit 4096) if len(formatted_text) > 4000: @@ -194,15 +240,15 @@ async def handle_voice_message(message: Message, bot: Bot): 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") + await message.reply_voice(voice=voice_file, caption="🎙 Голосовой отклик Майи") except Exception as tts_err: logger.error(f"TTS Send error: {tts_err}") else: - await status_msg.edit_text("🔇 Голос принят, но Gemini не разобрал слов. Попробуй наговорить еще раз.") + await status_msg.edit_text("🔇 Аудиозапись принята, но слова не распознаны. Попробуй наговорить еще раз.") except Exception as e: - logger.error(f"❌ Gemini Voice Integration Error: {e}") - await status_msg.edit_text(f"❌ Ошибка голосового моста: {e}", parse_mode="HTML") + logger.error(f"❌ Maya Voice Integration Error: {e}") + await status_msg.edit_text(f"❌ Ошибка обработки аудио: {e}", parse_mode="HTML") voice_router = router diff --git a/requirements.txt b/requirements.txt index f90bbb0..70f3b0a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,3 +25,4 @@ google-auth-httplib2 google-auth-oauthlib +edge-tts>=7.0.0