import { EventEmitter } from 'events'; // TensorFlow.js imports - will be loaded dynamically let tf: typeof import('@tensorflow/tfjs') | null = null; let cocoSsd: typeof import('@tensorflow-models/coco-ssd') | null = null; interface Detection { class: string; score: number; bbox: [number, number, number, number]; } export class PresenceDetector extends EventEmitter { private model: Awaited> | null = null; private video: HTMLVideoElement | null = null; private canvas: HTMLCanvasElement | null = null; private context: CanvasRenderingContext2D | null = null; private stream: MediaStream | null = null; private detectionInterval: NodeJS.Timeout | null = null; private isRunning: boolean = false; private confidenceThreshold: number = 0.6; private personDetected: boolean = false; private noPersonCount: number = 0; private readonly NO_PERSON_THRESHOLD = 5; // Frames without person before clearing constructor(confidenceThreshold: number = 0.6) { super(); this.confidenceThreshold = confidenceThreshold; } async start(): Promise { if (this.isRunning) return; try { // Dynamically import TensorFlow.js tf = await import('@tensorflow/tfjs'); cocoSsd = await import('@tensorflow-models/coco-ssd'); // Set backend await tf.setBackend('webgl'); await tf.ready(); // Load COCO-SSD model console.log('Loading COCO-SSD model...'); this.model = await cocoSsd.load({ base: 'lite_mobilenet_v2', // Faster, lighter model }); console.log('Model loaded'); // Set up video capture await this.setupCamera(); // Start detection loop this.isRunning = true; this.startDetectionLoop(); } catch (error) { console.error('Failed to start presence detection:', error); throw error; } } private async setupCamera(): Promise { try { this.stream = await navigator.mediaDevices.getUserMedia({ video: { width: { ideal: 640 }, height: { ideal: 480 }, facingMode: 'user', }, audio: false, }); // Create video element this.video = document.createElement('video'); this.video.srcObject = this.stream; this.video.autoplay = true; this.video.playsInline = true; // Create canvas for processing this.canvas = document.createElement('canvas'); this.canvas.width = 640; this.canvas.height = 480; this.context = this.canvas.getContext('2d'); // Wait for video to be ready await new Promise((resolve) => { if (this.video) { this.video.onloadedmetadata = () => { this.video?.play(); resolve(); }; } }); } catch (error) { console.error('Failed to setup camera:', error); throw error; } } private startDetectionLoop(): void { // Run detection every 500ms this.detectionInterval = setInterval(async () => { await this.detectPerson(); }, 500); } private async detectPerson(): Promise { if (!this.model || !this.video || !this.context || !this.canvas) return; try { // Draw current frame to canvas this.context.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height); // Run detection const predictions: Detection[] = await this.model.detect(this.canvas); // Check for person with sufficient confidence const personDetection = predictions.find( (p) => p.class === 'person' && p.score >= this.confidenceThreshold ); if (personDetection) { this.noPersonCount = 0; if (!this.personDetected) { this.personDetected = true; this.emit('personDetected', personDetection); } } else { this.noPersonCount++; if (this.personDetected && this.noPersonCount >= this.NO_PERSON_THRESHOLD) { this.personDetected = false; this.emit('noPersonDetected'); } } } catch (error) { console.error('Detection error:', error); } } async stop(): Promise { this.isRunning = false; if (this.detectionInterval) { clearInterval(this.detectionInterval); this.detectionInterval = null; } if (this.stream) { this.stream.getTracks().forEach((track) => track.stop()); this.stream = null; } if (this.video) { this.video.srcObject = null; this.video = null; } this.canvas = null; this.context = null; this.model = null; } isDetecting(): boolean { return this.isRunning; } hasPersonPresent(): boolean { return this.personDetected; } }