Spaces:
Sleeping
Sleeping
| from flask import Flask, render_template, request, Response | |
| import requests | |
| import datetime | |
| from twilio.rest import Client # For Twilio integration | |
| from geopy.geocoders import Photon | |
| from geopy.exc import GeocoderTimedOut, GeocoderServiceError | |
| from transformers import pipeline | |
| import warnings | |
| # Suppress warnings | |
| warnings.filterwarnings('ignore') | |
| app = Flask(__name__) | |
| # Initialize Photon geocoder (no API key required) | |
| photon_geolocator = Photon(user_agent="MyWeatherApp", timeout=10) | |
| # Initialize LLM for personalized recommendations | |
| print("Loading LLM model for personalized recommendations...") | |
| try: | |
| llm_generator = pipeline( | |
| "text-generation", | |
| model="distilgpt2", # Lightweight model | |
| max_length=200, | |
| device=-1 # CPU | |
| ) | |
| print("✅ LLM model loaded successfully!") | |
| except Exception as e: | |
| print(f"⚠️ LLM model loading failed: {e}") | |
| llm_generator = None | |
| def parse_iso_datetime(timestr): | |
| """ | |
| Parse an ISO8601 datetime string (removing any trailing 'Z'). | |
| """ | |
| if timestr.endswith("Z"): | |
| timestr = timestr[:-1] | |
| return datetime.datetime.fromisoformat(timestr) | |
| def find_closest_hour_index(hour_times, current_time_str): | |
| """ | |
| Find the index in hour_times that is closest to the current_time_str. | |
| """ | |
| if not hour_times: | |
| return None | |
| dt_current = parse_iso_datetime(current_time_str) | |
| min_diff = None | |
| best_index = None | |
| for i, ht in enumerate(hour_times): | |
| dt_ht = parse_iso_datetime(ht) | |
| diff = abs((dt_ht - dt_current).total_seconds()) | |
| if min_diff is None or diff < min_diff: | |
| min_diff = diff | |
| best_index = i | |
| return best_index | |
| def get_weather_icon(code): | |
| """Map the Open-Meteo weathercode to an emoji icon.""" | |
| if code == 0: | |
| return "☀️" # Clear sky | |
| elif code in [1, 2, 3]: | |
| return "⛅" | |
| elif code in [45, 48]: | |
| return "🌫️" | |
| elif code in [51, 53, 55]: | |
| return "🌦️" | |
| elif code in [56, 57]: | |
| return "🌧️" | |
| elif code in [61, 63, 65]: | |
| return "🌧️" | |
| elif code in [66, 67]: | |
| return "🌧️" | |
| elif code in [71, 73, 75, 77]: | |
| return "❄️" | |
| elif code in [80, 81, 82]: | |
| return "🌦️" | |
| elif code in [85, 86]: | |
| return "❄️" | |
| elif code in [95, 96, 99]: | |
| return "⛈️" | |
| else: | |
| return "❓" | |
| def get_weather_description(code): | |
| """Short textual description for the weathercode.""" | |
| descriptions = { | |
| 0: "Clear sky", | |
| 1: "Mainly clear", | |
| 2: "Partly cloudy", | |
| 3: "Overcast", | |
| 45: "Fog", | |
| 48: "Depositing rime fog", | |
| 51: "Light drizzle", | |
| 53: "Moderate drizzle", | |
| 55: "Dense drizzle", | |
| 56: "Freezing drizzle", | |
| 57: "Freezing drizzle", | |
| 61: "Slight rain", | |
| 63: "Moderate rain", | |
| 65: "Heavy rain", | |
| 66: "Freezing rain", | |
| 67: "Freezing rain", | |
| 71: "Slight snow fall", | |
| 73: "Moderate snow fall", | |
| 75: "Heavy snow fall", | |
| 77: "Snow grains", | |
| 80: "Slight rain showers", | |
| 81: "Moderate rain showers", | |
| 82: "Violent rain showers", | |
| 85: "Slight snow showers", | |
| 86: "Heavy snow showers", | |
| 95: "Thunderstorm", | |
| 96: "Thunderstorm w/ slight hail", | |
| 99: "Thunderstorm w/ heavy hail" | |
| } | |
| return descriptions.get(code, "Unknown") | |
| def reverse_geocode(lat, lon): | |
| """ | |
| Use Photon (via geopy) to convert latitude and longitude into a human-readable address. | |
| If the geocoding fails, returns a fallback string with the coordinates. | |
| """ | |
| try: | |
| location = photon_geolocator.reverse((lat, lon), exactly_one=True) | |
| if location: | |
| return location.address | |
| except (GeocoderTimedOut, GeocoderServiceError) as e: | |
| print("Photon reverse geocode error:", e) | |
| return f"Lat: {lat}, Lon: {lon}" | |
| # ----------------------------- | |
| # LLM-Powered Personalized Recommendations | |
| # ----------------------------- | |
| def generate_personalized_recommendations(weather_summary, location_address, critical_days, warning_days): | |
| """ | |
| Generate personalized agricultural recommendations using LLM based on weather and location. | |
| """ | |
| if llm_generator is None: | |
| return None | |
| try: | |
| # Extract region info from location | |
| region_parts = location_address.split(',') | |
| region = region_parts[-1].strip() if len(region_parts) > 0 else "your region" | |
| # Determine weather condition | |
| weather_condition = "cloudy conditions" | |
| if critical_days: | |
| if len(critical_days) > 3: | |
| weather_condition = "severe weather alerts" | |
| else: | |
| weather_condition = "critical weather conditions" | |
| elif warning_days: | |
| weather_condition = "warning-level weather" | |
| # Create a better structured prompt | |
| prompt = f"""Agricultural advice for farmers in {region}: | |
| Weather: {weather_condition} expected | |
| Days affected: {len(critical_days) + len(warning_days)} days | |
| Farming recommendations: | |
| 1. Crop care:""" | |
| # Generate recommendations | |
| response = llm_generator( | |
| prompt, | |
| max_new_tokens=80, | |
| num_return_sequences=1, | |
| temperature=0.8, | |
| do_sample=True, | |
| pad_token_id=50256, | |
| repetition_penalty=1.5 # Reduce repetition | |
| ) | |
| generated_text = response[0]['generated_text'] | |
| # Extract only the generated part | |
| recommendations = generated_text[len(prompt):].strip() | |
| # Clean up the output | |
| lines = recommendations.split('\n') | |
| clean_lines = [] | |
| seen = set() | |
| for line in lines[:5]: # Max 5 lines | |
| line = line.strip() | |
| # Skip empty, repetitive, or nonsensical lines | |
| if line and len(line) > 10 and line not in seen: | |
| # Check for repetition patterns | |
| if not any(line.count(word) > 2 for word in line.split()): | |
| clean_lines.append(line) | |
| seen.add(line) | |
| if clean_lines: | |
| recommendations = ' '.join(clean_lines) | |
| # Limit length | |
| if len(recommendations) > 180: | |
| recommendations = recommendations[:177] + "..." | |
| return recommendations | |
| else: | |
| # Fallback to rule-based if LLM output is poor | |
| return generate_rule_based_recommendations(weather_condition, region, critical_days, warning_days) | |
| except Exception as e: | |
| print(f"LLM generation error: {e}") | |
| return generate_rule_based_recommendations("cloudy conditions", "your region", critical_days, warning_days) | |
| def generate_rule_based_recommendations(weather_condition, region, critical_days, warning_days): | |
| """ | |
| Fallback rule-based recommendations when LLM fails or produces poor output. | |
| """ | |
| if critical_days and len(critical_days) > 0: | |
| return f"For {region}: Secure crops and equipment. Postpone spraying. Monitor drainage systems. Harvest ready crops before severe weather." | |
| elif warning_days and len(warning_days) > 0: | |
| if "cloudy" in weather_condition.lower() or "overcast" in weather_condition.lower(): | |
| return f"For {region}: Reduce irrigation due to lower evaporation. Monitor for fungal diseases. Apply preventive fungicides if needed." | |
| else: | |
| return f"For {region}: Adjust irrigation schedule. Monitor soil moisture. Delay non-essential field operations." | |
| else: | |
| return f"For {region}: Continue normal farming operations. Monitor weather updates regularly." | |
| # ----------------------------- | |
| # Twilio WhatsApp Integration | |
| # ----------------------------- | |
| def check_and_collect_alerts(forecast_list): | |
| """ | |
| Check the forecast for hazardous weather conditions and collect detailed alert messages. | |
| """ | |
| alerts = [] | |
| critical_days = [] | |
| warning_days = [] | |
| for day in forecast_list: | |
| day_alerts = [] | |
| severity = "INFO" | |
| # Temperature Analysis | |
| if day.get("tmax") and day.get("tmin"): | |
| tmax = day["tmax"] | |
| tmin = day["tmin"] | |
| if tmax > 40: | |
| day_alerts.append(f"🌡️ Extreme Heat: {tmax}°C - High risk of crop stress and water loss") | |
| severity = "CRITICAL" | |
| elif tmax > 35: | |
| day_alerts.append(f"🌡️ High Temperature: {tmax}°C - Increase irrigation frequency") | |
| severity = "WARNING" | |
| if tmin < 5: | |
| day_alerts.append(f"❄️ Frost Risk: {tmin}°C - Protect sensitive crops") | |
| severity = "CRITICAL" | |
| elif tmin < 10: | |
| day_alerts.append(f"🌡️ Cold Night: {tmin}°C - Monitor young plants") | |
| severity = "WARNING" | |
| # Temperature swing | |
| temp_diff = tmax - tmin | |
| if temp_diff > 20: | |
| day_alerts.append(f"📊 Large temperature swing: {temp_diff}°C - May stress plants") | |
| # Weather Condition Analysis | |
| desc = day.get("desc", "").lower() | |
| if "thunderstorm" in desc or "heavy" in desc: | |
| day_alerts.append(f"⛈️ Severe Weather: {day['desc']} - Secure equipment, delay spraying") | |
| severity = "CRITICAL" | |
| elif "rain" in desc or "drizzle" in desc: | |
| day_alerts.append(f"🌧️ Rainfall Expected: {day['desc']} - Postpone irrigation, avoid field work") | |
| severity = "WARNING" | |
| elif "overcast" in desc or "cloudy" in desc: | |
| day_alerts.append(f"☁️ Cloudy Conditions: {day['desc']} - Reduced photosynthesis, monitor for diseases") | |
| severity = "INFO" | |
| # Compile day alert if any conditions met | |
| if day_alerts: | |
| day_header = f"\n*{day['day_name']} ({day['date_str']})*" | |
| if severity == "CRITICAL": | |
| day_header = f"🚨 {day_header} - CRITICAL" | |
| critical_days.append(day['day_name']) | |
| elif severity == "WARNING": | |
| day_header = f"⚠️ {day_header} - WARNING" | |
| warning_days.append(day['day_name']) | |
| alert_text = day_header + "\n" + "\n".join(f" • {alert}" for alert in day_alerts) | |
| # Add temperature range | |
| if day.get("tmax") and day.get("tmin"): | |
| alert_text += f"\n 📈 Temp Range: {day['tmin']}°C - {day['tmax']}°C" | |
| if day.get("morning_temp"): | |
| alert_text += f"\n 🌅 Morning: {day['morning_temp']}°C" | |
| if day.get("evening_temp"): | |
| alert_text += f"\n 🌆 Evening: {day['evening_temp']}°C" | |
| alerts.append(alert_text) | |
| return alerts, critical_days, warning_days | |
| def send_whatsapp_message(message, location, location_address, critical_days, warning_days): | |
| """ | |
| Send a WhatsApp message using Twilio API with enhanced agricultural insights. | |
| """ | |
| google_maps_url = f"https://www.google.com/maps?q={location[0]},{location[1]}" | |
| # Build concise severity summary | |
| severity_summary = "" | |
| if critical_days: | |
| severity_summary += f"🚨 {len(critical_days)} CRITICAL: {', '.join(critical_days[:3])}\n" | |
| if warning_days: | |
| severity_summary += f"⚠️ {len(warning_days)} WARNING: {', '.join(warning_days[:3])}\n" | |
| # Generate personalized LLM recommendations (shortened) | |
| weather_summary = f"{len(critical_days)} critical, {len(warning_days)} warning days" | |
| llm_recommendations = generate_personalized_recommendations( | |
| weather_summary, | |
| location_address, | |
| critical_days, | |
| warning_days | |
| ) | |
| # Shorten LLM recommendations if too long | |
| if llm_recommendations and len(llm_recommendations) > 200: | |
| llm_recommendations = llm_recommendations[:197] + "..." | |
| # Build concise message | |
| message_content = ( | |
| f"🌾 *WEATHER ALERT* 🌾\n\n" | |
| f"{severity_summary}\n" | |
| ) | |
| # Add condensed forecast (max 3 days) | |
| alert_lines = message.strip().split('\n\n')[:3] | |
| for alert in alert_lines: | |
| # Shorten each alert | |
| lines = alert.split('\n') | |
| if lines: | |
| message_content += f"{lines[0]}\n" # Just the header | |
| if len(lines) > 1: | |
| message_content += f"{lines[1][:80]}\n" # First detail only | |
| message_content += "\n" | |
| # Add AI recommendations if available | |
| if llm_recommendations: | |
| message_content += f"🤖 *AI ADVICE:*\n{llm_recommendations}\n\n" | |
| # Add critical actions only | |
| if critical_days: | |
| message_content += ( | |
| f"🚨 *URGENT:*\n" | |
| f"• Secure equipment\n" | |
| f"• Harvest ready crops\n" | |
| f"• Protect livestock\n\n" | |
| ) | |
| elif warning_days: | |
| message_content += ( | |
| f"⚠️ *ACTIONS:*\n" | |
| f"• Adjust irrigation\n" | |
| f"• Monitor soil moisture\n" | |
| f"• Delay field work\n\n" | |
| ) | |
| # Add location | |
| message_content += ( | |
| f"📍 {location_address}\n" | |
| f"🗺️ {google_maps_url}\n\n" | |
| f"_Weather Forecast for Farmers_" | |
| ) | |
| # Ensure under 1600 characters | |
| if len(message_content) > 1590: | |
| message_content = message_content[:1587] + "..." | |
| account_sid = 'ACe45f7038c5338a153d1126ca6d547c84' | |
| auth_token = '48b9eea898885ef395d48edc74924340' | |
| client = Client(account_sid, auth_token) | |
| try: | |
| msg = client.messages.create( | |
| from_='whatsapp:+14155238886', | |
| body=message_content, | |
| to='whatsapp:+919763059811' | |
| ) | |
| print(f"✅ WhatsApp sent! SID: {msg.sid}, Length: {len(message_content)} chars") | |
| except Exception as e: | |
| print(f"❌ WhatsApp error: {e}") | |
| print(f"Message length was: {len(message_content)} characters") | |
| def index(): | |
| # Default coordinates | |
| default_lat = 18.5196 | |
| default_lon = 73.8553 | |
| if request.method == "POST": | |
| try: | |
| lat = float(request.form.get("lat", default_lat)) | |
| lon = float(request.form.get("lon", default_lon)) | |
| except ValueError: | |
| lat, lon = default_lat, default_lon | |
| else: | |
| lat = float(request.args.get("lat", default_lat)) | |
| lon = float(request.args.get("lon", default_lon)) | |
| location_address = reverse_geocode(lat, lon) | |
| # Call Open-Meteo API for forecast data | |
| url = "https://api.open-meteo.com/v1/forecast" | |
| params = { | |
| "latitude": lat, | |
| "longitude": lon, | |
| "hourly": ( | |
| "temperature_2m,relative_humidity_2m,precipitation," | |
| "cloudcover,windspeed_10m,pressure_msl,soil_moisture_3_to_9cm,uv_index" | |
| ), | |
| "daily": ( | |
| "weathercode,temperature_2m_max,temperature_2m_min," | |
| "sunrise,sunset,uv_index_max" | |
| ), | |
| "current_weather": True, | |
| "forecast_days": 10, | |
| "timezone": "auto" | |
| } | |
| resp = requests.get(url, params=params) | |
| data = resp.json() | |
| timezone = data.get("timezone", "Local") | |
| current_weather = data.get("current_weather", {}) | |
| current_temp = current_weather.get("temperature") | |
| current_time = current_weather.get("time") | |
| current_code = current_weather.get("weathercode") | |
| current_icon = get_weather_icon(current_code) | |
| current_desc = get_weather_description(current_code) | |
| current_wind_speed = current_weather.get("windspeed", 0.0) | |
| current_wind_dir = current_weather.get("winddirection", 0) | |
| if current_time: | |
| dt_current = parse_iso_datetime(current_time) | |
| current_time_formatted = dt_current.strftime("%A, %b %d, %Y %I:%M %p") | |
| else: | |
| current_time_formatted = "" | |
| hourly_data = data.get("hourly", {}) | |
| hour_times = hourly_data.get("time", []) | |
| hour_temp = hourly_data.get("temperature_2m", []) | |
| hour_humidity = hourly_data.get("relative_humidity_2m", []) | |
| hour_precip = hourly_data.get("precipitation", []) | |
| hour_clouds = hourly_data.get("cloudcover", []) | |
| hour_wind = hourly_data.get("windspeed_10m", []) | |
| hour_pressure = hourly_data.get("pressure_msl", []) | |
| hour_soil = hourly_data.get("soil_moisture_3_to_9cm", []) | |
| hour_uv = hourly_data.get("uv_index", []) | |
| current_index = None | |
| if current_time: | |
| current_index = find_closest_hour_index(hour_times, current_time) | |
| feels_like = current_temp | |
| if current_index is not None and current_index < len(hour_humidity): | |
| h = hour_humidity[current_index] | |
| feels_like = round(current_temp - 0.2 * (100 - h) / 10, 1) | |
| today_highlights = {} | |
| if current_index is not None: | |
| today_highlights["humidity"] = hour_humidity[current_index] if current_index < len(hour_humidity) else None | |
| today_highlights["precipitation"] = hour_precip[current_index] if current_index < len(hour_precip) else None | |
| today_highlights["clouds"] = hour_clouds[current_index] if current_index < len(hour_clouds) else None | |
| today_highlights["windspeed"] = hour_wind[current_index] if current_index < len(hour_wind) else None | |
| today_highlights["pressure"] = hour_pressure[current_index] if current_index < len(hour_pressure) else None | |
| today_highlights["soil_moisture"] = hour_soil[current_index] if current_index < len(hour_soil) else None | |
| today_highlights["uv_index"] = hour_uv[current_index] if current_index < len(hour_uv) else None | |
| else: | |
| for k in ["humidity", "precipitation", "cloudcover", "windspeed", "pressure", "soil_moisture", "uv_index"]: | |
| today_highlights[k] = None | |
| daily_data = data.get("daily", {}) | |
| daily_sunrise = daily_data.get("sunrise", []) | |
| daily_sunset = daily_data.get("sunset", []) | |
| if len(daily_sunrise) > 0: | |
| today_highlights["sunrise"] = daily_sunrise[0][11:16] | |
| else: | |
| today_highlights["sunrise"] = None | |
| if len(daily_sunset) > 0: | |
| today_highlights["sunset"] = daily_sunset[0][11:16] | |
| else: | |
| today_highlights["sunset"] = None | |
| daily_times = daily_data.get("time", []) | |
| daily_codes = daily_data.get("weathercode", []) | |
| daily_tmax = daily_data.get("temperature_2m_max", []) | |
| daily_tmin = daily_data.get("temperature_2m_min", []) | |
| forecast_list = [] | |
| def get_hour_temp(date_str, hour_str): | |
| target = date_str + "T" + hour_str + ":00" | |
| best_idx = None | |
| best_diff = None | |
| dt_target = parse_iso_datetime(target) | |
| for i, ht in enumerate(hour_times): | |
| dt_ht = parse_iso_datetime(ht) | |
| diff = abs((dt_ht - dt_target).total_seconds()) | |
| if best_diff is None or diff < best_diff: | |
| best_diff = diff | |
| best_idx = i | |
| if best_idx is not None and best_idx < len(hour_temp): | |
| return hour_temp[best_idx] | |
| return None | |
| for i in range(len(daily_times)): | |
| date_str = daily_times[i] | |
| dt_obj = parse_iso_datetime(date_str) | |
| day_name = dt_obj.strftime("%A") | |
| short_date = dt_obj.strftime("%b %d") | |
| code = daily_codes[i] if i < len(daily_codes) else None | |
| icon = get_weather_icon(code) | |
| desc = get_weather_description(code) | |
| tmax = daily_tmax[i] if i < len(daily_tmax) else None | |
| tmin = daily_tmin[i] if i < len(daily_tmin) else None | |
| avg_temp = round((tmax + tmin) / 2, 1) if tmax is not None and tmin is not None else None | |
| morning_temp = get_hour_temp(date_str, "09") | |
| evening_temp = get_hour_temp(date_str, "21") | |
| sr = daily_sunrise[i][11:16] if i < len(daily_sunrise) else None | |
| ss = daily_sunset[i][11:16] if i < len(daily_sunset) else None | |
| forecast_list.append({ | |
| "day_name": day_name, | |
| "date_str": short_date, | |
| "icon": icon, | |
| "desc": desc, | |
| "avg_temp": avg_temp, | |
| "morning_temp": morning_temp, | |
| "evening_temp": evening_temp, | |
| "sunrise": sr, | |
| "sunset": ss, | |
| "tmax": tmax, | |
| "tmin": tmin | |
| }) | |
| alerts, critical_days, warning_days = check_and_collect_alerts(forecast_list) | |
| # Generate AI recommendations for frontend display | |
| ai_recommendations = None | |
| if alerts or critical_days or warning_days: | |
| weather_summary = f"{len(critical_days)} critical days, {len(warning_days)} warning days" | |
| ai_recommendations = generate_personalized_recommendations( | |
| weather_summary, | |
| location_address, | |
| critical_days, | |
| warning_days | |
| ) | |
| # Send WhatsApp alerts if needed | |
| if alerts: | |
| alert_message = "\n".join(alerts) | |
| send_whatsapp_message(alert_message, (lat, lon), location_address, critical_days, warning_days) | |
| alerts_sent = True | |
| else: | |
| alerts_sent = False | |
| return render_template( | |
| "index.html", | |
| lat=lat, | |
| lon=lon, | |
| location_address=location_address, | |
| current_temp=current_temp, | |
| current_icon=current_icon, | |
| current_desc=current_desc, | |
| current_time=current_time_formatted, | |
| current_wind_speed=current_wind_speed, | |
| current_wind_dir=current_wind_dir, | |
| feels_like=feels_like, | |
| today_highlights=today_highlights, | |
| forecast_list=forecast_list, | |
| timezone=timezone, | |
| alerts_sent=alerts_sent, | |
| ai_recommendations=ai_recommendations, | |
| critical_days=critical_days, | |
| warning_days=warning_days | |
| ) | |
| if __name__ == "__main__": | |
| app.run(debug=True,port=5001) |