Docfile commited on
Commit
e22e1db
·
verified ·
1 Parent(s): 7facaba

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +172 -737
app.py CHANGED
@@ -1,6 +1,4 @@
1
-
2
- # --- IMPORTS ---
3
- from flask import Flask, render_template, request, jsonify, Response, stream_with_context
4
  from google import genai
5
  import os
6
  from google.genai import types
@@ -17,822 +15,249 @@ import subprocess
17
  import shutil
18
  import re
19
 
20
- # --- FLASK APP INITIALIZATION ---
21
  app = Flask(__name__)
22
 
23
- # --- CONFIGURATION ---
24
- # API Keys
25
  GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
26
- # IMPORTANT: For production, move these to environment variables or a secure config
27
- TELEGRAM_BOT_TOKEN = "8004545342:AAGcZaoDjYg8dmbbXRsR1N3TfSSbEiAGz88"
28
  TELEGRAM_CHAT_ID = "-1002564204301"
 
29
 
30
- # Gemini Client Initialization
31
  if GOOGLE_API_KEY:
32
  try:
33
  client = genai.Client(api_key=GOOGLE_API_KEY)
34
  except Exception as e:
35
- print(f"Erreur lors de l'initialisation du client Gemini: {e}")
36
  client = None
37
  else:
38
- print("GEMINI_API_KEY non trouvé. Le client Gemini ne sera pas initialisé.")
39
  client = None
40
 
41
- # Dictionnaire pour stocker les résultats des tâches en cours
42
  task_results = {}
43
 
44
- # --- PROMPT DEFINITIONS ---
45
- # All prompts are now defined directly in the code instead of separate files.
46
-
47
- def get_prompt_extract_problem():
48
- """Prompt pour extraire l'énoncé mathématique à partir des fichiers."""
49
- return """
50
- From the provided image(s) and/or PDF document, your task is to identify and extract the complete mathematical problem statement.
51
-
52
- - Focus solely on the problem itself, including all conditions, variables, and the question being asked.
53
- - Exclude any surrounding text like page numbers, author names, or irrelevant context.
54
- - Format all mathematical expressions using TeX (e.g., $f(x) = x^2 - 1$, for all $x \in \mathbb{R}$).
55
- - Your output must be ONLY the clean, extracted problem statement. Do not add any conversational text like "Here is the problem statement:".
56
- """
57
-
58
- def get_prompt_light():
59
- """Prompt pour une solution simple et directe (style 'light')."""
60
- return """
61
- Vous êtes un expert en mathématiques. Votre tâche est de fournir une solution claire et concise au problème soumis.
62
- La sortie doit être un code LaTeX complet, propre et directement compilable.
63
- La solution doit être bien expliquée, mais sans fioritures visuelles.
64
- Structurez la solution avec des sections logiques.
65
- Produisez UNIQUEMENT le code LaTeX.
66
- """
67
-
68
- def get_prompt_colorful():
69
- """Prompt pour une solution pédagogique et colorée (style 'colorful')."""
70
- # Ce prompt est une copie directe de votre "prompt coloful".
71
- return r"""
72
- # 📝 GÉNÉRATEUR DE CORRECTION MATHÉMATIQUE PROFESSIONNELLE
73
-
74
- ## 🎓 VOTRE RÔLE
75
- Vous êtes **Mariam-MATHEX-PRO**, un système d'intelligence artificielle ultra-spécialisé dans la création de documents mathématiques parfaits. Vous combinez l'expertise d'un:
76
- * 🧠 Professeur agrégé de mathématiques avec 25 ans d'expérience
77
- * 🖋️ Expert LaTeX de niveau international
78
- * 👨‍🏫 Pédagogue reconnu pour votre clarté exceptionnelle
79
-
80
- Votre mission: transformer un simple énoncé mathématique en une correction LaTeX impeccable, aérée et pédagogiquement parfaite.
81
-
82
- ## 📊 FORMAT D'ENTRÉE ET SORTIE
83
-
84
- **ENTRÉE:** L'énoncé d'un exercice mathématique (niveau Terminale/Supérieur)
85
-
86
- **SORTIE:** UNIQUEMENT le code source LaTeX complet (.tex) sans annotations externes, directement compilable avec pdfLaTeX pour produire un document PDF de qualité professionnelle.
87
-
88
- ## 🌟 PRINCIPES FONDAMENTAUX
89
-
90
- 1. **DESIGN AÉRÉ ET ÉLÉGANT**
91
- * Utilisez généreusement l'espace vertical entre tous les éléments
92
- * Créez un document visuellement reposant avec beaucoup d'espaces blancs
93
- * Évitez absolument la densité visuelle et le texte compact
94
-
95
- 2. **EXCELLENCE PÉDAGOGIQUE**
96
- * Une seule étape de raisonnement par paragraphe
97
- * Développement méticuleux de chaque calcul sans sauts logiques
98
- * Mise en évidence claire des points clés et des résultats
99
-
100
- 3. **ESTHÉTIQUE PROFESSIONNELLE**
101
- * Utilisation experte de la couleur pour guider l'attention
102
- * Boîtes thématiques élégantes pour structurer l'information
103
- * Typographie mathématique irréprochable
104
-
105
- ## 🛠️ SPÉCIFICATIONS TECHNIQUES DÉTAILLÉES
106
-
107
- ### 📑 STRUCTURE DE BASE
108
-
109
- ```latex
110
- \documentclass[12pt,a4paper]{article}
111
-
112
- % --- PACKAGES FONDAMENTAUX ---
113
- \usepackage[utf8]{inputenc}
114
- \usepackage[T1]{fontenc}
115
- \usepackage[french]{babel}
116
- \usepackage{lmodern}
117
- \usepackage{microtype}
118
-
119
- % --- PACKAGES MATHÉMATIQUES ---
120
- \usepackage{amsmath,amssymb,amsfonts,mathtools}
121
- \usepackage{bm} % Gras en mode mathématique
122
- \usepackage{siunitx} % Unités SI
123
-
124
- % --- MISE EN PAGE ---
125
- \usepackage[a4paper,margin=2.5cm]{geometry}
126
- \usepackage{setspace}
127
- \usepackage{fancyhdr}
128
- \usepackage{titlesec,titletoc}
129
- \usepackage{multicol}
130
- \usepackage{enumitem} % Listes personnalisées
131
-
132
- % --- ÉLÉMENTS VISUELS ---
133
- \usepackage{xcolor}
134
- \usepackage[most]{tcolorbox}
135
- \usepackage{fontawesome5}
136
- \usepackage{graphicx}
137
-
138
- % --- GRAPHIQUES ---
139
- \usepackage{tikz}
140
- \usetikzlibrary{calc,shapes,arrows.meta,positioning}
141
- \usepackage{pgfplots}
142
- \pgfplotsset{compat=1.18}
143
- \usepgfplotslibrary{fillbetween}
144
-
145
- % --- HYPERLIENS ET MÉTADONNÉES ---
146
- \usepackage{hyperref}
147
- \usepackage{bookmark}
148
-
149
- % --- ESPACEMENT EXTRA-AÉRÉ ---
150
- \setlength{\parindent}{0pt}
151
- \setlength{\parskip}{2.5ex plus 0.8ex minus 0.4ex} % Espacement paragraphes généreux
152
- \onehalfspacing % Interligne 1.5
153
- ```
154
-
155
- ### 🎨 PALETTE DE COULEURS ET STYLES VISUELS
156
-
157
- ```latex
158
- % --- DÉFINITION DES COULEURS ---
159
- \definecolor{maincolor}{RGB}{30, 100, 180} % Bleu principal
160
- \definecolor{secondcolor}{RGB}{0, 150, 136} % Vert-bleu
161
- \definecolor{thirdcolor}{RGB}{140, 0, 140} % Violet
162
- \definecolor{accentcolor}{RGB}{255, 140, 0} % Orange
163
- \definecolor{ubgcolor}{RGB}{245, 250, 255} % Fond bleuté très clair
164
- \definecolor{lightgray}{RGB}{248, 248, 248} % Gris très clair
165
- \definecolor{gridcolor}{RGB}{220, 220, 220} % Gris pour grilles
166
- \definecolor{highlightcolor}{RGB}{255, 255, 200} % Jaune clair pour surlignage
167
- \definecolor{asymptotecolor}{RGB}{220, 0, 0} % Rouge pour asymptotes
168
-
169
- % --- CONFIGURATION DE PAGE ---
170
- \pagestyle{fancy}
171
- \fancyhf{}
172
- \fancyhead[L]{\textcolor{maincolor}{\small\textit{Correction Mathématiques}}}
173
- \fancyhead[R]{\textcolor{maincolor}{\small\thepage}}
174
- \renewcommand{\headrulewidth}{0.2pt}
175
- \renewcommand{\headrule}{\hbox to\headwidth{\color{maincolor}\leaders\hrule height \headrulewidth\hfill}}
176
- \setlength{\headheight}{15pt}
177
- \setlength{\headsep}{25pt} % Plus d'espace sous l'en-tête
178
-
179
- % --- CONFIGURATION DES TITRES DE SECTION ---
180
- \titleformat{\section}
181
- {\normalfont\Large\bfseries\color{maincolor}}
182
- {\colorbox{maincolor}{\color{white}\thesection}}
183
- {1em}{}[\vspace{0.2cm}\titlerule[0.8pt]\vspace{0.8cm}]
184
-
185
- \titleformat{\subsection}
186
- {\normalfont\large\bfseries\color{secondcolor}}
187
- {\thesubsection}
188
- {1em}{}[\vspace{0.5cm}]
189
-
190
- \titlespacing*{\section}{0pt}{3.5ex plus 1ex minus .2ex}{2.3ex plus .2ex}
191
- \titlespacing*{\subsection}{0pt}{3.25ex plus 1ex minus .2ex}{1.5ex plus .2ex}
192
- ```
193
-
194
- ### 📦 BOÎTES THÉMATIQUES AÉRÉES
195
-
196
- ```latex
197
- % --- DÉFINITION DES BOÎTES THÉMATIQUES ---
198
- \newtcolorbox{enoncebox}{
199
- enhanced,
200
- breakable,
201
- colback=lightgray!50,
202
- colframe=gray!70,
203
- fonttitle=\bfseries,
204
- top=12pt, bottom=12pt, left=12pt, right=12pt,
205
- boxrule=0.5pt,
206
- arc=3mm,
207
- title={\faBook\ Énoncé},
208
- attach boxed title to top left={xshift=0.5cm,yshift=-\tcboxedtitleheight/2},
209
- boxed title style={colback=gray!70, colframe=gray!70},
210
- before={\vspace{15pt}},
211
- after={\vspace{15pt}}
212
- }
213
-
214
- \newtcolorbox{definitionbox}{
215
- enhanced,
216
- breakable,
217
- colback=secondcolor!10,
218
- colframe=secondcolor,
219
- fonttitle=\bfseries,
220
- top=12pt, bottom=12pt, left=12pt, right=12pt,
221
- boxrule=0.5pt,
222
- arc=3mm,
223
- title={\faLightbulb\ Définition/Théorème},
224
- attach boxed title to top left={xshift=0.5cm,yshift=-\tcboxedtitleheight/2},
225
- boxed title style={colback=secondcolor, colframe=secondcolor, color=white},
226
- before={\vspace{15pt}},
227
- after={\vspace{15pt}}
228
- }
229
-
230
- \newtcolorbox{resultbox}{
231
- enhanced,
232
- breakable,
233
- colback=accentcolor!10,
234
- colframe=accentcolor,
235
- fonttitle=\bfseries,
236
- top=12pt, bottom=12pt, left=12pt, right=12pt,
237
- boxrule=0.5pt,
238
- arc=3mm,
239
- title={\faCheckCircle\ Résultat},
240
- attach boxed title to top left={xshift=0.5cm,yshift=-\tcboxedtitleheight/2},
241
- boxed title style={colback=accentcolor, colframe=accentcolor, color=white},
242
- before={\vspace{15pt}},
243
- after={\vspace{15pt}}
244
- }
245
-
246
- \newtcolorbox{notebox}{
247
- enhanced,
248
- breakable,
249
- colback=thirdcolor!10,
250
- colframe=thirdcolor,
251
- fonttitle=\bfseries,
252
- top=12pt, bottom=12pt, left=12pt, right=12pt,
253
- boxrule=0.5pt,
254
- arc=3mm,
255
- title={\faInfoCircle\ Remarque/Astuce},
256
- attach boxed title to top left={xshift=0.5cm,yshift=-\tcboxedtitleheight/2},
257
- boxed title style={colback=thirdcolor, colframe=thirdcolor, color=white},
258
- before={\vspace{15pt}},
259
- after={\vspace{15pt}}
260
- }
261
-
262
- \newtcolorbox{examplebox}{
263
- enhanced,
264
- breakable,
265
- colback=green!10,
266
- colframe=green!70!black,
267
- fonttitle=\bfseries,
268
- top=12pt, bottom=12pt, left=12pt, right=12pt,
269
- boxrule=0.5pt,
270
- arc=3mm,
271
- title={\faClipboard\ Exemple/Méthode},
272
- attach boxed title to top left={xshift=0.5cm,yshift=-\tcboxedtitleheight/2},
273
- boxed title style={colback=green!70!black, colframe=green!70!black, color=white},
274
- before={\vspace{15pt}},
275
- after={\vspace{15pt}}
276
- }
277
- ```
278
-
279
- ### 🧮 COMMANDES MATHÉMATIQUES PERSONNALISÉES
280
-
281
- ```latex
282
- % --- COMMANDES MATHÉMATIQUES ---
283
- \newcommand{\R}{\mathbb{R}}
284
- \newcommand{\C}{\mathbb{C}}
285
- \newcommand{\N}{\mathbb{N}}
286
- \newcommand{\Z}{\mathbb{Z}}
287
- \newcommand{\Q}{\mathbb{Q}}
288
-
289
- \newcommand{\limx}[1]{\lim_{x \to #1}}
290
- \newcommand{\limxp}[1]{\lim_{x \to #1^+}}
291
- \newcommand{\limxm}[1]{\lim_{x \to #1^-}}
292
- \newcommand{\limsinf}{\lim_{n \to +\infty}}
293
- \newcommand{\liminf}{\lim_{x \to +\infty}}
294
-
295
- \newcommand{\derivee}[2]{\frac{d#1}{d#2}}
296
- \newcommand{\ddx}[1]{\frac{d}{dx}\left(#1\right)}
297
- \newcommand{\dfdx}[1]{\frac{df}{dx}\left(#1\right)}
298
-
299
- \newcommand{\abs}[1]{\left|#1\right|}
300
- \newcommand{\norm}[1]{\left\|#1\right\|}
301
- \newcommand{\vect}[1]{\overrightarrow{#1}}
302
- \newcommand{\ds}{\displaystyle}
303
-
304
- \newcommand{\highlight}[1]{\colorbox{highlightcolor}{$#1$}}
305
- \newcommand{\finalresult}[1]{\colorbox{accentcolor!20}{$\displaystyle #1$}}
306
-
307
- % Environnement pour équations importantes
308
- \newcommand{\boxedeq}[1]{%
309
- \begin{center}
310
- \begin{tcolorbox}[
311
- enhanced,
312
- colback=ubgcolor,
313
- colframe=maincolor,
314
- arc=3mm,
315
- boxrule=0.5pt,
316
- left=10pt,right=10pt,top=6pt,bottom=6pt
317
- ]
318
- $\displaystyle #1$
319
- \end{tcolorbox}
320
- \end{center}
321
- }
322
-
323
- % Configuration pour espacement des listes
324
- \setlist{itemsep=8pt, parsep=4pt}
325
-
326
- % Configuration des environnements mathématiques pour plus d'espacement
327
- \setlength{\abovedisplayskip}{12pt plus 3pt minus 7pt}
328
- \setlength{\belowdisplayskip}{12pt plus 3pt minus 7pt}
329
- \setlength{\abovedisplayshortskip}{7pt plus 2pt minus 4pt}
330
- \setlength{\belowdisplayshortskip}{7pt plus 2pt minus 4pt}
331
- ```
332
-
333
- ### 📊 CONFIGURATION DE GRAPHIQUES
334
-
335
- ```latex
336
- % --- CONFIGURATION DE PGFPLOTS POUR GRAPHIQUES ---
337
- \pgfplotsset{
338
- every axis/.append style={
339
- axis lines=middle,
340
- xlabel={$x$},
341
- ylabel={$y$},
342
- xlabel style={at={(ticklabel* cs:1.05)}, anchor=west},
343
- ylabel style={at={(ticklabel* cs:1.05)}, anchor=south},
344
- legend pos=outer north east,
345
- grid=both,
346
- grid style={gridcolor, line width=0.1pt},
347
- tick align=outside,
348
- minor tick num=4,
349
- enlargelimits={abs=0.2},
350
- axis line style={-Latex, line width=0.6pt},
351
- xmajorgrids=true,
352
- ymajorgrids=true,
353
- ticklabel style={font=\footnotesize}
354
- }
355
- }
356
- ```
357
-
358
- ### 🖌️ MODÈLE DE PAGE DE TITRE
359
-
360
- ```latex
361
- % --- PAGE DE TITRE ÉLÉGANTE ---
362
- \newcommand{\maketitlepage}[2]{%
363
- \begin{titlepage}
364
- \centering
365
- \vspace*{2cm}
366
- {\Huge\bfseries\color{maincolor} Correction Mathématiques\par}
367
- \vspace{1.5cm}
368
- {\huge\bfseries #1\par}
369
- \vspace{1cm}
370
- {\Large\textit{#2}\par}
371
- \vspace{2cm}
372
-
373
- \begin{tikzpicture}
374
- \draw[line width=0.5pt, maincolor] (0,0) -- (12,0);
375
- \foreach \x in {0,1,...,12} {
376
- \draw[line width=1pt, maincolor] (\x,0) -- (\x,-0.2);
377
- }
378
- \draw[line width=0.5pt, secondcolor] (0,-0.6) -- (12,-0.6);
379
- \end{tikzpicture}
380
-
381
- \vspace{1.5cm}
382
-
383
- {\Large\today\par}
384
-
385
- \vfill
386
-
387
- \begin{tcolorbox}[
388
- enhanced,
389
- colback=ubgcolor,
390
- colframe=maincolor,
391
- arc=5mm,
392
- boxrule=0.5pt,
393
- width=0.8\textwidth
394
- ]
395
- \centering
396
- \large\textit{Document généré avec soin pour une clarté et une pédagogie optimales}
397
- \end{tcolorbox}
398
-
399
- \vspace{1cm}
400
- \end{titlepage}
401
- }
402
-
403
- % Configuration hyperref pour liens colorés
404
- \hypersetup{
405
- colorlinks=true,
406
- linkcolor=maincolor,
407
- filecolor=secondcolor,
408
- urlcolor=thirdcolor,
409
- pdfauthor={},
410
- pdftitle={Correction Mathématiques},
411
- pdfsubject={},
412
- pdfkeywords={}
413
- }
414
- ```
415
-
416
- ## 🔄 STRUCTURE DU DOCUMENT COMPLET
417
-
418
- ```latex
419
- \begin{document}
420
-
421
- % Page de titre élégante
422
- \maketitlepage{Titre de l'Exercice}{Solution Détaillée et Commentée}
423
-
424
- % Espacement après la page de titre
425
- \newpage
426
- \vspace*{1cm}
427
-
428
- % Table des matières distincte et aérée
429
- \begingroup
430
- \setlength{\parskip}{8pt}
431
- \tableofcontents
432
- \endgroup
433
-
434
- \vspace{2cm}
435
- \begin{enoncebox}
436
- [TEXTE COMPLET DE L'ÉNONCÉ]
437
- \end{enoncebox}
438
-
439
- \vspace{1.5cm}
440
-
441
- \section{Première partie de la résolution}
442
- \vspace{0.8cm}
443
-
444
- [SOLUTION DÉTAILLÉE]
445
-
446
- \vspace{1.2cm}
447
- \section{Deuxième partie de la résolution}
448
- \vspace{0.8cm}
449
-
450
- [SUITE DE LA SOLUTION]
451
-
452
- % Et ainsi de suite...
453
-
454
- {Mariam AI}
455
- \end{document}
456
- ```
457
-
458
- ## 💡 INSTRUCTIONS POUR UNE PRÉSENTATION ULTRA-AÉRÉE
459
-
460
- 1. **ESPACES VERTICAUX GÉNÉREUX**
461
- * Utilisez `\vspace{1cm}` fréquemment entre les sections logiques
462
- * Minimum 0.8cm d'espace après chaque titre de section
463
- * Au moins 0.5cm d'espace avant/après chaque environnement mathématique
464
- * Ne lésinez JAMAIS sur les espacements verticaux
465
-
466
- 2. **FORMULATION DE LA SOLUTION**
467
- * Une seule idée par paragraphe, jamais plus
468
- * Espacez généreusement les étapes des raisonnements
469
- * Insérez une ligne vide avant ET après chaque équation ou bloc d'équations
470
- * Utilisez abondamment les environnements thématiques avec leurs espacements inclus
471
-
472
- 3. **MISE EN VALEUR VISUELLE**
473
- * Encadrez chaque résultat principal dans une `resultbox`
474
- * Isolez les définitions et rappels théoriques dans des `definitionbox`
475
- * Utilisez `\boxedeq{}` pour les formules clés qui méritent attention
476
- * Alternez paragraphes textuels courts et expressions mathématiques pour créer du rythme visuel
477
-
478
- ## ⭐ RÉSULTAT FINAL ATTENDU
479
-
480
- Le document final doit:
481
- * Être EXTRÊMEMENT aéré, avec beaucoup plus d'espace blanc que de contenu
482
- * Présenter un équilibre parfait entre texte explicatif et développements mathématiques
483
- * Guider visuellement l'attention grâce aux couleurs et aux encadrements
484
- * Faciliter la compréhension par la décomposition méthodique et l'espacement généreux
485
-
486
- ✅ PRODUISEZ UNIQUEMENT LE CODE LATEX COMPLET, rien d'autre.
487
- """
488
-
489
- def get_prompt_for_style(style):
490
- """Retourne le prompt approprié selon le style."""
491
- if style == 'light':
492
- return get_prompt_light()
493
- else: # 'colorful' par défaut
494
- return get_prompt_colorful()
495
-
496
- # --- MATH SOLVER PIPELINE ---
497
- # La logique du pipeline de résolution est maintenant intégrée ici.
498
-
499
- # Configuration du pipeline
500
- SOLVER_MODEL_NAME = "gemini-2.5-pro"
501
- SOLVER_MAX_ITERATIONS = 5
502
- SOLVER_PASSES_NEEDED = 2
503
- SOLVER_TEMPERATURE = 0.1
504
-
505
- def _get_solver_prompt_initial(problem_statement):
506
- return f"### Core Instructions ###\n* **Rigor is Paramount:** Your primary goal is to produce a complete and rigorously justified solution. Every step must be logically sound.\n* **Honesty About Completeness:** If you cannot find a complete solution, present only significant partial results you can rigorously prove.\n* **Use TeX for All Mathematics:** All mathematical elements must be in TeX (e.g., $n \in \mathbb{{Z}}$).\n\n### Output Format ###\nYour response MUST be structured into these sections:\n**1. Summary**\n* **a. Verdict:** State if the solution is complete or partial.\n* **b. Method Sketch:** A high-level outline of your argument.\n**2. Detailed Solution**\nThe full, step-by-step mathematical proof.\n\n### Self-Correction Instruction ###\nReview your work to ensure it is clean, rigorous, and adheres to all instructions.\n\n### Problem ###\n{problem_statement}"
507
-
508
- def _get_solver_prompt_improve(solution_attempt):
509
- return f"You are a world-class mathematician. Review the following draft solution for flaws, gaps, or clarity issues.\nThen, produce a new, improved, and more rigorous version. Do not comment on the changes, just provide the final, clean proof.\n\n### Draft Solution ###\n{solution_attempt}\n\n### Improved Solution ###"
510
-
511
- def _get_solver_prompt_verifier(problem_statement, solution_to_verify):
512
- return f"You are an expert IMO grader. Your task is to rigorously verify the provided solution. A solution is correct ONLY if every step is justified. Do NOT correct errors, only report them.\n\n### Instructions ###\n1. **Core Instructions:** Find and report all issues.\n2. **Issue Classification:**\n * **a. Critical Error:** An error that breaks the proof's logic. Stop verifying dependant steps.\n * **b. Justification Gap:** A correct but insufficiently justified step. Assume it's true and continue verifying.\n3. **Output Format:**\n * **a. Summary:**\n * **Final Verdict:** A single sentence (e.g., \"The solution is correct.\").\n * **List of Findings:** A bulleted list of every issue found.\n * **b. Detailed Verification Log:** A step-by-step analysis.\n\n---\n### Problem ###\n{problem_statement}\n\n---\n### Solution ###\n{solution_to_verify}\n---\n### Verification Task Reminder ###\nGenerate the summary and the step-by-step verification log."
513
-
514
- def _get_solver_prompt_correction(solution_attempt, verification_report):
515
- return f"You are a brilliant mathematician. Your previous solution has been reviewed.\nYour task is to write a new, corrected version of your solution that meticulously addresses all issues raised in the verifier's report.\n\n### Verification Report on Your Last Attempt ###\n{verification_report}\n\n### Your Previous Flawed Solution ###\n{solution_attempt}\n\n### Your Task ###\nProvide a new, complete, and rigorously correct solution that fixes all identified issues. Follow the original structured output format (Summary and Detailed Solution)."
516
-
517
- def _call_solver_llm(prompt, task_id, step_name):
518
- """Fonction d'appel LLM spécifique pour le pipeline de résolution."""
519
- print(f"Task {task_id}: [Math Solver] - {step_name}...")
520
  try:
521
- response = client.models.generate_content(
522
- model=SOLVER_MODEL_NAME,
523
- contents=[prompt],
524
- generation_config={"temperature": SOLVER_TEMPERATURE}
525
- )
526
- time.sleep(2) # Éviter de surcharger l'API
527
- return response.text
528
  except Exception as e:
529
- print(f"Task {task_id}: An error occurred with the LLM API during '{step_name}': {e}")
530
- return None
531
-
532
- def _parse_verifier_verdict(report):
533
- if not report: return "ERROR"
534
- report_lower = report.lower()
535
- if "the solution is correct" in report_lower: return "CORRECT"
536
- if "critical error" in report_lower: return "CRITICAL_ERROR"
537
- if "justification gap" in report_lower: return "GAPS"
538
- return "UNKNOWN"
539
-
540
- def run_solver_pipeline(problem_statement, task_id, task_results):
541
- """Orchestrateur du pipeline de résolution mathématique."""
542
- # Étape 1: Génération Initiale
543
- task_results[task_id]['status'] = 'solving_generating'
544
- initial_prompt = _get_solver_prompt_initial(problem_statement)
545
- current_solution = _call_solver_llm(initial_prompt, task_id, "Initial Generation")
546
- if not current_solution: return "Failed at initial generation."
547
-
548
- # Étape 2: Auto-Amélioration
549
- task_results[task_id]['status'] = 'solving_improving'
550
- improve_prompt = _get_solver_prompt_improve(current_solution)
551
- current_solution = _call_solver_llm(improve_prompt, task_id, "Self-Improvement")
552
- if not current_solution: return "Failed at self-improvement."
553
-
554
- # Étape 3-5: Boucle de Vérification et Correction
555
- iteration = 0
556
- consecutive_passes = 0
557
- while iteration < SOLVER_MAX_ITERATIONS:
558
- iteration += 1
559
- task_results[task_id]['status'] = f'solving_verifying_iter_{iteration}'
560
- verifier_prompt = _get_solver_prompt_verifier(problem_statement, current_solution)
561
- verification_report = _call_solver_llm(verifier_prompt, task_id, f"Verification (Iter {iteration})")
562
- if not verification_report: break
563
-
564
- verdict = _parse_verifier_verdict(verification_report)
565
- if verdict == "CORRECT":
566
- consecutive_passes += 1
567
- print(f"Task {task_id}: [Math Solver] - PASS! Consecutive: {consecutive_passes}/{SOLVER_PASSES_NEEDED}")
568
- if consecutive_passes >= SOLVER_PASSES_NEEDED:
569
- print(f"Task {task_id}: [Math Solver] - Solution verified. Exiting loop.")
570
- return current_solution
571
- else:
572
- consecutive_passes = 0
573
- task_results[task_id]['status'] = f'solving_correcting_iter_{iteration}'
574
- correction_prompt = _get_solver_prompt_correction(current_solution, verification_report)
575
- new_solution = _call_solver_llm(correction_prompt, task_id, f"Correction (Iter {iteration})")
576
- if not new_solution: break
577
- current_solution = new_solution
578
-
579
- print(f"Task {task_id}: [Math Solver] - Solver finished. Returning last valid solution.")
580
- return current_solution
581
 
 
 
582
 
583
- # --- HELPER FUNCTIONS (LaTeX, Telegram, etc.) ---
584
  def check_latex_installation():
585
- """Vérifie si pdflatex est installé sur le système."""
586
  try:
587
  subprocess.run(["pdflatex", "-version"], capture_output=True, check=True, timeout=10)
588
- print("INFO: pdflatex est installé et accessible.")
589
  return True
590
- except (FileNotFoundError, subprocess.TimeoutExpired, subprocess.CalledProcessError) as e:
591
- print(f"AVERTISSEMENT: pdflatex non installé ou non fonctionnel: {e}")
592
  return False
593
 
594
  IS_LATEX_INSTALLED = check_latex_installation()
595
 
596
  def clean_latex_code(latex_code):
597
- """Removes markdown code block fences (```latex ... ``` or ``` ... ```) if present."""
598
  match_latex = re.search(r"```(?:latex|tex)\s*(.*?)\s*```", latex_code, re.DOTALL | re.IGNORECASE)
599
- if match_latex: return match_latex.group(1).strip()
 
600
  match_generic = re.search(r"```\s*(\\documentclass.*?)\s*```", latex_code, re.DOTALL | re.IGNORECASE)
601
- if match_generic: return match_generic.group(1).strip()
 
602
  return latex_code.strip()
603
 
604
- def latex_to_pdf(latex_code, output_filename_base="document"):
605
- """Converts LaTeX code to PDF."""
606
  if not IS_LATEX_INSTALLED:
607
- return None, "pdflatex n'est pas disponible sur le système."
608
- with tempfile.TemporaryDirectory() as temp_dir_compile:
609
- tex_path = os.path.join(temp_dir_compile, f"{output_filename_base}.tex")
610
- pdf_path_in_compile_dir = os.path.join(temp_dir_compile, f"{output_filename_base}.pdf")
611
- try:
612
- with open(tex_path, "w", encoding="utf-8") as tex_file: tex_file.write(latex_code)
613
- my_env = os.environ.copy()
614
- my_env["LC_ALL"] = "C.UTF-8"
615
- last_result = None
616
- for _ in range(2): # Run twice for references
617
- process = subprocess.run(
618
- ["pdflatex", "-interaction=nonstopmode", "-output-directory", temp_dir_compile, tex_path],
619
- capture_output=True, text=True, check=False, encoding="utf-8", errors="replace", env=my_env
620
- )
621
- last_result = process
622
- if not os.path.exists(pdf_path_in_compile_dir) and process.returncode != 0: break
623
- if os.path.exists(pdf_path_in_compile_dir):
624
- temp_pdf_out_file = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False)
625
- shutil.copy(pdf_path_in_compile_dir, temp_pdf_out_file.name)
626
- return temp_pdf_out_file.name, "PDF généré avec succès."
627
- else:
628
- error_log = last_result.stdout if last_result else "Aucun résultat de compilation."
629
- print(f"Erreur de compilation PDF pour {output_filename_base}:\n{error_log}")
630
- match_error = re.search(r"! LaTeX Error: (.*?)\n", error_log)
631
- if match_error: return None, f"Erreur de compilation PDF: {match_error.group(1).strip()}"
632
- return None, f"Erreur lors de la compilation du PDF. Détails dans les logs du serveur."
633
- except Exception as e:
634
- print(f"Exception inattendue lors de la génération du PDF ({output_filename_base}): {e}")
635
- return None, f"Exception inattendue lors de la génération du PDF: {str(e)}"
636
 
637
- def send_to_telegram(file_data, filename, caption="Nouveau fichier"):
638
- """Envoie un fichier (image ou PDF) à un chat Telegram."""
 
 
639
  try:
640
- if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.webp')):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
641
  url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendPhoto"
642
  files = {'photo': (filename, file_data)}
643
  else:
644
  url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendDocument"
645
  files = {'document': (filename, file_data)}
646
  data = {'chat_id': TELEGRAM_CHAT_ID, 'caption': caption}
647
- response = requests.post(url, files=files, data=data, timeout=30)
648
- if response.status_code == 200:
649
- print(f"Fichier '{filename}' envoyé avec succès à Telegram")
650
- return True
651
- else:
652
- print(f"Erreur envoi Telegram: {response.status_code} - {response.text}")
653
- return False
654
- except Exception as e:
655
- print(f"Exception envoi Telegram: {e}")
656
- return False
657
-
658
- def send_document_to_telegram(content_or_path, filename="reponse.txt", caption="Réponse", is_pdf=False):
659
- """Envoie un document texte ou PDF à Telegram."""
660
- try:
661
- url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendDocument"
662
- data = {'chat_id': TELEGRAM_CHAT_ID, 'caption': caption}
663
- if is_pdf:
664
- with open(content_or_path, 'rb') as f:
665
- files = {'document': (filename, f.read(), 'application/pdf')}
666
- response = requests.post(url, files=files, data=data, timeout=60)
667
- else: # Text content
668
- files = {'document': (filename, content_or_path.encode('utf-8'), 'text/plain')}
669
- response = requests.post(url, files=files, data=data, timeout=60)
670
-
671
- if response.status_code == 200:
672
- print(f"Document '{filename}' envoyé avec succès à Telegram.")
673
- return True
674
- else:
675
- print(f"Erreur envoi document Telegram: {response.status_code} - {response.text}")
676
- return False
677
  except Exception as e:
678
- print(f"Exception envoi document Telegram: {e}")
679
- return False
680
 
681
- # --- BACKGROUND FILE PROCESSING (Main Logic) ---
682
- def process_files_background(task_id, files_data, resolution_style='colorful'):
683
- """Traite les fichiers, applique le pipeline de résolution et génère le PDF final."""
684
- pdf_file_to_clean = None
685
  uploaded_file_refs = []
686
-
687
  try:
688
  task_results[task_id]['status'] = 'processing'
689
- if not client: raise ConnectionError("Client Gemini non initialisé.")
 
690
 
691
- # Préparer le contenu initial pour Gemini (images/PDFs)
692
- initial_contents = []
693
  for file_info in files_data:
694
- file_type = file_info['type']
695
- file_data = file_info['data']
696
- if file_type.startswith('image/'):
697
- img = Image.open(io.BytesIO(file_data))
698
  buffered = io.BytesIO()
699
  img.save(buffered, format="PNG")
700
  img_base64_str = base64.b64encode(buffered.getvalue()).decode()
701
- initial_contents.append({'inline_data': {'mime_type': 'image/png', 'data': img_base64_str}})
702
- elif file_type == 'application/pdf':
703
- with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_pdf:
704
- temp_pdf.write(file_data)
705
- file_ref = client.files.upload(file=temp_pdf.name)
 
 
706
  uploaded_file_refs.append(file_ref)
707
- initial_contents.append(file_ref)
708
- os.unlink(temp_pdf.name)
 
 
709
 
710
- if not initial_contents: raise ValueError("Aucun contenu valide trouvé.")
711
-
712
- full_latex_response = ""
713
- if resolution_style == 'colorful':
714
- # PIPELINE AVANCÉ
715
- task_results[task_id]['status'] = 'extracting_problem'
716
- print(f"Task {task_id}: Étape 1 - Extraction de l'énoncé...")
717
- extraction_response = client.models.generate_content(
718
- model=SOLVER_MODEL_NAME, contents=[*initial_contents, get_prompt_extract_problem()])
719
- problem_statement_text = extraction_response.text
720
- print(f"Task {task_id}: Énoncé extrait: {problem_statement_text[:200]}...")
721
-
722
- print(f"Task {task_id}: Étape 2 - Lancement du pipeline de résolution mathématique...")
723
- rigorous_solution_text = run_solver_pipeline(problem_statement_text, task_id, task_results)
724
- if not rigorous_solution_text: raise ValueError("Le pipeline de résolution n'a pas retourné de solution.")
725
-
726
- task_results[task_id]['status'] = 'designing_pdf'
727
- print(f"Task {task_id}: Étape 3 - Génération du document LaTeX final...")
728
- colorful_prompt_template = get_prompt_for_style('colorful')
729
- final_design_prompt = f"{colorful_prompt_template}\n\n---\n## CONTENU À METTRE EN FORME\n\n### ÉNONCÉ DE L'EXERCICE\n```\n{problem_statement_text}\n```\n\n### SOLUTION RIGIOUREUSE À METTRE EN PAGE\n```\n{rigorous_solution_text}\n```\n\nMaintenant, produis le code source LaTeX complet et uniquement le code."
730
- gemini_response = client.models.generate_content(model=SOLVER_MODEL_NAME, contents=[final_design_prompt])
731
- full_latex_response = gemini_response.text
732
- else:
733
- # PIPELINE SIMPLE (style 'light')
734
- task_results[task_id]['status'] = 'generating_latex'
735
- print(f"Task {task_id}: Génération LaTeX simple (style: {resolution_style})...")
736
- prompt_to_use = get_prompt_for_style(resolution_style)
737
- gemini_response = client.models.generate_content(model=SOLVER_MODEL_NAME, contents=[*initial_contents, prompt_to_use])
738
- full_latex_response = gemini_response.text
739
 
740
- # --- Traitement commun : Compilation PDF et envoi ---
741
- if not full_latex_response.strip(): raise ValueError("Gemini a retourné une réponse vide.")
 
 
 
742
 
 
 
 
743
  task_results[task_id]['status'] = 'cleaning_latex'
744
  cleaned_latex = clean_latex_code(full_latex_response)
745
-
746
- if not IS_LATEX_INSTALLED:
747
- print(f"Task {task_id}: pdflatex non disponible. Envoi du .tex uniquement.")
748
- send_document_to_telegram(cleaned_latex, f"solution_{task_id}.tex", f"Code LaTeX pour tâche {task_id}")
749
- task_results[task_id]['status'] = 'completed_tex_only'
750
- task_results[task_id]['response'] = cleaned_latex
751
- return
752
 
753
  task_results[task_id]['status'] = 'generating_pdf'
754
  pdf_filename_base = f"solution_{task_id}"
755
- pdf_file_to_clean, pdf_message = latex_to_pdf(cleaned_latex, output_filename_base=pdf_filename_base)
756
 
757
- if pdf_file_to_clean:
758
- send_document_to_telegram(pdf_file_to_clean, f"{pdf_filename_base}.pdf", f"Solution PDF pour tâche {task_id}", is_pdf=True)
759
  task_results[task_id]['status'] = 'completed'
760
- task_results[task_id]['response'] = cleaned_latex
 
761
  else:
762
- task_results[task_id]['status'] = 'pdf_error'
763
- task_results[task_id]['error_detail'] = f"Erreur PDF: {pdf_message}"
764
- send_document_to_telegram(cleaned_latex, f"solution_{task_id}.tex", f"Code LaTeX (Erreur PDF: {pdf_message[:150]})")
765
- task_results[task_id]['response'] = cleaned_latex
766
 
767
- except Exception as e_outer:
768
- print(f"Task {task_id}: Exception majeure dans la tâche de fond: {e_outer}")
769
  task_results[task_id]['status'] = 'error'
770
- task_results[task_id]['error'] = f"Erreur système: {str(e_outer)}"
 
771
  finally:
772
- if pdf_file_to_clean and os.path.exists(pdf_file_to_clean):
773
- try:
774
- os.remove(pdf_file_to_clean)
775
- except Exception as e_clean:
776
- print(f"Task {task_id}: Erreur suppression PDF temp: {e_clean}")
777
- # Les références de fichiers Gemini expirent automatiquement
778
 
779
- # --- FLASK ROUTES ---
780
  @app.route('/')
781
  def index():
782
  return render_template('index.html')
783
 
784
- @app.route('/free')
785
- def free():
786
- return render_template('index.html')
787
-
788
  @app.route('/solve', methods=['POST'])
789
  def solve():
790
  try:
791
- if 'user_files' not in request.files: return jsonify({'error': 'Aucun fichier fourni'}), 400
 
 
792
  uploaded_files = request.files.getlist('user_files')
793
- if not uploaded_files or all(f.filename == '' for f in uploaded_files): return jsonify({'error': 'Aucun fichier sélectionné'}), 400
 
794
 
795
  resolution_style = request.form.get('style', 'colorful')
796
  files_data = []
797
- for file in uploaded_files:
798
- if file.filename != '':
799
- file_data = file.read()
800
- file_type = file.content_type or 'application/octet-stream'
801
- if file_type.startswith('image/') or file_type == 'application/pdf':
802
- files_data.append({'filename': file.filename, 'data': file_data, 'type': file_type})
803
- send_to_telegram(file_data, file.filename, f"Mariam(Pro) - Style: {resolution_style}")
804
 
805
- if not files_data: return jsonify({'error': 'Aucun fichier valide (images/PDF acceptés)'}), 400
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
806
 
807
  task_id = str(uuid.uuid4())
808
- task_results[task_id] = {'status': 'pending', 'response': ''}
 
 
 
 
 
 
 
 
809
 
810
  threading.Thread(target=process_files_background, args=(task_id, files_data, resolution_style)).start()
811
 
812
- return jsonify({'task_id': task_id, 'status': 'pending'})
813
 
814
  except Exception as e:
815
- print(f"Exception lors de la création de la tâche: {e}")
816
  return jsonify({'error': f'Erreur serveur: {e}'}), 500
817
 
818
  @app.route('/task/<task_id>', methods=['GET'])
819
  def get_task_status(task_id):
820
- if task_id not in task_results: return jsonify({'error': 'Tâche introuvable'}), 404
821
- task = task_results[task_id]
822
- return jsonify({
823
- 'status': task.get('status'),
824
- 'response': task.get('response'),
825
- 'error': task.get('error'),
826
- 'error_detail': task.get('error_detail')
827
- })
 
828
 
829
  @app.route('/stream/<task_id>', methods=['GET'])
830
  def stream_task_progress(task_id):
831
  def generate():
832
- if task_id not in task_results:
833
- yield f'data: {json.dumps({"error": "Tâche introuvable", "status": "error"})}\n\n'
834
- return
835
-
836
  last_status_sent = None
837
  while True:
838
  task = task_results.get(task_id)
@@ -843,28 +268,38 @@ def stream_task_progress(task_id):
843
  current_status = task['status']
844
  if current_status != last_status_sent:
845
  data_to_send = {"status": current_status}
846
- if current_status in ['completed', 'completed_tex_only', 'pdf_error']:
847
  data_to_send["response"] = task.get("response", "")
848
- if current_status in ['error', 'pdf_error']:
849
- data_to_send["error"] = task.get("error", "Erreur")
850
- if task.get("error_detail"): data_to_send["error_detail"] = task.get("error_detail")
851
-
852
  yield f'data: {json.dumps(data_to_send)}\n\n'
853
  last_status_sent = current_status
854
 
855
- if current_status in ['completed', 'error', 'pdf_error', 'completed_tex_only']:
856
  break
857
 
858
  time.sleep(1)
859
 
860
- return Response(stream_with_context(generate()), mimetype='text/event-stream')
 
 
 
 
 
 
 
 
 
 
 
861
 
862
- # --- MAIN EXECUTION BLOCK ---
863
  if __name__ == '__main__':
 
864
  if not GOOGLE_API_KEY:
865
- print("CRITICAL: GOOGLE_API_KEY non définie.")
866
  if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
867
- print("CRITICAL: Variables Telegram non définies.")
868
 
869
- app.run(debug=True, host='0.0.0.0', port=5000)
870
-
 
1
+ from flask import Flask, render_template, request, jsonify, Response, stream_with_context, send_from_directory
 
 
2
  from google import genai
3
  import os
4
  from google.genai import types
 
15
  import shutil
16
  import re
17
 
 
18
  app = Flask(__name__)
19
 
 
 
20
  GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
21
+ TELEGRAM_BOT_TOKEN = "8004545342:AAGcZaoDjYg8dmbbXRsR1N3TfSSbEiAGz88"
 
22
  TELEGRAM_CHAT_ID = "-1002564204301"
23
+ GENERATED_PDF_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'generated_pdfs')
24
 
 
25
  if GOOGLE_API_KEY:
26
  try:
27
  client = genai.Client(api_key=GOOGLE_API_KEY)
28
  except Exception as e:
29
+ print(f"Erreur client Gemini: {e}")
30
  client = None
31
  else:
32
+ print("GEMINI_API_KEY non trouvé.")
33
  client = None
34
 
 
35
  task_results = {}
36
 
37
+ def load_prompt_from_file(filename):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  try:
39
+ prompts_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'prompts')
40
+ filepath = os.path.join(prompts_dir, filename)
41
+ with open(filepath, 'r', encoding='utf-8') as f:
42
+ return f.read()
 
 
 
43
  except Exception as e:
44
+ print(f"Erreur chargement prompt '{filename}': {e}")
45
+ return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
+ def get_prompt_for_style(style):
48
+ return load_prompt_from_file('prompt_light.txt') if style == 'light' else load_prompt_from_file('prompt_colorful.txt')
49
 
 
50
  def check_latex_installation():
 
51
  try:
52
  subprocess.run(["pdflatex", "-version"], capture_output=True, check=True, timeout=10)
 
53
  return True
54
+ except Exception:
 
55
  return False
56
 
57
  IS_LATEX_INSTALLED = check_latex_installation()
58
 
59
  def clean_latex_code(latex_code):
 
60
  match_latex = re.search(r"```(?:latex|tex)\s*(.*?)\s*```", latex_code, re.DOTALL | re.IGNORECASE)
61
+ if match_latex:
62
+ return match_latex.group(1).strip()
63
  match_generic = re.search(r"```\s*(\\documentclass.*?)\s*```", latex_code, re.DOTALL | re.IGNORECASE)
64
+ if match_generic:
65
+ return match_generic.group(1).strip()
66
  return latex_code.strip()
67
 
68
+ def latex_to_pdf(latex_code, output_filename_base, output_dir):
 
69
  if not IS_LATEX_INSTALLED:
70
+ return None, "pdflatex non disponible."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
+ tex_filename = f"{output_filename_base}.tex"
73
+ tex_path = os.path.join(output_dir, tex_filename)
74
+ pdf_path = os.path.join(output_dir, f"{output_filename_base}.pdf")
75
+
76
  try:
77
+ with open(tex_path, "w", encoding="utf-8") as tex_file:
78
+ tex_file.write(latex_code)
79
+
80
+ my_env = os.environ.copy()
81
+ my_env["LC_ALL"] = "C.UTF-8"
82
+ my_env["LANG"] = "C.UTF-8"
83
+
84
+ last_result = None
85
+ for _ in range(2):
86
+ process = subprocess.run(
87
+ ["pdflatex", "-interaction=nonstopmode", "-output-directory", output_dir, tex_path],
88
+ capture_output=True, text=True, check=False, encoding="utf-8", errors="replace", env=my_env,
89
+ )
90
+ last_result = process
91
+ if not os.path.exists(pdf_path) and process.returncode != 0:
92
+ break
93
+
94
+ if os.path.exists(pdf_path):
95
+ return pdf_path, f"PDF généré: {os.path.basename(pdf_path)}"
96
+ else:
97
+ error_log = last_result.stdout + "\n" + last_result.stderr if last_result else "Aucun résultat de compilation."
98
+ return None, f"Erreur de compilation PDF. Log: ...{error_log[-1000:]}"
99
+ except Exception as e:
100
+ return None, f"Exception génération PDF: {str(e)}"
101
+
102
+ def send_to_telegram(file_data, filename, caption="Nouveau fichier uploadé"):
103
+ try:
104
+ if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp')):
105
  url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendPhoto"
106
  files = {'photo': (filename, file_data)}
107
  else:
108
  url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendDocument"
109
  files = {'document': (filename, file_data)}
110
  data = {'chat_id': TELEGRAM_CHAT_ID, 'caption': caption}
111
+ requests.post(url, files=files, data=data, timeout=30)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  except Exception as e:
113
+ print(f"Erreur envoi Telegram: {e}")
 
114
 
115
+ def process_files_background(task_id, files_data, resolution_style):
 
 
 
116
  uploaded_file_refs = []
 
117
  try:
118
  task_results[task_id]['status'] = 'processing'
119
+ if not client:
120
+ raise ConnectionError("Client Gemini non initialisé.")
121
 
122
+ contents = []
 
123
  for file_info in files_data:
124
+ if file_info['type'].startswith('image/'):
125
+ img = Image.open(io.BytesIO(file_info['data']))
 
 
126
  buffered = io.BytesIO()
127
  img.save(buffered, format="PNG")
128
  img_base64_str = base64.b64encode(buffered.getvalue()).decode()
129
+ contents.append({'inline_data': {'mime_type': 'image/png', 'data': img_base64_str}})
130
+ elif file_info['type'] == 'application/pdf':
131
+ try:
132
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_pdf:
133
+ temp_pdf.write(file_info['data'])
134
+ temp_pdf_path = temp_pdf.name
135
+ file_ref = client.files.upload(file=temp_pdf_path)
136
  uploaded_file_refs.append(file_ref)
137
+ contents.append(file_ref)
138
+ os.unlink(temp_pdf_path)
139
+ except Exception as e:
140
+ raise ValueError(f"Impossible d'uploader le PDF: {str(e)}")
141
 
142
+ if not contents:
143
+ raise ValueError("Aucun contenu valide.")
144
+
145
+ prompt_to_use = get_prompt_for_style(resolution_style)
146
+ if not prompt_to_use:
147
+ raise ValueError(f"Prompt introuvable pour le style '{resolution_style}'.")
148
+ contents.append(prompt_to_use)
149
+
150
+ task_results[task_id]['status'] = 'generating_latex'
151
+ gemini_response = client.models.generate_content(
152
+ model="gemini-2.5-pro",
153
+ contents=contents,
154
+ config=types.GenerateContentConfig(tools=[types.Tool(code_execution=types.ToolCodeExecution)])
155
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
 
157
+ full_latex_response = ""
158
+ if gemini_response.candidates and gemini_response.candidates[0].content and gemini_response.candidates[0].content.parts:
159
+ for part in gemini_response.candidates[0].content.parts:
160
+ if hasattr(part, 'text') and part.text:
161
+ full_latex_response += part.text
162
 
163
+ if not full_latex_response.strip():
164
+ raise ValueError("Gemini a retourné une réponse vide.")
165
+
166
  task_results[task_id]['status'] = 'cleaning_latex'
167
  cleaned_latex = clean_latex_code(full_latex_response)
 
 
 
 
 
 
 
168
 
169
  task_results[task_id]['status'] = 'generating_pdf'
170
  pdf_filename_base = f"solution_{task_id}"
171
+ pdf_file_path, pdf_message = latex_to_pdf(cleaned_latex, pdf_filename_base, GENERATED_PDF_DIR)
172
 
173
+ if pdf_file_path:
 
174
  task_results[task_id]['status'] = 'completed'
175
+ task_results[task_id]['pdf_filename'] = os.path.basename(pdf_file_path)
176
+ task_results[task_id]['response'] = f"PDF généré avec succès: {os.path.basename(pdf_file_path)}"
177
  else:
178
+ raise RuntimeError(f"Échec de la génération PDF: {pdf_message}")
 
 
 
179
 
180
+ except Exception as e:
181
+ print(f"Task {task_id} Erreur: {e}")
182
  task_results[task_id]['status'] = 'error'
183
+ task_results[task_id]['error'] = str(e)
184
+ task_results[task_id]['response'] = f"Erreur: {str(e)}"
185
  finally:
186
+ for file_ref in uploaded_file_refs:
187
+ try: client.files.delete(file_ref)
188
+ except: pass
 
 
 
189
 
 
190
  @app.route('/')
191
  def index():
192
  return render_template('index.html')
193
 
 
 
 
 
194
  @app.route('/solve', methods=['POST'])
195
  def solve():
196
  try:
197
+ if 'user_files' not in request.files:
198
+ return jsonify({'error': 'Aucun fichier fourni'}), 400
199
+
200
  uploaded_files = request.files.getlist('user_files')
201
+ if not uploaded_files or all(f.filename == '' for f in uploaded_files):
202
+ return jsonify({'error': 'Aucun fichier sélectionné'}), 400
203
 
204
  resolution_style = request.form.get('style', 'colorful')
205
  files_data = []
206
+ file_count = {'images': 0, 'pdfs': 0}
 
 
 
 
 
 
207
 
208
+ for file in uploaded_files:
209
+ if not file.filename: continue
210
+ file_data = file.read()
211
+ file_type = file.content_type or 'application/octet-stream'
212
+
213
+ if file_type.startswith('image/'):
214
+ file_count['images'] += 1
215
+ files_data.append({'filename': file.filename, 'data': file_data, 'type': file_type})
216
+ send_to_telegram(file_data, file.filename, f"Image reçue - Style: {resolution_style}")
217
+ elif file_type == 'application/pdf':
218
+ if file_count['pdfs'] >= 1:
219
+ return jsonify({'error': 'Un seul PDF autorisé'}), 400
220
+ file_count['pdfs'] += 1
221
+ files_data.append({'filename': file.filename, 'data': file_data, 'type': file_type})
222
+ send_to_telegram(file_data, file.filename, f"PDF reçu - Style: {resolution_style}")
223
+
224
+ if not files_data:
225
+ return jsonify({'error': 'Aucun fichier valide (image/pdf) trouvé'}), 400
226
 
227
  task_id = str(uuid.uuid4())
228
+ task_results[task_id] = {
229
+ 'status': 'pending',
230
+ 'response': '',
231
+ 'error': None,
232
+ 'time_started': time.time(),
233
+ 'style': resolution_style,
234
+ 'file_count': file_count,
235
+ 'first_filename': files_data[0]['filename']
236
+ }
237
 
238
  threading.Thread(target=process_files_background, args=(task_id, files_data, resolution_style)).start()
239
 
240
+ return jsonify({'task_id': task_id, 'status': 'pending', 'first_filename': files_data[0]['filename']})
241
 
242
  except Exception as e:
243
+ print(f"Erreur /solve: {e}")
244
  return jsonify({'error': f'Erreur serveur: {e}'}), 500
245
 
246
  @app.route('/task/<task_id>', methods=['GET'])
247
  def get_task_status(task_id):
248
+ task = task_results.get(task_id)
249
+ if not task:
250
+ return jsonify({'error': 'Tâche introuvable'}), 404
251
+
252
+ response_data = {'status': task['status'], 'response': task.get('response'), 'error': task.get('error')}
253
+ if task['status'] == 'completed':
254
+ response_data['download_url'] = f"/download/{task_id}"
255
+
256
+ return jsonify(response_data)
257
 
258
  @app.route('/stream/<task_id>', methods=['GET'])
259
  def stream_task_progress(task_id):
260
  def generate():
 
 
 
 
261
  last_status_sent = None
262
  while True:
263
  task = task_results.get(task_id)
 
268
  current_status = task['status']
269
  if current_status != last_status_sent:
270
  data_to_send = {"status": current_status}
271
+ if current_status == 'completed':
272
  data_to_send["response"] = task.get("response", "")
273
+ data_to_send["download_url"] = f"/download/{task_id}"
274
+ elif current_status == 'error':
275
+ data_to_send["error"] = task.get("error", "Erreur inconnue")
276
+
277
  yield f'data: {json.dumps(data_to_send)}\n\n'
278
  last_status_sent = current_status
279
 
280
+ if current_status in ['completed', 'error']:
281
  break
282
 
283
  time.sleep(1)
284
 
285
+ return Response(stream_with_context(generate()), mimetype='text/event-stream', headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
286
+
287
+ @app.route('/download/<task_id>')
288
+ def download_pdf(task_id):
289
+ task = task_results.get(task_id)
290
+ if not task or task['status'] != 'completed' or 'pdf_filename' not in task:
291
+ return "Fichier non trouvé ou non prêt.", 404
292
+
293
+ try:
294
+ return send_from_directory(GENERATED_PDF_DIR, task['pdf_filename'], as_attachment=True)
295
+ except FileNotFoundError:
296
+ return "Fichier introuvable sur le serveur.", 404
297
 
 
298
  if __name__ == '__main__':
299
+ os.makedirs(GENERATED_PDF_DIR, exist_ok=True)
300
  if not GOOGLE_API_KEY:
301
+ print("CRITICAL: GOOGLE_API_KEY non défini.")
302
  if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
303
+ print("CRITICAL: Clés Telegram non définies.")
304
 
305
+ app.run(debug=True, host='0.0.0.0', port=5000)