AI 監督システム: コンピューター ビジョンによるプライバシー最優先
2020年は大学受験が工学系の問題になった。パンデミックにより、 何百万人もの学生が自宅や教育機関から技能試験を受けていることに気づきました 学校はデジタル監視システムで迅速に対応する必要がありました。 その結果、市場は爆発的に拡大した AI 監督、しかしまた これらのシステムの倫理的および法的影響に対する認識が高まっています。
2025 年、この分野は重要な成熟度に達しています。ザ」EU AI法、入力されました 完全に施行され、教育における生体認証システムが広告システムとして分類される ハイリスク、厳格な透明性、文書化、 人間の監視。の GDPR データ処理にさらなる制約が追加される 生体認証。これらの要件を無視したプラットフォームには、最大 4% の罰金が課される危険があります。 世界の年間売上高。
本稿では、プライバシーを起点としたAI監督システムを構築します。 後から考えたものではなく、創業の原則。顔検出、視線の実装方法を見ていきます。 ローカルで処理された生体認証データを維持しながら、追跡と異常検出を実現します。 収集の最小化と完全な GDPR 準拠。
何を学ぶか
- プライバシー最優先のアーキテクチャ: オンデバイス処理とクラウド処理
- MediaPipe FaceMesh による顔検出: 468 個のランドマークをリアルタイムに検出
- 視線追跡: 標準的な Web カメラからの視線方向の推定
- 不審な動作の異常検出: 音声、タブ切り替え、マルチフェイス
- GDPR への準拠: データの最小化、同意、保持ポリシー
- 人間による監視: フラグシステムと強制的な人間によるレビュー
1. プライバシー第一の設計原則
プライバシー最優先の監督システムは、従来の「すべてを収集し、 後でフィルターします。」基本原則は次のとおりです。
- データの最小化:連続動画を収集しません。録音のみ 限られた時間バッファーによる異常イベント (例: 異常から ±10 秒)。
- オンデバイス処理: ブラウザ内の生体認証データを処理します。 WebAssembly/JavaScript。サーバーは生のビデオ フィードを決して参照せず、メタデータのみを参照します。 イベントの匿名化。
- 擬似匿名化: 学生 ID には不可逆ハッシュを使用します 監督システム内で、登録された身元から分離されます。
- 目的の制限: 収集されたデータは次の目的でのみ使用できます。 特定の試験セッション。30 日後に自動的にキャンセルされます。
- 人間による監視の義務化: 拘束力のある自動決定はありません。 AI が異常にフラグを立て、その結果を判断するのは人間です。
// Architettura Privacy-First: on-device processing con MediaPipe
// Nessun dato biometrico trasmesso al server - solo eventi anonimizzati
import * as faceMesh from '@mediapipe/face_mesh';
import * as camera from '@mediapipe/camera_utils';
interface ProctoringEvent {
type: 'gaze_away' | 'face_missing' | 'multiple_faces' | 'tab_switch' | 'audio_anomaly';
timestamp: number;
duration?: number; // ms di persistenza dell'anomalia
confidence: number; // 0-1
// NESSUN dato biometrico raw - solo metadata
}
interface ProctoringSession {
sessionId: string; // Pseudoanonimo - hash dell'exam_id + student_hash
startTime: number;
events: ProctoringEvent[];
snapshotCount: number; // Solo contatore, no immagini
}
class PrivacyFirstProctor {
private faceMeshInstance: faceMesh.FaceMesh | null = null;
private session: ProctoringSession;
private gazeTracker: GazeTracker;
private audioAnalyzer: AudioAnomalyDetector;
private eventBuffer: ProctoringEvent[] = [];
// Soglie configurabili per bilanciare falsi positivi/negativi
private readonly CONFIG = {
GAZE_AWAY_THRESHOLD_DEG: 30, // Gradi di deviazione per "gaze away"
GAZE_AWAY_DURATION_MS: 3000, // 3s continuativi per flaggare
FACE_MISSING_DURATION_MS: 2000, // 2s senza volto per flaggare
MAX_EVENTS_PER_SESSION: 100, // Limite per GDPR data minimization
SNAPSHOT_BUFFER_SEC: 10, // Buffer ±10s per snapshot anomalia
};
constructor(private readonly sessionHash: string) {
this.session = {
sessionId: sessionHash,
startTime: Date.now(),
events: [],
snapshotCount: 0
};
this.gazeTracker = new GazeTracker(this.CONFIG.GAZE_AWAY_THRESHOLD_DEG);
this.audioAnalyzer = new AudioAnomalyDetector();
}
async initialize(videoElement: HTMLVideoElement): Promise<void> {
// Inizializza MediaPipe FaceMesh - processing LOCALE nel browser
this.faceMeshInstance = new faceMesh.FaceMesh({
locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${file}`
});
this.faceMeshInstance.setOptions({
maxNumFaces: 3, // Rileva fino a 3 volti per multi-face detection
refineLandmarks: true, // Landmark oculari precisi per gaze tracking
minDetectionConfidence: 0.7,
minTrackingConfidence: 0.7
});
this.faceMeshInstance.onResults((results) => {
this.processResults(results);
});
// Setup camera
const cameraInstance = new camera.Camera(videoElement, {
onFrame: async () => {
await this.faceMeshInstance!.send({ image: videoElement });
},
width: 640,
height: 480
});
await cameraInstance.start();
// Setup visibility API per tab switching
this.setupTabSwitchDetection();
// Setup audio monitoring (opzionale, richiede consenso separato)
await this.audioAnalyzer.initialize();
}
private processResults(results: faceMesh.Results): void {
const landmarks = results.multiFaceLandmarks;
const faceCount = landmarks?.length || 0;
// Anomalia: nessun volto rilevato
if (faceCount === 0) {
this.recordEvent({
type: 'face_missing',
timestamp: Date.now(),
confidence: 0.9
});
return;
}
// Anomalia: più di un volto (possibile persona che aiuta)
if (faceCount > 1) {
this.recordEvent({
type: 'multiple_faces',
timestamp: Date.now(),
confidence: Math.min(0.6 + faceCount * 0.1, 0.95)
});
}
// Analisi gaze per il volto principale
const primaryFace = landmarks[0];
const gazeResult = this.gazeTracker.analyze(primaryFace);
if (gazeResult.isLookingAway) {
this.recordEvent({
type: 'gaze_away',
timestamp: Date.now(),
confidence: gazeResult.confidence,
// NON salvare la direzione precisa dello sguardo - solo il flag
});
}
}
private recordEvent(event: ProctoringEvent): void {
// GDPR: limit massimo eventi per minimizzazione dati
if (this.session.events.length >= this.CONFIG.MAX_EVENTS_PER_SESSION) {
console.warn('Max events per session reached - data minimization applied');
return;
}
this.session.events.push(event);
}
private setupTabSwitchDetection(): void {
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.recordEvent({
type: 'tab_switch',
timestamp: Date.now(),
confidence: 1.0 // Deterministica
});
}
});
// Previeni apertura DevTools (limitata efficacia, ma segnale utile)
window.addEventListener('resize', () => {
const threshold = 160;
if (
window.outerWidth - window.innerWidth > threshold ||
window.outerHeight - window.innerHeight > threshold
) {
this.recordEvent({
type: 'tab_switch', // Approssimazione per DevTools
timestamp: Date.now(),
confidence: 0.5
});
}
});
}
getReport(): ProctoringReport {
return ProctoringReportGenerator.generate(this.session);
}
}
2. MediaPipe FaceMesh を使用した視線追跡
標準的な Web カメラの視線追跡 (特殊なハードウェアなし) により方向を推定します 目の端に対する瞳孔の位置を分析することにより視線を測定します。 MediaPipe FaceMesh は、各目の 71 個のランドマークを含む 468 個の顔のランドマークを提供します 重大な逸脱を検出するのに十分な精度で。
// Indici landmark MediaPipe FaceMesh per gli occhi
// Documentazione: https://google.github.io/mediapipe/solutions/face_mesh.html
const FACE_MESH_INDICES = {
// Occhio sinistro
LEFT_EYE: {
OUTER: 33, // Angolo esterno
INNER: 133, // Angolo interno
TOP: 159, // Palpebra superiore
BOTTOM: 145, // Palpebra inferiore
IRIS_CENTER: 468, // Centro iride (solo con refineLandmarks: true)
},
// Occhio destro
RIGHT_EYE: {
OUTER: 362,
INNER: 263,
TOP: 386,
BOTTOM: 374,
IRIS_CENTER: 473,
},
// Naso (per orientamento testa)
NOSE_TIP: 4,
LEFT_CHEEK: 234,
RIGHT_CHEEK: 454
};
interface GazeResult {
isLookingAway: boolean;
horizontalAngle: number; // Gradi: negativo=sinistra, positivo=destra
verticalAngle: number; // Gradi: negativo=basso, positivo=alto
confidence: number;
}
class GazeTracker {
private gazeAwayStartTime: number | null = null;
private readonly AWAY_THRESHOLD_DEG: number;
private readonly AWAY_DURATION_MS = 3000;
constructor(awayThresholdDeg: number = 30) {
this.AWAY_THRESHOLD_DEG = awayThresholdDeg;
}
analyze(landmarks: faceMesh.NormalizedLandmarkList): GazeResult {
// Stima orientamento della testa tramite landmark
const headPose = this.estimateHeadPose(landmarks);
// Stima posizione iride relativa all'occhio
const leftGaze = this.estimateEyeGaze(
landmarks,
FACE_MESH_INDICES.LEFT_EYE,
FACE_MESH_INDICES.LEFT_EYE.IRIS_CENTER
);
const rightGaze = this.estimateEyeGaze(
landmarks,
FACE_MESH_INDICES.RIGHT_EYE,
FACE_MESH_INDICES.RIGHT_EYE.IRIS_CENTER
);
// Media pesata occhio sinistro e destro
const combinedHorizontal = (leftGaze.horizontal + rightGaze.horizontal) / 2;
const combinedVertical = (leftGaze.vertical + rightGaze.vertical) / 2;
// Combina con head pose
const totalHorizontal = combinedHorizontal + headPose.yaw;
const totalVertical = combinedVertical + headPose.pitch;
const isAway = (
Math.abs(totalHorizontal) > this.AWAY_THRESHOLD_DEG ||
Math.abs(totalVertical) > this.AWAY_THRESHOLD_DEG
);
// Timing per durata
if (isAway && this.gazeAwayStartTime === null) {
this.gazeAwayStartTime = Date.now();
} else if (!isAway) {
this.gazeAwayStartTime = null;
}
const isPersistentlyAway = this.gazeAwayStartTime !== null &&
(Date.now() - this.gazeAwayStartTime) > this.AWAY_DURATION_MS;
return {
isLookingAway: isPersistentlyAway,
horizontalAngle: totalHorizontal,
verticalAngle: totalVertical,
confidence: 0.7 // Stima conservativa per webcam standard
};
}
private estimateEyeGaze(
landmarks: faceMesh.NormalizedLandmarkList,
eyeIndices: typeof FACE_MESH_INDICES.LEFT_EYE,
irisIndex: number
): { horizontal: number; vertical: number } {
const outer = landmarks[eyeIndices.OUTER];
const inner = landmarks[eyeIndices.INNER];
const top = landmarks[eyeIndices.TOP];
const bottom = landmarks[eyeIndices.BOTTOM];
const iris = landmarks[irisIndex];
if (!iris) {
// refineLandmarks disabilitato o iris non rilevata
return { horizontal: 0, vertical: 0 };
}
// Centro geometrico dell'occhio
const eyeCenterX = (outer.x + inner.x) / 2;
const eyeCenterY = (top.y + bottom.y) / 2;
// Dimensioni dell'occhio
const eyeWidth = Math.abs(outer.x - inner.x);
const eyeHeight = Math.abs(top.y - bottom.y);
// Offset normalizzato dell'iride rispetto al centro
const normalizedX = (iris.x - eyeCenterX) / (eyeWidth / 2 + 1e-6);
const normalizedY = (iris.y - eyeCenterY) / (eyeHeight / 2 + 1e-6);
// Converti in gradi approssimativi
return {
horizontal: normalizedX * 45, // Max ~45 gradi
vertical: normalizedY * 30 // Max ~30 gradi verticali
};
}
private estimateHeadPose(
landmarks: faceMesh.NormalizedLandmarkList
): { yaw: number; pitch: number } {
const noseTip = landmarks[FACE_MESH_INDICES.NOSE_TIP];
const leftCheek = landmarks[FACE_MESH_INDICES.LEFT_CHEEK];
const rightCheek = landmarks[FACE_MESH_INDICES.RIGHT_CHEEK];
// Yaw: asimmetria sinistra/destra delle guance
const cheekMidX = (leftCheek.x + rightCheek.x) / 2;
const cheekWidth = Math.abs(rightCheek.x - leftCheek.x);
const yawRatio = (noseTip.x - cheekMidX) / (cheekWidth / 2 + 1e-6);
const yaw = yawRatio * 45;
// Pitch: posizione verticale del naso
const cheekMidY = (leftCheek.y + rightCheek.y) / 2;
const pitchRatio = (noseTip.y - cheekMidY) / 0.1;
const pitch = Math.max(-30, Math.min(30, pitchRatio * 20));
return { yaw, pitch };
}
}
3. マルチモーダルな異常検出
堅牢な監督システムは、複数のソースからの信号を組み合わせて、 誤検知。単一の信号 (例: 3 秒間目をそらす) で、 無害な行為である可能性が高い。信号の組み合わせ 効率が大幅に向上します。
interface AnomalyScore {
overall: number; // 0-1
breakdown: {
gaze: number;
face: number;
audio: number;
behavior: number;
};
riskLevel: 'low' | 'medium' | 'high';
requiresHumanReview: boolean;
}
class MultiModalAnomalyDetector {
/**
* Calcola uno score di anomalia composito.
* Nessuna decisione automatica - solo scoring per review umana.
*/
computeRiskScore(session: ProctoringSession): AnomalyScore {
const events = session.events;
const sessionDuration = (Date.now() - session.startTime) / 60000; // minuti
// Normalizza per durata sessione (eventi per 10 minuti)
const normalize = (count: number) => Math.min(count / (sessionDuration / 10 + 1), 1.0);
// Score gaze: ponderato per confidenza degli eventi
const gazeEvents = events.filter(e => e.type === 'gaze_away');
const gazeScore = normalize(
gazeEvents.reduce((sum, e) => sum + e.confidence, 0)
);
// Score face: assenza o volti multipli
const faceEvents = events.filter(e =>
e.type === 'face_missing' || e.type === 'multiple_faces'
);
const faceScore = Math.min(
normalize(faceEvents.length) +
(faceEvents.some(e => e.type === 'multiple_faces') ? 0.3 : 0),
1.0
);
// Score audio: anomalie audio
const audioEvents = events.filter(e => e.type === 'audio_anomaly');
const audioScore = normalize(audioEvents.length);
// Score comportamentale: tab switching
const tabEvents = events.filter(e => e.type === 'tab_switch');
const behaviorScore = Math.min(tabEvents.length * 0.25, 1.0);
// Score overall: media pesata
const overall = (
gazeScore * 0.25 +
faceScore * 0.35 +
audioScore * 0.20 +
behaviorScore * 0.20
);
const riskLevel = overall < 0.3 ? 'low' : overall < 0.6 ? 'medium' : 'high';
return {
overall: Math.round(overall * 100) / 100,
breakdown: {
gaze: Math.round(gazeScore * 100) / 100,
face: Math.round(faceScore * 100) / 100,
audio: Math.round(audioScore * 100) / 100,
behavior: Math.round(behaviorScore * 100) / 100
},
riskLevel,
// GDPR EU AI Act: review umana OBBLIGATORIA per qualunque livello di rischio
requiresHumanReview: riskLevel === 'high' || behaviorScore > 0.5
};
}
}
// Report finale - struttura GDPR-compliant
interface ProctoringReport {
sessionId: string; // Pseudoanonimo
examId: string;
eventCount: number;
anomalyScore: AnomalyScore;
recommendation: 'pass' | 'review_required';
reviewDeadline: Date; // 30 giorni per revisione, poi cancellazione
dataRetentionDate: Date; // Data cancellazione automatica
// NESSUN video, NESSUNA immagine biometrica
}
4. GDPRおよびEU AI法の遵守
生体認証データを収集する AI 監督システムは、法第 9 条の対象となります。 GDPR (データの特別なカテゴリー) および EU AI 法 (状況に応じた高リスクシステム) 教育的)。主な要件は次のとおりです。
| 要件 | GDPR | EU AI法 | 実装 |
|---|---|---|---|
| 明示的な同意 | 第9条(2)(a) | 第13条 | 試験前の同意書 |
| データの最小化 | 第 5 条(1)(c) | 第10条 | イベントメタデータのみ、連続ビデオなし |
| 限定された保持 | 第5条(1)(e) | - | 30日後に自動削除 |
| 人間による監視 | 第22条 | 第14条 | 拘束力のある自動決定はありません |
| 透明性 | 第13条/第14条 | 第13条 | 何が検出されたのかについての明確な情報 |
| DPIA 必須 | 第35条 | - | 導入前の影響評価 |
// Implementazione consenso GDPR-compliant
// Il sistema non si avvia senza consenso esplicito e registrato
interface ConsentRecord {
studentHash: string; // Hash irreversibile dell'ID studente
examId: string;
timestamp: number;
consentVersion: string; // Versione dell'informativa
dataProcessed: string[]; // Lista esplicita di dati raccolti
retentionDays: number;
signature: string; // Hash consenso per audit
}
class GDPRConsentManager {
private static readonly CONSENT_VERSION = '2025-v2';
private static readonly DEFAULT_RETENTION_DAYS = 30;
async requestConsent(
studentId: string,
examId: string
): Promise<ConsentRecord | null> {
const consentData = {
dataProcessed: [
'Rilevamento presenza volto (si/no)',
'Direzione sguardo (deviazione significativa si/no)',
'Numero di volti rilevati',
'Cambio scheda/finestra (si/no)',
'Anomalie audio (si/no)'
],
notProcessed: [
'Video continuo della sessione',
'Riconoscimento facciale o identificazione biometrica',
'Screenshot dello schermo',
'Contenuto audio'
],
retentionDays: this.constructor.DEFAULT_RETENTION_DAYS,
canWithdraw: true,
consequenceOfWithdrawal: 'Impossibilita di sostenere l\'esame in modalità remota'
};
// Mostra UI consenso e attende risposta (implementazione UI omessa)
const accepted = await this.showConsentDialog(consentData);
if (!accepted) return null;
const studentHash = await this.hashStudentId(studentId);
const consentRecord: ConsentRecord = {
studentHash,
examId,
timestamp: Date.now(),
consentVersion: GDPRConsentManager.CONSENT_VERSION,
dataProcessed: consentData.dataProcessed,
retentionDays: consentData.retentionDays,
signature: await this.computeConsentSignature(studentHash, examId)
};
// Persistenza del consenso per audit trail
await this.persistConsent(consentRecord);
return consentRecord;
}
private async hashStudentId(studentId: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(studentId + process.env.PROCTORING_SALT);
const hash = await crypto.subtle.digest('SHA-256', data);
return Array.from(new Uint8Array(hash))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
async scheduleDataDeletion(consentRecord: ConsentRecord): Promise<void> {
const deleteAt = new Date(
consentRecord.timestamp + consentRecord.retentionDays * 86400 * 1000
);
// Schedule deletion job (implementazione dipende dall'infrastruttura)
await scheduleJob({
type: 'delete_proctoring_data',
sessionId: consentRecord.studentHash + '_' + consentRecord.examId,
executeAt: deleteAt.toISOString()
});
}
}
警告: AI 監督システムにおけるバイアス
MIT (2019) 以降の研究では、認識システムが フェイシャルでは、肌の色が濃い人の場合、エラー率が大幅に高くなります。 メガネをかけた女性や個人。監督システムはデータセットでテストする必要がある 人口統計的に代表的であり、モニタリングに資本指標を含める 継続的な。特定のグループに対する系統的に高い誤検知は、 EU AI法に基づく間接差別。
5. ヒューマンレビューインターフェース
高リスクシステムに関する EU AI 法の基本原則は、次のような決定を下すことです。 重大な影響を与える個人には、人間による大幅な監督が必要です。 レビュー インターフェイスはシステムの重要なコンポーネントです。
// API endpoint per il pannello di review dei docenti
// Solo chi ha role 'exam_reviewer' accede ai report
import express from 'express';
import { authenticate, requireRole } from './auth';
const router = express.Router();
// GET /api/proctoring/review/:examId
router.get('/review/:examId',
authenticate,
requireRole('exam_reviewer'),
async (req, res) => {
const { examId } = req.params;
// Solo sessioni con flag HIGH o che richiedono review
const flaggedSessions = await db.query(`
SELECT
session_id,
overall_score,
risk_level,
event_count,
gaze_score,
face_score,
behavior_score,
created_at,
review_deadline
FROM proctoring_sessions
WHERE exam_id = $1
AND (risk_level = 'high' OR requires_human_review = true)
AND reviewed_by IS NULL
ORDER BY overall_score DESC
`, [examId]);
res.json({
examId,
pendingReviews: flaggedSessions.rows.length,
sessions: flaggedSessions.rows,
reviewGuidelines: {
highRisk: 'Score > 0.6: valuta attentamente tutti gli eventi',
mediumRisk: 'Score 0.3-0.6: verifica pattern di tab switching e multi-face',
note: 'Lo score AI e indicativo. La decisione finale spetta al revisore umano.'
}
});
}
);
// POST /api/proctoring/review/:sessionId/decision
router.post('/review/:sessionId/decision',
authenticate,
requireRole('exam_reviewer'),
async (req, res) => {
const { sessionId } = req.params;
const { decision, notes, reviewerId } = req.body;
// Validation
if (!['pass', 'fail', 'inconclusive'].includes(decision)) {
return res.status(400).json({ error: 'Invalid decision value' });
}
await db.query(`
UPDATE proctoring_sessions
SET
final_decision = $1,
reviewer_notes = $2,
reviewed_by = $3,
reviewed_at = NOW()
WHERE session_id = $4
`, [decision, notes, reviewerId, sessionId]);
// Audit log obbligatorio per EU AI Act
await auditLog.record({
action: 'proctoring_decision',
sessionId,
decision,
reviewerId,
timestamp: new Date().toISOString()
});
res.json({ success: true, decision });
}
);
結論
責任ある AI 監督システムは矛盾していません。それは可能です。 学生の尊厳を尊重した学術的誠実のための効果的なツールを構築する 現在の法律を遵守します。鍵となるのは 設計によるプライバシー: データをローカルで処理し、異常なイベントに関するメタデータのみを収集し、 人間が最終決定権を持っている限り。
EU AI 法が完全に施行されているため、遵守は任意ではありません。その機関は、 監督システムを実装するには、DPIA を完了し、システムを文書化する必要があります。 監査人を訓練し、決定に異議を唱える能力を確保します。
EdTech シリーズの関連記事
- 記事 00: スケーラブルな LMS アーキテクチャ: マルチテナント パターン
- 記事 01: 適応学習アルゴリズム
- 記事 06: xAPI と Kafka を使用した分析の学習







