krushimitravit commited on
Commit
6bdc836
·
verified ·
1 Parent(s): 09c1bbe

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +17 -0
  2. app.py +585 -0
  3. requirements.txt +7 -0
  4. templates/index.html +1019 -0
  5. test_llm.py +46 -0
Dockerfile ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # Install dependencies
11
+ RUN pip install --no-cache-dir -r requirements.txt
12
+
13
+ # Expose the port your app runs on
14
+ EXPOSE 7860
15
+
16
+ # Command to run the application
17
+ CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:7860", "app:app"]
app.py ADDED
@@ -0,0 +1,585 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, Response
2
+ import requests
3
+ import datetime
4
+ from twilio.rest import Client # For Twilio integration
5
+ from geopy.geocoders import Photon
6
+ from geopy.exc import GeocoderTimedOut, GeocoderServiceError
7
+ from transformers import pipeline
8
+ import warnings
9
+
10
+ # Suppress warnings
11
+ warnings.filterwarnings('ignore')
12
+
13
+ app = Flask(__name__)
14
+
15
+ # Initialize Photon geocoder (no API key required)
16
+ photon_geolocator = Photon(user_agent="MyWeatherApp", timeout=10)
17
+
18
+ # Initialize LLM for personalized recommendations
19
+ print("Loading LLM model for personalized recommendations...")
20
+ try:
21
+ llm_generator = pipeline(
22
+ "text-generation",
23
+ model="distilgpt2", # Lightweight model
24
+ max_length=200,
25
+ device=-1 # CPU
26
+ )
27
+ print("✅ LLM model loaded successfully!")
28
+ except Exception as e:
29
+ print(f"⚠️ LLM model loading failed: {e}")
30
+ llm_generator = None
31
+
32
+ def parse_iso_datetime(timestr):
33
+ """
34
+ Parse an ISO8601 datetime string (removing any trailing 'Z').
35
+ """
36
+ if timestr.endswith("Z"):
37
+ timestr = timestr[:-1]
38
+ return datetime.datetime.fromisoformat(timestr)
39
+
40
+ def find_closest_hour_index(hour_times, current_time_str):
41
+ """
42
+ Find the index in hour_times that is closest to the current_time_str.
43
+ """
44
+ if not hour_times:
45
+ return None
46
+ dt_current = parse_iso_datetime(current_time_str)
47
+ min_diff = None
48
+ best_index = None
49
+ for i, ht in enumerate(hour_times):
50
+ dt_ht = parse_iso_datetime(ht)
51
+ diff = abs((dt_ht - dt_current).total_seconds())
52
+ if min_diff is None or diff < min_diff:
53
+ min_diff = diff
54
+ best_index = i
55
+ return best_index
56
+
57
+ def get_weather_icon(code):
58
+ """Map the Open-Meteo weathercode to an emoji icon."""
59
+ if code == 0:
60
+ return "☀️" # Clear sky
61
+ elif code in [1, 2, 3]:
62
+ return "⛅"
63
+ elif code in [45, 48]:
64
+ return "🌫️"
65
+ elif code in [51, 53, 55]:
66
+ return "🌦️"
67
+ elif code in [56, 57]:
68
+ return "🌧️"
69
+ elif code in [61, 63, 65]:
70
+ return "🌧️"
71
+ elif code in [66, 67]:
72
+ return "🌧️"
73
+ elif code in [71, 73, 75, 77]:
74
+ return "❄️"
75
+ elif code in [80, 81, 82]:
76
+ return "🌦️"
77
+ elif code in [85, 86]:
78
+ return "❄️"
79
+ elif code in [95, 96, 99]:
80
+ return "⛈️"
81
+ else:
82
+ return "❓"
83
+
84
+ def get_weather_description(code):
85
+ """Short textual description for the weathercode."""
86
+ descriptions = {
87
+ 0: "Clear sky",
88
+ 1: "Mainly clear",
89
+ 2: "Partly cloudy",
90
+ 3: "Overcast",
91
+ 45: "Fog",
92
+ 48: "Depositing rime fog",
93
+ 51: "Light drizzle",
94
+ 53: "Moderate drizzle",
95
+ 55: "Dense drizzle",
96
+ 56: "Freezing drizzle",
97
+ 57: "Freezing drizzle",
98
+ 61: "Slight rain",
99
+ 63: "Moderate rain",
100
+ 65: "Heavy rain",
101
+ 66: "Freezing rain",
102
+ 67: "Freezing rain",
103
+ 71: "Slight snow fall",
104
+ 73: "Moderate snow fall",
105
+ 75: "Heavy snow fall",
106
+ 77: "Snow grains",
107
+ 80: "Slight rain showers",
108
+ 81: "Moderate rain showers",
109
+ 82: "Violent rain showers",
110
+ 85: "Slight snow showers",
111
+ 86: "Heavy snow showers",
112
+ 95: "Thunderstorm",
113
+ 96: "Thunderstorm w/ slight hail",
114
+ 99: "Thunderstorm w/ heavy hail"
115
+ }
116
+ return descriptions.get(code, "Unknown")
117
+
118
+ def reverse_geocode(lat, lon):
119
+ """
120
+ Use Photon (via geopy) to convert latitude and longitude into a human-readable address.
121
+ If the geocoding fails, returns a fallback string with the coordinates.
122
+ """
123
+ try:
124
+ location = photon_geolocator.reverse((lat, lon), exactly_one=True)
125
+ if location:
126
+ return location.address
127
+ except (GeocoderTimedOut, GeocoderServiceError) as e:
128
+ print("Photon reverse geocode error:", e)
129
+ return f"Lat: {lat}, Lon: {lon}"
130
+
131
+ # -----------------------------
132
+ # LLM-Powered Personalized Recommendations
133
+ # -----------------------------
134
+ def generate_personalized_recommendations(weather_summary, location_address, critical_days, warning_days):
135
+ """
136
+ Generate personalized agricultural recommendations using LLM based on weather and location.
137
+ """
138
+ if llm_generator is None:
139
+ return None
140
+
141
+ try:
142
+ # Extract region info from location
143
+ region_parts = location_address.split(',')
144
+ region = region_parts[-1].strip() if len(region_parts) > 0 else "your region"
145
+
146
+ # Determine weather condition
147
+ weather_condition = "cloudy conditions"
148
+ if critical_days:
149
+ if len(critical_days) > 3:
150
+ weather_condition = "severe weather alerts"
151
+ else:
152
+ weather_condition = "critical weather conditions"
153
+ elif warning_days:
154
+ weather_condition = "warning-level weather"
155
+
156
+ # Create a better structured prompt
157
+ prompt = f"""Agricultural advice for farmers in {region}:
158
+
159
+ Weather: {weather_condition} expected
160
+ Days affected: {len(critical_days) + len(warning_days)} days
161
+
162
+ Farming recommendations:
163
+ 1. Crop care:"""
164
+
165
+ # Generate recommendations
166
+ response = llm_generator(
167
+ prompt,
168
+ max_new_tokens=80,
169
+ num_return_sequences=1,
170
+ temperature=0.8,
171
+ do_sample=True,
172
+ pad_token_id=50256,
173
+ repetition_penalty=1.5 # Reduce repetition
174
+ )
175
+
176
+ generated_text = response[0]['generated_text']
177
+ # Extract only the generated part
178
+ recommendations = generated_text[len(prompt):].strip()
179
+
180
+ # Clean up the output
181
+ lines = recommendations.split('\n')
182
+ clean_lines = []
183
+ seen = set()
184
+
185
+ for line in lines[:5]: # Max 5 lines
186
+ line = line.strip()
187
+ # Skip empty, repetitive, or nonsensical lines
188
+ if line and len(line) > 10 and line not in seen:
189
+ # Check for repetition patterns
190
+ if not any(line.count(word) > 2 for word in line.split()):
191
+ clean_lines.append(line)
192
+ seen.add(line)
193
+
194
+ if clean_lines:
195
+ recommendations = ' '.join(clean_lines)
196
+ # Limit length
197
+ if len(recommendations) > 180:
198
+ recommendations = recommendations[:177] + "..."
199
+ return recommendations
200
+ else:
201
+ # Fallback to rule-based if LLM output is poor
202
+ return generate_rule_based_recommendations(weather_condition, region, critical_days, warning_days)
203
+
204
+ except Exception as e:
205
+ print(f"LLM generation error: {e}")
206
+ return generate_rule_based_recommendations("cloudy conditions", "your region", critical_days, warning_days)
207
+
208
+ def generate_rule_based_recommendations(weather_condition, region, critical_days, warning_days):
209
+ """
210
+ Fallback rule-based recommendations when LLM fails or produces poor output.
211
+ """
212
+ if critical_days and len(critical_days) > 0:
213
+ return f"For {region}: Secure crops and equipment. Postpone spraying. Monitor drainage systems. Harvest ready crops before severe weather."
214
+ elif warning_days and len(warning_days) > 0:
215
+ if "cloudy" in weather_condition.lower() or "overcast" in weather_condition.lower():
216
+ return f"For {region}: Reduce irrigation due to lower evaporation. Monitor for fungal diseases. Apply preventive fungicides if needed."
217
+ else:
218
+ return f"For {region}: Adjust irrigation schedule. Monitor soil moisture. Delay non-essential field operations."
219
+ else:
220
+ return f"For {region}: Continue normal farming operations. Monitor weather updates regularly."
221
+
222
+ # -----------------------------
223
+ # Twilio WhatsApp Integration
224
+ # -----------------------------
225
+ def check_and_collect_alerts(forecast_list):
226
+ """
227
+ Check the forecast for hazardous weather conditions and collect detailed alert messages.
228
+ """
229
+ alerts = []
230
+ critical_days = []
231
+ warning_days = []
232
+
233
+ for day in forecast_list:
234
+ day_alerts = []
235
+ severity = "INFO"
236
+
237
+ # Temperature Analysis
238
+ if day.get("tmax") and day.get("tmin"):
239
+ tmax = day["tmax"]
240
+ tmin = day["tmin"]
241
+
242
+ if tmax > 40:
243
+ day_alerts.append(f"🌡️ Extreme Heat: {tmax}°C - High risk of crop stress and water loss")
244
+ severity = "CRITICAL"
245
+ elif tmax > 35:
246
+ day_alerts.append(f"🌡️ High Temperature: {tmax}°C - Increase irrigation frequency")
247
+ severity = "WARNING"
248
+
249
+ if tmin < 5:
250
+ day_alerts.append(f"❄️ Frost Risk: {tmin}°C - Protect sensitive crops")
251
+ severity = "CRITICAL"
252
+ elif tmin < 10:
253
+ day_alerts.append(f"🌡️ Cold Night: {tmin}°C - Monitor young plants")
254
+ severity = "WARNING"
255
+
256
+ # Temperature swing
257
+ temp_diff = tmax - tmin
258
+ if temp_diff > 20:
259
+ day_alerts.append(f"📊 Large temperature swing: {temp_diff}°C - May stress plants")
260
+
261
+ # Weather Condition Analysis
262
+ desc = day.get("desc", "").lower()
263
+ if "thunderstorm" in desc or "heavy" in desc:
264
+ day_alerts.append(f"⛈️ Severe Weather: {day['desc']} - Secure equipment, delay spraying")
265
+ severity = "CRITICAL"
266
+ elif "rain" in desc or "drizzle" in desc:
267
+ day_alerts.append(f"🌧️ Rainfall Expected: {day['desc']} - Postpone irrigation, avoid field work")
268
+ severity = "WARNING"
269
+ elif "overcast" in desc or "cloudy" in desc:
270
+ day_alerts.append(f"☁️ Cloudy Conditions: {day['desc']} - Reduced photosynthesis, monitor for diseases")
271
+ severity = "INFO"
272
+
273
+ # Compile day alert if any conditions met
274
+ if day_alerts:
275
+ day_header = f"\n*{day['day_name']} ({day['date_str']})*"
276
+ if severity == "CRITICAL":
277
+ day_header = f"🚨 {day_header} - CRITICAL"
278
+ critical_days.append(day['day_name'])
279
+ elif severity == "WARNING":
280
+ day_header = f"⚠️ {day_header} - WARNING"
281
+ warning_days.append(day['day_name'])
282
+
283
+ alert_text = day_header + "\n" + "\n".join(f" • {alert}" for alert in day_alerts)
284
+
285
+ # Add temperature range
286
+ if day.get("tmax") and day.get("tmin"):
287
+ alert_text += f"\n 📈 Temp Range: {day['tmin']}°C - {day['tmax']}°C"
288
+ if day.get("morning_temp"):
289
+ alert_text += f"\n 🌅 Morning: {day['morning_temp']}°C"
290
+ if day.get("evening_temp"):
291
+ alert_text += f"\n 🌆 Evening: {day['evening_temp']}°C"
292
+
293
+ alerts.append(alert_text)
294
+
295
+ return alerts, critical_days, warning_days
296
+
297
+ def send_whatsapp_message(message, location, location_address, critical_days, warning_days):
298
+ """
299
+ Send a WhatsApp message using Twilio API with enhanced agricultural insights.
300
+ """
301
+ google_maps_url = f"https://www.google.com/maps?q={location[0]},{location[1]}"
302
+
303
+ # Build concise severity summary
304
+ severity_summary = ""
305
+ if critical_days:
306
+ severity_summary += f"🚨 {len(critical_days)} CRITICAL: {', '.join(critical_days[:3])}\n"
307
+ if warning_days:
308
+ severity_summary += f"⚠️ {len(warning_days)} WARNING: {', '.join(warning_days[:3])}\n"
309
+
310
+ # Generate personalized LLM recommendations (shortened)
311
+ weather_summary = f"{len(critical_days)} critical, {len(warning_days)} warning days"
312
+ llm_recommendations = generate_personalized_recommendations(
313
+ weather_summary,
314
+ location_address,
315
+ critical_days,
316
+ warning_days
317
+ )
318
+
319
+ # Shorten LLM recommendations if too long
320
+ if llm_recommendations and len(llm_recommendations) > 200:
321
+ llm_recommendations = llm_recommendations[:197] + "..."
322
+
323
+ # Build concise message
324
+ message_content = (
325
+ f"🌾 *WEATHER ALERT* 🌾\n\n"
326
+ f"{severity_summary}\n"
327
+ )
328
+
329
+ # Add condensed forecast (max 3 days)
330
+ alert_lines = message.strip().split('\n\n')[:3]
331
+ for alert in alert_lines:
332
+ # Shorten each alert
333
+ lines = alert.split('\n')
334
+ if lines:
335
+ message_content += f"{lines[0]}\n" # Just the header
336
+ if len(lines) > 1:
337
+ message_content += f"{lines[1][:80]}\n" # First detail only
338
+
339
+ message_content += "\n"
340
+
341
+ # Add AI recommendations if available
342
+ if llm_recommendations:
343
+ message_content += f"🤖 *AI ADVICE:*\n{llm_recommendations}\n\n"
344
+
345
+ # Add critical actions only
346
+ if critical_days:
347
+ message_content += (
348
+ f"🚨 *URGENT:*\n"
349
+ f"• Secure equipment\n"
350
+ f"• Harvest ready crops\n"
351
+ f"• Protect livestock\n\n"
352
+ )
353
+ elif warning_days:
354
+ message_content += (
355
+ f"⚠️ *ACTIONS:*\n"
356
+ f"• Adjust irrigation\n"
357
+ f"• Monitor soil moisture\n"
358
+ f"• Delay field work\n\n"
359
+ )
360
+
361
+ # Add location
362
+ message_content += (
363
+ f"📍 {location_address}\n"
364
+ f"🗺️ {google_maps_url}\n\n"
365
+ f"_Weather Forecast for Farmers_"
366
+ )
367
+
368
+ # Ensure under 1600 characters
369
+ if len(message_content) > 1590:
370
+ message_content = message_content[:1587] + "..."
371
+
372
+ account_sid = 'ACe45f7038c5338a153d1126ca6d547c84'
373
+ auth_token = '48b9eea898885ef395d48edc74924340'
374
+ client = Client(account_sid, auth_token)
375
+
376
+ try:
377
+ msg = client.messages.create(
378
+ from_='whatsapp:+14155238886',
379
+ body=message_content,
380
+ to='whatsapp:+919763059811'
381
+ )
382
+ print(f"✅ WhatsApp sent! SID: {msg.sid}, Length: {len(message_content)} chars")
383
+ except Exception as e:
384
+ print(f"❌ WhatsApp error: {e}")
385
+ print(f"Message length was: {len(message_content)} characters")
386
+
387
+ @app.route("/", methods=["GET", "POST"])
388
+ def index():
389
+ # Default coordinates
390
+ default_lat = 18.5196
391
+ default_lon = 73.8553
392
+
393
+ if request.method == "POST":
394
+ try:
395
+ lat = float(request.form.get("lat", default_lat))
396
+ lon = float(request.form.get("lon", default_lon))
397
+ except ValueError:
398
+ lat, lon = default_lat, default_lon
399
+ else:
400
+ lat = float(request.args.get("lat", default_lat))
401
+ lon = float(request.args.get("lon", default_lon))
402
+
403
+ location_address = reverse_geocode(lat, lon)
404
+
405
+ # Call Open-Meteo API for forecast data
406
+ url = "https://api.open-meteo.com/v1/forecast"
407
+ params = {
408
+ "latitude": lat,
409
+ "longitude": lon,
410
+ "hourly": (
411
+ "temperature_2m,relative_humidity_2m,precipitation,"
412
+ "cloudcover,windspeed_10m,pressure_msl,soil_moisture_3_to_9cm,uv_index"
413
+ ),
414
+ "daily": (
415
+ "weathercode,temperature_2m_max,temperature_2m_min,"
416
+ "sunrise,sunset,uv_index_max"
417
+ ),
418
+ "current_weather": True,
419
+ "forecast_days": 10,
420
+ "timezone": "auto"
421
+ }
422
+ resp = requests.get(url, params=params)
423
+ data = resp.json()
424
+
425
+ timezone = data.get("timezone", "Local")
426
+ current_weather = data.get("current_weather", {})
427
+ current_temp = current_weather.get("temperature")
428
+ current_time = current_weather.get("time")
429
+ current_code = current_weather.get("weathercode")
430
+ current_icon = get_weather_icon(current_code)
431
+ current_desc = get_weather_description(current_code)
432
+ current_wind_speed = current_weather.get("windspeed", 0.0)
433
+ current_wind_dir = current_weather.get("winddirection", 0)
434
+
435
+ if current_time:
436
+ dt_current = parse_iso_datetime(current_time)
437
+ current_time_formatted = dt_current.strftime("%A, %b %d, %Y %I:%M %p")
438
+ else:
439
+ current_time_formatted = ""
440
+
441
+ hourly_data = data.get("hourly", {})
442
+ hour_times = hourly_data.get("time", [])
443
+ hour_temp = hourly_data.get("temperature_2m", [])
444
+ hour_humidity = hourly_data.get("relative_humidity_2m", [])
445
+ hour_precip = hourly_data.get("precipitation", [])
446
+ hour_clouds = hourly_data.get("cloudcover", [])
447
+ hour_wind = hourly_data.get("windspeed_10m", [])
448
+ hour_pressure = hourly_data.get("pressure_msl", [])
449
+ hour_soil = hourly_data.get("soil_moisture_3_to_9cm", [])
450
+ hour_uv = hourly_data.get("uv_index", [])
451
+
452
+ current_index = None
453
+ if current_time:
454
+ current_index = find_closest_hour_index(hour_times, current_time)
455
+
456
+ feels_like = current_temp
457
+ if current_index is not None and current_index < len(hour_humidity):
458
+ h = hour_humidity[current_index]
459
+ feels_like = round(current_temp - 0.2 * (100 - h) / 10, 1)
460
+
461
+ today_highlights = {}
462
+ if current_index is not None:
463
+ today_highlights["humidity"] = hour_humidity[current_index] if current_index < len(hour_humidity) else None
464
+ today_highlights["precipitation"] = hour_precip[current_index] if current_index < len(hour_precip) else None
465
+ today_highlights["clouds"] = hour_clouds[current_index] if current_index < len(hour_clouds) else None
466
+ today_highlights["windspeed"] = hour_wind[current_index] if current_index < len(hour_wind) else None
467
+ today_highlights["pressure"] = hour_pressure[current_index] if current_index < len(hour_pressure) else None
468
+ today_highlights["soil_moisture"] = hour_soil[current_index] if current_index < len(hour_soil) else None
469
+ today_highlights["uv_index"] = hour_uv[current_index] if current_index < len(hour_uv) else None
470
+ else:
471
+ for k in ["humidity", "precipitation", "cloudcover", "windspeed", "pressure", "soil_moisture", "uv_index"]:
472
+ today_highlights[k] = None
473
+
474
+ daily_data = data.get("daily", {})
475
+ daily_sunrise = daily_data.get("sunrise", [])
476
+ daily_sunset = daily_data.get("sunset", [])
477
+ if len(daily_sunrise) > 0:
478
+ today_highlights["sunrise"] = daily_sunrise[0][11:16]
479
+ else:
480
+ today_highlights["sunrise"] = None
481
+ if len(daily_sunset) > 0:
482
+ today_highlights["sunset"] = daily_sunset[0][11:16]
483
+ else:
484
+ today_highlights["sunset"] = None
485
+
486
+ daily_times = daily_data.get("time", [])
487
+ daily_codes = daily_data.get("weathercode", [])
488
+ daily_tmax = daily_data.get("temperature_2m_max", [])
489
+ daily_tmin = daily_data.get("temperature_2m_min", [])
490
+ forecast_list = []
491
+
492
+ def get_hour_temp(date_str, hour_str):
493
+ target = date_str + "T" + hour_str + ":00"
494
+ best_idx = None
495
+ best_diff = None
496
+ dt_target = parse_iso_datetime(target)
497
+ for i, ht in enumerate(hour_times):
498
+ dt_ht = parse_iso_datetime(ht)
499
+ diff = abs((dt_ht - dt_target).total_seconds())
500
+ if best_diff is None or diff < best_diff:
501
+ best_diff = diff
502
+ best_idx = i
503
+ if best_idx is not None and best_idx < len(hour_temp):
504
+ return hour_temp[best_idx]
505
+ return None
506
+
507
+ for i in range(len(daily_times)):
508
+ date_str = daily_times[i]
509
+ dt_obj = parse_iso_datetime(date_str)
510
+ day_name = dt_obj.strftime("%A")
511
+ short_date = dt_obj.strftime("%b %d")
512
+
513
+ code = daily_codes[i] if i < len(daily_codes) else None
514
+ icon = get_weather_icon(code)
515
+ desc = get_weather_description(code)
516
+
517
+ tmax = daily_tmax[i] if i < len(daily_tmax) else None
518
+ tmin = daily_tmin[i] if i < len(daily_tmin) else None
519
+ avg_temp = round((tmax + tmin) / 2, 1) if tmax is not None and tmin is not None else None
520
+
521
+ morning_temp = get_hour_temp(date_str, "09")
522
+ evening_temp = get_hour_temp(date_str, "21")
523
+
524
+ sr = daily_sunrise[i][11:16] if i < len(daily_sunrise) else None
525
+ ss = daily_sunset[i][11:16] if i < len(daily_sunset) else None
526
+
527
+ forecast_list.append({
528
+ "day_name": day_name,
529
+ "date_str": short_date,
530
+ "icon": icon,
531
+ "desc": desc,
532
+ "avg_temp": avg_temp,
533
+ "morning_temp": morning_temp,
534
+ "evening_temp": evening_temp,
535
+ "sunrise": sr,
536
+ "sunset": ss,
537
+ "tmax": tmax,
538
+ "tmin": tmin
539
+ })
540
+
541
+
542
+ alerts, critical_days, warning_days = check_and_collect_alerts(forecast_list)
543
+
544
+ # Generate AI recommendations for frontend display
545
+ ai_recommendations = None
546
+ if alerts or critical_days or warning_days:
547
+ weather_summary = f"{len(critical_days)} critical days, {len(warning_days)} warning days"
548
+ ai_recommendations = generate_personalized_recommendations(
549
+ weather_summary,
550
+ location_address,
551
+ critical_days,
552
+ warning_days
553
+ )
554
+
555
+ # Send WhatsApp alerts if needed
556
+ if alerts:
557
+ alert_message = "\n".join(alerts)
558
+ send_whatsapp_message(alert_message, (lat, lon), location_address, critical_days, warning_days)
559
+ alerts_sent = True
560
+ else:
561
+ alerts_sent = False
562
+
563
+ return render_template(
564
+ "index.html",
565
+ lat=lat,
566
+ lon=lon,
567
+ location_address=location_address,
568
+ current_temp=current_temp,
569
+ current_icon=current_icon,
570
+ current_desc=current_desc,
571
+ current_time=current_time_formatted,
572
+ current_wind_speed=current_wind_speed,
573
+ current_wind_dir=current_wind_dir,
574
+ feels_like=feels_like,
575
+ today_highlights=today_highlights,
576
+ forecast_list=forecast_list,
577
+ timezone=timezone,
578
+ alerts_sent=alerts_sent,
579
+ ai_recommendations=ai_recommendations,
580
+ critical_days=critical_days,
581
+ warning_days=warning_days
582
+ )
583
+
584
+ if __name__ == "__main__":
585
+ app.run(debug=True,port=5001)
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ flask
2
+ gunicorn
3
+ requests
4
+ twilio
5
+ geopy
6
+ transformers
7
+ torch
templates/index.html ADDED
@@ -0,0 +1,1019 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>Weather Forecast for Farmers</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
9
+ <style>
10
+ * {
11
+ margin: 0;
12
+ padding: 0;
13
+ box-sizing: border-box;
14
+ }
15
+
16
+ :root {
17
+ /* Primary Colors */
18
+ --deep-green: #1a5d3a;
19
+ --accent-green: #198754;
20
+ --darker-accent: #143d2e;
21
+
22
+ /* Neutral Colors */
23
+ --bg-color: #f8f9fa;
24
+ --surface: #ffffff;
25
+ --text-dark: #212529;
26
+ --text-muted: #6c757d;
27
+ --border: #dee2e6;
28
+ }
29
+
30
+ body {
31
+ font-family: 'Outfit', sans-serif;
32
+ background: #f0feec;
33
+ color: var(--text-dark);
34
+ line-height: 1.6;
35
+ }
36
+
37
+ /* Header with Flat Bottom */
38
+ .hero-header {
39
+ background: var(--deep-green);
40
+ color: white;
41
+ padding: 3rem 2rem 3rem;
42
+ box-shadow: 0 4px 20px rgba(0,0,0,0.1);
43
+ }
44
+
45
+ .hero-header h1 {
46
+ font-size: 2.5rem;
47
+ font-weight: 700;
48
+ text-align: center;
49
+ margin-bottom: 0.5rem;
50
+ }
51
+
52
+ .hero-header p {
53
+ text-align: center;
54
+ font-size: 1.1rem;
55
+ font-weight: 300;
56
+ opacity: 0.9;
57
+ }
58
+
59
+ /* Process Steps */
60
+ .process-steps {
61
+ display: flex;
62
+ justify-content: center;
63
+ gap: 3rem;
64
+ margin: -2rem auto 2rem;
65
+ max-width: 600px;
66
+ padding: 0 1rem;
67
+ }
68
+
69
+ .process-step {
70
+ text-align: center;
71
+ color: var(--text-muted);
72
+ }
73
+
74
+ .process-step i {
75
+ font-size: 2rem;
76
+ color: var(--accent-green);
77
+ margin-bottom: 0.5rem;
78
+ display: block;
79
+ }
80
+
81
+ .process-step span {
82
+ font-size: 0.9rem;
83
+ font-weight: 500;
84
+ }
85
+
86
+ /* Main Container */
87
+ .container {
88
+ max-width: 1400px;
89
+ margin: 0 auto;
90
+ padding: 0 2rem 3rem;
91
+ }
92
+
93
+ /* Floating Card */
94
+ .floating-card {
95
+ background: var(--surface);
96
+ border-radius: 20px;
97
+ border: 3px dashed var(--deep-green) !important;
98
+ padding: 3rem;
99
+ box-shadow: 0 10px 40px rgba(0,0,0,0.08);
100
+ margin-top: 2rem;
101
+ margin-bottom: 2rem;
102
+ }
103
+
104
+ /* Input Groups */
105
+ .input-row {
106
+ display: grid;
107
+ grid-template-columns: 1fr 1fr;
108
+ gap: 1.5rem;
109
+ margin-bottom: 1.5rem;
110
+ }
111
+
112
+ .input-group {
113
+ display: flex;
114
+ flex-direction: column;
115
+ }
116
+
117
+ .input-group label {
118
+ font-weight: 500;
119
+ color: var(--text-dark);
120
+ margin-bottom: 0.5rem;
121
+ font-size: 0.95rem;
122
+ }
123
+
124
+ .input-wrapper {
125
+ display: flex;
126
+ border: 1px solid var(--border);
127
+ border-radius: 8px;
128
+ overflow: hidden;
129
+ background: var(--bg-color);
130
+ transition: all 0.3s ease;
131
+ }
132
+
133
+ .input-wrapper:focus-within {
134
+ border-color: var(--accent-green);
135
+ box-shadow: 0 0 0 4px rgba(25, 135, 84, 0.1);
136
+ background: var(--surface);
137
+ }
138
+
139
+ .input-icon {
140
+ background: var(--surface);
141
+ padding: 0.75rem 1rem;
142
+ display: flex;
143
+ align-items: center;
144
+ color: var(--accent-green);
145
+ border-right: 1px solid var(--border);
146
+ }
147
+
148
+ .input-wrapper:focus-within .input-icon {
149
+ border-right-color: var(--accent-green);
150
+ }
151
+
152
+ .input-field {
153
+ flex: 1;
154
+ padding: 0.75rem 1rem;
155
+ border: none;
156
+ background: transparent;
157
+ font-family: 'Outfit', sans-serif;
158
+ font-size: 1rem;
159
+ color: var(--text-dark);
160
+ }
161
+
162
+ .input-field:focus {
163
+ outline: none;
164
+ }
165
+
166
+ /* Buttons */
167
+ .btn-row {
168
+ display: grid;
169
+ grid-template-columns: 1fr 1fr;
170
+ gap: 1rem;
171
+ margin-top: 2rem;
172
+ }
173
+
174
+ .btn-primary {
175
+ background: var(--deep-green);
176
+ color: white;
177
+ border: none;
178
+ border-radius: 8px;
179
+ padding: 0.875rem 1.5rem;
180
+ font-weight: 500;
181
+ font-size: 1rem;
182
+ cursor: pointer;
183
+ display: flex;
184
+ align-items: center;
185
+ justify-content: center;
186
+ gap: 0.5rem;
187
+ transition: all 0.3s ease;
188
+ font-family: 'Outfit', sans-serif;
189
+ }
190
+
191
+ .btn-primary:hover {
192
+ background: var(--darker-accent);
193
+ transform: translateY(-2px);
194
+ box-shadow: 0 4px 12px rgba(26, 93, 58, 0.3);
195
+ }
196
+
197
+ .btn-secondary {
198
+ background: var(--surface);
199
+ color: var(--accent-green);
200
+ border: 1px solid var(--accent-green);
201
+ border-radius: 8px;
202
+ padding: 0.875rem 1.5rem;
203
+ font-weight: 500;
204
+ font-size: 1rem;
205
+ cursor: pointer;
206
+ display: flex;
207
+ align-items: center;
208
+ justify-content: center;
209
+ gap: 0.5rem;
210
+ transition: all 0.3s ease;
211
+ font-family: 'Outfit', sans-serif;
212
+ }
213
+
214
+ .btn-secondary:hover {
215
+ background: var(--accent-green);
216
+ color: white;
217
+ transform: translateY(-2px);
218
+ box-shadow: 0 4px 12px rgba(25, 135, 84, 0.3);
219
+ }
220
+
221
+ /* Current Weather Card */
222
+ .current-weather {
223
+ background: var(--surface);
224
+ border-radius: 20px;
225
+ border: 3px solid var(--deep-green) !important;
226
+ padding: 2.5rem;
227
+ box-shadow: 0 10px 40px rgba(0,0,0,0.08);
228
+ margin-bottom: 2rem;
229
+ }
230
+
231
+ .location-header {
232
+ display: flex;
233
+ justify-content: space-between;
234
+ align-items: center;
235
+ margin-bottom: 2rem;
236
+ flex-wrap: wrap;
237
+ gap: 1rem;
238
+ }
239
+
240
+ .location-info h2 {
241
+ font-size: 1.75rem;
242
+ font-weight: 600;
243
+ color: var(--deep-green);
244
+ display: flex;
245
+ align-items: center;
246
+ gap: 0.5rem;
247
+ margin-bottom: 0.25rem;
248
+ }
249
+
250
+ .location-info p {
251
+ color: var(--text-muted);
252
+ font-size: 0.95rem;
253
+ display: flex;
254
+ align-items: center;
255
+ gap: 0.5rem;
256
+ }
257
+
258
+ .weather-main {
259
+ display: flex;
260
+ justify-content: space-between;
261
+ align-items: center;
262
+ padding: 2rem 0;
263
+ border-top: 1px solid var(--border);
264
+ border-bottom: 1px solid var(--border);
265
+ flex-wrap: wrap;
266
+ gap: 2rem;
267
+ }
268
+
269
+ .temp-display {
270
+ display: flex;
271
+ align-items: center;
272
+ gap: 1.5rem;
273
+ }
274
+
275
+ .temp-icon {
276
+ font-size: 4rem;
277
+ color: var(--accent-green);
278
+ }
279
+
280
+ .temp-value {
281
+ font-size: 4rem;
282
+ font-weight: 700;
283
+ color: var(--deep-green);
284
+ }
285
+
286
+ .weather-desc {
287
+ text-align: right;
288
+ }
289
+
290
+ .weather-desc h3 {
291
+ font-size: 1.5rem;
292
+ font-weight: 600;
293
+ color: var(--deep-green);
294
+ margin-bottom: 0.5rem;
295
+ }
296
+
297
+ .weather-details {
298
+ display: flex;
299
+ gap: 2rem;
300
+ color: var(--text-muted);
301
+ }
302
+
303
+ .weather-detail {
304
+ display: flex;
305
+ align-items: center;
306
+ gap: 0.5rem;
307
+ }
308
+
309
+ /* Alert Badge */
310
+ .alert-badge {
311
+ background: #fff3cd;
312
+ border: 1px solid #ffc107;
313
+ border-radius: 8px;
314
+ padding: 1rem 1.5rem;
315
+ margin-bottom: 2rem;
316
+ display: flex;
317
+ align-items: center;
318
+ gap: 1rem;
319
+ }
320
+
321
+ .alert-badge i {
322
+ font-size: 1.5rem;
323
+ color: #856404;
324
+ }
325
+
326
+ .alert-badge .alert-content {
327
+ flex: 1;
328
+ }
329
+
330
+ .alert-badge h4 {
331
+ font-weight: 600;
332
+ color: #856404;
333
+ margin-bottom: 0.25rem;
334
+ }
335
+
336
+ .alert-badge p {
337
+ color: #856404;
338
+ margin: 0;
339
+ font-size: 0.95rem;
340
+ }
341
+
342
+ /* AI Recommendations Card */
343
+ .ai-recommendations-card {
344
+ background: linear-gradient(135deg, #f0fff4 0%, #e6f7ed 100%);
345
+ border: 3px solid var(--accent-green);
346
+ border-radius: 20px;
347
+ padding: 2.5rem;
348
+ margin-bottom: 2rem;
349
+ box-shadow: 0 10px 40px rgba(25, 135, 84, 0.15);
350
+ animation: fadeInUp 0.5s ease-out;
351
+ }
352
+
353
+ .ai-header {
354
+ display: flex;
355
+ justify-content: space-between;
356
+ align-items: flex-start;
357
+ margin-bottom: 2rem;
358
+ flex-wrap: wrap;
359
+ gap: 1rem;
360
+ }
361
+
362
+ .ai-subtitle {
363
+ color: var(--text-muted);
364
+ font-size: 0.95rem;
365
+ margin: 0;
366
+ }
367
+
368
+ .severity-badges {
369
+ display: flex;
370
+ gap: 0.75rem;
371
+ flex-wrap: wrap;
372
+ }
373
+
374
+ .badge {
375
+ padding: 0.5rem 1rem;
376
+ border-radius: 50px;
377
+ font-size: 0.85rem;
378
+ font-weight: 600;
379
+ display: inline-flex;
380
+ align-items: center;
381
+ gap: 0.5rem;
382
+ }
383
+
384
+ .badge-critical {
385
+ background: #fee;
386
+ color: #c00;
387
+ border: 2px solid #c00;
388
+ }
389
+
390
+ .badge-warning {
391
+ background: #fff3cd;
392
+ color: #856404;
393
+ border: 2px solid #ffc107;
394
+ }
395
+
396
+ .ai-content {
397
+ display: flex;
398
+ gap: 1.5rem;
399
+ align-items: flex-start;
400
+ background: white;
401
+ padding: 2rem;
402
+ border-radius: 15px;
403
+ border: 2px dashed var(--accent-green);
404
+ margin-bottom: 1.5rem;
405
+ }
406
+
407
+ .ai-icon {
408
+ font-size: 3rem;
409
+ color: var(--accent-green);
410
+ flex-shrink: 0;
411
+ }
412
+
413
+ .ai-text {
414
+ flex: 1;
415
+ }
416
+
417
+ .ai-text p {
418
+ margin: 0;
419
+ line-height: 1.8;
420
+ color: var(--text-dark);
421
+ font-size: 1.05rem;
422
+ white-space: pre-wrap;
423
+ }
424
+
425
+ .ai-footer {
426
+ display: flex;
427
+ align-items: center;
428
+ gap: 0.75rem;
429
+ padding: 1rem 1.5rem;
430
+ background: rgba(25, 135, 84, 0.1);
431
+ border-radius: 10px;
432
+ font-size: 0.85rem;
433
+ color: var(--text-muted);
434
+ }
435
+
436
+ .ai-footer i {
437
+ font-size: 1.2rem;
438
+ color: var(--accent-green);
439
+ flex-shrink: 0;
440
+ }
441
+
442
+ @keyframes fadeInUp {
443
+ from {
444
+ opacity: 0;
445
+ transform: translateY(20px);
446
+ }
447
+ to {
448
+ opacity: 1;
449
+ transform: translateY(0);
450
+ }
451
+ }
452
+
453
+ /* Section Titles */
454
+ .section-title {
455
+ font-size: 1.5rem;
456
+ font-weight: 600;
457
+ color: var(--deep-green);
458
+ margin-bottom: 1.5rem;
459
+ display: flex;
460
+ align-items: center;
461
+ gap: 0.75rem;
462
+ }
463
+
464
+ /* Highlights Grid */
465
+ .highlights-grid {
466
+ display: grid;
467
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
468
+ gap: 1.5rem;
469
+ margin-bottom: 3rem;
470
+ }
471
+
472
+ .highlight-card {
473
+ background: var(--surface);
474
+ border-radius: 20px;
475
+ border: 3px solid var(--deep-green) !important;
476
+ padding: 1.75rem;
477
+ box-shadow: 0 10px 40px rgba(0,0,0,0.08);
478
+ transition: all 0.3s ease;
479
+ }
480
+
481
+ .highlight-card:hover {
482
+ transform: translateY(-5px);
483
+ box-shadow: 0 15px 50px rgba(0,0,0,0.12);
484
+ }
485
+
486
+ .highlight-card i {
487
+ font-size: 2rem;
488
+ color: var(--accent-green);
489
+ margin-bottom: 1rem;
490
+ display: block;
491
+ }
492
+
493
+ .highlight-card h3 {
494
+ font-size: 0.95rem;
495
+ font-weight: 500;
496
+ color: var(--text-muted);
497
+ margin-bottom: 0.5rem;
498
+ }
499
+
500
+ .highlight-card .value {
501
+ font-size: 1.75rem;
502
+ font-weight: 600;
503
+ color: var(--deep-green);
504
+ }
505
+
506
+ .highlight-card .sun-times {
507
+ display: flex;
508
+ justify-content: space-around;
509
+ align-items: center;
510
+ }
511
+
512
+ .sun-time {
513
+ text-align: center;
514
+ }
515
+
516
+ .sun-time i {
517
+ font-size: 1.5rem;
518
+ margin-bottom: 0.5rem;
519
+ }
520
+
521
+ .sun-time .value {
522
+ font-size: 1.25rem;
523
+ }
524
+
525
+ /* Forecast Grid */
526
+ .forecast-grid {
527
+ display: grid;
528
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
529
+ gap: 1.5rem;
530
+ margin-bottom: 2rem;
531
+ }
532
+
533
+ .forecast-card {
534
+ background: var(--surface);
535
+ border-radius: 20px;
536
+ border: 3px solid var(--deep-green) !important;
537
+ padding: 1.75rem;
538
+ box-shadow: 0 10px 40px rgba(0,0,0,0.08);
539
+ text-align: center;
540
+ transition: all 0.3s ease;
541
+ }
542
+
543
+ .forecast-card:hover {
544
+ transform: translateY(-5px);
545
+ box-shadow: 0 15px 50px rgba(0,0,0,0.12);
546
+ }
547
+
548
+ .forecast-card .day-name {
549
+ font-size: 1.1rem;
550
+ font-weight: 600;
551
+ color: var(--deep-green);
552
+ margin-bottom: 0.25rem;
553
+ }
554
+
555
+ .forecast-card .date {
556
+ font-size: 0.9rem;
557
+ color: var(--text-muted);
558
+ margin-bottom: 1rem;
559
+ }
560
+
561
+ .forecast-card i {
562
+ font-size: 2.5rem;
563
+ color: var(--accent-green);
564
+ margin: 1rem 0;
565
+ display: block;
566
+ }
567
+
568
+ .forecast-card .description {
569
+ font-size: 0.9rem;
570
+ color: var(--text-muted);
571
+ margin-bottom: 0.75rem;
572
+ }
573
+
574
+ .forecast-card .temp {
575
+ font-size: 1.5rem;
576
+ font-weight: 700;
577
+ color: var(--deep-green);
578
+ margin: 0.5rem 0;
579
+ }
580
+
581
+ .temp-range {
582
+ display: flex;
583
+ justify-content: center;
584
+ gap: 1rem;
585
+ font-size: 0.9rem;
586
+ color: var(--text-muted);
587
+ margin-bottom: 1rem;
588
+ }
589
+
590
+ .temp-range span {
591
+ display: flex;
592
+ align-items: center;
593
+ gap: 0.25rem;
594
+ }
595
+
596
+ .day-temps {
597
+ padding-top: 1rem;
598
+ border-top: 1px solid var(--border);
599
+ border: 3px dashed var(--deep-green) !important;
600
+ font-size: 0.85rem;
601
+ color: var(--text-muted);
602
+ }
603
+
604
+ .day-temps div {
605
+ margin-bottom: 0.25rem;
606
+ }
607
+
608
+ .sun-info {
609
+ padding-top: 1rem;
610
+ border-top: 1px dashed var(--border);
611
+ border: 3px dashed var(--deep-green) !important;
612
+ margin-top: 1rem;
613
+ font-size: 0.85rem;
614
+ color: var(--text-muted);
615
+ }
616
+
617
+ .sun-info div {
618
+ display: flex;
619
+ align-items: center;
620
+ justify-content: center;
621
+ gap: 0.5rem;
622
+ margin-bottom: 0.25rem;
623
+ }
624
+
625
+ /* Footer */
626
+ .footer {
627
+ text-align: center;
628
+ padding: 2rem 0;
629
+ color: var(--text-muted);
630
+ font-size: 0.9rem;
631
+ }
632
+
633
+ /* Responsive */
634
+ @media (max-width: 768px) {
635
+ .hero-header h1 {
636
+ font-size: 2rem;
637
+ }
638
+
639
+ .process-steps {
640
+ gap: 1.5rem;
641
+ }
642
+
643
+ .floating-card {
644
+ padding: 2rem;
645
+ }
646
+
647
+ .input-row {
648
+ grid-template-columns: 1fr;
649
+ }
650
+
651
+ .btn-row {
652
+ grid-template-columns: 1fr;
653
+ }
654
+
655
+ .location-header {
656
+ flex-direction: column;
657
+ align-items: flex-start;
658
+ }
659
+
660
+ .weather-main {
661
+ flex-direction: column;
662
+ text-align: center;
663
+ }
664
+
665
+ .weather-desc {
666
+ text-align: center;
667
+ }
668
+
669
+ .temp-display {
670
+ flex-direction: column;
671
+ }
672
+
673
+ .highlights-grid {
674
+ grid-template-columns: 1fr;
675
+ }
676
+
677
+ .forecast-grid {
678
+ grid-template-columns: 1fr;
679
+ }
680
+
681
+ .ai-recommendations-card {
682
+ padding: 1.5rem;
683
+ }
684
+
685
+ .ai-content {
686
+ flex-direction: column;
687
+ padding: 1.5rem;
688
+ }
689
+
690
+ .ai-icon {
691
+ font-size: 2rem;
692
+ }
693
+
694
+ .ai-text p {
695
+ font-size: 0.95rem;
696
+ }
697
+
698
+ .severity-badges {
699
+ width: 100%;
700
+ }
701
+
702
+ .badge {
703
+ flex: 1;
704
+ justify-content: center;
705
+ }
706
+ }
707
+
708
+ /* Loading Animation */
709
+ @keyframes pulse {
710
+ 0%, 100% { opacity: 1; }
711
+ 50% { opacity: 0.5; }
712
+ }
713
+
714
+ .loading {
715
+ animation: pulse 1.5s ease-in-out infinite;
716
+ }
717
+ </style>
718
+ </head>
719
+ <body>
720
+ <!-- Hero Header -->
721
+ <header class="hero-header">
722
+ <h1><i class="bi bi-cloud-sun"></i> Weather Forecast for Farmers</h1>
723
+ <p>Real-time weather insights for your farm</p>
724
+ </header>
725
+
726
+
727
+ <div class="container">
728
+ <!-- Location Input Card -->
729
+ <div class="floating-card">
730
+ <form method="POST" action="/">
731
+ <div class="input-row">
732
+ <div class="input-group">
733
+ <label for="lat">Latitude</label>
734
+ <div class="input-wrapper">
735
+ <div class="input-icon">
736
+ <i class="bi bi-compass"></i>
737
+ </div>
738
+ <input type="number" step="any" id="lat" name="lat" class="input-field" value="{{ lat }}" placeholder="e.g., 18.5196" required />
739
+ </div>
740
+ </div>
741
+ <div class="input-group">
742
+ <label for="lon">Longitude</label>
743
+ <div class="input-wrapper">
744
+ <div class="input-icon">
745
+ <i class="bi bi-compass"></i>
746
+ </div>
747
+ <input type="number" step="any" id="lon" name="lon" class="input-field" value="{{ lon }}" placeholder="e.g., 73.8553" required />
748
+ </div>
749
+ </div>
750
+ </div>
751
+ <div class="btn-row">
752
+ <button type="submit" class="btn-primary">
753
+ <i class="bi bi-search"></i>
754
+ Get Weather Forecast
755
+ </button>
756
+ <button type="button" id="get-location-btn" class="btn-secondary">
757
+ <i class="bi bi-geo-alt-fill"></i>
758
+ Use Current Location
759
+ </button>
760
+ </div>
761
+ </form>
762
+ </div>
763
+
764
+ <!-- Alert Notification -->
765
+
766
+
767
+ <!-- Current Weather -->
768
+ <div class="current-weather">
769
+ <div class="location-header">
770
+ <div class="location-info">
771
+ <h2>
772
+ <i class="bi bi-geo-alt-fill"></i>
773
+ {{ location_address }}
774
+ </h2>
775
+ <p>
776
+ <i class="bi bi-clock"></i>
777
+ {{ current_time }} ({{ timezone }})
778
+ </p>
779
+ </div>
780
+ </div>
781
+
782
+ <div class="weather-main">
783
+ <div class="temp-display">
784
+ {% if current_icon == "☀️" %}
785
+ <i class="bi bi-sun-fill temp-icon"></i>
786
+ {% elif current_icon == "⛅" %}
787
+ <i class="bi bi-cloud-sun-fill temp-icon"></i>
788
+ {% elif current_icon == "🌫️" %}
789
+ <i class="bi bi-cloud-fog2-fill temp-icon"></i>
790
+ {% elif current_icon == "🌦️" %}
791
+ <i class="bi bi-cloud-drizzle-fill temp-icon"></i>
792
+ {% elif current_icon == "🌧️" %}
793
+ <i class="bi bi-cloud-rain-fill temp-icon"></i>
794
+ {% elif current_icon == "❄️" %}
795
+ <i class="bi bi-snow temp-icon"></i>
796
+ {% elif current_icon == "⛈️" %}
797
+ <i class="bi bi-cloud-lightning-rain-fill temp-icon"></i>
798
+ {% else %}
799
+ <i class="bi bi-question-circle temp-icon"></i>
800
+ {% endif %}
801
+ <div class="temp-value">{{ current_temp }}°C</div>
802
+ </div>
803
+ <div class="weather-desc">
804
+ <h3>{{ current_desc }}</h3>
805
+ <div class="weather-details">
806
+ <div class="weather-detail">
807
+ <i class="bi bi-thermometer-half"></i>
808
+ Feels like {{ feels_like }}°C
809
+ </div>
810
+ <div class="weather-detail">
811
+ <i class="bi bi-wind"></i>
812
+ {{ current_wind_speed }} km/h
813
+ </div>
814
+ </div>
815
+ </div>
816
+ </div>
817
+ </div>
818
+
819
+ <!-- AI-Powered Recommendations -->
820
+ {% if ai_recommendations %}
821
+ <div class="ai-recommendations-card">
822
+ <div class="ai-header">
823
+ <div>
824
+ <h2 class="section-title" style="margin-bottom: 0.5rem;">
825
+ <i class="bi bi-robot"></i>
826
+ AI-Powered Personalized Recommendations
827
+ </h2>
828
+ <p class="ai-subtitle">Based on your location and upcoming weather conditions</p>
829
+ </div>
830
+ <div class="severity-badges">
831
+ {% if critical_days %}
832
+ <span class="badge badge-critical">
833
+ <i class="bi bi-exclamation-triangle-fill"></i>
834
+ {{ critical_days|length }} Critical Day(s)
835
+ </span>
836
+ {% endif %}
837
+ {% if warning_days %}
838
+ <span class="badge badge-warning">
839
+ <i class="bi bi-exclamation-circle-fill"></i>
840
+ {{ warning_days|length }} Warning Day(s)
841
+ </span>
842
+ {% endif %}
843
+ </div>
844
+ </div>
845
+
846
+ <div class="ai-content">
847
+ <div class="ai-icon">
848
+ <i class="bi bi-lightbulb-fill"></i>
849
+ </div>
850
+ <div class="ai-text">
851
+ <p>{{ ai_recommendations }}</p>
852
+ </div>
853
+ </div>
854
+
855
+ <div class="ai-footer">
856
+ <i class="bi bi-info-circle"></i>
857
+ <span>These recommendations are AI-generated based on weather patterns and your farm location. Always consult with local agricultural experts for critical decisions.</span>
858
+ </div>
859
+ </div>
860
+ {% endif %}
861
+
862
+ <!-- Today's Highlights -->
863
+ <h2 class="section-title">
864
+ <i class="bi bi-graph-up"></i>
865
+ Today's Highlights
866
+ </h2>
867
+ <div class="highlights-grid">
868
+ <div class="highlight-card">
869
+ <i class="bi bi-droplet-fill"></i>
870
+ <h3>Humidity</h3>
871
+ <div class="value">{{ today_highlights.humidity }}%</div>
872
+ </div>
873
+ <div class="highlight-card">
874
+ <i class="bi bi-wind"></i>
875
+ <h3>Wind Speed</h3>
876
+ <div class="value">{{ today_highlights.windspeed }} km/h</div>
877
+ </div>
878
+ <div class="highlight-card">
879
+ <i class="bi bi-brightness-high-fill"></i>
880
+ <h3>UV Index</h3>
881
+ <div class="value">{{ today_highlights.uv_index }}</div>
882
+ </div>
883
+ <div class="highlight-card">
884
+ <i class="bi bi-speedometer2"></i>
885
+ <h3>Pressure</h3>
886
+ <div class="value">{{ today_highlights.pressure|round(1) }} hPa</div>
887
+ </div>
888
+ <div class="highlight-card">
889
+ <i class="bi bi-cloud-fill"></i>
890
+ <h3>Cloud Cover</h3>
891
+ <div class="value">{{ today_highlights.clouds }}%</div>
892
+ </div>
893
+ <div class="highlight-card">
894
+ <i class="bi bi-cloud-rain-fill"></i>
895
+ <h3>Precipitation</h3>
896
+ <div class="value">{{ today_highlights.precipitation|round(2) }} mm</div>
897
+ </div>
898
+ <div class="highlight-card">
899
+ <i class="bi bi-moisture"></i>
900
+ <h3>Soil Moisture</h3>
901
+ <div class="value">{{ today_highlights.soil_moisture|round(3) }} m³/m³</div>
902
+ </div>
903
+ <div class="highlight-card">
904
+ <div class="sun-times">
905
+ <div class="sun-time">
906
+ <i class="bi bi-sunrise-fill" style="color: #fbbf24;"></i>
907
+ <h3>Sunrise</h3>
908
+ <div class="value">{{ today_highlights.sunrise }}</div>
909
+ </div>
910
+ <div style="width: 1px; background: var(--border); height: 60px;"></div>
911
+ <div class="sun-time">
912
+ <i class="bi bi-sunset-fill" style="color: #f97316;"></i>
913
+ <h3>Sunset</h3>
914
+ <div class="value">{{ today_highlights.sunset }}</div>
915
+ </div>
916
+ </div>
917
+ </div>
918
+ </div>
919
+
920
+ <!-- 10-Day Forecast -->
921
+ <h2 class="section-title">
922
+ <i class="bi bi-calendar-range"></i>
923
+ 10-Day Forecast
924
+ </h2>
925
+ <div class="forecast-grid">
926
+ {% for day in forecast_list %}
927
+ <div class="forecast-card">
928
+ <div class="day-name">{{ day.day_name }}</div>
929
+ <div class="date">{{ day.date_str }}</div>
930
+
931
+ {% if day.icon == "☀️" %}
932
+ <i class="bi bi-sun-fill"></i>
933
+ {% elif day.icon == "⛅" %}
934
+ <i class="bi bi-cloud-sun-fill"></i>
935
+ {% elif day.icon == "🌫️" %}
936
+ <i class="bi bi-cloud-fog2-fill"></i>
937
+ {% elif day.icon == "🌦️" %}
938
+ <i class="bi bi-cloud-drizzle-fill"></i>
939
+ {% elif day.icon == "🌧️" %}
940
+ <i class="bi bi-cloud-rain-fill"></i>
941
+ {% elif day.icon == "❄️" %}
942
+ <i class="bi bi-snow"></i>
943
+ {% elif day.icon == "⛈️" %}
944
+ <i class="bi bi-cloud-lightning-rain-fill"></i>
945
+ {% else %}
946
+ <i class="bi bi-question-circle"></i>
947
+ {% endif %}
948
+
949
+ <div class="description">{{ day.desc }}</div>
950
+ <div class="temp">{{ day.avg_temp }}°C</div>
951
+ <div class="temp-range">
952
+ <span><i class="bi bi-arrow-down" style="color: #3b82f6;"></i> {{ day.tmin }}°C</span>
953
+ <span><i class="bi bi-arrow-up" style="color: #ef4444;"></i> {{ day.tmax }}°C</span>
954
+ </div>
955
+ <div class="day-temps">
956
+ <div><strong>Morning:</strong> {{ day.morning_temp }}°C</div>
957
+ <div><strong>Evening:</strong> {{ day.evening_temp }}°C</div>
958
+ </div>
959
+ <div class="sun-info">
960
+ <div><i class="bi bi-sunrise-fill" style="color: #fbbf24;"></i> {{ day.sunrise }}</div>
961
+ <div><i class="bi bi-sunset-fill" style="color: #f97316;"></i> {{ day.sunset }}</div>
962
+ </div>
963
+ </div>
964
+ {% endfor %}
965
+ </div>
966
+
967
+ <!-- Footer -->
968
+ <footer class="footer">
969
+ <p>Last updated: {{ current_time }}</p>
970
+ <p>Powered by Open-Meteo API | Weather alerts via Twilio WhatsApp</p>
971
+ </footer>
972
+ </div>
973
+
974
+ <!-- Scripts -->
975
+ <script>
976
+ // Get Current Location
977
+ document.getElementById('get-location-btn').addEventListener('click', function() {
978
+ if (navigator.geolocation) {
979
+ navigator.geolocation.getCurrentPosition(function(position) {
980
+ document.getElementById('lat').value = position.coords.latitude.toFixed(4);
981
+ document.getElementById('lon').value = position.coords.longitude.toFixed(4);
982
+ document.querySelector('form').submit();
983
+ }, function(error) {
984
+ alert('Error: ' + error.message);
985
+ });
986
+ } else {
987
+ alert('Geolocation is not supported by this browser.');
988
+ }
989
+ });
990
+
991
+ // Input Validation
992
+ const latInput = document.getElementById('lat');
993
+ const lonInput = document.getElementById('lon');
994
+
995
+ latInput.addEventListener('input', function() {
996
+ const value = parseFloat(this.value);
997
+ if (value < -90 || value > 90) {
998
+ this.setCustomValidity('Latitude must be between -90 and 90 degrees');
999
+ } else {
1000
+ this.setCustomValidity('');
1001
+ }
1002
+ });
1003
+
1004
+ lonInput.addEventListener('input', function() {
1005
+ const value = parseFloat(this.value);
1006
+ if (value < -180 || value > 180) {
1007
+ this.setCustomValidity('Longitude must be between -180 and 180 degrees');
1008
+ } else {
1009
+ this.setCustomValidity('');
1010
+ }
1011
+ });
1012
+
1013
+ // Loading Animation
1014
+ document.querySelector('form').addEventListener('submit', function() {
1015
+ document.querySelector('.container').classList.add('loading');
1016
+ });
1017
+ </script>
1018
+ </body>
1019
+ </html>
test_llm.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Quick test for LLM integration
2
+ from transformers import pipeline
3
+ import warnings
4
+
5
+ warnings.filterwarnings('ignore')
6
+
7
+ print("Testing LLM integration...")
8
+ print("Loading model...")
9
+
10
+ try:
11
+ llm_generator = pipeline(
12
+ "text-generation",
13
+ model="distilgpt2",
14
+ max_length=200,
15
+ device=-1
16
+ )
17
+ print("✅ Model loaded successfully!")
18
+
19
+ # Test generation
20
+ prompt = """As an agricultural expert for Maharashtra, provide specific farming recommendations:
21
+
22
+ Weather: 6 warning days with overcast conditions
23
+ Severity: WARNING conditions on Saturday, Tuesday, Wednesday, Thursday, Friday, Sunday.
24
+
25
+ Recommendations for farmers:
26
+ 1. Crop protection:"""
27
+
28
+ print("\nGenerating test recommendation...")
29
+ response = llm_generator(
30
+ prompt,
31
+ max_new_tokens=100,
32
+ num_return_sequences=1,
33
+ temperature=0.7,
34
+ do_sample=True,
35
+ pad_token_id=50256
36
+ )
37
+
38
+ generated_text = response[0]['generated_text']
39
+ recommendations = generated_text[len(prompt):].strip()
40
+
41
+ print("\n✅ Generated Recommendation:")
42
+ print(recommendations)
43
+ print("\n✅ LLM integration test successful!")
44
+
45
+ except Exception as e:
46
+ print(f"❌ Error: {e}")