@framers/agentos-ext-openwakeword 0.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/LICENSE +23 -0
- package/SKILL.md +17 -0
- package/dist/OpenWakeWordProvider.d.ts +129 -0
- package/dist/OpenWakeWordProvider.d.ts.map +1 -0
- package/dist/OpenWakeWordProvider.js +180 -0
- package/dist/OpenWakeWordProvider.js.map +1 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +73 -0
- package/dist/index.js.map +1 -0
- package/manifest.json +8 -0
- package/package.json +43 -0
- package/src/OpenWakeWordProvider.ts +245 -0
- package/src/index.ts +107 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Framers
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# openwakeword — ONNX Wake-Word Extension Pack
|
|
2
|
+
|
|
3
|
+
Provides wake-word detection using [OpenWakeWord](https://github.com/dscripka/openWakeWord) ONNX models via `onnxruntime-node`.
|
|
4
|
+
|
|
5
|
+
## Configuration
|
|
6
|
+
|
|
7
|
+
| Option | Default | Description |
|
|
8
|
+
|--------|---------|-------------|
|
|
9
|
+
| `modelPath` | `OPENWAKEWORD_MODEL_PATH` env or `~/.agentos/models/openwakeword/hey_mycroft.onnx` | Path to ONNX model file |
|
|
10
|
+
| `threshold` | `0.5` | Detection probability threshold |
|
|
11
|
+
| `keyword` | `'hey mycroft'` | Human-readable keyword label |
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
- Fully offline — no network required
|
|
15
|
+
- Any ONNX-compatible wake-word model supported
|
|
16
|
+
- Configurable detection threshold
|
|
17
|
+
- Feature extraction: RMS energy + zero-crossing rate from 80 ms audio frames
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file OpenWakeWordProvider.ts
|
|
3
|
+
* @description Wake-word detection provider using OpenWakeWord ONNX models.
|
|
4
|
+
*
|
|
5
|
+
* [OpenWakeWord](https://github.com/dscripka/openWakeWord) is an open-source
|
|
6
|
+
* wake-word / wake-phrase detection framework. This provider loads any
|
|
7
|
+
* compatible ONNX model via `onnxruntime-node` and processes 80 ms audio
|
|
8
|
+
* frames (1280 samples at 16 kHz).
|
|
9
|
+
*
|
|
10
|
+
* Feature extraction uses a simple but effective two-element vector:
|
|
11
|
+
* - **RMS energy**: root-mean-square amplitude of the frame, normalised to
|
|
12
|
+
* INT16 range → [0, 1].
|
|
13
|
+
* - **Zero-crossing rate**: fraction of consecutive sample pairs whose sign
|
|
14
|
+
* differs → [0, 1].
|
|
15
|
+
*
|
|
16
|
+
* These features capture both energy and spectral texture without requiring
|
|
17
|
+
* an additional mel-filterbank preprocessing step, making the implementation
|
|
18
|
+
* self-contained and dependency-free beyond `onnxruntime-node`.
|
|
19
|
+
*
|
|
20
|
+
* @module openwakeword
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* A detected wake-word event returned by {@link OpenWakeWordProvider.detect}.
|
|
24
|
+
*/
|
|
25
|
+
export interface WakeWordDetection {
|
|
26
|
+
/** Human-readable keyword label (from constructor options). */
|
|
27
|
+
keyword: string;
|
|
28
|
+
/** Detection probability in [0, 1] as reported by the ONNX model. */
|
|
29
|
+
confidence: number;
|
|
30
|
+
/** Stable provider identifier. */
|
|
31
|
+
providerId: 'openwakeword';
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Constructor options for {@link OpenWakeWordProvider}.
|
|
35
|
+
*/
|
|
36
|
+
export interface OpenWakeWordProviderOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Absolute path to the ONNX wake-word model file.
|
|
39
|
+
* Resolved from `OPENWAKEWORD_MODEL_PATH` env var when omitted.
|
|
40
|
+
* @defaultValue `~/.agentos/models/openwakeword/hey_mycroft.onnx`
|
|
41
|
+
*/
|
|
42
|
+
modelPath?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Detection probability threshold. The model output must exceed this value
|
|
45
|
+
* for a detection to be returned.
|
|
46
|
+
* @defaultValue `0.5`
|
|
47
|
+
*/
|
|
48
|
+
threshold?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Human-readable keyword label included in every {@link WakeWordDetection}.
|
|
51
|
+
* @defaultValue `'hey mycroft'`
|
|
52
|
+
*/
|
|
53
|
+
keyword?: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* OpenWakeWord ONNX wake-word provider.
|
|
57
|
+
*
|
|
58
|
+
* Implements the `WakeWordProvider` contract expected by the AgentOS voice
|
|
59
|
+
* pipeline without taking a hard runtime dependency on the interface types.
|
|
60
|
+
*/
|
|
61
|
+
export declare class OpenWakeWordProvider {
|
|
62
|
+
/** Stable provider identifier used by the AgentOS extension registry. */
|
|
63
|
+
readonly id = "openwakeword";
|
|
64
|
+
private readonly _modelPath;
|
|
65
|
+
private readonly _threshold;
|
|
66
|
+
private readonly _keyword;
|
|
67
|
+
/** Lazily loaded ONNX inference session. */
|
|
68
|
+
private _session;
|
|
69
|
+
/**
|
|
70
|
+
* Create a new {@link OpenWakeWordProvider}.
|
|
71
|
+
*
|
|
72
|
+
* @param options - Optional configuration. All fields have sensible defaults.
|
|
73
|
+
*/
|
|
74
|
+
constructor(options?: OpenWakeWordProviderOptions);
|
|
75
|
+
/**
|
|
76
|
+
* Lazily create the ONNX `InferenceSession`.
|
|
77
|
+
*
|
|
78
|
+
* Dynamic import keeps the peer dep truly optional at module-load time.
|
|
79
|
+
*/
|
|
80
|
+
private _getSession;
|
|
81
|
+
/**
|
|
82
|
+
* Extract a two-element feature vector from a raw PCM frame.
|
|
83
|
+
*
|
|
84
|
+
* The features are:
|
|
85
|
+
* 1. **Normalised RMS energy** — captures overall loudness.
|
|
86
|
+
* 2. **Zero-crossing rate** — captures high-frequency content / spectral texture.
|
|
87
|
+
*
|
|
88
|
+
* Both values are in [0, 1] and are concatenated into a `Float32Array` of
|
|
89
|
+
* length 2 for consumption by the ONNX model.
|
|
90
|
+
*
|
|
91
|
+
* @param frame - 16-bit signed PCM samples (Int16Array).
|
|
92
|
+
* @returns A `Float32Array` with `[rmsNorm, zcr]`.
|
|
93
|
+
*/
|
|
94
|
+
private _extractFeatures;
|
|
95
|
+
/**
|
|
96
|
+
* Process a single 80 ms audio frame and detect any wake-word.
|
|
97
|
+
*
|
|
98
|
+
* The frame should contain 1280 samples of 16-bit PCM at 16 kHz. The method
|
|
99
|
+
* extracts RMS energy and zero-crossing rate as a two-element feature vector,
|
|
100
|
+
* runs ONNX inference, and returns a detection if the output probability
|
|
101
|
+
* exceeds the configured threshold.
|
|
102
|
+
*
|
|
103
|
+
* @param frame - 16-bit PCM audio frame as an `Int16Array` (1280 samples).
|
|
104
|
+
* @param _sampleRate - Sample rate (informational; must be 16000).
|
|
105
|
+
* @returns A {@link WakeWordDetection} when a wake-word is detected, or `null`.
|
|
106
|
+
*/
|
|
107
|
+
detect(frame: Int16Array, _sampleRate: number): Promise<WakeWordDetection | null>;
|
|
108
|
+
/**
|
|
109
|
+
* No-op reset.
|
|
110
|
+
*
|
|
111
|
+
* The simple feature extraction used here is stateless. Call this method
|
|
112
|
+
* if you want to explicitly signal a context boundary (e.g. after a false
|
|
113
|
+
* positive), but it currently has no effect.
|
|
114
|
+
*/
|
|
115
|
+
reset(): void;
|
|
116
|
+
/**
|
|
117
|
+
* Release the ONNX session resources.
|
|
118
|
+
*
|
|
119
|
+
* Call this when the provider is no longer needed.
|
|
120
|
+
*/
|
|
121
|
+
dispose(): Promise<void>;
|
|
122
|
+
/** Returns the resolved model path. */
|
|
123
|
+
getModelPath(): string;
|
|
124
|
+
/** Returns the configured detection threshold. */
|
|
125
|
+
getThreshold(): number;
|
|
126
|
+
/** Returns the configured keyword label. */
|
|
127
|
+
getKeyword(): string;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=OpenWakeWordProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OpenWakeWordProvider.d.ts","sourceRoot":"","sources":["../src/OpenWakeWordProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AASH;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,+DAA+D;IAC/D,OAAO,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,UAAU,EAAE,MAAM,CAAC;IACnB,kCAAkC;IAClC,UAAU,EAAE,cAAc,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAMD;;;;;GAKG;AACH,qBAAa,oBAAoB;IAC/B,yEAAyE;IACzE,QAAQ,CAAC,EAAE,kBAAkB;IAE7B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAElC,4CAA4C;IAE5C,OAAO,CAAC,QAAQ,CAAoB;IAEpC;;;;OAIG;gBACS,OAAO,GAAE,2BAAgC;IAarD;;;;OAIG;YAEW,WAAW;IAQzB;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,gBAAgB;IAkCxB;;;;;;;;;;;OAWG;IACG,MAAM,CAAC,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IA2BvF;;;;;;OAMG;IACH,KAAK,IAAI,IAAI;IAIb;;;;OAIG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAgB9B,uCAAuC;IACvC,YAAY,IAAI,MAAM;IAEtB,kDAAkD;IAClD,YAAY,IAAI,MAAM;IAEtB,4CAA4C;IAC5C,UAAU,IAAI,MAAM;CACrB"}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file OpenWakeWordProvider.ts
|
|
3
|
+
* @description Wake-word detection provider using OpenWakeWord ONNX models.
|
|
4
|
+
*
|
|
5
|
+
* [OpenWakeWord](https://github.com/dscripka/openWakeWord) is an open-source
|
|
6
|
+
* wake-word / wake-phrase detection framework. This provider loads any
|
|
7
|
+
* compatible ONNX model via `onnxruntime-node` and processes 80 ms audio
|
|
8
|
+
* frames (1280 samples at 16 kHz).
|
|
9
|
+
*
|
|
10
|
+
* Feature extraction uses a simple but effective two-element vector:
|
|
11
|
+
* - **RMS energy**: root-mean-square amplitude of the frame, normalised to
|
|
12
|
+
* INT16 range → [0, 1].
|
|
13
|
+
* - **Zero-crossing rate**: fraction of consecutive sample pairs whose sign
|
|
14
|
+
* differs → [0, 1].
|
|
15
|
+
*
|
|
16
|
+
* These features capture both energy and spectral texture without requiring
|
|
17
|
+
* an additional mel-filterbank preprocessing step, making the implementation
|
|
18
|
+
* self-contained and dependency-free beyond `onnxruntime-node`.
|
|
19
|
+
*
|
|
20
|
+
* @module openwakeword
|
|
21
|
+
*/
|
|
22
|
+
import os from 'os';
|
|
23
|
+
import path from 'path';
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Provider
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
/**
|
|
28
|
+
* OpenWakeWord ONNX wake-word provider.
|
|
29
|
+
*
|
|
30
|
+
* Implements the `WakeWordProvider` contract expected by the AgentOS voice
|
|
31
|
+
* pipeline without taking a hard runtime dependency on the interface types.
|
|
32
|
+
*/
|
|
33
|
+
export class OpenWakeWordProvider {
|
|
34
|
+
/** Stable provider identifier used by the AgentOS extension registry. */
|
|
35
|
+
id = 'openwakeword';
|
|
36
|
+
_modelPath;
|
|
37
|
+
_threshold;
|
|
38
|
+
_keyword;
|
|
39
|
+
/** Lazily loaded ONNX inference session. */
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
41
|
+
_session = null;
|
|
42
|
+
/**
|
|
43
|
+
* Create a new {@link OpenWakeWordProvider}.
|
|
44
|
+
*
|
|
45
|
+
* @param options - Optional configuration. All fields have sensible defaults.
|
|
46
|
+
*/
|
|
47
|
+
constructor(options = {}) {
|
|
48
|
+
this._modelPath =
|
|
49
|
+
options.modelPath ??
|
|
50
|
+
process.env['OPENWAKEWORD_MODEL_PATH'] ??
|
|
51
|
+
path.join(os.homedir(), '.agentos', 'models', 'openwakeword', 'hey_mycroft.onnx');
|
|
52
|
+
this._threshold = options.threshold ?? 0.5;
|
|
53
|
+
this._keyword = options.keyword ?? 'hey mycroft';
|
|
54
|
+
}
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Private helpers
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
/**
|
|
59
|
+
* Lazily create the ONNX `InferenceSession`.
|
|
60
|
+
*
|
|
61
|
+
* Dynamic import keeps the peer dep truly optional at module-load time.
|
|
62
|
+
*/
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
+
async _getSession() {
|
|
65
|
+
if (!this._session) {
|
|
66
|
+
const ort = await import('onnxruntime-node');
|
|
67
|
+
this._session = await ort.InferenceSession.create(this._modelPath);
|
|
68
|
+
}
|
|
69
|
+
return this._session;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Extract a two-element feature vector from a raw PCM frame.
|
|
73
|
+
*
|
|
74
|
+
* The features are:
|
|
75
|
+
* 1. **Normalised RMS energy** — captures overall loudness.
|
|
76
|
+
* 2. **Zero-crossing rate** — captures high-frequency content / spectral texture.
|
|
77
|
+
*
|
|
78
|
+
* Both values are in [0, 1] and are concatenated into a `Float32Array` of
|
|
79
|
+
* length 2 for consumption by the ONNX model.
|
|
80
|
+
*
|
|
81
|
+
* @param frame - 16-bit signed PCM samples (Int16Array).
|
|
82
|
+
* @returns A `Float32Array` with `[rmsNorm, zcr]`.
|
|
83
|
+
*/
|
|
84
|
+
_extractFeatures(frame) {
|
|
85
|
+
const n = frame.length;
|
|
86
|
+
if (n === 0) {
|
|
87
|
+
return new Float32Array([0, 0]);
|
|
88
|
+
}
|
|
89
|
+
// RMS energy, normalised to [0, 1] using the INT16 max (32768).
|
|
90
|
+
let sumSq = 0;
|
|
91
|
+
for (let i = 0; i < n; i++) {
|
|
92
|
+
const s = frame[i];
|
|
93
|
+
sumSq += s * s;
|
|
94
|
+
}
|
|
95
|
+
const rms = Math.sqrt(sumSq / n);
|
|
96
|
+
const rmsNorm = Math.min(rms / 32768, 1);
|
|
97
|
+
// Zero-crossing rate: count sign changes / (n - 1).
|
|
98
|
+
let crossings = 0;
|
|
99
|
+
for (let i = 1; i < n; i++) {
|
|
100
|
+
const prev = frame[i - 1];
|
|
101
|
+
const curr = frame[i];
|
|
102
|
+
if ((prev >= 0 && curr < 0) || (prev < 0 && curr >= 0)) {
|
|
103
|
+
crossings++;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const zcr = n > 1 ? crossings / (n - 1) : 0;
|
|
107
|
+
return new Float32Array([rmsNorm, zcr]);
|
|
108
|
+
}
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Public API
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
/**
|
|
113
|
+
* Process a single 80 ms audio frame and detect any wake-word.
|
|
114
|
+
*
|
|
115
|
+
* The frame should contain 1280 samples of 16-bit PCM at 16 kHz. The method
|
|
116
|
+
* extracts RMS energy and zero-crossing rate as a two-element feature vector,
|
|
117
|
+
* runs ONNX inference, and returns a detection if the output probability
|
|
118
|
+
* exceeds the configured threshold.
|
|
119
|
+
*
|
|
120
|
+
* @param frame - 16-bit PCM audio frame as an `Int16Array` (1280 samples).
|
|
121
|
+
* @param _sampleRate - Sample rate (informational; must be 16000).
|
|
122
|
+
* @returns A {@link WakeWordDetection} when a wake-word is detected, or `null`.
|
|
123
|
+
*/
|
|
124
|
+
async detect(frame, _sampleRate) {
|
|
125
|
+
const ort = await import('onnxruntime-node');
|
|
126
|
+
const session = await this._getSession();
|
|
127
|
+
const features = this._extractFeatures(frame);
|
|
128
|
+
const inputTensor = new ort.Tensor('float32', features, [1, features.length]);
|
|
129
|
+
const outputs = await session.run({ input: inputTensor });
|
|
130
|
+
// The model is expected to output a single probability value. We accept
|
|
131
|
+
// the first element of the first output tensor regardless of key name.
|
|
132
|
+
const outputValues = Object.values(outputs);
|
|
133
|
+
const firstOutput = outputValues[0];
|
|
134
|
+
const probability = firstOutput?.data != null ? Number(firstOutput.data[0] ?? 0) : 0;
|
|
135
|
+
if (probability > this._threshold) {
|
|
136
|
+
return {
|
|
137
|
+
keyword: this._keyword,
|
|
138
|
+
confidence: probability,
|
|
139
|
+
providerId: 'openwakeword',
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* No-op reset.
|
|
146
|
+
*
|
|
147
|
+
* The simple feature extraction used here is stateless. Call this method
|
|
148
|
+
* if you want to explicitly signal a context boundary (e.g. after a false
|
|
149
|
+
* positive), but it currently has no effect.
|
|
150
|
+
*/
|
|
151
|
+
reset() {
|
|
152
|
+
// Intentional no-op — feature extraction is stateless.
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Release the ONNX session resources.
|
|
156
|
+
*
|
|
157
|
+
* Call this when the provider is no longer needed.
|
|
158
|
+
*/
|
|
159
|
+
async dispose() {
|
|
160
|
+
if (this._session) {
|
|
161
|
+
// onnxruntime-node sessions do not expose a release/destroy API in all
|
|
162
|
+
// versions; attempt graceful release if the method exists.
|
|
163
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
164
|
+
if (typeof this._session.release === 'function') {
|
|
165
|
+
await this._session.release();
|
|
166
|
+
}
|
|
167
|
+
this._session = null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// Accessors (for testing / diagnostics)
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
/** Returns the resolved model path. */
|
|
174
|
+
getModelPath() { return this._modelPath; }
|
|
175
|
+
/** Returns the configured detection threshold. */
|
|
176
|
+
getThreshold() { return this._threshold; }
|
|
177
|
+
/** Returns the configured keyword label. */
|
|
178
|
+
getKeyword() { return this._keyword; }
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=OpenWakeWordProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OpenWakeWordProvider.js","sourceRoot":"","sources":["../src/OpenWakeWordProvider.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file index.ts
|
|
3
|
+
* @description Pack factory for the OpenWakeWord extension pack.
|
|
4
|
+
*
|
|
5
|
+
* Exports the main {@link createOpenWakeWord} factory function and the
|
|
6
|
+
* {@link createExtensionPack} bridge function that conforms to the AgentOS
|
|
7
|
+
* manifest factory convention.
|
|
8
|
+
*
|
|
9
|
+
* ### Usage (direct)
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { createOpenWakeWord } from '@framers/agentos-ext-openwakeword';
|
|
12
|
+
*
|
|
13
|
+
* const wakeWord = createOpenWakeWord({ modelPath: '/opt/models/hey_mycroft.onnx' });
|
|
14
|
+
* const detection = await wakeWord.detect(frame, 16000);
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* ### Usage (manifest-driven)
|
|
18
|
+
* ```json
|
|
19
|
+
* { "packs": [{ "module": "@framers/agentos-ext-openwakeword" }] }
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @module openwakeword
|
|
23
|
+
*/
|
|
24
|
+
import { OpenWakeWordProvider } from './OpenWakeWordProvider.js';
|
|
25
|
+
import type { OpenWakeWordProviderOptions } from './OpenWakeWordProvider.js';
|
|
26
|
+
/** Subset of ExtensionDescriptor required by this pack. */
|
|
27
|
+
interface ExtensionDescriptor {
|
|
28
|
+
id: string;
|
|
29
|
+
kind: string;
|
|
30
|
+
payload: unknown;
|
|
31
|
+
enableByDefault?: boolean;
|
|
32
|
+
metadata?: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
/** Subset of ExtensionPack required by this pack. */
|
|
35
|
+
interface ExtensionPack {
|
|
36
|
+
id: string;
|
|
37
|
+
descriptors: ExtensionDescriptor[];
|
|
38
|
+
}
|
|
39
|
+
/** Subset of ExtensionPackContext required by this pack. */
|
|
40
|
+
interface ExtensionPackContext {
|
|
41
|
+
getSecret?: (id: string) => string | undefined;
|
|
42
|
+
options?: Record<string, unknown>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Create a standalone {@link OpenWakeWordProvider} instance.
|
|
46
|
+
*
|
|
47
|
+
* @param options - Optional constructor options.
|
|
48
|
+
* @returns Configured {@link OpenWakeWordProvider}.
|
|
49
|
+
*/
|
|
50
|
+
export declare function createOpenWakeWord(options?: OpenWakeWordProviderOptions): OpenWakeWordProvider;
|
|
51
|
+
/**
|
|
52
|
+
* AgentOS manifest factory function.
|
|
53
|
+
*
|
|
54
|
+
* Reads optional configuration from the context `options` map and returns an
|
|
55
|
+
* {@link ExtensionPack} containing a single `wake-word-provider` descriptor
|
|
56
|
+
* backed by {@link OpenWakeWordProvider}.
|
|
57
|
+
*
|
|
58
|
+
* @param context - Pack context supplied by the extension manager.
|
|
59
|
+
* @returns A fully configured {@link ExtensionPack}.
|
|
60
|
+
*/
|
|
61
|
+
export declare function createExtensionPack(context: ExtensionPackContext): ExtensionPack;
|
|
62
|
+
export { OpenWakeWordProvider } from './OpenWakeWordProvider.js';
|
|
63
|
+
export type { WakeWordDetection, OpenWakeWordProviderOptions } from './OpenWakeWordProvider.js';
|
|
64
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AAM7E,2DAA2D;AAC3D,UAAU,mBAAmB;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,qDAAqD;AACrD,UAAU,aAAa;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,mBAAmB,EAAE,CAAC;CACpC;AAED,4DAA4D;AAC5D,UAAU,oBAAoB;IAC5B,SAAS,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IAC/C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AASD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,CAAC,EAAE,2BAA2B,GAAG,oBAAoB,CAE9F;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CAoBhF;AAMD,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,YAAY,EAAE,iBAAiB,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file index.ts
|
|
3
|
+
* @description Pack factory for the OpenWakeWord extension pack.
|
|
4
|
+
*
|
|
5
|
+
* Exports the main {@link createOpenWakeWord} factory function and the
|
|
6
|
+
* {@link createExtensionPack} bridge function that conforms to the AgentOS
|
|
7
|
+
* manifest factory convention.
|
|
8
|
+
*
|
|
9
|
+
* ### Usage (direct)
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { createOpenWakeWord } from '@framers/agentos-ext-openwakeword';
|
|
12
|
+
*
|
|
13
|
+
* const wakeWord = createOpenWakeWord({ modelPath: '/opt/models/hey_mycroft.onnx' });
|
|
14
|
+
* const detection = await wakeWord.detect(frame, 16000);
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* ### Usage (manifest-driven)
|
|
18
|
+
* ```json
|
|
19
|
+
* { "packs": [{ "module": "@framers/agentos-ext-openwakeword" }] }
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @module openwakeword
|
|
23
|
+
*/
|
|
24
|
+
import { OpenWakeWordProvider } from './OpenWakeWordProvider.js';
|
|
25
|
+
/** Kind constant matching packages/agentos/src/extensions/types.ts. */
|
|
26
|
+
const EXTENSION_KIND_WAKE_WORD = 'wake-word-provider';
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Factories
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
/**
|
|
31
|
+
* Create a standalone {@link OpenWakeWordProvider} instance.
|
|
32
|
+
*
|
|
33
|
+
* @param options - Optional constructor options.
|
|
34
|
+
* @returns Configured {@link OpenWakeWordProvider}.
|
|
35
|
+
*/
|
|
36
|
+
export function createOpenWakeWord(options) {
|
|
37
|
+
return new OpenWakeWordProvider(options);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* AgentOS manifest factory function.
|
|
41
|
+
*
|
|
42
|
+
* Reads optional configuration from the context `options` map and returns an
|
|
43
|
+
* {@link ExtensionPack} containing a single `wake-word-provider` descriptor
|
|
44
|
+
* backed by {@link OpenWakeWordProvider}.
|
|
45
|
+
*
|
|
46
|
+
* @param context - Pack context supplied by the extension manager.
|
|
47
|
+
* @returns A fully configured {@link ExtensionPack}.
|
|
48
|
+
*/
|
|
49
|
+
export function createExtensionPack(context) {
|
|
50
|
+
const opts = context.options ?? {};
|
|
51
|
+
const provider = new OpenWakeWordProvider({
|
|
52
|
+
modelPath: opts['modelPath'],
|
|
53
|
+
threshold: opts['threshold'],
|
|
54
|
+
keyword: opts['keyword'],
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
id: 'openwakeword',
|
|
58
|
+
descriptors: [
|
|
59
|
+
{
|
|
60
|
+
id: 'openwakeword',
|
|
61
|
+
kind: EXTENSION_KIND_WAKE_WORD,
|
|
62
|
+
payload: provider,
|
|
63
|
+
enableByDefault: true,
|
|
64
|
+
metadata: { providerId: 'openwakeword' },
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Re-exports
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
export { OpenWakeWordProvider } from './OpenWakeWordProvider.js';
|
|
73
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AA4BjE,uEAAuE;AACvE,MAAM,wBAAwB,GAAG,oBAAoB,CAAC;AAEtD,8EAA8E;AAC9E,YAAY;AACZ,8EAA8E;AAE9E;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAqC;IACtE,OAAO,IAAI,oBAAoB,CAAC,OAAO,CAAC,CAAC;AAC3C,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAA6B;IAC/D,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;IACnC,MAAM,QAAQ,GAAG,IAAI,oBAAoB,CAAC;QACxC,SAAS,EAAE,IAAI,CAAC,WAAW,CAAuB;QAClD,SAAS,EAAE,IAAI,CAAC,WAAW,CAAuB;QAClD,OAAO,EAAE,IAAI,CAAC,SAAS,CAAuB;KAC/C,CAAC,CAAC;IAEH,OAAO;QACL,EAAE,EAAE,cAAc;QAClB,WAAW,EAAE;YACX;gBACE,EAAE,EAAE,cAAc;gBAClB,IAAI,EAAE,wBAAwB;gBAC9B,OAAO,EAAE,QAAQ;gBACjB,eAAe,EAAE,IAAI;gBACrB,QAAQ,EAAE,EAAE,UAAU,EAAE,cAAc,EAAE;aACzC;SACF;KACF,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC"}
|
package/manifest.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@framers/agentos-ext-openwakeword",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Wake-word detection via OpenWakeWord ONNX models for AgentOS voice pipeline",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src",
|
|
17
|
+
"SKILL.md",
|
|
18
|
+
"manifest.json"
|
|
19
|
+
],
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"@framers/agentos": "^0.1.0",
|
|
22
|
+
"onnxruntime-node": "^1.17.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"typescript": "^5.5.0",
|
|
26
|
+
"vitest": "^1.6.0",
|
|
27
|
+
"@framers/agentos": "0.1.94"
|
|
28
|
+
},
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"author": "Frame.dev",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/framersai/agentos-extensions.git",
|
|
34
|
+
"directory": "registry/curated/voice/openwakeword"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc -p tsconfig.json",
|
|
41
|
+
"test": "vitest run"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file OpenWakeWordProvider.ts
|
|
3
|
+
* @description Wake-word detection provider using OpenWakeWord ONNX models.
|
|
4
|
+
*
|
|
5
|
+
* [OpenWakeWord](https://github.com/dscripka/openWakeWord) is an open-source
|
|
6
|
+
* wake-word / wake-phrase detection framework. This provider loads any
|
|
7
|
+
* compatible ONNX model via `onnxruntime-node` and processes 80 ms audio
|
|
8
|
+
* frames (1280 samples at 16 kHz).
|
|
9
|
+
*
|
|
10
|
+
* Feature extraction uses a simple but effective two-element vector:
|
|
11
|
+
* - **RMS energy**: root-mean-square amplitude of the frame, normalised to
|
|
12
|
+
* INT16 range → [0, 1].
|
|
13
|
+
* - **Zero-crossing rate**: fraction of consecutive sample pairs whose sign
|
|
14
|
+
* differs → [0, 1].
|
|
15
|
+
*
|
|
16
|
+
* These features capture both energy and spectral texture without requiring
|
|
17
|
+
* an additional mel-filterbank preprocessing step, making the implementation
|
|
18
|
+
* self-contained and dependency-free beyond `onnxruntime-node`.
|
|
19
|
+
*
|
|
20
|
+
* @module openwakeword
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import os from 'os';
|
|
24
|
+
import path from 'path';
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Public types
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A detected wake-word event returned by {@link OpenWakeWordProvider.detect}.
|
|
32
|
+
*/
|
|
33
|
+
export interface WakeWordDetection {
|
|
34
|
+
/** Human-readable keyword label (from constructor options). */
|
|
35
|
+
keyword: string;
|
|
36
|
+
/** Detection probability in [0, 1] as reported by the ONNX model. */
|
|
37
|
+
confidence: number;
|
|
38
|
+
/** Stable provider identifier. */
|
|
39
|
+
providerId: 'openwakeword';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Constructor options for {@link OpenWakeWordProvider}.
|
|
44
|
+
*/
|
|
45
|
+
export interface OpenWakeWordProviderOptions {
|
|
46
|
+
/**
|
|
47
|
+
* Absolute path to the ONNX wake-word model file.
|
|
48
|
+
* Resolved from `OPENWAKEWORD_MODEL_PATH` env var when omitted.
|
|
49
|
+
* @defaultValue `~/.agentos/models/openwakeword/hey_mycroft.onnx`
|
|
50
|
+
*/
|
|
51
|
+
modelPath?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Detection probability threshold. The model output must exceed this value
|
|
54
|
+
* for a detection to be returned.
|
|
55
|
+
* @defaultValue `0.5`
|
|
56
|
+
*/
|
|
57
|
+
threshold?: number;
|
|
58
|
+
/**
|
|
59
|
+
* Human-readable keyword label included in every {@link WakeWordDetection}.
|
|
60
|
+
* @defaultValue `'hey mycroft'`
|
|
61
|
+
*/
|
|
62
|
+
keyword?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Provider
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* OpenWakeWord ONNX wake-word provider.
|
|
71
|
+
*
|
|
72
|
+
* Implements the `WakeWordProvider` contract expected by the AgentOS voice
|
|
73
|
+
* pipeline without taking a hard runtime dependency on the interface types.
|
|
74
|
+
*/
|
|
75
|
+
export class OpenWakeWordProvider {
|
|
76
|
+
/** Stable provider identifier used by the AgentOS extension registry. */
|
|
77
|
+
readonly id = 'openwakeword';
|
|
78
|
+
|
|
79
|
+
private readonly _modelPath: string;
|
|
80
|
+
private readonly _threshold: number;
|
|
81
|
+
private readonly _keyword: string;
|
|
82
|
+
|
|
83
|
+
/** Lazily loaded ONNX inference session. */
|
|
84
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
85
|
+
private _session: any | null = null;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Create a new {@link OpenWakeWordProvider}.
|
|
89
|
+
*
|
|
90
|
+
* @param options - Optional configuration. All fields have sensible defaults.
|
|
91
|
+
*/
|
|
92
|
+
constructor(options: OpenWakeWordProviderOptions = {}) {
|
|
93
|
+
this._modelPath =
|
|
94
|
+
options.modelPath ??
|
|
95
|
+
process.env['OPENWAKEWORD_MODEL_PATH'] ??
|
|
96
|
+
path.join(os.homedir(), '.agentos', 'models', 'openwakeword', 'hey_mycroft.onnx');
|
|
97
|
+
this._threshold = options.threshold ?? 0.5;
|
|
98
|
+
this._keyword = options.keyword ?? 'hey mycroft';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Private helpers
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Lazily create the ONNX `InferenceSession`.
|
|
107
|
+
*
|
|
108
|
+
* Dynamic import keeps the peer dep truly optional at module-load time.
|
|
109
|
+
*/
|
|
110
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
111
|
+
private async _getSession(): Promise<any> {
|
|
112
|
+
if (!this._session) {
|
|
113
|
+
const ort = await import('onnxruntime-node');
|
|
114
|
+
this._session = await ort.InferenceSession.create(this._modelPath);
|
|
115
|
+
}
|
|
116
|
+
return this._session;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Extract a two-element feature vector from a raw PCM frame.
|
|
121
|
+
*
|
|
122
|
+
* The features are:
|
|
123
|
+
* 1. **Normalised RMS energy** — captures overall loudness.
|
|
124
|
+
* 2. **Zero-crossing rate** — captures high-frequency content / spectral texture.
|
|
125
|
+
*
|
|
126
|
+
* Both values are in [0, 1] and are concatenated into a `Float32Array` of
|
|
127
|
+
* length 2 for consumption by the ONNX model.
|
|
128
|
+
*
|
|
129
|
+
* @param frame - 16-bit signed PCM samples (Int16Array).
|
|
130
|
+
* @returns A `Float32Array` with `[rmsNorm, zcr]`.
|
|
131
|
+
*/
|
|
132
|
+
private _extractFeatures(frame: Int16Array): Float32Array {
|
|
133
|
+
const n = frame.length;
|
|
134
|
+
|
|
135
|
+
if (n === 0) {
|
|
136
|
+
return new Float32Array([0, 0]);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// RMS energy, normalised to [0, 1] using the INT16 max (32768).
|
|
140
|
+
let sumSq = 0;
|
|
141
|
+
for (let i = 0; i < n; i++) {
|
|
142
|
+
const s = frame[i]!;
|
|
143
|
+
sumSq += s * s;
|
|
144
|
+
}
|
|
145
|
+
const rms = Math.sqrt(sumSq / n);
|
|
146
|
+
const rmsNorm = Math.min(rms / 32768, 1);
|
|
147
|
+
|
|
148
|
+
// Zero-crossing rate: count sign changes / (n - 1).
|
|
149
|
+
let crossings = 0;
|
|
150
|
+
for (let i = 1; i < n; i++) {
|
|
151
|
+
const prev = frame[i - 1]!;
|
|
152
|
+
const curr = frame[i]!;
|
|
153
|
+
if ((prev >= 0 && curr < 0) || (prev < 0 && curr >= 0)) {
|
|
154
|
+
crossings++;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const zcr = n > 1 ? crossings / (n - 1) : 0;
|
|
158
|
+
|
|
159
|
+
return new Float32Array([rmsNorm, zcr]);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Public API
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Process a single 80 ms audio frame and detect any wake-word.
|
|
168
|
+
*
|
|
169
|
+
* The frame should contain 1280 samples of 16-bit PCM at 16 kHz. The method
|
|
170
|
+
* extracts RMS energy and zero-crossing rate as a two-element feature vector,
|
|
171
|
+
* runs ONNX inference, and returns a detection if the output probability
|
|
172
|
+
* exceeds the configured threshold.
|
|
173
|
+
*
|
|
174
|
+
* @param frame - 16-bit PCM audio frame as an `Int16Array` (1280 samples).
|
|
175
|
+
* @param _sampleRate - Sample rate (informational; must be 16000).
|
|
176
|
+
* @returns A {@link WakeWordDetection} when a wake-word is detected, or `null`.
|
|
177
|
+
*/
|
|
178
|
+
async detect(frame: Int16Array, _sampleRate: number): Promise<WakeWordDetection | null> {
|
|
179
|
+
const ort = await import('onnxruntime-node');
|
|
180
|
+
const session = await this._getSession();
|
|
181
|
+
|
|
182
|
+
const features = this._extractFeatures(frame);
|
|
183
|
+
const inputTensor = new ort.Tensor('float32', features, [1, features.length]);
|
|
184
|
+
|
|
185
|
+
const outputs = await session.run({ input: inputTensor });
|
|
186
|
+
|
|
187
|
+
// The model is expected to output a single probability value. We accept
|
|
188
|
+
// the first element of the first output tensor regardless of key name.
|
|
189
|
+
const outputValues = Object.values(outputs) as Array<{ data: Float32Array | number[] }>;
|
|
190
|
+
const firstOutput = outputValues[0];
|
|
191
|
+
const probability: number =
|
|
192
|
+
firstOutput?.data != null ? Number(firstOutput.data[0] ?? 0) : 0;
|
|
193
|
+
|
|
194
|
+
if (probability > this._threshold) {
|
|
195
|
+
return {
|
|
196
|
+
keyword: this._keyword,
|
|
197
|
+
confidence: probability,
|
|
198
|
+
providerId: 'openwakeword',
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* No-op reset.
|
|
207
|
+
*
|
|
208
|
+
* The simple feature extraction used here is stateless. Call this method
|
|
209
|
+
* if you want to explicitly signal a context boundary (e.g. after a false
|
|
210
|
+
* positive), but it currently has no effect.
|
|
211
|
+
*/
|
|
212
|
+
reset(): void {
|
|
213
|
+
// Intentional no-op — feature extraction is stateless.
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Release the ONNX session resources.
|
|
218
|
+
*
|
|
219
|
+
* Call this when the provider is no longer needed.
|
|
220
|
+
*/
|
|
221
|
+
async dispose(): Promise<void> {
|
|
222
|
+
if (this._session) {
|
|
223
|
+
// onnxruntime-node sessions do not expose a release/destroy API in all
|
|
224
|
+
// versions; attempt graceful release if the method exists.
|
|
225
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
226
|
+
if (typeof this._session.release === 'function') {
|
|
227
|
+
await (this._session as { release(): Promise<void> }).release();
|
|
228
|
+
}
|
|
229
|
+
this._session = null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Accessors (for testing / diagnostics)
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
/** Returns the resolved model path. */
|
|
238
|
+
getModelPath(): string { return this._modelPath; }
|
|
239
|
+
|
|
240
|
+
/** Returns the configured detection threshold. */
|
|
241
|
+
getThreshold(): number { return this._threshold; }
|
|
242
|
+
|
|
243
|
+
/** Returns the configured keyword label. */
|
|
244
|
+
getKeyword(): string { return this._keyword; }
|
|
245
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file index.ts
|
|
3
|
+
* @description Pack factory for the OpenWakeWord extension pack.
|
|
4
|
+
*
|
|
5
|
+
* Exports the main {@link createOpenWakeWord} factory function and the
|
|
6
|
+
* {@link createExtensionPack} bridge function that conforms to the AgentOS
|
|
7
|
+
* manifest factory convention.
|
|
8
|
+
*
|
|
9
|
+
* ### Usage (direct)
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { createOpenWakeWord } from '@framers/agentos-ext-openwakeword';
|
|
12
|
+
*
|
|
13
|
+
* const wakeWord = createOpenWakeWord({ modelPath: '/opt/models/hey_mycroft.onnx' });
|
|
14
|
+
* const detection = await wakeWord.detect(frame, 16000);
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* ### Usage (manifest-driven)
|
|
18
|
+
* ```json
|
|
19
|
+
* { "packs": [{ "module": "@framers/agentos-ext-openwakeword" }] }
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @module openwakeword
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { OpenWakeWordProvider } from './OpenWakeWordProvider.js';
|
|
26
|
+
import type { OpenWakeWordProviderOptions } from './OpenWakeWordProvider.js';
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Local interface mirrors — avoids a hard runtime dep on @framers/agentos
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
/** Subset of ExtensionDescriptor required by this pack. */
|
|
33
|
+
interface ExtensionDescriptor {
|
|
34
|
+
id: string;
|
|
35
|
+
kind: string;
|
|
36
|
+
payload: unknown;
|
|
37
|
+
enableByDefault?: boolean;
|
|
38
|
+
metadata?: Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Subset of ExtensionPack required by this pack. */
|
|
42
|
+
interface ExtensionPack {
|
|
43
|
+
id: string;
|
|
44
|
+
descriptors: ExtensionDescriptor[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Subset of ExtensionPackContext required by this pack. */
|
|
48
|
+
interface ExtensionPackContext {
|
|
49
|
+
getSecret?: (id: string) => string | undefined;
|
|
50
|
+
options?: Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Kind constant matching packages/agentos/src/extensions/types.ts. */
|
|
54
|
+
const EXTENSION_KIND_WAKE_WORD = 'wake-word-provider';
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Factories
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create a standalone {@link OpenWakeWordProvider} instance.
|
|
62
|
+
*
|
|
63
|
+
* @param options - Optional constructor options.
|
|
64
|
+
* @returns Configured {@link OpenWakeWordProvider}.
|
|
65
|
+
*/
|
|
66
|
+
export function createOpenWakeWord(options?: OpenWakeWordProviderOptions): OpenWakeWordProvider {
|
|
67
|
+
return new OpenWakeWordProvider(options);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* AgentOS manifest factory function.
|
|
72
|
+
*
|
|
73
|
+
* Reads optional configuration from the context `options` map and returns an
|
|
74
|
+
* {@link ExtensionPack} containing a single `wake-word-provider` descriptor
|
|
75
|
+
* backed by {@link OpenWakeWordProvider}.
|
|
76
|
+
*
|
|
77
|
+
* @param context - Pack context supplied by the extension manager.
|
|
78
|
+
* @returns A fully configured {@link ExtensionPack}.
|
|
79
|
+
*/
|
|
80
|
+
export function createExtensionPack(context: ExtensionPackContext): ExtensionPack {
|
|
81
|
+
const opts = context.options ?? {};
|
|
82
|
+
const provider = new OpenWakeWordProvider({
|
|
83
|
+
modelPath: opts['modelPath'] as string | undefined,
|
|
84
|
+
threshold: opts['threshold'] as number | undefined,
|
|
85
|
+
keyword: opts['keyword'] as string | undefined,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
id: 'openwakeword',
|
|
90
|
+
descriptors: [
|
|
91
|
+
{
|
|
92
|
+
id: 'openwakeword',
|
|
93
|
+
kind: EXTENSION_KIND_WAKE_WORD,
|
|
94
|
+
payload: provider,
|
|
95
|
+
enableByDefault: true,
|
|
96
|
+
metadata: { providerId: 'openwakeword' },
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Re-exports
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
export { OpenWakeWordProvider } from './OpenWakeWordProvider.js';
|
|
107
|
+
export type { WakeWordDetection, OpenWakeWordProviderOptions } from './OpenWakeWordProvider.js';
|