@zenzap-co/openclaw-plugin 0.1.2 → 0.1.4-dev.098f97b
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.js +59 -14
- package/package.json +1 -1
- package/skills/zenzap/SKILL.md +7 -6
- package/dist/index.d.ts +0 -15
- package/dist/listener.d.ts +0 -97
- package/dist/listener.js +0 -589
- package/dist/poller.d.ts +0 -24
- package/dist/poller.js +0 -124
- package/dist/tools.d.ts +0 -779
- package/dist/tools.js +0 -401
- package/dist/transcription.d.ts +0 -23
- package/dist/transcription.js +0 -163
package/dist/tools.js
DELETED
|
@@ -1,401 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenClaw Tools for Zenzap
|
|
3
|
-
*/
|
|
4
|
-
import { getClient } from '@zenzap-co/sdk';
|
|
5
|
-
export const tools = [
|
|
6
|
-
{
|
|
7
|
-
id: 'zenzap_get_me',
|
|
8
|
-
name: 'Get My Profile',
|
|
9
|
-
description: 'Get your own bot profile: name, member ID, and status. Use this to confirm your identity or refresh your own details.',
|
|
10
|
-
inputSchema: {
|
|
11
|
-
type: 'object',
|
|
12
|
-
properties: {},
|
|
13
|
-
required: [],
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
id: 'zenzap_send_message',
|
|
18
|
-
name: 'Send Zenzap Message',
|
|
19
|
-
description: 'Send a text message to a Zenzap topic',
|
|
20
|
-
inputSchema: {
|
|
21
|
-
type: 'object',
|
|
22
|
-
properties: {
|
|
23
|
-
topicId: { type: 'string', description: 'UUID of the target topic' },
|
|
24
|
-
text: { type: 'string', description: 'Message text (max 10000 characters)' },
|
|
25
|
-
},
|
|
26
|
-
required: ['topicId', 'text'],
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
id: 'zenzap_send_image',
|
|
31
|
-
name: 'Send Zenzap Image',
|
|
32
|
-
description: 'Send an image to a Zenzap topic using either a URL or base64 data, with optional caption',
|
|
33
|
-
inputSchema: {
|
|
34
|
-
type: 'object',
|
|
35
|
-
properties: {
|
|
36
|
-
topicId: { type: 'string', description: 'UUID of the target topic' },
|
|
37
|
-
imageUrl: { type: 'string', description: 'Public or signed URL to the image to upload. Use either imageUrl or imageBase64.' },
|
|
38
|
-
imageBase64: { type: 'string', description: 'Base64-encoded image data (raw base64 or data URI). Use either imageBase64 or imageUrl.' },
|
|
39
|
-
mimeType: { type: 'string', description: 'Optional MIME type for imageBase64 payloads (e.g. image/png)' },
|
|
40
|
-
caption: { type: 'string', description: 'Optional caption for the image' },
|
|
41
|
-
externalId: { type: 'string', description: 'Optional external ID for idempotency/tracking' },
|
|
42
|
-
fileName: { type: 'string', description: 'Optional override for uploaded filename' },
|
|
43
|
-
},
|
|
44
|
-
required: ['topicId'],
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
id: 'zenzap_create_topic',
|
|
49
|
-
name: 'Create Zenzap Topic',
|
|
50
|
-
description: 'Create a new topic (group chat) in Zenzap with specified members',
|
|
51
|
-
inputSchema: {
|
|
52
|
-
type: 'object',
|
|
53
|
-
properties: {
|
|
54
|
-
name: { type: 'string', description: 'Topic name (max 64 characters)' },
|
|
55
|
-
members: {
|
|
56
|
-
type: 'array',
|
|
57
|
-
items: { type: 'string' },
|
|
58
|
-
description: 'Array of member UUIDs to add',
|
|
59
|
-
},
|
|
60
|
-
description: { type: 'string', description: 'Optional topic description' },
|
|
61
|
-
externalId: { type: 'string', description: 'Optional external ID (unique per bot)' },
|
|
62
|
-
},
|
|
63
|
-
required: ['name', 'members'],
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
id: 'zenzap_get_topic',
|
|
68
|
-
name: 'Get Zenzap Topic',
|
|
69
|
-
description: 'Get details of a topic including its name, description, and member list',
|
|
70
|
-
inputSchema: {
|
|
71
|
-
type: 'object',
|
|
72
|
-
properties: {
|
|
73
|
-
topicId: { type: 'string', description: 'UUID of the topic' },
|
|
74
|
-
},
|
|
75
|
-
required: ['topicId'],
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
id: 'zenzap_update_topic',
|
|
80
|
-
name: 'Update Zenzap Topic',
|
|
81
|
-
description: 'Update a topic name and/or description',
|
|
82
|
-
inputSchema: {
|
|
83
|
-
type: 'object',
|
|
84
|
-
properties: {
|
|
85
|
-
topicId: { type: 'string', description: 'UUID of the topic to update' },
|
|
86
|
-
name: { type: 'string', description: 'New topic name (max 64 characters)' },
|
|
87
|
-
description: { type: 'string', description: 'New topic description' },
|
|
88
|
-
},
|
|
89
|
-
required: ['topicId'],
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
id: 'zenzap_add_members',
|
|
94
|
-
name: 'Add Members to Zenzap Topic',
|
|
95
|
-
description: 'Add members to a topic (max 5 per call). Members must exist in the organization.',
|
|
96
|
-
inputSchema: {
|
|
97
|
-
type: 'object',
|
|
98
|
-
properties: {
|
|
99
|
-
topicId: { type: 'string', description: 'UUID of the topic' },
|
|
100
|
-
members: {
|
|
101
|
-
type: 'array',
|
|
102
|
-
items: { type: 'string' },
|
|
103
|
-
description: 'Array of member UUIDs to add (max 5)',
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
required: ['topicId', 'members'],
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
id: 'zenzap_remove_members',
|
|
111
|
-
name: 'Remove Members from Zenzap Topic',
|
|
112
|
-
description: 'Remove members from a topic (max 5 per call)',
|
|
113
|
-
inputSchema: {
|
|
114
|
-
type: 'object',
|
|
115
|
-
properties: {
|
|
116
|
-
topicId: { type: 'string', description: 'UUID of the topic' },
|
|
117
|
-
members: {
|
|
118
|
-
type: 'array',
|
|
119
|
-
items: { type: 'string' },
|
|
120
|
-
description: 'Array of member UUIDs to remove (max 5)',
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
required: ['topicId', 'members'],
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
id: 'zenzap_get_member',
|
|
128
|
-
name: 'Get Zenzap Member',
|
|
129
|
-
description: 'Look up a member by their ID to get their name, email, and type (user/bot). Use this to resolve who sent a message when you only have their member ID.',
|
|
130
|
-
inputSchema: {
|
|
131
|
-
type: 'object',
|
|
132
|
-
properties: {
|
|
133
|
-
memberId: { type: 'string', description: 'Member UUID (e.g. the senderId from a message)' },
|
|
134
|
-
},
|
|
135
|
-
required: ['memberId'],
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
id: 'zenzap_list_members',
|
|
140
|
-
name: 'List Zenzap Members',
|
|
141
|
-
description: 'List or search members in the organization. Use this to discover who is in the workspace — returns name, ID, email, and type for each member.',
|
|
142
|
-
inputSchema: {
|
|
143
|
-
type: 'object',
|
|
144
|
-
properties: {
|
|
145
|
-
limit: { type: 'number', description: 'Max members to return (default: 50)' },
|
|
146
|
-
cursor: { type: 'string', description: 'Pagination cursor from a previous response' },
|
|
147
|
-
emails: {
|
|
148
|
-
oneOf: [
|
|
149
|
-
{ type: 'string' },
|
|
150
|
-
{ type: 'array', items: { type: 'string' } },
|
|
151
|
-
],
|
|
152
|
-
description: 'Filter by one or more email addresses. Accepts comma-separated string or string array.',
|
|
153
|
-
},
|
|
154
|
-
email: { type: 'string', description: 'Deprecated alias for emails (single address).' },
|
|
155
|
-
},
|
|
156
|
-
},
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
id: 'zenzap_list_topics',
|
|
160
|
-
name: 'List Zenzap Topics',
|
|
161
|
-
description: 'List all topics the bot is a member of',
|
|
162
|
-
inputSchema: {
|
|
163
|
-
type: 'object',
|
|
164
|
-
properties: {
|
|
165
|
-
limit: { type: 'number', description: 'Max topics to return (default: 50)' },
|
|
166
|
-
cursor: { type: 'string', description: 'Pagination cursor from a previous response' },
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
id: 'zenzap_list_tasks',
|
|
172
|
-
name: 'List Zenzap Tasks',
|
|
173
|
-
description: 'List tasks the bot can access, optionally filtered by topic, status, or assignee. Use this before updating tasks.',
|
|
174
|
-
inputSchema: {
|
|
175
|
-
type: 'object',
|
|
176
|
-
properties: {
|
|
177
|
-
topicId: { type: 'string', description: 'Optional topic UUID to list tasks from a single topic' },
|
|
178
|
-
status: { type: 'string', enum: ['Open', 'Done'], description: 'Optional task status filter' },
|
|
179
|
-
assignee: {
|
|
180
|
-
type: 'string',
|
|
181
|
-
description: 'Optional assignee member UUID. Use empty string ("") to list unassigned tasks.',
|
|
182
|
-
},
|
|
183
|
-
limit: { type: 'number', description: 'Max tasks to return (default: 50, max: 100)' },
|
|
184
|
-
cursor: { type: 'string', description: 'Pagination cursor from a previous response' },
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
{
|
|
189
|
-
id: 'zenzap_get_task',
|
|
190
|
-
name: 'Get Zenzap Task',
|
|
191
|
-
description: 'Get full details for a specific task by ID',
|
|
192
|
-
inputSchema: {
|
|
193
|
-
type: 'object',
|
|
194
|
-
properties: {
|
|
195
|
-
taskId: { type: 'string', description: 'UUID of the task' },
|
|
196
|
-
},
|
|
197
|
-
required: ['taskId'],
|
|
198
|
-
},
|
|
199
|
-
},
|
|
200
|
-
{
|
|
201
|
-
id: 'zenzap_create_task',
|
|
202
|
-
name: 'Create Zenzap Task',
|
|
203
|
-
description: 'Create a task in a Zenzap topic with optional assignee and due date',
|
|
204
|
-
inputSchema: {
|
|
205
|
-
type: 'object',
|
|
206
|
-
properties: {
|
|
207
|
-
topicId: { type: 'string', description: 'UUID of the topic to create the task in' },
|
|
208
|
-
title: { type: 'string', description: 'Task title (max 256 characters)' },
|
|
209
|
-
description: { type: 'string', description: 'Task description (max 10000 characters)' },
|
|
210
|
-
assignee: { type: 'string', description: 'Member UUID to assign (must be a topic member)' },
|
|
211
|
-
assignees: {
|
|
212
|
-
type: 'array',
|
|
213
|
-
items: { type: 'string' },
|
|
214
|
-
description: 'Deprecated: if provided, first member UUID will be used as assignee',
|
|
215
|
-
},
|
|
216
|
-
dueDate: {
|
|
217
|
-
type: 'number',
|
|
218
|
-
description: 'Due date as Unix timestamp in milliseconds (e.g. Date.now() + 86400000 for tomorrow)',
|
|
219
|
-
},
|
|
220
|
-
},
|
|
221
|
-
required: ['topicId', 'title'],
|
|
222
|
-
},
|
|
223
|
-
},
|
|
224
|
-
{
|
|
225
|
-
id: 'zenzap_update_task',
|
|
226
|
-
name: 'Update Zenzap Task',
|
|
227
|
-
description: 'Update task fields: rename, description, assignee/unassign, or status (Done/Open)',
|
|
228
|
-
inputSchema: {
|
|
229
|
-
type: 'object',
|
|
230
|
-
properties: {
|
|
231
|
-
taskId: { type: 'string', description: 'UUID of the task to update' },
|
|
232
|
-
topicId: {
|
|
233
|
-
type: 'string',
|
|
234
|
-
description: 'Topic UUID. Required when changing status (Done/Open).',
|
|
235
|
-
},
|
|
236
|
-
name: { type: 'string', description: 'New task title (alias of title). Use either name OR title.' },
|
|
237
|
-
title: { type: 'string', description: 'New task title. Use either title OR name.' },
|
|
238
|
-
description: { type: 'string', description: 'New task description' },
|
|
239
|
-
assignee: {
|
|
240
|
-
type: 'string',
|
|
241
|
-
description: 'Assignee member UUID. Use empty string ("") to unassign.',
|
|
242
|
-
},
|
|
243
|
-
dueDate: {
|
|
244
|
-
type: 'number',
|
|
245
|
-
description: 'Due date as Unix timestamp in milliseconds. Set to 0 to clear the due date.',
|
|
246
|
-
},
|
|
247
|
-
status: {
|
|
248
|
-
type: 'string',
|
|
249
|
-
enum: ['Open', 'Done'],
|
|
250
|
-
description: 'Set to Done to close task, Open to reopen task',
|
|
251
|
-
},
|
|
252
|
-
},
|
|
253
|
-
required: ['taskId'],
|
|
254
|
-
},
|
|
255
|
-
},
|
|
256
|
-
{
|
|
257
|
-
id: 'zenzap_get_messages',
|
|
258
|
-
name: 'Get Zenzap Topic Messages',
|
|
259
|
-
description: 'Fetch message history from a topic. Useful for catching up on what was discussed, summarizing a conversation, or finding a specific message.',
|
|
260
|
-
inputSchema: {
|
|
261
|
-
type: 'object',
|
|
262
|
-
properties: {
|
|
263
|
-
topicId: { type: 'string', description: 'UUID of the topic' },
|
|
264
|
-
limit: {
|
|
265
|
-
type: 'number',
|
|
266
|
-
description: 'Number of messages to fetch (default: 30, max: 100)',
|
|
267
|
-
},
|
|
268
|
-
order: {
|
|
269
|
-
type: 'string',
|
|
270
|
-
enum: ['asc', 'desc'],
|
|
271
|
-
description: 'asc = oldest first, desc = newest first (default: desc)',
|
|
272
|
-
},
|
|
273
|
-
before: { type: 'number', description: 'Fetch messages before this Unix timestamp (ms)' },
|
|
274
|
-
after: { type: 'number', description: 'Fetch messages after this Unix timestamp (ms)' },
|
|
275
|
-
cursor: { type: 'string', description: 'Pagination cursor from a previous response' },
|
|
276
|
-
},
|
|
277
|
-
required: ['topicId'],
|
|
278
|
-
},
|
|
279
|
-
},
|
|
280
|
-
{
|
|
281
|
-
id: 'zenzap_react',
|
|
282
|
-
name: 'React to Zenzap Message',
|
|
283
|
-
description: 'Add an emoji reaction to a message. Use this instead of a text reply when you have completed a simple action and have nothing more to say (e.g. task created, member added). Prefer ✅ for success.',
|
|
284
|
-
inputSchema: {
|
|
285
|
-
type: 'object',
|
|
286
|
-
properties: {
|
|
287
|
-
messageId: { type: 'string', description: 'UUID of the message to react to' },
|
|
288
|
-
reaction: { type: 'string', description: 'Emoji to react with (e.g. ✅, 👍, ❤️, 👀)' },
|
|
289
|
-
},
|
|
290
|
-
required: ['messageId', 'reaction'],
|
|
291
|
-
},
|
|
292
|
-
},
|
|
293
|
-
];
|
|
294
|
-
export async function executeTool(toolId, input) {
|
|
295
|
-
const client = getClient();
|
|
296
|
-
switch (toolId) {
|
|
297
|
-
case 'zenzap_get_me':
|
|
298
|
-
return client.getCurrentMember();
|
|
299
|
-
case 'zenzap_send_message':
|
|
300
|
-
return client.sendMessage({ topicId: input.topicId, text: input.text });
|
|
301
|
-
case 'zenzap_send_image': {
|
|
302
|
-
const hasImageUrl = typeof input.imageUrl === 'string' && input.imageUrl.trim().length > 0;
|
|
303
|
-
const hasImageBase64 = typeof input.imageBase64 === 'string' && input.imageBase64.trim().length > 0;
|
|
304
|
-
if (hasImageUrl === hasImageBase64) {
|
|
305
|
-
throw new Error('Provide exactly one of imageUrl or imageBase64.');
|
|
306
|
-
}
|
|
307
|
-
return client.sendImageMessage({
|
|
308
|
-
topicId: input.topicId,
|
|
309
|
-
imageUrl: hasImageUrl ? input.imageUrl : undefined,
|
|
310
|
-
imageBase64: hasImageBase64 ? input.imageBase64 : undefined,
|
|
311
|
-
mimeType: input.mimeType,
|
|
312
|
-
caption: input.caption,
|
|
313
|
-
externalId: input.externalId,
|
|
314
|
-
fileName: input.fileName,
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
case 'zenzap_create_topic':
|
|
318
|
-
return client.createTopic({
|
|
319
|
-
name: input.name,
|
|
320
|
-
members: input.members,
|
|
321
|
-
description: input.description,
|
|
322
|
-
externalId: input.externalId,
|
|
323
|
-
});
|
|
324
|
-
case 'zenzap_get_topic':
|
|
325
|
-
return client.getTopicDetails(input.topicId);
|
|
326
|
-
case 'zenzap_update_topic':
|
|
327
|
-
return client.updateTopic(input.topicId, {
|
|
328
|
-
name: input.name,
|
|
329
|
-
description: input.description,
|
|
330
|
-
});
|
|
331
|
-
case 'zenzap_add_members':
|
|
332
|
-
return client.addMembersToTopic(input.topicId, input.members);
|
|
333
|
-
case 'zenzap_remove_members':
|
|
334
|
-
return client.removeMembersFromTopic(input.topicId, input.members);
|
|
335
|
-
case 'zenzap_get_member':
|
|
336
|
-
return client.getMember(input.memberId);
|
|
337
|
-
case 'zenzap_list_members':
|
|
338
|
-
return client.listMembers({
|
|
339
|
-
limit: input.limit || 50,
|
|
340
|
-
cursor: input.cursor,
|
|
341
|
-
emails: input.emails ?? input.email,
|
|
342
|
-
});
|
|
343
|
-
case 'zenzap_list_topics':
|
|
344
|
-
return client.listTopics({ limit: input.limit || 50, cursor: input.cursor });
|
|
345
|
-
case 'zenzap_list_tasks':
|
|
346
|
-
return client.listTasks({
|
|
347
|
-
topicId: input.topicId,
|
|
348
|
-
status: input.status,
|
|
349
|
-
assignee: input.assignee,
|
|
350
|
-
limit: input.limit || 50,
|
|
351
|
-
cursor: input.cursor,
|
|
352
|
-
});
|
|
353
|
-
case 'zenzap_get_task':
|
|
354
|
-
return client.getTask(input.taskId);
|
|
355
|
-
case 'zenzap_get_messages':
|
|
356
|
-
return client.getTopicMessages(input.topicId, {
|
|
357
|
-
limit: input.limit,
|
|
358
|
-
order: input.order,
|
|
359
|
-
before: input.before,
|
|
360
|
-
after: input.after,
|
|
361
|
-
cursor: input.cursor,
|
|
362
|
-
});
|
|
363
|
-
case 'zenzap_react':
|
|
364
|
-
return client.addReaction(input.messageId, input.reaction);
|
|
365
|
-
case 'zenzap_create_task':
|
|
366
|
-
return client.createTask({
|
|
367
|
-
topicId: input.topicId,
|
|
368
|
-
title: input.title,
|
|
369
|
-
description: input.description,
|
|
370
|
-
assignee: input.assignee ?? (Array.isArray(input.assignees) ? input.assignees[0] : undefined),
|
|
371
|
-
dueDate: input.dueDate,
|
|
372
|
-
});
|
|
373
|
-
case 'zenzap_update_task': {
|
|
374
|
-
if (input.name !== undefined && input.title !== undefined) {
|
|
375
|
-
throw new Error('Provide either name or title, not both.');
|
|
376
|
-
}
|
|
377
|
-
if (input.name === undefined &&
|
|
378
|
-
input.title === undefined &&
|
|
379
|
-
input.description === undefined &&
|
|
380
|
-
input.assignee === undefined &&
|
|
381
|
-
input.dueDate === undefined &&
|
|
382
|
-
input.status === undefined) {
|
|
383
|
-
throw new Error('At least one field must be provided: name/title, description, assignee, dueDate, or status.');
|
|
384
|
-
}
|
|
385
|
-
if (input.status !== undefined && !input.topicId) {
|
|
386
|
-
throw new Error('topicId is required when updating task status.');
|
|
387
|
-
}
|
|
388
|
-
return client.updateTask(input.taskId, {
|
|
389
|
-
topicId: input.topicId,
|
|
390
|
-
name: input.name,
|
|
391
|
-
title: input.title,
|
|
392
|
-
description: input.description,
|
|
393
|
-
assignee: input.assignee,
|
|
394
|
-
dueDate: input.dueDate,
|
|
395
|
-
status: input.status,
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
default:
|
|
399
|
-
throw new Error(`Unknown tool: ${toolId}`);
|
|
400
|
-
}
|
|
401
|
-
}
|
package/dist/transcription.d.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
export interface AudioAttachmentLike {
|
|
2
|
-
id?: string;
|
|
3
|
-
name?: string;
|
|
4
|
-
url?: string;
|
|
5
|
-
transcription?: {
|
|
6
|
-
status?: string;
|
|
7
|
-
text?: string;
|
|
8
|
-
};
|
|
9
|
-
}
|
|
10
|
-
export interface AudioTranscriptionContext {
|
|
11
|
-
topicId: string;
|
|
12
|
-
messageId?: string;
|
|
13
|
-
senderId?: string;
|
|
14
|
-
}
|
|
15
|
-
export type AudioTranscriber = (attachment: AudioAttachmentLike, ctx: AudioTranscriptionContext) => Promise<string | null>;
|
|
16
|
-
export interface WhisperAudioTranscriberOptions {
|
|
17
|
-
enabled?: boolean;
|
|
18
|
-
model?: string;
|
|
19
|
-
language?: string;
|
|
20
|
-
timeoutMs?: number;
|
|
21
|
-
maxBytes?: number;
|
|
22
|
-
}
|
|
23
|
-
export declare function createWhisperAudioTranscriber(options?: WhisperAudioTranscriberOptions): AudioTranscriber;
|
package/dist/transcription.js
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'crypto';
|
|
2
|
-
import { spawn } from 'child_process';
|
|
3
|
-
import { tmpdir } from 'os';
|
|
4
|
-
import { extname, join } from 'path';
|
|
5
|
-
import { mkdtemp, readdir, readFile, rm, writeFile } from 'fs/promises';
|
|
6
|
-
const DEFAULT_MAX_BYTES = 30 * 1024 * 1024;
|
|
7
|
-
const DEFAULT_TIMEOUT_MS = 3 * 60 * 1000;
|
|
8
|
-
function inferExtension(nameOrUrl) {
|
|
9
|
-
if (!nameOrUrl)
|
|
10
|
-
return '.audio';
|
|
11
|
-
try {
|
|
12
|
-
const maybeUrl = new URL(nameOrUrl);
|
|
13
|
-
const ext = extname(maybeUrl.pathname || '');
|
|
14
|
-
if (ext && ext.length <= 10)
|
|
15
|
-
return ext;
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
// not a URL, fall through
|
|
19
|
-
}
|
|
20
|
-
const ext = extname(nameOrUrl);
|
|
21
|
-
if (ext && ext.length <= 10)
|
|
22
|
-
return ext;
|
|
23
|
-
return '.audio';
|
|
24
|
-
}
|
|
25
|
-
async function runCommand(command, args, timeoutMs) {
|
|
26
|
-
return new Promise((resolve) => {
|
|
27
|
-
const child = spawn(command, args, { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
28
|
-
let stderr = '';
|
|
29
|
-
let settled = false;
|
|
30
|
-
let timedOut = false;
|
|
31
|
-
const timer = setTimeout(() => {
|
|
32
|
-
timedOut = true;
|
|
33
|
-
child.kill('SIGKILL');
|
|
34
|
-
}, timeoutMs);
|
|
35
|
-
child.stderr.on('data', (chunk) => {
|
|
36
|
-
stderr += chunk.toString();
|
|
37
|
-
});
|
|
38
|
-
child.on('error', (err) => {
|
|
39
|
-
if (settled)
|
|
40
|
-
return;
|
|
41
|
-
settled = true;
|
|
42
|
-
clearTimeout(timer);
|
|
43
|
-
resolve({
|
|
44
|
-
ok: false,
|
|
45
|
-
code: null,
|
|
46
|
-
notFound: err?.code === 'ENOENT',
|
|
47
|
-
stderr: err?.message ?? String(err),
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
child.on('close', (code) => {
|
|
51
|
-
if (settled)
|
|
52
|
-
return;
|
|
53
|
-
settled = true;
|
|
54
|
-
clearTimeout(timer);
|
|
55
|
-
resolve({
|
|
56
|
-
ok: code === 0 && !timedOut,
|
|
57
|
-
code,
|
|
58
|
-
notFound: false,
|
|
59
|
-
stderr: timedOut ? `${stderr}\ncommand timed out` : stderr,
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
async function fetchAttachmentBytes(url, maxBytes) {
|
|
65
|
-
const res = await fetch(url, { signal: AbortSignal.timeout(30000) });
|
|
66
|
-
if (!res.ok) {
|
|
67
|
-
throw new Error(`download failed: HTTP ${res.status}`);
|
|
68
|
-
}
|
|
69
|
-
const contentLength = Number(res.headers.get('content-length') || 0);
|
|
70
|
-
if (contentLength > 0 && contentLength > maxBytes) {
|
|
71
|
-
throw new Error(`attachment too large (${contentLength} bytes > ${maxBytes})`);
|
|
72
|
-
}
|
|
73
|
-
const body = new Uint8Array(await res.arrayBuffer());
|
|
74
|
-
if (body.byteLength > maxBytes) {
|
|
75
|
-
throw new Error(`attachment too large (${body.byteLength} bytes > ${maxBytes})`);
|
|
76
|
-
}
|
|
77
|
-
return body;
|
|
78
|
-
}
|
|
79
|
-
async function readTranscriptionText(outputDir) {
|
|
80
|
-
const files = await readdir(outputDir);
|
|
81
|
-
const txtFiles = files.filter((f) => f.endsWith('.txt'));
|
|
82
|
-
if (!txtFiles.length)
|
|
83
|
-
return null;
|
|
84
|
-
let best = '';
|
|
85
|
-
for (const file of txtFiles) {
|
|
86
|
-
const data = await readFile(join(outputDir, file), 'utf8');
|
|
87
|
-
if (data.trim().length > best.trim().length)
|
|
88
|
-
best = data;
|
|
89
|
-
}
|
|
90
|
-
const cleaned = best.trim();
|
|
91
|
-
return cleaned || null;
|
|
92
|
-
}
|
|
93
|
-
export function createWhisperAudioTranscriber(options = {}) {
|
|
94
|
-
const enabled = options.enabled ?? true;
|
|
95
|
-
const model = options.model || 'base';
|
|
96
|
-
const language = options.language || 'en';
|
|
97
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
98
|
-
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
99
|
-
let warnedMissingBinary = false;
|
|
100
|
-
return async (attachment, ctx) => {
|
|
101
|
-
if (!enabled)
|
|
102
|
-
return null;
|
|
103
|
-
if (!attachment?.url)
|
|
104
|
-
return null;
|
|
105
|
-
const ext = inferExtension(attachment.name || attachment.url);
|
|
106
|
-
const contextKey = `${ctx.topicId}:${ctx.messageId || attachment.id || 'audio'}`;
|
|
107
|
-
const trace = createHash('sha1').update(contextKey).digest('hex').slice(0, 8);
|
|
108
|
-
const workDir = await mkdtemp(join(tmpdir(), 'zenzap-whisper-'));
|
|
109
|
-
const inputPath = join(workDir, `input${ext}`);
|
|
110
|
-
try {
|
|
111
|
-
const bytes = await fetchAttachmentBytes(attachment.url, maxBytes);
|
|
112
|
-
await writeFile(inputPath, bytes);
|
|
113
|
-
const baseArgs = [
|
|
114
|
-
inputPath,
|
|
115
|
-
'--model',
|
|
116
|
-
model,
|
|
117
|
-
'--task',
|
|
118
|
-
'transcribe',
|
|
119
|
-
'--output_format',
|
|
120
|
-
'txt',
|
|
121
|
-
'--output_dir',
|
|
122
|
-
workDir,
|
|
123
|
-
'--language',
|
|
124
|
-
language,
|
|
125
|
-
];
|
|
126
|
-
const candidates = [
|
|
127
|
-
{ command: 'whisper', args: baseArgs },
|
|
128
|
-
{ command: 'python3', args: ['-m', 'whisper', ...baseArgs] },
|
|
129
|
-
];
|
|
130
|
-
let lastErr = '';
|
|
131
|
-
for (const candidate of candidates) {
|
|
132
|
-
const result = await runCommand(candidate.command, candidate.args, timeoutMs);
|
|
133
|
-
if (result.notFound) {
|
|
134
|
-
lastErr = `${candidate.command}: command not found`;
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (!result.ok) {
|
|
138
|
-
lastErr = `${candidate.command} exited with code ${result.code}: ${result.stderr.trim()}`;
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
const transcript = await readTranscriptionText(workDir);
|
|
142
|
-
if (transcript)
|
|
143
|
-
return transcript;
|
|
144
|
-
lastErr = `${candidate.command}: no transcript file produced`;
|
|
145
|
-
}
|
|
146
|
-
if (!warnedMissingBinary && /command not found/.test(lastErr)) {
|
|
147
|
-
warnedMissingBinary = true;
|
|
148
|
-
console.warn('[Zenzap] Whisper binary not found. Install `whisper` or `python3 -m whisper` to enable local audio transcription.');
|
|
149
|
-
}
|
|
150
|
-
else if (lastErr) {
|
|
151
|
-
console.warn(`[Zenzap] Whisper transcription failed (${trace}): ${lastErr}`);
|
|
152
|
-
}
|
|
153
|
-
return null;
|
|
154
|
-
}
|
|
155
|
-
catch (err) {
|
|
156
|
-
console.warn(`[Zenzap] Audio transcription error (${trace}): ${err?.message ?? err}`);
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
159
|
-
finally {
|
|
160
|
-
await rm(workDir, { recursive: true, force: true }).catch(() => { });
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
}
|