Spaces:
Running
Download README.md from lucashudsn/wavereader: direct link, hf CLI and curl.
- Browser
- Download file 14.5 kB
-
https://huggingface.co/spaces/lucashudsn/wavereader/resolve/main/README.md
- Command line
-
hf download hf://spaces/lucashudsn/wavereader/README.md
-
curl -L -o README.md https://huggingface.co/spaces/lucashudsn/wavereader/resolve/main/README.md
A newer version of the Gradio SDK is available: 6.30.0
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 β 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,
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 breakrank_region_weekβ every break in a region, ranked best-firstexplain_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 and Python β₯ 3.14.
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:
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.
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:
Automatic, on every push to
main. The.github/workflows/deploy-space.ymlaction deploysmainto the Space using the repo'sHF_TOKENActions secret. Merge and walk away; watch progress under the repo's Actions tab and the Space's Logs β Build.Manual, any branch. From a clean tree with your work committed:
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 syncThe 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: usernamelucashudsn, 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
- 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. - 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.
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