Mariam-pdf / app.py
Docfile's picture
Update app.py
ac7a3b5 verified
raw
history blame
14.3 kB
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 ---
@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é'})
# 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
})
@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'])
# 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)