diff --git a/backend/app/Controllers/OTPController.php b/backend/app/Controllers/OTPController.php index 1da2e93..623d0c1 100644 --- a/backend/app/Controllers/OTPController.php +++ b/backend/app/Controllers/OTPController.php @@ -67,6 +67,7 @@ class OTPController extends BaseController } // 2. Check SaaS subscription quotas + $useElevenLabs = false; if ($companyId !== 1) { $activeSub = CompanySubscription::findActiveByCompany($companyId); if (!$activeSub) { @@ -80,18 +81,18 @@ class OTPController extends BaseController } if ($type === 'voice') { - if (!CompanySubscriptionUsage::hasRemainingLimit($companyId, 'voice')) { - $response->status(403)->json(['error' => 'Voice request quota exceeded. Please upgrade your plan.']); - return; - } - - // Starter plan doesn't support Voice Notes $features = json_decode($activeSub['features'] ?: '{}', true); - if (isset($features['voice']) && !$features['voice']) { - $response->status(403)->json(['error' => 'Voice OTP is not supported in your current subscription plan.']); - return; + $voiceFeatureEnabled = isset($features['voice']) ? (bool)$features['voice'] : false; + $hasVoiceLimit = CompanySubscriptionUsage::hasRemainingLimit($companyId, 'voice'); + + if ($voiceFeatureEnabled && $hasVoiceLimit) { + $useElevenLabs = true; } } + } else { + if ($type === 'voice') { + $useElevenLabs = true; + } } // 3. Generate verification code @@ -99,19 +100,48 @@ class OTPController extends BaseController // 4. Send Message try { + $usedElevenLabs = false; if ($type === 'voice') { // Spacing the digits to force slow Arabic pronunciation: e.g. "1 2 3 4" $spacedCode = implode(' ', str_split($code)); $textToRead = "رمز التحقق الخاص بك هو: {$spacedCode}. أكرر، رمز التحقق هو: {$spacedCode}."; - $audioBase64 = TTSService::textToSpeechArabic($textToRead); + $audioBase64 = null; + $mimeType = 'audio/mp3'; + + if ($useElevenLabs) { + $rule = \App\Models\ChatbotRule::findActiveForRule($companyId, $session['id']); + $configuredElKey = ($rule && !empty($rule['elevenlabs_api_key'])) ? $rule['elevenlabs_api_key'] : null; + $elApiKey = \App\Services\GeminiService::getElevenLabsApiKey($configuredElKey); + + $configuredVoiceId = ($rule && !empty($rule['elevenlabs_voice_id'])) ? $rule['elevenlabs_voice_id'] : null; + $elVoiceId = \App\Services\GeminiService::getElevenLabsVoiceId($configuredVoiceId); + + if (!empty($elApiKey)) { + $elVoiceId = !empty($elVoiceId) ? $elVoiceId : 'pNInz6obpgDQGcFmaJgB'; // Default to Adam + $audioData = \App\Services\GeminiService::generateAudioResponseWithElevenLabs($elApiKey, $textToRead, $elVoiceId); + if ($audioData) { + $audioBase64 = $audioData['audio']; + $mimeType = $audioData['mimeType']; + $usedElevenLabs = true; + } + } + } + + if (!$audioBase64) { + // Fallback to Google Translate TTS + $audioBase64 = TTSService::textToSpeechArabic($textToRead); + $mimeType = 'audio/mp3'; + $usedElevenLabs = false; + } + if (!$audioBase64) { $response->status(500)->json(['error' => 'Failed to generate voice OTP audio.']); return; } // Send voice note - ConversationFlowEngine::sendReply($session, $phone, '', null, $audioBase64, 'audio/mp3'); + ConversationFlowEngine::sendReply($session, $phone, '', null, $audioBase64, $mimeType); } else { // Send text $textMsg = "رمز التحقق الخاص بك لمتجر نابه هو: *{$code}* \n الرجاء عدم مشاركته مع أي شخص."; @@ -121,7 +151,7 @@ class OTPController extends BaseController // Increment usage stats if ($companyId !== 1) { CompanySubscriptionUsage::incrementUsage($companyId, 'request'); - if ($type === 'voice') { + if ($type === 'voice' && $usedElevenLabs) { CompanySubscriptionUsage::incrementUsage($companyId, 'voice'); } } diff --git a/backend/app/Controllers/WhatsAppController.php b/backend/app/Controllers/WhatsAppController.php index 2cdfece..d5e2fe6 100644 --- a/backend/app/Controllers/WhatsAppController.php +++ b/backend/app/Controllers/WhatsAppController.php @@ -471,8 +471,10 @@ class WhatsAppController extends BaseController $replyText = null; $replyAudio = null; $replyAudioMimeType = null; + $usedElevenLabs = false; $companyId = $session['company_id']; + $useElevenLabs = true; // Limit enforcement for non-admin companies (company 1 is admin/demo) if ($companyId !== 1) { @@ -488,10 +490,15 @@ class WhatsAppController extends BaseController return; } - // Check voice limit if input is audio + // Check voice limit if input is audio to determine if we can use premium ElevenLabs if ($hasAudio && !\App\Models\CompanySubscriptionUsage::hasRemainingLimit($companyId, 'voice')) { - error_log("[Chatbot Warning] Company {$companyId} has exceeded its voice request limit."); - $replyText = "⚠️ عذراً، لقد استهلك هذا المتجر الحد المسموح له من الرسائل الصوتية لهذا الشهر. يرجى إرسال استفسارك نصياً لكي نتمكن من مساعدتك."; + $useElevenLabs = false; + } + + // Check if voice feature is enabled in subscription + $features = json_decode($activeSub['features'] ?: '{}', true); + if (isset($features['voice']) && !$features['voice']) { + $useElevenLabs = false; } // Check OCR limit if input is image @@ -570,10 +577,10 @@ class WhatsAppController extends BaseController $mimeType = trim(explode(';', $mimeType)[0]); } $configuredElKey = !empty($rule['elevenlabs_api_key']) ? $rule['elevenlabs_api_key'] : null; - $elApiKey = \App\Services\GeminiService::getElevenLabsApiKey($configuredElKey); + $elApiKey = $useElevenLabs ? \App\Services\GeminiService::getElevenLabsApiKey($configuredElKey) : null; $configuredVoiceId = !empty($rule['elevenlabs_voice_id']) ? $rule['elevenlabs_voice_id'] : null; - $elVoiceId = \App\Services\GeminiService::getElevenLabsVoiceId($configuredVoiceId); + $elVoiceId = $useElevenLabs ? \App\Services\GeminiService::getElevenLabsVoiceId($configuredVoiceId) : null; // Try generating native audio response first $audioResponse = \App\Services\GeminiService::generateAudioResponseFromAudio( @@ -589,6 +596,9 @@ class WhatsAppController extends BaseController $replyAudio = $audioResponse['audio']; $replyAudioMimeType = $audioResponse['mimeType'] ?? 'audio/mp4'; $replyText = '[صوت من الذكاء الاصطناعي]'; + if (!empty($elApiKey) && $replyAudioMimeType !== 'audio/mp3') { + $usedElevenLabs = true; + } } else { // Fallback to text output from audio $replyText = \App\Services\GeminiService::generateResponseFromAudio($apiKey, $systemPrompt, $msgData['audio'], $mimeType); @@ -677,7 +687,7 @@ class WhatsAppController extends BaseController // Increment SaaS usage stats if successfully sent if ($status === 'sent' && $companyId !== 1) { \App\Models\CompanySubscriptionUsage::incrementUsage($companyId, 'request'); - if ($hasAudio || !empty($replyAudio)) { + if ($usedElevenLabs) { \App\Models\CompanySubscriptionUsage::incrementUsage($companyId, 'voice'); } if ($hasImage) { diff --git a/backend/app/Services/GeminiService.php b/backend/app/Services/GeminiService.php index 7a2e1f7..46ecc5f 100644 --- a/backend/app/Services/GeminiService.php +++ b/backend/app/Services/GeminiService.php @@ -425,9 +425,20 @@ class GeminiService if ($audioData) { return $audioData; } - error_log("[TTS Service] ElevenLabs failed, falling back to Gemini TTS."); + error_log("[TTS Service] ElevenLabs failed, falling back to Google Translate TTS."); } + // Fallback to Google Translate TTS (free and reliable) + $googleTtsAudio = \App\Services\TTSService::textToSpeechArabic($userMessage); + if ($googleTtsAudio) { + return [ + 'audio' => $googleTtsAudio, + 'mimeType' => 'audio/mp3' + ]; + } + + error_log("[TTS Service] Google Translate TTS failed, falling back to Gemini TTS models."); + // Gemini Fallback Logic: $models = [ 'gemini-3.1-flash-tts-preview', diff --git a/backend/public/index.php b/backend/public/index.php index 09f1eaa..7c6eec8 100644 --- a/backend/public/index.php +++ b/backend/public/index.php @@ -36,6 +36,14 @@ $router->get('/admin', function ($request, $response) { exit; }); +// Serve plan.html roadmap on /roadmap path +$router->get('/roadmap', function ($request, $response) { + $response->setHeader('Content-Type', 'text/html; charset=utf-8'); + $response->sendHeaders(); + readfile(__DIR__ . '/plan.html'); + exit; +}); + // Health Check — no php_version or environment in production to avoid info disclosure $router->get('/api/health', function ($request, $response) { $response->json([