MakPr016 commited on
Commit
ed38d82
Β·
1 Parent(s): c590f07

Updated response and returning proper summary

Browse files
Files changed (3) hide show
  1. app/main.py +8 -17
  2. app/post_processor.py +155 -101
  3. tests/create_reports.py +278 -0
app/main.py CHANGED
@@ -11,9 +11,12 @@ import json
11
  from .text_extractor import extract_text_from_pdf, extract_text_from_image
12
  from .image_extractor import extract_images_from_pdf
13
  from .ner_processor import load_model, process_text
14
- from .post_processor import structure_entities, generate_summary, generate_recommendations
15
  from .models import EncryptedRequest
16
  from .crypto_utils import CryptoManager
 
 
 
17
 
18
  app = FastAPI(
19
  title="Radiology Report NER API",
@@ -34,7 +37,7 @@ app.add_middleware(
34
  app.add_middleware(GZipMiddleware, minimum_size=1000)
35
 
36
  nlp_model = None
37
- SECRET_KEY = os.getenv("ENCRYPTION_KEY", "654b33943b1d80b27ef812d7f17c51d1c41e1596af54959fee0871c4f8851003")
38
  crypto_manager = CryptoManager(SECRET_KEY)
39
 
40
  @app.on_event("startup")
@@ -198,15 +201,7 @@ async def root():
198
  <li>⚠️ <strong>Critical finding detection</strong></li>
199
  <li>πŸ’Š <strong>Clinical recommendations</strong></li>
200
  <li>πŸ“¦ <strong>Gzip compression</strong> (25% bandwidth savings)</li>
201
- </ul>
202
-
203
- <h2>Model Information</h2>
204
- <ul>
205
- <li><strong>Architecture:</strong> spaCy NER (HashEmbedCNN)</li>
206
- <li><strong>Training Data:</strong> 2,674 radiology reports</li>
207
- <li><strong>Entity Types:</strong> ANATOMY, OBSERVATION</li>
208
- <li><strong>OCR Engine:</strong> EasyOCR (95%+ accuracy)</li>
209
- <li><strong>Deployment:</strong> HuggingFace Spaces</li>
210
  </ul>
211
 
212
  <h2>Documentation</h2>
@@ -215,12 +210,6 @@ async def root():
215
  πŸ“˜ <a href="/redoc" target="_blank">Alternative Documentation (ReDoc)</a><br>
216
  πŸ’š <a href="/health" target="_blank">Health Check Endpoint</a>
217
  </p>
218
-
219
- <h2>Security & Privacy</h2>
220
- <p>
221
- This API implements military-grade encryption to ensure HIPAA compliance and protect sensitive medical data.
222
- All communications are encrypted end-to-end using NaCl cryptography with XSalsa20-Poly1305.
223
- </p>
224
  </div>
225
  </body>
226
  </html>
@@ -280,6 +269,7 @@ async def analyze_secure(request: EncryptedRequest):
280
  entities = process_text(nlp_model, extracted_text)
281
  structured = structure_entities(entities)
282
  summary = generate_summary(structured)
 
283
  recommendations = generate_recommendations(structured)
284
 
285
  processing_time = time.time() - start_time
@@ -297,6 +287,7 @@ async def analyze_secure(request: EncryptedRequest):
297
  "images": images,
298
  "structured_report": structured,
299
  "summary": summary,
 
300
  "recommendations": recommendations
301
  }
302
 
 
11
  from .text_extractor import extract_text_from_pdf, extract_text_from_image
12
  from .image_extractor import extract_images_from_pdf
13
  from .ner_processor import load_model, process_text
14
+ from .post_processor import structure_entities, generate_summary, generate_recommendations, generate_patient_friendly_summary
15
  from .models import EncryptedRequest
16
  from .crypto_utils import CryptoManager
17
+ from dotenv import load_dotenv
18
+
19
+ load_dotenv()
20
 
21
  app = FastAPI(
22
  title="Radiology Report NER API",
 
37
  app.add_middleware(GZipMiddleware, minimum_size=1000)
38
 
39
  nlp_model = None
40
+ SECRET_KEY = os.getenv("ENCRYPTION_KEY")
41
  crypto_manager = CryptoManager(SECRET_KEY)
42
 
43
  @app.on_event("startup")
 
201
  <li>⚠️ <strong>Critical finding detection</strong></li>
202
  <li>πŸ’Š <strong>Clinical recommendations</strong></li>
203
  <li>πŸ“¦ <strong>Gzip compression</strong> (25% bandwidth savings)</li>
204
+ <li>πŸ‘₯ <strong>Patient-friendly summaries</strong></li>
 
 
 
 
 
 
 
 
205
  </ul>
206
 
207
  <h2>Documentation</h2>
 
210
  πŸ“˜ <a href="/redoc" target="_blank">Alternative Documentation (ReDoc)</a><br>
211
  πŸ’š <a href="/health" target="_blank">Health Check Endpoint</a>
212
  </p>
 
 
 
 
 
 
213
  </div>
214
  </body>
215
  </html>
 
269
  entities = process_text(nlp_model, extracted_text)
270
  structured = structure_entities(entities)
271
  summary = generate_summary(structured)
272
+ patient_summary = generate_patient_friendly_summary(structured)
273
  recommendations = generate_recommendations(structured)
274
 
275
  processing_time = time.time() - start_time
 
287
  "images": images,
288
  "structured_report": structured,
289
  "summary": summary,
290
+ "patient_friendly_summary": patient_summary,
291
  "recommendations": recommendations
292
  }
293
 
app/post_processor.py CHANGED
@@ -1,115 +1,169 @@
1
- """
2
- Post-processing and structuring of NER results
3
- """
4
 
5
- from typing import List, Dict
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
- # Critical finding keywords
8
- CRITICAL_KEYWORDS = [
9
- "pneumothorax", "tension pneumothorax", "hemothorax",
10
- "hemorrhage", "bleeding", "rupture", "ruptured",
11
- "acute", "urgent", "emergency", "stat",
12
- "fracture", "displaced fracture",
13
- "large", "massive", "severe",
14
- "dissection", "aneurysm",
15
- "pulmonary embolism", "embolus"
16
- ]
17
-
18
- # Negative finding keywords
19
- NEGATIVE_KEYWORDS = [
20
- "no", "negative", "absent", "clear",
21
- "normal", "unremarkable", "stable",
22
- "within normal limits", "no evidence"
23
- ]
24
-
25
- def structure_entities(entities: List[Dict]) -> Dict:
26
- """
27
- Convert flat entity list into structured report
28
- """
29
- anatomy = []
30
- observations = []
31
-
32
- # Separate by entity type
33
  for entity in entities:
34
- if entity["label"] == "ANATOMY":
35
- anatomy.append(entity["text"])
36
- elif entity["label"] == "OBSERVATION":
37
- observations.append(entity["text"])
38
-
39
- # Remove duplicates while preserving order
40
- anatomy = list(dict.fromkeys(anatomy))
41
- observations = list(dict.fromkeys(observations))
42
-
43
- # Identify negative findings
44
- negative_findings = [
45
- obs for obs in observations
46
- if any(keyword in obs.lower() for keyword in NEGATIVE_KEYWORDS)
47
- ]
48
-
49
- # Identify positive/abnormal findings
50
- positive_findings = [
51
- obs for obs in observations
52
- if obs not in negative_findings
53
- ]
54
-
55
- # Identify critical findings
56
- critical_findings = [
57
- obs for obs in positive_findings
58
- if any(keyword in obs.lower() for keyword in CRITICAL_KEYWORDS)
59
- ]
60
 
61
  return {
62
- "anatomy": anatomy,
63
- "all_observations": observations,
64
- "positive_findings": positive_findings,
65
- "negative_findings": negative_findings,
66
- "critical_findings": critical_findings
67
  }
68
 
69
- def generate_summary(structured_report: Dict) -> Dict:
70
- """
71
- Generate summary statistics
72
- """
 
 
 
73
  return {
74
- "total_entities": len(structured_report["anatomy"]) + len(structured_report["all_observations"]),
75
- "anatomy_count": len(structured_report["anatomy"]),
76
- "observations_count": len(structured_report["all_observations"]),
77
- "has_critical_findings": len(structured_report["critical_findings"]) > 0,
78
- "has_abnormalities": len(structured_report["positive_findings"]) > 0
79
  }
80
 
81
- def generate_recommendations(structured_report: Dict) -> List[str]:
82
- """
83
- Generate clinical recommendations based on findings
84
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  recommendations = []
86
 
87
- # Critical findings
88
- if structured_report["critical_findings"]:
89
- recommendations.append(
90
- "⚠️ URGENT: Critical findings detected. Immediate clinical review recommended."
91
- )
92
- recommendations.append(
93
- f"Critical findings: {', '.join(structured_report['critical_findings'][:3])}"
94
- )
95
-
96
- # Positive findings
97
- if structured_report["positive_findings"]:
98
- if not structured_report["critical_findings"]:
99
- recommendations.append(
100
- "Clinical correlation recommended for reported findings."
101
- )
102
-
103
- # Multiple abnormalities
104
- if len(structured_report["positive_findings"]) > 3:
105
- recommendations.append(
106
- "Multiple abnormalities detected. Consider follow-up imaging."
107
- )
108
-
109
- # Normal study
110
- if not structured_report["positive_findings"]:
111
- recommendations.append(
112
- "No significant abnormalities detected in this report."
113
- )
114
 
115
  return recommendations
 
1
+ NEGATIVE_TERMS = ['clear', 'normal', 'unremarkable', 'no', 'negative', 'absent', 'within normal limits', 'stable']
2
+ POSITIVE_TERMS = ['opacity', 'infiltrate', 'consolidation', 'fracture', 'abnormal', 'effusion', 'edema', 'pneumothorax', 'cardiomegaly', 'mass', 'nodule', 'atelectasis']
3
+ CRITICAL_TERMS = ['acute', 'severe', 'large', 'extensive', 'pneumothorax', 'fracture', 'mass', 'suspicious']
4
 
5
+ MEDICAL_GLOSSARY = {
6
+ 'pulmonary edema': 'Fluid buildup in the lungs, making breathing difficult',
7
+ 'focal consolidation': 'Dense area in lung tissue, possibly indicating infection',
8
+ 'pleural effusion': 'Fluid accumulation around the lungs',
9
+ 'pneumothorax': 'Collapsed lung due to air leak',
10
+ 'cardiomegaly': 'Enlarged heart',
11
+ 'opacity': 'Cloudy area on X-ray, may indicate infection or fluid',
12
+ 'infiltrate': 'Abnormal substance in lung tissue',
13
+ 'atelectasis': 'Partial lung collapse',
14
+ 'nodule': 'Small round growth in the lung',
15
+ 'mass': 'Larger abnormal tissue growth requiring investigation',
16
+ 'consolidation': 'Area where air spaces are filled with fluid or tissue',
17
+ 'effusion': 'Abnormal fluid collection'
18
+ }
19
 
20
+ def structure_entities(entities):
21
+ anatomy_entities = []
22
+ observation_entities = []
23
+ positive_findings = []
24
+ negative_findings = []
25
+ critical_findings = []
26
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  for entity in entities:
28
+ if entity['label'] == 'ANATOMY':
29
+ anatomy_entities.append(entity['text'])
30
+ elif entity['label'] == 'OBSERVATION':
31
+ observation_entities.append(entity['text'])
32
+
33
+ obs_lower = entity['text'].lower()
34
+
35
+ is_critical = any(term in obs_lower for term in CRITICAL_TERMS)
36
+ is_negative = any(term in obs_lower for term in NEGATIVE_TERMS)
37
+ is_positive = any(term in obs_lower for term in POSITIVE_TERMS)
38
+
39
+ if is_critical:
40
+ critical_findings.append(entity['text'])
41
+ elif is_negative:
42
+ negative_findings.append(entity['text'])
43
+ elif is_positive:
44
+ positive_findings.append(entity['text'])
 
 
 
 
 
 
 
 
 
45
 
46
  return {
47
+ 'anatomy': list(set(anatomy_entities)),
48
+ 'all_observations': observation_entities,
49
+ 'positive_findings': list(set(positive_findings)),
50
+ 'negative_findings': list(set(negative_findings)),
51
+ 'critical_findings': list(set(critical_findings))
52
  }
53
 
54
+ def generate_summary(structured):
55
+ total_entities = len(structured['anatomy']) + len(structured['all_observations'])
56
+ anatomy_count = len(structured['anatomy'])
57
+ observations_count = len(structured['all_observations'])
58
+ has_critical = len(structured['critical_findings']) > 0
59
+ has_abnormalities = len(structured['positive_findings']) > 0
60
+
61
  return {
62
+ 'total_entities': total_entities,
63
+ 'anatomy_count': anatomy_count,
64
+ 'observations_count': observations_count,
65
+ 'has_critical_findings': has_critical,
66
+ 'has_abnormalities': has_abnormalities
67
  }
68
 
69
+ def generate_patient_friendly_summary(structured):
70
+ summary = {
71
+ 'overall_status': '',
72
+ 'key_findings': [],
73
+ 'areas_of_concern': [],
74
+ 'next_steps': [],
75
+ 'explanation': ''
76
+ }
77
+
78
+ total_abnormalities = len(structured['positive_findings'])
79
+ total_critical = len(structured['critical_findings'])
80
+
81
+ if total_critical > 0:
82
+ summary['overall_status'] = 'URGENT ATTENTION REQUIRED'
83
+ summary['explanation'] = 'Your X-ray shows findings that need immediate medical attention. Please contact your doctor right away.'
84
+ elif total_abnormalities > 0:
85
+ summary['overall_status'] = 'ABNORMALITIES DETECTED'
86
+ summary['explanation'] = 'Your X-ray shows some areas of concern that should be discussed with your doctor. This does not necessarily mean a serious problem, but follow-up is recommended.'
87
+ else:
88
+ summary['overall_status'] = 'NO MAJOR CONCERNS'
89
+ summary['explanation'] = 'Your chest X-ray appears normal with no significant abnormalities detected. Continue regular health maintenance.'
90
+
91
+ for finding in structured['positive_findings']:
92
+ finding_lower = finding.lower()
93
+ explanation = None
94
+
95
+ for term, desc in MEDICAL_GLOSSARY.items():
96
+ if term in finding_lower:
97
+ explanation = desc
98
+ break
99
+
100
+ summary['key_findings'].append({
101
+ 'finding': finding,
102
+ 'explanation': explanation if explanation else 'Please consult your doctor for clarification on this finding'
103
+ })
104
+
105
+ for critical in structured['critical_findings']:
106
+ critical_lower = critical.lower()
107
+ explanation = None
108
+
109
+ for term, desc in MEDICAL_GLOSSARY.items():
110
+ if term in critical_lower:
111
+ explanation = desc
112
+ break
113
+
114
+ summary['areas_of_concern'].append({
115
+ 'finding': critical,
116
+ 'explanation': explanation if explanation else 'This requires immediate medical evaluation',
117
+ 'severity': 'HIGH'
118
+ })
119
+
120
+ if total_critical > 0:
121
+ summary['next_steps'] = [
122
+ 'Contact your doctor immediately',
123
+ 'Do not delay seeking medical attention',
124
+ 'Bring this report to your appointment',
125
+ 'Follow all recommended treatments'
126
+ ]
127
+ elif total_abnormalities > 0:
128
+ summary['next_steps'] = [
129
+ 'Schedule a follow-up with your doctor within 1-2 weeks',
130
+ 'Additional tests or imaging may be recommended',
131
+ 'Discuss treatment options if needed',
132
+ 'Monitor for any new or worsening symptoms'
133
+ ]
134
+ else:
135
+ summary['next_steps'] = [
136
+ 'Maintain regular health check-ups',
137
+ 'Continue healthy lifestyle practices',
138
+ 'Report any new respiratory symptoms to your doctor',
139
+ 'Follow recommended screening schedules'
140
+ ]
141
+
142
+ return summary
143
+
144
+ def generate_recommendations(structured):
145
  recommendations = []
146
 
147
+ if len(structured['critical_findings']) > 0:
148
+ recommendations.append("Immediate medical consultation required")
149
+ recommendations.append("Consider emergency department visit if symptomatic")
150
+
151
+ if len(structured['positive_findings']) > 0:
152
+ recommendations.append("Follow-up imaging may be needed")
153
+ recommendations.append("Consult with a pulmonologist or radiologist")
154
+
155
+ if 'pneumothorax' in ' '.join(structured['positive_findings']).lower():
156
+ recommendations.append("Urgent evaluation for possible chest tube placement")
157
+
158
+ if 'mass' in ' '.join(structured['positive_findings']).lower() or 'nodule' in ' '.join(structured['positive_findings']).lower():
159
+ recommendations.append("CT scan recommended for further evaluation")
160
+ recommendations.append("Consider biopsy if clinically indicated")
161
+
162
+ if 'effusion' in ' '.join(structured['positive_findings']).lower():
163
+ recommendations.append("Consider thoracentesis if symptomatic")
164
+
165
+ if len(structured['critical_findings']) == 0 and len(structured['positive_findings']) == 0:
166
+ recommendations.append("Continue routine health maintenance")
167
+ recommendations.append("Repeat imaging only if clinically indicated")
 
 
 
 
 
 
168
 
169
  return recommendations
tests/create_reports.py ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ from reportlab.lib.pagesizes import letter
3
+ from reportlab.lib import colors
4
+ from reportlab.lib.units import inch
5
+ from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image, PageBreak
6
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
7
+ from reportlab.lib.enums import TA_CENTER, TA_LEFT
8
+ from datetime import datetime
9
+ import random
10
+ import os
11
+
12
+ reports_df = pd.read_csv('indiana_reports.csv')
13
+ projections_df = pd.read_csv('indiana_projections.csv')
14
+
15
+ def header_footer(canvas, doc):
16
+ canvas.saveState()
17
+
18
+ canvas.setFillColor(colors.HexColor('#1a365d'))
19
+ canvas.setFont('Helvetica-Bold', 14)
20
+ canvas.drawString(0.75*inch, letter[1] - 0.5*inch, "ST. LUKE'S MEDICAL CENTER")
21
+
22
+ canvas.setFont('Helvetica', 9)
23
+ canvas.setFillColor(colors.HexColor('#4a5568'))
24
+ canvas.drawString(0.75*inch, letter[1] - 0.65*inch, "Global City β€’ We love life")
25
+
26
+ canvas.setFont('Helvetica-Bold', 11)
27
+ canvas.setFillColor(colors.HexColor('#2b6cb0'))
28
+ canvas.drawRightString(letter[0] - 0.75*inch, letter[1] - 0.5*inch, "DIAGNOSTIC X-RAY")
29
+
30
+ canvas.setStrokeColor(colors.HexColor('#e2e8f0'))
31
+ canvas.setLineWidth(0.5)
32
+ canvas.line(0.75*inch, letter[1] - 0.75*inch, letter[0] - 0.75*inch, letter[1] - 0.75*inch)
33
+
34
+ canvas.setFont('Helvetica', 8)
35
+ canvas.setFillColor(colors.HexColor('#718096'))
36
+ canvas.drawCentredString(letter[0]/2, 0.5*inch,
37
+ f"Page {doc.page} of 2 | Validated: {datetime.now().strftime('%d-%b-%Y %I:%M %p')}")
38
+
39
+ canvas.drawCentredString(letter[0]/2, 0.35*inch,
40
+ "This report has been electronically signed and validated. No signature is required.")
41
+
42
+ canvas.restoreState()
43
+
44
+ def create_radiology_report(uid, report_row, output_filename, report_num):
45
+ doc = SimpleDocTemplate(output_filename, pagesize=letter, rightMargin=0.75*inch, leftMargin=0.75*inch,
46
+ topMargin=1*inch, bottomMargin=0.8*inch)
47
+
48
+ story = []
49
+ styles = getSampleStyleSheet()
50
+
51
+ section_style = ParagraphStyle(
52
+ 'Section',
53
+ parent=styles['Heading2'],
54
+ fontSize=11,
55
+ textColor=colors.HexColor('#1a365d'),
56
+ spaceAfter=6,
57
+ spaceBefore=10,
58
+ fontName='Helvetica-Bold'
59
+ )
60
+
61
+ body_style = ParagraphStyle(
62
+ 'Body',
63
+ parent=styles['Normal'],
64
+ fontSize=10,
65
+ leading=14,
66
+ textColor=colors.HexColor('#2d3748'),
67
+ alignment=TA_LEFT
68
+ )
69
+
70
+ patient_id = f"{random.randint(1000000000, 9999999999)}"
71
+ exam_date = datetime.now().strftime("%d-%b-%Y")
72
+ exam_time = datetime.now().strftime("%I:%M %p")
73
+ dob = f"{random.randint(1,28)}-{random.choice(['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP'])}-{random.randint(1950,2000)}"
74
+ age = random.randint(25,85)
75
+ gender = random.choice(['M', 'F'])
76
+
77
+ print(f"\n{'='*70}")
78
+ print(f" REPORT #{report_num} - UID: {uid}")
79
+ print(f"{'='*70}")
80
+ print(f" Patient ID: {patient_id}")
81
+ print(f" Exam Date: {exam_date} {exam_time}")
82
+ print(f" Age/Gender: {age}Y / {gender}")
83
+
84
+ projection_rows_list = projections_df[projections_df['uid'] == uid]
85
+ print(f" Images Expected: {len(projection_rows_list)}")
86
+
87
+ images_found_count = 0
88
+ for idx, proj_row in projection_rows_list.iterrows():
89
+ filename = proj_row['filename']
90
+ image_path = f"images/{filename}"
91
+ if os.path.exists(image_path):
92
+ exists = "βœ“"
93
+ images_found_count += 1
94
+ else:
95
+ exists = "βœ—"
96
+ print(f" {exists} {filename}")
97
+
98
+ indication = report_row.get('indication', '')
99
+ findings = report_row.get('findings', '')
100
+ impression = report_row.get('impression', '')
101
+
102
+ print(f"\n History: {str(indication)[:60]}..." if len(str(indication)) > 60 else f"\n History: {indication}")
103
+ print(f" Findings: {str(findings)[:60]}..." if len(str(findings)) > 60 else f" Findings: {findings}")
104
+ print(f" Impression: {str(impression)[:60]}..." if len(str(impression)) > 60 else f" Impression: {impression}")
105
+
106
+ patient_name = random.choice(['DE LEON, Debbie Dichoco', 'SANTOS, Maria Clara', 'CRUZ, Juan Pedro', 'SOLOMON, Jannica Mica Estolano'])
107
+ referring_physician = random.choice(['ANG, SAMUEL DEE M.D.', 'REYES, MARIA SANTOS M.D.', 'CRUZ, JOSE ANTONIO M.D.'])
108
+
109
+ patient_info = [
110
+ ['', '', Paragraph(f"<b>DATE OF BIRTH:</b>", body_style), Paragraph(dob, body_style), Paragraph(f"<b>AGE:</b>", body_style), Paragraph(f"{age} year/s", body_style)],
111
+ [Paragraph(f"<b>{patient_id}</b>", body_style), '', '', '', Paragraph(f"<b>GENDER:</b>", body_style), Paragraph(gender, body_style)],
112
+ [Paragraph(f"<b>{patient_name}</b>", body_style), '', '', '', '', ''],
113
+ [Paragraph(f"<b>ROOM/BED:</b>", body_style), Paragraph("OutPatient", body_style),
114
+ Paragraph(f"<b>REFERRING<br/>PHYSICIAN:</b>", body_style), Paragraph(referring_physician, body_style),
115
+ Paragraph(f"<b>DATE/TIME OF<br/>EXAM:</b>", body_style), Paragraph(f"{exam_date}<br/>{exam_time}", body_style)],
116
+ ['', '', '', '', Paragraph(f"<b>DATE/TIME OF<br/>REQUEST:</b>", body_style), Paragraph(f"{exam_date}<br/>{exam_time}", body_style)]
117
+ ]
118
+
119
+ patient_table = Table(patient_info, colWidths=[1.3*inch, 1.2*inch, 1.2*inch, 1.4*inch, 1.2*inch, 1.2*inch])
120
+ patient_table.setStyle(TableStyle([
121
+ ('VALIGN', (0, 0), (-1, -1), 'TOP'),
122
+ ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
123
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 3),
124
+ ('TOPPADDING', (0, 0), (-1, -1), 3),
125
+ ('LEFTPADDING', (0, 0), (-1, -1), 0),
126
+ ('RIGHTPADDING', (0, 0), (-1, -1), 5),
127
+ ]))
128
+
129
+
130
+ story.append(patient_table)
131
+ story.append(Spacer(1, 0.25*inch))
132
+
133
+ story.append(Paragraph("EXAMINATION:", section_style))
134
+ story.append(Paragraph("CHEST", body_style))
135
+ story.append(Spacer(1, 0.1*inch))
136
+
137
+ if pd.notna(indication) and str(indication).strip() and str(indication).lower() not in ['none', 'nan', '']:
138
+ story.append(Paragraph("HISTORY:", section_style))
139
+ clean_indication = str(indication).replace('XXXX', '[redacted]')
140
+ story.append(Paragraph(clean_indication, body_style))
141
+ story.append(Spacer(1, 0.1*inch))
142
+
143
+ comparison = report_row.get('comparison', '')
144
+ if pd.notna(comparison) and str(comparison).strip() and str(comparison).lower() not in ['none', 'none.', 'nan', '']:
145
+ story.append(Paragraph("COMPARISON:", section_style))
146
+ clean_comparison = str(comparison).replace('XXXX', '[redacted]')
147
+ story.append(Paragraph(clean_comparison, body_style))
148
+ story.append(Spacer(1, 0.1*inch))
149
+
150
+ if len(projection_rows_list) > 0:
151
+ techniques = ", ".join([row['projection'] for _, row in projection_rows_list.iterrows()])
152
+ story.append(Paragraph("TECHNIQUE:", section_style))
153
+ story.append(Paragraph(f"{techniques} view", body_style))
154
+ story.append(Spacer(1, 0.1*inch))
155
+
156
+ story.append(Paragraph("FINDINGS:", section_style))
157
+ if pd.notna(findings) and str(findings).strip() and str(findings).lower() != 'nan':
158
+ clean_findings = str(findings).replace('XXXX', '[redacted]')
159
+ story.append(Paragraph(clean_findings, body_style))
160
+ else:
161
+ story.append(Paragraph("No significant findings.", body_style))
162
+ story.append(Spacer(1, 0.1*inch))
163
+
164
+ story.append(Paragraph("IMPRESSION:", section_style))
165
+ if pd.notna(impression) and str(impression).strip() and str(impression).lower() != 'nan':
166
+ clean_impression = str(impression).replace('XXXX', '[redacted]')
167
+ story.append(Paragraph(clean_impression, body_style))
168
+ else:
169
+ story.append(Paragraph("Normal chest radiograph.", body_style))
170
+ story.append(Spacer(1, 0.2*inch))
171
+
172
+ approval_data = [
173
+ [Paragraph("<b>Approved By:</b>", body_style), '']
174
+ ]
175
+
176
+ signature_path = "images/signature.jpg"
177
+ radiologist_name = random.choice(['JOSE ANTONIO CRUZ', 'NERELLA KRISHNA TEJA', 'MARIA ELENA SANTOS', 'MICHAEL VINCENTO CORSINO'])
178
+
179
+ if os.path.exists(signature_path):
180
+ sig_img = Image(signature_path, width=1.2*inch, height=0.4*inch)
181
+ approval_data.append([sig_img, ''])
182
+
183
+ approval_data.append([Paragraph(f"<b>{radiologist_name}, M.D. (Radiologist)</b>", body_style), ''])
184
+ approval_data.append([Paragraph(f"MBBS, MD RADIO DIAGNOSIS | REG NO. APMC/FMR/{random.randint(90000, 99999)}",
185
+ ParagraphStyle('Small', parent=body_style, fontSize=8)), ''])
186
+
187
+ approval_table = Table(approval_data, colWidths=[3*inch, 4*inch])
188
+ approval_table.setStyle(TableStyle([
189
+ ('VALIGN', (0, 0), (-1, -1), 'TOP'),
190
+ ('ALIGN', (0, 0), (0, -1), 'LEFT'),
191
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 2),
192
+ ('TOPPADDING', (0, 0), (-1, -1), 2),
193
+ ]))
194
+
195
+ story.append(approval_table)
196
+
197
+ story.append(PageBreak())
198
+
199
+ if len(projection_rows_list) > 0:
200
+ images_found = []
201
+ for _, proj_row in projection_rows_list.iterrows():
202
+ filename = proj_row['filename']
203
+ image_path = f"images/{filename}"
204
+
205
+ if os.path.exists(image_path):
206
+ try:
207
+ projection_label = proj_row.get('projection', 'View')
208
+ images_found.append((image_path, projection_label))
209
+ except Exception as e:
210
+ print(f" βœ— Error checking image {image_path}: {e}")
211
+
212
+ print(f"\n Images Embedded: {len(images_found)}")
213
+
214
+ if images_found:
215
+ image_table_data = []
216
+ for i in range(0, len(images_found), 2):
217
+ row_images = []
218
+ for j in range(2):
219
+ if i + j < len(images_found):
220
+ img_path, label = images_found[i + j]
221
+ try:
222
+ img = Image(img_path, width=3*inch, height=3*inch)
223
+ caption = Paragraph(f"<b>{label} View</b>",
224
+ ParagraphStyle('Caption', parent=body_style, alignment=TA_CENTER, fontSize=10))
225
+ cell = Table([[img], [caption]], colWidths=[3*inch])
226
+ cell.setStyle(TableStyle([
227
+ ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
228
+ ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
229
+ ]))
230
+ row_images.append(cell)
231
+ except Exception as e:
232
+ print(f" βœ— Could not load image {img_path}: {e}")
233
+ row_images.append('')
234
+ else:
235
+ row_images.append('')
236
+
237
+ if row_images:
238
+ image_table_data.append(row_images)
239
+
240
+ if image_table_data:
241
+ image_table = Table(image_table_data, colWidths=[3.3*inch, 3.3*inch])
242
+ image_table.setStyle(TableStyle([
243
+ ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
244
+ ('VALIGN', (0, 0), (-1, -1), 'TOP'),
245
+ ('LEFTPADDING', (0, 0), (-1, -1), 10),
246
+ ('RIGHTPADDING', (0, 0), (-1, -1), 10),
247
+ ('TOPPADDING', (0, 0), (-1, -1), 15),
248
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 15),
249
+ ]))
250
+
251
+ story.append(image_table)
252
+
253
+ doc.build(story, onFirstPage=header_footer, onLaterPages=header_footer)
254
+ print(f"\n βœ“ PDF Created: {output_filename}")
255
+ print(f"{'='*70}\n")
256
+
257
+ output_dir = "generated_reports"
258
+ os.makedirs(output_dir, exist_ok=True)
259
+
260
+ sample_reports = reports_df.head(5)
261
+
262
+ print("\n" + "="*70)
263
+ print(" RADIOLOGY REPORT GENERATOR")
264
+ print("="*70)
265
+ print(f" Total Reports: {len(sample_reports)}")
266
+ print(f" Output Directory: {output_dir}/")
267
+ print("="*70)
268
+
269
+ for idx, (_, report_row) in enumerate(sample_reports.iterrows(), 1):
270
+ uid = report_row['uid']
271
+ output_filename = f"{output_dir}/radiology_report_{idx}.pdf"
272
+
273
+ create_radiology_report(uid, report_row, output_filename, idx)
274
+
275
+ print("\n" + "="*70)
276
+ print(f" βœ“ ALL REPORTS GENERATED SUCCESSFULLY!")
277
+ print(f" βœ“ Location: {output_dir}/")
278
+ print("="*70 + "\n")