@iobroker/assistant-satellite 0.1.1 → 0.1.2
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 +1 -1
- package/build/audio.d.ts +6 -1
- package/build/audio.js +50 -3
- package/build/audio.js.map +1 -1
- package/build/probe.js +1 -1
- package/build/probe.js.map +1 -1
- package/build/satellite.js +1 -1
- package/build/satellite.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -143,7 +143,7 @@ threshold should be validated on the target device.
|
|
|
143
143
|
Placeholder for the next version (at the beginning of the line):
|
|
144
144
|
### **WORK IN PROGRESS**
|
|
145
145
|
-->
|
|
146
|
-
### 0.1.
|
|
146
|
+
### 0.1.2 (2026-07-05)
|
|
147
147
|
* (@GermanBluefox) Added Wake-word probe
|
|
148
148
|
|
|
149
149
|
### 0.0.4 (2026-07-05)
|
package/build/audio.d.ts
CHANGED
|
@@ -18,9 +18,14 @@ export declare class Mic {
|
|
|
18
18
|
private readonly device;
|
|
19
19
|
private readonly log;
|
|
20
20
|
private proc;
|
|
21
|
+
/** Set by stop() so the expected exit isn't reported as an error. */
|
|
22
|
+
private stopping;
|
|
23
|
+
/** Last stderr line from the capture process (surfaced if it dies unexpectedly). */
|
|
24
|
+
private lastErr;
|
|
21
25
|
constructor(backend: AudioBackend, device: string, log: Logger);
|
|
22
26
|
start(onData: (pcm: Buffer) => void): void;
|
|
23
|
-
|
|
27
|
+
/** Stop capture and wait for the process to actually exit so ALSA/the device is released. */
|
|
28
|
+
stop(): Promise<void>;
|
|
24
29
|
}
|
|
25
30
|
/**
|
|
26
31
|
* Play raw mono 16-bit PCM at the given rate. Returns the child process (so playback can be cancelled
|
package/build/audio.js
CHANGED
|
@@ -37,12 +37,18 @@ class Mic {
|
|
|
37
37
|
device;
|
|
38
38
|
log;
|
|
39
39
|
proc = null;
|
|
40
|
+
/** Set by stop() so the expected exit isn't reported as an error. */
|
|
41
|
+
stopping = false;
|
|
42
|
+
/** Last stderr line from the capture process (surfaced if it dies unexpectedly). */
|
|
43
|
+
lastErr = '';
|
|
40
44
|
constructor(backend, device, log) {
|
|
41
45
|
this.backend = backend;
|
|
42
46
|
this.device = device;
|
|
43
47
|
this.log = log;
|
|
44
48
|
}
|
|
45
49
|
start(onData) {
|
|
50
|
+
this.stopping = false;
|
|
51
|
+
this.lastErr = '';
|
|
46
52
|
const [cmd, args] = this.backend === 'ffmpeg'
|
|
47
53
|
? [
|
|
48
54
|
'ffmpeg',
|
|
@@ -77,13 +83,54 @@ class Mic {
|
|
|
77
83
|
];
|
|
78
84
|
this.proc = (0, node_child_process_1.spawn)(cmd, args);
|
|
79
85
|
this.proc.stdout?.on('data', (d) => onData(d));
|
|
80
|
-
this.proc.stderr?.on('data', (d) =>
|
|
86
|
+
this.proc.stderr?.on('data', (d) => {
|
|
87
|
+
this.lastErr = d.toString().trim();
|
|
88
|
+
this.log.debug(`${cmd}: ${this.lastErr}`);
|
|
89
|
+
});
|
|
81
90
|
this.proc.on('error', e => this.log.error(`${cmd} failed: ${e.message} — is it installed? ${this.backend === 'ffmpeg' ? '(install ffmpeg)' : '(sudo apt install alsa-utils)'}`));
|
|
91
|
+
// A healthy capture runs until stop(); an early exit means the device could not be opened.
|
|
92
|
+
this.proc.on('close', code => {
|
|
93
|
+
if (this.stopping) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const hint = this.backend === 'alsa'
|
|
97
|
+
? " Pick a real capture device — run 'arecord -l' and set micDevice to e.g. 'plughw:1,0'. The ALSA 'default' device often has no capture slave."
|
|
98
|
+
: ' Check the microphone device / that ffmpeg can open it.';
|
|
99
|
+
this.log.warn(`Microphone capture stopped unexpectedly (${cmd} exit ${code ?? '?'}).${this.lastErr ? ` Last error: ${this.lastErr}.` : ''}${hint}`);
|
|
100
|
+
});
|
|
82
101
|
this.log.info(`Microphone capture started (${this.backend}: ${this.device || 'default'} @ ${protocol_1.AUDIO_SAMPLE_RATE} Hz).`);
|
|
83
102
|
}
|
|
84
|
-
|
|
85
|
-
|
|
103
|
+
/** Stop capture and wait for the process to actually exit so ALSA/the device is released. */
|
|
104
|
+
async stop() {
|
|
105
|
+
this.stopping = true;
|
|
106
|
+
const proc = this.proc;
|
|
86
107
|
this.proc = null;
|
|
108
|
+
if (!proc) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
await new Promise(resolve => {
|
|
112
|
+
let done = false;
|
|
113
|
+
const finish = () => {
|
|
114
|
+
if (!done) {
|
|
115
|
+
done = true;
|
|
116
|
+
resolve();
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
proc.once('close', finish);
|
|
120
|
+
proc.kill(); // SIGTERM
|
|
121
|
+
// Safety net: force-kill if it does not exit promptly, then resolve anyway.
|
|
122
|
+
setTimeout(() => {
|
|
123
|
+
if (!done) {
|
|
124
|
+
try {
|
|
125
|
+
proc.kill('SIGKILL');
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
/* already gone */
|
|
129
|
+
}
|
|
130
|
+
finish();
|
|
131
|
+
}
|
|
132
|
+
}, 1500);
|
|
133
|
+
});
|
|
87
134
|
}
|
|
88
135
|
}
|
|
89
136
|
exports.Mic = Mic;
|
package/build/audio.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audio.js","sourceRoot":"","sources":["../src/audio.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Audio I/O with two backends:\n * - 'alsa' : spawn `arecord`/`aplay` — robust on a Pi, no native build (Linux only).\n * - 'ffmpeg': spawn `ffmpeg`/`ffplay` — cross-platform (Windows / macOS / Linux).\n *\n * Capture is always 16 kHz mono 16-bit; use a `plughw:CARD,DEV` device (ALSA) so it resamples.\n */\nimport { spawn, type ChildProcess } from 'node:child_process';\nimport { AUDIO_SAMPLE_RATE } from './protocol';\nimport type { Logger } from './index';\n\nexport type AudioBackend = 'alsa' | 'ffmpeg';\n\n/** Resolve 'auto' → alsa on Linux, ffmpeg elsewhere. */\nexport function resolveBackend(pref: string): AudioBackend {\n if (pref === 'alsa' || pref === 'ffmpeg') {\n return pref;\n }\n return process.platform === 'linux' ? 'alsa' : 'ffmpeg';\n}\n\n/** ffmpeg capture input args per platform (device = dshow name / avfoundation index / ALSA name). */\nexport function ffmpegInput(device: string): string[] {\n if (process.platform === 'win32') {\n return ['-f', 'dshow', '-i', `audio=${device || 'default'}`];\n }\n if (process.platform === 'darwin') {\n return ['-f', 'avfoundation', '-i', `:${device || '0'}`];\n }\n return ['-f', 'alsa', '-i', device || 'default'];\n}\n\n/** Continuous microphone capture at 16 kHz mono 16-bit; emits raw PCM chunks. */\nexport class Mic {\n private proc: ChildProcess | null = null;\n\n constructor(\n private readonly backend: AudioBackend,\n private readonly device: string,\n private readonly log: Logger,\n ) {}\n\n start(onData: (pcm: Buffer) => void): void {\n const [cmd, args] =\n this.backend === 'ffmpeg'\n ? ([\n 'ffmpeg',\n [\n '-hide_banner',\n '-loglevel',\n 'error',\n ...ffmpegInput(this.device),\n '-ac',\n '1',\n '-ar',\n String(AUDIO_SAMPLE_RATE),\n '-f',\n 's16le',\n '-',\n ],\n ] as const)\n : ([\n 'arecord',\n [\n '-q',\n '-t',\n 'raw',\n '-f',\n 'S16_LE',\n '-c',\n '1',\n '-r',\n String(AUDIO_SAMPLE_RATE),\n ...(this.device && this.device !== 'default' ? ['-D', this.device] : []),\n ],\n ] as const);\n\n this.proc = spawn(cmd, args);\n this.proc.stdout?.on('data', (d: Buffer) => onData(d));\n this.proc.stderr?.on('data', (d: Buffer) => this.log.debug(`${cmd}: ${d.toString().trim()}`));\n this.proc.on('error', e =>\n this.log.error(\n `${cmd} failed: ${e.message} — is it installed? ${this.backend === 'ffmpeg' ? '(install ffmpeg)' : '(sudo apt install alsa-utils)'}`,\n ),\n );\n this.log.info(\n `Microphone capture started (${this.backend}: ${this.device || 'default'} @ ${AUDIO_SAMPLE_RATE} Hz).`,\n );\n }\n\n stop(): void {\n this.proc?.kill();\n this.proc = null;\n }\n}\n\n/**\n * Play raw mono 16-bit PCM at the given rate. Returns the child process (so playback can be cancelled\n * for barge-in) and a `done` promise that resolves when playback finishes or is killed.\n */\nexport function playPcm(\n pcm: Buffer,\n sampleRate: number,\n backend: AudioBackend,\n device: string,\n log: Logger,\n): { proc: ChildProcess; done: Promise<void> } {\n const [cmd, args] =\n backend === 'ffmpeg'\n ? ([\n 'ffplay',\n [\n '-hide_banner',\n '-loglevel',\n 'error',\n '-nodisp',\n '-autoexit',\n '-f',\n 's16le',\n '-ar',\n String(sampleRate),\n '-ch_layout',\n 'mono',\n '-i',\n '-',\n ],\n ] as const)\n : ([\n 'aplay',\n [\n '-q',\n '-t',\n 'raw',\n '-f',\n 'S16_LE',\n '-c',\n '1',\n '-r',\n String(sampleRate),\n ...(device && device !== 'default' ? ['-D', device] : []),\n ],\n ] as const);\n\n const proc = spawn(cmd, args);\n const done = new Promise<void>(resolve => {\n proc.on('close', () => resolve());\n proc.on('error', e => {\n log.error(`${cmd} failed: ${e.message}`);\n resolve();\n });\n });\n proc.stdin?.end(pcm);\n return { proc, done };\n}\n\n/** A short rising \"listening\" beep (mono 16-bit @ 16 kHz), synthesised once. */\nexport function pling(): Buffer {\n const rate = AUDIO_SAMPLE_RATE;\n const dur = 0.18;\n const n = Math.floor(rate * dur);\n const buf = Buffer.alloc(n * 2);\n for (let i = 0; i < n; i++) {\n const t = i / rate;\n const freq = 880 + (1320 - 880) * (i / n);\n const fade = Math.sin((Math.PI * i) / n);\n const val = Math.max(\n -32768,\n Math.min(32767, Math.round(32767 * 0.6 * fade * Math.sin(2 * Math.PI * freq * t))),\n );\n buf.writeInt16LE(val, i * 2);\n }\n return buf;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"audio.js","sourceRoot":"","sources":["../src/audio.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Audio I/O with two backends:\n * - 'alsa' : spawn `arecord`/`aplay` — robust on a Pi, no native build (Linux only).\n * - 'ffmpeg': spawn `ffmpeg`/`ffplay` — cross-platform (Windows / macOS / Linux).\n *\n * Capture is always 16 kHz mono 16-bit; use a `plughw:CARD,DEV` device (ALSA) so it resamples.\n */\nimport { spawn, type ChildProcess } from 'node:child_process';\nimport { AUDIO_SAMPLE_RATE } from './protocol';\nimport type { Logger } from './index';\n\nexport type AudioBackend = 'alsa' | 'ffmpeg';\n\n/** Resolve 'auto' → alsa on Linux, ffmpeg elsewhere. */\nexport function resolveBackend(pref: string): AudioBackend {\n if (pref === 'alsa' || pref === 'ffmpeg') {\n return pref;\n }\n return process.platform === 'linux' ? 'alsa' : 'ffmpeg';\n}\n\n/** ffmpeg capture input args per platform (device = dshow name / avfoundation index / ALSA name). */\nexport function ffmpegInput(device: string): string[] {\n if (process.platform === 'win32') {\n return ['-f', 'dshow', '-i', `audio=${device || 'default'}`];\n }\n if (process.platform === 'darwin') {\n return ['-f', 'avfoundation', '-i', `:${device || '0'}`];\n }\n return ['-f', 'alsa', '-i', device || 'default'];\n}\n\n/** Continuous microphone capture at 16 kHz mono 16-bit; emits raw PCM chunks. */\nexport class Mic {\n private proc: ChildProcess | null = null;\n /** Set by stop() so the expected exit isn't reported as an error. */\n private stopping = false;\n /** Last stderr line from the capture process (surfaced if it dies unexpectedly). */\n private lastErr = '';\n\n constructor(\n private readonly backend: AudioBackend,\n private readonly device: string,\n private readonly log: Logger,\n ) {}\n\n start(onData: (pcm: Buffer) => void): void {\n this.stopping = false;\n this.lastErr = '';\n const [cmd, args] =\n this.backend === 'ffmpeg'\n ? ([\n 'ffmpeg',\n [\n '-hide_banner',\n '-loglevel',\n 'error',\n ...ffmpegInput(this.device),\n '-ac',\n '1',\n '-ar',\n String(AUDIO_SAMPLE_RATE),\n '-f',\n 's16le',\n '-',\n ],\n ] as const)\n : ([\n 'arecord',\n [\n '-q',\n '-t',\n 'raw',\n '-f',\n 'S16_LE',\n '-c',\n '1',\n '-r',\n String(AUDIO_SAMPLE_RATE),\n ...(this.device && this.device !== 'default' ? ['-D', this.device] : []),\n ],\n ] as const);\n\n this.proc = spawn(cmd, args);\n this.proc.stdout?.on('data', (d: Buffer) => onData(d));\n this.proc.stderr?.on('data', (d: Buffer) => {\n this.lastErr = d.toString().trim();\n this.log.debug(`${cmd}: ${this.lastErr}`);\n });\n this.proc.on('error', e =>\n this.log.error(\n `${cmd} failed: ${e.message} — is it installed? ${this.backend === 'ffmpeg' ? '(install ffmpeg)' : '(sudo apt install alsa-utils)'}`,\n ),\n );\n // A healthy capture runs until stop(); an early exit means the device could not be opened.\n this.proc.on('close', code => {\n if (this.stopping) {\n return;\n }\n const hint =\n this.backend === 'alsa'\n ? \" Pick a real capture device — run 'arecord -l' and set micDevice to e.g. 'plughw:1,0'. The ALSA 'default' device often has no capture slave.\"\n : ' Check the microphone device / that ffmpeg can open it.';\n this.log.warn(\n `Microphone capture stopped unexpectedly (${cmd} exit ${code ?? '?'}).${this.lastErr ? ` Last error: ${this.lastErr}.` : ''}${hint}`,\n );\n });\n this.log.info(\n `Microphone capture started (${this.backend}: ${this.device || 'default'} @ ${AUDIO_SAMPLE_RATE} Hz).`,\n );\n }\n\n /** Stop capture and wait for the process to actually exit so ALSA/the device is released. */\n async stop(): Promise<void> {\n this.stopping = true;\n const proc = this.proc;\n this.proc = null;\n if (!proc) {\n return;\n }\n await new Promise<void>(resolve => {\n let done = false;\n const finish = (): void => {\n if (!done) {\n done = true;\n resolve();\n }\n };\n proc.once('close', finish);\n proc.kill(); // SIGTERM\n // Safety net: force-kill if it does not exit promptly, then resolve anyway.\n setTimeout(() => {\n if (!done) {\n try {\n proc.kill('SIGKILL');\n } catch {\n /* already gone */\n }\n finish();\n }\n }, 1500);\n });\n }\n}\n\n/**\n * Play raw mono 16-bit PCM at the given rate. Returns the child process (so playback can be cancelled\n * for barge-in) and a `done` promise that resolves when playback finishes or is killed.\n */\nexport function playPcm(\n pcm: Buffer,\n sampleRate: number,\n backend: AudioBackend,\n device: string,\n log: Logger,\n): { proc: ChildProcess; done: Promise<void> } {\n const [cmd, args] =\n backend === 'ffmpeg'\n ? ([\n 'ffplay',\n [\n '-hide_banner',\n '-loglevel',\n 'error',\n '-nodisp',\n '-autoexit',\n '-f',\n 's16le',\n '-ar',\n String(sampleRate),\n '-ch_layout',\n 'mono',\n '-i',\n '-',\n ],\n ] as const)\n : ([\n 'aplay',\n [\n '-q',\n '-t',\n 'raw',\n '-f',\n 'S16_LE',\n '-c',\n '1',\n '-r',\n String(sampleRate),\n ...(device && device !== 'default' ? ['-D', device] : []),\n ],\n ] as const);\n\n const proc = spawn(cmd, args);\n const done = new Promise<void>(resolve => {\n proc.on('close', () => resolve());\n proc.on('error', e => {\n log.error(`${cmd} failed: ${e.message}`);\n resolve();\n });\n });\n proc.stdin?.end(pcm);\n return { proc, done };\n}\n\n/** A short rising \"listening\" beep (mono 16-bit @ 16 kHz), synthesised once. */\nexport function pling(): Buffer {\n const rate = AUDIO_SAMPLE_RATE;\n const dur = 0.18;\n const n = Math.floor(rate * dur);\n const buf = Buffer.alloc(n * 2);\n for (let i = 0; i < n; i++) {\n const t = i / rate;\n const freq = 880 + (1320 - 880) * (i / n);\n const fade = Math.sin((Math.PI * i) / n);\n const val = Math.max(\n -32768,\n Math.min(32767, Math.round(32767 * 0.6 * fade * Math.sin(2 * Math.PI * freq * t))),\n );\n buf.writeInt16LE(val, i * 2);\n }\n return buf;\n}\n"]}
|
package/build/probe.js
CHANGED
|
@@ -62,8 +62,8 @@ onProgress) {
|
|
|
62
62
|
void pump();
|
|
63
63
|
});
|
|
64
64
|
await new Promise(res => setTimeout(res, Math.max(1, seconds) * 1000));
|
|
65
|
-
mic.stop();
|
|
66
65
|
await new Promise(res => setTimeout(res, 200)); // let the last frames drain
|
|
66
|
+
await mic.stop();
|
|
67
67
|
return { detected, peakScore, peakRms, frames, threshold: cfg.wakewordThreshold };
|
|
68
68
|
}
|
|
69
69
|
//# sourceMappingURL=probe.js.map
|
package/build/probe.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"probe.js","sourceRoot":"","sources":["../src/probe.ts"],"names":[],"mappings":";;AA0BA,sCAyDC;AAnFD;;;;GAIG;AACH,qCAAwC;AACxC,yCAAsC;AACtC,mCAA8C;AAC9C,+BAA4B;AAI5B,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,6BAA6B;AAU3D;;;GAGG;AACI,KAAK,UAAU,aAAa,CAC/B,GAAoB,EACpB,GAAW,EACX,OAAe;AACf,8FAA8F;AAC9F,UAAoE;IAEpE,MAAM,MAAM,GAAG,MAAM,IAAA,qBAAY,EAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC;IACzE,MAAM,QAAQ,GAAG,IAAI,mBAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,iBAAiB,EAAE,GAAG,CAAC,CAAC;IAClE,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IACtB,MAAM,GAAG,GAAG,IAAI,WAAG,CAAC,IAAA,sBAAc,EAAC,GAAG,CAAC,YAAY,CAAC,EAAE,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IAE1E,IAAI,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAChC,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,QAAQ,GAAG,KAAK,CAAC;IAErB,MAAM,IAAI,GAAG,KAAK,IAAmB,EAAE;QACnC,IAAI,OAAO,EAAE,CAAC;YACV,OAAO;QACX,CAAC;QACD,OAAO,GAAG,IAAI,CAAC;QACf,IAAI,CAAC;YACD,OAAO,SAAS,CAAC,MAAM,IAAI,WAAW,EAAE,CAAC;gBACrC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC;gBAC9D,SAAS,GAAG,SAAS,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;gBAC5C,MAAM,EAAE,CAAC;gBACT,MAAM,KAAK,GAAG,IAAA,SAAG,EAAC,KAAK,CAAC,CAAC;gBACzB,IAAI,KAAK,GAAG,OAAO,EAAE,CAAC;oBAClB,OAAO,GAAG,KAAK,CAAC;gBACpB,CAAC;gBACD,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;gBAC5C,MAAM,OAAO,GAAG,KAAK,IAAI,CAAC,CAAC;gBAC3B,IAAI,OAAO,GAAG,SAAS,EAAE,CAAC;oBACtB,SAAS,GAAG,OAAO,CAAC;gBACxB,CAAC;gBACD,IAAI,QAAQ,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;oBAC5B,QAAQ,GAAG,IAAI,CAAC;gBACpB,CAAC;gBACD,UAAU,EAAE,CAAC,OAAO,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,wCAAwC;YACpF,CAAC;QACL,CAAC;gBAAS,CAAC;YACP,OAAO,GAAG,KAAK,CAAC;QACpB,CAAC;IACL,CAAC,CAAC;IAEF,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;QACV,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC;QAC1C,KAAK,IAAI,EAAE,CAAC;IAChB,CAAC,CAAC,CAAC;IACH,MAAM,IAAI,OAAO,CAAO,GAAG,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;IAC7E,
|
|
1
|
+
{"version":3,"file":"probe.js","sourceRoot":"","sources":["../src/probe.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Wake-word probe: open the microphone for a few seconds and report whether the wake word was\n * detected and the peak score/level seen. Used by the adapter's \"Test wake word\" button so users can\n * verify detection from the GUI — works even before the satellite is fully configured (no server needed).\n */\nimport { ensureModels } from './models';\nimport { WakeWord } from './wakeword';\nimport { Mic, resolveBackend } from './audio';\nimport { rms } from './vad';\nimport type { SatelliteConfig } from './config';\nimport type { Logger } from './index';\n\nconst FRAME_BYTES = 1280 * 2; // 80 ms @ 16 kHz mono 16-bit\n\nexport interface WakeProbeResult {\n detected: boolean;\n peakScore: number;\n peakRms: number;\n frames: number;\n threshold: number;\n}\n\n/**\n * Listen for `seconds` and run wake-word detection. `onProgress` is called with the running peak\n * score/RMS so the caller can show a live indicator. Resolves when the window ends.\n */\nexport async function probeWakeWord(\n cfg: SatelliteConfig,\n log: Logger,\n seconds: number,\n /** Called per frame with the CURRENT score/RMS and whether the wake word has fired so far. */\n onProgress?: (score: number, rms: number, detected: boolean) => void,\n): Promise<WakeProbeResult> {\n const models = await ensureModels(cfg.modelsDir, cfg.wakewordModel, log);\n const wakeword = new WakeWord(models, cfg.wakewordThreshold, log);\n await wakeword.load();\n const mic = new Mic(resolveBackend(cfg.audioBackend), cfg.micDevice, log);\n\n let remainder = Buffer.alloc(0);\n let pumping = false;\n let peakScore = 0;\n let peakRms = 0;\n let frames = 0;\n let detected = false;\n\n const pump = async (): Promise<void> => {\n if (pumping) {\n return;\n }\n pumping = true;\n try {\n while (remainder.length >= FRAME_BYTES) {\n const frame = Buffer.from(remainder.subarray(0, FRAME_BYTES));\n remainder = remainder.subarray(FRAME_BYTES);\n frames++;\n const level = rms(frame);\n if (level > peakRms) {\n peakRms = level;\n }\n const score = await wakeword.process(frame);\n const current = score ?? 0;\n if (current > peakScore) {\n peakScore = current;\n }\n if (wakeword.triggered(score)) {\n detected = true;\n }\n onProgress?.(current, level, detected); // current frame values for a live meter\n }\n } finally {\n pumping = false;\n }\n };\n\n mic.start(d => {\n remainder = Buffer.concat([remainder, d]);\n void pump();\n });\n await new Promise<void>(res => setTimeout(res, Math.max(1, seconds) * 1000));\n await new Promise<void>(res => setTimeout(res, 200)); // let the last frames drain\n await mic.stop();\n\n return { detected, peakScore, peakRms, frames, threshold: cfg.wakewordThreshold };\n}\n"]}
|
package/build/satellite.js
CHANGED
package/build/satellite.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"satellite.js","sourceRoot":"","sources":["../src/satellite.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Satellite orchestration: resolve the adapter address → register → listen for the wake word →\n * stream the utterance → play the spoken reply. Protocol-compatible with the Hannah satellite.\n */\nimport * as dgram from 'node:dgram';\nimport {\n AUDIO_SAMPLE_RATE,\n TYPE_AUDIO,\n TYPE_CONTROL,\n TYPE_TTS,\n decodePacket,\n encodeAudio,\n encodeControl,\n type SatToServer,\n type SatelliteState,\n} from './protocol';\nimport type { ChildProcess } from 'node:child_process';\nimport { Mic, playPcm, pling, resolveBackend, type AudioBackend } from './audio';\nimport { SilenceDetector, rms } from './vad';\nimport { WakeWord } from './wakeword';\nimport { ensureModels } from './models';\nimport { parseWakewords, type SatelliteConfig } from './config';\nimport type { SatelliteHost, Logger } from './index';\n\n/** 1280 samples (80 ms) of 16 kHz mono 16-bit PCM per processing frame. */\nconst FRAME_BYTES = 1280 * 2;\nconst FRAME_MS = 80;\n/** Missed heartbeat ACKs before the satellite assumes the adapter is gone and re-registers. */\nconst MAX_HEARTBEAT_MISSES = 3;\nconst RECONNECT_MAX_BACKOFF_MS = 30_000;\n\nconst sleep = (ms: number): Promise<void> => new Promise(res => setTimeout(res, ms));\n\nexport class Satellite {\n private readonly log: Logger;\n private socket: dgram.Socket | null = null;\n private serverHost = '';\n private serverPort = 0;\n\n private wakeword!: WakeWord;\n private mic: Mic | null = null;\n private readonly backend: AudioBackend;\n private plingPcm: Buffer = Buffer.alloc(0);\n /** Current reply playback process (for barge-in); null when nothing is playing. */\n private playbackProc: ChildProcess | null = null;\n private heartbeatTimer: NodeJS.Timeout | null = null;\n\n // mic frame assembly\n private micRemainder: Buffer = Buffer.alloc(0);\n private pumping = false;\n private micBytesLogged = false;\n\n // wake-word diagnostics (periodic summary)\n private wakeFrames = 0;\n private wakeMaxScore = 0;\n private wakeMaxRms = 0;\n\n // recording state\n private recording = false;\n private silence: SilenceDetector | null = null;\n private preBuffer: Buffer[] = [];\n private recFrames = 0;\n private recPeakRms = 0;\n\n // tts receiver\n private ttsChunks: Buffer[] = [];\n private ttsDiscard = false;\n\n private registerResolve: (() => void) | null = null;\n private running = false;\n private heartbeatMisses = 0;\n private awaitingHeartbeatAck = false;\n private reconnecting = false;\n\n constructor(\n private readonly cfg: SatelliteConfig,\n private readonly host: SatelliteHost,\n ) {\n this.log = host.log;\n this.backend = resolveBackend(cfg.audioBackend);\n }\n\n async start(): Promise<void> {\n this.running = true;\n const addr = this.resolveAddress();\n this.serverHost = addr.host;\n this.serverPort = addr.port;\n this.log.info(`Adapter address: ${this.serverHost}:${this.serverPort}`);\n\n await this.openSocket();\n\n const words = parseWakewords(this.cfg.wakewordModel);\n const modelSets = await Promise.all(words.map(w => ensureModels(this.cfg.modelsDir, w, this.log)));\n this.wakeword = new WakeWord(modelSets, this.cfg.wakewordThreshold, this.log);\n await this.wakeword.load();\n this.plingPcm = pling();\n\n // Retry until the adapter answers, so the satellite survives the adapter being down at boot.\n await this.registerWithRetry();\n this.heartbeatTimer = setInterval(() => this.heartbeatTick(), this.cfg.heartbeatIntervalMs);\n\n this.mic = new Mic(this.backend, this.cfg.micDevice, this.log);\n this.mic.start(d => this.onMicData(d));\n this.setStatus('idle');\n this.log.info(`Satellite '${this.cfg.device}' ready. Listening for the wake word …`);\n }\n\n async stop(): Promise<void> {\n this.running = false;\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n this.mic?.stop();\n this.mic = null;\n const socket = this.socket;\n this.socket = null;\n if (socket) {\n await new Promise<void>(res => socket.close(() => res()));\n }\n }\n\n // ── setup ──────────────────────────────────────────────────────────────\n\n private resolveAddress(): { host: string; port: number } {\n if (!this.cfg.host) {\n throw new Error('No adapter address: set \"host\" (and \"port\") in the config.');\n }\n return { host: this.cfg.host, port: this.cfg.port };\n }\n\n private openSocket(): Promise<void> {\n return new Promise((resolve, reject) => {\n const socket = dgram.createSocket('udp4');\n this.socket = socket;\n socket.on('message', d => this.onMessage(d));\n socket.on('error', e => this.log.error(`UDP error: ${e.message}`));\n socket.once('error', reject);\n socket.bind(this.cfg.listenPort, () => {\n this.log.info(`Listening for TTS on UDP ${this.cfg.listenPort}.`);\n resolve();\n });\n });\n }\n\n /** Register, retrying with exponential backoff until the adapter acknowledges (or we stop). */\n private async registerWithRetry(): Promise<void> {\n let backoff = 1000;\n while (this.running) {\n try {\n await this.register();\n this.awaitingHeartbeatAck = false;\n this.heartbeatMisses = 0;\n return;\n } catch (e) {\n this.log.warn(`${(e as Error).message} — retrying in ${Math.round(backoff / 1000)} s …`);\n await sleep(backoff);\n backoff = Math.min(backoff * 2, RECONNECT_MAX_BACKOFF_MS);\n }\n }\n }\n\n /** Triggered when heartbeats stop being acknowledged: re-register (adapter probably restarted). */\n private async reconnect(): Promise<void> {\n if (this.reconnecting || !this.running) {\n return;\n }\n this.reconnecting = true;\n this.heartbeatMisses = 0;\n this.awaitingHeartbeatAck = false;\n this.log.warn('Adapter unreachable — re-registering …');\n try {\n await this.registerWithRetry();\n this.log.info('Re-registered with the adapter.');\n } finally {\n this.reconnecting = false;\n }\n }\n\n private heartbeatTick(): void {\n if (this.reconnecting) {\n return;\n }\n if (this.awaitingHeartbeatAck) {\n this.heartbeatMisses++;\n this.log.warn(`Heartbeat not acknowledged (${this.heartbeatMisses}/${MAX_HEARTBEAT_MISSES}).`);\n if (this.heartbeatMisses >= MAX_HEARTBEAT_MISSES) {\n void this.reconnect();\n return;\n }\n }\n this.awaitingHeartbeatAck = true;\n this.sendControl({ type: 'heartbeat', device: this.cfg.device });\n }\n\n private register(): Promise<void> {\n return new Promise((resolve, reject) => {\n this.registerResolve = resolve;\n this.sendControl({\n type: 'register',\n device: this.cfg.device,\n room: this.cfg.room,\n listen_port: this.cfg.listenPort,\n });\n this.log.info(`Registration sent to ${this.serverHost}:${this.serverPort} (device '${this.cfg.device}').`);\n setTimeout(() => {\n if (this.registerResolve) {\n this.registerResolve = null;\n reject(new Error(`registration timed out after ${this.cfg.registrationTimeoutMs} ms`));\n }\n }, this.cfg.registrationTimeoutMs);\n });\n }\n\n // ── microphone → wake word → recording ─────────────────────────────────\n\n private onMicData(chunk: Buffer): void {\n if (!this.micBytesLogged) {\n this.micBytesLogged = true;\n this.log.info(`Microphone is producing audio (first chunk ${chunk.length} bytes).`);\n }\n this.micRemainder = Buffer.concat([this.micRemainder, chunk]);\n if (!this.pumping) {\n void this.pump();\n }\n }\n\n private async pump(): Promise<void> {\n this.pumping = true;\n try {\n while (this.micRemainder.length >= FRAME_BYTES) {\n const frame = Buffer.from(this.micRemainder.subarray(0, FRAME_BYTES));\n this.micRemainder = this.micRemainder.subarray(FRAME_BYTES);\n await this.handleFrame(frame);\n }\n } catch (e) {\n this.log.error(`frame processing error: ${(e as Error).message}`);\n } finally {\n this.pumping = false;\n }\n }\n\n private async handleFrame(frame: Buffer): Promise<void> {\n if (this.recording) {\n this.sendAudio(frame);\n this.recFrames++;\n const level = rms(frame);\n if (level > this.recPeakRms) {\n this.recPeakRms = level;\n }\n if (this.silence!.push(frame)) {\n this.endRecording();\n }\n return;\n }\n\n // Wake-word listening: keep a rolling pre-buffer so speech during inference is not lost.\n this.preBuffer.push(frame);\n if (this.preBuffer.length > this.cfg.preBufferChunks) {\n this.preBuffer.shift();\n }\n const score = await this.wakeword.process(frame);\n\n // Periodic diagnostics: every ~2 s report how many frames ran and the peak score, so you can\n // watch the score rise while saying the wake word and tune `wakewordThreshold`.\n this.wakeFrames++;\n if (score !== null && score > this.wakeMaxScore) {\n this.wakeMaxScore = score;\n }\n const level = rms(frame);\n if (level > this.wakeMaxRms) {\n this.wakeMaxRms = level;\n }\n if (this.wakeFrames >= 25) {\n this.log.debug(\n `wake: ${this.wakeFrames} frames, peak mic RMS ${this.wakeMaxRms.toFixed(0)}, ` +\n `peak score ${this.wakeMaxScore.toFixed(3)} (threshold ${this.cfg.wakewordThreshold})`,\n );\n this.wakeFrames = 0;\n this.wakeMaxScore = 0;\n this.wakeMaxRms = 0;\n }\n\n if (this.wakeword.triggered(score)) {\n this.log.info(`Wake word detected (score ${(score as number).toFixed(3)}).`);\n // Barge-in: if a reply is currently playing, stop it so the user can interrupt.\n if (this.cfg.bargeIn && this.playbackProc) {\n this.log.info('Barge-in: stopping playback.');\n this.playbackProc.kill();\n this.playbackProc = null;\n }\n this.wakeword.reset();\n await this.startRecording();\n }\n }\n\n private async startRecording(): Promise<void> {\n this.setStatus('listening');\n this.ttsDiscard = true; // drop any late TTS from a previous turn\n await playPcm(this.plingPcm, AUDIO_SAMPLE_RATE, this.backend, this.cfg.speakerDevice, this.log).done;\n this.ttsDiscard = false;\n this.ttsChunks = [];\n\n // Drop everything captured up to now (the beep echo + any inference backlog) so recording runs\n // in real time — otherwise the silence detector races through buffered quiet frames and ends the\n // utterance before you finish speaking (→ the adapter's STT gets silence → \"(empty)\").\n this.micRemainder = Buffer.alloc(0);\n this.preBuffer = [];\n this.recFrames = 0;\n this.recPeakRms = 0;\n this.silence = new SilenceDetector(\n this.cfg.silenceThreshold,\n Math.round(this.cfg.silenceMs / FRAME_MS),\n Math.round(this.cfg.minRecordMs / FRAME_MS),\n Math.round(this.cfg.maxRecordMs / FRAME_MS),\n );\n this.recording = true;\n this.log.info('Beep done — listening for your command …');\n }\n\n private endRecording(): void {\n this.recording = false;\n this.silence = null;\n this.sendControl({ type: 'audio_end', device: this.cfg.device });\n const maxFrames = Math.round(this.cfg.maxRecordMs / FRAME_MS);\n const reason = this.recFrames >= maxFrames ? 'max length' : 'silence';\n this.log.info(\n `Recording finished: ${this.recFrames} frames (${this.recFrames * FRAME_MS} ms), ` +\n `peak RMS ${this.recPeakRms.toFixed(0)}, ended on ${reason} — audio_end sent.`,\n );\n }\n\n // ── incoming UDP (control + TTS) ───────────────────────────────────────\n\n private onMessage(data: Buffer): void {\n if (!data.length) {\n return;\n }\n const { type, payload } = decodePacket(data);\n if (type === TYPE_AUDIO) {\n return; // satellites do not receive audio\n }\n if (type === TYPE_TTS) {\n if (!this.ttsDiscard) {\n this.ttsChunks.push(Buffer.from(payload));\n }\n return;\n }\n if (type !== TYPE_CONTROL) {\n return;\n }\n let msg: { type?: string; ok?: boolean; state?: SatelliteState; sample_rate?: number };\n try {\n msg = JSON.parse(payload.toString('utf8'));\n } catch {\n return;\n }\n switch (msg.type) {\n case 'registered':\n if (msg.ok && this.registerResolve) {\n this.log.info('Registration confirmed (ACK).');\n this.registerResolve();\n this.registerResolve = null;\n }\n break;\n case 'reregister':\n this.register().catch(e => this.log.warn(`re-register failed: ${(e as Error).message}`));\n break;\n case 'status':\n if (msg.state) {\n this.setStatus(msg.state);\n }\n break;\n case 'tts_end':\n void this.playTts(msg.sample_rate || AUDIO_SAMPLE_RATE);\n break;\n case 'heartbeat_ack':\n this.awaitingHeartbeatAck = false;\n this.heartbeatMisses = 0;\n break;\n }\n }\n\n private async playTts(sampleRate: number): Promise<void> {\n const pcm = Buffer.concat(this.ttsChunks);\n this.ttsChunks = [];\n if (!pcm.length) {\n return;\n }\n this.log.info(`Playing reply (${(pcm.length / 2 / sampleRate).toFixed(1)} s @ ${sampleRate} Hz).`);\n const { proc, done } = playPcm(pcm, sampleRate, this.backend, this.cfg.speakerDevice, this.log);\n this.playbackProc = proc; // tracked so the wake word can barge-in\n try {\n await done;\n } finally {\n this.playbackProc = null;\n }\n }\n\n // ── senders / status ───────────────────────────────────────────────────\n\n private sendAudio(pcm: Buffer): void {\n this.socket?.send(encodeAudio(pcm), this.serverPort, this.serverHost);\n }\n\n private sendControl(msg: SatToServer): void {\n this.socket?.send(encodeControl(msg), this.serverPort, this.serverHost);\n }\n\n private setStatus(state: SatelliteState): void {\n this.host.onStatus?.(state);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"satellite.js","sourceRoot":"","sources":["../src/satellite.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Satellite orchestration: resolve the adapter address → register → listen for the wake word →\n * stream the utterance → play the spoken reply. Protocol-compatible with the Hannah satellite.\n */\nimport * as dgram from 'node:dgram';\nimport {\n AUDIO_SAMPLE_RATE,\n TYPE_AUDIO,\n TYPE_CONTROL,\n TYPE_TTS,\n decodePacket,\n encodeAudio,\n encodeControl,\n type SatToServer,\n type SatelliteState,\n} from './protocol';\nimport type { ChildProcess } from 'node:child_process';\nimport { Mic, playPcm, pling, resolveBackend, type AudioBackend } from './audio';\nimport { SilenceDetector, rms } from './vad';\nimport { WakeWord } from './wakeword';\nimport { ensureModels } from './models';\nimport { parseWakewords, type SatelliteConfig } from './config';\nimport type { SatelliteHost, Logger } from './index';\n\n/** 1280 samples (80 ms) of 16 kHz mono 16-bit PCM per processing frame. */\nconst FRAME_BYTES = 1280 * 2;\nconst FRAME_MS = 80;\n/** Missed heartbeat ACKs before the satellite assumes the adapter is gone and re-registers. */\nconst MAX_HEARTBEAT_MISSES = 3;\nconst RECONNECT_MAX_BACKOFF_MS = 30_000;\n\nconst sleep = (ms: number): Promise<void> => new Promise(res => setTimeout(res, ms));\n\nexport class Satellite {\n private readonly log: Logger;\n private socket: dgram.Socket | null = null;\n private serverHost = '';\n private serverPort = 0;\n\n private wakeword!: WakeWord;\n private mic: Mic | null = null;\n private readonly backend: AudioBackend;\n private plingPcm: Buffer = Buffer.alloc(0);\n /** Current reply playback process (for barge-in); null when nothing is playing. */\n private playbackProc: ChildProcess | null = null;\n private heartbeatTimer: NodeJS.Timeout | null = null;\n\n // mic frame assembly\n private micRemainder: Buffer = Buffer.alloc(0);\n private pumping = false;\n private micBytesLogged = false;\n\n // wake-word diagnostics (periodic summary)\n private wakeFrames = 0;\n private wakeMaxScore = 0;\n private wakeMaxRms = 0;\n\n // recording state\n private recording = false;\n private silence: SilenceDetector | null = null;\n private preBuffer: Buffer[] = [];\n private recFrames = 0;\n private recPeakRms = 0;\n\n // tts receiver\n private ttsChunks: Buffer[] = [];\n private ttsDiscard = false;\n\n private registerResolve: (() => void) | null = null;\n private running = false;\n private heartbeatMisses = 0;\n private awaitingHeartbeatAck = false;\n private reconnecting = false;\n\n constructor(\n private readonly cfg: SatelliteConfig,\n private readonly host: SatelliteHost,\n ) {\n this.log = host.log;\n this.backend = resolveBackend(cfg.audioBackend);\n }\n\n async start(): Promise<void> {\n this.running = true;\n const addr = this.resolveAddress();\n this.serverHost = addr.host;\n this.serverPort = addr.port;\n this.log.info(`Adapter address: ${this.serverHost}:${this.serverPort}`);\n\n await this.openSocket();\n\n const words = parseWakewords(this.cfg.wakewordModel);\n const modelSets = await Promise.all(words.map(w => ensureModels(this.cfg.modelsDir, w, this.log)));\n this.wakeword = new WakeWord(modelSets, this.cfg.wakewordThreshold, this.log);\n await this.wakeword.load();\n this.plingPcm = pling();\n\n // Retry until the adapter answers, so the satellite survives the adapter being down at boot.\n await this.registerWithRetry();\n this.heartbeatTimer = setInterval(() => this.heartbeatTick(), this.cfg.heartbeatIntervalMs);\n\n this.mic = new Mic(this.backend, this.cfg.micDevice, this.log);\n this.mic.start(d => this.onMicData(d));\n this.setStatus('idle');\n this.log.info(`Satellite '${this.cfg.device}' ready. Listening for the wake word …`);\n }\n\n async stop(): Promise<void> {\n this.running = false;\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n await this.mic?.stop();\n this.mic = null;\n const socket = this.socket;\n this.socket = null;\n if (socket) {\n await new Promise<void>(res => socket.close(() => res()));\n }\n }\n\n // ── setup ──────────────────────────────────────────────────────────────\n\n private resolveAddress(): { host: string; port: number } {\n if (!this.cfg.host) {\n throw new Error('No adapter address: set \"host\" (and \"port\") in the config.');\n }\n return { host: this.cfg.host, port: this.cfg.port };\n }\n\n private openSocket(): Promise<void> {\n return new Promise((resolve, reject) => {\n const socket = dgram.createSocket('udp4');\n this.socket = socket;\n socket.on('message', d => this.onMessage(d));\n socket.on('error', e => this.log.error(`UDP error: ${e.message}`));\n socket.once('error', reject);\n socket.bind(this.cfg.listenPort, () => {\n this.log.info(`Listening for TTS on UDP ${this.cfg.listenPort}.`);\n resolve();\n });\n });\n }\n\n /** Register, retrying with exponential backoff until the adapter acknowledges (or we stop). */\n private async registerWithRetry(): Promise<void> {\n let backoff = 1000;\n while (this.running) {\n try {\n await this.register();\n this.awaitingHeartbeatAck = false;\n this.heartbeatMisses = 0;\n return;\n } catch (e) {\n this.log.warn(`${(e as Error).message} — retrying in ${Math.round(backoff / 1000)} s …`);\n await sleep(backoff);\n backoff = Math.min(backoff * 2, RECONNECT_MAX_BACKOFF_MS);\n }\n }\n }\n\n /** Triggered when heartbeats stop being acknowledged: re-register (adapter probably restarted). */\n private async reconnect(): Promise<void> {\n if (this.reconnecting || !this.running) {\n return;\n }\n this.reconnecting = true;\n this.heartbeatMisses = 0;\n this.awaitingHeartbeatAck = false;\n this.log.warn('Adapter unreachable — re-registering …');\n try {\n await this.registerWithRetry();\n this.log.info('Re-registered with the adapter.');\n } finally {\n this.reconnecting = false;\n }\n }\n\n private heartbeatTick(): void {\n if (this.reconnecting) {\n return;\n }\n if (this.awaitingHeartbeatAck) {\n this.heartbeatMisses++;\n this.log.warn(`Heartbeat not acknowledged (${this.heartbeatMisses}/${MAX_HEARTBEAT_MISSES}).`);\n if (this.heartbeatMisses >= MAX_HEARTBEAT_MISSES) {\n void this.reconnect();\n return;\n }\n }\n this.awaitingHeartbeatAck = true;\n this.sendControl({ type: 'heartbeat', device: this.cfg.device });\n }\n\n private register(): Promise<void> {\n return new Promise((resolve, reject) => {\n this.registerResolve = resolve;\n this.sendControl({\n type: 'register',\n device: this.cfg.device,\n room: this.cfg.room,\n listen_port: this.cfg.listenPort,\n });\n this.log.info(`Registration sent to ${this.serverHost}:${this.serverPort} (device '${this.cfg.device}').`);\n setTimeout(() => {\n if (this.registerResolve) {\n this.registerResolve = null;\n reject(new Error(`registration timed out after ${this.cfg.registrationTimeoutMs} ms`));\n }\n }, this.cfg.registrationTimeoutMs);\n });\n }\n\n // ── microphone → wake word → recording ─────────────────────────────────\n\n private onMicData(chunk: Buffer): void {\n if (!this.micBytesLogged) {\n this.micBytesLogged = true;\n this.log.info(`Microphone is producing audio (first chunk ${chunk.length} bytes).`);\n }\n this.micRemainder = Buffer.concat([this.micRemainder, chunk]);\n if (!this.pumping) {\n void this.pump();\n }\n }\n\n private async pump(): Promise<void> {\n this.pumping = true;\n try {\n while (this.micRemainder.length >= FRAME_BYTES) {\n const frame = Buffer.from(this.micRemainder.subarray(0, FRAME_BYTES));\n this.micRemainder = this.micRemainder.subarray(FRAME_BYTES);\n await this.handleFrame(frame);\n }\n } catch (e) {\n this.log.error(`frame processing error: ${(e as Error).message}`);\n } finally {\n this.pumping = false;\n }\n }\n\n private async handleFrame(frame: Buffer): Promise<void> {\n if (this.recording) {\n this.sendAudio(frame);\n this.recFrames++;\n const level = rms(frame);\n if (level > this.recPeakRms) {\n this.recPeakRms = level;\n }\n if (this.silence!.push(frame)) {\n this.endRecording();\n }\n return;\n }\n\n // Wake-word listening: keep a rolling pre-buffer so speech during inference is not lost.\n this.preBuffer.push(frame);\n if (this.preBuffer.length > this.cfg.preBufferChunks) {\n this.preBuffer.shift();\n }\n const score = await this.wakeword.process(frame);\n\n // Periodic diagnostics: every ~2 s report how many frames ran and the peak score, so you can\n // watch the score rise while saying the wake word and tune `wakewordThreshold`.\n this.wakeFrames++;\n if (score !== null && score > this.wakeMaxScore) {\n this.wakeMaxScore = score;\n }\n const level = rms(frame);\n if (level > this.wakeMaxRms) {\n this.wakeMaxRms = level;\n }\n if (this.wakeFrames >= 25) {\n this.log.debug(\n `wake: ${this.wakeFrames} frames, peak mic RMS ${this.wakeMaxRms.toFixed(0)}, ` +\n `peak score ${this.wakeMaxScore.toFixed(3)} (threshold ${this.cfg.wakewordThreshold})`,\n );\n this.wakeFrames = 0;\n this.wakeMaxScore = 0;\n this.wakeMaxRms = 0;\n }\n\n if (this.wakeword.triggered(score)) {\n this.log.info(`Wake word detected (score ${(score as number).toFixed(3)}).`);\n // Barge-in: if a reply is currently playing, stop it so the user can interrupt.\n if (this.cfg.bargeIn && this.playbackProc) {\n this.log.info('Barge-in: stopping playback.');\n this.playbackProc.kill();\n this.playbackProc = null;\n }\n this.wakeword.reset();\n await this.startRecording();\n }\n }\n\n private async startRecording(): Promise<void> {\n this.setStatus('listening');\n this.ttsDiscard = true; // drop any late TTS from a previous turn\n await playPcm(this.plingPcm, AUDIO_SAMPLE_RATE, this.backend, this.cfg.speakerDevice, this.log).done;\n this.ttsDiscard = false;\n this.ttsChunks = [];\n\n // Drop everything captured up to now (the beep echo + any inference backlog) so recording runs\n // in real time — otherwise the silence detector races through buffered quiet frames and ends the\n // utterance before you finish speaking (→ the adapter's STT gets silence → \"(empty)\").\n this.micRemainder = Buffer.alloc(0);\n this.preBuffer = [];\n this.recFrames = 0;\n this.recPeakRms = 0;\n this.silence = new SilenceDetector(\n this.cfg.silenceThreshold,\n Math.round(this.cfg.silenceMs / FRAME_MS),\n Math.round(this.cfg.minRecordMs / FRAME_MS),\n Math.round(this.cfg.maxRecordMs / FRAME_MS),\n );\n this.recording = true;\n this.log.info('Beep done — listening for your command …');\n }\n\n private endRecording(): void {\n this.recording = false;\n this.silence = null;\n this.sendControl({ type: 'audio_end', device: this.cfg.device });\n const maxFrames = Math.round(this.cfg.maxRecordMs / FRAME_MS);\n const reason = this.recFrames >= maxFrames ? 'max length' : 'silence';\n this.log.info(\n `Recording finished: ${this.recFrames} frames (${this.recFrames * FRAME_MS} ms), ` +\n `peak RMS ${this.recPeakRms.toFixed(0)}, ended on ${reason} — audio_end sent.`,\n );\n }\n\n // ── incoming UDP (control + TTS) ───────────────────────────────────────\n\n private onMessage(data: Buffer): void {\n if (!data.length) {\n return;\n }\n const { type, payload } = decodePacket(data);\n if (type === TYPE_AUDIO) {\n return; // satellites do not receive audio\n }\n if (type === TYPE_TTS) {\n if (!this.ttsDiscard) {\n this.ttsChunks.push(Buffer.from(payload));\n }\n return;\n }\n if (type !== TYPE_CONTROL) {\n return;\n }\n let msg: { type?: string; ok?: boolean; state?: SatelliteState; sample_rate?: number };\n try {\n msg = JSON.parse(payload.toString('utf8'));\n } catch {\n return;\n }\n switch (msg.type) {\n case 'registered':\n if (msg.ok && this.registerResolve) {\n this.log.info('Registration confirmed (ACK).');\n this.registerResolve();\n this.registerResolve = null;\n }\n break;\n case 'reregister':\n this.register().catch(e => this.log.warn(`re-register failed: ${(e as Error).message}`));\n break;\n case 'status':\n if (msg.state) {\n this.setStatus(msg.state);\n }\n break;\n case 'tts_end':\n void this.playTts(msg.sample_rate || AUDIO_SAMPLE_RATE);\n break;\n case 'heartbeat_ack':\n this.awaitingHeartbeatAck = false;\n this.heartbeatMisses = 0;\n break;\n }\n }\n\n private async playTts(sampleRate: number): Promise<void> {\n const pcm = Buffer.concat(this.ttsChunks);\n this.ttsChunks = [];\n if (!pcm.length) {\n return;\n }\n this.log.info(`Playing reply (${(pcm.length / 2 / sampleRate).toFixed(1)} s @ ${sampleRate} Hz).`);\n const { proc, done } = playPcm(pcm, sampleRate, this.backend, this.cfg.speakerDevice, this.log);\n this.playbackProc = proc; // tracked so the wake word can barge-in\n try {\n await done;\n } finally {\n this.playbackProc = null;\n }\n }\n\n // ── senders / status ───────────────────────────────────────────────────\n\n private sendAudio(pcm: Buffer): void {\n this.socket?.send(encodeAudio(pcm), this.serverPort, this.serverHost);\n }\n\n private sendControl(msg: SatToServer): void {\n this.socket?.send(encodeControl(msg), this.serverPort, this.serverHost);\n }\n\n private setStatus(state: SatelliteState): void {\n this.host.onStatus?.(state);\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iobroker/assistant-satellite",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Standalone voice satellite for the ioBroker.assistant adapter (wake word + mic streaming + TTS playback over the Hannah UDP protocol). Runs on a bare Pi via npx, no ioBroker required.",
|
|
5
5
|
"author": "ioBroker",
|
|
6
6
|
"publishConfig": {
|