@framers/agentos-ext-porcupine 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,16 @@
1
+ # porcupine — Wake-Word Extension Pack
2
+
3
+ Provides wake-word detection via the [Picovoice Porcupine](https://picovoice.ai/platform/porcupine/) engine.
4
+
5
+ ## Configuration
6
+
7
+ | Option | Description |
8
+ |--------|-------------|
9
+ | `accessKey` | **Required.** Picovoice access key from [console.picovoice.ai](https://console.picovoice.ai/) |
10
+ | `keywords` | Array of built-in keyword names (e.g. `['porcupine', 'bumblebee']`) |
11
+ | `sensitivities` | Per-keyword detection sensitivity in [0, 1] (default 0.5 each) |
12
+
13
+ ## Features
14
+ - On-device, privacy-preserving detection (no audio sent to cloud)
15
+ - Stateless per-frame processing
16
+ - Configurable sensitivity per keyword
@@ -0,0 +1,109 @@
1
+ /**
2
+ * @file PorcupineWakeWordProvider.ts
3
+ * @description Wake-word detection provider backed by Picovoice Porcupine.
4
+ *
5
+ * [Porcupine](https://picovoice.ai/platform/porcupine/) is an on-device
6
+ * wake-word engine. This provider wraps `@picovoice/porcupine-node` and
7
+ * exposes the `WakeWordProvider` contract expected by the AgentOS voice
8
+ * pipeline.
9
+ *
10
+ * The `@picovoice/porcupine-node` package is declared as a peer dependency so
11
+ * it is loaded only at runtime via dynamic `import()`.
12
+ *
13
+ * ### Threading model
14
+ * Porcupine's `process()` method is synchronous and stateless per-frame — each
15
+ * 512-sample frame is processed independently. The provider is therefore safe
16
+ * to call from any async context.
17
+ *
18
+ * @module porcupine
19
+ */
20
+ /**
21
+ * A detected wake-word event returned by {@link PorcupineWakeWordProvider.detect}.
22
+ */
23
+ export interface WakeWordDetection {
24
+ /** The keyword string that was detected (e.g. `'porcupine'`). */
25
+ keyword: string;
26
+ /**
27
+ * Confidence score. Porcupine does not expose a per-detection confidence;
28
+ * this is always `1.0` to signal a positive detection.
29
+ */
30
+ confidence: 1.0;
31
+ /** Stable provider identifier. */
32
+ providerId: 'porcupine';
33
+ }
34
+ /**
35
+ * Constructor options for {@link PorcupineWakeWordProvider}.
36
+ */
37
+ export interface PorcupineWakeWordProviderOptions {
38
+ /** Picovoice access key from https://console.picovoice.ai/ */
39
+ accessKey: string;
40
+ /**
41
+ * Built-in keyword names to detect (e.g. `['porcupine', 'bumblebee']`).
42
+ * @defaultValue `['porcupine']`
43
+ */
44
+ keywords?: string[];
45
+ /**
46
+ * Detection sensitivity in [0, 1] for each keyword.
47
+ * Must be the same length as `keywords` when provided.
48
+ * @defaultValue `[0.5]` (or `0.5` per keyword)
49
+ */
50
+ sensitivities?: number[];
51
+ }
52
+ /**
53
+ * Picovoice Porcupine wake-word provider.
54
+ *
55
+ * Implements the `WakeWordProvider` contract expected by the AgentOS voice
56
+ * pipeline without taking a hard runtime dependency on the interface types.
57
+ */
58
+ export declare class PorcupineWakeWordProvider {
59
+ /** Stable provider identifier used by the AgentOS extension registry. */
60
+ readonly id = "porcupine";
61
+ private readonly _accessKey;
62
+ private readonly _keywords;
63
+ private readonly _sensitivities;
64
+ /** Lazily initialised Porcupine instance. */
65
+ private _porcupine;
66
+ /**
67
+ * Create a new {@link PorcupineWakeWordProvider}.
68
+ *
69
+ * @param options - Configuration including the required Picovoice access key.
70
+ */
71
+ constructor(options: PorcupineWakeWordProviderOptions);
72
+ /**
73
+ * Lazily initialise the Porcupine engine on first use.
74
+ *
75
+ * Dynamic import keeps the peer dep truly optional at module-load time.
76
+ */
77
+ private _getPorcupine;
78
+ /**
79
+ * Process a single audio frame and detect any wake-word.
80
+ *
81
+ * Each frame must be exactly 512 samples of 16-bit PCM at 16 kHz (the
82
+ * Porcupine frame length). `sampleRate` is accepted for interface
83
+ * compatibility but Porcupine always operates at 16 kHz.
84
+ *
85
+ * @param frame - 16-bit PCM audio frame as an `Int16Array` (512 samples).
86
+ * @param sampleRate - Sample rate (informational; must be 16000 for Porcupine).
87
+ * @returns A {@link WakeWordDetection} when a keyword is detected, or `null`.
88
+ */
89
+ detect(frame: Int16Array, _sampleRate: number): Promise<WakeWordDetection | null>;
90
+ /**
91
+ * No-op reset.
92
+ *
93
+ * Porcupine processes frames statelessly; there is no internal buffer to
94
+ * flush. This method exists for interface compatibility.
95
+ */
96
+ reset(): void;
97
+ /**
98
+ * Release the native Porcupine engine resources.
99
+ *
100
+ * Call this when the provider is no longer needed to free memory held by
101
+ * the native addon.
102
+ */
103
+ dispose(): Promise<void>;
104
+ /** Returns the configured keyword list. */
105
+ getKeywords(): string[];
106
+ /** Returns the configured sensitivity list. */
107
+ getSensitivities(): number[];
108
+ }
109
+ //# sourceMappingURL=PorcupineWakeWordProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PorcupineWakeWordProvider.d.ts","sourceRoot":"","sources":["../src/PorcupineWakeWordProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAMH;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,iEAAiE;IACjE,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,UAAU,EAAE,GAAG,CAAC;IAChB,kCAAkC;IAClC,UAAU,EAAE,WAAW,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,gCAAgC;IAC/C,8DAA8D;IAC9D,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAMD;;;;;GAKG;AACH,qBAAa,yBAAyB;IACpC,yEAAyE;IACzE,QAAQ,CAAC,EAAE,eAAe;IAE1B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAW;IACrC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAW;IAE1C,6CAA6C;IAE7C,OAAO,CAAC,UAAU,CAAoB;IAEtC;;;;OAIG;gBACS,OAAO,EAAE,gCAAgC;IAWrD;;;;OAIG;YAEW,aAAa;IAgB3B;;;;;;;;;;OAUG;IACG,MAAM,CAAC,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAgBvF;;;;;OAKG;IACH,KAAK,IAAI,IAAI;IAIb;;;;;OAKG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAW9B,2CAA2C;IAC3C,WAAW,IAAI,MAAM,EAAE;IAEvB,+CAA+C;IAC/C,gBAAgB,IAAI,MAAM,EAAE;CAC7B"}
@@ -0,0 +1,121 @@
1
+ /**
2
+ * @file PorcupineWakeWordProvider.ts
3
+ * @description Wake-word detection provider backed by Picovoice Porcupine.
4
+ *
5
+ * [Porcupine](https://picovoice.ai/platform/porcupine/) is an on-device
6
+ * wake-word engine. This provider wraps `@picovoice/porcupine-node` and
7
+ * exposes the `WakeWordProvider` contract expected by the AgentOS voice
8
+ * pipeline.
9
+ *
10
+ * The `@picovoice/porcupine-node` package is declared as a peer dependency so
11
+ * it is loaded only at runtime via dynamic `import()`.
12
+ *
13
+ * ### Threading model
14
+ * Porcupine's `process()` method is synchronous and stateless per-frame — each
15
+ * 512-sample frame is processed independently. The provider is therefore safe
16
+ * to call from any async context.
17
+ *
18
+ * @module porcupine
19
+ */
20
+ // ---------------------------------------------------------------------------
21
+ // Provider
22
+ // ---------------------------------------------------------------------------
23
+ /**
24
+ * Picovoice Porcupine wake-word provider.
25
+ *
26
+ * Implements the `WakeWordProvider` contract expected by the AgentOS voice
27
+ * pipeline without taking a hard runtime dependency on the interface types.
28
+ */
29
+ export class PorcupineWakeWordProvider {
30
+ /** Stable provider identifier used by the AgentOS extension registry. */
31
+ id = 'porcupine';
32
+ _accessKey;
33
+ _keywords;
34
+ _sensitivities;
35
+ /** Lazily initialised Porcupine instance. */
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ _porcupine = null;
38
+ /**
39
+ * Create a new {@link PorcupineWakeWordProvider}.
40
+ *
41
+ * @param options - Configuration including the required Picovoice access key.
42
+ */
43
+ constructor(options) {
44
+ this._accessKey = options.accessKey;
45
+ this._keywords = options.keywords ?? ['porcupine'];
46
+ this._sensitivities =
47
+ options.sensitivities ?? this._keywords.map(() => 0.5);
48
+ }
49
+ // ---------------------------------------------------------------------------
50
+ // Private helpers
51
+ // ---------------------------------------------------------------------------
52
+ /**
53
+ * Lazily initialise the Porcupine engine on first use.
54
+ *
55
+ * Dynamic import keeps the peer dep truly optional at module-load time.
56
+ */
57
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
+ async _getPorcupine() {
59
+ if (!this._porcupine) {
60
+ const { Porcupine } = await import('@picovoice/porcupine-node');
61
+ this._porcupine = new Porcupine(this._accessKey, this._keywords, this._sensitivities);
62
+ }
63
+ return this._porcupine;
64
+ }
65
+ // ---------------------------------------------------------------------------
66
+ // Public API
67
+ // ---------------------------------------------------------------------------
68
+ /**
69
+ * Process a single audio frame and detect any wake-word.
70
+ *
71
+ * Each frame must be exactly 512 samples of 16-bit PCM at 16 kHz (the
72
+ * Porcupine frame length). `sampleRate` is accepted for interface
73
+ * compatibility but Porcupine always operates at 16 kHz.
74
+ *
75
+ * @param frame - 16-bit PCM audio frame as an `Int16Array` (512 samples).
76
+ * @param sampleRate - Sample rate (informational; must be 16000 for Porcupine).
77
+ * @returns A {@link WakeWordDetection} when a keyword is detected, or `null`.
78
+ */
79
+ async detect(frame, _sampleRate) {
80
+ const porcupine = await this._getPorcupine();
81
+ const keywordIndex = porcupine.process(frame);
82
+ if (keywordIndex < 0) {
83
+ // No detection.
84
+ return null;
85
+ }
86
+ return {
87
+ keyword: this._keywords[keywordIndex] ?? String(keywordIndex),
88
+ confidence: 1.0,
89
+ providerId: 'porcupine',
90
+ };
91
+ }
92
+ /**
93
+ * No-op reset.
94
+ *
95
+ * Porcupine processes frames statelessly; there is no internal buffer to
96
+ * flush. This method exists for interface compatibility.
97
+ */
98
+ reset() {
99
+ // Intentional no-op — Porcupine is stateless per frame.
100
+ }
101
+ /**
102
+ * Release the native Porcupine engine resources.
103
+ *
104
+ * Call this when the provider is no longer needed to free memory held by
105
+ * the native addon.
106
+ */
107
+ async dispose() {
108
+ if (this._porcupine) {
109
+ this._porcupine.release();
110
+ this._porcupine = null;
111
+ }
112
+ }
113
+ // ---------------------------------------------------------------------------
114
+ // Accessors (for testing / diagnostics)
115
+ // ---------------------------------------------------------------------------
116
+ /** Returns the configured keyword list. */
117
+ getKeywords() { return [...this._keywords]; }
118
+ /** Returns the configured sensitivity list. */
119
+ getSensitivities() { return [...this._sensitivities]; }
120
+ }
121
+ //# sourceMappingURL=PorcupineWakeWordProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PorcupineWakeWordProvider.js","sourceRoot":"","sources":["../src/PorcupineWakeWordProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAwCH,8EAA8E;AAC9E,WAAW;AACX,8EAA8E;AAE9E;;;;;GAKG;AACH,MAAM,OAAO,yBAAyB;IACpC,yEAAyE;IAChE,EAAE,GAAG,WAAW,CAAC;IAET,UAAU,CAAS;IACnB,SAAS,CAAW;IACpB,cAAc,CAAW;IAE1C,6CAA6C;IAC7C,8DAA8D;IACtD,UAAU,GAAe,IAAI,CAAC;IAEtC;;;;OAIG;IACH,YAAY,OAAyC;QACnD,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC;QACpC,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,QAAQ,IAAI,CAAC,WAAW,CAAC,CAAC;QACnD,IAAI,CAAC,cAAc;YACjB,OAAO,CAAC,aAAa,IAAI,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC;IAC3D,CAAC;IAED,8EAA8E;IAC9E,kBAAkB;IAClB,8EAA8E;IAE9E;;;;OAIG;IACH,8DAA8D;IACtD,KAAK,CAAC,aAAa;QACzB,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YACrB,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,2BAA2B,CAAC,CAAC;YAChE,IAAI,CAAC,UAAU,GAAG,IAAI,SAAS,CAC7B,IAAI,CAAC,UAAU,EACf,IAAI,CAAC,SAAS,EACd,IAAI,CAAC,cAAc,CACpB,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IAED,8EAA8E;IAC9E,aAAa;IACb,8EAA8E;IAE9E;;;;;;;;;;OAUG;IACH,KAAK,CAAC,MAAM,CAAC,KAAiB,EAAE,WAAmB;QACjD,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC;QAC7C,MAAM,YAAY,GAAW,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAEtD,IAAI,YAAY,GAAG,CAAC,EAAE,CAAC;YACrB,gBAAgB;YAChB,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO;YACL,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,IAAI,MAAM,CAAC,YAAY,CAAC;YAC7D,UAAU,EAAE,GAAG;YACf,UAAU,EAAE,WAAW;SACxB,CAAC;IACJ,CAAC;IAED;;;;;OAKG;IACH,KAAK;QACH,wDAAwD;IAC1D,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,OAAO;QACX,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;YAC1B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACzB,CAAC;IACH,CAAC;IAED,8EAA8E;IAC9E,wCAAwC;IACxC,8EAA8E;IAE9E,2CAA2C;IAC3C,WAAW,KAAe,OAAO,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IAEvD,+CAA+C;IAC/C,gBAAgB,KAAe,OAAO,CAAC,GAAG,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;CAClE"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @file index.ts
3
+ * @description Pack factory for the Porcupine wake-word extension pack.
4
+ *
5
+ * Exports the main {@link createPorcupine} 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 { createPorcupine } from '@framers/agentos-ext-porcupine';
12
+ *
13
+ * const wakeWord = createPorcupine({ accessKey: 'YOUR_KEY', keywords: ['porcupine'] });
14
+ * const detection = await wakeWord.detect(frame, 16000);
15
+ * ```
16
+ *
17
+ * ### Usage (manifest-driven)
18
+ * ```json
19
+ * { "packs": [{ "module": "@framers/agentos-ext-porcupine" }] }
20
+ * ```
21
+ *
22
+ * @module porcupine
23
+ */
24
+ import { PorcupineWakeWordProvider } from './PorcupineWakeWordProvider.js';
25
+ import type { PorcupineWakeWordProviderOptions } from './PorcupineWakeWordProvider.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 PorcupineWakeWordProvider} instance.
46
+ *
47
+ * @param options - Constructor options (access key required).
48
+ * @returns Configured {@link PorcupineWakeWordProvider}.
49
+ */
50
+ export declare function createPorcupine(options: PorcupineWakeWordProviderOptions): PorcupineWakeWordProvider;
51
+ /**
52
+ * AgentOS manifest factory function.
53
+ *
54
+ * Reads the `PICOVOICE_ACCESS_KEY` secret and optional keyword configuration
55
+ * from the context, and returns an {@link ExtensionPack} containing a single
56
+ * `wake-word-provider` descriptor.
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 { PorcupineWakeWordProvider } from './PorcupineWakeWordProvider.js';
63
+ export type { WakeWordDetection, PorcupineWakeWordProviderOptions, } from './PorcupineWakeWordProvider.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,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAC3E,OAAO,KAAK,EAAE,gCAAgC,EAAE,MAAM,gCAAgC,CAAC;AAMvF,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,eAAe,CAC7B,OAAO,EAAE,gCAAgC,GACxC,yBAAyB,CAE3B;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CAoBhF;AAMD,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAC3E,YAAY,EACV,iBAAiB,EACjB,gCAAgC,GACjC,MAAM,gCAAgC,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * @file index.ts
3
+ * @description Pack factory for the Porcupine wake-word extension pack.
4
+ *
5
+ * Exports the main {@link createPorcupine} 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 { createPorcupine } from '@framers/agentos-ext-porcupine';
12
+ *
13
+ * const wakeWord = createPorcupine({ accessKey: 'YOUR_KEY', keywords: ['porcupine'] });
14
+ * const detection = await wakeWord.detect(frame, 16000);
15
+ * ```
16
+ *
17
+ * ### Usage (manifest-driven)
18
+ * ```json
19
+ * { "packs": [{ "module": "@framers/agentos-ext-porcupine" }] }
20
+ * ```
21
+ *
22
+ * @module porcupine
23
+ */
24
+ import { PorcupineWakeWordProvider } from './PorcupineWakeWordProvider.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 PorcupineWakeWordProvider} instance.
32
+ *
33
+ * @param options - Constructor options (access key required).
34
+ * @returns Configured {@link PorcupineWakeWordProvider}.
35
+ */
36
+ export function createPorcupine(options) {
37
+ return new PorcupineWakeWordProvider(options);
38
+ }
39
+ /**
40
+ * AgentOS manifest factory function.
41
+ *
42
+ * Reads the `PICOVOICE_ACCESS_KEY` secret and optional keyword configuration
43
+ * from the context, and returns an {@link ExtensionPack} containing a single
44
+ * `wake-word-provider` descriptor.
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 accessKey = context.getSecret?.('PICOVOICE_ACCESS_KEY') ?? '';
51
+ const opts = context.options ?? {};
52
+ const keywords = opts['keywords'];
53
+ const sensitivities = opts['sensitivities'];
54
+ const provider = new PorcupineWakeWordProvider({ accessKey, keywords, sensitivities });
55
+ return {
56
+ id: 'porcupine',
57
+ descriptors: [
58
+ {
59
+ id: 'porcupine',
60
+ kind: EXTENSION_KIND_WAKE_WORD,
61
+ payload: provider,
62
+ enableByDefault: true,
63
+ metadata: { providerId: 'porcupine' },
64
+ },
65
+ ],
66
+ };
67
+ }
68
+ // ---------------------------------------------------------------------------
69
+ // Re-exports
70
+ // ---------------------------------------------------------------------------
71
+ export { PorcupineWakeWordProvider } from './PorcupineWakeWordProvider.js';
72
+ //# 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,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AA4B3E,uEAAuE;AACvE,MAAM,wBAAwB,GAAG,oBAAoB,CAAC;AAEtD,8EAA8E;AAC9E,YAAY;AACZ,8EAA8E;AAE9E;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAC7B,OAAyC;IAEzC,OAAO,IAAI,yBAAyB,CAAC,OAAO,CAAC,CAAC;AAChD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAA6B;IAC/D,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,sBAAsB,CAAC,IAAI,EAAE,CAAC;IACpE,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;IACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAyB,CAAC;IAC1D,MAAM,aAAa,GAAG,IAAI,CAAC,eAAe,CAAyB,CAAC;IAEpE,MAAM,QAAQ,GAAG,IAAI,yBAAyB,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,EAAE,CAAC,CAAC;IAEvF,OAAO;QACL,EAAE,EAAE,WAAW;QACf,WAAW,EAAE;YACX;gBACE,EAAE,EAAE,WAAW;gBACf,IAAI,EAAE,wBAAwB;gBAC9B,OAAO,EAAE,QAAQ;gBACjB,eAAe,EAAE,IAAI;gBACrB,QAAQ,EAAE,EAAE,UAAU,EAAE,WAAW,EAAE;aACtC;SACF;KACF,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC"}
package/manifest.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "@framers/agentos-ext-porcupine",
3
+ "version": "0.1.0",
4
+ "description": "Wake-word detection via Picovoice Porcupine",
5
+ "kind": "wake-word-provider",
6
+ "extensionId": "porcupine",
7
+ "entryPoint": "./dist/index.js"
8
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@framers/agentos-ext-porcupine",
3
+ "version": "0.2.0",
4
+ "description": "Wake-word detection via Picovoice Porcupine 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
+ "@picovoice/porcupine-node": "^3.0.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/porcupine"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "scripts": {
40
+ "build": "tsc -p tsconfig.json",
41
+ "test": "vitest run"
42
+ }
43
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * @file PorcupineWakeWordProvider.ts
3
+ * @description Wake-word detection provider backed by Picovoice Porcupine.
4
+ *
5
+ * [Porcupine](https://picovoice.ai/platform/porcupine/) is an on-device
6
+ * wake-word engine. This provider wraps `@picovoice/porcupine-node` and
7
+ * exposes the `WakeWordProvider` contract expected by the AgentOS voice
8
+ * pipeline.
9
+ *
10
+ * The `@picovoice/porcupine-node` package is declared as a peer dependency so
11
+ * it is loaded only at runtime via dynamic `import()`.
12
+ *
13
+ * ### Threading model
14
+ * Porcupine's `process()` method is synchronous and stateless per-frame — each
15
+ * 512-sample frame is processed independently. The provider is therefore safe
16
+ * to call from any async context.
17
+ *
18
+ * @module porcupine
19
+ */
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Public types
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /**
26
+ * A detected wake-word event returned by {@link PorcupineWakeWordProvider.detect}.
27
+ */
28
+ export interface WakeWordDetection {
29
+ /** The keyword string that was detected (e.g. `'porcupine'`). */
30
+ keyword: string;
31
+ /**
32
+ * Confidence score. Porcupine does not expose a per-detection confidence;
33
+ * this is always `1.0` to signal a positive detection.
34
+ */
35
+ confidence: 1.0;
36
+ /** Stable provider identifier. */
37
+ providerId: 'porcupine';
38
+ }
39
+
40
+ /**
41
+ * Constructor options for {@link PorcupineWakeWordProvider}.
42
+ */
43
+ export interface PorcupineWakeWordProviderOptions {
44
+ /** Picovoice access key from https://console.picovoice.ai/ */
45
+ accessKey: string;
46
+ /**
47
+ * Built-in keyword names to detect (e.g. `['porcupine', 'bumblebee']`).
48
+ * @defaultValue `['porcupine']`
49
+ */
50
+ keywords?: string[];
51
+ /**
52
+ * Detection sensitivity in [0, 1] for each keyword.
53
+ * Must be the same length as `keywords` when provided.
54
+ * @defaultValue `[0.5]` (or `0.5` per keyword)
55
+ */
56
+ sensitivities?: number[];
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Provider
61
+ // ---------------------------------------------------------------------------
62
+
63
+ /**
64
+ * Picovoice Porcupine wake-word provider.
65
+ *
66
+ * Implements the `WakeWordProvider` contract expected by the AgentOS voice
67
+ * pipeline without taking a hard runtime dependency on the interface types.
68
+ */
69
+ export class PorcupineWakeWordProvider {
70
+ /** Stable provider identifier used by the AgentOS extension registry. */
71
+ readonly id = 'porcupine';
72
+
73
+ private readonly _accessKey: string;
74
+ private readonly _keywords: string[];
75
+ private readonly _sensitivities: number[];
76
+
77
+ /** Lazily initialised Porcupine instance. */
78
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
+ private _porcupine: any | null = null;
80
+
81
+ /**
82
+ * Create a new {@link PorcupineWakeWordProvider}.
83
+ *
84
+ * @param options - Configuration including the required Picovoice access key.
85
+ */
86
+ constructor(options: PorcupineWakeWordProviderOptions) {
87
+ this._accessKey = options.accessKey;
88
+ this._keywords = options.keywords ?? ['porcupine'];
89
+ this._sensitivities =
90
+ options.sensitivities ?? this._keywords.map(() => 0.5);
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Private helpers
95
+ // ---------------------------------------------------------------------------
96
+
97
+ /**
98
+ * Lazily initialise the Porcupine engine on first use.
99
+ *
100
+ * Dynamic import keeps the peer dep truly optional at module-load time.
101
+ */
102
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
+ private async _getPorcupine(): Promise<any> {
104
+ if (!this._porcupine) {
105
+ const { Porcupine } = await import('@picovoice/porcupine-node');
106
+ this._porcupine = new Porcupine(
107
+ this._accessKey,
108
+ this._keywords,
109
+ this._sensitivities,
110
+ );
111
+ }
112
+ return this._porcupine;
113
+ }
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // Public API
117
+ // ---------------------------------------------------------------------------
118
+
119
+ /**
120
+ * Process a single audio frame and detect any wake-word.
121
+ *
122
+ * Each frame must be exactly 512 samples of 16-bit PCM at 16 kHz (the
123
+ * Porcupine frame length). `sampleRate` is accepted for interface
124
+ * compatibility but Porcupine always operates at 16 kHz.
125
+ *
126
+ * @param frame - 16-bit PCM audio frame as an `Int16Array` (512 samples).
127
+ * @param sampleRate - Sample rate (informational; must be 16000 for Porcupine).
128
+ * @returns A {@link WakeWordDetection} when a keyword is detected, or `null`.
129
+ */
130
+ async detect(frame: Int16Array, _sampleRate: number): Promise<WakeWordDetection | null> {
131
+ const porcupine = await this._getPorcupine();
132
+ const keywordIndex: number = porcupine.process(frame);
133
+
134
+ if (keywordIndex < 0) {
135
+ // No detection.
136
+ return null;
137
+ }
138
+
139
+ return {
140
+ keyword: this._keywords[keywordIndex] ?? String(keywordIndex),
141
+ confidence: 1.0,
142
+ providerId: 'porcupine',
143
+ };
144
+ }
145
+
146
+ /**
147
+ * No-op reset.
148
+ *
149
+ * Porcupine processes frames statelessly; there is no internal buffer to
150
+ * flush. This method exists for interface compatibility.
151
+ */
152
+ reset(): void {
153
+ // Intentional no-op — Porcupine is stateless per frame.
154
+ }
155
+
156
+ /**
157
+ * Release the native Porcupine engine resources.
158
+ *
159
+ * Call this when the provider is no longer needed to free memory held by
160
+ * the native addon.
161
+ */
162
+ async dispose(): Promise<void> {
163
+ if (this._porcupine) {
164
+ this._porcupine.release();
165
+ this._porcupine = null;
166
+ }
167
+ }
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // Accessors (for testing / diagnostics)
171
+ // ---------------------------------------------------------------------------
172
+
173
+ /** Returns the configured keyword list. */
174
+ getKeywords(): string[] { return [...this._keywords]; }
175
+
176
+ /** Returns the configured sensitivity list. */
177
+ getSensitivities(): number[] { return [...this._sensitivities]; }
178
+ }
package/src/index.ts ADDED
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @file index.ts
3
+ * @description Pack factory for the Porcupine wake-word extension pack.
4
+ *
5
+ * Exports the main {@link createPorcupine} 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 { createPorcupine } from '@framers/agentos-ext-porcupine';
12
+ *
13
+ * const wakeWord = createPorcupine({ accessKey: 'YOUR_KEY', keywords: ['porcupine'] });
14
+ * const detection = await wakeWord.detect(frame, 16000);
15
+ * ```
16
+ *
17
+ * ### Usage (manifest-driven)
18
+ * ```json
19
+ * { "packs": [{ "module": "@framers/agentos-ext-porcupine" }] }
20
+ * ```
21
+ *
22
+ * @module porcupine
23
+ */
24
+
25
+ import { PorcupineWakeWordProvider } from './PorcupineWakeWordProvider.js';
26
+ import type { PorcupineWakeWordProviderOptions } from './PorcupineWakeWordProvider.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 PorcupineWakeWordProvider} instance.
62
+ *
63
+ * @param options - Constructor options (access key required).
64
+ * @returns Configured {@link PorcupineWakeWordProvider}.
65
+ */
66
+ export function createPorcupine(
67
+ options: PorcupineWakeWordProviderOptions,
68
+ ): PorcupineWakeWordProvider {
69
+ return new PorcupineWakeWordProvider(options);
70
+ }
71
+
72
+ /**
73
+ * AgentOS manifest factory function.
74
+ *
75
+ * Reads the `PICOVOICE_ACCESS_KEY` secret and optional keyword configuration
76
+ * from the context, and returns an {@link ExtensionPack} containing a single
77
+ * `wake-word-provider` descriptor.
78
+ *
79
+ * @param context - Pack context supplied by the extension manager.
80
+ * @returns A fully configured {@link ExtensionPack}.
81
+ */
82
+ export function createExtensionPack(context: ExtensionPackContext): ExtensionPack {
83
+ const accessKey = context.getSecret?.('PICOVOICE_ACCESS_KEY') ?? '';
84
+ const opts = context.options ?? {};
85
+ const keywords = opts['keywords'] as string[] | undefined;
86
+ const sensitivities = opts['sensitivities'] as number[] | undefined;
87
+
88
+ const provider = new PorcupineWakeWordProvider({ accessKey, keywords, sensitivities });
89
+
90
+ return {
91
+ id: 'porcupine',
92
+ descriptors: [
93
+ {
94
+ id: 'porcupine',
95
+ kind: EXTENSION_KIND_WAKE_WORD,
96
+ payload: provider,
97
+ enableByDefault: true,
98
+ metadata: { providerId: 'porcupine' },
99
+ },
100
+ ],
101
+ };
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Re-exports
106
+ // ---------------------------------------------------------------------------
107
+
108
+ export { PorcupineWakeWordProvider } from './PorcupineWakeWordProvider.js';
109
+ export type {
110
+ WakeWordDetection,
111
+ PorcupineWakeWordProviderOptions,
112
+ } from './PorcupineWakeWordProvider.js';