Deploy: 2026-05-23 01:28:41
This commit is contained in:
@@ -67,6 +67,7 @@ class OTPController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check SaaS subscription quotas
|
// 2. Check SaaS subscription quotas
|
||||||
|
$useElevenLabs = false;
|
||||||
if ($companyId !== 1) {
|
if ($companyId !== 1) {
|
||||||
$activeSub = CompanySubscription::findActiveByCompany($companyId);
|
$activeSub = CompanySubscription::findActiveByCompany($companyId);
|
||||||
if (!$activeSub) {
|
if (!$activeSub) {
|
||||||
@@ -80,18 +81,18 @@ class OTPController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($type === 'voice') {
|
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);
|
$features = json_decode($activeSub['features'] ?: '{}', true);
|
||||||
if (isset($features['voice']) && !$features['voice']) {
|
$voiceFeatureEnabled = isset($features['voice']) ? (bool)$features['voice'] : false;
|
||||||
$response->status(403)->json(['error' => 'Voice OTP is not supported in your current subscription plan.']);
|
$hasVoiceLimit = CompanySubscriptionUsage::hasRemainingLimit($companyId, 'voice');
|
||||||
return;
|
|
||||||
|
if ($voiceFeatureEnabled && $hasVoiceLimit) {
|
||||||
|
$useElevenLabs = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if ($type === 'voice') {
|
||||||
|
$useElevenLabs = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Generate verification code
|
// 3. Generate verification code
|
||||||
@@ -99,19 +100,48 @@ class OTPController extends BaseController
|
|||||||
|
|
||||||
// 4. Send Message
|
// 4. Send Message
|
||||||
try {
|
try {
|
||||||
|
$usedElevenLabs = false;
|
||||||
if ($type === 'voice') {
|
if ($type === 'voice') {
|
||||||
// Spacing the digits to force slow Arabic pronunciation: e.g. "1 2 3 4"
|
// Spacing the digits to force slow Arabic pronunciation: e.g. "1 2 3 4"
|
||||||
$spacedCode = implode(' ', str_split($code));
|
$spacedCode = implode(' ', str_split($code));
|
||||||
$textToRead = "رمز التحقق الخاص بك هو: {$spacedCode}. أكرر، رمز التحقق هو: {$spacedCode}.";
|
$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) {
|
if (!$audioBase64) {
|
||||||
$response->status(500)->json(['error' => 'Failed to generate voice OTP audio.']);
|
$response->status(500)->json(['error' => 'Failed to generate voice OTP audio.']);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send voice note
|
// Send voice note
|
||||||
ConversationFlowEngine::sendReply($session, $phone, '', null, $audioBase64, 'audio/mp3');
|
ConversationFlowEngine::sendReply($session, $phone, '', null, $audioBase64, $mimeType);
|
||||||
} else {
|
} else {
|
||||||
// Send text
|
// Send text
|
||||||
$textMsg = "رمز التحقق الخاص بك لمتجر نابه هو: *{$code}* \n الرجاء عدم مشاركته مع أي شخص.";
|
$textMsg = "رمز التحقق الخاص بك لمتجر نابه هو: *{$code}* \n الرجاء عدم مشاركته مع أي شخص.";
|
||||||
@@ -121,7 +151,7 @@ class OTPController extends BaseController
|
|||||||
// Increment usage stats
|
// Increment usage stats
|
||||||
if ($companyId !== 1) {
|
if ($companyId !== 1) {
|
||||||
CompanySubscriptionUsage::incrementUsage($companyId, 'request');
|
CompanySubscriptionUsage::incrementUsage($companyId, 'request');
|
||||||
if ($type === 'voice') {
|
if ($type === 'voice' && $usedElevenLabs) {
|
||||||
CompanySubscriptionUsage::incrementUsage($companyId, 'voice');
|
CompanySubscriptionUsage::incrementUsage($companyId, 'voice');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -471,8 +471,10 @@ class WhatsAppController extends BaseController
|
|||||||
$replyText = null;
|
$replyText = null;
|
||||||
$replyAudio = null;
|
$replyAudio = null;
|
||||||
$replyAudioMimeType = null;
|
$replyAudioMimeType = null;
|
||||||
|
$usedElevenLabs = false;
|
||||||
|
|
||||||
$companyId = $session['company_id'];
|
$companyId = $session['company_id'];
|
||||||
|
$useElevenLabs = true;
|
||||||
|
|
||||||
// Limit enforcement for non-admin companies (company 1 is admin/demo)
|
// Limit enforcement for non-admin companies (company 1 is admin/demo)
|
||||||
if ($companyId !== 1) {
|
if ($companyId !== 1) {
|
||||||
@@ -488,10 +490,15 @@ class WhatsAppController extends BaseController
|
|||||||
return;
|
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')) {
|
if ($hasAudio && !\App\Models\CompanySubscriptionUsage::hasRemainingLimit($companyId, 'voice')) {
|
||||||
error_log("[Chatbot Warning] Company {$companyId} has exceeded its voice request limit.");
|
$useElevenLabs = false;
|
||||||
$replyText = "⚠️ عذراً، لقد استهلك هذا المتجر الحد المسموح له من الرسائل الصوتية لهذا الشهر. يرجى إرسال استفسارك نصياً لكي نتمكن من مساعدتك.";
|
}
|
||||||
|
|
||||||
|
// 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
|
// Check OCR limit if input is image
|
||||||
@@ -570,10 +577,10 @@ class WhatsAppController extends BaseController
|
|||||||
$mimeType = trim(explode(';', $mimeType)[0]);
|
$mimeType = trim(explode(';', $mimeType)[0]);
|
||||||
}
|
}
|
||||||
$configuredElKey = !empty($rule['elevenlabs_api_key']) ? $rule['elevenlabs_api_key'] : null;
|
$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;
|
$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
|
// Try generating native audio response first
|
||||||
$audioResponse = \App\Services\GeminiService::generateAudioResponseFromAudio(
|
$audioResponse = \App\Services\GeminiService::generateAudioResponseFromAudio(
|
||||||
@@ -589,6 +596,9 @@ class WhatsAppController extends BaseController
|
|||||||
$replyAudio = $audioResponse['audio'];
|
$replyAudio = $audioResponse['audio'];
|
||||||
$replyAudioMimeType = $audioResponse['mimeType'] ?? 'audio/mp4';
|
$replyAudioMimeType = $audioResponse['mimeType'] ?? 'audio/mp4';
|
||||||
$replyText = '[صوت من الذكاء الاصطناعي]';
|
$replyText = '[صوت من الذكاء الاصطناعي]';
|
||||||
|
if (!empty($elApiKey) && $replyAudioMimeType !== 'audio/mp3') {
|
||||||
|
$usedElevenLabs = true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback to text output from audio
|
// Fallback to text output from audio
|
||||||
$replyText = \App\Services\GeminiService::generateResponseFromAudio($apiKey, $systemPrompt, $msgData['audio'], $mimeType);
|
$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
|
// Increment SaaS usage stats if successfully sent
|
||||||
if ($status === 'sent' && $companyId !== 1) {
|
if ($status === 'sent' && $companyId !== 1) {
|
||||||
\App\Models\CompanySubscriptionUsage::incrementUsage($companyId, 'request');
|
\App\Models\CompanySubscriptionUsage::incrementUsage($companyId, 'request');
|
||||||
if ($hasAudio || !empty($replyAudio)) {
|
if ($usedElevenLabs) {
|
||||||
\App\Models\CompanySubscriptionUsage::incrementUsage($companyId, 'voice');
|
\App\Models\CompanySubscriptionUsage::incrementUsage($companyId, 'voice');
|
||||||
}
|
}
|
||||||
if ($hasImage) {
|
if ($hasImage) {
|
||||||
|
|||||||
@@ -425,9 +425,20 @@ class GeminiService
|
|||||||
if ($audioData) {
|
if ($audioData) {
|
||||||
return $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:
|
// Gemini Fallback Logic:
|
||||||
$models = [
|
$models = [
|
||||||
'gemini-3.1-flash-tts-preview',
|
'gemini-3.1-flash-tts-preview',
|
||||||
|
|||||||
@@ -36,6 +36,14 @@ $router->get('/admin', function ($request, $response) {
|
|||||||
exit;
|
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
|
// Health Check — no php_version or environment in production to avoid info disclosure
|
||||||
$router->get('/api/health', function ($request, $response) {
|
$router->get('/api/health', function ($request, $response) {
|
||||||
$response->json([
|
$response->json([
|
||||||
|
|||||||
Reference in New Issue
Block a user