iobroker.assistant 1.0.3 → 1.2.0
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 +65 -0
- package/admin/custom/.vite/manifest.json +62 -62
- package/admin/custom/assets/Components-BSewCBfS.js +2 -0
- package/admin/custom/assets/{_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-A4gakomR.js → _virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-C5NPM0Of.js} +1 -1
- package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-CwqmfMvn.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-IaosaymG.js} +1 -1
- package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-BMuLYe6f.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-Cdyjt5KT.js} +39 -39
- package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__loadShare__shared-BxjZmnpc.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare__shared-Dsw5zWS8.js} +1 -1
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__prebuild___mf_0_iobroker_mf_1_json_mf_2_config__prebuild__-SgSagTOv.js +122 -0
- package/admin/custom/assets/{hostInit-4roC4hEj.js → hostInit-jJe8bIfV.js} +1 -1
- package/admin/custom/assets/{lib-CchzgTUs.js → lib-CL8mXvI6.js} +1 -1
- package/admin/custom/assets/{mf-entry-bootstrap-0-4806bbe4.js → mf-entry-bootstrap-0-ab227aa6.js} +2 -2
- package/admin/custom/assets/{pendingShares-DLlv8yFY.js → pendingShares-wrGtfliQ.js} +1 -1
- package/admin/custom/customComponents.js +2 -2
- package/admin/custom/i18n/de.json +40 -0
- package/admin/custom/i18n/en.json +40 -0
- package/admin/custom/i18n/es.json +40 -0
- package/admin/custom/i18n/fr.json +40 -0
- package/admin/custom/i18n/it.json +40 -0
- package/admin/custom/i18n/nl.json +40 -0
- package/admin/custom/i18n/pl.json +40 -0
- package/admin/custom/i18n/pt.json +40 -0
- package/admin/custom/i18n/ru.json +40 -0
- package/admin/custom/i18n/uk.json +40 -0
- package/admin/custom/i18n/zh-cn.json +40 -0
- package/admin/custom/index.html +2 -2
- package/admin/custom/mf-manifest.json +1 -1
- package/admin/custom/mf-stats.json +1 -1
- package/admin/i18n/de.json +13 -3
- package/admin/i18n/en.json +13 -3
- package/admin/i18n/es.json +13 -3
- package/admin/i18n/fr.json +13 -3
- package/admin/i18n/it.json +13 -3
- package/admin/i18n/nl.json +13 -3
- package/admin/i18n/pl.json +13 -3
- package/admin/i18n/pt.json +13 -3
- package/admin/i18n/ru.json +13 -3
- package/admin/i18n/uk.json +13 -3
- package/admin/i18n/zh-cn.json +13 -3
- package/admin/jsonConfig.json +61 -2
- package/build/lib/journal.js +298 -2
- package/build/lib/journal.js.map +1 -1
- package/build/lib/llm.js +105 -4
- package/build/lib/llm.js.map +1 -1
- package/build/lib/localLlm.js +47 -10
- package/build/lib/localLlm.js.map +1 -1
- package/build/lib/nlu.js +124 -5
- package/build/lib/nlu.js.map +1 -1
- package/build/lib/tools.js +3 -0
- package/build/lib/tools.js.map +1 -1
- package/build/lib/voice/esphome.js +81 -7
- package/build/lib/voice/esphome.js.map +1 -1
- package/build/lib/voice/esphomeProto.js.map +1 -1
- package/build/lib/voice/mediaServer.js +29 -1
- package/build/lib/voice/mediaServer.js.map +1 -1
- package/build/lib/voice/protocol.js.map +1 -1
- package/build/lib/voice/voiceServer.js +81 -7
- package/build/lib/voice/voiceServer.js.map +1 -1
- package/build/lib/voice/wakeWords.js +284 -0
- package/build/lib/voice/wakeWords.js.map +1 -0
- package/build/lib/voice/wyoming.js +12 -4
- package/build/lib/voice/wyoming.js.map +1 -1
- package/build/lib/weather.js +160 -0
- package/build/lib/weather.js.map +1 -1
- package/build/lib/youtube.js +625 -0
- package/build/lib/youtube.js.map +1 -0
- package/build/main.js +1152 -76
- package/build/main.js.map +1 -1
- package/io-package.json +30 -14
- package/package.json +6 -6
- package/admin/custom/assets/Components-C6qcpaRC.js +0 -1
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__prebuild___mf_0_iobroker_mf_1_json_mf_2_config__prebuild__-TM4l0OYm.js +0 -122
- /package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-BkH7fzDw.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-BkH7fzDw.js} +0 -0
- /package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__prebuild__react_mf_1_jsx_mf_2_runtime__prebuild__-M61rjtmp.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__prebuild__react_mf_1_jsx_mf_2_runtime__prebuild__-M61rjtmp.js} +0 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WAKE_WORD_URL_PREFIX = exports.WAKE_WORD_DIR = exports.WAKE_WORD_ID_RE = exports.MAX_MODEL_BYTES = void 0;
|
|
4
|
+
exports.slugify = slugify;
|
|
5
|
+
exports.isTfliteModel = isTfliteModel;
|
|
6
|
+
exports.readTfliteIo = readTfliteIo;
|
|
7
|
+
exports.microModelProblem = microModelProblem;
|
|
8
|
+
exports.prepareWakeWord = prepareWakeWord;
|
|
9
|
+
exports.loadWakeWord = loadWakeWord;
|
|
10
|
+
/**
|
|
11
|
+
* Custom wake words for ESPHome satellites — models the user uploads, offered to the devices as
|
|
12
|
+
* "external wake words".
|
|
13
|
+
*
|
|
14
|
+
* The ESPHome API has no upload message. Instead the client lists models in every
|
|
15
|
+
* `VoiceAssistantConfigurationRequest` (`external_wake_words`: id, phrase, languages, type, size,
|
|
16
|
+
* SHA-256 and the URL of a JSON config). The device adds them to its available list, and only when one
|
|
17
|
+
* is selected does it fetch the config from `url` and the model from the **same directory** as
|
|
18
|
+
* `<id>.tflite` — query and fragment are dropped — storing both as `<id>.json` / `<id>.tflite` next to
|
|
19
|
+
* its built-in models. Verified in the ThirdReality firmware (`linux-voice-assistant-cpp`,
|
|
20
|
+
* `Satellite.cpp` `ResolveWakeWordConfig`, `audio/ExternalWakeWord.cpp`). Consequences baked in here:
|
|
21
|
+
*
|
|
22
|
+
* - **Only microWakeWord models** (`model_type: "micro"`): the firmware skips any other type.
|
|
23
|
+
* - The device loads the model named by the JSON's `model` field, relative to the JSON — so the JSON we
|
|
24
|
+
* serve always says `<id>.tflite`, whatever the uploaded file was called.
|
|
25
|
+
* - Once downloaded, a model is a *local* one on the device and wins over the external offer by id. A
|
|
26
|
+
* changed model under the same id would never reach a device that already has it, so the id carries
|
|
27
|
+
* a short hash of the model: a new upload is a new wake word.
|
|
28
|
+
* - Downloaded models stay on the device; the API has no way to remove them.
|
|
29
|
+
*/
|
|
30
|
+
const node_crypto_1 = require("node:crypto");
|
|
31
|
+
/** microWakeWord models are 50–200 KB; anything near this is not one. */
|
|
32
|
+
exports.MAX_MODEL_BYTES = 1024 * 1024;
|
|
33
|
+
/** Ids are file names on the device and path segments in our URL. */
|
|
34
|
+
exports.WAKE_WORD_ID_RE = /^[a-z0-9_]{1,64}$/;
|
|
35
|
+
/** Folder in the instance's file storage (`assistant.0/wakewords/`). */
|
|
36
|
+
exports.WAKE_WORD_DIR = 'wakewords';
|
|
37
|
+
/** Where the media server serves them: `<prefix><id>.json` and `<prefix><id>.tflite`. */
|
|
38
|
+
exports.WAKE_WORD_URL_PREFIX = '/wakeword/';
|
|
39
|
+
/** Defaults of the official v2 models, used when a model comes without its JSON. */
|
|
40
|
+
const DEFAULT_MICRO = { probability_cutoff: 0.97, sliding_window_size: 5 };
|
|
41
|
+
/** `hey_hannah.tflite` → `hey_hannah`; anything that is not a-z/0-9 becomes `_`. */
|
|
42
|
+
function slugify(value) {
|
|
43
|
+
return value
|
|
44
|
+
.replace(/^.*[\\/]/, '')
|
|
45
|
+
.toLowerCase()
|
|
46
|
+
.replace(/\.(tflite|json)$/, '')
|
|
47
|
+
.normalize('NFKD')
|
|
48
|
+
.replace(/[̀-ͯ]/g, '')
|
|
49
|
+
.replace(/ß/g, 'ss')
|
|
50
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
51
|
+
.replace(/^_+|_+$/g, '')
|
|
52
|
+
.slice(0, 40)
|
|
53
|
+
.replace(/_+$/, '');
|
|
54
|
+
}
|
|
55
|
+
/** `hey_hannah` → `Hey Hannah` — a phrase for a model that came without its JSON. */
|
|
56
|
+
function phraseFromFileName(fileName) {
|
|
57
|
+
const base = fileName.replace(/^.*[\\/]/, '').replace(/\.(tflite|json)$/i, '');
|
|
58
|
+
return base
|
|
59
|
+
.split(/[_\-\s]+/)
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.map(w => w[0].toUpperCase() + w.slice(1))
|
|
62
|
+
.join(' ');
|
|
63
|
+
}
|
|
64
|
+
/** A TensorFlow Lite flatbuffer carries the file identifier `TFL3` at byte 4. */
|
|
65
|
+
function isTfliteModel(model) {
|
|
66
|
+
return model.length > 8 && model.toString('latin1', 4, 8) === 'TFL3';
|
|
67
|
+
}
|
|
68
|
+
/** microWakeWord feeds 40 mel features per frame (`MicroFeatures::kFeatureSize` on the device). */
|
|
69
|
+
const MICRO_FEATURES = 40;
|
|
70
|
+
/** openWakeWord classifiers take 96-value speech embeddings, e.g. input `[1, 16, 96]`. */
|
|
71
|
+
const OPEN_WAKE_WORD_FEATURES = 96;
|
|
72
|
+
/** Shapes longer than this are not a wake word model; it also bounds the loop over a broken file. */
|
|
73
|
+
const MAX_DIMENSIONS = 8;
|
|
74
|
+
/**
|
|
75
|
+
* Read the first input and output tensor of the main graph — the two the device checks before it runs a
|
|
76
|
+
* model. Walks the flatbuffer by hand (schema: `tensorflow/compiler/mlir/lite/schema/schema.fbs`), which
|
|
77
|
+
* spares a TensorFlow dependency for four fields. `null` when the file is not a readable model.
|
|
78
|
+
*/
|
|
79
|
+
function readTfliteIo(model) {
|
|
80
|
+
// Field positions in the schema's tables.
|
|
81
|
+
const MODEL_SUBGRAPHS = 2;
|
|
82
|
+
const SUBGRAPH_TENSORS = 0;
|
|
83
|
+
const SUBGRAPH_INPUTS = 1;
|
|
84
|
+
const SUBGRAPH_OUTPUTS = 2;
|
|
85
|
+
const TENSOR_SHAPE = 0;
|
|
86
|
+
const TENSOR_TYPE = 1;
|
|
87
|
+
const TENSOR_QUANTIZATION = 4;
|
|
88
|
+
const QUANTIZATION_SCALE = 2;
|
|
89
|
+
/** Position of field `index` of the table at `table`, 0 when it is absent. */
|
|
90
|
+
const field = (table, index) => {
|
|
91
|
+
const vtable = table - model.readInt32LE(table);
|
|
92
|
+
const offset = 4 + 2 * index;
|
|
93
|
+
if (offset >= model.readUInt16LE(vtable)) {
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
const at = model.readUInt16LE(vtable + offset);
|
|
97
|
+
return at ? table + at : 0;
|
|
98
|
+
};
|
|
99
|
+
/** Follow the offset stored in field `index` to the table, vector or string it points at. */
|
|
100
|
+
const ref = (table, index) => {
|
|
101
|
+
const at = field(table, index);
|
|
102
|
+
return at ? at + model.readUInt32LE(at) : 0;
|
|
103
|
+
};
|
|
104
|
+
const length = (vector) => (vector ? model.readUInt32LE(vector) : 0);
|
|
105
|
+
/** Element `k` of a vector of tables. */
|
|
106
|
+
const tableAt = (vector, k) => {
|
|
107
|
+
const at = vector + 4 + 4 * k;
|
|
108
|
+
return at + model.readUInt32LE(at);
|
|
109
|
+
};
|
|
110
|
+
const intAt = (vector, k) => model.readInt32LE(vector + 4 + 4 * k);
|
|
111
|
+
const tensor = (subgraph, list) => {
|
|
112
|
+
const tensors = ref(subgraph, SUBGRAPH_TENSORS);
|
|
113
|
+
if (!length(list)) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const index = intAt(list, 0);
|
|
117
|
+
if (index < 0 || index >= length(tensors)) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const t = tableAt(tensors, index);
|
|
121
|
+
const dims = ref(t, TENSOR_SHAPE);
|
|
122
|
+
if (length(dims) > MAX_DIMENSIONS) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const shape = Array.from({ length: length(dims) }, (_, k) => intAt(dims, k));
|
|
126
|
+
const typeAt = field(t, TENSOR_TYPE);
|
|
127
|
+
const quantization = ref(t, TENSOR_QUANTIZATION);
|
|
128
|
+
const scales = quantization ? ref(quantization, QUANTIZATION_SCALE) : 0;
|
|
129
|
+
return {
|
|
130
|
+
shape,
|
|
131
|
+
type: typeAt ? model.readUInt8(typeAt) : 0,
|
|
132
|
+
quantized: length(scales) > 0 && model.readFloatLE(scales + 4) !== 0,
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
if (!isTfliteModel(model)) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const subgraphs = ref(model.readUInt32LE(0), MODEL_SUBGRAPHS);
|
|
140
|
+
if (!length(subgraphs)) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const main = tableAt(subgraphs, 0);
|
|
144
|
+
const input = tensor(main, ref(main, SUBGRAPH_INPUTS));
|
|
145
|
+
const output = tensor(main, ref(main, SUBGRAPH_OUTPUTS));
|
|
146
|
+
return input && output ? { input, output } : null;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null; // an offset pointed outside the file
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Why the device cannot run this model, or `null` when it can. Mirrors the checks of the firmware's
|
|
154
|
+
* `MicroWakeWord::FromConfig`: input `[1, stride, 40]`, quantized input and output. An openWakeWord
|
|
155
|
+
* model is named as such — it is the most likely mix-up, both come as `.tflite`, and the device runs
|
|
156
|
+
* openWakeWord only when its firmware was started in that mode (then without the built-in models).
|
|
157
|
+
*
|
|
158
|
+
* @param model the `.tflite` file
|
|
159
|
+
*/
|
|
160
|
+
function microModelProblem(model) {
|
|
161
|
+
if (!isTfliteModel(model)) {
|
|
162
|
+
return 'the model file is not a TensorFlow Lite model (.tflite)';
|
|
163
|
+
}
|
|
164
|
+
const io = readTfliteIo(model);
|
|
165
|
+
if (!io) {
|
|
166
|
+
return 'the model file is damaged — it could not be read as a TensorFlow Lite model';
|
|
167
|
+
}
|
|
168
|
+
const shape = io.input.shape;
|
|
169
|
+
if (shape[shape.length - 1] === OPEN_WAKE_WORD_FEATURES) {
|
|
170
|
+
return 'this is an openWakeWord model — the device runs only microWakeWord models, so the wake word has to be trained with microWakeWord';
|
|
171
|
+
}
|
|
172
|
+
if (shape.length !== 3 || shape[2] !== MICRO_FEATURES) {
|
|
173
|
+
return `this is not a microWakeWord model (input [${shape.join(', ')}], a microWakeWord model takes [1, n, ${MICRO_FEATURES}])`;
|
|
174
|
+
}
|
|
175
|
+
if (!io.input.quantized || !io.output.quantized) {
|
|
176
|
+
return 'the model is not quantized — the device runs only quantized microWakeWord models';
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
function parseConfig(config) {
|
|
181
|
+
if (config === undefined || config === null || config === '') {
|
|
182
|
+
return {};
|
|
183
|
+
}
|
|
184
|
+
if (typeof config === 'object') {
|
|
185
|
+
return config;
|
|
186
|
+
}
|
|
187
|
+
let parsed;
|
|
188
|
+
try {
|
|
189
|
+
parsed = JSON.parse(config);
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
throw new Error(`the JSON file is not valid JSON: ${e.message}`);
|
|
193
|
+
}
|
|
194
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
195
|
+
throw new Error('the JSON file must contain an object');
|
|
196
|
+
}
|
|
197
|
+
return parsed;
|
|
198
|
+
}
|
|
199
|
+
function languageList(value) {
|
|
200
|
+
return Array.isArray(value) ? [...new Set(value.map(v => String(v).trim().toLowerCase()).filter(Boolean))] : [];
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Validate an upload and build the wake word the devices will download. Throws with a message meant for
|
|
204
|
+
* the user when the files cannot work on the device.
|
|
205
|
+
*
|
|
206
|
+
* @param upload what was uploaded
|
|
207
|
+
* @param defaultLanguage language to record when neither the upload nor its JSON names one
|
|
208
|
+
*/
|
|
209
|
+
function prepareWakeWord(upload, defaultLanguage = '') {
|
|
210
|
+
const model = upload.model;
|
|
211
|
+
if (!model?.length) {
|
|
212
|
+
throw new Error('no model file (.tflite) was uploaded');
|
|
213
|
+
}
|
|
214
|
+
if (model.length > exports.MAX_MODEL_BYTES) {
|
|
215
|
+
throw new Error(`the model is ${Math.round(model.length / 1024)} KB — microWakeWord models are well below ${exports.MAX_MODEL_BYTES / 1024} KB`);
|
|
216
|
+
}
|
|
217
|
+
const problem = microModelProblem(model);
|
|
218
|
+
if (problem) {
|
|
219
|
+
throw new Error(problem);
|
|
220
|
+
}
|
|
221
|
+
const source = parseConfig(upload.config);
|
|
222
|
+
const type = typeof source.type === 'string' ? source.type : '';
|
|
223
|
+
if (type && type !== 'micro') {
|
|
224
|
+
throw new Error(`this is a "${type}" model — the device only accepts microWakeWord models ("micro") from outside`);
|
|
225
|
+
}
|
|
226
|
+
const fileName = upload.fileName || (typeof source.model === 'string' ? source.model : '');
|
|
227
|
+
const phrase = (upload.phrase?.trim() ||
|
|
228
|
+
(typeof source.wake_word === 'string' ? source.wake_word.trim() : '') ||
|
|
229
|
+
phraseFromFileName(fileName)).slice(0, 64);
|
|
230
|
+
if (!phrase) {
|
|
231
|
+
throw new Error('the wake word needs a phrase (what you say to wake the device)');
|
|
232
|
+
}
|
|
233
|
+
let languages = languageList(upload.languages);
|
|
234
|
+
if (!languages.length) {
|
|
235
|
+
languages = languageList(source.trained_languages);
|
|
236
|
+
}
|
|
237
|
+
if (!languages.length && defaultLanguage) {
|
|
238
|
+
languages = [defaultLanguage.toLowerCase()];
|
|
239
|
+
}
|
|
240
|
+
const sha256 = (0, node_crypto_1.createHash)('sha256').update(model).digest('hex');
|
|
241
|
+
const id = `${slugify(fileName) || slugify(phrase) || 'wake_word'}_${sha256.slice(0, 8)}`;
|
|
242
|
+
const micro = source.micro && typeof source.micro === 'object' ? source.micro : {};
|
|
243
|
+
const config = {
|
|
244
|
+
...source,
|
|
245
|
+
type: 'micro',
|
|
246
|
+
wake_word: phrase,
|
|
247
|
+
model: `${id}.tflite`,
|
|
248
|
+
trained_languages: languages,
|
|
249
|
+
version: typeof source.version === 'number' ? source.version : 2,
|
|
250
|
+
micro: { ...DEFAULT_MICRO, ...micro },
|
|
251
|
+
};
|
|
252
|
+
return {
|
|
253
|
+
id,
|
|
254
|
+
phrase,
|
|
255
|
+
languages,
|
|
256
|
+
config: Buffer.from(`${JSON.stringify(config, null, 2)}\n`),
|
|
257
|
+
model,
|
|
258
|
+
size: model.length,
|
|
259
|
+
sha256,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Rebuild a stored wake word from its two files (adapter start). The JSON is the one we wrote, so it is
|
|
264
|
+
* trusted for the phrase; size and hash are recomputed from the model rather than stored.
|
|
265
|
+
*/
|
|
266
|
+
function loadWakeWord(id, config, model) {
|
|
267
|
+
if (!exports.WAKE_WORD_ID_RE.test(id)) {
|
|
268
|
+
throw new Error(`invalid wake word id "${id}"`);
|
|
269
|
+
}
|
|
270
|
+
if (!isTfliteModel(model)) {
|
|
271
|
+
throw new Error(`${id}.tflite is not a TensorFlow Lite model`);
|
|
272
|
+
}
|
|
273
|
+
const parsed = parseConfig(config.toString('utf8'));
|
|
274
|
+
return {
|
|
275
|
+
id,
|
|
276
|
+
phrase: typeof parsed.wake_word === 'string' ? parsed.wake_word : id,
|
|
277
|
+
languages: languageList(parsed.trained_languages),
|
|
278
|
+
config,
|
|
279
|
+
model,
|
|
280
|
+
size: model.length,
|
|
281
|
+
sha256: (0, node_crypto_1.createHash)('sha256').update(model).digest('hex'),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
//# sourceMappingURL=wakeWords.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wakeWords.js","sourceRoot":"","sources":["../../../src/lib/voice/wakeWords.ts"],"names":[],"mappings":";;;AAmEA,0BAYC;AAaD,sCAEC;AAwBD,oCA0EC;AAUD,8CAmBC;AAgCD,0CAgEC;AAMD,oCAiBC;AApVD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,6CAAyC;AAEzC,yEAAyE;AAC5D,QAAA,eAAe,GAAG,IAAI,GAAG,IAAI,CAAC;AAE3C,qEAAqE;AACxD,QAAA,eAAe,GAAG,mBAAmB,CAAC;AAEnD,wEAAwE;AAC3D,QAAA,aAAa,GAAG,WAAW,CAAC;AAEzC,yFAAyF;AAC5E,QAAA,oBAAoB,GAAG,YAAY,CAAC;AAEjD,oFAAoF;AACpF,MAAM,aAAa,GAAG,EAAE,kBAAkB,EAAE,IAAI,EAAE,mBAAmB,EAAE,CAAC,EAAE,CAAC;AA+B3E,oFAAoF;AACpF,SAAgB,OAAO,CAAC,KAAa;IACjC,OAAO,KAAK;SACP,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;SACvB,WAAW,EAAE;SACb,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC;SAC/B,SAAS,CAAC,MAAM,CAAC;SACjB,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC;SACnB,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;SACvB,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;SACZ,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAC5B,CAAC;AAED,qFAAqF;AACrF,SAAS,kBAAkB,CAAC,QAAgB;IACxC,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC;IAC/E,OAAO,IAAI;SACN,KAAK,CAAC,UAAU,CAAC;SACjB,MAAM,CAAC,OAAO,CAAC;SACf,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;SACzC,IAAI,CAAC,GAAG,CAAC,CAAC;AACnB,CAAC;AAED,iFAAiF;AACjF,SAAgB,aAAa,CAAC,KAAa;IACvC,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC;AACzE,CAAC;AAED,mGAAmG;AACnG,MAAM,cAAc,GAAG,EAAE,CAAC;AAE1B,0FAA0F;AAC1F,MAAM,uBAAuB,GAAG,EAAE,CAAC;AAEnC,qGAAqG;AACrG,MAAM,cAAc,GAAG,CAAC,CAAC;AAUzB;;;;GAIG;AACH,SAAgB,YAAY,CAAC,KAAa;IACtC,0CAA0C;IAC1C,MAAM,eAAe,GAAG,CAAC,CAAC;IAC1B,MAAM,gBAAgB,GAAG,CAAC,CAAC;IAC3B,MAAM,eAAe,GAAG,CAAC,CAAC;IAC1B,MAAM,gBAAgB,GAAG,CAAC,CAAC;IAC3B,MAAM,YAAY,GAAG,CAAC,CAAC;IACvB,MAAM,WAAW,GAAG,CAAC,CAAC;IACtB,MAAM,mBAAmB,GAAG,CAAC,CAAC;IAC9B,MAAM,kBAAkB,GAAG,CAAC,CAAC;IAE7B,8EAA8E;IAC9E,MAAM,KAAK,GAAG,CAAC,KAAa,EAAE,KAAa,EAAU,EAAE;QACnD,MAAM,MAAM,GAAG,KAAK,GAAG,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAChD,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QAC7B,IAAI,MAAM,IAAI,KAAK,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC;YACvC,OAAO,CAAC,CAAC;QACb,CAAC;QACD,MAAM,EAAE,GAAG,KAAK,CAAC,YAAY,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;QAC/C,OAAO,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/B,CAAC,CAAC;IACF,6FAA6F;IAC7F,MAAM,GAAG,GAAG,CAAC,KAAa,EAAE,KAAa,EAAU,EAAE;QACjD,MAAM,EAAE,GAAG,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QAC/B,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAChD,CAAC,CAAC;IACF,MAAM,MAAM,GAAG,CAAC,MAAc,EAAU,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACrF,yCAAyC;IACzC,MAAM,OAAO,GAAG,CAAC,MAAc,EAAE,CAAS,EAAU,EAAE;QAClD,MAAM,EAAE,GAAG,MAAM,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC9B,OAAO,EAAE,GAAG,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;IACvC,CAAC,CAAC;IACF,MAAM,KAAK,GAAG,CAAC,MAAc,EAAE,CAAS,EAAU,EAAE,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IAE3F,MAAM,MAAM,GAAG,CAAC,QAAgB,EAAE,IAAY,EAAuB,EAAE;QACnE,MAAM,OAAO,GAAG,GAAG,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC;QAChD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;YAChB,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QAC7B,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,IAAI,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;YACxC,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,MAAM,CAAC,GAAG,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAClC,MAAM,IAAI,GAAG,GAAG,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC;QAClC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,cAAc,EAAE,CAAC;YAChC,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QAC7E,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;QACrC,MAAM,YAAY,GAAG,GAAG,CAAC,CAAC,EAAE,mBAAmB,CAAC,CAAC;QACjD,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,YAAY,EAAE,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACxE,OAAO;YACH,KAAK;YACL,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;YAC1C,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC;SACvE,CAAC;IACN,CAAC,CAAC;IAEF,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,IAAI,CAAC;IAChB,CAAC;IACD,IAAI,CAAC;QACD,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,eAAe,CAAC,CAAC;QAC9D,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;YACrB,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,MAAM,IAAI,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;QACnC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC;QACvD,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC,CAAC;QACzD,OAAO,KAAK,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACtD,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,IAAI,CAAC,CAAC,qCAAqC;IACtD,CAAC;AACL,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,iBAAiB,CAAC,KAAa;IAC3C,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,yDAAyD,CAAC;IACrE,CAAC;IACD,MAAM,EAAE,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IAC/B,IAAI,CAAC,EAAE,EAAE,CAAC;QACN,OAAO,6EAA6E,CAAC;IACzF,CAAC;IACD,MAAM,KAAK,GAAG,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC;IAC7B,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,uBAAuB,EAAE,CAAC;QACtD,OAAO,kIAAkI,CAAC;IAC9I,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,cAAc,EAAE,CAAC;QACpD,OAAO,6CAA6C,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,yCAAyC,cAAc,IAAI,CAAC;IACpI,CAAC;IACD,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,SAAS,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;QAC9C,OAAO,kFAAkF,CAAC;IAC9F,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC;AAED,SAAS,WAAW,CAAC,MAAgC;IACjD,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,EAAE,EAAE,CAAC;QAC3D,OAAO,EAAE,CAAC;IACd,CAAC;IACD,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,MAAM,CAAC;IAClB,CAAC;IACD,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACD,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAChC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CAAC,oCAAqC,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;IAChF,CAAC;IACD,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACjE,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,MAAiC,CAAC;AAC7C,CAAC;AAED,SAAS,YAAY,CAAC,KAAc;IAChC,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACpH,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,eAAe,CAAC,MAAsB,EAAE,eAAe,GAAG,EAAE;IACxE,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;IAC3B,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC5D,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,GAAG,uBAAe,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CACX,gBAAgB,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,6CAA6C,uBAAe,GAAG,IAAI,KAAK,CAC1H,CAAC;IACN,CAAC;IACD,MAAM,OAAO,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;IACzC,IAAI,OAAO,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;IAED,MAAM,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC1C,MAAM,IAAI,GAAG,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IAChE,IAAI,IAAI,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CACX,cAAc,IAAI,+EAA+E,CACpG,CAAC;IACN,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,IAAI,CAAC,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAC3F,MAAM,MAAM,GAAG,CACX,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE;QACrB,CAAC,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACrE,kBAAkB,CAAC,QAAQ,CAAC,CAC/B,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACf,IAAI,CAAC,MAAM,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CAAC,gEAAgE,CAAC,CAAC;IACtF,CAAC;IAED,IAAI,SAAS,GAAG,YAAY,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAC/C,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC;QACpB,SAAS,GAAG,YAAY,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC;IACvD,CAAC;IACD,IAAI,CAAC,SAAS,CAAC,MAAM,IAAI,eAAe,EAAE,CAAC;QACvC,SAAS,GAAG,CAAC,eAAe,CAAC,WAAW,EAAE,CAAC,CAAC;IAChD,CAAC;IAED,MAAM,MAAM,GAAG,IAAA,wBAAU,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAChE,MAAM,EAAE,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,IAAI,WAAW,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;IAE1F,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAE,MAAM,CAAC,KAAiC,CAAC,CAAC,CAAC,EAAE,CAAC;IAChH,MAAM,MAAM,GAAG;QACX,GAAG,MAAM;QACT,IAAI,EAAE,OAAO;QACb,SAAS,EAAE,MAAM;QACjB,KAAK,EAAE,GAAG,EAAE,SAAS;QACrB,iBAAiB,EAAE,SAAS;QAC5B,OAAO,EAAE,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAChE,KAAK,EAAE,EAAE,GAAG,aAAa,EAAE,GAAG,KAAK,EAAE;KACxC,CAAC;IAEF,OAAO;QACH,EAAE;QACF,MAAM;QACN,SAAS;QACT,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;QAC3D,KAAK;QACL,IAAI,EAAE,KAAK,CAAC,MAAM;QAClB,MAAM;KACT,CAAC;AACN,CAAC;AAED;;;GAGG;AACH,SAAgB,YAAY,CAAC,EAAU,EAAE,MAAc,EAAE,KAAa;IAClE,IAAI,CAAC,uBAAe,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,yBAAyB,EAAE,GAAG,CAAC,CAAC;IACpD,CAAC;IACD,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,GAAG,EAAE,wCAAwC,CAAC,CAAC;IACnE,CAAC;IACD,MAAM,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IACpD,OAAO;QACH,EAAE;QACF,MAAM,EAAE,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;QACpE,SAAS,EAAE,YAAY,CAAC,MAAM,CAAC,iBAAiB,CAAC;QACjD,MAAM;QACN,KAAK;QACL,IAAI,EAAE,KAAK,CAAC,MAAM;QAClB,MAAM,EAAE,IAAA,wBAAU,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;KAC3D,CAAC;AACN,CAAC","sourcesContent":["/**\n * Custom wake words for ESPHome satellites — models the user uploads, offered to the devices as\n * \"external wake words\".\n *\n * The ESPHome API has no upload message. Instead the client lists models in every\n * `VoiceAssistantConfigurationRequest` (`external_wake_words`: id, phrase, languages, type, size,\n * SHA-256 and the URL of a JSON config). The device adds them to its available list, and only when one\n * is selected does it fetch the config from `url` and the model from the **same directory** as\n * `<id>.tflite` — query and fragment are dropped — storing both as `<id>.json` / `<id>.tflite` next to\n * its built-in models. Verified in the ThirdReality firmware (`linux-voice-assistant-cpp`,\n * `Satellite.cpp` `ResolveWakeWordConfig`, `audio/ExternalWakeWord.cpp`). Consequences baked in here:\n *\n * - **Only microWakeWord models** (`model_type: \"micro\"`): the firmware skips any other type.\n * - The device loads the model named by the JSON's `model` field, relative to the JSON — so the JSON we\n * serve always says `<id>.tflite`, whatever the uploaded file was called.\n * - Once downloaded, a model is a *local* one on the device and wins over the external offer by id. A\n * changed model under the same id would never reach a device that already has it, so the id carries\n * a short hash of the model: a new upload is a new wake word.\n * - Downloaded models stay on the device; the API has no way to remove them.\n */\nimport { createHash } from 'node:crypto';\n\n/** microWakeWord models are 50–200 KB; anything near this is not one. */\nexport const MAX_MODEL_BYTES = 1024 * 1024;\n\n/** Ids are file names on the device and path segments in our URL. */\nexport const WAKE_WORD_ID_RE = /^[a-z0-9_]{1,64}$/;\n\n/** Folder in the instance's file storage (`assistant.0/wakewords/`). */\nexport const WAKE_WORD_DIR = 'wakewords';\n\n/** Where the media server serves them: `<prefix><id>.json` and `<prefix><id>.tflite`. */\nexport const WAKE_WORD_URL_PREFIX = '/wakeword/';\n\n/** Defaults of the official v2 models, used when a model comes without its JSON. */\nconst DEFAULT_MICRO = { probability_cutoff: 0.97, sliding_window_size: 5 };\n\n/** One uploaded wake word, ready to be offered and served. */\nexport interface CustomWakeWord {\n id: string;\n /** What the user says, e.g. \"Hey Hannah\". */\n phrase: string;\n /** ISO-639-1 codes the model was trained on. */\n languages: string[];\n /** The JSON config exactly as the device will store it. */\n config: Buffer;\n /** The `.tflite` model. */\n model: Buffer;\n size: number;\n /** SHA-256 of the model, lower-case hex — the device compares it with its cached copy. */\n sha256: string;\n}\n\nexport interface WakeWordUpload {\n /** The `.tflite` file. */\n model: Buffer;\n /** Name of the uploaded model file — the readable half of the id. */\n fileName?: string;\n /** The `.json` that came with the model, if any. */\n config?: string | Record<string, unknown>;\n /** Overrides the phrase from the JSON. */\n phrase?: string;\n /** Overrides the languages from the JSON. */\n languages?: string[];\n}\n\n/** `hey_hannah.tflite` → `hey_hannah`; anything that is not a-z/0-9 becomes `_`. */\nexport function slugify(value: string): string {\n return value\n .replace(/^.*[\\\\/]/, '')\n .toLowerCase()\n .replace(/\\.(tflite|json)$/, '')\n .normalize('NFKD')\n .replace(/[̀-ͯ]/g, '')\n .replace(/ß/g, 'ss')\n .replace(/[^a-z0-9]+/g, '_')\n .replace(/^_+|_+$/g, '')\n .slice(0, 40)\n .replace(/_+$/, '');\n}\n\n/** `hey_hannah` → `Hey Hannah` — a phrase for a model that came without its JSON. */\nfunction phraseFromFileName(fileName: string): string {\n const base = fileName.replace(/^.*[\\\\/]/, '').replace(/\\.(tflite|json)$/i, '');\n return base\n .split(/[_\\-\\s]+/)\n .filter(Boolean)\n .map(w => w[0].toUpperCase() + w.slice(1))\n .join(' ');\n}\n\n/** A TensorFlow Lite flatbuffer carries the file identifier `TFL3` at byte 4. */\nexport function isTfliteModel(model: Buffer): boolean {\n return model.length > 8 && model.toString('latin1', 4, 8) === 'TFL3';\n}\n\n/** microWakeWord feeds 40 mel features per frame (`MicroFeatures::kFeatureSize` on the device). */\nconst MICRO_FEATURES = 40;\n\n/** openWakeWord classifiers take 96-value speech embeddings, e.g. input `[1, 16, 96]`. */\nconst OPEN_WAKE_WORD_FEATURES = 96;\n\n/** Shapes longer than this are not a wake word model; it also bounds the loop over a broken file. */\nconst MAX_DIMENSIONS = 8;\n\nexport interface TfliteTensor {\n shape: number[];\n /** TFLite `TensorType`: 0 float32, 3 uint8, 9 int8, … */\n type: number;\n /** Has a non-zero quantization scale. */\n quantized: boolean;\n}\n\n/**\n * Read the first input and output tensor of the main graph — the two the device checks before it runs a\n * model. Walks the flatbuffer by hand (schema: `tensorflow/compiler/mlir/lite/schema/schema.fbs`), which\n * spares a TensorFlow dependency for four fields. `null` when the file is not a readable model.\n */\nexport function readTfliteIo(model: Buffer): { input: TfliteTensor; output: TfliteTensor } | null {\n // Field positions in the schema's tables.\n const MODEL_SUBGRAPHS = 2;\n const SUBGRAPH_TENSORS = 0;\n const SUBGRAPH_INPUTS = 1;\n const SUBGRAPH_OUTPUTS = 2;\n const TENSOR_SHAPE = 0;\n const TENSOR_TYPE = 1;\n const TENSOR_QUANTIZATION = 4;\n const QUANTIZATION_SCALE = 2;\n\n /** Position of field `index` of the table at `table`, 0 when it is absent. */\n const field = (table: number, index: number): number => {\n const vtable = table - model.readInt32LE(table);\n const offset = 4 + 2 * index;\n if (offset >= model.readUInt16LE(vtable)) {\n return 0;\n }\n const at = model.readUInt16LE(vtable + offset);\n return at ? table + at : 0;\n };\n /** Follow the offset stored in field `index` to the table, vector or string it points at. */\n const ref = (table: number, index: number): number => {\n const at = field(table, index);\n return at ? at + model.readUInt32LE(at) : 0;\n };\n const length = (vector: number): number => (vector ? model.readUInt32LE(vector) : 0);\n /** Element `k` of a vector of tables. */\n const tableAt = (vector: number, k: number): number => {\n const at = vector + 4 + 4 * k;\n return at + model.readUInt32LE(at);\n };\n const intAt = (vector: number, k: number): number => model.readInt32LE(vector + 4 + 4 * k);\n\n const tensor = (subgraph: number, list: number): TfliteTensor | null => {\n const tensors = ref(subgraph, SUBGRAPH_TENSORS);\n if (!length(list)) {\n return null;\n }\n const index = intAt(list, 0);\n if (index < 0 || index >= length(tensors)) {\n return null;\n }\n const t = tableAt(tensors, index);\n const dims = ref(t, TENSOR_SHAPE);\n if (length(dims) > MAX_DIMENSIONS) {\n return null;\n }\n const shape = Array.from({ length: length(dims) }, (_, k) => intAt(dims, k));\n const typeAt = field(t, TENSOR_TYPE);\n const quantization = ref(t, TENSOR_QUANTIZATION);\n const scales = quantization ? ref(quantization, QUANTIZATION_SCALE) : 0;\n return {\n shape,\n type: typeAt ? model.readUInt8(typeAt) : 0,\n quantized: length(scales) > 0 && model.readFloatLE(scales + 4) !== 0,\n };\n };\n\n if (!isTfliteModel(model)) {\n return null;\n }\n try {\n const subgraphs = ref(model.readUInt32LE(0), MODEL_SUBGRAPHS);\n if (!length(subgraphs)) {\n return null;\n }\n const main = tableAt(subgraphs, 0);\n const input = tensor(main, ref(main, SUBGRAPH_INPUTS));\n const output = tensor(main, ref(main, SUBGRAPH_OUTPUTS));\n return input && output ? { input, output } : null;\n } catch {\n return null; // an offset pointed outside the file\n }\n}\n\n/**\n * Why the device cannot run this model, or `null` when it can. Mirrors the checks of the firmware's\n * `MicroWakeWord::FromConfig`: input `[1, stride, 40]`, quantized input and output. An openWakeWord\n * model is named as such — it is the most likely mix-up, both come as `.tflite`, and the device runs\n * openWakeWord only when its firmware was started in that mode (then without the built-in models).\n *\n * @param model the `.tflite` file\n */\nexport function microModelProblem(model: Buffer): string | null {\n if (!isTfliteModel(model)) {\n return 'the model file is not a TensorFlow Lite model (.tflite)';\n }\n const io = readTfliteIo(model);\n if (!io) {\n return 'the model file is damaged — it could not be read as a TensorFlow Lite model';\n }\n const shape = io.input.shape;\n if (shape[shape.length - 1] === OPEN_WAKE_WORD_FEATURES) {\n return 'this is an openWakeWord model — the device runs only microWakeWord models, so the wake word has to be trained with microWakeWord';\n }\n if (shape.length !== 3 || shape[2] !== MICRO_FEATURES) {\n return `this is not a microWakeWord model (input [${shape.join(', ')}], a microWakeWord model takes [1, n, ${MICRO_FEATURES}])`;\n }\n if (!io.input.quantized || !io.output.quantized) {\n return 'the model is not quantized — the device runs only quantized microWakeWord models';\n }\n return null;\n}\n\nfunction parseConfig(config: WakeWordUpload['config']): Record<string, unknown> {\n if (config === undefined || config === null || config === '') {\n return {};\n }\n if (typeof config === 'object') {\n return config;\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(config);\n } catch (e) {\n throw new Error(`the JSON file is not valid JSON: ${(e as Error).message}`);\n }\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error('the JSON file must contain an object');\n }\n return parsed as Record<string, unknown>;\n}\n\nfunction languageList(value: unknown): string[] {\n return Array.isArray(value) ? [...new Set(value.map(v => String(v).trim().toLowerCase()).filter(Boolean))] : [];\n}\n\n/**\n * Validate an upload and build the wake word the devices will download. Throws with a message meant for\n * the user when the files cannot work on the device.\n *\n * @param upload what was uploaded\n * @param defaultLanguage language to record when neither the upload nor its JSON names one\n */\nexport function prepareWakeWord(upload: WakeWordUpload, defaultLanguage = ''): CustomWakeWord {\n const model = upload.model;\n if (!model?.length) {\n throw new Error('no model file (.tflite) was uploaded');\n }\n if (model.length > MAX_MODEL_BYTES) {\n throw new Error(\n `the model is ${Math.round(model.length / 1024)} KB — microWakeWord models are well below ${MAX_MODEL_BYTES / 1024} KB`,\n );\n }\n const problem = microModelProblem(model);\n if (problem) {\n throw new Error(problem);\n }\n\n const source = parseConfig(upload.config);\n const type = typeof source.type === 'string' ? source.type : '';\n if (type && type !== 'micro') {\n throw new Error(\n `this is a \"${type}\" model — the device only accepts microWakeWord models (\"micro\") from outside`,\n );\n }\n\n const fileName = upload.fileName || (typeof source.model === 'string' ? source.model : '');\n const phrase = (\n upload.phrase?.trim() ||\n (typeof source.wake_word === 'string' ? source.wake_word.trim() : '') ||\n phraseFromFileName(fileName)\n ).slice(0, 64);\n if (!phrase) {\n throw new Error('the wake word needs a phrase (what you say to wake the device)');\n }\n\n let languages = languageList(upload.languages);\n if (!languages.length) {\n languages = languageList(source.trained_languages);\n }\n if (!languages.length && defaultLanguage) {\n languages = [defaultLanguage.toLowerCase()];\n }\n\n const sha256 = createHash('sha256').update(model).digest('hex');\n const id = `${slugify(fileName) || slugify(phrase) || 'wake_word'}_${sha256.slice(0, 8)}`;\n\n const micro = source.micro && typeof source.micro === 'object' ? (source.micro as Record<string, unknown>) : {};\n const config = {\n ...source,\n type: 'micro',\n wake_word: phrase,\n model: `${id}.tflite`,\n trained_languages: languages,\n version: typeof source.version === 'number' ? source.version : 2,\n micro: { ...DEFAULT_MICRO, ...micro },\n };\n\n return {\n id,\n phrase,\n languages,\n config: Buffer.from(`${JSON.stringify(config, null, 2)}\\n`),\n model,\n size: model.length,\n sha256,\n };\n}\n\n/**\n * Rebuild a stored wake word from its two files (adapter start). The JSON is the one we wrote, so it is\n * trusted for the phrase; size and hash are recomputed from the model rather than stored.\n */\nexport function loadWakeWord(id: string, config: Buffer, model: Buffer): CustomWakeWord {\n if (!WAKE_WORD_ID_RE.test(id)) {\n throw new Error(`invalid wake word id \"${id}\"`);\n }\n if (!isTfliteModel(model)) {\n throw new Error(`${id}.tflite is not a TensorFlow Lite model`);\n }\n const parsed = parseConfig(config.toString('utf8'));\n return {\n id,\n phrase: typeof parsed.wake_word === 'string' ? parsed.wake_word : id,\n languages: languageList(parsed.trained_languages),\n config,\n model,\n size: model.length,\n sha256: createHash('sha256').update(model).digest('hex'),\n };\n}\n"]}
|
|
@@ -225,10 +225,13 @@ class WyomingConnection {
|
|
|
225
225
|
if (!text) {
|
|
226
226
|
return;
|
|
227
227
|
}
|
|
228
|
-
const
|
|
228
|
+
const audioMs = Math.round((pcm.length / 2 / this.audioRate) * 1000);
|
|
229
|
+
const answer = (await this.opts.answer(text, sttMs, audioMs)).trim();
|
|
229
230
|
this.opts.log.info(`Wyoming A: ${answer}`);
|
|
230
|
-
|
|
231
|
-
|
|
231
|
+
const spoken = answer ? await this.speak(answer) : null;
|
|
232
|
+
if (spoken) {
|
|
233
|
+
// Only a reply counts — a bare `synthesize` from the client is no request of ours.
|
|
234
|
+
this.opts.onSpoken?.(spoken);
|
|
232
235
|
}
|
|
233
236
|
}
|
|
234
237
|
catch (e) {
|
|
@@ -236,22 +239,27 @@ class WyomingConnection {
|
|
|
236
239
|
this.write('error', { text: e.message });
|
|
237
240
|
}
|
|
238
241
|
}
|
|
242
|
+
/** Synthesise and stream `text`; returns how long synthesis took and how long the audio plays. */
|
|
239
243
|
async speak(text) {
|
|
240
244
|
if (!text) {
|
|
241
|
-
return;
|
|
245
|
+
return null;
|
|
242
246
|
}
|
|
243
247
|
try {
|
|
248
|
+
const ttsStart = Date.now();
|
|
244
249
|
const { pcm, sampleRate } = await this.opts.tts.synthesize(text, this.lang);
|
|
250
|
+
const spoken = { ms: Date.now() - ttsStart, audioMs: (pcm.length / 2 / sampleRate) * 1000 };
|
|
245
251
|
const meta = { rate: sampleRate, width: 2, channels: 1 };
|
|
246
252
|
this.write('audio-start', meta);
|
|
247
253
|
for (let off = 0; off < pcm.length; off += AUDIO_CHUNK) {
|
|
248
254
|
this.write('audio-chunk', meta, pcm.subarray(off, off + AUDIO_CHUNK));
|
|
249
255
|
}
|
|
250
256
|
this.write('audio-stop', {});
|
|
257
|
+
return spoken;
|
|
251
258
|
}
|
|
252
259
|
catch (e) {
|
|
253
260
|
this.opts.log.error(`Wyoming TTS failed: ${e.message}`);
|
|
254
261
|
this.write('error', { text: e.message });
|
|
262
|
+
return null;
|
|
255
263
|
}
|
|
256
264
|
}
|
|
257
265
|
/** Capabilities advertised in response to `describe`. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wyoming.js","sourceRoot":"","sources":["../../../src/lib/voice/wyoming.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;;;;;;;;;;;GAWG;AACH,8CAAgC;AAGhC,6CAAqC;AAsBrC,6CAA6C;AAC7C,MAAM,WAAW,GAAG,IAAI,CAAC;AAEzB,MAAa,aAAa;IAIO;IAHrB,MAAM,GAAsB,IAAI,CAAC;IACxB,KAAK,GAAG,IAAI,GAAG,EAAc,CAAC;IAE/C,YAA6B,IAA0B;QAA1B,SAAI,GAAJ,IAAI,CAAsB;IAAG,CAAC;IAE3D,KAAK;QACD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACnC,MAAM,MAAM,GAAG,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;YACjE,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;YACrB,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAC7B,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YACnF,MAAM,IAAI,GACN,IAAI,CAAC,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC;YACrG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE;gBACrC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,mCAAmC,IAAI,IAAI,SAAS,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;gBAC7F,OAAO,EAAE,CAAC;YACd,CAAC,CAAC,CAAC;QACP,CAAC,CAAC,CAAC;IACP,CAAC;IAED,KAAK,CAAC,IAAI;QACN,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACzB,CAAC,CAAC,OAAO,EAAE,CAAC;QAChB,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACnB,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;IAEO,YAAY,CAAC,IAAgB;QACjC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACrB,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;QAChD,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;QAChD,IAAI,iBAAiB,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3C,CAAC;CACJ;AAvCD,sCAuCC;AAED,4DAA4D;AAC5D,MAAM,iBAAiB;IASE;IACA;IATb,GAAG,GAAG,oBAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACtB,QAAQ,GAAG,KAAK,CAAC;IACjB,WAAW,GAAa,EAAE,CAAC;IAC3B,SAAS,GAAG,KAAK,CAAC;IAC1B,0FAA0F;IAClF,IAAI,CAAS;IAErB,YACqB,IAAgB,EAChB,IAA0B;QAD1B,SAAI,GAAJ,IAAI,CAAY;QAChB,SAAI,GAAJ,IAAI,CAAsB;QAE3C,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC1B,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE;YAChB,IAAI,CAAC,GAAG,GAAG,oBAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;YACxC,KAAK,IAAI,CAAC,KAAK,EAAE,CAAC;QACtB,CAAC,CAAC,CAAC;IACP,CAAC;IAED,uFAAuF;IAC/E,KAAK,CAAC,KAAK;QACf,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAChB,OAAO;QACX,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,IAAI,CAAC;YACD,SAAS,CAAC;gBACN,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO;gBAC1C,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;oBACT,MAAM;gBACV,CAAC;gBACD,IAAI,MAAkF,CAAC;gBACvF,IAAI,CAAC;oBACD,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;gBACnE,CAAC;gBAAC,MAAM,CAAC;oBACL,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,kBAAkB;oBACxD,SAAS;gBACb,CAAC;gBACD,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,cAAc,IAAI,CAAC,CAAC,CAAC;gBACtD,MAAM,KAAK,GAAG,EAAE,GAAG,CAAC,GAAG,UAAU,CAAC;gBAClC,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,KAAK,EAAE,CAAC;oBAC1B,MAAM,CAAC,mCAAmC;gBAC9C,CAAC;gBACD,MAAM,OAAO,GAAG,UAAU,CAAC,CAAC,CAAC,oBAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;gBACvF,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;gBACpC,MAAM,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;YACvF,CAAC;QACL,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,6BAA8B,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;QAC5E,CAAC;gBAAS,CAAC;YACP,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QAC1B,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,IAAY,EAAE,IAA8B,EAAE,OAAgB;QACxE,MAAM,MAAM,GAA4B,EAAE,IAAI,EAAE,CAAC;QACjD,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACrB,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC;QACvB,CAAC;QACD,IAAI,OAAO,EAAE,CAAC;YACV,MAAM,CAAC,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC;QAC3C,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC/C,IAAI,OAAO,EAAE,CAAC;YACV,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC7B,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,MAAM,CAAC,EAAW;QAC5B,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;YACd,KAAK,UAAU;gBACX,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;gBAChC,MAAM;YACV,kFAAkF;YAClF,KAAK,YAAY,CAAC;YAClB,KAAK,eAAe,CAAC;YACrB,KAAK,aAAa;gBACd,IAAI,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC;oBACpB,IAAI,CAAC,IAAI;wBACL,OAAO,EAAE,CAAC,IAAI,CAAC,QAAQ,KAAK,QAAQ;4BAChC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;4BAClC,CAAC,CAAE,EAAE,CAAC,IAAI,CAAC,QAAmB,CAAC,QAAQ,EAAE,CAAC;gBACtD,CAAC;gBACD,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;oBAChB,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC;gBAC5D,CAAC;gBACD,IAAI,CAAC,WAAW,GAAG,EAAE,CAAC;gBACtB,MAAM;YACV,KAAK,aAAa;gBACd,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC;oBACb,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;wBAC5C,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC;oBAC5D,CAAC;oBACD,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC;gBACtC,CAAC;gBACD,MAAM;YACV,yEAAyE;YACzE,KAAK,YAAY,CAAC;YAClB,KAAK,eAAe;gBAChB,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;gBACzB,MAAM;YACV,KAAK,YAAY;gBACb,IAAI,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC;oBACpB,IAAI,CAAC,IAAI;wBACL,OAAO,EAAE,CAAC,IAAI,CAAC,QAAQ,KAAK,QAAQ;4BAChC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;4BAClC,CAAC,CAAE,EAAE,CAAC,IAAI,CAAC,QAAmB,CAAC,QAAQ,EAAE,CAAC;gBACtD,CAAC;gBACD,MAAM,IAAI,CAAC,KAAK,CACZ,EAAE,CAAC,IAAI,EAAE,IAAI;oBACT,CAAC,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,QAAQ;wBAC/B,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;wBAC9B,CAAC,CAAE,EAAE,CAAC,IAAI,CAAC,IAAe,CAAC,QAAQ,EAAE;oBACzC,CAAC,CAAC,EAAE,CACX,CAAC;gBACF,MAAM;YACV,KAAK,MAAM;gBACP,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;gBAC5B,MAAM;QACd,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,WAAW;QACrB,MAAM,GAAG,GAAG,oBAAM,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAC5C,IAAI,CAAC,WAAW,GAAG,EAAE,CAAC;QACtB,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;YACd,OAAO;QACX,CAAC;QACD,IAAI,CAAC;YACD,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;YAC1E,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAC5B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YACnF,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,CAAC;YACpC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,cAAc,IAAI,IAAI,SAAS,EAAE,CAAC,CAAC;YACtD,IAAI,CAAC,KAAK,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;YACnC,IAAI,CAAC,IAAI,EAAE,CAAC;gBACR,OAAO;YACX,CAAC;YACD,MAAM,MAAM,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YAC5D,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,cAAc,MAAM,EAAE,CAAC,CAAC;YAC3C,IAAI,MAAM,EAAE,CAAC;gBACT,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC7B,CAAC;QACL,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,4BAA6B,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;YACxE,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,IAAI,EAAG,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;QACxD,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,KAAK,CAAC,IAAY;QAC5B,IAAI,CAAC,IAAI,EAAE,CAAC;YACR,OAAO;QACX,CAAC;QACD,IAAI,CAAC;YACD,MAAM,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;YAC5E,MAAM,IAAI,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;YACzD,IAAI,CAAC,KAAK,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;YAChC,KAAK,IAAI,GAAG,GAAG,CAAC,EAAE,GAAG,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,IAAI,WAAW,EAAE,CAAC;gBACrD,IAAI,CAAC,KAAK,CAAC,aAAa,EAAE,IAAI,EAAE,GAAG,CAAC,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,WAAW,CAAC,CAAC,CAAC;YAC1E,CAAC;YACD,IAAI,CAAC,KAAK,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;QACjC,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,uBAAwB,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;YACnE,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,IAAI,EAAG,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;QACxD,CAAC;IACL,CAAC;IAED,yDAAyD;IACjD,IAAI;QACR,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACxD,MAAM,WAAW,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,gDAAgD,EAAE,CAAC;QAChG,OAAO;YACH,GAAG,EAAE;gBACD;oBACI,IAAI,EAAE,oBAAoB;oBAC1B,WAAW;oBACX,SAAS,EAAE,IAAI;oBACf,WAAW,EAAE,mCAAmC;oBAChD,OAAO,EAAE,KAAK;oBACd,MAAM,EAAE;wBACJ;4BACI,IAAI,EAAE,SAAS;4BACf,WAAW;4BACX,SAAS,EAAE,IAAI;4BACf,WAAW,EAAE,EAAE;4BACf,OAAO,EAAE,KAAK;4BACd,SAAS,EAAE,CAAC,IAAI,CAAC;yBACpB;qBACJ;iBACJ;aACJ;YACD,GAAG,EAAE;gBACD;oBACI,IAAI,EAAE,oBAAoB;oBAC1B,WAAW;oBACX,SAAS,EAAE,IAAI;oBACf,WAAW,EAAE,mCAAmC;oBAChD,OAAO,EAAE,KAAK;oBACd,MAAM,EAAE;wBACJ;4BACI,IAAI,EAAE,SAAS;4BACf,WAAW;4BACX,SAAS,EAAE,IAAI;4BACf,WAAW,EAAE,EAAE;4BACf,OAAO,EAAE,KAAK;4BACd,SAAS,EAAE,CAAC,IAAI,CAAC;yBACpB;qBACJ;iBACJ;aACJ;YACD,MAAM,EAAE;gBACJ;oBACI,IAAI,EAAE,oBAAoB;oBAC1B,WAAW;oBACX,SAAS,EAAE,IAAI;oBACf,WAAW,EAAE,oCAAoC;oBACjD,OAAO,EAAE,KAAK;oBACd,MAAM,EAAE;wBACJ;4BACI,IAAI,EAAE,SAAS;4BACf,WAAW;4BACX,SAAS,EAAE,IAAI;4BACf,WAAW,EAAE,EAAE;4BACf,OAAO,EAAE,KAAK;4BACd,SAAS,EAAE,CAAC,IAAI,CAAC;yBACpB;qBACJ;iBACJ;aACJ;SACJ,CAAC;IACN,CAAC;CACJ","sourcesContent":["/**\n * Wyoming protocol endpoint (https://github.com/rhasspy/wyoming) — a TCP server that bridges Wyoming\n * events to the existing STT → answer → TTS pipeline, so a `wyoming-satellite`, Home Assistant Voice PE\n * or ESPHome voice device can stream to this adapter directly.\n *\n * Wire format: one JSON header line terminated by '\\n', optionally followed by `payload_length` raw\n * bytes (audio). We handle: describe → info; audio-start/chunk/stop → STT → answer → TTS (streamed back\n * as audio-*); synthesize → TTS.\n *\n * ⚠️ Framing is unit-tested (loopback); interop with real HA Voice PE / wyoming-satellite still needs\n * on-device validation.\n */\nimport * as net from 'node:net';\nimport type { SttEngine } from './stt';\nimport type { TtsEngine } from './tts';\nimport { Buffer } from 'node:buffer';\n\nexport interface WyomingServerOptions {\n port: number;\n bindAddress?: string;\n /** ISO-639-1 language hint for STT/TTS. */\n language: string;\n stt: SttEngine;\n tts: TtsEngine;\n /** `sttMs`: how long recognition took (for the journal). */\n answer: (question: string, sttMs: number) => 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}\n\ninterface WyEvent {\n type: string;\n data?: Record<string, unknown>;\n payload?: Buffer;\n}\n\n/** Bytes of PCM per outgoing audio-chunk. */\nconst AUDIO_CHUNK = 8192;\n\nexport class WyomingServer {\n private server: net.Server | null = null;\n private readonly conns = new Set<net.Socket>();\n\n constructor(private readonly opts: WyomingServerOptions) {}\n\n start(): Promise<void> {\n return new Promise((resolve, reject) => {\n const server = net.createServer(sock => this.onConnection(sock));\n this.server = server;\n server.once('error', reject);\n server.on('error', e => this.opts.log.error(`Wyoming server error: ${e.message}`));\n const addr =\n this.opts.bindAddress && this.opts.bindAddress !== '0.0.0.0' ? this.opts.bindAddress : undefined;\n server.listen(this.opts.port, addr, () => {\n this.opts.log.info(`Wyoming server listening on TCP ${addr || '0.0.0.0'}:${this.opts.port}`);\n resolve();\n });\n });\n }\n\n async stop(): Promise<void> {\n for (const s of this.conns) {\n s.destroy();\n }\n this.conns.clear();\n const server = this.server;\n this.server = null;\n if (server) {\n await new Promise<void>(res => server.close(() => res()));\n }\n }\n\n private onConnection(sock: net.Socket): void {\n this.conns.add(sock);\n sock.on('close', () => this.conns.delete(sock));\n sock.on('error', () => this.conns.delete(sock));\n new WyomingConnection(sock, this.opts);\n }\n}\n\n/** One client connection: frame parser + event handling. */\nclass WyomingConnection {\n private buf = Buffer.alloc(0);\n private draining = false;\n private audioChunks: Buffer[] = [];\n private audioRate = 16000;\n /** Per-utterance language (overridable via `transcribe`/`synthesize` `data.language`). */\n private lang: string;\n\n constructor(\n private readonly sock: net.Socket,\n private readonly opts: WyomingServerOptions,\n ) {\n this.lang = opts.language;\n sock.on('data', d => {\n this.buf = Buffer.concat([this.buf, d]);\n void this.drain();\n });\n }\n\n /** Pull complete events (header line + optional payload) out of the receive buffer. */\n private async drain(): Promise<void> {\n if (this.draining) {\n return;\n }\n this.draining = true;\n try {\n for (;;) {\n const nl = this.buf.indexOf(0x0a); // '\\n'\n if (nl < 0) {\n break;\n }\n let header: { type?: string; data?: Record<string, unknown>; payload_length?: number };\n try {\n header = JSON.parse(this.buf.subarray(0, nl).toString('utf8'));\n } catch {\n this.buf = this.buf.subarray(nl + 1); // skip a bad line\n continue;\n }\n const payloadLen = Number(header.payload_length || 0);\n const total = nl + 1 + payloadLen;\n if (this.buf.length < total) {\n break; // wait for the rest of the payload\n }\n const payload = payloadLen ? Buffer.from(this.buf.subarray(nl + 1, total)) : undefined;\n this.buf = this.buf.subarray(total);\n await this.handle({ type: String(header.type || ''), data: header.data, payload });\n }\n } catch (e) {\n this.opts.log.warn(`Wyoming connection error: ${(e as Error).message}`);\n } finally {\n this.draining = false;\n }\n }\n\n private write(type: string, data?: Record<string, unknown>, payload?: Buffer): void {\n const header: Record<string, unknown> = { type };\n if (data !== undefined) {\n header.data = data;\n }\n if (payload) {\n header.payload_length = payload.length;\n }\n this.sock.write(`${JSON.stringify(header)}\\n`);\n if (payload) {\n this.sock.write(payload);\n }\n }\n\n private async handle(ev: WyEvent): Promise<void> {\n switch (ev.type) {\n case 'describe':\n this.write('info', this.info());\n break;\n // ASR request / start-of-speech (VAD) — reset the buffer, optional language hint.\n case 'transcribe':\n case 'voice-started':\n case 'audio-start':\n if (ev.data?.language) {\n this.lang =\n typeof ev.data.language === 'object'\n ? JSON.stringify(ev.data.language)\n : (ev.data.language as string).toString();\n }\n if (ev.data?.rate) {\n this.audioRate = Number(ev.data.rate) || this.audioRate;\n }\n this.audioChunks = [];\n break;\n case 'audio-chunk':\n if (ev.payload) {\n if (!this.audioChunks.length && ev.data?.rate) {\n this.audioRate = Number(ev.data.rate) || this.audioRate;\n }\n this.audioChunks.push(ev.payload);\n }\n break;\n // End-of-speech — either the explicit audio-stop or a VAD voice-stopped.\n case 'audio-stop':\n case 'voice-stopped':\n await this.onUtterance();\n break;\n case 'synthesize':\n if (ev.data?.language) {\n this.lang =\n typeof ev.data.language === 'object'\n ? JSON.stringify(ev.data.language)\n : (ev.data.language as string).toString();\n }\n await this.speak(\n ev.data?.text\n ? typeof ev.data?.text === 'object'\n ? JSON.stringify(ev.data.text)\n : (ev.data.text as string).toString()\n : '',\n );\n break;\n case 'ping':\n this.write('pong', ev.data);\n break;\n }\n }\n\n private async onUtterance(): Promise<void> {\n const pcm = Buffer.concat(this.audioChunks);\n this.audioChunks = [];\n if (!pcm.length) {\n return;\n }\n try {\n const hints = this.opts.getHints ? await this.opts.getHints() : undefined;\n const sttStart = Date.now();\n const text = await this.opts.stt.transcribe(pcm, this.audioRate, this.lang, hints);\n const sttMs = Date.now() - sttStart;\n this.opts.log.info(`Wyoming Q: ${text || '(empty)'}`);\n this.write('transcript', { text });\n if (!text) {\n return;\n }\n const answer = (await this.opts.answer(text, sttMs)).trim();\n this.opts.log.info(`Wyoming A: ${answer}`);\n if (answer) {\n await this.speak(answer);\n }\n } catch (e) {\n this.opts.log.error(`Wyoming pipeline failed: ${(e as Error).message}`);\n this.write('error', { text: (e as Error).message });\n }\n }\n\n private async speak(text: string): Promise<void> {\n if (!text) {\n return;\n }\n try {\n const { pcm, sampleRate } = await this.opts.tts.synthesize(text, this.lang);\n const meta = { rate: sampleRate, width: 2, channels: 1 };\n this.write('audio-start', meta);\n for (let off = 0; off < pcm.length; off += AUDIO_CHUNK) {\n this.write('audio-chunk', meta, pcm.subarray(off, off + AUDIO_CHUNK));\n }\n this.write('audio-stop', {});\n } catch (e) {\n this.opts.log.error(`Wyoming TTS failed: ${(e as Error).message}`);\n this.write('error', { text: (e as Error).message });\n }\n }\n\n /** Capabilities advertised in response to `describe`. */\n private info(): Record<string, unknown> {\n const lang = (this.opts.language || 'en').split('-')[0];\n const attribution = { name: 'ioBroker', url: 'https://github.com/ioBroker/ioBroker.assistant' };\n return {\n asr: [\n {\n name: 'iobroker-assistant',\n attribution,\n installed: true,\n description: 'ioBroker.assistant speech-to-text',\n version: '1.0',\n models: [\n {\n name: 'default',\n attribution,\n installed: true,\n description: '',\n version: '1.0',\n languages: [lang],\n },\n ],\n },\n ],\n tts: [\n {\n name: 'iobroker-assistant',\n attribution,\n installed: true,\n description: 'ioBroker.assistant text-to-speech',\n version: '1.0',\n voices: [\n {\n name: 'default',\n attribution,\n installed: true,\n description: '',\n version: '1.0',\n languages: [lang],\n },\n ],\n },\n ],\n handle: [\n {\n name: 'iobroker-assistant',\n attribution,\n installed: true,\n description: 'ioBroker.assistant intent handling',\n version: '1.0',\n models: [\n {\n name: 'default',\n attribution,\n installed: true,\n description: '',\n version: '1.0',\n languages: [lang],\n },\n ],\n },\n ],\n };\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"wyoming.js","sourceRoot":"","sources":["../../../src/lib/voice/wyoming.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Wyoming protocol endpoint (https://github.com/rhasspy/wyoming) — a TCP server that bridges Wyoming\n * events to the existing STT → answer → TTS pipeline, so a `wyoming-satellite`, Home Assistant Voice PE\n * or ESPHome voice device can stream to this adapter directly.\n *\n * Wire format: one JSON header line terminated by '\\n', optionally followed by `payload_length` raw\n * bytes (audio). We handle: describe → info; audio-start/chunk/stop → STT → answer → TTS (streamed back\n * as audio-*); synthesize → TTS.\n *\n * ⚠️ Framing is unit-tested (loopback); interop with real HA Voice PE / wyoming-satellite still needs\n * on-device validation.\n */\nimport * as net from 'node:net';\nimport type { SttEngine } from './stt';\nimport type { TtsEngine } from './tts';\nimport { Buffer } from 'node:buffer';\n\nexport interface WyomingServerOptions {\n port: number;\n bindAddress?: string;\n /** ISO-639-1 language hint for STT/TTS. */\n language: string;\n stt: SttEngine;\n tts: TtsEngine;\n /** `sttMs`: how long recognition took, `audioMs`: length of the recording (both for the journal). */\n answer: (question: string, sttMs: number, audioMs: number) => Promise<string>;\n /** The reply was synthesised: how long that took and how long it plays (for the request journal). */\n onSpoken?: (info: { ms: number; audioMs: number }) => void;\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}\n\ninterface WyEvent {\n type: string;\n data?: Record<string, unknown>;\n payload?: Buffer;\n}\n\n/** Bytes of PCM per outgoing audio-chunk. */\nconst AUDIO_CHUNK = 8192;\n\nexport class WyomingServer {\n private server: net.Server | null = null;\n private readonly conns = new Set<net.Socket>();\n\n constructor(private readonly opts: WyomingServerOptions) {}\n\n start(): Promise<void> {\n return new Promise((resolve, reject) => {\n const server = net.createServer(sock => this.onConnection(sock));\n this.server = server;\n server.once('error', reject);\n server.on('error', e => this.opts.log.error(`Wyoming server error: ${e.message}`));\n const addr =\n this.opts.bindAddress && this.opts.bindAddress !== '0.0.0.0' ? this.opts.bindAddress : undefined;\n server.listen(this.opts.port, addr, () => {\n this.opts.log.info(`Wyoming server listening on TCP ${addr || '0.0.0.0'}:${this.opts.port}`);\n resolve();\n });\n });\n }\n\n async stop(): Promise<void> {\n for (const s of this.conns) {\n s.destroy();\n }\n this.conns.clear();\n const server = this.server;\n this.server = null;\n if (server) {\n await new Promise<void>(res => server.close(() => res()));\n }\n }\n\n private onConnection(sock: net.Socket): void {\n this.conns.add(sock);\n sock.on('close', () => this.conns.delete(sock));\n sock.on('error', () => this.conns.delete(sock));\n new WyomingConnection(sock, this.opts);\n }\n}\n\n/** One client connection: frame parser + event handling. */\nclass WyomingConnection {\n private buf = Buffer.alloc(0);\n private draining = false;\n private audioChunks: Buffer[] = [];\n private audioRate = 16000;\n /** Per-utterance language (overridable via `transcribe`/`synthesize` `data.language`). */\n private lang: string;\n\n constructor(\n private readonly sock: net.Socket,\n private readonly opts: WyomingServerOptions,\n ) {\n this.lang = opts.language;\n sock.on('data', d => {\n this.buf = Buffer.concat([this.buf, d]);\n void this.drain();\n });\n }\n\n /** Pull complete events (header line + optional payload) out of the receive buffer. */\n private async drain(): Promise<void> {\n if (this.draining) {\n return;\n }\n this.draining = true;\n try {\n for (;;) {\n const nl = this.buf.indexOf(0x0a); // '\\n'\n if (nl < 0) {\n break;\n }\n let header: { type?: string; data?: Record<string, unknown>; payload_length?: number };\n try {\n header = JSON.parse(this.buf.subarray(0, nl).toString('utf8'));\n } catch {\n this.buf = this.buf.subarray(nl + 1); // skip a bad line\n continue;\n }\n const payloadLen = Number(header.payload_length || 0);\n const total = nl + 1 + payloadLen;\n if (this.buf.length < total) {\n break; // wait for the rest of the payload\n }\n const payload = payloadLen ? Buffer.from(this.buf.subarray(nl + 1, total)) : undefined;\n this.buf = this.buf.subarray(total);\n await this.handle({ type: String(header.type || ''), data: header.data, payload });\n }\n } catch (e) {\n this.opts.log.warn(`Wyoming connection error: ${(e as Error).message}`);\n } finally {\n this.draining = false;\n }\n }\n\n private write(type: string, data?: Record<string, unknown>, payload?: Buffer): void {\n const header: Record<string, unknown> = { type };\n if (data !== undefined) {\n header.data = data;\n }\n if (payload) {\n header.payload_length = payload.length;\n }\n this.sock.write(`${JSON.stringify(header)}\\n`);\n if (payload) {\n this.sock.write(payload);\n }\n }\n\n private async handle(ev: WyEvent): Promise<void> {\n switch (ev.type) {\n case 'describe':\n this.write('info', this.info());\n break;\n // ASR request / start-of-speech (VAD) — reset the buffer, optional language hint.\n case 'transcribe':\n case 'voice-started':\n case 'audio-start':\n if (ev.data?.language) {\n this.lang =\n typeof ev.data.language === 'object'\n ? JSON.stringify(ev.data.language)\n : (ev.data.language as string).toString();\n }\n if (ev.data?.rate) {\n this.audioRate = Number(ev.data.rate) || this.audioRate;\n }\n this.audioChunks = [];\n break;\n case 'audio-chunk':\n if (ev.payload) {\n if (!this.audioChunks.length && ev.data?.rate) {\n this.audioRate = Number(ev.data.rate) || this.audioRate;\n }\n this.audioChunks.push(ev.payload);\n }\n break;\n // End-of-speech — either the explicit audio-stop or a VAD voice-stopped.\n case 'audio-stop':\n case 'voice-stopped':\n await this.onUtterance();\n break;\n case 'synthesize':\n if (ev.data?.language) {\n this.lang =\n typeof ev.data.language === 'object'\n ? JSON.stringify(ev.data.language)\n : (ev.data.language as string).toString();\n }\n await this.speak(\n ev.data?.text\n ? typeof ev.data?.text === 'object'\n ? JSON.stringify(ev.data.text)\n : (ev.data.text as string).toString()\n : '',\n );\n break;\n case 'ping':\n this.write('pong', ev.data);\n break;\n }\n }\n\n private async onUtterance(): Promise<void> {\n const pcm = Buffer.concat(this.audioChunks);\n this.audioChunks = [];\n if (!pcm.length) {\n return;\n }\n try {\n const hints = this.opts.getHints ? await this.opts.getHints() : undefined;\n const sttStart = Date.now();\n const text = await this.opts.stt.transcribe(pcm, this.audioRate, this.lang, hints);\n const sttMs = Date.now() - sttStart;\n this.opts.log.info(`Wyoming Q: ${text || '(empty)'}`);\n this.write('transcript', { text });\n if (!text) {\n return;\n }\n const audioMs = Math.round((pcm.length / 2 / this.audioRate) * 1000);\n const answer = (await this.opts.answer(text, sttMs, audioMs)).trim();\n this.opts.log.info(`Wyoming A: ${answer}`);\n const spoken = answer ? await this.speak(answer) : null;\n if (spoken) {\n // Only a reply counts — a bare `synthesize` from the client is no request of ours.\n this.opts.onSpoken?.(spoken);\n }\n } catch (e) {\n this.opts.log.error(`Wyoming pipeline failed: ${(e as Error).message}`);\n this.write('error', { text: (e as Error).message });\n }\n }\n\n /** Synthesise and stream `text`; returns how long synthesis took and how long the audio plays. */\n private async speak(text: string): Promise<{ ms: number; audioMs: number } | null> {\n if (!text) {\n return null;\n }\n try {\n const ttsStart = Date.now();\n const { pcm, sampleRate } = await this.opts.tts.synthesize(text, this.lang);\n const spoken = { ms: Date.now() - ttsStart, audioMs: (pcm.length / 2 / sampleRate) * 1000 };\n const meta = { rate: sampleRate, width: 2, channels: 1 };\n this.write('audio-start', meta);\n for (let off = 0; off < pcm.length; off += AUDIO_CHUNK) {\n this.write('audio-chunk', meta, pcm.subarray(off, off + AUDIO_CHUNK));\n }\n this.write('audio-stop', {});\n return spoken;\n } catch (e) {\n this.opts.log.error(`Wyoming TTS failed: ${(e as Error).message}`);\n this.write('error', { text: (e as Error).message });\n return null;\n }\n }\n\n /** Capabilities advertised in response to `describe`. */\n private info(): Record<string, unknown> {\n const lang = (this.opts.language || 'en').split('-')[0];\n const attribution = { name: 'ioBroker', url: 'https://github.com/ioBroker/ioBroker.assistant' };\n return {\n asr: [\n {\n name: 'iobroker-assistant',\n attribution,\n installed: true,\n description: 'ioBroker.assistant speech-to-text',\n version: '1.0',\n models: [\n {\n name: 'default',\n attribution,\n installed: true,\n description: '',\n version: '1.0',\n languages: [lang],\n },\n ],\n },\n ],\n tts: [\n {\n name: 'iobroker-assistant',\n attribution,\n installed: true,\n description: 'ioBroker.assistant text-to-speech',\n version: '1.0',\n voices: [\n {\n name: 'default',\n attribution,\n installed: true,\n description: '',\n version: '1.0',\n languages: [lang],\n },\n ],\n },\n ],\n handle: [\n {\n name: 'iobroker-assistant',\n attribution,\n installed: true,\n description: 'ioBroker.assistant intent handling',\n version: '1.0',\n models: [\n {\n name: 'default',\n attribution,\n installed: true,\n description: '',\n version: '1.0',\n languages: [lang],\n },\n ],\n },\n ],\n };\n }\n}\n"]}
|