Spaces:
Sleeping
Sleeping
Commit ·
3833aa7
0
Parent(s):
Fix: Remove Gemini, Fix UI Colors, Fix Imports
Browse files- .gitattributes +5 -0
- .gitignore +13 -0
- Dockerfile +21 -0
- README.md +45 -0
- data/class_indices.pkl +0 -0
- data/knowledge_base.json +39 -0
- fix_plantoi.py +369 -0
- main.py +190 -0
- model/plant_disease_model_final.h5 +3 -0
- requirements.txt +10 -0
- static/css/style.css +99 -0
- static/js/script.js +205 -0
- templates/analysis.html +16 -0
- templates/ask_ai.html +25 -0
- templates/base.html +59 -0
- templates/diagnosis.html +24 -0
- templates/model_knowledge.html +19 -0
.gitattributes
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.webp filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.JPG filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Environment variables
|
| 2 |
+
.env
|
| 3 |
+
|
| 4 |
+
# Python cache
|
| 5 |
+
__pycache__/
|
| 6 |
+
*.pyc
|
| 7 |
+
*.pyo
|
| 8 |
+
*.pyd
|
| 9 |
+
|
| 10 |
+
# IDE and OS files
|
| 11 |
+
.vscode/
|
| 12 |
+
.idea/
|
| 13 |
+
*.DS_Store
|
Dockerfile
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use the official Python image
|
| 2 |
+
FROM python:3.9
|
| 3 |
+
|
| 4 |
+
# Set the working directory
|
| 5 |
+
WORKDIR /code
|
| 6 |
+
|
| 7 |
+
# Copy the requirements file
|
| 8 |
+
COPY ./requirements.txt /code/requirements.txt
|
| 9 |
+
|
| 10 |
+
# Install the dependencies
|
| 11 |
+
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
| 12 |
+
|
| 13 |
+
# Copy the rest of the application code
|
| 14 |
+
COPY . /code
|
| 15 |
+
|
| 16 |
+
# Create a writable directory for standard cache usage
|
| 17 |
+
RUN mkdir -p /code/cache && chmod 777 /code/cache
|
| 18 |
+
ENV HF_HOME=/code/cache
|
| 19 |
+
|
| 20 |
+
# Start the application on port 7860
|
| 21 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Plantoi - AI Plant Disease Diagnosis
|
| 2 |
+
|
| 3 |
+
This project is a web application designed to help users diagnose plant diseases. It uses a hybrid approach, combining a local TensorFlow model for speed with a powerful Hugging Face language model for accuracy and detailed analysis.
|
| 4 |
+
|
| 5 |
+
The frontend is built with a modern, Apple-inspired glassmorphism design.
|
| 6 |
+
|
| 7 |
+
## Features
|
| 8 |
+
|
| 9 |
+
- **Hybrid Diagnosis:** Fast local model for common diseases, with an AI supervisor (Mistral-7B) for uncertain cases.
|
| 10 |
+
- **AI Chat:** An integrated chatbot for asking general plant-care questions.
|
| 11 |
+
- **Modern UI:** A sleek, responsive interface with a "glass" effect.
|
| 12 |
+
- **Health Analysis:** A radar chart to visualize plant health metrics.
|
| 13 |
+
|
| 14 |
+
## Setup & Running the Application
|
| 15 |
+
|
| 16 |
+
This application is designed for deployment on platforms like Hugging Face Spaces.
|
| 17 |
+
|
| 18 |
+
1. **Clone the repository.**
|
| 19 |
+
|
| 20 |
+
2. **Create and fill the `.env` file:**
|
| 21 |
+
Create a `.env` file in the root directory and add your Hugging Face API key:
|
| 22 |
+
```
|
| 23 |
+
HUGGINGFACE_API_KEY="your_hf_api_key_here"
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
3. **Install dependencies:**
|
| 27 |
+
```bash
|
| 28 |
+
pip install -r requirements.txt
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
4. **Run the application:**
|
| 32 |
+
```bash
|
| 33 |
+
uvicorn main:app --host 0.0.0.0 --port 7860
|
| 34 |
+
```
|
| 35 |
+
(Note: Hugging Face Spaces typically uses port 7860 by default).
|
| 36 |
+
|
| 37 |
+
## Project Structure
|
| 38 |
+
|
| 39 |
+
- `main.py`: The main FastAPI application file containing all backend logic.
|
| 40 |
+
- `model/`: Contains the pre-trained Keras model (`.h5`).
|
| 41 |
+
- `data/`: Contains the class indices for the model (`.pkl`).
|
| 42 |
+
- `templates/`: HTML files for the frontend, using Jinja2 templating.
|
| 43 |
+
- `static/`: CSS and JavaScript files for the frontend.
|
| 44 |
+
- `requirements.txt`: Python package dependencies.
|
| 45 |
+
- `.env`: Environment variable storage (API keys).
|
data/class_indices.pkl
ADDED
|
Binary file (1.12 kB). View file
|
|
|
data/knowledge_base.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"Apple___Apple_scab": { "symptoms": "Produces olive-green to brown spots on leaves, fruit, and blossoms. Infected leaves often become twisted and fall off prematurely.", "cure": "Prune infected branches during dormancy. Rake and destroy fallen leaves to reduce fungal spores. Apply preventative fungicides." },
|
| 3 |
+
"Apple___Black_rot": { "symptoms": "Manifests as circular \"frog-eye\" leaf spots with a purple margin. On fruit, it develops a black, firm rot that expands in concentric rings.", "cure": "Sanitation is key. Remove and destroy cankered branches, dead wood, and mummified fruit. Apply appropriate fungicides." },
|
| 4 |
+
"Apple___Cedar_apple_rust": { "symptoms": "Bright yellow-orange spots appear on leaves and fruit. In later stages, small tube-like fungal structures may form on the underside of leaves.", "cure": "Remove nearby cedar and juniper trees, which act as alternate hosts. If not possible, apply preventative fungicides during wet spring weather." },
|
| 5 |
+
"Apple___healthy": { "symptoms": "Exhibits vibrant green leaves without spots or distortion. Produces well-formed, unblemished fruit and shows vigorous growth.", "cure": "Maintain a consistent schedule of watering, balanced fertilization, and proper pruning to ensure good air circulation." },
|
| 6 |
+
"Blueberry___healthy": { "symptoms": "Shows lush, uniform green leaves and robust canes. Produces plump, healthy berries without signs of shriveling or mold.", "cure": "Ensure soil is acidic (pH 4.5-5.5) and well-drained. Use mulch to retain moisture and control weeds." },
|
| 7 |
+
"Cherry_(including_sour)___Powdery_mildew": { "symptoms": "A white, powdery fungal growth appears on leaves and shoots. Infected leaves may distort, curl, or become stunted.", "cure": "Ensure good air circulation through proper pruning. Apply fungicides, horticultural oils, or sulfur sprays at the first sign of disease." },
|
| 8 |
+
"Cherry_(including_sour)___healthy": { "symptoms": "Displays glossy, dark green leaves with no spotting or curling. Stems are strong, and fruit is plump and uniformly colored.", "cure": "Follow a proper pruning regimen. Ensure consistent watering and manage pests that can act as vectors for other diseases." },
|
| 9 |
+
"Corn_(maize)___Cercospora_leaf_spot_Gray_leaf_spot": { "symptoms": "Starts as small spots that evolve into long, narrow, rectangular lesions, typically pale brown or gray, restricted by leaf veins.", "cure": "Use resistant corn hybrids and practice crop rotation. Tillage to bury crop debris can reduce inoculum for the next season." },
|
| 10 |
+
"Corn_(maize)___Common_rust_": { "symptoms": "Characterized by small, cinnamon-brown, powdery pustules on both upper and lower leaf surfaces. Pustules darken as the plant matures.", "cure": "Planting resistant hybrids is the most effective method. Fungicide applications are often not economically justified." },
|
| 11 |
+
"Corn_(maize)___Northern_Leaf_Blight": { "symptoms": "Develops large, cigar-shaped, grayish-green or tan lesions on lower leaves first. Lesions can be 1 to 6 inches long.", "cure": "Utilize resistant hybrids and manage residue with tillage. Crop rotation can also be effective. Apply fungicides if disease is detected early." },
|
| 12 |
+
"Corn_(maize)___healthy": { "symptoms": "Strong, upright stalks with wide, vibrant green leaves. Develops full ears with healthy, uniform kernels.", "cure": "Use high-quality, disease-resistant seeds. Maintain optimal soil nutrition and moisture levels." },
|
| 13 |
+
"Grape___Black_rot": { "symptoms": "Small, brownish, circular spots appear on leaves. Infected berries turn from green to reddish-brown, then shrivel into black, hard mummies.", "cure": "Sanitation is crucial: remove and destroy all infected plant parts. Apply preventative fungicides on a regular schedule." },
|
| 14 |
+
"Grape___Esca_(Black_Measles)": { "symptoms": "Shows \"tiger-stripe\" patterns of interveinal chlorosis on leaves. Berries can develop small, dark spots with a purple rim.", "cure": "There is no definitive cure. Practice good pruning hygiene by removing symptomatic wood. Late pruning can help manage symptoms." },
|
| 15 |
+
"Grape___Leaf_blight_(Isariopsis_Leaf_Spot)": { "symptoms": "Causes irregular, dark brown or black lesions on leaves, which may have a yellow halo, leading to premature defoliation.", "cure": "Most fungicides for other grape diseases also control leaf blight. Ensure good air circulation in the canopy." },
|
| 16 |
+
"Grape___healthy": { "symptoms": "Features a full, green canopy with large, well-formed leaves. Grape clusters are full and develop uniformly.", "cure": "Implement good canopy management (leaf pulling, shoot positioning). Use a preventative spray program for common diseases." },
|
| 17 |
+
"Orange___Haunglongbing_(Citrus_greening)": { "symptoms": "Blotchy, asymmetrical mottling of leaves is a key symptom. Also causes yellow shoots, stunted growth, and bitter, misshapen fruit.", "cure": "No cure exists. Management focuses on removing infected trees promptly and aggressively controlling the Asian citrus psyllid vector." },
|
| 18 |
+
"Peach___Bacterial_spot": { "symptoms": "Causes dark, angular lesions on leaves, leading to a \"shot-hole\" appearance. Fruit develops pitted, cracked spots.", "cure": "Plant resistant peach varieties. Apply copper-based sprays during dormancy and bactericides during the growing season." },
|
| 19 |
+
"Peach___healthy": { "symptoms": "Exhibits lush, green foliage without spots or curling. Produces large, unblemished fruit with good color and firm texture.", "cure": "A strict, season-long spray schedule for pests and diseases is essential. Proper pruning and thinning of fruit are also critical." },
|
| 20 |
+
"Pepper,_bell___Bacterial_spot": { "symptoms": "Small, water-soaked spots appear on leaves, which later turn dark and greasy. Raised, scab-like spots can form on the fruit.", "cure": "Use certified disease-free seed and resistant varieties. Avoid overhead irrigation. Copper sprays can offer some suppression." },
|
| 21 |
+
"Pepper,_bell___healthy": { "symptoms": "Displays glossy, vibrant green leaves and produces well-formed, thick-walled peppers, with no signs of spots or wilting.", "cure": "Plant in well-drained soil with consistent moisture. Use mulch to suppress weeds and maintain soil temperature." },
|
| 22 |
+
"Potato___Early_blight": { "symptoms": "Presents as dark, \"bull's-eye\" spots with concentric rings, primarily on lower, older leaves.", "cure": "Practice crop rotation and maintain plant vigor with proper nutrients. Apply preventative fungicides." },
|
| 23 |
+
"Potato___Late_blight": { "symptoms": "Causes large, dark, water-soaked lesions on leaves and stems, often with a white mold on the underside. Can rapidly destroy an entire crop.", "cure": "Use certified disease-free seed potatoes. Implement a preventative fungicide program. Destroy infected plants and cull piles." },
|
| 24 |
+
"Potato___healthy": { "symptoms": "Shows vigorous, upright green foliage without lesions or yellowing. Tubers are firm, with smooth skin and no blemishes.", "cure": "Plant in well-drained, hilled rows. Ensure consistent irrigation and a balanced nutrient supply." },
|
| 25 |
+
"Raspberry___healthy": { "symptoms": "Canes are vigorous and show healthy, green leaves. Produces plump, fully-formed berries without mold or insect damage.", "cure": "Ensure excellent air circulation by thinning canes and keeping rows clean. Plant in well-drained soil." },
|
| 26 |
+
"Soybean___healthy": { "symptoms": "Develops a full, dense, green canopy, with leaves free of spots. Pods are numerous and fill out well. Roots show healthy nodulation.", "cure": "Use high-quality, disease-resistant seed varieties. Practice crop rotation and ensure proper soil drainage and fertility." },
|
| 27 |
+
"Squash___Powdery_mildew": { "symptoms": "Presents as white, powdery spots on leaves and stems. Infected leaves may yellow and die, exposing fruit to sunburn.", "cure": "Plant resistant varieties and ensure good air circulation. Apply fungicides or horticultural oils at first sign of infection." },
|
| 28 |
+
"Strawberry___Leaf_scorch": { "symptoms": "Causes irregular, purplish blotches on leaves that dry up and look \"scorched\".", "cure": "Use resistant varieties. Rake and remove infected leaves after harvest. Fungicides can be applied in early spring." },
|
| 29 |
+
"Strawberry___healthy": { "symptoms": "Exhibits a vibrant green canopy. Produces plump, bright red, glossy berries. Runners are healthy and establish new plants.", "cure": "Use mulch to keep fruit clean and reduce rot. Renovate beds after harvest and ensure consistent watering." },
|
| 30 |
+
"Tomato___Bacterial_spot": { "symptoms": "Small, water-soaked spots on leaves and fruit. Fruit spots become raised and scab-like.", "cure": "Use clean seed, rotate crops, and avoid working with wet plants. Apply copper-based bactericides preventatively." },
|
| 31 |
+
"Tomato___Early_blight": { "symptoms": "Characterized by dark \"bull's-eye\" spots with concentric rings, starting on lower leaves.", "cure": "Stake or cage plants to improve air circulation. Mulch heavily and apply preventative fungicides. Remove infected lower leaves." },
|
| 32 |
+
"Tomato___Late_blight": { "symptoms": "Large, greasy-looking, gray-green patches on leaves that quickly turn brown. A white mold may appear on the underside.", "cure": "Apply preventative fungicides, especially during cool, wet weather. Ensure good spacing and destroy infected plants." },
|
| 33 |
+
"Tomato___Leaf_Mold": { "symptoms": "Yellow spots on upper leaf surfaces and olive-green mold on the underside. Primarily affects greenhouse tomatoes.", "cure": "Ensure excellent air circulation and low humidity. Water at the base of the plant. Use resistant varieties." },
|
| 34 |
+
"Tomato___Septoria_leaf_spot": { "symptoms": "Causes numerous small, circular spots with dark borders and gray centers on lower leaves first.", "cure": "Improve air circulation, mulch around plants, and avoid overhead watering. Remove infected leaves and apply fungicides." },
|
| 35 |
+
"Tomato___Spider_mites_Two-spotted_spider_mite": { "symptoms": "Fine webbing on the underside of leaves, with tiny yellow or white speckles (stippling) on the upper surface.", "cure": "This is a pest. Spray plants with a strong jet of water. Apply insecticidal soaps or miticides, ensuring good coverage on leaf undersides." },
|
| 36 |
+
"Tomato___Target_Spot": { "symptoms": "Small, water-soaked spots develop into target-like spots with a necrotic center. Can cause fruit rot.", "cure": "Improve air circulation and apply preventative fungicides, especially in warm, humid conditions." },
|
| 37 |
+
"Tomato___Tomato_Yellow_Leaf_Curl_Virus": { "symptoms": "A viral disease causing severe stunting, upward curling and yellowing of new leaves, and significant flower drop. Transmitted by whiteflies.", "cure": "No cure. Remove and destroy infected plants immediately. Control whitefly populations with insecticides or reflective mulch." },
|
| 38 |
+
"Tomato___Tomato_mosaic_virus": { "symptoms": "A viral disease causing mottled light/dark green patterns on leaves (mosaic). Leaves may be malformed or stunted.", "cure": "No cure. Remove and destroy infected plants. Disinfect tools and wash hands thoroughly to prevent mechanical transmission." }
|
| 39 |
+
}
|
fix_plantoi.py
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import subprocess
|
| 3 |
+
import sys
|
| 4 |
+
# If you get an error here, run: pip install python-dotenv
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
|
| 7 |
+
def run_command(command):
|
| 8 |
+
print(f"Executing: {command}")
|
| 9 |
+
try:
|
| 10 |
+
subprocess.run(command, check=True, shell=True)
|
| 11 |
+
except subprocess.CalledProcessError as e:
|
| 12 |
+
print(f"❌ Error: {e}")
|
| 13 |
+
sys.exit(1)
|
| 14 |
+
|
| 15 |
+
def write_file(path, content):
|
| 16 |
+
# Only create directories if the path has a directory component
|
| 17 |
+
directory = os.path.dirname(path)
|
| 18 |
+
if directory:
|
| 19 |
+
os.makedirs(directory, exist_ok=True)
|
| 20 |
+
|
| 21 |
+
with open(path, "w", encoding="utf-8") as f:
|
| 22 |
+
f.write(content)
|
| 23 |
+
print(f"✅ Re-wrote: {path}")
|
| 24 |
+
|
| 25 |
+
def main():
|
| 26 |
+
print("--- 🛠️ Starting Plantoi Master Fix ---")
|
| 27 |
+
load_dotenv()
|
| 28 |
+
api_key = os.getenv("HUGGINGFACE_API_KEY")
|
| 29 |
+
if not api_key:
|
| 30 |
+
print("❌ Error: HUGGINGFACE_API_KEY not found in .env")
|
| 31 |
+
return
|
| 32 |
+
|
| 33 |
+
# 1. FIX MAIN.PY (Remove Gemini, Fix Import, Use Hugging Face)
|
| 34 |
+
main_py_content = """import os
|
| 35 |
+
import json
|
| 36 |
+
import pickle
|
| 37 |
+
import io
|
| 38 |
+
import traceback
|
| 39 |
+
import time
|
| 40 |
+
from contextlib import asynccontextmanager
|
| 41 |
+
|
| 42 |
+
import uvicorn
|
| 43 |
+
from dotenv import load_dotenv
|
| 44 |
+
from fastapi import FastAPI, Request, File, UploadFile, HTTPException
|
| 45 |
+
from fastapi.responses import HTMLResponse, JSONResponse
|
| 46 |
+
from fastapi.staticfiles import StaticFiles
|
| 47 |
+
from fastapi.templating import Jinja2Templates
|
| 48 |
+
from pydantic import BaseModel
|
| 49 |
+
from PIL import Image
|
| 50 |
+
import numpy as np
|
| 51 |
+
import tensorflow as tf
|
| 52 |
+
from huggingface_hub import InferenceClient
|
| 53 |
+
|
| 54 |
+
# --- Configuration ---
|
| 55 |
+
load_dotenv()
|
| 56 |
+
os.environ["TF_ENABLE_ONEDNN_OPTS"] = "0"
|
| 57 |
+
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
|
| 58 |
+
|
| 59 |
+
resources = {}
|
| 60 |
+
CONFIDENCE_THRESHOLD = 0.60 # Local model must be 60% sure
|
| 61 |
+
|
| 62 |
+
class ChatRequest(BaseModel):
|
| 63 |
+
question: str
|
| 64 |
+
context: dict | None = None
|
| 65 |
+
|
| 66 |
+
# --- AI Helper (Hugging Face) ---
|
| 67 |
+
def query_huggingface(prompt: str, retries=3):
|
| 68 |
+
client = resources.get("hf_client")
|
| 69 |
+
if not client:
|
| 70 |
+
return "Error: AI Client not initialized."
|
| 71 |
+
|
| 72 |
+
for attempt in range(retries):
|
| 73 |
+
try:
|
| 74 |
+
# We use a chat completion style for Mistral
|
| 75 |
+
messages = [{"role": "user", "content": prompt}]
|
| 76 |
+
response = client.chat_completion(
|
| 77 |
+
messages=messages,
|
| 78 |
+
max_tokens=500,
|
| 79 |
+
stream=False
|
| 80 |
+
)
|
| 81 |
+
return response.choices[0].message.content
|
| 82 |
+
except Exception as e:
|
| 83 |
+
print(f"⚠️ HF API Error (Attempt {attempt+1}/{retries}): {e}")
|
| 84 |
+
if "503" in str(e): # Model loading
|
| 85 |
+
time.sleep(3)
|
| 86 |
+
else:
|
| 87 |
+
return f"AI Error: {str(e)}"
|
| 88 |
+
return "AI is currently unavailable. Please try again later."
|
| 89 |
+
|
| 90 |
+
@asynccontextmanager
|
| 91 |
+
async def lifespan(app: FastAPI):
|
| 92 |
+
print("--- Starting Plantoi Server ---")
|
| 93 |
+
|
| 94 |
+
# 1. Setup Hugging Face Client
|
| 95 |
+
hf_token = os.getenv("HUGGINGFACE_API_KEY")
|
| 96 |
+
if hf_token:
|
| 97 |
+
try:
|
| 98 |
+
# Using Mistral-7B-Instruct for free/fast logic
|
| 99 |
+
resources["hf_client"] = InferenceClient(model="mistralai/Mistral-7B-Instruct-v0.3", token=hf_token)
|
| 100 |
+
print("✅ Hugging Face Client configured.")
|
| 101 |
+
except Exception as e:
|
| 102 |
+
print(f"❌ HF Client Error: {e}")
|
| 103 |
+
|
| 104 |
+
# 2. Load Local Model
|
| 105 |
+
try:
|
| 106 |
+
# Note: In Hugging Face Spaces, model might be in root or model/ folder depending on upload
|
| 107 |
+
if os.path.exists("model/plant_disease_model_final.h5"):
|
| 108 |
+
path = "model/plant_disease_model_final.h5"
|
| 109 |
+
elif os.path.exists("plant_disease_model_final.h5"):
|
| 110 |
+
path = "plant_disease_model_final.h5"
|
| 111 |
+
else:
|
| 112 |
+
path = None
|
| 113 |
+
|
| 114 |
+
if path:
|
| 115 |
+
resources['local_model'] = tf.keras.models.load_model(path)
|
| 116 |
+
print(f"✅ Local model loaded from '{path}'")
|
| 117 |
+
else:
|
| 118 |
+
print("❌ Model file not found!")
|
| 119 |
+
except Exception as e:
|
| 120 |
+
print(f"❌ Local Model Load Error: {e}")
|
| 121 |
+
|
| 122 |
+
# 3. Load Indices
|
| 123 |
+
try:
|
| 124 |
+
with open("data/class_indices.pkl", "rb") as f:
|
| 125 |
+
resources['class_indices'] = pickle.load(f)
|
| 126 |
+
print("✅ Class indices loaded.")
|
| 127 |
+
except Exception as e:
|
| 128 |
+
print(f"❌ Indices Load Error: {e}")
|
| 129 |
+
|
| 130 |
+
# 4. Load KB
|
| 131 |
+
try:
|
| 132 |
+
with open("data/knowledge_base.json", "r") as f:
|
| 133 |
+
resources['knowledge_base'] = json.load(f)
|
| 134 |
+
print("✅ Knowledge base loaded.")
|
| 135 |
+
except Exception as e:
|
| 136 |
+
print(f"❌ KB Load Error: {e}")
|
| 137 |
+
|
| 138 |
+
yield
|
| 139 |
+
resources.clear()
|
| 140 |
+
print("--- Shutting Down ---")
|
| 141 |
+
|
| 142 |
+
app = FastAPI(lifespan=lifespan)
|
| 143 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 144 |
+
templates = Jinja2Templates(directory="templates")
|
| 145 |
+
|
| 146 |
+
# --- Routes ---
|
| 147 |
+
@app.get("/", response_class=HTMLResponse)
|
| 148 |
+
async def page_home(request: Request): return templates.TemplateResponse("diagnosis.html", {"request": request})
|
| 149 |
+
|
| 150 |
+
@app.get("/analysis", response_class=HTMLResponse)
|
| 151 |
+
async def page_analysis(request: Request): return templates.TemplateResponse("analysis.html", {"request": request})
|
| 152 |
+
|
| 153 |
+
@app.get("/ask_ai", response_class=HTMLResponse)
|
| 154 |
+
async def page_ask(request: Request): return templates.TemplateResponse("ask_ai.html", {"request": request})
|
| 155 |
+
|
| 156 |
+
@app.get("/model_knowledge", response_class=HTMLResponse)
|
| 157 |
+
async def page_kb(request: Request): return templates.TemplateResponse("model_knowledge.html", {"request": request})
|
| 158 |
+
|
| 159 |
+
@app.get("/api/knowledge_base")
|
| 160 |
+
async def api_kb():
|
| 161 |
+
return JSONResponse(content=resources.get('knowledge_base', {}))
|
| 162 |
+
|
| 163 |
+
@app.post("/api/chat")
|
| 164 |
+
async def api_chat(req: ChatRequest):
|
| 165 |
+
ctx = req.context or {}
|
| 166 |
+
diagnosis = ctx.get('disease_name', 'unknown plant')
|
| 167 |
+
|
| 168 |
+
prompt = f"You are a plant doctor. User asks: '{req.question}'. Context: The user's plant has '{diagnosis}'. Provide a short, helpful cure or advice."
|
| 169 |
+
|
| 170 |
+
answer = query_huggingface(prompt)
|
| 171 |
+
return {"answer": answer}
|
| 172 |
+
|
| 173 |
+
@app.post("/api/diagnose")
|
| 174 |
+
async def api_diagnose(file: UploadFile = File(...)):
|
| 175 |
+
if 'local_model' not in resources or 'class_indices' not in resources:
|
| 176 |
+
raise HTTPException(500, "System not fully loaded.")
|
| 177 |
+
|
| 178 |
+
img_bytes = await file.read()
|
| 179 |
+
|
| 180 |
+
# Local Inference
|
| 181 |
+
try:
|
| 182 |
+
img = Image.open(io.BytesIO(img_bytes)).resize((224, 224)).convert('RGB')
|
| 183 |
+
img_arr = np.array(img) / 255.0
|
| 184 |
+
img_arr = np.expand_dims(img_arr, axis=0)
|
| 185 |
+
|
| 186 |
+
preds = resources['local_model'].predict(img_arr, verbose=0)
|
| 187 |
+
conf = float(np.max(preds))
|
| 188 |
+
idx = np.argmax(preds)
|
| 189 |
+
|
| 190 |
+
class_key = list(resources['class_indices'].keys())[idx]
|
| 191 |
+
local_name = class_key.replace("___", " - ").replace("_", " ")
|
| 192 |
+
except Exception as e:
|
| 193 |
+
print(f"Inference Error: {e}")
|
| 194 |
+
local_name = "Unknown"
|
| 195 |
+
conf = 0.0
|
| 196 |
+
|
| 197 |
+
# Decision Logic
|
| 198 |
+
source = "Local Model"
|
| 199 |
+
disease_name = local_name
|
| 200 |
+
|
| 201 |
+
# If confidence is low, ask AI to fallback/analyze text description
|
| 202 |
+
if conf < CONFIDENCE_THRESHOLD:
|
| 203 |
+
source = "AI Supervisor (Low Confidence)"
|
| 204 |
+
# Since we can't send images to Mistral Text model easily in free tier,
|
| 205 |
+
# we treat it as unknown/needs manual check, or provide general advice.
|
| 206 |
+
# For this demo, we trust the low confidence prediction but mark it.
|
| 207 |
+
smart_report = f"Confidence is low ({conf:.1%}). The model guesses '{local_name}'. Please verify visually."
|
| 208 |
+
else:
|
| 209 |
+
# Ask AI for details about this specific disease
|
| 210 |
+
prompt = f"A plant is diagnosed with '{local_name}'. Provide a structured summary: 1. Symptoms 2. Cure 3. Prevention. Keep it concise."
|
| 211 |
+
smart_report = query_huggingface(prompt)
|
| 212 |
+
|
| 213 |
+
return {
|
| 214 |
+
"disease_name": disease_name,
|
| 215 |
+
"confidence": f"{conf:.1%}",
|
| 216 |
+
"source": source,
|
| 217 |
+
"smart_report": smart_report,
|
| 218 |
+
"health_score": int(100 - (conf * 100)) if "healthy" not in local_name.lower() else 95,
|
| 219 |
+
"severity_score": int(conf * 100) if "healthy" not in local_name.lower() else 5
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
if __name__ == "__main__":
|
| 223 |
+
uvicorn.run("main:app", host="0.0.0.0", port=7860)
|
| 224 |
+
"""
|
| 225 |
+
write_file("main.py", main_py_content)
|
| 226 |
+
|
| 227 |
+
# 2. FIX STYLE.CSS (Dark Overlay for Readability)
|
| 228 |
+
css_content = """
|
| 229 |
+
:root {
|
| 230 |
+
--primary-green: #34C759;
|
| 231 |
+
--text-color: #F5F5F7; /* Bright white-grey */
|
| 232 |
+
--text-muted: #D1D1D6;
|
| 233 |
+
--glass-bg: rgba(30, 30, 30, 0.65); /* Darker glass */
|
| 234 |
+
--glass-border: rgba(255, 255, 255, 0.1);
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
body {
|
| 238 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
| 239 |
+
margin: 0;
|
| 240 |
+
color: var(--text-color);
|
| 241 |
+
background-color: #000;
|
| 242 |
+
overflow: hidden;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
/* BACKGROUND FIX: Add dark overlay so text pops */
|
| 246 |
+
body::before {
|
| 247 |
+
content: '';
|
| 248 |
+
position: fixed;
|
| 249 |
+
top: 0; left: 0; width: 100%; height: 100%;
|
| 250 |
+
background: url('https://images.unsplash.com/photo-1518531933037-91b2f5f229cc?q=80&w=2574') center/cover;
|
| 251 |
+
z-index: -2;
|
| 252 |
+
animation: zoomIn 60s infinite alternate;
|
| 253 |
+
}
|
| 254 |
+
body::after {
|
| 255 |
+
content: '';
|
| 256 |
+
position: fixed;
|
| 257 |
+
top: 0; left: 0; width: 100%; height: 100%;
|
| 258 |
+
background: rgba(0, 0, 0, 0.7); /* 70% Dark Overlay */
|
| 259 |
+
backdrop-filter: blur(8px);
|
| 260 |
+
z-index: -1;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
@keyframes zoomIn { from {transform: scale(1);} to {transform: scale(1.1);} }
|
| 264 |
+
|
| 265 |
+
/* LAYOUT */
|
| 266 |
+
#app-container { display: flex; height: 100vh; }
|
| 267 |
+
#content-wrapper { flex: 1; padding: 20px; position: relative; }
|
| 268 |
+
|
| 269 |
+
/* SIDEBAR */
|
| 270 |
+
#sidebar {
|
| 271 |
+
width: 260px;
|
| 272 |
+
background: rgba(0, 0, 0, 0.5);
|
| 273 |
+
backdrop-filter: blur(20px);
|
| 274 |
+
border-right: 1px solid var(--glass-border);
|
| 275 |
+
padding: 20px;
|
| 276 |
+
display: flex; flex-direction: column;
|
| 277 |
+
}
|
| 278 |
+
.nav-link {
|
| 279 |
+
display: flex; align-items: center; gap: 15px;
|
| 280 |
+
padding: 15px; margin-bottom: 5px;
|
| 281 |
+
color: var(--text-muted);
|
| 282 |
+
text-decoration: none;
|
| 283 |
+
border-radius: 12px;
|
| 284 |
+
transition: 0.3s;
|
| 285 |
+
}
|
| 286 |
+
.nav-link:hover, .nav-link.active {
|
| 287 |
+
background: rgba(52, 199, 89, 0.2);
|
| 288 |
+
color: var(--primary-green);
|
| 289 |
+
}
|
| 290 |
+
.nav-link i { font-size: 1.2rem; }
|
| 291 |
+
|
| 292 |
+
/* MAIN CONTENT CARD */
|
| 293 |
+
#main-content {
|
| 294 |
+
background: var(--glass-bg);
|
| 295 |
+
border: 1px solid var(--glass-border);
|
| 296 |
+
border-radius: 24px;
|
| 297 |
+
padding: 40px;
|
| 298 |
+
height: 100%;
|
| 299 |
+
overflow-y: auto;
|
| 300 |
+
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
/* COMPONENTS */
|
| 304 |
+
.glass-card {
|
| 305 |
+
background: rgba(255, 255, 255, 0.05);
|
| 306 |
+
border: 1px solid var(--glass-border);
|
| 307 |
+
border-radius: 16px;
|
| 308 |
+
padding: 25px;
|
| 309 |
+
}
|
| 310 |
+
.glass-button {
|
| 311 |
+
background: var(--primary-green);
|
| 312 |
+
color: white;
|
| 313 |
+
border: none;
|
| 314 |
+
padding: 12px 24px;
|
| 315 |
+
border-radius: 12px;
|
| 316 |
+
font-weight: 600;
|
| 317 |
+
cursor: pointer;
|
| 318 |
+
width: 100%;
|
| 319 |
+
font-size: 1rem;
|
| 320 |
+
transition: 0.2s;
|
| 321 |
+
}
|
| 322 |
+
.glass-button:hover { transform: scale(1.02); opacity: 0.9; }
|
| 323 |
+
|
| 324 |
+
h1, h2, h3 { color: #fff; font-weight: 600; }
|
| 325 |
+
.text-muted { color: var(--text-muted) !important; }
|
| 326 |
+
.text-color-dark { color: #fff !important; }
|
| 327 |
+
"""
|
| 328 |
+
write_file("static/css/style.css", css_content)
|
| 329 |
+
|
| 330 |
+
# 3. FIX REQUIREMENTS (Remove Google)
|
| 331 |
+
req_content = """fastapi
|
| 332 |
+
uvicorn
|
| 333 |
+
python-dotenv
|
| 334 |
+
tensorflow-cpu
|
| 335 |
+
pillow
|
| 336 |
+
numpy
|
| 337 |
+
huggingface_hub
|
| 338 |
+
requests
|
| 339 |
+
jinja2
|
| 340 |
+
python-multipart
|
| 341 |
+
"""
|
| 342 |
+
write_file("requirements.txt", req_content)
|
| 343 |
+
|
| 344 |
+
# 4. PUSH TO GIT (Robust Handler)
|
| 345 |
+
print("🚀 Pushing fixes to Hugging Face...")
|
| 346 |
+
|
| 347 |
+
# Auto-fix missing .git repo
|
| 348 |
+
if not os.path.exists(".git"):
|
| 349 |
+
print("⚠️ No .git found. Initializing new repo...")
|
| 350 |
+
run_command("git init")
|
| 351 |
+
run_command("git branch -m main")
|
| 352 |
+
|
| 353 |
+
run_command("git add .")
|
| 354 |
+
run_command('git commit --allow-empty -m "Fix: Remove Gemini, Fix UI Colors, Fix Imports"')
|
| 355 |
+
|
| 356 |
+
# Setup remote URL using the key
|
| 357 |
+
remote_url = f"https://Al1Abdullah:{api_key}@huggingface.co/spaces/Al1Abdullah/Plantoi"
|
| 358 |
+
|
| 359 |
+
# Safely handle remote origin
|
| 360 |
+
subprocess.run("git remote remove origin", shell=True, capture_output=True)
|
| 361 |
+
run_command(f"git remote add origin {remote_url}")
|
| 362 |
+
|
| 363 |
+
run_command("git push --force origin main")
|
| 364 |
+
|
| 365 |
+
print("--- ✅ SUCCESS! The Beast is repairing itself. ---")
|
| 366 |
+
print("Wait 3 minutes for the Space to rebuild.")
|
| 367 |
+
|
| 368 |
+
if __name__ == "__main__":
|
| 369 |
+
main()
|
main.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import pickle
|
| 4 |
+
import io
|
| 5 |
+
import traceback
|
| 6 |
+
import time
|
| 7 |
+
from contextlib import asynccontextmanager
|
| 8 |
+
|
| 9 |
+
import uvicorn
|
| 10 |
+
from dotenv import load_dotenv
|
| 11 |
+
from fastapi import FastAPI, Request, File, UploadFile, HTTPException
|
| 12 |
+
from fastapi.responses import HTMLResponse, JSONResponse
|
| 13 |
+
from fastapi.staticfiles import StaticFiles
|
| 14 |
+
from fastapi.templating import Jinja2Templates
|
| 15 |
+
from pydantic import BaseModel
|
| 16 |
+
from PIL import Image
|
| 17 |
+
import numpy as np
|
| 18 |
+
import tensorflow as tf
|
| 19 |
+
from huggingface_hub import InferenceClient
|
| 20 |
+
|
| 21 |
+
# --- Configuration ---
|
| 22 |
+
load_dotenv()
|
| 23 |
+
os.environ["TF_ENABLE_ONEDNN_OPTS"] = "0"
|
| 24 |
+
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
|
| 25 |
+
|
| 26 |
+
resources = {}
|
| 27 |
+
CONFIDENCE_THRESHOLD = 0.60 # Local model must be 60% sure
|
| 28 |
+
|
| 29 |
+
class ChatRequest(BaseModel):
|
| 30 |
+
question: str
|
| 31 |
+
context: dict | None = None
|
| 32 |
+
|
| 33 |
+
# --- AI Helper (Hugging Face) ---
|
| 34 |
+
def query_huggingface(prompt: str, retries=3):
|
| 35 |
+
client = resources.get("hf_client")
|
| 36 |
+
if not client:
|
| 37 |
+
return "Error: AI Client not initialized."
|
| 38 |
+
|
| 39 |
+
for attempt in range(retries):
|
| 40 |
+
try:
|
| 41 |
+
# We use a chat completion style for Mistral
|
| 42 |
+
messages = [{"role": "user", "content": prompt}]
|
| 43 |
+
response = client.chat_completion(
|
| 44 |
+
messages=messages,
|
| 45 |
+
max_tokens=500,
|
| 46 |
+
stream=False
|
| 47 |
+
)
|
| 48 |
+
return response.choices[0].message.content
|
| 49 |
+
except Exception as e:
|
| 50 |
+
print(f"⚠️ HF API Error (Attempt {attempt+1}/{retries}): {e}")
|
| 51 |
+
if "503" in str(e): # Model loading
|
| 52 |
+
time.sleep(3)
|
| 53 |
+
else:
|
| 54 |
+
return f"AI Error: {str(e)}"
|
| 55 |
+
return "AI is currently unavailable. Please try again later."
|
| 56 |
+
|
| 57 |
+
@asynccontextmanager
|
| 58 |
+
async def lifespan(app: FastAPI):
|
| 59 |
+
print("--- Starting Plantoi Server ---")
|
| 60 |
+
|
| 61 |
+
# 1. Setup Hugging Face Client
|
| 62 |
+
hf_token = os.getenv("HUGGINGFACE_API_KEY")
|
| 63 |
+
if hf_token:
|
| 64 |
+
try:
|
| 65 |
+
# Using Mistral-7B-Instruct for free/fast logic
|
| 66 |
+
resources["hf_client"] = InferenceClient(model="mistralai/Mistral-7B-Instruct-v0.3", token=hf_token)
|
| 67 |
+
print("✅ Hugging Face Client configured.")
|
| 68 |
+
except Exception as e:
|
| 69 |
+
print(f"❌ HF Client Error: {e}")
|
| 70 |
+
|
| 71 |
+
# 2. Load Local Model
|
| 72 |
+
try:
|
| 73 |
+
# Note: In Hugging Face Spaces, model might be in root or model/ folder depending on upload
|
| 74 |
+
if os.path.exists("model/plant_disease_model_final.h5"):
|
| 75 |
+
path = "model/plant_disease_model_final.h5"
|
| 76 |
+
elif os.path.exists("plant_disease_model_final.h5"):
|
| 77 |
+
path = "plant_disease_model_final.h5"
|
| 78 |
+
else:
|
| 79 |
+
path = None
|
| 80 |
+
|
| 81 |
+
if path:
|
| 82 |
+
resources['local_model'] = tf.keras.models.load_model(path)
|
| 83 |
+
print(f"✅ Local model loaded from '{path}'")
|
| 84 |
+
else:
|
| 85 |
+
print("❌ Model file not found!")
|
| 86 |
+
except Exception as e:
|
| 87 |
+
print(f"❌ Local Model Load Error: {e}")
|
| 88 |
+
|
| 89 |
+
# 3. Load Indices
|
| 90 |
+
try:
|
| 91 |
+
with open("data/class_indices.pkl", "rb") as f:
|
| 92 |
+
resources['class_indices'] = pickle.load(f)
|
| 93 |
+
print("✅ Class indices loaded.")
|
| 94 |
+
except Exception as e:
|
| 95 |
+
print(f"❌ Indices Load Error: {e}")
|
| 96 |
+
|
| 97 |
+
# 4. Load KB
|
| 98 |
+
try:
|
| 99 |
+
with open("data/knowledge_base.json", "r") as f:
|
| 100 |
+
resources['knowledge_base'] = json.load(f)
|
| 101 |
+
print("✅ Knowledge base loaded.")
|
| 102 |
+
except Exception as e:
|
| 103 |
+
print(f"❌ KB Load Error: {e}")
|
| 104 |
+
|
| 105 |
+
yield
|
| 106 |
+
resources.clear()
|
| 107 |
+
print("--- Shutting Down ---")
|
| 108 |
+
|
| 109 |
+
app = FastAPI(lifespan=lifespan)
|
| 110 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 111 |
+
templates = Jinja2Templates(directory="templates")
|
| 112 |
+
|
| 113 |
+
# --- Routes ---
|
| 114 |
+
@app.get("/", response_class=HTMLResponse)
|
| 115 |
+
async def page_home(request: Request): return templates.TemplateResponse("diagnosis.html", {"request": request})
|
| 116 |
+
|
| 117 |
+
@app.get("/analysis", response_class=HTMLResponse)
|
| 118 |
+
async def page_analysis(request: Request): return templates.TemplateResponse("analysis.html", {"request": request})
|
| 119 |
+
|
| 120 |
+
@app.get("/ask_ai", response_class=HTMLResponse)
|
| 121 |
+
async def page_ask(request: Request): return templates.TemplateResponse("ask_ai.html", {"request": request})
|
| 122 |
+
|
| 123 |
+
@app.get("/model_knowledge", response_class=HTMLResponse)
|
| 124 |
+
async def page_kb(request: Request): return templates.TemplateResponse("model_knowledge.html", {"request": request})
|
| 125 |
+
|
| 126 |
+
@app.get("/api/knowledge_base")
|
| 127 |
+
async def api_kb():
|
| 128 |
+
return JSONResponse(content=resources.get('knowledge_base', {}))
|
| 129 |
+
|
| 130 |
+
@app.post("/api/chat")
|
| 131 |
+
async def api_chat(req: ChatRequest):
|
| 132 |
+
ctx = req.context or {}
|
| 133 |
+
diagnosis = ctx.get('disease_name', 'unknown plant')
|
| 134 |
+
|
| 135 |
+
prompt = f"You are a plant doctor. User asks: '{req.question}'. Context: The user's plant has '{diagnosis}'. Provide a short, helpful cure or advice."
|
| 136 |
+
|
| 137 |
+
answer = query_huggingface(prompt)
|
| 138 |
+
return {"answer": answer}
|
| 139 |
+
|
| 140 |
+
@app.post("/api/diagnose")
|
| 141 |
+
async def api_diagnose(file: UploadFile = File(...)):
|
| 142 |
+
if 'local_model' not in resources or 'class_indices' not in resources:
|
| 143 |
+
raise HTTPException(500, "System not fully loaded.")
|
| 144 |
+
|
| 145 |
+
img_bytes = await file.read()
|
| 146 |
+
|
| 147 |
+
# Local Inference
|
| 148 |
+
try:
|
| 149 |
+
img = Image.open(io.BytesIO(img_bytes)).resize((224, 224)).convert('RGB')
|
| 150 |
+
img_arr = np.array(img) / 255.0
|
| 151 |
+
img_arr = np.expand_dims(img_arr, axis=0)
|
| 152 |
+
|
| 153 |
+
preds = resources['local_model'].predict(img_arr, verbose=0)
|
| 154 |
+
conf = float(np.max(preds))
|
| 155 |
+
idx = np.argmax(preds)
|
| 156 |
+
|
| 157 |
+
class_key = list(resources['class_indices'].keys())[idx]
|
| 158 |
+
local_name = class_key.replace("___", " - ").replace("_", " ")
|
| 159 |
+
except Exception as e:
|
| 160 |
+
print(f"Inference Error: {e}")
|
| 161 |
+
local_name = "Unknown"
|
| 162 |
+
conf = 0.0
|
| 163 |
+
|
| 164 |
+
# Decision Logic
|
| 165 |
+
source = "Local Model"
|
| 166 |
+
disease_name = local_name
|
| 167 |
+
|
| 168 |
+
# If confidence is low, ask AI to fallback/analyze text description
|
| 169 |
+
if conf < CONFIDENCE_THRESHOLD:
|
| 170 |
+
source = "AI Supervisor (Low Confidence)"
|
| 171 |
+
# Since we can't send images to Mistral Text model easily in free tier,
|
| 172 |
+
# we treat it as unknown/needs manual check, or provide general advice.
|
| 173 |
+
# For this demo, we trust the low confidence prediction but mark it.
|
| 174 |
+
smart_report = f"Confidence is low ({conf:.1%}). The model guesses '{local_name}'. Please verify visually."
|
| 175 |
+
else:
|
| 176 |
+
# Ask AI for details about this specific disease
|
| 177 |
+
prompt = f"A plant is diagnosed with '{local_name}'. Provide a structured summary: 1. Symptoms 2. Cure 3. Prevention. Keep it concise."
|
| 178 |
+
smart_report = query_huggingface(prompt)
|
| 179 |
+
|
| 180 |
+
return {
|
| 181 |
+
"disease_name": disease_name,
|
| 182 |
+
"confidence": f"{conf:.1%}",
|
| 183 |
+
"source": source,
|
| 184 |
+
"smart_report": smart_report,
|
| 185 |
+
"health_score": int(100 - (conf * 100)) if "healthy" not in local_name.lower() else 95,
|
| 186 |
+
"severity_score": int(conf * 100) if "healthy" not in local_name.lower() else 5
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
if __name__ == "__main__":
|
| 190 |
+
uvicorn.run("main:app", host="0.0.0.0", port=7860)
|
model/plant_disease_model_final.h5
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:52f2fe0e7265b36ae45a8ab9623b6522a1d2933fc5ec73d60272abce2ae2e796
|
| 3 |
+
size 26488068
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
python-dotenv
|
| 4 |
+
tensorflow-cpu
|
| 5 |
+
pillow
|
| 6 |
+
numpy
|
| 7 |
+
huggingface_hub
|
| 8 |
+
requests
|
| 9 |
+
jinja2
|
| 10 |
+
python-multipart
|
static/css/style.css
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
:root {
|
| 3 |
+
--primary-green: #34C759;
|
| 4 |
+
--text-color: #F5F5F7; /* Bright white-grey */
|
| 5 |
+
--text-muted: #D1D1D6;
|
| 6 |
+
--glass-bg: rgba(30, 30, 30, 0.65); /* Darker glass */
|
| 7 |
+
--glass-border: rgba(255, 255, 255, 0.1);
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
body {
|
| 11 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
| 12 |
+
margin: 0;
|
| 13 |
+
color: var(--text-color);
|
| 14 |
+
background-color: #000;
|
| 15 |
+
overflow: hidden;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
/* BACKGROUND FIX: Add dark overlay so text pops */
|
| 19 |
+
body::before {
|
| 20 |
+
content: '';
|
| 21 |
+
position: fixed;
|
| 22 |
+
top: 0; left: 0; width: 100%; height: 100%;
|
| 23 |
+
background: url('https://images.unsplash.com/photo-1518531933037-91b2f5f229cc?q=80&w=2574') center/cover;
|
| 24 |
+
z-index: -2;
|
| 25 |
+
animation: zoomIn 60s infinite alternate;
|
| 26 |
+
}
|
| 27 |
+
body::after {
|
| 28 |
+
content: '';
|
| 29 |
+
position: fixed;
|
| 30 |
+
top: 0; left: 0; width: 100%; height: 100%;
|
| 31 |
+
background: rgba(0, 0, 0, 0.7); /* 70% Dark Overlay */
|
| 32 |
+
backdrop-filter: blur(8px);
|
| 33 |
+
z-index: -1;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
@keyframes zoomIn { from {transform: scale(1);} to {transform: scale(1.1);} }
|
| 37 |
+
|
| 38 |
+
/* LAYOUT */
|
| 39 |
+
#app-container { display: flex; height: 100vh; }
|
| 40 |
+
#content-wrapper { flex: 1; padding: 20px; position: relative; }
|
| 41 |
+
|
| 42 |
+
/* SIDEBAR */
|
| 43 |
+
#sidebar {
|
| 44 |
+
width: 260px;
|
| 45 |
+
background: rgba(0, 0, 0, 0.5);
|
| 46 |
+
backdrop-filter: blur(20px);
|
| 47 |
+
border-right: 1px solid var(--glass-border);
|
| 48 |
+
padding: 20px;
|
| 49 |
+
display: flex; flex-direction: column;
|
| 50 |
+
}
|
| 51 |
+
.nav-link {
|
| 52 |
+
display: flex; align-items: center; gap: 15px;
|
| 53 |
+
padding: 15px; margin-bottom: 5px;
|
| 54 |
+
color: var(--text-muted);
|
| 55 |
+
text-decoration: none;
|
| 56 |
+
border-radius: 12px;
|
| 57 |
+
transition: 0.3s;
|
| 58 |
+
}
|
| 59 |
+
.nav-link:hover, .nav-link.active {
|
| 60 |
+
background: rgba(52, 199, 89, 0.2);
|
| 61 |
+
color: var(--primary-green);
|
| 62 |
+
}
|
| 63 |
+
.nav-link i { font-size: 1.2rem; }
|
| 64 |
+
|
| 65 |
+
/* MAIN CONTENT CARD */
|
| 66 |
+
#main-content {
|
| 67 |
+
background: var(--glass-bg);
|
| 68 |
+
border: 1px solid var(--glass-border);
|
| 69 |
+
border-radius: 24px;
|
| 70 |
+
padding: 40px;
|
| 71 |
+
height: 100%;
|
| 72 |
+
overflow-y: auto;
|
| 73 |
+
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
/* COMPONENTS */
|
| 77 |
+
.glass-card {
|
| 78 |
+
background: rgba(255, 255, 255, 0.05);
|
| 79 |
+
border: 1px solid var(--glass-border);
|
| 80 |
+
border-radius: 16px;
|
| 81 |
+
padding: 25px;
|
| 82 |
+
}
|
| 83 |
+
.glass-button {
|
| 84 |
+
background: var(--primary-green);
|
| 85 |
+
color: white;
|
| 86 |
+
border: none;
|
| 87 |
+
padding: 12px 24px;
|
| 88 |
+
border-radius: 12px;
|
| 89 |
+
font-weight: 600;
|
| 90 |
+
cursor: pointer;
|
| 91 |
+
width: 100%;
|
| 92 |
+
font-size: 1rem;
|
| 93 |
+
transition: 0.2s;
|
| 94 |
+
}
|
| 95 |
+
.glass-button:hover { transform: scale(1.02); opacity: 0.9; }
|
| 96 |
+
|
| 97 |
+
h1, h2, h3 { color: #fff; font-weight: 600; }
|
| 98 |
+
.text-muted { color: var(--text-muted) !important; }
|
| 99 |
+
.text-color-dark { color: #fff !important; }
|
static/js/script.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 2 |
+
|
| 3 |
+
// --- Sidebar Navigation ---
|
| 4 |
+
const toggleBtn = document.getElementById('toggle-btn');
|
| 5 |
+
const sidebar = document.querySelector('.sidebar');
|
| 6 |
+
const navLinks = document.querySelectorAll('.nav-link');
|
| 7 |
+
|
| 8 |
+
if (toggleBtn && sidebar) {
|
| 9 |
+
toggleBtn.addEventListener('click', () => {
|
| 10 |
+
sidebar.classList.toggle('collapsed');
|
| 11 |
+
});
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
// Set active link based on current page
|
| 15 |
+
const currentPage = window.location.pathname.split('/').pop();
|
| 16 |
+
navLinks.forEach(link => {
|
| 17 |
+
if (link.getAttribute('href') === currentPage) {
|
| 18 |
+
link.classList.add('active');
|
| 19 |
+
}
|
| 20 |
+
});
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
// --- Diagnosis Page Logic ---
|
| 24 |
+
const uploadZone = document.getElementById('upload-zone');
|
| 25 |
+
const fileInput = document.getElementById('plant-image');
|
| 26 |
+
const resultCard = document.getElementById('result-card');
|
| 27 |
+
const resultContainer = document.getElementById('result-container');
|
| 28 |
+
|
| 29 |
+
if (uploadZone && fileInput) {
|
| 30 |
+
uploadZone.addEventListener('click', () => fileInput.click());
|
| 31 |
+
|
| 32 |
+
fileInput.addEventListener('change', async (event) => {
|
| 33 |
+
const file = event.target.files[0];
|
| 34 |
+
if (!file) return;
|
| 35 |
+
|
| 36 |
+
// Show spinner and hide previous result
|
| 37 |
+
resultCard.classList.remove('visible');
|
| 38 |
+
resultContainer.innerHTML = '<div class="spinner"></div>';
|
| 39 |
+
resultCard.classList.add('visible');
|
| 40 |
+
|
| 41 |
+
const formData = new FormData();
|
| 42 |
+
formData.append('file', file);
|
| 43 |
+
|
| 44 |
+
try {
|
| 45 |
+
const response = await fetch('/api/diagnose', {
|
| 46 |
+
method: 'POST',
|
| 47 |
+
body: formData
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
if (!response.ok) {
|
| 51 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
const data = await response.json();
|
| 55 |
+
displayDiagnosisResult(data);
|
| 56 |
+
|
| 57 |
+
} catch (error) {
|
| 58 |
+
console.error("Error during diagnosis:", error);
|
| 59 |
+
resultContainer.innerHTML = `<p style="color: #ff453a;">An error occurred. Please try again.</p>`;
|
| 60 |
+
}
|
| 61 |
+
});
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
function displayDiagnosisResult(data) {
|
| 65 |
+
resultContainer.innerHTML = `
|
| 66 |
+
<h3>Diagnosis Result</h3>
|
| 67 |
+
<div class="result-grid">
|
| 68 |
+
<div class="result-item">
|
| 69 |
+
<strong>Disease</strong>
|
| 70 |
+
<span>${data.disease_name || 'N/A'}</span>
|
| 71 |
+
</div>
|
| 72 |
+
<div class="result-item">
|
| 73 |
+
<strong>Confidence</strong>
|
| 74 |
+
<span>${data.confidence ? (data.confidence * 100).toFixed(2) + '%' : 'N/A'}</span>
|
| 75 |
+
</div>
|
| 76 |
+
<div class="result-item">
|
| 77 |
+
<strong>Source</strong>
|
| 78 |
+
<span class="source-badge ${data.source.toLowerCase()}">${data.source || 'N/A'}</span>
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
<hr style="border-color: var(--glass-border); margin: 1.5rem 0;">
|
| 82 |
+
<div class="result-item">
|
| 83 |
+
<strong>Symptoms</strong>
|
| 84 |
+
<p>${data.symptoms || 'No detailed symptoms provided.'}</p>
|
| 85 |
+
</div>
|
| 86 |
+
<div class="result-item" style="margin-top: 1rem;">
|
| 87 |
+
<strong>Recommended Cure</strong>
|
| 88 |
+
<p>${data.cure || 'No specific cure provided.'}</p>
|
| 89 |
+
</div>
|
| 90 |
+
<div class="result-item" style="margin-top: 1rem;">
|
| 91 |
+
<strong>Prevention</strong>
|
| 92 |
+
<p>${data.prevention || 'No specific prevention steps provided.'}</p>
|
| 93 |
+
</div>
|
| 94 |
+
`;
|
| 95 |
+
resultCard.classList.add('visible');
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
// --- Ask AI Page Logic ---
|
| 100 |
+
const chatForm = document.getElementById('chat-form');
|
| 101 |
+
const chatInput = document.getElementById('chat-input');
|
| 102 |
+
const sendBtn = document.getElementById('send-btn');
|
| 103 |
+
const messagesContainer = document.querySelector('.chat-messages');
|
| 104 |
+
|
| 105 |
+
if (chatForm) {
|
| 106 |
+
chatForm.addEventListener('submit', async (event) => {
|
| 107 |
+
event.preventDefault();
|
| 108 |
+
const message = chatInput.value.trim();
|
| 109 |
+
if (!message) return;
|
| 110 |
+
|
| 111 |
+
// Display user message
|
| 112 |
+
addMessage(message, 'user');
|
| 113 |
+
chatInput.value = '';
|
| 114 |
+
sendBtn.disabled = true;
|
| 115 |
+
|
| 116 |
+
// Add placeholder for AI response
|
| 117 |
+
const aiMessagePlaceholder = addMessage('...', 'ai', true);
|
| 118 |
+
|
| 119 |
+
try {
|
| 120 |
+
const response = await fetch('/api/chat', {
|
| 121 |
+
method: 'POST',
|
| 122 |
+
headers: { 'Content-Type': 'application/json' },
|
| 123 |
+
body: JSON.stringify({ message: message })
|
| 124 |
+
});
|
| 125 |
+
|
| 126 |
+
if (!response.ok) {
|
| 127 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 128 |
+
}
|
| 129 |
+
const data = await response.json();
|
| 130 |
+
aiMessagePlaceholder.textContent = data.reply;
|
| 131 |
+
|
| 132 |
+
} catch (error) {
|
| 133 |
+
console.error("Chat error:", error);
|
| 134 |
+
aiMessagePlaceholder.textContent = "Sorry, I couldn't get a response. Please try again.";
|
| 135 |
+
} finally {
|
| 136 |
+
sendBtn.disabled = false;
|
| 137 |
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
| 138 |
+
}
|
| 139 |
+
});
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
function addMessage(text, sender, isPlaceholder = false) {
|
| 143 |
+
const messageBubble = document.createElement('div');
|
| 144 |
+
messageBubble.classList.add('message-bubble', `${sender}-message`);
|
| 145 |
+
messageBubble.textContent = text;
|
| 146 |
+
if(isPlaceholder) {
|
| 147 |
+
messageBubble.innerHTML = '<span class="typing-indicator"></span><span class="typing-indicator"></span><span class="typing-indicator"></span>';
|
| 148 |
+
}
|
| 149 |
+
messagesContainer.appendChild(messageBubble);
|
| 150 |
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
| 151 |
+
return messageBubble; // Return for potential updates
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
// --- Analysis Page Logic (Chart.js) ---
|
| 156 |
+
const radarChartCanvas = document.getElementById('health-radar-chart');
|
| 157 |
+
if (radarChartCanvas && typeof Chart !== 'undefined') {
|
| 158 |
+
const ctx = radarChartCanvas.getContext('2d');
|
| 159 |
+
new Chart(ctx, {
|
| 160 |
+
type: 'radar',
|
| 161 |
+
data: {
|
| 162 |
+
labels: ['Leaf Health', 'Stem Integrity', 'Soil Match', 'Pest-Free', 'Hydration', 'Nutrient Balance'],
|
| 163 |
+
datasets: [{
|
| 164 |
+
label: 'Plant Health Analysis',
|
| 165 |
+
data: [65, 59, 90, 81, 56, 55], // Example data
|
| 166 |
+
fill: true,
|
| 167 |
+
backgroundColor: 'rgba(0, 122, 255, 0.2)',
|
| 168 |
+
borderColor: 'rgb(0, 122, 255)',
|
| 169 |
+
pointBackgroundColor: 'rgb(0, 122, 255)',
|
| 170 |
+
pointBorderColor: '#fff',
|
| 171 |
+
pointHoverBackgroundColor: '#fff',
|
| 172 |
+
pointHoverBorderColor: 'rgb(0, 122, 255)'
|
| 173 |
+
}]
|
| 174 |
+
},
|
| 175 |
+
options: {
|
| 176 |
+
elements: {
|
| 177 |
+
line: {
|
| 178 |
+
borderWidth: 3
|
| 179 |
+
}
|
| 180 |
+
},
|
| 181 |
+
scales: {
|
| 182 |
+
r: {
|
| 183 |
+
angleLines: { color: 'rgba(255, 255, 255, 0.2)' },
|
| 184 |
+
grid: { color: 'rgba(255, 255, 255, 0.2)' },
|
| 185 |
+
pointLabels: {
|
| 186 |
+
color: 'rgba(255, 255, 255, 0.85)',
|
| 187 |
+
font: { size: 12 }
|
| 188 |
+
},
|
| 189 |
+
ticks: {
|
| 190 |
+
color: 'rgba(255, 255, 255, 0.85)',
|
| 191 |
+
backdropColor: 'rgba(0,0,0,0)'
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
},
|
| 195 |
+
plugins: {
|
| 196 |
+
legend: {
|
| 197 |
+
labels: {
|
| 198 |
+
color: 'rgba(255, 255, 255, 0.85)'
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
}
|
| 202 |
+
}
|
| 203 |
+
});
|
| 204 |
+
}
|
| 205 |
+
});
|
templates/analysis.html
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Analysis - Plantoi{% endblock %}
|
| 3 |
+
|
| 4 |
+
{% block content %}
|
| 5 |
+
<section id="analysis">
|
| 6 |
+
<h1>Health Analysis</h1>
|
| 7 |
+
<p style="color: var(--text-muted); max-width: 60ch; margin-bottom: 2rem;">
|
| 8 |
+
This radar chart provides a visual representation of the plant's overall health based on various key factors. Data is generated from the diagnosis.
|
| 9 |
+
</p>
|
| 10 |
+
|
| 11 |
+
<div class="glass-card">
|
| 12 |
+
<h2>Plant Health Radar</h2>
|
| 13 |
+
<canvas id="health-radar-chart"></canvas>
|
| 14 |
+
</div>
|
| 15 |
+
</section>
|
| 16 |
+
{% endblock %}
|
templates/ask_ai.html
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Ask AI - Plantoi{% endblock %}
|
| 3 |
+
|
| 4 |
+
{% block content %}
|
| 5 |
+
<section id="ask-ai">
|
| 6 |
+
<h1>Ask Our AI Expert</h1>
|
| 7 |
+
<p style="color: var(--text-muted); max-width: 60ch; margin-bottom: 2rem;">
|
| 8 |
+
Have questions about plant care, diseases, or just general gardening tips? Ask our AI assistant, powered by Mistral-7B.
|
| 9 |
+
</p>
|
| 10 |
+
|
| 11 |
+
<div class="chat-container glass-card">
|
| 12 |
+
<div class="chat-messages">
|
| 13 |
+
<div class="message-bubble ai-message">
|
| 14 |
+
Hello! How can I help you with your plants today?
|
| 15 |
+
</div>
|
| 16 |
+
</div>
|
| 17 |
+
<form id="chat-form" class="chat-input-area">
|
| 18 |
+
<input type="text" id="chat-input" placeholder="Type your message here..." autocomplete="off">
|
| 19 |
+
<button type="submit" id="send-btn">
|
| 20 |
+
<i class="fa-solid fa-paper-plane"></i>
|
| 21 |
+
</button>
|
| 22 |
+
</form>
|
| 23 |
+
</div>
|
| 24 |
+
</section>
|
| 25 |
+
{% endblock %}
|
templates/base.html
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>{% block title %}Plantoi{% endblock %}</title>
|
| 7 |
+
<link rel="stylesheet" href="/static/css/style.css">
|
| 8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css">
|
| 9 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
| 10 |
+
</head>
|
| 11 |
+
<body>
|
| 12 |
+
<div class="background-container"></div>
|
| 13 |
+
|
| 14 |
+
<aside class="sidebar">
|
| 15 |
+
<div class="sidebar-header">
|
| 16 |
+
<i class="fa-solid fa-leaf logo-icon"></i>
|
| 17 |
+
<h1 class="logo-text">Plantoi</h1>
|
| 18 |
+
</div>
|
| 19 |
+
<ul class="nav-menu">
|
| 20 |
+
<li class="nav-item">
|
| 21 |
+
<a href="/diagnosis" class="nav-link">
|
| 22 |
+
<i class="fa-solid fa-stethoscope nav-icon"></i>
|
| 23 |
+
<span class="nav-text">Diagnose</span>
|
| 24 |
+
</a>
|
| 25 |
+
</li>
|
| 26 |
+
<li class="nav-item">
|
| 27 |
+
<a href="/analysis" class="nav-link">
|
| 28 |
+
<i class="fa-solid fa-chart-pie nav-icon"></i>
|
| 29 |
+
<span class="nav-text">Analysis</span>
|
| 30 |
+
</a>
|
| 31 |
+
</li>
|
| 32 |
+
<li class="nav-item">
|
| 33 |
+
<a href="/ask_ai" class="nav-link">
|
| 34 |
+
<i class="fa-solid fa-comments nav-icon"></i>
|
| 35 |
+
<span class="nav-text">Ask AI</span>
|
| 36 |
+
</a>
|
| 37 |
+
</li>
|
| 38 |
+
<li class="nav-item">
|
| 39 |
+
<a href="/model_knowledge" class="nav-link">
|
| 40 |
+
<i class="fa-solid fa-book-atlas nav-icon"></i>
|
| 41 |
+
<span class="nav-text">Knowledge</span>
|
| 42 |
+
</a>
|
| 43 |
+
</li>
|
| 44 |
+
</ul>
|
| 45 |
+
<div class="sidebar-footer">
|
| 46 |
+
<button id="toggle-btn">
|
| 47 |
+
<i class="fa-solid fa-chevron-left nav-icon"></i>
|
| 48 |
+
<span class="nav-text">Collapse</span>
|
| 49 |
+
</button>
|
| 50 |
+
</div>
|
| 51 |
+
</aside>
|
| 52 |
+
|
| 53 |
+
<main class="main-content">
|
| 54 |
+
{% block content %}{% endblock %}
|
| 55 |
+
</main>
|
| 56 |
+
|
| 57 |
+
<script src="/static/js/script.js"></script>
|
| 58 |
+
</body>
|
| 59 |
+
</html>
|
templates/diagnosis.html
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Diagnosis - Plantoi{% endblock %}
|
| 3 |
+
|
| 4 |
+
{% block content %}
|
| 5 |
+
<section id="diagnosis">
|
| 6 |
+
<h1>Plant Disease Diagnosis</h1>
|
| 7 |
+
<p style="color: var(--text-muted); max-width: 60ch; margin-bottom: 2rem;">
|
| 8 |
+
Upload an image of a plant leaf. The system will first use a local model for a quick diagnosis. If the confidence is low, a more advanced AI will provide a secondary analysis.
|
| 9 |
+
</p>
|
| 10 |
+
|
| 11 |
+
<div id="upload-zone" class="glass-card">
|
| 12 |
+
<input type="file" id="plant-image" accept="image/*">
|
| 13 |
+
<i class="fa-solid fa-cloud-arrow-up"></i>
|
| 14 |
+
<h3>Click to Upload an Image</h3>
|
| 15 |
+
<p>Or drag and drop a file</p>
|
| 16 |
+
</div>
|
| 17 |
+
|
| 18 |
+
<div id="result-card" class="glass-card">
|
| 19 |
+
<div id="result-container">
|
| 20 |
+
<!-- Asynchronous results will be loaded here by script.js -->
|
| 21 |
+
</div>
|
| 22 |
+
</div>
|
| 23 |
+
</section>
|
| 24 |
+
{% endblock %}
|
templates/model_knowledge.html
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
{% block title %}Model Knowledge - Plantoi{% endblock %}
|
| 3 |
+
|
| 4 |
+
{% block content %}
|
| 5 |
+
<section id="knowledge">
|
| 6 |
+
<h1>Model Knowledge Base</h1>
|
| 7 |
+
<p style="color: var(--text-muted); max-width: 60ch; margin-bottom: 2rem;">
|
| 8 |
+
This section provides insight into the diseases the local model has been trained to identify.
|
| 9 |
+
</p>
|
| 10 |
+
|
| 11 |
+
<div class="glass-card">
|
| 12 |
+
<h2>Recognized Diseases</h2>
|
| 13 |
+
<p>The local deep learning model is trained on a dataset of common plant diseases. When an image is uploaded, the model identifies features and patterns to classify the disease. If the model's confidence in its prediction is low, the query is escalated to a more powerful general AI for a second opinion based on the visual input and any additional text you provide.</p>
|
| 14 |
+
<br>
|
| 15 |
+
<p>This hybrid approach ensures both speed and accuracy, giving you a fast diagnosis when possible and a more detailed, context-aware analysis when needed.</p>
|
| 16 |
+
<!-- In a real application, you might dynamically load the class names here -->
|
| 17 |
+
</div>
|
| 18 |
+
</section>
|
| 19 |
+
{% endblock %}
|