diff --git a/Dockerfile b/Dockerfile index 689c8d17bc67ffc5cea2ad069b0026953f336d46..73af6605a80c5e74b3743a1292cfcbdeace6e2a8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,8 +36,8 @@ RUN pip install -r requirements.txt || pip install -r backend/requirements.txt | # Install runtime dependencies explicitly RUN pip install --no-cache-dir fastapi uvicorn python-multipart ultralytics opencv-python-headless pillow numpy scikit-learn tensorflow keras -# Hugging Face Spaces expect port 7860 +# Hugging Face Spaces sets PORT (default 7860). Listen on $PORT for compatibility. EXPOSE 7860 -# Run FastAPI app -CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"] +# Run FastAPI app (respect PORT env var if set by the platform) +CMD ["sh", "-c", "uvicorn app:app --host 0.0.0.0 --port ${PORT:-7860}"] diff --git a/HUGGINGFACE_DEPLOYMENT.md b/HUGGINGFACE_DEPLOYMENT.md new file mode 100644 index 0000000000000000000000000000000000000000..46d2bdf9b3550aa890e889ff51cd6c5c6cc31336 --- /dev/null +++ b/HUGGINGFACE_DEPLOYMENT.md @@ -0,0 +1,216 @@ +# Hugging Face Spaces Deployment Guide + +## ✅ Changes Made for Hugging Face Spaces Compatibility + +### 1. Port Configuration +- **Updated `backend/app.py`**: Server now reads `PORT` from environment variable (default: 7860) + ```python + port = int(os.environ.get("PORT", 7860)) + uvicorn.run(app, host="0.0.0.0", port=port) + ``` +- **Updated `Dockerfile`**: CMD uses `${PORT:-7860}` for dynamic port binding + +### 2. Filesystem Permissions +- **Changed output directory**: `OUTPUT_DIR` now uses `/tmp/outputs` instead of `./outputs` + - Hugging Face Spaces containers have read-only `/app` directory + - `/tmp` is writable for temporary files + - **Note**: Files in `/tmp` are ephemeral and lost on restart + +### 3. Static File Serving +- **Fixed sample image serving**: Mounted `/cyto`, `/colpo`, `/histo` directories from `frontend/dist` +- **Added catch-all route**: Serves static files (logos, banners) from dist root +- **Frontend dist path fallback**: Checks both `./frontend/dist` (Docker) and `../frontend/dist` (local dev) + +### 4. Frontend Configuration +- **Frontend already configured**: Uses `window.location.origin` in production, so API calls work on any domain +- **Vite build**: Copies `public/` contents to `dist/` automatically + +--- + +## 📋 Deployment Checklist + +### Step 1: Create Hugging Face Space +1. Go to https://huggingface.co/spaces +2. Click **"Create new Space"** +3. Choose: + - **Space SDK**: Docker + - **Hardware**: CPU Basic (free) or GPU (for faster inference) + - **Visibility**: Public or Private + +### Step 2: Set Up Git LFS (for large model files) +Your project has large model files (`.pt`, `.pth`, `.keras`). Track them with Git LFS: + +```bash +# Install Git LFS if not already installed +git lfs install + +# Track model files +git lfs track "*.pt" +git lfs track "*.pth" +git lfs track "*.keras" +git lfs track "*.pkl" + +# Commit .gitattributes +git add .gitattributes +git commit -m "Track model files with Git LFS" +``` + +### Step 3: Configure Secrets (Optional) +If you want AI-generated summaries using Mistral, add a secret: + +1. Go to Space Settings → Variables and secrets +2. Add new secret: + - Name: `HF_TOKEN` + - Value: Your Hugging Face token (from https://huggingface.co/settings/tokens) + +### Step 4: Push Code to Space +```bash +# Add Space as remote +git remote add space https://huggingface.co/spaces// + +# Push to Space +git push space main +``` + +### Step 5: Monitor Build +- Hugging Face will build the Docker image (this may take 10-20 minutes) +- Watch logs in the Space's "Logs" tab +- Once built, the Space will automatically start + +--- + +## 🔍 Troubleshooting + +### Build Issues + +**Problem**: Docker build times out or fails +- **Solution**: Reduce image size by pinning lighter dependencies in `requirements.txt` +- **Solution**: Consider using pre-built wheels for TensorFlow/PyTorch + +**Problem**: Model files not found +- **Solution**: Ensure Git LFS is configured and model files are committed +- **Solution**: Check that model paths in `backend/app.py` match actual filenames + +### Runtime Issues + +**Problem**: 404 errors for sample images +- **Solution**: Rebuild frontend: `cd frontend && npm run build` +- **Solution**: Verify `frontend/public/` contents are copied to `dist/` + +**Problem**: Permission denied errors +- **Solution**: All writes should go to `/tmp/outputs` (already fixed) +- **Solution**: Never write to `/app` directory + +**Problem**: Port binding errors +- **Solution**: Use `$PORT` env var (already configured in Dockerfile and app.py) + +### Performance Issues + +**Problem**: Slow startup or inference +- **Solution**: Models load at startup; consider lazy loading on first request +- **Solution**: Upgrade to GPU hardware tier for faster inference +- **Solution**: Add caching for model weights + +--- + +## 📁 File Structure Expected in Space + +``` +/app/ +├── app.py # Main FastAPI app +├── model.py, model_histo.py, etc. # Model definitions +├── augmentations.py # Image preprocessing +├── requirements.txt # Python dependencies +├── best2.pt # YOLO cytology model +├── MWTclass2.pth # MWT classifier +├── yolo_colposcopy.pt # YOLO colposcopy model +├── histopathology_trained_model.keras # Histopathology model +├── logistic_regression_model.pkl # CIN classifier (optional) +└── frontend/ + └── dist/ # Built frontend + ├── index.html + ├── assets/ # JS/CSS bundles + ├── cyto/ # Sample cytology images + ├── colpo/ # Sample colposcopy images + ├── histo/ # Sample histopathology images + └── *.png, *.jpeg # Logos, banners +``` + +--- + +## 🌐 Access Your Space + +Once deployed, your app will be available at: +``` +https://huggingface.co/spaces// +``` + +The frontend serves at `/` and the API is accessible at: +- `POST /predict/` - Run model inference +- `POST /reports/` - Generate medical reports +- `GET /health` - Health check +- `GET /models` - List available models + +--- + +## âš ī¸ Important Notes + +### Ephemeral Storage +- Files in `/tmp/outputs` are **lost on restart** +- For persistent reports, consider: + - Downloading immediately after generation + - Uploading to external storage (S3, Hugging Face Datasets) + - Using Persistent Storage (requires paid tier) + +### Model Loading Time +- All models load at startup (~30-60 seconds) +- First request after restart may be slower +- Consider implementing health check endpoint that waits for models + +### Resource Limits +- Free CPU tier: Limited RAM and CPU +- Models are memory-intensive (TensorFlow + PyTorch + YOLO) +- May need **CPU Upgrade** or **GPU** tier for production use + +### CORS +- Currently allows all origins (`allow_origins=["*"]`) +- For production, restrict to your Space domain + +--- + +## 🚀 Next Steps After Deployment + +1. **Test all three models**: + - Upload cytology sample → Test YOLO detection + - Upload colposcopy sample → Test CIN classification + - Upload histopathology sample → Test breast cancer classification + +2. **Generate a test report**: + - Run an analysis + - Fill out patient metadata + - Generate HTML/PDF report + - Verify download links work + +3. **Monitor performance**: + - Check inference times + - Monitor memory usage in Space logs + - Consider upgrading hardware if needed + +4. **Share your Space**: + - Add a README with usage instructions + - Include sample images in the repo + - Add citations for model papers + +--- + +## 📞 Support + +If you encounter issues: +1. Check Space logs: Settings → Logs +2. Verify all model files are present: Settings → Files +3. Test locally with Docker: `docker build -t pathora . && docker run -p 7860:7860 pathora` +4. Open an issue on Hugging Face Discuss: https://discuss.huggingface.co/ + +--- + +**Deployment ready! 🎉** diff --git a/backend/Ultralytics/settings.json b/backend/Ultralytics/settings.json new file mode 100644 index 0000000000000000000000000000000000000000..a4b68e608aa85907d2291abd5d27cfd64381418e --- /dev/null +++ b/backend/Ultralytics/settings.json @@ -0,0 +1,21 @@ +{ + "settings_version": "0.0.6", + "datasets_dir": "C:\\Users\\datasets", + "weights_dir": "C:\\Users\\hp\\weights", + "runs_dir": "C:\\Users\\hp\\runs", + "uuid": "b8318bcfc07be7433d16fc866923188fe796248b2a6e702ba099b726960ef46b", + "sync": true, + "api_key": "", + "openai_api_key": "", + "clearml": true, + "comet": true, + "dvc": true, + "hub": true, + "mlflow": true, + "neptune": true, + "raytune": true, + "tensorboard": false, + "wandb": false, + "vscode_msg": true, + "openvino_msg": true +} \ No newline at end of file diff --git a/backend/__pycache__/app.cpython-311.pyc b/backend/__pycache__/app.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d1f431e90d9f1af4af9fbbcc012f6220863bec2d Binary files /dev/null and b/backend/__pycache__/app.cpython-311.pyc differ diff --git a/backend/__pycache__/app.cpython-312.pyc b/backend/__pycache__/app.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3cbc420c6755fbdca389b8d06156170df3d885d3 Binary files /dev/null and b/backend/__pycache__/app.cpython-312.pyc differ diff --git a/backend/__pycache__/augmentations.cpython-311.pyc b/backend/__pycache__/augmentations.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..03356d05c5b8bf5454684c8599f0d047bd94dd33 Binary files /dev/null and b/backend/__pycache__/augmentations.cpython-311.pyc differ diff --git a/backend/__pycache__/augmentations.cpython-312.pyc b/backend/__pycache__/augmentations.cpython-312.pyc index 690d4d039d5d46a0e7c49a644450993a15ef027e..35793d0e1b6f960d9c54bfb4541a6c833fe3f68d 100644 Binary files a/backend/__pycache__/augmentations.cpython-312.pyc and b/backend/__pycache__/augmentations.cpython-312.pyc differ diff --git a/backend/__pycache__/model.cpython-311.pyc b/backend/__pycache__/model.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..321f5d1e4b4e53b2999b5dbb18edec8468d7b941 Binary files /dev/null and b/backend/__pycache__/model.cpython-311.pyc differ diff --git a/backend/__pycache__/model.cpython-312.pyc b/backend/__pycache__/model.cpython-312.pyc index 2aec20e5902e31ec5e20606dd821c1329d5db331..2ce5daa5375006cf8505a92599134bca72ae9214 100644 Binary files a/backend/__pycache__/model.cpython-312.pyc and b/backend/__pycache__/model.cpython-312.pyc differ diff --git a/backend/__pycache__/model_histo.cpython-311.pyc b/backend/__pycache__/model_histo.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..efe678070ab847cee91ccdbc16606202691ddc21 Binary files /dev/null and b/backend/__pycache__/model_histo.cpython-311.pyc differ diff --git a/backend/__pycache__/model_histo.cpython-312.pyc b/backend/__pycache__/model_histo.cpython-312.pyc index 50b65b0c25c18cb329c45f8d43def0a6856bda09..c757d11304c1dfb273cae5e2a0b781e456638e04 100644 Binary files a/backend/__pycache__/model_histo.cpython-312.pyc and b/backend/__pycache__/model_histo.cpython-312.pyc differ diff --git a/backend/app.py b/backend/app.py index 852ac5b8c6fbb2b066bff24d38722be9f554a424..a9cdf55636488858fde048ba9002d7cdec4ca955 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,7 +1,6 @@ import os import shutil - for d in ["/tmp/huggingface", "/tmp/Ultralytics", "/tmp/matplotlib", "/tmp/torch", "/root/.cache"]: shutil.rmtree(d, ignore_errors=True) @@ -11,75 +10,284 @@ os.environ["TORCH_HOME"] = "/tmp/torch" os.environ["MPLCONFIGDIR"] = "/tmp/matplotlib" os.environ["YOLO_CONFIG_DIR"] = "/tmp/Ultralytics" - -from huggingface_hub import login - - - -hf_token = os.getenv("HF_TOKEN") -if hf_token: - login(token=hf_token) - +import json +import uuid +import datetime +import numpy as np +import torch +import cv2 +import joblib +import torch.nn as nn +import torchvision.transforms as transforms +import torchvision.models as models +from io import BytesIO +from PIL import Image as PILImage from fastapi import FastAPI, File, UploadFile, Form from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse, FileResponse +import tensorflow as tf +from model_histo import BreastCancerClassifier from fastapi.staticfiles import StaticFiles -from ultralytics import YOLO -from io import BytesIO -from PIL import Image import uvicorn -import json, os, uuid, numpy as np, torch, cv2, joblib, io, tensorflow as tf -import torch.nn as nn -import torchvision.transforms as transforms -import torchvision.models as models +try: + from reportlab.lib.pagesizes import letter + from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image as ReportLabImage + from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle + from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY + from reportlab.lib.units import inch + from reportlab.lib.colors import navy, black + REPORTLAB_AVAILABLE = True +except ImportError: + REPORTLAB_AVAILABLE = False +from ultralytics import YOLO from sklearn.preprocessing import MinMaxScaler from model import MWT as create_model from augmentations import Augmentations -from model_histo import BreastCancerClassifier # TensorFlow model +from huggingface_hub import InferenceClient + +# ===================================================== + +# SETUP TEMP DIRS AND ENV + +# ===================================================== + +for d in ["/tmp/huggingface", "/tmp/Ultralytics", "/tmp/matplotlib", "/tmp/torch"]: + shutil.rmtree(d, ignore_errors=True) + +os.environ["HF_HOME"] = "/tmp/huggingface" +os.environ["HUGGINGFACE_HUB_CACHE"] = "/tmp/huggingface" +os.environ["TORCH_HOME"] = "/tmp/torch" +os.environ["MPLCONFIGDIR"] = "/tmp/matplotlib" +os.environ["YOLO_CONFIG_DIR"] = "/tmp/Ultralytics" + +# ===================================================== + +# HUGGING FACE CLIENT SETUP + +# ===================================================== + +HF_MODEL_ID = "mistralai/Mistral-7B-v0.1" +hf_token = os.getenv("HF_TOKEN") +client = None + +if hf_token: + try: + client = InferenceClient(model=HF_MODEL_ID, token=hf_token) + print(f"✅ Hugging Face InferenceClient initialized for {HF_MODEL_ID}") + except Exception as e: + print("âš ī¸ Failed to initialize Hugging Face client:", e) +else: + print("âš ī¸ Warning: No HF_TOKEN found — summaries will be skipped.") + +def generate_ai_summary(abnormal_cells, normal_cells, avg_confidence): + """Generate a brief medical interpretation using Mistral.""" + if not client: + return "âš ī¸ Hugging Face client not initialized — skipping summary." + + try: + prompt = f"""Act as a cytopathology expert providing a brief diagnostic interpretation. + +Observed Cell Counts: +- {abnormal_cells} Abnormal Cells +- {normal_cells} Normal Cells +- Detection Confidence: {avg_confidence:.1f}% + +Write a 2-3 sentence professional medical assessment focusing on: +1. Cell count analysis +2. Abnormality ratio ({abnormal_cells/(abnormal_cells + normal_cells)*100:.1f}%) +3. Clinical significance + +Use objective, scientific language suitable for a pathology report.""" + + # Use streaming to avoid StopIteration + response = client.text_generation( + prompt, + max_new_tokens=200, + temperature=0.7, + stream=False, + details=True, + stop_sequences=["\n\n", "###"] + ) + + # Handle different response formats + if hasattr(response, 'generated_text'): + return response.generated_text.strip() + elif isinstance(response, dict): + return response.get('generated_text', '').strip() + elif isinstance(response, str): + return response.strip() + + # Fallback summary if response format is unexpected + ratio = abnormal_cells / (abnormal_cells + normal_cells) * 100 if (abnormal_cells + normal_cells) > 0 else 0 + return f"Analysis shows {abnormal_cells} abnormal cells ({ratio:.1f}%) and {normal_cells} normal cells, with average detection confidence of {avg_confidence:.1f}%." + + except Exception as e: + # Provide a structured fallback summary instead of error message + total = abnormal_cells + normal_cells + if total == 0: + return "No cells were detected in the sample. Consider re-scanning or adjusting detection parameters." + + ratio = (abnormal_cells / total) * 100 + severity = "high" if ratio > 70 else "moderate" if ratio > 30 else "low" + + 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}%." + + +def generate_mwt_summary(predicted_label, confidences, avg_confidence): + """Generate a short MWT-specific interpretation using the HF client when available.""" + if not client: + return "âš ī¸ Hugging Face client not initialized — skipping AI interpretation." + try: + prompt = f""" +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. + +Result: +- Predicted label: {predicted_label} +- Confidence (average): {avg_confidence:.1f}% +- Class probabilities: {json.dumps(confidences)} + +Provide guidance on the significance of the result and any suggested next steps in plain, objective language. +""" + + response = client.text_generation( + prompt, + max_new_tokens=120, + temperature=0.2, + stream=False, + details=True, + stop_sequences=["\n\n", "###"] + ) + + if hasattr(response, 'generated_text'): + return response.generated_text.strip() + elif isinstance(response, dict): + return response.get('generated_text', '').strip() + elif isinstance(response, str): + return response.strip() + + return f"Result: {predicted_label} (avg confidence {avg_confidence:.1f}%)." + except Exception as e: + return f"Quantitative result: {predicted_label} with average confidence {avg_confidence:.1f}%." + + +def generate_cin_summary(predicted_grade, confidences, avg_confidence): + """Generate a short CIN-specific interpretation using the HF client when available.""" + if not client: + return "âš ī¸ Hugging Face client not initialized — skipping AI interpretation." + + try: + prompt = f""" +You are a concise gynecologic pathology expert. Given a CIN classifier result, write a 1-2 sentence professional interpretation suitable for a diagnostic report. + +Result: +- Predicted grade: {predicted_grade} +- Confidence (average): {avg_confidence:.1f}% +- Class probabilities: {json.dumps(confidences)} + +Provide a brief statement about clinical significance and suggested next steps (e.g., further colposcopic evaluation) in objective, clinical language. +""" + + response = client.text_generation( + prompt, + max_new_tokens=140, + temperature=0.2, + stream=False, + details=True, + stop_sequences=["\n\n", "###"] + ) + + if hasattr(response, 'generated_text'): + return response.generated_text.strip() + elif isinstance(response, dict): + return response.get('generated_text', '').strip() + elif isinstance(response, str): + return response.strip() + + return f"Result: {predicted_grade} (avg confidence {avg_confidence:.1f}%)." + except Exception: + return f"Quantitative result: {predicted_grade} with average confidence {avg_confidence:.1f}%." # ===================================================== -# App setup + +# FASTAPI SETUP + # ===================================================== -app = FastAPI(title="Unified Cervical & Breast Cancer Analysis API") + +app = FastAPI(title="Pathora Medical Diagnostic API") app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=["*", "http://localhost:5173", "http://127.0.0.1:5173"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], + expose_headers=["*"] # Allow access to response headers ) -OUTPUT_DIR = "/tmp/outputs" +# Use /tmp for outputs in Hugging Face Spaces (writable directory) +OUTPUT_DIR = os.environ.get("OUTPUT_DIR", "/tmp/outputs") os.makedirs(OUTPUT_DIR, exist_ok=True) + +# Create image outputs dir +IMAGES_DIR = os.path.join(OUTPUT_DIR, "images") +os.makedirs(IMAGES_DIR, exist_ok=True) + app.mount("/outputs", StaticFiles(directory=OUTPUT_DIR), name="outputs") +# Mount public sample images from frontend dist (Vite copies public/ to dist/ root) +# Check both possible locations: frontend/dist (Docker) and ../frontend/dist (local dev) +FRONTEND_DIST_CHECK = os.path.join(os.path.dirname(__file__), "frontend/dist") +if not os.path.isdir(FRONTEND_DIST_CHECK): + FRONTEND_DIST_CHECK = os.path.abspath(os.path.join(os.path.dirname(__file__), "../frontend/dist")) + +for sample_dir in ["cyto", "colpo", "histo"]: + sample_path = os.path.join(FRONTEND_DIST_CHECK, sample_dir) + if os.path.isdir(sample_path): + app.mount(f"/{sample_dir}", StaticFiles(directory=sample_path), name=sample_dir) + print(f"✅ Mounted /{sample_dir} from {sample_path}") + else: + print(f"âš ī¸ Sample directory not found: {sample_path}") + +# Mount other static assets (logos, banners) from dist root +for static_file in ["banner.jpeg", "white_logo.png", "black_logo.png", "manalife_LOGO.jpg"]: + static_path = os.path.join(FRONTEND_DIST_CHECK, static_file) + if os.path.isfile(static_path): + print(f"✅ Static file available: /{static_file}") + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # ===================================================== -# Model 1: YOLO (Colposcopy Detection) + +# MODEL LOADS + # ===================================================== + print("🔹 Loading YOLO model...") yolo_model = YOLO("best2.pt") -# ===================================================== -# Model 2: MWT Classifier -# ===================================================== print("🔹 Loading MWT model...") mwt_model = create_model(num_classes=2).to(device) mwt_model.load_state_dict(torch.load("MWTclass2.pth", map_location=device)) mwt_model.eval() -mwt_class_names = ['Negative', 'Positive'] +mwt_class_names = ["Negative", "Positive"] -# ===================================================== -# Model 3: CIN Classifier -# ===================================================== print("🔹 Loading CIN model...") -clf = joblib.load("logistic_regression_model.pkl") +try: + clf = joblib.load("logistic_regression_model.pkl") +except Exception as e: + print(f"âš ī¸ CIN classifier not available (logistic_regression_model.pkl missing or invalid): {e}") + clf = None + yolo_colposcopy = YOLO("yolo_colposcopy.pt") +# ===================================================== + +# RESNET FEATURE EXTRACTORS FOR CIN + +# ===================================================== + def build_resnet(model_name="resnet50"): if model_name == "resnet50": model = models.resnet50(weights=models.ResNet50_Weights.DEFAULT) @@ -92,7 +300,6 @@ def build_resnet(model_name="resnet50"): nn.Sequential(model.conv1, model.bn1, model.relu, model.maxpool), model.layer1, model.layer2, model.layer3, model.layer4, ) - gap = nn.AdaptiveAvgPool2d((1, 1)) gmp = nn.AdaptiveMaxPool2d((1, 1)) resnet50_blocks = build_resnet("resnet50") @@ -100,29 +307,14 @@ resnet101_blocks = build_resnet("resnet101") resnet152_blocks = build_resnet("resnet152") transform = transforms.Compose([ - transforms.ToPILImage(), - transforms.Resize((224, 224)), - transforms.ToTensor(), - transforms.Normalize(mean=[0.485, 0.456, 0.406], - std=[0.229, 0.224, 0.225]), +transforms.ToPILImage(), +transforms.Resize((224, 224)), +transforms.ToTensor(), +transforms.Normalize(mean=[0.485, 0.456, 0.406], +std=[0.229, 0.224, 0.225]), ]) -# ===================================================== -# Model 4: Histopathology Classifier (TensorFlow) -# ===================================================== -print("🔹 Loading Breast Cancer Histopathology model...") -classifier = BreastCancerClassifier(fine_tune=False) -if not classifier.authenticate_huggingface(): - raise RuntimeError("HuggingFace authentication failed.") -if not classifier.load_path_foundation(): - raise RuntimeError("Failed to load Path Foundation model.") -model_path = "histopathology_trained_model.keras" -classifier.model = tf.keras.models.load_model(model_path) -print(f"✅ Loaded model from {model_path}") -# ===================================================== -# Helper functions -# ===================================================== def preprocess_for_mwt(image_np): img = cv2.resize(image_np, (224, 224)) img = Augmentations.Normalization((0, 1))(img) @@ -145,48 +337,147 @@ def extract_cbf_features(blocks, img_t): p3 = gap(f3).view(-1) p4 = gap(f4).view(-1) p5 = gap(f5).view(-1) - cbf_feature = torch.cat([p1, p2, p3, p4, p5], dim=0) - return cbf_feature.cpu().numpy() - -def predict_histopathology(image: Image.Image): - if image.mode != "RGB": - image = image.convert("RGB") - image = image.resize((224, 224)) - img_array = np.expand_dims(np.array(image).astype("float32") / 255.0, axis=0) - embeddings = classifier.extract_embeddings(img_array) - prediction_proba = classifier.model.predict(embeddings, verbose=0)[0] - predicted_class = int(np.argmax(prediction_proba)) - class_names = ["Benign", "Malignant"] - return { - "model_used": "Breast Cancer Histopathology Classifier", - "prediction": class_names[predicted_class], - "confidence": float(np.max(prediction_proba)), - "probabilities": { - "Benign": float(prediction_proba[0]), - "Malignant": float(prediction_proba[1]) + return torch.cat([p1, p2, p3, p4, p5], dim=0).cpu().numpy() + +# ===================================================== +# Model 4: Histopathology Classifier (TensorFlow) +# ===================================================== +print("🔹 Attempting to load Breast Cancer Histopathology model...") + +try: + classifier = BreastCancerClassifier(fine_tune=False) + + # Safely handle Hugging Face token auth + hf_token = os.getenv("HF_TOKEN") + if hf_token: + if classifier.authenticate_huggingface(): + print("✅ Hugging Face authentication successful.") + else: + print("âš ī¸ Warning: Hugging Face authentication failed, using local model only.") + else: + print("âš ī¸ HF_TOKEN not found in environment — skipping authentication.") + + # Load Path Foundation model + if classifier.load_path_foundation(): + print("✅ Loaded Path Foundation base model.") + else: + print("âš ī¸ Could not load Path Foundation base model, continuing with local weights only.") + + # Load trained histopathology model + model_path = "histopathology_trained_model.keras" + if os.path.exists(model_path): + classifier.model = tf.keras.models.load_model(model_path) + print(f"✅ Loaded local histopathology model: {model_path}") + else: + print(f"âš ī¸ Model file not found: {model_path}") + +except Exception as e: + classifier = None + print(f"❌ Error initializing histopathology model: {e}") + +def predict_histopathology(image): + if classifier is None: + return {"error": "Histopathology model not available."} + + try: + if image.mode != "RGB": + image = image.convert("RGB") + image = image.resize((224, 224)) + img_array = np.expand_dims(np.array(image).astype("float32") / 255.0, axis=0) + embeddings = classifier.extract_embeddings(img_array) + prediction_proba = classifier.model.predict(embeddings, verbose=0)[0] + predicted_class = int(np.argmax(prediction_proba)) + class_names = ["Benign", "Malignant"] + + # Return confidence as dictionary with both class probabilities (like MWT/CIN) + confidences = {class_names[i]: float(prediction_proba[i]) for i in range(len(class_names))} + avg_confidence = float(np.max(prediction_proba)) * 100 + + return { + "model_used": "Histopathology Classifier", + "prediction": class_names[predicted_class], + "confidence": confidences, + "summary": { + "avg_confidence": round(avg_confidence, 2), + "ai_interpretation": f"Histopathological analysis indicates {class_names[predicted_class].lower()} tissue with {avg_confidence:.1f}% confidence.", + }, } - } + except Exception as e: + return {"error": f"Histopathology prediction failed: {e}"} + # ===================================================== -# Main endpoint + +# MAIN ENDPOINT + # ===================================================== + + @app.post("/predict/") async def predict(model_name: str = Form(...), file: UploadFile = File(...)): + print(f"Received prediction request - model: {model_name}, file: {file.filename}") + + # Validate model name + if model_name not in ["yolo", "mwt", "cin", "histopathology"]: + return JSONResponse( + content={ + "error": f"Invalid model_name: {model_name}. Must be one of: yolo, mwt, cin, histopathology" + }, + status_code=400 + ) + + # Validate and read file + if not file.filename: + return JSONResponse( + content={"error": "No file provided"}, + status_code=400 + ) + contents = await file.read() - image = Image.open(BytesIO(contents)).convert("RGB") - image_np = np.array(image) + if len(contents) == 0: + return JSONResponse( + content={"error": "Empty file provided"}, + status_code=400 + ) + + # Attempt to open and validate image + try: + image = PILImage.open(BytesIO(contents)).convert("RGB") + image_np = np.array(image) + if image_np.size == 0: + raise ValueError("Empty image array") + print(f"Successfully loaded image, shape: {image_np.shape}") + except Exception as e: + return JSONResponse( + content={"error": f"Invalid image file: {str(e)}"}, + status_code=400 + ) if model_name == "yolo": results = yolo_model(image) detections_json = results[0].to_json() detections = json.loads(detections_json) + + abnormal_cells = sum(1 for d in detections if d["name"] == "abnormal") + normal_cells = sum(1 for d in detections if d["name"] == "normal") + avg_confidence = np.mean([d.get("confidence", 0) for d in detections]) * 100 if detections else 0 + + ai_summary = generate_ai_summary(abnormal_cells, normal_cells, avg_confidence) + output_filename = f"detected_{uuid.uuid4().hex[:8]}.jpg" - output_path = os.path.join(OUTPUT_DIR, output_filename) + output_path = os.path.join(IMAGES_DIR, output_filename) results[0].save(filename=output_path) + return { "model_used": "YOLO Detection", "detections": detections, - "annotated_image_url": f"/outputs/{output_filename}" + "annotated_image_url": f"/outputs/images/{output_filename}", + "summary": { + "abnormal_cells": abnormal_cells, + "normal_cells": normal_cells, + "avg_confidence": round(float(avg_confidence), 2), + "ai_interpretation": ai_summary, + }, } elif model_name == "mwt": @@ -195,15 +486,35 @@ async def predict(model_name: str = Form(...), file: UploadFile = File(...)): output = mwt_model(tensor.to(device)).cpu() probs = torch.softmax(output, dim=1)[0] confidences = {mwt_class_names[i]: float(probs[i]) for i in range(2)} - predicted_label = mwt_class_names[torch.argmax(probs)] - return {"model_used": "MWT Classifier", "prediction": predicted_label, "confidence": confidences} + predicted_label = mwt_class_names[int(torch.argmax(probs).item())] + # Average / primary confidence for display + avg_confidence = float(torch.max(probs).item()) * 100 + + # Generate a brief AI interpretation using the Mistral client (if available) + ai_interp = generate_mwt_summary(predicted_label, confidences, avg_confidence) + + return { + "model_used": "MWT Classifier", + "prediction": predicted_label, + "confidence": confidences, + "summary": { + "avg_confidence": round(avg_confidence, 2), + "ai_interpretation": ai_interp, + }, + } elif model_name == "cin": + if clf is None: + return JSONResponse( + content={"error": "CIN classifier not available on server."}, + status_code=503, + ) nparr = np.frombuffer(contents, np.uint8) img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) results = yolo_colposcopy.predict(source=img, conf=0.7, save=False, verbose=False) if len(results[0].boxes) == 0: return {"error": "No cervix detected"} + x1, y1, x2, y2 = map(int, results[0].boxes.xyxy[0].cpu().numpy()) crop = img[y1:y2, x1:x2] crop = cv2.resize(crop, (224, 224)) @@ -215,45 +526,542 @@ async def predict(model_name: str = Form(...), file: UploadFile = File(...)): X_scaled = MinMaxScaler().fit_transform(features) pred = clf.predict(X_scaled)[0] proba = clf.predict_proba(X_scaled)[0] - classes = ["CIN1", "CIN2", "CIN3"] + # Get actual number of classes from model output + classes = ["Low-grade", "High-grade"] # Binary CIN classification predicted_label = classes[pred] confidences = {classes[i]: float(proba[i]) for i in range(len(classes))} + + # Map to more detailed classification based on confidence + if predicted_label == "High-grade" and confidences["High-grade"] > 0.8: + detailed_class = "CIN3" + elif predicted_label == "High-grade": + detailed_class = "CIN2" + else: + detailed_class = "CIN1" + + # Average / primary confidence for display + avg_confidence = float(np.max(proba)) * 100 + + # Generate a brief AI interpretation using the Mistral client (if available) + ai_interp = generate_cin_summary(predicted_label, confidences, avg_confidence) + return { "model_used": "CIN Classifier", - "prediction": predicted_label, - "confidence": confidences + "prediction": detailed_class, + "grade": predicted_label, + "confidence": confidences, + "summary": { + "avg_confidence": round(avg_confidence, 2), + "ai_interpretation": ai_interp, + }, } - elif model_name == "histopathology": - result = predict_histopathology(image) - return result + result = predict_histopathology(image) + return result + else: return JSONResponse(content={"error": "Invalid model name"}, status_code=400) +# ===================================================== + +# ROUTES + +# ===================================================== + +def create_designed_pdf(pdf_path, report_data, analysis_summary_json): + doc = SimpleDocTemplate(pdf_path, pagesize=letter, + rightMargin=72, leftMargin=72, + topMargin=72, bottomMargin=18) + styles = getSampleStyleSheet() + story = [] + + styles.add(ParagraphStyle(name='Title', fontSize=20, fontName='Helvetica-Bold', alignment=TA_CENTER, textColor=navy)) + styles.add(ParagraphStyle(name='Section', fontSize=14, fontName='Helvetica-Bold', spaceBefore=10, spaceAfter=6)) + styles.add(ParagraphStyle(name='NormalSmall', fontSize=10, leading=12)) + styles.add(ParagraphStyle(name='Heading', fontSize=16, fontName='Helvetica-Bold', textColor=navy, spaceBefore=6, spaceAfter=4)) + + patient = report_data['patient'] + analysis = report_data.get('analysis', {}) + + # Safely parse analysis_summary_json + try: + ai_summary = json.loads(analysis_summary_json) if analysis_summary_json else {} + except (json.JSONDecodeError, TypeError): + ai_summary = {} + + # Determine report type based on model used + model_used = ai_summary.get('model_used', '') + if 'YOLO' in model_used or 'yolo' in str(analysis.get('id', '')).lower(): + report_type = "CYTOLOGY" + report_title = "Cytology Report" + elif 'CIN' in model_used or 'cin' in str(analysis.get('id', '')).lower() or 'colpo' in str(analysis.get('id', '')).lower(): + report_type = "COLPOSCOPY" + report_title = "Colposcopy Report" + elif 'histo' in str(analysis.get('id', '')).lower() or 'histopathology' in model_used.lower(): + report_type = "HISTOPATHOLOGY" + report_title = "Histopathology Report" + else: + report_type = "CYTOLOGY" + report_title = "Medical Analysis Report" + + # Header + story.append(Paragraph("MANALIFE AI", styles['Title'])) + story.append(Paragraph("Advanced Medical Analysis", styles['NormalSmall'])) + story.append(Spacer(1, 0.3*inch)) + story.append(Paragraph(f"MEDICAL ANALYSIS REPORT OF {report_type}", styles['Heading'])) + story.append(Paragraph(report_title, styles['Section'])) + story.append(Spacer(1, 0.2*inch)) + + # Report ID and Date + story.append(Paragraph(f"Report ID: {report_data.get('report_id', 'N/A')}", styles['NormalSmall'])) + story.append(Paragraph(f"Generated: {datetime.datetime.now().strftime('%b %d, %Y, %I:%M %p')}", styles['NormalSmall'])) + story.append(Spacer(1, 0.2*inch)) + + # Patient Information Section + story.append(Paragraph("Patient Information", styles['Section'])) + story.append(Paragraph(f"Patient ID: {patient.get('id', 'N/A')}", styles['NormalSmall'])) + story.append(Paragraph(f"Exam Date: {patient.get('exam_date', 'N/A')}", styles['NormalSmall'])) + story.append(Paragraph(f"Physician: {patient.get('physician', 'N/A')}", styles['NormalSmall'])) + story.append(Paragraph(f"Facility: {patient.get('facility', 'N/A')}", styles['NormalSmall'])) + story.append(Spacer(1, 0.2*inch)) + + # Sample Information Section + story.append(Paragraph("Sample Information", styles['Section'])) + story.append(Paragraph(f"Specimen Type: {patient.get('specimen_type', 'Cervical Cytology')}", styles['NormalSmall'])) + story.append(Paragraph(f"Clinical History: {patient.get('clinical_history', 'N/A')}", styles['NormalSmall'])) + story.append(Spacer(1, 0.2*inch)) + + # AI Analysis Section + story.append(Paragraph("AI-ASSISTED ANALYSIS", styles['Section'])) + story.append(Paragraph("System: Manalife AI System — Automated Analysis", styles['NormalSmall'])) + story.append(Paragraph(f"Confidence Score: {ai_summary.get('avg_confidence', 'N/A')}%", styles['NormalSmall'])) + + # Add metrics based on report type + if report_type == "HISTOPATHOLOGY": + # For histopathology, show Benign/Malignant confidence + confidence_dict = ai_summary.get('confidence', {}) + if isinstance(confidence_dict, dict): + benign_conf = confidence_dict.get('Benign', 0) * 100 + malignant_conf = confidence_dict.get('Malignant', 0) * 100 + story.append(Paragraph(f"Benign Confidence: {benign_conf:.2f}%", styles['NormalSmall'])) + story.append(Paragraph(f"Malignant Confidence: {malignant_conf:.2f}%", styles['NormalSmall'])) + elif report_type == "CYTOLOGY": + # For cytology (YOLO), show abnormal/normal cells + if 'abnormal_cells' in ai_summary: + story.append(Paragraph(f"Abnormal Cells: {ai_summary.get('abnormal_cells', 'N/A')}", styles['NormalSmall'])) + if 'normal_cells' in ai_summary: + story.append(Paragraph(f"Normal Cells: {ai_summary.get('normal_cells', 'N/A')}", styles['NormalSmall'])) + else: + # For CIN/Colposcopy, show class confidences + confidence_dict = ai_summary.get('confidence', {}) + if isinstance(confidence_dict, dict): + for cls, val in confidence_dict.items(): + conf_pct = val * 100 if isinstance(val, (int, float)) else 0 + story.append(Paragraph(f"{cls} Confidence: {conf_pct:.2f}%", styles['NormalSmall'])) + + story.append(Spacer(1, 0.1*inch)) + story.append(Paragraph("AI Interpretation:", styles['NormalSmall'])) + story.append(Paragraph(ai_summary.get('ai_interpretation', 'Not available.'), styles['NormalSmall'])) + story.append(Spacer(1, 0.2*inch)) + + # Doctor's Notes + story.append(Paragraph("Doctor's Notes", styles['Section'])) + story.append(Paragraph(report_data.get('doctor_notes') or 'No additional notes provided.', styles['NormalSmall'])) + story.append(Spacer(1, 0.2*inch)) + + # Recommendations + story.append(Paragraph("RECOMMENDATIONS", styles['Section'])) + story.append(Paragraph("Continue routine screening as per standard guidelines. Follow up as directed by your physician.", styles['NormalSmall'])) + story.append(Spacer(1, 0.3*inch)) + + # Signatures + story.append(Paragraph("Signatures", styles['Section'])) + story.append(Paragraph("Dr. Emily Roberts, MD (Cytopathologist)", styles['NormalSmall'])) + story.append(Paragraph("Dr. James Wilson, MD (Pathologist)", styles['NormalSmall'])) + story.append(Spacer(1, 0.1*inch)) + story.append(Paragraph(f"Generated on: {datetime.datetime.now().strftime('%b %d, %Y, %I:%M %p')}", styles['NormalSmall'])) + + doc.build(story) + + + +@app.post("/reports/") +async def generate_report( + patient_id: str = Form(...), + exam_date: str = Form(...), + metadata: str = Form(...), + notes: str = Form(None), + analysis_id: str = Form(None), + analysis_summary: str = Form(None), +): + """Generate a structured medical report from analysis results and metadata.""" + try: + # Create reports directory if it doesn't exist + reports_dir = os.path.join(OUTPUT_DIR, "reports") + os.makedirs(reports_dir, exist_ok=True) + + # Generate unique report ID + report_id = f"{patient_id}_{uuid.uuid4().hex[:8]}" + report_dir = os.path.join(reports_dir, report_id) + os.makedirs(report_dir, exist_ok=True) + + # Parse metadata + metadata_dict = json.loads(metadata) + + # Get analysis results - assuming stored in memory or retrievable + # TODO: Implement analysis results storage/retrieval + + # Construct report data + report_data = { + "report_id": report_id, + "generated_at": datetime.datetime.now().isoformat(), + "patient": { + "id": patient_id, + "exam_date": exam_date, + **metadata_dict + }, + "analysis": { + "id": analysis_id, + # If the analysis_id is actually an annotated image URL, store it for report embedding + "annotated_image_url": analysis_id, + # TODO: Add actual analysis results + }, + "doctor_notes": notes + } + + # Save report data + report_json = os.path.join(report_dir, "report.json") + with open(report_json, "w", encoding="utf-8") as f: + json.dump(report_data, f, indent=2, ensure_ascii=False) + + # Attempt to create a PDF version if reportlab is available + pdf_url = None + if REPORTLAB_AVAILABLE: + try: + pdf_path = os.path.join(report_dir, "report.pdf") + create_designed_pdf(pdf_path, report_data, analysis_summary) + pdf_url = f"/outputs/reports/{report_id}/report.pdf" + except Exception as e: + print(f"Error creating designed PDF: {e}") + pdf_url = None + + # Parse analysis_summary to get AI results + try: + ai_summary = json.loads(analysis_summary) if analysis_summary else {} + except (json.JSONDecodeError, TypeError): + ai_summary = {} + + # Determine report type based on analysis summary or model used + model_used = ai_summary.get('model_used', '') + if 'YOLO' in model_used or 'yolo' in str(analysis_id).lower(): + report_type = "Cytology" + report_title = "Cytology Report" + elif 'CIN' in model_used or 'cin' in str(analysis_id).lower() or 'colpo' in str(analysis_id).lower(): + report_type = "Colposcopy" + report_title = "Colposcopy Report" + elif 'histo' in str(analysis_id).lower() or 'histopathology' in model_used.lower(): + report_type = "Histopathology" + report_title = "Histopathology Report" + else: + # Default fallback + report_type = "Cytology" + report_title = "Medical Analysis Report" + + # Build analysis metrics HTML based on report type + if report_type == "Histopathology": + # For histopathology, show Benign/Malignant confidence from the confidence dict + confidence_dict = ai_summary.get('confidence', {}) + benign_conf = confidence_dict.get('Benign', 0) * 100 if isinstance(confidence_dict, dict) else 0 + malignant_conf = confidence_dict.get('Malignant', 0) * 100 if isinstance(confidence_dict, dict) else 0 + + analysis_metrics_html = f""" + SystemManalife AI System — Automated Analysis + Confidence Score{ai_summary.get('avg_confidence', 'N/A')}% + Benign Confidence{benign_conf:.2f}% + Malignant Confidence{malignant_conf:.2f}% + """ + elif report_type == "Cytology": + # For cytology (YOLO), show abnormal/normal cells + analysis_metrics_html = f""" + SystemManalife AI System — Automated Analysis + Confidence Score{ai_summary.get('avg_confidence', 'N/A')}% + Abnormal Cells{ai_summary.get('abnormal_cells', 'N/A')} + Normal Cells{ai_summary.get('normal_cells', 'N/A')} + """ + else: + # For CIN/Colposcopy or other models, show generic confidence + confidence_dict = ai_summary.get('confidence', {}) + confidence_rows = "" + if isinstance(confidence_dict, dict): + for cls, val in confidence_dict.items(): + conf_pct = val * 100 if isinstance(val, (int, float)) else 0 + confidence_rows += f"{cls} Confidence{conf_pct:.2f}%\n " + + analysis_metrics_html = f""" + SystemManalife AI System — Automated Analysis + Confidence Score{ai_summary.get('avg_confidence', 'N/A')}% + {confidence_rows} + """ + + # Build final HTML including download links and embedded annotated image + report_html = os.path.join(report_dir, "report.html") + json_url = f"/outputs/reports/{report_id}/report.json" + html_url = f"/outputs/reports/{report_id}/report.html" + annotated_img = report_data.get("analysis", {}).get("annotated_image_url") or "" + + # Get base URL for the annotated image (if it's a relative path) + annotated_img_full = f"http://localhost:8000{annotated_img}" if annotated_img and annotated_img.startswith('/') else annotated_img + + download_pdf_btn = f'' if pdf_url else '' + + # Format generated time + generated_time = datetime.datetime.now().strftime('%b %d, %Y, %I:%M %p') + + html_content = f""" + + + + + Medical Analysis Report — Manalife AI + + + +
+
+
+ + Manalife Logo +
+
+

MANALIFE AI — Medical Analysis

+
Advanced cytological colposcopy and histopathology reporting
+
contact@manalife.ai â€ĸ +1 (555) 123-4567
+
+
+ +
+
+
+
MEDICAL ANALYSIS REPORT OF {report_type.upper()}
+

{report_title}

+
+
+
Report ID: {report_id}
+
Generated: {generated_time}
+
+
+ +
+ +
+
+
Patient Information
+ + + + + +
Patient ID{patient_id}
Exam Date{exam_date}
Physician{metadata_dict.get('physician', 'N/A')}
Facility{metadata_dict.get('facility', 'N/A')}
+
+ +
+
Sample Information
+ + + + + +
Specimen Type{metadata_dict.get('specimen_type', 'N/A')}
Clinical History{metadata_dict.get('clinical_history', 'N/A')}
Collected{exam_date}
Reported{generated_time}
+
+ +
+
AI-Assisted Analysis
+ + {analysis_metrics_html} +
+
+
AI Interpretation:
+
{ai_summary.get('ai_interpretation', 'No AI interpretation available.')}
+
+
+ + {'
Annotated Analysis Image
Annotated Analysis Result
' if annotated_img else ''} + +
+
Doctor\'s Notes
+

{notes or 'No additional notes provided.'}

+
+ +
+
Recommendations
+

Continue routine screening as per standard guidelines. Follow up as directed by your physician.

+
+ +
+
Signatures
+
+
+
Dr. Emily Roberts
+
MD, pathologist
+
+
+
Dr. James Wilson
+
MD, pathologist
+
+
+
+
+ + +
+ +
+ {download_pdf_btn} + +
+
+ +""" + + with open(report_html, "w", encoding="utf-8") as f: + f.write(html_content) + + return { + "report_id": report_id, + "json_url": json_url, + "html_url": html_url, + "pdf_url": pdf_url, + } + + except Exception as e: + return JSONResponse( + content={"error": f"Failed to generate report: {str(e)}"}, + status_code=500 + ) + +@app.get("/reports/{report_id}") +async def get_report(report_id: str): + """Fetch a generated report by ID.""" + report_dir = os.path.join(OUTPUT_DIR, "reports", report_id) + report_json = os.path.join(report_dir, "report.json") + + if not os.path.exists(report_json): + return JSONResponse( + content={"error": "Report not found"}, + status_code=404 + ) + + with open(report_json, "r") as f: + report_data = json.load(f) + + return report_data + +@app.get("/reports") +async def list_reports(patient_id: str = None): + """List all generated reports, optionally filtered by patient ID.""" + reports_dir = os.path.join(OUTPUT_DIR, "reports") + if not os.path.exists(reports_dir): + return {"reports": []} + + reports = [] + for report_id in os.listdir(reports_dir): + report_json = os.path.join(reports_dir, report_id, "report.json") + if os.path.exists(report_json): + with open(report_json, "r") as f: + report_data = json.load(f) + if not patient_id or report_data["patient"]["id"] == patient_id: + reports.append({ + "report_id": report_id, + "patient_id": report_data["patient"]["id"], + "exam_date": report_data["patient"]["exam_date"], + "generated_at": report_data["generated_at"] + }) + + return {"reports": sorted(reports, key=lambda r: r["generated_at"], reverse=True)} @app.get("/models") def get_models(): return {"available_models": ["yolo", "mwt", "cin", "histopathology"]} - @app.get("/health") def health(): - return {"message": "Unified Cervical & Breast Cancer API is running!"} + return {"message": "Pathora Medical Diagnostic API is running!"} + +# FRONTEND + +# ===================================================== -# After other app.mount()s -app.mount("/outputs", StaticFiles(directory=OUTPUT_DIR), name="outputs") -app.mount("/assets", StaticFiles(directory="frontend/dist/assets"), name="assets") -from fastapi.staticfiles import StaticFiles -app.mount("/", StaticFiles(directory="frontend/dist", html=True), name="static") +# Serve frontend only if it has been built; avoid startup failure when dist/ is missing. +FRONTEND_DIST = os.path.abspath(os.path.join(os.path.dirname(__file__), "../frontend/dist")) +# Check if frontend/dist exists in /app (Docker), otherwise check relative to script location +if not os.path.isdir(FRONTEND_DIST): + # Fallback for Docker: frontend is copied to ./frontend/dist during build + FRONTEND_DIST = os.path.join(os.path.dirname(__file__), "frontend/dist") + +ASSETS_DIR = os.path.join(FRONTEND_DIST, "assets") + +if os.path.isdir(ASSETS_DIR): + app.mount("/assets", StaticFiles(directory=ASSETS_DIR), name="assets") +else: + print("â„šī¸ Frontend assets directory not found — skipping /assets mount.") @app.get("/") async def serve_frontend(): - index_path = os.path.join("frontend", "dist", "index.html") - return FileResponse(index_path) + index_path = os.path.join(FRONTEND_DIST, "index.html") + if os.path.isfile(index_path): + return FileResponse(index_path) + return JSONResponse({"message": "Backend is running. Frontend build not found."}) + +@app.get("/{file_path:path}") +async def serve_static_files(file_path: str): + """Serve static files from frontend dist (images, logos, etc.)""" + # Skip API routes + if file_path.startswith(("predict", "reports", "models", "health", "outputs", "assets", "cyto", "colpo", "histo")): + return JSONResponse({"error": "Not found"}, status_code=404) + + # Try to serve file from dist root + static_file = os.path.join(FRONTEND_DIST, file_path) + if os.path.isfile(static_file): + return FileResponse(static_file) + + # Fallback to index.html for client-side routing + index_path = os.path.join(FRONTEND_DIST, "index.html") + if os.path.isfile(index_path): + return FileResponse(index_path) + + return JSONResponse({"error": "Not found"}, status_code=404) if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0", port=7860) - \ No newline at end of file + # Use PORT provided by the environment (Hugging Face Spaces sets PORT=7860) + port = int(os.environ.get("PORT", 7860)) + uvicorn.run(app, host="0.0.0.0", port=port) diff --git a/backend/outputs/detected_1a8f90ea.jpg b/backend/outputs/detected_1a8f90ea.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1277a2bde8a676cb647fa2ee41311a98c2eebc86 --- /dev/null +++ b/backend/outputs/detected_1a8f90ea.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e +size 863185 diff --git a/backend/outputs/detected_1c20231d.jpg b/backend/outputs/detected_1c20231d.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2b114d9e518d7088db365ad39ed0a994a514b6d8 --- /dev/null +++ b/backend/outputs/detected_1c20231d.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d01f349abe5183ae4bdbcd42a4ad40be5e073d45489cf5c1fd5a5f12091b8e4 +size 250535 diff --git a/backend/outputs/detected_2198d45d.jpg b/backend/outputs/detected_2198d45d.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1277a2bde8a676cb647fa2ee41311a98c2eebc86 --- /dev/null +++ b/backend/outputs/detected_2198d45d.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e +size 863185 diff --git a/backend/outputs/detected_258af4fa.jpg b/backend/outputs/detected_258af4fa.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1277a2bde8a676cb647fa2ee41311a98c2eebc86 --- /dev/null +++ b/backend/outputs/detected_258af4fa.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e +size 863185 diff --git a/backend/outputs/detected_268b6165.jpg b/backend/outputs/detected_268b6165.jpg new file mode 100644 index 0000000000000000000000000000000000000000..771b957912af8dc94e7bf2649abb4831f4663989 --- /dev/null +++ b/backend/outputs/detected_268b6165.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e83314a6775b3665bdd77b769922222da8268e43871b489739a69fdcbf1970e6 +size 1438849 diff --git a/backend/outputs/detected_39406fb4.jpg b/backend/outputs/detected_39406fb4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2b114d9e518d7088db365ad39ed0a994a514b6d8 --- /dev/null +++ b/backend/outputs/detected_39406fb4.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d01f349abe5183ae4bdbcd42a4ad40be5e073d45489cf5c1fd5a5f12091b8e4 +size 250535 diff --git a/backend/outputs/detected_39c91983.jpg b/backend/outputs/detected_39c91983.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2b114d9e518d7088db365ad39ed0a994a514b6d8 --- /dev/null +++ b/backend/outputs/detected_39c91983.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d01f349abe5183ae4bdbcd42a4ad40be5e073d45489cf5c1fd5a5f12091b8e4 +size 250535 diff --git a/backend/outputs/detected_46cf6466.jpg b/backend/outputs/detected_46cf6466.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2b114d9e518d7088db365ad39ed0a994a514b6d8 --- /dev/null +++ b/backend/outputs/detected_46cf6466.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d01f349abe5183ae4bdbcd42a4ad40be5e073d45489cf5c1fd5a5f12091b8e4 +size 250535 diff --git a/backend/outputs/detected_48f5ddde.jpg b/backend/outputs/detected_48f5ddde.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1277a2bde8a676cb647fa2ee41311a98c2eebc86 --- /dev/null +++ b/backend/outputs/detected_48f5ddde.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e +size 863185 diff --git a/backend/outputs/detected_4dc34d38.jpg b/backend/outputs/detected_4dc34d38.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1277a2bde8a676cb647fa2ee41311a98c2eebc86 --- /dev/null +++ b/backend/outputs/detected_4dc34d38.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e +size 863185 diff --git a/backend/outputs/detected_4e71956d.jpg b/backend/outputs/detected_4e71956d.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1277a2bde8a676cb647fa2ee41311a98c2eebc86 --- /dev/null +++ b/backend/outputs/detected_4e71956d.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e +size 863185 diff --git a/backend/outputs/detected_53ef21ce.jpg b/backend/outputs/detected_53ef21ce.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ec6811d4cd80da6104946f9d8de78163bca87330 --- /dev/null +++ b/backend/outputs/detected_53ef21ce.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91944d821c3f975756875882ead5953ca6bf39834f799e7e22f47149141bec7f +size 8816 diff --git a/backend/outputs/detected_5af838c6.jpg b/backend/outputs/detected_5af838c6.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1277a2bde8a676cb647fa2ee41311a98c2eebc86 --- /dev/null +++ b/backend/outputs/detected_5af838c6.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e +size 863185 diff --git a/backend/outputs/detected_5e93888d.jpg b/backend/outputs/detected_5e93888d.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1277a2bde8a676cb647fa2ee41311a98c2eebc86 --- /dev/null +++ b/backend/outputs/detected_5e93888d.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e +size 863185 diff --git a/backend/outputs/detected_6526bf9c.jpg b/backend/outputs/detected_6526bf9c.jpg new file mode 100644 index 0000000000000000000000000000000000000000..771b957912af8dc94e7bf2649abb4831f4663989 --- /dev/null +++ b/backend/outputs/detected_6526bf9c.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e83314a6775b3665bdd77b769922222da8268e43871b489739a69fdcbf1970e6 +size 1438849 diff --git a/backend/outputs/detected_669a5877.jpg b/backend/outputs/detected_669a5877.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a384840324419b4a682c472143829065efd701b8 --- /dev/null +++ b/backend/outputs/detected_669a5877.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c91fa3e78d8106656cdeace2f16de48742e3042e265057712922c6653cad828 +size 1320206 diff --git a/backend/outputs/detected_85aa7683.jpg b/backend/outputs/detected_85aa7683.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2b114d9e518d7088db365ad39ed0a994a514b6d8 --- /dev/null +++ b/backend/outputs/detected_85aa7683.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d01f349abe5183ae4bdbcd42a4ad40be5e073d45489cf5c1fd5a5f12091b8e4 +size 250535 diff --git a/backend/outputs/detected_8eed01a5.jpg b/backend/outputs/detected_8eed01a5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1277a2bde8a676cb647fa2ee41311a98c2eebc86 --- /dev/null +++ b/backend/outputs/detected_8eed01a5.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e +size 863185 diff --git a/backend/outputs/detected_907c0662.jpg b/backend/outputs/detected_907c0662.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1277a2bde8a676cb647fa2ee41311a98c2eebc86 --- /dev/null +++ b/backend/outputs/detected_907c0662.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e +size 863185 diff --git a/backend/outputs/detected_91737a8d.jpg b/backend/outputs/detected_91737a8d.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1277a2bde8a676cb647fa2ee41311a98c2eebc86 --- /dev/null +++ b/backend/outputs/detected_91737a8d.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e +size 863185 diff --git a/backend/outputs/detected_abff463d.jpg b/backend/outputs/detected_abff463d.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1277a2bde8a676cb647fa2ee41311a98c2eebc86 --- /dev/null +++ b/backend/outputs/detected_abff463d.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e +size 863185 diff --git a/backend/outputs/detected_b02db619.jpg b/backend/outputs/detected_b02db619.jpg new file mode 100644 index 0000000000000000000000000000000000000000..771b957912af8dc94e7bf2649abb4831f4663989 --- /dev/null +++ b/backend/outputs/detected_b02db619.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e83314a6775b3665bdd77b769922222da8268e43871b489739a69fdcbf1970e6 +size 1438849 diff --git a/backend/outputs/detected_cb4d377f.jpg b/backend/outputs/detected_cb4d377f.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1277a2bde8a676cb647fa2ee41311a98c2eebc86 --- /dev/null +++ b/backend/outputs/detected_cb4d377f.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e +size 863185 diff --git a/backend/outputs/detected_df1af137.jpg b/backend/outputs/detected_df1af137.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2b114d9e518d7088db365ad39ed0a994a514b6d8 --- /dev/null +++ b/backend/outputs/detected_df1af137.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d01f349abe5183ae4bdbcd42a4ad40be5e073d45489cf5c1fd5a5f12091b8e4 +size 250535 diff --git a/backend/outputs/detected_e01aa42a.jpg b/backend/outputs/detected_e01aa42a.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2b114d9e518d7088db365ad39ed0a994a514b6d8 --- /dev/null +++ b/backend/outputs/detected_e01aa42a.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d01f349abe5183ae4bdbcd42a4ad40be5e073d45489cf5c1fd5a5f12091b8e4 +size 250535 diff --git a/backend/outputs/detected_e5762460.jpg b/backend/outputs/detected_e5762460.jpg new file mode 100644 index 0000000000000000000000000000000000000000..771b957912af8dc94e7bf2649abb4831f4663989 --- /dev/null +++ b/backend/outputs/detected_e5762460.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e83314a6775b3665bdd77b769922222da8268e43871b489739a69fdcbf1970e6 +size 1438849 diff --git a/backend/outputs/detected_e705fd2d.jpg b/backend/outputs/detected_e705fd2d.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1277a2bde8a676cb647fa2ee41311a98c2eebc86 --- /dev/null +++ b/backend/outputs/detected_e705fd2d.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e +size 863185 diff --git a/backend/outputs/detected_e9a00302.jpg b/backend/outputs/detected_e9a00302.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1277a2bde8a676cb647fa2ee41311a98c2eebc86 --- /dev/null +++ b/backend/outputs/detected_e9a00302.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e +size 863185 diff --git a/backend/outputs/detected_fd305c21.jpg b/backend/outputs/detected_fd305c21.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2b114d9e518d7088db365ad39ed0a994a514b6d8 --- /dev/null +++ b/backend/outputs/detected_fd305c21.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d01f349abe5183ae4bdbcd42a4ad40be5e073d45489cf5c1fd5a5f12091b8e4 +size 250535 diff --git a/backend/outputs/images/detected_104914d9.jpg b/backend/outputs/images/detected_104914d9.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1277a2bde8a676cb647fa2ee41311a98c2eebc86 --- /dev/null +++ b/backend/outputs/images/detected_104914d9.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e +size 863185 diff --git a/backend/outputs/images/detected_14e068c7.jpg b/backend/outputs/images/detected_14e068c7.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2b114d9e518d7088db365ad39ed0a994a514b6d8 --- /dev/null +++ b/backend/outputs/images/detected_14e068c7.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d01f349abe5183ae4bdbcd42a4ad40be5e073d45489cf5c1fd5a5f12091b8e4 +size 250535 diff --git a/backend/outputs/images/detected_3861cc56.jpg b/backend/outputs/images/detected_3861cc56.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2b114d9e518d7088db365ad39ed0a994a514b6d8 --- /dev/null +++ b/backend/outputs/images/detected_3861cc56.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d01f349abe5183ae4bdbcd42a4ad40be5e073d45489cf5c1fd5a5f12091b8e4 +size 250535 diff --git a/backend/outputs/images/detected_5e5e7d55.jpg b/backend/outputs/images/detected_5e5e7d55.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2b114d9e518d7088db365ad39ed0a994a514b6d8 --- /dev/null +++ b/backend/outputs/images/detected_5e5e7d55.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d01f349abe5183ae4bdbcd42a4ad40be5e073d45489cf5c1fd5a5f12091b8e4 +size 250535 diff --git a/backend/outputs/images/detected_7b413ad9.jpg b/backend/outputs/images/detected_7b413ad9.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1277a2bde8a676cb647fa2ee41311a98c2eebc86 --- /dev/null +++ b/backend/outputs/images/detected_7b413ad9.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:406210b3f9314db8ac98883d1fdb393bf75315288910184b9a5a8e9a5b6a013e +size 863185 diff --git a/backend/outputs/images/detected_7d0787ca.jpg b/backend/outputs/images/detected_7d0787ca.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6e7a2ee5d9173c43b2be0e7e445b326f8e539f25 --- /dev/null +++ b/backend/outputs/images/detected_7d0787ca.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abfdd2c63ba9ec5e08dd8e51f235e7b3264b82b8473d8963e101d547be771f0d +size 869865 diff --git a/backend/outputs/images/detected_8e6cc7ce.jpg b/backend/outputs/images/detected_8e6cc7ce.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2b114d9e518d7088db365ad39ed0a994a514b6d8 --- /dev/null +++ b/backend/outputs/images/detected_8e6cc7ce.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d01f349abe5183ae4bdbcd42a4ad40be5e073d45489cf5c1fd5a5f12091b8e4 +size 250535 diff --git a/backend/outputs/images/detected_a13a604c.jpg b/backend/outputs/images/detected_a13a604c.jpg new file mode 100644 index 0000000000000000000000000000000000000000..771b957912af8dc94e7bf2649abb4831f4663989 --- /dev/null +++ b/backend/outputs/images/detected_a13a604c.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e83314a6775b3665bdd77b769922222da8268e43871b489739a69fdcbf1970e6 +size 1438849 diff --git a/backend/outputs/images/detected_ca6e3067.jpg b/backend/outputs/images/detected_ca6e3067.jpg new file mode 100644 index 0000000000000000000000000000000000000000..771b957912af8dc94e7bf2649abb4831f4663989 --- /dev/null +++ b/backend/outputs/images/detected_ca6e3067.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e83314a6775b3665bdd77b769922222da8268e43871b489739a69fdcbf1970e6 +size 1438849 diff --git a/backend/outputs/images/detected_d8cb543f.jpg b/backend/outputs/images/detected_d8cb543f.jpg new file mode 100644 index 0000000000000000000000000000000000000000..771b957912af8dc94e7bf2649abb4831f4663989 --- /dev/null +++ b/backend/outputs/images/detected_d8cb543f.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e83314a6775b3665bdd77b769922222da8268e43871b489739a69fdcbf1970e6 +size 1438849 diff --git a/backend/outputs/images/detected_e3d5055d.jpg b/backend/outputs/images/detected_e3d5055d.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2b114d9e518d7088db365ad39ed0a994a514b6d8 --- /dev/null +++ b/backend/outputs/images/detected_e3d5055d.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d01f349abe5183ae4bdbcd42a4ad40be5e073d45489cf5c1fd5a5f12091b8e4 +size 250535 diff --git a/backend/outputs/reports/dsbn_63a52149/report.html b/backend/outputs/reports/dsbn_63a52149/report.html new file mode 100644 index 0000000000000000000000000000000000000000..4ee730c3bb3fd2f8a8be0a55178c5877e1689937 --- /dev/null +++ b/backend/outputs/reports/dsbn_63a52149/report.html @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98ee4eb44e52b0d37a356dd36d6df6b0f49df76fe0aae2d1b29d3401246bc174 +size 6036 diff --git a/backend/outputs/reports/dsbn_63a52149/report.json b/backend/outputs/reports/dsbn_63a52149/report.json new file mode 100644 index 0000000000000000000000000000000000000000..4fca5351e186cb46c1ba38546a711b6697404687 --- /dev/null +++ b/backend/outputs/reports/dsbn_63a52149/report.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19dbfd782ff0095de8b7ef319e802b27718c245091f81e537b3d2cbe050ea614 +size 355 diff --git a/backend/outputs/reports/erza_6e66bad8/report.html b/backend/outputs/reports/erza_6e66bad8/report.html new file mode 100644 index 0000000000000000000000000000000000000000..8ce35a6cebf58e9b5a8f43cae189e5b32e9a5715 --- /dev/null +++ b/backend/outputs/reports/erza_6e66bad8/report.html @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:557412e4ce57eea9966b394b1f7066ac80f1cb13e4390d4ff6502c025b0d8d53 +size 1128 diff --git a/backend/outputs/reports/erza_6e66bad8/report.json b/backend/outputs/reports/erza_6e66bad8/report.json new file mode 100644 index 0000000000000000000000000000000000000000..2d112d44635d86a9cd45ffa693b8ce936741ce1c --- /dev/null +++ b/backend/outputs/reports/erza_6e66bad8/report.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89760baa43ee738c62e3e03ccc2c77787f76353d1ed3c07e367b117d2c6bb907 +size 441 diff --git a/backend/outputs/reports/erza_6e66bad8/report.pdf b/backend/outputs/reports/erza_6e66bad8/report.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5ed86a10be255b928bd7c8cdfc2cb788ef54e96b --- /dev/null +++ b/backend/outputs/reports/erza_6e66bad8/report.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec19b489c1950a30f09301ffb4565f1fad2f125f04e863387f0bf47868e98e1c +size 1800630 diff --git a/backend/outputs/reports/erza_8f376dea/report.html b/backend/outputs/reports/erza_8f376dea/report.html new file mode 100644 index 0000000000000000000000000000000000000000..370d79e865384f3857833c0aee328691c85b7823 --- /dev/null +++ b/backend/outputs/reports/erza_8f376dea/report.html @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb395c40b21a3c3fb78addfe0cec2667a963ff3086e6d6e9f3f2fb377a9b09cb +size 1143 diff --git a/backend/outputs/reports/erza_8f376dea/report.json b/backend/outputs/reports/erza_8f376dea/report.json new file mode 100644 index 0000000000000000000000000000000000000000..76249ef75b208e5456550e60077980fb6a771802 --- /dev/null +++ b/backend/outputs/reports/erza_8f376dea/report.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55faf6e8d7d9ca0b194bc4aea7fa72f12c1196eff0020ba4ff2191d6ff020a62 +size 363 diff --git a/backend/outputs/reports/erza_8f376dea/report.pdf b/backend/outputs/reports/erza_8f376dea/report.pdf new file mode 100644 index 0000000000000000000000000000000000000000..385f28c28e68f36d4070127669900b63fc8e4ffd --- /dev/null +++ b/backend/outputs/reports/erza_8f376dea/report.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd0d5573a079ba62bb899961f7f5c22ec16ca32f1ba939b840c21bec55e81b3f +size 1734 diff --git a/backend/outputs/reports/erza_a2651888/report.html b/backend/outputs/reports/erza_a2651888/report.html new file mode 100644 index 0000000000000000000000000000000000000000..31fc16b60ba9dce4c9b84e9aa5b31cfc77787341 --- /dev/null +++ b/backend/outputs/reports/erza_a2651888/report.html @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2454da2bced12e7b260e20f5e9cc4cbb43d4897aaee6d9e3d52ccc234d574f3 +size 1128 diff --git a/backend/outputs/reports/erza_a2651888/report.json b/backend/outputs/reports/erza_a2651888/report.json new file mode 100644 index 0000000000000000000000000000000000000000..ca949c5e41d1e9c76df3c9812ef7548a14bdba23 --- /dev/null +++ b/backend/outputs/reports/erza_a2651888/report.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c783389ab4da5d7e8b59c008f92c7584ad850c27177805051a0125cd3ce7b7cc +size 336 diff --git a/backend/outputs/reports/erza_a94d4cf9/report.html b/backend/outputs/reports/erza_a94d4cf9/report.html new file mode 100644 index 0000000000000000000000000000000000000000..debd0769931ba95aa1fd015a9681fd1007eb8204 --- /dev/null +++ b/backend/outputs/reports/erza_a94d4cf9/report.html @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9d5ab9b7c9806dbaec855fc716c20db6d0e5af52c658e99294823133af1a961 +size 1917 diff --git a/backend/outputs/reports/erza_a94d4cf9/report.json b/backend/outputs/reports/erza_a94d4cf9/report.json new file mode 100644 index 0000000000000000000000000000000000000000..bf4f5440cb18491dd9518412ff4d2c3d0a8e58f4 --- /dev/null +++ b/backend/outputs/reports/erza_a94d4cf9/report.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d1a3171da8e1b7c19fbc84b43ca1375f2508b080be2de5da1d7070581a2f0e5 +size 367 diff --git a/backend/outputs/reports/erza_a94d4cf9/report.pdf b/backend/outputs/reports/erza_a94d4cf9/report.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8c9718869cd91bc505135980a243a9feb86a62df --- /dev/null +++ b/backend/outputs/reports/erza_a94d4cf9/report.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b38e0ff2afc86db2ceb092c3edf5169ba9619e2f10f1d0ff2fafaa822dc5d839 +size 1720 diff --git a/backend/outputs/reports/erza_bbbcdc90/report.html b/backend/outputs/reports/erza_bbbcdc90/report.html new file mode 100644 index 0000000000000000000000000000000000000000..29fdea9e9e287353a34760e200f15d28ef9be7e7 --- /dev/null +++ b/backend/outputs/reports/erza_bbbcdc90/report.html @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1f4744fe6c70612d761ec5e030f19585d88324e9254ca0f645809d20ffab977 +size 1917 diff --git a/backend/outputs/reports/erza_bbbcdc90/report.json b/backend/outputs/reports/erza_bbbcdc90/report.json new file mode 100644 index 0000000000000000000000000000000000000000..7ae8396bc9cdf88f1f970e638945d9156ef74246 --- /dev/null +++ b/backend/outputs/reports/erza_bbbcdc90/report.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:657a727866b3ddf1cfbb81d713cb34ffb7dcbff2ac5f0bce6108ca68e40b71fb +size 367 diff --git a/backend/outputs/reports/erza_bbbcdc90/report.pdf b/backend/outputs/reports/erza_bbbcdc90/report.pdf new file mode 100644 index 0000000000000000000000000000000000000000..cc8cdd49642966db28bf5900589df311ca23c1ee --- /dev/null +++ b/backend/outputs/reports/erza_bbbcdc90/report.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6896eb4af03613a036e0db24f5abda60dd174d32328e9f653689a490733f0f13 +size 1720 diff --git a/backend/outputs/reports/erza_bc2f2423/report.html b/backend/outputs/reports/erza_bc2f2423/report.html new file mode 100644 index 0000000000000000000000000000000000000000..5b7906d1c5492467d9a4a9ed3e790f808e0ba9ea --- /dev/null +++ b/backend/outputs/reports/erza_bc2f2423/report.html @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d7d8f6df016f6ad7ad252df0b9853281cc1b070ef5fec989afcc21e00cf9f84 +size 1128 diff --git a/backend/outputs/reports/erza_bc2f2423/report.json b/backend/outputs/reports/erza_bc2f2423/report.json new file mode 100644 index 0000000000000000000000000000000000000000..d8b1cbe36da188aae0685e0638112b0148701126 --- /dev/null +++ b/backend/outputs/reports/erza_bc2f2423/report.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6a1a1fb680605d77bd47061285a996c1c1c662f2c6521988982fec1e2a84801 +size 367 diff --git a/backend/outputs/reports/erza_c11f160b/report.html b/backend/outputs/reports/erza_c11f160b/report.html new file mode 100644 index 0000000000000000000000000000000000000000..0ef3f0f67873ac3f03a0c2e414e4c8a6e48f0621 --- /dev/null +++ b/backend/outputs/reports/erza_c11f160b/report.html @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a98a65e44aeac76c1b897689ab5bd490d1fe778f9d383de6039c54014ab5d88 +size 1143 diff --git a/backend/outputs/reports/erza_c11f160b/report.json b/backend/outputs/reports/erza_c11f160b/report.json new file mode 100644 index 0000000000000000000000000000000000000000..4b69c433f3a8a50fd90a13ef9021512e20f40561 --- /dev/null +++ b/backend/outputs/reports/erza_c11f160b/report.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a12b4d02d92d7072b006dedc13ffeaaf41399ecc2a59c7aa9d335874d1b72c2 +size 366 diff --git a/backend/outputs/reports/erza_d901cb83/report.html b/backend/outputs/reports/erza_d901cb83/report.html new file mode 100644 index 0000000000000000000000000000000000000000..060938d8370216ad6af8eee85e32801ceb63c02b --- /dev/null +++ b/backend/outputs/reports/erza_d901cb83/report.html @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59f37c38ed184685a81cc8473036141a0f08b459f920021be152cc16b2ac6e02 +size 1134 diff --git a/backend/outputs/reports/erza_d901cb83/report.json b/backend/outputs/reports/erza_d901cb83/report.json new file mode 100644 index 0000000000000000000000000000000000000000..8e5f41e09b1aca9fb4c2ed51dbfcd8d0f0b7a36f --- /dev/null +++ b/backend/outputs/reports/erza_d901cb83/report.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa563d2e9d6c23078199f6ce7670c5f1bbf2e827cfbf1ece697006fb4860f0c4 +size 342 diff --git a/backend/outputs/reports/erza_f72ad44c/report.html b/backend/outputs/reports/erza_f72ad44c/report.html new file mode 100644 index 0000000000000000000000000000000000000000..7a7d17ffa8b10fcbb51802a5c15294c5610f1043 --- /dev/null +++ b/backend/outputs/reports/erza_f72ad44c/report.html @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7d34dcd58befed2ba4ce7a366189694ce7606e3acf69e15d7c0393392c6714c +size 1143 diff --git a/backend/outputs/reports/erza_f72ad44c/report.json b/backend/outputs/reports/erza_f72ad44c/report.json new file mode 100644 index 0000000000000000000000000000000000000000..f5e0ed33787c9e2233d48393fb6ad002273a3485 --- /dev/null +++ b/backend/outputs/reports/erza_f72ad44c/report.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f5ea84bbd4aba31eb99e845742139bf0d46dcb2bc6842dd1a26edfe018f418fb +size 363 diff --git a/backend/outputs/reports/erza_f72ad44c/report.pdf b/backend/outputs/reports/erza_f72ad44c/report.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e06c05dd7fe3c3e37f8883cebfdfaa44409083d2 --- /dev/null +++ b/backend/outputs/reports/erza_f72ad44c/report.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c01c18a3690053bcd3d11e6c2608d41954886b468a39fa56e55a602ab030ebb3 +size 1734 diff --git a/backend/outputs/reports/fsezfzf_083367e2/report.html b/backend/outputs/reports/fsezfzf_083367e2/report.html new file mode 100644 index 0000000000000000000000000000000000000000..c13fceff115ba735fb6044df075263a0a55fec15 --- /dev/null +++ b/backend/outputs/reports/fsezfzf_083367e2/report.html @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67a3cb2a45172965bdfc10da7e7f4ed8283a8ca18ba7de8a0a0aadd01ad63b05 +size 6197 diff --git a/backend/outputs/reports/fsezfzf_083367e2/report.json b/backend/outputs/reports/fsezfzf_083367e2/report.json new file mode 100644 index 0000000000000000000000000000000000000000..0357e01545af3e9e017c3d698297308f56c265fb --- /dev/null +++ b/backend/outputs/reports/fsezfzf_083367e2/report.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d333f58b71bfa8ba13fc024f349576cb18274df0755f564ab97f6279d7ebcf9 +size 436 diff --git a/backend/outputs/reports/fsezfzf_1074d06e/report.html b/backend/outputs/reports/fsezfzf_1074d06e/report.html new file mode 100644 index 0000000000000000000000000000000000000000..c2d70fdf1389a7dfab82abe23b3f4e33214bc17f --- /dev/null +++ b/backend/outputs/reports/fsezfzf_1074d06e/report.html @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9646a4a543d0af230d9a6ceef1717890045c0e65bc4570aa85c732c380045c4 +size 5950 diff --git a/backend/outputs/reports/fsezfzf_1074d06e/report.json b/backend/outputs/reports/fsezfzf_1074d06e/report.json new file mode 100644 index 0000000000000000000000000000000000000000..139c6ccb947498935f203e0fa2cf82f27419ab3c --- /dev/null +++ b/backend/outputs/reports/fsezfzf_1074d06e/report.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7df899d9ecdcf67178cc03e285e01c130f0623b143bfb7f45a5aa4094fa3c74 +size 362 diff --git a/backend/outputs/reports/fsezfzf_4ffb1eea/report.html b/backend/outputs/reports/fsezfzf_4ffb1eea/report.html new file mode 100644 index 0000000000000000000000000000000000000000..db64b891161b78b7402b3b4cc7b4d28455148cdd --- /dev/null +++ b/backend/outputs/reports/fsezfzf_4ffb1eea/report.html @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f5b97af37f13ef36a6405a7d2919aefba3eed0c3019ce4043cfed680d0ab3b4 +size 6175 diff --git a/backend/outputs/reports/fsezfzf_4ffb1eea/report.json b/backend/outputs/reports/fsezfzf_4ffb1eea/report.json new file mode 100644 index 0000000000000000000000000000000000000000..8c6c4925156bb46c832c61599a50f180c1c8cf71 --- /dev/null +++ b/backend/outputs/reports/fsezfzf_4ffb1eea/report.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ef4d08cc3d554ffe087fcf8dcbc5776b8dd508efd4fa6120b8fe55d80becd8a +size 432 diff --git a/backend/outputs/reports/fsezfzf_7266695f/report.html b/backend/outputs/reports/fsezfzf_7266695f/report.html new file mode 100644 index 0000000000000000000000000000000000000000..99e680de6a8f5bfb64e3a8c89cd7f62f8fc38292 --- /dev/null +++ b/backend/outputs/reports/fsezfzf_7266695f/report.html @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c418fb34b9b6d969b8bb40dba8dd24f96221234aff40b78165bb18934f9448ed +size 6068 diff --git a/backend/outputs/reports/fsezfzf_7266695f/report.json b/backend/outputs/reports/fsezfzf_7266695f/report.json new file mode 100644 index 0000000000000000000000000000000000000000..870b5929a0acd6561387d395ef764e24afe4bc54 --- /dev/null +++ b/backend/outputs/reports/fsezfzf_7266695f/report.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcffc829fb71fba6333176dec1699743a2814bbe066e6a4312bd9ccad390ec98 +size 359 diff --git a/backend/outputs/reports/fsezfzf_7f251e78/report.html b/backend/outputs/reports/fsezfzf_7f251e78/report.html new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/outputs/reports/fsezfzf_7f251e78/report.json b/backend/outputs/reports/fsezfzf_7f251e78/report.json new file mode 100644 index 0000000000000000000000000000000000000000..8a0f832b5b2b37be4dc1faa01ec1157e830cd17f --- /dev/null +++ b/backend/outputs/reports/fsezfzf_7f251e78/report.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a34f1a1dc76164893a9a694a5fa175908e7ee35b3a4ff824e3b49da682119ad +size 367 diff --git a/backend/outputs/reports/fsezfzf_ab2400ea/report.html b/backend/outputs/reports/fsezfzf_ab2400ea/report.html new file mode 100644 index 0000000000000000000000000000000000000000..6bd0c59553cb5e214d963cd03bc7715b59002f72 --- /dev/null +++ b/backend/outputs/reports/fsezfzf_ab2400ea/report.html @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:226a71c1234f0980e6e97ebcd8a057d8ca3308bd6e42abb05efcf6c06b579238 +size 2098 diff --git a/backend/outputs/reports/fsezfzf_ab2400ea/report.json b/backend/outputs/reports/fsezfzf_ab2400ea/report.json new file mode 100644 index 0000000000000000000000000000000000000000..667ee0df73c6b4e7ed9667fa8dee60f33f9619d0 --- /dev/null +++ b/backend/outputs/reports/fsezfzf_ab2400ea/report.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54a69941ecd1e5410fc5af9b3568ef83a2034df07fa02de9c9ea5bb3fdc88cfe +size 432 diff --git a/backend/outputs/reports/fsezfzf_b577f52d/report.html b/backend/outputs/reports/fsezfzf_b577f52d/report.html new file mode 100644 index 0000000000000000000000000000000000000000..17f3edf61a309cd8741408f7b1f8288fee1e034a --- /dev/null +++ b/backend/outputs/reports/fsezfzf_b577f52d/report.html @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a144efe6b53536d1eb9edbfecde67c14b1d9670344ce7cdd105178b7b4cf4d9c +size 2103 diff --git a/backend/outputs/reports/fsezfzf_b577f52d/report.json b/backend/outputs/reports/fsezfzf_b577f52d/report.json new file mode 100644 index 0000000000000000000000000000000000000000..b868c94625277a224551e2f3a38545598f2936c2 --- /dev/null +++ b/backend/outputs/reports/fsezfzf_b577f52d/report.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc44d339da0602641d3f5b8176b9acc64cc99317101bb555e23bb4a9177e29aa +size 437 diff --git a/backend/outputs/reports/fsezfzf_c34e62e5/report.html b/backend/outputs/reports/fsezfzf_c34e62e5/report.html new file mode 100644 index 0000000000000000000000000000000000000000..59843d735a87fe39e461b1373ce075aaba017287 --- /dev/null +++ b/backend/outputs/reports/fsezfzf_c34e62e5/report.html @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59af5ec85987985b589e30ad2350408e2ac60d69078f4b237e506fea539a3d8c +size 6026 diff --git a/backend/outputs/reports/fsezfzf_c34e62e5/report.json b/backend/outputs/reports/fsezfzf_c34e62e5/report.json new file mode 100644 index 0000000000000000000000000000000000000000..b744fe5b922e83f57151fe7e9503bb86a8c8307e --- /dev/null +++ b/backend/outputs/reports/fsezfzf_c34e62e5/report.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e9b24602521692a23c10e8a1408566845ee92092c1ebc54f05f917fff3d1d92 +size 359 diff --git a/backend/outputs/reports/fsezfzf_d21fd26c/report.html b/backend/outputs/reports/fsezfzf_d21fd26c/report.html new file mode 100644 index 0000000000000000000000000000000000000000..e6a83be64b71002d4553927bb0cd3b255df4fdf8 --- /dev/null +++ b/backend/outputs/reports/fsezfzf_d21fd26c/report.html @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2107164c27448d071d367eb7b3f84b1c53058c1f63ec397d8dab13fe1f6cfb5a +size 1844 diff --git a/backend/outputs/reports/fsezfzf_d21fd26c/report.json b/backend/outputs/reports/fsezfzf_d21fd26c/report.json new file mode 100644 index 0000000000000000000000000000000000000000..1119b3ea997e8428b1e17c9b92bfb3a6d3334b17 --- /dev/null +++ b/backend/outputs/reports/fsezfzf_d21fd26c/report.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:adfb93f55eb7f3dd66ec2d90d4e992734d1bcc258f571beb1aebbf7900e76363 +size 366 diff --git a/backend/outputs/reports/fsezfzf_e177c227/report.html b/backend/outputs/reports/fsezfzf_e177c227/report.html new file mode 100644 index 0000000000000000000000000000000000000000..c72329588a10bb22aeadbd555f3042f123c4a11f --- /dev/null +++ b/backend/outputs/reports/fsezfzf_e177c227/report.html @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:268947ee349350b7983280dd2dc68dcb71fff33f1f8b8ef8b36bb87ef8155c93 +size 5994 diff --git a/backend/outputs/reports/fsezfzf_e177c227/report.json b/backend/outputs/reports/fsezfzf_e177c227/report.json new file mode 100644 index 0000000000000000000000000000000000000000..a64cc011ed46579f1db417bab762d12d96553b90 --- /dev/null +++ b/backend/outputs/reports/fsezfzf_e177c227/report.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbf818e8e1849016141bf0876618d122d4e9a9b35b103218a08d1ff21f094978 +size 368 diff --git a/backend/outputs/reports/fsezfzf_fe7e1188/report.html b/backend/outputs/reports/fsezfzf_fe7e1188/report.html new file mode 100644 index 0000000000000000000000000000000000000000..d65dde7cb3e9e59d6e3639ba630b58de7a5551ad --- /dev/null +++ b/backend/outputs/reports/fsezfzf_fe7e1188/report.html @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a45e7766c2dec02e483f6e8035f624919c2a2f8a4831bc23a260d0251007368a +size 1843 diff --git a/backend/outputs/reports/fsezfzf_fe7e1188/report.json b/backend/outputs/reports/fsezfzf_fe7e1188/report.json new file mode 100644 index 0000000000000000000000000000000000000000..56b7fa29822bf323a9396f5cd2de6350b0b94ac8 --- /dev/null +++ b/backend/outputs/reports/fsezfzf_fe7e1188/report.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac26adc8a438440fb43681d9527b2a0b726e2d4f95a2d42a387491679cee9daa +size 365 diff --git a/frontend/public/colpo/ab1.jpg b/frontend/public/colpo/ab1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0d0a73846ae43982c21b410b2f946720a075baee Binary files /dev/null and b/frontend/public/colpo/ab1.jpg differ diff --git a/frontend/public/colpo/ab2.jpg b/frontend/public/colpo/ab2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..60dfb71731d1e58fbcf93bb029002543601ff522 Binary files /dev/null and b/frontend/public/colpo/ab2.jpg differ diff --git a/frontend/public/colpo/ab3.jpg b/frontend/public/colpo/ab3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7044e02ace3af87c79c200c73f1b63b4ffc73a97 Binary files /dev/null and b/frontend/public/colpo/ab3.jpg differ diff --git a/frontend/public/colpo/colp1.jpg b/frontend/public/colpo/colp1.jpg deleted file mode 100644 index 03fe9c522a6192401eef3adb7ee737ffd4d61326..0000000000000000000000000000000000000000 Binary files a/frontend/public/colpo/colp1.jpg and /dev/null differ diff --git a/frontend/public/colpo/colp2.jpg b/frontend/public/colpo/colp2.jpg deleted file mode 100644 index fab09d0512eab9d2b7b391f223497e0bba19437c..0000000000000000000000000000000000000000 Binary files a/frontend/public/colpo/colp2.jpg and /dev/null differ diff --git a/frontend/public/colpo/colp3.jpg b/frontend/public/colpo/colp3.jpg deleted file mode 100644 index 1e650f8ca6ef4208372f9c9473df1d90afd05761..0000000000000000000000000000000000000000 Binary files a/frontend/public/colpo/colp3.jpg and /dev/null differ diff --git a/frontend/public/colpo/nor1.jpg b/frontend/public/colpo/nor1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c48982892d2d07ed88a8ee23fc6552123db6a00b Binary files /dev/null and b/frontend/public/colpo/nor1.jpg differ diff --git a/frontend/public/colpo/nor2.jpg b/frontend/public/colpo/nor2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..59886de662008a0ae595c7ab64ffe4fa76585b47 Binary files /dev/null and b/frontend/public/colpo/nor2.jpg differ diff --git a/frontend/public/colpo/nor3.jpg b/frontend/public/colpo/nor3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..00eca46b2fb5622af2093a9cbe1b389e7eeb2043 Binary files /dev/null and b/frontend/public/colpo/nor3.jpg differ diff --git a/frontend/public/cyto/HSIL1.jpg b/frontend/public/cyto/HSIL1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..70035118e44d3b610c532ef408a7893c89722dbe --- /dev/null +++ b/frontend/public/cyto/HSIL1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab998d78cda70140e3b997b89edb1e89aaa3d79fbdf47de668059f9da9e43202 +size 280092 diff --git a/frontend/public/cyto/HSIL2.JPG b/frontend/public/cyto/HSIL2.JPG new file mode 100644 index 0000000000000000000000000000000000000000..15ddc020bae8044d1e1b2cd342cea75176b3b9d4 --- /dev/null +++ b/frontend/public/cyto/HSIL2.JPG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:256d4ba4fd86fa0d0df7d6fb5131d1fb2a7184d0157398a0f77dc425193dcd54 +size 301744 diff --git a/frontend/public/cyto/LSIL1.png b/frontend/public/cyto/LSIL1.png new file mode 100644 index 0000000000000000000000000000000000000000..bdcc09f779999e7db017bfacfa99bdcded7ce4a5 --- /dev/null +++ b/frontend/public/cyto/LSIL1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e60ee9d597bc9f297c0bc97c97d4f80b5090f74776cf70742f96f5cc3a15d6f +size 475707 diff --git a/frontend/public/cyto/LSIL2.png b/frontend/public/cyto/LSIL2.png new file mode 100644 index 0000000000000000000000000000000000000000..76d4b8de04aed3b022e33a5742bf086f44c08fb5 --- /dev/null +++ b/frontend/public/cyto/LSIL2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0905a58c8d96933659716857c3ec6012637bcc1a74640d2229b0b78b6fcc4286 +size 481705 diff --git a/frontend/public/cyto/cyt1.jpg b/frontend/public/cyto/cyt1.jpg deleted file mode 100644 index 966f3c5446c5070a8b0cdcd0262e24d6d50c2100..0000000000000000000000000000000000000000 --- a/frontend/public/cyto/cyt1.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5272c04ec47e122e332f3197d8248298eb40f496b51cfe4c37f3f43ec5a9ea2c -size 716187 diff --git a/frontend/public/cyto/cyt3.png b/frontend/public/cyto/cyt3.png deleted file mode 100644 index 5a32ef1ccd916e262aaa453724fa0bff32b31091..0000000000000000000000000000000000000000 --- a/frontend/public/cyto/cyt3.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7533ab6eea48e4a4671c14c87334f8c387e39bf09e7995bbc960db6b04c2cba7 -size 5858373 diff --git a/frontend/public/cyto/neg1.bmp b/frontend/public/cyto/neg1.bmp new file mode 100644 index 0000000000000000000000000000000000000000..1cc954a847d368a55c47f14578d358ec912baf5b --- /dev/null +++ b/frontend/public/cyto/neg1.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed0648c6bc8b490230e99e71a20ef0ddb2f82afef0aa27653d84492e4f0add6d +size 9437238 diff --git a/frontend/public/cyto/cyt2.png b/frontend/public/cyto/neg2.png similarity index 100% rename from frontend/public/cyto/cyt2.png rename to frontend/public/cyto/neg2.png diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ac7c3486faf653fc9b8a0df1ac0738b52eead378..11015f45777216debabbd4a5a3f520ee0d3bf708 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,9 +8,6 @@ import { Footer } from "./components/Footer"; import { ProgressBar } from "./components/progressbar"; export function App() { -// ---------------------------- -// State Management -// ---------------------------- const [selectedTest, setSelectedTest] = useState("cytology"); const [uploadedImage, setUploadedImage] = useState(null); const [selectedModel, setSelectedModel] = useState(""); @@ -19,18 +16,14 @@ const [showResults, setShowResults] = useState(false); const [currentStep, setCurrentStep] = useState(0); const [loading, setLoading] = useState(false); -// ---------------------------- // Progress bar logic -// ---------------------------- useEffect(() => { if (showResults) setCurrentStep(2); else if (uploadedImage) setCurrentStep(1); else setCurrentStep(0); }, [uploadedImage, showResults]); -// ---------------------------- -// Reset logic — new test -// ---------------------------- +// Reset logic useEffect(() => { setCurrentStep(0); setShowResults(false); @@ -39,52 +32,79 @@ setSelectedModel(""); setApiResult(null); }, [selectedTest]); -// ---------------------------- -// Analyze handler (Backend call) -// ---------------------------- -const handleAnalyze = async () => { -if (!uploadedImage || !selectedModel) { -alert("Please select a model and upload an image first!"); -return; -} - - -setLoading(true); -setShowResults(false); -setApiResult(null); - -try { - // Convert Base64 → File - const blob = await fetch(uploadedImage).then((r) => r.blob()); - const file = new File([blob], "input.jpg", { type: blob.type }); - - const formData = new FormData(); - formData.append("file", file); - formData.append("analysis_type", selectedTest); - formData.append("model_name", selectedModel); - - // POST to backend -const baseURL = - import.meta.env.MODE === "development" + const handleAnalyze = async () => { + console.log('Analyze button clicked', { uploadedImage, selectedModel }); + if (!uploadedImage || !selectedModel) { + alert("Please select a model and upload an image first!"); + return; + } + + setLoading(true); + setShowResults(false); + setApiResult(null); + + try { + // Extract file extension from data URL or use .jpg default + const extension = uploadedImage.startsWith('data:image/') + ? uploadedImage.split(';')[0].split('/')[1] + : 'jpg'; + + // Create a more descriptive filename + const filename = `analysis_input.${extension}`; + + let blob: Blob; + if (uploadedImage.startsWith('data:')) { + // Handle data URLs (from file upload) + blob = await fetch(uploadedImage).then(r => r.blob()); + } else { + // Handle sample images (relative URLs) + blob = await fetch(uploadedImage) + .then(r => r.blob()) + .catch(() => { + // If fetch fails, try with base URL + const baseURL = import.meta.env.MODE === "development" + ? "http://127.0.0.1:5173" + : window.location.origin; + return fetch(`${baseURL}${uploadedImage}`).then(r => r.blob()); + }); + } + + const file = new File([blob], filename, { type: blob.type || `image/${extension}` }); + const formData = new FormData(); + formData.append("file", file); + formData.append("model_name", selectedModel); + + console.log('Sending request:', { + filename, + type: file.type, + size: file.size, + model: selectedModel + }); + + const baseURL = import.meta.env.MODE === "development" ? "http://127.0.0.1:8000" : window.location.origin; - const res = await axios.post(`${baseURL}/predict/`, formData, { - headers: { "Content-Type": "multipart/form-data" }, - }); - - setApiResult(res.data); - setShowResults(true); - } catch (err) { - console.error("❌ Error during inference:", err); - alert("Error analyzing the image. Check backend logs."); - } finally { - setLoading(false); - } + const res = await axios.post(`${baseURL}/predict/`, formData, { + headers: { "Content-Type": "multipart/form-data" }, + }); + + if (res.data.error) { + throw new Error(res.data.error); + } + + console.log('Received response:', res.data); + setApiResult(res.data); + setShowResults(true); + } catch (err: any) { + console.error("❌ Error during inference:", err); + const errorMessage = err.response?.data?.error || err.message || "Unknown error occurred"; + alert(`Error analyzing the image: ${errorMessage}`); + } finally { + setLoading(false); + } }; -// ---------------------------- -// Layout -// ---------------------------- + return (
@@ -93,7 +113,6 @@ return (
- {/* Upload & Model Selection */}
- {/* Results Panel (show while loading OR when results ready) */} - {(loading || showResults) && ( + {showResults && ( @@ -120,4 +140,4 @@ return (
{/* Overlay for readability */} -
+
{/* Main footer content */}
diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index cccce8c9c74f068effc53db69219ed462941a9fc..49d0174feb74dadd5c364857226da88c49adf03b 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -10,7 +10,7 @@ export function Header() { "url('/banner.jpeg')", }} > -
+
{/* Logo + Title */}
(null); + const [imgSrc, setImgSrc] = useState(src); + const [error, setError] = useState(false); + + useEffect(() => { + setImgSrc(src); + setError(false); + }, [src]); + + const handleError = () => { + if (!error && imgSrc.startsWith('/')) { + setError(true); + // Try with explicit origin if relative URL + const baseURL = import.meta.env.MODE === "development" + ? "http://127.0.0.1:8000" + : window.location.origin; + setImgSrc(`${baseURL}${imgSrc}`); + } + }; + + return ( + {alt} + ); +} \ No newline at end of file diff --git a/frontend/src/components/ReportModal.tsx b/frontend/src/components/ReportModal.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8d85530bd740e93803822fa703b95da825dd4e19 --- /dev/null +++ b/frontend/src/components/ReportModal.tsx @@ -0,0 +1,226 @@ +import { useState } from 'react'; +import { XIcon } from 'lucide-react'; + +interface ReportModalProps { + isOpen: boolean; + onClose: () => void; + onSubmit: (formData: FormData) => void; // ✅ changed + analysisId: string; + analysisSummaryJson: string; +} + + +export interface ReportFormData { + patient_id: string; + exam_date: string; + metadata: { + physician: string; + facility: string; + specimen_type: string; + clinical_history?: string; + }; + notes?: string; + analysis_id: string; +} + + +export function ReportModal({ isOpen, onClose, onSubmit, analysisId, analysisSummaryJson }: ReportModalProps) { + const [formData, setFormData] = useState({ + patient_id: '', + exam_date: new Date().toISOString().split('T')[0], + metadata: { + physician: '', + facility: '', + specimen_type: '', + clinical_history: '', + }, + notes: '', + analysis_id: analysisId, + }); + + if (!isOpen) return null; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + // Build FormData for FastAPI endpoint + const payload = new FormData(); + payload.append("patient_id", formData.patient_id); + payload.append("exam_date", formData.exam_date); + payload.append("metadata", JSON.stringify(formData.metadata)); + payload.append("notes", formData.notes || ""); + payload.append("analysis_id", formData.analysis_id); + payload.append("analysis_summary", analysisSummaryJson); // 🧠 from /predict/ + + // Pass the FormData object to the parent onSubmit + onSubmit(payload); +}; + + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + if (name.startsWith('metadata.')) { + const field = name.split('.')[1]; + setFormData(prev => ({ + ...prev, + metadata: { + ...prev.metadata, + [field]: value, + }, + })); + } else { + setFormData(prev => ({ + ...prev, + [name]: value, + })); + } + }; + + return ( +
+
+
+
+

Generate Medical Report

+ +
+ +
+ {/* Patient Information */} +
+

Patient Information

+
+
+ + +
+
+ + +
+
+
+ + {/* Slide Metadata */} +
+

Slide Metadata

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {/* Notes */} +
+

Doctor's Notes

+