Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -1,4 +1,6 @@
|
|
1 |
-
|
|
|
|
|
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"
|
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 |
-
|
|
|
|
|
23 |
return True, "Installation LaTeX (pdflatex) trouvée."
|
24 |
except (subprocess.CalledProcessError, FileNotFoundError, TimeoutError) as e:
|
25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
|
27 |
def initialize_genai_client():
|
28 |
"""Initialise et retourne le client Google GenAI."""
|
29 |
try:
|
30 |
-
|
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 |
-
|
|
|
|
|
|
|
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 |
-
|
|
|
45 |
|
|
|
46 |
cleaned = raw_latex_text.strip()
|
47 |
|
48 |
-
# Supprime les marqueurs de bloc de code (```latex ... ``` ou ``` ... ```)
|
49 |
-
if cleaned.startswith("```
|
50 |
-
cleaned = cleaned[
|
51 |
-
|
52 |
-
|
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].
|
62 |
|
63 |
-
#
|
64 |
-
if not cleaned.startswith("\\documentclass"):
|
65 |
-
|
|
|
|
|
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 |
-
|
71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
72 |
else:
|
73 |
-
|
74 |
-
|
75 |
-
|
|
|
|
|
|
|
76 |
|
77 |
-
|
|
|
|
|
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
|
102 |
-
3. **
|
103 |
-
4. **
|
104 |
-
5. **
|
105 |
-
6. **
|
|
|
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 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
138 |
|
139 |
if latex_content_raw:
|
140 |
latex_content_cleaned = clean_latex(latex_content_raw)
|
141 |
-
|
142 |
-
# Vérification
|
|
|
143 |
if LATEX_MENTION not in latex_content_cleaned:
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
|
|
|
|
|
|
|
|
|
|
152 |
return latex_content_cleaned, thinking_content, None
|
153 |
else:
|
154 |
-
|
|
|
155 |
|
156 |
except types.StopCandidateException as e:
|
157 |
-
|
|
|
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 |
-
|
173 |
-
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
184 |
|
185 |
# Exécute pdflatex
|
|
|
|
|
|
|
186 |
command = [
|
187 |
"pdflatex",
|
188 |
"-interaction=nonstopmode",
|
|
|
189 |
f"-output-directory={temp_dir}",
|
190 |
-
f"-jobname={
|
191 |
tex_filepath
|
192 |
]
|
|
|
193 |
|
194 |
pdf_generated = False
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
|
|
|
|
199 |
try:
|
200 |
# Augmentation du timeout pour les compilations potentiellement longues
|
201 |
-
result = subprocess.run(command, capture_output=True, check=
|
202 |
-
encoding='utf-8', errors='replace', timeout=
|
203 |
-
|
204 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
228 |
except Exception as e:
|
229 |
-
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
238 |
except IOError as e:
|
239 |
-
|
|
|
|
|
|
|
240 |
else:
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
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 |
-
|
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":
|
|
|
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 |
-
|
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 |
-
|
|
|
|
|
|
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)
|