import sharp from 'sharp'; export interface SmartThumbnailResult { 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 SmartThumbnailService { /** * Download image and get metadata */ private async downloadImage(imageUrl: string): Promise<{ sharpInstance: sharp.Sharp; 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 sharpInstance = sharp(buffer); const metadata = await sharpInstance.metadata(); return { sharpInstance, 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 using Sharp's built-in stats to find areas of interest */ private async analyzeImageContent(sharpInstance: sharp.Sharp, width: number, height: number): Promise<{ x: number; y: number; confidence: number }[]> { try { const focalPoints: { x: number; y: number; confidence: number }[] = []; // Analyze different regions of the image const gridSize = 3; // 3x3 grid const regionWidth = Math.floor(width / gridSize); const regionHeight = Math.floor(height / gridSize); for (let row = 0; row < gridSize; row++) { for (let col = 0; col < gridSize; col++) { const x = col * regionWidth; const y = row * regionHeight; try { // Extract region and analyze const regionStats = await sharpInstance .clone() .extract({ left: x, top: y, width: regionWidth, height: regionHeight }) .stats(); // Calculate region score based on contrast and brightness distribution let regionScore = 0; if (regionStats.channels && regionStats.channels.length > 0) { const channel = regionStats.channels[0]; // Use first channel (or grayscale) // Good regions typically have: // 1. Moderate mean brightness (not too dark/bright) // 2. Good standard deviation (contrast) // 3. Balanced distribution const meanBrightness = channel.mean; const contrast = channel.std; // Score brightness (prefer middle range 80-180) const brightnessScore = meanBrightness > 80 && meanBrightness < 180 ? 1 - Math.abs(meanBrightness - 130) / 130 : 0.2; // Score contrast (higher is better, up to a point) const contrastScore = Math.min(contrast / 50, 1); // Combine scores regionScore = (brightnessScore * 0.4 + contrastScore * 0.6); // Boost center regions slightly (faces often in center) if (row === 1 && col === 1) { regionScore *= 1.2; } // Boost upper-center region (faces often in upper portion) if (row === 0 && col === 1) { regionScore *= 1.1; } } if (regionScore > 0.3) { focalPoints.push({ x: x + regionWidth / 2, // Center of region y: y + regionHeight / 2, confidence: regionScore }); } } catch (regionError) { // Skip this region if analysis fails console.warn(`Failed to analyze region ${row},${col}:`, regionError); } } } // Sort by confidence return focalPoints.sort((a, b) => b.confidence - a.confidence); } catch (error) { console.error('Error analyzing image content:', 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) { return this.calculateCenterCrop(imageWidth, imageHeight, targetAspect); } const primaryFocalPoint = focalPoints[0]; let cropWidth: number, cropHeight: number, cropX: number, cropY: number; if (imageWidth / imageHeight > targetAspect) { // Image is wider - crop horizontally cropWidth = Math.floor(imageHeight * targetAspect); cropHeight = imageHeight; // Center crop around focal point, but keep within bounds cropX = Math.max(0, Math.min( Math.floor(primaryFocalPoint.x - cropWidth / 2), imageWidth - cropWidth )); cropY = 0; } else { // Image is taller - crop vertically cropWidth = imageWidth; cropHeight = Math.floor(imageWidth / targetAspect); // Center crop around focal point vertically cropY = Math.max(0, Math.min( Math.floor(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 to find optimal crop position */ public async processThumbnail(thumbnailUrl: string): Promise { try { console.log(`🔍 Analyzing thumbnail: ${thumbnailUrl}`); const { sharpInstance, width, height } = await this.downloadImage(thumbnailUrl); if (width === 0 || height === 0) { throw new Error('Invalid image dimensions'); } // Analyze image content for focal points const focalPoints = await this.analyzeImageContent(sharpInstance, 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 high-confidence focal points as potential faces if (confidence > 0.7) { 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 { 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 */ 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'; } 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 smartThumbnailService = new SmartThumbnailService();