File size: 15,807 Bytes
28b64a0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
from flask import Flask, render_template, request, jsonify, send_file
from google import genai
from PIL import Image
import subprocess
import tempfile
import os
import io
import shutil
import time
import uuid
import base64
from dotenv import load_dotenv

# Chargement des variables d'environnement
load_dotenv()

app = Flask(__name__)

# --- Constantes ---
MODEL_SINGLE_GENERATION = "gemini-2.5-pro-exp-03-25"  # Modèle avec "thinking" pour la génération complète
LATEX_MENTION = r"\vspace{1cm}\noindent\textit{Ce devoir a été généré par Mariam AI. \url{https://mariam-241.vercel.app}}"

# --- Fonctions Utilitaires ---

def check_latex_installation():
    """Vérifie si pdflatex est accessible dans le PATH système."""
    try:
        # Tente d'exécuter pdflatex pour vérifier son existence et son fonctionnement
        subprocess.run(['pdflatex', '--version'], capture_output=True, check=True, timeout=10)
        return True, "Installation LaTeX (pdflatex) trouvée."
    except (subprocess.CalledProcessError, FileNotFoundError, TimeoutError) as e:
        return False, f"pdflatex introuvable ou ne répond pas. Veuillez installer une distribution LaTeX. Erreur: {e}"

def initialize_genai_client():
    """Initialise et retourne le client Google GenAI."""
    try:
        api_key = os.environ.get("GOOGLE_API_KEY")
        if not api_key:
            return None, "Clé API Google non trouvée dans les variables d'environnement."
        
        client = genai.Client(api_key=api_key, http_options={'api_version':'v1alpha'})
        return client, "Client Mariam AI initialisé."
    except Exception as e:
        return None, f"Erreur lors de l'initialisation du client: {e}"

def clean_latex(raw_latex_text):
    """Nettoie le code LaTeX brut potentiellement fourni par Gemini."""
    if not isinstance(raw_latex_text, str):
        return ""  # Retourne une chaîne vide si l'entrée n'est pas une chaîne

    cleaned = raw_latex_text.strip()

    # Supprime les marqueurs de bloc de code (```latex ... ``` ou ``` ... ```)
    if cleaned.startswith("```latex"):
        cleaned = cleaned[8:]
    elif cleaned.startswith("```"):
         # Gère le cas où il y a un saut de ligne après ```
        lines = cleaned.split('\n', 1)
        if len(lines) > 1:
            cleaned = lines[1]
        else:
            cleaned = cleaned[3:]  # Si pas de saut de ligne, juste enlever ```

    # Supprime les marqueurs de fin de bloc de code
    if cleaned.endswith("```"):
        cleaned = cleaned[:-3].strip()  # Enlève ``` et les espaces potentiels avant

    # Vérification du début du document
    if not cleaned.startswith("\\documentclass"):
        app.logger.warning("Le code LaTeX reçu ne commence pas par \\documentclass.")

    # Assure que le document se termine exactement par \end{document}
    end_doc_tag = "\\end{document}"
    if end_doc_tag in cleaned:
        end_idx = cleaned.rfind(end_doc_tag) + len(end_doc_tag)
        cleaned = cleaned[:end_idx]
    else:
        app.logger.warning("Le code LaTeX reçu ne contient pas \\end{document}. Tentative d'ajout.")
        # Ajoute seulement si \documentclass est présent pour éviter de créer un doc invalide
        if cleaned.strip().startswith("\\documentclass"):
             cleaned += f"\n{end_doc_tag}"

    return cleaned.strip()  # Retourne le résultat nettoyé

def generate_complete_latex(client, image):
    """
    Génère une solution complète en LaTeX à partir de l'image en une seule étape,
    en utilisant un modèle capable de "réfléchir" (thinking) et de produire un document bien formaté.
    """
    prompt = f"""
    # ROLE & OBJECTIF
Agis en tant qu'expert en mathématiques et tuteur pédagogue. Ton objectif est de créer une correction détaillée et irréprochable pour l'exercice mathématique présenté dans l'image fournie. La correction doit être sous forme de document LaTeX complet et directement compilable.

# CONTEXTE
- **Input:** Une image contenant un exercice de mathématiques.
- **Niveau Cible:** Élève de Terminale S (Lycée, filière scientifique française).
- **Output Attendu:** Un fichier source LaTeX (.tex) autonome.

# TÂCHE PRINCIPALE
1.  Analyse l'image pour comprendre parfaitement l'énoncé de l'exercice.
2.  Résous l'exercice de manière rigoureuse, étape par étape.
3.  Rédige la solution complète directement en code LaTeX, en respectant **toutes** les spécifications ci-dessous.

# SPÉCIFICATIONS TECHNIQUES DU CODE LATEX
1.  **Structure du Document:** Commence **strictement** par `\\documentclass{{article}}` et se termine **strictement** par `\\end{{document}}`.
2.  **Packages Requis:** Inclus impérativement les packages suivants via `\\usepackage{{...}}`: `amsmath`, `amssymb`, `geometry`, `hyperref`, `url`. Assure-toi qu'ils sont déclarés dans le préambule.
3.  **Compilabilité:** Le code généré doit être valide et compilable sans erreur avec `pdflatex`.
4.  **Formatage du Code:** Produis un code LaTeX propre, bien indenté et lisible.
5.  **Environnements Mathématiques:** Utilise les environnements LaTeX appropriés (`align`, `equation`, `gather`, etc.) pour présenter les calculs et les équations de manière claire et standard.
6.  **AUCUN Marqueur de Code:** Le résultat doit être **uniquement** le code LaTeX brut. N'inclus **JAMAIS** de marqueurs de code comme ```latex ... ``` ou ``` ... ``` au début ou à la fin.

# STYLE & CONTENU DE LA SOLUTION
1.  **Pédagogie:** La correction doit être claire, aérée et facile à comprendre pour un élève de Terminale S.
2.  **Justifications:** Justifie **chaque** étape clé du raisonnement mathématique. Explique *pourquoi* une certaine méthode est utilisée ou *comment* on passe d'une étape à l'autre.
3.  **Rigueur:** Assure l'exactitude mathématique complète de la solution.
4.  **Structure Logique:** Organise la solution de manière logique. Utilise des sections (`\\section*{{...}}`, `\\subsection*{{...}}`) si cela améliore la clarté pour des problèmes longs ou multi-parties.
5.  **Mention Obligatoire:** Insère la ligne suivante **exactement** telle quelle, juste avant la ligne `\\end{{document}}`:
    {LATEX_MENTION}

# PROCESSUS INTERNE RECOMMANDÉ (Pour l'IA)
1.  **Analyse Approfondie:** Décompose le problème en sous-étapes logiques.
2.  **Résolution Étape par Étape:** Effectue la résolution mathématique complète en interne.
3.  **Traduction en LaTeX:** Convertis ta résolution raisonnée en code LaTeX, en appliquant méticuleusement toutes les spécifications de formatage et de style demandées
    """
    try:
        from google.genai import types
        
        response = client.models.generate_content(
            model=MODEL_SINGLE_GENERATION,
            contents=[image, prompt],
            config=types.GenerateContentConfig(
                temperature=0.3,
            ))

        latex_content_raw = None
        thinking_content = None

        # Extrait le contenu LaTeX et le raisonnement (thought)
        if response.candidates and response.candidates[0].content and response.candidates[0].content.parts:
            for part in response.candidates[0].content.parts:
                if getattr(part, 'thought', None):
                    thinking_content = part.text
                else:
                    latex_content_raw = part.text

        if latex_content_raw:
            latex_content_cleaned = clean_latex(latex_content_raw)
            
            # Vérification supplémentaire : est-ce que la mention obligatoire est présente ?
            if LATEX_MENTION not in latex_content_cleaned:
                app.logger.warning("La mention obligatoire semble manquer. Tentative d'insertion.")
                # Tente de l'insérer avant \end{document}
                end_doc_tag = "\\end{document}"
                if end_doc_tag in latex_content_cleaned:
                    latex_content_cleaned = latex_content_cleaned.replace(end_doc_tag, f"{LATEX_MENTION}\n{end_doc_tag}")
                else:
                    # Si \end{document} n'est pas là, ajoute les deux à la fin
                    latex_content_cleaned += f"\n{LATEX_MENTION}\n{end_doc_tag}"
                    
            return latex_content_cleaned, thinking_content
        else:
            return None, thinking_content

    except Exception as e:
        app.logger.error(f"Erreur lors de la génération du LaTeX: {e}")
        return None, None

def compile_latex_to_pdf(latex_content):
    """
    Compile une chaîne de caractères contenant du code LaTeX en fichier PDF.
    Utilise un répertoire temporaire pour les fichiers intermédiaires.
    """
    if not latex_content:
        return None, "Impossible de compiler : contenu LaTeX vide."

    # Utilise un répertoire temporaire sécurisé qui sera nettoyé automatiquement
    with tempfile.TemporaryDirectory() as temp_dir:
        tex_filename = "solution.tex"
        pdf_filename = "solution.pdf"
        tex_filepath = os.path.join(temp_dir, tex_filename)
        pdf_filepath = os.path.join(temp_dir, pdf_filename)
        log_filepath = os.path.join(temp_dir, "solution.log")  # Pour le débogage

        # Écrit le contenu LaTeX dans le fichier .tex avec encodage UTF-8
        try:
            with open(tex_filepath, "w", encoding="utf-8") as f:
                f.write(latex_content)
        except IOError as e:
            return None, f"Erreur lors de l'écriture du fichier .tex temporaire: {e}"

        # Commande pdflatex
        command = [
            "pdflatex",
            "-interaction=nonstopmode",
            f"-output-directory={temp_dir}",
            f"-jobname={os.path.splitext(pdf_filename)[0]}",  # Nom sans extension
            tex_filepath
        ]

        pdf_generated = False
        log_output = ""
        
        for pass_num in range(1, 3):  # Lance jusqu'à 2 passes
            try:
                # Augmentation du timeout pour les compilations potentiellement longues
                result = subprocess.run(command, capture_output=True, check=True, text=True, encoding='utf-8', errors='replace', timeout=60)
                # Vérifie si le PDF a été créé après la première passe (au moins)
                if os.path.exists(pdf_filepath):
                    pdf_generated = True

            except subprocess.CalledProcessError as e:
                error_msg = f"Erreur lors de la compilation LaTeX (Passe {pass_num}). Code: {e.returncode}"
                # Essayer de lire le fichier .log pour plus d'infos
                try:
                    with open(log_filepath, "r", encoding="utf-8", errors='replace') as log_file:
                        log_output = log_file.read()[-2000:]  # Dernières lignes du log
                except IOError:
                    log_output = "Impossible de lire le fichier log de LaTeX."
                return None, f"{error_msg}\n{log_output}"
                
            except subprocess.TimeoutExpired:
                return None, "La compilation LaTeX a dépassé le délai imparti."
                
            except Exception as e:
                return None, f"Erreur inattendue lors de l'exécution de pdflatex: {e}"

        # Vérifie si le fichier PDF existe après les passes
        if pdf_generated and os.path.exists(pdf_filepath):
            try:
                # Lit le contenu binaire du PDF généré
                with open(pdf_filepath, "rb") as f:
                    pdf_data = f.read()
                return pdf_data, "PDF généré avec succès !"
            except IOError as e:
                return None, f"Erreur lors de la lecture du fichier PDF généré: {e}"
        else:
            # Tenter de lire le log si le PDF n'a pas été généré
            try:
                with open(log_filepath, "r", encoding="utf-8", errors='replace') as log_file:
                    log_output = log_file.read()[-2000:]
            except IOError:
                log_output = "Impossible de lire le fichier log de LaTeX."
            return None, f"Le PDF n'a pas été généré après la compilation LaTeX.\n{log_output}"

# --- Routes Flask ---

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

@app.route('/check_latex', methods=['GET'])
def check_latex():
    latex_installed, message = check_latex_installation()
    return jsonify({
        'success': latex_installed,
        'message': message
    })

@app.route('/check_api', methods=['GET'])
def check_api():
    client, message = initialize_genai_client()
    return jsonify({
        'success': client is not None,
        'message': message
    })

@app.route('/process_image', methods=['POST'])
def process_image():
    if 'image' not in request.files:
        return jsonify({'success': False, 'message': 'Aucune image téléchargée'})
    
    file = request.files['image']
    if file.filename == '':
        return jsonify({'success': False, 'message': 'Aucun fichier sélectionné'})
    
    try:
        # Lecture de l'image
        image_data = file.read()
        image = Image.open(io.BytesIO(image_data))
        
        # Initialisation du client
        client, client_message = initialize_genai_client()
        if not client:
            return jsonify({'success': False, 'message': client_message})
        
        # Génération du LaTeX
        latex_content, thinking_content = generate_complete_latex(client, image)
        if not latex_content:
            return jsonify({
                'success': False, 
                'message': 'Échec de la génération de la solution LaTeX',
                'thinking': thinking_content
            })
        
        # Compilation en PDF
        pdf_data, pdf_message = compile_latex_to_pdf(latex_content)
        if not pdf_data:
            return jsonify({
                'success': False, 
                'message': f'Échec de la compilation PDF: {pdf_message}',
                'latex': latex_content,
                'thinking': thinking_content
            })
        
        # Génération d'un ID unique pour le PDF
        pdf_id = str(uuid.uuid4())
        pdf_path = os.path.join(app.root_path, 'static', 'pdfs')
        
        # Créer le dossier s'il n'existe pas
        os.makedirs(pdf_path, exist_ok=True)
        
        # Sauvegarder le PDF
        full_pdf_path = os.path.join(pdf_path, f"{pdf_id}.pdf")
        with open(full_pdf_path, 'wb') as f:
            f.write(pdf_data)
        
        # Convertir l'image en base64 pour l'affichage
        image_base64 = base64.b64encode(image_data).decode('utf-8')
        
        return jsonify({
            'success': True,
            'message': 'PDF généré avec succès',
            'pdf_url': f'/static/pdfs/{pdf_id}.pdf',
            'latex': latex_content,
            'thinking': thinking_content,
            'image_base64': image_base64
        })
        
    except Exception as e:
        app.logger.error(f"Erreur dans process_image: {e}")
        return jsonify({'success': False, 'message': f'Erreur inattendue: {str(e)}'})

@app.route('/download_pdf/<pdf_id>', methods=['GET'])
def download_pdf(pdf_id):
    # Vérifier que l'ID ne contient pas de caractères dangereux
    if not pdf_id.isalnum() and '-' not in pdf_id:
        return "ID PDF invalide", 400
    
    pdf_path = os.path.join(app.root_path, 'static', 'pdfs', f"{pdf_id}.pdf")
    
    if not os.path.exists(pdf_path):
        return "PDF introuvable", 404
    
    return send_file(pdf_path, as_attachment=True, download_name="solution_mariam_ai.pdf")

if __name__ == '__main__':
    # Création du dossier pour les PDFs
    os.makedirs(os.path.join(app.root_path, 'static', 'pdfs'), exist_ok=True)
    
    # Lancement de l'application
    app.run(debug=True)