feat(voice): upgrade Maya voice hub with audio file support, mind structuring, and deep female TTS
This commit is contained in:
parent
39e7646766
commit
a47d1eee77
2 changed files with 93 additions and 46 deletions
|
|
@ -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 с глубоким тембром"""
|
||||
# Если в тексте есть тег <voice>...</voice>, озвучиваем именно его
|
||||
match = re.search(r"<voice>(.*?)</voice>", 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"
|
||||
" В САМОМ КОНЦЕ ответа ОБЯЗАТЕЛЬНО добавь тег <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
|
||||
|
||||
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("🎙 <i>Слушаю и расшифровываю через Gemini Multimodal API...</i>", parse_mode="HTML")
|
||||
await bot.send_chat_action(message.chat.id, "typing")
|
||||
# Определяем тип медиафайла
|
||||
msg_type = "VOICE"
|
||||
duration = 0
|
||||
filename = "audio.ogg"
|
||||
file_id = None
|
||||
|
||||
try:
|
||||
if message.voice:
|
||||
file_id = message.voice.file_id
|
||||
filename = f"voice_{file_id}.ogg"
|
||||
duration = message.voice.duration
|
||||
msg_type = "VOICE"
|
||||
else:
|
||||
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")
|
||||
|
||||
try:
|
||||
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"🗣 <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")
|
||||
|
||||
# 2. Исполнение Antigravity Agent с вызовом функций хоста
|
||||
# 2. Исполнение Maya Lead Agent (систематизация + команды хоста)
|
||||
agent_response = await execute_antigravity_agent(transcribed_text)
|
||||
|
||||
# 3. Отправка текстового ответа
|
||||
formatted_text = f"🎯 <b>Ответ Antigravity Agent:</b>\n\n{agent_response}"
|
||||
# 3. Подготовка текста для отправки (вырезаем тег <voice> из визуального сообщения)
|
||||
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)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -25,3 +25,4 @@ google-auth-httplib2
|
|||
google-auth-oauthlib
|
||||
|
||||
|
||||
edge-tts>=7.0.0
|
||||
|
|
|
|||
Loading…
Reference in a new issue