contaigents
Version:
Modular AI Content Ecosystem with Audio Generation
246 lines (241 loc) • 9.43 kB
JavaScript
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];
}
}