Docfile commited on
Commit
9d6d8b4
·
verified ·
1 Parent(s): 94799d7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +101 -105
app.py CHANGED
@@ -1,27 +1,20 @@
1
- from flask import Flask, render_template, request, jsonify, send_file
2
  from google import genai
 
3
  from PIL import Image
4
  import subprocess
5
  import tempfile
6
  import os
7
  import io
8
- import shutil
9
- import time
10
- import uuid
11
  import base64
12
- from dotenv import load_dotenv
13
-
14
- # Chargement des variables d'environnement
15
- load_dotenv()
16
-
17
- app = Flask(__name__)
18
 
19
  # --- Constantes ---
20
  MODEL_SINGLE_GENERATION = "gemini-2.5-pro-exp-03-25" # Modèle avec "thinking" pour la génération complète
21
  LATEX_MENTION = r"\vspace{1cm}\noindent\textit{Ce devoir a été généré par Mariam AI. \url{https://mariam-241.vercel.app}}"
22
 
23
- # --- Fonctions Utilitaires ---
24
 
 
25
  def check_latex_installation():
26
  """Vérifie si pdflatex est accessible dans le PATH système."""
27
  try:
@@ -29,11 +22,13 @@ def check_latex_installation():
29
  subprocess.run(['pdflatex', '--version'], capture_output=True, check=True, timeout=10)
30
  return True, "Installation LaTeX (pdflatex) trouvée."
31
  except (subprocess.CalledProcessError, FileNotFoundError, TimeoutError) as e:
32
- return False, f"pdflatex introuvable ou ne répond pas. Veuillez installer une distribution LaTeX. Erreur: {e}"
33
 
34
  def initialize_genai_client():
35
  """Initialise et retourne le client Google GenAI."""
36
  try:
 
 
37
  api_key = os.environ.get("GOOGLE_API_KEY")
38
  if not api_key:
39
  return None, "Clé API Google non trouvée dans les variables d'environnement."
@@ -65,9 +60,9 @@ def clean_latex(raw_latex_text):
65
  if cleaned.endswith("```"):
66
  cleaned = cleaned[:-3].strip() # Enlève ``` et les espaces potentiels avant
67
 
68
- # Vérification du début du document
69
  if not cleaned.startswith("\\documentclass"):
70
- app.logger.warning("Le code LaTeX reçu ne commence pas par \\documentclass.")
71
 
72
  # Assure que le document se termine exactement par \end{document}
73
  end_doc_tag = "\\end{document}"
@@ -75,13 +70,13 @@ def clean_latex(raw_latex_text):
75
  end_idx = cleaned.rfind(end_doc_tag) + len(end_doc_tag)
76
  cleaned = cleaned[:end_idx]
77
  else:
78
- app.logger.warning("Le code LaTeX reçu ne contient pas \\end{document}. Tentative d'ajout.")
79
  # Ajoute seulement si \documentclass est présent pour éviter de créer un doc invalide
80
  if cleaned.strip().startswith("\\documentclass"):
81
  cleaned += f"\n{end_doc_tag}"
82
 
83
  return cleaned.strip() # Retourne le résultat nettoyé
84
 
 
85
  def generate_complete_latex(client, image):
86
  """
87
  Génère une solution complète en LaTeX à partir de l'image en une seule étape,
@@ -123,8 +118,6 @@ Agis en tant qu'expert en mathématiques et tuteur pédagogue. Ton objectif est
123
  3. **Traduction en LaTeX:** Convertis ta résolution raisonnée en code LaTeX, en appliquant méticuleusement toutes les spécifications de formatage et de style demandées
124
  """
125
  try:
126
- from google.genai import types
127
-
128
  response = client.models.generate_content(
129
  model=MODEL_SINGLE_GENERATION,
130
  contents=[image, prompt],
@@ -148,7 +141,6 @@ Agis en tant qu'expert en mathématiques et tuteur pédagogue. Ton objectif est
148
 
149
  # Vérification supplémentaire : est-ce que la mention obligatoire est présente ?
150
  if LATEX_MENTION not in latex_content_cleaned:
151
- app.logger.warning("La mention obligatoire semble manquer. Tentative d'insertion.")
152
  # Tente de l'insérer avant \end{document}
153
  end_doc_tag = "\\end{document}"
154
  if end_doc_tag in latex_content_cleaned:
@@ -157,14 +149,16 @@ Agis en tant qu'expert en mathématiques et tuteur pédagogue. Ton objectif est
157
  # Si \end{document} n'est pas là, ajoute les deux à la fin
158
  latex_content_cleaned += f"\n{LATEX_MENTION}\n{end_doc_tag}"
159
 
160
- return latex_content_cleaned, thinking_content
161
  else:
162
- return None, thinking_content
163
 
 
 
164
  except Exception as e:
165
- app.logger.error(f"Erreur lors de la génération du LaTeX: {e}")
166
- return None, None
167
 
 
168
  def compile_latex_to_pdf(latex_content):
169
  """
170
  Compile une chaîne de caractères contenant du code LaTeX en fichier PDF.
@@ -186,9 +180,9 @@ def compile_latex_to_pdf(latex_content):
186
  with open(tex_filepath, "w", encoding="utf-8") as f:
187
  f.write(latex_content)
188
  except IOError as e:
189
- return None, f"Erreur lors de l'écriture du fichier .tex temporaire: {e}"
190
 
191
- # Commande pdflatex
192
  command = [
193
  "pdflatex",
194
  "-interaction=nonstopmode",
@@ -198,31 +192,41 @@ def compile_latex_to_pdf(latex_content):
198
  ]
199
 
200
  pdf_generated = False
201
- log_output = ""
202
 
203
  for pass_num in range(1, 3): # Lance jusqu'à 2 passes
 
204
  try:
205
  # Augmentation du timeout pour les compilations potentiellement longues
206
- result = subprocess.run(command, capture_output=True, check=True, text=True, encoding='utf-8', errors='replace', timeout=60)
 
 
207
  # Vérifie si le PDF a été créé après la première passe (au moins)
208
  if os.path.exists(pdf_filepath):
209
  pdf_generated = True
210
 
211
  except subprocess.CalledProcessError as e:
212
- error_msg = f"Erreur lors de la compilation LaTeX (Passe {pass_num}). Code: {e.returncode}"
 
 
 
 
 
 
213
  # Essayer de lire le fichier .log pour plus d'infos
214
  try:
215
  with open(log_filepath, "r", encoding="utf-8", errors='replace') as log_file:
216
- log_output = log_file.read()[-2000:] # Dernières lignes du log
 
217
  except IOError:
218
- log_output = "Impossible de lire le fichier log de LaTeX."
219
- return None, f"{error_msg}\n{log_output}"
220
 
 
 
221
  except subprocess.TimeoutExpired:
222
- return None, "La compilation LaTeX a dépassé le délai imparti."
223
-
224
  except Exception as e:
225
- return None, f"Erreur inattendue lors de l'exécution de pdflatex: {e}"
226
 
227
  # Vérifie si le fichier PDF existe après les passes
228
  if pdf_generated and os.path.exists(pdf_filepath):
@@ -232,120 +236,112 @@ def compile_latex_to_pdf(latex_content):
232
  pdf_data = f.read()
233
  return pdf_data, "PDF généré avec succès !"
234
  except IOError as e:
235
- return None, f"Erreur lors de la lecture du fichier PDF généré: {e}"
236
  else:
237
- # Tenter de lire le log si le PDF n'a pas été généré
 
238
  try:
239
  with open(log_filepath, "r", encoding="utf-8", errors='replace') as log_file:
240
- log_output = log_file.read()[-2000:]
 
241
  except IOError:
242
- log_output = "Impossible de lire le fichier log de LaTeX."
243
- return None, f"Le PDF n'a pas été généré après la compilation LaTeX.\n{log_output}"
244
-
245
- # --- Routes Flask ---
246
 
247
  @app.route('/')
248
  def index():
249
  return render_template('index.html')
250
 
251
- @app.route('/check_latex', methods=['GET'])
252
  def check_latex():
253
- latex_installed, message = check_latex_installation()
254
- return jsonify({
255
- 'success': latex_installed,
256
- 'message': message
257
- })
258
-
259
- @app.route('/check_api', methods=['GET'])
260
- def check_api():
261
- client, message = initialize_genai_client()
262
  return jsonify({
263
- 'success': client is not None,
264
- 'message': message
265
  })
266
 
267
- @app.route('/process_image', methods=['POST'])
268
  def process_image():
269
  if 'image' not in request.files:
270
- return jsonify({'success': False, 'message': 'Aucune image téléchargée'})
271
 
272
  file = request.files['image']
273
  if file.filename == '':
274
- return jsonify({'success': False, 'message': 'Aucun fichier sélectionné'})
275
 
276
  try:
277
- # Lecture de l'image
278
- image_data = file.read()
279
- image = Image.open(io.BytesIO(image_data))
280
-
281
- # Initialisation du client
282
  client, client_message = initialize_genai_client()
283
  if not client:
284
- return jsonify({'success': False, 'message': client_message})
 
 
 
285
 
286
  # Génération du LaTeX
287
- latex_content, thinking_content = generate_complete_latex(client, image)
 
288
  if not latex_content:
289
  return jsonify({
290
- 'success': False,
291
- 'message': 'Échec de la génération de la solution LaTeX',
292
- 'thinking': thinking_content
293
  })
294
 
295
  # Compilation en PDF
296
  pdf_data, pdf_message = compile_latex_to_pdf(latex_content)
 
297
  if not pdf_data:
298
  return jsonify({
299
- 'success': False,
300
- 'message': f'Échec de la compilation PDF: {pdf_message}',
301
- 'latex': latex_content,
302
- 'thinking': thinking_content
 
303
  })
304
 
305
- # Génération d'un ID unique pour le PDF
306
- pdf_id = str(uuid.uuid4())
307
- pdf_path = os.path.join(app.root_path, 'static', 'pdfs')
308
-
309
- # Créer le dossier s'il n'existe pas
310
- os.makedirs(pdf_path, exist_ok=True)
311
-
312
- # Sauvegarder le PDF
313
- full_pdf_path = os.path.join(pdf_path, f"{pdf_id}.pdf")
314
- with open(full_pdf_path, 'wb') as f:
315
- f.write(pdf_data)
316
-
317
- # Convertir l'image en base64 pour l'affichage
318
- image_base64 = base64.b64encode(image_data).decode('utf-8')
319
 
 
320
  return jsonify({
321
- 'success': True,
322
- 'message': 'PDF généré avec succès',
323
- 'pdf_url': f'/static/pdfs/{pdf_id}.pdf',
324
- 'latex': latex_content,
325
- 'thinking': thinking_content,
326
- 'image_base64': image_base64
327
  })
328
 
329
  except Exception as e:
330
- app.logger.error(f"Erreur dans process_image: {e}")
331
- return jsonify({'success': False, 'message': f'Erreur inattendue: {str(e)}'})
332
-
333
- @app.route('/download_pdf/<pdf_id>', methods=['GET'])
334
- def download_pdf(pdf_id):
335
- # Vérifier que l'ID ne contient pas de caractères dangereux
336
- if not pdf_id.isalnum() and '-' not in pdf_id:
337
- return "ID PDF invalide", 400
 
 
 
 
 
 
338
 
339
- pdf_path = os.path.join(app.root_path, 'static', 'pdfs', f"{pdf_id}.pdf")
 
340
 
341
- if not os.path.exists(pdf_path):
342
- return "PDF introuvable", 404
 
 
343
 
344
- return send_file(pdf_path, as_attachment=True, download_name="solution_mariam_ai.pdf")
 
 
 
 
 
345
 
346
  if __name__ == '__main__':
347
- # Création du dossier pour les PDFs
348
- os.makedirs(os.path.join(app.root_path, 'static', 'pdfs'), exist_ok=True)
349
-
350
- # Lancement de l'application
351
  app.run(debug=True)
 
1
+ from flask import Flask, render_template, request, send_file, jsonify
2
  from google import genai
3
+ from google.genai import types
4
  from PIL import Image
5
  import subprocess
6
  import tempfile
7
  import os
8
  import io
 
 
 
9
  import base64
 
 
 
 
 
 
10
 
11
  # --- Constantes ---
12
  MODEL_SINGLE_GENERATION = "gemini-2.5-pro-exp-03-25" # Modèle avec "thinking" pour la génération complète
13
  LATEX_MENTION = r"\vspace{1cm}\noindent\textit{Ce devoir a été généré par Mariam AI. \url{https://mariam-241.vercel.app}}"
14
 
15
+ app = Flask(__name__)
16
 
17
+ # --- Fonctions Utilitaires ---
18
  def check_latex_installation():
19
  """Vérifie si pdflatex est accessible dans le PATH système."""
20
  try:
 
22
  subprocess.run(['pdflatex', '--version'], capture_output=True, check=True, timeout=10)
23
  return True, "Installation LaTeX (pdflatex) trouvée."
24
  except (subprocess.CalledProcessError, FileNotFoundError, TimeoutError) as e:
25
+ return False, f"pdflatex introuvable ou ne répond pas. Veuillez installer une distribution LaTeX (comme TeX Live ou MiKTeX) et vous assurer qu'elle est dans le PATH système. Erreur: {e}"
26
 
27
  def initialize_genai_client():
28
  """Initialise et retourne le client Google GenAI."""
29
  try:
30
+ # Dans un environnement Flask, vous devrez configurer votre API key différemment
31
+ # Par exemple, en utilisant les variables d'environnement
32
  api_key = os.environ.get("GOOGLE_API_KEY")
33
  if not api_key:
34
  return None, "Clé API Google non trouvée dans les variables d'environnement."
 
60
  if cleaned.endswith("```"):
61
  cleaned = cleaned[:-3].strip() # Enlève ``` et les espaces potentiels avant
62
 
63
+ # Assure que le document commence correctement
64
  if not cleaned.startswith("\\documentclass"):
65
+ pass # On log un avertissement dans l'application web
66
 
67
  # Assure que le document se termine exactement par \end{document}
68
  end_doc_tag = "\\end{document}"
 
70
  end_idx = cleaned.rfind(end_doc_tag) + len(end_doc_tag)
71
  cleaned = cleaned[:end_idx]
72
  else:
 
73
  # Ajoute seulement si \documentclass est présent pour éviter de créer un doc invalide
74
  if cleaned.strip().startswith("\\documentclass"):
75
  cleaned += f"\n{end_doc_tag}"
76
 
77
  return cleaned.strip() # Retourne le résultat nettoyé
78
 
79
+ # --- Fonction Principale (API GenAI) ---
80
  def generate_complete_latex(client, image):
81
  """
82
  Génère une solution complète en LaTeX à partir de l'image en une seule étape,
 
118
  3. **Traduction en LaTeX:** Convertis ta résolution raisonnée en code LaTeX, en appliquant méticuleusement toutes les spécifications de formatage et de style demandées
119
  """
120
  try:
 
 
121
  response = client.models.generate_content(
122
  model=MODEL_SINGLE_GENERATION,
123
  contents=[image, prompt],
 
141
 
142
  # Vérification supplémentaire : est-ce que la mention obligatoire est présente ?
143
  if LATEX_MENTION not in latex_content_cleaned:
 
144
  # Tente de l'insérer avant \end{document}
145
  end_doc_tag = "\\end{document}"
146
  if end_doc_tag in latex_content_cleaned:
 
149
  # Si \end{document} n'est pas là, ajoute les deux à la fin
150
  latex_content_cleaned += f"\n{LATEX_MENTION}\n{end_doc_tag}"
151
 
152
+ return latex_content_cleaned, thinking_content, None
153
  else:
154
+ return None, thinking_content, "Aucun contenu LaTeX n'a été généré par le modèle."
155
 
156
+ except types.StopCandidateException as e:
157
+ return None, None, "Génération stoppée (possible contenu inapproprié)"
158
  except Exception as e:
159
+ return None, None, f"Erreur lors de la génération du LaTeX: {str(e)}"
 
160
 
161
+ # --- Fonction de Compilation LaTeX ---
162
  def compile_latex_to_pdf(latex_content):
163
  """
164
  Compile une chaîne de caractères contenant du code LaTeX en fichier PDF.
 
180
  with open(tex_filepath, "w", encoding="utf-8") as f:
181
  f.write(latex_content)
182
  except IOError as e:
183
+ return None, f"Erreur lors de l'écriture du fichier .tex temporaire: {str(e)}"
184
 
185
+ # Exécute pdflatex
186
  command = [
187
  "pdflatex",
188
  "-interaction=nonstopmode",
 
192
  ]
193
 
194
  pdf_generated = False
195
+ compile_log = []
196
 
197
  for pass_num in range(1, 3): # Lance jusqu'à 2 passes
198
+ compile_log.append(f"Compilation LaTeX (Passe {pass_num})...")
199
  try:
200
  # Augmentation du timeout pour les compilations potentiellement longues
201
+ result = subprocess.run(command, capture_output=True, check=True, text=True,
202
+ encoding='utf-8', errors='replace', timeout=60)
203
+ compile_log.append(result.stdout)
204
  # Vérifie si le PDF a été créé après la première passe (au moins)
205
  if os.path.exists(pdf_filepath):
206
  pdf_generated = True
207
 
208
  except subprocess.CalledProcessError as e:
209
+ compile_log.append(f"Erreur lors de la compilation LaTeX (Passe {pass_num}).")
210
+ compile_log.append(f"Code de retour: {e.returncode}")
211
+ compile_log.append("Sortie de pdflatex (stdout):")
212
+ compile_log.append(e.stdout or "Aucune sortie standard.")
213
+ compile_log.append("Erreurs de pdflatex (stderr):")
214
+ compile_log.append(e.stderr or "Aucune sortie d'erreur.")
215
+
216
  # Essayer de lire le fichier .log pour plus d'infos
217
  try:
218
  with open(log_filepath, "r", encoding="utf-8", errors='replace') as log_file:
219
+ compile_log.append("Extrait du fichier log (solution.log):")
220
+ compile_log.append(log_file.read()[-2000:]) # Affiche les dernières lignes
221
  except IOError:
222
+ compile_log.append("Impossible de lire le fichier log de LaTeX.")
 
223
 
224
+ return None, "\n".join(compile_log) # Arrête la compilation en cas d'erreur
225
+
226
  except subprocess.TimeoutExpired:
227
+ return None, "La compilation LaTeX a dépassé le délai imparti. Le document est peut-être trop complexe ou contient une boucle infinie."
 
228
  except Exception as e:
229
+ return None, f"Une erreur inattendue est survenue lors de l'exécution de pdflatex: {str(e)}"
230
 
231
  # Vérifie si le fichier PDF existe après les passes
232
  if pdf_generated and os.path.exists(pdf_filepath):
 
236
  pdf_data = f.read()
237
  return pdf_data, "PDF généré avec succès !"
238
  except IOError as e:
239
+ return None, f"Erreur lors de la lecture du fichier PDF généré: {str(e)}"
240
  else:
241
+ compile_log.append("Le fichier PDF n'a pas été généré après la compilation LaTeX.")
242
+ # Tenter de lire le log même si CalledProcessError n'a pas été levée
243
  try:
244
  with open(log_filepath, "r", encoding="utf-8", errors='replace') as log_file:
245
+ compile_log.append("Extrait du fichier log (solution.log) après échec de génération PDF:")
246
+ compile_log.append(log_file.read()[-2000:])
247
  except IOError:
248
+ compile_log.append("Impossible de lire le fichier log de LaTeX.")
249
+
250
+ return None, "\n".join(compile_log)
 
251
 
252
  @app.route('/')
253
  def index():
254
  return render_template('index.html')
255
 
256
+ @app.route('/check-latex')
257
  def check_latex():
258
+ latex_ok, message = check_latex_installation()
 
 
 
 
 
 
 
 
259
  return jsonify({
260
+ "success": latex_ok,
261
+ "message": message
262
  })
263
 
264
+ @app.route('/process-image', methods=['POST'])
265
  def process_image():
266
  if 'image' not in request.files:
267
+ return jsonify({"success": False, "message": "Aucune image téléchargée"})
268
 
269
  file = request.files['image']
270
  if file.filename == '':
271
+ return jsonify({"success": False, "message": "Aucun fichier sélectionné"})
272
 
273
  try:
274
+ # Initialisation du client GenAI
 
 
 
 
275
  client, client_message = initialize_genai_client()
276
  if not client:
277
+ return jsonify({"success": False, "message": client_message})
278
+
279
+ # Traitement de l'image
280
+ image = Image.open(file.stream)
281
 
282
  # Génération du LaTeX
283
+ latex_content, thinking_process, error_message = generate_complete_latex(client, image)
284
+
285
  if not latex_content:
286
  return jsonify({
287
+ "success": False,
288
+ "message": error_message or "Échec de la génération de la solution LaTeX"
 
289
  })
290
 
291
  # Compilation en PDF
292
  pdf_data, pdf_message = compile_latex_to_pdf(latex_content)
293
+
294
  if not pdf_data:
295
  return jsonify({
296
+ "success": False,
297
+ "message": "Échec de la compilation PDF",
298
+ "latex": latex_content,
299
+ "thinking": thinking_process,
300
+ "compilation_log": pdf_message
301
  })
302
 
303
+ # Convertir le PDF en base64 pour l'affichage dans le navigateur
304
+ pdf_base64 = base64.b64encode(pdf_data).decode('utf-8')
 
 
 
 
 
 
 
 
 
 
 
 
305
 
306
+ # Préparer les données de réponse
307
  return jsonify({
308
+ "success": True,
309
+ "message": "PDF généré avec succès",
310
+ "latex": latex_content,
311
+ "thinking": thinking_process,
312
+ "pdf_base64": pdf_base64
 
313
  })
314
 
315
  except Exception as e:
316
+ return jsonify({
317
+ "success": False,
318
+ "message": f"Erreur lors du traitement: {str(e)}"
319
+ })
320
+
321
+ @app.route('/download-pdf', methods=['POST'])
322
+ def download_pdf():
323
+ # Récupérer le contenu LaTeX
324
+ latex_content = request.form.get('latex')
325
+ if not latex_content:
326
+ return jsonify({"success": False, "message": "Aucun contenu LaTeX fourni"})
327
+
328
+ # Compiler en PDF
329
+ pdf_data, pdf_message = compile_latex_to_pdf(latex_content)
330
 
331
+ if not pdf_data:
332
+ return jsonify({"success": False, "message": pdf_message})
333
 
334
+ # Créer un fichier temporaire pour le téléchargement
335
+ temp_pdf = tempfile.NamedTemporaryFile(delete=False)
336
+ temp_pdf.write(pdf_data)
337
+ temp_pdf.close()
338
 
339
+ return send_file(
340
+ temp_pdf.name,
341
+ as_attachment=True,
342
+ download_name="solution_mariam_ai.pdf",
343
+ mimetype="application/pdf"
344
+ )
345
 
346
  if __name__ == '__main__':
 
 
 
 
347
  app.run(debug=True)