Spaces:
Sleeping
Sleeping
| import os | |
| import json | |
| import re | |
| import logging | |
| from datetime import datetime, timedelta | |
| from flask import ( | |
| Flask, render_template, request, jsonify, session, | |
| make_response, redirect, url_for | |
| ) | |
| from werkzeug.middleware.proxy_fix import ProxyFix | |
| from flask_session import Session | |
| import google.generativeai as genai | |
| from dotenv import load_dotenv | |
| from cachelib import SimpleCache | |
| # Load environment | |
| load_dotenv() | |
| # Logging | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger("app") | |
| # Flask app | |
| app = Flask(__name__, static_folder="static", template_folder="templates") | |
| # Secret key | |
| app.secret_key = os.getenv("FLASK_SECRET_KEY", os.urandom(24)) | |
| # ProxyFix for deployments behind proxies (Hugging Face) | |
| app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1, x_prefix=1) | |
| # Session configuration (filesystem-based) -- persists server-side during container life | |
| # Flask-Session will write files under /tmp by default | |
| app.config["SESSION_TYPE"] = "filesystem" | |
| app.config["SESSION_FILE_DIR"] = os.getenv("SESSION_FILE_DIR", "/tmp/flask_session") | |
| app.config["SESSION_PERMANENT"] = True | |
| app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(days=7) | |
| # Cookie settings - browsers require SameSite=None + Secure for cross-site cookies | |
| disable_secure = os.getenv("DISABLE_SECURE_COOKIE", "0") in ("1", "true", "True") | |
| app.config["SESSION_COOKIE_SAMESITE"] = "None" | |
| app.config["SESSION_COOKIE_SECURE"] = False if disable_secure else True | |
| app.config["SESSION_COOKIE_NAME"] = os.getenv("SESSION_COOKIE_NAME", "hf_app_session") | |
| # Initialize server-side sessions | |
| Session(app) | |
| # Simple in-memory cache (for API results) | |
| cache = SimpleCache(threshold=200, default_timeout=60 * 60 * 24 * 7) # 7 days | |
| # Gemini API | |
| GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") | |
| if GEMINI_API_KEY: | |
| try: | |
| genai.configure(api_key=GEMINI_API_KEY) | |
| logger.info("Gemini client configured.") | |
| except Exception as e: | |
| logger.exception("Failed to configure Gemini client: %s", e) | |
| else: | |
| logger.warning("GEMINI_API_KEY not set. API calls will return placeholders.") | |
| # Supported languages and pest list (original) | |
| LANGUAGES = { | |
| "en": "English", "hi": "हिंदी (Hindi)", "bn": "বাংলা (Bengali)", "te": "తెలుగు (Telugu)", | |
| "mr": "मराठी (Marathi)", "ta": "தமிழ் (Tamil)", "gu": "ગુજરાતી (Gujarati)", "ur": "اردو (Urdu)", | |
| "kn": "ಕನ್ನಡ (Kannada)", "or": "ଓଡ଼ିଆ (Odia)", "ml": "മലയാളം (Malayalam)" | |
| } | |
| PESTS_DISEASES = [ | |
| {"id": 1, "name": "Fall Armyworm", "type": "pest", "crop": "Maize, Sorghum", "image_url": "/static/images/fall_armyworm.jpg"}, | |
| {"id": 2, "name": "Rice Blast", "type": "disease", "crop": "Rice", "image_url": "/static/images/rice_blast.jpg"}, | |
| {"id": 3, "name": "Aphids", "type": "pest", "crop": "Various crops", "image_url": "/static/images/aphids.jpg"}, | |
| {"id": 4, "name": "Powdery Mildew", "type": "disease", "crop": "Wheat, Vegetables", "image_url": "/static/images/powdery_mildew.jpg"}, | |
| {"id": 5, "name": "Stem Borer", "type": "pest", "crop": "Rice, Sugarcane", "image_url": "/static/images/stem_borer.jpg"}, | |
| {"id": 6, "name": "Yellow Mosaic Virus", "type": "disease", "crop": "Pulses", "image_url": "/static/images/yellow_mosaic.jpg"}, | |
| {"id": 7, "name": "Brown Planthopper", "type": "pest", "crop": "Rice", "image_url": "/static/images/brown_planthopper.jpg"}, | |
| {"id": 8, "name": "Bacterial Leaf Blight", "type": "disease", "crop": "Rice", "image_url": "/static/images/bacterial_leaf_blight.jpg"}, | |
| {"id": 9, "name": "Jassids", "type": "pest", "crop": "Cotton, Maize", "image_url": "/static/images/jassids.jpg"}, | |
| {"id": 10, "name": "Downy Mildew", "type": "disease", "crop": "Grapes, Onion", "image_url": "/static/images/downy_mildew.jpg"}, | |
| {"id": 11, "name": "Whitefly", "type": "pest", "crop": "Tomato, Cotton", "image_url": "/static/images/whitefly.jpg"}, | |
| {"id": 12, "name": "Fusarium Wilt", "type": "disease", "crop": "Banana, Tomato", "image_url": "/static/images/fusarium_wilt.jpg"} | |
| ] | |
| # JSON extraction helper (resilient to noisy model output) | |
| def extract_json_from_response(content: str): | |
| try: | |
| return json.loads(content) | |
| except json.JSONDecodeError: | |
| json_match = re.search(r'```json(.*?)```', content, re.DOTALL) | |
| if json_match: | |
| json_str = json_match.group(1).strip() | |
| else: | |
| brace_match = re.search(r'(\{(?:.|\n)*\})', content) | |
| json_str = brace_match.group(1) if brace_match else content | |
| json_str = json_str.replace('```json', '').replace('```', '').strip() | |
| json_str = re.sub(r',(\s*[\]\}])', r'\1', json_str) | |
| try: | |
| return json.loads(json_str) | |
| except json.JSONDecodeError as e: | |
| logger.error("JSON parsing error after cleanup: %s\nSnippet: %s", e, content[:1000]) | |
| raise ValueError("Failed to parse JSON response from API") | |
| def index(): | |
| # Use session first, cookie fallback. This sets HTML lang attribute properly. | |
| language = session.get("language") or request.cookies.get("language") or "en" | |
| session["language"] = language | |
| session.permanent = True | |
| return render_template("index.html", pests_diseases=PESTS_DISEASES, languages=LANGUAGES, current_language=language) | |
| # Keep this for compatibility; not required if client only sets cookie & passes lang in fetch | |
| def set_language(): | |
| language = request.form.get("language", "en") | |
| if language not in LANGUAGES: | |
| language = "en" | |
| session["language"] = language | |
| session.permanent = True | |
| logger.info("Language set to %s in session", language) | |
| resp = make_response(redirect(url_for("index"))) | |
| max_age = 60 * 60 * 24 * 7 | |
| secure_flag = False if disable_secure else True | |
| resp.set_cookie("language", language, max_age=max_age, secure=secure_flag, samesite="None", httponly=False, path="/") | |
| return resp | |
| def get_details(pest_id): | |
| # Priority: lang query param > session > cookie > default | |
| lang_param = request.args.get("lang") | |
| language = lang_param or session.get("language") or request.cookies.get("language") or "en" | |
| session["language"] = language | |
| session.permanent = True | |
| pest = next((p for p in PESTS_DISEASES if p["id"] == pest_id), None) | |
| if not pest: | |
| return jsonify({"error": "Not found"}), 404 | |
| cache_key = f"pest_{pest_id}_{language}" | |
| cached = cache.get(cache_key) | |
| if cached: | |
| logger.info("Cache hit for pest %s (%s)", pest_id, language) | |
| return jsonify(cached) | |
| if not GEMINI_API_KEY: | |
| logger.warning("GEMINI_API_KEY missing; returning placeholder details") | |
| result = { | |
| **pest, | |
| "details": { | |
| "description": {"title": "Description", "text": f"Placeholder description for {pest['name']} ({language})."}, | |
| "lifecycle": {"title": "Lifecycle", "text": "N/A"}, | |
| "symptoms": {"title": "Symptoms", "text": "N/A"}, | |
| "impact": {"title": "Economic Impact", "text": "N/A"}, | |
| "management": {"title": "Management", "text": "N/A"}, | |
| "prevention": {"title": "Prevention", "text": "N/A"}, | |
| }, | |
| "language": language, | |
| "cached_at": datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") | |
| } | |
| cache.set(cache_key, result) | |
| return jsonify(result) | |
| # Build prompt for Gemini | |
| lang_instructions = { | |
| "en": "Respond in English", | |
| "hi": "हिंदी में जवाब दें (Respond in Hindi)", | |
| "bn": "বাংলায় উত্তর দিন (Respond in Bengali)", | |
| "te": "తెలుగులో సమాధానం ఇవ్వండి (Respond in Telugu)", | |
| "mr": "मराठीत उत्तर द्या (Respond in Marathi)", | |
| "ta": "தமிழில் பதிலளிக்கவும் (Respond in Tamil)", | |
| "gu": "ગુજરાતીમાં જવાબ આપો (Respond in Gujarati)", | |
| "ur": "اردو میں جواب دیں (Respond in Urdu)", | |
| "kn": "ಕನ್ನಡದಲ್ಲಿ ಉತ್ತರಿಸಿ (Respond in Kannada)", | |
| "or": "ଓଡ଼ିଆରେ ଉତ୍ତର ଦିଅନ୍ତୁ (Respond in Odia)", | |
| "ml": "മലയാളത്തിൽ മറുപടി നൽകുക (Respond in Malayalam)" | |
| } | |
| prompt = f""" | |
| {lang_instructions.get(language, 'Respond in English')} | |
| Provide detailed information about the agricultural {pest['type']} '{pest['name']}' affecting '{pest['crop']}' in India. | |
| Include these sections: Description and identification, Lifecycle and spread, Damage symptoms, Economic impact, Management and control (organic and chemical), Preventive measures. | |
| Format the response as a single, strictly valid JSON object with keys: "description", "lifecycle", "symptoms", "impact", "management", "prevention". | |
| Each key must contain an object with "title" and "text" fields. | |
| """ | |
| try: | |
| model = genai.GenerativeModel("gemini-1.5-flash") | |
| response = model.generate_content(prompt) | |
| text = getattr(response, "text", "") or str(response) | |
| detailed_info = extract_json_from_response(text) | |
| required_keys = ["description", "lifecycle", "symptoms", "impact", "management", "prevention"] | |
| for key in required_keys: | |
| if key not in detailed_info or "text" not in detailed_info.get(key, {}): | |
| detailed_info[key] = {"title": key.capitalize(), "text": "Information could not be generated for this section."} | |
| logger.warning("Missing section in API response: %s", key) | |
| result = { | |
| **pest, | |
| "details": detailed_info, | |
| "language": language, | |
| "cached_at": datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") | |
| } | |
| cache.set(cache_key, result) | |
| logger.info("Cached details for pest %s (%s)", pest_id, language) | |
| return jsonify(result) | |
| except Exception as e: | |
| logger.exception("Error fetching from Gemini: %s", e) | |
| return jsonify({"error": "Failed to fetch information", "message": str(e)}), 500 | |
| if __name__ == "__main__": | |
| port = int(os.environ.get("PORT", 7860)) | |
| host = os.environ.get("HOST", "0.0.0.0") | |
| app.run(host=host, port=port) | |