Docfile commited on
Commit
af4cdb5
·
verified ·
1 Parent(s): 5712525

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +336 -152
app.py CHANGED
@@ -1,4 +1,6 @@
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
@@ -7,74 +9,109 @@ 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:
21
  # Tente d'exécuter pdflatex pour vérifier son existence et son fonctionnement
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."
35
-
36
  client = genai.Client(api_key=api_key, http_options={'api_version':'v1alpha'})
 
 
 
37
  return client, "Client Mariam AI initialisé."
38
  except Exception as e:
39
- return None, f"Erreur lors de l'initialisation du client: {e}"
 
 
 
40
 
41
  def clean_latex(raw_latex_text):
42
  """Nettoie le code LaTeX brut potentiellement fourni par Gemini."""
43
  if not isinstance(raw_latex_text, str):
44
- return "" # Retourne une chaîne vide si l'entrée n'est pas une chaîne
 
45
 
 
46
  cleaned = raw_latex_text.strip()
47
 
48
- # Supprime les marqueurs de bloc de code (```latex ... ``` ou ``` ... ```)
49
- if cleaned.startswith("```latex"):
50
- cleaned = cleaned[8:]
51
- elif cleaned.startswith("```"):
52
- # Gère le cas il y a un saut de ligne après ```
53
- lines = cleaned.split('\n', 1)
54
- if len(lines) > 1:
55
- cleaned = lines[1]
56
- else:
57
- cleaned = cleaned[3:] # Si pas de saut de ligne, juste enlever ```
58
 
59
  # Supprime les marqueurs de fin de bloc de code
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}"
69
- if end_doc_tag in cleaned:
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):
@@ -97,65 +134,111 @@ Agis en tant qu'expert en mathématiques et tuteur pédagogue. Ton objectif est
97
  3. Rédige la solution complète directement en code LaTeX, en respectant **toutes** les spécifications ci-dessous.
98
 
99
  # SPÉCIFICATIONS TECHNIQUES DU CODE LATEX
100
- 1. **Structure du Document:** Commence **strictement** par `\\documentclass{{article}}` et se termine **strictement** par `\\end{{document}}`.
101
- 2. **Packages Requis:** Inclus impérativement les packages suivants via `\\usepackage{{...}}`: `amsmath`, `amssymb`, `geometry`, `hyperref`, `url`. Assure-toi qu'ils sont déclarés dans le préambule.
102
- 3. **Compilabilité:** Le code généré doit être valide et compilable sans erreur avec `pdflatex`.
103
- 4. **Formatage du Code:** Produis un code LaTeX propre, bien indenté et lisible.
104
- 5. **Environnements Mathématiques:** Utilise les environnements LaTeX appropriés (`align`, `equation`, `gather`, etc.) pour présenter les calculs et les équations de manière claire et standard.
105
- 6. **AUCUN Marqueur de Code:** Le résultat doit être **uniquement** le code LaTeX brut. N'inclus **JAMAIS** de marqueurs de code comme ```latex ... ``` ou ``` ... ``` au début ou à la fin.
 
106
 
107
  # STYLE & CONTENU DE LA SOLUTION
108
- 1. **Pédagogie:** La correction doit être claire, aérée et facile à comprendre pour un élève de Terminale S.
109
- 2. **Justifications:** Justifie **chaque** étape clé du raisonnement mathématique. Explique *pourquoi* une certaine méthode est utilisée ou *comment* on passe d'une étape à l'autre.
110
- 3. **Rigueur:** Assure l'exactitude mathématique complète de la solution.
111
- 4. **Structure Logique:** Organise la solution de manière logique. Utilise des sections (`\\section*{{...}}`, `\\subsection*{{...}}`) si cela améliore la clarté pour des problèmes longs ou multi-parties.
112
  5. **Mention Obligatoire:** Insère la ligne suivante **exactement** telle quelle, juste avant la ligne `\\end{{document}}`:
113
  {LATEX_MENTION}
114
 
115
  # PROCESSUS INTERNE RECOMMANDÉ (Pour l'IA)
116
- 1. **Analyse Approfondie:** Décompose le problème en sous-étapes logiques.
117
- 2. **Résolution Étape par Étape:** Effectue la résolution mathématique complète en interne.
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],
124
  config=types.GenerateContentConfig(
125
- temperature=0.3,
126
- ))
 
 
 
127
 
128
  latex_content_raw = None
129
  thinking_content = None
130
 
131
  # Extrait le contenu LaTeX et le raisonnement (thought)
132
- if response.candidates and response.candidates[0].content and response.candidates[0].content.parts:
133
- for part in response.candidates[0].content.parts:
134
- if getattr(part, 'thought', None):
135
- thinking_content = part.text
136
- else:
137
- latex_content_raw = part.text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
  if latex_content_raw:
140
  latex_content_cleaned = clean_latex(latex_content_raw)
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:
147
- latex_content_cleaned = latex_content_cleaned.replace(end_doc_tag, f"{LATEX_MENTION}\n{end_doc_tag}")
148
- else:
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 ---
@@ -164,97 +247,175 @@ def compile_latex_to_pdf(latex_content):
164
  Compile une chaîne de caractères contenant du code LaTeX en fichier PDF.
165
  Utilise un répertoire temporaire pour les fichiers intermédiaires.
166
  """
167
- if not latex_content:
 
168
  return None, "Impossible de compiler : contenu LaTeX vide."
169
 
170
  # Utilise un répertoire temporaire sécurisé qui sera nettoyé automatiquement
171
  with tempfile.TemporaryDirectory() as temp_dir:
172
- tex_filename = "solution.tex"
173
- pdf_filename = "solution.pdf"
 
 
 
 
174
  tex_filepath = os.path.join(temp_dir, tex_filename)
175
  pdf_filepath = os.path.join(temp_dir, pdf_filename)
176
- log_filepath = os.path.join(temp_dir, "solution.log") # Pour le débogage
 
 
 
 
177
 
178
  # Écrit le contenu LaTeX dans le fichier .tex avec encodage UTF-8
179
  try:
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",
 
189
  f"-output-directory={temp_dir}",
190
- f"-jobname={os.path.splitext(pdf_filename)[0]}", # Nom sans extension
191
  tex_filepath
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):
233
  try:
 
234
  # Lit le contenu binaire du PDF généré
235
  with open(pdf_filepath, "rb") as f:
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,
@@ -263,85 +424,108 @@ def check_latex():
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)
 
 
 
1
+ # --- START OF FILE app - 2025-04-25T003524.273.py ---
2
+
3
+ from flask import Flask, render_template, request, send_file, jsonify, current_app
4
  from google import genai
5
  from google.genai import types
6
  from PIL import Image
 
9
  import os
10
  import io
11
  import base64
12
+ import logging # Importer le module de logging
13
 
14
  # --- Constantes ---
15
+ MODEL_SINGLE_GENERATION = "gemini-2.5-pro-exp-03-25"
16
  LATEX_MENTION = r"\vspace{1cm}\noindent\textit{Ce devoir a été généré par Mariam AI. \url{https://mariam-241.vercel.app}}"
17
 
18
  app = Flask(__name__)
19
 
20
+ # Configuration du logging
21
+ logging.basicConfig(level=logging.DEBUG) # Afficher les messages DEBUG et supérieurs
22
+
23
  # --- Fonctions Utilitaires ---
24
  def check_latex_installation():
25
  """Vérifie si pdflatex est accessible dans le PATH système."""
26
  try:
27
  # Tente d'exécuter pdflatex pour vérifier son existence et son fonctionnement
28
+ current_app.logger.info("Vérification de l'installation de pdflatex...")
29
+ result = subprocess.run(['pdflatex', '--version'], capture_output=True, check=True, timeout=10, text=True, encoding='utf-8', errors='replace')
30
+ current_app.logger.info(f"pdflatex trouvé. Version: {result.stdout.splitlines()[0]}")
31
  return True, "Installation LaTeX (pdflatex) trouvée."
32
  except (subprocess.CalledProcessError, FileNotFoundError, TimeoutError) as e:
33
+ error_msg = 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}"
34
+ current_app.logger.error(error_msg)
35
+ # Si FileNotFoundError, donner plus de détails
36
+ if isinstance(e, FileNotFoundError):
37
+ error_msg += f"\nLe système ne trouve pas l'exécutable 'pdflatex'. Est-il dans le PATH ?"
38
+ elif isinstance(e, TimeoutError):
39
+ error_msg += f"\nLa commande 'pdflatex --version' a dépassé le délai. L'installation est peut-être corrompue."
40
+ elif isinstance(e, subprocess.CalledProcessError):
41
+ error_msg += f"\nLa commande 'pdflatex --version' a retourné une erreur: {e.stderr}"
42
+ return False, error_msg
43
+ except Exception as e:
44
+ error_msg = f"Erreur inattendue lors de la vérification de pdflatex: {e}"
45
+ current_app.logger.error(error_msg)
46
+ return False, error_msg
47
 
48
  def initialize_genai_client():
49
  """Initialise et retourne le client Google GenAI."""
50
  try:
51
+ current_app.logger.info("Initialisation du client GenAI...")
 
52
  api_key = os.environ.get("GOOGLE_API_KEY")
53
  if not api_key:
54
+ current_app.logger.error("Clé API Google non trouvée dans les variables d'environnement.")
55
  return None, "Clé API Google non trouvée dans les variables d'environnement."
56
+
57
  client = genai.Client(api_key=api_key, http_options={'api_version':'v1alpha'})
58
+ # Test rapide de la connexion (optionnel mais utile)
59
+ client.models.get_model(f'models/{MODEL_SINGLE_GENERATION}')
60
+ current_app.logger.info("Client GenAI initialisé et modèle vérifié.")
61
  return client, "Client Mariam AI initialisé."
62
  except Exception as e:
63
+ error_msg = f"Erreur lors de l'initialisation ou de la vérification du client GenAI: {e}"
64
+ current_app.logger.error(error_msg)
65
+ return None, error_msg
66
+
67
 
68
  def clean_latex(raw_latex_text):
69
  """Nettoie le code LaTeX brut potentiellement fourni par Gemini."""
70
  if not isinstance(raw_latex_text, str):
71
+ current_app.logger.warning("clean_latex reçu une entrée non-string.")
72
+ return ""
73
 
74
+ current_app.logger.debug(f"LaTeX brut reçu:\n{raw_latex_text[:500]}...\n...\n{raw_latex_text[-500:]}") # Log début et fin
75
  cleaned = raw_latex_text.strip()
76
 
77
+ # Supprime les marqueurs de bloc de code (```latex ... ``` ou ``` ... ```) de manière plus robuste
78
+ if cleaned.startswith("```"):
79
+ cleaned = cleaned.split('\n', 1)[-1] # Prend tout après la première ligne
80
+ if cleaned.startswith("latex"): # Gère le cas ```latex
81
+ cleaned = cleaned[len("latex"):].lstrip() # Enlève 'latex' et espaces suivants
 
 
 
 
 
82
 
83
  # Supprime les marqueurs de fin de bloc de code
84
  if cleaned.endswith("```"):
85
+ cleaned = cleaned[:-3].rstrip() # Enlève ``` et les espaces/sauts de ligne potentiels avant
86
 
87
+ # Vérification de la structure minimale
88
+ if not cleaned.strip().startswith("\\documentclass"):
89
+ current_app.logger.warning("Code LaTeX nettoyé ne commence pas par \\documentclass.")
90
+ # Ne pas ajouter \documentclass ici, car on ne sait pas quel type ou options sont nécessaires.
91
+ # L'erreur de compilation qui suivra sera plus informative.
92
 
 
93
  end_doc_tag = "\\end{document}"
94
+ if end_doc_tag not in cleaned:
95
+ # Si \documentclass est présent mais pas \end{document}, c'est probablement une erreur de génération
96
+ if cleaned.strip().startswith("\\documentclass"):
97
+ current_app.logger.warning(f"Tag '{end_doc_tag}' manquant dans le LaTeX généré. Tentative d'ajout.")
98
+ cleaned += f"\n{end_doc_tag}"
99
+ else:
100
+ # Si même \documentclass manque, le code est probablement inutile
101
+ current_app.logger.error("Structure LaTeX de base manquante (\\documentclass et \\end{document}).")
102
+ # On retourne quand même ce qu'on a, la compilation échouera.
103
+ pass
104
  else:
105
+ # Assurer qu'il n'y a rien après \end{document} (sauf des espaces/sauts de ligne)
106
+ end_idx = cleaned.rfind(end_doc_tag) + len(end_doc_tag)
107
+ trailing_content = cleaned[end_idx:].strip()
108
+ if trailing_content:
109
+ current_app.logger.warning(f"Contenu trouvé après {end_doc_tag} : '{trailing_content[:100]}...' - Suppression.")
110
+ cleaned = cleaned[:end_idx]
111
 
112
+
113
+ current_app.logger.debug(f"LaTeX nettoyé:\n{cleaned[:500]}...\n...\n{cleaned[-500:]}")
114
+ return cleaned.strip() # Retourne le résultat nettoyé final
115
 
116
  # --- Fonction Principale (API GenAI) ---
117
  def generate_complete_latex(client, image):
 
134
  3. Rédige la solution complète directement en code LaTeX, en respectant **toutes** les spécifications ci-dessous.
135
 
136
  # SPÉCIFICATIONS TECHNIQUES DU CODE LATEX
137
+ 1. **Structure du Document:** Commence **strictement** par `\\documentclass{{article}}` et se termine **strictement** par `\\end{{document}}`. N'ajoute rien avant `\\documentclass` ou après `\\end{{document}}`.
138
+ 2. **Packages Requis:** Inclus impérativement les packages suivants dans le préambule (entre `\\documentclass` et `\\begin{{document}}`): `amsmath`, `amssymb`, `geometry` (avec `\\geometry{{a4paper, margin=2cm}}`), `hyperref` (charge-le en dernier si possible), `graphicx` (si des figures sont nécessaires), `inputenc` (avec `\\usepackage[utf8]{{inputenc}}`), `fontenc` (avec `\\usepackage[T1]{{fontenc}}`), `babel` (avec `\\usepackage[french]{{babel}}`), `url`.
139
+ 3. **Encodage:** Assure-toi que le document est encodé en UTF-8 (implicite avec `inputenc`). Utilise correctement les accents français (é, à, ç, etc.) directement ou via les commandes LaTeX appropriées si nécessaire.
140
+ 4. **Compilabilité:** Le code généré doit être valide et compilable sans erreur avec `pdflatex`. Évite les packages ou commandes obsolètes ou trop exotiques.
141
+ 5. **Formatage du Code:** Produis un code LaTeX propre, bien indenté et lisible.
142
+ 6. **Environnements Mathématiques:** Utilise les environnements LaTeX appropriés (`align`, `align*`, `equation`, `gather`, `cases`, etc.) pour présenter les calculs et les équations de manière claire et standard. Préfère `align*` pour les suites d'égalités non numérotées. Utilise `\( ... \)` ou `$ ... $` pour les maths en ligne, et `\[ ... \]` ou `$$ ... $$` (préférer `\[ ... \]` ou `equation*`) pour les maths hors ligne non numérotées.
143
+ 7. **AUCUN Marqueur de Code:** Le résultat doit être **uniquement** le code LaTeX brut. N'inclus **JAMAIS** de marqueurs de code comme ```latex ... ``` ou ``` ... ``` au début ou à la fin.
144
 
145
  # STYLE & CONTENU DE LA SOLUTION
146
+ 1. **Pédagogie:** La correction doit être claire, aérée et facile à comprendre pour un élève de Terminale S. Utilise des phrases complètes.
147
+ 2. **Justifications:** Justifie **chaque** étape clé du raisonnement mathématique. Explique *pourquoi* une certaine méthode est utilisée ou *comment* on passe d'une étape à l'autre. N'hésite pas à rappeler brièvement une propriété ou une formule utilisée.
148
+ 3. **Rigueur:** Assure l'exactitude mathématique complète de la solution. Vérifie les calculs.
149
+ 4. **Structure Logique:** Organise la solution de manière logique. Numérote les questions si l'exercice en comporte plusieurs. Utilise des sections (`\\section*{{...}}`, `\\subsection*{{...}}`) si cela améliore la clarté pour des problèmes longs ou multi-parties. Commence par un titre simple comme `\\title*{{Correction de l'exercice}} \\date{{}} \\maketitle`.
150
  5. **Mention Obligatoire:** Insère la ligne suivante **exactement** telle quelle, juste avant la ligne `\\end{{document}}`:
151
  {LATEX_MENTION}
152
 
153
  # PROCESSUS INTERNE RECOMMANDÉ (Pour l'IA)
154
+ 1. **Analyse Approfondie:** Décompose le problème en sous-étapes logiques. Identifie les concepts mathématiques clés.
155
+ 2. **Résolution Étape par Étape:** Effectue la résolution mathématique complète en interne. Sois très détaillé dans ton raisonnement.
156
+ 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. Porte une attention particulière aux packages requis et à la structure du document. Vérifie que la mention obligatoire est bien placée.
157
  """
158
  try:
159
+ current_app.logger.info(f"Génération LaTeX demandée au modèle {MODEL_SINGLE_GENERATION}...")
160
  response = client.models.generate_content(
161
  model=MODEL_SINGLE_GENERATION,
162
  contents=[image, prompt],
163
  config=types.GenerateContentConfig(
164
+ temperature=0.3, # Température basse pour plus de consistance
165
+ # max_output_tokens=8192, # Augmenter si nécessaire
166
+ ),
167
+ # request_options={'timeout': 120} # Timeout pour la requête API elle-même si besoin
168
+ )
169
 
170
  latex_content_raw = None
171
  thinking_content = None
172
 
173
  # Extrait le contenu LaTeX et le raisonnement (thought)
174
+ # La structure peut varier légèrement, tentons d'être robustes
175
+ if response.candidates:
176
+ candidate = response.candidates[0]
177
+ if candidate.content and candidate.content.parts:
178
+ # Parfois le 'thought' est dans une part séparée, parfois attaché
179
+ if hasattr(candidate, 'thought') and candidate.thought:
180
+ thinking_content = candidate.thought
181
+ # Le contenu textuel est généralement la dernière partie
182
+ latex_content_raw = candidate.text # .text combine les parts textuelles
183
+ # Chercher explicitement le 'thought' dans les parts si non trouvé avant
184
+ if not thinking_content:
185
+ for part in candidate.content.parts:
186
+ if hasattr(part, 'thought') and part.thought:
187
+ thinking_content = part.thought
188
+ # Si le thought est dans une part, le latex brut est peut-être ailleurs
189
+ # Re-extraire le texte des autres parts si nécessaire
190
+ text_parts = [p.text for p in candidate.content.parts if not hasattr(p, 'thought')]
191
+ if text_parts:
192
+ latex_content_raw = "\n".join(text_parts)
193
+ break # Supposons qu'il n'y a qu'un seul 'thought'
194
+
195
+ # Log de ce qui a été extrait
196
+ current_app.logger.info(f"Génération terminée. Contenu brut extrait: {'Oui' if latex_content_raw else 'Non'}, Thinking process extrait: {'Oui' if thinking_content else 'Non'}")
197
+ if thinking_content:
198
+ current_app.logger.debug(f"Thinking process:\n{thinking_content[:500]}...")
199
+
200
+ else:
201
+ current_app.logger.warning("Aucun candidat trouvé dans la réponse de l'API.")
202
+ # Tenter de voir s'il y a une erreur dans la réponse 'prompt_feedback'
203
+ if response.prompt_feedback and response.prompt_feedback.block_reason:
204
+ block_reason = response.prompt_feedback.block_reason.name
205
+ current_app.logger.error(f"Génération bloquée par l'API. Raison: {block_reason}")
206
+ safety_ratings = response.prompt_feedback.safety_ratings
207
+ current_app.logger.error(f"Safety Ratings: {safety_ratings}")
208
+ return None, None, f"Génération bloquée (Raison: {block_reason}). Vérifiez les Safety Ratings dans les logs serveur."
209
+ else:
210
+ return None, None, "Aucun candidat retourné par l'API GenAI."
211
+
212
 
213
  if latex_content_raw:
214
  latex_content_cleaned = clean_latex(latex_content_raw)
215
+
216
+ # Vérification et insertion de la mention obligatoire AVANT la balise de fin
217
+ end_doc_tag = "\\end{document}"
218
  if LATEX_MENTION not in latex_content_cleaned:
219
+ current_app.logger.warning("Mention obligatoire non trouvée dans le LaTeX nettoyé. Tentative d'insertion.")
220
+ if end_doc_tag in latex_content_cleaned:
221
+ # Insère juste avant \end{document}
222
+ cleaned_parts = latex_content_cleaned.rsplit(end_doc_tag, 1)
223
+ latex_content_cleaned = cleaned_parts[0].rstrip() + f"\n{LATEX_MENTION}\n{end_doc_tag}" + cleaned_parts[1] # Conserve ce qui pourrait être après (même si clean_latex devrait l'enlever)
224
+ else:
225
+ # Si \end{document} manque (malgré clean_latex), on l'ajoute avec la mention
226
+ current_app.logger.warning(f"'{end_doc_tag}' manquait aussi. Ajout de la mention et de la balise à la fin.")
227
+ latex_content_cleaned += f"\n{LATEX_MENTION}\n{end_doc_tag}"
228
+ else:
229
+ current_app.logger.info("Mention obligatoire trouvée dans le LaTeX nettoyé.")
230
+
231
+
232
  return latex_content_cleaned, thinking_content, None
233
  else:
234
+ current_app.logger.error("Aucun contenu LaTeX brut n'a pu être extrait de la réponse de l'API.")
235
+ return None, thinking_content, "Aucun contenu LaTeX n'a été généré ou extrait de la réponse de l'API."
236
 
237
  except types.StopCandidateException as e:
238
+ current_app.logger.error(f"Génération stoppée par StopCandidateException: {e}")
239
+ return None, None, f"Génération stoppée (possible contenu inapproprié ou autre raison interne à l'API): {e}"
240
  except Exception as e:
241
+ current_app.logger.exception("Erreur imprévue lors de la génération du LaTeX via l'API GenAI.")
242
  return None, None, f"Erreur lors de la génération du LaTeX: {str(e)}"
243
 
244
  # --- Fonction de Compilation LaTeX ---
 
247
  Compile une chaîne de caractères contenant du code LaTeX en fichier PDF.
248
  Utilise un répertoire temporaire pour les fichiers intermédiaires.
249
  """
250
+ if not latex_content or not latex_content.strip():
251
+ current_app.logger.error("Tentative de compilation de contenu LaTeX vide.")
252
  return None, "Impossible de compiler : contenu LaTeX vide."
253
 
254
  # Utilise un répertoire temporaire sécurisé qui sera nettoyé automatiquement
255
  with tempfile.TemporaryDirectory() as temp_dir:
256
+ base_filename = "solution"
257
+ tex_filename = f"{base_filename}.tex"
258
+ pdf_filename = f"{base_filename}.pdf"
259
+ log_filename = f"{base_filename}.log"
260
+ aux_filename = f"{base_filename}.aux" # Aussi utile pour le debug
261
+
262
  tex_filepath = os.path.join(temp_dir, tex_filename)
263
  pdf_filepath = os.path.join(temp_dir, pdf_filename)
264
+ log_filepath = os.path.join(temp_dir, log_filename)
265
+ aux_filepath = os.path.join(temp_dir, aux_filename)
266
+
267
+ current_app.logger.info(f"Tentative de compilation LaTeX dans le répertoire temporaire : {temp_dir}")
268
+ current_app.logger.debug(f"Chemin du fichier .tex : {tex_filepath}")
269
 
270
  # Écrit le contenu LaTeX dans le fichier .tex avec encodage UTF-8
271
  try:
272
  with open(tex_filepath, "w", encoding="utf-8") as f:
273
  f.write(latex_content)
274
+ current_app.logger.info(f"Fichier .tex écrit avec succès ({len(latex_content)} octets).")
275
  except IOError as e:
276
+ error_msg = f"Erreur lors de l'écriture du fichier .tex temporaire: {str(e)}"
277
+ current_app.logger.error(error_msg)
278
+ return None, error_msg
279
 
280
  # Exécute pdflatex
281
+ # -interaction=nonstopmode : N'attend pas d'input utilisateur en cas d'erreur
282
+ # -halt-on-error : S'arrête à la première erreur (peut être utile pour debug, mais nonstopmode est plus courant pour l'automatisation)
283
+ # -file-line-error : Affiche les erreurs avec nom de fichier et numéro de ligne
284
  command = [
285
  "pdflatex",
286
  "-interaction=nonstopmode",
287
+ "-file-line-error",
288
  f"-output-directory={temp_dir}",
289
+ f"-jobname={base_filename}", # Nom sans extension
290
  tex_filepath
291
  ]
292
+ current_app.logger.info(f"Commande pdflatex (passe 1): {' '.join(command)}")
293
 
294
  pdf_generated = False
295
+ compile_log_lines = [] # Stocke toutes les lignes de log (stdout, stderr, infos custom)
296
+ max_passes = 2 # Généralement suffisant pour les références croisées/table des matières simples
297
+
298
+ for pass_num in range(1, max_passes + 1):
299
+ compile_log_lines.append(f"--- Compilation LaTeX (Passe {pass_num}/{max_passes}) ---")
300
+ current_app.logger.info(f"Lancement de la passe {pass_num} de pdflatex...")
301
  try:
302
  # Augmentation du timeout pour les compilations potentiellement longues
303
+ result = subprocess.run(command, capture_output=True, check=False, # check=False pour analyser nous-mêmes le résultat
304
+ text=True, encoding='utf-8', errors='replace', timeout=90) # Timeout augmenté à 90s
305
+
306
+ compile_log_lines.append(f"Code de retour pdflatex: {result.returncode}")
307
+ compile_log_lines.append("--- Sortie Standard (stdout) ---")
308
+ compile_log_lines.append(result.stdout if result.stdout else "Aucune sortie standard.")
309
+ compile_log_lines.append("--- Sortie Erreur (stderr) ---")
310
+ compile_log_lines.append(result.stderr if result.stderr else "Aucune sortie d'erreur.")
311
+
312
+ # Essayer de lire le fichier .log de LaTeX pour des détails cruciaux
313
+ log_content = ""
314
+ try:
315
+ if os.path.exists(log_filepath):
316
+ with open(log_filepath, "r", encoding="utf-8", errors='replace') as log_file:
317
+ log_content = log_file.read()
318
+ compile_log_lines.append(f"--- Contenu du fichier log ({log_filename}) ---")
319
+ compile_log_lines.append(log_content)
320
+ current_app.logger.debug(f"Fichier log lu ({len(log_content)} octets).")
321
+ else:
322
+ compile_log_lines.append(f"--- Fichier log ({log_filename}) non trouvé. ---")
323
+ current_app.logger.warning(f"Fichier log non trouvé après la passe {pass_num}.")
324
+
325
+ except IOError as log_e:
326
+ log_read_error = f"Impossible de lire le fichier log de LaTeX ({log_filename}): {log_e}"
327
+ compile_log_lines.append(f"--- ERREUR LECTURE LOG: {log_read_error} ---")
328
+ current_app.logger.error(log_read_error)
329
+
330
+
331
+ # Vérification cruciale : le PDF existe-t-il ?
332
  if os.path.exists(pdf_filepath):
333
+ current_app.logger.info(f"Fichier PDF trouvé après la passe {pass_num}.")
334
  pdf_generated = True
335
+ # Si le PDF est là et c'est la dernière passe, ou s'il n'y a pas d'erreurs majeures signalées
336
+ # dans le log (heuristique simple : chercher "!")
337
+ # et que le return code est 0 (ou même 1 parfois avec nonstopmode si le PDF est quand même généré)
338
+ # on pourrait arrêter plus tôt. Pour la robustesse, faisons les 2 passes si pas d'erreur fatale.
339
+ # Si une erreur fatale est détectée (e.g., "! LaTeX Error:"), on arrête.
340
+ if "! LaTeX Error:" in log_content or "Fatal error occurred" in log_content:
341
+ current_app.logger.error(f"Erreur fatale détectée dans le log LaTeX pendant la passe {pass_num}. Arrêt de la compilation.")
342
+ compile_log_lines.append("--- ERREUR FATALE DÉTECTÉE DANS LE LOG - COMPILATION INTERROMPUE ---")
343
+ return None, "\n".join(compile_log_lines)
344
+
345
+ # Si on est à la dernière passe et le PDF existe, c'est bon signe
346
+ if pass_num == max_passes:
347
+ break # Sortir de la boucle des passes
348
+
349
+ else:
350
+ # Si le PDF n'existe pas après une passe, il y a un problème sérieux
351
+ current_app.logger.error(f"Fichier PDF non généré après la passe {pass_num}.")
352
+ compile_log_lines.append(f"--- ERREUR: Fichier PDF non trouvé après la passe {pass_num}. ---")
353
+ # Si le fichier log existe, il contient probablement la raison
354
+ if not log_content and os.path.exists(log_filepath): # Relire le log si pas déjà fait
355
+ try:
356
+ with open(log_filepath, "r", encoding="utf-8", errors='replace') as log_file:
357
+ log_content = log_file.read()
358
+ compile_log_lines.append(f"--- Contenu du fichier log ({log_filename}) après échec PDF ---")
359
+ compile_log_lines.append(log_content)
360
+ except IOError: pass # Déjà loggué plus haut si erreur de lecture
361
+
362
+ # Inutile de faire une autre passe si la première a échoué si gravement
363
+ return None, "\n".join(compile_log_lines)
364
+
365
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
  except subprocess.TimeoutExpired:
367
+ timeout_msg = f"La compilation LaTeX (Passe {pass_num}) a dépassé le délai de {90} secondes. Le document est peut-être trop complexe ou contient une boucle infinie."
368
+ current_app.logger.error(timeout_msg)
369
+ compile_log_lines.append(f"--- ERREUR: TIMEOUT ({timeout_msg}) ---")
370
+ # Essayer de lire le log même après timeout
371
+ try:
372
+ if os.path.exists(log_filepath):
373
+ with open(log_filepath, "r", encoding="utf-8", errors='replace') as log_file:
374
+ compile_log_lines.append(f"--- Contenu partiel du log ({log_filename}) après timeout ---")
375
+ compile_log_lines.append(log_file.read()[-5000:]) # Derniers 5000 caractères
376
+ except IOError: pass
377
+ return None, "\n".join(compile_log_lines)
378
+
379
  except Exception as e:
380
+ exec_error = f"Une erreur inattendue est survenue lors de l'exécution de pdflatex (Passe {pass_num}): {str(e)}"
381
+ current_app.logger.exception(exec_error) # Log l'exception complète
382
+ compile_log_lines.append(f"--- ERREUR SYSTÈME PENDANT L'EXÉCUTION: {exec_error} ---")
383
+ return None, "\n".join(compile_log_lines)
384
+
385
+ # Fin de la boucle des passes
386
 
387
+ # Vérifie si le fichier PDF existe après toutes les passes
388
  if pdf_generated and os.path.exists(pdf_filepath):
389
  try:
390
+ current_app.logger.info("Compilation terminée avec succès. Lecture du fichier PDF...")
391
  # Lit le contenu binaire du PDF généré
392
  with open(pdf_filepath, "rb") as f:
393
  pdf_data = f.read()
394
+ current_app.logger.info(f"PDF lu avec succès ({len(pdf_data)} octets).")
395
+ # Retourner aussi une partie du log pour info, même en cas de succès
396
+ success_log_summary = "\n".join(compile_log_lines[-10:]) # Dernières 10 lignes du log interne
397
+ return pdf_data, f"PDF généré avec succès !\n--- Extrait final du log ---\n{success_log_summary}"
398
  except IOError as e:
399
+ read_error = f"PDF généré mais erreur lors de la lecture du fichier: {str(e)}"
400
+ current_app.logger.error(read_error)
401
+ compile_log_lines.append(f"--- ERREUR LECTURE PDF: {read_error} ---")
402
+ return None, "\n".join(compile_log_lines)
403
  else:
404
+ current_app.logger.error("Le fichier PDF n'a pas été généré après toutes les passes de compilation.")
405
+ compile_log_lines.append("--- ÉCHEC FINAL: Le fichier PDF n'a pas été trouvé après toutes les passes. ---")
406
+ # Le log contient déjà les détails des erreurs des passes précédentes.
407
+ return None, "\n".join(compile_log_lines)
408
+
 
 
 
 
 
409
 
410
  @app.route('/')
411
  def index():
412
+ # Log l'accès à la page d'accueil
413
+ current_app.logger.info(f"Requête reçue pour / par {request.remote_addr}")
414
  return render_template('index.html')
415
 
416
  @app.route('/check-latex')
417
  def check_latex():
418
+ current_app.logger.info(f"Requête reçue pour /check-latex par {request.remote_addr}")
419
  latex_ok, message = check_latex_installation()
420
  return jsonify({
421
  "success": latex_ok,
 
424
 
425
  @app.route('/process-image', methods=['POST'])
426
  def process_image():
427
+ current_app.logger.info(f"Requête reçue pour /process-image par {request.remote_addr}")
428
  if 'image' not in request.files:
429
+ current_app.logger.warning("Aucune image reçue dans la requête /process-image")
430
  return jsonify({"success": False, "message": "Aucune image téléchargée"})
431
+
432
  file = request.files['image']
433
  if file.filename == '':
434
+ current_app.logger.warning("Nom de fichier vide dans la requête /process-image")
435
  return jsonify({"success": False, "message": "Aucun fichier sélectionné"})
436
+
437
+ # Vérifier l'extension (simple vérification côté serveur)
438
+ allowed_extensions = {'png', 'jpg', 'jpeg'}
439
+ if '.' not in file.filename or file.filename.rsplit('.', 1)[1].lower() not in allowed_extensions:
440
+ current_app.logger.warning(f"Type de fichier non autorisé: {file.filename}")
441
+ return jsonify({"success": False, "message": "Type de fichier non autorisé. Utilisez PNG, JPG ou JPEG."})
442
+
443
+ current_app.logger.info(f"Image reçue: {file.filename}, type: {file.mimetype}")
444
+
445
  try:
446
  # Initialisation du client GenAI
447
  client, client_message = initialize_genai_client()
448
  if not client:
449
+ # Message déjà loggué dans initialize_genai_client
450
  return jsonify({"success": False, "message": client_message})
451
+
452
  # Traitement de l'image
453
+ current_app.logger.info("Ouverture de l'image...")
454
+ try:
455
+ image = Image.open(file.stream)
456
+ # Optionnel: Redimensionner si trop grande pour l'API ?
457
+ # image.thumbnail((1024, 1024))
458
+ current_app.logger.info(f"Image ouverte avec succès. Format: {image.format}, Taille: {image.size}")
459
+ except Exception as img_e:
460
+ current_app.logger.error(f"Erreur lors de l'ouverture de l'image: {img_e}")
461
+ return jsonify({"success": False, "message": f"Impossible de lire le fichier image: {img_e}"})
462
+
463
+
464
  # Génération du LaTeX
465
  latex_content, thinking_process, error_message = generate_complete_latex(client, image)
466
+
467
+ if error_message:
468
+ current_app.logger.error(f"Erreur retournée par generate_complete_latex: {error_message}")
469
+ # Ne pas retourner immédiatement s'il y a quand même du contenu LaTeX partiel
470
+ # return jsonify({"success": False, "message": error_message})
471
+
472
  if not latex_content:
473
+ current_app.logger.error("Échec final de la génération LaTeX, aucun contenu retourné.")
474
+ # S'assurer qu'un message d'erreur est fourni
475
+ final_error_msg = error_message or "Échec de la génération de la solution LaTeX par l'API."
476
  return jsonify({
477
  "success": False,
478
+ "message": final_error_msg,
479
+ "thinking": thinking_process # Retourner le thinking même si le latex a échoué
480
  })
481
+
482
+ # S'il y avait une erreur non fatale mais qu'on a du contenu, on le logge et on continue
483
+ if error_message:
484
+ current_app.logger.warning(f"Erreur mineure durant la génération LaTeX ({error_message}), mais du contenu a été produit. Tentative de compilation...")
485
+
486
+
487
  # Compilation en PDF
488
+ current_app.logger.info("Lancement de la compilation LaTeX vers PDF...")
489
  pdf_data, pdf_message = compile_latex_to_pdf(latex_content)
490
+
491
  if not pdf_data:
492
+ current_app.logger.error(f"Échec de la compilation PDF. Message/Log: {pdf_message[:1000]}...") # Log début du message/log d'erreur
493
  return jsonify({
494
  "success": False,
495
+ "message": "Échec de la compilation PDF.", # Message générique pour l'utilisateur
496
  "latex": latex_content,
497
  "thinking": thinking_process,
498
+ "compilation_log": pdf_message # Le log complet est crucial ici
499
  })
500
+
501
+ # Succès !
502
+ current_app.logger.info("PDF généré et lu avec succès.")
503
  # Convertir le PDF en base64 pour l'affichage dans le navigateur
504
  pdf_base64 = base64.b64encode(pdf_data).decode('utf-8')
505
+ current_app.logger.info(f"PDF encodé en base64 (longueur: {len(pdf_base64)}).")
506
+
507
  # Préparer les données de réponse
508
  return jsonify({
509
  "success": True,
510
  "message": "PDF généré avec succès",
511
  "latex": latex_content,
512
  "thinking": thinking_process,
513
+ "pdf_base64": pdf_base64,
514
+ "compilation_log": pdf_message # Retourner aussi le log de succès (peut contenir des warnings utiles)
515
  })
516
+
517
  except Exception as e:
518
+ current_app.logger.exception("Erreur non gérée dans /process-image") # Log l'exception complète
519
  return jsonify({
520
  "success": False,
521
+ "message": f"Erreur serveur inattendue lors du traitement: {str(e)}"
522
  })
523
 
524
+ # @app.route('/download-pdf', methods=['POST']) # Déprécié, téléchargement via JS+Base64
525
+ # def download_pdf():
526
+ # # ... (gardé pour référence mais non utilisé par le frontend actuel)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
 
528
  if __name__ == '__main__':
529
+ # Utiliser '0.0.0.0' pour rendre accessible sur le réseau local si besoin
530
+ # debug=True active le rechargement auto et le débuggeur (NE PAS UTILISER EN PRODUCTION)
531
+ app.run(debug=True, port=5000)