Spaces:
Sleeping
Sleeping
Upload casl_analysis.py
Browse files- casl_analysis.py +377 -74
casl_analysis.py
CHANGED
@@ -127,17 +127,35 @@ def load_patient_record(record_id):
|
|
127 |
"""Load patient record from storage"""
|
128 |
try:
|
129 |
# Find the record in the CSV file
|
|
|
|
|
|
|
|
|
130 |
with open(RECORDS_FILE, 'r', newline='') as f:
|
131 |
reader = csv.reader(f)
|
132 |
next(reader) # Skip header
|
133 |
for row in reader:
|
|
|
|
|
|
|
|
|
134 |
if row[0] == record_id:
|
135 |
file_path = row[8]
|
136 |
|
|
|
|
|
|
|
|
|
|
|
137 |
# Load and return the data
|
138 |
-
|
139 |
-
|
|
|
|
|
|
|
|
|
140 |
|
|
|
141 |
return None
|
142 |
|
143 |
except Exception as e:
|
@@ -148,27 +166,112 @@ def get_all_patient_records():
|
|
148 |
"""Return a list of all patient records"""
|
149 |
try:
|
150 |
records = []
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
166 |
return records
|
167 |
|
168 |
except Exception as e:
|
169 |
logger.error(f"Error getting patient records: {str(e)}")
|
170 |
return []
|
171 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
172 |
# ===============================
|
173 |
# Utility Functions
|
174 |
# ===============================
|
@@ -937,8 +1040,11 @@ def generate_report(patient_info, analysis_results, report_type="formal"):
|
|
937 |
1. Patient information and assessment details
|
938 |
2. Summary of findings (strengths and areas of concern)
|
939 |
3. Detailed analysis of language domains
|
940 |
-
4. Specific recommendations for therapy
|
941 |
5. Recommendation for frequency and duration of services
|
|
|
|
|
|
|
942 |
|
943 |
Use clear, professional language appropriate for {'educational professionals' if report_type == 'formal' else 'parents and caregivers'}.
|
944 |
Format the report with proper headings and sections.
|
@@ -1008,6 +1114,14 @@ def generate_report(patient_info, analysis_results, report_type="formal"):
|
|
1008 |
|
1009 |
The prognosis for improvement is good with consistent therapeutic intervention and support. Regular reassessment is recommended to monitor progress.
|
1010 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1011 |
Respectfully submitted,
|
1012 |
|
1013 |
{clinician}
|
@@ -1072,6 +1186,14 @@ def generate_report(patient_info, analysis_results, report_type="formal"):
|
|
1072 |
|
1073 |
With regular therapy and support at home, we expect your child to make good progress in these areas.
|
1074 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1075 |
Please reach out with any questions!
|
1076 |
|
1077 |
{clinician}
|
@@ -1355,24 +1477,33 @@ def create_interface():
|
|
1355 |
with gr.Column(scale=1):
|
1356 |
gr.Markdown("### Patient Records")
|
1357 |
|
1358 |
-
# Records table
|
1359 |
patient_records_table = gr.Dataframe(
|
1360 |
-
headers=["ID", "Name", "Record ID", "Age", "Gender", "Assessment Date", "Clinician"],
|
1361 |
-
datatype=["str", "str", "str", "str", "str", "str", "str"],
|
1362 |
label="Saved Patients",
|
1363 |
interactive=False
|
1364 |
)
|
1365 |
|
1366 |
-
|
|
|
|
|
|
|
1367 |
records_status = gr.Markdown("")
|
1368 |
|
1369 |
# Record selection
|
1370 |
selected_record_id = gr.Textbox(label="Selected Record ID", visible=False)
|
1371 |
-
|
|
|
|
|
|
|
1372 |
|
1373 |
with gr.Column(scale=1):
|
1374 |
# Record details
|
1375 |
record_details = gr.Markdown(label="Record Details")
|
|
|
|
|
|
|
1376 |
|
1377 |
# Event handlers for records
|
1378 |
def refresh_patient_records():
|
@@ -1480,6 +1611,37 @@ def create_interface():
|
|
1480 |
status_msg
|
1481 |
)
|
1482 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1483 |
load_record_btn.click(
|
1484 |
load_patient_record_to_analysis,
|
1485 |
inputs=[selected_record_id],
|
@@ -1490,6 +1652,17 @@ def create_interface():
|
|
1490 |
records_status
|
1491 |
]
|
1492 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1493 |
|
1494 |
# ===============================
|
1495 |
# Report Generator Tab
|
@@ -1836,15 +2009,21 @@ def create_interface():
|
|
1836 |
]
|
1837 |
)
|
1838 |
|
1839 |
-
#
|
1840 |
-
def export_pdf(report_text, patient_name="Patient", record_id=""):
|
1841 |
try:
|
1842 |
from reportlab.lib.pagesizes import letter
|
1843 |
-
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
|
1844 |
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
|
|
1845 |
import tempfile
|
1846 |
import webbrowser
|
1847 |
import os
|
|
|
|
|
|
|
|
|
|
|
1848 |
|
1849 |
# Generate a safe filename
|
1850 |
if patient_name and record_id:
|
@@ -1854,20 +2033,20 @@ def create_interface():
|
|
1854 |
else:
|
1855 |
safe_name = f"speech_analysis_{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
1856 |
|
1857 |
-
# Create
|
1858 |
-
|
1859 |
-
pdf_path = os.path.join(temp_dir, f"{safe_name}.pdf")
|
1860 |
|
1861 |
# Create the PDF document
|
1862 |
doc = SimpleDocTemplate(pdf_path, pagesize=letter)
|
1863 |
styles = getSampleStyleSheet()
|
1864 |
|
1865 |
-
# Create custom styles
|
1866 |
styles.add(ParagraphStyle(
|
1867 |
name='Heading1',
|
1868 |
parent=styles['Heading1'],
|
1869 |
fontSize=16,
|
1870 |
-
spaceAfter=12
|
|
|
1871 |
))
|
1872 |
|
1873 |
styles.add(ParagraphStyle(
|
@@ -1875,88 +2054,212 @@ def create_interface():
|
|
1875 |
parent=styles['Heading2'],
|
1876 |
fontSize=14,
|
1877 |
spaceAfter=10,
|
1878 |
-
spaceBefore=10
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1879 |
))
|
1880 |
|
1881 |
styles.add(ParagraphStyle(
|
1882 |
name='BodyText',
|
1883 |
parent=styles['BodyText'],
|
1884 |
-
fontSize=
|
1885 |
-
spaceAfter=8
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1886 |
))
|
1887 |
|
1888 |
# Convert markdown to PDF elements
|
1889 |
-
# Very basic conversion - in a real app, use a proper markdown to PDF library
|
1890 |
story = []
|
1891 |
|
1892 |
-
# Add title
|
1893 |
story.append(Paragraph("Speech Language Assessment Report", styles['Title']))
|
1894 |
story.append(Spacer(1, 12))
|
1895 |
|
1896 |
-
#
|
1897 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1898 |
|
1899 |
for line in report_text.split('\n'):
|
|
|
|
|
1900 |
# Skip empty lines
|
1901 |
-
if not line
|
1902 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1903 |
continue
|
1904 |
|
1905 |
# Check for headings
|
1906 |
if line.startswith('# '):
|
|
|
|
|
|
|
|
|
|
|
|
|
1907 |
story.append(Paragraph(line[2:], styles['Heading1']))
|
1908 |
elif line.startswith('## '):
|
|
|
|
|
|
|
|
|
|
|
|
|
1909 |
story.append(Paragraph(line[3:], styles['Heading2']))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1910 |
elif line.startswith('- '):
|
1911 |
-
# Bullet points
|
1912 |
-
|
|
|
1913 |
elif line.startswith('**') and line.endswith('**'):
|
1914 |
# Bold text - assuming it's a short line like a heading
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1915 |
text = line.replace('**', '')
|
1916 |
story.append(Paragraph(f"<b>{text}</b>", styles['BodyText']))
|
1917 |
else:
|
1918 |
# Regular text
|
1919 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1920 |
|
1921 |
# Build the PDF
|
1922 |
doc.build(story)
|
1923 |
|
1924 |
-
#
|
1925 |
-
|
1926 |
-
|
|
|
|
|
|
|
1927 |
|
1928 |
except Exception as e:
|
1929 |
logger.exception("Error creating PDF")
|
1930 |
-
return f"Error creating PDF: {str(e)}
|
1931 |
|
1932 |
-
#
|
1933 |
-
|
1934 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1935 |
|
1936 |
-
|
1937 |
-
|
1938 |
-
|
1939 |
-
|
1940 |
-
|
1941 |
-
|
1942 |
-
|
1943 |
-
|
1944 |
-
|
1945 |
-
|
1946 |
-
|
1947 |
-
|
1948 |
-
|
1949 |
-
# Running locally, use actual PDF generation
|
1950 |
-
export_btn.click(
|
1951 |
-
lambda x, y, z: export_pdf(x, y, z),
|
1952 |
-
inputs=[full_analysis, patient_name, record_id],
|
1953 |
-
outputs=[export_status]
|
1954 |
-
)
|
1955 |
-
report_download_btn.click(
|
1956 |
-
lambda x, y, z: export_pdf(x, y, z),
|
1957 |
-
inputs=[report_output, report_patient_name, report_record_id],
|
1958 |
-
outputs=[report_download_status]
|
1959 |
-
)
|
1960 |
|
1961 |
# Report generator button
|
1962 |
def on_generate_report(name, record_id, age, gender, date, clinician, results, report_type):
|
|
|
127 |
"""Load patient record from storage"""
|
128 |
try:
|
129 |
# Find the record in the CSV file
|
130 |
+
if not os.path.exists(RECORDS_FILE):
|
131 |
+
logger.error(f"Records file does not exist: {RECORDS_FILE}")
|
132 |
+
return None
|
133 |
+
|
134 |
with open(RECORDS_FILE, 'r', newline='') as f:
|
135 |
reader = csv.reader(f)
|
136 |
next(reader) # Skip header
|
137 |
for row in reader:
|
138 |
+
if len(row) < 9: # Ensure row has enough elements
|
139 |
+
logger.warning(f"Skipping malformed record row: {row}")
|
140 |
+
continue
|
141 |
+
|
142 |
if row[0] == record_id:
|
143 |
file_path = row[8]
|
144 |
|
145 |
+
# Check if the file exists
|
146 |
+
if not os.path.exists(file_path):
|
147 |
+
logger.error(f"Analysis file not found: {file_path}")
|
148 |
+
return None
|
149 |
+
|
150 |
# Load and return the data
|
151 |
+
try:
|
152 |
+
with open(file_path, 'rb') as f:
|
153 |
+
return pickle.load(f)
|
154 |
+
except (pickle.PickleError, EOFError) as pickle_err:
|
155 |
+
logger.error(f"Error unpickling file {file_path}: {str(pickle_err)}")
|
156 |
+
return None
|
157 |
|
158 |
+
logger.warning(f"Record ID not found: {record_id}")
|
159 |
return None
|
160 |
|
161 |
except Exception as e:
|
|
|
166 |
"""Return a list of all patient records"""
|
167 |
try:
|
168 |
records = []
|
169 |
+
|
170 |
+
# Ensure data directories exist
|
171 |
+
ensure_data_dirs()
|
172 |
+
|
173 |
+
if not os.path.exists(RECORDS_FILE):
|
174 |
+
logger.warning(f"Records file does not exist, creating it: {RECORDS_FILE}")
|
175 |
+
with open(RECORDS_FILE, 'w', newline='') as f:
|
176 |
+
writer = csv.writer(f)
|
177 |
+
writer.writerow([
|
178 |
+
"ID", "Name", "Record ID", "Age", "Gender",
|
179 |
+
"Assessment Date", "Clinician", "Analysis Date", "File Path"
|
180 |
+
])
|
181 |
+
return records
|
182 |
+
|
183 |
+
# Read existing records
|
184 |
+
valid_records = []
|
185 |
+
with open(RECORDS_FILE, 'r', newline='') as f:
|
186 |
+
reader = csv.reader(f)
|
187 |
+
next(reader) # Skip header
|
188 |
+
for row in reader:
|
189 |
+
if len(row) < 9: # Check for malformed rows
|
190 |
+
continue
|
191 |
+
|
192 |
+
# Check if the analysis file exists
|
193 |
+
file_path = row[8]
|
194 |
+
file_exists = os.path.exists(file_path)
|
195 |
+
|
196 |
+
record = {
|
197 |
+
"id": row[0],
|
198 |
+
"name": row[1],
|
199 |
+
"record_id": row[2],
|
200 |
+
"age": row[3],
|
201 |
+
"gender": row[4],
|
202 |
+
"assessment_date": row[5],
|
203 |
+
"clinician": row[6],
|
204 |
+
"analysis_date": row[7],
|
205 |
+
"file_path": file_path,
|
206 |
+
"status": "Valid" if file_exists else "Missing File"
|
207 |
+
}
|
208 |
+
records.append(record)
|
209 |
+
|
210 |
+
# Keep track of valid records for potential cleanup
|
211 |
+
if file_exists:
|
212 |
+
valid_records.append(row)
|
213 |
+
|
214 |
+
# If we found invalid records, consider rewriting the CSV with only valid entries
|
215 |
+
if len(valid_records) < len(records):
|
216 |
+
logger.warning(f"Found {len(records) - len(valid_records)} invalid records")
|
217 |
+
# Uncomment to enable automatic cleanup:
|
218 |
+
# with open(RECORDS_FILE, 'w', newline='') as f:
|
219 |
+
# writer = csv.writer(f)
|
220 |
+
# writer.writerow([
|
221 |
+
# "ID", "Name", "Record ID", "Age", "Gender",
|
222 |
+
# "Assessment Date", "Clinician", "Analysis Date", "File Path"
|
223 |
+
# ])
|
224 |
+
# for row in valid_records:
|
225 |
+
# writer.writerow(row)
|
226 |
+
|
227 |
return records
|
228 |
|
229 |
except Exception as e:
|
230 |
logger.error(f"Error getting patient records: {str(e)}")
|
231 |
return []
|
232 |
|
233 |
+
def delete_patient_record(record_id):
|
234 |
+
"""Delete a patient record"""
|
235 |
+
try:
|
236 |
+
if not os.path.exists(RECORDS_FILE):
|
237 |
+
return False
|
238 |
+
|
239 |
+
# Find the record and its file
|
240 |
+
file_path = None
|
241 |
+
with open(RECORDS_FILE, 'r', newline='') as f:
|
242 |
+
reader = csv.reader(f)
|
243 |
+
rows = list(reader)
|
244 |
+
header = rows[0]
|
245 |
+
|
246 |
+
for i, row in enumerate(rows[1:], 1):
|
247 |
+
if len(row) < 9:
|
248 |
+
continue
|
249 |
+
|
250 |
+
if row[0] == record_id:
|
251 |
+
file_path = row[8]
|
252 |
+
break
|
253 |
+
|
254 |
+
if not file_path:
|
255 |
+
return False
|
256 |
+
|
257 |
+
# Delete the analysis file if it exists
|
258 |
+
if os.path.exists(file_path):
|
259 |
+
os.remove(file_path)
|
260 |
+
|
261 |
+
# Remove the record from the CSV
|
262 |
+
rows_to_keep = [row for row in rows[1:] if len(row) >= 9 and row[0] != record_id]
|
263 |
+
|
264 |
+
with open(RECORDS_FILE, 'w', newline='') as f:
|
265 |
+
writer = csv.writer(f)
|
266 |
+
writer.writerow(header)
|
267 |
+
writer.writerows(rows_to_keep)
|
268 |
+
|
269 |
+
return True
|
270 |
+
|
271 |
+
except Exception as e:
|
272 |
+
logger.error(f"Error deleting patient record: {str(e)}")
|
273 |
+
return False
|
274 |
+
|
275 |
# ===============================
|
276 |
# Utility Functions
|
277 |
# ===============================
|
|
|
1040 |
1. Patient information and assessment details
|
1041 |
2. Summary of findings (strengths and areas of concern)
|
1042 |
3. Detailed analysis of language domains
|
1043 |
+
4. Specific recommendations for therapy that directly address the patient's unique speech patterns
|
1044 |
5. Recommendation for frequency and duration of services
|
1045 |
+
6. 2-3 specific questions to ask the patient that target their particular speech difficulties
|
1046 |
+
|
1047 |
+
IMPORTANT: For both recommendations and questions, refer to specific examples from the patient's speech sample to make them personalized. Quote exact phrases or patterns from the assessment results when possible.
|
1048 |
|
1049 |
Use clear, professional language appropriate for {'educational professionals' if report_type == 'formal' else 'parents and caregivers'}.
|
1050 |
Format the report with proper headings and sections.
|
|
|
1114 |
|
1115 |
The prognosis for improvement is good with consistent therapeutic intervention and support. Regular reassessment is recommended to monitor progress.
|
1116 |
|
1117 |
+
## TARGETED QUESTIONS FOR ASSESSMENT FOLLOW-UP
|
1118 |
+
|
1119 |
+
1. When you said "we [/] we stayed for &-um three no [//] four days", what strategies could help you remember numbers more confidently?
|
1120 |
+
|
1121 |
+
2. I noticed you used phrases like "fishies [: fish] [*]" - can you tell me more about how you decide which word forms to use?
|
1122 |
+
|
1123 |
+
3. When you're trying to think of a word like when you said "&-um &-um sprinkles! that's the word", what helps you find the right word?
|
1124 |
+
|
1125 |
Respectfully submitted,
|
1126 |
|
1127 |
{clinician}
|
|
|
1186 |
|
1187 |
With regular therapy and support at home, we expect your child to make good progress in these areas.
|
1188 |
|
1189 |
+
## QUESTIONS TO ASK AT HOME
|
1190 |
+
|
1191 |
+
1. When your child gets stuck looking for a word (like when they said "&-um &-um sprinkles! that's the word"), what helps them find it most effectively?
|
1192 |
+
|
1193 |
+
2. Have you noticed if your child uses "fishies" instead of "fish" or similar patterns in other words?
|
1194 |
+
|
1195 |
+
3. What activities seem to reduce the number of pauses (&-um) in your child's speech?
|
1196 |
+
|
1197 |
Please reach out with any questions!
|
1198 |
|
1199 |
{clinician}
|
|
|
1477 |
with gr.Column(scale=1):
|
1478 |
gr.Markdown("### Patient Records")
|
1479 |
|
1480 |
+
# Records table with status column
|
1481 |
patient_records_table = gr.Dataframe(
|
1482 |
+
headers=["ID", "Name", "Record ID", "Age", "Gender", "Assessment Date", "Clinician", "Status"],
|
1483 |
+
datatype=["str", "str", "str", "str", "str", "str", "str", "str"],
|
1484 |
label="Saved Patients",
|
1485 |
interactive=False
|
1486 |
)
|
1487 |
|
1488 |
+
with gr.Row():
|
1489 |
+
refresh_records_btn = gr.Button("Refresh Records", size="sm")
|
1490 |
+
delete_record_btn = gr.Button("Delete Selected Record", size="sm", variant="secondary")
|
1491 |
+
|
1492 |
records_status = gr.Markdown("")
|
1493 |
|
1494 |
# Record selection
|
1495 |
selected_record_id = gr.Textbox(label="Selected Record ID", visible=False)
|
1496 |
+
|
1497 |
+
with gr.Row():
|
1498 |
+
load_record_btn = gr.Button("Load for Analysis", variant="primary")
|
1499 |
+
load_to_report_btn = gr.Button("Load to Report Generator", variant="secondary")
|
1500 |
|
1501 |
with gr.Column(scale=1):
|
1502 |
# Record details
|
1503 |
record_details = gr.Markdown(label="Record Details")
|
1504 |
+
|
1505 |
+
with gr.Accordion("Record Actions", open=False):
|
1506 |
+
export_record_btn = gr.Button("Export Record as PDF", variant="secondary")
|
1507 |
|
1508 |
# Event handlers for records
|
1509 |
def refresh_patient_records():
|
|
|
1611 |
status_msg
|
1612 |
)
|
1613 |
|
1614 |
+
# Function to load patient record into the report generator tab
|
1615 |
+
def load_patient_record_to_report(record_id):
|
1616 |
+
if not record_id:
|
1617 |
+
return gr.update(selected=1), {}, "", "", "", "", "", ""
|
1618 |
+
|
1619 |
+
record_data = load_patient_record(record_id)
|
1620 |
+
if not record_data:
|
1621 |
+
return gr.update(selected=1), "", "", "", "male", "", "", ""
|
1622 |
+
|
1623 |
+
# Extract data
|
1624 |
+
patient_info = record_data.get("patient_info", {})
|
1625 |
+
analysis_results = record_data.get("analysis_results", {})
|
1626 |
+
|
1627 |
+
# Get the raw analysis results as a string
|
1628 |
+
raw_response = analysis_results.get("raw_response", "")
|
1629 |
+
|
1630 |
+
# Create status message for the record loading
|
1631 |
+
status_msg = f"✅ Record loaded successfully: {patient_info.get('name', 'Unknown')} ({record_id})"
|
1632 |
+
|
1633 |
+
return (
|
1634 |
+
gr.update(selected=2), # Switch to Report Generator tab
|
1635 |
+
patient_info.get("name", ""),
|
1636 |
+
patient_info.get("record_id", ""),
|
1637 |
+
patient_info.get("age", ""),
|
1638 |
+
patient_info.get("gender", "male"),
|
1639 |
+
patient_info.get("assessment_date", ""),
|
1640 |
+
patient_info.get("clinician", ""),
|
1641 |
+
raw_response,
|
1642 |
+
status_msg
|
1643 |
+
)
|
1644 |
+
|
1645 |
load_record_btn.click(
|
1646 |
load_patient_record_to_analysis,
|
1647 |
inputs=[selected_record_id],
|
|
|
1652 |
records_status
|
1653 |
]
|
1654 |
)
|
1655 |
+
|
1656 |
+
load_to_report_btn.click(
|
1657 |
+
load_patient_record_to_report,
|
1658 |
+
inputs=[selected_record_id],
|
1659 |
+
outputs=[
|
1660 |
+
main_tabs,
|
1661 |
+
report_patient_name, report_record_id, report_age, report_gender,
|
1662 |
+
report_date, report_clinician, report_results,
|
1663 |
+
records_status
|
1664 |
+
]
|
1665 |
+
)
|
1666 |
|
1667 |
# ===============================
|
1668 |
# Report Generator Tab
|
|
|
2009 |
]
|
2010 |
)
|
2011 |
|
2012 |
+
# Improved PDF export functionality
|
2013 |
+
def export_pdf(report_text, patient_name="Patient", record_id="", age="", gender="", assessment_date="", clinician=""):
|
2014 |
try:
|
2015 |
from reportlab.lib.pagesizes import letter
|
2016 |
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
|
2017 |
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
2018 |
+
from reportlab.lib import colors
|
2019 |
import tempfile
|
2020 |
import webbrowser
|
2021 |
import os
|
2022 |
+
import shutil
|
2023 |
+
|
2024 |
+
# Create a proper downloads directory in the app folder
|
2025 |
+
downloads_dir = os.path.join(DATA_DIR, "downloads")
|
2026 |
+
os.makedirs(downloads_dir, exist_ok=True)
|
2027 |
|
2028 |
# Generate a safe filename
|
2029 |
if patient_name and record_id:
|
|
|
2033 |
else:
|
2034 |
safe_name = f"speech_analysis_{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
2035 |
|
2036 |
+
# Create the PDF path in our downloads directory
|
2037 |
+
pdf_path = os.path.join(downloads_dir, f"{safe_name}.pdf")
|
|
|
2038 |
|
2039 |
# Create the PDF document
|
2040 |
doc = SimpleDocTemplate(pdf_path, pagesize=letter)
|
2041 |
styles = getSampleStyleSheet()
|
2042 |
|
2043 |
+
# Create enhanced custom styles
|
2044 |
styles.add(ParagraphStyle(
|
2045 |
name='Heading1',
|
2046 |
parent=styles['Heading1'],
|
2047 |
fontSize=16,
|
2048 |
+
spaceAfter=12,
|
2049 |
+
textColor=colors.navy
|
2050 |
))
|
2051 |
|
2052 |
styles.add(ParagraphStyle(
|
|
|
2054 |
parent=styles['Heading2'],
|
2055 |
fontSize=14,
|
2056 |
spaceAfter=10,
|
2057 |
+
spaceBefore=10,
|
2058 |
+
textColor=colors.darkblue
|
2059 |
+
))
|
2060 |
+
|
2061 |
+
styles.add(ParagraphStyle(
|
2062 |
+
name='Heading3',
|
2063 |
+
parent=styles['Heading2'],
|
2064 |
+
fontSize=12,
|
2065 |
+
spaceAfter=8,
|
2066 |
+
spaceBefore=8,
|
2067 |
+
textColor=colors.darkblue
|
2068 |
))
|
2069 |
|
2070 |
styles.add(ParagraphStyle(
|
2071 |
name='BodyText',
|
2072 |
parent=styles['BodyText'],
|
2073 |
+
fontSize=11,
|
2074 |
+
spaceAfter=8,
|
2075 |
+
leading=14
|
2076 |
+
))
|
2077 |
+
|
2078 |
+
styles.add(ParagraphStyle(
|
2079 |
+
name='BulletPoint',
|
2080 |
+
parent=styles['BodyText'],
|
2081 |
+
fontSize=11,
|
2082 |
+
leftIndent=20,
|
2083 |
+
firstLineIndent=-15,
|
2084 |
+
spaceAfter=4,
|
2085 |
+
leading=14
|
2086 |
))
|
2087 |
|
2088 |
# Convert markdown to PDF elements
|
|
|
2089 |
story = []
|
2090 |
|
2091 |
+
# Add title and date
|
2092 |
story.append(Paragraph("Speech Language Assessment Report", styles['Title']))
|
2093 |
story.append(Spacer(1, 12))
|
2094 |
|
2095 |
+
# Add patient information table
|
2096 |
+
if patient_name or record_id or age or gender:
|
2097 |
+
# Prepare patient info data
|
2098 |
+
data = []
|
2099 |
+
if patient_name:
|
2100 |
+
data.append(["Patient Name:", patient_name])
|
2101 |
+
if record_id:
|
2102 |
+
data.append(["Record ID:", record_id])
|
2103 |
+
if age:
|
2104 |
+
data.append(["Age:", f"{age} years"])
|
2105 |
+
if gender:
|
2106 |
+
data.append(["Gender:", gender])
|
2107 |
+
if assessment_date:
|
2108 |
+
data.append(["Assessment Date:", assessment_date])
|
2109 |
+
if clinician:
|
2110 |
+
data.append(["Clinician:", clinician])
|
2111 |
+
|
2112 |
+
if data:
|
2113 |
+
# Create a table with the data
|
2114 |
+
patient_table = Table(data, colWidths=[120, 350])
|
2115 |
+
patient_table.setStyle(TableStyle([
|
2116 |
+
('BACKGROUND', (0, 0), (0, -1), colors.lightgrey),
|
2117 |
+
('TEXTCOLOR', (0, 0), (0, -1), colors.darkblue),
|
2118 |
+
('ALIGN', (0, 0), (0, -1), 'RIGHT'),
|
2119 |
+
('ALIGN', (1, 0), (1, -1), 'LEFT'),
|
2120 |
+
('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
|
2121 |
+
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
|
2122 |
+
('TOPPADDING', (0, 0), (-1, -1), 6),
|
2123 |
+
('GRID', (0, 0), (-1, -1), 0.5, colors.lightgrey),
|
2124 |
+
]))
|
2125 |
+
story.append(patient_table)
|
2126 |
+
story.append(Spacer(1, 12))
|
2127 |
+
|
2128 |
+
# Process the markdown content
|
2129 |
+
in_bullet_list = False
|
2130 |
+
current_list_items = []
|
2131 |
|
2132 |
for line in report_text.split('\n'):
|
2133 |
+
line = line.strip()
|
2134 |
+
|
2135 |
# Skip empty lines
|
2136 |
+
if not line:
|
2137 |
+
if in_bullet_list:
|
2138 |
+
# End the current list
|
2139 |
+
in_bullet_list = False
|
2140 |
+
for item in current_list_items:
|
2141 |
+
story.append(Paragraph(f"• {item}", styles['BulletPoint']))
|
2142 |
+
current_list_items = []
|
2143 |
+
story.append(Spacer(1, 6))
|
2144 |
+
else:
|
2145 |
+
story.append(Spacer(1, 6))
|
2146 |
continue
|
2147 |
|
2148 |
# Check for headings
|
2149 |
if line.startswith('# '):
|
2150 |
+
if in_bullet_list:
|
2151 |
+
# End the current list before starting a new section
|
2152 |
+
in_bullet_list = False
|
2153 |
+
for item in current_list_items:
|
2154 |
+
story.append(Paragraph(f"• {item}", styles['BulletPoint']))
|
2155 |
+
current_list_items = []
|
2156 |
story.append(Paragraph(line[2:], styles['Heading1']))
|
2157 |
elif line.startswith('## '):
|
2158 |
+
if in_bullet_list:
|
2159 |
+
# End the current list before starting a new section
|
2160 |
+
in_bullet_list = False
|
2161 |
+
for item in current_list_items:
|
2162 |
+
story.append(Paragraph(f"• {item}", styles['BulletPoint']))
|
2163 |
+
current_list_items = []
|
2164 |
story.append(Paragraph(line[3:], styles['Heading2']))
|
2165 |
+
elif line.startswith('### '):
|
2166 |
+
if in_bullet_list:
|
2167 |
+
# End the current list before starting a new section
|
2168 |
+
in_bullet_list = False
|
2169 |
+
for item in current_list_items:
|
2170 |
+
story.append(Paragraph(f"• {item}", styles['BulletPoint']))
|
2171 |
+
current_list_items = []
|
2172 |
+
story.append(Paragraph(line[4:], styles['Heading3']))
|
2173 |
elif line.startswith('- '):
|
2174 |
+
# Bullet points - collect them to process as a list
|
2175 |
+
in_bullet_list = True
|
2176 |
+
current_list_items.append(line[2:])
|
2177 |
elif line.startswith('**') and line.endswith('**'):
|
2178 |
# Bold text - assuming it's a short line like a heading
|
2179 |
+
if in_bullet_list:
|
2180 |
+
# End the current list before adding this element
|
2181 |
+
in_bullet_list = False
|
2182 |
+
for item in current_list_items:
|
2183 |
+
story.append(Paragraph(f"• {item}", styles['BulletPoint']))
|
2184 |
+
current_list_items = []
|
2185 |
+
|
2186 |
text = line.replace('**', '')
|
2187 |
story.append(Paragraph(f"<b>{text}</b>", styles['BodyText']))
|
2188 |
else:
|
2189 |
# Regular text
|
2190 |
+
if in_bullet_list:
|
2191 |
+
# End the current list before adding regular text
|
2192 |
+
in_bullet_list = False
|
2193 |
+
for item in current_list_items:
|
2194 |
+
story.append(Paragraph(f"• {item}", styles['BulletPoint']))
|
2195 |
+
current_list_items = []
|
2196 |
+
|
2197 |
+
# Handle lines with bold text within them
|
2198 |
+
formatted_line = line
|
2199 |
+
bold_pattern = re.compile(r'\*\*(.*?)\*\*')
|
2200 |
+
for match in bold_pattern.finditer(line):
|
2201 |
+
bold_text = match.group(1)
|
2202 |
+
formatted_line = formatted_line.replace(f"**{bold_text}**", f"<b>{bold_text}</b>")
|
2203 |
+
|
2204 |
+
story.append(Paragraph(formatted_line, styles['BodyText']))
|
2205 |
+
|
2206 |
+
# If there are any remaining list items, add them now
|
2207 |
+
if in_bullet_list:
|
2208 |
+
for item in current_list_items:
|
2209 |
+
story.append(Paragraph(f"• {item}", styles['BulletPoint']))
|
2210 |
+
|
2211 |
+
# Add footer with date
|
2212 |
+
footer_text = f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
2213 |
+
story.append(Spacer(1, 20))
|
2214 |
+
story.append(Paragraph(footer_text, ParagraphStyle(
|
2215 |
+
name='Footer',
|
2216 |
+
parent=styles['Normal'],
|
2217 |
+
fontSize=8,
|
2218 |
+
textColor=colors.grey
|
2219 |
+
)))
|
2220 |
|
2221 |
# Build the PDF
|
2222 |
doc.build(story)
|
2223 |
|
2224 |
+
# Create a copy in the temp directory to make it accessible in web environments
|
2225 |
+
temp_dir = tempfile.gettempdir()
|
2226 |
+
temp_pdf_path = os.path.join(temp_dir, f"{safe_name}.pdf")
|
2227 |
+
shutil.copy2(pdf_path, temp_pdf_path)
|
2228 |
+
|
2229 |
+
return f"Report saved as PDF: {pdf_path}<br>Temporary copy: {temp_pdf_path}"
|
2230 |
|
2231 |
except Exception as e:
|
2232 |
logger.exception("Error creating PDF")
|
2233 |
+
return f"Error creating PDF: {str(e)}"
|
2234 |
|
2235 |
+
# Use the full PDF export function regardless of environment
|
2236 |
+
export_btn.click(
|
2237 |
+
export_pdf,
|
2238 |
+
inputs=[
|
2239 |
+
full_analysis,
|
2240 |
+
patient_name,
|
2241 |
+
record_id,
|
2242 |
+
age,
|
2243 |
+
gender,
|
2244 |
+
assessment_date,
|
2245 |
+
clinician_name
|
2246 |
+
],
|
2247 |
+
outputs=[export_status]
|
2248 |
+
)
|
2249 |
|
2250 |
+
report_download_btn.click(
|
2251 |
+
export_pdf,
|
2252 |
+
inputs=[
|
2253 |
+
report_output,
|
2254 |
+
report_patient_name,
|
2255 |
+
report_record_id,
|
2256 |
+
report_age,
|
2257 |
+
report_gender,
|
2258 |
+
report_date,
|
2259 |
+
report_clinician
|
2260 |
+
],
|
2261 |
+
outputs=[report_download_status]
|
2262 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2263 |
|
2264 |
# Report generator button
|
2265 |
def on_generate_report(name, record_id, age, gender, date, clinician, results, report_type):
|