| import os |
| import json |
| import uuid |
| import threading |
| import time |
| from datetime import datetime |
| from flask import Flask, request, render_template, jsonify, send_file |
| from flask_socketio import SocketIO, emit |
| from werkzeug.utils import secure_filename |
| import base64 |
| from io import BytesIO |
| from PIL import Image |
| import pytesseract |
| from crewai import Agent, Task, Crew, Process, LLM |
| import re |
| import logging |
|
|
| |
| app = Flask(__name__) |
| app.config['SECRET_KEY'] = 'votre_cle_secrete_ici' |
| app.config['UPLOAD_FOLDER'] = 'uploads' |
| app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 |
|
|
| |
| socketio = SocketIO(app, cors_allowed_origins="*") |
|
|
| |
| os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) |
| os.makedirs('solutions', exist_ok=True) |
|
|
| |
| ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff'} |
|
|
| |
| os.environ["GEMINI_API_KEY"] = os.environ.get("GEMINI_API_KEY", "AIzaSyAMYpF67aqFnWDJESWOx1dC-w3sEU29VcM") |
|
|
| |
| active_tasks = {} |
|
|
| |
| llm = LLM( |
| model="gemini/gemini-2.5-flash", |
| temperature=0.1, |
| |
| ) |
|
|
| |
| MAX_ITERATIONS = 5 |
| PASSES_NEEDED_FOR_SUCCESS = 2 |
|
|
| |
| |
| |
|
|
| def allowed_file(filename): |
| return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS |
|
|
| def extract_text_from_image(image_path): |
| """Extrait le texte d'une image en utilisant OCR.""" |
| try: |
| |
| image = Image.open(image_path) |
| |
| |
| if image.mode != 'RGB': |
| image = image.convert('RGB') |
| |
| |
| text = pytesseract.image_to_string(image, lang='eng') |
| |
| return text.strip() |
| except Exception as e: |
| return f"Erreur lors de l'extraction du texte: {str(e)}" |
|
|
| def emit_log(task_id, level, message): |
| """Émet un log vers le client via WebSocket.""" |
| log_data = { |
| 'timestamp': datetime.now().strftime('%H:%M:%S'), |
| 'level': level, |
| 'message': message |
| } |
| socketio.emit('log_update', {'task_id': task_id, 'log': log_data}) |
|
|
| |
| |
| |
|
|
| def get_imo_paper_solver_prompt(problem_statement, hint=""): |
| full_problem = f"{problem_statement}\n{hint}" if hint else problem_statement |
| return f""" |
| ### Core Instructions ### |
| **Rigor is Paramount:** Your primary goal is to produce a complete and rigorously justified solution. |
| You are solving an International Mathematical Olympiad (IMO) level problem. |
| |
| ### Problem ### |
| {full_problem} |
| |
| ### Your Task ### |
| Provide a complete, rigorous mathematical proof. Structure your solution clearly with: |
| 1. A brief summary of your approach |
| 2. A detailed step-by-step proof |
| 3. Clear justification for each step |
| """ |
|
|
| def get_self_improve_prompt(solution_attempt): |
| return f""" |
| You are a world-class mathematician. You have just produced the following draft solution. |
| Review it carefully for flaws, gaps, or unclear parts, then produce a new, improved, and more rigorous version. |
| |
| ### Draft Solution ### |
| {solution_attempt} |
| |
| ### Your Task ### |
| Provide the improved and finalized version of the solution. Only output the final, clean proof. |
| """ |
|
|
| def get_imo_paper_verifier_prompt(problem_statement, solution_to_verify): |
| return f""" |
| You are an expert mathematician and a meticulous grader for an IMO level exam. |
| |
| ### Problem ### |
| {problem_statement} |
| |
| ### Solution ### |
| {solution_to_verify} |
| |
| ### Verification Task ### |
| Act as an IMO grader. Analyze the solution for: |
| 1. Mathematical correctness |
| 2. Logical gaps or errors |
| 3. Clarity and rigor |
| |
| Provide your verdict in the format: |
| **Final Verdict:** [The solution is correct / Critical error found / Justification gaps found] |
| |
| Then provide detailed feedback on any issues found. |
| """ |
|
|
| def get_correction_prompt(solution_attempt, verification_report): |
| return f""" |
| You are a brilliant mathematician. Your previous solution attempt has been reviewed by a verifier. |
| Your task is to write a new, corrected version of your solution that addresses all the issues raised. |
| |
| ### Verification Report ### |
| {verification_report} |
| |
| ### Your Previous Solution ### |
| {solution_attempt} |
| |
| ### Your Task ### |
| Provide a new, complete, and rigorously correct solution that fixes all identified issues. |
| """ |
|
|
| |
| |
| |
|
|
| solver_agent = Agent( |
| role='Mathématicien de classe mondiale et résolveur de problèmes créatifs', |
| goal='Générer et affiner des preuves mathématiques complètes et rigoureuses pour des problèmes de niveau Olympiades.', |
| backstory="""Médaillé d'or des OIM, vous êtes connu pour votre capacité à trouver des solutions |
| à la fois non conventionnelles et impeccablement rigoureuses. Vous excellez à décomposer |
| des problèmes complexes et à présenter des arguments clairs, étape par étape.""", |
| verbose=True, |
| allow_delegation=False, |
| llm=llm |
| ) |
|
|
| verifier_agent = Agent( |
| role='Examinateur méticuleux pour les Olympiades Internationales de Mathématiques', |
| goal='Analyser de manière critique une preuve mathématique pour y trouver la moindre erreur logique ou le moindre manque de justification.', |
| backstory="""Professeur de mathématiques réputé, membre du comité de notation des OIM. Votre devise est 'la rigueur avant tout'. |
| Vous ne corrigez jamais, vous ne faites que juger et rapporter les failles.""", |
| verbose=True, |
| allow_delegation=False, |
| llm=llm |
| ) |
|
|
| |
| |
| |
|
|
| def parse_verifier_verdict(report): |
| if not report: |
| return "ERROR" |
| |
| report_text = str(report) |
| match = re.search(r"\*\*Final Verdict:\*\* (.*)", report_text, re.IGNORECASE) |
| if not match: |
| return "UNKNOWN" |
| |
| verdict_text = match.group(1).lower() |
| if "the solution is correct" in verdict_text: |
| return "CORRECT" |
| if "critical error" in verdict_text: |
| return "CRITICAL_ERROR" |
| if "justification gap" in verdict_text: |
| return "GAPS" |
| return "UNKNOWN" |
|
|
| def run_imo_pipeline_async(task_id, problem_statement, initial_hint=""): |
| """Version asynchrone du pipeline avec émission de logs.""" |
| try: |
| emit_log(task_id, 'info', "🚀 Démarrage du Pipeline de Résolution Mathématique") |
| |
| |
| emit_log(task_id, 'info', "--- ÉTAPE 1: Génération de la Solution Initiale ---") |
| |
| initial_task = Task( |
| description=get_imo_paper_solver_prompt(problem_statement, initial_hint), |
| expected_output="Une ébauche de preuve mathématique structurée avec un résumé et une solution détaillée.", |
| agent=solver_agent |
| ) |
| |
| initial_crew = Crew( |
| agents=[solver_agent], |
| tasks=[initial_task], |
| process=Process.sequential, |
| verbose=False |
| ) |
| |
| initial_result = initial_crew.kickoff() |
| initial_solution = initial_result.raw if hasattr(initial_result, 'raw') else str(initial_result) |
| |
| emit_log(task_id, 'success', "✅ Solution initiale générée") |
| |
| |
| emit_log(task_id, 'info', "--- ÉTAPE 2: Auto-Amélioration de la Solution ---") |
| |
| improve_task = Task( |
| description=get_self_improve_prompt(initial_solution), |
| expected_output="Une version améliorée et plus rigoureuse de la preuve.", |
| agent=solver_agent |
| ) |
| |
| improve_crew = Crew( |
| agents=[solver_agent], |
| tasks=[improve_task], |
| process=Process.sequential, |
| verbose=False |
| ) |
| |
| improve_result = improve_crew.kickoff() |
| current_solution = improve_result.raw if hasattr(improve_result, 'raw') else str(improve_result) |
| |
| emit_log(task_id, 'success', "✅ Solution améliorée") |
| |
| |
| iteration = 0 |
| consecutive_passes = 0 |
| |
| while iteration < MAX_ITERATIONS: |
| iteration += 1 |
| emit_log(task_id, 'info', f"--- CYCLE DE VÉRIFICATION {iteration}/{MAX_ITERATIONS} ---") |
|
|
| verify_task = Task( |
| description=get_imo_paper_verifier_prompt(problem_statement, current_solution), |
| expected_output="Un rapport de vérification complet au format OIM.", |
| agent=verifier_agent |
| ) |
| |
| correct_task = Task( |
| description=get_correction_prompt(current_solution, "Rapport du vérificateur (voir contexte)"), |
| expected_output="Une nouvelle version complète et corrigée de la preuve mathématique.", |
| agent=solver_agent, |
| context=[verify_task] |
| ) |
| |
| correction_crew = Crew( |
| agents=[verifier_agent, solver_agent], |
| tasks=[verify_task, correct_task], |
| process=Process.sequential, |
| verbose=False |
| ) |
| |
| cycle_result = correction_crew.kickoff() |
| |
| if hasattr(cycle_result, 'tasks_output') and len(cycle_result.tasks_output) >= 2: |
| verification_report = cycle_result.tasks_output[0].raw |
| corrected_solution = cycle_result.tasks_output[1].raw |
| else: |
| verification_report = str(cycle_result) |
| corrected_solution = str(cycle_result) |
| |
| verdict = parse_verifier_verdict(verification_report) |
| |
| emit_log(task_id, 'info', f"VERDICT DU CYCLE {iteration}: {verdict}") |
| |
| if verdict == "CORRECT": |
| consecutive_passes += 1 |
| emit_log(task_id, 'success', f"✅ PASSAGE RÉUSSI ! Passes consécutives : {consecutive_passes}/{PASSES_NEEDED_FOR_SUCCESS}") |
| if consecutive_passes >= PASSES_NEEDED_FOR_SUCCESS: |
| emit_log(task_id, 'success', "🏆 La solution a été validée avec succès ! 🏆") |
| |
| |
| solution_file = f"solutions/solution_{task_id}.txt" |
| with open(solution_file, 'w', encoding='utf-8') as f: |
| f.write(f"Problème:\n{problem_statement}\n\n") |
| f.write(f"Solution finale:\n{current_solution}") |
| |
| active_tasks[task_id]['status'] = 'completed' |
| active_tasks[task_id]['solution_file'] = solution_file |
| |
| socketio.emit('task_completed', {'task_id': task_id}) |
| return current_solution |
| else: |
| consecutive_passes = 0 |
| emit_log(task_id, 'warning', "⚠️ Problèmes détectés. Correction en cours...") |
| current_solution = corrected_solution |
|
|
| emit_log(task_id, 'error', f"❌ Échec après {MAX_ITERATIONS} itérations") |
| |
| |
| solution_file = f"solutions/solution_{task_id}_partial.txt" |
| with open(solution_file, 'w', encoding='utf-8') as f: |
| f.write(f"Problème:\n{problem_statement}\n\n") |
| f.write(f"Solution partielle (non validée):\n{current_solution}") |
| |
| active_tasks[task_id]['status'] = 'failed' |
| active_tasks[task_id]['solution_file'] = solution_file |
| |
| socketio.emit('task_failed', {'task_id': task_id}) |
| return current_solution |
| |
| except Exception as e: |
| emit_log(task_id, 'error', f"❌ Erreur critique: {str(e)}") |
| active_tasks[task_id]['status'] = 'error' |
| active_tasks[task_id]['error'] = str(e) |
| socketio.emit('task_error', {'task_id': task_id, 'error': str(e)}) |
|
|
| |
| |
| |
|
|
| @app.route('/') |
| def index(): |
| return render_template('index.html') |
|
|
| @app.route('/upload', methods=['POST']) |
| def upload_file(): |
| if 'file' not in request.files: |
| return jsonify({'error': 'Aucun fichier fourni'}), 400 |
| |
| file = request.files['file'] |
| if file.filename == '': |
| return jsonify({'error': 'Aucun fichier sélectionné'}), 400 |
| |
| if file and allowed_file(file.filename): |
| |
| task_id = str(uuid.uuid4()) |
| |
| |
| filename = secure_filename(file.filename) |
| filepath = os.path.join(app.config['UPLOAD_FOLDER'], f"{task_id}_{filename}") |
| file.save(filepath) |
| |
| |
| try: |
| extracted_text = extract_text_from_image(filepath) |
| |
| |
| active_tasks[task_id] = { |
| 'status': 'processing', |
| 'problem': extracted_text, |
| 'started_at': datetime.now(), |
| 'filename': filename |
| } |
| |
| |
| thread = threading.Thread( |
| target=run_imo_pipeline_async, |
| args=(task_id, extracted_text) |
| ) |
| thread.daemon = True |
| thread.start() |
| |
| return jsonify({ |
| 'task_id': task_id, |
| 'extracted_text': extracted_text, |
| 'message': 'Traitement commencé' |
| }) |
| |
| except Exception as e: |
| return jsonify({'error': f'Erreur lors du traitement de l\'image: {str(e)}'}), 500 |
| |
| return jsonify({'error': 'Type de fichier non autorisé'}), 400 |
|
|
| @app.route('/status/<task_id>') |
| def get_status(task_id): |
| if task_id not in active_tasks: |
| return jsonify({'error': 'Tâche non trouvée'}), 404 |
| |
| task = active_tasks[task_id] |
| return jsonify({ |
| 'task_id': task_id, |
| 'status': task['status'], |
| 'problem': task.get('problem', ''), |
| 'started_at': task['started_at'].isoformat(), |
| 'has_solution': 'solution_file' in task |
| }) |
|
|
| @app.route('/download/<task_id>') |
| def download_solution(task_id): |
| if task_id not in active_tasks: |
| return jsonify({'error': 'Tâche non trouvée'}), 404 |
| |
| task = active_tasks[task_id] |
| if 'solution_file' not in task: |
| return jsonify({'error': 'Aucune solution disponible'}), 404 |
| |
| return send_file(task['solution_file'], as_attachment=True, download_name=f'solution_{task_id}.txt') |
|
|
| @app.route('/tasks') |
| def list_tasks(): |
| tasks_info = [] |
| for task_id, task in active_tasks.items(): |
| tasks_info.append({ |
| 'task_id': task_id, |
| 'status': task['status'], |
| 'filename': task.get('filename', ''), |
| 'started_at': task['started_at'].isoformat(), |
| 'has_solution': 'solution_file' in task |
| }) |
| return jsonify(tasks_info) |
|
|
| |
| |
| |
|
|
| @app.route('/template') |
| def get_template(): |
| template_content = ''' |
| <!DOCTYPE html> |
| <html lang="fr"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Résolveur Mathématique IA</title> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.5/socket.io.min.js"></script> |
| <link rel="preconnect" href="https://fonts.googleapis.com"> |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> |
| <style> |
| /* --- Reset & Variables --- */ |
| :root { |
| --bg-color: #f4f7fa; |
| --main-color: #ffffff; |
| --primary-color: #3b82f6; |
| --primary-hover: #2563eb; |
| --text-color: #1f2937; |
| --text-light: #6b7280; |
| --border-color: #e5e7eb; |
| --dark-bg: #111827; |
| --success-color: #10b981; |
| --warning-color: #f59e0b; |
| --error-color: #ef4444; |
| --shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); |
| --border-radius: 0.75rem; |
| } |
| |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| } |
| |
| body { |
| font-family: 'Inter', sans-serif; |
| background-color: var(--bg-color); |
| color: var(--text-color); |
| line-height: 1.6; |
| padding: 2rem; |
| } |
| |
| /* --- Layout & Main Container --- */ |
| .container { |
| max-width: 1200px; |
| margin: 0 auto; |
| background-color: var(--main-color); |
| border-radius: var(--border-radius); |
| box-shadow: var(--shadow); |
| overflow: hidden; |
| } |
| |
| /* --- Header --- */ |
| .header { |
| padding: 2.5rem; |
| text-align: center; |
| border-bottom: 1px solid var(--border-color); |
| background: linear-gradient(to top, #ffffff, #f9fafb); |
| } |
| .header h1 { |
| font-size: 2.25rem; |
| font-weight: 700; |
| letter-spacing: -0.025em; |
| color: var(--text-color); |
| } |
| .header h1 .icon { |
| color: var(--primary-color); |
| } |
| .header p { |
| margin-top: 0.5rem; |
| color: var(--text-light); |
| font-size: 1.1rem; |
| max-width: 600px; |
| margin-left: auto; |
| margin-right: auto; |
| } |
| |
| /* --- Upload Section --- */ |
| .main-content { |
| padding: 2.5rem; |
| } |
| |
| .upload-area { |
| border: 2px dashed var(--border-color); |
| border-radius: var(--border-radius); |
| padding: 3rem; |
| text-align: center; |
| background-color: #fcfdff; |
| transition: all 0.3s ease; |
| } |
| .upload-area.dragover { |
| border-color: var(--primary-color); |
| background-color: #eff6ff; |
| } |
| .upload-icon { |
| font-size: 3rem; |
| color: var(--primary-color); |
| margin-bottom: 1rem; |
| } |
| .upload-area p { |
| color: var(--text-light); |
| margin-bottom: 1.5rem; |
| } |
| |
| /* --- Main Button --- */ |
| .btn { |
| display: inline-block; |
| background-color: var(--primary-color); |
| color: white; |
| padding: 0.8rem 2rem; |
| border: none; |
| border-radius: 9999px; /* pill shape */ |
| font-size: 1rem; |
| font-weight: 500; |
| cursor: pointer; |
| text-decoration: none; |
| transition: background-color 0.2s ease, transform 0.2s ease; |
| } |
| .btn:hover { |
| background-color: var(--primary-hover); |
| transform: translateY(-2px); |
| } |
| |
| /* --- Results & Logs Section --- */ |
| .results-grid { |
| margin-top: 2.5rem; |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); |
| gap: 2rem; |
| } |
| |
| .panel { |
| background-color: #f9fafb; |
| border: 1px solid var(--border-color); |
| border-radius: var(--border-radius); |
| padding: 1.5rem; |
| } |
| .panel h3 { |
| font-size: 1.25rem; |
| font-weight: 600; |
| margin-bottom: 1rem; |
| border-bottom: 1px solid var(--border-color); |
| padding-bottom: 0.75rem; |
| } |
| |
| #extractedText { |
| white-space: pre-wrap; |
| word-wrap: break-word; |
| font-family: 'Courier New', monospace; |
| background-color: var(--main-color); |
| padding: 1rem; |
| border-radius: 0.5rem; |
| max-height: 450px; |
| overflow-y: auto; |
| color: var(--text-light); |
| } |
| |
| /* --- Logs --- */ |
| #logContainer { |
| height: 450px; |
| overflow-y: auto; |
| background: var(--dark-bg); |
| color: #d1d5db; |
| padding: 1rem; |
| border-radius: 0.5rem; |
| font-family: 'Courier New', monospace; |
| font-size: 0.875rem; |
| } |
| .log-entry { |
| margin-bottom: 0.25rem; |
| padding: 0.2rem 0.5rem; |
| border-radius: 3px; |
| } |
| .log-timestamp { color: #9ca3af; margin-right: 0.5rem; } |
| .log-info { color: #3b82f6; } |
| .log-success { color: #10b981; } |
| .log-warning { color: #f59e0b; } |
| .log-error { color: #ef4444; font-weight: bold; } |
| |
| /* --- Status Bar & Download --- */ |
| #status-section { |
| padding: 1.5rem 2.5rem; |
| background-color: #f9fafb; |
| border-top: 1px solid var(--border-color); |
| text-align: center; |
| } |
| .status-badge { |
| display: inline-flex; |
| align-items: center; |
| gap: 0.5rem; |
| padding: 0.5rem 1.25rem; |
| border-radius: 9999px; |
| font-weight: 500; |
| } |
| .status-processing { background-color: #fef3c7; color: #92400e; } |
| .status-completed { background-color: #d1fae5; color: #065f46; } |
| .status-failed { background-color: #fee2e2; color: #991b1b; } |
| |
| #downloadSection { |
| margin-top: 1rem; |
| } |
| |
| .hidden { |
| display: none; |
| } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <header class="header"> |
| <h1><span class="icon">🧮</span> Résolveur Mathématique IA</h1> |
| <p>Soumettez une image d'un problème mathématique et laissez nos agents IA générer une solution rigoureuse, étape par étape.</p> |
| </header> |
| |
| <main class="main-content"> |
| <!-- Section d'upload corrigée --> |
| <section id="upload-section"> |
| <div class="upload-area" id="uploadArea"> |
| <div class="upload-icon">📄</div> |
| <p>Glissez-déposez votre image ici, ou cliquez sur le bouton pour la sélectionner.</p> |
| <!-- Ce bouton est maintenant le point d'interaction principal pour le clic --> |
| <button type="button" class="btn" id="selectFileBtn">Choisir un fichier</button> |
| <!-- L'input de fichier est caché et sera activé par le JS --> |
| <input type="file" id="fileInput" accept="image/*" class="hidden"> |
| </div> |
| </section> |
| |
| <!-- Section pour afficher le statut global, cachée au début --> |
| <section id="status-section" class="hidden"> |
| <div id="statusBadge" class="status-badge"></div> |
| <div id="downloadSection" class="hidden"> |
| <a href="#" id="downloadBtn" class="btn">📥 Télécharger la solution</a> |
| </div> |
| </section> |
| |
| <!-- Grille pour les résultats, cachée au début --> |
| <section id="results-grid" class="results-grid hidden"> |
| <div class="panel"> |
| <h3>📝 Texte extrait de l'image</h3> |
| <div id="extractedText">Le texte de votre image apparaîtra ici...</div> |
| </div> |
| <div class="panel"> |
| <h3>📊 Journal de traitement</h3> |
| <div id="logContainer"></div> |
| </div> |
| </section> |
| |
| </main> |
| </div> |
| |
| <script> |
| const socket = io(); |
| let currentTaskId = null; |
| |
| // --- DOM Elements --- |
| const uploadArea = document.getElementById('uploadArea'); |
| const fileInput = document.getElementById('fileInput'); |
| const selectFileBtn = document.getElementById('selectFileBtn'); |
| const statusSection = document.getElementById('status-section'); |
| const statusBadge = document.getElementById('statusBadge'); |
| const resultsGrid = document.getElementById('results-grid'); |
| const extractedTextEl = document.getElementById('extractedText'); |
| const logContainer = document.getElementById('logContainer'); |
| const downloadSection = document.getElementById('downloadSection'); |
| const downloadBtn = document.getElementById('downloadBtn'); |
| |
| // --- Event Listeners --- |
| |
| // CORRECTION : Le bouton déclenche l'input de fichier |
| selectFileBtn.addEventListener('click', () => { |
| fileInput.click(); |
| }); |
| |
| // L'input de fichier, une fois un fichier choisi, lance l'upload |
| fileInput.addEventListener('change', (e) => { |
| if (e.target.files.length > 0) { |
| handleFileUpload(e.target.files[0]); |
| } |
| }); |
| |
| // Gestion du glisser-déposer (Drag & Drop) |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { |
| uploadArea.addEventListener(eventName, preventDefaults, false); |
| }); |
| function preventDefaults(e) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| } |
| |
| ['dragenter', 'dragover'].forEach(eventName => { |
| uploadArea.addEventListener(eventName, () => uploadArea.classList.add('dragover'), false); |
| }); |
| ['dragleave', 'drop'].forEach(eventName => { |
| uploadArea.addEventListener(eventName, () => uploadArea.classList.remove('dragover'), false); |
| }); |
| |
| uploadArea.addEventListener('drop', (e) => { |
| const files = e.dataTransfer.files; |
| if (files.length > 0) { |
| handleFileUpload(files[0]); |
| } |
| }); |
| |
| // --- Core Functions --- |
| |
| function resetUI() { |
| statusSection.classList.add('hidden'); |
| resultsGrid.classList.add('hidden'); |
| downloadSection.classList.add('hidden'); |
| logContainer.innerHTML = ''; |
| extractedTextEl.textContent = 'Le texte de votre image apparaîtra ici...'; |
| currentTaskId = null; |
| } |
| |
| function handleFileUpload(file) { |
| resetUI(); |
| const formData = new FormData(); |
| formData.append('file', file); |
| |
| updateStatus('processing', '🚀 Envoi et analyse de l\'image...'); |
| statusSection.classList.remove('hidden'); |
| |
| fetch('/upload', { |
| method: 'POST', |
| body: formData |
| }) |
| .then(response => { |
| if (!response.ok) { |
| throw new Error(`Erreur serveur: ${response.statusText}`); |
| } |
| return response.json(); |
| }) |
| .then(data => { |
| if (data.error) { |
| throw new Error(data.error); |
| } |
| currentTaskId = data.task_id; |
| extractedTextEl.textContent = data.extracted_text; |
| resultsGrid.classList.remove('hidden'); |
| updateStatus('processing', '🧠 Résolution en cours...'); |
| }) |
| .catch(error => { |
| updateStatus('failed', `❌ Erreur critique : ${error.message}`); |
| console.error('Upload Error:', error); |
| }); |
| } |
| |
| function updateStatus(status, message) { |
| statusBadge.className = `status-badge status-${status}`; |
| statusBadge.textContent = message; |
| } |
| |
| function addLog(timestamp, level, message) { |
| const logEntry = document.createElement('div'); |
| logEntry.className = `log-entry log-${level}`; |
| logEntry.innerHTML = `<span class="log-timestamp">[${timestamp}]</span> ${message}`; |
| logContainer.appendChild(logEntry); |
| logContainer.scrollTop = logContainer.scrollHeight; |
| } |
| |
| // --- WebSocket Event Handlers --- |
| |
| socket.on('log_update', (data) => { |
| if (data.task_id === currentTaskId) { |
| addLog(data.log.timestamp, data.log.level, data.log.message); |
| } |
| }); |
| |
| socket.on('task_completed', (data) => { |
| if (data.task_id === currentTaskId) { |
| updateStatus('completed', '✅ Solution générée avec succès !'); |
| downloadSection.classList.remove('hidden'); |
| downloadBtn.href = `/download/${currentTaskId}`; |
| } |
| }); |
| |
| socket.on('task_failed', (data) => { |
| if (data.task_id === currentTaskId) { |
| updateStatus('failed', '⚠️ Échec de la résolution (solution partielle disponible)'); |
| downloadSection.classList.remove('hidden'); |
| downloadBtn.href = `/download/${currentTaskId}`; |
| } |
| }); |
| |
| socket.on('task_error', (data) => { |
| if (data.task_id === currentTaskId) { |
| updateStatus('failed', `❌ Erreur système: ${data.error}`); |
| } |
| }); |
| </script> |
| </body> |
| </html> |
| |
| |
| ''' |
| return template_content |
|
|
| if __name__ == '__main__': |
| |
| os.makedirs('templates', exist_ok=True) |
| with open('templates/index.html', 'w', encoding='utf-8') as f: |
| |
| from flask import Flask |
| temp_app = Flask(__name__) |
| with temp_app.app_context(): |
| template_content = get_template() |
| f.write(template_content) |
| |
| print("🚀 Démarrage de l'application Flask...") |
| print("📝 Template HTML créé dans templates/index.html") |
| print("🌐 Application disponible sur http://localhost:5000") |
| |
| socketio.run(app, debug=True, host='0.0.0.0', port=5000) |