Docfile commited on
Commit
28b64a0
·
verified ·
1 Parent(s): 04f1d36

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +351 -0
app.py ADDED
@@ -0,0 +1,351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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:
28
+ # Tente d'exécuter pdflatex pour vérifier son existence et son fonctionnement
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."
40
+
41
+ client = genai.Client(api_key=api_key, http_options={'api_version':'v1alpha'})
42
+ return client, "Client Mariam AI initialisé."
43
+ except Exception as e:
44
+ return None, f"Erreur lors de l'initialisation du client: {e}"
45
+
46
+ def clean_latex(raw_latex_text):
47
+ """Nettoie le code LaTeX brut potentiellement fourni par Gemini."""
48
+ if not isinstance(raw_latex_text, str):
49
+ return "" # Retourne une chaîne vide si l'entrée n'est pas une chaîne
50
+
51
+ cleaned = raw_latex_text.strip()
52
+
53
+ # Supprime les marqueurs de bloc de code (```latex ... ``` ou ``` ... ```)
54
+ if cleaned.startswith("```latex"):
55
+ cleaned = cleaned[8:]
56
+ elif cleaned.startswith("```"):
57
+ # Gère le cas où il y a un saut de ligne après ```
58
+ lines = cleaned.split('\n', 1)
59
+ if len(lines) > 1:
60
+ cleaned = lines[1]
61
+ else:
62
+ cleaned = cleaned[3:] # Si pas de saut de ligne, juste enlever ```
63
+
64
+ # Supprime les marqueurs de fin de bloc de code
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}"
74
+ if end_doc_tag in cleaned:
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,
88
+ en utilisant un modèle capable de "réfléchir" (thinking) et de produire un document bien formaté.
89
+ """
90
+ prompt = f"""
91
+ # ROLE & OBJECTIF
92
+ Agis en tant qu'expert en mathématiques et tuteur pédagogue. Ton objectif est de créer une correction détaillée et irréprochable pour l'exercice mathématique présenté dans l'image fournie. La correction doit être sous forme de document LaTeX complet et directement compilable.
93
+
94
+ # CONTEXTE
95
+ - **Input:** Une image contenant un exercice de mathématiques.
96
+ - **Niveau Cible:** Élève de Terminale S (Lycée, filière scientifique française).
97
+ - **Output Attendu:** Un fichier source LaTeX (.tex) autonome.
98
+
99
+ # TÂCHE PRINCIPALE
100
+ 1. Analyse l'image pour comprendre parfaitement l'énoncé de l'exercice.
101
+ 2. Résous l'exercice de manière rigoureuse, étape par étape.
102
+ 3. Rédige la solution complète directement en code LaTeX, en respectant **toutes** les spécifications ci-dessous.
103
+
104
+ # SPÉCIFICATIONS TECHNIQUES DU CODE LATEX
105
+ 1. **Structure du Document:** Commence **strictement** par `\\documentclass{{article}}` et se termine **strictement** par `\\end{{document}}`.
106
+ 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.
107
+ 3. **Compilabilité:** Le code généré doit être valide et compilable sans erreur avec `pdflatex`.
108
+ 4. **Formatage du Code:** Produis un code LaTeX propre, bien indenté et lisible.
109
+ 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.
110
+ 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.
111
+
112
+ # STYLE & CONTENU DE LA SOLUTION
113
+ 1. **Pédagogie:** La correction doit être claire, aérée et facile à comprendre pour un élève de Terminale S.
114
+ 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.
115
+ 3. **Rigueur:** Assure l'exactitude mathématique complète de la solution.
116
+ 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.
117
+ 5. **Mention Obligatoire:** Insère la ligne suivante **exactement** telle quelle, juste avant la ligne `\\end{{document}}`:
118
+ {LATEX_MENTION}
119
+
120
+ # PROCESSUS INTERNE RECOMMANDÉ (Pour l'IA)
121
+ 1. **Analyse Approfondie:** Décompose le problème en sous-étapes logiques.
122
+ 2. **Résolution Étape par Étape:** Effectue la résolution mathématique complète en interne.
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],
131
+ config=types.GenerateContentConfig(
132
+ temperature=0.3,
133
+ ))
134
+
135
+ latex_content_raw = None
136
+ thinking_content = None
137
+
138
+ # Extrait le contenu LaTeX et le raisonnement (thought)
139
+ if response.candidates and response.candidates[0].content and response.candidates[0].content.parts:
140
+ for part in response.candidates[0].content.parts:
141
+ if getattr(part, 'thought', None):
142
+ thinking_content = part.text
143
+ else:
144
+ latex_content_raw = part.text
145
+
146
+ if latex_content_raw:
147
+ latex_content_cleaned = clean_latex(latex_content_raw)
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:
155
+ latex_content_cleaned = latex_content_cleaned.replace(end_doc_tag, f"{LATEX_MENTION}\n{end_doc_tag}")
156
+ else:
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.
171
+ Utilise un répertoire temporaire pour les fichiers intermédiaires.
172
+ """
173
+ if not latex_content:
174
+ return None, "Impossible de compiler : contenu LaTeX vide."
175
+
176
+ # Utilise un répertoire temporaire sécurisé qui sera nettoyé automatiquement
177
+ with tempfile.TemporaryDirectory() as temp_dir:
178
+ tex_filename = "solution.tex"
179
+ pdf_filename = "solution.pdf"
180
+ tex_filepath = os.path.join(temp_dir, tex_filename)
181
+ pdf_filepath = os.path.join(temp_dir, pdf_filename)
182
+ log_filepath = os.path.join(temp_dir, "solution.log") # Pour le débogage
183
+
184
+ # Écrit le contenu LaTeX dans le fichier .tex avec encodage UTF-8
185
+ try:
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",
195
+ f"-output-directory={temp_dir}",
196
+ f"-jobname={os.path.splitext(pdf_filename)[0]}", # Nom sans extension
197
+ tex_filepath
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):
229
+ try:
230
+ # Lit le contenu binaire du PDF généré
231
+ with open(pdf_filepath, "rb") as f:
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)