import os from flask import Flask, render_template, request, jsonify, redirect, url_for, flash, session import requests from werkzeug.utils import secure_filename import google.generativeai as genai import base64 import json from datetime import datetime, timedelta import threading import time from gtts import gTTS import dotenv import markdown from openai import OpenAI from typing import Optional, Dict, Any # Load environment variables if a .env file is present dotenv.load_dotenv() def markdown_to_html(text): """Convert markdown text to HTML for proper rendering.""" if not text: return text return markdown.markdown(text, extensions=['nl2br']) # --- Configuration --- # Ensure you have set these as environment variables in your deployment environment GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY") GOOGLE_CX = os.getenv("GOOGLE_CX") NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY") # Validate API keys with helpful messages if not GEMINI_API_KEY: print("WARNING: GEMINI_API_KEY not found. Will rely on NVIDIA fallback if available.") print(" → For Hugging Face: Set this in Space Settings > Repository Secrets") if not GOOGLE_API_KEY or not GOOGLE_CX: print("WARNING: GOOGLE_API_KEY or GOOGLE_CX is not set. Web and product search features will be disabled.") print(" → For Hugging Face: Set these in Space Settings > Repository Secrets") if not NVIDIA_API_KEY: print("WARNING: NVIDIA_API_KEY not set. NVIDIA fallback will not be available.") print(" → For Hugging Face: Set this in Space Settings > Repository Secrets") # Configure Gemini only if API key is available if GEMINI_API_KEY: genai.configure(api_key=GEMINI_API_KEY) else: print("⚠️ Gemini API not configured. Application will use NVIDIA fallback only.") # --- ENHANCED MODEL CONFIGURATION --- GEMINI_MODELS = [ {"name": "gemini-2.0-flash-exp", "max_retries": 2, "timeout": 30, "description": "Latest experimental"}, {"name": "gemini-1.5-pro-latest", "max_retries": 2, "timeout": 45, "description": "Most capable"}, {"name": "gemini-1.5-flash", "max_retries": 3, "timeout": 20, "description": "Fast and reliable"}, {"name": "gemini-1.5-flash-8b", "max_retries": 3, "timeout": 15, "description": "Lightweight"}, ] NVIDIA_MODELS = [ {"name": "meta/llama-3.2-90b-vision-instruct", "max_retries": 2, "timeout": 40, "description": "High capability"}, {"name": "meta/llama-3.2-11b-vision-instruct", "max_retries": 2, "timeout": 30, "description": "Balanced"}, ] app = Flask(__name__) app.secret_key = os.getenv("SECRET_KEY", "a-strong-default-secret-key") # Configure folders UPLOAD_FOLDER = 'static/uploads' AUDIO_FOLDER = 'static/audio' ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg'} # Create directories with better error handling for Hugging Face try: os.makedirs(UPLOAD_FOLDER, exist_ok=True) os.makedirs(AUDIO_FOLDER, exist_ok=True) print(f"✓ Created directories: {UPLOAD_FOLDER}, {AUDIO_FOLDER}") except OSError as e: print(f"⚠️ Warning: Could not create directories: {e}") print(" → This may be normal on Hugging Face Spaces with read-only filesystem") app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER def allowed_file(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS def encode_image(image_path): with open(image_path, "rb") as image_file: return base64.b64encode(image_file.read()).decode('utf-8') def retry_with_backoff(func, max_retries=3, initial_delay=1): """ Retry a function with exponential backoff. Args: func: Function to retry max_retries: Maximum number of retry attempts initial_delay: Initial delay in seconds (doubles each retry) Returns: Result of successful function call Raises: Last exception if all retries fail """ last_exception = None for attempt in range(max_retries): try: return func() except Exception as e: last_exception = e if attempt == max_retries - 1: raise delay = initial_delay * (2 ** attempt) print(f" >> Retry {attempt + 1}/{max_retries} after {delay}s delay (Error: {type(e).__name__})") time.sleep(delay) raise last_exception def analyze_with_gemini(image_path, prompt, model_config): """ Analyze with a specific Gemini model with retry logic. Args: image_path: Path to the image file prompt: Text prompt for analysis model_config: Dict with model name, retries, timeout Returns: Response text or None if failed """ model_name = model_config["name"] max_retries = model_config.get("max_retries", 2) def _attempt(): print(f" >> Attempting Gemini model: {model_name} ({model_config.get('description', '')})") model = genai.GenerativeModel(model_name) image_parts = [{"mime_type": "image/jpeg", "data": encode_image(image_path)}] response = model.generate_content( [prompt] + image_parts, generation_config={ "temperature": 0.2, "max_output_tokens": 2048, } ) if not response or not response.text: raise ValueError("Empty response from model") return response.text try: return retry_with_backoff(_attempt, max_retries=max_retries) except Exception as e: error_type = type(e).__name__ error_msg = str(e)[:100] print(f" >> FAILED {model_name}: {error_type}: {error_msg}") return None def analyze_with_nvidia(image_path, prompt, model_config): """ Analyze image using NVIDIA's API via OpenAI-compatible client with retry logic. Args: image_path: Path to the image file prompt: Text prompt for analysis model_config: Dict with model name, retries, timeout Returns: Response text or None if failed """ model_name = model_config["name"] max_retries = model_config.get("max_retries", 2) timeout = model_config.get("timeout", 30) if not NVIDIA_API_KEY: print("NVIDIA API key not available.") return None def _attempt(): print(f" >> Attempting NVIDIA model: {model_name} ({model_config.get('description', '')})") client = OpenAI( base_url="https://integrate.api.nvidia.com/v1", api_key=NVIDIA_API_KEY ) base64_image = encode_image(image_path) completion = client.chat.completions.create( model=model_name, messages=[{ "role": "user", "content": [ {"type": "text", "text": prompt}, { "type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"} } ] }], max_tokens=2000, temperature=0.2, timeout=timeout ) response_text = completion.choices[0].message.content if not response_text: raise ValueError("Empty response from NVIDIA") return response_text try: return retry_with_backoff(_attempt, max_retries=max_retries) except Exception as e: error_type = type(e).__name__ error_msg = str(e)[:100] print(f" >> FAILED NVIDIA {model_name}: {error_type}: {error_msg}") return None def get_web_pesticide_info(disease, plant_type="Unknown"): """Fetch pesticide information from web sources.""" if not GOOGLE_API_KEY or not GOOGLE_CX: print("Skipping web search: Google API credentials not set.") return None query = f"site:agrowon.esakal.com {disease} in {plant_type}" url = "https://www.googleapis.com/customsearch/v1" params = {"key": GOOGLE_API_KEY, "cx": GOOGLE_CX, "q": query, "num": 1} try: response = requests.get(url, params=params, timeout=10) response.raise_for_status() data = response.json() if "items" in data and data["items"]: item = data["items"][0] return { "title": item.get("title", "No title"), "link": item.get("link", "#"), "summary": item.get("snippet", "No summary available") } except requests.exceptions.RequestException as e: print(f"Error retrieving web pesticide info for '{disease}': {e}") return None def get_commercial_product_info(recommendation, disease_name): """Fetch commercial product information.""" if not GOOGLE_API_KEY or not GOOGLE_CX: print("Skipping product search: Google API credentials not set.") return [] queries = [ f"site:indiamart.com pesticide for '{disease_name}' '{recommendation}'", f"site:krishisevakendra.in pesticide for '{disease_name}' '{recommendation}'" ] results = [] for query in queries: url = "https://www.googleapis.com/customsearch/v1" params = {"key": GOOGLE_API_KEY, "cx": GOOGLE_CX, "q": query, "num": 2} try: response = requests.get(url, params=params, timeout=10) response.raise_for_status() data = response.json() if "items" in data: for item in data["items"]: results.append({ "title": item.get("title", "No title"), "link": item.get("link", "#"), "snippet": item.get("snippet", "No snippet available") }) except requests.exceptions.RequestException as e: print(f"Error retrieving product info with query '{query}': {e}") return results def generate_audio(text, language, filename): """Generate an MP3 file from text using gTTS.""" try: lang_mapping = {"English": "en", "Hindi": "hi", "Bengali": "bn", "Telugu": "te", "Marathi": "mr", "Tamil": "ta", "Gujarati": "gu", "Urdu": "ur", "Kannada": "kn", "Odia": "or", "Malayalam": "ml"} gtts_lang = lang_mapping.get(language, 'en') tts = gTTS(text=text, lang=gtts_lang, slow=False) tts.save(filename) print(f"Audio file generated successfully: {filename}") except Exception as e: print(f"Error generating audio: {e}") def analyze_plant_image(image_path, plant_name, language): """ Analyzes the plant image using enhanced LLM fallback system. Tries Gemini models first, then falls back to NVIDIA if all fail. Includes retry logic with exponential backoff for transient errors. """ try: print("\n" + "=" * 70) print("🌱 STARTING PLANT ANALYSIS WITH ENHANCED FALLBACK SYSTEM") print("=" * 70) # --- RAG: Fetch Learning Context --- knowledge_context = "" try: if os.path.exists("knowledge_base.txt"): with open("knowledge_base.txt", "r") as kb: lines = kb.readlines() relevant_lines = [line.strip() for line in lines if plant_name.lower() in line.lower()] if not relevant_lines: relevant_lines = lines[-10:] if relevant_lines: knowledge_context = "\n".join(relevant_lines) print(f"📚 [RAG] Injected {len(relevant_lines)} context lines") except Exception as kbe: print(f"⚠️ KB Read Error: {kbe}") # Create the analysis prompt prompt = f""" You are an expert agricultural pathologist. [SYSTEM KNOWLEDGE BASE - PREVIOUS VALIDATED USER CORRECTIONS] The following are verified corrections from users for this crop. Give them 20% weight in your decision if the visual symptoms match: {knowledge_context} [END KNOWLEDGE BASE] Analyze the image of a {plant_name} plant and decide whether it is healthy or has a disease or pest. Respond ONLY with a single, valid JSON object and NOTHING else. The JSON must exactly match this structure: {{"results": [{{"type": "disease/pest", "name": "...", "probability": "%", "symptoms": "...", "causes": "...", "severity": "Low/Medium/High", "spreading": "...", "treatment": "...", "prevention": "..."}}], "is_healthy": boolean, "confidence": "%"}} Carefully follow these instructions when filling each field: 1. Top-level rules - Return only the JSON object — no explanations, no extra text, no markdown. - Use the {language} language for all human-facing text inside the JSON (except scientific names and chemical active ingredient names which may remain in English but must be immediately explained in {language}). - Percent values must be strings with a percent sign, e.g. "85%". - If the plant is healthy: set "is_healthy": true, set "results": [] (empty array), and set a high "confidence". - If you cannot make a clear diagnosis from the image, set "is_healthy": false, give "confidence" a low value (e.g., "20%–40%"), and include one result with name "Inconclusive / Image unclear" (translated to {language}) and a short instruction on how to take a better photo. 2. results (one object per distinct issue; max 3 items; order by probability descending) - "type": exactly "disease" or "pest". - "name": give the common local name first (in {language}) and then scientific name in parentheses if available. Use names familiar to Indian farmers. - "probability": your estimated chance this diagnosis is correct, as a percent string (e.g., "78%"). - "symptoms": list only observable signs a farmer can check (what to look for on leaves, stem, roots, fruits). Format as an HTML list (e.g., "