malavikapradeep2001 commited on
Commit
6df1c09
·
1 Parent(s): 558435d
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Dockerfile +3 -3
  2. HUGGINGFACE_DEPLOYMENT.md +216 -0
  3. backend/Ultralytics/settings.json +21 -0
  4. backend/__pycache__/app.cpython-311.pyc +0 -0
  5. backend/__pycache__/app.cpython-312.pyc +0 -0
  6. backend/__pycache__/augmentations.cpython-311.pyc +0 -0
  7. backend/__pycache__/augmentations.cpython-312.pyc +0 -0
  8. backend/__pycache__/model.cpython-311.pyc +0 -0
  9. backend/__pycache__/model.cpython-312.pyc +0 -0
  10. backend/__pycache__/model_histo.cpython-311.pyc +0 -0
  11. backend/__pycache__/model_histo.cpython-312.pyc +0 -0
  12. backend/app.py +904 -96
  13. frontend/public/cyto/cyt1.jpg → backend/outputs/detected_1a8f90ea.jpg +2 -2
  14. frontend/public/cyto/cyt3.png → backend/outputs/detected_1c20231d.jpg +2 -2
  15. backend/outputs/detected_2198d45d.jpg +3 -0
  16. backend/outputs/detected_258af4fa.jpg +3 -0
  17. backend/outputs/detected_268b6165.jpg +3 -0
  18. backend/outputs/detected_39406fb4.jpg +3 -0
  19. backend/outputs/detected_39c91983.jpg +3 -0
  20. backend/outputs/detected_46cf6466.jpg +3 -0
  21. backend/outputs/detected_48f5ddde.jpg +3 -0
  22. backend/outputs/detected_4dc34d38.jpg +3 -0
  23. backend/outputs/detected_4e71956d.jpg +3 -0
  24. backend/outputs/detected_53ef21ce.jpg +3 -0
  25. backend/outputs/detected_5af838c6.jpg +3 -0
  26. backend/outputs/detected_5e93888d.jpg +3 -0
  27. backend/outputs/detected_6526bf9c.jpg +3 -0
  28. backend/outputs/detected_669a5877.jpg +3 -0
  29. backend/outputs/detected_85aa7683.jpg +3 -0
  30. backend/outputs/detected_8eed01a5.jpg +3 -0
  31. backend/outputs/detected_907c0662.jpg +3 -0
  32. backend/outputs/detected_91737a8d.jpg +3 -0
  33. backend/outputs/detected_abff463d.jpg +3 -0
  34. backend/outputs/detected_b02db619.jpg +3 -0
  35. backend/outputs/detected_cb4d377f.jpg +3 -0
  36. backend/outputs/detected_df1af137.jpg +3 -0
  37. backend/outputs/detected_e01aa42a.jpg +3 -0
  38. backend/outputs/detected_e5762460.jpg +3 -0
  39. backend/outputs/detected_e705fd2d.jpg +3 -0
  40. backend/outputs/detected_e9a00302.jpg +3 -0
  41. backend/outputs/detected_fd305c21.jpg +3 -0
  42. backend/outputs/images/detected_104914d9.jpg +3 -0
  43. backend/outputs/images/detected_14e068c7.jpg +3 -0
  44. backend/outputs/images/detected_3861cc56.jpg +3 -0
  45. backend/outputs/images/detected_5e5e7d55.jpg +3 -0
  46. backend/outputs/images/detected_7b413ad9.jpg +3 -0
  47. backend/outputs/images/detected_7d0787ca.jpg +3 -0
  48. backend/outputs/images/detected_8e6cc7ce.jpg +3 -0
  49. backend/outputs/images/detected_a13a604c.jpg +3 -0
  50. backend/outputs/images/detected_ca6e3067.jpg +3 -0
Dockerfile CHANGED
@@ -36,8 +36,8 @@ RUN pip install -r requirements.txt || pip install -r backend/requirements.txt |
36
  # Install runtime dependencies explicitly
37
  RUN pip install --no-cache-dir fastapi uvicorn python-multipart ultralytics opencv-python-headless pillow numpy scikit-learn tensorflow keras
38
 
39
- # Hugging Face Spaces expect port 7860
40
  EXPOSE 7860
41
 
42
- # Run FastAPI app
43
- CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
36
  # Install runtime dependencies explicitly
37
  RUN pip install --no-cache-dir fastapi uvicorn python-multipart ultralytics opencv-python-headless pillow numpy scikit-learn tensorflow keras
38
 
39
+ # Hugging Face Spaces sets PORT (default 7860). Listen on $PORT for compatibility.
40
  EXPOSE 7860
41
 
42
+ # Run FastAPI app (respect PORT env var if set by the platform)
43
+ CMD ["sh", "-c", "uvicorn app:app --host 0.0.0.0 --port ${PORT:-7860}"]
HUGGINGFACE_DEPLOYMENT.md ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hugging Face Spaces Deployment Guide
2
+
3
+ ## ✅ Changes Made for Hugging Face Spaces Compatibility
4
+
5
+ ### 1. Port Configuration
6
+ - **Updated `backend/app.py`**: Server now reads `PORT` from environment variable (default: 7860)
7
+ ```python
8
+ port = int(os.environ.get("PORT", 7860))
9
+ uvicorn.run(app, host="0.0.0.0", port=port)
10
+ ```
11
+ - **Updated `Dockerfile`**: CMD uses `${PORT:-7860}` for dynamic port binding
12
+
13
+ ### 2. Filesystem Permissions
14
+ - **Changed output directory**: `OUTPUT_DIR` now uses `/tmp/outputs` instead of `./outputs`
15
+ - Hugging Face Spaces containers have read-only `/app` directory
16
+ - `/tmp` is writable for temporary files
17
+ - **Note**: Files in `/tmp` are ephemeral and lost on restart
18
+
19
+ ### 3. Static File Serving
20
+ - **Fixed sample image serving**: Mounted `/cyto`, `/colpo`, `/histo` directories from `frontend/dist`
21
+ - **Added catch-all route**: Serves static files (logos, banners) from dist root
22
+ - **Frontend dist path fallback**: Checks both `./frontend/dist` (Docker) and `../frontend/dist` (local dev)
23
+
24
+ ### 4. Frontend Configuration
25
+ - **Frontend already configured**: Uses `window.location.origin` in production, so API calls work on any domain
26
+ - **Vite build**: Copies `public/` contents to `dist/` automatically
27
+
28
+ ---
29
+
30
+ ## 📋 Deployment Checklist
31
+
32
+ ### Step 1: Create Hugging Face Space
33
+ 1. Go to https://huggingface.co/spaces
34
+ 2. Click **"Create new Space"**
35
+ 3. Choose:
36
+ - **Space SDK**: Docker
37
+ - **Hardware**: CPU Basic (free) or GPU (for faster inference)
38
+ - **Visibility**: Public or Private
39
+
40
+ ### Step 2: Set Up Git LFS (for large model files)
41
+ Your project has large model files (`.pt`, `.pth`, `.keras`). Track them with Git LFS:
42
+
43
+ ```bash
44
+ # Install Git LFS if not already installed
45
+ git lfs install
46
+
47
+ # Track model files
48
+ git lfs track "*.pt"
49
+ git lfs track "*.pth"
50
+ git lfs track "*.keras"
51
+ git lfs track "*.pkl"
52
+
53
+ # Commit .gitattributes
54
+ git add .gitattributes
55
+ git commit -m "Track model files with Git LFS"
56
+ ```
57
+
58
+ ### Step 3: Configure Secrets (Optional)
59
+ If you want AI-generated summaries using Mistral, add a secret:
60
+
61
+ 1. Go to Space Settings → Variables and secrets
62
+ 2. Add new secret:
63
+ - Name: `HF_TOKEN`
64
+ - Value: Your Hugging Face token (from https://huggingface.co/settings/tokens)
65
+
66
+ ### Step 4: Push Code to Space
67
+ ```bash
68
+ # Add Space as remote
69
+ git remote add space https://huggingface.co/spaces/<YOUR_USERNAME>/<SPACE_NAME>
70
+
71
+ # Push to Space
72
+ git push space main
73
+ ```
74
+
75
+ ### Step 5: Monitor Build
76
+ - Hugging Face will build the Docker image (this may take 10-20 minutes)
77
+ - Watch logs in the Space's "Logs" tab
78
+ - Once built, the Space will automatically start
79
+
80
+ ---
81
+
82
+ ## 🔍 Troubleshooting
83
+
84
+ ### Build Issues
85
+
86
+ **Problem**: Docker build times out or fails
87
+ - **Solution**: Reduce image size by pinning lighter dependencies in `requirements.txt`
88
+ - **Solution**: Consider using pre-built wheels for TensorFlow/PyTorch
89
+
90
+ **Problem**: Model files not found
91
+ - **Solution**: Ensure Git LFS is configured and model files are committed
92
+ - **Solution**: Check that model paths in `backend/app.py` match actual filenames
93
+
94
+ ### Runtime Issues
95
+
96
+ **Problem**: 404 errors for sample images
97
+ - **Solution**: Rebuild frontend: `cd frontend && npm run build`
98
+ - **Solution**: Verify `frontend/public/` contents are copied to `dist/`
99
+
100
+ **Problem**: Permission denied errors
101
+ - **Solution**: All writes should go to `/tmp/outputs` (already fixed)
102
+ - **Solution**: Never write to `/app` directory
103
+
104
+ **Problem**: Port binding errors
105
+ - **Solution**: Use `$PORT` env var (already configured in Dockerfile and app.py)
106
+
107
+ ### Performance Issues
108
+
109
+ **Problem**: Slow startup or inference
110
+ - **Solution**: Models load at startup; consider lazy loading on first request
111
+ - **Solution**: Upgrade to GPU hardware tier for faster inference
112
+ - **Solution**: Add caching for model weights
113
+
114
+ ---
115
+
116
+ ## 📁 File Structure Expected in Space
117
+
118
+ ```
119
+ /app/
120
+ ├── app.py # Main FastAPI app
121
+ ├── model.py, model_histo.py, etc. # Model definitions
122
+ ├── augmentations.py # Image preprocessing
123
+ ├── requirements.txt # Python dependencies
124
+ ├── best2.pt # YOLO cytology model
125
+ ├── MWTclass2.pth # MWT classifier
126
+ ├── yolo_colposcopy.pt # YOLO colposcopy model
127
+ ├── histopathology_trained_model.keras # Histopathology model
128
+ ├── logistic_regression_model.pkl # CIN classifier (optional)
129
+ └── frontend/
130
+ └── dist/ # Built frontend
131
+ ├── index.html
132
+ ├── assets/ # JS/CSS bundles
133
+ ├── cyto/ # Sample cytology images
134
+ ├── colpo/ # Sample colposcopy images
135
+ ├── histo/ # Sample histopathology images
136
+ └── *.png, *.jpeg # Logos, banners
137
+ ```
138
+
139
+ ---
140
+
141
+ ## 🌐 Access Your Space
142
+
143
+ Once deployed, your app will be available at:
144
+ ```
145
+ https://huggingface.co/spaces/<YOUR_USERNAME>/<SPACE_NAME>
146
+ ```
147
+
148
+ The frontend serves at `/` and the API is accessible at:
149
+ - `POST /predict/` - Run model inference
150
+ - `POST /reports/` - Generate medical reports
151
+ - `GET /health` - Health check
152
+ - `GET /models` - List available models
153
+
154
+ ---
155
+
156
+ ## ⚠️ Important Notes
157
+
158
+ ### Ephemeral Storage
159
+ - Files in `/tmp/outputs` are **lost on restart**
160
+ - For persistent reports, consider:
161
+ - Downloading immediately after generation
162
+ - Uploading to external storage (S3, Hugging Face Datasets)
163
+ - Using Persistent Storage (requires paid tier)
164
+
165
+ ### Model Loading Time
166
+ - All models load at startup (~30-60 seconds)
167
+ - First request after restart may be slower
168
+ - Consider implementing health check endpoint that waits for models
169
+
170
+ ### Resource Limits
171
+ - Free CPU tier: Limited RAM and CPU
172
+ - Models are memory-intensive (TensorFlow + PyTorch + YOLO)
173
+ - May need **CPU Upgrade** or **GPU** tier for production use
174
+
175
+ ### CORS
176
+ - Currently allows all origins (`allow_origins=["*"]`)
177
+ - For production, restrict to your Space domain
178
+
179
+ ---
180
+
181
+ ## 🚀 Next Steps After Deployment
182
+
183
+ 1. **Test all three models**:
184
+ - Upload cytology sample → Test YOLO detection
185
+ - Upload colposcopy sample → Test CIN classification
186
+ - Upload histopathology sample → Test breast cancer classification
187
+
188
+ 2. **Generate a test report**:
189
+ - Run an analysis
190
+ - Fill out patient metadata
191
+ - Generate HTML/PDF report
192
+ - Verify download links work
193
+
194
+ 3. **Monitor performance**:
195
+ - Check inference times
196
+ - Monitor memory usage in Space logs
197
+ - Consider upgrading hardware if needed
198
+
199
+ 4. **Share your Space**:
200
+ - Add a README with usage instructions
201
+ - Include sample images in the repo
202
+ - Add citations for model papers
203
+
204
+ ---
205
+
206
+ ## 📞 Support
207
+
208
+ If you encounter issues:
209
+ 1. Check Space logs: Settings → Logs
210
+ 2. Verify all model files are present: Settings → Files
211
+ 3. Test locally with Docker: `docker build -t pathora . && docker run -p 7860:7860 pathora`
212
+ 4. Open an issue on Hugging Face Discuss: https://discuss.huggingface.co/
213
+
214
+ ---
215
+
216
+ **Deployment ready! 🎉**
backend/Ultralytics/settings.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "settings_version": "0.0.6",
3
+ "datasets_dir": "C:\\Users\\datasets",
4
+ "weights_dir": "C:\\Users\\hp\\weights",
5
+ "runs_dir": "C:\\Users\\hp\\runs",
6
+ "uuid": "b8318bcfc07be7433d16fc866923188fe796248b2a6e702ba099b726960ef46b",
7
+ "sync": true,
8
+ "api_key": "",
9
+ "openai_api_key": "",
10
+ "clearml": true,
11
+ "comet": true,
12
+ "dvc": true,
13
+ "hub": true,
14
+ "mlflow": true,
15
+ "neptune": true,
16
+ "raytune": true,
17
+ "tensorboard": false,
18
+ "wandb": false,
19
+ "vscode_msg": true,
20
+ "openvino_msg": true
21
+ }
backend/__pycache__/app.cpython-311.pyc ADDED
Binary file (35.6 kB). View file
 
backend/__pycache__/app.cpython-312.pyc ADDED
Binary file (50.8 kB). View file
 
backend/__pycache__/augmentations.cpython-311.pyc ADDED
Binary file (22.7 kB). View file
 
backend/__pycache__/augmentations.cpython-312.pyc CHANGED
Binary files a/backend/__pycache__/augmentations.cpython-312.pyc and b/backend/__pycache__/augmentations.cpython-312.pyc differ
 
backend/__pycache__/model.cpython-311.pyc ADDED
Binary file (34.2 kB). View file
 
backend/__pycache__/model.cpython-312.pyc CHANGED
Binary files a/backend/__pycache__/model.cpython-312.pyc and b/backend/__pycache__/model.cpython-312.pyc differ
 
backend/__pycache__/model_histo.cpython-311.pyc ADDED
Binary file (74.3 kB). View file
 
backend/__pycache__/model_histo.cpython-312.pyc CHANGED
Binary files a/backend/__pycache__/model_histo.cpython-312.pyc and b/backend/__pycache__/model_histo.cpython-312.pyc differ
 
backend/app.py CHANGED
@@ -1,7 +1,6 @@
1
  import os
2
  import shutil
3
 
4
-
5
  for d in ["/tmp/huggingface", "/tmp/Ultralytics", "/tmp/matplotlib", "/tmp/torch", "/root/.cache"]:
6
  shutil.rmtree(d, ignore_errors=True)
7
 
@@ -11,75 +10,284 @@ os.environ["TORCH_HOME"] = "/tmp/torch"
11
  os.environ["MPLCONFIGDIR"] = "/tmp/matplotlib"
12
  os.environ["YOLO_CONFIG_DIR"] = "/tmp/Ultralytics"
13
 
14
-
15
- from huggingface_hub import login
16
-
17
-
18
-
19
- hf_token = os.getenv("HF_TOKEN")
20
- if hf_token:
21
- login(token=hf_token)
22
-
 
 
 
23
  from fastapi import FastAPI, File, UploadFile, Form
24
  from fastapi.middleware.cors import CORSMiddleware
25
  from fastapi.responses import JSONResponse, FileResponse
 
 
26
  from fastapi.staticfiles import StaticFiles
27
- from ultralytics import YOLO
28
- from io import BytesIO
29
- from PIL import Image
30
  import uvicorn
31
- import json, os, uuid, numpy as np, torch, cv2, joblib, io, tensorflow as tf
32
- import torch.nn as nn
33
- import torchvision.transforms as transforms
34
- import torchvision.models as models
 
 
 
 
 
 
 
35
  from sklearn.preprocessing import MinMaxScaler
36
  from model import MWT as create_model
37
  from augmentations import Augmentations
38
- from model_histo import BreastCancerClassifier # TensorFlow model
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
 
42
  # =====================================================
43
- # App setup
 
 
44
  # =====================================================
45
- app = FastAPI(title="Unified Cervical & Breast Cancer Analysis API")
 
46
 
47
  app.add_middleware(
48
  CORSMiddleware,
49
- allow_origins=["*"],
50
  allow_credentials=True,
51
  allow_methods=["*"],
52
  allow_headers=["*"],
 
53
  )
54
 
55
- OUTPUT_DIR = "/tmp/outputs"
 
56
  os.makedirs(OUTPUT_DIR, exist_ok=True)
 
 
 
 
 
57
  app.mount("/outputs", StaticFiles(directory=OUTPUT_DIR), name="outputs")
58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
60
 
61
  # =====================================================
62
- # Model 1: YOLO (Colposcopy Detection)
 
 
63
  # =====================================================
 
64
  print("🔹 Loading YOLO model...")
65
  yolo_model = YOLO("best2.pt")
66
 
67
- # =====================================================
68
- # Model 2: MWT Classifier
69
- # =====================================================
70
  print("🔹 Loading MWT model...")
71
  mwt_model = create_model(num_classes=2).to(device)
72
  mwt_model.load_state_dict(torch.load("MWTclass2.pth", map_location=device))
73
  mwt_model.eval()
74
- mwt_class_names = ['Negative', 'Positive']
75
 
76
- # =====================================================
77
- # Model 3: CIN Classifier
78
- # =====================================================
79
  print("🔹 Loading CIN model...")
80
- clf = joblib.load("logistic_regression_model.pkl")
 
 
 
 
 
81
  yolo_colposcopy = YOLO("yolo_colposcopy.pt")
82
 
 
 
 
 
 
 
83
  def build_resnet(model_name="resnet50"):
84
  if model_name == "resnet50":
85
  model = models.resnet50(weights=models.ResNet50_Weights.DEFAULT)
@@ -92,7 +300,6 @@ def build_resnet(model_name="resnet50"):
92
  nn.Sequential(model.conv1, model.bn1, model.relu, model.maxpool),
93
  model.layer1, model.layer2, model.layer3, model.layer4,
94
  )
95
-
96
  gap = nn.AdaptiveAvgPool2d((1, 1))
97
  gmp = nn.AdaptiveMaxPool2d((1, 1))
98
  resnet50_blocks = build_resnet("resnet50")
@@ -100,29 +307,14 @@ resnet101_blocks = build_resnet("resnet101")
100
  resnet152_blocks = build_resnet("resnet152")
101
 
102
  transform = transforms.Compose([
103
- transforms.ToPILImage(),
104
- transforms.Resize((224, 224)),
105
- transforms.ToTensor(),
106
- transforms.Normalize(mean=[0.485, 0.456, 0.406],
107
- std=[0.229, 0.224, 0.225]),
108
  ])
109
 
110
- # =====================================================
111
- # Model 4: Histopathology Classifier (TensorFlow)
112
- # =====================================================
113
- print("🔹 Loading Breast Cancer Histopathology model...")
114
- classifier = BreastCancerClassifier(fine_tune=False)
115
- if not classifier.authenticate_huggingface():
116
- raise RuntimeError("HuggingFace authentication failed.")
117
- if not classifier.load_path_foundation():
118
- raise RuntimeError("Failed to load Path Foundation model.")
119
- model_path = "histopathology_trained_model.keras"
120
- classifier.model = tf.keras.models.load_model(model_path)
121
- print(f"✅ Loaded model from {model_path}")
122
 
123
- # =====================================================
124
- # Helper functions
125
- # =====================================================
126
  def preprocess_for_mwt(image_np):
127
  img = cv2.resize(image_np, (224, 224))
128
  img = Augmentations.Normalization((0, 1))(img)
@@ -145,48 +337,147 @@ def extract_cbf_features(blocks, img_t):
145
  p3 = gap(f3).view(-1)
146
  p4 = gap(f4).view(-1)
147
  p5 = gap(f5).view(-1)
148
- cbf_feature = torch.cat([p1, p2, p3, p4, p5], dim=0)
149
- return cbf_feature.cpu().numpy()
150
-
151
- def predict_histopathology(image: Image.Image):
152
- if image.mode != "RGB":
153
- image = image.convert("RGB")
154
- image = image.resize((224, 224))
155
- img_array = np.expand_dims(np.array(image).astype("float32") / 255.0, axis=0)
156
- embeddings = classifier.extract_embeddings(img_array)
157
- prediction_proba = classifier.model.predict(embeddings, verbose=0)[0]
158
- predicted_class = int(np.argmax(prediction_proba))
159
- class_names = ["Benign", "Malignant"]
160
- return {
161
- "model_used": "Breast Cancer Histopathology Classifier",
162
- "prediction": class_names[predicted_class],
163
- "confidence": float(np.max(prediction_proba)),
164
- "probabilities": {
165
- "Benign": float(prediction_proba[0]),
166
- "Malignant": float(prediction_proba[1])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  }
168
- }
 
 
169
 
170
  # =====================================================
171
- # Main endpoint
 
 
172
  # =====================================================
 
 
173
  @app.post("/predict/")
174
  async def predict(model_name: str = Form(...), file: UploadFile = File(...)):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  contents = await file.read()
176
- image = Image.open(BytesIO(contents)).convert("RGB")
177
- image_np = np.array(image)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
  if model_name == "yolo":
180
  results = yolo_model(image)
181
  detections_json = results[0].to_json()
182
  detections = json.loads(detections_json)
 
 
 
 
 
 
 
183
  output_filename = f"detected_{uuid.uuid4().hex[:8]}.jpg"
184
- output_path = os.path.join(OUTPUT_DIR, output_filename)
185
  results[0].save(filename=output_path)
 
186
  return {
187
  "model_used": "YOLO Detection",
188
  "detections": detections,
189
- "annotated_image_url": f"/outputs/{output_filename}"
 
 
 
 
 
 
190
  }
191
 
192
  elif model_name == "mwt":
@@ -195,15 +486,35 @@ async def predict(model_name: str = Form(...), file: UploadFile = File(...)):
195
  output = mwt_model(tensor.to(device)).cpu()
196
  probs = torch.softmax(output, dim=1)[0]
197
  confidences = {mwt_class_names[i]: float(probs[i]) for i in range(2)}
198
- predicted_label = mwt_class_names[torch.argmax(probs)]
199
- return {"model_used": "MWT Classifier", "prediction": predicted_label, "confidence": confidences}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
 
201
  elif model_name == "cin":
 
 
 
 
 
202
  nparr = np.frombuffer(contents, np.uint8)
203
  img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
204
  results = yolo_colposcopy.predict(source=img, conf=0.7, save=False, verbose=False)
205
  if len(results[0].boxes) == 0:
206
  return {"error": "No cervix detected"}
 
207
  x1, y1, x2, y2 = map(int, results[0].boxes.xyxy[0].cpu().numpy())
208
  crop = img[y1:y2, x1:x2]
209
  crop = cv2.resize(crop, (224, 224))
@@ -215,45 +526,542 @@ async def predict(model_name: str = Form(...), file: UploadFile = File(...)):
215
  X_scaled = MinMaxScaler().fit_transform(features)
216
  pred = clf.predict(X_scaled)[0]
217
  proba = clf.predict_proba(X_scaled)[0]
218
- classes = ["CIN1", "CIN2", "CIN3"]
 
219
  predicted_label = classes[pred]
220
  confidences = {classes[i]: float(proba[i]) for i in range(len(classes))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  return {
222
  "model_used": "CIN Classifier",
223
- "prediction": predicted_label,
224
- "confidence": confidences
 
 
 
 
 
225
  }
226
-
227
  elif model_name == "histopathology":
228
- result = predict_histopathology(image)
229
- return result
 
230
 
231
  else:
232
  return JSONResponse(content={"error": "Invalid model name"}, status_code=400)
233
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
 
235
  @app.get("/models")
236
  def get_models():
237
  return {"available_models": ["yolo", "mwt", "cin", "histopathology"]}
238
 
239
-
240
  @app.get("/health")
241
  def health():
242
- return {"message": "Unified Cervical & Breast Cancer API is running!"}
 
 
 
 
243
 
244
- # After other app.mount()s
245
- app.mount("/outputs", StaticFiles(directory=OUTPUT_DIR), name="outputs")
246
- app.mount("/assets", StaticFiles(directory="frontend/dist/assets"), name="assets")
247
- from fastapi.staticfiles import StaticFiles
248
 
249
- app.mount("/", StaticFiles(directory="frontend/dist", html=True), name="static")
 
250
 
 
 
 
 
 
 
 
 
 
 
 
251
 
252
  @app.get("/")
253
  async def serve_frontend():
254
- index_path = os.path.join("frontend", "dist", "index.html")
255
- return FileResponse(index_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
  if __name__ == "__main__":
258
- uvicorn.run(app, host="0.0.0.0", port=7860)
259
-
 
 
1
  import os
2
  import shutil
3
 
 
4
  for d in ["/tmp/huggingface", "/tmp/Ultralytics", "/tmp/matplotlib", "/tmp/torch", "/root/.cache"]:
5
  shutil.rmtree(d, ignore_errors=True)
6
 
 
10
  os.environ["MPLCONFIGDIR"] = "/tmp/matplotlib"
11
  os.environ["YOLO_CONFIG_DIR"] = "/tmp/Ultralytics"
12
 
13
+ import json
14
+ import uuid
15
+ import datetime
16
+ import numpy as np
17
+ import torch
18
+ import cv2
19
+ import joblib
20
+ import torch.nn as nn
21
+ import torchvision.transforms as transforms
22
+ import torchvision.models as models
23
+ from io import BytesIO
24
+ from PIL import Image as PILImage
25
  from fastapi import FastAPI, File, UploadFile, Form
26
  from fastapi.middleware.cors import CORSMiddleware
27
  from fastapi.responses import JSONResponse, FileResponse
28
+ import tensorflow as tf
29
+ from model_histo import BreastCancerClassifier
30
  from fastapi.staticfiles import StaticFiles
 
 
 
31
  import uvicorn
32
+ try:
33
+ from reportlab.lib.pagesizes import letter
34
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image as ReportLabImage
35
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
36
+ from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY
37
+ from reportlab.lib.units import inch
38
+ from reportlab.lib.colors import navy, black
39
+ REPORTLAB_AVAILABLE = True
40
+ except ImportError:
41
+ REPORTLAB_AVAILABLE = False
42
+ from ultralytics import YOLO
43
  from sklearn.preprocessing import MinMaxScaler
44
  from model import MWT as create_model
45
  from augmentations import Augmentations
46
+ from huggingface_hub import InferenceClient
47
+
48
+ # =====================================================
49
+
50
+ # SETUP TEMP DIRS AND ENV
51
+
52
+ # =====================================================
53
+
54
+ for d in ["/tmp/huggingface", "/tmp/Ultralytics", "/tmp/matplotlib", "/tmp/torch"]:
55
+ shutil.rmtree(d, ignore_errors=True)
56
+
57
+ os.environ["HF_HOME"] = "/tmp/huggingface"
58
+ os.environ["HUGGINGFACE_HUB_CACHE"] = "/tmp/huggingface"
59
+ os.environ["TORCH_HOME"] = "/tmp/torch"
60
+ os.environ["MPLCONFIGDIR"] = "/tmp/matplotlib"
61
+ os.environ["YOLO_CONFIG_DIR"] = "/tmp/Ultralytics"
62
+
63
+ # =====================================================
64
+
65
+ # HUGGING FACE CLIENT SETUP
66
+
67
+ # =====================================================
68
+
69
+ HF_MODEL_ID = "mistralai/Mistral-7B-v0.1"
70
+ hf_token = os.getenv("HF_TOKEN")
71
+ client = None
72
+
73
+ if hf_token:
74
+ try:
75
+ client = InferenceClient(model=HF_MODEL_ID, token=hf_token)
76
+ print(f"✅ Hugging Face InferenceClient initialized for {HF_MODEL_ID}")
77
+ except Exception as e:
78
+ print("⚠️ Failed to initialize Hugging Face client:", e)
79
+ else:
80
+ print("⚠️ Warning: No HF_TOKEN found — summaries will be skipped.")
81
+
82
+ def generate_ai_summary(abnormal_cells, normal_cells, avg_confidence):
83
+ """Generate a brief medical interpretation using Mistral."""
84
+ if not client:
85
+ return "⚠️ Hugging Face client not initialized — skipping summary."
86
+
87
+ try:
88
+ prompt = f"""Act as a cytopathology expert providing a brief diagnostic interpretation.
89
+
90
+ Observed Cell Counts:
91
+ - {abnormal_cells} Abnormal Cells
92
+ - {normal_cells} Normal Cells
93
+ - Detection Confidence: {avg_confidence:.1f}%
94
+
95
+ Write a 2-3 sentence professional medical assessment focusing on:
96
+ 1. Cell count analysis
97
+ 2. Abnormality ratio ({abnormal_cells/(abnormal_cells + normal_cells)*100:.1f}%)
98
+ 3. Clinical significance
99
+
100
+ Use objective, scientific language suitable for a pathology report."""
101
+
102
+ # Use streaming to avoid StopIteration
103
+ response = client.text_generation(
104
+ prompt,
105
+ max_new_tokens=200,
106
+ temperature=0.7,
107
+ stream=False,
108
+ details=True,
109
+ stop_sequences=["\n\n", "###"]
110
+ )
111
+
112
+ # Handle different response formats
113
+ if hasattr(response, 'generated_text'):
114
+ return response.generated_text.strip()
115
+ elif isinstance(response, dict):
116
+ return response.get('generated_text', '').strip()
117
+ elif isinstance(response, str):
118
+ return response.strip()
119
+
120
+ # Fallback summary if response format is unexpected
121
+ ratio = abnormal_cells / (abnormal_cells + normal_cells) * 100 if (abnormal_cells + normal_cells) > 0 else 0
122
+ return f"Analysis shows {abnormal_cells} abnormal cells ({ratio:.1f}%) and {normal_cells} normal cells, with average detection confidence of {avg_confidence:.1f}%."
123
+
124
+ except Exception as e:
125
+ # Provide a structured fallback summary instead of error message
126
+ total = abnormal_cells + normal_cells
127
+ if total == 0:
128
+ return "No cells were detected in the sample. Consider re-scanning or adjusting detection parameters."
129
+
130
+ ratio = (abnormal_cells / total) * 100
131
+ severity = "high" if ratio > 70 else "moderate" if ratio > 30 else "low"
132
+
133
+ return f"Quantitative analysis detected {abnormal_cells} abnormal cells ({ratio:.1f}%) among {total} total cells, indicating {severity} abnormality ratio. Average detection confidence: {avg_confidence:.1f}%."
134
+
135
+
136
+ def generate_mwt_summary(predicted_label, confidences, avg_confidence):
137
+ """Generate a short MWT-specific interpretation using the HF client when available."""
138
+ if not client:
139
+ return "⚠️ Hugging Face client not initialized — skipping AI interpretation."
140
 
141
+ try:
142
+ prompt = f"""
143
+ You are a concise cytopathology expert. Given an MWT classifier result, write a 1-2 sentence professional interpretation suitable for embedding in a diagnostic report.
144
+
145
+ Result:
146
+ - Predicted label: {predicted_label}
147
+ - Confidence (average): {avg_confidence:.1f}%
148
+ - Class probabilities: {json.dumps(confidences)}
149
+
150
+ Provide guidance on the significance of the result and any suggested next steps in plain, objective language.
151
+ """
152
+
153
+ response = client.text_generation(
154
+ prompt,
155
+ max_new_tokens=120,
156
+ temperature=0.2,
157
+ stream=False,
158
+ details=True,
159
+ stop_sequences=["\n\n", "###"]
160
+ )
161
+
162
+ if hasattr(response, 'generated_text'):
163
+ return response.generated_text.strip()
164
+ elif isinstance(response, dict):
165
+ return response.get('generated_text', '').strip()
166
+ elif isinstance(response, str):
167
+ return response.strip()
168
+
169
+ return f"Result: {predicted_label} (avg confidence {avg_confidence:.1f}%)."
170
+ except Exception as e:
171
+ return f"Quantitative result: {predicted_label} with average confidence {avg_confidence:.1f}%."
172
+
173
+
174
+ def generate_cin_summary(predicted_grade, confidences, avg_confidence):
175
+ """Generate a short CIN-specific interpretation using the HF client when available."""
176
+ if not client:
177
+ return "⚠️ Hugging Face client not initialized — skipping AI interpretation."
178
+
179
+ try:
180
+ prompt = f"""
181
+ You are a concise gynecologic pathology expert. Given a CIN classifier result, write a 1-2 sentence professional interpretation suitable for a diagnostic report.
182
+
183
+ Result:
184
+ - Predicted grade: {predicted_grade}
185
+ - Confidence (average): {avg_confidence:.1f}%
186
+ - Class probabilities: {json.dumps(confidences)}
187
+
188
+ Provide a brief statement about clinical significance and suggested next steps (e.g., further colposcopic evaluation) in objective, clinical language.
189
+ """
190
+
191
+ response = client.text_generation(
192
+ prompt,
193
+ max_new_tokens=140,
194
+ temperature=0.2,
195
+ stream=False,
196
+ details=True,
197
+ stop_sequences=["\n\n", "###"]
198
+ )
199
+
200
+ if hasattr(response, 'generated_text'):
201
+ return response.generated_text.strip()
202
+ elif isinstance(response, dict):
203
+ return response.get('generated_text', '').strip()
204
+ elif isinstance(response, str):
205
+ return response.strip()
206
+
207
+ return f"Result: {predicted_grade} (avg confidence {avg_confidence:.1f}%)."
208
+ except Exception:
209
+ return f"Quantitative result: {predicted_grade} with average confidence {avg_confidence:.1f}%."
210
 
211
 
212
  # =====================================================
213
+
214
+ # FASTAPI SETUP
215
+
216
  # =====================================================
217
+
218
+ app = FastAPI(title="Pathora Medical Diagnostic API")
219
 
220
  app.add_middleware(
221
  CORSMiddleware,
222
+ allow_origins=["*", "http://localhost:5173", "http://127.0.0.1:5173"],
223
  allow_credentials=True,
224
  allow_methods=["*"],
225
  allow_headers=["*"],
226
+ expose_headers=["*"] # Allow access to response headers
227
  )
228
 
229
+ # Use /tmp for outputs in Hugging Face Spaces (writable directory)
230
+ OUTPUT_DIR = os.environ.get("OUTPUT_DIR", "/tmp/outputs")
231
  os.makedirs(OUTPUT_DIR, exist_ok=True)
232
+
233
+ # Create image outputs dir
234
+ IMAGES_DIR = os.path.join(OUTPUT_DIR, "images")
235
+ os.makedirs(IMAGES_DIR, exist_ok=True)
236
+
237
  app.mount("/outputs", StaticFiles(directory=OUTPUT_DIR), name="outputs")
238
 
239
+ # Mount public sample images from frontend dist (Vite copies public/ to dist/ root)
240
+ # Check both possible locations: frontend/dist (Docker) and ../frontend/dist (local dev)
241
+ FRONTEND_DIST_CHECK = os.path.join(os.path.dirname(__file__), "frontend/dist")
242
+ if not os.path.isdir(FRONTEND_DIST_CHECK):
243
+ FRONTEND_DIST_CHECK = os.path.abspath(os.path.join(os.path.dirname(__file__), "../frontend/dist"))
244
+
245
+ for sample_dir in ["cyto", "colpo", "histo"]:
246
+ sample_path = os.path.join(FRONTEND_DIST_CHECK, sample_dir)
247
+ if os.path.isdir(sample_path):
248
+ app.mount(f"/{sample_dir}", StaticFiles(directory=sample_path), name=sample_dir)
249
+ print(f"✅ Mounted /{sample_dir} from {sample_path}")
250
+ else:
251
+ print(f"⚠️ Sample directory not found: {sample_path}")
252
+
253
+ # Mount other static assets (logos, banners) from dist root
254
+ for static_file in ["banner.jpeg", "white_logo.png", "black_logo.png", "manalife_LOGO.jpg"]:
255
+ static_path = os.path.join(FRONTEND_DIST_CHECK, static_file)
256
+ if os.path.isfile(static_path):
257
+ print(f"✅ Static file available: /{static_file}")
258
+
259
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
260
 
261
  # =====================================================
262
+
263
+ # MODEL LOADS
264
+
265
  # =====================================================
266
+
267
  print("🔹 Loading YOLO model...")
268
  yolo_model = YOLO("best2.pt")
269
 
 
 
 
270
  print("🔹 Loading MWT model...")
271
  mwt_model = create_model(num_classes=2).to(device)
272
  mwt_model.load_state_dict(torch.load("MWTclass2.pth", map_location=device))
273
  mwt_model.eval()
274
+ mwt_class_names = ["Negative", "Positive"]
275
 
 
 
 
276
  print("🔹 Loading CIN model...")
277
+ try:
278
+ clf = joblib.load("logistic_regression_model.pkl")
279
+ except Exception as e:
280
+ print(f"⚠️ CIN classifier not available (logistic_regression_model.pkl missing or invalid): {e}")
281
+ clf = None
282
+
283
  yolo_colposcopy = YOLO("yolo_colposcopy.pt")
284
 
285
+ # =====================================================
286
+
287
+ # RESNET FEATURE EXTRACTORS FOR CIN
288
+
289
+ # =====================================================
290
+
291
  def build_resnet(model_name="resnet50"):
292
  if model_name == "resnet50":
293
  model = models.resnet50(weights=models.ResNet50_Weights.DEFAULT)
 
300
  nn.Sequential(model.conv1, model.bn1, model.relu, model.maxpool),
301
  model.layer1, model.layer2, model.layer3, model.layer4,
302
  )
 
303
  gap = nn.AdaptiveAvgPool2d((1, 1))
304
  gmp = nn.AdaptiveMaxPool2d((1, 1))
305
  resnet50_blocks = build_resnet("resnet50")
 
307
  resnet152_blocks = build_resnet("resnet152")
308
 
309
  transform = transforms.Compose([
310
+ transforms.ToPILImage(),
311
+ transforms.Resize((224, 224)),
312
+ transforms.ToTensor(),
313
+ transforms.Normalize(mean=[0.485, 0.456, 0.406],
314
+ std=[0.229, 0.224, 0.225]),
315
  ])
316
 
 
 
 
 
 
 
 
 
 
 
 
 
317
 
 
 
 
318
  def preprocess_for_mwt(image_np):
319
  img = cv2.resize(image_np, (224, 224))
320
  img = Augmentations.Normalization((0, 1))(img)
 
337
  p3 = gap(f3).view(-1)
338
  p4 = gap(f4).view(-1)
339
  p5 = gap(f5).view(-1)
340
+ return torch.cat([p1, p2, p3, p4, p5], dim=0).cpu().numpy()
341
+
342
+ # =====================================================
343
+ # Model 4: Histopathology Classifier (TensorFlow)
344
+ # =====================================================
345
+ print("🔹 Attempting to load Breast Cancer Histopathology model...")
346
+
347
+ try:
348
+ classifier = BreastCancerClassifier(fine_tune=False)
349
+
350
+ # Safely handle Hugging Face token auth
351
+ hf_token = os.getenv("HF_TOKEN")
352
+ if hf_token:
353
+ if classifier.authenticate_huggingface():
354
+ print("✅ Hugging Face authentication successful.")
355
+ else:
356
+ print("⚠️ Warning: Hugging Face authentication failed, using local model only.")
357
+ else:
358
+ print("⚠️ HF_TOKEN not found in environment — skipping authentication.")
359
+
360
+ # Load Path Foundation model
361
+ if classifier.load_path_foundation():
362
+ print("✅ Loaded Path Foundation base model.")
363
+ else:
364
+ print("⚠️ Could not load Path Foundation base model, continuing with local weights only.")
365
+
366
+ # Load trained histopathology model
367
+ model_path = "histopathology_trained_model.keras"
368
+ if os.path.exists(model_path):
369
+ classifier.model = tf.keras.models.load_model(model_path)
370
+ print(f"✅ Loaded local histopathology model: {model_path}")
371
+ else:
372
+ print(f"⚠️ Model file not found: {model_path}")
373
+
374
+ except Exception as e:
375
+ classifier = None
376
+ print(f"❌ Error initializing histopathology model: {e}")
377
+
378
+ def predict_histopathology(image):
379
+ if classifier is None:
380
+ return {"error": "Histopathology model not available."}
381
+
382
+ try:
383
+ if image.mode != "RGB":
384
+ image = image.convert("RGB")
385
+ image = image.resize((224, 224))
386
+ img_array = np.expand_dims(np.array(image).astype("float32") / 255.0, axis=0)
387
+ embeddings = classifier.extract_embeddings(img_array)
388
+ prediction_proba = classifier.model.predict(embeddings, verbose=0)[0]
389
+ predicted_class = int(np.argmax(prediction_proba))
390
+ class_names = ["Benign", "Malignant"]
391
+
392
+ # Return confidence as dictionary with both class probabilities (like MWT/CIN)
393
+ confidences = {class_names[i]: float(prediction_proba[i]) for i in range(len(class_names))}
394
+ avg_confidence = float(np.max(prediction_proba)) * 100
395
+
396
+ return {
397
+ "model_used": "Histopathology Classifier",
398
+ "prediction": class_names[predicted_class],
399
+ "confidence": confidences,
400
+ "summary": {
401
+ "avg_confidence": round(avg_confidence, 2),
402
+ "ai_interpretation": f"Histopathological analysis indicates {class_names[predicted_class].lower()} tissue with {avg_confidence:.1f}% confidence.",
403
+ },
404
  }
405
+ except Exception as e:
406
+ return {"error": f"Histopathology prediction failed: {e}"}
407
+
408
 
409
  # =====================================================
410
+
411
+ # MAIN ENDPOINT
412
+
413
  # =====================================================
414
+
415
+
416
  @app.post("/predict/")
417
  async def predict(model_name: str = Form(...), file: UploadFile = File(...)):
418
+ print(f"Received prediction request - model: {model_name}, file: {file.filename}")
419
+
420
+ # Validate model name
421
+ if model_name not in ["yolo", "mwt", "cin", "histopathology"]:
422
+ return JSONResponse(
423
+ content={
424
+ "error": f"Invalid model_name: {model_name}. Must be one of: yolo, mwt, cin, histopathology"
425
+ },
426
+ status_code=400
427
+ )
428
+
429
+ # Validate and read file
430
+ if not file.filename:
431
+ return JSONResponse(
432
+ content={"error": "No file provided"},
433
+ status_code=400
434
+ )
435
+
436
  contents = await file.read()
437
+ if len(contents) == 0:
438
+ return JSONResponse(
439
+ content={"error": "Empty file provided"},
440
+ status_code=400
441
+ )
442
+
443
+ # Attempt to open and validate image
444
+ try:
445
+ image = PILImage.open(BytesIO(contents)).convert("RGB")
446
+ image_np = np.array(image)
447
+ if image_np.size == 0:
448
+ raise ValueError("Empty image array")
449
+ print(f"Successfully loaded image, shape: {image_np.shape}")
450
+ except Exception as e:
451
+ return JSONResponse(
452
+ content={"error": f"Invalid image file: {str(e)}"},
453
+ status_code=400
454
+ )
455
 
456
  if model_name == "yolo":
457
  results = yolo_model(image)
458
  detections_json = results[0].to_json()
459
  detections = json.loads(detections_json)
460
+
461
+ abnormal_cells = sum(1 for d in detections if d["name"] == "abnormal")
462
+ normal_cells = sum(1 for d in detections if d["name"] == "normal")
463
+ avg_confidence = np.mean([d.get("confidence", 0) for d in detections]) * 100 if detections else 0
464
+
465
+ ai_summary = generate_ai_summary(abnormal_cells, normal_cells, avg_confidence)
466
+
467
  output_filename = f"detected_{uuid.uuid4().hex[:8]}.jpg"
468
+ output_path = os.path.join(IMAGES_DIR, output_filename)
469
  results[0].save(filename=output_path)
470
+
471
  return {
472
  "model_used": "YOLO Detection",
473
  "detections": detections,
474
+ "annotated_image_url": f"/outputs/images/{output_filename}",
475
+ "summary": {
476
+ "abnormal_cells": abnormal_cells,
477
+ "normal_cells": normal_cells,
478
+ "avg_confidence": round(float(avg_confidence), 2),
479
+ "ai_interpretation": ai_summary,
480
+ },
481
  }
482
 
483
  elif model_name == "mwt":
 
486
  output = mwt_model(tensor.to(device)).cpu()
487
  probs = torch.softmax(output, dim=1)[0]
488
  confidences = {mwt_class_names[i]: float(probs[i]) for i in range(2)}
489
+ predicted_label = mwt_class_names[int(torch.argmax(probs).item())]
490
+ # Average / primary confidence for display
491
+ avg_confidence = float(torch.max(probs).item()) * 100
492
+
493
+ # Generate a brief AI interpretation using the Mistral client (if available)
494
+ ai_interp = generate_mwt_summary(predicted_label, confidences, avg_confidence)
495
+
496
+ return {
497
+ "model_used": "MWT Classifier",
498
+ "prediction": predicted_label,
499
+ "confidence": confidences,
500
+ "summary": {
501
+ "avg_confidence": round(avg_confidence, 2),
502
+ "ai_interpretation": ai_interp,
503
+ },
504
+ }
505
 
506
  elif model_name == "cin":
507
+ if clf is None:
508
+ return JSONResponse(
509
+ content={"error": "CIN classifier not available on server."},
510
+ status_code=503,
511
+ )
512
  nparr = np.frombuffer(contents, np.uint8)
513
  img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
514
  results = yolo_colposcopy.predict(source=img, conf=0.7, save=False, verbose=False)
515
  if len(results[0].boxes) == 0:
516
  return {"error": "No cervix detected"}
517
+
518
  x1, y1, x2, y2 = map(int, results[0].boxes.xyxy[0].cpu().numpy())
519
  crop = img[y1:y2, x1:x2]
520
  crop = cv2.resize(crop, (224, 224))
 
526
  X_scaled = MinMaxScaler().fit_transform(features)
527
  pred = clf.predict(X_scaled)[0]
528
  proba = clf.predict_proba(X_scaled)[0]
529
+ # Get actual number of classes from model output
530
+ classes = ["Low-grade", "High-grade"] # Binary CIN classification
531
  predicted_label = classes[pred]
532
  confidences = {classes[i]: float(proba[i]) for i in range(len(classes))}
533
+
534
+ # Map to more detailed classification based on confidence
535
+ if predicted_label == "High-grade" and confidences["High-grade"] > 0.8:
536
+ detailed_class = "CIN3"
537
+ elif predicted_label == "High-grade":
538
+ detailed_class = "CIN2"
539
+ else:
540
+ detailed_class = "CIN1"
541
+
542
+ # Average / primary confidence for display
543
+ avg_confidence = float(np.max(proba)) * 100
544
+
545
+ # Generate a brief AI interpretation using the Mistral client (if available)
546
+ ai_interp = generate_cin_summary(predicted_label, confidences, avg_confidence)
547
+
548
  return {
549
  "model_used": "CIN Classifier",
550
+ "prediction": detailed_class,
551
+ "grade": predicted_label,
552
+ "confidence": confidences,
553
+ "summary": {
554
+ "avg_confidence": round(avg_confidence, 2),
555
+ "ai_interpretation": ai_interp,
556
+ },
557
  }
 
558
  elif model_name == "histopathology":
559
+ result = predict_histopathology(image)
560
+ return result
561
+
562
 
563
  else:
564
  return JSONResponse(content={"error": "Invalid model name"}, status_code=400)
565
 
566
+ # =====================================================
567
+
568
+ # ROUTES
569
+
570
+ # =====================================================
571
+
572
+ def create_designed_pdf(pdf_path, report_data, analysis_summary_json):
573
+ doc = SimpleDocTemplate(pdf_path, pagesize=letter,
574
+ rightMargin=72, leftMargin=72,
575
+ topMargin=72, bottomMargin=18)
576
+ styles = getSampleStyleSheet()
577
+ story = []
578
+
579
+ styles.add(ParagraphStyle(name='Title', fontSize=20, fontName='Helvetica-Bold', alignment=TA_CENTER, textColor=navy))
580
+ styles.add(ParagraphStyle(name='Section', fontSize=14, fontName='Helvetica-Bold', spaceBefore=10, spaceAfter=6))
581
+ styles.add(ParagraphStyle(name='NormalSmall', fontSize=10, leading=12))
582
+ styles.add(ParagraphStyle(name='Heading', fontSize=16, fontName='Helvetica-Bold', textColor=navy, spaceBefore=6, spaceAfter=4))
583
+
584
+ patient = report_data['patient']
585
+ analysis = report_data.get('analysis', {})
586
+
587
+ # Safely parse analysis_summary_json
588
+ try:
589
+ ai_summary = json.loads(analysis_summary_json) if analysis_summary_json else {}
590
+ except (json.JSONDecodeError, TypeError):
591
+ ai_summary = {}
592
+
593
+ # Determine report type based on model used
594
+ model_used = ai_summary.get('model_used', '')
595
+ if 'YOLO' in model_used or 'yolo' in str(analysis.get('id', '')).lower():
596
+ report_type = "CYTOLOGY"
597
+ report_title = "Cytology Report"
598
+ elif 'CIN' in model_used or 'cin' in str(analysis.get('id', '')).lower() or 'colpo' in str(analysis.get('id', '')).lower():
599
+ report_type = "COLPOSCOPY"
600
+ report_title = "Colposcopy Report"
601
+ elif 'histo' in str(analysis.get('id', '')).lower() or 'histopathology' in model_used.lower():
602
+ report_type = "HISTOPATHOLOGY"
603
+ report_title = "Histopathology Report"
604
+ else:
605
+ report_type = "CYTOLOGY"
606
+ report_title = "Medical Analysis Report"
607
+
608
+ # Header
609
+ story.append(Paragraph("MANALIFE AI", styles['Title']))
610
+ story.append(Paragraph("Advanced Medical Analysis", styles['NormalSmall']))
611
+ story.append(Spacer(1, 0.3*inch))
612
+ story.append(Paragraph(f"MEDICAL ANALYSIS REPORT OF {report_type}", styles['Heading']))
613
+ story.append(Paragraph(report_title, styles['Section']))
614
+ story.append(Spacer(1, 0.2*inch))
615
+
616
+ # Report ID and Date
617
+ story.append(Paragraph(f"<b>Report ID:</b> {report_data.get('report_id', 'N/A')}", styles['NormalSmall']))
618
+ story.append(Paragraph(f"<b>Generated:</b> {datetime.datetime.now().strftime('%b %d, %Y, %I:%M %p')}", styles['NormalSmall']))
619
+ story.append(Spacer(1, 0.2*inch))
620
+
621
+ # Patient Information Section
622
+ story.append(Paragraph("Patient Information", styles['Section']))
623
+ story.append(Paragraph(f"<b>Patient ID:</b> {patient.get('id', 'N/A')}", styles['NormalSmall']))
624
+ story.append(Paragraph(f"<b>Exam Date:</b> {patient.get('exam_date', 'N/A')}", styles['NormalSmall']))
625
+ story.append(Paragraph(f"<b>Physician:</b> {patient.get('physician', 'N/A')}", styles['NormalSmall']))
626
+ story.append(Paragraph(f"<b>Facility:</b> {patient.get('facility', 'N/A')}", styles['NormalSmall']))
627
+ story.append(Spacer(1, 0.2*inch))
628
+
629
+ # Sample Information Section
630
+ story.append(Paragraph("Sample Information", styles['Section']))
631
+ story.append(Paragraph(f"<b>Specimen Type:</b> {patient.get('specimen_type', 'Cervical Cytology')}", styles['NormalSmall']))
632
+ story.append(Paragraph(f"<b>Clinical History:</b> {patient.get('clinical_history', 'N/A')}", styles['NormalSmall']))
633
+ story.append(Spacer(1, 0.2*inch))
634
+
635
+ # AI Analysis Section
636
+ story.append(Paragraph("AI-ASSISTED ANALYSIS", styles['Section']))
637
+ story.append(Paragraph("<b>System:</b> Manalife AI System — Automated Analysis", styles['NormalSmall']))
638
+ story.append(Paragraph(f"<b>Confidence Score:</b> {ai_summary.get('avg_confidence', 'N/A')}%", styles['NormalSmall']))
639
+
640
+ # Add metrics based on report type
641
+ if report_type == "HISTOPATHOLOGY":
642
+ # For histopathology, show Benign/Malignant confidence
643
+ confidence_dict = ai_summary.get('confidence', {})
644
+ if isinstance(confidence_dict, dict):
645
+ benign_conf = confidence_dict.get('Benign', 0) * 100
646
+ malignant_conf = confidence_dict.get('Malignant', 0) * 100
647
+ story.append(Paragraph(f"<b>Benign Confidence:</b> {benign_conf:.2f}%", styles['NormalSmall']))
648
+ story.append(Paragraph(f"<b>Malignant Confidence:</b> {malignant_conf:.2f}%", styles['NormalSmall']))
649
+ elif report_type == "CYTOLOGY":
650
+ # For cytology (YOLO), show abnormal/normal cells
651
+ if 'abnormal_cells' in ai_summary:
652
+ story.append(Paragraph(f"<b>Abnormal Cells:</b> {ai_summary.get('abnormal_cells', 'N/A')}", styles['NormalSmall']))
653
+ if 'normal_cells' in ai_summary:
654
+ story.append(Paragraph(f"<b>Normal Cells:</b> {ai_summary.get('normal_cells', 'N/A')}", styles['NormalSmall']))
655
+ else:
656
+ # For CIN/Colposcopy, show class confidences
657
+ confidence_dict = ai_summary.get('confidence', {})
658
+ if isinstance(confidence_dict, dict):
659
+ for cls, val in confidence_dict.items():
660
+ conf_pct = val * 100 if isinstance(val, (int, float)) else 0
661
+ story.append(Paragraph(f"<b>{cls} Confidence:</b> {conf_pct:.2f}%", styles['NormalSmall']))
662
+
663
+ story.append(Spacer(1, 0.1*inch))
664
+ story.append(Paragraph("<b>AI Interpretation:</b>", styles['NormalSmall']))
665
+ story.append(Paragraph(ai_summary.get('ai_interpretation', 'Not available.'), styles['NormalSmall']))
666
+ story.append(Spacer(1, 0.2*inch))
667
+
668
+ # Doctor's Notes
669
+ story.append(Paragraph("Doctor's Notes", styles['Section']))
670
+ story.append(Paragraph(report_data.get('doctor_notes') or 'No additional notes provided.', styles['NormalSmall']))
671
+ story.append(Spacer(1, 0.2*inch))
672
+
673
+ # Recommendations
674
+ story.append(Paragraph("RECOMMENDATIONS", styles['Section']))
675
+ story.append(Paragraph("Continue routine screening as per standard guidelines. Follow up as directed by your physician.", styles['NormalSmall']))
676
+ story.append(Spacer(1, 0.3*inch))
677
+
678
+ # Signatures
679
+ story.append(Paragraph("Signatures", styles['Section']))
680
+ story.append(Paragraph("Dr. Emily Roberts, MD (Cytopathologist)", styles['NormalSmall']))
681
+ story.append(Paragraph("Dr. James Wilson, MD (Pathologist)", styles['NormalSmall']))
682
+ story.append(Spacer(1, 0.1*inch))
683
+ story.append(Paragraph(f"Generated on: {datetime.datetime.now().strftime('%b %d, %Y, %I:%M %p')}", styles['NormalSmall']))
684
+
685
+ doc.build(story)
686
+
687
+
688
+
689
+ @app.post("/reports/")
690
+ async def generate_report(
691
+ patient_id: str = Form(...),
692
+ exam_date: str = Form(...),
693
+ metadata: str = Form(...),
694
+ notes: str = Form(None),
695
+ analysis_id: str = Form(None),
696
+ analysis_summary: str = Form(None),
697
+ ):
698
+ """Generate a structured medical report from analysis results and metadata."""
699
+ try:
700
+ # Create reports directory if it doesn't exist
701
+ reports_dir = os.path.join(OUTPUT_DIR, "reports")
702
+ os.makedirs(reports_dir, exist_ok=True)
703
+
704
+ # Generate unique report ID
705
+ report_id = f"{patient_id}_{uuid.uuid4().hex[:8]}"
706
+ report_dir = os.path.join(reports_dir, report_id)
707
+ os.makedirs(report_dir, exist_ok=True)
708
+
709
+ # Parse metadata
710
+ metadata_dict = json.loads(metadata)
711
+
712
+ # Get analysis results - assuming stored in memory or retrievable
713
+ # TODO: Implement analysis results storage/retrieval
714
+
715
+ # Construct report data
716
+ report_data = {
717
+ "report_id": report_id,
718
+ "generated_at": datetime.datetime.now().isoformat(),
719
+ "patient": {
720
+ "id": patient_id,
721
+ "exam_date": exam_date,
722
+ **metadata_dict
723
+ },
724
+ "analysis": {
725
+ "id": analysis_id,
726
+ # If the analysis_id is actually an annotated image URL, store it for report embedding
727
+ "annotated_image_url": analysis_id,
728
+ # TODO: Add actual analysis results
729
+ },
730
+ "doctor_notes": notes
731
+ }
732
+
733
+ # Save report data
734
+ report_json = os.path.join(report_dir, "report.json")
735
+ with open(report_json, "w", encoding="utf-8") as f:
736
+ json.dump(report_data, f, indent=2, ensure_ascii=False)
737
+
738
+ # Attempt to create a PDF version if reportlab is available
739
+ pdf_url = None
740
+ if REPORTLAB_AVAILABLE:
741
+ try:
742
+ pdf_path = os.path.join(report_dir, "report.pdf")
743
+ create_designed_pdf(pdf_path, report_data, analysis_summary)
744
+ pdf_url = f"/outputs/reports/{report_id}/report.pdf"
745
+ except Exception as e:
746
+ print(f"Error creating designed PDF: {e}")
747
+ pdf_url = None
748
+
749
+ # Parse analysis_summary to get AI results
750
+ try:
751
+ ai_summary = json.loads(analysis_summary) if analysis_summary else {}
752
+ except (json.JSONDecodeError, TypeError):
753
+ ai_summary = {}
754
+
755
+ # Determine report type based on analysis summary or model used
756
+ model_used = ai_summary.get('model_used', '')
757
+ if 'YOLO' in model_used or 'yolo' in str(analysis_id).lower():
758
+ report_type = "Cytology"
759
+ report_title = "Cytology Report"
760
+ elif 'CIN' in model_used or 'cin' in str(analysis_id).lower() or 'colpo' in str(analysis_id).lower():
761
+ report_type = "Colposcopy"
762
+ report_title = "Colposcopy Report"
763
+ elif 'histo' in str(analysis_id).lower() or 'histopathology' in model_used.lower():
764
+ report_type = "Histopathology"
765
+ report_title = "Histopathology Report"
766
+ else:
767
+ # Default fallback
768
+ report_type = "Cytology"
769
+ report_title = "Medical Analysis Report"
770
+
771
+ # Build analysis metrics HTML based on report type
772
+ if report_type == "Histopathology":
773
+ # For histopathology, show Benign/Malignant confidence from the confidence dict
774
+ confidence_dict = ai_summary.get('confidence', {})
775
+ benign_conf = confidence_dict.get('Benign', 0) * 100 if isinstance(confidence_dict, dict) else 0
776
+ malignant_conf = confidence_dict.get('Malignant', 0) * 100 if isinstance(confidence_dict, dict) else 0
777
+
778
+ analysis_metrics_html = f"""
779
+ <tr><th>System</th><td>Manalife AI System — Automated Analysis</td></tr>
780
+ <tr><th>Confidence Score</th><td>{ai_summary.get('avg_confidence', 'N/A')}%</td></tr>
781
+ <tr><th>Benign Confidence</th><td>{benign_conf:.2f}%</td></tr>
782
+ <tr><th>Malignant Confidence</th><td>{malignant_conf:.2f}%</td></tr>
783
+ """
784
+ elif report_type == "Cytology":
785
+ # For cytology (YOLO), show abnormal/normal cells
786
+ analysis_metrics_html = f"""
787
+ <tr><th>System</th><td>Manalife AI System — Automated Analysis</td></tr>
788
+ <tr><th>Confidence Score</th><td>{ai_summary.get('avg_confidence', 'N/A')}%</td></tr>
789
+ <tr><th>Abnormal Cells</th><td>{ai_summary.get('abnormal_cells', 'N/A')}</td></tr>
790
+ <tr><th>Normal Cells</th><td>{ai_summary.get('normal_cells', 'N/A')}</td></tr>
791
+ """
792
+ else:
793
+ # For CIN/Colposcopy or other models, show generic confidence
794
+ confidence_dict = ai_summary.get('confidence', {})
795
+ confidence_rows = ""
796
+ if isinstance(confidence_dict, dict):
797
+ for cls, val in confidence_dict.items():
798
+ conf_pct = val * 100 if isinstance(val, (int, float)) else 0
799
+ confidence_rows += f"<tr><th>{cls} Confidence</th><td>{conf_pct:.2f}%</td></tr>\n "
800
+
801
+ analysis_metrics_html = f"""
802
+ <tr><th>System</th><td>Manalife AI System — Automated Analysis</td></tr>
803
+ <tr><th>Confidence Score</th><td>{ai_summary.get('avg_confidence', 'N/A')}%</td></tr>
804
+ {confidence_rows}
805
+ """
806
+
807
+ # Build final HTML including download links and embedded annotated image
808
+ report_html = os.path.join(report_dir, "report.html")
809
+ json_url = f"/outputs/reports/{report_id}/report.json"
810
+ html_url = f"/outputs/reports/{report_id}/report.html"
811
+ annotated_img = report_data.get("analysis", {}).get("annotated_image_url") or ""
812
+
813
+ # Get base URL for the annotated image (if it's a relative path)
814
+ annotated_img_full = f"http://localhost:8000{annotated_img}" if annotated_img and annotated_img.startswith('/') else annotated_img
815
+
816
+ download_pdf_btn = f'<a href="{pdf_url}" download style="text-decoration:none"><button class="btn-secondary">Download PDF</button></a>' if pdf_url else ''
817
+
818
+ # Format generated time
819
+ generated_time = datetime.datetime.now().strftime('%b %d, %Y, %I:%M %p')
820
+
821
+ html_content = f"""<!doctype html>
822
+ <html lang="en">
823
+ <head>
824
+ <meta charset="utf-8" />
825
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
826
+ <title>Medical Analysis Report — Manalife AI</title>
827
+ <style>
828
+ :root{{--bg:#f8fafc;--card:#ffffff;--muted:#6b7280;--accent:#0f172a}}
829
+ body{{font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial;margin:0;background:var(--bg);color:var(--accent);line-height:1.45}}
830
+ .container{{max-width:900px;margin:36px auto;padding:20px}}
831
+ header{{display:flex;align-items:center;gap:16px}}
832
+ .brand{{display:flex;flex-direction:column}}
833
+ h1{{margin:0;font-size:20px}}
834
+ .sub{{color:var(--muted);font-size:13px}}
835
+ .card{{background:var(--card);box-shadow:0 6px 18px rgba(15,23,42,0.06);border-radius:12px;padding:20px;margin-top:18px}}
836
+ .grid{{display:grid;grid-template-columns:1fr 1fr;gap:12px}}
837
+ .section-title{{font-weight:600;margin-top:8px}}
838
+ table{{width:100%;border-collapse:collapse;margin-top:8px}}
839
+ td,th{{padding:8px;border-bottom:1px dashed #e6e9ef;text-align:left;font-size:14px}}
840
+ .full{{grid-column:1/-1}}
841
+ .muted{{color:var(--muted);font-size:13px}}
842
+ .footer{{margin-top:20px;font-size:13px;color:var(--muted)}}
843
+ .pill{{background:#eef2ff;color:#1e3a8a;padding:6px 10px;border-radius:999px;font-weight:600;font-size:13px}}
844
+ @media (max-width:700px){{.grid{{grid-template-columns:1fr}}}}
845
+ .signatures{{display:flex;gap:20px;flex-wrap:wrap;margin-top:12px}}
846
+ .sig{{background:#fbfbfd;border:1px solid #f0f1f5;padding:10px;border-radius:8px;min-width:180px}}
847
+ .annotated-image{{max-width:100%;height:auto;border-radius:8px;margin-top:12px;border:1px solid #e6e9ef}}
848
+ .btn-primary{{padding:10px 14px;border-radius:8px;border:1px solid #2563eb;background:#2563eb;color:white;font-weight:700;cursor:pointer}}
849
+ .btn-secondary{{padding:10px 14px;border-radius:8px;border:1px solid #e6eefc;background:#eef2ff;font-weight:700;cursor:pointer}}
850
+ .actions-bar{{margin-top:12px;display:flex;gap:8px;flex-wrap:wrap}}
851
+ </style>
852
+ </head>
853
+ <body>
854
+ <div class="container">
855
+ <header>
856
+ <div>
857
+ <!-- Use the static logo from frontend public/ (copied to dist by Vite) -->
858
+ <img src="/manalife_LOGO.jpg" alt="Manalife Logo" width="64" height="64">
859
+ </div>
860
+ <div class="brand">
861
+ <h1>MANALIFE AI — Medical Analysis</h1>
862
+ <div class="sub">Advanced cytological colposcopy and histopathology reporting</div>
863
+ <div class="muted">[email protected] • +1 (555) 123-4567</div>
864
+ </div>
865
+ </header>
866
+
867
+ <div class="card">
868
+ <div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap">
869
+ <div>
870
+ <div class="muted">MEDICAL ANALYSIS REPORT OF {report_type.upper()}</div>
871
+ <h2 style="margin:6px 0 0 0">{report_title}</h2>
872
+ </div>
873
+ <div style="text-align:right">
874
+ <div class="pill">Report ID: {report_id}</div>
875
+ <div class="muted" style="margin-top:6px">Generated: {generated_time}</div>
876
+ </div>
877
+ </div>
878
+
879
+ <hr style="border:none;border-top:1px solid #eef2f6;margin:16px 0">
880
+
881
+ <div class="grid">
882
+ <div>
883
+ <div class="section-title">Patient Information</div>
884
+ <table>
885
+ <tr><th>Patient ID</th><td>{patient_id}</td></tr>
886
+ <tr><th>Exam Date</th><td>{exam_date}</td></tr>
887
+ <tr><th>Physician</th><td>{metadata_dict.get('physician', 'N/A')}</td></tr>
888
+ <tr><th>Facility</th><td>{metadata_dict.get('facility', 'N/A')}</td></tr>
889
+ </table>
890
+ </div>
891
+
892
+ <div>
893
+ <div class="section-title">Sample Information</div>
894
+ <table>
895
+ <tr><th>Specimen Type</th><td>{metadata_dict.get('specimen_type', 'N/A')}</td></tr>
896
+ <tr><th>Clinical History</th><td>{metadata_dict.get('clinical_history', 'N/A')}</td></tr>
897
+ <tr><th>Collected</th><td>{exam_date}</td></tr>
898
+ <tr><th>Reported</th><td>{generated_time}</td></tr>
899
+ </table>
900
+ </div>
901
+
902
+ <div class="full">
903
+ <div class="section-title">AI-Assisted Analysis</div>
904
+ <table>
905
+ {analysis_metrics_html}
906
+ </table>
907
+ <div style="margin-top:12px;padding:12px;background:#f8fafc;border-radius:8px;border-left:4px solid #2563eb">
908
+ <div style="font-weight:600;margin-bottom:6px">AI Interpretation:</div>
909
+ <div class="muted">{ai_summary.get('ai_interpretation', 'No AI interpretation available.')}</div>
910
+ </div>
911
+ </div>
912
+
913
+ {'<div class="full"><div class="section-title">Annotated Analysis Image</div><img src="' + annotated_img_full + '" class="annotated-image" alt="Annotated Analysis Result" /></div>' if annotated_img else ''}
914
+
915
+ <div class="full">
916
+ <div class="section-title">Doctor\'s Notes</div>
917
+ <p class="muted">{notes or 'No additional notes provided.'}</p>
918
+ </div>
919
+
920
+ <div class="full">
921
+ <div class="section-title">Recommendations</div>
922
+ <p class="muted">Continue routine screening as per standard guidelines. Follow up as directed by your physician.</p>
923
+ </div>
924
+
925
+ <div class="full">
926
+ <div class="section-title">Signatures</div>
927
+ <div class="signatures">
928
+ <div class="sig">
929
+ <div style="font-weight:700">Dr. Emily Roberts</div>
930
+ <div class="muted">MD, pathologist</div>
931
+ </div>
932
+ <div class="sig">
933
+ <div style="font-weight:700">Dr. James Wilson</div>
934
+ <div class="muted">MD, pathologist</div>
935
+ </div>
936
+ </div>
937
+ </div>
938
+ </div>
939
+
940
+ <div class="footer">
941
+ <div>AI System: Manalife AI — Automated Analysis</div>
942
+ <div style="margin-top:6px">Report generated: {report_data['generated_at']}</div>
943
+ </div>
944
+ </div>
945
+
946
+ <div class="actions-bar">
947
+ {download_pdf_btn}
948
+ <button class="btn-secondary" onclick="window.print()">Print Report</button>
949
+ </div>
950
+ </div>
951
+ </body>
952
+ </html>"""
953
+
954
+ with open(report_html, "w", encoding="utf-8") as f:
955
+ f.write(html_content)
956
+
957
+ return {
958
+ "report_id": report_id,
959
+ "json_url": json_url,
960
+ "html_url": html_url,
961
+ "pdf_url": pdf_url,
962
+ }
963
+
964
+ except Exception as e:
965
+ return JSONResponse(
966
+ content={"error": f"Failed to generate report: {str(e)}"},
967
+ status_code=500
968
+ )
969
+
970
+ @app.get("/reports/{report_id}")
971
+ async def get_report(report_id: str):
972
+ """Fetch a generated report by ID."""
973
+ report_dir = os.path.join(OUTPUT_DIR, "reports", report_id)
974
+ report_json = os.path.join(report_dir, "report.json")
975
+
976
+ if not os.path.exists(report_json):
977
+ return JSONResponse(
978
+ content={"error": "Report not found"},
979
+ status_code=404
980
+ )
981
+
982
+ with open(report_json, "r") as f:
983
+ report_data = json.load(f)
984
+
985
+ return report_data
986
+
987
+ @app.get("/reports")
988
+ async def list_reports(patient_id: str = None):
989
+ """List all generated reports, optionally filtered by patient ID."""
990
+ reports_dir = os.path.join(OUTPUT_DIR, "reports")
991
+ if not os.path.exists(reports_dir):
992
+ return {"reports": []}
993
+
994
+ reports = []
995
+ for report_id in os.listdir(reports_dir):
996
+ report_json = os.path.join(reports_dir, report_id, "report.json")
997
+ if os.path.exists(report_json):
998
+ with open(report_json, "r") as f:
999
+ report_data = json.load(f)
1000
+ if not patient_id or report_data["patient"]["id"] == patient_id:
1001
+ reports.append({
1002
+ "report_id": report_id,
1003
+ "patient_id": report_data["patient"]["id"],
1004
+ "exam_date": report_data["patient"]["exam_date"],
1005
+ "generated_at": report_data["generated_at"]
1006
+ })
1007
+
1008
+ return {"reports": sorted(reports, key=lambda r: r["generated_at"], reverse=True)}
1009
 
1010
  @app.get("/models")
1011
  def get_models():
1012
  return {"available_models": ["yolo", "mwt", "cin", "histopathology"]}
1013
 
 
1014
  @app.get("/health")
1015
  def health():
1016
+ return {"message": "Pathora Medical Diagnostic API is running!"}
1017
+
1018
+ # FRONTEND
1019
+
1020
+ # =====================================================
1021
 
 
 
 
 
1022
 
1023
+ # Serve frontend only if it has been built; avoid startup failure when dist/ is missing.
1024
+ FRONTEND_DIST = os.path.abspath(os.path.join(os.path.dirname(__file__), "../frontend/dist"))
1025
 
1026
+ # Check if frontend/dist exists in /app (Docker), otherwise check relative to script location
1027
+ if not os.path.isdir(FRONTEND_DIST):
1028
+ # Fallback for Docker: frontend is copied to ./frontend/dist during build
1029
+ FRONTEND_DIST = os.path.join(os.path.dirname(__file__), "frontend/dist")
1030
+
1031
+ ASSETS_DIR = os.path.join(FRONTEND_DIST, "assets")
1032
+
1033
+ if os.path.isdir(ASSETS_DIR):
1034
+ app.mount("/assets", StaticFiles(directory=ASSETS_DIR), name="assets")
1035
+ else:
1036
+ print("ℹ️ Frontend assets directory not found — skipping /assets mount.")
1037
 
1038
  @app.get("/")
1039
  async def serve_frontend():
1040
+ index_path = os.path.join(FRONTEND_DIST, "index.html")
1041
+ if os.path.isfile(index_path):
1042
+ return FileResponse(index_path)
1043
+ return JSONResponse({"message": "Backend is running. Frontend build not found."})
1044
+
1045
+ @app.get("/{file_path:path}")
1046
+ async def serve_static_files(file_path: str):
1047
+ """Serve static files from frontend dist (images, logos, etc.)"""
1048
+ # Skip API routes
1049
+ if file_path.startswith(("predict", "reports", "models", "health", "outputs", "assets", "cyto", "colpo", "histo")):
1050
+ return JSONResponse({"error": "Not found"}, status_code=404)
1051
+
1052
+ # Try to serve file from dist root
1053
+ static_file = os.path.join(FRONTEND_DIST, file_path)
1054
+ if os.path.isfile(static_file):
1055
+ return FileResponse(static_file)
1056
+
1057
+ # Fallback to index.html for client-side routing
1058
+ index_path = os.path.join(FRONTEND_DIST, "index.html")
1059
+ if os.path.isfile(index_path):
1060
+ return FileResponse(index_path)
1061
+
1062
+ return JSONResponse({"error": "Not found"}, status_code=404)
1063
 
1064
  if __name__ == "__main__":
1065
+ # Use PORT provided by the environment (Hugging Face Spaces sets PORT=7860)
1066
+ port = int(os.environ.get("PORT", 7860))
1067
+ uvicorn.run(app, host="0.0.0.0", port=port)
frontend/public/cyto/cyt1.jpg → backend/outputs/detected_1a8f90ea.jpg RENAMED
File without changes
frontend/public/cyto/cyt3.png → backend/outputs/detected_1c20231d.jpg RENAMED
File without changes
backend/outputs/detected_2198d45d.jpg ADDED

Git LFS Details

  • SHA256: 406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e
  • Pointer size: 131 Bytes
  • Size of remote file: 863 kB
backend/outputs/detected_258af4fa.jpg ADDED

Git LFS Details

  • SHA256: 406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e
  • Pointer size: 131 Bytes
  • Size of remote file: 863 kB
backend/outputs/detected_268b6165.jpg ADDED

Git LFS Details

  • SHA256: e83314a6775b3665bdd77b769922222da8268e43871b489739a69fdcbf1970e6
  • Pointer size: 132 Bytes
  • Size of remote file: 1.44 MB
backend/outputs/detected_39406fb4.jpg ADDED

Git LFS Details

  • SHA256: 6d01f349abe5183ae4bdbcd42a4ad40be5e073d45489cf5c1fd5a5f12091b8e4
  • Pointer size: 131 Bytes
  • Size of remote file: 251 kB
backend/outputs/detected_39c91983.jpg ADDED

Git LFS Details

  • SHA256: 6d01f349abe5183ae4bdbcd42a4ad40be5e073d45489cf5c1fd5a5f12091b8e4
  • Pointer size: 131 Bytes
  • Size of remote file: 251 kB
backend/outputs/detected_46cf6466.jpg ADDED

Git LFS Details

  • SHA256: 6d01f349abe5183ae4bdbcd42a4ad40be5e073d45489cf5c1fd5a5f12091b8e4
  • Pointer size: 131 Bytes
  • Size of remote file: 251 kB
backend/outputs/detected_48f5ddde.jpg ADDED

Git LFS Details

  • SHA256: 406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e
  • Pointer size: 131 Bytes
  • Size of remote file: 863 kB
backend/outputs/detected_4dc34d38.jpg ADDED

Git LFS Details

  • SHA256: 406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e
  • Pointer size: 131 Bytes
  • Size of remote file: 863 kB
backend/outputs/detected_4e71956d.jpg ADDED

Git LFS Details

  • SHA256: 406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e
  • Pointer size: 131 Bytes
  • Size of remote file: 863 kB
backend/outputs/detected_53ef21ce.jpg ADDED

Git LFS Details

  • SHA256: 91944d821c3f975756875882ead5953ca6bf39834f799e7e22f47149141bec7f
  • Pointer size: 129 Bytes
  • Size of remote file: 8.82 kB
backend/outputs/detected_5af838c6.jpg ADDED

Git LFS Details

  • SHA256: 406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e
  • Pointer size: 131 Bytes
  • Size of remote file: 863 kB
backend/outputs/detected_5e93888d.jpg ADDED

Git LFS Details

  • SHA256: 406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e
  • Pointer size: 131 Bytes
  • Size of remote file: 863 kB
backend/outputs/detected_6526bf9c.jpg ADDED

Git LFS Details

  • SHA256: e83314a6775b3665bdd77b769922222da8268e43871b489739a69fdcbf1970e6
  • Pointer size: 132 Bytes
  • Size of remote file: 1.44 MB
backend/outputs/detected_669a5877.jpg ADDED

Git LFS Details

  • SHA256: 2c91fa3e78d8106656cdeace2f16de48742e3042e265057712922c6653cad828
  • Pointer size: 132 Bytes
  • Size of remote file: 1.32 MB
backend/outputs/detected_85aa7683.jpg ADDED

Git LFS Details

  • SHA256: 6d01f349abe5183ae4bdbcd42a4ad40be5e073d45489cf5c1fd5a5f12091b8e4
  • Pointer size: 131 Bytes
  • Size of remote file: 251 kB
backend/outputs/detected_8eed01a5.jpg ADDED

Git LFS Details

  • SHA256: 406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e
  • Pointer size: 131 Bytes
  • Size of remote file: 863 kB
backend/outputs/detected_907c0662.jpg ADDED

Git LFS Details

  • SHA256: 406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e
  • Pointer size: 131 Bytes
  • Size of remote file: 863 kB
backend/outputs/detected_91737a8d.jpg ADDED

Git LFS Details

  • SHA256: 406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e
  • Pointer size: 131 Bytes
  • Size of remote file: 863 kB
backend/outputs/detected_abff463d.jpg ADDED

Git LFS Details

  • SHA256: 406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e
  • Pointer size: 131 Bytes
  • Size of remote file: 863 kB
backend/outputs/detected_b02db619.jpg ADDED

Git LFS Details

  • SHA256: e83314a6775b3665bdd77b769922222da8268e43871b489739a69fdcbf1970e6
  • Pointer size: 132 Bytes
  • Size of remote file: 1.44 MB
backend/outputs/detected_cb4d377f.jpg ADDED

Git LFS Details

  • SHA256: 406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e
  • Pointer size: 131 Bytes
  • Size of remote file: 863 kB
backend/outputs/detected_df1af137.jpg ADDED

Git LFS Details

  • SHA256: 6d01f349abe5183ae4bdbcd42a4ad40be5e073d45489cf5c1fd5a5f12091b8e4
  • Pointer size: 131 Bytes
  • Size of remote file: 251 kB
backend/outputs/detected_e01aa42a.jpg ADDED

Git LFS Details

  • SHA256: 6d01f349abe5183ae4bdbcd42a4ad40be5e073d45489cf5c1fd5a5f12091b8e4
  • Pointer size: 131 Bytes
  • Size of remote file: 251 kB
backend/outputs/detected_e5762460.jpg ADDED

Git LFS Details

  • SHA256: e83314a6775b3665bdd77b769922222da8268e43871b489739a69fdcbf1970e6
  • Pointer size: 132 Bytes
  • Size of remote file: 1.44 MB
backend/outputs/detected_e705fd2d.jpg ADDED

Git LFS Details

  • SHA256: 406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e
  • Pointer size: 131 Bytes
  • Size of remote file: 863 kB
backend/outputs/detected_e9a00302.jpg ADDED

Git LFS Details

  • SHA256: 406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e
  • Pointer size: 131 Bytes
  • Size of remote file: 863 kB
backend/outputs/detected_fd305c21.jpg ADDED

Git LFS Details

  • SHA256: 6d01f349abe5183ae4bdbcd42a4ad40be5e073d45489cf5c1fd5a5f12091b8e4
  • Pointer size: 131 Bytes
  • Size of remote file: 251 kB
backend/outputs/images/detected_104914d9.jpg ADDED

Git LFS Details

  • SHA256: 406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e
  • Pointer size: 131 Bytes
  • Size of remote file: 863 kB
backend/outputs/images/detected_14e068c7.jpg ADDED

Git LFS Details

  • SHA256: 6d01f349abe5183ae4bdbcd42a4ad40be5e073d45489cf5c1fd5a5f12091b8e4
  • Pointer size: 131 Bytes
  • Size of remote file: 251 kB
backend/outputs/images/detected_3861cc56.jpg ADDED

Git LFS Details

  • SHA256: 6d01f349abe5183ae4bdbcd42a4ad40be5e073d45489cf5c1fd5a5f12091b8e4
  • Pointer size: 131 Bytes
  • Size of remote file: 251 kB
backend/outputs/images/detected_5e5e7d55.jpg ADDED

Git LFS Details

  • SHA256: 6d01f349abe5183ae4bdbcd42a4ad40be5e073d45489cf5c1fd5a5f12091b8e4
  • Pointer size: 131 Bytes
  • Size of remote file: 251 kB
backend/outputs/images/detected_7b413ad9.jpg ADDED

Git LFS Details

  • SHA256: 406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e
  • Pointer size: 131 Bytes
  • Size of remote file: 863 kB
backend/outputs/images/detected_7d0787ca.jpg ADDED

Git LFS Details

  • SHA256: abfdd2c63ba9ec5e08dd8e51f235e7b3264b82b8473d8963e101d547be771f0d
  • Pointer size: 131 Bytes
  • Size of remote file: 870 kB
backend/outputs/images/detected_8e6cc7ce.jpg ADDED

Git LFS Details

  • SHA256: 6d01f349abe5183ae4bdbcd42a4ad40be5e073d45489cf5c1fd5a5f12091b8e4
  • Pointer size: 131 Bytes
  • Size of remote file: 251 kB
backend/outputs/images/detected_a13a604c.jpg ADDED

Git LFS Details

  • SHA256: e83314a6775b3665bdd77b769922222da8268e43871b489739a69fdcbf1970e6
  • Pointer size: 132 Bytes
  • Size of remote file: 1.44 MB
backend/outputs/images/detected_ca6e3067.jpg ADDED

Git LFS Details

  • SHA256: e83314a6775b3665bdd77b769922222da8268e43871b489739a69fdcbf1970e6
  • Pointer size: 132 Bytes
  • Size of remote file: 1.44 MB