Spaces:
Running
Running
File size: 14,548 Bytes
65a92b4 64d8003 a1f52bf 2dfa899 a1f52bf e5f1ed2 a1f52bf e5f1ed2 a1f52bf e5f1ed2 a1f52bf e5f1ed2 a1f52bf 64d8003 a1f52bf 64d8003 a1f52bf 5ba47e2 a1f52bf 5ba47e2 a1f52bf 64d8003 5ba47e2 a1f52bf 64d8003 a1f52bf 64d8003 5ba47e2 e5f1ed2 5ba47e2 64d8003 a1f52bf 64d8003 2dfa899 c5672b8 d4c9a3c 2dfa899 c5672b8 2dfa899 a1f52bf 5ba47e2 a1f52bf e5f1ed2 a1f52bf e5f1ed2 a1f52bf e5f1ed2 5ba47e2 a1f52bf 5ba47e2 a1f52bf 5ba47e2 a1f52bf 5ba47e2 2dfa899 a1f52bf 64d8003 a1f52bf 5ba47e2 a1f52bf 5ba47e2 a1f52bf 5ba47e2 a1f52bf e5f1ed2 a1f52bf 5ba47e2 a1f52bf 18911ed 5ba47e2 18911ed a1f52bf 2dfa899 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 | ---
title: wave~reader
emoji: π
colorFrom: blue
colorTo: indigo
sdk: gradio
sdk_version: 6.26.0
python_version: "3.14"
app_file: app.py
license: mit
pinned: false
short_description: Surf-forecast world model for the Australian coast
---
# wave~reader
**A surf-forecasting world model for the Australian coast: 238 breaks, 168
deterministic scores per spot, a 3D seafloor built from real GEBCO
bathymetry β and one open model that narrates the numbers instead of
inventing them.**
wave~reader is a single-page Gradio app, built in September 2026
as an entry for the **GTC Berlin Golden Ticket Developer Contest**. I have
just moved to Berlin; I wanted to build something to make me enjoy ML and
engineering again.
> [Built for GTC Berlin](#built-for-the-gtc-berlin-golden-ticket-developer-contest-) β
> NVIDIA open weights on Hugging Face rails. #NVIDIAGTC
## One rule holds the whole app together
**The LLM never owns numbers.** Every wave height, wind reading, and 0β10
score on screen comes out of plain, deterministic Python β
`wavereader/scoring.py`, `wavereader/openmeteo.py`, `wavereader/tools.py` β
running over Open-Meteo marine data and the enriched break catalogue. The
model's contract, written into its system prompt, is to interpret those
numbers, name the tool that produced them, and never compute a forecast
itself. The agent can _recommend_; it can't _hallucinate a swell_.
That split β a deterministic core with the model bolted on as an
interpreter β is the entire architecture, and it's what makes an
open-weights model safe to plan a surf trip around.
## One screen, four instruments
There are no tabs. One surface, four synchronized instruments, one source
of truth (`selected`): picking a spot re-aims everything at once.
| Instrument | What it does |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Map lens** (left) | All 238 breaks on a Plotly map, filtered by state / region / skill. A pick loads the full field guide and re-aims the forecast, world model, and climatology. |
| **Forecast story** (center) | The engines spin up in visible stages β status lines name the engine and its latency. You get a 0β10 score for every hour of the next 7 days, score / swell / wind strips with weekend shading, then a **smoothed 3D seafloor with an animated swell layer** riding the forecast's dominant period (press βΆ swell). An optional streamed narrator writes it up β from the scores only. |
| **Intel rail** (right) | Field guide (peak type, ideal swell / wind / tide, hazards, crowd factor), a **5-year ERA5 swell climate rose** that audits the dataset's own claims, a wetsuit hint, and nearby surf cams. |
| **Agent bar** (full width) | Chat: _"where's it going to be good in NSW this weekend?"_ A smolagents `ToolCallingAgent` on Nemotron 3 Ultra finds spots, ranks regions, explains score breakdowns, and suggests similar breaks. Every tool card shows its **latency in ms**; charts render mid-answer straight from tool payloads. Hard budget: 6 steps, 700 tokens a turn. |
## Machinery you can watch
The app wears its internals on its sleeve: a strip across the top shows
every engine (π world model Β· π‘ feeds Β· π‘ climate Β· β scorer Β· π§ LLM Β·
π MCP) with live timings, and slow work loads in stages so the charts are
never blocked. Measured on a laptop against the disk cache:
| Engine | Cold | Warm | Where |
| -------------------------------------------- | -------- | ---------- | ------------------------- |
| β scoring engine (168 hourly 0β10 scores) | ~300 ms | **0.8 ms** | `wavereader/scoring.py` |
| π‘ Open-Meteo marine + wind feed | ~300 ms | **~1 ms** | `wavereader/openmeteo.py` |
| π GEBCO world model (10Γ10 bathymetry grid) | ~1.3 s | **~1 ms** | `wavereader/seafloor.py` |
| π§ agent tool call (`score_week` over MCP) | ~2 ms | **0.7 ms** | `wavereader/tools.py` |
| Chart trio (score / swell / wind) | 12β14 ms | β | `ui/charts/` |
Forecast charts land in ~80 ms while the world model resolves in a
parallel thread. `scripts/warm_caches.py` pre-fills every cache before a
demo, so a cold start never happens on camera. The seafloor view bicubically
upsamples the 10Γ10 GEBCO grid to 41Γ41 and animates a translucent swell
surface at the forecast's dominant height and period β a world model you
can watch working. (Smoothing and animation are display-only; statistics
always come from the raw grid.)
## The stack: NVIDIA open weights on Hugging Face rails
Every model call in the repo goes through one door β **Hugging Face
Inference Providers**, served by DeepInfra β running
[`nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-BF16`](https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-BF16),
the open-weights Ultra of NVIDIA's Nemotron 3 family. No private APIs, no
proprietary endpoints, one `HF_TOKEN`. Around the model, everything is
open too: Open-Meteo forecasts, ERA5 reanalysis, GEBCO 2020 bathymetry via
OpenTopoData, and a scoring engine you can read in one sitting.
With NVIDIA and Hugging Face now one family, this app got to be an early
wedding photo. π
## The app is the tool: MCP built in
`app.py` launches with `mcp_server=True`, so any MCP client (Claude
Desktop, opencode, β¦) can drive the deterministic engines directly at
`<space-or-local-url>/gradio_api/mcp/`. The public surface is exactly
three typed tools β the same functions the agent calls internally:
- `score_week` β hourly 0β10 scores, daily bests, best window for one break
- `rank_region_week` β every break in a region, ranked best-first
- `explain_score` β the size / direction / wind / period breakdown of one scored hour
Verify with `curl <url>/gradio_api/mcp/schema`. Everything else in the UI
is registered with `api_name=False` on purpose β the MCP surface stays
clean.
## Quickstart
Needs [uv](https://docs.astral.sh/uv/) and Python β₯ 3.14.
```sh
uv sync
cp .env.example .env # fill in HF_TOKEN (https://huggingface.co/settings/tokens)
uv run app.py
```
Or skip the UI and drive the agent from Python:
```sh
uv run python -c "from wavereader.agent import run_stream; \
[print(e.get('text', e.get('name', '')), end='') for e in run_stream('best beginner morning this weekend in VIC?')]"
```
`HF_TOKEN` is only needed for the agent and the narrator β the forecast
charts, world model, and MCP tools run token-free with plain network
access to Open-Meteo / OpenTopoData and a disk cache under `.cache/`.
On a HF Space the secret is the fallback; the agent panel's BYO-token box
is session-only and never logged.
## Deploying to HF Spaces
The app is live at [huggingface.co/spaces/lucashudsn/wavereader](https://huggingface.co/spaces/lucashudsn/wavereader).
The Space _is_ this git repo β the `space` remote points at it. Spaces
read their config from YAML frontmatter at the top of `README.md`, but
GitHub renders that block as an ugly metadata table, so it lives in
`space-config.yaml` instead and the deploy flow prepends it in a
throwaway commit at push time (the repo README stays clean; HF gets its
config). Keep `sdk_version` in lockstep with the gradio pin in
`requirements.txt`. If you change Space settings through the web UI, HF
commits them into the Space's `README.md` β mirror anything you care
about back into `space-config.yaml`.
**Two ways to ship β deploys work from any branch, not just `main`:**
1. **Automatic, on every push to `main`.** The
`.github/workflows/deploy-space.yml` action deploys `main` to the
Space using the repo's `HF_TOKEN` Actions secret. Merge and walk
away; watch progress under the repo's _Actions_ tab and the Space's
_Logs β Build_.
2. **Manual, any branch.** From a clean tree with your work committed:
```sh
scripts/deploy_space.sh # deploy the branch you're on
scripts/deploy_space.sh some-branch # deploy any committed ref
git push origin some-branch # keep GitHub in sync
```
The script builds its throwaway commit in a temp worktree, so your
checkout is never touched; it force-pushes that tree to the Space's
`main` (deploys are throwaway commits β only the tip matters to the
build). The push asks for credentials: username `lucashudsn`,
password = an HF token with write access
(https://huggingface.co/settings/tokens).
One-time CI setup: the Action needs `HF_TOKEN` as a **GitHub** repo
secret (_Settings β Secrets and variables β Actions_, or
`gh secret set HF_TOKEN`). The Space's own `HF_TOKEN` is a separate
secret, already set. Builds take a few minutes; watch them under the
Space's _Logs β Build_ tab or poll
`HfApi().get_space_runtime("lucashudsn/wavereader")`.
Secrets live in the Space's _Settings β Variables and secrets_ β
`HF_TOKEN` is already set, which is what lets the agent and narrator work
for visitors. `WR_MODEL` / `WR_PROVIDER` / `WR_BASE_URL` can be added the
same way if you ever want to reroute the LLM. Caches under `.cache/` are
ephemeral on the Space and rebuild lazily after a restart; nothing else
needs doing.
## How it's laid out
```
app.py # HF Space entry: single-page UI + gr.api MCP tools
ui/ # the front end
app.py # layout + wiring (β€3 gr.State; Bells on load)
panels/{lens,story,intel,agent}.py # map lens | forecast story | intel rail | agent bar
charts/{map,strip,score,seafloor,climate,_style}.py # Plotly builders, one theme
contracts.py # SELECT/FETCH/AGENT key contracts β no output-order drift
_compat.py # backend access: v2 core β legacy.app.* β stubs
theme.py # APP_CSS β IBM Plex Mono lo-fi identity
wavereader/ # pure core: typed, tested, zero Gradio imports
breaks.py openmeteo.py scoring.py seafloor.py climate.py
tools.py # the deterministic tool functions (agent + MCP)
agent.py # smolagents ToolCallingAgent, typed event stream
narrate.py llm.py # streamed report writer; one InferenceClient factory
scripts/
warm_caches.py # demo hot start: forecast + seafloor caches
build_climate.py # ERA5 5-yr climatology builder (re-embeds catalogue)
check_coords.py check_coast.py # coordinate + GEBCO coastline maintenance
eval_agent.py # agent evaluation harness
data/
australia-surf-breaks-enriched.json # 238 schema-valid breaks, ERA5 embedded
surf-break-schema.json # the conformance contract
surf-break-example-bells.json # prompt worked example
surf-cams.json # camera links keyed name | state | region
documents/
ENCYCLOPEDIA.md FORECAST.md SURFAGENT.md
legacy/ # frozen v1 front end, kept as the compat fallback
```
Cheap checks: `uv run pytest -q` (offline fixtures, including an agent
dry-run through an injected fake model) and `uv run python -c "import app"`.
## Where the data came from
The break catalogue was generated by running **Nemotron 3.5 Lightning**
once per break through the v1 pipeline (now archived in `legacy/`): one
chat call per spot at temperature 0.4, with the JSON schema and the Bells
Beach worked example loaded from `data/` into the prompt.
`state` / `region` are pinned from the input list, never taken from the
model, and every record validates against
`data/surf-break-schema.json` with strict enums for swell, wind, tide,
skill, and hazards. The batch runner is resumable.
After the build, `scripts/build_climate.py` gave each break a **5-year
ERA5 swell profile** (embedded in the record itself β 236 of 238; two
duplicate Gnaraloo rows miss out) and `scripts/check_coords.py` /
`scripts/check_coast.py` keep coordinates honest against OSM and the GEBCO
shoreline. The climate rose in the intel rail uses those profiles to audit
the dataset: where the model's claimed "ideal swell" disagrees with five
years of observed reanalysis, the data wins.
## The forecast pipeline, end to end
1. **Deterministic core.** break β `scoring.py` β
`openmeteo.get_forecast` (Open-Meteo marine + wind, disk-cached,
snapped seaward to the right grid cell) β `score_week` β 168 hourly
scores β Plotly strips + best-window hero. In parallel, the **world
model** pulls the GEBCO 2020 grid around the takeoff zone, classifies
the shelf shape, and renders depth, 3D, and transects with the
animated swell layer.
2. **Narrator (optional).** The prompt carries only the daily bests
(β€7 lines) plus the best window; Nemotron 3 Ultra streams a dot-point
write-up of exactly those numbers. Nothing more.
Full detail in [`documents/FORECAST.md`](documents/FORECAST.md).
## Built for the GTC Berlin Golden Ticket Developer Contest ποΈ
wave~reader is a contest entry: an app that could only exist because
NVIDIA ships frontier-class open weights and Hugging Face turns them into
one-token inference. It leans on both halves deliberately β Nemotron 3
Ultra for language, deterministic Python for physics β and shows its work
on screen the whole way.
Tagging @Merve Noyan β thanks for the nudge. #NVIDIAGTC
|