From 78ed98bc436e69c42919a1adc4d4b9fca55126aa Mon Sep 17 00:00:00 2001 From: "kushal.saha" Date: Wed, 12 Aug 2026 13:48:22 +0000 Subject: [PATCH] fix: phone detection and human redabble logs --- resources/js/candidate-proctor.js | 53 ++++++++++++++----- resources/js/interview-call.js | 22 ++------ .../views/components/audit-log-item.blade.php | 22 ++------ resources/views/interview/index.blade.php | 4 +- resources/views/interview/show.blade.php | 2 +- 5 files changed, 52 insertions(+), 51 deletions(-) diff --git a/resources/js/candidate-proctor.js b/resources/js/candidate-proctor.js index a418df7..981a954 100644 --- a/resources/js/candidate-proctor.js +++ b/resources/js/candidate-proctor.js @@ -877,11 +877,26 @@ class PhoneDetector { } detect(video, timestamp) { - const detections = this.detector.detectForVideo(video, timestamp).detections || []; - const phone = detections - .map((detection) => ({ detection, category: detection.categories?.[0] })) - .find(({ category }) => category && /cell phone|mobile phone|phone/i.test(category.categoryName || '') && (category.score || 0) >= 0.45); - return phone ? { confidence: phone.category.score, label: phone.category.categoryName } : null; + if (!this.detector || !video || video.readyState < 2 || !video.videoWidth) return null; + try { + const ts = Math.round(timestamp); + const result = this.detector.detectForVideo(video, ts); + const detections = result ? (result.detections || []) : []; + for (const detection of detections) { + const categories = detection.categories || []; + for (const category of categories) { + const name = (category.categoryName || category.displayName || '').toLowerCase(); + const score = category.score || 0; + if (/cell phone|mobile phone|phone|remote|handheld|device|calculator|tablet/i.test(name) && score >= 0.15) { + console.log('[proctoring:phone_detected_raw]', { name, score }); + return { confidence: score, label: category.categoryName || category.displayName || 'cell phone' }; + } + } + } + } catch (e) { + console.warn('[proctoring:phone_detect_error]', e.message); + } + return null; } } @@ -1106,19 +1121,29 @@ class CandidateProctoring { try { const vision = await this.importVisionModule(); const fileset = await vision.FilesetResolver.forVisionTasks(WASM_ROOT); - const [faceLandmarker, objectDetector] = await this.withAmdLoaderDisabled(() => Promise.all([ + + const faceLandmarker = await this.withAmdLoaderDisabled(() => this.createModel(vision.FaceLandmarker, fileset, { modelAssetPath: FACE_MODEL_URL, runningMode: 'VIDEO', numFaces: 3, minFaceDetectionConfidence: 0.5, minFacePresenceConfidence: 0.5, minTrackingConfidence: 0.5, - }), - this.createModel(vision.ObjectDetector, fileset, { - modelAssetPath: OBJECT_MODEL_URL, - runningMode: 'VIDEO', maxResults: 5, scoreThreshold: 0.35, - }), - ])); + }) + ); this.faceDetector = new FacePoseDetector(faceLandmarker); - this.phoneDetector = new PhoneDetector(objectDetector); + + try { + const objectDetector = await this.withAmdLoaderDisabled(() => + this.createModel(vision.ObjectDetector, fileset, { + modelAssetPath: OBJECT_MODEL_URL, + runningMode: 'VIDEO', maxResults: 10, scoreThreshold: 0.15, + }) + ); + this.phoneDetector = new PhoneDetector(objectDetector); + console.log('[proctoring:object_detector_ready]'); + } catch (objErr) { + console.error('[proctoring:object_detector_failed]', objErr); + } + this.modelsReady = true; this.running = true; this.scheduleNextSample(); @@ -1181,7 +1206,7 @@ class CandidateProctoring { const timestamp = performance.now(); try { const face = this.faceDetector.detect(this.video, timestamp); - const phone = this.phoneDetector.detect(this.video, timestamp); + const phone = this.phoneDetector ? this.phoneDetector.detect(this.video, timestamp) : null; this.processFace(face, timestamp); this.processPhone(phone, timestamp); } catch (error) { diff --git a/resources/js/interview-call.js b/resources/js/interview-call.js index 2a41506..108a0ea 100644 --- a/resources/js/interview-call.js +++ b/resources/js/interview-call.js @@ -140,15 +140,8 @@ export function formatProctoringLogDetails(l) { ? detailsObj.reasons.map(r => reasonLabels[r] || r.replace(/_/g, ' ')).join(', ') : ''; - const parts = []; - if (reasonsStr) parts.push(reasonsStr); - if (typeof detailsObj.yaw === 'number') parts.push(`Yaw: ${detailsObj.yaw > 0 ? '+' : ''}${detailsObj.yaw.toFixed(2)}`); - if (typeof detailsObj.pitch === 'number') parts.push(`Pitch: ${detailsObj.pitch > 0 ? '+' : ''}${detailsObj.pitch.toFixed(2)}`); - if (typeof detailsObj.gaze_horizontal === 'number') parts.push(`Gaze H: ${detailsObj.gaze_horizontal.toFixed(2)}`); - if (typeof detailsObj.gaze_vertical === 'number') parts.push(`Gaze V: ${detailsObj.gaze_vertical.toFixed(2)}`); - - const durationPrefix = (type === 'prolonged_looking_away') ? 'Looking away continuously (>10s) — ' : ''; - return `${durationPrefix}${parts.join(' | ')}`; + const durationPrefix = (type === 'prolonged_looking_away') ? 'Looking away continuously (>10s)' : 'Looking away from camera'; + return reasonsStr ? `${durationPrefix} (${reasonsStr})` : durationPrefix; } else if (typeof l.details === 'string' && l.details && !l.details.startsWith('{')) { return l.details; } @@ -163,15 +156,10 @@ export function formatProctoringLogDetails(l) { if (type === 'prolonged_corner_gaze') { if (typeof detailsObj === 'object' && detailsObj && (detailsObj.corner_label || detailsObj.corner)) { const cornerLabel = detailsObj.corner_label || (detailsObj.corner || 'screen corner').replace(/_/g, ' '); - const dur = detailsObj.duration_s || Math.round((detailsObj.duration_ms || 25000) / 1000); - const parts = [`Fixated on ${cornerLabel} for ${dur}s`]; - if (typeof detailsObj.yaw === 'number') parts.push(`Yaw: ${detailsObj.yaw > 0 ? '+' : ''}${detailsObj.yaw.toFixed(2)}`); - if (typeof detailsObj.pitch === 'number') parts.push(`Pitch: ${detailsObj.pitch > 0 ? '+' : ''}${detailsObj.pitch.toFixed(2)}`); - if (typeof detailsObj.gaze_horizontal === 'number') parts.push(`Gaze H: ${detailsObj.gaze_horizontal.toFixed(2)}`); - if (typeof detailsObj.gaze_vertical === 'number') parts.push(`Gaze V: ${detailsObj.gaze_vertical.toFixed(2)}`); - return parts.join(' | '); + const dur = detailsObj.duration_s || Math.round((detailsObj.duration_ms || 18000) / 1000); + return `Fixated on ${cornerLabel} for ${dur}s`; } - return 'Candidate fixated on screen corner for prolonged period (25+ seconds)'; + return 'Candidate fixated on screen corner for prolonged period'; } if (type === 'talking_secondary_person') { diff --git a/resources/views/components/audit-log-item.blade.php b/resources/views/components/audit-log-item.blade.php index a652369..6278c89 100644 --- a/resources/views/components/audit-log-item.blade.php +++ b/resources/views/components/audit-log-item.blade.php @@ -68,15 +68,8 @@ $reasonsArr = isset($detailsObj['reasons']) && is_array($detailsObj['reasons']) ? array_map(fn($r) => $reasonLabels[$r] ?? str_replace('_', ' ', $r), $detailsObj['reasons']) : []; $reasonsStr = implode(', ', $reasonsArr); - $parts = []; - if ($reasonsStr) $parts[] = $reasonsStr; - if (isset($detailsObj['yaw']) && is_numeric($detailsObj['yaw'])) $parts[] = 'Yaw: ' . ($detailsObj['yaw'] > 0 ? '+' : '') . number_format((float)$detailsObj['yaw'], 2); - if (isset($detailsObj['pitch']) && is_numeric($detailsObj['pitch'])) $parts[] = 'Pitch: ' . ($detailsObj['pitch'] > 0 ? '+' : '') . number_format((float)$detailsObj['pitch'], 2); - if (isset($detailsObj['gaze_horizontal']) && is_numeric($detailsObj['gaze_horizontal'])) $parts[] = 'Gaze H: ' . number_format((float)$detailsObj['gaze_horizontal'], 2); - if (isset($detailsObj['gaze_vertical']) && is_numeric($detailsObj['gaze_vertical'])) $parts[] = 'Gaze V: ' . number_format((float)$detailsObj['gaze_vertical'], 2); - - $prefix = ($type === 'prolonged_looking_away') ? 'Looking away continuously (>10s) — ' : ''; - $humanDetails = $prefix . implode(' | ', $parts); + $prefix = ($type === 'prolonged_looking_away') ? 'Looking away continuously (>10s)' : 'Looking away from camera'; + $humanDetails = $reasonsStr ? "{$prefix} ({$reasonsStr})" : $prefix; } else { $humanDetails = is_string($details) && !str_starts_with(trim($details), '{') ? $details : ($type === 'prolonged_looking_away' ? 'Candidate prolonged looking away' : 'Candidate looking away from camera'); } @@ -86,15 +79,10 @@ } elseif ($type === 'prolonged_corner_gaze') { if (is_array($detailsObj) && (isset($detailsObj['corner_label']) || isset($detailsObj['corner']))) { $cornerLabel = $detailsObj['corner_label'] ?? str_replace('_', ' ', $detailsObj['corner'] ?? 'screen corner'); - $dur = $detailsObj['duration_s'] ?? round(($detailsObj['duration_ms'] ?? 25000) / 1000); - $parts = ["Fixated on {$cornerLabel} for {$dur}s"]; - if (isset($detailsObj['yaw']) && is_numeric($detailsObj['yaw'])) $parts[] = 'Yaw: ' . ($detailsObj['yaw'] > 0 ? '+' : '') . number_format((float)$detailsObj['yaw'], 2); - if (isset($detailsObj['pitch']) && is_numeric($detailsObj['pitch'])) $parts[] = 'Pitch: ' . ($detailsObj['pitch'] > 0 ? '+' : '') . number_format((float)$detailsObj['pitch'], 2); - if (isset($detailsObj['gaze_horizontal']) && is_numeric($detailsObj['gaze_horizontal'])) $parts[] = 'Gaze H: ' . number_format((float)$detailsObj['gaze_horizontal'], 2); - if (isset($detailsObj['gaze_vertical']) && is_numeric($detailsObj['gaze_vertical'])) $parts[] = 'Gaze V: ' . number_format((float)$detailsObj['gaze_vertical'], 2); - $humanDetails = implode(' | ', $parts); + $dur = $detailsObj['duration_s'] ?? round(($detailsObj['duration_ms'] ?? 18000) / 1000); + $humanDetails = "Fixated on {$cornerLabel} for {$dur}s"; } else { - $humanDetails = 'Candidate fixated on screen corner for prolonged period (25+ seconds)'; + $humanDetails = 'Candidate fixated on screen corner for prolonged period'; } } elseif ($type === 'talking_secondary_person') { $humanDetails = is_string($details) && !str_starts_with(trim($details), '{') ? $details : 'Candidate detected speaking out loud to a secondary person'; diff --git a/resources/views/interview/index.blade.php b/resources/views/interview/index.blade.php index 5c9508e..1bcfd21 100644 --- a/resources/views/interview/index.blade.php +++ b/resources/views/interview/index.blade.php @@ -47,7 +47,7 @@ -
+
@if(session('success'))
{{ session('success') }} @@ -55,7 +55,7 @@ @endif -
+

diff --git a/resources/views/interview/show.blade.php b/resources/views/interview/show.blade.php index f7a1f0b..9dd9cff 100644 --- a/resources/views/interview/show.blade.php +++ b/resources/views/interview/show.blade.php @@ -68,7 +68,7 @@ -
+