Spaces:
Running
Running
| """ | |
| Farm Analyzer with Nebius AI Studio | |
| ==================================== | |
| Analyzes farm/crop images using Qwen2.5-VL-72B-Instruct via Nebius API. | |
| Provides overall health assessment, disease detection, and recommendations. | |
| Model: Qwen/Qwen2.5-VL-72B-Instruct | |
| API: OpenAI-compatible | |
| Usage: | |
| from src.nebius_analyzer import analyze_farm_image | |
| result = analyze_farm_image(pil_image) | |
| print(result["health_status"]) # "Diseased" | |
| """ | |
| import os | |
| import json | |
| import base64 | |
| from io import BytesIO | |
| from PIL import Image | |
| # ============================================================ | |
| # CONFIGURATION | |
| # ============================================================ | |
| NEBIUS_API_URL = "https://api.studio.nebius.com/v1/" | |
| MODEL_NAME = "Qwen/Qwen2.5-VL-72B-Instruct" | |
| # ============================================================ | |
| # MODULE STATE | |
| # ============================================================ | |
| _client = None | |
| # ============================================================ | |
| # PRIVATE FUNCTIONS | |
| # ============================================================ | |
| def _get_api_key() -> str: | |
| """Get Nebius API key from environment variable.""" | |
| return os.environ.get("NEBIUS_API_KEY", "") | |
| def _get_client(): | |
| """ | |
| Get OpenAI client configured for Nebius. | |
| Returns: | |
| OpenAI client or None if API key not set | |
| """ | |
| global _client | |
| if _client is not None: | |
| return _client | |
| api_key = _get_api_key() | |
| if not api_key: | |
| print("⚠️ NEBIUS_API_KEY not set") | |
| return None | |
| try: | |
| from openai import OpenAI | |
| print("🤖 Initializing Nebius client...") | |
| _client = OpenAI( | |
| base_url=NEBIUS_API_URL, | |
| api_key=api_key, | |
| ) | |
| print(f"✅ Nebius client ready (model: {MODEL_NAME})") | |
| return _client | |
| except ImportError: | |
| print("❌ openai package not installed. Run: pip install openai") | |
| return None | |
| except Exception as e: | |
| print(f"❌ Error initializing Nebius client: {e}") | |
| return None | |
| def _encode_image_to_base64(image: Image.Image) -> str: | |
| """ | |
| Encode PIL Image to base64 string. | |
| Args: | |
| image: PIL Image | |
| Returns: | |
| Base64 encoded string | |
| """ | |
| buffered = BytesIO() | |
| image.save(buffered, format="JPEG") | |
| return base64.b64encode(buffered.getvalue()).decode("utf-8") | |
| def _build_prompt() -> str: | |
| """Build the analysis prompt for the vision model.""" | |
| return """You are an expert agronomist analyzing a farm/crop image. | |
| Analyze this image and provide a detailed assessment of the crop health. | |
| Respond ONLY with valid JSON (no markdown, no code blocks, no extra text): | |
| { | |
| "crop_identified": "name of the crop or 'Unknown'", | |
| "health_status": "Healthy" or "Diseased" or "Stressed" or "Unknown", | |
| "confidence": 0-100, | |
| "issues_detected": [ | |
| { | |
| "name": "issue name", | |
| "severity": "Low" or "Medium" or "High" or "Critical", | |
| "affected_area": "percentage or description" | |
| } | |
| ], | |
| "overall_severity": "None" or "Low" or "Medium" or "High" or "Critical", | |
| "recommendations": [ | |
| "recommendation 1", | |
| "recommendation 2", | |
| "recommendation 3" | |
| ], | |
| "observations": "any additional observations about the crop/field" | |
| } | |
| Important: | |
| - If you cannot identify the crop, set crop_identified to "Unknown" | |
| - Always provide at least one recommendation | |
| - Be specific about issues you observe | |
| - Output ONLY the JSON, nothing else""" | |
| def _parse_response(text: str) -> dict: | |
| """Parse model response to dict.""" | |
| # Clean markdown if present | |
| cleaned = text.strip() | |
| if "```json" in cleaned: | |
| cleaned = cleaned.split("```json")[1].split("```")[0] | |
| elif "```" in cleaned: | |
| cleaned = cleaned.split("```")[1].split("```")[0] | |
| cleaned = cleaned.strip() | |
| try: | |
| return json.loads(cleaned) | |
| except json.JSONDecodeError as e: | |
| # Try to extract JSON with regex as fallback | |
| import re | |
| match = re.search(r'\{[\s\S]*\}', cleaned) | |
| if match: | |
| try: | |
| return json.loads(match.group()) | |
| except: | |
| pass | |
| return { | |
| "parse_error": str(e), | |
| "raw_response": text | |
| } | |
| # ============================================================ | |
| # PUBLIC FUNCTION | |
| # ============================================================ | |
| def analyze_farm_image(image: Image.Image) -> dict: | |
| """ | |
| Analyze a farm/crop image using Qwen2.5-VL-72B via Nebius. | |
| Args: | |
| image: PIL Image of farm/crop | |
| Returns: | |
| dict with result: | |
| { | |
| "success": True, | |
| "crop_identified": "Tomato", | |
| "health_status": "Diseased", | |
| "confidence": 85, | |
| "issues_detected": [...], | |
| "overall_severity": "Medium", | |
| "recommendations": [...], | |
| "observations": "..." | |
| } | |
| On error: | |
| { | |
| "success": False, | |
| "error": "Error description" | |
| } | |
| """ | |
| # Validate input | |
| if image is None: | |
| return { | |
| "success": False, | |
| "error": "No image provided" | |
| } | |
| if not isinstance(image, Image.Image): | |
| return { | |
| "success": False, | |
| "error": f"Invalid image type: {type(image)}. Expected PIL.Image" | |
| } | |
| # Check API key | |
| if not _get_api_key(): | |
| return { | |
| "success": False, | |
| "error": "NEBIUS_API_KEY not configured. Set it as environment variable." | |
| } | |
| # Get client | |
| client = _get_client() | |
| if client is None: | |
| return { | |
| "success": False, | |
| "error": "Failed to initialize Nebius client" | |
| } | |
| try: | |
| # Convert to RGB if needed | |
| image = image.convert("RGB") | |
| # Encode image to base64 | |
| image_base64 = _encode_image_to_base64(image) | |
| # Build prompt | |
| prompt = _build_prompt() | |
| # Call Nebius API with vision model | |
| response = client.chat.completions.create( | |
| model=MODEL_NAME, | |
| messages=[ | |
| { | |
| "role": "user", | |
| "content": [ | |
| { | |
| "type": "image_url", | |
| "image_url": { | |
| "url": f"data:image/jpeg;base64,{image_base64}" | |
| } | |
| }, | |
| { | |
| "type": "text", | |
| "text": prompt | |
| } | |
| ] | |
| } | |
| ], | |
| max_tokens=1024, | |
| temperature=0.3 # Lower temperature for more consistent JSON output | |
| ) | |
| # Extract response text | |
| response_text = response.choices[0].message.content | |
| # Parse response | |
| result = _parse_response(response_text) | |
| # Check for parse error | |
| if "parse_error" in result: | |
| return { | |
| "success": False, | |
| "error": f"Failed to parse response: {result['parse_error']}", | |
| "raw_response": result.get("raw_response", "") | |
| } | |
| # Add success flag | |
| result["success"] = True | |
| return result | |
| except Exception as e: | |
| return { | |
| "success": False, | |
| "error": str(e) | |
| } | |