214 lines
5.9 KiB
TypeScript
214 lines
5.9 KiB
TypeScript
import type { DetectionConfig, DetectionResult, DetectionMetrics, WorkerMessage, WorkerResponse } from './types';
|
|
import { ModelCache } from './model-cache';
|
|
import { MODEL_VARIANTS } from './model-config';
|
|
|
|
/**
|
|
* Manages the detection worker and handles communication
|
|
*/
|
|
export class DetectionWorkerManager {
|
|
private worker: Worker | null = null;
|
|
private messageId = 0;
|
|
private pendingMessages = new Map<string, { resolve: (value: any) => void; reject: (reason?: any) => void }>();
|
|
private modelCache = new ModelCache();
|
|
private isWorkerReady = false;
|
|
|
|
constructor() {
|
|
this.initializeWorker();
|
|
}
|
|
|
|
private async initializeWorker() {
|
|
try {
|
|
// Create worker from the detection worker file
|
|
this.worker = new Worker(
|
|
new URL('../../workers/detection-worker.ts', import.meta.url),
|
|
{ type: 'module' }
|
|
);
|
|
|
|
this.worker.onmessage = (event: MessageEvent<WorkerResponse>) => {
|
|
this.handleWorkerMessage(event.data);
|
|
};
|
|
|
|
this.worker.onerror = (error) => {
|
|
console.error('Worker error:', error);
|
|
this.isWorkerReady = false;
|
|
};
|
|
|
|
this.isWorkerReady = true;
|
|
console.log('Detection worker initialized');
|
|
await this.sendMessage('INITIALIZE', undefined);
|
|
|
|
} catch (error) {
|
|
console.error('Failed to initialize worker:', error);
|
|
this.isWorkerReady = false;
|
|
}
|
|
}
|
|
|
|
private handleWorkerMessage(message: WorkerResponse) {
|
|
const { type, id } = message;
|
|
|
|
const pending = this.pendingMessages.get(id);
|
|
if (!pending) {
|
|
console.warn('Received response for unknown message ID:', id);
|
|
return;
|
|
}
|
|
|
|
this.pendingMessages.delete(id);
|
|
|
|
if (type === 'ERROR') {
|
|
pending.reject(new Error((message as { error: string }).error));
|
|
} else if (type === 'DETECTION_RESULT') {
|
|
const detectionMessage = message as { result: DetectionResult | null };
|
|
pending.resolve({ result: detectionMessage.result });
|
|
} else if (type === 'INITIALIZED') {
|
|
pending.resolve(undefined);
|
|
} else if (type === 'METRICS_UPDATE') {
|
|
pending.resolve({ metrics: (message as { metrics: Partial<DetectionMetrics> }).metrics });
|
|
} else if (type === 'LOADED_MODEL') {
|
|
pending.resolve(undefined);
|
|
} else if (type === 'CONFIGURED') {
|
|
pending.resolve(undefined);
|
|
} else {
|
|
pending.resolve(message);
|
|
}
|
|
}
|
|
|
|
private async sendMessage<T>(type: WorkerMessage['type'], payload: unknown): Promise<T> {
|
|
if (!this.worker || !this.isWorkerReady) {
|
|
throw new Error('Worker not available');
|
|
}
|
|
|
|
const id = (this.messageId++).toString();
|
|
|
|
return new Promise((resolve, reject) => {
|
|
this.pendingMessages.set(id, { resolve, reject });
|
|
|
|
let message: WorkerMessage & { id: string };
|
|
|
|
if (type === 'INITIALIZE') {
|
|
message = { type, id };
|
|
} else if (type === 'DETECT') {
|
|
message = { type, imageData: payload.imageData, id };
|
|
} else if (type === 'UPDATE_CONFIG' || type === 'CONFIGURE') {
|
|
message = { type, config: payload, id };
|
|
} else if (type === 'LOAD_MODEL') {
|
|
message = { type, variant: payload.variant, modelData: payload.modelData, id };
|
|
} else {
|
|
throw new Error(`Unknown message type for sendMessage: ${type}`);
|
|
}
|
|
|
|
this.worker!.postMessage(message);
|
|
|
|
// Timeout after 30 seconds
|
|
setTimeout(() => {
|
|
if (this.pendingMessages.has(id)) {
|
|
this.pendingMessages.delete(id);
|
|
reject(new Error('Worker message timeout'));
|
|
}
|
|
}, 90000);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Load a model into the worker
|
|
*/
|
|
async loadModel(variant: 'quantized' | 'standard' | 'full', onProgress?: (progress: number) => void): Promise<void> {
|
|
const modelInfo = MODEL_VARIANTS[variant];
|
|
|
|
try {
|
|
// Get model data from cache or download
|
|
const modelData = await this.modelCache.getModel(variant, modelInfo, onProgress);
|
|
|
|
// Send model data to worker
|
|
await this.sendMessage('LOAD_MODEL', {
|
|
variant,
|
|
modelData
|
|
});
|
|
|
|
console.log(`Model ${variant} loaded successfully`);
|
|
|
|
} catch (error) {
|
|
console.error(`Failed to load model ${variant}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Configure the detection settings
|
|
*/
|
|
async configure(config: DetectionConfig): Promise<void> {
|
|
await this.sendMessage('CONFIGURE', config);
|
|
}
|
|
|
|
/**
|
|
* Perform detection on an image
|
|
*/
|
|
async detect(imageData: ImageData): Promise<DetectionResult[]> {
|
|
if (!this.isWorkerReady) {
|
|
throw new Error('Worker not ready');
|
|
}
|
|
|
|
try {
|
|
const results = await this.sendMessage<{ result: DetectionResult | null }>('DETECT', { imageData });
|
|
|
|
// Handle the case where results or results.result is undefined
|
|
if (!results || results.result === undefined || results.result === null) {
|
|
return [];
|
|
}
|
|
|
|
return [results.result];
|
|
} catch (error) {
|
|
console.error('Detection failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get worker metrics
|
|
*/
|
|
async getMetrics(): Promise<object> {
|
|
return await this.sendMessage('getMetrics', {});
|
|
}
|
|
|
|
/**
|
|
* Check if worker is ready
|
|
*/
|
|
isReady(): boolean {
|
|
return this.isWorkerReady;
|
|
}
|
|
|
|
/**
|
|
* Terminate the worker
|
|
*/
|
|
destroy() {
|
|
if (this.worker) {
|
|
this.worker.terminate();
|
|
this.worker = null;
|
|
this.isWorkerReady = false;
|
|
}
|
|
|
|
// Reject all pending messages
|
|
this.pendingMessages.forEach(({ reject }) => {
|
|
reject(new Error('Worker terminated'));
|
|
});
|
|
this.pendingMessages.clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Singleton instance manager
|
|
*/
|
|
let workerManager: DetectionWorkerManager | null = null;
|
|
|
|
export function getDetectionWorkerManager(): DetectionWorkerManager {
|
|
if (!workerManager) {
|
|
workerManager = new DetectionWorkerManager();
|
|
}
|
|
return workerManager;
|
|
}
|
|
|
|
export function destroyDetectionWorkerManager() {
|
|
if (workerManager) {
|
|
workerManager.destroy();
|
|
workerManager = null;
|
|
}
|
|
} |