Initial commit

This commit is contained in:
2025-12-25 12:01:35 +03:00
commit c574533f63
85 changed files with 6027 additions and 0 deletions
+96
View File
@@ -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.
+1
View File
@@ -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.
+92
View File
@@ -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": "Нет активной сессии"}
+110
View File
@@ -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))
+11
View File
@@ -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)
+35
View File
@@ -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()
+204
View File
@@ -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))
+1
View File
@@ -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.
+59
View File
@@ -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
+181
View File
@@ -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
+41
View File
@@ -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()
+50
View File
@@ -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
+31
View File
@@ -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()
+40
View File
@@ -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
View File
@@ -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
View File
@@ -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}")