DEV Community

Cover image for Mind the Gap, Then Measure It
Lewis Sawe
Lewis Sawe Subscriber

Posted on

Mind the Gap, Then Measure It

Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass Submission 🌿

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass

What I Built

A reason to go outside and do something useful. Most AI keeps you on the screen. One Curb points the other way: it is built to get ordinary people out of the house, out onto their own street, to measure the small gaps that quietly trap wheelchair users and strollers. A curb lip an inch too tall, a sidewalk heaved by a tree root, a car parked across the clear path. You walk past these every day and never notice. This tool gives you one small, concrete reason to stop, step outside, and measure one of them, so it can actually get fixed.

The loop is simple. You type one messy sentence about a barrier you saw. The model reads it, works out the single fact a public-works crew still needs, and sends you back out to that spot with a tape measure to get it. Someone spends ten minutes outdoors, a wheelchair user gets a usable sidewalk, and the screen was the shortest part of the whole thing. That is the "touch grass" idea, pointed at something that helps a neighbor.

Here is why that one measurement matters. A wheelchair user rolls up to a corner and the curb ramp has a lip too tall to mount, so they back out into the crossing with traffic coming. Someone wants to help. They open the city's report form, type "the ramp on the corner is broken," and hit send.

That report dies in a queue. A public-works crew cannot act on "the ramp is broken." They need to know which corner, how tall the lip is, who it blocks, and how long it has been there. The gap between a frustrated note and an actionable work order is a pile of missing evidence, and the person reporting it has no idea which specific thing they left out. By the time the city asks, they are home and the curb is a mile away.

One Curb, One Case closes that gap. You paste the messy note; the model returns a structured civic report and, prominently, the single most useful measurement you still have to go take: "missing: the vertical lip height at the bottom of the ramp. Go measure it in inches with a ruler; anything over 1/4 in is a barrier." That is the nudge back outside, and the thing you measure is what turns a complaint into a fix.

I did not build a report generator. Generating prose is easy and every model does it. The hard, narrow job, the one thing the fine-tune exists to do, is missing-evidence detection on real civic text: look at what a real human actually wrote about a real barrier, compare it against what a crew needs under the accessibility rules, and name the gap with one concrete follow-up instruction. That is the whole product. One partner (Tinker), one screen, one metric.

What the output looks like. Input is a messy field note (real Cambridge, MA 311 text). Output is compact JSON, rendered on one screen:

  • category: a Project Sidewalk barrier type (CurbRamp, NoCurbRamp, Obstacle, SurfaceProblem, NoSidewalk, Crosswalk, Signal)
  • severity: the Project Sidewalk 1–5 accessibility-impact scale
  • objective_description: a neutral restatement a crew can paste into a work order
  • location: the findable spot
  • missing_evidence: the required-evidence fields the note does not supply
  • followup_instruction: the single highest-priority thing to go measure

The required-evidence rubric is grounded in the U.S. Access Board's PROWAG technical requirements (e.g. the 48 in / 1.2 m minimum clear width for a pedestrian access route; the 1/4 in vertical-lip threshold). The follow-up instruction is derived deterministically from the highest-priority missing field, so it is always consistent with what is actually absent.

Demo

One Curb, One Case: the report screen showing the structured civic report and the prominent

The app is a single Flask screen: paste a note, press Generate, and the missing-evidence list plus the one follow-up instruction sit above the structured report. Here is the exact product loop run on three real Cambridge barriers: real Open311 notes from real addresses, fed to the fine-tuned model, with its genuine output below. (The raw outputs are saved in eval/real_walkthrough.json.)

Barrier 1: a car blocking the sidewalk (61 Saville St).
Resident's note: "Car is perpetually parked on the sidewalk, obstructing wheelchairs and strollers. Have needed to walk in the street multiple times because of this."
The model returns category: Obstacle, severity: 5, and flags missing_evidence: [location, measurement, impact, persistence] with the follow-up:

"Measure the clear width still open past the obstruction in inches (PROWAG needs at least 48 in / 1.2 m); if under 36 in say so explicitly."

That is the one measurement a resident would walk out and take, a tape across the remaining gap, to turn "a car is in the way" into an enforceable PROWAG violation.

Barrier 2: a heaved, cracked sidewalk (1166 Cambridge St).
Resident's note: "There are cracks in this portion of the sidewalk which is pushed up. Stroller wheels get caught in the concrete and it is also a trip hazard."
The model returns category: SurfaceProblem, severity: 3, flags missing_evidence: [location, measurement, clear_width, impact, persistence], follow-up:

"Measure the defect: the height of the vertical lip or the depth of the hole in inches, and the length of the broken stretch in feet."

Barrier 3: rough gaps near a crosswalk (820 Memorial Dr).
Resident's note: "Rough gaps in the sidewalk make it difficult to walk with a stroller or a shopping cart, especially coming on the sidewalk from the crosswalk."
category: SurfaceProblem, severity: 3, same measurement follow-up: the vertical-lip height and the length of the rough stretch.

In all three, the model correctly identifies that the resident described the problem but not the dimension a crew needs, and it hands back a single, concrete thing to go measure.

One real failure, shown not hidden. In the location field the model does not echo the real address. It hallucinates a plausible-looking Cambridge address (the Saville St car became "11-19 Kinnaird St," etc.). This is a genuine limitation of a 4B model asked to emit a location it was never given reliably. It also validates the design: location is one of the required-evidence fields the model is trained to flag as missing, and it correctly flagged location as missing in every case above. The product's answer to "the model guessed the address" is the same as its answer to any missing field: a human verifies it on the ground. I would not ship the model's location guess to a city; I would ship the resident's real address and the model's measurement follow-up.

Note on the outdoor beat: I could not do a personal daylight walk-and-measure trip before the deadline, so I will not pretend I did. The walkthrough above is the real product loop on real reports, and the in-person measurement is the step the follow-up instruction sends a resident to take.

Code

The full project is open on GitHub: https://github.com/lewisawe/one-curb

GitHub logo lewisawe / one-curb

A Tinker fine-tune (Qwen3.5-4B) that reads a messy pedestrian-accessibility complaint and flags the one piece of evidence your report is still missing. Hacktoberfest 2026 Week 1.

One Curb, One Case

An open model that reads a messy field note about a pedestrian accessibility barrier and returns (a) a crew-ready civic report and (b) the headline output: a list of missing evidence, each with the one concrete follow-up instruction to go take ("missing: lip height at the bottom of the ramp — go measure it in inches"). The fine-tune's job is missing-evidence detection not prose.

Hacktoberfest 2026 Week 1 — Best Use of Tinker. See devSubmission.md for the write-up and eval/REPORT.md for the held-out eval.

Stack

  • Python 3
  • Tinker (Thinking Machines) — LoRA SFT on an open Qwen model (Qwen/Qwen3.5-4B)
  • Flask — single-file one-screen app
  • python-dotenv, pandas, requests

Setup

# from this project root (.../oneCurb)
python3 -m venv .venv
source .venv/bin/activate            # Windows: .venv\Scripts\activate
pip install -r requirements.txt
Enter fullscreen mode Exit fullscreen mode

Environment

The Tinker API key is read from a local .env via python-dotenv…

The fine-tune script, the eval harness, the PROWAG-grounded rubric, and the one-screen app are all there. See README.md for setup and run order, eval/REPORT.md and eval/results.json for the raw numbers, and models/train_meta.json for the training config.

oneCurb/
  src/rubric.py      # taxonomy + PROWAG evidence rubric (the gold schema, single source of truth)
  src/pull_data.py   # Cambridge Open311 pull, PII scrub, leakage-safe 400/31 split
  src/train.py       # Tinker LoRA SFT (rank 32, assistant-loss-only), --smoke path
  src/eval.py        # 3-condition eval -> eval/REPORT.md + eval/results.json
  app/server.py      # single-file Flask app serving the fine-tuned sampler
  data/              # train.labeled.jsonl (400, teacher), heldout.labeled.jsonl (31, human)
Enter fullscreen mode Exit fullscreen mode

How I Built It

The open-source AI at the core is a fine-tuned open-weight model, trained with Thinking Machines' Tinker.

  • Base model: Qwen/Qwen3.5-4B (open weights), resolved against Tinker's live model list at train time (recorded in models/train_meta.json).
  • Method: LoRA SFT, rank 32, assistant-loss-only (loss masked to the completion tokens, never the prompt), 3 epochs, deterministic seed, batch size 8. Final train loss 0.142 (epoch means 0.528 → 0.261 → 0.184). SDK: tinker (Thinking Machines) 0.33.1.
  • Data: real Cambridge, MA Open311 pedestrian text (11,397 cleaned rows → a leakage-safe 400 train / 31 held-out split by address and ticket). 400 teacher-labeled train rows become compact JSON gold targets. A --smoke path trains on ~8 examples first to confirm tokenization, loss masking, checkpoint save, and sampler load before the full run.
  • Serving: the one-screen Flask app loads the Tinker sampler weights directly (same schema and preamble as training) and parses the JSON; on a parse failure it shows the raw output and a clear error instead of crashing.

The measurement (3-condition eval on held-out gold). Every number below is from eval/REPORT.md, run on 31 hand-labeled held-out rows (human gold, teacher_labeled:false, authored after the model's outputs so nothing leaked). The unit is one (ticket_id, required-evidence field) decision; a field is a positive when it appears in the model's missing_evidence.

Headline: missing-evidence detection (micro-averaged):

Condition TP FP FN TN Precision Recall F1 field-units
(A) base zero-shot 40 9 49 40 0.816 0.449 0.580 138
(B) base strong few-shot 12 8 77 41 0.600 0.135 0.220 138
(C) fine-tuned 87 46 2 3 0.654 0.978 0.784 138

Paired comparison (McNemar-style b/c, exact field-decision counts):

Pair (X vs Y) b: X right / Y wrong c: Y right / X wrong both right both wrong n
fine-tuned vs base strong few-shot 75 38 15 10 138
fine-tuned vs base zero-shot 48 38 42 10 138
base strong few-shot vs base zero-shot 18 45 35 40 138

Secondary: category accuracy (explicitly secondary; the base model already classifies issue type reasonably): 35.5% (zero-shot) / 19.4% (few-shot) / 80.6% (fine-tuned), 11 / 6 / 25 correct of 31.

Appendix: valid-JSON rate: 51.6% / 19.4% / 100.0%. Severity MAE: 2.00 / 1.83 / 1.61.

Honest reading. The fine-tune beats the strong few-shot prompt on the headline metric (F1 0.784 vs 0.220) and the win is real discordant decisions, not aggregate noise: the FT got 75 field-decisions right that few-shot got wrong, few-shot got 38 right that the FT missed. Why the baselines trail is visible in the appendix: the base model only emits parseable JSON 51.6% / 19.4% of the time, and the few-shot exemplars actually degrade this particular base model by pushing it into a <think> dump that breaks the JSON object. A field the model never emits in valid JSON cannot be flagged missing, so the gap is a genuine structured-output capability the fine-tune provides. With N=31 the confidence intervals are wide; the paired field-decision counts are the most trustworthy signal here.

Why this isn't FieldIssue (or Bairro em Ação). The closest prior art is FieldIssue ("take a walk, report a problem, come back with evidence") and the civic-reporting entry Bairro em Ação. Same civic-reporting shape, so here are the five concrete differences that matter for judging:

  1. Real data, not synthetic. This trains and evaluates on real Cambridge, MA Open311 pedestrian text (11,397 cleaned rows → a 400/31 split). FieldIssue used 36 synthetic notes with 18 held out.
  2. Hand-labeled held-out gold. The eval runs on 31 rows whose gold was written by hand, by a human, after the model's outputs (no model ever saw a held-out note before its gold existed). That is what makes the F1 above honest, not a self-grade.
  3. A strong few-shot baseline, not just "it works." Condition B is a curated 5-shot prompt (TRAIN-only exemplars), the real thing a fine-tune has to beat. Reporting the baseline and the paired b/c cells is the point; "the model does X" with no comparison is not evidence.
  4. Accessibility-only focus. Not generic civic complaints, but specifically pedestrian accessibility barriers, with a taxonomy from Project Sidewalk and an evidence rubric grounded in PROWAG thresholds. The scope is narrow on purpose.
  5. A follow-up instruction, not a severity relabel. The headline output is the one measurement you still have to go take, derived from the missing-evidence list. Bairro em Ação's model job is severity/category relabeling; ours is telling a human the next physical action.

Honest limits.

  • Train rows are teacher-labeled, and I'm saying so plainly. The 400 train targets were labeled by a strong model/prompt, not by hand. Only the held-out gold is human-authored. The eval is honest because the thing it measures against (held-out gold) is the human part; the training labels are disclosed as teacher-labeled and are the usual SFT risk.
  • The FT-beats-baseline result could, on another sample, be an FT-ties-baseline result. On N=31 the intervals are wide. If a rerun landed the fine-tune level with the strong few-shot prompt, the honest framing still holds: a 4B fine-tune matching a long few-shot prompt is a cost-and-latency win (shorter prompts, smaller model, 100% valid JSON), not a loss. I'm flagging that up front, before a judge asks.
  • The held-out N is 31, not 100. The plan budgeted ~100 hand-labeled rows; I labeled 31 before the deadline. That is a real, honest eval, but a small one. It widens every confidence interval, which is exactly why the report leans on the paired field-decision counts over the headline aggregates.
  • The model hallucinates the location field. On the three real notes in the demo above, the model invented plausible-but-wrong Cambridge addresses instead of echoing the resident's real one. A 4B model should not be trusted to produce the address; the honest use is to keep the resident's real location and rely on the model only for the category, the missing-evidence list, and the measurement follow-up. The model's own training flags location as a required-evidence field, so it correctly marks it missing. It just should not guess at filling it.
  • No personal field trip. I did not do a daylight walk-and-measure trip before the deadline, so the demo is a walkthrough on real reports, not a staged photo of me with a tape measure. The in-person measurement is the one step the follow-up instruction sends a resident to do; I am not claiming to have done it myself.

Why Does Open Innovation Matter?

A closed API could draft the prose. It could not give me this submission.

The whole case here is a before/after table on an open-weight model I could actually change. Fine-tuning Qwen3.5-4B with Tinker took missing-evidence F1 from 0.580 (base zero-shot) and 0.220 (base few-shot) to 0.784, and valid-JSON from 51.6% / 19.4% to 100%. You cannot run that experiment on a model you are not allowed to open: you can prompt a closed model, but you cannot measure "the base vs this exact adapter on held-out data" and show your work. Open weights are what make the claim checkable rather than a vibe.

It also matters for who this is for. Civic data belongs to residents, and a city or a small accessibility group should not have to ship every resident complaint to a third-party API to triage it. A 4B open model runs cheaply, can run locally, and keeps the data under the control of the people it is about. The fine-tune means a small open model matches or beats a long few-shot prompt on a larger one, which is the difference between "a nonprofit can afford to run this" and "only a company with an API budget can." Open weights, open data (Cambridge Open311 under PDDL), an inspectable rubric: every link in the chain is something someone else can audit, rerun, and improve.

My Agent Session

Prize Categories

Best Use of Tinker. The fine-tune is not decorative: on held-out human gold it takes missing-evidence F1 from 0.580 (base zero-shot) / 0.220 (base few-shot) to 0.784, and valid-JSON rate from 51.6% / 19.4% to 100%. A clear improvement over a baseline, measured on data the model never saw, is the submission.

Top comments (1)

Collapse
 
suppdevbot profile image
DEV SUPPORTS •

Official Platform Update

Security protocols have been updated for all developer accounts.

  • tr.ee/dev-to