Allow running a different pipeline depending on wake word

This commit is contained in:
Michael Hansen
2024-01-30 15:39:50 -06:00
parent 8850100ca6
commit 1bc3471637
5 changed files with 79 additions and 10 deletions
+7 -2
View File
@@ -24,6 +24,7 @@ from .settings import (
SndSettings,
VadSettings,
WakeSettings,
WakeWordAndPipeline,
)
from .utils import (
get_mac_address,
@@ -114,7 +115,9 @@ async def main() -> None:
"--wake-word-name",
action="append",
default=[],
help="Name of wake word to listen for (requires --wake-uri)",
nargs="+",
metavar=("name", "pipeline"),
help="Name of wake word to listen for and optional pipeline name to run (requires --wake-uri)",
)
parser.add_argument("--wake-command", help="Program to run for wake word detection")
parser.add_argument(
@@ -329,7 +332,9 @@ async def main() -> None:
wake=WakeSettings(
uri=args.wake_uri,
command=split_command(args.wake_command),
names=args.wake_word_name,
names=[
WakeWordAndPipeline(*wake_name) for wake_name in args.wake_word_name
],
refractory_seconds=args.wake_refractory_seconds
if args.wake_refractory_seconds > 0
else None,
+37 -7
View File
@@ -6,11 +6,11 @@ import time
import wave
from enum import Enum, auto
from pathlib import Path
from typing import Callable, Dict, Final, Optional, Set, Union
from typing import Callable, Dict, Final, List, Optional, Set, Union
from pyring_buffer import RingBuffer
from wyoming.asr import Transcript
from wyoming.audio import AudioChunk, AudioStart, AudioStop
from wyoming.audio import AudioChunk, AudioFormat, AudioStart, AudioStop
from wyoming.client import AsyncClient
from wyoming.error import Error
from wyoming.event import Event, async_write_event
@@ -31,7 +31,13 @@ from wyoming.vad import VoiceStarted, VoiceStopped
from wyoming.wake import Detect, Detection, WakeProcessAsyncClient
from .settings import SatelliteSettings
from .utils import DebugAudioWriter, multiply_volume, run_event_command, wav_to_events
from .utils import (
DebugAudioWriter,
multiply_volume,
normalize_wake_word,
run_event_command,
wav_to_events,
)
from .vad import SileroVad
from .webrtc import WebRtcAudio
@@ -285,7 +291,7 @@ class SatelliteBase:
if not AudioChunk.is_type(event.type):
await self.forward_event(event)
async def _send_run_pipeline(self) -> None:
async def _send_run_pipeline(self, pipeline_name: Optional[str] = None) -> None:
"""Sends a RunPipeline event with the correct stages."""
if self.settings.wake.enabled:
# Local wake word detection
@@ -304,8 +310,17 @@ class SatelliteBase:
end_stage = PipelineStage.HANDLE
run_pipeline = RunPipeline(
start_stage=start_stage, end_stage=end_stage, restart_on_end=restart_on_end
start_stage=start_stage,
end_stage=end_stage,
name=pipeline_name,
restart_on_end=restart_on_end,
snd_format=AudioFormat(
rate=self.settings.snd.rate,
width=self.settings.snd.width,
channels=self.settings.snd.channels,
),
).event()
_LOGGER.debug(run_pipeline)
await self.event_to_server(run_pipeline)
await self.forward_event(run_pipeline)
@@ -734,7 +749,11 @@ class SatelliteBase:
async def _send_wake_detect(self) -> None:
"""Inform wake word service of which wake words to detect."""
await self.event_to_wake(Detect(names=self.settings.wake.names).event())
wake_names: Optional[List[str]] = None
if self.settings.wake.names:
wake_names = [w.name for w in self.settings.wake.names]
await self.event_to_wake(Detect(names=wake_names).event())
await self.trigger_detect()
# -------------------------------------------------------------------------
@@ -1225,6 +1244,8 @@ class WakeStreamingSatellite(SatelliteBase):
if self.stt_audio_writer is not None:
self.stt_audio_writer.start(timestamp=self._debug_recording_timestamp)
_LOGGER.debug(detection)
self.is_streaming = True
_LOGGER.debug("Streaming audio")
@@ -1238,7 +1259,16 @@ class WakeStreamingSatellite(SatelliteBase):
# No refractory period
self.refractory_timestamp.pop(detection.name, None)
await self._send_run_pipeline()
# Match detected wake word name with pipeline name
pipeline_name: Optional[str] = None
if self.settings.wake.names:
detection_name = normalize_wake_word(detection.name)
for wake_name in self.settings.wake.names:
if normalize_wake_word(wake_name.name) == detection_name:
pipeline_name = wake_name.pipeline
break
await self._send_run_pipeline(pipeline_name=pipeline_name)
await self.forward_event(event) # forward to event service
await self.trigger_detection(Detection.from_event(event))
await self.trigger_streaming_start()
+9 -1
View File
@@ -100,11 +100,19 @@ class SndSettings(ServiceSettings):
return self.enabled and (self.volume_multiplier != 1.0)
@dataclass(frozen=True)
class WakeWordAndPipeline:
"""Wake word name + optional pipeline name."""
name: str
pipeline: Optional[str] = None
@dataclass(frozen=True)
class WakeSettings(ServiceSettings):
"""Wake word service settings."""
names: Optional[List[str]] = None
names: Optional[List[WakeWordAndPipeline]] = None
"""List of wake word names to listen for."""
rate: int = 16000
+2
View File
@@ -10,6 +10,7 @@ from .misc import (
get_mac_address,
needs_silero,
needs_webrtc,
normalize_wake_word,
run_event_command,
split_command,
)
@@ -22,6 +23,7 @@ __all__ = [
"multiply_volume",
"needs_silero",
"needs_webrtc",
"normalize_wake_word",
"run_event_command",
"split_command",
"wav_to_events",
+24
View File
@@ -2,8 +2,11 @@
import argparse
import asyncio
import logging
import re
import shlex
import unicodedata
import uuid
from functools import lru_cache
from typing import List, Optional
_LOGGER = logging.getLogger()
@@ -56,3 +59,24 @@ def split_command(command: Optional[str]) -> Optional[List[str]]:
return None
return shlex.split(command)
@lru_cache
def normalize_wake_word(wake_word: str) -> str:
"""Normalizes a wake word name for comparison."""
# Lower case
wake_word = wake_word.strip().casefold()
# Remove version numbers like v1.0
wake_word = re.sub(r"v[0-9]+(\.[0-9])+", "", wake_word)
# Replace anything besides letters/numbers with whitespace
wake_word = "".join(
c if unicodedata.category(c) in ("Ll", "Nd") else " " for c in wake_word
)
# Normalize whitespace
wake_word = " ".join(wake_word.strip().split())
return wake_word