@mixio-pro/kalaasetu-mcp
Version:
A powerful Model Context Protocol server providing AI tools for content generation and analysis
162 lines (132 loc) • 6.34 kB
text/typescript
import { z } from "zod";
import * as fs from "fs";
import * as path from "path";
import { GoogleGenAI } from "@google/genai";
async function wait(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function fileToBase64(filePath: string): { data: string; mimeType: string } {
if (!fs.existsSync(filePath)) {
throw new Error(`File not found: ${filePath}`);
}
const buf = fs.readFileSync(filePath);
const data = Buffer.from(buf).toString("base64");
// Detect mime type from extension
const ext = path.extname(filePath).toLowerCase();
const mimeType = ext === '.jpg' || ext === '.jpeg' ? 'image/jpeg' :
ext === '.png' ? 'image/png' :
ext === '.webp' ? 'image/webp' : 'image/png';
return { data, mimeType };
}
export const imageToVideo = {
name: "imageToVideo",
description: "Generate videos from an image as starting first frame using Vertex Veo models (predictLongRunning + fetchPredictOperation).",
parameters: z.object({
prompt: z.string().describe("Text description for the video"),
image_path: z.string().optional().describe("Path to source image for image-to-video generation"),
aspect_ratio: z.string().optional().describe("Video aspect ratio: '16:9' or '9:16' (default: '9:16')"),
duration_seconds: z.number().optional().describe("Video duration in seconds: 4, 6, or 8 (default: 6)"),
resolution: z.string().optional().describe("Video resolution: '720p' or '1080p' (default: '720p')"),
negative_prompt: z.string().optional().describe("Text describing what not to include in the video"),
person_generation: z.string().optional().describe("Controls generation of people: 'allow_adult' (default for image-to-video) or 'allow_all'"),
reference_images: z.array(z.string()).optional().describe("Additional image paths for reference (max 3)"),
output_path: z.string().optional().describe("Output MP4 file path (if multiple predictions, index suffix is added)"),
gemini_api_key: z.string().optional().describe("Gemini API key (uses GEMINI_API_KEY env var if not provided)"),
model_id: z.string().optional().describe("Model ID (default: veo-2.0-generate-001)"),
}),
execute: async (args: {
prompt: string;
image_path?: string;
aspect_ratio?: string;
duration_seconds?: number;
resolution?: string;
negative_prompt?: string;
person_generation?: string;
reference_images?: string[];
output_path?: string;
gemini_api_key?: string;
model_id?: string;
}) => {
const apiKey = args.gemini_api_key || process.env.GEMINI_API_KEY;
if (!apiKey) {
throw new Error("Gemini API key is required. Set GEMINI_API_KEY environment variable or pass gemini_api_key parameter. Get one at https://aistudio.google.com/app/apikey");
}
const model = args.model_id || "veo-2.0-generate-001";
// Initialize Google GenAI client
const genai = new GoogleGenAI({ apiKey });
// Build config for video generation
const config: any = {};
if (args.duration_seconds !== undefined) {
config.duration_seconds = args.duration_seconds;
} else {
config.duration_seconds = 6; // default
}
if (args.aspect_ratio) {
config.aspect_ratio = args.aspect_ratio;
}
try {
// Start video generation operation
console.log(`Starting video generation with model: ${model}`);
let operation = await genai.models.generateVideos({
model,
prompt: args.prompt,
config,
});
console.log("Operation started, waiting for completion...");
// Poll until operation is complete (max 10 minutes)
let tries = 0;
const maxTries = 60; // 10 minutes with 10s intervals
while (!operation.done && tries < maxTries) {
await wait(10000); // Wait 10 seconds
tries++;
console.log(`Polling attempt ${tries}/${maxTries}...`);
operation = await genai.operations.getVideosOperation({
operation: operation,
});
}
if (!operation.done) {
throw new Error("Video generation timed out after 10 minutes");
}
console.log("Operation completed!");
console.log("Full Response:", JSON.stringify(operation.response, null, 2));
// Extract generated videos from response
const generatedVideos = operation.response?.generatedVideos || [];
if (!generatedVideos || generatedVideos.length === 0) {
const respStr = JSON.stringify(operation.response, null, 2);
return `Video generation completed but no videos found in response.\n\nFull Response:\n${respStr.slice(0, 2000)}${respStr.length > 2000 ? '\n...(truncated)' : ''}`;
}
// Download and save videos
const outputs: string[] = [];
for (let i = 0; i < generatedVideos.length; i++) {
const generatedVideo = generatedVideos[i];
const videoUri = generatedVideo?.video?.uri;
if (!videoUri) {
console.warn(`Video ${i} has no URI`);
continue;
}
console.log(`Downloading video ${i + 1}/${generatedVideos.length}...`);
// Download video from URI
const videoUrl = `${videoUri}&key=${apiKey}`;
const response = await fetch(videoUrl);
if (!response.ok) {
throw new Error(`Failed to download video: ${response.status} ${response.statusText}`);
}
const buffer = await response.arrayBuffer();
// Save video to file
const filePath = args.output_path
? (i === 0 ? args.output_path : args.output_path.replace(/\.mp4$/i, `_${i}.mp4`))
: `video_output_${Date.now()}${i === 0 ? '' : '_' + i}.mp4`;
const absPath = path.resolve(filePath);
fs.writeFileSync(absPath, Buffer.from(buffer));
outputs.push(absPath);
console.log(`Saved video to: ${absPath}`);
}
if (outputs.length > 0) {
return `Video(s) saved successfully:\n${outputs.map((p, i) => `${i + 1}. ${p}`).join('\n')}`;
}
return "Video generation completed but no videos were saved.";
} catch (error: any) {
throw new Error(`Video generation failed: ${error.message || JSON.stringify(error)}`);
}
},
};