SreekarB commited on
Commit
a9de5f0
Β·
verified Β·
1 Parent(s): dd1f25c

Upload 13 files

Browse files
README_APPS.md ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # πŸ—£οΈ CASL Analysis Tool - App Options
2
+
3
+ ## πŸ“‚ Main Applications (Choose One for Deployment)
4
+
5
+ ### 1. **`simple_casl_app.py`** ⭐ RECOMMENDED
6
+ - **Lines**: 186
7
+ - **Features**: File upload β†’ LLM analysis β†’ Results display
8
+ - **Best for**: Quick deployment, reliable functionality
9
+ - **Dependencies**: Minimal (gradio, boto3)
10
+ - **Complexity**: ⭐ Simple
11
+
12
+ ### 2. **`moderate_casl_app.py`**
13
+ - **Lines**: 760
14
+ - **Features**: Analysis + Audio transcription + PDF export
15
+ - **Best for**: Balanced features without complexity
16
+ - **Dependencies**: Moderate (+ speech_recognition, reportlab)
17
+ - **Complexity**: ⭐⭐ Moderate
18
+
19
+ ### 3. **`full_casl_app.py`**
20
+ - **Lines**: 683
21
+ - **Features**: Complete interface + visualizations + records
22
+ - **Best for**: Full-featured deployment
23
+ - **Dependencies**: Full set (+ matplotlib, numpy, pandas)
24
+ - **Complexity**: ⭐⭐⭐ Advanced
25
+
26
+ ### 4. **`experimental_casl_app.py`**
27
+ - **Lines**: 1443
28
+ - **Features**: Enhanced analytics + patient database + advanced visualizations
29
+ - **Best for**: Research/experimental features
30
+ - **Dependencies**: Extended (+ seaborn, typing)
31
+ - **Complexity**: ⭐⭐⭐⭐ Experimental
32
+
33
+ ## πŸ“š Reference Files
34
+
35
+ ### **`aphasia_analysis_app_code.py`**
36
+ - **Purpose**: Reference implementation with working Bedrock API calls
37
+ - **Contains**: Correct model format, API structure
38
+ - **Use**: Copy Bedrock call patterns from this file
39
+
40
+ ## πŸ—‚οΈ Reference Files (Archived)
41
+ Located in `/reference_files/` folder:
42
+ - Original implementations and variations
43
+ - Legacy code for reference
44
+ - Alternative approaches
45
+
46
+ ## πŸš€ Quick Start
47
+
48
+ ### For HuggingFace Spaces:
49
+
50
+ 1. **Choose your app** (recommend `simple_casl_app.py`)
51
+ 2. **Update README.md**:
52
+ ```yaml
53
+ app_file: simple_casl_app.py
54
+ ```
55
+ 3. **Deploy** with `requirements.txt`
56
+
57
+ ### Local Testing:
58
+ ```bash
59
+ python simple_casl_app.py # Simplest
60
+ python moderate_casl_app.py # Balanced
61
+ python full_casl_app.py # Complete
62
+ python experimental_casl_app.py # Advanced
63
+ ```
64
+
65
+ ## 🎯 Deployment Recommendations
66
+
67
+ | Use Case | Recommended App | Why |
68
+ |----------|----------------|-----|
69
+ | **Quick Demo** | `simple_casl_app.py` | Fast, reliable, minimal dependencies |
70
+ | **Production** | `moderate_casl_app.py` | Good features, stable |
71
+ | **Research** | `full_casl_app.py` | Complete functionality |
72
+ | **Development** | `experimental_casl_app.py` | Latest features |
73
+
74
+ ## πŸ“‹ Current README.md Configuration
75
+ - Currently points to: `app.py` (needs update)
76
+ - Should point to your chosen app file
experimental_casl_app.py ADDED
@@ -0,0 +1,1444 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import boto3
3
+ import json
4
+ import pandas as pd
5
+ import matplotlib.pyplot as plt
6
+ import numpy as np
7
+ import re
8
+ import logging
9
+ import os
10
+ import pickle
11
+ import csv
12
+ from PIL import Image
13
+ import io
14
+ import uuid
15
+ from datetime import datetime
16
+ import tempfile
17
+ import time
18
+ import seaborn as sns
19
+ from typing import Dict, List, Tuple, Optional
20
+
21
+ # Try to import ReportLab (needed for PDF generation)
22
+ try:
23
+ from reportlab.lib.pagesizes import letter, A4
24
+ from reportlab.lib import colors
25
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image as RLImage
26
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
27
+ from reportlab.lib.units import inch
28
+ REPORTLAB_AVAILABLE = True
29
+ except ImportError:
30
+ REPORTLAB_AVAILABLE = False
31
+
32
+ # Try to import PyPDF2 (needed for PDF reading)
33
+ try:
34
+ import PyPDF2
35
+ PYPDF2_AVAILABLE = True
36
+ except ImportError:
37
+ PYPDF2_AVAILABLE = False
38
+
39
+ # Try to import speech recognition for local audio processing
40
+ try:
41
+ import speech_recognition as sr
42
+ import pydub
43
+ SPEECH_RECOGNITION_AVAILABLE = True
44
+ except ImportError:
45
+ SPEECH_RECOGNITION_AVAILABLE = False
46
+
47
+ # Configure logging
48
+ logging.basicConfig(level=logging.INFO)
49
+ logger = logging.getLogger(__name__)
50
+
51
+ # AWS credentials for Bedrock API (optional - app works without AWS)
52
+ AWS_ACCESS_KEY = os.getenv("AWS_ACCESS_KEY", "")
53
+ AWS_SECRET_KEY = os.getenv("AWS_SECRET_KEY", "")
54
+ AWS_REGION = os.getenv("AWS_REGION", "us-east-1")
55
+
56
+ # Initialize AWS clients if credentials are available
57
+ bedrock_client = None
58
+
59
+ if AWS_ACCESS_KEY and AWS_SECRET_KEY:
60
+ try:
61
+ bedrock_client = boto3.client(
62
+ 'bedrock-runtime',
63
+ aws_access_key_id=AWS_ACCESS_KEY,
64
+ aws_secret_access_key=AWS_SECRET_KEY,
65
+ region_name=AWS_REGION
66
+ )
67
+ logger.info("Bedrock client initialized successfully")
68
+ except Exception as e:
69
+ logger.error(f"Failed to initialize AWS Bedrock client: {str(e)}")
70
+
71
+ # Enhanced sample transcripts for different scenarios
72
+ SAMPLE_TRANSCRIPTS = {
73
+ "Beach Trip (Child)": """*PAR: today I would &-um like to talk about &-um a fun trip I took last &-um summer with my family.
74
+ *PAR: we went to the &-um &-um beach [//] no to the mountains [//] I mean the beach actually.
75
+ *PAR: there was lots of &-um &-um swimming and &-um sun.
76
+ *PAR: we [/] we stayed for &-um three no [//] four days in a &-um hotel near the water [: ocean] [*].
77
+ *PAR: my favorite part was &-um building &-um castles with sand.
78
+ *PAR: sometimes I forget [//] forgetted [: forgot] [*] what they call those things we built.
79
+ *PAR: my brother he [//] he helped me dig a big hole.
80
+ *PAR: we saw [/] saw fishies [: fish] [*] swimming in the water.
81
+ *PAR: sometimes I wonder [/] wonder where fishies [: fish] [*] go when it's cold.
82
+ *PAR: maybe they have [/] have houses under the water.
83
+ *PAR: after swimming we [//] I eat [: ate] [*] &-um ice cream with &-um chocolate things on top.
84
+ *PAR: what do you call those &-um &-um sprinkles! that's the word.
85
+ *PAR: my mom said to &-um that I could have &-um two scoops next time.
86
+ *PAR: I want to go back to the beach [/] beach next year.""",
87
+
88
+ "School Day (Adolescent)": """*PAR: yesterday was &-um kind of a weird day at school.
89
+ *PAR: I had this big test in math and I was like really nervous about it.
90
+ *PAR: when I got there [//] when I got to class the teacher said we could use calculators.
91
+ *PAR: I was like &-oh &-um that's good because I always mess up the &-um the calculations.
92
+ *PAR: there was this one problem about &-um what do you call it &-um geometry I think.
93
+ *PAR: I couldn't remember the formula for [//] I mean I knew it but I just couldn't think of it.
94
+ *PAR: so I raised my hand and asked the teacher and she was really nice about it.
95
+ *PAR: after the test me and my friends went to lunch and we talked about how we did.
96
+ *PAR: everyone was saying it was hard but I think I did okay.
97
+ *PAR: oh and then in English class we had to read our essays out loud.
98
+ *PAR: I hate doing that because I get really nervous and I start talking fast.
99
+ *PAR: but the teacher said mine was good which made me feel better.""",
100
+
101
+ "Adult Stroke Recovery": """*PAR: I &-um I want to talk about &-uh my &-um recovery.
102
+ *PAR: it's been &-um [//] it's hard to &-um to find the words sometimes.
103
+ *PAR: before the &-um the stroke I was &-um working at the &-uh at the bank.
104
+ *PAR: now I have to &-um practice speaking every day with my therapist.
105
+ *PAR: my wife she [//] she helps me a lot at home.
106
+ *PAR: we do &-um exercises together like &-uh reading and &-um talking about pictures.
107
+ *PAR: sometimes I get frustrated because I know what I want to say but &-um the words don't come out right.
108
+ *PAR: but I'm getting better little by little.
109
+ *PAR: the doctor says I'm making good progress.
110
+ *PAR: I hope to go back to work someday but right now I'm focusing on &-um getting better."""
111
+ }
112
+
113
+ # ===============================
114
+ # Database and Storage Functions
115
+ # ===============================
116
+
117
+ # Create data directories if they don't exist
118
+ DATA_DIR = os.environ.get("DATA_DIR", "patient_data")
119
+ RECORDS_FILE = os.path.join(DATA_DIR, "patient_records.csv")
120
+ ANALYSES_DIR = os.path.join(DATA_DIR, "analyses")
121
+ DOWNLOADS_DIR = os.path.join(DATA_DIR, "downloads")
122
+ AUDIO_DIR = os.path.join(DATA_DIR, "audio")
123
+
124
+ def ensure_data_dirs():
125
+ """Ensure data directories exist with enhanced error handling"""
126
+ global DOWNLOADS_DIR, AUDIO_DIR, ANALYSES_DIR, RECORDS_FILE
127
+ try:
128
+ os.makedirs(DATA_DIR, exist_ok=True)
129
+ os.makedirs(ANALYSES_DIR, exist_ok=True)
130
+ os.makedirs(DOWNLOADS_DIR, exist_ok=True)
131
+ os.makedirs(AUDIO_DIR, exist_ok=True)
132
+ logger.info(f"Data directories created: {DATA_DIR}")
133
+
134
+ # Create records file if it doesn't exist
135
+ if not os.path.exists(RECORDS_FILE):
136
+ with open(RECORDS_FILE, 'w', newline='', encoding='utf-8') as f:
137
+ writer = csv.writer(f)
138
+ writer.writerow([
139
+ "ID", "Name", "Record ID", "Age", "Gender",
140
+ "Assessment Date", "Clinician", "Analysis Date", "File Path",
141
+ "Summary Score", "Notes"
142
+ ])
143
+ except Exception as e:
144
+ logger.warning(f"Could not create data directories: {str(e)}")
145
+ # Fallback to tmp directory for cloud environments
146
+ temp_base = os.path.join(tempfile.gettempdir(), "casl_data")
147
+ DOWNLOADS_DIR = os.path.join(temp_base, "downloads")
148
+ AUDIO_DIR = os.path.join(temp_base, "audio")
149
+ ANALYSES_DIR = os.path.join(temp_base, "analyses")
150
+ RECORDS_FILE = os.path.join(temp_base, "patient_records.csv")
151
+
152
+ os.makedirs(DOWNLOADS_DIR, exist_ok=True)
153
+ os.makedirs(AUDIO_DIR, exist_ok=True)
154
+ os.makedirs(ANALYSES_DIR, exist_ok=True)
155
+
156
+ if not os.path.exists(RECORDS_FILE):
157
+ with open(RECORDS_FILE, 'w', newline='', encoding='utf-8') as f:
158
+ writer = csv.writer(f)
159
+ writer.writerow([
160
+ "ID", "Name", "Record ID", "Age", "Gender",
161
+ "Assessment Date", "Clinician", "Analysis Date", "File Path",
162
+ "Summary Score", "Notes"
163
+ ])
164
+
165
+ logger.info(f"Using temporary directories: {temp_base}")
166
+
167
+ # Initialize data directories
168
+ ensure_data_dirs()
169
+
170
+ def save_patient_record(patient_info: Dict, analysis_results: Dict, transcript: str) -> Optional[str]:
171
+ """Save patient record to storage with enhanced data structure"""
172
+ try:
173
+ record_id = str(uuid.uuid4())
174
+
175
+ # Extract patient information
176
+ name = patient_info.get("name", "")
177
+ patient_id = patient_info.get("record_id", "")
178
+ age = patient_info.get("age", "")
179
+ gender = patient_info.get("gender", "")
180
+ assessment_date = patient_info.get("assessment_date", "")
181
+ clinician = patient_info.get("clinician", "")
182
+ notes = patient_info.get("notes", "")
183
+
184
+ # Calculate summary score (average of CASL domain scores)
185
+ summary_score = calculate_summary_score(analysis_results)
186
+
187
+ # Create filename for the analysis data
188
+ filename = f"analysis_{record_id}.pkl"
189
+ filepath = os.path.join(ANALYSES_DIR, filename)
190
+
191
+ # Save enhanced analysis data
192
+ analysis_data = {
193
+ "patient_info": patient_info,
194
+ "analysis_results": analysis_results,
195
+ "transcript": transcript,
196
+ "timestamp": datetime.now().isoformat(),
197
+ "summary_score": summary_score,
198
+ "version": "2.0" # For future compatibility
199
+ }
200
+
201
+ with open(filepath, 'wb') as f:
202
+ pickle.dump(analysis_data, f)
203
+
204
+ # Add record to CSV file
205
+ with open(RECORDS_FILE, 'a', newline='', encoding='utf-8') as f:
206
+ writer = csv.writer(f)
207
+ writer.writerow([
208
+ record_id, name, patient_id, age, gender,
209
+ assessment_date, clinician, datetime.now().strftime('%Y-%m-%d'),
210
+ filepath, summary_score, notes
211
+ ])
212
+
213
+ return record_id
214
+
215
+ except Exception as e:
216
+ logger.error(f"Error saving patient record: {str(e)}")
217
+ return None
218
+
219
+ def calculate_summary_score(analysis_results: Dict) -> float:
220
+ """Calculate an overall summary score from CASL domain scores"""
221
+ try:
222
+ # Extract CASL scores from results
223
+ casl_data = analysis_results.get('casl_data', '')
224
+ scores = []
225
+
226
+ # Look for standard scores in the CASL data
227
+ score_pattern = r'Standard Score \((\d+)\)'
228
+ matches = re.findall(score_pattern, casl_data)
229
+
230
+ if matches:
231
+ scores = [int(score) for score in matches]
232
+ return round(sum(scores) / len(scores), 1)
233
+
234
+ return 85.0 # Default score if parsing fails
235
+ except Exception:
236
+ return 85.0
237
+
238
+ def get_all_patient_records() -> List[Dict]:
239
+ """Return a list of all patient records with enhanced filtering"""
240
+ try:
241
+ records = []
242
+ ensure_data_dirs()
243
+
244
+ if not os.path.exists(RECORDS_FILE):
245
+ return records
246
+
247
+ with open(RECORDS_FILE, 'r', newline='', encoding='utf-8') as f:
248
+ reader = csv.reader(f)
249
+ header = next(reader, None)
250
+ if not header:
251
+ return records
252
+
253
+ for row in reader:
254
+ if len(row) < 9:
255
+ continue
256
+
257
+ file_path = row[8] if len(row) > 8 else ""
258
+ file_exists = os.path.exists(file_path) if file_path else False
259
+ summary_score = row[9] if len(row) > 9 else "N/A"
260
+ notes = row[10] if len(row) > 10 else ""
261
+
262
+ record = {
263
+ "id": row[0],
264
+ "name": row[1],
265
+ "record_id": row[2],
266
+ "age": row[3],
267
+ "gender": row[4],
268
+ "assessment_date": row[5],
269
+ "clinician": row[6],
270
+ "analysis_date": row[7],
271
+ "file_path": file_path,
272
+ "summary_score": summary_score,
273
+ "notes": notes,
274
+ "status": "Valid" if file_exists else "Missing File"
275
+ }
276
+ records.append(record)
277
+
278
+ # Sort by analysis date (most recent first)
279
+ records.sort(key=lambda x: x.get('analysis_date', ''), reverse=True)
280
+ return records
281
+
282
+ except Exception as e:
283
+ logger.error(f"Error getting patient records: {str(e)}")
284
+ return []
285
+
286
+ # ===============================
287
+ # Enhanced Utility Functions
288
+ # ===============================
289
+
290
+ def read_pdf(file_path: str) -> str:
291
+ """Read text from a PDF file with better error handling"""
292
+ if not PYPDF2_AVAILABLE:
293
+ return "Error: PDF reading requires PyPDF2 library. Install with: pip install PyPDF2"
294
+
295
+ try:
296
+ with open(file_path, 'rb') as file:
297
+ pdf_reader = PyPDF2.PdfReader(file)
298
+ text = ""
299
+ for page_num, page in enumerate(pdf_reader.pages):
300
+ try:
301
+ text += page.extract_text() + "\n"
302
+ except Exception as e:
303
+ logger.warning(f"Error reading page {page_num}: {str(e)}")
304
+ continue
305
+ return text.strip()
306
+ except Exception as e:
307
+ logger.error(f"Error reading PDF: {str(e)}")
308
+ return f"Error reading PDF: {str(e)}"
309
+
310
+ def read_cha_file(file_path: str) -> str:
311
+ """Enhanced CHA file parser with better CHAT format support"""
312
+ try:
313
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
314
+ content = f.read()
315
+
316
+ # Extract participant lines (starting with *PAR: or *CHI:)
317
+ participant_lines = []
318
+ investigator_lines = []
319
+
320
+ for line in content.splitlines():
321
+ line = line.strip()
322
+ if line.startswith('*PAR:') or line.startswith('*CHI:'):
323
+ participant_lines.append(line)
324
+ elif line.startswith('*INV:') or line.startswith('*EXA:'):
325
+ investigator_lines.append(line)
326
+
327
+ # Combine participant and investigator lines in chronological order
328
+ all_lines = []
329
+ for line in content.splitlines():
330
+ line = line.strip()
331
+ if line.startswith('*PAR:') or line.startswith('*CHI:') or line.startswith('*INV:') or line.startswith('*EXA:'):
332
+ all_lines.append(line)
333
+
334
+ if all_lines:
335
+ return '\n'.join(all_lines)
336
+ elif participant_lines:
337
+ return '\n'.join(participant_lines)
338
+ else:
339
+ return content
340
+
341
+ except Exception as e:
342
+ logger.error(f"Error reading CHA file: {str(e)}")
343
+ return ""
344
+
345
+ def process_upload(file) -> str:
346
+ """Enhanced file processing with support for multiple formats"""
347
+ if file is None:
348
+ return ""
349
+
350
+ file_path = file.name
351
+ file_ext = os.path.splitext(file_path)[1].lower()
352
+
353
+ try:
354
+ if file_ext == '.pdf':
355
+ return read_pdf(file_path)
356
+ elif file_ext == '.cha':
357
+ return read_cha_file(file_path)
358
+ elif file_ext in ['.txt', '.doc', '.docx']:
359
+ # For .doc/.docx, you might want to add python-docx support
360
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
361
+ return f.read()
362
+ else:
363
+ # Try to read as text file
364
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
365
+ content = f.read()
366
+ if len(content.strip()) == 0:
367
+ return "Error: File appears to be empty or in an unsupported format."
368
+ return content
369
+ except Exception as e:
370
+ logger.error(f"Error processing uploaded file: {str(e)}")
371
+ return f"Error reading file: {str(e)}"
372
+
373
+ # ===============================
374
+ # Enhanced Audio Processing (Local)
375
+ # ===============================
376
+
377
+ def transcribe_audio_local(audio_path: str) -> str:
378
+ """Local audio transcription using speech_recognition library"""
379
+ if not SPEECH_RECOGNITION_AVAILABLE:
380
+ return generate_demo_transcription()
381
+
382
+ try:
383
+ r = sr.Recognizer()
384
+
385
+ # Convert audio to WAV if needed
386
+ if not audio_path.endswith('.wav'):
387
+ try:
388
+ audio = pydub.AudioSegment.from_file(audio_path)
389
+ wav_path = audio_path.rsplit('.', 1)[0] + '.wav'
390
+ audio.export(wav_path, format="wav")
391
+ audio_path = wav_path
392
+ except Exception as e:
393
+ logger.error(f"Error converting audio: {str(e)}")
394
+ return f"Error: Could not process audio file. {str(e)}"
395
+
396
+ # Transcribe audio
397
+ with sr.AudioFile(audio_path) as source:
398
+ audio_data = r.record(source)
399
+ try:
400
+ text = r.recognize_google(audio_data)
401
+ return format_transcription_as_chat(text)
402
+ except sr.UnknownValueError:
403
+ return "Error: Could not understand audio"
404
+ except sr.RequestError as e:
405
+ return f"Error: Could not request results; {e}"
406
+
407
+ except Exception as e:
408
+ logger.error(f"Error in local transcription: {str(e)}")
409
+ return generate_demo_transcription()
410
+
411
+ def format_transcription_as_chat(text: str) -> str:
412
+ """Format transcribed text into CHAT format"""
413
+ # Split text into sentences and format as participant speech
414
+ sentences = re.split(r'[.!?]+', text)
415
+ chat_lines = []
416
+
417
+ for sentence in sentences:
418
+ sentence = sentence.strip()
419
+ if sentence:
420
+ chat_lines.append(f"*PAR: {sentence}.")
421
+
422
+ return '\n'.join(chat_lines)
423
+
424
+ def generate_demo_transcription() -> str:
425
+ """Generate a demo transcription when real transcription isn't available"""
426
+ return """*PAR: today I want to tell you about my favorite toy.
427
+ *PAR: it's a &-um teddy bear that I got for my birthday.
428
+ *PAR: he has &-um brown fur and a red bow.
429
+ *PAR: I like to sleep with him every night.
430
+ *PAR: sometimes I take him to school in my backpack.
431
+ *INV: what's your teddy bear's name?
432
+ *PAR: his name is &-um Brownie because he's brown.
433
+ *PAR: he makes me feel &-um safe when I'm scared."""
434
+
435
+ # ===============================
436
+ # Enhanced AI Analysis Functions
437
+ # ===============================
438
+
439
+ def call_bedrock(prompt: str, max_tokens: int = 4096) -> str:
440
+ """Enhanced Bedrock API call with better error handling"""
441
+ if not bedrock_client:
442
+ logger.info("Bedrock client not available, using enhanced demo response")
443
+ return generate_enhanced_demo_response(prompt)
444
+
445
+ try:
446
+ body = json.dumps({
447
+ "anthropic_version": "bedrock-2023-05-31",
448
+ "max_tokens": max_tokens,
449
+ "messages": [{"role": "user", "content": prompt}],
450
+ "temperature": 0.3,
451
+ "top_p": 0.9
452
+ })
453
+
454
+ response = bedrock_client.invoke_model(
455
+ body=body,
456
+ modelId='anthropic.claude-3-sonnet-20240229-v1:0',
457
+ accept='application/json',
458
+ contentType='application/json'
459
+ )
460
+ response_body = json.loads(response.get('body').read())
461
+ return response_body['content'][0]['text']
462
+ except Exception as e:
463
+ logger.error(f"Error in call_bedrock: {str(e)}")
464
+ return generate_enhanced_demo_response(prompt)
465
+
466
+ def generate_enhanced_demo_response(prompt: str) -> str:
467
+ """Generate sophisticated demo responses based on transcript analysis"""
468
+ # Analyze the transcript in the prompt to generate more realistic responses
469
+ transcript_match = re.search(r'TRANSCRIPT:\s*(.*?)(?=\n\n|\Z)', prompt, re.DOTALL)
470
+ transcript = transcript_match.group(1) if transcript_match else ""
471
+
472
+ # Count various speech patterns
473
+ um_count = len(re.findall(r'&-um|&-uh', transcript))
474
+ revision_count = len(re.findall(r'\[//\]', transcript))
475
+ repetition_count = len(re.findall(r'\[/\]', transcript))
476
+ error_count = len(re.findall(r'\[\*\]', transcript))
477
+
478
+ # Generate scores based on patterns found
479
+ fluency_score = max(70, 100 - (um_count * 2))
480
+ syntactic_score = max(70, 100 - (error_count * 3))
481
+ semantic_score = max(75, 105 - (revision_count * 2))
482
+
483
+ # Convert to percentiles
484
+ fluency_percentile = int(np.interp(fluency_score, [70, 85, 100, 115], [5, 16, 50, 84]))
485
+ syntactic_percentile = int(np.interp(syntactic_score, [70, 85, 100, 115], [5, 16, 50, 84]))
486
+ semantic_percentile = int(np.interp(semantic_score, [70, 85, 100, 115], [5, 16, 50, 84]))
487
+
488
+ # Determine performance levels
489
+ def get_performance_level(score):
490
+ if score < 70: return "Well Below Average"
491
+ elif score < 85: return "Below Average"
492
+ elif score < 115: return "Average"
493
+ elif score < 130: return "Above Average"
494
+ else: return "Well Above Average"
495
+
496
+ response = f"""<SPEECH_FACTORS_START>
497
+ Difficulty producing fluent speech: {um_count + revision_count}, {100 - fluency_percentile}
498
+ Examples:
499
+ - Direct quotes showing disfluencies from transcript
500
+ - Pauses and hesitations noted
501
+
502
+ Word retrieval issues: {um_count // 2 + 1}, {90 - semantic_percentile}
503
+ Examples:
504
+ - Word-finding difficulties observed
505
+ - Circumlocutions and fillers
506
+
507
+ Grammatical errors: {error_count}, {85 - syntactic_percentile}
508
+ Examples:
509
+ - Morphological and syntactic errors identified
510
+ - Verb tense and agreement issues
511
+
512
+ Repetitions and revisions: {repetition_count + revision_count}, {80 - fluency_percentile}
513
+ Examples:
514
+ - Self-corrections and repairs noted
515
+ - Repetitive patterns observed
516
+ <SPEECH_FACTORS_END>
517
+
518
+ <CASL_SKILLS_START>
519
+ Lexical/Semantic Skills: Standard Score ({semantic_score}), Percentile Rank ({semantic_percentile}%), {get_performance_level(semantic_score)}
520
+ Examples:
521
+ - Vocabulary usage and word selection patterns
522
+ - Semantic precision and concept expression
523
+
524
+ Syntactic Skills: Standard Score ({syntactic_score}), Percentile Rank ({syntactic_percentile}%), {get_performance_level(syntactic_score)}
525
+ Examples:
526
+ - Sentence structure and grammatical accuracy
527
+ - Morphological skill demonstration
528
+
529
+ Supralinguistic Skills: Standard Score ({fluency_score}), Percentile Rank ({fluency_percentile}%), {get_performance_level(fluency_score)}
530
+ Examples:
531
+ - Discourse organization and coherence
532
+ - Pragmatic language use and narrative skills
533
+ <CASL_SKILLS_END>
534
+
535
+ <TREATMENT_RECOMMENDATIONS_START>
536
+ - Target word-finding strategies with semantic feature analysis and phonemic cuing
537
+ - Implement sentence formulation exercises focusing on grammatical accuracy
538
+ - Practice narrative structure with visual supports and story grammar elements
539
+ - Use self-monitoring techniques to increase awareness of communication breakdowns
540
+ - Incorporate fluency shaping strategies to reduce disfluencies and improve flow
541
+ <TREATMENT_RECOMMENDATIONS_END>
542
+
543
+ <EXPLANATION_START>
544
+ The language sample demonstrates patterns consistent with a mild-to-moderate language disorder affecting primarily expressive skills. Word-finding difficulties and syntactic challenges are evident, while overall communicative intent remains clear. The presence of self-corrections indicates good metalinguistic awareness, which is a positive prognostic indicator for treatment.
545
+ <EXPLANATION_END>
546
+
547
+ <ADDITIONAL_ANALYSIS_START>
548
+ Strengths include maintained topic coherence and attempt at complex narrative structure. Areas of concern center on retrieval efficiency and grammatical formulation. The pattern suggests intact receptive language with specific expressive challenges that would benefit from targeted intervention focusing on lexical access and syntactic formulation.
549
+ <ADDITIONAL_ANALYSIS_END>
550
+
551
+ <DIAGNOSTIC_IMPRESSIONS_START>
552
+ Based on comprehensive analysis, this profile suggests a specific language impairment affecting expressive domains more significantly than receptive abilities. The combination of word-finding difficulties, grammatical errors, and disfluencies indicates need for structured language intervention with focus on lexical organization, syntactic practice, and metacognitive strategy development.
553
+ <DIAGNOSTIC_IMPRESSIONS_END>
554
+
555
+ <ERROR_EXAMPLES_START>
556
+ Word-finding difficulties:
557
+ - Examples of circumlocutions and word substitutions
558
+ - Pause patterns before content words
559
+
560
+ Grammatical errors:
561
+ - Specific morphological and syntactic errors
562
+ - Verb tense and agreement difficulties
563
+
564
+ Fluency disruptions:
565
+ - Repetitions, revisions, and false starts
566
+ - Filled and unfilled pause patterns
567
+ <ERROR_EXAMPLES_END>"""
568
+
569
+ return response
570
+
571
+ def parse_casl_response(response: str) -> Dict:
572
+ """Enhanced parsing of LLM response with better error handling and structure"""
573
+ # Extract sections using improved regex patterns
574
+ sections = {
575
+ 'speech_factors': extract_section(response, 'SPEECH_FACTORS'),
576
+ 'casl_data': extract_section(response, 'CASL_SKILLS'),
577
+ 'treatment_suggestions': extract_section(response, 'TREATMENT_RECOMMENDATIONS'),
578
+ 'explanation': extract_section(response, 'EXPLANATION'),
579
+ 'additional_analysis': extract_section(response, 'ADDITIONAL_ANALYSIS'),
580
+ 'diagnostic_impressions': extract_section(response, 'DIAGNOSTIC_IMPRESSIONS'),
581
+ 'specific_errors': extract_section(response, 'ERROR_EXAMPLES')
582
+ }
583
+
584
+ # Create structured analysis
585
+ structured_data = process_speech_factors(sections['speech_factors'])
586
+ casl_structured = process_casl_skills(sections['casl_data'])
587
+
588
+ # Build comprehensive report
589
+ full_report = build_comprehensive_report(sections)
590
+
591
+ return {
592
+ 'speech_factors': structured_data['dataframe'],
593
+ 'casl_data': casl_structured['dataframe'],
594
+ 'treatment_suggestions': parse_treatment_recommendations(sections['treatment_suggestions']),
595
+ 'explanation': sections['explanation'],
596
+ 'additional_analysis': sections['additional_analysis'],
597
+ 'diagnostic_impressions': sections['diagnostic_impressions'],
598
+ 'specific_errors': structured_data['errors'],
599
+ 'full_report': full_report,
600
+ 'raw_response': response,
601
+ 'summary_scores': casl_structured['summary']
602
+ }
603
+
604
+ def extract_section(text: str, section_name: str) -> str:
605
+ """Extract content between section markers"""
606
+ pattern = re.compile(f"<{section_name}_START>(.*?)<{section_name}_END>", re.DOTALL)
607
+ match = pattern.search(text)
608
+ return match.group(1).strip() if match else ""
609
+
610
+ def process_speech_factors(factors_text: str) -> Dict:
611
+ """Process speech factors into structured format"""
612
+ data = {
613
+ 'Factor': [],
614
+ 'Occurrences': [],
615
+ 'Severity': [],
616
+ 'Examples': []
617
+ }
618
+
619
+ errors = {}
620
+ lines = factors_text.split('\n')
621
+ current_factor = None
622
+
623
+ for line in lines:
624
+ line = line.strip()
625
+ if not line:
626
+ continue
627
+
628
+ # Look for factor pattern: "Factor name: count, percentile"
629
+ factor_match = re.match(r'([^:]+):\s*(\d+),\s*(\d+)', line)
630
+ if factor_match:
631
+ factor = factor_match.group(1).strip()
632
+ occurrences = int(factor_match.group(2))
633
+ severity = int(factor_match.group(3))
634
+
635
+ data['Factor'].append(factor)
636
+ data['Occurrences'].append(occurrences)
637
+ data['Severity'].append(severity)
638
+ data['Examples'].append("") # Will be filled later
639
+ current_factor = factor
640
+
641
+ elif line.startswith('- ') and current_factor:
642
+ # This is an example for the current factor
643
+ example = line[2:].strip()
644
+ if example:
645
+ # Update the last added example
646
+ if data['Examples'] and current_factor in data['Factor']:
647
+ idx = data['Factor'].index(current_factor)
648
+ if not data['Examples'][idx]:
649
+ data['Examples'][idx] = example
650
+ else:
651
+ data['Examples'][idx] += f"; {example}"
652
+ errors[current_factor] = example
653
+
654
+ return {
655
+ 'dataframe': pd.DataFrame(data),
656
+ 'errors': errors
657
+ }
658
+
659
+ def process_casl_skills(casl_text: str) -> Dict:
660
+ """Process CASL skills into structured format"""
661
+ data = {
662
+ 'Domain': ['Lexical/Semantic', 'Syntactic', 'Supralinguistic'],
663
+ 'Standard Score': [85, 85, 85], # Default values
664
+ 'Percentile': [16, 16, 16],
665
+ 'Performance Level': ['Below Average', 'Below Average', 'Below Average'],
666
+ 'Examples': ['', '', '']
667
+ }
668
+
669
+ lines = casl_text.split('\n')
670
+
671
+ for line in lines:
672
+ line = line.strip()
673
+ if not line:
674
+ continue
675
+
676
+ # Look for domain scores
677
+ score_match = re.search(r'(Lexical/Semantic|Syntactic|Supralinguistic)\s+Skills:\s+Standard Score \((\d+)\),\s+Percentile Rank \((\d+)%\),\s+(.+)', line)
678
+ if score_match:
679
+ domain = score_match.group(1)
680
+ score = int(score_match.group(2))
681
+ percentile = int(score_match.group(3))
682
+ level = score_match.group(4).strip()
683
+
684
+ if domain == 'Lexical/Semantic':
685
+ idx = 0
686
+ elif domain == 'Syntactic':
687
+ idx = 1
688
+ elif domain == 'Supralinguistic':
689
+ idx = 2
690
+ else:
691
+ continue
692
+
693
+ data['Standard Score'][idx] = score
694
+ data['Percentile'][idx] = percentile
695
+ data['Performance Level'][idx] = level
696
+
697
+ # Calculate summary statistics
698
+ avg_score = sum(data['Standard Score']) / len(data['Standard Score'])
699
+ avg_percentile = sum(data['Percentile']) / len(data['Percentile'])
700
+
701
+ return {
702
+ 'dataframe': pd.DataFrame(data),
703
+ 'summary': {
704
+ 'average_score': round(avg_score, 1),
705
+ 'average_percentile': round(avg_percentile, 1),
706
+ 'overall_level': get_performance_level(avg_score)
707
+ }
708
+ }
709
+
710
+ def get_performance_level(score: float) -> str:
711
+ """Determine performance level from standard score"""
712
+ if score < 70:
713
+ return "Well Below Average"
714
+ elif score < 85:
715
+ return "Below Average"
716
+ elif score < 115:
717
+ return "Average"
718
+ elif score < 130:
719
+ return "Above Average"
720
+ else:
721
+ return "Well Above Average"
722
+
723
+ def parse_treatment_recommendations(treatment_text: str) -> List[str]:
724
+ """Parse treatment recommendations into a list"""
725
+ recommendations = []
726
+ lines = treatment_text.split('\n')
727
+
728
+ for line in lines:
729
+ line = line.strip()
730
+ if line.startswith('- '):
731
+ recommendations.append(line[2:])
732
+ elif line.startswith('β€’ '):
733
+ recommendations.append(line[2:])
734
+ elif line and not line.startswith('#'):
735
+ recommendations.append(line)
736
+
737
+ return [rec for rec in recommendations if rec]
738
+
739
+ def build_comprehensive_report(sections: Dict) -> str:
740
+ """Build a comprehensive formatted report"""
741
+ report = """# Speech Language Assessment Report
742
+
743
+ ## Speech Factors Analysis
744
+
745
+ {speech_factors}
746
+
747
+ ## CASL Skills Assessment
748
+
749
+ {casl_data}
750
+
751
+ ## Treatment Recommendations
752
+
753
+ {treatment_suggestions}
754
+
755
+ ## Clinical Explanation
756
+
757
+ {explanation}
758
+ """.format(**sections)
759
+
760
+ if sections['additional_analysis']:
761
+ report += f"\n## Additional Analysis\n\n{sections['additional_analysis']}"
762
+
763
+ if sections['diagnostic_impressions']:
764
+ report += f"\n## Diagnostic Impressions\n\n{sections['diagnostic_impressions']}"
765
+
766
+ if sections['specific_errors']:
767
+ report += f"\n## Detailed Error Examples\n\n{sections['specific_errors']}"
768
+
769
+ return report
770
+
771
+ def create_enhanced_visualizations(speech_factors_df: pd.DataFrame, casl_data_df: pd.DataFrame) -> plt.Figure:
772
+ """Create enhanced visualizations with better styling"""
773
+ # Set professional styling
774
+ plt.style.use('default')
775
+ sns.set_palette("husl")
776
+
777
+ fig = plt.figure(figsize=(15, 10))
778
+
779
+ # Create a 2x2 grid
780
+ gs = fig.add_gridspec(2, 2, hspace=0.3, wspace=0.3)
781
+
782
+ # Speech factors bar chart
783
+ ax1 = fig.add_subplot(gs[0, 0])
784
+ if not speech_factors_df.empty:
785
+ factors_sorted = speech_factors_df.sort_values('Occurrences', ascending=True)
786
+ bars = ax1.barh(factors_sorted['Factor'], factors_sorted['Occurrences'],
787
+ color=sns.color_palette("viridis", len(factors_sorted)))
788
+ ax1.set_title('Speech Factors Frequency', fontsize=12, fontweight='bold')
789
+ ax1.set_xlabel('Occurrences')
790
+
791
+ # Add value labels
792
+ for i, bar in enumerate(bars):
793
+ width = bar.get_width()
794
+ ax1.text(width + 0.1, bar.get_y() + bar.get_height()/2,
795
+ f'{width:.0f}', ha='left', va='center')
796
+
797
+ # CASL scores
798
+ ax2 = fig.add_subplot(gs[0, 1])
799
+ if not casl_data_df.empty:
800
+ bars = ax2.bar(casl_data_df['Domain'], casl_data_df['Standard Score'],
801
+ color=sns.color_palette("muted", len(casl_data_df)))
802
+ ax2.set_title('CASL Domain Scores', fontsize=12, fontweight='bold')
803
+ ax2.set_ylabel('Standard Score')
804
+ ax2.axhline(y=100, color='red', linestyle='--', alpha=0.7, label='Average (100)')
805
+ ax2.axhline(y=85, color='orange', linestyle='--', alpha=0.7, label='Below Average (85)')
806
+ ax2.legend()
807
+
808
+ # Add score labels
809
+ for i, bar in enumerate(bars):
810
+ height = bar.get_height()
811
+ ax2.text(bar.get_x() + bar.get_width()/2, height + 1,
812
+ f'{height:.0f}', ha='center', va='bottom')
813
+
814
+ # Severity heatmap
815
+ ax3 = fig.add_subplot(gs[1, :])
816
+ if not speech_factors_df.empty:
817
+ # Create a severity matrix
818
+ severity_data = speech_factors_df[['Factor', 'Severity']].set_index('Factor')
819
+ severity_matrix = severity_data.T
820
+
821
+ im = ax3.imshow([severity_data['Severity'].values], cmap='RdYlBu_r', aspect='auto')
822
+ ax3.set_xticks(range(len(severity_data)))
823
+ ax3.set_xticklabels(severity_data.index, rotation=45, ha='right')
824
+ ax3.set_yticks([])
825
+ ax3.set_title('Severity Percentiles (Higher = More Severe)', fontsize=12, fontweight='bold')
826
+
827
+ # Add colorbar
828
+ cbar = plt.colorbar(im, ax=ax3, orientation='horizontal', pad=0.1, shrink=0.8)
829
+ cbar.set_label('Severity Percentile')
830
+
831
+ # Add text annotations
832
+ for i, severity in enumerate(severity_data['Severity'].values):
833
+ ax3.text(i, 0, f'{severity}%', ha='center', va='center',
834
+ color='white' if severity > 50 else 'black', fontweight='bold')
835
+
836
+ plt.tight_layout()
837
+ return fig
838
+
839
+ def analyze_transcript_enhanced(transcript: str, age: int, gender: str) -> Dict:
840
+ """Enhanced transcript analysis with comprehensive assessment"""
841
+
842
+ # Enhanced CASL analysis prompt
843
+ prompt = f"""
844
+ You are an expert speech-language pathologist conducting a comprehensive CASL-2 assessment.
845
+ Analyze this transcript for a {age}-year-old {gender} patient.
846
+
847
+ TRANSCRIPT:
848
+ {transcript}
849
+
850
+ Provide a detailed analysis following this exact format with specific section markers:
851
+
852
+ <SPEECH_FACTORS_START>
853
+ [For each factor, provide: Factor name: count, severity_percentile
854
+ Then list 2-3 specific examples with "- " bullets]
855
+ Difficulty producing fluent speech: X, Y
856
+ Examples:
857
+ - "exact quote from transcript"
858
+ - "another exact quote"
859
+
860
+ Word retrieval issues: X, Y
861
+ Examples:
862
+ - "exact quote showing word-finding difficulty"
863
+ - "another example"
864
+
865
+ [Continue for all relevant factors...]
866
+ <SPEECH_FACTORS_END>
867
+
868
+ <CASL_SKILLS_START>
869
+ Lexical/Semantic Skills: Standard Score (X), Percentile Rank (Y%), Performance Level
870
+ Examples:
871
+ - "specific example of vocabulary use"
872
+
873
+ Syntactic Skills: Standard Score (X), Percentile Rank (Y%), Performance Level
874
+ Examples:
875
+ - "specific grammatical pattern example"
876
+
877
+ Supralinguistic Skills: Standard Score (X), Percentile Rank (Y%), Performance Level
878
+ Examples:
879
+ - "discourse organization example"
880
+ <CASL_SKILLS_END>
881
+
882
+ <TREATMENT_RECOMMENDATIONS_START>
883
+ - Specific, actionable treatment recommendation
884
+ - Another targeted intervention strategy
885
+ - Additional therapeutic approach
886
+ <TREATMENT_RECOMMENDATIONS_END>
887
+
888
+ <EXPLANATION_START>
889
+ Comprehensive clinical explanation of findings and their significance.
890
+ <EXPLANATION_END>
891
+
892
+ <ADDITIONAL_ANALYSIS_START>
893
+ Additional insights for treatment planning and prognosis.
894
+ <ADDITIONAL_ANALYSIS_END>
895
+
896
+ <DIAGNOSTIC_IMPRESSIONS_START>
897
+ Summary of diagnostic findings with specific evidence and recommendations.
898
+ <DIAGNOSTIC_IMPRESSIONS_END>
899
+
900
+ <ERROR_EXAMPLES_START>
901
+ Organized listing of all specific error examples by category.
902
+ <ERROR_EXAMPLES_END>
903
+
904
+ Be sure to:
905
+ 1. Use exact quotes from the transcript as evidence
906
+ 2. Provide realistic standard scores (70-130 range, mean=100, SD=15)
907
+ 3. Calculate appropriate percentiles
908
+ 4. Give specific, evidence-based treatment recommendations
909
+ 5. Consider the patient's age and developmental expectations
910
+ """
911
+
912
+ # Get analysis from AI or demo
913
+ response = call_bedrock(prompt)
914
+
915
+ # Parse and structure the response
916
+ results = parse_casl_response(response)
917
+
918
+ return results
919
+
920
+ # ===============================
921
+ # Enhanced PDF Export Functions
922
+ # ===============================
923
+
924
+ def export_enhanced_pdf(results: Dict, patient_info: Dict) -> str:
925
+ """Create enhanced PDF report with professional styling"""
926
+ if not REPORTLAB_AVAILABLE:
927
+ return "ERROR: PDF export requires ReportLab library. Install with: pip install reportlab"
928
+
929
+ try:
930
+ # Generate filename
931
+ patient_name = patient_info.get("name", "Unknown")
932
+ safe_name = re.sub(r'[^\w\s-]', '', patient_name).strip()
933
+ if not safe_name:
934
+ safe_name = f"analysis_{datetime.now().strftime('%Y%m%d%H%M%S')}"
935
+
936
+ ensure_data_dirs()
937
+ pdf_path = os.path.join(DOWNLOADS_DIR, f"{safe_name}_CASL_Report.pdf")
938
+
939
+ # Create document with better styling
940
+ doc = SimpleDocTemplate(pdf_path, pagesize=A4,
941
+ rightMargin=72, leftMargin=72,
942
+ topMargin=72, bottomMargin=18)
943
+
944
+ styles = getSampleStyleSheet()
945
+
946
+ # Custom styles
947
+ title_style = ParagraphStyle(
948
+ 'CustomTitle',
949
+ parent=styles['Heading1'],
950
+ fontSize=18,
951
+ spaceAfter=30,
952
+ alignment=1, # Center
953
+ textColor=colors.navy
954
+ )
955
+
956
+ heading_style = ParagraphStyle(
957
+ 'CustomHeading',
958
+ parent=styles['Heading2'],
959
+ fontSize=14,
960
+ spaceAfter=12,
961
+ textColor=colors.darkblue,
962
+ borderWidth=1,
963
+ borderColor=colors.lightgrey,
964
+ borderPadding=5,
965
+ backColor=colors.lightgrey
966
+ )
967
+
968
+ story = []
969
+
970
+ # Title page
971
+ story.append(Paragraph("COMPREHENSIVE SPEECH-LANGUAGE ASSESSMENT", title_style))
972
+ story.append(Paragraph("CASL-2 Analysis Report", styles['Heading2']))
973
+ story.append(Spacer(1, 20))
974
+
975
+ # Patient information table
976
+ patient_data = []
977
+ for key, value in patient_info.items():
978
+ if value:
979
+ display_key = key.replace('_', ' ').title()
980
+ patient_data.append([display_key + ":", str(value)])
981
+
982
+ if patient_data:
983
+ patient_table = Table(patient_data, colWidths=[150, 300])
984
+ patient_table.setStyle(TableStyle([
985
+ ('BACKGROUND', (0, 0), (0, -1), colors.lightgrey),
986
+ ('TEXTCOLOR', (0, 0), (0, -1), colors.black),
987
+ ('ALIGN', (0, 0), (0, -1), 'RIGHT'),
988
+ ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
989
+ ('FONTSIZE', (0, 0), (-1, -1), 10),
990
+ ('GRID', (0, 0), (-1, -1), 1, colors.black),
991
+ ('VALIGN', (0, 0), (-1, -1), 'TOP'),
992
+ ]))
993
+ story.append(patient_table)
994
+ story.append(Spacer(1, 20))
995
+
996
+ # Add sections
997
+ sections = [
998
+ ("Speech Factors Analysis", results.get('speech_factors', pd.DataFrame())),
999
+ ("CASL Skills Assessment", results.get('casl_data', pd.DataFrame())),
1000
+ ("Treatment Recommendations", results.get('treatment_suggestions', [])),
1001
+ ("Clinical Explanation", results.get('explanation', "")),
1002
+ ("Additional Analysis", results.get('additional_analysis', "")),
1003
+ ("Diagnostic Impressions", results.get('diagnostic_impressions', ""))
1004
+ ]
1005
+
1006
+ for section_title, content in sections:
1007
+ story.append(Paragraph(section_title, heading_style))
1008
+
1009
+ if isinstance(content, pd.DataFrame) and not content.empty:
1010
+ # Convert DataFrame to table
1011
+ table_data = [content.columns.tolist()] + content.values.tolist()
1012
+ table = Table(table_data)
1013
+ table.setStyle(TableStyle([
1014
+ ('BACKGROUND', (0, 0), (-1, 0), colors.grey),
1015
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
1016
+ ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
1017
+ ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
1018
+ ('FONTSIZE', (0, 0), (-1, -1), 9),
1019
+ ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
1020
+ ('BACKGROUND', (0, 1), (-1, -1), colors.beige),
1021
+ ('GRID', (0, 0), (-1, -1), 1, colors.black)
1022
+ ]))
1023
+ story.append(table)
1024
+ elif isinstance(content, list):
1025
+ for item in content:
1026
+ story.append(Paragraph(f"β€’ {item}", styles['Normal']))
1027
+ elif isinstance(content, str) and content:
1028
+ story.append(Paragraph(content, styles['Normal']))
1029
+
1030
+ story.append(Spacer(1, 12))
1031
+
1032
+ # Footer
1033
+ story.append(Spacer(1, 30))
1034
+ footer_text = f"Report generated on {datetime.now().strftime('%B %d, %Y at %I:%M %p')}"
1035
+ story.append(Paragraph(footer_text, styles['Normal']))
1036
+
1037
+ # Build PDF
1038
+ doc.build(story)
1039
+ logger.info(f"Enhanced PDF report saved: {pdf_path}")
1040
+ return pdf_path
1041
+
1042
+ except Exception as e:
1043
+ logger.error(f"Error creating enhanced PDF: {str(e)}")
1044
+ return f"Error creating PDF: {str(e)}"
1045
+
1046
+ # ===============================
1047
+ # Enhanced Gradio Interface
1048
+ # ===============================
1049
+
1050
+ def create_enhanced_interface():
1051
+ """Create the enhanced Gradio interface with improved UX"""
1052
+
1053
+ # Custom CSS for better styling
1054
+ custom_css = """
1055
+ .gradio-container {
1056
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1057
+ }
1058
+ .tab-nav {
1059
+ background-color: #f8f9fa;
1060
+ }
1061
+ .output-markdown {
1062
+ background-color: #f8f9fa;
1063
+ border: 1px solid #dee2e6;
1064
+ border-radius: 0.375rem;
1065
+ padding: 1rem;
1066
+ }
1067
+ """
1068
+
1069
+ with gr.Blocks(title="Enhanced CASL Analysis Tool", css=custom_css, theme=gr.themes.Soft()) as app:
1070
+
1071
+ gr.Markdown("""
1072
+ # πŸ—£οΈ Enhanced CASL Analysis Tool
1073
+
1074
+ **Comprehensive Assessment of Spoken Language (CASL-2)**
1075
+
1076
+ Professional speech-language assessment tool with advanced analytics and reporting capabilities.
1077
+ """)
1078
+
1079
+ with gr.Tabs() as main_tabs:
1080
+
1081
+ # Enhanced Analysis Tab
1082
+ with gr.TabItem("πŸ“Š Analysis", id=0):
1083
+ with gr.Row():
1084
+ with gr.Column(scale=1):
1085
+ gr.Markdown("### πŸ‘€ Patient Information")
1086
+
1087
+ patient_name = gr.Textbox(
1088
+ label="Patient Name",
1089
+ placeholder="Enter patient name"
1090
+ )
1091
+ record_id = gr.Textbox(
1092
+ label="Medical Record ID",
1093
+ placeholder="Enter medical record ID"
1094
+ )
1095
+
1096
+ with gr.Row():
1097
+ age = gr.Number(
1098
+ label="Age (years)",
1099
+ value=8,
1100
+ minimum=1,
1101
+ maximum=120
1102
+ )
1103
+ gender = gr.Radio(
1104
+ ["male", "female", "other"],
1105
+ label="Gender",
1106
+ value="male"
1107
+ )
1108
+
1109
+ assessment_date = gr.Textbox(
1110
+ label="Assessment Date",
1111
+ placeholder="MM/DD/YYYY",
1112
+ value=datetime.now().strftime('%m/%d/%Y')
1113
+ )
1114
+ clinician_name = gr.Textbox(
1115
+ label="Clinician Name",
1116
+ placeholder="Enter clinician name"
1117
+ )
1118
+ clinical_notes = gr.Textbox(
1119
+ label="Clinical Notes",
1120
+ placeholder="Additional observations or context",
1121
+ lines=2
1122
+ )
1123
+
1124
+ gr.Markdown("### πŸ“ Speech Transcript")
1125
+
1126
+ # Sample transcript selection
1127
+ sample_selector = gr.Dropdown(
1128
+ choices=list(SAMPLE_TRANSCRIPTS.keys()),
1129
+ label="Load Sample Transcript"
1130
+ )
1131
+
1132
+ file_upload = gr.File(
1133
+ label="Upload Transcript File",
1134
+ file_types=[".txt", ".cha", ".pdf"]
1135
+ )
1136
+
1137
+ transcript = gr.Textbox(
1138
+ label="Speech Transcript (CHAT format preferred)",
1139
+ placeholder="Enter or upload transcript...",
1140
+ lines=12
1141
+ )
1142
+
1143
+ with gr.Row():
1144
+ analyze_btn = gr.Button(
1145
+ "πŸ” Analyze Transcript",
1146
+ variant="primary"
1147
+ )
1148
+ save_record_btn = gr.Button(
1149
+ "πŸ’Ύ Save Record",
1150
+ variant="secondary"
1151
+ )
1152
+
1153
+ with gr.Column(scale=1):
1154
+ gr.Markdown("### πŸ“ˆ Analysis Results")
1155
+
1156
+ # Results tabs
1157
+ with gr.Tabs():
1158
+ with gr.TabItem("πŸ“‹ Report"):
1159
+ analysis_output = gr.Markdown(
1160
+ label="Analysis Report"
1161
+ )
1162
+
1163
+ with gr.TabItem("πŸ“Š Visualizations"):
1164
+ plot_output = gr.Plot(
1165
+ label="Analysis Plots"
1166
+ )
1167
+
1168
+ with gr.TabItem("πŸ“‘ Data Tables"):
1169
+ with gr.Row():
1170
+ factors_table = gr.Dataframe(
1171
+ label="Speech Factors",
1172
+ interactive=False
1173
+ )
1174
+ with gr.Row():
1175
+ casl_table = gr.Dataframe(
1176
+ label="CASL Domain Scores",
1177
+ interactive=False
1178
+ )
1179
+
1180
+ # Export options
1181
+ gr.Markdown("### πŸ“€ Export Options")
1182
+ with gr.Row():
1183
+ if REPORTLAB_AVAILABLE:
1184
+ export_pdf_btn = gr.Button(
1185
+ "πŸ“„ Export PDF Report",
1186
+ variant="secondary"
1187
+ )
1188
+ else:
1189
+ gr.Markdown("⚠️ PDF export unavailable - install ReportLab")
1190
+
1191
+ export_csv_btn = gr.Button(
1192
+ "πŸ“Š Export Data (CSV)",
1193
+ variant="secondary"
1194
+ )
1195
+
1196
+ export_status = gr.Markdown("")
1197
+
1198
+ # Enhanced Transcription Tab
1199
+ with gr.TabItem("🎀 Transcription", id=1):
1200
+ with gr.Row():
1201
+ with gr.Column(scale=1):
1202
+ gr.Markdown("### 🎡 Audio Processing")
1203
+ gr.Markdown("""
1204
+ Upload audio recordings for automatic transcription.
1205
+ Supports various audio formats and provides CHAT-formatted output.
1206
+ """)
1207
+
1208
+ transcription_age = gr.Number(
1209
+ label="Patient Age",
1210
+ value=8,
1211
+ minimum=1,
1212
+ maximum=120
1213
+ )
1214
+
1215
+ audio_input = gr.Audio(
1216
+ type="filepath",
1217
+ label="Audio Recording"
1218
+ )
1219
+
1220
+ transcribe_btn = gr.Button(
1221
+ "🎧 Transcribe Audio",
1222
+ variant="primary"
1223
+ )
1224
+
1225
+ with gr.Column(scale=1):
1226
+ transcription_output = gr.Textbox(
1227
+ label="Transcription Result",
1228
+ placeholder="Transcribed text will appear here...",
1229
+ lines=15
1230
+ )
1231
+
1232
+ transcription_status = gr.Markdown("")
1233
+
1234
+ with gr.Row():
1235
+ copy_to_analysis_btn = gr.Button(
1236
+ "πŸ“‹ Use for Analysis",
1237
+ variant="secondary"
1238
+ )
1239
+ save_transcription_btn = gr.Button(
1240
+ "πŸ’Ύ Save Transcription",
1241
+ variant="secondary"
1242
+ )
1243
+
1244
+ # Enhanced Records Management Tab
1245
+ with gr.TabItem("πŸ“š Records", id=2):
1246
+ gr.Markdown("### πŸ—ƒοΈ Patient Records Management")
1247
+
1248
+ with gr.Row():
1249
+ refresh_records_btn = gr.Button(
1250
+ "πŸ”„ Refresh Records",
1251
+ variant="secondary"
1252
+ )
1253
+ delete_record_btn = gr.Button(
1254
+ "πŸ—‘οΈ Delete Selected",
1255
+ variant="stop"
1256
+ )
1257
+
1258
+ records_table = gr.Dataframe(
1259
+ label="Patient Records",
1260
+ headers=["ID", "Name", "Age", "Gender", "Date", "Clinician", "Score", "Status"],
1261
+ interactive=True,
1262
+ wrap=True
1263
+ )
1264
+
1265
+ selected_record_info = gr.Markdown("")
1266
+
1267
+ with gr.Row():
1268
+ load_record_btn = gr.Button(
1269
+ "πŸ“‚ Load Selected Record",
1270
+ variant="primary"
1271
+ )
1272
+ export_records_btn = gr.Button(
1273
+ "πŸ“Š Export All Records",
1274
+ variant="secondary"
1275
+ )
1276
+
1277
+ # ===============================
1278
+ # Event Handlers
1279
+ # ===============================
1280
+
1281
+ def load_sample_transcript(sample_name):
1282
+ if sample_name in SAMPLE_TRANSCRIPTS:
1283
+ return SAMPLE_TRANSCRIPTS[sample_name]
1284
+ return ""
1285
+
1286
+ def perform_analysis(transcript_text, age_val, gender_val, name, record_id, clinician, assessment_date, notes):
1287
+ if not transcript_text or len(transcript_text.strip()) < 20:
1288
+ return "❌ Error: Please provide a longer transcript (at least 20 characters)", None, None, None
1289
+
1290
+ try:
1291
+ # Perform enhanced analysis
1292
+ results = analyze_transcript_enhanced(transcript_text, age_val, gender_val)
1293
+
1294
+ # Create visualizations
1295
+ if not results['speech_factors'].empty or not results['casl_data'].empty:
1296
+ fig = create_enhanced_visualizations(results['speech_factors'], results['casl_data'])
1297
+ else:
1298
+ fig = None
1299
+
1300
+ return (
1301
+ results['full_report'],
1302
+ fig,
1303
+ results['speech_factors'],
1304
+ results['casl_data']
1305
+ )
1306
+
1307
+ except Exception as e:
1308
+ logger.exception("Error during analysis")
1309
+ return f"❌ Error during analysis: {str(e)}", None, None, None
1310
+
1311
+ def save_patient_record_handler(name, record_id, age_val, gender_val, assessment_date, clinician, notes, transcript_text, analysis_report):
1312
+ if not name or not transcript_text or not analysis_report:
1313
+ return "❌ Error: Missing required information for saving record"
1314
+
1315
+ try:
1316
+ patient_info = {
1317
+ "name": name,
1318
+ "record_id": record_id,
1319
+ "age": age_val,
1320
+ "gender": gender_val,
1321
+ "assessment_date": assessment_date,
1322
+ "clinician": clinician,
1323
+ "notes": notes
1324
+ }
1325
+
1326
+ # For saving, we need to re-parse the analysis
1327
+ # This is a simplified version - in practice you'd store the full results
1328
+ results = {"full_report": analysis_report}
1329
+
1330
+ saved_id = save_patient_record(patient_info, results, transcript_text)
1331
+
1332
+ if saved_id:
1333
+ return f"βœ… Record saved successfully! ID: {saved_id}"
1334
+ else:
1335
+ return "❌ Error: Failed to save record"
1336
+
1337
+ except Exception as e:
1338
+ return f"❌ Error saving record: {str(e)}"
1339
+
1340
+ def transcribe_audio_handler(audio_path, age_val):
1341
+ if not audio_path:
1342
+ return "Please upload an audio file first.", "❌ No audio file provided"
1343
+
1344
+ try:
1345
+ result = transcribe_audio_local(audio_path)
1346
+
1347
+ if SPEECH_RECOGNITION_AVAILABLE:
1348
+ status = "βœ… Transcription completed using local speech recognition"
1349
+ else:
1350
+ status = "ℹ️ Demo transcription (install speech_recognition for real transcription)"
1351
+
1352
+ return result, status
1353
+
1354
+ except Exception as e:
1355
+ error_msg = f"❌ Transcription failed: {str(e)}"
1356
+ return f"Error: {str(e)}", error_msg
1357
+
1358
+ def load_records():
1359
+ records = get_all_patient_records()
1360
+ if not records:
1361
+ return []
1362
+
1363
+ # Format for display
1364
+ display_records = []
1365
+ for record in records:
1366
+ display_records.append([
1367
+ record['id'][:8] + "...", # Truncated ID
1368
+ record['name'],
1369
+ record['age'],
1370
+ record['gender'],
1371
+ record['assessment_date'],
1372
+ record['clinician'],
1373
+ record.get('summary_score', 'N/A'),
1374
+ record['status']
1375
+ ])
1376
+
1377
+ return display_records
1378
+
1379
+ # Connect event handlers
1380
+ sample_selector.change(load_sample_transcript, sample_selector, transcript)
1381
+ file_upload.upload(process_upload, file_upload, transcript)
1382
+
1383
+ analyze_btn.click(
1384
+ perform_analysis,
1385
+ inputs=[transcript, age, gender, patient_name, record_id, clinician_name, assessment_date, clinical_notes],
1386
+ outputs=[analysis_output, plot_output, factors_table, casl_table]
1387
+ )
1388
+
1389
+ save_record_btn.click(
1390
+ save_patient_record_handler,
1391
+ inputs=[patient_name, record_id, age, gender, assessment_date, clinician_name, clinical_notes, transcript, analysis_output],
1392
+ outputs=[export_status]
1393
+ )
1394
+
1395
+ transcribe_btn.click(
1396
+ transcribe_audio_handler,
1397
+ inputs=[audio_input, transcription_age],
1398
+ outputs=[transcription_output, transcription_status]
1399
+ )
1400
+
1401
+ copy_to_analysis_btn.click(
1402
+ lambda x: (x, gr.update(selected=0)),
1403
+ inputs=[transcription_output],
1404
+ outputs=[transcript, main_tabs]
1405
+ )
1406
+
1407
+ refresh_records_btn.click(
1408
+ load_records,
1409
+ outputs=[records_table]
1410
+ )
1411
+
1412
+ # Load records on startup
1413
+ app.load(load_records, outputs=[records_table])
1414
+
1415
+ return app
1416
+
1417
+ if __name__ == "__main__":
1418
+ # Check dependencies and provide helpful messages
1419
+ missing_deps = []
1420
+ if not REPORTLAB_AVAILABLE:
1421
+ missing_deps.append("reportlab (for PDF export)")
1422
+ if not PYPDF2_AVAILABLE:
1423
+ missing_deps.append("PyPDF2 (for PDF reading)")
1424
+ if not SPEECH_RECOGNITION_AVAILABLE:
1425
+ missing_deps.append("speech_recognition & pydub (for audio transcription)")
1426
+
1427
+ if missing_deps:
1428
+ print("πŸ“‹ Optional dependencies not found:")
1429
+ for dep in missing_deps:
1430
+ print(f" - {dep}")
1431
+ print("\nThe app will work with reduced functionality. Install missing packages for full features.")
1432
+
1433
+ if not AWS_ACCESS_KEY or not AWS_SECRET_KEY:
1434
+ print("ℹ️ AWS credentials not configured - using demo mode for AI analysis.")
1435
+ print(" Set AWS_ACCESS_KEY and AWS_SECRET_KEY environment variables for full functionality.")
1436
+
1437
+ print("πŸš€ Starting Enhanced CASL Analysis Tool...")
1438
+ app = create_enhanced_interface()
1439
+ app.launch(
1440
+ show_api=False,
1441
+ server_name="0.0.0.0", # For cloud deployment
1442
+ server_port=7860, # Standard Gradio port
1443
+ share=False
1444
+ )
full_casl_app.py ADDED
@@ -0,0 +1,684 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import boto3
3
+ import json
4
+ import numpy as np
5
+ import re
6
+ import logging
7
+ import os
8
+ from datetime import datetime
9
+ import tempfile
10
+
11
+ # Configure logging
12
+ logging.basicConfig(level=logging.INFO)
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # Try to import optional dependencies
16
+ try:
17
+ from reportlab.lib.pagesizes import letter
18
+ from reportlab.lib import colors
19
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
20
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
21
+ REPORTLAB_AVAILABLE = True
22
+ except ImportError:
23
+ REPORTLAB_AVAILABLE = False
24
+ logger.info("ReportLab not available - PDF export disabled")
25
+
26
+ try:
27
+ import speech_recognition as sr
28
+ import pydub
29
+ SPEECH_RECOGNITION_AVAILABLE = True
30
+ except ImportError:
31
+ SPEECH_RECOGNITION_AVAILABLE = False
32
+ logger.info("Speech recognition not available - audio transcription will use demo mode")
33
+
34
+ # AWS credentials (optional)
35
+ AWS_ACCESS_KEY = os.getenv("AWS_ACCESS_KEY", "")
36
+ AWS_SECRET_KEY = os.getenv("AWS_SECRET_KEY", "")
37
+ AWS_REGION = os.getenv("AWS_REGION", "us-east-1")
38
+
39
+ # Initialize AWS client if available
40
+ bedrock_client = None
41
+ if AWS_ACCESS_KEY and AWS_SECRET_KEY:
42
+ try:
43
+ bedrock_client = boto3.client(
44
+ 'bedrock-runtime',
45
+ aws_access_key_id=AWS_ACCESS_KEY,
46
+ aws_secret_access_key=AWS_SECRET_KEY,
47
+ region_name=AWS_REGION
48
+ )
49
+ logger.info("Bedrock client initialized successfully")
50
+ except Exception as e:
51
+ logger.error(f"Failed to initialize AWS Bedrock client: {str(e)}")
52
+ else:
53
+ logger.info("AWS credentials not configured - using demo mode")
54
+
55
+ # Data directories
56
+ DATA_DIR = os.environ.get("DATA_DIR", "patient_data")
57
+
58
+ def ensure_data_dirs():
59
+ """Ensure data directories exist"""
60
+ try:
61
+ os.makedirs(DATA_DIR, exist_ok=True)
62
+ logger.info(f"Data directories created: {DATA_DIR}")
63
+ except Exception as e:
64
+ logger.warning(f"Could not create data directories: {str(e)}")
65
+ logger.info("Using temporary directory for data storage")
66
+
67
+ ensure_data_dirs()
68
+
69
+ # Sample transcripts
70
+ SAMPLE_TRANSCRIPTS = {
71
+ "Beach Trip (Child)": """*PAR: today I would &-um like to talk about &-um a fun trip I took last &-um summer with my family.
72
+ *PAR: we went to the &-um &-um beach [//] no to the mountains [//] I mean the beach actually.
73
+ *PAR: there was lots of &-um &-um swimming and &-um sun.
74
+ *PAR: we [/] we stayed for &-um three no [//] four days in a &-um hotel near the water [: ocean] [*].
75
+ *PAR: my favorite part was &-um building &-um castles with sand.
76
+ *PAR: sometimes I forget [//] forgetted [: forgot] [*] what they call those things we built.
77
+ *PAR: my brother he [//] he helped me dig a big hole.
78
+ *PAR: we saw [/] saw fishies [: fish] [*] swimming in the water.
79
+ *PAR: sometimes I wonder [/] wonder where fishies [: fish] [*] go when it's cold.
80
+ *PAR: maybe they have [/] have houses under the water.
81
+ *PAR: after swimming we [//] I eat [: ate] [*] &-um ice cream with &-um chocolate things on top.
82
+ *PAR: what do you call those &-um &-um sprinkles! that's the word.
83
+ *PAR: my mom said to &-um that I could have &-um two scoops next time.
84
+ *PAR: I want to go back to the beach [/] beach next year.""",
85
+
86
+ "School Day (Adolescent)": """*PAR: yesterday was &-um kind of a weird day at school.
87
+ *PAR: I had this big test in math and I was like really nervous about it.
88
+ *PAR: when I got there [//] when I got to class the teacher said we could use calculators.
89
+ *PAR: I was like &-oh &-um that's good because I always mess up the &-um the calculations.
90
+ *PAR: there was this one problem about &-um what do you call it &-um geometry I think.
91
+ *PAR: I couldn't remember the formula for [//] I mean I knew it but I just couldn't think of it.
92
+ *PAR: so I raised my hand and asked the teacher and she was really nice about it.
93
+ *PAR: after the test me and my friends went to lunch and we talked about how we did.
94
+ *PAR: everyone was saying it was hard but I think I did okay.
95
+ *PAR: oh and then in English class we had to read our essays out loud.
96
+ *PAR: I hate doing that because I get really nervous and I start talking fast.
97
+ *PAR: but the teacher said mine was good which made me feel better.""",
98
+
99
+ "Adult Recovery": """*PAR: I &-um I want to talk about &-uh my &-um recovery.
100
+ *PAR: it's been &-um [//] it's hard to &-um to find the words sometimes.
101
+ *PAR: before the &-um the stroke I was &-um working at the &-uh at the bank.
102
+ *PAR: now I have to &-um practice speaking every day with my therapist.
103
+ *PAR: my wife she [//] she helps me a lot at home.
104
+ *PAR: we do &-um exercises together like &-uh reading and &-um talking about pictures.
105
+ *PAR: sometimes I get frustrated because I know what I want to say but &-um the words don't come out right.
106
+ *PAR: but I'm getting better little by little.
107
+ *PAR: the doctor says I'm making good progress.
108
+ *PAR: I hope to go back to work someday but right now I'm focusing on &-um getting better."""
109
+ }
110
+
111
+ def call_bedrock(prompt, max_tokens=4096):
112
+ """Call AWS Bedrock API with correct format or return demo response"""
113
+ if not bedrock_client:
114
+ return generate_demo_response(prompt)
115
+
116
+ try:
117
+ body = json.dumps({
118
+ "anthropic_version": "bedrock-2023-05-31",
119
+ "max_tokens": max_tokens,
120
+ "top_k": 250,
121
+ "stop_sequences": [],
122
+ "temperature": 0.3,
123
+ "top_p": 0.9,
124
+ "messages": [
125
+ {
126
+ "role": "user",
127
+ "content": [
128
+ {
129
+ "type": "text",
130
+ "text": prompt
131
+ }
132
+ ]
133
+ }
134
+ ]
135
+ })
136
+
137
+ # Use the correct model ID
138
+ modelId = 'anthropic.claude-3-5-sonnet-20240620-v1:0'
139
+
140
+ response = bedrock_client.invoke_model(
141
+ body=body,
142
+ modelId=modelId,
143
+ accept='application/json',
144
+ contentType='application/json'
145
+ )
146
+ response_body = json.loads(response.get('body').read())
147
+ return response_body['content'][0]['text']
148
+ except Exception as e:
149
+ logger.error(f"Error calling Bedrock: {str(e)}")
150
+ return generate_demo_response(prompt)
151
+
152
+ def generate_demo_response(prompt):
153
+ """Generate demo analysis response based on transcript patterns"""
154
+ # Extract transcript from prompt
155
+ transcript_match = re.search(r'TRANSCRIPT:\s*(.*?)(?=\n\n|\Z)', prompt, re.DOTALL)
156
+ transcript = transcript_match.group(1) if transcript_match else ""
157
+
158
+ # Count speech patterns
159
+ um_count = len(re.findall(r'&-um|&-uh', transcript))
160
+ revision_count = len(re.findall(r'\[//\]', transcript))
161
+ repetition_count = len(re.findall(r'\[/\]', transcript))
162
+ error_count = len(re.findall(r'\[\*\]', transcript))
163
+
164
+ # Generate realistic scores based on patterns
165
+ fluency_score = max(70, 100 - (um_count * 2))
166
+ syntactic_score = max(70, 100 - (error_count * 3))
167
+ semantic_score = max(75, 105 - (revision_count * 2))
168
+
169
+ # Convert to percentiles
170
+ fluency_percentile = int(np.interp(fluency_score, [70, 85, 100, 115], [5, 16, 50, 84]))
171
+ syntactic_percentile = int(np.interp(syntactic_score, [70, 85, 100, 115], [5, 16, 50, 84]))
172
+ semantic_percentile = int(np.interp(semantic_score, [70, 85, 100, 115], [5, 16, 50, 84]))
173
+
174
+ def get_performance_level(score):
175
+ if score < 70: return "Well Below Average"
176
+ elif score < 85: return "Below Average"
177
+ elif score < 115: return "Average"
178
+ else: return "Above Average"
179
+
180
+ return f"""<SPEECH_FACTORS_START>
181
+ Difficulty producing fluent speech: {um_count + revision_count}, {100 - fluency_percentile}
182
+ Examples:
183
+ - Frequent use of fillers (&-um, &-uh) observed throughout transcript
184
+ - Self-corrections and revisions interrupt speech flow
185
+
186
+ Word retrieval issues: {um_count // 2 + 1}, {90 - semantic_percentile}
187
+ Examples:
188
+ - Hesitations and pauses before content words noted
189
+ - Circumlocutions and word-finding difficulties evident
190
+
191
+ Grammatical errors: {error_count}, {85 - syntactic_percentile}
192
+ Examples:
193
+ - Morphological errors marked with [*] in transcript
194
+ - Verb tense and agreement inconsistencies observed
195
+
196
+ Repetitions and revisions: {repetition_count + revision_count}, {80 - fluency_percentile}
197
+ Examples:
198
+ - Self-corrections marked with [//] throughout sample
199
+ - Word and phrase repetitions marked with [/] noted
200
+ <SPEECH_FACTORS_END>
201
+
202
+ <CASL_SKILLS_START>
203
+ Lexical/Semantic Skills: Standard Score ({semantic_score}), Percentile Rank ({semantic_percentile}%), {get_performance_level(semantic_score)}
204
+ Examples:
205
+ - Vocabulary diversity and semantic precision assessed
206
+ - Word-finding strategies and retrieval patterns analyzed
207
+
208
+ Syntactic Skills: Standard Score ({syntactic_score}), Percentile Rank ({syntactic_percentile}%), {get_performance_level(syntactic_score)}
209
+ Examples:
210
+ - Sentence structure complexity and grammatical accuracy evaluated
211
+ - Morphological skill development measured
212
+
213
+ Supralinguistic Skills: Standard Score ({fluency_score}), Percentile Rank ({fluency_percentile}%), {get_performance_level(fluency_score)}
214
+ Examples:
215
+ - Discourse organization and narrative coherence reviewed
216
+ - Pragmatic language use and communication effectiveness assessed
217
+ <CASL_SKILLS_END>
218
+
219
+ <TREATMENT_RECOMMENDATIONS_START>
220
+ - Implement word-finding strategies with semantic feature analysis and phonemic cuing
221
+ - Practice sentence formulation exercises targeting grammatical accuracy and complexity
222
+ - Use narrative structure activities with visual supports to improve discourse organization
223
+ - Incorporate self-monitoring techniques to increase awareness of speech patterns
224
+ - Apply fluency shaping strategies to reduce disfluencies and improve communication flow
225
+ <TREATMENT_RECOMMENDATIONS_END>
226
+
227
+ <EXPLANATION_START>
228
+ The language sample demonstrates patterns consistent with expressive language challenges affecting fluency, word retrieval, and syntactic formulation. The presence of self-corrections indicates preserved metalinguistic awareness, which is a positive prognostic indicator. Intervention should focus on strengthening lexical access, grammatical formulation, and discourse-level skills while building on existing self-monitoring abilities.
229
+ <EXPLANATION_END>"""
230
+
231
+ def parse_casl_response(response):
232
+ """Parse structured response into components"""
233
+ def extract_section(text, section_name):
234
+ pattern = re.compile(f"<{section_name}_START>(.*?)<{section_name}_END>", re.DOTALL)
235
+ match = pattern.search(text)
236
+ return match.group(1).strip() if match else ""
237
+
238
+ sections = {
239
+ 'speech_factors': extract_section(response, 'SPEECH_FACTORS'),
240
+ 'casl_data': extract_section(response, 'CASL_SKILLS'),
241
+ 'treatment_suggestions': extract_section(response, 'TREATMENT_RECOMMENDATIONS'),
242
+ 'explanation': extract_section(response, 'EXPLANATION')
243
+ }
244
+
245
+ # Build formatted report
246
+ full_report = f"""# Speech Language Assessment Report
247
+
248
+ ## Speech Factors Analysis
249
+ {sections['speech_factors']}
250
+
251
+ ## CASL Skills Assessment
252
+ {sections['casl_data']}
253
+
254
+ ## Treatment Recommendations
255
+ {sections['treatment_suggestions']}
256
+
257
+ ## Clinical Explanation
258
+ {sections['explanation']}
259
+ """
260
+
261
+ return {
262
+ 'speech_factors': sections['speech_factors'],
263
+ 'casl_data': sections['casl_data'],
264
+ 'treatment_suggestions': sections['treatment_suggestions'],
265
+ 'explanation': sections['explanation'],
266
+ 'full_report': full_report,
267
+ 'raw_response': response
268
+ }
269
+
270
+ def analyze_transcript(transcript, age, gender):
271
+ """Analyze transcript using CASL framework"""
272
+ prompt = f"""
273
+ You are an expert speech-language pathologist conducting a comprehensive CASL-2 assessment.
274
+ Analyze this transcript for a {age}-year-old {gender} patient.
275
+
276
+ TRANSCRIPT:
277
+ {transcript}
278
+
279
+ Provide detailed analysis in this exact format:
280
+
281
+ <SPEECH_FACTORS_START>
282
+ Difficulty producing fluent speech: X, Y
283
+ Examples:
284
+ - "exact quote from transcript showing disfluency"
285
+ - "another example with specific evidence"
286
+
287
+ Word retrieval issues: X, Y
288
+ Examples:
289
+ - "quote showing word-finding difficulty"
290
+ - "example of circumlocution or pause"
291
+
292
+ Grammatical errors: X, Y
293
+ Examples:
294
+ - "quote showing morphological error"
295
+ - "example of syntactic difficulty"
296
+
297
+ Repetitions and revisions: X, Y
298
+ Examples:
299
+ - "quote showing self-correction"
300
+ - "example of repetition or revision"
301
+ <SPEECH_FACTORS_END>
302
+
303
+ <CASL_SKILLS_START>
304
+ Lexical/Semantic Skills: Standard Score (X), Percentile Rank (Y%), Performance Level
305
+ Examples:
306
+ - "specific vocabulary usage example"
307
+ - "semantic precision demonstration"
308
+
309
+ Syntactic Skills: Standard Score (X), Percentile Rank (Y%), Performance Level
310
+ Examples:
311
+ - "grammatical structure example"
312
+ - "morphological skill demonstration"
313
+
314
+ Supralinguistic Skills: Standard Score (X), Percentile Rank (Y%), Performance Level
315
+ Examples:
316
+ - "discourse organization example"
317
+ - "narrative coherence demonstration"
318
+ <CASL_SKILLS_END>
319
+
320
+ <TREATMENT_RECOMMENDATIONS_START>
321
+ - Specific, evidence-based treatment recommendation
322
+ - Another targeted intervention strategy
323
+ - Additional therapeutic approach with clear rationale
324
+ <TREATMENT_RECOMMENDATIONS_END>
325
+
326
+ <EXPLANATION_START>
327
+ Comprehensive clinical explanation of findings, their significance for diagnosis and prognosis, and relationship to functional communication needs.
328
+ <EXPLANATION_END>
329
+
330
+ Requirements:
331
+ 1. Use exact quotes from the transcript as evidence
332
+ 2. Provide realistic standard scores (70-130 range, mean=100, SD=15)
333
+ 3. Calculate appropriate percentiles based on age norms
334
+ 4. Give specific, actionable treatment recommendations
335
+ 5. Consider developmental expectations for the patient's age
336
+ """
337
+
338
+ response = call_bedrock(prompt)
339
+ return parse_casl_response(response)
340
+
341
+ def process_upload(file):
342
+ """Process uploaded transcript file"""
343
+ if file is None:
344
+ return ""
345
+
346
+ file_path = file.name
347
+ file_ext = os.path.splitext(file_path)[1].lower()
348
+
349
+ try:
350
+ if file_ext == '.cha':
351
+ # Process CHAT format file
352
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
353
+ content = f.read()
354
+
355
+ # Extract participant lines
356
+ par_lines = []
357
+ inv_lines = []
358
+ for line in content.splitlines():
359
+ line = line.strip()
360
+ if line.startswith('*PAR:') or line.startswith('*CHI:'):
361
+ par_lines.append(line)
362
+ elif line.startswith('*INV:') or line.startswith('*EXA:'):
363
+ inv_lines.append(line)
364
+
365
+ # Combine all relevant lines
366
+ all_lines = []
367
+ for line in content.splitlines():
368
+ line = line.strip()
369
+ if any(line.startswith(prefix) for prefix in ['*PAR:', '*CHI:', '*INV:', '*EXA:']):
370
+ all_lines.append(line)
371
+
372
+ return '\n'.join(all_lines) if all_lines else content
373
+ else:
374
+ # Read as plain text
375
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
376
+ return f.read()
377
+ except Exception as e:
378
+ logger.error(f"Error reading uploaded file: {str(e)}")
379
+ return f"Error reading file: {str(e)}"
380
+
381
+ def transcribe_audio(audio_path):
382
+ """Transcribe audio file to CHAT format"""
383
+ if not audio_path:
384
+ return "Please upload an audio file first.", "❌ No audio file provided"
385
+
386
+ if SPEECH_RECOGNITION_AVAILABLE:
387
+ try:
388
+ r = sr.Recognizer()
389
+
390
+ # Convert to WAV if needed
391
+ wav_path = audio_path
392
+ if not audio_path.endswith('.wav'):
393
+ try:
394
+ audio = pydub.AudioSegment.from_file(audio_path)
395
+ wav_path = audio_path.rsplit('.', 1)[0] + '.wav'
396
+ audio.export(wav_path, format="wav")
397
+ except Exception as e:
398
+ logger.warning(f"Audio conversion failed: {e}")
399
+
400
+ # Transcribe
401
+ with sr.AudioFile(wav_path) as source:
402
+ audio_data = r.record(source)
403
+ text = r.recognize_google(audio_data)
404
+
405
+ # Format as CHAT
406
+ sentences = re.split(r'[.!?]+', text)
407
+ chat_lines = []
408
+ for sentence in sentences:
409
+ sentence = sentence.strip()
410
+ if sentence:
411
+ chat_lines.append(f"*PAR: {sentence}.")
412
+
413
+ result = '\n'.join(chat_lines)
414
+ return result, "βœ… Transcription completed successfully"
415
+
416
+ except sr.UnknownValueError:
417
+ return "Could not understand audio clearly", "❌ Speech not recognized"
418
+ except sr.RequestError as e:
419
+ return f"Error with speech recognition service: {e}", "❌ Service error"
420
+ except Exception as e:
421
+ logger.error(f"Transcription error: {e}")
422
+ return f"Error during transcription: {str(e)}", f"❌ Transcription failed"
423
+ else:
424
+ # Demo transcription
425
+ demo_text = """*PAR: this is a demonstration transcription.
426
+ *PAR: to enable real audio processing install speech_recognition and pydub.
427
+ *PAR: the demo shows how transcribed text would appear in CHAT format."""
428
+ return demo_text, "ℹ️ Demo mode - install speech_recognition for real audio processing"
429
+
430
+ def create_interface():
431
+ """Create the main Gradio interface"""
432
+
433
+ with gr.Blocks(title="CASL Analysis Tool", theme=gr.themes.Soft()) as app:
434
+
435
+ gr.Markdown("""
436
+ # πŸ—£οΈ CASL Analysis Tool
437
+ **Comprehensive Assessment of Spoken Language (CASL-2)**
438
+
439
+ Professional speech-language assessment tool for clinical practice and research.
440
+ Supports transcript analysis, audio transcription, and comprehensive reporting.
441
+ """)
442
+
443
+ with gr.Tabs():
444
+
445
+ # Main Analysis Tab
446
+ with gr.TabItem("πŸ“Š Analysis"):
447
+ with gr.Row():
448
+ with gr.Column():
449
+ gr.Markdown("### πŸ‘€ Patient Information")
450
+
451
+ patient_name = gr.Textbox(
452
+ label="Patient Name",
453
+ placeholder="Enter patient name"
454
+ )
455
+ record_id = gr.Textbox(
456
+ label="Medical Record ID",
457
+ placeholder="Enter medical record ID"
458
+ )
459
+
460
+ with gr.Row():
461
+ age = gr.Number(
462
+ label="Age (years)",
463
+ value=8,
464
+ minimum=1,
465
+ maximum=120
466
+ )
467
+ gender = gr.Radio(
468
+ ["male", "female", "other"],
469
+ label="Gender",
470
+ value="male"
471
+ )
472
+
473
+ assessment_date = gr.Textbox(
474
+ label="Assessment Date",
475
+ placeholder="MM/DD/YYYY",
476
+ value=datetime.now().strftime('%m/%d/%Y')
477
+ )
478
+ clinician_name = gr.Textbox(
479
+ label="Clinician Name",
480
+ placeholder="Enter clinician name"
481
+ )
482
+
483
+ gr.Markdown("### πŸ“ Speech Transcript")
484
+
485
+ sample_selector = gr.Dropdown(
486
+ choices=list(SAMPLE_TRANSCRIPTS.keys()),
487
+ label="Load Sample Transcript",
488
+ placeholder="Choose a sample to load"
489
+ )
490
+
491
+ file_upload = gr.File(
492
+ label="Upload Transcript File",
493
+ file_types=[".txt", ".cha"]
494
+ )
495
+
496
+ transcript = gr.Textbox(
497
+ label="Speech Transcript (CHAT format preferred)",
498
+ placeholder="Enter transcript text or load from samples/file...",
499
+ lines=12
500
+ )
501
+
502
+ analyze_btn = gr.Button(
503
+ "πŸ” Analyze Transcript",
504
+ variant="primary"
505
+ )
506
+
507
+ with gr.Column():
508
+ gr.Markdown("### πŸ“ˆ Analysis Results")
509
+
510
+ analysis_output = gr.Markdown(
511
+ label="Comprehensive CASL Analysis Report",
512
+ value="Analysis results will appear here after clicking 'Analyze Transcript'..."
513
+ )
514
+
515
+ gr.Markdown("### πŸ“€ Export Options")
516
+ if REPORTLAB_AVAILABLE:
517
+ export_btn = gr.Button("πŸ“„ Export as PDF", variant="secondary")
518
+ export_status = gr.Markdown("")
519
+ else:
520
+ gr.Markdown("⚠️ PDF export unavailable (ReportLab not installed)")
521
+
522
+ # Audio Transcription Tab
523
+ with gr.TabItem("🎀 Audio Transcription"):
524
+ with gr.Row():
525
+ with gr.Column():
526
+ gr.Markdown("### 🎡 Audio Processing")
527
+ gr.Markdown("""
528
+ Upload audio recordings for automatic transcription into CHAT format.
529
+ Supports common audio formats (.wav, .mp3, .m4a, .ogg, etc.)
530
+ """)
531
+
532
+ audio_input = gr.Audio(
533
+ type="filepath",
534
+ label="Audio Recording"
535
+ )
536
+
537
+ transcribe_btn = gr.Button(
538
+ "🎧 Transcribe Audio",
539
+ variant="primary"
540
+ )
541
+
542
+ with gr.Column():
543
+ transcription_output = gr.Textbox(
544
+ label="Transcription Result (CHAT Format)",
545
+ placeholder="Transcribed text will appear here...",
546
+ lines=15
547
+ )
548
+
549
+ transcription_status = gr.Markdown("")
550
+
551
+ copy_to_analysis_btn = gr.Button(
552
+ "πŸ“‹ Use for Analysis",
553
+ variant="secondary"
554
+ )
555
+
556
+ # Information Tab
557
+ with gr.TabItem("ℹ️ About"):
558
+ gr.Markdown("""
559
+ ## About the CASL Analysis Tool
560
+
561
+ This tool provides comprehensive speech-language assessment using the CASL-2 (Comprehensive Assessment of Spoken Language) framework.
562
+
563
+ ### Features:
564
+ - **Speech Factor Analysis**: Automated detection of disfluencies, word retrieval issues, grammatical errors, and repetitions
565
+ - **CASL-2 Domains**: Assessment of Lexical/Semantic, Syntactic, and Supralinguistic skills
566
+ - **Professional Scoring**: Standard scores, percentiles, and performance levels
567
+ - **Audio Transcription**: Convert speech recordings to CHAT format transcripts
568
+ - **Treatment Recommendations**: Evidence-based intervention suggestions
569
+
570
+ ### Supported Formats:
571
+ - **Text Files**: .txt format with manual transcript entry
572
+ - **CHAT Files**: .cha format following CHILDES conventions
573
+ - **Audio Files**: .wav, .mp3, .m4a, .ogg for automatic transcription
574
+
575
+ ### CHAT Format Guidelines:
576
+ - Use `*PAR:` for patient utterances
577
+ - Use `*INV:` for investigator/clinician utterances
578
+ - Mark filled pauses as `&-um`, `&-uh`
579
+ - Mark repetitions with `[/]`
580
+ - Mark revisions with `[//]`
581
+ - Mark errors with `[*]`
582
+
583
+ ### Usage Tips:
584
+ 1. Load a sample transcript to see the expected format
585
+ 2. Enter patient information for context-appropriate analysis
586
+ 3. Upload or type transcript in CHAT format for best results
587
+ 4. Review analysis results and treatment recommendations
588
+ 5. Export professional PDF reports for clinical documentation
589
+
590
+ ### Technical Notes:
591
+ - **Demo Mode**: Works without external dependencies using simulated analysis
592
+ - **Enhanced Mode**: Requires AWS Bedrock credentials for AI-powered analysis
593
+ - **Audio Processing**: Requires speech_recognition library for real transcription
594
+ - **PDF Export**: Requires ReportLab library for professional reports
595
+
596
+ For support or questions, please refer to the documentation.
597
+ """)
598
+
599
+ # Event Handlers
600
+ def load_sample_transcript(sample_name):
601
+ """Load selected sample transcript"""
602
+ if sample_name and sample_name in SAMPLE_TRANSCRIPTS:
603
+ return SAMPLE_TRANSCRIPTS[sample_name]
604
+ return ""
605
+
606
+ def perform_analysis(transcript_text, age_val, gender_val):
607
+ """Perform CASL analysis on transcript"""
608
+ if not transcript_text or len(transcript_text.strip()) < 20:
609
+ return "❌ **Error**: Please provide a longer transcript (minimum 20 characters) for meaningful analysis."
610
+
611
+ try:
612
+ # Perform analysis
613
+ results = analyze_transcript(transcript_text, age_val, gender_val)
614
+ return results['full_report']
615
+
616
+ except Exception as e:
617
+ logger.exception("Analysis error")
618
+ return f"❌ **Error during analysis**: {str(e)}\n\nPlease check your transcript format and try again."
619
+
620
+ def copy_transcription_to_analysis(transcription_text):
621
+ """Copy transcription result to analysis tab"""
622
+ return transcription_text
623
+
624
+ # Connect event handlers
625
+ sample_selector.change(
626
+ load_sample_transcript,
627
+ inputs=[sample_selector],
628
+ outputs=[transcript]
629
+ )
630
+
631
+ file_upload.upload(
632
+ process_upload,
633
+ inputs=[file_upload],
634
+ outputs=[transcript]
635
+ )
636
+
637
+ analyze_btn.click(
638
+ perform_analysis,
639
+ inputs=[transcript, age, gender],
640
+ outputs=[analysis_output]
641
+ )
642
+
643
+ transcribe_btn.click(
644
+ transcribe_audio,
645
+ inputs=[audio_input],
646
+ outputs=[transcription_output, transcription_status]
647
+ )
648
+
649
+ copy_to_analysis_btn.click(
650
+ copy_transcription_to_analysis,
651
+ inputs=[transcription_output],
652
+ outputs=[transcript]
653
+ )
654
+
655
+ return app
656
+
657
+ # Create and launch the application
658
+ if __name__ == "__main__":
659
+ # Check for optional dependencies
660
+ missing_deps = []
661
+ if not REPORTLAB_AVAILABLE:
662
+ missing_deps.append("reportlab (for PDF export)")
663
+ if not SPEECH_RECOGNITION_AVAILABLE:
664
+ missing_deps.append("speech_recognition & pydub (for audio transcription)")
665
+
666
+ if missing_deps:
667
+ print("πŸ“‹ Optional dependencies not found:")
668
+ for dep in missing_deps:
669
+ print(f" - {dep}")
670
+ print("The app will work with reduced functionality.")
671
+
672
+ if not bedrock_client:
673
+ print("ℹ️ AWS credentials not configured - using demo mode for analysis.")
674
+ print(" Configure AWS_ACCESS_KEY and AWS_SECRET_KEY for enhanced AI analysis.")
675
+
676
+ print("πŸš€ Starting CASL Analysis Tool...")
677
+
678
+ # Create and launch the app
679
+ app = create_interface()
680
+ app.launch(
681
+ show_api=False,
682
+ server_name="0.0.0.0",
683
+ server_port=7860
684
+ )
moderate_casl_app.py ADDED
@@ -0,0 +1,838 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import boto3
3
+ import json
4
+ import re
5
+ import logging
6
+ import os
7
+ import tempfile
8
+ import shutil
9
+ import time
10
+ import uuid
11
+ from datetime import datetime
12
+
13
+ # Configure logging
14
+ logging.basicConfig(level=logging.INFO)
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Try to import ReportLab (needed for PDF generation)
18
+ try:
19
+ from reportlab.lib.pagesizes import letter
20
+ from reportlab.lib import colors
21
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
22
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
23
+ REPORTLAB_AVAILABLE = True
24
+ except ImportError:
25
+ logger.warning("ReportLab library not available - PDF export will be disabled")
26
+ REPORTLAB_AVAILABLE = False
27
+
28
+ # Try to import speech recognition for local audio processing
29
+ try:
30
+ import speech_recognition as sr
31
+ import pydub
32
+ SPEECH_RECOGNITION_AVAILABLE = True
33
+ except ImportError:
34
+ SPEECH_RECOGNITION_AVAILABLE = False
35
+
36
+ # AWS credentials for Bedrock API and S3
37
+ AWS_ACCESS_KEY = os.getenv("AWS_ACCESS_KEY", "")
38
+ AWS_SECRET_KEY = os.getenv("AWS_SECRET_KEY", "")
39
+ AWS_REGION = os.getenv("AWS_REGION", "us-east-1")
40
+ S3_BUCKET = os.getenv("S3_BUCKET", "casl-audio-uploads")
41
+
42
+ # Initialize AWS clients if credentials are available
43
+ bedrock_client = None
44
+ s3_client = None
45
+
46
+ if AWS_ACCESS_KEY and AWS_SECRET_KEY:
47
+ try:
48
+ # Initialize Bedrock client for AI analysis
49
+ bedrock_client = boto3.client(
50
+ 'bedrock-runtime',
51
+ aws_access_key_id=AWS_ACCESS_KEY,
52
+ aws_secret_access_key=AWS_SECRET_KEY,
53
+ region_name=AWS_REGION
54
+ )
55
+
56
+ # Initialize S3 client for audio file storage
57
+ s3_client = boto3.client(
58
+ 's3',
59
+ aws_access_key_id=AWS_ACCESS_KEY,
60
+ aws_secret_access_key=AWS_SECRET_KEY,
61
+ region_name=AWS_REGION
62
+ )
63
+
64
+ logger.info("AWS clients initialized successfully")
65
+ except Exception as e:
66
+ logger.error(f"Failed to initialize AWS clients: {str(e)}")
67
+
68
+ # Create data directories if they don't exist
69
+ DATA_DIR = os.environ.get("DATA_DIR", "patient_data")
70
+ DOWNLOADS_DIR = os.path.join(DATA_DIR, "downloads")
71
+ AUDIO_DIR = os.path.join(DATA_DIR, "audio")
72
+
73
+ def ensure_data_dirs():
74
+ """Ensure data directories exist"""
75
+ global DOWNLOADS_DIR, AUDIO_DIR
76
+ try:
77
+ os.makedirs(DATA_DIR, exist_ok=True)
78
+ os.makedirs(DOWNLOADS_DIR, exist_ok=True)
79
+ os.makedirs(AUDIO_DIR, exist_ok=True)
80
+ logger.info(f"Data directories created: {DATA_DIR}, {DOWNLOADS_DIR}, {AUDIO_DIR}")
81
+ except Exception as e:
82
+ logger.warning(f"Could not create data directories: {str(e)}")
83
+ # Fallback to tmp directory on HF Spaces
84
+ DOWNLOADS_DIR = os.path.join(tempfile.gettempdir(), "casl_downloads")
85
+ AUDIO_DIR = os.path.join(tempfile.gettempdir(), "casl_audio")
86
+ os.makedirs(DOWNLOADS_DIR, exist_ok=True)
87
+ os.makedirs(AUDIO_DIR, exist_ok=True)
88
+ logger.info(f"Using fallback directories: {DOWNLOADS_DIR}, {AUDIO_DIR}")
89
+
90
+ # Initialize data directories
91
+ ensure_data_dirs()
92
+
93
+ # Sample transcript for the demo
94
+ SAMPLE_TRANSCRIPT = """*PAR: today I would &-um like to talk about &-um a fun trip I took last &-um summer with my family.
95
+ *PAR: we went to the &-um &-um beach [//] no to the mountains [//] I mean the beach actually.
96
+ *PAR: there was lots of &-um &-um swimming and &-um sun.
97
+ *PAR: we [/] we stayed for &-um three no [//] four days in a &-um hotel near the water [: ocean] [*].
98
+ *PAR: my favorite part was &-um building &-um castles with sand.
99
+ *PAR: sometimes I forget [//] forgetted [: forgot] [*] what they call those things we built.
100
+ *PAR: my brother he [//] he helped me dig a big hole.
101
+ *PAR: we saw [/] saw fishies [: fish] [*] swimming in the water.
102
+ *PAR: sometimes I wonder [/] wonder where fishies [: fish] [*] go when it's cold.
103
+ *PAR: maybe they have [/] have houses under the water.
104
+ *PAR: after swimming we [//] I eat [: ate] [*] &-um ice cream with &-um chocolate things on top.
105
+ *PAR: what do you call those &-um &-um sprinkles! that's the word.
106
+ *PAR: my mom said to &-um that I could have &-um two scoops next time.
107
+ *PAR: I want to go back to the beach [/] beach next year."""
108
+
109
+ def read_cha_file(file_path):
110
+ """Read and parse a .cha transcript file"""
111
+ try:
112
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
113
+ content = f.read()
114
+
115
+ # Extract participant lines (starting with *PAR:)
116
+ par_lines = []
117
+ for line in content.splitlines():
118
+ if line.startswith('*PAR:'):
119
+ par_lines.append(line)
120
+
121
+ # If no PAR lines found, just return the whole content
122
+ if not par_lines:
123
+ return content
124
+
125
+ return '\n'.join(par_lines)
126
+
127
+ except Exception as e:
128
+ logger.error(f"Error reading CHA file: {str(e)}")
129
+ return ""
130
+
131
+ def process_upload(file):
132
+ """Process an uploaded file (PDF, text, or CHA)"""
133
+ if file is None:
134
+ return ""
135
+
136
+ file_path = file.name
137
+ if file_path.endswith('.pdf'):
138
+ # For PDF, we would need PyPDF2 or similar
139
+ return "PDF upload not supported in this simple version"
140
+ elif file_path.endswith('.cha'):
141
+ return read_cha_file(file_path)
142
+ else:
143
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
144
+ return f.read()
145
+
146
+ def call_bedrock(prompt, max_tokens=4096):
147
+ """Call the AWS Bedrock API to analyze text using Claude"""
148
+ if not bedrock_client:
149
+ return "AWS credentials not configured. Using demo response instead."
150
+
151
+ try:
152
+ body = json.dumps({
153
+ "anthropic_version": "bedrock-2023-05-31",
154
+ "max_tokens": max_tokens,
155
+ "messages": [
156
+ {
157
+ "role": "user",
158
+ "content": prompt
159
+ }
160
+ ],
161
+ "temperature": 0.3,
162
+ "top_p": 0.9
163
+ })
164
+
165
+ modelId = 'anthropic.claude-3-sonnet-20240229-v1:0'
166
+ response = bedrock_client.invoke_model(
167
+ body=body,
168
+ modelId=modelId,
169
+ accept='application/json',
170
+ contentType='application/json'
171
+ )
172
+ response_body = json.loads(response.get('body').read())
173
+ return response_body['content'][0]['text']
174
+ except Exception as e:
175
+ logger.error(f"Error in call_bedrock: {str(e)}")
176
+ return f"Error: {str(e)}"
177
+
178
+ def upload_to_s3(file_path, file_name):
179
+ """Upload a file to S3 bucket"""
180
+ if not s3_client:
181
+ logger.warning("S3 client not available")
182
+ return None
183
+
184
+ try:
185
+ s3_key = f"audio/{datetime.now().strftime('%Y-%m-%d')}/{file_name}"
186
+ s3_client.upload_file(file_path, S3_BUCKET, s3_key)
187
+
188
+ # Generate a presigned URL that expires in 1 hour
189
+ url = s3_client.generate_presigned_url(
190
+ 'get_object',
191
+ Params={'Bucket': S3_BUCKET, 'Key': s3_key},
192
+ ExpiresIn=3600
193
+ )
194
+
195
+ logger.info(f"File uploaded to S3: {s3_key}")
196
+ return {'s3_key': s3_key, 'url': url}
197
+ except Exception as e:
198
+ logger.error(f"Error uploading to S3: {str(e)}")
199
+ return None
200
+
201
+ def save_audio_file(audio_path, patient_name="", record_id=""):
202
+ """Save audio file locally and optionally to S3"""
203
+ try:
204
+ # Generate unique filename
205
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
206
+ filename_base = f"{patient_name}_{record_id}_{timestamp}" if patient_name and record_id else f"audio_{timestamp}"
207
+
208
+ # Get file extension
209
+ file_ext = os.path.splitext(audio_path)[1] or '.wav'
210
+ final_filename = f"{filename_base}{file_ext}"
211
+
212
+ # Save locally
213
+ local_path = os.path.join(AUDIO_DIR, final_filename)
214
+ shutil.copy2(audio_path, local_path)
215
+
216
+ # Upload to S3 if available
217
+ s3_info = None
218
+ if s3_client:
219
+ s3_info = upload_to_s3(local_path, final_filename)
220
+
221
+ return {
222
+ 'local_path': local_path,
223
+ 'filename': final_filename,
224
+ 's3_info': s3_info
225
+ }
226
+ except Exception as e:
227
+ logger.error(f"Error saving audio file: {str(e)}")
228
+ return None
229
+
230
+ def transcribe_audio_local(audio_path, patient_name="", record_id=""):
231
+ """Local audio transcription using speech_recognition library with S3 storage"""
232
+ if not SPEECH_RECOGNITION_AVAILABLE:
233
+ return generate_demo_transcription()
234
+
235
+ try:
236
+ # Save the audio file (locally and to S3)
237
+ saved_file_info = save_audio_file(audio_path, patient_name, record_id)
238
+ if saved_file_info:
239
+ logger.info(f"Audio saved: {saved_file_info['filename']}")
240
+ if saved_file_info['s3_info']:
241
+ logger.info(f"Audio uploaded to S3: {saved_file_info['s3_info']['s3_key']}")
242
+
243
+ r = sr.Recognizer()
244
+
245
+ # Convert audio to WAV if needed
246
+ if not audio_path.endswith('.wav'):
247
+ try:
248
+ audio = pydub.AudioSegment.from_file(audio_path)
249
+ wav_path = audio_path.rsplit('.', 1)[0] + '.wav'
250
+ audio.export(wav_path, format="wav")
251
+ audio_path = wav_path
252
+ except Exception as e:
253
+ logger.error(f"Error converting audio: {str(e)}")
254
+ return f"Error: Could not process audio file. {str(e)}"
255
+
256
+ # Transcribe audio
257
+ with sr.AudioFile(audio_path) as source:
258
+ audio_data = r.record(source)
259
+ try:
260
+ text = r.recognize_google(audio_data)
261
+ return format_transcription_as_chat(text)
262
+ except sr.UnknownValueError:
263
+ return "Error: Could not understand audio"
264
+ except sr.RequestError as e:
265
+ return f"Error: Could not request results; {e}"
266
+
267
+ except Exception as e:
268
+ logger.error(f"Error in local transcription: {str(e)}")
269
+ return generate_demo_transcription()
270
+
271
+ def format_transcription_as_chat(text):
272
+ """Format transcribed text into CHAT format"""
273
+ # Split text into sentences and format as participant speech
274
+ sentences = re.split(r'[.!?]+', text)
275
+ chat_lines = []
276
+
277
+ for sentence in sentences:
278
+ sentence = sentence.strip()
279
+ if sentence:
280
+ chat_lines.append(f"*PAR: {sentence}.")
281
+
282
+ return '\n'.join(chat_lines)
283
+
284
+ def generate_demo_transcription():
285
+ """Generate a simulated transcription response"""
286
+ return """*PAR: today I want to tell you about my favorite toy.
287
+ *PAR: it's a &-um teddy bear that I got for my birthday.
288
+ *PAR: he has &-um brown fur and a red bow.
289
+ *PAR: I like to sleep with him every night.
290
+ *PAR: sometimes I take him to school in my backpack.
291
+ *INV: what's your teddy bear's name?
292
+ *PAR: his name is &-um Brownie because he's brown."""
293
+
294
+ def generate_demo_response(prompt):
295
+ """Generate a response using Bedrock if available, otherwise return a demo response"""
296
+ # This function will attempt to call Bedrock, and only fall back to the demo response
297
+ # if Bedrock is not available or fails
298
+
299
+ # Try to call Bedrock first if client is available
300
+ if bedrock_client:
301
+ try:
302
+ return call_bedrock(prompt)
303
+ except Exception as e:
304
+ logger.error(f"Error calling Bedrock: {str(e)}")
305
+ logger.info("Falling back to demo response")
306
+ # Continue to fallback response if Bedrock call fails
307
+
308
+ # Fallback demo response
309
+ logger.warning("Using demo response - Bedrock client not available or call failed")
310
+ return """<SPEECH_FACTORS_START>
311
+ Difficulty producing fluent speech: 8, 65
312
+ Examples:
313
+ - "today I would &-um like to talk about &-um a fun trip I took last &-um summer with my family"
314
+ - "we went to the &-um &-um beach [//] no to the mountains [//] I mean the beach actually"
315
+
316
+ Word retrieval issues: 6, 72
317
+ Examples:
318
+ - "what do you call those &-um &-um sprinkles! that's the word"
319
+ - "sometimes I forget [//] forgetted [: forgot] [*] what they call those things we built"
320
+
321
+ Grammatical errors: 4, 58
322
+ Examples:
323
+ - "after swimming we [//] I eat [: ate] [*] &-um ice cream"
324
+ - "sometimes I forget [//] forgetted [: forgot] [*] what they call those things we built"
325
+
326
+ Repetitions and revisions: 5, 62
327
+ Examples:
328
+ - "we [/] we stayed for &-um three no [//] four days"
329
+ - "we went to the &-um &-um beach [//] no to the mountains [//] I mean the beach actually"
330
+ <SPEECH_FACTORS_END>
331
+
332
+ <CASL_SKILLS_START>
333
+ Lexical/Semantic Skills: Standard Score (92), Percentile Rank (30%), Average Performance
334
+ Examples:
335
+ - "what do you call those &-um &-um sprinkles! that's the word"
336
+ - "we went to the &-um &-um beach [//] no to the mountains [//] I mean the beach actually"
337
+
338
+ Syntactic Skills: Standard Score (87), Percentile Rank (19%), Low Average Performance
339
+ Examples:
340
+ - "my brother he [//] he helped me dig a big hole"
341
+ - "after swimming we [//] I eat [: ate] [*] &-um ice cream with &-um chocolate things on top"
342
+
343
+ Supralinguistic Skills: Standard Score (90), Percentile Rank (25%), Average Performance
344
+ Examples:
345
+ - "sometimes I wonder [/] wonder where fishies [: fish] [*] go when it's cold"
346
+ - "maybe they have [/] have houses under the water"
347
+ <CASL_SKILLS_END>
348
+
349
+ <TREATMENT_RECOMMENDATIONS_START>
350
+ - Implement word-finding strategies with semantic cuing focused on everyday objects and activities, using the patient's beach experience as a context (e.g., "sprinkles," "castles")
351
+ - Practice structured narrative tasks with visual supports to reduce revisions and improve sequencing
352
+ - Use sentence formulation exercises focusing on verb tense consistency (addressing errors like "forgetted" and "eat" for "ate")
353
+ - Incorporate self-monitoring techniques to help identify and correct grammatical errors
354
+ - Work on increasing vocabulary specificity (e.g., "things on top" to "sprinkles")
355
+ <TREATMENT_RECOMMENDATIONS_END>
356
+
357
+ <EXPLANATION_START>
358
+ This child demonstrates moderate word-finding difficulties with compensatory strategies including fillers ("&-um") and repetitions. The frequent use of self-corrections shows good metalinguistic awareness, but the pauses and repairs impact conversational fluency. Syntactic errors primarily involve verb tense inconsistency. Overall, the pattern suggests a mild-to-moderate language disorder with stronger receptive than expressive skills.
359
+ <EXPLANATION_END>
360
+
361
+ <ADDITIONAL_ANALYSIS_START>
362
+ The child shows relative strengths in maintaining topic coherence and conveying a complete narrative structure despite the language challenges. The pattern of errors suggests that word-finding difficulties and processing speed are primary concerns rather than conceptual or cognitive issues. Semantic network activities that strengthen word associations would likely be beneficial, particularly when paired with visual supports.
363
+ <ADDITIONAL_ANALYSIS_END>
364
+
365
+ <DIAGNOSTIC_IMPRESSIONS_START>
366
+ Based on the language sample, this child presents with a profile consistent with a mild-to-moderate expressive language disorder. The most prominent features include:
367
+
368
+ 1. Word-finding difficulties characterized by fillers, pauses, and self-corrections when attempting to retrieve specific vocabulary
369
+ 2. Grammatical challenges primarily affecting verb tense consistency and morphological markers
370
+ 3. Relatively intact narrative structure and topic maintenance
371
+
372
+ These findings suggest intervention should focus on word retrieval strategies, grammatical form practice, and continued support for narrative development, with an emphasis on fluency and self-monitoring.
373
+ <DIAGNOSTIC_IMPRESSIONS_END>
374
+
375
+ <ERROR_EXAMPLES_START>
376
+ Word-finding difficulties:
377
+ - "what do you call those &-um &-um sprinkles! that's the word"
378
+ - "we went to the &-um &-um beach [//] no to the mountains [//] I mean the beach actually"
379
+ - "there was lots of &-um &-um swimming and &-um sun"
380
+
381
+ Grammatical errors:
382
+ - "after swimming we [//] I eat [: ate] [*] &-um ice cream"
383
+ - "sometimes I forget [//] forgetted [: forgot] [*] what they call those things we built"
384
+ - "we saw [/] saw fishies [: fish] [*] swimming in the water"
385
+
386
+ Repetitions and revisions:
387
+ - "we [/] we stayed for &-um three no [//] four days"
388
+ - "I want to go back to the beach [/] beach next year"
389
+ - "sometimes I wonder [/] wonder where fishies [: fish] [*] go when it's cold"
390
+ <ERROR_EXAMPLES_END>"""
391
+
392
+ def parse_casl_response(response):
393
+ """Parse the LLM response for CASL analysis into structured data"""
394
+ # Extract speech factors section using section markers
395
+ speech_factors_section = ""
396
+ factors_pattern = re.compile(r"<SPEECH_FACTORS_START>(.*?)<SPEECH_FACTORS_END>", re.DOTALL)
397
+ factors_match = factors_pattern.search(response)
398
+
399
+ if factors_match:
400
+ speech_factors_section = factors_match.group(1).strip()
401
+ else:
402
+ speech_factors_section = "Error extracting speech factors from analysis."
403
+
404
+ # Extract CASL skills section
405
+ casl_section = ""
406
+ casl_pattern = re.compile(r"<CASL_SKILLS_START>(.*?)<CASL_SKILLS_END>", re.DOTALL)
407
+ casl_match = casl_pattern.search(response)
408
+
409
+ if casl_match:
410
+ casl_section = casl_match.group(1).strip()
411
+ else:
412
+ casl_section = "Error extracting CASL skills from analysis."
413
+
414
+ # Extract treatment recommendations
415
+ treatment_text = ""
416
+ treatment_pattern = re.compile(r"<TREATMENT_RECOMMENDATIONS_START>(.*?)<TREATMENT_RECOMMENDATIONS_END>", re.DOTALL)
417
+ treatment_match = treatment_pattern.search(response)
418
+
419
+ if treatment_match:
420
+ treatment_text = treatment_match.group(1).strip()
421
+ else:
422
+ treatment_text = "Error extracting treatment recommendations from analysis."
423
+
424
+ # Extract explanation section
425
+ explanation_text = ""
426
+ explanation_pattern = re.compile(r"<EXPLANATION_START>(.*?)<EXPLANATION_END>", re.DOTALL)
427
+ explanation_match = explanation_pattern.search(response)
428
+
429
+ if explanation_match:
430
+ explanation_text = explanation_match.group(1).strip()
431
+ else:
432
+ explanation_text = "Error extracting clinical explanation from analysis."
433
+
434
+ # Extract additional analysis
435
+ additional_analysis = ""
436
+ additional_pattern = re.compile(r"<ADDITIONAL_ANALYSIS_START>(.*?)<ADDITIONAL_ANALYSIS_END>", re.DOTALL)
437
+ additional_match = additional_pattern.search(response)
438
+
439
+ if additional_match:
440
+ additional_analysis = additional_match.group(1).strip()
441
+
442
+ # Extract diagnostic impressions
443
+ diagnostic_impressions = ""
444
+ diagnostic_pattern = re.compile(r"<DIAGNOSTIC_IMPRESSIONS_START>(.*?)<DIAGNOSTIC_IMPRESSIONS_END>", re.DOTALL)
445
+ diagnostic_match = diagnostic_pattern.search(response)
446
+
447
+ if diagnostic_match:
448
+ diagnostic_impressions = diagnostic_match.group(1).strip()
449
+
450
+ # Extract specific error examples
451
+ specific_errors_text = ""
452
+ errors_pattern = re.compile(r"<ERROR_EXAMPLES_START>(.*?)<ERROR_EXAMPLES_END>", re.DOTALL)
453
+ errors_match = errors_pattern.search(response)
454
+
455
+ if errors_match:
456
+ specific_errors_text = errors_match.group(1).strip()
457
+
458
+ # Create full report text
459
+ full_report = f"""
460
+ ## Speech Factors Analysis
461
+
462
+ {speech_factors_section}
463
+
464
+ ## CASL Skills Assessment
465
+
466
+ {casl_section}
467
+
468
+ ## Treatment Recommendations
469
+
470
+ {treatment_text}
471
+
472
+ ## Clinical Explanation
473
+
474
+ {explanation_text}
475
+ """
476
+
477
+ if additional_analysis:
478
+ full_report += f"\n## Additional Analysis\n\n{additional_analysis}"
479
+
480
+ if diagnostic_impressions:
481
+ full_report += f"\n## Diagnostic Impressions\n\n{diagnostic_impressions}"
482
+
483
+ if specific_errors_text:
484
+ full_report += f"\n## Detailed Error Examples\n\n{specific_errors_text}"
485
+
486
+ return {
487
+ 'speech_factors': speech_factors_section,
488
+ 'casl_data': casl_section,
489
+ 'treatment_suggestions': treatment_text,
490
+ 'explanation': explanation_text,
491
+ 'additional_analysis': additional_analysis,
492
+ 'diagnostic_impressions': diagnostic_impressions,
493
+ 'specific_errors': specific_errors_text,
494
+ 'full_report': full_report,
495
+ 'raw_response': response
496
+ }
497
+
498
+ def analyze_transcript(transcript, age, gender):
499
+ """Analyze a speech transcript using Claude"""
500
+ # CASL-2 assessment cheat sheet
501
+ cheat_sheet = """
502
+ # Speech-Language Pathologist Analysis Cheat Sheet
503
+
504
+ ## Types of Speech Patterns to Identify:
505
+
506
+ 1. Difficulty producing fluent, grammatical speech
507
+ - Fillers (um, uh) and pauses
508
+ - False starts and revisions
509
+ - Incomplete sentences
510
+
511
+ 2. Word retrieval issues
512
+ - Pauses before content words
513
+ - Circumlocutions (talking around a word)
514
+ - Word substitutions
515
+
516
+ 3. Grammatical errors
517
+ - Verb tense inconsistencies
518
+ - Subject-verb agreement errors
519
+ - Morphological errors (plurals, possessives)
520
+
521
+ 4. Repetitions and revisions
522
+ - Word or phrase repetitions [/]
523
+ - Self-corrections [//]
524
+ - Retracing
525
+
526
+ 5. Neologisms
527
+ - Made-up words
528
+ - Word blends
529
+
530
+ 6. Perseveration
531
+ - Inappropriate repetition of ideas
532
+ - Recurring themes
533
+
534
+ 7. Comprehension issues
535
+ - Topic maintenance difficulties
536
+ - Non-sequiturs
537
+ - Inappropriate responses
538
+ """
539
+
540
+ # Instructions for the analysis
541
+ instructions = """
542
+ Analyze this speech transcript to identify specific patterns and provide a detailed CASL-2 (Comprehensive Assessment of Spoken Language) assessment.
543
+
544
+ For each speech pattern you identify:
545
+ 1. Count the occurrences in the transcript
546
+ 2. Estimate a percentile (how typical/atypical this is for the age)
547
+ 3. Provide DIRECT QUOTES from the transcript as evidence
548
+
549
+ Then assess the following CASL-2 domains:
550
+
551
+ 1. Lexical/Semantic Skills:
552
+ - Assess vocabulary diversity, word-finding abilities, semantic precision
553
+ - Provide Standard Score (mean=100, SD=15), percentile rank, and performance level
554
+ - Include SPECIFIC QUOTES as evidence
555
+
556
+ 2. Syntactic Skills:
557
+ - Evaluate grammatical accuracy, sentence complexity, morphological skills
558
+ - Provide Standard Score, percentile rank, and performance level
559
+ - Include SPECIFIC QUOTES as evidence
560
+
561
+ 3. Supralinguistic Skills:
562
+ - Assess figurative language use, inferencing, and abstract reasoning
563
+ - Provide Standard Score, percentile rank, and performance level
564
+ - Include SPECIFIC QUOTES as evidence
565
+
566
+ YOUR RESPONSE MUST USE THESE EXACT SECTION MARKERS FOR PARSING:
567
+
568
+ <SPEECH_FACTORS_START>
569
+ Difficulty producing fluent, grammatical speech: (occurrences), (percentile)
570
+ Examples:
571
+ - "(direct quote from transcript)"
572
+ - "(direct quote from transcript)"
573
+
574
+ Word retrieval issues: (occurrences), (percentile)
575
+ Examples:
576
+ - "(direct quote from transcript)"
577
+ - "(direct quote from transcript)"
578
+
579
+ (And so on for each factor)
580
+ <SPEECH_FACTORS_END>
581
+
582
+ <CASL_SKILLS_START>
583
+ Lexical/Semantic Skills: Standard Score (X), Percentile Rank (X%), Performance Level
584
+ Examples:
585
+ - "(direct quote showing strength or weakness)"
586
+ - "(direct quote showing strength or weakness)"
587
+
588
+ Syntactic Skills: Standard Score (X), Percentile Rank (X%), Performance Level
589
+ Examples:
590
+ - "(direct quote showing strength or weakness)"
591
+ - "(direct quote showing strength or weakness)"
592
+
593
+ Supralinguistic Skills: Standard Score (X), Percentile Rank (X%), Performance Level
594
+ Examples:
595
+ - "(direct quote showing strength or weakness)"
596
+ - "(direct quote showing strength or weakness)"
597
+ <CASL_SKILLS_END>
598
+
599
+ <TREATMENT_RECOMMENDATIONS_START>
600
+ - (treatment recommendation)
601
+ - (treatment recommendation)
602
+ - (treatment recommendation)
603
+ <TREATMENT_RECOMMENDATIONS_END>
604
+
605
+ <EXPLANATION_START>
606
+ (brief diagnostic rationale based on findings)
607
+ <EXPLANATION_END>
608
+
609
+ <ADDITIONAL_ANALYSIS_START>
610
+ (specific insights that would be helpful for treatment planning)
611
+ <ADDITIONAL_ANALYSIS_END>
612
+
613
+ <DIAGNOSTIC_IMPRESSIONS_START>
614
+ (summarize findings across domains using specific examples and clear explanations)
615
+ <DIAGNOSTIC_IMPRESSIONS_END>
616
+
617
+ <ERROR_EXAMPLES_START>
618
+ (Copy all the specific quote examples here again, organized by error type or skill domain)
619
+ <ERROR_EXAMPLES_END>
620
+
621
+ MOST IMPORTANT:
622
+ 1. Use EXACTLY the section markers provided (like <SPEECH_FACTORS_START>) to make parsing reliable
623
+ 2. For EVERY factor and domain you analyze, you MUST provide direct quotes from the transcript as evidence
624
+ 3. Be very specific and cite the exact text
625
+ 4. Do not omit any of the required sections
626
+ """
627
+
628
+ # Prepare prompt for Claude with the user's role context
629
+ role_context = """
630
+ You are a speech pathologist, a healthcare professional who specializes in evaluating, diagnosing, and treating communication disorders, including speech, language, cognitive-communication, voice, swallowing, and fluency disorders. Your role is to help patients improve their speech and communication skills through various therapeutic techniques and exercises.
631
+
632
+ You are working with a student with speech impediments.
633
+
634
+ The most important thing is that you stay kind to the child. Be constructive and helpful rather than critical.
635
+ """
636
+
637
+ prompt = f"""
638
+ {role_context}
639
+
640
+ You are analyzing a transcript for a patient who is {age} years old and {gender}.
641
+
642
+ TRANSCRIPT:
643
+ {transcript}
644
+
645
+ {cheat_sheet}
646
+
647
+ {instructions}
648
+
649
+ Remember to be precise but compassionate in your analysis. Use direct quotes from the transcript for every factor and domain you analyze.
650
+ """
651
+
652
+ # Call the appropriate API or fallback to demo mode
653
+ response = generate_demo_response(prompt)
654
+
655
+ # Parse the response
656
+ results = parse_casl_response(response)
657
+
658
+ return results
659
+
660
+ def create_interface():
661
+ """Create the Gradio interface"""
662
+ # Set a theme compatible with Hugging Face Spaces
663
+ theme = gr.themes.Soft(
664
+ primary_hue="blue",
665
+ secondary_hue="indigo",
666
+ )
667
+
668
+ with gr.Blocks(title="Simple CASL Analysis Tool", theme=theme) as app:
669
+ gr.Markdown("# CASL Analysis Tool")
670
+ gr.Markdown("A simplified tool for analyzing speech transcripts and audio using CASL framework")
671
+
672
+ with gr.Tabs() as main_tabs:
673
+ # Analysis Tab
674
+ with gr.TabItem("Analysis", id=0):
675
+ with gr.Row():
676
+ with gr.Column(scale=1):
677
+ # Patient info
678
+ gr.Markdown("### Patient Information")
679
+ patient_name = gr.Textbox(label="Patient Name", placeholder="Enter patient name")
680
+ record_id = gr.Textbox(label="Record ID", placeholder="Enter record ID")
681
+
682
+ with gr.Row():
683
+ age = gr.Number(label="Age", value=8, minimum=1, maximum=120)
684
+ gender = gr.Radio(["male", "female", "other"], label="Gender", value="male")
685
+
686
+ assessment_date = gr.Textbox(
687
+ label="Assessment Date",
688
+ placeholder="MM/DD/YYYY",
689
+ value=datetime.now().strftime('%m/%d/%Y')
690
+ )
691
+ clinician_name = gr.Textbox(label="Clinician", placeholder="Enter clinician name")
692
+
693
+ # Transcript input
694
+ gr.Markdown("### Transcript")
695
+ sample_btn = gr.Button("Load Sample Transcript")
696
+ file_upload = gr.File(label="Upload transcript file (.txt or .cha)")
697
+ transcript = gr.Textbox(
698
+ label="Speech transcript (CHAT format preferred)",
699
+ placeholder="Enter transcript text or upload a file...",
700
+ lines=10
701
+ )
702
+
703
+ # Analysis button
704
+ analyze_btn = gr.Button("Analyze Transcript", variant="primary")
705
+
706
+ with gr.Column(scale=1):
707
+ # Results display
708
+ gr.Markdown("### Analysis Results")
709
+
710
+ analysis_output = gr.Markdown(label="Full Analysis")
711
+
712
+ # PDF export (only shown if ReportLab is available)
713
+ export_status = gr.Markdown("")
714
+ if REPORTLAB_AVAILABLE:
715
+ export_btn = gr.Button("Export as PDF", variant="secondary")
716
+ else:
717
+ gr.Markdown("⚠️ PDF export is disabled - ReportLab library is not installed")
718
+
719
+ # Transcription Tab
720
+ with gr.TabItem("Transcription", id=1):
721
+ with gr.Row():
722
+ with gr.Column(scale=1):
723
+ gr.Markdown("### Audio Transcription")
724
+ gr.Markdown("Upload an audio recording to automatically transcribe it in CHAT format")
725
+
726
+ # Patient's age helps with transcription accuracy
727
+ transcription_age = gr.Number(label="Patient Age", value=8, minimum=1, maximum=120,
728
+ info="For children under 10, special language models may be used")
729
+
730
+ # Audio input - FIXED: removed format parameter
731
+ audio_input = gr.Audio(type="filepath", label="Upload Audio Recording")
732
+
733
+ # Transcribe button
734
+ transcribe_btn = gr.Button("Transcribe Audio", variant="primary")
735
+
736
+ with gr.Column(scale=1):
737
+ # Transcription output
738
+ transcription_output = gr.Textbox(
739
+ label="Transcription Result",
740
+ placeholder="Transcription will appear here...",
741
+ lines=12
742
+ )
743
+
744
+ with gr.Row():
745
+ # Button to use transcription in analysis
746
+ copy_to_analysis_btn = gr.Button("Use for Analysis", variant="secondary")
747
+
748
+ # Status/info message
749
+ transcription_status = gr.Markdown("")
750
+
751
+ # Load sample transcript button
752
+ def load_sample():
753
+ return SAMPLE_TRANSCRIPT
754
+
755
+ sample_btn.click(load_sample, outputs=[transcript])
756
+
757
+ # File upload handler
758
+ file_upload.upload(process_upload, file_upload, transcript)
759
+
760
+ # Analysis button handler
761
+ def on_analyze_click(transcript_text, age_val, gender_val, patient_name_val, record_id_val, clinician_val, assessment_date_val):
762
+ if not transcript_text or len(transcript_text.strip()) < 50:
763
+ return "Error: Please provide a longer transcript for analysis."
764
+
765
+ try:
766
+ # Get the analysis results
767
+ results = analyze_transcript(transcript_text, age_val, gender_val)
768
+
769
+ # Return the full report
770
+ return results['full_report']
771
+
772
+ except Exception as e:
773
+ logger.exception("Error during analysis")
774
+ return f"Error during analysis: {str(e)}"
775
+
776
+ analyze_btn.click(
777
+ on_analyze_click,
778
+ inputs=[
779
+ transcript, age, gender,
780
+ patient_name, record_id, clinician_name, assessment_date
781
+ ],
782
+ outputs=[analysis_output]
783
+ )
784
+
785
+ # Transcription button handler
786
+ def on_transcribe_audio(audio_path, age_val, patient_name_val, record_id_val):
787
+ try:
788
+ if not audio_path:
789
+ return "Please upload an audio file to transcribe.", "Error: No audio file provided."
790
+
791
+ # Process the audio file with local transcription and S3 upload
792
+ transcription = transcribe_audio_local(audio_path, patient_name_val, record_id_val)
793
+
794
+ # Return status message based on whether it's a demo or real transcription
795
+ if not SPEECH_RECOGNITION_AVAILABLE:
796
+ status_msg = "⚠️ Demo mode: Using example transcription (speech_recognition not installed)"
797
+ else:
798
+ s3_status = " and uploaded to S3" if s3_client else ""
799
+ status_msg = f"βœ… Transcription completed successfully{s3_status}"
800
+
801
+ return transcription, status_msg
802
+ except Exception as e:
803
+ logger.exception("Error transcribing audio")
804
+ return f"Error: {str(e)}", f"❌ Transcription failed: {str(e)}"
805
+
806
+ # Connect the transcribe button to its handler
807
+ transcribe_btn.click(
808
+ on_transcribe_audio,
809
+ inputs=[audio_input, transcription_age, patient_name, record_id],
810
+ outputs=[transcription_output, transcription_status]
811
+ )
812
+
813
+ # Copy transcription to analysis tab
814
+ def copy_to_analysis(transcription):
815
+ return transcription, gr.update(selected=0) # Switch to Analysis tab
816
+
817
+ copy_to_analysis_btn.click(
818
+ copy_to_analysis,
819
+ inputs=[transcription_output],
820
+ outputs=[transcript, main_tabs]
821
+ )
822
+
823
+ return app
824
+
825
+ if __name__ == "__main__":
826
+ # Check for AWS credentials
827
+ if not AWS_ACCESS_KEY or not AWS_SECRET_KEY:
828
+ print("NOTE: AWS credentials not found. The app will run in demo mode with simulated responses.")
829
+ print("To enable full functionality, set AWS_ACCESS_KEY, AWS_SECRET_KEY, and optionally S3_BUCKET environment variables.")
830
+ else:
831
+ print(f"AWS clients initialized. S3 bucket: {S3_BUCKET}")
832
+ if s3_client:
833
+ print("βœ… S3 audio storage enabled")
834
+ else:
835
+ print("⚠️ S3 client not available")
836
+
837
+ app = create_interface()
838
+ app.launch(show_api=False) # Disable API tab for security
reference_files/CLEANUP_PLAN.md ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CASL Directory Cleanup Plan
2
+
3
+ ## βœ… KEEP (Deployment Ready)
4
+
5
+ ### For Simple Deployment:
6
+ - `README.md` - HuggingFace Spaces config
7
+ - `simple_casl.py` - Ultra-simple version (186 lines)
8
+ - `requirements.txt` - Dependencies
9
+
10
+ ### For Full-Featured Deployment:
11
+ - `app.py` - Complete version (683 lines)
12
+ - `simple_app_fixed.py` - Alternative moderate version
13
+
14
+ ### Reference:
15
+ - `aphasia_analysis_app_code.py` - Working Bedrock API reference
16
+
17
+ ## πŸ—‘οΈ REMOVE (Redundant/Problematic)
18
+
19
+ ### Large/Complex Files with Issues:
20
+ - `casl_analysis.py` (2493 lines) - S3 dependencies, errors
21
+ - `casl_analysis_improved.py` (1443 lines) - Compatibility issues
22
+ - `copy_of_casl_analysis.py` (1490 lines) - Duplicate
23
+ - `simple_app.py` (1207 lines) - S3 dependencies, replaced
24
+
25
+ ### Redundant Files:
26
+ - `requirements_improved.txt` - Use main requirements.txt instead
27
+
28
+ ### Auto-Generated:
29
+ - `patient_data/` directory - Will be recreated automatically
30
+
31
+ ## 🎯 FINAL DEPLOYMENT STRUCTURE
32
+
33
+ ### Option 1: Ultra-Simple
34
+ ```
35
+ /CASL/
36
+ β”œβ”€β”€ README.md (app_file: simple_casl.py)
37
+ β”œβ”€β”€ simple_casl.py
38
+ β”œβ”€β”€ requirements.txt
39
+ └── aphasia_analysis_app_code.py (reference)
40
+ ```
41
+
42
+ ### Option 2: Full-Featured
43
+ ```
44
+ /CASL/
45
+ β”œβ”€β”€ README.md (app_file: app.py)
46
+ β”œβ”€β”€ app.py
47
+ β”œβ”€β”€ requirements.txt
48
+ └── aphasia_analysis_app_code.py (reference)
49
+ ```
50
+
51
+ ## πŸ“‹ CLEANUP COMMANDS
52
+
53
+ ```bash
54
+ # Remove redundant files
55
+ rm casl_analysis.py
56
+ rm casl_analysis_improved.py
57
+ rm copy_of_casl_analysis.py
58
+ rm simple_app.py
59
+ rm requirements_improved.txt
60
+
61
+ # Remove auto-generated data
62
+ rm -rf patient_data/
63
+
64
+ # Update README.md to point to chosen app file
65
+ ```
66
+
67
+ ## πŸš€ RECOMMENDATION
68
+
69
+ **Use Option 1 (Ultra-Simple)** for reliable deployment:
70
+ - Smallest codebase (186 lines)
71
+ - Fewest dependencies
72
+ - Proven Bedrock API format
73
+ - Clean, focused functionality
reference_files/casl_analysis.py ADDED
The diff for this file is too large to render. See raw diff
 
reference_files/copy_of_casl_analysis.py ADDED
@@ -0,0 +1,1491 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import boto3
3
+ import json
4
+ import pandas as pd
5
+ import matplotlib.pyplot as plt
6
+ import numpy as np
7
+ import re
8
+ import logging
9
+ import os
10
+ import pickle
11
+ import csv
12
+ from PIL import Image
13
+ import io
14
+ from datetime import datetime
15
+ import uuid
16
+
17
+ # Configure logging
18
+ logging.basicConfig(level=logging.INFO)
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # Try to import ReportLab (needed for PDF generation)
22
+ try:
23
+ from reportlab.lib.pagesizes import letter
24
+ from reportlab.lib import colors
25
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
26
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
27
+ REPORTLAB_AVAILABLE = True
28
+ except ImportError:
29
+ logger.warning("ReportLab library not available - PDF export will be disabled")
30
+ REPORTLAB_AVAILABLE = False
31
+
32
+ # Try to import PyPDF2 (needed for PDF reading)
33
+ try:
34
+ import PyPDF2
35
+ PYPDF2_AVAILABLE = True
36
+ except ImportError:
37
+ logger.warning("PyPDF2 library not available - PDF reading will be disabled")
38
+ PYPDF2_AVAILABLE = False
39
+
40
+ # AWS credentials for Bedrock API
41
+ # For HuggingFace Spaces, set these as secrets in the Space settings
42
+ AWS_ACCESS_KEY = os.getenv("AWS_ACCESS_KEY", "")
43
+ AWS_SECRET_KEY = os.getenv("AWS_SECRET_KEY", "")
44
+ AWS_REGION = os.getenv("AWS_REGION", "us-east-1")
45
+
46
+ # Initialize AWS clients if credentials are available
47
+ bedrock_client = None
48
+ transcribe_client = None
49
+ s3_client = None
50
+
51
+ if AWS_ACCESS_KEY and AWS_SECRET_KEY:
52
+ try:
53
+ # Initialize Bedrock client for AI analysis
54
+ bedrock_client = boto3.client(
55
+ 'bedrock-runtime',
56
+ aws_access_key_id=AWS_ACCESS_KEY,
57
+ aws_secret_access_key=AWS_SECRET_KEY,
58
+ region_name=AWS_REGION
59
+ )
60
+ logger.info("Bedrock client initialized successfully")
61
+
62
+ # Initialize Transcribe client for speech-to-text
63
+ transcribe_client = boto3.client(
64
+ 'transcribe',
65
+ aws_access_key_id=AWS_ACCESS_KEY,
66
+ aws_secret_access_key=AWS_SECRET_KEY,
67
+ region_name=AWS_REGION
68
+ )
69
+ logger.info("Transcribe client initialized successfully")
70
+
71
+ # Initialize S3 client for storing audio files
72
+ s3_client = boto3.client(
73
+ 's3',
74
+ aws_access_key_id=AWS_ACCESS_KEY,
75
+ aws_secret_access_key=AWS_SECRET_KEY,
76
+ region_name=AWS_REGION
77
+ )
78
+ logger.info("S3 client initialized successfully")
79
+ except Exception as e:
80
+ logger.error(f"Failed to initialize AWS clients: {str(e)}")
81
+
82
+ # S3 bucket for storing audio files
83
+ S3_BUCKET = os.environ.get("S3_BUCKET", "casl-audio-files")
84
+ S3_PREFIX = "transcribe-audio/"
85
+
86
+ # Sample transcript for the demo
87
+ SAMPLE_TRANSCRIPT = """*PAR: today I would &-um like to talk about &-um a fun trip I took last &-um summer with my family.
88
+ *PAR: we went to the &-um &-um beach [//] no to the mountains [//] I mean the beach actually.
89
+ *PAR: there was lots of &-um &-um swimming and &-um sun.
90
+ *PAR: we [/] we stayed for &-um three no [//] four days in a &-um hotel near the water [: ocean] [*].
91
+ *PAR: my favorite part was &-um building &-um castles with sand.
92
+ *PAR: sometimes I forget [//] forgetted [: forgot] [*] what they call those things we built.
93
+ *PAR: my brother he [//] he helped me dig a big hole.
94
+ *PAR: we saw [/] saw fishies [: fish] [*] swimming in the water.
95
+ *PAR: sometimes I wonder [/] wonder where fishies [: fish] [*] go when it's cold.
96
+ *PAR: maybe they have [/] have houses under the water.
97
+ *PAR: after swimming we [//] I eat [: ate] [*] &-um ice cream with &-um chocolate things on top.
98
+ *PAR: what do you call those &-um &-um sprinkles! that's the word.
99
+ *PAR: my mom said to &-um that I could have &-um two scoops next time.
100
+ *PAR: I want to go back to the beach [/] beach next year."""
101
+
102
+ # ===============================
103
+ # Database and Storage Functions
104
+ # ===============================
105
+
106
+ # Create data directories if they don't exist
107
+ DATA_DIR = os.environ.get("DATA_DIR", "patient_data")
108
+ RECORDS_FILE = os.path.join(DATA_DIR, "patient_records.csv")
109
+ ANALYSES_DIR = os.path.join(DATA_DIR, "analyses")
110
+ DOWNLOADS_DIR = os.path.join(DATA_DIR, "downloads")
111
+ AUDIO_DIR = os.path.join(DATA_DIR, "audio")
112
+
113
+ def ensure_data_dirs():
114
+ """Ensure data directories exist"""
115
+ global DOWNLOADS_DIR, AUDIO_DIR
116
+ try:
117
+ os.makedirs(DATA_DIR, exist_ok=True)
118
+ os.makedirs(ANALYSES_DIR, exist_ok=True)
119
+ os.makedirs(DOWNLOADS_DIR, exist_ok=True)
120
+ os.makedirs(AUDIO_DIR, exist_ok=True)
121
+ logger.info(f"Data directories created: {DATA_DIR}, {ANALYSES_DIR}, {DOWNLOADS_DIR}, {AUDIO_DIR}")
122
+
123
+ # Create records file if it doesn't exist
124
+ if not os.path.exists(RECORDS_FILE):
125
+ with open(RECORDS_FILE, 'w', newline='') as f:
126
+ writer = csv.writer(f)
127
+ writer.writerow([
128
+ "ID", "Name", "Record ID", "Age", "Gender",
129
+ "Assessment Date", "Clinician", "Analysis Date", "File Path"
130
+ ])
131
+ except Exception as e:
132
+ logger.warning(f"Could not create data directories: {str(e)}")
133
+ # Fallback to tmp directory on HF Spaces
134
+ DOWNLOADS_DIR = os.path.join(os.path.expanduser("~"), "casl_downloads")
135
+ AUDIO_DIR = os.path.join(os.path.expanduser("~"), "casl_audio")
136
+ os.makedirs(DOWNLOADS_DIR, exist_ok=True)
137
+ os.makedirs(AUDIO_DIR, exist_ok=True)
138
+ logger.info(f"Using fallback directories: {DOWNLOADS_DIR}, {AUDIO_DIR}")
139
+
140
+ # Initialize data directories
141
+ ensure_data_dirs()
142
+
143
+ def save_patient_record(patient_info, analysis_results, transcript):
144
+ """Save patient record to storage"""
145
+ try:
146
+ # Generate unique ID for the record
147
+ record_id = str(uuid.uuid4())
148
+
149
+ # Extract patient information
150
+ name = patient_info.get("name", "")
151
+ patient_id = patient_info.get("record_id", "")
152
+ age = patient_info.get("age", "")
153
+ gender = patient_info.get("gender", "")
154
+ assessment_date = patient_info.get("assessment_date", "")
155
+ clinician = patient_info.get("clinician", "")
156
+
157
+ # Create filename for the analysis data
158
+ filename = f"analysis_{record_id}.pkl"
159
+ filepath = os.path.join(ANALYSES_DIR, filename)
160
+
161
+ # Save analysis data
162
+ with open(filepath, 'wb') as f:
163
+ pickle.dump({
164
+ "patient_info": patient_info,
165
+ "analysis_results": analysis_results,
166
+ "transcript": transcript,
167
+ "timestamp": datetime.now().isoformat(),
168
+ }, f)
169
+
170
+ # Add record to CSV file
171
+ with open(RECORDS_FILE, 'a', newline='') as f:
172
+ writer = csv.writer(f)
173
+ writer.writerow([
174
+ record_id, name, patient_id, age, gender,
175
+ assessment_date, clinician, datetime.now().strftime('%Y-%m-%d'),
176
+ filepath
177
+ ])
178
+
179
+ return record_id
180
+
181
+ except Exception as e:
182
+ logger.error(f"Error saving patient record: {str(e)}")
183
+ return None
184
+
185
+ def load_patient_record(record_id):
186
+ """Load patient record from storage"""
187
+ try:
188
+ # Find the record in the CSV file
189
+ if not os.path.exists(RECORDS_FILE):
190
+ logger.error(f"Records file does not exist: {RECORDS_FILE}")
191
+ return None
192
+
193
+ with open(RECORDS_FILE, 'r', newline='') as f:
194
+ reader = csv.reader(f)
195
+ next(reader) # Skip header
196
+ for row in reader:
197
+ if len(row) < 9: # Ensure row has enough elements
198
+ logger.warning(f"Skipping malformed record row: {row}")
199
+ continue
200
+
201
+ if row[0] == record_id:
202
+ file_path = row[8]
203
+
204
+ # Check if the file exists
205
+ if not os.path.exists(file_path):
206
+ logger.error(f"Analysis file not found: {file_path}")
207
+ return None
208
+
209
+ # Load and return the data
210
+ try:
211
+ with open(file_path, 'rb') as f:
212
+ return pickle.load(f)
213
+ except (pickle.PickleError, EOFError) as pickle_err:
214
+ logger.error(f"Error unpickling file {file_path}: {str(pickle_err)}")
215
+ return None
216
+
217
+ logger.warning(f"Record ID not found: {record_id}")
218
+ return None
219
+
220
+ except Exception as e:
221
+ logger.error(f"Error loading patient record: {str(e)}")
222
+ return None
223
+
224
+ def get_all_patient_records():
225
+ """Return a list of all patient records"""
226
+ try:
227
+ records = []
228
+
229
+ # Ensure data directories exist
230
+ ensure_data_dirs()
231
+
232
+ if not os.path.exists(RECORDS_FILE):
233
+ logger.warning(f"Records file does not exist, creating it: {RECORDS_FILE}")
234
+ with open(RECORDS_FILE, 'w', newline='') as f:
235
+ writer = csv.writer(f)
236
+ writer.writerow([
237
+ "ID", "Name", "Record ID", "Age", "Gender",
238
+ "Assessment Date", "Clinician", "Analysis Date", "File Path"
239
+ ])
240
+ return records
241
+
242
+ # Read existing records
243
+ valid_records = []
244
+ with open(RECORDS_FILE, 'r', newline='') as f:
245
+ reader = csv.reader(f)
246
+ next(reader) # Skip header
247
+ for row in reader:
248
+ if len(row) < 9: # Check for malformed rows
249
+ continue
250
+
251
+ # Check if the analysis file exists
252
+ file_path = row[8]
253
+ file_exists = os.path.exists(file_path)
254
+
255
+ record = {
256
+ "id": row[0],
257
+ "name": row[1],
258
+ "record_id": row[2],
259
+ "age": row[3],
260
+ "gender": row[4],
261
+ "assessment_date": row[5],
262
+ "clinician": row[6],
263
+ "analysis_date": row[7],
264
+ "file_path": file_path,
265
+ "status": "Valid" if file_exists else "Missing File"
266
+ }
267
+ records.append(record)
268
+
269
+ # Keep track of valid records for potential cleanup
270
+ if file_exists:
271
+ valid_records.append(row)
272
+
273
+ # If we found invalid records, consider rewriting the CSV with only valid entries
274
+ if len(valid_records) < len(records):
275
+ logger.warning(f"Found {len(records) - len(valid_records)} invalid records")
276
+ # Uncomment to enable automatic cleanup:
277
+ # with open(RECORDS_FILE, 'w', newline='') as f:
278
+ # writer = csv.writer(f)
279
+ # writer.writerow([
280
+ # "ID", "Name", "Record ID", "Age", "Gender",
281
+ # "Assessment Date", "Clinician", "Analysis Date", "File Path"
282
+ # ])
283
+ # for row in valid_records:
284
+ # writer.writerow(row)
285
+
286
+ return records
287
+
288
+ except Exception as e:
289
+ logger.error(f"Error getting patient records: {str(e)}")
290
+ return []
291
+
292
+ def delete_patient_record(record_id):
293
+ """Delete a patient record"""
294
+ try:
295
+ if not os.path.exists(RECORDS_FILE):
296
+ return False
297
+
298
+ # Find the record and its file
299
+ file_path = None
300
+ with open(RECORDS_FILE, 'r', newline='') as f:
301
+ reader = csv.reader(f)
302
+ rows = list(reader)
303
+ header = rows[0]
304
+
305
+ for i, row in enumerate(rows[1:], 1):
306
+ if len(row) < 9:
307
+ continue
308
+
309
+ if row[0] == record_id:
310
+ file_path = row[8]
311
+ break
312
+
313
+ if not file_path:
314
+ return False
315
+
316
+ # Delete the analysis file if it exists
317
+ if os.path.exists(file_path):
318
+ os.remove(file_path)
319
+
320
+ # Remove the record from the CSV
321
+ rows_to_keep = [row for row in rows[1:] if len(row) >= 9 and row[0] != record_id]
322
+
323
+ with open(RECORDS_FILE, 'w', newline='') as f:
324
+ writer = csv.writer(f)
325
+ writer.writerow(header)
326
+ writer.writerows(rows_to_keep)
327
+
328
+ return True
329
+
330
+ except Exception as e:
331
+ logger.error(f"Error deleting patient record: {str(e)}")
332
+ return False
333
+
334
+ # ===============================
335
+ # Utility Functions
336
+ # ===============================
337
+
338
+ def read_pdf(file_path):
339
+ """Read text from a PDF file"""
340
+ if not PYPDF2_AVAILABLE:
341
+ return "Error: PDF reading is not available - PyPDF2 library is not installed"
342
+
343
+ try:
344
+ with open(file_path, 'rb') as file:
345
+ pdf_reader = PyPDF2.PdfReader(file)
346
+ text = ""
347
+ for page in pdf_reader.pages:
348
+ text += page.extract_text()
349
+ return text
350
+ except Exception as e:
351
+ logger.error(f"Error reading PDF: {str(e)}")
352
+ return ""
353
+
354
+ def read_cha_file(file_path):
355
+ """Read and parse a .cha transcript file"""
356
+ try:
357
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
358
+ content = f.read()
359
+
360
+ # Extract participant lines (starting with *PAR:)
361
+ par_lines = []
362
+ for line in content.splitlines():
363
+ if line.startswith('*PAR:'):
364
+ par_lines.append(line)
365
+
366
+ # If no PAR lines found, just return the whole content
367
+ if not par_lines:
368
+ return content
369
+
370
+ return '\n'.join(par_lines)
371
+
372
+ except Exception as e:
373
+ logger.error(f"Error reading CHA file: {str(e)}")
374
+ return ""
375
+
376
+ def process_upload(file):
377
+ """Process an uploaded file (PDF, text, or CHA)"""
378
+ if file is None:
379
+ return ""
380
+
381
+ file_path = file.name
382
+ if file_path.endswith('.pdf'):
383
+ if PYPDF2_AVAILABLE:
384
+ return read_pdf(file_path)
385
+ else:
386
+ return "Error: PDF reading is disabled - PyPDF2 library is not installed"
387
+ elif file_path.endswith('.cha'):
388
+ return read_cha_file(file_path)
389
+ else:
390
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
391
+ return f.read()
392
+
393
+ # ===============================
394
+ # AI Model Interface Functions
395
+ # ===============================
396
+
397
+ def call_bedrock(prompt, max_tokens=4096):
398
+ """Call the AWS Bedrock API to analyze text using Claude"""
399
+ if not bedrock_client:
400
+ return "AWS credentials not configured. Using demo response instead."
401
+
402
+ try:
403
+ body = json.dumps({
404
+ "anthropic_version": "bedrock-2023-05-31",
405
+ "max_tokens": max_tokens,
406
+ "messages": [
407
+ {
408
+ "role": "user",
409
+ "content": prompt
410
+ }
411
+ ],
412
+ "temperature": 0.3,
413
+ "top_p": 0.9
414
+ })
415
+
416
+ modelId = 'anthropic.claude-3-sonnet-20240229-v1:0'
417
+ response = bedrock_client.invoke_model(
418
+ body=body,
419
+ modelId=modelId,
420
+ accept='application/json',
421
+ contentType='application/json'
422
+ )
423
+ response_body = json.loads(response.get('body').read())
424
+ return response_body['content'][0]['text']
425
+ except Exception as e:
426
+ logger.error(f"Error in call_bedrock: {str(e)}")
427
+ return f"Error: {str(e)}"
428
+
429
+ def transcribe_audio(audio_path, patient_age=8):
430
+ """Transcribe an audio recording using Amazon Transcribe and format in CHAT format"""
431
+ if not os.path.exists(audio_path):
432
+ logger.error(f"Audio file not found: {audio_path}")
433
+ return "Error: Audio file not found."
434
+
435
+ if not transcribe_client or not s3_client:
436
+ logger.warning("AWS clients not initialized, using demo transcription")
437
+ return generate_demo_transcription()
438
+
439
+ try:
440
+ # Get file info
441
+ file_name = os.path.basename(audio_path)
442
+ file_size = os.path.getsize(audio_path)
443
+ _, file_extension = os.path.splitext(file_name)
444
+
445
+ # Check file format
446
+ supported_formats = ['.mp3', '.mp4', '.wav', '.flac', '.ogg', '.amr', '.webm']
447
+ if file_extension.lower() not in supported_formats:
448
+ logger.error(f"Unsupported audio format: {file_extension}")
449
+ return f"Error: Unsupported audio format. Please use one of: {', '.join(supported_formats)}"
450
+
451
+ # Generate a unique job name
452
+ timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
453
+ job_name = f"casl-transcription-{timestamp}"
454
+ s3_key = f"{S3_PREFIX}{job_name}{file_extension}"
455
+
456
+ # Upload to S3
457
+ logger.info(f"Uploading {file_name} to S3 bucket {S3_BUCKET}")
458
+ try:
459
+ with open(audio_path, 'rb') as audio_file:
460
+ s3_client.upload_fileobj(audio_file, S3_BUCKET, s3_key)
461
+ except Exception as e:
462
+ logger.error(f"Failed to upload to S3: {str(e)}")
463
+
464
+ # If upload fails, try to create the bucket
465
+ try:
466
+ s3_client.create_bucket(Bucket=S3_BUCKET)
467
+ logger.info(f"Created S3 bucket: {S3_BUCKET}")
468
+
469
+ # Try upload again
470
+ with open(audio_path, 'rb') as audio_file:
471
+ s3_client.upload_fileobj(audio_file, S3_BUCKET, s3_key)
472
+ except Exception as bucket_error:
473
+ logger.error(f"Failed to create bucket and upload: {str(bucket_error)}")
474
+ return "Error: Failed to upload audio file. Please check your AWS permissions."
475
+
476
+ # Start transcription job
477
+ logger.info(f"Starting transcription job: {job_name}")
478
+ media_format = file_extension.lower()[1:] # Remove the dot
479
+ if media_format == 'webm':
480
+ media_format = 'webm' # Amazon Transcribe expects this
481
+
482
+ # Determine language settings based on patient age
483
+ if patient_age < 10:
484
+ # For younger children, enabling child language model is helpful
485
+ language_options = {
486
+ 'LanguageCode': 'en-US',
487
+ 'Settings': {
488
+ 'ShowSpeakerLabels': True,
489
+ 'MaxSpeakerLabels': 2 # Typically patient + clinician
490
+ }
491
+ }
492
+ else:
493
+ language_options = {
494
+ 'LanguageCode': 'en-US',
495
+ 'Settings': {
496
+ 'ShowSpeakerLabels': True,
497
+ 'MaxSpeakerLabels': 2 # Typically patient + clinician
498
+ }
499
+ }
500
+
501
+ transcribe_client.start_transcription_job(
502
+ TranscriptionJobName=job_name,
503
+ Media={
504
+ 'MediaFileUri': f"s3://{S3_BUCKET}/{s3_key}"
505
+ },
506
+ MediaFormat=media_format,
507
+ **language_options
508
+ )
509
+
510
+ # Wait for the job to complete (with timeout)
511
+ logger.info("Waiting for transcription to complete...")
512
+ max_tries = 30 # 5 minutes max wait
513
+ tries = 0
514
+
515
+ while tries < max_tries:
516
+ try:
517
+ job = transcribe_client.get_transcription_job(TranscriptionJobName=job_name)
518
+ status = job['TranscriptionJob']['TranscriptionJobStatus']
519
+
520
+ if status == 'COMPLETED':
521
+ # Get the transcript
522
+ transcript_uri = job['TranscriptionJob']['Transcript']['TranscriptFileUri']
523
+
524
+ # Download the transcript
525
+ import urllib.request
526
+ import json
527
+
528
+ with urllib.request.urlopen(transcript_uri) as response:
529
+ transcript_json = json.loads(response.read().decode('utf-8'))
530
+
531
+ # Convert to CHAT format
532
+ chat_transcript = format_as_chat(transcript_json)
533
+ return chat_transcript
534
+
535
+ elif status == 'FAILED':
536
+ reason = job['TranscriptionJob'].get('FailureReason', 'Unknown failure')
537
+ logger.error(f"Transcription job failed: {reason}")
538
+ return f"Error: Transcription failed - {reason}"
539
+
540
+ # Still in progress, wait and try again
541
+ tries += 1
542
+ time.sleep(10) # Check every 10 seconds
543
+
544
+ except Exception as e:
545
+ logger.error(f"Error checking transcription job: {str(e)}")
546
+ return f"Error getting transcription: {str(e)}"
547
+
548
+ # If we got here, we timed out
549
+ return "Error: Transcription timed out. The process is taking longer than expected."
550
+
551
+ except Exception as e:
552
+ logger.exception("Error in audio transcription")
553
+ return f"Error transcribing audio: {str(e)}"
554
+
555
+ def format_as_chat(transcript_json):
556
+ """Format the Amazon Transcribe JSON result as CHAT format"""
557
+ try:
558
+ # Get transcript items
559
+ items = transcript_json['results']['items']
560
+
561
+ # Get speaker labels if available
562
+ speakers = {}
563
+ if 'speaker_labels' in transcript_json['results']:
564
+ speaker_segments = transcript_json['results']['speaker_labels']['segments']
565
+
566
+ # Map each item to its speaker
567
+ for segment in speaker_segments:
568
+ for item in segment['items']:
569
+ start_time = item['start_time']
570
+ speakers[start_time] = segment['speaker_label']
571
+
572
+ # Build transcript by combining words into utterances by speaker
573
+ current_speaker = None
574
+ current_utterance = []
575
+ utterances = []
576
+
577
+ for item in items:
578
+ # Skip non-pronunciation items (like punctuation)
579
+ if item['type'] != 'pronunciation':
580
+ continue
581
+
582
+ word = item['alternatives'][0]['content']
583
+ start_time = item.get('start_time')
584
+
585
+ # Determine speaker if available
586
+ speaker = speakers.get(start_time, 'spk_0')
587
+
588
+ # If speaker changed, start a new utterance
589
+ if speaker != current_speaker and current_utterance:
590
+ utterances.append((current_speaker, ' '.join(current_utterance)))
591
+ current_utterance = []
592
+
593
+ current_speaker = speaker
594
+ current_utterance.append(word)
595
+
596
+ # Add the last utterance
597
+ if current_utterance:
598
+ utterances.append((current_speaker, ' '.join(current_utterance)))
599
+
600
+ # Format as CHAT
601
+ chat_lines = []
602
+ for speaker, text in utterances:
603
+ # Map speakers to CHAT format
604
+ # Assuming spk_0 is the patient (PAR) and spk_1 is the clinician (INV)
605
+ chat_speaker = "*PAR:" if speaker == "spk_0" else "*INV:"
606
+ chat_lines.append(f"{chat_speaker} {text}.")
607
+
608
+ return '\n'.join(chat_lines)
609
+
610
+ except Exception as e:
611
+ logger.exception("Error formatting transcript")
612
+ return "*PAR: (Error formatting transcript)"
613
+
614
+ def generate_demo_transcription():
615
+ """Generate a simulated transcription response"""
616
+ return """*PAR: today I want to tell you about my favorite toy.
617
+ *PAR: it's a &-um teddy bear that I got for my birthday.
618
+ *PAR: he has &-um brown fur and a red bow.
619
+ *PAR: I like to sleep with him every night.
620
+ *PAR: sometimes I take him to school in my backpack.
621
+ *INV: what's your teddy bear's name?
622
+ *PAR: his name is &-um Brownie because he's brown."""
623
+
624
+ def generate_demo_response(prompt):
625
+ """Generate a simulated response for demo purposes"""
626
+ # This function generates a realistic but fake response for demo purposes
627
+ # In a real deployment, you would call an actual LLM API
628
+
629
+ return """<SPEECH_FACTORS_START>
630
+ Difficulty producing fluent speech: 8, 65
631
+ Examples:
632
+ - "today I would &-um like to talk about &-um a fun trip I took last &-um summer with my family"
633
+ - "we went to the &-um &-um beach [//] no to the mountains [//] I mean the beach actually"
634
+
635
+ Word retrieval issues: 6, 72
636
+ Examples:
637
+ - "what do you call those &-um &-um sprinkles! that's the word"
638
+ - "sometimes I forget [//] forgetted [: forgot] [*] what they call those things we built"
639
+
640
+ Grammatical errors: 4, 58
641
+ Examples:
642
+ - "after swimming we [//] I eat [: ate] [*] &-um ice cream"
643
+ - "sometimes I forget [//] forgetted [: forgot] [*] what they call those things we built"
644
+
645
+ Repetitions and revisions: 5, 62
646
+ Examples:
647
+ - "we [/] we stayed for &-um three no [//] four days"
648
+ - "we went to the &-um &-um beach [//] no to the mountains [//] I mean the beach actually"
649
+ <SPEECH_FACTORS_END>
650
+
651
+ <CASL_SKILLS_START>
652
+ Lexical/Semantic Skills: Standard Score (92), Percentile Rank (30%), Average Performance
653
+ Examples:
654
+ - "what do you call those &-um &-um sprinkles! that's the word"
655
+ - "we went to the &-um &-um beach [//] no to the mountains [//] I mean the beach actually"
656
+
657
+ Syntactic Skills: Standard Score (87), Percentile Rank (19%), Low Average Performance
658
+ Examples:
659
+ - "my brother he [//] he helped me dig a big hole"
660
+ - "after swimming we [//] I eat [: ate] [*] &-um ice cream with &-um chocolate things on top"
661
+
662
+ Supralinguistic Skills: Standard Score (90), Percentile Rank (25%), Average Performance
663
+ Examples:
664
+ - "sometimes I wonder [/] wonder where fishies [: fish] [*] go when it's cold"
665
+ - "maybe they have [/] have houses under the water"
666
+ <CASL_SKILLS_END>
667
+
668
+ <TREATMENT_RECOMMENDATIONS_START>
669
+ - Implement word-finding strategies with semantic cuing focused on everyday objects and activities, using the patient's beach experience as a context (e.g., "sprinkles," "castles")
670
+ - Practice structured narrative tasks with visual supports to reduce revisions and improve sequencing
671
+ - Use sentence formulation exercises focusing on verb tense consistency (addressing errors like "forgetted" and "eat" for "ate")
672
+ - Incorporate self-monitoring techniques to help identify and correct grammatical errors
673
+ - Work on increasing vocabulary specificity (e.g., "things on top" to "sprinkles")
674
+ <TREATMENT_RECOMMENDATIONS_END>
675
+
676
+ <EXPLANATION_START>
677
+ This child demonstrates moderate word-finding difficulties with compensatory strategies including fillers ("&-um") and repetitions. The frequent use of self-corrections shows good metalinguistic awareness, but the pauses and repairs impact conversational fluency. Syntactic errors primarily involve verb tense inconsistency. Overall, the pattern suggests a mild-to-moderate language disorder with stronger receptive than expressive skills.
678
+ <EXPLANATION_END>
679
+
680
+ <ADDITIONAL_ANALYSIS_START>
681
+ The child shows relative strengths in maintaining topic coherence and conveying a complete narrative structure despite the language challenges. The pattern of errors suggests that word-finding difficulties and processing speed are primary concerns rather than conceptual or cognitive issues. Semantic network activities that strengthen word associations would likely be beneficial, particularly when paired with visual supports.
682
+ <ADDITIONAL_ANALYSIS_END>
683
+
684
+ <DIAGNOSTIC_IMPRESSIONS_START>
685
+ Based on the language sample, this child presents with a profile consistent with a mild-to-moderate expressive language disorder. The most prominent features include:
686
+
687
+ 1. Word-finding difficulties characterized by fillers, pauses, and self-corrections when attempting to retrieve specific vocabulary
688
+ 2. Grammatical challenges primarily affecting verb tense consistency and morphological markers
689
+ 3. Relatively intact narrative structure and topic maintenance
690
+
691
+ These findings suggest intervention should focus on word retrieval strategies, grammatical form practice, and continued support for narrative development, with an emphasis on fluency and self-monitoring.
692
+ <DIAGNOSTIC_IMPRESSIONS_END>
693
+
694
+ <ERROR_EXAMPLES_START>
695
+ Word-finding difficulties:
696
+ - "what do you call those &-um &-um sprinkles! that's the word"
697
+ - "we went to the &-um &-um beach [//] no to the mountains [//] I mean the beach actually"
698
+ - "there was lots of &-um &-um swimming and &-um sun"
699
+
700
+ Grammatical errors:
701
+ - "after swimming we [//] I eat [: ate] [*] &-um ice cream"
702
+ - "sometimes I forget [//] forgetted [: forgot] [*] what they call those things we built"
703
+ - "we saw [/] saw fishies [: fish] [*] swimming in the water"
704
+
705
+ Repetitions and revisions:
706
+ - "we [/] we stayed for &-um three no [//] four days"
707
+ - "I want to go back to the beach [/] beach next year"
708
+ - "sometimes I wonder [/] wonder where fishies [: fish] [*] go when it's cold"
709
+ <ERROR_EXAMPLES_END>"""
710
+
711
+ def parse_casl_response(response):
712
+ """Parse the LLM response for CASL analysis into structured data"""
713
+ # Extract speech factors section using section markers
714
+ speech_factors_section = ""
715
+ factors_pattern = re.compile(r"<SPEECH_FACTORS_START>(.*?)<SPEECH_FACTORS_END>", re.DOTALL)
716
+ factors_match = factors_pattern.search(response)
717
+
718
+ if factors_match:
719
+ speech_factors_section = factors_match.group(1).strip()
720
+ else:
721
+ speech_factors_section = "Error extracting speech factors from analysis."
722
+
723
+ # Extract CASL skills section
724
+ casl_section = ""
725
+ casl_pattern = re.compile(r"<CASL_SKILLS_START>(.*?)<CASL_SKILLS_END>", re.DOTALL)
726
+ casl_match = casl_pattern.search(response)
727
+
728
+ if casl_match:
729
+ casl_section = casl_match.group(1).strip()
730
+ else:
731
+ casl_section = "Error extracting CASL skills from analysis."
732
+
733
+ # Extract treatment recommendations
734
+ treatment_text = ""
735
+ treatment_pattern = re.compile(r"<TREATMENT_RECOMMENDATIONS_START>(.*?)<TREATMENT_RECOMMENDATIONS_END>", re.DOTALL)
736
+ treatment_match = treatment_pattern.search(response)
737
+
738
+ if treatment_match:
739
+ treatment_text = treatment_match.group(1).strip()
740
+ else:
741
+ treatment_text = "Error extracting treatment recommendations from analysis."
742
+
743
+ # Extract explanation section
744
+ explanation_text = ""
745
+ explanation_pattern = re.compile(r"<EXPLANATION_START>(.*?)<EXPLANATION_END>", re.DOTALL)
746
+ explanation_match = explanation_pattern.search(response)
747
+
748
+ if explanation_match:
749
+ explanation_text = explanation_match.group(1).strip()
750
+ else:
751
+ explanation_text = "Error extracting clinical explanation from analysis."
752
+
753
+ # Extract additional analysis
754
+ additional_analysis = ""
755
+ additional_pattern = re.compile(r"<ADDITIONAL_ANALYSIS_START>(.*?)<ADDITIONAL_ANALYSIS_END>", re.DOTALL)
756
+ additional_match = additional_pattern.search(response)
757
+
758
+ if additional_match:
759
+ additional_analysis = additional_match.group(1).strip()
760
+
761
+ # Extract diagnostic impressions
762
+ diagnostic_impressions = ""
763
+ diagnostic_pattern = re.compile(r"<DIAGNOSTIC_IMPRESSIONS_START>(.*?)<DIAGNOSTIC_IMPRESSIONS_END>", re.DOTALL)
764
+ diagnostic_match = diagnostic_pattern.search(response)
765
+
766
+ if diagnostic_match:
767
+ diagnostic_impressions = diagnostic_match.group(1).strip()
768
+
769
+ # Extract specific error examples
770
+ specific_errors_text = ""
771
+ errors_pattern = re.compile(r"<ERROR_EXAMPLES_START>(.*?)<ERROR_EXAMPLES_END>", re.DOTALL)
772
+ errors_match = errors_pattern.search(response)
773
+
774
+ if errors_match:
775
+ specific_errors_text = errors_match.group(1).strip()
776
+
777
+ # Create full report text
778
+ full_report = f"""
779
+ ## Speech Factors Analysis
780
+
781
+ {speech_factors_section}
782
+
783
+ ## CASL Skills Assessment
784
+
785
+ {casl_section}
786
+
787
+ ## Treatment Recommendations
788
+
789
+ {treatment_text}
790
+
791
+ ## Clinical Explanation
792
+
793
+ {explanation_text}
794
+ """
795
+
796
+ if additional_analysis:
797
+ full_report += f"\n## Additional Analysis\n\n{additional_analysis}"
798
+
799
+ if diagnostic_impressions:
800
+ full_report += f"\n## Diagnostic Impressions\n\n{diagnostic_impressions}"
801
+
802
+ if specific_errors_text:
803
+ full_report += f"\n## Detailed Error Examples\n\n{specific_errors_text}"
804
+
805
+ return {
806
+ 'speech_factors': speech_factors_section,
807
+ 'casl_data': casl_section,
808
+ 'treatment_suggestions': treatment_text,
809
+ 'explanation': explanation_text,
810
+ 'additional_analysis': additional_analysis,
811
+ 'diagnostic_impressions': diagnostic_impressions,
812
+ 'specific_errors': specific_errors_text,
813
+ 'full_report': full_report,
814
+ 'raw_response': response
815
+ }
816
+
817
+ def analyze_transcript(transcript, age, gender):
818
+ """Analyze a speech transcript using Claude"""
819
+ # CASL-2 assessment cheat sheet
820
+ cheat_sheet = """
821
+ # Speech-Language Pathologist Analysis Cheat Sheet
822
+
823
+ ## Types of Speech Patterns to Identify:
824
+
825
+ 1. Difficulty producing fluent, grammatical speech
826
+ - Fillers (um, uh) and pauses
827
+ - False starts and revisions
828
+ - Incomplete sentences
829
+
830
+ 2. Word retrieval issues
831
+ - Pauses before content words
832
+ - Circumlocutions (talking around a word)
833
+ - Word substitutions
834
+
835
+ 3. Grammatical errors
836
+ - Verb tense inconsistencies
837
+ - Subject-verb agreement errors
838
+ - Morphological errors (plurals, possessives)
839
+
840
+ 4. Repetitions and revisions
841
+ - Word or phrase repetitions [/]
842
+ - Self-corrections [//]
843
+ - Retracing
844
+
845
+ 5. Neologisms
846
+ - Made-up words
847
+ - Word blends
848
+
849
+ 6. Perseveration
850
+ - Inappropriate repetition of ideas
851
+ - Recurring themes
852
+
853
+ 7. Comprehension issues
854
+ - Topic maintenance difficulties
855
+ - Non-sequiturs
856
+ - Inappropriate responses
857
+ """
858
+
859
+ # Instructions for the analysis
860
+ instructions = """
861
+ Analyze this speech transcript to identify specific patterns and provide a detailed CASL-2 (Comprehensive Assessment of Spoken Language) assessment.
862
+
863
+ For each speech pattern you identify:
864
+ 1. Count the occurrences in the transcript
865
+ 2. Estimate a percentile (how typical/atypical this is for the age)
866
+ 3. Provide DIRECT QUOTES from the transcript as evidence
867
+
868
+ Then assess the following CASL-2 domains:
869
+
870
+ 1. Lexical/Semantic Skills:
871
+ - Assess vocabulary diversity, word-finding abilities, semantic precision
872
+ - Provide Standard Score (mean=100, SD=15), percentile rank, and performance level
873
+ - Include SPECIFIC QUOTES as evidence
874
+
875
+ 2. Syntactic Skills:
876
+ - Evaluate grammatical accuracy, sentence complexity, morphological skills
877
+ - Provide Standard Score, percentile rank, and performance level
878
+ - Include SPECIFIC QUOTES as evidence
879
+
880
+ 3. Supralinguistic Skills:
881
+ - Assess figurative language use, inferencing, and abstract reasoning
882
+ - Provide Standard Score, percentile rank, and performance level
883
+ - Include SPECIFIC QUOTES as evidence
884
+
885
+ YOUR RESPONSE MUST USE THESE EXACT SECTION MARKERS FOR PARSING:
886
+
887
+ <SPEECH_FACTORS_START>
888
+ Difficulty producing fluent speech: (occurrences), (percentile)
889
+ Examples:
890
+ - "(direct quote from transcript)"
891
+ - "(direct quote from transcript)"
892
+
893
+ Word retrieval issues: (occurrences), (percentile)
894
+ Examples:
895
+ - "(direct quote from transcript)"
896
+ - "(direct quote from transcript)"
897
+
898
+ (And so on for each factor)
899
+ <SPEECH_FACTORS_END>
900
+
901
+ <CASL_SKILLS_START>
902
+ Lexical/Semantic Skills: Standard Score (X), Percentile Rank (X%), Performance Level
903
+ Examples:
904
+ - "(direct quote showing strength or weakness)"
905
+ - "(direct quote showing strength or weakness)"
906
+
907
+ Syntactic Skills: Standard Score (X), Percentile Rank (X%), Performance Level
908
+ Examples:
909
+ - "(direct quote showing strength or weakness)"
910
+ - "(direct quote showing strength or weakness)"
911
+
912
+ Supralinguistic Skills: Standard Score (X), Percentile Rank (X%), Performance Level
913
+ Examples:
914
+ - "(direct quote showing strength or weakness)"
915
+ - "(direct quote showing strength or weakness)"
916
+ <CASL_SKILLS_END>
917
+
918
+ <TREATMENT_RECOMMENDATIONS_START>
919
+ - (treatment recommendation)
920
+ - (treatment recommendation)
921
+ - (treatment recommendation)
922
+ <TREATMENT_RECOMMENDATIONS_END>
923
+
924
+ <EXPLANATION_START>
925
+ (brief diagnostic rationale based on findings)
926
+ <EXPLANATION_END>
927
+
928
+ <ADDITIONAL_ANALYSIS_START>
929
+ (specific insights that would be helpful for treatment planning)
930
+ <ADDITIONAL_ANALYSIS_END>
931
+
932
+ <DIAGNOSTIC_IMPRESSIONS_START>
933
+ (summarize findings across domains using specific examples and clear explanations)
934
+ <DIAGNOSTIC_IMPRESSIONS_END>
935
+
936
+ <ERROR_EXAMPLES_START>
937
+ (Copy all the specific quote examples here again, organized by error type or skill domain)
938
+ <ERROR_EXAMPLES_END>
939
+
940
+ MOST IMPORTANT:
941
+ 1. Use EXACTLY the section markers provided (like <SPEECH_FACTORS_START>) to make parsing reliable
942
+ 2. For EVERY factor and domain you analyze, you MUST provide direct quotes from the transcript as evidence
943
+ 3. Be very specific and cite the exact text
944
+ 4. Do not omit any of the required sections
945
+ """
946
+
947
+ # Prepare prompt for Claude with the user's role context
948
+ role_context = """
949
+ You are a speech pathologist, a healthcare professional who specializes in evaluating, diagnosing, and treating communication disorders, including speech, language, cognitive-communication, voice, swallowing, and fluency disorders. Your role is to help patients improve their speech and communication skills through various therapeutic techniques and exercises.
950
+
951
+ You are working with a student with speech impediments.
952
+
953
+ The most important thing is that you stay kind to the child. Be constructive and helpful rather than critical.
954
+ """
955
+
956
+ prompt = f"""
957
+ {role_context}
958
+
959
+ You are analyzing a transcript for a patient who is {age} years old and {gender}.
960
+
961
+ TRANSCRIPT:
962
+ {transcript}
963
+
964
+ {cheat_sheet}
965
+
966
+ {instructions}
967
+
968
+ Remember to be precise but compassionate in your analysis. Use direct quotes from the transcript for every factor and domain you analyze.
969
+ """
970
+
971
+ # Call the appropriate API or fallback to demo mode
972
+ if bedrock_client:
973
+ response = call_bedrock(prompt)
974
+ else:
975
+ response = generate_demo_response(prompt)
976
+
977
+ # Parse the response
978
+ results = parse_casl_response(response)
979
+
980
+ return results
981
+
982
+ def export_pdf(results, patient_name="", record_id="", age="", gender="", assessment_date="", clinician=""):
983
+ """Export analysis results to a PDF report"""
984
+ global DOWNLOADS_DIR
985
+
986
+ # Check if ReportLab is available
987
+ if not REPORTLAB_AVAILABLE:
988
+ return "ERROR: PDF export is not available - ReportLab library is not installed. Please run 'pip install reportlab'."
989
+
990
+ try:
991
+ # Generate a safe filename
992
+ if patient_name:
993
+ safe_name = f"{patient_name.replace(' ', '_')}"
994
+ else:
995
+ safe_name = f"speech_analysis_{datetime.now().strftime('%Y%m%d%H%M%S')}"
996
+
997
+ # Make sure the downloads directory exists
998
+ try:
999
+ os.makedirs(DOWNLOADS_DIR, exist_ok=True)
1000
+ except Exception as e:
1001
+ logger.warning(f"Could not access downloads directory: {str(e)}")
1002
+ # Fallback to temp directory
1003
+ DOWNLOADS_DIR = os.path.join(os.path.expanduser("~"), "casl_downloads")
1004
+ os.makedirs(DOWNLOADS_DIR, exist_ok=True)
1005
+
1006
+ # Create the PDF path in our downloads directory
1007
+ pdf_path = os.path.join(DOWNLOADS_DIR, f"{safe_name}.pdf")
1008
+
1009
+ # Create the PDF document
1010
+ doc = SimpleDocTemplate(pdf_path, pagesize=letter)
1011
+ styles = getSampleStyleSheet()
1012
+
1013
+ # Create enhanced custom styles
1014
+ styles.add(ParagraphStyle(
1015
+ name='Heading1',
1016
+ parent=styles['Heading1'],
1017
+ fontSize=16,
1018
+ spaceAfter=12,
1019
+ textColor=colors.navy
1020
+ ))
1021
+
1022
+ styles.add(ParagraphStyle(
1023
+ name='Heading2',
1024
+ parent=styles['Heading2'],
1025
+ fontSize=14,
1026
+ spaceAfter=10,
1027
+ spaceBefore=10,
1028
+ textColor=colors.darkblue
1029
+ ))
1030
+
1031
+ styles.add(ParagraphStyle(
1032
+ name='Heading3',
1033
+ parent=styles['Heading2'],
1034
+ fontSize=12,
1035
+ spaceAfter=8,
1036
+ spaceBefore=8,
1037
+ textColor=colors.darkblue
1038
+ ))
1039
+
1040
+ styles.add(ParagraphStyle(
1041
+ name='BodyText',
1042
+ parent=styles['BodyText'],
1043
+ fontSize=11,
1044
+ spaceAfter=8,
1045
+ leading=14
1046
+ ))
1047
+
1048
+ styles.add(ParagraphStyle(
1049
+ name='BulletPoint',
1050
+ parent=styles['BodyText'],
1051
+ fontSize=11,
1052
+ leftIndent=20,
1053
+ firstLineIndent=-15,
1054
+ spaceAfter=4,
1055
+ leading=14
1056
+ ))
1057
+
1058
+ # Convert markdown to PDF elements
1059
+ story = []
1060
+
1061
+ # Add title and date
1062
+ story.append(Paragraph("Speech Language Assessment Report", styles['Title']))
1063
+ story.append(Spacer(1, 12))
1064
+
1065
+ # Add patient information table
1066
+ if patient_name or record_id or age or gender:
1067
+ # Prepare patient info data
1068
+ data = []
1069
+ if patient_name:
1070
+ data.append(["Patient Name:", patient_name])
1071
+ if record_id:
1072
+ data.append(["Record ID:", record_id])
1073
+ if age:
1074
+ data.append(["Age:", f"{age} years"])
1075
+ if gender:
1076
+ data.append(["Gender:", gender])
1077
+ if assessment_date:
1078
+ data.append(["Assessment Date:", assessment_date])
1079
+ if clinician:
1080
+ data.append(["Clinician:", clinician])
1081
+
1082
+ if data:
1083
+ # Create a table with the data
1084
+ patient_table = Table(data, colWidths=[120, 350])
1085
+ patient_table.setStyle(TableStyle([
1086
+ ('BACKGROUND', (0, 0), (0, -1), colors.lightgrey),
1087
+ ('TEXTCOLOR', (0, 0), (0, -1), colors.darkblue),
1088
+ ('ALIGN', (0, 0), (0, -1), 'RIGHT'),
1089
+ ('ALIGN', (1, 0), (1, -1), 'LEFT'),
1090
+ ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
1091
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
1092
+ ('TOPPADDING', (0, 0), (-1, -1), 6),
1093
+ ('GRID', (0, 0), (-1, -1), 0.5, colors.lightgrey),
1094
+ ]))
1095
+ story.append(patient_table)
1096
+ story.append(Spacer(1, 12))
1097
+
1098
+ # Add clinical analysis sections
1099
+ story.append(Paragraph("Speech Factors Analysis", styles['Heading1']))
1100
+ speech_factors_paragraphs = []
1101
+ for line in results['speech_factors'].split('\n'):
1102
+ line = line.strip()
1103
+ if not line:
1104
+ continue
1105
+ if line.startswith('- '):
1106
+ story.append(Paragraph(f"β€’ {line[2:]}", styles['BulletPoint']))
1107
+ else:
1108
+ story.append(Paragraph(line, styles['BodyText']))
1109
+ story.append(Spacer(1, 12))
1110
+
1111
+ story.append(Paragraph("CASL Skills Assessment", styles['Heading1']))
1112
+ for line in results['casl_data'].split('\n'):
1113
+ line = line.strip()
1114
+ if not line:
1115
+ continue
1116
+ if line.startswith('- '):
1117
+ story.append(Paragraph(f"β€’ {line[2:]}", styles['BulletPoint']))
1118
+ else:
1119
+ story.append(Paragraph(line, styles['BodyText']))
1120
+ story.append(Spacer(1, 12))
1121
+
1122
+ story.append(Paragraph("Treatment Recommendations", styles['Heading1']))
1123
+
1124
+ # Process treatment recommendations as bullet points
1125
+ for line in results['treatment_suggestions'].split('\n'):
1126
+ line = line.strip()
1127
+ if not line:
1128
+ continue
1129
+ if line.startswith('- '):
1130
+ story.append(Paragraph(f"β€’ {line[2:]}", styles['BulletPoint']))
1131
+ else:
1132
+ story.append(Paragraph(line, styles['BodyText']))
1133
+
1134
+ story.append(Spacer(1, 12))
1135
+
1136
+ story.append(Paragraph("Clinical Explanation", styles['Heading1']))
1137
+ story.append(Paragraph(results['explanation'], styles['BodyText']))
1138
+ story.append(Spacer(1, 12))
1139
+
1140
+ if results['additional_analysis']:
1141
+ story.append(Paragraph("Additional Analysis", styles['Heading1']))
1142
+ story.append(Paragraph(results['additional_analysis'], styles['BodyText']))
1143
+ story.append(Spacer(1, 12))
1144
+
1145
+ if results['diagnostic_impressions']:
1146
+ story.append(Paragraph("Diagnostic Impressions", styles['Heading1']))
1147
+ story.append(Paragraph(results['diagnostic_impressions'], styles['BodyText']))
1148
+ story.append(Spacer(1, 12))
1149
+
1150
+ # Add footer with date
1151
+ footer_text = f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
1152
+ story.append(Spacer(1, 20))
1153
+ story.append(Paragraph(footer_text, ParagraphStyle(
1154
+ name='Footer',
1155
+ parent=styles['Normal'],
1156
+ fontSize=8,
1157
+ textColor=colors.grey
1158
+ )))
1159
+
1160
+ # Build the PDF
1161
+ doc.build(story)
1162
+
1163
+ logger.info(f"Report saved as PDF: {pdf_path}")
1164
+ return pdf_path
1165
+
1166
+ except Exception as e:
1167
+ logger.exception("Error creating PDF")
1168
+ return f"Error creating PDF: {str(e)}"
1169
+
1170
+ def create_interface():
1171
+ """Create the Gradio interface"""
1172
+ # Set a theme compatible with Hugging Face Spaces
1173
+ theme = gr.themes.Soft(
1174
+ primary_hue="blue",
1175
+ secondary_hue="indigo",
1176
+ )
1177
+
1178
+ with gr.Blocks(title="CASL Analysis Tool", theme=theme) as app:
1179
+ gr.Markdown("# CASL Analysis Tool")
1180
+ gr.Markdown("A tool for analyzing speech transcripts and audio using the CASL framework")
1181
+
1182
+ with gr.Tabs() as main_tabs:
1183
+ # Analysis Tab
1184
+ with gr.TabItem("Analysis", id=0):
1185
+ with gr.Row():
1186
+ with gr.Column(scale=1):
1187
+ # Patient info
1188
+ gr.Markdown("### Patient Information")
1189
+ patient_name = gr.Textbox(label="Patient Name", placeholder="Enter patient name")
1190
+ record_id = gr.Textbox(label="Record ID", placeholder="Enter record ID")
1191
+
1192
+ with gr.Row():
1193
+ age = gr.Number(label="Age", value=8, minimum=1, maximum=120)
1194
+ gender = gr.Radio(["male", "female", "other"], label="Gender", value="male")
1195
+
1196
+ assessment_date = gr.Textbox(
1197
+ label="Assessment Date",
1198
+ placeholder="MM/DD/YYYY",
1199
+ value=datetime.now().strftime('%m/%d/%Y')
1200
+ )
1201
+ clinician_name = gr.Textbox(label="Clinician", placeholder="Enter clinician name")
1202
+
1203
+ # Transcript input
1204
+ gr.Markdown("### Transcript")
1205
+ sample_btn = gr.Button("Load Sample Transcript")
1206
+ file_upload = gr.File(label="Upload transcript file (.txt or .cha)")
1207
+ transcript = gr.Textbox(
1208
+ label="Speech transcript (CHAT format preferred)",
1209
+ placeholder="Enter transcript text or upload a file...",
1210
+ lines=10
1211
+ )
1212
+
1213
+ # Analysis button
1214
+ analyze_btn = gr.Button("Analyze Transcript", variant="primary")
1215
+
1216
+ with gr.Column(scale=1):
1217
+ # Results display
1218
+ with gr.Tabs() as results_tabs:
1219
+ with gr.TabItem("Summary", id=0):
1220
+ gr.Markdown("### Speech Factors Analysis")
1221
+ speech_factors_md = gr.Markdown()
1222
+
1223
+ gr.Markdown("### CASL Skills Assessment")
1224
+ casl_results_md = gr.Markdown()
1225
+
1226
+ with gr.TabItem("Treatment", id=1):
1227
+ gr.Markdown("### Treatment Recommendations")
1228
+ treatment_md = gr.Markdown()
1229
+
1230
+ gr.Markdown("### Clinical Explanation")
1231
+ explanation_md = gr.Markdown()
1232
+
1233
+ with gr.TabItem("Error Examples", id=2):
1234
+ specific_errors_md = gr.Markdown()
1235
+
1236
+ with gr.TabItem("Full Report", id=3):
1237
+ full_analysis = gr.Markdown()
1238
+
1239
+ # PDF export (only shown if ReportLab is available)
1240
+ export_status = gr.Markdown("")
1241
+ if REPORTLAB_AVAILABLE:
1242
+ export_btn = gr.Button("Export as PDF", variant="secondary")
1243
+ else:
1244
+ gr.Markdown("⚠️ PDF export is disabled - ReportLab library is not installed")
1245
+
1246
+ # Transcription Tab
1247
+ with gr.TabItem("Transcription", id=1):
1248
+ with gr.Row():
1249
+ with gr.Column(scale=1):
1250
+ gr.Markdown("### Audio Transcription")
1251
+ gr.Markdown("Upload an audio recording to automatically transcribe it in CHAT format")
1252
+
1253
+ # Patient's age helps with transcription accuracy
1254
+ transcription_age = gr.Number(label="Patient Age", value=8, minimum=1, maximum=120,
1255
+ info="For children under 10, special language models may be used")
1256
+
1257
+ # Audio input
1258
+ audio_input = gr.Audio(type="filepath", label="Upload Audio Recording",
1259
+ format="mp3,wav,ogg,webm",
1260
+ elem_id="audio-input")
1261
+
1262
+ # Transcribe button
1263
+ transcribe_btn = gr.Button("Transcribe Audio", variant="primary")
1264
+
1265
+ with gr.Column(scale=1):
1266
+ # Transcription output
1267
+ transcription_output = gr.Textbox(
1268
+ label="Transcription Result",
1269
+ placeholder="Transcription will appear here...",
1270
+ lines=12
1271
+ )
1272
+
1273
+ with gr.Row():
1274
+ # Button to use transcription in analysis
1275
+ copy_to_analysis_btn = gr.Button("Use for Analysis", variant="secondary")
1276
+
1277
+ # Status/info message
1278
+ transcription_status = gr.Markdown("")
1279
+
1280
+ # Load sample transcript button
1281
+ def load_sample():
1282
+ return SAMPLE_TRANSCRIPT
1283
+
1284
+ sample_btn.click(load_sample, outputs=[transcript])
1285
+
1286
+ # File upload handler
1287
+ file_upload.upload(process_upload, file_upload, transcript)
1288
+
1289
+ # Analysis button handler
1290
+ def on_analyze_click(transcript_text, age_val, gender_val, patient_name_val, record_id_val, clinician_val, assessment_date_val):
1291
+ if not transcript_text or len(transcript_text.strip()) < 50:
1292
+ return "Error: Please provide a longer transcript for analysis.", "Error: Insufficient data", "Error: Insufficient data", "Error: Please provide a transcript of at least 50 characters for meaningful analysis.", "Error: Not enough transcript data for analysis.", "Error: No detailed error examples available for an empty transcript."
1293
+
1294
+ try:
1295
+ # Get the analysis results
1296
+ results = analyze_transcript(transcript_text, age_val, gender_val)
1297
+
1298
+ # Save patient record
1299
+ patient_info = {
1300
+ "name": patient_name_val,
1301
+ "record_id": record_id_val,
1302
+ "age": age_val,
1303
+ "gender": gender_val,
1304
+ "assessment_date": assessment_date_val,
1305
+ "clinician": clinician_val
1306
+ }
1307
+
1308
+ saved_id = save_patient_record(patient_info, results, transcript_text)
1309
+
1310
+ if saved_id:
1311
+ save_msg = f"βœ… Patient record saved successfully. ID: {saved_id}"
1312
+ else:
1313
+ save_msg = "⚠️ Could not save patient record. Check directory permissions."
1314
+
1315
+ # Return the results
1316
+ return results['speech_factors'], results['casl_data'], results['treatment_suggestions'], results['explanation'], results['full_report'], save_msg, results['specific_errors']
1317
+
1318
+ except Exception as e:
1319
+ logger.exception("Error during analysis")
1320
+ return f"Error during analysis: {str(e)}", "Analysis failed", "Not available", f"Error: {str(e)}", f"Analysis error: {str(e)}", "", ""
1321
+
1322
+ analyze_btn.click(
1323
+ on_analyze_click,
1324
+ inputs=[
1325
+ transcript, age, gender,
1326
+ patient_name, record_id, clinician_name, assessment_date
1327
+ ],
1328
+ outputs=[
1329
+ speech_factors_md,
1330
+ casl_results_md,
1331
+ treatment_md,
1332
+ explanation_md,
1333
+ full_analysis,
1334
+ export_status,
1335
+ specific_errors_md
1336
+ ]
1337
+ )
1338
+
1339
+ # PDF export function
1340
+ def on_export_pdf(report_text, p_name, p_record_id, p_age, p_gender, p_date, p_clinician):
1341
+ # Check if ReportLab is available
1342
+ if not REPORTLAB_AVAILABLE:
1343
+ return "ERROR: PDF export is not available because the ReportLab library is not installed. Please install it with 'pip install reportlab'."
1344
+
1345
+ if not report_text or len(report_text.strip()) < 50:
1346
+ return "Error: Please run the analysis first before exporting to PDF."
1347
+
1348
+ try:
1349
+ # Parse the report text back into sections
1350
+ results = {
1351
+ 'speech_factors': '',
1352
+ 'casl_data': '',
1353
+ 'treatment_suggestions': '',
1354
+ 'explanation': '',
1355
+ 'additional_analysis': '',
1356
+ 'diagnostic_impressions': '',
1357
+ 'specific_errors': '',
1358
+ }
1359
+
1360
+ sections = report_text.split('##')
1361
+ for section in sections:
1362
+ section = section.strip()
1363
+ if not section:
1364
+ continue
1365
+
1366
+ title_content = section.split('\n', 1)
1367
+ if len(title_content) < 2:
1368
+ continue
1369
+
1370
+ title = title_content[0].strip()
1371
+ content = title_content[1].strip()
1372
+
1373
+ if "Speech Factors Analysis" in title:
1374
+ results['speech_factors'] = content
1375
+ elif "CASL Skills Assessment" in title:
1376
+ results['casl_data'] = content
1377
+ elif "Treatment Recommendations" in title:
1378
+ results['treatment_suggestions'] = content
1379
+ elif "Clinical Explanation" in title:
1380
+ results['explanation'] = content
1381
+ elif "Additional Analysis" in title:
1382
+ results['additional_analysis'] = content
1383
+ elif "Diagnostic Impressions" in title:
1384
+ results['diagnostic_impressions'] = content
1385
+ elif "Detailed Error Examples" in title:
1386
+ results['specific_errors'] = content
1387
+
1388
+ pdf_path = export_pdf(
1389
+ results,
1390
+ patient_name=p_name,
1391
+ record_id=p_record_id,
1392
+ age=p_age,
1393
+ gender=p_gender,
1394
+ assessment_date=p_date,
1395
+ clinician=p_clinician
1396
+ )
1397
+
1398
+ # Check if the export was successful
1399
+ if pdf_path.startswith("ERROR:"):
1400
+ return pdf_path
1401
+
1402
+ # Make it downloadable in Hugging Face Spaces
1403
+ download_link = f'<a href="file={pdf_path}" download="{os.path.basename(pdf_path)}">Download PDF Report</a>'
1404
+ return f"Report saved as PDF: {pdf_path}<br>{download_link}"
1405
+ except Exception as e:
1406
+ logger.exception("Error exporting to PDF")
1407
+ return f"Error creating PDF: {str(e)}"
1408
+
1409
+ # Only set up the PDF export button if ReportLab is available
1410
+ if REPORTLAB_AVAILABLE:
1411
+ export_btn.click(
1412
+ on_export_pdf,
1413
+ inputs=[
1414
+ full_analysis,
1415
+ patient_name,
1416
+ record_id,
1417
+ age,
1418
+ gender,
1419
+ assessment_date,
1420
+ clinician_name
1421
+ ],
1422
+ outputs=[export_status]
1423
+ )
1424
+
1425
+ # Transcription button handler
1426
+ def on_transcribe_audio(audio_path, age_val):
1427
+ try:
1428
+ if not audio_path:
1429
+ return "Please upload an audio file to transcribe.", "Error: No audio file provided."
1430
+
1431
+ # Process the audio file with Amazon Transcribe
1432
+ transcription = transcribe_audio(audio_path, age_val)
1433
+
1434
+ # Return status message based on whether it's a demo or real transcription
1435
+ if not transcribe_client:
1436
+ status_msg = "⚠️ Demo mode: Using example transcription (AWS credentials not configured)"
1437
+ else:
1438
+ status_msg = "βœ… Transcription completed successfully"
1439
+
1440
+ return transcription, status_msg
1441
+ except Exception as e:
1442
+ logger.exception("Error transcribing audio")
1443
+ return f"Error: {str(e)}", f"❌ Transcription failed: {str(e)}"
1444
+
1445
+ # Connect the transcribe button to its handler
1446
+ transcribe_btn.click(
1447
+ on_transcribe_audio,
1448
+ inputs=[audio_input, transcription_age],
1449
+ outputs=[transcription_output, transcription_status]
1450
+ )
1451
+
1452
+ # Copy transcription to analysis tab
1453
+ def copy_to_analysis(transcription):
1454
+ return transcription, gr.update(selected=0) # Switch to Analysis tab
1455
+
1456
+ copy_to_analysis_btn.click(
1457
+ copy_to_analysis,
1458
+ inputs=[transcription_output],
1459
+ outputs=[transcript, main_tabs]
1460
+ )
1461
+
1462
+ return app
1463
+
1464
+ # Create requirements.txt file for HuggingFace Spaces
1465
+ def create_requirements_file():
1466
+ requirements = [
1467
+ "gradio>=4.0.0",
1468
+ "pandas",
1469
+ "numpy",
1470
+ "matplotlib",
1471
+ "Pillow",
1472
+ "reportlab>=3.6.0", # Required for PDF exports
1473
+ "PyPDF2>=3.0.0", # Required for PDF reading
1474
+ "boto3>=1.28.0" # Required for AWS services
1475
+ ]
1476
+
1477
+ with open("requirements.txt", "w") as f:
1478
+ for req in requirements:
1479
+ f.write(f"{req}\n")
1480
+
1481
+ if __name__ == "__main__":
1482
+ # Create requirements.txt for HuggingFace Spaces
1483
+ create_requirements_file()
1484
+
1485
+ # Check for AWS credentials
1486
+ if not AWS_ACCESS_KEY or not AWS_SECRET_KEY:
1487
+ print("NOTE: AWS credentials not found. The app will run in demo mode with simulated responses.")
1488
+ print("To enable full functionality, set AWS_ACCESS_KEY and AWS_SECRET_KEY environment variables.")
1489
+
1490
+ app = create_interface()
1491
+ app.launch(show_api=False) # Disable API tab for security
reference_files/requirements_improved.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ pandas>=1.5.0
3
+ numpy>=1.21.0
4
+ matplotlib>=3.5.0
5
+ seaborn>=0.11.0
6
+ Pillow>=8.0.0
7
+ reportlab>=3.6.0
8
+ boto3>=1.28.0
9
+ botocore>=1.31.0
10
+ PyPDF2>=3.0.0
11
+ speech_recognition>=3.10.0
12
+ pydub>=0.25.0
reference_files/simple_app.py ADDED
@@ -0,0 +1,1208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import boto3
3
+ import json
4
+ import re
5
+ import logging
6
+ import os
7
+ import tempfile
8
+ import shutil
9
+ import time
10
+ import uuid
11
+ from datetime import datetime
12
+
13
+ # Configure logging
14
+ logging.basicConfig(level=logging.INFO)
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Try to import ReportLab (needed for PDF generation)
18
+ try:
19
+ from reportlab.lib.pagesizes import letter
20
+ from reportlab.lib import colors
21
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
22
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
23
+ REPORTLAB_AVAILABLE = True
24
+ except ImportError:
25
+ logger.warning("ReportLab library not available - PDF export will be disabled")
26
+ REPORTLAB_AVAILABLE = False
27
+
28
+ # AWS credentials for Bedrock API
29
+ AWS_ACCESS_KEY = os.getenv("AWS_ACCESS_KEY", "")
30
+ AWS_SECRET_KEY = os.getenv("AWS_SECRET_KEY", "")
31
+ AWS_REGION = os.getenv("AWS_REGION", "us-east-1")
32
+
33
+ # Initialize AWS clients if credentials are available
34
+ bedrock_client = None
35
+ transcribe_client = None
36
+ s3_client = None
37
+
38
+ if AWS_ACCESS_KEY and AWS_SECRET_KEY:
39
+ try:
40
+ # Initialize Bedrock client for AI analysis
41
+ bedrock_client = boto3.client(
42
+ 'bedrock-runtime',
43
+ aws_access_key_id=AWS_ACCESS_KEY,
44
+ aws_secret_access_key=AWS_SECRET_KEY,
45
+ region_name=AWS_REGION
46
+ )
47
+ logger.info("Bedrock client initialized successfully")
48
+
49
+ # Initialize Transcribe client for speech-to-text
50
+ transcribe_client = boto3.client(
51
+ 'transcribe',
52
+ aws_access_key_id=AWS_ACCESS_KEY,
53
+ aws_secret_access_key=AWS_SECRET_KEY,
54
+ region_name=AWS_REGION
55
+ )
56
+ logger.info("Transcribe client initialized successfully")
57
+
58
+ # Initialize S3 client for storing audio files
59
+ s3_client = boto3.client(
60
+ 's3',
61
+ aws_access_key_id=AWS_ACCESS_KEY,
62
+ aws_secret_access_key=AWS_SECRET_KEY,
63
+ region_name=AWS_REGION
64
+ )
65
+ logger.info("S3 client initialized successfully")
66
+ except Exception as e:
67
+ logger.error(f"Failed to initialize AWS clients: {str(e)}")
68
+
69
+ # S3 bucket for storing audio files
70
+ S3_BUCKET = os.environ.get("S3_BUCKET", "casl-audio-files")
71
+ S3_PREFIX = "transcribe-audio/"
72
+
73
+ # Create data directories if they don't exist
74
+ DATA_DIR = os.environ.get("DATA_DIR", "patient_data")
75
+ DOWNLOADS_DIR = os.path.join(DATA_DIR, "downloads")
76
+ AUDIO_DIR = os.path.join(DATA_DIR, "audio")
77
+
78
+ def ensure_data_dirs():
79
+ """Ensure data directories exist"""
80
+ global DOWNLOADS_DIR, AUDIO_DIR
81
+ try:
82
+ os.makedirs(DATA_DIR, exist_ok=True)
83
+ os.makedirs(DOWNLOADS_DIR, exist_ok=True)
84
+ os.makedirs(AUDIO_DIR, exist_ok=True)
85
+ logger.info(f"Data directories created: {DATA_DIR}, {DOWNLOADS_DIR}, {AUDIO_DIR}")
86
+ except Exception as e:
87
+ logger.warning(f"Could not create data directories: {str(e)}")
88
+ # Fallback to tmp directory on HF Spaces
89
+ DOWNLOADS_DIR = os.path.join(tempfile.gettempdir(), "casl_downloads")
90
+ AUDIO_DIR = os.path.join(tempfile.gettempdir(), "casl_audio")
91
+ os.makedirs(DOWNLOADS_DIR, exist_ok=True)
92
+ os.makedirs(AUDIO_DIR, exist_ok=True)
93
+ logger.info(f"Using fallback directories: {DOWNLOADS_DIR}, {AUDIO_DIR}")
94
+
95
+ # Initialize data directories
96
+ ensure_data_dirs()
97
+
98
+ # Sample transcript for the demo
99
+ SAMPLE_TRANSCRIPT = """*PAR: today I would &-um like to talk about &-um a fun trip I took last &-um summer with my family.
100
+ *PAR: we went to the &-um &-um beach [//] no to the mountains [//] I mean the beach actually.
101
+ *PAR: there was lots of &-um &-um swimming and &-um sun.
102
+ *PAR: we [/] we stayed for &-um three no [//] four days in a &-um hotel near the water [: ocean] [*].
103
+ *PAR: my favorite part was &-um building &-um castles with sand.
104
+ *PAR: sometimes I forget [//] forgetted [: forgot] [*] what they call those things we built.
105
+ *PAR: my brother he [//] he helped me dig a big hole.
106
+ *PAR: we saw [/] saw fishies [: fish] [*] swimming in the water.
107
+ *PAR: sometimes I wonder [/] wonder where fishies [: fish] [*] go when it's cold.
108
+ *PAR: maybe they have [/] have houses under the water.
109
+ *PAR: after swimming we [//] I eat [: ate] [*] &-um ice cream with &-um chocolate things on top.
110
+ *PAR: what do you call those &-um &-um sprinkles! that's the word.
111
+ *PAR: my mom said to &-um that I could have &-um two scoops next time.
112
+ *PAR: I want to go back to the beach [/] beach next year."""
113
+
114
+ def read_cha_file(file_path):
115
+ """Read and parse a .cha transcript file"""
116
+ try:
117
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
118
+ content = f.read()
119
+
120
+ # Extract participant lines (starting with *PAR:)
121
+ par_lines = []
122
+ for line in content.splitlines():
123
+ if line.startswith('*PAR:'):
124
+ par_lines.append(line)
125
+
126
+ # If no PAR lines found, just return the whole content
127
+ if not par_lines:
128
+ return content
129
+
130
+ return '\n'.join(par_lines)
131
+
132
+ except Exception as e:
133
+ logger.error(f"Error reading CHA file: {str(e)}")
134
+ return ""
135
+
136
+ def process_upload(file):
137
+ """Process an uploaded file (PDF, text, or CHA)"""
138
+ if file is None:
139
+ return ""
140
+
141
+ file_path = file.name
142
+ if file_path.endswith('.pdf'):
143
+ # For PDF, we would need PyPDF2 or similar
144
+ return "PDF upload not supported in this simple version"
145
+ elif file_path.endswith('.cha'):
146
+ return read_cha_file(file_path)
147
+ else:
148
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
149
+ return f.read()
150
+
151
+ def call_bedrock(prompt, max_tokens=4096):
152
+ """Call the AWS Bedrock API to analyze text using Claude"""
153
+ if not bedrock_client:
154
+ return "AWS credentials not configured. Using demo response instead."
155
+
156
+ try:
157
+ body = json.dumps({
158
+ "anthropic_version": "bedrock-2023-05-31",
159
+ "max_tokens": max_tokens,
160
+ "messages": [
161
+ {
162
+ "role": "user",
163
+ "content": prompt
164
+ }
165
+ ],
166
+ "temperature": 0.3,
167
+ "top_p": 0.9
168
+ })
169
+
170
+ modelId = 'anthropic.claude-3-sonnet-20240229-v1:0'
171
+ response = bedrock_client.invoke_model(
172
+ body=body,
173
+ modelId=modelId,
174
+ accept='application/json',
175
+ contentType='application/json'
176
+ )
177
+ response_body = json.loads(response.get('body').read())
178
+ return response_body['content'][0]['text']
179
+ except Exception as e:
180
+ logger.error(f"Error in call_bedrock: {str(e)}")
181
+ return f"Error: {str(e)}"
182
+
183
+ def transcribe_audio(audio_path, patient_age=8):
184
+ """Transcribe an audio recording using Amazon Transcribe and format in CHAT format"""
185
+ if not os.path.exists(audio_path):
186
+ logger.error(f"Audio file not found: {audio_path}")
187
+ return "Error: Audio file not found."
188
+
189
+ if not transcribe_client or not s3_client:
190
+ logger.warning("AWS clients not initialized, using demo transcription")
191
+ return generate_demo_transcription()
192
+
193
+ try:
194
+ # Get file info
195
+ file_name = os.path.basename(audio_path)
196
+ file_size = os.path.getsize(audio_path)
197
+ _, file_extension = os.path.splitext(file_name)
198
+
199
+ # Check file format
200
+ supported_formats = ['.mp3', '.mp4', '.wav', '.flac', '.ogg', '.amr', '.webm']
201
+ if file_extension.lower() not in supported_formats:
202
+ logger.error(f"Unsupported audio format: {file_extension}")
203
+ return f"Error: Unsupported audio format. Please use one of: {', '.join(supported_formats)}"
204
+
205
+ # Generate a unique job name
206
+ timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
207
+ job_name = f"casl-transcription-{timestamp}"
208
+ s3_key = f"{S3_PREFIX}{job_name}{file_extension}"
209
+
210
+ # Upload to S3
211
+ logger.info(f"Uploading {file_name} to S3 bucket {S3_BUCKET}")
212
+ try:
213
+ with open(audio_path, 'rb') as audio_file:
214
+ s3_client.upload_fileobj(audio_file, S3_BUCKET, s3_key)
215
+ except Exception as e:
216
+ logger.error(f"Failed to upload to S3: {str(e)}")
217
+
218
+ # If upload fails, try to create the bucket
219
+ try:
220
+ s3_client.create_bucket(Bucket=S3_BUCKET)
221
+ logger.info(f"Created S3 bucket: {S3_BUCKET}")
222
+
223
+ # Try upload again
224
+ with open(audio_path, 'rb') as audio_file:
225
+ s3_client.upload_fileobj(audio_file, S3_BUCKET, s3_key)
226
+ except Exception as bucket_error:
227
+ logger.error(f"Failed to create bucket and upload: {str(bucket_error)}")
228
+ return "Error: Failed to upload audio file. Please check your AWS permissions."
229
+
230
+ # Start transcription job
231
+ logger.info(f"Starting transcription job: {job_name}")
232
+ media_format = file_extension.lower()[1:] # Remove the dot
233
+ if media_format == 'webm':
234
+ media_format = 'webm' # Amazon Transcribe expects this
235
+
236
+ # Determine language settings based on patient age
237
+ if patient_age < 10:
238
+ # For younger children, enabling child language model is helpful
239
+ language_options = {
240
+ 'LanguageCode': 'en-US',
241
+ 'Settings': {
242
+ 'LanguageModelName': 'ChildLanguage'
243
+ }
244
+ }
245
+ else:
246
+ language_options = {
247
+ 'LanguageCode': 'en-US'
248
+ }
249
+
250
+ transcribe_client.start_transcription_job(
251
+ TranscriptionJobName=job_name,
252
+ Media={
253
+ 'MediaFileUri': f"s3://{S3_BUCKET}/{s3_key}"
254
+ },
255
+ MediaFormat=media_format,
256
+ **language_options,
257
+ Settings={
258
+ 'ShowSpeakerLabels': True,
259
+ 'MaxSpeakerLabels': 2 # Typically patient + clinician
260
+ }
261
+ )
262
+
263
+ # Wait for the job to complete (with timeout)
264
+ logger.info("Waiting for transcription to complete...")
265
+ max_tries = 30 # 5 minutes max wait
266
+ tries = 0
267
+
268
+ while tries < max_tries:
269
+ try:
270
+ job = transcribe_client.get_transcription_job(TranscriptionJobName=job_name)
271
+ status = job['TranscriptionJob']['TranscriptionJobStatus']
272
+
273
+ if status == 'COMPLETED':
274
+ # Get the transcript
275
+ transcript_uri = job['TranscriptionJob']['Transcript']['TranscriptFileUri']
276
+
277
+ # Download the transcript
278
+ import urllib.request
279
+ import json
280
+
281
+ with urllib.request.urlopen(transcript_uri) as response:
282
+ transcript_json = json.loads(response.read().decode('utf-8'))
283
+
284
+ # Convert to CHAT format
285
+ chat_transcript = format_as_chat(transcript_json)
286
+ return chat_transcript
287
+
288
+ elif status == 'FAILED':
289
+ reason = job['TranscriptionJob'].get('FailureReason', 'Unknown failure')
290
+ logger.error(f"Transcription job failed: {reason}")
291
+ return f"Error: Transcription failed - {reason}"
292
+
293
+ # Still in progress, wait and try again
294
+ tries += 1
295
+ time.sleep(10) # Check every 10 seconds
296
+
297
+ except Exception as e:
298
+ logger.error(f"Error checking transcription job: {str(e)}")
299
+ return f"Error getting transcription: {str(e)}"
300
+
301
+ # If we got here, we timed out
302
+ return "Error: Transcription timed out. The process is taking longer than expected."
303
+
304
+ except Exception as e:
305
+ logger.exception("Error in audio transcription")
306
+ return f"Error transcribing audio: {str(e)}"
307
+
308
+ def format_as_chat(transcript_json):
309
+ """Format the Amazon Transcribe JSON result as CHAT format"""
310
+ try:
311
+ # Get transcript items
312
+ items = transcript_json['results']['items']
313
+
314
+ # Get speaker labels if available
315
+ speakers = {}
316
+ if 'speaker_labels' in transcript_json['results']:
317
+ speaker_segments = transcript_json['results']['speaker_labels']['segments']
318
+
319
+ # Map each item to its speaker
320
+ for segment in speaker_segments:
321
+ for item in segment['items']:
322
+ start_time = item['start_time']
323
+ speakers[start_time] = segment['speaker_label']
324
+
325
+ # Build transcript by combining words into utterances by speaker
326
+ current_speaker = None
327
+ current_utterance = []
328
+ utterances = []
329
+
330
+ for item in items:
331
+ # Skip non-pronunciation items (like punctuation)
332
+ if item['type'] != 'pronunciation':
333
+ continue
334
+
335
+ word = item['alternatives'][0]['content']
336
+ start_time = item.get('start_time')
337
+
338
+ # Determine speaker if available
339
+ speaker = speakers.get(start_time, 'spk_0')
340
+
341
+ # If speaker changed, start a new utterance
342
+ if speaker != current_speaker and current_utterance:
343
+ utterances.append((current_speaker, ' '.join(current_utterance)))
344
+ current_utterance = []
345
+
346
+ current_speaker = speaker
347
+ current_utterance.append(word)
348
+
349
+ # Add the last utterance
350
+ if current_utterance:
351
+ utterances.append((current_speaker, ' '.join(current_utterance)))
352
+
353
+ # Format as CHAT
354
+ chat_lines = []
355
+ for speaker, text in utterances:
356
+ # Map speakers to CHAT format
357
+ # Assuming spk_0 is the patient (PAR) and spk_1 is the clinician (INV)
358
+ chat_speaker = "*PAR:" if speaker == "spk_0" else "*INV:"
359
+ chat_lines.append(f"{chat_speaker} {text}.")
360
+
361
+ return '\n'.join(chat_lines)
362
+
363
+ except Exception as e:
364
+ logger.exception("Error formatting transcript")
365
+ return "*PAR: (Error formatting transcript)"
366
+
367
+ def generate_demo_transcription():
368
+ """Generate a simulated transcription response"""
369
+ return """*PAR: today I want to tell you about my favorite toy.
370
+ *PAR: it's a &-um teddy bear that I got for my birthday.
371
+ *PAR: he has &-um brown fur and a red bow.
372
+ *PAR: I like to sleep with him every night.
373
+ *PAR: sometimes I take him to school in my backpack.
374
+ *INV: what's your teddy bear's name?
375
+ *PAR: his name is &-um Brownie because he's brown."""
376
+
377
+ def generate_demo_response(prompt):
378
+ """Generate a response using Bedrock if available, otherwise return a demo response"""
379
+ # This function will attempt to call Bedrock, and only fall back to the demo response
380
+ # if Bedrock is not available or fails
381
+
382
+ # Try to call Bedrock first if client is available
383
+ if bedrock_client:
384
+ try:
385
+ return call_bedrock(prompt)
386
+ except Exception as e:
387
+ logger.error(f"Error calling Bedrock: {str(e)}")
388
+ logger.info("Falling back to demo response")
389
+ # Continue to fallback response if Bedrock call fails
390
+
391
+ # Fallback demo response
392
+ logger.warning("Using demo response - Bedrock client not available or call failed")
393
+ return """<SPEECH_FACTORS_START>
394
+ Difficulty producing fluent speech: 8, 65
395
+ Examples:
396
+ - "today I would &-um like to talk about &-um a fun trip I took last &-um summer with my family"
397
+ - "we went to the &-um &-um beach [//] no to the mountains [//] I mean the beach actually"
398
+
399
+ Word retrieval issues: 6, 72
400
+ Examples:
401
+ - "what do you call those &-um &-um sprinkles! that's the word"
402
+ - "sometimes I forget [//] forgetted [: forgot] [*] what they call those things we built"
403
+
404
+ Grammatical errors: 4, 58
405
+ Examples:
406
+ - "after swimming we [//] I eat [: ate] [*] &-um ice cream"
407
+ - "sometimes I forget [//] forgetted [: forgot] [*] what they call those things we built"
408
+
409
+ Repetitions and revisions: 5, 62
410
+ Examples:
411
+ - "we [/] we stayed for &-um three no [//] four days"
412
+ - "we went to the &-um &-um beach [//] no to the mountains [//] I mean the beach actually"
413
+ <SPEECH_FACTORS_END>
414
+
415
+ <CASL_SKILLS_START>
416
+ Lexical/Semantic Skills: Standard Score (92), Percentile Rank (30%), Average Performance
417
+ Examples:
418
+ - "what do you call those &-um &-um sprinkles! that's the word"
419
+ - "we went to the &-um &-um beach [//] no to the mountains [//] I mean the beach actually"
420
+
421
+ Syntactic Skills: Standard Score (87), Percentile Rank (19%), Low Average Performance
422
+ Examples:
423
+ - "my brother he [//] he helped me dig a big hole"
424
+ - "after swimming we [//] I eat [: ate] [*] &-um ice cream with &-um chocolate things on top"
425
+
426
+ Supralinguistic Skills: Standard Score (90), Percentile Rank (25%), Average Performance
427
+ Examples:
428
+ - "sometimes I wonder [/] wonder where fishies [: fish] [*] go when it's cold"
429
+ - "maybe they have [/] have houses under the water"
430
+ <CASL_SKILLS_END>
431
+
432
+ <TREATMENT_RECOMMENDATIONS_START>
433
+ - Implement word-finding strategies with semantic cuing focused on everyday objects and activities, using the patient's beach experience as a context (e.g., "sprinkles," "castles")
434
+ - Practice structured narrative tasks with visual supports to reduce revisions and improve sequencing
435
+ - Use sentence formulation exercises focusing on verb tense consistency (addressing errors like "forgetted" and "eat" for "ate")
436
+ - Incorporate self-monitoring techniques to help identify and correct grammatical errors
437
+ - Work on increasing vocabulary specificity (e.g., "things on top" to "sprinkles")
438
+ <TREATMENT_RECOMMENDATIONS_END>
439
+
440
+ <EXPLANATION_START>
441
+ This child demonstrates moderate word-finding difficulties with compensatory strategies including fillers ("&-um") and repetitions. The frequent use of self-corrections shows good metalinguistic awareness, but the pauses and repairs impact conversational fluency. Syntactic errors primarily involve verb tense inconsistency. Overall, the pattern suggests a mild-to-moderate language disorder with stronger receptive than expressive skills.
442
+ <EXPLANATION_END>
443
+
444
+ <ADDITIONAL_ANALYSIS_START>
445
+ The child shows relative strengths in maintaining topic coherence and conveying a complete narrative structure despite the language challenges. The pattern of errors suggests that word-finding difficulties and processing speed are primary concerns rather than conceptual or cognitive issues. Semantic network activities that strengthen word associations would likely be beneficial, particularly when paired with visual supports.
446
+ <ADDITIONAL_ANALYSIS_END>
447
+
448
+ <DIAGNOSTIC_IMPRESSIONS_START>
449
+ Based on the language sample, this child presents with a profile consistent with a mild-to-moderate expressive language disorder. The most prominent features include:
450
+
451
+ 1. Word-finding difficulties characterized by fillers, pauses, and self-corrections when attempting to retrieve specific vocabulary
452
+ 2. Grammatical challenges primarily affecting verb tense consistency and morphological markers
453
+ 3. Relatively intact narrative structure and topic maintenance
454
+
455
+ These findings suggest intervention should focus on word retrieval strategies, grammatical form practice, and continued support for narrative development, with an emphasis on fluency and self-monitoring.
456
+ <DIAGNOSTIC_IMPRESSIONS_END>
457
+
458
+ <ERROR_EXAMPLES_START>
459
+ Word-finding difficulties:
460
+ - "what do you call those &-um &-um sprinkles! that's the word"
461
+ - "we went to the &-um &-um beach [//] no to the mountains [//] I mean the beach actually"
462
+ - "there was lots of &-um &-um swimming and &-um sun"
463
+
464
+ Grammatical errors:
465
+ - "after swimming we [//] I eat [: ate] [*] &-um ice cream"
466
+ - "sometimes I forget [//] forgetted [: forgot] [*] what they call those things we built"
467
+ - "we saw [/] saw fishies [: fish] [*] swimming in the water"
468
+
469
+ Repetitions and revisions:
470
+ - "we [/] we stayed for &-um three no [//] four days"
471
+ - "I want to go back to the beach [/] beach next year"
472
+ - "sometimes I wonder [/] wonder where fishies [: fish] [*] go when it's cold"
473
+ <ERROR_EXAMPLES_END>"""
474
+
475
+ def parse_casl_response(response):
476
+ """Parse the LLM response for CASL analysis into structured data"""
477
+ # Extract speech factors section using section markers
478
+ speech_factors_section = ""
479
+ factors_pattern = re.compile(r"<SPEECH_FACTORS_START>(.*?)<SPEECH_FACTORS_END>", re.DOTALL)
480
+ factors_match = factors_pattern.search(response)
481
+
482
+ if factors_match:
483
+ speech_factors_section = factors_match.group(1).strip()
484
+ else:
485
+ speech_factors_section = "Error extracting speech factors from analysis."
486
+
487
+ # Extract CASL skills section
488
+ casl_section = ""
489
+ casl_pattern = re.compile(r"<CASL_SKILLS_START>(.*?)<CASL_SKILLS_END>", re.DOTALL)
490
+ casl_match = casl_pattern.search(response)
491
+
492
+ if casl_match:
493
+ casl_section = casl_match.group(1).strip()
494
+ else:
495
+ casl_section = "Error extracting CASL skills from analysis."
496
+
497
+ # Extract treatment recommendations
498
+ treatment_text = ""
499
+ treatment_pattern = re.compile(r"<TREATMENT_RECOMMENDATIONS_START>(.*?)<TREATMENT_RECOMMENDATIONS_END>", re.DOTALL)
500
+ treatment_match = treatment_pattern.search(response)
501
+
502
+ if treatment_match:
503
+ treatment_text = treatment_match.group(1).strip()
504
+ else:
505
+ treatment_text = "Error extracting treatment recommendations from analysis."
506
+
507
+ # Extract explanation section
508
+ explanation_text = ""
509
+ explanation_pattern = re.compile(r"<EXPLANATION_START>(.*?)<EXPLANATION_END>", re.DOTALL)
510
+ explanation_match = explanation_pattern.search(response)
511
+
512
+ if explanation_match:
513
+ explanation_text = explanation_match.group(1).strip()
514
+ else:
515
+ explanation_text = "Error extracting clinical explanation from analysis."
516
+
517
+ # Extract additional analysis
518
+ additional_analysis = ""
519
+ additional_pattern = re.compile(r"<ADDITIONAL_ANALYSIS_START>(.*?)<ADDITIONAL_ANALYSIS_END>", re.DOTALL)
520
+ additional_match = additional_pattern.search(response)
521
+
522
+ if additional_match:
523
+ additional_analysis = additional_match.group(1).strip()
524
+
525
+ # Extract diagnostic impressions
526
+ diagnostic_impressions = ""
527
+ diagnostic_pattern = re.compile(r"<DIAGNOSTIC_IMPRESSIONS_START>(.*?)<DIAGNOSTIC_IMPRESSIONS_END>", re.DOTALL)
528
+ diagnostic_match = diagnostic_pattern.search(response)
529
+
530
+ if diagnostic_match:
531
+ diagnostic_impressions = diagnostic_match.group(1).strip()
532
+
533
+ # Extract specific error examples
534
+ specific_errors_text = ""
535
+ errors_pattern = re.compile(r"<ERROR_EXAMPLES_START>(.*?)<ERROR_EXAMPLES_END>", re.DOTALL)
536
+ errors_match = errors_pattern.search(response)
537
+
538
+ if errors_match:
539
+ specific_errors_text = errors_match.group(1).strip()
540
+
541
+ # Create full report text
542
+ full_report = f"""
543
+ ## Speech Factors Analysis
544
+
545
+ {speech_factors_section}
546
+
547
+ ## CASL Skills Assessment
548
+
549
+ {casl_section}
550
+
551
+ ## Treatment Recommendations
552
+
553
+ {treatment_text}
554
+
555
+ ## Clinical Explanation
556
+
557
+ {explanation_text}
558
+ """
559
+
560
+ if additional_analysis:
561
+ full_report += f"\n## Additional Analysis\n\n{additional_analysis}"
562
+
563
+ if diagnostic_impressions:
564
+ full_report += f"\n## Diagnostic Impressions\n\n{diagnostic_impressions}"
565
+
566
+ if specific_errors_text:
567
+ full_report += f"\n## Detailed Error Examples\n\n{specific_errors_text}"
568
+
569
+ return {
570
+ 'speech_factors': speech_factors_section,
571
+ 'casl_data': casl_section,
572
+ 'treatment_suggestions': treatment_text,
573
+ 'explanation': explanation_text,
574
+ 'additional_analysis': additional_analysis,
575
+ 'diagnostic_impressions': diagnostic_impressions,
576
+ 'specific_errors': specific_errors_text,
577
+ 'full_report': full_report,
578
+ 'raw_response': response
579
+ }
580
+
581
+ def analyze_transcript(transcript, age, gender):
582
+ """Analyze a speech transcript using Claude"""
583
+ # CASL-2 assessment cheat sheet
584
+ cheat_sheet = """
585
+ # Speech-Language Pathologist Analysis Cheat Sheet
586
+
587
+ ## Types of Speech Patterns to Identify:
588
+
589
+ 1. Difficulty producing fluent, grammatical speech
590
+ - Fillers (um, uh) and pauses
591
+ - False starts and revisions
592
+ - Incomplete sentences
593
+
594
+ 2. Word retrieval issues
595
+ - Pauses before content words
596
+ - Circumlocutions (talking around a word)
597
+ - Word substitutions
598
+
599
+ 3. Grammatical errors
600
+ - Verb tense inconsistencies
601
+ - Subject-verb agreement errors
602
+ - Morphological errors (plurals, possessives)
603
+
604
+ 4. Repetitions and revisions
605
+ - Word or phrase repetitions [/]
606
+ - Self-corrections [//]
607
+ - Retracing
608
+
609
+ 5. Neologisms
610
+ - Made-up words
611
+ - Word blends
612
+
613
+ 6. Perseveration
614
+ - Inappropriate repetition of ideas
615
+ - Recurring themes
616
+
617
+ 7. Comprehension issues
618
+ - Topic maintenance difficulties
619
+ - Non-sequiturs
620
+ - Inappropriate responses
621
+ """
622
+
623
+ # Instructions for the analysis
624
+ instructions = """
625
+ Analyze this speech transcript to identify specific patterns and provide a detailed CASL-2 (Comprehensive Assessment of Spoken Language) assessment.
626
+
627
+ For each speech pattern you identify:
628
+ 1. Count the occurrences in the transcript
629
+ 2. Estimate a percentile (how typical/atypical this is for the age)
630
+ 3. Provide DIRECT QUOTES from the transcript as evidence
631
+
632
+ Then assess the following CASL-2 domains:
633
+
634
+ 1. Lexical/Semantic Skills:
635
+ - Assess vocabulary diversity, word-finding abilities, semantic precision
636
+ - Provide Standard Score (mean=100, SD=15), percentile rank, and performance level
637
+ - Include SPECIFIC QUOTES as evidence
638
+
639
+ 2. Syntactic Skills:
640
+ - Evaluate grammatical accuracy, sentence complexity, morphological skills
641
+ - Provide Standard Score, percentile rank, and performance level
642
+ - Include SPECIFIC QUOTES as evidence
643
+
644
+ 3. Supralinguistic Skills:
645
+ - Assess figurative language use, inferencing, and abstract reasoning
646
+ - Provide Standard Score, percentile rank, and performance level
647
+ - Include SPECIFIC QUOTES as evidence
648
+
649
+ YOUR RESPONSE MUST USE THESE EXACT SECTION MARKERS FOR PARSING:
650
+
651
+ <SPEECH_FACTORS_START>
652
+ Difficulty producing fluent, grammatical speech: (occurrences), (percentile)
653
+ Examples:
654
+ - "(direct quote from transcript)"
655
+ - "(direct quote from transcript)"
656
+
657
+ Word retrieval issues: (occurrences), (percentile)
658
+ Examples:
659
+ - "(direct quote from transcript)"
660
+ - "(direct quote from transcript)"
661
+
662
+ (And so on for each factor)
663
+ <SPEECH_FACTORS_END>
664
+
665
+ <CASL_SKILLS_START>
666
+ Lexical/Semantic Skills: Standard Score (X), Percentile Rank (X%), Performance Level
667
+ Examples:
668
+ - "(direct quote showing strength or weakness)"
669
+ - "(direct quote showing strength or weakness)"
670
+
671
+ Syntactic Skills: Standard Score (X), Percentile Rank (X%), Performance Level
672
+ Examples:
673
+ - "(direct quote showing strength or weakness)"
674
+ - "(direct quote showing strength or weakness)"
675
+
676
+ Supralinguistic Skills: Standard Score (X), Percentile Rank (X%), Performance Level
677
+ Examples:
678
+ - "(direct quote showing strength or weakness)"
679
+ - "(direct quote showing strength or weakness)"
680
+ <CASL_SKILLS_END>
681
+
682
+ <TREATMENT_RECOMMENDATIONS_START>
683
+ - (treatment recommendation)
684
+ - (treatment recommendation)
685
+ - (treatment recommendation)
686
+ <TREATMENT_RECOMMENDATIONS_END>
687
+
688
+ <EXPLANATION_START>
689
+ (brief diagnostic rationale based on findings)
690
+ <EXPLANATION_END>
691
+
692
+ <ADDITIONAL_ANALYSIS_START>
693
+ (specific insights that would be helpful for treatment planning)
694
+ <ADDITIONAL_ANALYSIS_END>
695
+
696
+ <DIAGNOSTIC_IMPRESSIONS_START>
697
+ (summarize findings across domains using specific examples and clear explanations)
698
+ <DIAGNOSTIC_IMPRESSIONS_END>
699
+
700
+ <ERROR_EXAMPLES_START>
701
+ (Copy all the specific quote examples here again, organized by error type or skill domain)
702
+ <ERROR_EXAMPLES_END>
703
+
704
+ MOST IMPORTANT:
705
+ 1. Use EXACTLY the section markers provided (like <SPEECH_FACTORS_START>) to make parsing reliable
706
+ 2. For EVERY factor and domain you analyze, you MUST provide direct quotes from the transcript as evidence
707
+ 3. Be very specific and cite the exact text
708
+ 4. Do not omit any of the required sections
709
+ """
710
+
711
+ # Prepare prompt for Claude with the user's role context
712
+ role_context = """
713
+ You are a speech pathologist, a healthcare professional who specializes in evaluating, diagnosing, and treating communication disorders, including speech, language, cognitive-communication, voice, swallowing, and fluency disorders. Your role is to help patients improve their speech and communication skills through various therapeutic techniques and exercises.
714
+
715
+ You are working with a student with speech impediments.
716
+
717
+ The most important thing is that you stay kind to the child. Be constructive and helpful rather than critical.
718
+ """
719
+
720
+ prompt = f"""
721
+ {role_context}
722
+
723
+ You are analyzing a transcript for a patient who is {age} years old and {gender}.
724
+
725
+ TRANSCRIPT:
726
+ {transcript}
727
+
728
+ {cheat_sheet}
729
+
730
+ {instructions}
731
+
732
+ Remember to be precise but compassionate in your analysis. Use direct quotes from the transcript for every factor and domain you analyze.
733
+ """
734
+
735
+ # Call the appropriate API or fallback to demo mode
736
+ if bedrock_client:
737
+ response = call_bedrock(prompt)
738
+ else:
739
+ response = generate_demo_response(prompt)
740
+
741
+ # Parse the response
742
+ results = parse_casl_response(response)
743
+
744
+ return results
745
+
746
+ def export_pdf(results, patient_name="", record_id="", age="", gender="", assessment_date="", clinician=""):
747
+ """Export analysis results to a PDF report"""
748
+ global DOWNLOADS_DIR
749
+
750
+ # Check if ReportLab is available
751
+ if not REPORTLAB_AVAILABLE:
752
+ return "ERROR: PDF export is not available - ReportLab library is not installed. Please run 'pip install reportlab'."
753
+
754
+ try:
755
+ # Generate a safe filename
756
+ if patient_name:
757
+ safe_name = f"{patient_name.replace(' ', '_')}"
758
+ else:
759
+ safe_name = f"speech_analysis_{datetime.now().strftime('%Y%m%d%H%M%S')}"
760
+
761
+ # Make sure the downloads directory exists
762
+ try:
763
+ os.makedirs(DOWNLOADS_DIR, exist_ok=True)
764
+ except Exception as e:
765
+ logger.warning(f"Could not access downloads directory: {str(e)}")
766
+ # Fallback to temp directory
767
+ DOWNLOADS_DIR = os.path.join(tempfile.gettempdir(), "casl_downloads")
768
+ os.makedirs(DOWNLOADS_DIR, exist_ok=True)
769
+
770
+ # Create the PDF path in our downloads directory
771
+ pdf_path = os.path.join(DOWNLOADS_DIR, f"{safe_name}.pdf")
772
+
773
+ # Create the PDF document
774
+ doc = SimpleDocTemplate(pdf_path, pagesize=letter)
775
+ styles = getSampleStyleSheet()
776
+
777
+ # Create enhanced custom styles
778
+ styles.add(ParagraphStyle(
779
+ name='Heading1',
780
+ parent=styles['Heading1'],
781
+ fontSize=16,
782
+ spaceAfter=12,
783
+ textColor=colors.navy
784
+ ))
785
+
786
+ styles.add(ParagraphStyle(
787
+ name='Heading2',
788
+ parent=styles['Heading2'],
789
+ fontSize=14,
790
+ spaceAfter=10,
791
+ spaceBefore=10,
792
+ textColor=colors.darkblue
793
+ ))
794
+
795
+ styles.add(ParagraphStyle(
796
+ name='Heading3',
797
+ parent=styles['Heading2'],
798
+ fontSize=12,
799
+ spaceAfter=8,
800
+ spaceBefore=8,
801
+ textColor=colors.darkblue
802
+ ))
803
+
804
+ styles.add(ParagraphStyle(
805
+ name='BodyText',
806
+ parent=styles['BodyText'],
807
+ fontSize=11,
808
+ spaceAfter=8,
809
+ leading=14
810
+ ))
811
+
812
+ styles.add(ParagraphStyle(
813
+ name='BulletPoint',
814
+ parent=styles['BodyText'],
815
+ fontSize=11,
816
+ leftIndent=20,
817
+ firstLineIndent=-15,
818
+ spaceAfter=4,
819
+ leading=14
820
+ ))
821
+
822
+ # Convert markdown to PDF elements
823
+ story = []
824
+
825
+ # Add title and date
826
+ story.append(Paragraph("Speech Language Assessment Report", styles['Title']))
827
+ story.append(Spacer(1, 12))
828
+
829
+ # Add patient information table
830
+ if patient_name or record_id or age or gender:
831
+ # Prepare patient info data
832
+ data = []
833
+ if patient_name:
834
+ data.append(["Patient Name:", patient_name])
835
+ if record_id:
836
+ data.append(["Record ID:", record_id])
837
+ if age:
838
+ data.append(["Age:", f"{age} years"])
839
+ if gender:
840
+ data.append(["Gender:", gender])
841
+ if assessment_date:
842
+ data.append(["Assessment Date:", assessment_date])
843
+ if clinician:
844
+ data.append(["Clinician:", clinician])
845
+
846
+ if data:
847
+ # Create a table with the data
848
+ patient_table = Table(data, colWidths=[120, 350])
849
+ patient_table.setStyle(TableStyle([
850
+ ('BACKGROUND', (0, 0), (0, -1), colors.lightgrey),
851
+ ('TEXTCOLOR', (0, 0), (0, -1), colors.darkblue),
852
+ ('ALIGN', (0, 0), (0, -1), 'RIGHT'),
853
+ ('ALIGN', (1, 0), (1, -1), 'LEFT'),
854
+ ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
855
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
856
+ ('TOPPADDING', (0, 0), (-1, -1), 6),
857
+ ('GRID', (0, 0), (-1, -1), 0.5, colors.lightgrey),
858
+ ]))
859
+ story.append(patient_table)
860
+ story.append(Spacer(1, 12))
861
+
862
+ # Add clinical analysis sections
863
+ story.append(Paragraph("Speech Factors Analysis", styles['Heading1']))
864
+ speech_factors_paragraphs = []
865
+ for line in results['speech_factors'].split('\n'):
866
+ line = line.strip()
867
+ if not line:
868
+ continue
869
+ if line.startswith('- '):
870
+ story.append(Paragraph(f"β€’ {line[2:]}", styles['BulletPoint']))
871
+ else:
872
+ story.append(Paragraph(line, styles['BodyText']))
873
+ story.append(Spacer(1, 12))
874
+
875
+ story.append(Paragraph("CASL Skills Assessment", styles['Heading1']))
876
+ for line in results['casl_data'].split('\n'):
877
+ line = line.strip()
878
+ if not line:
879
+ continue
880
+ if line.startswith('- '):
881
+ story.append(Paragraph(f"β€’ {line[2:]}", styles['BulletPoint']))
882
+ else:
883
+ story.append(Paragraph(line, styles['BodyText']))
884
+ story.append(Spacer(1, 12))
885
+
886
+ story.append(Paragraph("Treatment Recommendations", styles['Heading1']))
887
+
888
+ # Process treatment recommendations as bullet points
889
+ for line in results['treatment_suggestions'].split('\n'):
890
+ line = line.strip()
891
+ if not line:
892
+ continue
893
+ if line.startswith('- '):
894
+ story.append(Paragraph(f"β€’ {line[2:]}", styles['BulletPoint']))
895
+ else:
896
+ story.append(Paragraph(line, styles['BodyText']))
897
+
898
+ story.append(Spacer(1, 12))
899
+
900
+ story.append(Paragraph("Clinical Explanation", styles['Heading1']))
901
+ story.append(Paragraph(results['explanation'], styles['BodyText']))
902
+ story.append(Spacer(1, 12))
903
+
904
+ if results['additional_analysis']:
905
+ story.append(Paragraph("Additional Analysis", styles['Heading1']))
906
+ story.append(Paragraph(results['additional_analysis'], styles['BodyText']))
907
+ story.append(Spacer(1, 12))
908
+
909
+ if results['diagnostic_impressions']:
910
+ story.append(Paragraph("Diagnostic Impressions", styles['Heading1']))
911
+ story.append(Paragraph(results['diagnostic_impressions'], styles['BodyText']))
912
+ story.append(Spacer(1, 12))
913
+
914
+ # Add footer with date
915
+ footer_text = f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
916
+ story.append(Spacer(1, 20))
917
+ story.append(Paragraph(footer_text, ParagraphStyle(
918
+ name='Footer',
919
+ parent=styles['Normal'],
920
+ fontSize=8,
921
+ textColor=colors.grey
922
+ )))
923
+
924
+ # Build the PDF
925
+ doc.build(story)
926
+
927
+ logger.info(f"Report saved as PDF: {pdf_path}")
928
+ return pdf_path
929
+
930
+ except Exception as e:
931
+ logger.exception("Error creating PDF")
932
+ return f"Error creating PDF: {str(e)}"
933
+
934
+ def create_interface():
935
+ """Create the Gradio interface"""
936
+ # Set a theme compatible with Hugging Face Spaces
937
+ theme = gr.themes.Soft(
938
+ primary_hue="blue",
939
+ secondary_hue="indigo",
940
+ )
941
+
942
+ with gr.Blocks(title="Simple CASL Analysis Tool", theme=theme) as app:
943
+ gr.Markdown("# CASL Analysis Tool")
944
+ gr.Markdown("A simplified tool for analyzing speech transcripts and audio using CASL framework")
945
+
946
+ with gr.Tabs() as main_tabs:
947
+ # Analysis Tab
948
+ with gr.TabItem("Analysis", id=0):
949
+ with gr.Row():
950
+ with gr.Column(scale=1):
951
+ # Patient info
952
+ gr.Markdown("### Patient Information")
953
+ patient_name = gr.Textbox(label="Patient Name", placeholder="Enter patient name")
954
+ record_id = gr.Textbox(label="Record ID", placeholder="Enter record ID")
955
+
956
+ with gr.Row():
957
+ age = gr.Number(label="Age", value=8, minimum=1, maximum=120)
958
+ gender = gr.Radio(["male", "female", "other"], label="Gender", value="male")
959
+
960
+ assessment_date = gr.Textbox(
961
+ label="Assessment Date",
962
+ placeholder="MM/DD/YYYY",
963
+ value=datetime.now().strftime('%m/%d/%Y')
964
+ )
965
+ clinician_name = gr.Textbox(label="Clinician", placeholder="Enter clinician name")
966
+
967
+ # Transcript input
968
+ gr.Markdown("### Transcript")
969
+ sample_btn = gr.Button("Load Sample Transcript")
970
+ file_upload = gr.File(label="Upload transcript file (.txt or .cha)")
971
+ transcript = gr.Textbox(
972
+ label="Speech transcript (CHAT format preferred)",
973
+ placeholder="Enter transcript text or upload a file...",
974
+ lines=10
975
+ )
976
+
977
+ # Analysis button
978
+ analyze_btn = gr.Button("Analyze Transcript", variant="primary")
979
+
980
+ with gr.Column(scale=1):
981
+ # Results display
982
+ gr.Markdown("### Analysis Results")
983
+
984
+ analysis_output = gr.Markdown(label="Full Analysis")
985
+
986
+ # PDF export (only shown if ReportLab is available)
987
+ export_status = gr.Markdown("")
988
+ if REPORTLAB_AVAILABLE:
989
+ export_btn = gr.Button("Export as PDF", variant="secondary")
990
+ else:
991
+ gr.Markdown("⚠️ PDF export is disabled - ReportLab library is not installed")
992
+
993
+ # Transcription Tab
994
+ with gr.TabItem("Transcription", id=1):
995
+ with gr.Row():
996
+ with gr.Column(scale=1):
997
+ gr.Markdown("### Audio Transcription")
998
+ gr.Markdown("Upload an audio recording to automatically transcribe it in CHAT format")
999
+
1000
+ # Patient's age helps with transcription accuracy
1001
+ transcription_age = gr.Number(label="Patient Age", value=8, minimum=1, maximum=120,
1002
+ info="For children under 10, special language models may be used")
1003
+
1004
+ # Audio input
1005
+ audio_input = gr.Audio(type="filepath", label="Upload Audio Recording",
1006
+ elem_id="audio-input")
1007
+
1008
+ # Transcribe button
1009
+ transcribe_btn = gr.Button("Transcribe Audio", variant="primary")
1010
+
1011
+ with gr.Column(scale=1):
1012
+ # Transcription output
1013
+ transcription_output = gr.Textbox(
1014
+ label="Transcription Result",
1015
+ placeholder="Transcription will appear here...",
1016
+ lines=12
1017
+ )
1018
+
1019
+ with gr.Row():
1020
+ # Button to use transcription in analysis
1021
+ copy_to_analysis_btn = gr.Button("Use for Analysis", variant="secondary")
1022
+
1023
+ # Status/info message
1024
+ transcription_status = gr.Markdown("")
1025
+
1026
+ # Load sample transcript button
1027
+ def load_sample():
1028
+ return SAMPLE_TRANSCRIPT
1029
+
1030
+ sample_btn.click(load_sample, outputs=[transcript])
1031
+
1032
+ # File upload handler
1033
+ file_upload.upload(process_upload, file_upload, transcript)
1034
+
1035
+ # Analysis button handler
1036
+ def on_analyze_click(transcript_text, age_val, gender_val, patient_name_val, record_id_val, clinician_val, assessment_date_val):
1037
+ if not transcript_text or len(transcript_text.strip()) < 50:
1038
+ return "Error: Please provide a longer transcript for analysis."
1039
+
1040
+ try:
1041
+ # Get the analysis results
1042
+ results = analyze_transcript(transcript_text, age_val, gender_val)
1043
+
1044
+ # Return the full report
1045
+ return results['full_report']
1046
+
1047
+ except Exception as e:
1048
+ logger.exception("Error during analysis")
1049
+ return f"Error during analysis: {str(e)}"
1050
+
1051
+ analyze_btn.click(
1052
+ on_analyze_click,
1053
+ inputs=[
1054
+ transcript, age, gender,
1055
+ patient_name, record_id, clinician_name, assessment_date
1056
+ ],
1057
+ outputs=[analysis_output]
1058
+ )
1059
+
1060
+ # PDF export function
1061
+ def on_export_pdf(report_text, p_name, p_record_id, p_age, p_gender, p_date, p_clinician):
1062
+ # Check if ReportLab is available
1063
+ if not REPORTLAB_AVAILABLE:
1064
+ return "ERROR: PDF export is not available because the ReportLab library is not installed. Please install it with 'pip install reportlab'."
1065
+
1066
+ if not report_text or len(report_text.strip()) < 50:
1067
+ return "Error: Please run the analysis first before exporting to PDF."
1068
+
1069
+ try:
1070
+ # Parse the report text back into sections
1071
+ results = {
1072
+ 'speech_factors': '',
1073
+ 'casl_data': '',
1074
+ 'treatment_suggestions': '',
1075
+ 'explanation': '',
1076
+ 'additional_analysis': '',
1077
+ 'diagnostic_impressions': '',
1078
+ }
1079
+
1080
+ sections = report_text.split('##')
1081
+ for section in sections:
1082
+ section = section.strip()
1083
+ if not section:
1084
+ continue
1085
+
1086
+ title_content = section.split('\n', 1)
1087
+ if len(title_content) < 2:
1088
+ continue
1089
+
1090
+ title = title_content[0].strip()
1091
+ content = title_content[1].strip()
1092
+
1093
+ if "Speech Factors Analysis" in title:
1094
+ results['speech_factors'] = content
1095
+ elif "CASL Skills Assessment" in title:
1096
+ results['casl_data'] = content
1097
+ elif "Treatment Recommendations" in title:
1098
+ results['treatment_suggestions'] = content
1099
+ elif "Clinical Explanation" in title:
1100
+ results['explanation'] = content
1101
+ elif "Additional Analysis" in title:
1102
+ results['additional_analysis'] = content
1103
+ elif "Diagnostic Impressions" in title:
1104
+ results['diagnostic_impressions'] = content
1105
+
1106
+ pdf_path = export_pdf(
1107
+ results,
1108
+ patient_name=p_name,
1109
+ record_id=p_record_id,
1110
+ age=p_age,
1111
+ gender=p_gender,
1112
+ assessment_date=p_date,
1113
+ clinician=p_clinician
1114
+ )
1115
+
1116
+ # Check if the export was successful
1117
+ if pdf_path.startswith("ERROR:"):
1118
+ return pdf_path
1119
+
1120
+ # Make it downloadable in Hugging Face Spaces
1121
+ download_link = f'<a href="file={pdf_path}" download="{os.path.basename(pdf_path)}">Download PDF Report</a>'
1122
+ return f"Report saved as PDF: {pdf_path}<br>{download_link}"
1123
+ except Exception as e:
1124
+ logger.exception("Error exporting to PDF")
1125
+ return f"Error creating PDF: {str(e)}"
1126
+
1127
+ # Only set up the PDF export button if ReportLab is available
1128
+ if REPORTLAB_AVAILABLE:
1129
+ export_btn.click(
1130
+ on_export_pdf,
1131
+ inputs=[
1132
+ analysis_output,
1133
+ patient_name,
1134
+ record_id,
1135
+ age,
1136
+ gender,
1137
+ assessment_date,
1138
+ clinician_name
1139
+ ],
1140
+ outputs=[export_status]
1141
+ )
1142
+
1143
+ # Transcription button handler
1144
+ def on_transcribe_audio(audio_path, age_val):
1145
+ try:
1146
+ if not audio_path:
1147
+ return "Please upload an audio file to transcribe.", "Error: No audio file provided."
1148
+
1149
+ # Process the audio file with Amazon Transcribe
1150
+ transcription = transcribe_audio(audio_path, age_val)
1151
+
1152
+ # Return status message based on whether it's a demo or real transcription
1153
+ if not transcribe_client:
1154
+ status_msg = "⚠️ Demo mode: Using example transcription (AWS credentials not configured)"
1155
+ else:
1156
+ status_msg = "βœ… Transcription completed successfully"
1157
+
1158
+ return transcription, status_msg
1159
+ except Exception as e:
1160
+ logger.exception("Error transcribing audio")
1161
+ return f"Error: {str(e)}", f"❌ Transcription failed: {str(e)}"
1162
+
1163
+ # Connect the transcribe button to its handler
1164
+ transcribe_btn.click(
1165
+ on_transcribe_audio,
1166
+ inputs=[audio_input, transcription_age],
1167
+ outputs=[transcription_output, transcription_status]
1168
+ )
1169
+
1170
+ # Copy transcription to analysis tab
1171
+ def copy_to_analysis(transcription):
1172
+ return transcription, gr.update(selected=0) # Switch to Analysis tab
1173
+
1174
+ copy_to_analysis_btn.click(
1175
+ copy_to_analysis,
1176
+ inputs=[transcription_output],
1177
+ outputs=[transcript, main_tabs]
1178
+ )
1179
+
1180
+ return app
1181
+
1182
+ # Create requirements.txt file for HuggingFace Spaces
1183
+ def create_requirements_file():
1184
+ requirements = [
1185
+ "gradio>=4.0.0",
1186
+ "pandas",
1187
+ "numpy",
1188
+ "Pillow",
1189
+ "boto3>=1.28.0", # Required for AWS services
1190
+ "botocore>=1.31.0", # Required for AWS services
1191
+ "reportlab>=3.6.0" # Optional for PDF exports
1192
+ ]
1193
+
1194
+ with open("requirements.txt", "w") as f:
1195
+ for req in requirements:
1196
+ f.write(f"{req}\n")
1197
+
1198
+ if __name__ == "__main__":
1199
+ # Create requirements.txt for HuggingFace Spaces
1200
+ create_requirements_file()
1201
+
1202
+ # Check for AWS credentials
1203
+ if not AWS_ACCESS_KEY or not AWS_SECRET_KEY:
1204
+ print("NOTE: AWS credentials not found. The app will run in demo mode with simulated responses.")
1205
+ print("To enable full functionality, set AWS_ACCESS_KEY and AWS_SECRET_KEY environment variables.")
1206
+
1207
+ app = create_interface()
1208
+ app.launch(show_api=False) # Disable API tab for security
requirements.txt CHANGED
@@ -1,12 +1,9 @@
1
  gradio>=4.0.0
2
- pandas>=1.5.0
3
- numpy>=1.21.0
4
- matplotlib>=3.5.0
5
- seaborn>=0.11.0
6
- Pillow>=8.0.0
7
  reportlab>=3.6.0
8
- boto3>=1.28.0
9
- botocore>=1.31.0
10
- PyPDF2>=3.0.0
11
- SpeechRecognition>=3.8.1
12
  pydub>=0.25.0
 
1
  gradio>=4.0.0
2
+ pandas>=1.3.0
3
+ numpy>=1.20.0
4
+ matplotlib>=3.3.0
5
+ boto3>=1.20.0
 
6
  reportlab>=3.6.0
7
+ PyPDF2>=2.0.0
8
+ speech_recognition>=3.8.0
 
 
9
  pydub>=0.25.0
simple_casl_app.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import boto3
3
+ import json
4
+ import os
5
+ import logging
6
+
7
+ # Configure logging
8
+ logging.basicConfig(level=logging.INFO)
9
+ logger = logging.getLogger(__name__)
10
+
11
+ # AWS credentials
12
+ AWS_ACCESS_KEY = os.getenv("AWS_ACCESS_KEY", "")
13
+ AWS_SECRET_KEY = os.getenv("AWS_SECRET_KEY", "")
14
+ AWS_REGION = os.getenv("AWS_REGION", "us-east-1")
15
+
16
+ # Initialize Bedrock client
17
+ bedrock_client = None
18
+ if AWS_ACCESS_KEY and AWS_SECRET_KEY:
19
+ try:
20
+ bedrock_client = boto3.client(
21
+ 'bedrock-runtime',
22
+ aws_access_key_id=AWS_ACCESS_KEY,
23
+ aws_secret_access_key=AWS_SECRET_KEY,
24
+ region_name=AWS_REGION
25
+ )
26
+ logger.info("Bedrock client initialized successfully")
27
+ except Exception as e:
28
+ logger.error(f"Failed to initialize AWS Bedrock client: {str(e)}")
29
+
30
+ def call_bedrock(prompt):
31
+ """Call AWS Bedrock API with correct format"""
32
+ if not bedrock_client:
33
+ return "❌ AWS Bedrock not configured. Please set AWS credentials."
34
+
35
+ try:
36
+ body = json.dumps({
37
+ "anthropic_version": "bedrock-2023-05-31",
38
+ "max_tokens": 4096,
39
+ "top_k": 250,
40
+ "stop_sequences": [],
41
+ "temperature": 0.3,
42
+ "top_p": 0.9,
43
+ "messages": [
44
+ {
45
+ "role": "user",
46
+ "content": [
47
+ {
48
+ "type": "text",
49
+ "text": prompt
50
+ }
51
+ ]
52
+ }
53
+ ]
54
+ })
55
+
56
+ response = bedrock_client.invoke_model(
57
+ body=body,
58
+ modelId='anthropic.claude-3-5-sonnet-20240620-v1:0',
59
+ accept='application/json',
60
+ contentType='application/json'
61
+ )
62
+ response_body = json.loads(response.get('body').read())
63
+ return response_body['content'][0]['text']
64
+
65
+ except Exception as e:
66
+ logger.error(f"Error calling Bedrock: {str(e)}")
67
+ return f"❌ Error calling Bedrock: {str(e)}"
68
+
69
+ def process_file(file):
70
+ """Process uploaded file"""
71
+ if file is None:
72
+ return "Please upload a file first."
73
+
74
+ try:
75
+ # Read file content
76
+ with open(file.name, 'r', encoding='utf-8', errors='ignore') as f:
77
+ content = f.read()
78
+
79
+ if not content.strip():
80
+ return "File appears to be empty."
81
+
82
+ return content
83
+ except Exception as e:
84
+ return f"Error reading file: {str(e)}"
85
+
86
+ def analyze_transcript(file, age, gender):
87
+ """Simple CASL analysis"""
88
+ if file is None:
89
+ return "Please upload a transcript file first."
90
+
91
+ # Get transcript content
92
+ transcript = process_file(file)
93
+ if transcript.startswith("Error") or transcript.startswith("Please"):
94
+ return transcript
95
+
96
+ # Simple analysis prompt
97
+ prompt = f"""
98
+ You are a speech-language pathologist analyzing a transcript for CASL assessment.
99
+
100
+ Patient: {age}-year-old {gender}
101
+
102
+ TRANSCRIPT:
103
+ {transcript}
104
+
105
+ Please provide a CASL analysis including:
106
+
107
+ 1. SPEECH FACTORS (with counts and severity):
108
+ - Difficulty producing fluent speech
109
+ - Word retrieval issues
110
+ - Grammatical errors
111
+ - Repetitions and revisions
112
+
113
+ 2. CASL SKILLS ASSESSMENT:
114
+ - Lexical/Semantic Skills (Standard Score, Percentile, Level)
115
+ - Syntactic Skills (Standard Score, Percentile, Level)
116
+ - Supralinguistic Skills (Standard Score, Percentile, Level)
117
+
118
+ 3. TREATMENT RECOMMENDATIONS:
119
+ - List 3-5 specific intervention strategies
120
+
121
+ 4. CLINICAL SUMMARY:
122
+ - Brief explanation of findings and prognosis
123
+
124
+ Use exact quotes from the transcript as evidence.
125
+ Provide realistic standard scores (70-130 range, mean=100).
126
+ """
127
+
128
+ # Get analysis from Bedrock
129
+ result = call_bedrock(prompt)
130
+ return result
131
+
132
+ # Create simple interface
133
+ with gr.Blocks(title="Simple CASL Analysis", theme=gr.themes.Soft()) as app:
134
+
135
+ gr.Markdown("# πŸ—£οΈ Simple CASL Analysis Tool")
136
+ gr.Markdown("Upload a speech transcript and get instant CASL assessment results.")
137
+
138
+ with gr.Row():
139
+ with gr.Column():
140
+ gr.Markdown("### Upload & Settings")
141
+
142
+ file_upload = gr.File(
143
+ label="Upload Transcript File",
144
+ file_types=[".txt", ".cha"]
145
+ )
146
+
147
+ age = gr.Number(
148
+ label="Patient Age",
149
+ value=8,
150
+ minimum=1,
151
+ maximum=120
152
+ )
153
+
154
+ gender = gr.Radio(
155
+ ["male", "female", "other"],
156
+ label="Gender",
157
+ value="male"
158
+ )
159
+
160
+ analyze_btn = gr.Button(
161
+ "πŸ” Analyze Transcript",
162
+ variant="primary"
163
+ )
164
+
165
+ with gr.Column():
166
+ gr.Markdown("### Analysis Results")
167
+
168
+ output = gr.Textbox(
169
+ label="CASL Analysis Report",
170
+ placeholder="Analysis results will appear here...",
171
+ lines=25,
172
+ max_lines=30
173
+ )
174
+
175
+ # Connect the analyze button
176
+ analyze_btn.click(
177
+ analyze_transcript,
178
+ inputs=[file_upload, age, gender],
179
+ outputs=[output]
180
+ )
181
+
182
+ if __name__ == "__main__":
183
+ print("πŸš€ Starting Simple CASL Analysis Tool...")
184
+ if not bedrock_client:
185
+ print("⚠️ AWS credentials not configured - analysis will show error message")
186
+
187
+ app.launch(show_api=False)