Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -1,27 +1,20 @@
|
|
1 |
-
from flask import Flask, render_template, request,
|
2 |
from google import genai
|
|
|
3 |
from PIL import Image
|
4 |
import subprocess
|
5 |
import tempfile
|
6 |
import os
|
7 |
import io
|
8 |
-
import shutil
|
9 |
-
import time
|
10 |
-
import uuid
|
11 |
import base64
|
12 |
-
from dotenv import load_dotenv
|
13 |
-
|
14 |
-
# Chargement des variables d'environnement
|
15 |
-
load_dotenv()
|
16 |
-
|
17 |
-
app = Flask(__name__)
|
18 |
|
19 |
# --- Constantes ---
|
20 |
MODEL_SINGLE_GENERATION = "gemini-2.5-pro-exp-03-25" # Modèle avec "thinking" pour la génération complète
|
21 |
LATEX_MENTION = r"\vspace{1cm}\noindent\textit{Ce devoir a été généré par Mariam AI. \url{https://mariam-241.vercel.app}}"
|
22 |
|
23 |
-
|
24 |
|
|
|
25 |
def check_latex_installation():
|
26 |
"""Vérifie si pdflatex est accessible dans le PATH système."""
|
27 |
try:
|
@@ -29,11 +22,13 @@ def check_latex_installation():
|
|
29 |
subprocess.run(['pdflatex', '--version'], capture_output=True, check=True, timeout=10)
|
30 |
return True, "Installation LaTeX (pdflatex) trouvée."
|
31 |
except (subprocess.CalledProcessError, FileNotFoundError, TimeoutError) as e:
|
32 |
-
return False, f"pdflatex introuvable ou ne répond pas. Veuillez installer une distribution LaTeX. Erreur: {e}"
|
33 |
|
34 |
def initialize_genai_client():
|
35 |
"""Initialise et retourne le client Google GenAI."""
|
36 |
try:
|
|
|
|
|
37 |
api_key = os.environ.get("GOOGLE_API_KEY")
|
38 |
if not api_key:
|
39 |
return None, "Clé API Google non trouvée dans les variables d'environnement."
|
@@ -65,9 +60,9 @@ def clean_latex(raw_latex_text):
|
|
65 |
if cleaned.endswith("```"):
|
66 |
cleaned = cleaned[:-3].strip() # Enlève ``` et les espaces potentiels avant
|
67 |
|
68 |
-
#
|
69 |
if not cleaned.startswith("\\documentclass"):
|
70 |
-
|
71 |
|
72 |
# Assure que le document se termine exactement par \end{document}
|
73 |
end_doc_tag = "\\end{document}"
|
@@ -75,13 +70,13 @@ def clean_latex(raw_latex_text):
|
|
75 |
end_idx = cleaned.rfind(end_doc_tag) + len(end_doc_tag)
|
76 |
cleaned = cleaned[:end_idx]
|
77 |
else:
|
78 |
-
app.logger.warning("Le code LaTeX reçu ne contient pas \\end{document}. Tentative d'ajout.")
|
79 |
# Ajoute seulement si \documentclass est présent pour éviter de créer un doc invalide
|
80 |
if cleaned.strip().startswith("\\documentclass"):
|
81 |
cleaned += f"\n{end_doc_tag}"
|
82 |
|
83 |
return cleaned.strip() # Retourne le résultat nettoyé
|
84 |
|
|
|
85 |
def generate_complete_latex(client, image):
|
86 |
"""
|
87 |
Génère une solution complète en LaTeX à partir de l'image en une seule étape,
|
@@ -123,8 +118,6 @@ Agis en tant qu'expert en mathématiques et tuteur pédagogue. Ton objectif est
|
|
123 |
3. **Traduction en LaTeX:** Convertis ta résolution raisonnée en code LaTeX, en appliquant méticuleusement toutes les spécifications de formatage et de style demandées
|
124 |
"""
|
125 |
try:
|
126 |
-
from google.genai import types
|
127 |
-
|
128 |
response = client.models.generate_content(
|
129 |
model=MODEL_SINGLE_GENERATION,
|
130 |
contents=[image, prompt],
|
@@ -148,7 +141,6 @@ Agis en tant qu'expert en mathématiques et tuteur pédagogue. Ton objectif est
|
|
148 |
|
149 |
# Vérification supplémentaire : est-ce que la mention obligatoire est présente ?
|
150 |
if LATEX_MENTION not in latex_content_cleaned:
|
151 |
-
app.logger.warning("La mention obligatoire semble manquer. Tentative d'insertion.")
|
152 |
# Tente de l'insérer avant \end{document}
|
153 |
end_doc_tag = "\\end{document}"
|
154 |
if end_doc_tag in latex_content_cleaned:
|
@@ -157,14 +149,16 @@ Agis en tant qu'expert en mathématiques et tuteur pédagogue. Ton objectif est
|
|
157 |
# Si \end{document} n'est pas là, ajoute les deux à la fin
|
158 |
latex_content_cleaned += f"\n{LATEX_MENTION}\n{end_doc_tag}"
|
159 |
|
160 |
-
return latex_content_cleaned, thinking_content
|
161 |
else:
|
162 |
-
return None, thinking_content
|
163 |
|
|
|
|
|
164 |
except Exception as e:
|
165 |
-
|
166 |
-
return None, None
|
167 |
|
|
|
168 |
def compile_latex_to_pdf(latex_content):
|
169 |
"""
|
170 |
Compile une chaîne de caractères contenant du code LaTeX en fichier PDF.
|
@@ -186,9 +180,9 @@ def compile_latex_to_pdf(latex_content):
|
|
186 |
with open(tex_filepath, "w", encoding="utf-8") as f:
|
187 |
f.write(latex_content)
|
188 |
except IOError as e:
|
189 |
-
return None, f"Erreur lors de l'écriture du fichier .tex temporaire: {e}"
|
190 |
|
191 |
-
#
|
192 |
command = [
|
193 |
"pdflatex",
|
194 |
"-interaction=nonstopmode",
|
@@ -198,31 +192,41 @@ def compile_latex_to_pdf(latex_content):
|
|
198 |
]
|
199 |
|
200 |
pdf_generated = False
|
201 |
-
|
202 |
|
203 |
for pass_num in range(1, 3): # Lance jusqu'à 2 passes
|
|
|
204 |
try:
|
205 |
# Augmentation du timeout pour les compilations potentiellement longues
|
206 |
-
result = subprocess.run(command, capture_output=True, check=True, text=True,
|
|
|
|
|
207 |
# Vérifie si le PDF a été créé après la première passe (au moins)
|
208 |
if os.path.exists(pdf_filepath):
|
209 |
pdf_generated = True
|
210 |
|
211 |
except subprocess.CalledProcessError as e:
|
212 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
213 |
# Essayer de lire le fichier .log pour plus d'infos
|
214 |
try:
|
215 |
with open(log_filepath, "r", encoding="utf-8", errors='replace') as log_file:
|
216 |
-
|
|
|
217 |
except IOError:
|
218 |
-
|
219 |
-
return None, f"{error_msg}\n{log_output}"
|
220 |
|
|
|
|
|
221 |
except subprocess.TimeoutExpired:
|
222 |
-
|
223 |
-
|
224 |
except Exception as e:
|
225 |
-
|
226 |
|
227 |
# Vérifie si le fichier PDF existe après les passes
|
228 |
if pdf_generated and os.path.exists(pdf_filepath):
|
@@ -232,120 +236,112 @@ def compile_latex_to_pdf(latex_content):
|
|
232 |
pdf_data = f.read()
|
233 |
return pdf_data, "PDF généré avec succès !"
|
234 |
except IOError as e:
|
235 |
-
return None, f"Erreur lors de la lecture du fichier PDF généré: {e}"
|
236 |
else:
|
237 |
-
|
|
|
238 |
try:
|
239 |
with open(log_filepath, "r", encoding="utf-8", errors='replace') as log_file:
|
240 |
-
|
|
|
241 |
except IOError:
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
# --- Routes Flask ---
|
246 |
|
247 |
@app.route('/')
|
248 |
def index():
|
249 |
return render_template('index.html')
|
250 |
|
251 |
-
@app.route('/
|
252 |
def check_latex():
|
253 |
-
|
254 |
-
return jsonify({
|
255 |
-
'success': latex_installed,
|
256 |
-
'message': message
|
257 |
-
})
|
258 |
-
|
259 |
-
@app.route('/check_api', methods=['GET'])
|
260 |
-
def check_api():
|
261 |
-
client, message = initialize_genai_client()
|
262 |
return jsonify({
|
263 |
-
|
264 |
-
|
265 |
})
|
266 |
|
267 |
-
@app.route('/
|
268 |
def process_image():
|
269 |
if 'image' not in request.files:
|
270 |
-
return jsonify({
|
271 |
|
272 |
file = request.files['image']
|
273 |
if file.filename == '':
|
274 |
-
return jsonify({
|
275 |
|
276 |
try:
|
277 |
-
#
|
278 |
-
image_data = file.read()
|
279 |
-
image = Image.open(io.BytesIO(image_data))
|
280 |
-
|
281 |
-
# Initialisation du client
|
282 |
client, client_message = initialize_genai_client()
|
283 |
if not client:
|
284 |
-
return jsonify({
|
|
|
|
|
|
|
285 |
|
286 |
# Génération du LaTeX
|
287 |
-
latex_content,
|
|
|
288 |
if not latex_content:
|
289 |
return jsonify({
|
290 |
-
|
291 |
-
|
292 |
-
'thinking': thinking_content
|
293 |
})
|
294 |
|
295 |
# Compilation en PDF
|
296 |
pdf_data, pdf_message = compile_latex_to_pdf(latex_content)
|
|
|
297 |
if not pdf_data:
|
298 |
return jsonify({
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
|
|
303 |
})
|
304 |
|
305 |
-
#
|
306 |
-
|
307 |
-
pdf_path = os.path.join(app.root_path, 'static', 'pdfs')
|
308 |
-
|
309 |
-
# Créer le dossier s'il n'existe pas
|
310 |
-
os.makedirs(pdf_path, exist_ok=True)
|
311 |
-
|
312 |
-
# Sauvegarder le PDF
|
313 |
-
full_pdf_path = os.path.join(pdf_path, f"{pdf_id}.pdf")
|
314 |
-
with open(full_pdf_path, 'wb') as f:
|
315 |
-
f.write(pdf_data)
|
316 |
-
|
317 |
-
# Convertir l'image en base64 pour l'affichage
|
318 |
-
image_base64 = base64.b64encode(image_data).decode('utf-8')
|
319 |
|
|
|
320 |
return jsonify({
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
'image_base64': image_base64
|
327 |
})
|
328 |
|
329 |
except Exception as e:
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
338 |
|
339 |
-
|
|
|
340 |
|
341 |
-
|
342 |
-
|
|
|
|
|
343 |
|
344 |
-
return send_file(
|
|
|
|
|
|
|
|
|
|
|
345 |
|
346 |
if __name__ == '__main__':
|
347 |
-
# Création du dossier pour les PDFs
|
348 |
-
os.makedirs(os.path.join(app.root_path, 'static', 'pdfs'), exist_ok=True)
|
349 |
-
|
350 |
-
# Lancement de l'application
|
351 |
app.run(debug=True)
|
|
|
1 |
+
from flask import Flask, render_template, request, send_file, jsonify
|
2 |
from google import genai
|
3 |
+
from google.genai import types
|
4 |
from PIL import Image
|
5 |
import subprocess
|
6 |
import tempfile
|
7 |
import os
|
8 |
import io
|
|
|
|
|
|
|
9 |
import base64
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
|
11 |
# --- Constantes ---
|
12 |
MODEL_SINGLE_GENERATION = "gemini-2.5-pro-exp-03-25" # Modèle avec "thinking" pour la génération complète
|
13 |
LATEX_MENTION = r"\vspace{1cm}\noindent\textit{Ce devoir a été généré par Mariam AI. \url{https://mariam-241.vercel.app}}"
|
14 |
|
15 |
+
app = Flask(__name__)
|
16 |
|
17 |
+
# --- Fonctions Utilitaires ---
|
18 |
def check_latex_installation():
|
19 |
"""Vérifie si pdflatex est accessible dans le PATH système."""
|
20 |
try:
|
|
|
22 |
subprocess.run(['pdflatex', '--version'], capture_output=True, check=True, timeout=10)
|
23 |
return True, "Installation LaTeX (pdflatex) trouvée."
|
24 |
except (subprocess.CalledProcessError, FileNotFoundError, TimeoutError) as e:
|
25 |
+
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}"
|
26 |
|
27 |
def initialize_genai_client():
|
28 |
"""Initialise et retourne le client Google GenAI."""
|
29 |
try:
|
30 |
+
# Dans un environnement Flask, vous devrez configurer votre API key différemment
|
31 |
+
# Par exemple, en utilisant les variables d'environnement
|
32 |
api_key = os.environ.get("GOOGLE_API_KEY")
|
33 |
if not api_key:
|
34 |
return None, "Clé API Google non trouvée dans les variables d'environnement."
|
|
|
60 |
if cleaned.endswith("```"):
|
61 |
cleaned = cleaned[:-3].strip() # Enlève ``` et les espaces potentiels avant
|
62 |
|
63 |
+
# Assure que le document commence correctement
|
64 |
if not cleaned.startswith("\\documentclass"):
|
65 |
+
pass # On log un avertissement dans l'application web
|
66 |
|
67 |
# Assure que le document se termine exactement par \end{document}
|
68 |
end_doc_tag = "\\end{document}"
|
|
|
70 |
end_idx = cleaned.rfind(end_doc_tag) + len(end_doc_tag)
|
71 |
cleaned = cleaned[:end_idx]
|
72 |
else:
|
|
|
73 |
# Ajoute seulement si \documentclass est présent pour éviter de créer un doc invalide
|
74 |
if cleaned.strip().startswith("\\documentclass"):
|
75 |
cleaned += f"\n{end_doc_tag}"
|
76 |
|
77 |
return cleaned.strip() # Retourne le résultat nettoyé
|
78 |
|
79 |
+
# --- Fonction Principale (API GenAI) ---
|
80 |
def generate_complete_latex(client, image):
|
81 |
"""
|
82 |
Génère une solution complète en LaTeX à partir de l'image en une seule étape,
|
|
|
118 |
3. **Traduction en LaTeX:** Convertis ta résolution raisonnée en code LaTeX, en appliquant méticuleusement toutes les spécifications de formatage et de style demandées
|
119 |
"""
|
120 |
try:
|
|
|
|
|
121 |
response = client.models.generate_content(
|
122 |
model=MODEL_SINGLE_GENERATION,
|
123 |
contents=[image, prompt],
|
|
|
141 |
|
142 |
# Vérification supplémentaire : est-ce que la mention obligatoire est présente ?
|
143 |
if LATEX_MENTION not in latex_content_cleaned:
|
|
|
144 |
# Tente de l'insérer avant \end{document}
|
145 |
end_doc_tag = "\\end{document}"
|
146 |
if end_doc_tag in latex_content_cleaned:
|
|
|
149 |
# Si \end{document} n'est pas là, ajoute les deux à la fin
|
150 |
latex_content_cleaned += f"\n{LATEX_MENTION}\n{end_doc_tag}"
|
151 |
|
152 |
+
return latex_content_cleaned, thinking_content, None
|
153 |
else:
|
154 |
+
return None, thinking_content, "Aucun contenu LaTeX n'a été généré par le modèle."
|
155 |
|
156 |
+
except types.StopCandidateException as e:
|
157 |
+
return None, None, "Génération stoppée (possible contenu inapproprié)"
|
158 |
except Exception as e:
|
159 |
+
return None, None, f"Erreur lors de la génération du LaTeX: {str(e)}"
|
|
|
160 |
|
161 |
+
# --- Fonction de Compilation LaTeX ---
|
162 |
def compile_latex_to_pdf(latex_content):
|
163 |
"""
|
164 |
Compile une chaîne de caractères contenant du code LaTeX en fichier PDF.
|
|
|
180 |
with open(tex_filepath, "w", encoding="utf-8") as f:
|
181 |
f.write(latex_content)
|
182 |
except IOError as e:
|
183 |
+
return None, f"Erreur lors de l'écriture du fichier .tex temporaire: {str(e)}"
|
184 |
|
185 |
+
# Exécute pdflatex
|
186 |
command = [
|
187 |
"pdflatex",
|
188 |
"-interaction=nonstopmode",
|
|
|
192 |
]
|
193 |
|
194 |
pdf_generated = False
|
195 |
+
compile_log = []
|
196 |
|
197 |
for pass_num in range(1, 3): # Lance jusqu'à 2 passes
|
198 |
+
compile_log.append(f"Compilation LaTeX (Passe {pass_num})...")
|
199 |
try:
|
200 |
# Augmentation du timeout pour les compilations potentiellement longues
|
201 |
+
result = subprocess.run(command, capture_output=True, check=True, text=True,
|
202 |
+
encoding='utf-8', errors='replace', timeout=60)
|
203 |
+
compile_log.append(result.stdout)
|
204 |
# Vérifie si le PDF a été créé après la première passe (au moins)
|
205 |
if os.path.exists(pdf_filepath):
|
206 |
pdf_generated = True
|
207 |
|
208 |
except subprocess.CalledProcessError as e:
|
209 |
+
compile_log.append(f"Erreur lors de la compilation LaTeX (Passe {pass_num}).")
|
210 |
+
compile_log.append(f"Code de retour: {e.returncode}")
|
211 |
+
compile_log.append("Sortie de pdflatex (stdout):")
|
212 |
+
compile_log.append(e.stdout or "Aucune sortie standard.")
|
213 |
+
compile_log.append("Erreurs de pdflatex (stderr):")
|
214 |
+
compile_log.append(e.stderr or "Aucune sortie d'erreur.")
|
215 |
+
|
216 |
# Essayer de lire le fichier .log pour plus d'infos
|
217 |
try:
|
218 |
with open(log_filepath, "r", encoding="utf-8", errors='replace') as log_file:
|
219 |
+
compile_log.append("Extrait du fichier log (solution.log):")
|
220 |
+
compile_log.append(log_file.read()[-2000:]) # Affiche les dernières lignes
|
221 |
except IOError:
|
222 |
+
compile_log.append("Impossible de lire le fichier log de LaTeX.")
|
|
|
223 |
|
224 |
+
return None, "\n".join(compile_log) # Arrête la compilation en cas d'erreur
|
225 |
+
|
226 |
except subprocess.TimeoutExpired:
|
227 |
+
return None, "La compilation LaTeX a dépassé le délai imparti. Le document est peut-être trop complexe ou contient une boucle infinie."
|
|
|
228 |
except Exception as e:
|
229 |
+
return None, f"Une erreur inattendue est survenue lors de l'exécution de pdflatex: {str(e)}"
|
230 |
|
231 |
# Vérifie si le fichier PDF existe après les passes
|
232 |
if pdf_generated and os.path.exists(pdf_filepath):
|
|
|
236 |
pdf_data = f.read()
|
237 |
return pdf_data, "PDF généré avec succès !"
|
238 |
except IOError as e:
|
239 |
+
return None, f"Erreur lors de la lecture du fichier PDF généré: {str(e)}"
|
240 |
else:
|
241 |
+
compile_log.append("Le fichier PDF n'a pas été généré après la compilation LaTeX.")
|
242 |
+
# Tenter de lire le log même si CalledProcessError n'a pas été levée
|
243 |
try:
|
244 |
with open(log_filepath, "r", encoding="utf-8", errors='replace') as log_file:
|
245 |
+
compile_log.append("Extrait du fichier log (solution.log) après échec de génération PDF:")
|
246 |
+
compile_log.append(log_file.read()[-2000:])
|
247 |
except IOError:
|
248 |
+
compile_log.append("Impossible de lire le fichier log de LaTeX.")
|
249 |
+
|
250 |
+
return None, "\n".join(compile_log)
|
|
|
251 |
|
252 |
@app.route('/')
|
253 |
def index():
|
254 |
return render_template('index.html')
|
255 |
|
256 |
+
@app.route('/check-latex')
|
257 |
def check_latex():
|
258 |
+
latex_ok, message = check_latex_installation()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
259 |
return jsonify({
|
260 |
+
"success": latex_ok,
|
261 |
+
"message": message
|
262 |
})
|
263 |
|
264 |
+
@app.route('/process-image', methods=['POST'])
|
265 |
def process_image():
|
266 |
if 'image' not in request.files:
|
267 |
+
return jsonify({"success": False, "message": "Aucune image téléchargée"})
|
268 |
|
269 |
file = request.files['image']
|
270 |
if file.filename == '':
|
271 |
+
return jsonify({"success": False, "message": "Aucun fichier sélectionné"})
|
272 |
|
273 |
try:
|
274 |
+
# Initialisation du client GenAI
|
|
|
|
|
|
|
|
|
275 |
client, client_message = initialize_genai_client()
|
276 |
if not client:
|
277 |
+
return jsonify({"success": False, "message": client_message})
|
278 |
+
|
279 |
+
# Traitement de l'image
|
280 |
+
image = Image.open(file.stream)
|
281 |
|
282 |
# Génération du LaTeX
|
283 |
+
latex_content, thinking_process, error_message = generate_complete_latex(client, image)
|
284 |
+
|
285 |
if not latex_content:
|
286 |
return jsonify({
|
287 |
+
"success": False,
|
288 |
+
"message": error_message or "Échec de la génération de la solution LaTeX"
|
|
|
289 |
})
|
290 |
|
291 |
# Compilation en PDF
|
292 |
pdf_data, pdf_message = compile_latex_to_pdf(latex_content)
|
293 |
+
|
294 |
if not pdf_data:
|
295 |
return jsonify({
|
296 |
+
"success": False,
|
297 |
+
"message": "Échec de la compilation PDF",
|
298 |
+
"latex": latex_content,
|
299 |
+
"thinking": thinking_process,
|
300 |
+
"compilation_log": pdf_message
|
301 |
})
|
302 |
|
303 |
+
# Convertir le PDF en base64 pour l'affichage dans le navigateur
|
304 |
+
pdf_base64 = base64.b64encode(pdf_data).decode('utf-8')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
305 |
|
306 |
+
# Préparer les données de réponse
|
307 |
return jsonify({
|
308 |
+
"success": True,
|
309 |
+
"message": "PDF généré avec succès",
|
310 |
+
"latex": latex_content,
|
311 |
+
"thinking": thinking_process,
|
312 |
+
"pdf_base64": pdf_base64
|
|
|
313 |
})
|
314 |
|
315 |
except Exception as e:
|
316 |
+
return jsonify({
|
317 |
+
"success": False,
|
318 |
+
"message": f"Erreur lors du traitement: {str(e)}"
|
319 |
+
})
|
320 |
+
|
321 |
+
@app.route('/download-pdf', methods=['POST'])
|
322 |
+
def download_pdf():
|
323 |
+
# Récupérer le contenu LaTeX
|
324 |
+
latex_content = request.form.get('latex')
|
325 |
+
if not latex_content:
|
326 |
+
return jsonify({"success": False, "message": "Aucun contenu LaTeX fourni"})
|
327 |
+
|
328 |
+
# Compiler en PDF
|
329 |
+
pdf_data, pdf_message = compile_latex_to_pdf(latex_content)
|
330 |
|
331 |
+
if not pdf_data:
|
332 |
+
return jsonify({"success": False, "message": pdf_message})
|
333 |
|
334 |
+
# Créer un fichier temporaire pour le téléchargement
|
335 |
+
temp_pdf = tempfile.NamedTemporaryFile(delete=False)
|
336 |
+
temp_pdf.write(pdf_data)
|
337 |
+
temp_pdf.close()
|
338 |
|
339 |
+
return send_file(
|
340 |
+
temp_pdf.name,
|
341 |
+
as_attachment=True,
|
342 |
+
download_name="solution_mariam_ai.pdf",
|
343 |
+
mimetype="application/pdf"
|
344 |
+
)
|
345 |
|
346 |
if __name__ == '__main__':
|
|
|
|
|
|
|
|
|
347 |
app.run(debug=True)
|