@framers/agentos-ext-piper 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,18 @@
1
+ # piper — Offline TTS Extension Pack
2
+
3
+ Provides offline text-to-speech by spawning the [Piper](https://github.com/rhasspy/piper) binary.
4
+
5
+ ## Configuration
6
+
7
+ | Option | Default | Description |
8
+ |--------|---------|-------------|
9
+ | `binaryPath` | `PIPER_BIN` env or `piper` | Path to the Piper executable |
10
+ | `modelPath` | `PIPER_MODEL_PATH` env or `~/.agentos/models/piper/en_US-lessac-medium.onnx` | ONNX model file |
11
+ | `maxBufferBytes` | `10485760` (10 MB) | Maximum WAV output size |
12
+ | `timeoutMs` | `30000` | Process timeout in milliseconds |
13
+
14
+ ## Features
15
+ - Fully offline — no network required after model download
16
+ - Zero npm dependencies (uses `child_process.spawn`)
17
+ - Returns WAV audio (`audio/wav`)
18
+ - Cost always 0
@@ -0,0 +1,105 @@
1
+ /**
2
+ * @file PiperTTSProvider.ts
3
+ * @description Offline text-to-speech provider that invokes the Piper binary.
4
+ *
5
+ * [Piper](https://github.com/rhasspy/piper) is a fast, local neural TTS
6
+ * system. This provider spawns the binary via `child_process.spawn`, writes
7
+ * the input text to `stdin`, and collects the WAV audio written to `stdout`.
8
+ *
9
+ * No npm dependencies are required beyond Node's built-in `child_process`
10
+ * module. The Piper binary must be installed separately and be accessible
11
+ * via the `binaryPath` option or the `PIPER_BIN` environment variable
12
+ * (defaults to `piper`, which expects the binary to be on `PATH`).
13
+ *
14
+ * @module piper
15
+ */
16
+ /**
17
+ * Constructor options for {@link PiperTTSProvider}.
18
+ */
19
+ export interface PiperTTSProviderOptions {
20
+ /**
21
+ * Absolute path to the Piper executable, or just `'piper'` if it is on
22
+ * `PATH`. Resolved from `PIPER_BIN` env var when omitted.
23
+ * @defaultValue `process.env.PIPER_BIN ?? 'piper'`
24
+ */
25
+ binaryPath?: string;
26
+ /**
27
+ * Absolute path to the ONNX voice model file.
28
+ * Resolved from `PIPER_MODEL_PATH` env var when omitted.
29
+ * @defaultValue `~/.agentos/models/piper/en_US-lessac-medium.onnx`
30
+ */
31
+ modelPath?: string;
32
+ /**
33
+ * Maximum number of bytes accepted from Piper's stdout.
34
+ * Prevents runaway memory usage if the binary misbehaves.
35
+ * @defaultValue `10485760` (10 MB)
36
+ */
37
+ maxBufferBytes?: number;
38
+ /**
39
+ * Milliseconds before the spawned process is killed with SIGTERM and the
40
+ * promise is rejected.
41
+ * @defaultValue `30000`
42
+ */
43
+ timeoutMs?: number;
44
+ }
45
+ /**
46
+ * Synthesised audio returned by {@link PiperTTSProvider.synthesize}.
47
+ */
48
+ export interface SynthesisResult {
49
+ /** Raw WAV audio bytes produced by Piper. */
50
+ audioBuffer: Buffer;
51
+ /** MIME type of the audio data. Always `'audio/wav'` for this provider. */
52
+ mimeType: 'audio/wav';
53
+ /** Billable cost (always 0 — fully local). */
54
+ cost: 0;
55
+ }
56
+ /**
57
+ * Piper offline text-to-speech provider.
58
+ *
59
+ * Implements the `TextToSpeechProvider` contract expected by the AgentOS voice
60
+ * pipeline without taking a hard runtime dependency on the interface types.
61
+ */
62
+ export declare class PiperTTSProvider {
63
+ /** Stable provider identifier used by the AgentOS extension registry. */
64
+ readonly id = "piper";
65
+ /**
66
+ * Streaming is not supported — Piper outputs a complete WAV file.
67
+ * Setting this to `false` signals the pipeline to use batch mode.
68
+ */
69
+ readonly supportsStreaming = false;
70
+ private readonly _binaryPath;
71
+ private readonly _modelPath;
72
+ private readonly _maxBufferBytes;
73
+ private readonly _timeoutMs;
74
+ /**
75
+ * Create a new {@link PiperTTSProvider}.
76
+ *
77
+ * @param options - Optional configuration. All fields have sensible defaults
78
+ * or fall back to environment variables.
79
+ */
80
+ constructor(options?: PiperTTSProviderOptions);
81
+ /**
82
+ * Synthesise plain text to WAV audio using the local Piper binary.
83
+ *
84
+ * Piper is invoked as:
85
+ * ```
86
+ * piper --model <modelPath> --output_file -
87
+ * ```
88
+ * The text is written to `stdin`; WAV bytes are read from `stdout`.
89
+ *
90
+ * @param text - Plain text to synthesise.
91
+ * @returns {@link SynthesisResult} containing the raw WAV buffer.
92
+ * @throws If Piper exits with a non-zero code, times out, or the buffer
93
+ * limit is exceeded.
94
+ */
95
+ synthesize(text: string): Promise<SynthesisResult>;
96
+ /** Returns the resolved binary path. */
97
+ getBinaryPath(): string;
98
+ /** Returns the resolved model path. */
99
+ getModelPath(): string;
100
+ /** Returns the configured max buffer size in bytes. */
101
+ getMaxBufferBytes(): number;
102
+ /** Returns the configured timeout in milliseconds. */
103
+ getTimeoutMs(): number;
104
+ }
105
+ //# sourceMappingURL=PiperTTSProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PiperTTSProvider.d.ts","sourceRoot":"","sources":["../src/PiperTTSProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAUH;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,6CAA6C;IAC7C,WAAW,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,QAAQ,EAAE,WAAW,CAAC;IACtB,8CAA8C;IAC9C,IAAI,EAAE,CAAC,CAAC;CACT;AAMD;;;;;GAKG;AACH,qBAAa,gBAAgB;IAC3B,yEAAyE;IACzE,QAAQ,CAAC,EAAE,WAAW;IAEtB;;;OAGG;IACH,QAAQ,CAAC,iBAAiB,SAAS;IAEnC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IAEpC;;;;;OAKG;gBACS,OAAO,GAAE,uBAA4B;IAcjD;;;;;;;;;;;;;OAaG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IA0ExD,wCAAwC;IACxC,aAAa,IAAI,MAAM;IAEvB,uCAAuC;IACvC,YAAY,IAAI,MAAM;IAEtB,uDAAuD;IACvD,iBAAiB,IAAI,MAAM;IAE3B,sDAAsD;IACtD,YAAY,IAAI,MAAM;CACvB"}
@@ -0,0 +1,143 @@
1
+ /**
2
+ * @file PiperTTSProvider.ts
3
+ * @description Offline text-to-speech provider that invokes the Piper binary.
4
+ *
5
+ * [Piper](https://github.com/rhasspy/piper) is a fast, local neural TTS
6
+ * system. This provider spawns the binary via `child_process.spawn`, writes
7
+ * the input text to `stdin`, and collects the WAV audio written to `stdout`.
8
+ *
9
+ * No npm dependencies are required beyond Node's built-in `child_process`
10
+ * module. The Piper binary must be installed separately and be accessible
11
+ * via the `binaryPath` option or the `PIPER_BIN` environment variable
12
+ * (defaults to `piper`, which expects the binary to be on `PATH`).
13
+ *
14
+ * @module piper
15
+ */
16
+ import { spawn } from 'child_process';
17
+ import os from 'os';
18
+ import path from 'path';
19
+ // ---------------------------------------------------------------------------
20
+ // Provider
21
+ // ---------------------------------------------------------------------------
22
+ /**
23
+ * Piper offline text-to-speech provider.
24
+ *
25
+ * Implements the `TextToSpeechProvider` contract expected by the AgentOS voice
26
+ * pipeline without taking a hard runtime dependency on the interface types.
27
+ */
28
+ export class PiperTTSProvider {
29
+ /** Stable provider identifier used by the AgentOS extension registry. */
30
+ id = 'piper';
31
+ /**
32
+ * Streaming is not supported — Piper outputs a complete WAV file.
33
+ * Setting this to `false` signals the pipeline to use batch mode.
34
+ */
35
+ supportsStreaming = false;
36
+ _binaryPath;
37
+ _modelPath;
38
+ _maxBufferBytes;
39
+ _timeoutMs;
40
+ /**
41
+ * Create a new {@link PiperTTSProvider}.
42
+ *
43
+ * @param options - Optional configuration. All fields have sensible defaults
44
+ * or fall back to environment variables.
45
+ */
46
+ constructor(options = {}) {
47
+ this._binaryPath = options.binaryPath ?? process.env['PIPER_BIN'] ?? 'piper';
48
+ this._modelPath =
49
+ options.modelPath ??
50
+ process.env['PIPER_MODEL_PATH'] ??
51
+ path.join(os.homedir(), '.agentos', 'models', 'piper', 'en_US-lessac-medium.onnx');
52
+ this._maxBufferBytes = options.maxBufferBytes ?? 10 * 1024 * 1024; // 10 MB
53
+ this._timeoutMs = options.timeoutMs ?? 30_000;
54
+ }
55
+ // ---------------------------------------------------------------------------
56
+ // Public API
57
+ // ---------------------------------------------------------------------------
58
+ /**
59
+ * Synthesise plain text to WAV audio using the local Piper binary.
60
+ *
61
+ * Piper is invoked as:
62
+ * ```
63
+ * piper --model <modelPath> --output_file -
64
+ * ```
65
+ * The text is written to `stdin`; WAV bytes are read from `stdout`.
66
+ *
67
+ * @param text - Plain text to synthesise.
68
+ * @returns {@link SynthesisResult} containing the raw WAV buffer.
69
+ * @throws If Piper exits with a non-zero code, times out, or the buffer
70
+ * limit is exceeded.
71
+ */
72
+ async synthesize(text) {
73
+ return new Promise((resolve, reject) => {
74
+ const proc = spawn(this._binaryPath, [
75
+ '--model', this._modelPath,
76
+ '--output_file', '-',
77
+ ]);
78
+ const chunks = [];
79
+ let totalBytes = 0;
80
+ let settled = false;
81
+ /** Settle the promise once, ignoring subsequent events. */
82
+ const settle = (err, result) => {
83
+ if (settled)
84
+ return;
85
+ settled = true;
86
+ clearTimeout(timer);
87
+ if (err) {
88
+ proc.kill('SIGTERM');
89
+ reject(err);
90
+ }
91
+ else {
92
+ resolve(result);
93
+ }
94
+ };
95
+ // Timeout guard.
96
+ const timer = setTimeout(() => {
97
+ settle(new Error(`Piper process timed out after ${this._timeoutMs} ms`));
98
+ }, this._timeoutMs);
99
+ proc.stdout.on('data', (chunk) => {
100
+ totalBytes += chunk.length;
101
+ if (totalBytes > this._maxBufferBytes) {
102
+ settle(new Error(`Piper output exceeded maxBufferBytes limit (${this._maxBufferBytes} bytes)`));
103
+ return;
104
+ }
105
+ chunks.push(chunk);
106
+ });
107
+ proc.stderr.on('data', (chunk) => {
108
+ // Piper writes progress/info to stderr; capture for error reporting.
109
+ // We do NOT reject on stderr data alone — only on non-zero exit.
110
+ void chunk; // intentionally unused unless exit code is non-zero
111
+ });
112
+ proc.on('error', (err) => {
113
+ settle(new Error(`Failed to spawn Piper binary "${this._binaryPath}": ${err.message}`));
114
+ });
115
+ proc.on('close', (code) => {
116
+ if (code !== 0 && code !== null) {
117
+ settle(new Error(`Piper exited with non-zero code ${code}`));
118
+ return;
119
+ }
120
+ settle(null, {
121
+ audioBuffer: Buffer.concat(chunks),
122
+ mimeType: 'audio/wav',
123
+ cost: 0,
124
+ });
125
+ });
126
+ // Write text to stdin and close the stream so Piper knows input is done.
127
+ proc.stdin.write(text);
128
+ proc.stdin.end();
129
+ });
130
+ }
131
+ // ---------------------------------------------------------------------------
132
+ // Accessors (for testing / diagnostics)
133
+ // ---------------------------------------------------------------------------
134
+ /** Returns the resolved binary path. */
135
+ getBinaryPath() { return this._binaryPath; }
136
+ /** Returns the resolved model path. */
137
+ getModelPath() { return this._modelPath; }
138
+ /** Returns the configured max buffer size in bytes. */
139
+ getMaxBufferBytes() { return this._maxBufferBytes; }
140
+ /** Returns the configured timeout in milliseconds. */
141
+ getTimeoutMs() { return this._timeoutMs; }
142
+ }
143
+ //# sourceMappingURL=PiperTTSProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PiperTTSProvider.js","sourceRoot":"","sources":["../src/PiperTTSProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AACtC,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AAmDxB,8EAA8E;AAC9E,WAAW;AACX,8EAA8E;AAE9E;;;;;GAKG;AACH,MAAM,OAAO,gBAAgB;IAC3B,yEAAyE;IAChE,EAAE,GAAG,OAAO,CAAC;IAEtB;;;OAGG;IACM,iBAAiB,GAAG,KAAK,CAAC;IAElB,WAAW,CAAS;IACpB,UAAU,CAAS;IACnB,eAAe,CAAS;IACxB,UAAU,CAAS;IAEpC;;;;;OAKG;IACH,YAAY,UAAmC,EAAE;QAC/C,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,OAAO,CAAC;QAC7E,IAAI,CAAC,UAAU;YACb,OAAO,CAAC,SAAS;gBACjB,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;gBAC/B,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,0BAA0B,CAAC,CAAC;QACrF,IAAI,CAAC,eAAe,GAAG,OAAO,CAAC,cAAc,IAAI,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,QAAQ;QAC3E,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,SAAS,IAAI,MAAM,CAAC;IAChD,CAAC;IAED,8EAA8E;IAC9E,aAAa;IACb,8EAA8E;IAE9E;;;;;;;;;;;;;OAaG;IACH,KAAK,CAAC,UAAU,CAAC,IAAY;QAC3B,OAAO,IAAI,OAAO,CAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACtD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE;gBACnC,SAAS,EAAE,IAAI,CAAC,UAAU;gBAC1B,eAAe,EAAE,GAAG;aACrB,CAAC,CAAC;YAEH,MAAM,MAAM,GAAa,EAAE,CAAC;YAC5B,IAAI,UAAU,GAAG,CAAC,CAAC;YACnB,IAAI,OAAO,GAAG,KAAK,CAAC;YAEpB,2DAA2D;YAC3D,MAAM,MAAM,GAAG,CAAC,GAAiB,EAAE,MAAwB,EAAE,EAAE;gBAC7D,IAAI,OAAO;oBAAE,OAAO;gBACpB,OAAO,GAAG,IAAI,CAAC;gBACf,YAAY,CAAC,KAAK,CAAC,CAAC;gBACpB,IAAI,GAAG,EAAE,CAAC;oBACR,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;oBACrB,MAAM,CAAC,GAAG,CAAC,CAAC;gBACd,CAAC;qBAAM,CAAC;oBACN,OAAO,CAAC,MAAO,CAAC,CAAC;gBACnB,CAAC;YACH,CAAC,CAAC;YAEF,iBAAiB;YACjB,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;gBAC5B,MAAM,CAAC,IAAI,KAAK,CAAC,iCAAiC,IAAI,CAAC,UAAU,KAAK,CAAC,CAAC,CAAC;YAC3E,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;YAEpB,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;gBACvC,UAAU,IAAI,KAAK,CAAC,MAAM,CAAC;gBAC3B,IAAI,UAAU,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;oBACtC,MAAM,CACJ,IAAI,KAAK,CACP,+CAA+C,IAAI,CAAC,eAAe,SAAS,CAC7E,CACF,CAAC;oBACF,OAAO;gBACT,CAAC;gBACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACrB,CAAC,CAAC,CAAC;YAEH,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;gBACvC,qEAAqE;gBACrE,iEAAiE;gBACjE,KAAK,KAAK,CAAC,CAAC,oDAAoD;YAClE,CAAC,CAAC,CAAC;YAEH,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAU,EAAE,EAAE;gBAC9B,MAAM,CAAC,IAAI,KAAK,CAAC,iCAAiC,IAAI,CAAC,WAAW,MAAM,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YAC1F,CAAC,CAAC,CAAC;YAEH,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAmB,EAAE,EAAE;gBACvC,IAAI,IAAI,KAAK,CAAC,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;oBAChC,MAAM,CAAC,IAAI,KAAK,CAAC,mCAAmC,IAAI,EAAE,CAAC,CAAC,CAAC;oBAC7D,OAAO;gBACT,CAAC;gBACD,MAAM,CAAC,IAAI,EAAE;oBACX,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC;oBAClC,QAAQ,EAAE,WAAW;oBACrB,IAAI,EAAE,CAAC;iBACR,CAAC,CAAC;YACL,CAAC,CAAC,CAAC;YAEH,yEAAyE;YACzE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACvB,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;QACnB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,8EAA8E;IAC9E,wCAAwC;IACxC,8EAA8E;IAE9E,wCAAwC;IACxC,aAAa,KAAa,OAAO,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;IAEpD,uCAAuC;IACvC,YAAY,KAAa,OAAO,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;IAElD,uDAAuD;IACvD,iBAAiB,KAAa,OAAO,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC;IAE5D,sDAAsD;IACtD,YAAY,KAAa,OAAO,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;CACnD"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @file index.ts
3
+ * @description Pack factory for the Piper TTS extension pack.
4
+ *
5
+ * Exports the main {@link createPiperTTS} 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 { createPiperTTS } from '@framers/agentos-ext-piper';
12
+ *
13
+ * const tts = createPiperTTS({ modelPath: '/opt/piper/en_US-lessac-medium.onnx' });
14
+ * const { audioBuffer } = await tts.synthesize('Hello world');
15
+ * ```
16
+ *
17
+ * ### Usage (manifest-driven)
18
+ * ```json
19
+ * { "packs": [{ "module": "@framers/agentos-ext-piper" }] }
20
+ * ```
21
+ *
22
+ * @module piper
23
+ */
24
+ import { PiperTTSProvider } from './PiperTTSProvider.js';
25
+ import type { PiperTTSProviderOptions } from './PiperTTSProvider.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 PiperTTSProvider} instance.
46
+ *
47
+ * @param options - Optional constructor options.
48
+ * @returns Configured {@link PiperTTSProvider}.
49
+ */
50
+ export declare function createPiperTTS(options?: PiperTTSProviderOptions): PiperTTSProvider;
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 `tts-provider` descriptor backed
56
+ * by {@link PiperTTSProvider}.
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 { PiperTTSProvider } from './PiperTTSProvider.js';
63
+ export type { SynthesisResult, PiperTTSProviderOptions } from './PiperTTSProvider.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,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAMrE,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,cAAc,CAAC,OAAO,CAAC,EAAE,uBAAuB,GAAG,gBAAgB,CAElF;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CAqBhF;AAMD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,YAAY,EAAE,eAAe,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @file index.ts
3
+ * @description Pack factory for the Piper TTS extension pack.
4
+ *
5
+ * Exports the main {@link createPiperTTS} 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 { createPiperTTS } from '@framers/agentos-ext-piper';
12
+ *
13
+ * const tts = createPiperTTS({ modelPath: '/opt/piper/en_US-lessac-medium.onnx' });
14
+ * const { audioBuffer } = await tts.synthesize('Hello world');
15
+ * ```
16
+ *
17
+ * ### Usage (manifest-driven)
18
+ * ```json
19
+ * { "packs": [{ "module": "@framers/agentos-ext-piper" }] }
20
+ * ```
21
+ *
22
+ * @module piper
23
+ */
24
+ import { PiperTTSProvider } from './PiperTTSProvider.js';
25
+ /** Kind constant matching packages/agentos/src/extensions/types.ts. */
26
+ const EXTENSION_KIND_TTS = 'tts-provider';
27
+ // ---------------------------------------------------------------------------
28
+ // Factories
29
+ // ---------------------------------------------------------------------------
30
+ /**
31
+ * Create a standalone {@link PiperTTSProvider} instance.
32
+ *
33
+ * @param options - Optional constructor options.
34
+ * @returns Configured {@link PiperTTSProvider}.
35
+ */
36
+ export function createPiperTTS(options) {
37
+ return new PiperTTSProvider(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 `tts-provider` descriptor backed
44
+ * by {@link PiperTTSProvider}.
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 PiperTTSProvider({
52
+ binaryPath: opts['binaryPath'],
53
+ modelPath: opts['modelPath'],
54
+ maxBufferBytes: opts['maxBufferBytes'],
55
+ timeoutMs: opts['timeoutMs'],
56
+ });
57
+ return {
58
+ id: 'piper',
59
+ descriptors: [
60
+ {
61
+ id: 'piper',
62
+ kind: EXTENSION_KIND_TTS,
63
+ payload: provider,
64
+ enableByDefault: true,
65
+ metadata: { providerId: 'piper' },
66
+ },
67
+ ],
68
+ };
69
+ }
70
+ // ---------------------------------------------------------------------------
71
+ // Re-exports
72
+ // ---------------------------------------------------------------------------
73
+ export { PiperTTSProvider } from './PiperTTSProvider.js';
74
+ //# 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,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AA4BzD,uEAAuE;AACvE,MAAM,kBAAkB,GAAG,cAAc,CAAC;AAE1C,8EAA8E;AAC9E,YAAY;AACZ,8EAA8E;AAE9E;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,OAAiC;IAC9D,OAAO,IAAI,gBAAgB,CAAC,OAAO,CAAC,CAAC;AACvC,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,gBAAgB,CAAC;QACpC,UAAU,EAAE,IAAI,CAAC,YAAY,CAAuB;QACpD,SAAS,EAAE,IAAI,CAAC,WAAW,CAAuB;QAClD,cAAc,EAAE,IAAI,CAAC,gBAAgB,CAAuB;QAC5D,SAAS,EAAE,IAAI,CAAC,WAAW,CAAuB;KACnD,CAAC,CAAC;IAEH,OAAO;QACL,EAAE,EAAE,OAAO;QACX,WAAW,EAAE;YACX;gBACE,EAAE,EAAE,OAAO;gBACX,IAAI,EAAE,kBAAkB;gBACxB,OAAO,EAAE,QAAQ;gBACjB,eAAe,EAAE,IAAI;gBACrB,QAAQ,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE;aAClC;SACF;KACF,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC"}
package/manifest.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "@framers/agentos-ext-piper",
3
+ "version": "0.1.0",
4
+ "description": "Offline text-to-speech via Piper binary",
5
+ "kind": "tts-provider",
6
+ "extensionId": "piper",
7
+ "entryPoint": "./dist/index.js"
8
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@framers/agentos-ext-piper",
3
+ "version": "0.2.0",
4
+ "description": "Offline text-to-speech via Piper binary 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
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^20.0.0",
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/piper"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "scripts": {
40
+ "build": "tsc -p tsconfig.json",
41
+ "test": "vitest run"
42
+ }
43
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * @file PiperTTSProvider.ts
3
+ * @description Offline text-to-speech provider that invokes the Piper binary.
4
+ *
5
+ * [Piper](https://github.com/rhasspy/piper) is a fast, local neural TTS
6
+ * system. This provider spawns the binary via `child_process.spawn`, writes
7
+ * the input text to `stdin`, and collects the WAV audio written to `stdout`.
8
+ *
9
+ * No npm dependencies are required beyond Node's built-in `child_process`
10
+ * module. The Piper binary must be installed separately and be accessible
11
+ * via the `binaryPath` option or the `PIPER_BIN` environment variable
12
+ * (defaults to `piper`, which expects the binary to be on `PATH`).
13
+ *
14
+ * @module piper
15
+ */
16
+
17
+ import { spawn } from 'child_process';
18
+ import os from 'os';
19
+ import path from 'path';
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Public types
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /**
26
+ * Constructor options for {@link PiperTTSProvider}.
27
+ */
28
+ export interface PiperTTSProviderOptions {
29
+ /**
30
+ * Absolute path to the Piper executable, or just `'piper'` if it is on
31
+ * `PATH`. Resolved from `PIPER_BIN` env var when omitted.
32
+ * @defaultValue `process.env.PIPER_BIN ?? 'piper'`
33
+ */
34
+ binaryPath?: string;
35
+
36
+ /**
37
+ * Absolute path to the ONNX voice model file.
38
+ * Resolved from `PIPER_MODEL_PATH` env var when omitted.
39
+ * @defaultValue `~/.agentos/models/piper/en_US-lessac-medium.onnx`
40
+ */
41
+ modelPath?: string;
42
+
43
+ /**
44
+ * Maximum number of bytes accepted from Piper's stdout.
45
+ * Prevents runaway memory usage if the binary misbehaves.
46
+ * @defaultValue `10485760` (10 MB)
47
+ */
48
+ maxBufferBytes?: number;
49
+
50
+ /**
51
+ * Milliseconds before the spawned process is killed with SIGTERM and the
52
+ * promise is rejected.
53
+ * @defaultValue `30000`
54
+ */
55
+ timeoutMs?: number;
56
+ }
57
+
58
+ /**
59
+ * Synthesised audio returned by {@link PiperTTSProvider.synthesize}.
60
+ */
61
+ export interface SynthesisResult {
62
+ /** Raw WAV audio bytes produced by Piper. */
63
+ audioBuffer: Buffer;
64
+ /** MIME type of the audio data. Always `'audio/wav'` for this provider. */
65
+ mimeType: 'audio/wav';
66
+ /** Billable cost (always 0 — fully local). */
67
+ cost: 0;
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Provider
72
+ // ---------------------------------------------------------------------------
73
+
74
+ /**
75
+ * Piper offline text-to-speech provider.
76
+ *
77
+ * Implements the `TextToSpeechProvider` contract expected by the AgentOS voice
78
+ * pipeline without taking a hard runtime dependency on the interface types.
79
+ */
80
+ export class PiperTTSProvider {
81
+ /** Stable provider identifier used by the AgentOS extension registry. */
82
+ readonly id = 'piper';
83
+
84
+ /**
85
+ * Streaming is not supported — Piper outputs a complete WAV file.
86
+ * Setting this to `false` signals the pipeline to use batch mode.
87
+ */
88
+ readonly supportsStreaming = false;
89
+
90
+ private readonly _binaryPath: string;
91
+ private readonly _modelPath: string;
92
+ private readonly _maxBufferBytes: number;
93
+ private readonly _timeoutMs: number;
94
+
95
+ /**
96
+ * Create a new {@link PiperTTSProvider}.
97
+ *
98
+ * @param options - Optional configuration. All fields have sensible defaults
99
+ * or fall back to environment variables.
100
+ */
101
+ constructor(options: PiperTTSProviderOptions = {}) {
102
+ this._binaryPath = options.binaryPath ?? process.env['PIPER_BIN'] ?? 'piper';
103
+ this._modelPath =
104
+ options.modelPath ??
105
+ process.env['PIPER_MODEL_PATH'] ??
106
+ path.join(os.homedir(), '.agentos', 'models', 'piper', 'en_US-lessac-medium.onnx');
107
+ this._maxBufferBytes = options.maxBufferBytes ?? 10 * 1024 * 1024; // 10 MB
108
+ this._timeoutMs = options.timeoutMs ?? 30_000;
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Public API
113
+ // ---------------------------------------------------------------------------
114
+
115
+ /**
116
+ * Synthesise plain text to WAV audio using the local Piper binary.
117
+ *
118
+ * Piper is invoked as:
119
+ * ```
120
+ * piper --model <modelPath> --output_file -
121
+ * ```
122
+ * The text is written to `stdin`; WAV bytes are read from `stdout`.
123
+ *
124
+ * @param text - Plain text to synthesise.
125
+ * @returns {@link SynthesisResult} containing the raw WAV buffer.
126
+ * @throws If Piper exits with a non-zero code, times out, or the buffer
127
+ * limit is exceeded.
128
+ */
129
+ async synthesize(text: string): Promise<SynthesisResult> {
130
+ return new Promise<SynthesisResult>((resolve, reject) => {
131
+ const proc = spawn(this._binaryPath, [
132
+ '--model', this._modelPath,
133
+ '--output_file', '-',
134
+ ]);
135
+
136
+ const chunks: Buffer[] = [];
137
+ let totalBytes = 0;
138
+ let settled = false;
139
+
140
+ /** Settle the promise once, ignoring subsequent events. */
141
+ const settle = (err: Error | null, result?: SynthesisResult) => {
142
+ if (settled) return;
143
+ settled = true;
144
+ clearTimeout(timer);
145
+ if (err) {
146
+ proc.kill('SIGTERM');
147
+ reject(err);
148
+ } else {
149
+ resolve(result!);
150
+ }
151
+ };
152
+
153
+ // Timeout guard.
154
+ const timer = setTimeout(() => {
155
+ settle(new Error(`Piper process timed out after ${this._timeoutMs} ms`));
156
+ }, this._timeoutMs);
157
+
158
+ proc.stdout.on('data', (chunk: Buffer) => {
159
+ totalBytes += chunk.length;
160
+ if (totalBytes > this._maxBufferBytes) {
161
+ settle(
162
+ new Error(
163
+ `Piper output exceeded maxBufferBytes limit (${this._maxBufferBytes} bytes)`,
164
+ ),
165
+ );
166
+ return;
167
+ }
168
+ chunks.push(chunk);
169
+ });
170
+
171
+ proc.stderr.on('data', (chunk: Buffer) => {
172
+ // Piper writes progress/info to stderr; capture for error reporting.
173
+ // We do NOT reject on stderr data alone — only on non-zero exit.
174
+ void chunk; // intentionally unused unless exit code is non-zero
175
+ });
176
+
177
+ proc.on('error', (err: Error) => {
178
+ settle(new Error(`Failed to spawn Piper binary "${this._binaryPath}": ${err.message}`));
179
+ });
180
+
181
+ proc.on('close', (code: number | null) => {
182
+ if (code !== 0 && code !== null) {
183
+ settle(new Error(`Piper exited with non-zero code ${code}`));
184
+ return;
185
+ }
186
+ settle(null, {
187
+ audioBuffer: Buffer.concat(chunks),
188
+ mimeType: 'audio/wav',
189
+ cost: 0,
190
+ });
191
+ });
192
+
193
+ // Write text to stdin and close the stream so Piper knows input is done.
194
+ proc.stdin.write(text);
195
+ proc.stdin.end();
196
+ });
197
+ }
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // Accessors (for testing / diagnostics)
201
+ // ---------------------------------------------------------------------------
202
+
203
+ /** Returns the resolved binary path. */
204
+ getBinaryPath(): string { return this._binaryPath; }
205
+
206
+ /** Returns the resolved model path. */
207
+ getModelPath(): string { return this._modelPath; }
208
+
209
+ /** Returns the configured max buffer size in bytes. */
210
+ getMaxBufferBytes(): number { return this._maxBufferBytes; }
211
+
212
+ /** Returns the configured timeout in milliseconds. */
213
+ getTimeoutMs(): number { return this._timeoutMs; }
214
+ }
package/src/index.ts ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * @file index.ts
3
+ * @description Pack factory for the Piper TTS extension pack.
4
+ *
5
+ * Exports the main {@link createPiperTTS} 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 { createPiperTTS } from '@framers/agentos-ext-piper';
12
+ *
13
+ * const tts = createPiperTTS({ modelPath: '/opt/piper/en_US-lessac-medium.onnx' });
14
+ * const { audioBuffer } = await tts.synthesize('Hello world');
15
+ * ```
16
+ *
17
+ * ### Usage (manifest-driven)
18
+ * ```json
19
+ * { "packs": [{ "module": "@framers/agentos-ext-piper" }] }
20
+ * ```
21
+ *
22
+ * @module piper
23
+ */
24
+
25
+ import { PiperTTSProvider } from './PiperTTSProvider.js';
26
+ import type { PiperTTSProviderOptions } from './PiperTTSProvider.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_TTS = 'tts-provider';
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Factories
58
+ // ---------------------------------------------------------------------------
59
+
60
+ /**
61
+ * Create a standalone {@link PiperTTSProvider} instance.
62
+ *
63
+ * @param options - Optional constructor options.
64
+ * @returns Configured {@link PiperTTSProvider}.
65
+ */
66
+ export function createPiperTTS(options?: PiperTTSProviderOptions): PiperTTSProvider {
67
+ return new PiperTTSProvider(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 `tts-provider` descriptor backed
75
+ * by {@link PiperTTSProvider}.
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 PiperTTSProvider({
83
+ binaryPath: opts['binaryPath'] as string | undefined,
84
+ modelPath: opts['modelPath'] as string | undefined,
85
+ maxBufferBytes: opts['maxBufferBytes'] as number | undefined,
86
+ timeoutMs: opts['timeoutMs'] as number | undefined,
87
+ });
88
+
89
+ return {
90
+ id: 'piper',
91
+ descriptors: [
92
+ {
93
+ id: 'piper',
94
+ kind: EXTENSION_KIND_TTS,
95
+ payload: provider,
96
+ enableByDefault: true,
97
+ metadata: { providerId: 'piper' },
98
+ },
99
+ ],
100
+ };
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Re-exports
105
+ // ---------------------------------------------------------------------------
106
+
107
+ export { PiperTTSProvider } from './PiperTTSProvider.js';
108
+ export type { SynthesisResult, PiperTTSProviderOptions } from './PiperTTSProvider.js';