fix: phone detection and human redabble logs

This commit is contained in:
kushal.saha 2026-08-12 13:48:22 +00:00
parent eba3ec96b3
commit 78ed98bc43
5 changed files with 52 additions and 51 deletions

View File

@ -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.faceDetector = new FacePoseDetector(faceLandmarker);
try {
const objectDetector = await this.withAmdLoaderDisabled(() =>
this.createModel(vision.ObjectDetector, fileset, {
modelAssetPath: OBJECT_MODEL_URL,
runningMode: 'VIDEO', maxResults: 5, scoreThreshold: 0.35,
}),
]));
this.faceDetector = new FacePoseDetector(faceLandmarker);
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) {

View File

@ -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') {

View File

@ -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';

View File

@ -47,7 +47,7 @@
</div>
</header>
<main class="max-w-[1400px] w-full mx-auto my-6 px-4 flex-1">
<main class="w-full mx-auto my-6 px-7 flex-1">
@if(session('success'))
<div class="mb-6 bg-emerald-500/15 border border-emerald-500/30 text-emerald-300 px-5 py-3 rounded-xl text-sm">
{{ session('success') }}
@ -55,7 +55,7 @@
@endif
<!-- Global Admin Settings & Proctoring Control Panel -->
<section class="mb-6 p-5 bg-gradient-to-r from-slate-900/95 to-slate-800/95 border border-indigo-500/35 rounded-2xl shadow-xl">
<section class="mb-6 p-5 bg-linear-to-r from-slate-900/95 to-slate-800/95 border border-indigo-500/35 rounded-2xl shadow-xl">
<div class="flex justify-between items-center pb-3 mb-4 border-b border-white/10">
<div>
<h3 class="font-outfit text-xl font-bold text-white m-0 flex items-center gap-2">

View File

@ -68,7 +68,7 @@
</head>
<body class="min-h-screen flex flex-col antialiased">
<div class="max-w-[1320px] w-full mx-auto px-7 py-5 pb-16 flex-1">
<div class="w-full mx-auto px-7 py-5 pb-16 flex-1">
<!-- Top bar -->
<div class="flex items-center justify-between pb-4 mb-4 border-b border-[#1b2233]">