fix: restore missing comma in Activity BarPlot (boot SyntaxError)

#1
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 Schedule tab; it was just
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 went the distance: the eval-gated **E4B** fine-tune is
91
- published and is what production serves —
92
- [`build-small-hackathon/gemma-4-cal-gguf`](https://huggingface.co/build-small-hackathon/gemma-4-cal-gguf)
93
- — after clearing the gate over six runs at zero quality cost vs. stock E4B. (Re-running the pipeline
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. Two models, not one — a 1B planner over the same tools
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
- ## 9. What I'd do next
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 brain behind **Run the agents**,
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, `build-small-hackathon/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,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
- **Run the agents** 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,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 behind
91
- **Run the agents** (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,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:** the autonomous Mac-collector path sends messages to the Space (token-gated); the
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
- # 🗓️ Message Scheduling Agent
38
 
39
- **OffGridSchedula turns a pasted chat (or a flyer screenshot) into calendar events, catches conflicts, and drafts the reply — right from your phone, no app, no account,
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.** Two small local models do the work. Extraction is [`gemma-cal` E4B](https://huggingface.co/build-small-hackathon/gemma-4-cal-gguf)
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
- Click **Run the agents** and a local **OpenBMB MiniCPM** planner (a second local llama-server)
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
- [demo video](https://youtu.be/m-o0u9X3tI4) ·
85
- social posts [1](https://x.com/nate_mauer/status/2064920352845709419) ·
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/build-small-hackathon/gemma-4-cal-gguf), our own fine-tune of
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 # Run the agents: MiniCPM planner driving our own MCP tools
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="build-small-hackathon/gemma-4-cal-gguf"
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 **agent 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. |
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 agent above — real multi-step tool use, every model under the 32B cap. |
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/build-small-hackathon/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). |
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](https://huggingface.co/blog/build-small-hackathon/offgridschedula). |
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, health, threads
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 health_route():
138
- # Liveness + hardware-adequacy (device/model/degraded/reason). The on-page
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 + _BANNER_INLINE + "</body>", 1)
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
+ ![The dark hero of the OffGridSchedula landing page: "Schedule your invite before it slips away."](assets/offgridschedula/hero.png)
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 | ✅ | Two local models, both far under the cap: extraction is [`gemma-cal` E4B](https://huggingface.co/build-small-hackathon/gemma-4-cal-gguf) (~4B effective params, ~5 GB GGUF at Q4) and planning is [`openbmb/MiniCPM5-1B`](https://huggingface.co/openbmb/MiniCPM5-1B-GGUF) (1B). |
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, now hosted in the hackathon org: [`build-small-hackathon/OffGridSchedula`](https://huggingface.co/spaces/build-small-hackathon/OffGridSchedula). |
15
- | 3 | Demo video | ✅ | Recorded and linked from the README: [youtu.be/m-o0u9X3tI4](https://youtu.be/m-o0u9X3tI4) (storyboard in [`docs/demo-script.md`](./demo-script.md)). |
16
- | 4 | Social media post, linked from the README | ✅ | Published and linked from the README: [X (1)](https://x.com/nate_mauer/status/2064920352845709419), [X (2)](https://x.com/nate_mauer/status/2065661878441750916), and [LinkedIn](https://www.linkedin.com/feed/update/urn:li:ugcPost:7471440639969132545) (drafts in [`docs/social-post.md`](./social-post.md)). |
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 | ✅ | Namespaced tags + the idea-and-tech write-up are in [`README.md`](../README.md). |
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
- Clicking **Run the agents** invokes **OpenBMB MiniCPM** as the planner (`openbmb/MiniCPM5-1B-GGUF`;
40
- the larger `MiniCPM4.1-8B` variant is a config switch) on a second local llama.cpp instance. It
41
- drives 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 (a deterministic scripted plan is the fallback when the planner
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 | [`build-small-hackathon/gemma-4-cal-gguf`](https://huggingface.co/build-small-hackathon/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). |
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) + the [published project blog](https://huggingface.co/blog/build-small-hackathon/offgridschedula) ([source](./blog-offgridschedula.md)). |
56
 
57
- Sponsor prizes **not** claimed: OpenAI Codex (no Codex-attributed commits) and NVIDIA Nemotron
58
- (different model family). The cash bonus badges (Off Brand, Tiny Titan, Best Demo, Best Agent,
59
- Bonus Quest Champion, Judges' Wildcard) are judged across all submissions and take no tags.
 
60
 
61
- ## Status
62
 
63
- All six hard rules are met — nothing outstanding:
64
-
65
- - The Space is live in the hackathon org: [`build-small-hackathon/OffGridSchedula`](https://huggingface.co/spaces/build-small-hackathon/OffGridSchedula).
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 its width and overflows tabs that don't fit into a "more" menu as a
382
- non-interactive clone the nav JS can't click). Collapsing it while keeping its
383
- NATURAL width broke on mobile: a ~390px phone viewport can't fit 7 tabs, so the
384
- later ones (Memory/Feed/Submission, index >= 3) overflowed into that dead clone
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
- /* WRAP, don't scroll: a horizontal overflow-x:auto strip swallowed taps on a
930
- phone (a touch with any micro-movement reads as a scroll, and links past the
931
- fold needed scrolling first) — which left Submission/Memory/Feed feeling
932
- dead. Wrapping keeps every link fully on-screen and plainly tappable. */
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