new_fixes #18
@ -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 ?? '');
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.'}
|
||||
|
||||
@ -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']);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user