iobroker.assistant 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/LICENSE +21 -0
- package/README.md +176 -0
- package/admin/assistant.svg +19 -0
- package/admin/custom/.vite/manifest.json +169 -0
- package/admin/custom/assets/Components-DcnuVFM6.js +1 -0
- package/admin/custom/assets/_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-DogEjv3U.js +1 -0
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-Dw5TkU1t.js +1 -0
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-BEFjTs9S.js +1 -0
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-C4r3udVz.js +1243 -0
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_iobroker_mf_1_json_mf_2_config__loadShare__.js-DlOBHPL0.js +1494 -0
- package/admin/custom/assets/hostInit-DKMI-sLp.js +1 -0
- package/admin/custom/assets/index-B8wW0kQx.js +1 -0
- package/admin/custom/assets/lib-DpE8sc4h.js +3 -0
- package/admin/custom/assets/mf-entry-bootstrap-0-e7fa582d.js +36 -0
- package/admin/custom/assets/rolldown-runtime-C0FnF6B9.js +1 -0
- package/admin/custom/assets/virtualExposes-DOTO22UK.js +1 -0
- package/admin/custom/assets/virtual_mf-REMOTE_ENTRY_ID___mfe_internal__ConfigCustomAssistant__customComponents_js-CQa59yVi.js +1 -0
- package/admin/custom/assets/virtual_mf-exposes-ssr___mfe_internal__ConfigCustomAssistant__customComponents_js-9g2Fj8tk.js +2 -0
- package/admin/custom/assets/virtual_mf-exposes___mfe_internal__ConfigCustomAssistant__customComponents_js-BTUQTbYo.js +2 -0
- package/admin/custom/assets/vite-preload-helper-B7qeedMF.js +1 -0
- package/admin/custom/customComponents.js +1 -0
- package/admin/custom/customComponents.ssr.js +1 -0
- package/admin/custom/i18n/de.json +58 -0
- package/admin/custom/i18n/en.json +58 -0
- package/admin/custom/i18n/es.json +34 -0
- package/admin/custom/i18n/fr.json +34 -0
- package/admin/custom/i18n/it.json +34 -0
- package/admin/custom/i18n/nl.json +34 -0
- package/admin/custom/i18n/pl.json +34 -0
- package/admin/custom/i18n/pt.json +34 -0
- package/admin/custom/i18n/ru.json +34 -0
- package/admin/custom/i18n/uk.json +34 -0
- package/admin/custom/i18n/zh-cn.json +34 -0
- package/admin/custom/index.html +32 -0
- package/admin/custom/mf-manifest.json +1 -0
- package/admin/custom/mf-stats.json +1 -0
- package/admin/i18n/de.json +130 -0
- package/admin/i18n/en.json +130 -0
- package/admin/i18n/es.json +106 -0
- package/admin/i18n/fr.json +106 -0
- package/admin/i18n/it.json +106 -0
- package/admin/i18n/nl.json +106 -0
- package/admin/i18n/pl.json +106 -0
- package/admin/i18n/pt.json +106 -0
- package/admin/i18n/ru.json +106 -0
- package/admin/i18n/uk.json +106 -0
- package/admin/i18n/zh-cn.json +106 -0
- package/admin/jsonConfig.json +743 -0
- package/admin/sounds/alarm.wav +0 -0
- package/admin/sounds/timer.wav +0 -0
- package/build/lib/alarms.js +281 -0
- package/build/lib/alarms.js.map +1 -0
- package/build/lib/context.js +52 -0
- package/build/lib/context.js.map +1 -0
- package/build/lib/credentials.js +121 -0
- package/build/lib/credentials.js.map +1 -0
- package/build/lib/llm.js +303 -0
- package/build/lib/llm.js.map +1 -0
- package/build/lib/localLlm.js +221 -0
- package/build/lib/localLlm.js.map +1 -0
- package/build/lib/memory.js +190 -0
- package/build/lib/memory.js.map +1 -0
- package/build/lib/nlu.js +929 -0
- package/build/lib/nlu.js.map +1 -0
- package/build/lib/timers.js +245 -0
- package/build/lib/timers.js.map +1 -0
- package/build/lib/tools.js +299 -0
- package/build/lib/tools.js.map +1 -0
- package/build/lib/voice/aws.js +102 -0
- package/build/lib/voice/aws.js.map +1 -0
- package/build/lib/voice/azure.js +144 -0
- package/build/lib/voice/azure.js.map +1 -0
- package/build/lib/voice/download.js +76 -0
- package/build/lib/voice/download.js.map +1 -0
- package/build/lib/voice/engines.js +89 -0
- package/build/lib/voice/engines.js.map +1 -0
- package/build/lib/voice/lang.js +33 -0
- package/build/lib/voice/lang.js.map +1 -0
- package/build/lib/voice/piper.js +191 -0
- package/build/lib/voice/piper.js.map +1 -0
- package/build/lib/voice/protocol.js +35 -0
- package/build/lib/voice/protocol.js.map +1 -0
- package/build/lib/voice/stt.js +116 -0
- package/build/lib/voice/stt.js.map +1 -0
- package/build/lib/voice/tts.js +36 -0
- package/build/lib/voice/tts.js.map +1 -0
- package/build/lib/voice/voiceServer.js +249 -0
- package/build/lib/voice/voiceServer.js.map +1 -0
- package/build/lib/voice/vosk.js +260 -0
- package/build/lib/voice/vosk.js.map +1 -0
- package/build/lib/voice/wyoming.js +320 -0
- package/build/lib/voice/wyoming.js.map +1 -0
- package/build/lib/weather.js +423 -0
- package/build/lib/weather.js.map +1 -0
- package/build/main.js +3152 -0
- package/build/main.js.map +1 -0
- package/io-package.json +528 -0
- package/package.json +68 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.OpenAiTts = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Text-to-speech engines. V1 ships the cloud engine (OpenAI TTS); local engines
|
|
9
|
+
* (Piper / Wyoming) plug in behind the same `TtsEngine` interface later.
|
|
10
|
+
*/
|
|
11
|
+
const openai_1 = __importDefault(require("openai"));
|
|
12
|
+
/**
|
|
13
|
+
* Cloud TTS via the OpenAI speech API. Requests `pcm` output — 24 kHz mono 16-bit signed LE —
|
|
14
|
+
* so no client-side decoding is needed; the satellite resamples to its speaker rate.
|
|
15
|
+
*/
|
|
16
|
+
class OpenAiTts {
|
|
17
|
+
client;
|
|
18
|
+
voice;
|
|
19
|
+
model;
|
|
20
|
+
constructor(apiKey, voice = 'alloy', model = 'tts-1') {
|
|
21
|
+
this.client = new openai_1.default({ apiKey });
|
|
22
|
+
this.voice = voice;
|
|
23
|
+
this.model = model;
|
|
24
|
+
}
|
|
25
|
+
async synthesize(text, _lang) {
|
|
26
|
+
const resp = await this.client.audio.speech.create({
|
|
27
|
+
model: this.model,
|
|
28
|
+
voice: this.voice,
|
|
29
|
+
input: text,
|
|
30
|
+
response_format: 'pcm',
|
|
31
|
+
});
|
|
32
|
+
return { pcm: Buffer.from(await resp.arrayBuffer()), sampleRate: 24000 };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exports.OpenAiTts = OpenAiTts;
|
|
36
|
+
//# sourceMappingURL=tts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tts.js","sourceRoot":"","sources":["../../../src/lib/voice/tts.ts"],"names":[],"mappings":";;;;;;AAAA;;;GAGG;AACH,oDAA4B;AAiB5B;;;GAGG;AACH,MAAa,SAAS;IACD,MAAM,CAAS;IACf,KAAK,CAAc;IACnB,KAAK,CAAS;IAE/B,YAAY,MAAc,EAAE,QAAqB,OAAO,EAAE,KAAK,GAAG,OAAO;QACrE,IAAI,CAAC,MAAM,GAAG,IAAI,gBAAM,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;QACrC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACvB,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,IAAY,EAAE,KAAa;QACxC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC;YAC/C,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,KAAK,EAAE,IAAI;YACX,eAAe,EAAE,KAAK;SACzB,CAAC,CAAC;QACH,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;IAC7E,CAAC;CACJ;AApBD,8BAoBC","sourcesContent":["/**\n * Text-to-speech engines. V1 ships the cloud engine (OpenAI TTS); local engines\n * (Piper / Wyoming) plug in behind the same `TtsEngine` interface later.\n */\nimport OpenAI from 'openai';\n\nexport interface TtsResult {\n /** Raw mono 16-bit signed LE PCM. */\n pcm: Buffer;\n sampleRate: number;\n}\n\nexport interface TtsEngine {\n synthesize(text: string, lang: string): Promise<TtsResult>;\n /** Optional warm-up (install/download engine + voice for `lang`) so the first request isn't slow. */\n prepare?(lang: string): Promise<void>;\n}\n\n/** OpenAI TTS voice ids. */\nexport type OpenAiVoice = 'alloy' | 'echo' | 'fable' | 'onyx' | 'nova' | 'shimmer';\n\n/**\n * Cloud TTS via the OpenAI speech API. Requests `pcm` output — 24 kHz mono 16-bit signed LE —\n * so no client-side decoding is needed; the satellite resamples to its speaker rate.\n */\nexport class OpenAiTts implements TtsEngine {\n private readonly client: OpenAI;\n private readonly voice: OpenAiVoice;\n private readonly model: string;\n\n constructor(apiKey: string, voice: OpenAiVoice = 'alloy', model = 'tts-1') {\n this.client = new OpenAI({ apiKey });\n this.voice = voice;\n this.model = model;\n }\n\n async synthesize(text: string, _lang: string): Promise<TtsResult> {\n const resp = await this.client.audio.speech.create({\n model: this.model,\n voice: this.voice,\n input: text,\n response_format: 'pcm',\n });\n return { pcm: Buffer.from(await resp.arrayBuffer()), sampleRate: 24000 };\n }\n}\n"]}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.VoiceServer = void 0;
|
|
37
|
+
/**
|
|
38
|
+
* UDP voice server — the "brain" side of the satellite protocol.
|
|
39
|
+
*
|
|
40
|
+
* Receives streamed microphone audio from satellites, runs STT → answer callback → TTS,
|
|
41
|
+
* and streams the spoken reply back. Registration + heartbeat keep the satellite registry
|
|
42
|
+
* live. Protocol is Hannah-compatible (see `protocol.ts`).
|
|
43
|
+
*/
|
|
44
|
+
const dgram = __importStar(require("node:dgram"));
|
|
45
|
+
const protocol_1 = require("./protocol");
|
|
46
|
+
/** Payload bytes per TTS datagram — well under the typical MTU-fragmentation-free UDP size. */
|
|
47
|
+
const TTS_CHUNK_BYTES = 8192;
|
|
48
|
+
/** Drop a satellite after this long without any packet (3× the satellite's 10 s heartbeat). */
|
|
49
|
+
const STALE_MS = 30_000;
|
|
50
|
+
class VoiceServer {
|
|
51
|
+
opts;
|
|
52
|
+
sock = null;
|
|
53
|
+
/** Keyed by `${address}:${port}` — audio packets carry no device name, only their source. */
|
|
54
|
+
sats = new Map();
|
|
55
|
+
sweepTimer = null;
|
|
56
|
+
constructor(opts) {
|
|
57
|
+
this.opts = opts;
|
|
58
|
+
}
|
|
59
|
+
start() {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const sock = dgram.createSocket('udp4');
|
|
62
|
+
this.sock = sock;
|
|
63
|
+
sock.on('message', (data, rinfo) => this.onMessage(data, rinfo));
|
|
64
|
+
sock.on('error', e => this.opts.log.error(`voice UDP error: ${e.message}`));
|
|
65
|
+
sock.once('error', reject);
|
|
66
|
+
// '0.0.0.0'/'' → bind all interfaces (omit the address); otherwise bind the given IP.
|
|
67
|
+
const addr = this.opts.bindAddress && this.opts.bindAddress !== '0.0.0.0' ? this.opts.bindAddress : undefined;
|
|
68
|
+
const onBound = () => {
|
|
69
|
+
this.opts.log.info(`Voice server listening on UDP ${addr || '0.0.0.0'}:${this.opts.port}`);
|
|
70
|
+
this.sweepTimer = setInterval(() => this.sweepStale(), 15_000);
|
|
71
|
+
resolve();
|
|
72
|
+
};
|
|
73
|
+
if (addr) {
|
|
74
|
+
sock.bind(this.opts.port, addr, onBound);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
sock.bind(this.opts.port, onBound);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async stop() {
|
|
82
|
+
if (this.sweepTimer) {
|
|
83
|
+
clearInterval(this.sweepTimer);
|
|
84
|
+
this.sweepTimer = null;
|
|
85
|
+
}
|
|
86
|
+
const sock = this.sock;
|
|
87
|
+
this.sock = null;
|
|
88
|
+
this.sats.clear();
|
|
89
|
+
if (sock) {
|
|
90
|
+
await new Promise(res => sock.close(() => res()));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
static addrKey(address, port) {
|
|
94
|
+
return `${address}:${port}`;
|
|
95
|
+
}
|
|
96
|
+
onMessage(data, rinfo) {
|
|
97
|
+
if (!data.length) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const { type, payload } = (0, protocol_1.decodePacket)(data);
|
|
101
|
+
if (type === protocol_1.TYPE_AUDIO) {
|
|
102
|
+
const sat = this.sats.get(VoiceServer.addrKey(rinfo.address, rinfo.port));
|
|
103
|
+
if (sat) {
|
|
104
|
+
sat.lastSeen = Date.now();
|
|
105
|
+
sat.chunks.push(Buffer.from(payload)); // copy: payload is a view onto a reused recv buffer
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (type === protocol_1.TYPE_CONTROL) {
|
|
110
|
+
let msg;
|
|
111
|
+
try {
|
|
112
|
+
msg = JSON.parse(payload.toString('utf8'));
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
this.onControl(msg, rinfo).catch(e => this.opts.log.warn(`voice control error: ${e.message}`));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async onControl(msg, rinfo) {
|
|
121
|
+
const key = VoiceServer.addrKey(rinfo.address, rinfo.port);
|
|
122
|
+
if (msg.type === 'register') {
|
|
123
|
+
const sat = {
|
|
124
|
+
device: msg.device || key,
|
|
125
|
+
room: msg.room || '',
|
|
126
|
+
address: rinfo.address,
|
|
127
|
+
port: rinfo.port,
|
|
128
|
+
lastSeen: Date.now(),
|
|
129
|
+
chunks: [],
|
|
130
|
+
};
|
|
131
|
+
this.sats.set(key, sat);
|
|
132
|
+
void this.send((0, protocol_1.encodeControl)({ type: 'registered', ok: true }), sat);
|
|
133
|
+
this.opts.log.info(`Satellite registered: ${sat.device} (room: ${sat.room || '-'}) @ ${key}`);
|
|
134
|
+
this.setStatus(sat, 'idle');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const sat = this.sats.get(key);
|
|
138
|
+
if (!sat) {
|
|
139
|
+
// Unknown source (server restarted?) — tell the satellite to register again.
|
|
140
|
+
this.sock?.send((0, protocol_1.encodeControl)({ type: 'reregister' }), rinfo.port, rinfo.address);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
sat.lastSeen = Date.now();
|
|
144
|
+
if (msg.type === 'heartbeat') {
|
|
145
|
+
void this.send((0, protocol_1.encodeControl)({ type: 'heartbeat_ack' }), sat);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (msg.type === 'audio_end') {
|
|
149
|
+
await this.handleUtterance(sat);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** STT → answer → TTS for one completed utterance, with status transitions along the way. */
|
|
153
|
+
async handleUtterance(sat) {
|
|
154
|
+
const pcm = Buffer.concat(sat.chunks);
|
|
155
|
+
sat.chunks = [];
|
|
156
|
+
if (!pcm.length) {
|
|
157
|
+
this.setStatus(sat, 'idle');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
this.setStatus(sat, 'processing');
|
|
161
|
+
try {
|
|
162
|
+
const hints = this.opts.getHints ? await this.opts.getHints() : undefined;
|
|
163
|
+
const text = await this.opts.stt.transcribe(pcm, protocol_1.AUDIO_SAMPLE_RATE, this.opts.language, hints);
|
|
164
|
+
this.opts.log.info(`Voice Q (${sat.device}): ${text || '(empty)'}`);
|
|
165
|
+
if (!text) {
|
|
166
|
+
this.setStatus(sat, 'idle');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const answer = (await this.opts.answer(text, { device: sat.device, room: sat.room })).trim();
|
|
170
|
+
this.opts.log.info(`Voice A (${sat.device}): ${answer}`);
|
|
171
|
+
if (!answer) {
|
|
172
|
+
this.setStatus(sat, 'idle');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
this.setStatus(sat, 'speaking');
|
|
176
|
+
const { pcm: ttsPcm, sampleRate } = await this.opts.tts.synthesize(answer, this.opts.language);
|
|
177
|
+
await this.streamTts(sat, ttsPcm, sampleRate);
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
this.opts.log.error(`Voice pipeline failed (${sat.device}): ${e.message}`);
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
this.setStatus(sat, 'idle');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async streamTts(sat, pcm, sampleRate) {
|
|
187
|
+
for (let off = 0, i = 0; off < pcm.length; off += TTS_CHUNK_BYTES, i++) {
|
|
188
|
+
await this.send((0, protocol_1.encodeTts)(pcm.subarray(off, off + TTS_CHUNK_BYTES)), sat);
|
|
189
|
+
// Yield every 16 packets so a long reply doesn't burst the OS send buffer / the satellite's recv.
|
|
190
|
+
if ((i & 15) === 15) {
|
|
191
|
+
await new Promise(res => setImmediate(res));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
await this.send((0, protocol_1.encodeControl)({ type: 'tts_end', sample_rate: sampleRate }), sat);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Play arbitrary PCM on one satellite (by device name) or all registered satellites (device=null).
|
|
198
|
+
* Used for announcements (`tts.text` / per-satellite `tts`), independent of the wake-word flow.
|
|
199
|
+
*/
|
|
200
|
+
async announce(device, pcm, sampleRate) {
|
|
201
|
+
if (!pcm.length) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const targets = [...this.sats.values()].filter(s => !device || s.device === device);
|
|
205
|
+
if (!targets.length) {
|
|
206
|
+
this.opts.log.warn(`announce: no ${device ? `satellite '${device}'` : 'satellites'} registered`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
await Promise.all(targets.map(async (sat) => {
|
|
210
|
+
this.setStatus(sat, 'speaking');
|
|
211
|
+
try {
|
|
212
|
+
await this.streamTts(sat, pcm, sampleRate);
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
this.setStatus(sat, 'idle');
|
|
216
|
+
}
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
/** Device names of the currently registered satellites. */
|
|
220
|
+
devices() {
|
|
221
|
+
return [...this.sats.values()].map(s => s.device);
|
|
222
|
+
}
|
|
223
|
+
setStatus(sat, state) {
|
|
224
|
+
void this.send((0, protocol_1.encodeControl)({ type: 'status', state }), sat);
|
|
225
|
+
this.opts.onStatus?.(sat.device, sat.room, state);
|
|
226
|
+
}
|
|
227
|
+
send(data, sat) {
|
|
228
|
+
return new Promise((resolve, reject) => {
|
|
229
|
+
const sock = this.sock;
|
|
230
|
+
if (!sock) {
|
|
231
|
+
resolve();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
sock.send(data, sat.port, sat.address, err => (err ? reject(err) : resolve()));
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
sweepStale() {
|
|
238
|
+
const now = Date.now();
|
|
239
|
+
for (const [key, sat] of this.sats) {
|
|
240
|
+
if (now - sat.lastSeen > STALE_MS) {
|
|
241
|
+
this.sats.delete(key);
|
|
242
|
+
this.opts.log.info(`Satellite offline (timeout): ${sat.device}`);
|
|
243
|
+
this.opts.onStatus?.(sat.device, sat.room, 'offline');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
exports.VoiceServer = VoiceServer;
|
|
249
|
+
//# sourceMappingURL=voiceServer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voiceServer.js","sourceRoot":"","sources":["../../../src/lib/voice/voiceServer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * UDP voice server — the \"brain\" side of the satellite protocol.\n *\n * Receives streamed microphone audio from satellites, runs STT → answer callback → TTS,\n * and streams the spoken reply back. Registration + heartbeat keep the satellite registry\n * live. Protocol is Hannah-compatible (see `protocol.ts`).\n */\nimport * as dgram from 'node:dgram';\nimport {\n AUDIO_SAMPLE_RATE,\n TYPE_AUDIO,\n TYPE_CONTROL,\n decodePacket,\n encodeControl,\n encodeTts,\n type SatToServer,\n type SatelliteState,\n} from './protocol';\nimport type { SttEngine } from './stt';\nimport type { TtsEngine } from './tts';\n\ninterface Satellite {\n device: string;\n room: string;\n address: string;\n port: number;\n lastSeen: number;\n /** Buffered audio chunks of the utterance currently being spoken. */\n chunks: Buffer[];\n}\n\nexport interface VoiceServerOptions {\n port: number;\n /** Interface to bind to ('0.0.0.0'/'' = all interfaces, or a specific IP). */\n bindAddress?: string;\n /** ISO-639-1 language hint for STT/TTS ('' = auto/provider default). */\n language: string;\n stt: SttEngine;\n tts: TtsEngine;\n /** Produce the assistant reply for a transcribed utterance. */\n answer: (question: string, ctx: { device: string; room: string }) => Promise<string>;\n /** Optional STT vocabulary hints (device/room names) to bias recognition; re-read per utterance. */\n getHints?: () => string[] | Promise<string[]>;\n log: ioBroker.Logger;\n /** Notified on every satellite state transition (idle/listening/processing/speaking/offline). */\n onStatus?: (device: string, room: string, state: SatelliteState | 'offline') => void;\n}\n\n/** Payload bytes per TTS datagram — well under the typical MTU-fragmentation-free UDP size. */\nconst TTS_CHUNK_BYTES = 8192;\n/** Drop a satellite after this long without any packet (3× the satellite's 10 s heartbeat). */\nconst STALE_MS = 30_000;\n\nexport class VoiceServer {\n private sock: dgram.Socket | null = null;\n /** Keyed by `${address}:${port}` — audio packets carry no device name, only their source. */\n private readonly sats = new Map<string, Satellite>();\n private sweepTimer: NodeJS.Timeout | null = null;\n\n constructor(private readonly opts: VoiceServerOptions) {}\n\n start(): Promise<void> {\n return new Promise((resolve, reject) => {\n const sock = dgram.createSocket('udp4');\n this.sock = sock;\n sock.on('message', (data, rinfo) => this.onMessage(data, rinfo));\n sock.on('error', e => this.opts.log.error(`voice UDP error: ${e.message}`));\n sock.once('error', reject);\n // '0.0.0.0'/'' → bind all interfaces (omit the address); otherwise bind the given IP.\n const addr =\n this.opts.bindAddress && this.opts.bindAddress !== '0.0.0.0' ? this.opts.bindAddress : undefined;\n const onBound = (): void => {\n this.opts.log.info(`Voice server listening on UDP ${addr || '0.0.0.0'}:${this.opts.port}`);\n this.sweepTimer = setInterval(() => this.sweepStale(), 15_000);\n resolve();\n };\n if (addr) {\n sock.bind(this.opts.port, addr, onBound);\n } else {\n sock.bind(this.opts.port, onBound);\n }\n });\n }\n\n async stop(): Promise<void> {\n if (this.sweepTimer) {\n clearInterval(this.sweepTimer);\n this.sweepTimer = null;\n }\n const sock = this.sock;\n this.sock = null;\n this.sats.clear();\n if (sock) {\n await new Promise<void>(res => sock.close(() => res()));\n }\n }\n\n private static addrKey(address: string, port: number): string {\n return `${address}:${port}`;\n }\n\n private onMessage(data: Buffer, rinfo: dgram.RemoteInfo): void {\n if (!data.length) {\n return;\n }\n const { type, payload } = decodePacket(data);\n if (type === TYPE_AUDIO) {\n const sat = this.sats.get(VoiceServer.addrKey(rinfo.address, rinfo.port));\n if (sat) {\n sat.lastSeen = Date.now();\n sat.chunks.push(Buffer.from(payload)); // copy: payload is a view onto a reused recv buffer\n }\n return;\n }\n if (type === TYPE_CONTROL) {\n let msg: SatToServer;\n try {\n msg = JSON.parse(payload.toString('utf8')) as SatToServer;\n } catch {\n return;\n }\n this.onControl(msg, rinfo).catch(e => this.opts.log.warn(`voice control error: ${(e as Error).message}`));\n }\n }\n\n private async onControl(msg: SatToServer, rinfo: dgram.RemoteInfo): Promise<void> {\n const key = VoiceServer.addrKey(rinfo.address, rinfo.port);\n\n if (msg.type === 'register') {\n const sat: Satellite = {\n device: msg.device || key,\n room: msg.room || '',\n address: rinfo.address,\n port: rinfo.port,\n lastSeen: Date.now(),\n chunks: [],\n };\n this.sats.set(key, sat);\n void this.send(encodeControl({ type: 'registered', ok: true }), sat);\n this.opts.log.info(`Satellite registered: ${sat.device} (room: ${sat.room || '-'}) @ ${key}`);\n this.setStatus(sat, 'idle');\n return;\n }\n\n const sat = this.sats.get(key);\n if (!sat) {\n // Unknown source (server restarted?) — tell the satellite to register again.\n this.sock?.send(encodeControl({ type: 'reregister' }), rinfo.port, rinfo.address);\n return;\n }\n sat.lastSeen = Date.now();\n\n if (msg.type === 'heartbeat') {\n void this.send(encodeControl({ type: 'heartbeat_ack' }), sat);\n return;\n }\n if (msg.type === 'audio_end') {\n await this.handleUtterance(sat);\n }\n }\n\n /** STT → answer → TTS for one completed utterance, with status transitions along the way. */\n private async handleUtterance(sat: Satellite): Promise<void> {\n const pcm = Buffer.concat(sat.chunks);\n sat.chunks = [];\n if (!pcm.length) {\n this.setStatus(sat, 'idle');\n return;\n }\n\n this.setStatus(sat, 'processing');\n try {\n const hints = this.opts.getHints ? await this.opts.getHints() : undefined;\n const text = await this.opts.stt.transcribe(pcm, AUDIO_SAMPLE_RATE, this.opts.language, hints);\n this.opts.log.info(`Voice Q (${sat.device}): ${text || '(empty)'}`);\n if (!text) {\n this.setStatus(sat, 'idle');\n return;\n }\n\n const answer = (await this.opts.answer(text, { device: sat.device, room: sat.room })).trim();\n this.opts.log.info(`Voice A (${sat.device}): ${answer}`);\n if (!answer) {\n this.setStatus(sat, 'idle');\n return;\n }\n\n this.setStatus(sat, 'speaking');\n const { pcm: ttsPcm, sampleRate } = await this.opts.tts.synthesize(answer, this.opts.language);\n await this.streamTts(sat, ttsPcm, sampleRate);\n } catch (e) {\n this.opts.log.error(`Voice pipeline failed (${sat.device}): ${(e as Error).message}`);\n } finally {\n this.setStatus(sat, 'idle');\n }\n }\n\n private async streamTts(sat: Satellite, pcm: Buffer, sampleRate: number): Promise<void> {\n for (let off = 0, i = 0; off < pcm.length; off += TTS_CHUNK_BYTES, i++) {\n await this.send(encodeTts(pcm.subarray(off, off + TTS_CHUNK_BYTES)), sat);\n // Yield every 16 packets so a long reply doesn't burst the OS send buffer / the satellite's recv.\n if ((i & 15) === 15) {\n await new Promise<void>(res => setImmediate(res));\n }\n }\n await this.send(encodeControl({ type: 'tts_end', sample_rate: sampleRate }), sat);\n }\n\n /**\n * Play arbitrary PCM on one satellite (by device name) or all registered satellites (device=null).\n * Used for announcements (`tts.text` / per-satellite `tts`), independent of the wake-word flow.\n */\n async announce(device: string | null, pcm: Buffer, sampleRate: number): Promise<void> {\n if (!pcm.length) {\n return;\n }\n const targets = [...this.sats.values()].filter(s => !device || s.device === device);\n if (!targets.length) {\n this.opts.log.warn(`announce: no ${device ? `satellite '${device}'` : 'satellites'} registered`);\n return;\n }\n await Promise.all(\n targets.map(async sat => {\n this.setStatus(sat, 'speaking');\n try {\n await this.streamTts(sat, pcm, sampleRate);\n } finally {\n this.setStatus(sat, 'idle');\n }\n }),\n );\n }\n\n /** Device names of the currently registered satellites. */\n devices(): string[] {\n return [...this.sats.values()].map(s => s.device);\n }\n\n private setStatus(sat: Satellite, state: SatelliteState): void {\n void this.send(encodeControl({ type: 'status', state }), sat);\n this.opts.onStatus?.(sat.device, sat.room, state);\n }\n\n private send(data: Buffer, sat: Satellite): Promise<void> {\n return new Promise((resolve, reject) => {\n const sock = this.sock;\n if (!sock) {\n resolve();\n return;\n }\n sock.send(data, sat.port, sat.address, err => (err ? reject(err) : resolve()));\n });\n }\n\n private sweepStale(): void {\n const now = Date.now();\n for (const [key, sat] of this.sats) {\n if (now - sat.lastSeen > STALE_MS) {\n this.sats.delete(key);\n this.opts.log.info(`Satellite offline (timeout): ${sat.device}`);\n this.opts.onStatus?.(sat.device, sat.room, 'offline');\n }\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.VoskStt = void 0;
|
|
37
|
+
exports.listVoskModels = listVoskModels;
|
|
38
|
+
/**
|
|
39
|
+
* Local STT via Vosk (https://alphacephei.com/vosk).
|
|
40
|
+
*
|
|
41
|
+
* The official `vosk` npm module depends on `ffi-napi`, which does NOT compile on Node ≥ 20/22
|
|
42
|
+
* (node-api finalizer signature change). So instead of that module we bind the prebuilt **libvosk**
|
|
43
|
+
* shared library directly via **koffi** — a maintained FFI shipped as prebuilt binaries (no node-gyp).
|
|
44
|
+
* Both koffi (npm) and libvosk (GitHub release) are installed on demand into the instance data dir,
|
|
45
|
+
* plus a small language model. Input is 16 kHz mono 16-bit PCM — exactly what satellites stream.
|
|
46
|
+
*/
|
|
47
|
+
const node_child_process_1 = require("node:child_process");
|
|
48
|
+
const node_module_1 = require("node:module");
|
|
49
|
+
const path = __importStar(require("node:path"));
|
|
50
|
+
const fs = __importStar(require("node:fs"));
|
|
51
|
+
const download_1 = require("./download");
|
|
52
|
+
const NPM = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
53
|
+
/** Prebuilt libvosk release to use (contains the shared library + header). */
|
|
54
|
+
const LIBVOSK_VERSION = '0.3.45';
|
|
55
|
+
/** Default small Vosk model per language (fast enough on a Pi). */
|
|
56
|
+
const MODEL = {
|
|
57
|
+
en: 'vosk-model-small-en-us-0.15',
|
|
58
|
+
de: 'vosk-model-small-de-0.15',
|
|
59
|
+
ru: 'vosk-model-small-ru-0.22',
|
|
60
|
+
fr: 'vosk-model-small-fr-0.22',
|
|
61
|
+
es: 'vosk-model-small-es-0.42',
|
|
62
|
+
it: 'vosk-model-small-it-0.22',
|
|
63
|
+
nl: 'vosk-model-small-nl-0.22',
|
|
64
|
+
pt: 'vosk-model-small-pt-0.3',
|
|
65
|
+
uk: 'vosk-model-small-uk-v3-small',
|
|
66
|
+
zh: 'vosk-model-small-cn-0.22',
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Curated Vosk models per language for the config dropdown (small first = the default; larger models are
|
|
70
|
+
* more accurate but heavier). See https://alphacephei.com/vosk/models for the full catalog.
|
|
71
|
+
*/
|
|
72
|
+
const MODEL_CATALOG = {
|
|
73
|
+
en: ['vosk-model-small-en-us-0.15', 'vosk-model-en-us-0.22', 'vosk-model-en-us-0.42-gigaspeech'],
|
|
74
|
+
de: ['vosk-model-small-de-0.15', 'vosk-model-de-0.21', 'vosk-model-de-tuda-0.6-900k'],
|
|
75
|
+
ru: ['vosk-model-small-ru-0.22', 'vosk-model-ru-0.42'],
|
|
76
|
+
fr: ['vosk-model-small-fr-0.22', 'vosk-model-fr-0.22'],
|
|
77
|
+
es: ['vosk-model-small-es-0.42'],
|
|
78
|
+
it: ['vosk-model-small-it-0.22', 'vosk-model-it-0.22'],
|
|
79
|
+
nl: ['vosk-model-small-nl-0.22', 'vosk-model-nl-spraakherkenning-0.6'],
|
|
80
|
+
pt: ['vosk-model-small-pt-0.3'],
|
|
81
|
+
uk: ['vosk-model-small-uk-v3-small', 'vosk-model-uk-v3'],
|
|
82
|
+
zh: ['vosk-model-small-cn-0.22', 'vosk-model-cn-0.22'],
|
|
83
|
+
};
|
|
84
|
+
/** Suggested Vosk model names for a language (small default first). Falls back to English. */
|
|
85
|
+
function listVoskModels(language) {
|
|
86
|
+
const iso = (language || 'en').split('-')[0].toLowerCase();
|
|
87
|
+
return MODEL_CATALOG[iso] || MODEL_CATALOG.en;
|
|
88
|
+
}
|
|
89
|
+
/** Prebuilt libvosk asset (release zip name + library file name) for the current platform. */
|
|
90
|
+
function libvoskAsset() {
|
|
91
|
+
const v = LIBVOSK_VERSION;
|
|
92
|
+
const p = process.platform;
|
|
93
|
+
const a = process.arch;
|
|
94
|
+
if (p === 'linux' && a === 'arm64') {
|
|
95
|
+
return { archive: `vosk-linux-aarch64-${v}`, libName: 'libvosk.so' };
|
|
96
|
+
}
|
|
97
|
+
if (p === 'linux' && (a === 'x64' || a === 'ia32')) {
|
|
98
|
+
return { archive: `vosk-linux-x86_64-${v}`, libName: 'libvosk.so' };
|
|
99
|
+
}
|
|
100
|
+
if (p === 'win32') {
|
|
101
|
+
return { archive: `vosk-win64-${v}`, libName: 'libvosk.dll' };
|
|
102
|
+
}
|
|
103
|
+
if (p === 'darwin') {
|
|
104
|
+
return { archive: `vosk-osx-${v}`, libName: 'libvosk.dyld' };
|
|
105
|
+
}
|
|
106
|
+
throw new Error(`No prebuilt libvosk for ${p}/${a}`);
|
|
107
|
+
}
|
|
108
|
+
/** Recursively find a file by name under a directory (for the extracted libvosk library). */
|
|
109
|
+
function findFile(dir, name) {
|
|
110
|
+
let entries;
|
|
111
|
+
try {
|
|
112
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
for (const e of entries) {
|
|
118
|
+
const full = path.join(dir, e.name);
|
|
119
|
+
if (e.isDirectory()) {
|
|
120
|
+
const hit = findFile(full, name);
|
|
121
|
+
if (hit) {
|
|
122
|
+
return hit;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else if (e.name === name) {
|
|
126
|
+
return full;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
function koffiInstalled(dataDir) {
|
|
132
|
+
return fs.existsSync(path.join(dataDir, 'node_modules', 'koffi', 'package.json'));
|
|
133
|
+
}
|
|
134
|
+
/** Install the prebuilt `koffi` FFI module into the data dir via a spawned `npm install` (no node-gyp). */
|
|
135
|
+
function installKoffi(dataDir, log) {
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
138
|
+
const pkg = path.join(dataDir, 'package.json');
|
|
139
|
+
if (!fs.existsSync(pkg)) {
|
|
140
|
+
fs.writeFileSync(pkg, JSON.stringify({ name: 'assistant-local-voice', private: true, version: '1.0.0' }));
|
|
141
|
+
}
|
|
142
|
+
log.info('Installing koffi (first use) — this takes a moment …');
|
|
143
|
+
const child = (0, node_child_process_1.spawn)(NPM, ['install', 'koffi', '--omit=dev', '--no-audit', '--no-fund'], {
|
|
144
|
+
cwd: dataDir,
|
|
145
|
+
shell: process.platform === 'win32',
|
|
146
|
+
});
|
|
147
|
+
child.stdout.on('data', d => log.debug(`npm: ${String(d).trim()}`));
|
|
148
|
+
child.stderr.on('data', d => log.debug(`npm: ${String(d).trim()}`));
|
|
149
|
+
child.on('error', reject);
|
|
150
|
+
child.on('close', code => (code === 0 ? resolve() : reject(new Error(`npm install koffi exited ${code}`))));
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
class VoskStt {
|
|
154
|
+
dataDir;
|
|
155
|
+
modelOverride;
|
|
156
|
+
log;
|
|
157
|
+
api = null;
|
|
158
|
+
ready = null;
|
|
159
|
+
models = new Map(); // iso → libvosk model pointer
|
|
160
|
+
constructor(dataDir,
|
|
161
|
+
/** Override model name/dir; '' → default per language. */
|
|
162
|
+
modelOverride, log) {
|
|
163
|
+
this.dataDir = dataDir;
|
|
164
|
+
this.modelOverride = modelOverride;
|
|
165
|
+
this.log = log;
|
|
166
|
+
}
|
|
167
|
+
ensureReady() {
|
|
168
|
+
if (!this.ready) {
|
|
169
|
+
this.ready = this.load().catch(e => {
|
|
170
|
+
this.ready = null;
|
|
171
|
+
throw e;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return this.ready;
|
|
175
|
+
}
|
|
176
|
+
/** Ensure koffi + libvosk are present, then bind the C API. */
|
|
177
|
+
async load() {
|
|
178
|
+
if (!koffiInstalled(this.dataDir)) {
|
|
179
|
+
await installKoffi(this.dataDir, this.log);
|
|
180
|
+
}
|
|
181
|
+
const libPath = await this.ensureLibvosk();
|
|
182
|
+
const req = (0, node_module_1.createRequire)(path.join(this.dataDir, 'package.json'));
|
|
183
|
+
const koffi = req('koffi');
|
|
184
|
+
const lib = koffi.load(libPath);
|
|
185
|
+
this.api = {
|
|
186
|
+
setLogLevel: lib.func('void vosk_set_log_level(int level)'),
|
|
187
|
+
modelNew: lib.func('void* vosk_model_new(const char* path)'),
|
|
188
|
+
recNew: lib.func('void* vosk_recognizer_new(void* model, float rate)'),
|
|
189
|
+
recFree: lib.func('void vosk_recognizer_free(void* rec)'),
|
|
190
|
+
recAccept: lib.func('int vosk_recognizer_accept_waveform(void* rec, const char* data, int len)'),
|
|
191
|
+
recFinal: lib.func('const char* vosk_recognizer_final_result(void* rec)'),
|
|
192
|
+
};
|
|
193
|
+
this.api.setLogLevel(-1); // silence vosk's own logging
|
|
194
|
+
this.log.info('Vosk (libvosk via koffi) loaded.');
|
|
195
|
+
}
|
|
196
|
+
/** Download + extract the prebuilt libvosk for this platform; return the absolute library path. */
|
|
197
|
+
async ensureLibvosk() {
|
|
198
|
+
const { archive, libName } = libvoskAsset();
|
|
199
|
+
const libDir = path.join(this.dataDir, 'vosk-lib');
|
|
200
|
+
const existing = findFile(libDir, libName);
|
|
201
|
+
if (existing) {
|
|
202
|
+
return existing;
|
|
203
|
+
}
|
|
204
|
+
const zip = path.join(libDir, `${archive}.zip`);
|
|
205
|
+
await (0, download_1.downloadFile)(`https://github.com/alphacep/vosk-api/releases/download/v${LIBVOSK_VERSION}/${archive}.zip`, zip, this.log);
|
|
206
|
+
await (0, download_1.extractArchive)(zip, libDir, this.log);
|
|
207
|
+
const found = findFile(libDir, libName);
|
|
208
|
+
if (!found) {
|
|
209
|
+
throw new Error(`libvosk (${libName}) not found after extracting ${archive}.zip`);
|
|
210
|
+
}
|
|
211
|
+
return found;
|
|
212
|
+
}
|
|
213
|
+
async ensureModel(lang) {
|
|
214
|
+
const iso = (lang || 'en').split('-')[0].toLowerCase();
|
|
215
|
+
const cached = this.models.get(iso);
|
|
216
|
+
if (cached) {
|
|
217
|
+
return cached;
|
|
218
|
+
}
|
|
219
|
+
const name = this.modelOverride || MODEL[iso] || MODEL.en;
|
|
220
|
+
const modelsDir = path.join(this.dataDir, 'vosk-models');
|
|
221
|
+
const modelDir = path.join(modelsDir, name);
|
|
222
|
+
if (!fs.existsSync(modelDir)) {
|
|
223
|
+
const zip = path.join(modelsDir, `${name}.zip`);
|
|
224
|
+
await (0, download_1.downloadFile)(`https://alphacephei.com/vosk/models/${name}.zip`, zip, this.log);
|
|
225
|
+
await (0, download_1.extractArchive)(zip, modelsDir, this.log);
|
|
226
|
+
}
|
|
227
|
+
const model = this.api.modelNew(modelDir);
|
|
228
|
+
if (!model) {
|
|
229
|
+
throw new Error(`vosk_model_new failed for ${modelDir}`);
|
|
230
|
+
}
|
|
231
|
+
this.models.set(iso, model);
|
|
232
|
+
return model;
|
|
233
|
+
}
|
|
234
|
+
/** Warm-up: install koffi + libvosk and download the model so the first command isn't slow. */
|
|
235
|
+
async prepare(lang) {
|
|
236
|
+
await this.ensureReady();
|
|
237
|
+
await this.ensureModel(lang);
|
|
238
|
+
}
|
|
239
|
+
// Note: the SttEngine `hints` vocabulary is intentionally not applied here. Vosk can bias only via a
|
|
240
|
+
// grammar (vosk_recognizer_new_grm), but that is a *hard* constraint — it restricts recognition to the
|
|
241
|
+
// listed words, which would break free-form questions (weather, general chat). Its words must also exist
|
|
242
|
+
// in the model's lexicon, which arbitrary device names usually don't. So we keep open dictation instead.
|
|
243
|
+
async transcribe(pcm, sampleRate, lang) {
|
|
244
|
+
await this.ensureReady();
|
|
245
|
+
const model = await this.ensureModel(lang);
|
|
246
|
+
const api = this.api;
|
|
247
|
+
const rec = api.recNew(model, sampleRate);
|
|
248
|
+
try {
|
|
249
|
+
api.recAccept(rec, pcm, pcm.length);
|
|
250
|
+
const json = api.recFinal(rec);
|
|
251
|
+
const res = JSON.parse(json || '{}');
|
|
252
|
+
return (res.text || '').trim();
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
api.recFree(rec);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
exports.VoskStt = VoskStt;
|
|
260
|
+
//# sourceMappingURL=vosk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vosk.js","sourceRoot":"","sources":["../../../src/lib/voice/vosk.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Local STT via Vosk (https://alphacephei.com/vosk).\n *\n * The official `vosk` npm module depends on `ffi-napi`, which does NOT compile on Node ≥ 20/22\n * (node-api finalizer signature change). So instead of that module we bind the prebuilt **libvosk**\n * shared library directly via **koffi** — a maintained FFI shipped as prebuilt binaries (no node-gyp).\n * Both koffi (npm) and libvosk (GitHub release) are installed on demand into the instance data dir,\n * plus a small language model. Input is 16 kHz mono 16-bit PCM — exactly what satellites stream.\n */\nimport { spawn } from 'node:child_process';\nimport { createRequire } from 'node:module';\nimport * as path from 'node:path';\nimport * as fs from 'node:fs';\nimport { downloadFile, extractArchive, type VoiceLogger } from './download';\nimport type { SttEngine } from './stt';\n\nconst NPM = process.platform === 'win32' ? 'npm.cmd' : 'npm';\n\n/** Prebuilt libvosk release to use (contains the shared library + header). */\nconst LIBVOSK_VERSION = '0.3.45';\n\n/** Default small Vosk model per language (fast enough on a Pi). */\nconst MODEL: Record<string, string> = {\n en: 'vosk-model-small-en-us-0.15',\n de: 'vosk-model-small-de-0.15',\n ru: 'vosk-model-small-ru-0.22',\n fr: 'vosk-model-small-fr-0.22',\n es: 'vosk-model-small-es-0.42',\n it: 'vosk-model-small-it-0.22',\n nl: 'vosk-model-small-nl-0.22',\n pt: 'vosk-model-small-pt-0.3',\n uk: 'vosk-model-small-uk-v3-small',\n zh: 'vosk-model-small-cn-0.22',\n};\n\n/**\n * Curated Vosk models per language for the config dropdown (small first = the default; larger models are\n * more accurate but heavier). See https://alphacephei.com/vosk/models for the full catalog.\n */\nconst MODEL_CATALOG: Record<string, string[]> = {\n en: ['vosk-model-small-en-us-0.15', 'vosk-model-en-us-0.22', 'vosk-model-en-us-0.42-gigaspeech'],\n de: ['vosk-model-small-de-0.15', 'vosk-model-de-0.21', 'vosk-model-de-tuda-0.6-900k'],\n ru: ['vosk-model-small-ru-0.22', 'vosk-model-ru-0.42'],\n fr: ['vosk-model-small-fr-0.22', 'vosk-model-fr-0.22'],\n es: ['vosk-model-small-es-0.42'],\n it: ['vosk-model-small-it-0.22', 'vosk-model-it-0.22'],\n nl: ['vosk-model-small-nl-0.22', 'vosk-model-nl-spraakherkenning-0.6'],\n pt: ['vosk-model-small-pt-0.3'],\n uk: ['vosk-model-small-uk-v3-small', 'vosk-model-uk-v3'],\n zh: ['vosk-model-small-cn-0.22', 'vosk-model-cn-0.22'],\n};\n\n/** Suggested Vosk model names for a language (small default first). Falls back to English. */\nexport function listVoskModels(language: string): string[] {\n const iso = (language || 'en').split('-')[0].toLowerCase();\n return MODEL_CATALOG[iso] || MODEL_CATALOG.en;\n}\n\n/** Prebuilt libvosk asset (release zip name + library file name) for the current platform. */\nfunction libvoskAsset(): { archive: string; libName: string } {\n const v = LIBVOSK_VERSION;\n const p = process.platform;\n const a = process.arch;\n if (p === 'linux' && a === 'arm64') {\n return { archive: `vosk-linux-aarch64-${v}`, libName: 'libvosk.so' };\n }\n if (p === 'linux' && (a === 'x64' || a === 'ia32')) {\n return { archive: `vosk-linux-x86_64-${v}`, libName: 'libvosk.so' };\n }\n if (p === 'win32') {\n return { archive: `vosk-win64-${v}`, libName: 'libvosk.dll' };\n }\n if (p === 'darwin') {\n return { archive: `vosk-osx-${v}`, libName: 'libvosk.dyld' };\n }\n throw new Error(`No prebuilt libvosk for ${p}/${a}`);\n}\n\n/** Recursively find a file by name under a directory (for the extracted libvosk library). */\nfunction findFile(dir: string, name: string): string | null {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch {\n return null;\n }\n for (const e of entries) {\n const full = path.join(dir, e.name);\n if (e.isDirectory()) {\n const hit = findFile(full, name);\n if (hit) {\n return hit;\n }\n } else if (e.name === name) {\n return full;\n }\n }\n return null;\n}\n\nfunction koffiInstalled(dataDir: string): boolean {\n return fs.existsSync(path.join(dataDir, 'node_modules', 'koffi', 'package.json'));\n}\n\n/** Install the prebuilt `koffi` FFI module into the data dir via a spawned `npm install` (no node-gyp). */\nfunction installKoffi(dataDir: string, log: VoiceLogger): Promise<void> {\n return new Promise((resolve, reject) => {\n fs.mkdirSync(dataDir, { recursive: true });\n const pkg = path.join(dataDir, 'package.json');\n if (!fs.existsSync(pkg)) {\n fs.writeFileSync(pkg, JSON.stringify({ name: 'assistant-local-voice', private: true, version: '1.0.0' }));\n }\n log.info('Installing koffi (first use) — this takes a moment …');\n const child = spawn(NPM, ['install', 'koffi', '--omit=dev', '--no-audit', '--no-fund'], {\n cwd: dataDir,\n shell: process.platform === 'win32',\n });\n child.stdout.on('data', d => log.debug(`npm: ${String(d).trim()}`));\n child.stderr.on('data', d => log.debug(`npm: ${String(d).trim()}`));\n child.on('error', reject);\n child.on('close', code => (code === 0 ? resolve() : reject(new Error(`npm install koffi exited ${code}`))));\n });\n}\n\n/** libvosk C-API functions bound via koffi (opaque pointers as `void *`). */\ninterface VoskApi {\n setLogLevel: (level: number) => void;\n modelNew: (path: string) => unknown;\n recNew: (model: unknown, rate: number) => unknown;\n recFree: (rec: unknown) => void;\n recAccept: (rec: unknown, data: Buffer, len: number) => number;\n recFinal: (rec: unknown) => string;\n}\n\nexport class VoskStt implements SttEngine {\n private api: VoskApi | null = null;\n private ready: Promise<void> | null = null;\n private readonly models = new Map<string, unknown>(); // iso → libvosk model pointer\n\n constructor(\n private readonly dataDir: string,\n /** Override model name/dir; '' → default per language. */\n private readonly modelOverride: string,\n private readonly log: VoiceLogger,\n ) {}\n\n private ensureReady(): Promise<void> {\n if (!this.ready) {\n this.ready = this.load().catch(e => {\n this.ready = null;\n throw e;\n });\n }\n return this.ready;\n }\n\n /** Ensure koffi + libvosk are present, then bind the C API. */\n private async load(): Promise<void> {\n if (!koffiInstalled(this.dataDir)) {\n await installKoffi(this.dataDir, this.log);\n }\n const libPath = await this.ensureLibvosk();\n const req = createRequire(path.join(this.dataDir, 'package.json'));\n const koffi = req('koffi');\n const lib = koffi.load(libPath);\n this.api = {\n setLogLevel: lib.func('void vosk_set_log_level(int level)'),\n modelNew: lib.func('void* vosk_model_new(const char* path)'),\n recNew: lib.func('void* vosk_recognizer_new(void* model, float rate)'),\n recFree: lib.func('void vosk_recognizer_free(void* rec)'),\n recAccept: lib.func('int vosk_recognizer_accept_waveform(void* rec, const char* data, int len)'),\n recFinal: lib.func('const char* vosk_recognizer_final_result(void* rec)'),\n };\n this.api.setLogLevel(-1); // silence vosk's own logging\n this.log.info('Vosk (libvosk via koffi) loaded.');\n }\n\n /** Download + extract the prebuilt libvosk for this platform; return the absolute library path. */\n private async ensureLibvosk(): Promise<string> {\n const { archive, libName } = libvoskAsset();\n const libDir = path.join(this.dataDir, 'vosk-lib');\n const existing = findFile(libDir, libName);\n if (existing) {\n return existing;\n }\n const zip = path.join(libDir, `${archive}.zip`);\n await downloadFile(\n `https://github.com/alphacep/vosk-api/releases/download/v${LIBVOSK_VERSION}/${archive}.zip`,\n zip,\n this.log,\n );\n await extractArchive(zip, libDir, this.log);\n const found = findFile(libDir, libName);\n if (!found) {\n throw new Error(`libvosk (${libName}) not found after extracting ${archive}.zip`);\n }\n return found;\n }\n\n private async ensureModel(lang: string): Promise<unknown> {\n const iso = (lang || 'en').split('-')[0].toLowerCase();\n const cached = this.models.get(iso);\n if (cached) {\n return cached;\n }\n const name = this.modelOverride || MODEL[iso] || MODEL.en;\n const modelsDir = path.join(this.dataDir, 'vosk-models');\n const modelDir = path.join(modelsDir, name);\n if (!fs.existsSync(modelDir)) {\n const zip = path.join(modelsDir, `${name}.zip`);\n await downloadFile(`https://alphacephei.com/vosk/models/${name}.zip`, zip, this.log);\n await extractArchive(zip, modelsDir, this.log);\n }\n const model = this.api!.modelNew(modelDir);\n if (!model) {\n throw new Error(`vosk_model_new failed for ${modelDir}`);\n }\n this.models.set(iso, model);\n return model;\n }\n\n /** Warm-up: install koffi + libvosk and download the model so the first command isn't slow. */\n async prepare(lang: string): Promise<void> {\n await this.ensureReady();\n await this.ensureModel(lang);\n }\n\n // Note: the SttEngine `hints` vocabulary is intentionally not applied here. Vosk can bias only via a\n // grammar (vosk_recognizer_new_grm), but that is a *hard* constraint — it restricts recognition to the\n // listed words, which would break free-form questions (weather, general chat). Its words must also exist\n // in the model's lexicon, which arbitrary device names usually don't. So we keep open dictation instead.\n async transcribe(pcm: Buffer, sampleRate: number, lang: string): Promise<string> {\n await this.ensureReady();\n const model = await this.ensureModel(lang);\n const api = this.api!;\n const rec = api.recNew(model, sampleRate);\n try {\n api.recAccept(rec, pcm, pcm.length);\n const json = api.recFinal(rec);\n const res = JSON.parse(json || '{}') as { text?: string };\n return (res.text || '').trim();\n } finally {\n api.recFree(rec);\n }\n }\n}\n"]}
|