Allow running a different pipeline depending on wake word
This commit is contained in:
@@ -24,6 +24,7 @@ from .settings import (
|
|||||||
SndSettings,
|
SndSettings,
|
||||||
VadSettings,
|
VadSettings,
|
||||||
WakeSettings,
|
WakeSettings,
|
||||||
|
WakeWordAndPipeline,
|
||||||
)
|
)
|
||||||
from .utils import (
|
from .utils import (
|
||||||
get_mac_address,
|
get_mac_address,
|
||||||
@@ -114,7 +115,9 @@ async def main() -> None:
|
|||||||
"--wake-word-name",
|
"--wake-word-name",
|
||||||
action="append",
|
action="append",
|
||||||
default=[],
|
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("--wake-command", help="Program to run for wake word detection")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@@ -329,7 +332,9 @@ async def main() -> None:
|
|||||||
wake=WakeSettings(
|
wake=WakeSettings(
|
||||||
uri=args.wake_uri,
|
uri=args.wake_uri,
|
||||||
command=split_command(args.wake_command),
|
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
|
refractory_seconds=args.wake_refractory_seconds
|
||||||
if args.wake_refractory_seconds > 0
|
if args.wake_refractory_seconds > 0
|
||||||
else None,
|
else None,
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import time
|
|||||||
import wave
|
import wave
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from pathlib import Path
|
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 pyring_buffer import RingBuffer
|
||||||
from wyoming.asr import Transcript
|
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.client import AsyncClient
|
||||||
from wyoming.error import Error
|
from wyoming.error import Error
|
||||||
from wyoming.event import Event, async_write_event
|
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 wyoming.wake import Detect, Detection, WakeProcessAsyncClient
|
||||||
|
|
||||||
from .settings import SatelliteSettings
|
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 .vad import SileroVad
|
||||||
from .webrtc import WebRtcAudio
|
from .webrtc import WebRtcAudio
|
||||||
|
|
||||||
@@ -285,7 +291,7 @@ class SatelliteBase:
|
|||||||
if not AudioChunk.is_type(event.type):
|
if not AudioChunk.is_type(event.type):
|
||||||
await self.forward_event(event)
|
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."""
|
"""Sends a RunPipeline event with the correct stages."""
|
||||||
if self.settings.wake.enabled:
|
if self.settings.wake.enabled:
|
||||||
# Local wake word detection
|
# Local wake word detection
|
||||||
@@ -304,8 +310,17 @@ class SatelliteBase:
|
|||||||
end_stage = PipelineStage.HANDLE
|
end_stage = PipelineStage.HANDLE
|
||||||
|
|
||||||
run_pipeline = RunPipeline(
|
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()
|
).event()
|
||||||
|
_LOGGER.debug(run_pipeline)
|
||||||
await self.event_to_server(run_pipeline)
|
await self.event_to_server(run_pipeline)
|
||||||
await self.forward_event(run_pipeline)
|
await self.forward_event(run_pipeline)
|
||||||
|
|
||||||
@@ -734,7 +749,11 @@ class SatelliteBase:
|
|||||||
|
|
||||||
async def _send_wake_detect(self) -> None:
|
async def _send_wake_detect(self) -> None:
|
||||||
"""Inform wake word service of which wake words to detect."""
|
"""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()
|
await self.trigger_detect()
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
@@ -1225,6 +1244,8 @@ class WakeStreamingSatellite(SatelliteBase):
|
|||||||
if self.stt_audio_writer is not None:
|
if self.stt_audio_writer is not None:
|
||||||
self.stt_audio_writer.start(timestamp=self._debug_recording_timestamp)
|
self.stt_audio_writer.start(timestamp=self._debug_recording_timestamp)
|
||||||
|
|
||||||
|
_LOGGER.debug(detection)
|
||||||
|
|
||||||
self.is_streaming = True
|
self.is_streaming = True
|
||||||
_LOGGER.debug("Streaming audio")
|
_LOGGER.debug("Streaming audio")
|
||||||
|
|
||||||
@@ -1238,7 +1259,16 @@ class WakeStreamingSatellite(SatelliteBase):
|
|||||||
# No refractory period
|
# No refractory period
|
||||||
self.refractory_timestamp.pop(detection.name, None)
|
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.forward_event(event) # forward to event service
|
||||||
await self.trigger_detection(Detection.from_event(event))
|
await self.trigger_detection(Detection.from_event(event))
|
||||||
await self.trigger_streaming_start()
|
await self.trigger_streaming_start()
|
||||||
|
|||||||
@@ -100,11 +100,19 @@ class SndSettings(ServiceSettings):
|
|||||||
return self.enabled and (self.volume_multiplier != 1.0)
|
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)
|
@dataclass(frozen=True)
|
||||||
class WakeSettings(ServiceSettings):
|
class WakeSettings(ServiceSettings):
|
||||||
"""Wake word service settings."""
|
"""Wake word service settings."""
|
||||||
|
|
||||||
names: Optional[List[str]] = None
|
names: Optional[List[WakeWordAndPipeline]] = None
|
||||||
"""List of wake word names to listen for."""
|
"""List of wake word names to listen for."""
|
||||||
|
|
||||||
rate: int = 16000
|
rate: int = 16000
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from .misc import (
|
|||||||
get_mac_address,
|
get_mac_address,
|
||||||
needs_silero,
|
needs_silero,
|
||||||
needs_webrtc,
|
needs_webrtc,
|
||||||
|
normalize_wake_word,
|
||||||
run_event_command,
|
run_event_command,
|
||||||
split_command,
|
split_command,
|
||||||
)
|
)
|
||||||
@@ -22,6 +23,7 @@ __all__ = [
|
|||||||
"multiply_volume",
|
"multiply_volume",
|
||||||
"needs_silero",
|
"needs_silero",
|
||||||
"needs_webrtc",
|
"needs_webrtc",
|
||||||
|
"normalize_wake_word",
|
||||||
"run_event_command",
|
"run_event_command",
|
||||||
"split_command",
|
"split_command",
|
||||||
"wav_to_events",
|
"wav_to_events",
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
|
import unicodedata
|
||||||
import uuid
|
import uuid
|
||||||
|
from functools import lru_cache
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
_LOGGER = logging.getLogger()
|
_LOGGER = logging.getLogger()
|
||||||
@@ -56,3 +59,24 @@ def split_command(command: Optional[str]) -> Optional[List[str]]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
return shlex.split(command)
|
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