Sidoineko commited on
Commit
9cd3369
·
verified ·
1 Parent(s): d7b2a46

Update src/streamlit_app_stable.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app_stable.py +68 -124
src/streamlit_app_stable.py CHANGED
@@ -4,11 +4,13 @@ import io
4
  from PIL import Image
5
  import requests
6
  import torch
7
- import google.generativeai as genai # Importé mais non utilisé dans ce code
8
  import gc
9
  import time
10
  import sys
11
  import psutil
 
 
 
12
 
13
  # --- Configuration de la page ---
14
  st.set_page_config(
@@ -19,6 +21,7 @@ st.set_page_config(
19
  )
20
 
21
  # --- Initialisation des variables de session ---
 
22
  if 'model_loaded' not in st.session_state:
23
  st.session_state.model_loaded = False
24
  if 'model' not in st.session_state:
@@ -33,54 +36,50 @@ if 'language' not in st.session_state:
33
  st.session_state.language = "fr"
34
  if 'load_attempt_count' not in st.session_state:
35
  st.session_state.load_attempt_count = 0
 
 
36
 
37
  # --- Fonctions d'aide système ---
38
 
39
  def check_model_health():
40
  """Vérifie si le modèle et le processeur sont chargés et semblent opérationnels."""
41
  try:
42
- # Vérifie si les objets existent et si le modèle a un attribut 'device' (ce qui suggère une initialisation)
43
  return (st.session_state.model is not None and
44
  st.session_state.processor is not None and
45
  hasattr(st.session_state.model, 'device'))
46
  except Exception:
47
- # Si une exception survient (ex: AttributeError si model ou processor est None)
48
  return False
49
 
50
  def diagnose_loading_issues():
51
  """Diagnostique les problèmes potentiels avant le chargement du modèle."""
52
  issues = []
53
 
54
- # Vérification RAM
55
  try:
56
  ram = psutil.virtual_memory()
57
  ram_gb = ram.total / (1024**3)
58
- if ram_gb < 8: # Recommandation générale pour les gros modèles
59
  issues.append(f"⚠️ RAM faible: {ram_gb:.1f}GB (recommandé: 8GB+ pour ce modèle)")
60
  except Exception as e:
61
  issues.append(f"⚠️ Impossible de vérifier la RAM : {e}")
62
 
63
- # Vérification Espace Disque (utile si le modèle est téléchargé localement)
64
  try:
65
  disk_usage = psutil.disk_usage('/')
66
  disk_gb = disk_usage.free / (1024**3)
67
- if disk_gb < 10: # Espace nécessaire pour télécharger le modèle (environ 4-5Go pour Gemma 3n)
68
  issues.append(f"⚠️ Espace disque faible: {disk_gb:.1f}GB libre sur '/'")
69
  except Exception as e:
70
  issues.append(f"⚠️ Impossible de vérifier l'espace disque : {e}")
71
 
72
- # Vérification connexion Hugging Face
73
  try:
74
  requests.get("https://huggingface.co", timeout=5)
75
  except requests.exceptions.RequestException:
76
  issues.append("⚠️ Problème de connexion à Hugging Face Hub")
77
 
78
- # Vérification CUDA
79
  if torch.cuda.is_available():
80
  try:
81
  gpu_memory = torch.cuda.get_device_properties(0).total_memory / (1024**3)
82
- if gpu_memory < 4: # Recommandation minimale pour ce type de modèle
83
- issues.append(f"⚠️ GPU mémoire faible: {gpu_memory:.1f}GB (recommandé: 6GB+ pour ce modèle)")
84
  except Exception as e:
85
  issues.append(f"⚠️ Erreur lors de la vérification de la mémoire GPU : {e}")
86
  else:
@@ -88,14 +87,13 @@ def diagnose_loading_issues():
88
 
89
  return issues
90
 
91
- def resize_image_if_needed(image, max_size=(800, 800)):
92
  """Redimensionne l'image si ses dimensions dépassent max_size."""
93
  original_size = image.size
94
  if image.size[0] > max_size[0] or image.size[1] > max_size[1]:
95
- # Utiliser Image.Resampling.LANCZOS pour une meilleure qualité de redimensionnement
96
  image.thumbnail(max_size, Image.Resampling.LANCZOS)
97
- return image, True # Retourne l'image redimensionnée et un booléen indiquant que le redimensionnement a eu lieu
98
- return image, False # Retourne l'image originale et False
99
 
100
  def afficher_ram_disponible(context=""):
101
  """Affiche l'utilisation de la RAM de manière lisible."""
@@ -109,7 +107,6 @@ def afficher_ram_disponible(context=""):
109
  st.write(f"💾 Impossible d'afficher l'utilisation de la RAM {context}: {e}")
110
 
111
  # --- Gestion des traductions ---
112
-
113
  def t(key):
114
  """Fonction pour gérer les traductions."""
115
  translations = {
@@ -144,18 +141,22 @@ def t(key):
144
  "model_status": "AI Model Status"
145
  }
146
  }
147
- # Retourne la traduction si elle existe, sinon retourne la clé elle-même
148
  return translations[st.session_state.language].get(key, key)
149
 
150
  # --- Fonctions de chargement et d'analyse du modèle ---
151
 
152
- # IMPORTANT: Sur Hugging Face Spaces, le chemin local D:/Dev/model_gemma n'existera PAS.
153
- # Le code se rabattra automatiquement sur le chargement depuis le Hub Hugging Face.
154
- # Si vous exécutez ceci localement et que vous avez téléchargé le modèle,
155
- # assurez-vous que le chemin est correct et accessible.
156
  MODEL_ID_LOCAL = "D:/Dev/model_gemma" # Path local (pour votre machine)
157
  MODEL_ID_HF = "google/gemma-3n-E4B-it" # ID du modèle sur Hugging Face Hub
158
 
 
 
 
 
 
 
 
 
 
159
  def load_model():
160
  """
161
  Charge le modèle Gemma 3n et son processeur associé.
@@ -163,11 +164,9 @@ def load_model():
163
  Gère les erreurs et les tentatives de chargement.
164
  """
165
  try:
166
- # Importation des bibliothèques nécessaires UNIQUEMENT lors du chargement du modèle
167
- # pour éviter des erreurs si elles ne sont pas installées dans l'environnement de base
168
  from transformers import AutoProcessor, Gemma3nForConditionalGeneration
169
 
170
- # Limiter les tentatives de chargement pour éviter les boucles infinies en cas de problème persistant
171
  if st.session_state.load_attempt_count >= 3:
172
  st.error("❌ Trop de tentatives de chargement ont échoué. Veuillez vérifier votre configuration et redémarrer l'application.")
173
  return None, None
@@ -180,19 +179,15 @@ def load_model():
180
  for issue in issues:
181
  st.write(issue)
182
 
183
- # Libérer la mémoire GPU si disponible et si nécessaire
184
  gc.collect()
185
  if torch.cuda.is_available():
186
  torch.cuda.empty_cache()
187
 
188
  processor = None
189
  model = None
190
-
191
- # --- Stratégie de chargement : Local d'abord, puis Hub ---
192
-
193
- # Vérifier si le chemin local existe et s'il contient des fichiers de modèle
194
- local_model_found = os.path.exists(MODEL_ID_LOCAL) and \
195
- os.path.exists(os.path.join(MODEL_ID_LOCAL, "config.json"))
196
 
197
  if local_model_found:
198
  try:
@@ -200,42 +195,38 @@ def load_model():
200
  processor = AutoProcessor.from_pretrained(MODEL_ID_LOCAL, trust_remote_code=True)
201
  model = Gemma3nForConditionalGeneration.from_pretrained(
202
  MODEL_ID_LOCAL,
203
- torch_dtype=torch.bfloat16, # Recommandé pour Gemma 3n si le matériel le supporte
204
  trust_remote_code=True,
205
- low_cpu_mem_usage=True, # Tente de réduire l'utilisation de la RAM CPU pendant le chargement
206
- device_map="cpu" # Forcer l'utilisation du CPU. Changer en "auto" si vous avez un GPU configuré.
207
  )
208
  st.success("✅ Modèle chargé avec succès depuis le dossier local.")
209
  st.session_state.model_status = "Chargé (Local)"
210
  except Exception as e:
211
  st.warning(f"⚠️ Échec du chargement depuis le local ({e}). Tentative depuis Hugging Face Hub...")
212
- # Si le chargement local échoue malgré l'existence du dossier, on tente le Hub.
213
- # On ne met pas à jour model et processor ici pour permettre la tentative Hub.
214
 
215
- # Si le modèle n'a pas été chargé localement, tenter depuis Hugging Face Hub
216
- if model is None:
217
  try:
218
  st.info(f"Chargement du modèle depuis Hugging Face Hub : {MODEL_ID_HF}...")
219
  processor = AutoProcessor.from_pretrained(MODEL_ID_HF, trust_remote_code=True)
220
  model = Gemma3nForConditionalGeneration.from_pretrained(
221
  MODEL_ID_HF,
222
- torch_dtype=torch.bfloat16,
223
  trust_remote_code=True,
224
  low_cpu_mem_usage=True,
225
- device_map="cpu" # Forcer l'utilisation du CPU
226
  )
227
  st.success(f"✅ Modèle chargé avec succès depuis Hugging Face Hub ({MODEL_ID_HF}).")
228
  st.session_state.model_status = "Chargé (Hub)"
229
  except Exception as e:
230
  st.error(f"❌ Échec du chargement du modèle depuis Hugging Face Hub : {e}")
231
- return None, None # Échec final
232
 
233
- # Mettre à jour l'état de la session si le chargement a réussi
234
  st.session_state.model = model
235
  st.session_state.processor = processor
236
  st.session_state.model_loaded = True
237
  st.session_state.model_load_time = time.time()
238
- st.session_state.load_attempt_count = 0 # Réinitialiser le compteur si le chargement réussit
239
 
240
  return model, processor
241
 
@@ -256,11 +247,9 @@ def analyze_image_multilingual(image, prompt_text=""):
256
  return None
257
 
258
  try:
259
- # S'assurer que l'image est en mode RGB
260
  if image.mode != 'RGB':
261
  image = image.convert('RGB')
262
 
263
- # Préparation du prompt par défaut s'il n'est pas fourni
264
  if not prompt_text:
265
  prompt_text = """Analyse cette image de plante et fournis un diagnostic complet :
266
  1. **État général de la plante :** Décris son apparence globale et sa vitalité.
@@ -270,39 +259,31 @@ def analyze_image_multilingual(image, prompt_text=""):
270
  5. **Recommandations de traitement :** Propose des solutions concrètes et adaptées.
271
  6. **Conseils préventifs :** Donne des astuces pour éviter que le problème ne revienne.
272
 
273
- Réponds de manière structurée et claire en français. Utilise des termes simples et compréhensibles pour un jardinier."""
274
 
275
- # Préparation des entrées pour le modèle
276
- # Le processeur s'attend à une liste d'images si plusieurs sont passées, même s'il n'y en a qu'une.
277
  inputs = st.session_state.processor(
278
  images=[image],
279
  text=prompt_text,
280
  return_tensors="pt"
281
  )
282
 
283
- # Déplacer les tensors sur le bon device (CPU dans notre cas, ou GPU si `device_map="auto"` était utilisé)
284
  inputs = {key: val.to(st.session_state.model.device) for key, val in inputs.items()}
285
 
286
- # Générer la réponse avec le modèle
287
  with st.spinner("🔍 Analyse d'image en cours..."):
288
  outputs = st.session_state.model.generate(
289
  **inputs,
290
- max_new_tokens=512, # Nombre maximal de nouveaux tokens à générer
291
- do_sample=True, # Permet la génération stochastique
292
- temperature=0.7, # Contrôle la créativité (plus bas = plus déterministe)
293
- top_p=0.9 # Stratégie d'échantillonnage pour contrôler la diversité
294
  )
295
 
296
- # Décoder la réponse générée
297
  response = st.session_state.processor.decode(outputs[0], skip_special_tokens=True)
298
 
299
- # Extraire uniquement la partie générée par le modèle, en retirant le prompt initial
300
- # Ceci peut nécessiter un ajustement si le modèle répète le prompt ou s'y réfère différemment
301
  if prompt_text.strip() in response:
302
  response_only = response.split(prompt_text.strip())[-1].strip()
303
  else:
304
- # Si le prompt n'est pas exactement dans la réponse, essayons de trouver une partie logique
305
- response_only = response.strip() # Utiliser la réponse brute si split échoue
306
 
307
  return response_only
308
 
@@ -320,30 +301,27 @@ def analyze_text_multilingual(text_description):
320
  return None
321
 
322
  try:
323
- # Préparation du prompt pour l'analyse textuelle
324
  prompt = f"""Analyse la description des symptômes de cette plante et fournis un diagnostic détaillé :
325
 
326
  **Description des symptômes :**
327
  {text_description}
328
 
329
- **Instructions pour le diagnostic :**
330
- 1. **Diagnostic probable :** Quel est le problème principal (maladie, carence, etc.) ?
331
- 2. **Causes possibles :** Quelles sont les raisons derrière ces symptômes ?
332
- 3. **Recommandations de traitement :** Quels traitements sont les plus adaptés ?
333
- 4. **Conseils de soins préventifs :** Comment éviter que le problème ne se reproduise ?
334
 
335
- Réponds de manière structurée, claire et en français. Ne répète pas la description des symptômes dans ta réponse."""
336
 
337
- # Préparation des entrées pour le modèle (sans image ici)
338
  inputs = st.session_state.processor(
339
  text=prompt,
340
  return_tensors="pt"
341
  )
342
 
343
- # Déplacer les tensors sur le bon device
344
  inputs = {key: val.to(st.session_state.model.device) for key, val in inputs.items()}
345
 
346
- # Générer la réponse
347
  with st.spinner("🔍 Analyse textuelle en cours..."):
348
  outputs = st.session_state.model.generate(
349
  **inputs,
@@ -353,10 +331,8 @@ Réponds de manière structurée, claire et en français. Ne répète pas la des
353
  top_p=0.9
354
  )
355
 
356
- # Décoder la réponse
357
  response = st.session_state.processor.decode(outputs[0], skip_special_tokens=True)
358
 
359
- # Extraire uniquement la partie générée par le modèle
360
  if prompt.strip() in response:
361
  response_only = response.split(prompt.strip())[-1].strip()
362
  else:
@@ -370,7 +346,6 @@ Réponds de manière structurée, claire et en français. Ne répète pas la des
370
 
371
  # --- Interface Utilisateur Streamlit ---
372
 
373
- # Titre principal et sous-titre
374
  st.title(t("title"))
375
  st.markdown(t("subtitle"))
376
 
@@ -378,7 +353,6 @@ st.markdown(t("subtitle"))
378
  with st.sidebar:
379
  st.header(t("config_title"))
380
 
381
- # Option de sélection de langue
382
  lang_selector_options = ["Français", "English"]
383
  current_lang_index = 0 if st.session_state.language == "fr" else 1
384
  language_selected = st.selectbox(
@@ -387,52 +361,41 @@ with st.sidebar:
387
  index=current_lang_index,
388
  help="Sélectionnez la langue de l'interface et des réponses."
389
  )
390
- # Mise à jour de la langue de la session si elle change
391
  st.session_state.language = "fr" if language_selected == "Français" else "en"
392
 
393
  st.divider()
394
 
395
- # Section pour la gestion du modèle IA
396
  st.header(t("model_status"))
397
 
398
  if st.session_state.model_loaded and check_model_health():
399
  st.success("✅ Modèle chargé et fonctionnel")
400
  st.write(f"**Statut :** `{st.session_state.model_status}`")
401
  if st.session_state.model_load_time:
402
- # Formatte le temps de chargement pour une meilleure lisibilité
403
  load_time_str = time.strftime('%H:%M:%S', time.localtime(st.session_state.model_load_time))
404
  st.write(f"**Heure de chargement :** {load_time_str}")
405
 
406
- # Bouton pour décharger et recharger le modèle (utile pour tester les chargements)
407
  if st.button("🔄 Recharger le modèle", type="secondary"):
408
  st.session_state.model_loaded = False
409
  st.session_state.model = None
410
  st.session_state.processor = None
411
  st.session_state.model_status = "Non chargé"
412
- st.session_state.load_attempt_count = 0 # Réinitialiser le compteur pour une nouvelle tentative
413
  st.info("Modèle déchargé. Cliquez sur 'Charger le modèle IA' pour le recharger.")
414
- # Sur Hugging Face Spaces, il n'est pas nécessaire de faire `st.rerun()` après ce bouton,
415
- # car le redémarrage se fait via l'interface du Space si nécessaire.
416
  else:
417
  st.warning("⚠️ Modèle IA non chargé")
418
 
419
- # Bouton pour lancer le chargement du modèle
420
  if st.button(t("load_model"), type="primary"):
421
  with st.spinner("🔄 Chargement du modèle IA en cours..."):
422
  model_loaded_success = load_model()
423
  if model_loaded_success[0] is not None and model_loaded_success[1] is not None:
424
  st.success("✅ Modèle IA chargé avec succès !")
425
- # `st.rerun()` est utile si vous voulez rafraîchir l'interface immédiatement après le chargement
426
- # mais il peut causer des boucles si mal géré. Souvent, un redémarrage du Space est plus propre.
427
- # st.rerun()
428
  else:
429
  st.error("❌ Échec du chargement du modèle IA.")
430
 
431
- # Informations sur l'utilisation des ressources
432
  st.divider()
433
  st.subheader("📊 Ressources Système")
434
  afficher_ram_disponible()
435
- # Afficher l'utilisation GPU si disponible
436
  if torch.cuda.is_available():
437
  try:
438
  gpu_memory = torch.cuda.memory_allocated(0) / (1024**3)
@@ -450,13 +413,12 @@ with tab1: # Onglet Analyse d'Image
450
  st.header(t("image_analysis_title"))
451
  st.markdown(t("image_analysis_desc"))
452
 
453
- # Choix entre upload et capture webcam
454
  capture_option = st.radio(
455
  "Choisissez votre méthode de capture :",
456
  ["📁 Upload d'image" if st.session_state.language == "fr" else "📁 Upload Image",
457
  "📷 Capture par webcam" if st.session_state.language == "fr" else "📷 Webcam Capture"],
458
  horizontal=True,
459
- key="image_capture_method" # Clé unique pour le widget radio
460
  )
461
 
462
  uploaded_file = None
@@ -466,20 +428,18 @@ with tab1: # Onglet Analyse d'Image
466
  uploaded_file = st.file_uploader(
467
  t("choose_image"),
468
  type=['png', 'jpg', 'jpeg'],
469
- help="Formats acceptés : PNG, JPG, JPEG (taille max recommandée : 10MB pour une meilleure performance)."
470
  )
471
- # Limiter la taille du fichier uploadé pour une meilleure expérience
472
- if uploaded_file is not None and uploaded_file.size > 10 * 1024 * 1024: # 10MB
473
  st.warning("Le fichier est très volumineux. Il est recommandé d'utiliser des images de taille raisonnable pour une analyse plus rapide.")
474
  else:
475
  st.markdown("**📷 Capture d'image par webcam**")
476
  st.info("Positionnez votre plante malade devant la webcam et cliquez sur 'Prendre une photo'.")
477
  captured_image = st.camera_input(
478
  "Prendre une photo de la plante",
479
- key="webcam_photo" # Clé unique pour le widget caméra
480
  )
481
 
482
- # Traitement de l'image si un fichier est uploadé ou capturé
483
  image_to_analyze = None
484
  if uploaded_file is not None:
485
  try:
@@ -492,11 +452,9 @@ with tab1: # Onglet Analyse d'Image
492
  except Exception as e:
493
  st.error(f"❌ Erreur lors du traitement de l'image capturée : {e}")
494
 
495
- # Affichage de l'image et du bouton d'analyse si une image est prête
496
  if image_to_analyze is not None:
497
- # Redimensionner l'image si nécessaire pour l'affichage et l'analyse
498
  original_size = image_to_analyze.size
499
- resized_image, was_resized = resize_image_if_needed(image_to_analyze, max_size=(1024, 1024)) # Augmentation de la taille max pour l'analyse
500
 
501
  col1, col2 = st.columns([1, 1])
502
  with col1:
@@ -505,7 +463,6 @@ with tab1: # Onglet Analyse d'Image
505
  st.info(f"ℹ️ Image redimensionnée de {original_size} à {resized_image.size} pour l'analyse.")
506
 
507
  with col2:
508
- # Options d'analyse seulement si le modèle est chargé
509
  if st.session_state.model_loaded and check_model_health():
510
  st.subheader("Options d'analyse")
511
  analysis_type = st.selectbox(
@@ -526,9 +483,8 @@ with tab1: # Onglet Analyse d'Image
526
  )
527
 
528
  if st.button("🔍 Analyser l'image", type="primary", key="analyze_image_button"):
529
- # Déterminer le prompt basé sur la sélection de l'utilisateur
530
- final_prompt = custom_prompt_input.strip() # Utiliser le prompt personnalisé s'il existe
531
- if not final_prompt: # Sinon, utiliser un prompt par défaut basé sur le type d'analyse
532
  if analysis_type.startswith("Diagnostic complet"):
533
  final_prompt = """Analyse cette image de plante et fournis un diagnostic complet :
534
  1. **État général de la plante :** Décris son apparence globale et sa vitalité.
@@ -538,7 +494,7 @@ with tab1: # Onglet Analyse d'Image
538
  5. **Recommandations de traitement :** Propose des solutions concrètes et adaptées.
539
  6. **Conseils préventifs :** Donne des astuces pour éviter que le problème ne revienne.
540
 
541
- Réponds de manière structurée et claire en français. Utilise des termes simples et compréhensibles pour un jardinier."""
542
  elif analysis_type.startswith("Identification et diagnostic de maladies"):
543
  final_prompt = """Diagnostique cette plante en te concentrant sur les maladies et parasites :
544
  1. Identifie les symptômes visuels spécifiques aux maladies ou parasites.
@@ -547,7 +503,7 @@ Réponds de manière structurée et claire en français. Utilise des termes simp
547
  4. Propose des traitements ciblés et des méthodes de lutte.
548
 
549
  Réponds en français de manière structurée."""
550
- else: # Conseils de soins et d'entretien
551
  final_prompt = """Analyse cette plante et donne des conseils de soins détaillés :
552
  1. État général de la plante : Évalue sa santé actuelle.
553
  2. Besoins spécifiques : Précise ses besoins en eau, lumière, nutriments et substrat.
@@ -556,13 +512,12 @@ Réponds en français de manière structurée."""
556
 
557
  Réponds en français de manière structurée."""
558
 
559
- # Appel de la fonction d'analyse
560
  analysis_result = analyze_image_multilingual(resized_image, prompt_text=final_prompt)
561
 
562
  if analysis_result:
563
  st.success("✅ Analyse terminée !")
564
  st.markdown("### 📋 Résultats de l'analyse")
565
- st.markdown(analysis_result) # Afficher le résultat Markdown
566
  else:
567
  st.error("❌ Échec de l'analyse de l'image.")
568
  else:
@@ -572,18 +527,15 @@ with tab2: # Onglet Analyse de Texte
572
  st.header(t("text_analysis_title"))
573
  st.markdown(t("text_analysis_desc"))
574
 
575
- # Zone de texte pour que l'utilisateur décrive les symptômes
576
  text_description_input = st.text_area(
577
  t("enter_description"),
578
  height=200,
579
  placeholder="Décrivez ici les symptômes observés sur votre plante : feuilles jaunes, taches, flétrissement, présence d'insectes, etc."
580
  )
581
 
582
- # Bouton d'analyse du texte
583
  if st.button("🔍 Analyser la description", type="primary", key="analyze_text_button"):
584
- if text_description_input.strip(): # Vérifie si l'utilisateur a entré du texte
585
  if st.session_state.model_loaded and check_model_health():
586
- # Appel de la fonction d'analyse textuelle
587
  analysis_result = analyze_text_multilingual(text_description_input)
588
 
589
  if analysis_result:
@@ -600,12 +552,10 @@ with tab2: # Onglet Analyse de Texte
600
  with tab3: # Onglet Configuration & Informations
601
  st.header(t("config_title"))
602
 
603
- col1, col2 = st.columns(2) # Créer deux colonnes pour organiser les informations
604
 
605
- with col1: # Section Informations Système
606
  st.subheader("🔧 Informations Système")
607
-
608
- # Affichage des informations système générales
609
  try:
610
  ram = psutil.virtual_memory()
611
  st.write(f"**RAM Totale :** {ram.total / (1024**3):.1f} GB")
@@ -623,7 +573,7 @@ with tab3: # Onglet Configuration & Informations
623
  except Exception as e:
624
  st.error(f"Erreur lors de la récupération des informations système : {e}")
625
 
626
- with col2: # Section Statistiques du Modèle IA
627
  st.subheader("📊 Statistiques du Modèle IA")
628
 
629
  if st.session_state.model_loaded and check_model_health():
@@ -643,7 +593,6 @@ with tab3: # Onglet Configuration & Informations
643
  with tab4: # Onglet À Propos
644
  st.header(t("about_title"))
645
 
646
- # Utilisation de Markdown pour une mise en page plus riche
647
  st.markdown("""
648
  ## 🌱 AgriLens AI : Votre Assistant d'Analyse de Plantes
649
 
@@ -658,12 +607,8 @@ with tab4: # Onglet À Propos
658
 
659
  ### 🤖 Technologie Utilisée :
660
 
661
- * **Modèle IA :** Fine-tuné sur des tâches de vision et de langage, avec une spécialisation dans l'agronomie et la botanique. (Utilise le modèle **Google Gemma 3n E4B IT** pour cette application).
662
- * **Bibliothèques :**
663
- * `transformers` et `torch` pour le fonctionnement des modèles IA.
664
- * `Streamlit` pour une interface utilisateur web interactive et facile à utiliser.
665
- * `Pillow` pour le traitement d'images.
666
- * `psutil` pour l'affichage des ressources système.
667
 
668
  ### 📝 Comment Utiliser AgriLens AI :
669
 
@@ -675,9 +620,8 @@ with tab4: # Onglet À Propos
675
 
676
  ### 🔧 Support et Optimisation :
677
 
678
- * **Mode Local vs. Hub :** Le modèle peut être chargé depuis un répertoire local (si configuré sur votre machine) ou directement depuis le Hugging Face Hub.
679
- * **Gestion Mémoire :** L'application essaie d'optimiser l'utilisation de la mémoire, mais les grands modèles IA nécessitent des ressources significatives (RAM et potentiellement VRAM).
680
- * **Performance :** Les performances peuvent varier en fonction de votre matériel et de la configuration de votre environnement (CPU vs GPU). Pour une expérience optimale avec ce type de modèle, un GPU est fortement recommandé.
681
 
682
  ---
683
 
@@ -685,7 +629,7 @@ with tab4: # Onglet À Propos
685
  """)
686
 
687
  # --- Pied de page ---
688
- st.divider() # Ligne de séparation
689
  st.markdown("""
690
  <div style='text-align: center; color: #666;'>
691
  🌱 AgriLens AI - Assistant d'Analyse de Plantes |
 
4
  from PIL import Image
5
  import requests
6
  import torch
 
7
  import gc
8
  import time
9
  import sys
10
  import psutil
11
+ # from transformers import AutoProcessor, Gemma3nForConditionalGeneration # Importés plus bas pour éviter de charger inutilement
12
+ # Importations pour le chargement du modèle seulement si nécessaire
13
+ # from transformers import AutoProcessor, Gemma3nForConditionalGeneration
14
 
15
  # --- Configuration de la page ---
16
  st.set_page_config(
 
21
  )
22
 
23
  # --- Initialisation des variables de session ---
24
+ # Initialise les variables de session pour gérer l'état de l'application
25
  if 'model_loaded' not in st.session_state:
26
  st.session_state.model_loaded = False
27
  if 'model' not in st.session_state:
 
36
  st.session_state.language = "fr"
37
  if 'load_attempt_count' not in st.session_state:
38
  st.session_state.load_attempt_count = 0
39
+ if 'device' not in st.session_state:
40
+ st.session_state.device = "cpu" # Valeur par défaut
41
 
42
  # --- Fonctions d'aide système ---
43
 
44
  def check_model_health():
45
  """Vérifie si le modèle et le processeur sont chargés et semblent opérationnels."""
46
  try:
 
47
  return (st.session_state.model is not None and
48
  st.session_state.processor is not None and
49
  hasattr(st.session_state.model, 'device'))
50
  except Exception:
 
51
  return False
52
 
53
  def diagnose_loading_issues():
54
  """Diagnostique les problèmes potentiels avant le chargement du modèle."""
55
  issues = []
56
 
 
57
  try:
58
  ram = psutil.virtual_memory()
59
  ram_gb = ram.total / (1024**3)
60
+ if ram_gb < 8:
61
  issues.append(f"⚠️ RAM faible: {ram_gb:.1f}GB (recommandé: 8GB+ pour ce modèle)")
62
  except Exception as e:
63
  issues.append(f"⚠️ Impossible de vérifier la RAM : {e}")
64
 
 
65
  try:
66
  disk_usage = psutil.disk_usage('/')
67
  disk_gb = disk_usage.free / (1024**3)
68
+ if disk_gb < 10:
69
  issues.append(f"⚠️ Espace disque faible: {disk_gb:.1f}GB libre sur '/'")
70
  except Exception as e:
71
  issues.append(f"⚠️ Impossible de vérifier l'espace disque : {e}")
72
 
 
73
  try:
74
  requests.get("https://huggingface.co", timeout=5)
75
  except requests.exceptions.RequestException:
76
  issues.append("⚠️ Problème de connexion à Hugging Face Hub")
77
 
 
78
  if torch.cuda.is_available():
79
  try:
80
  gpu_memory = torch.cuda.get_device_properties(0).total_memory / (1024**3)
81
+ if gpu_memory < 6: # Recommandation pour ce type de modèle multimodal
82
+ issues.append(f"⚠️ GPU mémoire faible: {gpu_memory:.1f}GB (recommandé: 6GB+)")
83
  except Exception as e:
84
  issues.append(f"⚠️ Erreur lors de la vérification de la mémoire GPU : {e}")
85
  else:
 
87
 
88
  return issues
89
 
90
+ def resize_image_if_needed(image, max_size=(1024, 1024)): # Augmenté pour une meilleure analyse si possible
91
  """Redimensionne l'image si ses dimensions dépassent max_size."""
92
  original_size = image.size
93
  if image.size[0] > max_size[0] or image.size[1] > max_size[1]:
 
94
  image.thumbnail(max_size, Image.Resampling.LANCZOS)
95
+ return image, True
96
+ return image, False
97
 
98
  def afficher_ram_disponible(context=""):
99
  """Affiche l'utilisation de la RAM de manière lisible."""
 
107
  st.write(f"💾 Impossible d'afficher l'utilisation de la RAM {context}: {e}")
108
 
109
  # --- Gestion des traductions ---
 
110
  def t(key):
111
  """Fonction pour gérer les traductions."""
112
  translations = {
 
141
  "model_status": "AI Model Status"
142
  }
143
  }
 
144
  return translations[st.session_state.language].get(key, key)
145
 
146
  # --- Fonctions de chargement et d'analyse du modèle ---
147
 
 
 
 
 
148
  MODEL_ID_LOCAL = "D:/Dev/model_gemma" # Path local (pour votre machine)
149
  MODEL_ID_HF = "google/gemma-3n-E4B-it" # ID du modèle sur Hugging Face Hub
150
 
151
+ def get_device_map():
152
+ """Détermine si le modèle doit être chargé sur GPU ou CPU."""
153
+ if torch.cuda.is_available():
154
+ st.session_state.device = "cuda"
155
+ return "auto" # Hugging Face gérera l'allocation sur GPU
156
+ else:
157
+ st.session_state.device = "cpu"
158
+ return "cpu" # Forcer l'utilisation du CPU
159
+
160
  def load_model():
161
  """
162
  Charge le modèle Gemma 3n et son processeur associé.
 
164
  Gère les erreurs et les tentatives de chargement.
165
  """
166
  try:
167
+ # Importer seulement lorsque c'est nécessaire
 
168
  from transformers import AutoProcessor, Gemma3nForConditionalGeneration
169
 
 
170
  if st.session_state.load_attempt_count >= 3:
171
  st.error("❌ Trop de tentatives de chargement ont échoué. Veuillez vérifier votre configuration et redémarrer l'application.")
172
  return None, None
 
179
  for issue in issues:
180
  st.write(issue)
181
 
 
182
  gc.collect()
183
  if torch.cuda.is_available():
184
  torch.cuda.empty_cache()
185
 
186
  processor = None
187
  model = None
188
+ device_map = get_device_map() # Déterminer le device_map
189
+
190
+ local_model_found = os.path.exists(MODEL_ID_LOCAL) and os.path.exists(os.path.join(MODEL_ID_LOCAL, "config.json"))
 
 
 
191
 
192
  if local_model_found:
193
  try:
 
195
  processor = AutoProcessor.from_pretrained(MODEL_ID_LOCAL, trust_remote_code=True)
196
  model = Gemma3nForConditionalGeneration.from_pretrained(
197
  MODEL_ID_LOCAL,
198
+ torch_dtype=torch.bfloat16 if device_map == "auto" else torch.float32, # Utilise bfloat16 si GPU, sinon float32 sur CPU
199
  trust_remote_code=True,
200
+ low_cpu_mem_usage=True,
201
+ device_map=device_map
202
  )
203
  st.success("✅ Modèle chargé avec succès depuis le dossier local.")
204
  st.session_state.model_status = "Chargé (Local)"
205
  except Exception as e:
206
  st.warning(f"⚠️ Échec du chargement depuis le local ({e}). Tentative depuis Hugging Face Hub...")
 
 
207
 
208
+ if model is None: # Si le chargement local a échoué ou n'a pas été tenté
 
209
  try:
210
  st.info(f"Chargement du modèle depuis Hugging Face Hub : {MODEL_ID_HF}...")
211
  processor = AutoProcessor.from_pretrained(MODEL_ID_HF, trust_remote_code=True)
212
  model = Gemma3nForConditionalGeneration.from_pretrained(
213
  MODEL_ID_HF,
214
+ torch_dtype=torch.bfloat16 if device_map == "auto" else torch.float32,
215
  trust_remote_code=True,
216
  low_cpu_mem_usage=True,
217
+ device_map=device_map
218
  )
219
  st.success(f"✅ Modèle chargé avec succès depuis Hugging Face Hub ({MODEL_ID_HF}).")
220
  st.session_state.model_status = "Chargé (Hub)"
221
  except Exception as e:
222
  st.error(f"❌ Échec du chargement du modèle depuis Hugging Face Hub : {e}")
223
+ return None, None
224
 
 
225
  st.session_state.model = model
226
  st.session_state.processor = processor
227
  st.session_state.model_loaded = True
228
  st.session_state.model_load_time = time.time()
229
+ st.session_state.load_attempt_count = 0
230
 
231
  return model, processor
232
 
 
247
  return None
248
 
249
  try:
 
250
  if image.mode != 'RGB':
251
  image = image.convert('RGB')
252
 
 
253
  if not prompt_text:
254
  prompt_text = """Analyse cette image de plante et fournis un diagnostic complet :
255
  1. **État général de la plante :** Décris son apparence globale et sa vitalité.
 
259
  5. **Recommandations de traitement :** Propose des solutions concrètes et adaptées.
260
  6. **Conseils préventifs :** Donne des astuces pour éviter que le problème ne revienne.
261
 
262
+ Réponds de manière structurée et claire en français.""" # Prompt légèrement simplifié
263
 
 
 
264
  inputs = st.session_state.processor(
265
  images=[image],
266
  text=prompt_text,
267
  return_tensors="pt"
268
  )
269
 
 
270
  inputs = {key: val.to(st.session_state.model.device) for key, val in inputs.items()}
271
 
 
272
  with st.spinner("🔍 Analyse d'image en cours..."):
273
  outputs = st.session_state.model.generate(
274
  **inputs,
275
+ max_new_tokens=512,
276
+ do_sample=True,
277
+ temperature=0.7,
278
+ top_p=0.9
279
  )
280
 
 
281
  response = st.session_state.processor.decode(outputs[0], skip_special_tokens=True)
282
 
 
 
283
  if prompt_text.strip() in response:
284
  response_only = response.split(prompt_text.strip())[-1].strip()
285
  else:
286
+ response_only = response.strip()
 
287
 
288
  return response_only
289
 
 
301
  return None
302
 
303
  try:
304
+ # Prompt légèrement simplifié pour réduire la complexité et les potentiels problèmes de cache sur CPU
305
  prompt = f"""Analyse la description des symptômes de cette plante et fournis un diagnostic détaillé :
306
 
307
  **Description des symptômes :**
308
  {text_description}
309
 
310
+ **Instructions :**
311
+ 1. **Diagnostic probable :** Quel est le problème principal ?
312
+ 2. **Causes possibles :** Pourquoi ce problème survient-il ?
313
+ 3. **Traitement recommandé :** Comment le résoudre ?
314
+ 4. **Conseils préventifs :** Comment l'éviter à l'avenir ?
315
 
316
+ Réponds en français de manière claire et structurée."""
317
 
 
318
  inputs = st.session_state.processor(
319
  text=prompt,
320
  return_tensors="pt"
321
  )
322
 
 
323
  inputs = {key: val.to(st.session_state.model.device) for key, val in inputs.items()}
324
 
 
325
  with st.spinner("🔍 Analyse textuelle en cours..."):
326
  outputs = st.session_state.model.generate(
327
  **inputs,
 
331
  top_p=0.9
332
  )
333
 
 
334
  response = st.session_state.processor.decode(outputs[0], skip_special_tokens=True)
335
 
 
336
  if prompt.strip() in response:
337
  response_only = response.split(prompt.strip())[-1].strip()
338
  else:
 
346
 
347
  # --- Interface Utilisateur Streamlit ---
348
 
 
349
  st.title(t("title"))
350
  st.markdown(t("subtitle"))
351
 
 
353
  with st.sidebar:
354
  st.header(t("config_title"))
355
 
 
356
  lang_selector_options = ["Français", "English"]
357
  current_lang_index = 0 if st.session_state.language == "fr" else 1
358
  language_selected = st.selectbox(
 
361
  index=current_lang_index,
362
  help="Sélectionnez la langue de l'interface et des réponses."
363
  )
 
364
  st.session_state.language = "fr" if language_selected == "Français" else "en"
365
 
366
  st.divider()
367
 
 
368
  st.header(t("model_status"))
369
 
370
  if st.session_state.model_loaded and check_model_health():
371
  st.success("✅ Modèle chargé et fonctionnel")
372
  st.write(f"**Statut :** `{st.session_state.model_status}`")
373
  if st.session_state.model_load_time:
 
374
  load_time_str = time.strftime('%H:%M:%S', time.localtime(st.session_state.model_load_time))
375
  st.write(f"**Heure de chargement :** {load_time_str}")
376
 
 
377
  if st.button("🔄 Recharger le modèle", type="secondary"):
378
  st.session_state.model_loaded = False
379
  st.session_state.model = None
380
  st.session_state.processor = None
381
  st.session_state.model_status = "Non chargé"
382
+ st.session_state.load_attempt_count = 0
383
  st.info("Modèle déchargé. Cliquez sur 'Charger le modèle IA' pour le recharger.")
 
 
384
  else:
385
  st.warning("⚠️ Modèle IA non chargé")
386
 
 
387
  if st.button(t("load_model"), type="primary"):
388
  with st.spinner("🔄 Chargement du modèle IA en cours..."):
389
  model_loaded_success = load_model()
390
  if model_loaded_success[0] is not None and model_loaded_success[1] is not None:
391
  st.success("✅ Modèle IA chargé avec succès !")
392
+ # `st.rerun()` peut être tenté, mais un redémarrage du Space est souvent plus sûr.
 
 
393
  else:
394
  st.error("❌ Échec du chargement du modèle IA.")
395
 
 
396
  st.divider()
397
  st.subheader("📊 Ressources Système")
398
  afficher_ram_disponible()
 
399
  if torch.cuda.is_available():
400
  try:
401
  gpu_memory = torch.cuda.memory_allocated(0) / (1024**3)
 
413
  st.header(t("image_analysis_title"))
414
  st.markdown(t("image_analysis_desc"))
415
 
 
416
  capture_option = st.radio(
417
  "Choisissez votre méthode de capture :",
418
  ["📁 Upload d'image" if st.session_state.language == "fr" else "📁 Upload Image",
419
  "📷 Capture par webcam" if st.session_state.language == "fr" else "📷 Webcam Capture"],
420
  horizontal=True,
421
+ key="image_capture_method"
422
  )
423
 
424
  uploaded_file = None
 
428
  uploaded_file = st.file_uploader(
429
  t("choose_image"),
430
  type=['png', 'jpg', 'jpeg'],
431
+ help="Formats acceptés : PNG, JPG, JPEG (taille max recommandée : 10MB)."
432
  )
433
+ if uploaded_file is not None and uploaded_file.size > 10 * 1024 * 1024:
 
434
  st.warning("Le fichier est très volumineux. Il est recommandé d'utiliser des images de taille raisonnable pour une analyse plus rapide.")
435
  else:
436
  st.markdown("**📷 Capture d'image par webcam**")
437
  st.info("Positionnez votre plante malade devant la webcam et cliquez sur 'Prendre une photo'.")
438
  captured_image = st.camera_input(
439
  "Prendre une photo de la plante",
440
+ key="webcam_photo"
441
  )
442
 
 
443
  image_to_analyze = None
444
  if uploaded_file is not None:
445
  try:
 
452
  except Exception as e:
453
  st.error(f"❌ Erreur lors du traitement de l'image capturée : {e}")
454
 
 
455
  if image_to_analyze is not None:
 
456
  original_size = image_to_analyze.size
457
+ resized_image, was_resized = resize_image_if_needed(image_to_analyze)
458
 
459
  col1, col2 = st.columns([1, 1])
460
  with col1:
 
463
  st.info(f"ℹ️ Image redimensionnée de {original_size} à {resized_image.size} pour l'analyse.")
464
 
465
  with col2:
 
466
  if st.session_state.model_loaded and check_model_health():
467
  st.subheader("Options d'analyse")
468
  analysis_type = st.selectbox(
 
483
  )
484
 
485
  if st.button("🔍 Analyser l'image", type="primary", key="analyze_image_button"):
486
+ final_prompt = custom_prompt_input.strip()
487
+ if not final_prompt:
 
488
  if analysis_type.startswith("Diagnostic complet"):
489
  final_prompt = """Analyse cette image de plante et fournis un diagnostic complet :
490
  1. **État général de la plante :** Décris son apparence globale et sa vitalité.
 
494
  5. **Recommandations de traitement :** Propose des solutions concrètes et adaptées.
495
  6. **Conseils préventifs :** Donne des astuces pour éviter que le problème ne revienne.
496
 
497
+ Réponds de manière structurée et claire en français."""
498
  elif analysis_type.startswith("Identification et diagnostic de maladies"):
499
  final_prompt = """Diagnostique cette plante en te concentrant sur les maladies et parasites :
500
  1. Identifie les symptômes visuels spécifiques aux maladies ou parasites.
 
503
  4. Propose des traitements ciblés et des méthodes de lutte.
504
 
505
  Réponds en français de manière structurée."""
506
+ else:
507
  final_prompt = """Analyse cette plante et donne des conseils de soins détaillés :
508
  1. État général de la plante : Évalue sa santé actuelle.
509
  2. Besoins spécifiques : Précise ses besoins en eau, lumière, nutriments et substrat.
 
512
 
513
  Réponds en français de manière structurée."""
514
 
 
515
  analysis_result = analyze_image_multilingual(resized_image, prompt_text=final_prompt)
516
 
517
  if analysis_result:
518
  st.success("✅ Analyse terminée !")
519
  st.markdown("### 📋 Résultats de l'analyse")
520
+ st.markdown(analysis_result)
521
  else:
522
  st.error("❌ Échec de l'analyse de l'image.")
523
  else:
 
527
  st.header(t("text_analysis_title"))
528
  st.markdown(t("text_analysis_desc"))
529
 
 
530
  text_description_input = st.text_area(
531
  t("enter_description"),
532
  height=200,
533
  placeholder="Décrivez ici les symptômes observés sur votre plante : feuilles jaunes, taches, flétrissement, présence d'insectes, etc."
534
  )
535
 
 
536
  if st.button("🔍 Analyser la description", type="primary", key="analyze_text_button"):
537
+ if text_description_input.strip():
538
  if st.session_state.model_loaded and check_model_health():
 
539
  analysis_result = analyze_text_multilingual(text_description_input)
540
 
541
  if analysis_result:
 
552
  with tab3: # Onglet Configuration & Informations
553
  st.header(t("config_title"))
554
 
555
+ col1, col2 = st.columns(2)
556
 
557
+ with col1:
558
  st.subheader("🔧 Informations Système")
 
 
559
  try:
560
  ram = psutil.virtual_memory()
561
  st.write(f"**RAM Totale :** {ram.total / (1024**3):.1f} GB")
 
573
  except Exception as e:
574
  st.error(f"Erreur lors de la récupération des informations système : {e}")
575
 
576
+ with col2:
577
  st.subheader("📊 Statistiques du Modèle IA")
578
 
579
  if st.session_state.model_loaded and check_model_health():
 
593
  with tab4: # Onglet À Propos
594
  st.header(t("about_title"))
595
 
 
596
  st.markdown("""
597
  ## 🌱 AgriLens AI : Votre Assistant d'Analyse de Plantes
598
 
 
607
 
608
  ### 🤖 Technologie Utilisée :
609
 
610
+ * **Modèle IA :** Google Gemma 3n E4B IT, un modèle multimodal performant pour l'analyse de plantes.
611
+ * **Bibliothèques :** `transformers`, `torch`, `streamlit`, `Pillow`, `psutil`, `requests`, `huggingface-hub`.
 
 
 
 
612
 
613
  ### 📝 Comment Utiliser AgriLens AI :
614
 
 
620
 
621
  ### 🔧 Support et Optimisation :
622
 
623
+ * **Gestion des Ressources :** Le modèle nécessite une quantité significative de RAM et idéalement un GPU pour des performances optimales. L'application s'adapte à votre environnement (CPU/GPU).
624
+ * **Performance :** Les temps de réponse dépendent de votre matériel. Un GPU est fortement recommandé pour une expérience fluide.
 
625
 
626
  ---
627
 
 
629
  """)
630
 
631
  # --- Pied de page ---
632
+ st.divider()
633
  st.markdown("""
634
  <div style='text-align: center; color: #666;'>
635
  🌱 AgriLens AI - Assistant d'Analyse de Plantes |