Spaces:
Running
Running
from flask import Flask, render_template, request, send_file, jsonify | |
from google import genai | |
from PIL import Image | |
import subprocess | |
import tempfile | |
import os | |
import io | |
import shutil | |
import base64 | |
app = Flask(__name__) | |
# --- Convstantes --- | |
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}}" | |
# --- Configuration des secrets --- | |
# Dans un environnement de production, utilisez les variables d'environnement ou un fichier .env | |
GOOGLE_API_KEY = os.environ.get('GOOGLE_API_KEY', '') | |
# --- Fonctions Utilitaires --- | |
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, 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: {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 "" # 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 | |
# Assure que le document commence correctement | |
if not cleaned.startswith("\\documentclass"): | |
# On pourrait choisir de lever une erreur ou de tenter une correction | |
pass | |
# 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: | |
# 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é | |
# --- Fonction Principale (API GenAI) --- | |
def generate_complete_latex(client, image_bytes): | |
""" | |
Génère une solution complète en LaTeX à partir de l'image en utilisant l'exécution de code. | |
""" | |
try: | |
# Convertir les bytes en image PIL | |
image = Image.open(io.BytesIO(image_bytes)) | |
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. | |
# TÂCHE PRINCIPALE | |
1. Analyse l'image pour comprendre l'énoncé de l'exercice. | |
2. Utilise Python pour effectuer des calculs complexes si nécessaire. | |
3. Résous l'exercice mathématique étape par étape. | |
4. Génère ensuite la solution complète en LaTeX. | |
# SPÉCIFICATIONS TECHNIQUES DU CODE LATEX | |
[vos spécifications LaTeX actuelles...] | |
""" | |
response = client.models.generate_content( | |
model=MODEL_SINGLE_GENERATION, | |
contents=[image, prompt], | |
config=genai.types.GenerateContentConfig( | |
temperature=0.3, | |
tools=[genai.types.Tool( | |
code_execution=genai.types.ToolCodeExecution() | |
)] | |
) | |
) | |
# Traitement de la réponse avec gestion de l'exécution de code | |
latex_content_raw = None | |
thinking_content = None | |
code_execution_results = [] | |
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 | |
elif getattr(part, 'executable_code', None): | |
# Capture le code exécutable et son résultat | |
code = part.executable_code.code | |
result = getattr(part, 'code_execution_result', None) | |
result_output = result.output if result else "Pas de résultat d'exécution" | |
code_execution_results.append({ | |
'code': code, | |
'result': result_output | |
}) | |
elif part.text: | |
# Le contenu texte contient probablement le LaTeX | |
if '\\documentclass' in part.text: | |
latex_content_raw = part.text | |
# Si aucun contenu LaTeX n'a été trouvé dans les parties, chercher dans le texte global | |
if not latex_content_raw and response.text: | |
if '\\documentclass' in response.text: | |
latex_content_raw = response.text | |
if latex_content_raw: | |
latex_content_cleaned = clean_latex(latex_content_raw) | |
# Vérifier et ajouter la mention obligatoire | |
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, code_execution_results | |
else: | |
return None, thinking_content, "Aucun contenu LaTeX n'a été généré par le modèle.", code_execution_results | |
except Exception as e: | |
return None, None, f"❌ Erreur lors de la génération du LaTeX: {str(e)}", [] | |
# --- Fonction de Compilation LaTeX --- | |
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: {str(e)}" | |
# Exécute 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 | |
compilation_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) | |
compilation_output += f"Passe {pass_num}: Succès\n" | |
# 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_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" | |
# Essayer de lire le fichier .log pour plus d'infos | |
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)}" | |
# 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é: {str(e)}" | |
else: | |
error_message = "❌ Le fichier PDF n'a pas été généré après la compilation LaTeX.\n" | |
# Tenter de lire le log | |
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 | |
# --- Routes Flask --- | |
def index(): | |
return render_template('index.html') | |
def check_latex(): | |
latex_installed, message = check_latex_installation() | |
return jsonify({ | |
'success': latex_installed, | |
'message': message | |
}) | |
def check_api(): | |
client, message = initialize_genai_client() | |
return jsonify({ | |
'success': client is not None, | |
'message': message | |
}) | |
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é'}) | |
# Lire l'image | |
image_bytes = file.read() | |
# Initialiser le client | |
client, message = initialize_genai_client() | |
if client is None: | |
return jsonify({'success': False, 'message': message}) | |
# Générer le LaTeX avec exécution de code | |
latex_content, thinking_content, error, code_execution_results = generate_complete_latex(client, image_bytes) | |
if error: | |
return jsonify({'success': False, 'message': error}) | |
# Compiler en PDF | |
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, | |
'code_execution': code_execution_results | |
}) | |
# Encoder le PDF en base64 pour le retourner au front-end | |
pdf_base64 = base64.b64encode(pdf_data).decode('utf-8') | |
# Retourner les données | |
return jsonify({ | |
'success': True, | |
'message': 'PDF généré avec succès', | |
'latex': latex_content, | |
'thinking': thinking_content, | |
'code_execution': code_execution_results, | |
'pdf_base64': pdf_base64 | |
}) | |
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']) | |
# Créer un fichier temporaire pour le PDF | |
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_file: | |
temp_file.write(pdf_data) | |
temp_path = temp_file.name | |
# Envoyer le fichier | |
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) |