Spaces:
Running
Running
File size: 5,837 Bytes
3255634 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | """PDF report generation for DeepAMR predictions."""
import io
from datetime import datetime
from typing import Dict, List, Optional
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import mm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable,
)
def generate_prediction_report(prediction: Dict) -> bytes:
"""Generate a PDF clinical report for an AMR prediction.
Args:
prediction: Prediction dict from the database (frontend format).
Returns:
PDF file content as bytes.
"""
buf = io.BytesIO()
doc = SimpleDocTemplate(buf, pagesize=A4, topMargin=20 * mm, bottomMargin=20 * mm)
styles = getSampleStyleSheet()
title_style = ParagraphStyle("ReportTitle", parent=styles["Title"], fontSize=18, spaceAfter=6)
subtitle_style = ParagraphStyle("Sub", parent=styles["Normal"], fontSize=10, textColor=colors.grey)
section_style = ParagraphStyle("Section", parent=styles["Heading2"], fontSize=13, spaceBefore=14)
body_style = styles["Normal"]
disclaimer_style = ParagraphStyle("Disclaimer", parent=styles["Normal"], fontSize=8, textColor=colors.grey)
elements: list = []
# Title
elements.append(Paragraph("DeepAMR - Antimicrobial Resistance Report", title_style))
elements.append(Paragraph(f"Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M UTC')}", subtitle_style))
elements.append(HRFlowable(width="100%", thickness=1, color=colors.grey))
elements.append(Spacer(1, 6 * mm))
# Sample info
elements.append(Paragraph("Sample Information", section_style))
sample_data = [
["Sample ID", prediction.get("sampleId", "N/A")],
["Organism", prediction.get("organism", "Unknown")],
["File", prediction.get("fileName", "N/A")],
["Date", prediction.get("createdAt", "N/A")],
["Overall Risk", (prediction.get("overallRisk") or "N/A").upper()],
]
if prediction.get("model_version"):
sample_data.append(["Model Version", prediction["model_version"]])
t = Table(sample_data, colWidths=[120, 350])
t.setStyle(TableStyle([
("FONTNAME", (0, 0), (0, -1), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 10),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
]))
elements.append(t)
elements.append(Spacer(1, 6 * mm))
# Drug resistance table
results = prediction.get("results") or []
if results:
elements.append(Paragraph("Drug Resistance Profile", section_style))
header = ["Antibiotic", "Status", "Confidence"]
rows = [header]
for r in results:
status = r.get("status", "?")
conf = r.get("confidence", 0)
rows.append([
r.get("antibiotic", r.get("class", "?")),
"Resistant" if status == "R" else "Susceptible",
f"{conf * 100:.1f}%",
])
drug_table = Table(rows, colWidths=[200, 120, 100])
# Color-code status column
style_cmds = [
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1e40af")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 9),
("GRID", (0, 0), (-1, -1), 0.5, colors.grey),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
("TOPPADDING", (0, 0), (-1, -1), 4),
]
for i, r in enumerate(results, start=1):
if r.get("status") == "R":
style_cmds.append(("BACKGROUND", (1, i), (1, i), colors.HexColor("#fee2e2")))
style_cmds.append(("TEXTCOLOR", (1, i), (1, i), colors.HexColor("#dc2626")))
else:
style_cmds.append(("BACKGROUND", (1, i), (1, i), colors.HexColor("#dcfce7")))
style_cmds.append(("TEXTCOLOR", (1, i), (1, i), colors.HexColor("#16a34a")))
drug_table.setStyle(TableStyle(style_cmds))
elements.append(drug_table)
elements.append(Spacer(1, 6 * mm))
# Summary
summary = prediction.get("summary")
if summary:
elements.append(Paragraph("Summary", section_style))
elements.append(Paragraph(
f"Resistant: {summary.get('resistant', 0)} | "
f"Intermediate: {summary.get('intermediate', 0)} | "
f"Susceptible: {summary.get('susceptible', 0)}",
body_style,
))
elements.append(Spacer(1, 4 * mm))
# Recommendations (if stored)
recs = prediction.get("recommendations")
if recs:
elements.append(Paragraph("Clinical Recommendations", section_style))
for rec in recs:
elements.append(Paragraph(f"• {rec}", body_style))
elements.append(Spacer(1, 4 * mm))
# Bangladesh context (if stored)
bd_recs = prediction.get("bangladesh_recommendations")
if bd_recs:
elements.append(Paragraph("Bangladesh Clinical Context", section_style))
for rec in bd_recs:
elements.append(Paragraph(f"• {rec}", body_style))
elements.append(Spacer(1, 4 * mm))
# Disclaimer
elements.append(Spacer(1, 10 * mm))
elements.append(HRFlowable(width="100%", thickness=0.5, color=colors.grey))
elements.append(Paragraph(
"DISCLAIMER: This report is generated by an AI model (DeepAMR Advanced DL, "
"Micro F1: 84.3%, AUC: 98.6%). Results are intended to assist clinical decision-making "
"and should NOT replace laboratory-confirmed susceptibility testing. "
"Always consult qualified healthcare professionals before making treatment decisions.",
disclaimer_style,
))
doc.build(elements)
return buf.getvalue()
|