|
from flask import Flask, render_template, request, send_file, jsonify |
|
from google import genai |
|
from google.genai import types |
|
from PIL import Image |
|
import subprocess |
|
import tempfile |
|
import os |
|
import io |
|
import shutil |
|
import base64 |
|
|
|
app = Flask(__name__) |
|
|
|
|
|
MODEL_SINGLE_GENERATION = "gemini-2.5-pro-exp-03-25" |
|
LATEX_MENTION = r"\vspace{1cm}\noindent\textit{Ce devoir a été généré par Mariam AI. \url{https://mariam-241.vercel.app}}" |
|
|
|
|
|
|
|
GOOGLE_API_KEY = os.environ.get('GOOGLE_API_KEY', '') |
|
|
|
|
|
|
|
def check_latex_installation(): |
|
"""Vérifie si pdflatex est accessible dans le PATH système.""" |
|
try: |
|
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 (comme TeX Live ou MiKTeX) et vous assurer qu'elle est dans le PATH système. Erreur: {e}" |
|
|
|
def initialize_genai_client(): |
|
"""Initialise et retourne le client Google GenAI.""" |
|
try: |
|
if not GOOGLE_API_KEY: |
|
return None, "Clé API Google non trouvée dans les variables d'environnement." |
|
|
|
client = genai.Client(api_key=GOOGLE_API_KEY) |
|
return client, "✅ Client Mariam AI initialisé." |
|
except Exception as e: |
|
return None, f"❌ Erreur lors de l'initialisation du client: {str(e)}" |
|
|
|
def clean_latex(raw_latex_text): |
|
"""Nettoie le code LaTeX brut potentiellement fourni par Gemini.""" |
|
if not isinstance(raw_latex_text, str): |
|
return "" |
|
|
|
cleaned = raw_latex_text.strip() |
|
|
|
|
|
if cleaned.startswith("```latex"): |
|
cleaned = cleaned[8:] |
|
elif cleaned.startswith("```"): |
|
|
|
lines = cleaned.split('\n', 1) |
|
if len(lines) > 1: |
|
cleaned = lines[1] |
|
else: |
|
cleaned = cleaned[3:] |
|
|
|
|
|
if cleaned.endswith("```"): |
|
cleaned = cleaned[:-3].strip() |
|
|
|
|
|
if not cleaned.startswith("\\documentclass"): |
|
|
|
pass |
|
|
|
|
|
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: |
|
|
|
if cleaned.strip().startswith("\\documentclass"): |
|
cleaned += f"\n{end_doc_tag}" |
|
|
|
return cleaned.strip() |
|
|
|
|
|
|
|
def generate_complete_latex(client, image_bytes): |
|
""" |
|
Génère une solution complète en LaTeX à partir de l'image en une seule étape. |
|
""" |
|
try: |
|
|
|
image = Image.open(io.BytesIO(image_bytes)) |
|
|
|
prompt =r""" |
|
|
|
|
|
# RÔLE & OBJECTIF |
|
|
|
Tu es un expert en mathématiques et un excellent professeur. |
|
Ton objectif est de créer la solution complète d'un exercice de mathématiques. Cette solution doit être rédigée de manière à produire un **document final très soigné, clair, et d'aspect professionnel (pense à un rendu LaTeX de haute qualité)**. Tu dois fournir le code source qui permettra de générer ce document. |
|
|
|
# CONTEXTE |
|
|
|
* **Input :** L'énoncé de l'exercice de mathématiques (niveau Terminale, système français) sera fourni sous forme de texte, immédiatement après ce prompt. |
|
* **Output Attendu :** **Uniquement** le code source nécessaire pour générer le document final. Pas d'explications en dehors de ce code, ni de marqueurs de code (comme ``` ```). |
|
|
|
# TÂCHE PRINCIPALE |
|
|
|
1. **Analyse :** Lis et comprends parfaitement l'énoncé de l'exercice fourni. |
|
2. **Résolution :** Résous l'exercice de manière exhaustive, en détaillant chaque étape de raisonnement et de calcul. |
|
3. **Rédaction du Code Source :** Rédige la solution complète *directement* sous forme de code source. Ce code doit être structuré pour produire un document final qui respecte les standards de qualité décrits ci-dessous. |
|
|
|
# QUALITÉ ET PRÉSENTATION ATTENDUES DU DOCUMENT FINAL |
|
|
|
* **Structure Professionnelle :** |
|
* Le document doit avoir une présentation initiale claire (titre de l'exercice, sous-titre, date). |
|
* Une table des matières doit être incluse si la solution est un peu longue. |
|
* Les titres des sections et sous-sections doivent être clairs et bien mis en forme. |
|
* **Mise en Valeur Pédagogique :** |
|
* L'énoncé original de l'exercice doit être clairement rappelé au début. |
|
* Utilise des mises en forme distinctes (par exemple, des boîtes ou des styles spécifiques) pour : |
|
* Les définitions ou théorèmes importants. |
|
* Les résultats finaux ou les conclusions clés de chaque question. |
|
* Les remarques, astuces méthodologiques, ou points d'attention. |
|
* Les exemples illustratifs, si besoin. |
|
* **Clarté Mathématique :** |
|
* Les formules mathématiques doivent être écrites correctement, alignées et faciles à lire. |
|
* Utilise des notations mathématiques standard et cohérentes (par exemple, pour les ensembles de nombres $\mathbb{R}, \mathbb{N}$, les limites, les dérivées, etc.). |
|
* **Lisibilité Générale :** |
|
* Le texte doit être en français, clair et précis. |
|
* La mise en page doit être aérée (paragraphes distincts, espacements). |
|
* **Graphiques (si pertinent) :** |
|
* Si l'exercice implique une étude de fonction ou de la géométrie, inclus des graphiques propres et bien légendés (axes, courbes, points importants, asymptotes). Les couleurs doivent être utilisées judicieusement pour améliorer la compréhension. |
|
|
|
# CONTENU PÉDAGOGIQUE DE LA SOLUTION |
|
|
|
1. **Rigueur et Justification :** Justifie *chaque* étape de calcul ou de raisonnement. |
|
2. **Clarté des Explications :** Explique les "pourquoi" derrière les méthodes utilisées. Utilise un langage mathématique précis. |
|
3. **Complétude :** Traite toutes les questions de l'énoncé. |
|
4. **Conclusion :** Si pertinent, conclus en résumant les points essentiels. |
|
|
|
# CONTRAINTES STRICTES |
|
|
|
* Le seul output doit être le code source brut permettant de générer le document décrit. |
|
* Aucun texte avant la première ligne du code source ou après la dernière ligne (sauf la mention spéciale ci-dessous). |
|
* Inclus la ligne `{LATEX_MENTION}` juste avant la toute fin du code source. |
|
|
|
""" |
|
|
|
response = client.models.generate_content( |
|
model=MODEL_SINGLE_GENERATION, |
|
contents=[image, prompt], |
|
config=genai.types.GenerateContentConfig( |
|
temperature=1, |
|
), |
|
|
|
) |
|
|
|
latex_content_raw = None |
|
thinking_content = None |
|
|
|
|
|
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) |
|
|
|
|
|
if LATEX_MENTION not in latex_content_cleaned: |
|
|
|
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: |
|
|
|
latex_content_cleaned += f"\n{LATEX_MENTION}\n{end_doc_tag}" |
|
|
|
return latex_content_cleaned, thinking_content, None |
|
else: |
|
return None, thinking_content, "Aucun contenu LaTeX n'a été généré par le modèle." |
|
|
|
except genai.types.StopCandidateException as e: |
|
return None, None, "❌ Génération stoppée (possible contenu inapproprié)" |
|
except Exception as e: |
|
return None, None, f"❌ Erreur lors de la génération du LaTeX: {str(e)}" |
|
|
|
|
|
|
|
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." |
|
|
|
|
|
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") |
|
|
|
|
|
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: {str(e)}" |
|
|
|
|
|
command = [ |
|
"pdflatex", |
|
"-interaction=nonstopmode", |
|
f"-output-directory={temp_dir}", |
|
f"-jobname={os.path.splitext(pdf_filename)[0]}", |
|
tex_filepath |
|
] |
|
|
|
pdf_generated = False |
|
compilation_output = "" |
|
|
|
for pass_num in range(1, 3): |
|
try: |
|
|
|
result = subprocess.run(command, capture_output=True, check=True, text=True, encoding='utf-8', errors='replace', timeout=60) |
|
compilation_output += f"Passe {pass_num}: Succès\n" |
|
|
|
|
|
if os.path.exists(pdf_filepath): |
|
pdf_generated = True |
|
|
|
except subprocess.CalledProcessError as e: |
|
error_message = f"❌ Erreur lors de la compilation LaTeX (Passe {pass_num}).\n" |
|
error_message += f"Code de retour: {e.returncode}\n" |
|
error_message += f"Sortie: {e.stdout}\n" |
|
error_message += f"Erreurs: {e.stderr}\n" |
|
|
|
|
|
try: |
|
with open(log_filepath, "r", encoding="utf-8", errors='replace') as log_file: |
|
error_message += f"Extrait du fichier log (solution.log):\n{log_file.read()[-2000:]}" |
|
except IOError: |
|
error_message += "Impossible de lire le fichier log de LaTeX." |
|
|
|
return None, error_message |
|
|
|
except subprocess.TimeoutExpired: |
|
return None, "❌ La compilation LaTeX a dépassé le délai imparti. Le document est peut-être trop complexe ou contient une boucle infinie." |
|
|
|
except Exception as e: |
|
return None, f"❌ Une erreur inattendue est survenue lors de l'exécution de pdflatex: {str(e)}" |
|
|
|
|
|
if pdf_generated and os.path.exists(pdf_filepath): |
|
try: |
|
|
|
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é: {str(e)}" |
|
else: |
|
error_message = "❌ Le fichier PDF n'a pas été généré après la compilation LaTeX.\n" |
|
|
|
try: |
|
with open(log_filepath, "r", encoding="utf-8", errors='replace') as log_file: |
|
error_message += f"Extrait du fichier log (solution.log) après échec de génération PDF:\n{log_file.read()[-2000:]}" |
|
except IOError: |
|
error_message += "Impossible de lire le fichier log de LaTeX." |
|
return None, error_message |
|
|
|
|
|
|
|
@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', methods=['POST']) |
|
def process(): |
|
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é'}) |
|
|
|
|
|
image_bytes = file.read() |
|
|
|
|
|
client, message = initialize_genai_client() |
|
if client is None: |
|
return jsonify({'success': False, 'message': message}) |
|
|
|
|
|
latex_content, thinking_content, error = generate_complete_latex(client, image_bytes) |
|
if error: |
|
return jsonify({'success': False, 'message': error}) |
|
|
|
|
|
pdf_data, message = compile_latex_to_pdf(latex_content) |
|
if pdf_data is None: |
|
return jsonify({ |
|
'success': False, |
|
'message': message, |
|
'latex': latex_content, |
|
'thinking': thinking_content |
|
}) |
|
|
|
|
|
pdf_base64 = base64.b64encode(pdf_data).decode('utf-8') |
|
|
|
|
|
return jsonify({ |
|
'success': True, |
|
'message': 'PDF généré avec succès', |
|
'latex': latex_content, |
|
'thinking': thinking_content, |
|
'pdf_base64': pdf_base64 |
|
}) |
|
|
|
@app.route('/download-pdf', methods=['POST']) |
|
def download_pdf(): |
|
if 'pdf_data' not in request.json: |
|
return jsonify({'success': False, 'message': 'Données PDF manquantes'}) |
|
|
|
try: |
|
pdf_data = base64.b64decode(request.json['pdf_data']) |
|
|
|
|
|
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_file: |
|
temp_file.write(pdf_data) |
|
temp_path = temp_file.name |
|
|
|
|
|
return send_file( |
|
temp_path, |
|
mimetype='application/pdf', |
|
as_attachment=True, |
|
download_name='solution_mariam_ai.pdf' |
|
) |
|
except Exception as e: |
|
return jsonify({'success': False, 'message': f'Erreur lors du téléchargement: {str(e)}'}) |
|
|
|
if __name__ == "__main__": |
|
app.run(debug=True) |