Files
imperial-command-center/electron/services/PresenceDetector.ts

171 lines
4.7 KiB
TypeScript

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<ReturnType<typeof import('@tensorflow-models/coco-ssd').load>> | 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<void> {
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<void> {
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<void>((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<void> {
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<void> {
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;
}
}