krushimitravit commited on
Commit
5b642ef
·
verified ·
1 Parent(s): 08a05e9

Upload 8 files

Browse files
Files changed (8) hide show
  1. .env +5 -0
  2. Dockerfile +18 -0
  3. app.py +231 -0
  4. app_enhanced.py +628 -0
  5. requirements.txt +6 -0
  6. rf_ferti_name.pkl +3 -0
  7. rf_ferti_value.pkl +3 -0
  8. templates/index.html +539 -0
.env ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ GEMINI_API_KEY=AIzaSyASGSRDs6fhaNFp0MKYQen_LBXRZ5BXWLo
2
+ GOOGLE_API_KEY=AIzaSyAcU0xE4i_YvoKY4c2GYE5X2UwQei7J8s8
3
+ GOOGLE_CX=f045c337ba1174c08
4
+ SECRET_KEY=AIzaSyCyLzLovfjmSPJxPfzcpZ1yVeVRH4iCfQ0
5
+ NVIDIA_API_KEY=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
Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official Python runtime as a parent image
2
+ FROM python:3.9-slim
3
+
4
+ # Set the working directory
5
+ WORKDIR /app
6
+
7
+ # Copy the application files to the container
8
+ COPY . /app
9
+
10
+ # Install Python dependencies
11
+ RUN pip install --no-cache-dir -r requirements.txt
12
+
13
+
14
+ # Expose the Flask port (Hugging Face Spaces uses port 7860 by default)
15
+ EXPOSE 7860
16
+
17
+ # Command to run the Flask app
18
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request
2
+ import joblib
3
+ import pandas as pd
4
+ import google.generativeai as genai
5
+ from openai import OpenAI
6
+ import os
7
+ import time
8
+ from dotenv import load_dotenv
9
+
10
+ # Load environment variables from .env file
11
+ load_dotenv()
12
+
13
+ app = Flask(__name__)
14
+
15
+ # Load the trained Random Forest models
16
+ rf_ferti_name = joblib.load('rf_ferti_name.pkl')
17
+ rf_ferti_value = joblib.load('rf_ferti_value.pkl')
18
+
19
+ # Manually define the encodings based on the provided dictionaries
20
+ soil_type_encodings = {'Black': 0, 'Clayey': 1, 'Loamy': 2, 'Red': 3, 'Sandy': 4}
21
+ crop_type_encodings = {'Barley': 0, 'Cotton': 1, 'Ground Nuts': 2, 'Maize': 3, 'Millets': 4,
22
+ 'Oil seeds': 5, 'Other Variety': 6, 'Paddy': 7, 'Pulses': 8, 'Sugarcane': 9,
23
+ 'Tobacco': 10, 'Wheat': 11}
24
+ fertilizer_name_encodings = {'10-26-26': 0, '14-35-14': 1, '15-15-15': 2, '17-17-17': 3, '20-20': 4,
25
+ '20-20-20': 5, '28-28': 6, 'Ammonium sulfate': 7, 'Biofertilizer (e.g., Rhizobium)': 8,
26
+ 'Calcium nitrate': 9, 'DAP': 10, 'Ferrous sulfate': 11, 'Magnesium sulfate': 12,
27
+ 'Potassium chloride/Muriate of potash (MOP)': 13, 'Potassium sulfate/Sulfate of potash (SOP)': 14,
28
+ 'Rock phosphate (RP)': 15, 'Single superphosphate (SSP)': 16, 'Triple superphosphate (TSP)': 17,
29
+ 'Urea': 18, 'Zinc sulfate': 19}
30
+
31
+ # --- ENHANCED LLM CONFIGURATION ---
32
+ GEMINI_API_KEY = os.getenv('GEMINI_API_KEY')
33
+ NVIDIA_API_KEY = os.getenv('NVIDIA_API_KEY')
34
+
35
+ if GEMINI_API_KEY:
36
+ genai.configure(api_key=GEMINI_API_KEY)
37
+
38
+ # Model configurations with retry settings
39
+ GEMINI_MODELS = [
40
+ {"name": "gemini-2.0-flash-exp", "max_retries": 2, "timeout": 30, "description": "Latest experimental"},
41
+ {"name": "gemini-1.5-pro-latest", "max_retries": 2, "timeout": 45, "description": "Most capable"},
42
+ {"name": "gemini-1.5-flash", "max_retries": 3, "timeout": 20, "description": "Fast and reliable"},
43
+ {"name": "gemini-1.5-flash-8b", "max_retries": 3, "timeout": 15, "description": "Lightweight"},
44
+ ]
45
+
46
+ NVIDIA_MODELS = [
47
+ {"name": "meta/llama-3.2-90b-vision-instruct", "max_retries": 2, "timeout": 40, "description": "High capability"},
48
+ {"name": "meta/llama-3.2-11b-vision-instruct", "max_retries": 2, "timeout": 30, "description": "Balanced"},
49
+ ]
50
+
51
+
52
+ def retry_with_backoff(func, max_retries=3, initial_delay=1):
53
+ """Retry a function with exponential backoff."""
54
+ for attempt in range(max_retries):
55
+ try:
56
+ return func()
57
+ except Exception as e:
58
+ if attempt == max_retries - 1:
59
+ raise
60
+ delay = initial_delay * (2 ** attempt)
61
+ print(f" >> Retry {attempt + 1}/{max_retries} after {delay}s (Error: {type(e).__name__})")
62
+ time.sleep(delay)
63
+
64
+
65
+ def generate_with_gemini(prompt, model_config):
66
+ """Generate text using a specific Gemini model with retry logic."""
67
+ model_name = model_config["name"]
68
+ max_retries = model_config.get("max_retries", 2)
69
+
70
+ def _attempt():
71
+ print(f" >> Attempting Gemini: {model_name}")
72
+ model = genai.GenerativeModel(model_name)
73
+ response = model.generate_content(prompt)
74
+
75
+ if not response or not response.text:
76
+ raise ValueError("Empty response from model")
77
+
78
+ return response.text
79
+
80
+ try:
81
+ return retry_with_backoff(_attempt, max_retries=max_retries)
82
+ except Exception as e:
83
+ print(f" >> FAILED {model_name}: {type(e).__name__}")
84
+ return None
85
+
86
+
87
+ def generate_with_nvidia(prompt, model_config):
88
+ """Generate text using NVIDIA API with retry logic."""
89
+ if not NVIDIA_API_KEY:
90
+ return None
91
+
92
+ model_name = model_config["name"]
93
+ max_retries = model_config.get("max_retries", 2)
94
+
95
+ def _attempt():
96
+ print(f" >> Attempting NVIDIA: {model_name}")
97
+ client = OpenAI(
98
+ base_url="https://integrate.api.nvidia.com/v1",
99
+ api_key=NVIDIA_API_KEY
100
+ )
101
+
102
+ completion = client.chat.completions.create(
103
+ model=model_name,
104
+ messages=[{"role": "user", "content": prompt}],
105
+ max_tokens=500,
106
+ temperature=0.7
107
+ )
108
+
109
+ response_text = completion.choices[0].message.content
110
+ if not response_text:
111
+ raise ValueError("Empty response from NVIDIA")
112
+
113
+ return response_text
114
+
115
+ try:
116
+ return retry_with_backoff(_attempt, max_retries=max_retries)
117
+ except Exception as e:
118
+ print(f" >> FAILED NVIDIA {model_name}: {type(e).__name__}")
119
+ return None
120
+
121
+
122
+ def generate_ai_suggestions(pred_fertilizer_name):
123
+ """Generate AI suggestions with enhanced fallback system."""
124
+ print("\n" + "=" * 60)
125
+ print(f"🌱 GENERATING AI SUGGESTIONS FOR: {pred_fertilizer_name}")
126
+ print("=" * 60)
127
+
128
+ prompt = (
129
+ f"For {pred_fertilizer_name} fertilizer, generate 3-4 sentences each on a new line. "
130
+ f"Text should be justified and should not contain any special characters."
131
+ )
132
+
133
+ response_text = None
134
+ used_model = "None"
135
+
136
+ # PHASE 1: Try Gemini models
137
+ if GEMINI_API_KEY:
138
+ print("\n--- PHASE 1: Trying Gemini Models ---")
139
+ for idx, model_config in enumerate(GEMINI_MODELS, 1):
140
+ print(f"[{idx}/{len(GEMINI_MODELS)}] Testing {model_config['name']}...")
141
+ response_text = generate_with_gemini(prompt, model_config)
142
+
143
+ if response_text:
144
+ used_model = f"Gemini-{model_config['name']}"
145
+ print(f" ✓ SUCCESS with {used_model}")
146
+ break
147
+
148
+ # PHASE 2: Try NVIDIA models (fallback)
149
+ if not response_text and NVIDIA_API_KEY:
150
+ print("\n--- PHASE 2: Trying NVIDIA Models (Fallback) ---")
151
+ for idx, model_config in enumerate(NVIDIA_MODELS, 1):
152
+ print(f"[{idx}/{len(NVIDIA_MODELS)}] Testing {model_config['name']}...")
153
+ response_text = generate_with_nvidia(prompt, model_config)
154
+
155
+ if response_text:
156
+ used_model = f"NVIDIA-{model_config['name']}"
157
+ print(f" ✓ SUCCESS with {used_model}")
158
+ break
159
+
160
+ # PHASE 3: Final fallback
161
+ if not response_text:
162
+ print("\n❌ All LLM providers failed. Using fallback text.")
163
+ response_text = (
164
+ f"{pred_fertilizer_name} is a commonly used fertilizer in agriculture. "
165
+ f"It provides essential nutrients to crops. "
166
+ f"Follow recommended dosage for best results. "
167
+ f"Consult local agricultural experts for specific guidance."
168
+ )
169
+ used_model = "Fallback"
170
+
171
+ print(f"\n✅ Generated using: {used_model}")
172
+ print("=" * 60 + "\n")
173
+
174
+ return response_text
175
+
176
+
177
+ @app.route('/', methods=['GET', 'POST'])
178
+ def index():
179
+ if request.method == 'POST':
180
+ # Retrieve form data
181
+ temperature = float(request.form['temperature'])
182
+ humidity = float(request.form['humidity'])
183
+ moisture = float(request.form['moisture'])
184
+ soil_type = request.form['soil_type']
185
+ crop_type = request.form['crop_type']
186
+ nitrogen = float(request.form['nitrogen'])
187
+ potassium = float(request.form['potassium'])
188
+ phosphorous = float(request.form['phosphorous'])
189
+
190
+ # Encode categorical data
191
+ soil_type_encoded = soil_type_encodings.get(soil_type, -1)
192
+ crop_type_encoded = crop_type_encodings.get(crop_type, -1)
193
+
194
+ # Create a DataFrame for the input
195
+ user_input = pd.DataFrame({
196
+ 'Temperature': [temperature],
197
+ 'Humidity': [humidity],
198
+ 'Moisture': [moisture],
199
+ 'Nitrogen': [nitrogen],
200
+ 'Potassium': [potassium],
201
+ 'Phosphorous': [phosphorous],
202
+ 'Soil Type': [soil_type_encoded],
203
+ 'Crop Type': [crop_type_encoded]
204
+ })
205
+
206
+ # Predict Fertilizer Name
207
+ pred_fertilizer_name = rf_ferti_name.predict(user_input)[0]
208
+ pred_fertilizer_name = [name for name, value in fertilizer_name_encodings.items() if value == pred_fertilizer_name][0]
209
+
210
+ # Predict Fertilizer Quantity
211
+ pred_fertilizer_qty = rf_ferti_value.predict(user_input)[0]
212
+
213
+ # Generate AI suggestions with fallback system
214
+ pred_info = generate_ai_suggestions(pred_fertilizer_name)
215
+
216
+ return render_template('index.html', prediction=True, fertilizer_name=pred_fertilizer_name,
217
+ fertilizer_qty=pred_fertilizer_qty, optimal_usage=pred_fertilizer_qty, pred_info=pred_info)
218
+ return render_template('index.html', prediction=False)
219
+
220
+
221
+ if __name__ == '__main__':
222
+ print("\n" + "=" * 60)
223
+ print("🚀 Starting Fertilizer Recommendation App")
224
+ print("=" * 60)
225
+ print(f"📊 Configuration:")
226
+ print(f" - Gemini API: {'✓ Configured' if GEMINI_API_KEY else '✗ Not configured'}")
227
+ print(f" - NVIDIA API: {'✓ Configured' if NVIDIA_API_KEY else '✗ Not configured'}")
228
+ print(f" - Gemini Models: {len(GEMINI_MODELS)}")
229
+ print(f" - NVIDIA Models: {len(NVIDIA_MODELS)}")
230
+ print("=" * 60 + "\n")
231
+ app.run(port=7860, host='0.0.0.0')
app_enhanced.py ADDED
@@ -0,0 +1,628 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from flask import Flask, render_template, request, jsonify, redirect, url_for, flash, session
3
+ import requests
4
+ from werkzeug.utils import secure_filename
5
+ import google.generativeai as genai
6
+ import base64
7
+ import json
8
+ from datetime import datetime, timedelta
9
+ import threading
10
+ import time
11
+ from gtts import gTTS
12
+ import dotenv
13
+ import markdown
14
+ from openai import OpenAI
15
+ from typing import Optional, Dict, Any
16
+
17
+ # Load environment variables if a .env file is present
18
+ dotenv.load_dotenv()
19
+
20
+ def markdown_to_html(text):
21
+ """Convert markdown text to HTML for proper rendering."""
22
+ if not text:
23
+ return text
24
+ return markdown.markdown(text, extensions=['nl2br'])
25
+
26
+ # --- Configuration ---
27
+ # Ensure you have set these as environment variables in your deployment environment
28
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
29
+ GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
30
+ GOOGLE_CX = os.getenv("GOOGLE_CX")
31
+ NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY")
32
+
33
+ # Validate API keys with helpful messages
34
+ if not GEMINI_API_KEY:
35
+ print("WARNING: GEMINI_API_KEY not found. Will rely on NVIDIA fallback if available.")
36
+ print(" → For Hugging Face: Set this in Space Settings > Repository Secrets")
37
+ if not GOOGLE_API_KEY or not GOOGLE_CX:
38
+ print("WARNING: GOOGLE_API_KEY or GOOGLE_CX is not set. Web and product search features will be disabled.")
39
+ print(" → For Hugging Face: Set these in Space Settings > Repository Secrets")
40
+ if not NVIDIA_API_KEY:
41
+ print("WARNING: NVIDIA_API_KEY not set. NVIDIA fallback will not be available.")
42
+ print(" → For Hugging Face: Set this in Space Settings > Repository Secrets")
43
+
44
+ # Configure Gemini only if API key is available
45
+ if GEMINI_API_KEY:
46
+ genai.configure(api_key=GEMINI_API_KEY)
47
+ else:
48
+ print("⚠️ Gemini API not configured. Application will use NVIDIA fallback only.")
49
+
50
+ # --- ENHANCED MODEL CONFIGURATION ---
51
+ GEMINI_MODELS = [
52
+ {"name": "gemini-2.0-flash-exp", "max_retries": 2, "timeout": 30, "description": "Latest experimental"},
53
+ {"name": "gemini-1.5-pro-latest", "max_retries": 2, "timeout": 45, "description": "Most capable"},
54
+ {"name": "gemini-1.5-flash", "max_retries": 3, "timeout": 20, "description": "Fast and reliable"},
55
+ {"name": "gemini-1.5-flash-8b", "max_retries": 3, "timeout": 15, "description": "Lightweight"},
56
+ ]
57
+
58
+ NVIDIA_MODELS = [
59
+ {"name": "meta/llama-3.2-90b-vision-instruct", "max_retries": 2, "timeout": 40, "description": "High capability"},
60
+ {"name": "meta/llama-3.2-11b-vision-instruct", "max_retries": 2, "timeout": 30, "description": "Balanced"},
61
+ ]
62
+
63
+ app = Flask(__name__)
64
+ app.secret_key = os.getenv("SECRET_KEY", "a-strong-default-secret-key")
65
+
66
+ # Configure folders
67
+ UPLOAD_FOLDER = 'static/uploads'
68
+ AUDIO_FOLDER = 'static/audio'
69
+ ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg'}
70
+
71
+ # Create directories with better error handling for Hugging Face
72
+ try:
73
+ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
74
+ os.makedirs(AUDIO_FOLDER, exist_ok=True)
75
+ print(f"✓ Created directories: {UPLOAD_FOLDER}, {AUDIO_FOLDER}")
76
+ except OSError as e:
77
+ print(f"⚠️ Warning: Could not create directories: {e}")
78
+ print(" → This may be normal on Hugging Face Spaces with read-only filesystem")
79
+
80
+ app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
81
+
82
+
83
+ def allowed_file(filename):
84
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
85
+
86
+
87
+ def encode_image(image_path):
88
+ with open(image_path, "rb") as image_file:
89
+ return base64.b64encode(image_file.read()).decode('utf-8')
90
+
91
+
92
+ def retry_with_backoff(func, max_retries=3, initial_delay=1):
93
+ """
94
+ Retry a function with exponential backoff.
95
+
96
+ Args:
97
+ func: Function to retry
98
+ max_retries: Maximum number of retry attempts
99
+ initial_delay: Initial delay in seconds (doubles each retry)
100
+
101
+ Returns:
102
+ Result of successful function call
103
+
104
+ Raises:
105
+ Last exception if all retries fail
106
+ """
107
+ last_exception = None
108
+ for attempt in range(max_retries):
109
+ try:
110
+ return func()
111
+ except Exception as e:
112
+ last_exception = e
113
+ if attempt == max_retries - 1:
114
+ raise
115
+ delay = initial_delay * (2 ** attempt)
116
+ print(f" >> Retry {attempt + 1}/{max_retries} after {delay}s delay (Error: {type(e).__name__})")
117
+ time.sleep(delay)
118
+
119
+ raise last_exception
120
+
121
+
122
+ def analyze_with_gemini(image_path, prompt, model_config):
123
+ """
124
+ Analyze with a specific Gemini model with retry logic.
125
+
126
+ Args:
127
+ image_path: Path to the image file
128
+ prompt: Text prompt for analysis
129
+ model_config: Dict with model name, retries, timeout
130
+
131
+ Returns:
132
+ Response text or None if failed
133
+ """
134
+ model_name = model_config["name"]
135
+ max_retries = model_config.get("max_retries", 2)
136
+
137
+ def _attempt():
138
+ print(f" >> Attempting Gemini model: {model_name} ({model_config.get('description', '')})")
139
+ model = genai.GenerativeModel(model_name)
140
+ image_parts = [{"mime_type": "image/jpeg", "data": encode_image(image_path)}]
141
+
142
+ response = model.generate_content(
143
+ [prompt] + image_parts,
144
+ generation_config={
145
+ "temperature": 0.2,
146
+ "max_output_tokens": 2048,
147
+ }
148
+ )
149
+
150
+ if not response or not response.text:
151
+ raise ValueError("Empty response from model")
152
+
153
+ return response.text
154
+
155
+ try:
156
+ return retry_with_backoff(_attempt, max_retries=max_retries)
157
+ except Exception as e:
158
+ error_type = type(e).__name__
159
+ error_msg = str(e)[:100]
160
+ print(f" >> FAILED {model_name}: {error_type}: {error_msg}")
161
+ return None
162
+
163
+
164
+ def analyze_with_nvidia(image_path, prompt, model_config):
165
+ """
166
+ Analyze image using NVIDIA's API via OpenAI-compatible client with retry logic.
167
+
168
+ Args:
169
+ image_path: Path to the image file
170
+ prompt: Text prompt for analysis
171
+ model_config: Dict with model name, retries, timeout
172
+
173
+ Returns:
174
+ Response text or None if failed
175
+ """
176
+ model_name = model_config["name"]
177
+ max_retries = model_config.get("max_retries", 2)
178
+ timeout = model_config.get("timeout", 30)
179
+
180
+ if not NVIDIA_API_KEY:
181
+ print("NVIDIA API key not available.")
182
+ return None
183
+
184
+ def _attempt():
185
+ print(f" >> Attempting NVIDIA model: {model_name} ({model_config.get('description', '')})")
186
+
187
+ client = OpenAI(
188
+ base_url="https://integrate.api.nvidia.com/v1",
189
+ api_key=NVIDIA_API_KEY
190
+ )
191
+
192
+ base64_image = encode_image(image_path)
193
+
194
+ completion = client.chat.completions.create(
195
+ model=model_name,
196
+ messages=[{
197
+ "role": "user",
198
+ "content": [
199
+ {"type": "text", "text": prompt},
200
+ {
201
+ "type": "image_url",
202
+ "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}
203
+ }
204
+ ]
205
+ }],
206
+ max_tokens=2000,
207
+ temperature=0.2,
208
+ timeout=timeout
209
+ )
210
+
211
+ response_text = completion.choices[0].message.content
212
+ if not response_text:
213
+ raise ValueError("Empty response from NVIDIA")
214
+
215
+ return response_text
216
+
217
+ try:
218
+ return retry_with_backoff(_attempt, max_retries=max_retries)
219
+ except Exception as e:
220
+ error_type = type(e).__name__
221
+ error_msg = str(e)[:100]
222
+ print(f" >> FAILED NVIDIA {model_name}: {error_type}: {error_msg}")
223
+ return None
224
+
225
+
226
+ def get_web_pesticide_info(disease, plant_type="Unknown"):
227
+ """Fetch pesticide information from web sources."""
228
+ if not GOOGLE_API_KEY or not GOOGLE_CX:
229
+ print("Skipping web search: Google API credentials not set.")
230
+ return None
231
+ query = f"site:agrowon.esakal.com {disease} in {plant_type}"
232
+ url = "https://www.googleapis.com/customsearch/v1"
233
+ params = {"key": GOOGLE_API_KEY, "cx": GOOGLE_CX, "q": query, "num": 1}
234
+ try:
235
+ response = requests.get(url, params=params, timeout=10)
236
+ response.raise_for_status()
237
+ data = response.json()
238
+ if "items" in data and data["items"]:
239
+ item = data["items"][0]
240
+ return {
241
+ "title": item.get("title", "No title"),
242
+ "link": item.get("link", "#"),
243
+ "summary": item.get("snippet", "No summary available")
244
+ }
245
+ except requests.exceptions.RequestException as e:
246
+ print(f"Error retrieving web pesticide info for '{disease}': {e}")
247
+ return None
248
+
249
+
250
+ def get_commercial_product_info(recommendation, disease_name):
251
+ """Fetch commercial product information."""
252
+ if not GOOGLE_API_KEY or not GOOGLE_CX:
253
+ print("Skipping product search: Google API credentials not set.")
254
+ return []
255
+ queries = [
256
+ f"site:indiamart.com pesticide for '{disease_name}' '{recommendation}'",
257
+ f"site:krishisevakendra.in pesticide for '{disease_name}' '{recommendation}'"
258
+ ]
259
+ results = []
260
+ for query in queries:
261
+ url = "https://www.googleapis.com/customsearch/v1"
262
+ params = {"key": GOOGLE_API_KEY, "cx": GOOGLE_CX, "q": query, "num": 2}
263
+ try:
264
+ response = requests.get(url, params=params, timeout=10)
265
+ response.raise_for_status()
266
+ data = response.json()
267
+ if "items" in data:
268
+ for item in data["items"]:
269
+ results.append({
270
+ "title": item.get("title", "No title"),
271
+ "link": item.get("link", "#"),
272
+ "snippet": item.get("snippet", "No snippet available")
273
+ })
274
+ except requests.exceptions.RequestException as e:
275
+ print(f"Error retrieving product info with query '{query}': {e}")
276
+ return results
277
+
278
+
279
+ def generate_audio(text, language, filename):
280
+ """Generate an MP3 file from text using gTTS."""
281
+ try:
282
+ lang_mapping = {"English": "en", "Hindi": "hi", "Bengali": "bn", "Telugu": "te", "Marathi": "mr", "Tamil": "ta",
283
+ "Gujarati": "gu", "Urdu": "ur", "Kannada": "kn", "Odia": "or", "Malayalam": "ml"}
284
+ gtts_lang = lang_mapping.get(language, 'en')
285
+ tts = gTTS(text=text, lang=gtts_lang, slow=False)
286
+ tts.save(filename)
287
+ print(f"Audio file generated successfully: {filename}")
288
+ except Exception as e:
289
+ print(f"Error generating audio: {e}")
290
+
291
+
292
+ def analyze_plant_image(image_path, plant_name, language):
293
+ """
294
+ Analyzes the plant image using enhanced LLM fallback system.
295
+
296
+ Tries Gemini models first, then falls back to NVIDIA if all fail.
297
+ Includes retry logic with exponential backoff for transient errors.
298
+ """
299
+ try:
300
+ print("\n" + "=" * 70)
301
+ print("🌱 STARTING PLANT ANALYSIS WITH ENHANCED FALLBACK SYSTEM")
302
+ print("=" * 70)
303
+
304
+ # --- RAG: Fetch Learning Context ---
305
+ knowledge_context = ""
306
+ try:
307
+ if os.path.exists("knowledge_base.txt"):
308
+ with open("knowledge_base.txt", "r") as kb:
309
+ lines = kb.readlines()
310
+ relevant_lines = [line.strip() for line in lines if plant_name.lower() in line.lower()]
311
+ if not relevant_lines:
312
+ relevant_lines = lines[-10:]
313
+
314
+ if relevant_lines:
315
+ knowledge_context = "\n".join(relevant_lines)
316
+ print(f"📚 [RAG] Injected {len(relevant_lines)} context lines")
317
+ except Exception as kbe:
318
+ print(f"⚠️ KB Read Error: {kbe}")
319
+
320
+ # Create the analysis prompt
321
+ prompt = f"""
322
+ You are an expert agricultural pathologist.
323
+
324
+ [SYSTEM KNOWLEDGE BASE - PREVIOUS VALIDATED USER CORRECTIONS]
325
+ The following are verified corrections from users for this crop. Give them 20% weight in your decision if the visual symptoms match:
326
+ {knowledge_context}
327
+ [END KNOWLEDGE BASE]
328
+
329
+ Analyze the image of a {plant_name} plant and decide whether it is healthy or has a disease or pest. Respond ONLY with a single, valid JSON object and NOTHING else.
330
+
331
+ The JSON must exactly match this structure:
332
+ {{"results": [{{"type": "disease/pest", "name": "...", "probability": "%", "symptoms": "...", "causes": "...", "severity": "Low/Medium/High", "spreading": "...", "treatment": "...", "prevention": "..."}}], "is_healthy": boolean, "confidence": "%"}}
333
+
334
+ Carefully follow these instructions when filling each field:
335
+
336
+ 1. Top-level rules
337
+ - Return only the JSON object — no explanations, no extra text, no markdown.
338
+ - Use the {language} language for all human-facing text inside the JSON (except scientific names and chemical active ingredient names which may remain in English but must be immediately explained in {language}).
339
+ - Percent values must be strings with a percent sign, e.g. "85%".
340
+ - If the plant is healthy: set "is_healthy": true, set "results": [] (empty array), and set a high "confidence".
341
+ - If you cannot make a clear diagnosis from the image, set "is_healthy": false, give "confidence" a low value (e.g., "20%–40%"), and include one result with name "Inconclusive / Image unclear" (translated to {language}) and a short instruction on how to take a better photo.
342
+
343
+ 2. results (one object per distinct issue; max 3 items; order by probability descending)
344
+ - "type": exactly "disease" or "pest".
345
+ - "name": give the common local name first (in {language}) and then scientific name in parentheses if available. Use names familiar to Indian farmers.
346
+ - "probability": your estimated chance this diagnosis is correct, as a percent string (e.g., "78%").
347
+ - "symptoms": list only observable signs a farmer can check (what to look for on leaves, stem, roots, fruits). Format as an HTML list (e.g., "<ul><li>Spot 1</li><li>Spot 2</li></ul>"). Use short simple sentences.
348
+ - "causes": 1–3 likely causes. Format as an HTML list (e.g., "<ul><li>Cause 1</li><li>Cause 2</li></ul>").
349
+ - "severity": choose exactly one of "Low", "Medium", or "High" and append a short reason in the same string (e.g., "High — fruit dropping"). Do NOT create a separate field.
350
+ - "spreading": describe how it spreads in simple terms (wind, water splash, touch, insects) and use one of these speed labels in the explanation: "None", "Slow", "Moderate", "Fast". Keep it short.
351
+ - "treatment": give a prioritized, farmer-friendly, step-by-step plan (max 5 steps). Format as an HTML ordered list (e.g., "<ol><li>Step 1</li><li>Step 2</li></ol>").
352
+ 1) Low-cost cultural controls,
353
+ 2) Biological/organic options,
354
+ 3) Chemical options only if necessary: list **active ingredient** names.
355
+ Write treatment steps in simple, imperative sentences.
356
+ - "prevention": provide 4–6 simple preventive tips. Format as an HTML list (e.g., "<ul><li>Tip 1</li><li>Tip 2</li></ul>").
357
+
358
+ 3. Additional formatting & behavior rules
359
+ - Use no null values; if unknown, use empty string "".
360
+ - Keep each text field concise and simple — aim for sentences a low-literacy farmer can understand.
361
+ - If you reference any chemical or biological product by active ingredient, include a short safety note and the phrase (in {language}): "देखें लेबल / क्षेत्रीय कृषि अधिकारी से सलाह लें" or equivalent in {language}.
362
+ - If recommending to contact an expert, mention the nearest trusted resource in India: "Krishi Vigyan Kendra / स्थानीय कृषि अधिकारी" (translated into {language}).
363
+ - If multiple issues are present, include up to 3 results. If only one issue, include only one result.
364
+
365
+ 4. Image-quality fallback
366
+ - If the image is blurry, dark, or shows only part of the plant, put an honest low confidence (e.g., "30%"), set "is_healthy": false, and in results provide "Inconclusive / Image unclear" with one short line in {language} explaining how to take a clear photo (full leaf + whole plant + close-up of affected area + daylight).
367
+
368
+ Strictly produce only the JSON object following the structure above and the language requirement. No additional output.
369
+ """
370
+
371
+ response_text = None
372
+ used_model_name = "None"
373
+
374
+ # --- PHASE 1: Try Gemini Models ---
375
+ if GEMINI_API_KEY:
376
+ print("\n" + "-" * 70)
377
+ print("📡 PHASE 1: Trying Gemini Models")
378
+ print("-" * 70)
379
+
380
+ for idx, model_config in enumerate(GEMINI_MODELS, 1):
381
+ print(f"\n[{idx}/{len(GEMINI_MODELS)}] Testing {model_config['name']}...")
382
+ response_text = analyze_with_gemini(image_path, prompt, model_config)
383
+
384
+ if response_text:
385
+ used_model_name = f"Gemini-{model_config['name']}"
386
+ print(f" ✓ SUCCESS with {used_model_name}")
387
+ break
388
+ else:
389
+ print(f" ✗ Failed, trying next model...")
390
+ else:
391
+ print("\n" + "-" * 70)
392
+ print("⚠️ PHASE 1: SKIPPED (No Gemini API key)")
393
+ print("-" * 70)
394
+
395
+ # --- PHASE 2: Try NVIDIA Models (Fallback) ---
396
+ if not response_text and NVIDIA_API_KEY:
397
+ print("\n" + "-" * 70)
398
+ print("📡 PHASE 2: Trying NVIDIA Models (Fallback)")
399
+ print("-" * 70)
400
+
401
+ for idx, model_config in enumerate(NVIDIA_MODELS, 1):
402
+ print(f"\n[{idx}/{len(NVIDIA_MODELS)}] Testing {model_config['name']}...")
403
+ response_text = analyze_with_nvidia(image_path, prompt, model_config)
404
+
405
+ if response_text:
406
+ used_model_name = f"NVIDIA-{model_config['name']}"
407
+ print(f" ✓ SUCCESS with {used_model_name}")
408
+ break
409
+ else:
410
+ print(f" ✗ Failed, trying next model...")
411
+ elif not response_text:
412
+ print("\n" + "-" * 70)
413
+ print("⚠️ PHASE 2: SKIPPED (No NVIDIA API key)")
414
+ print("-" * 70)
415
+
416
+ # --- PHASE 3: Final Error Handling ---
417
+ if not response_text:
418
+ error_msg = "❌ All LLM providers failed after retries."
419
+ if not GEMINI_API_KEY and not NVIDIA_API_KEY:
420
+ error_msg = "❌ No API keys configured. Set GEMINI_API_KEY or NVIDIA_API_KEY in environment."
421
+
422
+ print("\n" + "=" * 70)
423
+ print(f"ANALYSIS FAILED: {error_msg}")
424
+ print("=" * 70 + "\n")
425
+ raise RuntimeError(error_msg)
426
+
427
+ print("\n" + "=" * 70)
428
+ print(f"✅ ANALYSIS COMPLETE using {used_model_name}")
429
+ print("=" * 70)
430
+ print(f"📄 Response preview (first 300 chars): {response_text[:300]}...")
431
+ print("=" * 70 + "\n")
432
+
433
+ # --- Parse JSON Response ---
434
+ try:
435
+ json_start = response_text.find('{')
436
+ json_end = response_text.rfind('}') + 1
437
+ if json_start == -1 or json_end == 0:
438
+ raise ValueError("No JSON object found in the response.")
439
+ json_str = response_text[json_start:json_end]
440
+ analysis_result = json.loads(json_str)
441
+ print("✓ Successfully parsed JSON response.")
442
+ except (json.JSONDecodeError, ValueError) as e:
443
+ print(f"❌ ERROR: Failed to parse JSON from response.")
444
+ print(f"Error: {e}")
445
+ print(f"Raw Response Text: {response_text}")
446
+ return {"error": "Failed to parse API response. The format was invalid."}
447
+
448
+ # --- Generate Audio Summary ---
449
+ print("🔊 Generating audio summary...")
450
+ if analysis_result.get('is_healthy'):
451
+ summary_text = f"Your {plant_name} plant appears to be healthy."
452
+ elif analysis_result.get('results'):
453
+ result = analysis_result['results'][0]
454
+ summary_text = f"Issue detected: {result.get('name')}. Treatment suggestion: {result.get('treatment')}"
455
+ else:
456
+ summary_text = "Analysis was inconclusive."
457
+
458
+ audio_filename = "audio_result.mp3"
459
+ audio_path = os.path.join(AUDIO_FOLDER, audio_filename)
460
+ generate_audio(summary_text, language, audio_path)
461
+ analysis_result['audio_file'] = os.path.join('audio', audio_filename).replace('\\', '/')
462
+ print(f"✓ Audio file generated: {analysis_result['audio_file']}")
463
+
464
+ return analysis_result
465
+
466
+ except Exception as e:
467
+ print(f"\n{'=' * 70}")
468
+ print(f"❌ FATAL ERROR in analyze_plant_image: {e}")
469
+ print(f"{'=' * 70}\n")
470
+ return {"error": str(e), "is_healthy": None, "results": []}
471
+
472
+
473
+ @app.route('/', methods=['GET'])
474
+ def index():
475
+ return render_template('index.html')
476
+
477
+
478
+ @app.route('/feedback', methods=['POST'])
479
+ def feedback():
480
+ feedback_text = request.form.get("feedback")
481
+ plant_name = request.form.get("plant_name", "Unknown")
482
+ if not feedback_text:
483
+ flash("Please provide your feedback before submitting.")
484
+ return redirect(url_for('index'))
485
+
486
+ feedback_data = {"plant_name": plant_name, "feedback": feedback_text, "timestamp": datetime.now().isoformat()}
487
+
488
+ # --- FEEDBACK REINFORCEMENT LEARNING LOOP ---
489
+ def validate_and_learn(f_data, img_path):
490
+ """
491
+ Background task:
492
+ 1. Ask Gemini if this feedback is scientifically valid for the image.
493
+ 2. If valid, append to 'knowledge_base.txt' for future prompt injection.
494
+ """
495
+ try:
496
+ print(f"--- [RL] Validating Feedback: '{f_data['feedback']}' ---")
497
+
498
+ if not img_path or not os.path.exists(img_path):
499
+ print("--- [RL] No image found for validation. Skipping.")
500
+ return
501
+
502
+ if not GEMINI_API_KEY:
503
+ print("--- [RL] No Gemini API key. Skipping validation.")
504
+ return
505
+
506
+ model = genai.GenerativeModel('gemini-1.5-flash')
507
+ img_file = {"mime_type": "image/jpeg", "data": encode_image(img_path)}
508
+
509
+ validation_prompt = f"""
510
+ You are a Senior Agricultural Quality Control Auditor.
511
+ A user provided the following feedback/correction for an AI diagnosis of this {f_data['plant_name']} plant:
512
+
513
+ USER FEEDBACK: "{f_data['feedback']}"
514
+
515
+ Task:
516
+ 1. Analyze the image and the user's claim.
517
+ 2. Determine if the feedback is PLAUSIBLE or CORRECT based on visual evidence.
518
+ 3. Respond with ONLY 'VALID' or 'INVALID'.
519
+ """
520
+
521
+ resp = model.generate_content([validation_prompt, img_file])
522
+ verdict = resp.text.strip().upper()
523
+
524
+ print(f"--- [RL] Verdict: {verdict} ---")
525
+
526
+ if "VALID" in verdict:
527
+ kb_entry = f"[{f_data['plant_name']}] Verified User Insight: {f_data['feedback']} (Visuals confirmed)\n"
528
+ with open("knowledge_base.txt", "a") as kb:
529
+ kb.write(kb_entry)
530
+ print("--- [RL] Knowledge Base Updated! ---")
531
+
532
+ except Exception as e:
533
+ print(f"--- [RL] Validation Failed: {e}")
534
+
535
+ image_filename = request.form.get("image_filename")
536
+ if image_filename:
537
+ full_img_path = os.path.join(app.config['UPLOAD_FOLDER'], image_filename)
538
+ threading.Thread(target=validate_and_learn, args=(feedback_data, full_img_path)).start()
539
+
540
+ flash("Thank you! Your feedback is being analyzed to improve future predictions.")
541
+ return redirect(url_for('index'))
542
+
543
+
544
+ @app.route('/analyze', methods=['POST'])
545
+ def analyze():
546
+ print("\n" + "=" * 70)
547
+ print("🔬 NEW ANALYSIS REQUEST RECEIVED")
548
+ print("=" * 70)
549
+
550
+ if 'plant_image' not in request.files:
551
+ flash('No file part')
552
+ return redirect(request.url)
553
+
554
+ file = request.files['plant_image']
555
+ plant_name = request.form.get('plant_name', 'Unknown Plant')
556
+ language = request.form.get('language', 'English')
557
+
558
+ if file.filename == '':
559
+ flash('No selected file')
560
+ return redirect(request.url)
561
+
562
+ if file and allowed_file(file.filename):
563
+ try:
564
+ filename = secure_filename(file.filename)
565
+ file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
566
+ print(f"💾 Saving uploaded file to: {file_path}")
567
+ file.save(file_path)
568
+
569
+ analysis_result = analyze_plant_image(file_path, plant_name, language)
570
+
571
+ if 'error' in analysis_result:
572
+ flash(f"Analysis Error: {analysis_result['error']}")
573
+ return redirect(url_for('index'))
574
+
575
+ # Convert markdown to HTML in all result fields
576
+ if analysis_result.get('results'):
577
+ for result in analysis_result['results']:
578
+ for field in ['symptoms', 'causes', 'spreading', 'treatment', 'prevention']:
579
+ if field in result:
580
+ result[field] = markdown_to_html(result[field])
581
+
582
+ web_info = {}
583
+ product_info = {}
584
+ if not analysis_result.get('is_healthy') and analysis_result.get('results'):
585
+ print("🔍 Disease detected. Fetching additional web and product info...")
586
+ for result in analysis_result['results']:
587
+ disease_name = result.get('name', '')
588
+ if disease_name:
589
+ web_info[disease_name] = get_web_pesticide_info(disease_name, plant_name)
590
+ product_info[disease_name] = get_commercial_product_info(result.get('treatment', ''), disease_name)
591
+ print("✓ Finished fetching additional info.")
592
+
593
+ print("\n" + "=" * 70)
594
+ print("✅ Analysis complete. Rendering results page.")
595
+ print("=" * 70 + "\n")
596
+
597
+ return render_template(
598
+ 'results.html',
599
+ results=analysis_result,
600
+ plant_name=plant_name,
601
+ image_path='uploads/' + filename,
602
+ web_info=web_info,
603
+ product_info=product_info
604
+ )
605
+ except Exception as e:
606
+ print(f"\n{'=' * 70}")
607
+ print(f"❌ FATAL ERROR IN /analyze ROUTE: {e}")
608
+ print(f"{'=' * 70}\n")
609
+ flash(f"A critical server error occurred: {e}")
610
+ return redirect(url_for('index'))
611
+
612
+ flash('Invalid file type. Please upload an image (png, jpg, jpeg).')
613
+ return redirect(request.url)
614
+
615
+
616
+ if __name__ == '__main__':
617
+ port = int(os.environ.get("PORT", 7860))
618
+ print("\n" + "=" * 70)
619
+ print(f"🚀 Starting Flask Application on port {port}")
620
+ print("=" * 70)
621
+ print(f"📊 Configuration Status:")
622
+ print(f" - Gemini API: {'✓ Configured' if GEMINI_API_KEY else '✗ Not configured'}")
623
+ print(f" - NVIDIA API: {'✓ Configured' if NVIDIA_API_KEY else '✗ Not configured'}")
624
+ print(f" - Google Search: {'✓ Configured' if (GOOGLE_API_KEY and GOOGLE_CX) else '✗ Not configured'}")
625
+ print(f" - Available Gemini Models: {len(GEMINI_MODELS)}")
626
+ print(f" - Available NVIDIA Models: {len(NVIDIA_MODELS)}")
627
+ print("=" * 70 + "\n")
628
+ app.run(host='0.0.0.0', port=port, debug=True)
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ gunicorn
2
+ joblib==1.4.2
3
+ scikit-learn==1.5.2
4
+ pandas
5
+ google.generativeai
6
+ flask
rf_ferti_name.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cc6660f582c74c073743822c359cecff86469b6fd0b9df2a5b1c6d30d9d6f766
3
+ size 18260649
rf_ferti_value.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1cbcee671a228c55f243ad16b5d71bc76655bfea88d5a99bf8e5a0a738cb9468
3
+ size 45422609
templates/index.html ADDED
@@ -0,0 +1,539 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Fertilizer Recommender & Usage Requirement Estimator</title>
8
+ <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
9
+ <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
10
+ <link rel="preconnect" href="https://fonts.googleapis.com">
11
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
13
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
14
+
15
+ <style>
16
+ :root {
17
+ --color-primary: #1a5d3a;
18
+ --color-primary-light: #2d7a52;
19
+ --color-primary-dark: #143d2e;
20
+ --color-accent: #198754;
21
+ --color-accent-light: #28a745;
22
+ --bg-light: #eaf6ee;
23
+ --bg-gradient-start: #f0fdf4;
24
+ --bg-gradient-end: #dcfce7;
25
+ --surface: #ffffff;
26
+ --text: #1f2937;
27
+ --text-light: #6b7280;
28
+ --border: #143d2e;
29
+ --border-light: rgba(20, 61, 46, 0.2);
30
+
31
+ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.04);
32
+ --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.08);
33
+ --shadow-lg: 0 10px 40px rgba(0, 0, 0, 0.12);
34
+ --shadow-xl: 0 20px 60px rgba(0, 0, 0, 0.15);
35
+
36
+ --radius-sm: 8px;
37
+ --radius-md: 12px;
38
+ --radius-lg: 20px;
39
+ --radius-xl: 24px;
40
+
41
+ --space-xs: 0.5rem;
42
+ --space-sm: 1rem;
43
+ --space-md: 1.5rem;
44
+ --space-lg: 2rem;
45
+ --space-xl: 3rem;
46
+ --space-2xl: 4rem;
47
+ }
48
+
49
+ * {
50
+ box-sizing: border-box;
51
+ margin: 0;
52
+ padding: 0;
53
+ }
54
+
55
+ body {
56
+ background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%);
57
+ font-family: 'Outfit', sans-serif;
58
+ color: var(--text);
59
+ min-height: 100vh;
60
+ line-height: 1.6;
61
+ }
62
+
63
+ .main-container {
64
+ max-width: 1400px;
65
+ margin: 0 auto;
66
+ padding: 0 var(--space-md);
67
+ }
68
+
69
+ .heading {
70
+ background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-light) 100%);
71
+ color: white;
72
+ padding: var(--space-2xl) var(--space-md);
73
+ text-align: center;
74
+ position: relative;
75
+ overflow: hidden;
76
+ margin: 0 0 var(--space-xl) 0;
77
+ border-radius: 0 0 var(--radius-xl) var(--radius-xl);
78
+ }
79
+
80
+ .heading::before {
81
+ content: '';
82
+ position: absolute;
83
+ top: 0;
84
+ left: 0;
85
+ right: 0;
86
+ bottom: 0;
87
+ background:
88
+ radial-gradient(circle at 20% 50%, rgba(255,255,255,0.1) 0%, transparent 50%),
89
+ radial-gradient(circle at 80% 80%, rgba(255,255,255,0.08) 0%, transparent 50%);
90
+ pointer-events: none;
91
+ }
92
+
93
+ .heading i {
94
+ font-size: 3rem;
95
+ margin-bottom: var(--space-sm);
96
+ display: block;
97
+ opacity: 0.9;
98
+ }
99
+
100
+ .heading h1 {
101
+ font-size: 2.5rem;
102
+ font-weight: 700;
103
+ letter-spacing: -0.5px;
104
+ position: relative;
105
+ z-index: 1;
106
+ text-shadow: 0 2px 10px rgba(0,0,0,0.1);
107
+ margin: 0;
108
+ }
109
+
110
+ .heading p {
111
+ font-size: 1.1rem;
112
+ margin-top: var(--space-sm);
113
+ opacity: 0.95;
114
+ font-weight: 300;
115
+ }
116
+
117
+ .input-container {
118
+ background: var(--bg-light);
119
+ border-radius: 20px;
120
+ padding: var(--space-2xl);
121
+ box-shadow: var(--shadow-xl);
122
+ margin-bottom: var(--space-xl);
123
+ position: relative;
124
+ z-index: 10;
125
+ border: 2px solid var(--border);
126
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
127
+ }
128
+
129
+ .input-container:hover {
130
+ transform: translateY(-4px);
131
+ box-shadow: 0 25px 70px rgba(0, 0, 0, 0.18);
132
+ }
133
+
134
+ .input-container::before {
135
+ content: '';
136
+ position: absolute;
137
+ top: 0;
138
+ left: 0;
139
+ right: 0;
140
+ height: 4px;
141
+ background: linear-gradient(90deg, var(--color-primary) 0%, var(--color-accent) 100%);
142
+ border-radius: var(--radius-xl) var(--radius-xl) 0 0;
143
+ }
144
+
145
+ .form-group {
146
+ margin-bottom: var(--space-md);
147
+ }
148
+
149
+ .form-group label {
150
+ display: flex;
151
+ align-items: center;
152
+ gap: var(--space-xs);
153
+ font-weight: 600;
154
+ color: var(--text);
155
+ margin-bottom: var(--space-xs);
156
+ font-size: 0.95rem;
157
+ }
158
+
159
+ .form-group label i {
160
+ font-size: 1.1rem;
161
+ color: var(--color-accent);
162
+ }
163
+
164
+ .form-control {
165
+ background: var(--bg-light);
166
+ border: 2px solid transparent;
167
+ border-radius: var(--radius-md);
168
+
169
+ font-family: 'Outfit', sans-serif;
170
+ font-size: 1rem;
171
+ color: var(--text);
172
+ width: 100%;
173
+ transition: all 0.3s ease;
174
+ box-shadow: var(--shadow-sm);
175
+ }
176
+
177
+ .form-control:hover {
178
+ border-color: var(--border-light);
179
+ }
180
+
181
+ .form-control:focus {
182
+ background: var(--surface);
183
+ outline: none;
184
+ border-color: var(--color-accent);
185
+ box-shadow: 0 0 0 4px rgba(25, 135, 84, 0.1), var(--shadow-md);
186
+ transform: translateY(-1px);
187
+ }
188
+
189
+ .form-control::placeholder {
190
+ color: var(--text-light);
191
+ font-weight: 300;
192
+ }
193
+
194
+ .predict-btn {
195
+ background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-accent) 100%);
196
+ color: white;
197
+ border: none;
198
+ border-radius: var(--radius-md);
199
+ padding: 1rem 2rem;
200
+ font-family: 'Outfit', sans-serif;
201
+ font-weight: 600;
202
+ font-size: 1.1rem;
203
+ cursor: pointer;
204
+ width: 100%;
205
+ margin-top: var(--space-md);
206
+ position: relative;
207
+ overflow: hidden;
208
+ transition: all 0.3s ease;
209
+ box-shadow: 0 4px 15px rgba(26, 93, 58, 0.3);
210
+ }
211
+
212
+ .predict-btn::before {
213
+ content: '';
214
+ position: absolute;
215
+ top: 0;
216
+ left: -100%;
217
+ width: 100%;
218
+ height: 100%;
219
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
220
+ transition: left 0.5s ease;
221
+ }
222
+
223
+ .predict-btn:hover::before {
224
+ left: 100%;
225
+ }
226
+
227
+ .predict-btn:hover {
228
+ transform: translateY(-2px);
229
+ box-shadow: 0 6px 25px rgba(26, 93, 58, 0.4);
230
+ }
231
+
232
+ .predict-btn:active {
233
+ transform: translateY(0);
234
+ }
235
+
236
+ .predict-btn:disabled {
237
+ opacity: 0.7;
238
+ cursor: not-allowed;
239
+ }
240
+
241
+ .result-container {
242
+ display: grid;
243
+ grid-template-columns: 1fr 1fr;
244
+ gap: var(--space-xl);
245
+ margin-top: var(--space-xl);
246
+ margin-bottom: var(--space-2xl);
247
+ animation: fadeInUp 0.6s ease-out;
248
+ }
249
+
250
+ @media (max-width: 968px) {
251
+ .result-container {
252
+ grid-template-columns: 1fr;
253
+ }
254
+ }
255
+
256
+ .left-container,
257
+ .right-container {
258
+ background: var(--surface);
259
+ border: 1px solid var(--border-light);
260
+ border-radius: var(--radius-lg);
261
+ padding: var(--space-xl);
262
+ box-shadow: var(--shadow-lg);
263
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
264
+ position: relative;
265
+ overflow: hidden;
266
+ }
267
+
268
+ .left-container::before,
269
+ .right-container::before {
270
+ content: '';
271
+ position: absolute;
272
+ top: 0;
273
+ left: 0;
274
+ right: 0;
275
+ height: 3px;
276
+ background: linear-gradient(90deg, var(--color-primary) 0%, var(--color-accent) 100%);
277
+ }
278
+
279
+ .left-container:hover,
280
+ .right-container:hover {
281
+ transform: translateY(-6px);
282
+ box-shadow: 0 15px 50px rgba(0, 0, 0, 0.15);
283
+ }
284
+
285
+ .section-heading {
286
+ color: var(--color-primary);
287
+ font-weight: 600;
288
+ text-align: center;
289
+ margin-bottom: var(--space-lg);
290
+ font-size: 1.5rem;
291
+ }
292
+
293
+ .fertilizer-name {
294
+ background: linear-gradient(135deg, rgba(26, 93, 58, 0.08) 0%, rgba(25, 135, 84, 0.08) 100%);
295
+ color: var(--color-primary);
296
+ font-weight: 700;
297
+ font-size: 2rem;
298
+ text-align: center;
299
+ padding: var(--space-md);
300
+ border-radius: var(--radius-md);
301
+ border-left: 5px solid var(--color-accent);
302
+ margin: var(--space-md) 0;
303
+ box-shadow: var(--shadow-sm);
304
+ }
305
+
306
+ .fertilizer-info {
307
+ background: var(--bg-light);
308
+ padding: var(--space-lg);
309
+ border-radius: var(--radius-md);
310
+ border: 1px solid var(--border-light);
311
+ margin-top: var(--space-md);
312
+ line-height: 1.8;
313
+ color: var(--text);
314
+ font-size: 1.05rem;
315
+ }
316
+
317
+ .fertilizer-info strong {
318
+ color: var(--color-primary);
319
+ font-weight: 600;
320
+ }
321
+
322
+ .gauge-container {
323
+ display: flex;
324
+ justify-content: center;
325
+ align-items: center;
326
+ padding: var(--space-md);
327
+ background: var(--bg-light);
328
+ border-radius: var(--radius-md);
329
+ }
330
+
331
+ @keyframes fadeInUp {
332
+ from {
333
+ opacity: 0;
334
+ transform: translateY(30px);
335
+ }
336
+ to {
337
+ opacity: 1;
338
+ transform: translateY(0);
339
+ }
340
+ }
341
+
342
+ .input-container {
343
+ animation: fadeInUp 0.5s ease-out;
344
+ }
345
+
346
+ .form-row {
347
+ display: grid;
348
+ grid-template-columns: 1fr 1fr;
349
+ gap: var(--space-md);
350
+ margin-bottom: 0;
351
+ }
352
+
353
+ @media (max-width: 768px) {
354
+ .form-row {
355
+ grid-template-columns: 1fr;
356
+ }
357
+
358
+ .heading h1 {
359
+ font-size: 2rem;
360
+ }
361
+ }
362
+
363
+ button:focus,
364
+ input:focus,
365
+ select:focus {
366
+ outline: 2px solid var(--color-accent);
367
+ outline-offset: 2px;
368
+ }
369
+
370
+ ::-webkit-scrollbar {
371
+ width: 10px;
372
+ }
373
+
374
+ ::-webkit-scrollbar-track {
375
+ background: var(--bg-light);
376
+ }
377
+
378
+ ::-webkit-scrollbar-thumb {
379
+ background: var(--color-primary);
380
+ border-radius: 5px;
381
+ }
382
+
383
+ ::-webkit-scrollbar-thumb:hover {
384
+ background: var(--color-primary-dark);
385
+ }
386
+ </style>
387
+ </head>
388
+
389
+ <body>
390
+ <div class="heading">
391
+ <i class="bi bi-flower2"></i>
392
+ <h1>Fertilizer Recommender & Usage Requirement Estimator</h1>
393
+ <p>Get intelligent fertilizer recommendations based on your soil and crop conditions</p>
394
+ </div>
395
+
396
+ <div class="main-container">
397
+ <form method="post" class="input-container">
398
+ <div class="form-row">
399
+ <div class="form-group">
400
+ <label for="temperature"><i class="bi bi-thermometer-half"></i> Temperature (°C)</label>
401
+ <input type="number" class="form-control" id="temperature" name="temperature" placeholder="Enter temperature" step="0.1" required>
402
+ </div>
403
+ <div class="form-group">
404
+ <label for="humidity"><i class="bi bi-droplet-half"></i> Humidity (%)</label>
405
+ <input type="number" class="form-control" id="humidity" name="humidity" placeholder="Enter humidity" step="0.1" min="0" max="100" required>
406
+ </div>
407
+ </div>
408
+
409
+ <div class="form-row">
410
+ <div class="form-group">
411
+ <label for="moisture"><i class="bi bi-moisture"></i> Soil Moisture (%)</label>
412
+ <input type="number" class="form-control" id="moisture" name="moisture" placeholder="Enter soil moisture" step="0.1" min="0" max="100" required>
413
+ </div>
414
+ <div class="form-group">
415
+ <label for="soil_type"><i class="bi bi-layers-fill"></i> Soil Type</label>
416
+ <select class="form-control" id="soil_type" name="soil_type" required>
417
+ <option value="" disabled selected>Select soil type</option>
418
+ <option value="Black">Black</option>
419
+ <option value="Sandy">Sandy</option>
420
+ <option value="Loamy">Loamy</option>
421
+ <option value="Clayey">Clayey</option>
422
+ <option value="Red">Red</option>
423
+ </select>
424
+ </div>
425
+ </div>
426
+
427
+ <div class="form-row">
428
+ <div class="form-group">
429
+ <label for="crop_type"><i class="bi bi-flower3"></i> Crop Type</label>
430
+ <select class="form-control" id="crop_type" name="crop_type" required>
431
+ <option value="" disabled selected>Select crop type</option>
432
+ <option value="Barley">Barley</option>
433
+ <option value="Coffee">Coffee</option>
434
+ <option value="Cotton">Cotton</option>
435
+ <option value="Ground Nuts">Ground Nuts</option>
436
+ <option value="Maize">Maize</option>
437
+ <option value="Millets">Millets</option>
438
+ <option value="Oil seeds">Oil seeds</option>
439
+ <option value="Paddy">Paddy</option>
440
+ <option value="Pulses">Pulses</option>
441
+ <option value="Rice">Rice</option>
442
+ <option value="Sugarcane">Sugarcane</option>
443
+ <option value="Tobacco">Tobacco</option>
444
+ <option value="Wheat">Wheat</option>
445
+ <option value="Other Variety">Other Variety</option>
446
+ </select>
447
+ </div>
448
+ <div class="form-group">
449
+ <label for="nitrogen"><i class="bi bi-circle-fill" style="color: #4169E1;"></i> Nitrogen (N)</label>
450
+ <input type="number" class="form-control" id="nitrogen" name="nitrogen" placeholder="Enter nitrogen level" step="0.1" min="0" required>
451
+ </div>
452
+ </div>
453
+
454
+ <div class="form-row">
455
+ <div class="form-group">
456
+ <label for="potassium"><i class="bi bi-circle-fill" style="color: #FF6347;"></i> Potassium (K)</label>
457
+ <input type="number" class="form-control" id="potassium" name="potassium" placeholder="Enter potassium level" step="0.1" min="0" required>
458
+ </div>
459
+ <div class="form-group">
460
+ <label for="phosphorous"><i class="bi bi-circle-fill" style="color: #FFD700;"></i> Phosphorous (P)</label>
461
+ <input type="number" class="form-control" id="phosphorous" name="phosphorous" placeholder="Enter phosphorous level" step="0.1" min="0" required>
462
+ </div>
463
+ </div>
464
+
465
+ <button type="submit" class="predict-btn">
466
+ <i class="bi bi-search"></i> Recommend Fertilizer
467
+ </button>
468
+ </form>
469
+
470
+ {% if prediction %}
471
+ <div class="result-container">
472
+ <div class="left-container">
473
+ <h3 class="section-heading">Recommended Fertilizer</h3>
474
+ <div class="fertilizer-name">{{ fertilizer_name }}</div>
475
+ {% if pred_info %}
476
+ <div class="fertilizer-info">{{ pred_info|safe }}</div>
477
+ {% endif %}
478
+ </div>
479
+
480
+ <div class="right-container">
481
+ <h3 class="section-heading">Optimal Fertilizer Usage (kg/Acres)</h3>
482
+ <div class="gauge-container">
483
+ <div id="gauge"></div>
484
+ </div>
485
+ <script>
486
+ var optimalUsage = {{ optimal_usage }};
487
+ var gaugeData = [{
488
+ type: 'indicator',
489
+ mode: 'gauge+number',
490
+ value: optimalUsage,
491
+ gauge: {
492
+ axis: {
493
+ range: [0, 100],
494
+ tickwidth: 2,
495
+ tickcolor: "#1a5d3a"
496
+ },
497
+ bar: { color: "#198754" },
498
+ bgcolor: "white",
499
+ bordercolor: "#143d2e",
500
+ borderwidth: 2,
501
+ steps: [
502
+ { range: [0, 33], color: "#e8f5e9" },
503
+ { range: [33, 66], color: "#a5d6a7" },
504
+ { range: [66, 100], color: "#66bb6a" }
505
+ ],
506
+ threshold: {
507
+ line: { color: "#c62828", width: 4 },
508
+ thickness: 0.75,
509
+ value: 90
510
+ }
511
+ }
512
+ }];
513
+
514
+ var layout = {
515
+ width: 400,
516
+ height: 300,
517
+ margin: { t: 25, b: 25, l: 25, r: 25 },
518
+ paper_bgcolor: 'rgba(0,0,0,0)',
519
+ font: {
520
+ family: 'Outfit, sans-serif',
521
+ size: 14,
522
+ color: '#1f2937'
523
+ }
524
+ };
525
+
526
+ var config = {
527
+ responsive: true,
528
+ displayModeBar: false
529
+ };
530
+
531
+ Plotly.newPlot('gauge', gaugeData, layout, config);
532
+ </script>
533
+ </div>
534
+ </div>
535
+ {% endif %}
536
+ </div>
537
+ </body>
538
+
539
+ </html>