FEAT: Voice Recognition (Whisper Small) for owner - v4.1
This commit is contained in:
parent
f7c06fd471
commit
1d5752dfd4
5 changed files with 145 additions and 29 deletions
|
|
@ -5,7 +5,7 @@ FROM python:3.12-slim
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Установка системных зависимостей (если понадобятся для каких-то либ)
|
# Установка системных зависимостей (если понадобятся для каких-то либ)
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \ ffmpeg
|
||||||
sqlite3 \
|
sqlite3 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
|
||||||
58
handlers/voice.py
Normal file
58
handlers/voice.py
Normal file
|
|
@ -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"📝 <b>Распознанный текст:</b>\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
|
||||||
40
main.py
40
main.py
|
|
@ -2,22 +2,18 @@
|
||||||
"""
|
"""
|
||||||
🏠 Домовой Бот — Telegram-бот для чата многоквартирного дома
|
🏠 Домовой Бот — Telegram-бот для чата многоквартирного дома
|
||||||
|
|
||||||
ВЕРСИЯ: v4.0 — REBORN (LKM37 CORE)
|
ВЕРСИЯ: v4.1 — VOICE-REBORN
|
||||||
ДАТА ФИКСАЦИИ: 17 апреля 2026 г.
|
ДАТА ФИКСАЦИИ: 21 апреля 2026 г.
|
||||||
СТАТУС: ✅ АКТИВИРОВАН
|
СТАТУС: ✅ ОБНОВЛЕН
|
||||||
|
|
||||||
НОВОЕ В v4.0:
|
НОВОЕ В v4.1:
|
||||||
- ✅ Единый стиль LKM37 (Slate & Cyan)
|
- ✅ Распознавание голосовых сообщений и видеокружков (Whisper Small)
|
||||||
- ✅ Стабильное меню (Grid 2xN)
|
- ✅ Логирование распознанного текста в БД
|
||||||
- ✅ Маркет 2.0 (Фото + Категории)
|
|
||||||
- ✅ SpyDetector 2.0 (Анализ токсичности)
|
|
||||||
- ✅ Взаимовыручка соседей (Neighbor Aid)
|
|
||||||
- ✅ Авто-Дайджест с подтверждением
|
|
||||||
- ✅ Матричная веб-панель
|
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Добавляем путь к проекту
|
# Добавляем путь к проекту
|
||||||
|
|
@ -45,10 +41,10 @@ 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
|
from services.web_server import WebServerService
|
||||||
from database.models import ScheduledPost
|
|
||||||
|
|
||||||
# Настройка логирования
|
# Настройка логирования
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
|
|
@ -60,30 +56,18 @@ logging.basicConfig(
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
import os
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
# МАРКЕР ЗАПУСКА БОТА
|
|
||||||
logger.info(f"!!! [STARTUP] main.py execution started. PID: {os.getpid()}")
|
|
||||||
|
|
||||||
async def on_startup(bot: Bot):
|
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()
|
await init_db()
|
||||||
|
|
||||||
scheduler = Scheduler(bot)
|
scheduler = Scheduler(bot)
|
||||||
scheduler.start()
|
scheduler.start()
|
||||||
|
|
||||||
# Веб-сервер теперь запускается отдельным сервисом (domovoy-web.service)
|
|
||||||
# web_server = WebServerService(port=8000)
|
|
||||||
# web_server.start()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await bot.send_message(
|
await bot.send_message(
|
||||||
ADMIN_USER_ID,
|
ADMIN_USER_ID,
|
||||||
'✅ <b>LKM37 CORE v4.0 АКТИВИРОВАН</b>\n\n'
|
'✅ <b>LKM37 CORE v4.1 VOICE-REBORN АКТИВИРОВАН</b>\n\n'
|
||||||
'Система переведена в режим REBORN.\n'
|
'Модуль распознавания речи (Whisper Small) запущен.\n'
|
||||||
'Все модули стабильны.',
|
'Шли мне голосовухи — я их теперь "читаю"!',
|
||||||
parse_mode=ParseMode.HTML
|
parse_mode=ParseMode.HTML
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -102,7 +86,7 @@ async def main():
|
||||||
ads_router, payments_router, multicount_router, topics_router,
|
ads_router, payments_router, multicount_router, topics_router,
|
||||||
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,
|
||||||
mutual_aid_router
|
mutual_aid_router, voice_router # Добавляем голосовой роутер
|
||||||
]
|
]
|
||||||
|
|
||||||
for r in routers:
|
for r in routers:
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,5 @@ fastapi>=0.109.0
|
||||||
uvicorn>=0.27.0
|
uvicorn>=0.27.0
|
||||||
jinja2>=3.1.3
|
jinja2>=3.1.3
|
||||||
python-multipart>=0.0.6
|
python-multipart>=0.0.6
|
||||||
|
faster-whisper
|
||||||
|
pydub
|
||||||
|
|
|
||||||
72
services/voice_service.py
Normal file
72
services/voice_service.py
Normal file
|
|
@ -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
|
||||||
Loading…
Reference in a new issue