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) {
|
detect(video, timestamp) {
|
||||||
const detections = this.detector.detectForVideo(video, timestamp).detections || [];
|
if (!this.detector || !video || video.readyState < 2 || !video.videoWidth) return null;
|
||||||
const phone = detections
|
try {
|
||||||
.map((detection) => ({ detection, category: detection.categories?.[0] }))
|
const ts = Math.round(timestamp);
|
||||||
.find(({ category }) => category && /cell phone|mobile phone|phone/i.test(category.categoryName || '') && (category.score || 0) >= 0.45);
|
const result = this.detector.detectForVideo(video, ts);
|
||||||
return phone ? { confidence: phone.category.score, label: phone.category.categoryName } : null;
|
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 {
|
try {
|
||||||
const vision = await this.importVisionModule();
|
const vision = await this.importVisionModule();
|
||||||
const fileset = await vision.FilesetResolver.forVisionTasks(WASM_ROOT);
|
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, {
|
this.createModel(vision.FaceLandmarker, fileset, {
|
||||||
modelAssetPath: FACE_MODEL_URL,
|
modelAssetPath: FACE_MODEL_URL,
|
||||||
runningMode: 'VIDEO', numFaces: 3, minFaceDetectionConfidence: 0.5,
|
runningMode: 'VIDEO', numFaces: 3, minFaceDetectionConfidence: 0.5,
|
||||||
minFacePresenceConfidence: 0.5, minTrackingConfidence: 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.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.modelsReady = true;
|
||||||
this.running = true;
|
this.running = true;
|
||||||
this.scheduleNextSample();
|
this.scheduleNextSample();
|
||||||
@ -1181,7 +1206,7 @@ class CandidateProctoring {
|
|||||||
const timestamp = performance.now();
|
const timestamp = performance.now();
|
||||||
try {
|
try {
|
||||||
const face = this.faceDetector.detect(this.video, timestamp);
|
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.processFace(face, timestamp);
|
||||||
this.processPhone(phone, timestamp);
|
this.processPhone(phone, timestamp);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -140,15 +140,8 @@ export function formatProctoringLogDetails(l) {
|
|||||||
? detailsObj.reasons.map(r => reasonLabels[r] || r.replace(/_/g, ' ')).join(', ')
|
? detailsObj.reasons.map(r => reasonLabels[r] || r.replace(/_/g, ' ')).join(', ')
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const parts = [];
|
const durationPrefix = (type === 'prolonged_looking_away') ? 'Looking away continuously (>10s)' : 'Looking away from camera';
|
||||||
if (reasonsStr) parts.push(reasonsStr);
|
return reasonsStr ? `${durationPrefix} (${reasonsStr})` : durationPrefix;
|
||||||
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(' | ')}`;
|
|
||||||
} else if (typeof l.details === 'string' && l.details && !l.details.startsWith('{')) {
|
} else if (typeof l.details === 'string' && l.details && !l.details.startsWith('{')) {
|
||||||
return l.details;
|
return l.details;
|
||||||
}
|
}
|
||||||
@ -163,15 +156,10 @@ export function formatProctoringLogDetails(l) {
|
|||||||
if (type === 'prolonged_corner_gaze') {
|
if (type === 'prolonged_corner_gaze') {
|
||||||
if (typeof detailsObj === 'object' && detailsObj && (detailsObj.corner_label || detailsObj.corner)) {
|
if (typeof detailsObj === 'object' && detailsObj && (detailsObj.corner_label || detailsObj.corner)) {
|
||||||
const cornerLabel = detailsObj.corner_label || (detailsObj.corner || 'screen corner').replace(/_/g, ' ');
|
const cornerLabel = detailsObj.corner_label || (detailsObj.corner || 'screen corner').replace(/_/g, ' ');
|
||||||
const dur = detailsObj.duration_s || Math.round((detailsObj.duration_ms || 25000) / 1000);
|
const dur = detailsObj.duration_s || Math.round((detailsObj.duration_ms || 18000) / 1000);
|
||||||
const parts = [`Fixated on ${cornerLabel} for ${dur}s`];
|
return `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(' | ');
|
|
||||||
}
|
}
|
||||||
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') {
|
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']) : [];
|
$reasonsArr = isset($detailsObj['reasons']) && is_array($detailsObj['reasons']) ? array_map(fn($r) => $reasonLabels[$r] ?? str_replace('_', ' ', $r), $detailsObj['reasons']) : [];
|
||||||
$reasonsStr = implode(', ', $reasonsArr);
|
$reasonsStr = implode(', ', $reasonsArr);
|
||||||
|
|
||||||
$parts = [];
|
$prefix = ($type === 'prolonged_looking_away') ? 'Looking away continuously (>10s)' : 'Looking away from camera';
|
||||||
if ($reasonsStr) $parts[] = $reasonsStr;
|
$humanDetails = $reasonsStr ? "{$prefix} ({$reasonsStr})" : $prefix;
|
||||||
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);
|
|
||||||
} else {
|
} else {
|
||||||
$humanDetails = is_string($details) && !str_starts_with(trim($details), '{') ? $details : ($type === 'prolonged_looking_away' ? 'Candidate prolonged looking away' : 'Candidate looking away from camera');
|
$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') {
|
} elseif ($type === 'prolonged_corner_gaze') {
|
||||||
if (is_array($detailsObj) && (isset($detailsObj['corner_label']) || isset($detailsObj['corner']))) {
|
if (is_array($detailsObj) && (isset($detailsObj['corner_label']) || isset($detailsObj['corner']))) {
|
||||||
$cornerLabel = $detailsObj['corner_label'] ?? str_replace('_', ' ', $detailsObj['corner'] ?? 'screen corner');
|
$cornerLabel = $detailsObj['corner_label'] ?? str_replace('_', ' ', $detailsObj['corner'] ?? 'screen corner');
|
||||||
$dur = $detailsObj['duration_s'] ?? round(($detailsObj['duration_ms'] ?? 25000) / 1000);
|
$dur = $detailsObj['duration_s'] ?? round(($detailsObj['duration_ms'] ?? 18000) / 1000);
|
||||||
$parts = ["Fixated on {$cornerLabel} for {$dur}s"];
|
$humanDetails = "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);
|
|
||||||
} else {
|
} 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') {
|
} elseif ($type === 'talking_secondary_person') {
|
||||||
$humanDetails = is_string($details) && !str_starts_with(trim($details), '{') ? $details : 'Candidate detected speaking out loud to a 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>
|
</div>
|
||||||
</header>
|
</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'))
|
@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">
|
<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') }}
|
{{ session('success') }}
|
||||||
@ -55,7 +55,7 @@
|
|||||||
@endif
|
@endif
|
||||||
|
|
||||||
<!-- Global Admin Settings & Proctoring Control Panel -->
|
<!-- 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 class="flex justify-between items-center pb-3 mb-4 border-b border-white/10">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-outfit text-xl font-bold text-white m-0 flex items-center gap-2">
|
<h3 class="font-outfit text-xl font-bold text-white m-0 flex items-center gap-2">
|
||||||
|
|||||||
@ -68,7 +68,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen flex flex-col antialiased">
|
<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 -->
|
<!-- Top bar -->
|
||||||
<div class="flex items-center justify-between pb-4 mb-4 border-b border-[#1b2233]">
|
<div class="flex items-center justify-between pb-4 mb-4 border-b border-[#1b2233]">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user