Docfile commited on
Commit
a08ef55
·
verified ·
1 Parent(s): 9c39fae

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +90 -196
app.py CHANGED
@@ -7,8 +7,12 @@ import traceback # Ajout pour afficher les tracebacks complets
7
 
8
  from flask import Flask, request, session, jsonify, redirect, url_for, flash, render_template
9
  from dotenv import load_dotenv
 
10
  from google import genai
11
- from google.genai import types # Important pour Part, FileData, etc.
 
 
 
12
  import requests
13
  from werkzeug.utils import secure_filename
14
  import markdown # Pour convertir la réponse Markdown en HTML
@@ -39,14 +43,13 @@ print(f"Dossier d'upload configuré : {os.path.abspath(UPLOAD_FOLDER)}")
39
 
40
  # --- Configuration de l'API Gemini ---
41
  # Utilisez les noms de modèles spécifiés
42
- MODEL_FLASH = 'gemini-2.0-flash' # Mis à jour
43
- MODEL_PRO = 'gemini-2.5-pro-exp-03-25' # Mis à jour - Pro est souvent nécessaire/meilleur pour la vidéo
44
 
45
  # Instruction système pour le modèle
46
  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."
47
 
48
- # Paramètres de sécurité (ajuster si nécessaire)
49
- # Utilisation de la structure attendue par GenerateContentConfig
50
  SAFETY_SETTINGS_CONFIG = [
51
  types.SafetySetting(
52
  category=types.HarmCategory.HARM_CATEGORY_HATE_SPEECH,
@@ -68,28 +71,23 @@ SAFETY_SETTINGS_CONFIG = [
68
 
69
 
70
  GEMINI_CONFIGURED = False
71
- gemini_client = None # Client API pour les opérations sur les fichiers (upload vidéo)
72
 
73
  try:
74
  gemini_api_key = os.getenv("GOOGLE_API_KEY")
75
  if not gemini_api_key:
76
  raise ValueError("Clé API GOOGLE_API_KEY manquante dans le fichier .env")
77
 
78
- # Initialise le client pour les opérations sur les fichiers ET pour lister les modèles
79
  gemini_client = genai.Client(api_key=gemini_api_key)
80
- # Configure également l'espace de noms global pour GenerativeModel, etc. (Peut être redondant si client est utilisé)
81
- # genai.configure(api_key=gemini_api_key)
82
 
83
- # Vérifie si les modèles requis sont disponibles en utilisant le client (Correction: client.models.list())
84
  print("Vérification des modèles Gemini disponibles...")
85
- # Note: l'API liste les modèles sans le préfixe 'models/', on l'ajoute pour la comparaison
86
- available_models_full_names = [m.name for m in gemini_client.models.list()]
87
- required_models_prefixes = [MODEL_FLASH, MODEL_PRO] # Noms courts
88
- # Vérifie si les modèles requis (commençant par les noms courts) existent dans la liste complète
89
  models_found = {req: False for req in required_models_prefixes}
90
  for available in available_models_full_names:
91
  for req in required_models_prefixes:
92
- # Vérifie si le nom disponible commence par 'models/' suivi du nom requis
93
  if available.startswith(f'models/{req}'):
94
  models_found[req] = True
95
 
@@ -104,9 +102,10 @@ try:
104
 
105
  except Exception as e:
106
  print(f"ERREUR Critique lors de la configuration initiale de Gemini : {e}")
 
107
  print("L'application fonctionnera sans les fonctionnalités IA.")
108
- gemini_client = None # S'assure que le client est None si la config échoue
109
- GEMINI_CONFIGURED = False # S'assurer qu'il est False en cas d'erreur
110
 
111
  # --- Fonctions Utilitaires ---
112
 
@@ -124,94 +123,74 @@ def is_video_file(filename):
124
 
125
  def is_youtube_url(url):
126
  """Vérifie si la chaîne ressemble à une URL YouTube valide."""
127
- if not url: # Gère les cas où l'URL est None ou vide
128
- return False
129
- # Regex simple pour les formats courants d'URL YouTube
130
  youtube_regex = re.compile(
131
- r'(https?://)?(www.)?' # Protocole et www optionnels
132
- r'(youtube|youtu|youtube-nocookie).(com|be)/' # Domaines youtube.com, youtu.be, etc.
133
- r'(watch?v=|embed/|v/|.+?v=)?' # Différents chemins possibles
134
- r'([^&=%\?]{11})') # L'ID vidéo de 11 caractères
135
  return youtube_regex.match(url) is not None
136
 
137
  # --- Fonction d'Upload Vidéo avec Polling ---
138
 
139
  def upload_video_with_polling(filepath, mime_type, max_wait_seconds=300, poll_interval=10):
140
- """
141
- Upload une vidéo via client.files.upload et attend son traitement.
142
- Retourne l'objet File traité ou lève une exception.
143
- """
144
  if not gemini_client:
145
  raise ConnectionError("Le client Gemini n'est pas initialisé.")
146
 
147
  print(f"Début de l'upload vidéo via client.files: {filepath} ({mime_type})")
148
- video_file = None # Initialise la variable pour le bloc finally
149
  try:
150
- # Lance l'upload
151
  video_file = gemini_client.files.upload(path=filepath, mime_type=mime_type)
152
  print(f"Upload initialisé. Nom du fichier distant: {video_file.name}. Attente du traitement...")
153
 
154
  start_time = time.time()
155
- # Boucle de polling tant que l'état est "PROCESSING"
156
- while video_file.state == types.FileState.PROCESSING: # Utilise types.FileState
157
  elapsed_time = time.time() - start_time
158
- # Vérifie le timeout
159
  if elapsed_time > max_wait_seconds:
160
  raise TimeoutError(f"Le traitement de la vidéo a dépassé le délai de {max_wait_seconds} secondes.")
161
-
162
  print(f"Vidéo en cours de traitement... (État: {video_file.state.name}, {int(elapsed_time)}s écoulées)")
163
  time.sleep(poll_interval)
164
- # Récupère l'état mis à jour du fichier
165
  video_file = gemini_client.files.get(name=video_file.name)
166
 
167
- # Vérifie l'état final après la boucle
168
- if video_file.state == types.FileState.FAILED: # Utilise types.FileState
169
  print(f"ERREUR: Le traitement de la vidéo a échoué. État: {video_file.state.name}")
170
  raise ValueError("Le traitement de la vidéo a échoué côté serveur.")
171
-
172
- if video_file.state == types.FileState.ACTIVE: # Utilise types.FileState
173
  print(f"Traitement vidéo terminé avec succès: {video_file.uri}")
174
- return video_file # Retourne l'objet fichier SDK réussi
175
-
176
  else:
177
- # Gère d'autres états inattendus si nécessaire
178
  print(f"AVERTISSEMENT: État inattendu du fichier vidéo après traitement: {video_file.state.name}")
179
  raise RuntimeError(f"État inattendu du fichier vidéo: {video_file.state.name}")
180
 
181
  except Exception as e:
182
  print(f"Erreur lors de l'upload/traitement vidéo via client.files: {e}")
183
- # Tente de supprimer le fichier distant en cas d'erreur pendant le polling/upload
184
  if video_file and hasattr(video_file, 'name'):
185
  try:
186
  gemini_client.files.delete(name=video_file.name)
187
  print(f"Tentative de nettoyage du fichier distant {video_file.name} après erreur.")
188
  except Exception as delete_err:
189
  print(f"Échec du nettoyage du fichier distant {video_file.name} après erreur: {delete_err}")
190
- raise # Relance l'exception originale pour qu'elle soit gérée par l'appelant
191
 
192
  # --- Fonctions de Recherche Web (inchangées - implémentez si nécessaire) ---
193
 
194
  def perform_web_search(query):
195
- """Effectue une recherche web via l'API Serper (Exemple)."""
196
  serper_api_key = os.getenv("SERPER_API_KEY")
197
  if not serper_api_key:
198
  print("AVERTISSEMENT: Clé API SERPER_API_KEY manquante. Recherche web désactivée.")
199
  return None
200
- # ... (votre implémentation de la recherche Serper) ...
201
  print(f"Recherche Web (simulation) pour : {query}")
202
- # Simuler des résultats pour le test
203
- # return {"organic": [{"title": "Résultat Web 1", "link": "#", "snippet": "Description du résultat 1..."}]}
204
- return None # Désactivé par défaut
205
 
206
  def format_search_results(data):
207
- """Met en forme les résultats de recherche (Exemple)."""
208
  if not data: return "Aucun résultat de recherche web pertinent."
209
- # ... (votre implémentation du formatage) ...
210
  results = ["Résultats Web:"]
211
  if data.get('organic'):
212
  for item in data['organic'][:3]:
213
  results.append(f"- {item.get('title', '')}: {item.get('snippet', '')}")
214
  return "\n".join(results)
 
215
 
216
  # --- Préparation Historique (Corrigé pour utiliser types.Part) ---
217
 
@@ -219,15 +198,11 @@ def prepare_gemini_history(chat_history):
219
  """Convertit l'historique de session pour l'API Gemini (texte seulement)."""
220
  gemini_history = []
221
  for message in chat_history:
222
- # Ne transmet que le texte brut des messages précédents
223
  role = 'user' if message['role'] == 'user' else 'model'
224
- raw_text_content = message.get('raw_text', '') # Utilise raw_text stocké
225
-
226
- # Ne pas inclure les fichiers/médias des tours précédents pour simplifier
227
- if raw_text_content: # N'ajoute que s'il y a du texte
228
  # Correction: Créer un objet Part pour le texte
229
  text_part_object = types.Part(text=raw_text_content)
230
- # Ajouter le dictionnaire avec le rôle et la liste contenant l'objet Part
231
  gemini_history.append({'role': role, 'parts': [text_part_object]})
232
  return gemini_history
233
 
@@ -236,45 +211,36 @@ def prepare_gemini_history(chat_history):
236
  @app.route('/')
237
  def root():
238
  """Sert la page HTML principale."""
239
- # Assurez-vous d'avoir un fichier templates/index.html
240
  try:
241
  return render_template('index.html')
242
  except Exception as e:
243
- # Retourne une erreur simple si le template n'est pas trouvé
244
  print(f"Erreur lors du rendu du template index.html: {e}")
245
  return "Erreur: Impossible de charger la page principale. Vérifiez que 'templates/index.html' existe.", 500
246
 
247
-
248
  @app.route('/api/history', methods=['GET'])
249
  def get_history():
250
  """Fournit l'historique de chat (formaté pour affichage) en JSON."""
251
  if 'chat_history' not in session:
252
  session['chat_history'] = []
253
-
254
- # Prépare l'historique pour l'affichage (contient le HTML pour l'assistant)
255
  display_history = [
256
  {'role': msg.get('role', 'unknown'), 'text': msg.get('text', '')}
257
  for msg in session.get('chat_history', [])
258
  ]
259
- # print(f"API: Récupération historique ({len(display_history)} messages)") # Debug
260
  return jsonify({'success': True, 'history': display_history})
261
 
262
  @app.route('/api/chat', methods=['POST'])
263
  def chat_api():
264
  """Gère les requêtes de chat (texte, fichier/vidéo uploadé, URL YouTube)."""
265
- # Vérifie si Gemini est configuré correctement
266
  if not GEMINI_CONFIGURED or not gemini_client:
267
  print("API ERREUR: Tentative d'appel à /api/chat sans configuration Gemini valide.")
268
  return jsonify({'success': False, 'error': "Le service IA n'est pas configuré correctement."}), 503
269
 
270
- # --- Récupération des données du formulaire ---
271
  prompt = request.form.get('prompt', '').strip()
272
- youtube_url = request.form.get('youtube_url', '').strip() # Récupère le champ YouTube dédié
273
  use_web_search = request.form.get('web_search', 'false').lower() == 'true'
274
  use_advanced = request.form.get('advanced_reasoning', 'false').lower() == 'true'
275
- file = request.files.get('file') # Récupère le fichier uploadé
276
 
277
- # --- Validation de l'entrée (au moins un type d'input requis) ---
278
  if not file and not youtube_url and not prompt:
279
  return jsonify({'success': False, 'error': 'Veuillez fournir un message, un fichier/vidéo ou un lien YouTube.'}), 400
280
 
@@ -284,179 +250,123 @@ def chat_api():
284
  print(f" URL YouTube: {youtube_url if youtube_url else 'Non'}")
285
  print(f" Web Search: {use_web_search}, Advanced: {use_advanced}")
286
 
287
- # Initialise l'historique de session si nécessaire
288
  if 'chat_history' not in session:
289
  session['chat_history'] = []
290
 
291
- # --- Variables pour le traitement ---
292
- uploaded_media_part = None # Part(file_data=...) pour Gemini
293
- uploaded_filename_for_display = None # Nom à afficher dans le chat user
294
- filepath_to_delete = None # Chemin du fichier temporaire à supprimer
295
- is_media_request = False # True si fichier ou URL YT est l'input principal
296
- media_type = None # 'file', 'video', 'youtube', ou 'text'
297
 
298
- # --- Traitement de l'entrée (Priorité: Fichier > YouTube > Texte) ---
299
  try:
300
- # 1. Traiter le fichier uploadé s'il existe
301
  if file and file.filename != '':
302
  is_media_request = True
303
- media_type = 'file' # Par défaut, pourrait devenir 'video'
304
  uploaded_filename_for_display = secure_filename(file.filename)
305
-
306
  if not allowed_file(uploaded_filename_for_display):
307
  raise ValueError(f"Type de fichier non autorisé: {uploaded_filename_for_display}")
308
 
309
- # Sauvegarde temporaire du fichier
310
  filepath = os.path.join(app.config['UPLOAD_FOLDER'], uploaded_filename_for_display)
311
  file.save(filepath)
312
- filepath_to_delete = filepath # Marque pour suppression future
313
  print(f" Fichier '{uploaded_filename_for_display}' sauvegardé -> '{filepath}'")
314
  mime_type = mimetypes.guess_type(filepath)[0] or 'application/octet-stream'
315
 
316
- # Utilise le polling pour les vidéos, upload direct pour les autres
317
  if is_video_file(uploaded_filename_for_display):
318
  media_type = 'video'
319
  print(" Traitement VIDÉO Uploadée (avec polling)...")
320
- # Appel bloquant qui attend le traitement
321
  processed_media_file = upload_video_with_polling(filepath, mime_type)
322
- # Crée le Part Gemini à partir de l'objet File retourné
323
- uploaded_media_part = types.Part(file_data=processed_media_file) # Utilise types.Part
324
  else:
325
- print(" Traitement FICHIER standard...")
326
- # Utilise l'upload global plus simple pour les non-vidéos
327
- # Note: genai.upload_file n'existe pas directement, utiliser client.files.upload
328
- # S'il s'agit d'une image ou autre fichier non-vidéo, l'upload avec polling peut
329
- # retourner rapidement si le traitement est rapide, ou on peut simplifier.
330
- # Utilisons le client ici aussi pour la cohérence.
331
- print(f" Upload via client.files pour fichier non-vidéo: {filepath}")
332
  processed_media_file = gemini_client.files.upload(path=filepath, mime_type=mime_type)
333
- # Pas besoin de polling complexe ici en général, on suppose que c'est rapide.
334
- # On peut ajouter un petit wait ou une vérification simple si nécessaire.
335
- # Attente très courte pour s'assurer que l'état passe à ACTIVE (simplifié)
336
- time.sleep(2)
337
  processed_media_file = gemini_client.files.get(name=processed_media_file.name)
338
  if processed_media_file.state != types.FileState.ACTIVE:
339
- print(f"AVERTISSEMENT: Fichier non-vidéo '{processed_media_file.name}' n'est pas ACTIF après upload ({processed_media_file.state.name}). Tentative de continuer.")
340
- # On pourrait lever une erreur ici si l'état ACTIVE est crucial
341
- # raise RuntimeError(f"Échec de l'activation du fichier {processed_media_file.name}")
342
-
343
- uploaded_media_part = types.Part(file_data=processed_media_file) # Utilise types.Part
344
  print(f" Part Média ({media_type}) créé: {processed_media_file.uri}")
345
 
346
- # 2. Sinon, traiter l'URL YouTube si fournie et valide
347
  elif youtube_url:
348
  if not is_youtube_url(youtube_url):
349
- print(f" AVERTISSEMENT: '{youtube_url}' n'est pas un lien YouTube valide, sera ignoré ou traité comme texte si prompt vide.")
350
- media_type = 'text' # Considéré comme texte simple si invalide
351
  else:
352
  is_media_request = True
353
  media_type = 'youtube'
354
  print(" Traitement LIEN YouTube...")
355
- uploaded_filename_for_display = youtube_url # Affiche l'URL pour l'utilisateur
356
- youtube_uri = youtube_url # Utilise l'URL validée comme URI
357
- # Crée un Part FileData directement à partir de l'URI
358
- uploaded_media_part = types.Part( # Utilise types.Part
359
- file_data=types.FileData(file_uri=youtube_uri, mime_type="video/mp4") # Utilise types.FileData
360
  )
361
  print(f" Part YouTube créé pour: {youtube_uri}")
362
  if not prompt:
363
  prompt = "Décris ou analyse le contenu de cette vidéo YouTube."
364
  print(f" Prompt par défaut ajouté pour YouTube: '{prompt}'")
365
 
366
- # 3. Si ni fichier ni URL YT valide, c'est une requête texte
367
  elif prompt:
368
  media_type = 'text'
369
  print(" Traitement PROMPT texte seul.")
370
  else:
371
- raise ValueError("Aucune entrée valide (fichier, URL YouTube ou texte) fournie.")
372
 
373
- # --- Préparer et stocker le message utilisateur dans l'historique ---
374
  display_user_text = prompt
375
  if media_type == 'file' or media_type == 'video':
376
  display_user_text = f"[{uploaded_filename_for_display}]" + (f" {prompt}" if prompt else "")
377
  elif media_type == 'youtube':
378
  display_user_text = f"[YouTube]" + (f" {prompt}" if prompt else "") + f"\n{uploaded_filename_for_display}"
379
 
380
- user_history_entry = {
381
- 'role': 'user',
382
- 'text': display_user_text,
383
- 'raw_text': prompt
384
- }
385
  session['chat_history'].append(user_history_entry)
386
  session.modified = True
387
 
388
- # --- Préparer les 'parts' pour l'appel API Gemini ---
389
  current_gemini_parts = []
390
  if uploaded_media_part:
391
  current_gemini_parts.append(uploaded_media_part)
392
 
393
  final_prompt_for_gemini = prompt
394
-
395
  if use_web_search and prompt and media_type == 'text':
396
  print(" Activation Recherche Web...")
397
  search_data = perform_web_search(prompt)
398
  if search_data:
399
  formatted_results = format_search_results(search_data)
400
- final_prompt_for_gemini = f"""Basé sur la question suivante et les informations web ci-dessous, fournis une réponse complète.
401
-
402
- Question Originale:
403
- "{prompt}"
404
-
405
- Informations Web Pertinentes:
406
- --- DEBUT RESULTATS WEB ---
407
- {formatted_results}
408
- --- FIN RESULTATS WEB ---
409
-
410
- Réponse:"""
411
  print(" Prompt enrichi avec les résultats web.")
412
  else:
413
  print(" Aucun résultat de recherche web trouvé ou pertinent.")
414
 
415
  if final_prompt_for_gemini:
416
- # Correction: Toujours créer un objet Part pour le texte
417
- current_gemini_parts.append(types.Part(text=final_prompt_for_gemini)) # Utilise types.Part
418
 
419
  if not current_gemini_parts:
420
- print("ERREUR: Aucune partie (média ou texte) à envoyer à Gemini.")
421
  raise ValueError("Impossible de traiter la requête : contenu vide.")
422
 
423
- # --- Appel à l'API Gemini ---
424
  gemini_history = prepare_gemini_history(session['chat_history'][:-1])
 
425
  contents_for_gemini = gemini_history + [{'role': 'user', 'parts': current_gemini_parts}]
426
 
427
- # Sélectionne le modèle : Pro pour média ou si avancé demandé, sinon Flash
428
- # Préfixe 'models/' nécessaire pour l'API generate_content
429
- selected_model = f'models/{MODEL_PRO}' if is_media_request or use_advanced else f'models/{MODEL_FLASH}'
430
- print(f" Modèle sélectionné pour l'API: {selected_model}")
431
-
432
- # Crée l'instance de configuration
433
- generation_config = types.GenerationConfig(
434
- # candidate_count=1, # Optionnel: demander une seule réponse
435
- # stop_sequences=["..."], # Optionnel
436
- # max_output_tokens=..., # Optionnel
437
- # temperature=..., # Optionnel
438
- # top_p=..., # Optionnel
439
- # top_k=..., # Optionnel
440
- )
441
 
442
- # Appel API (Correction: utilise 'model', non 'model_name')
443
- # Utilisation de la méthode generate_content sur le client
444
- print(f" Envoi de la requête à {selected_model} ({len(contents_for_gemini)} messages/tours)...")
445
- response = gemini_client.generate_content(
446
- model=selected_model, # Correction: utilise 'model'
447
  contents=contents_for_gemini,
448
- generation_config=generation_config, # Passe l'objet config
449
- safety_settings=SAFETY_SETTINGS_CONFIG, # Passe les safety settings
450
- system_instruction=types.Content(parts=[types.Part(text=SYSTEM_INSTRUCTION)], role="system") # Instruction système formatée
451
  )
452
 
453
-
454
- # --- Traitement de la Réponse ---
455
  response_text_raw = ""
456
  response_html = ""
457
  try:
458
  response_text_raw = response.text
459
- except ValueError as ve: # Typiquement levé si la réponse est bloquée
460
  print(f" ERREUR: La réponse de Gemini a été bloquée (ValueError): {ve}")
461
  try:
462
  print(f" Détails du blocage (Prompt Feedback): {response.prompt_feedback}")
@@ -465,57 +375,47 @@ def chat_api():
465
  except Exception as feedback_err:
466
  print(f" Impossible de récupérer les détails du blocage: {feedback_err}")
467
  response_text_raw = "Désolé, ma réponse a été bloquée car elle pourrait enfreindre les règles de sécurité."
468
- except Exception as resp_err: # Gère d'autres erreurs potentielles
469
  print(f" ERREUR inattendue lors de l'accès à response.text : {resp_err}")
470
- print(f" Réponse brute complète : {response}") # Log la réponse brute pour le debug
471
  response_text_raw = "Désolé, une erreur interne s'est produite lors de la réception de la réponse."
472
 
473
- # Convertit la réponse (même les messages d'erreur) en HTML
474
  print(f" Réponse reçue (début): '{response_text_raw[:100]}...'")
475
  response_html = markdown.markdown(response_text_raw, extensions=['fenced_code', 'tables', 'nl2br'])
476
  print(" Réponse convertie en HTML.")
477
 
478
- # --- Stocker la réponse de l'assistant et retourner au client ---
479
- assistant_history_entry = {
480
- 'role': 'assistant',
481
- 'text': response_html,
482
- 'raw_text': response_text_raw
483
- }
484
  session['chat_history'].append(assistant_history_entry)
485
  session.modified = True
486
 
487
  print(" Envoi de la réponse HTML au client.")
488
  return jsonify({'success': True, 'message': response_html})
489
 
490
- # --- Gestion des Erreurs spécifiques (Timeout, Fichier invalide, etc.) ---
491
- except (TimeoutError, ValueError, ConnectionError, FileNotFoundError, types.StopCandidateException) as e:
492
  error_message = f"Erreur lors du traitement de la requête: {e}"
493
  print(f"ERREUR (Traitement/Appel API): {error_message}")
494
- if isinstance(e, types.StopCandidateException):
 
495
  error_message = "La génération a été stoppée, probablement à cause du contenu."
496
  print(f" StopCandidateException: {e}")
497
 
498
- if session.get('chat_history'):
499
- # Retire le dernier message utilisateur SEULEMENT s'il a été ajouté dans ce try
500
- if session['chat_history'][-1]['role'] == 'user':
501
- session['chat_history'].pop()
502
- session.modified = True
503
- print(" Dernier message utilisateur retiré de l'historique après erreur.")
504
  return jsonify({'success': False, 'error': error_message}), 500
505
 
506
- # --- Gestion des Erreurs Génériques/Inattendues ---
507
  except Exception as e:
508
  error_message = f"Une erreur interne inattendue est survenue: {e}"
509
  print(f"ERREUR CRITIQUE INATTENDUE: {error_message}")
510
- traceback.print_exc() # Correction: Utilise traceback pour afficher les détails
511
- if session.get('chat_history'):
512
- if session['chat_history'][-1]['role'] == 'user':
513
- session['chat_history'].pop()
514
- session.modified = True
515
- print(" Dernier message utilisateur retiré de l'historique après erreur inattendue.")
516
- return jsonify({'success': False, 'error': "Une erreur interne inattendue est survenue."}), 500 # Message générique au client
517
-
518
- # --- Nettoyage (Exécuté dans tous les cas : succès ou erreur) ---
519
  finally:
520
  if filepath_to_delete and os.path.exists(filepath_to_delete):
521
  try:
@@ -527,12 +427,10 @@ def chat_api():
527
  @app.route('/clear', methods=['POST'])
528
  def clear_chat():
529
  """Efface l'historique de chat dans la session."""
530
- session.pop('chat_history', None) # Supprime la clé de la session
531
  print("API: Historique de chat effacé via /clear.")
532
-
533
  is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest' or \
534
  'application/json' in request.headers.get('Accept', '')
535
-
536
  if is_ajax:
537
  return jsonify({'success': True, 'message': 'Historique effacé.'})
538
  else:
@@ -543,10 +441,6 @@ def clear_chat():
543
 
544
  if __name__ == '__main__':
545
  print("Démarrage du serveur Flask...")
546
- # Utiliser un port différent si 5000/7860 est déjà pris (ex: 5001)
547
- # Le port 7860 est souvent utilisé par Gradio/Streamlit, vérifiez la disponibilité
548
- port = int(os.environ.get('PORT', 7860)) # Garde 7860 comme vu dans vos logs
549
- # debug=False en production ! debug=True pour développement seulement.
550
- # ATTENTION: Les logs indiquaient debug=off, mais app.run(debug=True) force le mode debug.
551
- # Mettez debug=False si vous ne voulez pas le rechargement auto et les tracebacks dans le navigateur.
552
  app.run(debug=True, host='0.0.0.0', port=port)
 
7
 
8
  from flask import Flask, request, session, jsonify, redirect, url_for, flash, render_template
9
  from dotenv import load_dotenv
10
+ # Importe genai et types séparément pour plus de clarté
11
  from google import genai
12
+ from google.genai import types # Important pour Part, FileData, SafetySetting etc.
13
+ # L'exception StopCandidateException est directement sous genai
14
+ # from google.generativeai.types import StopCandidateException # Ne fonctionne pas
15
+
16
  import requests
17
  from werkzeug.utils import secure_filename
18
  import markdown # Pour convertir la réponse Markdown en HTML
 
43
 
44
  # --- Configuration de l'API Gemini ---
45
  # Utilisez les noms de modèles spécifiés
46
+ MODEL_FLASH = 'gemini-2.0-flash'
47
+ MODEL_PRO = 'gemini-2.5-pro-exp-03-25' # Pro est souvent nécessaire/meilleur pour la vidéo
48
 
49
  # Instruction système pour le modèle
50
  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."
51
 
52
+ # Paramètres de sécurité (structure correcte pour GenerateContentConfig)
 
53
  SAFETY_SETTINGS_CONFIG = [
54
  types.SafetySetting(
55
  category=types.HarmCategory.HARM_CATEGORY_HATE_SPEECH,
 
71
 
72
 
73
  GEMINI_CONFIGURED = False
74
+ gemini_client = None # Client API
75
 
76
  try:
77
  gemini_api_key = os.getenv("GOOGLE_API_KEY")
78
  if not gemini_api_key:
79
  raise ValueError("Clé API GOOGLE_API_KEY manquante dans le fichier .env")
80
 
81
+ # Initialise le client
82
  gemini_client = genai.Client(api_key=gemini_api_key)
 
 
83
 
84
+ # Vérifie si les modèles requis sont disponibles en utilisant le client
85
  print("Vérification des modèles Gemini disponibles...")
86
+ available_models_full_names = [m.name for m in gemini_client.models.list()] # Correction: client.models.list()
87
+ required_models_prefixes = [MODEL_FLASH, MODEL_PRO]
 
 
88
  models_found = {req: False for req in required_models_prefixes}
89
  for available in available_models_full_names:
90
  for req in required_models_prefixes:
 
91
  if available.startswith(f'models/{req}'):
92
  models_found[req] = True
93
 
 
102
 
103
  except Exception as e:
104
  print(f"ERREUR Critique lors de la configuration initiale de Gemini : {e}")
105
+ traceback.print_exc() # Affiche le traceback pour l'erreur d'init
106
  print("L'application fonctionnera sans les fonctionnalités IA.")
107
+ gemini_client = None
108
+ GEMINI_CONFIGURED = False
109
 
110
  # --- Fonctions Utilitaires ---
111
 
 
123
 
124
  def is_youtube_url(url):
125
  """Vérifie si la chaîne ressemble à une URL YouTube valide."""
126
+ if not url: return False
 
 
127
  youtube_regex = re.compile(
128
+ r'(https?://)?(www.)?'
129
+ r'(youtube|youtu|youtube-nocookie).(com|be)/'
130
+ r'(watch?v=|embed/|v/|.+?v=)?'
131
+ r'([^&=%\?]{11})')
132
  return youtube_regex.match(url) is not None
133
 
134
  # --- Fonction d'Upload Vidéo avec Polling ---
135
 
136
  def upload_video_with_polling(filepath, mime_type, max_wait_seconds=300, poll_interval=10):
137
+ """Upload une vidéo via client.files.upload et attend son traitement."""
 
 
 
138
  if not gemini_client:
139
  raise ConnectionError("Le client Gemini n'est pas initialisé.")
140
 
141
  print(f"Début de l'upload vidéo via client.files: {filepath} ({mime_type})")
142
+ video_file = None
143
  try:
 
144
  video_file = gemini_client.files.upload(path=filepath, mime_type=mime_type)
145
  print(f"Upload initialisé. Nom du fichier distant: {video_file.name}. Attente du traitement...")
146
 
147
  start_time = time.time()
148
+ while video_file.state == types.FileState.PROCESSING:
 
149
  elapsed_time = time.time() - start_time
 
150
  if elapsed_time > max_wait_seconds:
151
  raise TimeoutError(f"Le traitement de la vidéo a dépassé le délai de {max_wait_seconds} secondes.")
 
152
  print(f"Vidéo en cours de traitement... (État: {video_file.state.name}, {int(elapsed_time)}s écoulées)")
153
  time.sleep(poll_interval)
 
154
  video_file = gemini_client.files.get(name=video_file.name)
155
 
156
+ if video_file.state == types.FileState.FAILED:
 
157
  print(f"ERREUR: Le traitement de la vidéo a échoué. État: {video_file.state.name}")
158
  raise ValueError("Le traitement de la vidéo a échoué côté serveur.")
159
+ if video_file.state == types.FileState.ACTIVE:
 
160
  print(f"Traitement vidéo terminé avec succès: {video_file.uri}")
161
+ return video_file
 
162
  else:
 
163
  print(f"AVERTISSEMENT: État inattendu du fichier vidéo après traitement: {video_file.state.name}")
164
  raise RuntimeError(f"État inattendu du fichier vidéo: {video_file.state.name}")
165
 
166
  except Exception as e:
167
  print(f"Erreur lors de l'upload/traitement vidéo via client.files: {e}")
 
168
  if video_file and hasattr(video_file, 'name'):
169
  try:
170
  gemini_client.files.delete(name=video_file.name)
171
  print(f"Tentative de nettoyage du fichier distant {video_file.name} après erreur.")
172
  except Exception as delete_err:
173
  print(f"Échec du nettoyage du fichier distant {video_file.name} après erreur: {delete_err}")
174
+ raise
175
 
176
  # --- Fonctions de Recherche Web (inchangées - implémentez si nécessaire) ---
177
 
178
  def perform_web_search(query):
 
179
  serper_api_key = os.getenv("SERPER_API_KEY")
180
  if not serper_api_key:
181
  print("AVERTISSEMENT: Clé API SERPER_API_KEY manquante. Recherche web désactivée.")
182
  return None
 
183
  print(f"Recherche Web (simulation) pour : {query}")
184
+ return None
 
 
185
 
186
  def format_search_results(data):
 
187
  if not data: return "Aucun résultat de recherche web pertinent."
 
188
  results = ["Résultats Web:"]
189
  if data.get('organic'):
190
  for item in data['organic'][:3]:
191
  results.append(f"- {item.get('title', '')}: {item.get('snippet', '')}")
192
  return "\n".join(results)
193
+ return "Aucun résultat organique trouvé."
194
 
195
  # --- Préparation Historique (Corrigé pour utiliser types.Part) ---
196
 
 
198
  """Convertit l'historique de session pour l'API Gemini (texte seulement)."""
199
  gemini_history = []
200
  for message in chat_history:
 
201
  role = 'user' if message['role'] == 'user' else 'model'
202
+ raw_text_content = message.get('raw_text', '')
203
+ if raw_text_content:
 
 
204
  # Correction: Créer un objet Part pour le texte
205
  text_part_object = types.Part(text=raw_text_content)
 
206
  gemini_history.append({'role': role, 'parts': [text_part_object]})
207
  return gemini_history
208
 
 
211
  @app.route('/')
212
  def root():
213
  """Sert la page HTML principale."""
 
214
  try:
215
  return render_template('index.html')
216
  except Exception as e:
 
217
  print(f"Erreur lors du rendu du template index.html: {e}")
218
  return "Erreur: Impossible de charger la page principale. Vérifiez que 'templates/index.html' existe.", 500
219
 
 
220
  @app.route('/api/history', methods=['GET'])
221
  def get_history():
222
  """Fournit l'historique de chat (formaté pour affichage) en JSON."""
223
  if 'chat_history' not in session:
224
  session['chat_history'] = []
 
 
225
  display_history = [
226
  {'role': msg.get('role', 'unknown'), 'text': msg.get('text', '')}
227
  for msg in session.get('chat_history', [])
228
  ]
 
229
  return jsonify({'success': True, 'history': display_history})
230
 
231
  @app.route('/api/chat', methods=['POST'])
232
  def chat_api():
233
  """Gère les requêtes de chat (texte, fichier/vidéo uploadé, URL YouTube)."""
 
234
  if not GEMINI_CONFIGURED or not gemini_client:
235
  print("API ERREUR: Tentative d'appel à /api/chat sans configuration Gemini valide.")
236
  return jsonify({'success': False, 'error': "Le service IA n'est pas configuré correctement."}), 503
237
 
 
238
  prompt = request.form.get('prompt', '').strip()
239
+ youtube_url = request.form.get('youtube_url', '').strip()
240
  use_web_search = request.form.get('web_search', 'false').lower() == 'true'
241
  use_advanced = request.form.get('advanced_reasoning', 'false').lower() == 'true'
242
+ file = request.files.get('file')
243
 
 
244
  if not file and not youtube_url and not prompt:
245
  return jsonify({'success': False, 'error': 'Veuillez fournir un message, un fichier/vidéo ou un lien YouTube.'}), 400
246
 
 
250
  print(f" URL YouTube: {youtube_url if youtube_url else 'Non'}")
251
  print(f" Web Search: {use_web_search}, Advanced: {use_advanced}")
252
 
 
253
  if 'chat_history' not in session:
254
  session['chat_history'] = []
255
 
256
+ uploaded_media_part = None
257
+ uploaded_filename_for_display = None
258
+ filepath_to_delete = None
259
+ is_media_request = False
260
+ media_type = None
 
261
 
 
262
  try:
 
263
  if file and file.filename != '':
264
  is_media_request = True
265
+ media_type = 'file'
266
  uploaded_filename_for_display = secure_filename(file.filename)
 
267
  if not allowed_file(uploaded_filename_for_display):
268
  raise ValueError(f"Type de fichier non autorisé: {uploaded_filename_for_display}")
269
 
 
270
  filepath = os.path.join(app.config['UPLOAD_FOLDER'], uploaded_filename_for_display)
271
  file.save(filepath)
272
+ filepath_to_delete = filepath
273
  print(f" Fichier '{uploaded_filename_for_display}' sauvegardé -> '{filepath}'")
274
  mime_type = mimetypes.guess_type(filepath)[0] or 'application/octet-stream'
275
 
 
276
  if is_video_file(uploaded_filename_for_display):
277
  media_type = 'video'
278
  print(" Traitement VIDÉO Uploadée (avec polling)...")
 
279
  processed_media_file = upload_video_with_polling(filepath, mime_type)
280
+ uploaded_media_part = types.Part(file_data=processed_media_file)
 
281
  else:
282
+ print(" Traitement FICHIER standard via client.files...")
 
 
 
 
 
 
283
  processed_media_file = gemini_client.files.upload(path=filepath, mime_type=mime_type)
284
+ time.sleep(2) # Attente simplifiée
 
 
 
285
  processed_media_file = gemini_client.files.get(name=processed_media_file.name)
286
  if processed_media_file.state != types.FileState.ACTIVE:
287
+ print(f"AVERTISSEMENT: Fichier non-vidéo '{processed_media_file.name}' n'est pas ACTIF ({processed_media_file.state.name}).")
288
+ uploaded_media_part = types.Part(file_data=processed_media_file)
 
 
 
289
  print(f" Part Média ({media_type}) créé: {processed_media_file.uri}")
290
 
 
291
  elif youtube_url:
292
  if not is_youtube_url(youtube_url):
293
+ print(f" AVERTISSEMENT: '{youtube_url}' n'est pas un lien YouTube valide.")
294
+ media_type = 'text'
295
  else:
296
  is_media_request = True
297
  media_type = 'youtube'
298
  print(" Traitement LIEN YouTube...")
299
+ uploaded_filename_for_display = youtube_url
300
+ youtube_uri = youtube_url
301
+ uploaded_media_part = types.Part(
302
+ file_data=types.FileData(file_uri=youtube_uri, mime_type="video/mp4")
 
303
  )
304
  print(f" Part YouTube créé pour: {youtube_uri}")
305
  if not prompt:
306
  prompt = "Décris ou analyse le contenu de cette vidéo YouTube."
307
  print(f" Prompt par défaut ajouté pour YouTube: '{prompt}'")
308
 
 
309
  elif prompt:
310
  media_type = 'text'
311
  print(" Traitement PROMPT texte seul.")
312
  else:
313
+ raise ValueError("Aucune entrée valide fournie.")
314
 
 
315
  display_user_text = prompt
316
  if media_type == 'file' or media_type == 'video':
317
  display_user_text = f"[{uploaded_filename_for_display}]" + (f" {prompt}" if prompt else "")
318
  elif media_type == 'youtube':
319
  display_user_text = f"[YouTube]" + (f" {prompt}" if prompt else "") + f"\n{uploaded_filename_for_display}"
320
 
321
+ user_history_entry = {'role': 'user', 'text': display_user_text, 'raw_text': prompt}
 
 
 
 
322
  session['chat_history'].append(user_history_entry)
323
  session.modified = True
324
 
 
325
  current_gemini_parts = []
326
  if uploaded_media_part:
327
  current_gemini_parts.append(uploaded_media_part)
328
 
329
  final_prompt_for_gemini = prompt
 
330
  if use_web_search and prompt and media_type == 'text':
331
  print(" Activation Recherche Web...")
332
  search_data = perform_web_search(prompt)
333
  if search_data:
334
  formatted_results = format_search_results(search_data)
335
+ final_prompt_for_gemini = f"""Basé sur la question suivante et les informations web ci-dessous, fournis une réponse complète.\n\nQuestion Originale:\n"{prompt}"\n\nInformations Web Pertinentes:\n--- DEBUT RESULTATS WEB ---\n{formatted_results}\n--- FIN RESULTATS WEB ---\n\nRéponse:"""
 
 
 
 
 
 
 
 
 
 
336
  print(" Prompt enrichi avec les résultats web.")
337
  else:
338
  print(" Aucun résultat de recherche web trouvé ou pertinent.")
339
 
340
  if final_prompt_for_gemini:
341
+ current_gemini_parts.append(types.Part(text=final_prompt_for_gemini)) # Correction: Toujours Part
 
342
 
343
  if not current_gemini_parts:
 
344
  raise ValueError("Impossible de traiter la requête : contenu vide.")
345
 
 
346
  gemini_history = prepare_gemini_history(session['chat_history'][:-1])
347
+ # Structure correcte pour contents (liste de dictionnaires role/parts)
348
  contents_for_gemini = gemini_history + [{'role': 'user', 'parts': current_gemini_parts}]
349
 
350
+ selected_model_api_name = f'models/{MODEL_PRO}' if is_media_request or use_advanced else f'models/{MODEL_FLASH}'
351
+ print(f" Modèle sélectionné pour l'API: {selected_model_api_name}")
352
+
353
+ generation_config = types.GenerationConfig() # Config vide par défaut, ajouter des params si besoin
 
 
 
 
 
 
 
 
 
 
354
 
355
+ # Correction: Appel via client.models.generate_content
356
+ print(f" Envoi de la requête à {selected_model_api_name} ({len(contents_for_gemini)} messages/tours)...")
357
+ response = gemini_client.models.generate_content(
358
+ model=selected_model_api_name, # Utilise 'model'
 
359
  contents=contents_for_gemini,
360
+ generation_config=generation_config,
361
+ safety_settings=SAFETY_SETTINGS_CONFIG,
362
+ system_instruction=types.Content(parts=[types.Part(text=SYSTEM_INSTRUCTION)], role="system")
363
  )
364
 
 
 
365
  response_text_raw = ""
366
  response_html = ""
367
  try:
368
  response_text_raw = response.text
369
+ except ValueError as ve:
370
  print(f" ERREUR: La réponse de Gemini a été bloquée (ValueError): {ve}")
371
  try:
372
  print(f" Détails du blocage (Prompt Feedback): {response.prompt_feedback}")
 
375
  except Exception as feedback_err:
376
  print(f" Impossible de récupérer les détails du blocage: {feedback_err}")
377
  response_text_raw = "Désolé, ma réponse a été bloquée car elle pourrait enfreindre les règles de sécurité."
378
+ except Exception as resp_err:
379
  print(f" ERREUR inattendue lors de l'accès à response.text : {resp_err}")
380
+ print(f" Réponse brute complète : {response}")
381
  response_text_raw = "Désolé, une erreur interne s'est produite lors de la réception de la réponse."
382
 
 
383
  print(f" Réponse reçue (début): '{response_text_raw[:100]}...'")
384
  response_html = markdown.markdown(response_text_raw, extensions=['fenced_code', 'tables', 'nl2br'])
385
  print(" Réponse convertie en HTML.")
386
 
387
+ assistant_history_entry = {'role': 'assistant', 'text': response_html, 'raw_text': response_text_raw}
 
 
 
 
 
388
  session['chat_history'].append(assistant_history_entry)
389
  session.modified = True
390
 
391
  print(" Envoi de la réponse HTML au client.")
392
  return jsonify({'success': True, 'message': response_html})
393
 
394
+ # Correction: Utiliser genai.StopCandidateException
395
+ except (TimeoutError, ValueError, ConnectionError, FileNotFoundError, genai.StopCandidateException) as e:
396
  error_message = f"Erreur lors du traitement de la requête: {e}"
397
  print(f"ERREUR (Traitement/Appel API): {error_message}")
398
+ # Correction: Utiliser genai.StopCandidateException pour la vérification
399
+ if isinstance(e, genai.StopCandidateException):
400
  error_message = "La génération a été stoppée, probablement à cause du contenu."
401
  print(f" StopCandidateException: {e}")
402
 
403
+ if session.get('chat_history') and session['chat_history'][-1]['role'] == 'user':
404
+ session['chat_history'].pop()
405
+ session.modified = True
406
+ print(" Dernier message utilisateur retiré de l'historique après erreur.")
 
 
407
  return jsonify({'success': False, 'error': error_message}), 500
408
 
 
409
  except Exception as e:
410
  error_message = f"Une erreur interne inattendue est survenue: {e}"
411
  print(f"ERREUR CRITIQUE INATTENDUE: {error_message}")
412
+ traceback.print_exc() # Correction: Utilise traceback pour détails
413
+ if session.get('chat_history') and session['chat_history'][-1]['role'] == 'user':
414
+ session['chat_history'].pop()
415
+ session.modified = True
416
+ print(" Dernier message utilisateur retiré de l'historique après erreur inattendue.")
417
+ return jsonify({'success': False, 'error': "Une erreur interne inattendue est survenue."}), 500
418
+
 
 
419
  finally:
420
  if filepath_to_delete and os.path.exists(filepath_to_delete):
421
  try:
 
427
  @app.route('/clear', methods=['POST'])
428
  def clear_chat():
429
  """Efface l'historique de chat dans la session."""
430
+ session.pop('chat_history', None)
431
  print("API: Historique de chat effacé via /clear.")
 
432
  is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest' or \
433
  'application/json' in request.headers.get('Accept', '')
 
434
  if is_ajax:
435
  return jsonify({'success': True, 'message': 'Historique effacé.'})
436
  else:
 
441
 
442
  if __name__ == '__main__':
443
  print("Démarrage du serveur Flask...")
444
+ port = int(os.environ.get('PORT', 7860))
445
+ # Mettre debug=False pour la production
 
 
 
 
446
  app.run(debug=True, host='0.0.0.0', port=port)