@elizaos/plugin-browser 1.0.0-alpha.26
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 +21 -0
- package/README.md +384 -0
- package/dist/index.js +1174 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
- package/scripts/postinstall.js +70 -0
- package/tsup.config.ts +22 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1174 @@
|
|
|
1
|
+
// src/services/awsS3.ts
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
GetObjectCommand,
|
|
6
|
+
PutObjectCommand,
|
|
7
|
+
S3Client
|
|
8
|
+
} from "@aws-sdk/client-s3";
|
|
9
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
10
|
+
import {
|
|
11
|
+
Service,
|
|
12
|
+
ServiceTypes,
|
|
13
|
+
logger
|
|
14
|
+
} from "@elizaos/core";
|
|
15
|
+
var AwsS3Service = class _AwsS3Service extends Service {
|
|
16
|
+
static serviceType = ServiceTypes.REMOTE_FILES;
|
|
17
|
+
capabilityDescription = "The agent is able to upload and download files from AWS S3";
|
|
18
|
+
s3Client = null;
|
|
19
|
+
bucket = "";
|
|
20
|
+
fileUploadPath = "";
|
|
21
|
+
runtime = null;
|
|
22
|
+
/**
|
|
23
|
+
* Constructor for a new instance of a class.
|
|
24
|
+
* @param {IAgentRuntime} runtime - The runtime object for the agent.
|
|
25
|
+
*/
|
|
26
|
+
constructor(runtime) {
|
|
27
|
+
super();
|
|
28
|
+
this.runtime = runtime;
|
|
29
|
+
this.fileUploadPath = runtime.getSetting("AWS_S3_UPLOAD_PATH") ?? "";
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Initializes AwsS3Service with the given runtime and settings.
|
|
33
|
+
* @param {IAgentRuntime} runtime - The runtime object
|
|
34
|
+
* @returns {Promise<AwsS3Service>} - The AwsS3Service instance
|
|
35
|
+
*/
|
|
36
|
+
static async start(runtime) {
|
|
37
|
+
logger.log("Initializing AwsS3Service");
|
|
38
|
+
const service = new _AwsS3Service(runtime);
|
|
39
|
+
service.runtime = runtime;
|
|
40
|
+
service.fileUploadPath = runtime.getSetting("AWS_S3_UPLOAD_PATH") ?? "";
|
|
41
|
+
return service;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Stops the remote file service.
|
|
45
|
+
*
|
|
46
|
+
* @param {IAgentRuntime} runtime - The agent runtime
|
|
47
|
+
* @returns {Promise<void>} - A promise that resolves once the service is stopped
|
|
48
|
+
*/
|
|
49
|
+
static async stop(runtime) {
|
|
50
|
+
const service = runtime.getService(ServiceTypes.REMOTE_FILES);
|
|
51
|
+
if (service) {
|
|
52
|
+
await service.stop();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Asynchronously stops the S3 client if it exists by destroying the client and setting it to null.
|
|
57
|
+
*/
|
|
58
|
+
async stop() {
|
|
59
|
+
if (this.s3Client) {
|
|
60
|
+
await this.s3Client.destroy();
|
|
61
|
+
this.s3Client = null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Initializes the S3 client with the provided settings.
|
|
66
|
+
* If the S3 client is already initialized, it returns true.
|
|
67
|
+
* If any required setting is missing or invalid, it returns false.
|
|
68
|
+
*
|
|
69
|
+
* @returns A Promise that resolves to true if the S3 client is successfully initialized, false otherwise.
|
|
70
|
+
*/
|
|
71
|
+
async initializeS3Client() {
|
|
72
|
+
if (this.s3Client) return true;
|
|
73
|
+
if (!this.runtime) return false;
|
|
74
|
+
const AWS_ACCESS_KEY_ID = this.runtime.getSetting("AWS_ACCESS_KEY_ID");
|
|
75
|
+
const AWS_SECRET_ACCESS_KEY = this.runtime.getSetting(
|
|
76
|
+
"AWS_SECRET_ACCESS_KEY"
|
|
77
|
+
);
|
|
78
|
+
const AWS_REGION = this.runtime.getSetting("AWS_REGION");
|
|
79
|
+
const AWS_S3_BUCKET = this.runtime.getSetting("AWS_S3_BUCKET");
|
|
80
|
+
if (!AWS_ACCESS_KEY_ID || !AWS_SECRET_ACCESS_KEY || !AWS_REGION || !AWS_S3_BUCKET) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const endpoint = this.runtime.getSetting("AWS_S3_ENDPOINT");
|
|
84
|
+
const sslEnabled = this.runtime.getSetting("AWS_S3_SSL_ENABLED");
|
|
85
|
+
const forcePathStyle = this.runtime.getSetting("AWS_S3_FORCE_PATH_STYLE");
|
|
86
|
+
this.s3Client = new S3Client({
|
|
87
|
+
...endpoint ? { endpoint } : {},
|
|
88
|
+
...sslEnabled ? { sslEnabled } : {},
|
|
89
|
+
...forcePathStyle ? { forcePathStyle: Boolean(forcePathStyle) } : {},
|
|
90
|
+
region: AWS_REGION,
|
|
91
|
+
credentials: {
|
|
92
|
+
accessKeyId: AWS_ACCESS_KEY_ID,
|
|
93
|
+
secretAccessKey: AWS_SECRET_ACCESS_KEY
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
this.bucket = AWS_S3_BUCKET;
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Uploads a file to AWS S3 with optional configuration options.
|
|
101
|
+
* @param {string} filePath - The path to the file to upload.
|
|
102
|
+
* @param {string} [subDirectory=""] - The subdirectory within the bucket to upload the file to.
|
|
103
|
+
* @param {boolean} [useSignedUrl=false] - Indicates whether to use a signed URL for the file.
|
|
104
|
+
* @param {number} [expiresIn=900] - The expiration time in seconds for the signed URL.
|
|
105
|
+
* @returns {Promise<UploadResult>} A promise that resolves to an object containing the upload result.
|
|
106
|
+
*/
|
|
107
|
+
async uploadFile(filePath, subDirectory = "", useSignedUrl = false, expiresIn = 900) {
|
|
108
|
+
try {
|
|
109
|
+
if (!await this.initializeS3Client()) {
|
|
110
|
+
return {
|
|
111
|
+
success: false,
|
|
112
|
+
error: "AWS S3 credentials not configured"
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (!fs.existsSync(filePath)) {
|
|
116
|
+
return {
|
|
117
|
+
success: false,
|
|
118
|
+
error: "File does not exist"
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const fileContent = fs.readFileSync(filePath);
|
|
122
|
+
const baseFileName = `${Date.now()}-${path.basename(filePath)}`;
|
|
123
|
+
const fileName = `${this.fileUploadPath}${subDirectory}/${baseFileName}`.replaceAll(
|
|
124
|
+
"//",
|
|
125
|
+
"/"
|
|
126
|
+
);
|
|
127
|
+
const uploadParams = {
|
|
128
|
+
Bucket: this.bucket,
|
|
129
|
+
Key: fileName,
|
|
130
|
+
Body: fileContent,
|
|
131
|
+
ContentType: this.getContentType(filePath)
|
|
132
|
+
};
|
|
133
|
+
await this.s3Client.send(new PutObjectCommand(uploadParams));
|
|
134
|
+
const result = {
|
|
135
|
+
success: true
|
|
136
|
+
};
|
|
137
|
+
if (!useSignedUrl) {
|
|
138
|
+
if (this.s3Client.config.endpoint) {
|
|
139
|
+
const endpoint = await this.s3Client.config.endpoint();
|
|
140
|
+
const port = endpoint.port ? `:${endpoint.port}` : "";
|
|
141
|
+
result.url = `${endpoint.protocol}//${endpoint.hostname}${port}${endpoint.path}${this.bucket}/${fileName}`;
|
|
142
|
+
} else {
|
|
143
|
+
result.url = `https://${this.bucket}.s3.${process.env.AWS_REGION}.amazonaws.com/${fileName}`;
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
const getObjectCommand = new GetObjectCommand({
|
|
147
|
+
Bucket: this.bucket,
|
|
148
|
+
Key: fileName
|
|
149
|
+
});
|
|
150
|
+
result.url = await getSignedUrl(this.s3Client, getObjectCommand, {
|
|
151
|
+
expiresIn
|
|
152
|
+
// 15 minutes in seconds
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return {
|
|
158
|
+
success: false,
|
|
159
|
+
error: error instanceof Error ? error.message : "Unknown error occurred"
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Generate signed URL for existing file
|
|
165
|
+
*/
|
|
166
|
+
/**
|
|
167
|
+
* Generates a signed URL for accessing the specified file in the S3 bucket.
|
|
168
|
+
*
|
|
169
|
+
* @param {string} fileName - The name of the file to generate a signed URL for.
|
|
170
|
+
* @param {number} expiresIn - The expiration time in seconds for the signed URL (default is 900 seconds).
|
|
171
|
+
* @returns {Promise<string>} A promise that resolves with the signed URL for accessing the file.
|
|
172
|
+
* @throws {Error} If AWS S3 credentials are not configured properly.
|
|
173
|
+
*/
|
|
174
|
+
async generateSignedUrl(fileName, expiresIn = 900) {
|
|
175
|
+
if (!await this.initializeS3Client()) {
|
|
176
|
+
throw new Error("AWS S3 credentials not configured");
|
|
177
|
+
}
|
|
178
|
+
const command = new GetObjectCommand({
|
|
179
|
+
Bucket: this.bucket,
|
|
180
|
+
Key: fileName
|
|
181
|
+
});
|
|
182
|
+
return await getSignedUrl(this.s3Client, command, { expiresIn });
|
|
183
|
+
}
|
|
184
|
+
getContentType(filePath) {
|
|
185
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
186
|
+
const contentTypes = {
|
|
187
|
+
".png": "image/png",
|
|
188
|
+
".jpg": "image/jpeg",
|
|
189
|
+
".jpeg": "image/jpeg",
|
|
190
|
+
".gif": "image/gif",
|
|
191
|
+
".webp": "image/webp"
|
|
192
|
+
};
|
|
193
|
+
return contentTypes[ext] || "application/octet-stream";
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Upload JSON object to S3
|
|
197
|
+
* @param jsonData JSON data to upload
|
|
198
|
+
* @param fileName File name (optional, without path)
|
|
199
|
+
* @param subDirectory Subdirectory (optional)
|
|
200
|
+
* @param useSignedUrl Whether to use signed URL
|
|
201
|
+
* @param expiresIn Signed URL expiration time (seconds)
|
|
202
|
+
*/
|
|
203
|
+
async uploadJson(jsonData, fileName, subDirectory, useSignedUrl = false, expiresIn = 900) {
|
|
204
|
+
try {
|
|
205
|
+
if (!await this.initializeS3Client()) {
|
|
206
|
+
return {
|
|
207
|
+
success: false,
|
|
208
|
+
error: "AWS S3 credentials not configured"
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (!jsonData) {
|
|
212
|
+
return {
|
|
213
|
+
success: false,
|
|
214
|
+
error: "JSON data is required"
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const timestamp = Date.now();
|
|
218
|
+
const actualFileName = fileName || `${timestamp}.json`;
|
|
219
|
+
let fullPath = this.fileUploadPath || "";
|
|
220
|
+
if (subDirectory) {
|
|
221
|
+
fullPath = `${fullPath}/${subDirectory}`.replace(/\/+/g, "/");
|
|
222
|
+
}
|
|
223
|
+
const key = `${fullPath}/${actualFileName}`.replace(/\/+/g, "/");
|
|
224
|
+
const jsonString = JSON.stringify(jsonData, null, 2);
|
|
225
|
+
const uploadParams = {
|
|
226
|
+
Bucket: this.bucket,
|
|
227
|
+
Key: key,
|
|
228
|
+
Body: jsonString,
|
|
229
|
+
ContentType: "application/json"
|
|
230
|
+
};
|
|
231
|
+
await this.s3Client.send(new PutObjectCommand(uploadParams));
|
|
232
|
+
const result = {
|
|
233
|
+
success: true,
|
|
234
|
+
key
|
|
235
|
+
};
|
|
236
|
+
if (!useSignedUrl) {
|
|
237
|
+
if (this.s3Client.config.endpoint) {
|
|
238
|
+
const endpoint = await this.s3Client.config.endpoint();
|
|
239
|
+
const port = endpoint.port ? `:${endpoint.port}` : "";
|
|
240
|
+
result.url = `${endpoint.protocol}//${endpoint.hostname}${port}${endpoint.path}${this.bucket}/${key}`;
|
|
241
|
+
} else {
|
|
242
|
+
result.url = `https://${this.bucket}.s3.${process.env.AWS_REGION}.amazonaws.com/${key}`;
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
const getObjectCommand = new GetObjectCommand({
|
|
246
|
+
Bucket: this.bucket,
|
|
247
|
+
Key: key
|
|
248
|
+
});
|
|
249
|
+
result.url = await getSignedUrl(this.s3Client, getObjectCommand, {
|
|
250
|
+
expiresIn
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return result;
|
|
254
|
+
} catch (error) {
|
|
255
|
+
return {
|
|
256
|
+
success: false,
|
|
257
|
+
error: error instanceof Error ? error.message : "Unknown error occurred"
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// src/services/browser.ts
|
|
264
|
+
import {
|
|
265
|
+
ModelTypes,
|
|
266
|
+
Service as Service2,
|
|
267
|
+
ServiceTypes as ServiceTypes2,
|
|
268
|
+
logger as logger2,
|
|
269
|
+
parseJSONObjectFromText,
|
|
270
|
+
settings,
|
|
271
|
+
stringToUuid,
|
|
272
|
+
trimTokens
|
|
273
|
+
} from "@elizaos/core";
|
|
274
|
+
import CaptchaSolver from "capsolver-npm";
|
|
275
|
+
import {
|
|
276
|
+
chromium
|
|
277
|
+
} from "patchright";
|
|
278
|
+
async function generateSummary(runtime, text) {
|
|
279
|
+
text = await trimTokens(text, 1e5, runtime);
|
|
280
|
+
const prompt = `Please generate a concise summary for the following text:
|
|
281
|
+
|
|
282
|
+
Text: """
|
|
283
|
+
${text}
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
Respond with a JSON object in the following format:
|
|
287
|
+
\`\`\`json
|
|
288
|
+
{
|
|
289
|
+
"title": "Generated Title",
|
|
290
|
+
"summary": "Generated summary and/or description of the text"
|
|
291
|
+
}
|
|
292
|
+
\`\`\``;
|
|
293
|
+
const response = await runtime.useModel(ModelTypes.TEXT_SMALL, {
|
|
294
|
+
prompt
|
|
295
|
+
});
|
|
296
|
+
const parsedResponse = parseJSONObjectFromText(response);
|
|
297
|
+
if (parsedResponse?.title && parsedResponse?.summary) {
|
|
298
|
+
return {
|
|
299
|
+
title: parsedResponse.title,
|
|
300
|
+
description: parsedResponse.summary
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
title: "",
|
|
305
|
+
description: ""
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
var BrowserService = class _BrowserService extends Service2 {
|
|
309
|
+
browser;
|
|
310
|
+
context;
|
|
311
|
+
captchaSolver;
|
|
312
|
+
cacheKey = "content/browser";
|
|
313
|
+
static serviceType = ServiceTypes2.BROWSER;
|
|
314
|
+
capabilityDescription = "The agent is able to browse the web and fetch content";
|
|
315
|
+
/**
|
|
316
|
+
* Constructor for the Agent class.
|
|
317
|
+
* @param {IAgentRuntime} runtime - The runtime object for the agent.
|
|
318
|
+
*/
|
|
319
|
+
constructor(runtime) {
|
|
320
|
+
super();
|
|
321
|
+
this.runtime = runtime;
|
|
322
|
+
this.browser = void 0;
|
|
323
|
+
this.context = void 0;
|
|
324
|
+
this.captchaSolver = new CaptchaSolver(settings.CAPSOLVER_API_KEY || "");
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Starts the BrowserService asynchronously.
|
|
328
|
+
*
|
|
329
|
+
* @param {IAgentRuntime} runtime - The runtime for the agent.
|
|
330
|
+
* @returns {Promise<BrowserService>} A promise that resolves to the initialized BrowserService.
|
|
331
|
+
*/
|
|
332
|
+
static async start(runtime) {
|
|
333
|
+
const service = new _BrowserService(runtime);
|
|
334
|
+
await service.initializeBrowser();
|
|
335
|
+
return service;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Function to stop the browser service asynchronously.
|
|
339
|
+
*
|
|
340
|
+
* @param {IAgentRuntime} runtime - The runtime environment for the agent.
|
|
341
|
+
*/
|
|
342
|
+
static async stop(runtime) {
|
|
343
|
+
const service = runtime.getService(ServiceTypes2.BROWSER);
|
|
344
|
+
if (service) {
|
|
345
|
+
await service.stop();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Initializes the browser by launching Chromium with specified options and setting the user agent based on the platform.
|
|
350
|
+
* @returns {Promise<void>} A promise that resolves once the browser is successfully initialized.
|
|
351
|
+
*/
|
|
352
|
+
async initializeBrowser() {
|
|
353
|
+
if (!this.browser) {
|
|
354
|
+
this.browser = await chromium.launch({
|
|
355
|
+
headless: true,
|
|
356
|
+
args: [
|
|
357
|
+
"--disable-dev-shm-usage",
|
|
358
|
+
// Uses /tmp instead of /dev/shm. Prevents memory issues on low-memory systems
|
|
359
|
+
"--block-new-web-contents"
|
|
360
|
+
// Prevents creation of new windows/tabs
|
|
361
|
+
]
|
|
362
|
+
});
|
|
363
|
+
const platform = process.platform;
|
|
364
|
+
let userAgent = "";
|
|
365
|
+
switch (platform) {
|
|
366
|
+
case "darwin":
|
|
367
|
+
userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
368
|
+
break;
|
|
369
|
+
case "win32":
|
|
370
|
+
userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
371
|
+
break;
|
|
372
|
+
case "linux":
|
|
373
|
+
userAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
374
|
+
break;
|
|
375
|
+
default:
|
|
376
|
+
userAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
377
|
+
}
|
|
378
|
+
this.context = await this.browser.newContext({
|
|
379
|
+
userAgent,
|
|
380
|
+
acceptDownloads: false
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Asynchronously stops the browser and context if they are currently running.
|
|
386
|
+
*/
|
|
387
|
+
async stop() {
|
|
388
|
+
if (this.context) {
|
|
389
|
+
await this.context.close();
|
|
390
|
+
this.context = void 0;
|
|
391
|
+
}
|
|
392
|
+
if (this.browser) {
|
|
393
|
+
await this.browser.close();
|
|
394
|
+
this.browser = void 0;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Asynchronously fetches the content of a web page.
|
|
399
|
+
*
|
|
400
|
+
* @param {string} url - The URL of the web page to fetch content from.
|
|
401
|
+
* @param {IAgentRuntime} runtime - The runtime environment for the web scraping agent.
|
|
402
|
+
* @returns {Promise<PageContent>} A Promise that resolves with the content of the web page.
|
|
403
|
+
*/
|
|
404
|
+
async getPageContent(url, runtime) {
|
|
405
|
+
await this.initializeBrowser();
|
|
406
|
+
return await this.fetchPageContent(url, runtime);
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Generates a cache key for the provided URL by converting it to a UUID string.
|
|
410
|
+
*
|
|
411
|
+
* @param {string} url - The URL for which a cache key is being generated.
|
|
412
|
+
* @returns {string} A UUID string representing the cache key for the URL.
|
|
413
|
+
*/
|
|
414
|
+
getCacheKey(url) {
|
|
415
|
+
return stringToUuid(url);
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Fetches the content of a page from the specified URL using a headless browser.
|
|
419
|
+
*
|
|
420
|
+
* @param {string} url - The URL of the page to fetch the content from.
|
|
421
|
+
* @param {IAgentRuntime} runtime - The runtime environment for the agent.
|
|
422
|
+
* @returns {Promise<PageContent>} A promise that resolves to the content of the fetched page.
|
|
423
|
+
*/
|
|
424
|
+
async fetchPageContent(url, runtime) {
|
|
425
|
+
const cacheKey = this.getCacheKey(url);
|
|
426
|
+
const cached = await runtime.getCache(`${this.cacheKey}/${cacheKey}`);
|
|
427
|
+
if (cached) {
|
|
428
|
+
return cached.content;
|
|
429
|
+
}
|
|
430
|
+
let page;
|
|
431
|
+
try {
|
|
432
|
+
if (!this.context) {
|
|
433
|
+
logger2.log(
|
|
434
|
+
"Browser context not initialized. Call initializeBrowser() first."
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
page = await this.context.newPage();
|
|
438
|
+
await page.setExtraHTTPHeaders({
|
|
439
|
+
"Accept-Language": "en-US,en;q=0.9"
|
|
440
|
+
});
|
|
441
|
+
const response = await page.goto(url, { waitUntil: "networkidle" });
|
|
442
|
+
if (!response) {
|
|
443
|
+
logger2.error("Failed to load the page");
|
|
444
|
+
}
|
|
445
|
+
if (response.status() === 403 || response.status() === 404) {
|
|
446
|
+
return await this.tryAlternativeSources(url, runtime);
|
|
447
|
+
}
|
|
448
|
+
const captchaDetected = await this.detectCaptcha(page);
|
|
449
|
+
if (captchaDetected) {
|
|
450
|
+
await this.solveCaptcha(page, url);
|
|
451
|
+
}
|
|
452
|
+
const documentTitle = await page.evaluate(() => document.title);
|
|
453
|
+
const bodyContent = await page.evaluate(() => document.body.innerText);
|
|
454
|
+
const { title: parsedTitle, description } = await generateSummary(
|
|
455
|
+
runtime,
|
|
456
|
+
`${documentTitle}
|
|
457
|
+
${bodyContent}`
|
|
458
|
+
);
|
|
459
|
+
const content = { title: parsedTitle, description, bodyContent };
|
|
460
|
+
await runtime.setCache(`${this.cacheKey}/${cacheKey}`, {
|
|
461
|
+
url,
|
|
462
|
+
content
|
|
463
|
+
});
|
|
464
|
+
return content;
|
|
465
|
+
} catch (error) {
|
|
466
|
+
logger2.error("Error:", error);
|
|
467
|
+
return {
|
|
468
|
+
title: url,
|
|
469
|
+
description: "Error, could not fetch content",
|
|
470
|
+
bodyContent: ""
|
|
471
|
+
};
|
|
472
|
+
} finally {
|
|
473
|
+
if (page) {
|
|
474
|
+
await page.close();
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Detects if a captcha is present on the page based on the specified selectors.
|
|
480
|
+
*
|
|
481
|
+
* @param {Page} page The Puppeteer page to check for captcha.
|
|
482
|
+
* @returns {Promise<boolean>} A boolean indicating whether a captcha was detected.
|
|
483
|
+
*/
|
|
484
|
+
async detectCaptcha(page) {
|
|
485
|
+
const captchaSelectors = [
|
|
486
|
+
'iframe[src*="captcha"]',
|
|
487
|
+
'div[class*="captcha"]',
|
|
488
|
+
"#captcha",
|
|
489
|
+
".g-recaptcha",
|
|
490
|
+
".h-captcha"
|
|
491
|
+
];
|
|
492
|
+
for (const selector of captchaSelectors) {
|
|
493
|
+
const element = await page.$(selector);
|
|
494
|
+
if (element) return true;
|
|
495
|
+
}
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Solves the CAPTCHA challenge on the provided page using either hCaptcha or reCaptcha.
|
|
500
|
+
*
|
|
501
|
+
* @param {Page} page - The page where the CAPTCHA challenge needs to be solved.
|
|
502
|
+
* @param {string} url - The URL of the website with the CAPTCHA challenge.
|
|
503
|
+
* @returns {Promise<void>} - A promise that resolves once the CAPTCHA is solved.
|
|
504
|
+
*/
|
|
505
|
+
async solveCaptcha(page, url) {
|
|
506
|
+
try {
|
|
507
|
+
const hcaptchaKey = await this.getHCaptchaWebsiteKey(page);
|
|
508
|
+
if (hcaptchaKey) {
|
|
509
|
+
const solution = await this.captchaSolver.hcaptchaProxyless({
|
|
510
|
+
websiteURL: url,
|
|
511
|
+
websiteKey: hcaptchaKey
|
|
512
|
+
});
|
|
513
|
+
await page.evaluate((token) => {
|
|
514
|
+
window.hcaptcha.setResponse(token);
|
|
515
|
+
}, solution.gRecaptchaResponse);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const recaptchaKey = await this.getReCaptchaWebsiteKey(page);
|
|
519
|
+
if (recaptchaKey) {
|
|
520
|
+
const solution = await this.captchaSolver.recaptchaV2Proxyless({
|
|
521
|
+
websiteURL: url,
|
|
522
|
+
websiteKey: recaptchaKey
|
|
523
|
+
});
|
|
524
|
+
await page.evaluate((token) => {
|
|
525
|
+
document.getElementById("g-recaptcha-response").innerHTML = token;
|
|
526
|
+
}, solution.gRecaptchaResponse);
|
|
527
|
+
}
|
|
528
|
+
} catch (error) {
|
|
529
|
+
logger2.error("Error solving CAPTCHA:", error);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Get the hCaptcha website key from the given Page
|
|
534
|
+
* @param {Page} page - The Page object to extract the hCaptcha website key from
|
|
535
|
+
* @returns {Promise<string>} The hCaptcha website key
|
|
536
|
+
*/
|
|
537
|
+
async getHCaptchaWebsiteKey(page) {
|
|
538
|
+
return page.evaluate(() => {
|
|
539
|
+
const hcaptchaIframe = document.querySelector(
|
|
540
|
+
'iframe[src*="hcaptcha.com"]'
|
|
541
|
+
);
|
|
542
|
+
if (hcaptchaIframe) {
|
|
543
|
+
const src = hcaptchaIframe.getAttribute("src");
|
|
544
|
+
const match = src?.match(/sitekey=([^&]*)/);
|
|
545
|
+
return match ? match[1] : "";
|
|
546
|
+
}
|
|
547
|
+
return "";
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Retrieves the ReCaptcha website key from a given page.
|
|
552
|
+
* @param {Page} page - The page to extract the ReCaptcha website key from.
|
|
553
|
+
* @returns {Promise<string>} The ReCaptcha website key, or an empty string if not found.
|
|
554
|
+
*/
|
|
555
|
+
async getReCaptchaWebsiteKey(page) {
|
|
556
|
+
return page.evaluate(() => {
|
|
557
|
+
const recaptchaElement = document.querySelector(".g-recaptcha");
|
|
558
|
+
return recaptchaElement ? recaptchaElement.getAttribute("data-sitekey") || "" : "";
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Try fetching content from alternative sources if the original source fails.
|
|
563
|
+
*
|
|
564
|
+
* @param {string} url - The URL of the content to fetch.
|
|
565
|
+
* @param {IAgentRuntime} runtime - The runtime environment.
|
|
566
|
+
* @returns {Promise<{ title: string; description: string; bodyContent: string }>} The fetched content with title, description, and body.
|
|
567
|
+
*/
|
|
568
|
+
async tryAlternativeSources(url, runtime) {
|
|
569
|
+
if (!url.match(/web.archive.org\/web/)) {
|
|
570
|
+
const archiveUrl = `https://web.archive.org/web/${url}`;
|
|
571
|
+
try {
|
|
572
|
+
return await this.fetchPageContent(archiveUrl, runtime);
|
|
573
|
+
} catch (error) {
|
|
574
|
+
logger2.error("Error fetching from Internet Archive:", error);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (!url.match(/www.google.com\/search/)) {
|
|
578
|
+
const googleSearchUrl = `https://www.google.com/search?q=${encodeURIComponent(
|
|
579
|
+
url
|
|
580
|
+
)}`;
|
|
581
|
+
try {
|
|
582
|
+
return await this.fetchPageContent(googleSearchUrl, runtime);
|
|
583
|
+
} catch (error) {
|
|
584
|
+
logger2.error("Error fetching from Google Search:", error);
|
|
585
|
+
logger2.error("Failed to fetch content from alternative sources");
|
|
586
|
+
return {
|
|
587
|
+
title: url,
|
|
588
|
+
description: "Error, could not fetch content from alternative sources",
|
|
589
|
+
bodyContent: ""
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
// src/services/pdf.ts
|
|
597
|
+
import {
|
|
598
|
+
Service as Service3,
|
|
599
|
+
ServiceTypes as ServiceTypes3
|
|
600
|
+
} from "@elizaos/core";
|
|
601
|
+
import { getDocument } from "pdfjs-dist";
|
|
602
|
+
var PdfService = class _PdfService extends Service3 {
|
|
603
|
+
static serviceType = ServiceTypes3.PDF;
|
|
604
|
+
capabilityDescription = "The agent is able to convert PDF files to text";
|
|
605
|
+
/**
|
|
606
|
+
* Constructor for creating a new instance of the class.
|
|
607
|
+
*
|
|
608
|
+
* @param {IAgentRuntime} runtime - The runtime object passed to the constructor.
|
|
609
|
+
*/
|
|
610
|
+
constructor(runtime) {
|
|
611
|
+
super();
|
|
612
|
+
this.runtime = runtime;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Starts the PdfService asynchronously.
|
|
616
|
+
* @param {IAgentRuntime} runtime - The runtime object for the agent.
|
|
617
|
+
* @returns {Promise<PdfService>} A promise that resolves with the PdfService instance.
|
|
618
|
+
*/
|
|
619
|
+
static async start(runtime) {
|
|
620
|
+
const service = new _PdfService(runtime);
|
|
621
|
+
return service;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Stop the PDF service in the given runtime.
|
|
625
|
+
*
|
|
626
|
+
* @param {IAgentRuntime} runtime - The runtime to stop the PDF service in.
|
|
627
|
+
* @returns {Promise<void>} - A promise that resolves once the PDF service is stopped.
|
|
628
|
+
*/
|
|
629
|
+
static async stop(runtime) {
|
|
630
|
+
const service = runtime.getService(ServiceTypes3.PDF);
|
|
631
|
+
if (service) {
|
|
632
|
+
await service.stop();
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Asynchronously stops the process.
|
|
637
|
+
* Does nothing.
|
|
638
|
+
*/
|
|
639
|
+
async stop() {
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Converts a PDF Buffer to text.
|
|
643
|
+
*
|
|
644
|
+
* @param {Buffer} pdfBuffer - The PDF Buffer to convert to text.
|
|
645
|
+
* @returns {Promise<string>} A Promise that resolves with the text content of the PDF.
|
|
646
|
+
*/
|
|
647
|
+
async convertPdfToText(pdfBuffer) {
|
|
648
|
+
const uint8Array = new Uint8Array(pdfBuffer);
|
|
649
|
+
const pdf = await getDocument({ data: uint8Array }).promise;
|
|
650
|
+
const numPages = pdf.numPages;
|
|
651
|
+
const textPages = [];
|
|
652
|
+
for (let pageNum = 1; pageNum <= numPages; pageNum++) {
|
|
653
|
+
const page = await pdf.getPage(pageNum);
|
|
654
|
+
const textContent = await page.getTextContent();
|
|
655
|
+
const pageText = textContent.items.filter(isTextItem).map((item) => item.str).join(" ");
|
|
656
|
+
textPages.push(pageText);
|
|
657
|
+
}
|
|
658
|
+
return textPages.join("\n");
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
function isTextItem(item) {
|
|
662
|
+
return "str" in item;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/services/video.ts
|
|
666
|
+
import fs2 from "node:fs";
|
|
667
|
+
import { tmpdir } from "node:os";
|
|
668
|
+
import path2 from "node:path";
|
|
669
|
+
import {
|
|
670
|
+
ModelTypes as ModelTypes2,
|
|
671
|
+
Service as Service4,
|
|
672
|
+
ServiceTypes as ServiceTypes4,
|
|
673
|
+
logger as logger3,
|
|
674
|
+
stringToUuid as stringToUuid2
|
|
675
|
+
} from "@elizaos/core";
|
|
676
|
+
import ffmpeg from "fluent-ffmpeg";
|
|
677
|
+
import ytdl, { create } from "youtube-dl-exec";
|
|
678
|
+
function getYoutubeDL() {
|
|
679
|
+
if (fs2.existsSync("/usr/local/bin/yt-dlp")) {
|
|
680
|
+
return create("/usr/local/bin/yt-dlp");
|
|
681
|
+
}
|
|
682
|
+
if (fs2.existsSync("/usr/bin/yt-dlp")) {
|
|
683
|
+
return create("/usr/bin/yt-dlp");
|
|
684
|
+
}
|
|
685
|
+
return ytdl;
|
|
686
|
+
}
|
|
687
|
+
var VideoService = class _VideoService extends Service4 {
|
|
688
|
+
static serviceType = ServiceTypes4.VIDEO;
|
|
689
|
+
capabilityDescription = "The agent is able to download and process videos";
|
|
690
|
+
cacheKey = "content/video";
|
|
691
|
+
dataDir = "./cache";
|
|
692
|
+
queue = [];
|
|
693
|
+
processing = false;
|
|
694
|
+
/**
|
|
695
|
+
* Constructor for creating a new instance of the object.
|
|
696
|
+
*
|
|
697
|
+
* @param {IAgentRuntime} runtime - The runtime object to be used by the instance
|
|
698
|
+
*/
|
|
699
|
+
constructor(runtime) {
|
|
700
|
+
super();
|
|
701
|
+
this.runtime = runtime;
|
|
702
|
+
this.ensureDataDirectoryExists();
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Starts the VideoService by initializing it with the given IAgentRuntime instance.
|
|
706
|
+
*
|
|
707
|
+
* @param {IAgentRuntime} runtime - The IAgentRuntime instance to initialize the service with.
|
|
708
|
+
* @returns {Promise<VideoService>} A promise that resolves to the initialized VideoService instance.
|
|
709
|
+
*/
|
|
710
|
+
static async start(runtime) {
|
|
711
|
+
const service = new _VideoService(runtime);
|
|
712
|
+
return service;
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Stops the video service if it is running.
|
|
716
|
+
*
|
|
717
|
+
* @param {IAgentRuntime} runtime - The agent runtime instance
|
|
718
|
+
* @returns {Promise<void>} A promise that resolves once the video service is stopped
|
|
719
|
+
*/
|
|
720
|
+
static async stop(runtime) {
|
|
721
|
+
const service = runtime.getService(ServiceTypes4.VIDEO);
|
|
722
|
+
if (service) {
|
|
723
|
+
await service.stop();
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Asynchronous method to stop the operation.
|
|
728
|
+
*/
|
|
729
|
+
async stop() {
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Checks if the data directory exists, and if not, creates it.
|
|
733
|
+
*/
|
|
734
|
+
ensureDataDirectoryExists() {
|
|
735
|
+
if (!fs2.existsSync(this.dataDir)) {
|
|
736
|
+
fs2.mkdirSync(this.dataDir);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Check if a given URL is a video URL from YouTube or Vimeo.
|
|
741
|
+
*
|
|
742
|
+
* @param {string} url - The URL to check.
|
|
743
|
+
* @return {boolean} Returns true if the URL is from YouTube or Vimeo, false otherwise.
|
|
744
|
+
*/
|
|
745
|
+
isVideoUrl(url) {
|
|
746
|
+
return url.includes("youtube.com") || url.includes("youtu.be") || url.includes("vimeo.com");
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Downloads media from a given URL. If the media already exists, it returns the file path.
|
|
750
|
+
*
|
|
751
|
+
* @param {string} url - The URL of the media to download.
|
|
752
|
+
* @returns {Promise<string>} A promise that resolves to the file path of the downloaded media.
|
|
753
|
+
* @throws {Error} If there is an error downloading the media.
|
|
754
|
+
*/
|
|
755
|
+
async downloadMedia(url) {
|
|
756
|
+
const videoId = this.getVideoId(url);
|
|
757
|
+
const outputFile = path2.join(this.dataDir, `${videoId}.mp4`);
|
|
758
|
+
if (fs2.existsSync(outputFile)) {
|
|
759
|
+
return outputFile;
|
|
760
|
+
}
|
|
761
|
+
try {
|
|
762
|
+
await getYoutubeDL()(url, {
|
|
763
|
+
verbose: true,
|
|
764
|
+
output: outputFile,
|
|
765
|
+
writeInfoJson: true
|
|
766
|
+
});
|
|
767
|
+
return outputFile;
|
|
768
|
+
} catch (error) {
|
|
769
|
+
logger3.log("Error downloading media:", error);
|
|
770
|
+
throw new Error("Failed to download media");
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Downloads a video using the videoInfo object provided and returns the path to the downloaded video file.
|
|
775
|
+
* If the video file already exists, it will return the path without re-downloading.
|
|
776
|
+
* @param {Object} videoInfo - Information about the video to download.
|
|
777
|
+
* @returns {Promise<string>} - Path to the downloaded video file.
|
|
778
|
+
*/
|
|
779
|
+
async downloadVideo(videoInfo) {
|
|
780
|
+
const videoId = this.getVideoId(videoInfo.webpage_url);
|
|
781
|
+
const outputFile = path2.join(this.dataDir, `${videoId}.mp4`);
|
|
782
|
+
if (fs2.existsSync(outputFile)) {
|
|
783
|
+
return outputFile;
|
|
784
|
+
}
|
|
785
|
+
try {
|
|
786
|
+
await getYoutubeDL()(videoInfo.webpage_url, {
|
|
787
|
+
verbose: true,
|
|
788
|
+
output: outputFile,
|
|
789
|
+
format: "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best",
|
|
790
|
+
writeInfoJson: true
|
|
791
|
+
});
|
|
792
|
+
return outputFile;
|
|
793
|
+
} catch (error) {
|
|
794
|
+
logger3.log("Error downloading video:", error);
|
|
795
|
+
throw new Error("Failed to download video");
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Process a video from the given URL using the provided agent runtime.
|
|
800
|
+
*
|
|
801
|
+
* @param {string} url - The URL of the video to be processed
|
|
802
|
+
* @param {IAgentRuntime} runtime - The agent runtime to be used for processing the video
|
|
803
|
+
* @returns {Promise<Media>} A promise that resolves to the processed media
|
|
804
|
+
*/
|
|
805
|
+
async processVideo(url, runtime) {
|
|
806
|
+
this.queue.push(url);
|
|
807
|
+
await this.processQueue(runtime);
|
|
808
|
+
return new Promise((resolve, reject) => {
|
|
809
|
+
const checkQueue = async () => {
|
|
810
|
+
const index = this.queue.indexOf(url);
|
|
811
|
+
if (index !== -1) {
|
|
812
|
+
setTimeout(checkQueue, 100);
|
|
813
|
+
} else {
|
|
814
|
+
this.processVideoFromUrl(url, runtime).then(resolve).catch(reject);
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
checkQueue();
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Processes the queue of URLs by calling processVideoFromUrl for each URL.
|
|
822
|
+
*
|
|
823
|
+
* @param {any} runtime - The runtime information for processing the videos.
|
|
824
|
+
* @returns {Promise<void>} - A promise that resolves when the queue has been processed.
|
|
825
|
+
*/
|
|
826
|
+
async processQueue(runtime) {
|
|
827
|
+
if (this.processing || this.queue.length === 0) {
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
this.processing = true;
|
|
831
|
+
while (this.queue.length > 0) {
|
|
832
|
+
const url = this.queue.shift();
|
|
833
|
+
await this.processVideoFromUrl(url, runtime);
|
|
834
|
+
}
|
|
835
|
+
this.processing = false;
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Processes a video from a given URL.
|
|
839
|
+
* Retrieves video information, transcript, and caches the result.
|
|
840
|
+
*
|
|
841
|
+
* @param {string} url - The URL of the video to process.
|
|
842
|
+
* @param {IAgentRuntime} runtime - The runtime environment for the agent.
|
|
843
|
+
* @returns {Promise<Media>} A promise that resolves to the processed video data.
|
|
844
|
+
* @throws {Error} If there is an error processing the video.
|
|
845
|
+
*/
|
|
846
|
+
async processVideoFromUrl(url, runtime) {
|
|
847
|
+
const videoId = url.match(
|
|
848
|
+
/(?:youtu\.be\/|youtube\.com(?:\/embed\/|\/v\/|\/watch\?v=|\/watch\?.+&v=))([^\/&?]+)/
|
|
849
|
+
// eslint-disable-line
|
|
850
|
+
)?.[1] || "";
|
|
851
|
+
const videoUuid = this.getVideoId(videoId);
|
|
852
|
+
const cacheKey = `${this.cacheKey}/${videoUuid}`;
|
|
853
|
+
const cached = await runtime.getCache(cacheKey);
|
|
854
|
+
if (cached) {
|
|
855
|
+
logger3.log("Returning cached video file");
|
|
856
|
+
return cached;
|
|
857
|
+
}
|
|
858
|
+
try {
|
|
859
|
+
logger3.log("Cache miss, processing video");
|
|
860
|
+
logger3.log("Fetching video info");
|
|
861
|
+
const videoInfo = await this.fetchVideoInfo(url);
|
|
862
|
+
console.log("Getting transcript");
|
|
863
|
+
const transcript = await this.getTranscript(url, videoInfo, runtime);
|
|
864
|
+
const result = {
|
|
865
|
+
id: videoUuid,
|
|
866
|
+
url,
|
|
867
|
+
title: videoInfo.title,
|
|
868
|
+
source: videoInfo.channel,
|
|
869
|
+
description: videoInfo.description,
|
|
870
|
+
text: transcript
|
|
871
|
+
};
|
|
872
|
+
await runtime.setCache(cacheKey, result);
|
|
873
|
+
return result;
|
|
874
|
+
} catch (error) {
|
|
875
|
+
throw new Error(`Error processing video: ${error.message || error}`);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Returns the unique video ID generated from the provided URL.
|
|
880
|
+
* @param {string} url - The URL used to generate the video ID.
|
|
881
|
+
* @returns {string} The unique video ID.
|
|
882
|
+
*/
|
|
883
|
+
getVideoId(url) {
|
|
884
|
+
return stringToUuid2(url);
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Asynchronously fetches video information from the provided URL. If the URL ends with ".mp4" or includes ".mp4?", attempts to fetch the video directly using fetch. If successful, returns a simplified video info object with title, description, and channel. If direct download fails, falls back to using youtube-dl to fetch video information. Utilizes options such as dumpJson, verbose, callHome, noCheckCertificates, preferFreeFormats, youtubeSkipDashManifest, writeSub, writeAutoSub, subLang, and skipDownload when calling youtube-dl. Throws an error if the response from youtube-dl is empty or if there is an error during the process.
|
|
888
|
+
*
|
|
889
|
+
* @param {string} url - The URL from which to fetch video information
|
|
890
|
+
* @returns {Promise<any>} A Promise resolving to the fetched video information or rejecting with an error message
|
|
891
|
+
*/
|
|
892
|
+
async fetchVideoInfo(url) {
|
|
893
|
+
console.log("url", url);
|
|
894
|
+
if (url.endsWith(".mp4") || url.includes(".mp4?")) {
|
|
895
|
+
try {
|
|
896
|
+
const response = await fetch(url);
|
|
897
|
+
if (response.ok) {
|
|
898
|
+
return {
|
|
899
|
+
title: path2.basename(url),
|
|
900
|
+
description: "",
|
|
901
|
+
channel: ""
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
} catch (error) {
|
|
905
|
+
logger3.log("Error downloading MP4 file:", error);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
try {
|
|
909
|
+
const result = await getYoutubeDL()(url, {
|
|
910
|
+
dumpJson: true,
|
|
911
|
+
verbose: true,
|
|
912
|
+
callHome: false,
|
|
913
|
+
noCheckCertificates: true,
|
|
914
|
+
preferFreeFormats: true,
|
|
915
|
+
youtubeSkipDashManifest: true,
|
|
916
|
+
writeSub: true,
|
|
917
|
+
writeAutoSub: true,
|
|
918
|
+
subLang: "en",
|
|
919
|
+
skipDownload: true
|
|
920
|
+
});
|
|
921
|
+
if (!result || Object.keys(result).length === 0) {
|
|
922
|
+
throw new Error("Empty response from youtube-dl");
|
|
923
|
+
}
|
|
924
|
+
return result;
|
|
925
|
+
} catch (error) {
|
|
926
|
+
throw new Error(
|
|
927
|
+
`Failed to fetch video information: ${error.message || error}`
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Asynchronously retrieves the transcript of a video based on the provided URL, video information, and runtime environment.
|
|
933
|
+
*
|
|
934
|
+
* @param {string} url - The URL of the video.
|
|
935
|
+
* @param {any} videoInfo - Information about the video, including subtitles, automatic captions, and categories.
|
|
936
|
+
* @param {IAgentRuntime} runtime - The runtime environment of the agent.
|
|
937
|
+
* @returns {Promise<string>} A Promise that resolves to the transcript of the video.
|
|
938
|
+
*/
|
|
939
|
+
async getTranscript(url, videoInfo, runtime) {
|
|
940
|
+
logger3.log("Getting transcript");
|
|
941
|
+
try {
|
|
942
|
+
if (videoInfo.subtitles?.en) {
|
|
943
|
+
logger3.log("Manual subtitles found");
|
|
944
|
+
const srtContent = await this.downloadSRT(
|
|
945
|
+
videoInfo.subtitles.en[0].url
|
|
946
|
+
);
|
|
947
|
+
return this.parseSRT(srtContent);
|
|
948
|
+
}
|
|
949
|
+
if (videoInfo.automatic_captions?.en) {
|
|
950
|
+
logger3.log("Automatic captions found");
|
|
951
|
+
const captionUrl = videoInfo.automatic_captions.en[0].url;
|
|
952
|
+
const captionContent = await this.downloadCaption(captionUrl);
|
|
953
|
+
return this.parseCaption(captionContent);
|
|
954
|
+
}
|
|
955
|
+
if (videoInfo.categories?.includes("Music")) {
|
|
956
|
+
logger3.log("Music video detected, no lyrics available");
|
|
957
|
+
return "No lyrics available.";
|
|
958
|
+
}
|
|
959
|
+
logger3.log(
|
|
960
|
+
"No subtitles or captions found, falling back to audio transcription"
|
|
961
|
+
);
|
|
962
|
+
return this.transcribeAudio(url, runtime);
|
|
963
|
+
} catch (error) {
|
|
964
|
+
logger3.log("Error in getTranscript:", error);
|
|
965
|
+
throw error;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Downloads a caption from the specified URL.
|
|
970
|
+
* @param {string} url - The URL from which to download the caption.
|
|
971
|
+
* @returns {Promise<string>} A promise that resolves with the downloaded caption as a string.
|
|
972
|
+
* @throws {Error} If the caption download fails, an error is thrown with the reason.
|
|
973
|
+
*/
|
|
974
|
+
async downloadCaption(url) {
|
|
975
|
+
logger3.log("Downloading caption from:", url);
|
|
976
|
+
const response = await fetch(url);
|
|
977
|
+
if (!response.ok) {
|
|
978
|
+
throw new Error(`Failed to download caption: ${response.statusText}`);
|
|
979
|
+
}
|
|
980
|
+
return await response.text();
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Parses the given caption content to extract relevant information.
|
|
984
|
+
*
|
|
985
|
+
* @param {string} captionContent - The caption content to parse.
|
|
986
|
+
* @returns {string} The extracted caption information as a string.
|
|
987
|
+
*/
|
|
988
|
+
parseCaption(captionContent) {
|
|
989
|
+
logger3.log("Parsing caption");
|
|
990
|
+
try {
|
|
991
|
+
const jsonContent = JSON.parse(captionContent);
|
|
992
|
+
if (jsonContent.events) {
|
|
993
|
+
return jsonContent.events.filter((event) => event.segs).map((event) => event.segs.map((seg) => seg.utf8).join("")).join("").replace("\n", " ");
|
|
994
|
+
}
|
|
995
|
+
logger3.log("Unexpected caption format:", jsonContent);
|
|
996
|
+
return "Error: Unable to parse captions";
|
|
997
|
+
} catch (error) {
|
|
998
|
+
logger3.log("Error parsing caption:", error);
|
|
999
|
+
return "Error: Unable to parse captions";
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Parses SRT (SubRip) content to extract subtitles.
|
|
1004
|
+
*
|
|
1005
|
+
* @param {string} srtContent - The SRT content to parse.
|
|
1006
|
+
* @returns {string} The parsed subtitles as a single string.
|
|
1007
|
+
*/
|
|
1008
|
+
parseSRT(srtContent) {
|
|
1009
|
+
return srtContent.split("\n\n").map((block) => block.split("\n").slice(2).join(" ")).join(" ");
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Asynchronously downloads a SubRip subtitle file from the specified URL.
|
|
1013
|
+
*
|
|
1014
|
+
* @param {string} url - The URL of the subtitle file to download.
|
|
1015
|
+
* @returns {Promise<string>} A promise that resolves to the text content of the downloaded subtitle file.
|
|
1016
|
+
*/
|
|
1017
|
+
async downloadSRT(url) {
|
|
1018
|
+
logger3.log("downloadSRT");
|
|
1019
|
+
const response = await fetch(url);
|
|
1020
|
+
return await response.text();
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Asynchronously transcribes audio from the provided URL using the agent runtime.
|
|
1024
|
+
*
|
|
1025
|
+
* @param {string} url - The URL of the audio file to transcribe.
|
|
1026
|
+
* @param {IAgentRuntime} runtime - The agent runtime to use for transcription.
|
|
1027
|
+
* @returns {Promise<string>} A promise that resolves with the transcription result, or "Transcription failed" if the process was unsuccessful.
|
|
1028
|
+
*/
|
|
1029
|
+
async transcribeAudio(url, runtime) {
|
|
1030
|
+
logger3.log("Preparing audio for transcription...");
|
|
1031
|
+
try {
|
|
1032
|
+
await new Promise((resolve, reject) => {
|
|
1033
|
+
ffmpeg.getAvailableCodecs((err, _codecs) => {
|
|
1034
|
+
if (err) reject(err);
|
|
1035
|
+
resolve(null);
|
|
1036
|
+
});
|
|
1037
|
+
});
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
logger3.log("FFmpeg not found:", error);
|
|
1040
|
+
return null;
|
|
1041
|
+
}
|
|
1042
|
+
const mp4FilePath = path2.join(this.dataDir, `${this.getVideoId(url)}.mp4`);
|
|
1043
|
+
const webmFilePath = path2.join(
|
|
1044
|
+
this.dataDir,
|
|
1045
|
+
`${this.getVideoId(url)}.webm`
|
|
1046
|
+
);
|
|
1047
|
+
const mp3FilePath = path2.join(this.dataDir, `${this.getVideoId(url)}.mp3`);
|
|
1048
|
+
if (!fs2.existsSync(mp3FilePath)) {
|
|
1049
|
+
if (fs2.existsSync(webmFilePath)) {
|
|
1050
|
+
logger3.log("WEBM file found. Converting to MP3...");
|
|
1051
|
+
await this.convertWebmToMp3(webmFilePath, mp3FilePath);
|
|
1052
|
+
} else if (fs2.existsSync(mp4FilePath)) {
|
|
1053
|
+
logger3.log("MP4 file found. Converting to MP3...");
|
|
1054
|
+
await this.convertMp4ToMp3(mp4FilePath, mp3FilePath);
|
|
1055
|
+
} else {
|
|
1056
|
+
logger3.log("Downloading audio...");
|
|
1057
|
+
await this.downloadAudio(url, mp3FilePath);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
logger3.log(`Audio prepared at ${mp3FilePath}`);
|
|
1061
|
+
const audioBuffer = fs2.readFileSync(mp3FilePath);
|
|
1062
|
+
logger3.log(`Audio file size: ${audioBuffer.length} bytes`);
|
|
1063
|
+
logger3.log("Starting transcription...");
|
|
1064
|
+
const startTime = Date.now();
|
|
1065
|
+
const transcript = await runtime.useModel(
|
|
1066
|
+
ModelTypes2.TRANSCRIPTION,
|
|
1067
|
+
audioBuffer
|
|
1068
|
+
);
|
|
1069
|
+
const endTime = Date.now();
|
|
1070
|
+
logger3.log(
|
|
1071
|
+
`Transcription completed in ${(endTime - startTime) / 1e3} seconds`
|
|
1072
|
+
);
|
|
1073
|
+
return transcript || "Transcription failed";
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Converts a given MP4 file to MP3 format.
|
|
1077
|
+
*
|
|
1078
|
+
* @param {string} inputPath - The path to the input MP4 file.
|
|
1079
|
+
* @param {string} outputPath - The desired path for the output MP3 file.
|
|
1080
|
+
* @returns {Promise<void>} A Promise that resolves once the conversion is complete or rejects with an error.
|
|
1081
|
+
*/
|
|
1082
|
+
async convertMp4ToMp3(inputPath, outputPath) {
|
|
1083
|
+
return new Promise((resolve, reject) => {
|
|
1084
|
+
ffmpeg(inputPath).output(outputPath).noVideo().audioCodec("libmp3lame").on("end", () => {
|
|
1085
|
+
logger3.log("Conversion to MP3 complete");
|
|
1086
|
+
resolve();
|
|
1087
|
+
}).on("error", (err) => {
|
|
1088
|
+
logger3.log("Error converting to MP3:", err);
|
|
1089
|
+
reject(err);
|
|
1090
|
+
}).run();
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Convert a WebM file to MP3 format.
|
|
1095
|
+
*
|
|
1096
|
+
* @param {string} inputPath - The path of the WebM file to convert.
|
|
1097
|
+
* @param {string} outputPath - The path where the MP3 file will be saved.
|
|
1098
|
+
* @returns {Promise<void>} Promise that resolves when the conversion is complete.
|
|
1099
|
+
*/
|
|
1100
|
+
async convertWebmToMp3(inputPath, outputPath) {
|
|
1101
|
+
return new Promise((resolve, reject) => {
|
|
1102
|
+
ffmpeg(inputPath).output(outputPath).noVideo().audioCodec("libmp3lame").on("end", () => {
|
|
1103
|
+
logger3.log("Conversion to MP3 complete");
|
|
1104
|
+
resolve();
|
|
1105
|
+
}).on("error", (err) => {
|
|
1106
|
+
logger3.log("Error converting to MP3:", err);
|
|
1107
|
+
reject(err);
|
|
1108
|
+
}).run();
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Downloads audio from a given URL and saves it to the specified output file.
|
|
1113
|
+
* If no output file is provided, it will default to saving the audio in the data directory with the video ID as the filename.
|
|
1114
|
+
* Supports downloading and converting MP4 files to MP3 as well as downloading audio from YouTube videos using youtube-dl.
|
|
1115
|
+
*
|
|
1116
|
+
* @param url - The URL of the audio to download.
|
|
1117
|
+
* @param outputFile - The path to save the downloaded audio file. If not provided, it defaults to saving in the data directory with the video ID as the filename.
|
|
1118
|
+
* @returns A Promise that resolves with the path to the downloaded audio file.
|
|
1119
|
+
* @throws Error if there is an issue during the download process.
|
|
1120
|
+
*/
|
|
1121
|
+
async downloadAudio(url, outputFile) {
|
|
1122
|
+
logger3.log("Downloading audio");
|
|
1123
|
+
outputFile = outputFile ?? path2.join(this.dataDir, `${this.getVideoId(url)}.mp3`);
|
|
1124
|
+
try {
|
|
1125
|
+
if (url.endsWith(".mp4") || url.includes(".mp4?")) {
|
|
1126
|
+
logger3.log(
|
|
1127
|
+
"Direct MP4 file detected, downloading and converting to MP3"
|
|
1128
|
+
);
|
|
1129
|
+
const tempMp4File = path2.join(tmpdir(), `${this.getVideoId(url)}.mp4`);
|
|
1130
|
+
const response = await fetch(url);
|
|
1131
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
1132
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
1133
|
+
fs2.writeFileSync(tempMp4File, buffer);
|
|
1134
|
+
await new Promise((resolve, reject) => {
|
|
1135
|
+
ffmpeg(tempMp4File).output(outputFile).noVideo().audioCodec("libmp3lame").on("end", () => {
|
|
1136
|
+
fs2.unlinkSync(tempMp4File);
|
|
1137
|
+
resolve();
|
|
1138
|
+
}).on("error", (err) => {
|
|
1139
|
+
reject(err);
|
|
1140
|
+
}).run();
|
|
1141
|
+
});
|
|
1142
|
+
} else {
|
|
1143
|
+
logger3.log("YouTube video detected, downloading audio with youtube-dl");
|
|
1144
|
+
await getYoutubeDL()(url, {
|
|
1145
|
+
verbose: true,
|
|
1146
|
+
extractAudio: true,
|
|
1147
|
+
audioFormat: "mp3",
|
|
1148
|
+
output: outputFile,
|
|
1149
|
+
writeInfoJson: true
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
return outputFile;
|
|
1153
|
+
} catch (error) {
|
|
1154
|
+
logger3.log("Error downloading audio:", error);
|
|
1155
|
+
throw new Error("Failed to download audio");
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
|
|
1160
|
+
// src/index.ts
|
|
1161
|
+
var nodePlugin = {
|
|
1162
|
+
name: "default",
|
|
1163
|
+
description: "Default plugin, with basic actions and evaluators",
|
|
1164
|
+
services: [BrowserService, PdfService, VideoService, AwsS3Service],
|
|
1165
|
+
actions: []
|
|
1166
|
+
};
|
|
1167
|
+
export {
|
|
1168
|
+
AwsS3Service,
|
|
1169
|
+
BrowserService,
|
|
1170
|
+
PdfService,
|
|
1171
|
+
VideoService,
|
|
1172
|
+
nodePlugin
|
|
1173
|
+
};
|
|
1174
|
+
//# sourceMappingURL=index.js.map
|