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