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