Spaces:
Sleeping
Sleeping
Update src/streamlit_app_stable.py
Browse files- 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 |
-
#
|
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 |
-
#
|
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" #
|
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
|
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 |
-
|
58 |
-
|
|
|
|
|
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
|
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 <
|
79 |
-
issues.append(f"⚠️ GPU mémoire faible: {gpu_memory:.1f}GB (recommandé:
|
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)):
|
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 |
-
"""
|
109 |
translations = {
|
110 |
"fr": {
|
111 |
-
"title": "🌱 AgriLens AI -
|
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 |
-
# ---
|
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
|
150 |
if torch.cuda.is_available():
|
151 |
st.session_state.device = "cuda"
|
152 |
-
return "auto" # Hugging Face
|
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 |
-
|
162 |
"""
|
163 |
try:
|
164 |
-
#
|
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 |
-
|
|
|
180 |
if torch.cuda.is_available():
|
181 |
-
torch.cuda.empty_cache()
|
182 |
|
183 |
processor = None
|
184 |
model = None
|
185 |
-
device_map = get_device_map() #
|
186 |
|
187 |
-
#
|
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 |
-
|
|
|
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 |
-
|
|
|
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 |
-
#
|
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 #
|
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 |
-
#
|
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 |
-
#
|
268 |
-
#
|
269 |
messages = [
|
270 |
{
|
271 |
"role": "user",
|
272 |
"content": [
|
273 |
-
{"type": "image", "image": image}, #
|
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, #
|
282 |
tokenize=True,
|
283 |
return_dict=True,
|
284 |
return_tensors="pt",
|
285 |
-
).to(st.session_state.model.device)
|
286 |
|
287 |
-
#
|
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 |
-
#
|
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 |
-
#
|
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, #
|
342 |
tokenize=True,
|
343 |
return_dict=True,
|
344 |
return_tensors="pt",
|
345 |
-
).to(st.session_state.model.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()
|
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():
|