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) | |