Initial commit: полный проект my_speaker
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user