larxius commited on
Commit ·
d288282
1
Parent(s): 7361fab
feat(lab): Lab Report Analysis - OCR + Groq AI
Browse files- .env.example +3 -0
- app.py +35 -0
- lab_prediction.py +127 -0
- ocr_utils.py +67 -0
- requirements.txt +3 -0
- routes/doctor.py +5 -0
- templates/doctor/lab_analysis.html +124 -0
- templates/layout.html +6 -0
.env.example
CHANGED
|
@@ -34,5 +34,8 @@ MAIL_USERNAME=
|
|
| 34 |
MAIL_PASSWORD=
|
| 35 |
MAIL_DEFAULT_SENDER=
|
| 36 |
|
|
|
|
|
|
|
|
|
|
| 37 |
# Set ALLOW_DROP_TABLES=true ONLY for a deliberate local reset. Never on prod.
|
| 38 |
ALLOW_DROP_TABLES=false
|
|
|
|
| 34 |
MAIL_PASSWORD=
|
| 35 |
MAIL_DEFAULT_SENDER=
|
| 36 |
|
| 37 |
+
# Lab report AI (Groq API-only, no local model)
|
| 38 |
+
GROQ_API_KEY=
|
| 39 |
+
|
| 40 |
# Set ALLOW_DROP_TABLES=true ONLY for a deliberate local reset. Never on prod.
|
| 41 |
ALLOW_DROP_TABLES=false
|
app.py
CHANGED
|
@@ -562,6 +562,41 @@ def create_app():
|
|
| 562 |
except Exception as e:
|
| 563 |
return jsonify({'success': False, 'error': str(e)}), 500
|
| 564 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 565 |
@app.route('/api/save_gastro_audit', methods=['POST'])
|
| 566 |
def save_gastro_audit():
|
| 567 |
try:
|
|
|
|
| 562 |
except Exception as e:
|
| 563 |
return jsonify({'success': False, 'error': str(e)}), 500
|
| 564 |
|
| 565 |
+
@app.route('/api/extract_lab_report', methods=['POST'])
|
| 566 |
+
def extract_lab_report():
|
| 567 |
+
try:
|
| 568 |
+
if 'file' not in request.files:
|
| 569 |
+
return jsonify({'success': False, 'error': 'No file uploaded.'})
|
| 570 |
+
f = request.files['file']
|
| 571 |
+
if not f.filename:
|
| 572 |
+
return jsonify({'success': False, 'error': 'No file selected.'})
|
| 573 |
+
import uuid
|
| 574 |
+
from werkzeug.utils import secure_filename
|
| 575 |
+
fname = f"{uuid.uuid4().hex}_{secure_filename(f.filename)}"
|
| 576 |
+
fpath = os.path.join(app.config['UPLOAD_FOLDER'], fname)
|
| 577 |
+
f.save(fpath)
|
| 578 |
+
from lab_prediction import process_lab_image
|
| 579 |
+
return jsonify(process_lab_image(fpath))
|
| 580 |
+
except Exception as e:
|
| 581 |
+
return jsonify({'success': False, 'error': str(e)}), 500
|
| 582 |
+
|
| 583 |
+
@app.route('/api/predict_lab_report', methods=['POST'])
|
| 584 |
+
def predict_lab_report():
|
| 585 |
+
try:
|
| 586 |
+
data = request.json or {}
|
| 587 |
+
from lab_prediction import predict_lab_disease, FEATURES
|
| 588 |
+
values = {k: (data.get(k) if data.get(k) != '' else None) for k in FEATURES}
|
| 589 |
+
for k in list(values):
|
| 590 |
+
v = values[k]
|
| 591 |
+
if k != 'Gender' and v is not None:
|
| 592 |
+
try:
|
| 593 |
+
values[k] = float(v)
|
| 594 |
+
except (ValueError, TypeError):
|
| 595 |
+
pass
|
| 596 |
+
return jsonify(predict_lab_disease(values))
|
| 597 |
+
except Exception as e:
|
| 598 |
+
return jsonify({'success': False, 'error': str(e)}), 500
|
| 599 |
+
|
| 600 |
@app.route('/api/save_gastro_audit', methods=['POST'])
|
| 601 |
def save_gastro_audit():
|
| 602 |
try:
|
lab_prediction.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Lab report disease prediction — Groq API only (no local .pkl model).
|
| 2 |
+
|
| 3 |
+
Pipeline:
|
| 4 |
+
process_lab_image(path) -> OCR (ocr_utils) -> 13-field dict + raw text
|
| 5 |
+
predict_lab_disease(values) -> Groq chat-completions -> disease + confidence
|
| 6 |
+
|
| 7 |
+
Predictable diseases: Anemia, Asthma, Heart Attack, Liver Disease,
|
| 8 |
+
Infection, Diabetics, Cardiovascular Disease, Pancreatitis, Kidney Disease.
|
| 9 |
+
"""
|
| 10 |
+
import json
|
| 11 |
+
import os
|
| 12 |
+
import re
|
| 13 |
+
|
| 14 |
+
import requests
|
| 15 |
+
|
| 16 |
+
FEATURES = [
|
| 17 |
+
"Gender", "Age", "Hemoglobin", "RBC", "WBC", "AST", "ALT",
|
| 18 |
+
"Cholesterol", "Spirometry", "Creatinine", "Glucose", "Lipase", "Troponin",
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
DISEASES = [
|
| 22 |
+
"Anemia", "Asthma", "Heart Attack", "Liver Disease", "Infection",
|
| 23 |
+
"Diabetics", "Cardiovascular Disease", "Pancreatitis", "Kidney Disease",
|
| 24 |
+
]
|
| 25 |
+
|
| 26 |
+
MODEL_CANDIDATES = [
|
| 27 |
+
"openai/gpt-oss-120b",
|
| 28 |
+
"openai/gpt-oss-20b",
|
| 29 |
+
"qwen/qwen3-32b",
|
| 30 |
+
"llama-3.3-70b-versatile",
|
| 31 |
+
"llama-3.1-8b-instant",
|
| 32 |
+
]
|
| 33 |
+
|
| 34 |
+
GROQ_URL = "https://api.groq.com/openai/v1/chat/completions"
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _groq_key() -> str:
|
| 38 |
+
return os.environ.get("GROQ_API_KEY", "")
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def process_lab_image(image_path: str) -> dict:
|
| 42 |
+
"""OCR a lab report image -> {success, extracted_values, raw_text}."""
|
| 43 |
+
from ocr_utils import ocr_image, extract_lab_values
|
| 44 |
+
try:
|
| 45 |
+
raw = ocr_image(image_path)
|
| 46 |
+
except Exception as e:
|
| 47 |
+
return {"success": False, "error": f"OCR failed: {e}"}
|
| 48 |
+
return {
|
| 49 |
+
"success": True,
|
| 50 |
+
"extracted_values": extract_lab_values(raw),
|
| 51 |
+
"raw_text": raw[:2000],
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _prompt(values: dict) -> str:
|
| 56 |
+
lines = "\n".join(f"- {k}: {values.get(k)}" for k in FEATURES)
|
| 57 |
+
return (
|
| 58 |
+
"You are a clinical lab assistant. Given these lab values:\n"
|
| 59 |
+
f"{lines}\n\n"
|
| 60 |
+
f"Possible diseases: {', '.join(DISEASES)}.\n"
|
| 61 |
+
"Reply with ONLY valid JSON, no markdown, no extra text, "
|
| 62 |
+
'in this shape: {"disease": "<one of the list>", '
|
| 63 |
+
'"confidence": <0-100 number>, '
|
| 64 |
+
'"top3": [{"disease": "<name>", "confidence": <0-100>}]}'
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _parse_reply(text: str) -> dict:
|
| 69 |
+
cleaned = text.strip()
|
| 70 |
+
if cleaned.startswith("```"):
|
| 71 |
+
cleaned = re_sub_fences(cleaned)
|
| 72 |
+
data = json.loads(cleaned)
|
| 73 |
+
disease = str(data.get("disease", "")).strip()
|
| 74 |
+
conf = float(data.get("confidence", 0))
|
| 75 |
+
top3 = data.get("top3") or [{"disease": disease, "confidence": conf}]
|
| 76 |
+
return {"disease": disease, "confidence": round(conf, 1), "top3": top3}
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def re_sub_fences(s: str) -> str:
|
| 80 |
+
s = re.sub(r"^```[a-zA-Z]*\n?", "", s)
|
| 81 |
+
s = re.sub(r"\n?```$", "", s)
|
| 82 |
+
return s.strip()
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def predict_lab_disease(values: dict) -> dict:
|
| 86 |
+
"""Call Groq (model fallback chain) -> disease prediction dict."""
|
| 87 |
+
api_key = _groq_key()
|
| 88 |
+
if not api_key:
|
| 89 |
+
return {"success": False, "error": "GROQ_API_KEY is not set on the server."}
|
| 90 |
+
|
| 91 |
+
missing = [k for k in FEATURES if values.get(k) in (None, "")]
|
| 92 |
+
prompt = _prompt(values)
|
| 93 |
+
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
| 94 |
+
last_err = "unknown error"
|
| 95 |
+
|
| 96 |
+
for model in MODEL_CANDIDATES:
|
| 97 |
+
try:
|
| 98 |
+
r = requests.post(
|
| 99 |
+
GROQ_URL,
|
| 100 |
+
headers=headers,
|
| 101 |
+
json={
|
| 102 |
+
"model": model,
|
| 103 |
+
"messages": [{"role": "user", "content": prompt}],
|
| 104 |
+
"temperature": 0.2,
|
| 105 |
+
"max_tokens": 512,
|
| 106 |
+
},
|
| 107 |
+
timeout=60,
|
| 108 |
+
)
|
| 109 |
+
if r.status_code != 200:
|
| 110 |
+
last_err = f"{model}: HTTP {r.status_code} {r.text[:200]}"
|
| 111 |
+
continue
|
| 112 |
+
content = r.json()["choices"][0]["message"]["content"]
|
| 113 |
+
parsed = _parse_reply(content)
|
| 114 |
+
if parsed["disease"] not in DISEASES:
|
| 115 |
+
# Accept model answer anyway but flag it
|
| 116 |
+
pass
|
| 117 |
+
parsed.update({
|
| 118 |
+
"success": True,
|
| 119 |
+
"missing": missing,
|
| 120 |
+
"accuracy": f"Groq AI ({model})",
|
| 121 |
+
})
|
| 122 |
+
return parsed
|
| 123 |
+
except Exception as e:
|
| 124 |
+
last_err = f"{model}: {e}"
|
| 125 |
+
continue
|
| 126 |
+
|
| 127 |
+
return {"success": False, "error": f"AI Prediction Failed: {last_err}"}
|
ocr_utils.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""OCR helpers for lab report images (EasyOCR + OpenCV + regex).
|
| 2 |
+
|
| 3 |
+
13 extracted fields: Gender, Age, Hemoglobin, RBC, WBC, AST, ALT,
|
| 4 |
+
Cholesterol, Spirometry, Creatinine, Glucose, Lipase, Troponin.
|
| 5 |
+
Missing values are returned as None.
|
| 6 |
+
"""
|
| 7 |
+
import re
|
| 8 |
+
|
| 9 |
+
_reader = None
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def get_reader():
|
| 13 |
+
"""Lazy singleton EasyOCR reader (downloads ~100MB weights on first call)."""
|
| 14 |
+
global _reader
|
| 15 |
+
if _reader is None:
|
| 16 |
+
import easyocr
|
| 17 |
+
_reader = easyocr.Reader(['en'], gpu=False)
|
| 18 |
+
return _reader
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def ocr_image(image_path: str) -> str:
|
| 22 |
+
"""Run OCR on an image file and return raw text."""
|
| 23 |
+
import cv2
|
| 24 |
+
reader = get_reader()
|
| 25 |
+
img = cv2.imread(image_path)
|
| 26 |
+
if img is None:
|
| 27 |
+
raise ValueError("Could not read image. Use JPG/PNG.")
|
| 28 |
+
# EasyOCR handles numpy arrays (BGR) directly.
|
| 29 |
+
results = reader.readtext(img)
|
| 30 |
+
return " ".join([t[1] for t in results])
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _num(patterns, text, cast=float):
|
| 34 |
+
for pat in patterns:
|
| 35 |
+
m = re.search(pat, text, re.IGNORECASE)
|
| 36 |
+
if m:
|
| 37 |
+
try:
|
| 38 |
+
return cast(m.group(1).replace(',', ''))
|
| 39 |
+
except (ValueError, IndexError):
|
| 40 |
+
continue
|
| 41 |
+
return None
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def extract_lab_values(text: str) -> dict:
|
| 45 |
+
"""Regex-extract the 13 lab fields from OCR text (None if not found)."""
|
| 46 |
+
t = text or ""
|
| 47 |
+
gender = None
|
| 48 |
+
if re.search(r'\bfemale\b', t, re.IGNORECASE):
|
| 49 |
+
gender = "Female"
|
| 50 |
+
elif re.search(r'\bmale\b', t, re.IGNORECASE):
|
| 51 |
+
gender = "Male"
|
| 52 |
+
|
| 53 |
+
return {
|
| 54 |
+
"Gender": gender,
|
| 55 |
+
"Age": _num([r'age\D{0,10}(\d{1,3})'], t, int),
|
| 56 |
+
"Hemoglobin": _num([r'h(?:ae)?moglobin\D{0,15}(\d+\.?\d*)', r'\bHb\D{0,10}(\d+\.?\d*)'], t),
|
| 57 |
+
"RBC": _num([r'\bRBC\D{0,10}(\d+\.?\d*)'], t),
|
| 58 |
+
"WBC": _num([r'\bWBC\D{0,10}([\d,]+\.?\d*)', r'W\.?B\.?C\.?\D{0,10}([\d,]+\.?\d*)'], t),
|
| 59 |
+
"AST": _num([r'\bAST\D{0,10}(\d+\.?\d*)', r'\bSGOT\D{0,10}(\d+\.?\d*)'], t),
|
| 60 |
+
"ALT": _num([r'\bALT\D{0,10}(\d+\.?\d*)', r'\bSGPT\D{0,10}(\d+\.?\d*)'], t),
|
| 61 |
+
"Cholesterol": _num([r'cholesterol\D{0,15}(\d+\.?\d*)'], t),
|
| 62 |
+
"Spirometry": _num([r'spirometry\D{0,15}(\d+\.?\d*)', r'\bFEV1?\D{0,10}(\d+\.?\d*)'], t),
|
| 63 |
+
"Creatinine": _num([r'creatinine\D{0,15}(\d+\.?\d*)'], t),
|
| 64 |
+
"Glucose": _num([r'glucose\D{0,15}(\d+\.?\d*)', r'blood sugar\D{0,15}(\d+\.?\d*)'], t),
|
| 65 |
+
"Lipase": _num([r'lipase\D{0,15}(\d+\.?\d*)'], t),
|
| 66 |
+
"Troponin": _num([r'troponin\D{0,15}(\d+\.?\d*)'], t),
|
| 67 |
+
}
|
requirements.txt
CHANGED
|
@@ -17,5 +17,8 @@ python-dotenv
|
|
| 17 |
# --- AI image model (CPU builds recommended, see Dockerfile) ---
|
| 18 |
torch
|
| 19 |
torchvision
|
|
|
|
|
|
|
|
|
|
| 20 |
# --- Optional: GCP Cloud SQL Python Connector (only needed if USE_CLOUD_SQL_CONNECTOR=true) ---
|
| 21 |
# cloud-sql-python-connector[pg8000]
|
|
|
|
| 17 |
# --- AI image model (CPU builds recommended, see Dockerfile) ---
|
| 18 |
torch
|
| 19 |
torchvision
|
| 20 |
+
# --- Lab report OCR ---
|
| 21 |
+
opencv-python==4.8.1.78
|
| 22 |
+
easyocr==1.7.1
|
| 23 |
# --- Optional: GCP Cloud SQL Python Connector (only needed if USE_CLOUD_SQL_CONNECTOR=true) ---
|
| 24 |
# cloud-sql-python-connector[pg8000]
|
routes/doctor.py
CHANGED
|
@@ -281,3 +281,8 @@ def gastro_analysis():
|
|
| 281 |
@login_required
|
| 282 |
def urology_analysis():
|
| 283 |
return render_template('doctor/urology_analysis.html')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
@login_required
|
| 282 |
def urology_analysis():
|
| 283 |
return render_template('doctor/urology_analysis.html')
|
| 284 |
+
|
| 285 |
+
@doctor_bp.route('/lab_analysis')
|
| 286 |
+
@login_required
|
| 287 |
+
def lab_analysis():
|
| 288 |
+
return render_template('doctor/lab_analysis.html')
|
templates/doctor/lab_analysis.html
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'layout.html' %}
|
| 2 |
+
{% block content %}
|
| 3 |
+
<div class="container-fluid py-4">
|
| 4 |
+
<div class="row mb-4">
|
| 5 |
+
<div class="col-12">
|
| 6 |
+
<h2>Lab Report Analysis</h2>
|
| 7 |
+
<p class="text-muted">Upload a lab report image to auto-fill values, verify them, then predict disease (Groq AI).</p>
|
| 8 |
+
</div>
|
| 9 |
+
</div>
|
| 10 |
+
<div class="row justify-content-center">
|
| 11 |
+
<div class="col-12 col-xl-10">
|
| 12 |
+
<div class="card shadow-sm mb-4">
|
| 13 |
+
<div class="card-body p-4">
|
| 14 |
+
<h5 class="card-title mb-3"><i data-lucide="upload" class="me-2"></i>1. Upload Lab Report (JPG/PNG)</h5>
|
| 15 |
+
<div class="d-flex gap-2 align-items-center flex-wrap">
|
| 16 |
+
<input type="file" id="lab-file" class="form-control" accept=".jpg,.jpeg,.png,image/*" style="max-width: 400px;">
|
| 17 |
+
<button type="button" class="btn btn-secondary" id="btn-extract">
|
| 18 |
+
<i data-lucide="scan" class="me-2"></i>Extract Values
|
| 19 |
+
</button>
|
| 20 |
+
<span class="text-muted small" id="extract-status">No file processed.</span>
|
| 21 |
+
</div>
|
| 22 |
+
</div>
|
| 23 |
+
</div>
|
| 24 |
+
|
| 25 |
+
<form id="labForm">
|
| 26 |
+
<div class="card shadow-sm mb-4">
|
| 27 |
+
<div class="card-body p-4">
|
| 28 |
+
<h5 class="card-title mb-3"><i data-lucide="clipboard-check" class="me-2"></i>2. Verify / Enter Values</h5>
|
| 29 |
+
<div class="row g-2">
|
| 30 |
+
<div class="col-md-3"><label class="form-label">Gender</label><select class="form-select" id="Gender"><option value="">--</option><option>Male</option><option>Female</option></select></div>
|
| 31 |
+
<div class="col-md-3"><label class="form-label">Age</label><input type="number" class="form-control" id="Age" step="1"></div>
|
| 32 |
+
<div class="col-md-3"><label class="form-label">Hemoglobin</label><input type="number" class="form-control" id="Hemoglobin" step="0.1"></div>
|
| 33 |
+
<div class="col-md-3"><label class="form-label">RBC</label><input type="number" class="form-control" id="RBC" step="0.01"></div>
|
| 34 |
+
<div class="col-md-3"><label class="form-label">WBC</label><input type="number" class="form-control" id="WBC" step="1"></div>
|
| 35 |
+
<div class="col-md-3"><label class="form-label">AST</label><input type="number" class="form-control" id="AST" step="0.1"></div>
|
| 36 |
+
<div class="col-md-3"><label class="form-label">ALT</label><input type="number" class="form-control" id="ALT" step="0.1"></div>
|
| 37 |
+
<div class="col-md-3"><label class="form-label">Cholesterol</label><input type="number" class="form-control" id="Cholesterol" step="1"></div>
|
| 38 |
+
<div class="col-md-3"><label class="form-label">Spirometry</label><input type="number" class="form-control" id="Spirometry" step="0.01"></div>
|
| 39 |
+
<div class="col-md-3"><label class="form-label">Creatinine</label><input type="number" class="form-control" id="Creatinine" step="0.01"></div>
|
| 40 |
+
<div class="col-md-3"><label class="form-label">Glucose</label><input type="number" class="form-control" id="Glucose" step="1"></div>
|
| 41 |
+
<div class="col-md-3"><label class="form-label">Lipase</label><input type="number" class="form-control" id="Lipase" step="1"></div>
|
| 42 |
+
<div class="col-md-3"><label class="form-label">Troponin</label><input type="number" class="form-control" id="Troponin" step="0.001"></div>
|
| 43 |
+
</div>
|
| 44 |
+
<div class="d-flex justify-content-end mt-3">
|
| 45 |
+
<button type="button" class="btn btn-primary px-4 py-2 fw-bold shadow-sm" id="btn-predict-lab">
|
| 46 |
+
<i data-lucide="cpu" class="me-2"></i>Predict Disease
|
| 47 |
+
</button>
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
</div>
|
| 51 |
+
</form>
|
| 52 |
+
|
| 53 |
+
<div id="lab-result" style="display:none;" class="card shadow-sm border-0">
|
| 54 |
+
<div class="card-body p-4">
|
| 55 |
+
<h5 class="card-title text-primary border-bottom pb-2 mb-3"><i data-lucide="activity" class="me-2"></i>Prediction Result</h5>
|
| 56 |
+
<div class="alert alert-info mb-3">
|
| 57 |
+
<div class="small text-muted text-uppercase fw-bold">Predicted Disease</div>
|
| 58 |
+
<h4 id="lab-disease" class="mb-1"></h4>
|
| 59 |
+
<small class="text-muted">Confidence: <span id="lab-confidence"></span>% (<span id="lab-model"></span>)</small>
|
| 60 |
+
</div>
|
| 61 |
+
<h6>Top predictions</h6>
|
| 62 |
+
<ul id="lab-top3" class="list-group mb-0"></ul>
|
| 63 |
+
</div>
|
| 64 |
+
</div>
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
|
| 69 |
+
<script>
|
| 70 |
+
const LAB_FIELDS = ["Gender","Age","Hemoglobin","RBC","WBC","AST","ALT","Cholesterol","Spirometry","Creatinine","Glucose","Lipase","Troponin"];
|
| 71 |
+
|
| 72 |
+
document.getElementById('btn-extract').addEventListener('click', function () {
|
| 73 |
+
const fileInput = document.getElementById('lab-file');
|
| 74 |
+
const status = document.getElementById('extract-status');
|
| 75 |
+
if (!fileInput.files.length) { alert('Select an image first.'); return; }
|
| 76 |
+
const btn = this; btn.disabled = true; status.innerText = 'Running OCR...';
|
| 77 |
+
const fd = new FormData(); fd.append('file', fileInput.files[0]);
|
| 78 |
+
fetch('/api/extract_lab_report', { method: 'POST', body: fd })
|
| 79 |
+
.then(r => r.json())
|
| 80 |
+
.then(data => {
|
| 81 |
+
btn.disabled = false;
|
| 82 |
+
if (data.success) {
|
| 83 |
+
const v = data.extracted_values || {};
|
| 84 |
+
LAB_FIELDS.forEach(f => {
|
| 85 |
+
const el = document.getElementById(f);
|
| 86 |
+
if (el && v[f] !== null && v[f] !== undefined) el.value = v[f];
|
| 87 |
+
});
|
| 88 |
+
status.innerText = 'Values extracted — please verify.';
|
| 89 |
+
} else {
|
| 90 |
+
status.innerText = 'Extract failed: ' + data.error;
|
| 91 |
+
}
|
| 92 |
+
})
|
| 93 |
+
.catch(e => { btn.disabled = false; status.innerText = 'Error: ' + e; });
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
+
document.getElementById('btn-predict-lab').addEventListener('click', function () {
|
| 97 |
+
const payload = {};
|
| 98 |
+
LAB_FIELDS.forEach(f => { payload[f] = document.getElementById(f).value; });
|
| 99 |
+
const btn = this; btn.disabled = true; btn.innerText = 'Predicting...';
|
| 100 |
+
fetch('/api/predict_lab_report', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload) })
|
| 101 |
+
.then(r => r.json())
|
| 102 |
+
.then(data => {
|
| 103 |
+
btn.disabled = false; btn.innerHTML = 'Predict Disease';
|
| 104 |
+
if (data.success) {
|
| 105 |
+
document.getElementById('lab-result').style.display = 'block';
|
| 106 |
+
document.getElementById('lab-disease').innerText = data.disease;
|
| 107 |
+
document.getElementById('lab-confidence').innerText = data.confidence;
|
| 108 |
+
document.getElementById('lab-model').innerText = data.accuracy || '';
|
| 109 |
+
const ul = document.getElementById('lab-top3'); ul.innerHTML = '';
|
| 110 |
+
(data.top3 || []).forEach(t => {
|
| 111 |
+
const li = document.createElement('li');
|
| 112 |
+
li.className = 'list-group-item d-flex justify-content-between';
|
| 113 |
+
li.innerHTML = '<span>' + t.disease + '</span><strong>' + t.confidence + '%</strong>';
|
| 114 |
+
ul.appendChild(li);
|
| 115 |
+
});
|
| 116 |
+
document.getElementById('lab-result').scrollIntoView({ behavior: 'smooth' });
|
| 117 |
+
} else {
|
| 118 |
+
alert('AI Prediction Failed: ' + data.error);
|
| 119 |
+
}
|
| 120 |
+
})
|
| 121 |
+
.catch(e => { btn.disabled = false; alert('Error: ' + e); });
|
| 122 |
+
});
|
| 123 |
+
</script>
|
| 124 |
+
{% endblock %}
|
templates/layout.html
CHANGED
|
@@ -49,6 +49,12 @@
|
|
| 49 |
<span>Urology AI Form</span>
|
| 50 |
</a>
|
| 51 |
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
{% elif current_user.role == 'patient' %}
|
| 53 |
<li class="sidebar-menu-item {% if request.endpoint == 'patient.dashboard' %}active{% endif %}">
|
| 54 |
<a href="{{ url_for('patient.dashboard') }}">
|
|
|
|
| 49 |
<span>Urology AI Form</span>
|
| 50 |
</a>
|
| 51 |
</li>
|
| 52 |
+
<li class="sidebar-menu-item {% if request.endpoint == 'doctor.lab_analysis' %}active{% endif %}">
|
| 53 |
+
<a href="{{ url_for('doctor.lab_analysis') }}">
|
| 54 |
+
<i data-lucide="flask-conical"></i>
|
| 55 |
+
<span>Lab Report</span>
|
| 56 |
+
</a>
|
| 57 |
+
</li>
|
| 58 |
{% elif current_user.role == 'patient' %}
|
| 59 |
<li class="sidebar-menu-item {% if request.endpoint == 'patient.dashboard' %}active{% endif %}">
|
| 60 |
<a href="{{ url_for('patient.dashboard') }}">
|