feat: switch wake word to waltron
This commit is contained in:
12
README.md
12
README.md
@@ -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` |
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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"}'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
20
app/main.py
20
app/main.py
@@ -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
1
assets/models/LICENSE.txt
Executable file
@@ -0,0 +1 @@
|
|||||||
|
A copy of license terms is available at https://picovoice.ai/docs/terms-of-use/
|
||||||
BIN
assets/models/Waltron_en_linux_v4_0_0.ppn
Normal file
BIN
assets/models/Waltron_en_linux_v4_0_0.ppn
Normal file
Binary file not shown.
Reference in New Issue
Block a user