Update app.py
Browse files
app.py
CHANGED
|
@@ -11,16 +11,15 @@ import tempfile
|
|
| 11 |
import subprocess
|
| 12 |
import shutil
|
| 13 |
import re
|
|
|
|
| 14 |
from flask import Flask, render_template, request, jsonify, Response, stream_with_context, send_from_directory
|
| 15 |
from google import genai
|
| 16 |
from google.genai import types
|
| 17 |
from PIL import Image
|
| 18 |
|
| 19 |
-
# ---
|
| 20 |
-
# Configuration d'un logger qui écrit dans la console (stdout).
|
| 21 |
-
# C'est la pratique recommandée pour les applications conteneurisées (Docker) ou déployées sur des services comme Heroku/Render.
|
| 22 |
logging.basicConfig(
|
| 23 |
-
level=logging.INFO,
|
| 24 |
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 25 |
datefmt='%Y-%m-%d %H:%M:%S'
|
| 26 |
)
|
|
@@ -34,6 +33,7 @@ GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
|
|
| 34 |
TELEGRAM_BOT_TOKEN = "8004545342:AAGcZaoDjYg8dmbbXRsR1N3TfSSbEiAGz88"
|
| 35 |
TELEGRAM_CHAT_ID = "-1002564204301"
|
| 36 |
GENERATED_PDF_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'generated_pdfs')
|
|
|
|
| 37 |
|
| 38 |
# --- Initialisation des Services Externes ---
|
| 39 |
client = None
|
|
@@ -71,8 +71,6 @@ def check_latex_installation():
|
|
| 71 |
"""Vérifie si pdflatex est installé et accessible dans le PATH."""
|
| 72 |
logger.info("Vérification de l'installation de LaTeX (pdflatex)...")
|
| 73 |
try:
|
| 74 |
-
# Exécute 'pdflatex -version' pour vérifier son existence.
|
| 75 |
-
# capture_output=True masque la sortie, check=True lève une exception en cas d'échec.
|
| 76 |
subprocess.run(["pdflatex", "-version"], capture_output=True, check=True, timeout=10)
|
| 77 |
logger.info("Vérification réussie: pdflatex est installé et fonctionnel.")
|
| 78 |
return True
|
|
@@ -82,16 +80,89 @@ def check_latex_installation():
|
|
| 82 |
|
| 83 |
IS_LATEX_INSTALLED = check_latex_installation()
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
def clean_latex_code(latex_code):
|
| 86 |
"""Extrait le code LaTeX brut des blocs de code formatés (```latex ... ```)."""
|
| 87 |
logger.info("Nettoyage du code LaTeX reçu de Gemini...")
|
| 88 |
-
# Cherche un bloc de code explicite ```latex ... ```
|
| 89 |
match_latex = re.search(r"```(?:latex|tex)\s*(.*?)\s*```", latex_code, re.DOTALL | re.IGNORECASE)
|
| 90 |
if match_latex:
|
| 91 |
logger.info("Bloc de code 'latex' ou 'tex' trouvé et extrait.")
|
| 92 |
return match_latex.group(1).strip()
|
| 93 |
|
| 94 |
-
# Plan B : Cherche un bloc de code générique qui commence par \documentclass
|
| 95 |
match_generic = re.search(r"```\s*(\\documentclass.*?)\s*```", latex_code, re.DOTALL | re.IGNORECASE)
|
| 96 |
if match_generic:
|
| 97 |
logger.info("Bloc de code générique avec '\\documentclass' trouvé et extrait.")
|
|
@@ -113,18 +184,15 @@ def latex_to_pdf(latex_code, output_filename_base, output_dir):
|
|
| 113 |
logger.info(f"Début de la compilation LaTeX vers PDF pour '{output_filename_base}'")
|
| 114 |
|
| 115 |
try:
|
| 116 |
-
# Écriture du fichier .tex
|
| 117 |
with open(tex_path, "w", encoding="utf-8") as tex_file:
|
| 118 |
tex_file.write(latex_code)
|
| 119 |
logger.info(f"Fichier .tex '{tex_path}' créé avec succès.")
|
| 120 |
|
| 121 |
-
# Copie de l'environnement et configuration pour UTF-8 pour éviter les erreurs d'encodage
|
| 122 |
my_env = os.environ.copy()
|
| 123 |
my_env["LC_ALL"] = "C.UTF-8"
|
| 124 |
my_env["LANG"] = "C.UTF-8"
|
| 125 |
|
| 126 |
last_result = None
|
| 127 |
-
# Exécution de pdflatex deux fois pour résoudre les références (table des matières, etc.)
|
| 128 |
for i in range(2):
|
| 129 |
logger.info(f"Exécution de pdflatex - Passe {i+1}/2...")
|
| 130 |
process = subprocess.run(
|
|
@@ -132,7 +200,6 @@ def latex_to_pdf(latex_code, output_filename_base, output_dir):
|
|
| 132 |
capture_output=True, text=True, check=False, encoding="utf-8", errors="replace", env=my_env, timeout=60
|
| 133 |
)
|
| 134 |
last_result = process
|
| 135 |
-
# Si le PDF n'est pas créé et que la première passe a échoué, inutile de continuer
|
| 136 |
if not os.path.exists(pdf_path) and process.returncode != 0:
|
| 137 |
logger.warning(f"La passe {i+1} de pdflatex a échoué et aucun PDF n'a été créé. Arrêt de la compilation.")
|
| 138 |
break
|
|
@@ -143,7 +210,7 @@ def latex_to_pdf(latex_code, output_filename_base, output_dir):
|
|
| 143 |
else:
|
| 144 |
error_log = last_result.stdout + "\n" + last_result.stderr if last_result else "Aucun résultat de compilation disponible."
|
| 145 |
logger.error(f"Échec de la compilation PDF pour '{tex_filename}'. Log de pdflatex:\n{error_log}")
|
| 146 |
-
return None, f"Erreur de compilation PDF. Log: ...{error_log[-1000:]}"
|
| 147 |
|
| 148 |
except Exception as e:
|
| 149 |
logger.error(f"Exception pendant la génération du PDF: {e}", exc_info=True)
|
|
@@ -165,12 +232,11 @@ def send_to_telegram(file_data, filename, caption="Nouveau fichier uploadé"):
|
|
| 165 |
logger.info(log_msg)
|
| 166 |
data = {'chat_id': TELEGRAM_CHAT_ID, 'caption': caption}
|
| 167 |
response = requests.post(url, files=files, data=data, timeout=30)
|
| 168 |
-
response.raise_for_status()
|
| 169 |
logger.info(f"Fichier '{filename}' envoyé avec succès à Telegram.")
|
| 170 |
except Exception as e:
|
| 171 |
logger.error(f"Erreur lors de l'envoi à Telegram: {e}", exc_info=True)
|
| 172 |
|
| 173 |
-
|
| 174 |
# --- Logique Principale (Worker en arrière-plan) ---
|
| 175 |
|
| 176 |
def process_files_background(task_id, files_data, resolution_style):
|
|
@@ -188,9 +254,13 @@ def process_files_background(task_id, files_data, resolution_style):
|
|
| 188 |
for file_info in files_data:
|
| 189 |
if file_info['type'].startswith('image/'):
|
| 190 |
logger.info(f"[Task {task_id}] Traitement de l'image '{file_info['filename']}'.")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
img = Image.open(io.BytesIO(file_info['data']))
|
| 192 |
buffered = io.BytesIO()
|
| 193 |
-
img.save(buffered, format="PNG")
|
| 194 |
img_base64_str = base64.b64encode(buffered.getvalue()).decode()
|
| 195 |
contents.append({'inline_data': {'mime_type': 'image/png', 'data': img_base64_str}})
|
| 196 |
|
|
@@ -200,11 +270,10 @@ def process_files_background(task_id, files_data, resolution_style):
|
|
| 200 |
temp_pdf.write(file_info['data'])
|
| 201 |
temp_pdf_path = temp_pdf.name
|
| 202 |
|
| 203 |
-
# Upload du fichier et ajout de la référence à la liste de nettoyage
|
| 204 |
file_ref = client.files.upload(file=temp_pdf_path)
|
| 205 |
uploaded_file_refs.append(file_ref)
|
| 206 |
contents.append(file_ref)
|
| 207 |
-
os.unlink(temp_pdf_path)
|
| 208 |
logger.info(f"[Task {task_id}] PDF '{file_info['filename']}' uploadé avec succès. Référence: {file_ref.name}")
|
| 209 |
|
| 210 |
if not contents:
|
|
@@ -238,7 +307,6 @@ def process_files_background(task_id, files_data, resolution_style):
|
|
| 238 |
cleaned_latex = clean_latex_code(full_latex_response)
|
| 239 |
logger.debug(f"[Task {task_id}] Code LaTeX nettoyé:\n---\n{cleaned_latex[:500]}...\n---")
|
| 240 |
|
| 241 |
-
|
| 242 |
task_results[task_id]['status'] = 'generating_pdf'
|
| 243 |
pdf_filename_base = f"solution_{task_id}"
|
| 244 |
pdf_file_path, pdf_message = latex_to_pdf(cleaned_latex, pdf_filename_base, GENERATED_PDF_DIR)
|
|
@@ -257,7 +325,6 @@ def process_files_background(task_id, files_data, resolution_style):
|
|
| 257 |
task_results[task_id]['error'] = str(e)
|
| 258 |
task_results[task_id]['response'] = f"Une erreur est survenue: {str(e)}"
|
| 259 |
finally:
|
| 260 |
-
# Nettoyage des fichiers uploadés à l'API Gemini
|
| 261 |
if uploaded_file_refs:
|
| 262 |
logger.info(f"[Task {task_id}] Nettoyage des {len(uploaded_file_refs)} fichiers temporaires de l'API Gemini.")
|
| 263 |
for file_ref in uploaded_file_refs:
|
|
@@ -274,6 +341,71 @@ def index():
|
|
| 274 |
logger.info(f"Requête servie pour l'endpoint '/' depuis {request.remote_addr}")
|
| 275 |
return render_template('index.html')
|
| 276 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
@app.route('/solve', methods=['POST'])
|
| 278 |
def solve():
|
| 279 |
logger.info(f"Nouvelle requête sur /solve depuis {request.remote_addr}")
|
|
@@ -296,7 +428,6 @@ def solve():
|
|
| 296 |
file_data = file.read()
|
| 297 |
file_type = file.content_type or 'application/octet-stream'
|
| 298 |
|
| 299 |
-
# Validation et traitement des fichiers
|
| 300 |
if file_type.startswith('image/'):
|
| 301 |
file_count['images'] += 1
|
| 302 |
files_data.append({'filename': file.filename, 'data': file_data, 'type': file_type})
|
|
@@ -374,7 +505,7 @@ def stream_task_progress(task_id):
|
|
| 374 |
logger.info(f"Fermeture de la connexion SSE pour la tâche terminée/échouée {task_id}")
|
| 375 |
break
|
| 376 |
|
| 377 |
-
time.sleep(1)
|
| 378 |
|
| 379 |
return Response(stream_with_context(generate()), mimetype='text/event-stream', headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
|
| 380 |
|
|
@@ -396,8 +527,9 @@ def download_pdf(task_id):
|
|
| 396 |
if __name__ == '__main__':
|
| 397 |
logger.info("Démarrage de l'application Flask.")
|
| 398 |
|
| 399 |
-
# Création
|
| 400 |
os.makedirs(GENERATED_PDF_DIR, exist_ok=True)
|
| 401 |
-
|
|
|
|
| 402 |
|
| 403 |
-
|
|
|
|
| 11 |
import subprocess
|
| 12 |
import shutil
|
| 13 |
import re
|
| 14 |
+
from datetime import datetime
|
| 15 |
from flask import Flask, render_template, request, jsonify, Response, stream_with_context, send_from_directory
|
| 16 |
from google import genai
|
| 17 |
from google.genai import types
|
| 18 |
from PIL import Image
|
| 19 |
|
| 20 |
+
# --- Configuration du Logging ---
|
|
|
|
|
|
|
| 21 |
logging.basicConfig(
|
| 22 |
+
level=logging.INFO,
|
| 23 |
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 24 |
datefmt='%Y-%m-%d %H:%M:%S'
|
| 25 |
)
|
|
|
|
| 33 |
TELEGRAM_BOT_TOKEN = "8004545342:AAGcZaoDjYg8dmbbXRsR1N3TfSSbEiAGz88"
|
| 34 |
TELEGRAM_CHAT_ID = "-1002564204301"
|
| 35 |
GENERATED_PDF_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'generated_pdfs')
|
| 36 |
+
USER_IMAGES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'user_images')
|
| 37 |
|
| 38 |
# --- Initialisation des Services Externes ---
|
| 39 |
client = None
|
|
|
|
| 71 |
"""Vérifie si pdflatex est installé et accessible dans le PATH."""
|
| 72 |
logger.info("Vérification de l'installation de LaTeX (pdflatex)...")
|
| 73 |
try:
|
|
|
|
|
|
|
| 74 |
subprocess.run(["pdflatex", "-version"], capture_output=True, check=True, timeout=10)
|
| 75 |
logger.info("Vérification réussie: pdflatex est installé et fonctionnel.")
|
| 76 |
return True
|
|
|
|
| 80 |
|
| 81 |
IS_LATEX_INSTALLED = check_latex_installation()
|
| 82 |
|
| 83 |
+
def save_user_image(image_data, filename, task_id):
|
| 84 |
+
"""Sauvegarde une image utilisateur dans le dossier user_images."""
|
| 85 |
+
try:
|
| 86 |
+
# Créer un nom de fichier unique avec le task_id
|
| 87 |
+
file_extension = os.path.splitext(filename)[1]
|
| 88 |
+
safe_filename = f"{task_id}_{filename}"
|
| 89 |
+
image_path = os.path.join(USER_IMAGES_DIR, safe_filename)
|
| 90 |
+
|
| 91 |
+
with open(image_path, 'wb') as f:
|
| 92 |
+
f.write(image_data)
|
| 93 |
+
|
| 94 |
+
logger.info(f"Image utilisateur sauvegardée: {safe_filename}")
|
| 95 |
+
return safe_filename
|
| 96 |
+
except Exception as e:
|
| 97 |
+
logger.error(f"Erreur lors de la sauvegarde de l'image {filename}: {e}")
|
| 98 |
+
return None
|
| 99 |
+
|
| 100 |
+
def get_all_tasks_info():
|
| 101 |
+
"""Récupère toutes les informations des tâches pour le centre de gestion."""
|
| 102 |
+
tasks_info = []
|
| 103 |
+
|
| 104 |
+
for task_id, task_data in task_results.items():
|
| 105 |
+
task_info = {
|
| 106 |
+
'id': task_id,
|
| 107 |
+
'status': task_data['status'],
|
| 108 |
+
'style': task_data.get('style', 'unknown'),
|
| 109 |
+
'first_filename': task_data.get('first_filename', 'Unknown'),
|
| 110 |
+
'time_started': task_data.get('time_started', 0),
|
| 111 |
+
'time_started_formatted': datetime.fromtimestamp(task_data.get('time_started', 0)).strftime('%Y-%m-%d %H:%M:%S'),
|
| 112 |
+
'file_count': task_data.get('file_count', {'images': 0, 'pdfs': 0}),
|
| 113 |
+
'pdf_filename': task_data.get('pdf_filename'),
|
| 114 |
+
'error': task_data.get('error'),
|
| 115 |
+
'response': task_data.get('response', ''),
|
| 116 |
+
'user_images': []
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
# Chercher les images utilisateur associées à cette tâche
|
| 120 |
+
if os.path.exists(USER_IMAGES_DIR):
|
| 121 |
+
for img_file in os.listdir(USER_IMAGES_DIR):
|
| 122 |
+
if img_file.startswith(f"{task_id}_"):
|
| 123 |
+
task_info['user_images'].append(img_file)
|
| 124 |
+
|
| 125 |
+
tasks_info.append(task_info)
|
| 126 |
+
|
| 127 |
+
# Trier par date de création (plus récent en premier)
|
| 128 |
+
tasks_info.sort(key=lambda x: x['time_started'], reverse=True)
|
| 129 |
+
return tasks_info
|
| 130 |
+
|
| 131 |
+
def get_system_stats():
|
| 132 |
+
"""Récupère les statistiques du système."""
|
| 133 |
+
total_tasks = len(task_results)
|
| 134 |
+
completed_tasks = sum(1 for task in task_results.values() if task['status'] == 'completed')
|
| 135 |
+
error_tasks = sum(1 for task in task_results.values() if task['status'] == 'error')
|
| 136 |
+
pending_tasks = sum(1 for task in task_results.values() if task['status'] not in ['completed', 'error'])
|
| 137 |
+
|
| 138 |
+
# Compter les fichiers PDF générés
|
| 139 |
+
pdf_files = 0
|
| 140 |
+
if os.path.exists(GENERATED_PDF_DIR):
|
| 141 |
+
pdf_files = len([f for f in os.listdir(GENERATED_PDF_DIR) if f.endswith('.pdf')])
|
| 142 |
+
|
| 143 |
+
# Compter les images utilisateur
|
| 144 |
+
user_images = 0
|
| 145 |
+
if os.path.exists(USER_IMAGES_DIR):
|
| 146 |
+
user_images = len([f for f in os.listdir(USER_IMAGES_DIR) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp'))])
|
| 147 |
+
|
| 148 |
+
return {
|
| 149 |
+
'total_tasks': total_tasks,
|
| 150 |
+
'completed_tasks': completed_tasks,
|
| 151 |
+
'error_tasks': error_tasks,
|
| 152 |
+
'pending_tasks': pending_tasks,
|
| 153 |
+
'pdf_files': pdf_files,
|
| 154 |
+
'user_images': user_images,
|
| 155 |
+
'latex_installed': IS_LATEX_INSTALLED
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
def clean_latex_code(latex_code):
|
| 159 |
"""Extrait le code LaTeX brut des blocs de code formatés (```latex ... ```)."""
|
| 160 |
logger.info("Nettoyage du code LaTeX reçu de Gemini...")
|
|
|
|
| 161 |
match_latex = re.search(r"```(?:latex|tex)\s*(.*?)\s*```", latex_code, re.DOTALL | re.IGNORECASE)
|
| 162 |
if match_latex:
|
| 163 |
logger.info("Bloc de code 'latex' ou 'tex' trouvé et extrait.")
|
| 164 |
return match_latex.group(1).strip()
|
| 165 |
|
|
|
|
| 166 |
match_generic = re.search(r"```\s*(\\documentclass.*?)\s*```", latex_code, re.DOTALL | re.IGNORECASE)
|
| 167 |
if match_generic:
|
| 168 |
logger.info("Bloc de code générique avec '\\documentclass' trouvé et extrait.")
|
|
|
|
| 184 |
logger.info(f"Début de la compilation LaTeX vers PDF pour '{output_filename_base}'")
|
| 185 |
|
| 186 |
try:
|
|
|
|
| 187 |
with open(tex_path, "w", encoding="utf-8") as tex_file:
|
| 188 |
tex_file.write(latex_code)
|
| 189 |
logger.info(f"Fichier .tex '{tex_path}' créé avec succès.")
|
| 190 |
|
|
|
|
| 191 |
my_env = os.environ.copy()
|
| 192 |
my_env["LC_ALL"] = "C.UTF-8"
|
| 193 |
my_env["LANG"] = "C.UTF-8"
|
| 194 |
|
| 195 |
last_result = None
|
|
|
|
| 196 |
for i in range(2):
|
| 197 |
logger.info(f"Exécution de pdflatex - Passe {i+1}/2...")
|
| 198 |
process = subprocess.run(
|
|
|
|
| 200 |
capture_output=True, text=True, check=False, encoding="utf-8", errors="replace", env=my_env, timeout=60
|
| 201 |
)
|
| 202 |
last_result = process
|
|
|
|
| 203 |
if not os.path.exists(pdf_path) and process.returncode != 0:
|
| 204 |
logger.warning(f"La passe {i+1} de pdflatex a échoué et aucun PDF n'a été créé. Arrêt de la compilation.")
|
| 205 |
break
|
|
|
|
| 210 |
else:
|
| 211 |
error_log = last_result.stdout + "\n" + last_result.stderr if last_result else "Aucun résultat de compilation disponible."
|
| 212 |
logger.error(f"Échec de la compilation PDF pour '{tex_filename}'. Log de pdflatex:\n{error_log}")
|
| 213 |
+
return None, f"Erreur de compilation PDF. Log: ...{error_log[-1000:]}"
|
| 214 |
|
| 215 |
except Exception as e:
|
| 216 |
logger.error(f"Exception pendant la génération du PDF: {e}", exc_info=True)
|
|
|
|
| 232 |
logger.info(log_msg)
|
| 233 |
data = {'chat_id': TELEGRAM_CHAT_ID, 'caption': caption}
|
| 234 |
response = requests.post(url, files=files, data=data, timeout=30)
|
| 235 |
+
response.raise_for_status()
|
| 236 |
logger.info(f"Fichier '{filename}' envoyé avec succès à Telegram.")
|
| 237 |
except Exception as e:
|
| 238 |
logger.error(f"Erreur lors de l'envoi à Telegram: {e}", exc_info=True)
|
| 239 |
|
|
|
|
| 240 |
# --- Logique Principale (Worker en arrière-plan) ---
|
| 241 |
|
| 242 |
def process_files_background(task_id, files_data, resolution_style):
|
|
|
|
| 254 |
for file_info in files_data:
|
| 255 |
if file_info['type'].startswith('image/'):
|
| 256 |
logger.info(f"[Task {task_id}] Traitement de l'image '{file_info['filename']}'.")
|
| 257 |
+
|
| 258 |
+
# Sauvegarder l'image utilisateur
|
| 259 |
+
saved_filename = save_user_image(file_info['data'], file_info['filename'], task_id)
|
| 260 |
+
|
| 261 |
img = Image.open(io.BytesIO(file_info['data']))
|
| 262 |
buffered = io.BytesIO()
|
| 263 |
+
img.save(buffered, format="PNG")
|
| 264 |
img_base64_str = base64.b64encode(buffered.getvalue()).decode()
|
| 265 |
contents.append({'inline_data': {'mime_type': 'image/png', 'data': img_base64_str}})
|
| 266 |
|
|
|
|
| 270 |
temp_pdf.write(file_info['data'])
|
| 271 |
temp_pdf_path = temp_pdf.name
|
| 272 |
|
|
|
|
| 273 |
file_ref = client.files.upload(file=temp_pdf_path)
|
| 274 |
uploaded_file_refs.append(file_ref)
|
| 275 |
contents.append(file_ref)
|
| 276 |
+
os.unlink(temp_pdf_path)
|
| 277 |
logger.info(f"[Task {task_id}] PDF '{file_info['filename']}' uploadé avec succès. Référence: {file_ref.name}")
|
| 278 |
|
| 279 |
if not contents:
|
|
|
|
| 307 |
cleaned_latex = clean_latex_code(full_latex_response)
|
| 308 |
logger.debug(f"[Task {task_id}] Code LaTeX nettoyé:\n---\n{cleaned_latex[:500]}...\n---")
|
| 309 |
|
|
|
|
| 310 |
task_results[task_id]['status'] = 'generating_pdf'
|
| 311 |
pdf_filename_base = f"solution_{task_id}"
|
| 312 |
pdf_file_path, pdf_message = latex_to_pdf(cleaned_latex, pdf_filename_base, GENERATED_PDF_DIR)
|
|
|
|
| 325 |
task_results[task_id]['error'] = str(e)
|
| 326 |
task_results[task_id]['response'] = f"Une erreur est survenue: {str(e)}"
|
| 327 |
finally:
|
|
|
|
| 328 |
if uploaded_file_refs:
|
| 329 |
logger.info(f"[Task {task_id}] Nettoyage des {len(uploaded_file_refs)} fichiers temporaires de l'API Gemini.")
|
| 330 |
for file_ref in uploaded_file_refs:
|
|
|
|
| 341 |
logger.info(f"Requête servie pour l'endpoint '/' depuis {request.remote_addr}")
|
| 342 |
return render_template('index.html')
|
| 343 |
|
| 344 |
+
@app.route('/admin')
|
| 345 |
+
def admin_panel():
|
| 346 |
+
"""Centre de gestion complet."""
|
| 347 |
+
logger.info(f"Accès au centre de gestion depuis {request.remote_addr}")
|
| 348 |
+
tasks_info = get_all_tasks_info()
|
| 349 |
+
system_stats = get_system_stats()
|
| 350 |
+
|
| 351 |
+
return render_template('admin.html', tasks=tasks_info, stats=system_stats)
|
| 352 |
+
|
| 353 |
+
@app.route('/admin/api/tasks')
|
| 354 |
+
def admin_api_tasks():
|
| 355 |
+
"""API JSON pour récupérer les informations des tâches."""
|
| 356 |
+
tasks_info = get_all_tasks_info()
|
| 357 |
+
return jsonify(tasks_info)
|
| 358 |
+
|
| 359 |
+
@app.route('/admin/api/stats')
|
| 360 |
+
def admin_api_stats():
|
| 361 |
+
"""API JSON pour récupérer les statistiques système."""
|
| 362 |
+
stats = get_system_stats()
|
| 363 |
+
return jsonify(stats)
|
| 364 |
+
|
| 365 |
+
@app.route('/admin/delete_task/<task_id>', methods=['POST'])
|
| 366 |
+
def admin_delete_task(task_id):
|
| 367 |
+
"""Supprime une tâche et ses fichiers associés."""
|
| 368 |
+
logger.info(f"Demande de suppression de la tâche {task_id}")
|
| 369 |
+
|
| 370 |
+
if task_id not in task_results:
|
| 371 |
+
return jsonify({'error': 'Tâche introuvable'}), 404
|
| 372 |
+
|
| 373 |
+
try:
|
| 374 |
+
task_data = task_results[task_id]
|
| 375 |
+
|
| 376 |
+
# Supprimer le PDF s'il existe
|
| 377 |
+
if 'pdf_filename' in task_data:
|
| 378 |
+
pdf_path = os.path.join(GENERATED_PDF_DIR, task_data['pdf_filename'])
|
| 379 |
+
if os.path.exists(pdf_path):
|
| 380 |
+
os.remove(pdf_path)
|
| 381 |
+
logger.info(f"PDF supprimé: {pdf_path}")
|
| 382 |
+
|
| 383 |
+
# Supprimer les images utilisateur associées
|
| 384 |
+
if os.path.exists(USER_IMAGES_DIR):
|
| 385 |
+
for img_file in os.listdir(USER_IMAGES_DIR):
|
| 386 |
+
if img_file.startswith(f"{task_id}_"):
|
| 387 |
+
img_path = os.path.join(USER_IMAGES_DIR, img_file)
|
| 388 |
+
os.remove(img_path)
|
| 389 |
+
logger.info(f"Image utilisateur supprimée: {img_path}")
|
| 390 |
+
|
| 391 |
+
# Supprimer la tâche de la mémoire
|
| 392 |
+
del task_results[task_id]
|
| 393 |
+
|
| 394 |
+
logger.info(f"Tâche {task_id} supprimée avec succès")
|
| 395 |
+
return jsonify({'success': True})
|
| 396 |
+
|
| 397 |
+
except Exception as e:
|
| 398 |
+
logger.error(f"Erreur lors de la suppression de la tâche {task_id}: {e}")
|
| 399 |
+
return jsonify({'error': str(e)}), 500
|
| 400 |
+
|
| 401 |
+
@app.route('/user_images/<filename>')
|
| 402 |
+
def serve_user_image(filename):
|
| 403 |
+
"""Sert les images utilisateur."""
|
| 404 |
+
try:
|
| 405 |
+
return send_from_directory(USER_IMAGES_DIR, filename)
|
| 406 |
+
except FileNotFoundError:
|
| 407 |
+
return "Image non trouvée", 404
|
| 408 |
+
|
| 409 |
@app.route('/solve', methods=['POST'])
|
| 410 |
def solve():
|
| 411 |
logger.info(f"Nouvelle requête sur /solve depuis {request.remote_addr}")
|
|
|
|
| 428 |
file_data = file.read()
|
| 429 |
file_type = file.content_type or 'application/octet-stream'
|
| 430 |
|
|
|
|
| 431 |
if file_type.startswith('image/'):
|
| 432 |
file_count['images'] += 1
|
| 433 |
files_data.append({'filename': file.filename, 'data': file_data, 'type': file_type})
|
|
|
|
| 505 |
logger.info(f"Fermeture de la connexion SSE pour la tâche terminée/échouée {task_id}")
|
| 506 |
break
|
| 507 |
|
| 508 |
+
time.sleep(1)
|
| 509 |
|
| 510 |
return Response(stream_with_context(generate()), mimetype='text/event-stream', headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
|
| 511 |
|
|
|
|
| 527 |
if __name__ == '__main__':
|
| 528 |
logger.info("Démarrage de l'application Flask.")
|
| 529 |
|
| 530 |
+
# Création des répertoires nécessaires
|
| 531 |
os.makedirs(GENERATED_PDF_DIR, exist_ok=True)
|
| 532 |
+
os.makedirs(USER_IMAGES_DIR, exist_ok=True)
|
| 533 |
+
logger.info(f"Répertoires assurés d'exister: '{GENERATED_PDF_DIR}' et '{USER_IMAGES_DIR}'")
|
| 534 |
|
| 535 |
+
app.run(debug=True, host='0.0.0.0', port=5000)
|