@iobroker/assistant-satellite 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/build/config.d.ts +6 -0
- package/build/config.js +13 -0
- package/build/config.js.map +1 -1
- package/build/satellite.d.ts +1 -1
- package/build/satellite.js +4 -2
- package/build/satellite.js.map +1 -1
- package/build/wakeword.d.ts +6 -3
- package/build/wakeword.js +25 -14
- package/build/wakeword.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.1 (2026-07-05)
|
|
147
147
|
* (@GermanBluefox) Added Wake-word probe
|
|
148
148
|
|
|
149
149
|
### 0.0.4 (2026-07-05)
|
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
|
package/build/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";;;AAsEA,gCAEC;
|
|
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/satellite.d.ts
CHANGED
package/build/satellite.js
CHANGED
|
@@ -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
|
|
104
|
-
|
|
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.
|
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 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;;;GAGG;AACH,kDAAoC;AACpC,yCAUoB;AAEpB,mCAAiF;AACjF,+BAA6C;AAC7C,yCAAsC;AACtC,qCAAwC;AACxC,qCAAgE;AAGhE,2EAA2E;AAC3E,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,CAAC;AAC7B,MAAM,QAAQ,GAAG,EAAE,CAAC;AACpB,+FAA+F;AAC/F,MAAM,oBAAoB,GAAG,CAAC,CAAC;AAC/B,MAAM,wBAAwB,GAAG,MAAM,CAAC;AAExC,MAAM,KAAK,GAAG,CAAC,EAAU,EAAiB,EAAE,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;AAErF,MAAa,SAAS;IA0CG;IACA;IA1CJ,GAAG,CAAS;IACrB,MAAM,GAAwB,IAAI,CAAC;IACnC,UAAU,GAAG,EAAE,CAAC;IAChB,UAAU,GAAG,CAAC,CAAC;IAEf,QAAQ,CAAY;IACpB,GAAG,GAAe,IAAI,CAAC;IACd,OAAO,CAAe;IAC/B,QAAQ,GAAW,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC3C,mFAAmF;IAC3E,YAAY,GAAwB,IAAI,CAAC;IACzC,cAAc,GAA0B,IAAI,CAAC;IAErD,qBAAqB;IACb,YAAY,GAAW,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACvC,OAAO,GAAG,KAAK,CAAC;IAChB,cAAc,GAAG,KAAK,CAAC;IAE/B,2CAA2C;IACnC,UAAU,GAAG,CAAC,CAAC;IACf,YAAY,GAAG,CAAC,CAAC;IACjB,UAAU,GAAG,CAAC,CAAC;IAEvB,kBAAkB;IACV,SAAS,GAAG,KAAK,CAAC;IAClB,OAAO,GAA2B,IAAI,CAAC;IACvC,SAAS,GAAa,EAAE,CAAC;IACzB,SAAS,GAAG,CAAC,CAAC;IACd,UAAU,GAAG,CAAC,CAAC;IAEvB,eAAe;IACP,SAAS,GAAa,EAAE,CAAC;IACzB,UAAU,GAAG,KAAK,CAAC;IAEnB,eAAe,GAAwB,IAAI,CAAC;IAC5C,OAAO,GAAG,KAAK,CAAC;IAChB,eAAe,GAAG,CAAC,CAAC;IACpB,oBAAoB,GAAG,KAAK,CAAC;IAC7B,YAAY,GAAG,KAAK,CAAC;IAE7B,YACqB,GAAoB,EACpB,IAAmB;QADnB,QAAG,GAAH,GAAG,CAAiB;QACpB,SAAI,GAAJ,IAAI,CAAe;QAEpC,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC;QACpB,IAAI,CAAC,OAAO,GAAG,IAAA,sBAAc,EAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IACpD,CAAC;IAED,KAAK,CAAC,KAAK;QACP,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,MAAM,IAAI,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;QACnC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC;QAC5B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC;QAC5B,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,oBAAoB,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;QAExE,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC;QAExB,MAAM,KAAK,GAAG,IAAA,uBAAc,EAAC,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QACrD,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAA,qBAAY,EAAC,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACnG,IAAI,CAAC,QAAQ,GAAG,IAAI,mBAAQ,CAAC,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,iBAAiB,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;QAC9E,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QAC3B,IAAI,CAAC,QAAQ,GAAG,IAAA,aAAK,GAAE,CAAC;QAExB,6FAA6F;QAC7F,MAAM,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAC/B,IAAI,CAAC,cAAc,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;QAE5F,IAAI,CAAC,GAAG,GAAG,IAAI,WAAG,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;QAC/D,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;QACvC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QACvB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,cAAc,IAAI,CAAC,GAAG,CAAC,MAAM,wCAAwC,CAAC,CAAC;IACzF,CAAC;IAED,KAAK,CAAC,IAAI;QACN,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;QACrB,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACtB,aAAa,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;YACnC,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC/B,CAAC;QACD,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;QACjB,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC;QAChB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC3B,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,MAAM,EAAE,CAAC;YACT,MAAM,IAAI,OAAO,CAAO,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QAC9D,CAAC;IACL,CAAC;IAED,0EAA0E;IAElE,cAAc;QAClB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;QAClF,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;IACxD,CAAC;IAEO,UAAU;QACd,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACnC,MAAM,MAAM,GAAG,KAAK,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;YAC1C,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;YACrB,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;YAC7C,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YACnE,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAC7B,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,GAAG,EAAE;gBAClC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,4BAA4B,IAAI,CAAC,GAAG,CAAC,UAAU,GAAG,CAAC,CAAC;gBAClE,OAAO,EAAE,CAAC;YACd,CAAC,CAAC,CAAC;QACP,CAAC,CAAC,CAAC;IACP,CAAC;IAED,+FAA+F;IACvF,KAAK,CAAC,iBAAiB;QAC3B,IAAI,OAAO,GAAG,IAAI,CAAC;QACnB,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC;YAClB,IAAI,CAAC;gBACD,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACtB,IAAI,CAAC,oBAAoB,GAAG,KAAK,CAAC;gBAClC,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC;gBACzB,OAAO;YACX,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACT,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAI,CAAW,CAAC,OAAO,kBAAkB,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;gBACzF,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC;gBACrB,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,GAAG,CAAC,EAAE,wBAAwB,CAAC,CAAC;YAC9D,CAAC;QACL,CAAC;IACL,CAAC;IAED,mGAAmG;IAC3F,KAAK,CAAC,SAAS;QACnB,IAAI,IAAI,CAAC,YAAY,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YACrC,OAAO;QACX,CAAC;QACD,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;QACzB,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC;QACzB,IAAI,CAAC,oBAAoB,GAAG,KAAK,CAAC;QAClC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,wCAAwC,CAAC,CAAC;QACxD,IAAI,CAAC;YACD,MAAM,IAAI,CAAC,iBAAiB,EAAE,CAAC;YAC/B,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,iCAAiC,CAAC,CAAC;QACrD,CAAC;gBAAS,CAAC;YACP,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;QAC9B,CAAC;IACL,CAAC;IAEO,aAAa;QACjB,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACpB,OAAO;QACX,CAAC;QACD,IAAI,IAAI,CAAC,oBAAoB,EAAE,CAAC;YAC5B,IAAI,CAAC,eAAe,EAAE,CAAC;YACvB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,+BAA+B,IAAI,CAAC,eAAe,IAAI,oBAAoB,IAAI,CAAC,CAAC;YAC/F,IAAI,IAAI,CAAC,eAAe,IAAI,oBAAoB,EAAE,CAAC;gBAC/C,KAAK,IAAI,CAAC,SAAS,EAAE,CAAC;gBACtB,OAAO;YACX,CAAC;QACL,CAAC;QACD,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;QACjC,IAAI,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;IACrE,CAAC;IAEO,QAAQ;QACZ,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACnC,IAAI,CAAC,eAAe,GAAG,OAAO,CAAC;YAC/B,IAAI,CAAC,WAAW,CAAC;gBACb,IAAI,EAAE,UAAU;gBAChB,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM;gBACvB,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI;gBACnB,WAAW,EAAE,IAAI,CAAC,GAAG,CAAC,UAAU;aACnC,CAAC,CAAC;YACH,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,wBAAwB,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,UAAU,aAAa,IAAI,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC,CAAC;YAC3G,UAAU,CAAC,GAAG,EAAE;gBACZ,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;oBACvB,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;oBAC5B,MAAM,CAAC,IAAI,KAAK,CAAC,gCAAgC,IAAI,CAAC,GAAG,CAAC,qBAAqB,KAAK,CAAC,CAAC,CAAC;gBAC3F,CAAC;YACL,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;QACvC,CAAC,CAAC,CAAC;IACP,CAAC;IAED,0EAA0E;IAElE,SAAS,CAAC,KAAa;QAC3B,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC;YACvB,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;YAC3B,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,8CAA8C,KAAK,CAAC,MAAM,UAAU,CAAC,CAAC;QACxF,CAAC;QACD,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC,CAAC;QAC9D,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YAChB,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC;QACrB,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,IAAI;QACd,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,CAAC;YACD,OAAO,IAAI,CAAC,YAAY,CAAC,MAAM,IAAI,WAAW,EAAE,CAAC;gBAC7C,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC;gBACtE,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;gBAC5D,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;YAClC,CAAC;QACL,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,2BAA4B,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;QACtE,CAAC;gBAAS,CAAC;YACP,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;QACzB,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,WAAW,CAAC,KAAa;QACnC,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACjB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;YACtB,IAAI,CAAC,SAAS,EAAE,CAAC;YACjB,MAAM,KAAK,GAAG,IAAA,SAAG,EAAC,KAAK,CAAC,CAAC;YACzB,IAAI,KAAK,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;gBAC1B,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;YAC5B,CAAC;YACD,IAAI,IAAI,CAAC,OAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC5B,IAAI,CAAC,YAAY,EAAE,CAAC;YACxB,CAAC;YACD,OAAO;QACX,CAAC;QAED,yFAAyF;QACzF,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3B,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,CAAC;YACnD,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;QAC3B,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAEjD,6FAA6F;QAC7F,gFAAgF;QAChF,IAAI,CAAC,UAAU,EAAE,CAAC;QAClB,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;YAC9C,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;QAC9B,CAAC;QACD,MAAM,KAAK,GAAG,IAAA,SAAG,EAAC,KAAK,CAAC,CAAC;QACzB,IAAI,KAAK,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;YAC1B,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;QAC5B,CAAC;QACD,IAAI,IAAI,CAAC,UAAU,IAAI,EAAE,EAAE,CAAC;YACxB,IAAI,CAAC,GAAG,CAAC,KAAK,CACV,SAAS,IAAI,CAAC,UAAU,yBAAyB,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI;gBAC3E,cAAc,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,IAAI,CAAC,GAAG,CAAC,iBAAiB,GAAG,CAC7F,CAAC;YACF,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC;YACpB,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;YACtB,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC;QACxB,CAAC;QAED,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;YACjC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,6BAA8B,KAAgB,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;YAC7E,gFAAgF;YAChF,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;gBACxC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC;gBAC9C,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;gBACzB,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;YAC7B,CAAC;YACD,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;YACtB,MAAM,IAAI,CAAC,cAAc,EAAE,CAAC;QAChC,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,cAAc;QACxB,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;QAC5B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC,yCAAyC;QACjE,MAAM,IAAA,eAAO,EAAC,IAAI,CAAC,QAAQ,EAAE,4BAAiB,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;QACrG,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;QACxB,IAAI,CAAC,SAAS,GAAG,EAAE,CAAC;QAEpB,+FAA+F;QAC/F,iGAAiG;QACjG,uFAAuF;QACvF,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QACpC,IAAI,CAAC,SAAS,GAAG,EAAE,CAAC;QACpB,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC;QACnB,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC;QACpB,IAAI,CAAC,OAAO,GAAG,IAAI,qBAAe,CAC9B,IAAI,CAAC,GAAG,CAAC,gBAAgB,EACzB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,GAAG,QAAQ,CAAC,EACzC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,GAAG,QAAQ,CAAC,EAC3C,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,GAAG,QAAQ,CAAC,CAC9C,CAAC;QACF,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,0CAA0C,CAAC,CAAC;IAC9D,CAAC;IAEO,YAAY;QAChB,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;QACvB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;QACjE,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,GAAG,QAAQ,CAAC,CAAC;QAC9D,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,IAAI,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC;QACtE,IAAI,CAAC,GAAG,CAAC,IAAI,CACT,uBAAuB,IAAI,CAAC,SAAS,YAAY,IAAI,CAAC,SAAS,GAAG,QAAQ,QAAQ;YAC9E,YAAY,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,cAAc,MAAM,oBAAoB,CACrF,CAAC;IACN,CAAC;IAED,0EAA0E;IAElE,SAAS,CAAC,IAAY;QAC1B,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACf,OAAO;QACX,CAAC;QACD,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,IAAA,uBAAY,EAAC,IAAI,CAAC,CAAC;QAC7C,IAAI,IAAI,KAAK,qBAAU,EAAE,CAAC;YACtB,OAAO,CAAC,kCAAkC;QAC9C,CAAC;QACD,IAAI,IAAI,KAAK,mBAAQ,EAAE,CAAC;YACpB,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;gBACnB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;YAC9C,CAAC;YACD,OAAO;QACX,CAAC;QACD,IAAI,IAAI,KAAK,uBAAY,EAAE,CAAC;YACxB,OAAO;QACX,CAAC;QACD,IAAI,GAAkF,CAAC;QACvF,IAAI,CAAC;YACD,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;QAC/C,CAAC;QAAC,MAAM,CAAC;YACL,OAAO;QACX,CAAC;QACD,QAAQ,GAAG,CAAC,IAAI,EAAE,CAAC;YACf,KAAK,YAAY;gBACb,IAAI,GAAG,CAAC,EAAE,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;oBACjC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,+BAA+B,CAAC,CAAC;oBAC/C,IAAI,CAAC,eAAe,EAAE,CAAC;oBACvB,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;gBAChC,CAAC;gBACD,MAAM;YACV,KAAK,YAAY;gBACb,IAAI,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,uBAAwB,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;gBACzF,MAAM;YACV,KAAK,QAAQ;gBACT,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;oBACZ,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;gBAC9B,CAAC;gBACD,MAAM;YACV,KAAK,SAAS;gBACV,KAAK,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,4BAAiB,CAAC,CAAC;gBACxD,MAAM;YACV,KAAK,eAAe;gBAChB,IAAI,CAAC,oBAAoB,GAAG,KAAK,CAAC;gBAClC,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC;gBACzB,MAAM;QACd,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,OAAO,CAAC,UAAkB;QACpC,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC1C,IAAI,CAAC,SAAS,GAAG,EAAE,CAAC;QACpB,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;YACd,OAAO;QACX,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,GAAG,UAAU,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,UAAU,OAAO,CAAC,CAAC;QACnG,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,IAAA,eAAO,EAAC,GAAG,EAAE,UAAU,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;QAChG,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC,wCAAwC;QAClE,IAAI,CAAC;YACD,MAAM,IAAI,CAAC;QACf,CAAC;gBAAS,CAAC;YACP,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;QAC7B,CAAC;IACL,CAAC;IAED,0EAA0E;IAElE,SAAS,CAAC,GAAW;QACzB,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,IAAA,sBAAW,EAAC,GAAG,CAAC,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IAC1E,CAAC;IAEO,WAAW,CAAC,GAAgB;QAChC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,IAAA,wBAAa,EAAC,GAAG,CAAC,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IAC5E,CAAC;IAEO,SAAS,CAAC,KAAqB;QACnC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC;CACJ;AA3XD,8BA2XC","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"]}
|
package/build/wakeword.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
this.
|
|
77
|
-
this.
|
|
78
|
-
this.
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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;
|
package/build/wakeword.js.map
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.1.1",
|
|
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": {
|