Allow running a different pipeline depending on wake word
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user