signalk-piper 0.0.1 → 0.1.0

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/README.md CHANGED
@@ -1,15 +1,81 @@
1
1
  # signalk-piper
2
2
 
3
- > **Status: pre-release placeholder.** This package reserves the name nothing is functional yet. The design is under community review at **[hoeken/signalk-wyoming](https://github.com/hoeken/signalk-wyoming)** read the spec there and leave feedback.
3
+ > **Status: ALPHA** This SignalK Wyoming system is 100% vibecoded slop. I don't have the right hardware yet to test it, so I'm putting it out there for people to test in the meantime. It _should_ work. File issues for anything that doestn.
4
4
 
5
- [Wyoming](https://github.com/rhasspy/wyoming) [Piper](https://github.com/rhasspy/wyoming-piper) text-to-speech service for [Signal K](https://signalk.org), running as a managed container. Part of the **signalk-wyoming** offline voice assistant family, but fully standalone: any Wyoming client (including Home Assistant) can use it.
5
+ [Piper](https://github.com/rhasspy/wyoming-piper) text-to-speech for [Signal K](https://signalk.org), running as a managed container and speaking the [Wyoming protocol](https://github.com/rhasspy/wyoming). The plugin pulls and supervises the pinned `rhasspy/wyoming-piper` image, gates readiness on a real Wyoming `describe`/`info` exchange (so "ready" means the voice model is downloaded _and_ the service answers), health-checks it continuously, and advertises the service on the `wyoming-service` PropertyValues convention for the [signalk-wyoming](https://github.com/hoeken/signalk-wyoming) orchestrator the family spec lives in that repo. It is also fully standalone: any Wyoming client (including Home Assistant) can use the service directly.
6
6
 
7
- | Plugin | Role |
8
- |--------|------|
9
- | [signalk-wyoming](https://github.com/hoeken/signalk-wyoming) | Orchestrator — spec and discussion live here |
10
- | [signalk-whisper](https://github.com/hoeken/signalk-whisper) | Speech-to-text |
11
- | **signalk-piper** | Text-to-speech |
12
- | [signalk-openwakeword](https://github.com/hoeken/signalk-openwakeword) | Wake word detection |
7
+ | Plugin | Role |
8
+ | ---------------------------------------------------------------------- | --------------------------------------------------- |
9
+ | [signalk-wyoming](https://github.com/hoeken/signalk-wyoming) | Orchestrator — family spec and discussion live here |
10
+ | [signalk-whisper](https://github.com/hoeken/signalk-whisper) | Speech-to-text |
11
+ | **signalk-piper** | Text-to-speech |
12
+ | [signalk-openwakeword](https://github.com/hoeken/signalk-openwakeword) | Wake word detection |
13
+
14
+ ## Requirements
15
+
16
+ - The **[signalk-container](https://www.npmjs.com/package/signalk-container)** plugin, with a working podman or docker runtime (the plugin declares this via `"signalk": { "requires": ["signalk-container"] }`).
17
+ - **Node.js ≥ 24** (Signal K server ≥ 2.x).
18
+ - RAM: piper holds one loaded voice at **~150 MB resident**; the container is capped at 512 MB by default. A TTS-only voice install (piper + orchestrator) runs comfortably alongside Signal K on a Pi 4 / 2 GB.
19
+ - Disk: ~410 MB (amd64) / ~370 MB (arm64) compressed image + ~60 MB per voice. Image architectures: `linux/amd64`, `linux/arm64` (no armv7).
20
+
21
+ ## Install
22
+
23
+ Install **signalk-piper** from the Signal K App Store (Appstore → Available → search "piper"), then enable it in Plugin Config. Make sure signalk-container is installed and enabled first.
24
+
25
+ ## Configuration reference
26
+
27
+ | Setting | Default | Description |
28
+ | -------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
29
+ | `voice` | `en_US-lessac-medium` | Piper voice to load and serve. **Use 22.05 kHz voices only** (`-medium` / `-high` qualities); `-low`/`-x_low` voices are 16 kHz and outside the signalk-wyoming v1 audio contract — the plugin warns but does not block. Browse voices at [rhasspy/piper-voices](https://huggingface.co/rhasspy/piper-voices). |
30
+ | `imageTag` | `auto` | Image tag for `rhasspy/wyoming-piper`. `auto` runs the pinned, tested release (**2.3.1**) and follows plugin updates; set an explicit tag to pin an upstream version. |
31
+ | `port` | `10200` | Host port for the Wyoming TTS service. |
32
+ | `advanced.bind` | `127.0.0.1` | `127.0.0.1`: only Signal K can reach the service (recommended). `0.0.0.0`: publish on all interfaces for other machines (see [Security](#security)). |
33
+ | `advanced.memoryLimit` | `512m` | Hard container memory cap (swap disabled). |
34
+ | `advanced.restartPolicy` | `unless-stopped` | Container restart policy (`no` / `unless-stopped` / `always`). |
35
+ | `advanced.updateVoices` | `false` | Pass `--update-voices`: refresh the voices catalog from HuggingFace at container start (needs internet). |
36
+ | `advanced.sentenceSilence` | _(unset)_ | Pass `--sentence-silence <s>`: seconds of silence appended after each sentence. |
37
+ | `advanced.lengthScale` | _(unset)_ | Pass `--length-scale <n>`: phoneme length scale; > 1 slows speech down. |
38
+
39
+ ## First start downloads the voice
40
+
41
+ On first start the container downloads the configured voice (**~60 MB**) from HuggingFace into the plugin's data directory (mounted at `/data`), where it survives container recreation and image updates. Until the service answers, the plugin status shows _"Starting — first start downloads ~60 MB per voice"_.
42
+
43
+ **Offline warning:** the first start of a given voice needs internet. Do this at the dock, not at sea — after that, everything runs fully offline. Per-request voices that were never downloaded also trigger a download at request time (see below), so pre-download any voice you plan to use.
44
+
45
+ ## How other software uses it
46
+
47
+ The service is a plain Wyoming TTS endpoint at `tcp://<host>:10200`.
48
+
49
+ - **signalk-wyoming** discovers it automatically via the `wyoming-service` PropertyValues emission (`{ plugin: "signalk-piper", type: "tts", uri, status }`) — no manual wiring needed.
50
+ - **Home Assistant** (standalone use): set `advanced.bind` to `0.0.0.0`, then add a Wyoming Protocol integration pointing at `tcp://<boat-ip>:10200`.
51
+ - **Per-request voice override:** Wyoming `synthesize` requests may name any known voice (`voice.name`); piper downloads voices it knows about but hasn't cached — a mid-request network hit, so pre-download voices you rely on.
52
+
53
+ The plugin also serves (on its Signal K router, `/plugins/signalk-piper`):
54
+
55
+ - `GET /api/status` — `{ status, uri, tag, containerState, lastHealth, info }` (readonly auth where supported)
56
+ - `GET /api/update/check` / `POST /api/update/apply` — container image update endpoints (admin)
57
+
58
+ ## Health and notifications
59
+
60
+ - Readiness is protocol-native: the plugin polls Wyoming `describe` every 2 s (for up to 10 minutes, to ride out slow first-start downloads) and reports ready only on a valid `info` response. Wyoming protocol versions outside 1.x log a loud warning but don't block.
61
+ - After that, a health loop sends `describe` every 30 s (5 s timeout). **Three consecutive failures** raise the Signal K notification `notifications.voice.piper` (`state: "alarm"`, `method: ["visual"]`), set a plugin error, and emit `wyoming-service` status `error`; recovery clears the notification (`state: "normal"`) and re-emits `ready`.
62
+ - Status emissions are debounced (≥ 500 ms apart, duplicate suppression, flap guard) so a wobbly service can't flood the server's PropertyValues table.
63
+
64
+ ## Security
65
+
66
+ Wyoming has **no authentication**. The default `bind: 127.0.0.1` keeps the service reachable only from the Signal K host (and from Signal K itself in containerized deployments) — leave it that way unless another machine genuinely needs TTS. If you set `bind: 0.0.0.0` (e.g. to share the service with Home Assistant), firewall the port at the boat router or put it on a trusted VLAN; anyone who can reach the port can use the service. See the family spec's security section for the full picture (satellite mic ports are the sensitive ones).
67
+
68
+ ## Development
69
+
70
+ ```sh
71
+ npm install
72
+ npm run build # tsc → dist/
73
+ npm test # typecheck + vitest (mock Wyoming server + fake container manager; no docker needed)
74
+ npm run ci-lint # eslint + prettier check
75
+ npm run format # prettier + eslint --fix
76
+ ```
77
+
78
+ Tests use the scriptable mock Wyoming server from `signalk-wyoming/mock` (a devDependency on the [signalk-wyoming](https://github.com/hoeken/signalk-wyoming) package). Production code has no dependency on signalk-wyoming; it embeds its own minimal describe client (`src/wyoming.ts`).
13
79
 
14
80
  ## License
15
81
 
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Configuration schema, defaults and container-config construction for
3
+ * signalk-piper.
4
+ *
5
+ * Signal K does NOT seed schema defaults into the config it hands to
6
+ * `start()` — `resolveSettings()` merges user config over `DEFAULT_SETTINGS`.
7
+ * `buildContainerConfig()` must stay a pure, deterministic function of
8
+ * (settings, tag): signalk-container diffs the produced config against the
9
+ * live container to decide whether a recreate is needed.
10
+ */
11
+ import type { ContainerConfig } from "signalk-container-helper";
12
+ /** Upstream image (repo without tag). */
13
+ export declare const IMAGE = "rhasspy/wyoming-piper";
14
+ /** Pinned upstream release that `imageTag: "auto"` resolves to. */
15
+ export declare const PINNED_TAG = "2.3.1";
16
+ /** Wyoming TCP port piper listens on inside the container (image default). */
17
+ export declare const DEFAULT_PORT = 10200;
18
+ /** Approximate first-start download per voice (README + status hint). */
19
+ export declare const DOWNLOAD_HINT = "~60 MB per voice";
20
+ export interface PiperAdvancedSettings {
21
+ /**
22
+ * "127.0.0.1": Signal K-only networking via signalkAccessiblePorts.
23
+ * "0.0.0.0": publish the port on all interfaces (LAN-reachable).
24
+ */
25
+ bind: "127.0.0.1" | "0.0.0.0";
26
+ memoryLimit: string;
27
+ restartPolicy: "no" | "unless-stopped" | "always";
28
+ updateVoices: boolean;
29
+ sentenceSilence?: number;
30
+ lengthScale?: number;
31
+ }
32
+ export interface PiperSettings {
33
+ imageTag: string;
34
+ voice: string;
35
+ port: number;
36
+ advanced: PiperAdvancedSettings;
37
+ }
38
+ export declare const DEFAULT_SETTINGS: PiperSettings;
39
+ /** Merge raw plugin config (possibly partial/undefined) over the defaults. */
40
+ export declare function resolveSettings(raw: unknown): PiperSettings;
41
+ /** `imageTag: "auto"` → the pinned upstream release; anything else verbatim. */
42
+ export declare function resolveTag(requested: string): string;
43
+ /** True for plain numeric semver tags like "2.3.1" (update-check filter). */
44
+ export declare function isSemverTag(tag: string): boolean;
45
+ /** True for 16 kHz voices, which the v1 family spec excludes (22.05 kHz only). */
46
+ export declare function isLowSampleRateVoice(voice: string): boolean;
47
+ /**
48
+ * `docker run` arguments appended to the image ENTRYPOINT (which already
49
+ * bakes `--uri tcp://0.0.0.0:10200 --data-dir /data`).
50
+ */
51
+ export declare function buildCommand(settings: PiperSettings): string[];
52
+ /** Pure (settings, tag) → ContainerConfig. */
53
+ export declare function buildContainerConfig(settings: PiperSettings, tag: string): ContainerConfig;
54
+ /** JSON schema for the Signal K plugin-config UI. */
55
+ export declare const CONFIG_SCHEMA: {
56
+ readonly type: "object";
57
+ readonly properties: {
58
+ readonly voice: {
59
+ readonly type: "string";
60
+ readonly title: "Voice";
61
+ readonly default: string;
62
+ readonly description: string;
63
+ };
64
+ readonly imageTag: {
65
+ readonly type: "string";
66
+ readonly title: "Image tag";
67
+ readonly default: string;
68
+ readonly description: string;
69
+ };
70
+ readonly port: {
71
+ readonly type: "number";
72
+ readonly title: "Port";
73
+ readonly default: number;
74
+ readonly description: string;
75
+ };
76
+ readonly advanced: {
77
+ readonly type: "object";
78
+ readonly title: "Advanced";
79
+ readonly properties: {
80
+ readonly bind: {
81
+ readonly type: "string";
82
+ readonly title: "Bind address";
83
+ readonly enum: readonly ["127.0.0.1", "0.0.0.0"];
84
+ readonly default: "127.0.0.1" | "0.0.0.0";
85
+ readonly description: string;
86
+ };
87
+ readonly memoryLimit: {
88
+ readonly type: "string";
89
+ readonly title: "Memory limit";
90
+ readonly default: string;
91
+ readonly description: "Hard container memory cap (swap disabled), e.g. \"512m\" or \"1g\".";
92
+ };
93
+ readonly restartPolicy: {
94
+ readonly type: "string";
95
+ readonly title: "Restart policy";
96
+ readonly enum: readonly ["no", "unless-stopped", "always"];
97
+ readonly default: "no" | "unless-stopped" | "always";
98
+ readonly description: "Container restart policy.";
99
+ };
100
+ readonly updateVoices: {
101
+ readonly type: "boolean";
102
+ readonly title: "Update voices list on start";
103
+ readonly default: boolean;
104
+ readonly description: string;
105
+ };
106
+ readonly sentenceSilence: {
107
+ readonly type: "number";
108
+ readonly title: "Sentence silence (seconds)";
109
+ readonly description: "Optional seconds of silence appended after each sentence.";
110
+ };
111
+ readonly lengthScale: {
112
+ readonly type: "number";
113
+ readonly title: "Length scale";
114
+ readonly description: "Optional phoneme length scale; values above 1 slow speech down.";
115
+ };
116
+ };
117
+ };
118
+ };
119
+ };
120
+ export declare const UI_SCHEMA: {
121
+ readonly advanced: {
122
+ readonly "ui:options": {
123
+ readonly collapsible: true;
124
+ };
125
+ };
126
+ };
127
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAEhE,yCAAyC;AACzC,eAAO,MAAM,KAAK,0BAA0B,CAAC;AAE7C,mEAAmE;AACnE,eAAO,MAAM,UAAU,UAAU,CAAC;AAElC,8EAA8E;AAC9E,eAAO,MAAM,YAAY,QAAQ,CAAC;AAElC,yEAAyE;AACzE,eAAO,MAAM,aAAa,qBAAqB,CAAC;AAEhD,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,IAAI,EAAE,WAAW,GAAG,SAAS,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,IAAI,GAAG,gBAAgB,GAAG,QAAQ,CAAC;IAClD,YAAY,EAAE,OAAO,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,qBAAqB,CAAC;CACjC;AAED,eAAO,MAAM,gBAAgB,EAAE,aAU9B,CAAC;AAEF,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,aAAa,CAyB3D;AAED,gFAAgF;AAChF,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED,kFAAkF;AAClF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,aAAa,GAAG,MAAM,EAAE,CAkB9D;AAED,8CAA8C;AAC9C,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,aAAa,EACvB,GAAG,EAAE,MAAM,GACV,eAAe,CAyBjB;AAED,qDAAqD;AACrD,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmFhB,CAAC;AAEX,eAAO,MAAM,SAAS;;;;;;CAIZ,CAAC"}
package/dist/config.js ADDED
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Configuration schema, defaults and container-config construction for
3
+ * signalk-piper.
4
+ *
5
+ * Signal K does NOT seed schema defaults into the config it hands to
6
+ * `start()` — `resolveSettings()` merges user config over `DEFAULT_SETTINGS`.
7
+ * `buildContainerConfig()` must stay a pure, deterministic function of
8
+ * (settings, tag): signalk-container diffs the produced config against the
9
+ * live container to decide whether a recreate is needed.
10
+ */
11
+ /** Upstream image (repo without tag). */
12
+ export const IMAGE = "rhasspy/wyoming-piper";
13
+ /** Pinned upstream release that `imageTag: "auto"` resolves to. */
14
+ export const PINNED_TAG = "2.3.1";
15
+ /** Wyoming TCP port piper listens on inside the container (image default). */
16
+ export const DEFAULT_PORT = 10200;
17
+ /** Approximate first-start download per voice (README + status hint). */
18
+ export const DOWNLOAD_HINT = "~60 MB per voice";
19
+ export const DEFAULT_SETTINGS = {
20
+ imageTag: "auto",
21
+ voice: "en_US-lessac-medium",
22
+ port: DEFAULT_PORT,
23
+ advanced: {
24
+ bind: "127.0.0.1",
25
+ memoryLimit: "512m",
26
+ restartPolicy: "unless-stopped",
27
+ updateVoices: false,
28
+ },
29
+ };
30
+ /** Merge raw plugin config (possibly partial/undefined) over the defaults. */
31
+ export function resolveSettings(raw) {
32
+ const cfg = isObject(raw) ? raw : {};
33
+ const adv = isObject(cfg.advanced) ? cfg.advanced : {};
34
+ const settings = {
35
+ imageTag: str(cfg.imageTag) ?? DEFAULT_SETTINGS.imageTag,
36
+ voice: str(cfg.voice) ?? DEFAULT_SETTINGS.voice,
37
+ port: num(cfg.port) ?? DEFAULT_SETTINGS.port,
38
+ advanced: {
39
+ bind: adv.bind === "0.0.0.0" ? "0.0.0.0" : "127.0.0.1",
40
+ memoryLimit: str(adv.memoryLimit) ?? DEFAULT_SETTINGS.advanced.memoryLimit,
41
+ restartPolicy: adv.restartPolicy === "no" || adv.restartPolicy === "always"
42
+ ? adv.restartPolicy
43
+ : DEFAULT_SETTINGS.advanced.restartPolicy,
44
+ updateVoices: adv.updateVoices === true,
45
+ },
46
+ };
47
+ const sentenceSilence = num(adv.sentenceSilence);
48
+ if (sentenceSilence !== undefined) {
49
+ settings.advanced.sentenceSilence = sentenceSilence;
50
+ }
51
+ const lengthScale = num(adv.lengthScale);
52
+ if (lengthScale !== undefined)
53
+ settings.advanced.lengthScale = lengthScale;
54
+ return settings;
55
+ }
56
+ /** `imageTag: "auto"` → the pinned upstream release; anything else verbatim. */
57
+ export function resolveTag(requested) {
58
+ return requested === "auto" ? PINNED_TAG : requested;
59
+ }
60
+ /** True for plain numeric semver tags like "2.3.1" (update-check filter). */
61
+ export function isSemverTag(tag) {
62
+ return /^\d+\.\d+\.\d+$/.test(tag);
63
+ }
64
+ /** True for 16 kHz voices, which the v1 family spec excludes (22.05 kHz only). */
65
+ export function isLowSampleRateVoice(voice) {
66
+ return voice.endsWith("-low") || voice.endsWith("-x_low");
67
+ }
68
+ /**
69
+ * `docker run` arguments appended to the image ENTRYPOINT (which already
70
+ * bakes `--uri tcp://0.0.0.0:10200 --data-dir /data`).
71
+ */
72
+ export function buildCommand(settings) {
73
+ const command = ["--voice", settings.voice];
74
+ if (settings.advanced.updateVoices)
75
+ command.push("--update-voices");
76
+ if (settings.advanced.sentenceSilence !== undefined) {
77
+ command.push("--sentence-silence", String(settings.advanced.sentenceSilence));
78
+ }
79
+ if (settings.advanced.lengthScale !== undefined) {
80
+ command.push("--length-scale", String(settings.advanced.lengthScale));
81
+ }
82
+ if (settings.port !== DEFAULT_PORT) {
83
+ // --uri is last-wins in upstream argparse, so this overrides the baked-in
84
+ // port and makes the container actually listen on the configured port.
85
+ command.push("--uri", `tcp://0.0.0.0:${settings.port}`);
86
+ }
87
+ return command;
88
+ }
89
+ /** Pure (settings, tag) → ContainerConfig. */
90
+ export function buildContainerConfig(settings, tag) {
91
+ const config = {
92
+ image: IMAGE,
93
+ tag,
94
+ command: buildCommand(settings),
95
+ // Voice models download into /data on first start; the Signal K data
96
+ // mount survives container recreation.
97
+ signalkDataMount: "/data",
98
+ restart: settings.advanced.restartPolicy,
99
+ resources: {
100
+ memory: settings.advanced.memoryLimit,
101
+ memorySwap: settings.advanced.memoryLimit,
102
+ },
103
+ };
104
+ if (settings.advanced.bind === "0.0.0.0") {
105
+ // LAN exposure (e.g. sharing the service with Home Assistant): explicit
106
+ // all-interfaces publish. Never combined with signalkAccessiblePorts —
107
+ // the manager would overwrite/drop the manual binding.
108
+ config.ports = { [String(settings.port)]: `0.0.0.0:${settings.port}` };
109
+ }
110
+ else {
111
+ // Default: loopback-only on bare metal, container networking when
112
+ // Signal K itself runs in a container. Only Signal K needs to reach it.
113
+ config.signalkAccessiblePorts = [settings.port];
114
+ }
115
+ return config;
116
+ }
117
+ /** JSON schema for the Signal K plugin-config UI. */
118
+ export const CONFIG_SCHEMA = {
119
+ type: "object",
120
+ properties: {
121
+ voice: {
122
+ type: "string",
123
+ title: "Voice",
124
+ default: DEFAULT_SETTINGS.voice,
125
+ description: "Piper voice to load and serve (e.g. en_US-lessac-medium). Use 22.05 kHz " +
126
+ "voices only — -medium and -high qualities. Voices ending in -low or " +
127
+ "-x_low are 16 kHz and outside the signalk-wyoming v1 audio contract. " +
128
+ "Browse voices at https://huggingface.co/rhasspy/piper-voices. The " +
129
+ "voice (~60 MB) is downloaded on first start.",
130
+ },
131
+ imageTag: {
132
+ type: "string",
133
+ title: "Image tag",
134
+ default: DEFAULT_SETTINGS.imageTag,
135
+ description: `Docker image tag for ${IMAGE}. "auto" runs the pinned, tested ` +
136
+ `release (${PINNED_TAG}) and follows plugin updates. Set an explicit ` +
137
+ "tag only to pin a specific upstream version.",
138
+ },
139
+ port: {
140
+ type: "number",
141
+ title: "Port",
142
+ default: DEFAULT_SETTINGS.port,
143
+ description: "Host port for the Wyoming TTS service. Leave at 10200 unless it " +
144
+ "collides with something else on this machine.",
145
+ },
146
+ advanced: {
147
+ type: "object",
148
+ title: "Advanced",
149
+ properties: {
150
+ bind: {
151
+ type: "string",
152
+ title: "Bind address",
153
+ enum: ["127.0.0.1", "0.0.0.0"],
154
+ default: DEFAULT_SETTINGS.advanced.bind,
155
+ description: "127.0.0.1 (recommended): only Signal K can reach the service. " +
156
+ "0.0.0.0 publishes the port on all interfaces so other machines " +
157
+ "(e.g. a Home Assistant box) can use it — Wyoming has no " +
158
+ "authentication, so firewall accordingly.",
159
+ },
160
+ memoryLimit: {
161
+ type: "string",
162
+ title: "Memory limit",
163
+ default: DEFAULT_SETTINGS.advanced.memoryLimit,
164
+ description: 'Hard container memory cap (swap disabled), e.g. "512m" or "1g".',
165
+ },
166
+ restartPolicy: {
167
+ type: "string",
168
+ title: "Restart policy",
169
+ enum: ["no", "unless-stopped", "always"],
170
+ default: DEFAULT_SETTINGS.advanced.restartPolicy,
171
+ description: "Container restart policy.",
172
+ },
173
+ updateVoices: {
174
+ type: "boolean",
175
+ title: "Update voices list on start",
176
+ default: DEFAULT_SETTINGS.advanced.updateVoices,
177
+ description: "Download the latest voices.json from HuggingFace at container " +
178
+ "start (needs internet; off by default for offline-first boats).",
179
+ },
180
+ sentenceSilence: {
181
+ type: "number",
182
+ title: "Sentence silence (seconds)",
183
+ description: "Optional seconds of silence appended after each sentence.",
184
+ },
185
+ lengthScale: {
186
+ type: "number",
187
+ title: "Length scale",
188
+ description: "Optional phoneme length scale; values above 1 slow speech down.",
189
+ },
190
+ },
191
+ },
192
+ },
193
+ };
194
+ export const UI_SCHEMA = {
195
+ advanced: {
196
+ "ui:options": { collapsible: true },
197
+ },
198
+ };
199
+ function isObject(v) {
200
+ return v !== null && typeof v === "object" && !Array.isArray(v);
201
+ }
202
+ function str(v) {
203
+ return typeof v === "string" && v.length > 0 ? v : undefined;
204
+ }
205
+ function num(v) {
206
+ return typeof v === "number" && Number.isFinite(v) ? v : undefined;
207
+ }
208
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * signalk-piper — Piper text-to-speech (Wyoming protocol) as a Signal K
3
+ * managed container plugin.
4
+ *
5
+ * Runs rhasspy/wyoming-piper via the signalk-container plugin, gates
6
+ * readiness on a real Wyoming `describe`/`info` exchange, health-checks the
7
+ * service, and advertises it on the `wyoming-service` PropertyValues
8
+ * convention for the signalk-wyoming orchestrator (or any other consumer).
9
+ */
10
+ import type { Plugin, ServerAPI } from "@signalk/server-api";
11
+ export default function createPlugin(app: ServerAPI): Plugin;
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAK7D,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,GAAG,EAAE,SAAS,GAAG,MAAM,CAyC3D"}
package/dist/index.js ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * signalk-piper — Piper text-to-speech (Wyoming protocol) as a Signal K
3
+ * managed container plugin.
4
+ *
5
+ * Runs rhasspy/wyoming-piper via the signalk-container plugin, gates
6
+ * readiness on a real Wyoming `describe`/`info` exchange, health-checks the
7
+ * service, and advertises it on the `wyoming-service` PropertyValues
8
+ * convention for the signalk-wyoming orchestrator (or any other consumer).
9
+ */
10
+ import { CONFIG_SCHEMA, UI_SCHEMA } from "./config.js";
11
+ import { PLUGIN_ID, PiperService } from "./service.js";
12
+ export default function createPlugin(app) {
13
+ // One PiperService per server process: Express routers cannot be
14
+ // deregistered, so routes must keep pointing at a live object across
15
+ // plugin stop/start cycles.
16
+ let service = null;
17
+ let lastRawConfig;
18
+ const getService = () => {
19
+ service ??= new PiperService(app);
20
+ return service;
21
+ };
22
+ const plugin = {
23
+ id: PLUGIN_ID,
24
+ name: "Piper TTS (Wyoming)",
25
+ description: "Piper text-to-speech in a managed container (Wyoming protocol) — " +
26
+ "spoken announcements for the signalk-wyoming voice assistant family, " +
27
+ "or standalone for Home Assistant and other Wyoming clients.",
28
+ schema: () => CONFIG_SCHEMA,
29
+ uiSchema: () => UI_SCHEMA,
30
+ start(config) {
31
+ lastRawConfig = config;
32
+ getService().start(config);
33
+ },
34
+ stop() {
35
+ return service?.stop() ?? Promise.resolve();
36
+ },
37
+ registerWithRouter(router) {
38
+ getService().registerRoutes(router, () => lastRawConfig);
39
+ },
40
+ };
41
+ return plugin;
42
+ }
43
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,YAAY,EAAmB,MAAM,cAAc,CAAC;AAExE,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,GAAc;IACjD,iEAAiE;IACjE,qEAAqE;IACrE,4BAA4B;IAC5B,IAAI,OAAO,GAAwB,IAAI,CAAC;IACxC,IAAI,aAAsB,CAAC;IAE3B,MAAM,UAAU,GAAG,GAAiB,EAAE;QACpC,OAAO,KAAK,IAAI,YAAY,CAAC,GAA4B,CAAC,CAAC;QAC3D,OAAO,OAAO,CAAC;IACjB,CAAC,CAAC;IAEF,MAAM,MAAM,GAAW;QACrB,EAAE,EAAE,SAAS;QACb,IAAI,EAAE,qBAAqB;QAC3B,WAAW,EACT,mEAAmE;YACnE,uEAAuE;YACvE,6DAA6D;QAE/D,MAAM,EAAE,GAAG,EAAE,CAAC,aAAkC;QAChD,QAAQ,EAAE,GAAG,EAAE,CAAC,SAA8B;QAE9C,KAAK,CAAC,MAAc;YAClB,aAAa,GAAG,MAAM,CAAC;YACvB,UAAU,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC7B,CAAC;QAED,IAAI;YACF,OAAO,OAAO,EAAE,IAAI,EAAE,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QAC9C,CAAC;QAED,kBAAkB,CAAC,MAAM;YACvB,UAAU,EAAE,CAAC,cAAc,CACzB,MAA+B,EAC/B,GAAG,EAAE,CAAC,aAAa,CACpB,CAAC;QACJ,CAAC;KACF,CAAC;IAEF,OAAO,MAAM,CAAC;AAChB,CAAC"}