@alfe.ai/openclaw-voice 0.1.13 → 0.1.15
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/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/plugin.d.cts +40 -15
- package/dist/plugin.d.ts +40 -15
- package/dist/plugin2.cjs +87 -174
- package/dist/plugin2.d.cts +2 -0
- package/dist/plugin2.d.ts +2 -0
- package/dist/plugin2.js +87 -174
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -3
package/dist/index.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import plugin from "./plugin.cjs";
|
|
1
|
+
import { t as plugin } from "./plugin.cjs";
|
|
2
2
|
export { plugin as default };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import plugin from "./plugin.js";
|
|
1
|
+
import { t as plugin } from "./plugin.js";
|
|
2
2
|
export { plugin as default };
|
package/dist/plugin.d.cts
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
|
-
|
|
1
|
+
//#region ../openclaw-plugin-kit/dist/index.d.ts
|
|
2
2
|
|
|
3
|
+
//# sourceMappingURL=types.d.ts.map
|
|
4
|
+
//#endregion
|
|
5
|
+
//#region src/tools.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Tool-definition helpers — the `defineTool` / `ok` / `errResult` trio that
|
|
8
|
+
* was copy-pasted across 8 plugins (identity, google, teams, mobile,
|
|
9
|
+
* whatsapp, voice, chat a2a-tools, base openclaw).
|
|
10
|
+
*
|
|
11
|
+
* Error handling is standardized on the openclaw-google variant — the only
|
|
12
|
+
* copy that survived non-`Error` throws (`e instanceof Error ? e.message :
|
|
13
|
+
* "Unknown error"`). The other copies did `(e as Error).message`, which
|
|
14
|
+
* crashes the tool executor when a handler throws a string/object.
|
|
15
|
+
*/
|
|
16
|
+
/** Shape returned to OpenClaw from a tool `execute`. */
|
|
17
|
+
interface ToolResult {
|
|
18
|
+
content: {
|
|
19
|
+
type: "text";
|
|
20
|
+
text: string;
|
|
21
|
+
}[];
|
|
22
|
+
details: unknown;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* An OpenClaw tool definition.
|
|
26
|
+
*
|
|
27
|
+
* `parameters` is generic because the fleet is split between TypeBox
|
|
28
|
+
* `TSchema` schemas (identity/google/teams/mobile/whatsapp) and plain
|
|
29
|
+
* JSON-Schema objects (chat a2a-tools). Instantiate with whichever schema
|
|
30
|
+
* type the plugin uses — the kit itself has no schema dependency.
|
|
31
|
+
*/
|
|
32
|
+
interface ToolDef<TParameters = unknown> {
|
|
33
|
+
name: string;
|
|
34
|
+
description: string;
|
|
35
|
+
label: string;
|
|
36
|
+
parameters: TParameters;
|
|
37
|
+
execute: (toolCallId: string, params: Record<string, unknown>) => Promise<ToolResult>;
|
|
38
|
+
}
|
|
39
|
+
/** Wrap a successful handler result in the OpenClaw tool-result envelope. */
|
|
40
|
+
//#endregion
|
|
3
41
|
//#region src/plugin.d.ts
|
|
4
42
|
|
|
5
43
|
interface Logger {
|
|
@@ -46,19 +84,6 @@ interface OpenClawPluginApi {
|
|
|
46
84
|
priority?: number;
|
|
47
85
|
}): void;
|
|
48
86
|
}
|
|
49
|
-
interface ToolDef {
|
|
50
|
-
name: string;
|
|
51
|
-
description: string;
|
|
52
|
-
label: string;
|
|
53
|
-
parameters: TSchema;
|
|
54
|
-
execute: (toolCallId: string, params: Record<string, unknown>) => Promise<{
|
|
55
|
-
content: {
|
|
56
|
-
type: 'text';
|
|
57
|
-
text: string;
|
|
58
|
-
}[];
|
|
59
|
-
details: unknown;
|
|
60
|
-
}>;
|
|
61
|
-
}
|
|
62
87
|
declare const plugin: {
|
|
63
88
|
id: string;
|
|
64
89
|
name: string;
|
|
@@ -68,4 +93,4 @@ declare const plugin: {
|
|
|
68
93
|
deactivate(api: OpenClawPluginApi): void;
|
|
69
94
|
};
|
|
70
95
|
//#endregion
|
|
71
|
-
export { plugin as
|
|
96
|
+
export { plugin as t };
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
|
-
|
|
1
|
+
//#region ../openclaw-plugin-kit/dist/index.d.ts
|
|
2
2
|
|
|
3
|
+
//# sourceMappingURL=types.d.ts.map
|
|
4
|
+
//#endregion
|
|
5
|
+
//#region src/tools.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Tool-definition helpers — the `defineTool` / `ok` / `errResult` trio that
|
|
8
|
+
* was copy-pasted across 8 plugins (identity, google, teams, mobile,
|
|
9
|
+
* whatsapp, voice, chat a2a-tools, base openclaw).
|
|
10
|
+
*
|
|
11
|
+
* Error handling is standardized on the openclaw-google variant — the only
|
|
12
|
+
* copy that survived non-`Error` throws (`e instanceof Error ? e.message :
|
|
13
|
+
* "Unknown error"`). The other copies did `(e as Error).message`, which
|
|
14
|
+
* crashes the tool executor when a handler throws a string/object.
|
|
15
|
+
*/
|
|
16
|
+
/** Shape returned to OpenClaw from a tool `execute`. */
|
|
17
|
+
interface ToolResult {
|
|
18
|
+
content: {
|
|
19
|
+
type: "text";
|
|
20
|
+
text: string;
|
|
21
|
+
}[];
|
|
22
|
+
details: unknown;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* An OpenClaw tool definition.
|
|
26
|
+
*
|
|
27
|
+
* `parameters` is generic because the fleet is split between TypeBox
|
|
28
|
+
* `TSchema` schemas (identity/google/teams/mobile/whatsapp) and plain
|
|
29
|
+
* JSON-Schema objects (chat a2a-tools). Instantiate with whichever schema
|
|
30
|
+
* type the plugin uses — the kit itself has no schema dependency.
|
|
31
|
+
*/
|
|
32
|
+
interface ToolDef<TParameters = unknown> {
|
|
33
|
+
name: string;
|
|
34
|
+
description: string;
|
|
35
|
+
label: string;
|
|
36
|
+
parameters: TParameters;
|
|
37
|
+
execute: (toolCallId: string, params: Record<string, unknown>) => Promise<ToolResult>;
|
|
38
|
+
}
|
|
39
|
+
/** Wrap a successful handler result in the OpenClaw tool-result envelope. */
|
|
40
|
+
//#endregion
|
|
3
41
|
//#region src/plugin.d.ts
|
|
4
42
|
|
|
5
43
|
interface Logger {
|
|
@@ -46,19 +84,6 @@ interface OpenClawPluginApi {
|
|
|
46
84
|
priority?: number;
|
|
47
85
|
}): void;
|
|
48
86
|
}
|
|
49
|
-
interface ToolDef {
|
|
50
|
-
name: string;
|
|
51
|
-
description: string;
|
|
52
|
-
label: string;
|
|
53
|
-
parameters: TSchema;
|
|
54
|
-
execute: (toolCallId: string, params: Record<string, unknown>) => Promise<{
|
|
55
|
-
content: {
|
|
56
|
-
type: 'text';
|
|
57
|
-
text: string;
|
|
58
|
-
}[];
|
|
59
|
-
details: unknown;
|
|
60
|
-
}>;
|
|
61
|
-
}
|
|
62
87
|
declare const plugin: {
|
|
63
88
|
id: string;
|
|
64
89
|
name: string;
|
|
@@ -68,4 +93,4 @@ declare const plugin: {
|
|
|
68
93
|
deactivate(api: OpenClawPluginApi): void;
|
|
69
94
|
};
|
|
70
95
|
//#endregion
|
|
71
|
-
export { plugin as
|
|
96
|
+
export { plugin as t };
|
package/dist/plugin2.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
let _sinclair_typebox = require("@sinclair/typebox");
|
|
2
2
|
let _alfe_ai_config = require("@alfe.ai/config");
|
|
3
3
|
let _alfe_ai_agent_api_client = require("@alfe.ai/agent-api-client");
|
|
4
|
+
let _alfe_ai_openclaw_plugin_kit = require("@alfe.ai/openclaw-plugin-kit");
|
|
4
5
|
let node_fs_promises = require("node:fs/promises");
|
|
5
6
|
let node_path = require("node:path");
|
|
6
7
|
let node_module = require("node:module");
|
|
@@ -79,9 +80,11 @@ function parseWav(buf) {
|
|
|
79
80
|
* - voice_tts tool — text → audio file via POST /voice/tts (agent-authed)
|
|
80
81
|
* - voice_stt tool — audio file → transcript via POST /voice/stt (agent-authed)
|
|
81
82
|
* - voice.speak RPC — legacy gateway RPC (reads plugin config; vestigial)
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
83
|
+
*
|
|
84
|
+
* voice_hangup / voice_transfer / voice_dtmf were removed deliberately — they
|
|
85
|
+
* threw unconditionally ("requires a channel service"), which poisons the
|
|
86
|
+
* model's tool selection with always-failing options. Re-add per-channel when
|
|
87
|
+
* the twilio-adapter/Discord dispatch actually lands (voice-engineer roadmap).
|
|
85
88
|
*
|
|
86
89
|
* The voice_tts/voice_stt tools source URL + agent key from ~/.alfe/config.toml
|
|
87
90
|
* via `@alfe.ai/config` (resolveConfig) and call through `AgentApiClient` — the
|
|
@@ -95,6 +98,7 @@ const VOICE_CAPABILITIES = [
|
|
|
95
98
|
"voice.dtmf",
|
|
96
99
|
"voice.hangup"
|
|
97
100
|
];
|
|
101
|
+
const VOICE_ACTIVATION_KEY = (0, _alfe_ai_openclaw_plugin_kit.getActivationKey)("voice");
|
|
98
102
|
let voiceServiceUrl = "";
|
|
99
103
|
let voiceServiceApiKey = "";
|
|
100
104
|
async function voiceApi(method, path, body) {
|
|
@@ -114,65 +118,6 @@ async function voiceApi(method, path, body) {
|
|
|
114
118
|
return json;
|
|
115
119
|
}
|
|
116
120
|
let daemonIpcClient = null;
|
|
117
|
-
async function connectToDaemon(socketPath, log) {
|
|
118
|
-
try {
|
|
119
|
-
const IPCClientCtor = (await import("@alfe.ai/openclaw")).IPCClient;
|
|
120
|
-
const client = new IPCClientCtor(socketPath, log);
|
|
121
|
-
client.on("connected", async () => {
|
|
122
|
-
log.info("Connected to Alfe daemon — registering voice capabilities...");
|
|
123
|
-
const response = await client.request("capability.register", {
|
|
124
|
-
plugin: "@alfe.ai/openclaw-voice",
|
|
125
|
-
capabilities: [...VOICE_CAPABILITIES]
|
|
126
|
-
});
|
|
127
|
-
if (response.ok) log.info("Voice capabilities registered with daemon");
|
|
128
|
-
else log.warn(`Failed to register voice capabilities: ${response.error?.message ?? "unknown"}`);
|
|
129
|
-
});
|
|
130
|
-
client.on("disconnected", (reason) => {
|
|
131
|
-
log.warn(`Disconnected from Alfe daemon: ${String(reason)}`);
|
|
132
|
-
});
|
|
133
|
-
client.on("error", (err) => {
|
|
134
|
-
log.debug(`Daemon IPC error: ${err.message}`);
|
|
135
|
-
});
|
|
136
|
-
client.start();
|
|
137
|
-
return client;
|
|
138
|
-
} catch {
|
|
139
|
-
log.info("Alfe daemon not available — voice plugin running standalone");
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
function ok(data) {
|
|
144
|
-
return {
|
|
145
|
-
content: [{
|
|
146
|
-
type: "text",
|
|
147
|
-
text: JSON.stringify(data)
|
|
148
|
-
}],
|
|
149
|
-
details: data
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
function errResult(message) {
|
|
153
|
-
return {
|
|
154
|
-
content: [{
|
|
155
|
-
type: "text",
|
|
156
|
-
text: JSON.stringify({ error: message })
|
|
157
|
-
}],
|
|
158
|
-
details: { error: message }
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
function defineTool(def) {
|
|
162
|
-
return {
|
|
163
|
-
name: def.name,
|
|
164
|
-
description: def.description,
|
|
165
|
-
label: def.name,
|
|
166
|
-
parameters: def.parameters,
|
|
167
|
-
execute: async (_toolCallId, params) => {
|
|
168
|
-
try {
|
|
169
|
-
return ok(await def.handler(params));
|
|
170
|
-
} catch (e) {
|
|
171
|
-
return errResult(e.message);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
121
|
let voiceClient = null;
|
|
177
122
|
function getVoiceClient() {
|
|
178
123
|
if (voiceClient) return voiceClient;
|
|
@@ -187,102 +132,69 @@ function getVoiceClient() {
|
|
|
187
132
|
function resolveWorkspacePath(p) {
|
|
188
133
|
return (0, node_path.isAbsolute)(p) ? p : (0, node_path.resolve)(process.cwd(), p);
|
|
189
134
|
}
|
|
190
|
-
const voiceTools = [
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
model: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.Union([_sinclair_typebox.Type.Literal("eleven_turbo_v2_5"), _sinclair_typebox.Type.Literal("eleven_multilingual_v2")], { description: "TTS model. eleven_turbo_v2_5 (lower latency) when omitted." }))
|
|
200
|
-
}),
|
|
201
|
-
handler: async (params) => {
|
|
202
|
-
const text = params.text;
|
|
203
|
-
const format = params.format ?? "wav";
|
|
204
|
-
const result = await getVoiceClient().tts({
|
|
205
|
-
text,
|
|
206
|
-
voiceId: params.voiceId,
|
|
207
|
-
model: params.model
|
|
208
|
-
});
|
|
209
|
-
const bytes = format === "wav" ? pcmToWav(result.audio, {
|
|
210
|
-
sampleRate: result.sampleRate,
|
|
211
|
-
channels: result.channels,
|
|
212
|
-
bitDepth: result.bitDepth
|
|
213
|
-
}) : result.audio;
|
|
214
|
-
const outputPath = params.outputPath ?? `alfe-tts-${String(Date.now())}.${format}`;
|
|
215
|
-
const absolutePath = resolveWorkspacePath(outputPath);
|
|
216
|
-
await (0, node_fs_promises.writeFile)(absolutePath, bytes);
|
|
217
|
-
return {
|
|
218
|
-
path: outputPath,
|
|
219
|
-
absolutePath,
|
|
220
|
-
format,
|
|
221
|
-
sampleRate: result.sampleRate,
|
|
222
|
-
channels: result.channels,
|
|
223
|
-
bitDepth: result.bitDepth,
|
|
224
|
-
bytes: bytes.length,
|
|
225
|
-
characters: text.length
|
|
226
|
-
};
|
|
227
|
-
}
|
|
135
|
+
const voiceTools = [(0, _alfe_ai_openclaw_plugin_kit.defineTool)({
|
|
136
|
+
name: "voice_tts",
|
|
137
|
+
description: "Convert text to speech using the Alfe voice service (ElevenLabs). Writes a playable audio file to the workspace and returns its path. Billed per character to your tenant credit pool.",
|
|
138
|
+
parameters: _sinclair_typebox.Type.Object({
|
|
139
|
+
text: _sinclair_typebox.Type.String({ description: "Text to synthesize (1–5000 characters)." }),
|
|
140
|
+
outputPath: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Where to write the audio file (absolute, or relative to the working directory). Defaults to alfe-tts-<timestamp>.wav in the working directory." })),
|
|
141
|
+
format: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.Union([_sinclair_typebox.Type.Literal("wav"), _sinclair_typebox.Type.Literal("pcm")], { description: "Output container. 'wav' (default) is a playable file; 'pcm' is headerless 24kHz/mono/16-bit raw PCM." })),
|
|
142
|
+
voiceId: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "ElevenLabs voice ID. Platform default when omitted." })),
|
|
143
|
+
model: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.Union([_sinclair_typebox.Type.Literal("eleven_turbo_v2_5"), _sinclair_typebox.Type.Literal("eleven_multilingual_v2")], { description: "TTS model. eleven_turbo_v2_5 (lower latency) when omitted." }))
|
|
228
144
|
}),
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
}),
|
|
263
|
-
defineTool({
|
|
264
|
-
name: "voice_transfer",
|
|
265
|
-
description: "Transfer the current phone call to another number. Only works for Twilio calls.",
|
|
266
|
-
parameters: _sinclair_typebox.Type.Object({
|
|
267
|
-
targetNumber: _sinclair_typebox.Type.String({ description: "Phone number to transfer to (E.164 format)" }),
|
|
268
|
-
sessionId: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Voice session ID." }))
|
|
269
|
-
}),
|
|
270
|
-
handler: () => {
|
|
271
|
-
throw new Error("voice_transfer requires the Twilio service. The voice service no longer manages phone calls directly.");
|
|
272
|
-
}
|
|
145
|
+
handler: async (params) => {
|
|
146
|
+
const text = params.text;
|
|
147
|
+
const format = params.format ?? "wav";
|
|
148
|
+
const result = await getVoiceClient().tts({
|
|
149
|
+
text,
|
|
150
|
+
voiceId: params.voiceId,
|
|
151
|
+
model: params.model
|
|
152
|
+
});
|
|
153
|
+
const bytes = format === "wav" ? pcmToWav(result.audio, {
|
|
154
|
+
sampleRate: result.sampleRate,
|
|
155
|
+
channels: result.channels,
|
|
156
|
+
bitDepth: result.bitDepth
|
|
157
|
+
}) : result.audio;
|
|
158
|
+
const outputPath = params.outputPath ?? `alfe-tts-${String(Date.now())}.${format}`;
|
|
159
|
+
const absolutePath = resolveWorkspacePath(outputPath);
|
|
160
|
+
await (0, node_fs_promises.writeFile)(absolutePath, bytes);
|
|
161
|
+
return {
|
|
162
|
+
path: outputPath,
|
|
163
|
+
absolutePath,
|
|
164
|
+
format,
|
|
165
|
+
sampleRate: result.sampleRate,
|
|
166
|
+
channels: result.channels,
|
|
167
|
+
bitDepth: result.bitDepth,
|
|
168
|
+
bytes: bytes.length,
|
|
169
|
+
characters: text.length
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}), (0, _alfe_ai_openclaw_plugin_kit.defineTool)({
|
|
173
|
+
name: "voice_stt",
|
|
174
|
+
description: "Transcribe an audio file to text using the Alfe voice service (Deepgram). Accepts a WAV file or headerless mono 16-bit PCM. Billed per audio-second to your tenant credit pool.",
|
|
175
|
+
parameters: _sinclair_typebox.Type.Object({
|
|
176
|
+
path: _sinclair_typebox.Type.String({ description: "Path to the audio file (absolute, or relative to the working directory). WAV or raw linear16 mono PCM." }),
|
|
177
|
+
sampleRate: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.Number({ description: "Sample rate in Hz (8000–48000). Only used for headerless PCM input; ignored for WAV (its header wins). Defaults to 24000." }))
|
|
273
178
|
}),
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
179
|
+
handler: async (params) => {
|
|
180
|
+
const raw = await (0, node_fs_promises.readFile)(resolveWorkspacePath(params.path));
|
|
181
|
+
const wav = parseWav(raw);
|
|
182
|
+
let audio;
|
|
183
|
+
let sampleRate;
|
|
184
|
+
if (wav) {
|
|
185
|
+
if (wav.bitDepth !== 16 || wav.channels !== 1) throw new Error(`voice_stt needs 16-bit mono audio; got ${String(wav.bitDepth)}-bit / ${String(wav.channels)}-channel WAV. Re-export as 16-bit mono.`);
|
|
186
|
+
audio = wav.pcm;
|
|
187
|
+
sampleRate = wav.sampleRate;
|
|
188
|
+
} else {
|
|
189
|
+
audio = raw;
|
|
190
|
+
sampleRate = params.sampleRate ?? 24e3;
|
|
283
191
|
}
|
|
284
|
-
|
|
285
|
-
|
|
192
|
+
return getVoiceClient().stt({
|
|
193
|
+
audio,
|
|
194
|
+
sampleRate
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
})];
|
|
286
198
|
const plugin = {
|
|
287
199
|
id: "@alfe.ai/openclaw-voice",
|
|
288
200
|
name: "Alfe Voice Plugin",
|
|
@@ -296,27 +208,27 @@ const plugin = {
|
|
|
296
208
|
const fullConfig = api.config ?? {};
|
|
297
209
|
const pluginConfig = fullConfig.plugins?.entries?.["@alfe.ai/openclaw-voice"]?.config ?? fullConfig.plugins?.entries?.["voice-gateway"]?.config ?? {};
|
|
298
210
|
const startVoiceService = () => {
|
|
299
|
-
|
|
300
|
-
log.
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
211
|
+
(0, _alfe_ai_openclaw_plugin_kit.guardedStart)(VOICE_ACTIVATION_KEY, log, () => {
|
|
212
|
+
log.info("Alfe Voice plugin activating...");
|
|
213
|
+
voiceServiceUrl = pluginConfig.voiceServiceUrl ?? `http://localhost:${String(pluginConfig.voiceServicePort ?? "3100")}`;
|
|
214
|
+
voiceServiceApiKey = pluginConfig.voiceServiceApiKey ?? "";
|
|
215
|
+
log.info(`Voice service: ${voiceServiceUrl}`);
|
|
216
|
+
let alfeConfig = null;
|
|
217
|
+
try {
|
|
218
|
+
alfeConfig = (0, _alfe_ai_config.resolveConfig)();
|
|
219
|
+
} catch {}
|
|
220
|
+
(0, _alfe_ai_openclaw_plugin_kit.connectToDaemon)(pluginConfig.daemonSocket ?? alfeConfig?.socketPath ?? _alfe_ai_config.DEFAULT_SOCKET_PATH, log, {
|
|
221
|
+
pluginId: "@alfe.ai/openclaw-voice",
|
|
222
|
+
capabilities: VOICE_CAPABILITIES,
|
|
223
|
+
standaloneNote: "Alfe daemon not available — voice plugin running standalone"
|
|
224
|
+
}).then((client) => {
|
|
225
|
+
daemonIpcClient = client;
|
|
226
|
+
}).catch((err) => {
|
|
227
|
+
log.debug(`Daemon connect failed: ${err.message}`);
|
|
228
|
+
});
|
|
316
229
|
});
|
|
317
230
|
};
|
|
318
231
|
const stopVoiceService = () => {
|
|
319
|
-
globalThis.__voiceGatewayActivated = false;
|
|
320
232
|
if (daemonIpcClient) {
|
|
321
233
|
try {
|
|
322
234
|
daemonIpcClient.stop();
|
|
@@ -326,6 +238,7 @@ const plugin = {
|
|
|
326
238
|
}
|
|
327
239
|
daemonIpcClient = null;
|
|
328
240
|
}
|
|
241
|
+
(0, _alfe_ai_openclaw_plugin_kit.resetActivation)(VOICE_ACTIVATION_KEY);
|
|
329
242
|
log.info("Alfe Voice plugin deactivated");
|
|
330
243
|
};
|
|
331
244
|
api.registerGatewayMethod("voice.speak", async (...args) => {
|
|
@@ -351,7 +264,6 @@ const plugin = {
|
|
|
351
264
|
log.info("Alfe Voice plugin activated");
|
|
352
265
|
},
|
|
353
266
|
deactivate(api) {
|
|
354
|
-
globalThis.__voiceGatewayActivated = false;
|
|
355
267
|
const log = api.logger;
|
|
356
268
|
log.info("Alfe Voice plugin deactivating...");
|
|
357
269
|
if (daemonIpcClient) {
|
|
@@ -363,6 +275,7 @@ const plugin = {
|
|
|
363
275
|
}
|
|
364
276
|
daemonIpcClient = null;
|
|
365
277
|
}
|
|
278
|
+
(0, _alfe_ai_openclaw_plugin_kit.resetActivation)(VOICE_ACTIVATION_KEY);
|
|
366
279
|
log.info("Alfe Voice plugin deactivated");
|
|
367
280
|
}
|
|
368
281
|
};
|
package/dist/plugin2.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
3
|
import { DEFAULT_SOCKET_PATH, resolveConfig } from "@alfe.ai/config";
|
|
4
4
|
import { AgentApiClient, installToolErrorCapture } from "@alfe.ai/agent-api-client";
|
|
5
|
+
import { connectToDaemon, defineTool, getActivationKey, guardedStart, resetActivation } from "@alfe.ai/openclaw-plugin-kit";
|
|
5
6
|
import { readFile, writeFile } from "node:fs/promises";
|
|
6
7
|
import { isAbsolute, resolve } from "node:path";
|
|
7
8
|
//#region src/audio.ts
|
|
@@ -79,9 +80,11 @@ function parseWav(buf) {
|
|
|
79
80
|
* - voice_tts tool — text → audio file via POST /voice/tts (agent-authed)
|
|
80
81
|
* - voice_stt tool — audio file → transcript via POST /voice/stt (agent-authed)
|
|
81
82
|
* - voice.speak RPC — legacy gateway RPC (reads plugin config; vestigial)
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
83
|
+
*
|
|
84
|
+
* voice_hangup / voice_transfer / voice_dtmf were removed deliberately — they
|
|
85
|
+
* threw unconditionally ("requires a channel service"), which poisons the
|
|
86
|
+
* model's tool selection with always-failing options. Re-add per-channel when
|
|
87
|
+
* the twilio-adapter/Discord dispatch actually lands (voice-engineer roadmap).
|
|
85
88
|
*
|
|
86
89
|
* The voice_tts/voice_stt tools source URL + agent key from ~/.alfe/config.toml
|
|
87
90
|
* via `@alfe.ai/config` (resolveConfig) and call through `AgentApiClient` — the
|
|
@@ -95,6 +98,7 @@ const VOICE_CAPABILITIES = [
|
|
|
95
98
|
"voice.dtmf",
|
|
96
99
|
"voice.hangup"
|
|
97
100
|
];
|
|
101
|
+
const VOICE_ACTIVATION_KEY = getActivationKey("voice");
|
|
98
102
|
let voiceServiceUrl = "";
|
|
99
103
|
let voiceServiceApiKey = "";
|
|
100
104
|
async function voiceApi(method, path, body) {
|
|
@@ -114,65 +118,6 @@ async function voiceApi(method, path, body) {
|
|
|
114
118
|
return json;
|
|
115
119
|
}
|
|
116
120
|
let daemonIpcClient = null;
|
|
117
|
-
async function connectToDaemon(socketPath, log) {
|
|
118
|
-
try {
|
|
119
|
-
const IPCClientCtor = (await import("@alfe.ai/openclaw")).IPCClient;
|
|
120
|
-
const client = new IPCClientCtor(socketPath, log);
|
|
121
|
-
client.on("connected", async () => {
|
|
122
|
-
log.info("Connected to Alfe daemon — registering voice capabilities...");
|
|
123
|
-
const response = await client.request("capability.register", {
|
|
124
|
-
plugin: "@alfe.ai/openclaw-voice",
|
|
125
|
-
capabilities: [...VOICE_CAPABILITIES]
|
|
126
|
-
});
|
|
127
|
-
if (response.ok) log.info("Voice capabilities registered with daemon");
|
|
128
|
-
else log.warn(`Failed to register voice capabilities: ${response.error?.message ?? "unknown"}`);
|
|
129
|
-
});
|
|
130
|
-
client.on("disconnected", (reason) => {
|
|
131
|
-
log.warn(`Disconnected from Alfe daemon: ${String(reason)}`);
|
|
132
|
-
});
|
|
133
|
-
client.on("error", (err) => {
|
|
134
|
-
log.debug(`Daemon IPC error: ${err.message}`);
|
|
135
|
-
});
|
|
136
|
-
client.start();
|
|
137
|
-
return client;
|
|
138
|
-
} catch {
|
|
139
|
-
log.info("Alfe daemon not available — voice plugin running standalone");
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
function ok(data) {
|
|
144
|
-
return {
|
|
145
|
-
content: [{
|
|
146
|
-
type: "text",
|
|
147
|
-
text: JSON.stringify(data)
|
|
148
|
-
}],
|
|
149
|
-
details: data
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
function errResult(message) {
|
|
153
|
-
return {
|
|
154
|
-
content: [{
|
|
155
|
-
type: "text",
|
|
156
|
-
text: JSON.stringify({ error: message })
|
|
157
|
-
}],
|
|
158
|
-
details: { error: message }
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
function defineTool(def) {
|
|
162
|
-
return {
|
|
163
|
-
name: def.name,
|
|
164
|
-
description: def.description,
|
|
165
|
-
label: def.name,
|
|
166
|
-
parameters: def.parameters,
|
|
167
|
-
execute: async (_toolCallId, params) => {
|
|
168
|
-
try {
|
|
169
|
-
return ok(await def.handler(params));
|
|
170
|
-
} catch (e) {
|
|
171
|
-
return errResult(e.message);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
121
|
let voiceClient = null;
|
|
177
122
|
function getVoiceClient() {
|
|
178
123
|
if (voiceClient) return voiceClient;
|
|
@@ -187,102 +132,69 @@ function getVoiceClient() {
|
|
|
187
132
|
function resolveWorkspacePath(p) {
|
|
188
133
|
return isAbsolute(p) ? p : resolve(process.cwd(), p);
|
|
189
134
|
}
|
|
190
|
-
const voiceTools = [
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
model: Type.Optional(Type.Union([Type.Literal("eleven_turbo_v2_5"), Type.Literal("eleven_multilingual_v2")], { description: "TTS model. eleven_turbo_v2_5 (lower latency) when omitted." }))
|
|
200
|
-
}),
|
|
201
|
-
handler: async (params) => {
|
|
202
|
-
const text = params.text;
|
|
203
|
-
const format = params.format ?? "wav";
|
|
204
|
-
const result = await getVoiceClient().tts({
|
|
205
|
-
text,
|
|
206
|
-
voiceId: params.voiceId,
|
|
207
|
-
model: params.model
|
|
208
|
-
});
|
|
209
|
-
const bytes = format === "wav" ? pcmToWav(result.audio, {
|
|
210
|
-
sampleRate: result.sampleRate,
|
|
211
|
-
channels: result.channels,
|
|
212
|
-
bitDepth: result.bitDepth
|
|
213
|
-
}) : result.audio;
|
|
214
|
-
const outputPath = params.outputPath ?? `alfe-tts-${String(Date.now())}.${format}`;
|
|
215
|
-
const absolutePath = resolveWorkspacePath(outputPath);
|
|
216
|
-
await writeFile(absolutePath, bytes);
|
|
217
|
-
return {
|
|
218
|
-
path: outputPath,
|
|
219
|
-
absolutePath,
|
|
220
|
-
format,
|
|
221
|
-
sampleRate: result.sampleRate,
|
|
222
|
-
channels: result.channels,
|
|
223
|
-
bitDepth: result.bitDepth,
|
|
224
|
-
bytes: bytes.length,
|
|
225
|
-
characters: text.length
|
|
226
|
-
};
|
|
227
|
-
}
|
|
135
|
+
const voiceTools = [defineTool({
|
|
136
|
+
name: "voice_tts",
|
|
137
|
+
description: "Convert text to speech using the Alfe voice service (ElevenLabs). Writes a playable audio file to the workspace and returns its path. Billed per character to your tenant credit pool.",
|
|
138
|
+
parameters: Type.Object({
|
|
139
|
+
text: Type.String({ description: "Text to synthesize (1–5000 characters)." }),
|
|
140
|
+
outputPath: Type.Optional(Type.String({ description: "Where to write the audio file (absolute, or relative to the working directory). Defaults to alfe-tts-<timestamp>.wav in the working directory." })),
|
|
141
|
+
format: Type.Optional(Type.Union([Type.Literal("wav"), Type.Literal("pcm")], { description: "Output container. 'wav' (default) is a playable file; 'pcm' is headerless 24kHz/mono/16-bit raw PCM." })),
|
|
142
|
+
voiceId: Type.Optional(Type.String({ description: "ElevenLabs voice ID. Platform default when omitted." })),
|
|
143
|
+
model: Type.Optional(Type.Union([Type.Literal("eleven_turbo_v2_5"), Type.Literal("eleven_multilingual_v2")], { description: "TTS model. eleven_turbo_v2_5 (lower latency) when omitted." }))
|
|
228
144
|
}),
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
}),
|
|
263
|
-
defineTool({
|
|
264
|
-
name: "voice_transfer",
|
|
265
|
-
description: "Transfer the current phone call to another number. Only works for Twilio calls.",
|
|
266
|
-
parameters: Type.Object({
|
|
267
|
-
targetNumber: Type.String({ description: "Phone number to transfer to (E.164 format)" }),
|
|
268
|
-
sessionId: Type.Optional(Type.String({ description: "Voice session ID." }))
|
|
269
|
-
}),
|
|
270
|
-
handler: () => {
|
|
271
|
-
throw new Error("voice_transfer requires the Twilio service. The voice service no longer manages phone calls directly.");
|
|
272
|
-
}
|
|
145
|
+
handler: async (params) => {
|
|
146
|
+
const text = params.text;
|
|
147
|
+
const format = params.format ?? "wav";
|
|
148
|
+
const result = await getVoiceClient().tts({
|
|
149
|
+
text,
|
|
150
|
+
voiceId: params.voiceId,
|
|
151
|
+
model: params.model
|
|
152
|
+
});
|
|
153
|
+
const bytes = format === "wav" ? pcmToWav(result.audio, {
|
|
154
|
+
sampleRate: result.sampleRate,
|
|
155
|
+
channels: result.channels,
|
|
156
|
+
bitDepth: result.bitDepth
|
|
157
|
+
}) : result.audio;
|
|
158
|
+
const outputPath = params.outputPath ?? `alfe-tts-${String(Date.now())}.${format}`;
|
|
159
|
+
const absolutePath = resolveWorkspacePath(outputPath);
|
|
160
|
+
await writeFile(absolutePath, bytes);
|
|
161
|
+
return {
|
|
162
|
+
path: outputPath,
|
|
163
|
+
absolutePath,
|
|
164
|
+
format,
|
|
165
|
+
sampleRate: result.sampleRate,
|
|
166
|
+
channels: result.channels,
|
|
167
|
+
bitDepth: result.bitDepth,
|
|
168
|
+
bytes: bytes.length,
|
|
169
|
+
characters: text.length
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}), defineTool({
|
|
173
|
+
name: "voice_stt",
|
|
174
|
+
description: "Transcribe an audio file to text using the Alfe voice service (Deepgram). Accepts a WAV file or headerless mono 16-bit PCM. Billed per audio-second to your tenant credit pool.",
|
|
175
|
+
parameters: Type.Object({
|
|
176
|
+
path: Type.String({ description: "Path to the audio file (absolute, or relative to the working directory). WAV or raw linear16 mono PCM." }),
|
|
177
|
+
sampleRate: Type.Optional(Type.Number({ description: "Sample rate in Hz (8000–48000). Only used for headerless PCM input; ignored for WAV (its header wins). Defaults to 24000." }))
|
|
273
178
|
}),
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
179
|
+
handler: async (params) => {
|
|
180
|
+
const raw = await readFile(resolveWorkspacePath(params.path));
|
|
181
|
+
const wav = parseWav(raw);
|
|
182
|
+
let audio;
|
|
183
|
+
let sampleRate;
|
|
184
|
+
if (wav) {
|
|
185
|
+
if (wav.bitDepth !== 16 || wav.channels !== 1) throw new Error(`voice_stt needs 16-bit mono audio; got ${String(wav.bitDepth)}-bit / ${String(wav.channels)}-channel WAV. Re-export as 16-bit mono.`);
|
|
186
|
+
audio = wav.pcm;
|
|
187
|
+
sampleRate = wav.sampleRate;
|
|
188
|
+
} else {
|
|
189
|
+
audio = raw;
|
|
190
|
+
sampleRate = params.sampleRate ?? 24e3;
|
|
283
191
|
}
|
|
284
|
-
|
|
285
|
-
|
|
192
|
+
return getVoiceClient().stt({
|
|
193
|
+
audio,
|
|
194
|
+
sampleRate
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
})];
|
|
286
198
|
const plugin = {
|
|
287
199
|
id: "@alfe.ai/openclaw-voice",
|
|
288
200
|
name: "Alfe Voice Plugin",
|
|
@@ -296,27 +208,27 @@ const plugin = {
|
|
|
296
208
|
const fullConfig = api.config ?? {};
|
|
297
209
|
const pluginConfig = fullConfig.plugins?.entries?.["@alfe.ai/openclaw-voice"]?.config ?? fullConfig.plugins?.entries?.["voice-gateway"]?.config ?? {};
|
|
298
210
|
const startVoiceService = () => {
|
|
299
|
-
|
|
300
|
-
log.
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
211
|
+
guardedStart(VOICE_ACTIVATION_KEY, log, () => {
|
|
212
|
+
log.info("Alfe Voice plugin activating...");
|
|
213
|
+
voiceServiceUrl = pluginConfig.voiceServiceUrl ?? `http://localhost:${String(pluginConfig.voiceServicePort ?? "3100")}`;
|
|
214
|
+
voiceServiceApiKey = pluginConfig.voiceServiceApiKey ?? "";
|
|
215
|
+
log.info(`Voice service: ${voiceServiceUrl}`);
|
|
216
|
+
let alfeConfig = null;
|
|
217
|
+
try {
|
|
218
|
+
alfeConfig = resolveConfig();
|
|
219
|
+
} catch {}
|
|
220
|
+
connectToDaemon(pluginConfig.daemonSocket ?? alfeConfig?.socketPath ?? DEFAULT_SOCKET_PATH, log, {
|
|
221
|
+
pluginId: "@alfe.ai/openclaw-voice",
|
|
222
|
+
capabilities: VOICE_CAPABILITIES,
|
|
223
|
+
standaloneNote: "Alfe daemon not available — voice plugin running standalone"
|
|
224
|
+
}).then((client) => {
|
|
225
|
+
daemonIpcClient = client;
|
|
226
|
+
}).catch((err) => {
|
|
227
|
+
log.debug(`Daemon connect failed: ${err.message}`);
|
|
228
|
+
});
|
|
316
229
|
});
|
|
317
230
|
};
|
|
318
231
|
const stopVoiceService = () => {
|
|
319
|
-
globalThis.__voiceGatewayActivated = false;
|
|
320
232
|
if (daemonIpcClient) {
|
|
321
233
|
try {
|
|
322
234
|
daemonIpcClient.stop();
|
|
@@ -326,6 +238,7 @@ const plugin = {
|
|
|
326
238
|
}
|
|
327
239
|
daemonIpcClient = null;
|
|
328
240
|
}
|
|
241
|
+
resetActivation(VOICE_ACTIVATION_KEY);
|
|
329
242
|
log.info("Alfe Voice plugin deactivated");
|
|
330
243
|
};
|
|
331
244
|
api.registerGatewayMethod("voice.speak", async (...args) => {
|
|
@@ -351,7 +264,6 @@ const plugin = {
|
|
|
351
264
|
log.info("Alfe Voice plugin activated");
|
|
352
265
|
},
|
|
353
266
|
deactivate(api) {
|
|
354
|
-
globalThis.__voiceGatewayActivated = false;
|
|
355
267
|
const log = api.logger;
|
|
356
268
|
log.info("Alfe Voice plugin deactivating...");
|
|
357
269
|
if (daemonIpcClient) {
|
|
@@ -363,6 +275,7 @@ const plugin = {
|
|
|
363
275
|
}
|
|
364
276
|
daemonIpcClient = null;
|
|
365
277
|
}
|
|
278
|
+
resetActivation(VOICE_ACTIVATION_KEY);
|
|
366
279
|
log.info("Alfe Voice plugin deactivated");
|
|
367
280
|
}
|
|
368
281
|
};
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfe.ai/openclaw-voice",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "OpenClaw voice plugin for Alfe — Discord audio, Twilio, Recall.ai",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -28,8 +28,9 @@
|
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@sinclair/typebox": "^0.34.48",
|
|
31
|
-
"@alfe.ai/agent-api-client": "0.
|
|
32
|
-
"@alfe.ai/config": "0.
|
|
31
|
+
"@alfe.ai/agent-api-client": "0.14.0",
|
|
32
|
+
"@alfe.ai/config": "0.4.0",
|
|
33
|
+
"@alfe.ai/openclaw-plugin-kit": "0.1.0"
|
|
33
34
|
},
|
|
34
35
|
"peerDependencies": {
|
|
35
36
|
"openclaw": ">=2026.3.0"
|