This commit is contained in:
subhajit 2026-08-24 17:35:05 +05:30
parent 7e44093e8d
commit 80b772bc8b
3 changed files with 195 additions and 13 deletions

View File

@ -282,8 +282,8 @@ public function joinGuest(Request $request, $code)
*/ */
public function detectEmotion(Request $request, $code) public function detectEmotion(Request $request, $code)
{ {
$speechText = $request->input('speech_text', ''); $speechText = (string)($request->input('speech_text') ?? '');
$imageFrame = $request->input('image_frame', null); $imageFrame = $request->input('image_frame');
$result = $this->aiService->detectEmotion($speechText, $imageFrame); $result = $this->aiService->detectEmotion($speechText, $imageFrame);

View File

@ -21,8 +21,13 @@ class AiClientCallService
* @param string $clientName * @param string $clientName
* @return array * @return array
*/ */
public function generateMoM(string $transcript, string $liveNotes = '', string $projectName = '', string $clientName = ''): array public function generateMoM(?string $transcript = '', ?string $liveNotes = '', ?string $projectName = '', ?string $clientName = ''): array
{ {
$transcript = (string)($transcript ?? '');
$liveNotes = (string)($liveNotes ?? '');
$projectName = (string)($projectName ?? '');
$clientName = (string)($clientName ?? '');
$apiKey = env('GEMINI_API_KEY') ?: (env('GOOGLE_API_KEY') ?: (getenv('GEMINI_API_KEY') ?: getenv('GOOGLE_API_KEY'))); $apiKey = env('GEMINI_API_KEY') ?: (env('GOOGLE_API_KEY') ?: (getenv('GEMINI_API_KEY') ?: getenv('GOOGLE_API_KEY')));
$combinedContent = "Project: {$projectName}\nClient: {$clientName}\n\n"; $combinedContent = "Project: {$projectName}\nClient: {$clientName}\n\n";
@ -105,11 +110,11 @@ public function generateMoM(string $transcript, string $liveNotes = '', string $
* @param string|null $imageFrameBase64 * @param string|null $imageFrameBase64
* @return array * @return array
*/ */
public function detectEmotion(string $speechText = '', ?string $imageFrameBase64 = null): array public function detectEmotion(?string $speechText = '', ?string $imageFrameBase64 = null): array
{ {
$apiKey = env('GEMINI_API_KEY') ?: (env('GOOGLE_API_KEY') ?: (getenv('GEMINI_API_KEY') ?: getenv('GOOGLE_API_KEY'))); $apiKey = env('GEMINI_API_KEY') ?: (env('GOOGLE_API_KEY') ?: (getenv('GEMINI_API_KEY') ?: getenv('GOOGLE_API_KEY')));
$lowerText = strtolower(trim($speechText)); $lowerText = strtolower(trim($speechText ?? ''));
// 1. Check for negative polarity / negations that invert positive sentiment // 1. Check for negative polarity / negations that invert positive sentiment
$negatedHappyPhrases = [ $negatedHappyPhrases = [
@ -121,7 +126,7 @@ public function detectEmotion(string $speechText = '', ?string $imageFrameBase64
$foundNegations = []; $foundNegations = [];
foreach ($negatedHappyPhrases as $negPhrase) { foreach ($negatedHappyPhrases as $negPhrase) {
if (str_contains($lowerText, $negPhrase)) { if (preg_match('/\b' . preg_quote($negPhrase, '/') . '\b/i', $lowerText)) {
$foundNegations[] = $negPhrase; $foundNegations[] = $negPhrase;
} }
} }
@ -154,7 +159,7 @@ public function detectEmotion(string $speechText = '', ?string $imageFrameBase64
$detectedAngryWords = []; $detectedAngryWords = [];
foreach ($angryTriggers as $word) { foreach ($angryTriggers as $word) {
if (preg_match('/\b' . preg_quote($word, '/') . '\b/i', $lowerText) || str_contains($lowerText, $word)) { if (preg_match('/\b' . preg_quote($word, '/') . '\b/i', $lowerText)) {
$detectedAngryWords[] = $word; $detectedAngryWords[] = $word;
} }
} }
@ -164,7 +169,7 @@ public function detectEmotion(string $speechText = '', ?string $imageFrameBase64
$detectedSadWords = []; $detectedSadWords = [];
foreach ($sadTriggers as $word) { foreach ($sadTriggers as $word) {
if (preg_match('/\b' . preg_quote($word, '/') . '\b/i', $lowerText) || str_contains($lowerText, $word)) { if (preg_match('/\b' . preg_quote($word, '/') . '\b/i', $lowerText)) {
$detectedSadWords[] = $word; $detectedSadWords[] = $word;
} }
} }
@ -173,7 +178,7 @@ public function detectEmotion(string $speechText = '', ?string $imageFrameBase64
// Only consider happy words if there are no strong negations of happiness // Only consider happy words if there are no strong negations of happiness
if (empty($foundNegations)) { if (empty($foundNegations)) {
foreach ($happyTriggers as $word) { foreach ($happyTriggers as $word) {
if (preg_match('/\b' . preg_quote($word, '/') . '\b/i', $lowerText) || str_contains($lowerText, $word)) { if (preg_match('/\b' . preg_quote($word, '/') . '\b/i', $lowerText)) {
$detectedHappyWords[] = $word; $detectedHappyWords[] = $word;
} }
} }
@ -551,21 +556,21 @@ protected function generateFallbackMoM(string $combinedContent, string $transcri
$lower = strtolower($combinedContent); $lower = strtolower($combinedContent);
$negatedHappyPhrases = ['not happy', 'not satisfied', 'not good', 'not great', 'not working', 'not impressed', 'not pleased', 'no progress', 'displeased', 'unhappy', 'dissatisfied', 'cannot accept', 'unacceptable']; $negatedHappyPhrases = ['not happy', 'not satisfied', 'not good', 'not great', 'not working', 'not impressed', 'not pleased', 'no progress', 'displeased', 'unhappy', 'dissatisfied', 'cannot accept', 'unacceptable'];
$foundNegations = array_values(array_filter($negatedHappyPhrases, fn($w) => str_contains($lower, $w))); $foundNegations = array_values(array_filter($negatedHappyPhrases, fn($w) => (bool)preg_match('/\b' . preg_quote($w, '/') . '\b/i', $lower)));
$angryWords = ['angry', 'furious', 'unacceptable', 'ridiculous', 'terrible', 'waste of time', 'delay', 'delays', 'delayed', 'broken', 'disaster', 'unhappy', 'frustrated', 'mad', 'annoyed', 'horrible', 'refund', 'cancel', 'fail', 'incompetent', 'unprofessional', 'missed deadline']; $angryWords = ['angry', 'furious', 'unacceptable', 'ridiculous', 'terrible', 'waste of time', 'delay', 'delays', 'delayed', 'broken', 'disaster', 'unhappy', 'frustrated', 'mad', 'annoyed', 'horrible', 'refund', 'cancel', 'fail', 'incompetent', 'unprofessional', 'missed deadline'];
$sadWords = ['sad', 'disappointed', 'worried', 'concerned', 'nervous', 'scared', 'afraid', 'regret', 'doubt', 'hesitant', 'uncertain', 'at risk']; $sadWords = ['sad', 'disappointed', 'worried', 'concerned', 'nervous', 'scared', 'afraid', 'regret', 'doubt', 'hesitant', 'uncertain', 'at risk'];
$happyWords = ['great', 'excellent', 'fantastic', 'awesome', 'good job', 'love it', 'happy', 'pleased', 'perfect', 'superb', 'wonderful', 'thank you', 'appreciate', 'impressed', 'kudos', 'brilliant', 'delighted']; $happyWords = ['great', 'excellent', 'fantastic', 'awesome', 'good job', 'love it', 'happy', 'pleased', 'perfect', 'superb', 'wonderful', 'thank you', 'appreciate', 'impressed', 'kudos', 'brilliant', 'delighted'];
$foundAngry = array_values(array_unique(array_merge( $foundAngry = array_values(array_unique(array_merge(
array_filter($angryWords, fn($w) => str_contains($lower, $w)), array_filter($angryWords, fn($w) => (bool)preg_match('/\b' . preg_quote($w, '/') . '\b/i', $lower)),
$foundNegations $foundNegations
))); )));
$foundSad = array_values(array_filter($sadWords, fn($w) => str_contains($lower, $w))); $foundSad = array_values(array_filter($sadWords, fn($w) => (bool)preg_match('/\b' . preg_quote($w, '/') . '\b/i', $lower)));
$foundHappy = []; $foundHappy = [];
if (empty($foundNegations)) { if (empty($foundNegations)) {
$foundHappy = array_values(array_filter($happyWords, fn($w) => str_contains($lower, $w))); $foundHappy = array_values(array_filter($happyWords, fn($w) => (bool)preg_match('/\b' . preg_quote($w, '/') . '\b/i', $lower)));
} }
$sentiment = 'neutral'; $sentiment = 'neutral';

View File

@ -782,4 +782,181 @@ public function test_client_hub_and_logs_pages_render_recording_actions_when_pre
->assertSee('Play Video') ->assertSee('Play Video')
->assertSee('Download Video Recording', false); ->assertSee('Download Video Recording', false);
} }
public function test_ai_emotion_detection_all_scenarios(): void
{
$client = Client::create([
'client_name' => 'Emotion Test Client',
'client_email' => 'emotion@test.com',
'project_name' => 'Emotion AI Testing',
'responsible_user_id' => $this->pm->id,
'created_by' => $this->admin->id,
'status' => 'active',
]);
$meeting = ClientMeeting::create([
'client_id' => $client->id,
'title' => 'Emotion Scenarios Test',
'meeting_code' => 'meet-emotion-scenarios',
'host_user_id' => $this->pm->id,
'status' => 'in_progress',
]);
// Scenario 1: Frustrated speech -> Angry
$resAngry = $this->postJson(route('client-calls.detect-emotion', $meeting->meeting_code), [
'speech_text' => 'This continuous delay is unacceptable and we missed deadline.',
]);
$resAngry->assertStatus(200)->assertJson(['emotion' => 'angry']);
// Scenario 2: Negated happy phrase -> Angry
$resNegated = $this->postJson(route('client-calls.detect-emotion', $meeting->meeting_code), [
'speech_text' => 'We are not happy and not satisfied with the progress.',
]);
$resNegated->assertStatus(200)->assertJson(['emotion' => 'angry']);
// Scenario 3: Concern / hesitation -> Sad
$resSad = $this->postJson(route('client-calls.detect-emotion', $meeting->meeting_code), [
'speech_text' => 'We are worried and concerned about the upcoming launch date.',
]);
$resSad->assertStatus(200)->assertJson(['emotion' => 'sad']);
// Scenario 4: Praise and delight -> Happy
$resHappy = $this->postJson(route('client-calls.detect-emotion', $meeting->meeting_code), [
'speech_text' => 'The sprint demo looks fantastic and we love the new dashboard.',
]);
$resHappy->assertStatus(200)->assertJson(['emotion' => 'happy']);
// Scenario 5: Dialogue with words like "made" and "message" (formerly false positive triggers) -> Neutral
$resNeutral = $this->postJson(route('client-calls.detect-emotion', $meeting->meeting_code), [
'speech_text' => 'We made a roadmap plan and I will send you a message shortly.',
]);
$resNeutral->assertStatus(200)->assertJson(['emotion' => 'neutral']);
// Scenario 6: Empty speech -> Neutral
$resEmpty = $this->postJson(route('client-calls.detect-emotion', $meeting->meeting_code), [
'speech_text' => '',
]);
$resEmpty->assertStatus(200)->assertJson(['emotion' => 'neutral']);
}
public function test_ai_service_mom_generation_and_fallback_engine(): void
{
$aiService = app(\App\Services\AiClientCallService::class);
// Test happy conversation MoM
$happyMoM = $aiService->generateMoM(
'Client: The release looks great, wonderful job team!',
'Reviewed sprint deliverables demo.',
'Payment Gateway',
'Fintech Corp'
);
$this->assertEquals('happy', $happyMoM['detected_sentiment']);
$this->assertNotEmpty($happyMoM['summary']);
$this->assertNotEmpty($happyMoM['todo_items']);
$this->assertNotEmpty($happyMoM['emotion_details']);
// Test angry conversation MoM
$angryMoM = $aiService->generateMoM(
'Client: This broken feature and missed deadline is unacceptable!',
'Client escalated timeline delay.',
'Payment Gateway',
'Fintech Corp'
);
$this->assertEquals('angry', $angryMoM['detected_sentiment']);
$this->assertTrue($angryMoM['angry_analysis']['is_angry']);
$this->assertNotEmpty($angryMoM['angry_analysis']['trigger_words']);
// Test sad conversation MoM
$sadMoM = $aiService->generateMoM(
'Client: We are disappointed and worried about budget allocations.',
'Discussion on risk factors.',
'Payment Gateway',
'Fintech Corp'
);
$this->assertEquals('sad', $sadMoM['detected_sentiment']);
// Test neutral conversation MoM
$neutralMoM = $aiService->generateMoM(
'Client: We made notes and will review the API contracts next week.',
'Standard sync.',
'Payment Gateway',
'Fintech Corp'
);
$this->assertEquals('neutral', $neutralMoM['detected_sentiment']);
}
public function test_resend_mom_email_endpoint(): void
{
Mail::fake();
$client = Client::create([
'client_name' => 'Email Test Client',
'client_email' => 'emailclient@test.com',
'project_name' => 'Email Dispatches',
'responsible_user_id' => $this->pm->id,
'created_by' => $this->admin->id,
'status' => 'active',
]);
$meeting = ClientMeeting::create([
'client_id' => $client->id,
'title' => 'MoM Dispatch Sync',
'meeting_code' => 'meet-dispatch-789',
'host_user_id' => $this->pm->id,
'status' => 'completed',
]);
$note = ClientCallNote::create([
'client_meeting_id' => $meeting->id,
'client_id' => $client->id,
'created_by' => $this->pm->id,
'summary' => 'MoM dispatch testing',
'mom_content' => 'Full MoM details here.',
'detected_sentiment' => 'happy',
]);
$response = $this->actingAs($this->pm)
->postJson(route('client-calls.resend-email', $note->id));
$response->assertStatus(200)
->assertJson([
'success' => true,
]);
Mail::assertSent(ClientCallMomMail::class);
}
public function test_signaling_heartbeat_and_chat_messages(): void
{
$client = Client::create([
'client_name' => 'Signal Client',
'client_email' => 'signal@test.com',
'project_name' => 'Signal Project',
'responsible_user_id' => $this->pm->id,
'created_by' => $this->admin->id,
'status' => 'active',
]);
$meeting = ClientMeeting::create([
'client_id' => $client->id,
'title' => 'WebRTC Signal Test',
'meeting_code' => 'meet-signal-test-321',
'host_user_id' => $this->pm->id,
'status' => 'in_progress',
]);
$response = $this->postJson(route('client-calls.heartbeat', $meeting->meeting_code), [
'peer_id' => 'peer_test_1',
'peer_name' => 'Alice Tester',
'chat_message' => ['text' => 'Hello everyone!'],
]);
$response->assertStatus(200)
->assertJsonStructure(['active_peers', 'signals', 'chat_messages']);
$meeting->refresh();
$this->assertNotEmpty($meeting->active_peers);
$this->assertNotEmpty($meeting->chat_messages);
$this->assertEquals('Hello everyone!', $meeting->chat_messages[0]['text']);
}
} }