Emmanuel Frimpong Asante
commited on
Commit
·
be56c50
1
Parent(s):
a7e3aed
update space
Browse files- .idea/workspace.xml +16 -11
- models/schemas/health_record_schema.py +51 -0
- routes/administration.py +22 -37
- routes/disease_detection.py +46 -26
- services/health_alerts_service.py +42 -0
- templates/health_dashboard.html +36 -0
.idea/workspace.xml
CHANGED
|
@@ -5,7 +5,12 @@
|
|
| 5 |
</component>
|
| 6 |
<component name="ChangeListManager">
|
| 7 |
<list default="true" id="27c9ae1a-a6fa-4472-8bcd-a7087620894b" name="Changes" comment="update space">
|
| 8 |
-
<change
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
</list>
|
| 10 |
<option name="SHOW_DIALOG" value="false" />
|
| 11 |
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
@@ -119,15 +124,7 @@
|
|
| 119 |
<workItem from="1730305361670" duration="2425000" />
|
| 120 |
<workItem from="1730378091154" duration="2435000" />
|
| 121 |
<workItem from="1730381153348" duration="8786000" />
|
| 122 |
-
<workItem from="1730397485849" duration="
|
| 123 |
-
</task>
|
| 124 |
-
<task id="LOCAL-00033" summary="update space">
|
| 125 |
-
<option name="closed" value="true" />
|
| 126 |
-
<created>1730356036066</created>
|
| 127 |
-
<option name="number" value="00033" />
|
| 128 |
-
<option name="presentableId" value="LOCAL-00033" />
|
| 129 |
-
<option name="project" value="LOCAL" />
|
| 130 |
-
<updated>1730356036066</updated>
|
| 131 |
</task>
|
| 132 |
<task id="LOCAL-00034" summary="update space">
|
| 133 |
<option name="closed" value="true" />
|
|
@@ -513,7 +510,15 @@
|
|
| 513 |
<option name="project" value="LOCAL" />
|
| 514 |
<updated>1730409232117</updated>
|
| 515 |
</task>
|
| 516 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 517 |
<servers />
|
| 518 |
</component>
|
| 519 |
<component name="TypeScriptGeneratedFilesManager">
|
|
|
|
| 5 |
</component>
|
| 6 |
<component name="ChangeListManager">
|
| 7 |
<list default="true" id="27c9ae1a-a6fa-4472-8bcd-a7087620894b" name="Changes" comment="update space">
|
| 8 |
+
<change afterPath="$PROJECT_DIR$/models/schemas/health_record_schema.py" afterDir="false" />
|
| 9 |
+
<change afterPath="$PROJECT_DIR$/services/health_alerts_service.py" afterDir="false" />
|
| 10 |
+
<change afterPath="$PROJECT_DIR$/templates/health_dashboard.html" afterDir="false" />
|
| 11 |
+
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
| 12 |
+
<change beforePath="$PROJECT_DIR$/routes/administration.py" beforeDir="false" afterPath="$PROJECT_DIR$/routes/administration.py" afterDir="false" />
|
| 13 |
+
<change beforePath="$PROJECT_DIR$/routes/disease_detection.py" beforeDir="false" afterPath="$PROJECT_DIR$/routes/disease_detection.py" afterDir="false" />
|
| 14 |
</list>
|
| 15 |
<option name="SHOW_DIALOG" value="false" />
|
| 16 |
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
|
|
| 124 |
<workItem from="1730305361670" duration="2425000" />
|
| 125 |
<workItem from="1730378091154" duration="2435000" />
|
| 126 |
<workItem from="1730381153348" duration="8786000" />
|
| 127 |
+
<workItem from="1730397485849" duration="9041000" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
</task>
|
| 129 |
<task id="LOCAL-00034" summary="update space">
|
| 130 |
<option name="closed" value="true" />
|
|
|
|
| 510 |
<option name="project" value="LOCAL" />
|
| 511 |
<updated>1730409232117</updated>
|
| 512 |
</task>
|
| 513 |
+
<task id="LOCAL-00082" summary="update space">
|
| 514 |
+
<option name="closed" value="true" />
|
| 515 |
+
<created>1730433894107</created>
|
| 516 |
+
<option name="number" value="00082" />
|
| 517 |
+
<option name="presentableId" value="LOCAL-00082" />
|
| 518 |
+
<option name="project" value="LOCAL" />
|
| 519 |
+
<updated>1730433894107</updated>
|
| 520 |
+
</task>
|
| 521 |
+
<option name="localTasksCounter" value="83" />
|
| 522 |
<servers />
|
| 523 |
</component>
|
| 524 |
<component name="TypeScriptGeneratedFilesManager">
|
models/schemas/health_record_schema.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# models/schemas/health_record_schema.py
|
| 2 |
+
|
| 3 |
+
from pydantic import BaseModel, Field, validator
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from bson import ObjectId
|
| 6 |
+
from typing import Optional
|
| 7 |
+
|
| 8 |
+
class PyObjectId(ObjectId):
|
| 9 |
+
"""Custom ObjectId type for Pydantic to enable validation and proper MongoDB support."""
|
| 10 |
+
@classmethod
|
| 11 |
+
def __get_validators__(cls):
|
| 12 |
+
yield cls.validate
|
| 13 |
+
|
| 14 |
+
@classmethod
|
| 15 |
+
def validate(cls, value):
|
| 16 |
+
if not ObjectId.is_valid(value):
|
| 17 |
+
raise ValueError("Invalid ObjectId format")
|
| 18 |
+
return ObjectId(value)
|
| 19 |
+
|
| 20 |
+
class HealthRecord(BaseModel):
|
| 21 |
+
id: PyObjectId = Field(default_factory=PyObjectId, alias="_id")
|
| 22 |
+
bird_id: str = Field(..., description="Unique identifier for a specific bird or batch of birds")
|
| 23 |
+
date: datetime = Field(default_factory=datetime.utcnow, description="Timestamp of the health record entry")
|
| 24 |
+
weight: float = Field(..., gt=0, description="Weight measurement in kilograms")
|
| 25 |
+
mortality_rate: float = Field(..., ge=0, le=100, description="Mortality rate as a percentage")
|
| 26 |
+
feed_intake: float = Field(..., ge=0, description="Daily feed intake in kilograms")
|
| 27 |
+
disease_detected: Optional[str] = Field(None, description="Detected disease name, if any")
|
| 28 |
+
status: str = Field(default="Healthy", description="Health status determined by metrics")
|
| 29 |
+
treatment_recommendation: Optional[str] = Field(None, description="Suggested treatment based on diagnosis")
|
| 30 |
+
|
| 31 |
+
@validator('status')
|
| 32 |
+
def validate_status(cls, value):
|
| 33 |
+
allowed_statuses = {"Healthy", "At Risk", "Critical"}
|
| 34 |
+
if value not in allowed_statuses:
|
| 35 |
+
raise ValueError(f"Status must be one of {allowed_statuses}")
|
| 36 |
+
return value
|
| 37 |
+
|
| 38 |
+
class Config:
|
| 39 |
+
json_encoders = {ObjectId: str}
|
| 40 |
+
schema_extra = {
|
| 41 |
+
"example": {
|
| 42 |
+
"bird_id": "batch_123",
|
| 43 |
+
"date": "2024-11-01T14:30:00Z",
|
| 44 |
+
"weight": 1.5,
|
| 45 |
+
"mortality_rate": 2.0,
|
| 46 |
+
"feed_intake": 0.4,
|
| 47 |
+
"disease_detected": "Coccidiosis",
|
| 48 |
+
"status": "Critical",
|
| 49 |
+
"treatment_recommendation": "Administer anti-coccidial medication and improve hygiene"
|
| 50 |
+
}
|
| 51 |
+
}
|
routes/administration.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
|
|
|
|
|
| 1 |
import io
|
| 2 |
from datetime import datetime
|
| 3 |
-
from typing import
|
| 4 |
-
|
| 5 |
import pandas as pd
|
| 6 |
from bson import ObjectId
|
| 7 |
from fastapi import APIRouter, Depends, Request, HTTPException, status
|
|
@@ -9,7 +10,6 @@ from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse, Fil
|
|
| 9 |
from fastapi.templating import Jinja2Templates
|
| 10 |
from reportlab.lib.pagesizes import letter
|
| 11 |
from reportlab.pdfgen import canvas
|
| 12 |
-
|
| 13 |
from models.schemas.todo_schema import ToDoItem, ToDoCreate, ToDoUpdate
|
| 14 |
from services.auth_service import get_current_user, is_admin_user
|
| 15 |
from services.health_monitoring_service import evaluate_health_data
|
|
@@ -33,11 +33,8 @@ async def health_dashboard(request: Request, current_user: str = Depends(get_cur
|
|
| 33 |
"""
|
| 34 |
Display health metrics with real-time and historical alerts on the dashboard.
|
| 35 |
"""
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
"mortality_rate": 1,
|
| 39 |
-
"reduced_feed_intake_percentage": 8
|
| 40 |
-
}
|
| 41 |
real_time_alerts = evaluate_health_data(current_health_metrics).get("notifications", [])
|
| 42 |
historical_alerts = list(health_collection.find().sort("timestamp", -1).limit(20))
|
| 43 |
|
|
@@ -50,6 +47,15 @@ async def health_dashboard(request: Request, current_user: str = Depends(get_cur
|
|
| 50 |
return templates.TemplateResponse("dashboard.html", context)
|
| 51 |
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
# --- Admin Task Management Routes ---
|
| 54 |
|
| 55 |
@dashboard_router.get("/tasks", response_class=HTMLResponse)
|
|
@@ -62,12 +68,7 @@ async def manage_tasks(request: Request, current_user: str = Depends(get_current
|
|
| 62 |
|
| 63 |
tasks = list(todo_collection.find({}))
|
| 64 |
farmers = list(user_collection.find({"role": "farmer"}))
|
| 65 |
-
|
| 66 |
-
context = {
|
| 67 |
-
"request": request,
|
| 68 |
-
"tasks": tasks,
|
| 69 |
-
"farmers": farmers,
|
| 70 |
-
}
|
| 71 |
return templates.TemplateResponse("admin_tasks.html", context)
|
| 72 |
|
| 73 |
|
|
@@ -80,12 +81,7 @@ async def assign_task(todo_data: ToDoCreate, assigned_to: str, current_user: str
|
|
| 80 |
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required.")
|
| 81 |
|
| 82 |
task = todo_data.dict()
|
| 83 |
-
task.update({
|
| 84 |
-
"assigned_to": assigned_to,
|
| 85 |
-
"created_by": current_user,
|
| 86 |
-
"is_completed": False,
|
| 87 |
-
"created_at": datetime.utcnow()
|
| 88 |
-
})
|
| 89 |
|
| 90 |
result = todo_collection.insert_one(task)
|
| 91 |
if result.inserted_id:
|
|
@@ -103,15 +99,10 @@ async def create_todo_item(todo_data: ToDoCreate, current_user: str = Depends(ge
|
|
| 103 |
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required.")
|
| 104 |
|
| 105 |
todo_item = todo_data.dict()
|
| 106 |
-
todo_item.update({
|
| 107 |
-
"is_completed": False,
|
| 108 |
-
"created_by": current_user,
|
| 109 |
-
"created_at": datetime.utcnow(),
|
| 110 |
-
})
|
| 111 |
result = todo_collection.insert_one(todo_item)
|
| 112 |
if result.inserted_id:
|
| 113 |
-
return JSONResponse(status_code=status.HTTP_201_CREATED,
|
| 114 |
-
content={"message": "To-do item created successfully."})
|
| 115 |
else:
|
| 116 |
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to create to-do item.")
|
| 117 |
|
|
@@ -123,7 +114,6 @@ async def get_all_todo_items(current_user: str = Depends(get_current_user)):
|
|
| 123 |
"""
|
| 124 |
if not is_admin_user(current_user):
|
| 125 |
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required.")
|
| 126 |
-
|
| 127 |
return list(todo_collection.find({}))
|
| 128 |
|
| 129 |
|
|
@@ -175,11 +165,9 @@ async def complete_assigned_todo_item(todo_id: str, current_user: str = Depends(
|
|
| 175 |
"""
|
| 176 |
todo = todo_collection.find_one({"_id": ObjectId(todo_id), "assigned_to": current_user})
|
| 177 |
if not todo:
|
| 178 |
-
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
| 179 |
-
detail="To-do item not found or not assigned to you.")
|
| 180 |
|
| 181 |
-
todo_collection.update_one({"_id": ObjectId(todo_id)},
|
| 182 |
-
{"$set": {"is_completed": True, "completed_at": datetime.utcnow()}})
|
| 183 |
return {"message": f"To-do item '{todo.get('title')}' marked as completed."}
|
| 184 |
|
| 185 |
|
|
@@ -242,8 +230,7 @@ async def export_health_pdf(current_user: str = Depends(get_current_user)):
|
|
| 242 |
buffer.seek(0)
|
| 243 |
|
| 244 |
log_activity("Exported health records as PDF", current_user)
|
| 245 |
-
return StreamingResponse(buffer, media_type="application/pdf",
|
| 246 |
-
headers={"Content-Disposition": "attachment;filename=health_records.pdf"})
|
| 247 |
|
| 248 |
|
| 249 |
@dashboard_router.get("/export/health/xls")
|
|
@@ -260,6 +247,4 @@ async def export_health_xls(current_user: str = Depends(get_current_user)):
|
|
| 260 |
df.to_excel(file_path, index=False)
|
| 261 |
|
| 262 |
log_activity("Exported health records as XLS", current_user)
|
| 263 |
-
return FileResponse(file_path, media_type="application/vnd.ms-excel",
|
| 264 |
-
headers={"Content-Disposition": "attachment;filename=health_records.xlsx"})
|
| 265 |
-
|
|
|
|
| 1 |
+
# routes/administration.py
|
| 2 |
+
|
| 3 |
import io
|
| 4 |
from datetime import datetime
|
| 5 |
+
from typing import List, Optional
|
|
|
|
| 6 |
import pandas as pd
|
| 7 |
from bson import ObjectId
|
| 8 |
from fastapi import APIRouter, Depends, Request, HTTPException, status
|
|
|
|
| 10 |
from fastapi.templating import Jinja2Templates
|
| 11 |
from reportlab.lib.pagesizes import letter
|
| 12 |
from reportlab.pdfgen import canvas
|
|
|
|
| 13 |
from models.schemas.todo_schema import ToDoItem, ToDoCreate, ToDoUpdate
|
| 14 |
from services.auth_service import get_current_user, is_admin_user
|
| 15 |
from services.health_monitoring_service import evaluate_health_data
|
|
|
|
| 33 |
"""
|
| 34 |
Display health metrics with real-time and historical alerts on the dashboard.
|
| 35 |
"""
|
| 36 |
+
# Retrieve current metrics and alerts
|
| 37 |
+
current_health_metrics = {"weight_loss_percentage": 4, "mortality_rate": 1, "reduced_feed_intake_percentage": 8}
|
|
|
|
|
|
|
|
|
|
| 38 |
real_time_alerts = evaluate_health_data(current_health_metrics).get("notifications", [])
|
| 39 |
historical_alerts = list(health_collection.find().sort("timestamp", -1).limit(20))
|
| 40 |
|
|
|
|
| 47 |
return templates.TemplateResponse("dashboard.html", context)
|
| 48 |
|
| 49 |
|
| 50 |
+
@dashboard_router.get("/health_dashboard", response_class=HTMLResponse)
|
| 51 |
+
async def health_dashboard_view(request: Request):
|
| 52 |
+
"""
|
| 53 |
+
Render the health dashboard with the latest 10 health records.
|
| 54 |
+
"""
|
| 55 |
+
health_records = list(health_collection.find().sort("date", -1).limit(10))
|
| 56 |
+
return templates.TemplateResponse("health_dashboard.html", {"request": request, "health_records": health_records})
|
| 57 |
+
|
| 58 |
+
|
| 59 |
# --- Admin Task Management Routes ---
|
| 60 |
|
| 61 |
@dashboard_router.get("/tasks", response_class=HTMLResponse)
|
|
|
|
| 68 |
|
| 69 |
tasks = list(todo_collection.find({}))
|
| 70 |
farmers = list(user_collection.find({"role": "farmer"}))
|
| 71 |
+
context = {"request": request, "tasks": tasks, "farmers": farmers}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
return templates.TemplateResponse("admin_tasks.html", context)
|
| 73 |
|
| 74 |
|
|
|
|
| 81 |
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required.")
|
| 82 |
|
| 83 |
task = todo_data.dict()
|
| 84 |
+
task.update({"assigned_to": assigned_to, "created_by": current_user, "is_completed": False, "created_at": datetime.utcnow()})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
result = todo_collection.insert_one(task)
|
| 87 |
if result.inserted_id:
|
|
|
|
| 99 |
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required.")
|
| 100 |
|
| 101 |
todo_item = todo_data.dict()
|
| 102 |
+
todo_item.update({"is_completed": False, "created_by": current_user, "created_at": datetime.utcnow()})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
result = todo_collection.insert_one(todo_item)
|
| 104 |
if result.inserted_id:
|
| 105 |
+
return JSONResponse(status_code=status.HTTP_201_CREATED, content={"message": "To-do item created successfully."})
|
|
|
|
| 106 |
else:
|
| 107 |
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to create to-do item.")
|
| 108 |
|
|
|
|
| 114 |
"""
|
| 115 |
if not is_admin_user(current_user):
|
| 116 |
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required.")
|
|
|
|
| 117 |
return list(todo_collection.find({}))
|
| 118 |
|
| 119 |
|
|
|
|
| 165 |
"""
|
| 166 |
todo = todo_collection.find_one({"_id": ObjectId(todo_id), "assigned_to": current_user})
|
| 167 |
if not todo:
|
| 168 |
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="To-do item not found or not assigned to you.")
|
|
|
|
| 169 |
|
| 170 |
+
todo_collection.update_one({"_id": ObjectId(todo_id)}, {"$set": {"is_completed": True, "completed_at": datetime.utcnow()}})
|
|
|
|
| 171 |
return {"message": f"To-do item '{todo.get('title')}' marked as completed."}
|
| 172 |
|
| 173 |
|
|
|
|
| 230 |
buffer.seek(0)
|
| 231 |
|
| 232 |
log_activity("Exported health records as PDF", current_user)
|
| 233 |
+
return StreamingResponse(buffer, media_type="application/pdf", headers={"Content-Disposition": "attachment;filename=health_records.pdf"})
|
|
|
|
| 234 |
|
| 235 |
|
| 236 |
@dashboard_router.get("/export/health/xls")
|
|
|
|
| 247 |
df.to_excel(file_path, index=False)
|
| 248 |
|
| 249 |
log_activity("Exported health records as XLS", current_user)
|
| 250 |
+
return FileResponse(file_path, media_type="application/vnd.ms-excel", headers={"Content-Disposition": "attachment;filename=health_records.xlsx"})
|
|
|
|
|
|
routes/disease_detection.py
CHANGED
|
@@ -1,67 +1,87 @@
|
|
| 1 |
# routes/disease_detection.py
|
| 2 |
|
|
|
|
| 3 |
import logging
|
|
|
|
| 4 |
from fastapi import APIRouter, UploadFile, File, HTTPException
|
| 5 |
from fastapi.responses import JSONResponse
|
| 6 |
from PIL import Image
|
| 7 |
import numpy as np
|
| 8 |
-
|
|
|
|
| 9 |
from services.disease_detection_service import bot
|
|
|
|
| 10 |
|
| 11 |
-
#
|
| 12 |
logger = logging.getLogger(__name__)
|
| 13 |
|
| 14 |
# Initialize the disease detection router
|
| 15 |
disease_router = APIRouter()
|
| 16 |
|
|
|
|
| 17 |
@disease_router.post("/detect_disease", response_class=JSONResponse)
|
| 18 |
async def detect_disease(file: UploadFile = File(...)):
|
| 19 |
"""
|
| 20 |
-
Detect disease from an uploaded poultry
|
| 21 |
|
| 22 |
Parameters:
|
| 23 |
file (UploadFile): The image file to analyze.
|
| 24 |
|
| 25 |
Returns:
|
| 26 |
-
JSONResponse:
|
| 27 |
-
|
| 28 |
-
- status: Severity of the detected disease
|
| 29 |
-
- confidence: Confidence level of the prediction
|
| 30 |
-
- recommendation: Suggested treatment based on the diagnosis
|
| 31 |
-
- details: Additional information about the disease
|
| 32 |
"""
|
| 33 |
-
logger.info("
|
| 34 |
|
| 35 |
-
# Validate file type
|
| 36 |
if file.content_type not in ["image/jpeg", "image/png"]:
|
| 37 |
-
logger.warning(f"Invalid file type
|
| 38 |
raise HTTPException(status_code=400, detail="Invalid file type. Please upload a JPEG or PNG image.")
|
| 39 |
|
| 40 |
try:
|
| 41 |
-
#
|
| 42 |
image_data = await file.read()
|
| 43 |
image = Image.open(io.BytesIO(image_data)).convert("RGB")
|
| 44 |
image_np = np.array(image)
|
| 45 |
-
logger.info("Image successfully loaded and converted to RGB.")
|
| 46 |
except Exception as e:
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
raise HTTPException(status_code=500, detail=error_message)
|
| 50 |
|
| 51 |
-
#
|
| 52 |
try:
|
| 53 |
detailed_response, disease_name, status, recommendation, confidence = bot.diagnose_and_respond(image_np)
|
| 54 |
-
logger.info(f"Disease
|
| 55 |
except Exception as e:
|
| 56 |
-
logger.error(f"Error
|
| 57 |
raise HTTPException(status_code=500, detail="Error processing the image for disease detection.")
|
| 58 |
|
| 59 |
-
#
|
| 60 |
-
if disease_name
|
| 61 |
-
logger.warning("
|
| 62 |
-
return JSONResponse(content={"error": "Unable to detect disease. Please try
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
-
#
|
| 65 |
response_content = {
|
| 66 |
"disease": disease_name,
|
| 67 |
"status": status,
|
|
@@ -69,5 +89,5 @@ async def detect_disease(file: UploadFile = File(...)):
|
|
| 69 |
"recommendation": recommendation,
|
| 70 |
"details": detailed_response
|
| 71 |
}
|
| 72 |
-
logger.info(f"
|
| 73 |
return JSONResponse(content=response_content)
|
|
|
|
| 1 |
# routes/disease_detection.py
|
| 2 |
|
| 3 |
+
import datetime
|
| 4 |
import logging
|
| 5 |
+
import io
|
| 6 |
from fastapi import APIRouter, UploadFile, File, HTTPException
|
| 7 |
from fastapi.responses import JSONResponse
|
| 8 |
from PIL import Image
|
| 9 |
import numpy as np
|
| 10 |
+
|
| 11 |
+
from routes.administration import health_collection
|
| 12 |
from services.disease_detection_service import bot
|
| 13 |
+
from services.health_alerts_service import check_health_thresholds
|
| 14 |
|
| 15 |
+
# Configure logging for this module
|
| 16 |
logger = logging.getLogger(__name__)
|
| 17 |
|
| 18 |
# Initialize the disease detection router
|
| 19 |
disease_router = APIRouter()
|
| 20 |
|
| 21 |
+
|
| 22 |
@disease_router.post("/detect_disease", response_class=JSONResponse)
|
| 23 |
async def detect_disease(file: UploadFile = File(...)):
|
| 24 |
"""
|
| 25 |
+
Detect disease from an uploaded poultry image and provide treatment recommendations.
|
| 26 |
|
| 27 |
Parameters:
|
| 28 |
file (UploadFile): The image file to analyze.
|
| 29 |
|
| 30 |
Returns:
|
| 31 |
+
JSONResponse: JSON with disease prediction, confidence, treatment recommendation,
|
| 32 |
+
and other details.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
"""
|
| 34 |
+
logger.info("Received disease detection request.")
|
| 35 |
|
| 36 |
+
# Step 1: Validate the uploaded file type
|
| 37 |
if file.content_type not in ["image/jpeg", "image/png"]:
|
| 38 |
+
logger.warning(f"Invalid file type: {file.content_type}")
|
| 39 |
raise HTTPException(status_code=400, detail="Invalid file type. Please upload a JPEG or PNG image.")
|
| 40 |
|
| 41 |
try:
|
| 42 |
+
# Step 2: Read and process the image
|
| 43 |
image_data = await file.read()
|
| 44 |
image = Image.open(io.BytesIO(image_data)).convert("RGB")
|
| 45 |
image_np = np.array(image)
|
| 46 |
+
logger.info("Image successfully loaded and converted to RGB format.")
|
| 47 |
except Exception as e:
|
| 48 |
+
logger.error(f"Failed to load image: {e}")
|
| 49 |
+
raise HTTPException(status_code=500, detail="Error processing the uploaded image.")
|
|
|
|
| 50 |
|
| 51 |
+
# Step 3: Perform disease detection
|
| 52 |
try:
|
| 53 |
detailed_response, disease_name, status, recommendation, confidence = bot.diagnose_and_respond(image_np)
|
| 54 |
+
logger.info(f"Disease detected: {disease_name} with {confidence:.2f}% confidence.")
|
| 55 |
except Exception as e:
|
| 56 |
+
logger.error(f"Error in disease detection: {e}")
|
| 57 |
raise HTTPException(status_code=500, detail="Error processing the image for disease detection.")
|
| 58 |
|
| 59 |
+
# Step 4: Check if a valid disease was detected
|
| 60 |
+
if not disease_name:
|
| 61 |
+
logger.warning("No disease detected in the image.")
|
| 62 |
+
return JSONResponse(content={"error": "Unable to detect disease. Please try a different image."},
|
| 63 |
+
status_code=500)
|
| 64 |
+
|
| 65 |
+
# Step 5: Log the detected disease and recommended treatment in health records
|
| 66 |
+
record = {
|
| 67 |
+
"bird_id": "batch_123", # Placeholder, replace with actual bird/batch ID
|
| 68 |
+
"date": datetime.datetime.utcnow(),
|
| 69 |
+
"disease_detected": disease_name,
|
| 70 |
+
"status": status,
|
| 71 |
+
"confidence": confidence,
|
| 72 |
+
"treatment_recommendation": recommendation,
|
| 73 |
+
"weight": 1.4, # Placeholder, replace with actual weight measurement
|
| 74 |
+
"mortality_rate": 0.5, # Placeholder, replace with actual mortality rate
|
| 75 |
+
"feed_intake": 0.35 # Placeholder, replace with actual feed intake
|
| 76 |
+
}
|
| 77 |
+
health_collection.insert_one(record)
|
| 78 |
+
logger.info("Disease and treatment recommendation logged in health records.")
|
| 79 |
+
|
| 80 |
+
# Step 6: Check for health alerts based on recorded data
|
| 81 |
+
check_health_thresholds(record)
|
| 82 |
+
logger.info("Health thresholds checked for potential alerts.")
|
| 83 |
|
| 84 |
+
# Step 7: Construct and return response
|
| 85 |
response_content = {
|
| 86 |
"disease": disease_name,
|
| 87 |
"status": status,
|
|
|
|
| 89 |
"recommendation": recommendation,
|
| 90 |
"details": detailed_response
|
| 91 |
}
|
| 92 |
+
logger.info(f"Response content prepared: {response_content}")
|
| 93 |
return JSONResponse(content=response_content)
|
services/health_alerts_service.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# services/health_alerts_service.py
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
from services.utils import db
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
|
| 7 |
+
health_collection = db.get_collection("health_records")
|
| 8 |
+
alert_collection = db.get_collection("alerts")
|
| 9 |
+
|
| 10 |
+
# Define thresholds for automated alerts
|
| 11 |
+
THRESHOLDS = {
|
| 12 |
+
"weight": 1.0, # Example: Weight below 1.0 kg triggers alert
|
| 13 |
+
"mortality_rate": 2.0, # Mortality rate over 2% triggers alert
|
| 14 |
+
"feed_intake": 0.3 # Feed intake below 0.3 kg triggers alert
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
def check_health_thresholds(record):
|
| 18 |
+
alerts = []
|
| 19 |
+
if record["weight"] < THRESHOLDS["weight"]:
|
| 20 |
+
alerts.append("Low weight detected")
|
| 21 |
+
|
| 22 |
+
if record["mortality_rate"] > THRESHOLDS["mortality_rate"]:
|
| 23 |
+
alerts.append("High mortality rate detected")
|
| 24 |
+
|
| 25 |
+
if record["feed_intake"] < THRESHOLDS["feed_intake"]:
|
| 26 |
+
alerts.append("Reduced feed intake detected")
|
| 27 |
+
|
| 28 |
+
if alerts:
|
| 29 |
+
alert_record = {
|
| 30 |
+
"bird_id": record["bird_id"],
|
| 31 |
+
"date": datetime.utcnow(),
|
| 32 |
+
"alerts": alerts,
|
| 33 |
+
"status": record["status"],
|
| 34 |
+
"treatment_recommendation": record["treatment_recommendation"]
|
| 35 |
+
}
|
| 36 |
+
alert_collection.insert_one(alert_record)
|
| 37 |
+
# Send alert notification (e.g., email or SMS)
|
| 38 |
+
send_alert_notification(alert_record)
|
| 39 |
+
|
| 40 |
+
def send_alert_notification(alert):
|
| 41 |
+
farmer_email = os.getenv("FARMER_EMAIL")
|
| 42 |
+
# Logic to send notification via email or SMS
|
templates/health_dashboard.html
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block content %}
|
| 4 |
+
<div class="container">
|
| 5 |
+
<h1>Health & Disease Dashboard</h1>
|
| 6 |
+
|
| 7 |
+
<table class="table table-striped table-bordered">
|
| 8 |
+
<thead>
|
| 9 |
+
<tr>
|
| 10 |
+
<th>Bird ID</th>
|
| 11 |
+
<th>Date</th>
|
| 12 |
+
<th>Weight (kg)</th>
|
| 13 |
+
<th>Mortality Rate (%)</th>
|
| 14 |
+
<th>Feed Intake (kg)</th>
|
| 15 |
+
<th>Disease Detected</th>
|
| 16 |
+
<th>Status</th>
|
| 17 |
+
<th>Treatment Recommendation</th>
|
| 18 |
+
</tr>
|
| 19 |
+
</thead>
|
| 20 |
+
<tbody>
|
| 21 |
+
{% for record in health_records %}
|
| 22 |
+
<tr>
|
| 23 |
+
<td>{{ record.bird_id }}</td>
|
| 24 |
+
<td>{{ record.date.strftime('%Y-%m-%d') }}</td>
|
| 25 |
+
<td>{{ record.weight }}</td>
|
| 26 |
+
<td>{{ record.mortality_rate }}</td>
|
| 27 |
+
<td>{{ record.feed_intake }}</td>
|
| 28 |
+
<td>{{ record.disease_detected or 'N/A' }}</td>
|
| 29 |
+
<td>{{ record.status }}</td>
|
| 30 |
+
<td>{{ record.treatment_recommendation or 'N/A' }}</td>
|
| 31 |
+
</tr>
|
| 32 |
+
{% endfor %}
|
| 33 |
+
</tbody>
|
| 34 |
+
</table>
|
| 35 |
+
</div>
|
| 36 |
+
{% endblock %}
|