import { NextRequest, NextResponse } from 'next/server' import { photoService } from '@/lib/photo-service' import { imageClassifier } from '@/lib/image-classifier' import { existsSync } from 'fs' interface BatchClassifyRequest { directory?: string limit?: number offset?: number minConfidence?: number onlyUntagged?: boolean dryRun?: boolean customLabels?: string[] maxResults?: number comprehensive?: boolean // Use both ViT + CLIP for more tags categories?: { general?: string[] time?: string[] weather?: string[] subjects?: string[] locations?: string[] style?: string[] seasons?: string[] } } export async function POST(request: NextRequest) { try { const body: BatchClassifyRequest = await request.json() const { directory, limit = 50, // Process 50 photos at a time to avoid memory issues offset = 0, minConfidence = 0.3, onlyUntagged = false, dryRun = false, customLabels, maxResults, comprehensive = false, categories } = body // Build classifier configuration for this batch const classifierConfig = { minConfidence: comprehensive ? 0.05 : minConfidence, // Lower threshold for comprehensive mode maxResults: comprehensive ? 25 : (maxResults || 10), // More results for comprehensive mode customLabels, categories } // Initialize classifier(s) if (comprehensive) { console.log('[BATCH CLASSIFY] Initializing comprehensive classifiers (ViT + CLIP)...') await Promise.all([ imageClassifier.initialize(), imageClassifier.initializeZeroShot() ]) } else { console.log('[BATCH CLASSIFY] Initializing image classifier...') await imageClassifier.initialize() } // Get photos to process console.log('[BATCH CLASSIFY] Getting photos to classify...') let photos = photoService.getPhotos({ directory, limit, offset, sortBy: 'created_at', sortOrder: 'ASC' }) // Filter to only untagged photos if requested if (onlyUntagged) { photos = photos.filter(photo => { const tags = photoService.getPhotoTags(photo.id) return tags.length === 0 }) } console.log(`[BATCH CLASSIFY] Processing ${photos.length} photos...`) const results = [] let processed = 0 let successful = 0 let failed = 0 let totalTagsAdded = 0 for (const photo of photos) { try { processed++ console.log(`[BATCH CLASSIFY] Processing ${processed}/${photos.length}: ${photo.filename}`) // Try to get cached thumbnail first, fall back to file path let imageSource: string | Buffer = photo.filepath let usingThumbnail = false const thumbnailBlob = photoService.getCachedThumbnail(photo.id, 200) // Use 200px thumbnail if (thumbnailBlob) { console.log(`[BATCH CLASSIFY] Using cached thumbnail: ${photo.filename}`) imageSource = thumbnailBlob usingThumbnail = true } else { // Check if file exists for fallback if (!existsSync(photo.filepath)) { console.warn(`[BATCH CLASSIFY] File not found and no thumbnail: ${photo.filepath}`) failed++ continue } console.log(`[BATCH CLASSIFY] Using original file (no thumbnail): ${photo.filename}`) } // Get existing tags if any const existingTags = photoService.getPhotoTags(photo.id) // Classify the image with fallback handling let classifications try { if (comprehensive) { const comprehensiveResult = await imageClassifier.classifyImageComprehensive(imageSource, classifierConfig) classifications = comprehensiveResult.combinedResults console.log(`[BATCH CLASSIFY] Comprehensive: ${comprehensiveResult.objectClassification.length} object + ${comprehensiveResult.styleClassification.length} style tags`) } else { classifications = await imageClassifier.classifyImage(imageSource, customLabels, classifierConfig) } } catch (error) { if (usingThumbnail && existsSync(photo.filepath)) { console.warn(`[BATCH CLASSIFY] Thumbnail failed for ${photo.filename}, falling back to original file:`, error) try { // Fallback to original file if (comprehensive) { const comprehensiveResult = await imageClassifier.classifyImageComprehensive(photo.filepath, classifierConfig) classifications = comprehensiveResult.combinedResults } else { classifications = await imageClassifier.classifyImage(photo.filepath, customLabels, classifierConfig) } console.log(`[BATCH CLASSIFY] Fallback to original file successful: ${photo.filename}`) } catch (fallbackError) { console.error(`[BATCH CLASSIFY] Both thumbnail and original file failed for ${photo.filename}:`, fallbackError) throw fallbackError } } else { throw error } } // Filter by confidence and exclude existing tags // Note: classifications are already filtered by confidence in classifyImage const existingTagNames = existingTags.map(tag => tag.name.toLowerCase()) const newTags = classifications .filter(result => !existingTagNames.includes(result.label.toLowerCase())) .map(result => ({ name: result.label, confidence: result.score })) if (dryRun) { // Just log what would be added results.push({ photoId: photo.id, filename: photo.filename, existingTags: existingTags.length, newClassifications: newTags, wouldAdd: newTags.length }) } else { // Actually add the tags const addedCount = photoService.addPhotoTags(photo.id, newTags) totalTagsAdded += addedCount results.push({ photoId: photo.id, filename: photo.filename, existingTags: existingTags.length, classificationsFound: classifications.length, newTagsAdded: addedCount, topTags: newTags.slice(0, 5) // Show top 5 tags for logging }) } successful++ // Log progress every 10 photos if (processed % 10 === 0) { console.log(`[BATCH CLASSIFY] Progress: ${processed}/${photos.length} (${successful} successful, ${failed} failed)`) } } catch (error) { console.error(`[BATCH CLASSIFY] Error processing ${photo.filename}:`, error) failed++ results.push({ photoId: photo.id, filename: photo.filename, error: error instanceof Error ? error.message : 'Unknown error' }) } } console.log(`[BATCH CLASSIFY] Completed: ${successful} successful, ${failed} failed, ${totalTagsAdded} total tags added`) return NextResponse.json({ summary: { processed, successful, failed, totalTagsAdded, config: classifierConfig, dryRun, onlyUntagged }, results: dryRun ? results : results.slice(0, 10), // Limit results in response for performance hasMore: photos.length === limit // Indicate if there might be more photos to process }) } catch (error) { console.error('[BATCH CLASSIFY] Error:', error) return NextResponse.json( { error: 'Failed to batch classify images' }, { status: 500 } ) } } // Get classification status export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url) const directory = searchParams.get('directory') || undefined // Get total photos const allPhotos = photoService.getPhotos({ directory }) // Get photos with tags const photosWithTags = allPhotos.filter(photo => { const tags = photoService.getPhotoTags(photo.id) return tags.length > 0 }) // Get most common tags const tagCounts: { [tagName: string]: number } = {} allPhotos.forEach(photo => { const tags = photoService.getPhotoTags(photo.id) tags.forEach(tag => { tagCounts[tag.name] = (tagCounts[tag.name] || 0) + 1 }) }) const topTags = Object.entries(tagCounts) .sort(([,a], [,b]) => b - a) .slice(0, 20) .map(([name, count]) => ({ name, count })) return NextResponse.json({ total: allPhotos.length, tagged: photosWithTags.length, untagged: allPhotos.length - photosWithTags.length, taggedPercentage: Math.round((photosWithTags.length / allPhotos.length) * 100), topTags, classifierReady: imageClassifier.isReady() }) } catch (error) { console.error('[BATCH CLASSIFY STATUS] Error:', error) return NextResponse.json( { error: 'Failed to get classification status' }, { status: 500 } ) } }