Улучшенная работа погоды + ускорение работы + фикс неработоспособности после пары часов
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 subprocess
|
||||||
import re
|
import re
|
||||||
|
import platform
|
||||||
|
|
||||||
# Карта для перевода слов в цифры ("пять" -> 5)
|
# Карта для перевода слов в цифры ("пять" -> 5)
|
||||||
NUMBER_MAP = {
|
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:
|
def set_volume(level: int) -> bool:
|
||||||
"""
|
"""
|
||||||
Устанавливает системную громкость (шкала 1-10).
|
Устанавливает системную громкость (шкала 1-10).
|
||||||
@@ -51,16 +117,22 @@ def set_volume(level: int) -> bool:
|
|||||||
|
|
||||||
percentage = level * 10
|
percentage = level * 10
|
||||||
|
|
||||||
|
# Получаем команду для текущей ОС
|
||||||
|
cmd = _get_volume_command(level)
|
||||||
|
|
||||||
|
if cmd is None:
|
||||||
|
print(f"❌ Не найдена подходящая утилита для изменения громкости на вашей системе")
|
||||||
|
print(f"💡 Установите PulseAudio (pactl) или ALSA (amixer) для управления громкостью")
|
||||||
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Вызов команды amixer для изменения громкости Master канала
|
# Выполняем команду
|
||||||
# -q: quiet (без вывода)
|
result = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
||||||
# sset: simple set
|
|
||||||
cmd = ["amixer", "-q", "sset", "Master", f"{percentage}%"]
|
|
||||||
subprocess.run(cmd, check=True)
|
|
||||||
print(f"🔊 Громкость установлена на {level} ({percentage}%)")
|
print(f"🔊 Громкость установлена на {level} ({percentage}%)")
|
||||||
return True
|
return True
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
print(f"❌ Ошибка при установке громкости: {e}")
|
print(f"❌ Ошибка при установке громкости: {e}")
|
||||||
|
print(f"💡 Убедитесь, что у вас установлены и настроены аудио утилиты (pactl, amixer)")
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Неизвестная ошибка громкости: {e}")
|
print(f"❌ Неизвестная ошибка громкости: {e}")
|
||||||
|
|||||||
115
app/audio/stt.py
115
app/audio/stt.py
@@ -11,6 +11,7 @@ import asyncio
|
|||||||
import time
|
import time
|
||||||
import pyaudio
|
import pyaudio
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from ..core.config import DEEPGRAM_API_KEY, SAMPLE_RATE
|
from ..core.config import DEEPGRAM_API_KEY, SAMPLE_RATE
|
||||||
from deepgram import (
|
from deepgram import (
|
||||||
DeepgramClient,
|
DeepgramClient,
|
||||||
@@ -52,6 +53,7 @@ class SpeechRecognizer:
|
|||||||
self.pa = None
|
self.pa = None
|
||||||
self.stream = None
|
self.stream = None
|
||||||
self.transcript = ""
|
self.transcript = ""
|
||||||
|
self.last_successful_operation = datetime.now()
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
"""Инициализация клиента Deepgram и PyAudio."""
|
"""Инициализация клиента Deepgram и PyAudio."""
|
||||||
@@ -62,10 +64,34 @@ class SpeechRecognizer:
|
|||||||
config = DeepgramClientOptions(
|
config = DeepgramClientOptions(
|
||||||
verbose=logging.WARNING,
|
verbose=logging.WARNING,
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
self.dg_client = DeepgramClient(DEEPGRAM_API_KEY, config)
|
self.dg_client = DeepgramClient(DEEPGRAM_API_KEY, config)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка при создании клиента Deepgram: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
self.pa = get_audio_manager().get_pyaudio()
|
self.pa = get_audio_manager().get_pyaudio()
|
||||||
print("✅ Deepgram клиент готов")
|
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):
|
def _get_stream(self):
|
||||||
"""Открывает аудиопоток PyAudio, если он еще не открыт."""
|
"""Открывает аудиопоток PyAudio, если он еще не открыт."""
|
||||||
@@ -111,15 +137,27 @@ class SpeechRecognizer:
|
|||||||
|
|
||||||
def on_speech_started(unused_self, speech_started, **kwargs):
|
def on_speech_started(unused_self, speech_started, **kwargs):
|
||||||
"""Вызывается, когда VAD (Voice Activity Detection) слышит голос."""
|
"""Вызывается, когда VAD (Voice Activity Detection) слышит голос."""
|
||||||
|
try:
|
||||||
loop.call_soon_threadsafe(speech_started_event.set)
|
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):
|
def on_utterance_end(unused_self, utterance_end, **kwargs):
|
||||||
"""Вызывается, когда Deepgram решает, что фраза закончилась (пауза)."""
|
"""Вызывается, когда Deepgram решает, что фраза закончилась (пауза)."""
|
||||||
|
try:
|
||||||
loop.call_soon_threadsafe(stop_event.set)
|
loop.call_soon_threadsafe(stop_event.set)
|
||||||
|
except RuntimeError:
|
||||||
|
# Event loop might be closed, ignore
|
||||||
|
pass
|
||||||
|
|
||||||
def on_error(unused_self, error, **kwargs):
|
def on_error(unused_self, error, **kwargs):
|
||||||
print(f"Error: {error}")
|
print(f"Deepgram Error: {error}")
|
||||||
|
try:
|
||||||
loop.call_soon_threadsafe(stop_event.set)
|
loop.call_soon_threadsafe(stop_event.set)
|
||||||
|
except RuntimeError:
|
||||||
|
# Event loop might be closed, ignore
|
||||||
|
pass
|
||||||
|
|
||||||
# Подписываемся на события
|
# Подписываемся на события
|
||||||
dg_connection.on(LiveTranscriptionEvents.Transcript, on_transcript)
|
dg_connection.on(LiveTranscriptionEvents.Transcript, on_transcript)
|
||||||
@@ -138,6 +176,8 @@ class SpeechRecognizer:
|
|||||||
interim_results=True,
|
interim_results=True,
|
||||||
utterance_end_ms=1000, # Пауза 1.0с считается концом фразы (было 1.2)
|
utterance_end_ms=1000, # Пауза 1.0с считается концом фразы (было 1.2)
|
||||||
vad_events=True,
|
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():
|
if stream.is_active():
|
||||||
data = stream.read(4096, exception_on_overflow=False)
|
data = stream.read(4096, exception_on_overflow=False)
|
||||||
audio_buffer.append(data)
|
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:
|
if connect_future.result() is False:
|
||||||
@@ -180,14 +228,18 @@ class SpeechRecognizer:
|
|||||||
audio_buffer = None # Освобождаем память
|
audio_buffer = None # Освобождаем память
|
||||||
|
|
||||||
# 4. Продолжаем стримить в реальном времени
|
# 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():
|
if stream.is_active():
|
||||||
data = stream.read(4096, exception_on_overflow=False)
|
data = stream.read(4096, exception_on_overflow=False)
|
||||||
dg_connection.send(data)
|
dg_connection.send(data)
|
||||||
chunks_sent += 1
|
chunks_sent += 1
|
||||||
if chunks_sent % 50 == 0:
|
if chunks_sent % 50 == 0:
|
||||||
print(".", end="", flush=True)
|
print(".", end="", flush=True)
|
||||||
await asyncio.sleep(0.005)
|
await asyncio.sleep(0.002) # Уменьшаем задержку для более быстрого реагирования
|
||||||
|
stream_timeout += 1
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Audio send error: {e}")
|
print(f"Audio send error: {e}")
|
||||||
@@ -209,7 +261,7 @@ class SpeechRecognizer:
|
|||||||
speech_started_event.wait(), timeout=detection_timeout
|
speech_started_event.wait(), timeout=detection_timeout
|
||||||
)
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
# Если за detection_timeout (5 сек) никто не начал говорить, выходим
|
# Если за detection_timeout никто не начал говорить, выходим
|
||||||
stop_event.set()
|
stop_event.set()
|
||||||
|
|
||||||
# 2. Если речь началась (или таймаута нет), ждем завершения (stop_event)
|
# 2. Если речь началась (или таймаута нет), ждем завершения (stop_event)
|
||||||
@@ -219,11 +271,20 @@ class SpeechRecognizer:
|
|||||||
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
pass # Общий таймаут вышел
|
pass # Общий таймаут вышел
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in waiting for events: {e}")
|
||||||
|
|
||||||
stop_event.set()
|
stop_event.set()
|
||||||
|
try:
|
||||||
await sender_task
|
await sender_task
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error waiting for sender task: {e}")
|
||||||
|
|
||||||
# Завершаем соединение и ждем последние результаты
|
# Завершаем соединение и ждем последние результаты
|
||||||
|
try:
|
||||||
dg_connection.finish()
|
dg_connection.finish()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error finishing connection: {e}")
|
||||||
|
|
||||||
return self.transcript
|
return self.transcript
|
||||||
|
|
||||||
@@ -244,17 +305,21 @@ class SpeechRecognizer:
|
|||||||
if not self.dg_client:
|
if not self.dg_client:
|
||||||
self.initialize()
|
self.initialize()
|
||||||
|
|
||||||
|
# Проверяем здоровье соединения перед началом прослушивания
|
||||||
|
self.check_connection_health()
|
||||||
|
|
||||||
self.current_lang = lang
|
self.current_lang = lang
|
||||||
print(f"🎙️ Слушаю ({lang})...")
|
print(f"🎙️ Слушаю ({lang})...")
|
||||||
|
|
||||||
last_error = None
|
last_error = None
|
||||||
|
|
||||||
# Делаем 2 попытки на случай сбоя сети
|
# Делаем 3 попытки на случай сбоя сети
|
||||||
for attempt in range(2):
|
for attempt in range(3):
|
||||||
|
dg_connection = None
|
||||||
|
try:
|
||||||
# Создаем новое live подключение для каждой сессии
|
# Создаем новое live подключение для каждой сессии
|
||||||
dg_connection = self.dg_client.listen.live.v("1")
|
dg_connection = self.dg_client.listen.live.v("1")
|
||||||
|
|
||||||
try:
|
|
||||||
# Запускаем асинхронный процесс обработки
|
# Запускаем асинхронный процесс обработки
|
||||||
transcript = asyncio.run(
|
transcript = asyncio.run(
|
||||||
self._process_audio(
|
self._process_audio(
|
||||||
@@ -264,20 +329,32 @@ class SpeechRecognizer:
|
|||||||
final_text = transcript.strip() if transcript else ""
|
final_text = transcript.strip() if transcript else ""
|
||||||
if final_text:
|
if final_text:
|
||||||
print(f"📝 Распознано: {final_text}")
|
print(f"📝 Распознано: {final_text}")
|
||||||
|
# Обновляем время последней успешной операции
|
||||||
|
self.last_successful_operation = datetime.now()
|
||||||
return final_text
|
return final_text
|
||||||
else:
|
else:
|
||||||
# Если вернулась пустая строка (тишина), считаем это штатным завершением.
|
# Если вернулась пустая строка (тишина), считаем это штатным завершением.
|
||||||
# Не нужно повторять попытку, как при ошибке сети.
|
# Не нужно повторять попытку, как при ошибке сети.
|
||||||
|
# Все равно обновляем время последней успешной операции
|
||||||
|
self.last_successful_operation = datetime.now()
|
||||||
return ""
|
return ""
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
last_error = e
|
last_error = e
|
||||||
|
print(f"Attempt {attempt + 1} failed: {e}")
|
||||||
|
|
||||||
if attempt == 0:
|
# Закрываем соединение, если оно было создано
|
||||||
print("⚠️ Не удалось подключиться к Deepgram, повторяю...")
|
if dg_connection:
|
||||||
time.sleep(1)
|
try:
|
||||||
|
dg_connection.finish()
|
||||||
|
except:
|
||||||
|
pass # Игнорируем ошибки при завершении
|
||||||
|
|
||||||
|
if attempt < 2: # Не ждем после последней попытки
|
||||||
|
print(f"⚠️ Не удалось подключиться к Deepgram, попытка {attempt + 1}/3, повторяю...")
|
||||||
|
time.sleep(1) # Уменьшаем задержку между попытками
|
||||||
|
|
||||||
if last_error:
|
if last_error:
|
||||||
print(f"❌ Ошибка STT: {last_error}")
|
print(f"❌ Ошибка STT после всех попыток: {last_error}")
|
||||||
else:
|
else:
|
||||||
print("⚠️ Речь не распознана")
|
print("⚠️ Речь не распознана")
|
||||||
return ""
|
return ""
|
||||||
@@ -285,10 +362,19 @@ class SpeechRecognizer:
|
|||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
"""Очистка ресурсов."""
|
"""Очистка ресурсов."""
|
||||||
if self.stream:
|
if self.stream:
|
||||||
|
try:
|
||||||
|
if self.stream.is_active():
|
||||||
self.stream.stop_stream()
|
self.stream.stop_stream()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при остановке потока: {e}")
|
||||||
|
try:
|
||||||
self.stream.close()
|
self.stream.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при закрытии потока: {e}")
|
||||||
self.stream = None
|
self.stream = None
|
||||||
# self.pa.terminate() - Используем общий менеджер
|
# self.pa.terminate() - Используем общий менеджер
|
||||||
|
# Сбросим клиента для принудительного переподключения
|
||||||
|
self.dg_client = None
|
||||||
|
|
||||||
|
|
||||||
# Глобальный экземпляр
|
# Глобальный экземпляр
|
||||||
@@ -313,5 +399,8 @@ def cleanup():
|
|||||||
"""Внешняя функция очистки."""
|
"""Внешняя функция очистки."""
|
||||||
global _recognizer
|
global _recognizer
|
||||||
if _recognizer:
|
if _recognizer:
|
||||||
|
try:
|
||||||
_recognizer.cleanup()
|
_recognizer.cleanup()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при очистке STT: {e}")
|
||||||
_recognizer = None
|
_recognizer = None
|
||||||
|
|||||||
@@ -326,7 +326,7 @@ class TextToSpeech:
|
|||||||
while sd.get_stream().active:
|
while sd.get_stream().active:
|
||||||
if self._interrupted:
|
if self._interrupted:
|
||||||
break
|
break
|
||||||
time.sleep(0.05)
|
time.sleep(0.02) # Уменьшаем задержку для более быстрого реагирования
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# Сообщаем потоку-наблюдателю, что пора завершаться
|
# Сообщаем потоку-наблюдателю, что пора завершаться
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class WakeWordDetector:
|
|||||||
keyword_index = self.porcupine.process(pcm)
|
keyword_index = self.porcupine.process(pcm)
|
||||||
if keyword_index >= 0:
|
if keyword_index >= 0:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if now - self._last_hit_ts < 0.4:
|
if now - self._last_hit_ts < 0.2: # Уменьшаем интервал для более быстрой реакции
|
||||||
return False
|
return False
|
||||||
self._last_hit_ts = now
|
self._last_hit_ts = now
|
||||||
print("🛑 Wake word обнаружен во время ответа!")
|
print("🛑 Wake word обнаружен во время ответа!")
|
||||||
|
|||||||
@@ -47,11 +47,12 @@ def _send_request(messages, max_tokens, temperature, error_text):
|
|||||||
"messages": messages,
|
"messages": messages,
|
||||||
"max_tokens": max_tokens,
|
"max_tokens": max_tokens,
|
||||||
"temperature": temperature,
|
"temperature": temperature,
|
||||||
|
"stream": False # Убираем стриминг для более быстрого ответа
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
PERPLEXITY_API_URL, headers=headers, json=payload, timeout=30
|
PERPLEXITY_API_URL, headers=headers, json=payload, timeout=15 # Уменьшаем таймаут
|
||||||
)
|
)
|
||||||
response.raise_for_status() # Проверка на ошибки HTTP (4xx, 5xx)
|
response.raise_for_status() # Проверка на ошибки HTTP (4xx, 5xx)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
@@ -129,7 +130,7 @@ def ask_ai_stream(messages_history: list):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
PERPLEXITY_API_URL, headers=headers, json=payload, timeout=30, stream=True
|
PERPLEXITY_API_URL, headers=headers, json=payload, timeout=15, stream=True # Уменьшаем таймаут
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
|
|||||||
@@ -41,21 +41,360 @@ def get_wmo_description(code: int) -> str:
|
|||||||
}
|
}
|
||||||
return codes.get(code, "осадки")
|
return codes.get(code, "осадки")
|
||||||
|
|
||||||
def get_weather_report() -> str:
|
def get_temperature_text(temp: int) -> str:
|
||||||
|
"""
|
||||||
|
Returns the correct Russian form for temperature degrees based on the number.
|
||||||
|
Handles proper Russian grammar cases (падежи) for temperature values.
|
||||||
|
"""
|
||||||
|
# Get the absolute value to handle negative temperatures
|
||||||
|
abs_temp = abs(temp)
|
||||||
|
|
||||||
|
# Get the last digit
|
||||||
|
last_digit = abs_temp % 10
|
||||||
|
# Get the last two digits to handle special cases like 11-14
|
||||||
|
last_two_digits = abs_temp % 100
|
||||||
|
|
||||||
|
# Special cases for numbers ending in 11-14 (e.g., 11, 12, 13, 14, 111, 112, etc.)
|
||||||
|
if 11 <= last_two_digits <= 14:
|
||||||
|
return f"{temp} градусов"
|
||||||
|
|
||||||
|
# Cases based on the last digit
|
||||||
|
if last_digit == 1:
|
||||||
|
return f"{temp} градус"
|
||||||
|
elif 2 <= last_digit <= 4:
|
||||||
|
return f"{temp} градуса"
|
||||||
|
else: # 5-9, 0
|
||||||
|
return f"{temp} градусов"
|
||||||
|
|
||||||
|
def normalize_city_name(city_name: str) -> str:
|
||||||
|
"""
|
||||||
|
Converts city names from various grammatical cases to the base form for geocoding.
|
||||||
|
Handles common Russian grammatical cases (падежи) for city names.
|
||||||
|
"""
|
||||||
|
# Convert to lowercase for comparison
|
||||||
|
lower_city = city_name.lower()
|
||||||
|
|
||||||
|
# Remove common Russian location descriptors that might be included by mistake
|
||||||
|
# For example, if someone says "в городе Волгоград", the city_name might be "городе волгоград"
|
||||||
|
# So we want to extract just "волгоград"
|
||||||
|
if 'городе' in lower_city:
|
||||||
|
# Extract the part after "городе"
|
||||||
|
parts = lower_city.split('городе')
|
||||||
|
if len(parts) > 1:
|
||||||
|
lower_city = parts[1].strip()
|
||||||
|
elif 'город' in lower_city:
|
||||||
|
# Extract the part after "город"
|
||||||
|
parts = lower_city.split('город')
|
||||||
|
if len(parts) > 1:
|
||||||
|
lower_city = parts[1].strip()
|
||||||
|
|
||||||
|
# Common endings for different cases in Russian
|
||||||
|
# Prepositional case endings (-е, -и, -у, etc.)
|
||||||
|
prepositional_endings = ['е', 'и', 'у', 'о', 'й']
|
||||||
|
genitive_endings = ['а', 'я', 'ов', 'ев', 'ин', 'ын']
|
||||||
|
instrumental_endings = ['ом', 'ем', 'ой', 'ей']
|
||||||
|
|
||||||
|
# If the city ends with a prepositional ending, try removing it to get the base form
|
||||||
|
if lower_city.endswith(tuple(prepositional_endings)):
|
||||||
|
# Try to remove the ending and see if we get a valid base form
|
||||||
|
base_form = lower_city
|
||||||
|
# Try removing 1-2 characters to get the base form
|
||||||
|
for i in range(2, 0, -1): # Try removing 2 chars, then 1 char
|
||||||
|
if len(base_form) > i:
|
||||||
|
potential_base = base_form[:-i]
|
||||||
|
# Check if the removed part is a common ending
|
||||||
|
if base_form[-i:] in ['ке', 'ме', 'не', 'ве', 'ге', 'де', 'те']:
|
||||||
|
base_form = potential_base
|
||||||
|
break
|
||||||
|
elif base_form[-1] in prepositional_endings:
|
||||||
|
base_form = base_form[:-1]
|
||||||
|
break
|
||||||
|
|
||||||
|
# Special handling for common patterns
|
||||||
|
if base_form.endswith('йорке'): # "нью-йорке" -> "нью-йорк"
|
||||||
|
base_form = base_form[:-1] + 'к'
|
||||||
|
elif base_form.endswith('ске'): # "москве" -> "москва", "париже" -> "париж"
|
||||||
|
# This is more complex, but for "москве" -> "москва", "париже" -> "париж"
|
||||||
|
# We'll handle the most common cases
|
||||||
|
if base_form == 'москве':
|
||||||
|
base_form = 'москва'
|
||||||
|
elif base_form == 'париже':
|
||||||
|
base_form = 'париж'
|
||||||
|
elif base_form == 'лондоне':
|
||||||
|
base_form = 'лондон'
|
||||||
|
elif base_form == 'берлине':
|
||||||
|
base_form = 'берлин'
|
||||||
|
elif base_form == 'токио': # токио stays токио
|
||||||
|
base_form = 'токио'
|
||||||
|
else:
|
||||||
|
# General rule: replace -е with -а or -ь
|
||||||
|
if base_form.endswith('ске'):
|
||||||
|
base_form = base_form[:-1] + 'а'
|
||||||
|
elif base_form.endswith('ие'):
|
||||||
|
base_form = base_form[:-2] + 'ия'
|
||||||
|
|
||||||
|
# Capitalize appropriately
|
||||||
|
if base_form != lower_city:
|
||||||
|
return base_form.capitalize()
|
||||||
|
|
||||||
|
# Dictionary mapping specific known variations
|
||||||
|
case_variations = {
|
||||||
|
"нью-йорке": "Нью-Йорк",
|
||||||
|
"нью-йорка": "Нью-Йорк",
|
||||||
|
"нью-йорком": "Нью-Йорк",
|
||||||
|
"москве": "Москва",
|
||||||
|
"москвы": "Москва",
|
||||||
|
"москвой": "Москва",
|
||||||
|
"москву": "Москва",
|
||||||
|
"лондоне": "Лондон",
|
||||||
|
"лондона": "Лондон",
|
||||||
|
"лондоном": "Лондон",
|
||||||
|
"париже": "Париж",
|
||||||
|
"парижа": "Париж",
|
||||||
|
"парижем": "Париж",
|
||||||
|
"берлине": "Берлин",
|
||||||
|
"берлина": "Берлин",
|
||||||
|
"берлином": "Берлин",
|
||||||
|
"пекине": "Пекин",
|
||||||
|
"пекина": "Пекин",
|
||||||
|
"пекином": "Пекин",
|
||||||
|
"роме": "Рим",
|
||||||
|
"рима": "Рим",
|
||||||
|
"римом": "Рим",
|
||||||
|
"мадриде": "Мадрид",
|
||||||
|
"мадрида": "Мадрид",
|
||||||
|
"мадридом": "Мадрид",
|
||||||
|
"сиднее": "Сидней",
|
||||||
|
"сиднея": "Сидней",
|
||||||
|
"сиднеем": "Сидней",
|
||||||
|
"вашингтоне": "Вашингтон",
|
||||||
|
"вашингтона": "Вашингтон",
|
||||||
|
"вашингтоном": "Вашингтон",
|
||||||
|
"лос-анджелесе": "Лос-Анджелес",
|
||||||
|
"лос-анджелеса": "Лос-Анджелес",
|
||||||
|
"лос-анджелесом": "Лос-Анджелес",
|
||||||
|
"сиэтле": "Сиэтл",
|
||||||
|
"сиэтла": "Сиэтл",
|
||||||
|
"сиэтлом": "Сиэтл",
|
||||||
|
"бостоне": "Бостон",
|
||||||
|
"бостона": "Бостон",
|
||||||
|
"бостоном": "Бостон",
|
||||||
|
"денвере": "Денвер",
|
||||||
|
"денвера": "Денвер",
|
||||||
|
"денвером": "Денвер",
|
||||||
|
"хьюстоне": "Хьюстон",
|
||||||
|
"хьюстона": "Хьюстон",
|
||||||
|
"хьюстоном": "Хьюстон",
|
||||||
|
"фениксе": "Феникс",
|
||||||
|
"феникса": "Феникс",
|
||||||
|
"фениксом": "Феникс",
|
||||||
|
"атланте": "Атланта",
|
||||||
|
"атланты": "Атланта",
|
||||||
|
"атлантой": "Атланта",
|
||||||
|
"портленде": "Портленд",
|
||||||
|
"портленда": "Портленд",
|
||||||
|
"портлендом": "Портленд",
|
||||||
|
"остине": "Остин",
|
||||||
|
"остина": "Остин",
|
||||||
|
"остином": "Остин",
|
||||||
|
"нэшвилле": "Нэшвилл",
|
||||||
|
"нэшвилла": "Нэшвилл",
|
||||||
|
"нэшвиллом": "Нэшвилл",
|
||||||
|
"сан-франциско": "Сан-Франциско",
|
||||||
|
"токио": "Токио",
|
||||||
|
"торонто": "Торонто",
|
||||||
|
"чикаго": "Чикаго",
|
||||||
|
"майами": "Майами",
|
||||||
|
}
|
||||||
|
|
||||||
|
return case_variations.get(lower_city, city_name)
|
||||||
|
|
||||||
|
def get_coordinates_by_city(city_name: str) -> tuple:
|
||||||
|
"""
|
||||||
|
Gets coordinates (lat, lon) for a given city name using Open-Meteo geocoding API.
|
||||||
|
Returns (lat, lon, city_display_name) or (None, None, None) if not found.
|
||||||
|
"""
|
||||||
|
# First try with the original name
|
||||||
|
try_names = [city_name]
|
||||||
|
|
||||||
|
# Add normalized version
|
||||||
|
normalized_city = normalize_city_name(city_name)
|
||||||
|
if normalized_city != city_name:
|
||||||
|
try_names.append(normalized_city)
|
||||||
|
|
||||||
|
# Also try with English version if it's a known translation
|
||||||
|
city_to_eng = {
|
||||||
|
"москва": "Moscow",
|
||||||
|
"санкт-петербург": "Saint Petersburg",
|
||||||
|
"новосибирск": "Novosibirsk",
|
||||||
|
"екатеринбург": "Yekaterinburg",
|
||||||
|
"казань": "Kazan",
|
||||||
|
"нижний новгород": "Nizhny Novgorod",
|
||||||
|
"челябинск": "Chelyabinsk",
|
||||||
|
"омск": "Omsk",
|
||||||
|
"самара": "Samara",
|
||||||
|
"ростов-на-дону": "Rostov-on-Don",
|
||||||
|
"уфа": "Ufa",
|
||||||
|
"красноярск": "Krasnoyarsk",
|
||||||
|
"владивосток": "Vladivostok",
|
||||||
|
"сочи": "Sochi",
|
||||||
|
"новокузнецк": "Novokuznetsk",
|
||||||
|
"ярославль": "Yaroslavl",
|
||||||
|
"владикавказ": "Vladikavkaz",
|
||||||
|
"магнитогорск": "Magnitogorsk",
|
||||||
|
"иркутск": "Irkutsk",
|
||||||
|
"хабаровск": "Khabarovsk",
|
||||||
|
"оренбург": "Orenburg",
|
||||||
|
"калининград": "Kaliningrad",
|
||||||
|
"пермь": "Perm",
|
||||||
|
"волгоград": "Volgograd",
|
||||||
|
"волгограде": "Volgograd",
|
||||||
|
"краснодар": "Krasnodar",
|
||||||
|
"саратов": "Saratov",
|
||||||
|
"тында": "Tynda",
|
||||||
|
"тольятти": "Tolyatti",
|
||||||
|
"барнаул": "Barnaul",
|
||||||
|
"улан-удэ": "Ulan-Ude",
|
||||||
|
"иваново": "Ivanovo",
|
||||||
|
"мурманск": "Murmansk",
|
||||||
|
"кузнецк": "Kuznetsk",
|
||||||
|
"архангельск": "Arkhangelsk",
|
||||||
|
"владимир": "Vladimir",
|
||||||
|
"калининград": "Kaliningrad",
|
||||||
|
"смоленск": "Smolensk",
|
||||||
|
"калука": "Kaluga",
|
||||||
|
"воронеж": "Voronezh",
|
||||||
|
"курск": "Kursk",
|
||||||
|
"астрахань": "Astrakhan",
|
||||||
|
"липецк": "Lipetsk",
|
||||||
|
"тамбов": "Tambov",
|
||||||
|
"курган": "Kurgan",
|
||||||
|
"пенза": "Penza",
|
||||||
|
"рязн": "Ryazan",
|
||||||
|
"орёл": "Oryol",
|
||||||
|
"якутск": "Yakutsk",
|
||||||
|
"владикавказ": "Vladikavkaz",
|
||||||
|
"магас": "Magas",
|
||||||
|
"нарьян-мар": "Naryan-Mar",
|
||||||
|
"ханты-мансийск": "Khanty-Mansiysk",
|
||||||
|
"анадырь": "Anadyr",
|
||||||
|
"салехард": "Salekhard",
|
||||||
|
"лондон": "London",
|
||||||
|
"нью-йорк": "New York",
|
||||||
|
"токио": "Tokyo",
|
||||||
|
"париж": "Paris",
|
||||||
|
"берлин": "Berlin",
|
||||||
|
"мадрид": "Madrid",
|
||||||
|
"рим": "Rome",
|
||||||
|
"милан": "Milan",
|
||||||
|
"венеция": "Venice",
|
||||||
|
"амстердам": "Amsterdam",
|
||||||
|
"прага": "Prague",
|
||||||
|
"будапешт": "Budapest",
|
||||||
|
"вена": "Vienna",
|
||||||
|
"варшава": "Warsaw",
|
||||||
|
"киев": "Kyiv",
|
||||||
|
"минск": "Minsk",
|
||||||
|
"ташкент": "Tashkent",
|
||||||
|
"алматы": "Almaty",
|
||||||
|
"астана": "Astana",
|
||||||
|
"баку": "Baku",
|
||||||
|
"ереван": "Yerevan",
|
||||||
|
"тбилиси": "Tbilisi",
|
||||||
|
"софия": "Sofia",
|
||||||
|
"белград": "Belgrade",
|
||||||
|
"любляна": "Ljubljana",
|
||||||
|
"загреб": "Zagreb",
|
||||||
|
"рекьявик": "Reykjavik",
|
||||||
|
"осло": "Oslo",
|
||||||
|
"стокгольм": "Stockholm",
|
||||||
|
"копенгаген": "Copenhagen",
|
||||||
|
"хельсинки": "Helsinki",
|
||||||
|
"дублин": "Dublin",
|
||||||
|
"эдинбург": "Edinburgh",
|
||||||
|
"манчестер": "Manchester",
|
||||||
|
"бirmingham": "Birmingham",
|
||||||
|
"ливерпуль": "Liverpool",
|
||||||
|
"глазго": "Glasgow",
|
||||||
|
"брюссель": "Brussels",
|
||||||
|
"цюрих": "Zurich",
|
||||||
|
"женева": "Geneva",
|
||||||
|
"осака": "Osaka",
|
||||||
|
"киото": "Kyoto",
|
||||||
|
"сингапур": "Singapore",
|
||||||
|
"бангкок": "Bangkok",
|
||||||
|
"пекин": "Beijing",
|
||||||
|
"шанхай": "Shanghai",
|
||||||
|
"гонконг": "Hong Kong",
|
||||||
|
"сеул": "Seoul",
|
||||||
|
"дели": "Delhi",
|
||||||
|
"мумбаи": "Mumbai",
|
||||||
|
"бомбей": "Mumbai",
|
||||||
|
}
|
||||||
|
|
||||||
|
eng_name = city_to_eng.get(city_name.lower())
|
||||||
|
if eng_name:
|
||||||
|
try_names.append(eng_name)
|
||||||
|
|
||||||
|
# Try each name in sequence
|
||||||
|
for name_to_try in try_names:
|
||||||
|
try:
|
||||||
|
# Use Open-Meteo's geocoding API
|
||||||
|
geocode_url = "https://geocoding-api.open-meteo.com/v1/search"
|
||||||
|
params = {
|
||||||
|
"name": name_to_try,
|
||||||
|
"count": 1,
|
||||||
|
"language": "ru",
|
||||||
|
"format": "json"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.get(geocode_url, params=params, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
if "results" in data and len(data["results"]) > 0:
|
||||||
|
result = data["results"][0]
|
||||||
|
lat = result["latitude"]
|
||||||
|
lon = result["longitude"]
|
||||||
|
display_name = result.get("name", name_to_try) # Use the name from the API if available
|
||||||
|
return lat, lon, display_name
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка при поиске координат для города {name_to_try}: {e}")
|
||||||
|
continue # Try the next name
|
||||||
|
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
|
||||||
|
def get_weather_report(requested_city: str = None) -> str:
|
||||||
"""
|
"""
|
||||||
Fetches detailed weather report.
|
Fetches detailed weather report.
|
||||||
Structure:
|
Structure:
|
||||||
1. Current temp and precipitation.
|
1. Current temp and precipitation.
|
||||||
2. Today's min/max temp.
|
2. Today's min/max temp.
|
||||||
3. Next 4 hours forecast (temp + precipitation).
|
3. Next 4 hours forecast (temp + precipitation).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
requested_city: Optional city name to get weather for. If None, uses default city.
|
||||||
"""
|
"""
|
||||||
|
# Determine which city to use
|
||||||
|
if requested_city:
|
||||||
|
# Try to get coordinates for the requested city
|
||||||
|
lat, lon, city_display_name = get_coordinates_by_city(requested_city)
|
||||||
|
if lat is None or lon is None:
|
||||||
|
return f"Не удалось найти город {requested_city}. Проверьте название и попробуйте снова."
|
||||||
|
else:
|
||||||
|
# Use default city from config
|
||||||
if not all([WEATHER_LAT, WEATHER_LON, WEATHER_CITY]):
|
if not all([WEATHER_LAT, WEATHER_LON, WEATHER_CITY]):
|
||||||
return "Настройки погоды не найдены. Проверьте конфигурацию."
|
return "Настройки погоды не найдены. Проверьте конфигурацию."
|
||||||
|
lat = float(WEATHER_LAT)
|
||||||
|
lon = float(WEATHER_LON)
|
||||||
|
city_display_name = WEATHER_CITY
|
||||||
|
|
||||||
url = "https://api.open-meteo.com/v1/forecast"
|
url = "https://api.open-meteo.com/v1/forecast"
|
||||||
params = {
|
params = {
|
||||||
"latitude": WEATHER_LAT,
|
"latitude": lat,
|
||||||
"longitude": WEATHER_LON,
|
"longitude": lon,
|
||||||
"current": "temperature_2m,precipitation,weather_code",
|
"current": "temperature_2m,precipitation,weather_code",
|
||||||
"hourly": "temperature_2m,precipitation,weather_code",
|
"hourly": "temperature_2m,precipitation,weather_code",
|
||||||
"daily": "temperature_2m_max,temperature_2m_min",
|
"daily": "temperature_2m_max,temperature_2m_min",
|
||||||
@@ -64,7 +403,7 @@ def get_weather_report() -> str:
|
|||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, params=params, timeout=5)
|
response = requests.get(url, params=params, timeout=10)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
@@ -75,7 +414,7 @@ def get_weather_report() -> str:
|
|||||||
code_now = curr["weather_code"]
|
code_now = curr["weather_code"]
|
||||||
desc_now = get_wmo_description(code_now)
|
desc_now = get_wmo_description(code_now)
|
||||||
|
|
||||||
report = f"Сейчас в городе {WEATHER_CITY} {temp_now} градусов, {desc_now}."
|
report = f"Сейчас в городе {city_display_name} {get_temperature_text(temp_now)}, {desc_now}."
|
||||||
if precip_now > 0:
|
if precip_now > 0:
|
||||||
report += f" Выпало {precip_now} миллиметров осадков."
|
report += f" Выпало {precip_now} миллиметров осадков."
|
||||||
|
|
||||||
@@ -85,7 +424,7 @@ def get_weather_report() -> str:
|
|||||||
t_max = round(daily["temperature_2m_max"][0])
|
t_max = round(daily["temperature_2m_max"][0])
|
||||||
t_min = round(daily["temperature_2m_min"][0])
|
t_min = round(daily["temperature_2m_min"][0])
|
||||||
|
|
||||||
report += f" Сегодня температура будет от {t_min} до {t_max} градусов."
|
report += f" Сегодня температура будет от {get_temperature_text(t_min)} до {get_temperature_text(t_max)}."
|
||||||
|
|
||||||
# --- 3. Forecast Next 4 Hours ---
|
# --- 3. Forecast Next 4 Hours ---
|
||||||
current_hour = datetime.now().hour
|
current_hour = datetime.now().hour
|
||||||
@@ -104,14 +443,6 @@ def get_weather_report() -> str:
|
|||||||
if next_temps:
|
if next_temps:
|
||||||
report += " Прогноз на ближайшие 4 часа: "
|
report += " Прогноз на ближайшие 4 часа: "
|
||||||
|
|
||||||
# Group by roughly similar weather to avoid repetition?
|
|
||||||
# Or just list them simply.
|
|
||||||
# "В 14:00 -5, ясно. В 15:00 -5, снег." -> a bit verbose.
|
|
||||||
# Simplified: "Температура около -5, возможен слабый снег."
|
|
||||||
|
|
||||||
# Let's verify if weather changes significantly.
|
|
||||||
# If consistent, summarize. If not, list.
|
|
||||||
|
|
||||||
# Simple approach for TTS:
|
# Simple approach for TTS:
|
||||||
avg_temp = round(sum(next_temps) / len(next_temps))
|
avg_temp = round(sum(next_temps) / len(next_temps))
|
||||||
|
|
||||||
@@ -130,7 +461,7 @@ def get_weather_report() -> str:
|
|||||||
else:
|
else:
|
||||||
weather_desc = "переменная облачность"
|
weather_desc = "переменная облачность"
|
||||||
|
|
||||||
report += f"температура около {avg_temp} градусов, {weather_desc}."
|
report += f"температура около {get_temperature_text(avg_temp)}, {weather_desc}."
|
||||||
|
|
||||||
if will_precip:
|
if will_precip:
|
||||||
report += " Ожидаются осадки."
|
report += " Ожидаются осадки."
|
||||||
@@ -139,6 +470,18 @@ def get_weather_report() -> str:
|
|||||||
|
|
||||||
return report
|
return report
|
||||||
|
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
print(f"❌ Ошибка подключения к сервису погоды: невозможно подключиться к серверу")
|
||||||
|
return f"Не удалось подключиться к сервису погоды. Проверьте интернет-соединение."
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
print(f"❌ Таймаут запроса к сервису погоды")
|
||||||
|
return f"Время ожидания ответа от сервиса погоды истекло."
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
print(f"❌ HTTP ошибка при получении погоды: {e}")
|
||||||
|
return f"Ошибка при получении данных о погоде: {e}"
|
||||||
|
except KeyError as e:
|
||||||
|
print(f"❌ Ошибка структуры данных погоды: {e}")
|
||||||
|
return f"Получены некорректные данные о погоде."
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Ошибка получения погоды: {e}")
|
print(f"❌ Ошибка получения погоды: {e}")
|
||||||
return "Не удалось получить полные данные о погоде."
|
return "Не удалось получить полные данные о погоде."
|
||||||
102
app/main.py
102
app/main.py
@@ -20,6 +20,7 @@ import re
|
|||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
|
||||||
# Для воспроизведения звуков (mp3)
|
# Для воспроизведения звуков (mp3)
|
||||||
@@ -61,8 +62,14 @@ def signal_handler(sig, frame):
|
|||||||
Позволяет корректно завершить работу программы, освободив ресурсы (микрофон, модели).
|
Позволяет корректно завершить работу программы, освободив ресурсы (микрофон, модели).
|
||||||
"""
|
"""
|
||||||
print("\n\n👋 Завершение работы...")
|
print("\n\n👋 Завершение работы...")
|
||||||
|
try:
|
||||||
cleanup_wakeword() # Остановка Porcupine
|
cleanup_wakeword() # Остановка Porcupine
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при остановке wakeword: {e}")
|
||||||
|
try:
|
||||||
cleanup_stt() # Остановка Deepgram
|
cleanup_stt() # Остановка Deepgram
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при остановке STT: {e}")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
@@ -180,8 +187,20 @@ def main():
|
|||||||
# (True = режим диалога, слушаем сразу. False = ждем "Alexandr")
|
# (True = режим диалога, слушаем сразу. False = ждем "Alexandr")
|
||||||
skip_wakeword = False
|
skip_wakeword = False
|
||||||
|
|
||||||
|
# Переменная для отслеживания последней проверки здоровья STT
|
||||||
|
last_stt_check = time.time()
|
||||||
|
|
||||||
# БЕСКОНЕЧНЫЙ ЦИКЛ РАБОТЫ
|
# БЕСКОНЕЧНЫЙ ЦИКЛ РАБОТЫ
|
||||||
while True:
|
while True:
|
||||||
|
# Периодическая проверка здоровья STT каждые 10 минут
|
||||||
|
if time.time() - last_stt_check > 600: # 10 минут = 600 секунд
|
||||||
|
try:
|
||||||
|
recognizer = get_recognizer()
|
||||||
|
if hasattr(recognizer, 'check_connection_health'):
|
||||||
|
recognizer.check_connection_health()
|
||||||
|
last_stt_check = time.time()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при проверке здоровья STT: {e}")
|
||||||
try:
|
try:
|
||||||
# Гарантируем, что микрофон детектора wake word освобожден
|
# Гарантируем, что микрофон детектора wake word освобожден
|
||||||
stop_wakeword_monitoring()
|
stop_wakeword_monitoring()
|
||||||
@@ -201,8 +220,8 @@ def main():
|
|||||||
|
|
||||||
# --- Шаг 1: Активация ---
|
# --- Шаг 1: Активация ---
|
||||||
if not skip_wakeword:
|
if not skip_wakeword:
|
||||||
# Ожидание фразы "Alexandr". Используем таймаут 1 сек, чтобы часто проверять будильники.
|
# Ожидание фразы "Alexandr". Используем таймаут 0.5 сек, чтобы чаще проверять будильники.
|
||||||
detected = wait_for_wakeword(timeout=1.0)
|
detected = wait_for_wakeword(timeout=0.5)
|
||||||
|
|
||||||
# Если время вышло, а фразы не было — начинаем цикл заново (проверяем будильники)
|
# Если время вышло, а фразы не было — начинаем цикл заново (проверяем будильники)
|
||||||
if not detected:
|
if not detected:
|
||||||
@@ -212,13 +231,34 @@ def main():
|
|||||||
if ding_sound:
|
if ding_sound:
|
||||||
ding_sound.play()
|
ding_sound.play()
|
||||||
|
|
||||||
# Фраза услышана! Слушаем команду пользователя (7 секунд тишины макс)
|
# Фраза услышана! Слушаем команду пользователя (5 секунд тишины макс)
|
||||||
user_text = listen(timeout_seconds=7.0)
|
try:
|
||||||
|
user_text = listen(timeout_seconds=5.0)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при прослушивании: {e}")
|
||||||
|
print("Переинициализация STT...")
|
||||||
|
try:
|
||||||
|
cleanup_stt()
|
||||||
|
get_recognizer().initialize()
|
||||||
|
except Exception as init_error:
|
||||||
|
print(f"Ошибка переинициализации STT: {init_error}")
|
||||||
|
continue # Продолжаем цикл
|
||||||
else:
|
else:
|
||||||
# Режим диалога (Follow-up): ждем продолжения речи без "Alexandr"
|
# Режим диалога (Follow-up): ждем продолжения речи без "Alexandr"
|
||||||
print("👂 Слушаю продолжение диалога (5 сек)...")
|
print("👂 Слушаю продолжение диалога (3 сек)...")
|
||||||
# Ждем начала речи 5 сек. Если начали говорить, слушаем до 10 сек.
|
# Ждем начала речи 3 сек. Если начали говорить, слушаем до 7 сек.
|
||||||
user_text = listen(timeout_seconds=10.0, detection_timeout=5.0)
|
try:
|
||||||
|
user_text = listen(timeout_seconds=7.0, detection_timeout=3.0)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при прослушивании: {e}")
|
||||||
|
print("Переинициализация STT...")
|
||||||
|
try:
|
||||||
|
cleanup_stt()
|
||||||
|
get_recognizer().initialize()
|
||||||
|
except Exception as init_error:
|
||||||
|
print(f"Ошибка переинициализации STT: {init_error}")
|
||||||
|
skip_wakeword = False
|
||||||
|
continue
|
||||||
|
|
||||||
if not user_text:
|
if not user_text:
|
||||||
# Пользователь промолчал — выходим из режима диалога, засыпаем.
|
# Пользователь промолчал — выходим из режима диалога, засыпаем.
|
||||||
@@ -326,10 +366,42 @@ def main():
|
|||||||
"нужен ли зонт",
|
"нужен ли зонт",
|
||||||
"брать ли зонт",
|
"брать ли зонт",
|
||||||
"прогноз погоды",
|
"прогноз погоды",
|
||||||
|
"че там на улице",
|
||||||
|
"что там на улице",
|
||||||
|
"как на улице",
|
||||||
|
"как на улице-то",
|
||||||
]
|
]
|
||||||
|
|
||||||
if any(trigger in user_text.lower() for trigger in weather_triggers):
|
# Проверяем, содержит ли запрос информацию о конкретном городе
|
||||||
weather_report = get_weather_report()
|
requested_city = None
|
||||||
|
user_text_lower = user_text.lower()
|
||||||
|
|
||||||
|
# Проверяем наличие упоминания города в запросе (например, "погода в Нью-Йорке", "какая погода в Москве")
|
||||||
|
import re
|
||||||
|
city_patterns = [
|
||||||
|
r"в\s+городе\s+([а-яёa-z]+[-\s]*[а-яёa-z]*(?:[-\s]+[а-яёa-z]+)*)", # "в городе Волгоград"
|
||||||
|
r"в\s+([а-яёa-z]+[-\s]*[а-яёa-z]*(?:[-\s]+[а-яёa-z]+)*)", # "в Нью-Йорке" - улучшенный паттерн для составных названий
|
||||||
|
r"погода\s+в\s+([а-яёa-z]+[-\s]*[а-яёa-z]*(?:[-\s]+[а-яёa-z]+)*)", # "погода в Москве" - улучшенный паттерн
|
||||||
|
r"погода\s+([а-яёa-z]+[-\s]*[а-яёa-z]*(?:[-\s]+[а-яёa-z]+)*)\s+(?:какая|сейчас|там)", # "погода Москва какая"
|
||||||
|
r"(?:какая|как)\s+погода\s+в\s+([а-яёa-z]+[-\s]*[а-яёa-z]*(?:[-\s]+[а-яёa-z]+)*)", # "какая погода в Москве"
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern in city_patterns:
|
||||||
|
match = re.search(pattern, user_text_lower, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
potential_city = match.group(1).strip()
|
||||||
|
# Проверяем, что это не местоимение или другое слово, а реально название города
|
||||||
|
invalid_words = ["этом", "том", "той", "тут", "здесь", "там", "всё", "все", "всей", "всего", "всем", "всеми", "городе", "город", "село", "деревня", "посёлок", "аул", "станция", "область", "район", "край", "республика"]
|
||||||
|
if potential_city and len(potential_city) > 1 and not any(word in potential_city for word in invalid_words):
|
||||||
|
requested_city = potential_city.title() # Приводим к формату "Нью-Йорк", "Москва"
|
||||||
|
break
|
||||||
|
|
||||||
|
# Проверяем, содержит ли запрос одну из погодных команд
|
||||||
|
has_weather_trigger = any(trigger in user_text_lower for trigger in weather_triggers)
|
||||||
|
|
||||||
|
if has_weather_trigger:
|
||||||
|
from .features.weather import get_weather_report
|
||||||
|
weather_report = get_weather_report(requested_city)
|
||||||
clean_report = clean_response(weather_report, language="ru")
|
clean_report = clean_response(weather_report, language="ru")
|
||||||
speak(clean_report)
|
speak(clean_report)
|
||||||
last_response = clean_report
|
last_response = clean_report
|
||||||
@@ -362,9 +434,21 @@ def main():
|
|||||||
)
|
)
|
||||||
speak(prompt)
|
speak(prompt)
|
||||||
# Слушаем саму фразу на нужном языке
|
# Слушаем саму фразу на нужном языке
|
||||||
|
try:
|
||||||
text_to_translate = listen(
|
text_to_translate = listen(
|
||||||
timeout_seconds=7.0, detection_timeout=5.0, lang=source_lang
|
timeout_seconds=7.0, detection_timeout=5.0, lang=source_lang
|
||||||
)
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при прослушивании для перевода: {e}")
|
||||||
|
print("Переинициализация STT...")
|
||||||
|
try:
|
||||||
|
cleanup_stt()
|
||||||
|
get_recognizer().initialize()
|
||||||
|
except Exception as init_error:
|
||||||
|
print(f"Ошибка переинициализации STT: {init_error}")
|
||||||
|
speak("Произошла ошибка при распознавании речи.")
|
||||||
|
skip_wakeword = False
|
||||||
|
continue
|
||||||
|
|
||||||
if not text_to_translate:
|
if not text_to_translate:
|
||||||
speak("Я не расслышал текст для перевода.")
|
speak("Я не расслышал текст для перевода.")
|
||||||
|
|||||||
Reference in New Issue
Block a user