@iobroker/assistant-satellite 0.1.0 → 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 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.0 (2026-07-05)
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
- stop(): void;
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) => this.log.debug(`${cmd}: ${d.toString().trim()}`));
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
- stop() {
85
- this.proc?.kill();
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;
@@ -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":";;;AAcA,wCAKC;AAGD,kCAQC;AAuHD,0BAqDC;AAGD,sBAgBC;AA7ND;;;;;;GAMG;AACH,2DAA8D;AAC9D,yCAA+C;AAK/C,wDAAwD;AACxD,SAAgB,cAAc,CAAC,IAAY;IACvC,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QACvC,OAAO,IAAI,CAAC;IAChB,CAAC;IACD,OAAO,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC5D,CAAC;AAED,qGAAqG;AACrG,SAAgB,WAAW,CAAC,MAAc;IACtC,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAC/B,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,MAAM,IAAI,SAAS,EAAE,CAAC,CAAC;IACjE,CAAC;IACD,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAChC,OAAO,CAAC,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,IAAI,MAAM,IAAI,GAAG,EAAE,CAAC,CAAC;IAC7D,CAAC;IACD,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,IAAI,SAAS,CAAC,CAAC;AACrD,CAAC;AAED,iFAAiF;AACjF,MAAa,GAAG;IAQS;IACA;IACA;IATb,IAAI,GAAwB,IAAI,CAAC;IACzC,qEAAqE;IAC7D,QAAQ,GAAG,KAAK,CAAC;IACzB,oFAAoF;IAC5E,OAAO,GAAG,EAAE,CAAC;IAErB,YACqB,OAAqB,EACrB,MAAc,EACd,GAAW;QAFX,YAAO,GAAP,OAAO,CAAc;QACrB,WAAM,GAAN,MAAM,CAAQ;QACd,QAAG,GAAH,GAAG,CAAQ;IAC7B,CAAC;IAEJ,KAAK,CAAC,MAA6B;QAC/B,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QACtB,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;QAClB,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,GACb,IAAI,CAAC,OAAO,KAAK,QAAQ;YACrB,CAAC,CAAE;gBACG,QAAQ;gBACR;oBACI,cAAc;oBACd,WAAW;oBACX,OAAO;oBACP,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC;oBAC3B,KAAK;oBACL,GAAG;oBACH,KAAK;oBACL,MAAM,CAAC,4BAAiB,CAAC;oBACzB,IAAI;oBACJ,OAAO;oBACP,GAAG;iBACN;aACM;YACb,CAAC,CAAE;gBACG,SAAS;gBACT;oBACI,IAAI;oBACJ,IAAI;oBACJ,KAAK;oBACL,IAAI;oBACJ,QAAQ;oBACR,IAAI;oBACJ,GAAG;oBACH,IAAI;oBACJ,MAAM,CAAC,4BAAiB,CAAC;oBACzB,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;iBAC3E;aACM,CAAC;QAEtB,IAAI,CAAC,IAAI,GAAG,IAAA,0BAAK,EAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC7B,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QACvD,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAS,EAAE,EAAE;YACvC,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;YACnC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,GAAG,KAAK,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QAC9C,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,CACtB,IAAI,CAAC,GAAG,CAAC,KAAK,CACV,GAAG,GAAG,YAAY,CAAC,CAAC,OAAO,uBAAuB,IAAI,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,+BAA+B,EAAE,CACvI,CACJ,CAAC;QACF,2FAA2F;QAC3F,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE;YACzB,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAChB,OAAO;YACX,CAAC;YACD,MAAM,IAAI,GACN,IAAI,CAAC,OAAO,KAAK,MAAM;gBACnB,CAAC,CAAC,8IAA8I;gBAChJ,CAAC,CAAC,yDAAyD,CAAC;YACpE,IAAI,CAAC,GAAG,CAAC,IAAI,CACT,4CAA4C,GAAG,SAAS,IAAI,IAAI,GAAG,KAAK,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,gBAAgB,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,CACvI,CAAC;QACN,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,GAAG,CAAC,IAAI,CACT,+BAA+B,IAAI,CAAC,OAAO,KAAK,IAAI,CAAC,MAAM,IAAI,SAAS,MAAM,4BAAiB,OAAO,CACzG,CAAC;IACN,CAAC;IAED,6FAA6F;IAC7F,KAAK,CAAC,IAAI;QACN,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,IAAI,EAAE,CAAC;YACR,OAAO;QACX,CAAC;QACD,MAAM,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE;YAC9B,IAAI,IAAI,GAAG,KAAK,CAAC;YACjB,MAAM,MAAM,GAAG,GAAS,EAAE;gBACtB,IAAI,CAAC,IAAI,EAAE,CAAC;oBACR,IAAI,GAAG,IAAI,CAAC;oBACZ,OAAO,EAAE,CAAC;gBACd,CAAC;YACL,CAAC,CAAC;YACF,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAC3B,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,UAAU;YACvB,4EAA4E;YAC5E,UAAU,CAAC,GAAG,EAAE;gBACZ,IAAI,CAAC,IAAI,EAAE,CAAC;oBACR,IAAI,CAAC;wBACD,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;oBACzB,CAAC;oBAAC,MAAM,CAAC;wBACL,kBAAkB;oBACtB,CAAC;oBACD,MAAM,EAAE,CAAC;gBACb,CAAC;YACL,CAAC,EAAE,IAAI,CAAC,CAAC;QACb,CAAC,CAAC,CAAC;IACP,CAAC;CACJ;AA9GD,kBA8GC;AAED;;;GAGG;AACH,SAAgB,OAAO,CACnB,GAAW,EACX,UAAkB,EAClB,OAAqB,EACrB,MAAc,EACd,GAAW;IAEX,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,GACb,OAAO,KAAK,QAAQ;QAChB,CAAC,CAAE;YACG,QAAQ;YACR;gBACI,cAAc;gBACd,WAAW;gBACX,OAAO;gBACP,SAAS;gBACT,WAAW;gBACX,IAAI;gBACJ,OAAO;gBACP,KAAK;gBACL,MAAM,CAAC,UAAU,CAAC;gBAClB,YAAY;gBACZ,MAAM;gBACN,IAAI;gBACJ,GAAG;aACN;SACM;QACb,CAAC,CAAE;YACG,OAAO;YACP;gBACI,IAAI;gBACJ,IAAI;gBACJ,KAAK;gBACL,IAAI;gBACJ,QAAQ;gBACR,IAAI;gBACJ,GAAG;gBACH,IAAI;gBACJ,MAAM,CAAC,UAAU,CAAC;gBAClB,GAAG,CAAC,MAAM,IAAI,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;aAC5D;SACM,CAAC;IAEtB,MAAM,IAAI,GAAG,IAAA,0BAAK,EAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC9B,MAAM,IAAI,GAAG,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE;QACrC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;QAClC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE;YACjB,GAAG,CAAC,KAAK,CAAC,GAAG,GAAG,YAAY,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;YACzC,OAAO,EAAE,CAAC;QACd,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;IACH,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;IACrB,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AAC1B,CAAC;AAED,gFAAgF;AAChF,SAAgB,KAAK;IACjB,MAAM,IAAI,GAAG,4BAAiB,CAAC;IAC/B,MAAM,GAAG,GAAG,IAAI,CAAC;IACjB,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC;IACjC,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAChC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACzB,MAAM,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;QACnB,MAAM,IAAI,GAAG,GAAG,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAC1C,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACzC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAChB,CAAC,KAAK,EACN,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,EAAE,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CACrF,CAAC;QACF,GAAG,CAAC,YAAY,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACjC,CAAC;IACD,OAAO,GAAG,CAAC;AACf,CAAC","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/config.d.ts CHANGED
@@ -39,3 +39,9 @@ export interface SatelliteConfig {
39
39
  export declare const DEFAULT_CONFIG: SatelliteConfig;
40
40
  /** Merge a partial config (from config.json) onto the defaults. */
41
41
  export declare function loadConfig(partial: Partial<SatelliteConfig>): SatelliteConfig;
42
+ /**
43
+ * Parse the `wakewordModel` config into a list of wake words. Accepts several words separated by
44
+ * commas or whitespace (e.g. `"hey_jarvis, alexa"`). An empty spec yields `['']`, which `ensureModels`
45
+ * resolves to the built-in "hey_jarvis".
46
+ */
47
+ export declare function parseWakewords(spec: string): string[];
package/build/config.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DEFAULT_CONFIG = void 0;
4
4
  exports.loadConfig = loadConfig;
5
+ exports.parseWakewords = parseWakewords;
5
6
  exports.DEFAULT_CONFIG = {
6
7
  logLevel: 'info',
7
8
  device: 'satellite',
@@ -28,4 +29,16 @@ exports.DEFAULT_CONFIG = {
28
29
  function loadConfig(partial) {
29
30
  return { ...exports.DEFAULT_CONFIG, ...partial };
30
31
  }
32
+ /**
33
+ * Parse the `wakewordModel` config into a list of wake words. Accepts several words separated by
34
+ * commas or whitespace (e.g. `"hey_jarvis, alexa"`). An empty spec yields `['']`, which `ensureModels`
35
+ * resolves to the built-in "hey_jarvis".
36
+ */
37
+ function parseWakewords(spec) {
38
+ const list = (spec || '')
39
+ .split(/[,\s]+/)
40
+ .map(s => s.trim())
41
+ .filter(Boolean);
42
+ return list.length ? list : [''];
43
+ }
31
44
  //# sourceMappingURL=config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";;;AAsEA,gCAEC;AA1BY,QAAA,cAAc,GAAoB;IAC3C,QAAQ,EAAE,MAAM;IAChB,MAAM,EAAE,WAAW;IACnB,IAAI,EAAE,EAAE;IACR,IAAI,EAAE,EAAE;IACR,IAAI,EAAE,IAAI;IACV,UAAU,EAAE,IAAI;IAChB,YAAY,EAAE,MAAM;IACpB,SAAS,EAAE,SAAS;IACpB,aAAa,EAAE,SAAS;IACxB,aAAa,EAAE,EAAE;IACjB,iBAAiB,EAAE,GAAG;IACtB,SAAS,EAAE,QAAQ;IACnB,gBAAgB,EAAE,GAAG;IACrB,SAAS,EAAE,GAAG;IACd,WAAW,EAAE,GAAG;IAChB,WAAW,EAAE,IAAI;IACjB,eAAe,EAAE,CAAC;IAClB,OAAO,EAAE,IAAI;IACb,qBAAqB,EAAE,IAAI;IAC3B,mBAAmB,EAAE,KAAK;CAC7B,CAAC;AAEF,mEAAmE;AACnE,SAAgB,UAAU,CAAC,OAAiC;IACxD,OAAO,EAAE,GAAG,sBAAc,EAAE,GAAG,OAAO,EAAE,CAAC;AAC7C,CAAC","sourcesContent":["/** Satellite configuration (loaded from config.json / CLI). */\nexport interface SatelliteConfig {\n /** Log verbosity. 'debug' also prints the wake-word/mic diagnostics. */\n logLevel: 'info' | 'debug';\n\n /** Device identity reported to the adapter. */\n device: string;\n room: string;\n\n /** Adapter (voice-server) address. */\n host: string;\n port: number;\n /** UDP port the satellite listens on for TTS/control from the adapter. */\n listenPort: number;\n\n /** Audio backend: 'auto' (alsa on Linux, ffmpeg elsewhere), or force 'alsa' / 'ffmpeg'. */\n audioBackend: 'auto' | 'alsa' | 'ffmpeg';\n /**\n * Capture/playback devices. ALSA: e.g. \"plughw:2,0\". ffmpeg mic: dshow device name (Windows),\n * avfoundation index (macOS). \"default\" or \"\" = system default.\n */\n micDevice: string;\n speakerDevice: string;\n\n /** Wake word. modelPath: path to a wakeword .onnx ('' = built-in \"hey_jarvis\"). */\n wakewordModel: string;\n wakewordThreshold: number;\n /** Directory the melspectrogram/embedding/wakeword models are downloaded to/read from. */\n modelsDir: string;\n\n /** Recording (VAD) — silence detection to end an utterance. */\n silenceThreshold: number;\n silenceMs: number;\n minRecordMs: number;\n maxRecordMs: number;\n /** Number of pre-wake 80 ms chunks prepended to the recording (so no speech is lost). */\n preBufferChunks: number;\n\n /** Barge-in: if the wake word fires while the reply is playing, stop playback and listen again. */\n bargeIn: boolean;\n\n /** Heartbeat / registration. */\n registrationTimeoutMs: number;\n heartbeatIntervalMs: number;\n}\n\nexport const DEFAULT_CONFIG: SatelliteConfig = {\n logLevel: 'info',\n device: 'satellite',\n room: '',\n host: '',\n port: 7775,\n listenPort: 7776,\n audioBackend: 'auto',\n micDevice: 'default',\n speakerDevice: 'default',\n wakewordModel: '',\n wakewordThreshold: 0.5,\n modelsDir: 'models',\n silenceThreshold: 300,\n silenceMs: 800,\n minRecordMs: 800,\n maxRecordMs: 8000,\n preBufferChunks: 5,\n bargeIn: true,\n registrationTimeoutMs: 5000,\n heartbeatIntervalMs: 10000,\n};\n\n/** Merge a partial config (from config.json) onto the defaults. */\nexport function loadConfig(partial: Partial<SatelliteConfig>): SatelliteConfig {\n return { ...DEFAULT_CONFIG, ...partial };\n}\n"]}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";;;AAsEA,gCAEC;AAOD,wCAMC;AAvCY,QAAA,cAAc,GAAoB;IAC3C,QAAQ,EAAE,MAAM;IAChB,MAAM,EAAE,WAAW;IACnB,IAAI,EAAE,EAAE;IACR,IAAI,EAAE,EAAE;IACR,IAAI,EAAE,IAAI;IACV,UAAU,EAAE,IAAI;IAChB,YAAY,EAAE,MAAM;IACpB,SAAS,EAAE,SAAS;IACpB,aAAa,EAAE,SAAS;IACxB,aAAa,EAAE,EAAE;IACjB,iBAAiB,EAAE,GAAG;IACtB,SAAS,EAAE,QAAQ;IACnB,gBAAgB,EAAE,GAAG;IACrB,SAAS,EAAE,GAAG;IACd,WAAW,EAAE,GAAG;IAChB,WAAW,EAAE,IAAI;IACjB,eAAe,EAAE,CAAC;IAClB,OAAO,EAAE,IAAI;IACb,qBAAqB,EAAE,IAAI;IAC3B,mBAAmB,EAAE,KAAK;CAC7B,CAAC;AAEF,mEAAmE;AACnE,SAAgB,UAAU,CAAC,OAAiC;IACxD,OAAO,EAAE,GAAG,sBAAc,EAAE,GAAG,OAAO,EAAE,CAAC;AAC7C,CAAC;AAED;;;;GAIG;AACH,SAAgB,cAAc,CAAC,IAAY;IACvC,MAAM,IAAI,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;SACpB,KAAK,CAAC,QAAQ,CAAC;SACf,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SAClB,MAAM,CAAC,OAAO,CAAC,CAAC;IACrB,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AACrC,CAAC","sourcesContent":["/** Satellite configuration (loaded from config.json / CLI). */\nexport interface SatelliteConfig {\n /** Log verbosity. 'debug' also prints the wake-word/mic diagnostics. */\n logLevel: 'info' | 'debug';\n\n /** Device identity reported to the adapter. */\n device: string;\n room: string;\n\n /** Adapter (voice-server) address. */\n host: string;\n port: number;\n /** UDP port the satellite listens on for TTS/control from the adapter. */\n listenPort: number;\n\n /** Audio backend: 'auto' (alsa on Linux, ffmpeg elsewhere), or force 'alsa' / 'ffmpeg'. */\n audioBackend: 'auto' | 'alsa' | 'ffmpeg';\n /**\n * Capture/playback devices. ALSA: e.g. \"plughw:2,0\". ffmpeg mic: dshow device name (Windows),\n * avfoundation index (macOS). \"default\" or \"\" = system default.\n */\n micDevice: string;\n speakerDevice: string;\n\n /** Wake word. modelPath: path to a wakeword .onnx ('' = built-in \"hey_jarvis\"). */\n wakewordModel: string;\n wakewordThreshold: number;\n /** Directory the melspectrogram/embedding/wakeword models are downloaded to/read from. */\n modelsDir: string;\n\n /** Recording (VAD) — silence detection to end an utterance. */\n silenceThreshold: number;\n silenceMs: number;\n minRecordMs: number;\n maxRecordMs: number;\n /** Number of pre-wake 80 ms chunks prepended to the recording (so no speech is lost). */\n preBufferChunks: number;\n\n /** Barge-in: if the wake word fires while the reply is playing, stop playback and listen again. */\n bargeIn: boolean;\n\n /** Heartbeat / registration. */\n registrationTimeoutMs: number;\n heartbeatIntervalMs: number;\n}\n\nexport const DEFAULT_CONFIG: SatelliteConfig = {\n logLevel: 'info',\n device: 'satellite',\n room: '',\n host: '',\n port: 7775,\n listenPort: 7776,\n audioBackend: 'auto',\n micDevice: 'default',\n speakerDevice: 'default',\n wakewordModel: '',\n wakewordThreshold: 0.5,\n modelsDir: 'models',\n silenceThreshold: 300,\n silenceMs: 800,\n minRecordMs: 800,\n maxRecordMs: 8000,\n preBufferChunks: 5,\n bargeIn: true,\n registrationTimeoutMs: 5000,\n heartbeatIntervalMs: 10000,\n};\n\n/** Merge a partial config (from config.json) onto the defaults. */\nexport function loadConfig(partial: Partial<SatelliteConfig>): SatelliteConfig {\n return { ...DEFAULT_CONFIG, ...partial };\n}\n\n/**\n * Parse the `wakewordModel` config into a list of wake words. Accepts several words separated by\n * commas or whitespace (e.g. `\"hey_jarvis, alexa\"`). An empty spec yields `['']`, which `ensureModels`\n * resolves to the built-in \"hey_jarvis\".\n */\nexport function parseWakewords(spec: string): string[] {\n const list = (spec || '')\n .split(/[,\\s]+/)\n .map(s => s.trim())\n .filter(Boolean);\n return list.length ? list : [''];\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
@@ -1 +1 @@
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 mic.stop();\n await new Promise<void>(res => setTimeout(res, 200)); // let the last frames drain\n\n return { detected, peakScore, peakRms, frames, threshold: cfg.wakewordThreshold };\n}\n"]}
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"]}
@@ -1,4 +1,4 @@
1
- import type { SatelliteConfig } from './config';
1
+ import { type SatelliteConfig } from './config';
2
2
  import type { SatelliteHost } from './index';
3
3
  export declare class Satellite {
4
4
  private readonly cfg;
@@ -44,6 +44,7 @@ const audio_1 = require("./audio");
44
44
  const vad_1 = require("./vad");
45
45
  const wakeword_1 = require("./wakeword");
46
46
  const models_1 = require("./models");
47
+ const config_1 = require("./config");
47
48
  /** 1280 samples (80 ms) of 16 kHz mono 16-bit PCM per processing frame. */
48
49
  const FRAME_BYTES = 1280 * 2;
49
50
  const FRAME_MS = 80;
@@ -100,8 +101,9 @@ class Satellite {
100
101
  this.serverPort = addr.port;
101
102
  this.log.info(`Adapter address: ${this.serverHost}:${this.serverPort}`);
102
103
  await this.openSocket();
103
- const models = await (0, models_1.ensureModels)(this.cfg.modelsDir, this.cfg.wakewordModel, this.log);
104
- this.wakeword = new wakeword_1.WakeWord(models, this.cfg.wakewordThreshold, this.log);
104
+ const words = (0, config_1.parseWakewords)(this.cfg.wakewordModel);
105
+ const modelSets = await Promise.all(words.map(w => (0, models_1.ensureModels)(this.cfg.modelsDir, w, this.log)));
106
+ this.wakeword = new wakeword_1.WakeWord(modelSets, this.cfg.wakewordThreshold, this.log);
105
107
  await this.wakeword.load();
106
108
  this.plingPcm = (0, audio_1.pling)();
107
109
  // Retry until the adapter answers, so the satellite survives the adapter being down at boot.
@@ -118,7 +120,7 @@ class Satellite {
118
120
  clearInterval(this.heartbeatTimer);
119
121
  this.heartbeatTimer = null;
120
122
  }
121
- this.mic?.stop();
123
+ await this.mic?.stop();
122
124
  this.mic = null;
123
125
  const socket = this.socket;
124
126
  this.socket = null;
@@ -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 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 models = await ensureModels(this.cfg.modelsDir, this.cfg.wakewordModel, this.log);\n this.wakeword = new WakeWord(models, 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"]}
@@ -1,18 +1,20 @@
1
1
  import type { ModelPaths } from './models';
2
2
  import type { Logger } from './index';
3
3
  export declare class WakeWord {
4
- private readonly models;
5
4
  private readonly threshold;
6
5
  private readonly log;
7
6
  private melspec;
8
7
  private embedding;
9
- private classifier;
8
+ /** One classifier per wake word; the mel/embedding pipeline is shared. */
9
+ private classifiers;
10
10
  private melBuffer;
11
11
  private melProcessed;
12
12
  private embBuffer;
13
13
  private melShapeLogged;
14
14
  private embShapeLogged;
15
- constructor(models: ModelPaths, threshold: number, log: Logger);
15
+ /** `models` may hold several wake words; they must share the same melspec/embedding models. */
16
+ private readonly models;
17
+ constructor(models: ModelPaths | ModelPaths[], threshold: number, log: Logger);
16
18
  load(): Promise<void>;
17
19
  /** Reset the streaming buffers (call after a detection so it re-arms cleanly). */
18
20
  reset(): void;
@@ -25,5 +27,6 @@ export declare class WakeWord {
25
27
  triggered(score: number | null): boolean;
26
28
  private appendMel;
27
29
  private appendEmbeddings;
30
+ /** Run every wake-word classifier over the current embedding window; return the highest score. */
28
31
  private classify;
29
32
  }
package/build/wakeword.js CHANGED
@@ -55,30 +55,35 @@ const EMB_HOP = 8; // mel frames advanced per embedding
55
55
  const WW_FRAMES = 16; // embeddings per wake-word inference
56
56
  const EMB_DIM = 96;
57
57
  class WakeWord {
58
- models;
59
58
  threshold;
60
59
  log;
61
60
  melspec;
62
61
  embedding;
63
- classifier;
62
+ /** One classifier per wake word; the mel/embedding pipeline is shared. */
63
+ classifiers = [];
64
64
  melBuffer = []; // rows of 32 mel bins
65
65
  melProcessed = 0; // mel frames already turned into embeddings
66
66
  embBuffer = []; // rows of 96 embedding dims
67
67
  melShapeLogged = false;
68
68
  embShapeLogged = false;
69
+ /** `models` may hold several wake words; they must share the same melspec/embedding models. */
70
+ models;
69
71
  constructor(models, threshold, log) {
70
- this.models = models;
71
72
  this.threshold = threshold;
72
73
  this.log = log;
74
+ this.models = Array.isArray(models) ? models : [models];
75
+ if (!this.models.length) {
76
+ throw new Error('WakeWord: at least one model is required');
77
+ }
73
78
  }
74
79
  async load() {
75
- this.melspec = await ort.InferenceSession.create(this.models.melspec);
76
- this.embedding = await ort.InferenceSession.create(this.models.embedding);
77
- this.classifier = await ort.InferenceSession.create(this.models.wakeword);
78
- this.log.info('Wake-word models loaded.');
80
+ // melspec + embedding are identical across openWakeWord models — load them once from the first.
81
+ this.melspec = await ort.InferenceSession.create(this.models[0].melspec);
82
+ this.embedding = await ort.InferenceSession.create(this.models[0].embedding);
83
+ this.classifiers = await Promise.all(this.models.map(m => ort.InferenceSession.create(m.wakeword)));
84
+ this.log.info(`Wake-word models loaded (${this.classifiers.length} wake word(s)).`);
79
85
  this.log.info(` melspec IO: in=[${this.melspec.inputNames.join(', ')}] out=[${this.melspec.outputNames.join(', ')}]`);
80
86
  this.log.info(` embedding IO: in=[${this.embedding.inputNames.join(', ')}] out=[${this.embedding.outputNames.join(', ')}]`);
81
- this.log.info(` classifier IO: in=[${this.classifier.inputNames.join(', ')}] out=[${this.classifier.outputNames.join(', ')}]`);
82
87
  }
83
88
  /** Reset the streaming buffers (call after a detection so it re-arms cleanly). */
84
89
  reset() {
@@ -159,9 +164,8 @@ class WakeWord {
159
164
  this.embBuffer.splice(0, this.embBuffer.length - WW_FRAMES * 4);
160
165
  }
161
166
  }
167
+ /** Run every wake-word classifier over the current embedding window; return the highest score. */
162
168
  async classify() {
163
- const inName = this.classifier.inputNames[0];
164
- const outName = this.classifier.outputNames[0];
165
169
  const window = this.embBuffer.slice(-WW_FRAMES);
166
170
  const flat = new Float32Array(WW_FRAMES * EMB_DIM);
167
171
  for (let f = 0; f < WW_FRAMES; f++) {
@@ -169,10 +173,17 @@ class WakeWord {
169
173
  flat[f * EMB_DIM + d] = window[f][d];
170
174
  }
171
175
  }
172
- const out = await this.classifier.run({
173
- [inName]: new ort.Tensor('float32', flat, [1, WW_FRAMES, EMB_DIM]),
174
- });
175
- return out[outName].data[0];
176
+ let best = 0;
177
+ for (const clf of this.classifiers) {
178
+ const out = await clf.run({
179
+ [clf.inputNames[0]]: new ort.Tensor('float32', flat, [1, WW_FRAMES, EMB_DIM]),
180
+ });
181
+ const score = out[clf.outputNames[0]].data[0];
182
+ if (score > best) {
183
+ best = score;
184
+ }
185
+ }
186
+ return best;
176
187
  }
177
188
  }
178
189
  exports.WakeWord = WakeWord;
@@ -1 +1 @@
1
- {"version":3,"file":"wakeword.js","sourceRoot":"","sources":["../src/wakeword.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * OpenWakeWord inference in Node via onnxruntime-node.\n *\n * Pipeline (streaming): 16 kHz mono int16 audio → melspectrogram model → sliding embedding model\n * (Google speech embeddings) → wake-word classifier → score in [0,1].\n *\n * Shapes follow the openWakeWord models:\n * melspec: in [1, samples] (int16 values as float32) → out [1,1,F,32]; then mel = mel/10 + 2\n * embedding: in [1,76,32,1] → out [1,1,1,96]; window of 76 mel frames, hop 8\n * wakeword: in [1,16,96] → out [1,1] ; last 16 embeddings\n *\n * ⚠️ The exact frame counts (mel frames per chunk, warm-up) can vary by model version — validate the\n * detection threshold on the target device and adjust `wakewordThreshold`.\n */\nimport * as ort from 'onnxruntime-node';\nimport type { ModelPaths } from './models';\nimport type { Logger } from './index';\n\nconst MEL_BINS = 32;\nconst EMB_WINDOW = 76; // mel frames per embedding\nconst EMB_HOP = 8; // mel frames advanced per embedding\nconst WW_FRAMES = 16; // embeddings per wake-word inference\nconst EMB_DIM = 96;\n\nexport class WakeWord {\n private melspec!: ort.InferenceSession;\n private embedding!: ort.InferenceSession;\n private classifier!: ort.InferenceSession;\n\n private melBuffer: number[][] = []; // rows of 32 mel bins\n private melProcessed = 0; // mel frames already turned into embeddings\n private embBuffer: number[][] = []; // rows of 96 embedding dims\n private melShapeLogged = false;\n private embShapeLogged = false;\n\n constructor(\n private readonly models: ModelPaths,\n private readonly threshold: number,\n private readonly log: Logger,\n ) {}\n\n async load(): Promise<void> {\n this.melspec = await ort.InferenceSession.create(this.models.melspec);\n this.embedding = await ort.InferenceSession.create(this.models.embedding);\n this.classifier = await ort.InferenceSession.create(this.models.wakeword);\n this.log.info('Wake-word models loaded.');\n this.log.info(\n ` melspec IO: in=[${this.melspec.inputNames.join(', ')}] out=[${this.melspec.outputNames.join(', ')}]`,\n );\n this.log.info(\n ` embedding IO: in=[${this.embedding.inputNames.join(', ')}] out=[${this.embedding.outputNames.join(', ')}]`,\n );\n this.log.info(\n ` classifier IO: in=[${this.classifier.inputNames.join(', ')}] out=[${this.classifier.outputNames.join(', ')}]`,\n );\n }\n\n /** Reset the streaming buffers (call after a detection so it re-arms cleanly). */\n reset(): void {\n this.melBuffer = [];\n this.melProcessed = 0;\n this.embBuffer = [];\n }\n\n /**\n * Feed one audio chunk (16-bit signed LE PCM, ideally ~1280 samples = 80 ms) and return the\n * current wake-word score, or null if not enough context yet.\n */\n async process(pcm: Buffer): Promise<number | null> {\n await this.appendMel(pcm);\n await this.appendEmbeddings();\n if (this.embBuffer.length < WW_FRAMES) {\n return null;\n }\n return this.classify();\n }\n\n /** True when the given score crosses the configured detection threshold. */\n triggered(score: number | null): boolean {\n return score !== null && score >= this.threshold;\n }\n\n private async appendMel(pcm: Buffer): Promise<void> {\n const n = Math.floor(pcm.length / 2);\n const audio = new Float32Array(n);\n for (let i = 0; i < n; i++) {\n audio[i] = pcm.readInt16LE(i * 2); // int16 value as float (openWakeWord convention)\n }\n const inName = this.melspec.inputNames[0];\n const outName = this.melspec.outputNames[0];\n const out = await this.melspec.run({ [inName]: new ort.Tensor('float32', audio, [1, n]) });\n const t = out[outName];\n const data = t.data as Float32Array;\n if (!this.melShapeLogged) {\n this.log.info(`melspec output dims for ${n} samples: [${t.dims.join(',')}] (expected [1,1,F,32])`);\n this.melShapeLogged = true;\n }\n // dims [1,1,F,32]\n const frames = Number(t.dims[2]);\n for (let f = 0; f < frames; f++) {\n const row = new Array<number>(MEL_BINS);\n for (let b = 0; b < MEL_BINS; b++) {\n row[b] = data[f * MEL_BINS + b] / 10 + 2; // openWakeWord mel transform\n }\n this.melBuffer.push(row);\n }\n }\n\n private async appendEmbeddings(): Promise<void> {\n const inName = this.embedding.inputNames[0];\n const outName = this.embedding.outputNames[0];\n while (this.melProcessed + EMB_WINDOW <= this.melBuffer.length) {\n const flat = new Float32Array(EMB_WINDOW * MEL_BINS);\n for (let f = 0; f < EMB_WINDOW; f++) {\n const row = this.melBuffer[this.melProcessed + f];\n for (let b = 0; b < MEL_BINS; b++) {\n flat[f * MEL_BINS + b] = row[b];\n }\n }\n const out = await this.embedding.run({\n [inName]: new ort.Tensor('float32', flat, [1, EMB_WINDOW, MEL_BINS, 1]),\n });\n if (!this.embShapeLogged) {\n this.log.info(`embedding output dims: [${out[outName].dims.join(',')}] (expected [1,1,1,96])`);\n this.embShapeLogged = true;\n }\n const emb = out[outName].data as Float32Array; // [1,1,1,96]\n this.embBuffer.push(Array.from(emb.slice(0, EMB_DIM)));\n this.melProcessed += EMB_HOP;\n }\n // Trim consumed mel frames to keep memory bounded.\n if (this.melProcessed > EMB_WINDOW) {\n const drop = this.melProcessed - EMB_WINDOW;\n this.melBuffer.splice(0, drop);\n this.melProcessed -= drop;\n }\n if (this.embBuffer.length > WW_FRAMES * 4) {\n this.embBuffer.splice(0, this.embBuffer.length - WW_FRAMES * 4);\n }\n }\n\n private async classify(): Promise<number> {\n const inName = this.classifier.inputNames[0];\n const outName = this.classifier.outputNames[0];\n const window = this.embBuffer.slice(-WW_FRAMES);\n const flat = new Float32Array(WW_FRAMES * EMB_DIM);\n for (let f = 0; f < WW_FRAMES; f++) {\n for (let d = 0; d < EMB_DIM; d++) {\n flat[f * EMB_DIM + d] = window[f][d];\n }\n }\n const out = await this.classifier.run({\n [inName]: new ort.Tensor('float32', flat, [1, WW_FRAMES, EMB_DIM]),\n });\n return (out[outName].data as Float32Array)[0];\n }\n}\n"]}
1
+ {"version":3,"file":"wakeword.js","sourceRoot":"","sources":["../src/wakeword.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * OpenWakeWord inference in Node via onnxruntime-node.\n *\n * Pipeline (streaming): 16 kHz mono int16 audio → melspectrogram model → sliding embedding model\n * (Google speech embeddings) → wake-word classifier → score in [0,1].\n *\n * Shapes follow the openWakeWord models:\n * melspec: in [1, samples] (int16 values as float32) → out [1,1,F,32]; then mel = mel/10 + 2\n * embedding: in [1,76,32,1] → out [1,1,1,96]; window of 76 mel frames, hop 8\n * wakeword: in [1,16,96] → out [1,1] ; last 16 embeddings\n *\n * ⚠️ The exact frame counts (mel frames per chunk, warm-up) can vary by model version — validate the\n * detection threshold on the target device and adjust `wakewordThreshold`.\n */\nimport * as ort from 'onnxruntime-node';\nimport type { ModelPaths } from './models';\nimport type { Logger } from './index';\n\nconst MEL_BINS = 32;\nconst EMB_WINDOW = 76; // mel frames per embedding\nconst EMB_HOP = 8; // mel frames advanced per embedding\nconst WW_FRAMES = 16; // embeddings per wake-word inference\nconst EMB_DIM = 96;\n\nexport class WakeWord {\n private melspec!: ort.InferenceSession;\n private embedding!: ort.InferenceSession;\n /** One classifier per wake word; the mel/embedding pipeline is shared. */\n private classifiers: ort.InferenceSession[] = [];\n\n private melBuffer: number[][] = []; // rows of 32 mel bins\n private melProcessed = 0; // mel frames already turned into embeddings\n private embBuffer: number[][] = []; // rows of 96 embedding dims\n private melShapeLogged = false;\n private embShapeLogged = false;\n\n /** `models` may hold several wake words; they must share the same melspec/embedding models. */\n private readonly models: ModelPaths[];\n\n constructor(\n models: ModelPaths | ModelPaths[],\n private readonly threshold: number,\n private readonly log: Logger,\n ) {\n this.models = Array.isArray(models) ? models : [models];\n if (!this.models.length) {\n throw new Error('WakeWord: at least one model is required');\n }\n }\n\n async load(): Promise<void> {\n // melspec + embedding are identical across openWakeWord models — load them once from the first.\n this.melspec = await ort.InferenceSession.create(this.models[0].melspec);\n this.embedding = await ort.InferenceSession.create(this.models[0].embedding);\n this.classifiers = await Promise.all(this.models.map(m => ort.InferenceSession.create(m.wakeword)));\n this.log.info(`Wake-word models loaded (${this.classifiers.length} wake word(s)).`);\n this.log.info(\n ` melspec IO: in=[${this.melspec.inputNames.join(', ')}] out=[${this.melspec.outputNames.join(', ')}]`,\n );\n this.log.info(\n ` embedding IO: in=[${this.embedding.inputNames.join(', ')}] out=[${this.embedding.outputNames.join(', ')}]`,\n );\n }\n\n /** Reset the streaming buffers (call after a detection so it re-arms cleanly). */\n reset(): void {\n this.melBuffer = [];\n this.melProcessed = 0;\n this.embBuffer = [];\n }\n\n /**\n * Feed one audio chunk (16-bit signed LE PCM, ideally ~1280 samples = 80 ms) and return the\n * current wake-word score, or null if not enough context yet.\n */\n async process(pcm: Buffer): Promise<number | null> {\n await this.appendMel(pcm);\n await this.appendEmbeddings();\n if (this.embBuffer.length < WW_FRAMES) {\n return null;\n }\n return this.classify();\n }\n\n /** True when the given score crosses the configured detection threshold. */\n triggered(score: number | null): boolean {\n return score !== null && score >= this.threshold;\n }\n\n private async appendMel(pcm: Buffer): Promise<void> {\n const n = Math.floor(pcm.length / 2);\n const audio = new Float32Array(n);\n for (let i = 0; i < n; i++) {\n audio[i] = pcm.readInt16LE(i * 2); // int16 value as float (openWakeWord convention)\n }\n const inName = this.melspec.inputNames[0];\n const outName = this.melspec.outputNames[0];\n const out = await this.melspec.run({ [inName]: new ort.Tensor('float32', audio, [1, n]) });\n const t = out[outName];\n const data = t.data as Float32Array;\n if (!this.melShapeLogged) {\n this.log.info(`melspec output dims for ${n} samples: [${t.dims.join(',')}] (expected [1,1,F,32])`);\n this.melShapeLogged = true;\n }\n // dims [1,1,F,32]\n const frames = Number(t.dims[2]);\n for (let f = 0; f < frames; f++) {\n const row = new Array<number>(MEL_BINS);\n for (let b = 0; b < MEL_BINS; b++) {\n row[b] = data[f * MEL_BINS + b] / 10 + 2; // openWakeWord mel transform\n }\n this.melBuffer.push(row);\n }\n }\n\n private async appendEmbeddings(): Promise<void> {\n const inName = this.embedding.inputNames[0];\n const outName = this.embedding.outputNames[0];\n while (this.melProcessed + EMB_WINDOW <= this.melBuffer.length) {\n const flat = new Float32Array(EMB_WINDOW * MEL_BINS);\n for (let f = 0; f < EMB_WINDOW; f++) {\n const row = this.melBuffer[this.melProcessed + f];\n for (let b = 0; b < MEL_BINS; b++) {\n flat[f * MEL_BINS + b] = row[b];\n }\n }\n const out = await this.embedding.run({\n [inName]: new ort.Tensor('float32', flat, [1, EMB_WINDOW, MEL_BINS, 1]),\n });\n if (!this.embShapeLogged) {\n this.log.info(`embedding output dims: [${out[outName].dims.join(',')}] (expected [1,1,1,96])`);\n this.embShapeLogged = true;\n }\n const emb = out[outName].data as Float32Array; // [1,1,1,96]\n this.embBuffer.push(Array.from(emb.slice(0, EMB_DIM)));\n this.melProcessed += EMB_HOP;\n }\n // Trim consumed mel frames to keep memory bounded.\n if (this.melProcessed > EMB_WINDOW) {\n const drop = this.melProcessed - EMB_WINDOW;\n this.melBuffer.splice(0, drop);\n this.melProcessed -= drop;\n }\n if (this.embBuffer.length > WW_FRAMES * 4) {\n this.embBuffer.splice(0, this.embBuffer.length - WW_FRAMES * 4);\n }\n }\n\n /** Run every wake-word classifier over the current embedding window; return the highest score. */\n private async classify(): Promise<number> {\n const window = this.embBuffer.slice(-WW_FRAMES);\n const flat = new Float32Array(WW_FRAMES * EMB_DIM);\n for (let f = 0; f < WW_FRAMES; f++) {\n for (let d = 0; d < EMB_DIM; d++) {\n flat[f * EMB_DIM + d] = window[f][d];\n }\n }\n let best = 0;\n for (const clf of this.classifiers) {\n const out = await clf.run({\n [clf.inputNames[0]]: new ort.Tensor('float32', flat, [1, WW_FRAMES, EMB_DIM]),\n });\n const score = (out[clf.outputNames[0]].data as Float32Array)[0];\n if (score > best) {\n best = score;\n }\n }\n return best;\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iobroker/assistant-satellite",
3
- "version": "0.1.0",
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": {