diff --git a/README.md b/README.md index 0b687b5..3fd074e 100644 --- a/README.md +++ b/README.md @@ -15,14 +15,14 @@ ## Что это -`Alexander Smart Speaker` слушает ключевое слово `Alexandr`, распознает речь, маршрутизирует команду в нужный модуль и озвучивает ответ. +`Alexander Smart Speaker` слушает ключевое слово `Waltron`, распознает речь, маршрутизирует команду в нужный модуль и озвучивает ответ. Проект оптимизирован под русский язык, но поддерживает RU/EN сценарии (включая перевод и mixed-language TTS). Проект собран как локальная голосовая колонка под Linux: активация по wake word, распознавание речи, маршрутизация команд, ответ через AI или встроенные модули и затем озвучка результата. ## Возможности -- Активация по wake word `Alexandr` (Porcupine). +- Активация по wake word `Waltron` (Porcupine). - Follow-up окно 4 секунды после ответа: если пользователь молчит, ассистент возвращается к ожиданию wake word. - Распознавание речи через Deepgram (WebSocket, VAD, fast stop). - Озвучка через Silero TTS (RU + EN, с прерыванием по wake word). @@ -38,7 +38,7 @@ ```mermaid flowchart TD - A[Wake Word: Alexandr] --> B[STT: Deepgram] + A[Wake Word: Waltron] --> B[STT: Deepgram] B --> C{Маршрутизация команды} C --> D[Feature modules] C --> E[AI/Translation] @@ -116,7 +116,7 @@ make run python run.py ``` -После запуска ассистент перейдет в режим ожидания фразы `Alexandr`. +После запуска ассистент перейдет в режим ожидания фразы `Waltron`. ### Кросс-платформенный аудио режим @@ -164,7 +164,7 @@ python run.py | Категория | Примеры | |---|---| -| Активация | `Alexandr` | +| Активация | `Waltron` | | AI-диалог | `Почему небо голубое?` | | Перевод | `Переведи на английский: как дела` | | Погода | `Какая погода?`, `Погода в Москве` | @@ -216,7 +216,7 @@ alexander_smart-speaker/ | Проблема | Что проверить | |---|---| -| Не реагирует на `Alexandr` | `PORCUPINE_ACCESS_KEY`, микрофон, чувствительность `PORCUPINE_SENSITIVITY` | +| Не реагирует на `Waltron` | `PORCUPINE_ACCESS_KEY`, микрофон, чувствительность `PORCUPINE_SENSITIVITY` | | STT не распознает речь | `DEEPGRAM_API_KEY`, сетевой доступ, выбранный микрофон | | Нет звука | корректное аудиоустройство и доступность `pactl`/`amixer` | | `Audio input/output initialization failed` | проверить, что звук-сервер запущен (PipeWire/PulseAudio), и при необходимости задать `AUDIO_INPUT_DEVICE_NAME`/`AUDIO_OUTPUT_DEVICE_NAME` | diff --git a/app/audio/stt.py b/app/audio/stt.py index 851a97a..80bfde3 100644 --- a/app/audio/stt.py +++ b/app/audio/stt.py @@ -13,7 +13,7 @@ import time import pyaudio 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, WAKE_WORD_ALIASES from deepgram import ( DeepgramClient, DeepgramClientOptions, @@ -50,13 +50,13 @@ logging.getLogger("deepgram").setLevel(logging.WARNING) # Базовые пороги для остановки STT INITIAL_SILENCE_TIMEOUT_SECONDS = 5.0 -POST_SPEECH_SILENCE_TIMEOUT_SECONDS = 3.5 +POST_SPEECH_SILENCE_TIMEOUT_SECONDS = 2.0 # Длинный защитный предел, чтобы не обрывать обычную длинную фразу. -# Фактическое завершение происходит примерно после 3.5 сек тишины после речи. +# Фактическое завершение происходит примерно после 2.0 сек тишины после речи. MAX_ACTIVE_SPEECH_SECONDS = 300.0 _FAST_STOP_UTTERANCE_RE = re.compile( - r"^(?:(?:александр|алесандр|alexander|alexandr)\s+)?" + r"^(?:(?:" + "|".join(re.escape(alias) for alias in WAKE_WORD_ALIASES) + r")\s+)?" r"(?:стоп|хватит|перестань|прекрати|замолчи|тихо|пауза)" r"(?:\s+(?:пожалуйста|please))?$", flags=re.IGNORECASE, diff --git a/app/audio/tts.py b/app/audio/tts.py index 077f435..6b6fbbb 100644 --- a/app/audio/tts.py +++ b/app/audio/tts.py @@ -6,7 +6,7 @@ Supports interruption via wake word detection using threading. # Модуль синтеза речи (TTS - Text-to-Speech). # Использует нейросеть Silero TTS для качественной русской речи. -# Также поддерживает прерывание речи, если пользователь скажет "Alexandr". +# Также поддерживает прерывание речи по wake word. import re import threading diff --git a/app/audio/wakeword.py b/app/audio/wakeword.py index d834a8c..4ed938a 100644 --- a/app/audio/wakeword.py +++ b/app/audio/wakeword.py @@ -1,10 +1,10 @@ """ Wake word detection module using Porcupine. -Listens for the "Alexandr" wake word. +Listens for the configured wake word. """ # Этот модуль отвечает за "уши" ассистента в режиме ожидания. -# Он использует библиотеку Porcupine для эффективного (мало CPU) обнаружения ключевой фразы "Alexandr". +# Он использует библиотеку Porcupine для эффективного (мало CPU) обнаружения ключевой фразы. import pvporcupine import pyaudio @@ -14,6 +14,7 @@ from ..core.config import ( PORCUPINE_ACCESS_KEY, PORCUPINE_KEYWORD_PATH, PORCUPINE_SENSITIVITY, + WAKE_WORD, ) from ..core.audio_manager import get_audio_manager @@ -47,7 +48,7 @@ class WakeWordDetector: self.pa = self._audio_manager.get_pyaudio() self._open_stream() print( - "🎤 Ожидание wake word 'Alexandr' " + f"🎤 Ожидание wake word '{WAKE_WORD}' " f"(sens={PORCUPINE_SENSITIVITY:.2f}, mic_rate={self._capture_sample_rate})..." ) @@ -133,7 +134,7 @@ class WakeWordDetector: def wait_for_wakeword(self, timeout: float = None) -> bool: """ - Блокирующая функция: ждет, пока не будет услышана фраза "Alexandr" + Блокирующая функция: ждет, пока не будет услышана wake word или пока не истечет timeout. Args: diff --git a/app/core/ai.py b/app/core/ai.py index 13388fa..b9275cf 100644 --- a/app/core/ai.py +++ b/app/core/ai.py @@ -21,6 +21,8 @@ from .config import ( OPENROUTER_API_KEY, OPENROUTER_API_URL, OPENROUTER_MODEL, + WAKE_WORD, + WAKE_WORD_ALIASES, ZAI_API_KEY, ZAI_API_URL, ZAI_MODEL, @@ -29,15 +31,18 @@ from .config import ( _HTTP = requests.Session() # Системный промпт -SYSTEM_PROMPT = """Ты — Александр, умный голосовой ассистент с человеческим поведением. +_wake_word_aliases_text = ", ".join(WAKE_WORD_ALIASES) +SYSTEM_PROMPT = f"""Ты — умный голосовой ассистент с человеческим поведением. Веди себя как живой человек: будь дружелюбным, естественным и немного эмоциональным, где это уместно. Твоя главная цель — помогать пользователю и поддерживать интересный диалог. Отвечай кратко и по существу, на русском языке. Избегай длинных списков, сложного форматирования и спецсимволов, так как твои ответы озвучиваются голосом. Пиши в разговорном стиле, как при живом общении, но не забывай о вежливости и правильности твоих ответов. -ВАЖНО: Не используй в ответах панибратские или сленговые приветствия и обращения, такие как "Эй", "Хэй", "Слушай" в начале фразы и подобные.""" +ВАЖНО: Не используй в ответах панибратские или сленговые приветствия и обращения, такие как "Эй", "Хэй", "Слушай" в начале фразы и подобные. +Тебя активируют словом "{WAKE_WORD}". Никогда не произноси это слово и его варианты ({_wake_word_aliases_text}) ни в каком ответе. +Если пользователь спрашивает, как тебя зовут или как к тебе обращаться, отвечай нейтрально: "Я ваш голосовой ассистент".""" SYSTEM_PROMPT += ( - '\nROLE_JSON: {"name":"Александр","role":"умный голосовой ассистент",' + '\nROLE_JSON: {"name":"голосовой ассистент","role":"умный голосовой ассистент",' '"language":"ru","style":["дружелюбный","естественный","краткий"],"format":"plain"}' ) diff --git a/app/core/cleaner.py b/app/core/cleaner.py index 449c870..b276814 100644 --- a/app/core/cleaner.py +++ b/app/core/cleaner.py @@ -3,6 +3,7 @@ import re import pymorphy3 from num2words import num2words +from .config import WAKE_WORD, WAKE_WORD_ALIASES from .roman import roman_to_int morph = pymorphy3.MorphAnalyzer() @@ -83,6 +84,10 @@ MONTHS_GENITIVE = [ # Время TIME_UNIT_LEMMAS = {"час", "минута", "секунда"} +WAKE_WORD_BLOCKED_PATTERNS = [ + re.compile(rf"\b{re.escape(alias)}\b", flags=re.IGNORECASE) + for alias in set(WAKE_WORD_ALIASES) | {WAKE_WORD.lower()} +] # Суффиксы порядковых _ORDINAL_SUFFIX_MAP = { @@ -419,6 +424,10 @@ def clean_response(text: str, language: str = "ru") -> str: flags=re.IGNORECASE | re.MULTILINE, ) + # Запрет на произнесение wake word в любых ответах ассистента. + for pattern in WAKE_WORD_BLOCKED_PATTERNS: + text = pattern.sub("ассистент", text) + # Числа в слова if language == "ru": text = roman_numerals_to_words(text) diff --git a/app/core/config.py b/app/core/config.py index 28c15c6..201dea0 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -66,8 +66,17 @@ DEEPGRAM_API_KEY = os.getenv("DEEPGRAM_API_KEY") # --- Настройки активации голосом (Porcupine) --- # Ключ доступа PicoVoice PORCUPINE_ACCESS_KEY = os.getenv("PORCUPINE_ACCESS_KEY") +# Wake word label and common ASR aliases. +WAKE_WORD = "Waltron" +WAKE_WORD_ALIASES = ( + "waltron", + "voltron", + "волтрон", + "уолтрон", + "валтрон", +) # Путь к файлу модели ключевого слова (.ppn), который лежит в папке assets/models -PORCUPINE_KEYWORD_PATH = BASE_DIR / "assets" / "models" / "Alexandr_en_linux_v4_0_0.ppn" +PORCUPINE_KEYWORD_PATH = BASE_DIR / "assets" / "models" / "Waltron_en_linux_v4_0_0.ppn" # Чувствительность wake word (0..1). Выше = ловит легче, но больше ложных срабатываний. PORCUPINE_SENSITIVITY = float(os.getenv("PORCUPINE_SENSITIVITY", "0.8")) diff --git a/app/features/music.py b/app/features/music.py index 54ff668..4652166 100644 --- a/app/features/music.py +++ b/app/features/music.py @@ -290,8 +290,8 @@ class SpotifyMusicController: # Явные команды распознавания музыки (типа "угадай песню") recognize_patterns = [ - r"((александр|александра|алесандр|alexander)\s+)?(угадай|распознай|определи)\s+(мелод|музык|песн|трек)", - r"((александр|александра|алесандр|alexander)\s+)?(что за|какая это)\s+(музык|песн|трек)", + r"((waltron|voltron|волтрон|уолтрон|валтрон)\s+)?(угадай|распознай|определи)\s+(мелод|музык|песн|трек)", + r"((waltron|voltron|волтрон|уолтрон|валтрон)\s+)?(что за|какая это)\s+(музык|песн|трек)", r"(identify|recognize)\s+(song|music|track)", ] for pattern in recognize_patterns: diff --git a/app/main.py b/app/main.py index 24eb834..b7b01ad 100644 --- a/app/main.py +++ b/app/main.py @@ -34,7 +34,7 @@ from .audio.wakeword import ( stop_monitoring as stop_wakeword_monitoring, ) from .core.ai import ask_ai_stream, translate_text -from .core.config import BASE_DIR +from .core.config import BASE_DIR, WAKE_WORD from .core.cleaner import clean_response from .core.commands import is_stop_command from .core.smalltalk import get_smalltalk_response @@ -87,10 +87,14 @@ _REPEAT_PHRASES = { "скажи еще раз", "что ты сказал", "повтори пожалуйста", - "александр еще раз", - "еще раз александр", - "александр повтори", - "повтори александр", + "waltron еще раз", + "еще раз waltron", + "waltron повтори", + "повтори waltron", + "волтрон еще раз", + "еще раз волтрон", + "волтрон повтори", + "повтори волтрон", } _WEATHER_TRIGGERS = ( @@ -201,7 +205,7 @@ def main(): print("=" * 50) print("🔊 УМНАЯ КОЛОНКА") print("=" * 50) - print("Скажите 'Alexandr' для активации") + print(f"Скажите '{WAKE_WORD}' для активации") print("Нажмите Ctrl+C для выхода") print("=" * 50) print() @@ -248,7 +252,7 @@ def main(): # Режим диалога (без wake word) skip_wakeword = False - followup_idle_timeout_seconds = 4.0 + followup_idle_timeout_seconds = 3.7 # Контекст уточнения времени для таймера/будильника pending_time_target = None @@ -347,7 +351,7 @@ def main(): skip_wakeword = False continue print("_" * 50) - print("💤 Жду 'Alexandr'...") + print(f"💤 Жду '{WAKE_WORD}'...") skip_wakeword = False continue diff --git a/assets/models/LICENSE.txt b/assets/models/LICENSE.txt new file mode 100755 index 0000000..74a468f --- /dev/null +++ b/assets/models/LICENSE.txt @@ -0,0 +1 @@ +A copy of license terms is available at https://picovoice.ai/docs/terms-of-use/ \ No newline at end of file diff --git a/assets/models/Waltron_en_linux_v4_0_0.ppn b/assets/models/Waltron_en_linux_v4_0_0.ppn new file mode 100644 index 0000000..c95f0e9 Binary files /dev/null and b/assets/models/Waltron_en_linux_v4_0_0.ppn differ