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