Support multiple wake words
This commit is contained in:
@@ -6,7 +6,7 @@ import logging
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from queue import Queue
|
||||
from typing import Dict
|
||||
from typing import Dict, List
|
||||
|
||||
import sounddevice as sd
|
||||
|
||||
@@ -109,16 +109,27 @@ async def main() -> None:
|
||||
_LOGGER.debug("libtensorflowlite_c path: %s", libtensorflowlite_c_path)
|
||||
|
||||
# Load wake/stop models
|
||||
wake_models: Dict[str, MicroWakeWord] = {}
|
||||
wake_config_path = wake_word_dir / f"{args.wake_model}.json"
|
||||
if preferences.active_wake_words:
|
||||
wake_word_id = preferences.active_wake_words[0]
|
||||
maybe_wake_config_path = wake_word_dir / f"{wake_word_id}.json"
|
||||
if maybe_wake_config_path.exists():
|
||||
# Override with last set
|
||||
wake_config_path = maybe_wake_config_path
|
||||
# Load preferred models
|
||||
for wake_word_id in preferences.active_wake_words:
|
||||
wake_config_path = wake_word_dir / f"{wake_word_id}.json"
|
||||
if not wake_config_path.exists():
|
||||
continue
|
||||
|
||||
_LOGGER.debug("Loading wake model: %s", wake_config_path)
|
||||
wake_model = MicroWakeWord.from_config(wake_config_path, libtensorflowlite_c_path)
|
||||
_LOGGER.debug("Loading wake model: %s", wake_config_path)
|
||||
wake_models[wake_word_id] = MicroWakeWord.from_config(
|
||||
wake_config_path, libtensorflowlite_c_path
|
||||
)
|
||||
|
||||
if not wake_models:
|
||||
# Load default model
|
||||
_LOGGER.debug("Loading wake model: %s", wake_config_path)
|
||||
wake_word_id = wake_config_path.stem
|
||||
wake_models[wake_word_id] = MicroWakeWord.from_config(
|
||||
wake_config_path, libtensorflowlite_c_path
|
||||
)
|
||||
|
||||
stop_config_path = wake_word_dir / f"{args.stop_model}.json"
|
||||
_LOGGER.debug("Loading stop model: %s", stop_config_path)
|
||||
@@ -130,7 +141,7 @@ async def main() -> None:
|
||||
audio_queue=Queue(),
|
||||
entities=[],
|
||||
available_wake_words=available_wake_words,
|
||||
wake_word=wake_model,
|
||||
wake_words=wake_models,
|
||||
stop_word=stop_model,
|
||||
music_player=MpvMediaPlayer(device=args.audio_output_device),
|
||||
tts_player=MpvMediaPlayer(device=args.audio_output_device),
|
||||
@@ -138,6 +149,7 @@ async def main() -> None:
|
||||
timer_finished_sound=args.timer_finished_sound,
|
||||
preferences=preferences,
|
||||
preferences_path=preferences_path,
|
||||
libtensorflowlite_c_path=libtensorflowlite_c_path,
|
||||
)
|
||||
|
||||
process_audio_thread = threading.Thread(
|
||||
@@ -179,6 +191,9 @@ async def main() -> None:
|
||||
|
||||
|
||||
def process_audio(state: ServerState):
|
||||
"""Process audio chunks from the microphone."""
|
||||
|
||||
wake_words: List[MicroWakeWord] = []
|
||||
|
||||
try:
|
||||
while True:
|
||||
@@ -189,13 +204,20 @@ def process_audio(state: ServerState):
|
||||
if state.satellite is None:
|
||||
continue
|
||||
|
||||
if (not wake_words) or (state.wake_words_changed and state.wake_words):
|
||||
# Update list of wake word models to process
|
||||
state.wake_words_changed = False
|
||||
wake_words = list(state.wake_words.values())
|
||||
|
||||
try:
|
||||
state.satellite.handle_audio(audio_chunk)
|
||||
|
||||
if state.wake_word.is_active and state.wake_word.process_streaming(
|
||||
audio_chunk
|
||||
):
|
||||
state.satellite.wakeup()
|
||||
for wake_word in wake_words:
|
||||
if not wake_word.is_active:
|
||||
continue
|
||||
|
||||
if wake_word.process_streaming(audio_chunk):
|
||||
state.satellite.wakeup(wake_word)
|
||||
|
||||
if state.stop_word.is_active and state.stop_word.process_streaming(
|
||||
audio_chunk
|
||||
|
||||
@@ -36,7 +36,7 @@ class ServerState:
|
||||
audio_queue: "Queue[Optional[bytes]]"
|
||||
entities: "List[ESPHomeEntity]"
|
||||
available_wake_words: "Dict[str, AvailableWakeWord]"
|
||||
wake_word: "MicroWakeWord"
|
||||
wake_words: "Dict[str, MicroWakeWord]"
|
||||
stop_word: "MicroWakeWord"
|
||||
music_player: "MpvMediaPlayer"
|
||||
tts_player: "MpvMediaPlayer"
|
||||
@@ -44,8 +44,10 @@ class ServerState:
|
||||
timer_finished_sound: str
|
||||
preferences: Preferences
|
||||
preferences_path: Path
|
||||
libtensorflowlite_c_path: Path
|
||||
media_player_entity: "Optional[MediaPlayerEntity]" = None
|
||||
satellite: "Optional[VoiceSatelliteProtocol]" = None
|
||||
wake_words_changed: bool = False
|
||||
|
||||
def save_preferences(self) -> None:
|
||||
"""Save preferences as JSON."""
|
||||
|
||||
@@ -175,36 +175,42 @@ class VoiceSatelliteProtocol(APIServer):
|
||||
)
|
||||
for ww in self.state.available_wake_words.values()
|
||||
],
|
||||
active_wake_words=[self.state.wake_word.id],
|
||||
max_active_wake_words=1,
|
||||
active_wake_words=list(self.state.wake_words.keys()),
|
||||
max_active_wake_words=2,
|
||||
)
|
||||
_LOGGER.info("Connected to Home Assistant")
|
||||
elif isinstance(msg, VoiceAssistantSetConfiguration):
|
||||
# TODO: support multiple wake words
|
||||
# Change active wake words
|
||||
active_wake_words: Set[str] = set()
|
||||
|
||||
for wake_word_id in msg.active_wake_words:
|
||||
if wake_word_id == self.state.wake_word.id:
|
||||
if wake_word_id in self.state.wake_words:
|
||||
# Already active
|
||||
active_wake_words.add(wake_word_id)
|
||||
break
|
||||
continue
|
||||
|
||||
model_info = self.state.available_wake_words.get(wake_word_id)
|
||||
if not model_info:
|
||||
continue
|
||||
|
||||
_LOGGER.debug("Loading wake word: %s", model_info.config_path)
|
||||
self.state.wake_word = MicroWakeWord.from_config(
|
||||
self.state.wake_words[wake_word_id] = MicroWakeWord.from_config(
|
||||
model_info.config_path,
|
||||
self.state.wake_word.libtensorflowlite_c_path,
|
||||
self.state.libtensorflowlite_c_path,
|
||||
)
|
||||
|
||||
_LOGGER.info("Wake word set: %s", self.state.wake_word.wake_word)
|
||||
_LOGGER.info("Wake word set: %s", wake_word_id)
|
||||
active_wake_words.add(wake_word_id)
|
||||
break
|
||||
|
||||
for wake_word in self.state.wake_words.values():
|
||||
wake_word.is_active = wake_word.id in active_wake_words
|
||||
|
||||
_LOGGER.debug("Active wake words: %s", active_wake_words)
|
||||
|
||||
self.state.preferences.active_wake_words = list(active_wake_words)
|
||||
self.state.save_preferences()
|
||||
self.state.wake_words_changed = True
|
||||
|
||||
def handle_audio(self, audio_chunk: bytes) -> None:
|
||||
|
||||
@@ -213,7 +219,7 @@ class VoiceSatelliteProtocol(APIServer):
|
||||
|
||||
self.send_messages([VoiceAssistantAudio(data=audio_chunk)])
|
||||
|
||||
def wakeup(self) -> None:
|
||||
def wakeup(self, wake_word: MicroWakeWord) -> None:
|
||||
if self._timer_finished:
|
||||
# Stop timer instead
|
||||
self._timer_finished = False
|
||||
@@ -221,7 +227,7 @@ class VoiceSatelliteProtocol(APIServer):
|
||||
_LOGGER.debug("Stopping timer finished sound")
|
||||
return
|
||||
|
||||
wake_word_phrase = self.state.wake_word.wake_word
|
||||
wake_word_phrase = wake_word.wake_word
|
||||
_LOGGER.debug("Detected wake word: %s", wake_word_phrase)
|
||||
self.send_messages(
|
||||
[VoiceAssistantRequest(start=True, wake_word_phrase=wake_word_phrase)]
|
||||
|
||||
Reference in New Issue
Block a user