diff --git a/DEVELOPMENT_PLAN.md b/DEVELOPMENT_PLAN.md index 7229cf2..b30d221 100644 --- a/DEVELOPMENT_PLAN.md +++ b/DEVELOPMENT_PLAN.md @@ -427,3 +427,19 @@ v2.0 🔮 Будущее --- *Документ обновляется после каждой версии* + +--- + +## 🤖 v4.3 — AI Voice & Media (Whisper AI) — ЗАВЕРШЕНО (22.04.2026) +**Статус:** ✅ Завершено + +**Функционал:** +- [x] Интеграция модели Faster-Whisper (small) для CPU +- [x] Авто-расшифровка голосовых сообщений (Voice) +- [x] Авто-расшифровка видео-сообщений (Video Notes / Кружочки) +- [x] Исправлена проблема с системными путями ffmpeg/ffprobe в systemd +- [x] Логирование расшифровок в базу данных `voice_recognition_logs` + +**Планы на будущее:** +- [ ] ⏳ **Расшифровка для жильцов:** Разрешить верифицированным пользователям использовать расшифровку (с квотой, например, 3 сообщения в день). +- [ ] 🧠 **AI-Ассистент:** Использование Gemini API для ответов на вопросы жильцов по базе знаний дома. diff --git a/domovoy-bot.service b/domovoy-bot.service index 46d8258..8c9ea66 100644 --- a/domovoy-bot.service +++ b/domovoy-bot.service @@ -7,7 +7,7 @@ Type=simple User=matrixhasyou Group=matrixhasyou WorkingDirectory=/home/matrixhasyou/domovoy_bot -Environment="PATH=/home/matrixhasyou/domovoy_bot/venv/bin" +Environment="PATH=/home/matrixhasyou/domovoy_bot/venv/bin:/usr/local/bin:/usr/bin:/bin" ExecStart=/home/matrixhasyou/domovoy_bot/venv/bin/python /home/matrixhasyou/domovoy_bot/main.py Restart=always RestartSec=10 diff --git a/handlers/voice.py b/handlers/voice.py index 9996d2b..b0bea33 100644 --- a/handlers/voice.py +++ b/handlers/voice.py @@ -1,58 +1,70 @@ import os import logging from aiogram import Router, F, Bot -from aiogram.types import Message, ContentType +from aiogram.types import Message from config import ADMIN_USER_ID from services.voice_service import VoiceService +from pathlib import Path 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") +_voice_service = None -@router.message(F.content_type.in_({ContentType.VOICE, ContentType.VIDEO_NOTE})) +def get_voice_service(): + global _voice_service + if _voice_service is None: + DB_PATH = "/home/matrixhasyou/domovoy_bot/database/domovoy.db" + _voice_service = VoiceService(db_path=DB_PATH, model_size="small") + return _voice_service + +@router.message(F.voice | F.video_note) async def handle_voice_message(message: Message, bot: Bot): - # Только для владельца - if message.from_user.id != ADMIN_USER_ID: + if int(message.from_user.id) != int(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 - - # Отправляем статус "печатает" + status_msg = await message.reply("⏳ Расшифровываю...", parse_mode="HTML") 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) + # Абсолютный путь к временной папке + temp_dir = Path("/home/matrixhasyou/domovoy_bot/temp_voice") + temp_dir.mkdir(parents=True, exist_ok=True) try: + if message.voice: + file_id = message.voice.file_id + ext = "oga" + msg_type = "VOICE" + else: + file_id = message.video_note.file_id + ext = "mp4" + msg_type = "VIDEO_NOTE" + 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) + file_path = temp_dir / f"{file_id}.{ext}" - # Распознавание - 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 - ) + # Скачиваем + await bot.download_file(file.file_path, str(file_path)) - # Удаляем временный файл - if os.path.exists(file_path): os.remove(file_path) + # Ждем секунду, чтобы ФС успела "переварить" файл + import asyncio + await asyncio.sleep(1) + + if not file_path.exists(): + raise FileNotFoundError(f"Файл {file_path} не был скачан!") + + service = get_voice_service() + text = service.transcribe(str(file_path), message.from_user.id, message.from_user.username or "Owner", msg_type) + + if file_path.exists(): file_path.unlink() if text: - await message.reply(f"📝 Распознанный текст:\n\n{text}", parse_mode="HTML") + await status_msg.edit_text(f"📝 РАСШИФРОВКА:\n\n{text}", parse_mode="HTML") else: - await message.reply("🔇 Аудио сообщение пустое или не распознано.") + await status_msg.edit_text("🔇 Не удалось распознать текст.") except Exception as e: - logger.error(f"Voice handler error: {e}") - await message.reply(f"❌ Ошибка обработки: {e}") + logger.error(f"❌ Voice Error: {e}") + await status_msg.edit_text(f"❌ Ошибка: {e}") -# Export router voice_router = router diff --git a/main.py b/main.py index 43585a5..d09bfe5 100644 --- a/main.py +++ b/main.py @@ -1,15 +1,4 @@ #!/usr/bin/env python3 -""" -🏠 Домовой Бот — Telegram-бот для чата многоквартирного дома - -ВЕРСИЯ: v4.1 — VOICE-REBORN -ДАТА ФИКСАЦИИ: 21 апреля 2026 г. -СТАТУС: ✅ ОБНОВЛЕН - -НОВОЕ В v4.1: -- ✅ Распознавание голосовых сообщений и видеокружков (Whisper Small) -- ✅ Логирование распознанного текста в БД -""" import logging import asyncio import sys @@ -21,18 +10,14 @@ BASE_DIR = Path(__file__).resolve().parent sys.path.insert(0, str(BASE_DIR)) from aiogram import Bot, Dispatcher -from aiogram.client.default import DefaultBotProperties from aiogram.enums import ParseMode -from aiogram.filters import Command -from aiogram.types import Message -from aiogram.client.session.aiohttp import AiohttpSession from aiogram.fsm.storage.memory import MemoryStorage -from config import ( - BOT_TOKEN, ADMIN_USER_ID, ADMIN_CHAT_ID, get_proxy_url, - USE_PROXY, PROXY_TYPE, PROXY_HOST, PROXY_PORT -) +from config import BOT_TOKEN, ADMIN_USER_ID from database.db import init_db + +# Импортируем роутеры +from handlers.voice import voice_router from handlers import ( admin_router, users_router, verification_router, chat_monitor_router, security_router, apartments_router, antimat_router, phones_router, @@ -41,10 +26,8 @@ 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 # Настройка логирования logging.basicConfig( @@ -55,7 +38,6 @@ logging.basicConfig( logging.StreamHandler() ] ) - logger = logging.getLogger(__name__) async def on_startup(bot: Bot): @@ -63,33 +45,31 @@ async def on_startup(bot: Bot): scheduler = Scheduler(bot) scheduler.start() try: - await bot.send_message( - ADMIN_USER_ID, - '✅ LKM37 CORE v4.1 VOICE-REBORN АКТИВИРОВАН\n\n' - 'Модуль распознавания речи (Whisper Small) запущен.\n' - 'Шли мне голосовухи — я их теперь "читаю"!', - parse_mode=ParseMode.HTML - ) - except Exception as e: - logger.error(f'Ошибка уведомления админа: {e}') + await bot.send_message(ADMIN_USER_ID, '✅ LKM37 CORE v4.3 АКТИВИРОВАН') + except: pass async def main(): from bot_instance import get_bot bot = get_bot() dp = Dispatcher(storage=MemoryStorage()) - # Регистрация роутеров - routers = [ - admin_router, users_router, verification_router, chat_monitor_router, - security_router, apartments_router, antimat_router, phones_router, - polls_router, antispam_router, ratelimit_router, schedule_router, - 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, voice_router # Добавляем голосовой роутер + # КРИТИЧЕСКИ ВАЖНО: voice_router должен быть САМЫМ ПЕРВЫМ + dp.include_router(voice_router) + + # Регистрация остальных + dp.include_router(admin_router) + dp.include_router(users_router) + + other_routers = [ + verification_router, chat_monitor_router, security_router, + apartments_router, antimat_router, phones_router, polls_router, + antispam_router, ratelimit_router, schedule_router, 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 ] - for r in routers: + for r in other_routers: dp.include_router(r) from handlers.smart_broadcast import router as smart_broadcast_router @@ -98,8 +78,7 @@ async def main(): dp.include_router(digest_router) dp.startup.register(on_startup) - - logger.info('🤖 Ожидание команд субъектов...') + logger.info('🤖 Бот запускается...') await dp.start_polling(bot) if __name__ == '__main__': diff --git a/services/voice_service.py b/services/voice_service.py index 7cf0829..c04b8e5 100644 --- a/services/voice_service.py +++ b/services/voice_service.py @@ -4,14 +4,19 @@ from faster_whisper import WhisperModel import sqlite3 from datetime import datetime from pydub import AudioSegment +from pathlib import Path logger = logging.getLogger(__name__) +# ЖЕСТКИЙ ФИКС ПУТЕЙ К БИНАРНИКАМ +AudioSegment.converter = "/usr/bin/ffmpeg" +AudioSegment.ffprobe = "/usr/bin/ffprobe" + 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 + logger.info(f"💾 Загрузка модели Whisper ({model_size})...") self.model = WhisperModel(model_size, device="cpu", compute_type="int8") self._init_db() @@ -33,28 +38,31 @@ class VoiceService: conn.close() def transcribe(self, file_path: str, user_id: int, username: str, msg_type: str) -> str: + f_path = Path(file_path) + if not f_path.exists(): + raise FileNotFoundError(f"Файл не найден: {file_path}") + try: - start_time = datetime.now() + logger.info(f"🔄 Конвертация {f_path.name}...") + wav_path = str(f_path.with_suffix('.wav')) - # Convert to wav if needed (Telegram uses .oga/.ogg) - wav_path = file_path.rsplit('.', 1)[0] + ".wav" - audio = AudioSegment.from_file(file_path) + # Конвертируем + audio = AudioSegment.from_file(str(f_path)) duration = int(len(audio) / 1000) audio.export(wav_path, format="wav") + logger.info(f"🧠 Расшифровка ({duration}с)...") 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}" + logger.error(f"❌ Ошибка транскрибации: {e}", exc_info=True) + raise e def _log_to_db(self, user_id, username, msg_type, duration, text): try: @@ -68,5 +76,3 @@ class VoiceService: conn.close() except Exception as e: logger.error(f"DB Log error: {e}") - -# Global instance will be initialized in main.py or handlers diff --git a/temp_voice/AwACAgIAAxkBAAIEaWnon8hg8TzKl_27z4Cs7HLQqtmRAAJ6lwACT5A5S2Bo6N2mNVhEOwQ.oga b/temp_voice/AwACAgIAAxkBAAIEaWnon8hg8TzKl_27z4Cs7HLQqtmRAAJ6lwACT5A5S2Bo6N2mNVhEOwQ.oga new file mode 100644 index 0000000..fbacfee Binary files /dev/null and b/temp_voice/AwACAgIAAxkBAAIEaWnon8hg8TzKl_27z4Cs7HLQqtmRAAJ6lwACT5A5S2Bo6N2mNVhEOwQ.oga differ