Download backend/vastai/handler.py from scomb2/Radar: direct link, hf CLI and curl.
- Browser
- Download file 6.48 kB
-
https://huggingface.co/spaces/scomb2/Radar/resolve/main/backend/vastai/handler.py
- Command line
-
hf download hf://spaces/scomb2/Radar/backend/vastai/handler.py
-
curl -L -o handler.py https://huggingface.co/spaces/scomb2/Radar/resolve/main/backend/vastai/handler.py
6.48 kB
| """Vast.ai Serverless handler: same intake + vendored chain, FastAPI surface. | |
| Not wired to traffic (INFERENCE_BACKEND stays zerogpu). When activation is | |
| scheduled: build the image below, create the Serverless endpoint from | |
| vast-config.json, then flip Pages envs — frontend needs zero code change. | |
| Response shape matches frontend/src/lib/radar.ts vastai branch: | |
| {"scores": {finding: score}, "rows": [{finding, score}]} sorted desc. | |
| Error strings mirror intake.py/app.py; surfaced as HTTP 400 {"detail": msg}. | |
| """ | |
| import os | |
| import re | |
| import shutil | |
| import sys | |
| import tempfile | |
| from contextlib import asynccontextmanager | |
| from fastapi import FastAPI, HTTPException, UploadFile # noqa: E402 | |
| import gradio as gr # noqa: E402 — reused for intake's error literals | |
| from huggingface_hub import snapshot_download # noqa: E402 | |
| HERE = os.path.dirname(os.path.abspath(__file__)) | |
| def _vendor_path() -> str: | |
| """Locate vendored RADAR_inference in repo checkout or image layout (/srv).""" | |
| ancestors = [HERE] | |
| parent = HERE | |
| for _ in range(3): | |
| parent = os.path.dirname(parent) | |
| ancestors.append(parent) | |
| for base in ancestors + ["/srv", os.getcwd()]: | |
| if os.path.isdir(os.path.join(base, "RADAR_inference")): | |
| return os.path.join(base, "RADAR_inference") | |
| return os.path.join(HERE, "..", "..", "..", "RADAR_inference") | |
| sys.path.insert(0, _vendor_path()) | |
| from intake import ( # noqa: E402 | |
| HEADER_SUFFIXES, | |
| VOLUME_SUFFIXES, | |
| _series_to_nifti, | |
| _stage_uploads, | |
| _to_nifti, | |
| ) | |
| from inference_demo import evaluate, initialize # noqa: E402 | |
| REPO_ID = "radar-generalist/RADAR" | |
| CHECKPOINT_NAME = "checkpoint_radar_pretrain.pth" | |
| _state: dict = {} | |
| def _english_name(column: str) -> str: | |
| # Same mapping as app.py; duplicated (not imported) so this service never | |
| # imports the Space module, whose import has CUDA/GPU side effects. | |
| m = re.search(r"\((.+)\)\s*$", column) | |
| return m.group(1) if m else column | |
| def _ensure_ckpts() -> None: | |
| # Mirrors app.py _ensure_ckpts patterns: checkpoint + tokenizer both land | |
| # in MODEL_ROOT. infer_text_embedding_radar.pt ships in ckpt/ (git LFS — | |
| # run `git lfs pull` before docker build or it bakes the pointer). | |
| model_root = os.getenv("MODEL_ROOT", "/srv/ckpt") | |
| snapshot_download( | |
| REPO_ID, | |
| allow_patterns=[CHECKPOINT_NAME, "bert-base-chinese/*"], | |
| local_dir=model_root, | |
| ) | |
| async def lifespan(app: FastAPI): | |
| _ensure_ckpts() | |
| pad_func, model = initialize() # once; per-request work stays in /score | |
| _state["pad_func"] = pad_func | |
| _state["model"] = model | |
| yield | |
| _state.clear() | |
| app = FastAPI(title="RADAR Vast.ai scoring", lifespan=lifespan) | |
| async def score(ct_files: list[UploadFile]): | |
| """Stage uploads (intake.py) then run the vendored chain once.""" | |
| tmpdir = tempfile.mkdtemp(prefix="radar_case_") | |
| outdir = tempfile.mkdtemp(prefix="radar_out_") | |
| incoming = tempfile.mkdtemp(prefix="radar_incoming_") | |
| staged = tempfile.mkdtemp(prefix="radar_dcm_") | |
| try: | |
| try: | |
| # Raw uploads land in incoming/; _stage_uploads alone populates | |
| # staged/ (it also sanitizes names and expands zips). | |
| paths: list[str] = [] | |
| for i, upload in enumerate(ct_files or []): | |
| raw = ((upload.filename or "").strip() or f"upload_{i}").replace("\\", "/") | |
| safe = os.path.basename(raw) or f"upload_{i}" | |
| dest = os.path.join(incoming, f"{i}_{safe}") | |
| with open(dest, "wb") as dst: | |
| dst.write(await upload.read()) | |
| paths.append(dest) | |
| singles = [p for p in paths if p.lower().endswith(VOLUME_SUFFIXES)] | |
| rest = [p for p in paths if p not in singles] | |
| if singles and rest: | |
| raise gr.Error("upload either one volume file or one DICOM series, not both") | |
| if len(singles) > 1: | |
| raise gr.Error("upload a single volume file") | |
| if singles: | |
| src = _to_nifti(singles[0], tmpdir) | |
| fname = os.path.basename(src) | |
| if src != os.path.join(tmpdir, fname): | |
| if not fname.endswith((".nii", ".nii.gz")): | |
| fname += ".nii.gz" | |
| shutil.copy(src, os.path.join(tmpdir, fname)) | |
| else: | |
| if len(paths) == 1 and paths[0].lower().endswith(".dcm"): | |
| raise gr.Error( | |
| "a single .dcm is one slice, not a volume: upload the full series" | |
| ) | |
| headers = [p for p in paths if p.lower().endswith(HEADER_SUFFIXES)] | |
| if headers: | |
| raise gr.Error( | |
| f"{os.path.basename(headers[0])} needs its raw pair: " | |
| "convert to .nii.gz/.nrrd/.mha first" | |
| ) | |
| _stage_uploads(paths, staged) | |
| _series_to_nifti(staged, tmpdir) | |
| import pandas as pd | |
| try: | |
| evaluate(_state["pad_func"], _state["model"], tmpdir, outdir, "vast") | |
| except (OSError, ValueError, RuntimeError) as exc: | |
| raise gr.Error(f"could not process this volume: {exc}") | |
| import glob | |
| matches = glob.glob(os.path.join(outdir, "RADAR_infer_results_vast.csv")) | |
| if not matches: | |
| raise gr.Error("model skipped this volume (check dimensions/spacing)") | |
| df = pd.read_csv(matches[0], encoding="utf-8-sig") | |
| if df.empty: | |
| raise gr.Error("model skipped this volume (check dimensions/spacing)") | |
| row = df.iloc[0] | |
| scores = { | |
| _english_name(col): float(row[col]) | |
| for col in df.columns[1:] | |
| if pd.notna(row[col]) and str(row[col]).strip() != "" | |
| } | |
| rows = [ | |
| {"finding": k, "score": v} | |
| for k, v in sorted(scores.items(), key=lambda kv: kv[1], reverse=True) | |
| ] | |
| return {"scores": scores, "rows": rows} | |
| except gr.Error as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) | |
| finally: | |
| shutil.rmtree(tmpdir, ignore_errors=True) | |
| shutil.rmtree(outdir, ignore_errors=True) | |
| shutil.rmtree(incoming, ignore_errors=True) | |
| shutil.rmtree(staged, ignore_errors=True) | |