@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 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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"}
@@ -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
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "@framers/agentos-ext-openwakeword",
3
+ "version": "0.1.0",
4
+ "description": "Wake-word detection via OpenWakeWord ONNX models",
5
+ "kind": "wake-word-provider",
6
+ "extensionId": "openwakeword",
7
+ "entryPoint": "./dist/index.js"
8
+ }
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';