diff --git a/backend/.env.example b/backend/.env.example index 1cca1e1..fb586db 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -13,6 +13,8 @@ DB_PASSWORD= # AI Model Configuration GEMINI_API_KEY= +ELEVENLABS_API_KEY= +ELEVENLABS_VOICE_ID= # Messaging Gateway Settings WHATSAPP_GATEWAY_URL=http://localhost:3722 diff --git a/backend/app/Models/ChatbotRule.php b/backend/app/Models/ChatbotRule.php index feae6f0..1785c1a 100644 --- a/backend/app/Models/ChatbotRule.php +++ b/backend/app/Models/ChatbotRule.php @@ -29,6 +29,9 @@ class ChatbotRule extends BaseModel if (!empty($rule['gemini_api_key'])) { $rule['gemini_api_key'] = Security::decrypt($rule['gemini_api_key']); } + if (!empty($rule['elevenlabs_api_key'])) { + $rule['elevenlabs_api_key'] = Security::decrypt($rule['elevenlabs_api_key']); + } } return $rules; @@ -53,8 +56,13 @@ class ChatbotRule extends BaseModel ); } - if ($rule && !empty($rule['gemini_api_key'])) { - $rule['gemini_api_key'] = Security::decrypt($rule['gemini_api_key']); + if ($rule) { + if (!empty($rule['gemini_api_key'])) { + $rule['gemini_api_key'] = Security::decrypt($rule['gemini_api_key']); + } + if (!empty($rule['elevenlabs_api_key'])) { + $rule['elevenlabs_api_key'] = Security::decrypt($rule['elevenlabs_api_key']); + } } return $rule; @@ -71,6 +79,10 @@ class ChatbotRule extends BaseModel $data['gemini_api_key'] = Security::encrypt($data['gemini_api_key']); } + if (!empty($data['elevenlabs_api_key'])) { + $data['elevenlabs_api_key'] = Security::encrypt($data['elevenlabs_api_key']); + } + if (isset($data['id'])) { $id = $data['id']; unset($data['id']); @@ -89,14 +101,26 @@ class ChatbotRule extends BaseModel static $checked = false; if ($checked) return; try { - // Check if column exists + // Check if gemini_api_key exists $columns = Database::select("SHOW COLUMNS FROM " . static::$table . " LIKE 'gemini_api_key'"); if (empty($columns)) { Database::execute("ALTER TABLE " . static::$table . " ADD COLUMN gemini_api_key VARCHAR(512) DEFAULT NULL AFTER ai_prompt"); } + + // Check if elevenlabs_api_key exists + $elColumns = Database::select("SHOW COLUMNS FROM " . static::$table . " LIKE 'elevenlabs_api_key'"); + if (empty($elColumns)) { + Database::execute("ALTER TABLE " . static::$table . " ADD COLUMN elevenlabs_api_key VARCHAR(512) DEFAULT NULL AFTER gemini_api_key"); + } + + // Check if elevenlabs_voice_id exists + $voiceColumns = Database::select("SHOW COLUMNS FROM " . static::$table . " LIKE 'elevenlabs_voice_id'"); + if (empty($voiceColumns)) { + Database::execute("ALTER TABLE " . static::$table . " ADD COLUMN elevenlabs_voice_id VARCHAR(100) DEFAULT NULL AFTER elevenlabs_api_key"); + } $checked = true; } catch (\Exception $e) { - error_log("Failed to ensure chatbot_rules column: " . $e->getMessage()); + error_log("Failed to ensure chatbot_rules columns: " . $e->getMessage()); } } } diff --git a/backend/app/Services/GeminiService.php b/backend/app/Services/GeminiService.php index 9fb6f8b..fad4280 100644 --- a/backend/app/Services/GeminiService.php +++ b/backend/app/Services/GeminiService.php @@ -214,10 +214,68 @@ class GeminiService } /** - * Call Gemini API to generate a native audio (speech) response from text + * Call ElevenLabs API to generate a native audio response from text */ - public static function generateAudioResponse(string $apiKey, string $systemPrompt, string $userMessage, string $voiceName = 'Puck'): ?array + public static function generateAudioResponseWithElevenLabs(string $elApiKey, string $text, string $voiceId): ?array { + $url = 'https://api.elevenlabs.io/v1/text-to-speech/' . $voiceId; + + $payload = json_encode([ + 'text' => $text, + 'model_id' => 'eleven_multilingual_v2', + 'voice_settings' => [ + 'stability' => 0.5, + 'similarity_boost' => 0.8 + ] + ]); + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'xi-api-key: ' . $elApiKey + ]); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode === 200) { + return [ + 'audio' => base64_encode($response), + 'mimeType' => 'audio/mpeg' + ]; + } else { + error_log("[ElevenLabs API Error] HTTP " . $httpCode . " | Response: " . $response); + return null; + } + } + + /** + * Call Gemini API or ElevenLabs to generate a native audio (speech) response from text + */ + public static function generateAudioResponse( + string $apiKey, + string $systemPrompt, + string $userMessage, + string $voiceName = 'Puck', + ?string $elApiKey = null, + ?string $elVoiceId = null + ): ?array { + // Use ElevenLabs if the API Key is provided + if (!empty($elApiKey)) { + $voiceId = !empty($elVoiceId) ? $elVoiceId : 'pNInz6obpgDQGcFmaJgB'; // Default to Adam + $audioData = self::generateAudioResponseWithElevenLabs($elApiKey, $userMessage, $voiceId); + if ($audioData) { + return $audioData; + } + error_log("[TTS Service] ElevenLabs failed, falling back to Gemini TTS."); + } + + // Gemini Fallback Logic: $models = [ 'gemini-3.1-flash-tts-preview', 'gemini-2.5-flash-preview-tts' @@ -284,8 +342,15 @@ class GeminiService /** * Call Gemini API with audio inline data to generate a native audio response */ - public static function generateAudioResponseFromAudio(string $apiKey, string $systemPrompt, string $audioBase64, string $mimeType, string $voiceName = 'Puck'): ?array - { + public static function generateAudioResponseFromAudio( + string $apiKey, + string $systemPrompt, + string $audioBase64, + string $mimeType, + string $voiceName = 'Puck', + ?string $elApiKey = null, + ?string $elVoiceId = null + ): ?array { // Step 1: Use gemini-flash-lite-latest (which supports audio input) to understand the audio message and generate a text reply $replyText = self::generateResponseFromAudio($apiKey, $systemPrompt, $audioBase64, $mimeType); if (empty($replyText)) { @@ -293,7 +358,7 @@ class GeminiService return null; } - // Step 2: Use gemini-3.1-flash-tts-preview to convert the text response into a native audio voice note - return self::generateAudioResponse($apiKey, $systemPrompt, $replyText, $voiceName); + // Step 2: Use ElevenLabs or Gemini TTS to convert the text response into a native audio voice note + return self::generateAudioResponse($apiKey, $systemPrompt, $replyText, $voiceName, $elApiKey, $elVoiceId); } }