Sidoineko commited on
Commit
5d1f12f
·
verified ·
1 Parent(s): 945b813

Update src/streamlit_app_stable.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app_stable.py +84 -68
src/streamlit_app_stable.py CHANGED
@@ -8,9 +8,8 @@ import gc
8
  import time
9
  import sys
10
  import psutil
11
- # Les imports de transformers sont maintenant effectués dans load_model() pour une meilleure gestion
12
 
13
- # --- Configuration de la page ---
14
  st.set_page_config(
15
  page_title="AgriLens AI - Analyse de Plantes",
16
  page_icon="🌱",
@@ -18,7 +17,7 @@ st.set_page_config(
18
  initial_sidebar_state="expanded"
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:
@@ -34,12 +33,12 @@ if 'language' not in st.session_state:
34
  if 'load_attempt_count' not in st.session_state:
35
  st.session_state.load_attempt_count = 0
36
  if 'device' not in st.session_state:
37
- st.session_state.device = "cpu" # Valeur par défaut
38
 
39
- # --- Fonctions d'aide système ---
40
 
41
  def check_model_health():
42
- """Vérifie si le modèle et le processeur sont chargés et semblent opérationnels."""
43
  try:
44
  return (st.session_state.model is not None and
45
  st.session_state.processor is not None and
@@ -48,44 +47,46 @@ def check_model_health():
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
  try:
55
  ram = psutil.virtual_memory()
56
  ram_gb = ram.total / (1024**3)
57
- if ram_gb < 8: # Recommandation générale pour les gros modèles
58
- issues.append(f"⚠️ RAM faible: {ram_gb:.1f}GB (recommandé: 8GB+ pour ce modèle)")
 
 
59
  except Exception as e:
60
  issues.append(f"⚠️ Impossible de vérifier la RAM : {e}")
61
 
62
  try:
63
  disk_usage = psutil.disk_usage('/')
64
  disk_gb = disk_usage.free / (1024**3)
65
- if disk_gb < 10: # Espace nécessaire pour télécharger le modèle (environ 4-5Go pour Gemma 3n)
66
  issues.append(f"⚠️ Espace disque faible: {disk_gb:.1f}GB libre sur '/'")
67
  except Exception as e:
68
  issues.append(f"⚠️ Impossible de vérifier l'espace disque : {e}")
69
 
70
  try:
71
- requests.get("https://huggingface.co", timeout=5)
72
  except requests.exceptions.RequestException:
73
  issues.append("⚠️ Problème de connexion à Hugging Face Hub")
74
 
75
  if torch.cuda.is_available():
76
  try:
77
  gpu_memory = torch.cuda.get_device_properties(0).total_memory / (1024**3)
78
- if gpu_memory < 6: # Recommandation pour ce type de modèle multimodal
79
- issues.append(f"⚠️ GPU mémoire faible: {gpu_memory:.1f}GB (recommandé: 6GB+)")
80
  except Exception as e:
81
  issues.append(f"⚠️ Erreur lors de la vérification de la mémoire GPU : {e}")
82
  else:
83
- issues.append("ℹ️ CUDA non disponible - Le modèle fonctionnera sur CPU (lentement)")
84
 
85
  return issues
86
 
87
- def resize_image_if_needed(image, max_size=(1024, 1024)): # Augmenté pour une meilleure analyse si possible
88
- """Redimensionne l'image si ses dimensions dépassent max_size."""
89
  original_size = image.size
90
  if image.size[0] > max_size[0] or image.size[1] > max_size[1]:
91
  image.thumbnail(max_size, Image.Resampling.LANCZOS)
@@ -105,10 +106,10 @@ def afficher_ram_disponible(context=""):
105
 
106
  # --- Gestion des traductions ---
107
  def t(key):
108
- """Fonction pour gérer les traductions."""
109
  translations = {
110
  "fr": {
111
- "title": "🌱 AgriLens AI - Assistant d'Analyse de Plantes",
112
  "subtitle": "Analysez vos plantes avec l'IA pour détecter les maladies",
113
  "tabs": ["📸 Analyse d'Image", "📝 Analyse de Texte", "⚙️ Configuration", "ℹ️ À Propos"],
114
  "image_analysis_title": "📸 Analyse d'Image de Plante",
@@ -140,16 +141,15 @@ def t(key):
140
  }
141
  return translations[st.session_state.language].get(key, key)
142
 
143
- # --- Fonctions de chargement et d'analyse du modèle ---
144
-
145
- MODEL_ID_LOCAL = "D:/Dev/model_gemma" # Path local (pour votre machine)
146
  MODEL_ID_HF = "google/gemma-3n-E4B-it" # ID du modèle sur Hugging Face Hub
147
 
148
  def get_device_map():
149
- """Détermine si le modèle doit être chargé sur GPU ou CPU."""
150
  if torch.cuda.is_available():
151
  st.session_state.device = "cuda"
152
- return "auto" # Hugging Face gérera l'allocation sur GPU
153
  else:
154
  st.session_state.device = "cpu"
155
  return "cpu" # Forcer l'utilisation du CPU
@@ -157,34 +157,36 @@ def get_device_map():
157
  def load_model():
158
  """
159
  Charge le modèle Gemma 3n et son processeur associé.
160
- Tente d'abord le chargement local, puis depuis Hugging Face Hub.
161
- Gère les erreurs et les tentatives de chargement.
162
  """
163
  try:
164
- # Importation des bibliothèques nécessaires UNIQUEMENT lors du chargement du modèle
165
  from transformers import AutoProcessor, Gemma3nForConditionalGeneration
166
 
 
167
  if st.session_state.load_attempt_count >= 3:
168
  st.error("❌ Trop de tentatives de chargement ont échoué. Veuillez vérifier votre configuration et redémarrer l'application.")
169
  return None, None
170
  st.session_state.load_attempt_count += 1
171
 
172
- st.info("🔍 Diagnostic de l'environnement avant chargement...")
173
  issues = diagnose_loading_issues()
174
  if issues:
175
  with st.expander("📊 Diagnostic système", expanded=False):
176
  for issue in issues:
177
  st.write(issue)
178
 
179
- gc.collect() # Libère la mémoire inutile
 
180
  if torch.cuda.is_available():
181
- torch.cuda.empty_cache() # Vide le cache GPU
182
 
183
  processor = None
184
  model = None
185
- device_map = get_device_map() # Déterminer le device_map (auto pour GPU, cpu pour CPU)
186
 
187
- # Stratégie de chargement : Local d'abord, puis Hub
188
  local_model_found = os.path.exists(MODEL_ID_LOCAL) and os.path.exists(os.path.join(MODEL_ID_LOCAL, "config.json"))
189
 
190
  if local_model_found:
@@ -193,17 +195,19 @@ def load_model():
193
  processor = AutoProcessor.from_pretrained(MODEL_ID_LOCAL, trust_remote_code=True)
194
  model = Gemma3nForConditionalGeneration.from_pretrained(
195
  MODEL_ID_LOCAL,
196
- torch_dtype=torch.bfloat16 if device_map == "auto" else torch.float32, # Utilise bfloat16 si GPU, sinon float32 sur CPU
 
197
  trust_remote_code=True,
198
- low_cpu_mem_usage=True,
199
- device_map=device_map
200
  )
201
  st.success("✅ Modèle chargé avec succès depuis le dossier local.")
202
  st.session_state.model_status = "Chargé (Local)"
203
  except Exception as e:
204
  st.warning(f"⚠️ Échec du chargement depuis le local ({e}). Tentative depuis Hugging Face Hub...")
205
 
206
- if model is None: # Si le chargement local a échoué ou n'a pas été tenté
 
207
  try:
208
  st.info(f"Chargement du modèle depuis Hugging Face Hub : {MODEL_ID_HF}...")
209
  processor = AutoProcessor.from_pretrained(MODEL_ID_HF, trust_remote_code=True)
@@ -218,29 +222,33 @@ def load_model():
218
  st.session_state.model_status = "Chargé (Hub)"
219
  except Exception as e:
220
  st.error(f"❌ Échec du chargement du modèle depuis Hugging Face Hub : {e}")
221
- return None, None # Échec final
222
 
223
- # Mise à jour de l'état de la session si le chargement a réussi
224
  st.session_state.model = model
225
  st.session_state.processor = processor
226
  st.session_state.model_loaded = True
227
  st.session_state.model_load_time = time.time()
228
- st.session_state.load_attempt_count = 0 # Réinitialiser le compteur si le chargement réussit
229
 
230
  return model, processor
231
 
232
  except ImportError:
233
- st.error("❌ Erreur : Les bibliothèques `transformers` ou `torch` ne sont pas installées.")
234
  return None, None
235
  except Exception as e:
 
236
  st.error(f"❌ Erreur générale lors du chargement du modèle : {e}")
237
  return None, None
238
 
 
 
239
  def analyze_image_multilingual(image, prompt_text=""):
240
  """
241
  Analyse une image de plante en utilisant le modèle Gemma et un prompt personnalisé.
242
  Retourne le résultat de l'analyse.
243
  """
 
244
  if not st.session_state.model_loaded or not check_model_health():
245
  st.error("❌ Modèle IA non chargé ou non fonctionnel. Veuillez le charger via la barre latérale.")
246
  return None
@@ -249,9 +257,8 @@ def analyze_image_multilingual(image, prompt_text=""):
249
  if image.mode != 'RGB':
250
  image = image.convert('RGB')
251
 
252
- # --- GESTION DU PROMPT ET DES TOKENS SPÉCIAUX (MISE À JOUR CRITIQUE) ---
253
  if not prompt_text:
254
- # Prompt par défaut pour l'analyse d'image (sans le token <image> ici, il est ajouté plus bas via messages)
255
  user_text_prompt = """Analyse cette image de plante et fournis un diagnostic complet :
256
  1. **État général de la plante :** Décris son apparence globale et sa vitalité.
257
  2. **Identification des problèmes :** Liste les maladies, parasites ou carences visibles.
@@ -264,41 +271,42 @@ Réponds de manière structurée et claire en français."""
264
  else:
265
  user_text_prompt = prompt_text
266
 
267
- # Utilisation de processor.apply_chat_template pour formater les messages
268
- # C'est la méthode recommandée pour les modèles instruction-tuned et multimodaux de Gemma
269
  messages = [
270
  {
271
  "role": "user",
272
  "content": [
273
- {"type": "image", "image": image}, # Passer l'objet Image PIL ici
274
  {"type": "text", "text": user_text_prompt}
275
  ]
276
  }
277
  ]
278
 
 
279
  inputs = st.session_state.processor.apply_chat_template(
280
  messages,
281
- add_generation_prompt=True, # Important pour dire au modèle de commencer à générer après le prompt
282
  tokenize=True,
283
  return_dict=True,
284
  return_tensors="pt",
285
- ).to(st.session_state.model.device) # Déplacer les inputs sur le bon device
286
 
287
- # Générer la réponse
288
- input_len = inputs["input_ids"].shape[-1]
289
  with st.spinner("🔍 Analyse d'image en cours..."):
290
  outputs = st.session_state.model.generate(
291
  **inputs,
292
- max_new_tokens=512,
293
- do_sample=True,
294
- temperature=0.7,
295
- top_p=0.9
296
  )
297
- # Sélectionner uniquement les tokens générés après le prompt.
298
  generation = outputs[0][input_len:]
299
  response = st.session_state.processor.decode(generation, skip_special_tokens=True)
300
 
301
- return response.strip()
302
 
303
  except Exception as e:
304
  st.error(f"❌ Erreur lors de l'analyse de l'image : {e}")
@@ -314,7 +322,7 @@ def analyze_text_multilingual(text_description):
314
  return None
315
 
316
  try:
317
- # Prompt pour l'analyse textuelle, également dans un format de message
318
  messages = [
319
  {
320
  "role": "user",
@@ -335,16 +343,14 @@ Réponds en français de manière claire et structurée."""}
335
  }
336
  ]
337
 
338
- # Utilisation de processor.apply_chat_template pour formater les messages
339
  inputs = st.session_state.processor.apply_chat_template(
340
  messages,
341
- add_generation_prompt=True, # Indique au modèle de commencer à générer après le prompt
342
  tokenize=True,
343
  return_dict=True,
344
  return_tensors="pt",
345
- ).to(st.session_state.model.device) # Déplacer les inputs sur le bon device
346
 
347
- # Générer la réponse
348
  input_len = inputs["input_ids"].shape[-1]
349
  with st.spinner("🔍 Analyse textuelle en cours..."):
350
  outputs = st.session_state.model.generate(
@@ -354,7 +360,6 @@ Réponds en français de manière claire et structurée."""}
354
  temperature=0.7,
355
  top_p=0.9
356
  )
357
- # Sélectionner uniquement les tokens générés après le prompt.
358
  generation = outputs[0][input_len:]
359
  response = st.session_state.processor.decode(generation, skip_special_tokens=True)
360
 
@@ -373,6 +378,7 @@ st.markdown(t("subtitle"))
373
  with st.sidebar:
374
  st.header(t("config_title"))
375
 
 
376
  lang_selector_options = ["Français", "English"]
377
  current_lang_index = 0 if st.session_state.language == "fr" else 1
378
  language_selected = st.selectbox(
@@ -385,6 +391,7 @@ with st.sidebar:
385
 
386
  st.divider()
387
 
 
388
  st.header(t("model_status"))
389
 
390
  if st.session_state.model_loaded and check_model_health():
@@ -394,6 +401,7 @@ with st.sidebar:
394
  load_time_str = time.strftime('%H:%M:%S', time.localtime(st.session_state.model_load_time))
395
  st.write(f"**Heure de chargement :** {load_time_str}")
396
 
 
397
  if st.button("🔄 Recharger le modèle", type="secondary"):
398
  st.session_state.model_loaded = False
399
  st.session_state.model = None
@@ -404,15 +412,17 @@ with st.sidebar:
404
  else:
405
  st.warning("⚠️ Modèle IA non chargé")
406
 
 
407
  if st.button(t("load_model"), type="primary"):
408
  with st.spinner("🔄 Chargement du modèle IA en cours..."):
409
  model_loaded_success = load_model()
410
  if model_loaded_success[0] is not None and model_loaded_success[1] is not None:
411
  st.success("✅ Modèle IA chargé avec succès !")
412
- # st.rerun() # Ne pas utiliser st.rerun() ici si vous voulez un comportement stable sur Spaces
413
  else:
414
  st.error("❌ Échec du chargement du modèle IA.")
415
 
 
416
  st.divider()
417
  st.subheader("📊 Ressources Système")
418
  afficher_ram_disponible()
@@ -426,13 +436,14 @@ with st.sidebar:
426
  else:
427
  st.write("🚀 GPU : Non disponible (utilisation CPU)")
428
 
429
- # --- Onglets Principaux ---
430
  tab1, tab2, tab3, tab4 = st.tabs(t("tabs"))
431
 
432
  with tab1: # Onglet Analyse d'Image
433
  st.header(t("image_analysis_title"))
434
  st.markdown(t("image_analysis_desc"))
435
 
 
436
  capture_option = st.radio(
437
  "Choisissez votre méthode de capture :",
438
  ["📁 Upload d'image" if st.session_state.language == "fr" else "📁 Upload Image",
@@ -448,7 +459,7 @@ with tab1: # Onglet Analyse d'Image
448
  uploaded_file = st.file_uploader(
449
  t("choose_image"),
450
  type=['png', 'jpg', 'jpeg'],
451
- help="Formats acceptés : PNG, JPG, JPEG (taille max recommandée : 10MB)."
452
  )
453
  if uploaded_file is not None and uploaded_file.size > 10 * 1024 * 1024:
454
  st.warning("Le fichier est très volumineux. Il est recommandé d'utiliser des images de taille raisonnable pour une analyse plus rapide.")
@@ -460,6 +471,7 @@ with tab1: # Onglet Analyse d'Image
460
  key="webcam_photo"
461
  )
462
 
 
463
  image_to_analyze = None
464
  if uploaded_file is not None:
465
  try:
@@ -472,6 +484,7 @@ with tab1: # Onglet Analyse d'Image
472
  except Exception as e:
473
  st.error(f"❌ Erreur lors du traitement de l'image capturée : {e}")
474
 
 
475
  if image_to_analyze is not None:
476
  original_size = image_to_analyze.size
477
  resized_image, was_resized = resize_image_if_needed(image_to_analyze)
@@ -483,6 +496,7 @@ with tab1: # Onglet Analyse d'Image
483
  st.info(f"ℹ️ Image redimensionnée de {original_size} à {resized_image.size} pour l'analyse.")
484
 
485
  with col2:
 
486
  if st.session_state.model_loaded and check_model_health():
487
  st.subheader("Options d'analyse")
488
  analysis_type = st.selectbox(
@@ -504,7 +518,7 @@ with tab1: # Onglet Analyse d'Image
504
 
505
  if st.button("🔍 Analyser l'image", type="primary", key="analyze_image_button"):
506
  final_prompt = custom_prompt_input.strip()
507
- if not final_prompt:
508
  if analysis_type.startswith("Diagnostic complet"):
509
  final_prompt = """Analyse cette image de plante et fournis un diagnostic complet :
510
  1. **État général de la plante :** Décris son apparence globale et sa vitalité.
@@ -523,7 +537,7 @@ Réponds de manière structurée et claire en français."""
523
  4. Propose des traitements ciblés et des méthodes de lutte.
524
 
525
  Réponds en français de manière structurée."""
526
- else:
527
  final_prompt = """Analyse cette plante et donne des conseils de soins détaillés :
528
  1. État général de la plante : Évalue sa santé actuelle.
529
  2. Besoins spécifiques : Précise ses besoins en eau, lumière, nutriments et substrat.
@@ -547,6 +561,7 @@ with tab2: # Onglet Analyse de Texte
547
  st.header(t("text_analysis_title"))
548
  st.markdown(t("text_analysis_desc"))
549
 
 
550
  text_description_input = st.text_area(
551
  t("enter_description"),
552
  height=200,
@@ -554,7 +569,7 @@ with tab2: # Onglet Analyse de Texte
554
  )
555
 
556
  if st.button("🔍 Analyser la description", type="primary", key="analyze_text_button"):
557
- if text_description_input.strip():
558
  if st.session_state.model_loaded and check_model_health():
559
  analysis_result = analyze_text_multilingual(text_description_input)
560
 
@@ -574,8 +589,9 @@ with tab3: # Onglet Configuration & Informations
574
 
575
  col1, col2 = st.columns(2)
576
 
577
- with col1:
578
  st.subheader("🔧 Informations Système")
 
579
  try:
580
  ram = psutil.virtual_memory()
581
  st.write(f"**RAM Totale :** {ram.total / (1024**3):.1f} GB")
@@ -593,7 +609,7 @@ with tab3: # Onglet Configuration & Informations
593
  except Exception as e:
594
  st.error(f"Erreur lors de la récupération des informations système : {e}")
595
 
596
- with col2:
597
  st.subheader("📊 Statistiques du Modèle IA")
598
 
599
  if st.session_state.model_loaded and check_model_health():
 
8
  import time
9
  import sys
10
  import psutil
 
11
 
12
+ # Configuration de la page Streamlit
13
  st.set_page_config(
14
  page_title="AgriLens AI - Analyse de Plantes",
15
  page_icon="🌱",
 
17
  initial_sidebar_state="expanded"
18
  )
19
 
20
+ # Initialisation des variables de session pour maintenir l'état de l'application
21
  if 'model_loaded' not in st.session_state:
22
  st.session_state.model_loaded = False
23
  if 'model' not in st.session_state:
 
33
  if 'load_attempt_count' not in st.session_state:
34
  st.session_state.load_attempt_count = 0
35
  if 'device' not in st.session_state:
36
+ st.session_state.device = "cpu" # Défaut à CPU, sera mis à jour si GPU disponible
37
 
38
+ # --- Fonctions d'aide système et diagnostic ---
39
 
40
  def check_model_health():
41
+ """Vérifie si le modèle et le processeur sont correctement chargés et opérationnels."""
42
  try:
43
  return (st.session_state.model is not None and
44
  st.session_state.processor is not None and
 
47
  return False
48
 
49
  def diagnose_loading_issues():
50
+ """Diagnostique les problèmes potentiels (RAM, disque, connexion, GPU) avant le chargement du modèle."""
51
  issues = []
52
 
53
  try:
54
  ram = psutil.virtual_memory()
55
  ram_gb = ram.total / (1024**3)
56
+ # 16GB est le minimum pour Gemma 3n sur CPU. Le Space 'basic' a 16GB.
57
+ # Tout ce qui est en dessous de 15GB libre est un risque.
58
+ if ram_gb < 15: # Ajusté pour être plus réaliste pour le CPU basic
59
+ issues.append(f"⚠️ RAM faible: {ram_gb:.1f}GB (minimum requis: 15GB pour ce modèle sur CPU)")
60
  except Exception as e:
61
  issues.append(f"⚠️ Impossible de vérifier la RAM : {e}")
62
 
63
  try:
64
  disk_usage = psutil.disk_usage('/')
65
  disk_gb = disk_usage.free / (1024**3)
66
+ if disk_gb < 10: # Espace nécessaire pour le modèle et le cache
67
  issues.append(f"⚠️ Espace disque faible: {disk_gb:.1f}GB libre sur '/'")
68
  except Exception as e:
69
  issues.append(f"⚠️ Impossible de vérifier l'espace disque : {e}")
70
 
71
  try:
72
+ requests.get("https://huggingface.co", timeout=5) # Vérifie la connexion au Hub HF
73
  except requests.exceptions.RequestException:
74
  issues.append("⚠️ Problème de connexion à Hugging Face Hub")
75
 
76
  if torch.cuda.is_available():
77
  try:
78
  gpu_memory = torch.cuda.get_device_properties(0).total_memory / (1024**3)
79
+ if gpu_memory < 8: # Gemma 3n a besoin d'au moins 8GB VRAM pour fonctionner confortablement
80
+ issues.append(f"⚠️ GPU mémoire faible: {gpu_memory:.1f}GB (recommandé: 8GB+)")
81
  except Exception as e:
82
  issues.append(f"⚠️ Erreur lors de la vérification de la mémoire GPU : {e}")
83
  else:
84
+ issues.append("ℹ️ CUDA non disponible - Le modèle fonctionnera sur CPU (très lentement et potentiellement avec des erreurs de mémoire)")
85
 
86
  return issues
87
 
88
+ def resize_image_if_needed(image, max_size=(1024, 1024)):
89
+ """Redimensionne l'image si ses dimensions dépassent max_size pour optimiser l'entrée du modèle."""
90
  original_size = image.size
91
  if image.size[0] > max_size[0] or image.size[1] > max_size[1]:
92
  image.thumbnail(max_size, Image.Resampling.LANCZOS)
 
106
 
107
  # --- Gestion des traductions ---
108
  def t(key):
109
+ """Gère les traductions pour l'interface utilisateur."""
110
  translations = {
111
  "fr": {
112
+ "title": "🌱 AgriLens AI - Analyse de Plantes",
113
  "subtitle": "Analysez vos plantes avec l'IA pour détecter les maladies",
114
  "tabs": ["📸 Analyse d'Image", "📝 Analyse de Texte", "⚙️ Configuration", "ℹ️ À Propos"],
115
  "image_analysis_title": "📸 Analyse d'Image de Plante",
 
141
  }
142
  return translations[st.session_state.language].get(key, key)
143
 
144
+ # --- Constantes pour le modèle (ID et chemins locaux) ---
145
+ MODEL_ID_LOCAL = "D:/Dev/model_gemma" # Chemin local pour votre machine (sera ignoré sur HF Spaces)
 
146
  MODEL_ID_HF = "google/gemma-3n-E4B-it" # ID du modèle sur Hugging Face Hub
147
 
148
  def get_device_map():
149
+ """Détermine le device_map pour le chargement du modèle (GPU si disponible, sinon CPU)."""
150
  if torch.cuda.is_available():
151
  st.session_state.device = "cuda"
152
+ return "auto" # `device_map="auto"` permet à Hugging Face de gérer l'allocation GPU
153
  else:
154
  st.session_state.device = "cpu"
155
  return "cpu" # Forcer l'utilisation du CPU
 
157
  def load_model():
158
  """
159
  Charge le modèle Gemma 3n et son processeur associé.
160
+ Tente d'abord le chargement depuis un dossier local, puis depuis Hugging Face Hub.
161
+ Comprend des optimisations pour la gestion de la mémoire.
162
  """
163
  try:
164
+ # Importe les classes de transformers ici pour un chargement paresseux et optimisé de la mémoire
165
  from transformers import AutoProcessor, Gemma3nForConditionalGeneration
166
 
167
+ # Limite le nombre de tentatives de chargement pour éviter des boucles infinies
168
  if st.session_state.load_attempt_count >= 3:
169
  st.error("❌ Trop de tentatives de chargement ont échoué. Veuillez vérifier votre configuration et redémarrer l'application.")
170
  return None, None
171
  st.session_state.load_attempt_count += 1
172
 
173
+ st.info("🔍 Diagnostic de l'environnement avant chargement du modèle...")
174
  issues = diagnose_loading_issues()
175
  if issues:
176
  with st.expander("📊 Diagnostic système", expanded=False):
177
  for issue in issues:
178
  st.write(issue)
179
 
180
+ # Libère la mémoire du Garbage Collector de Python et le cache GPU (si applicable)
181
+ gc.collect()
182
  if torch.cuda.is_available():
183
+ torch.cuda.empty_cache()
184
 
185
  processor = None
186
  model = None
187
+ device_map = get_device_map() # Détermine si le GPU ou le CPU sera utilisé
188
 
189
+ # Vérifie si le modèle est disponible localement et complet
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:
 
195
  processor = AutoProcessor.from_pretrained(MODEL_ID_LOCAL, trust_remote_code=True)
196
  model = Gemma3nForConditionalGeneration.from_pretrained(
197
  MODEL_ID_LOCAL,
198
+ # Utilise bfloat16 pour le GPU (plus rapide, moins de mémoire), float32 pour le CPU (compatibilité)
199
+ torch_dtype=torch.bfloat16 if device_map == "auto" else torch.float32,
200
  trust_remote_code=True,
201
+ low_cpu_mem_usage=True, # Tente de réduire la consommation de RAM CPU pendant le chargement
202
+ device_map=device_map # Le mapping du périphérique (GPU ou CPU)
203
  )
204
  st.success("✅ Modèle chargé avec succès depuis le dossier local.")
205
  st.session_state.model_status = "Chargé (Local)"
206
  except Exception as e:
207
  st.warning(f"⚠️ Échec du chargement depuis le local ({e}). Tentative depuis Hugging Face Hub...")
208
 
209
+ # Si le modèle n'a pas été chargé localement ou si le chargement local a échoué, tente depuis Hugging Face Hub
210
+ if model is None:
211
  try:
212
  st.info(f"Chargement du modèle depuis Hugging Face Hub : {MODEL_ID_HF}...")
213
  processor = AutoProcessor.from_pretrained(MODEL_ID_HF, trust_remote_code=True)
 
222
  st.session_state.model_status = "Chargé (Hub)"
223
  except Exception as e:
224
  st.error(f"❌ Échec du chargement du modèle depuis Hugging Face Hub : {e}")
225
+ return None, None # Échec final, le modèle n'a pas pu être chargé
226
 
227
+ # Met à jour les variables de session si le modèle a été chargé avec succès
228
  st.session_state.model = model
229
  st.session_state.processor = processor
230
  st.session_state.model_loaded = True
231
  st.session_state.model_load_time = time.time()
232
+ st.session_state.load_attempt_count = 0 # Réinitialise le compteur après un chargement réussi
233
 
234
  return model, processor
235
 
236
  except ImportError:
237
+ st.error("❌ Erreur : Les bibliothèques `transformers` ou `torch` ne sont pas installées. Veuillez vérifier votre `requirements.txt`.")
238
  return None, None
239
  except Exception as e:
240
+ # Capture toutes les autres exceptions non spécifiques à l'import (ex: OOM lors du chargement)
241
  st.error(f"❌ Erreur générale lors du chargement du modèle : {e}")
242
  return None, None
243
 
244
+ # --- Fonctions d'analyse (Image et Texte) ---
245
+
246
  def analyze_image_multilingual(image, prompt_text=""):
247
  """
248
  Analyse une image de plante en utilisant le modèle Gemma et un prompt personnalisé.
249
  Retourne le résultat de l'analyse.
250
  """
251
+ # Vérifie que le modèle est bien chargé avant de tenter l'analyse
252
  if not st.session_state.model_loaded or not check_model_health():
253
  st.error("❌ Modèle IA non chargé ou non fonctionnel. Veuillez le charger via la barre latérale.")
254
  return None
 
257
  if image.mode != 'RGB':
258
  image = image.convert('RGB')
259
 
260
+ # Prépare le prompt textuel qui accompagnera l'image
261
  if not prompt_text:
 
262
  user_text_prompt = """Analyse cette image de plante et fournis un diagnostic complet :
263
  1. **État général de la plante :** Décris son apparence globale et sa vitalité.
264
  2. **Identification des problèmes :** Liste les maladies, parasites ou carences visibles.
 
271
  else:
272
  user_text_prompt = prompt_text
273
 
274
+ # Utilise `processor.apply_chat_template` pour formater l'entrée multimodale (image + texte).
275
+ # Ceci est essentiel pour les modèles comme Gemma 3n.
276
  messages = [
277
  {
278
  "role": "user",
279
  "content": [
280
+ {"type": "image", "image": image}, # L'objet Image PIL est passé ici
281
  {"type": "text", "text": user_text_prompt}
282
  ]
283
  }
284
  ]
285
 
286
+ # Traite les messages en inputs tensoriels et les déplace sur le device du modèle
287
  inputs = st.session_state.processor.apply_chat_template(
288
  messages,
289
+ add_generation_prompt=True, # Indique au modèle de commencer à générer après ce prompt
290
  tokenize=True,
291
  return_dict=True,
292
  return_tensors="pt",
293
+ ).to(st.session_state.model.device)
294
 
295
+ # Génère la réponse du modèle
296
+ input_len = inputs["input_ids"].shape[-1] # Longueur du prompt encodé
297
  with st.spinner("🔍 Analyse d'image en cours..."):
298
  outputs = st.session_state.model.generate(
299
  **inputs,
300
+ max_new_tokens=512, # Limite la longueur de la réponse
301
+ do_sample=True, # Active l'échantillonnage (réponses plus variées)
302
+ temperature=0.7, # Contrôle le niveau de créativité/aléatoire
303
+ top_p=0.9 # Stratégie d'échantillonnage Top-P
304
  )
305
+ # Décode uniquement la partie générée par le modèle (exclut le prompt initial)
306
  generation = outputs[0][input_len:]
307
  response = st.session_state.processor.decode(generation, skip_special_tokens=True)
308
 
309
+ return response.strip() # Retourne la réponse nettoyée des espaces inutiles
310
 
311
  except Exception as e:
312
  st.error(f"❌ Erreur lors de l'analyse de l'image : {e}")
 
322
  return None
323
 
324
  try:
325
+ # Prépare le prompt textuel dans le format 'messages' pour `apply_chat_template`
326
  messages = [
327
  {
328
  "role": "user",
 
343
  }
344
  ]
345
 
 
346
  inputs = st.session_state.processor.apply_chat_template(
347
  messages,
348
+ add_generation_prompt=True, # Important pour la génération
349
  tokenize=True,
350
  return_dict=True,
351
  return_tensors="pt",
352
+ ).to(st.session_state.model.device)
353
 
 
354
  input_len = inputs["input_ids"].shape[-1]
355
  with st.spinner("🔍 Analyse textuelle en cours..."):
356
  outputs = st.session_state.model.generate(
 
360
  temperature=0.7,
361
  top_p=0.9
362
  )
 
363
  generation = outputs[0][input_len:]
364
  response = st.session_state.processor.decode(generation, skip_special_tokens=True)
365
 
 
378
  with st.sidebar:
379
  st.header(t("config_title"))
380
 
381
+ # Sélecteur 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(
 
391
 
392
  st.divider()
393
 
394
+ # Section de gestion du modèle IA
395
  st.header(t("model_status"))
396
 
397
  if st.session_state.model_loaded and check_model_health():
 
401
  load_time_str = time.strftime('%H:%M:%S', time.localtime(st.session_state.model_load_time))
402
  st.write(f"**Heure de chargement :** {load_time_str}")
403
 
404
+ # Bouton pour décharger et recharger le modèle
405
  if st.button("🔄 Recharger le modèle", type="secondary"):
406
  st.session_state.model_loaded = False
407
  st.session_state.model = None
 
412
  else:
413
  st.warning("⚠️ Modèle IA non chargé")
414
 
415
+ # Bouton pour lancer le chargement du modèle
416
  if st.button(t("load_model"), type="primary"):
417
  with st.spinner("🔄 Chargement du modèle IA en cours..."):
418
  model_loaded_success = load_model()
419
  if model_loaded_success[0] is not None and model_loaded_success[1] is not None:
420
  st.success("✅ Modèle IA chargé avec succès !")
421
+ # `st.rerun()` est généralement évité ici sur Spaces, car le redémarrage est géré par la plateforme.
422
  else:
423
  st.error("❌ Échec du chargement du modèle IA.")
424
 
425
+ # Informations sur l'utilisation des ressources système
426
  st.divider()
427
  st.subheader("📊 Ressources Système")
428
  afficher_ram_disponible()
 
436
  else:
437
  st.write("🚀 GPU : Non disponible (utilisation CPU)")
438
 
439
+ # --- Onglets Principaux pour l'interface utilisateur ---
440
  tab1, tab2, tab3, tab4 = st.tabs(t("tabs"))
441
 
442
  with tab1: # Onglet Analyse d'Image
443
  st.header(t("image_analysis_title"))
444
  st.markdown(t("image_analysis_desc"))
445
 
446
+ # Choix de la source de l'image
447
  capture_option = st.radio(
448
  "Choisissez votre méthode de capture :",
449
  ["📁 Upload d'image" if st.session_state.language == "fr" else "📁 Upload Image",
 
459
  uploaded_file = st.file_uploader(
460
  t("choose_image"),
461
  type=['png', 'jpg', 'jpeg'],
462
+ help="Formats acceptés : PNG, JPG, JPEG (taille max recommandée : 10MB pour optimiser la performance)."
463
  )
464
  if uploaded_file is not None and uploaded_file.size > 10 * 1024 * 1024:
465
  st.warning("Le fichier est très volumineux. Il est recommandé d'utiliser des images de taille raisonnable pour une analyse plus rapide.")
 
471
  key="webcam_photo"
472
  )
473
 
474
+ # Traitement de l'image chargée ou capturée
475
  image_to_analyze = None
476
  if uploaded_file is not None:
477
  try:
 
484
  except Exception as e:
485
  st.error(f"❌ Erreur lors du traitement de l'image capturée : {e}")
486
 
487
+ # Affichage de l'image et options d'analyse si une image est disponible
488
  if image_to_analyze is not None:
489
  original_size = image_to_analyze.size
490
  resized_image, was_resized = resize_image_if_needed(image_to_analyze)
 
496
  st.info(f"ℹ️ Image redimensionnée de {original_size} à {resized_image.size} pour l'analyse.")
497
 
498
  with col2:
499
+ # Les options d'analyse sont disponibles seulement si le modèle est chargé
500
  if st.session_state.model_loaded and check_model_health():
501
  st.subheader("Options d'analyse")
502
  analysis_type = st.selectbox(
 
518
 
519
  if st.button("🔍 Analyser l'image", type="primary", key="analyze_image_button"):
520
  final_prompt = custom_prompt_input.strip()
521
+ if not final_prompt: # Utilise un prompt par défaut si aucun prompt personnalisé n'est fourni
522
  if analysis_type.startswith("Diagnostic complet"):
523
  final_prompt = """Analyse cette image de plante et fournis un diagnostic complet :
524
  1. **État général de la plante :** Décris son apparence globale et sa vitalité.
 
537
  4. Propose des traitements ciblés et des méthodes de lutte.
538
 
539
  Réponds en français de manière structurée."""
540
+ else: # Conseils de soins et d'entretien
541
  final_prompt = """Analyse cette plante et donne des conseils de soins détaillés :
542
  1. État général de la plante : Évalue sa santé actuelle.
543
  2. Besoins spécifiques : Précise ses besoins en eau, lumière, nutriments et substrat.
 
561
  st.header(t("text_analysis_title"))
562
  st.markdown(t("text_analysis_desc"))
563
 
564
+ # Zone de texte pour la description des symptômes
565
  text_description_input = st.text_area(
566
  t("enter_description"),
567
  height=200,
 
569
  )
570
 
571
  if st.button("🔍 Analyser la description", type="primary", key="analyze_text_button"):
572
+ if text_description_input.strip(): # Vérifie si l'utilisateur a entré du texte
573
  if st.session_state.model_loaded and check_model_health():
574
  analysis_result = analyze_text_multilingual(text_description_input)
575
 
 
589
 
590
  col1, col2 = st.columns(2)
591
 
592
+ with col1: # Section Informations Système
593
  st.subheader("🔧 Informations Système")
594
+
595
  try:
596
  ram = psutil.virtual_memory()
597
  st.write(f"**RAM Totale :** {ram.total / (1024**3):.1f} GB")
 
609
  except Exception as e:
610
  st.error(f"Erreur lors de la récupération des informations système : {e}")
611
 
612
+ with col2: # Section Statistiques du Modèle IA
613
  st.subheader("📊 Statistiques du Modèle IA")
614
 
615
  if st.session_state.model_loaded and check_model_health():