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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +150 -115
app.py CHANGED
@@ -3,10 +3,12 @@ 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
  from google import genai
9
- from google.genai import types
10
  import requests
11
  from werkzeug.utils import secure_filename
12
  import markdown # Pour convertir la réponse Markdown en HTML
@@ -36,22 +38,38 @@ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
36
  print(f"Dossier d'upload configuré : {os.path.abspath(UPLOAD_FOLDER)}")
37
 
38
  # --- Configuration de l'API Gemini ---
39
- # Utilisez les noms de modèles les plus récents auxquels vous avez accès
40
- MODEL_FLASH = 'gemini-2.0-flash'
41
- MODEL_PRO = 'gemini-2.5-pro-exp-03-25' # Pro est souvent nécessaire/meilleur pour la vidéo
42
 
43
  # Instruction système pour le modèle
44
  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."
45
 
46
- # Paramètres de sécurité (ajuster si nécessaire, BLOCK_NONE est très permissif)
47
- SAFETY_SETTINGS = [ {"category": c, "threshold": "BLOCK_NONE"} for c in [
48
- "HARM_CATEGORY_HARASSMENT", "HARM_CATEGORY_HATE_SPEECH",
49
- "HARM_CATEGORY_SEXUALLY_EXPLICIT", "HARM_CATEGORY_DANGEROUS_CONTENT"]]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
  GEMINI_CONFIGURED = False
52
  gemini_client = None # Client API pour les opérations sur les fichiers (upload vidéo)
53
 
54
-
55
  try:
56
  gemini_api_key = os.getenv("GOOGLE_API_KEY")
57
  if not gemini_api_key:
@@ -59,20 +77,29 @@ try:
59
 
60
  # Initialise le client pour les opérations sur les fichiers ET pour lister les modèles
61
  gemini_client = genai.Client(api_key=gemini_api_key)
62
- # Configure également l'espace de noms global pour GenerativeModel, etc.
63
- # genai.configure(api_key=gemini_api_key) # Cette ligne peut être redondante si le client est utilisé partout
64
 
65
- # Vérifie si les modèles requis sont disponibles en utilisant le client
66
  print("Vérification des modèles Gemini disponibles...")
67
- models_list = [m.name for m in gemini_client.models.list()] # <-- LIGNE CORRIGÉE
68
- required_models = [f'models/{MODEL_FLASH}', f'models/{MODEL_PRO}']
69
- if all(model in models_list for model in required_models):
70
- print(f"Configuration Gemini effectuée. Modèles requis trouvés.")
 
 
 
 
 
 
 
 
 
71
  print(f"Instruction Système: {SYSTEM_INSTRUCTION}")
72
  GEMINI_CONFIGURED = True
73
  else:
74
- missing = [model for model in required_models if model not in models_list]
75
- raise ValueError(f"Les modèles Gemini requis suivants sont manquants: {missing}")
76
 
77
 
78
  except Exception as e:
@@ -81,7 +108,6 @@ except Exception as e:
81
  gemini_client = None # S'assure que le client est None si la config échoue
82
  GEMINI_CONFIGURED = False # S'assurer qu'il est False en cas d'erreur
83
 
84
-
85
  # --- Fonctions Utilitaires ---
86
 
87
  def get_file_extension(filename):
@@ -127,7 +153,7 @@ def upload_video_with_polling(filepath, mime_type, max_wait_seconds=300, poll_in
127
 
128
  start_time = time.time()
129
  # Boucle de polling tant que l'état est "PROCESSING"
130
- while video_file.state == genai.types.FileState.PROCESSING:
131
  elapsed_time = time.time() - start_time
132
  # Vérifie le timeout
133
  if elapsed_time > max_wait_seconds:
@@ -139,11 +165,11 @@ def upload_video_with_polling(filepath, mime_type, max_wait_seconds=300, poll_in
139
  video_file = gemini_client.files.get(name=video_file.name)
140
 
141
  # Vérifie l'état final après la boucle
142
- if video_file.state == genai.types.FileState.FAILED:
143
  print(f"ERREUR: Le traitement de la vidéo a échoué. État: {video_file.state.name}")
144
  raise ValueError("Le traitement de la vidéo a échoué côté serveur.")
145
 
146
- if video_file.state == genai.types.FileState.ACTIVE:
147
  print(f"Traitement vidéo terminé avec succès: {video_file.uri}")
148
  return video_file # Retourne l'objet fichier SDK réussi
149
 
@@ -187,7 +213,7 @@ def format_search_results(data):
187
  results.append(f"- {item.get('title', '')}: {item.get('snippet', '')}")
188
  return "\n".join(results)
189
 
190
- # --- Préparation Historique (inchangé) ---
191
 
192
  def prepare_gemini_history(chat_history):
193
  """Convertit l'historique de session pour l'API Gemini (texte seulement)."""
@@ -195,10 +221,14 @@ def prepare_gemini_history(chat_history):
195
  for message in chat_history:
196
  # Ne transmet que le texte brut des messages précédents
197
  role = 'user' if message['role'] == 'user' else 'model'
198
- text_part = message.get('raw_text', '') # Utilise raw_text stocké
 
199
  # Ne pas inclure les fichiers/médias des tours précédents pour simplifier
200
- if text_part: # N'ajoute que s'il y a du texte
201
- gemini_history.append({'role': role, 'parts': [text_part]})
 
 
 
202
  return gemini_history
203
 
204
  # --- Routes Flask ---
@@ -206,7 +236,14 @@ def prepare_gemini_history(chat_history):
206
  @app.route('/')
207
  def root():
208
  """Sert la page HTML principale."""
209
- return render_template('index.html')
 
 
 
 
 
 
 
210
 
211
  @app.route('/api/history', methods=['GET'])
212
  def get_history():
@@ -283,20 +320,33 @@ def chat_api():
283
  # Appel bloquant qui attend le traitement
284
  processed_media_file = upload_video_with_polling(filepath, mime_type)
285
  # Crée le Part Gemini à partir de l'objet File retourné
286
- uploaded_media_part = genai.types.Part(file_data=processed_media_file)
287
  else:
288
  print(" Traitement FICHIER standard...")
289
  # Utilise l'upload global plus simple pour les non-vidéos
290
- processed_media_file = genai.upload_file(path=filepath, mime_type=mime_type)
291
- uploaded_media_part = genai.types.Part(file_data=processed_media_file)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  print(f" Part Média ({media_type}) créé: {processed_media_file.uri}")
293
 
294
  # 2. Sinon, traiter l'URL YouTube si fournie et valide
295
  elif youtube_url:
296
  if not is_youtube_url(youtube_url):
297
  print(f" AVERTISSEMENT: '{youtube_url}' n'est pas un lien YouTube valide, sera ignoré ou traité comme texte si prompt vide.")
298
- # Si le prompt est aussi vide, on ne fait rien de spécial ici,
299
- # la validation initiale aurait dû échouer. Si prompt existe, il sera traité.
300
  media_type = 'text' # Considéré comme texte simple si invalide
301
  else:
302
  is_media_request = True
@@ -305,12 +355,10 @@ def chat_api():
305
  uploaded_filename_for_display = youtube_url # Affiche l'URL pour l'utilisateur
306
  youtube_uri = youtube_url # Utilise l'URL validée comme URI
307
  # Crée un Part FileData directement à partir de l'URI
308
- # On peut spécifier un mime_type générique, Gemini gère les liens YT
309
- uploaded_media_part = genai.types.Part(
310
- file_data=genai.types.FileData(file_uri=youtube_uri, mime_type="video/mp4")
311
  )
312
  print(f" Part YouTube créé pour: {youtube_uri}")
313
- # Ajoute un prompt par défaut si l'utilisateur n'en a pas mis
314
  if not prompt:
315
  prompt = "Décris ou analyse le contenu de cette vidéo YouTube."
316
  print(f" Prompt par défaut ajouté pour YouTube: '{prompt}'")
@@ -320,44 +368,35 @@ def chat_api():
320
  media_type = 'text'
321
  print(" Traitement PROMPT texte seul.")
322
  else:
323
- # Ce cas ne devrait pas arriver à cause de la validation initiale
324
  raise ValueError("Aucune entrée valide (fichier, URL YouTube ou texte) fournie.")
325
 
326
  # --- Préparer et stocker le message utilisateur dans l'historique ---
327
- # Construit le texte qui sera affiché dans l'historique du chat
328
- display_user_text = prompt # Par défaut, le prompt texte
329
  if media_type == 'file' or media_type == 'video':
330
- # Précède le prompt par le nom du fichier entre crochets
331
  display_user_text = f"[{uploaded_filename_for_display}]" + (f" {prompt}" if prompt else "")
332
  elif media_type == 'youtube':
333
- # Précède par [YouTube], ajoute le prompt, puis l'URL sur une nouvelle ligne
334
  display_user_text = f"[YouTube]" + (f" {prompt}" if prompt else "") + f"\n{uploaded_filename_for_display}"
335
 
336
  user_history_entry = {
337
  'role': 'user',
338
- 'text': display_user_text, # Texte formaté pour l'affichage
339
- 'raw_text': prompt # Texte brut original pour futurs appels Gemini
340
- # Note: On ne stocke pas l'objet media_part dans la session
341
  }
342
  session['chat_history'].append(user_history_entry)
343
- session.modified = True # Marque la session comme modifiée
344
 
345
  # --- Préparer les 'parts' pour l'appel API Gemini ---
346
  current_gemini_parts = []
347
- # Ajouter le média (fichier uploadé ou URL YT) s'il existe
348
  if uploaded_media_part:
349
  current_gemini_parts.append(uploaded_media_part)
350
 
351
- # Préparer le prompt textuel final (peut être enrichi par la recherche web)
352
  final_prompt_for_gemini = prompt
353
 
354
- # --- Recherche Web (Optionnelle, uniquement pour les requêtes texte) ---
355
  if use_web_search and prompt and media_type == 'text':
356
  print(" Activation Recherche Web...")
357
  search_data = perform_web_search(prompt)
358
  if search_data:
359
  formatted_results = format_search_results(search_data)
360
- # Construit un prompt enrichi
361
  final_prompt_for_gemini = f"""Basé sur la question suivante et les informations web ci-dessous, fournis une réponse complète.
362
 
363
  Question Originale:
@@ -373,70 +412,59 @@ def chat_api():
373
  else:
374
  print(" Aucun résultat de recherche web trouvé ou pertinent.")
375
 
376
- # Ajouter la partie texte (originale ou enrichie) s'il y a du texte
377
  if final_prompt_for_gemini:
378
- current_gemini_parts.append(genai.types.Part(text=final_prompt_for_gemini))
 
379
 
380
- # Vérification de sécurité : il doit y avoir au moins une partie (média ou texte)
381
  if not current_gemini_parts:
382
  print("ERREUR: Aucune partie (média ou texte) à envoyer à Gemini.")
383
  raise ValueError("Impossible de traiter la requête : contenu vide.")
384
 
385
-
386
  # --- Appel à l'API Gemini ---
387
- # Prépare l'historique des messages PRÉCÉDENTS (texte seulement)
388
- gemini_history = prepare_gemini_history(session['chat_history'][:-1]) # Exclut le message actuel
389
- # Construit le contenu complet pour l'appel API
390
  contents_for_gemini = gemini_history + [{'role': 'user', 'parts': current_gemini_parts}]
391
 
392
  # Sélectionne le modèle : Pro pour média ou si avancé demandé, sinon Flash
393
- selected_model_name = MODEL_PRO if is_media_request or use_advanced else MODEL_FLASH
394
- print(f" Modèle sélectionné: {selected_model_name}")
395
-
396
- # Crée l'instance du modèle spécifique pour cette requête
397
- active_model = gemini_client.models.generate_content(
398
- model=selected_model_name,
399
- contents = contents_for_gemini, # Virgule ajoutée ici
400
- config = genai.types.GenerateContentConfig(
401
- system_instruction=SYSTEM_INSTRUCTION,
402
- safety_settings=[
403
-
404
- genai.types.SafetySetting(
405
- category=genai.types.HarmCategory.HARM_CATEGORY_HATE_SPEECH,
406
- threshold=genai.types.HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
407
- ),
408
- genai.types.SafetySetting(
409
- category=genai.types.HarmCategory.HARM_CATEGORY_HARASSMENT,
410
- threshold=genai.types.HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
411
- ),
412
- genai.types.SafetySetting(
413
- category=genai.types.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
414
- threshold=genai.types.HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
415
- ),
416
- genai.types.SafetySetting(
417
- category=genai.types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
418
- threshold=genai.types.HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
419
- )
420
- ]
421
- ),
422
  )
423
 
424
- print(f" Envoi de la requête à {selected_model_name} ({len(contents_for_gemini)} messages/tours)...")
425
- # Appel API (non-streamé pour correspondre au code précédent)
426
- response = active_model
427
 
428
  # --- Traitement de la Réponse ---
429
  response_text_raw = ""
430
  response_html = ""
431
  try:
432
- # Accède au texte de la réponse. Peut lever une exception si bloqué.
433
  response_text_raw = response.text
434
- except ValueError: # Typiquement levé si la réponse est bloquée par les filtres
435
- print(" ERREUR: La réponse de Gemini a été bloquée (probablement par les safety settings).")
436
- # Fournit les détails du blocage si disponibles
437
- try: print(f" Détails du blocage : {response.prompt_feedback}")
438
- except Exception: pass # Ignore si prompt_feedback n'est pas accessible
439
- response_text_raw = "Désolé, ma réponse a été bloquée car elle pourrait enfreindre les règles de sécurité."
 
 
 
440
  except Exception as resp_err: # Gère d'autres erreurs potentielles
441
  print(f" ERREUR inattendue lors de l'accès à response.text : {resp_err}")
442
  print(f" Réponse brute complète : {response}") # Log la réponse brute pour le debug
@@ -450,8 +478,8 @@ def chat_api():
450
  # --- Stocker la réponse de l'assistant et retourner au client ---
451
  assistant_history_entry = {
452
  'role': 'assistant',
453
- 'text': response_html, # HTML pour l'affichage via get_history
454
- 'raw_text': response_text_raw # Texte brut pour les futurs appels Gemini
455
  }
456
  session['chat_history'].append(assistant_history_entry)
457
  session.modified = True
@@ -460,25 +488,32 @@ def chat_api():
460
  return jsonify({'success': True, 'message': response_html})
461
 
462
  # --- Gestion des Erreurs spécifiques (Timeout, Fichier invalide, etc.) ---
463
- except (TimeoutError, ValueError, ConnectionError, FileNotFoundError) as e:
464
  error_message = f"Erreur lors du traitement de la requête: {e}"
465
  print(f"ERREUR (Traitement/Appel API): {error_message}")
466
- # Retire le dernier message utilisateur de l'historique en cas d'erreur pour éviter les boucles
 
 
 
467
  if session.get('chat_history'):
468
- session['chat_history'].pop()
469
- session.modified = True
470
- print(" Dernier message utilisateur retiré de l'historique après erreur.")
471
- return jsonify({'success': False, 'error': error_message}), 500 # Erreur serveur
 
 
472
 
473
  # --- Gestion des Erreurs Génériques/Inattendues ---
474
  except Exception as e:
475
  error_message = f"Une erreur interne inattendue est survenue: {e}"
476
- print(f"ERREUR CRITIQUE INATTENDUE: {error_message}", exc_info=True) # Log la traceback complète
 
477
  if session.get('chat_history'):
478
- session['chat_history'].pop()
479
- session.modified = True
480
- print(" Dernier message utilisateur retiré de l'historique après erreur inattendue.")
481
- return jsonify({'success': False, 'error': error_message}), 500
 
482
 
483
  # --- Nettoyage (Exécuté dans tous les cas : succès ou erreur) ---
484
  finally:
@@ -495,23 +530,23 @@ def clear_chat():
495
  session.pop('chat_history', None) # Supprime la clé de la session
496
  print("API: Historique de chat effacé via /clear.")
497
 
498
- # Détecte si la requête vient d'AJAX (fetch) pour répondre en JSON
499
  is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest' or \
500
  'application/json' in request.headers.get('Accept', '')
501
 
502
  if is_ajax:
503
  return jsonify({'success': True, 'message': 'Historique effacé.'})
504
  else:
505
- # Comportement pour une soumission de formulaire classique (moins probable ici)
506
  flash("Conversation effacée.", "info")
507
- return redirect(url_for('root')) # Redirige vers la page d'accueil
508
 
509
  # --- Démarrage de l'application Flask ---
510
 
511
  if __name__ == '__main__':
512
  print("Démarrage du serveur Flask...")
513
- # Utiliser un port différent si 5000 est déjà pris (ex: 5001)
514
- port = int(os.environ.get('PORT', 5001))
515
- # debug=True : Rechargement auto, messages d'erreur détaillés (NE PAS UTILISER EN PRODUCTION)
516
- # host='0.0.0.0' : Rend l'app accessible sur le réseau local
 
 
517
  app.run(debug=True, host='0.0.0.0', port=port)
 
3
  import mimetypes
4
  import time
5
  import re # Pour la détection d'URL YouTube
6
+ 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
 
38
  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,
53
+ threshold=types.HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
54
+ ),
55
+ types.SafetySetting(
56
+ category=types.HarmCategory.HARM_CATEGORY_HARASSMENT,
57
+ threshold=types.HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
58
+ ),
59
+ types.SafetySetting(
60
+ category=types.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
61
+ threshold=types.HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
62
+ ),
63
+ types.SafetySetting(
64
+ category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
65
+ threshold=types.HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
66
+ )
67
+ ]
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:
 
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
+
96
+ if all(models_found.values()):
97
+ print(f"Configuration Gemini effectuée. Modèles requis ({', '.join(required_models_prefixes)}) trouvés.")
98
  print(f"Instruction Système: {SYSTEM_INSTRUCTION}")
99
  GEMINI_CONFIGURED = True
100
  else:
101
+ missing = [req for req, found in models_found.items() if not found]
102
+ raise ValueError(f"Les modèles Gemini requis suivants (ou commençant par) sont manquants : {missing}")
103
 
104
 
105
  except Exception as e:
 
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
 
113
  def get_file_extension(filename):
 
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:
 
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
 
 
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
 
218
  def prepare_gemini_history(chat_history):
219
  """Convertit l'historique de session pour l'API Gemini (texte seulement)."""
 
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
 
234
  # --- Routes Flask ---
 
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():
 
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
 
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}'")
 
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:
 
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}")
463
+ block_reason = response.prompt_feedback.block_reason_message or "Raison non spécifiée"
464
+ response_text_raw = f"Désolé, ma réponse a été bloquée car elle pourrait enfreindre les règles de sécurité ({block_reason})."
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
 
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
 
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:
 
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:
 
539
  flash("Conversation effacée.", "info")
540
+ return redirect(url_for('root'))
541
 
542
  # --- Démarrage de l'application Flask ---
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)