| from __future__ import annotations |
|
|
| import base64 |
| import importlib.util |
| import importlib.metadata |
| import html |
| import json |
| import os |
| import platform |
| import subprocess |
| import sys |
| import time |
| from functools import lru_cache |
| from pathlib import Path |
|
|
| import gradio as gr |
|
|
| try: |
| import spaces as hf_spaces |
| except Exception: |
| hf_spaces = None |
|
|
|
|
| HERE = Path(__file__).resolve().parent |
| DEMO_WEBMAP = HERE / "demo_webmap" |
| MANIFEST_PATH = DEMO_WEBMAP / "manifest.json" |
| LIVE_ROOT = Path(os.getenv("STORMSCOPE_LIVE_ROOT", "/tmp/stormscope-live")) |
| LIVE_TIMEOUT_SEC = int(os.getenv("STORMSCOPE_LIVE_TIMEOUT_SEC", "900")) |
| PYDEPS_ROOT = Path(os.getenv("STORMSCOPE_PYDEPS_ROOT", str(LIVE_ROOT / "pydeps"))) |
| EARTH2GRID_REF = os.getenv("STORMSCOPE_EARTH2GRID_REF", "8fdff5a78d324f8d25afe224915301b3169bffe2") |
|
|
|
|
| def gpu_decorator(*args, **kwargs): |
| if hf_spaces is not None and hasattr(hf_spaces, "GPU"): |
| return hf_spaces.GPU(*args, **kwargs) |
|
|
| def passthrough(func): |
| return func |
|
|
| return passthrough |
|
|
|
|
| def data_uri(path: Path) -> str: |
| encoded = base64.b64encode(path.read_bytes()).decode("ascii") |
| return f"data:image/png;base64,{encoded}" |
|
|
|
|
| def embed_manifest(webmap_dir: Path, *, cached: bool = False) -> dict: |
| manifest_path = webmap_dir / "manifest.json" |
| manifest = json.loads(manifest_path.read_text(encoding="utf-8")) |
| for product in manifest["products"].values(): |
| for frame in product["frames"]: |
| frame_path = webmap_dir / frame["url"] |
| frame["url"] = data_uri(frame_path) |
| frame.pop("source_npz", None) |
| manifest["source"] = "bundled-demo" if cached else "live" |
| return manifest |
|
|
|
|
| @lru_cache(maxsize=1) |
| def embedded_demo_manifest() -> dict: |
| return embed_manifest(DEMO_WEBMAP, cached=True) |
|
|
|
|
| def map_html(manifest: dict | None = None) -> str: |
| manifest = manifest or embedded_demo_manifest() |
| manifest_json = json.dumps(manifest) |
| document = f"""<!doctype html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1"> |
| <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"> |
| <style> |
| html, body {{ margin: 0; width: 100%; height: 100%; }} |
| #stormscope-map-wrap {{ |
| position: relative; |
| width: 100%; |
| height: min(74vh, 760px); |
| min-height: 560px; |
| border: 1px solid #cad4cf; |
| border-radius: 8px; |
| overflow: hidden; |
| background: #eef2ef; |
| }} |
| #stormscope-map {{ width: 100%; height: 100%; }} |
| #stormscope-panel {{ |
| position: absolute; |
| z-index: 700; |
| left: 12px; |
| right: 12px; |
| top: 12px; |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| gap: 12px; |
| padding: 10px 12px; |
| background: rgba(255,255,255,.93); |
| border: 1px solid #d5ddd7; |
| border-radius: 8px; |
| box-shadow: 0 8px 28px rgba(20,30,25,.12); |
| }} |
| #stormscope-panel h1 {{ |
| margin: 0; |
| font-size: 16px; |
| line-height: 1.1; |
| letter-spacing: 0; |
| }} |
| #stormscope-meta {{ color: #59655f; font-size: 12px; margin-top: 3px; }} |
| #stormscope-controls {{ display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }} |
| #stormscope-controls button, #stormscope-controls select {{ |
| min-height: 32px; |
| border: 1px solid #cbd5ce; |
| border-radius: 6px; |
| background: white; |
| color: #17201b; |
| font: inherit; |
| font-weight: 650; |
| padding: 0 10px; |
| }} |
| #stormscope-controls input[type="range"] {{ width: min(38vw, 420px); }} |
| #stormscope-stamp {{ min-width: 88px; font-weight: 750; text-align: center; }} |
| .leaflet-control-attribution {{ font-size: 10px; }} |
| @media (max-width: 760px) {{ |
| #stormscope-map-wrap {{ min-height: 620px; }} |
| #stormscope-panel {{ display: grid; left: 8px; right: 8px; top: 8px; }} |
| #stormscope-controls {{ justify-content: start; }} |
| #stormscope-controls input[type="range"] {{ width: 100%; }} |
| }} |
| </style> |
| </head> |
| <body> |
| <div id="stormscope-map-wrap"> |
| <div id="stormscope-map"></div> |
| <div id="stormscope-panel"> |
| <div> |
| <h1 id="stormscope-title">StormScope Fast</h1> |
| <div id="stormscope-meta"></div> |
| </div> |
| <div id="stormscope-controls"> |
| <select id="stormscope-product"></select> |
| <button id="stormscope-prev" type="button">Prev</button> |
| <input id="stormscope-frame" type="range" min="0" max="0" value="0"> |
| <button id="stormscope-next" type="button">Next</button> |
| <button id="stormscope-play" type="button">Play</button> |
| <span id="stormscope-stamp"></span> |
| </div> |
| </div> |
| </div> |
| <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> |
| <script> |
| (() => {{ |
| const manifest = {manifest_json}; |
| const existing = document.getElementById("stormscope-map"); |
| if (!existing || existing.dataset.ready === "1") return; |
| existing.dataset.ready = "1"; |
| |
| const bounds = [[manifest.bounds.south, manifest.bounds.west], [manifest.bounds.north, manifest.bounds.east]]; |
| const view = manifest.view_bounds || manifest.bounds; |
| const viewBounds = [[view.south, view.west], [view.north, view.east]]; |
| const map = L.map("stormscope-map", {{ preferCanvas: true, zoomControl: false }}); |
| L.tileLayer("https://tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png", {{ |
| maxZoom: 12, |
| attribution: "© OpenStreetMap contributors" |
| }}).addTo(map); |
| L.control.zoom({{ position: "bottomleft" }}).addTo(map); |
| map.fitBounds(viewBounds, {{ padding: [18, 18] }}); |
| |
| const productSelect = document.getElementById("stormscope-product"); |
| const slider = document.getElementById("stormscope-frame"); |
| const stamp = document.getElementById("stormscope-stamp"); |
| const playButton = document.getElementById("stormscope-play"); |
| document.getElementById("stormscope-title").textContent = `StormScope ${{manifest.mode}} ${{manifest.cycle}}`; |
| document.getElementById("stormscope-meta").textContent = `${{manifest.init_time}} | ${{manifest.resolution}} | ${{manifest.frame_count}} frames`; |
| |
| Object.entries(manifest.products).forEach(([slug, product]) => {{ |
| const option = document.createElement("option"); |
| option.value = slug; |
| option.textContent = product.title; |
| productSelect.appendChild(option); |
| }}); |
| |
| let product = productSelect.value; |
| let frame = 0; |
| let timer = null; |
| let overlay = null; |
| |
| function productData() {{ return manifest.products[product]; }} |
| function frameData() {{ return productData().frames[frame]; }} |
| function update() {{ |
| const p = productData(); |
| slider.max = String(p.frames.length - 1); |
| if (frame >= p.frames.length) frame = p.frames.length - 1; |
| slider.value = String(frame); |
| stamp.textContent = frameData().label; |
| if (!overlay) {{ |
| overlay = L.imageOverlay(frameData().url, bounds, {{ opacity: p.opacity, interactive: false }}).addTo(map); |
| }} else {{ |
| overlay.setUrl(frameData().url); |
| overlay.setOpacity(p.opacity); |
| }} |
| }} |
| |
| productSelect.addEventListener("change", () => {{ |
| product = productSelect.value; |
| frame = Math.min(frame, productData().frames.length - 1); |
| update(); |
| }}); |
| slider.addEventListener("input", () => {{ frame = Number(slider.value); update(); }}); |
| document.getElementById("stormscope-prev").addEventListener("click", () => {{ |
| frame = (frame + productData().frames.length - 1) % productData().frames.length; |
| update(); |
| }}); |
| document.getElementById("stormscope-next").addEventListener("click", () => {{ |
| frame = (frame + 1) % productData().frames.length; |
| update(); |
| }}); |
| playButton.addEventListener("click", () => {{ |
| if (timer) {{ |
| clearInterval(timer); |
| timer = null; |
| playButton.textContent = "Play"; |
| }} else {{ |
| timer = setInterval(() => {{ |
| frame = (frame + 1) % productData().frames.length; |
| update(); |
| }}, 650); |
| playButton.textContent = "Pause"; |
| }} |
| }}); |
| update(); |
| }})(); |
| </script> |
| </body> |
| </html> |
| """ |
| return ( |
| '<iframe title="StormScope OpenStreetMap preview" ' |
| 'style="width:100%;height:min(78vh,820px);min-height:620px;border:0;border-radius:8px;overflow:hidden;" ' |
| f'srcdoc="{html.escape(document, quote=True)}"></iframe>' |
| ) |
|
|
|
|
| def live_enabled() -> bool: |
| return os.getenv("STORMSCOPE_ENABLE_LIVE", "0").lower() in {"1", "true", "yes", "on"} |
|
|
|
|
| def package_available() -> bool: |
| return importlib.util.find_spec("rustwx_stormscope") is not None |
|
|
|
|
| def env_flag(name: str, default: str = "0") -> bool: |
| return os.getenv(name, default).lower() in {"1", "true", "yes", "on"} |
|
|
|
|
| def ensure_pydeps_path() -> None: |
| if PYDEPS_ROOT.exists(): |
| path = str(PYDEPS_ROOT) |
| if path not in sys.path: |
| sys.path.insert(0, path) |
|
|
|
|
| def command_probe(cmd: list[str]) -> str: |
| try: |
| proc = subprocess.run( |
| cmd, |
| cwd=HERE, |
| text=True, |
| capture_output=True, |
| timeout=45, |
| ) |
| except Exception as exc: |
| return "+ " + " ".join(cmd) + f"\nfailed: {type(exc).__name__}: {exc}" |
|
|
| stdout = proc.stdout.strip() |
| stderr = proc.stderr.strip() |
| lines = [ |
| "+ " + " ".join(cmd), |
| f"exit code: {proc.returncode}", |
| ] |
| if stdout: |
| lines.append(stdout[:1200]) |
| if stderr: |
| lines.append(stderr[:1200]) |
| return "\n".join(lines) |
|
|
|
|
| def bootstrap_earth2grid() -> str: |
| ensure_pydeps_path() |
| if importlib.util.find_spec("earth2grid") is not None: |
| return "earth2grid bootstrap: already importable" |
| if not env_flag("STORMSCOPE_BOOTSTRAP_EARTH2GRID"): |
| return "earth2grid bootstrap: disabled (set STORMSCOPE_BOOTSTRAP_EARTH2GRID=1)" |
|
|
| PYDEPS_ROOT.mkdir(parents=True, exist_ok=True) |
| requirement = f"earth2grid @ git+https://github.com/NVlabs/earth2grid.git@{EARTH2GRID_REF}" |
| env = os.environ.copy() |
| env["PYTHONPATH"] = str(PYDEPS_ROOT) + os.pathsep + env.get("PYTHONPATH", "") |
| cmd = [ |
| sys.executable, |
| "-m", |
| "pip", |
| "install", |
| "--no-cache-dir", |
| "--no-build-isolation", |
| "--target", |
| str(PYDEPS_ROOT), |
| "--upgrade", |
| requirement, |
| ] |
| try: |
| log = run_command(cmd, cwd=HERE, env=env) |
| except Exception as exc: |
| return f"earth2grid bootstrap failed: {type(exc).__name__}: {exc}" |
|
|
| importlib.invalidate_caches() |
| ensure_pydeps_path() |
| if importlib.util.find_spec("earth2grid") is None: |
| return log + "\n\nearth2grid bootstrap finished but module is still not importable" |
| return log + "\n\nearth2grid bootstrap: ok" |
|
|
|
|
| def module_probe(module_name: str, package_name: str | None = None) -> list[str]: |
| package_name = package_name or module_name |
| lines = [f"[{module_name}]"] |
| spec = importlib.util.find_spec(module_name) |
| lines.append(f"spec: {spec is not None}") |
| try: |
| version = importlib.metadata.version(package_name) |
| lines.append(f"version: {version}") |
| except Exception as exc: |
| lines.append(f"version: unavailable ({type(exc).__name__})") |
| if spec is None: |
| return lines |
| started = time.monotonic() |
| try: |
| __import__(module_name) |
| lines.append(f"import: ok ({time.monotonic() - started:.2f}s)") |
| except Exception as exc: |
| lines.append(f"import: failed ({type(exc).__name__}: {exc})") |
| return lines |
|
|
|
|
| def package_probe() -> str: |
| lines = [ |
| "StormScope package probe", |
| f"python: {platform.python_version()}", |
| f"rustwx_stormscope importable: {package_available()}", |
| ] |
| if not package_available(): |
| lines.append("") |
| lines.append("The Space payload does not include the release package source.") |
| lines.append("Push with `scripts/push-hf-space.py --profile package-probe` to test package import and CLI help.") |
| return "\n".join(lines) |
|
|
| lines.append("") |
| lines.append(command_probe([sys.executable, "-m", "rustwx_stormscope.run", "--help"])) |
| lines.append("") |
| lines.append(command_probe([sys.executable, "-m", "rustwx_stormscope.webmap_preview", "--help"])) |
| return "\n".join(lines) |
|
|
|
|
| @gpu_decorator(duration=120) |
| def weather_stack_probe() -> str: |
| lines = [ |
| "Weather model live-inference stack probe", |
| f"python: {platform.python_version()}", |
| f"platform: {platform.platform()}", |
| f"STORMSCOPE_ENABLE_LIVE: {os.getenv('STORMSCOPE_ENABLE_LIVE', '0')}", |
| f"cwd: {HERE}", |
| "", |
| ] |
|
|
| try: |
| import torch |
|
|
| lines.extend( |
| [ |
| "[torch/cuda]", |
| f"torch: {torch.__version__}", |
| f"cuda available: {torch.cuda.is_available()}", |
| ] |
| ) |
| if torch.cuda.is_available(): |
| lines.extend( |
| [ |
| f"gpu: {torch.cuda.get_device_name(0)}", |
| f"capability: {torch.cuda.get_device_capability(0)}", |
| f"arch list: {torch.cuda.get_arch_list()}", |
| f"bf16 supported: {torch.cuda.is_bf16_supported()}", |
| f"device memory GB: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f}", |
| ] |
| ) |
| except Exception as exc: |
| lines.append(f"[torch/cuda]\nfailed: {type(exc).__name__}: {exc}") |
|
|
| lines.append("") |
| lines.append("[runtime bootstrap]") |
| lines.append(bootstrap_earth2grid()) |
| lines.append("") |
| for module_name, package_name in [ |
| ("rustwx_stormscope", "rustwx-stormscope"), |
| ("earth2studio", "earth2studio"), |
| ("earth2grid", "earth2grid"), |
| ("physicsnemo", "nvidia-physicsnemo"), |
| ("natten", "natten"), |
| ("torchao", "torchao"), |
| ("h5netcdf", "h5netcdf"), |
| ("xarray", "xarray"), |
| ("scipy", "scipy"), |
| ]: |
| lines.extend(module_probe(module_name, package_name)) |
| lines.append("") |
|
|
| lines.append("[earth2studio StormScope import]") |
| started = time.monotonic() |
| try: |
| from earth2studio.models.px.stormscope import StormScopeBase, StormScopeGOES, StormScopeMRMS |
|
|
| lines.append(f"import: ok ({time.monotonic() - started:.2f}s)") |
| lines.append(f"classes: {StormScopeBase.__name__}, {StormScopeGOES.__name__}, {StormScopeMRMS.__name__}") |
| except Exception as exc: |
| lines.append(f"import: failed ({type(exc).__name__}: {exc})") |
|
|
| lines.append("") |
| lines.append("[nvidia-smi]") |
| lines.append(command_probe(["nvidia-smi"])) |
| return "\n".join(lines) |
|
|
|
|
| def live_status() -> str: |
| lines = [ |
| "Live inference status", |
| f"STORMSCOPE_ENABLE_LIVE: {os.getenv('STORMSCOPE_ENABLE_LIVE', '0')}", |
| f"rustwx_stormscope importable: {package_available()}", |
| f"live root: {LIVE_ROOT}", |
| f"timeout seconds: {LIVE_TIMEOUT_SEC}", |
| ] |
| if not live_enabled(): |
| lines.append("") |
| lines.append("Live inference is disabled for this public demo deployment.") |
| lines.append("Set STORMSCOPE_ENABLE_LIVE=1 only after the Space image has the full StormScope dependency stack.") |
| elif not package_available(): |
| lines.append("") |
| lines.append("Live inference is enabled, but rustwx_stormscope is not installed in this Space runtime.") |
| else: |
| lines.append("") |
| lines.append("Live inference is enabled and the package is importable.") |
| return "\n".join(lines) |
|
|
|
|
| def subprocess_env() -> dict: |
| env = os.environ.copy() |
| cache_root = Path(env.get("STORMSCOPE_CACHE_DIR", str(LIVE_ROOT / "cache"))) |
| env.setdefault("RUSTWX_STORMSCOPE_CACHE_DIR", str(cache_root)) |
| env.setdefault("EARTH2STUDIO_CACHE", str(cache_root / "earth2studio")) |
| env.setdefault("RUSTWX_STORMSCOPE_GOES_CACHE", str(cache_root / "goes_nc")) |
| env.setdefault("RUSTWX_STORMSCOPE_MRMS_CACHE", str(cache_root / "mrms_grib2")) |
| env.setdefault("TORCHINDUCTOR_CACHE_DIR", str(cache_root / "torchinductor")) |
| env.setdefault("TRITON_CACHE_DIR", str(cache_root / "triton")) |
| env["PYTHONPATH"] = str(PYDEPS_ROOT) + os.pathsep + env.get("PYTHONPATH", "") |
| return env |
|
|
|
|
| def run_command(cmd: list[str], *, cwd: Path, env: dict) -> str: |
| started = time.monotonic() |
| proc = subprocess.run( |
| cmd, |
| cwd=cwd, |
| env=env, |
| text=True, |
| capture_output=True, |
| timeout=LIVE_TIMEOUT_SEC, |
| ) |
| elapsed = time.monotonic() - started |
| lines = [ |
| "+ " + " ".join(cmd), |
| f"exit code: {proc.returncode}", |
| f"elapsed: {elapsed:.1f}s", |
| ] |
| if proc.stdout.strip(): |
| lines.append("") |
| lines.append(proc.stdout.strip()[-4000:]) |
| if proc.stderr.strip(): |
| lines.append("") |
| lines.append(proc.stderr.strip()[-4000:]) |
| if proc.returncode != 0: |
| raise RuntimeError("\n".join(lines)) |
| return "\n".join(lines) |
|
|
|
|
| def latest_meta(outdir: Path) -> Path: |
| metas = sorted(outdir.glob("stormscope_nowcast_6km_*_meta.json"), key=lambda path: path.stat().st_mtime) |
| if not metas: |
| raise FileNotFoundError(f"no StormScope metadata written in {outdir}") |
| return metas[-1] |
|
|
|
|
| def live_duration(init_time: str, frames: int, num_steps: int, backend: str, mxfp8: bool) -> int: |
| del init_time, backend, mxfp8 |
| return min(240, max(90, int(frames) * max(30, int(num_steps) * 4))) |
|
|
|
|
| @gpu_decorator(duration=live_duration) |
| def run_live_nowcast(init_time: str, frames: int, num_steps: int, backend: str, mxfp8: bool) -> tuple[str, str]: |
| if not live_enabled(): |
| return map_html(), live_status() |
| if not package_available(): |
| return map_html(), live_status() |
| ensure_pydeps_path() |
|
|
| frames = max(1, min(int(frames), 3)) |
| num_steps = max(4, min(int(num_steps), 24)) |
| backend = backend if backend in {"earth2studio", "rust"} else "earth2studio" |
| init_time = (init_time or "").strip() |
|
|
| run_id = time.strftime("%Y%m%d_%H%M%S") |
| run_root = LIVE_ROOT / "runs" / run_id |
| outdir = run_root / "out" |
| webmap_dir = run_root / "webmap" |
| outdir.mkdir(parents=True, exist_ok=True) |
| webmap_dir.mkdir(parents=True, exist_ok=True) |
| env = subprocess_env() |
| for path in [ |
| env["RUSTWX_STORMSCOPE_CACHE_DIR"], |
| env["EARTH2STUDIO_CACHE"], |
| env["RUSTWX_STORMSCOPE_GOES_CACHE"], |
| env["RUSTWX_STORMSCOPE_MRMS_CACHE"], |
| ]: |
| Path(path).mkdir(parents=True, exist_ok=True) |
|
|
| run_cmd = [ |
| sys.executable, |
| "-m", |
| "rustwx_stormscope.run", |
| "--mode", |
| "nowcast", |
| "--res", |
| "6km", |
| "--steps", |
| str(frames), |
| "--num-steps", |
| str(num_steps), |
| "--no-compile", |
| "--backend", |
| backend, |
| "--outdir", |
| str(outdir), |
| ] |
| if init_time and init_time.lower() not in {"latest", "auto"}: |
| run_cmd.extend(["--date", init_time]) |
| if mxfp8: |
| run_cmd.append("--mxfp8") |
|
|
| preview_cmd = [ |
| sys.executable, |
| "-m", |
| "rustwx_stormscope.webmap_preview", |
| "--outdir", |
| str(outdir), |
| "--output-dir", |
| str(webmap_dir), |
| "--products", |
| "refc,ir", |
| "--view", |
| "central_plains", |
| "--max-frames", |
| str(frames), |
| "--force", |
| ] |
|
|
| logs = [live_status(), ""] |
| try: |
| logs.append(bootstrap_earth2grid()) |
| logs.append("") |
| logs.append(run_command(run_cmd, cwd=HERE, env=env)) |
| meta = latest_meta(outdir) |
| preview_cmd.extend(["--meta", str(meta)]) |
| logs.append("") |
| logs.append(run_command(preview_cmd, cwd=HERE, env=env)) |
| manifest = embed_manifest(webmap_dir) |
| return map_html(manifest), "\n".join(logs) |
| except subprocess.TimeoutExpired as exc: |
| return map_html(), "\n".join(logs + [f"live inference timed out after {exc.timeout}s"]) |
| except Exception as exc: |
| return map_html(), "\n".join(logs + [f"live inference failed: {type(exc).__name__}: {exc}"]) |
|
|
|
|
| @gpu_decorator(duration=45) |
| def zero_gpu_probe() -> str: |
| lines = [ |
| "ZeroGPU runtime probe", |
| f"python: {platform.python_version()}", |
| f"platform: {platform.platform()}", |
| f"STORMSCOPE_ENABLE_LIVE: {os.getenv('STORMSCOPE_ENABLE_LIVE', '0')}", |
| ] |
| try: |
| import torch |
|
|
| lines.append(f"torch: {torch.__version__}") |
| lines.append(f"cuda available: {torch.cuda.is_available()}") |
| if torch.cuda.is_available(): |
| lines.append(f"gpu: {torch.cuda.get_device_name(0)}") |
| lines.append(f"capability: {torch.cuda.get_device_capability(0)}") |
| lines.append(f"arch list: {torch.cuda.get_arch_list()}") |
| except Exception as exc: |
| lines.append(f"torch probe failed: {type(exc).__name__}: {exc}") |
|
|
| lines.append("") |
| lines.append(f"Live StormScope inference enabled: {live_enabled()}") |
| lines.append("The production fast path targets a validated CUDA/PyTorch/NATTEN/torchao stack with MXFP8.") |
| lines.append("This probe reports the GPU Hugging Face assigns at launch time; hardware can vary.") |
| lines.append("This Space proves the public web-map product and hosted UX; live inference needs a separately validated HF dependency path.") |
| lines.append("") |
| lines.append(package_probe()) |
| lines.append("") |
| lines.append(live_status()) |
| return "\n".join(lines) |
|
|
|
|
| with gr.Blocks(title="StormScope Fast Demo") as demo: |
| gr.Markdown( |
| """ |
| # StormScope Fast Demo |
| |
| StormScope 10-minute nowcast frames rendered as transparent overlays on an |
| OpenStreetMap basemap. This is the hosted visualization shape for the |
| `rustwx-stormscope` release package. |
| """ |
| ) |
| map_output = gr.HTML(map_html()) |
| gr.Markdown("## Live Server Probe") |
| with gr.Row(): |
| probe = gr.Button("Check ZeroGPU Runtime", variant="secondary") |
| package_button = gr.Button("Check Package CLI", variant="secondary") |
| weather_button = gr.Button("Check Weather Stack", variant="secondary") |
| probe_output = gr.Textbox(label="Runtime probe", lines=12) |
| probe.click(zero_gpu_probe, outputs=probe_output) |
| package_button.click(package_probe, outputs=probe_output) |
| weather_button.click(weather_stack_probe, outputs=probe_output) |
| gr.Markdown("## Live Nowcast") |
| with gr.Row(): |
| init_time = gr.Textbox(label="Init time", value="2026-05-30T22:40:00", scale=2) |
| frames = gr.Slider(label="Frames", minimum=1, maximum=3, step=1, value=1, scale=1) |
| sampler_steps = gr.Slider(label="Sampler steps", minimum=4, maximum=24, step=1, value=8, scale=1) |
| with gr.Row(): |
| backend = gr.Dropdown(label="Data backend", choices=["earth2studio", "rust"], value="earth2studio") |
| mxfp8 = gr.Checkbox(label="MXFP8", value=False) |
| live_button = gr.Button("Run Live Nowcast", variant="primary") |
| live_output = gr.Textbox(label="Live inference log", value=live_status(), lines=16) |
| live_button.click( |
| run_live_nowcast, |
| inputs=[init_time, frames, sampler_steps, backend, mxfp8], |
| outputs=[map_output, live_output], |
| ) |
| gr.Markdown( |
| """ |
| The bundled cycle is pre-rendered from StormScope output. Full live inference is |
| guarded by `STORMSCOPE_ENABLE_LIVE=1` and uses the packaged CLI when the full |
| CUDA/PyTorch/NATTEN/torchao stack, model weights, and live satellite fetch path |
| are available on ZeroGPU. |
| """ |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| demo.launch() |
|
|