Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,11 @@ ssh visitor@bdmap1.wse.jhu.edu
```

#### 1. Pull latest changes
Production must always deploy from `main`. Confirm the branch first, then pull.
```
cd /home/visitor/PanTS-Viewer
git fetch
git checkout main
git pull
```

Expand All @@ -72,8 +74,9 @@ cd /home/visitor/PanTS-Viewer/PanTS-Demo && npm ci && npm run build

#### 3. Restart the backend
```
# Kill the old gunicorn process
kill $(pgrep -f "gunicorn.*app:app")
# Stop the old gunicorn process and wait for the port to free
pkill -f "gunicorn.*app:app"; sleep 2
pgrep -f "gunicorn.*app:app" && echo "still running - rerun the line above" || echo "port clear"

# Start a new gunicorn process
nohup /home/visitor/.conda/envs/PanTS_backend/bin/gunicorn \
Expand All @@ -85,8 +88,13 @@ echo "PID: $!"
```

#### 4. Verify the backend is running
Give it a few seconds to load, then check the backend booted, the dataset loads, and masks serve (all three must succeed).
```
sleep 3 && curl http://127.0.0.1:8000/api/ping
sleep 8
curl http://127.0.0.1:8000/api/ping
curl -s "http://127.0.0.1:8000/api/search?limit=1" | head -c 120; echo
curl -s -o /dev/null -w "segmentations: %{http_code}\n" "http://127.0.0.1:8000/api/get-segmentations/17.nii.gz"
```
Expect `{"message":"pong"}`, a JSON object with `items`, and `segmentations: 200`. If the backend fails to boot, check the log for the traceback.

Logs are written to `/tmp/gunicorn.log`.
15 changes: 11 additions & 4 deletions flask-server/api/api_blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -760,10 +760,17 @@ async def get_segmentations(combined_labels_id):
try:
serve_path = nifti_path
if img.get_data_dtype() != np.uint8:
# The source is a float label map; Cornerstone needs uint8. Write the
# converted copy to a sibling file and serve THAT — never overwrite the
# original ground-truth mask on disk (an HTTP GET must not mutate data).
converted_path = nifti_path.replace('.nii.gz', '_uint8.nii.gz')
# The source is a float label map; Cornerstone needs uint8. Cache the
# converted copy in a WRITABLE temp dir and serve THAT — never write
# into the dataset's mask_only/ (it is read-only on the server, and an
# HTTP GET must not mutate ground-truth data). Writing the sibling into
# mask_only/ 500'd the segmentation endpoint in production.
cache_dir = "/tmp/pants_uint8"
os.makedirs(cache_dir, exist_ok=True)
converted_path = os.path.join(
cache_dir,
f"{get_panTS_id(secure_filename(combined_labels_id))}_combined_labels_uint8.nii.gz",
)
if not os.path.exists(converted_path):
print("⚠️ Detected float label map, converting to uint8 for Cornerstone compatibility...")
raw = np.asanyarray(img.dataobj)
Expand Down
40 changes: 40 additions & 0 deletions flask-server/services/segmentation_metrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""
services/segmentation_metrics.py

Per-session segmentation metrics for the AI assistant when a user is viewing
an *uploaded* (session) case rather than a numeric dataset case.

This module is intentionally defensive. Its only caller
(``api_blueprint._ai_load_metrics``) treats a returned dict that contains an
``"error"`` key as "no server metrics available" and falls back to
frontend-supplied metrics. Therefore this function must NEVER raise and must
always return a dict: a missing/undecodable session mask has to degrade
gracefully to the frontend metrics instead of breaking AI chat -- or, as we
learned the hard way, breaking the whole backend at import time when this
module was referenced but never committed.

Server-side computation of per-organ metrics for uploaded sessions is not
implemented in this build, so we return a clean "unavailable" result. The
public shape mirrors ``get_mask_data_internal`` ({"organ_metrics": [...]}).
"""

from __future__ import annotations


def calculate_session_metrics(session_id, sessions_dir_name=None):
"""Best-effort per-organ metrics for an uploaded session case.

Returns a dict shaped like ``get_mask_data_internal`` on success, or
``{"error": ...}`` when the session's mask cannot be located/computed.
Never raises.
"""
try:
identifier = str(session_id or "").strip()
if not identifier:
return {"error": "no session id", "organ_metrics": []}

# Session masks are not computed server-side in this build; signal
# "unavailable" so the caller falls back to frontend-supplied metrics.
return {"error": "session metrics unavailable", "organ_metrics": []}
except Exception as error: # never let this break the import or the caller
return {"error": f"{type(error).__name__}: {error}", "organ_metrics": []}
Loading