This commit introduces face detection capabilities to the video platform, enabling automatic identification of faces in video thumbnails. It integrates face-api.js and sharp for image analysis, allowing for face-centered thumbnail crops and dynamic object-positioning. New API endpoints are added to process thumbnails individually and in batches. The database schema is updated to store face detection data, and the storage layer is modified to support these updates and cache face data. The project's dependencies are also updated to include necessary libraries for these new features. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 2eb1084e-b728-4449-9231-f1665924c8d5 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/2eb1084e-b728-4449-9231-f1665924c8d5/xF0EUqR
145 lines
4.4 KiB
TypeScript
145 lines
4.4 KiB
TypeScript
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<FaceDetectionResult> {
|
|
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(); |