| 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; |
| } |
| |
| |
| onStateChange(callback: (state: TrainerScanState) => void) { |
| this.stateCallbacks.push(callback); |
| } |
| |
| |
| private async notifyStateChange(state: Partial<TrainerScanState>) { |
| const currentState = await this.getCurrentState(); |
| const fullState = { ...currentState, ...state }; |
| this.stateCallbacks.forEach(callback => callback(fullState)); |
| } |
| |
| |
| private async getCurrentState(): Promise<TrainerScanState> { |
| 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' |
| }; |
| } |
| } |
| |
| |
| async initializeFromFile(): Promise<void> { |
| 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'); |
| } |
| } |
| |
| |
| async startScanning(): Promise<void> { |
| if (this.isScanning) { |
| throw new Error('Scanning is already in progress'); |
| } |
| |
| |
| const stats = await getScanningStats(); |
| if (stats.total === 0) { |
| await this.initializeFromFile(); |
| } |
| |
| |
| const currentProcessing = await getCurrentProcessingImage(); |
| if (currentProcessing) { |
| |
| 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 }); |
| |
| |
| 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%' |
| }); |
| } |
| } |
| |
| |
| stopScanning(): void { |
| this.shouldStop = true; |
| } |
| |
| |
| private async processingLoop(): Promise<void> { |
| while (!this.shouldStop) { |
| const nextImage = await getNextPendingImage(); |
| |
| if (!nextImage) { |
| |
| 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})`); |
| |
| 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); |
| |
| |
| console.log(`🔄 Continuing to next image despite failure...`); |
| } |
| |
| |
| await this.notifyStateChange({}); |
| } |
| } |
| |
| |
| |
| private async processImage(imagePath: string, remoteUrl: string): Promise<void> { |
| throw new Error('TrainerScanService is deprecated - use PicletGenerator directly'); |
| } |
| |
| |
| private async fetchRemoteImage(remoteUrl: string, originalPath: string): Promise<File> { |
| 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 }); |
| } |
| |
| } |