FarmEyes / config.py
Fola-AI
Initial FarmEyes deployment - AI Powered Crop Disease Detection Program
f45df09
"""
FarmEyes Configuration File
===========================
Central configuration for the FarmEyes crop disease detection application.
Contains model paths, class mappings, device settings, API configurations,
session management, and Whisper speech-to-text settings.
Device: Apple Silicon M1 Pro with MPS (Metal Performance Shaders) acceleration
Deployment: Local development + HuggingFace Spaces
Model: Custom trained YOLOv11 for 6 disease classes
Crops: Cassava, Cocoa, Tomato
"""
import os
from pathlib import Path
from typing import Dict, List, Optional
from dataclasses import dataclass, field
# =============================================================================
# PATH CONFIGURATIONS
# =============================================================================
# Base project directory
BASE_DIR = Path(__file__).parent.resolve()
# Data directories
DATA_DIR = BASE_DIR / "data"
STATIC_DIR = BASE_DIR / "static"
MODELS_DIR = BASE_DIR / "models"
OUTPUTS_DIR = BASE_DIR / "outputs"
FRONTEND_DIR = BASE_DIR / "frontend"
UPLOADS_DIR = BASE_DIR / "uploads"
# Create directories if they don't exist
for directory in [DATA_DIR, STATIC_DIR, MODELS_DIR, OUTPUTS_DIR, UPLOADS_DIR]:
directory.mkdir(parents=True, exist_ok=True)
# Knowledge base and UI translations paths
KNOWLEDGE_BASE_PATH = DATA_DIR / "knowledge_base.json"
UI_TRANSLATIONS_PATH = STATIC_DIR / "ui_translations.json"
# =============================================================================
# API CONFIGURATION
# =============================================================================
@dataclass
class APIConfig:
"""Configuration for FastAPI backend"""
# Server settings
host: str = "0.0.0.0"
port: int = 7860 # Default HuggingFace Spaces port
# API metadata
title: str = "FarmEyes API"
description: str = "AI-Powered Crop Disease Detection for Nigerian Farmers"
version: str = "2.0.0"
# CORS settings (for frontend access)
cors_origins: List[str] = field(default_factory=lambda: [
"http://localhost:7860",
"http://127.0.0.1:7860",
"https://*.hf.space", # HuggingFace Spaces
"*" # Allow all for development - restrict in production
])
# Request limits
max_upload_size: int = 10 * 1024 * 1024 # 10MB max image upload
request_timeout: int = 60 # seconds
# Rate limiting (basic)
rate_limit_requests: int = 100 # requests per minute
rate_limit_window: int = 60 # seconds
# Debug mode
debug: bool = os.environ.get("DEBUG", "false").lower() == "true"
# Environment detection
@property
def is_huggingface(self) -> bool:
"""Check if running on HuggingFace Spaces"""
return os.environ.get("SPACE_ID") is not None
@property
def base_url(self) -> str:
"""Get base URL based on environment"""
if self.is_huggingface:
space_id = os.environ.get("SPACE_ID", "")
return f"https://{space_id.replace('/', '-')}.hf.space"
return f"http://{self.host}:{self.port}"
# =============================================================================
# SESSION CONFIGURATION
# =============================================================================
@dataclass
class SessionConfig:
"""Configuration for session management"""
# Session settings
session_lifetime: int = 3600 # 1 hour in seconds
max_sessions: int = 1000 # Maximum concurrent sessions
# Chat history settings
max_chat_history: int = 50 # Maximum messages per session
max_message_length: int = 2000 # Maximum characters per message
# Context retention
retain_diagnosis: bool = True # Keep diagnosis context in session
# Cleanup settings
cleanup_interval: int = 300 # 5 minutes - check for expired sessions
# =============================================================================
# WHISPER CONFIGURATION
# =============================================================================
@dataclass
class WhisperConfig:
"""Configuration for Whisper speech-to-text"""
# Model settings
model_size: str = "base" # tiny, base, small, medium, large
# Supported model sizes with approximate VRAM/RAM requirements
# tiny: ~1GB - Fastest, least accurate
# base: ~1GB - Good balance (SELECTED)
# small: ~2GB - Better accuracy
# medium: ~5GB - High accuracy
# large: ~10GB - Best accuracy
# Device settings
device: str = "cpu" # Use CPU for broader compatibility
# Note: On Apple Silicon, Whisper runs well on CPU
# For GPU: set to "cuda" (NVIDIA) or use mlx-whisper for Apple Silicon
# Audio settings
sample_rate: int = 16000 # Whisper expects 16kHz audio
max_audio_duration: int = 30 # Maximum seconds of audio to process
# Language settings - Whisper auto-detects but we can hint
language_hints: Dict[str, str] = field(default_factory=lambda: {
"en": "english",
"ha": "hausa",
"yo": "yoruba",
"ig": "igbo"
})
# Transcription settings
task: str = "transcribe" # transcribe or translate
# Performance settings
fp16: bool = False # Use FP32 for CPU compatibility
# Supported audio formats
supported_formats: List[str] = field(default_factory=lambda: [
".wav", ".mp3", ".m4a", ".ogg", ".flac", ".webm"
])
# Maximum audio file size (5MB)
max_file_size: int = 5 * 1024 * 1024
# =============================================================================
# MODEL CONFIGURATIONS
# =============================================================================
@dataclass
class YOLOConfig:
"""Configuration for YOLOv11 disease detection model"""
# Path to trained YOLOv11 model weights (.pt file)
model_path: Path = MODELS_DIR / "farmeyes_yolov11.pt"
# Confidence threshold for detections (0.0 - 1.0)
confidence_threshold: float = 0.5
# IoU threshold for non-maximum suppression
iou_threshold: float = 0.45
# Input image size (YOLOv11 default)
input_size: int = 640
# Maximum number of detections per image
max_detections: int = 10
# Device for inference ('mps' for Apple Silicon, 'cuda' for NVIDIA, 'cpu' for CPU)
device: str = "mps"
@dataclass
class NATLaSConfig:
"""Configuration for N-ATLaS language model (GGUF format)"""
# Hugging Face model repository
hf_repo: str = "tosinamuda/N-ATLaS-GGUF"
# GGUF model filename (Q4_K_M quantized version - smaller, faster)
model_filename: str = "N-ATLaS-GGUF-Q4_K_M.gguf"
# Local path where model will be downloaded/cached
model_path: Path = MODELS_DIR / "natlas"
# Full path to the GGUF file
@property
def gguf_path(self) -> Path:
return self.model_path / self.model_filename
# Context window size (tokens)
context_length: int = 4096
# Maximum tokens to generate in response
max_tokens: int = 1024
# Chat-specific max tokens (shorter for responsiveness)
chat_max_tokens: int = 512
# Temperature for text generation (0.0 = deterministic, 1.0 = creative)
temperature: float = 0.7
# Chat temperature (slightly lower for more focused responses)
chat_temperature: float = 0.6
# Top-p (nucleus) sampling
top_p: float = 0.9
# Number of GPU layers to offload (for MPS acceleration)
# Set to -1 to offload all layers, 0 for CPU only
n_gpu_layers: int = -1
# Number of threads for CPU computation
n_threads: int = 8
# Batch size for prompt processing
n_batch: int = 512
# Device for inference
device: str = "mps"
# =============================================================================
# DISEASE CLASS MAPPINGS (6 CLASSES - NO HEALTHY CLASSES)
# =============================================================================
# YOLOv11 class index to disease key mapping
CLASS_INDEX_TO_KEY: Dict[int, str] = {
0: "cassava_bacterial_blight",
1: "cassava_mosaic_virus",
2: "cocoa_monilia_disease",
3: "cocoa_phytophthora_disease",
4: "tomato_gray_mold",
5: "tomato_wilt_disease"
}
# Reverse mapping: disease key to class index
KEY_TO_CLASS_INDEX: Dict[str, int] = {v: k for k, v in CLASS_INDEX_TO_KEY.items()}
# Class names as they appear in YOLO training (6 classes)
CLASS_NAMES: List[str] = [
"Cassava Bacteria Blight", # Index 0
"Cassava Mosaic Virus", # Index 1
"Cocoa Monilia Disease", # Index 2
"Cocoa Phytophthora Disease", # Index 3
"Tomato Gray Mold Disease", # Index 4
"Tomato Wilt Disease" # Index 5
]
# No healthy class indices in 6-class model
HEALTHY_CLASS_INDICES: List[int] = []
# All class indices are disease classes
DISEASE_CLASS_INDICES: List[int] = [0, 1, 2, 3, 4, 5]
# Crop type mapping (6 classes only)
CROP_TYPES: Dict[str, List[int]] = {
"cassava": [0, 1],
"cocoa": [2, 3],
"tomato": [4, 5]
}
# Reverse mapping: class index to crop type
CLASS_TO_CROP: Dict[int, str] = {}
for crop, indices in CROP_TYPES.items():
for idx in indices:
CLASS_TO_CROP[idx] = crop
# =============================================================================
# LANGUAGE CONFIGURATIONS
# =============================================================================
@dataclass
class LanguageConfig:
"""Configuration for supported languages"""
# Supported language codes
supported_languages: List[str] = field(default_factory=lambda: ["en", "ha", "yo", "ig"])
# Default language
default_language: str = "en"
# Language display names
language_names: Dict[str, str] = field(default_factory=lambda: {
"en": "English",
"ha": "Hausa",
"yo": "Yorùbá",
"ig": "Igbo"
})
# Language codes for N-ATLaS prompts
language_full_names: Dict[str, str] = field(default_factory=lambda: {
"en": "English",
"ha": "Hausa",
"yo": "Yoruba",
"ig": "Igbo"
})
# Native language names (for display in selector)
native_names: Dict[str, str] = field(default_factory=lambda: {
"en": "English",
"ha": "Hausa",
"yo": "Yorùbá",
"ig": "Asụsụ Igbo"
})
# =============================================================================
# CHAT CONFIGURATION
# =============================================================================
@dataclass
class ChatConfig:
"""Configuration for contextual chatbot"""
# System prompt for agricultural chat
system_prompt: str = """You are FarmEyes, an AI agricultural assistant helping Nigerian farmers.
You are currently discussing a specific crop disease diagnosis with the farmer.
Your role is to:
1. Answer questions ONLY about the diagnosed disease and related agricultural topics
2. Provide practical, actionable advice in simple language
3. Use local context (Nigerian farming practices, costs in Naira)
4. Be respectful, patient, and supportive
5. If asked about unrelated topics, politely redirect to agricultural matters
IMPORTANT: Stay focused on the diagnosis context provided. Do not make up information.
If you don't know something, say so honestly and suggest consulting a local agricultural extension officer."""
# Context template for chat
context_template: str = """CURRENT DIAGNOSIS CONTEXT:
- Crop: {crop_type}
- Disease: {disease_name}
- Confidence: {confidence}%
- Severity: {severity}
- Key symptoms: {symptoms}
- Recommended treatment: {treatment_summary}
The farmer may ask follow-up questions about this diagnosis."""
# Allowed topic keywords (for moderate context restriction)
allowed_topics: List[str] = field(default_factory=lambda: [
# Disease-related
"disease", "infection", "symptom", "treatment", "cure", "prevention",
"spread", "cause", "severity", "recovery",
# Crop-related
"crop", "plant", "leaf", "stem", "root", "fruit", "harvest", "yield",
"cassava", "cocoa", "tomato", "farming", "agriculture",
# Treatment-related
"medicine", "chemical", "organic", "traditional", "spray", "apply",
"fungicide", "pesticide", "fertilizer", "cost", "price", "naira",
# General farming
"farm", "field", "soil", "water", "weather", "season", "planting",
"seed", "variety", "resistant", "healthy"
])
# Response length limits
max_response_tokens: int = 400
# Welcome message template
welcome_template: str = """Hello! I'm your FarmEyes assistant. I've analyzed your {crop_type} plant and detected {disease_name} with {confidence}% confidence.
I can help you understand:
• More about this disease and its symptoms
• Treatment options and costs
• Prevention methods
• When to seek expert help
What would you like to know?"""
# =============================================================================
# APPLICATION CONFIGURATIONS
# =============================================================================
@dataclass
class AppConfig:
"""General application configuration"""
# App information
app_name: str = "FarmEyes"
app_version: str = "2.0.0"
app_tagline: str = "AI-Powered Crop Disease Detection for Nigerian Farmers"
# Server settings (legacy Gradio support)
server_host: str = "0.0.0.0"
server_port: int = 7860
share: bool = False
# Debug mode
debug: bool = True
# Maximum image file size (in bytes) - 10MB
max_image_size: int = 10 * 1024 * 1024
# Supported image formats
supported_image_formats: List[str] = field(default_factory=lambda: [
".jpg", ".jpeg", ".png", ".webp", ".bmp"
])
# Confidence thresholds for user feedback
high_confidence_threshold: float = 0.85
medium_confidence_threshold: float = 0.60
low_confidence_threshold: float = 0.40
# Enable/disable features
enable_voice_input: bool = True # Voice input with Whisper
enable_chat: bool = True # Contextual chat
enable_history: bool = True # Session history
# =============================================================================
# DEVICE CONFIGURATION (Apple Silicon Specific)
# =============================================================================
@dataclass
class DeviceConfig:
"""Device and hardware configuration for Apple Silicon M1 Pro"""
# Primary compute device
compute_device: str = "mps"
# Fallback device if primary is unavailable
fallback_device: str = "cpu"
# Enable MPS (Metal Performance Shaders) for PyTorch
use_mps: bool = True
# Memory management
clear_cache_after_inference: bool = True
@staticmethod
def get_device() -> str:
"""Determine the best available device for computation."""
import torch
if torch.backends.mps.is_available():
return "mps"
elif torch.cuda.is_available():
return "cuda"
else:
return "cpu"
@staticmethod
def get_device_info() -> Dict[str, str]:
"""Get information about the current compute device."""
import torch
import platform
info = {
"platform": platform.system(),
"processor": platform.processor(),
"python_version": platform.python_version(),
"pytorch_version": torch.__version__,
"device": DeviceConfig.get_device()
}
if torch.backends.mps.is_available():
info["mps_available"] = "Yes"
info["mps_built"] = str(torch.backends.mps.is_built())
return info
# =============================================================================
# PROMPT TEMPLATES CONFIGURATION
# =============================================================================
@dataclass
class PromptConfig:
"""Configuration for N-ATLaS prompt templates"""
# System prompt for the N-ATLaS model
system_prompt: str = """You are FarmEyes, an AI agricultural assistant helping Nigerian farmers.
You provide advice about crop diseases and treatments in a clear, simple, and helpful manner.
Always be respectful and use language that farmers can easily understand.
When providing treatment costs, use Nigerian Naira (₦).
Focus on practical advice that farmers can implement."""
# Maximum length for translated text
max_translation_length: int = 500
# Temperature for different tasks
translation_temperature: float = 0.3
diagnosis_temperature: float = 0.7
chat_temperature: float = 0.6
# =============================================================================
# LOGGING CONFIGURATION
# =============================================================================
@dataclass
class LogConfig:
"""Logging configuration"""
# Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL
log_level: str = "INFO"
# Log file path
log_file: Path = BASE_DIR / "logs" / "farmeyes.log"
# Log format
log_format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
# Enable console logging
console_logging: bool = True
# Enable file logging
file_logging: bool = True
# =============================================================================
# INSTANTIATE DEFAULT CONFIGURATIONS
# =============================================================================
# Create default configuration instances
api_config = APIConfig()
session_config = SessionConfig()
whisper_config = WhisperConfig()
yolo_config = YOLOConfig()
natlas_config = NATLaSConfig()
language_config = LanguageConfig()
chat_config = ChatConfig()
app_config = AppConfig()
device_config = DeviceConfig()
prompt_config = PromptConfig()
log_config = LogConfig()
# =============================================================================
# UTILITY FUNCTIONS
# =============================================================================
def get_disease_key(class_index: int) -> Optional[str]:
"""Get disease key from class index."""
return CLASS_INDEX_TO_KEY.get(class_index)
def get_class_index(disease_key: str) -> Optional[int]:
"""Get class index from disease key."""
return KEY_TO_CLASS_INDEX.get(disease_key)
def get_crop_type(class_index: int) -> Optional[str]:
"""Get crop type from class index."""
return CLASS_TO_CROP.get(class_index)
def is_healthy(class_index: int) -> bool:
"""Check if class index represents a healthy plant (always False for 6-class)."""
return class_index in HEALTHY_CLASS_INDICES
def validate_config() -> Dict[str, bool]:
"""Validate that all required configuration files and paths exist."""
validations = {
"knowledge_base_exists": KNOWLEDGE_BASE_PATH.exists(),
"ui_translations_exists": UI_TRANSLATIONS_PATH.exists(),
"models_dir_exists": MODELS_DIR.exists(),
"yolo_model_exists": yolo_config.model_path.exists(),
"natlas_model_exists": natlas_config.gguf_path.exists(),
"frontend_dir_exists": FRONTEND_DIR.exists(),
}
return validations
def print_config_summary():
"""Print a summary of the current configuration."""
print("=" * 60)
print("FarmEyes Configuration Summary v2.0")
print("=" * 60)
print(f"\n📁 Paths:")
print(f" Base Directory: {BASE_DIR}")
print(f" Knowledge Base: {KNOWLEDGE_BASE_PATH}")
print(f" Frontend: {FRONTEND_DIR}")
print(f"\n🌐 API Configuration:")
print(f" Host: {api_config.host}:{api_config.port}")
print(f" Debug: {api_config.debug}")
print(f" HuggingFace: {api_config.is_huggingface}")
print(f"\n🤖 YOLOv11 Model:")
print(f" Model Path: {yolo_config.model_path}")
print(f" Confidence: {yolo_config.confidence_threshold}")
print(f" Classes: {len(CLASS_NAMES)}")
print(f"\n🗣️ N-ATLaS Model:")
print(f" HF Repo: {natlas_config.hf_repo}")
print(f" Chat Max Tokens: {natlas_config.chat_max_tokens}")
print(f"\n🎤 Whisper (Voice):")
print(f" Model Size: {whisper_config.model_size}")
print(f" Device: {whisper_config.device}")
print(f"\n💬 Chat:")
print(f" Enabled: {app_config.enable_chat}")
print(f" Voice Input: {app_config.enable_voice_input}")
print(f"\n🌍 Languages:")
print(f" Supported: {', '.join(language_config.supported_languages)}")
print("\n" + "=" * 60)
# =============================================================================
# MAIN - Run configuration check
# =============================================================================
if __name__ == "__main__":
print_config_summary()