Tonic commited on
Commit
c94e1f8
·
verified ·
1 Parent(s): a82dcd2

ci: deploy pro_gradio_demo (b76bf27af8b8)

Browse files
app.py CHANGED
@@ -1,6 +1,7 @@
1
  from __future__ import annotations
2
 
3
  from nutonic_pro_gradio_demo.gradio_app import build_demo
 
4
 
5
  # Hugging Face ZeroGPU Spaces require at least one `@spaces.GPU` function to be
6
  # defined at import time in the Space entry file (`app.py`).
@@ -18,5 +19,5 @@ except ImportError:
18
  demo = build_demo()
19
 
20
  if __name__ == "__main__":
21
- demo.launch()
22
 
 
1
  from __future__ import annotations
2
 
3
  from nutonic_pro_gradio_demo.gradio_app import build_demo
4
+ from nutonic_pro_gradio_demo.gradio_app import LEAFLET_JS
5
 
6
  # Hugging Face ZeroGPU Spaces require at least one `@spaces.GPU` function to be
7
  # defined at import time in the Space entry file (`app.py`).
 
19
  demo = build_demo()
20
 
21
  if __name__ == "__main__":
22
+ demo.launch(ssr_mode=False, js=LEAFLET_JS)
23
 
nutonic_pro_gradio_demo/client.py CHANGED
@@ -10,7 +10,7 @@ from urllib.parse import urljoin, urlparse
10
  import httpx
11
 
12
  from nutonic_pro_gradio_demo.hmac_signing import nutonic_hmac_headers
13
- from nutonic_pro_gradio_demo.models import ProJobCreateIn, ProJobCreateOut, ProJobStatusOut, ProVlmModelManifest
14
  from nutonic_pro_gradio_demo.settings import Settings
15
 
16
 
@@ -52,6 +52,8 @@ class NutonicServerClient:
52
  "User-Agent": "nutonic-pro-gradio-demo/0.1 (+https://huggingface.co/spaces/Tonic/nutonic-pro-demo)",
53
  },
54
  )
 
 
55
 
56
  @property
57
  def origin(self) -> str:
@@ -70,15 +72,24 @@ class NutonicServerClient:
70
  return self._origin + path
71
 
72
  def post_pro_job(self, body: ProJobCreateIn) -> ProJobCreateOut:
73
- r = self._request_with_backoff("POST", "/api/v1/pro/jobs", json_body=body.model_dump(mode="json"))
 
 
 
 
 
74
  return ProJobCreateOut.model_validate(r.json())
75
 
76
  def get_pro_job(self, job_id: str) -> ProJobStatusOut:
77
- r = self._request_with_backoff("GET", f"/api/v1/pro/jobs/{job_id}")
78
  return ProJobStatusOut.model_validate(r.json())
79
 
80
  def get_artifact(self, *, job_id: str, artifact_id: str) -> bytes:
81
- r = self._request_with_backoff("GET", f"/api/v1/pro/jobs/{job_id}/artifacts/{artifact_id}")
 
 
 
 
82
  return r.content
83
 
84
  def get_bytes_by_url(self, url_or_path: str) -> bytes:
@@ -87,9 +98,23 @@ class NutonicServerClient:
87
  return r.content
88
 
89
  def get_vlm_model_manifest(self) -> ProVlmModelManifest:
90
- r = self._request_with_backoff("GET", "/api/v1/pro/vlm/model-manifest")
91
  return ProVlmModelManifest.model_validate(r.json())
92
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  def _signed_headers(self, *, method: str, url: str, body: bytes) -> dict[str, str]:
94
  if not self._hmac_secret:
95
  return {}
@@ -103,6 +128,7 @@ class NutonicServerClient:
103
  absolute: bool = False,
104
  max_retries: int = 4,
105
  json_body: Any | None = None,
 
106
  ) -> httpx.Response:
107
  """
108
  Hugging Face Spaces can return 429 under load. Handle 429 (and some transient 5xx)
@@ -123,6 +149,8 @@ class NutonicServerClient:
123
  attempt += 1
124
  url = path_or_url if absolute else self._url(path_or_url)
125
  headers: dict[str, str] = self._signed_headers(method=method, url=url, body=body_bytes)
 
 
126
  request_kwargs: dict[str, Any] = {}
127
  if json_body is not None:
128
  # Send the same exact bytes we hashed; otherwise httpx may re-serialize
 
10
  import httpx
11
 
12
  from nutonic_pro_gradio_demo.hmac_signing import nutonic_hmac_headers
13
+ from nutonic_pro_gradio_demo.models import ProJobCreateIn, ProJobCreateOut, ProJobStatusOut, ProVlmModelManifest, TokenResponse
14
  from nutonic_pro_gradio_demo.settings import Settings
15
 
16
 
 
52
  "User-Agent": "nutonic-pro-gradio-demo/0.1 (+https://huggingface.co/spaces/Tonic/nutonic-pro-demo)",
53
  },
54
  )
55
+ self._session_token: str | None = None
56
+ self._session_token_expires_at: float = 0.0
57
 
58
  @property
59
  def origin(self) -> str:
 
72
  return self._origin + path
73
 
74
  def post_pro_job(self, body: ProJobCreateIn) -> ProJobCreateOut:
75
+ r = self._request_with_backoff(
76
+ "POST",
77
+ "/api/v1/pro/jobs",
78
+ json_body=body.model_dump(mode="json"),
79
+ require_bearer=True,
80
+ )
81
  return ProJobCreateOut.model_validate(r.json())
82
 
83
  def get_pro_job(self, job_id: str) -> ProJobStatusOut:
84
+ r = self._request_with_backoff("GET", f"/api/v1/pro/jobs/{job_id}", require_bearer=True)
85
  return ProJobStatusOut.model_validate(r.json())
86
 
87
  def get_artifact(self, *, job_id: str, artifact_id: str) -> bytes:
88
+ r = self._request_with_backoff(
89
+ "GET",
90
+ f"/api/v1/pro/jobs/{job_id}/artifacts/{artifact_id}",
91
+ require_bearer=True,
92
+ )
93
  return r.content
94
 
95
  def get_bytes_by_url(self, url_or_path: str) -> bytes:
 
98
  return r.content
99
 
100
  def get_vlm_model_manifest(self) -> ProVlmModelManifest:
101
+ r = self._request_with_backoff("GET", "/api/v1/pro/vlm/model-manifest", require_bearer=True)
102
  return ProVlmModelManifest.model_validate(r.json())
103
 
104
+ def post_auth_token(self) -> TokenResponse:
105
+ r = self._request_with_backoff("POST", "/api/v1/auth/token")
106
+ return TokenResponse.model_validate(r.json())
107
+
108
+ def _ensure_bearer(self) -> str:
109
+ now = time.time()
110
+ if self._session_token and now < self._session_token_expires_at:
111
+ return self._session_token
112
+ issued = self.post_auth_token()
113
+ self._session_token = issued.access_token
114
+ # Refresh a bit early to avoid edge-of-expiry 401s.
115
+ self._session_token_expires_at = now + max(10, int(issued.expires_in) - 15)
116
+ return self._session_token
117
+
118
  def _signed_headers(self, *, method: str, url: str, body: bytes) -> dict[str, str]:
119
  if not self._hmac_secret:
120
  return {}
 
128
  absolute: bool = False,
129
  max_retries: int = 4,
130
  json_body: Any | None = None,
131
+ require_bearer: bool = False,
132
  ) -> httpx.Response:
133
  """
134
  Hugging Face Spaces can return 429 under load. Handle 429 (and some transient 5xx)
 
149
  attempt += 1
150
  url = path_or_url if absolute else self._url(path_or_url)
151
  headers: dict[str, str] = self._signed_headers(method=method, url=url, body=body_bytes)
152
+ if require_bearer:
153
+ headers["Authorization"] = f"Bearer {self._ensure_bearer()}"
154
  request_kwargs: dict[str, Any] = {}
155
  if json_body is not None:
156
  # Send the same exact bytes we hashed; otherwise httpx may re-serialize
nutonic_pro_gradio_demo/gradio_app.py CHANGED
@@ -14,6 +14,8 @@ from nutonic_pro_gradio_demo.settings import get_settings
14
  from nutonic_pro_gradio_demo.vlm_parse import parse_vlm_output
15
  from nutonic_pro_gradio_demo.vlm_runtime import ensure_model_loaded, infer_caption_and_boxes
16
 
 
 
17
 
18
  def _bbox_half_km_for_zoom(zoom: int) -> float:
19
  z = int(zoom)
@@ -221,6 +223,8 @@ def _run_full_pipeline(
221
 
222
  def build_demo() -> gr.Blocks:
223
  settings = get_settings()
 
 
224
  leaflet_js = r"""
225
  function() {
226
  // Load Leaflet (CSS + JS) dynamically (OSM tiles; no Mapbox).
@@ -300,7 +304,10 @@ function() {
300
  }
301
  """
302
 
303
- with gr.Blocks(title="NU:TONIC PRO (ZeroGPU demo)", js=leaflet_js) as demo:
 
 
 
304
  if settings.require_server_origin and not settings.nutonic_server_origin.strip():
305
  gr.Markdown(
306
  "## Configuration required\n\n"
 
14
  from nutonic_pro_gradio_demo.vlm_parse import parse_vlm_output
15
  from nutonic_pro_gradio_demo.vlm_runtime import ensure_model_loaded, infer_caption_and_boxes
16
 
17
+ LEAFLET_JS = ""
18
+
19
 
20
  def _bbox_half_km_for_zoom(zoom: int) -> float:
21
  z = int(zoom)
 
223
 
224
  def build_demo() -> gr.Blocks:
225
  settings = get_settings()
226
+ # Used by app.py (Gradio 6 moved js= from Blocks(...) to launch()).
227
+ # Keep it as a local value and return it via a module constant below.
228
  leaflet_js = r"""
229
  function() {
230
  // Load Leaflet (CSS + JS) dynamically (OSM tiles; no Mapbox).
 
304
  }
305
  """
306
 
307
+ global LEAFLET_JS
308
+ LEAFLET_JS = leaflet_js
309
+
310
+ with gr.Blocks(title="NU:TONIC PRO (ZeroGPU demo)") as demo:
311
  if settings.require_server_origin and not settings.nutonic_server_origin.strip():
312
  gr.Markdown(
313
  "## Configuration required\n\n"
nutonic_pro_gradio_demo/models.py CHANGED
@@ -5,6 +5,12 @@ from typing import Any, Literal
5
  from pydantic import BaseModel, Field
6
 
7
 
 
 
 
 
 
 
8
  ProJobProfile = Literal[
9
  "wildfire",
10
  "oceanscout_ship_detection",
 
5
  from pydantic import BaseModel, Field
6
 
7
 
8
+ class TokenResponse(BaseModel):
9
+ access_token: str
10
+ token_type: str = "bearer"
11
+ expires_in: int
12
+
13
+
14
  ProJobProfile = Literal[
15
  "wildfire",
16
  "oceanscout_ship_detection",