diff --git a/src/bot/main.py b/src/bot/main.py index f4a8b10..9c9422e 100644 --- a/src/bot/main.py +++ b/src/bot/main.py @@ -13,6 +13,7 @@ from telegram import InlineKeyboardButton, InlineKeyboardMarkup from config.config import get_settings from src.tools.orchestrator import Orchestrator from src.tools.xray import get_xray_client +from src.speech.speech import SpeechRecognizer from src.bot.states import chat_state, ChatMode, XRayState from src.bot.config_manager import get_selected_tool, get_selected_model, set_tool @@ -27,6 +28,7 @@ logging.getLogger("asyncssh").setLevel(logging.ERROR) settings = get_settings() orchestrator = Orchestrator() +speech_recognizer = SpeechRecognizer() DANGEROUS_PATTERNS = [ r'\bwrite\b', r'\bedit\b', r'\bcopy\b', r'\bmove\b', r'\bdelete\b', @@ -124,7 +126,8 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE): "/open - Выбрать модель OpenCode\n" "/mode confirm/auto - Режим подтверждения\n" "/forget - Очистить историю\n" - "/xray [email] - Добавить пользователя XRay\n\n" + "/xray [email] - Добавить пользователя XRay\n" + "/stt on|off - Распознавание речи\n\n" f"🔧 Текущая модель: {current_tool}" ) if model: @@ -297,6 +300,24 @@ async def forget_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await update.message.reply_text("🗑️ История чата очищена.") +async def stt_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Включение/выключение распознавания речи""" + if not context.args: + status = "включено" if speech_recognizer.is_enabled() else "отключено" + await update.message.reply_text(f"🎤 Распознавание речи: {status}\n\nИспользование: /stt on | off") + return + + arg = context.args[0].lower() + if arg == "on": + speech_recognizer.toggle(True) + await update.message.reply_text("🎤 Распознавание речи включено") + elif arg == "off": + speech_recognizer.toggle(False) + await update.message.reply_text("🎤 Распознавание речи отключено") + else: + await update.message.reply_text("Использование: /stt on | off") + + async def xray_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Команда /xray - добавление пользователя XRay @@ -398,14 +419,101 @@ async def handle_xray_email(update: Update, context: ContextTypes.DEFAULT_TYPE): """Обработчик ввода email для команды /xray""" chat_id = update.effective_chat.id state = chat_state.get_xray_state(chat_id) - + if state != XRayState.WAITING_EMAIL: return # Не в состоянии ожидания email - + email = update.message.text.strip() await process_xray_email(update, context, email) +async def handle_voice(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработчик голосовых сообщений""" + if not speech_recognizer.is_enabled(): + await update.message.reply_text( + "🎤 Распознавание речи отключено.\n" + "Включите командой /stt on" + ) + return + + chat_id = update.effective_chat.id + + # Получаем файл голосового сообщения + file = await update.message.voice.get_file() + + # Создаем временный файл для сохранения + temp_dir = "/tmp/valera_voice" + os.makedirs(temp_dir, exist_ok=True) + temp_ogg = os.path.join(temp_dir, f"{chat_id}_{file.file_id}.ogg") + + try: + # Скачиваем файл + await file.download_to_drive(temp_ogg) + + progress_msg = await update.message.reply_text("🎤 Распознаю голосовое сообщение...") + + # Конвертируем и распознаем + wav_path = temp_ogg.replace(".ogg", ".wav") + + # Конвертация через ffmpeg + process = await asyncio.create_subprocess_exec( + "ffmpeg", "-i", temp_ogg, "-ar", "16000", "-ac", "1", wav_path, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + await process.communicate() + + # Распознавание + text = await speech_recognizer.recognize(wav_path) + + # Удаляем временные файлы + if os.path.exists(temp_ogg): + os.remove(temp_ogg) + + if not text: + await progress_msg.edit_text("❌ Не удалось распознать голосовое сообщение") + return + + await progress_msg.delete() + + # Показываем распознанный текст + await update.message.reply_text(f"🎤 Вы сказали:\n{text}") + + # Обрабатываем текст как обычное сообщение + await process_text_as_message(update, context, text) + + except Exception as e: + logger.error(f"Ошибка распознавания голоса: {e}") + if os.path.exists(temp_ogg): + os.remove(temp_ogg) + await update.message.reply_text(f"❌ Ошибка распознавания: {e}") + + +async def process_text_as_message(update: Update, context: ContextTypes.DEFAULT_TYPE, text: str): + """Обработка распознанного текста как обычного сообщения""" + chat_id = update.effective_chat.id + mode = chat_state.get_mode(chat_id) + tool = get_selected_tool() + model = get_selected_model() if tool == "opencode" else None + + # Проверяем не ждем ли мы email для XRay + if chat_state.get_xray_state(chat_id) == XRayState.WAITING_EMAIL: + email = text.strip() + await process_xray_email(update, context, email) + return + + thinking_msg = await update.message.reply_text("🤔 Думаю...") + + result, success = await orchestrator.ask(text, chat_id, tool, model, yolo=True) + + text_result = result.strip() if result else "" + if not text_result: + text_result = "⚠️ Пустой ответ от модели." + + text_result = text_result[:4096] + await thinking_msg.edit_text(text_result, parse_mode="Markdown") + + async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE): prompt = update.message.text chat_id = update.effective_chat.id @@ -438,6 +546,9 @@ def main(): builder = builder.proxy(settings.telegram_proxy_url) application = builder.build() + + # Загружаем модель распознавания речи + speech_recognizer.load_model() application.add_handler(CommandHandler("start", start)) application.add_handler(CommandHandler("help", help_command)) @@ -446,7 +557,9 @@ def main(): application.add_handler(CommandHandler("open", open_command)) application.add_handler(CommandHandler("forget", forget_command)) application.add_handler(CommandHandler("xray", xray_command)) + application.add_handler(CommandHandler("stt", stt_command)) application.add_handler(CallbackQueryHandler(confirm_callback)) + application.add_handler(MessageHandler(filters.VOICE, handle_voice)) application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message)) application.run_polling(allowed_updates=Update.ALL_TYPES)