Initial commit: полный проект my_speaker
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
# app/__init__.py
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from contextlib import asynccontextmanager
|
||||
import logging
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
import os
|
||||
|
||||
# Настройка логгера
|
||||
logger = logging.getLogger('VADApp')
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Управление жизненным циклом приложения"""
|
||||
from app.core.config import get_settings
|
||||
from app.core.vad import init_vad_and_stt, cleanup_vad_and_stt
|
||||
from app.api.session import set_session_manager
|
||||
|
||||
logger.info("Инициализация приложения...")
|
||||
try:
|
||||
# Загружаем настройки
|
||||
settings = get_settings()
|
||||
|
||||
# Инициализируем аудиоподсистему для TTS
|
||||
import sys
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import piper_TTS
|
||||
logger.info("Инициализация аудиоподсистемы...")
|
||||
piper_TTS.initialize_audio()
|
||||
|
||||
# Инициализируем VAD и STT
|
||||
vad_instance, stt_instance, commands, session_manager = init_vad_and_stt()
|
||||
app.state.vad_instance = vad_instance
|
||||
app.state.stt_instance = stt_instance
|
||||
app.state.commands = commands
|
||||
app.state.session_manager = session_manager
|
||||
|
||||
# Передаем session_manager в API модуль
|
||||
set_session_manager(session_manager)
|
||||
|
||||
from app.core.status import status_tracker, SystemStatus # Добавлено
|
||||
app.state.status_tracker = status_tracker # Добавлено
|
||||
status_tracker.set_status(SystemStatus.LISTENING) # Добавлено
|
||||
|
||||
logger.info("Сервер запущен")
|
||||
yield
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в lifespan: {e}", exc_info=True)
|
||||
raise
|
||||
finally:
|
||||
logger.info("Завершение работы сервера...")
|
||||
cleanup_vad_and_stt(app.state.vad_instance, app.state.stt_instance)
|
||||
logger.info("Сервер завершил работу")
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
"""Создание и настройка приложения FastAPI"""
|
||||
app = FastAPI(title="My Speaker API", lifespan=lifespan)
|
||||
|
||||
# Настройка CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # В продакшене указать конкретные домены
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Подключение маршрутов API
|
||||
from app.api.router import api_router
|
||||
app.include_router(api_router)
|
||||
|
||||
# Монтирование статических файлов на корень
|
||||
app.mount("/", StaticFiles(directory="webapp", html=True), name="static")
|
||||
|
||||
# Удаляем лишние маршруты, так как StaticFiles обрабатывает всё
|
||||
# @app.get("/")
|
||||
# async def read_root():
|
||||
# return FileResponse(os.path.join("webapp", "index.html"))
|
||||
#
|
||||
# @app.get("/index.html")
|
||||
# async def read_index():
|
||||
# return FileResponse(os.path.join("webapp", "index.html"))
|
||||
#
|
||||
# @app.get("/{full_path:path}")
|
||||
# async def catch_all(full_path: str):
|
||||
# if full_path.startswith("api/") or full_path.startswith("static/"):
|
||||
# raise HTTPException(status_code=404, detail="Not Found")
|
||||
# return FileResponse(os.path.join("webapp", "index.html"))
|
||||
#
|
||||
# @app.exception_handler(StarletteHTTPException)
|
||||
# async def custom_http_exception_handler(request, exc):
|
||||
# if exc.status_code == 404:
|
||||
# return FileResponse(os.path.join("webapp", "index.html"))
|
||||
# raise exc
|
||||
|
||||
return app
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
# API package initialization
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,92 @@
|
||||
# app/api/auth.py
|
||||
from fastapi import APIRouter, Header, Query, HTTPException, Depends
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
from pydantic import BaseModel
|
||||
from app.core.config import get_settings
|
||||
from app.core.security import create_session_token, validate_session_token, remove_session_token, ACTIVE_SESSIONS
|
||||
from typing import Optional
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||
security = HTTPBasic()
|
||||
|
||||
class LoginResponse(BaseModel):
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
username: str
|
||||
is_authenticated: bool
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
@router.post("/login", response_model=LoginResponse)
|
||||
async def login(login_data: LoginRequest):
|
||||
"""Базовая авторизация по логину и паролю"""
|
||||
settings = get_settings()
|
||||
|
||||
if login_data.username != settings.auth.username or login_data.password != settings.auth.password:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Неверные учетные данные",
|
||||
headers={"WWW-Authenticate": "Basic"},
|
||||
)
|
||||
|
||||
# Создаем сессионный токен (без JWT, просто уникальный идентификатор)
|
||||
token = create_session_token(login_data.username)
|
||||
|
||||
return LoginResponse(access_token=token)
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
async def get_current_user(
|
||||
authorization: Optional[str] = Header(None), # ← Указываем что это Header
|
||||
token_param: Optional[str] = Query(None, alias="token") # ← Указываем что это Query
|
||||
):
|
||||
"""Получение информации о текущем пользователе"""
|
||||
token_value = None
|
||||
|
||||
# Проверяем заголовок Authorization
|
||||
if authorization and authorization.startswith("Bearer "):
|
||||
token_value = authorization.replace("Bearer ", "")
|
||||
print(f"✓ Токен из заголовка: {token_value}")
|
||||
# Если токен не найден в заголовке, проверяем параметр запроса
|
||||
elif token_param:
|
||||
token_value = token_param
|
||||
print(f"✓ Токен из query параметра: {token_value}")
|
||||
# Для обратной совместимости проверяем, если authorization передан как параметр запроса
|
||||
elif authorization and not authorization.startswith("Bearer "):
|
||||
token_value = authorization
|
||||
print(f"✓ Токен из authorization (без Bearer): {token_value}")
|
||||
|
||||
print(f"Проверка токена: {token_value}")
|
||||
print(f"Активные сессии: {list(ACTIVE_SESSIONS.keys())}")
|
||||
|
||||
# Проверяем токен в активных сессиях
|
||||
if token_value and token_value in ACTIVE_SESSIONS:
|
||||
username = ACTIVE_SESSIONS[token_value]
|
||||
print(f"✓ Токен найден! Пользователь: {username}")
|
||||
return UserResponse(username=username, is_authenticated=True)
|
||||
|
||||
print(f"✗ Токен не найден в активных сессиях")
|
||||
return UserResponse(username="", is_authenticated=False)
|
||||
|
||||
@router.post("/logout")
|
||||
async def logout(authorization: Optional[str] = None, token_param: Optional[str] = None):
|
||||
"""Выход из системы"""
|
||||
token_value = None
|
||||
|
||||
# Проверяем заголовок Authorization
|
||||
if authorization and authorization.startswith("Bearer "):
|
||||
token_value = authorization.replace("Bearer ", "")
|
||||
# Если токен не найден в заголовке, проверяем параметр запроса
|
||||
elif token_param:
|
||||
token_value = token_param
|
||||
# Для обратной совместимости проверяем, если authorization передан как параметр запроса
|
||||
elif authorization and not authorization.startswith("Bearer "):
|
||||
token_value = authorization
|
||||
|
||||
if token_value and token_value in ACTIVE_SESSIONS:
|
||||
remove_session_token(token_value)
|
||||
return {"status": "success", "message": "Выход выполнен успешно"}
|
||||
|
||||
return {"status": "success", "message": "Нет активной сессии"}
|
||||
@@ -0,0 +1,110 @@
|
||||
# app/api/dashboard.py
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Dict, Any, Optional
|
||||
from app.core.security import require_auth
|
||||
from app.core.vad import get_vad_instance, get_stt_instance
|
||||
from app.core.status import status_tracker
|
||||
from app.core.system_monitor import get_system_load
|
||||
|
||||
router = APIRouter(prefix="/api/dashboard", tags=["dashboard"])
|
||||
|
||||
class SpeechData(BaseModel):
|
||||
state: str
|
||||
speech_prob: float
|
||||
speech_threshold: float # Добавлено: порог обнаружения речи
|
||||
energy: float
|
||||
energy_threshold: float
|
||||
speech_detected: bool # Добавлено: признак обнаружения речи
|
||||
continuous_frames: int # Добавлено: количество непрерывных кадров речи
|
||||
gain: float # Добавлено: текущий коэффициент усиления
|
||||
noise_floor: float # Добавлено: уровень шума
|
||||
|
||||
class RecognizedPhrase(BaseModel):
|
||||
text: str
|
||||
timestamp: str
|
||||
action: str = None
|
||||
|
||||
class SystemLoad(BaseModel): # Добавлено: модель для системной нагрузки
|
||||
cpu_percent: float
|
||||
memory_percent: float
|
||||
temperature: float
|
||||
|
||||
@router.get("/speech-data", response_model=SpeechData)
|
||||
async def get_speech_data(request: Request, token: str = Depends(require_auth)):
|
||||
"""Получение текущих данных о речи и энергии, включая расширенные метрики VAD и STT"""
|
||||
try:
|
||||
vad = get_vad_instance(request)
|
||||
|
||||
if not vad:
|
||||
return SpeechData(
|
||||
state=status_tracker.get_status(),
|
||||
speech_prob=0.0,
|
||||
speech_threshold=0.0,
|
||||
energy=0,
|
||||
energy_threshold=0,
|
||||
speech_detected=False,
|
||||
continuous_frames=0,
|
||||
gain=1.0,
|
||||
noise_floor=0.0,
|
||||
)
|
||||
|
||||
# Получаем данные из last_result VAD
|
||||
last_result = vad.last_result
|
||||
# Дополнительные атрибуты VAD (на основе класса VADPolling)
|
||||
gain = vad.current_gain
|
||||
noise_floor = vad.noise_floor
|
||||
continuous_frames = last_result.get('continuous_frames', vad.continuous_speech_frames) # Если есть в last_result или атрибуте
|
||||
|
||||
|
||||
return SpeechData(
|
||||
state=status_tracker.get_status(),
|
||||
speech_prob=float(last_result.get('speech_ratio', 0.0)),
|
||||
speech_threshold=float(last_result.get('speech_threshold', 0.0)),
|
||||
energy=float(last_result.get('energy', 0)),
|
||||
energy_threshold=float(last_result.get('energy_threshold', 0)),
|
||||
speech_detected=bool(last_result.get('speech_detected', False)),
|
||||
continuous_frames=int(continuous_frames),
|
||||
gain=float(gain),
|
||||
noise_floor=float(noise_floor),
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/sys-monitor", response_model=SystemLoad) # Добавлено: новый эндпоинт
|
||||
async def get_sys_monitor(token: str = Depends(require_auth)):
|
||||
"""Получение данных о нагрузке системы (CPU, память, температура)"""
|
||||
try:
|
||||
load_data = get_system_load()
|
||||
return SystemLoad(**load_data)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/recent-phrases", response_model=List[RecognizedPhrase])
|
||||
async def get_recent_phrases(request: Request, limit: int = 10, token: str = Depends(require_auth)):
|
||||
"""Получение последних распознанных фраз"""
|
||||
try:
|
||||
stt = get_stt_instance(request)
|
||||
if not stt or not hasattr(stt, 'get_history'):
|
||||
from app.core.history import get_recognition_history
|
||||
phrases = get_recognition_history(limit)
|
||||
else:
|
||||
phrases = stt.get_history(limit)
|
||||
return phrases
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/stats", response_model=Dict[str, Any])
|
||||
async def get_stats(request: Request, token: str = Depends(require_auth)):
|
||||
"""Получение статистики работы системы"""
|
||||
try:
|
||||
# Здесь будет логика получения статистики работы системы
|
||||
# Например, количество распознанных команд, время работы и т.д.
|
||||
return {
|
||||
"uptime": "0:00:00", # Будет заменено на реальное время работы
|
||||
"recognized_phrases_count": 0,
|
||||
"commands_executed_count": 0,
|
||||
"system_load": 0.0
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
@@ -0,0 +1,11 @@
|
||||
# app/api/router.py
|
||||
from fastapi import APIRouter
|
||||
from app.api import auth, dashboard, settings, session
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
# Подключаем все маршруты API
|
||||
api_router.include_router(auth.router)
|
||||
api_router.include_router(dashboard.router)
|
||||
api_router.include_router(settings.router)
|
||||
api_router.include_router(session.router)
|
||||
@@ -0,0 +1,35 @@
|
||||
# app/api/session.py
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from app.core.security import require_auth
|
||||
|
||||
router = APIRouter(prefix="/session", tags=["session"])
|
||||
|
||||
# Глобальная переменная для хранения экземпляра SessionManager
|
||||
_session_manager = None
|
||||
|
||||
def set_session_manager(session_manager):
|
||||
"""Устанавливает глобальный экземпляр SessionManager"""
|
||||
global _session_manager
|
||||
_session_manager = session_manager
|
||||
|
||||
def get_session_manager():
|
||||
"""Возвращает глобальный экземпляр SessionManager"""
|
||||
return _session_manager
|
||||
|
||||
class SessionStatus(BaseModel):
|
||||
"""Модель для статуса сессии"""
|
||||
active: bool
|
||||
time_left: float
|
||||
start_time: Optional[str] = None
|
||||
end_time: Optional[str] = None
|
||||
|
||||
@router.get("/status", response_model=SessionStatus)
|
||||
async def get_session_status(request: Request, token: str = Depends(require_auth)):
|
||||
"""Получить текущий статус сессии"""
|
||||
session_manager = get_session_manager()
|
||||
if not session_manager:
|
||||
return SessionStatus(active=False, time_left=0)
|
||||
|
||||
return session_manager.get_session_status()
|
||||
@@ -0,0 +1,204 @@
|
||||
# app/api/settings.py
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from pydantic import BaseModel
|
||||
from typing import Dict, Any, Optional
|
||||
from app.core.security import require_auth
|
||||
from app.core.config import get_settings, save_settings
|
||||
from app.core.vad import restart_vad_and_stt
|
||||
|
||||
router = APIRouter(prefix="/api/settings", tags=["settings"])
|
||||
|
||||
class VADSettings(BaseModel):
|
||||
mode: int = 1
|
||||
sample_rate: int = 16000
|
||||
frame_duration_ms: int = 30
|
||||
ring_buffer_size: int = 20
|
||||
speech_threshold: float = 0.5
|
||||
energy_threshold: int = 1000
|
||||
min_speech_duration_ms: int = 300
|
||||
record_stop_delay_sec: float = 1.0
|
||||
prebuffer_duration_sec: float = 1.0
|
||||
max_record_duration_sec: float = 5.0
|
||||
target_rms: float = 2000.0
|
||||
max_gain: float = 5.0
|
||||
noise_reduction_factor: float = 1.5
|
||||
noise_update_alpha: float = 0.05
|
||||
min_rms_threshold_factor: float = 2.0
|
||||
gain_smoothing_alpha: float = 0.1
|
||||
adaptive_energy_std_factor: float = 2.5
|
||||
adaptive_energy: bool = True
|
||||
noise_reduction_enabled: bool = True
|
||||
auto_gain_enabled: bool = True
|
||||
record_on_speech: bool = True
|
||||
|
||||
class SystemSettings(BaseModel):
|
||||
history_maxlen: int = 100
|
||||
polling_interval_ms: int = 500
|
||||
logging_level: str = "INFO"
|
||||
log_file: str = "logs/app.log"
|
||||
log_save_file: bool = True
|
||||
|
||||
class STTSettings(BaseModel):
|
||||
model_path: str = ""
|
||||
|
||||
class CommandsSettings(BaseModel):
|
||||
commands_file: str = ""
|
||||
|
||||
class AuthSettings(BaseModel):
|
||||
username: str = "admin"
|
||||
password: str = "admin"
|
||||
|
||||
class TTSSettings(BaseModel):
|
||||
host: str = "192.168.200.81"
|
||||
port: int = 10200
|
||||
timeout: int = 30
|
||||
pause_duration: float = 0.5
|
||||
post_stop_wait: float = 1.0
|
||||
voice: str = "ru_RU-ruslan-medium"
|
||||
save_TTS_files: bool = False
|
||||
|
||||
class AudioSettings(BaseModel):
|
||||
playback_device: int = 2
|
||||
capture_device: int = 2
|
||||
playback_volume: int = 0
|
||||
capture_volume: int = 90
|
||||
|
||||
|
||||
class AllSettings(BaseModel):
|
||||
vad: VADSettings
|
||||
system: SystemSettings
|
||||
stt: STTSettings
|
||||
commands: CommandsSettings
|
||||
auth: AuthSettings
|
||||
tts: TTSSettings
|
||||
audio_settings: AudioSettings
|
||||
|
||||
@router.get("", response_model=AllSettings)
|
||||
async def get_all_settings(token: str = Depends(require_auth)):
|
||||
"""Получение всех настроек"""
|
||||
try:
|
||||
config = get_settings()
|
||||
return AllSettings(
|
||||
vad=VADSettings(**config.vad.dict()),
|
||||
system=SystemSettings(**config.system.dict()),
|
||||
stt=STTSettings(**config.stt.dict()),
|
||||
commands=CommandsSettings(**config.commands.dict()),
|
||||
auth=AuthSettings(**config.auth.dict()),
|
||||
tts=TTSSettings(**config.tts.dict()),
|
||||
audio_settings=AudioSettings(**config.audio_settings.dict())
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.put("/vad", response_model=VADSettings)
|
||||
async def update_vad_settings(settings: VADSettings, token: str = Depends(require_auth)):
|
||||
"""Обновление настроек VAD"""
|
||||
try:
|
||||
config = get_settings()
|
||||
settings_dict = settings.dict()
|
||||
for key, value in settings_dict.items():
|
||||
setattr(config.vad, key, value)
|
||||
save_settings(config)
|
||||
return VADSettings(**config.vad.dict())
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.put("/system", response_model=SystemSettings)
|
||||
async def update_system_settings(settings: SystemSettings, token: str = Depends(require_auth)):
|
||||
"""Обновление системных настроек"""
|
||||
try:
|
||||
config = get_settings()
|
||||
settings_dict = settings.dict()
|
||||
for key, value in settings_dict.items():
|
||||
setattr(config.system, key, value)
|
||||
save_settings(config)
|
||||
return SystemSettings(**config.system.dict())
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.put("/stt", response_model=STTSettings)
|
||||
async def update_stt_settings(settings: STTSettings, token: str = Depends(require_auth)):
|
||||
"""Обновление настроек STT"""
|
||||
try:
|
||||
config = get_settings()
|
||||
settings_dict = settings.dict()
|
||||
for key, value in settings_dict.items():
|
||||
setattr(config.stt, key, value)
|
||||
save_settings(config)
|
||||
return STTSettings(**config.stt.dict())
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.put("/commands", response_model=CommandsSettings)
|
||||
async def update_commands_settings(settings: CommandsSettings, token: str = Depends(require_auth)):
|
||||
"""Обновление настроек команд"""
|
||||
try:
|
||||
config = get_settings()
|
||||
settings_dict = settings.dict()
|
||||
for key, value in settings_dict.items():
|
||||
setattr(config.commands, key, value)
|
||||
save_settings(config)
|
||||
return CommandsSettings(**config.commands.dict())
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.put("/auth", response_model=AuthSettings)
|
||||
async def update_auth_settings(settings: AuthSettings, token: str = Depends(require_auth)):
|
||||
"""Обновление настроек авторизации"""
|
||||
try:
|
||||
config = get_settings()
|
||||
settings_dict = settings.dict()
|
||||
for key, value in settings_dict.items():
|
||||
setattr(config.auth, key, value)
|
||||
save_settings(config)
|
||||
return AuthSettings(**config.auth.dict())
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.put("/tts", response_model=TTSSettings)
|
||||
async def update_tts_settings(settings: TTSSettings, token: str = Depends(require_auth)):
|
||||
"""Обновление настроек TTS"""
|
||||
try:
|
||||
config = get_settings()
|
||||
settings_dict = settings.dict()
|
||||
for key, value in settings_dict.items():
|
||||
setattr(config.tts, key, value)
|
||||
save_settings(config)
|
||||
return TTSSettings(**config.tts.dict())
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.put("/audio_settings", response_model=AudioSettings)
|
||||
async def update_audio_settings(settings: AudioSettings, token: str = Depends(require_auth)):
|
||||
"""Обновление настроек аудио"""
|
||||
try:
|
||||
from volume import set_volume
|
||||
|
||||
config = get_settings()
|
||||
old_settings = config.audio_settings.dict()
|
||||
settings_dict = settings.dict()
|
||||
|
||||
# Обновляем настройки в конфигурации
|
||||
for key, value in settings_dict.items():
|
||||
setattr(config.audio_settings, key, value)
|
||||
|
||||
# Применяем настройки громкости, если они изменились
|
||||
if old_settings['playback_volume'] != settings_dict['playback_volume']:
|
||||
set_volume(settings_dict['playback_device'], settings_dict['playback_volume'], mixer_type='Playback')
|
||||
|
||||
if old_settings['capture_volume'] != settings_dict['capture_volume']:
|
||||
set_volume(settings_dict['capture_device'], settings_dict['capture_volume'], mixer_type='Capture')
|
||||
|
||||
save_settings(config)
|
||||
return AudioSettings(**config.audio_settings.dict())
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post("/restart")
|
||||
async def restart_vad(request: Request, token: str = Depends(require_auth)):
|
||||
"""Перезапуск VAD и STT с новыми настройками"""
|
||||
try:
|
||||
restart_vad_and_stt(request)
|
||||
return {"status": "success", "message": "VAD и STT перезапущены"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
@@ -0,0 +1 @@
|
||||
# Core package initialization
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,59 @@
|
||||
# app/core/auth.py
|
||||
from fastapi import Depends, HTTPException, status, Request
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
from typing import Dict, Optional, Any
|
||||
import secrets
|
||||
import logging
|
||||
from app.core.config import get_settings
|
||||
|
||||
logger = logging.getLogger('VADApp')
|
||||
|
||||
# Базовая HTTP аутентификация
|
||||
security = HTTPBasic()
|
||||
|
||||
# Хранилище сессий (в реальном приложении лучше использовать Redis или другое хранилище)
|
||||
sessions: Dict[str, str] = {}
|
||||
|
||||
def verify_credentials(credentials: HTTPBasicCredentials) -> bool:
|
||||
"""Проверка учетных данных пользователя"""
|
||||
settings = get_settings()
|
||||
correct_username = settings.auth.username
|
||||
correct_password = settings.auth.password
|
||||
|
||||
# Безопасное сравнение строк для предотвращения timing-атак
|
||||
is_username_correct = secrets.compare_digest(credentials.username, correct_username)
|
||||
is_password_correct = secrets.compare_digest(credentials.password, correct_password)
|
||||
|
||||
return is_username_correct and is_password_correct
|
||||
|
||||
def create_session_token(username: str) -> str:
|
||||
"""Создание токена сессии"""
|
||||
token = secrets.token_hex(16)
|
||||
sessions[token] = username
|
||||
logger.info(f"Создана новая сессия для пользователя {username}")
|
||||
return token
|
||||
|
||||
def remove_session(token: str) -> None:
|
||||
"""Удаление сессии"""
|
||||
if token in sessions:
|
||||
username = sessions.pop(token)
|
||||
logger.info(f"Сессия для пользователя {username} удалена")
|
||||
|
||||
def get_current_user(request: Request) -> Optional[str]:
|
||||
"""Получение текущего пользователя из сессии"""
|
||||
token = request.cookies.get("session_token")
|
||||
if token and token in sessions:
|
||||
return sessions[token]
|
||||
return None
|
||||
|
||||
def require_auth(request: Request) -> str:
|
||||
"""Проверка аутентификации пользователя"""
|
||||
username = get_current_user(request)
|
||||
if not username:
|
||||
logger.warning("Попытка доступа без аутентификации")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Требуется аутентификация",
|
||||
headers={"WWW-Authenticate": "Basic"},
|
||||
)
|
||||
return username
|
||||
@@ -0,0 +1,181 @@
|
||||
# app/core/config.py
|
||||
import os
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
from pydantic import BaseModel, Field
|
||||
from ruamel.yaml import YAML
|
||||
from functools import lru_cache
|
||||
|
||||
# Инициализация логгера
|
||||
logger = logging.getLogger('VADApp')
|
||||
|
||||
# Путь к файлу конфигурации
|
||||
CONFIG_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'config.yaml')
|
||||
class AuthConfig(BaseModel):
|
||||
username: str = "admin"
|
||||
password: str = "admin"
|
||||
|
||||
class VADConfig(BaseModel):
|
||||
mode: int = 1
|
||||
sample_rate: int = 16000
|
||||
frame_duration_ms: int = 30
|
||||
ring_buffer_size: int = 20
|
||||
speech_threshold: float = 0.5
|
||||
energy_threshold: int = 1000
|
||||
min_speech_duration_ms: int = 300
|
||||
record_stop_delay_sec: float = 1.0
|
||||
prebuffer_duration_sec: float = 1.0
|
||||
max_record_duration_sec: float = 5.0
|
||||
target_rms: float = 2000.0
|
||||
max_gain: float = 5.0
|
||||
noise_reduction_factor: float = 1.5
|
||||
noise_update_alpha: float = 0.05
|
||||
min_rms_threshold_factor: float = 2.0
|
||||
gain_smoothing_alpha: float = 0.1
|
||||
adaptive_energy_std_factor: float = 2.5
|
||||
adaptive_energy: bool = True
|
||||
noise_reduction_enabled: bool = True
|
||||
auto_gain_enabled: bool = True
|
||||
record_on_speech: bool = True
|
||||
|
||||
class SystemConfig(BaseModel):
|
||||
history_maxlen: int = 100
|
||||
polling_interval_ms: int = 500
|
||||
logging_level: str = "INFO"
|
||||
log_file: str = "logs/app.log"
|
||||
log_save_file: bool = True
|
||||
|
||||
class STTConfig(BaseModel):
|
||||
model_path: str = ""
|
||||
|
||||
class CommandsConfig(BaseModel):
|
||||
commands_file: str = ""
|
||||
|
||||
class SessionConfig(BaseModel):
|
||||
session_timeout_seconds: int = 30
|
||||
|
||||
class TTSConfig(BaseModel):
|
||||
host: str = "192.168.200.81"
|
||||
port: int = 10200
|
||||
timeout: int = 30
|
||||
pause_duration: float = 0.5
|
||||
post_stop_wait: float = 1.0
|
||||
voice: str = "ru_RU-ruslan-medium"
|
||||
save_TTS_files: bool = False
|
||||
|
||||
class AudioSettingsConfig(BaseModel):
|
||||
playback_device: int = 0
|
||||
capture_device: int = 0
|
||||
playback_volume: int = 80
|
||||
capture_volume: int = 90
|
||||
|
||||
class Settings(BaseModel):
|
||||
auth: AuthConfig = Field(default_factory=AuthConfig)
|
||||
vad: VADConfig = Field(default_factory=VADConfig)
|
||||
system: SystemConfig = Field(default_factory=SystemConfig)
|
||||
stt: STTConfig = Field(default_factory=STTConfig)
|
||||
commands: CommandsConfig = Field(default_factory=CommandsConfig)
|
||||
session: SessionConfig = Field(default_factory=SessionConfig)
|
||||
tts: TTSConfig = Field(default_factory=TTSConfig)
|
||||
audio_settings: AudioSettingsConfig = Field(default_factory=AudioSettingsConfig)
|
||||
|
||||
def setup_logging(logging_level='INFO'):
|
||||
"""Настройка уровня логирования"""
|
||||
logger.debug(f"Установка уровня логирования: {logging_level}")
|
||||
try:
|
||||
logger.setLevel(getattr(logging, logging_level.upper(), logging.INFO))
|
||||
logger.info(f"Уровень логирования установлен: {logging_level}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка настройки логирования: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def load_yaml_config() -> Dict[str, Any]:
|
||||
"""Загрузка конфигурации из YAML файла"""
|
||||
logger.debug("Загрузка конфигурации из YAML")
|
||||
try:
|
||||
yaml = YAML()
|
||||
yaml.preserve_quotes = True
|
||||
if not os.path.exists(CONFIG_PATH):
|
||||
logger.error(f"Файл конфигурации не найден: {CONFIG_PATH}")
|
||||
raise FileNotFoundError(f"Файл конфигурации не найден: {CONFIG_PATH}")
|
||||
with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
|
||||
config = yaml.load(f) or {}
|
||||
|
||||
# Проверяем и адаптируем пути для текущей ОС
|
||||
if 'commands' in config and 'commands_file' in config['commands']:
|
||||
commands_file = config['commands']['commands_file']
|
||||
# Преобразуем путь для текущей ОС, если необходимо
|
||||
commands_file = os.path.normpath(commands_file)
|
||||
config['commands']['commands_file'] = commands_file
|
||||
logger.debug(f"Путь к файлу команд: {commands_file}")
|
||||
|
||||
if 'stt' in config and 'model_path' in config['stt']:
|
||||
model_path = config['stt']['model_path']
|
||||
# Преобразуем путь для текущей ОС, если необходимо
|
||||
model_path = os.path.normpath(model_path)
|
||||
config['stt']['model_path'] = model_path
|
||||
logger.debug(f"Путь к модели STT: {model_path}")
|
||||
|
||||
return config
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка загрузки конфигурации: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
@lru_cache()
|
||||
def get_settings() -> Settings:
|
||||
"""Получение настроек приложения (кэшируется)"""
|
||||
config = load_yaml_config()
|
||||
|
||||
# Создаем объект настроек
|
||||
settings = Settings(
|
||||
auth=AuthConfig(**config.get('auth', {})),
|
||||
vad=VADConfig(**config.get('vad', {})),
|
||||
system=SystemConfig(**config.get('system', {})),
|
||||
stt=STTConfig(**config.get('stt', {})),
|
||||
commands=CommandsConfig(**config.get('commands', {})),
|
||||
tts=TTSConfig(**config.get('tts', {})),
|
||||
audio_settings=AudioSettingsConfig(**config.get('audio_settings', {}))
|
||||
)
|
||||
|
||||
# Настраиваем логирование
|
||||
setup_logging(settings.system.logging_level)
|
||||
|
||||
return settings
|
||||
|
||||
def save_settings(settings: Settings) -> bool:
|
||||
"""Сохранение настроек в YAML файл"""
|
||||
logger.debug("Сохранение конфигурации")
|
||||
try:
|
||||
# Загружаем текущую конфигурацию
|
||||
yaml = YAML()
|
||||
yaml.indent(mapping=2, sequence=4, offset=2)
|
||||
yaml.preserve_quotes = True
|
||||
|
||||
if not os.path.exists(CONFIG_PATH):
|
||||
logger.error(f"Файл конфигурации не найден: {CONFIG_PATH}")
|
||||
raise FileNotFoundError(f"Файл конфигурации не найден: {CONFIG_PATH}")
|
||||
|
||||
with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
|
||||
config = yaml.load(f) or {}
|
||||
|
||||
# Обновляем конфигурацию
|
||||
config['auth'] = settings.auth.dict()
|
||||
config['vad'] = settings.vad.dict()
|
||||
config['system'] = settings.system.dict()
|
||||
config['stt'] = settings.stt.dict()
|
||||
config['commands'] = settings.commands.dict()
|
||||
config['tts'] = settings.tts.dict()
|
||||
config['audio_settings'] = settings.audio_settings.dict()
|
||||
|
||||
# Сохраняем обновленную конфигурацию
|
||||
with open(CONFIG_PATH, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(config, f)
|
||||
|
||||
# Сбрасываем кэш настроек
|
||||
get_settings.cache_clear()
|
||||
|
||||
logger.info("Конфигурация сохранена")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка сохранения конфигурации: {e}", exc_info=True)
|
||||
return False
|
||||
@@ -0,0 +1,41 @@
|
||||
# app/core/history.py
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any
|
||||
from pydantic import BaseModel
|
||||
from app.core.config import get_settings
|
||||
import collections
|
||||
|
||||
class RecognizedPhrase(BaseModel):
|
||||
text: str
|
||||
timestamp: str
|
||||
action: str = None
|
||||
|
||||
# Хранилище распознанных фраз (в реальном приложении лучше использовать базу данных)
|
||||
recognition_history: collections.deque = collections.deque(maxlen=100)
|
||||
|
||||
def add_recognized_phrase(text: str, action: str = None) -> None:
|
||||
"""Добавление распознанной фразы в историю"""
|
||||
global recognition_history
|
||||
|
||||
# Обновляем максимальный размер истории из настроек
|
||||
settings = get_settings()
|
||||
recognition_history = collections.deque(list(recognition_history),
|
||||
maxlen=settings.system.history_maxlen)
|
||||
|
||||
# Добавляем только фразы с действиями (команды)
|
||||
if action is not None:
|
||||
phrase = RecognizedPhrase(
|
||||
text=text,
|
||||
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
action=action
|
||||
)
|
||||
recognition_history.append(phrase)
|
||||
|
||||
def get_recognition_history(limit: int = 10) -> List[RecognizedPhrase]:
|
||||
"""Получение истории распознанных фраз"""
|
||||
# Возвращаем последние N фраз (самые новые в начале списка)
|
||||
return list(reversed(list(recognition_history)))[:limit]
|
||||
|
||||
def clear_recognition_history() -> None:
|
||||
"""Очистка истории распознанных фраз"""
|
||||
recognition_history.clear()
|
||||
@@ -0,0 +1,50 @@
|
||||
import uuid
|
||||
from typing import Dict, Optional
|
||||
from fastapi import Depends, HTTPException, status, Request
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
|
||||
# Простое хранилище активных сессий (в реальном приложении лучше использовать Redis или другое хранилище)
|
||||
ACTIVE_SESSIONS: Dict[str, str] = {}
|
||||
|
||||
# Схема безопасности для проверки токена
|
||||
security = HTTPBearer(auto_error=False)
|
||||
|
||||
def create_session_token(username: str) -> str:
|
||||
"""Создание простого сессионного токена"""
|
||||
token = str(uuid.uuid4())
|
||||
ACTIVE_SESSIONS[token] = username
|
||||
return token
|
||||
|
||||
def remove_session_token(token: str) -> None:
|
||||
"""Удаление сессионного токена"""
|
||||
if token in ACTIVE_SESSIONS:
|
||||
del ACTIVE_SESSIONS[token]
|
||||
|
||||
async def validate_session_token(
|
||||
request: Request,
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)
|
||||
) -> Optional[str]:
|
||||
"""Проверка сессионного токена"""
|
||||
token_value = None
|
||||
|
||||
# Проверяем заголовок Authorization
|
||||
if credentials:
|
||||
token_value = credentials.credentials
|
||||
# Или query-параметр token (для обратной совместимости)
|
||||
elif 'token' in request.query_params:
|
||||
token_value = request.query_params['token']
|
||||
|
||||
if not token_value or token_value not in ACTIVE_SESSIONS:
|
||||
return None
|
||||
|
||||
return ACTIVE_SESSIONS[token_value]
|
||||
|
||||
async def require_auth(token: Optional[str] = Depends(validate_session_token)) -> str:
|
||||
"""Требование обязательной аутентификации"""
|
||||
if not token:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Не авторизован",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
return token
|
||||
@@ -0,0 +1,31 @@
|
||||
# app/core/status.py
|
||||
from enum import Enum
|
||||
import logging
|
||||
from threading import Lock
|
||||
|
||||
logger = logging.getLogger('VADApp')
|
||||
|
||||
class SystemStatus(Enum):
|
||||
INITIALIZATION = "Инициализация"
|
||||
LISTENING = "Прослушивание"
|
||||
RECORDING = "Запись"
|
||||
RECOGNITION = "Распознавание"
|
||||
COMMAND_PROCESSING = "Обработка команды"
|
||||
RESPONSE = "Ответ"
|
||||
|
||||
class StatusTracker:
|
||||
def __init__(self):
|
||||
self.current_status = SystemStatus.INITIALIZATION
|
||||
self.lock = Lock()
|
||||
|
||||
def set_status(self, status: SystemStatus):
|
||||
with self.lock:
|
||||
self.current_status = status
|
||||
logger.info(f"Статус: {status.value}")
|
||||
|
||||
def get_status(self) -> str:
|
||||
with self.lock:
|
||||
return self.current_status.value
|
||||
|
||||
# Global instance (or can be stored in app.state)
|
||||
status_tracker = StatusTracker()
|
||||
@@ -0,0 +1,40 @@
|
||||
# app/core/system_monitor.py
|
||||
import logging
|
||||
import psutil
|
||||
from typing import Dict
|
||||
|
||||
logger = logging.getLogger('VADApp')
|
||||
|
||||
def get_system_load() -> Dict[str, float]:
|
||||
"""
|
||||
Получение текущей нагрузки системы для Raspberry Pi 3B+.
|
||||
|
||||
Возвращает словарь с:
|
||||
- 'cpu_percent': Процент загрузки CPU
|
||||
- 'memory_percent': Процент использования памяти
|
||||
- 'temperature': Температура CPU в °C
|
||||
"""
|
||||
try:
|
||||
# Загрузка CPU (с интервалом для более точного измерения)
|
||||
cpu_percent = psutil.cpu_percent(interval=0.5)
|
||||
|
||||
# Использование памяти
|
||||
memory = psutil.virtual_memory()
|
||||
memory_percent = memory.percent
|
||||
|
||||
# Температура CPU (специфично для Raspberry Pi)
|
||||
with open('/sys/class/thermal/thermal_zone0/temp', 'r') as temp_file:
|
||||
temperature = float(temp_file.read().strip()) / 1000.0
|
||||
|
||||
return {
|
||||
'cpu_percent': cpu_percent,
|
||||
'memory_percent': memory_percent,
|
||||
'temperature': temperature
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения нагрузки системы: {e}")
|
||||
return {
|
||||
'cpu_percent': 0.0,
|
||||
'memory_percent': 0.0,
|
||||
'temperature': 0.0
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
# app/core/vad.py
|
||||
from fastapi import Request
|
||||
import os
|
||||
from app.core.config import get_settings
|
||||
from typing import Tuple, Dict, Any, Optional
|
||||
import logging
|
||||
from app.core.status import status_tracker, SystemStatus # Добавлено
|
||||
from app.core.history import add_recognized_phrase
|
||||
logger = logging.getLogger('VADApp')
|
||||
|
||||
def get_vad_instance(request: Request):
|
||||
"""Получение экземпляра VAD из состояния приложения"""
|
||||
return getattr(request.app.state, 'vad_instance', None)
|
||||
|
||||
def get_stt_instance(request: Request):
|
||||
"""Получение экземпляра STT из состояния приложения"""
|
||||
return getattr(request.app.state, 'stt_instance', None)
|
||||
|
||||
def get_commands(request: Request):
|
||||
"""Получение команд из состояния приложения"""
|
||||
return getattr(request.app.state, 'commands', None)
|
||||
|
||||
def init_vad_and_stt(request: Request = None) -> Tuple[Any, Any, Dict[str, Any], Any]:
|
||||
"""Инициализация VAD и STT"""
|
||||
logger.debug("Инициализация VAD и STT")
|
||||
status_tracker.set_status(SystemStatus.INITIALIZATION) # Добавлено
|
||||
try:
|
||||
# Импортируем здесь, чтобы избежать циклических импортов
|
||||
from vad_core import VADPolling
|
||||
from vosk_local_STT import VoskSTT # Исправлено с vosk_stt на vosk_local_STT
|
||||
from commands import handle_command
|
||||
from session_manager import SessionManager
|
||||
from volume import set_volume
|
||||
|
||||
config = get_settings()
|
||||
commands = {}
|
||||
|
||||
# Инициализация громкости аудиоустройств
|
||||
try:
|
||||
# Устанавливаем громкость для устройства воспроизведения
|
||||
set_volume(config.audio_settings.playback_device, config.audio_settings.playback_volume, mixer_type='Playback')
|
||||
# Устанавливаем громкость для устройства записи
|
||||
set_volume(config.audio_settings.capture_device, config.audio_settings.capture_volume, mixer_type='Capture')
|
||||
logger.info(f"Громкость устройств инициализирована: воспроизведение={config.audio_settings.playback_volume}%, запись={config.audio_settings.capture_volume}%")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при инициализации громкости: {e}")
|
||||
|
||||
# Загружаем команды из файла
|
||||
if config.commands.commands_file and os.path.exists(config.commands.commands_file):
|
||||
from ruamel.yaml import YAML
|
||||
yaml = YAML()
|
||||
with open(config.commands.commands_file, 'r', encoding='utf-8') as f:
|
||||
commands = yaml.load(f) or {}
|
||||
|
||||
# Инициализируем SessionManager
|
||||
session_manager = SessionManager(config.session.session_timeout_seconds)
|
||||
logger.info(f"SessionManager инициализирован с таймаутом {config.session.session_timeout_seconds} секунд")
|
||||
|
||||
# Инициализируем STT
|
||||
stt_instance = VoskSTT(
|
||||
model_path=config.stt.model_path,
|
||||
commands_file=config.commands.commands_file,
|
||||
sample_rate=config.vad.sample_rate
|
||||
)
|
||||
|
||||
# Функция обработки распознанной речи
|
||||
def process_speech(audio_data, sample_rate):
|
||||
try:
|
||||
status_tracker.set_status(SystemStatus.RECOGNITION) # Добавлено
|
||||
recognized = stt_instance.recognize(audio_data)
|
||||
if recognized:
|
||||
logger.info(f"Команда распознана: {recognized}")
|
||||
status_tracker.set_status(SystemStatus.COMMAND_PROCESSING) # Добавлено
|
||||
action = handle_command(recognized, commands, session_manager)
|
||||
status_tracker.set_status(SystemStatus.RESPONSE) # Добавлено (предполагая, что здесь происходит ответ; если нет, удалите)
|
||||
|
||||
# Добавляем распознанную фразу в историю
|
||||
|
||||
if action:
|
||||
add_recognized_phrase(recognized, action=action)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки речи: {e}")
|
||||
status_tracker.set_status(SystemStatus.LISTENING) # Добавлено (возврат к прослушиванию)
|
||||
|
||||
# Инициализируем VAD
|
||||
vad_config = config.vad.dict()
|
||||
vad_config['polling_interval_ms'] = config.system.polling_interval_ms
|
||||
vad_config['history_maxlen'] = config.system.history_maxlen
|
||||
vad_instance = VADPolling(vad_config, on_speech_end_callback=process_speech)
|
||||
|
||||
logger.info("VAD и STT инициализированы")
|
||||
status_tracker.set_status(SystemStatus.LISTENING) # Добавлено
|
||||
return vad_instance, stt_instance, commands, session_manager
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка инициализации VAD/STT: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def cleanup_vad_and_stt(vad_instance, stt_instance):
|
||||
"""Завершение работы VAD и STT"""
|
||||
logger.debug("Завершение работы VAD и STT")
|
||||
try:
|
||||
if vad_instance:
|
||||
logger.info("Завершаем VAD...")
|
||||
vad_instance.close()
|
||||
if stt_instance:
|
||||
logger.info("Завершаем STT...")
|
||||
stt_instance = None
|
||||
logger.info("VAD и STT завершены")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка завершения VAD/STT: {e}")
|
||||
|
||||
def restart_vad_and_stt(request: Request):
|
||||
"""Перезапуск VAD и STT с новыми настройками"""
|
||||
logger.info("Перезапуск VAD и STT")
|
||||
status_tracker.set_status(SystemStatus.INITIALIZATION) # Добавлено
|
||||
try:
|
||||
# Завершаем текущие экземпляры
|
||||
vad_instance = get_vad_instance(request)
|
||||
stt_instance = get_stt_instance(request)
|
||||
cleanup_vad_and_stt(vad_instance, stt_instance)
|
||||
|
||||
# Инициализируем новые экземпляры
|
||||
vad_instance, stt_instance, commands = init_vad_and_stt()
|
||||
|
||||
# Обновляем состояние приложения
|
||||
request.app.state.vad_instance = vad_instance
|
||||
request.app.state.stt_instance = stt_instance
|
||||
request.app.state.commands = commands
|
||||
|
||||
logger.info("VAD и STT перезапущены")
|
||||
status_tracker.set_status(SystemStatus.LISTENING) # Добавлено
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка перезапуска VAD/STT: {e}", exc_info=True)
|
||||
raise
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
# app/utils.py
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
from ruamel.yaml import YAML
|
||||
from vad_core import VADPolling
|
||||
from vosk_local_STT import VoskSTT
|
||||
from commands import handle_command
|
||||
from volume import set_volume
|
||||
|
||||
# Инициализация логгера
|
||||
logger = logging.getLogger('VADApp')
|
||||
logger.setLevel(logging.DEBUG)
|
||||
logger.handlers.clear()
|
||||
log_format = logging.Formatter('[%(asctime)s] %(levelname)s] %(message)s', datefmt='%H:%M:%S')
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setFormatter(log_format)
|
||||
logger.addHandler(console_handler)
|
||||
logger.debug("Логгер VADApp инициализирован")
|
||||
|
||||
CONFIG_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config.yaml')
|
||||
|
||||
def setup_logging(logging_level='INFO'):
|
||||
logger.debug(f"Установка уровня логирования: {logging_level}")
|
||||
try:
|
||||
logger.setLevel(getattr(logging, logging_level.upper(), logging.INFO))
|
||||
logger.info(f"Уровень логирования установлен: {logging_level}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка настройки логирования: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def load_config():
|
||||
logger.debug("Загрузка конфигурации")
|
||||
try:
|
||||
yaml = YAML()
|
||||
yaml.preserve_quotes = True
|
||||
if not os.path.exists(CONFIG_PATH):
|
||||
logger.error(f"Файл конфигурации не найден: {CONFIG_PATH}")
|
||||
raise FileNotFoundError(f"Файл конфигурации не найден: {CONFIG_PATH}")
|
||||
with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
|
||||
config = yaml.load(f) or {}
|
||||
|
||||
commands_file = config.get('commands', {}).get('commands_file', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'commands', 'commands.yaml'))
|
||||
if commands_file:
|
||||
if not os.path.exists(commands_file):
|
||||
logger.error(f"Файл команд не найден: {commands_file}")
|
||||
raise FileNotFoundError(f"Файл команд не найден: {commands_file}")
|
||||
with open(commands_file, 'r', encoding='utf-8') as cmd_file:
|
||||
config['commands'] = yaml.load(cmd_file) or {}
|
||||
config['commands_file'] = commands_file
|
||||
logger.debug(f"Команды загружены: {commands_file}")
|
||||
|
||||
config.setdefault('system', {})
|
||||
config['system'].setdefault('history_maxlen', 100)
|
||||
config['system'].setdefault('polling_interval_ms', 500)
|
||||
config['system'].setdefault('logging_level', 'INFO')
|
||||
|
||||
config.setdefault('vad', {})
|
||||
config['vad'].setdefault('mode', 1)
|
||||
config['vad'].setdefault('sample_rate', 16000)
|
||||
config['vad'].setdefault('frame_duration_ms', 30)
|
||||
config['vad'].setdefault('ring_buffer_size', 20)
|
||||
config['vad'].setdefault('speech_threshold', 0.5)
|
||||
config['vad'].setdefault('energy_threshold', 1000)
|
||||
config['vad'].setdefault('min_speech_duration_ms', 300)
|
||||
config['vad'].setdefault('record_stop_delay_sec', 1.0)
|
||||
config['vad'].setdefault('prebuffer_duration_sec', 1.0)
|
||||
config['vad'].setdefault('max_record_duration_sec', 5.0)
|
||||
config['vad'].setdefault('target_rms', 2000.0)
|
||||
config['vad'].setdefault('max_gain', 5.0)
|
||||
config['vad'].setdefault('noise_reduction_factor', 1.5)
|
||||
config['vad'].setdefault('noise_update_alpha', 0.05)
|
||||
config['vad'].setdefault('min_rms_threshold_factor', 2.0)
|
||||
config['vad'].setdefault('gain_smoothing_alpha', 0.1)
|
||||
config['vad'].setdefault('adaptive_energy_std_factor', 2.5)
|
||||
config['vad'].setdefault('adaptive_energy', True)
|
||||
config['vad'].setdefault('noise_reduction_enabled', True)
|
||||
config['vad'].setdefault('auto_gain_enabled', True)
|
||||
config['vad'].setdefault('record_on_speech', True)
|
||||
|
||||
config.setdefault('auth', {})
|
||||
config['auth'].setdefault('username', 'admin')
|
||||
config['auth'].setdefault('password', 'admin')
|
||||
|
||||
setup_logging(config['system']['logging_level'])
|
||||
logger.info("Конфигурация загружена")
|
||||
return config
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка загрузки конфигурации: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def save_config(config):
|
||||
logger.debug("Сохранение конфигурации")
|
||||
try:
|
||||
yaml = YAML()
|
||||
yaml.indent(mapping=2, sequence=4, offset=2)
|
||||
yaml.preserve_quotes = True
|
||||
config_to_save = config.copy()
|
||||
if 'commands_file' in config_to_save:
|
||||
del config_to_save['commands_file']
|
||||
with open(CONFIG_PATH, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(config_to_save, f)
|
||||
logger.info("Конфигурация сохранена")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка сохранения конфигурации: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
def init_vad_and_stt():
|
||||
logger.debug("Инициализация VAD и STT")
|
||||
try:
|
||||
config = load_config()
|
||||
commands = config.get('commands', {})
|
||||
|
||||
stt_instance = VoskSTT(
|
||||
model_path=config.get('stt', {}).get('model_path', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'models', 'vosk-model-small-ru-0.22')),
|
||||
commands_file=config.get('commands_file', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'commands', 'commands.yaml')),
|
||||
sample_rate=config.get('vad', {}).get('sample_rate', 16000)
|
||||
)
|
||||
|
||||
def process_speech(audio_data, sample_rate):
|
||||
try:
|
||||
recognized = stt_instance.recognize(audio_data)
|
||||
if recognized:
|
||||
logger.info(f"Команда распознана: {recognized}")
|
||||
handle_command(recognized, commands)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки речи: {e}")
|
||||
|
||||
vad_config = config.get('vad', {})
|
||||
vad_config['polling_interval_ms'] = config['system']['polling_interval_ms']
|
||||
vad_config['history_maxlen'] = config['system']['history_maxlen']
|
||||
vad_instance = VADPolling(vad_config, on_speech_end_callback=process_speech)
|
||||
|
||||
logger.info("VAD и STT инициализированы")
|
||||
return vad_instance, stt_instance, commands
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка инициализации VAD/STT: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def cleanup_vad_and_stt(vad_instance, stt_instance):
|
||||
logger.debug("Завершение работы VAD и STT")
|
||||
try:
|
||||
if vad_instance:
|
||||
logger.info("Завершаем VAD...")
|
||||
vad_instance.close()
|
||||
if stt_instance:
|
||||
logger.info("Завершаем STT...")
|
||||
stt_instance = None
|
||||
logger.info("VAD и STT завершены")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка завершения VAD/STT: {e}")
|
||||
Reference in New Issue
Block a user