signalk-wyoming 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +30 -0
- package/SPEC.md +332 -0
- package/index.js +23 -0
- package/package.json +29 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# signalk-wyoming
|
|
2
|
+
|
|
3
|
+
> **Status: RFC.** This is a design under community review — nothing is functional yet. The npm package is a 0.0.1 placeholder reserving the name. **Read [SPEC.md](SPEC.md) and tell us what you think** via [issues](https://github.com/hoeken/signalk-wyoming/issues).
|
|
4
|
+
|
|
5
|
+
An offline voice assistant for your boat, built on [Signal K](https://signalk.org) and the [Wyoming protocol](https://github.com/rhasspy/wyoming) (the voice ecosystem behind Home Assistant's Assist: Whisper, Piper, openWakeWord).
|
|
6
|
+
|
|
7
|
+
- **The boat talks:** any plugin, node-red flow, or API client can speak through satellites anywhere on the boat — and Signal K notifications (anchor alarm, engine temperature, AIS proximity) are spoken automatically.
|
|
8
|
+
- **The boat listens:** wake-word-triggered voice commands are transcribed and published to a subscribable Signal K path (`voice.command`) for anything to consume.
|
|
9
|
+
- **Fully offline.** No cloud. Everything runs in containers on the boat.
|
|
10
|
+
|
|
11
|
+
## The plugin family
|
|
12
|
+
|
|
13
|
+
| Plugin | Role | Standalone? |
|
|
14
|
+
|--------|------|-------------|
|
|
15
|
+
| [signalk-wyoming](https://github.com/hoeken/signalk-wyoming) | Orchestrator: pipelines, satellite manager, TTS routing, notification→speech bridge, `voice.*` paths, webapp | needs the services below |
|
|
16
|
+
| [signalk-whisper](https://github.com/hoeken/signalk-whisper) | Speech-to-text (Wyoming Whisper) | ✅ usable by any Wyoming client, incl. Home Assistant |
|
|
17
|
+
| [signalk-piper](https://github.com/hoeken/signalk-piper) | Text-to-speech (Wyoming Piper) | ✅ |
|
|
18
|
+
| [signalk-openwakeword](https://github.com/hoeken/signalk-openwakeword) | Wake word detection (Wyoming openWakeWord) | ✅ |
|
|
19
|
+
|
|
20
|
+
All services run as containers managed through signalk-container / [signalk-container-helper](https://github.com/hoeken/signalk-container-helper). Audio endpoints are [wyoming-satellite](https://github.com/rhasspy/wyoming-satellite) devices around the boat — including an optional "local satellite" on the server box itself.
|
|
21
|
+
|
|
22
|
+
## Read the spec
|
|
23
|
+
|
|
24
|
+
**[SPEC.md](SPEC.md)** covers the architecture, decisions made (and why), plugin configuration, the `voice.*` path schema, the `say()` API surface, audio configuration strategy, and the roadmap. [IDEA.md](IDEA.md) is the original ideation doc, kept for provenance.
|
|
25
|
+
|
|
26
|
+
Feedback is very welcome — especially from anyone who has fought boat audio, runs Signal K in a container, or uses Wyoming satellites with Home Assistant.
|
|
27
|
+
|
|
28
|
+
## License
|
|
29
|
+
|
|
30
|
+
Apache-2.0
|
package/SPEC.md
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# signalk-wyoming — Specification
|
|
2
|
+
|
|
3
|
+
**Status:** Draft v0.2 — RFC, community comments welcome (2026-07-23)
|
|
4
|
+
**Scope:** A family of SignalK plugins integrating the [Wyoming protocol](https://github.com/rhasspy/wyoming) voice-assistant ecosystem (Whisper STT, Piper TTS, openWakeWord) into SignalK, delivered as containerized microservices via signalk-container / [signalk-container-helper](https://github.com/hoeken/signalk-container-helper).
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. Decision log
|
|
9
|
+
|
|
10
|
+
Decisions made during ideation, with the deciding rationale:
|
|
11
|
+
|
|
12
|
+
| # | Decision | Rationale |
|
|
13
|
+
|---|----------|-----------|
|
|
14
|
+
| D1 | **Orchestrator and self-satellite are one plugin** (`signalk-wyoming`), self-satellite as opt-in config, off by default | SignalK plugins share one Node process, so a separate plugin buys no isolation; merging avoids cross-plugin lifecycle handshakes, a shared library, and a second install for the common case. A `role` setting (satellite-only mode) remains a future escape hatch. |
|
|
15
|
+
| D2 | **Local audio via a containerized satellite** (self-maintained `wyoming-satellite`-based image), not native in-plugin audio | Many SignalK installs are themselves containerized; native `arecord`/`aplay` would require users to modify the SignalK image/compose. A sibling container launched via the host Docker socket works identically across all deployments, and a container runtime is already a hard prerequisite of this plugin family. No upstream satellite image exists, so we build one either way — and owning it lets us bake in device-test tooling. |
|
|
16
|
+
| D3 | **Publish-only intent model** — recognized text is published to `voice.command`; intent handling is explicitly out of scope | Clean boundary. node-red, custom plugins, or a future intents/LLM plugin consume commands and respond via the `say()` API. |
|
|
17
|
+
| D4 | **Notification→speech bridge is a core v1 feature** | The killer boat feature; works with zero microphones, making Piper + orchestrator alone a useful install. |
|
|
18
|
+
| D5 | **Flat plugin names** (`signalk-whisper`, `signalk-piper`, `signalk-openwakeword`, `signalk-wyoming`) | Shorter than family-prefixed names; npm availability confirmed for all four. Names reserved with 0.0.1 placeholder releases. |
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 2. Overview
|
|
23
|
+
|
|
24
|
+
### 2.1 What this is
|
|
25
|
+
|
|
26
|
+
Voice in and voice out for a boat's SignalK server, fully offline:
|
|
27
|
+
|
|
28
|
+
- **Voice out:** any plugin, node-red flow, or API client can make the boat speak ("Anchor alarm: drag detected") through speakers anywhere on the boat. SignalK notifications are spoken automatically.
|
|
29
|
+
- **Voice in:** wake-word-triggered voice commands ("ok nabu, log my position") are transcribed and published to a subscribable SignalK path for anything to consume.
|
|
30
|
+
|
|
31
|
+
### 2.2 The plugin family
|
|
32
|
+
|
|
33
|
+
| Plugin | Role | Wraps | Default port | Standalone? |
|
|
34
|
+
|--------|------|-------|--------------|-------------|
|
|
35
|
+
| `signalk-whisper` | Speech-to-text service | `rhasspy/wyoming-whisper` (faster-whisper) | 10300 | ✅ Usable by any Wyoming client, incl. Home Assistant |
|
|
36
|
+
| `signalk-piper` | Text-to-speech service | `rhasspy/wyoming-piper` | 10200 | ✅ Same |
|
|
37
|
+
| `signalk-openwakeword` | Wake word detection service | `rhasspy/wyoming-openwakeword` | 10400 | ✅ Same |
|
|
38
|
+
| `signalk-wyoming` | Orchestrator: pipelines, satellite manager, TTS router, notification bridge, SignalK paths/API, webapp. Optionally runs a **local satellite** container (mic/speaker on the server box). | our own satellite image (§7) | satellite: 10700, control: 10800 | Needs ≥1 of whisper/piper + ≥1 audio endpoint |
|
|
39
|
+
|
|
40
|
+
All four require **signalk-container** (container runtime manager, reached via `globalThis`) and use **signalk-container-helper** (`ManagedContainer`, `buildConfig()`, `startSafely()`, offline-tolerant conventions).
|
|
41
|
+
|
|
42
|
+
### 2.3 Non-goals (v1)
|
|
43
|
+
|
|
44
|
+
- Intent handling / NLU / conversation (D3 — future separate plugin)
|
|
45
|
+
- ESPHome-based voice devices (HA Voice PE, ESP32-S3-BOX) — they speak ESPHome's native protocol, not Wyoming
|
|
46
|
+
- Cloud STT/TTS providers (offline-first; pluggable providers possible later)
|
|
47
|
+
- Media/music playback, ducking, multiroom audio sync
|
|
48
|
+
- GPU acceleration
|
|
49
|
+
- Self-satellite on non-Linux hosts
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 3. Architecture
|
|
54
|
+
|
|
55
|
+
### 3.1 Topology
|
|
56
|
+
|
|
57
|
+
Wyoming services are plain TCP servers. **The orchestrator is a Wyoming TCP client to everything** — it dials out to whisper, piper, openwakeword, and every satellite (satellites *listen* on :10700; the server connects to them, as Home Assistant does).
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
┌──── boat LAN ────────────┐
|
|
61
|
+
┌────────────────── SignalK server host ──────────────┐ │ │
|
|
62
|
+
│ │ │ Remote satellites │
|
|
63
|
+
│ SignalK server process (native OR containerized) │ │ (upstream or our image, │
|
|
64
|
+
│ ┌───────────────────────────────────────────────┐ │ │ each listens :10700) │
|
|
65
|
+
│ │ signalk-wyoming (orchestrator) │──────► cockpit │
|
|
66
|
+
│ │ · satellite mgr · pipelines · VAD │ │ │ ► salon │
|
|
67
|
+
│ │ · say() router · notification bridge │ │ │ ► cabin │
|
|
68
|
+
│ │ · voice.* paths · REST/PUT API · webapp │ │ └──────────────────────────┘
|
|
69
|
+
│ └───┬────────────┬────────────┬────────────┬────┘ │
|
|
70
|
+
│ │ :10300 │ :10200 │ :10400 │ :10700/:10800
|
|
71
|
+
│ ┌───▼──────┐ ┌───▼─────┐ ┌────▼─────────┐ ┌▼───────────────────┐
|
|
72
|
+
│ │ whisper │ │ piper │ │ openwakeword │ │ local satellite │
|
|
73
|
+
│ │ (STT) │ │ (TTS) │ │ (wake) │ │ (mic/spkr,/dev/snd)│
|
|
74
|
+
│ └──────────┘ └─────────┘ └──────────────┘ └────────────────────┘
|
|
75
|
+
│ sibling containers managed via signalk-container │
|
|
76
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
When SignalK itself runs in a container, signalk-container launches these as **sibling containers** via the host Docker socket; the SignalK container needs no audio access of any kind.
|
|
80
|
+
|
|
81
|
+
### 3.2 The `Satellite` interface (the seam)
|
|
82
|
+
|
|
83
|
+
The orchestrator manages satellites through one internal interface:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
interface Satellite {
|
|
87
|
+
id: string; name: string; zone?: string;
|
|
88
|
+
wakeMode: 'satellite' | 'central';
|
|
89
|
+
state: 'disconnected' | 'idle' | 'listening' | 'transcribing' | 'speaking';
|
|
90
|
+
connect(): void; // maintains connection w/ backoff
|
|
91
|
+
play(audio: AudioStream, opts?: { priority?: 'normal' | 'urgent' }): Promise<void>;
|
|
92
|
+
events: /* wake detections, audio stream, state changes */;
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
v1 ships one implementation: `RemoteSatellite` (Wyoming TCP client). The **local satellite is simply a `RemoteSatellite` pointed at `tcp://localhost:10700`** whose container lifecycle the plugin also owns. A future native in-process backend (bare-metal optimization) or a `role: satellite-only` mode slots in behind this interface with zero orchestrator changes.
|
|
97
|
+
|
|
98
|
+
### 3.3 Pipeline flows
|
|
99
|
+
|
|
100
|
+
**Voice command — satellite wake mode** (satellite drives its own wake detection):
|
|
101
|
+
1. Satellite streams its mic audio to a wake service (`--wake-uri`, typically the boat's central `signalk-openwakeword`).
|
|
102
|
+
2. On detection, satellite plays its awake sound locally and asks the orchestrator to run a pipeline; audio streams to the orchestrator.
|
|
103
|
+
3. Orchestrator opens a `transcribe` session with whisper, forwards audio, and runs **endpointing** (§3.4) to decide when the utterance ended.
|
|
104
|
+
4. Whisper returns `transcript`; orchestrator publishes to `voice.command` (§5.1), updates state paths, satellite plays its done sound.
|
|
105
|
+
|
|
106
|
+
**Voice command — central wake mode** (dumb satellite, always streaming):
|
|
107
|
+
Same, except the satellite streams continuously to the orchestrator, which tees audio to openwakeword and watches for `detection` itself. Trades continuous LAN audio (~256 kbit/s/satellite raw PCM) and server CPU for simpler satellites.
|
|
108
|
+
|
|
109
|
+
**Announcement:**
|
|
110
|
+
1. `say()` invoked (REST / PUT / PropertyValues / notification bridge).
|
|
111
|
+
2. Orchestrator sends `synthesize` to piper, receives `audio-start/chunk/stop`.
|
|
112
|
+
3. Audio streams to each target satellite's per-satellite FIFO queue; `priority: 'urgent'` jumps the queue. State paths update (`speaking`).
|
|
113
|
+
|
|
114
|
+
**Notification bridge:**
|
|
115
|
+
`notifications.*` delta → filter (state ≥ `minState`, `method` includes `sound` unless configured otherwise, only on state *transitions*) → `say(value.message ?? "Alarm: <path>", targets, urgent if emergency)`.
|
|
116
|
+
|
|
117
|
+
### 3.4 Endpointing (utterance segmentation)
|
|
118
|
+
|
|
119
|
+
Lives in the orchestrator. v1: silero-VAD via `onnxruntime-node`, with an energy-gate + timeout fallback if the dependency proves painful. Tunables (advanced config): `silenceMs` (default 800), `maxUtteranceMs` (10000), `minUtteranceMs` (300). This is the one genuinely non-trivial implementation detail in the orchestrator — spike it early (M2).
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 4. Cross-cutting conventions
|
|
124
|
+
|
|
125
|
+
### 4.1 Service discovery (PropertyValues)
|
|
126
|
+
|
|
127
|
+
Each service plugin advertises itself on the shared property name **`wyoming-service`**:
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
app.emitPropertyValue('wyoming-service', {
|
|
131
|
+
plugin: 'signalk-whisper', // key for latest-wins de-duplication
|
|
132
|
+
type: 'asr', // 'asr' | 'tts' | 'wake'
|
|
133
|
+
uri: 'tcp://localhost:10300',
|
|
134
|
+
status: 'ready', // 'starting' | 'ready' | 'stopped' | 'error'
|
|
135
|
+
})
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
PropertyValues replays history to late subscribers, so start order never matters. Consumers keep the **latest value per `plugin`**. Plugins emit again on every status change (including `stopped` in their `stop()` hook). Third-party plugins may advertise additional Wyoming services under the same convention.
|
|
139
|
+
|
|
140
|
+
The orchestrator's service config defaults to `auto` (use discovery) with manual `tcp://host:port` overrides — so an off-boat GPU box or an existing Home Assistant add-on can be used instead of the sibling plugins.
|
|
141
|
+
|
|
142
|
+
### 4.2 Model management (offline-first)
|
|
143
|
+
|
|
144
|
+
"First use at sea with no internet" is a real failure mode. Therefore:
|
|
145
|
+
|
|
146
|
+
- Models download **at plugin install/first-start**, into the plugin's `signalkDataMount` volume, and survive container recreation.
|
|
147
|
+
- Plugin status (`app.setPluginStatus`) reports download progress and blocks "ready" (and the `wyoming-service` `ready` emission) until models are present.
|
|
148
|
+
- Approximate sizes documented in each plugin README: whisper tiny ≈ 75 MB / base ≈ 150 MB / small ≈ 500 MB (int8 variants smaller); piper voice ≈ 60 MB; openWakeWord models ≈ 1–5 MB.
|
|
149
|
+
|
|
150
|
+
### 4.3 Error surfacing
|
|
151
|
+
|
|
152
|
+
- Service unreachable / container unhealthy → SignalK notification `notifications.voice.<service>` (state `alarm`), plus plugin status text.
|
|
153
|
+
- `say()` with piper down → immediate error result (REST 503 / PUT failure / rejected promise), never a silent drop.
|
|
154
|
+
- All containers use container-helper health checks + restart policy; offline Docker-pull failures are non-fatal per helper conventions.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 5. `signalk-wyoming` (orchestrator) — detailed spec
|
|
159
|
+
|
|
160
|
+
### 5.1 SignalK paths (under `vessels.self`)
|
|
161
|
+
|
|
162
|
+
Custom `voice.*` branch (outside the SignalK schema — normal plugin practice):
|
|
163
|
+
|
|
164
|
+
| Path | Value | Notes |
|
|
165
|
+
|------|-------|-------|
|
|
166
|
+
| `voice.command` | `{ text, satellite, zone?, language, wakeWord?, durationMs? }` | One delta per utterance. `$source` = `signalk-wyoming.<satelliteId>` so source-based filtering also works. |
|
|
167
|
+
| `voice.satellites.<id>.connected` | `boolean` | |
|
|
168
|
+
| `voice.satellites.<id>.state` | `'idle' \| 'listening' \| 'transcribing' \| 'speaking'` | |
|
|
169
|
+
| `voice.say` | write-only PUT target | See §5.2 |
|
|
170
|
+
|
|
171
|
+
### 5.2 The `say()` surface (all three delegate to one internal function)
|
|
172
|
+
|
|
173
|
+
`say(opts)` where `opts = { text, targets?: string[] /* satellite ids or zones, default all */, voice?, priority?: 'normal'|'urgent' }` → `Promise<{ ok, queued: string[], errors?: [...] }>`
|
|
174
|
+
|
|
175
|
+
1. **REST** (respects SignalK write auth):
|
|
176
|
+
- `POST /plugins/signalk-wyoming/api/say` — body is `opts`; returns 202 + result
|
|
177
|
+
- `GET /plugins/signalk-wyoming/api/satellites` — status list
|
|
178
|
+
- `GET /plugins/signalk-wyoming/api/services` — discovered services + health
|
|
179
|
+
- `POST /plugins/signalk-wyoming/api/satellites/:id/test` — play test tone
|
|
180
|
+
2. **PUT** on `voice.say` — value is a plain string or the `opts` object. Works from node-red and any SignalK client with write permission.
|
|
181
|
+
3. **PropertyValues API object** (in-process, for other plugins):
|
|
182
|
+
```js
|
|
183
|
+
app.emitPropertyValue('signalk-wyoming.api', { version: 1, say })
|
|
184
|
+
```
|
|
185
|
+
Emitting an API object (not say-*events*) is deliberate: PropertyValues replays history, which would re-speak stale messages; replaying an API handle is instead a feature — late-loading plugins get it automatically.
|
|
186
|
+
|
|
187
|
+
### 5.3 Notification bridge (core v1)
|
|
188
|
+
|
|
189
|
+
Config:
|
|
190
|
+
|
|
191
|
+
| Field | Default | Notes |
|
|
192
|
+
|-------|---------|-------|
|
|
193
|
+
| `enabled` | `true` | |
|
|
194
|
+
| `minState` | `alarm` | speak `alert`/`warn`/`alarm`/`emergency` at or above this |
|
|
195
|
+
| `requireSoundMethod` | `true` | only speak notifications whose `method` includes `sound` |
|
|
196
|
+
| `includePaths` / `excludePaths` | `[]` | glob patterns on the notification path |
|
|
197
|
+
| `targets` | `all` | satellite ids or zones |
|
|
198
|
+
|
|
199
|
+
Behavior: speak on state *transitions* into qualifying states (no re-speak of unchanged notifications; escalation re-speaks). `emergency` maps to `priority: 'urgent'`. Spoken text is the notification's `message`, falling back to a path-derived phrase. Quiet hours and repeat/nag policies are v1.x (§10).
|
|
200
|
+
|
|
201
|
+
### 5.4 Satellites config
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
satellites: [
|
|
205
|
+
{ id: 'cockpit', name: 'Cockpit', host: '10.10.10.21', port: 10700,
|
|
206
|
+
zone: 'deck', wakeMode: 'satellite' | 'central', wakeWords?: ['ok_nabu'] }
|
|
207
|
+
]
|
|
208
|
+
localSatellite: {
|
|
209
|
+
enabled: false, // D1: opt-in, off by default
|
|
210
|
+
micDevice: 'auto', sndDevice: 'auto',// populated via control-endpoint enumeration
|
|
211
|
+
wakeMode: 'central', wakeWords?: [],
|
|
212
|
+
audioMode: 'alsa' | 'pulse-socket', // §8: headless vs desktop hosts
|
|
213
|
+
feedbackSounds: true
|
|
214
|
+
}
|
|
215
|
+
services: { asr: 'auto'|uri, tts: 'auto'|uri, wake: 'auto'|uri }
|
|
216
|
+
defaults: { language: 'en', voice: /* piper voice */ }
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Degraded modes are first-class: TTS-only (no whisper — announcements only, the recommended starter install), STT-only (no piper — commands published, no spoken replies). `wakeMode: central` or a wake-enabled local satellite without openwakeword available → config-time warning + `notifications.voice.wake` at runtime.
|
|
220
|
+
|
|
221
|
+
### 5.5 Webapp
|
|
222
|
+
|
|
223
|
+
One webapp, four screens. This is the answer to "audio config is frustrating":
|
|
224
|
+
|
|
225
|
+
1. **Status** — satellites (connection/state), services (health, model info), recent commands/announcements log.
|
|
226
|
+
2. **Audio setup** — device **dropdowns** (populated from the satellite image's control endpoint, §7), *record 3 s & play back* button, *play tone* button, live VU meter (websocket). No free-text ALSA device strings unless the user opens "advanced".
|
|
227
|
+
3. **Test** — type-and-say box (target picker), **push-to-talk** button (browser mic → orchestrator → whisper; doubles as the STT test before any wake word exists — note browsers require HTTPS or localhost for `getUserMedia`), wake-word test with live detection feedback.
|
|
228
|
+
4. **Config** — friendlier editor for the plugin config (the JSON-schema auto-UI degrades at this schema size).
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## 6. Service plugins — config specs
|
|
233
|
+
|
|
234
|
+
Common to all three (via container-helper `buildConfig()`): image tag (default pinned, `auto` supported), port, restart policy, resource limits (memory cap especially — whisper), data mount for models. Each emits `wyoming-service` per §4.1. Recommended default: bind whisper/piper to localhost/docker-network only (§9).
|
|
235
|
+
|
|
236
|
+
### 6.1 `signalk-whisper`
|
|
237
|
+
|
|
238
|
+
| Param | Default | Stretch |
|
|
239
|
+
|-------|---------|---------|
|
|
240
|
+
| `model` | `tiny-int8` | — |
|
|
241
|
+
| `language` | `auto` | fixing to e.g. `en` improves speed/accuracy |
|
|
242
|
+
| `port` | 10300 | |
|
|
243
|
+
| — | | `beamSize`, `computeType`, `initialPrompt` (nautical vocabulary priming — likely high value: "genoa", "windlass", "gybe"), GPU |
|
|
244
|
+
|
|
245
|
+
### 6.2 `signalk-piper`
|
|
246
|
+
|
|
247
|
+
| Param | Default | Stretch |
|
|
248
|
+
|-------|---------|---------|
|
|
249
|
+
| `voice` | `en_US-lessac-medium` | multiple preloaded voices; per-message `voice` override honored from `say()` |
|
|
250
|
+
| `port` | 10200 | |
|
|
251
|
+
| — | | `lengthScale` (speech rate), streaming synthesis |
|
|
252
|
+
|
|
253
|
+
### 6.3 `signalk-openwakeword`
|
|
254
|
+
|
|
255
|
+
| Param | Default | Stretch |
|
|
256
|
+
|-------|---------|---------|
|
|
257
|
+
| `wakeWords` | `['ok_nabu']` | per-satellite wake words |
|
|
258
|
+
| `threshold` | 0.5 | |
|
|
259
|
+
| `triggerLevel` | 1 | |
|
|
260
|
+
| `port` | 10400 | |
|
|
261
|
+
| — | | custom `.tflite` model upload via webapp |
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## 7. The satellite container image
|
|
266
|
+
|
|
267
|
+
**The one piece we build from scratch** (no upstream image exists; upstream pushes systemd installs). Working name: `ghcr.io/hoeken/wyoming-satellite` — deliberately SignalK-agnostic, usable standalone on any Docker host (cockpit Pi, or even by Home Assistant users; a small contribution back to the Wyoming ecosystem).
|
|
268
|
+
|
|
269
|
+
- **Contents:** upstream `wyoming-satellite` (pinned release) + `alsa-utils`, on a slim Debian/Python base. MIT, with attribution.
|
|
270
|
+
- **Arch:** `linux/arm64`, `linux/amd64` (armv7 = open question).
|
|
271
|
+
- **Ports:** `:10700` Wyoming satellite protocol; `:10800` control API.
|
|
272
|
+
- **Control API** (the audio-UX enabler, identical across all deployment scenarios):
|
|
273
|
+
- `GET /devices` — parsed `arecord -l` / `aplay -l`
|
|
274
|
+
- `POST /test/record` — record N seconds, return WAV
|
|
275
|
+
- `POST /test/play` — tone or uploaded WAV
|
|
276
|
+
- `GET /vu` — websocket/SSE stream of mic RMS levels
|
|
277
|
+
- `GET /health`
|
|
278
|
+
- **Env config:** `MIC_DEVICE`, `SND_DEVICE`, `WAKE_URI`, `VAD`, `NOISE_SUPPRESSION`, `AUTO_GAIN`, `MIC_VOLUME`, `AWAKE_WAV`/`DONE_WAV` (defaults baked in, overridable via data mount).
|
|
279
|
+
- Runs upstream's VAD / noise-suppression / auto-gain — the Python audio stack we chose not to reimplement in Node (D2).
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## 8. Audio configuration strategy
|
|
284
|
+
|
|
285
|
+
Principle: **audio never touches the SignalK process.** Whisper/piper/openwakeword are pure network services; all capture/playback happens in satellite containers (or remote satellites), which our image + control API make testable from the webapp.
|
|
286
|
+
|
|
287
|
+
| Deployment | Local-satellite approach |
|
|
288
|
+
|------------|--------------------------|
|
|
289
|
+
| SignalK native or containerized, **headless host** (Pi OS Lite — the common boat server) | `/dev/snd` passthrough, `audio` group. Raw ALSA, no sound server to fight. **Default path.** |
|
|
290
|
+
| **Desktop host** (OpenPlotter / Pi OS desktop, PipeWire owns devices) | `audioMode: pulse-socket` — mount the host sound-server socket instead of `/dev/snd`. Documented recipe; contention warning surfaced in webapp if `/dev/snd` mode fails. |
|
|
291
|
+
|
|
292
|
+
**Hardware recommendation (documented prominently):** a USB conference speakerphone (Anker PowerConf, Jabra Speak) — onboard DSP does echo cancellation / AGC / noise suppression in hardware, better than any software path, and it's one cable. Remote satellites: ship a tested "boat satellite" recipe (Pi Zero 2 W + speakerphone + our image or upstream installer).
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## 9. Security
|
|
297
|
+
|
|
298
|
+
- REST/PUT respect SignalK's access control (write permission for `say`, admin for config). Webapp config screens require admin.
|
|
299
|
+
- **Wyoming has no authentication.** Defaults: whisper/piper bound to localhost/docker network (only the orchestrator needs them). openwakeword must be LAN-reachable only if remote satellites use `wakeMode: satellite` — call this out in config UI. Satellites are LAN-trust; document firewalling guidance for marina wifi. TLS/auth on Wyoming links: out of scope v1.
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## 10. Roadmap
|
|
304
|
+
|
|
305
|
+
**M0 — Foundations (prerequisites, other repos):**
|
|
306
|
+
- signalk-container + helper: `devices: ['/dev/snd']`, `groupAdd: ['audio']` in `buildConfig()`; hot-plug handled properly (`device-cgroup-rule` + `/dev/snd` bind-mount rather than static `--device`) — generally useful beyond audio (serial/GPS dongles).
|
|
307
|
+
- Satellite image repo + multi-arch CI + control API.
|
|
308
|
+
- ~~Reserve npm names~~ Done — 0.0.1 placeholders published without the `signalk-node-server-plugin` keyword, so they stay out of the app store until a functional release.
|
|
309
|
+
|
|
310
|
+
**M1 — "The boat talks"** (useful with zero microphones): `signalk-piper`; orchestrator with `say()` core (REST + PUT + PropertyValues), local satellite (output-only), **notification bridge**, status paths, webapp Status + basic Test screens.
|
|
311
|
+
|
|
312
|
+
**M2 — "The boat listens":** `signalk-whisper`; pipeline engine + VAD endpointing spike; webapp push-to-talk and record/playback screens; `voice.command` publishing. (PTT makes STT testable before wake words exist.)
|
|
313
|
+
|
|
314
|
+
**M3 — "Hands-free":** `signalk-openwakeword`; both wake modes; feedback sounds; wake test UI.
|
|
315
|
+
|
|
316
|
+
**M4 — "Whole-boat":** remote satellites hardened (reconnect/backoff soak testing), zones/targets, queue polish, Pi satellite recipe docs.
|
|
317
|
+
|
|
318
|
+
**v1.x candidates:** quiet hours + repeat/nag policy for the bridge; priority queue refinement; per-satellite voices; mDNS "scan for satellites"; custom wake-word model upload.
|
|
319
|
+
|
|
320
|
+
**Stretch:** browser satellite webapp (`getUserMedia` → websocket satellite; zero audio config on any tablet — needs HTTPS); `role: satellite-only` mode; native in-process local satellite (bare-metal desktop optimization behind the `Satellite` seam); streaming TTS; Snapcast as an announce target; intents/LLM assistant as a **separate plugin** consuming `voice.command` + `signalk-wyoming.api`; GPU whisper.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## 11. Open questions
|
|
325
|
+
|
|
326
|
+
1. **`voice.command` value shape:** object (recommended, self-contained for node-red) vs plain string + `$source`-only satellite attribution. Spec assumes object.
|
|
327
|
+
2. **VAD choice:** silero via `onnxruntime-node` vs energy gate for v1 — decide after M2 spike.
|
|
328
|
+
3. **Satellite image registry/name:** ghcr.io vs Docker Hub; `wyoming-satellite` vs `signalk-wyoming-satellite`.
|
|
329
|
+
4. **armv7 support** for the satellite image (Pi Zero 2 W runs arm64; older Pis?).
|
|
330
|
+
5. **Whisper default model:** `tiny-int8` (Pi-safe) vs `base-int8` (better accuracy, still OK on Pi 5) — benchmark on real hardware in M2.
|
|
331
|
+
6. **Control API port** (10800 proposed) — confirm no conflicts with common marine stacks.
|
|
332
|
+
7. Assumption to verify: signalk-container is available/public in time for M0 (spec written against the helper README's description of it).
|
package/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module.exports = function (app) {
|
|
2
|
+
const plugin = {
|
|
3
|
+
id: 'signalk-wyoming',
|
|
4
|
+
name: 'Wyoming Voice Assistant',
|
|
5
|
+
description:
|
|
6
|
+
'Wyoming protocol voice assistant orchestrator for Signal K — pre-release placeholder, not yet functional.',
|
|
7
|
+
|
|
8
|
+
schema: () => ({
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {},
|
|
11
|
+
}),
|
|
12
|
+
|
|
13
|
+
start: (options) => {
|
|
14
|
+
app.setPluginStatus(
|
|
15
|
+
'Pre-release placeholder — not yet functional. See https://github.com/hoeken/signalk-wyoming'
|
|
16
|
+
)
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
stop: () => {},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return plugin
|
|
23
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "signalk-wyoming",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Wyoming protocol voice assistant orchestrator for Signal K — voice commands in, spoken announcements out. Pre-release placeholder.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.js",
|
|
8
|
+
"README.md",
|
|
9
|
+
"SPEC.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"wyoming",
|
|
14
|
+
"voice-assistant",
|
|
15
|
+
"voice",
|
|
16
|
+
"speech",
|
|
17
|
+
"marine",
|
|
18
|
+
"boat"
|
|
19
|
+
],
|
|
20
|
+
"author": "hoeken",
|
|
21
|
+
"license": "Apache-2.0",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/hoeken/signalk-wyoming.git"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=22"
|
|
28
|
+
}
|
|
29
|
+
}
|