From e1a94c68db3864add3dabcf98bea2bf7d03b082c Mon Sep 17 00:00:00 2001 From: future Date: Sun, 15 Mar 2026 02:59:13 +0300 Subject: [PATCH] feat: switch wake word to waltron --- README.md | 12 ++++++------ app/audio/stt.py | 8 ++++---- app/audio/tts.py | 2 +- app/audio/wakeword.py | 9 +++++---- app/core/ai.py | 11 ++++++++--- app/core/cleaner.py | 9 +++++++++ app/core/config.py | 11 ++++++++++- app/features/music.py | 4 ++-- app/main.py | 20 ++++++++++++-------- assets/models/LICENSE.txt | 1 + assets/models/Waltron_en_linux_v4_0_0.ppn | Bin 0 -> 3964 bytes 11 files changed, 58 insertions(+), 29 deletions(-) create mode 100755 assets/models/LICENSE.txt create mode 100644 assets/models/Waltron_en_linux_v4_0_0.ppn 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 0000000000000000000000000000000000000000..c95f0e9d78075e06e87dcadf82b121d6cc47f014 GIT binary patch literal 3964 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 WminchOSoP_^s@Jef3PkAY#jmiQo6$c literal 0 HcmV?d00001