Olivier commited on
Commit
5855ebc
·
verified ·
1 Parent(s): 30e5cf1

Delete game_engine.py

Browse files
Files changed (1) hide show
  1. game_engine.py +0 -593
game_engine.py DELETED
@@ -1,593 +0,0 @@
1
- # ==========================================
2
- # game_engine.py - Calcul OCR v3.0
3
- # ==========================================
4
-
5
- """
6
- Moteur de jeu mathématique complet
7
- """
8
-
9
- import random
10
- import time
11
- import datetime
12
- import gradio as gr
13
- import os
14
- import uuid
15
- import gc
16
- import base64
17
- from io import BytesIO
18
- import numpy as np
19
- from PIL import Image
20
-
21
- # Import optimisé avec images
22
- from image_processing import (
23
- recognize_number_fast_with_image,
24
- create_thumbnail_fast,
25
- create_white_canvas,
26
- cleanup_memory,
27
- log_memory_usage
28
- )
29
-
30
- # Imports dataset avec gestion d'erreur
31
- try:
32
- from datasets import Dataset, load_dataset
33
- DATASET_AVAILABLE = True
34
- print("✅ Modules dataset disponibles")
35
- except ImportError as e:
36
- DATASET_AVAILABLE = False
37
- print(f"⚠️ Modules dataset non disponibles: {e}")
38
-
39
- # Nom du nouveau dataset
40
- DATASET_NAME = "hoololi/calcul_ocr_dataset"
41
-
42
- # Configuration des difficultés par opération
43
- DIFFICULTY_RANGES = {
44
- "×": {
45
- "Facile": (2, 9),
46
- "Difficile": (4, 12)
47
- },
48
- "+": {
49
- "Facile": (1, 50),
50
- "Difficile": (10, 100)
51
- },
52
- "-": {
53
- "Facile": (1, 50),
54
- "Difficile": (10, 100)
55
- },
56
- "÷": {
57
- "Facile": (1, 10), # Pour les résultats
58
- "Difficile": (2, 12) # Pour les résultats
59
- }
60
- }
61
-
62
- def create_result_row_with_images(i: int, image: dict | np.ndarray | Image.Image, expected: int, operation_data: tuple[int, int, str, int]) -> dict:
63
-
64
- # OCR optimisé
65
- recognized, optimized_image, dataset_image_data = recognize_number_fast_with_image(image)
66
-
67
- try:
68
- recognized_num = int(recognized) if recognized.isdigit() else 0
69
- except:
70
- recognized_num = 0
71
-
72
- is_correct = recognized_num == expected
73
- a, b, operation, correct_result = operation_data
74
-
75
- status_icon = "✅" if is_correct else "❌"
76
- status_text = "Correct" if is_correct else "Incorrect"
77
- row_color = "#e8f5e8" if is_correct else "#ffe8e8"
78
-
79
- # Miniature
80
- image_thumbnail = create_thumbnail_fast(optimized_image, size=(50, 50))
81
-
82
- # Libérer mémoire
83
- if optimized_image and hasattr(optimized_image, 'close'):
84
- try:
85
- optimized_image.close()
86
- except:
87
- pass
88
-
89
- return {
90
- 'html_row': f"""
91
- <tr style="background-color: {row_color};">
92
- <td style="text-align: center; padding: 8px; border: 1px solid #ddd; color: #333;">{i+1}</td>
93
- <td style="text-align: center; padding: 8px; border: 1px solid #ddd; font-weight: bold; color: #333;">{a}</td>
94
- <td style="text-align: center; padding: 8px; border: 1px solid #ddd; font-weight: bold; color: #333;">{operation}</td>
95
- <td style="text-align: center; padding: 8px; border: 1px solid #ddd; font-weight: bold; color: #333;">{b}</td>
96
- <td style="text-align: center; padding: 8px; border: 1px solid #ddd; font-weight: bold; color: #333;">{expected}</td>
97
- <td style="text-align: center; padding: 8px; border: 1px solid #ddd;">{image_thumbnail}</td>
98
- <td style="text-align: center; padding: 8px; border: 1px solid #ddd; font-weight: bold; color: #333;">{recognized_num}</td>
99
- <td style="text-align: center; padding: 8px; border: 1px solid #ddd; color: #333;">{status_icon} {status_text}</td>
100
- </tr>
101
- """,
102
- 'is_correct': is_correct,
103
- 'recognized': recognized,
104
- 'recognized_num': recognized_num,
105
- 'dataset_image_data': dataset_image_data
106
- }
107
-
108
-
109
- class MathGame:
110
- """Moteur de jeu mathématique complet"""
111
-
112
- def __init__(self):
113
- self.is_running = False
114
- self.start_time = 0
115
- self.current_operation = ""
116
- self.correct_answer = 0
117
- self.user_images = []
118
- self.expected_answers = []
119
- self.operations_history = []
120
- self.question_count = 0
121
- self.time_remaining = 30
122
- self.session_data = []
123
-
124
- # Configuration session
125
- self.duration = 30
126
- self.operation_type = "×"
127
- self.difficulty = "Facile"
128
-
129
- # Gestion export
130
- self.export_status = "not_exported"
131
- self.export_timestamp = None
132
- self.export_result = None
133
-
134
- def get_export_status(self) -> dict[str, str | bool | None]:
135
- return {
136
- "status": self.export_status,
137
- "timestamp": self.export_timestamp,
138
- "result": self.export_result,
139
- "can_export": self.export_status == "not_exported" and len(self.session_data) > 0
140
- }
141
-
142
- def mark_export_in_progress(self) -> None:
143
- self.export_status = "exporting"
144
- self.export_timestamp = datetime.datetime.now().isoformat()
145
-
146
- def mark_export_completed(self, result: str) -> None:
147
- self.export_status = "exported"
148
- self.export_result = result
149
-
150
- def generate_multiplication(self, difficulty: str) -> tuple[str, int]:
151
- """Génère une multiplication"""
152
- min_val, max_val = DIFFICULTY_RANGES["×"][difficulty]
153
- a = random.randint(min_val, max_val)
154
- b = random.randint(min_val, max_val)
155
- return f"{a} × {b}", a * b
156
-
157
- def generate_addition(self, difficulty: str) -> tuple[str, int]:
158
- """Génère une addition"""
159
- min_val, max_val = DIFFICULTY_RANGES["+"][difficulty]
160
- a = random.randint(min_val, max_val)
161
- b = random.randint(min_val, max_val)
162
- return f"{a} + {b}", a + b
163
-
164
- def generate_subtraction(self, difficulty: str) -> tuple[str, int]:
165
- """Génère une soustraction (résultat toujours positif)"""
166
- min_val, max_val = DIFFICULTY_RANGES["-"][difficulty]
167
- a = random.randint(min_val, max_val)
168
- b = random.randint(min_val, a) # b <= a pour éviter les négatifs
169
- return f"{a} - {b}", a - b
170
-
171
- def generate_division(self, difficulty: str) -> tuple[str, int]:
172
- """Génère une division exacte"""
173
- min_result, max_result = DIFFICULTY_RANGES["÷"][difficulty]
174
- result = random.randint(min_result, max_result)
175
- if difficulty == "Facile":
176
- divisor = random.randint(2, 9)
177
- else:
178
- divisor = random.randint(2, 12)
179
- dividend = result * divisor
180
- return f"{dividend} ÷ {divisor}", result
181
-
182
- def generate_operation(self, operation_type: str, difficulty: str) -> tuple[str, int]:
183
- """Génère une opération selon le type et la difficulté"""
184
- if operation_type == "×":
185
- return self.generate_multiplication(difficulty)
186
- elif operation_type == "+":
187
- return self.generate_addition(difficulty)
188
- elif operation_type == "-":
189
- return self.generate_subtraction(difficulty)
190
- elif operation_type == "÷":
191
- return self.generate_division(difficulty)
192
- elif operation_type == "Aléatoire":
193
- # Choisir aléatoirement une opération
194
- random_op = random.choice(["×", "+", "-", "÷"])
195
- return self.generate_operation(random_op, difficulty)
196
- else:
197
- # Par défaut, multiplication
198
- return self.generate_multiplication(difficulty)
199
-
200
- def start_game(self, duration: str, operation: str, difficulty: str) -> tuple[str, Image.Image, str, str, gr.update, gr.update, str]:
201
- """Démarre le jeu avec la configuration choisie"""
202
-
203
- # log_memory_usage("avant nettoyage start_game") # DEBUG: Désactivé
204
-
205
- # Configuration
206
- self.duration = 60 if duration == "60 secondes" else 30
207
- self.operation_type = operation
208
- self.difficulty = difficulty
209
-
210
- # Nettoyage
211
- if hasattr(self, 'user_images') and self.user_images:
212
- for img in self.user_images:
213
- if hasattr(img, 'close'):
214
- try:
215
- img.close()
216
- except:
217
- pass
218
-
219
- if hasattr(self, 'session_data') and self.session_data:
220
- for entry in self.session_data:
221
- if 'user_drawing' in entry and entry['user_drawing']:
222
- entry['user_drawing'] = None
223
- self.session_data.clear()
224
-
225
- # Réinit
226
- self.is_running = True
227
- self.start_time = time.time()
228
- self.user_images = []
229
- self.expected_answers = []
230
- self.operations_history = []
231
- self.question_count = 0
232
- self.time_remaining = self.duration
233
- self.session_data = []
234
-
235
- # Reset export
236
- self.export_status = "not_exported"
237
- self.export_timestamp = None
238
- self.export_result = None
239
-
240
- gc.collect()
241
- # log_memory_usage("après nettoyage start_game") # DEBUG: Désactivé
242
-
243
- # Première opération
244
- operation_str, answer = self.generate_operation(self.operation_type, self.difficulty)
245
- self.current_operation = operation_str
246
- self.correct_answer = answer
247
-
248
- # Parser l'opération pour l'historique
249
- parts = operation_str.split()
250
- a, op, b = int(parts[0]), parts[1], int(parts[2])
251
- self.operations_history.append((a, b, op, answer))
252
-
253
- # Affichage adapté selon l'opération
254
- operation_emoji = {
255
- "×": "✖️", "+": "➕", "-": "➖", "÷": "➗", "Aléatoire": "🎲"
256
- }
257
- emoji = operation_emoji.get(self.operation_type, "🔢")
258
-
259
- return (
260
- f'<div style="font-size: 3em; font-weight: bold; text-align: center; padding: 20px; background: linear-gradient(45deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 10px;">{operation_str}</div>',
261
- create_white_canvas(),
262
- f"🎯 {emoji} {self.operation_type} • {self.difficulty} • Écrivez votre réponse !",
263
- f"⏱️ Temps restant: {self.time_remaining}s",
264
- gr.update(interactive=False),
265
- gr.update(interactive=True),
266
- ""
267
- )
268
-
269
- def next_question(self, image_data: dict | np.ndarray | Image.Image | None) -> tuple[str, Image.Image, str, str, gr.update, gr.update, str]:
270
- if not self.is_running:
271
- return (
272
- f'<div style="font-size: 3em; font-weight: bold; text-align: center; padding: 20px; background: linear-gradient(45deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 10px;">{self.current_operation}</div>',
273
- image_data,
274
- "❌ Le jeu n'est pas en cours !",
275
- "⏱️ Temps: 0s",
276
- gr.update(interactive=True),
277
- gr.update(interactive=False),
278
- ""
279
- )
280
-
281
- elapsed_time = time.time() - self.start_time
282
- if elapsed_time >= self.duration:
283
- return self.end_game(image_data)
284
-
285
- if image_data is not None:
286
- self.user_images.append(image_data)
287
- self.expected_answers.append(self.correct_answer)
288
- self.question_count += 1
289
-
290
- # Nouvelle opération
291
- operation_str, answer = self.generate_operation(self.operation_type, self.difficulty)
292
- self.current_operation = operation_str
293
- self.correct_answer = answer
294
-
295
- # Parser pour l'historique
296
- parts = operation_str.split()
297
- a, op, b = int(parts[0]), parts[1], int(parts[2])
298
- self.operations_history.append((a, b, op, answer))
299
-
300
- time_remaining = max(0, self.duration - int(elapsed_time))
301
- self.time_remaining = time_remaining
302
-
303
- if time_remaining <= 0:
304
- return self.end_game(image_data)
305
-
306
- # Emoji pour l'opération
307
- operation_emoji = {
308
- "×": "✖️", "+": "➕", "-": "➖", "÷": "➗", "Aléatoire": "🎲"
309
- }
310
- emoji = operation_emoji.get(self.operation_type, "🔢")
311
-
312
- return (
313
- f'<div style="font-size: 3em; font-weight: bold; text-align: center; padding: 20px; background: linear-gradient(45deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 10px;">{operation_str}</div>',
314
- create_white_canvas(),
315
- f"🎯 {emoji} Question {self.question_count + 1} • {self.difficulty}",
316
- f"⏱️ Temps restant: {time_remaining}s",
317
- gr.update(interactive=False),
318
- gr.update(interactive=True),
319
- ""
320
- )
321
-
322
- def end_game(self, final_image: dict | np.ndarray | Image.Image | None) -> tuple[str, Image.Image, str, str, gr.update, gr.update, str]:
323
-
324
- self.is_running = False
325
-
326
- # log_memory_usage("début end_game") # DEBUG: Désactivé
327
-
328
- if final_image is not None:
329
- self.user_images.append(final_image)
330
- self.expected_answers.append(self.correct_answer)
331
- self.question_count += 1
332
- if len(self.operations_history) < len(self.user_images):
333
- parts = self.current_operation.split()
334
- a, op, b = int(parts[0]), parts[1], int(parts[2])
335
- self.operations_history.append((a, b, op, self.correct_answer))
336
-
337
- correct_answers = 0
338
- total_questions = len(self.user_images)
339
- table_rows_html = ""
340
-
341
- session_timestamp = datetime.datetime.now().isoformat()
342
- session_id = f"session_{int(datetime.datetime.now().timestamp())}_{str(uuid.uuid4())[:8]}"
343
-
344
- self.session_data = []
345
- images_saved = 0
346
- total_image_size_kb = 0
347
-
348
- # Traitement optimisé avec DEBUG
349
- print(f"🔄 Traitement de {total_questions} images...")
350
- start_processing = time.time()
351
-
352
- for i, (image, expected, operation_data) in enumerate(zip(self.user_images, self.expected_answers, self.operations_history)):
353
- print(f" → Image {i+1}/{total_questions}...")
354
- img_start = time.time()
355
-
356
- row_data = create_result_row_with_images(i, image, expected, operation_data)
357
- table_rows_html += row_data['html_row']
358
-
359
- img_time = time.time() - img_start
360
- print(f" ✅ Traitée en {img_time:.1f}s")
361
-
362
- if row_data['is_correct']:
363
- correct_answers += 1
364
-
365
- # Structure pour NOUVEAU DATASET CALCUL OCR
366
- a, b, operation, correct_result = operation_data
367
-
368
- entry = {
369
- "session_id": session_id,
370
- "timestamp": session_timestamp,
371
- "question_number": i + 1,
372
-
373
- # Configuration session
374
- "session_duration": self.duration,
375
- "operation_type": self.operation_type,
376
- "difficulty_level": self.difficulty,
377
-
378
- # Mathématiques
379
- "operand_a": a,
380
- "operand_b": b,
381
- "operation": operation,
382
- "correct_answer": expected,
383
-
384
- # OCR & Résultats
385
- "user_answer_ocr": row_data['recognized'],
386
- "user_answer_parsed": row_data['recognized_num'],
387
- "is_correct": row_data['is_correct'],
388
-
389
- # Métadonnées
390
- "total_questions": total_questions,
391
- "app_version": "3.0_calcul_ocr"
392
- }
393
-
394
- # Ajouter image si disponible
395
- if row_data['dataset_image_data']:
396
- entry["handwriting_image"] = row_data['dataset_image_data']["image_base64"]
397
- entry["image_width"] = int(row_data['dataset_image_data']["compressed_size"][0])
398
- entry["image_height"] = int(row_data['dataset_image_data']["compressed_size"][1])
399
- entry["image_size_kb"] = float(row_data['dataset_image_data']["file_size_kb"])
400
- entry["has_image"] = True
401
- images_saved += 1
402
- total_image_size_kb += row_data['dataset_image_data']["file_size_kb"]
403
- else:
404
- entry["has_image"] = False
405
-
406
- self.session_data.append(entry)
407
-
408
- processing_time = time.time() - start_processing
409
- print(f"⏱️ Traitement total: {processing_time:.1f}s")
410
-
411
- accuracy = (correct_answers / total_questions * 100) if total_questions > 0 else 0
412
-
413
- for entry in self.session_data:
414
- entry["session_accuracy"] = accuracy
415
-
416
- # Nettoyage mémoire
417
- for img in self.user_images:
418
- if hasattr(img, 'close'):
419
- try:
420
- img.close()
421
- except:
422
- pass
423
-
424
- gc.collect()
425
- # log_memory_usage("après nettoyage end_game") # DEBUG: Désactivé
426
-
427
- # HTML résultats
428
- table_html = f"""
429
- <div style="overflow-x: auto; margin: 20px 0;">
430
- <table style="width: 100%; border-collapse: collapse; border: 2px solid #4a90e2;">
431
- <thead>
432
- <tr style="background: #4a90e2; color: white;">
433
- <th style="padding: 8px;">Question</th>
434
- <th style="padding: 8px;">A</th>
435
- <th style="padding: 8px;">Op</th>
436
- <th style="padding: 8px;">B</th>
437
- <th style="padding: 8px;">Réponse</th>
438
- <th style="padding: 8px;">Votre dessin</th>
439
- <th style="padding: 8px;">OCR</th>
440
- <th style="padding: 8px;">Statut</th>
441
- </tr>
442
- </thead>
443
- <tbody>
444
- {table_rows_html}
445
- </tbody>
446
- </table>
447
- </div>
448
- """
449
-
450
- # Configuration session pour affichage
451
- config_display = f"{self.operation_type} • {self.difficulty} • {self.duration}s"
452
- operation_emoji = {
453
- "×": "✖️", "+": "➕", "-": "➖", "÷": "➗", "Aléatoire": "🎲"
454
- }
455
- emoji = operation_emoji.get(self.operation_type, "🔢")
456
-
457
- export_info = self.get_export_status()
458
- if export_info["can_export"]:
459
- export_section = f"""
460
- <div style="margin-top: 20px; padding: 15px; background-color: #e8f5e8; border-radius: 8px;">
461
- <h3 style="color: #2e7d32;">📤 Ajouter cette série au dataset ?</h3>
462
- <p style="color: #2e7d32;">
463
- ✅ {total_questions} réponses • 📊 {accuracy:.1f}% de précision<br>
464
- 📸 {images_saved} opérations et images sauvegardées ({total_image_size_kb:.1f}KB)<br>
465
- ⚙️ Configuration: {config_display}
466
- </p>
467
- </div>
468
- """
469
- else:
470
- export_section = ""
471
-
472
- final_results = f"""
473
- <div style="margin: 20px 0;">
474
- <h1 style="text-align: center; color: #4a90e2;">🎉 Session terminée !</h1>
475
- <div style="background: linear-gradient(45deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 10px; margin: 20px 0;">
476
- <h2>📈 Résultats</h2>
477
- <div style="text-align: center; margin-bottom: 15px;">
478
- <strong>{emoji} {config_display}</strong>
479
- </div>
480
- <div style="display: flex; justify-content: space-around; flex-wrap: wrap;">
481
- <div style="text-align: center; margin: 10px;">
482
- <div style="font-size: 2em; font-weight: bold;">{total_questions}</div>
483
- <div>Questions</div>
484
- </div>
485
- <div style="text-align: center; margin: 10px;">
486
- <div style="font-size: 2em; font-weight: bold; color: #90EE90;">{correct_answers}</div>
487
- <div>Correctes</div>
488
- </div>
489
- <div style="text-align: center; margin: 10px;">
490
- <div style="font-size: 2em; font-weight: bold; color: #FFB6C1;">{total_questions - correct_answers}</div>
491
- <div>Incorrectes</div>
492
- </div>
493
- <div style="text-align: center; margin: 10px;">
494
- <div style="font-size: 2em; font-weight: bold;">{accuracy:.1f}%</div>
495
- <div>Précision</div>
496
- </div>
497
- </div>
498
- </div>
499
- <h2 style="color: #4a90e2;">📊 Détail des Réponses</h2>
500
- {table_html}
501
- {export_section}
502
- </div>
503
- """
504
-
505
- return (
506
- """<div style="font-size: 3em; font-weight: bold; text-align: center; padding: 20px; background: linear-gradient(45deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 10px;">🏁 C'est fini !</div>""",
507
- create_white_canvas(),
508
- f"✨ Session {config_display} terminée !",
509
- "⏱️ Temps écoulé !",
510
- gr.update(interactive=True),
511
- gr.update(interactive=False),
512
- final_results
513
- )
514
-
515
-
516
- def export_to_clean_dataset(session_data: list[dict], dataset_name: str = DATASET_NAME) -> str:
517
- """Export vers le nouveau dataset calcul_ocr_dataset"""
518
- if not DATASET_AVAILABLE:
519
- return "❌ Modules dataset non disponibles"
520
-
521
- hf_token = os.getenv("HF_TOKEN") or os.getenv("tk_calcul_ocr") # Support des deux noms
522
- if not hf_token:
523
- return "❌ Token HuggingFace manquant (HF_TOKEN ou tk_calcul_ocr)"
524
-
525
- try:
526
- print(f"\n🚀 === EXPORT VERS DATASET CALCUL OCR ===")
527
- print(f"📊 Dataset: {dataset_name}")
528
-
529
- # Filtrer les entrées avec images
530
- clean_entries = []
531
-
532
- for entry in session_data:
533
- if entry.get('has_image', False):
534
- clean_entries.append(entry)
535
-
536
- print(f"✅ {len(clean_entries)} entrées avec images converties")
537
-
538
- if len(clean_entries) == 0:
539
- return "❌ Aucune entrée avec image à exporter"
540
-
541
- # Charger dataset existant OU créer nouveau
542
- try:
543
- existing_dataset = load_dataset(dataset_name, split="train")
544
- existing_data = existing_dataset.to_list()
545
- print(f"📊 {len(existing_data)} entrées existantes")
546
- except:
547
- existing_data = []
548
- print("📊 Création nouveau dataset calcul_ocr")
549
-
550
- # Combiner
551
- combined_data = existing_data + clean_entries
552
- clean_dataset = Dataset.from_list(combined_data)
553
-
554
- print(f"✅ Dataset créé - Features:")
555
- for feature_name in clean_dataset.features:
556
- print(f" - {feature_name}: {clean_dataset.features[feature_name]}")
557
-
558
- # Statistiques par opération
559
- operations_count = {}
560
- for entry in clean_entries:
561
- op = entry.get('operation_type', 'unknown')
562
- operations_count[op] = operations_count.get(op, 0) + 1
563
-
564
- operations_summary = ", ".join([f"{op}: {count}" for op, count in operations_count.items()])
565
-
566
- # Push vers HuggingFace
567
- print(f"📤 Push vers {dataset_name}...")
568
- clean_dataset.push_to_hub(
569
- dataset_name,
570
- private=False,
571
- token=hf_token,
572
- commit_message=f"Add {len(clean_entries)} handwriting samples for math OCR ({operations_summary})"
573
- )
574
-
575
- cleanup_memory()
576
-
577
- success_message = f"""✅ Session ajoutée au dataset avec succès !
578
-
579
- 📊 Dataset: {dataset_name}
580
- 📸 Images: {len(clean_entries)}
581
- 🔢 Opérations: {operations_summary}
582
- 📈 Total: {len(clean_dataset)}
583
-
584
- 🔗 Le dataset est consultable ici : https://huggingface.co/datasets/{dataset_name}"""
585
-
586
- return success_message
587
-
588
- except Exception as e:
589
- print(f"❌ ERREUR: {e}")
590
- import traceback
591
- traceback.print_exc()
592
- error_message = f"❌ Erreur: {str(e)}"
593
- return error_message