Spaces:
Sleeping
Sleeping
Upload 5 files
Browse files- game_engine.py +121 -75
- image_processing_cpu.py +9 -48
- image_processing_gpu.py +10 -80
- 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 |
-
|
|
|
|
| 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 |
-
'
|
| 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 '
|
| 348 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
'
|
| 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 |
-
|
| 577 |
-
|
| 578 |
-
entry["
|
| 579 |
-
entry["
|
| 580 |
-
entry["
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
| 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 |
-
|
| 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 |
-
#
|
| 719 |
-
|
| 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 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 728 |
return "❌ Aucune entrée avec image à exporter"
|
| 729 |
|
| 730 |
-
#
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 735 |
# Charger dataset existant et combiner (IMPORTANT!)
|
| 736 |
try:
|
| 737 |
-
|
|
|
|
|
|
|
| 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 +
|
| 743 |
-
clean_dataset = Dataset.from_list(combined_data)
|
| 744 |
-
print(f"📊 Dataset combiné: {len(existing_data)} existantes + {len(
|
| 745 |
|
| 746 |
except Exception as e:
|
| 747 |
-
print(f"📊 Dataset non
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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(
|
| 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(
|
| 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 |
-
#
|
| 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,
|
| 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,
|
| 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 |
-
|
|
|
|
| 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,
|
| 102 |
|
| 103 |
except Exception as e:
|
| 104 |
print(f"❌ Erreur OCR EasyOCR: {e}")
|
| 105 |
return "0", None, None
|
| 106 |
|
| 107 |
-
|
| 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 |
-
#
|
| 47 |
-
processor = None
|
| 48 |
-
model = None
|
| 49 |
-
OCR_MODEL_NAME = "TrOCR-base-handwritten"
|
| 50 |
|
| 51 |
-
|
| 52 |
-
|
| 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 |
-
|
|
|
|
| 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,
|
| 164 |
|
| 165 |
except Exception as e:
|
| 166 |
print(f"❌ Erreur OCR TrOCR ZeroGPU: {e}")
|
| 167 |
return "0", None, None
|
| 168 |
|
| 169 |
-
|
| 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 =
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 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:
|