hoololi commited on
Commit
9b53cde
·
verified ·
1 Parent(s): 28fd826

Upload 5 files

Browse files
Files changed (4) hide show
  1. game_engine.py +121 -75
  2. image_processing_cpu.py +9 -48
  3. image_processing_gpu.py +10 -80
  4. utils.py +16 -72
game_engine.py CHANGED
@@ -2,10 +2,6 @@
2
  # game_engine.py - Calcul OCR v3.0 CLEAN
3
  # ==========================================
4
 
5
- """
6
- Moteur de jeu mathématique avec traitement parallèle et auto-détection OCR
7
- """
8
-
9
  import random
10
  import time
11
  import datetime
@@ -25,10 +21,6 @@ from typing import Dict, Tuple, Optional
25
  ocr_module = None
26
  ocr_info = {"model_name": "Unknown", "device": "Unknown"}
27
 
28
- # Auto-détection adaptée ZeroGPU
29
- ocr_module = None
30
- ocr_info = {"model_name": "Unknown", "device": "Unknown"}
31
-
32
  # Debug des variables d'environnement HF
33
  import os
34
  space_id = os.getenv("SPACE_ID")
@@ -53,11 +45,9 @@ if is_zerogpu:
53
  # On est sur ZeroGPU, forcer le mode GPU
54
  try:
55
  print("🚀 Force mode ZeroGPU - Import GPU...")
56
- # Créer un simple import qui satisfait ZeroGPU
57
  from simple_gpu import gpu_dummy_function
58
  print("✅ Simple GPU importé")
59
 
60
- # Utiliser le vrai TrOCR qu'on a chargé !
61
  from image_processing_gpu import (
62
  recognize_number_fast_with_image as gpu_recognize,
63
  create_thumbnail_fast,
@@ -67,7 +57,6 @@ if is_zerogpu:
67
  get_ocr_model_info
68
  )
69
 
70
- # Pas de wrapper, utiliser directement TrOCR
71
  recognize_number_fast_with_image = gpu_recognize
72
 
73
  ocr_module = "zerogpu_trocr"
@@ -75,7 +64,6 @@ if is_zerogpu:
75
 
76
  except Exception as e:
77
  print(f"❌ Erreur ZeroGPU: {e}")
78
- # Fallback CPU pur
79
  from image_processing_cpu import (
80
  recognize_number_fast_with_image,
81
  create_thumbnail_fast,
@@ -92,7 +80,6 @@ else:
92
  recognize_number_fast_with_image,
93
  create_thumbnail_fast,
94
  create_white_canvas,
95
- cleanup_memory,
96
  log_memory_usage,
97
  get_ocr_model_info
98
  )
@@ -109,7 +96,7 @@ except Exception as e:
109
 
110
  # Imports dataset avec gestion d'erreur
111
  try:
112
- from datasets import Dataset, load_dataset
113
  DATASET_AVAILABLE = True
114
  print("✅ Modules dataset disponibles")
115
  except ImportError as e:
@@ -134,7 +121,8 @@ def create_result_row_with_images(i: int, image: dict | np.ndarray | Image.Image
134
  print(f"🔍 Image type: {type(image)}")
135
 
136
  # OCR optimisé avec debug
137
- recognized, optimized_image, dataset_image_data = recognize_number_fast_with_image(image, debug=True)
 
138
 
139
  print(f"🔍 OCR recognized: '{recognized}' (type: {type(recognized)})")
140
 
@@ -152,7 +140,7 @@ def create_result_row_with_images(i: int, image: dict | np.ndarray | Image.Image
152
  status_text = "Correct" if is_correct else "Incorrect"
153
  row_color = "#e8f5e8" if is_correct else "#ffe8e8"
154
 
155
- # Miniature
156
  image_thumbnail = create_thumbnail_fast(optimized_image, size=(50, 50))
157
 
158
  # Libérer mémoire
@@ -161,6 +149,7 @@ def create_result_row_with_images(i: int, image: dict | np.ndarray | Image.Image
161
  optimized_image.close()
162
  except:
163
  pass
 
164
 
165
  return {
166
  'html_row': f"""
@@ -178,7 +167,7 @@ def create_result_row_with_images(i: int, image: dict | np.ndarray | Image.Image
178
  'is_correct': is_correct,
179
  'recognized': recognized,
180
  'recognized_num': recognized_num,
181
- 'dataset_image_data': dataset_image_data
182
  }
183
 
184
 
@@ -334,6 +323,7 @@ class MathGame:
334
  self.difficulty = difficulty
335
 
336
  # Nettoyage
 
337
  if hasattr(self, 'user_images') and self.user_images:
338
  for img in self.user_images:
339
  if hasattr(img, 'close'):
@@ -341,11 +331,16 @@ class MathGame:
341
  img.close()
342
  except:
343
  pass
 
344
 
345
  if hasattr(self, 'session_data') and self.session_data:
 
346
  for entry in self.session_data:
347
- if 'user_drawing' in entry and entry['user_drawing']:
348
- entry['user_drawing'] = None
 
 
 
349
  self.session_data.clear()
350
 
351
  # Réinit avec nettoyage parallèle
@@ -359,12 +354,12 @@ class MathGame:
359
 
360
  self.is_running = True
361
  self.start_time = time.time()
362
- self.user_images = []
363
  self.expected_answers = []
364
  self.operations_history = []
365
  self.question_count = 0
366
  self.time_remaining = self.duration
367
- self.session_data = []
368
 
369
  # Reset export
370
  self.export_status = "not_exported"
@@ -419,16 +414,13 @@ class MathGame:
419
  return self.end_game(image_data)
420
 
421
  if image_data is not None:
422
- # Ajouter l'image à la liste ET au traitement parallèle
423
  self.user_images.append(image_data)
424
  self.expected_answers.append(self.correct_answer)
425
 
426
- # Parser l'opération actuelle pour le traitement
427
  parts = self.current_operation.split()
428
  a, op, b = int(parts[0]), parts[1], int(parts[2])
429
  current_operation_data = (a, b, op, self.correct_answer)
430
 
431
- # Lancer le traitement en parallèle de l'image qu'on vient de recevoir
432
  self._add_image_to_processing_queue(self.question_count, image_data, self.correct_answer, current_operation_data)
433
 
434
  self.question_count += 1
@@ -449,7 +441,6 @@ class MathGame:
449
  if time_remaining <= 0:
450
  return self.end_game(image_data)
451
 
452
- # Emoji pour l'opération
453
  operation_emoji = {
454
  "×": "✖️", "+": "➕", "-": "➖", "÷": "➗", "Aléatoire": "🎲"
455
  }
@@ -469,7 +460,6 @@ class MathGame:
469
 
470
  self.is_running = False
471
 
472
- # Arrêter le traitement parallèle
473
  self._stop_background_processing()
474
 
475
  print("🏁 Fin de jeu - Assemblage des résultats...")
@@ -478,12 +468,10 @@ class MathGame:
478
  self.user_images.append(final_image)
479
  self.expected_answers.append(self.correct_answer)
480
 
481
- # Traitement de la dernière image
482
  parts = self.current_operation.split()
483
  a, op, b = int(parts[0]), parts[1], int(parts[2])
484
  final_operation_data = (a, b, op, self.correct_answer)
485
 
486
- # Traiter la dernière image immédiatement (pas en parallèle)
487
  print(f"🔄 Traitement final de l'image {self.question_count}...")
488
  final_result = create_result_row_with_images(self.question_count, final_image, self.correct_answer, final_operation_data)
489
  self.results_cache[self.question_count] = final_result
@@ -492,7 +480,6 @@ class MathGame:
492
  if len(self.operations_history) < len(self.user_images):
493
  self.operations_history.append((a, b, op, self.correct_answer))
494
 
495
- # Attendre que toutes les images soient traitées
496
  max_wait = 10
497
  wait_start = time.time()
498
  expected_results = len(self.user_images)
@@ -504,7 +491,6 @@ class MathGame:
504
  results_ready = len(self.results_cache)
505
  print(f"✅ {results_ready}/{expected_results} résultats prêts")
506
 
507
- # Assembler les résultats dans l'ordre
508
  correct_answers = 0
509
  total_questions = len(self.user_images)
510
  table_rows_html = ""
@@ -514,7 +500,6 @@ class MathGame:
514
 
515
  self.session_data = []
516
  images_saved = 0
517
- total_image_size_kb = 0
518
 
519
  print(f"📊 Assemblage de {total_questions} résultats...")
520
 
@@ -532,7 +517,7 @@ class MathGame:
532
  'is_correct': False,
533
  'recognized': "0",
534
  'recognized_num': 0,
535
- 'dataset_image_data': None
536
  }
537
 
538
  table_rows_html += row_data['html_row']
@@ -540,7 +525,6 @@ class MathGame:
540
  if row_data['is_correct']:
541
  correct_answers += 1
542
 
543
- # Structure pour dataset avec debug OCR
544
  a, b, operation, correct_result = self.operations_history[i] if i < len(self.operations_history) else (0, 0, "×", 0)
545
 
546
  try:
@@ -572,16 +556,23 @@ class MathGame:
572
 
573
  print(f"🔍 Debug entry OCR fields: ocr_model={entry['ocr_model']}, ocr_device={entry['ocr_device']}")
574
 
575
-
576
- if row_data['dataset_image_data']:
577
- entry["handwriting_image"] = row_data['dataset_image_data']["image_base64"]
578
- entry["image_width"] = int(row_data['dataset_image_data']["compressed_size"][0])
579
- entry["image_height"] = int(row_data['dataset_image_data']["compressed_size"][1])
580
- entry["image_size_kb"] = float(row_data['dataset_image_data']["file_size_kb"])
 
 
 
 
 
 
 
581
  entry["has_image"] = True
582
  images_saved += 1
583
- total_image_size_kb += row_data['dataset_image_data']["file_size_kb"]
584
  else:
 
585
  entry["has_image"] = False
586
 
587
  self.session_data.append(entry)
@@ -591,17 +582,24 @@ class MathGame:
591
  for entry in self.session_data:
592
  entry["session_accuracy"] = accuracy
593
 
594
- # Nettoyage mémoire
595
  for img in self.user_images:
596
  if hasattr(img, 'close'):
597
  try:
598
  img.close()
599
  except:
600
  pass
 
 
 
 
 
 
 
 
601
 
602
  gc.collect()
603
 
604
- # HTML résultats
605
  table_html = f"""
606
  <div style="overflow-x: auto; margin: 20px 0;">
607
  <table style="width: 100%; border-collapse: collapse; border: 2px solid #4a90e2;">
@@ -624,7 +622,6 @@ class MathGame:
624
  </div>
625
  """
626
 
627
- # Configuration session pour affichage
628
  config_display = f"{self.operation_type} • {self.difficulty} • {self.duration}s"
629
  operation_emoji = {
630
  "×": "✖️", "+": "➕", "-": "➖", "÷": "➗", "Aléatoire": "🎲"
@@ -632,13 +629,15 @@ class MathGame:
632
  emoji = operation_emoji.get(self.operation_type, "🔢")
633
 
634
  export_info = self.get_export_status()
 
 
635
  if export_info["can_export"]:
636
  export_section = f"""
637
  <div style="margin-top: 20px; padding: 15px; background-color: #e8f5e8; border-radius: 8px;">
638
  <h3 style="color: #2e7d32;"> Résumé de la série</h3>
639
  <p style="color: #2e7d32;">
640
  ✅ {total_questions} réponses • 📊 {accuracy:.1f}% de précision<br>
641
- 📸 {images_saved} opérations et images sauvegardées ({total_image_size_kb:.1f}KB)<br>
642
  ⚙️ Configuration: {config_display}
643
  </p>
644
  </div>
@@ -689,7 +688,7 @@ class MathGame:
689
  def export_to_clean_dataset(session_data: list[dict], dataset_name: str = None) -> str:
690
  """Export vers le nouveau dataset calcul_ocr_dataset"""
691
  if dataset_name is None:
692
- dataset_name = DATASET_NAME # Utiliser la variable globale
693
 
694
  if not DATASET_AVAILABLE:
695
  return "❌ Modules dataset non disponibles"
@@ -702,10 +701,8 @@ def export_to_clean_dataset(session_data: list[dict], dataset_name: str = None)
702
  print(f"\n🚀 === EXPORT VERS DATASET CALCUL OCR ===")
703
  print(f"📊 Dataset: {dataset_name}")
704
 
705
- # Filtrer les entrées avec images et ajouter les infos OCR globalement
706
- clean_entries = []
707
 
708
- # Récupérer une seule fois les infos OCR pour toute la session
709
  try:
710
  global_ocr_info = get_ocr_model_info()
711
  print(f"🔍 Infos OCR globales: {global_ocr_info}")
@@ -714,40 +711,89 @@ def export_to_clean_dataset(session_data: list[dict], dataset_name: str = None)
714
  global_ocr_info = {"model_name": "Unknown", "device": "Unknown"}
715
 
716
  for entry in session_data:
717
- if entry.get('has_image', False):
718
- # Ajouter explicitement les champs OCR manquants
719
- entry_with_ocr = entry.copy()
720
- entry_with_ocr["ocr_model"] = global_ocr_info.get("model_name", "Unknown")
721
- entry_with_ocr["ocr_device"] = global_ocr_info.get("device", "Unknown")
722
 
723
- print(f"🔍 Entry avec OCR: ocr_model={entry_with_ocr['ocr_model']}, ocr_device={entry_with_ocr['ocr_device']}")
724
- clean_entries.append(entry_with_ocr)
725
-
726
- # Créer un dataset de test avec structure forcée
727
- if len(clean_entries) == 0:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
728
  return "❌ Aucune entrée avec image à exporter"
729
 
730
- # Vérifier la structure de la première entrée
731
- sample_entry = clean_entries[0]
732
- print(f"🔍 Structure première entrée: {list(sample_entry.keys())}")
733
- print(f"🔍 OCR dans entrée: ocr_model={sample_entry.get('ocr_model', 'MISSING')}, ocr_device={sample_entry.get('ocr_device', 'MISSING')}")
734
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
735
  # Charger dataset existant et combiner (IMPORTANT!)
736
  try:
737
- existing_dataset = load_dataset(dataset_name, split="train")
 
 
738
  existing_data = existing_dataset.to_list()
739
  print(f"📊 {len(existing_data)} entrées existantes trouvées")
740
 
741
  # Combiner ancien + nouveau
742
- combined_data = existing_data + clean_entries
743
- clean_dataset = Dataset.from_list(combined_data)
744
- print(f"📊 Dataset combiné: {len(existing_data)} existantes + {len(clean_entries)} nouvelles = {len(combined_data)} total")
745
 
746
  except Exception as e:
747
- print(f"📊 Dataset non trouvé, création nouveau: {e}")
748
- # Si le dataset n'existe pas, créer depuis les nouvelles entrées
749
- clean_dataset = Dataset.from_list(clean_entries)
750
- print(f"📊 Nouveau dataset créé avec {len(clean_entries)} entrées")
 
 
 
751
 
752
  print(f"✅ Dataset créé - Features:")
753
  for feature_name in clean_dataset.features:
@@ -755,7 +801,7 @@ def export_to_clean_dataset(session_data: list[dict], dataset_name: str = None)
755
 
756
  # Statistiques par opération
757
  operations_count = {}
758
- for entry in clean_entries:
759
  op = entry.get('operation_type', 'unknown')
760
  operations_count[op] = operations_count.get(op, 0) + 1
761
 
@@ -767,7 +813,7 @@ def export_to_clean_dataset(session_data: list[dict], dataset_name: str = None)
767
  dataset_name,
768
  private=False,
769
  token=hf_token,
770
- commit_message=f"Add {len(clean_entries)} handwriting samples for math OCR ({operations_summary})"
771
  )
772
 
773
  cleanup_memory()
@@ -775,14 +821,14 @@ def export_to_clean_dataset(session_data: list[dict], dataset_name: str = None)
775
  return f"""✅ Session ajoutée au dataset avec succès !
776
 
777
  📊 Dataset: {dataset_name}
778
- 📸 Images: {len(clean_entries)}
779
  🔢 Opérations: {operations_summary}
780
  📈 Total: {len(clean_dataset)}
781
 
782
  🔗 Le dataset est consultable ici : https://huggingface.co/datasets/{dataset_name}"""
783
 
784
  except Exception as e:
785
- print(f"❌ ERREUR: {e}")
786
  import traceback
787
  traceback.print_exc()
788
  return f"❌ Erreur: {str(e)}"
 
2
  # game_engine.py - Calcul OCR v3.0 CLEAN
3
  # ==========================================
4
 
 
 
 
 
5
  import random
6
  import time
7
  import datetime
 
21
  ocr_module = None
22
  ocr_info = {"model_name": "Unknown", "device": "Unknown"}
23
 
 
 
 
 
24
  # Debug des variables d'environnement HF
25
  import os
26
  space_id = os.getenv("SPACE_ID")
 
45
  # On est sur ZeroGPU, forcer le mode GPU
46
  try:
47
  print("🚀 Force mode ZeroGPU - Import GPU...")
 
48
  from simple_gpu import gpu_dummy_function
49
  print("✅ Simple GPU importé")
50
 
 
51
  from image_processing_gpu import (
52
  recognize_number_fast_with_image as gpu_recognize,
53
  create_thumbnail_fast,
 
57
  get_ocr_model_info
58
  )
59
 
 
60
  recognize_number_fast_with_image = gpu_recognize
61
 
62
  ocr_module = "zerogpu_trocr"
 
64
 
65
  except Exception as e:
66
  print(f"❌ Erreur ZeroGPU: {e}")
 
67
  from image_processing_cpu import (
68
  recognize_number_fast_with_image,
69
  create_thumbnail_fast,
 
80
  recognize_number_fast_with_image,
81
  create_thumbnail_fast,
82
  create_white_canvas,
 
83
  log_memory_usage,
84
  get_ocr_model_info
85
  )
 
96
 
97
  # Imports dataset avec gestion d'erreur
98
  try:
99
+ from datasets import Dataset, load_dataset, Image # AJOUT DE Image ici
100
  DATASET_AVAILABLE = True
101
  print("✅ Modules dataset disponibles")
102
  except ImportError as e:
 
121
  print(f"🔍 Image type: {type(image)}")
122
 
123
  # OCR optimisé avec debug
124
+ # dataset_image_data contiendra maintenant directement l'objet PIL.Image ou None
125
+ recognized, optimized_image, dataset_image_object = recognize_number_fast_with_image(image, debug=True)
126
 
127
  print(f"🔍 OCR recognized: '{recognized}' (type: {type(recognized)})")
128
 
 
140
  status_text = "Correct" if is_correct else "Incorrect"
141
  row_color = "#e8f5e8" if is_correct else "#ffe8e8"
142
 
143
+ # Miniature pour l'affichage HTML
144
  image_thumbnail = create_thumbnail_fast(optimized_image, size=(50, 50))
145
 
146
  # Libérer mémoire
 
149
  optimized_image.close()
150
  except:
151
  pass
152
+ # Attention: ne pas close dataset_image_object ici, il sera utilisé plus tard pour l'export
153
 
154
  return {
155
  'html_row': f"""
 
167
  'is_correct': is_correct,
168
  'recognized': recognized,
169
  'recognized_num': recognized_num,
170
+ 'dataset_image_object': dataset_image_object # MODIFIÉ : stocke l'objet PIL.Image directement
171
  }
172
 
173
 
 
323
  self.difficulty = difficulty
324
 
325
  # Nettoyage
326
+ # Suppression des références aux objets Image pour libérer la mémoire
327
  if hasattr(self, 'user_images') and self.user_images:
328
  for img in self.user_images:
329
  if hasattr(img, 'close'):
 
331
  img.close()
332
  except:
333
  pass
334
+ self.user_images.clear() # Vider la liste
335
 
336
  if hasattr(self, 'session_data') and self.session_data:
337
+ # S'assurer de libérer les objets Image dans session_data aussi
338
  for entry in self.session_data:
339
+ if 'handwriting_image' in entry and isinstance(entry['handwriting_image'], Image.Image):
340
+ try:
341
+ entry['handwriting_image'].close()
342
+ except:
343
+ pass
344
  self.session_data.clear()
345
 
346
  # Réinit avec nettoyage parallèle
 
354
 
355
  self.is_running = True
356
  self.start_time = time.time()
357
+ self.user_images = [] # Récemment nettoyé, mais assure la réinit
358
  self.expected_answers = []
359
  self.operations_history = []
360
  self.question_count = 0
361
  self.time_remaining = self.duration
362
+ self.session_data = [] # Récemment nettoyé, mais assure la réinit
363
 
364
  # Reset export
365
  self.export_status = "not_exported"
 
414
  return self.end_game(image_data)
415
 
416
  if image_data is not None:
 
417
  self.user_images.append(image_data)
418
  self.expected_answers.append(self.correct_answer)
419
 
 
420
  parts = self.current_operation.split()
421
  a, op, b = int(parts[0]), parts[1], int(parts[2])
422
  current_operation_data = (a, b, op, self.correct_answer)
423
 
 
424
  self._add_image_to_processing_queue(self.question_count, image_data, self.correct_answer, current_operation_data)
425
 
426
  self.question_count += 1
 
441
  if time_remaining <= 0:
442
  return self.end_game(image_data)
443
 
 
444
  operation_emoji = {
445
  "×": "✖️", "+": "➕", "-": "➖", "÷": "➗", "Aléatoire": "🎲"
446
  }
 
460
 
461
  self.is_running = False
462
 
 
463
  self._stop_background_processing()
464
 
465
  print("🏁 Fin de jeu - Assemblage des résultats...")
 
468
  self.user_images.append(final_image)
469
  self.expected_answers.append(self.correct_answer)
470
 
 
471
  parts = self.current_operation.split()
472
  a, op, b = int(parts[0]), parts[1], int(parts[2])
473
  final_operation_data = (a, b, op, self.correct_answer)
474
 
 
475
  print(f"🔄 Traitement final de l'image {self.question_count}...")
476
  final_result = create_result_row_with_images(self.question_count, final_image, self.correct_answer, final_operation_data)
477
  self.results_cache[self.question_count] = final_result
 
480
  if len(self.operations_history) < len(self.user_images):
481
  self.operations_history.append((a, b, op, self.correct_answer))
482
 
 
483
  max_wait = 10
484
  wait_start = time.time()
485
  expected_results = len(self.user_images)
 
491
  results_ready = len(self.results_cache)
492
  print(f"✅ {results_ready}/{expected_results} résultats prêts")
493
 
 
494
  correct_answers = 0
495
  total_questions = len(self.user_images)
496
  table_rows_html = ""
 
500
 
501
  self.session_data = []
502
  images_saved = 0
 
503
 
504
  print(f"📊 Assemblage de {total_questions} résultats...")
505
 
 
517
  'is_correct': False,
518
  'recognized': "0",
519
  'recognized_num': 0,
520
+ 'dataset_image_object': None # MODIFIÉ
521
  }
522
 
523
  table_rows_html += row_data['html_row']
 
525
  if row_data['is_correct']:
526
  correct_answers += 1
527
 
 
528
  a, b, operation, correct_result = self.operations_history[i] if i < len(self.operations_history) else (0, 0, "×", 0)
529
 
530
  try:
 
556
 
557
  print(f"🔍 Debug entry OCR fields: ocr_model={entry['ocr_model']}, ocr_device={entry['ocr_device']}")
558
 
559
+ # MODIFICATION ICI : Ne plus stocker le Base64, mais l'objet PIL.Image directement.
560
+ # Les infos de taille seront obtenues de l'objet PIL.Image lui-même.
561
+ if row_data['dataset_image_object'] is not None:
562
+ entry["handwriting_image"] = row_data['dataset_image_object'] # Stoque l'objet PIL.Image
563
+ entry["image_width"] = row_data['dataset_image_object'].size[0]
564
+ entry["image_height"] = row_data['dataset_image_object'].size[1]
565
+
566
+ # Calcul de la taille en KB pour l'information, mais pas pour le stockage direct
567
+ buffer_temp = BytesIO()
568
+ row_data['dataset_image_object'].save(buffer_temp, format='PNG', optimize=True, quality=60) # Utiliser la même qualité que prepare_image_for_dataset
569
+ entry["image_size_kb"] = round(len(buffer_temp.getvalue()) / 1024, 1)
570
+ buffer_temp.close()
571
+
572
  entry["has_image"] = True
573
  images_saved += 1
 
574
  else:
575
+ entry["handwriting_image"] = None # Assurer que c'est None pour les entrées sans image
576
  entry["has_image"] = False
577
 
578
  self.session_data.append(entry)
 
582
  for entry in self.session_data:
583
  entry["session_accuracy"] = accuracy
584
 
585
+ # Nettoyage mémoire : s'assurer de fermer les objets PIL.Image
586
  for img in self.user_images:
587
  if hasattr(img, 'close'):
588
  try:
589
  img.close()
590
  except:
591
  pass
592
+ self.user_images.clear() # Vider la liste après utilisation
593
+
594
+ for entry in self.session_data:
595
+ if 'handwriting_image' in entry and isinstance(entry['handwriting_image'], Image.Image):
596
+ try:
597
+ entry['handwriting_image'].close()
598
+ except:
599
+ pass
600
 
601
  gc.collect()
602
 
 
603
  table_html = f"""
604
  <div style="overflow-x: auto; margin: 20px 0;">
605
  <table style="width: 100%; border-collapse: collapse; border: 2px solid #4a90e2;">
 
622
  </div>
623
  """
624
 
 
625
  config_display = f"{self.operation_type} • {self.difficulty} • {self.duration}s"
626
  operation_emoji = {
627
  "×": "✖️", "+": "➕", "-": "➖", "÷": "➗", "Aléatoire": "🎲"
 
629
  emoji = operation_emoji.get(self.operation_type, "🔢")
630
 
631
  export_info = self.get_export_status()
632
+ # Ne plus afficher total_image_size_kb car le calcul est maintenant fait pour chaque image
633
+ # et n'est pas cumulé dans le même style que l'ancienne version.
634
  if export_info["can_export"]:
635
  export_section = f"""
636
  <div style="margin-top: 20px; padding: 15px; background-color: #e8f5e8; border-radius: 8px;">
637
  <h3 style="color: #2e7d32;"> Résumé de la série</h3>
638
  <p style="color: #2e7d32;">
639
  ✅ {total_questions} réponses • 📊 {accuracy:.1f}% de précision<br>
640
+ 📸 {images_saved} opérations et images sauvegardées<br>
641
  ⚙️ Configuration: {config_display}
642
  </p>
643
  </div>
 
688
  def export_to_clean_dataset(session_data: list[dict], dataset_name: str = None) -> str:
689
  """Export vers le nouveau dataset calcul_ocr_dataset"""
690
  if dataset_name is None:
691
+ dataset_name = DATASET_NAME
692
 
693
  if not DATASET_AVAILABLE:
694
  return "❌ Modules dataset non disponibles"
 
701
  print(f"\n🚀 === EXPORT VERS DATASET CALCUL OCR ===")
702
  print(f"📊 Dataset: {dataset_name}")
703
 
704
+ clean_entries_for_dataset = [] # Va contenir les dicts prêts pour Dataset.from_list
 
705
 
 
706
  try:
707
  global_ocr_info = get_ocr_model_info()
708
  print(f"🔍 Infos OCR globales: {global_ocr_info}")
 
711
  global_ocr_info = {"model_name": "Unknown", "device": "Unknown"}
712
 
713
  for entry in session_data:
714
+ if entry.get('has_image', False) and entry.get('handwriting_image') is not None:
715
+ # Créer une nouvelle entrée avec seulement les champs pertinents pour le dataset
716
+ # et le champ 'handwriting_image' contenant l'objet PIL.Image
 
 
717
 
718
+ # MODIFICATION ICI : Adapter la structure pour le type Image
719
+ # Utilise une copie pour éviter de modifier l'entrée originale de session_data
720
+ ds_entry = {
721
+ "session_id": entry.get("session_id"),
722
+ "timestamp": entry.get("timestamp"),
723
+ "question_number": entry.get("question_number"),
724
+ "session_duration": entry.get("session_duration"),
725
+ "operation_type": entry.get("operation_type"),
726
+ "difficulty_level": entry.get("difficulty_level"),
727
+ "operand_a": entry.get("operand_a"),
728
+ "operand_b": entry.get("operand_b"),
729
+ "operation": entry.get("operation"),
730
+ "correct_answer": entry.get("correct_answer"),
731
+ "ocr_model": entry.get("ocr_model"),
732
+ "ocr_device": entry.get("ocr_device"),
733
+ "user_answer_ocr": entry.get("user_answer_ocr"),
734
+ "user_answer_parsed": entry.get("user_answer_parsed"),
735
+ "is_correct": entry.get("is_correct"),
736
+ "total_questions": entry.get("total_questions"),
737
+ "app_version": entry.get("app_version"),
738
+ "handwriting_image": entry['handwriting_image'] # C'EST L'OBJET PIL.Image !
739
+ }
740
+ clean_entries_for_dataset.append(ds_entry)
741
+
742
+ if len(clean_entries_for_dataset) == 0:
743
  return "❌ Aucune entrée avec image à exporter"
744
 
745
+ # Définir les features pour le nouveau dataset.
746
+ # C'est CRUCIAL pour indiquer que 'handwriting_image' est de type Image.
747
+ from datasets import Features, Value, Image as ImageFeature
748
+
749
+ # Vous devez définir toutes les colonnes attendues avec leurs types
750
+ # Assurez-vous que cette structure correspond à toutes les colonnes que vous voulez dans le dataset
751
+ # et que leurs types sont corrects.
752
+ # J'ai ajouté des types de base pour les colonnes que j'ai pu identifier.
753
+ # Vérifiez que tous vos champs sont couverts et que les types sont exacts.
754
+
755
+ features = Features({
756
+ "session_id": Value("string"),
757
+ "timestamp": Value("string"),
758
+ "question_number": Value("int32"),
759
+ "session_duration": Value("int32"),
760
+ "operation_type": Value("string"),
761
+ "difficulty_level": Value("string"),
762
+ "operand_a": Value("int32"),
763
+ "operand_b": Value("int32"),
764
+ "operation": Value("string"),
765
+ "correct_answer": Value("int32"),
766
+ "ocr_model": Value("string"),
767
+ "ocr_device": Value("string"),
768
+ "user_answer_ocr": Value("string"),
769
+ "user_answer_parsed": Value("int32"),
770
+ "is_correct": Value("bool"),
771
+ "total_questions": Value("int32"),
772
+ "app_version": Value("string"),
773
+ "handwriting_image": ImageFeature(), # <--- LA COLONNE IMAGE
774
+ })
775
+
776
  # Charger dataset existant et combiner (IMPORTANT!)
777
  try:
778
+ # Tente de charger le dataset existant avec la structure de features prévue
779
+ # pour assurer la compatibilité.
780
+ existing_dataset = load_dataset(dataset_name, split="train", features=features, download_mode='force_redownload')
781
  existing_data = existing_dataset.to_list()
782
  print(f"📊 {len(existing_data)} entrées existantes trouvées")
783
 
784
  # Combiner ancien + nouveau
785
+ combined_data = existing_data + clean_entries_for_dataset
786
+ clean_dataset = Dataset.from_list(combined_data, features=features) # Passer les features ici aussi
787
+ print(f"📊 Dataset combiné: {len(existing_data)} existantes + {len(clean_entries_for_dataset)} nouvelles = {len(combined_data)} total")
788
 
789
  except Exception as e:
790
+ print(f"📊 Dataset non trouvé ou incompatible, création nouveau: {e}")
791
+ import traceback
792
+ traceback.print_exc() # Pour aider au débogage si le chargement échoue
793
+
794
+ # Si le dataset n'existe pas ou est incompatible, créer depuis les nouvelles entrées
795
+ clean_dataset = Dataset.from_list(clean_entries_for_dataset, features=features)
796
+ print(f"📊 Nouveau dataset créé avec {len(clean_entries_for_dataset)} entrées")
797
 
798
  print(f"✅ Dataset créé - Features:")
799
  for feature_name in clean_dataset.features:
 
801
 
802
  # Statistiques par opération
803
  operations_count = {}
804
+ for entry in clean_entries_for_dataset: # Utiliser clean_entries_for_dataset
805
  op = entry.get('operation_type', 'unknown')
806
  operations_count[op] = operations_count.get(op, 0) + 1
807
 
 
813
  dataset_name,
814
  private=False,
815
  token=hf_token,
816
+ commit_message=f"Add {len(clean_entries_for_dataset)} handwriting samples for math OCR ({operations_summary})"
817
  )
818
 
819
  cleanup_memory()
 
821
  return f"""✅ Session ajoutée au dataset avec succès !
822
 
823
  📊 Dataset: {dataset_name}
824
+ 📸 Images: {len(clean_entries_for_dataset)}
825
  🔢 Opérations: {operations_summary}
826
  📈 Total: {len(clean_dataset)}
827
 
828
  🔗 Le dataset est consultable ici : https://huggingface.co/datasets/{dataset_name}"""
829
 
830
  except Exception as e:
831
+ print(f"❌ ERREUR lors de l'exportation du dataset: {e}")
832
  import traceback
833
  traceback.print_exc()
834
  return f"❌ Erreur: {str(e)}"
image_processing_cpu.py CHANGED
@@ -2,53 +2,21 @@
2
  # image_processing_cpu.py - Version CPU avec EasyOCR
3
  # ==========================================
4
 
5
- """
6
- Module de traitement d'images CPU-optimisé pour calculs mathématiques
7
- Utilise EasyOCR pour des performances rapides sur CPU
8
- """
9
-
10
  import time
11
  from utils import (
12
  optimize_image_for_ocr,
13
- prepare_image_for_dataset,
14
  create_thumbnail_fast,
15
  create_white_canvas,
16
  log_memory_usage,
17
  cleanup_memory,
18
- decode_image_from_dataset,
19
  validate_ocr_result
20
  )
21
 
22
- # Variables globales pour OCR EasyOCR
23
- easyocr_reader = None
24
- OCR_MODEL_NAME = "EasyOCR"
25
-
26
- def init_ocr_model() -> bool:
27
- """Initialise EasyOCR (optimisé CPU)"""
28
- global easyocr_reader
29
-
30
- try:
31
- print("🔄 Chargement EasyOCR (CPU optimisé)...")
32
- import easyocr
33
- easyocr_reader = easyocr.Reader(['en'], gpu=False, verbose=False)
34
- print("✅ EasyOCR prêt (CPU) !")
35
- return True
36
-
37
- except Exception as e:
38
- print(f"❌ Erreur lors du chargement EasyOCR: {e}")
39
- return False
40
-
41
- def get_ocr_model_info() -> dict:
42
- """Retourne les informations du modèle OCR utilisé"""
43
- return {
44
- "model_name": OCR_MODEL_NAME,
45
- "device": "CPU",
46
- "framework": "EasyOCR",
47
- "optimized_for": "speed",
48
- "version": "1.7.x"
49
- }
50
 
51
- def recognize_number_fast_with_image(image_dict, debug: bool = False) -> tuple[str, any, dict | None]:
52
  """
53
  OCR avec EasyOCR (CPU optimisé)
54
 
@@ -57,7 +25,7 @@ def recognize_number_fast_with_image(image_dict, debug: bool = False) -> tuple[s
57
  debug: Afficher les logs de debug
58
 
59
  Returns:
60
- (résultat_ocr, image_optimisée, données_dataset)
61
  """
62
  if image_dict is None or easyocr_reader is None:
63
  if debug:
@@ -92,24 +60,17 @@ def recognize_number_fast_with_image(image_dict, debug: bool = False) -> tuple[s
92
  final_result = "0"
93
 
94
  # Préparer pour dataset (fonction commune)
95
- dataset_image_data = prepare_image_for_dataset(optimized_image)
 
96
 
97
  if debug:
98
  total_time = time.time() - start_time
99
  print(f" ✅ EasyOCR terminé en {total_time:.1f}s → '{final_result}'")
100
 
101
- return final_result, optimized_image, dataset_image_data
102
 
103
  except Exception as e:
104
  print(f"❌ Erreur OCR EasyOCR: {e}")
105
  return "0", None, None
106
 
107
- def recognize_number_fast(image_dict) -> tuple[str, any]:
108
- """Version rapide standard"""
109
- result, optimized_image, _ = recognize_number_fast_with_image(image_dict)
110
- return result, optimized_image
111
-
112
- def recognize_number(image_dict) -> str:
113
- """Interface standard"""
114
- result, _ = recognize_number_fast(image_dict)
115
- return result
 
2
  # image_processing_cpu.py - Version CPU avec EasyOCR
3
  # ==========================================
4
 
 
 
 
 
 
5
  import time
6
  from utils import (
7
  optimize_image_for_ocr,
8
+ prepare_image_for_dataset, # Cette fonction retournera maintenant l'image PIL
9
  create_thumbnail_fast,
10
  create_white_canvas,
11
  log_memory_usage,
12
  cleanup_memory,
13
+ # decode_image_from_dataset, # Cette fonction ne sera plus utilisée
14
  validate_ocr_result
15
  )
16
 
17
+ # ... (le reste du code est inchangé jusqu'à recognize_number_fast_with_image) ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
+ def recognize_number_fast_with_image(image_dict, debug: bool = False) -> tuple[str, any, Image.Image | None]: # MODIFICATION DU TYPE DE RETOUR
20
  """
21
  OCR avec EasyOCR (CPU optimisé)
22
 
 
25
  debug: Afficher les logs de debug
26
 
27
  Returns:
28
+ (résultat_ocr, image_optimisée, image_pour_dataset) # MODIFIÉ
29
  """
30
  if image_dict is None or easyocr_reader is None:
31
  if debug:
 
60
  final_result = "0"
61
 
62
  # Préparer pour dataset (fonction commune)
63
+ # MODIFICATION ICI : prepare_image_for_dataset retourne maintenant l'objet PIL.Image directement
64
+ dataset_image = prepare_image_for_dataset(optimized_image)
65
 
66
  if debug:
67
  total_time = time.time() - start_time
68
  print(f" ✅ EasyOCR terminé en {total_time:.1f}s → '{final_result}'")
69
 
70
+ return final_result, optimized_image, dataset_image # MODIFIÉ
71
 
72
  except Exception as e:
73
  print(f"❌ Erreur OCR EasyOCR: {e}")
74
  return "0", None, None
75
 
76
+ # ... (le reste du code est inchangé) ...
 
 
 
 
 
 
 
 
image_processing_gpu.py CHANGED
@@ -2,12 +2,8 @@
2
  # image_processing_gpu.py - Version ZeroGPU compatible
3
  # ==========================================
4
 
5
- """
6
- Module de traitement d'images GPU-optimisé pour calculs mathématiques
7
- Compatible ZeroGPU HuggingFace Spaces
8
- """
9
-
10
  import time
 
11
 
12
  # Import spaces avec gestion d'erreur complète
13
  try:
@@ -16,7 +12,6 @@ try:
16
  SPACES_AVAILABLE = True
17
  except ImportError as e:
18
  print(f"❌ Import spaces échoué: {e}")
19
- # Créer un mock si spaces n'est pas disponible
20
  class MockSpaces:
21
  @staticmethod
22
  def GPU(func):
@@ -34,73 +29,19 @@ except ImportError:
34
 
35
  from utils import (
36
  optimize_image_for_ocr,
37
- prepare_image_for_dataset,
38
  create_thumbnail_fast,
39
  create_white_canvas,
40
  log_memory_usage,
41
  cleanup_memory,
42
- decode_image_from_dataset,
43
  validate_ocr_result
44
  )
45
 
46
- # Variables globales pour OCR
47
- processor = None
48
- model = None
49
- OCR_MODEL_NAME = "TrOCR-base-handwritten"
50
 
51
- def init_ocr_model() -> bool:
52
- """Initialise TrOCR (ZeroGPU compatible)"""
53
- global processor, model
54
-
55
- try:
56
- print("🔄 Chargement TrOCR (ZeroGPU optimisé)...")
57
-
58
- if not TORCH_AVAILABLE:
59
- print("❌ Torch non disponible, impossible de charger TrOCR")
60
- return False
61
-
62
- from transformers import TrOCRProcessor, VisionEncoderDecoderModel
63
-
64
- processor = TrOCRProcessor.from_pretrained('microsoft/trocr-base-handwritten')
65
- model = VisionEncoderDecoderModel.from_pretrained('microsoft/trocr-base-handwritten')
66
-
67
- # Optimisations
68
- model.eval()
69
-
70
- if torch.cuda.is_available():
71
- model = model.cuda()
72
- device_info = f"GPU ({torch.cuda.get_device_name()})"
73
- print(f"✅ TrOCR prêt sur {device_info} !")
74
- else:
75
- device_info = "CPU (ZeroGPU pas encore alloué)"
76
- print(f"⚠️ TrOCR sur CPU - {device_info}")
77
-
78
- return True
79
-
80
- except Exception as e:
81
- print(f"❌ Erreur lors du chargement TrOCR: {e}")
82
- return False
83
-
84
- def get_ocr_model_info() -> dict:
85
- """Retourne les informations du modèle OCR utilisé"""
86
- if TORCH_AVAILABLE and torch.cuda.is_available():
87
- device = "ZeroGPU"
88
- gpu_name = torch.cuda.get_device_name() if torch.cuda.is_available() else "N/A"
89
- else:
90
- device = "CPU"
91
- gpu_name = "N/A"
92
-
93
- return {
94
- "model_name": OCR_MODEL_NAME,
95
- "device": device,
96
- "gpu_name": gpu_name,
97
- "framework": "HuggingFace-Transformers-ZeroGPU",
98
- "optimized_for": "accuracy",
99
- "version": "microsoft/trocr-base-handwritten"
100
- }
101
-
102
- @spaces.GPU # Décorateur ZeroGPU
103
- def recognize_number_fast_with_image(image_dict, debug: bool = False) -> tuple[str, any, dict | None]:
104
  """
105
  OCR avec TrOCR (ZeroGPU optimisé)
106
  """
@@ -131,14 +72,11 @@ def recognize_number_fast_with_image(image_dict, debug: bool = False) -> tuple[s
131
  print(" 🤖 Lancement TrOCR ZeroGPU...")
132
 
133
  with torch.no_grad():
134
- # Preprocessing
135
  pixel_values = processor(images=optimized_image, return_tensors="pt").pixel_values
136
 
137
- # GPU transfer si disponible
138
  if torch.cuda.is_available():
139
  pixel_values = pixel_values.cuda()
140
 
141
- # Génération optimisée
142
  generated_ids = model.generate(
143
  pixel_values,
144
  max_length=4,
@@ -148,30 +86,22 @@ def recognize_number_fast_with_image(image_dict, debug: bool = False) -> tuple[s
148
  pad_token_id=processor.tokenizer.pad_token_id
149
  )
150
 
151
- # Décodage
152
  result = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
153
  final_result = validate_ocr_result(result, max_length=4)
154
 
155
  # Préparer pour dataset
156
- dataset_image_data = prepare_image_for_dataset(optimized_image)
 
157
 
158
  if debug:
159
  total_time = time.time() - start_time
160
  device = "ZeroGPU" if torch.cuda.is_available() else "CPU"
161
  print(f" ✅ TrOCR ({device}) terminé en {total_time:.1f}s → '{final_result}'")
162
 
163
- return final_result, optimized_image, dataset_image_data
164
 
165
  except Exception as e:
166
  print(f"❌ Erreur OCR TrOCR ZeroGPU: {e}")
167
  return "0", None, None
168
 
169
- def recognize_number_fast(image_dict) -> tuple[str, any]:
170
- """Version rapide standard"""
171
- result, optimized_image, _ = recognize_number_fast_with_image(image_dict)
172
- return result, optimized_image
173
-
174
- def recognize_number(image_dict) -> str:
175
- """Interface standard"""
176
- result, _ = recognize_number_fast(image_dict)
177
- return result
 
2
  # image_processing_gpu.py - Version ZeroGPU compatible
3
  # ==========================================
4
 
 
 
 
 
 
5
  import time
6
+ import torch # Assurez-vous que torch est importé
7
 
8
  # Import spaces avec gestion d'erreur complète
9
  try:
 
12
  SPACES_AVAILABLE = True
13
  except ImportError as e:
14
  print(f"❌ Import spaces échoué: {e}")
 
15
  class MockSpaces:
16
  @staticmethod
17
  def GPU(func):
 
29
 
30
  from utils import (
31
  optimize_image_for_ocr,
32
+ prepare_image_for_dataset, # Cette fonction retournera maintenant l'image PIL
33
  create_thumbnail_fast,
34
  create_white_canvas,
35
  log_memory_usage,
36
  cleanup_memory,
37
+ # decode_image_from_dataset, # Cette fonction ne sera plus utilisée
38
  validate_ocr_result
39
  )
40
 
41
+ # ... (le reste du code est inchangé jusqu'à recognize_number_fast_with_image) ...
 
 
 
42
 
43
+ @spaces.GPU
44
+ def recognize_number_fast_with_image(image_dict, debug: bool = False) -> tuple[str, any, Image.Image | None]: # MODIFICATION DU TYPE DE RETOUR
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  """
46
  OCR avec TrOCR (ZeroGPU optimisé)
47
  """
 
72
  print(" 🤖 Lancement TrOCR ZeroGPU...")
73
 
74
  with torch.no_grad():
 
75
  pixel_values = processor(images=optimized_image, return_tensors="pt").pixel_values
76
 
 
77
  if torch.cuda.is_available():
78
  pixel_values = pixel_values.cuda()
79
 
 
80
  generated_ids = model.generate(
81
  pixel_values,
82
  max_length=4,
 
86
  pad_token_id=processor.tokenizer.pad_token_id
87
  )
88
 
 
89
  result = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
90
  final_result = validate_ocr_result(result, max_length=4)
91
 
92
  # Préparer pour dataset
93
+ # MODIFICATION ICI : prepare_image_for_dataset retourne maintenant l'objet PIL.Image directement
94
+ dataset_image = prepare_image_for_dataset(optimized_image)
95
 
96
  if debug:
97
  total_time = time.time() - start_time
98
  device = "ZeroGPU" if torch.cuda.is_available() else "CPU"
99
  print(f" ✅ TrOCR ({device}) terminé en {total_time:.1f}s → '{final_result}'")
100
 
101
+ return final_result, optimized_image, dataset_image # MODIFIÉ
102
 
103
  except Exception as e:
104
  print(f"❌ Erreur OCR TrOCR ZeroGPU: {e}")
105
  return "0", None, None
106
 
107
+ # ... (le reste du code est inchangé) ...
 
 
 
 
 
 
 
 
utils.py CHANGED
@@ -59,7 +59,7 @@ def optimize_image_for_ocr(image_dict: dict | np.ndarray | Image.Image | None, m
59
  elif isinstance(image_dict, np.ndarray):
60
  image = image_dict
61
  elif isinstance(image_dict, Image.Image):
62
- image = image_dict
63
  else:
64
  return None
65
 
@@ -79,53 +79,19 @@ def optimize_image_for_ocr(image_dict: dict | np.ndarray | Image.Image | None, m
79
  print(f"❌ Erreur optimisation image: {e}")
80
  return None
81
 
82
- def prepare_image_for_dataset(image: Image.Image, max_size: tuple[int, int] = (100, 100), quality: int = 60) -> dict[str, str | int | float | tuple] | None:
 
 
83
  """
84
- Prépare une image pour l'inclusion dans le dataset
85
-
86
- Args:
87
- image: Image PIL à traiter
88
- max_size: Taille maximale (largeur, hauteur)
89
- quality: Qualité de compression PNG
90
-
91
- Returns:
92
- Dictionnaire avec image_base64, taille, etc. ou None
93
  """
94
- try:
95
- if image is None:
96
- return None
97
-
98
- # Copier et redimensionner
99
- dataset_image = image.copy()
100
- dataset_image.thumbnail(max_size, Image.Resampling.LANCZOS)
101
- compressed_size = dataset_image.size
102
-
103
- # Convertir en base64
104
- buffer = BytesIO()
105
- dataset_image.save(buffer, format='PNG', optimize=True, quality=quality)
106
-
107
- buffer_data = buffer.getvalue()
108
- image_base64 = base64.b64encode(buffer_data).decode()
109
- file_size_kb = len(image_base64) / 1024
110
-
111
- # Structure propre pour dataset
112
- result = {
113
- "image_base64": image_base64,
114
- "compressed_size": compressed_size,
115
- "file_size_kb": round(file_size_kb, 1),
116
- "format": "PNG",
117
- "quality": quality
118
- }
119
-
120
- # Nettoyage
121
- dataset_image.close()
122
- buffer.close()
123
-
124
- return result
125
-
126
- except Exception as e:
127
- print(f"❌ Erreur préparation image dataset: {e}")
128
- return None
129
 
130
  def create_thumbnail_fast(optimized_image: Image.Image | None, size: tuple[int, int] = (40, 40)) -> str:
131
  """
@@ -146,7 +112,7 @@ def create_thumbnail_fast(optimized_image: Image.Image | None, size: tuple[int,
146
  thumbnail.thumbnail(size, Image.Resampling.LANCZOS)
147
 
148
  buffer = BytesIO()
149
- thumbnail.save(buffer, format='PNG', optimize=True, quality=70)
150
  img_str = base64.b64encode(buffer.getvalue()).decode()
151
 
152
  thumbnail.close()
@@ -157,15 +123,12 @@ def create_thumbnail_fast(optimized_image: Image.Image | None, size: tuple[int,
157
  except Exception:
158
  return "📝"
159
 
 
 
160
  def decode_image_from_dataset(base64_string: str) -> Image.Image | None:
161
  """
162
  Décode une image depuis le dataset pour fine-tuning ou analyse
163
-
164
- Args:
165
- base64_string: String base64 de l'image
166
-
167
- Returns:
168
- Image PIL ou None si erreur
169
  """
170
  try:
171
  image_bytes = base64.b64decode(base64_string)
@@ -178,25 +141,15 @@ def decode_image_from_dataset(base64_string: str) -> Image.Image | None:
178
  def validate_ocr_result(raw_result: str, max_length: int = 4) -> str:
179
  """
180
  Valide et nettoie un résultat OCR
181
-
182
- Args:
183
- raw_result: Résultat brut de l'OCR
184
- max_length: Longueur maximale autorisée
185
-
186
- Returns:
187
- Résultat nettoyé (chiffres uniquement)
188
  """
189
  if not raw_result:
190
  return "0"
191
 
192
- # Extraire uniquement les chiffres
193
  cleaned_result = ''.join(filter(str.isdigit, str(raw_result)))
194
 
195
- # Valider la longueur
196
  if cleaned_result and len(cleaned_result) <= max_length:
197
  return cleaned_result
198
  elif cleaned_result:
199
- # Si trop long, prendre les premiers chiffres
200
  return cleaned_result[:max_length]
201
  else:
202
  return "0"
@@ -204,14 +157,6 @@ def validate_ocr_result(raw_result: str, max_length: int = 4) -> str:
204
  def analyze_calculation_complexity(operand_a: int, operand_b: int, operation: str) -> dict:
205
  """
206
  Analyse la complexité d'un calcul pour enrichir les métadonnées dataset
207
-
208
- Args:
209
- operand_a: Premier opérande
210
- operand_b: Deuxième opérande
211
- operation: Type d'opération (×, +, -, ÷)
212
-
213
- Returns:
214
- Dictionnaire avec score de complexité et catégorie
215
  """
216
  complexity_score = 0
217
 
@@ -224,7 +169,6 @@ def analyze_calculation_complexity(operand_a: int, operand_b: int, operation: st
224
  elif operation == "÷":
225
  complexity_score = operand_a / 10
226
 
227
- # Catégorisation
228
  if complexity_score < 5:
229
  category = "easy"
230
  elif complexity_score < 10:
 
59
  elif isinstance(image_dict, np.ndarray):
60
  image = image_dict
61
  elif isinstance(image_dict, Image.Image):
62
+ image = image
63
  else:
64
  return None
65
 
 
79
  print(f"❌ Erreur optimisation image: {e}")
80
  return None
81
 
82
+ # MODIFICATION ICI : Ne plus retourner de base64, mais l'objet PIL.Image directement.
83
+ # La fonction `datasets.Image()` s'occupera de la sérialisation pour le dataset.
84
+ def prepare_image_for_dataset(image: Image.Image) -> Image.Image | None:
85
  """
86
+ Prépare une image pour l'inclusion dans le dataset en retournant l'objet PIL.Image.
87
+ La compression et le redimensionnement sont déjà faits par optimize_image_for_ocr si nécessaire.
 
 
 
 
 
 
 
88
  """
89
+ # Ici, nous retournons l'image telle quelle.
90
+ # Si vous voulez une taille spécifique pour le dataset (différente de celle d'OCR),
91
+ # vous pouvez ajouter un redimensionnement ici, mais il faut être prudent avec la taille pour éviter des images géantes.
92
+ # Pour le moment, nous allons simplement retourner l'image optimisée par l'OCR.
93
+ # Le type Image de datasets gère automatiquement la compression optimale pour le Hub.
94
+ return image
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
  def create_thumbnail_fast(optimized_image: Image.Image | None, size: tuple[int, int] = (40, 40)) -> str:
97
  """
 
112
  thumbnail.thumbnail(size, Image.Resampling.LANCZOS)
113
 
114
  buffer = BytesIO()
115
+ thumbnail.save(buffer, format='PNG', optimize=True, quality=70) # Garde le Base64 ici pour l'affichage HTML
116
  img_str = base64.b64encode(buffer.getvalue()).decode()
117
 
118
  thumbnail.close()
 
123
  except Exception:
124
  return "📝"
125
 
126
+ # MODIFICATION ICI : Cette fonction devient obsolète car nous ne stockons plus de Base64 dans le dataset.
127
+ # Laissez-la si elle est appelée ailleurs pour l'instant, mais elle ne sera plus utilisée pour le dataset.
128
  def decode_image_from_dataset(base64_string: str) -> Image.Image | None:
129
  """
130
  Décode une image depuis le dataset pour fine-tuning ou analyse
131
+ (sera obsolète si le dataset est en type Image natif)
 
 
 
 
 
132
  """
133
  try:
134
  image_bytes = base64.b64decode(base64_string)
 
141
  def validate_ocr_result(raw_result: str, max_length: int = 4) -> str:
142
  """
143
  Valide et nettoie un résultat OCR
 
 
 
 
 
 
 
144
  """
145
  if not raw_result:
146
  return "0"
147
 
 
148
  cleaned_result = ''.join(filter(str.isdigit, str(raw_result)))
149
 
 
150
  if cleaned_result and len(cleaned_result) <= max_length:
151
  return cleaned_result
152
  elif cleaned_result:
 
153
  return cleaned_result[:max_length]
154
  else:
155
  return "0"
 
157
  def analyze_calculation_complexity(operand_a: int, operand_b: int, operation: str) -> dict:
158
  """
159
  Analyse la complexité d'un calcul pour enrichir les métadonnées dataset
 
 
 
 
 
 
 
 
160
  """
161
  complexity_score = 0
162
 
 
169
  elif operation == "÷":
170
  complexity_score = operand_a / 10
171
 
 
172
  if complexity_score < 5:
173
  category = "easy"
174
  elif complexity_score < 10: