Lnadeem commited on
Commit
26d565d
·
verified ·
1 Parent(s): 53b6de2

Upload 13 files

Browse files
Files changed (7) hide show
  1. HF_V6_2_CHECKLIST.md +25 -0
  2. README.md +4 -6
  3. SHA256SUMS.txt +13 -0
  4. app.py +25 -1
  5. database.py +27 -28
  6. janus_engine.py +12 -2
  7. requirements.txt +9 -11
HF_V6_2_CHECKLIST.md ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # V6.2 deployment checklist
2
+
3
+ 1. Upload all files to the root of the Gradio Space. Do not keep them inside a subfolder.
4
+ 2. Upload `best_efficientnet.pth` separately (the ZIP intentionally does not contain the model weights).
5
+ 3. Use ZeroGPU hardware.
6
+ 4. Keep these secrets:
7
+ - `SUPABASE_URL`
8
+ - `SUPABASE_SERVICE_ROLE_KEY`
9
+ 5. Variables:
10
+ - `SUPABASE_STORAGE_BUCKET=xray-images`
11
+ - `SUPABASE_STORAGE_PUBLIC=true`
12
+ - `JANUS_ENABLED=true`
13
+ - `JANUS_BILINGUAL=true`
14
+ - `JANUS_MODEL_ID=ZrH42/Janus-Pro-CXR-Final`
15
+ - `MODEL_PATH=/home/oai/share/best_efficientnet.pth` only if that is the actual path; otherwise omit it because the default is the Space root.
16
+ 6. In Supabase, create/verify the `xray-images` bucket once. The server no longer tries to create it during inference.
17
+ 7. The server stores Janus/integrity/safety inside `medical_report_json`; it does not send non-existent columns to `analyses`.
18
+ 8. After build, open `/` and test `/health`, then `/gpu_health`, then Analyze.
19
+ 9. A successful AI test should show:
20
+ - `janus.enabled: true`
21
+ - non-empty `janus.raw_text`
22
+ - English report source from Janus
23
+ - Arabic verified or explicitly marked for manual review
24
+ - `database.saved: true`
25
+ - `database.storage_saved: true`
README.md CHANGED
@@ -1,15 +1,13 @@
1
  ---
2
- title: FeatureX Clinical AI - ZeroGPU API
3
- author: Laith Nadeem Alabsi
4
  emoji: 🩻
5
  colorFrom: blue
6
  colorTo: indigo
7
  sdk: gradio
8
- sdk_version: 6.27.0
9
- python_version: "3.12.12"
10
  app_file: app.py
11
- preload_from_hub:
12
- - ZrH42/Janus-Pro-CXR-Final
13
  ---
14
 
15
  # FeatureX Clinical AI — Hugging Face ZeroGPU
 
1
  ---
2
+ title: FeatureX Clinical AI
 
3
  emoji: 🩻
4
  colorFrom: blue
5
  colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: 5.29.1
8
+ python_version: "3.12"
9
  app_file: app.py
10
+ suggested_hardware: cpu-basic
 
11
  ---
12
 
13
  # FeatureX Clinical AI — Hugging Face ZeroGPU
SHA256SUMS.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 2c7ee9e77fa13e08f3e62a64fb400d3d7eeb1cacfc409482b6940d94c386da71 DEPLOY_V6_GRADIO6.md
2
+ 5ff6aa4fba6c3b7474f32d9fcefdf060022cf03d7c025fbeb1fb08f39d3783ab HF_V6_2_CHECKLIST.md
3
+ e320901dec53dbb74dd0ffbbb631a98c036e9dab80f99e110c23e2da7f4c74cd README.md
4
+ d7632fa87691d1fbb05c64a58a31c2dcdddcbad7239f8b1136f4dc5e3b7c5bfb app.py
5
+ bb2a915c4e372a456d039dc7ec7350174b5032252ef610f17e8a602bc88976c6 arabic_translation.py
6
+ 82800f6795317e1d41a11c40100cc0e8aa2d4a73cf4ba4c99b73251111601882 database.py
7
+ d5a0ea93b03a52eeb8fa12cc421be19626b0e1e5f3d5d56c67028ca56da0a344 env.example
8
+ 3b27bb81bf2b11b7d4b266b86a4426ec3469af0f3235df0eab1ae8859fd0f98d flask_app_reference.py
9
+ acb0d739947c5aeeff0d91c7add3d041f40188a5ea4a82994b978bfd9621f91e inference.py
10
+ 2d76e40f526f982847334fbf71824a238ccabb704098933f6ddb32d1c90ee762 janus_engine.py
11
+ fb3695147bc149917ab8264f4b0d6adc5115f1758801fc25db5b5a44f8f0e97e requirements.txt
12
+ b03844e23a2073511d00b05ff42045859e9d65fbe505b643dd7e31f148986410 supabase_002_bilingual_analyses.sql
13
+ f7bec1683c145d65f07d4f30040ab58e29b7ff32fd16b8c4b95c4e0d790f0dfe test_gradio_api.py
app.py CHANGED
@@ -21,9 +21,30 @@ logging.basicConfig(
21
  )
22
  logger = logging.getLogger("featurex-zerogpu-server")
23
 
24
- SERVER_VERSION = "3.1.0-hf-zerogpu-gradio6"
25
  MAX_UPLOAD = 15 * 1024 * 1024
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  def _json(payload: Any) -> str:
29
  return json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
@@ -302,6 +323,7 @@ def gpu_health() -> str:
302
  "server_version": SERVER_VERSION,
303
  "transport": "Gradio Server + ZeroGPU",
304
  "database": database.config_info(),
 
305
  "api": {"analyze": True, "health": True, "classes": True},
306
  }
307
  )
@@ -316,6 +338,7 @@ def health() -> str:
316
  "transport": "Gradio Server + ZeroGPU",
317
  "zero_gpu": True,
318
  "database": database.config_info(),
 
319
  }
320
  )
321
  return _json(info)
@@ -349,6 +372,7 @@ def _health_endpoint() -> str:
349
  "transport": "Gradio Blocks + ZeroGPU",
350
  "zero_gpu": True,
351
  "database": database.config_info(),
 
352
  "api": {"analyze": True, "health": True, "gpu_health": True, "classes": True},
353
  }
354
  )
 
21
  )
22
  logger = logging.getLogger("featurex-zerogpu-server")
23
 
24
+ SERVER_VERSION = "3.2.0-hf-zerogpu-v6.2-janus49"
25
  MAX_UPLOAD = 15 * 1024 * 1024
26
 
27
+ def _runtime_info():
28
+ info = {}
29
+ try:
30
+ import torch
31
+ info["torch"] = torch.__version__
32
+ info["cuda_available"] = bool(torch.cuda.is_available())
33
+ info["cuda_device"] = torch.cuda.get_device_name(0) if torch.cuda.is_available() else None
34
+ except Exception as exc:
35
+ info["torch_error"] = str(exc)
36
+ try:
37
+ import transformers
38
+ info["transformers"] = transformers.__version__
39
+ except Exception as exc:
40
+ info["transformers_error"] = str(exc)
41
+ info["janus_enabled"] = os.environ.get("JANUS_ENABLED", "true")
42
+ info["janus_model_id"] = os.environ.get("JANUS_MODEL_ID", "ZrH42/Janus-Pro-CXR-Final")
43
+ info["model_path"] = os.environ.get("MODEL_PATH", os.path.join(os.path.dirname(os.path.abspath(__file__)), "best_efficientnet.pth"))
44
+ info["model_file_exists"] = os.path.isfile(info["model_path"])
45
+ return info
46
+
47
+
48
 
49
  def _json(payload: Any) -> str:
50
  return json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
 
323
  "server_version": SERVER_VERSION,
324
  "transport": "Gradio Server + ZeroGPU",
325
  "database": database.config_info(),
326
+ "runtime": _runtime_info(),
327
  "api": {"analyze": True, "health": True, "classes": True},
328
  }
329
  )
 
338
  "transport": "Gradio Server + ZeroGPU",
339
  "zero_gpu": True,
340
  "database": database.config_info(),
341
+ "runtime": _runtime_info(),
342
  }
343
  )
344
  return _json(info)
 
372
  "transport": "Gradio Blocks + ZeroGPU",
373
  "zero_gpu": True,
374
  "database": database.config_info(),
375
+ "runtime": _runtime_info(),
376
  "api": {"analyze": True, "health": True, "gpu_health": True, "classes": True},
377
  }
378
  )
database.py CHANGED
@@ -15,7 +15,10 @@ logger = logging.getLogger("clinical-ai-db")
15
 
16
  SUPABASE_URL = os.environ.get("SUPABASE_URL", "").rstrip("/")
17
  SUPABASE_SERVICE_ROLE_KEY = os.environ.get("SUPABASE_SERVICE_ROLE_KEY", "")
18
- SUPABASE_BUCKET = os.environ.get("SUPABASE_STORAGE_BUCKET", "xray-images")
 
 
 
19
  PUBLIC_STORAGE = (
20
  os.environ.get("SUPABASE_STORAGE_PUBLIC", "true").strip().lower() == "true"
21
  )
@@ -41,35 +44,33 @@ def config_info() -> Dict[str, Any]:
41
 
42
 
43
  def _ensure_bucket() -> None:
44
- """Ensure the configured Storage bucket exists when using the trusted server key."""
 
 
 
 
 
 
 
45
  if not enabled():
46
  raise RuntimeError("Supabase is not configured on the server.")
47
- url = f"{SUPABASE_URL}/storage/v1/bucket"
 
48
  try:
49
- response = requests.post(
50
  url,
51
  headers=_headers(),
52
- json={"id": SUPABASE_BUCKET, "name": SUPABASE_BUCKET, "public": PUBLIC_STORAGE},
53
  timeout=REQUEST_TIMEOUT,
54
  )
55
- if response.status_code in (200, 201, 409):
56
- return
57
- # Some projects already have the bucket but reject create; verify it exists.
58
- if response.status_code >= 300:
59
- check = requests.get(
60
- f"{url}/{SUPABASE_BUCKET}",
61
- headers=_headers(),
62
- timeout=REQUEST_TIMEOUT,
63
- )
64
- if check.status_code == 200:
65
- return
66
- raise RuntimeError(
67
- f"Storage bucket '{SUPABASE_BUCKET}' is unavailable: "
68
- f"{response.status_code} {response.text[:800]}"
69
- )
70
  except requests.RequestException as exc:
71
  raise RuntimeError(f"Storage bucket check failed: {exc}") from exc
72
 
 
 
 
 
 
 
73
 
74
  def _headers(content_type: str = "application/json") -> Dict[str, str]:
75
  if not enabled():
@@ -422,17 +423,16 @@ def save_analysis(
422
  "threshold_url": visual_urls.get("threshold"),
423
  "comparison_url": visual_urls.get("comparison"),
424
  "medical_report_json": json.dumps(
425
- selected_report,
 
 
 
 
 
426
  ensure_ascii=False,
427
  ),
428
  "report_ar": reports_ar,
429
  "report_en": reports_en,
430
- "janus_raw_text": (result.get("janus") or {}).get("raw_text") or selected_report.get("janus_raw_text") or "",
431
- "janus_model": (result.get("janus") or {}).get("model") or selected_report.get("report_model") or "",
432
- "report_source": selected_report.get("report_source") or "",
433
- "translation_methods": (result.get("report_integrity") or {}).get("translation_methods") or [],
434
- "report_integrity": result.get("report_integrity") or {},
435
- "safety": result.get("safety") or {},
436
  "patient_data": _localized_patient(patient),
437
  "predictions_data": predictions,
438
  "quality_metrics": quality_metrics,
@@ -440,7 +440,6 @@ def save_analysis(
440
  "model_info": model_info,
441
  "visualization_urls": visual_urls,
442
  "language_requested": requested_language,
443
- "language": requested_language,
444
  "original_filename": original_filename,
445
  "image_sha256": image_sha256,
446
  "server_version": model_info.get("version", "unknown"),
 
15
 
16
  SUPABASE_URL = os.environ.get("SUPABASE_URL", "").rstrip("/")
17
  SUPABASE_SERVICE_ROLE_KEY = os.environ.get("SUPABASE_SERVICE_ROLE_KEY", "")
18
+ _RAW_BUCKET = os.environ.get("SUPABASE_STORAGE_BUCKET", "xray-images").strip()
19
+ # The deployed SQL and successful Kaggle server use the hyphenated bucket.
20
+ # Accept the old xray_images spelling to avoid breaking an existing secret.
21
+ SUPABASE_BUCKET = "xray-images" if _RAW_BUCKET == "xray_images" else (_RAW_BUCKET or "xray-images")
22
  PUBLIC_STORAGE = (
23
  os.environ.get("SUPABASE_STORAGE_PUBLIC", "true").strip().lower() == "true"
24
  )
 
44
 
45
 
46
  def _ensure_bucket() -> None:
47
+ """Verify that the configured Storage bucket already exists.
48
+
49
+ The previous HF build tried to create the bucket on every request. On
50
+ Supabase projects with Storage RLS this can fail with:
51
+ "new row violates row-level security policy".
52
+ Bucket administration belongs in Supabase SQL/dashboard; the server only
53
+ verifies the bucket and uploads with the service-role key.
54
+ """
55
  if not enabled():
56
  raise RuntimeError("Supabase is not configured on the server.")
57
+
58
+ url = f"{SUPABASE_URL}/storage/v1/bucket/{SUPABASE_BUCKET}"
59
  try:
60
+ response = requests.get(
61
  url,
62
  headers=_headers(),
 
63
  timeout=REQUEST_TIMEOUT,
64
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  except requests.RequestException as exc:
66
  raise RuntimeError(f"Storage bucket check failed: {exc}") from exc
67
 
68
+ if response.status_code != 200:
69
+ body = _safe_response_text(response, 800)
70
+ raise RuntimeError(
71
+ f"Storage bucket '{SUPABASE_BUCKET}' is unavailable: "
72
+ f"{response.status_code} {body}"
73
+ )
74
 
75
  def _headers(content_type: str = "application/json") -> Dict[str, str]:
76
  if not enabled():
 
423
  "threshold_url": visual_urls.get("threshold"),
424
  "comparison_url": visual_urls.get("comparison"),
425
  "medical_report_json": json.dumps(
426
+ {
427
+ **selected_report,
428
+ "report_integrity": result.get("report_integrity") or {},
429
+ "janus": result.get("janus") or {},
430
+ "safety": result.get("safety") or {},
431
+ },
432
  ensure_ascii=False,
433
  ),
434
  "report_ar": reports_ar,
435
  "report_en": reports_en,
 
 
 
 
 
 
436
  "patient_data": _localized_patient(patient),
437
  "predictions_data": predictions,
438
  "quality_metrics": quality_metrics,
 
440
  "model_info": model_info,
441
  "visualization_urls": visual_urls,
442
  "language_requested": requested_language,
 
443
  "original_filename": original_filename,
444
  "image_sha256": image_sha256,
445
  "server_version": model_info.get("version", "unknown"),
janus_engine.py CHANGED
@@ -10,13 +10,22 @@ from typing import Dict, List
10
 
11
  import torch
12
 
 
 
 
 
 
 
 
 
 
13
  logger = logging.getLogger("featurex-janus")
14
 
15
  MODEL_ID = os.environ.get("JANUS_MODEL_ID", "ZrH42/Janus-Pro-CXR-Final")
16
  REPO_URL = "https://github.com/ZrH42/Janus-Pro-CXR.git"
17
  REPO_DIR = Path(os.environ.get("JANUS_REPO_DIR", "/tmp/Janus-Pro-CXR"))
18
  JANUS_ENABLED = os.environ.get("JANUS_ENABLED", "true").strip().lower() in {"1", "true", "yes", "on"}
19
- BILINGUAL = os.environ.get("JANUS_BILINGUAL", "true").strip().lower() in {"1", "true", "yes", "on"}
20
 
21
  _PROCESSOR = None
22
  _MODEL = None
@@ -59,11 +68,12 @@ def _initialize():
59
  global _PROCESSOR, _MODEL, _INITIALIZED
60
  if _INITIALIZED:
61
  return
 
62
  if not JANUS_ENABLED:
63
  _INITIALIZED = True
64
  return
65
  if not torch.cuda.is_available():
66
- raise RuntimeError("Janus requires CUDA on the Kaggle GPU server.")
67
 
68
  _clone_repo()
69
  _patch_compatibility()
 
10
 
11
  import torch
12
 
13
+ def _log_runtime_versions():
14
+ try:
15
+ import transformers
16
+ logger.info("Transformers version: %s", transformers.__version__)
17
+ except Exception as exc:
18
+ logger.warning("Could not read Transformers version: %s", exc)
19
+ logger.info("Torch version: %s | CUDA available: %s", torch.__version__, torch.cuda.is_available())
20
+
21
+
22
  logger = logging.getLogger("featurex-janus")
23
 
24
  MODEL_ID = os.environ.get("JANUS_MODEL_ID", "ZrH42/Janus-Pro-CXR-Final")
25
  REPO_URL = "https://github.com/ZrH42/Janus-Pro-CXR.git"
26
  REPO_DIR = Path(os.environ.get("JANUS_REPO_DIR", "/tmp/Janus-Pro-CXR"))
27
  JANUS_ENABLED = os.environ.get("JANUS_ENABLED", "true").strip().lower() in {"1", "true", "yes", "on"}
28
+ BILINGUAL = os.environ.get("JANUS_BILINGUAL", "false").strip().lower() in {"1", "true", "yes", "on"}
29
 
30
  _PROCESSOR = None
31
  _MODEL = None
 
68
  global _PROCESSOR, _MODEL, _INITIALIZED
69
  if _INITIALIZED:
70
  return
71
+ _log_runtime_versions()
72
  if not JANUS_ENABLED:
73
  _INITIALIZED = True
74
  return
75
  if not torch.cuda.is_available():
76
+ raise RuntimeError("Janus requires CUDA/ZeroGPU. The analyze function must execute on a ZeroGPU Space.")
77
 
78
  _clone_repo()
79
  _patch_compatibility()
requirements.txt CHANGED
@@ -1,21 +1,19 @@
1
- # Hugging Face ZeroGPU provides the platform-managed Gradio, spaces and
2
- # huggingface-hub packages. Do not pin those packages here.
3
- #
4
- # The current HF Gradio 6 runtime requires huggingface-hub 1.x, so the old
5
- # Transformers 4.49.0 pin is intentionally removed. Transformers 5.4.0 is
6
- # used here as the compatibility bridge for the current Hub/Gradio stack.
7
- # Janus-Pro-CXR is still loaded through its custom remote-code implementation.
8
-
9
  torch==2.10.0
10
  torchvision==0.25.0
11
- transformers==5.4.0
12
- accelerate>=1.2.1
 
 
13
  attrdict==2.0.1
14
  sentencepiece
15
  einops
16
  timm>=1.0.0
17
  safetensors>=0.5.0
18
-
19
  opencv-python-headless
20
  Pillow
21
  numpy==1.26.4
 
1
+ # FeatureX Clinical AI — Hugging Face ZeroGPU
2
+ # Gradio 5.29.1 is intentionally pinned because Janus-Pro-CXR was validated
3
+ # with the Transformers 4.x stack; Gradio 6 + Hub 1.x forced an incompatible
4
+ # Transformers 5.x bridge in the previous build.
5
+ gradio==5.29.1
 
 
 
6
  torch==2.10.0
7
  torchvision==0.25.0
8
+ transformers==4.49.0
9
+ accelerate==1.2.1
10
+ tokenizers==0.21.0
11
+ huggingface-hub<1.0
12
  attrdict==2.0.1
13
  sentencepiece
14
  einops
15
  timm>=1.0.0
16
  safetensors>=0.5.0
 
17
  opencv-python-headless
18
  Pillow
19
  numpy==1.26.4