File size: 13,483 Bytes
5435413
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5056db8
 
 
 
5435413
5056db8
 
 
 
 
 
 
 
 
 
 
 
5435413
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2aa7dab
 
 
 
 
 
 
 
 
 
5435413
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2aa7dab
5435413
 
 
 
2aa7dab
5435413
 
 
2aa7dab
 
 
 
 
 
 
 
 
 
 
 
 
5435413
 
 
 
 
 
 
 
 
 
 
 
2aa7dab
5435413
 
 
2aa7dab
5435413
 
 
2aa7dab
5435413
 
 
2aa7dab
5435413
 
 
2aa7dab
5435413
 
 
2aa7dab
5435413
 
 
2aa7dab
5435413
 
 
2aa7dab
5435413
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2aa7dab
 
 
 
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
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
import type { GradioClient } from '$lib/types';
import { 
  initializeTrainerScanProgress, 
  getNextPendingImage, 
  markImageProcessingStarted,
  markImageProcessingCompleted,
  markImageProcessingFailed,
  getScanningStats,
  getCurrentProcessingImage
} from '$lib/db/trainerScanning';
import { savePicletInstance, generatedDataToPicletInstance } from '$lib/db/piclets';
import { extractPicletMetadata } from './picletMetadata';
import { removeBackground } from '$lib/utils/professionalImageProcessing';
import type { PicletStats } from '$lib/types';

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 notifyStateChange(state: Partial<TrainerScanState>) {
    const fullState = { ...this.getCurrentState(), ...state };
    this.stateCallbacks.forEach(callback => callback(fullState));
  }
  
  // Get current scanning state
  private async getCurrentState(): Promise<TrainerScanState> {
    const stats = await getScanningStats();
    return {
      isScanning: this.isScanning,
      currentImage: null,
      currentTrainer: null,
      progress: {
        total: stats.total,
        completed: stats.completed,
        failed: stats.failed,
        pending: stats.pending
      },
      error: null
    };
  }
  
  // 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);
      this.notifyStateChange(await this.getCurrentState());
    } 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;
    this.notifyStateChange({ isScanning: true, error: null });
    
    try {
      await this.processingLoop();
    } catch (error) {
      console.error('Scanning error:', error);
      this.notifyStateChange({ error: error instanceof Error ? error.message : 'Unknown error' });
    } finally {
      this.isScanning = false;
      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;
      }
      
      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
      this.notifyStateChange(await this.getCurrentState());
    }
  }
  
  // Process a single remote image
  private async processImage(imagePath: string, remoteUrl: string): Promise<void> {
    await markImageProcessingStarted(imagePath);
    
    try {
      console.log(`πŸ”„ Processing ${imagePath}: Fetching remote image...`);
      // Fetch remote image
      const imageFile = await this.fetchRemoteImage(remoteUrl, imagePath);
      
      console.log(`πŸ”„ Processing ${imagePath}: Captioning image...`);
      // Caption the image
      const imageCaption = await this.captionImage(imageFile);
      
      console.log(`πŸ”„ Processing ${imagePath}: Generating concept...`);
      // Generate monster concept
      const picletConcept = await this.generatePicletConcept(imageCaption);
      
      console.log(`πŸ”„ Processing ${imagePath}: Generating stats...`);
      // Generate stats
      const picletStats = await this.generatePicletStats(picletConcept);
      
      console.log(`πŸ”„ Processing ${imagePath}: Generating image prompt...`);
      // Generate image prompt
      const imagePrompt = await this.generateImagePrompt(picletConcept);
      
      console.log(`πŸ”„ Processing ${imagePath}: Generating monster image...`);
      // Generate monster image
      const picletImageUrl = await this.generateMonsterImage(imagePrompt);
      
      console.log(`πŸ”„ Processing ${imagePath}: Processing generated image...`);
      // Process generated image (remove background)
      const imageData = await this.processGeneratedImage(picletImageUrl);
      
      console.log(`πŸ”„ Processing ${imagePath}: Creating piclet instance...`);
      // Create piclet instance
      const generatedData = {
        name: this.extractNameFromConcept(picletConcept),
        imageUrl: picletImageUrl,
        imageData,
        imageCaption,
        concept: picletConcept,
        imagePrompt,
        stats: picletStats,
        createdAt: new Date()
      };
      
      const picletInstance = await generatedDataToPicletInstance(generatedData, 5);
      const savedId = await savePicletInstance(picletInstance);
      
      await markImageProcessingCompleted(imagePath, savedId);
      
    } catch (error) {
      // Add context about which step failed
      const enhancedError = new Error(`Failed during processing of ${imagePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
      enhancedError.cause = error;
      throw enhancedError;
    }
  }
  
  // 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 });
  }
  
  // Caption image using Joy Caption
  private async captionImage(imageFile: File): Promise<string> {
    const result = await this.joyCaptionClient.predict("/caption", [imageFile, "descriptive", "any", false]);
    const captionResult = result.data[0] as string;
    
    if (!captionResult || captionResult.trim() === '') {
      throw new Error('Failed to generate image caption');
    }
    
    return captionResult.trim();
  }
  
  // Generate piclet concept using Zephyr
  private async generatePicletConcept(imageCaption: string): Promise<string> {
    const prompt = `Based on this image description, create a unique creature concept for a Pokemon-style monster collection game called "Pictuary":

"${imageCaption}"

Create a creative, original monster concept that:
1. Is inspired by elements from the image but is clearly a fantastical creature
2. Has a unique name and personality
3. Includes special abilities related to its appearance
4. Is suitable for a family-friendly game

Write a detailed monster concept (2-3 paragraphs).`;

    const result = await this.zephyrClient.predict("/chat", [
      [["user", prompt]],
      512,  // max tokens
      0.7,  // temperature
      0.9,  // top_p
    ]);
    
    const conceptResult = result.data[0][1][1] as string;
    
    if (!conceptResult || conceptResult.trim() === '') {
      throw new Error('Failed to generate piclet concept');
    }
    
    return conceptResult.trim();
  }
  
  // Generate piclet stats
  private async generatePicletStats(concept: string): Promise<PicletStats> {
    return await extractPicletMetadata(concept, this.zephyrClient);
  }
  
  // Generate image prompt for monster creation
  private async generateImagePrompt(concept: string): Promise<string> {
    const prompt = `Extract ONLY the visual appearance from this monster concept and describe it in one concise sentence:
"${concept}"

Focus on: colors, body shape, eyes, limbs, mouth, and key visual features. Omit backstory, abilities, and non-visual details.`;

    const result = await this.zephyrClient.predict("/chat", [
      [["user", prompt]],
      256,  // max tokens
      0.5,  // temperature  
      0.9,  // top_p
    ]);
    
    const promptResult = result.data[0][1][1] as string;
    
    if (!promptResult || promptResult.trim() === '') {
      throw new Error('Failed to generate image prompt');
    }
    
    return promptResult.trim();
  }
  
  // Generate monster image using Flux
  private async generateMonsterImage(imagePrompt: string): Promise<string> {
    const fullPrompt = `${imagePrompt}, digital art, creature design, fantasy monster, clean background, professional illustration`;
    
    const result = await this.fluxClient.predict("/infer", [
      fullPrompt,
      "",  // negative prompt
      832, // width
      1216, // height  
      1,   // num inference steps
      3.0, // guidance scale
      0,   // seed
    ]);
    
    const imageUrl = result.data[0] as string;
    
    if (!imageUrl) {
      throw new Error('Failed to generate monster image');
    }
    
    return imageUrl;
  }
  
  // Process generated image (remove background)
  private async processGeneratedImage(imageUrl: string): Promise<string> {
    try {
      return await removeBackground(imageUrl);
    } catch (error) {
      console.warn('Background removal failed, using original image:', error);
      return imageUrl;
    }
  }
  
  // Extract name from concept text
  private extractNameFromConcept(concept: string): string {
    // Try to find a name in common patterns
    const patterns = [
      /(?:called|named)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)/,
      /^([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)/,
      /this\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)/,
    ];
    
    for (const pattern of patterns) {
      const match = concept.match(pattern);
      if (match && match[1]) {
        return match[1].trim();
      }
    }
    
    // Fallback to generating a random trainer-inspired name
    const trainerNames = ['Snap', 'Blaze', 'Nimbus', 'Breaker', 'Trinket'];
    const randomName = trainerNames[Math.floor(Math.random() * trainerNames.length)];
    return `Trainer${randomName}`;
  }
}