Docfile commited on
Commit
984f26b
·
verified ·
1 Parent(s): 1d151d8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +345 -282
app.py CHANGED
@@ -1,171 +1,194 @@
1
  import os
2
  import json
3
  import mimetypes
 
 
4
  from flask import Flask, request, session, jsonify, redirect, url_for, flash, render_template
5
  from dotenv import load_dotenv
6
  import google.generativeai as genai
 
7
  import requests
8
  from werkzeug.utils import secure_filename
9
- import markdown # Pour convertir la réponse en HTML
10
 
11
  # --- Configuration Initiale ---
12
- load_dotenv()
13
 
14
  app = Flask(__name__)
15
-
16
- # Clé secrète FORTEMENT recommandée pour les sessions
17
- app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'dev-secret-key-replace-in-prod')
18
 
19
  # Configuration pour les uploads
20
  UPLOAD_FOLDER = 'temp'
21
- ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg'} # Extensions autorisées
 
 
22
  app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
23
- app.config['MAX_CONTENT_LENGTH'] = 25 * 1024 * 1024 # Limite de taille (ex: 25MB)
 
24
 
25
- # Créer le dossier temp s'il n'existe pas
26
  os.makedirs(UPLOAD_FOLDER, exist_ok=True)
27
  print(f"Dossier d'upload configuré : {os.path.abspath(UPLOAD_FOLDER)}")
28
 
29
  # --- Configuration de l'API Gemini ---
30
- MODEL_FLASH = 'gemini-2.0-flash' # Default model
31
- MODEL_PRO = 'gemini-2.5-pro-exp-03-25' # Advanced model
32
- SYSTEM_INSTRUCTION = "Tu es un assistant intelligent et amical nommé Mariam. Tu assistes les utilisateurs au mieux de tes capacités. Tu as été créé par Aenir."
33
- SAFETY_SETTINGS = [
34
- {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
35
- {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
36
- {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
37
- {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
38
- ]
 
 
 
39
  GEMINI_CONFIGURED = False
 
 
40
  try:
41
  gemini_api_key = os.getenv("GOOGLE_API_KEY")
42
  if not gemini_api_key:
43
- print("ERREUR: Clé API GOOGLE_API_KEY manquante dans le fichier .env")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  else:
45
- genai.configure(api_key=gemini_api_key)
46
- # Just configure, don't create model instance yet
47
- # Check if we can list models as a basic configuration test
48
- models_list = [m.name for m in genai.list_models()]
49
- if f'models/{MODEL_FLASH}' in models_list and f'models/{MODEL_PRO}' in models_list:
50
- print(f"Configuration Gemini effectuée. Modèles requis ({MODEL_FLASH}, {MODEL_PRO}) disponibles.")
51
- print(f"System instruction: {SYSTEM_INSTRUCTION}")
52
- GEMINI_CONFIGURED = True
53
- else:
54
- print(f"ERREUR: Les modèles requis ({MODEL_FLASH}, {MODEL_PRO}) ne sont pas tous disponibles via l'API.")
55
- print(f"Modèles trouvés: {models_list}")
56
 
57
  except Exception as e:
58
  print(f"ERREUR Critique lors de la configuration initiale de Gemini : {e}")
59
  print("L'application fonctionnera sans les fonctionnalités IA.")
 
60
 
61
  # --- Fonctions Utilitaires ---
62
 
 
 
 
 
63
  def allowed_file(filename):
64
- """Vérifie si l'extension du fichier est autorisée."""
65
- return '.' in filename and \
66
- filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  def perform_web_search(query):
69
- """Effectue une recherche web via l'API Serper."""
70
  serper_api_key = os.getenv("SERPER_API_KEY")
71
  if not serper_api_key:
72
  print("AVERTISSEMENT: Clé API SERPER_API_KEY manquante. Recherche web désactivée.")
73
  return None
74
-
75
- search_url = "https://google.serper.dev/search"
76
- headers = {
77
- 'X-API-KEY': serper_api_key,
78
- 'Content-Type': 'application/json'
79
- }
80
- payload = json.dumps({"q": query, "gl": "fr", "hl": "fr"}) # Ajout localisation FR
81
-
82
- try:
83
- print(f"Recherche Serper pour: '{query}'")
84
- response = requests.post(search_url, headers=headers, data=payload, timeout=10)
85
- response.raise_for_status() # Lève une exception pour les erreurs HTTP (4xx, 5xx)
86
- data = response.json()
87
- print("Résultats de recherche Serper obtenus.")
88
- # print(json.dumps(data, indent=2)) # Décommenter pour voir les résultats bruts
89
- return data
90
- except requests.exceptions.Timeout:
91
- print("Erreur lors de la recherche web : Timeout")
92
- return None
93
- except requests.exceptions.RequestException as e:
94
- print(f"Erreur lors de la recherche web : {e}")
95
- # Essayer de lire le corps de la réponse d'erreur si possible
96
- try:
97
- error_details = e.response.json()
98
- print(f"Détails de l'erreur Serper: {error_details}")
99
- except:
100
- pass # Ignorer si le corps n'est pas JSON ou n'existe pas
101
- return None
102
- except json.JSONDecodeError as e:
103
- print(f"Erreur lors du décodage de la réponse JSON de Serper : {e}")
104
- print(f"Réponse reçue (texte brut) : {response.text}")
105
- return None
106
 
107
  def format_search_results(data):
108
- """Met en forme les résultats de recherche (format Markdown)."""
109
- if not data:
110
- return "Aucun résultat de recherche web trouvé pertinent."
111
-
112
- results = []
113
-
114
- # Réponse directe (Answer Box)
115
- if data.get('answerBox'):
116
- ab = data['answerBox']
117
- title = ab.get('title', '')
118
- snippet = ab.get('snippet') or ab.get('answer', '')
119
- if snippet:
120
- results.append(f"**Réponse rapide : {title}**\n{snippet}\n")
121
-
122
- # Knowledge Graph
123
- if data.get('knowledgeGraph'):
124
- kg = data['knowledgeGraph']
125
- title = kg.get('title', '')
126
- type = kg.get('type', '')
127
- description = kg.get('description', '')
128
- if title and description:
129
- results.append(f"**{title} ({type})**\n{description}\n")
130
- if kg.get('attributes'):
131
- for attr, value in kg['attributes'].items():
132
- results.append(f"- {attr}: {value}")
133
-
134
-
135
- # Résultats organiques
136
  if data.get('organic'):
137
- results.append("**Pages web pertinentes :**")
138
- for i, item in enumerate(data['organic'][:3], 1): # Top 3
139
- title = item.get('title', 'Sans titre')
140
- link = item.get('link', '#')
141
- snippet = item.get('snippet', 'Pas de description.')
142
- results.append(f"{i}. **[{title}]({link})**\n {snippet}\n")
143
-
144
- # People Also Ask
145
- if data.get('peopleAlsoAsk'):
146
- results.append("**Questions liées :**")
147
- for i, item in enumerate(data['peopleAlsoAsk'][:2], 1): # Top 2
148
- results.append(f"- {item.get('question', '')}")
149
-
150
-
151
- if not results:
152
- return "Aucun résultat structuré trouvé dans la recherche web."
153
-
154
  return "\n".join(results)
155
 
 
 
156
  def prepare_gemini_history(chat_history):
157
- """Convertit l'historique stocké en session au format attendu par Gemini API."""
158
  gemini_history = []
159
  for message in chat_history:
 
160
  role = 'user' if message['role'] == 'user' else 'model'
161
- # Utiliser le 'raw_text' stocké pour Gemini
162
- text_part = message.get('raw_text', '') # Fallback au cas
163
- parts = [text_part]
164
- # NOTE: La gestion des fichiers des tours PRÉCÉDENTS n'est pas gérée ici.
165
- # L'API generate_content se concentre généralement sur le fichier du tour ACTUEL.
166
- # Si une référence de fichier passée était nécessaire, il faudrait la stocker
167
- # et la ré-attacher ici (potentiellement plus complexe).
168
- gemini_history.append({'role': role, 'parts': parts})
169
  return gemini_history
170
 
171
  # --- Routes Flask ---
@@ -177,185 +200,223 @@ def root():
177
 
178
  @app.route('/api/history', methods=['GET'])
179
  def get_history():
180
- """Fournit l'historique de chat stocké en session au format JSON."""
181
  if 'chat_history' not in session:
182
  session['chat_history'] = []
183
 
184
- # Préparer l'historique pour l'affichage (contient déjà le HTML pour l'assistant)
185
  display_history = [
186
  {'role': msg.get('role', 'unknown'), 'text': msg.get('text', '')}
187
  for msg in session.get('chat_history', [])
188
  ]
189
- print(f"API: Récupération de l'historique ({len(display_history)} messages)")
190
  return jsonify({'success': True, 'history': display_history})
191
 
192
  @app.route('/api/chat', methods=['POST'])
193
  def chat_api():
194
- """Gère les nouvelles requêtes de chat via AJAX."""
195
- if not GEMINI_CONFIGURED:
 
196
  print("API ERREUR: Tentative d'appel à /api/chat sans configuration Gemini valide.")
197
- return jsonify({'success': False, 'error': "Le service IA n'est pas configuré correctement."}), 503 # Service Unavailable
198
 
199
- # Récupération des données du formulaire
200
  prompt = request.form.get('prompt', '').strip()
201
- use_web_search_str = request.form.get('web_search', 'false') # 'true' ou 'false'
202
- use_web_search = use_web_search_str.lower() == 'true'
203
- file = request.files.get('file')
204
- use_advanced_str = request.form.get('advanced_reasoning', 'false') # Get the new flag
205
- use_advanced = use_advanced_str.lower() == 'true'
206
 
207
- # Validation simple
208
- if not prompt and not file:
209
- return jsonify({'success': False, 'error': 'Veuillez fournir un message ou un fichier.'}), 400
210
 
211
  print(f"\n--- Nouvelle requête /api/chat ---")
212
- print(f"Prompt reçu: '{prompt[:50]}...'")
213
- print(f"Recherche Web activée: {use_web_search}")
214
- print(f"Raisonnement avancé demandé: {use_advanced}")
215
- print(f"Fichier reçu: {file.filename if file else 'Aucun'}")
216
 
217
- # Initialiser l'historique de session si nécessaire
218
  if 'chat_history' not in session:
219
  session['chat_history'] = []
220
 
221
- uploaded_gemini_file = None # L'objet fichier retourné par genai.upload_file
222
- uploaded_filename = None # Juste le nom du fichier pour référence/affichage
223
- filepath_to_delete = None # Chemin du fichier local à supprimer après traitement
 
 
 
224
 
225
- # --- Gestion de l'upload de fichier ---
226
- if file and file.filename != '':
227
- if allowed_file(file.filename):
228
- try:
229
- filename = secure_filename(file.filename)
230
- filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
231
- file.save(filepath)
232
- filepath_to_delete = filepath # Marquer pour suppression
233
- uploaded_filename = filename
234
- print(f"Fichier '{filename}' sauvegardé temporairement dans '{filepath}'")
235
-
236
- # Détecter le MimeType pour Gemini
237
- mime_type = mimetypes.guess_type(filepath)[0]
238
- if not mime_type:
239
- mime_type = 'application/octet-stream' # Fallback
240
- print(f"AVERTISSEMENT: Impossible de deviner le MimeType pour '{filename}', utilisation de '{mime_type}'.")
241
-
242
- # Uploader vers Google AI (peut prendre du temps)
243
- print(f"Upload du fichier vers Google AI (MimeType: {mime_type})...")
244
- # Note: L'API upload_file est générique et ne dépend pas du modèle Flash/Pro
245
- uploaded_gemini_file = genai.upload_file(path=filepath, mime_type=mime_type)
246
- print(f"Fichier '{uploaded_gemini_file.name}' uploadé avec succès vers Google AI.")
247
-
248
- except Exception as e:
249
- print(f"ERREUR Critique lors du traitement/upload du fichier '{filename}': {e}")
250
- # Supprimer le fichier local même en cas d'erreur d'upload Gemini
251
- if filepath_to_delete and os.path.exists(filepath_to_delete):
252
- try:
253
- os.remove(filepath_to_delete)
254
- print(f"Fichier temporaire '{filepath_to_delete}' supprimé après erreur.")
255
- except OSError as del_e:
256
- print(f"Erreur lors de la suppression du fichier temporaire après erreur: {del_e}")
257
- # Renvoyer une erreur claire au client
258
- return jsonify({'success': False, 'error': f"Erreur lors du traitement du fichier: {e}"}), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  else:
260
- print(f"ERREUR: Type de fichier non autorisé: {file.filename}")
261
- return jsonify({'success': False, 'error': f"Type de fichier non autorisé. Extensions permises: {', '.join(ALLOWED_EXTENSIONS)}"}), 400
262
-
263
- # --- Préparation du message utilisateur pour l'historique et Gemini ---
264
- # Texte brut pour Gemini (et pour l'historique interne)
265
- raw_user_text = prompt
266
- # Texte pour l'affichage dans l'interface (peut inclure le nom de fichier)
267
- display_user_text = f"[{uploaded_filename}] {prompt}" if uploaded_filename and prompt else (prompt or f"[{uploaded_filename}]")
268
-
269
- # Ajout à l'historique de session
270
- user_history_entry = {
271
- 'role': 'user',
272
- 'text': display_user_text, # Pour get_history et potentiellement debug
273
- 'raw_text': raw_user_text, # Pour l'envoi à Gemini via prepare_gemini_history
274
- # On ne stocke PAS l'objet 'uploaded_gemini_file' dans la session
275
- }
276
- session['chat_history'].append(user_history_entry)
277
- session.modified = True # Indiquer que la session a été modifiée
278
-
279
- # --- Préparation des 'parts' pour l'appel Gemini ACTUEL ---
280
- current_gemini_parts = []
281
- if uploaded_gemini_file:
282
- current_gemini_parts.append(uploaded_gemini_file) # L'objet fichier uploadé
283
-
284
- final_prompt_for_gemini = raw_user_text # Commencer avec le texte brut
285
-
286
- # --- Recherche Web (si activée et si un prompt textuel existe) ---
287
- if use_web_search and raw_user_text:
288
- print("Activation de la recherche web...")
289
- search_data = perform_web_search(raw_user_text)
290
- if search_data:
291
- formatted_results = format_search_results(search_data)
292
- # Construire un prompt enrichi pour Gemini
293
- final_prompt_for_gemini = f"""Voici la question originale de l'utilisateur:
294
- "{raw_user_text}"
295
-
296
- J'ai effectué une recherche web et voici les informations pertinentes trouvées:
 
 
 
 
 
 
 
297
  --- DEBUT RESULTATS WEB ---
298
  {formatted_results}
299
  --- FIN RESULTATS WEB ---
300
 
301
- En te basant sur ces informations ET sur ta connaissance générale, fournis une réponse complète et bien structurée à la question originale de l'utilisateur."""
302
- print("Prompt enrichi avec les résultats de recherche web.")
303
- else:
304
- print("Aucun résultat de recherche web pertinent trouvé ou erreur, utilisation du prompt original.")
305
- # final_prompt_for_gemini reste raw_user_text
306
 
307
- # Ajouter le texte (potentiellement enrichi) aux parts pour Gemini
308
- current_gemini_parts.append(final_prompt_for_gemini)
 
309
 
310
- # --- Appel à l'API Gemini ---
311
- try:
312
- # Préparer l'historique des messages PRÉCÉDENTS
 
 
 
 
 
313
  gemini_history = prepare_gemini_history(session['chat_history'][:-1]) # Exclut le message actuel
314
- print(f"Préparation de l'appel Gemini avec {len(gemini_history)} messages d'historique.")
315
- # Construire le contenu complet pour l'appel
316
  contents_for_gemini = gemini_history + [{'role': 'user', 'parts': current_gemini_parts}]
317
 
318
- # Choisir le nom du modèle à utiliser
319
- selected_model_name = MODEL_PRO if use_advanced else MODEL_FLASH
320
- print(f"Utilisation du modèle Gemini: {selected_model_name}")
321
 
322
- # Créer l'instance du modèle spécifique pour cette requête
323
- # Réutiliser les paramètres globaux (safety, system instruction)
324
  active_model = genai.GenerativeModel(
325
  model_name=selected_model_name,
326
- safety_settings=SAFETY_SETTINGS, # defined globally
327
- system_instruction=SYSTEM_INSTRUCTION # defined globally
328
  )
329
 
330
- # Appel API
331
- print(f"Envoi de la requête à {selected_model_name}...")
332
- # Utilisation de generate_content en mode non-streamé
333
  response = active_model.generate_content(contents_for_gemini)
334
- # print(response) # Décommenter pour voir la réponse brute de l'API
335
 
336
- # Extraire le texte de la réponse (gestion d'erreur potentielle ici si la réponse est bloquée etc.)
337
- # Gérer le cas où la réponse est bloquée par les safety settings
 
338
  try:
 
339
  response_text_raw = response.text
340
- except ValueError:
341
- # Si response.text échoue, la réponse a probablement été bloquée.
342
- print("ERREUR: La réponse de Gemini a été bloquée (probablement par les safety settings).")
343
- print(f"Détails du blocage : {response.prompt_feedback}")
344
- # Vous pouvez décider quoi renvoyer au client ici.
345
- # Soit une erreur spécifique, soit un message générique.
346
  response_text_raw = "Désolé, ma réponse a été bloquée car elle pourrait enfreindre les règles de sécurité."
347
- # Convertir ce message d'erreur en HTML aussi pour la cohérence
348
- response_html = markdown.markdown(response_text_raw)
 
 
349
 
350
- else:
351
- # Si response.text réussit, continuer normalement
352
- print(f"Réponse reçue de Gemini (brute, début): '{response_text_raw[:100]}...'")
353
- # Convertir la réponse Markdown en HTML pour l'affichage
354
- response_html = markdown.markdown(response_text_raw, extensions=['fenced_code', 'tables', 'nl2br'])
355
- print("Réponse convertie en HTML.")
356
 
357
-
358
- # Ajouter la réponse de l'assistant à l'historique de session
359
  assistant_history_entry = {
360
  'role': 'assistant',
361
  'text': response_html, # HTML pour l'affichage via get_history
@@ -364,61 +425,63 @@ En te basant sur ces informations ET sur ta connaissance générale, fournis une
364
  session['chat_history'].append(assistant_history_entry)
365
  session.modified = True
366
 
367
- # Renvoyer la réponse HTML au frontend
368
- print("Envoi de la réponse HTML au client.")
369
  return jsonify({'success': True, 'message': response_html})
370
 
371
- except Exception as e:
372
- print(f"ERREUR Critique lors de l'appel à Gemini ou du traitement de la réponse : {e}")
373
- # En cas d'erreur, retirer le dernier message utilisateur de l'historique
374
- # pour éviter les boucles d'erreur si le message lui-même pose problème.
375
- # Vérifier si l'historique n'est pas vide avant de pop
376
  if session.get('chat_history'):
377
  session['chat_history'].pop()
378
  session.modified = True
379
- print("Le dernier message utilisateur a été retiré de l'historique suite à l'erreur.")
380
- else:
381
- print("L'historique était déjà vide lors de l'erreur.")
382
 
383
- # Renvoyer une erreur générique mais informative
384
- return jsonify({'success': False, 'error': f"Une erreur interne est survenue lors de la génération de la réponse. Détails: {e}"}), 500
 
 
 
 
 
 
 
385
 
 
386
  finally:
387
- # --- Nettoyage du fichier temporaire ---
388
  if filepath_to_delete and os.path.exists(filepath_to_delete):
389
  try:
390
  os.remove(filepath_to_delete)
391
- print(f"Fichier temporaire '{filepath_to_delete}' supprimé avec succès.")
392
  except OSError as e:
393
- print(f"ERREUR lors de la suppression du fichier temporaire '{filepath_to_delete}': {e}")
394
 
395
 
396
  @app.route('/clear', methods=['POST'])
397
  def clear_chat():
398
  """Efface l'historique de chat dans la session."""
399
- session.pop('chat_history', None)
400
- # session.pop('web_search', None) # On ne stocke pas ça en session
401
  print("API: Historique de chat effacé via /clear.")
402
 
403
- # Adapter la réponse selon si c'est une requête AJAX (fetch) ou une soumission classique
404
- # Vérification si la requête vient probablement de fetch (simple)
405
- is_ajax = 'XMLHttpRequest' == request.headers.get('X-Requested-With') or \
406
- 'application/json' in request.headers.get('Accept', '') # Plus robuste
407
 
408
  if is_ajax:
409
  return jsonify({'success': True, 'message': 'Historique effacé.'})
410
  else:
411
- # Comportement pour une soumission de formulaire classique (si jamais utilisé)
412
  flash("Conversation effacée.", "info")
413
- return redirect(url_for('root')) # Redirige vers la racine
414
 
415
 
416
- # --- Démarrage de l'application ---
417
  if __name__ == '__main__':
418
  print("Démarrage du serveur Flask...")
419
- # Utiliser host='0.0.0.0' pour rendre accessible sur le réseau local
420
- # debug=True est pratique pour le développement, mais à désactiver en production !
421
- # Changer le port si nécessaire (ex: 5000, 5001, 8080)
422
- # Utiliser un port différent si le port 5000 est déjà pris
423
  port = int(os.environ.get('PORT', 5001))
 
 
424
  app.run(debug=True, host='0.0.0.0', port=port)
 
1
  import os
2
  import json
3
  import mimetypes
4
+ import time
5
+ import re # Pour la détection d'URL YouTube
6
  from flask import Flask, request, session, jsonify, redirect, url_for, flash, render_template
7
  from dotenv import load_dotenv
8
  import google.generativeai as genai
9
+ import google.generativeai.types as genai_types # Important pour FileData, Part, etc.
10
  import requests
11
  from werkzeug.utils import secure_filename
12
+ import markdown # Pour convertir la réponse Markdown en HTML
13
 
14
  # --- Configuration Initiale ---
15
+ load_dotenv() # Charge les variables depuis .env
16
 
17
  app = Flask(__name__)
18
+ # Clé secrète FORTEMENT recommandée pour la sécurité des sessions
19
+ app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'une-cle-secrete-tres-difficile-a-deviner')
 
20
 
21
  # Configuration pour les uploads
22
  UPLOAD_FOLDER = 'temp'
23
+ # Extensions autorisées (incluant vidéo)
24
+ ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'mp4', 'mov', 'avi', 'mkv', 'webm'}
25
+ VIDEO_EXTENSIONS = {'mp4', 'mov', 'avi', 'mkv', 'webm'} # Pour identifier les vidéos
26
  app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
27
+ # Augmenter la limite pour les vidéos (ex: 100MB) - Ajustez si nécessaire
28
+ app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024
29
 
30
+ # Crée le dossier temporaire s'il n'existe pas
31
  os.makedirs(UPLOAD_FOLDER, exist_ok=True)
32
  print(f"Dossier d'upload configuré : {os.path.abspath(UPLOAD_FOLDER)}")
33
 
34
  # --- Configuration de l'API Gemini ---
35
+ # Utilisez les noms de modèles les plus récents auxquels vous avez accès
36
+ MODEL_FLASH = 'gemini-1.5-flash-latest'
37
+ MODEL_PRO = 'gemini-1.5-pro-latest' # Pro est souvent nécessaire/meilleur pour la vidéo
38
+
39
+ # Instruction système pour le modèle
40
+ SYSTEM_INSTRUCTION = "Tu es un assistant intelligent et amical nommé Mariam. Tu assistes les utilisateurs au mieux de tes capacités, y compris dans l'analyse de texte, d'images et de vidéos (via upload ou lien YouTube). Tu as été créé par Aenir."
41
+
42
+ # Paramètres de sécurité (ajuster si nécessaire, BLOCK_NONE est très permissif)
43
+ SAFETY_SETTINGS = [ {"category": c, "threshold": "BLOCK_NONE"} for c in [
44
+ "HARM_CATEGORY_HARASSMENT", "HARM_CATEGORY_HATE_SPEECH",
45
+ "HARM_CATEGORY_SEXUALLY_EXPLICIT", "HARM_CATEGORY_DANGEROUS_CONTENT"]]
46
+
47
  GEMINI_CONFIGURED = False
48
+ gemini_client = None # Client API pour les opérations sur les fichiers (upload vidéo)
49
+
50
  try:
51
  gemini_api_key = os.getenv("GOOGLE_API_KEY")
52
  if not gemini_api_key:
53
+ raise ValueError("Clé API GOOGLE_API_KEY manquante dans le fichier .env")
54
+
55
+ # Initialise le client pour les opérations sur les fichiers (upload vidéo avec polling)
56
+ gemini_client = genai.Client(api_key=gemini_api_key)
57
+ # Configure également l'espace de noms global pour GenerativeModel, etc.
58
+ genai.configure(api_key=gemini_api_key)
59
+
60
+ # Vérifie si les modèles requis sont disponibles
61
+ print("Vérification des modèles Gemini disponibles...")
62
+ models_list = [m.name for m in genai.list_models()]
63
+ required_models = [f'models/{MODEL_FLASH}', f'models/{MODEL_PRO}']
64
+ if all(model in models_list for model in required_models):
65
+ print(f"Configuration Gemini effectuée. Modèles requis trouvés.")
66
+ print(f"Instruction Système: {SYSTEM_INSTRUCTION}")
67
+ GEMINI_CONFIGURED = True
68
  else:
69
+ missing = [model for model in required_models if model not in models_list]
70
+ raise ValueError(f"Les modèles Gemini requis suivants sont manquants: {missing}")
 
 
 
 
 
 
 
 
 
71
 
72
  except Exception as e:
73
  print(f"ERREUR Critique lors de la configuration initiale de Gemini : {e}")
74
  print("L'application fonctionnera sans les fonctionnalités IA.")
75
+ gemini_client = None # S'assure que le client est None si la config échoue
76
 
77
  # --- Fonctions Utilitaires ---
78
 
79
+ def get_file_extension(filename):
80
+ """Retourne l'extension du fichier en minuscules, ou None."""
81
+ return filename.rsplit('.', 1)[1].lower() if '.' in filename else None
82
+
83
  def allowed_file(filename):
84
+ """Vérifie si l'extension du fichier est dans la liste autorisée."""
85
+ return get_file_extension(filename) in ALLOWED_EXTENSIONS
86
+
87
+ def is_video_file(filename):
88
+ """Vérifie si l'extension correspond à un type vidéo connu."""
89
+ return get_file_extension(filename) in VIDEO_EXTENSIONS
90
+
91
+ def is_youtube_url(url):
92
+ """Vérifie si la chaîne ressemble à une URL YouTube valide."""
93
+ if not url: # Gère les cas où l'URL est None ou vide
94
+ return False
95
+ # Regex simple pour les formats courants d'URL YouTube
96
+ youtube_regex = re.compile(
97
+ r'(https?://)?(www\.)?' # Protocole et www optionnels
98
+ r'(youtube|youtu|youtube-nocookie)\.(com|be)/' # Domaines youtube.com, youtu.be, etc.
99
+ r'(watch\?v=|embed/|v/|.+\?v=)?' # Différents chemins possibles
100
+ r'([^&=%\?]{11})') # L'ID vidéo de 11 caractères
101
+ return youtube_regex.match(url) is not None
102
+
103
+ # --- Fonction d'Upload Vidéo avec Polling ---
104
+ def upload_video_with_polling(filepath, mime_type, max_wait_seconds=300, poll_interval=10):
105
+ """
106
+ Upload une vidéo via client.files.upload et attend son traitement.
107
+ Retourne l'objet File traité ou lève une exception.
108
+ """
109
+ if not gemini_client:
110
+ raise ConnectionError("Le client Gemini n'est pas initialisé.")
111
+
112
+ print(f"Début de l'upload vidéo via client.files: {filepath} ({mime_type})")
113
+ video_file = None # Initialise la variable pour le bloc finally
114
+ try:
115
+ # Lance l'upload
116
+ video_file = gemini_client.files.upload(path=filepath, mime_type=mime_type)
117
+ print(f"Upload initialisé. Nom du fichier distant: {video_file.name}. Attente du traitement...")
118
+
119
+ start_time = time.time()
120
+ # Boucle de polling tant que l'état est "PROCESSING"
121
+ while video_file.state == genai_types.FileState.PROCESSING:
122
+ elapsed_time = time.time() - start_time
123
+ # Vérifie le timeout
124
+ if elapsed_time > max_wait_seconds:
125
+ raise TimeoutError(f"Le traitement de la vidéo a dépassé le délai de {max_wait_seconds} secondes.")
126
+
127
+ print(f"Vidéo en cours de traitement... (État: {video_file.state.name}, {int(elapsed_time)}s écoulées)")
128
+ time.sleep(poll_interval)
129
+ # Récupère l'état mis à jour du fichier
130
+ video_file = gemini_client.files.get(name=video_file.name)
131
+
132
+ # Vérifie l'état final après la boucle
133
+ if video_file.state == genai_types.FileState.FAILED:
134
+ print(f"ERREUR: Le traitement de la vidéo a échoué. État: {video_file.state.name}")
135
+ raise ValueError("Le traitement de la vidéo a échoué côté serveur.")
136
+
137
+ if video_file.state == genai_types.FileState.ACTIVE:
138
+ print(f"Traitement vidéo terminé avec succès: {video_file.uri}")
139
+ return video_file # Retourne l'objet fichier SDK réussi
140
+
141
+ else:
142
+ # Gère d'autres états inattendus si nécessaire
143
+ print(f"AVERTISSEMENT: État inattendu du fichier vidéo après traitement: {video_file.state.name}")
144
+ raise RuntimeError(f"État inattendu du fichier vidéo: {video_file.state.name}")
145
 
146
+ except Exception as e:
147
+ print(f"Erreur lors de l'upload/traitement vidéo via client.files: {e}")
148
+ # Tente de supprimer le fichier distant en cas d'erreur pendant le polling/upload
149
+ if video_file and hasattr(video_file, 'name'):
150
+ try:
151
+ gemini_client.files.delete(name=video_file.name)
152
+ print(f"Tentative de nettoyage du fichier distant {video_file.name} après erreur.")
153
+ except Exception as delete_err:
154
+ print(f"Échec du nettoyage du fichier distant {video_file.name} après erreur: {delete_err}")
155
+ raise # Relance l'exception originale pour qu'elle soit gérée par l'appelant
156
+
157
+ # --- Fonctions de Recherche Web (inchangées - implémentez si nécessaire) ---
158
  def perform_web_search(query):
159
+ """Effectue une recherche web via l'API Serper (Exemple)."""
160
  serper_api_key = os.getenv("SERPER_API_KEY")
161
  if not serper_api_key:
162
  print("AVERTISSEMENT: Clé API SERPER_API_KEY manquante. Recherche web désactivée.")
163
  return None
164
+ # ... (votre implémentation de la recherche Serper) ...
165
+ print(f"Recherche Web (simulation) pour : {query}")
166
+ # Simuler des résultats pour le test
167
+ # return {"organic": [{"title": "Résultat Web 1", "link": "#", "snippet": "Description du résultat 1..."}]}
168
+ return None # Désactivé par défaut
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
 
170
  def format_search_results(data):
171
+ """Met en forme les résultats de recherche (Exemple)."""
172
+ if not data: return "Aucun résultat de recherche web pertinent."
173
+ # ... (votre implémentation du formatage) ...
174
+ results = ["**Résultats Web:**"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  if data.get('organic'):
176
+ for item in data['organic'][:3]:
177
+ results.append(f"- {item.get('title', '')}: {item.get('snippet', '')}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  return "\n".join(results)
179
 
180
+
181
+ # --- Préparation Historique (inchangé) ---
182
  def prepare_gemini_history(chat_history):
183
+ """Convertit l'historique de session pour l'API Gemini (texte seulement)."""
184
  gemini_history = []
185
  for message in chat_history:
186
+ # Ne transmet que le texte brut des messages précédents
187
  role = 'user' if message['role'] == 'user' else 'model'
188
+ text_part = message.get('raw_text', '') # Utilise raw_text stocké
189
+ # Ne pas inclure les fichiers/médias des tours précédents pour simplifier
190
+ if text_part: # N'ajoute que s'il y a du texte
191
+ gemini_history.append({'role': role, 'parts': [text_part]})
 
 
 
 
192
  return gemini_history
193
 
194
  # --- Routes Flask ---
 
200
 
201
  @app.route('/api/history', methods=['GET'])
202
  def get_history():
203
+ """Fournit l'historique de chat (formaté pour affichage) en JSON."""
204
  if 'chat_history' not in session:
205
  session['chat_history'] = []
206
 
207
+ # Prépare l'historique pour l'affichage (contient le HTML pour l'assistant)
208
  display_history = [
209
  {'role': msg.get('role', 'unknown'), 'text': msg.get('text', '')}
210
  for msg in session.get('chat_history', [])
211
  ]
212
+ # print(f"API: Récupération historique ({len(display_history)} messages)") # Debug
213
  return jsonify({'success': True, 'history': display_history})
214
 
215
  @app.route('/api/chat', methods=['POST'])
216
  def chat_api():
217
+ """Gère les requêtes de chat (texte, fichier/vidéo uploadé, URL YouTube)."""
218
+ # Vérifie si Gemini est configuré correctement
219
+ if not GEMINI_CONFIGURED or not gemini_client:
220
  print("API ERREUR: Tentative d'appel à /api/chat sans configuration Gemini valide.")
221
+ return jsonify({'success': False, 'error': "Le service IA n'est pas configuré correctement."}), 503
222
 
223
+ # --- Récupération des données du formulaire ---
224
  prompt = request.form.get('prompt', '').strip()
225
+ youtube_url = request.form.get('youtube_url', '').strip() # Récupère le champ YouTube dédié
226
+ use_web_search = request.form.get('web_search', 'false').lower() == 'true'
227
+ use_advanced = request.form.get('advanced_reasoning', 'false').lower() == 'true'
228
+ file = request.files.get('file') # Récupère le fichier uploadé
 
229
 
230
+ # --- Validation de l'entrée (au moins un type d'input requis) ---
231
+ if not file and not youtube_url and not prompt:
232
+ return jsonify({'success': False, 'error': 'Veuillez fournir un message, un fichier/vidéo ou un lien YouTube.'}), 400
233
 
234
  print(f"\n--- Nouvelle requête /api/chat ---")
235
+ print(f" Prompt: '{prompt[:50]}...'")
236
+ print(f" Fichier: {file.filename if file else 'Non'}")
237
+ print(f" URL YouTube: {youtube_url if youtube_url else 'Non'}")
238
+ print(f" Web Search: {use_web_search}, Advanced: {use_advanced}")
239
 
240
+ # Initialise l'historique de session si nécessaire
241
  if 'chat_history' not in session:
242
  session['chat_history'] = []
243
 
244
+ # --- Variables pour le traitement ---
245
+ uploaded_media_part = None # Part(file_data=...) pour Gemini
246
+ uploaded_filename_for_display = None # Nom à afficher dans le chat user
247
+ filepath_to_delete = None # Chemin du fichier temporaire à supprimer
248
+ is_media_request = False # True si fichier ou URL YT est l'input principal
249
+ media_type = None # 'file', 'video', 'youtube', ou 'text'
250
 
251
+ # --- Traitement de l'entrée (Priorité: Fichier > YouTube > Texte) ---
252
+ try:
253
+ # 1. Traiter le fichier uploadé s'il existe
254
+ if file and file.filename != '':
255
+ is_media_request = True
256
+ media_type = 'file' # Par défaut, pourrait devenir 'video'
257
+ uploaded_filename_for_display = secure_filename(file.filename)
258
+
259
+ if not allowed_file(uploaded_filename_for_display):
260
+ raise ValueError(f"Type de fichier non autorisé: {uploaded_filename_for_display}")
261
+
262
+ # Sauvegarde temporaire du fichier
263
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], uploaded_filename_for_display)
264
+ file.save(filepath)
265
+ filepath_to_delete = filepath # Marque pour suppression future
266
+ print(f" Fichier '{uploaded_filename_for_display}' sauvegardé -> '{filepath}'")
267
+ mime_type = mimetypes.guess_type(filepath)[0] or 'application/octet-stream'
268
+
269
+ # Utilise le polling pour les vidéos, upload direct pour les autres
270
+ if is_video_file(uploaded_filename_for_display):
271
+ media_type = 'video'
272
+ print(" Traitement VIDÉO Uploadée (avec polling)...")
273
+ # Appel bloquant qui attend le traitement
274
+ processed_media_file = upload_video_with_polling(filepath, mime_type)
275
+ # Crée le Part Gemini à partir de l'objet File retourné
276
+ uploaded_media_part = genai_types.Part(file_data=processed_media_file)
277
+ else:
278
+ print(" Traitement FICHIER standard...")
279
+ # Utilise l'upload global plus simple pour les non-vidéos
280
+ processed_media_file = genai.upload_file(path=filepath, mime_type=mime_type)
281
+ uploaded_media_part = genai_types.Part(file_data=processed_media_file)
282
+ print(f" Part Média ({media_type}) créé: {processed_media_file.uri}")
283
+
284
+ # 2. Sinon, traiter l'URL YouTube si fournie et valide
285
+ elif youtube_url:
286
+ if not is_youtube_url(youtube_url):
287
+ print(f" AVERTISSEMENT: '{youtube_url}' n'est pas un lien YouTube valide, sera ignoré ou traité comme texte si prompt vide.")
288
+ # Si le prompt est aussi vide, on ne fait rien de spécial ici,
289
+ # la validation initiale aurait dû échouer. Si prompt existe, il sera traité.
290
+ media_type = 'text' # Considéré comme texte simple si invalide
291
+ else:
292
+ is_media_request = True
293
+ media_type = 'youtube'
294
+ print(" Traitement LIEN YouTube...")
295
+ uploaded_filename_for_display = youtube_url # Affiche l'URL pour l'utilisateur
296
+ youtube_uri = youtube_url # Utilise l'URL validée comme URI
297
+ # Crée un Part FileData directement à partir de l'URI
298
+ # On peut spécifier un mime_type générique, Gemini gère les liens YT
299
+ uploaded_media_part = genai_types.Part(
300
+ file_data=genai_types.FileData(file_uri=youtube_uri, mime_type="video/mp4")
301
+ )
302
+ print(f" Part YouTube créé pour: {youtube_uri}")
303
+ # Ajoute un prompt par défaut si l'utilisateur n'en a pas mis
304
+ if not prompt:
305
+ prompt = "Décris ou analyse le contenu de cette vidéo YouTube."
306
+ print(f" Prompt par défaut ajouté pour YouTube: '{prompt}'")
307
+
308
+ # 3. Si ni fichier ni URL YT valide, c'est une requête texte
309
+ elif prompt:
310
+ media_type = 'text'
311
+ print(" Traitement PROMPT texte seul.")
312
  else:
313
+ # Ce cas ne devrait pas arriver à cause de la validation initiale
314
+ raise ValueError("Aucune entrée valide (fichier, URL YouTube ou texte) fournie.")
315
+
316
+ # --- Préparer et stocker le message utilisateur dans l'historique ---
317
+ # Construit le texte qui sera affiché dans l'historique du chat
318
+ display_user_text = prompt # Par défaut, le prompt texte
319
+ if media_type == 'file' or media_type == 'video':
320
+ # Précède le prompt par le nom du fichier entre crochets
321
+ display_user_text = f"[{uploaded_filename_for_display}]" + (f" {prompt}" if prompt else "")
322
+ elif media_type == 'youtube':
323
+ # Précède par [YouTube], ajoute le prompt, puis l'URL sur une nouvelle ligne
324
+ display_user_text = f"[YouTube]" + (f" {prompt}" if prompt else "") + f"\n{uploaded_filename_for_display}"
325
+
326
+ user_history_entry = {
327
+ 'role': 'user',
328
+ 'text': display_user_text, # Texte formaté pour l'affichage
329
+ 'raw_text': prompt # Texte brut original pour futurs appels Gemini
330
+ # Note: On ne stocke pas l'objet media_part dans la session
331
+ }
332
+ session['chat_history'].append(user_history_entry)
333
+ session.modified = True # Marque la session comme modifiée
334
+
335
+ # --- Préparer les 'parts' pour l'appel API Gemini ---
336
+ current_gemini_parts = []
337
+ # Ajouter le média (fichier uploadé ou URL YT) s'il existe
338
+ if uploaded_media_part:
339
+ current_gemini_parts.append(uploaded_media_part)
340
+
341
+ # Préparer le prompt textuel final (peut être enrichi par la recherche web)
342
+ final_prompt_for_gemini = prompt
343
+
344
+ # --- Recherche Web (Optionnelle, uniquement pour les requêtes texte) ---
345
+ if use_web_search and prompt and media_type == 'text':
346
+ print(" Activation Recherche Web...")
347
+ search_data = perform_web_search(prompt)
348
+ if search_data:
349
+ formatted_results = format_search_results(search_data)
350
+ # Construit un prompt enrichi
351
+ final_prompt_for_gemini = f"""Basé sur la question suivante et les informations web ci-dessous, fournis une réponse complète.
352
+
353
+ Question Originale:
354
+ "{prompt}"
355
+
356
+ Informations Web Pertinentes:
357
  --- DEBUT RESULTATS WEB ---
358
  {formatted_results}
359
  --- FIN RESULTATS WEB ---
360
 
361
+ Réponse:"""
362
+ print(" Prompt enrichi avec les résultats web.")
363
+ else:
364
+ print(" Aucun résultat de recherche web trouvé ou pertinent.")
 
365
 
366
+ # Ajouter la partie texte (originale ou enrichie) s'il y a du texte
367
+ if final_prompt_for_gemini:
368
+ current_gemini_parts.append(genai_types.Part(text=final_prompt_for_gemini))
369
 
370
+ # Vérification de sécurité : il doit y avoir au moins une partie (média ou texte)
371
+ if not current_gemini_parts:
372
+ print("ERREUR: Aucune partie (média ou texte) à envoyer à Gemini.")
373
+ raise ValueError("Impossible de traiter la requête : contenu vide.")
374
+
375
+
376
+ # --- Appel à l'API Gemini ---
377
+ # Prépare l'historique des messages PRÉCÉDENTS (texte seulement)
378
  gemini_history = prepare_gemini_history(session['chat_history'][:-1]) # Exclut le message actuel
379
+ # Construit le contenu complet pour l'appel API
 
380
  contents_for_gemini = gemini_history + [{'role': 'user', 'parts': current_gemini_parts}]
381
 
382
+ # Sélectionne le modèle : Pro pour média ou si avancé demandé, sinon Flash
383
+ selected_model_name = MODEL_PRO if is_media_request or use_advanced else MODEL_FLASH
384
+ print(f" Modèle sélectionné: {selected_model_name}")
385
 
386
+ # Crée l'instance du modèle spécifique pour cette requête
 
387
  active_model = genai.GenerativeModel(
388
  model_name=selected_model_name,
389
+ safety_settings=SAFETY_SETTINGS,
390
+ system_instruction=SYSTEM_INSTRUCTION
391
  )
392
 
393
+ print(f" Envoi de la requête à {selected_model_name} ({len(contents_for_gemini)} messages/tours)...")
394
+ # Appel API (non-streamé pour correspondre au code précédent)
 
395
  response = active_model.generate_content(contents_for_gemini)
 
396
 
397
+ # --- Traitement de la Réponse ---
398
+ response_text_raw = ""
399
+ response_html = ""
400
  try:
401
+ # Accède au texte de la réponse. Peut lever une exception si bloqué.
402
  response_text_raw = response.text
403
+ except ValueError: # Typiquement levé si la réponse est bloquée par les filtres
404
+ print(" ERREUR: La réponse de Gemini a été bloquée (probablement par les safety settings).")
405
+ # Fournit les détails du blocage si disponibles
406
+ try: print(f" Détails du blocage : {response.prompt_feedback}")
407
+ except Exception: pass # Ignore si prompt_feedback n'est pas accessible
 
408
  response_text_raw = "Désolé, ma réponse a été bloquée car elle pourrait enfreindre les règles de sécurité."
409
+ except Exception as resp_err: # Gère d'autres erreurs potentielles
410
+ print(f" ERREUR inattendue lors de l'accès à response.text : {resp_err}")
411
+ print(f" Réponse brute complète : {response}") # Log la réponse brute pour le debug
412
+ response_text_raw = "Désolé, une erreur interne s'est produite lors de la réception de la réponse."
413
 
414
+ # Convertit la réponse (même les messages d'erreur) en HTML
415
+ print(f" Réponse reçue (début): '{response_text_raw[:100]}...'")
416
+ response_html = markdown.markdown(response_text_raw, extensions=['fenced_code', 'tables', 'nl2br'])
417
+ print(" Réponse convertie en HTML.")
 
 
418
 
419
+ # --- Stocker la réponse de l'assistant et retourner au client ---
 
420
  assistant_history_entry = {
421
  'role': 'assistant',
422
  'text': response_html, # HTML pour l'affichage via get_history
 
425
  session['chat_history'].append(assistant_history_entry)
426
  session.modified = True
427
 
428
+ print(" Envoi de la réponse HTML au client.")
 
429
  return jsonify({'success': True, 'message': response_html})
430
 
431
+ # --- Gestion des Erreurs spécifiques (Timeout, Fichier invalide, etc.) ---
432
+ except (TimeoutError, ValueError, ConnectionError, FileNotFoundError) as e:
433
+ error_message = f"Erreur lors du traitement de la requête: {e}"
434
+ print(f"ERREUR (Traitement/Appel API): {error_message}")
435
+ # Retire le dernier message utilisateur de l'historique en cas d'erreur pour éviter les boucles
436
  if session.get('chat_history'):
437
  session['chat_history'].pop()
438
  session.modified = True
439
+ print(" Dernier message utilisateur retiré de l'historique après erreur.")
440
+ return jsonify({'success': False, 'error': error_message}), 500 # Erreur serveur
 
441
 
442
+ # --- Gestion des Erreurs Génériques/Inattendues ---
443
+ except Exception as e:
444
+ error_message = f"Une erreur interne inattendue est survenue: {e}"
445
+ print(f"ERREUR CRITIQUE INATTENDUE: {error_message}", exc_info=True) # Log la traceback complète
446
+ if session.get('chat_history'):
447
+ session['chat_history'].pop()
448
+ session.modified = True
449
+ print(" Dernier message utilisateur retiré de l'historique après erreur inattendue.")
450
+ return jsonify({'success': False, 'error': error_message}), 500
451
 
452
+ # --- Nettoyage (Exécuté dans tous les cas : succès ou erreur) ---
453
  finally:
 
454
  if filepath_to_delete and os.path.exists(filepath_to_delete):
455
  try:
456
  os.remove(filepath_to_delete)
457
+ print(f" Fichier temporaire '{filepath_to_delete}' supprimé avec succès.")
458
  except OSError as e:
459
+ print(f" ERREUR lors de la suppression du fichier temporaire '{filepath_to_delete}': {e}")
460
 
461
 
462
  @app.route('/clear', methods=['POST'])
463
  def clear_chat():
464
  """Efface l'historique de chat dans la session."""
465
+ session.pop('chat_history', None) # Supprime la clé de la session
 
466
  print("API: Historique de chat effacé via /clear.")
467
 
468
+ # Détecte si la requête vient d'AJAX (fetch) pour répondre en JSON
469
+ is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest' or \
470
+ 'application/json' in request.headers.get('Accept', '')
 
471
 
472
  if is_ajax:
473
  return jsonify({'success': True, 'message': 'Historique effacé.'})
474
  else:
475
+ # Comportement pour une soumission de formulaire classique (moins probable ici)
476
  flash("Conversation effacée.", "info")
477
+ return redirect(url_for('root')) # Redirige vers la page d'accueil
478
 
479
 
480
+ # --- Démarrage de l'application Flask ---
481
  if __name__ == '__main__':
482
  print("Démarrage du serveur Flask...")
483
+ # Utiliser un port différent si 5000 est déjà pris (ex: 5001)
 
 
 
484
  port = int(os.environ.get('PORT', 5001))
485
+ # debug=True : Rechargement auto, messages d'erreur détaillés (NE PAS UTILISER EN PRODUCTION)
486
+ # host='0.0.0.0' : Rend l'app accessible sur le réseau local
487
  app.run(debug=True, host='0.0.0.0', port=port)