Spaces:
Sleeping
Sleeping
Upload 13 files
Browse files- README_APPS.md +76 -0
- experimental_casl_app.py +1444 -0
- full_casl_app.py +684 -0
- moderate_casl_app.py +838 -0
- reference_files/CLEANUP_PLAN.md +73 -0
- reference_files/casl_analysis.py +0 -0
- reference_files/copy_of_casl_analysis.py +1491 -0
- reference_files/requirements_improved.txt +12 -0
- reference_files/simple_app.py +1208 -0
- requirements.txt +6 -9
- simple_casl_app.py +187 -0
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.
|
3 |
-
numpy>=1.
|
4 |
-
matplotlib>=3.
|
5 |
-
|
6 |
-
Pillow>=8.0.0
|
7 |
reportlab>=3.6.0
|
8 |
-
|
9 |
-
|
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)
|