DermaDetect / backend /api /views.py
WilfredAyine's picture
deploy: clean initial commit for Hugging Face Spaces (no binary blobs)
1947612
from __future__ import annotations
import logging
from rest_framework import status
from rest_framework.parsers import MultiPartParser, JSONParser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from .groq_client import generate_clinical_response
from .inference import model_ready, run_inference
from .serializers import AnalyseRequestSerializer
from .pdf_generator import build_pdf
logger = logging.getLogger(__name__)
MODEL_VERSION = "dermavision-dinov2-v1"
class HealthView(APIView):
"""GET /health — liveness + model readiness probe."""
def get(self, request: Request) -> Response:
return Response({
"status": "ok",
"model_loaded": model_ready(),
"model_version": MODEL_VERSION,
})
class AnalyseView(APIView):
"""
POST /analyse
Multipart fields:
image (required) — skin lesion image file
include_heatmap (bool, default False)
include_narrative (bool, default True)
patient_name (str, optional)
patient_age (str, optional)
patient_sex (str, optional)
symptoms (str, optional)
Returns the full structured clinical response the frontend needs directly —
primaryFinding, confidence, urgency, urgencyText, treatmentNotes,
recommendedAction, referralNote, conditionCode, predictions, model_version.
"""
parser_classes = [MultiPartParser, JSONParser]
def post(self, request: Request) -> Response:
serializer = AnalyseRequestSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
image_file = serializer.validated_data["image"]
include_heatmap = serializer.validated_data["include_heatmap"]
include_narrative = serializer.validated_data["include_narrative"]
# Patient context — all optional, passed through to Groq prompt
patient_name = request.data.get("patient_name", "").strip()
patient_age = request.data.get("patient_age", "").strip()
patient_sex = request.data.get("patient_sex", "").strip()
symptoms = request.data.get("symptoms", "").strip()
image_bytes = image_file.read()
# --- Run ONNX inference ---
try:
inference_result = run_inference(image_bytes, include_heatmap=include_heatmap)
except FileNotFoundError as exc:
logger.error("Model not found: %s", exc)
return Response(
{"error": "Model not loaded. Please contact the administrator."},
status=status.HTTP_503_SERVICE_UNAVAILABLE,
)
except Exception as exc:
logger.exception("Inference error: %s", exc)
return Response(
{"error": "Inference failed. Check server logs."},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
predictions = inference_result["predictions"] # top-3 [{label, confidence}]
heatmap_b64 = inference_result["heatmap_b64"]
# --- Generate structured clinical response via Groq ---
clinical = {}
if include_narrative:
try:
clinical = generate_clinical_response(
predictions=predictions,
patient_name=patient_name,
patient_age=patient_age,
patient_sex=patient_sex,
symptoms=symptoms,
)
except Exception as exc:
logger.warning("Clinical response generation failed: %s", exc)
# --- Build final response ---
# clinical already contains all frontend fields.
# We add predictions + heatmap + model_version on top.
response_data = {
# Full structured fields from Groq (or fallback)
"primaryFinding": clinical.get("primaryFinding", predictions[0]["label"]),
"confidence": clinical.get("confidence", round(predictions[0]["confidence"] * 100)),
"urgency": clinical.get("urgency", "Moderate"),
"urgencyText": clinical.get("urgencyText", "Refer to clinic within 3 days."),
"treatmentNotes": clinical.get("treatmentNotes", []),
"recommendedAction": clinical.get("recommendedAction", "Refer to appropriate specialist."),
"referralNote": clinical.get("referralNote", ""),
"conditionCode": clinical.get("conditionCode", "ringworm"),
"therapyRegimen": clinical.get("therapyRegimen", {}),
"patientHandout": clinical.get("patientHandout", {}),
# Raw model output — kept so frontend can show differential runner-ups
"allPredictions": predictions,
"heatmap_b64": heatmap_b64,
"model_version": MODEL_VERSION,
}
logger.info(
"Analyse complete — patient: %s | finding: %s | urgency: %s",
patient_name or "anonymous",
response_data["primaryFinding"],
response_data["urgency"],
)
return Response(response_data, status=status.HTTP_200_OK)
class GeneratePdfView(APIView):
"""
POST /pdf
Expects a JSON payload containing:
- case_id
- patient (dict with name, age, sex, symptoms, healthWorkerName)
- clinical (dict with primaryFinding, confidence, urgency, referralNote, treatmentNotes)
- images (dict with original_b64, heatmap_b64)
"""
parser_classes = [JSONParser]
def post(self, request: Request) -> Response:
try:
data = request.data
case_id = data.get("case_id", "NEW-CASE")
patient = data.get("patient", {})
clinical = data.get("clinical", {})
images = data.get("images", {})
pdf_b64 = build_pdf(case_id, patient, clinical, images)
return Response({"pdf_b64": pdf_b64}, status=status.HTTP_200_OK)
except Exception as exc:
logger.exception("Failed to generate PDF: %s", exc)
return Response({"error": str(exc)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)