Docfile commited on
Commit
24e6e88
·
verified ·
1 Parent(s): 3aacddd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +162 -64
app.py CHANGED
@@ -1,8 +1,5 @@
1
- from flask import Flask, render_template, request, jsonify, Response, stream_with_context, send_from_directory
2
- from google import genai
3
  import os
4
- from google.genai import types
5
- from PIL import Image
6
  import io
7
  import base64
8
  import json
@@ -14,146 +11,219 @@ import tempfile
14
  import subprocess
15
  import shutil
16
  import re
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
 
18
  app = Flask(__name__)
19
 
 
20
  GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
21
  TELEGRAM_BOT_TOKEN = "8004545342:AAGcZaoDjYg8dmbbXRsR1N3TfSSbEiAGz88"
22
  TELEGRAM_CHAT_ID = "-1002564204301"
23
  GENERATED_PDF_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'generated_pdfs')
24
 
 
 
25
  if GOOGLE_API_KEY:
26
  try:
27
  client = genai.Client(api_key=GOOGLE_API_KEY)
 
28
  except Exception as e:
29
- print(f"Erreur client Gemini: {e}")
30
- client = None
31
  else:
32
- print("GEMINI_API_KEY non trouvé.")
33
- client = None
34
 
35
  task_results = {}
36
 
 
 
37
  def load_prompt_from_file(filename):
 
38
  try:
39
  prompts_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'prompts')
40
  filepath = os.path.join(prompts_dir, filename)
 
41
  with open(filepath, 'r', encoding='utf-8') as f:
42
  return f.read()
43
  except Exception as e:
44
- print(f"Erreur chargement prompt '{filename}': {e}")
45
  return ""
46
 
47
  def get_prompt_for_style(style):
 
 
48
  return load_prompt_from_file('prompt_light.txt') if style == 'light' else load_prompt_from_file('prompt_colorful.txt')
49
 
50
  def check_latex_installation():
 
 
51
  try:
 
 
52
  subprocess.run(["pdflatex", "-version"], capture_output=True, check=True, timeout=10)
 
53
  return True
54
- except Exception:
 
55
  return False
56
 
57
  IS_LATEX_INSTALLED = check_latex_installation()
58
 
59
  def clean_latex_code(latex_code):
 
 
 
60
  match_latex = re.search(r"```(?:latex|tex)\s*(.*?)\s*```", latex_code, re.DOTALL | re.IGNORECASE)
61
  if match_latex:
 
62
  return match_latex.group(1).strip()
 
 
63
  match_generic = re.search(r"```\s*(\\documentclass.*?)\s*```", latex_code, re.DOTALL | re.IGNORECASE)
64
  if match_generic:
 
65
  return match_generic.group(1).strip()
 
 
66
  return latex_code.strip()
67
 
68
  def latex_to_pdf(latex_code, output_filename_base, output_dir):
 
69
  if not IS_LATEX_INSTALLED:
70
- return None, "pdflatex non disponible."
 
71
 
72
  tex_filename = f"{output_filename_base}.tex"
73
  tex_path = os.path.join(output_dir, tex_filename)
74
  pdf_path = os.path.join(output_dir, f"{output_filename_base}.pdf")
75
 
 
 
76
  try:
 
77
  with open(tex_path, "w", encoding="utf-8") as tex_file:
78
  tex_file.write(latex_code)
 
79
 
 
80
  my_env = os.environ.copy()
81
  my_env["LC_ALL"] = "C.UTF-8"
82
  my_env["LANG"] = "C.UTF-8"
83
 
84
  last_result = None
85
- for _ in range(2):
 
 
86
  process = subprocess.run(
87
  ["pdflatex", "-interaction=nonstopmode", "-output-directory", output_dir, tex_path],
88
- capture_output=True, text=True, check=False, encoding="utf-8", errors="replace", env=my_env,
89
  )
90
  last_result = process
 
91
  if not os.path.exists(pdf_path) and process.returncode != 0:
 
92
  break
93
 
94
  if os.path.exists(pdf_path):
 
95
  return pdf_path, f"PDF généré: {os.path.basename(pdf_path)}"
96
  else:
97
- error_log = last_result.stdout + "\n" + last_result.stderr if last_result else "Aucun résultat de compilation."
98
- return None, f"Erreur de compilation PDF. Log: ...{error_log[-1000:]}"
 
 
99
  except Exception as e:
100
- return None, f"Exception génération PDF: {str(e)}"
 
101
 
102
  def send_to_telegram(file_data, filename, caption="Nouveau fichier uploadé"):
 
 
103
  try:
104
  if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp')):
105
  url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendPhoto"
106
  files = {'photo': (filename, file_data)}
 
107
  else:
108
  url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendDocument"
109
  files = {'document': (filename, file_data)}
 
 
 
110
  data = {'chat_id': TELEGRAM_CHAT_ID, 'caption': caption}
111
- requests.post(url, files=files, data=data, timeout=30)
 
 
112
  except Exception as e:
113
- print(f"Erreur envoi Telegram: {e}")
 
 
 
114
 
115
  def process_files_background(task_id, files_data, resolution_style):
 
 
 
116
  uploaded_file_refs = []
 
117
  try:
118
- task_results[task_id]['status'] = 'processing'
119
  if not client:
120
- raise ConnectionError("Client Gemini non initialisé.")
121
 
122
  contents = []
 
123
  for file_info in files_data:
124
  if file_info['type'].startswith('image/'):
 
125
  img = Image.open(io.BytesIO(file_info['data']))
126
  buffered = io.BytesIO()
127
- img.save(buffered, format="PNG")
128
  img_base64_str = base64.b64encode(buffered.getvalue()).decode()
129
  contents.append({'inline_data': {'mime_type': 'image/png', 'data': img_base64_str}})
 
130
  elif file_info['type'] == 'application/pdf':
131
- try:
132
- with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_pdf:
133
- temp_pdf.write(file_info['data'])
134
- temp_pdf_path = temp_pdf.name
135
- file_ref = client.files.upload(file=temp_pdf_path)
136
- uploaded_file_refs.append(file_ref)
137
- contents.append(file_ref)
138
- os.unlink(temp_pdf_path)
139
- except Exception as e:
140
- raise ValueError(f"Impossible d'uploader le PDF: {str(e)}")
141
-
 
142
  if not contents:
143
- raise ValueError("Aucun contenu valide.")
144
 
145
  prompt_to_use = get_prompt_for_style(resolution_style)
146
  if not prompt_to_use:
147
- raise ValueError(f"Prompt introuvable pour le style '{resolution_style}'.")
148
  contents.append(prompt_to_use)
149
 
150
  task_results[task_id]['status'] = 'generating_latex'
 
151
  gemini_response = client.models.generate_content(
152
  model="gemini-2.5-pro",
153
  contents=contents,
154
  config=types.GenerateContentConfig(tools=[types.Tool(code_execution=types.ToolCodeExecution)])
155
  )
156
 
 
157
  full_latex_response = ""
158
  if gemini_response.candidates and gemini_response.candidates[0].content and gemini_response.candidates[0].content.parts:
159
  for part in gemini_response.candidates[0].content.parts:
@@ -161,10 +231,13 @@ def process_files_background(task_id, files_data, resolution_style):
161
  full_latex_response += part.text
162
 
163
  if not full_latex_response.strip():
164
- raise ValueError("Gemini a retourné une réponse vide.")
 
165
 
166
  task_results[task_id]['status'] = 'cleaning_latex'
167
  cleaned_latex = clean_latex_code(full_latex_response)
 
 
168
 
169
  task_results[task_id]['status'] = 'generating_pdf'
170
  pdf_filename_base = f"solution_{task_id}"
@@ -174,31 +247,44 @@ def process_files_background(task_id, files_data, resolution_style):
174
  task_results[task_id]['status'] = 'completed'
175
  task_results[task_id]['pdf_filename'] = os.path.basename(pdf_file_path)
176
  task_results[task_id]['response'] = f"PDF généré avec succès: {os.path.basename(pdf_file_path)}"
 
177
  else:
178
- raise RuntimeError(f"Échec de la génération PDF: {pdf_message}")
179
 
180
  except Exception as e:
181
- print(f"Task {task_id} Erreur: {e}")
182
  task_results[task_id]['status'] = 'error'
183
  task_results[task_id]['error'] = str(e)
184
- task_results[task_id]['response'] = f"Erreur: {str(e)}"
185
  finally:
186
- for file_ref in uploaded_file_refs:
187
- try: client.files.delete(file_ref)
188
- except: pass
 
 
 
 
 
 
 
 
189
 
190
  @app.route('/')
191
  def index():
 
192
  return render_template('index.html')
193
 
194
  @app.route('/solve', methods=['POST'])
195
  def solve():
 
196
  try:
197
  if 'user_files' not in request.files:
198
- return jsonify({'error': 'Aucun fichier fourni'}), 400
 
199
 
200
  uploaded_files = request.files.getlist('user_files')
201
  if not uploaded_files or all(f.filename == '' for f in uploaded_files):
 
202
  return jsonify({'error': 'Aucun fichier sélectionné'}), 400
203
 
204
  resolution_style = request.form.get('style', 'colorful')
@@ -210,43 +296,46 @@ def solve():
210
  file_data = file.read()
211
  file_type = file.content_type or 'application/octet-stream'
212
 
 
213
  if file_type.startswith('image/'):
214
  file_count['images'] += 1
215
  files_data.append({'filename': file.filename, 'data': file_data, 'type': file_type})
216
- send_to_telegram(file_data, file.filename, f"Image reçue - Style: {resolution_style}")
217
  elif file_type == 'application/pdf':
218
  if file_count['pdfs'] >= 1:
219
- return jsonify({'error': 'Un seul PDF autorisé'}), 400
 
220
  file_count['pdfs'] += 1
221
  files_data.append({'filename': file.filename, 'data': file_data, 'type': file_type})
222
- send_to_telegram(file_data, file.filename, f"PDF reçu - Style: {resolution_style}")
 
 
223
 
224
  if not files_data:
225
- return jsonify({'error': 'Aucun fichier valide (image/pdf) trouvé'}), 400
 
226
 
227
  task_id = str(uuid.uuid4())
228
  task_results[task_id] = {
229
- 'status': 'pending',
230
- 'response': '',
231
- 'error': None,
232
- 'time_started': time.time(),
233
- 'style': resolution_style,
234
- 'file_count': file_count,
235
- 'first_filename': files_data[0]['filename']
236
  }
237
 
 
238
  threading.Thread(target=process_files_background, args=(task_id, files_data, resolution_style)).start()
239
 
240
  return jsonify({'task_id': task_id, 'status': 'pending', 'first_filename': files_data[0]['filename']})
241
 
242
  except Exception as e:
243
- print(f"Erreur /solve: {e}")
244
- return jsonify({'error': f'Erreur serveur: {e}'}), 500
245
 
246
  @app.route('/task/<task_id>', methods=['GET'])
247
  def get_task_status(task_id):
 
248
  task = task_results.get(task_id)
249
  if not task:
 
250
  return jsonify({'error': 'Tâche introuvable'}), 404
251
 
252
  response_data = {'status': task['status'], 'response': task.get('response'), 'error': task.get('error')}
@@ -257,12 +346,15 @@ def get_task_status(task_id):
257
 
258
  @app.route('/stream/<task_id>', methods=['GET'])
259
  def stream_task_progress(task_id):
 
260
  def generate():
 
261
  last_status_sent = None
262
  while True:
263
  task = task_results.get(task_id)
264
  if not task:
265
- yield f'data: {json.dumps({"error": "Tâche disparue", "status": "error"})}\n\n'
 
266
  break
267
 
268
  current_status = task['status']
@@ -274,32 +366,38 @@ def stream_task_progress(task_id):
274
  elif current_status == 'error':
275
  data_to_send["error"] = task.get("error", "Erreur inconnue")
276
 
 
277
  yield f'data: {json.dumps(data_to_send)}\n\n'
278
  last_status_sent = current_status
279
 
280
  if current_status in ['completed', 'error']:
 
281
  break
282
 
283
- time.sleep(1)
284
 
285
  return Response(stream_with_context(generate()), mimetype='text/event-stream', headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
286
 
287
  @app.route('/download/<task_id>')
288
  def download_pdf(task_id):
 
289
  task = task_results.get(task_id)
290
  if not task or task['status'] != 'completed' or 'pdf_filename' not in task:
291
- return "Fichier non trouvé ou non prêt.", 404
 
292
 
293
  try:
 
294
  return send_from_directory(GENERATED_PDF_DIR, task['pdf_filename'], as_attachment=True)
295
  except FileNotFoundError:
296
- return "Fichier introuvable sur le serveur.", 404
 
297
 
298
  if __name__ == '__main__':
 
 
 
299
  os.makedirs(GENERATED_PDF_DIR, exist_ok=True)
300
- if not GOOGLE_API_KEY:
301
- print("CRITICAL: GOOGLE_API_KEY non défini.")
302
- if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
303
- print("CRITICAL: Clés Telegram non définies.")
304
 
305
- app.run(debug=True, host='0.0.0.0', port=5000)
 
1
+ import logging
 
2
  import os
 
 
3
  import io
4
  import base64
5
  import json
 
11
  import subprocess
12
  import shutil
13
  import re
14
+ from flask import Flask, render_template, request, jsonify, Response, stream_with_context, send_from_directory
15
+ from google import genai
16
+ from google.genai import types
17
+ from PIL import Image
18
+
19
+ # --- Configuration du Logging ---
20
+ # Configuration d'un logger qui écrit dans la console (stdout).
21
+ # C'est la pratique recommandée pour les applications conteneurisées (Docker) ou déployées sur des services comme Heroku/Render.
22
+ logging.basicConfig(
23
+ level=logging.INFO, # Niveau de log par défaut. Changer à logging.DEBUG pour plus de détails.
24
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
25
+ datefmt='%Y-%m-%d %H:%M:%S'
26
+ )
27
+ logger = logging.getLogger(__name__)
28
 
29
+ # --- Configuration de l'Application Flask ---
30
  app = Flask(__name__)
31
 
32
+ # --- Constantes et Variables Globales ---
33
  GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
34
  TELEGRAM_BOT_TOKEN = "8004545342:AAGcZaoDjYg8dmbbXRsR1N3TfSSbEiAGz88"
35
  TELEGRAM_CHAT_ID = "-1002564204301"
36
  GENERATED_PDF_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'generated_pdfs')
37
 
38
+ # --- Initialisation des Services Externes ---
39
+ client = None
40
  if GOOGLE_API_KEY:
41
  try:
42
  client = genai.Client(api_key=GOOGLE_API_KEY)
43
+ logger.info("Client Google GenAI initialisé avec succès.")
44
  except Exception as e:
45
+ logger.critical(f"Erreur critique lors de l'initialisation du client Gemini: {e}", exc_info=True)
 
46
  else:
47
+ logger.critical("GEMINI_API_KEY non trouvé dans les variables d'environnement. Le service ne fonctionnera pas.")
 
48
 
49
  task_results = {}
50
 
51
+ # --- Fonctions Utilitaires ---
52
+
53
  def load_prompt_from_file(filename):
54
+ """Charge le contenu d'un fichier de prompt."""
55
  try:
56
  prompts_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'prompts')
57
  filepath = os.path.join(prompts_dir, filename)
58
+ logger.info(f"Chargement du prompt depuis '{filepath}'")
59
  with open(filepath, 'r', encoding='utf-8') as f:
60
  return f.read()
61
  except Exception as e:
62
+ logger.error(f"Erreur lors du chargement du prompt '{filename}': {e}", exc_info=True)
63
  return ""
64
 
65
  def get_prompt_for_style(style):
66
+ """Retourne le prompt approprié en fonction du style demandé."""
67
+ logger.info(f"Sélection du prompt pour le style: '{style}'")
68
  return load_prompt_from_file('prompt_light.txt') if style == 'light' else load_prompt_from_file('prompt_colorful.txt')
69
 
70
  def check_latex_installation():
71
+ """Vérifie si pdflatex est installé et accessible dans le PATH."""
72
+ logger.info("Vérification de l'installation de LaTeX (pdflatex)...")
73
  try:
74
+ # Exécute 'pdflatex -version' pour vérifier son existence.
75
+ # capture_output=True masque la sortie, check=True lève une exception en cas d'échec.
76
  subprocess.run(["pdflatex", "-version"], capture_output=True, check=True, timeout=10)
77
+ logger.info("Vérification réussie: pdflatex est installé et fonctionnel.")
78
  return True
79
+ except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired) as e:
80
+ logger.warning(f"pdflatex n'est pas installé ou n'est pas dans le PATH. La génération de PDF sera désactivée. Erreur: {e}")
81
  return False
82
 
83
  IS_LATEX_INSTALLED = check_latex_installation()
84
 
85
  def clean_latex_code(latex_code):
86
+ """Extrait le code LaTeX brut des blocs de code formatés (```latex ... ```)."""
87
+ logger.info("Nettoyage du code LaTeX reçu de Gemini...")
88
+ # Cherche un bloc de code explicite ```latex ... ```
89
  match_latex = re.search(r"```(?:latex|tex)\s*(.*?)\s*```", latex_code, re.DOTALL | re.IGNORECASE)
90
  if match_latex:
91
+ logger.info("Bloc de code 'latex' ou 'tex' trouvé et extrait.")
92
  return match_latex.group(1).strip()
93
+
94
+ # Plan B : Cherche un bloc de code générique qui commence par \documentclass
95
  match_generic = re.search(r"```\s*(\\documentclass.*?)\s*```", latex_code, re.DOTALL | re.IGNORECASE)
96
  if match_generic:
97
+ logger.info("Bloc de code générique avec '\\documentclass' trouvé et extrait.")
98
  return match_generic.group(1).strip()
99
+
100
+ logger.warning("Aucun bloc de code LaTeX (```...```) n'a été trouvé. Utilisation de la réponse brute.")
101
  return latex_code.strip()
102
 
103
  def latex_to_pdf(latex_code, output_filename_base, output_dir):
104
+ """Compile une chaîne de code LaTeX en fichier PDF."""
105
  if not IS_LATEX_INSTALLED:
106
+ logger.error("Tentative de compilation LaTeX alors que pdflatex n'est pas disponible.")
107
+ return None, "Erreur: pdflatex n'est pas installé sur le serveur."
108
 
109
  tex_filename = f"{output_filename_base}.tex"
110
  tex_path = os.path.join(output_dir, tex_filename)
111
  pdf_path = os.path.join(output_dir, f"{output_filename_base}.pdf")
112
 
113
+ logger.info(f"Début de la compilation LaTeX vers PDF pour '{output_filename_base}'")
114
+
115
  try:
116
+ # Écriture du fichier .tex
117
  with open(tex_path, "w", encoding="utf-8") as tex_file:
118
  tex_file.write(latex_code)
119
+ logger.info(f"Fichier .tex '{tex_path}' créé avec succès.")
120
 
121
+ # Copie de l'environnement et configuration pour UTF-8 pour éviter les erreurs d'encodage
122
  my_env = os.environ.copy()
123
  my_env["LC_ALL"] = "C.UTF-8"
124
  my_env["LANG"] = "C.UTF-8"
125
 
126
  last_result = None
127
+ # Exécution de pdflatex deux fois pour résoudre les références (table des matières, etc.)
128
+ for i in range(2):
129
+ logger.info(f"Exécution de pdflatex - Passe {i+1}/2...")
130
  process = subprocess.run(
131
  ["pdflatex", "-interaction=nonstopmode", "-output-directory", output_dir, tex_path],
132
+ capture_output=True, text=True, check=False, encoding="utf-8", errors="replace", env=my_env, timeout=60
133
  )
134
  last_result = process
135
+ # Si le PDF n'est pas créé et que la première passe a échoué, inutile de continuer
136
  if not os.path.exists(pdf_path) and process.returncode != 0:
137
+ logger.warning(f"La passe {i+1} de pdflatex a échoué et aucun PDF n'a été créé. Arrêt de la compilation.")
138
  break
139
 
140
  if os.path.exists(pdf_path):
141
+ logger.info(f"PDF généré avec succès : '{pdf_path}'")
142
  return pdf_path, f"PDF généré: {os.path.basename(pdf_path)}"
143
  else:
144
+ error_log = last_result.stdout + "\n" + last_result.stderr if last_result else "Aucun résultat de compilation disponible."
145
+ logger.error(f"Échec de la compilation PDF pour '{tex_filename}'. Log de pdflatex:\n{error_log}")
146
+ return None, f"Erreur de compilation PDF. Log: ...{error_log[-1000:]}" # Retourne les 1000 derniers caractères du log
147
+
148
  except Exception as e:
149
+ logger.error(f"Exception pendant la génération du PDF: {e}", exc_info=True)
150
+ return None, f"Exception durant la génération du PDF: {str(e)}"
151
 
152
  def send_to_telegram(file_data, filename, caption="Nouveau fichier uploadé"):
153
+ """Envoie un fichier au canal Telegram configuré."""
154
+ logger.info(f"Préparation de l'envoi du fichier '{filename}' à Telegram.")
155
  try:
156
  if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp')):
157
  url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendPhoto"
158
  files = {'photo': (filename, file_data)}
159
+ log_msg = f"Envoi de l'image '{filename}' à Telegram..."
160
  else:
161
  url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendDocument"
162
  files = {'document': (filename, file_data)}
163
+ log_msg = f"Envoi du document '{filename}' à Telegram..."
164
+
165
+ logger.info(log_msg)
166
  data = {'chat_id': TELEGRAM_CHAT_ID, 'caption': caption}
167
+ response = requests.post(url, files=files, data=data, timeout=30)
168
+ response.raise_for_status() # Lève une exception si le statut HTTP est une erreur (4xx ou 5xx)
169
+ logger.info(f"Fichier '{filename}' envoyé avec succès à Telegram.")
170
  except Exception as e:
171
+ logger.error(f"Erreur lors de l'envoi à Telegram: {e}", exc_info=True)
172
+
173
+
174
+ # --- Logique Principale (Worker en arrière-plan) ---
175
 
176
  def process_files_background(task_id, files_data, resolution_style):
177
+ """Fonction exécutée en thread pour traiter les fichiers, appeler Gemini et générer le PDF."""
178
+ logger.info(f"[Task {task_id}] Démarrage du traitement en arrière-plan.")
179
+ task_results[task_id]['status'] = 'processing'
180
  uploaded_file_refs = []
181
+
182
  try:
 
183
  if not client:
184
+ raise ConnectionError("Le client Gemini n'est pas initialisé.")
185
 
186
  contents = []
187
+ logger.info(f"[Task {task_id}] Préparation des fichiers pour l'API Gemini.")
188
  for file_info in files_data:
189
  if file_info['type'].startswith('image/'):
190
+ logger.info(f"[Task {task_id}] Traitement de l'image '{file_info['filename']}'.")
191
  img = Image.open(io.BytesIO(file_info['data']))
192
  buffered = io.BytesIO()
193
+ img.save(buffered, format="PNG") # Convertit en PNG pour la consistance
194
  img_base64_str = base64.b64encode(buffered.getvalue()).decode()
195
  contents.append({'inline_data': {'mime_type': 'image/png', 'data': img_base64_str}})
196
+
197
  elif file_info['type'] == 'application/pdf':
198
+ logger.info(f"[Task {task_id}] Upload du PDF '{file_info['filename']}' vers Google GenAI File API.")
199
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_pdf:
200
+ temp_pdf.write(file_info['data'])
201
+ temp_pdf_path = temp_pdf.name
202
+
203
+ # Upload du fichier et ajout de la référence à la liste de nettoyage
204
+ file_ref = client.files.upload(file=temp_pdf_path)
205
+ uploaded_file_refs.append(file_ref)
206
+ contents.append(file_ref)
207
+ os.unlink(temp_pdf_path) # Supprime le fichier temporaire local
208
+ logger.info(f"[Task {task_id}] PDF '{file_info['filename']}' uploadé avec succès. Référence: {file_ref.name}")
209
+
210
  if not contents:
211
+ raise ValueError("Aucun contenu valide (image ou PDF) n'a été traité.")
212
 
213
  prompt_to_use = get_prompt_for_style(resolution_style)
214
  if not prompt_to_use:
215
+ raise ValueError(f"Le fichier de prompt pour le style '{resolution_style}' est introuvable ou vide.")
216
  contents.append(prompt_to_use)
217
 
218
  task_results[task_id]['status'] = 'generating_latex'
219
+ logger.info(f"[Task {task_id}] Envoi de la requête à l'API Gemini (modèle gemini-2.5-pro).")
220
  gemini_response = client.models.generate_content(
221
  model="gemini-2.5-pro",
222
  contents=contents,
223
  config=types.GenerateContentConfig(tools=[types.Tool(code_execution=types.ToolCodeExecution)])
224
  )
225
 
226
+ logger.info(f"[Task {task_id}] Réponse reçue de Gemini.")
227
  full_latex_response = ""
228
  if gemini_response.candidates and gemini_response.candidates[0].content and gemini_response.candidates[0].content.parts:
229
  for part in gemini_response.candidates[0].content.parts:
 
231
  full_latex_response += part.text
232
 
233
  if not full_latex_response.strip():
234
+ raise ValueError("La réponse de Gemini était vide.")
235
+ logger.debug(f"[Task {task_id}] Réponse brute de Gemini:\n---\n{full_latex_response[:500]}...\n---")
236
 
237
  task_results[task_id]['status'] = 'cleaning_latex'
238
  cleaned_latex = clean_latex_code(full_latex_response)
239
+ logger.debug(f"[Task {task_id}] Code LaTeX nettoyé:\n---\n{cleaned_latex[:500]}...\n---")
240
+
241
 
242
  task_results[task_id]['status'] = 'generating_pdf'
243
  pdf_filename_base = f"solution_{task_id}"
 
247
  task_results[task_id]['status'] = 'completed'
248
  task_results[task_id]['pdf_filename'] = os.path.basename(pdf_file_path)
249
  task_results[task_id]['response'] = f"PDF généré avec succès: {os.path.basename(pdf_file_path)}"
250
+ logger.info(f"[Task {task_id}] Tâche terminée avec succès. PDF: {os.path.basename(pdf_file_path)}")
251
  else:
252
+ raise RuntimeError(f"Échec de la génération du PDF: {pdf_message}")
253
 
254
  except Exception as e:
255
+ logger.error(f"[Task {task_id}] Une erreur est survenue dans le thread de traitement.", exc_info=True)
256
  task_results[task_id]['status'] = 'error'
257
  task_results[task_id]['error'] = str(e)
258
+ task_results[task_id]['response'] = f"Une erreur est survenue: {str(e)}"
259
  finally:
260
+ # Nettoyage des fichiers uploadés à l'API Gemini
261
+ if uploaded_file_refs:
262
+ logger.info(f"[Task {task_id}] Nettoyage des {len(uploaded_file_refs)} fichiers temporaires de l'API Gemini.")
263
+ for file_ref in uploaded_file_refs:
264
+ try:
265
+ client.files.delete(file_ref)
266
+ logger.info(f"[Task {task_id}] Fichier temporaire Gemini '{file_ref.name}' supprimé.")
267
+ except Exception as del_e:
268
+ logger.warning(f"[Task {task_id}] Échec de la suppression du fichier temporaire Gemini '{file_ref.name}': {del_e}")
269
+
270
+ # --- Routes Flask (API Endpoints) ---
271
 
272
  @app.route('/')
273
  def index():
274
+ logger.info(f"Requête servie pour l'endpoint '/' depuis {request.remote_addr}")
275
  return render_template('index.html')
276
 
277
  @app.route('/solve', methods=['POST'])
278
  def solve():
279
+ logger.info(f"Nouvelle requête sur /solve depuis {request.remote_addr}")
280
  try:
281
  if 'user_files' not in request.files:
282
+ logger.warning(f"/solve: Requête de {request.remote_addr} sans 'user_files'.")
283
+ return jsonify({'error': 'Aucun champ de fichier dans la requête'}), 400
284
 
285
  uploaded_files = request.files.getlist('user_files')
286
  if not uploaded_files or all(f.filename == '' for f in uploaded_files):
287
+ logger.warning(f"/solve: Requête de {request.remote_addr} avec champ 'user_files' mais sans fichiers.")
288
  return jsonify({'error': 'Aucun fichier sélectionné'}), 400
289
 
290
  resolution_style = request.form.get('style', 'colorful')
 
296
  file_data = file.read()
297
  file_type = file.content_type or 'application/octet-stream'
298
 
299
+ # Validation et traitement des fichiers
300
  if file_type.startswith('image/'):
301
  file_count['images'] += 1
302
  files_data.append({'filename': file.filename, 'data': file_data, 'type': file_type})
303
+ send_to_telegram(file_data, file.filename, f"Image reçue: {file.filename} (Style: {resolution_style})")
304
  elif file_type == 'application/pdf':
305
  if file_count['pdfs'] >= 1:
306
+ logger.warning(f"/solve: Requête de {request.remote_addr} avec plusieurs PDFs. Rejetée.")
307
+ return jsonify({'error': 'Un seul fichier PDF est autorisé par requête'}), 400
308
  file_count['pdfs'] += 1
309
  files_data.append({'filename': file.filename, 'data': file_data, 'type': file_type})
310
+ send_to_telegram(file_data, file.filename, f"PDF reçu: {file.filename} (Style: {resolution_style})")
311
+ else:
312
+ logger.warning(f"/solve: Fichier non supporté '{file.filename}' de type '{file_type}' uploadé par {request.remote_addr}.")
313
 
314
  if not files_data:
315
+ logger.warning(f"/solve: Aucun fichier valide (image/pdf) trouvé dans la requête de {request.remote_addr}.")
316
+ return jsonify({'error': 'Aucun fichier valide (image ou PDF) n\'a été fourni'}), 400
317
 
318
  task_id = str(uuid.uuid4())
319
  task_results[task_id] = {
320
+ 'status': 'pending', 'response': '', 'error': None, 'time_started': time.time(),
321
+ 'style': resolution_style, 'file_count': file_count, 'first_filename': files_data[0]['filename']
 
 
 
 
 
322
  }
323
 
324
+ logger.info(f"Création de la tâche {task_id} pour {file_count['images']} image(s) et {file_count['pdfs']} PDF(s). Style: {resolution_style}.")
325
  threading.Thread(target=process_files_background, args=(task_id, files_data, resolution_style)).start()
326
 
327
  return jsonify({'task_id': task_id, 'status': 'pending', 'first_filename': files_data[0]['filename']})
328
 
329
  except Exception as e:
330
+ logger.error(f"Erreur inattendue dans l'endpoint /solve: {e}", exc_info=True)
331
+ return jsonify({'error': f'Erreur interne du serveur: {e}'}), 500
332
 
333
  @app.route('/task/<task_id>', methods=['GET'])
334
  def get_task_status(task_id):
335
+ logger.debug(f"Requête de statut pour la tâche {task_id}")
336
  task = task_results.get(task_id)
337
  if not task:
338
+ logger.warning(f"Tentative d'accès à une tâche inexistante: {task_id}")
339
  return jsonify({'error': 'Tâche introuvable'}), 404
340
 
341
  response_data = {'status': task['status'], 'response': task.get('response'), 'error': task.get('error')}
 
346
 
347
  @app.route('/stream/<task_id>', methods=['GET'])
348
  def stream_task_progress(task_id):
349
+ """Endpoint pour Server-Sent Events (SSE) pour streamer la progression."""
350
  def generate():
351
+ logger.info(f"Nouvelle connexion de streaming (SSE) pour la tâche {task_id}")
352
  last_status_sent = None
353
  while True:
354
  task = task_results.get(task_id)
355
  if not task:
356
+ logger.warning(f"La tâche {task_id} a disparu pendant le streaming.")
357
+ yield f'data: {json.dumps({"error": "La tâche a été perdue", "status": "error"})}\n\n'
358
  break
359
 
360
  current_status = task['status']
 
366
  elif current_status == 'error':
367
  data_to_send["error"] = task.get("error", "Erreur inconnue")
368
 
369
+ logger.info(f"[Task {task_id}] Envoi de la mise à jour de statut via SSE: {current_status}")
370
  yield f'data: {json.dumps(data_to_send)}\n\n'
371
  last_status_sent = current_status
372
 
373
  if current_status in ['completed', 'error']:
374
+ logger.info(f"Fermeture de la connexion SSE pour la tâche terminée/échouée {task_id}")
375
  break
376
 
377
+ time.sleep(1) # Attendre 1 seconde avant de vérifier à nouveau
378
 
379
  return Response(stream_with_context(generate()), mimetype='text/event-stream', headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
380
 
381
  @app.route('/download/<task_id>')
382
  def download_pdf(task_id):
383
+ logger.info(f"Requête de téléchargement pour la tâche {task_id}")
384
  task = task_results.get(task_id)
385
  if not task or task['status'] != 'completed' or 'pdf_filename' not in task:
386
+ logger.warning(f"Échec du téléchargement pour la tâche {task_id}: Fichier non trouvé ou tâche non terminée.")
387
+ return "Fichier non trouvé ou la tâche n'est pas encore terminée.", 404
388
 
389
  try:
390
+ logger.info(f"Envoi du fichier '{task['pdf_filename']}' pour la tâche {task_id}")
391
  return send_from_directory(GENERATED_PDF_DIR, task['pdf_filename'], as_attachment=True)
392
  except FileNotFoundError:
393
+ logger.error(f"Le fichier PDF '{task['pdf_filename']}' pour la tâche {task_id} est introuvable sur le disque.")
394
+ return "Erreur: Fichier introuvable sur le serveur.", 404
395
 
396
  if __name__ == '__main__':
397
+ logger.info("Démarrage de l'application Flask.")
398
+
399
+ # Création du répertoire pour les PDFs générés s'il n'existe pas
400
  os.makedirs(GENERATED_PDF_DIR, exist_ok=True)
401
+ logger.info(f"Répertoire pour les PDFs générés assuré d'exister: '{GENERATED_PDF_DIR}'")
 
 
 
402
 
403
+ # Vérifications cri