feat: switch wake word to waltron

This commit is contained in:
2026-03-15 02:59:13 +03:00
parent 6c2702d5e3
commit e1a94c68db
11 changed files with 58 additions and 29 deletions

View File

@@ -15,14 +15,14 @@
## Что это ## Что это
`Alexander Smart Speaker` слушает ключевое слово `Alexandr`, распознает речь, маршрутизирует команду в нужный модуль и озвучивает ответ. `Alexander Smart Speaker` слушает ключевое слово `Waltron`, распознает речь, маршрутизирует команду в нужный модуль и озвучивает ответ.
Проект оптимизирован под русский язык, но поддерживает RU/EN сценарии (включая перевод и mixed-language TTS). Проект оптимизирован под русский язык, но поддерживает RU/EN сценарии (включая перевод и mixed-language TTS).
Проект собран как локальная голосовая колонка под Linux: активация по wake word, распознавание речи, маршрутизация команд, ответ через AI или встроенные модули и затем озвучка результата. Проект собран как локальная голосовая колонка под Linux: активация по wake word, распознавание речи, маршрутизация команд, ответ через AI или встроенные модули и затем озвучка результата.
## Возможности ## Возможности
- Активация по wake word `Alexandr` (Porcupine). - Активация по wake word `Waltron` (Porcupine).
- Follow-up окно 4 секунды после ответа: если пользователь молчит, ассистент возвращается к ожиданию wake word. - Follow-up окно 4 секунды после ответа: если пользователь молчит, ассистент возвращается к ожиданию wake word.
- Распознавание речи через Deepgram (WebSocket, VAD, fast stop). - Распознавание речи через Deepgram (WebSocket, VAD, fast stop).
- Озвучка через Silero TTS (RU + EN, с прерыванием по wake word). - Озвучка через Silero TTS (RU + EN, с прерыванием по wake word).
@@ -38,7 +38,7 @@
```mermaid ```mermaid
flowchart TD flowchart TD
A[Wake Word: Alexandr] --> B[STT: Deepgram] A[Wake Word: Waltron] --> B[STT: Deepgram]
B --> C{Маршрутизация команды} B --> C{Маршрутизация команды}
C --> D[Feature modules] C --> D[Feature modules]
C --> E[AI/Translation] C --> E[AI/Translation]
@@ -116,7 +116,7 @@ make run
python run.py python run.py
``` ```
После запуска ассистент перейдет в режим ожидания фразы `Alexandr`. После запуска ассистент перейдет в режим ожидания фразы `Waltron`.
### Кросс-платформенный аудио режим ### Кросс-платформенный аудио режим
@@ -164,7 +164,7 @@ python run.py
| Категория | Примеры | | Категория | Примеры |
|---|---| |---|---|
| Активация | `Alexandr` | | Активация | `Waltron` |
| AI-диалог | `Почему небо голубое?` | | AI-диалог | `Почему небо голубое?` |
| Перевод | `Переведи на английский: как дела` | | Перевод | `Переведи на английский: как дела` |
| Погода | `Какая погода?`, `Погода в Москве` | | Погода | `Какая погода?`, `Погода в Москве` |
@@ -216,7 +216,7 @@ alexander_smart-speaker/
| Проблема | Что проверить | | Проблема | Что проверить |
|---|---| |---|---|
| Не реагирует на `Alexandr` | `PORCUPINE_ACCESS_KEY`, микрофон, чувствительность `PORCUPINE_SENSITIVITY` | | Не реагирует на `Waltron` | `PORCUPINE_ACCESS_KEY`, микрофон, чувствительность `PORCUPINE_SENSITIVITY` |
| STT не распознает речь | `DEEPGRAM_API_KEY`, сетевой доступ, выбранный микрофон | | STT не распознает речь | `DEEPGRAM_API_KEY`, сетевой доступ, выбранный микрофон |
| Нет звука | корректное аудиоустройство и доступность `pactl`/`amixer` | | Нет звука | корректное аудиоустройство и доступность `pactl`/`amixer` |
| `Audio input/output initialization failed` | проверить, что звук-сервер запущен (PipeWire/PulseAudio), и при необходимости задать `AUDIO_INPUT_DEVICE_NAME`/`AUDIO_OUTPUT_DEVICE_NAME` | | `Audio input/output initialization failed` | проверить, что звук-сервер запущен (PipeWire/PulseAudio), и при необходимости задать `AUDIO_INPUT_DEVICE_NAME`/`AUDIO_OUTPUT_DEVICE_NAME` |

View File

@@ -13,7 +13,7 @@ import time
import pyaudio import pyaudio
import logging import logging
from datetime import datetime, timedelta 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 ( from deepgram import (
DeepgramClient, DeepgramClient,
DeepgramClientOptions, DeepgramClientOptions,
@@ -50,13 +50,13 @@ logging.getLogger("deepgram").setLevel(logging.WARNING)
# Базовые пороги для остановки STT # Базовые пороги для остановки STT
INITIAL_SILENCE_TIMEOUT_SECONDS = 5.0 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 MAX_ACTIVE_SPEECH_SECONDS = 300.0
_FAST_STOP_UTTERANCE_RE = re.compile( _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"(?:стоп|хватит|перестань|прекрати|замолчи|тихо|пауза)"
r"(?:\s+(?:пожалуйста|please))?$", r"(?:\s+(?:пожалуйста|please))?$",
flags=re.IGNORECASE, flags=re.IGNORECASE,

View File

@@ -6,7 +6,7 @@ Supports interruption via wake word detection using threading.
# Модуль синтеза речи (TTS - Text-to-Speech). # Модуль синтеза речи (TTS - Text-to-Speech).
# Использует нейросеть Silero TTS для качественной русской речи. # Использует нейросеть Silero TTS для качественной русской речи.
# Также поддерживает прерывание речи, если пользователь скажет "Alexandr". # Также поддерживает прерывание речи по wake word.
import re import re
import threading import threading

View File

@@ -1,10 +1,10 @@
""" """
Wake word detection module using Porcupine. 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 pvporcupine
import pyaudio import pyaudio
@@ -14,6 +14,7 @@ from ..core.config import (
PORCUPINE_ACCESS_KEY, PORCUPINE_ACCESS_KEY,
PORCUPINE_KEYWORD_PATH, PORCUPINE_KEYWORD_PATH,
PORCUPINE_SENSITIVITY, PORCUPINE_SENSITIVITY,
WAKE_WORD,
) )
from ..core.audio_manager import get_audio_manager from ..core.audio_manager import get_audio_manager
@@ -47,7 +48,7 @@ class WakeWordDetector:
self.pa = self._audio_manager.get_pyaudio() self.pa = self._audio_manager.get_pyaudio()
self._open_stream() self._open_stream()
print( print(
"🎤 Ожидание wake word 'Alexandr' " f"🎤 Ожидание wake word '{WAKE_WORD}' "
f"(sens={PORCUPINE_SENSITIVITY:.2f}, mic_rate={self._capture_sample_rate})..." 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: def wait_for_wakeword(self, timeout: float = None) -> bool:
""" """
Блокирующая функция: ждет, пока не будет услышана фраза "Alexandr" Блокирующая функция: ждет, пока не будет услышана wake word
или пока не истечет timeout. или пока не истечет timeout.
Args: Args:

View File

@@ -21,6 +21,8 @@ from .config import (
OPENROUTER_API_KEY, OPENROUTER_API_KEY,
OPENROUTER_API_URL, OPENROUTER_API_URL,
OPENROUTER_MODEL, OPENROUTER_MODEL,
WAKE_WORD,
WAKE_WORD_ALIASES,
ZAI_API_KEY, ZAI_API_KEY,
ZAI_API_URL, ZAI_API_URL,
ZAI_MODEL, ZAI_MODEL,
@@ -29,15 +31,18 @@ from .config import (
_HTTP = requests.Session() _HTTP = requests.Session()
# Системный промпт # Системный промпт
SYSTEM_PROMPT = """Ты — Александр, умный голосовой ассистент с человеческим поведением. _wake_word_aliases_text = ", ".join(WAKE_WORD_ALIASES)
SYSTEM_PROMPT = f"""Ты — умный голосовой ассистент с человеческим поведением.
Веди себя как живой человек: будь дружелюбным, естественным и немного эмоциональным, где это уместно. Веди себя как живой человек: будь дружелюбным, естественным и немного эмоциональным, где это уместно.
Твоя главная цель — помогать пользователю и поддерживать интересный диалог. Твоя главная цель — помогать пользователю и поддерживать интересный диалог.
Отвечай кратко и по существу, на русском языке. Отвечай кратко и по существу, на русском языке.
Избегай длинных списков, сложного форматирования и спецсимволов, так как твои ответы озвучиваются голосом. Избегай длинных списков, сложного форматирования и спецсимволов, так как твои ответы озвучиваются голосом.
Пиши в разговорном стиле, как при живом общении, но не забывай о вежливости и правильности твоих ответов. Пиши в разговорном стиле, как при живом общении, но не забывай о вежливости и правильности твоих ответов.
ВАЖНО: Не используй в ответах панибратские или сленговые приветствия и обращения, такие как "Эй", "Хэй", "Слушай" в начале фразы и подобные.""" ВАЖНО: Не используй в ответах панибратские или сленговые приветствия и обращения, такие как "Эй", "Хэй", "Слушай" в начале фразы и подобные.
Тебя активируют словом "{WAKE_WORD}". Никогда не произноси это слово и его варианты ({_wake_word_aliases_text}) ни в каком ответе.
Если пользователь спрашивает, как тебя зовут или как к тебе обращаться, отвечай нейтрально: "Я ваш голосовой ассистент"."""
SYSTEM_PROMPT += ( SYSTEM_PROMPT += (
'\nROLE_JSON: {"name":"Александр","role":"умный голосовой ассистент",' '\nROLE_JSON: {"name":"голосовой ассистент","role":"умный голосовой ассистент",'
'"language":"ru","style":["дружелюбный","естественный","краткий"],"format":"plain"}' '"language":"ru","style":["дружелюбный","естественный","краткий"],"format":"plain"}'
) )

View File

@@ -3,6 +3,7 @@
import re import re
import pymorphy3 import pymorphy3
from num2words import num2words from num2words import num2words
from .config import WAKE_WORD, WAKE_WORD_ALIASES
from .roman import roman_to_int from .roman import roman_to_int
morph = pymorphy3.MorphAnalyzer() morph = pymorphy3.MorphAnalyzer()
@@ -83,6 +84,10 @@ MONTHS_GENITIVE = [
# Время # Время
TIME_UNIT_LEMMAS = {"час", "минута", "секунда"} 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 = { _ORDINAL_SUFFIX_MAP = {
@@ -419,6 +424,10 @@ def clean_response(text: str, language: str = "ru") -> str:
flags=re.IGNORECASE | re.MULTILINE, flags=re.IGNORECASE | re.MULTILINE,
) )
# Запрет на произнесение wake word в любых ответах ассистента.
for pattern in WAKE_WORD_BLOCKED_PATTERNS:
text = pattern.sub("ассистент", text)
# Числа в слова # Числа в слова
if language == "ru": if language == "ru":
text = roman_numerals_to_words(text) text = roman_numerals_to_words(text)

View File

@@ -66,8 +66,17 @@ DEEPGRAM_API_KEY = os.getenv("DEEPGRAM_API_KEY")
# --- Настройки активации голосом (Porcupine) --- # --- Настройки активации голосом (Porcupine) ---
# Ключ доступа PicoVoice # Ключ доступа PicoVoice
PORCUPINE_ACCESS_KEY = os.getenv("PORCUPINE_ACCESS_KEY") 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 # Путь к файлу модели ключевого слова (.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). Выше = ловит легче, но больше ложных срабатываний. # Чувствительность wake word (0..1). Выше = ловит легче, но больше ложных срабатываний.
PORCUPINE_SENSITIVITY = float(os.getenv("PORCUPINE_SENSITIVITY", "0.8")) PORCUPINE_SENSITIVITY = float(os.getenv("PORCUPINE_SENSITIVITY", "0.8"))

View File

@@ -290,8 +290,8 @@ class SpotifyMusicController:
# Явные команды распознавания музыки (типа "угадай песню") # Явные команды распознавания музыки (типа "угадай песню")
recognize_patterns = [ recognize_patterns = [
r"((александр|александра|алесандр|alexander)\s+)?(угадай|распознай|определи)\s+(мелод|музык|песн|трек)", r"((waltron|voltron|волтрон|уолтрон|валтрон)\s+)?(угадай|распознай|определи)\s+(мелод|музык|песн|трек)",
r"((александр|александра|алесандр|alexander)\s+)?(что за|какая это)\s+(музык|песн|трек)", r"((waltron|voltron|волтрон|уолтрон|валтрон)\s+)?(что за|какая это)\s+(музык|песн|трек)",
r"(identify|recognize)\s+(song|music|track)", r"(identify|recognize)\s+(song|music|track)",
] ]
for pattern in recognize_patterns: for pattern in recognize_patterns:

View File

@@ -34,7 +34,7 @@ from .audio.wakeword import (
stop_monitoring as stop_wakeword_monitoring, stop_monitoring as stop_wakeword_monitoring,
) )
from .core.ai import ask_ai_stream, translate_text 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.cleaner import clean_response
from .core.commands import is_stop_command from .core.commands import is_stop_command
from .core.smalltalk import get_smalltalk_response from .core.smalltalk import get_smalltalk_response
@@ -87,10 +87,14 @@ _REPEAT_PHRASES = {
"скажи еще раз", "скажи еще раз",
"что ты сказал", "что ты сказал",
"повтори пожалуйста", "повтори пожалуйста",
"александр еще раз", "waltron еще раз",
"еще раз александр", "еще раз waltron",
"александр повтори", "waltron повтори",
"повтори александр", "повтори waltron",
"волтрон еще раз",
"еще раз волтрон",
"волтрон повтори",
"повтори волтрон",
} }
_WEATHER_TRIGGERS = ( _WEATHER_TRIGGERS = (
@@ -201,7 +205,7 @@ def main():
print("=" * 50) print("=" * 50)
print("🔊 УМНАЯ КОЛОНКА") print("🔊 УМНАЯ КОЛОНКА")
print("=" * 50) print("=" * 50)
print("Скажите 'Alexandr' для активации") print(f"Скажите '{WAKE_WORD}' для активации")
print("Нажмите Ctrl+C для выхода") print("Нажмите Ctrl+C для выхода")
print("=" * 50) print("=" * 50)
print() print()
@@ -248,7 +252,7 @@ def main():
# Режим диалога (без wake word) # Режим диалога (без wake word)
skip_wakeword = False skip_wakeword = False
followup_idle_timeout_seconds = 4.0 followup_idle_timeout_seconds = 3.7
# Контекст уточнения времени для таймера/будильника # Контекст уточнения времени для таймера/будильника
pending_time_target = None pending_time_target = None
@@ -347,7 +351,7 @@ def main():
skip_wakeword = False skip_wakeword = False
continue continue
print("_" * 50) print("_" * 50)
print("💤 Жду 'Alexandr'...") print(f"💤 Жду '{WAKE_WORD}'...")
skip_wakeword = False skip_wakeword = False
continue continue

1
assets/models/LICENSE.txt Executable file
View File

@@ -0,0 +1 @@
A copy of license terms is available at https://picovoice.ai/docs/terms-of-use/

Binary file not shown.