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
$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;
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();
$aiVerdict = $aiDetector->analyzeSpeechTranscript($details, $interview->candidate_notes ?? '');
}

View File

@ -609,6 +609,8 @@ function getScreenShareVideoElement() {
// REAL CANDIDATE SYSTEM / COMPUTER SCREENSHOT CAPTURE ENGINE
function captureAndUploadTabScreenshot(reason = 'tab_switch') {
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 {
const tempCanvas = document.createElement('canvas');
@ -897,13 +899,13 @@ function analyzeEyeGazeAndStaring() {
// 2.4s initial alert
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) {
gazeDownwardDurationMs = 0; // Reset timer for next 10s window if candidate continues looking down
logViolation('gaze_lower_device', 'Candidate continuously looking at lower portion of screen for 10s (suspected reading from mobile device or cheat sheet)');
gazeDownwardDurationMs = 0;
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 {
gazeDownwardCounter = Math.max(0, gazeDownwardCounter - 1);
@ -923,7 +925,7 @@ function analyzeEyeGazeAndStaring() {
if (frameDelta >= 0.1 && frameDelta < 3.8) {
gazeFixedStaringCounter++;
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 {
gazeFixedStaringCounter = Math.max(0, gazeFixedStaringCounter - 1);
@ -972,11 +974,17 @@ function initQuestionRepeatDetection() {
candidateSpeechHistory.push(transcript);
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) {
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 {
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 {
// Talking to someone else detection when speech is detected during non-call state

View File

@ -1506,12 +1506,13 @@ function pollReviewData() {
let icon = '🔴';
if (l.type === 'focus_lost') 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 === 'talking_on_phone') icon = '📞';
if (l.type === 'external_ai_detected') 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);
}
@ -1552,9 +1553,13 @@ function pollReviewData() {
sosBtn.style.animation = 'sosPulse 1.2s infinite alternate';
const hasExternalAi = currentViolations.some(v => v.type === 'external_ai_detected');
const hasTabSwitch = currentViolations.some(v => v.type === 'tab_switch');
const hasPhoneTalk = currentViolations.some(v => v.type === 'talking_on_phone');
if (hasExternalAi) {
sosBtn.innerText = '🚨 candidate might use external ai';
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) {
sosBtn.innerText = '📸 Candidate Switched Browser Tab';
sosBtn.style.background = 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)';
@ -1580,6 +1585,14 @@ function openSosModal() {
typeTitle = 'candidate might use external ai';
} else if (v.type === 'tab_switch') {
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'};">
<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);
$poll2 = $this->get(route('interview.poll', $interview->id));
$poll2->assertStatus(200)
->assertJsonFragment(['type' => 'external_ai_detected']);
// 3. Talking on phone violation
$phoneResp = $this->post(route('interview.violation', $interview->id), [
'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']);
}
}