import { spawn } from 'child_process'; import path from 'path'; export interface FaceDetectionResult { success: boolean; faces_detected: number; primary_face_confidence: number; crop_info: { x: number; y: number; width: number; height: number; }; original_dimensions: { width: number; height: number; }; processed_image?: string; processing_strategy: 'face_centered' | 'center_crop'; error?: string; } export class FaceDetectionService { private pythonScriptPath: string; constructor() { this.pythonScriptPath = path.join(__dirname, 'face-detection.py'); } /** * Process thumbnail URL to detect faces and create centered crop */ async processThumbnail(thumbnailUrl: string): Promise { return new Promise((resolve, reject) => { const python = spawn('python3', [this.pythonScriptPath, thumbnailUrl]); let stdoutData = ''; let stderrData = ''; python.stdout.on('data', (data) => { stdoutData += data.toString(); }); python.stderr.on('data', (data) => { stderrData += data.toString(); }); python.on('close', (code) => { if (code !== 0) { console.error(`Face detection script exited with code ${code}`); console.error('stderr:', stderrData); resolve({ success: false, faces_detected: 0, primary_face_confidence: 0, crop_info: { x: 0, y: 0, width: 0, height: 0 }, original_dimensions: { width: 0, height: 0 }, processing_strategy: 'center_crop', error: `Script exited with code ${code}: ${stderrData}` }); return; } try { const result = JSON.parse(stdoutData); resolve(result); } catch (error) { console.error('Error parsing face detection result:', error); console.error('stdout:', stdoutData); resolve({ success: false, faces_detected: 0, primary_face_confidence: 0, crop_info: { x: 0, y: 0, width: 0, height: 0 }, original_dimensions: { width: 0, height: 0 }, processing_strategy: 'center_crop', error: `Failed to parse result: ${error}` }); } }); python.on('error', (error) => { console.error('Failed to start face detection script:', error); resolve({ success: false, faces_detected: 0, primary_face_confidence: 0, crop_info: { x: 0, y: 0, width: 0, height: 0 }, original_dimensions: { width: 0, height: 0 }, processing_strategy: 'center_crop', error: `Failed to start script: ${error.message}` }); }); }); } /** * Calculate CSS object-position for face-centered display */ calculateObjectPosition(cropInfo: FaceDetectionResult['crop_info'], originalDimensions: { width: number; height: number }): string { if (!cropInfo || !originalDimensions.width || !originalDimensions.height) { return 'center center'; } // Calculate the percentage position of the crop center const cropCenterX = cropInfo.x + cropInfo.width / 2; const cropCenterY = cropInfo.y + cropInfo.height / 2; const positionX = (cropCenterX / originalDimensions.width) * 100; const positionY = (cropCenterY / originalDimensions.height) * 100; return `${Math.max(0, Math.min(100, positionX))}% ${Math.max(0, Math.min(100, positionY))}%`; } /** * Get optimized thumbnail URL with face detection applied */ async getOptimizedThumbnailInfo(thumbnailUrl: string) { try { const result = await this.processThumbnail(thumbnailUrl); return { originalUrl: thumbnailUrl, faceCenterPosition: this.calculateObjectPosition(result.crop_info, result.original_dimensions), facesDetected: result.faces_detected, confidence: result.primary_face_confidence, strategy: result.processing_strategy, processedSuccessfully: result.success }; } catch (error) { console.error('Error optimizing thumbnail:', error); return { originalUrl: thumbnailUrl, faceCenterPosition: 'center center', facesDetected: 0, confidence: 0, strategy: 'center_crop' as const, processedSuccessfully: false }; } } } // Export singleton instance export const faceDetectionService = new FaceDetectionService();