fix: restore missing comma in Activity BarPlot (boot SyntaxError)
#1
by ParetoOptimal - opened
- FIELD_NOTES.md +7 -21
- PLAN.md +6 -9
- README.md +15 -25
- app.py +4 -19
- docs/blog-offgridschedula.md +181 -0
- docs/build-small-submission.md +19 -22
- docs/demo-script.md +34 -0
- docs/demo-walkthrough.md +127 -0
- docs/social-post.md +36 -0
- server/health.py +0 -54
- static/app.css +8 -34
- ui/blocks.py +0 -0
FIELD_NOTES.md
CHANGED
|
@@ -24,7 +24,7 @@ only place the messages exist in a queryable form is a Mac, where they sync to
|
|
| 24 |
The biggest adoption lesson came later: requiring a Mac collector + Full Disk
|
| 25 |
Access is a wall for a non-technical user. The fix was to make **paste-from-phone**
|
| 26 |
the hero path (the collector is now strictly optional) — no install, no DB, no
|
| 27 |
-
permissions. Most of that capability already existed in the
|
| 28 |
framed as secondary.
|
| 29 |
|
| 30 |
## 2. `attributedBody` is the iMessage parsing trap
|
|
@@ -87,30 +87,16 @@ exporting them, so the metric tracks value taken, not previews shown.
|
|
| 87 |
QLoRA on a 31B needs an 80GB GPU. [`training/modal_train.py`](./training/modal_train.py)
|
| 88 |
wraps the existing `train_qlora.py` + `export_gguf.sh` to run on a serverless
|
| 89 |
A100/H100 and publish the GGUF to HF — roughly **$5–15 per run**, so ~$250 of credit
|
| 90 |
-
is 15–40 iterations. The "Well-Tuned" track
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
still spends your own Modal credits; the turnkey path is there whenever you want to retrain.)
|
| 95 |
|
| 96 |
A small rule that paid off: training-data generation can use *any* offline tooling —
|
| 97 |
the "no cloud AI API" rule applies only to the **running app's inference**, not to
|
| 98 |
dataset prep.
|
| 99 |
|
| 100 |
-
## 7.
|
| 101 |
-
|
| 102 |
-
What shipped is two small local models, not one. The fine-tuned **gemma-cal E4B** does the
|
| 103 |
-
*reading* (thread → validated `ActionPlan`); a 1B **OpenBMB MiniCPM** does the *orchestrating*.
|
| 104 |
-
Clicking **Run the agents** hands the job to MiniCPM, which drives the Space's own MCP tools —
|
| 105 |
-
`extract_events → check_conflicts → make_ics` — as a visible multi-step agent
|
| 106 |
-
([`server/orchestrator.py`](./server/orchestrator.py)), consuming the *public* tool contract
|
| 107 |
-
instead of calling internals. Two things I'd underline: keep the planner **optional** (a
|
| 108 |
-
deterministic scripted plan is the fallback, so the agentic path never hard-depends on a second
|
| 109 |
-
model load), and don't let "agent" become a separate destination — the same **Run the agents**
|
| 110 |
-
action drives both the home workflow and the orchestrated trace, so it stays one engine, not a
|
| 111 |
-
second UI to keep in sync.
|
| 112 |
-
|
| 113 |
-
## 8. The Off-the-Grid tension
|
| 114 |
|
| 115 |
"No cloud AI APIs" and "serve a 31B" pull against each other: a Q4 31B GGUF is
|
| 116 |
~18–20GB and needs a GPU. Keeping inference **in the Space via `llama.cpp`** preserves
|
|
@@ -123,7 +109,7 @@ The same principle drove the trace-sharing design (below): the hosted Space hold
|
|
| 123 |
HF token** — it only offers a **local download**, and a separate local CLI does the
|
| 124 |
upload with your own auth.
|
| 125 |
|
| 126 |
-
##
|
| 127 |
|
| 128 |
- **Durable trace/metrics store.** The activity bus is an 800-entry in-memory ring
|
| 129 |
buffer ([`server/events.py`](./server/events.py)) — runs are lost on restart, so only
|
|
|
|
| 24 |
The biggest adoption lesson came later: requiring a Mac collector + Full Disk
|
| 25 |
Access is a wall for a non-technical user. The fix was to make **paste-from-phone**
|
| 26 |
the hero path (the collector is now strictly optional) — no install, no DB, no
|
| 27 |
+
permissions. Most of that capability already existed in the Review tab; it was just
|
| 28 |
framed as secondary.
|
| 29 |
|
| 30 |
## 2. `attributedBody` is the iMessage parsing trap
|
|
|
|
| 87 |
QLoRA on a 31B needs an 80GB GPU. [`training/modal_train.py`](./training/modal_train.py)
|
| 88 |
wraps the existing `train_qlora.py` + `export_gguf.sh` to run on a serverless
|
| 89 |
A100/H100 and publish the GGUF to HF — roughly **$5–15 per run**, so ~$250 of credit
|
| 90 |
+
is 15–40 iterations. The "Well-Tuned" track is genuinely *prepped* (pipeline +
|
| 91 |
+
expanded seed dataset + a turnkey publish path) but **not finished**: the actual run
|
| 92 |
+
spends the user's credits, so the repo is honest that the Space still ships on a
|
| 93 |
+
community GGUF until you run it.
|
|
|
|
| 94 |
|
| 95 |
A small rule that paid off: training-data generation can use *any* offline tooling —
|
| 96 |
the "no cloud AI API" rule applies only to the **running app's inference**, not to
|
| 97 |
dataset prep.
|
| 98 |
|
| 99 |
+
## 7. The Off-the-Grid tension
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
"No cloud AI APIs" and "serve a 31B" pull against each other: a Q4 31B GGUF is
|
| 102 |
~18–20GB and needs a GPU. Keeping inference **in the Space via `llama.cpp`** preserves
|
|
|
|
| 109 |
HF token** — it only offers a **local download**, and a separate local CLI does the
|
| 110 |
upload with your own auth.
|
| 111 |
|
| 112 |
+
## 8. What I'd do next
|
| 113 |
|
| 114 |
- **Durable trace/metrics store.** The activity bus is an 800-entry in-memory ring
|
| 115 |
buffer ([`server/events.py`](./server/events.py)) — runs are lost on restart, so only
|
PLAN.md
CHANGED
|
@@ -14,7 +14,7 @@ the point; the app must deliver value with no GPU (stub agent) first.
|
|
| 14 |
You want an agent that reads iMessage-style threads, understands the conversation, and turns them
|
| 15 |
into calendar events/reminders — exposed through a custom Gradio UI deployed as a Hugging Face Space.
|
| 16 |
Two local models share the work: our fine-tuned Gemma does the *reading* (thread → validated
|
| 17 |
-
ActionPlan), and an **OpenBMB MiniCPM** planner does the *orchestrating* — the
|
| 18 |
driving the Space's own MCP tools (`extract_events → check_conflicts → make_ics`) as a visible
|
| 19 |
multi-step agent. The build competes in the **Backyard AI** track (general and OpenBMB prizes are
|
| 20 |
awarded per track) and satisfies the quests secondary to the user story above: **Off the Grid** (no
|
|
@@ -37,7 +37,7 @@ Confirmed decisions:
|
|
| 37 |
- **Calendar output:** local `.ics` files first (strictly off-grid), with an *optional* Google
|
| 38 |
Calendar push toggle as a bonus.
|
| 39 |
- **Extraction model:** fine-tune Gemma, serve as GGUF via llama.cpp (production serves the
|
| 40 |
-
**E4B** edge fine-tune, `
|
| 41 |
- **Agent planner:** **OpenBMB MiniCPM** (`openbmb/MiniCPM4.1-8B-GGUF`, Q4; the 1B variant is a
|
| 42 |
config switch) on a second llama-server — it plans, the MCP tools execute, every step visible.
|
| 43 |
|
|
@@ -63,7 +63,7 @@ Confirmed decisions:
|
|
| 63 |
Flow: messages → extraction prompt → model emits structured JSON of candidate events →
|
| 64 |
validated → shown in UI for review → user approves → `.ics` generated (and/or pushed to GCal).
|
| 65 |
|
| 66 |
-
**
|
| 67 |
llama-server, OpenAI-compatible) consumes the Space's own **MCP tool surface** —
|
| 68 |
`extract_events → check_conflicts → make_ics` — through smolagents, so the pipeline above is
|
| 69 |
demonstrated as multi-step tool use over the public tool contract, with the planner's trace on
|
|
@@ -87,8 +87,8 @@ screen (`server/orchestrator.py`). Stub/CI falls back to a scripted planner so t
|
|
| 87 |
- **Docker SDK Space** (`README.md` frontmatter: `sdk: docker`, `app_port: 7860`).
|
| 88 |
- **llama.cpp** loads the fine-tuned GGUF and serves chat completions — satisfies
|
| 89 |
*Llama Champion*; no external AI call satisfies *Off the Grid*.
|
| 90 |
-
- **Agent orchestrator** (`server/orchestrator.py`): the **OpenBMB MiniCPM** planner
|
| 91 |
-
|
| 92 |
per-track prize case, and the same extraction pipeline exercised through the public tool
|
| 93 |
contract rather than private imports.
|
| 94 |
- `/ingest` (FastAPI, mounted alongside Gradio) receives messages, runs the extraction prompt,
|
|
@@ -174,9 +174,6 @@ screen (`server/orchestrator.py`). Stub/CI falls back to a scripted planner so t
|
|
| 174 |
the headline, E4B as the safety net for a reliable live demo.
|
| 175 |
- **`chat.db` schema / `attributedBody`.** Mitigated by using `imessage-exporter`/`imessage_reader`.
|
| 176 |
- **Full Disk Access** must be granted to the collector's process or reads return empty.
|
| 177 |
-
- **Privacy:**
|
| 178 |
-
hero phone-paste path keeps data client-side (calendar tokens live in the browser, nothing
|
| 179 |
-
persists server-side). The Space now lives in the public **`build-small-hackathon`** submission
|
| 180 |
-
org, so the *source* is public — but user data still never lands on the server.
|
| 181 |
- **Relative-date accuracy** is the main quality risk — pass the current datetime into the prompt
|
| 182 |
and weight the dataset toward relative-date examples.
|
|
|
|
| 14 |
You want an agent that reads iMessage-style threads, understands the conversation, and turns them
|
| 15 |
into calendar events/reminders — exposed through a custom Gradio UI deployed as a Hugging Face Space.
|
| 16 |
Two local models share the work: our fine-tuned Gemma does the *reading* (thread → validated
|
| 17 |
+
ActionPlan), and an **OpenBMB MiniCPM** planner does the *orchestrating* — the Agent tab's brain,
|
| 18 |
driving the Space's own MCP tools (`extract_events → check_conflicts → make_ics`) as a visible
|
| 19 |
multi-step agent. The build competes in the **Backyard AI** track (general and OpenBMB prizes are
|
| 20 |
awarded per track) and satisfies the quests secondary to the user story above: **Off the Grid** (no
|
|
|
|
| 37 |
- **Calendar output:** local `.ics` files first (strictly off-grid), with an *optional* Google
|
| 38 |
Calendar push toggle as a bonus.
|
| 39 |
- **Extraction model:** fine-tune Gemma, serve as GGUF via llama.cpp (production serves the
|
| 40 |
+
**E4B** edge fine-tune, `ParetoOptimal/gemma-4-cal-gguf`).
|
| 41 |
- **Agent planner:** **OpenBMB MiniCPM** (`openbmb/MiniCPM4.1-8B-GGUF`, Q4; the 1B variant is a
|
| 42 |
config switch) on a second llama-server — it plans, the MCP tools execute, every step visible.
|
| 43 |
|
|
|
|
| 63 |
Flow: messages → extraction prompt → model emits structured JSON of candidate events →
|
| 64 |
validated → shown in UI for review → user approves → `.ics` generated (and/or pushed to GCal).
|
| 65 |
|
| 66 |
+
The **Agent tab** runs the same flow agentically: an **OpenBMB MiniCPM** planner (second local
|
| 67 |
llama-server, OpenAI-compatible) consumes the Space's own **MCP tool surface** —
|
| 68 |
`extract_events → check_conflicts → make_ics` — through smolagents, so the pipeline above is
|
| 69 |
demonstrated as multi-step tool use over the public tool contract, with the planner's trace on
|
|
|
|
| 87 |
- **Docker SDK Space** (`README.md` frontmatter: `sdk: docker`, `app_port: 7860`).
|
| 88 |
- **llama.cpp** loads the fine-tuned GGUF and serves chat completions — satisfies
|
| 89 |
*Llama Champion*; no external AI call satisfies *Off the Grid*.
|
| 90 |
+
- **Agent orchestrator** (`server/orchestrator.py`): the Agent tab's **OpenBMB MiniCPM** planner
|
| 91 |
+
(its own llama-server) drives the Space's MCP tools as a multi-step agent — the OpenBMB
|
| 92 |
per-track prize case, and the same extraction pipeline exercised through the public tool
|
| 93 |
contract rather than private imports.
|
| 94 |
- `/ingest` (FastAPI, mounted alongside Gradio) receives messages, runs the extraction prompt,
|
|
|
|
| 174 |
the headline, E4B as the safety net for a reliable live demo.
|
| 175 |
- **`chat.db` schema / `attributedBody`.** Mitigated by using `imessage-exporter`/`imessage_reader`.
|
| 176 |
- **Full Disk Access** must be granted to the collector's process or reads return empty.
|
| 177 |
+
- **Privacy:** messages leave the Mac for the Space — keep the Space **private** and token-gated.
|
|
|
|
|
|
|
|
|
|
| 178 |
- **Relative-date accuracy** is the main quality risk — pass the current datetime into the prompt
|
| 179 |
and weight the dataset toward relative-date examples.
|
README.md
CHANGED
|
@@ -24,26 +24,15 @@ models:
|
|
| 24 |
demo_video:
|
| 25 |
- https://youtu.be/m-o0u9X3tI4
|
| 26 |
social_posts:
|
| 27 |
-
- https://x.com/nate_mauer/status/2065973341651882386
|
| 28 |
- https://x.com/nate_mauer/status/2064920352845709419
|
| 29 |
- https://x.com/nate_mauer/status/2065661878441750916
|
| 30 |
-
- https://www.linkedin.com/feed/update/urn:li:ugcPost:7471440639969132545
|
| 31 |
-
blog_post:
|
| 32 |
-
- https://huggingface.co/blog/build-small-hackathon/offgridschedula
|
| 33 |
made_by:
|
| 34 |
- ParetoOptimal - a.k.a., Nate Mauer
|
| 35 |
---
|
| 36 |
|
| 37 |
-
# 🗓️
|
| 38 |
|
| 39 |
-
|
| 40 |
-
no setup. iOS allows neither background iMessage access nor a persistent on-device LLM server, so there's no autonomous on-device agent to install; instead,
|
| 41 |
-
a foreground Shortcut ([docs/automations.md](./docs/automations.md)) hands a thread or screenshot to the agent in two taps (optionally using a remote model via `INFERENCE_BASE_URL`).**
|
| 42 |
-
|
| 43 |
-
The model runs on **your own server or even on the phone itself** and not on a cloud AI service. Your chats aren't shipped off to a third-party AI to be read; agent reads your snippet in memory and
|
| 44 |
-
discards it after replying. The run trace you can optionally share is a redacted, sent to the agent you control that turns it into ready-to-add calendar events.
|
| 45 |
-
|
| 46 |
-
**Hardware-aware.** With under-powered hardware, the app warns users with an upgrade banner rather than hanging, the real model needs a tiny GPU.
|
| 47 |
|
| 48 |
## Build Small submission — the idea & the tech
|
| 49 |
|
|
@@ -52,7 +41,7 @@ class chat, the practice that moved, the party flyer. OffGridSchedula turns thos
|
|
| 52 |
events: paste the chat (or snap the flyer) from a phone browser, review the extracted events, the
|
| 53 |
conflicts against your own `.ics`, and a drafted reply — then add to Apple/Google Calendar in a tap.
|
| 54 |
|
| 55 |
-
**The tech.**
|
| 56 |
(~4B effective params), our QLoRA fine-tune of Gemma-4 E4B that emits a single validated
|
| 57 |
**ActionPlan** (events · conflicts · reply · clarifying question), served with **vision** through
|
| 58 |
the official **llama.cpp** server inside this Docker Gradio Space — no cloud AI APIs. The
|
|
@@ -60,7 +49,7 @@ fine-tune + its 60-example task eval ran entirely on **Modal** serverless GPUs,
|
|
| 60 |
eval gate that rejected eight regressed models before this one shipped. Conflict math is
|
| 61 |
deterministic Python, the UI is fully custom, the agent doubles as an **MCP tool server**, and
|
| 62 |
redacted run traces are public on the [Hub](https://huggingface.co/datasets/ParetoOptimal/offgridschedula-traces).
|
| 63 |
-
|
| 64 |
drives this same Space's MCP tools as a multi-step agent — extract → check conflicts → render
|
| 65 |
`.ics` — with every step visible. Still zero cloud AI; every model under 32B.
|
| 66 |
|
|
@@ -81,9 +70,8 @@ captured, conflicts caught, and time saved; a per-device **Memory** (localStorag
|
|
| 81 |
samples) feeds names and preferences back into extraction.
|
| 82 |
|
| 83 |
**Submission links:** [requirement-by-requirement mapping](./docs/build-small-submission.md) ·
|
| 84 |
-
|
| 85 |
-
social
|
| 86 |
-
[2](https://x.com/nate_mauer/status/2065661878441750916)
|
| 87 |
|
| 88 |
## Who this is for
|
| 89 |
|
|
@@ -101,7 +89,7 @@ with no GPU (see *Accuracy upgrade* below).
|
|
| 101 |
## The model: `gemma-cal` E4B — one calendar-native LLM, built for exactly this
|
| 102 |
|
| 103 |
What makes this platform different isn't a prompt wrapped around a generic chatbot — it's
|
| 104 |
-
**[`gemma-cal` E4B](https://huggingface.co/
|
| 105 |
Gemma-4 E4B purpose-built for one job: turning messy human conversation into calendar-ready
|
| 106 |
structure.** The model doesn't chat. It reads a thread (or a flyer photo) and emits a single
|
| 107 |
validated **ActionPlan** — events with exact ISO datetimes, conflicts, proposed alternatives, a
|
|
@@ -205,7 +193,7 @@ Two caveats:
|
|
| 205 |
app.py # Gradio + FastAPI entrypoint (the Space)
|
| 206 |
server/
|
| 207 |
agent.py # thread (+images) -> validated ActionPlan
|
| 208 |
-
orchestrator.py #
|
| 209 |
schema.py # Event / Conflict / ActionPlan pydantic models
|
| 210 |
model.py # llama.cpp load: GGUF + vision mmproj, constrained JSON
|
| 211 |
imageutil.py # image -> base64 data URI
|
|
@@ -263,7 +251,7 @@ config works anywhere llama.cpp runs:
|
|
| 263 |
|
| 264 |
```bash
|
| 265 |
export USE_STUB_EXTRACTOR=0
|
| 266 |
-
export MODEL_HF_REPO="
|
| 267 |
export MODEL_FILE="gemma-cal-e4b-Q4_K_M.gguf" # ~5 GB edge fine-tune (what the Space serves)
|
| 268 |
export MMPROJ_REPO="unsloth/gemma-4-E4B-it-GGUF" # the E4B's own vision projector
|
| 269 |
export MMPROJ_FILE="mmproj-F16.gguf" # enables screenshot/vision input
|
|
@@ -321,7 +309,7 @@ parent whose family calendar is buried in a noisy class group chat.
|
|
| 321 |
| Award | Why this submission qualifies |
|
| 322 |
|---|---|
|
| 323 |
| 🟢 **Modal Awards** (best Modal-powered apps) | **Modal powered the development of the platform's model end-to-end** — required note, gladly given: [`training/modal_train.py`](./training/modal_train.py) (QLoRA fine-tune on serverless A100/H100s, Volumes caching weights), [`training/modal_eval.py`](./training/modal_eval.py) + [`modal_quant_eval.py`](./training/modal_quant_eval.py) (the task eval served on llama.cpp inside Modal, incl. an f16/Q8_0/Q4_K_M quantization study and the regex/text/vision A/B harness), and [`training/gated_retrain.py`](./training/gated_retrain.py) (train → staging → eval → promote *only past the gate* — eight regressed models rejected, every run a Modal job). |
|
| 324 |
-
| 🌱 **OpenBMB Awards** (standout MiniCPM builds, per track) | The **
|
| 325 |
|
| 326 |
*(Not claimed: the OpenAI Track — no Codex-attributed commits — and the NVIDIA Nemotron Quest —
|
| 327 |
different model family. We'd rather be honest than eligible.)*
|
|
@@ -334,7 +322,7 @@ different model family. We'd rather be honest than eligible.)*
|
|
| 334 |
| 🎨 **Off-Brand Award** | Custom landing page, hero + carousel, grouped nav, bespoke results cards and Activity dashboard — [`ui/blocks.py`](./ui/blocks.py) + [`static/app.css`](./static/app.css), far past the stock Gradio look. |
|
| 335 |
| 🐜 **Tiny Titan** | The platform's one and only model is **Gemma E4B — ~4B *effective* parameters** (~5 GB at Q4, serves on a 16 GB T4 or a laptop), and a 1B MiniCPM planner variant is a config switch. Honest framing: E4B is a MatFormer "effective-4B" — judges' call whether that's tiny enough. |
|
| 336 |
| 🎬 **Best Demo** | App + demo video + social post as one package — storyboard with every quest named on-camera in [`docs/demo-script.md`](./docs/demo-script.md). |
|
| 337 |
-
| 🤖 **Best Agent** | The MiniCPM-planned, MCP-tool-driven
|
| 338 |
| 🃏 **Judges' Wildcard** | No entry needed — but if "eval-gated fine-tuning with a public failure post-mortem" fits no category, we know where to find you. |
|
| 339 |
|
| 340 |
### Collectable quests — all six claimed
|
|
@@ -342,11 +330,11 @@ different model family. We'd rather be honest than eligible.)*
|
|
| 342 |
| Quest | Evidence |
|
| 343 |
|---|---|
|
| 344 |
| 🔌 **Off the Grid** (local-first, no cloud APIs) | All inference is llama.cpp inside the Space; the only optional outbound call is the user's own Google Calendar push. |
|
| 345 |
-
| 🎯 **Well-Tuned** (published fine-tune) | [`gemma-cal` E4B](https://huggingface.co/
|
| 346 |
| 🎨 **Off-Brand** (custom UI) | See the Off-Brand Award case above. |
|
| 347 |
| 🦙 **Llama Champion** (llama.cpp runtime) | The official `ghcr.io/ggml-org/llama.cpp` server image runs the GGUF + vision mmproj ([`Dockerfile`](./Dockerfile), [`scripts/start_space.sh`](./scripts/start_space.sh)). |
|
| 348 |
| 📡 **Sharing is Caring** (open trace on the Hub) | Redacted agent traces published to [`ParetoOptimal/offgridschedula-traces`](https://huggingface.co/datasets/ParetoOptimal/offgridschedula-traces) — one click from the Activity tab. |
|
| 349 |
-
| 📓 **Field Notes** (write-up) | [`FIELD_NOTES.md`](./FIELD_NOTES.md) + the [eval-gated fine-tuning post-mortem](./docs/blog-eval-gated-finetuning.md) + [project blog](
|
| 350 |
|
| 351 |
## Fine-tune on Modal (GPU)
|
| 352 |
|
|
@@ -433,3 +421,5 @@ gh variable set SPACE_HEALTH_URL -b "https://<owner>-<space>.hf.space/health"
|
|
| 433 |
CI installs `requirements-ci.txt` (excludes `llama-cpp-python` and the Google libs — both are
|
| 434 |
imported lazily and not needed for the stub-mode tests). A weekly Claude `/schedule` routine handles
|
| 435 |
the judgment work (grow `training/data/dataset.jsonl` → PR, triage CI failures).
|
|
|
|
|
|
|
|
|
| 24 |
demo_video:
|
| 25 |
- https://youtu.be/m-o0u9X3tI4
|
| 26 |
social_posts:
|
|
|
|
| 27 |
- https://x.com/nate_mauer/status/2064920352845709419
|
| 28 |
- https://x.com/nate_mauer/status/2065661878441750916
|
|
|
|
|
|
|
|
|
|
| 29 |
made_by:
|
| 30 |
- ParetoOptimal - a.k.a., Nate Mauer
|
| 31 |
---
|
| 32 |
|
| 33 |
+
# 🗓️ iMessage Scheduling Agent
|
| 34 |
|
| 35 |
+
**Turn a class group chat into calendar events — from your phone, no setup.**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
## Build Small submission — the idea & the tech
|
| 38 |
|
|
|
|
| 41 |
events: paste the chat (or snap the flyer) from a phone browser, review the extracted events, the
|
| 42 |
conflicts against your own `.ics`, and a drafted reply — then add to Apple/Google Calendar in a tap.
|
| 43 |
|
| 44 |
+
**The tech.** One small model does everything: [`gemma-cal` E4B](https://huggingface.co/ParetoOptimal/gemma-4-cal-gguf)
|
| 45 |
(~4B effective params), our QLoRA fine-tune of Gemma-4 E4B that emits a single validated
|
| 46 |
**ActionPlan** (events · conflicts · reply · clarifying question), served with **vision** through
|
| 47 |
the official **llama.cpp** server inside this Docker Gradio Space — no cloud AI APIs. The
|
|
|
|
| 49 |
eval gate that rejected eight regressed models before this one shipped. Conflict math is
|
| 50 |
deterministic Python, the UI is fully custom, the agent doubles as an **MCP tool server**, and
|
| 51 |
redacted run traces are public on the [Hub](https://huggingface.co/datasets/ParetoOptimal/offgridschedula-traces).
|
| 52 |
+
The **Agent tab** closes the loop: an **OpenBMB MiniCPM** planner (second local llama-server)
|
| 53 |
drives this same Space's MCP tools as a multi-step agent — extract → check conflicts → render
|
| 54 |
`.ics` — with every step visible. Still zero cloud AI; every model under 32B.
|
| 55 |
|
|
|
|
| 70 |
samples) feeds names and preferences back into extraction.
|
| 71 |
|
| 72 |
**Submission links:** [requirement-by-requirement mapping](./docs/build-small-submission.md) ·
|
| 73 |
+
demo video *(recording — https://youtu.be/m-o0u9X3tI4)* ·
|
| 74 |
+
social post *(queued — https://x.com/nate_mauer/status/2064920352845709419 and https://x.com/nate_mauer/status/2065661878441750916)*
|
|
|
|
| 75 |
|
| 76 |
## Who this is for
|
| 77 |
|
|
|
|
| 89 |
## The model: `gemma-cal` E4B — one calendar-native LLM, built for exactly this
|
| 90 |
|
| 91 |
What makes this platform different isn't a prompt wrapped around a generic chatbot — it's
|
| 92 |
+
**[`gemma-cal` E4B](https://huggingface.co/ParetoOptimal/gemma-4-cal-gguf), our own fine-tune of
|
| 93 |
Gemma-4 E4B purpose-built for one job: turning messy human conversation into calendar-ready
|
| 94 |
structure.** The model doesn't chat. It reads a thread (or a flyer photo) and emits a single
|
| 95 |
validated **ActionPlan** — events with exact ISO datetimes, conflicts, proposed alternatives, a
|
|
|
|
| 193 |
app.py # Gradio + FastAPI entrypoint (the Space)
|
| 194 |
server/
|
| 195 |
agent.py # thread (+images) -> validated ActionPlan
|
| 196 |
+
orchestrator.py # Agent tab: MiniCPM planner driving our own MCP tools
|
| 197 |
schema.py # Event / Conflict / ActionPlan pydantic models
|
| 198 |
model.py # llama.cpp load: GGUF + vision mmproj, constrained JSON
|
| 199 |
imageutil.py # image -> base64 data URI
|
|
|
|
| 251 |
|
| 252 |
```bash
|
| 253 |
export USE_STUB_EXTRACTOR=0
|
| 254 |
+
export MODEL_HF_REPO="ParetoOptimal/gemma-4-cal-gguf"
|
| 255 |
export MODEL_FILE="gemma-cal-e4b-Q4_K_M.gguf" # ~5 GB edge fine-tune (what the Space serves)
|
| 256 |
export MMPROJ_REPO="unsloth/gemma-4-E4B-it-GGUF" # the E4B's own vision projector
|
| 257 |
export MMPROJ_FILE="mmproj-F16.gguf" # enables screenshot/vision input
|
|
|
|
| 309 |
| Award | Why this submission qualifies |
|
| 310 |
|---|---|
|
| 311 |
| 🟢 **Modal Awards** (best Modal-powered apps) | **Modal powered the development of the platform's model end-to-end** — required note, gladly given: [`training/modal_train.py`](./training/modal_train.py) (QLoRA fine-tune on serverless A100/H100s, Volumes caching weights), [`training/modal_eval.py`](./training/modal_eval.py) + [`modal_quant_eval.py`](./training/modal_quant_eval.py) (the task eval served on llama.cpp inside Modal, incl. an f16/Q8_0/Q4_K_M quantization study and the regex/text/vision A/B harness), and [`training/gated_retrain.py`](./training/gated_retrain.py) (train → staging → eval → promote *only past the gate* — eight regressed models rejected, every run a Modal job). |
|
| 312 |
+
| 🌱 **OpenBMB Awards** (standout MiniCPM builds, per track) | The **Agent tab is planned by OpenBMB MiniCPM** (`openbmb/MiniCPM4.1-8B-GGUF`, Q4; the 1B variant is a config switch) on a second local llama-server, driving this Space's own MCP tools (`extract_events → check_conflicts → make_ics`) as a visible multi-step agent ([`server/orchestrator.py`](./server/orchestrator.py)). MiniCPM is the agent's brain, not a garnish. |
|
| 313 |
|
| 314 |
*(Not claimed: the OpenAI Track — no Codex-attributed commits — and the NVIDIA Nemotron Quest —
|
| 315 |
different model family. We'd rather be honest than eligible.)*
|
|
|
|
| 322 |
| 🎨 **Off-Brand Award** | Custom landing page, hero + carousel, grouped nav, bespoke results cards and Activity dashboard — [`ui/blocks.py`](./ui/blocks.py) + [`static/app.css`](./static/app.css), far past the stock Gradio look. |
|
| 323 |
| 🐜 **Tiny Titan** | The platform's one and only model is **Gemma E4B — ~4B *effective* parameters** (~5 GB at Q4, serves on a 16 GB T4 or a laptop), and a 1B MiniCPM planner variant is a config switch. Honest framing: E4B is a MatFormer "effective-4B" — judges' call whether that's tiny enough. |
|
| 324 |
| 🎬 **Best Demo** | App + demo video + social post as one package — storyboard with every quest named on-camera in [`docs/demo-script.md`](./docs/demo-script.md). |
|
| 325 |
+
| 🤖 **Best Agent** | The MiniCPM-planned, MCP-tool-driven Agent tab above — real multi-step tool use, every model under the 32B cap. |
|
| 326 |
| 🃏 **Judges' Wildcard** | No entry needed — but if "eval-gated fine-tuning with a public failure post-mortem" fits no category, we know where to find you. |
|
| 327 |
|
| 328 |
### Collectable quests — all six claimed
|
|
|
|
| 330 |
| Quest | Evidence |
|
| 331 |
|---|---|
|
| 332 |
| 🔌 **Off the Grid** (local-first, no cloud APIs) | All inference is llama.cpp inside the Space; the only optional outbound call is the user's own Google Calendar push. |
|
| 333 |
+
| 🎯 **Well-Tuned** (published fine-tune) | [`gemma-cal` E4B](https://huggingface.co/ParetoOptimal/gemma-4-cal-gguf) — our QLoRA fine-tune **is the model production serves**, shipped through the eval gate with the [honest scorecard public](./docs/eval-roadmap.md). |
|
| 334 |
| 🎨 **Off-Brand** (custom UI) | See the Off-Brand Award case above. |
|
| 335 |
| 🦙 **Llama Champion** (llama.cpp runtime) | The official `ghcr.io/ggml-org/llama.cpp` server image runs the GGUF + vision mmproj ([`Dockerfile`](./Dockerfile), [`scripts/start_space.sh`](./scripts/start_space.sh)). |
|
| 336 |
| 📡 **Sharing is Caring** (open trace on the Hub) | Redacted agent traces published to [`ParetoOptimal/offgridschedula-traces`](https://huggingface.co/datasets/ParetoOptimal/offgridschedula-traces) — one click from the Activity tab. |
|
| 337 |
+
| 📓 **Field Notes** (write-up) | [`FIELD_NOTES.md`](./FIELD_NOTES.md) + the [eval-gated fine-tuning post-mortem](./docs/blog-eval-gated-finetuning.md) + [project blog](./docs/blog-offgridschedula.md). |
|
| 338 |
|
| 339 |
## Fine-tune on Modal (GPU)
|
| 340 |
|
|
|
|
| 421 |
CI installs `requirements-ci.txt` (excludes `llama-cpp-python` and the Google libs — both are
|
| 422 |
imported lazily and not needed for the stub-mode tests). A weekly Claude `/schedule` routine handles
|
| 423 |
the judgment work (grow `training/data/dataset.jsonl` → PR, triage CI failures).
|
| 424 |
+
|
| 425 |
+
See **[PLAN.md](./PLAN.md)** for the full design, phases, and verification steps.
|
app.py
CHANGED
|
@@ -11,7 +11,7 @@ from fastapi import BackgroundTasks, FastAPI, Header, HTTPException, Request
|
|
| 11 |
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
| 12 |
from pydantic import BaseModel
|
| 13 |
|
| 14 |
-
from server import dedup, events,
|
| 15 |
from server.pipeline import AgentRequest, AgentResponse, run_pipeline
|
| 16 |
from ui.blocks import CAROUSEL_JS, CSS, THEME, build_demo
|
| 17 |
|
|
@@ -134,10 +134,8 @@ def ingest(batch: IngestBatch, background_tasks: BackgroundTasks,
|
|
| 134 |
|
| 135 |
|
| 136 |
@app.get("/health")
|
| 137 |
-
def
|
| 138 |
-
|
| 139 |
-
# status banner and the maintenance monitor both read this.
|
| 140 |
-
return health.health_status()
|
| 141 |
|
| 142 |
|
| 143 |
# --- Per-user Google Calendar OAuth (web flow) ----------------------------- #
|
|
@@ -254,19 +252,6 @@ if SERVE == "uvicorn":
|
|
| 254 |
from starlette.responses import Response as _Response
|
| 255 |
|
| 256 |
_CAROUSEL_INLINE = f'<script id="cz-inline-js">({CAROUSEL_JS})();</script>'
|
| 257 |
-
# Status banner: fetch /health on load and reveal #status-banner if degraded
|
| 258 |
-
# (e.g. real model on CPU-only hardware). Same inline-script pattern as the
|
| 259 |
-
# carousel, since js= is unreliable on a mounted app; it polls for the element
|
| 260 |
-
# because Gradio renders it client-side after </body>.
|
| 261 |
-
_BANNER_JS = (
|
| 262 |
-
"(function(){fetch('/health').then(function(r){return r.json();})"
|
| 263 |
-
".then(function(h){if(!h||!h.degraded){return;}(function s(){"
|
| 264 |
-
"var b=document.getElementById('status-banner');"
|
| 265 |
-
"if(!b){return setTimeout(s,400);}"
|
| 266 |
-
"b.textContent='\\u26a0\\ufe0f '+(h.reason||'This Space needs a GPU.')+' \\u26a0\\ufe0f';"
|
| 267 |
-
"b.style.display='block';})();}).catch(function(){});})();"
|
| 268 |
-
)
|
| 269 |
-
_BANNER_INLINE = f'<script id="cz-banner-js">{_BANNER_JS}</script>'
|
| 270 |
|
| 271 |
@app.middleware("http")
|
| 272 |
async def _inject_carousel_js(request, call_next): # noqa: ANN001
|
|
@@ -276,7 +261,7 @@ if SERVE == "uvicorn":
|
|
| 276 |
body = b"".join([chunk async for chunk in resp.body_iterator])
|
| 277 |
html = body.decode("utf-8", "ignore")
|
| 278 |
if "cz-inline-js" not in html and "</body>" in html:
|
| 279 |
-
html = html.replace("</body>", _CAROUSEL_INLINE +
|
| 280 |
headers = dict(resp.headers)
|
| 281 |
headers.pop("content-length", None) # body length changed; let Starlette recompute
|
| 282 |
return _Response(content=html, status_code=resp.status_code,
|
|
|
|
| 11 |
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
| 12 |
from pydantic import BaseModel
|
| 13 |
|
| 14 |
+
from server import dedup, events, threads
|
| 15 |
from server.pipeline import AgentRequest, AgentResponse, run_pipeline
|
| 16 |
from ui.blocks import CAROUSEL_JS, CSS, THEME, build_demo
|
| 17 |
|
|
|
|
| 134 |
|
| 135 |
|
| 136 |
@app.get("/health")
|
| 137 |
+
def health():
|
| 138 |
+
return {"ok": True}
|
|
|
|
|
|
|
| 139 |
|
| 140 |
|
| 141 |
# --- Per-user Google Calendar OAuth (web flow) ----------------------------- #
|
|
|
|
| 252 |
from starlette.responses import Response as _Response
|
| 253 |
|
| 254 |
_CAROUSEL_INLINE = f'<script id="cz-inline-js">({CAROUSEL_JS})();</script>'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
|
| 256 |
@app.middleware("http")
|
| 257 |
async def _inject_carousel_js(request, call_next): # noqa: ANN001
|
|
|
|
| 261 |
body = b"".join([chunk async for chunk in resp.body_iterator])
|
| 262 |
html = body.decode("utf-8", "ignore")
|
| 263 |
if "cz-inline-js" not in html and "</body>" in html:
|
| 264 |
+
html = html.replace("</body>", _CAROUSEL_INLINE + "</body>", 1)
|
| 265 |
headers = dict(resp.headers)
|
| 266 |
headers.pop("content-length", None) # body length changed; let Starlette recompute
|
| 267 |
return _Response(content=html, status_code=resp.status_code,
|
docs/blog-offgridschedula.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: "Off the Grid, On Schedule: a local-first, fine-tuned agent that turns group chats into calendar events"
|
| 3 |
+
thumbnail: /blog/assets/offgridschedula/thumbnail.png
|
| 4 |
+
authors:
|
| 5 |
+
- user: ParetoOptimal
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
# Off the Grid, On Schedule
|
| 9 |
+
|
| 10 |
+
> **TL;DR**
|
| 11 |
+
> - **OffGridSchedula** turns a pasted group chat — or a screenshot of a flyer or invite — into calendar events, with conflicts caught and a reply drafted, then a local `.ics` you own.
|
| 12 |
+
> - It runs a **fine-tuned Gemma 4** through **llama.cpp**, **in the Space**. No cloud AI APIs, no account, no app to install.
|
| 13 |
+
> - We built a full **QLoRA → GGUF** fine-tuning pipeline with an **eval gate** that refuses to publish a model that doesn't clear the bar — and used it to ship an own edge model at parity with the base.
|
| 14 |
+
> - It's also an **MCP server**: any MCP-aware agent can call `extract_events` / `make_ics` / `check_conflicts` as tools.
|
| 15 |
+
> - 👉 Try it: [**ParetoOptimal/OffGridSchedula**](https://huggingface.co/spaces/ParetoOptimal/OffGridSchedula) · Model: [**gemma-4-cal-gguf**](https://huggingface.co/ParetoOptimal/gemma-4-cal-gguf) · Traces: [**offgridschedula-traces**](https://huggingface.co/datasets/ParetoOptimal/offgridschedula-traces)
|
| 16 |
+
|
| 17 |
+
## Contents
|
| 18 |
+
- [The problem](#the-problem)
|
| 19 |
+
- [What we built](#what-we-built)
|
| 20 |
+
- [Architecture](#architecture)
|
| 21 |
+
- [Fine-tuning Gemma 4 for calendars](#fine-tuning-gemma-4-for-calendars)
|
| 22 |
+
- [Off the grid: llama.cpp in a Space](#off-the-grid-llamacpp-in-a-space)
|
| 23 |
+
- [From app to agent: MCP, memory, and shared traces](#from-app-to-agent-mcp-memory-and-shared-traces)
|
| 24 |
+
- [Lessons learned](#lessons-learned)
|
| 25 |
+
- [What's next](#whats-next)
|
| 26 |
+
- [Get started](#get-started)
|
| 27 |
+
- [Acknowledgements & citation](#acknowledgements--citation)
|
| 28 |
+
|
| 29 |
+
## The problem
|
| 30 |
+
|
| 31 |
+
Every parent knows this chat. The class group thread scrolls past: *"Picture day is Thursday at 9am, wear the green shirt."* *"Practice moves to Tuesday 5pm this week."* *"RSVP by Friday for the party."* You read it once, mean to add it later, and miss it.
|
| 32 |
+
|
| 33 |
+
That's the person we built for — not "users" in the abstract, but one busy parent whose kid's school and activity events are buried in noise. It's an honest fit for a small, local model: the input is a handful of recent messages or a screenshot, the output is a few calendar events, and the whole thing should work from a phone browser with nothing to install.
|
| 34 |
+
|
| 35 |
+

|
| 36 |
+
*The landing page. Paste a chat or a screenshot; get events back.*
|
| 37 |
+
|
| 38 |
+
## What we built
|
| 39 |
+
|
| 40 |
+
Paste the chat (or attach a screenshot of a flyer/invite). The agent returns:
|
| 41 |
+
|
| 42 |
+
1. **The events** — title, start/end, location, a sensible default reminder.
|
| 43 |
+
2. **A conflict check** against a calendar you upload (`.ics`), with clashes flagged.
|
| 44 |
+
3. **A ready-to-send reply** ("Got it — adding both, thanks!").
|
| 45 |
+
|
| 46 |
+
Everything is **surfaced for review before anything is saved**. The output is a local `.ics` file you can import into any calendar, with an optional one-click Google Calendar push. No inbox access, no auto-reading — it reads only what you choose to paste.
|
| 47 |
+
|
| 48 |
+
## Architecture
|
| 49 |
+
|
| 50 |
+
The app is a single **Gradio 6** Blocks UI mounted alongside a **FastAPI** app on one port, so the same Space serves the UI, a `/agent` endpoint, and an MCP server.
|
| 51 |
+
|
| 52 |
+
The model never decides *whether two events clash* — it decides **what** the events are; deterministic interval math decides **when they conflict**. The model emits a single constrained JSON object (a pydantic `ActionPlan`), grammar-constrained so it's always valid:
|
| 53 |
+
|
| 54 |
+
```python
|
| 55 |
+
class Event(BaseModel):
|
| 56 |
+
title: str
|
| 57 |
+
start: str # ISO 8601
|
| 58 |
+
end: str | None = None
|
| 59 |
+
location: str | None = None
|
| 60 |
+
reminder_minutes: int | None = 30
|
| 61 |
+
|
| 62 |
+
class ActionPlan(BaseModel):
|
| 63 |
+
reasoning: str
|
| 64 |
+
events: list[Event] = []
|
| 65 |
+
conflicts: list[Conflict] = []
|
| 66 |
+
proposed_times: list[str] = []
|
| 67 |
+
reply_draft: str = ""
|
| 68 |
+
needs_clarification: bool = False
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
Inference sits behind one seam — an `INFERENCE_BASE_URL` env var. Unset, the Space loads a GGUF and runs it in-process via llama.cpp. Set, it points at any OpenAI-compatible `llama-server` (e.g. one running on your own machine). Same app, two deployment shapes.
|
| 72 |
+
|
| 73 |
+
A nice consequence: a **stub extractor** (`USE_STUB_EXTRACTOR=1`, a tiny regex heuristic) lets the entire app, the test suite, and the demo run with **no GPU at all**. The expensive dependency is optional from day one — which means the cheap path doubles as the CI harness and the free-tier preview.
|
| 74 |
+
|
| 75 |
+
## Fine-tuning Gemma 4 for calendars
|
| 76 |
+
|
| 77 |
+
The fun part. We fine-tuned **Gemma 4** to read messy chats into the `ActionPlan` schema, and — just as importantly — built the **machinery to know whether a fine-tune is actually any good**.
|
| 78 |
+
|
| 79 |
+
**The pipeline.** QLoRA via Unsloth (4-bit, r=16) → merge → `convert_hf_to_gguf.py` → `llama-quantize Q4_K_M` → published as [`ParetoOptimal/gemma-4-cal-gguf`](https://huggingface.co/ParetoOptimal/gemma-4-cal-gguf). Training data is hand-authored examples plus a **SMCalFlow** importer (Microsoft Semantic Machines, CC BY-SA 4.0) that resolves LISP "dataflow" calendar programs into concrete, self-consistent datetimes — directly training the relative-date skill ("next Thursday", "the 14th") against a spread of 2026 reference dates.
|
| 80 |
+
|
| 81 |
+
**The real win: an eval gate.** Generic benchmarks (MMLU, etc.) don't apply to a narrow extraction task, so we wrote a task-specific scorer (`training/eval.py`) over a held-out set, and made it the **publishing process** (`training/gated_retrain.py`):
|
| 82 |
+
|
| 83 |
+
> Retrain → upload to a **staging** filename → score on the held-out eval → **gate**
|
| 84 |
+
> (`schema_validity ≥ 0.95`, `event_f1 ≥ 0.81`, `start-exact recall ≥ 0.773`) →
|
| 85 |
+
> **PASS** promotes staging to production (a free server-side copy); **FAIL** deletes staging, production untouched.
|
| 86 |
+
|
| 87 |
+
No retrain reaches users unless it beats the current best. That gate has already earned its keep — it caught regressions on bigger retrains and refused to ship them, so production was never degraded by an experiment.
|
| 88 |
+
|
| 89 |
+
Here's the fine-tune's scorecard (Q4_K_M, n=28), with the base model as context:
|
| 90 |
+
|
| 91 |
+
| Metric | Fine-tune `gemma-4-cal` | Base Gemma 4 (context) |
|
| 92 |
+
| --- | --- | --- |
|
| 93 |
+
| schema validity | 1.00 | 1.00 |
|
| 94 |
+
| no-event accuracy | 1.00 | 1.00 |
|
| 95 |
+
| **clarification recall** | **1.00** | 0.75 |
|
| 96 |
+
| end-time exact | 1.00 | 1.00 |
|
| 97 |
+
| event precision | 0.85 | 1.00 |
|
| 98 |
+
| start-exact recall | 0.77 | 0.955 |
|
| 99 |
+
| event F1 | 0.81 | 0.977 |
|
| 100 |
+
|
| 101 |
+
The standout is **clarification discipline** — the fine-tune reliably asks when a thread is "date TBD" instead of guessing, where the base model sometimes plows ahead. The base model is genuinely strong on this narrow task, so rather than argue about it, **we let the eval decide what production serves** — and we keep the published fine-tune as our Well-Tuned artifact.
|
| 102 |
+
|
| 103 |
+
**Where fine-tuning shines: the edge model.** We re-aimed the effort at a **Gemma 4 E4B (~4B)** model that runs on the **free CPU tier** — no paid GPU. Six gated runs, each fixing a diagnosed miss, with schema validity holding at **1.0 the entire time**. On an expanded 60-example eval, our fine-tuned E4B landed at **F1 0.97 / recall 0.96 / clarification 1.0** — an exact statistical tie with stock E4B (identical 48/1/2 true/false/missed), at **zero quality cost** and on a model small enough to run on a laptop. An own, local, parity fine-tune is exactly the "small + well-tuned" sweet spot the constraint rewards.
|
| 104 |
+
|
| 105 |
+
Two honest notes the eval forced us to write down:
|
| 106 |
+
- **Quantization was exonerated.** We tested the same weights at f16 / Q8 / Q4 — precision wasn't the bottleneck. Good to know before chasing a fix that wouldn't have helped.
|
| 107 |
+
- **Both the base and the fine-tune are ≤ 32B** (31B and ~4B), satisfying the size constraint, and the "no cloud AI" rule applies to the *running app* — dataset prep can use any offline tooling.
|
| 108 |
+
|
| 109 |
+
## Off the grid: llama.cpp in a Space
|
| 110 |
+
|
| 111 |
+
"Local-first" meant: inference runs **in the Space**, not via a hosted LLM API. We started on ZeroGPU but pivoted to a **Docker Space** built on the official `ggml-org/llama.cpp` image — `llama-server` is always warm, and (crucially) we don't pay a multi-minute build cost compiling llama.cpp from source on the free builders. The big 31B GGUF wants a GPU; the **E4B variant runs on the free CPU tier**, which is the honest "runs on a laptop" story.
|
| 112 |
+
|
| 113 |
+
The whole point: your messages never touch a third-party AI service. The default output is a file you own.
|
| 114 |
+
|
| 115 |
+
## From app to agent: MCP, memory, and shared traces
|
| 116 |
+
|
| 117 |
+
**It's an MCP server.** Gradio's `mcp_server=True` turns typed functions into tools any MCP-aware client (Claude Desktop, Cursor, …) can call — so a larger agent can use scheduling as an **off-grid skill**:
|
| 118 |
+
|
| 119 |
+
```python
|
| 120 |
+
def extract_events(thread: str, images: list[str] | None = None) -> dict:
|
| 121 |
+
"""Extract calendar events from a chat thread (and optional screenshots).
|
| 122 |
+
|
| 123 |
+
Args:
|
| 124 |
+
thread: the conversation text to scan for events.
|
| 125 |
+
images: optional base64 data-URIs of screenshots.
|
| 126 |
+
Returns: an ActionPlan (events, conflicts, proposed_times, reply_draft).
|
| 127 |
+
"""
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
…plus `make_ics(events)` and `check_conflicts(events, ics_base64)`. The MCP schema is generated straight from the type hints and docstrings.
|
| 131 |
+
|
| 132 |
+
**Memory that grows with you, on *your* device.** Facts like *"Dana is the soccer coach"* or *"you decline Mondays"* personalize every extraction. They live in your browser's **localStorage** — per-user by default, never on a server — and are injected into the prompt at run time. You can seed them with a 10-second onboarding, or import contacts (`.vcf`/CSV) and a calendar (`.ics`), parsed locally.
|
| 133 |
+
|
| 134 |
+
**Sharing is Caring.** One click publishes a **redacted** run trace to a public dataset, [`offgridschedula-traces`](https://huggingface.co/datasets/ParetoOptimal/offgridschedula-traces), for others to learn from. The trace is structural by design — counts and stage names, never your chat text — and redaction is forced for anything public.
|
| 135 |
+
|
| 136 |
+
## Lessons learned
|
| 137 |
+
|
| 138 |
+
A field-notes dump of things that cost us time so they don't cost you yours:
|
| 139 |
+
|
| 140 |
+
> **Gradio 6 ignores `css=`/`js=` set on `gr.Blocks` when it's mounted under FastAPI.** Custom styling silently doesn't apply. The fix that stuck: inject the CSS at mount time and the JS as a real inline `<script>` before `</body>` via middleware, so it always executes:
|
| 141 |
+
> ```python
|
| 142 |
+
> html = html.replace("</body>", f"<script>({CAROUSEL_JS})()</script></body>")
|
| 143 |
+
> ```
|
| 144 |
+
|
| 145 |
+
> **`display:none` ≠ "responsive."** Gradio's tab strip needed explicit CSS to hide on narrow screens; our nav buttons click the *real* hidden Gradio tabs underneath. A `MutationObserver` re-wires everything after each Gradio swap so dynamic updates don't break it.
|
| 146 |
+
|
| 147 |
+
> **Make the expensive dependency optional on day one.** The stub extractor + lazy imports meant the same codebase is our test harness, our offline demo, and our free tier — all at once.
|
| 148 |
+
|
| 149 |
+
> **The HF MCP badge is Gradio-SDK only.** A pure FastAPI app or a bare `llama-server` won't advertise MCP; you need Gradio's `mcp_server=True` and typed, docstringed tool functions.
|
| 150 |
+
|
| 151 |
+
> **Traces are redacted by construction, not after the fact.** The activity bus only ever emits counts and short status strings; the one free-text field (a chat name) is stripped. An allowlist beats a denylist when the cost of a leak is someone's private calendar.
|
| 152 |
+
|
| 153 |
+
> **Be honest about "off the grid."** A 31B GGUF (~18–20 GB) wants a GPU — and "your own cloud GPU" is easy to conflate with "a cloud AI API." We kept the headline model honest and shipped the E4B edge variant as the truly-local path.
|
| 154 |
+
|
| 155 |
+
And the meta-lesson: **an eval that gates publishing changes how you work.** Once "does it beat the bar?" is automated, you stop arguing about vibes and start shipping only what's measurably better — or, as happened here, you learn that a strong prompt on a capable base is sometimes the right answer, and you keep your fine-tune for where it actually wins.
|
| 156 |
+
|
| 157 |
+
## What's next
|
| 158 |
+
|
| 159 |
+
- Polish the optional Mac collector (reading `~/Library/Messages/chat.db`, including the `attributedBody` blobs modern Messages uses).
|
| 160 |
+
- Grow the on-device memory beyond contacts and preferences.
|
| 161 |
+
- Push the E4B edge model further so the fully-local path matches the 31B headline on the hardest relative-date cases.
|
| 162 |
+
|
| 163 |
+
## Get started
|
| 164 |
+
|
| 165 |
+
- **Try it (30 seconds, no install):** open the [Space](https://huggingface.co/spaces/ParetoOptimal/OffGridSchedula), tap **Try a sample**, and watch a chat become events + a conflict check + a reply.
|
| 166 |
+
- **The model:** [`ParetoOptimal/gemma-4-cal-gguf`](https://huggingface.co/ParetoOptimal/gemma-4-cal-gguf) (QLoRA fine-tune of Gemma 4, GGUF for llama.cpp).
|
| 167 |
+
- **The traces dataset:** [`ParetoOptimal/offgridschedula-traces`](https://huggingface.co/datasets/ParetoOptimal/offgridschedula-traces).
|
| 168 |
+
- **Use it as a tool:** add the Space as an MCP server and call `extract_events` from your own agent.
|
| 169 |
+
|
| 170 |
+
## Acknowledgements & citation
|
| 171 |
+
|
| 172 |
+
Built on the shoulders of [Gemma 4](https://huggingface.co/google), [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp), [Gradio](https://www.gradio.app/), and [Unsloth](https://github.com/unslothai/unsloth). Training data uses **SMCalFlow** (Semantic Machines et al., *"Task-Oriented Dialogue as Dataflow Synthesis,"* TACL 2020; CC BY-SA 4.0).
|
| 173 |
+
|
| 174 |
+
```bibtex
|
| 175 |
+
@misc{offgridschedula2026,
|
| 176 |
+
title = {OffGridSchedula: a local-first, fine-tuned scheduling agent},
|
| 177 |
+
author = {ParetoOptimal},
|
| 178 |
+
year = {2026},
|
| 179 |
+
howpublished = {\url{https://huggingface.co/spaces/ParetoOptimal/OffGridSchedula}}
|
| 180 |
+
}
|
| 181 |
+
```
|
docs/build-small-submission.md
CHANGED
|
@@ -10,12 +10,12 @@ Tags claimed in the README frontmatter use the field guide's namespaced taxonomy
|
|
| 10 |
|
| 11 |
| # | Rule | Status | Evidence |
|
| 12 |
|---|------|--------|----------|
|
| 13 |
-
| 1 | Every model under 32B parameters | ✅ |
|
| 14 |
-
| 2 | Gradio app, hosted as an HF Space (Docker OK) | ✅ | [`app.py`](../app.py) is a Gradio Blocks app served from a Docker SDK Space running llama.cpp
|
| 15 |
-
| 3 | Demo video |
|
| 16 |
-
| 4 | Social media post, linked from the README |
|
| 17 |
| 5 | ≤ 10 ZeroGPU apps per user | ✅ n/a | Runs on cpu-basic (stub preview) or a dedicated T4 — no ZeroGPU dependency. |
|
| 18 |
-
| 6 | README frontmatter tags + short write-up of idea & tech | ✅ |
|
| 19 |
|
| 20 |
## Track — `track:backyard` (Backyard AI)
|
| 21 |
|
|
@@ -36,33 +36,30 @@ Modal powered the **development** of the platform's model end-to-end:
|
|
| 36 |
|
| 37 |
## Sponsor prize — `sponsor:openbmb` (Best MiniCPM Build)
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
multi-step agent ([`server/orchestrator.py`](../server/orchestrator.py)) — MiniCPM is core to the
|
| 43 |
-
agent experience, not a garnish
|
| 44 |
-
isn't configured). Also the natural evidence for the judged **Best Agent** award.
|
| 45 |
|
| 46 |
## Achievement badges (self-declared, all claimed)
|
| 47 |
|
| 48 |
| Tag | Badge | Evidence |
|
| 49 |
|-----|-------|----------|
|
| 50 |
| `achievement:offgrid` | Off the Grid | All inference runs inside the Space via llama.cpp — no cloud AI APIs. The only optional outbound call is the user's own Google Calendar push. |
|
| 51 |
-
| `achievement:welltuned` | Well-Tuned | [`
|
| 52 |
| `achievement:offbrand` | Off-Brand | Custom landing page, grouped nav, dark hero + carousel, elevated tool card, bespoke CSS/JS ([`ui/blocks.py`](../ui/blocks.py), [`static/app.css`](../static/app.css)) — far past the stock Gradio look. |
|
| 53 |
| `achievement:llama` | Llama Champion | The official `ghcr.io/ggml-org/llama.cpp` server image runs the GGUF + vision mmproj ([`Dockerfile`](../Dockerfile), [`scripts/start_space.sh`](../scripts/start_space.sh)). |
|
| 54 |
| `achievement:sharing` | Sharing is Caring | Redacted agent traces published to the public dataset [`ParetoOptimal/offgridschedula-traces`](https://huggingface.co/datasets/ParetoOptimal/offgridschedula-traces) — one-click from the Activity tab, or [`training/share_trace.py`](../training/share_trace.py). |
|
| 55 |
-
| `achievement:fieldnotes` | Field Notes | [`FIELD_NOTES.md`](../FIELD_NOTES.md) (build retrospective) + [`docs/blog-eval-gated-finetuning.md`](./blog-eval-gated-finetuning.md) (fine-tuning post-mortem) +
|
| 56 |
|
| 57 |
-
Sponsor prizes **not** claimed: OpenAI Codex (no Codex-attributed commits)
|
| 58 |
-
(different model family). The cash bonus badges (Off Brand, Tiny Titan, Best
|
| 59 |
-
Bonus Quest Champion, Judges' Wildcard) are judged across all submissions and
|
|
|
|
| 60 |
|
| 61 |
-
##
|
| 62 |
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
- The model is published at [`build-small-hackathon/gemma-4-cal-gguf`](https://huggingface.co/build-small-hackathon/gemma-4-cal-gguf) (the planner is [`openbmb/MiniCPM5-1B-GGUF`](https://huggingface.co/openbmb/MiniCPM5-1B-GGUF)).
|
| 67 |
-
- The [demo video](https://youtu.be/m-o0u9X3tI4) and the social posts (X + LinkedIn) are published and linked from the README.
|
| 68 |
-
- The write-up is live as a Hugging Face blog post: [build-small-hackathon/offgridschedula](https://huggingface.co/blog/build-small-hackathon/offgridschedula).
|
|
|
|
| 10 |
|
| 11 |
| # | Rule | Status | Evidence |
|
| 12 |
|---|------|--------|----------|
|
| 13 |
+
| 1 | Every model under 32B parameters | ✅ | The platform's one and only model is [`gemma-cal` E4B](https://huggingface.co/ParetoOptimal/gemma-4-cal-gguf) (~4B effective parameters, ~5 GB GGUF at Q4) — far under the cap. |
|
| 14 |
+
| 2 | Gradio app, hosted as an HF Space (Docker OK) | ✅ / ⚠️ | [`app.py`](../app.py) is a Gradio Blocks app served from a Docker SDK Space running llama.cpp. ⚠️ Still to do: upload the Space **inside the `build-small-hackathon` org** (it currently lives in `ParetoOptimal`). |
|
| 15 |
+
| 3 | Demo video | ⚠️ to record | Script/storyboard ready in [`docs/demo-script.md`](./demo-script.md); video link goes in the README once recorded. |
|
| 16 |
+
| 4 | Social media post, linked from the README | ⚠️ to post | Drafts ready in [`docs/social-post.md`](./social-post.md); link goes in the README once published. |
|
| 17 |
| 5 | ≤ 10 ZeroGPU apps per user | ✅ n/a | Runs on cpu-basic (stub preview) or a dedicated T4 — no ZeroGPU dependency. |
|
| 18 |
+
| 6 | README frontmatter tags + short write-up of idea & tech | ✅ | Tags + write-up added to [`README.md`](../README.md) (this change). |
|
| 19 |
|
| 20 |
## Track — `track:backyard` (Backyard AI)
|
| 21 |
|
|
|
|
| 36 |
|
| 37 |
## Sponsor prize — `sponsor:openbmb` (Best MiniCPM Build)
|
| 38 |
|
| 39 |
+
The **Agent tab** is planned by **OpenBMB MiniCPM** (`openbmb/MiniCPM4.1-8B-GGUF`, Q4; the ≤4B
|
| 40 |
+
`MiniCPM5-1B` variant is a config switch) served by a second local llama.cpp instance. It drives
|
| 41 |
+
this Space's own MCP tools (`extract_events` → `check_conflicts` → `make_ics`) as a visible
|
| 42 |
multi-step agent ([`server/orchestrator.py`](../server/orchestrator.py)) — MiniCPM is core to the
|
| 43 |
+
agent experience, not a garnish. Also the natural evidence for the judged **Best Agent** award.
|
|
|
|
| 44 |
|
| 45 |
## Achievement badges (self-declared, all claimed)
|
| 46 |
|
| 47 |
| Tag | Badge | Evidence |
|
| 48 |
|-----|-------|----------|
|
| 49 |
| `achievement:offgrid` | Off the Grid | All inference runs inside the Space via llama.cpp — no cloud AI APIs. The only optional outbound call is the user's own Google Calendar push. |
|
| 50 |
+
| `achievement:welltuned` | Well-Tuned | [`ParetoOptimal/gemma-4-cal-gguf`](https://huggingface.co/ParetoOptimal/gemma-4-cal-gguf) — our published QLoRA fine-tune of Gemma-4 E4B **is the model production serves**, shipped through the eval gate with the [honest scorecard public](./eval-roadmap.md). |
|
| 51 |
| `achievement:offbrand` | Off-Brand | Custom landing page, grouped nav, dark hero + carousel, elevated tool card, bespoke CSS/JS ([`ui/blocks.py`](../ui/blocks.py), [`static/app.css`](../static/app.css)) — far past the stock Gradio look. |
|
| 52 |
| `achievement:llama` | Llama Champion | The official `ghcr.io/ggml-org/llama.cpp` server image runs the GGUF + vision mmproj ([`Dockerfile`](../Dockerfile), [`scripts/start_space.sh`](../scripts/start_space.sh)). |
|
| 53 |
| `achievement:sharing` | Sharing is Caring | Redacted agent traces published to the public dataset [`ParetoOptimal/offgridschedula-traces`](https://huggingface.co/datasets/ParetoOptimal/offgridschedula-traces) — one-click from the Activity tab, or [`training/share_trace.py`](../training/share_trace.py). |
|
| 54 |
+
| `achievement:fieldnotes` | Field Notes | [`FIELD_NOTES.md`](../FIELD_NOTES.md) (build retrospective) + [`docs/blog-eval-gated-finetuning.md`](./blog-eval-gated-finetuning.md) (fine-tuning post-mortem) + [`docs/blog-offgridschedula.md`](./blog-offgridschedula.md). |
|
| 55 |
|
| 56 |
+
Sponsor prizes **not** claimed: OpenAI Codex (no Codex-attributed commits), OpenBMB MiniCPM and
|
| 57 |
+
NVIDIA Nemotron (different model family). The cash bonus badges (Off Brand, Tiny Titan, Best
|
| 58 |
+
Demo, Best Agent, Bonus Quest Champion, Judges' Wildcard) are judged across all submissions and
|
| 59 |
+
take no tags.
|
| 60 |
|
| 61 |
+
## Remaining before the June 15 deadline
|
| 62 |
|
| 63 |
+
1. **Upload the Space into the `build-small-hackathon` org** (hard rule #2 — the ParetoOptimal Space doesn't count as a submission).
|
| 64 |
+
2. Record the demo video (script in [`docs/demo-script.md`](./demo-script.md)), host it publicly, link it from the README.
|
| 65 |
+
3. Publish the social post (drafts in [`docs/social-post.md`](./social-post.md)), link it from the README.
|
|
|
|
|
|
|
|
|
docs/demo-script.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OffGridSchedula — 95-second demo video script & storyboard
|
| 2 |
+
|
| 3 |
+
**Goal:** show a real busy parent turn a messy group chat into calendar events — locally, on a small
|
| 4 |
+
model — and name every claimed bonus quest out loud at its evidence moment. "Show, tell the judges."
|
| 5 |
+
|
| 6 |
+
> Recording it? The full operator's runbook — environment prep, demo-calendar
|
| 7 |
+
> setup, the talk track and the reasoning behind each line —
|
| 8 |
+
> is in **[demo-walkthrough.md](./demo-walkthrough.md)**.
|
| 9 |
+
|
| 10 |
+
**Model:** `ParetoOptimal/gemma-4-cal-gguf` — our fine-tune of **Gemma E4B** (~4B effective
|
| 11 |
+
params, far under the 32B cap), run via llama.cpp. (The 31B gguf in the repo is a legacy
|
| 12 |
+
artifact; the platform serves the E4B.)
|
| 13 |
+
|
| 14 |
+
**Bonus quests called out on camera** (field-guide names, verbatim): **Off the Grid** ·
|
| 15 |
+
**Well-Tuned** · **Llama Champion** · **Sharing is Caring** · **Off-Brand**. The sixth claimed
|
| 16 |
+
quest, **Field Notes**, lives on the end card + README (it's a write-up, not a screen).
|
| 17 |
+
|
| 18 |
+
| # | Time | Shot (visual) | Voiceover / on-screen text |
|
| 19 |
+
|---|------|---------------|----------------------------|
|
| 20 |
+
| 1 | 0:00–0:08 | Phone screen: a noisy class group chat scrolling ("Picture day Thursday 9am", "practice moved to Tuesday", "RSVP by Friday"). | "Every parent knows this chat. The dates are in here — and they're easy to miss." |
|
| 21 |
+
| 2 | 0:08–0:20 | Open the Space in a phone browser. Hero: *Catch every invite before it slips away.* Zoom the 🔒 badge. | "OffGridSchedula. No app, no account — a small model runs right in the Space. No cloud APIs anywhere: that's the **Off the Grid** quest — the whole thing runs on the model in front of you." |
|
| 22 |
+
| 3 | 0:20–0:34 | Paste the chat → tap **Find the events**. The "thinking" panel streams; flash the HF model card. | "Paste the chat. The model reading it is **Well-Tuned** — our own fine-tune of Gemma, published on Hugging Face with its eval scorecard — running through the llama.cpp runtime: **Llama Champion**." |
|
| 23 |
+
| 4 | 0:34–0:48 | Results: event cards (Picture Day · Thu 9:00, Practice · Tue 5:00), a ⚠️ conflict badge, free-time chips. | "It pulls out the events, checks them against your calendar, and catches the clash — before anything is saved." |
|
| 24 |
+
| 5 | 0:48–0:58 | Screenshot/flyer upload → an event appears from the image. | "A flyer or screenshot works too — the same local model reads the date, time and place from the picture." |
|
| 25 |
+
| 6 | 0:58–1:10 | Copy the ready-to-send reply; tap **Download .ics**; event drops into the calendar. | "It even drafts the reply. Then the events drop into your calendar — Google, Apple, anything that takes a .ics." |
|
| 26 |
+
| 7 | 1:10–1:22 | Activity tab: live pipeline → click **share trace** → the public Hub dataset page. | "Every agent run is observable — and one click publishes a redacted trace to a public dataset for anyone to learn from. **Sharing is Caring**: the open-trace quest." |
|
| 27 |
+
| 8 | 1:22–1:35 | Slow pan over the custom landing page → **🏆 Submission** tab scorecard (all six quests green) → end card with Space URL. | "And everything you've watched is a custom frontend pushed far past the default Gradio look — **Off-Brand**. Local-first, fine-tuned, llama.cpp, open traces, custom UI — built small, by the rules. Try it free — link below." |
|
| 28 |
+
|
| 29 |
+
**B-roll / cutaways:** the rotating use-case cards (carpool, appointments, parties); the Activity tab's
|
| 30 |
+
"time saved this week" tiles.
|
| 31 |
+
|
| 32 |
+
**Capture tips:** record the phone browser at 1080×1920; keep the cursor visible on desktop shots; show
|
| 33 |
+
the network panel offline once to prove no cloud calls; pre-load the traces dataset page in a tab
|
| 34 |
+
for shot 7.
|
docs/demo-walkthrough.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Demo video — recording walkthrough & talk track
|
| 2 |
+
|
| 3 |
+
The end-to-end runbook for recording the public demo video: environment prep,
|
| 4 |
+
the shot-by-shot walkthrough with the exact talk track, and *why each line is
|
| 5 |
+
worded the way it is*. The compact storyboard lives in
|
| 6 |
+
[demo-script.md](./demo-script.md); this document is the operator's guide for
|
| 7 |
+
actually producing it for a general-public audience (the hackathon
|
| 8 |
+
Submission-tab finale is replaced with a public closer).
|
| 9 |
+
|
| 10 |
+
## Decisions baked into this walkthrough
|
| 11 |
+
|
| 12 |
+
- **Backend: the live GPU Space.** Record against the real model — streaming
|
| 13 |
+
output, real extraction, working vision. Never record stub mode
|
| 14 |
+
(`USE_STUB_EXTRACTOR=1` output is visibly fake and vision is disabled).
|
| 15 |
+
- **Calendar: a brand-new demo Google account** (e.g. `offgridschedula.demo@`),
|
| 16 |
+
not a personal calendar. Zero PII can appear on camera — no real events, no
|
| 17 |
+
real email in the OAuth chrome — and the calendar is seeded with exactly the
|
| 18 |
+
event that produces the on-screen conflict.
|
| 19 |
+
- **The on-camera "event lands in the calendar" moment uses .ics import**, not
|
| 20 |
+
a server-side OAuth push. `credentials.json`/`token.json` must never live on
|
| 21 |
+
the public Space; importing the downloaded .ics into the demo account's
|
| 22 |
+
Google Calendar shows the same payoff with zero server credentials. (An
|
| 23 |
+
optional locally-recorded one-click-push splice is described at the end.)
|
| 24 |
+
|
| 25 |
+
## Part A — Prep (before recording day)
|
| 26 |
+
|
| 27 |
+
### A1. Demo Google account
|
| 28 |
+
1. Create the account; set a neutral profile name ("OffGrid Demo").
|
| 29 |
+
2. In its Google Calendar (local timezone), seed **one clashing event**:
|
| 30 |
+
`Dentist — Tuesday 5:00 PM`. It collides with "practice moved to Tuesday
|
| 31 |
+
5pm" in the UI's built-in sample thread, which is what makes the ⚠️ conflict
|
| 32 |
+
badge fire on camera. Add 1–2 filler events so the calendar looks lived-in.
|
| 33 |
+
3. Export the calendar (Settings → Import & export → Export) and save the
|
| 34 |
+
unzipped file as `demo-existing-calendar.ics` — this gets uploaded in the UI.
|
| 35 |
+
|
| 36 |
+
### A2. Assets
|
| 37 |
+
- **Flyer image** for the vision shot: a simple school-event flyer (title,
|
| 38 |
+
date, time, place) as PNG/JPEG — *not HEIC* (the vision pipeline can't decode
|
| 39 |
+
it), under 4 MB.
|
| 40 |
+
- **Staged iMessage chat** for the opening phone shot: stage a group chat with
|
| 41 |
+
demo contacts containing the storyboard lines ("Picture day Thursday 9am",
|
| 42 |
+
"practice moved to Tuesday 5", "RSVP by Friday") and screen-record it
|
| 43 |
+
scrolling. Never show a real chat.
|
| 44 |
+
|
| 45 |
+
### A3. Space
|
| 46 |
+
1. Put the Docker Space on a GPU tier (T4 is enough for E4B) and **restart
|
| 47 |
+
it** — restarting also clears `/tmp`, so no previously-ingested feed data is
|
| 48 |
+
visible anywhere in the UI.
|
| 49 |
+
2. Confirm `USE_STUB_EXTRACTOR` is unset and `MMPROJ_FILE` is set (vision).
|
| 50 |
+
3. Dry-run after llama-server health is green: sample thread → real streamed
|
| 51 |
+
output; upload `demo-existing-calendar.ics` → conflict badge; flyer → event
|
| 52 |
+
from image.
|
| 53 |
+
4. Relative dates resolve from the **server clock (UTC)** — check the dry-run
|
| 54 |
+
dates land on the expected weekdays and adjust the seeded Dentist event if
|
| 55 |
+
needed.
|
| 56 |
+
|
| 57 |
+
### A4. Recording
|
| 58 |
+
- 1080p+ screen capture (QuickTime / OBS) in a **clean browser profile** logged
|
| 59 |
+
into the demo Google account only.
|
| 60 |
+
- Record the voiceover separately after screen capture — cleaner than
|
| 61 |
+
narrating live, and the talk track below is written to be read.
|
| 62 |
+
|
| 63 |
+
## Part B — Shot list with talk track
|
| 64 |
+
|
| 65 |
+
| # | Time | Screen | Action | Talk track |
|
| 66 |
+
|---|------|--------|--------|------------|
|
| 67 |
+
| 1 | 0:00–0:08 | iPhone clip | Staged class group chat scrolls past | "Every parent knows this chat. The dates are in here — and they're easy to miss." |
|
| 68 |
+
| 2 | 0:08–0:20 | Browser | Open the Space; hero + badges visible; zoom the 🔒 badge | "OffGridSchedula. No app, no account — a small model runs right in the Space. No cloud APIs anywhere: that's the **Off the Grid** quest — local-first, the whole thing runs on the model in front of you." |
|
| 69 |
+
| 3 | 0:20–0:34 | Schedule flow | Upload `demo-existing-calendar.ics` first, then tap **Try a sample**; thinking panel streams; flash the model card link | "Paste the chat. The model reading it is **Well-Tuned** — our own fine-tune of Gemma, published on Hugging Face with its eval scorecard — and it runs through the llama.cpp runtime: **Llama Champion**." |
|
| 70 |
+
| 4 | 0:34–0:48 | Results | Event cards appear; ⚠️ conflict vs Dentist; free-slot chips | "It pulls out the events, checks your calendar, and catches the clash — before anything is saved." |
|
| 71 |
+
| 5 | 0:48–0:58 | Vision | Start over → drop in the flyer PNG → event extracted from the image | "A flyer or screenshot works too — the same local model reads the date, time and place right off the picture." |
|
| 72 |
+
| 6 | 0:58–1:10 | Export | Copy the drafted reply; **Download .ics**; switch tabs and import it into the demo Google Calendar — events appear on the grid | "It drafts the reply, and the events drop straight into your calendar — Google, Apple, anything that takes a .ics." |
|
| 73 |
+
| 7 | 1:10–1:22 | Activity | Activity tab's live pipeline; click **share trace**; the public dataset page flashes | "Every agent run is observable — and one click publishes a redacted trace to a public Hub dataset for anyone to learn from. That's **Sharing is Caring**: the open trace quest." |
|
| 74 |
+
| 8 | 1:22–1:35 | Closer | Slow pan over the custom landing page → end card with the Space URL | "And everything you've watched — the hero, the cards, that dashboard — is a custom frontend pushed far past the default Gradio look: **Off-Brand**. Open source, runs locally, your messages never leave the machine. Try it free — link below." |
|
| 75 |
+
|
| 76 |
+
Tip for shot 6: pre-open the demo calendar's import dialog in a second tab so
|
| 77 |
+
the on-camera import is two clicks. Tip for shot 7: have the
|
| 78 |
+
`ParetoOptimal/offgridschedula-traces` dataset page pre-loaded in a tab so the
|
| 79 |
+
"published" beat is one cut, not a page load.
|
| 80 |
+
|
| 81 |
+
## Why the talk track is worded this way
|
| 82 |
+
|
| 83 |
+
- **Shot 1 names the pain before the product.** "Every parent knows this chat"
|
| 84 |
+
earns the next 60 seconds from a general audience that has never heard of
|
| 85 |
+
local LLMs — the hook is the missed pickup, not the model.
|
| 86 |
+
- **The bonus quests are named out loud, verbatim, at their evidence moments.**
|
| 87 |
+
This track serves two audiences at once: the public hook stays in shots 1
|
| 88 |
+
and 4–6, while the field guide's quest names — **Off the Grid** (local-first,
|
| 89 |
+
no cloud APIs), **Well-Tuned** (published fine-tune), **Llama Champion**
|
| 90 |
+
(llama.cpp runtime), **Sharing is Caring** (open trace on the Hub), and
|
| 91 |
+
**Off-Brand** (custom UI past stock Gradio) — are each spoken exactly when
|
| 92 |
+
the screen shows the proof. Judges score the video against those names;
|
| 93 |
+
making them explicit means no claimed quest can be missed on a single watch.
|
| 94 |
+
(The sixth claimed quest, **Field Notes**, is print, not screen — it lives on
|
| 95 |
+
the end card and in the README, not the voiceover.)
|
| 96 |
+
- **Shot 2 still kills the two adoption objections immediately** ("no app, no
|
| 97 |
+
account") before the first quest callout — the badge language is framed as a
|
| 98 |
+
plain-English promise ("the whole thing runs on the model in front of you"),
|
| 99 |
+
so the callout lands for laypeople too.
|
| 100 |
+
- **Shot 4's "before anything is saved"** is the trust beat — the agent
|
| 101 |
+
proposes, the human disposes. This line pre-empts the "AI touched my
|
| 102 |
+
calendar" fear that kills scheduling tools.
|
| 103 |
+
- **Shot 6 generalizes the payoff** ("Google, Apple, anything that takes a
|
| 104 |
+
.ics") so the demo doesn't read as Google-only — and the on-screen action
|
| 105 |
+
matches the off-grid story: a local file the user chooses to import.
|
| 106 |
+
- **Shot 8 closes on the three-word value stack** — open source, local,
|
| 107 |
+
private — and a single call to action, with the Off-Brand callout folded
|
| 108 |
+
into the visual it describes. People remember one URL and one feeling.
|
| 109 |
+
|
| 110 |
+
## Part C — Edit & publish
|
| 111 |
+
|
| 112 |
+
- Assemble: phone clip → screen capture → end card. Speed-ramp model wait time
|
| 113 |
+
but keep ~2 s of genuine streaming so it reads as live.
|
| 114 |
+
- Burn in captions (most social viewers watch muted). Export 1920×1080;
|
| 115 |
+
optionally a 1080×1920 vertical crop of shots 3–6 for Shorts/Reels.
|
| 116 |
+
- Pair with the draft copy in [social-post.md](./social-post.md); link the
|
| 117 |
+
video from the README (Build Small hard rule #3).
|
| 118 |
+
- **Pause or downgrade the GPU Space after recording** unless the closer is
|
| 119 |
+
meant to drive live traffic.
|
| 120 |
+
|
| 121 |
+
## Optional: one-click Google Calendar push splice
|
| 122 |
+
|
| 123 |
+
To show the actual **Add to Google Calendar** button instead of the .ics
|
| 124 |
+
import: run the app locally, complete the one-time OAuth flow as the *demo*
|
| 125 |
+
account to mint a local `token.json`, enable the toggle, and record just that
|
| 126 |
+
click — the UI is pixel-identical and splices cleanly. Never upload
|
| 127 |
+
`credentials.json`/`token.json` to the public Space.
|
docs/social-post.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Draft social posts — OffGridSchedula (#BuildSmall)
|
| 2 |
+
|
| 3 |
+
## X / Twitter (primary)
|
| 4 |
+
|
| 5 |
+
> Your kid's school events are buried in the class group chat — and you miss them.
|
| 6 |
+
>
|
| 7 |
+
> 🗓️ OffGridSchedula turns a pasted chat (or a flyer screenshot) into calendar events,
|
| 8 |
+
> catches conflicts, and drafts the reply.
|
| 9 |
+
>
|
| 10 |
+
> 🔒 100% local · 🦙 llama.cpp · 🎯 fine-tuned Gemma 4 (≤32B) · no cloud, no account
|
| 11 |
+
>
|
| 12 |
+
> Built for #BuildSmall 👇
|
| 13 |
+
> https://huggingface.co/spaces/ParetoOptimal/OffGridSchedula
|
| 14 |
+
|
| 15 |
+
## LinkedIn (longer)
|
| 16 |
+
|
| 17 |
+
> **The dates are always in the group chat. The misses happen anyway.**
|
| 18 |
+
>
|
| 19 |
+
> For the Build-Small hackathon I built **OffGridSchedula** — a tiny, local scheduling agent for one
|
| 20 |
+
> very specific person: a busy parent drowning in a noisy class group chat.
|
| 21 |
+
>
|
| 22 |
+
> Paste the chat (or a screenshot of a flyer/invite) and it returns the events, a conflict check
|
| 23 |
+
> against your calendar, and a ready-to-send reply — all reviewed before anything is saved. Output is a
|
| 24 |
+
> local `.ics`, with optional Google Calendar push.
|
| 25 |
+
>
|
| 26 |
+
> The honest part: it's a *small* model doing a *small* job well. Gemma 4 (31B, ≤32B — fine-tuned with
|
| 27 |
+
> QLoRA and published on the Hub), run through **llama.cpp**, **100% local — no cloud AI APIs**. Custom
|
| 28 |
+
> Gradio UI, agent traces shared as a dataset, full field notes in the repo.
|
| 29 |
+
>
|
| 30 |
+
> Try it (phone browser, no install): https://huggingface.co/spaces/ParetoOptimal/OffGridSchedula
|
| 31 |
+
>
|
| 32 |
+
> #BuildSmall #Gradio #HuggingFace #llamacpp #LocalAI #Gemma
|
| 33 |
+
|
| 34 |
+
## Alt text for the demo clip
|
| 35 |
+
A phone browser pasting a school group chat into OffGridSchedula; it extracts "Picture Day, Thu 9am"
|
| 36 |
+
and "Practice, Tue 5pm", flags a conflict, and offers a Download .ics button — all running locally.
|
server/health.py
DELETED
|
@@ -1,54 +0,0 @@
|
|
| 1 |
-
"""Liveness + a hardware-adequacy signal for the UI banner and external monitors.
|
| 2 |
-
|
| 3 |
-
`health_status()` powers both `GET /health` and the on-page status banner. It
|
| 4 |
-
reports `degraded: true` when the *real* model would run on CPU-only hardware
|
| 5 |
-
(where extraction can be slow or time out). `FORCE_DEGRADED=1` forces it on so
|
| 6 |
-
the banner can be exercised without actually changing the Space's hardware.
|
| 7 |
-
"""
|
| 8 |
-
from __future__ import annotations
|
| 9 |
-
|
| 10 |
-
import os
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
def gpu_available() -> bool:
|
| 14 |
-
"""Best-effort GPU probe. This app serves via llama.cpp (no torch), so we
|
| 15 |
-
can't rely on torch.cuda — check for an NVIDIA device or `nvidia-smi`."""
|
| 16 |
-
import glob
|
| 17 |
-
import shutil
|
| 18 |
-
import subprocess
|
| 19 |
-
|
| 20 |
-
if glob.glob("/dev/nvidia[0-9]*"):
|
| 21 |
-
return True
|
| 22 |
-
if shutil.which("nvidia-smi"):
|
| 23 |
-
try:
|
| 24 |
-
return subprocess.run(["nvidia-smi"], capture_output=True, timeout=5).returncode == 0
|
| 25 |
-
except Exception: # noqa: BLE001 any probe failure -> treat as no GPU
|
| 26 |
-
return False
|
| 27 |
-
return False
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
def health_status() -> dict:
|
| 31 |
-
"""Return liveness + the degraded/device/model signal (read at call time)."""
|
| 32 |
-
if os.environ.get("FORCE_DEGRADED") == "1":
|
| 33 |
-
return {
|
| 34 |
-
"ok": True, "device": "cpu", "model": "real", "degraded": True,
|
| 35 |
-
"reason": "Running the model on CPU-only hardware. Extraction may be slow "
|
| 36 |
-
"or time out. Upgrade to a GPU.",
|
| 37 |
-
}
|
| 38 |
-
# Stub mode is a deliberate, fast, free preview — not a degraded state.
|
| 39 |
-
if os.environ.get("USE_STUB_EXTRACTOR") == "1":
|
| 40 |
-
return {"ok": True, "device": "cpu", "model": "stub", "degraded": False, "reason": ""}
|
| 41 |
-
|
| 42 |
-
# Real model. On the Space, INFERENCE_BASE_URL points at the *local* llama-server,
|
| 43 |
-
# so local inference still depends on this host having a GPU. A non-localhost URL
|
| 44 |
-
# means inference runs elsewhere, so this host's hardware is irrelevant.
|
| 45 |
-
base = os.environ.get("INFERENCE_BASE_URL", "")
|
| 46 |
-
local = (not base) or ("127.0.0.1" in base) or ("localhost" in base)
|
| 47 |
-
gpu = gpu_available()
|
| 48 |
-
degraded = local and not gpu
|
| 49 |
-
device = "cuda" if (local and gpu) else ("cpu" if local else "remote")
|
| 50 |
-
reason = (
|
| 51 |
-
"Running the model on CPU-only hardware. Extraction may be slow or time out. "
|
| 52 |
-
"Upgrade to a GPU." if degraded else ""
|
| 53 |
-
)
|
| 54 |
-
return {"ok": True, "device": device, "model": "real", "degraded": degraded, "reason": reason}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/app.css
CHANGED
|
@@ -378,25 +378,15 @@ html { overflow-x: hidden; scroll-behavior: smooth; }
|
|
| 378 |
|
| 379 |
/* Hide the default Gradio tab strip — the banner is the only navigation now.
|
| 380 |
IMPORTANT: don't use display:none / width:0 — Gradio 6's tab bar is responsive
|
| 381 |
-
(measures
|
| 382 |
-
non-interactive clone the nav JS can't click
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
and their banner links did nothing — while desktop's ~1100px fit them all.
|
| 386 |
-
Fix: park the strip far off-screen at a FIXED LARGE width so the responsive
|
| 387 |
-
measurement always sees room for every tab on one row, no overflow menu is
|
| 388 |
-
ever built, and all tab buttons stay real and programmatically clickable. */
|
| 389 |
.gradio-container .tab-wrapper,
|
| 390 |
.gradio-container .tab-nav {
|
| 391 |
height: 0 !important; min-height: 0 !important; overflow: hidden !important;
|
| 392 |
opacity: 0 !important; margin: 0 !important; padding: 0 !important; border: 0 !important;
|
| 393 |
pointer-events: none;
|
| 394 |
-
/* KEEP the strip from ever overflowing tabs into the dead "more" clone:
|
| 395 |
-
force its measured width far past the tab count and forbid wrapping, so on
|
| 396 |
-
a phone all 7 tabs still "fit" and stay real, clickable buttons. height:0 +
|
| 397 |
-
overflow:hidden keeps it invisible; html{overflow-x:hidden} clips the
|
| 398 |
-
oversized width so it can't add a horizontal scrollbar. */
|
| 399 |
-
min-width: 1600px !important; max-width: none !important; flex-wrap: nowrap !important;
|
| 400 |
}
|
| 401 |
|
| 402 |
/* ---- hero (full-bleed DARK band, split: copy + example-card grid) ---- */
|
|
@@ -926,13 +916,10 @@ a.footer-cta:hover { transform: translateY(-2px); filter: brightness(1.07); }
|
|
| 926 |
#site-nav .nav-inner { flex-direction: column; align-items: stretch;
|
| 927 |
justify-content: flex-start; gap: 6px; padding: 8px 14px; }
|
| 928 |
.nav-brand { justify-content: flex-start; font-size: .98rem; }
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
.nav-links { width: 100%; gap: 10px 16px; flex-wrap: wrap; }
|
| 934 |
-
.nav-solo { flex: 0 0 auto; white-space: nowrap; padding: 8px 6px;
|
| 935 |
-
touch-action: manipulation; }
|
| 936 |
}
|
| 937 |
|
| 938 |
/* Activity "Activity by stage" chart shares a 2-col row with the timeline, so on
|
|
@@ -946,16 +933,3 @@ a.footer-cta:hover { transform: translateY(-2px); filter: brightness(1.07); }
|
|
| 946 |
/* Footer: keep "on Hugging Face" (its own <a> in .footer-meta) on one line
|
| 947 |
instead of breaking as "on Hugging" / "Face". */
|
| 948 |
.footer-meta a { white-space: nowrap; }
|
| 949 |
-
|
| 950 |
-
/* Hardware-degraded banner — revealed by app.py's /health probe (inline JS). */
|
| 951 |
-
#status-banner-host { padding: 0; margin: 0; }
|
| 952 |
-
.status-banner {
|
| 953 |
-
display: none;
|
| 954 |
-
padding: 10px 16px;
|
| 955 |
-
text-align: center;
|
| 956 |
-
font-weight: 600;
|
| 957 |
-
font-size: 0.95rem;
|
| 958 |
-
line-height: 1.4;
|
| 959 |
-
background: var(--accent);
|
| 960 |
-
color: #fff;
|
| 961 |
-
}
|
|
|
|
| 378 |
|
| 379 |
/* Hide the default Gradio tab strip — the banner is the only navigation now.
|
| 380 |
IMPORTANT: don't use display:none / width:0 — Gradio 6's tab bar is responsive
|
| 381 |
+
(measures clientWidth and overflows tabs into a "more" menu), and zero width
|
| 382 |
+
pushes tabs into a non-interactive clone so the nav JS can't click them.
|
| 383 |
+
Instead collapse it vertically while KEEPING its width, so measurement works,
|
| 384 |
+
all tab buttons stay in the real container, and programmatic .click() switches. */
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
.gradio-container .tab-wrapper,
|
| 386 |
.gradio-container .tab-nav {
|
| 387 |
height: 0 !important; min-height: 0 !important; overflow: hidden !important;
|
| 388 |
opacity: 0 !important; margin: 0 !important; padding: 0 !important; border: 0 !important;
|
| 389 |
pointer-events: none;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
}
|
| 391 |
|
| 392 |
/* ---- hero (full-bleed DARK band, split: copy + example-card grid) ---- */
|
|
|
|
| 916 |
#site-nav .nav-inner { flex-direction: column; align-items: stretch;
|
| 917 |
justify-content: flex-start; gap: 6px; padding: 8px 14px; }
|
| 918 |
.nav-brand { justify-content: flex-start; font-size: .98rem; }
|
| 919 |
+
.nav-links { width: 100%; gap: 16px; flex-wrap: nowrap; overflow-x: auto;
|
| 920 |
+
-webkit-overflow-scrolling: touch; scrollbar-width: none; padding-bottom: 2px; }
|
| 921 |
+
.nav-links::-webkit-scrollbar { display: none; }
|
| 922 |
+
.nav-solo { flex: 0 0 auto; white-space: nowrap; }
|
|
|
|
|
|
|
|
|
|
| 923 |
}
|
| 924 |
|
| 925 |
/* Activity "Activity by stage" chart shares a 2-col row with the timeline, so on
|
|
|
|
| 933 |
/* Footer: keep "on Hugging Face" (its own <a> in .footer-meta) on one line
|
| 934 |
instead of breaking as "on Hugging" / "Face". */
|
| 935 |
.footer-meta a { white-space: nowrap; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui/blocks.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|