feat: 🎙️ AI Voice & Video recognition enabled via Faster-Whisper. Fix ffprobe path issues in systemd.

This commit is contained in:
Admin 2026-04-22 16:36:40 +04:00
parent 1d5752dfd4
commit 827bc97e92
6 changed files with 100 additions and 87 deletions

View file

@ -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 для ответов на вопросы жильцов по базе знаний дома.

View file

@ -7,7 +7,7 @@ Type=simple
User=matrixhasyou User=matrixhasyou
Group=matrixhasyou Group=matrixhasyou
WorkingDirectory=/home/matrixhasyou/domovoy_bot 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 ExecStart=/home/matrixhasyou/domovoy_bot/venv/bin/python /home/matrixhasyou/domovoy_bot/main.py
Restart=always Restart=always
RestartSec=10 RestartSec=10

View file

@ -1,58 +1,70 @@
import os import os
import logging import logging
from aiogram import Router, F, Bot from aiogram import Router, F, Bot
from aiogram.types import Message, ContentType from aiogram.types import Message
from config import ADMIN_USER_ID from config import ADMIN_USER_ID
from services.voice_service import VoiceService from services.voice_service import VoiceService
from pathlib import Path
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
router = Router() router = Router()
# Initialize VoiceService _voice_service = None
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})) 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): async def handle_voice_message(message: Message, bot: Bot):
# Только для владельца if int(message.from_user.id) != int(ADMIN_USER_ID):
if message.from_user.id != ADMIN_USER_ID:
return return
msg_type = "VOICE" if message.voice else "VIDEO_NOTE" status_msg = await message.reply("⏳ <i>Расшифровываю...</i>", parse_mode="HTML")
file_id = message.voice.file_id if message.voice else message.video_note.file_id
# Отправляем статус "печатает"
await bot.send_chat_action(message.chat.id, "typing") await bot.send_chat_action(message.chat.id, "typing")
# Создаем папку для временных файлов если её нет # Абсолютный путь к временной папке
temp_dir = os.path.join(BASE_DIR, "temp_voice") temp_dir = Path("/home/matrixhasyou/domovoy_bot/temp_voice")
os.makedirs(temp_dir, exist_ok=True) temp_dir.mkdir(parents=True, exist_ok=True)
try: 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 = 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") file_path = temp_dir / f"{file_id}.{ext}"
await bot.download_file(file.file_path, file_path)
# Распознавание # Скачиваем
text = voice_service.transcribe( await bot.download_file(file.file_path, str(file_path))
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) 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: if text:
await message.reply(f"📝 <b>Распознанный текст:</b>\n\n{text}", parse_mode="HTML") await status_msg.edit_text(f"📝 <b>РАСШИФРОВКА:</b>\n\n{text}", parse_mode="HTML")
else: else:
await message.reply("🔇 Аудио сообщение пустое или не распознано.") await status_msg.edit_text("🔇 Не удалось распознать текст.")
except Exception as e: except Exception as e:
logger.error(f"Voice handler error: {e}") logger.error(f"❌ Voice Error: {e}")
await message.reply(f"❌ Ошибка обработки: {e}") await status_msg.edit_text(f"❌ Ошибка: {e}")
# Export router
voice_router = router voice_router = router

65
main.py
View file

@ -1,15 +1,4 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""
🏠 Домовой Бот Telegram-бот для чата многоквартирного дома
ВЕРСИЯ: v4.1 VOICE-REBORN
ДАТА ФИКСАЦИИ: 21 апреля 2026 г.
СТАТУС: ОБНОВЛЕН
НОВОЕ В v4.1:
- Распознавание голосовых сообщений и видеокружков (Whisper Small)
- Логирование распознанного текста в БД
"""
import logging import logging
import asyncio import asyncio
import sys import sys
@ -21,18 +10,14 @@ BASE_DIR = Path(__file__).resolve().parent
sys.path.insert(0, str(BASE_DIR)) sys.path.insert(0, str(BASE_DIR))
from aiogram import Bot, Dispatcher from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode 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 aiogram.fsm.storage.memory import MemoryStorage
from config import ( from config import BOT_TOKEN, ADMIN_USER_ID
BOT_TOKEN, ADMIN_USER_ID, ADMIN_CHAT_ID, get_proxy_url,
USE_PROXY, PROXY_TYPE, PROXY_HOST, PROXY_PORT
)
from database.db import init_db from database.db import init_db
# Импортируем роутеры
from handlers.voice import voice_router
from handlers import ( from handlers import (
admin_router, users_router, verification_router, chat_monitor_router, admin_router, users_router, verification_router, chat_monitor_router,
security_router, apartments_router, antimat_router, phones_router, security_router, apartments_router, antimat_router, phones_router,
@ -41,10 +26,8 @@ from handlers import (
toxicity_router, profile_history_router, achievements_router, toxicity_router, profile_history_router, achievements_router,
thanks_router, events_router, initiative_group_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 handlers.mutual_aid import router as mutual_aid_router
from services.scheduler import Scheduler from services.scheduler import Scheduler
from services.web_server import WebServerService
# Настройка логирования # Настройка логирования
logging.basicConfig( logging.basicConfig(
@ -55,7 +38,6 @@ logging.basicConfig(
logging.StreamHandler() logging.StreamHandler()
] ]
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
async def on_startup(bot: Bot): async def on_startup(bot: Bot):
@ -63,33 +45,31 @@ async def on_startup(bot: Bot):
scheduler = Scheduler(bot) scheduler = Scheduler(bot)
scheduler.start() scheduler.start()
try: try:
await bot.send_message( await bot.send_message(ADMIN_USER_ID, '✅ <b>LKM37 CORE v4.3 АКТИВИРОВАН</b>')
ADMIN_USER_ID, except: pass
'✅ <b>LKM37 CORE v4.1 VOICE-REBORN АКТИВИРОВАН</b>\n\n'
'Модуль распознавания речи (Whisper Small) запущен.\n'
'Шли мне голосовухи — я их теперь "читаю"!',
parse_mode=ParseMode.HTML
)
except Exception as e:
logger.error(f'Ошибка уведомления админа: {e}')
async def main(): async def main():
from bot_instance import get_bot from bot_instance import get_bot
bot = get_bot() bot = get_bot()
dp = Dispatcher(storage=MemoryStorage()) dp = Dispatcher(storage=MemoryStorage())
# Регистрация роутеров # КРИТИЧЕСКИ ВАЖНО: voice_router должен быть САМЫМ ПЕРВЫМ
routers = [ dp.include_router(voice_router)
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, dp.include_router(admin_router)
ads_router, payments_router, multicount_router, topics_router, dp.include_router(users_router)
toxicity_router, profile_history_router, achievements_router,
thanks_router, events_router, initiative_group_router, other_routers = [
mutual_aid_router, voice_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
] ]
for r in routers: for r in other_routers:
dp.include_router(r) dp.include_router(r)
from handlers.smart_broadcast import router as smart_broadcast_router from handlers.smart_broadcast import router as smart_broadcast_router
@ -98,8 +78,7 @@ async def main():
dp.include_router(digest_router) dp.include_router(digest_router)
dp.startup.register(on_startup) dp.startup.register(on_startup)
logger.info('🤖 Бот запускается...')
logger.info('🤖 Ожидание команд субъектов...')
await dp.start_polling(bot) await dp.start_polling(bot)
if __name__ == '__main__': if __name__ == '__main__':

View file

@ -4,14 +4,19 @@ from faster_whisper import WhisperModel
import sqlite3 import sqlite3
from datetime import datetime from datetime import datetime
from pydub import AudioSegment from pydub import AudioSegment
from pathlib import Path
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# ЖЕСТКИЙ ФИКС ПУТЕЙ К БИНАРНИКАМ
AudioSegment.converter = "/usr/bin/ffmpeg"
AudioSegment.ffprobe = "/usr/bin/ffprobe"
class VoiceService: class VoiceService:
def __init__(self, db_path: str, model_size: str = "small"): def __init__(self, db_path: str, model_size: str = "small"):
self.db_path = db_path self.db_path = db_path
self.model_size = model_size 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.model = WhisperModel(model_size, device="cpu", compute_type="int8")
self._init_db() self._init_db()
@ -33,28 +38,31 @@ class VoiceService:
conn.close() conn.close()
def transcribe(self, file_path: str, user_id: int, username: str, msg_type: str) -> str: 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: 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(str(f_path))
audio = AudioSegment.from_file(file_path)
duration = int(len(audio) / 1000) duration = int(len(audio) / 1000)
audio.export(wav_path, format="wav") audio.export(wav_path, format="wav")
logger.info(f"🧠 Расшифровка ({duration}с)...")
segments, info = self.model.transcribe(wav_path, beam_size=5) segments, info = self.model.transcribe(wav_path, beam_size=5)
text = " ".join([segment.text for segment in segments]).strip() text = " ".join([segment.text for segment in segments]).strip()
# Clean up temp files # Чистим за собой
if os.path.exists(wav_path): os.remove(wav_path) if os.path.exists(wav_path): os.remove(wav_path)
# Log to DB
self._log_to_db(user_id, username, msg_type, duration, text) self._log_to_db(user_id, username, msg_type, duration, text)
return text return text
except Exception as e: except Exception as e:
logger.error(f"Transcription error: {e}") logger.error(f"❌ Ошибка транскрибации: {e}", exc_info=True)
return f"[Ошибка распознавания]: {e}" raise e
def _log_to_db(self, user_id, username, msg_type, duration, text): def _log_to_db(self, user_id, username, msg_type, duration, text):
try: try:
@ -68,5 +76,3 @@ class VoiceService:
conn.close() conn.close()
except Exception as e: except Exception as e:
logger.error(f"DB Log error: {e}") logger.error(f"DB Log error: {e}")
# Global instance will be initialized in main.py or handlers