Spaces:
Sleeping
Sleeping
ci: deploy pro_gradio_demo (b76bf27af8b8)
Browse files- app.py +2 -1
- nutonic_pro_gradio_demo/client.py +33 -5
- nutonic_pro_gradio_demo/gradio_app.py +8 -1
- nutonic_pro_gradio_demo/models.py +6 -0
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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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",
|