"""
ui.py - Gradio 6.x compatible UI - Extended timeout version
"""
import json
import logging
import os
from pathlib import Path
import gradio as gr
import httpx
logger = logging.getLogger(__name__)
BACKEND_URL = "http://127.0.0.1:8000"
REQUEST_TIMEOUT = 600 # 10 minutes — Phi-2 on CPU is slow
CUSTOM_CSS = """
.gr-markdown h1 { font-size: 2rem !important; color: #1a4a8a !important; font-weight: 700; border-bottom: 3px solid #1a4a8a; padding-bottom: 8px; }
.gr-markdown h3 { color: #1a4a8a !important; font-weight: 600; }
#report-box textarea { font-family: 'Courier New', monospace !important; font-size: 13px !important; line-height: 1.7 !important; background: #1e1e2e !important; color: #e0e0e0 !important; border: 1.5px solid #1a4a8a !important; border-radius: 8px !important; }
"""
def _check_backend() -> bool:
try:
r = httpx.get(f"{BACKEND_URL}/api/health", timeout=5)
return r.status_code == 200
except Exception:
return False
def _call_generate_report(image_path, patient_name, patient_age, patient_gender,
symptoms, clinical_history, modality, body_region, view):
with open(image_path, "rb") as f:
image_bytes = f.read()
filename = Path(image_path).name
with httpx.Client(timeout=REQUEST_TIMEOUT) as client:
response = client.post(
f"{BACKEND_URL}/api/generate-report",
files={"image": (filename, image_bytes, "application/octet-stream")},
data={
"patient_name": patient_name,
"patient_age": str(patient_age),
"patient_gender": patient_gender,
"symptoms": symptoms,
"clinical_history": clinical_history,
"modality": modality,
"body_region": body_region,
"view": view,
},
)
response.raise_for_status()
return response.json()
def generate_report(image_file, patient_name, patient_age, patient_gender,
symptoms, clinical_history, modality, body_region, view):
if image_file is None:
return "", "", "⚠️ Please upload a radiology image.", "", "❌ No image uploaded."
if not patient_name or not patient_name.strip():
return "", "", "⚠️ Please enter the patient name.", "", "❌ Patient name required."
if not patient_age or int(patient_age) <= 0:
return "", "", "⚠️ Please enter a valid patient age.", "", "❌ Invalid age."
if not symptoms.strip():
return "", "", "⚠️ Please enter presenting symptoms.", "", "❌ Symptoms required."
if not _check_backend():
return "", "", "❌ Backend offline. Run: uvicorn backend.main:app --reload", "", "❌ Backend offline."
try:
data = _call_generate_report(
image_path=image_file,
patient_name=patient_name.strip(),
patient_age=int(patient_age),
patient_gender=patient_gender,
symptoms=symptoms,
clinical_history=clinical_history,
modality=modality,
body_region=body_region,
view=view,
)
except httpx.ReadTimeout:
err = "❌ Request timed out — model is still processing. Wait 2 min and try again, or restart backend."
return "", "", err, "", err
except httpx.HTTPStatusError as exc:
err = f"❌ Server error {exc.response.status_code}: {exc.response.text[:200]}"
return "", "", err, "", err
except Exception as exc:
err = f"❌ Request failed: {exc}"
return "", "", err, "", err
report = data.get("report", {})
validation = data.get("validation", {})
report_id = data.get("report_id", "")
confidence = validation.get("confidence_score", 0.0)
gen_time = report.get("generation_time_seconds", 0)
report_text = data.get("report_text", "")
if not report_text:
r = data.get("report", {})
report_text = f"""RADIOLOGY REPORT
{'='*60}
EXAMINATION DETAILS:
{r.get('examination_details','—')}
CLINICAL INFORMATION:
{r.get('clinical_information','—')}
FINDINGS:
{r.get('findings','—')}
IMPRESSION:
{r.get('impression','—')}
RECOMMENDATIONS:
{r.get('recommendations','—')}
CONFIDENCE SCORE: {confidence:.2f}
{'='*60}
END OF REPORT"""
if confidence >= 0.80:
conf_label, conf_color = "HIGH", "#138808"
elif confidence >= 0.60:
conf_label, conf_color = "MODERATE", "#e67e00"
else:
conf_label, conf_color = "LOW", "#c0392b"
confidence_html = f"""
CONFIDENCE SCORE
{confidence:.2f} — {conf_label}
Generated in {gen_time:.1f}s
"""
issues = validation.get("issues", [])
warnings = validation.get("warnings", [])
suggestions = validation.get("suggestions", [])
section_scores = validation.get("section_scores", {})
val_lines = ["**Validation Results**\n"]
val_lines.append(f"{'✅ VALID' if validation.get('is_valid') else '⚠️ REVIEW NEEDED'}")
if section_scores:
val_lines.append("\n**Section Scores:**")
for sec, score in section_scores.items():
bar = "█" * int(score * 10) + "░" * (10 - int(score * 10))
val_lines.append(f" {sec.replace('_',' ').title()}: {bar} {score:.2f}")
if issues:
val_lines.append("\n**Issues:**")
val_lines.extend(f" ❌ {i}" for i in issues)
if warnings:
val_lines.append("\n**Warnings:**")
val_lines.extend(f" ⚠️ {w}" for w in warnings)
if suggestions:
val_lines.append("\n**Suggestions:**")
val_lines.extend(f" 💡 {s}" for s in suggestions)
# Medical Coding Block
coding = data.get("medical_coding", {})
val_lines.append("\n---")
val_lines.append("## Medical Coding (UMLS / ICD-10 / SNOMED CT)")
if coding and coding.get("total_findings", 0) > 0:
primary = coding.get("primary_diagnosis", {})
val_lines.append(f"**Primary Diagnosis:** {primary.get('label','—')} | CUI: {primary.get('cui','—')}")
val_lines.append(f"**Overall Severity:** {coding.get('overall_severity','—')}")
icd_list = coding.get("icd10_codes", [])
if icd_list:
val_lines.append(f"**ICD-10 Codes:** {' | '.join(icd_list)}")
val_lines.append(f"**All Concepts Found ({coding.get('total_findings',0)}):**")
for mc in coding.get("medical_codes", []):
val_lines.append(f" * {mc['label']} | CUI: {mc['cui']} | ICD-10: {mc['icd10']} | Severity: {mc['severity']}")
else:
val_lines.append("No significant pathological concepts detected (normal study).")
validation_text = "\n".join(val_lines)
status_final = f"✅ Report generated | ID: {report_id[:8]}"
return report_text, confidence_html, validation_text, report_id, status_final
def _downloads_dir() -> Path:
"""Returns a temp download folder that works on both Windows and Linux."""
d = Path(os.environ.get("TEMP", os.environ.get("TMP", "/tmp"))) / "radiology_reports"
d.mkdir(parents=True, exist_ok=True)
return d
def export_pdf(report_id):
if not report_id:
return None
try:
r = httpx.get(f"{BACKEND_URL}/api/export/pdf/{report_id}", timeout=60)
r.raise_for_status()
path = _downloads_dir() / f"radiology_report_{report_id[:8]}.pdf"
with open(path, "wb") as f:
f.write(r.content)
logger.info(f"PDF saved: {path}")
return str(path)
except Exception as exc:
logger.error(f"PDF export error: {exc}")
return None
def export_json_file(report_id):
if not report_id:
return None
try:
r = httpx.get(f"{BACKEND_URL}/api/export/json/{report_id}", timeout=30)
r.raise_for_status()
path = _downloads_dir() / f"radiology_report_{report_id[:8]}.json"
with open(path, "wb") as f:
f.write(r.content)
logger.info(f"JSON saved: {path}")
return str(path)
except Exception as exc:
logger.error(f"JSON export error: {exc}")
return None
def fill_demo():
return (
45, "Male",
"Left abdominal pain, swelling of the abdominal wall, fever",
"Suspected abdominal wall infection.",
"CT", "Abdomen", "Axial",
)
def check_backend_status():
if _check_backend():
return "🟢 Backend online — ready to generate reports."
return "🔴 Backend offline — run: uvicorn backend.main:app --reload"
def fetch_report_history():
"""Fetch all reports from DB for history tab."""
try:
r = httpx.get(f"{BACKEND_URL}/api/reports?limit=50", timeout=10)
r.raise_for_status()
reports = r.json().get("reports", [])
if not reports:
return "No reports found in database yet."
lines = ["| Patient Name | Age | Gender | Modality | Region | Confidence | Date |",
"|---|---|---|---|---|---|---|"]
for rep in reports:
conf = rep.get("confidence_score", 0) or 0
lines.append(
f"| {rep.get('patient_name','—')} "
f"| {rep.get('age','—')} "
f"| {rep.get('gender','—')} "
f"| {rep.get('modality','—')} "
f"| {rep.get('body_region','—')} "
f"| {conf:.2f} "
f"| {str(rep.get('created_at','—'))[:16]} |"
)
return "\n".join(lines)
except Exception as e:
return f"❌ Could not load history: {e}"
def fetch_db_stats():
"""Fetch database statistics."""
try:
r = httpx.get(f"{BACKEND_URL}/api/stats", timeout=10)
r.raise_for_status()
s = r.json()
mod_counts = s.get("modality_counts", {})
mod_text = " | ".join(f"{k}: {v}" for k,v in mod_counts.items())
return (
f"**Total Reports:** {s.get('total_reports', 0)} \n"
f"**Unique Patients:** {s.get('total_patients', 0)} \n"
f"**Avg Confidence:** {s.get('avg_confidence', 0):.2f} \n"
f"**By Modality:** {mod_text or 'None yet'}"
)
except Exception as e:
return f"❌ Could not load stats: {e}"
def search_patient_reports(name: str):
"""Search reports by patient name."""
if not name.strip():
return "Enter a patient name to search."
try:
r = httpx.get(
f"{BACKEND_URL}/api/reports/patient/{name.strip()}",
timeout=10
)
r.raise_for_status()
data = r.json()
reports = data.get("reports", [])
if not reports:
return f"No reports found for patient: {name}"
lines = [f"**{len(reports)} report(s) found for: {name}**\n",
"| Date | Modality | Region | Confidence | Impression |",
"|---|---|---|---|---|"]
for rep in reports:
conf = rep.get("confidence_score", 0) or 0
imp = str(rep.get("impression","—"))[:60] + "..."
lines.append(
f"| {str(rep.get('created_at','—'))[:16]} "
f"| {rep.get('modality','—')} "
f"| {rep.get('body_region','—')} "
f"| {conf:.2f} "
f"| {imp} |"
)
return "\n".join(lines)
except Exception as e:
return f"❌ Search failed: {e}"
def get_report_uuids_for_patient(name: str):
"""Returns list of (label, uuid) tuples for a patient's reports."""
if not name.strip():
return []
try:
r = httpx.get(
f"{BACKEND_URL}/api/reports/patient/{name.strip()}", timeout=10
)
r.raise_for_status()
reports = r.json().get("reports", [])
choices = []
for rep in reports:
label = (
f"{str(rep.get('created_at',''))[:16]} | "
f"{rep.get('modality','—')} {rep.get('body_region','—')} | "
f"Conf: {rep.get('confidence_score',0):.2f}"
)
choices.append((label, rep.get("report_uuid", "")))
return choices
except Exception:
return []
def load_patient_report_choices(name: str):
"""Populate the dropdown with patient's reports."""
choices = get_report_uuids_for_patient(name)
if not choices:
return gr.update(choices=[], value=None,
label="No reports found for this patient")
return gr.update(choices=choices, value=choices[0][1],
label=f"{len(choices)} report(s) found — select one to download")
def download_report_pdf(report_uuid: str):
"""Download PDF for a specific report UUID from DB."""
if not report_uuid:
return None
try:
r = httpx.get(
f"{BACKEND_URL}/api/export/pdf/{report_uuid}", timeout=60
)
r.raise_for_status()
path = _downloads_dir() / f"report_{report_uuid[:8]}.pdf"
with open(path, "wb") as f:
f.write(r.content)
return str(path)
except Exception as exc:
logger.error(f"PDF download failed: {exc}")
return None
def download_report_json(report_uuid: str):
"""Download JSON for a specific report UUID from DB."""
if not report_uuid:
return None
try:
r = httpx.get(
f"{BACKEND_URL}/api/export/json/{report_uuid}", timeout=30
)
r.raise_for_status()
path = _downloads_dir() / f"report_{report_uuid[:8]}.json"
with open(path, "wb") as f:
f.write(r.content)
return str(path)
except Exception as exc:
logger.error(f"JSON download failed: {exc}")
return None
def preview_selected_report(report_uuid: str):
"""Show full report text for selected report UUID."""
if not report_uuid:
return "Select a report to preview."
try:
r = httpx.get(
f"{BACKEND_URL}/api/report/{report_uuid}", timeout=10
)
r.raise_for_status()
data = r.json()
rep = data.get("report", {})
pat = data.get("patient", {})
img = data.get("imaging", {})
valid = data.get("validation", {})
conf = valid.get("confidence_score", rep.get("confidence_score", 0)) or 0
name = data.get("patient_name", pat.get("name", "Unknown"))
return f"""RADIOLOGY REPORT
{'='*60}
Patient Name : {name}
Age / Gender : {pat.get('age','—')} / {pat.get('gender','—')}
Study : {img.get('modality','—')} {img.get('body_region','—')} {img.get('view','') or ''}
Date : {str(data.get('timestamp',''))[:16]}
{'='*60}
EXAMINATION DETAILS:
{rep.get('examination_details','—')}
CLINICAL INFORMATION:
{rep.get('clinical_information','—')}
FINDINGS:
{rep.get('findings','—')}
IMPRESSION:
{rep.get('impression','—')}
RECOMMENDATIONS:
{rep.get('recommendations','—')}
CONFIDENCE SCORE: {conf:.2f}
{'='*60}
END OF REPORT"""
except Exception as exc:
return f"❌ Could not load report: {exc}"
def build_interface() -> gr.Blocks:
with gr.Blocks(title="Radiology AI — Report Generation System") as demo:
report_id_state = gr.State("")
gr.Markdown(
"# 🏥 Radiology AI — Report Generation System\n"
"### AI-Based Structured Radiology Report Generation with Clinical Validation & Patient Database"
)
backend_status = gr.Markdown("🔴 Checking backend…")
with gr.Tabs():
# ── TAB 1: Generate Report ─────────────────────────────────
with gr.Tab("🚀 Generate Report"):
gr.Markdown(
"> ⏳ **Note:** First report generation takes **30–60 seconds on GPU**. "
"Please click Generate Report and wait — do not click again."
)
with gr.Row(equal_height=False):
# LEFT: Inputs
with gr.Column(scale=1, min_width=340):
gr.Markdown("### 📤 Step 1: Upload Radiology Image")
image_input = gr.File(
label="Upload Image (.dcm / .png / .jpg)",
file_types=[".dcm", ".png", ".jpg", ".jpeg"],
type="filepath",
)
image_preview = gr.Image(
label="Preview",
interactive=False,
height=220,
)
gr.Markdown("### 👤 Step 2: Patient Information")
patient_name = gr.Textbox(
label="Patient Name",
placeholder="e.g. John Smith",
lines=1,
)
with gr.Row():
patient_age = gr.Number(
label="Age (years)", minimum=0, maximum=130, step=1, value=50
)
patient_gender = gr.Dropdown(
label="Gender",
choices=["Male", "Female", "Other"],
value="Male",
)
symptoms = gr.Textbox(
label="Presenting Symptoms",
placeholder="e.g. abdominal pain, fever, swelling",
lines=2,
)
clinical_history = gr.Textbox(
label="Clinical History",
placeholder="e.g. suspected infection, prior surgeries",
lines=2,
)
gr.Markdown("### 🔬 Step 3: Imaging Information")
with gr.Row():
modality = gr.Dropdown(
label="Modality",
choices=[
"X-ray", "CT", "MRI", "Ultrasound", "PET",
"Mammography", "Fluoroscopy", "Angiography",
"Nuclear Medicine",
],
value="X-ray",
)
body_region = gr.Dropdown(
label="Body Region",
choices=[
"Chest", "Brain", "Abdomen", "Pelvis",
"Spine", "Knee", "Hip", "Shoulder", "Neck",
"Cardiac", "Breast", "Liver", "Kidney",
"Thyroid", "Hand", "Wrist", "Foot", "Ankle",
"Skull", "Prostate", "Obstetric", "Vascular",
],
value="Chest",
)
view = gr.Dropdown(
label="View / Projection",
choices=[
"PA", "AP", "Lateral", "Axial", "Coronal",
"Sagittal", "Oblique", "Left Lateral", "Right Lateral",
"Supine", "Prone", "Decubitus", "Cephalocaudal (CC)",
"Mediolateral Oblique (MLO)", "T1", "T2", "FLAIR",
"DWI", "STIR", "",
],
value="PA",
)
with gr.Row():
demo_btn = gr.Button("📋 Fill Demo", variant="secondary", size="sm")
generate_btn = gr.Button("🚀 Generate Report", variant="primary", size="lg")
status_label = gr.Markdown("")
# RIGHT: Outputs
with gr.Column(scale=2, min_width=480):
gr.Markdown("### 📄 Generated Radiology Report")
report_output = gr.Textbox(
label="Structured Report",
lines=22,
interactive=False,
elem_id="report-box",
)
confidence_output = gr.HTML(
value=""
"Confidence score will appear here after generation.
"
)
with gr.Accordion("📊 Validation Details", open=False):
validation_output = gr.Markdown("")
gr.Markdown("### 💾 Export Report")
with gr.Row():
export_pdf_btn = gr.Button("⬇️ Download PDF", variant="secondary")
export_json_btn = gr.Button("⬇️ Download JSON", variant="secondary")
pdf_file = gr.File(label="PDF Report", visible=False)
json_file = gr.File(label="JSON Report", visible=False)
# ── TAB 2: Patient History ─────────────────────────────────
with gr.Tab("📋 Patient History"):
gr.Markdown("## 📋 Report History Database")
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### 🔍 Search by Patient Name")
search_input = gr.Textbox(
label="Patient Name", placeholder="e.g. John Smith"
)
search_btn = gr.Button("🔍 Search", variant="primary")
search_output = gr.Markdown("")
with gr.Column(scale=1):
gr.Markdown("### 📊 Database Statistics")
stats_output = gr.Markdown("")
refresh_stats_btn = gr.Button("🔄 Refresh Stats", variant="secondary")
# ── Download Previous Reports ──────────────────────────
gr.Markdown("---")
gr.Markdown("### ⬇️ Download Previous Reports")
with gr.Row():
with gr.Column(scale=2):
dl_patient_input = gr.Textbox(
label="Enter Patient Name to Load Reports",
placeholder="e.g. Gopalamanikandan A",
)
dl_load_btn = gr.Button(
"📂 Load Reports for This Patient",
variant="primary"
)
report_selector = gr.Dropdown(
label="Select Report to Download",
choices=[],
value=None,
interactive=True,
)
with gr.Column(scale=2):
gr.Markdown("#### 👁️ Report Preview")
report_preview = gr.Textbox(
label="Selected Report",
lines=18,
interactive=False,
elem_id="report-box",
)
with gr.Row():
dl_pdf_btn = gr.Button("⬇️ Download PDF", variant="secondary")
dl_json_btn = gr.Button("⬇️ Download JSON", variant="secondary")
dl_pdf_file = gr.File(label="PDF", visible=False)
dl_json_file = gr.File(label="JSON", visible=False)
# ── All Reports Table ──────────────────────────────────
gr.Markdown("---")
gr.Markdown("### 📜 All Reports")
history_btn = gr.Button("🔄 Load All Reports", variant="secondary")
history_output = gr.Markdown("")
# ── Events ────────────────────────────────────────────────────
image_input.change(fn=lambda f: f, inputs=image_input, outputs=image_preview)
demo_btn.click(
fn=fill_demo, inputs=[],
outputs=[patient_age, patient_gender, symptoms, clinical_history,
modality, body_region, view],
)
generate_btn.click(
fn=generate_report,
inputs=[image_input, patient_name, patient_age, patient_gender,
symptoms, clinical_history, modality, body_region, view],
outputs=[report_output, confidence_output, validation_output,
report_id_state, status_label],
)
export_pdf_btn.click(
fn=export_pdf, inputs=[report_id_state], outputs=[pdf_file],
).then(
fn=lambda f: gr.update(visible=f is not None),
inputs=[pdf_file], outputs=[pdf_file],
)
export_json_btn.click(
fn=export_json_file, inputs=[report_id_state], outputs=[json_file],
).then(
fn=lambda f: gr.update(visible=f is not None),
inputs=[json_file], outputs=[json_file],
)
# History tab events
history_btn.click(
fn=fetch_report_history, inputs=[], outputs=[history_output]
)
search_btn.click(
fn=search_patient_reports, inputs=[search_input], outputs=[search_output]
)
refresh_stats_btn.click(
fn=fetch_db_stats, inputs=[], outputs=[stats_output]
)
# Download previous report events
dl_load_btn.click(
fn=load_patient_report_choices,
inputs=[dl_patient_input],
outputs=[report_selector],
)
report_selector.change(
fn=preview_selected_report,
inputs=[report_selector],
outputs=[report_preview],
)
dl_pdf_btn.click(
fn=download_report_pdf,
inputs=[report_selector],
outputs=[dl_pdf_file],
).then(
fn=lambda f: gr.update(visible=f is not None),
inputs=[dl_pdf_file], outputs=[dl_pdf_file],
)
dl_json_btn.click(
fn=download_report_json,
inputs=[report_selector],
outputs=[dl_json_file],
).then(
fn=lambda f: gr.update(visible=f is not None),
inputs=[dl_json_file], outputs=[dl_json_file],
)
demo.load(fn=check_backend_status, outputs=[backend_status])
demo.load(fn=fetch_db_stats, outputs=[stats_output])
return demo
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
interface = build_interface()
interface.launch(
server_name="0.0.0.0",
server_port=int(os.getenv("GRADIO_PORT", "7860")),
share=False,
show_error=True,
css=CUSTOM_CSS,
theme=gr.themes.Soft(primary_hue="blue", neutral_hue="slate"),
)