другая структура проекта + beads + александр повтори + комментарии везде + readme
This commit is contained in:
348
app/main.py
Normal file
348
app/main.py
Normal file
@@ -0,0 +1,348 @@
|
||||
"""
|
||||
Smart Speaker - Main Application
|
||||
Голосовой ассистент с wake word detection, STT, AI и TTS.
|
||||
|
||||
Flow:
|
||||
1. Wait for wake word ("Alexandr")
|
||||
2. Listen to user speech (STT)
|
||||
3. Send query to AI (Perplexity)
|
||||
4. Clean response from markdown
|
||||
5. Speak response (TTS)
|
||||
6. Loop back to step 1
|
||||
"""
|
||||
|
||||
# Главный файл приложения (`main.py`).
|
||||
# Здесь находится основной бесконечный цикл, который связывает все компоненты воедино.
|
||||
|
||||
import signal
|
||||
import sys
|
||||
from collections import deque
|
||||
|
||||
# Импорт наших модулей
|
||||
from .audio.wakeword import (
|
||||
wait_for_wakeword,
|
||||
cleanup as cleanup_wakeword,
|
||||
check_wakeword_once,
|
||||
stop_monitoring as stop_wakeword_monitoring,
|
||||
)
|
||||
from .audio.stt import listen, cleanup as cleanup_stt, get_recognizer
|
||||
from .core.ai import ask_ai, translate_text
|
||||
from .core.cleaner import clean_response
|
||||
from .audio.tts import speak, initialize as init_tts
|
||||
from .audio.sound_level import set_volume, parse_volume_text
|
||||
from .features.alarm import get_alarm_clock
|
||||
|
||||
# Список стоп-слов, чтобы прервать диалог или остановить ассистента
|
||||
STOP_WORDS = {
|
||||
"стоп",
|
||||
"хватит",
|
||||
"перестань",
|
||||
"замолчи",
|
||||
"прекрати",
|
||||
"тихо",
|
||||
"stop",
|
||||
}
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
"""
|
||||
Обработчик сигнала Ctrl+C.
|
||||
Позволяет корректно завершить работу программы, освободив ресурсы (микрофон, модели).
|
||||
"""
|
||||
print("\n\n👋 Завершение работы...")
|
||||
cleanup_wakeword() # Остановка Porcupine
|
||||
cleanup_stt() # Остановка Deepgram
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def parse_translation_request(text: str):
|
||||
"""
|
||||
Определяет, является ли фраза запросом на перевод.
|
||||
|
||||
Пример: "Переведи на английский привет мир"
|
||||
Возвращает словарь: {'source_lang': 'ru', 'target_lang': 'en', 'text': 'привет мир'}
|
||||
Или None, если это не запрос перевода.
|
||||
"""
|
||||
text_lower = text.lower().strip()
|
||||
# Список префиксов команд перевода и соответствующих направлений языков
|
||||
commands = [
|
||||
("переведи на английский", "ru", "en"),
|
||||
("переведи на русский", "en", "ru"),
|
||||
("переведи с английского", "en", "ru"),
|
||||
("переведи с русского", "ru", "en"),
|
||||
("как по-английски", "ru", "en"),
|
||||
("как по английски", "ru", "en"),
|
||||
("как по-русски", "en", "ru"),
|
||||
("как по русски", "en", "ru"),
|
||||
("translate to english", "ru", "en"),
|
||||
("translate into english", "ru", "en"),
|
||||
("translate to russian", "en", "ru"),
|
||||
("translate into russian", "en", "ru"),
|
||||
("translate from english", "en", "ru"),
|
||||
("translate from russian", "ru", "en"),
|
||||
]
|
||||
|
||||
for prefix, source_lang, target_lang in commands:
|
||||
if text_lower.startswith(prefix):
|
||||
# Отрезаем команду (префикс), оставляем только текст для перевода
|
||||
rest = text[len(prefix) :].strip()
|
||||
return {
|
||||
"source_lang": source_lang,
|
||||
"target_lang": target_lang,
|
||||
"text": rest,
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def is_stop_command(text: str) -> bool:
|
||||
"""
|
||||
Проверяет, содержится ли в тексте команда остановки.
|
||||
Удаляет знаки препинания и ищет слова из списка STOP_WORDS.
|
||||
"""
|
||||
text_lower = text.lower()
|
||||
for ch in ",.!?:;":
|
||||
text_lower = text_lower.replace(ch, " ")
|
||||
words = text_lower.split()
|
||||
for word in words:
|
||||
if word in STOP_WORDS:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Основная функция (точка входа).
|
||||
"""
|
||||
print("=" * 50)
|
||||
print("🔊 УМНАЯ КОЛОНКА")
|
||||
print("=" * 50)
|
||||
print("Скажите 'Alexandr' для активации")
|
||||
print("Нажмите Ctrl+C для выхода")
|
||||
print("=" * 50)
|
||||
print()
|
||||
|
||||
# Устанавливаем перехватчик Ctrl+C
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
# Предварительная инициализация моделей (занимает пару секунд при старте)
|
||||
print("⏳ Инициализация моделей...")
|
||||
get_recognizer().initialize() # Подключение к Deepgram
|
||||
init_tts() # Загрузка нейросети для синтеза речи (Silero)
|
||||
alarm_clock = get_alarm_clock() # Загрузка будильников
|
||||
print()
|
||||
|
||||
# История чата (храним последние 10 обменов репликами для контекста)
|
||||
chat_history = deque(maxlen=20)
|
||||
|
||||
# Переменная для хранения последнего ответа ассистента
|
||||
last_response = None
|
||||
|
||||
# Переменная, указывающая, нужно ли пропускать ожидание wake word
|
||||
# (True = режим диалога, слушаем сразу. False = ждем "Alexandr")
|
||||
skip_wakeword = False
|
||||
|
||||
# БЕСКОНЕЧНЫЙ ЦИКЛ РАБОТЫ
|
||||
while True:
|
||||
try:
|
||||
# Гарантируем, что микрофон детектора wake word освобожден
|
||||
stop_wakeword_monitoring()
|
||||
|
||||
# --- Проверка будильников ---
|
||||
# Проверяем каждую итерацию. Если будильник сработал, он заблокирует выполнение, пока его не выключат.
|
||||
if alarm_clock.check_alarms():
|
||||
# Если будильник прозвенел и был выключен пользователем, сбрасываем режим диалога
|
||||
skip_wakeword = False
|
||||
continue
|
||||
|
||||
# --- Шаг 1: Активация ---
|
||||
if not skip_wakeword:
|
||||
# Ожидание фразы "Alexandr". Используем таймаут 1 сек, чтобы часто проверять будильники.
|
||||
detected = wait_for_wakeword(timeout=1.0)
|
||||
|
||||
# Если время вышло, а фразы не было — начинаем цикл заново (проверяем будильники)
|
||||
if not detected:
|
||||
continue
|
||||
|
||||
# Фраза услышана! Слушаем команду пользователя (7 секунд тишины макс)
|
||||
user_text = listen(timeout_seconds=7.0)
|
||||
else:
|
||||
# Режим диалога (Follow-up): ждем продолжения речи без "Alexandr"
|
||||
print("👂 Слушаю продолжение диалога (5 сек)...")
|
||||
# Ждем начала речи 5 сек. Если начали говорить, слушаем до 10 сек.
|
||||
user_text = listen(timeout_seconds=10.0, detection_timeout=5.0)
|
||||
|
||||
if not user_text:
|
||||
# Пользователь промолчал — выходим из режима диалога, засыпаем.
|
||||
skip_wakeword = False
|
||||
continue
|
||||
|
||||
# --- Шаг 2: Анализ распознанного текста ---
|
||||
if not user_text:
|
||||
# Была активация, но речь не распознана
|
||||
speak("Извините, я вас не расслышал. Попробуйте ещё раз.")
|
||||
skip_wakeword = False # Возвращаемся в режим ожидания имени
|
||||
continue
|
||||
|
||||
# Проверка на команду "Стоп"
|
||||
if is_stop_command(user_text):
|
||||
print("_" * 50)
|
||||
print("💤 Жду 'Alexandr' для активации...")
|
||||
skip_wakeword = False
|
||||
continue
|
||||
|
||||
# Проверка на команду "Повтори" / "Еще раз"
|
||||
user_text_lower = user_text.lower().strip()
|
||||
repeat_phrases = [
|
||||
"еще раз",
|
||||
"повтори",
|
||||
"скажи еще раз",
|
||||
"что ты сказал",
|
||||
"повтори пожалуйста",
|
||||
"александр еще раз",
|
||||
"еще раз александр",
|
||||
"александр повтори",
|
||||
"повтори александр",
|
||||
]
|
||||
# Проверяем точное совпадение или если фраза начинается с "повтори" (но не "повтори за мной")
|
||||
if user_text_lower in repeat_phrases or (
|
||||
user_text_lower.startswith("повтори") and "за мной" not in user_text_lower
|
||||
):
|
||||
if last_response:
|
||||
print(f"🔁 Повторяю: {last_response}")
|
||||
speak(last_response)
|
||||
else:
|
||||
speak("Я еще ничего не говорил.")
|
||||
# После повтора остаемся в диалоге
|
||||
skip_wakeword = True
|
||||
continue
|
||||
|
||||
# Проверка команд будильника ("поставь будильник на 7")
|
||||
alarm_response = alarm_clock.parse_command(user_text)
|
||||
if alarm_response:
|
||||
speak(alarm_response)
|
||||
last_response = alarm_response
|
||||
continue
|
||||
|
||||
# Проверка команды громкости ("громкость 5")
|
||||
if user_text.lower().startswith("громкость"):
|
||||
try:
|
||||
# Убираем слово "громкость" и ищем число
|
||||
vol_str = user_text.lower().replace("громкость", "", 1).strip()
|
||||
level = parse_volume_text(vol_str)
|
||||
|
||||
if level is not None:
|
||||
if set_volume(level):
|
||||
msg = f"Громкость установлена на {level}"
|
||||
speak(msg)
|
||||
last_response = msg
|
||||
else:
|
||||
speak("Не удалось установить громкость.")
|
||||
else:
|
||||
speak(
|
||||
"Я не понял число громкости. Скажите число от одного до десяти."
|
||||
)
|
||||
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка громкости: {e}")
|
||||
speak("Не удалось изменить громкость.")
|
||||
continue
|
||||
|
||||
# Проверка запроса на перевод
|
||||
translation_request = parse_translation_request(user_text)
|
||||
if translation_request:
|
||||
source_lang = translation_request["source_lang"]
|
||||
target_lang = translation_request["target_lang"]
|
||||
text_to_translate = translation_request["text"]
|
||||
|
||||
# Если сказано только "переведи на английский", спрашиваем "что перевести?"
|
||||
if not text_to_translate:
|
||||
prompt = (
|
||||
"Скажи фразу на английском."
|
||||
if source_lang == "en"
|
||||
else "Скажи фразу на русском."
|
||||
)
|
||||
speak(prompt)
|
||||
# Слушаем саму фразу на нужном языке
|
||||
text_to_translate = listen(
|
||||
timeout_seconds=7.0, detection_timeout=5.0, lang=source_lang
|
||||
)
|
||||
|
||||
if not text_to_translate:
|
||||
speak("Я не расслышал текст для перевода.")
|
||||
skip_wakeword = False
|
||||
continue
|
||||
|
||||
# Выполняем перевод через AI
|
||||
translated_text = translate_text(
|
||||
text_to_translate, source_lang, target_lang
|
||||
)
|
||||
# Очищаем результат (убираем лишние символы)
|
||||
clean_text = clean_response(translated_text, language=target_lang)
|
||||
|
||||
# Сохраняем для повтора
|
||||
last_response = clean_text
|
||||
|
||||
# Озвучиваем перевод на целевом языке
|
||||
completed = speak(
|
||||
clean_text,
|
||||
check_interrupt=check_wakeword_once,
|
||||
language=target_lang,
|
||||
)
|
||||
stop_wakeword_monitoring()
|
||||
skip_wakeword = True # Остаемся в диалоге
|
||||
|
||||
if not completed:
|
||||
print("⏹️ Перевод прерван - слушаю следующий вопрос")
|
||||
continue
|
||||
|
||||
# --- Шаг 3: Запрос к AI (обычный чат) ---
|
||||
# Добавляем сообщение пользователя в историю
|
||||
chat_history.append({"role": "user", "content": user_text})
|
||||
|
||||
# Отправляем историю диалога в Perplexity
|
||||
ai_response = ask_ai(list(chat_history))
|
||||
|
||||
# Добавляем ответ AI в историю
|
||||
chat_history.append({"role": "assistant", "content": ai_response})
|
||||
|
||||
# --- Шаг 4: Очистка ответа ---
|
||||
# Убираем Markdown (**жирный**, *курсив*) и готовим числа для озвучки
|
||||
clean_text = clean_response(ai_response, language="ru")
|
||||
|
||||
# Сохраняем последний ответ для функции "еще раз"
|
||||
last_response = clean_text
|
||||
|
||||
# --- Шаг 5: Озвучка ответа ---
|
||||
# check_interrupt=check_wakeword_once позволяет прервать речь, сказав "Alexandr"
|
||||
completed = speak(
|
||||
clean_text, check_interrupt=check_wakeword_once, language="ru"
|
||||
)
|
||||
|
||||
# После озвучки обязательно закрываем поток микрофона, который открывался для проверки прерывания
|
||||
stop_wakeword_monitoring()
|
||||
|
||||
# Включаем режим диалога (следующий запрос можно говорить без имени)
|
||||
skip_wakeword = True
|
||||
|
||||
if not completed:
|
||||
print("⏹️ Ответ прерван - слушаю следующий вопрос")
|
||||
# Если перебили, значит есть новый вопрос, сразу слушаем его (цикл перезапустится)
|
||||
pass
|
||||
|
||||
print()
|
||||
print("-" * 30)
|
||||
print()
|
||||
|
||||
# --- Шаг 6: Конец итерации, возврат в начало цикла ---
|
||||
|
||||
except KeyboardInterrupt:
|
||||
signal_handler(None, None)
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка: {e}")
|
||||
speak("Произошла ошибка. Попробуйте ещё раз.")
|
||||
skip_wakeword = False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user