Spaces:
Running
Running
| """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() | |