Улучшенная работа погоды + ускорение работы + фикс неработоспособности после пары часов
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@@ -4,10 +4,11 @@ Regulates system volume on a scale from 1 to 10.
|
||||
"""
|
||||
|
||||
# Модуль управления громкостью системы.
|
||||
# Работает через системную утилиту amixer (ALSA) в Linux.
|
||||
# Работает через различные системные утилиты в зависимости от ОС.
|
||||
|
||||
import subprocess
|
||||
import re
|
||||
import platform
|
||||
|
||||
# Карта для перевода слов в цифры ("пять" -> 5)
|
||||
NUMBER_MAP = {
|
||||
@@ -25,6 +26,71 @@ NUMBER_MAP = {
|
||||
}
|
||||
|
||||
|
||||
def _get_volume_command(level: int):
|
||||
"""
|
||||
Возвращает команду для изменения громкости в зависимости от ОС.
|
||||
|
||||
Args:
|
||||
level: Уровень громкости (1-10)
|
||||
|
||||
Returns:
|
||||
Список команд для выполнения или None, если команда не поддерживается
|
||||
"""
|
||||
percentage = level * 10
|
||||
|
||||
system = platform.system().lower()
|
||||
|
||||
if system == "linux":
|
||||
# Проверяем доступность различных утилит
|
||||
if _command_exists("pactl"):
|
||||
# Используем PulseAudio (более современный подход)
|
||||
return ["pactl", "set-sink-volume", "@DEFAULT_SINK@", f"{percentage}%"]
|
||||
elif _command_exists("amixer"):
|
||||
# Используем ALSA
|
||||
return ["amixer", "-q", "sset", "Master", f"{percentage}%"]
|
||||
else:
|
||||
# Проверяем alsamixer
|
||||
if _command_exists("alsamixer"):
|
||||
return ["amixer", "-q", "sset", "Master", f"{percentage}%"]
|
||||
elif system == "darwin": # macOS
|
||||
return ["osascript", "-e", f"set volume output volume {percentage}"]
|
||||
elif system == "windows":
|
||||
# Для Windows используем PowerShell команду
|
||||
# Это требует дополнительных библиотек, поэтому пока просто покажем сообщение
|
||||
print("⚠️ Настройка громкости на Windows требует дополнительных библиотек")
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _command_exists(command):
|
||||
"""
|
||||
Проверяет, существует ли команда в системе.
|
||||
|
||||
Args:
|
||||
command: Название команды
|
||||
|
||||
Returns:
|
||||
True, если команда существует
|
||||
"""
|
||||
try:
|
||||
subprocess.run(["which", command],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=False)
|
||||
return True
|
||||
except:
|
||||
try:
|
||||
# Альтернативная проверка для Windows
|
||||
subprocess.run(["where", command],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=False)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def set_volume(level: int) -> bool:
|
||||
"""
|
||||
Устанавливает системную громкость (шкала 1-10).
|
||||
@@ -51,16 +117,22 @@ def set_volume(level: int) -> bool:
|
||||
|
||||
percentage = level * 10
|
||||
|
||||
# Получаем команду для текущей ОС
|
||||
cmd = _get_volume_command(level)
|
||||
|
||||
if cmd is None:
|
||||
print(f"❌ Не найдена подходящая утилита для изменения громкости на вашей системе")
|
||||
print(f"💡 Установите PulseAudio (pactl) или ALSA (amixer) для управления громкостью")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Вызов команды amixer для изменения громкости Master канала
|
||||
# -q: quiet (без вывода)
|
||||
# sset: simple set
|
||||
cmd = ["amixer", "-q", "sset", "Master", f"{percentage}%"]
|
||||
subprocess.run(cmd, check=True)
|
||||
# Выполняем команду
|
||||
result = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
||||
print(f"🔊 Громкость установлена на {level} ({percentage}%)")
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ Ошибка при установке громкости: {e}")
|
||||
print(f"💡 Убедитесь, что у вас установлены и настроены аудио утилиты (pactl, amixer)")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ Неизвестная ошибка громкости: {e}")
|
||||
|
||||
137
app/audio/stt.py
137
app/audio/stt.py
@@ -11,6 +11,7 @@ import asyncio
|
||||
import time
|
||||
import pyaudio
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from ..core.config import DEEPGRAM_API_KEY, SAMPLE_RATE
|
||||
from deepgram import (
|
||||
DeepgramClient,
|
||||
@@ -52,6 +53,7 @@ class SpeechRecognizer:
|
||||
self.pa = None
|
||||
self.stream = None
|
||||
self.transcript = ""
|
||||
self.last_successful_operation = datetime.now()
|
||||
|
||||
def initialize(self):
|
||||
"""Инициализация клиента Deepgram и PyAudio."""
|
||||
@@ -62,10 +64,34 @@ class SpeechRecognizer:
|
||||
config = DeepgramClientOptions(
|
||||
verbose=logging.WARNING,
|
||||
)
|
||||
self.dg_client = DeepgramClient(DEEPGRAM_API_KEY, config)
|
||||
try:
|
||||
self.dg_client = DeepgramClient(DEEPGRAM_API_KEY, config)
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка при создании клиента Deepgram: {e}")
|
||||
raise
|
||||
|
||||
self.pa = get_audio_manager().get_pyaudio()
|
||||
print("✅ Deepgram клиент готов")
|
||||
# Обновляем время последней успешной операции
|
||||
self.last_successful_operation = datetime.now()
|
||||
|
||||
def check_connection_health(self):
|
||||
"""Проверяет здоровье соединения и при необходимости пересоздает клиента."""
|
||||
# Проверяем, прошло ли больше 15 минут с последней успешной операции
|
||||
if datetime.now() - self.last_successful_operation > timedelta(minutes=15):
|
||||
print("🔄 Обновление соединения Deepgram для предотвращения таймаута...")
|
||||
try:
|
||||
# Очищаем старый клиент
|
||||
if self.stream:
|
||||
self.stream.stop_stream()
|
||||
self.stream.close()
|
||||
self.stream = None
|
||||
# Создаем новый клиент
|
||||
self.dg_client = None
|
||||
self.initialize()
|
||||
print("✅ Соединение Deepgram обновлено")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Ошибка при обновлении соединения: {e}")
|
||||
|
||||
def _get_stream(self):
|
||||
"""Открывает аудиопоток PyAudio, если он еще не открыт."""
|
||||
@@ -111,15 +137,27 @@ class SpeechRecognizer:
|
||||
|
||||
def on_speech_started(unused_self, speech_started, **kwargs):
|
||||
"""Вызывается, когда VAD (Voice Activity Detection) слышит голос."""
|
||||
loop.call_soon_threadsafe(speech_started_event.set)
|
||||
try:
|
||||
loop.call_soon_threadsafe(speech_started_event.set)
|
||||
except RuntimeError:
|
||||
# Event loop might be closed, ignore
|
||||
pass
|
||||
|
||||
def on_utterance_end(unused_self, utterance_end, **kwargs):
|
||||
"""Вызывается, когда Deepgram решает, что фраза закончилась (пауза)."""
|
||||
loop.call_soon_threadsafe(stop_event.set)
|
||||
try:
|
||||
loop.call_soon_threadsafe(stop_event.set)
|
||||
except RuntimeError:
|
||||
# Event loop might be closed, ignore
|
||||
pass
|
||||
|
||||
def on_error(unused_self, error, **kwargs):
|
||||
print(f"Error: {error}")
|
||||
loop.call_soon_threadsafe(stop_event.set)
|
||||
print(f"Deepgram Error: {error}")
|
||||
try:
|
||||
loop.call_soon_threadsafe(stop_event.set)
|
||||
except RuntimeError:
|
||||
# Event loop might be closed, ignore
|
||||
pass
|
||||
|
||||
# Подписываемся на события
|
||||
dg_connection.on(LiveTranscriptionEvents.Transcript, on_transcript)
|
||||
@@ -138,6 +176,8 @@ class SpeechRecognizer:
|
||||
interim_results=True,
|
||||
utterance_end_ms=1000, # Пауза 1.0с считается концом фразы (было 1.2)
|
||||
vad_events=True,
|
||||
# Добавляем параметры таймаута для долгой работы
|
||||
endpointing=300, # Таймаут в миллисекундах для автоматического завершения
|
||||
)
|
||||
|
||||
# --- Задача отправки аудио с буферизацией ---
|
||||
@@ -159,11 +199,19 @@ class SpeechRecognizer:
|
||||
)
|
||||
|
||||
# Пока подключаемся, копим данные
|
||||
while not connect_future.done():
|
||||
timeout_count = 0
|
||||
max_timeout = 5000 # Максимальное количество итераций ожидания (около 2.5 секунд при 0.0005 задержке)
|
||||
|
||||
while not connect_future.done() and timeout_count < max_timeout:
|
||||
if stream.is_active():
|
||||
data = stream.read(4096, exception_on_overflow=False)
|
||||
audio_buffer.append(data)
|
||||
await asyncio.sleep(0.001)
|
||||
await asyncio.sleep(0.0005) # Уменьшаем задержку для более быстрой обработки
|
||||
timeout_count += 1
|
||||
|
||||
if timeout_count >= max_timeout:
|
||||
print("⏰ Timeout connecting to Deepgram")
|
||||
return
|
||||
|
||||
# Проверяем результат подключения
|
||||
if connect_future.result() is False:
|
||||
@@ -180,14 +228,18 @@ class SpeechRecognizer:
|
||||
audio_buffer = None # Освобождаем память
|
||||
|
||||
# 4. Продолжаем стримить в реальном времени
|
||||
while not stop_event.is_set():
|
||||
stream_timeout = 0
|
||||
max_stream_timeout = int(timeout_seconds / 0.002) # Примерный таймаут в зависимости от timeout_seconds
|
||||
|
||||
while not stop_event.is_set() and stream_timeout < max_stream_timeout:
|
||||
if stream.is_active():
|
||||
data = stream.read(4096, exception_on_overflow=False)
|
||||
dg_connection.send(data)
|
||||
chunks_sent += 1
|
||||
if chunks_sent % 50 == 0:
|
||||
print(".", end="", flush=True)
|
||||
await asyncio.sleep(0.005)
|
||||
await asyncio.sleep(0.002) # Уменьшаем задержку для более быстрого реагирования
|
||||
stream_timeout += 1
|
||||
|
||||
except Exception as e:
|
||||
print(f"Audio send error: {e}")
|
||||
@@ -209,7 +261,7 @@ class SpeechRecognizer:
|
||||
speech_started_event.wait(), timeout=detection_timeout
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
# Если за detection_timeout (5 сек) никто не начал говорить, выходим
|
||||
# Если за detection_timeout никто не начал говорить, выходим
|
||||
stop_event.set()
|
||||
|
||||
# 2. Если речь началась (или таймаута нет), ждем завершения (stop_event)
|
||||
@@ -219,11 +271,20 @@ class SpeechRecognizer:
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
pass # Общий таймаут вышел
|
||||
except Exception as e:
|
||||
print(f"Error in waiting for events: {e}")
|
||||
|
||||
stop_event.set()
|
||||
await sender_task
|
||||
try:
|
||||
await sender_task
|
||||
except Exception as e:
|
||||
print(f"Error waiting for sender task: {e}")
|
||||
|
||||
# Завершаем соединение и ждем последние результаты
|
||||
dg_connection.finish()
|
||||
try:
|
||||
dg_connection.finish()
|
||||
except Exception as e:
|
||||
print(f"Error finishing connection: {e}")
|
||||
|
||||
return self.transcript
|
||||
|
||||
@@ -244,17 +305,21 @@ class SpeechRecognizer:
|
||||
if not self.dg_client:
|
||||
self.initialize()
|
||||
|
||||
# Проверяем здоровье соединения перед началом прослушивания
|
||||
self.check_connection_health()
|
||||
|
||||
self.current_lang = lang
|
||||
print(f"🎙️ Слушаю ({lang})...")
|
||||
|
||||
last_error = None
|
||||
|
||||
# Делаем 2 попытки на случай сбоя сети
|
||||
for attempt in range(2):
|
||||
# Создаем новое live подключение для каждой сессии
|
||||
dg_connection = self.dg_client.listen.live.v("1")
|
||||
|
||||
# Делаем 3 попытки на случай сбоя сети
|
||||
for attempt in range(3):
|
||||
dg_connection = None
|
||||
try:
|
||||
# Создаем новое live подключение для каждой сессии
|
||||
dg_connection = self.dg_client.listen.live.v("1")
|
||||
|
||||
# Запускаем асинхронный процесс обработки
|
||||
transcript = asyncio.run(
|
||||
self._process_audio(
|
||||
@@ -264,20 +329,32 @@ class SpeechRecognizer:
|
||||
final_text = transcript.strip() if transcript else ""
|
||||
if final_text:
|
||||
print(f"📝 Распознано: {final_text}")
|
||||
# Обновляем время последней успешной операции
|
||||
self.last_successful_operation = datetime.now()
|
||||
return final_text
|
||||
else:
|
||||
# Если вернулась пустая строка (тишина), считаем это штатным завершением.
|
||||
# Не нужно повторять попытку, как при ошибке сети.
|
||||
# Все равно обновляем время последней успешной операции
|
||||
self.last_successful_operation = datetime.now()
|
||||
return ""
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
print(f"Attempt {attempt + 1} failed: {e}")
|
||||
|
||||
if attempt == 0:
|
||||
print("⚠️ Не удалось подключиться к Deepgram, повторяю...")
|
||||
time.sleep(1)
|
||||
# Закрываем соединение, если оно было создано
|
||||
if dg_connection:
|
||||
try:
|
||||
dg_connection.finish()
|
||||
except:
|
||||
pass # Игнорируем ошибки при завершении
|
||||
|
||||
if attempt < 2: # Не ждем после последней попытки
|
||||
print(f"⚠️ Не удалось подключиться к Deepgram, попытка {attempt + 1}/3, повторяю...")
|
||||
time.sleep(1) # Уменьшаем задержку между попытками
|
||||
|
||||
if last_error:
|
||||
print(f"❌ Ошибка STT: {last_error}")
|
||||
print(f"❌ Ошибка STT после всех попыток: {last_error}")
|
||||
else:
|
||||
print("⚠️ Речь не распознана")
|
||||
return ""
|
||||
@@ -285,10 +362,19 @@ class SpeechRecognizer:
|
||||
def cleanup(self):
|
||||
"""Очистка ресурсов."""
|
||||
if self.stream:
|
||||
self.stream.stop_stream()
|
||||
self.stream.close()
|
||||
try:
|
||||
if self.stream.is_active():
|
||||
self.stream.stop_stream()
|
||||
except Exception as e:
|
||||
print(f"Ошибка при остановке потока: {e}")
|
||||
try:
|
||||
self.stream.close()
|
||||
except Exception as e:
|
||||
print(f"Ошибка при закрытии потока: {e}")
|
||||
self.stream = None
|
||||
# self.pa.terminate() - Используем общий менеджер
|
||||
# Сбросим клиента для принудительного переподключения
|
||||
self.dg_client = None
|
||||
|
||||
|
||||
# Глобальный экземпляр
|
||||
@@ -313,5 +399,8 @@ def cleanup():
|
||||
"""Внешняя функция очистки."""
|
||||
global _recognizer
|
||||
if _recognizer:
|
||||
_recognizer.cleanup()
|
||||
try:
|
||||
_recognizer.cleanup()
|
||||
except Exception as e:
|
||||
print(f"Ошибка при очистке STT: {e}")
|
||||
_recognizer = None
|
||||
|
||||
@@ -326,7 +326,7 @@ class TextToSpeech:
|
||||
while sd.get_stream().active:
|
||||
if self._interrupted:
|
||||
break
|
||||
time.sleep(0.05)
|
||||
time.sleep(0.02) # Уменьшаем задержку для более быстрого реагирования
|
||||
|
||||
finally:
|
||||
# Сообщаем потоку-наблюдателю, что пора завершаться
|
||||
|
||||
@@ -135,7 +135,7 @@ class WakeWordDetector:
|
||||
keyword_index = self.porcupine.process(pcm)
|
||||
if keyword_index >= 0:
|
||||
now = time.time()
|
||||
if now - self._last_hit_ts < 0.4:
|
||||
if now - self._last_hit_ts < 0.2: # Уменьшаем интервал для более быстрой реакции
|
||||
return False
|
||||
self._last_hit_ts = now
|
||||
print("🛑 Wake word обнаружен во время ответа!")
|
||||
|
||||
Reference in New Issue
Block a user