From 5bfe439a0f2d0f53f74ac5df95a3e5ef1b4e1e70 Mon Sep 17 00:00:00 2001 From: Admin Date: Sun, 6 Sep 2026 00:37:03 +0400 Subject: [PATCH] fix(voice): true 3-stage voice processing: 1. raw recognized text in chat -> 2. edit message into crystal structure (essence, tasks, ideas) -> 3. separate report on Obsidian save and executed actions --- handlers/voice.py | 189 ++++++++++++++++++++++++++++++---------------- 1 file changed, 125 insertions(+), 64 deletions(-) diff --git a/handlers/voice.py b/handlers/voice.py index bf9eca7..a379767 100644 --- a/handlers/voice.py +++ b/handlers/voice.py @@ -281,35 +281,35 @@ async def transcribe_audio(audio_path: str) -> str: os.remove(tmp_wav) return "" -async def execute_antigravity_agent(user_text: str) -> str: - """Обработка команды и мыслей агентом Майа с систематизацией, Битрикс24 и правом исполнения bash-команд""" - system_instruction = ( - "Ты — Майа (Maya / Antigravity Lead Agent), суперинтеллектуальный помощник Александра с полными правами инженера.\n" - "Александр обращается к тебе голосом в Телеграмме (на ходу, на велосипеде, за рулем, спонтанный поток мыслей с паузами, оговорками и повторами).\n\n" - "ТВОЯ ГЛАВНАЯ ЦЕЛЬ — ПРЕВРАТИТЬ СПОНТАННЫЙ ПОТОК РЕЧИ В КРИСТАЛЬНО ЧИСТУЮ СТРУКТУРУ И ВЫПОЛНИТЬ ТРЕБУЕМЫЕ ДЕЙСТВИЯ.\n\n" - "ОБЯЗАТЕЛЬНЫЙ ШАБЛОН ТВОЕГО ТЕКСТОВОГО ОТВЕТА (СТРОГО СОБЛЮДАЙ ЭТУ СТРУКТУРУ В КАЖДОМ ОТВЕТЕ!):\n\n" - "🎯 Суть:\n" - "[Очищенная от слов-паразитов, междометий и сумбура суть сказанного в 1-3 емких предложениях]\n\n" - "📋 Задачи к выполнению (Action Items):\n" - "• [ ] [Конкретная задача 1]\n" - "• [ ] [Конкретная задача 2]\n" - "(Если задач к исполнению не было — напиши: «Конкретных задач не поставлено — зафиксировано как идея/мысли»)\n\n" - "💡 Идеи в копилку / Бэклог:\n" - "• [Инсайт, гипотеза или идея на будущее]\n" - "(Если идей не было — напиши: «—»)\n\n" - "🗄️ Действия и фиксация:\n" - "• [ОБЯЗАТЕЛЬНО: Вызови save_to_obsidian и укажи: ✅ Сохранено в Obsidian: раздел/подкаталог/файл.md]\n" - "• [Если Александр просил проверить или сделать что-то — вызови query_bitrix24 или execute_bash_command и напиши конкретный результат выполнения!]\n\n" - "ПРАВИЛА:\n" - "1. ЛЮБЫЕ мысли, идеи, планы, рассуждения ОБЯЗАТЕЛЬНО сохраняй в Obsidian через tool save_to_obsidian(section, subfolder, filename, title, content, tags).\n" - " - section: 'HomeLab', 'Work' или 'Свалка'.\n" - " - subfolder: 'AI_Swarm', 'Infrastructure', 'Bicycle_Ebike', 'Housing_LKM37', 'Visma', 'Bitrix24', 'General'.\n" - " - filename: говорящее имя на латинице (например: 'ebike_controller_upgrade.md').\n" - " - content: структурированный текст заметки.\n" - " КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО писать в чат только фразу «я сохранила в файл»! Ты ОБЯЗАНА полностью вывести блоки 🎯 Суть, 📋 Задачи, 💡 Идеи в сам ответ, чтобы Александр видел структуру прямо перед глазами!\n" - "2. Если Александр попросил действие (Битрикс24, свободное место, контейнеры, сервера) — вызови инструмент (query_bitrix24 / execute_bash_command) и выведи результат в блок «🗄️ Действия и фиксация».\n" - "3. В САМОМ КОНЦЕ ответа ОБЯЗАТЕЛЬНО добавь тег ... с короткой (1-2 предложения), теплой голосовой выжимкой для Шаде без Markdown и эмодзи." - ) +async def structure_thoughts(user_text: str) -> str: + """Этап 2: Превращение сумбурного потока мыслей в кристальную структуру с метаданными для Obsidian""" + prompt = f""" +Ты — Майа, личный аналитик и инженер Александра. +Александр наговорил голосовое сообщение на ходу. В речи могут быть паузы, оговорки, повторы, слова-паразиты, сумбур. + +Твоя задача — кристально структурировать надиктованное по строгой схеме: + +🎯 Суть: +[Главное зерно мысли в 1-2 емких, четких предложениях, полностью очищенных от мусорных слов] + +📋 Задачи к выполнению: +* [ ] [Конкретная задача 1] +* [ ] [Конкретная задача 2] +(Если задач нет — напиши: «Конкретных задач не поставлено — зафиксировано как идея/мысли») + +💡 Идеи в копилку / Бэклог: +* [Инсайт, гипотеза или идея на перспективу] +(Если идей нет — напиши: «—») + +ОБЯЗАТЕЛЬНО добавь в конце метаданные для сохранения в Obsidian: + + +И добавь тег с короткой теплой выжимкой для синтеза речи (1-2 предложения без markdown): +Текст выжимки от первого лица для озвучки + +Сырой текст речи Александра: +«{user_text}» +""" models_to_try = [ 'gemini-2.5-flash', @@ -317,30 +317,69 @@ async def execute_antigravity_agent(user_text: str) -> str: 'gemini-2.5-pro', 'gemini-3.5-flash' ] - last_error = None - def _sync_run_agent(m_name: str, u_text: str) -> str: + for model_name in models_to_try: + try: + logger.info(f"🧠 [Maya Structuring] Запуск с моделью: {model_name}") + def _sync_gen(m_name, p): + model = genai.GenerativeModel(m_name) + resp = model.generate_content(p) + return resp.text.strip() if resp and resp.text else "" + + res = await asyncio.to_thread(_sync_gen, model_name, prompt) + if res: + return res + except Exception as e: + logger.warning(f"⚠️ [Maya Structuring] Сбой на модели {model_name}: {e}") + await asyncio.sleep(0.5) + + return ( + f"🎯 Суть:\n{user_text}\n\n" + f"📋 Задачи к выполнению:\n* [ ] Разобрать надиктованное голосовое сообщение\n\n" + f"💡 Идеи в копилку / Бэклог:\n—\n\n" + f"\n" + f"Я зафиксировала твою мысль и сохранила её в Obsidian." + ) + +async def execute_voice_commands_if_any(user_text: str) -> str: + """Этап 3: Проверка и выполнение реальных инженерных команд (Битрикс24, bash, серверы)""" + system_instruction = ( + "Ты — Майа, системный инженер и исполнитель команд Александра.\n" + "Проанализируй текст Александра. Если он содержит ЯВНЫЕ команды к исполнению прямо сейчас " + "(например: проверить задачи в Битрикс24, посмотреть статус сервера, проверить диск/память/докер, выполнить команду):\n" + "1. Немедленно вызови соответствующий инструмент (query_bitrix24 или execute_bash_command).\n" + "2. Дай четкий, емкий инженерный результат выполнения.\n" + "Если же текст Александра — это ПРОСТО мысли, рассуждения, идеи или планы на будущее (без прямого приказа выполнить что-то прямо сейчас) — " + "верни СТРОГО ОДНО СЛОВО: НЕТ_КОМАНД" + ) + + models_to_try = [ + 'gemini-2.5-flash', + 'gemini-flash-latest', + 'gemini-2.5-pro' + ] + + def _sync_cmd(m_name, u_txt): model = genai.GenerativeModel( model_name=m_name, system_instruction=system_instruction, - tools=[query_bitrix24, execute_bash_command, save_to_obsidian] + tools=[query_bitrix24, execute_bash_command] ) chat = model.start_chat(enable_automatic_function_calling=True) - resp = chat.send_message(u_text) + resp = chat.send_message(u_txt) return resp.text.strip() if resp and resp.text else "" for model_name in models_to_try: try: - logger.info(f"🚀 [Maya Agent] Запуск с моделью: {model_name}") - response_text = await asyncio.to_thread(_sync_run_agent, model_name, user_text) - if response_text: - return response_text + logger.info(f"⚡️ [Maya Commands] Проверка наличия команд ({model_name})...") + res = await asyncio.to_thread(_sync_cmd, model_name, user_text) + if res: + return res except Exception as e: - logger.warning(f"⚠️ [Maya Agent] Сбой на модели {model_name}: {e}") - last_error = e + logger.warning(f"⚠️ [Maya Commands] Ошибка {model_name}: {e}") await asyncio.sleep(0.5) - return f"❌ Ошибка исполнения Maya Agent: {last_error}" + return "НЕТ_КОМАНД" @router.message(F.voice | F.video_note | F.audio | F.document) async def handle_voice_message(message: Message, bot: Bot): @@ -390,13 +429,13 @@ async def handle_voice_message(message: Message, bot: Bot): size_mb = round(file_size / (1024 * 1024), 1) await message.reply( f"⚠️ Файл слишком большой ({size_mb} МБ).\n\n" - "Telegram Bot API жестко ограничивает скачивание ботами файлов размером до 20 МБ.\n" - "💡 Решение: Сконвертируй запись диктофона в AAC/M4A/MP3 с более низким битрейтом (например, 64 kbps mono) или надиктуй голосовыми сообщениями прямо в чат!", + "Telegram Bot API ограничивает скачивание ботами файлов размером до 20 МБ.\n" + "💡 Решение: Сконвертируй запись диктофона в AAC/M4A/MP3 (64 kbps mono) или надиктуй голосовыми в чат!", parse_mode="HTML" ) return - status_msg = await message.reply("🎙 Слушаю и выполняю команду...", parse_mode="HTML") + status_msg = await message.reply("🎙 Слушаю аудиозапись...", parse_mode="HTML") await bot.send_chat_action(message.chat.id, "typing") try: @@ -404,7 +443,7 @@ async def handle_voice_message(message: Message, bot: Bot): file_info = await bot.get_file(file_id) await bot.download_file(file_info.file_path, local_audio_path) - # 1. Прямая расшифровка через локальный Whisper M4 (с фоллбэком на Gemini) + # 1. Расшифровка через Gemini Audio API (с нормализацией ffmpeg) transcribed_text = await transcribe_audio(local_audio_path) if os.path.exists(local_audio_path): @@ -413,34 +452,56 @@ 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) - safe_transcribed = html.escape(transcribed_text) + # ===== ЭТАП 1: Показываем сырой текст как есть (без эффектов) ===== + safe_raw = html.escape(transcribed_text) await status_msg.edit_text( - f"🗣 Вы сказали:\n{safe_transcribed}\n\n" - f"🧠 Майа структурирует мысли и выполняет действия...", + f"🗣 Услышано:\n«{safe_raw}»\n\n" + f"🧠 Майа структурирует мысли и вникает в суть...", parse_mode="HTML" ) - await bot.send_chat_action(message.chat.id, "record_voice") + await bot.send_chat_action(message.chat.id, "typing") - # 2. Исполнение Maya Lead Agent (Битрикс24, bash, анализ) - agent_response = await execute_antigravity_agent(transcribed_text) + # ===== ЭТАП 2: Структурирование мыслей через модель ===== + structured_raw = await structure_thoughts(transcribed_text) - # 3. Подготовка текста для отправки (вырезаем тег ) - clean_text = re.sub(r".*?", "", agent_response, flags=re.DOTALL).strip() - formatted_text = ( - f"🗣 Вы сказали:\n{safe_transcribed}\n\n" - f"🎯 Майа // Структурированный отклик:\n\n" - f"{clean_text}" + # Извлекаем теги метаданных и озвучки + m_obs = re.search( + r"", + structured_raw ) - - 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") + obs_section = m_obs.group(1) if m_obs else "HomeLab" + obs_subfolder = m_obs.group(2) if m_obs else "General" + obs_filename = m_obs.group(3) if m_obs else f"note_{int(datetime.now().timestamp())}.md" + obs_title = m_obs.group(4) if m_obs else "Голосовая мысль" - # 4. Синтез и отправка голосового ответа + # Очищаем структурированный текст для отображения в Telegram + clean_structure = re.sub(r"", "", structured_raw, flags=re.DOTALL) + clean_structure = re.sub(r".*?", "", clean_structure, flags=re.DOTALL).strip() + + # Редактируем статусное сообщение: сырой текст заменяется на красивую структуру! + formatted_structure = f"🎯 Майа // Структурированный отклик:\n\n{clean_structure}" + if len(formatted_structure) > 4000: + await status_msg.edit_text(formatted_structure[:4000], parse_mode="HTML") + else: + await status_msg.edit_text(formatted_structure, parse_mode="HTML") + + # ===== ЭТАП 3: Физические действия (Obsidian + Команды) отдельным сообщением! ===== + # 1. Запись в Obsidian + obsidian_res = save_to_obsidian(obs_section, obs_subfolder, obs_filename, obs_title, clean_structure) + + # 2. Выполнение команд (если были) + commands_result = await execute_voice_commands_if_any(transcribed_text) + + # 3. Отправка отчета о действиях ОТДЕЛЬНЫМ сообщением + action_report = f"🗄️ Действия и фиксация:\n• {obsidian_res}" + if commands_result and commands_result.strip() != "НЕТ_КОМАНД": + action_report += f"\n\n⚙️ Результат выполнения:\n{commands_result.strip()}" + + await message.reply(action_report, parse_mode="HTML") + + # ===== 4. Голосовой отклик голосом Шаде ===== try: - voice_bytes = await text_to_speech_bytes(agent_response) + voice_bytes = await text_to_speech_bytes(structured_raw) if voice_bytes: voice_file = BufferedInputFile(voice_bytes, filename="reply_voice.mp3") await message.reply_voice(voice=voice_file, caption="🎙 Голосовой отклик Майи")