From 1d5752dfd4cb154d142bac335589f79b8ede99ba Mon Sep 17 00:00:00 2001 From: Admin Date: Wed, 22 Apr 2026 12:42:37 +0400 Subject: [PATCH] FEAT: Voice Recognition (Whisper Small) for owner - v4.1 --- Dockerfile | 2 +- handlers/voice.py | 58 +++++++++++++++++++++++++++++++ main.py | 40 +++++++--------------- requirements.txt | 2 ++ services/voice_service.py | 72 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 145 insertions(+), 29 deletions(-) create mode 100644 handlers/voice.py create mode 100644 services/voice_service.py diff --git a/Dockerfile b/Dockerfile index ac55850..ae0f239 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ FROM python:3.12-slim WORKDIR /app # Установка системных зависимостей (если понадобятся для каких-то либ) -RUN apt-get update && apt-get install -y \ +RUN apt-get update && apt-get install -y \ ffmpeg sqlite3 \ && rm -rf /var/lib/apt/lists/* diff --git a/handlers/voice.py b/handlers/voice.py new file mode 100644 index 0000000..9996d2b --- /dev/null +++ b/handlers/voice.py @@ -0,0 +1,58 @@ +import os +import logging +from aiogram import Router, F, Bot +from aiogram.types import Message, ContentType +from config import ADMIN_USER_ID +from services.voice_service import VoiceService + +logger = logging.getLogger(__name__) +router = Router() + +# Initialize VoiceService +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +db_path = os.path.join(BASE_DIR, "database", "domovoy.db") +voice_service = VoiceService(db_path=db_path, model_size="small") + +@router.message(F.content_type.in_({ContentType.VOICE, ContentType.VIDEO_NOTE})) +async def handle_voice_message(message: Message, bot: Bot): + # Только для владельца + if message.from_user.id != ADMIN_USER_ID: + return + + msg_type = "VOICE" if message.voice else "VIDEO_NOTE" + file_id = message.voice.file_id if message.voice else message.video_note.file_id + + # Отправляем статус "печатает" + await bot.send_chat_action(message.chat.id, "typing") + + # Создаем папку для временных файлов если её нет + temp_dir = os.path.join(BASE_DIR, "temp_voice") + os.makedirs(temp_dir, exist_ok=True) + + try: + file = await bot.get_file(file_id) + file_path = os.path.join(temp_dir, f"{file_id}.oga" if message.voice else f"{file_id}.mp4") + await bot.download_file(file.file_path, file_path) + + # Распознавание + text = voice_service.transcribe( + file_path=file_path, + user_id=message.from_user.id, + username=message.from_user.username or "Unknown", + msg_type=msg_type + ) + + # Удаляем временный файл + if os.path.exists(file_path): os.remove(file_path) + + if text: + await message.reply(f"📝 Распознанный текст:\n\n{text}", parse_mode="HTML") + else: + await message.reply("🔇 Аудио сообщение пустое или не распознано.") + + except Exception as e: + logger.error(f"Voice handler error: {e}") + await message.reply(f"❌ Ошибка обработки: {e}") + +# Export router +voice_router = router diff --git a/main.py b/main.py index 3bbb463..43585a5 100644 --- a/main.py +++ b/main.py @@ -2,22 +2,18 @@ """ 🏠 Домовой Бот — Telegram-бот для чата многоквартирного дома -ВЕРСИЯ: v4.0 — REBORN (LKM37 CORE) -ДАТА ФИКСАЦИИ: 17 апреля 2026 г. -СТАТУС: ✅ АКТИВИРОВАН +ВЕРСИЯ: v4.1 — VOICE-REBORN +ДАТА ФИКСАЦИИ: 21 апреля 2026 г. +СТАТУС: ✅ ОБНОВЛЕН -НОВОЕ В v4.0: -- ✅ Единый стиль LKM37 (Slate & Cyan) -- ✅ Стабильное меню (Grid 2xN) -- ✅ Маркет 2.0 (Фото + Категории) -- ✅ SpyDetector 2.0 (Анализ токсичности) -- ✅ Взаимовыручка соседей (Neighbor Aid) -- ✅ Авто-Дайджест с подтверждением -- ✅ Матричная веб-панель +НОВОЕ В v4.1: +- ✅ Распознавание голосовых сообщений и видеокружков (Whisper Small) +- ✅ Логирование распознанного текста в БД """ import logging import asyncio import sys +import os from pathlib import Path # Добавляем путь к проекту @@ -45,10 +41,10 @@ from handlers import ( toxicity_router, profile_history_router, achievements_router, thanks_router, events_router, initiative_group_router ) +from handlers.voice import voice_router from handlers.mutual_aid import router as mutual_aid_router from services.scheduler import Scheduler from services.web_server import WebServerService -from database.models import ScheduledPost # Настройка логирования logging.basicConfig( @@ -60,30 +56,18 @@ logging.basicConfig( ] ) -import os logger = logging.getLogger(__name__) -# МАРКЕР ЗАПУСКА БОТА -logger.info(f"!!! [STARTUP] main.py execution started. PID: {os.getpid()}") async def on_startup(bot: Bot): - """Действия при запуске""" - logger.info(f"!!! [STARTUP] on_startup triggered. PID: {os.getpid()}") - logger.info('🚀 LKM37 CORE v4.0 запускается...') await init_db() - scheduler = Scheduler(bot) scheduler.start() - - # Веб-сервер теперь запускается отдельным сервисом (domovoy-web.service) - # web_server = WebServerService(port=8000) - # web_server.start() - try: await bot.send_message( ADMIN_USER_ID, - '✅ LKM37 CORE v4.0 АКТИВИРОВАН\n\n' - 'Система переведена в режим REBORN.\n' - 'Все модули стабильны.', + '✅ LKM37 CORE v4.1 VOICE-REBORN АКТИВИРОВАН\n\n' + 'Модуль распознавания речи (Whisper Small) запущен.\n' + 'Шли мне голосовухи — я их теперь "читаю"!', parse_mode=ParseMode.HTML ) except Exception as e: @@ -102,7 +86,7 @@ async def main(): ads_router, payments_router, multicount_router, topics_router, toxicity_router, profile_history_router, achievements_router, thanks_router, events_router, initiative_group_router, - mutual_aid_router + mutual_aid_router, voice_router # Добавляем голосовой роутер ] for r in routers: diff --git a/requirements.txt b/requirements.txt index 9e2ae4e..b650776 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,5 @@ fastapi>=0.109.0 uvicorn>=0.27.0 jinja2>=3.1.3 python-multipart>=0.0.6 +faster-whisper +pydub diff --git a/services/voice_service.py b/services/voice_service.py new file mode 100644 index 0000000..7cf0829 --- /dev/null +++ b/services/voice_service.py @@ -0,0 +1,72 @@ +import os +import logging +from faster_whisper import WhisperModel +import sqlite3 +from datetime import datetime +from pydub import AudioSegment + +logger = logging.getLogger(__name__) + +class VoiceService: + def __init__(self, db_path: str, model_size: str = "small"): + self.db_path = db_path + self.model_size = model_size + # CPU-only optimization: int8 is fast on standard CPUs + self.model = WhisperModel(model_size, device="cpu", compute_type="int8") + self._init_db() + + def _init_db(self): + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute(''' + CREATE TABLE IF NOT EXISTS voice_recognition_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + username TEXT, + message_type TEXT, + duration INTEGER, + text TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + conn.commit() + conn.close() + + def transcribe(self, file_path: str, user_id: int, username: str, msg_type: str) -> str: + try: + start_time = datetime.now() + + # Convert to wav if needed (Telegram uses .oga/.ogg) + wav_path = file_path.rsplit('.', 1)[0] + ".wav" + audio = AudioSegment.from_file(file_path) + duration = int(len(audio) / 1000) + audio.export(wav_path, format="wav") + + segments, info = self.model.transcribe(wav_path, beam_size=5) + text = " ".join([segment.text for segment in segments]).strip() + + # Clean up temp files + if os.path.exists(wav_path): os.remove(wav_path) + + # Log to DB + self._log_to_db(user_id, username, msg_type, duration, text) + + return text + except Exception as e: + logger.error(f"Transcription error: {e}") + return f"[Ошибка распознавания]: {e}" + + def _log_to_db(self, user_id, username, msg_type, duration, text): + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO voice_recognition_logs (user_id, username, message_type, duration, text) + VALUES (?, ?, ?, ?, ?) + ''', (user_id, username, msg_type, duration, text)) + conn.commit() + conn.close() + except Exception as e: + logger.error(f"DB Log error: {e}") + +# Global instance will be initialized in main.py or handlers