Skip to content

Commit bd57cc6

Browse files
authored
Merge pull request #22 from rhasspy/synesthesiam-20240808-mic-snd-info
Add support for remotely triggered pipelines
2 parents e61742f + 743673d commit bd57cc6

File tree

10 files changed

+115
-49
lines changed

10 files changed

+115
-49
lines changed

README.md

+41-2
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,17 @@ Describe available services.
134134
* `version` - version of the model (string, optional)
135135
* `satellite` - information about voice satellite (optional)
136136
* `area` - name of area where satellite is located (string, optional)
137-
* `snd_format` - optimal audio output format of satellite (optional)
137+
* `has_vad` - true if the end of voice commands will be detected locally (boolean, optional)
138+
* `active_wake_words` - list of wake words that are actively being listend for (list of string, optional)
139+
* `max_active_wake_words` - maximum number of local wake words that can be run simultaneously (number, optional)
140+
* `supports_trigger` - true if satellite supports remotely-triggered pipelines
141+
* `mic` - list of audio input services (optional)
142+
* `mic_format` - audio input format (required)
143+
* `rate` - sample rate in hertz (int, required)
144+
* `width` - sample width in bytes (int, required)
145+
* `channels` - number of channels (int, required)
146+
* `snd` - list of audio output services (optional)
147+
* `snd_format` - audio output format (required)
138148
* `rate` - sample rate in hertz (int, required)
139149
* `width` - sample width in bytes (int, required)
140150
* `channels` - number of channels (int, required)
@@ -222,19 +232,48 @@ Play audio stream.
222232
Control of one or more remote voice satellites connected to a central server.
223233

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

241+
Pipelines are run on the server, but can be triggered remotely from the server as well.
242+
243+
* `run-pipeline` - runs a pipeline on the server or asks the satellite to run it when possible
244+
* `start_stage` - pipeline stage to start at (string, required)
245+
* `end_stage` - pipeline stage to end at (string, required)
246+
* `wake_word_name` - name of detected wake word that started this pipeline (string, optional)
247+
* From client only
248+
* `wake_word_names` - names of wake words to listen for (list of string, optional)
249+
* From server only
250+
* `start_stage` must be "wake"
251+
* `announce_text` - text to speak on the satellite
252+
* From server only
253+
* `start_stage` must be "tts"
254+
* `restart_on_end` - true if the server should re-run the pipeline after it ends (boolean, default is false)
255+
* Only used for always-on streaming satellites
256+
232257
### Timers
233258

234259
* `timer-started` - a new timer has started
260+
* `id` - unique id of timer (string, required)
261+
* `total_seconds` - number of seconds the timer should run for (int, required)
262+
* `name` - user-provided name for timer (string, optional)
263+
* `start_hours` - hours the timer should run for as spoken by user (int, optional)
264+
* `start_minutes` - minutes the timer should run for as spoken by user (int, optional)
265+
* `start_seconds` - seconds the timer should run for as spoken by user (int, optional)
266+
* `command` - optional command that the server will execute when the timer is finished
267+
* `text` - text of command to execute (string, required)
268+
* `language` - language of the command (string, optional)
235269
* `timer-updated` - timer has been paused/resumed or time has been added/removed
270+
* `id` - unique id of timer (string, required)
271+
* `is_active` - true if timer is running, false if paused (bool, required)
272+
* `total_seconds` - number of seconds that the timer should run for now (int, required)
236273
* `timer-cancelled` - timer was cancelled
274+
* `id` - unique id of timer (string, required)
237275
* `timer-finished` - timer finished without being cancelled
276+
* `id` - unique id of timer (string, required)
238277

239278
## Event Flow
240279

script/package

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ _PROGRAM_DIR = _DIR.parent
88
_VENV_DIR = _PROGRAM_DIR / ".venv"
99

1010
context = venv.EnvBuilder().ensure_directories(_VENV_DIR)
11-
subprocess.check_call([context.env_exe, _PROGRAM_DIR / "setup.py", "bdist_wheel"])
11+
subprocess.check_call(
12+
[context.env_exe, _PROGRAM_DIR / "setup.py", "bdist_wheel", "sdist"]
13+
)

wyoming/VERSION

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.5.4
1+
1.6.0

wyoming/info.py

+44-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Information about available services, models, etc.."""
2+
23
from dataclasses import dataclass, field
34
from typing import Any, Dict, List, Optional
45

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

180-
snd_format: Optional[AudioFormat] = None
181-
"""Format of the satellite's audio output."""
181+
has_vad: Optional[bool] = None
182+
"""True if a local VAD will be used to detect the end of voice commands."""
183+
184+
active_wake_words: Optional[List[str]] = None
185+
"""Wake words that are currently being listened for."""
186+
187+
max_active_wake_words: Optional[int] = None
188+
"""Maximum number of local wake words that can be run simultaneously."""
189+
190+
supports_trigger: Optional[bool] = None
191+
"""Satellite supports remotely triggering pipeline runs."""
192+
193+
194+
# -----------------------------------------------------------------------------
195+
196+
197+
@dataclass
198+
class MicProgram(Artifact):
199+
"""Microphone information."""
200+
201+
mic_format: AudioFormat
202+
"""Input audio format."""
203+
204+
205+
# -----------------------------------------------------------------------------
206+
207+
208+
@dataclass
209+
class SndProgram(Artifact):
210+
"""Sound output information."""
211+
212+
snd_format: AudioFormat
213+
"""Output audio format."""
182214

183215

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

238+
mic: List[MicProgram] = field(default_factory=list)
239+
"""Audio input services."""
240+
241+
snd: List[SndProgram] = field(default_factory=list)
242+
"""Audio output services."""
243+
206244
satellite: Optional[Satellite] = None
207245
"""Satellite information."""
208246

@@ -217,6 +255,8 @@ def event(self) -> Event:
217255
"handle": [p.to_dict() for p in self.handle],
218256
"intent": [p.to_dict() for p in self.intent],
219257
"wake": [p.to_dict() for p in self.wake],
258+
"mic": [p.to_dict() for p in self.mic],
259+
"snd": [p.to_dict() for p in self.snd],
220260
}
221261

222262
if self.satellite is not None:
@@ -239,5 +279,7 @@ def from_event(event: Event) -> "Info":
239279
handle=[HandleProgram.from_dict(d) for d in event.data.get("handle", [])],
240280
intent=[IntentProgram.from_dict(d) for d in event.data.get("intent", [])],
241281
wake=[WakeProgram.from_dict(d) for d in event.data.get("wake", [])],
282+
mic=[MicProgram.from_dict(d) for d in event.data.get("mic", [])],
283+
snd=[SndProgram.from_dict(d) for d in event.data.get("snd", [])],
242284
satellite=satellite,
243285
)

wyoming/mic.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from .client import AsyncClient
1111
from .event import Event
1212

13-
_LOGGER = logging.getLogger()
13+
_LOGGER = logging.getLogger(__name__)
1414

1515
DOMAIN = "mic"
1616

wyoming/pipeline.py

+19-23
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"""Pipeline events."""
2+
23
from dataclasses import dataclass
34
from enum import Enum
4-
from typing import Any, Dict, Optional
5+
from typing import Any, Dict, List, Optional
56

6-
from .audio import AudioFormat
77
from .event import Event, Eventable
88

99
_RUN_PIPELINE_TYPE = "run-pipeline"
@@ -38,14 +38,17 @@ class RunPipeline(Eventable):
3838
end_stage: PipelineStage
3939
"""Stage to end the pipeline on."""
4040

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

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

47-
snd_format: Optional[AudioFormat] = None
48-
"""Desired format for audio output."""
47+
wake_word_names: Optional[List[str]] = None
48+
"""Wake word names to listen for (start_stage = wake)."""
49+
50+
announce_text: Optional[str] = None
51+
"""Text to announce using text-to-speech (start_stage = tts)"""
4952

5053
def __post_init__(self) -> None:
5154
start_valid = True
@@ -104,33 +107,26 @@ def event(self) -> Event:
104107
"restart_on_end": self.restart_on_end,
105108
}
106109

107-
if self.name is not None:
108-
data["name"] = self.name
110+
if self.wake_word_name is not None:
111+
data["wake_word_name"] = self.wake_word_name
112+
113+
if self.wake_word_names:
114+
data["wake_word_names"] = self.wake_word_names
109115

110-
if self.snd_format is not None:
111-
data["snd_format"] = {
112-
"rate": self.snd_format.rate,
113-
"width": self.snd_format.width,
114-
"channels": self.snd_format.channels,
115-
}
116+
if self.announce_text is not None:
117+
data["announce_text"] = self.announce_text
116118

117119
return Event(type=_RUN_PIPELINE_TYPE, data=data)
118120

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

124125
return RunPipeline(
125126
start_stage=PipelineStage(event.data["start_stage"]),
126127
end_stage=PipelineStage(event.data["end_stage"]),
127-
name=event.data.get("name"),
128+
wake_word_name=event.data.get("wake_word_name"),
128129
restart_on_end=event.data.get("restart_on_end", False),
129-
snd_format=AudioFormat(
130-
rate=snd_format["rate"],
131-
width=snd_format["width"],
132-
channels=snd_format["channels"],
133-
)
134-
if snd_format
135-
else None,
130+
wake_word_names=event.data.get("wake_word_names"),
131+
announce_text=event.data.get("announce_text"),
136132
)

wyoming/satellite.py

+3-16
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
"""Satellite events."""
2+
23
from dataclasses import dataclass
3-
from typing import Any, Dict, Optional
44

55
from .event import Event, Eventable
6-
from .pipeline import PipelineStage
76

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

20-
start_stage: Optional[PipelineStage] = None
21-
2219
@staticmethod
2320
def is_type(event_type: str) -> bool:
2421
return event_type == _RUN_SATELLITE_TYPE
2522

2623
def event(self) -> Event:
27-
data: Dict[str, Any] = {}
28-
29-
if self.start_stage is not None:
30-
data["start_stage"] = self.start_stage.value
31-
32-
return Event(type=_RUN_SATELLITE_TYPE, data=data)
24+
return Event(type=_RUN_SATELLITE_TYPE)
3325

3426
@staticmethod
3527
def from_event(event: Event) -> "RunSatellite":
36-
# note: older versions don't send event.data
37-
start_stage = None
38-
if value := (event.data or {}).get("start_stage"):
39-
start_stage = PipelineStage(value)
40-
41-
return RunSatellite(start_stage=start_stage)
28+
return RunSatellite()
4229

4330

4431
@dataclass

wyoming/snd.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from .client import AsyncClient
1111
from .event import Event, Eventable
1212

13-
_LOGGER = logging.getLogger()
13+
_LOGGER = logging.getLogger(__name__)
1414

1515
_PLAYED_TYPE = "played"
1616

wyoming/wake.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from .client import AsyncClient
1111
from .event import Event, Eventable
1212

13-
_LOGGER = logging.getLogger()
13+
_LOGGER = logging.getLogger(__name__)
1414

1515
DOMAIN = "wake"
1616
_DETECTION_TYPE = "detection"

wyoming/zeroconf.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import socket
55
from typing import Optional
66

7-
_LOGGER = logging.getLogger()
7+
_LOGGER = logging.getLogger(__name__)
88

99
try:
1010
from zeroconf.asyncio import AsyncServiceInfo, AsyncZeroconf

0 commit comments

Comments
 (0)