new_fixes #18

Merged
subhajit.pal merged 2 commits from new_fixes into main 2026-08-06 15:05:26 +00:00
4 changed files with 51 additions and 15 deletions
Showing only changes of commit 74516d0fcc - Show all commits

View File

@ -279,9 +279,9 @@ public function logViolation(Request $request, $id)
$type = $request->input('type'); // tab_switch, focus_lost, paste_event, gaze_anomaly, question_repetition, external_ai_detected $type = $request->input('type'); // tab_switch, focus_lost, paste_event, gaze_anomaly, question_repetition, external_ai_detected
$details = $request->input('details', ''); $details = $request->input('details', '');
// Automatically run AI Speech/Transcript Inspection if violation is speech/question repetition // Automatically run AI Speech/Transcript Inspection if violation is speech/question repetition/phone call
$aiVerdict = null; $aiVerdict = null;
if (in_array($type, ['question_repetition', 'question_repeat_lower', 'talking_secondary_person'])) { if (in_array($type, ['question_repetition', 'question_repeat_lower', 'talking_secondary_person', 'talking_on_phone', 'reading_external_device'])) {
$aiDetector = new \App\Services\AiCheatingDetectorService(); $aiDetector = new \App\Services\AiCheatingDetectorService();
$aiVerdict = $aiDetector->analyzeSpeechTranscript($details, $interview->candidate_notes ?? ''); $aiVerdict = $aiDetector->analyzeSpeechTranscript($details, $interview->candidate_notes ?? '');
} }

View File

@ -609,6 +609,8 @@ function getScreenShareVideoElement() {
// REAL CANDIDATE SYSTEM / COMPUTER SCREENSHOT CAPTURE ENGINE // REAL CANDIDATE SYSTEM / COMPUTER SCREENSHOT CAPTURE ENGINE
function captureAndUploadTabScreenshot(reason = 'tab_switch') { function captureAndUploadTabScreenshot(reason = 'tab_switch') {
if (!isTabSwitchScreenshotEnabled) return; if (!isTabSwitchScreenshotEnabled) return;
if (latestCallStatus !== 'active') return; // Screenshot ONLY when call is active
if (reason !== 'tab_switch' && reason !== 'call_start') return; // Screenshot ONLY when candidate switches tab
try { try {
const tempCanvas = document.createElement('canvas'); const tempCanvas = document.createElement('canvas');
@ -897,13 +899,13 @@ function analyzeEyeGazeAndStaring() {
// 2.4s initial alert // 2.4s initial alert
if (gazeDownwardCounter === 6) { if (gazeDownwardCounter === 6) {
logViolation('gaze_anomaly', 'Candidate lowered eye gaze toward screen bottom or desk'); logViolation('gaze_anomaly', 'Candidate lowered eye gaze toward screen bottom, desk, or secondary device');
} }
// 10.0s continuous lower screen stare alert (logs to proctor logs section in reviewer modal - SILENT FOR CANDIDATE) // 10.0s continuous lower screen stare alert
if (gazeDownwardDurationMs >= 10000) { if (gazeDownwardDurationMs >= 10000) {
gazeDownwardDurationMs = 0; // Reset timer for next 10s window if candidate continues looking down gazeDownwardDurationMs = 0;
logViolation('gaze_lower_device', 'Candidate continuously looking at lower portion of screen for 10s (suspected reading from mobile device or cheat sheet)'); logViolation('reading_external_device', 'Candidate continuously looking down at lower screen or desk for 10s (suspected reading from mobile device, secondary screen, or cheat sheet)');
} }
} else { } else {
gazeDownwardCounter = Math.max(0, gazeDownwardCounter - 1); gazeDownwardCounter = Math.max(0, gazeDownwardCounter - 1);
@ -923,7 +925,7 @@ function analyzeEyeGazeAndStaring() {
if (frameDelta >= 0.1 && frameDelta < 3.8) { if (frameDelta >= 0.1 && frameDelta < 3.8) {
gazeFixedStaringCounter++; gazeFixedStaringCounter++;
if (gazeFixedStaringCounter === 25) { // 10.0 seconds of continuous fixed stare if (gazeFixedStaringCounter === 25) { // 10.0 seconds of continuous fixed stare
logViolation('gaze_fixed_staring', 'Candidate maintaining fixed unnatural gaze (possible secondary screen/AI assistant)'); logViolation('reading_external_device', 'Candidate maintaining fixed unnatural off-center gaze for 10s (suspected reading from external device or secondary monitor)');
} }
} else { } else {
gazeFixedStaringCounter = Math.max(0, gazeFixedStaringCounter - 1); gazeFixedStaringCounter = Math.max(0, gazeFixedStaringCounter - 1);
@ -972,11 +974,17 @@ function initQuestionRepeatDetection() {
candidateSpeechHistory.push(transcript); candidateSpeechHistory.push(transcript);
if (candidateSpeechHistory.length > 15) candidateSpeechHistory.shift(); if (candidateSpeechHistory.length > 15) candidateSpeechHistory.shift();
if (isReadingQuestionOnScreen || isRepeatedSpeech) { // Phone call speech detection keywords
const phoneKeywords = ['hello', 'can you hear me', 'on phone', 'calling', 'call you back', 'hold on', 'on the line', 'speaker', 'phone', 'mobile', 'call me', 'speak later', 'hey call', 'on a call'];
const isPhoneTalk = phoneKeywords.some(kw => transcript.includes(kw));
if (isPhoneTalk) {
logViolation('talking_on_phone', `Candidate detected talking on phone / phone call during assessment: "${transcript}"`);
} else if (isReadingQuestionOnScreen || isRepeatedSpeech) {
if (gazeDownwardCounter > 3) { if (gazeDownwardCounter > 3) {
logViolation('question_repeat_lower', `Candidate reading/repeating question ("${transcript}") while looking down at lower portion/mobile device`); logViolation('reading_external_device', `Candidate detected reading from external device or mobile screen: "${transcript}"`);
} else { } else {
logViolation('question_repetition', `Candidate reading/repeating question out loud: "${transcript}" (suspected Parakeet AI speech prompt)`); logViolation('question_repetition', `Candidate detected reading / repeating question out loud: "${transcript}" (suspected AI prompt ingestion)`);
} }
} else { } else {
// Talking to someone else detection when speech is detected during non-call state // Talking to someone else detection when speech is detected during non-call state

View File

@ -1506,12 +1506,13 @@ function pollReviewData() {
let icon = '🔴'; let icon = '🔴';
if (l.type === 'focus_lost') icon = '🟡'; if (l.type === 'focus_lost') icon = '🟡';
if (l.type === 'gaze_anomaly' || l.type === 'gaze_fixed_staring') icon = '👁️'; if (l.type === 'gaze_anomaly' || l.type === 'gaze_fixed_staring') icon = '👁️';
if (l.type === 'gaze_lower_device') icon = '📱'; if (l.type === 'gaze_lower_device' || l.type === 'reading_external_device') icon = '📱';
if (l.type === 'question_repeat_lower' || l.type === 'question_repetition') icon = '🗣️'; if (l.type === 'question_repeat_lower' || l.type === 'question_repetition') icon = '🗣️';
if (l.type === 'talking_on_phone') icon = '📞';
if (l.type === 'external_ai_detected') icon = '🤖'; if (l.type === 'external_ai_detected') icon = '🤖';
if (l.type === 'tab_switch') icon = '📸'; if (l.type === 'tab_switch') icon = '📸';
if (['paste_event', 'tab_switch', 'focus_lost', 'gaze_anomaly', 'gaze_fixed_staring', 'gaze_lower_device', 'question_repeat_lower', 'question_repetition', 'external_ai_detected', 'talking_secondary_person'].includes(l.type)) { if (['paste_event', 'tab_switch', 'focus_lost', 'gaze_anomaly', 'gaze_fixed_staring', 'gaze_lower_device', 'question_repeat_lower', 'question_repetition', 'external_ai_detected', 'talking_secondary_person', 'talking_on_phone', 'reading_external_device'].includes(l.type)) {
currentViolations.push(l); currentViolations.push(l);
} }
@ -1552,9 +1553,13 @@ function pollReviewData() {
sosBtn.style.animation = 'sosPulse 1.2s infinite alternate'; sosBtn.style.animation = 'sosPulse 1.2s infinite alternate';
const hasExternalAi = currentViolations.some(v => v.type === 'external_ai_detected'); const hasExternalAi = currentViolations.some(v => v.type === 'external_ai_detected');
const hasTabSwitch = currentViolations.some(v => v.type === 'tab_switch'); const hasTabSwitch = currentViolations.some(v => v.type === 'tab_switch');
const hasPhoneTalk = currentViolations.some(v => v.type === 'talking_on_phone');
if (hasExternalAi) { if (hasExternalAi) {
sosBtn.innerText = '🚨 candidate might use external ai'; sosBtn.innerText = '🚨 candidate might use external ai';
sosBtn.style.background = 'linear-gradient(135deg, #ef4444 0%, #b91c1c 100%)'; sosBtn.style.background = 'linear-gradient(135deg, #ef4444 0%, #b91c1c 100%)';
} else if (hasPhoneTalk) {
sosBtn.innerText = '📞 Candidate Talking on Phone';
sosBtn.style.background = 'linear-gradient(135deg, #ef4444 0%, #b91c1c 100%)';
} else if (hasTabSwitch) { } else if (hasTabSwitch) {
sosBtn.innerText = '📸 Candidate Switched Browser Tab'; sosBtn.innerText = '📸 Candidate Switched Browser Tab';
sosBtn.style.background = 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)'; sosBtn.style.background = 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)';
@ -1580,6 +1585,14 @@ function openSosModal() {
typeTitle = 'candidate might use external ai'; typeTitle = 'candidate might use external ai';
} else if (v.type === 'tab_switch') { } else if (v.type === 'tab_switch') {
typeTitle = 'Candidate Switched Browser Tab'; typeTitle = 'Candidate Switched Browser Tab';
} else if (v.type === 'talking_on_phone') {
typeTitle = 'Candidate Talking on Phone';
} else if (v.type === 'reading_external_device') {
typeTitle = 'Candidate Reading from External Device';
} else if (v.type === 'question_repetition' || v.type === 'question_repeat_lower') {
typeTitle = 'Candidate Reading / Repeating Question Out Loud';
} else if (v.type === 'gaze_anomaly' || v.type === 'gaze_lower_device') {
typeTitle = 'Candidate Lower Eye Gaze Detected';
} }
html += `<div style="margin-bottom: 10px; padding: 8px; background: rgba(255,255,255,0.04); border-radius: 6px; border-left: 3px solid ${v.type === 'external_ai_detected' ? '#ef4444' : '#f59e0b'};"> html += `<div style="margin-bottom: 10px; padding: 8px; background: rgba(255,255,255,0.04); border-radius: 6px; border-left: 3px solid ${v.type === 'external_ai_detected' ? '#ef4444' : '#f59e0b'};">
<strong style="color: ${v.type === 'external_ai_detected' ? '#fca5a5' : '#fde047'};">🚨 ${typeTitle}:</strong> ${v.details || 'Suspicious candidate activity detected.'} <strong style="color: ${v.type === 'external_ai_detected' ? '#fca5a5' : '#fde047'};">🚨 ${typeTitle}:</strong> ${v.details || 'Suspicious candidate activity detected.'}

View File

@ -393,8 +393,23 @@ public function test_tab_switch_logging_and_external_ai_detection_behavior(): vo
]); ]);
$aiResp->assertStatus(200); $aiResp->assertStatus(200);
$poll2 = $this->get(route('interview.poll', $interview->id)); // 3. Talking on phone violation
$poll2->assertStatus(200) $phoneResp = $this->post(route('interview.violation', $interview->id), [
->assertJsonFragment(['type' => 'external_ai_detected']); 'type' => 'talking_on_phone',
'details' => 'Candidate detected talking on phone / phone call during assessment: "hello hold on calling you back"',
]);
$phoneResp->assertStatus(200);
// 4. Reading from external device violation
$extDevResp = $this->post(route('interview.violation', $interview->id), [
'type' => 'reading_external_device',
'details' => 'Candidate detected reading from external device, mobile screen, or secondary monitor',
]);
$extDevResp->assertStatus(200);
$poll3 = $this->get(route('interview.poll', $interview->id));
$poll3->assertStatus(200)
->assertJsonFragment(['type' => 'talking_on_phone'])
->assertJsonFragment(['type' => 'reading_external_device']);
} }
} }