videofolxtv/server/face-detection-service.ts
sebastjanartic d321b4f384 Add face detection and thumbnail centering for videos
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
2025-08-29 07:34:08 +00:00

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();