fix: phone detection and human redabble logs
This commit is contained in:
parent
eba3ec96b3
commit
78ed98bc43
@ -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) {
|
||||
|
||||
@ -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') {
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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]">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user