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