|
|
""" |
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
BASE_DIR = Path(__file__).parent.resolve() |
|
|
|
|
|
|
|
|
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" |
|
|
|
|
|
|
|
|
for directory in [DATA_DIR, STATIC_DIR, MODELS_DIR, OUTPUTS_DIR, UPLOADS_DIR]: |
|
|
directory.mkdir(parents=True, exist_ok=True) |
|
|
|
|
|
|
|
|
KNOWLEDGE_BASE_PATH = DATA_DIR / "knowledge_base.json" |
|
|
UI_TRANSLATIONS_PATH = STATIC_DIR / "ui_translations.json" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass |
|
|
class APIConfig: |
|
|
"""Configuration for FastAPI backend""" |
|
|
|
|
|
|
|
|
host: str = "0.0.0.0" |
|
|
port: int = 7860 |
|
|
|
|
|
|
|
|
title: str = "FarmEyes API" |
|
|
description: str = "AI-Powered Crop Disease Detection for Nigerian Farmers" |
|
|
version: str = "2.0.0" |
|
|
|
|
|
|
|
|
cors_origins: List[str] = field(default_factory=lambda: [ |
|
|
"http://localhost:7860", |
|
|
"http://127.0.0.1:7860", |
|
|
"https://*.hf.space", |
|
|
"*" |
|
|
]) |
|
|
|
|
|
|
|
|
max_upload_size: int = 10 * 1024 * 1024 |
|
|
request_timeout: int = 60 |
|
|
|
|
|
|
|
|
rate_limit_requests: int = 100 |
|
|
rate_limit_window: int = 60 |
|
|
|
|
|
|
|
|
debug: bool = os.environ.get("DEBUG", "false").lower() == "true" |
|
|
|
|
|
|
|
|
@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}" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass |
|
|
class SessionConfig: |
|
|
"""Configuration for session management""" |
|
|
|
|
|
|
|
|
session_lifetime: int = 3600 |
|
|
max_sessions: int = 1000 |
|
|
|
|
|
|
|
|
max_chat_history: int = 50 |
|
|
max_message_length: int = 2000 |
|
|
|
|
|
|
|
|
retain_diagnosis: bool = True |
|
|
|
|
|
|
|
|
cleanup_interval: int = 300 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass |
|
|
class WhisperConfig: |
|
|
"""Configuration for Whisper speech-to-text""" |
|
|
|
|
|
|
|
|
model_size: str = "base" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
device: str = "cpu" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sample_rate: int = 16000 |
|
|
max_audio_duration: int = 30 |
|
|
|
|
|
|
|
|
language_hints: Dict[str, str] = field(default_factory=lambda: { |
|
|
"en": "english", |
|
|
"ha": "hausa", |
|
|
"yo": "yoruba", |
|
|
"ig": "igbo" |
|
|
}) |
|
|
|
|
|
|
|
|
task: str = "transcribe" |
|
|
|
|
|
|
|
|
fp16: bool = False |
|
|
|
|
|
|
|
|
supported_formats: List[str] = field(default_factory=lambda: [ |
|
|
".wav", ".mp3", ".m4a", ".ogg", ".flac", ".webm" |
|
|
]) |
|
|
|
|
|
|
|
|
max_file_size: int = 5 * 1024 * 1024 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass |
|
|
class YOLOConfig: |
|
|
"""Configuration for YOLOv11 disease detection model""" |
|
|
|
|
|
|
|
|
model_path: Path = MODELS_DIR / "farmeyes_yolov11.pt" |
|
|
|
|
|
|
|
|
confidence_threshold: float = 0.5 |
|
|
|
|
|
|
|
|
iou_threshold: float = 0.45 |
|
|
|
|
|
|
|
|
input_size: int = 640 |
|
|
|
|
|
|
|
|
max_detections: int = 10 |
|
|
|
|
|
|
|
|
device: str = "mps" |
|
|
|
|
|
|
|
|
@dataclass |
|
|
class NATLaSConfig: |
|
|
"""Configuration for N-ATLaS language model (GGUF format)""" |
|
|
|
|
|
|
|
|
hf_repo: str = "tosinamuda/N-ATLaS-GGUF" |
|
|
|
|
|
|
|
|
model_filename: str = "N-ATLaS-GGUF-Q4_K_M.gguf" |
|
|
|
|
|
|
|
|
model_path: Path = MODELS_DIR / "natlas" |
|
|
|
|
|
|
|
|
@property |
|
|
def gguf_path(self) -> Path: |
|
|
return self.model_path / self.model_filename |
|
|
|
|
|
|
|
|
context_length: int = 4096 |
|
|
|
|
|
|
|
|
max_tokens: int = 1024 |
|
|
|
|
|
|
|
|
chat_max_tokens: int = 512 |
|
|
|
|
|
|
|
|
temperature: float = 0.7 |
|
|
|
|
|
|
|
|
chat_temperature: float = 0.6 |
|
|
|
|
|
|
|
|
top_p: float = 0.9 |
|
|
|
|
|
|
|
|
|
|
|
n_gpu_layers: int = -1 |
|
|
|
|
|
|
|
|
n_threads: int = 8 |
|
|
|
|
|
|
|
|
n_batch: int = 512 |
|
|
|
|
|
|
|
|
device: str = "mps" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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" |
|
|
} |
|
|
|
|
|
|
|
|
KEY_TO_CLASS_INDEX: Dict[str, int] = {v: k for k, v in CLASS_INDEX_TO_KEY.items()} |
|
|
|
|
|
|
|
|
CLASS_NAMES: List[str] = [ |
|
|
"Cassava Bacteria Blight", |
|
|
"Cassava Mosaic Virus", |
|
|
"Cocoa Monilia Disease", |
|
|
"Cocoa Phytophthora Disease", |
|
|
"Tomato Gray Mold Disease", |
|
|
"Tomato Wilt Disease" |
|
|
] |
|
|
|
|
|
|
|
|
HEALTHY_CLASS_INDICES: List[int] = [] |
|
|
|
|
|
|
|
|
DISEASE_CLASS_INDICES: List[int] = [0, 1, 2, 3, 4, 5] |
|
|
|
|
|
|
|
|
CROP_TYPES: Dict[str, List[int]] = { |
|
|
"cassava": [0, 1], |
|
|
"cocoa": [2, 3], |
|
|
"tomato": [4, 5] |
|
|
} |
|
|
|
|
|
|
|
|
CLASS_TO_CROP: Dict[int, str] = {} |
|
|
for crop, indices in CROP_TYPES.items(): |
|
|
for idx in indices: |
|
|
CLASS_TO_CROP[idx] = crop |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass |
|
|
class LanguageConfig: |
|
|
"""Configuration for supported languages""" |
|
|
|
|
|
|
|
|
supported_languages: List[str] = field(default_factory=lambda: ["en", "ha", "yo", "ig"]) |
|
|
|
|
|
|
|
|
default_language: str = "en" |
|
|
|
|
|
|
|
|
language_names: Dict[str, str] = field(default_factory=lambda: { |
|
|
"en": "English", |
|
|
"ha": "Hausa", |
|
|
"yo": "Yorùbá", |
|
|
"ig": "Igbo" |
|
|
}) |
|
|
|
|
|
|
|
|
language_full_names: Dict[str, str] = field(default_factory=lambda: { |
|
|
"en": "English", |
|
|
"ha": "Hausa", |
|
|
"yo": "Yoruba", |
|
|
"ig": "Igbo" |
|
|
}) |
|
|
|
|
|
|
|
|
native_names: Dict[str, str] = field(default_factory=lambda: { |
|
|
"en": "English", |
|
|
"ha": "Hausa", |
|
|
"yo": "Yorùbá", |
|
|
"ig": "Asụsụ Igbo" |
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass |
|
|
class ChatConfig: |
|
|
"""Configuration for contextual chatbot""" |
|
|
|
|
|
|
|
|
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: 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_topics: List[str] = field(default_factory=lambda: [ |
|
|
|
|
|
"disease", "infection", "symptom", "treatment", "cure", "prevention", |
|
|
"spread", "cause", "severity", "recovery", |
|
|
|
|
|
"crop", "plant", "leaf", "stem", "root", "fruit", "harvest", "yield", |
|
|
"cassava", "cocoa", "tomato", "farming", "agriculture", |
|
|
|
|
|
"medicine", "chemical", "organic", "traditional", "spray", "apply", |
|
|
"fungicide", "pesticide", "fertilizer", "cost", "price", "naira", |
|
|
|
|
|
"farm", "field", "soil", "water", "weather", "season", "planting", |
|
|
"seed", "variety", "resistant", "healthy" |
|
|
]) |
|
|
|
|
|
|
|
|
max_response_tokens: int = 400 |
|
|
|
|
|
|
|
|
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?""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass |
|
|
class AppConfig: |
|
|
"""General application configuration""" |
|
|
|
|
|
|
|
|
app_name: str = "FarmEyes" |
|
|
app_version: str = "2.0.0" |
|
|
app_tagline: str = "AI-Powered Crop Disease Detection for Nigerian Farmers" |
|
|
|
|
|
|
|
|
server_host: str = "0.0.0.0" |
|
|
server_port: int = 7860 |
|
|
share: bool = False |
|
|
|
|
|
|
|
|
debug: bool = True |
|
|
|
|
|
|
|
|
max_image_size: int = 10 * 1024 * 1024 |
|
|
|
|
|
|
|
|
supported_image_formats: List[str] = field(default_factory=lambda: [ |
|
|
".jpg", ".jpeg", ".png", ".webp", ".bmp" |
|
|
]) |
|
|
|
|
|
|
|
|
high_confidence_threshold: float = 0.85 |
|
|
medium_confidence_threshold: float = 0.60 |
|
|
low_confidence_threshold: float = 0.40 |
|
|
|
|
|
|
|
|
enable_voice_input: bool = True |
|
|
enable_chat: bool = True |
|
|
enable_history: bool = True |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass |
|
|
class DeviceConfig: |
|
|
"""Device and hardware configuration for Apple Silicon M1 Pro""" |
|
|
|
|
|
|
|
|
compute_device: str = "mps" |
|
|
|
|
|
|
|
|
fallback_device: str = "cpu" |
|
|
|
|
|
|
|
|
use_mps: bool = True |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass |
|
|
class PromptConfig: |
|
|
"""Configuration for N-ATLaS prompt templates""" |
|
|
|
|
|
|
|
|
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.""" |
|
|
|
|
|
|
|
|
max_translation_length: int = 500 |
|
|
|
|
|
|
|
|
translation_temperature: float = 0.3 |
|
|
diagnosis_temperature: float = 0.7 |
|
|
chat_temperature: float = 0.6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass |
|
|
class LogConfig: |
|
|
"""Logging configuration""" |
|
|
|
|
|
|
|
|
log_level: str = "INFO" |
|
|
|
|
|
|
|
|
log_file: Path = BASE_DIR / "logs" / "farmeyes.log" |
|
|
|
|
|
|
|
|
log_format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" |
|
|
|
|
|
|
|
|
console_logging: bool = True |
|
|
|
|
|
|
|
|
file_logging: bool = True |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
print_config_summary() |
|
|
|