Spaces:
Build error
Build error
| import streamlit as st | |
| from PIL import Image | |
| import os | |
| import folium | |
| from streamlit_folium import st_folium | |
| # --- NEW IMPORTS FOR CHATBOT FUNCTIONALITY --- | |
| # COMMENTED OUT: from groq import Groq | |
| # COMMENTED OUT: from dotenv import load_dotenv | |
| import base64 | |
| import tempfile | |
| # Removed pydub import as it's not needed if audio recording/conversion is removed. | |
| # from pydub import AudioSegment | |
| # --- NOUVEAUX IMPORTS POUR LA FONCTIONNALITÉ D'ANALYSE DE PLANTE --- | |
| import tensorflow as tf | |
| import numpy as np | |
| # --- CUSTOM AUDIO RECORDER COMPONENT PLACEHOLDER --- | |
| # Keep the declaration for now, in case it's used elsewhere or for future re-addition, | |
| # but its usage is removed from show_chatbot_view. | |
| try: | |
| import streamlit.components.v1 as components | |
| _audio_recorder_component = components.declare_component( | |
| "audio_recorder_component", | |
| path="./audio_recorder_component/frontend/build" # Adjust this path to your component's build folder | |
| ) | |
| def audio_recorder_component(): | |
| return _audio_recorder_component(key="audio_recorder_widget") | |
| except Exception as e: | |
| st.warning(f"Could not load custom audio recorder component. Audio recording functionality might be limited: {e}") | |
| def audio_recorder_component(): | |
| st.info("Audio recording component not loaded. Please ensure it's installed correctly and ffmpeg/ffprobe are available.") | |
| return None | |
| # --- Configuration de la page Streamlit --- | |
| st.set_page_config( | |
| page_title="GreenField Pro - Dashboard", | |
| page_icon=":seedling:", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # --- Chemins des ressources statiques et du modèle --- | |
| LOGO_PATH = os.path.join("static", "images", "Green Plant and Agriculture Logo (2).png") | |
| CSS_PATH = "style.css" | |
| # CHANGED: Use Hugging Face Hub for model loading | |
| HF_REPO_ID = "mopaoleonel/plante_tedection" | |
| HF_MODEL_FILENAME = "mon_modele.keras" | |
| # --- Dictionnaire des classes de maladies --- | |
| CLASS_NAMES = { | |
| 'Apple___Apple_scab': 0, 'Apple___Black_rot': 1, 'Apple___Cedar_apple_rust': 2, 'Apple___healthy': 3, | |
| 'Blueberry___healthy': 4, 'Cherry_(including_sour)___Powdery_mildew': 5, 'Cherry_(including_sour)___healthy': 6, | |
| 'Corn_(maize)___Cercospora_leaf_spot Gray_leaf_spot': 7, 'Corn_(maize)___Common_rust_': 8, 'Corn_(maize)___Northern_Leaf_Blight': 9, | |
| 'Corn_(maize)___healthy': 10, 'Grape___Black_rot': 11, 'Grape___Esca_(Black_Measles)': 12, 'Grape___Leaf_blight_(Isariopsis_Leaf_Spot)': 13, | |
| 'Grape___healthy': 14, 'Orange___Haunglongbing_(Citrus_greening)': 15, 'Peach___Bacterial_spot': 16, 'Peach___healthy': 17, | |
| 'Pepper,_bell___Bacterial_spot': 18, 'Pepper,_bell___healthy': 19, 'Potato___Early_blight': 20, 'Potato___Late_blight': 21, | |
| 'Potato___healthy': 22, 'Raspberry___healthy': 23, 'Soybean___healthy': 24, 'Squash___Powdery_mildew': 25, | |
| 'Strawberry___Leaf_scorch': 26, 'Strawberry___healthy': 27, 'Tomato___Bacterial_spot': 28, 'Tomato___Early_blight': 29, | |
| 'Tomato___Late_blight': 30, 'Tomato___Leaf_Mold': 31, 'Tomato___Septoria_leaf_spot': 32, 'Tomato___Spider_mites Two-spotted_spider_mite': 33, | |
| 'Tomato___Target_Spot': 34, 'Tomato___Tomato_Yellow_Leaf_Curl_Virus': 35, 'Tomato___Tomato_mosaic_virus': 36, 'Tomato___healthy': 37 | |
| } | |
| # Inverser le dictionnaire pour un accès facile par index | |
| CLASS_NAMES_INV = {v: k for k, v in CLASS_NAMES.items()} | |
| # Taille d'entrée attendue par votre modèle | |
| IMG_HEIGHT = 64 | |
| IMG_WIDTH = 64 | |
| IMG_CHANNELS = 3 # RGB | |
| # --- Fonctions utilitaires et de chargement --- | |
| def load_css(css_file): | |
| """Charge un fichier CSS externe.""" | |
| try: | |
| with open(css_file) as f: | |
| st.markdown(f"<style>{f.read()}</style>", unsafe_allow_html=True) | |
| except FileNotFoundError: | |
| st.warning(f"Fichier CSS non trouvé à : {css_file}. L'interface utilisera les styles par défaut.") | |
| except Exception as e: | |
| st.error(f"Erreur lors du chargement du CSS : {e}") | |
| # MODIFIED: Use hf_hub_download to get the model | |
| # Met en cache le modèle pour éviter les rechargements coûteux | |
| def load_keras_model_from_hub(repo_id, filename): | |
| """Charge le modèle Keras depuis Hugging Face Hub.""" | |
| try: | |
| from huggingface_hub import hf_hub_download | |
| model_path = hf_hub_download(repo_id=repo_id, filename=filename) | |
| st.info(f"Modèle téléchargé depuis Hugging Face: {model_path}") | |
| model = tf.keras.models.load_model(model_path) | |
| # NOUVELLES LIGNES DE DÉBOGAGE (vous pouvez les supprimer après résolution) | |
| st.info(f"Vérification de la version de TensorFlow sur Streamlit Cloud...") | |
| st.info(f"TensorFlow version sur Streamlit Cloud: {tf.__version__}") | |
| st.info(f"Keras version sur Streamlit Cloud: {tf.keras.__version__}") | |
| # FIN DES LIGNES DE DÉBOGAGE | |
| st.success("Modèle Keras chargé avec succès !") | |
| return model | |
| except Exception as e: | |
| st.error(f"Erreur Critique : Impossible de télécharger ou charger le modèle depuis Hugging Face Hub: {e}") | |
| st.info(f"Assurez-vous que le dépôt '{repo_id}' et le fichier '{filename}' existent et sont accessibles. Vérifiez également que votre modèle a été sauvegardé avec TensorFlow {tf.__version__}.") | |
| return None | |
| # --- Fonctions pour les vues spécifiques --- | |
| def show_dashboard_view(): | |
| """Affiche le tableau de bord principal avec les métriques et la carte.""" | |
| st.markdown("<h1>Tableau de Bord</h1>", unsafe_allow_html=True) | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: | |
| st.markdown(""" | |
| <a href="#" class="info-card"> | |
| <div> | |
| <div class="value">24°C</div> | |
| <div class="label">Température</div> | |
| </div> | |
| <div class="icon-container"><i class="fas fa-thermometer-three-quarters"></i></div> | |
| </a> | |
| """, unsafe_allow_html=True) | |
| with col2: | |
| st.markdown(""" | |
| <a href="#" class="info-card"> | |
| <div> | |
| <div class="value">42.5%</div> | |
| <div class="label">Humidité</div> | |
| </div> | |
| <div class="icon-container"><i class="fas fa-tint"></i></div> | |
| </a> | |
| """, unsafe_allow_html=True) | |
| with col3: | |
| st.markdown(""" | |
| <a href="#" class="info-card"> | |
| <div> | |
| <div class="value">3.0mm</div> | |
| <div class="label">Précipitation</div> | |
| </div> | |
| <div class="icon-container"><i class="fas fa-cloud-rain"></i></div> | |
| </a> | |
| """, unsafe_allow_html=True) | |
| with col4: | |
| st.markdown(""" | |
| <a href="#" class="info-card"> | |
| <div> | |
| <div class="value">3.5m/s</div> | |
| <div class="label">Vent</div> | |
| </div> | |
| <div class="icon-container"><i class="fas fa-wind"></i></div> | |
| </a> | |
| """, unsafe_allow_html=True) | |
| st.markdown("---") | |
| st.markdown("<h2 id='map-plantation'><i class='fa-solid fa-map-location-dot'></i> Carte des Plantations</h2>", unsafe_allow_html=True) | |
| center_lat = 5.4746 | |
| center_lon = 10.4243 | |
| m = folium.Map(location=[center_lat, center_lon], zoom_start=10, control_scale=True) | |
| folium.Marker( | |
| location=[center_lat, center_lon], | |
| popup="<b>Bafoussam</b><br>Zone de Plantation Principale", | |
| tooltip="Bafoussam" | |
| ).add_to(m) | |
| folium.Marker( | |
| location=[5.62, 10.05], | |
| popup="<b>Zone Ouest</b><br>Quelques fermes ici.", | |
| icon=folium.Icon(color="green", icon="leaf") | |
| ).add_to(m) | |
| st_folium(m, width='100%', height=500) | |
| st.markdown(""" | |
| <p style='color: var(--text-color); margin-top: 1rem;'> | |
| Visualisation des emplacements de vos plantations et des points d'intérêt. | |
| </p> | |
| """, unsafe_allow_html=True) | |
| def show_plant_analysis_view(groq_client, model): | |
| """Affiche la page d'analyse de maladie des plantes.""" | |
| st.markdown("<h2 id='analyse-plant'><i class='fa-solid fa-microscope'></i> Analyser ma plante</h2>", unsafe_allow_html=True) | |
| st.markdown("Téléchargez une image de la feuille de votre plante pour obtenir un diagnostic instantané et des recommandations.") | |
| if not model: | |
| st.error("Le service d'analyse est indisponible car le modèle de prédiction n'a pas pu être chargé. Veuillez vérifier le chemin du modèle.") | |
| return | |
| uploaded_file = st.file_uploader("Choisissez une image...", type=["jpg", "jpeg", "png"], label_visibility="collapsed", key="plant_analysis_uploader") | |
| if uploaded_file is not None: | |
| col1, col2 = st.columns([0.8, 1.2]) # Donner plus de place aux résultats | |
| with col1: | |
| st.image(uploaded_file, caption="Image téléchargée", use_container_width=True) | |
| with col2: | |
| with st.spinner("Analyse de l'image en cours..."): | |
| try: | |
| image = Image.open(uploaded_file).convert('RGB') | |
| # REDIMENSIONNEMENT À LA TAILLE ATTENDUE PAR VOTRE MODÈLE | |
| img_resized = image.resize((IMG_WIDTH, IMG_HEIGHT)) | |
| img_array = tf.keras.preprocessing.image.img_to_array(img_resized) | |
| img_array = tf.expand_dims(img_array, 0) # Ajoute une dimension de batch | |
| # NORMALISATION: Assurez-vous que c'est la même que celle utilisée lors de l'entraînement | |
| # Si votre modèle attend des valeurs entre 0 et 1 | |
| img_array = tf.cast(img_array, tf.float32) / 255.0 | |
| # Si votre modèle gère la normalisation en interne (ex: Rescaling layer), supprimez la ligne ci-dessus. | |
| prediction = model.predict(img_array) | |
| predicted_class_index = np.argmax(prediction) | |
| predicted_class_name = CLASS_NAMES_INV[predicted_class_index] | |
| # Formatage du nom pour une meilleure lisibilité | |
| parts = predicted_class_name.split('___') | |
| plant_name = parts[0].replace('_', ' ') | |
| disease_name = parts[1].replace('_', ' ') | |
| except Exception as e: | |
| st.error(f"Erreur lors du traitement de l'image ou de la prédiction : {e}") | |
| st.info("Assurez-vous que l'image est valide et que le modèle est compatible.") | |
| return # Arrête le traitement si erreur | |
| st.subheader("Résultats de l'analyse") | |
| if 'healthy' in disease_name.lower(): | |
| st.success(f"**Diagnostic :** La plante ({plant_name}) semble **saine**.") | |
| st.balloons() | |
| st.markdown("<hr>", unsafe_allow_html=True) | |
| st.subheader("Conseils pour une plante saine") | |
| # MODIFIED: Simulate Groq response for healthy plant | |
| st.markdown(f""" | |
| ### 🌱 Conseils pour une Plante Saine | |
| - **Arrosage Optimal :** Assurez-vous que votre plante {plant_name} reçoit la juste quantité d'eau, en évitant l'excès ou le manque. | |
| - **Lumière Adéquate :** Positionnez-la là où elle bénéficiera de l'exposition au soleil idéale pour son type. | |
| - **Nutriments Essentiels :** Apportez les engrais nécessaires selon les cycles de croissance et les besoins spécifiques de la plante. | |
| - **Surveillance Continue :** Inspectez régulièrement les feuilles et les tiges pour détecter tout signe inhabituel ou parasite. | |
| *Votre {plant_name} est en pleine forme ! Continuez d'appliquer ces bons gestes pour une croissance vigoureuse.* | |
| """) | |
| else: | |
| st.warning(f"**Diagnostic :** {plant_name} - **{disease_name}**") | |
| # MODIFIED: Simulate Groq response for diseased plant | |
| st.markdown("<hr>", unsafe_allow_html=True) | |
| st.subheader("Recommandations") | |
| st.markdown(f""" | |
| ### 🧐 Causes Principales | |
| - **Conditions Environnementales :** Souvent liée à une humidité excessive, un drainage insuffisant, ou des variations de température. | |
| - **Manque de Nutriments :** Une carence spécifique peut affaiblir la {plant_name} et la rendre susceptible à la {disease_name}. | |
| - **Agents Pathogènes :** La présence de champignons, bactéries ou virus, souvent propagés par le vent, l'eau ou les outils. | |
| ### 🛡️ Solutions et Traitements | |
| **Prévention :** | |
| - **Hygiène des Outils :** Nettoyez et désinfectez régulièrement vos outils de jardinage. | |
| - **Rotation des Cultures :** Évitez de replanter la même espèce au même endroit pour rompre le cycle des maladies. | |
| - **Ventilation :** Assurez une bonne circulation de l'air autour de vos plantes pour réduire l'humidité. | |
| **Traitements Biologiques :** | |
| - **Pulvérisations Naturelles :** Des solutions à base de neem, de bicarbonate de soude ou de purin d'ortie peuvent aider à contrôler la {disease_name}. | |
| - **Introduction de Prédateurs :** Dans certains cas, l'utilisation d'insectes bénéfiques peut aider à gérer les vecteurs de la maladie. | |
| **Traitements Chimiques :** | |
| - **Fongicides/Bactéricides Spécifiques :** Si la maladie est sévère, utilisez des produits homologués pour la {disease_name}, en respectant scrupuleusement les instructions et les délais avant récolte. | |
| *Agissez rapidement pour donner à votre {plant_name} les meilleures chances de se rétablir. Courage !* | |
| """) | |
| def show_chatbot_view(groq_client): # Keep groq_client in signature for consistency, but won't be used | |
| """Affiche la page de l'assistant virtuel (chatbot).""" | |
| st.markdown("<h2><i class='fa-solid fa-comments'></i> Assistant Virtuel</h2>", unsafe_allow_html=True) | |
| st.markdown("Posez-moi n'importe quelle question sur l'agriculture, vos cultures, ou les résultats de vos analyses.") | |
| # MODIFIED: No longer check for groq_client to enable/disable, but provide info | |
| st.info("L'assistant virtuel est actuellement en mode de démonstration. Il ne se connecte pas à une IA réelle.") | |
| # Bouton pour effacer l'historique dans la barre latérale | |
| with st.sidebar: | |
| if st.button("Effacer l'historique du Chat", use_container_width=True, key="clear_chat_button"): | |
| st.session_state.messages = [{"role": "bot", "content": "Bonjour ! Je suis votre assistant virtuel GreenField. Comment puis-je vous aider aujourd'hui ?"}] | |
| st.rerun() | |
| # Initialisation de l'historique du chat | |
| if "messages" not in st.session_state: | |
| st.session_state.messages = [{"role": "bot", "content": "Bonjour ! Je suis votre assistant virtuel GreenField. Comment puis-je vous aider aujourd'hui ?"}] | |
| # Affichage des messages | |
| for message in st.session_state.messages: | |
| with st.chat_message(message["role"], avatar='🧑🌾' if message["role"] == 'user' else '🤖'): | |
| st.markdown(message["content"]) | |
| st.markdown("<div style='margin-top: 1.5rem;'></div>", unsafe_allow_html=True) | |
| # --- Form for text input and file uploader --- | |
| with st.form("chat_input_form", clear_on_submit=True): | |
| message_input = st.text_input("💬 Entrez votre message ici :", "", key="chat_text_input", autocomplete="off") | |
| # COMMENTED OUT: Removed audio file uploader as it required Groq Whisper | |
| # audio_file_uploader = st.file_uploader("📢 Téléversez un message audio (format m4a, mp3, wav)", type=["m4a", "mp3", "wav"], key="chat_audio_uploader") | |
| send_button = st.form_submit_button("✉️ Envoyer Message", type="primary") | |
| processed_message_content = "" | |
| # Logique de traitement du formulaire soumis (texte seulement maintenant) | |
| if send_button: | |
| # COMMENTED OUT: Logic for audio file uploader, as it uses Groq Whisper | |
| # if audio_file_uploader: | |
| # st.info("Traitement du fichier audio téléversé...") | |
| # filename = None | |
| # try: | |
| # filename = tempfile.NamedTemporaryFile(delete=False, suffix=f".{audio_file_uploader.name.split('.')[-1]}").name | |
| # with open(filename, "wb") as f: | |
| # f.write(audio_file_uploader.getvalue()) | |
| # with open(filename, "rb") as file_to_transcribe: | |
| # with st.spinner("Transcription audio en cours..."): | |
| # # transcription = groq_client.audio.transcriptions.create(...) # This line is now commented out | |
| # processed_message_content = "Ceci est une transcription simulée de votre message audio." | |
| # st.session_state.messages.append({"role": "user", "content": processed_message_content}) | |
| # except Exception as e: | |
| # st.error(f"Erreur Transcription (Fichier Téléversé): {e}") | |
| # processed_message_content = "" | |
| # finally: | |
| # if filename and os.path.exists(filename): | |
| # os.remove(filename) | |
| if message_input: # If text input is provided | |
| processed_message_content = message_input | |
| st.session_state.messages.append({"role": "user", "content": processed_message_content}) | |
| # Si un message a été traité avec succès (texte seulement) | |
| if processed_message_content: | |
| with st.spinner("Le chatbot réfléchit..."): | |
| # MODIFIED: Simulate Groq response | |
| simulated_response = "Bonjour ! Je suis votre assistant virtuel GreenField. Pour l'instant, je ne peux que simuler des réponses, mais je suis là pour vous aider à explorer les fonctionnalités de l'application. N'hésitez pas à naviguer !" | |
| st.session_state.messages.append({"role": "bot", "content": simulated_response}) | |
| st.rerun() # Force rerun to update chat history and show response | |
| # --- LOGIQUE PRINCIPALE DE L'APPLICATION --- | |
| # Chargement du CSS et Font Awesome | |
| load_css(CSS_PATH) | |
| st.markdown("""<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">""", unsafe_allow_html=True) | |
| # COMMENTED OUT: Initialisation du client Groq | |
| groq_client = None # Set to None directly to indicate it's not active | |
| # try: | |
| # load_dotenv() # Charge les variables d'environnement du fichier .env | |
| # groq_api_key = os.environ.get("GROQ_API_KEY") | |
| # if groq_api_key: | |
| # groq_client = Groq(api_key=groq_api_key) | |
| # else: | |
| # st.sidebar.warning("GROQ_API_KEY non trouvée. Le chatbot et les recommandations d'analyse ne seront pas disponibles. Veuillez l'ajouter à votre fichier `.env`.") | |
| # except Exception as e: | |
| # st.sidebar.error(f"Erreur d'initialisation Groq : {e}. Le chatbot et les recommandations seront désactivés.") | |
| # Chargement du modèle Keras (mise en cache automatique par @st.cache_resource) | |
| # MODIFIED: Call the new function for loading from Hugging Face Hub | |
| keras_model = load_keras_model_from_hub(HF_REPO_ID, HF_MODEL_FILENAME) | |
| # Configuration de la barre latérale | |
| with st.sidebar: | |
| try: | |
| logo_image = Image.open(LOGO_PATH) | |
| st.image(logo_image, use_container_width=True) | |
| except FileNotFoundError: | |
| st.warning(f"Logo non trouvé à {LOGO_PATH}. L'image par défaut sera utilisée.") | |
| except Exception as e: | |
| st.warning(f"Erreur lors du chargement du logo : {e}") | |
| st.markdown("<h2 style='text-align: center;'>Menu Principal</h2>", unsafe_allow_html=True) # Titre du menu | |
| # Boutons de navigation | |
| if st.button("📊 Tableau de Bord", use_container_width=True, key="nav_dashboard"): | |
| st.session_state.current_view = "dashboard" | |
| st.rerun() | |
| # Bouton pour la nouvelle vue d'analyse de plante | |
| if st.button("🌱 Analyser ma plante", use_container_width=True, key="nav_analysis"): | |
| st.session_state.current_view = "plant_analysis" # Nouvelle vue | |
| st.rerun() | |
| if st.button("💬 Assistant Virtuel", use_container_width=True, key="nav_chat"): | |
| st.session_state.current_view = "chat" | |
| st.rerun() | |
| st.markdown("<hr style='margin: 1.5rem 0;'>", unsafe_allow_html=True) # Séparateur | |
| # Bouton de déconnexion (simulé) | |
| if st.button("🚪 Déconnexion", use_container_width=True, key="nav_logout"): | |
| st.warning("Déconnexion simulée. Ajoutez votre logique de déconnexion ici.") | |
| st.session_state.current_view = "dashboard" # Redirige vers le tableau de bord après déconnexion | |
| st.rerun() # Pour rafraîchir la page après la déconnexion simulée | |
| st.markdown("<div style='text-align: center; font-size: 0.8rem; color: var(--text-color-light); margin-top: 2rem;'>© 2025 GreenField Pro. Tous droits réservés.</div>", unsafe_allow_html=True) | |
| # Logique de routage pour afficher la bonne vue | |
| if "current_view" not in st.session_state: | |
| st.session_state.current_view = "dashboard" # Vue par défaut | |
| if st.session_state.current_view == "dashboard": | |
| show_dashboard_view() | |
| elif st.session_state.current_view == "plant_analysis": # Appel à la nouvelle vue | |
| show_plant_analysis_view(groq_client, keras_model) # groq_client est passé mais est None et n'est pas utilisé | |
| elif st.session_state.current_view == "chat": | |
| show_chatbot_view(groq_client) # groq_client est passé mais est None et n'est pas utilisé | |
| else: # Fallback pour toute valeur inattendue | |
| show_dashboard_view() |