krushimitravit commited on
Commit
f3d917c
·
verified ·
1 Parent(s): 3f4fb13

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +527 -528
app.py CHANGED
@@ -1,529 +1,528 @@
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)
 
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-2.5-flash", # Requested
394
+ "gemini-2.5-flash-lite", # Requested
395
+ "gemma-3-27b-it", # Requested
396
+ "gemma-3-12b-it", # Requested
397
+ "gemma-3-4b-it", # Requested
398
+ "gemma-3-1b-it", # Requested
399
+ "gemini-2.0-flash-exp", # Likely intended "modern" fallback
400
+ ]
401
+
402
+ report_data = {} # Default
403
+
404
+ for model_name in models_to_try:
405
+ try:
406
+ print(f"DEBUG: Attempting model {model_name}...")
407
+ response = client.models.generate_content(
408
+ model=model_name,
409
+ contents=prompt
410
+ )
411
+
412
+ # Robust JSON extraction
413
+ raw_text = response.text
414
+ # print(f"DEBUG: Raw response: {raw_text[:100]}...") # Limit log size
415
+
416
+ start_idx = raw_text.find('{')
417
+ end_idx = raw_text.rfind('}') + 1
418
+
419
+ if start_idx != -1 and end_idx != 0:
420
+ json_text = raw_text[start_idx:end_idx]
421
+ report_data = json.loads(json_text)
422
+ print(f"DEBUG: Successfully parsed JSON from {model_name}")
423
+ break # Success
424
+ else:
425
+ print(f"DEBUG: No JSON found in response from {model_name}")
426
+ raise ValueError("No JSON definition found")
427
+
428
+ except json.JSONDecodeError as e:
429
+ print(f"CRITICAL: JSON parsing error with {model_name}: {e}")
430
+ continue
431
+ except Exception as e:
432
+ print(f"CRITICAL: Error calling {model_name}: {e}")
433
+ continue
434
+
435
+ if not report_data:
436
+ print("DEBUG: All models failed to generate valid report.")
437
+ return {"error": "Failed to generate a valid report from the AI model. All fallback models failed. Please try again later."}, []
438
+
439
+ return report_data, weather_profile
440
+
441
+ @app.route('/predict', methods=['POST'])
442
+ def predict():
443
+ print("----- PREDICT ROUTE HIT -----")
444
+ form_data = request.form.to_dict()
445
+ print(f"Form Data Received: {form_data}")
446
+ language = form_data.get("language", "English")
447
+
448
+ report_data, weather_profile = call_gemini_api(form_data, language)
449
+
450
+ # ... (Error handling remains similar but simplified for template)
451
+ if "error" in report_data:
452
+ 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=[])
453
+
454
+ # Build the HTML report dynamically from the JSON data
455
+ location = report_data.get('location_info', {})
456
+
457
+ # Convert markdown to HTML in text fields
458
+ if 'agricultural_inputs_analysis' in report_data:
459
+ report_data['agricultural_inputs_analysis'] = markdown_to_html(report_data['agricultural_inputs_analysis'])
460
+ if 'predicted_pest_damage_info' in report_data:
461
+ report_data['predicted_pest_damage_info'] = markdown_to_html(report_data['predicted_pest_damage_info'])
462
+ if 'pest_prediction_table' in report_data:
463
+ for pest in report_data['pest_prediction_table']:
464
+ if 'precautionary_measures' in pest:
465
+ pest['precautionary_measures'] = markdown_to_html(pest['precautionary_measures'])
466
+
467
+ # Generate summary for voice (short summary)
468
+ pest_table = report_data.get('pest_prediction_table', [])
469
+ summary = f"Pest Outbreak Report for {location.get('derived_location', 'your location')}. "
470
+ summary += (report_data.get('agricultural_inputs_analysis', '')[:200] + "... ")
471
+ if pest_table:
472
+ summary += f"Predicted pests: " + ', '.join([p.get('pest_name', '') for p in pest_table]) + ". "
473
+ summary += f"Severity: " + ', '.join([p.get('severity', '') for p in pest_table]) + ". "
474
+ summary += report_data.get('predicted_pest_damage_info', '')[:200]
475
+
476
+ # Generate audio file
477
+ lang_mapping = {
478
+ "English": "en", "Hindi": "hi", "Bengali": "bn", "Telugu": "te",
479
+ "Marathi": "mr", "Tamil": "ta", "Gujarati": "gu", "Urdu": "ur",
480
+ "Kannada": "kn", "Odia": "or", "Malayalam": "ml"
481
+ }
482
+ gtts_lang = lang_mapping.get(language, 'en')
483
+
484
+ audio_url = None
485
+ try:
486
+ tts = gTTS(summary, lang=gtts_lang)
487
+ # Sanitize filename
488
+ safe_lat = str(location.get('latitude', '0')).replace('.', '_')
489
+ safe_lon = str(location.get('longitude', '0')).replace('.', '_')
490
+ audio_filename = f"pest_report_{safe_lat}_{safe_lon}.mp3"
491
+
492
+ # Ensure directory exists
493
+ os.makedirs(app.config['AUDIO_FOLDER'], exist_ok=True)
494
+
495
+ audio_path = os.path.join(app.config['AUDIO_FOLDER'], audio_filename)
496
+ tts.save(audio_path)
497
+ audio_url = f"/static/audio/{audio_filename}"
498
+ except Exception as e:
499
+ print(f"Error generating audio: {e}")
500
+
501
+ return render_template(
502
+ 'results.html',
503
+ report_data=report_data,
504
+ location=location,
505
+ current_date=datetime.now().strftime("%B %d, %Y"),
506
+ audio_url=audio_url,
507
+ language_code=gtts_lang,
508
+ weather_profile=weather_profile
509
+ )
510
+
511
+ @app.route('/get_timeline_weather', methods=['GET'])
512
+ def get_timeline_weather():
513
+ """Returns the seasonal weather profile for the frontend timeline."""
514
+ lat = request.args.get('lat')
515
+ lon = request.args.get('lon')
516
+ start = request.args.get('start_date')
517
+ end = request.args.get('end_date')
518
+
519
+ if not all([lat, lon, start, end]):
520
+ return jsonify({"error": "Missing parameters"}), 400
521
+
522
+ _, profile = get_historical_weather_summary(lat, lon, start, end)
523
+ return jsonify(profile)
524
+
525
+
526
+
527
+ if __name__ == '__main__':
 
528
  app.run(debug=True, port=5001)