mstepien commited on
Commit
98b0b74
·
1 Parent(s): 4b0b86d

Hugging Face specific Dockerfile and .jpg excluded

Browse files
.devcontainer/devcontainer.json CHANGED
@@ -1,8 +1,11 @@
1
  {
2
  "name": "Dermatolog AI Scanner Dev",
3
- "build": {
4
- "context": "..",
5
- "dockerfile": "../Dockerfile"
 
 
 
6
  },
7
  "customizations": {
8
  "vscode": {
@@ -15,7 +18,7 @@
15
  }
16
  },
17
  "forwardPorts": [
18
- 8000
19
  ],
20
  "postCreateCommand": "pip install -r requirements-dev.txt && playwright install --with-deps chromium && npm install",
21
  "runArgs": [
 
1
  {
2
  "name": "Dermatolog AI Scanner Dev",
3
+ "image": "medgemma_ic-medgemma-app:latest",
4
+ "features": {
5
+ "ghcr.io/devcontainers/features/common-utils:2": {
6
+ "installZsh": "true",
7
+ "upgradePackages": "true"
8
+ }
9
  },
10
  "customizations": {
11
  "vscode": {
 
18
  }
19
  },
20
  "forwardPorts": [
21
+ 8080
22
  ],
23
  "postCreateCommand": "pip install -r requirements-dev.txt && playwright install --with-deps chromium && npm install",
24
  "runArgs": [
Dockerfile CHANGED
@@ -1,7 +1,5 @@
1
  FROM python:3.11-slim
2
 
3
- WORKDIR /app
4
-
5
  # Install system dependencies
6
  RUN apt-get update && apt-get install -y \
7
  curl \
@@ -13,31 +11,35 @@ RUN apt-get update && apt-get install -y \
13
  && apt-get install -y nodejs \
14
  && rm -rf /var/lib/apt/lists/*
15
 
16
- # Install python dependencies
17
- COPY requirements.txt .
18
- RUN pip install --no-cache-dir -r requirements.txt
 
 
19
 
20
- # Pre-download the model to bake it into the image
21
- # This prevents downloading 4GB+ on every container start
22
- ARG HF_TOKEN
23
- ENV HF_TOKEN=${HF_TOKEN}
 
24
 
25
- RUN python -c "from transformers import AutoProcessor, AutoModel; \
 
 
 
26
  import os; \
27
- token = os.environ.get('HF_TOKEN'); \
28
- print(f'Downloading MedSigLIP model with token present: {bool(token)}...'); \
29
  AutoProcessor.from_pretrained('google/medsiglip-448', token=token); \
30
  AutoModel.from_pretrained('google/medsiglip-448', token=token)"
31
 
32
- # Pre-download YOLO model
33
  RUN python -c "from ultralytics import YOLO; YOLO('yolov8n.pt')"
34
 
35
- # Copy application code
36
- COPY . .
37
 
38
- # Expose port (Cloud Run defaults to 8080, providing a fallback)
39
- ENV PORT=8080
40
- EXPOSE $PORT
41
 
42
- # Command to run (Using Shell form so it evaluates $PORT)
43
- CMD uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8080}
 
1
  FROM python:3.11-slim
2
 
 
 
3
  # Install system dependencies
4
  RUN apt-get update && apt-get install -y \
5
  curl \
 
11
  && apt-get install -y nodejs \
12
  && rm -rf /var/lib/apt/lists/*
13
 
14
+ # Set up a new user named "user" with user ID 1000 (Required by HF Spaces)
15
+ RUN useradd -m -u 1000 user
16
+ USER user
17
+ ENV HOME=/home/user \
18
+ PATH=/home/user/.local/bin:$PATH
19
 
20
+ WORKDIR $HOME/app
21
+
22
+ # Copy requirements and install
23
+ COPY --chown=user requirements.txt .
24
+ RUN pip install --no-cache-dir -r requirements.txt
25
 
26
+ # Securely mount the HF_TOKEN secret during build to pre-download the model
27
+ # You configure HF_TOKEN in the "Secrets" tab of your Hugging Face Space settings
28
+ RUN --mount=type=secret,id=HF_TOKEN,mode=0444,required=true \
29
+ python -c "from transformers import AutoProcessor, AutoModel; \
30
  import os; \
31
+ token = open('/run/secrets/HF_TOKEN').read().strip(); \
 
32
  AutoProcessor.from_pretrained('google/medsiglip-448', token=token); \
33
  AutoModel.from_pretrained('google/medsiglip-448', token=token)"
34
 
35
+ # Pre-download YOLO
36
  RUN python -c "from ultralytics import YOLO; YOLO('yolov8n.pt')"
37
 
38
+ # Copy the rest of the application code
39
+ COPY --chown=user . $HOME/app
40
 
41
+ # Hugging Face defaults to port 7860
42
+ ENV PORT=7860
43
+ EXPOSE 7860
44
 
45
+ CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT}"]
 
README.md CHANGED
@@ -14,7 +14,7 @@ A privacy-first, free, and easy-to-use dermatology scan app powered by latest AI
14
  - **Privacy**: All data is scoped to your browser session.
15
  - **Zero-Shot Dermatology Analysis**:
16
  - Uses **Google Health's MedSigLIP** (`google/medsiglip-448`) model for localized analysis.
17
- - Classifies images against a comprehensive set of **25+ dermatological conditions** relevant to EU medical practices.
18
  - **Rationale**: The label set focuses on high-mortality cancers (Melanoma), high-prevalence conditions (Eczema, Acne), and common differential diagnoses to aid in effective triage.
19
 
20
  ### Confidence & Interpretation Logic
@@ -38,11 +38,11 @@ The system is tuned to detect the following conditions based on EU referral guid
38
 
39
  | Category | Conditions | Rationale |
40
  | :--- | :--- | :--- |
41
- | **Malignant / Pre-malignant** | Melanoma, Basal Cell Carcinoma (BCC), Squamous Cell Carcinoma (SCC), Actinic Keratosis, Bowen's Disease, Dysplastic Nevus | Priority for early detection due to mortality risk (Melanoma) or high prevalence impacting healthcare resources (BCC/SCC). |
42
- | **Inflammatory** | Psoriasis, Atopic Dermatitis (Eczema), Acne Vulgaris, Rosacea, Urticaria, Lichen Planus, Hidradenitis Suppurativa | Represents the highest burden of disease on quality of life in the EU population. |
43
- | **Infectious** | Fungal Infections (Tinea), Herpes Zoster (Shingles), Impetigo, Warts, Molluscum Contagiosum | Frequent reasons for primary care visits; contagious nature requires accurate identification. |
44
- | **Benign / Differential** | Melanocytic Nevus, Seborrheic Keratosis, Dermatofibroma, Haemangioma, Epidermoid Cyst, Lipoma | Crucial for distinguishing from malignant lesions to reduce unnecessary anxiety and referrals. |
45
- | **Other** | Vitiligo, Alopecia Areata, Melasma | Common pigmentary and hair disorders affecting psychological well-being. |
46
 
47
  ## Privacy & Security
48
 
@@ -110,10 +110,19 @@ The project is designed to be developed inside a **Dev Container**. This ensures
110
  5. **Start Dev Container**:
111
  - Open the folder in VS Code.
112
  - When prompted, click **"Reopen in Container"** (or run standard command `Dev Containers: Reopen in Container`).
113
- - VS Code will build the container and install all dependencies defined in `requirements-dev.txt` and `package.json`.
 
 
 
 
 
 
 
 
114
 
115
  Inside the integrated terminal of VS Code (running in the container):
116
  ```bash
 
117
  npm install # If not run automatically
118
  uvicorn app.main:app --reload --host 0.0.0.0 --port 8080
119
  ```
@@ -141,16 +150,33 @@ docker run -p 8080:8080 -e HF_TOKEN=$HF_TOKEN medgemma-app
141
 
142
  ### 🧪 Running Tests
143
 
144
- We use `pytest` for unit tests and `playwright` for end-to-end tests.
145
 
146
  - **Unit Tests**:
 
147
  ```bash
148
  pytest tests/unit
149
  ```
150
 
151
- - **Integration/E2E Tests**:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  ```bash
153
- pytest tests/e2e
 
154
  ```
155
 
156
  - **JavaScript Unit Tests**:
 
14
  - **Privacy**: All data is scoped to your browser session.
15
  - **Zero-Shot Dermatology Analysis**:
16
  - Uses **Google Health's MedSigLIP** (`google/medsiglip-448`) model for localized analysis.
17
+ - Classifies images against a focused set of **12 dermatological conditions** relevant to EU medical practices.
18
  - **Rationale**: The label set focuses on high-mortality cancers (Melanoma), high-prevalence conditions (Eczema, Acne), and common differential diagnoses to aid in effective triage.
19
 
20
  ### Confidence & Interpretation Logic
 
38
 
39
  | Category | Conditions | Rationale |
40
  | :--- | :--- | :--- |
41
+ | **Malignant / Pre-malignant** | Melanoma, Basal Cell Carcinoma (BCC) | Priority for early detection due to mortality risk (Melanoma) or high prevalence impacting healthcare resources (BCC). |
42
+ | **Inflammatory** | Psoriasis, Atopic Dermatitis (Eczema), Acne Vulgaris, Rosacea | Represents the highest burden of disease on quality of life in the EU population. |
43
+ | **Infectious** | Herpes Zoster (Shingles), Warts, Molluscum Contagiosum | Contagious nature requires accurate identification, often with distinct morphologies. |
44
+ | **Benign / Differential** | Melanocytic Nevus, Seborrheic Keratosis | Crucial for distinguishing from malignant lesions to reduce unnecessary anxiety and referrals. |
45
+ | **Baseline** | Normal Skin | Provides a control basis for healthy skin. |
46
 
47
  ## Privacy & Security
48
 
 
110
  5. **Start Dev Container**:
111
  - Open the folder in VS Code.
112
  - When prompted, click **"Reopen in Container"** (or run standard command `Dev Containers: Reopen in Container`).
113
+ - VS Code will build the container and install all dependencies defined in `requirements-dev.txt` and `package.json`.
114
+
115
+ **CLI Alternative:**
116
+ If you cannot find the "Rebuild" option in the UI, you can force a rebuild of the environment from your local terminal:
117
+ ```bash
118
+ # Ensure HF_TOKEN is exported for the build
119
+ export HF_TOKEN=$(cat .env | grep HF_TOKEN | cut -d'=' -f2)
120
+ docker compose up -d --build
121
+ ```
122
 
123
  Inside the integrated terminal of VS Code (running in the container):
124
  ```bash
125
+ source venv/bin/activate # If using a local virtual environment
126
  npm install # If not run automatically
127
  uvicorn app.main:app --reload --host 0.0.0.0 --port 8080
128
  ```
 
150
 
151
  ### 🧪 Running Tests
152
 
153
+ We use `pytest` for Python tests and `Jest` for JavaScript unit tests.
154
 
155
  - **Unit Tests**:
156
+ Tests the core logic (image preprocessing, result interpretation, API endpoints) with external dependencies like AI models mocked for speed.
157
  ```bash
158
  pytest tests/unit
159
  ```
160
 
161
+ - **YOLO Tests**:
162
+ Specifically tests the lesion detection and cropping logic.
163
+ ```bash
164
+ pytest tests/yolo_tests
165
+ ```
166
+
167
+ - **Running All Backend Tests**:
168
+ You can run the core logic and YOLO tests together:
169
+ ```bash
170
+ pytest tests/unit tests/yolo_tests
171
+ ```
172
+
173
+ > **Note on YOLO State**: Currently, both unit and YOLO tests use **mocking** for the actual YOLOv8 inference. This allows the test suite to run in seconds without requiring model weights or high-performance hardware.
174
+
175
+ - **Integration & E2E Tests**:
176
+ These tests interact with a live server and require `playwright` for browser automation.
177
  ```bash
178
+ # Run UI and E2E tests
179
+ pytest tests/test_ui.py tests/test_e2e_local.py
180
  ```
181
 
182
  - **JavaScript Unit Tests**:
tests/test_e2e_local.py CHANGED
@@ -26,16 +26,26 @@ def test_local_upload_and_analysis_flow(page, dummy_image, test_server):
26
  page.on("console", lambda msg: print(f"Browser Console: [{msg.type}] {msg.text}"))
27
  page.goto(test_server)
28
 
 
 
 
 
 
 
 
 
 
 
29
  # 1. Clear any existing state
30
- clear_btn = page.locator("sl-button", has_text="Clear All").first
31
  if clear_btn.count() > 0:
32
  try:
33
  clear_btn.wait_for(state="visible", timeout=2000)
34
  page.once("dialog", lambda dialog: dialog.accept())
35
  clear_btn.click()
36
- page.locator("text=No photos yet").wait_for(state="visible", timeout=5000)
37
  except:
38
- print("Clear All button not visible or timed out, continuing...")
39
 
40
  # 2. Trigger "Upload" (Local load)
41
  # We expect TO NOT see an /upload network call
@@ -63,16 +73,16 @@ def test_local_upload_and_analysis_flow(page, dummy_image, test_server):
63
  print(f"Analysis successful: {data['predictions'][0]['label']}")
64
 
65
  # 4. Verify results display in UI
66
- # The 'Clinical Assessment' text should appear
67
- print("Waiting for Clinical Assessment UI element...")
68
- page.locator("text=Clinical Assessment").first.wait_for(state="visible", timeout=300000)
69
 
70
  # Verify the label is visible
71
  label_text = data["predictions"][0]["label"]
72
  page.locator(f"text={label_text}").first.wait_for(state="visible", timeout=30000)
73
 
74
  # Verify Saliency Map toggle is present
75
- saliency_toggle = page.locator("sl-details", has_text="View Grad-CAM Saliency Map").first
76
  saliency_toggle.wait_for(state="visible", timeout=10000)
77
 
78
  # 5. Trigger Lazy Saliency Load
@@ -99,12 +109,22 @@ def test_local_duplicate_handling(page, dummy_image, test_server):
99
  """Verifies that selecting the same file twice doesn't create duplicate timeline items."""
100
  page.goto(test_server)
101
 
 
 
 
 
 
 
 
 
 
 
102
  # 1. Clear state
103
- clear_btn = page.locator("sl-button", has_text="Clear All").first
104
  if clear_btn.is_visible():
105
  page.once("dialog", lambda dialog: dialog.accept())
106
  clear_btn.click()
107
- page.locator("text=No photos yet").wait_for(state="visible", timeout=5000)
108
 
109
  # 2. Load first time
110
  page.set_input_files("input[type='file']", dummy_image)
 
26
  page.on("console", lambda msg: print(f"Browser Console: [{msg.type}] {msg.text}"))
27
  page.goto(test_server)
28
 
29
+ # Handle Medical Disclaimer
30
+ print("Checking for Medical Disclaimer...")
31
+ try:
32
+ disclaimer_btn = page.locator("sl-button", has_text="I Understand & Agree").first
33
+ disclaimer_btn.wait_for(state="visible", timeout=5000)
34
+ disclaimer_btn.click()
35
+ print("Disclaimer accepted.")
36
+ except:
37
+ print("Disclaimer not visible or already accepted, continuing...")
38
+
39
  # 1. Clear any existing state
40
+ clear_btn = page.locator("sl-button", has_text="Clear History").first
41
  if clear_btn.count() > 0:
42
  try:
43
  clear_btn.wait_for(state="visible", timeout=2000)
44
  page.once("dialog", lambda dialog: dialog.accept())
45
  clear_btn.click()
46
+ page.locator("text=History Empty").wait_for(state="visible", timeout=5000)
47
  except:
48
+ print("Clear History button not visible or timed out, continuing...")
49
 
50
  # 2. Trigger "Upload" (Local load)
51
  # We expect TO NOT see an /upload network call
 
73
  print(f"Analysis successful: {data['predictions'][0]['label']}")
74
 
75
  # 4. Verify results display in UI
76
+ # The 'AI Scan result' text should appear
77
+ print("Waiting for AI Scan result UI element...")
78
+ page.locator("text=AI Scan result").first.wait_for(state="visible", timeout=300000)
79
 
80
  # Verify the label is visible
81
  label_text = data["predictions"][0]["label"]
82
  page.locator(f"text={label_text}").first.wait_for(state="visible", timeout=30000)
83
 
84
  # Verify Saliency Map toggle is present
85
+ saliency_toggle = page.locator("sl-details", has_text="View Saliency Map").first
86
  saliency_toggle.wait_for(state="visible", timeout=10000)
87
 
88
  # 5. Trigger Lazy Saliency Load
 
109
  """Verifies that selecting the same file twice doesn't create duplicate timeline items."""
110
  page.goto(test_server)
111
 
112
+ # Handle Medical Disclaimer
113
+ print("Checking for Medical Disclaimer...")
114
+ try:
115
+ disclaimer_btn = page.locator("sl-button", has_text="I Understand & Agree").first
116
+ disclaimer_btn.wait_for(state="visible", timeout=5000)
117
+ disclaimer_btn.click()
118
+ print("Disclaimer accepted.")
119
+ except:
120
+ print("Disclaimer not visible or already accepted, continuing...")
121
+
122
  # 1. Clear state
123
+ clear_btn = page.locator("sl-button", has_text="Clear History").first
124
  if clear_btn.is_visible():
125
  page.once("dialog", lambda dialog: dialog.accept())
126
  clear_btn.click()
127
+ page.locator("text=History Empty").wait_for(state="visible", timeout=5000)
128
 
129
  # 2. Load first time
130
  page.set_input_files("input[type='file']", dummy_image)
tests/test_inference_trigger.py CHANGED
@@ -22,12 +22,22 @@ def test_inference_starts_on_upload(page, dummy_image, test_server):
22
  """
23
  page.goto(test_server)
24
 
 
 
 
 
 
 
 
 
 
 
25
  # Clear session to ensure we are fresh
26
- clear_btn = page.locator("sl-button", has_text="Clear All").first
27
  if clear_btn.is_visible():
28
  page.once("dialog", lambda dialog: dialog.accept())
29
  clear_btn.click()
30
- page.locator("text=No photos yet").wait_for(state="visible", timeout=5000)
31
 
32
  # We expect a POST to /api/photos/*/analyze
33
  # The app.js calls it after a 300ms timeout
 
22
  """
23
  page.goto(test_server)
24
 
25
+ # Handle Medical Disclaimer
26
+ print("Checking for Medical Disclaimer...")
27
+ try:
28
+ disclaimer_btn = page.locator("sl-button", has_text="I Understand & Agree").first
29
+ disclaimer_btn.wait_for(state="visible", timeout=5000)
30
+ disclaimer_btn.click()
31
+ print("Disclaimer accepted.")
32
+ except:
33
+ print("Disclaimer not visible or already accepted, continuing...")
34
+
35
  # Clear session to ensure we are fresh
36
+ clear_btn = page.locator("sl-button", has_text="Clear History").first
37
  if clear_btn.is_visible():
38
  page.once("dialog", lambda dialog: dialog.accept())
39
  clear_btn.click()
40
+ page.locator("text=History Empty").wait_for(state="visible", timeout=5000)
41
 
42
  # We expect a POST to /api/photos/*/analyze
43
  # The app.js calls it after a 300ms timeout
tests/test_ui.py CHANGED
@@ -26,6 +26,16 @@ def test_duplicate_upload_shows_warning_context(page, dummy_image, test_server):
26
  page.on("console", lambda msg: print(f"Browser Console: {msg.text}"))
27
  page.goto(test_server)
28
 
 
 
 
 
 
 
 
 
 
 
29
  # Check if we need to clear previous state
30
  # We use a locator for the button
31
  clear_btn = page.locator("sl-button", has_text="Clear All").first
 
26
  page.on("console", lambda msg: print(f"Browser Console: {msg.text}"))
27
  page.goto(test_server)
28
 
29
+ # Handle Medical Disclaimer
30
+ print("Checking for Medical Disclaimer...")
31
+ try:
32
+ disclaimer_btn = page.locator("sl-button", has_text="I Understand & Agree").first
33
+ disclaimer_btn.wait_for(state="visible", timeout=5000)
34
+ disclaimer_btn.click()
35
+ print("Disclaimer accepted.")
36
+ except:
37
+ print("Disclaimer not visible or already accepted, continuing...")
38
+
39
  # Check if we need to clear previous state
40
  # We use a locator for the button
41
  clear_btn = page.locator("sl-button", has_text="Clear All").first