feat(wake-word): replace Vosk+Fuzzy with openWakeWord neural detector #1
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
slaid098/voice_assistant#1
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Контекст
Голосовой ассистент для незрячих пользователей (мама, Паркинсон, слепота). Текущая подсистема wake word (
nlu/wake_word.py) даёт критические ложные срабатывания — реагирует на посторонние слова и шум, даже приWAKE_THRESHOLD=90. Это сильно напрягает пользователя.Корневые причины (подтверждены исследованием кода):
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 бесполезно.wake_word.py:183):вики, wiki, вика, ники, мики, фрики, вику, веке, викки. Узкая грамматика с короткими частотными словами → Vosk принудительно выбирает ближайшее для любого звука → ложные срабатывания на шум.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уже в окружении. План:uv add vastai→vastai set api-key $VAST_API_KEYvastai search offers 'gpu_name=RTX_4090 num_gpus=1 verified=true rentable=true' -o 'dlperf_usd-'→ взять cheapest (~$0.6-0.7/ч)vastai create instance OFFER_ID --image pytorch/pytorch:2.4.0-cuda12.4-cudnn9-runtime --disk 40 --ssh --directvastai show instance INSTANCE_IDдоstatus: running(~5 мин)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 webrtcvadwget -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» = «вики»)python generate_clips.py --model VITS --text "wiki" --N 5000 --max_per_speaker 1 --output_dir generated_clipswget https://huggingface.co/datasets/davidscripka/openwakeword_features/resolve/main/openwakeword_features_ACAV100M_2000_hrs_16bit.npy(17.5GB — поэтому диск 40GB)wget https://huggingface.co/datasets/davidscripka/openwakeword_features/resolve/main/validation_set_features.npywget https://github.com/dscripka/openWakeWord/releases/download/v0.5.1/embedding_model.onnx -O openwakeword/resources/models/embedding_model.onnxи melspectrogram.onnxtarget_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_modelmy_custom_model/wiki.onnx(~2MB)scp wiki.onnx→models/openwakeword/wiki.onnxв репозиторииvastai destroy instance INSTANCE_ID(cleanup, чтобы не списывать деньги)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_detectordefault →"openwakeword"(вместо"vosk")wake_threshold(не нужен — openWakeWord имеет свой score 0-1, порог 0.5 захардкожен в детекторе)wake_aliases(не нужны — нет грамматики Vosk, нет fuzzy матчера)wake_word(для логирования/озвучивания),wake_timeout_msassistant.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— зависимости для inferencethefuzzв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,VoskWakeWordDetectorOpenWakeWordDetector— мок Model.predict, проверка score >= 0.5 → возвращает слово, < 0.5 → None,is_available()при отсутствии файла → Falseactive_wake_word_detector: openwakeword → OpenWakeWordDetector, недоступен → None.env.template:WAKE_THRESHOLD,WAKE_ALIASESWAKE_WORD_DETECTOR=openwakeword(default)AGENTS.md:models/openwakeword/wiki.onnx(~2MB)Контракты
WakeWordDetectorProtocol (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— контракт не меняется.SAMPLERATE=16000), openWakeWord требует тот же формат.CHUNK_MS=100(1600 samples) — нужно либо изменить на 80ms, либо feed по 1280 samples из буфера.Инварианты
WAKE_WORD.STT_PROVIDERостаётся независимым — Google STT для команд, openWakeWord для активации.Sound.READY_TO_LISTENперед записью команды) — сохраняются.WakeWordDetector— не меняется (новая реализация, тот же интерфейс).wiki.onnx— ассистент логирует error и не слушает wake word (НЕ возвращаемся к fuzzy/Vosk — они удалены).Граничные случаи
wiki.onnxотсутствует →is_available()= False →active_wake_word_detector()= None → ассистент логирует error, не реагирует. Нужна понятная ошибка для пользователя (озвучка черезspeak("Модель активации не загружена")).onnxruntimeне установлен → ImportError при импортеopenwakeword→is_available()= False.Влияние на связанные компоненты
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 использовал).models/openwakeword/wiki.onnx(2MB, не критично).Вне scope
Критерии приемки
models/openwakeword/wiki.onnxсуществует (~2MB), обучен на Vast.ai, закоммичен в репоvastai show instances= 0)OpenWakeWordDetectorреализуетWakeWordDetectorProtocolVoskWakeWordDetector,FuzzyWakeWordDetector,is_wake_word,_matches_wake_word,_check_wake_word_fuzzy,_build_grammar— удалены изwake_word.pyWAKE_THRESHOLD,WAKE_ALIASES— удалены изconfig.pyи.env.templateWAKE_WORD_DETECTORdefault ="openwakeword"вconfig.pyи.env.templateassistant.py— всегда стриминг черезdetect_chunk, нет fuzzy-веткиopenwakeword,onnxruntimeвpyproject.tomltest_wake_word.pyпереписаны,test_root_matchудалёнnlu/intent.pyадаптирован (безwake_aliases)ruff check src/ tests/— чистоmypy src/— чистоpytest— зелёный, coverage >= 75%AGENTS.mdобновлён