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
This commit is contained in:
sebastjanartic
2025-08-29 07:34:08 +00:00
parent 8a4ea10fc4
commit d321b4f384
14 changed files with 3319 additions and 9 deletions
+50 -4
View File
@@ -640,6 +640,7 @@ export class MemStorage implements IStorage {
class BunnyStorage implements IStorage {
private bunnyService: BunnyService;
private viewsCache: Map<string, number> = new Map();
private faceDataCache: Map<string, { faceCenterPosition?: string; facesDetected?: number; faceConfidence?: number }> = new Map();
constructor() {
this.bunnyService = new BunnyService();
@@ -649,7 +650,12 @@ class BunnyStorage implements IStorage {
console.log(`Fetching videos from cache: limit=${limit}, offset=${offset}, search=${search}`);
const result = videoSyncService.getVideos(limit, offset, search);
console.log(`Returning ${result.videos.length} videos from cache (age: ${result.cacheAge}ms)`);
return result.videos;
// Apply face detection data from cache
return result.videos.map(video => {
const faceData = this.faceDataCache.get(video.id);
return faceData ? { ...video, ...faceData } : video;
});
}
async getVideo(id: string): Promise<Video | undefined> {
@@ -660,7 +666,10 @@ class BunnyStorage implements IStorage {
if (this.viewsCache.has(cachedVideo.id)) {
cachedVideo.views += this.viewsCache.get(cachedVideo.id)!;
}
return cachedVideo;
// Apply face detection data from cache
const faceData = this.faceDataCache.get(cachedVideo.id);
return faceData ? { ...cachedVideo, ...faceData } : cachedVideo;
}
// Fallback to direct API call
@@ -673,7 +682,9 @@ class BunnyStorage implements IStorage {
video.views += this.viewsCache.get(video.id)!;
}
return video;
// Apply face detection data from cache
const faceData = this.faceDataCache.get(video.id);
return faceData ? { ...video, ...faceData } : video;
} catch (error) {
console.error(`Error fetching video ${id} from Bunny:`, error);
return undefined;
@@ -685,7 +696,42 @@ class BunnyStorage implements IStorage {
}
async updateVideo(id: string, updates: UpdateVideo): Promise<Video | undefined> {
throw new Error("Updating videos is not supported with Bunny.net integration");
// Allow updating face detection data and views even with Bunny.net integration
const allowedUpdates = ['faceCenterPosition', 'facesDetected', 'faceConfidence', 'views'];
const filteredUpdates = Object.fromEntries(
Object.entries(updates).filter(([key]) => allowedUpdates.includes(key))
);
if (Object.keys(filteredUpdates).length === 0) {
throw new Error("Only face detection and view updates are supported with Bunny.net integration");
}
try {
// Try to update in database first
try {
await db.update(videos).set(filteredUpdates as UpdateVideo).where(eq(videos.id, id));
} catch (dbError) {
console.warn(`Database update failed for video ${id}, using cache:`, dbError);
}
// Update face detection data in cache (always reliable)
const { views, ...faceData } = filteredUpdates;
if (Object.keys(faceData).length > 0) {
const existing = this.faceDataCache.get(id) || {};
this.faceDataCache.set(id, { ...existing, ...faceData });
}
// Update views cache
if (views !== undefined) {
this.viewsCache.set(id, (this.viewsCache.get(id) || 0) + (views - (this.viewsCache.get(id) || 0)));
}
// Return updated video
return await this.getVideo(id);
} catch (error) {
console.error(`Error updating video ${id}:`, error);
throw error;
}
}
async deleteVideo(id: string): Promise<boolean> {