Update app.py
Browse files
app.py
CHANGED
@@ -1,8 +1,5 @@
|
|
1 |
-
|
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 |
-
|
30 |
-
client = None
|
31 |
else:
|
32 |
-
|
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 |
-
|
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
|
|
|
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 |
-
|
|
|
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 |
-
|
|
|
|
|
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 |
-
|
|
|
|
|
99 |
except Exception as e:
|
100 |
-
|
|
|
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 |
-
|
|
|
|
|
|
|
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("
|
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 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
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"
|
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("
|
|
|
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 |
-
|
182 |
task_results[task_id]['status'] = 'error'
|
183 |
task_results[task_id]['error'] = str(e)
|
184 |
-
task_results[task_id]['response'] = f"
|
185 |
finally:
|
186 |
-
|
187 |
-
|
188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
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
|
217 |
elif file_type == 'application/pdf':
|
218 |
if file_count['pdfs'] >= 1:
|
219 |
-
|
|
|
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
|
|
|
|
|
223 |
|
224 |
if not files_data:
|
225 |
-
|
|
|
226 |
|
227 |
task_id = str(uuid.uuid4())
|
228 |
task_results[task_id] = {
|
229 |
-
'status': 'pending',
|
230 |
-
'
|
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 |
-
|
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 |
-
|
|
|
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 |
-
|
|
|
292 |
|
293 |
try:
|
|
|
294 |
return send_from_directory(GENERATED_PDF_DIR, task['pdf_filename'], as_attachment=True)
|
295 |
except FileNotFoundError:
|
296 |
-
|
|
|
297 |
|
298 |
if __name__ == '__main__':
|
|
|
|
|
|
|
299 |
os.makedirs(GENERATED_PDF_DIR, exist_ok=True)
|
300 |
-
|
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 |
-
|
|
|
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
|