Docfile commited on
Commit
e0c2a65
·
verified ·
1 Parent(s): 1f4ef47

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +81 -144
app.py CHANGED
@@ -12,6 +12,7 @@ import time
12
  import tempfile # Added
13
  import subprocess # Added
14
  import shutil # Added
 
15
  import re # Added
16
 
17
  app = Flask(__name__)
@@ -20,9 +21,10 @@ app = Flask(__name__)
20
  GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
21
  # IMPORTANT: For production, move these to environment variables or a secure config
22
  TELEGRAM_BOT_TOKEN = "8004545342:AAGcZaoDjYg8dmbbXRsR1N3TfSSbEiAGz88"
23
- TELEGRAM_CHAT_ID = "-1002564204301"
24
 
25
  # Initialize Gemini client
 
26
  if GOOGLE_API_KEY:
27
  try:
28
  client = genai.Client(api_key=GOOGLE_API_KEY)
@@ -143,43 +145,6 @@ rend le très espacer. Ça doit être très aéré
143
 
144
  """
145
 
146
- PPMQTH_LIGHT = r"""
147
- # RÔLE & OBJECTIF
148
- Tu es un expert en mathématiques capable de fournir une solution claire et concise. Ton objectif est de générer une correction LaTeX directement compilable pour l'exercice fourni.
149
-
150
- # CONTEXTE
151
- * **Input:** L'énoncé de l'exercice (texte).
152
- * **Niveau Cible:** Terminale (scientifique).
153
- * **Output Attendu:** **Uniquement** le code source LaTeX (`.tex`) brut et complet. Pas de texte d'accompagnement en dehors du code.
154
-
155
- # TÂCHE PRINCIPALE
156
- 1. **Analyse:** Comprends l'énoncé.
157
- 2. **Résolution:** Résous l'exercice.
158
- 3. **Rédaction LaTeX:** Rédige la solution en LaTeX standard.
159
-
160
- # SPÉCIFICATIONS TECHNIQUES DU CODE LATEX (Minimal)
161
- * Classe: `\documentclass[12pt,a4paper]{article}`
162
- * Encodage: `\usepackage[utf8]{inputenc}`, `\usepackage[T1]{fontenc}`
163
- * Langue: `\usepackage[french]{babel}`
164
- * Maths: `\usepackage{amsmath}`, `\usepackage{amssymb}`
165
- * Optionnel: `\usepackage{graphicx}` si l'énoncé peut contenir des schémas à reproduire (rare pour du texte).
166
- * Pas de packages de mise en page complexes (pas de tcolorbox, fancyhdr, tikz, etc. sauf si *absolument* indispensable pour représenter la solution mathématique elle-même).
167
- * Utilise des environnements mathématiques standards (`align*`, `equation*`, `\[ ... \]`).
168
-
169
- # CONTENU PÉDAGOGIQUE DE LA SOLUTION
170
- * Rappeler l'énoncé.
171
- * Structurer la solution logiquement.
172
- * Justifier les étapes principales.
173
- * Langage mathématique précis.
174
-
175
- # CONTRAINTES STRICTES
176
- * Le seul output doit être le code LaTeX brut.
177
- * Aucun texte avant `\documentclass` ou après `\end{document}`.
178
- * Inclure la ligne `Concu par Mariam AI` juste avant `\end{document}`.
179
- * L'objectif est une solution fonctionnelle et lisible, sans mise en page sophistiquée.
180
- """
181
-
182
-
183
  # Dictionnaire pour stocker les résultats des tâches en cours
184
  task_results = {}
185
 
@@ -187,6 +152,7 @@ task_results = {}
187
  def check_latex_installation():
188
  """Vérifie si pdflatex est installé sur le système"""
189
  try:
 
190
  subprocess.run(["pdflatex", "-version"], capture_output=True, check=True, timeout=10)
191
  print("INFO: pdflatex est installé et accessible.")
192
  return True
@@ -199,15 +165,17 @@ IS_LATEX_INSTALLED = check_latex_installation()
199
 
200
  def clean_latex_code(latex_code):
201
  """Removes markdown code block fences (```latex ... ``` or ``` ... ```) if present."""
 
202
  match_latex = re.search(r"```(?:latex|tex)\s*(.*?)\s*```", latex_code, re.DOTALL | re.IGNORECASE)
203
  if match_latex:
204
  return match_latex.group(1).strip()
205
 
 
206
  match_generic = re.search(r"```\s*(\\documentclass.*?)\s*```", latex_code, re.DOTALL | re.IGNORECASE)
207
  if match_generic:
208
  return match_generic.group(1).strip()
209
 
210
- return latex_code.strip()
211
 
212
  def latex_to_pdf(latex_code, output_filename_base="document"):
213
  """
@@ -218,6 +186,7 @@ def latex_to_pdf(latex_code, output_filename_base="document"):
218
  if not IS_LATEX_INSTALLED:
219
  return None, "pdflatex n'est pas disponible sur le système."
220
 
 
221
  with tempfile.TemporaryDirectory() as temp_dir_compile:
222
  tex_filename = f"{output_filename_base}.tex"
223
  tex_path = os.path.join(temp_dir_compile, tex_filename)
@@ -228,23 +197,28 @@ def latex_to_pdf(latex_code, output_filename_base="document"):
228
  tex_file.write(latex_code)
229
 
230
  my_env = os.environ.copy()
231
- my_env["LC_ALL"] = "C.UTF-8"
232
  my_env["LANG"] = "C.UTF-8"
233
 
234
  last_result = None
235
- for i in range(2):
 
 
 
 
236
  process = subprocess.run(
237
  ["pdflatex", "-interaction=nonstopmode", "-output-directory", temp_dir_compile, tex_path],
238
- capture_output=True,
239
- text=True,
240
- check=False,
241
- encoding="utf-8",
242
- errors="replace",
243
  env=my_env,
244
- timeout=120
245
  )
246
  last_result = process
247
  if not os.path.exists(pdf_path_in_compile_dir) and process.returncode != 0:
 
248
  break
249
 
250
  if os.path.exists(pdf_path_in_compile_dir):
@@ -255,6 +229,7 @@ def latex_to_pdf(latex_code, output_filename_base="document"):
255
  else:
256
  error_log = last_result.stdout + "\n" + last_result.stderr if last_result else "Aucun résultat de compilation."
257
 
 
258
  if "LaTeX Error: File `" in error_log:
259
  match = re.search(r"LaTeX Error: File `(.*?)' not found.", error_log)
260
  if match:
@@ -264,11 +239,12 @@ def latex_to_pdf(latex_code, output_filename_base="document"):
264
  return None, "Erreur de compilation PDF: Commande LaTeX non définie. Vérifiez le code LaTeX pour des erreurs de syntaxe."
265
 
266
  if "! LaTeX Error:" in error_log:
267
- match = re.search(r"! LaTeX Error: (.*?)\n", error_log)
268
  if match:
269
  return None, f"Erreur de compilation PDF: {match.group(1).strip()}"
270
 
271
- log_preview = error_log[-1000:]
 
272
  print(f"Erreur de compilation PDF complète pour {output_filename_base}:\n{error_log}")
273
  return None, f"Erreur lors de la compilation du PDF. Détails dans les logs du serveur. Aperçu: ...{log_preview}"
274
 
@@ -283,9 +259,6 @@ def latex_to_pdf(latex_code, output_filename_base="document"):
283
  # --- Telegram Functions ---
284
  def send_to_telegram(image_data, caption="Nouvelle image uploadée"):
285
  """Envoie l'image à un chat Telegram spécifié"""
286
- if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
287
- print("AVERTISSEMENT: TELEGRAM_BOT_TOKEN ou TELEGRAM_CHAT_ID non configuré. Envoi Telegram désactivé.")
288
- return False
289
  try:
290
  url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendPhoto"
291
  files = {'photo': ('image.png', image_data)}
@@ -307,9 +280,6 @@ def send_to_telegram(image_data, caption="Nouvelle image uploadée"):
307
 
308
  def send_document_to_telegram(content_or_path, filename="reponse.txt", caption="Réponse", is_pdf=False):
309
  """Envoie un fichier (texte ou PDF) à un chat Telegram spécifié"""
310
- if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
311
- print("AVERTISSEMENT: TELEGRAM_BOT_TOKEN ou TELEGRAM_CHAT_ID non configuré. Envoi Telegram désactivé.")
312
- return False
313
  try:
314
  url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendDocument"
315
  files = None
@@ -317,11 +287,11 @@ def send_document_to_telegram(content_or_path, filename="reponse.txt", caption="
317
  if is_pdf:
318
  with open(content_or_path, 'rb') as f_pdf:
319
  files = {'document': (filename, f_pdf.read(), 'application/pdf')}
320
- else:
321
  files = {'document': (filename, content_or_path.encode('utf-8'), 'text/plain')}
322
 
323
  data = {'chat_id': TELEGRAM_CHAT_ID, 'caption': caption}
324
- response = requests.post(url, files=files, data=data, timeout=60)
325
 
326
  if response.status_code == 200:
327
  print(f"Document '{filename}' envoyé avec succès à Telegram.")
@@ -337,7 +307,7 @@ def send_document_to_telegram(content_or_path, filename="reponse.txt", caption="
337
  return False
338
 
339
  # --- Background Image Processing ---
340
- def process_image_background(task_id, image_data, prompt_type="refined"):
341
  """Traite l'image, génère LaTeX, convertit en PDF (si possible), et envoie via Telegram."""
342
  pdf_file_to_clean = None
343
  try:
@@ -347,70 +317,63 @@ def process_image_background(task_id, image_data, prompt_type="refined"):
347
  raise ConnectionError("Client Gemini non initialisé. Vérifiez la clé API et la configuration.")
348
 
349
  img = Image.open(io.BytesIO(image_data))
350
- buffered_for_gemini = io.BytesIO()
351
- img.save(buffered_for_gemini, format="PNG")
352
- img_bytes_for_gemini = buffered_for_gemini.getvalue()
353
 
354
  full_latex_response = ""
355
 
356
- selected_prompt = ppmqth
357
- if prompt_type == "light":
358
- selected_prompt = PPMQTH_LIGHT
359
- print(f"Task {task_id}: Utilisation du prompt LÉGER.")
360
- else:
361
- print(f"Task {task_id}: Utilisation du prompt RAFFINÉ ET COMPLET.")
362
-
363
  try:
364
  task_results[task_id]['status'] = 'generating_latex'
365
  print(f"Task {task_id}: Génération LaTeX par Gemini...")
366
 
 
 
 
367
  gemini_response = client.models.generate_content(
368
  model="gemini-2.5-pro-exp-03-25",
369
  contents=[
370
- {'inline_data': {'mime_type': 'image/png', 'data': base64.b64encode(img_bytes_for_gemini).decode()}},
371
- selected_prompt
372
- ],
373
- generation_config={"response_mime_type": "text/plain"}
 
374
  )
375
 
376
  if gemini_response.candidates:
377
  candidate = gemini_response.candidates[0]
378
- if candidate.finish_reason.name == "SAFETY": # Check for safety blocking
379
- raise genai.types.BlockedPromptException("Contenu bloqué pour des raisons de sécurité.")
380
  if candidate.content and candidate.content.parts:
381
  for part in candidate.content.parts:
382
  if hasattr(part, 'text') and part.text:
383
  full_latex_response += part.text
384
- elif hasattr(candidate, 'text') and candidate.text:
385
  full_latex_response = candidate.text
386
- elif hasattr(gemini_response, 'text') and gemini_response.text:
387
  full_latex_response = gemini_response.text
388
 
389
  if not full_latex_response.strip():
390
- raise ValueError("Mariam a retourné une réponse vide ou sans contenu textuel.")
391
 
392
  print(f"Task {task_id}: LaTeX brut reçu de Gemini (longueur: {len(full_latex_response)}).")
393
 
394
  task_results[task_id]['status'] = 'cleaning_latex'
395
  cleaned_latex = clean_latex_code(full_latex_response)
396
  print(f"Task {task_id}: LaTeX nettoyé (longueur: {len(cleaned_latex)}).")
397
- task_results[task_id]['response'] = cleaned_latex
398
-
399
- caption_base = f"Solution tâche {task_id} (Style: {prompt_type})"
400
 
401
  if not IS_LATEX_INSTALLED:
402
  print(f"Task {task_id}: pdflatex non disponible. Envoi du .tex uniquement.")
403
  send_document_to_telegram(
404
  cleaned_latex,
405
- filename=f"solution_{task_id}_{prompt_type}.tex",
406
- caption=f"{caption_base} - Code LaTeX (pdflatex non dispo)"
407
  )
408
  task_results[task_id]['status'] = 'completed_tex_only'
 
409
  return
410
 
411
  task_results[task_id]['status'] = 'generating_pdf'
412
  print(f"Task {task_id}: Génération du PDF...")
413
- pdf_filename_base = f"solution_{task_id}_{prompt_type}"
414
  pdf_file_to_clean, pdf_message = latex_to_pdf(cleaned_latex, output_filename_base=pdf_filename_base)
415
 
416
  if pdf_file_to_clean:
@@ -418,59 +381,40 @@ def process_image_background(task_id, image_data, prompt_type="refined"):
418
  send_document_to_telegram(
419
  pdf_file_to_clean,
420
  filename=f"{pdf_filename_base}.pdf",
421
- caption=f"{caption_base} - PDF",
422
  is_pdf=True
423
  )
424
  task_results[task_id]['status'] = 'completed'
 
425
  else:
426
  print(f"Task {task_id}: Échec de la génération PDF: {pdf_message}. Envoi du .tex en fallback.")
427
  task_results[task_id]['status'] = 'pdf_error'
428
- task_results[task_id]['error_detail'] = f"Erreur PDF: {pdf_message}"
429
  send_document_to_telegram(
430
  cleaned_latex,
431
- filename=f"{pdf_filename_base}.tex",
432
- caption=f"{caption_base} - Code LaTeX (Erreur PDF: {pdf_message[:100]})"
433
  )
 
434
 
435
- except genai.types.BlockedPromptException as e_blocked:
436
- error_msg = f"Prompt ou image bloqué par le filtre de sécurité Gemini: {e_blocked}"
437
- print(f"Task {task_id}: {error_msg}")
438
- task_results[task_id]['status'] = 'error'
439
- task_results[task_id]['error'] = "Le prompt ou l'image a été bloqué par le filtre de sécurité de l'IA. Essayez une autre image ou un prompt différent."
440
- if not task_results[task_id].get('response'):
441
- task_results[task_id]['response'] = task_results[task_id]['error']
442
- send_document_to_telegram(
443
- f"Erreur tâche {task_id}: {error_msg}",
444
- filename=f"error_{task_id}.txt",
445
- caption=f"Erreur Tâche {task_id} - Prompt Bloqué"
446
- )
447
  except Exception as e_gen:
448
- error_msg = f"Erreur lors de la génération Gemini ou traitement PDF: {e_gen}"
449
- print(f"Task {task_id}: {error_msg}")
450
  task_results[task_id]['status'] = 'error'
451
  task_results[task_id]['error'] = f"Erreur de traitement: {str(e_gen)}"
452
- if not task_results[task_id].get('response'): # S'il n'y a pas de LaTeX partiel
453
- task_results[task_id]['response'] = task_results[task_id]['error']
454
  send_document_to_telegram(
455
- f"Erreur tâche {task_id}: {error_msg}\n\nLaTeX partiel (si disponible):\n{task_results[task_id].get('response','')}",
456
  filename=f"error_{task_id}.txt",
457
  caption=f"Erreur Tâche {task_id}"
458
  )
 
459
 
460
- except ConnectionError as e_conn:
461
- error_msg = f"Erreur de connexion Gemini: {e_conn}"
462
- print(f"Task {task_id}: {error_msg}")
463
- task_results[task_id]['status'] = 'error'
464
- task_results[task_id]['error'] = str(e_conn)
465
- task_results[task_id]['response'] = str(e_conn) # Mettre l'erreur dans response pour affichage
466
 
467
  except Exception as e_outer:
468
- error_msg = f"Exception majeure dans la tâche de fond: {e_outer}"
469
- print(f"Task {task_id}: {error_msg}")
470
  task_results[task_id]['status'] = 'error'
471
  task_results[task_id]['error'] = f"Erreur système: {str(e_outer)}"
472
- if not task_results[task_id].get('response'):
473
- task_results[task_id]['response'] = task_results[task_id]['error']
474
  finally:
475
  if pdf_file_to_clean and os.path.exists(pdf_file_to_clean):
476
  try:
@@ -496,28 +440,25 @@ def solve():
496
 
497
  image_data = image_file.read()
498
 
499
- prompt_type = request.form.get('prompt_type', 'refined')
500
-
501
- send_to_telegram(image_data, f"Nouvelle image pour résolution (Style: {prompt_type}). Tâche à venir.")
502
 
503
  task_id = str(uuid.uuid4())
504
  task_results[task_id] = {
505
  'status': 'pending',
506
  'response': '',
507
  'error': None,
508
- 'error_detail': None, # Pour les erreurs spécifiques PDF
509
- 'time_started': time.time(),
510
- 'prompt_type': prompt_type
511
  }
512
 
513
  threading.Thread(
514
  target=process_image_background,
515
- args=(task_id, image_data, prompt_type)
516
  ).start()
517
 
518
  return jsonify({
519
  'task_id': task_id,
520
- 'status': 'pending' # Le statut initial est pending, le thread mettra à jour
521
  })
522
 
523
  except Exception as e:
@@ -531,12 +472,21 @@ def get_task_status(task_id):
531
 
532
  task = task_results[task_id]
533
 
 
 
 
 
 
 
 
 
534
  response_data = {
535
  'status': task['status'],
536
  'response': task.get('response'),
537
- 'error': task.get('error'),
538
- 'error_detail': task.get('error_detail')
539
  }
 
 
540
 
541
  return jsonify(response_data)
542
 
@@ -548,16 +498,9 @@ def stream_task_progress(task_id):
548
  return
549
 
550
  last_status_sent = None
551
- timeout_start = time.time() # Pour éviter que le stream ne dure indéfiniment
552
- MAX_STREAM_DURATION = 300 # 5 minutes max pour le stream
553
-
554
  while True:
555
- if time.time() - timeout_start > MAX_STREAM_DURATION:
556
- yield f'data: {json.dumps({"error": "Timeout du stream de statut", "status": "error"})}\n\n'
557
- break
558
-
559
  task = task_results.get(task_id)
560
- if not task:
561
  yield f'data: {json.dumps({"error": "Tâche disparue ou nettoyée", "status": "error"})}\n\n'
562
  break
563
 
@@ -565,31 +508,29 @@ def stream_task_progress(task_id):
565
 
566
  if current_status != last_status_sent:
567
  data_to_send = {"status": current_status}
568
- if current_status in ['completed', 'completed_tex_only', 'pdf_error']:
569
  data_to_send["response"] = task.get("response", "")
570
- if current_status in ['error', 'pdf_error']: # S'il y a une erreur générale ou spécifique au PDF
571
  data_to_send["error"] = task.get("error", "Erreur inconnue")
572
  if task.get("error_detail"):
573
  data_to_send["error_detail"] = task.get("error_detail")
574
- # Envoyer aussi la réponse (LaTeX partiel) s'il y en a une, même en cas d'erreur PDF
575
- if task.get("response") and not data_to_send.get("response"):
576
  data_to_send["response"] = task.get("response")
577
 
578
-
579
  yield f'data: {json.dumps(data_to_send)}\n\n'
580
  last_status_sent = current_status
581
 
582
  if current_status in ['completed', 'error', 'pdf_error', 'completed_tex_only']:
583
- break
584
 
585
- time.sleep(1)
586
 
587
  return Response(
588
  stream_with_context(generate()),
589
  mimetype='text/event-stream',
590
  headers={
591
  'Cache-Control': 'no-cache',
592
- 'X-Accel-Buffering': 'no',
593
  'Connection': 'keep-alive'
594
  }
595
  )
@@ -599,10 +540,6 @@ if __name__ == '__main__':
599
  print("CRITICAL: GEMINI_API_KEY variable d'environnement non définie. L'application risque de ne pas fonctionner.")
600
  if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
601
  print("CRITICAL: TELEGRAM_BOT_TOKEN ou TELEGRAM_CHAT_ID non définis. L'intégration Telegram échouera.")
602
- else:
603
- print(f"INFO: Prêt à envoyer des notifications Telegram au CHAT_ID: {TELEGRAM_CHAT_ID}")
604
-
605
- print(f"INFO: LaTeX est {'installé et activé' if IS_LATEX_INSTALLED else 'NON installé/activé. Les PDFs ne seront pas générés.'}")
606
 
607
  # For production, use a proper WSGI server like Gunicorn or uWSGI
608
  app.run(debug=True, host='0.0.0.0', port=5000)
 
12
  import tempfile # Added
13
  import subprocess # Added
14
  import shutil # Added
15
+ # import platform # Not strictly needed for app.py modifications
16
  import re # Added
17
 
18
  app = Flask(__name__)
 
21
  GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
22
  # IMPORTANT: For production, move these to environment variables or a secure config
23
  TELEGRAM_BOT_TOKEN = "8004545342:AAGcZaoDjYg8dmbbXRsR1N3TfSSbEiAGz88"
24
+ TELEGRAM_CHAT_ID = "-1002497861230"
25
 
26
  # Initialize Gemini client
27
+ # Assuming genai.Client and client.models.generate_content are part of user's specific library setup
28
  if GOOGLE_API_KEY:
29
  try:
30
  client = genai.Client(api_key=GOOGLE_API_KEY)
 
145
 
146
  """
147
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  # Dictionnaire pour stocker les résultats des tâches en cours
149
  task_results = {}
150
 
 
152
  def check_latex_installation():
153
  """Vérifie si pdflatex est installé sur le système"""
154
  try:
155
+ # Using timeout to prevent hanging if pdflatex is unresponsive
156
  subprocess.run(["pdflatex", "-version"], capture_output=True, check=True, timeout=10)
157
  print("INFO: pdflatex est installé et accessible.")
158
  return True
 
165
 
166
  def clean_latex_code(latex_code):
167
  """Removes markdown code block fences (```latex ... ``` or ``` ... ```) if present."""
168
+ # Pattern for ```latex ... ```
169
  match_latex = re.search(r"```(?:latex|tex)\s*(.*?)\s*```", latex_code, re.DOTALL | re.IGNORECASE)
170
  if match_latex:
171
  return match_latex.group(1).strip()
172
 
173
+ # Pattern for generic ``` ... ```, checking if it likely contains LaTeX
174
  match_generic = re.search(r"```\s*(\\documentclass.*?)\s*```", latex_code, re.DOTALL | re.IGNORECASE)
175
  if match_generic:
176
  return match_generic.group(1).strip()
177
 
178
+ return latex_code.strip() # Default to stripping whitespace if no fences found
179
 
180
  def latex_to_pdf(latex_code, output_filename_base="document"):
181
  """
 
186
  if not IS_LATEX_INSTALLED:
187
  return None, "pdflatex n'est pas disponible sur le système."
188
 
189
+ # Create a temporary directory for LaTeX compilation files
190
  with tempfile.TemporaryDirectory() as temp_dir_compile:
191
  tex_filename = f"{output_filename_base}.tex"
192
  tex_path = os.path.join(temp_dir_compile, tex_filename)
 
197
  tex_file.write(latex_code)
198
 
199
  my_env = os.environ.copy()
200
+ my_env["LC_ALL"] = "C.UTF-8" # Ensure UTF-8 locale for pdflatex
201
  my_env["LANG"] = "C.UTF-8"
202
 
203
  last_result = None
204
+ for i in range(2): # Run pdflatex twice for references, TOC, etc.
205
+ # Added -file-line-error for more precise error messages
206
+ # Added -halt-on-error to stop on first error (more efficient)
207
+ # Note: -halt-on-error might prevent a second pass that could fix some issues.
208
+ # For robustness, -interaction=nonstopmode is often preferred to try to complete.
209
  process = subprocess.run(
210
  ["pdflatex", "-interaction=nonstopmode", "-output-directory", temp_dir_compile, tex_path],
211
+ capture_output=True, # Capture stdout and stderr
212
+ text=True, # Decode output as text
213
+ check=False, # Do not raise exception on non-zero exit code
214
+ encoding="utf-8", # Specify encoding for output decoding
215
+ errors="replace", # Replace undecodable characters
216
  env=my_env,
217
+ timeout=120 # Timeout for pdflatex execution (e.g., 2 minutes)
218
  )
219
  last_result = process
220
  if not os.path.exists(pdf_path_in_compile_dir) and process.returncode != 0:
221
+ # If PDF not created and there was an error, no point in second pass
222
  break
223
 
224
  if os.path.exists(pdf_path_in_compile_dir):
 
229
  else:
230
  error_log = last_result.stdout + "\n" + last_result.stderr if last_result else "Aucun résultat de compilation."
231
 
232
+ # Try to extract more specific error messages
233
  if "LaTeX Error: File `" in error_log:
234
  match = re.search(r"LaTeX Error: File `(.*?)' not found.", error_log)
235
  if match:
 
239
  return None, "Erreur de compilation PDF: Commande LaTeX non définie. Vérifiez le code LaTeX pour des erreurs de syntaxe."
240
 
241
  if "! LaTeX Error:" in error_log:
242
+ match = re.search(r"! LaTeX Error: (.*?)\n", error_log) # Get first line of error
243
  if match:
244
  return None, f"Erreur de compilation PDF: {match.group(1).strip()}"
245
 
246
+ # Generic error if specific parsing fails
247
+ log_preview = error_log[-1000:] # Last 1000 characters of log for preview
248
  print(f"Erreur de compilation PDF complète pour {output_filename_base}:\n{error_log}")
249
  return None, f"Erreur lors de la compilation du PDF. Détails dans les logs du serveur. Aperçu: ...{log_preview}"
250
 
 
259
  # --- Telegram Functions ---
260
  def send_to_telegram(image_data, caption="Nouvelle image uploadée"):
261
  """Envoie l'image à un chat Telegram spécifié"""
 
 
 
262
  try:
263
  url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendPhoto"
264
  files = {'photo': ('image.png', image_data)}
 
280
 
281
  def send_document_to_telegram(content_or_path, filename="reponse.txt", caption="Réponse", is_pdf=False):
282
  """Envoie un fichier (texte ou PDF) à un chat Telegram spécifié"""
 
 
 
283
  try:
284
  url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendDocument"
285
  files = None
 
287
  if is_pdf:
288
  with open(content_or_path, 'rb') as f_pdf:
289
  files = {'document': (filename, f_pdf.read(), 'application/pdf')}
290
+ else: # Assuming text content
291
  files = {'document': (filename, content_or_path.encode('utf-8'), 'text/plain')}
292
 
293
  data = {'chat_id': TELEGRAM_CHAT_ID, 'caption': caption}
294
+ response = requests.post(url, files=files, data=data, timeout=60) # Increased timeout for larger files
295
 
296
  if response.status_code == 200:
297
  print(f"Document '{filename}' envoyé avec succès à Telegram.")
 
307
  return False
308
 
309
  # --- Background Image Processing ---
310
+ def process_image_background(task_id, image_data):
311
  """Traite l'image, génère LaTeX, convertit en PDF (si possible), et envoie via Telegram."""
312
  pdf_file_to_clean = None
313
  try:
 
317
  raise ConnectionError("Client Gemini non initialisé. Vérifiez la clé API et la configuration.")
318
 
319
  img = Image.open(io.BytesIO(image_data))
320
+ buffered = io.BytesIO()
321
+ img.save(buffered, format="PNG") # Gemini prefers PNG or JPEG
322
+ img_base64_str = base64.b64encode(buffered.getvalue()).decode()
323
 
324
  full_latex_response = ""
325
 
 
 
 
 
 
 
 
326
  try:
327
  task_results[task_id]['status'] = 'generating_latex'
328
  print(f"Task {task_id}: Génération LaTeX par Gemini...")
329
 
330
+ # User's original model: "gemini-2.5-pro-exp-03-25"
331
+ # Using "gemini-1.5-pro-latest" as a robust alternative. User can change if needed.
332
+ # The user's original Gemini call structure:
333
  gemini_response = client.models.generate_content(
334
  model="gemini-2.5-pro-exp-03-25",
335
  contents=[
336
+ {'inline_data': {'mime_type': 'image/png', 'data': img_base64_str}},
337
+ ppmqth
338
+ ]
339
+ # Consider adding generation_config for token limits, temperature if needed
340
+ # generation_config=genai.types.GenerationConfig(max_output_tokens=8192)
341
  )
342
 
343
  if gemini_response.candidates:
344
  candidate = gemini_response.candidates[0]
 
 
345
  if candidate.content and candidate.content.parts:
346
  for part in candidate.content.parts:
347
  if hasattr(part, 'text') and part.text:
348
  full_latex_response += part.text
349
+ elif hasattr(candidate, 'text') and candidate.text: # Fallback for simpler candidate structure
350
  full_latex_response = candidate.text
351
+ elif hasattr(gemini_response, 'text') and gemini_response.text: # Fallback for direct response.text
352
  full_latex_response = gemini_response.text
353
 
354
  if not full_latex_response.strip():
355
+ raise ValueError("Gemini a retourné une réponse vide ou sans contenu textuel.")
356
 
357
  print(f"Task {task_id}: LaTeX brut reçu de Gemini (longueur: {len(full_latex_response)}).")
358
 
359
  task_results[task_id]['status'] = 'cleaning_latex'
360
  cleaned_latex = clean_latex_code(full_latex_response)
361
  print(f"Task {task_id}: LaTeX nettoyé (longueur: {len(cleaned_latex)}).")
 
 
 
362
 
363
  if not IS_LATEX_INSTALLED:
364
  print(f"Task {task_id}: pdflatex non disponible. Envoi du .tex uniquement.")
365
  send_document_to_telegram(
366
  cleaned_latex,
367
+ filename=f"solution_{task_id}.tex",
368
+ caption=f"Code LaTeX pour tâche {task_id} (pdflatex non disponible)"
369
  )
370
  task_results[task_id]['status'] = 'completed_tex_only'
371
+ task_results[task_id]['response'] = cleaned_latex
372
  return
373
 
374
  task_results[task_id]['status'] = 'generating_pdf'
375
  print(f"Task {task_id}: Génération du PDF...")
376
+ pdf_filename_base = f"solution_{task_id}"
377
  pdf_file_to_clean, pdf_message = latex_to_pdf(cleaned_latex, output_filename_base=pdf_filename_base)
378
 
379
  if pdf_file_to_clean:
 
381
  send_document_to_telegram(
382
  pdf_file_to_clean,
383
  filename=f"{pdf_filename_base}.pdf",
384
+ caption=f"Solution PDF pour tâche {task_id}",
385
  is_pdf=True
386
  )
387
  task_results[task_id]['status'] = 'completed'
388
+ task_results[task_id]['response'] = cleaned_latex # Web UI still gets LaTeX source
389
  else:
390
  print(f"Task {task_id}: Échec de la génération PDF: {pdf_message}. Envoi du .tex en fallback.")
391
  task_results[task_id]['status'] = 'pdf_error'
392
+ task_results[task_id]['error_detail'] = f"Erreur PDF: {pdf_message}" # Store PDF specific error
393
  send_document_to_telegram(
394
  cleaned_latex,
395
+ filename=f"solution_{task_id}.tex",
396
+ caption=f"Code LaTeX pour tâche {task_id} (Erreur PDF: {pdf_message[:150]})"
397
  )
398
+ task_results[task_id]['response'] = cleaned_latex # Web UI gets LaTeX source
399
 
 
 
 
 
 
 
 
 
 
 
 
 
400
  except Exception as e_gen:
401
+ print(f"Task {task_id}: Erreur lors de la génération Gemini ou traitement PDF: {e_gen}")
 
402
  task_results[task_id]['status'] = 'error'
403
  task_results[task_id]['error'] = f"Erreur de traitement: {str(e_gen)}"
404
+ # Optionally send a simple text error to Telegram
 
405
  send_document_to_telegram(
406
+ f"Erreur lors du traitement de la tâche {task_id}: {str(e_gen)}",
407
  filename=f"error_{task_id}.txt",
408
  caption=f"Erreur Tâche {task_id}"
409
  )
410
+ task_results[task_id]['response'] = f"Erreur: {str(e_gen)}"
411
 
 
 
 
 
 
 
412
 
413
  except Exception as e_outer:
414
+ print(f"Task {task_id}: Exception majeure dans la tâche de fond: {e_outer}")
 
415
  task_results[task_id]['status'] = 'error'
416
  task_results[task_id]['error'] = f"Erreur système: {str(e_outer)}"
417
+ task_results[task_id]['response'] = f"Erreur système: {str(e_outer)}"
 
418
  finally:
419
  if pdf_file_to_clean and os.path.exists(pdf_file_to_clean):
420
  try:
 
440
 
441
  image_data = image_file.read()
442
 
443
+ # Envoyer l'image à Telegram (confirmation d'upload)
444
+ send_to_telegram(image_data, f"Nouvelle image pour résolution (Tâche à venir)")
 
445
 
446
  task_id = str(uuid.uuid4())
447
  task_results[task_id] = {
448
  'status': 'pending',
449
  'response': '',
450
  'error': None,
451
+ 'time_started': time.time()
 
 
452
  }
453
 
454
  threading.Thread(
455
  target=process_image_background,
456
+ args=(task_id, image_data)
457
  ).start()
458
 
459
  return jsonify({
460
  'task_id': task_id,
461
+ 'status': 'pending'
462
  })
463
 
464
  except Exception as e:
 
472
 
473
  task = task_results[task_id]
474
 
475
+ # Basic cleanup logic (can be improved with a dedicated cleanup thread)
476
+ current_time = time.time()
477
+ if task['status'] in ['completed', 'error', 'pdf_error', 'completed_tex_only'] and \
478
+ (current_time - task.get('time_started', 0) > 3600): # Cleanup after 1 hour
479
+ # del task_results[task_id] # Potentially remove, or mark for removal
480
+ # For now, just don't error if it's old
481
+ pass
482
+
483
  response_data = {
484
  'status': task['status'],
485
  'response': task.get('response'),
486
+ 'error': task.get('error')
 
487
  }
488
+ if task.get('error_detail'): # Include PDF specific error if present
489
+ response_data['error_detail'] = task.get('error_detail')
490
 
491
  return jsonify(response_data)
492
 
 
498
  return
499
 
500
  last_status_sent = None
 
 
 
501
  while True:
 
 
 
 
502
  task = task_results.get(task_id)
503
+ if not task: # Task might have been cleaned up
504
  yield f'data: {json.dumps({"error": "Tâche disparue ou nettoyée", "status": "error"})}\n\n'
505
  break
506
 
 
508
 
509
  if current_status != last_status_sent:
510
  data_to_send = {"status": current_status}
511
+ if current_status == 'completed' or current_status == 'completed_tex_only':
512
  data_to_send["response"] = task.get("response", "")
513
+ elif current_status == 'error' or current_status == 'pdf_error':
514
  data_to_send["error"] = task.get("error", "Erreur inconnue")
515
  if task.get("error_detail"):
516
  data_to_send["error_detail"] = task.get("error_detail")
517
+ if task.get("response"): # Send partial response if available on error
 
518
  data_to_send["response"] = task.get("response")
519
 
 
520
  yield f'data: {json.dumps(data_to_send)}\n\n'
521
  last_status_sent = current_status
522
 
523
  if current_status in ['completed', 'error', 'pdf_error', 'completed_tex_only']:
524
+ break # End stream for terminal states
525
 
526
+ time.sleep(1) # Check status every second
527
 
528
  return Response(
529
  stream_with_context(generate()),
530
  mimetype='text/event-stream',
531
  headers={
532
  'Cache-Control': 'no-cache',
533
+ 'X-Accel-Buffering': 'no', # Important for Nginx or other reverse proxies
534
  'Connection': 'keep-alive'
535
  }
536
  )
 
540
  print("CRITICAL: GEMINI_API_KEY variable d'environnement non définie. L'application risque de ne pas fonctionner.")
541
  if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
542
  print("CRITICAL: TELEGRAM_BOT_TOKEN ou TELEGRAM_CHAT_ID non définis. L'intégration Telegram échouera.")
 
 
 
 
543
 
544
  # For production, use a proper WSGI server like Gunicorn or uWSGI
545
  app.run(debug=True, host='0.0.0.0', port=5000)