Skip to content

Commit

Permalink
Merge pull request #22 from rhasspy/synesthesiam-20240808-mic-snd-info
Browse files Browse the repository at this point in the history
Add support for remotely triggered pipelines
  • Loading branch information
synesthesiam authored Aug 9, 2024
2 parents e61742f + 743673d commit bd57cc6
Show file tree
Hide file tree
Showing 10 changed files with 115 additions and 49 deletions.
43 changes: 41 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,17 @@ Describe available services.
* `version` - version of the model (string, optional)
* `satellite` - information about voice satellite (optional)
* `area` - name of area where satellite is located (string, optional)
* `snd_format` - optimal audio output format of satellite (optional)
* `has_vad` - true if the end of voice commands will be detected locally (boolean, optional)
* `active_wake_words` - list of wake words that are actively being listend for (list of string, optional)
* `max_active_wake_words` - maximum number of local wake words that can be run simultaneously (number, optional)
* `supports_trigger` - true if satellite supports remotely-triggered pipelines
* `mic` - list of audio input services (optional)
* `mic_format` - audio input format (required)
* `rate` - sample rate in hertz (int, required)
* `width` - sample width in bytes (int, required)
* `channels` - number of channels (int, required)
* `snd` - list of audio output services (optional)
* `snd_format` - audio output format (required)
* `rate` - sample rate in hertz (int, required)
* `width` - sample width in bytes (int, required)
* `channels` - number of channels (int, required)
Expand Down Expand Up @@ -222,19 +232,48 @@ Play audio stream.
Control of one or more remote voice satellites connected to a central server.

* `run-satellite` - informs satellite that server is ready to run pipelines
* `start_stage` - request pipelines with a specific starting stage (string, optional)
* `pause-satellite` - informs satellite that server is not ready anymore to run pipelines
* `satellite-connected` - satellite has connected to the server
* `satellite-disconnected` - satellite has been disconnected from the server
* `streaming-started` - satellite has started streaming audio to the server
* `streaming-stopped` - satellite has stopped streaming audio to the server

Pipelines are run on the server, but can be triggered remotely from the server as well.

* `run-pipeline` - runs a pipeline on the server or asks the satellite to run it when possible
* `start_stage` - pipeline stage to start at (string, required)
* `end_stage` - pipeline stage to end at (string, required)
* `wake_word_name` - name of detected wake word that started this pipeline (string, optional)
* From client only
* `wake_word_names` - names of wake words to listen for (list of string, optional)
* From server only
* `start_stage` must be "wake"
* `announce_text` - text to speak on the satellite
* From server only
* `start_stage` must be "tts"
* `restart_on_end` - true if the server should re-run the pipeline after it ends (boolean, default is false)
* Only used for always-on streaming satellites

### Timers

* `timer-started` - a new timer has started
* `id` - unique id of timer (string, required)
* `total_seconds` - number of seconds the timer should run for (int, required)
* `name` - user-provided name for timer (string, optional)
* `start_hours` - hours the timer should run for as spoken by user (int, optional)
* `start_minutes` - minutes the timer should run for as spoken by user (int, optional)
* `start_seconds` - seconds the timer should run for as spoken by user (int, optional)
* `command` - optional command that the server will execute when the timer is finished
* `text` - text of command to execute (string, required)
* `language` - language of the command (string, optional)
* `timer-updated` - timer has been paused/resumed or time has been added/removed
* `id` - unique id of timer (string, required)
* `is_active` - true if timer is running, false if paused (bool, required)
* `total_seconds` - number of seconds that the timer should run for now (int, required)
* `timer-cancelled` - timer was cancelled
* `id` - unique id of timer (string, required)
* `timer-finished` - timer finished without being cancelled
* `id` - unique id of timer (string, required)

## Event Flow

Expand Down
4 changes: 3 additions & 1 deletion script/package
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ _PROGRAM_DIR = _DIR.parent
_VENV_DIR = _PROGRAM_DIR / ".venv"

context = venv.EnvBuilder().ensure_directories(_VENV_DIR)
subprocess.check_call([context.env_exe, _PROGRAM_DIR / "setup.py", "bdist_wheel"])
subprocess.check_call(
[context.env_exe, _PROGRAM_DIR / "setup.py", "bdist_wheel", "sdist"]
)
2 changes: 1 addition & 1 deletion wyoming/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.5.4
1.6.0
46 changes: 44 additions & 2 deletions wyoming/info.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Information about available services, models, etc.."""

from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional

Expand Down Expand Up @@ -177,8 +178,39 @@ class Satellite(Artifact):
area: Optional[str] = None
"""Name of the area the satellite is in."""

snd_format: Optional[AudioFormat] = None
"""Format of the satellite's audio output."""
has_vad: Optional[bool] = None
"""True if a local VAD will be used to detect the end of voice commands."""

active_wake_words: Optional[List[str]] = None
"""Wake words that are currently being listened for."""

max_active_wake_words: Optional[int] = None
"""Maximum number of local wake words that can be run simultaneously."""

supports_trigger: Optional[bool] = None
"""Satellite supports remotely triggering pipeline runs."""


# -----------------------------------------------------------------------------


@dataclass
class MicProgram(Artifact):
"""Microphone information."""

mic_format: AudioFormat
"""Input audio format."""


# -----------------------------------------------------------------------------


@dataclass
class SndProgram(Artifact):
"""Sound output information."""

snd_format: AudioFormat
"""Output audio format."""


# -----------------------------------------------------------------------------
Expand All @@ -203,6 +235,12 @@ class Info(Eventable):
wake: List[WakeProgram] = field(default_factory=list)
"""Wake word detection services."""

mic: List[MicProgram] = field(default_factory=list)
"""Audio input services."""

snd: List[SndProgram] = field(default_factory=list)
"""Audio output services."""

satellite: Optional[Satellite] = None
"""Satellite information."""

Expand All @@ -217,6 +255,8 @@ def event(self) -> Event:
"handle": [p.to_dict() for p in self.handle],
"intent": [p.to_dict() for p in self.intent],
"wake": [p.to_dict() for p in self.wake],
"mic": [p.to_dict() for p in self.mic],
"snd": [p.to_dict() for p in self.snd],
}

if self.satellite is not None:
Expand All @@ -239,5 +279,7 @@ def from_event(event: Event) -> "Info":
handle=[HandleProgram.from_dict(d) for d in event.data.get("handle", [])],
intent=[IntentProgram.from_dict(d) for d in event.data.get("intent", [])],
wake=[WakeProgram.from_dict(d) for d in event.data.get("wake", [])],
mic=[MicProgram.from_dict(d) for d in event.data.get("mic", [])],
snd=[SndProgram.from_dict(d) for d in event.data.get("snd", [])],
satellite=satellite,
)
2 changes: 1 addition & 1 deletion wyoming/mic.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from .client import AsyncClient
from .event import Event

_LOGGER = logging.getLogger()
_LOGGER = logging.getLogger(__name__)

DOMAIN = "mic"

Expand Down
42 changes: 19 additions & 23 deletions wyoming/pipeline.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""Pipeline events."""

from dataclasses import dataclass
from enum import Enum
from typing import Any, Dict, Optional
from typing import Any, Dict, List, Optional

from .audio import AudioFormat
from .event import Event, Eventable

_RUN_PIPELINE_TYPE = "run-pipeline"
Expand Down Expand Up @@ -38,14 +38,17 @@ class RunPipeline(Eventable):
end_stage: PipelineStage
"""Stage to end the pipeline on."""

name: Optional[str] = None
"""Name of pipeline to run"""
wake_word_name: Optional[str] = None
"""Name of wake word that triggered this pipeline."""

restart_on_end: bool = False
"""True if pipeline should restart automatically after ending."""

snd_format: Optional[AudioFormat] = None
"""Desired format for audio output."""
wake_word_names: Optional[List[str]] = None
"""Wake word names to listen for (start_stage = wake)."""

announce_text: Optional[str] = None
"""Text to announce using text-to-speech (start_stage = tts)"""

def __post_init__(self) -> None:
start_valid = True
Expand Down Expand Up @@ -104,33 +107,26 @@ def event(self) -> Event:
"restart_on_end": self.restart_on_end,
}

if self.name is not None:
data["name"] = self.name
if self.wake_word_name is not None:
data["wake_word_name"] = self.wake_word_name

if self.wake_word_names:
data["wake_word_names"] = self.wake_word_names

if self.snd_format is not None:
data["snd_format"] = {
"rate": self.snd_format.rate,
"width": self.snd_format.width,
"channels": self.snd_format.channels,
}
if self.announce_text is not None:
data["announce_text"] = self.announce_text

return Event(type=_RUN_PIPELINE_TYPE, data=data)

@staticmethod
def from_event(event: Event) -> "RunPipeline":
assert event.data is not None
snd_format = event.data.get("snd_format")

return RunPipeline(
start_stage=PipelineStage(event.data["start_stage"]),
end_stage=PipelineStage(event.data["end_stage"]),
name=event.data.get("name"),
wake_word_name=event.data.get("wake_word_name"),
restart_on_end=event.data.get("restart_on_end", False),
snd_format=AudioFormat(
rate=snd_format["rate"],
width=snd_format["width"],
channels=snd_format["channels"],
)
if snd_format
else None,
wake_word_names=event.data.get("wake_word_names"),
announce_text=event.data.get("announce_text"),
)
19 changes: 3 additions & 16 deletions wyoming/satellite.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
"""Satellite events."""

from dataclasses import dataclass
from typing import Any, Dict, Optional

from .event import Event, Eventable
from .pipeline import PipelineStage

_RUN_SATELLITE_TYPE = "run-satellite"
_PAUSE_SATELLITE_TYPE = "pause-satellite"
Expand All @@ -17,28 +16,16 @@
class RunSatellite(Eventable):
"""Informs the satellite that the server is ready to run a pipeline."""

start_stage: Optional[PipelineStage] = None

@staticmethod
def is_type(event_type: str) -> bool:
return event_type == _RUN_SATELLITE_TYPE

def event(self) -> Event:
data: Dict[str, Any] = {}

if self.start_stage is not None:
data["start_stage"] = self.start_stage.value

return Event(type=_RUN_SATELLITE_TYPE, data=data)
return Event(type=_RUN_SATELLITE_TYPE)

@staticmethod
def from_event(event: Event) -> "RunSatellite":
# note: older versions don't send event.data
start_stage = None
if value := (event.data or {}).get("start_stage"):
start_stage = PipelineStage(value)

return RunSatellite(start_stage=start_stage)
return RunSatellite()


@dataclass
Expand Down
2 changes: 1 addition & 1 deletion wyoming/snd.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from .client import AsyncClient
from .event import Event, Eventable

_LOGGER = logging.getLogger()
_LOGGER = logging.getLogger(__name__)

_PLAYED_TYPE = "played"

Expand Down
2 changes: 1 addition & 1 deletion wyoming/wake.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from .client import AsyncClient
from .event import Event, Eventable

_LOGGER = logging.getLogger()
_LOGGER = logging.getLogger(__name__)

DOMAIN = "wake"
_DETECTION_TYPE = "detection"
Expand Down
2 changes: 1 addition & 1 deletion wyoming/zeroconf.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import socket
from typing import Optional

_LOGGER = logging.getLogger()
_LOGGER = logging.getLogger(__name__)

try:
from zeroconf.asyncio import AsyncServiceInfo, AsyncZeroconf
Expand Down

0 comments on commit bd57cc6

Please sign in to comment.