@mixio-pro/kalaasetu-mcp 1.1.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.ts +10 -9
- package/src/test-context.ts +52 -0
- package/src/test-error-handling.ts +31 -0
- package/src/tools/fal/config.ts +95 -1
- package/src/tools/fal/generate.ts +48 -17
- package/src/tools/fal/index.ts +2 -2
- package/src/tools/fal/models.ts +73 -27
- package/src/tools/fal/storage.ts +62 -58
- package/src/tools/gemini.ts +263 -237
- package/src/tools/image-to-video.ts +199 -185
- package/src/tools/perplexity.ts +194 -154
- package/src/tools/youtube.ts +52 -33
- package/src/utils/tool-wrapper.ts +86 -0
package/src/tools/gemini.ts
CHANGED
|
@@ -11,6 +11,7 @@ import * as wav from "wav";
|
|
|
11
11
|
import { PassThrough } from "stream";
|
|
12
12
|
import { getStorage } from "../storage";
|
|
13
13
|
import { generateTimestampedFilename } from "../utils/filename";
|
|
14
|
+
import { safeToolExecute } from "../utils/tool-wrapper";
|
|
14
15
|
|
|
15
16
|
const ai = new GoogleGenAI({
|
|
16
17
|
apiKey: process.env.GEMINI_API_KEY || "",
|
|
@@ -128,11 +129,17 @@ async function uploadFileToGemini(filePath: string): Promise<any> {
|
|
|
128
129
|
fs.unlinkSync(localPath);
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
// Wait for file processing to complete
|
|
132
|
+
// Wait for file processing to complete (max 60 seconds)
|
|
132
133
|
let getFile = await ai.files.get({ name: uploadedFile.name! });
|
|
133
|
-
|
|
134
|
+
let attempts = 0;
|
|
135
|
+
while (getFile.state === "PROCESSING" && attempts < 20) {
|
|
134
136
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
135
137
|
getFile = await ai.files.get({ name: uploadedFile.name! });
|
|
138
|
+
attempts++;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (getFile.state === "PROCESSING") {
|
|
142
|
+
throw new Error("File processing timed out after 60 seconds");
|
|
136
143
|
}
|
|
137
144
|
|
|
138
145
|
if (getFile.state === "FAILED") {
|
|
@@ -213,74 +220,77 @@ export const geminiTextToImage = {
|
|
|
213
220
|
.optional()
|
|
214
221
|
.describe("Optional reference image file paths to guide generation"),
|
|
215
222
|
}),
|
|
223
|
+
timeoutMs: 300000,
|
|
216
224
|
execute: async (args: {
|
|
217
225
|
prompt: string;
|
|
218
226
|
aspect_ratio?: string;
|
|
219
227
|
output_path?: string;
|
|
220
228
|
reference_images?: string[];
|
|
221
229
|
}) => {
|
|
222
|
-
|
|
223
|
-
|
|
230
|
+
return safeToolExecute(async () => {
|
|
231
|
+
try {
|
|
232
|
+
const contents: any[] = [args.prompt];
|
|
224
233
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
234
|
+
if (args.reference_images && Array.isArray(args.reference_images)) {
|
|
235
|
+
for (const refPath of args.reference_images) {
|
|
236
|
+
contents.push(await fileToGenerativePart(refPath));
|
|
237
|
+
}
|
|
228
238
|
}
|
|
229
|
-
}
|
|
230
239
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
240
|
+
const response = await ai.models.generateContent({
|
|
241
|
+
model: "gemini-3-pro-image-preview",
|
|
242
|
+
contents: contents,
|
|
243
|
+
config: {
|
|
244
|
+
responseModalities: ["TEXT", "IMAGE"],
|
|
245
|
+
imageConfig: {
|
|
246
|
+
aspectRatio: args.aspect_ratio || "9:16",
|
|
247
|
+
},
|
|
238
248
|
},
|
|
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
|
-
|
|
264
|
-
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const images = [];
|
|
252
|
+
let textResponse = "";
|
|
253
|
+
|
|
254
|
+
if (response.candidates && response.candidates[0]?.content?.parts) {
|
|
255
|
+
for (const part of response.candidates[0].content.parts) {
|
|
256
|
+
if (part.text) {
|
|
257
|
+
textResponse += part.text;
|
|
258
|
+
} else if (part.inlineData?.data) {
|
|
259
|
+
const imageData = part.inlineData.data;
|
|
260
|
+
// Always save the image - use provided path or generate one
|
|
261
|
+
const outputPath =
|
|
262
|
+
args.output_path ||
|
|
263
|
+
generateTimestampedFilename("generated_image.png");
|
|
264
|
+
const storage = getStorage();
|
|
265
|
+
const url = await storage.writeFile(
|
|
266
|
+
outputPath,
|
|
267
|
+
Buffer.from(imageData, "base64")
|
|
268
|
+
);
|
|
269
|
+
images.push({
|
|
270
|
+
url,
|
|
271
|
+
filename: outputPath,
|
|
272
|
+
mimeType: "image/png",
|
|
273
|
+
});
|
|
274
|
+
}
|
|
265
275
|
}
|
|
266
276
|
}
|
|
267
|
-
}
|
|
268
277
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
278
|
+
if (images.length > 0) {
|
|
279
|
+
// Return the URL directly for easy parsing
|
|
280
|
+
return JSON.stringify({
|
|
281
|
+
url: images?.[0]?.url,
|
|
282
|
+
images,
|
|
283
|
+
message: textResponse || "Image generated successfully",
|
|
284
|
+
});
|
|
285
|
+
}
|
|
277
286
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
287
|
+
return (
|
|
288
|
+
textResponse || "Image generation completed but no image was produced"
|
|
289
|
+
);
|
|
290
|
+
} catch (error: any) {
|
|
291
|
+
throw new Error(`Image generation failed: ${error.message}`);
|
|
292
|
+
}
|
|
293
|
+
}, "gemini-generateImage");
|
|
284
294
|
},
|
|
285
295
|
};
|
|
286
296
|
|
|
@@ -300,63 +310,68 @@ export const geminiEditImage = {
|
|
|
300
310
|
.optional()
|
|
301
311
|
.describe("Additional image paths for reference"),
|
|
302
312
|
}),
|
|
313
|
+
timeoutMs: 300000,
|
|
303
314
|
execute: async (args: {
|
|
304
315
|
image_path: string;
|
|
305
316
|
prompt: string;
|
|
306
317
|
output_path?: string;
|
|
307
318
|
reference_images?: string[];
|
|
308
319
|
}) => {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
320
|
+
return safeToolExecute(async () => {
|
|
321
|
+
try {
|
|
322
|
+
const imagePart = await fileToGenerativePart(args.image_path);
|
|
323
|
+
const contents: any[] = [args.prompt, imagePart];
|
|
312
324
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
325
|
+
if (args.reference_images) {
|
|
326
|
+
for (const refPath of args.reference_images) {
|
|
327
|
+
contents.push(await fileToGenerativePart(refPath));
|
|
328
|
+
}
|
|
316
329
|
}
|
|
317
|
-
}
|
|
318
330
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
331
|
+
const response = await ai.models.generateContent({
|
|
332
|
+
model: "gemini-3-pro-image-preview",
|
|
333
|
+
contents: contents,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const images = [];
|
|
337
|
+
let textResponse = "";
|
|
338
|
+
|
|
339
|
+
if (response.candidates && response.candidates[0]?.content?.parts) {
|
|
340
|
+
for (const part of response.candidates[0].content.parts) {
|
|
341
|
+
if (part.text) {
|
|
342
|
+
textResponse += part.text;
|
|
343
|
+
} else if (part.inlineData?.data) {
|
|
344
|
+
const imageData = part.inlineData.data;
|
|
345
|
+
if (args.output_path) {
|
|
346
|
+
const storage = getStorage();
|
|
347
|
+
const url = await storage.writeFile(
|
|
348
|
+
args.output_path,
|
|
349
|
+
Buffer.from(imageData, "base64")
|
|
350
|
+
);
|
|
351
|
+
images.push({
|
|
352
|
+
url,
|
|
353
|
+
filename: args.output_path,
|
|
354
|
+
mimeType: "image/png",
|
|
355
|
+
});
|
|
356
|
+
}
|
|
344
357
|
}
|
|
345
358
|
}
|
|
346
359
|
}
|
|
347
|
-
}
|
|
348
360
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
361
|
+
if (images.length > 0) {
|
|
362
|
+
return JSON.stringify({
|
|
363
|
+
images,
|
|
364
|
+
message: textResponse || "Image edited successfully",
|
|
365
|
+
});
|
|
366
|
+
}
|
|
355
367
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
368
|
+
return (
|
|
369
|
+
textResponse || "Image editing completed but no response received"
|
|
370
|
+
);
|
|
371
|
+
} catch (error: any) {
|
|
372
|
+
throw new Error(`Image editing failed: ${error.message}`);
|
|
373
|
+
}
|
|
374
|
+
}, "gemini-editImage");
|
|
360
375
|
},
|
|
361
376
|
};
|
|
362
377
|
|
|
@@ -370,59 +385,62 @@ export const geminiAnalyzeImages = {
|
|
|
370
385
|
.describe("Array of image file paths to analyze"),
|
|
371
386
|
prompt: z.string().describe("Text prompt or question about the images"),
|
|
372
387
|
}),
|
|
388
|
+
timeoutMs: 300000,
|
|
373
389
|
execute: async (args: { image_paths: string[]; prompt: string }) => {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
390
|
+
return safeToolExecute(async () => {
|
|
391
|
+
try {
|
|
392
|
+
// Handle array parsing
|
|
393
|
+
if (!args.image_paths) {
|
|
394
|
+
throw new Error("Image paths not provided");
|
|
395
|
+
}
|
|
379
396
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
397
|
+
// Convert to array if passed as string
|
|
398
|
+
let imagePaths: string[];
|
|
399
|
+
if (typeof args.image_paths === "string") {
|
|
400
|
+
const strValue = args.image_paths as string;
|
|
401
|
+
if (strValue.startsWith("[") && strValue.endsWith("]")) {
|
|
402
|
+
try {
|
|
403
|
+
imagePaths = JSON.parse(strValue);
|
|
404
|
+
} catch {
|
|
405
|
+
throw new Error("Invalid image_paths format");
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
imagePaths = [strValue];
|
|
389
409
|
}
|
|
410
|
+
} else if (Array.isArray(args.image_paths)) {
|
|
411
|
+
imagePaths = args.image_paths;
|
|
390
412
|
} else {
|
|
391
|
-
|
|
413
|
+
throw new Error("Invalid image_paths: must be array or string");
|
|
392
414
|
}
|
|
393
|
-
} else if (Array.isArray(args.image_paths)) {
|
|
394
|
-
imagePaths = args.image_paths;
|
|
395
|
-
} else {
|
|
396
|
-
throw new Error("Invalid image_paths: must be array or string");
|
|
397
|
-
}
|
|
398
415
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
416
|
+
if (imagePaths.length === 0) {
|
|
417
|
+
throw new Error("At least one image path must be provided");
|
|
418
|
+
}
|
|
402
419
|
|
|
403
|
-
|
|
420
|
+
const contents: any[] = [args.prompt];
|
|
404
421
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
422
|
+
for (const imagePath of imagePaths) {
|
|
423
|
+
contents.push(await fileToGenerativePart(imagePath));
|
|
424
|
+
}
|
|
408
425
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
426
|
+
const response = await ai.models.generateContent({
|
|
427
|
+
model: "gemini-2.5-pro",
|
|
428
|
+
contents: contents,
|
|
429
|
+
});
|
|
413
430
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
431
|
+
let result = "";
|
|
432
|
+
if (response.candidates && response.candidates[0]?.content?.parts) {
|
|
433
|
+
for (const part of response.candidates[0].content.parts) {
|
|
434
|
+
if (part.text) {
|
|
435
|
+
result += part.text;
|
|
436
|
+
}
|
|
419
437
|
}
|
|
420
438
|
}
|
|
439
|
+
return result || "Analysis completed but no text response received";
|
|
440
|
+
} catch (error: any) {
|
|
441
|
+
throw new Error(`Image analysis failed: ${error.message}`);
|
|
421
442
|
}
|
|
422
|
-
|
|
423
|
-
} catch (error: any) {
|
|
424
|
-
throw new Error(`Image analysis failed: ${error.message}`);
|
|
425
|
-
}
|
|
443
|
+
}, "gemini-analyzeImages");
|
|
426
444
|
},
|
|
427
445
|
};
|
|
428
446
|
|
|
@@ -444,53 +462,56 @@ export const geminiSingleSpeakerTts = {
|
|
|
444
462
|
"Output WAV file path (optional, defaults to timestamp-based filename)"
|
|
445
463
|
),
|
|
446
464
|
}),
|
|
465
|
+
timeoutMs: 300000,
|
|
447
466
|
execute: async (args: {
|
|
448
467
|
text: string;
|
|
449
468
|
voice_name: string;
|
|
450
469
|
output_path?: string;
|
|
451
470
|
}) => {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
471
|
+
return safeToolExecute(async () => {
|
|
472
|
+
try {
|
|
473
|
+
const response = await ai.models.generateContent({
|
|
474
|
+
model: "gemini-2.5-pro-preview-tts",
|
|
475
|
+
contents: [{ parts: [{ text: args.text }] }],
|
|
476
|
+
config: {
|
|
477
|
+
responseModalities: ["AUDIO"],
|
|
478
|
+
speechConfig: {
|
|
479
|
+
voiceConfig: {
|
|
480
|
+
prebuiltVoiceConfig: {
|
|
481
|
+
voiceName: args.voice_name || "Despina",
|
|
482
|
+
},
|
|
462
483
|
},
|
|
463
484
|
},
|
|
464
485
|
},
|
|
465
|
-
}
|
|
466
|
-
});
|
|
486
|
+
});
|
|
467
487
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
488
|
+
const data =
|
|
489
|
+
response.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data;
|
|
490
|
+
if (!data) {
|
|
491
|
+
throw new Error("No audio data received from Gemini API");
|
|
492
|
+
}
|
|
473
493
|
|
|
474
|
-
|
|
494
|
+
const audioBuffer = Buffer.from(data, "base64");
|
|
475
495
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
496
|
+
// Use provided output path or generate default with timestamp
|
|
497
|
+
const outputPath =
|
|
498
|
+
args.output_path || generateTimestampedFilename("voice_output.wav");
|
|
479
499
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
500
|
+
const storage = getStorage();
|
|
501
|
+
const url = await storage.writeFile(outputPath, audioBuffer);
|
|
502
|
+
|
|
503
|
+
return JSON.stringify({
|
|
504
|
+
audio: {
|
|
505
|
+
url,
|
|
506
|
+
filename: outputPath,
|
|
507
|
+
mimeType: "audio/wav",
|
|
508
|
+
},
|
|
509
|
+
message: "Audio generated successfully",
|
|
510
|
+
});
|
|
511
|
+
} catch (error: any) {
|
|
512
|
+
throw new Error(`Voice generation failed: ${error.message}`);
|
|
513
|
+
}
|
|
514
|
+
}, "gemini-generateSpeech");
|
|
494
515
|
},
|
|
495
516
|
};
|
|
496
517
|
|
|
@@ -530,6 +551,7 @@ export const geminiAnalyzeVideos = {
|
|
|
530
551
|
"Media resolution: 'default' or 'low' (low resolution uses ~100 tokens/sec vs 300 tokens/sec)"
|
|
531
552
|
),
|
|
532
553
|
}),
|
|
554
|
+
timeoutMs: 300000,
|
|
533
555
|
execute: async (args: {
|
|
534
556
|
video_inputs: string[];
|
|
535
557
|
prompt: string;
|
|
@@ -538,86 +560,90 @@ export const geminiAnalyzeVideos = {
|
|
|
538
560
|
end_offset?: string;
|
|
539
561
|
media_resolution?: string;
|
|
540
562
|
}) => {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
563
|
+
return safeToolExecute(async () => {
|
|
564
|
+
try {
|
|
565
|
+
// Handle array parsing
|
|
566
|
+
if (!args.video_inputs) {
|
|
567
|
+
throw new Error("Video inputs not provided");
|
|
568
|
+
}
|
|
546
569
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
570
|
+
// Convert to array if passed as string
|
|
571
|
+
let videoInputs: string[];
|
|
572
|
+
if (typeof args.video_inputs === "string") {
|
|
573
|
+
const strValue = args.video_inputs as string;
|
|
574
|
+
if (strValue.startsWith("[") && strValue.endsWith("]")) {
|
|
575
|
+
try {
|
|
576
|
+
videoInputs = JSON.parse(strValue);
|
|
577
|
+
} catch {
|
|
578
|
+
throw new Error("Invalid video_inputs format");
|
|
579
|
+
}
|
|
580
|
+
} else {
|
|
581
|
+
videoInputs = [strValue];
|
|
556
582
|
}
|
|
583
|
+
} else if (Array.isArray(args.video_inputs)) {
|
|
584
|
+
videoInputs = args.video_inputs;
|
|
557
585
|
} else {
|
|
558
|
-
|
|
586
|
+
throw new Error("Invalid video_inputs: must be array or string");
|
|
559
587
|
}
|
|
560
|
-
} else if (Array.isArray(args.video_inputs)) {
|
|
561
|
-
videoInputs = args.video_inputs;
|
|
562
|
-
} else {
|
|
563
|
-
throw new Error("Invalid video_inputs: must be array or string");
|
|
564
|
-
}
|
|
565
588
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
589
|
+
if (videoInputs.length === 0) {
|
|
590
|
+
throw new Error("At least one video input must be provided");
|
|
591
|
+
}
|
|
569
592
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
593
|
+
if (videoInputs.length > 10) {
|
|
594
|
+
throw new Error(
|
|
595
|
+
"Maximum 10 videos per request allowed for Gemini 2.5+ models"
|
|
596
|
+
);
|
|
597
|
+
}
|
|
575
598
|
|
|
576
|
-
|
|
577
|
-
|
|
599
|
+
// Prepare video parts for content
|
|
600
|
+
const videoParts: any[] = [];
|
|
578
601
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
602
|
+
// Process each video input
|
|
603
|
+
for (const videoInput of videoInputs) {
|
|
604
|
+
const videoConfig = {
|
|
605
|
+
fps: args.fps || (isYouTubeUrl(videoInput) ? 1 : 5), // Default 5 FPS for local, 1 FPS for YouTube
|
|
606
|
+
startOffset: args.start_offset,
|
|
607
|
+
endOffset: args.end_offset,
|
|
608
|
+
};
|
|
586
609
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
610
|
+
const videoPart = await processVideoInput(videoInput, videoConfig);
|
|
611
|
+
videoParts.push(videoPart);
|
|
612
|
+
}
|
|
590
613
|
|
|
591
|
-
|
|
592
|
-
|
|
614
|
+
// Build content using createUserContent and createPartFromUri for uploaded files
|
|
615
|
+
const contentParts: any[] = [args.prompt];
|
|
593
616
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
617
|
+
for (const videoPart of videoParts) {
|
|
618
|
+
if (videoPart.uri && videoPart.mimeType) {
|
|
619
|
+
contentParts.push(
|
|
620
|
+
createPartFromUri(videoPart.uri, videoPart.mimeType)
|
|
621
|
+
);
|
|
622
|
+
}
|
|
599
623
|
}
|
|
600
|
-
}
|
|
601
624
|
|
|
602
|
-
|
|
625
|
+
const finalContents = createUserContent(contentParts);
|
|
603
626
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
627
|
+
const response = await ai.models.generateContent({
|
|
628
|
+
model: "gemini-2.5-pro",
|
|
629
|
+
contents: finalContents,
|
|
630
|
+
});
|
|
608
631
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
632
|
+
let result = "";
|
|
633
|
+
if (response.candidates && response.candidates[0]?.content?.parts) {
|
|
634
|
+
for (const part of response.candidates[0].content.parts) {
|
|
635
|
+
if (part.text) {
|
|
636
|
+
result += part.text;
|
|
637
|
+
}
|
|
614
638
|
}
|
|
615
639
|
}
|
|
616
|
-
}
|
|
617
640
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
641
|
+
return (
|
|
642
|
+
result || "Video analysis completed but no text response received"
|
|
643
|
+
);
|
|
644
|
+
} catch (error: any) {
|
|
645
|
+
throw new Error(`Video analysis failed: ${error.message}`);
|
|
646
|
+
}
|
|
647
|
+
}, "gemini-analyzeVideos");
|
|
622
648
|
},
|
|
623
649
|
};
|