feat(wake-word): replace Vosk+Fuzzy with openWakeWord neural detector #1

Closed
opened 2026-08-09 19:17:28 +03:00 by slaid098 · 0 comments
Owner

Контекст

Голосовой ассистент для незрячих пользователей (мама, Паркинсон, слепота). Текущая подсистема wake word (nlu/wake_word.py) даёт критические ложные срабатывания — реагирует на посторонние слова и шум, даже при WAKE_THRESHOLD=90. Это сильно напрягает пользователя.

Корневые причины (подтверждены исследованием кода):

  1. Substring-проверки обходят порог (wake_word.py:78,82,87-88,200,203). WAKE_THRESHOLD управляет только fuzz.ratio, но три проверки срабатывают независимо: settings.wake_word in word, alias in word, root in word (root="вик" → «виктория» и т.п.). Повышение порога до 90 бесполезно.
  2. Грамматика Vosk = 9 слов (wake_word.py:183): вики, wiki, вика, ники, мики, фрики, вику, веке, викки. Узкая грамматика с короткими частотными словами → Vosk принудительно выбирает ближайшее для любого звука → ложные срабатывания на шум.
  3. WAKE_WORD_DETECTOR и STT_PROVIDER независимы (config.py:91,93). Дефолт vosk+google → wake word всегда через локальный Vosk даже при STT_PROVIDER=google. Пользователь думает «Google реагирует», а на самом деле Vosk.

Параметр WAKE_THRESHOLD в .env.template:12 называется «порог нечёткого распознавания слова активации», но фактически — только порог fuzz.ratio. Название вводит в заблуждение.

Задача

Полностью заменить Vosk wake word детектор И Fuzzy wake word детектор на openWakeWord — нейросетевой детектор на ONNX, реагирующий исключительно на одно слово «вики» (фонетически = английское «wiki»). Модель обучается на синтетических данных через piper-sample-generator + openWakeWord training toolkit, на GPU через Vast.ai.

Обучение модели (на Vast.ai, ~$0.30-0.50 из $3.12 balance)

Токен VAST_API_KEY уже в окружении. План:

  1. uv add vastaivastai set api-key $VAST_API_KEY
  2. vastai search offers 'gpu_name=RTX_4090 num_gpus=1 verified=true rentable=true' -o 'dlperf_usd-' → взять cheapest (~$0.6-0.7/ч)
  3. vastai create instance OFFER_ID --image pytorch/pytorch:2.4.0-cuda12.4-cudnn9-runtime --disk 40 --ssh --direct
  4. Poll vastai show instance INSTANCE_ID до status: running (~5 мин)
  5. SSH на инстанс:
    • git clone https://github.com/dscripka/openWakeWord && cd openWakeWord && pip install -e .
    • git clone https://github.com/rhasspy/piper-sample-generator && cd piper-sample-generator && pip install -r requirements.txt && pip install piper-phonemize webrtcvad
    • Скачать piper модель: wget -O models/en_US-libritts_r-medium.pt 'https://github.com/rhasspy/piper-sample-generator/releases/download/v2.0.0/en_US-libritts_r-medium.pt' (английская модель, фонетически «wiki» = «вики»)
    • Сгенерировать ~5000 клипов слова «wiki»: python generate_clips.py --model VITS --text "wiki" --N 5000 --max_per_speaker 1 --output_dir generated_clips
    • Скачать негативные фичи ACAV100M: wget https://huggingface.co/datasets/davidscripka/openwakeword_features/resolve/main/openwakeword_features_ACAV100M_2000_hrs_16bit.npy (17.5GB — поэтому диск 40GB)
    • Скачать validation set: wget https://huggingface.co/datasets/davidscripka/openwakeword_features/resolve/main/validation_set_features.npy
    • Скачать melspectrogram + embedding модели: wget https://github.com/dscripka/openWakeWord/releases/download/v0.5.1/embedding_model.onnx -O openwakeword/resources/models/embedding_model.onnx и melspectrogram.onnx
    • Настроить YAML конфиг (target_phrase: ["wiki"], n_samples: 5000, n_samples_val: 1000, steps: 10000)
    • Запустить: python openwakeword/train.py --training_config my_model.yaml --generate_clips--augment_clips--train_model
    • Результат: my_custom_model/wiki.onnx (~2MB)
  6. scp wiki.onnxmodels/openwakeword/wiki.onnx в репозитории
  7. ОБЯЗАТЕЛЬНО: vastai destroy instance INSTANCE_ID (cleanup, чтобы не списывать деньги)
  8. Закоммитить models/openwakeword/wiki.onnx (2MB) в репо

Изменения в коде

nlu/wake_word.py — полная переработка:

  • Новый класс OpenWakeWordDetector(WakeWordDetector): from openwakeword.model import Model, загружает models/openwakeword/wiki.onnx, detect_chunk(chunk) питает 16kHz 16-bit mono чанки (массивы по 80ms = 1280 samples), возвращает слово «вики» при score >= 0.5, иначе None. is_available() проверяет что модель загрузилась.
  • УДАЛИТЬ: FuzzyWakeWordDetector, VoskWakeWordDetector, is_wake_word(), _matches_wake_word(), _check_wake_word_fuzzy(), _build_grammar(). Вся substring/fuzzy логика уходит.
  • active_wake_word_detector(): при wake_word_detector == "openwakeword"OpenWakeWordDetector(), fallback на None (нет детектора) если модель не загрузилась.

config.py:

  • wake_word_detector default → "openwakeword" (вместо "vosk")
  • Убрать wake_threshold (не нужен — openWakeWord имеет свой score 0-1, порог 0.5 захардкожен в детекторе)
  • Убрать wake_aliases (не нужны — нет грамматики Vosk, нет fuzzy матчера)
  • Оставить wake_word (для логирования/озвучивания), wake_timeout_ms

assistant.py:

  • Упростить run_assistant_step: всегда стриминг через detector.detect_chunk (если детектор доступен). Убрать fuzzy-ветку (_listen_text_or_none для activation + is_wake_word проверку).
  • Если detector is None (модель не загрузилась) — логировать error и вернуть (нет fallback на fuzzy больше).

pyproject.toml:

  • uv add openwakeword onnxruntime — зависимости для inference
  • Проверить: используется ли thefuzz в nlu/intent.py — если да, оставить; если только в wake_word.py — убрать.

speech/model_loader.py:

  • Добавить резолвинг пути к models/openwakeword/wiki.onnx (аналогично piper/vosk путям)

Тесты tests/test_wake_word.py — переписать:

  • Убрать test_root_match (кодифицировал баг — substring root match)
  • Убрать все тесты is_wake_word, _matches_wake_word, _check_wake_word_fuzzy, _build_grammar, FuzzyWakeWordDetector, VoskWakeWordDetector
  • Новые тесты: OpenWakeWordDetector — мок Model.predict, проверка score >= 0.5 → возвращает слово, < 0.5 → None, is_available() при отсутствии файла → False
  • active_wake_word_detector: openwakeword → OpenWakeWordDetector, недоступен → None
  • False-positive кейсы: «виктория», «актёр», «привет» → None (мок отдаёт низкий score)

.env.template:

  • Убрать WAKE_THRESHOLD, WAKE_ALIASES
  • WAKE_WORD_DETECTOR=openwakeword (default)
  • Обновить комментарии

AGENTS.md:

  • Обновить секцию Wake word: «openWakeWord (neural, ONNX) — единственный детектор»
  • Убрать упоминание Vosk/Fuzzy для wake word
  • Обновить список моделей: models/openwakeword/wiki.onnx (~2MB)

Контракты

  • WakeWordDetector Protocol (wake_word.py:33-55) — НЕ меняется. OpenWakeWordDetector реализует тот же Protocol: name, detect_chunk(chunk: np.ndarray) -> str | None, is_available() -> bool.
  • assistant.py вызывает detector.detect_chunk(chunk) в стриминг-цикле через on_chunk колбэк record_user_speech — контракт не меняется.
  • Звук: 16kHz, 16-bit, mono — текущий формат (SAMPLERATE=16000), openWakeWord требует тот же формат.
  • Чанки: openWakeWord рекомендует 80ms фреймы (1280 samples при 16kHz). Текущий CHUNK_MS=100 (1600 samples) — нужно либо изменить на 80ms, либо feed по 1280 samples из буфера.

Инварианты

  • Слово активации остаётся «вики» (фонетически = «wiki») — не меняем WAKE_WORD.
  • STT_PROVIDER остаётся независимым — Google STT для команд, openWakeWord для активации.
  • Звуковые ярлыки (Sound.READY_TO_LISTEN перед записью команды) — сохраняются.
  • Протокол WakeWordDetector — не меняется (новая реализация, тот же интерфейс).
  • Fallback: при отсутствии модели wiki.onnx — ассистент логирует error и не слушает wake word (НЕ возвращаемся к fuzzy/Vosk — они удалены).

Граничные случаи

  • Модель wiki.onnx отсутствует → is_available() = False → active_wake_word_detector() = None → ассистент логирует error, не реагирует. Нужна понятная ошибка для пользователя (озвучка через speak("Модель активации не загружена")).
  • Score 0.4-0.6 (пограничный) → не срабатывает (порог 0.5). Это нормально — лучше miss чем false positive.
  • Шум/музыка/посторонние слова → openWakeWord отдаёт низкий score (< 0.5) → не срабатывает. Это основное преимущество над Vosk.
  • Слово «вики» сказано тихо/шёпотом → openWakeWord устойчив к этому (из docs: «models respond reasonably well to whispered wakewords»).
  • onnxruntime не установлен → ImportError при импорте openwakewordis_available() = False.
  • Чанк не кратен 80ms → нужно буферизировать и feed по 1280 samples.

Влияние на связанные компоненты

  • nlu/intent.py:61 (_strip_wake_word) — использует wake_aliases для удаления слова из команды. Без aliases нужно изменить: удалять wake_word (exact) из текста команды. Проверить и адаптировать.
  • speech/providers/stt/vosk_stt.py — Vosk STT провайдер остаётся (для STT_PROVIDER=vosk), но wake word больше не использует create_recognizer(grammar). Проверить что удаление wake word грамматики не ломает Vosk STT.
  • scripts/gen_phrases.py — не затрагивается.
  • pyproject.toml — добавляются openwakeword, onnxruntime; проверять thefuzz (возможно убрать если только wake_word использовал).
  • Release ZIP (PyInstaller) — включить models/openwakeword/wiki.onnx (2MB, не критично).

Вне scope

  • Авто-установка Vivaldi (отдельный future-issue)
  • Браузерные вкладки / uBlock (Issue 2)
  • openWakeWord verifier models (second-stage voice filter) — future enhancement
  • Поддержка других wake words (мульти-word) — сейчас только «вики»
  • Speex noise suppression (Linux only, пользователь на Windows)

Критерии приемки

  • models/openwakeword/wiki.onnx существует (~2MB), обучен на Vast.ai, закоммичен в репо
  • Vast.ai инстанс уничтожен после обучения (проверить vastai show instances = 0)
  • OpenWakeWordDetector реализует WakeWordDetector Protocol
  • VoskWakeWordDetector, FuzzyWakeWordDetector, is_wake_word, _matches_wake_word, _check_wake_word_fuzzy, _build_grammar — удалены из wake_word.py
  • WAKE_THRESHOLD, WAKE_ALIASES — удалены из config.py и .env.template
  • WAKE_WORD_DETECTOR default = "openwakeword" в config.py и .env.template
  • assistant.py — всегда стриминг через detect_chunk, нет fuzzy-ветки
  • openwakeword, onnxruntime в pyproject.toml
  • Тесты test_wake_word.py переписаны, test_root_match удалён
  • False-positive кейсы в тестах: «виктория», «актёр», «привет» → None
  • nlu/intent.py адаптирован (без wake_aliases)
  • ruff check src/ tests/ — чисто
  • mypy src/ — чисто
  • pytest — зелёный, coverage >= 75%
  • AGENTS.md обновлён
## Контекст Голосовой ассистент для незрячих пользователей (мама, Паркинсон, слепота). Текущая подсистема wake word (`nlu/wake_word.py`) даёт критические ложные срабатывания — реагирует на посторонние слова и шум, даже при `WAKE_THRESHOLD=90`. Это сильно напрягает пользователя. Корневые причины (подтверждены исследованием кода): 1. **Substring-проверки обходят порог** (`wake_word.py:78,82,87-88,200,203`). `WAKE_THRESHOLD` управляет только `fuzz.ratio`, но три проверки срабатывают независимо: `settings.wake_word in word`, `alias in word`, `root in word` (root="вик" → «виктория» и т.п.). Повышение порога до 90 бесполезно. 2. **Грамматика Vosk = 9 слов** (`wake_word.py:183`): `вики, wiki, вика, ники, мики, фрики, вику, веке, викки`. Узкая грамматика с короткими частотными словами → Vosk принудительно выбирает ближайшее для любого звука → ложные срабатывания на шум. 3. **`WAKE_WORD_DETECTOR` и `STT_PROVIDER` независимы** (`config.py:91,93`). Дефолт `vosk`+`google` → wake word всегда через локальный Vosk даже при `STT_PROVIDER=google`. Пользователь думает «Google реагирует», а на самом деле Vosk. Параметр `WAKE_THRESHOLD` в `.env.template:12` называется «порог нечёткого распознавания слова активации», но фактически — только порог `fuzz.ratio`. Название вводит в заблуждение. ## Задача Полностью заменить Vosk wake word детектор И Fuzzy wake word детектор на **openWakeWord** — нейросетевой детектор на ONNX, реагирующий исключительно на одно слово «вики» (фонетически = английское «wiki»). Модель обучается на синтетических данных через piper-sample-generator + openWakeWord training toolkit, на GPU через Vast.ai. ### Обучение модели (на Vast.ai, ~$0.30-0.50 из $3.12 balance) Токен `VAST_API_KEY` уже в окружении. План: 1. `uv add vastai` → `vastai set api-key $VAST_API_KEY` 2. `vastai search offers 'gpu_name=RTX_4090 num_gpus=1 verified=true rentable=true' -o 'dlperf_usd-'` → взять cheapest (~$0.6-0.7/ч) 3. `vastai create instance OFFER_ID --image pytorch/pytorch:2.4.0-cuda12.4-cudnn9-runtime --disk 40 --ssh --direct` 4. Poll `vastai show instance INSTANCE_ID` до `status: running` (~5 мин) 5. SSH на инстанс: - `git clone https://github.com/dscripka/openWakeWord && cd openWakeWord && pip install -e .` - `git clone https://github.com/rhasspy/piper-sample-generator && cd piper-sample-generator && pip install -r requirements.txt && pip install piper-phonemize webrtcvad` - Скачать piper модель: `wget -O models/en_US-libritts_r-medium.pt 'https://github.com/rhasspy/piper-sample-generator/releases/download/v2.0.0/en_US-libritts_r-medium.pt'` (английская модель, фонетически «wiki» = «вики») - Сгенерировать ~5000 клипов слова «wiki»: `python generate_clips.py --model VITS --text "wiki" --N 5000 --max_per_speaker 1 --output_dir generated_clips` - Скачать негативные фичи ACAV100M: `wget https://huggingface.co/datasets/davidscripka/openwakeword_features/resolve/main/openwakeword_features_ACAV100M_2000_hrs_16bit.npy` (17.5GB — поэтому диск 40GB) - Скачать validation set: `wget https://huggingface.co/datasets/davidscripka/openwakeword_features/resolve/main/validation_set_features.npy` - Скачать melspectrogram + embedding модели: `wget https://github.com/dscripka/openWakeWord/releases/download/v0.5.1/embedding_model.onnx -O openwakeword/resources/models/embedding_model.onnx` и melspectrogram.onnx - Настроить YAML конфиг (`target_phrase: ["wiki"]`, `n_samples: 5000`, `n_samples_val: 1000`, `steps: 10000`) - Запустить: `python openwakeword/train.py --training_config my_model.yaml --generate_clips` → `--augment_clips` → `--train_model` - Результат: `my_custom_model/wiki.onnx` (~2MB) 6. `scp wiki.onnx` → `models/openwakeword/wiki.onnx` в репозитории 7. **ОБЯЗАТЕЛЬНО**: `vastai destroy instance INSTANCE_ID` (cleanup, чтобы не списывать деньги) 8. Закоммитить `models/openwakeword/wiki.onnx` (2MB) в репо ### Изменения в коде **`nlu/wake_word.py`** — полная переработка: - Новый класс `OpenWakeWordDetector(WakeWordDetector)`: `from openwakeword.model import Model`, загружает `models/openwakeword/wiki.onnx`, `detect_chunk(chunk)` питает 16kHz 16-bit mono чанки (массивы по 80ms = 1280 samples), возвращает слово «вики» при score >= 0.5, иначе None. `is_available()` проверяет что модель загрузилась. - **УДАЛИТЬ**: `FuzzyWakeWordDetector`, `VoskWakeWordDetector`, `is_wake_word()`, `_matches_wake_word()`, `_check_wake_word_fuzzy()`, `_build_grammar()`. Вся substring/fuzzy логика уходит. - `active_wake_word_detector()`: при `wake_word_detector == "openwakeword"` → `OpenWakeWordDetector()`, fallback на None (нет детектора) если модель не загрузилась. **`config.py`**: - `wake_word_detector` default → `"openwakeword"` (вместо `"vosk"`) - **Убрать** `wake_threshold` (не нужен — openWakeWord имеет свой score 0-1, порог 0.5 захардкожен в детекторе) - **Убрать** `wake_aliases` (не нужны — нет грамматики Vosk, нет fuzzy матчера) - Оставить `wake_word` (для логирования/озвучивания), `wake_timeout_ms` **`assistant.py`**: - Упростить `run_assistant_step`: всегда стриминг через `detector.detect_chunk` (если детектор доступен). Убрать fuzzy-ветку (`_listen_text_or_none` для activation + `is_wake_word` проверку). - Если `detector is None` (модель не загрузилась) — логировать error и вернуть (нет fallback на fuzzy больше). **`pyproject.toml`**: - `uv add openwakeword onnxruntime` — зависимости для inference - Проверить: используется ли `thefuzz` в `nlu/intent.py` — если да, оставить; если только в wake_word.py — убрать. **`speech/model_loader.py`**: - Добавить резолвинг пути к `models/openwakeword/wiki.onnx` (аналогично piper/vosk путям) **Тесты `tests/test_wake_word.py`** — переписать: - Убрать `test_root_match` (кодифицировал баг — substring root match) - Убрать все тесты `is_wake_word`, `_matches_wake_word`, `_check_wake_word_fuzzy`, `_build_grammar`, `FuzzyWakeWordDetector`, `VoskWakeWordDetector` - Новые тесты: `OpenWakeWordDetector` — мок Model.predict, проверка score >= 0.5 → возвращает слово, < 0.5 → None, `is_available()` при отсутствии файла → False - `active_wake_word_detector`: openwakeword → OpenWakeWordDetector, недоступен → None - False-positive кейсы: «виктория», «актёр», «привет» → None (мок отдаёт низкий score) **`.env.template`**: - Убрать `WAKE_THRESHOLD`, `WAKE_ALIASES` - `WAKE_WORD_DETECTOR=openwakeword` (default) - Обновить комментарии **`AGENTS.md`**: - Обновить секцию Wake word: «openWakeWord (neural, ONNX) — единственный детектор» - Убрать упоминание Vosk/Fuzzy для wake word - Обновить список моделей: `models/openwakeword/wiki.onnx` (~2MB) ## Контракты - `WakeWordDetector` Protocol (`wake_word.py:33-55`) — НЕ меняется. `OpenWakeWordDetector` реализует тот же Protocol: `name`, `detect_chunk(chunk: np.ndarray) -> str | None`, `is_available() -> bool`. - `assistant.py` вызывает `detector.detect_chunk(chunk)` в стриминг-цикле через `on_chunk` колбэк `record_user_speech` — контракт не меняется. - Звук: 16kHz, 16-bit, mono — текущий формат (`SAMPLERATE=16000`), openWakeWord требует тот же формат. - Чанки: openWakeWord рекомендует 80ms фреймы (1280 samples при 16kHz). Текущий `CHUNK_MS=100` (1600 samples) — нужно либо изменить на 80ms, либо feed по 1280 samples из буфера. ## Инварианты - Слово активации остаётся «вики» (фонетически = «wiki») — не меняем `WAKE_WORD`. - `STT_PROVIDER` остаётся независимым — Google STT для команд, openWakeWord для активации. - Звуковые ярлыки (`Sound.READY_TO_LISTEN` перед записью команды) — сохраняются. - Протокол `WakeWordDetector` — не меняется (новая реализация, тот же интерфейс). - Fallback: при отсутствии модели `wiki.onnx` — ассистент логирует error и не слушает wake word (НЕ возвращаемся к fuzzy/Vosk — они удалены). ## Граничные случаи - Модель `wiki.onnx` отсутствует → `is_available()` = False → `active_wake_word_detector()` = None → ассистент логирует error, не реагирует. Нужна понятная ошибка для пользователя (озвучка через `speak("Модель активации не загружена")`). - Score 0.4-0.6 (пограничный) → не срабатывает (порог 0.5). Это нормально — лучше miss чем false positive. - Шум/музыка/посторонние слова → openWakeWord отдаёт низкий score (< 0.5) → не срабатывает. Это основное преимущество над Vosk. - Слово «вики» сказано тихо/шёпотом → openWakeWord устойчив к этому (из docs: «models respond reasonably well to whispered wakewords»). - `onnxruntime` не установлен → ImportError при импорте `openwakeword` → `is_available()` = False. - Чанк не кратен 80ms → нужно буферизировать и feed по 1280 samples. ## Влияние на связанные компоненты - `nlu/intent.py:61` (`_strip_wake_word`) — использует `wake_aliases` для удаления слова из команды. Без aliases нужно изменить: удалять `wake_word` (exact) из текста команды. Проверить и адаптировать. - `speech/providers/stt/vosk_stt.py` — Vosk STT провайдер остаётся (для `STT_PROVIDER=vosk`), но wake word больше не использует `create_recognizer(grammar)`. Проверить что удаление wake word грамматики не ломает Vosk STT. - `scripts/gen_phrases.py` — не затрагивается. - `pyproject.toml` — добавляются `openwakeword`, `onnxruntime`; проверять `thefuzz` (возможно убрать если только wake_word использовал). - Release ZIP (PyInstaller) — включить `models/openwakeword/wiki.onnx` (2MB, не критично). ## Вне scope - Авто-установка Vivaldi (отдельный future-issue) - Браузерные вкладки / uBlock (Issue 2) - openWakeWord verifier models (second-stage voice filter) — future enhancement - Поддержка других wake words (мульти-word) — сейчас только «вики» - Speex noise suppression (Linux only, пользователь на Windows) ## Критерии приемки - [ ] `models/openwakeword/wiki.onnx` существует (~2MB), обучен на Vast.ai, закоммичен в репо - [ ] Vast.ai инстанс уничтожен после обучения (проверить `vastai show instances` = 0) - [ ] `OpenWakeWordDetector` реализует `WakeWordDetector` Protocol - [ ] `VoskWakeWordDetector`, `FuzzyWakeWordDetector`, `is_wake_word`, `_matches_wake_word`, `_check_wake_word_fuzzy`, `_build_grammar` — удалены из `wake_word.py` - [ ] `WAKE_THRESHOLD`, `WAKE_ALIASES` — удалены из `config.py` и `.env.template` - [ ] `WAKE_WORD_DETECTOR` default = `"openwakeword"` в `config.py` и `.env.template` - [ ] `assistant.py` — всегда стриминг через `detect_chunk`, нет fuzzy-ветки - [ ] `openwakeword`, `onnxruntime` в `pyproject.toml` - [ ] Тесты `test_wake_word.py` переписаны, `test_root_match` удалён - [ ] False-positive кейсы в тестах: «виктория», «актёр», «привет» → None - [ ] `nlu/intent.py` адаптирован (без `wake_aliases`) - [ ] `ruff check src/ tests/` — чисто - [ ] `mypy src/` — чисто - [ ] `pytest` — зелёный, coverage >= 75% - [ ] `AGENTS.md` обновлён
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
slaid098/voice_assistant#1
No description provided.