@iobroker/assistant-satellite 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 +21 -0
- package/README.md +173 -0
- package/build/audio.d.ts +20 -0
- package/build/audio.js +127 -0
- package/build/audio.js.map +1 -0
- package/build/checks.d.ts +13 -0
- package/build/checks.js +150 -0
- package/build/checks.js.map +1 -0
- package/build/cli.d.ts +2 -0
- package/build/cli.js +134 -0
- package/build/cli.js.map +1 -0
- package/build/config.d.ts +46 -0
- package/build/config.js +36 -0
- package/build/config.js.map +1 -0
- package/build/index.d.ts +21 -0
- package/build/index.js +9 -0
- package/build/index.js.map +1 -0
- package/build/models.d.ts +15 -0
- package/build/models.js +101 -0
- package/build/models.js.map +1 -0
- package/build/mqtt.d.ts +6 -0
- package/build/mqtt.js +47 -0
- package/build/mqtt.js.map +1 -0
- package/build/protocol.d.ts +54 -0
- package/build/protocol.js +35 -0
- package/build/protocol.js.map +1 -0
- package/build/satellite.d.ts +54 -0
- package/build/satellite.js +384 -0
- package/build/satellite.js.map +1 -0
- package/build/service.d.ts +3 -0
- package/build/service.js +140 -0
- package/build/service.js.map +1 -0
- package/build/vad.d.ts +19 -0
- package/build/vad.js +55 -0
- package/build/vad.js.map +1 -0
- package/build/wakeword.d.ts +29 -0
- package/build/wakeword.js +179 -0
- package/build/wakeword.js.map +1 -0
- package/config.example.json +23 -0
- package/package.json +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ioBroker
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# @iobroker/assistant-satellite
|
|
2
|
+
|
|
3
|
+
A standalone **voice satellite** for the [`ioBroker.assistant`](https://github.com/ioBroker/ioBroker.assistant)
|
|
4
|
+
adapter. It runs the wake word on the device, streams the microphone to the adapter, and plays the
|
|
5
|
+
spoken reply — over the same UDP protocol as the Hannah satellite. **No ioBroker install required**;
|
|
6
|
+
runs on a bare Raspberry Pi via `npx`.
|
|
7
|
+
|
|
8
|
+
> STT → LLM → TTS all run in the adapter. The satellite only does: wake word · mic capture · playback.
|
|
9
|
+
|
|
10
|
+
## Requirements
|
|
11
|
+
|
|
12
|
+
- Node.js ≥ 18
|
|
13
|
+
- Audio backend (auto-selected by platform):
|
|
14
|
+
- **Linux**: ALSA tools — `sudo apt install alsa-utils` (`arecord`/`aplay`)
|
|
15
|
+
- **Windows / macOS**: **ffmpeg** (provides `ffmpeg`/`ffplay`) on the PATH
|
|
16
|
+
- A running `ioBroker.assistant` instance with the **Voice** server enabled
|
|
17
|
+
|
|
18
|
+
Wake-word inference uses `onnxruntime-node`, which ships prebuilt binaries for Linux (x64/arm64),
|
|
19
|
+
Windows (x64) and macOS (x64/arm64) — so the satellite runs on all three; only the audio backend differs.
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# 1. First run writes a default config and exits:
|
|
25
|
+
npx @iobroker/assistant-satellite
|
|
26
|
+
|
|
27
|
+
# 2. Edit config.json — at least set "host" (the ioBroker host) and your ALSA devices:
|
|
28
|
+
# "host": "192.168.1.129", "micDevice": "plughw:2,0", "speakerDevice": "plughw:2,0"
|
|
29
|
+
|
|
30
|
+
# 3. Run:
|
|
31
|
+
npx @iobroker/assistant-satellite config.json
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
On first run it downloads the OpenWakeWord models into `modelsDir`. Then say the wake word
|
|
35
|
+
(default **"hey jarvis"**) → speak → the answer is played back.
|
|
36
|
+
|
|
37
|
+
Find your ALSA device with `arecord -l` / `aplay -l` (→ `plughw:<card>,<device>`; the `plug` prefix
|
|
38
|
+
lets ALSA resample so 16 kHz capture works on any card).
|
|
39
|
+
|
|
40
|
+
### As a bare command
|
|
41
|
+
|
|
42
|
+
The package exposes an `assistant-satellite` binary (like `mocha`, `eslint`, … — via the `bin` field),
|
|
43
|
+
so you don't have to type `node build/cli.js`. Get it onto your PATH by installing globally:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm i -g @iobroker/assistant-satellite # after publish; or `npm i -g .` / `npm link` from a clone
|
|
47
|
+
assistant-satellite check config.json
|
|
48
|
+
assistant-satellite config.json
|
|
49
|
+
sudo assistant-satellite install config.json
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`npx @iobroker/assistant-satellite …` works without a global install. All subcommands
|
|
53
|
+
(`check` / `install` / `uninstall`) accept the same forms.
|
|
54
|
+
|
|
55
|
+
## Configuration (`config.json`)
|
|
56
|
+
|
|
57
|
+
| Key | Default | Meaning |
|
|
58
|
+
|----------------------------------|------------------|------------------------------------------------------|
|
|
59
|
+
| `logLevel` | `info` | `info` or `debug` (wake-word/mic diagnostics) |
|
|
60
|
+
| `device` / `room` | `satellite` / `` | identity reported to the adapter |
|
|
61
|
+
| `host` / `port` | `` / `7775` | adapter address (fixed → **no MQTT broker needed**) |
|
|
62
|
+
| `listenPort` | `7776` | UDP port the satellite receives TTS on |
|
|
63
|
+
| `mqttBroker` … | `` | optional discovery instead of a fixed `host` |
|
|
64
|
+
| `audioBackend` | `auto` | `auto` / `alsa` / `ffmpeg` |
|
|
65
|
+
| `micDevice` / `speakerDevice` | `default` | see per-platform notes below |
|
|
66
|
+
| `wakewordModel` | `hey_jarvis` | built-in name, URL, or local `.onnx` path |
|
|
67
|
+
| `wakewordThreshold` | `0.5` | detection sensitivity (0–1) |
|
|
68
|
+
| `silenceThreshold` / `silenceMs` | `300` / `800` | end-of-speech (VAD) |
|
|
69
|
+
| `minRecordMs` / `maxRecordMs` | `800` / `8000` | recording bounds |
|
|
70
|
+
|
|
71
|
+
Built-in wake words: `hey_jarvis`, `alexa`, `hey_mycroft`, `hey_rhasspy`.
|
|
72
|
+
|
|
73
|
+
### Audio devices per platform
|
|
74
|
+
|
|
75
|
+
- **Linux (alsa):** `micDevice`/`speakerDevice` = ALSA names, e.g. `plughw:2,0`. List with `arecord -l` /
|
|
76
|
+
`aplay -l` (the `plug` prefix lets ALSA resample so 16 kHz capture works on any card).
|
|
77
|
+
- **Windows (ffmpeg):** `micDevice` = the DirectShow device **name**, e.g. `Microphone (Poly Sync 20)`.
|
|
78
|
+
List with `ffmpeg -hide_banner -list_devices true -f dshow -i dummy`. Playback uses the default output
|
|
79
|
+
(`speakerDevice` is ignored via ffplay).
|
|
80
|
+
- **macOS (ffmpeg):** `micDevice` = the avfoundation audio **index**, e.g. `0`. List with
|
|
81
|
+
`ffmpeg -hide_banner -f avfoundation -list_devices true -i ""`.
|
|
82
|
+
|
|
83
|
+
## Run as a service (systemd)
|
|
84
|
+
|
|
85
|
+
The satellite already retries registration and re-registers automatically if the adapter restarts, so
|
|
86
|
+
`systemd` only needs to keep the process alive.
|
|
87
|
+
|
|
88
|
+
First verify everything is present (Node, systemd, root, audio tools, **live mic-in / speaker-out test**,
|
|
89
|
+
config) — `install` runs these automatically and aborts on failure:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
node build/cli.js check config.json # dry-run of the same checks
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Then install itself as a service (Linux, needs `sudo`):
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# from a clone (build first), pointing at your config:
|
|
99
|
+
npm run build
|
|
100
|
+
sudo node build/cli.js install config.json # add --force to install despite check failures
|
|
101
|
+
# …or, after `npm i -g @iobroker/assistant-satellite`:
|
|
102
|
+
sudo assistant-satellite install /path/to/config.json
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`install` writes `/etc/systemd/system/assistant-satellite.service` (running as **your** user, in the
|
|
106
|
+
`audio` group, with absolute paths to `node`, `cli.js` and the config), then `daemon-reload` +
|
|
107
|
+
`enable --now`. Manage it with:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
journalctl -u assistant-satellite -f # logs
|
|
111
|
+
sudo systemctl restart assistant-satellite
|
|
112
|
+
sudo node build/cli.js uninstall # stop + remove (or: sudo assistant-satellite uninstall)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
<details><summary>Prefer a handwritten unit file?</summary>
|
|
116
|
+
|
|
117
|
+
```ini
|
|
118
|
+
[Unit]
|
|
119
|
+
Description=ioBroker assistant satellite
|
|
120
|
+
After=network-online.target sound.target
|
|
121
|
+
Wants=network-online.target
|
|
122
|
+
|
|
123
|
+
[Service]
|
|
124
|
+
WorkingDirectory=/opt/assistant-satellite
|
|
125
|
+
ExecStart=/usr/bin/node /opt/assistant-satellite/build/cli.js /opt/assistant-satellite/config.json
|
|
126
|
+
Restart=always
|
|
127
|
+
RestartSec=5
|
|
128
|
+
User=iob
|
|
129
|
+
SupplementaryGroups=audio
|
|
130
|
+
|
|
131
|
+
[Install]
|
|
132
|
+
WantedBy=multi-user.target
|
|
133
|
+
```
|
|
134
|
+
</details>
|
|
135
|
+
|
|
136
|
+
## Status
|
|
137
|
+
|
|
138
|
+
Early scaffold. The audio/UDP/registration/playback pipeline follows the working Hannah satellite;
|
|
139
|
+
the **OpenWakeWord** inference (`src/wakeword.ts`) is a from-scratch Node port and its frame math /
|
|
140
|
+
threshold should be validated on the target device.
|
|
141
|
+
|
|
142
|
+
## Changelog
|
|
143
|
+
<!--
|
|
144
|
+
Placeholder for the next version (at the beginning of the line):
|
|
145
|
+
### **WORK IN PROGRESS**
|
|
146
|
+
-->
|
|
147
|
+
### **WORK IN PROGRESS**
|
|
148
|
+
* (@GermanBluefox) Initial commit
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
MIT License
|
|
154
|
+
|
|
155
|
+
Copyright (c) 2025-2026 Denis Haev <dogafox@gmail.com>
|
|
156
|
+
|
|
157
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
158
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
159
|
+
in the Software without restriction, including without limitation the rights
|
|
160
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
161
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
162
|
+
furnished to do so, subject to the following conditions:
|
|
163
|
+
|
|
164
|
+
The above copyright notice and this permission notice shall be included in all
|
|
165
|
+
copies or substantial portions of the Software.
|
|
166
|
+
|
|
167
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
168
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
169
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
170
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
171
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
172
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
173
|
+
SOFTWARE.
|
package/build/audio.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Logger } from './index';
|
|
2
|
+
export type AudioBackend = 'alsa' | 'ffmpeg';
|
|
3
|
+
/** Resolve 'auto' → alsa on Linux, ffmpeg elsewhere. */
|
|
4
|
+
export declare function resolveBackend(pref: string): AudioBackend;
|
|
5
|
+
/** ffmpeg capture input args per platform (device = dshow name / avfoundation index / ALSA name). */
|
|
6
|
+
export declare function ffmpegInput(device: string): string[];
|
|
7
|
+
/** Continuous microphone capture at 16 kHz mono 16-bit; emits raw PCM chunks. */
|
|
8
|
+
export declare class Mic {
|
|
9
|
+
private readonly backend;
|
|
10
|
+
private readonly device;
|
|
11
|
+
private readonly log;
|
|
12
|
+
private proc;
|
|
13
|
+
constructor(backend: AudioBackend, device: string, log: Logger);
|
|
14
|
+
start(onData: (pcm: Buffer) => void): void;
|
|
15
|
+
stop(): void;
|
|
16
|
+
}
|
|
17
|
+
/** Play raw mono 16-bit PCM at the given rate; resolves when playback finishes. */
|
|
18
|
+
export declare function playPcm(pcm: Buffer, sampleRate: number, backend: AudioBackend, device: string, log: Logger): Promise<void>;
|
|
19
|
+
/** A short rising "listening" beep (mono 16-bit @ 16 kHz), synthesised once. */
|
|
20
|
+
export declare function pling(): Buffer;
|
package/build/audio.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Mic = void 0;
|
|
4
|
+
exports.resolveBackend = resolveBackend;
|
|
5
|
+
exports.ffmpegInput = ffmpegInput;
|
|
6
|
+
exports.playPcm = playPcm;
|
|
7
|
+
exports.pling = pling;
|
|
8
|
+
/**
|
|
9
|
+
* Audio I/O with two backends:
|
|
10
|
+
* - 'alsa' : spawn `arecord`/`aplay` — robust on a Pi, no native build (Linux only).
|
|
11
|
+
* - 'ffmpeg': spawn `ffmpeg`/`ffplay` — cross-platform (Windows / macOS / Linux).
|
|
12
|
+
*
|
|
13
|
+
* Capture is always 16 kHz mono 16-bit; use a `plughw:CARD,DEV` device (ALSA) so it resamples.
|
|
14
|
+
*/
|
|
15
|
+
const node_child_process_1 = require("node:child_process");
|
|
16
|
+
const protocol_1 = require("./protocol");
|
|
17
|
+
/** Resolve 'auto' → alsa on Linux, ffmpeg elsewhere. */
|
|
18
|
+
function resolveBackend(pref) {
|
|
19
|
+
if (pref === 'alsa' || pref === 'ffmpeg') {
|
|
20
|
+
return pref;
|
|
21
|
+
}
|
|
22
|
+
return process.platform === 'linux' ? 'alsa' : 'ffmpeg';
|
|
23
|
+
}
|
|
24
|
+
/** ffmpeg capture input args per platform (device = dshow name / avfoundation index / ALSA name). */
|
|
25
|
+
function ffmpegInput(device) {
|
|
26
|
+
if (process.platform === 'win32') {
|
|
27
|
+
return ['-f', 'dshow', '-i', `audio=${device || 'default'}`];
|
|
28
|
+
}
|
|
29
|
+
if (process.platform === 'darwin') {
|
|
30
|
+
return ['-f', 'avfoundation', '-i', `:${device || '0'}`];
|
|
31
|
+
}
|
|
32
|
+
return ['-f', 'alsa', '-i', device || 'default'];
|
|
33
|
+
}
|
|
34
|
+
/** Continuous microphone capture at 16 kHz mono 16-bit; emits raw PCM chunks. */
|
|
35
|
+
class Mic {
|
|
36
|
+
backend;
|
|
37
|
+
device;
|
|
38
|
+
log;
|
|
39
|
+
proc = null;
|
|
40
|
+
constructor(backend, device, log) {
|
|
41
|
+
this.backend = backend;
|
|
42
|
+
this.device = device;
|
|
43
|
+
this.log = log;
|
|
44
|
+
}
|
|
45
|
+
start(onData) {
|
|
46
|
+
const [cmd, args] = this.backend === 'ffmpeg'
|
|
47
|
+
? [
|
|
48
|
+
'ffmpeg',
|
|
49
|
+
[
|
|
50
|
+
'-hide_banner',
|
|
51
|
+
'-loglevel',
|
|
52
|
+
'error',
|
|
53
|
+
...ffmpegInput(this.device),
|
|
54
|
+
'-ac',
|
|
55
|
+
'1',
|
|
56
|
+
'-ar',
|
|
57
|
+
String(protocol_1.AUDIO_SAMPLE_RATE),
|
|
58
|
+
'-f',
|
|
59
|
+
's16le',
|
|
60
|
+
'-',
|
|
61
|
+
],
|
|
62
|
+
]
|
|
63
|
+
: [
|
|
64
|
+
'arecord',
|
|
65
|
+
[
|
|
66
|
+
'-q',
|
|
67
|
+
'-t',
|
|
68
|
+
'raw',
|
|
69
|
+
'-f',
|
|
70
|
+
'S16_LE',
|
|
71
|
+
'-c',
|
|
72
|
+
'1',
|
|
73
|
+
'-r',
|
|
74
|
+
String(protocol_1.AUDIO_SAMPLE_RATE),
|
|
75
|
+
...(this.device && this.device !== 'default' ? ['-D', this.device] : []),
|
|
76
|
+
],
|
|
77
|
+
];
|
|
78
|
+
this.proc = (0, node_child_process_1.spawn)(cmd, args);
|
|
79
|
+
this.proc.stdout?.on('data', (d) => onData(d));
|
|
80
|
+
this.proc.stderr?.on('data', (d) => this.log.debug(`${cmd}: ${d.toString().trim()}`));
|
|
81
|
+
this.proc.on('error', e => this.log.error(`${cmd} failed: ${e.message} — is it installed? ` +
|
|
82
|
+
(this.backend === 'ffmpeg' ? '(install ffmpeg)' : "(sudo apt install alsa-utils)")));
|
|
83
|
+
this.log.info(`Microphone capture started (${this.backend}: ${this.device || 'default'} @ ${protocol_1.AUDIO_SAMPLE_RATE} Hz).`);
|
|
84
|
+
}
|
|
85
|
+
stop() {
|
|
86
|
+
this.proc?.kill();
|
|
87
|
+
this.proc = null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
exports.Mic = Mic;
|
|
91
|
+
/** Play raw mono 16-bit PCM at the given rate; resolves when playback finishes. */
|
|
92
|
+
function playPcm(pcm, sampleRate, backend, device, log) {
|
|
93
|
+
return new Promise(resolve => {
|
|
94
|
+
const [cmd, args] = backend === 'ffmpeg'
|
|
95
|
+
? [
|
|
96
|
+
'ffplay',
|
|
97
|
+
['-hide_banner', '-loglevel', 'error', '-nodisp', '-autoexit', '-f', 's16le', '-ar', String(sampleRate), '-ch_layout', 'mono', '-i', '-'],
|
|
98
|
+
]
|
|
99
|
+
: [
|
|
100
|
+
'aplay',
|
|
101
|
+
['-q', '-t', 'raw', '-f', 'S16_LE', '-c', '1', '-r', String(sampleRate), ...(device && device !== 'default' ? ['-D', device] : [])],
|
|
102
|
+
];
|
|
103
|
+
const proc = (0, node_child_process_1.spawn)(cmd, args);
|
|
104
|
+
proc.on('close', () => resolve());
|
|
105
|
+
proc.on('error', e => {
|
|
106
|
+
log.error(`${cmd} failed: ${e.message}`);
|
|
107
|
+
resolve();
|
|
108
|
+
});
|
|
109
|
+
proc.stdin?.end(pcm);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/** A short rising "listening" beep (mono 16-bit @ 16 kHz), synthesised once. */
|
|
113
|
+
function pling() {
|
|
114
|
+
const rate = protocol_1.AUDIO_SAMPLE_RATE;
|
|
115
|
+
const dur = 0.18;
|
|
116
|
+
const n = Math.floor(rate * dur);
|
|
117
|
+
const buf = Buffer.alloc(n * 2);
|
|
118
|
+
for (let i = 0; i < n; i++) {
|
|
119
|
+
const t = i / rate;
|
|
120
|
+
const freq = 880 + (1320 - 880) * (i / n);
|
|
121
|
+
const fade = Math.sin((Math.PI * i) / n);
|
|
122
|
+
const val = Math.max(-32768, Math.min(32767, Math.round(32767 * 0.6 * fade * Math.sin(2 * Math.PI * freq * t))));
|
|
123
|
+
buf.writeInt16LE(val, i * 2);
|
|
124
|
+
}
|
|
125
|
+
return buf;
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=audio.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio.js","sourceRoot":"","sources":["../src/audio.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type AudioBackend } from './audio';
|
|
2
|
+
import type { SatelliteConfig } from './config';
|
|
3
|
+
import type { Logger } from './index';
|
|
4
|
+
export type CheckStatus = 'ok' | 'warn' | 'fail';
|
|
5
|
+
export interface CheckResult {
|
|
6
|
+
name: string;
|
|
7
|
+
status: CheckStatus;
|
|
8
|
+
detail: string;
|
|
9
|
+
}
|
|
10
|
+
/** Run all checks. `forService` adds the systemd/root prerequisites. */
|
|
11
|
+
export declare function runChecks(cfg: SatelliteConfig, backend: AudioBackend, forService: boolean): CheckResult[];
|
|
12
|
+
/** Print the results; returns false if any check failed. */
|
|
13
|
+
export declare function printChecks(results: CheckResult[], log: Logger): boolean;
|
package/build/checks.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runChecks = runChecks;
|
|
37
|
+
exports.printChecks = printChecks;
|
|
38
|
+
/**
|
|
39
|
+
* Preflight checks: run before installing the service (and via the `check` subcommand) to verify
|
|
40
|
+
* everything is present — Node, systemd, root rights, audio tools, and functional mic-in / speaker-out.
|
|
41
|
+
*/
|
|
42
|
+
const node_child_process_1 = require("node:child_process");
|
|
43
|
+
const fs = __importStar(require("node:fs"));
|
|
44
|
+
const audio_1 = require("./audio");
|
|
45
|
+
const ok = (name, detail = '') => ({ name, status: 'ok', detail });
|
|
46
|
+
const warn = (name, detail) => ({ name, status: 'warn', detail });
|
|
47
|
+
const fail = (name, detail) => ({ name, status: 'fail', detail });
|
|
48
|
+
function which(cmd) {
|
|
49
|
+
try {
|
|
50
|
+
(0, node_child_process_1.execFileSync)(process.platform === 'win32' ? 'where' : 'which', [cmd], { stdio: 'ignore' });
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const firstLine = (s) => s.split('\n')[0].slice(0, 140);
|
|
58
|
+
/** Capture ~1 s from the mic and confirm real bytes come out. */
|
|
59
|
+
function checkCapture(cfg, backend) {
|
|
60
|
+
try {
|
|
61
|
+
let out;
|
|
62
|
+
if (backend === 'ffmpeg') {
|
|
63
|
+
out = (0, node_child_process_1.execFileSync)('ffmpeg', ['-hide_banner', '-loglevel', 'error', ...(0, audio_1.ffmpegInput)(cfg.micDevice), '-t', '1', '-ac', '1', '-ar', '16000', '-f', 's16le', '-'], { timeout: 8000, maxBuffer: 1 << 22 });
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const args = ['-q', '-f', 'S16_LE', '-r', '16000', '-c', '1', '-d', '1', '-t', 'raw'];
|
|
67
|
+
if (cfg.micDevice && cfg.micDevice !== 'default') {
|
|
68
|
+
args.push('-D', cfg.micDevice);
|
|
69
|
+
}
|
|
70
|
+
out = (0, node_child_process_1.execFileSync)('arecord', args, { timeout: 8000, maxBuffer: 1 << 22 });
|
|
71
|
+
}
|
|
72
|
+
return out.length > 1000
|
|
73
|
+
? ok('audio:in', `captured ${out.length} bytes from '${cfg.micDevice}'`)
|
|
74
|
+
: fail('audio:in', `captured only ${out.length} bytes — wrong device or no signal`);
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
return fail('audio:in', `capture failed (device busy/wrong?): ${firstLine(e.message)}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Play 0.2 s of silence to confirm the output device opens. */
|
|
81
|
+
function checkPlayback(cfg, backend) {
|
|
82
|
+
const silence = Buffer.alloc(Math.round((16000 * 2) / 5)); // 0.2 s @ 16 kHz mono 16-bit
|
|
83
|
+
try {
|
|
84
|
+
if (backend === 'ffmpeg') {
|
|
85
|
+
(0, node_child_process_1.execFileSync)('ffplay', ['-hide_banner', '-loglevel', 'error', '-nodisp', '-autoexit', '-f', 's16le', '-ar', '16000', '-ch_layout', 'mono', '-i', '-'], { input: silence, timeout: 8000 });
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
const args = ['-q', '-f', 'S16_LE', '-r', '16000', '-c', '1'];
|
|
89
|
+
if (cfg.speakerDevice && cfg.speakerDevice !== 'default') {
|
|
90
|
+
args.push('-D', cfg.speakerDevice);
|
|
91
|
+
}
|
|
92
|
+
(0, node_child_process_1.execFileSync)('aplay', args, { input: silence, timeout: 8000 });
|
|
93
|
+
}
|
|
94
|
+
return ok('audio:out', `output device '${cfg.speakerDevice}' opened`);
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
return fail('audio:out', `playback failed (device busy/wrong?): ${firstLine(e.message)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Run all checks. `forService` adds the systemd/root prerequisites. */
|
|
101
|
+
function runChecks(cfg, backend, forService) {
|
|
102
|
+
const results = [];
|
|
103
|
+
const major = Number(process.versions.node.split('.')[0]);
|
|
104
|
+
results.push(major >= 18 ? ok('node', `v${process.versions.node}`) : fail('node', `v${process.versions.node} (need ≥ 18)`));
|
|
105
|
+
if (forService) {
|
|
106
|
+
results.push(process.platform === 'linux'
|
|
107
|
+
? ok('platform', 'linux')
|
|
108
|
+
: fail('platform', `${process.platform} — systemd install is Linux-only`));
|
|
109
|
+
results.push(which('systemctl') ? ok('systemctl', 'found') : fail('systemctl', 'not found'));
|
|
110
|
+
const root = typeof process.getuid === 'function' && process.getuid() === 0;
|
|
111
|
+
results.push(root ? ok('root', 'running as root') : warn('root', 'not root — run install with sudo'));
|
|
112
|
+
}
|
|
113
|
+
for (const t of backend === 'ffmpeg' ? ['ffmpeg', 'ffplay'] : ['arecord', 'aplay']) {
|
|
114
|
+
results.push(which(t)
|
|
115
|
+
? ok(`tool:${t}`, 'found')
|
|
116
|
+
: fail(`tool:${t}`, `not found — install ${backend === 'ffmpeg' ? 'ffmpeg' : 'alsa-utils'}`));
|
|
117
|
+
}
|
|
118
|
+
results.push(checkCapture(cfg, backend));
|
|
119
|
+
results.push(checkPlayback(cfg, backend));
|
|
120
|
+
results.push(cfg.host || cfg.mqttBroker
|
|
121
|
+
? ok('config:server', cfg.host ? `host ${cfg.host}:${cfg.port}` : `discovery via ${cfg.mqttBroker}`)
|
|
122
|
+
: fail('config:server', 'set "host" (or "mqttBroker") so the satellite can reach the adapter'));
|
|
123
|
+
try {
|
|
124
|
+
fs.mkdirSync(cfg.modelsDir, { recursive: true });
|
|
125
|
+
fs.accessSync(cfg.modelsDir, fs.constants.W_OK);
|
|
126
|
+
results.push(ok('models', `'${cfg.modelsDir}' writable`));
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
results.push(warn('models', `'${cfg.modelsDir}' not writable — model download may fail`));
|
|
130
|
+
}
|
|
131
|
+
return results;
|
|
132
|
+
}
|
|
133
|
+
/** Print the results; returns false if any check failed. */
|
|
134
|
+
function printChecks(results, log) {
|
|
135
|
+
const label = { ok: 'OK ', warn: 'WARN', fail: 'FAIL' };
|
|
136
|
+
for (const c of results) {
|
|
137
|
+
const line = `[${label[c.status]}] ${c.name}${c.detail ? ` — ${c.detail}` : ''}`;
|
|
138
|
+
if (c.status === 'fail') {
|
|
139
|
+
log.error(line);
|
|
140
|
+
}
|
|
141
|
+
else if (c.status === 'warn') {
|
|
142
|
+
log.warn(line);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
log.info(line);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return !results.some(c => c.status === 'fail');
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=checks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checks.js","sourceRoot":"","sources":["../src/checks.ts"],"names":[],"mappings":"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"}
|
package/build/cli.d.ts
ADDED
package/build/cli.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
/**
|
|
38
|
+
* CLI entry point:
|
|
39
|
+
* assistant-satellite [config.json] run the satellite
|
|
40
|
+
* assistant-satellite install [config.json] install + start a systemd service (Linux, needs sudo)
|
|
41
|
+
* assistant-satellite uninstall stop + remove the systemd service (needs sudo)
|
|
42
|
+
*/
|
|
43
|
+
const fs = __importStar(require("node:fs"));
|
|
44
|
+
const index_1 = require("./index");
|
|
45
|
+
const service_1 = require("./service");
|
|
46
|
+
const checks_1 = require("./checks");
|
|
47
|
+
const audio_1 = require("./audio");
|
|
48
|
+
// Debug is off until we know the config's logLevel; the DEBUG env var forces it on regardless.
|
|
49
|
+
let debugEnabled = !!process.env.DEBUG;
|
|
50
|
+
const log = {
|
|
51
|
+
info: (m) => console.log(`[INFO] ${m}`),
|
|
52
|
+
warn: (m) => console.warn(`[WARN] ${m}`),
|
|
53
|
+
error: (m) => console.error(`[ERROR] ${m}`),
|
|
54
|
+
debug: (m) => {
|
|
55
|
+
if (debugEnabled) {
|
|
56
|
+
console.log(`[DEBUG] ${m}`);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
61
|
+
const configArg = rest.find(a => !a.startsWith('--')) || 'config.json';
|
|
62
|
+
// `check` — run preflight checks (rights, audio in/out, tools, config) and exit.
|
|
63
|
+
if (command === 'check') {
|
|
64
|
+
if (!fs.existsSync(configArg)) {
|
|
65
|
+
log.error(`config not found: ${configArg}`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const c = (0, index_1.loadConfig)(JSON.parse(fs.readFileSync(configArg, 'utf8')));
|
|
70
|
+
const passed = (0, checks_1.printChecks)((0, checks_1.runChecks)(c, (0, audio_1.resolveBackend)(c.audioBackend), true), log);
|
|
71
|
+
process.exit(passed ? 0 : 1);
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
log.error(e.message);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Service management subcommands (Linux/systemd). `install` runs the checks first.
|
|
79
|
+
if (command === 'install' || command === '--install') {
|
|
80
|
+
try {
|
|
81
|
+
(0, service_1.installService)(configArg, log, rest.includes('--force'));
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
log.error(e.message);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (command === 'uninstall' || command === '--uninstall') {
|
|
90
|
+
try {
|
|
91
|
+
(0, service_1.uninstallService)(log);
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
log.error(e.message);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const configPath = command || 'config.json';
|
|
100
|
+
if (!fs.existsSync(configPath)) {
|
|
101
|
+
fs.writeFileSync(configPath, `${JSON.stringify(index_1.DEFAULT_CONFIG, null, 4)}\n`);
|
|
102
|
+
log.info(`No config found — wrote defaults to ${configPath}.`);
|
|
103
|
+
log.info('Edit it (at least set "host" and "device"), then run again.');
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
let cfg;
|
|
107
|
+
try {
|
|
108
|
+
cfg = (0, index_1.loadConfig)(JSON.parse(fs.readFileSync(configPath, 'utf8')));
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
log.error(`Cannot read ${configPath}: ${e.message}`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
if (cfg.logLevel === 'debug') {
|
|
115
|
+
debugEnabled = true;
|
|
116
|
+
}
|
|
117
|
+
const satellite = new index_1.Satellite(cfg, { log, onStatus: s => log.debug(`status: ${s}`) });
|
|
118
|
+
satellite.start().catch(e => {
|
|
119
|
+
log.error(e.message);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
});
|
|
122
|
+
let stopping = false;
|
|
123
|
+
async function shutdown() {
|
|
124
|
+
if (stopping) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
stopping = true;
|
|
128
|
+
log.info('Shutting down …');
|
|
129
|
+
await satellite.stop();
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
process.on('SIGINT', () => void shutdown());
|
|
133
|
+
process.on('SIGTERM', () => void shutdown());
|
|
134
|
+
//# sourceMappingURL=cli.js.map
|