File size: 12,459 Bytes
e22e1db
7631217
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e22e1db
7631217
e22e1db
7631217
 
 
 
 
e22e1db
7631217
 
e22e1db
7631217
 
 
 
e22e1db
7631217
e22e1db
 
 
 
7631217
e22e1db
 
7631217
e22e1db
 
7631217
 
 
 
 
e22e1db
7631217
 
 
 
 
 
e22e1db
 
7631217
e22e1db
 
7631217
 
e22e1db
7631217
e22e1db
7631217
e22e1db
 
 
 
7631217
e22e1db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7631217
 
 
 
 
 
e22e1db
7631217
e22e1db
7631217
e22e1db
7631217
 
 
e22e1db
 
7631217
e22e1db
7631217
e22e1db
 
7631217
 
 
e22e1db
 
 
 
 
 
 
7631217
e22e1db
 
 
 
7631217
e22e1db
 
 
 
 
 
 
 
 
 
 
 
 
 
7631217
e22e1db
 
 
 
 
7631217
e22e1db
 
 
7631217
 
 
 
 
e22e1db
7631217
e22e1db
7631217
e22e1db
 
7631217
e22e1db
7631217
e22e1db
 
7631217
e22e1db
 
7631217
e22e1db
 
 
7631217
 
 
 
 
 
 
 
e22e1db
 
 
7631217
e22e1db
 
7631217
 
 
e22e1db
7631217
e22e1db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7631217
 
e22e1db
 
 
 
 
 
 
 
 
7631217
 
 
e22e1db
7631217
 
e22e1db
7631217
 
 
 
e22e1db
 
 
 
 
 
 
 
 
7631217
 
 
 
 
 
 
 
 
 
 
 
 
 
e22e1db
7631217
e22e1db
 
 
 
7631217
 
 
e22e1db
7631217
 
 
 
e22e1db
 
 
 
 
 
 
 
 
 
 
 
7631217
 
e22e1db
7631217
e22e1db
7631217
e22e1db
7631217
e22e1db
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
from flask import Flask, render_template, request, jsonify, Response, stream_with_context, send_from_directory
from google import genai
import os
from google.genai import types
from PIL import Image
import io
import base64
import json
import requests
import threading
import uuid
import time
import tempfile
import subprocess
import shutil
import re

app = Flask(__name__)

GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
TELEGRAM_BOT_TOKEN = "8004545342:AAGcZaoDjYg8dmbbXRsR1N3TfSSbEiAGz88"
TELEGRAM_CHAT_ID = "-1002564204301"
GENERATED_PDF_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'generated_pdfs')

if GOOGLE_API_KEY:
    try:
        client = genai.Client(api_key=GOOGLE_API_KEY)
    except Exception as e:
        print(f"Erreur client Gemini: {e}")
        client = None
else:
    print("GEMINI_API_KEY non trouvé.")
    client = None

task_results = {}

def load_prompt_from_file(filename):
    try:
        prompts_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'prompts')
        filepath = os.path.join(prompts_dir, filename)
        with open(filepath, 'r', encoding='utf-8') as f:
            return f.read()
    except Exception as e:
        print(f"Erreur chargement prompt '{filename}': {e}")
        return ""

def get_prompt_for_style(style):
    return load_prompt_from_file('prompt_light.txt') if style == 'light' else load_prompt_from_file('prompt_colorful.txt')

def check_latex_installation():
    try:
        subprocess.run(["pdflatex", "-version"], capture_output=True, check=True, timeout=10)
        return True
    except Exception:
        return False

IS_LATEX_INSTALLED = check_latex_installation()

def clean_latex_code(latex_code):
    match_latex = re.search(r"```(?:latex|tex)\s*(.*?)\s*```", latex_code, re.DOTALL | re.IGNORECASE)
    if match_latex:
        return match_latex.group(1).strip()
    match_generic = re.search(r"```\s*(\\documentclass.*?)\s*```", latex_code, re.DOTALL | re.IGNORECASE)
    if match_generic:
        return match_generic.group(1).strip()
    return latex_code.strip()

def latex_to_pdf(latex_code, output_filename_base, output_dir):
    if not IS_LATEX_INSTALLED:
        return None, "pdflatex non disponible."

    tex_filename = f"{output_filename_base}.tex"
    tex_path = os.path.join(output_dir, tex_filename)
    pdf_path = os.path.join(output_dir, f"{output_filename_base}.pdf")
    
    try:
        with open(tex_path, "w", encoding="utf-8") as tex_file:
            tex_file.write(latex_code)
        
        my_env = os.environ.copy()
        my_env["LC_ALL"] = "C.UTF-8"
        my_env["LANG"] = "C.UTF-8"
        
        last_result = None
        for _ in range(2):
            process = subprocess.run(
                ["pdflatex", "-interaction=nonstopmode", "-output-directory", output_dir, tex_path],
                capture_output=True, text=True, check=False, encoding="utf-8", errors="replace", env=my_env,
            )
            last_result = process
            if not os.path.exists(pdf_path) and process.returncode != 0:
                break
        
        if os.path.exists(pdf_path):
            return pdf_path, f"PDF généré: {os.path.basename(pdf_path)}"
        else:
            error_log = last_result.stdout + "\n" + last_result.stderr if last_result else "Aucun résultat de compilation."
            return None, f"Erreur de compilation PDF. Log: ...{error_log[-1000:]}"
    except Exception as e:
        return None, f"Exception génération PDF: {str(e)}"

def send_to_telegram(file_data, filename, caption="Nouveau fichier uploadé"):
    try:
        if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp')):
            url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendPhoto"
            files = {'photo': (filename, file_data)}
        else:
            url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendDocument"
            files = {'document': (filename, file_data)}
        data = {'chat_id': TELEGRAM_CHAT_ID, 'caption': caption}
        requests.post(url, files=files, data=data, timeout=30)
    except Exception as e:
        print(f"Erreur envoi Telegram: {e}")

def process_files_background(task_id, files_data, resolution_style):
    uploaded_file_refs = []
    try:
        task_results[task_id]['status'] = 'processing'
        if not client:
            raise ConnectionError("Client Gemini non initialisé.")

        contents = []
        for file_info in files_data:
            if file_info['type'].startswith('image/'):
                img = Image.open(io.BytesIO(file_info['data']))
                buffered = io.BytesIO()
                img.save(buffered, format="PNG")
                img_base64_str = base64.b64encode(buffered.getvalue()).decode()
                contents.append({'inline_data': {'mime_type': 'image/png', 'data': img_base64_str}})
            elif file_info['type'] == 'application/pdf':
                try:
                    with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_pdf:
                        temp_pdf.write(file_info['data'])
                        temp_pdf_path = temp_pdf.name
                    file_ref = client.files.upload(file=temp_pdf_path)
                    uploaded_file_refs.append(file_ref)
                    contents.append(file_ref)
                    os.unlink(temp_pdf_path)
                except Exception as e:
                    raise ValueError(f"Impossible d'uploader le PDF: {str(e)}")
        
        if not contents:
            raise ValueError("Aucun contenu valide.")
        
        prompt_to_use = get_prompt_for_style(resolution_style)
        if not prompt_to_use:
            raise ValueError(f"Prompt introuvable pour le style '{resolution_style}'.")
        contents.append(prompt_to_use)

        task_results[task_id]['status'] = 'generating_latex'
        gemini_response = client.models.generate_content(
            model="gemini-2.5-pro", 
            contents=contents,
            config=types.GenerateContentConfig(tools=[types.Tool(code_execution=types.ToolCodeExecution)])
        )

        full_latex_response = ""
        if gemini_response.candidates and gemini_response.candidates[0].content and gemini_response.candidates[0].content.parts:
            for part in gemini_response.candidates[0].content.parts:
                if hasattr(part, 'text') and part.text:
                    full_latex_response += part.text
        
        if not full_latex_response.strip():
            raise ValueError("Gemini a retourné une réponse vide.")

        task_results[task_id]['status'] = 'cleaning_latex'
        cleaned_latex = clean_latex_code(full_latex_response)

        task_results[task_id]['status'] = 'generating_pdf'
        pdf_filename_base = f"solution_{task_id}"
        pdf_file_path, pdf_message = latex_to_pdf(cleaned_latex, pdf_filename_base, GENERATED_PDF_DIR)

        if pdf_file_path:
            task_results[task_id]['status'] = 'completed'
            task_results[task_id]['pdf_filename'] = os.path.basename(pdf_file_path)
            task_results[task_id]['response'] = f"PDF généré avec succès: {os.path.basename(pdf_file_path)}"
        else:
            raise RuntimeError(f"Échec de la génération PDF: {pdf_message}")

    except Exception as e:
        print(f"Task {task_id} Erreur: {e}")
        task_results[task_id]['status'] = 'error'
        task_results[task_id]['error'] = str(e)
        task_results[task_id]['response'] = f"Erreur: {str(e)}"
    finally:
        for file_ref in uploaded_file_refs:
            try: client.files.delete(file_ref)
            except: pass

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/solve', methods=['POST'])
def solve():
    try:
        if 'user_files' not in request.files:
            return jsonify({'error': 'Aucun fichier fourni'}), 400
        
        uploaded_files = request.files.getlist('user_files')
        if not uploaded_files or all(f.filename == '' for f in uploaded_files):
            return jsonify({'error': 'Aucun fichier sélectionné'}), 400

        resolution_style = request.form.get('style', 'colorful')
        files_data = []
        file_count = {'images': 0, 'pdfs': 0}
        
        for file in uploaded_files:
            if not file.filename: continue
            file_data = file.read()
            file_type = file.content_type or 'application/octet-stream'
            
            if file_type.startswith('image/'):
                file_count['images'] += 1
                files_data.append({'filename': file.filename, 'data': file_data, 'type': file_type})
                send_to_telegram(file_data, file.filename, f"Image reçue - Style: {resolution_style}")
            elif file_type == 'application/pdf':
                if file_count['pdfs'] >= 1:
                    return jsonify({'error': 'Un seul PDF autorisé'}), 400
                file_count['pdfs'] += 1
                files_data.append({'filename': file.filename, 'data': file_data, 'type': file_type})
                send_to_telegram(file_data, file.filename, f"PDF reçu - Style: {resolution_style}")

        if not files_data:
            return jsonify({'error': 'Aucun fichier valide (image/pdf) trouvé'}), 400
        
        task_id = str(uuid.uuid4())
        task_results[task_id] = {
            'status': 'pending',
            'response': '',
            'error': None,
            'time_started': time.time(),
            'style': resolution_style,
            'file_count': file_count,
            'first_filename': files_data[0]['filename']
        }
        
        threading.Thread(target=process_files_background, args=(task_id, files_data, resolution_style)).start()
        
        return jsonify({'task_id': task_id, 'status': 'pending', 'first_filename': files_data[0]['filename']})

    except Exception as e:
        print(f"Erreur /solve: {e}")
        return jsonify({'error': f'Erreur serveur: {e}'}), 500

@app.route('/task/<task_id>', methods=['GET'])
def get_task_status(task_id):
    task = task_results.get(task_id)
    if not task:
        return jsonify({'error': 'Tâche introuvable'}), 404
    
    response_data = {'status': task['status'], 'response': task.get('response'), 'error': task.get('error')}
    if task['status'] == 'completed':
        response_data['download_url'] = f"/download/{task_id}"
        
    return jsonify(response_data)

@app.route('/stream/<task_id>', methods=['GET'])
def stream_task_progress(task_id):
    def generate():
        last_status_sent = None
        while True:
            task = task_results.get(task_id)
            if not task:
                yield f'data: {json.dumps({"error": "Tâche disparue", "status": "error"})}\n\n'
                break
            
            current_status = task['status']
            if current_status != last_status_sent:
                data_to_send = {"status": current_status}
                if current_status == 'completed':
                    data_to_send["response"] = task.get("response", "")
                    data_to_send["download_url"] = f"/download/{task_id}"
                elif current_status == 'error':
                    data_to_send["error"] = task.get("error", "Erreur inconnue")

                yield f'data: {json.dumps(data_to_send)}\n\n'
                last_status_sent = current_status
            
            if current_status in ['completed', 'error']:
                break
            
            time.sleep(1)
    
    return Response(stream_with_context(generate()), mimetype='text/event-stream', headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})

@app.route('/download/<task_id>')
def download_pdf(task_id):
    task = task_results.get(task_id)
    if not task or task['status'] != 'completed' or 'pdf_filename' not in task:
        return "Fichier non trouvé ou non prêt.", 404
    
    try:
        return send_from_directory(GENERATED_PDF_DIR, task['pdf_filename'], as_attachment=True)
    except FileNotFoundError:
        return "Fichier introuvable sur le serveur.", 404

if __name__ == '__main__':
    os.makedirs(GENERATED_PDF_DIR, exist_ok=True)
    if not GOOGLE_API_KEY:
        print("CRITICAL: GOOGLE_API_KEY non défini.")
    if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
        print("CRITICAL: Clés Telegram non définies.")
    
    app.run(debug=True, host='0.0.0.0', port=5000)