File size: 7,408 Bytes
5435413
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4920069
 
 
5435413
 
 
 
 
4920069
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5435413
 
 
 
 
 
5056db8
 
 
 
5435413
5056db8
 
 
 
414e700
 
 
c0fc1ad
414e700
5056db8
 
 
 
5435413
 
4920069
5435413
414e700
5435413
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4920069
5435413
 
 
 
 
4920069
5435413
 
4920069
2aa7dab
 
 
 
 
 
 
 
 
 
5435413
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4920069
5435413
2aa7dab
5435413
 
 
 
2aa7dab
5435413
 
 
2aa7dab
 
 
 
 
 
 
 
 
 
 
 
 
5435413
 
 
4920069
5435413
 
 
de7bb17
 
5435413
de7bb17
5435413
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
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 });
  }
  
}