Spaces:
Running
Running
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__) | |
# --- Cognvstantes --- | |
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 une seule étape. | |
""" | |
try: | |
# Convertir les bytes en image PIL | |
image = Image.open(io.BytesIO(image_bytes)) | |
prompt = f""" | |
# RÔLE & OBJECTIF | |
Agis en tant qu'expert en mathématiques et tuteur pédagogue de haut niveau. Ton objectif est de créer une correction détaillée, irréprochable et visuellement exceptionnelle pour l'exercice mathématique présenté dans l'image fournie. La correction doit être sous forme de document LaTeX complet, directement compilable et avec une présentation professionnelle. | |
# 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 avec mise en page professionnelle. | |
# 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 techniques et de présentation ci-dessous. | |
# SPÉCIFICATIONS TECHNIQUES DU CODE LATEX | |
1. **Structure de Base:** | |
- Document de classe article au format A4 avec police de 12pt | |
- Utilisation de la langue française avec babel | |
- Marges optimisées pour une lisibilité maximale | |
2. **Packages Essentiels:** Inclus impérativement les packages suivants: | |
- Fondamentaux: `amsmath`, `amssymb`, `amsfonts`, `mathtools`, `geometry` | |
- Mise en page: `fancyhdr`, `titlesec`, `setspace`, `hyperref`, `url` | |
- Présentation visuelle: `xcolor`, `tcolorbox`, `microtype`, `lmodern` | |
- Graphiques (si nécessaire): `tikz`, `pgfplots` | |
3. **Compilabilité:** Le code généré doit être valide et compilable sans erreur avec `pdflatex`. | |
4. **AUCUN Marqueur de Code:** Le résultat doit être **uniquement** le code LaTeX brut. N'inclus **JAMAIS** de marqueurs de code. | |
# STYLE & PRÉSENTATION PROFESSIONNELLE | |
1. **Page de Titre Élégante:** | |
- Titre encadré et mis en valeur | |
- Sous-titre descriptif | |
- Date du jour automatique | |
2. **Structure Visuelle Avancée:** | |
- En-têtes et pieds de page personnalisés avec le titre du document et numéros de page | |
- Table des matières pour les documents longs | |
- Sections et sous-sections avec titres colorés et hiérarchisés | |
- Espacement optimisé entre les paragraphes et les sections | |
3. **Boîtes Colorées pour Mettre en Évidence:** | |
- L'énoncé de l'exercice (boîte grise) | |
- Les résultats importants (boîte rouge) | |
- Les notes explicatives et astuces (boîte verte) | |
- Les définitions et théorèmes importants (boîte bleue) | |
4. **Typographie Soignée:** | |
- Utilisation de la police Latin Modern | |
- Micro-ajustements typographiques pour une lisibilité optimale | |
- Espacement mathématique équilibré | |
5. **Commandes Personnalisées:** | |
- Définir des commandes pour les notations mathématiques récurrentes | |
- Créer des environnements spécifiques pour les différentes parties de la solution | |
6. **Représentation Graphique:** | |
- Inclure une représentation graphique des fonctions ou concepts mathématiques quand c'est pertinent | |
- Utiliser TikZ pour créer des figures claires et profesionnelles | |
# CONTENU PÉDAGOGIQUE DE LA SOLUTION | |
1. **Structure Logique Claire:** | |
- Introduction rappelant l'énoncé dans une boîte distinctive | |
- Corps de la solution organisé par étapes numérotées ou sections | |
- Conclusion récapitulant les résultats principaux | |
2. **Approche Pédagogique:** | |
- Explication détaillée et justification de chaque étape | |
- Mise en évidence des points clés et des techniques importantes | |
- Suggestions d'astuces et de méthodes alternatives quand c'est pertinent | |
3. **Rigueur et Clarté:** | |
- Présentation irréprochable des calculs via des environnements adaptés | |
- Alignement soigné des équations mathématiques | |
- Explications textuelles entre les étapes mathématiques | |
4. **Mention Obligatoire:** Insère la ligne suivante **exactement** telle quelle, juste avant la ligne `\\end{{document}}`: | |
{LATEX_MENTION} | |
# PROCESSUS DE CRÉATION RECOMMANDÉ | |
1. **Conception du Préambule:** | |
- Configurer tous les packages et personnalisations pour une mise en page professionnelle | |
- Définir les environnements personnalisés pour l'énoncé, les résultats, les notes et les définitions | |
2. **Élaboration de la Structure:** | |
- Créer une page de titre élégante | |
- Organiser le contenu en sections logiques | |
- Préparer les en-têtes et pieds de page | |
3. **Rédaction de la Solution:** | |
- Présenter l'énoncé dans une boîte colorée distinctive | |
- Développer la solution étape par étape avec justifications | |
- Mettre en évidence les résultats importants dans des boîtes colorées | |
- Ajouter des figures ou graphiques si nécessaire | |
4. **Finalisation:** | |
- Vérifier la cohérence mathématique et la rigueur de la solution | |
- S'assurer que le code LaTeX est propre, bien indenté et commenté si nécessaire | |
- Confirmer que le document respecte toutes les spécifications demandées | |
Produis un document LaTeX qui allie excellence mathématique et présentation visuelle de très haut niveau, digne des meilleures publications académiques. | |
""" | |
response = client.models.generate_content( | |
model=MODEL_SINGLE_GENERATION, | |
contents=[image, prompt], | |
config=genai.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: | |
# 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, 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)}" | |
# --- 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 | |
latex_content, thinking_content, error = 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 | |
}) | |
# 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, | |
'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) |