"""HuggingFace Spaces entry point for the Gradio demo (docs/04_ROADMAP.md, Phase 9). Not used for local development -- `scripts/demo.py` is the local launcher, with a required `--checkpoint` flag pointing anywhere on disk. A Space has no such flag: HF Spaces runs `python app.py` with no arguments, so this file fetches the checkpoints from the separate model repo (huggingface.co/adarshcod30/drdetect-dr-screening -- research-use-only, see MODEL_CARD.md there) via `hf_hub_download` instead of bundling them into this Space's own repo. `hf_hub_download` caches each file locally after its first fetch, so a Space restart does not re-download them. Serves the 5-fold CNN ensemble (`ensemble_fold0.ckpt`..`ensemble_fold4.ckpt`), not the single `best.ckpt` this Space originally shipped -- the ensemble beat the single checkpoint on the DDR external test set's referable AUC (0.899 vs 0.891, DeLong p=0.038, see project memory from the GPU experimentation phase), so it is the better model to actually put in front of a visitor. `best.ckpt` and its ONNX export stay published as the documented, individually-benchmarked release artifact (MODEL_CARD.md); the ensemble is 5 of that same training recipe's cross-validation folds, run together, which is why five ~46MB files replace the one ~46MB file below rather than a differently-sized model. This project's model is CPU-only by design (see `drdetect.serve.pipeline.load_grader`'s own docstring: "a district screening kiosk is not assumed to have [a GPU]"), but the Space is hosted on HF's free ZeroGPU hardware tier (free accounts can no longer create free CPU-basic Gradio Spaces). Two ZeroGPU-specific fixes were needed, found by reading this Space's own runtime error, not guessed: 1. `import spaces` early -- without it, HF's supervisor stops the app shortly after a clean startup ("Stopping Node.js server..." with no other error visible in the application log). 2. **`_zerogpu_registration` below**: even with (1), the platform's own `get_space_runtime` reported the real reason plainly -- `"No @spaces.GPU function detected during startup"` -- ZeroGPU spaces require at least one such function to exist, whether or not it is ever called. This function is never invoked by the demo; grading always runs on CPU exactly as the rest of this project does. It exists solely to satisfy that platform check without changing this project's actual CPU-only inference path. 3. **`device="cpu"` passed explicitly to `build_interface` below**: its default auto-detection (`drdetect.serve.demo`) checks `torch.cuda.is_available()`, which ZeroGPU makes report `True` even outside an actual `@spaces.GPU` grant -- auto-detection then picked "cuda" and crashed the first real request with `RuntimeError: Low-level CUDA init... reached` the moment a tensor was moved to it, since nothing in the demo path is wrapped in `@spaces.GPU`. Forcing "cpu" here matches this project's actual design (CPU-only inference throughout) rather than trusting a detection heuristic that ZeroGPU's emulation invalidates. Both are wrapped so this file still runs unmodified outside a Space (`spaces` is not, and should not be, a dependency of the main `drdetect` package). """ from __future__ import annotations import sys from pathlib import Path try: import spaces @spaces.GPU def _zerogpu_registration() -> None: """Never called -- exists only so ZeroGPU's startup check finds a decorated function. Real inference always runs on CPU (see module docstring point 2).""" return None except ImportError: pass sys.path.insert(0, str(Path(__file__).resolve().parent / "src")) MODEL_REPO = "adarshcod30/drdetect-dr-screening" if __name__ == "__main__": from huggingface_hub import hf_hub_download checkpoints = [ hf_hub_download(repo_id=MODEL_REPO, filename=f"ensemble_fold{i}.ckpt") for i in range(5) ] from drdetect.serve.demo import build_interface # regression loss, not ce: it won the locked external evaluation decisively # (docs/22_PHASE8_VALIDATION_RESULTS.md, referable AUC 0.924 vs 0.888, DeLong p=6.1e-10) demo = build_interface( checkpoints, backbone="efficientnet_b0", loss_name="regression", size=512, device="cpu" ) demo.launch(ssr_mode=False)