Tantawi commited on
Commit
b96d38b
·
verified ·
1 Parent(s): 87f9c9b

Upload 9 files

Browse files
Files changed (9) hide show
  1. Lab_report_analysis.py +72 -0
  2. README.md +261 -10
  3. debug_analyzer.py +43 -0
  4. index.html +585 -0
  5. lab_analyzer.py +178 -0
  6. main.py +161 -0
  7. models.py +27 -0
  8. requirements.txt +14 -0
  9. test_client.py +83 -0
Lab_report_analysis.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import os
3
+ from huggingface_hub import InferenceClient
4
+ from tkinter import Tk, filedialog
5
+
6
+ def select_image():
7
+
8
+ root = Tk()
9
+ root.withdraw()
10
+ file_path = filedialog.askopenfilename(
11
+ title="Select a Lab Report Image",
12
+ filetypes=[("Image Files", "*.jpg;*.jpeg;*.png;*.bmp;*.tiff;*.webp")]
13
+ )
14
+ return file_path
15
+
16
+
17
+ client = InferenceClient(
18
+ provider="nebius",
19
+ api_key=os.getenv("HUGGINGFACE_API_KEY", "your-api-key-here"),
20
+ )
21
+ img = select_image()
22
+
23
+ with open(img, "rb") as f:
24
+ image_bytes = f.read()
25
+ image_b64 = base64.b64encode(image_bytes).decode("utf-8")
26
+
27
+ prompt = """
28
+ You are a medical analysis assistant.
29
+
30
+ Analyze the following lab report image and give a structured, professional summary
31
+ following these steps:
32
+
33
+ 1. Extract the results (with normal ranges if available).
34
+ 2. Highlight abnormal values clearly.
35
+ 3. Explain what the results suggest in simple terms.
36
+ 4. Provide an overall summary of health findings.
37
+ 5. End with the disclaimer:
38
+ "This analysis is for educational purposes only and should not replace professional medical advice."
39
+
40
+ If the image is unreadable, respond: "The image text is unclear."
41
+ """
42
+
43
+ completion = client.chat.completions.create(
44
+ model="google/gemma-3-27b-it",
45
+ messages=[
46
+ {
47
+ "role": "user",
48
+ "content": [
49
+ {"type": "text", "text": """
50
+ Analyze this lab report and give me a brief, structured summary.
51
+
52
+ Format your response as follows:
53
+
54
+ Summary: (2–3 sentences explaining what the report shows)
55
+ Key Findings: (3–5 bullet points with main abnormal or notable values)
56
+ Interpretation: (1–2 sentences explaining what the findings suggest)
57
+ Note: (One line disclaimer that it’s not medical advice)
58
+
59
+ Keep it short, clear, and professional — like a medical summary written for quick review.
60
+ """},
61
+ {
62
+ "type": "image_url",
63
+ "image_url": {
64
+ "url": f"data:image/jpeg;base64,{image_b64}"
65
+ }
66
+ }
67
+ ]
68
+ }
69
+ ],
70
+ )
71
+
72
+ print(completion.choices[0].message.content.strip())
README.md CHANGED
@@ -1,10 +1,261 @@
1
- ---
2
- title: Lab Analyzer
3
- emoji: 🌖
4
- colorFrom: pink
5
- colorTo: pink
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Lab Report Analysis API
2
+
3
+ A FastAPI-based web service for analyzing lab report images using AI. This service accepts lab report images and provides structured medical analysis with key findings, interpretations, and health insights.
4
+
5
+ ## Features
6
+
7
+ - 🖼️ Image upload support (JPG, PNG, BMP, TIFF, WEBP)
8
+ - 🔍 AI-powered lab report analysis
9
+ - 📊 Structured response with summary, key findings, and interpretation
10
+ - 🌐 RESTful API with automatic documentation
11
+ - 🧪 Built-in test client and web interface
12
+ - ⚡ Async processing for better performance
13
+
14
+ ## Project Structure
15
+
16
+ ```
17
+ Lab_analysis/
18
+ ├── main.py # FastAPI application
19
+ ├── lab_analyzer.py # Core analysis logic
20
+ ├── models.py # Pydantic models
21
+ ├── test_client.py # API test client
22
+ ├── index.html # Web interface
23
+ ├── requirements.txt # Dependencies
24
+ ├── Lab_report_analysis.py # Original script
25
+ └── README.md # This file
26
+ ```
27
+
28
+ ## Installation
29
+
30
+ 1. **Clone or navigate to the project directory:**
31
+
32
+ ```bash
33
+ cd "e:\E-JUST Assignments\Projects\HealthCare\Lab_analysis"
34
+ ```
35
+
36
+ 2. **Create a virtual environment (recommended):**
37
+
38
+ ```bash
39
+ python -m venv venv
40
+ venv\Scripts\activate # On Windows
41
+ ```
42
+
43
+ 3. **Install dependencies:**
44
+ ```bash
45
+ pip install -r requirements.txt
46
+ ```
47
+
48
+ ## Running the API
49
+
50
+ ### Method 1: Using Python directly
51
+
52
+ ```bash
53
+ python main.py
54
+ ```
55
+
56
+ ### Method 2: Using Uvicorn
57
+
58
+ ```bash
59
+ uvicorn main:app --host 0.0.0.0 --port 8000 --reload
60
+ ```
61
+
62
+ The API will be available at:
63
+
64
+ - **API Endpoints**: http://localhost:8000
65
+ - **Interactive Docs**: http://localhost:8000/docs
66
+ - **ReDoc**: http://localhost:8000/redoc
67
+
68
+ ## API Endpoints
69
+
70
+ ### Health Check
71
+
72
+ - **GET** `/health`
73
+ - Returns service status
74
+
75
+ ### Analyze Lab Report (File Upload)
76
+
77
+ - **POST** `/analyze`
78
+ - Upload an image file for analysis
79
+ - Accepts: `multipart/form-data` with `file` field
80
+
81
+ ### Analyze Lab Report (Base64)
82
+
83
+ - **POST** `/analyze-base64`
84
+ - Send base64 encoded image for analysis
85
+ - Accepts: JSON with `image` field containing base64 string
86
+
87
+ ## Usage Examples
88
+
89
+ ### Using cURL
90
+
91
+ 1. **Health check:**
92
+
93
+ ```bash
94
+ curl http://localhost:8000/health
95
+ ```
96
+
97
+ 2. **Analyze image file:**
98
+
99
+ ```bash
100
+ curl -X POST "http://localhost:8000/analyze" \
101
+ -H "accept: application/json" \
102
+ -H "Content-Type: multipart/form-data" \
103
+ -F "file=@your_lab_report.jpg"
104
+ ```
105
+
106
+ 3. **Analyze base64 image:**
107
+ ```bash
108
+ curl -X POST "http://localhost:8000/analyze-base64" \
109
+ -H "Content-Type: application/json" \
110
+ -d '{"image": "your_base64_encoded_image_here"}'
111
+ ```
112
+
113
+ ### Using Python Test Client
114
+
115
+ ```python
116
+ from test_client import LabReportAPIClient
117
+
118
+ client = LabReportAPIClient()
119
+
120
+ # Health check
121
+ health = client.health_check()
122
+ print(health)
123
+
124
+ # Analyze image
125
+ result = client.analyze_image_file("path/to/your/lab_report.jpg")
126
+ print(result)
127
+ ```
128
+
129
+ ### Using the Web Interface
130
+
131
+ 1. Start the API server
132
+ 2. Open `index.html` in your web browser
133
+ 3. Drag and drop or select a lab report image
134
+ 4. Click "Analyze Report" to get results
135
+
136
+ ## Response Format
137
+
138
+ Successful analysis returns:
139
+
140
+ ```json
141
+ {
142
+ "success": true,
143
+ "filename": "lab_report.jpg",
144
+ "analysis": {
145
+ "error": false,
146
+ "summary": "Brief summary of the lab report",
147
+ "key_findings": ["Finding 1", "Finding 2", "Finding 3"],
148
+ "interpretation": "Medical interpretation",
149
+ "note": "Disclaimer about medical advice",
150
+ "raw_response": "Complete AI response"
151
+ }
152
+ }
153
+ ```
154
+
155
+ ## Configuration
156
+
157
+ ### Environment Variables
158
+
159
+ You can set these environment variables to customize the behavior:
160
+
161
+ - `API_HOST`: Host to bind to (default: "0.0.0.0")
162
+ - `API_PORT`: Port to bind to (default: 8000)
163
+ - `HF_API_KEY`: Hugging Face API key (currently hardcoded in `lab_analyzer.py`)
164
+
165
+ ### Updating API Key
166
+
167
+ To use your own Hugging Face API key, modify the `lab_analyzer.py` file:
168
+
169
+ ```python
170
+ self.client = InferenceClient(
171
+ provider="nebius",
172
+ api_key="your_api_key_here", # Replace with your API key
173
+ )
174
+ ```
175
+
176
+ ## Development
177
+
178
+ ### Running in Development Mode
179
+
180
+ ```bash
181
+ uvicorn main:app --reload --host 0.0.0.0 --port 8000
182
+ ```
183
+
184
+ ### Testing
185
+
186
+ Run the test client:
187
+
188
+ ```bash
189
+ python test_client.py
190
+ ```
191
+
192
+ ### Adding New Features
193
+
194
+ 1. Add new endpoints to `main.py`
195
+ 2. Update models in `models.py` if needed
196
+ 3. Extend the analyzer in `lab_analyzer.py`
197
+ 4. Update documentation
198
+
199
+ ## Deployment
200
+
201
+ ### Docker (Optional)
202
+
203
+ Create a `Dockerfile`:
204
+
205
+ ```dockerfile
206
+ FROM python:3.9-slim
207
+
208
+ WORKDIR /app
209
+ COPY requirements.txt .
210
+ RUN pip install -r requirements.txt
211
+
212
+ COPY . .
213
+
214
+ EXPOSE 8000
215
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
216
+ ```
217
+
218
+ Build and run:
219
+
220
+ ```bash
221
+ docker build -t lab-analysis-api .
222
+ docker run -p 8000:8000 lab-analysis-api
223
+ ```
224
+
225
+ ### Production Considerations
226
+
227
+ - Use environment variables for API keys
228
+ - Set up proper CORS origins
229
+ - Add rate limiting
230
+ - Use HTTPS
231
+ - Add authentication if needed
232
+ - Set up logging and monitoring
233
+
234
+ ## Troubleshooting
235
+
236
+ ### Common Issues
237
+
238
+ 1. **Import errors**: Make sure all dependencies are installed
239
+ 2. **Port conflicts**: Change the port in the uvicorn command
240
+ 3. **API key issues**: Verify your Hugging Face API key is valid
241
+ 4. **Image format errors**: Ensure images are in supported formats
242
+
243
+ ### Logs
244
+
245
+ The application logs important events. Check console output for debugging information.
246
+
247
+ ## License
248
+
249
+ This project is for educational purposes. Please ensure you have proper licenses for any AI models used.
250
+
251
+ ## Contributing
252
+
253
+ 1. Fork the repository
254
+ 2. Create a feature branch
255
+ 3. Make your changes
256
+ 4. Test thoroughly
257
+ 5. Submit a pull request
258
+
259
+ ---
260
+
261
+ **Note**: This analysis is for educational purposes only and should not replace professional medical advice.
debug_analyzer.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import sys
3
+ import os
4
+
5
+ # Add current directory to Python path
6
+ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
7
+
8
+ from lab_analyzer import LabReportAnalyzer
9
+
10
+ async def test_analyzer():
11
+ """Test the analyzer with a dummy base64 string to see the response structure"""
12
+ analyzer = LabReportAnalyzer()
13
+
14
+ # Create a small dummy base64 image (1x1 white pixel PNG)
15
+ dummy_image_b64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="
16
+
17
+ print("Testing analyzer with dummy image...")
18
+ try:
19
+ result = await analyzer.analyze_report(dummy_image_b64)
20
+ print("\nAnalysis result structure:")
21
+ print("=" * 50)
22
+ print(f"Result type: {type(result)}")
23
+ print(f"Keys: {result.keys() if isinstance(result, dict) else 'Not a dict'}")
24
+ print("\nFull result:")
25
+ print(result)
26
+
27
+ # Check if it has the expected structure
28
+ if isinstance(result, dict):
29
+ print("\nStructure analysis:")
30
+ print(f"Has 'error' key: {'error' in result}")
31
+ print(f"Has 'summary' key: {'summary' in result}")
32
+ print(f"Has 'key_findings' key: {'key_findings' in result}")
33
+ print(f"Has 'interpretation' key: {'interpretation' in result}")
34
+ print(f"Has 'note' key: {'note' in result}")
35
+ print(f"Has 'raw_response' key: {'raw_response' in result}")
36
+
37
+ except Exception as e:
38
+ print(f"Error testing analyzer: {str(e)}")
39
+ import traceback
40
+ traceback.print_exc()
41
+
42
+ if __name__ == "__main__":
43
+ asyncio.run(test_analyzer())
index.html ADDED
@@ -0,0 +1,585 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Lab Report Analysis</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ padding: 20px;
19
+ }
20
+
21
+ .container {
22
+ max-width: 900px;
23
+ margin: 0 auto;
24
+ background: white;
25
+ border-radius: 15px;
26
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
27
+ overflow: hidden;
28
+ }
29
+
30
+ .header {
31
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
32
+ color: white;
33
+ text-align: center;
34
+ padding: 30px 20px;
35
+ }
36
+
37
+ .header h1 {
38
+ font-size: 2.5rem;
39
+ margin-bottom: 10px;
40
+ }
41
+
42
+ .header p {
43
+ font-size: 1.1rem;
44
+ opacity: 0.9;
45
+ }
46
+
47
+ .content {
48
+ padding: 40px;
49
+ }
50
+
51
+ .upload-section {
52
+ text-align: center;
53
+ margin-bottom: 30px;
54
+ }
55
+
56
+ .upload-area {
57
+ border: 3px dashed #4facfe;
58
+ border-radius: 15px;
59
+ padding: 50px 20px;
60
+ margin-bottom: 20px;
61
+ background: #f8fbff;
62
+ transition: all 0.3s ease;
63
+ cursor: pointer;
64
+ }
65
+
66
+ .upload-area:hover {
67
+ background: #e8f4ff;
68
+ border-color: #0084ff;
69
+ }
70
+
71
+ .upload-area.dragover {
72
+ background: #e8f4ff;
73
+ border-color: #0084ff;
74
+ transform: scale(1.02);
75
+ }
76
+
77
+ .upload-icon {
78
+ font-size: 4rem;
79
+ color: #4facfe;
80
+ margin-bottom: 20px;
81
+ }
82
+
83
+ .upload-text {
84
+ font-size: 1.2rem;
85
+ color: #666;
86
+ margin-bottom: 20px;
87
+ }
88
+
89
+ .file-input {
90
+ display: none;
91
+ }
92
+
93
+ .btn {
94
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
95
+ color: white;
96
+ border: none;
97
+ padding: 15px 30px;
98
+ border-radius: 50px;
99
+ font-size: 1.1rem;
100
+ font-weight: 600;
101
+ cursor: pointer;
102
+ transition: all 0.3s ease;
103
+ margin: 10px;
104
+ }
105
+
106
+ .btn:hover {
107
+ transform: translateY(-2px);
108
+ box-shadow: 0 5px 15px rgba(79, 172, 254, 0.4);
109
+ }
110
+
111
+ .btn:disabled {
112
+ background: #ccc;
113
+ cursor: not-allowed;
114
+ transform: none;
115
+ box-shadow: none;
116
+ }
117
+
118
+ .preview-section {
119
+ margin: 20px 0;
120
+ text-align: center;
121
+ }
122
+
123
+ .preview-image {
124
+ max-width: 100%;
125
+ max-height: 300px;
126
+ border-radius: 10px;
127
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
128
+ margin-bottom: 15px;
129
+ }
130
+
131
+ .results-section {
132
+ margin-top: 30px;
133
+ display: none;
134
+ }
135
+
136
+ .loading {
137
+ text-align: center;
138
+ padding: 40px;
139
+ color: #4facfe;
140
+ }
141
+
142
+ .loading-spinner {
143
+ border: 4px solid #f3f3f3;
144
+ border-top: 4px solid #4facfe;
145
+ border-radius: 50%;
146
+ width: 50px;
147
+ height: 50px;
148
+ animation: spin 1s linear infinite;
149
+ margin: 0 auto 20px;
150
+ }
151
+
152
+ @keyframes spin {
153
+ 0% {
154
+ transform: rotate(0deg);
155
+ }
156
+ 100% {
157
+ transform: rotate(360deg);
158
+ }
159
+ }
160
+
161
+ .results-content {
162
+ background: #f8fbff;
163
+ border-radius: 15px;
164
+ padding: 30px;
165
+ border-left: 5px solid #4facfe;
166
+ }
167
+
168
+ .result-title {
169
+ color: #333;
170
+ font-size: 1.8rem;
171
+ margin-bottom: 25px;
172
+ display: flex;
173
+ align-items: center;
174
+ gap: 10px;
175
+ }
176
+
177
+ .analysis-block {
178
+ background: white;
179
+ border-radius: 10px;
180
+ padding: 20px;
181
+ margin-bottom: 20px;
182
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
183
+ }
184
+
185
+ .analysis-block h3 {
186
+ color: #4facfe;
187
+ font-size: 1.3rem;
188
+ margin-bottom: 15px;
189
+ display: flex;
190
+ align-items: center;
191
+ gap: 10px;
192
+ }
193
+
194
+ .analysis-block p {
195
+ color: #555;
196
+ line-height: 1.6;
197
+ font-size: 1rem;
198
+ }
199
+
200
+ .findings-list {
201
+ list-style: none;
202
+ padding: 0;
203
+ }
204
+
205
+ .findings-list li {
206
+ background: #f0f8ff;
207
+ margin: 10px 0;
208
+ padding: 15px;
209
+ border-radius: 8px;
210
+ border-left: 4px solid #4facfe;
211
+ color: #333;
212
+ }
213
+
214
+ .findings-list li:before {
215
+ content: "•";
216
+ color: #4facfe;
217
+ font-weight: bold;
218
+ margin-right: 10px;
219
+ }
220
+
221
+ .error-message {
222
+ background: #ffe6e6;
223
+ color: #d63031;
224
+ padding: 20px;
225
+ border-radius: 10px;
226
+ border-left: 5px solid #d63031;
227
+ margin: 20px 0;
228
+ }
229
+
230
+ .success-message {
231
+ background: #e6ffe6;
232
+ color: #00b894;
233
+ padding: 20px;
234
+ border-radius: 10px;
235
+ border-left: 5px solid #00b894;
236
+ margin: 20px 0;
237
+ }
238
+
239
+ .disclaimer {
240
+ background: #fff3cd;
241
+ color: #856404;
242
+ padding: 15px;
243
+ border-radius: 8px;
244
+ border: 1px solid #ffeaa7;
245
+ margin-top: 20px;
246
+ font-style: italic;
247
+ }
248
+
249
+ .raw-response {
250
+ background: #f8f9fa;
251
+ border: 1px solid #e9ecef;
252
+ border-radius: 8px;
253
+ padding: 15px;
254
+ font-family: "Courier New", monospace;
255
+ font-size: 0.9rem;
256
+ white-space: pre-wrap;
257
+ max-height: 300px;
258
+ overflow-y: auto;
259
+ }
260
+
261
+ @media (max-width: 768px) {
262
+ .header h1 {
263
+ font-size: 2rem;
264
+ }
265
+
266
+ .content {
267
+ padding: 20px;
268
+ }
269
+
270
+ .upload-area {
271
+ padding: 30px 15px;
272
+ }
273
+ }
274
+ </style>
275
+ </head>
276
+ <body>
277
+ <div class="container">
278
+ <div class="header">
279
+ <h1>🔬 Lab Report Analysis</h1>
280
+ <p>AI-powered medical lab report analysis service</p>
281
+ </div>
282
+
283
+ <div class="content">
284
+ <div class="upload-section">
285
+ <div
286
+ class="upload-area"
287
+ onclick="document.getElementById('fileInput').click()"
288
+ >
289
+ <div class="upload-icon">📄</div>
290
+ <div class="upload-text">
291
+ Drag and drop your lab report image here<br />
292
+ or click to select a file
293
+ </div>
294
+ <div class="supported-formats">
295
+ <small>Supports: JPG, PNG, BMP, TIFF, WEBP</small>
296
+ </div>
297
+ </div>
298
+
299
+ <input
300
+ type="file"
301
+ id="fileInput"
302
+ class="file-input"
303
+ accept="image/*"
304
+ />
305
+
306
+ <button
307
+ class="btn"
308
+ id="analyzeBtn"
309
+ disabled
310
+ onclick="analyzeReport()"
311
+ >
312
+ 🔍 Analyze Lab Report
313
+ </button>
314
+ </div>
315
+
316
+ <div class="preview-section" id="previewSection" style="display: none">
317
+ <h3>Selected File:</h3>
318
+ <div id="imagePreview"></div>
319
+ </div>
320
+
321
+ <div class="results-section" id="resultsSection">
322
+ <div class="loading" id="loadingDiv" style="display: none">
323
+ <div class="loading-spinner"></div>
324
+ <h3>Analyzing your lab report...</h3>
325
+ <p>Please wait while our AI processes your image</p>
326
+ </div>
327
+
328
+ <div id="resultsContent"></div>
329
+ </div>
330
+ </div>
331
+ </div>
332
+
333
+ <script>
334
+ const API_URL = "http://localhost:8000";
335
+ let selectedFile = null;
336
+
337
+ // Get DOM elements
338
+ const fileInput = document.getElementById("fileInput");
339
+ const uploadArea = document.querySelector(".upload-area");
340
+ const analyzeBtn = document.getElementById("analyzeBtn");
341
+ const previewSection = document.getElementById("previewSection");
342
+ const imagePreview = document.getElementById("imagePreview");
343
+ const resultsSection = document.getElementById("resultsSection");
344
+ const loadingDiv = document.getElementById("loadingDiv");
345
+ const resultsContent = document.getElementById("resultsContent");
346
+
347
+ // Event listeners
348
+ fileInput.addEventListener("change", handleFileSelect);
349
+ uploadArea.addEventListener("dragover", handleDragOver);
350
+ uploadArea.addEventListener("dragleave", handleDragLeave);
351
+ uploadArea.addEventListener("drop", handleDrop);
352
+
353
+ function handleFileSelect(event) {
354
+ const file = event.target.files[0];
355
+ if (file && file.type.startsWith("image/")) {
356
+ selectedFile = file;
357
+ showPreview(file);
358
+ analyzeBtn.disabled = false;
359
+ } else {
360
+ showError("Please select a valid image file");
361
+ }
362
+ }
363
+
364
+ function handleDragOver(event) {
365
+ event.preventDefault();
366
+ uploadArea.classList.add("dragover");
367
+ }
368
+
369
+ function handleDragLeave(event) {
370
+ event.preventDefault();
371
+ uploadArea.classList.remove("dragover");
372
+ }
373
+
374
+ function handleDrop(event) {
375
+ event.preventDefault();
376
+ uploadArea.classList.remove("dragover");
377
+
378
+ const files = event.dataTransfer.files;
379
+ if (files.length > 0 && files[0].type.startsWith("image/")) {
380
+ selectedFile = files[0];
381
+ showPreview(files[0]);
382
+ analyzeBtn.disabled = false;
383
+ } else {
384
+ showError("Please drop a valid image file");
385
+ }
386
+ }
387
+
388
+ function showPreview(file) {
389
+ const reader = new FileReader();
390
+ reader.onload = function (e) {
391
+ imagePreview.innerHTML = `
392
+ <img src="${
393
+ e.target.result
394
+ }" alt="Lab Report Preview" class="preview-image">
395
+ <p><strong>File:</strong> ${file.name} (${(
396
+ file.size /
397
+ 1024 /
398
+ 1024
399
+ ).toFixed(2)} MB)</p>
400
+ `;
401
+ previewSection.style.display = "block";
402
+ };
403
+ reader.readAsDataURL(file);
404
+ }
405
+
406
+ async function analyzeReport() {
407
+ if (!selectedFile) {
408
+ showError("Please select an image file first");
409
+ return;
410
+ }
411
+
412
+ // Show loading
413
+ resultsSection.style.display = "block";
414
+ loadingDiv.style.display = "block";
415
+ resultsContent.innerHTML = "";
416
+ analyzeBtn.disabled = true;
417
+
418
+ try {
419
+ console.log("🔄 Sending file to API:", selectedFile.name);
420
+
421
+ const formData = new FormData();
422
+ formData.append("file", selectedFile);
423
+
424
+ const response = await fetch(`${API_URL}/analyze`, {
425
+ method: "POST",
426
+ body: formData,
427
+ });
428
+
429
+ console.log("📡 Response status:", response.status);
430
+ const responseData = await response.json();
431
+ console.log("📊 Full response data:", responseData);
432
+
433
+ loadingDiv.style.display = "none";
434
+
435
+ if (response.ok && responseData.success) {
436
+ console.log("✅ Analysis successful, displaying results");
437
+ displayResults(responseData.analysis);
438
+ } else {
439
+ console.error("❌ Analysis failed:", responseData);
440
+ showError(
441
+ responseData.detail || responseData.message || "Analysis failed"
442
+ );
443
+ }
444
+ } catch (error) {
445
+ console.error("🚨 Connection error:", error);
446
+ loadingDiv.style.display = "none";
447
+ showError(
448
+ `Connection error: ${error.message}. Make sure the API server is running on http://localhost:8000`
449
+ );
450
+ }
451
+
452
+ analyzeBtn.disabled = false;
453
+ }
454
+
455
+ function displayResults(analysis) {
456
+ console.log("🎨 Displaying analysis results:", analysis);
457
+
458
+ if (analysis.error) {
459
+ showError(analysis.message || "Analysis failed");
460
+ return;
461
+ }
462
+
463
+ let html = `
464
+ <div class="results-content">
465
+ <h2 class="result-title">📋 Analysis Results</h2>
466
+ `;
467
+
468
+ // Summary
469
+ if (analysis.summary && analysis.summary.trim()) {
470
+ console.log("📝 Adding summary:", analysis.summary);
471
+ html += `
472
+ <div class="analysis-block">
473
+ <h3>📝 Summary</h3>
474
+ <p>${analysis.summary}</p>
475
+ </div>
476
+ `;
477
+ }
478
+
479
+ // Key Findings
480
+ if (
481
+ analysis.key_findings &&
482
+ Array.isArray(analysis.key_findings) &&
483
+ analysis.key_findings.length > 0
484
+ ) {
485
+ console.log("🔍 Adding key findings:", analysis.key_findings);
486
+ html += `
487
+ <div class="analysis-block">
488
+ <h3>🔍 Key Findings</h3>
489
+ <ul class="findings-list">
490
+ ${analysis.key_findings
491
+ .map((finding) => `<li>${finding}</li>`)
492
+ .join("")}
493
+ </ul>
494
+ </div>
495
+ `;
496
+ }
497
+
498
+ // Interpretation
499
+ if (analysis.interpretation && analysis.interpretation.trim()) {
500
+ console.log("💡 Adding interpretation:", analysis.interpretation);
501
+ html += `
502
+ <div class="analysis-block">
503
+ <h3>💡 Interpretation</h3>
504
+ <p>${analysis.interpretation}</p>
505
+ </div>
506
+ `;
507
+ }
508
+
509
+ // Note/Disclaimer
510
+ if (analysis.note && analysis.note.trim()) {
511
+ console.log("⚠️ Adding note:", analysis.note);
512
+ html += `
513
+ <div class="disclaimer">
514
+ <strong>⚠️ Important Note:</strong> ${analysis.note}
515
+ </div>
516
+ `;
517
+ }
518
+
519
+ // If no structured data is available, show raw response
520
+ const hasStructuredData =
521
+ (analysis.summary && analysis.summary.trim()) ||
522
+ (analysis.key_findings && analysis.key_findings.length > 0) ||
523
+ (analysis.interpretation && analysis.interpretation.trim()) ||
524
+ (analysis.note && analysis.note.trim());
525
+
526
+ if (!hasStructuredData) {
527
+ console.log("📄 No structured data found, showing raw response");
528
+ html += `
529
+ <div class="analysis-block">
530
+ <h3>📄 Analysis Result</h3>
531
+ <div class="raw-response">${
532
+ analysis.raw_response ||
533
+ JSON.stringify(analysis, null, 2)
534
+ }</div>
535
+ </div>
536
+ `;
537
+ } else {
538
+ // Always show raw response for debugging
539
+ if (analysis.raw_response) {
540
+ html += `
541
+ <details style="margin-top: 20px;">
542
+ <summary style="cursor: pointer; color: #666; font-size: 0.9rem;">🔧 Show Raw AI Response (Debug)</summary>
543
+ <div class="raw-response" style="margin-top: 10px;">${analysis.raw_response}</div>
544
+ </details>
545
+ `;
546
+ }
547
+ }
548
+
549
+ html += "</div>";
550
+ resultsContent.innerHTML = html;
551
+ console.log("✨ Results displayed successfully");
552
+ }
553
+
554
+ function showError(message) {
555
+ console.error("❌ Showing error:", message);
556
+ resultsContent.innerHTML = `
557
+ <div class="error-message">
558
+ <h3>❌ Error</h3>
559
+ <p>${message}</p>
560
+ </div>
561
+ `;
562
+ resultsSection.style.display = "block";
563
+ }
564
+
565
+ // Check API health on page load
566
+ window.addEventListener("load", async () => {
567
+ try {
568
+ console.log("🏥 Checking API health...");
569
+ const response = await fetch(`${API_URL}/health`);
570
+ if (response.ok) {
571
+ const data = await response.json();
572
+ console.log("✅ API is healthy:", data);
573
+ } else {
574
+ console.warn("⚠️ API health check failed:", response.status);
575
+ }
576
+ } catch (error) {
577
+ console.warn("⚠️ Cannot connect to API:", error.message);
578
+ showError(
579
+ "Cannot connect to the API server. Please make sure the server is running on http://localhost:8000"
580
+ );
581
+ }
582
+ });
583
+ </script>
584
+ </body>
585
+ </html>
lab_analyzer.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import os
3
+ from huggingface_hub import InferenceClient
4
+ import asyncio
5
+ from typing import Dict, Any
6
+ import logging
7
+ from dotenv import load_dotenv
8
+
9
+ # Load environment variables from .env file
10
+ load_dotenv()
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ class LabReportAnalyzer:
15
+ """Lab Report Analysis service using Hugging Face Inference Client"""
16
+
17
+ def __init__(self):
18
+ """Initialize the analyzer with Hugging Face client"""
19
+ self.client = InferenceClient(
20
+ provider="nebius",
21
+ api_key=os.getenv("HUGGINGFACE_API_KEY", "your-api-key-here"),
22
+ )
23
+ self.model = "google/gemma-3-27b-it"
24
+
25
+ async def analyze_report(self, image_b64: str) -> Dict[str, Any]:
26
+ """
27
+ Analyze a lab report image and return structured results
28
+
29
+ Args:
30
+ image_b64: Base64 encoded image string
31
+
32
+ Returns:
33
+ Dictionary containing structured analysis results
34
+ """
35
+ try:
36
+ prompt = self._get_analysis_prompt()
37
+
38
+ # Run the inference in a thread pool to avoid blocking
39
+ loop = asyncio.get_event_loop()
40
+ completion = await loop.run_in_executor(
41
+ None,
42
+ self._run_inference,
43
+ image_b64,
44
+ prompt
45
+ )
46
+
47
+ # Extract and parse the response
48
+ analysis_text = completion.choices[0].message.content.strip()
49
+
50
+ # Parse the structured response
51
+ parsed_result = self._parse_analysis_result(analysis_text)
52
+
53
+ return parsed_result
54
+
55
+ except Exception as e:
56
+ logger.error(f"Error in analyze_report: {str(e)}")
57
+ return {
58
+ "error": True,
59
+ "message": f"Analysis failed: {str(e)}",
60
+ "raw_response": ""
61
+ }
62
+
63
+ def _run_inference(self, image_b64: str, prompt: str):
64
+ """Run the Hugging Face inference synchronously"""
65
+ return self.client.chat.completions.create(
66
+ model=self.model,
67
+ messages=[
68
+ {
69
+ "role": "user",
70
+ "content": [
71
+ {"type": "text", "text": prompt},
72
+ {
73
+ "type": "image_url",
74
+ "image_url": {
75
+ "url": f"data:image/jpeg;base64,{image_b64}"
76
+ }
77
+ }
78
+ ]
79
+ }
80
+ ],
81
+ )
82
+
83
+ def _get_analysis_prompt(self) -> str:
84
+ """Get the structured analysis prompt"""
85
+ return """
86
+ You are a medical analysis assistant.
87
+
88
+ Analyze the following lab report image and give a structured, professional summary
89
+ following these steps:
90
+
91
+ 1. Extract the results (with normal ranges if available).
92
+ 2. Highlight abnormal values clearly.
93
+ 3. Explain what the results suggest in simple terms.
94
+ 4. Provide an overall summary of health findings.
95
+ 5. End with the disclaimer:
96
+ "This analysis is for educational purposes only and should not replace professional medical advice."
97
+
98
+ If the image is unreadable, respond: "The image text is unclear."
99
+
100
+ Format your response as follows:
101
+
102
+ Summary: (2–3 sentences explaining what the report shows)
103
+ Key Findings: (3–5 bullet points with main abnormal or notable values)
104
+ Interpretation: (1–2 sentences explaining what the findings suggest)
105
+ Note: (One line disclaimer that it's not medical advice)
106
+
107
+ Keep it short, clear, and professional — like a medical summary written for quick review.
108
+ """
109
+
110
+ def _parse_analysis_result(self, analysis_text: str) -> Dict[str, Any]:
111
+ """
112
+ Parse the structured analysis result into a dictionary
113
+
114
+ Args:
115
+ analysis_text: Raw analysis text from the model
116
+
117
+ Returns:
118
+ Structured dictionary with parsed components
119
+ """
120
+ try:
121
+ result = {
122
+ "error": False,
123
+ "summary": "",
124
+ "key_findings": [],
125
+ "interpretation": "",
126
+ "note": "",
127
+ "raw_response": analysis_text
128
+ }
129
+
130
+ # Check if image is unreadable
131
+ if "The image text is unclear" in analysis_text:
132
+ result["error"] = True
133
+ result["message"] = "The image text is unclear or unreadable"
134
+ return result
135
+
136
+ lines = analysis_text.split('\n')
137
+ current_section = None
138
+
139
+ for line in lines:
140
+ line = line.strip()
141
+ if not line:
142
+ continue
143
+
144
+ # Identify sections (handle both plain text and markdown formats)
145
+ if line.startswith('Summary:') or line.startswith('**Summary:**'):
146
+ current_section = 'summary'
147
+ result['summary'] = line.replace('**Summary:**', '').replace('Summary:', '').strip()
148
+ elif line.startswith('Key Findings:') or line.startswith('**Key Findings:**'):
149
+ current_section = 'key_findings'
150
+ elif line.startswith('Interpretation:') or line.startswith('**Interpretation:**'):
151
+ current_section = 'interpretation'
152
+ result['interpretation'] = line.replace('**Interpretation:**', '').replace('Interpretation:', '').strip()
153
+ elif line.startswith('Note:') or line.startswith('**Note:**'):
154
+ current_section = 'note'
155
+ result['note'] = line.replace('**Note:**', '').replace('Note:', '').strip()
156
+ else:
157
+ # Continue previous section
158
+ if current_section == 'summary' and not result['summary']:
159
+ result['summary'] = line
160
+ elif current_section == 'key_findings' and (line.startswith(('•', '-', '*')) or line.strip().startswith('*')):
161
+ # Handle both regular bullets and markdown-style bullets
162
+ clean_line = line.lstrip('•-* ').strip()
163
+ if clean_line:
164
+ result['key_findings'].append(clean_line)
165
+ elif current_section == 'interpretation' and not result['interpretation']:
166
+ result['interpretation'] = line
167
+ elif current_section == 'note' and not result['note']:
168
+ result['note'] = line
169
+
170
+ return result
171
+
172
+ except Exception as e:
173
+ logger.error(f"Error parsing analysis result: {str(e)}")
174
+ return {
175
+ "error": True,
176
+ "message": f"Failed to parse analysis: {str(e)}",
177
+ "raw_response": analysis_text
178
+ }
main.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, File, UploadFile, HTTPException
2
+ from fastapi.responses import JSONResponse
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ import base64
5
+ import io
6
+ from PIL import Image
7
+ from lab_analyzer import LabReportAnalyzer
8
+ import logging
9
+
10
+ # Configure logging
11
+ logging.basicConfig(level=logging.INFO)
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # Initialize FastAPI app
15
+ app = FastAPI(
16
+ title="Lab Report Analysis API",
17
+ description="AI-powered lab report analysis service",
18
+ version="1.0.0"
19
+ )
20
+
21
+ # Add CORS middleware
22
+ app.add_middleware(
23
+ CORSMiddleware,
24
+ allow_origins=["*"], # Configure this properly for production
25
+ allow_credentials=True,
26
+ allow_methods=["*"],
27
+ allow_headers=["*"],
28
+ )
29
+
30
+ # Initialize the lab analyzer
31
+ analyzer = LabReportAnalyzer()
32
+
33
+ @app.get("/")
34
+ async def root():
35
+ """Health check endpoint"""
36
+ return {"message": "Lab Report Analysis API is running"}
37
+
38
+ @app.get("/health")
39
+ async def health_check():
40
+ """Health check endpoint for monitoring"""
41
+ return {"status": "healthy", "service": "lab-report-analyzer"}
42
+
43
+ @app.post("/analyze")
44
+ async def analyze_lab_report(file: UploadFile = File(...)):
45
+ """
46
+ Analyze a lab report image and return structured results
47
+
48
+ Args:
49
+ file: Uploaded image file (jpg, jpeg, png, bmp, tiff, webp)
50
+
51
+ Returns:
52
+ JSON response with analysis results
53
+ """
54
+ try:
55
+ # Read file contents first
56
+ contents = await file.read()
57
+ if len(contents) == 0:
58
+ raise HTTPException(status_code=400, detail="Empty file uploaded")
59
+
60
+ # Validate image can be opened (more reliable than content_type)
61
+ try:
62
+ image = Image.open(io.BytesIO(contents))
63
+ image.verify() # Verify it's a valid image
64
+
65
+ # Reset file pointer for re-reading
66
+ contents_for_analysis = contents
67
+
68
+ except Exception as e:
69
+ # Check if content type suggests it might be an image
70
+ allowed_types = ['image/', 'application/octet-stream']
71
+ if file.content_type and not any(t in file.content_type for t in allowed_types):
72
+ raise HTTPException(
73
+ status_code=400,
74
+ detail=f"File must be an image (jpg, jpeg, png, bmp, tiff, webp). Received: {file.content_type}"
75
+ )
76
+ else:
77
+ raise HTTPException(status_code=400, detail=f"Invalid image file: {str(e)}")
78
+
79
+ # Convert to base64 for analysis
80
+ image_b64 = base64.b64encode(contents_for_analysis).decode("utf-8")
81
+
82
+ # Analyze the lab report
83
+ logger.info(f"Analyzing lab report: {file.filename}")
84
+ analysis_result = await analyzer.analyze_report(image_b64)
85
+
86
+ return JSONResponse(
87
+ status_code=200,
88
+ content={
89
+ "success": True,
90
+ "filename": file.filename,
91
+ "analysis": analysis_result
92
+ }
93
+ )
94
+
95
+ except HTTPException:
96
+ raise
97
+ except Exception as e:
98
+ logger.error(f"Error analyzing lab report: {str(e)}")
99
+ raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
100
+
101
+ @app.post("/analyze-base64")
102
+ async def analyze_lab_report_base64(data: dict):
103
+ """
104
+ Analyze a lab report from base64 encoded image
105
+
106
+ Args:
107
+ data: JSON with 'image' key containing base64 encoded image
108
+
109
+ Returns:
110
+ JSON response with analysis results
111
+ """
112
+ try:
113
+ if 'image' not in data:
114
+ raise HTTPException(status_code=400, detail="Missing 'image' field in request body")
115
+
116
+ image_b64 = data['image']
117
+
118
+ # Remove data:image/...;base64, prefix if present
119
+ if image_b64.startswith('data:image'):
120
+ image_b64 = image_b64.split(',')[1]
121
+
122
+ # Validate base64 and image
123
+ try:
124
+ image_bytes = base64.b64decode(image_b64)
125
+ image = Image.open(io.BytesIO(image_bytes))
126
+ image.verify()
127
+ except Exception as e:
128
+ raise HTTPException(status_code=400, detail=f"Invalid base64 image: {str(e)}")
129
+
130
+ # Analyze the lab report
131
+ logger.info("Analyzing lab report from base64 data")
132
+ analysis_result = await analyzer.analyze_report(image_b64)
133
+
134
+ return JSONResponse(
135
+ status_code=200,
136
+ content={
137
+ "success": True,
138
+ "analysis": analysis_result
139
+ }
140
+ )
141
+
142
+ except HTTPException:
143
+ raise
144
+ except Exception as e:
145
+ logger.error(f"Error analyzing base64 lab report: {str(e)}")
146
+ raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
147
+
148
+ # Flutter-friendly endpoint aliases
149
+ @app.post("/api/analyze-lab")
150
+ async def analyze_lab_api(file: UploadFile = File(...)):
151
+ """Flutter-friendly endpoint for lab analysis"""
152
+ return await analyze_lab_report(file)
153
+
154
+ @app.post("/api/analyze-lab-base64")
155
+ async def analyze_lab_base64_api(data: dict):
156
+ """Flutter-friendly endpoint for base64 lab analysis"""
157
+ return await analyze_lab_report_base64(data)
158
+
159
+ if __name__ == "__main__":
160
+ import uvicorn
161
+ uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
models.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import List, Optional, Any
3
+
4
+ class AnalysisRequest(BaseModel):
5
+ """Request model for base64 image analysis"""
6
+ image: str = Field(..., description="Base64 encoded image string")
7
+
8
+ class AnalysisResponse(BaseModel):
9
+ """Response model for lab report analysis"""
10
+ success: bool = Field(..., description="Whether the analysis was successful")
11
+ filename: Optional[str] = Field(None, description="Original filename if uploaded via file")
12
+ analysis: dict = Field(..., description="Analysis results")
13
+
14
+ class ParsedAnalysis(BaseModel):
15
+ """Structured analysis result"""
16
+ error: bool = Field(False, description="Whether an error occurred")
17
+ summary: str = Field("", description="Summary of the lab report")
18
+ key_findings: List[str] = Field(default_factory=list, description="Key findings from the report")
19
+ interpretation: str = Field("", description="Medical interpretation")
20
+ note: str = Field("", description="Disclaimer note")
21
+ raw_response: str = Field("", description="Raw response from the AI model")
22
+ message: Optional[str] = Field(None, description="Error message if any")
23
+
24
+ class HealthResponse(BaseModel):
25
+ """Health check response"""
26
+ status: str = Field(..., description="Service status")
27
+ service: str = Field(..., description="Service name")
requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ transformers>=4.44.0
2
+ huggingface-hub>=0.24.0
3
+ torch>=2.2.0
4
+ Pillow>=10.0.0
5
+ opencv-python>=4.9.0
6
+ numpy>=1.26.0
7
+ requests>=2.31.0
8
+ matplotlib>=3.9.0
9
+ ipython>=8.25.0
10
+ pandas>=2.2.0
11
+ fastapi>=0.104.0
12
+ uvicorn>=0.24.0
13
+ python-multipart>=0.0.6
14
+ pydantic>=2.4.0
test_client.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import base64
3
+ import json
4
+ from pathlib import Path
5
+
6
+ class LabReportAPIClient:
7
+ """Client for testing the Lab Report Analysis API"""
8
+
9
+ def __init__(self, base_url: str = "http://localhost:8000"):
10
+ self.base_url = base_url.rstrip('/')
11
+
12
+ def health_check(self):
13
+ """Check if the API is running"""
14
+ try:
15
+ response = requests.get(f"{self.base_url}/health")
16
+ return response.json()
17
+ except requests.exceptions.RequestException as e:
18
+ return {"error": f"Failed to connect: {str(e)}"}
19
+
20
+ def analyze_image_file(self, image_path: str):
21
+ """Analyze a lab report image file"""
22
+ try:
23
+ with open(image_path, 'rb') as f:
24
+ files = {'file': (Path(image_path).name, f, 'image/jpeg')}
25
+ response = requests.post(f"{self.base_url}/analyze", files=files)
26
+
27
+ if response.status_code == 200:
28
+ return response.json()
29
+ else:
30
+ return {
31
+ "error": True,
32
+ "status_code": response.status_code,
33
+ "message": response.text
34
+ }
35
+ except Exception as e:
36
+ return {"error": f"Failed to analyze image: {str(e)}"}
37
+
38
+ def analyze_base64_image(self, image_path: str):
39
+ """Analyze a lab report using base64 encoding"""
40
+ try:
41
+ with open(image_path, 'rb') as f:
42
+ image_b64 = base64.b64encode(f.read()).decode('utf-8')
43
+
44
+ data = {"image": image_b64}
45
+ response = requests.post(
46
+ f"{self.base_url}/analyze-base64",
47
+ json=data,
48
+ headers={'Content-Type': 'application/json'}
49
+ )
50
+
51
+ if response.status_code == 200:
52
+ return response.json()
53
+ else:
54
+ return {
55
+ "error": True,
56
+ "status_code": response.status_code,
57
+ "message": response.text
58
+ }
59
+ except Exception as e:
60
+ return {"error": f"Failed to analyze base64 image: {str(e)}"}
61
+
62
+ def main():
63
+ """Test the API client"""
64
+ client = LabReportAPIClient()
65
+
66
+ # Health check
67
+ print("🏥 Testing Lab Report Analysis API")
68
+ print("=" * 50)
69
+
70
+ health = client.health_check()
71
+ print(f"Health Check: {health}")
72
+ print()
73
+
74
+ # You can test with an actual image file
75
+ # Uncomment and modify the path below to test with your lab report image
76
+
77
+ # image_path = "your_lab_report_image.jpg" # Replace with actual path
78
+ # print(f"Analyzing image: {image_path}")
79
+ # result = client.analyze_image_file(image_path)
80
+ # print(json.dumps(result, indent=2))
81
+
82
+ if __name__ == "__main__":
83
+ main()