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
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

View file

@ -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("⏳ <i>Расшифровываю...</i>", 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"📝 <b>Распознанный текст:</b>\n\n{text}", parse_mode="HTML")
await status_msg.edit_text(f"📝 <b>РАСШИФРОВКА:</b>\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

65
main.py
View file

@ -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,
'✅ <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}')
await bot.send_message(ADMIN_USER_ID, '✅ <b>LKM37 CORE v4.3 АКТИВИРОВАН</b>')
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__':

View file

@ -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