import sharp from 'sharp'; export interface SimpleFaceDetectionResult { 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; }; processing_strategy: 'face_centered' | 'center_crop' | 'smart_crop'; error?: string; } export class SimpleFaceDetectionService { /** * Download image and get basic info */ private async downloadImage(imageUrl: string): Promise<{ buffer: Buffer; width: number; height: number }> { try { const response = await fetch(imageUrl); if (!response.ok) { throw new Error(`Failed to download image: ${response.statusText}`); } const buffer = Buffer.from(await response.arrayBuffer()); const metadata = await sharp(buffer).metadata(); return { buffer, width: metadata.width || 0, height: metadata.height || 0 }; } catch (error) { throw new Error(`Image download failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Analyze image to find interesting areas (likely faces or main subjects) * Uses edge detection and high contrast areas to identify focal points */ private async analyzeImageFocalPoints(imageBuffer: Buffer, width: number, height: number): Promise<{ x: number; y: number; confidence: number }[]> { try { // Resize image for faster processing const resizedBuffer = await sharp(imageBuffer) .resize(Math.min(400, width), Math.min(400, height), { fit: 'inside' }) .greyscale() .raw() .toBuffer({ resolveWithObject: true }); const { data, info } = resizedBuffer; const { width: resizedWidth, height: resizedHeight } = info; // Simple edge detection to find areas of interest const focalPoints: { x: number; y: number; confidence: number }[] = []; const blockSize = 20; // Analyze in 20x20 pixel blocks for (let y = 0; y < resizedHeight - blockSize; y += blockSize) { for (let x = 0; x < resizedWidth - blockSize; x += blockSize) { let edgeStrength = 0; let averageBrightness = 0; let pixelCount = 0; // Analyze block for edge strength and brightness for (let dy = 0; dy < blockSize; dy++) { for (let dx = 0; dx < blockSize; dx++) { const currentY = y + dy; const currentX = x + dx; if (currentY < resizedHeight && currentX < resizedWidth) { const idx = currentY * resizedWidth + currentX; const currentPixel = data[idx]; averageBrightness += currentPixel; pixelCount++; // Simple edge detection (horizontal and vertical gradients) if (currentX < resizedWidth - 1 && currentY < resizedHeight - 1) { const rightPixel = data[currentY * resizedWidth + (currentX + 1)]; const downPixel = data[(currentY + 1) * resizedWidth + currentX]; const horizontalGradient = Math.abs(currentPixel - rightPixel); const verticalGradient = Math.abs(currentPixel - downPixel); edgeStrength += horizontalGradient + verticalGradient; } } } } averageBrightness /= pixelCount; // Calculate confidence based on edge strength and optimal brightness // Faces typically have moderate brightness (not too dark, not too bright) const brightnessScore = 1 - Math.abs(averageBrightness - 128) / 128; const edgeScore = Math.min(edgeStrength / (blockSize * blockSize * 100), 1); const confidence = (brightnessScore * 0.3 + edgeScore * 0.7); if (confidence > 0.3) { // Only consider blocks with reasonable confidence // Convert back to original image coordinates const originalX = (x / resizedWidth) * width; const originalY = (y / resizedHeight) * height; focalPoints.push({ x: originalX, y: originalY, confidence }); } } } // Sort by confidence and return top candidates return focalPoints .sort((a, b) => b.confidence - a.confidence) .slice(0, 3); // Keep top 3 focal points } catch (error) { console.error('Error analyzing image focal points:', error); return []; } } /** * Calculate smart crop based on focal points */ private calculateSmartCrop( imageWidth: number, imageHeight: number, focalPoints: { x: number; y: number; confidence: number }[], targetAspect: number = 9/16 ): { x: number; y: number; width: number; height: number } { if (focalPoints.length === 0) { // No focal points found, use center crop return this.calculateCenterCrop(imageWidth, imageHeight, targetAspect); } // Use the highest confidence focal point const primaryFocalPoint = focalPoints[0]; let cropWidth: number, cropHeight: number, cropX: number, cropY: number; if (imageWidth / imageHeight > targetAspect) { // Image is wider than target aspect ratio cropWidth = Math.floor(imageHeight * targetAspect); cropHeight = imageHeight; // Center crop horizontally around focal point cropX = Math.max(0, Math.min( primaryFocalPoint.x - cropWidth / 2, imageWidth - cropWidth )); cropY = 0; } else { // Image is taller than target aspect ratio cropWidth = imageWidth; cropHeight = Math.floor(imageWidth / targetAspect); // Center crop vertically around focal point cropY = Math.max(0, Math.min( primaryFocalPoint.y - cropHeight / 2, imageHeight - cropHeight )); cropX = 0; } return { x: cropX, y: cropY, width: cropWidth, height: cropHeight }; } /** * Calculate standard center crop */ private calculateCenterCrop( imageWidth: number, imageHeight: number, targetAspect: number = 9/16 ): { x: number; y: number; width: number; height: number } { let cropWidth: number, cropHeight: number, cropX: number, cropY: number; if (imageWidth / imageHeight > targetAspect) { cropWidth = Math.floor(imageHeight * targetAspect); cropHeight = imageHeight; cropX = Math.floor((imageWidth - cropWidth) / 2); cropY = 0; } else { cropWidth = imageWidth; cropHeight = Math.floor(imageWidth / targetAspect); cropX = 0; cropY = Math.floor((imageHeight - cropHeight) / 2); } return { x: cropX, y: cropY, width: cropWidth, height: cropHeight }; } /** * Process thumbnail image to find optimal crop position */ public async processThumbnail(thumbnailUrl: string): Promise { try { console.log(`🔍 Analyzing thumbnail: ${thumbnailUrl}`); // Download and analyze the image const { buffer, width, height } = await this.downloadImage(thumbnailUrl); if (width === 0 || height === 0) { throw new Error('Invalid image dimensions'); } // Analyze image for focal points (potential faces/subjects) const focalPoints = await this.analyzeImageFocalPoints(buffer, width, height); console.log(`🎯 Found ${focalPoints.length} focal points`); let cropInfo: { x: number; y: number; width: number; height: number }; let strategy: 'face_centered' | 'center_crop' | 'smart_crop'; let confidence = 0; let facesDetected = 0; if (focalPoints.length > 0) { const primaryFocalPoint = focalPoints[0]; confidence = primaryFocalPoint.confidence; // Consider it a "face" if confidence is high enough if (confidence > 0.6) { facesDetected = 1; strategy = 'face_centered'; } else { strategy = 'smart_crop'; } cropInfo = this.calculateSmartCrop(width, height, focalPoints); console.log(`✅ Smart crop calculated with confidence: ${confidence.toFixed(2)}`); } else { // No focal points found, use center crop cropInfo = this.calculateCenterCrop(width, height); strategy = 'center_crop'; console.log(`📐 Center crop calculated (no focal points detected)`); } return { success: true, faces_detected: facesDetected, primary_face_confidence: confidence, crop_info: cropInfo, original_dimensions: { width, height }, processing_strategy: strategy }; } catch (error) { console.error(`❌ Error processing thumbnail ${thumbnailUrl}:`, error); return { 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: error instanceof Error ? error.message : String(error) }; } } /** * Calculate CSS object-position for optimal display */ public calculateObjectPosition( cropInfo: { x: number; y: number; width: number; height: number }, 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)).toFixed(1)}% ${Math.max(0, Math.min(100, positionY)).toFixed(1)}%`; } /** * Get optimized thumbnail information */ public 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 simpleFaceDetectionService = new SimpleFaceDetectionService();