import type { GradioClient } from '$lib/types'; import { initializeTrainerScanProgress, getNextPendingImage, markImageProcessingStarted, markImageProcessingCompleted, markImageProcessingFailed, getScanningStats, getCurrentProcessingImage } from '$lib/db/trainerScanning'; export interface TrainerScanState { isScanning: boolean; currentImage: string | null; currentTrainer: string | null; progress: { total: number; completed: number; failed: number; pending: number; }; error: string | null; } export class TrainerScanService { private joyCaptionClient: GradioClient; private zephyrClient: GradioClient; private fluxClient: GradioClient; private isScanning = false; private shouldStop = false; private stateCallbacks: ((state: TrainerScanState) => void)[] = []; constructor( joyCaptionClient: GradioClient, zephyrClient: GradioClient, fluxClient: GradioClient ) { this.joyCaptionClient = joyCaptionClient; this.zephyrClient = zephyrClient; this.fluxClient = fluxClient; } // Subscribe to state changes onStateChange(callback: (state: TrainerScanState) => void) { this.stateCallbacks.push(callback); } // Notify all subscribers of state changes private async notifyStateChange(state: Partial) { const currentState = await this.getCurrentState(); const fullState = { ...currentState, ...state }; this.stateCallbacks.forEach(callback => callback(fullState)); } // Get current scanning state private async getCurrentState(): Promise { try { const stats = await getScanningStats(); return { isScanning: this.isScanning, currentImage: null, currentTrainer: null, progress: { total: stats?.total || 0, completed: stats?.completed || 0, failed: stats?.failed || 0, pending: stats?.pending || 0 }, error: null }; } catch (error) { console.error('Failed to get current state:', error); return { isScanning: this.isScanning, currentImage: null, currentTrainer: null, progress: { total: 0, completed: 0, failed: 0, pending: 0 }, error: 'Failed to load progress stats' }; } } // Initialize scanning database with image paths from file async initializeFromFile(): Promise { try { const response = await fetch('/trainer_image_paths.txt'); if (!response.ok) { throw new Error(`Failed to fetch trainer_image_paths.txt: ${response.statusText}`); } const content = await response.text(); if (!content) { throw new Error('trainer_image_paths.txt is empty'); } const imagePaths = content.trim().split('\n') .map(path => typeof path === 'string' ? path.trim() : '') .filter(path => path.length > 0); console.log(`Loaded ${imagePaths.length} trainer image paths`); if (imagePaths.length === 0) { throw new Error('No valid image paths found in trainer_image_paths.txt'); } await initializeTrainerScanProgress(imagePaths); await this.notifyStateChange({}); } catch (error) { console.error('Failed to initialize trainer scan progress:', error); throw new Error('Failed to load trainer image paths'); } } // Start automated scanning async startScanning(): Promise { if (this.isScanning) { throw new Error('Scanning is already in progress'); } // Initialize database if needed const stats = await getScanningStats(); if (stats.total === 0) { await this.initializeFromFile(); } // Check for interrupted processing const currentProcessing = await getCurrentProcessingImage(); if (currentProcessing) { // Reset interrupted processing back to pending await markImageProcessingFailed(currentProcessing.imagePath, 'Process interrupted'); } this.isScanning = true; this.shouldStop = false; await this.notifyStateChange({ isScanning: true, error: null }); try { await this.processingLoop(); } catch (error) { console.error('Scanning error:', error); await this.notifyStateChange({ error: error instanceof Error ? error.message : 'Unknown error' }); } finally { this.isScanning = false; await this.notifyStateChange({ isScanning: false, currentImage: null, currentTrainer: null }); // Log final summary const finalStats = await getScanningStats(); console.log(`🏁 Scanning session complete:`, { total: finalStats.total, completed: finalStats.completed, failed: finalStats.failed, pending: finalStats.pending, successRate: finalStats.total > 0 ? Math.round((finalStats.completed / finalStats.total) * 100) + '%' : '0%' }); } } // Stop scanning stopScanning(): void { this.shouldStop = true; } // Main processing loop private async processingLoop(): Promise { while (!this.shouldStop) { const nextImage = await getNextPendingImage(); if (!nextImage) { // No more pending images break; } await this.notifyStateChange({ currentImage: nextImage.imagePath, currentTrainer: typeof nextImage.trainerName === 'string' ? nextImage.trainerName : null }); try { await this.processImage(nextImage.imagePath, nextImage.remoteUrl); console.log(`✅ Successfully processed: ${nextImage.imagePath} (${nextImage.trainerName})`); // Add small delay between images to prevent overwhelming the system await new Promise(resolve => setTimeout(resolve, 1000)); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; console.error(`❌ Failed to process ${nextImage.imagePath} (${nextImage.trainerName}):`, { imagePath: nextImage.imagePath, trainerName: nextImage.trainerName, remoteUrl: nextImage.remoteUrl, error: errorMessage, fullError: error }); await markImageProcessingFailed(nextImage.imagePath, errorMessage); // Continue to next image - don't let individual failures stop the whole process console.log(`🔄 Continuing to next image despite failure...`); } // Update progress await this.notifyStateChange({}); } } // DEPRECATED: This service is no longer used // The AutoTrainerScanner now directly uses PicletGenerator component private async processImage(imagePath: string, remoteUrl: string): Promise { throw new Error('TrainerScanService is deprecated - use PicletGenerator directly'); } // Fetch remote image and convert to File private async fetchRemoteImage(remoteUrl: string, originalPath: string): Promise { const response = await fetch(remoteUrl); if (!response.ok) { throw new Error(`Failed to fetch ${remoteUrl}: ${response.statusText}`); } const blob = await response.blob(); const fileName = originalPath.split('/').pop() || 'trainer_image.jpg'; return new File([blob], fileName, { type: blob.type }); } }