Update app.py
Browse files
app.py
CHANGED
@@ -12,18 +12,22 @@ 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 |
|
@@ -33,16 +37,16 @@ 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-
|
37 |
-
MODEL_PRO = 'gemini-
|
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 |
-
|
45 |
-
|
46 |
|
47 |
GEMINI_CONFIGURED = False
|
48 |
gemini_client = None # Client API pour les opérations sur les fichiers (upload vidéo)
|
@@ -55,7 +59,7 @@ try:
|
|
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 |
-
|
59 |
|
60 |
# Vérifie si les modèles requis sont disponibles
|
61 |
print("Vérification des modèles Gemini disponibles...")
|
@@ -69,6 +73,7 @@ try:
|
|
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.")
|
@@ -94,13 +99,14 @@ def is_youtube_url(url):
|
|
94 |
return False
|
95 |
# Regex simple pour les formats courants d'URL YouTube
|
96 |
youtube_regex = re.compile(
|
97 |
-
r'(https?://)?(www
|
98 |
-
r'(youtube|youtu|youtube-nocookie)
|
99 |
-
r'(watch
|
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.
|
@@ -118,7 +124,7 @@ def upload_video_with_polling(filepath, mime_type, max_wait_seconds=300, poll_in
|
|
118 |
|
119 |
start_time = time.time()
|
120 |
# Boucle de polling tant que l'état est "PROCESSING"
|
121 |
-
while video_file.state ==
|
122 |
elapsed_time = time.time() - start_time
|
123 |
# Vérifie le timeout
|
124 |
if elapsed_time > max_wait_seconds:
|
@@ -130,11 +136,11 @@ def upload_video_with_polling(filepath, mime_type, max_wait_seconds=300, poll_in
|
|
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 ==
|
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 ==
|
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 |
|
@@ -155,6 +161,7 @@ def upload_video_with_polling(filepath, mime_type, max_wait_seconds=300, poll_in
|
|
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")
|
@@ -171,14 +178,14 @@ 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 = ["
|
175 |
if data.get('organic'):
|
176 |
for item in data['organic'][:3]:
|
177 |
results.append(f"- {item.get('title', '')}: {item.get('snippet', '')}")
|
178 |
-
|
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 = []
|
@@ -273,12 +280,12 @@ def chat_api():
|
|
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 =
|
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 =
|
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
|
@@ -296,8 +303,8 @@ def chat_api():
|
|
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 =
|
300 |
-
file_data=
|
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
|
@@ -350,22 +357,22 @@ def chat_api():
|
|
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(
|
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:
|
@@ -384,36 +391,35 @@ Réponse:"""
|
|
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 |
-
|
388 |
-
|
389 |
-
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
|
|
|
|
|
|
|
409 |
)
|
410 |
-
]
|
411 |
-
),
|
412 |
-
)
|
413 |
|
414 |
print(f" Envoi de la requête à {selected_model_name} ({len(contents_for_gemini)} messages/tours)...")
|
415 |
-
|
416 |
-
#(non-streamé pour correspondre au code précédent)
|
417 |
response = active_model
|
418 |
|
419 |
# --- Traitement de la Réponse ---
|
@@ -480,7 +486,6 @@ Réponse:"""
|
|
480 |
except OSError as e:
|
481 |
print(f" ERREUR lors de la suppression du fichier temporaire '{filepath_to_delete}': {e}")
|
482 |
|
483 |
-
|
484 |
@app.route('/clear', methods=['POST'])
|
485 |
def clear_chat():
|
486 |
"""Efface l'historique de chat dans la session."""
|
@@ -498,8 +503,8 @@ def clear_chat():
|
|
498 |
flash("Conversation effacée.", "info")
|
499 |
return redirect(url_for('root')) # Redirige vers la page d'accueil
|
500 |
|
501 |
-
|
502 |
# --- Démarrage de l'application Flask ---
|
|
|
503 |
if __name__ == '__main__':
|
504 |
print("Démarrage du serveur Flask...")
|
505 |
# Utiliser un port différent si 5000 est déjà pris (ex: 5001)
|
|
|
12 |
import markdown # Pour convertir la réponse Markdown en HTML
|
13 |
|
14 |
# --- Configuration Initiale ---
|
15 |
+
|
16 |
load_dotenv() # Charge les variables depuis .env
|
17 |
|
18 |
app = Flask(__name__)
|
19 |
+
|
20 |
# Clé secrète FORTEMENT recommandée pour la sécurité des sessions
|
21 |
app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'une-cle-secrete-tres-difficile-a-deviner')
|
22 |
|
23 |
# Configuration pour les uploads
|
24 |
UPLOAD_FOLDER = 'temp'
|
25 |
+
|
26 |
# Extensions autorisées (incluant vidéo)
|
27 |
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'mp4', 'mov', 'avi', 'mkv', 'webm'}
|
28 |
VIDEO_EXTENSIONS = {'mp4', 'mov', 'avi', 'mkv', 'webm'} # Pour identifier les vidéos
|
29 |
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
30 |
+
|
31 |
# Augmenter la limite pour les vidéos (ex: 100MB) - Ajustez si nécessaire
|
32 |
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024
|
33 |
|
|
|
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-1.5-flash-latest'
|
41 |
+
MODEL_PRO = 'gemini-1.5-pro-latest' # 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)
|
|
|
59 |
# Initialise le client pour les opérations sur les fichiers (upload vidéo avec polling)
|
60 |
gemini_client = genai.Client(api_key=gemini_api_key)
|
61 |
# Configure également l'espace de noms global pour GenerativeModel, etc.
|
62 |
+
|
63 |
|
64 |
# Vérifie si les modèles requis sont disponibles
|
65 |
print("Vérification des modèles Gemini disponibles...")
|
|
|
73 |
missing = [model for model in required_models if model not in models_list]
|
74 |
raise ValueError(f"Les modèles Gemini requis suivants sont manquants: {missing}")
|
75 |
|
76 |
+
|
77 |
except Exception as e:
|
78 |
print(f"ERREUR Critique lors de la configuration initiale de Gemini : {e}")
|
79 |
print("L'application fonctionnera sans les fonctionnalités IA.")
|
|
|
99 |
return False
|
100 |
# Regex simple pour les formats courants d'URL YouTube
|
101 |
youtube_regex = re.compile(
|
102 |
+
r'(https?://)?(www.)?' # Protocole et www optionnels
|
103 |
+
r'(youtube|youtu|youtube-nocookie).(com|be)/' # Domaines youtube.com, youtu.be, etc.
|
104 |
+
r'(watch?v=|embed/|v/|.+?v=)?' # Différents chemins possibles
|
105 |
r'([^&=%\?]{11})') # L'ID vidéo de 11 caractères
|
106 |
return youtube_regex.match(url) is not None
|
107 |
|
108 |
# --- Fonction d'Upload Vidéo avec Polling ---
|
109 |
+
|
110 |
def upload_video_with_polling(filepath, mime_type, max_wait_seconds=300, poll_interval=10):
|
111 |
"""
|
112 |
Upload une vidéo via client.files.upload et attend son traitement.
|
|
|
124 |
|
125 |
start_time = time.time()
|
126 |
# Boucle de polling tant que l'état est "PROCESSING"
|
127 |
+
while video_file.state == genai.types.FileState.PROCESSING:
|
128 |
elapsed_time = time.time() - start_time
|
129 |
# Vérifie le timeout
|
130 |
if elapsed_time > max_wait_seconds:
|
|
|
136 |
video_file = gemini_client.files.get(name=video_file.name)
|
137 |
|
138 |
# Vérifie l'état final après la boucle
|
139 |
+
if video_file.state == genai.types.FileState.FAILED:
|
140 |
print(f"ERREUR: Le traitement de la vidéo a échoué. État: {video_file.state.name}")
|
141 |
raise ValueError("Le traitement de la vidéo a échoué côté serveur.")
|
142 |
|
143 |
+
if video_file.state == genai.types.FileState.ACTIVE:
|
144 |
print(f"Traitement vidéo terminé avec succès: {video_file.uri}")
|
145 |
return video_file # Retourne l'objet fichier SDK réussi
|
146 |
|
|
|
161 |
raise # Relance l'exception originale pour qu'elle soit gérée par l'appelant
|
162 |
|
163 |
# --- Fonctions de Recherche Web (inchangées - implémentez si nécessaire) ---
|
164 |
+
|
165 |
def perform_web_search(query):
|
166 |
"""Effectue une recherche web via l'API Serper (Exemple)."""
|
167 |
serper_api_key = os.getenv("SERPER_API_KEY")
|
|
|
178 |
"""Met en forme les résultats de recherche (Exemple)."""
|
179 |
if not data: return "Aucun résultat de recherche web pertinent."
|
180 |
# ... (votre implémentation du formatage) ...
|
181 |
+
results = ["Résultats Web:"]
|
182 |
if data.get('organic'):
|
183 |
for item in data['organic'][:3]:
|
184 |
results.append(f"- {item.get('title', '')}: {item.get('snippet', '')}")
|
185 |
+
return "\n".join(results)
|
|
|
186 |
|
187 |
# --- Préparation Historique (inchangé) ---
|
188 |
+
|
189 |
def prepare_gemini_history(chat_history):
|
190 |
"""Convertit l'historique de session pour l'API Gemini (texte seulement)."""
|
191 |
gemini_history = []
|
|
|
280 |
# Appel bloquant qui attend le traitement
|
281 |
processed_media_file = upload_video_with_polling(filepath, mime_type)
|
282 |
# Crée le Part Gemini à partir de l'objet File retourné
|
283 |
+
uploaded_media_part = genai.types.Part(file_data=processed_media_file)
|
284 |
else:
|
285 |
print(" Traitement FICHIER standard...")
|
286 |
# Utilise l'upload global plus simple pour les non-vidéos
|
287 |
processed_media_file = genai.upload_file(path=filepath, mime_type=mime_type)
|
288 |
+
uploaded_media_part = genai.types.Part(file_data=processed_media_file)
|
289 |
print(f" Part Média ({media_type}) créé: {processed_media_file.uri}")
|
290 |
|
291 |
# 2. Sinon, traiter l'URL YouTube si fournie et valide
|
|
|
303 |
youtube_uri = youtube_url # Utilise l'URL validée comme URI
|
304 |
# Crée un Part FileData directement à partir de l'URI
|
305 |
# On peut spécifier un mime_type générique, Gemini gère les liens YT
|
306 |
+
uploaded_media_part = genai.types.Part(
|
307 |
+
file_data=genai.types.FileData(file_uri=youtube_uri, mime_type="video/mp4")
|
308 |
)
|
309 |
print(f" Part YouTube créé pour: {youtube_uri}")
|
310 |
# Ajoute un prompt par défaut si l'utilisateur n'en a pas mis
|
|
|
357 |
# Construit un prompt enrichi
|
358 |
final_prompt_for_gemini = f"""Basé sur la question suivante et les informations web ci-dessous, fournis une réponse complète.
|
359 |
|
360 |
+
Question Originale:
|
361 |
+
"{prompt}"
|
362 |
|
363 |
+
Informations Web Pertinentes:
|
364 |
+
--- DEBUT RESULTATS WEB ---
|
365 |
+
{formatted_results}
|
366 |
+
--- FIN RESULTATS WEB ---
|
367 |
|
368 |
+
Réponse:"""
|
369 |
print(" Prompt enrichi avec les résultats web.")
|
370 |
else:
|
371 |
print(" Aucun résultat de recherche web trouvé ou pertinent.")
|
372 |
|
373 |
# Ajouter la partie texte (originale ou enrichie) s'il y a du texte
|
374 |
if final_prompt_for_gemini:
|
375 |
+
current_gemini_parts.append(genai.types.Part(text=final_prompt_for_gemini))
|
376 |
|
377 |
# Vérification de sécurité : il doit y avoir au moins une partie (média ou texte)
|
378 |
if not current_gemini_parts:
|
|
|
391 |
print(f" Modèle sélectionné: {selected_model_name}")
|
392 |
|
393 |
# Crée l'instance du modèle spécifique pour cette requête
|
394 |
+
active_model = gemini_client.models.generate_content(
|
395 |
+
model_name=selected_model_name,
|
396 |
+
contents = contents_for_gemini, # Virgule ajoutée ici
|
397 |
+
config = genai.types.GenerateContentConfig(
|
398 |
+
system_instruction=SYSTEM_INSTRUCTION,
|
399 |
+
safety_settings=[
|
400 |
+
|
401 |
+
genai.types.SafetySetting(
|
402 |
+
category=genai.types.HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
403 |
+
threshold=genai.types.HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
|
404 |
+
),
|
405 |
+
genai.types.SafetySetting(
|
406 |
+
category=genai.types.HarmCategory.HARM_CATEGORY_HARASSMENT,
|
407 |
+
threshold=genai.types.HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
|
408 |
+
),
|
409 |
+
genai.types.SafetySetting(
|
410 |
+
category=genai.types.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
411 |
+
threshold=genai.types.HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
|
412 |
+
),
|
413 |
+
genai.types.SafetySetting(
|
414 |
+
category=genai.types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
415 |
+
threshold=genai.types.HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
|
416 |
+
)
|
417 |
+
]
|
418 |
+
),
|
419 |
)
|
|
|
|
|
|
|
420 |
|
421 |
print(f" Envoi de la requête à {selected_model_name} ({len(contents_for_gemini)} messages/tours)...")
|
422 |
+
# Appel API (non-streamé pour correspondre au code précédent)
|
|
|
423 |
response = active_model
|
424 |
|
425 |
# --- Traitement de la Réponse ---
|
|
|
486 |
except OSError as e:
|
487 |
print(f" ERREUR lors de la suppression du fichier temporaire '{filepath_to_delete}': {e}")
|
488 |
|
|
|
489 |
@app.route('/clear', methods=['POST'])
|
490 |
def clear_chat():
|
491 |
"""Efface l'historique de chat dans la session."""
|
|
|
503 |
flash("Conversation effacée.", "info")
|
504 |
return redirect(url_for('root')) # Redirige vers la page d'accueil
|
505 |
|
|
|
506 |
# --- Démarrage de l'application Flask ---
|
507 |
+
|
508 |
if __name__ == '__main__':
|
509 |
print("Démarrage du serveur Flask...")
|
510 |
# Utiliser un port différent si 5000 est déjà pris (ex: 5001)
|