krushimitravit commited on
Commit
1784254
·
verified ·
1 Parent(s): 0d56be3

Upload 13 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ static/audio/pest_report_18_493185_73_933801.mp3 filter=lfs diff=lfs merge=lfs -text
37
+ static/audio/pest_report_18_501157_73_924214.mp3 filter=lfs diff=lfs merge=lfs -text
38
+ static/audio/pest_report_18.783684_73.494558.mp3 filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Base image
2
+ FROM python:3.9-slim
3
+
4
+ # Set the working directory
5
+ WORKDIR /app
6
+
7
+ # Copy application files
8
+ COPY . /app
9
+
10
+ # Ensure the uploads folder exists and is writable
11
+ # Ensure the uploads and audio folders exist and are writable
12
+ RUN mkdir -p static/uploads static/audio && chmod -R 777 static/uploads static/audio
13
+
14
+ # Install dependencies
15
+ RUN pip install --no-cache-dir -r requirements.txt
16
+
17
+ # Expose the port your app runs on
18
+ EXPOSE 7860
19
+
20
+ # Run the application with Gunicorn and increase the worker timeout to 30 seconds
21
+ CMD ["gunicorn", "-w", "4", "--timeout", "30", "-b", "0.0.0.0:7860", "app:app"]
app.py ADDED
@@ -0,0 +1,529 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import requests
3
+ import json
4
+ from flask import Flask, render_template, request, jsonify, Response, send_file
5
+ from google import genai
6
+ from gtts import gTTS
7
+ import os
8
+ from dotenv import load_dotenv
9
+ from datetime import datetime, timedelta
10
+
11
+ # Load .env file
12
+ load_dotenv()
13
+
14
+ app = Flask(__name__)
15
+ app.config['AUDIO_FOLDER'] = 'static/audio'
16
+ os.makedirs(app.config['AUDIO_FOLDER'], exist_ok=True)
17
+
18
+ def markdown_to_html(text):
19
+ """Convert markdown text to HTML for proper rendering."""
20
+ if not text:
21
+ return text
22
+ import markdown as md
23
+ return md.markdown(text, extensions=['nl2br'])
24
+
25
+ # IMPORTANT: Replace with your actual Gemini API key
26
+ api_key = os.getenv('GEMINI_API_KEY')
27
+ if not api_key:
28
+ api_key = os.getenv('GEMINI_API_KEY_1')
29
+ if not api_key:
30
+ api_key = os.getenv('GEMINI_API_KEY_2')
31
+
32
+ if not api_key:
33
+ raise ValueError("GEMINI_API_KEY is not set. Please add it to your .env file.")
34
+
35
+ print(f"Initializing Gemini client with API key: {api_key[:10]}...")
36
+ client = genai.Client(api_key=api_key)
37
+ def validate_coordinates(lat, lon):
38
+ """Validate and convert latitude and longitude to float."""
39
+ try:
40
+ return float(lat), float(lon)
41
+ except (TypeError, ValueError):
42
+ return None, None
43
+
44
+ @app.route('/')
45
+ def index():
46
+ return render_template('index.html')
47
+
48
+ @app.route('/get_weather_data', methods=['GET'])
49
+ def get_weather_data():
50
+ """
51
+ Fetch weather data using Open-Meteo's forecast endpoint.
52
+ """
53
+ lat = request.args.get('lat')
54
+ lon = request.args.get('lon')
55
+ lat, lon = validate_coordinates(lat, lon)
56
+ if lat is None or lon is None:
57
+ return jsonify({"error": "Invalid coordinates"}), 400
58
+
59
+ try:
60
+ forecast_url = "https://api.open-meteo.com/v1/forecast"
61
+ forecast_params = {
62
+ "latitude": lat,
63
+ "longitude": lon,
64
+ "current_weather": "true",
65
+ "daily": "temperature_2m_max,temperature_2m_min,precipitation_sum",
66
+ "hourly": "relative_humidity_2m,soil_moisture_3_to_9cm,cloudcover,windspeed_10m",
67
+ "timezone": "auto"
68
+ }
69
+ resp = requests.get(forecast_url, params=forecast_params)
70
+ resp.raise_for_status()
71
+ data = resp.json()
72
+
73
+ daily = data.get("daily", {})
74
+ hourly = data.get("hourly", {})
75
+ current = data.get("current_weather", {})
76
+
77
+ # Daily data
78
+ max_temp = daily.get("temperature_2m_max", [None])[0]
79
+ min_temp = daily.get("temperature_2m_min", [None])[0]
80
+ rain = daily.get("precipitation_sum", [None])[0]
81
+
82
+ # Hourly data (averages)
83
+ humidity_list = hourly.get("relative_humidity_2m", [])
84
+ soil_list = hourly.get("soil_moisture_3_to_9cm", [])
85
+ cloud_list = hourly.get("cloudcover", [])
86
+
87
+ avg_humidity = sum(humidity_list)/len(humidity_list) if humidity_list else None
88
+ avg_soil_moisture = sum(soil_list)/len(soil_list) if soil_list else None
89
+ avg_cloud_cover = sum(cloud_list)/len(cloud_list) if cloud_list else None
90
+
91
+ # Current weather
92
+ current_temp = current.get("temperature")
93
+ wind_speed = current.get("windspeed")
94
+
95
+ weather = {
96
+ "max_temp": max_temp, "min_temp": min_temp, "rainfall": rain,
97
+ "humidity": avg_humidity, "soil_moisture": avg_soil_moisture,
98
+ "current_temp": current_temp, "wind_speed": wind_speed, "cloud_cover": avg_cloud_cover
99
+ }
100
+ return jsonify(weather)
101
+ except Exception as e:
102
+ return jsonify({"error": str(e)}), 500
103
+
104
+ def get_historical_weather_summary(lat, lon, start_date_str, end_date_str):
105
+ """
106
+ Fetches historical weather data from Open-Meteo Archive for the specified period.
107
+ If period is in future, shifts to previous year.
108
+ Returns a text summary of monthly averages.
109
+ """
110
+ try:
111
+ if not start_date_str or not end_date_str:
112
+ return "Weather data unavailable (dates missing)."
113
+
114
+ start = datetime.strptime(start_date_str, '%Y-%m-%d')
115
+ end = datetime.strptime(end_date_str, '%Y-%m-%d')
116
+ today = datetime.now()
117
+
118
+ # Logic: If start date is in future, use last year's data as proxy
119
+ is_proxy = False
120
+ if start > today:
121
+ start = start.replace(year=start.year - 1)
122
+ end = end.replace(year=end.year - 1)
123
+ is_proxy = True
124
+
125
+ # Ensure we don't query future for the archive (e.g. if harvest is next month)
126
+ # If the *adjusted* end date is still after today (rare if we shifted year, but possible), clip it.
127
+ if end > today:
128
+ end = today
129
+
130
+ # Call Open-Meteo Archive
131
+ archive_url = "https://archive-api.open-meteo.com/v1/archive"
132
+ params = {
133
+ "latitude": lat,
134
+ "longitude": lon,
135
+ "start_date": start.strftime('%Y-%m-%d'),
136
+ "end_date": end.strftime('%Y-%m-%d'),
137
+ "daily": "temperature_2m_max,temperature_2m_min,precipitation_sum,relative_humidity_2m_mean",
138
+ "timezone": "auto"
139
+ }
140
+
141
+ resp = requests.get(archive_url, params=params)
142
+
143
+ if resp.status_code != 200:
144
+ print(f"DEBUG: API Failed {resp.status_code} - {resp.text}")
145
+ return f"Could not fetch weather data: {resp.status_code}", []
146
+
147
+ data = resp.json()
148
+ daily = data.get("daily", {})
149
+
150
+ dates = daily.get("time", [])
151
+
152
+ print(f"DEBUG: Retrieved {len(dates)} days.")
153
+ if dates:
154
+ print(f"DEBUG: Range {dates[0]} to {dates[-1]}")
155
+
156
+ if not dates:
157
+ return "No weather data available for this range.", []
158
+
159
+ summary_parts = []
160
+ structured_data = []
161
+ if is_proxy:
162
+ summary_parts.append("(Note: Using last year's weather as proxy for future dates)")
163
+
164
+ # Simple aggregation loop
165
+ current_month = None
166
+ temp_sum = 0
167
+ hum_sum = 0
168
+ rain_sum = 0
169
+ count = 0
170
+
171
+ # Extract lists
172
+ max_temps = daily.get("temperature_2m_max", [])
173
+ humidities = daily.get("relative_humidity_2m_mean", [])
174
+ rains = daily.get("precipitation_sum", [])
175
+
176
+
177
+ # Simple aggregation loop
178
+ current_month = None
179
+ temp_sum = 0
180
+ hum_sum = 0
181
+ rain_sum = 0
182
+ count = 0
183
+ month_days = []
184
+
185
+ for i, d_str in enumerate(dates):
186
+ date_obj = datetime.strptime(d_str, '%Y-%m-%d')
187
+ month_key = date_obj.strftime('%B %Y')
188
+
189
+ # Accumulate values (handle None)
190
+ t = max_temps[i] if max_temps[i] is not None else 0
191
+ h = humidities[i] if humidities[i] is not None else 0
192
+ r = rains[i] if rains[i] is not None else 0
193
+
194
+ if current_month is None: current_month = month_key
195
+
196
+ if month_key != current_month:
197
+ # Flush previous month
198
+ avg_t = temp_sum / count if count else 0
199
+ avg_h = hum_sum / count if count else 0
200
+ summary_parts.append(f"{current_month}: Avg Temp {avg_t:.1f}C, Rain {rain_sum:.1f}mm, Humidity {avg_h:.1f}%")
201
+
202
+ # Add to structured data
203
+ structured_data.append({
204
+ "month": current_month,
205
+ "avg_temp": round(avg_t, 1),
206
+ "rainfall": round(rain_sum, 1),
207
+ "humidity": round(avg_h, 1),
208
+ "days": month_days
209
+ })
210
+
211
+ # Reset
212
+ current_month = month_key
213
+ temp_sum = 0; hum_sum = 0; rain_sum = 0; count = 0
214
+ month_days = []
215
+
216
+ # Accumulate
217
+ temp_sum += t
218
+ hum_sum += h
219
+ rain_sum += r
220
+ count += 1
221
+
222
+ # Add to daily list
223
+ month_days.append({
224
+ "date": d_str,
225
+ "temp": t,
226
+ "humidity": h,
227
+ "rain": r
228
+ })
229
+
230
+ # Flush last month
231
+ if count > 0:
232
+ avg_t = temp_sum / count
233
+ avg_h = hum_sum / count
234
+ summary_parts.append(f"{current_month}: Avg Temp {avg_t:.1f}C, Rain {rain_sum:.1f}mm, Humidity {avg_h:.1f}%")
235
+ structured_data.append({
236
+ "month": current_month,
237
+ "avg_temp": round(avg_t, 1),
238
+ "rainfall": round(rain_sum, 1),
239
+ "humidity": round(avg_h, 1),
240
+ "days": month_days
241
+ })
242
+
243
+ return "\n".join(summary_parts), structured_data
244
+
245
+ except Exception as e:
246
+ print(f"Weather Fetch Error: {e}")
247
+ return "Weather data processing failed.", []
248
+
249
+ def calculate_season_dates(season_name):
250
+ """
251
+ Derives standard sowing and harvest dates based on the Indian agricultural season.
252
+ """
253
+ today = datetime.today()
254
+ current_year = today.year
255
+
256
+ # Defaults
257
+ sowing_date = today
258
+ harvest_date = today + timedelta(days=120)
259
+
260
+ try:
261
+ if season_name == "Kharif":
262
+ # June 15 to Oct 15
263
+ sowing_date = datetime(current_year, 6, 15)
264
+ harvest_date = datetime(current_year, 10, 15)
265
+ # If we are past Oct, maybe predicts for next year?
266
+ # For simplicity, assume current year context or next if late.
267
+ if today.month > 10:
268
+ sowing_date = datetime(current_year + 1, 6, 15)
269
+ harvest_date = datetime(current_year + 1, 10, 15)
270
+
271
+ elif season_name == "Rabi":
272
+ # Nov 1 to April 1 (Crosses year boundary)
273
+ if today.month > 4:
274
+ # Predicting for upcoming Rabi
275
+ sowing_date = datetime(current_year, 11, 1)
276
+ harvest_date = datetime(current_year + 1, 4, 1)
277
+ else:
278
+ # We are IN Rabi or just past
279
+ sowing_date = datetime(current_year - 1, 11, 1)
280
+ harvest_date = datetime(current_year, 4, 1)
281
+
282
+ elif season_name == "Zaid":
283
+ # March 1 to June 1
284
+ sowing_date = datetime(current_year, 3, 1)
285
+ harvest_date = datetime(current_year, 6, 1)
286
+ if today.month > 6:
287
+ sowing_date = datetime(current_year + 1, 3, 1)
288
+ harvest_date = datetime(current_year + 1, 6, 1)
289
+
290
+ elif season_name == "Annual":
291
+ # 1 Year cycle
292
+ sowing_date = today
293
+ harvest_date = today + timedelta(days=365)
294
+
295
+ except Exception as e:
296
+ print(f"Date Calc Error: {e}")
297
+
298
+ return sowing_date.strftime('%Y-%m-%d'), harvest_date.strftime('%Y-%m-%d')
299
+
300
+ def call_gemini_api(input_data, language):
301
+ """
302
+ Calls the Gemini API to get a pest outbreak report in a structured JSON format.
303
+ Implements fallback mechanism to try multiple models if primary fails.
304
+ """
305
+
306
+ # 1. Logic: Determine Dates from Season
307
+ lat = input_data.get('latitude')
308
+ lon = input_data.get('longitude')
309
+ season = input_data.get('season')
310
+
311
+ # Auto-calculate dates based on Season
312
+ if season:
313
+ sowing, harvest = calculate_season_dates(season)
314
+ else:
315
+ # Fallback to manual if ever used, or defaults
316
+ sowing = input_data.get('sowing_date', datetime.today().strftime('%Y-%m-%d'))
317
+ harvest = input_data.get('harvest_date', (datetime.today() + timedelta(days=120)).strftime('%Y-%m-%d'))
318
+
319
+ print(f"Analysis Period: {season} ({sowing} to {harvest})")
320
+
321
+ # 2. Fetch Historical/Season Weather Profile
322
+ weather_summary, weather_profile = get_historical_weather_summary(lat, lon, sowing, harvest)
323
+ print(f"Generated Weather Summary: {weather_summary[:100]}...")
324
+
325
+ prompt = f"""
326
+ You are an expert Agricultural Entomologist. Analyze the provided inputs and the MONTH-WISE WEATHER PROFILE to generate a precise Pest Outbreak Prediction.
327
+
328
+ INPUTS:
329
+ - Crop: {input_data.get('crop_type')}
330
+ - Soil: {input_data.get('soil_type')}
331
+ - Season: {season} ({sowing} to {harvest})
332
+ - Location: {input_data.get('derived_location', 'Unknown')}
333
+
334
+ WEATHER PROFILE (Month-by-Month):
335
+ {weather_summary}
336
+
337
+ CRITICAL INSTRUCTION:
338
+ 1. ANALYZE EACH MONTH SEPARATELY. Correlation: High Rainfall in Month 2 -> Risk of Fungal Diseases.
339
+ 2. MULTIPLE PESTS: If a month has multiple risky pests, create SEPARATE entries for each pest in the table. Do not limit to just one per month.
340
+ 3. LANGUAGE RULE: The JSON KEYS (e.g., "report_title", "pest_name") MUST REMAIN IN ENGLISH. Only translate the VALUES into '{language}'.
341
+ 4. ACCURACY: Only predict pests that genuinely thrive in the given weather conditions. If a month is low risk, it is okay to have no pests for that month.
342
+ 5. MONTH NAMING: The 'outbreak_months' field MUST contain the exact full English month names (e.g., "June", "July") to match the weather profile.
343
+
344
+ Your response MUST be a single, valid JSON object and nothing else. Do not wrap it in markdown backticks.
345
+
346
+ This is the required JSON structure:
347
+ {{
348
+ "report_title": "Pest Outbreak Dashboard Report",
349
+ "location_info": {{
350
+ "latitude": "{lat}",
351
+ "longitude": "{lon}",
352
+ "derived_location": "A human-readable location derived from the coordinates (e.g., 'Nagpur, India')."
353
+ }},
354
+ "agricultural_inputs_analysis": "A detailed bullet-point analysis of how the chosen Season and Weather Profile impacts this specific crop.",
355
+ "pest_prediction_table": [
356
+ {{
357
+ "pest_name": "Name of the predicted pest",
358
+ "outbreak_months": "Predicted month(s) for the outbreak",
359
+ "severity": "Predicted severity level (e.g., Low, Medium, High)",
360
+ "impacting_stage": "The specific crop stage affected (e.g., Flowering, Vegetative)",
361
+ "potential_damage": "Short description of the damage caused (e.g., 'Causes dead hearts', 'Sucks sap leading to yellowing').",
362
+ "precautionary_measures": "A short description of key precautionary measures."
363
+ }}
364
+ ],
365
+ "pest_avoidance_practices": [
366
+ "A detailed, specific pest avoidance practice based on the inputs.",
367
+ "Another specific recommendation.",
368
+ "Provide 10-12 detailed bullet points."
369
+ ],
370
+ "agricultural_best_practices": [
371
+ "A specific agricultural best practice based on the inputs.",
372
+ "Another specific recommendation related to crop management."
373
+ ],
374
+ "predicted_pest_damage_info": "Detailed bullet points (markdown format using -) describing the potential damage the predicted pests could cause."
375
+ }}
376
+
377
+ Use the following data for your analysis:
378
+ - Location: {input_data.get('derived_location', 'Unknown')} (Lat: {input_data.get('latitude')}, Lon: {input_data.get('longitude')})
379
+ - Crop: {input_data.get('crop_type')}
380
+ - Sowing Date: {input_data.get('sowing_date')}
381
+ - Harvest Date: {input_data.get('harvest_date')}
382
+ - Current Growth Stage: {input_data.get('growth_stage')}
383
+ - Irrigation Frequency: {input_data.get('irrigation_freq')}
384
+ - Irrigation Method: {input_data.get('irrigation_method')}
385
+ - Soil Type: {input_data.get('soil_type')}
386
+
387
+ --- SEASONAL WEATHER PROFILE (Aggregated Monthly Data) ---
388
+ {weather_summary}
389
+ ----------------------------------------------------------
390
+ """
391
+
392
+ models_to_try = [
393
+ "gemini-3.0-flash", # Requested
394
+ "gemini-2.5-flash", # Requested
395
+ "gemini-2.5-flash-lite", # Requested
396
+ "gemma-3-27b-it", # Requested
397
+ "gemma-3-12b-it", # Requested
398
+ "gemma-3-4b-it", # Requested
399
+ "gemma-3-1b-it", # Requested
400
+ "gemini-2.0-flash-exp", # Likely intended "modern" fallback
401
+ ]
402
+
403
+ report_data = {} # Default
404
+
405
+ for model_name in models_to_try:
406
+ try:
407
+ print(f"DEBUG: Attempting model {model_name}...")
408
+ response = client.models.generate_content(
409
+ model=model_name,
410
+ contents=prompt
411
+ )
412
+
413
+ # Robust JSON extraction
414
+ raw_text = response.text
415
+ # print(f"DEBUG: Raw response: {raw_text[:100]}...") # Limit log size
416
+
417
+ start_idx = raw_text.find('{')
418
+ end_idx = raw_text.rfind('}') + 1
419
+
420
+ if start_idx != -1 and end_idx != 0:
421
+ json_text = raw_text[start_idx:end_idx]
422
+ report_data = json.loads(json_text)
423
+ print(f"DEBUG: Successfully parsed JSON from {model_name}")
424
+ break # Success
425
+ else:
426
+ print(f"DEBUG: No JSON found in response from {model_name}")
427
+ raise ValueError("No JSON definition found")
428
+
429
+ except json.JSONDecodeError as e:
430
+ print(f"CRITICAL: JSON parsing error with {model_name}: {e}")
431
+ continue
432
+ except Exception as e:
433
+ print(f"CRITICAL: Error calling {model_name}: {e}")
434
+ continue
435
+
436
+ if not report_data:
437
+ print("DEBUG: All models failed to generate valid report.")
438
+ return {"error": "Failed to generate a valid report from the AI model. All fallback models failed. Please try again later."}, []
439
+
440
+ return report_data, weather_profile
441
+
442
+ @app.route('/predict', methods=['POST'])
443
+ def predict():
444
+ print("----- PREDICT ROUTE HIT -----")
445
+ form_data = request.form.to_dict()
446
+ print(f"Form Data Received: {form_data}")
447
+ language = form_data.get("language", "English")
448
+
449
+ report_data, weather_profile = call_gemini_api(form_data, language)
450
+
451
+ # ... (Error handling remains similar but simplified for template)
452
+ if "error" in report_data:
453
+ return render_template('results.html', report_data={"report_title": "Error", "predicted_pest_damage_info": report_data['error']}, location={}, current_date=datetime.now().strftime("%B %d, %Y"), audio_url=None, language_code="en", weather_profile=[])
454
+
455
+ # Build the HTML report dynamically from the JSON data
456
+ location = report_data.get('location_info', {})
457
+
458
+ # Convert markdown to HTML in text fields
459
+ if 'agricultural_inputs_analysis' in report_data:
460
+ report_data['agricultural_inputs_analysis'] = markdown_to_html(report_data['agricultural_inputs_analysis'])
461
+ if 'predicted_pest_damage_info' in report_data:
462
+ report_data['predicted_pest_damage_info'] = markdown_to_html(report_data['predicted_pest_damage_info'])
463
+ if 'pest_prediction_table' in report_data:
464
+ for pest in report_data['pest_prediction_table']:
465
+ if 'precautionary_measures' in pest:
466
+ pest['precautionary_measures'] = markdown_to_html(pest['precautionary_measures'])
467
+
468
+ # Generate summary for voice (short summary)
469
+ pest_table = report_data.get('pest_prediction_table', [])
470
+ summary = f"Pest Outbreak Report for {location.get('derived_location', 'your location')}. "
471
+ summary += (report_data.get('agricultural_inputs_analysis', '')[:200] + "... ")
472
+ if pest_table:
473
+ summary += f"Predicted pests: " + ', '.join([p.get('pest_name', '') for p in pest_table]) + ". "
474
+ summary += f"Severity: " + ', '.join([p.get('severity', '') for p in pest_table]) + ". "
475
+ summary += report_data.get('predicted_pest_damage_info', '')[:200]
476
+
477
+ # Generate audio file
478
+ lang_mapping = {
479
+ "English": "en", "Hindi": "hi", "Bengali": "bn", "Telugu": "te",
480
+ "Marathi": "mr", "Tamil": "ta", "Gujarati": "gu", "Urdu": "ur",
481
+ "Kannada": "kn", "Odia": "or", "Malayalam": "ml"
482
+ }
483
+ gtts_lang = lang_mapping.get(language, 'en')
484
+
485
+ audio_url = None
486
+ try:
487
+ tts = gTTS(summary, lang=gtts_lang)
488
+ # Sanitize filename
489
+ safe_lat = str(location.get('latitude', '0')).replace('.', '_')
490
+ safe_lon = str(location.get('longitude', '0')).replace('.', '_')
491
+ audio_filename = f"pest_report_{safe_lat}_{safe_lon}.mp3"
492
+
493
+ # Ensure directory exists
494
+ os.makedirs(app.config['AUDIO_FOLDER'], exist_ok=True)
495
+
496
+ audio_path = os.path.join(app.config['AUDIO_FOLDER'], audio_filename)
497
+ tts.save(audio_path)
498
+ audio_url = f"/static/audio/{audio_filename}"
499
+ except Exception as e:
500
+ print(f"Error generating audio: {e}")
501
+
502
+ return render_template(
503
+ 'results.html',
504
+ report_data=report_data,
505
+ location=location,
506
+ current_date=datetime.now().strftime("%B %d, %Y"),
507
+ audio_url=audio_url,
508
+ language_code=gtts_lang,
509
+ weather_profile=weather_profile
510
+ )
511
+
512
+ @app.route('/get_timeline_weather', methods=['GET'])
513
+ def get_timeline_weather():
514
+ """Returns the seasonal weather profile for the frontend timeline."""
515
+ lat = request.args.get('lat')
516
+ lon = request.args.get('lon')
517
+ start = request.args.get('start_date')
518
+ end = request.args.get('end_date')
519
+
520
+ if not all([lat, lon, start, end]):
521
+ return jsonify({"error": "Missing parameters"}), 400
522
+
523
+ _, profile = get_historical_weather_summary(lat, lon, start, end)
524
+ return jsonify(profile)
525
+
526
+
527
+
528
+ if __name__ == '__main__':
529
+ app.run(debug=True, port=5001)
check_models.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import google.generativeai as genai
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ api_key = os.getenv("GEMINI_API_KEY")
8
+ if not api_key:
9
+ api_key = os.getenv("GEMINI_API_KEY_1")
10
+ if not api_key:
11
+ api_key = os.getenv("GEMINI_API_KEY_2")
12
+
13
+ if not api_key:
14
+ print("Error: GEMINI_API_KEY (or variants) not found in environment.")
15
+ else:
16
+ # Use the google.genai client similar to app.py
17
+ # But for listing models, the google.generativeai module is often easier/standard
18
+ # Let's try to stick to what works for listing.
19
+ genai.configure(api_key=api_key)
20
+ print(f"Checking models for key ending in ...{api_key[-4:]}")
21
+
22
+ try:
23
+ print("\nAvailable Models:")
24
+ for m in genai.list_models():
25
+ if 'generateContent' in m.supported_generation_methods:
26
+ print(f"- {m.name}")
27
+ except Exception as e:
28
+ print(f"Error listing models: {e}")
debug_weather.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ from datetime import datetime
3
+ import json
4
+
5
+ def get_historical_weather_summary_fixed(lat, lon, start_date_str, end_date_str):
6
+ print(f"\n--- Checking {start_date_str} to {end_date_str} ---")
7
+ try:
8
+ start = datetime.strptime(start_date_str, '%Y-%m-%d')
9
+ end = datetime.strptime(end_date_str, '%Y-%m-%d')
10
+ today = datetime.now()
11
+
12
+ if start > today:
13
+ start = start.replace(year=start.year - 1)
14
+ end = end.replace(year=end.year - 1)
15
+
16
+ if end > today:
17
+ end = today
18
+
19
+ archive_url = "https://archive-api.open-meteo.com/v1/archive"
20
+ params = {
21
+ "latitude": lat,
22
+ "longitude": lon,
23
+ "start_date": start.strftime('%Y-%m-%d'),
24
+ "end_date": end.strftime('%Y-%m-%d'),
25
+ "daily": "temperature_2m_max,temperature_2m_min,precipitation_sum,relative_humidity_2m_mean",
26
+ "timezone": "auto"
27
+ }
28
+
29
+ resp = requests.get(archive_url, params=params)
30
+ data = resp.json()
31
+ daily = data.get("daily", {})
32
+ dates = daily.get("time", [])
33
+
34
+ print(f"API Returned {len(dates)} days.")
35
+
36
+ summary_parts = []
37
+ structured_data = []
38
+
39
+ current_month = None
40
+ temp_sum = 0
41
+ hum_sum = 0
42
+ rain_sum = 0
43
+ count = 0
44
+
45
+ max_temps = daily.get("temperature_2m_max", [])
46
+ humidities = daily.get("relative_humidity_2m_mean", [])
47
+ rains = daily.get("precipitation_sum", [])
48
+
49
+ for i, d_str in enumerate(dates):
50
+ date_obj = datetime.strptime(d_str, '%Y-%m-%d')
51
+ month_key = date_obj.strftime('%B %Y')
52
+
53
+ if current_month is None: current_month = month_key
54
+
55
+ if month_key != current_month:
56
+ # Flush previous month
57
+ avg_t = temp_sum / count if count else 0
58
+ avg_h = hum_sum / count if count else 0
59
+
60
+ # FIXED LOGIC
61
+ structured_data.append({
62
+ "month": current_month,
63
+ "avg_temp": round(avg_t, 1),
64
+ "rainfall": round(rain_sum, 1),
65
+ "humidity": round(avg_h, 1)
66
+ })
67
+
68
+ # Reset
69
+ current_month = month_key
70
+ temp_sum = 0; hum_sum = 0; rain_sum = 0; count = 0
71
+
72
+ # Accumulate
73
+ t = max_temps[i] if max_temps[i] is not None else 0
74
+ h = humidities[i] if humidities[i] is not None else 0
75
+ r = rains[i] if rains[i] is not None else 0
76
+
77
+ temp_sum += t
78
+ hum_sum += h
79
+ rain_sum += r
80
+ count += 1
81
+
82
+ # Flush last month
83
+ if count > 0:
84
+ avg_t = temp_sum / count
85
+ avg_h = hum_sum / count
86
+ structured_data.append({
87
+ "month": current_month,
88
+ "avg_temp": round(avg_t, 1),
89
+ "rainfall": round(rain_sum, 1),
90
+ "humidity": round(avg_h, 1)
91
+ })
92
+
93
+ print(f"Structured Data Length: {len(structured_data)}")
94
+ for item in structured_data:
95
+ print(f" {item['month']}: T={item['avg_temp']}, R={item['rainfall']}")
96
+
97
+ except Exception as e:
98
+ print(f"Exception: {e}")
99
+
100
+ # Test with a 6 month range
101
+ get_historical_weather_summary_fixed(20.5937, 78.9629, "2026-06-01", "2026-11-10")
image_summarizer.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import os
3
+ from openai import OpenAI
4
+
5
+ # Initialize NVIDIA Client
6
+ client = OpenAI(
7
+ base_url="https://integrate.api.nvidia.com/v1",
8
+ api_key="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"
9
+ )
10
+
11
+ # Model configurations
12
+ PRIMARY_MODEL = "meta/llama-3.2-90b-vision-instruct"
13
+ FALLBACK_MODEL = "meta/llama-3.1-70b-instruct" # Text-only fallback model
14
+ IMAGE_PATH = "image.png"
15
+
16
+
17
+ def encode_image(image_path):
18
+ """Encode image to base64 string."""
19
+ with open(image_path, "rb") as image_file:
20
+ return base64.b64encode(image_file.read()).decode('utf-8')
21
+
22
+
23
+ def summarize_with_vision_model(base64_image):
24
+ """
25
+ Attempt to summarize image using vision model.
26
+
27
+ Args:
28
+ base64_image: Base64 encoded image string
29
+
30
+ Returns:
31
+ str: Summary text or None if failed
32
+ """
33
+ try:
34
+ print(f"🔍 Attempting with primary vision model: {PRIMARY_MODEL}...")
35
+
36
+ completion = client.chat.completions.create(
37
+ model=PRIMARY_MODEL,
38
+ messages=[
39
+ {
40
+ "role": "user",
41
+ "content": [
42
+ {"type": "text", "text": "Please summarize what you see in this image."},
43
+ {
44
+ "type": "image_url",
45
+ "image_url": {
46
+ "url": f"data:image/png;base64,{base64_image}"
47
+ }
48
+ }
49
+ ]
50
+ }
51
+ ],
52
+ max_tokens=500,
53
+ temperature=0.2,
54
+ stream=True
55
+ )
56
+
57
+ print("\n✅ Image Summary (Vision Model):\n" + "-" * 50)
58
+ summary = ""
59
+ for chunk in completion:
60
+ content = chunk.choices[0].delta.content
61
+ if content is not None:
62
+ print(content, end="", flush=True)
63
+ summary += content
64
+ print("\n" + "-" * 50)
65
+
66
+ return summary
67
+
68
+ except Exception as e:
69
+ print(f"\n⚠️ Vision model failed: {e}")
70
+ return None
71
+
72
+
73
+ def summarize_with_text_fallback():
74
+ """
75
+ Fallback method using text-only LLM.
76
+ Provides a generic response when vision model fails.
77
+
78
+ Returns:
79
+ str: Fallback response
80
+ """
81
+ try:
82
+ print(f"\n🔄 Falling back to text model: {FALLBACK_MODEL}...")
83
+
84
+ # Create a prompt that acknowledges the limitation
85
+ prompt = """I attempted to analyze an image but the vision model is unavailable.
86
+ Please provide a helpful response about what types of information can typically be extracted from images,
87
+ and suggest alternative approaches for image analysis."""
88
+
89
+ completion = client.chat.completions.create(
90
+ model=FALLBACK_MODEL,
91
+ messages=[
92
+ {
93
+ "role": "user",
94
+ "content": prompt
95
+ }
96
+ ],
97
+ max_tokens=500,
98
+ temperature=0.2,
99
+ stream=True
100
+ )
101
+
102
+ print("\n💡 Fallback Response (Text Model):\n" + "-" * 50)
103
+ response = ""
104
+ for chunk in completion:
105
+ content = chunk.choices[0].delta.content
106
+ if content is not None:
107
+ print(content, end="", flush=True)
108
+ response += content
109
+ print("\n" + "-" * 50)
110
+
111
+ return response
112
+
113
+ except Exception as e:
114
+ print(f"\n❌ Fallback model also failed: {e}")
115
+ return None
116
+
117
+
118
+ def summarize_image():
119
+ """
120
+ Main function to summarize an image with fallback support.
121
+
122
+ Attempts to use vision model first, falls back to text model if needed.
123
+ """
124
+ # Check if image exists
125
+ if not os.path.exists(IMAGE_PATH):
126
+ print(f"❌ Error: {IMAGE_PATH} not found.")
127
+ print(f"📁 Current directory: {os.getcwd()}")
128
+ print(f"📋 Files in current directory: {os.listdir('.')}")
129
+ return
130
+
131
+ print(f"📸 Processing {IMAGE_PATH}...")
132
+ print(f"📏 File size: {os.path.getsize(IMAGE_PATH)} bytes\n")
133
+
134
+ # Encode the image
135
+ try:
136
+ base64_image = encode_image(IMAGE_PATH)
137
+ except Exception as e:
138
+ print(f"❌ Error encoding image: {e}")
139
+ return
140
+
141
+ # Try vision model first
142
+ result = summarize_with_vision_model(base64_image)
143
+
144
+ # If vision model failed, use fallback
145
+ if result is None:
146
+ print("\n🔄 Primary model failed, attempting fallback...")
147
+ result = summarize_with_text_fallback()
148
+
149
+ # Final status
150
+ if result is None:
151
+ print("\n❌ All methods failed. Please check:")
152
+ print(" 1. API key validity")
153
+ print(" 2. Network connection")
154
+ print(" 3. NVIDIA API service status")
155
+ else:
156
+ print("\n✅ Image processing completed successfully!")
157
+
158
+
159
+ if __name__ == "__main__":
160
+ summarize_image()
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ gunicorn
2
+ requests
3
+ gtts
4
+ flask
5
+ google-genai
6
+ markdown
7
+ python-dotenv
static/audio/dummy.mp3 ADDED
@@ -0,0 +1 @@
 
 
1
+ xyz
static/audio/pest_report_18.783684_73.494558.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3ae4cbe6eab38abe416fe31c2186f4add70d7dac54b70536df98b3f4146a95dd
3
+ size 500352
static/audio/pest_report_18_493185_73_933801.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:952750732b405298f46fffcdcc8911830246719a3edc15cba9e0f6557ea2814f
3
+ size 597504
static/audio/pest_report_18_501157_73_924214.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0dc5372fd0617ff21eea435c497f3f29b1dca0b1ebfcd2aecb77cf0c10796724
3
+ size 958464
templates/index.html ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Pest Outbreak Prediction</title>
8
+ <!-- Fonts -->
9
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
10
+ <!-- Bootstrap 5 -->
11
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
12
+ <!-- Icons -->
13
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
14
+ <!-- Leaflet -->
15
+ <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
16
+ <link rel="stylesheet" href="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.css" />
17
+
18
+ <style>
19
+ :root {
20
+ --primary-green: #1a5d3a;
21
+ --accent-green: #198754;
22
+ --light-bg: #f3f4f6;
23
+ --glass-white: rgba(255, 255, 255, 0.95);
24
+ }
25
+
26
+ body {
27
+ font-family: 'Outfit', sans-serif;
28
+ background-color: var(--light-bg);
29
+ color: #1f2937;
30
+ padding-bottom: 3rem;
31
+ }
32
+
33
+ /* --- Header --- */
34
+ .header-section {
35
+ background-color: var(--primary-green);
36
+ color: white;
37
+ padding: 3rem 1rem 6rem;
38
+ text-align: center;
39
+ border-bottom-left-radius: 40px;
40
+ border-bottom-right-radius: 40px;
41
+ margin-bottom: -4rem;
42
+ }
43
+
44
+ .main-container {
45
+ max-width: 900px;
46
+ /* Reduced width for focus */
47
+ margin: 0 auto;
48
+ padding: 0 1rem;
49
+ }
50
+
51
+ /* --- Focused Card --- */
52
+ .dashboard-card {
53
+ background: white;
54
+ border-radius: 20px;
55
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08);
56
+ padding: 2.5rem;
57
+ border: 1px solid rgba(0, 0, 0, 0.04);
58
+ position: relative;
59
+ }
60
+
61
+ .section-label {
62
+ font-size: 0.75rem;
63
+ text-transform: uppercase;
64
+ letter-spacing: 1px;
65
+ font-weight: 700;
66
+ color: var(--accent-green);
67
+ margin-bottom: 1rem;
68
+ display: block;
69
+ border-bottom: 2px solid #f3f4f6;
70
+ padding-bottom: 0.5rem;
71
+ }
72
+
73
+ /* --- Map --- */
74
+ .map-wrapper {
75
+ border-radius: 12px;
76
+ overflow: hidden;
77
+ height: 250px;
78
+ margin-bottom: 1.5rem;
79
+ box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.1);
80
+ position: relative;
81
+ }
82
+
83
+ #map {
84
+ height: 100%;
85
+ width: 100%;
86
+ }
87
+
88
+ /* --- Inputs --- */
89
+ .form-label {
90
+ font-size: 0.9rem;
91
+ font-weight: 600;
92
+ color: #4b5563;
93
+ margin-bottom: 0.4rem;
94
+ }
95
+
96
+ .form-select,
97
+ .form-control {
98
+ border-radius: 10px;
99
+ padding: 0.75rem 1rem;
100
+ border: 1px solid #e5e7eb;
101
+ font-size: 1rem;
102
+ transition: 0.2s;
103
+ background-color: #f9fafb;
104
+ }
105
+
106
+ .form-select:focus,
107
+ .form-control:focus {
108
+ background-color: white;
109
+ border-color: var(--accent-green);
110
+ box-shadow: 0 0 0 4px rgba(25, 135, 84, 0.1);
111
+ }
112
+
113
+ .btn-predict {
114
+ background: linear-gradient(135deg, #1a5d3a 0%, #15803d 100%);
115
+ color: white;
116
+ font-size: 1.2rem;
117
+ font-weight: 700;
118
+ padding: 1rem 3rem;
119
+ border-radius: 50px;
120
+ border: none;
121
+ width: 100%;
122
+ margin-top: 2rem;
123
+ box-shadow: 0 10px 25px rgba(26, 93, 58, 0.25);
124
+ transition: 0.3s;
125
+ }
126
+
127
+ .btn-predict:hover {
128
+ transform: translateY(-3px);
129
+ box-shadow: 0 15px 35px rgba(26, 93, 58, 0.35);
130
+ }
131
+
132
+ /* Loaders */
133
+ #loaderOverlay {
134
+ position: fixed;
135
+ inset: 0;
136
+ background: rgba(255, 255, 255, 0.92);
137
+ backdrop-filter: blur(5px);
138
+ z-index: 9999;
139
+ display: none;
140
+ flex-direction: column;
141
+ justify-content: center;
142
+ align-items: center;
143
+ }
144
+ </style>
145
+ </head>
146
+
147
+ <body>
148
+
149
+ <div class="header-section">
150
+ <h1 class="fw-bold display-5 mb-2">Pest Outbreak Prediction</h1>
151
+ <p class="opacity-75 fs-5">AI-Powered Crop Intelligence</p>
152
+ </div>
153
+
154
+ <form id="farmForm" action="/predict" method="post" class="main-container">
155
+
156
+ <div class="dashboard-card">
157
+
158
+ <!-- 1. Location -->
159
+ <span class="section-label">01. Field Location</span>
160
+ <div class="row g-3 mb-4">
161
+ <div class="col-lg-8">
162
+ <div class="map-wrapper">
163
+ <div id="map"></div>
164
+ </div>
165
+ </div>
166
+ <div class="col-lg-4 d-flex flex-column justify-content-center">
167
+ <button type="button" id="fetchCurrentLocationBtn"
168
+ class="btn btn-outline-success fw-bold mb-3 w-100">
169
+ <i class="bi bi-crosshair me-2"></i> Use GPS Location
170
+ </button>
171
+ <div class="form-floating mb-2">
172
+ <input type="text" class="form-control" id="manual_lat" placeholder="Lat" readonly>
173
+ <label>Latitude</label>
174
+ </div>
175
+ <div class="form-floating">
176
+ <input type="text" class="form-control" id="manual_lon" placeholder="Lon" readonly>
177
+ <label>Longitude</label>
178
+ </div>
179
+ </div>
180
+
181
+ <!-- Hidden inputs for form submission -->
182
+ <input type="hidden" id="latitude" name="latitude">
183
+ <input type="hidden" id="longitude" name="longitude">
184
+ <!-- Weather hidden fields (populated dynamically if needed, or backend handles it) -->
185
+ <input type="hidden" id="max_temp_hidden" name="max_temp">
186
+ <input type="hidden" id="min_temp_hidden" name="min_temp">
187
+ <input type="hidden" id="current_temp_hidden" name="current_temp">
188
+ <input type="hidden" id="humidity_hidden" name="humidity">
189
+ <input type="hidden" id="rainfall_hidden" name="rain">
190
+ <input type="hidden" id="soil_moisture_hidden" name="soil_moisture">
191
+ <input type="hidden" id="wind_speed_hidden" name="wind_speed">
192
+ <input type="hidden" id="cloud_cover_hidden" name="cloud_cover">
193
+ </div>
194
+
195
+ <!-- 2. Crop Details -->
196
+ <span class="section-label">02. Crop Profile</span>
197
+ <div class="row g-3 mb-4">
198
+ <div class="col-md-6">
199
+ <label class="form-label">Crop Type</label>
200
+ <select class="form-select" id="crop_type" name="crop_type" required>
201
+ <option value="" selected disabled>Select Crop...</option>
202
+ <option value="Rice (Paddy)">Rice (Paddy)</option>
203
+ <option value="Wheat">Wheat</option>
204
+ <option value="Maize">Maize (Corn)</option>
205
+ <option value="Cotton">Cotton</option>
206
+ <option value="Sugarcane">Sugarcane</option>
207
+ <option value="Soybean">Soybean</option>
208
+ <option value="Tomato">Tomato</option>
209
+ <option value="Potato">Potato</option>
210
+ <option value="Onion">Onion</option>
211
+ <option value="Tea">Tea</option>
212
+ <option value="Coffee">Coffee</option>
213
+ </select>
214
+ </div>
215
+ <div class="col-md-6">
216
+ <label class="form-label">Soil Type</label>
217
+ <select class="form-select" id="soil_type" name="soil_type" required>
218
+ <option value="Black Soil">Black Soil</option>
219
+ <option value="Red Soil">Red Soil</option>
220
+ <option value="Loamy Soil">Loamy Soil</option>
221
+ <option value="Sandy Soil">Sandy Soil</option>
222
+ <option value="Clay Soil">Clay Soil</option>
223
+ </select>
224
+ </div>
225
+ </div>
226
+
227
+ <!-- 3. Smart Season Selection -->
228
+ <span class="section-label">03. Cropping Season</span>
229
+ <div class="row g-3 mb-4">
230
+ <div class="col-md-6">
231
+ <label class="form-label">Select Season</label>
232
+ <div class="input-group">
233
+ <span class="input-group-text bg-light"><i class="bi bi-calendar-range"></i></span>
234
+ <select class="form-select" id="season" name="season" required>
235
+ <option value="Kharif" selected>Kharif (Monsoon | June - Oct)</option>
236
+ <option value="Rabi">Rabi (Winter | Nov - April)</option>
237
+ <option value="Zaid">Zaid (Summer | March - June)</option>
238
+ <option value="Annual">Annual / Perennial</option>
239
+ </select>
240
+ </div>
241
+ </div>
242
+ <div class="col-md-6">
243
+ <label class="form-label">Report Language</label>
244
+ <select class="form-select" id="language" name="language">
245
+ <option value="English">English</option>
246
+ <option value="Hindi">Hindi</option>
247
+ <option value="Marathi">Marathi</option>
248
+ <option value="Bengali">Bengali</option>
249
+ <option value="Telugu">Telugu</option>
250
+ <option value="Tamil">Tamil</option>
251
+ <option value="Gujarati">Gujarati</option>
252
+ <option value="Kannada">Kannada</option>
253
+ </select>
254
+ </div>
255
+ </div>
256
+
257
+ <button type="submit" class="btn-predict">
258
+ ANALYZE CROP RISK <i class="bi bi-stars ms-2"></i>
259
+ </button>
260
+
261
+ <div class="text-center mt-3 text-muted small">
262
+ Powered by Gemini 2.0 Flash • Open-Meteo Historical Data
263
+ </div>
264
+
265
+ </div>
266
+ </form>
267
+
268
+ <!-- Loader -->
269
+ <div id="loaderOverlay">
270
+ <div class="spinner-border text-success mb-3" style="width: 3rem; height: 3rem;"></div>
271
+ <h4 class="fw-bold text-success">Analyzing Field Data...</h4>
272
+ <p class="text-muted">Fetching historical climate patterns & predicting risks</p>
273
+ </div>
274
+
275
+ <!-- Scripts -->
276
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
277
+ <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
278
+ <script src="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.js"></script>
279
+ <script>
280
+ // --- Map Init ---
281
+ var map = L.map('map').setView([20.5937, 78.9629], 5);
282
+ L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
283
+ attribution: 'Tiles © Esri'
284
+ }).addTo(map);
285
+
286
+ var marker;
287
+
288
+ function setMarker(latlng) {
289
+ if (marker) marker.setLatLng(latlng);
290
+ else marker = L.marker(latlng).addTo(map);
291
+
292
+ const lat = latlng.lat.toFixed(6);
293
+ const lng = latlng.lng.toFixed(6);
294
+
295
+ // Set hidden and visible inputs
296
+ document.getElementById('latitude').value = lat;
297
+ document.getElementById('longitude').value = lng;
298
+ document.getElementById('manual_lat').value = lat;
299
+ document.getElementById('manual_lon').value = lng;
300
+ }
301
+
302
+ map.on('click', function (e) { setMarker(e.latlng); });
303
+
304
+ // --- Geolocation ---
305
+ document.getElementById('fetchCurrentLocationBtn').addEventListener('click', () => {
306
+ if (navigator.geolocation) {
307
+ navigator.geolocation.getCurrentPosition(position => {
308
+ const latlng = {
309
+ lat: position.coords.latitude,
310
+ lng: position.coords.longitude
311
+ };
312
+ map.setView(latlng, 12);
313
+ setMarker(latlng);
314
+ }, () => {
315
+ alert("Location access denied or unavailable.");
316
+ });
317
+ }
318
+ });
319
+
320
+ // --- Form Submit Animation ---
321
+ document.getElementById('farmForm').addEventListener('submit', function (e) {
322
+ const lat = document.getElementById('latitude').value;
323
+ if (!lat) {
324
+ e.preventDefault();
325
+ alert("Please pin a location on the map first!");
326
+ return;
327
+ }
328
+ document.getElementById('loaderOverlay').style.display = 'flex';
329
+ });
330
+
331
+ // --- Init Default Date (Optional) ---
332
+ // Sets today as default sowing date for convenience
333
+ document.getElementById('sowing_date').valueAsDate = new Date();
334
+ </script>
335
+ </body>
336
+
337
+ </html>
templates/results.html ADDED
@@ -0,0 +1,675 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="{{ language_code }}">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Pest Prediction Intelligence</title>
8
+ <!-- Fonts -->
9
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap"
10
+ rel="stylesheet">
11
+ <!-- Bootstrap 5 -->
12
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
13
+ <!-- Icons -->
14
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
15
+
16
+ <style>
17
+ /* --- PROFESSIONAL UI SYSTEM --- */
18
+ :root {
19
+ --primary: #166534;
20
+ --surface: #ffffff;
21
+ --surface-subtle: #f8fafc;
22
+ --border-light: #e2e8f0;
23
+ --text-main: #1e293b;
24
+ --text-muted: #64748b;
25
+ --radius-lg: 24px;
26
+ --radius-md: 16px;
27
+ --shadow-soft: 0 20px 40px -10px rgba(0, 0, 0, 0.1);
28
+ --shadow-crisp: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
29
+ }
30
+
31
+ body {
32
+ font-family: 'Outfit', sans-serif;
33
+ background-color: var(--surface-subtle);
34
+ color: var(--text-main);
35
+ padding-bottom: 5rem;
36
+ overflow-x: hidden;
37
+ }
38
+
39
+ /* Hero */
40
+ .hero-banner {
41
+ background: linear-gradient(135deg, #166534 0%, #14532d 100%);
42
+ color: white;
43
+ padding: 3rem 1rem 6rem;
44
+ position: relative;
45
+ margin-bottom: -4rem;
46
+ }
47
+
48
+ .report-meta-badge {
49
+ background: rgba(255, 255, 255, 0.15);
50
+ backdrop-filter: blur(5px);
51
+ padding: 0.5rem 1rem;
52
+ border-radius: 50px;
53
+ font-size: 0.9rem;
54
+ display: inline-flex;
55
+ align-items: center;
56
+ gap: 0.5rem;
57
+ border: 1px solid rgba(255, 255, 255, 0.1);
58
+ }
59
+
60
+ /* Container */
61
+ .content-wrapper {
62
+ max-width: 1100px;
63
+ margin: 0 auto;
64
+ position: relative;
65
+ z-index: 20;
66
+ padding: 0 1.5rem;
67
+ }
68
+
69
+ /* 1. AUDIO STRIP */
70
+ .audio-sketch-box {
71
+ background: var(--surface);
72
+ border-radius: var(--radius-md);
73
+ padding: 1rem 1.5rem;
74
+ margin-bottom: 2rem;
75
+ box-shadow: var(--shadow-crisp);
76
+ border: 1px solid var(--border-light);
77
+ display: flex;
78
+ align-items: center;
79
+ justify-content: space-between;
80
+ }
81
+
82
+ .audio-icon-circle {
83
+ width: 48px;
84
+ height: 48px;
85
+ background: #f0fdf4;
86
+ color: var(--primary);
87
+ border-radius: 50%;
88
+ display: grid;
89
+ place-items: center;
90
+ font-size: 1.25rem;
91
+ }
92
+
93
+ /* 2. CALENDAR FLIPBOOK */
94
+ .flipbook-wrapper {
95
+ position: relative;
96
+ max-width: 1000px;
97
+ margin: 0 auto;
98
+ perspective: 2000px;
99
+ display: flex;
100
+ align-items: center;
101
+ gap: 1.5rem;
102
+ }
103
+
104
+ .flipbook-stage {
105
+ width: 100%;
106
+ height: 640px;
107
+ /* Fixed Optimal Height */
108
+ position: relative;
109
+ }
110
+
111
+ .flip-card {
112
+ position: absolute;
113
+ top: 0;
114
+ left: 0;
115
+ width: 100%;
116
+ height: 100%;
117
+ background: var(--surface);
118
+ border-radius: var(--radius-lg);
119
+ border: 1px solid var(--border-light);
120
+ box-shadow: var(--shadow-soft);
121
+ display: flex;
122
+ flex-direction: column;
123
+ overflow: hidden;
124
+
125
+ /* Transitions */
126
+ opacity: 0;
127
+ transform: translateX(50px) scale(0.95);
128
+ pointer-events: none;
129
+ transition: all 0.5s cubic-bezier(0.2, 0.8, 0.2, 1);
130
+ }
131
+
132
+ .flip-card.active {
133
+ opacity: 1;
134
+ transform: translateX(0) scale(1);
135
+ pointer-events: all;
136
+ z-index: 10;
137
+ }
138
+
139
+ /* HEADER: Stats Enhanced */
140
+ .cal-header {
141
+ background: white;
142
+ border-bottom: 1px solid var(--border-light);
143
+ padding: 1.2rem 2rem;
144
+ height: 100px;
145
+ display: flex;
146
+ align-items: center;
147
+ justify-content: space-between;
148
+ }
149
+
150
+ .month-title {
151
+ font-size: 2.2rem;
152
+ font-weight: 800;
153
+ color: #0f172a;
154
+ margin: 0;
155
+ letter-spacing: -1px;
156
+ }
157
+
158
+ .stats-row {
159
+ display: flex;
160
+ gap: 0.75rem;
161
+ }
162
+
163
+ .stat-badge {
164
+ display: flex;
165
+ flex-direction: column;
166
+ align-items: flex-start;
167
+ background: #f8fafc;
168
+ border: 1px solid #e2e8f0;
169
+ padding: 0.4rem 0.8rem;
170
+ border-radius: 8px;
171
+ min-width: 80px;
172
+ }
173
+
174
+ .stat-label {
175
+ font-size: 0.65rem;
176
+ text-transform: uppercase;
177
+ color: #64748b;
178
+ font-weight: 700;
179
+ }
180
+
181
+ .stat-val {
182
+ font-size: 0.95rem;
183
+ font-weight: 700;
184
+ color: #334155;
185
+ }
186
+
187
+ .stat-val i {
188
+ margin-right: 4px;
189
+ }
190
+
191
+ /* BODY SPLIT */
192
+ .cal-body {
193
+ flex-grow: 1;
194
+ display: flex;
195
+ overflow: hidden;
196
+ }
197
+
198
+ /* LEFT: RISKS */
199
+ .cal-left {
200
+ width: 35%;
201
+ background: #fdfdfd;
202
+ border-right: 1px solid var(--border-light);
203
+ padding: 1.5rem;
204
+ display: flex;
205
+ flex-direction: column;
206
+ }
207
+
208
+ .section-head {
209
+ font-size: 0.7rem;
210
+ font-weight: 800;
211
+ letter-spacing: 1px;
212
+ color: #94a3b8;
213
+ text-transform: uppercase;
214
+ margin-bottom: 1rem;
215
+ display: flex;
216
+ align-items: center;
217
+ gap: 0.5rem;
218
+ }
219
+
220
+ .threat-list {
221
+ overflow-y: auto;
222
+ flex-grow: 1;
223
+ padding-right: 0.5rem;
224
+ }
225
+
226
+ .threat-item {
227
+ background: white;
228
+ border: 1px solid var(--border-light);
229
+ padding: 0.9rem;
230
+ border-radius: 12px;
231
+ margin-bottom: 0.75rem;
232
+ display: flex;
233
+ align-items: center;
234
+ justify-content: space-between;
235
+ transition: 0.2s;
236
+ }
237
+
238
+ .threat-item:hover {
239
+ border-color: #94a3b8;
240
+ transform: translateX(2px);
241
+ }
242
+
243
+ .risk-dot {
244
+ width: 8px;
245
+ height: 8px;
246
+ border-radius: 50%;
247
+ display: block;
248
+ }
249
+
250
+ .risk-dot.high {
251
+ background: #ef4444;
252
+ box-shadow: 0 0 0 3px #fef2f2;
253
+ }
254
+
255
+ .risk-dot.medium {
256
+ background: #f97316;
257
+ }
258
+
259
+ /* RIGHT: STRATEGY */
260
+ .cal-right {
261
+ width: 65%;
262
+ background: white;
263
+ padding: 2rem;
264
+ display: flex;
265
+ flex-direction: column;
266
+ }
267
+
268
+ /* 1. Overview Grid */
269
+ .overview-panel {
270
+ display: grid;
271
+ grid-template-columns: 1fr 1fr;
272
+ gap: 1rem;
273
+ margin-bottom: 2rem;
274
+ }
275
+
276
+ .insight-card {
277
+ background: #f8fafc;
278
+ border: 1px solid #e2e8f0;
279
+ padding: 1rem;
280
+ border-radius: 12px;
281
+ display: flex;
282
+ align-items: center;
283
+ gap: 1rem;
284
+ }
285
+
286
+ .ic-icon {
287
+ font-size: 1.5rem;
288
+ }
289
+
290
+ .ic-content {
291
+ display: flex;
292
+ flex-direction: column;
293
+ }
294
+
295
+ .ic-label {
296
+ font-size: 0.7rem;
297
+ font-weight: 700;
298
+ color: #64748b;
299
+ text-transform: uppercase;
300
+ }
301
+
302
+ .ic-value {
303
+ font-size: 0.95rem;
304
+ font-weight: 800;
305
+ color: #0f172a;
306
+ }
307
+
308
+ /* Critical Logic */
309
+ .insight-card.critical {
310
+ background: #fff1f2;
311
+ border-color: #ffe4e6;
312
+ }
313
+
314
+ .insight-card.critical .ic-icon {
315
+ color: #e11d48;
316
+ }
317
+
318
+ .insight-card.critical .ic-value {
319
+ color: #881337;
320
+ }
321
+
322
+ .insight-card.advisory {
323
+ background: #eff6ff;
324
+ border-color: #dbeafe;
325
+ }
326
+
327
+ .insight-card.advisory .ic-icon {
328
+ color: #2563eb;
329
+ }
330
+
331
+ /* 2. Tasks */
332
+ .task-scroll {
333
+ flex-grow: 1;
334
+ overflow-y: auto;
335
+ padding-right: 0.5rem;
336
+ border-top: 1px dashed var(--border-light);
337
+ padding-top: 1.5rem;
338
+ }
339
+
340
+ .task-row {
341
+ display: flex;
342
+ gap: 1rem;
343
+ margin-bottom: 1.25rem;
344
+ }
345
+
346
+ .t-check {
347
+ width: 20px;
348
+ height: 20px;
349
+ border: 2px solid #cbd5e1;
350
+ border-radius: 6px;
351
+ flex-shrink: 0;
352
+ margin-top: 2px;
353
+ }
354
+
355
+ .task-row.urgent .t-check {
356
+ border-color: #ef4444;
357
+ background: #fee2e2;
358
+ }
359
+
360
+ .t-text {
361
+ font-size: 0.95rem;
362
+ color: #334155;
363
+ line-height: 1.5;
364
+ margin-bottom: 0.25rem;
365
+ }
366
+
367
+ .t-tag {
368
+ background: #f1f5f9;
369
+ color: #64748b;
370
+ padding: 2px 8px;
371
+ border-radius: 4px;
372
+ font-size: 0.7rem;
373
+ font-weight: 700;
374
+ }
375
+
376
+ /* Footer */
377
+ .stage-footer {
378
+ margin-top: 1rem;
379
+ padding-top: 1rem;
380
+ border-top: 1px solid var(--border-light);
381
+ display: flex;
382
+ gap: 0.5rem;
383
+ flex-wrap: wrap;
384
+ align-items: center;
385
+ }
386
+
387
+ .stage-pill {
388
+ font-size: 0.75rem;
389
+ font-weight: 700;
390
+ color: #b45309;
391
+ background: #fffbeb;
392
+ border: 1px solid #fcd34d;
393
+ padding: 0.25rem 0.75rem;
394
+ border-radius: 20px;
395
+ }
396
+
397
+ /* Nav Buttons */
398
+ .nav-btn {
399
+ width: 50px;
400
+ height: 50px;
401
+ border-radius: 50%;
402
+ background: #1e293b;
403
+ color: white;
404
+ border: none;
405
+ display: grid;
406
+ place-items: center;
407
+ font-size: 1.25rem;
408
+ cursor: pointer;
409
+ transition: 0.2s;
410
+ box-shadow: 0 8px 20px -5px rgba(0, 0, 0, 0.3);
411
+ }
412
+
413
+ .nav-btn:hover {
414
+ transform: scale(1.1);
415
+ background: #0f172a;
416
+ }
417
+
418
+ @media (max-width: 900px) {
419
+ .flipbook-stage {
420
+ height: auto;
421
+ }
422
+
423
+ .flip-card {
424
+ position: relative;
425
+ opacity: 1;
426
+ transform: none;
427
+ display: block;
428
+ margin-bottom: 2rem;
429
+ height: auto;
430
+ }
431
+
432
+ .cal-body {
433
+ flex-direction: column;
434
+ }
435
+
436
+ .cal-left {
437
+ width: 100%;
438
+ height: 300px;
439
+ border-right: none;
440
+ border-bottom: 1px solid var(--border-light);
441
+ }
442
+
443
+ .cal-right {
444
+ width: 100%;
445
+ border: none;
446
+ }
447
+
448
+ .overview-panel {
449
+ grid-template-columns: 1fr;
450
+ }
451
+ }
452
+ </style>
453
+ </head>
454
+
455
+ <body>
456
+
457
+ <div class="hero-banner">
458
+ <div class="container text-center">
459
+ <h1 class="display-4 fw-bold mb-3">{{ report_data.report_title }}</h1>
460
+ <div class="d-flex justify-content-center gap-3">
461
+ <span class="report-meta-badge"><i class="bi bi-geo-alt-fill"></i> {{ location.derived_location
462
+ }}</span>
463
+ <span class="report-meta-badge"><i class="bi bi-calendar-check"></i> {{ current_date }}</span>
464
+ </div>
465
+ </div>
466
+ </div>
467
+
468
+ <div class="content-wrapper">
469
+
470
+ <!-- AUDIO BOX -->
471
+ {% if audio_url %}
472
+ <div class="audio-sketch-box">
473
+ <div class="d-flex align-items-center gap-3">
474
+ <div class="audio-icon-circle"><i class="bi bi-mic-fill"></i></div>
475
+ <div>
476
+ <h5 class="fw-bold mb-0 text-dark">Field Briefing</h5>
477
+ <small class="text-muted">Executive Summary • {{ language_code|upper }}</small>
478
+ </div>
479
+ </div>
480
+ <audio controls style="height: 36px; width: 250px; border-radius: 20px;">
481
+ <source src="{{ audio_url }}" type="audio/mpeg">
482
+ </audio>
483
+ </div>
484
+ {% endif %}
485
+
486
+ <!-- CALENDAR -->
487
+ <div class="flipbook-wrapper">
488
+ <button class="nav-btn" onclick="changeMonth(-1)"><i class="bi bi-chevron-left"></i></button>
489
+
490
+ <div id="flipbookStage" class="flipbook-stage">
491
+ {% if weather_profile %}
492
+ {% for w in weather_profile %}
493
+ <div class="flip-card {{ 'active' if loop.index0 == 0 else '' }}">
494
+
495
+ <!-- Header with ENHANCED STATS -->
496
+ <div class="cal-header">
497
+ <h2 class="month-title">{{ w.month }}</h2>
498
+ <div class="stats-row">
499
+ <div class="stat-badge">
500
+ <span class="stat-label">Avg Temp</span>
501
+ <span class="stat-val"><i class="bi bi-thermometer-half text-danger"></i> {{ w.avg_temp
502
+ }}°</span>
503
+ </div>
504
+ <div class="stat-badge">
505
+ <span class="stat-label">Rainfall</span>
506
+ <span class="stat-val"><i class="bi bi-droplet-fill text-primary"></i> {{ w.rainfall
507
+ }}mm</span>
508
+ </div>
509
+
510
+ <!-- NEW: Humidity -->
511
+ <div class="stat-badge">
512
+ <span class="stat-label">Humidity</span>
513
+ <span class="stat-val"><i class="bi bi-moisture text-info"></i> {{ w.humidity }}%</span>
514
+ </div>
515
+
516
+ <!-- NEW: Rainy Days Calculation -->
517
+ {% set rainy_days = w.days | selectattr('rain', '>', 0) | list | length %}
518
+ <div class="stat-badge">
519
+ <span class="stat-label">Rainy Days</span>
520
+ <span class="stat-val"><i class="bi bi-cloud-drizzle-fill text-dark"></i> {{ rainy_days
521
+ }}</span>
522
+ </div>
523
+ </div>
524
+ </div>
525
+
526
+ <div class="cal-body">
527
+ <!-- Left: Threats -->
528
+ <div class="cal-left">
529
+ <div class="section-head"><i class="bi bi-bug"></i> Identified Risks</div>
530
+ <div class="threat-list">
531
+ {% set ns = namespace(found=false) %}
532
+ {% for pest in report_data.pest_prediction_table %}
533
+ {% set current_month_name = w.month.split(' ')[0] %}
534
+ {% if current_month_name in pest.outbreak_months or "All" in pest.outbreak_months %}
535
+ {% set ns.found = true %}
536
+ <div class="threat-item">
537
+ <span class="fw-bold text-dark">{{ pest.pest_name }}</span>
538
+ <span class="risk-dot {{ pest.severity|lower }}"></span>
539
+ </div>
540
+ {% endif %}
541
+ {% endfor %}
542
+ {% if not ns.found %}
543
+ <div class="text-center py-5 text-muted opacity-50">
544
+ <i class="bi bi-shield-check fs-1"></i>
545
+ <p class="small mt-2">Zero Threats</p>
546
+ </div>
547
+ {% endif %}
548
+ </div>
549
+ </div>
550
+
551
+ <!-- Right: Strategy -->
552
+ <div class="cal-right">
553
+ <!-- Overview -->
554
+ <div class="overview-panel">
555
+ {% set ns_meta = namespace(has_high=false, stages=[]) %}
556
+ {% for pest in report_data.pest_prediction_table %}
557
+ {% set current_month_name = w.month.split(' ')[0] %}
558
+ {% if current_month_name in pest.outbreak_months or "All" in pest.outbreak_months %}
559
+ {% if 'High' in pest.severity %} {% set ns_meta.has_high = true %} {% endif %}
560
+ {% if pest.impacting_stage and pest.impacting_stage not in ns_meta.stages %}
561
+ {% set _ = ns_meta.stages.append(pest.impacting_stage) %}
562
+ {% endif %}
563
+ {% endif %}
564
+ {% endfor %}
565
+
566
+ <!-- Status Card -->
567
+ <div class="insight-card {{ 'critical' if ns_meta.has_high else 'stable' }}">
568
+ <div class="ic-icon"><i
569
+ class="bi {{ 'bi-exclamation-octagon-fill' if ns_meta.has_high else 'bi-check-circle-fill text-success' }}"></i>
570
+ </div>
571
+ <div class="ic-content">
572
+ <span class="ic-label">Threat Level</span>
573
+ <span class="ic-value">{{ 'CRITICAL' if ns_meta.has_high else 'MONITORING'
574
+ }}</span>
575
+ </div>
576
+ </div>
577
+
578
+ <!-- Weather Tip Card -->
579
+ <div class="insight-card advisory">
580
+ <div class="ic-icon"><i class="bi bi-lightbulb-fill"></i></div>
581
+ <div class="ic-content">
582
+ <span class="ic-label">Advisory</span>
583
+ <span class="ic-value" style="font-size: 0.85rem; font-weight: 600;">
584
+ {% if w.rainfall > 150 %} Ensure Drainage
585
+ {% elif w.rainfall < 10 %} Increase Irrigation {% elif w.avg_temp> 35 %}
586
+ Heat Protection
587
+ {% else %} Standard Monitoring {% endif %}
588
+ </span>
589
+ </div>
590
+ </div>
591
+ </div>
592
+
593
+ <!-- Tasks -->
594
+ <div class="section-head text-dark"><i class="bi bi-list-check"></i> Action Checklist</div>
595
+ <div class="task-scroll">
596
+ {% set ns_act = namespace(found=false) %}
597
+ {% for pest in report_data.pest_prediction_table %}
598
+ {% set current_month_name = w.month.split(' ')[0] %}
599
+ {% if current_month_name in pest.outbreak_months or "All" in pest.outbreak_months %}
600
+ {% set ns_act.found = true %}
601
+ <div class="task-row {{ 'urgent' if 'High' in pest.severity else '' }}">
602
+ <div class="t-check"></div>
603
+ <div>
604
+ <div class="t-text">{{ pest.precautionary_measures | striptags }}</div>
605
+ <span class="t-tag">Preventing: {{ pest.pest_name }}</span>
606
+ </div>
607
+ </div>
608
+ {% endif %}
609
+ {% endfor %}
610
+
611
+ {% if not ns_act.found %}
612
+ <div class="task-row">
613
+ <div class="t-check bg-success border-success"></div>
614
+ <div class="t-text text-success">All clear. Proceed with standard crop husbandry.
615
+ </div>
616
+ </div>
617
+ {% endif %}
618
+ </div>
619
+
620
+ <!-- Footer -->
621
+ {% if ns_meta.stages %}
622
+ <div class="stage-footer">
623
+ <span class="small fw-bold text-muted">VULNERABLE STAGES:</span>
624
+ {% for stage in ns_meta.stages %}
625
+ <span class="stage-pill">{{ stage }}</span>
626
+ {% endfor %}
627
+ </div>
628
+ {% endif %}
629
+ </div>
630
+ </div>
631
+ </div>
632
+ {% endfor %}
633
+ {% else %}
634
+ <div class="alert alert-danger">Loading Data...</div>
635
+ {% endif %}
636
+ </div>
637
+
638
+ <button class="nav-btn" onclick="changeMonth(1)"><i class="bi bi-chevron-right"></i></button>
639
+ </div>
640
+
641
+ <div class="text-center mt-4 mb-5">
642
+ <span id="pIndicator" class="badge rounded-pill bg-dark py-2 px-4 shadow">Month 1</span>
643
+ </div>
644
+
645
+ <div class="text-center pb-5">
646
+ <a href="/" class="btn btn-outline-dark rounded-pill px-4"><i class="bi bi-arrow-repeat me-2"></i>Analyze
647
+ Another Field</a>
648
+ </div>
649
+
650
+ </div>
651
+
652
+ <!-- JS -->
653
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
654
+ <script>
655
+ let curr = 0;
656
+ const cards = document.querySelectorAll('.flip-card');
657
+ const ind = document.getElementById('pIndicator');
658
+ const len = cards.length;
659
+
660
+ function render() {
661
+ cards.forEach((c, i) => {
662
+ c.classList.remove('active');
663
+ if (i === curr) c.classList.add('active');
664
+ });
665
+ if (len > 0) ind.innerText = `Month ${curr + 1} of ${len}`;
666
+ }
667
+ function changeMonth(d) {
668
+ let n = curr + d;
669
+ if (n >= 0 && n < len) { curr = n; render(); }
670
+ }
671
+ render();
672
+ </script>
673
+ </body>
674
+
675
+ </html>
verify_endpoint.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import json
3
+
4
+ try:
5
+ # URL pointing to the local running flask app
6
+ url = "http://127.0.0.1:5001/get_timeline_weather"
7
+ params = {
8
+ "lat": "18.501157",
9
+ "lon": "73.924214",
10
+ "start_date": "2025-06-01",
11
+ "end_date": "2025-11-10"
12
+ }
13
+
14
+ print(f"Requesting: {url} with {params}")
15
+ response = requests.get(url, params=params)
16
+
17
+ print(f"Status Code: {response.status_code}")
18
+ if response.status_code == 200:
19
+ data = response.json()
20
+ print(f"Response Type: {type(data)}")
21
+ print(f"Response Length: {len(data) if isinstance(data, list) else 'N/A'}")
22
+ print("Response Body:")
23
+ print(json.dumps(data, indent=2))
24
+ else:
25
+ print("Error Response:")
26
+ print(response.text)
27
+
28
+ except Exception as e:
29
+ print(f"Failed to connect: {e}")