feat(voice): upgrade Maya voice hub with audio file support, mind structuring, and deep female TTS

This commit is contained in:
Admin 2026-09-04 12:40:55 +04:00
parent 39e7646766
commit a47d1eee77
2 changed files with 93 additions and 46 deletions

View file

@ -1,4 +1,5 @@
import os import os
import re
import logging import logging
import asyncio import asyncio
import sqlite3 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}") logger.error(f"❌ DB Log error: {e}")
async def text_to_speech_bytes(text: str) -> bytes: async def text_to_speech_bytes(text: str) -> bytes:
"""Синтез качественного голосового ответа на русском через Edge TTS""" """Синтез качественного женского голосового ответа на русском через Edge TTS с глубоким тембром"""
# Обрезаем длинный текст для аудио-резюме # Если в тексте есть тег <voice>...</voice>, озвучиваем именно его
short_text = text[:400] if len(text) > 400 else text match = re.search(r"<voice>(.*?)</voice>", text, re.DOTALL | re.IGNORECASE)
communicate = edge_tts.Communicate(short_text, "ru-RU-DmitryNeural") 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" out_path = f"/tmp/tts_{os.getpid()}_{int(datetime.now().timestamp())}.mp3"
await communicate.save(out_path) await communicate.save(out_path)
audio_bytes = b"" audio_bytes = b""
@ -83,8 +98,8 @@ def process_audio_with_gemini(audio_path: str) -> str:
audio_file = genai.upload_file(path=audio_path) audio_file = genai.upload_file(path=audio_path)
prompt = ( prompt = (
"Точно транскрибируй эту голосовую заметку на русском языке в текст команды или вопроса. " "Точно транскрибируй эту аудиозапись на русском языке в текст. "
"Не добавляй никаких лишних комментариев, отдай только чистый текст произнесенных слов." "Не добавляй никаких вводных слов, пояснений и комментариев, отдай исключительно чистый текст произнесенных слов."
) )
model = genai.GenerativeModel('gemini-2.5-flash') model = genai.GenerativeModel('gemini-2.5-flash')
@ -101,28 +116,32 @@ def process_audio_with_gemini(audio_path: str) -> str:
return "" return ""
async def execute_antigravity_agent(user_text: str) -> str: async def execute_antigravity_agent(user_text: str) -> str:
"""Обработка команды агентом Antigravity Lead Agent с правом выполнения bash-команд на хосте""" """Обработка команды и мыслей агентом Майа с систематизацией и правом исполнения bash-команд"""
system_instruction = ( system_instruction = (
"Ты — Antigravity Lead Agent, суперинтеллектуальный помощник Александра с абсолютными правами управления инфраструктурой. " "Ты — Майа (Maya / Antigravity Lead Agent), суперинтеллектуальный помощник Александра. "
"Пользователь Александр обращается к тебе голосом в Телеграмме. Ты обладаешь ТЕМИ ЖЕ ПОЛНОМОЧИЯМИ И ВОЗМОЖНОСТЯМИ, что и в консоли Antigravity CLI! " "Александр обращается к тебе голосом в Телеграмме или надиктованной аудиозаписью с диктофона. "
"Если пользователь просит проверить системные ресурсы, диски, процессы, файлы, контейнеры, сеть, проверить туннели, провести чистку или выполнить любые действия на хосте или соседних серверах — " "Твоя задача — быть идеальным личным инженером и аналитиком:\n\n"
"ОБЯЗАТЕЛЬНО используй функцию execute_bash_command для выполнения консольных команд! " "1. СТРУКТУРИРОВАНИЕ МЫСЛЕЙ (если Александр надиктовал идеи, мысли, список дел или сумбурный поток):\n"
"ВАЖНАЯ СПРАВКА ПО УЗЛАМ СЕТИ И SSH-ДОСТУПАМ:\n" " Разложи всё по четким блокам:\n"
"- NanoPi R3S главный роутер: 'ssh -o StrictHostKeyChecking=no root@192.168.10.1 ...' (ПОЛЬЗОВАТЕЛЬ ТОЛЬКО root! НЕ matrixhasyou!)\n" " 🎯 Суть: краткая главная мысль в 1-2 предложениях.\n"
"- Synology NAS: 'ssh -o StrictHostKeyChecking=no matrixhasyou@192.168.10.105 ...'\n" " 📋 Задачи к выполнению (Action Items): конкретные пункты с галочками [ ].\n"
"- MacMini M4: 'ssh -o StrictHostKeyChecking=no matrixhasyou@192.168.10.167 ...'\n" " 💡 Идеи в копилку / Бэклог: новые задумки, архитектурные гипотезы.\n"
"- MikroTik Home: 'ssh -o StrictHostKeyChecking=no matrixhasyou@192.168.10.215 ...'\n" " 🗄️ Куда зафиксировать: в какую заметку Obsidian (HomeLab или Work) это стоит сохранить.\n\n"
"- MikroTik Office: 'ssh -o StrictHostKeyChecking=no atyrin@10.7.0.1 ...'\n" "2. СИСТЕМНЫЕ КОМАНДЫ (если Александр просит проверить ресурсы, диски, сеть, контейнеры, запустить что-то):\n"
"- Office Server simtech: 'ssh -o StrictHostKeyChecking=no ops@10.7.4.173 ...'\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"
" В САМОМ КОНЦЕ ответа ОБЯЗАТЕЛЬНО добавь тег <voice>...</voice>.\n"
" Внутри него напиши короткое, живое, теплое и емкое резюме от Майи (1-3 предложения), которое будет озвучено вслух! "
" Внутри <voice> пиши только чистый русский текст без звездочек, эмодзи, списков и 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 last_error = None
for model_name in models_to_try: for model_name in models_to_try:
try: try:
logger.info(f"🚀 [Antigravity Agent] Попытка запуска с моделью: {model_name}") logger.info(f"🚀 [Maya Agent] Запуск с моделью: {model_name}")
model = genai.GenerativeModel( model = genai.GenerativeModel(
model_name=model_name, model_name=model_name,
system_instruction=system_instruction, system_instruction=system_instruction,
@ -133,33 +152,59 @@ async def execute_antigravity_agent(user_text: str) -> str:
if response and response.text: if response and response.text:
return response.text.strip() return response.text.strip()
except Exception as e: except Exception as e:
logger.warning(f"⚠️ [Antigravity Agent] Сбой на модели {model_name}: {e}") logger.warning(f"⚠️ [Maya Agent] Сбой на модели {model_name}: {e}")
last_error = 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): async def handle_voice_message(message: Message, bot: Bot):
# ПЕРЕВОДИМ И ИСПОЛНЯЕМ ТОЛЬКО ДЛЯ ТЕБЯ (СУПЕРАДМИНА) # ПЕРЕВОДИМ И ИСПОЛНЯЕМ СТРОГО ДЛЯ АЛЕКСАНДРА (ЧУЖИХ НЕ ПУСКАЕМ!)
if int(message.from_user.id) != int(ADMIN_USER_ID): if int(message.from_user.id) != int(ADMIN_USER_ID):
return return
status_msg = await message.reply("🎙 <i>Слушаю и расшифровываю через Gemini Multimodal API...</i>", 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("🎙 <i>Слушаю и анализирую аудиозапись...</i>", parse_mode="HTML")
await bot.send_chat_action(message.chat.id, "typing") await bot.send_chat_action(message.chat.id, "typing")
try: 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}" local_audio_path = f"/tmp/{filename}"
file_info = await bot.get_file(file_id) file_info = await bot.get_file(file_id)
await bot.download_file(file_info.file_path, local_audio_path) 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: if transcribed_text:
log_voice_to_db(message.from_user.id, message.from_user.username, msg_type, int(duration), 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"🗣 <b>Команда принята:</b> «<i>{transcribed_text}</i>»\n\n⚙️ <b>Агент выполняет системную команду...</b>", parse_mode="HTML") await status_msg.edit_text(f"🗣 <b>Услышано:</b> «<i>{transcribed_text}</i>»\n\n⚙️ <b>Майа систематизирует и готовит ответ...</b>", parse_mode="HTML")
await bot.send_chat_action(message.chat.id, "record_voice") 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) agent_response = await execute_antigravity_agent(transcribed_text)
# 3. Отправка текстового ответа # 3. Подготовка текста для отправки (вырезаем тег <voice> из визуального сообщения)
formatted_text = f"🎯 <b>Ответ Antigravity Agent:</b>\n\n{agent_response}" clean_text = re.sub(r"<voice>.*?</voice>", "", agent_response, flags=re.DOTALL).strip()
formatted_text = f"🎯 <b>Майа // Структурированный отклик:</b>\n\n{clean_text}"
# Разделяем длинный текст Telegram если нужно (TG limit 4096) # Разделяем длинный текст Telegram если нужно (TG limit 4096)
if len(formatted_text) > 4000: 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) voice_bytes = await text_to_speech_bytes(agent_response)
if voice_bytes: if voice_bytes:
voice_file = BufferedInputFile(voice_bytes, filename="reply_voice.mp3") 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: except Exception as tts_err:
logger.error(f"TTS Send error: {tts_err}") logger.error(f"TTS Send error: {tts_err}")
else: else:
await status_msg.edit_text("🔇 Голос принят, но Gemini не разобрал слов. Попробуй наговорить еще раз.") await status_msg.edit_text("🔇 Аудиозапись принята, но слова не распознаны. Попробуй наговорить еще раз.")
except Exception as e: except Exception as e:
logger.error(f"❌ Gemini Voice Integration Error: {e}") logger.error(f"❌ Maya Voice Integration Error: {e}")
await status_msg.edit_text(f"❌ Ошибка голосового моста: {e}", parse_mode="HTML") await status_msg.edit_text(f"❌ Ошибка обработки аудио: {e}", parse_mode="HTML")
voice_router = router voice_router = router

View file

@ -25,3 +25,4 @@ google-auth-httplib2
google-auth-oauthlib google-auth-oauthlib
edge-tts>=7.0.0