UNPKG

contaigents

Version:

Modular AI Content Ecosystem with Audio Generation

246 lines (241 loc) 9.43 kB
import { BaseTool } from './BaseTool.js'; import { AudioGenerator } from '../audioGenerator.js'; import { LLMFactory } from '../llm/LLMFactory.js'; import path from 'path'; import fs from 'fs/promises'; export class AudioGenerationTool extends BaseTool { constructor(baseDir) { super(); this.baseDir = baseDir || process.cwd(); } getName() { return 'audio_generation'; } getDescription() { return 'Generate audio files from text using AI providers (Gemini TTS). Supports various voices and automatically saves files to specified paths.'; } getAgentGuidance() { return ` ## 🎵 AUDIO GENERATION BEST PRACTICES When using the audio_generation tool, consider these guidelines for optimal results: ### 📝 TEXT OPTIMIZATION: - **Use clear, natural language** that flows well when spoken - **Add punctuation** for proper pauses and intonation - **Break up long sentences** for better pacing - **Consider pronunciation** of technical terms or names ### 🎭 VOICE SELECTION: - **zephyr**: Warm, friendly voice (default) - **despina**: Professional, clear voice - **Choose based on content type** (casual vs formal) ### 💡 CONTENT TIPS: - Scripts and narrations work best with clear structure - Add natural pauses with commas and periods - Consider the listening context (podcast, presentation, etc.) `; } getParameters() { return [ { name: 'text', type: 'string', description: 'Text content to convert to audio', required: true }, { name: 'output_path', type: 'string', description: 'Output file path (relative to project root)', required: true }, { name: 'provider', type: 'string', description: 'AI provider for audio generation (default: "gemini")', required: false, default: 'gemini' }, { name: 'voice', type: 'string', description: 'Voice to use for generation (default: "zephyr")', required: false, default: 'zephyr' }, { name: 'model', type: 'string', description: 'Specific model to use (provider-dependent)', required: false } ]; } async execute(parameters) { // Validate parameters const validationError = this.validateParameters(parameters); if (validationError) { return validationError; } const { text, output_path, provider = 'gemini', voice = 'zephyr', model } = parameters; try { // Validate text content if (!text || typeof text !== 'string' || !text.trim()) { return { success: false, error: 'Text content cannot be empty' }; } // Get configured LLM provider for API key const llmProvider = await this.getLLMProviderForAudio(provider); if (!llmProvider) { return { success: false, error: `No configured ${provider} provider found. Please configure the ${provider} provider using 'contaigents configure' command.` }; } // Validate and resolve output path const pathValidation = await this.validateAndResolveOutputPath(output_path); if (!pathValidation.success) { return { success: false, error: pathValidation.error }; } // Initialize AudioGenerator with API key from LLM config const audioGenerator = new AudioGenerator({ provider, apiKey: llmProvider.apiKey, voice, model }); // Validate voice if available if (!audioGenerator.isVoiceAvailable(voice)) { const availableVoices = audioGenerator.getAvailableVoices(); return { success: false, error: `Voice "${voice}" is not available for provider "${provider}". Available voices: ${availableVoices.join(', ')}` }; } // Generate audio console.log(`🎙️ Generating audio with ${provider} using voice: ${voice}`); const audioBuffer = await audioGenerator.generateAudio(text); // Save audio file const savedPath = await audioGenerator.saveAudioToFile(pathValidation.resolvedPath, audioBuffer); // Get file stats const fileStats = await fs.stat(savedPath); const fileSize = fileStats.size; // Estimate duration (rough estimate: ~150 words per minute, ~5 characters per word) const estimatedWords = text.length / 5; const estimatedDurationSeconds = (estimatedWords / 150) * 60; return { success: true, data: { output_file_path: path.relative(this.baseDir, savedPath), absolute_path: savedPath, file_size: fileSize, duration_estimate: `${estimatedDurationSeconds.toFixed(1)}s`, provider, voice, text_length: text.length, model: model || audioGenerator.getAvailableVoices()[0] // Fallback info }, message: `Audio generated successfully. Saved to: ${path.relative(this.baseDir, savedPath)} (${this.formatFileSize(fileSize)}, ~${estimatedDurationSeconds.toFixed(1)}s)` }; } catch (error) { return { success: false, error: `Failed to generate audio: ${error.message}` }; } } /** * Get configured LLM provider for audio generation */ async getLLMProviderForAudio(provider) { try { // Map audio provider names to LLM provider names const llmProviderName = this.mapAudioProviderToLLM(provider); // Get the configured LLM provider const llmProvider = LLMFactory.getProvider(llmProviderName); // Check if it's configured if (!(await llmProvider.isConfigured())) { return null; } // Get the configuration const config = llmProvider.getConfig(); // Return the API key return { apiKey: config.apiKey }; } catch (error) { console.error(`Failed to get LLM provider for audio: ${error}`); return null; } } /** * Map audio provider names to LLM provider names */ mapAudioProviderToLLM(audioProvider) { switch (audioProvider.toLowerCase()) { case 'gemini': case 'google': return 'Google'; case 'openai': return 'OpenAI'; default: // Try to use the provider name as-is (capitalize first letter) return audioProvider.charAt(0).toUpperCase() + audioProvider.slice(1).toLowerCase(); } } /** * Validate and resolve output path with security checks */ async validateAndResolveOutputPath(outputPath) { try { // Normalize the path to prevent directory traversal const normalizedPath = path.normalize(outputPath); // Check for directory traversal attempts if (normalizedPath.includes('..') || path.isAbsolute(normalizedPath)) { return { success: false, error: 'Output path must be relative and cannot contain ".." or be absolute for security reasons' }; } // Resolve full path const fullPath = path.join(this.baseDir, normalizedPath); // Ensure the resolved path is still within the base directory const relativePath = path.relative(this.baseDir, fullPath); if (relativePath.startsWith('..')) { return { success: false, error: 'Output path resolves outside of project directory' }; } // Create parent directories if they don't exist const parentDir = path.dirname(fullPath); await fs.mkdir(parentDir, { recursive: true }); return { success: true, resolvedPath: fullPath }; } catch (error) { return { success: false, error: `Failed to resolve output path: ${error.message}` }; } } /** * Format file size in human-readable format */ formatFileSize(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } }