piclets / src /lib /services /trainerScanService.ts
Fraser's picture
simpler
de7bb17
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<TrainerScanState>) {
const currentState = await this.getCurrentState();
const fullState = { ...currentState, ...state };
this.stateCallbacks.forEach(callback => callback(fullState));
}
// Get current scanning state
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'
};
}
}
// Initialize scanning database with image paths from file
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');
}
}
// Start automated scanning
async startScanning(): Promise<void> {
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<void> {
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<void> {
throw new Error('TrainerScanService is deprecated - use PicletGenerator directly');
}
// Fetch remote image and convert to File
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 });
}
}