[KongchangAI]
· 2 min read· 1,433 words

Building a Golden Hour Walk Planner with a 1B Model: Let AI Handle Language, Let Code Handle Math

Building a Golden Hour Walk Planner with a 1B Model: Let AI Handle Language, Let Code Handle Math

A 1B local model handles language; deterministic code handles routing — producing a printable walk card with minimal screen time.

This walk planner enforces a strict division of responsibility: local 1B–3B models only parse natural language requests and write route card copy, while all numerical computation (sunset timing, street network download, loop scoring) is handled by deterministic tools like `astral`, `osmnx`, and `networkx`. JSON Schema enforces structured output to prevent formatting errors, and a double-bracket place name validation mechanism post-hoc checks against real OSM data to catch hallucinations. Benchmarks show all three tested models stay under 0.5 GB RAM, with qwen2.5:3b performing best on pace interpretation and card accuracy. The deliverable is a large-print single-page route card and a GPX file — built around a philosophy of minimizing screen time so you can get outside faster.

A walk planner running on a local 1B model, built around a surprisingly restrained design philosophy: make the screen the shortest part of the entire experience. The developer's goal is clear — spend a few minutes at the keyboard, get a printable single-page route card and a GPX file, then pocket your phone and head out the door before sunset, stepping into a forest pierced by low-angle light.

Golden Hour Walk Planner

What makes this project worth paying attention to isn't the power of the model it uses — it's how a model with just 1B parameters can consistently produce results without "making things up." The answer is a clear division of responsibility: the model handles language, and deterministic code handles all numerical computation.

The Real Problem Worth Solving

Most "where should I walk" tools demand you stare at a screen. You open a maps app, drag around, check sun angles, pick a loop — and by the time you finally leave, the light has already moved. The developer wanted the opposite: a few minutes at the keyboard, yielding a large-print single-page route card with a mini-map, plus a GPX file ready to import into Organic Maps or OsmAnd.

The author describes a concrete real-world scenario: printing the single-page card at 17:27, with sunset at 18:07, the route leading to a nearby green space where low-angle sunlight cuts through the trees exactly where the card predicted. This "off-screen" product philosophy is the foundation of the entire project.

The Iron Rule That Makes the System Work: The Model Never Does Route Planning

This is the most critical engineering insight in the whole article. A 1B to 3B model simply cannot plan a loop route on a real street network — it will invent a road, miss a dead end, or hand you a route that cuts across a river. So the author draws a hard line:

  • The model handles language. It parses your request ("45 minutes before dark, trees, quiet") into structured parameters and writes the final card.
  • Deterministic code handles all numbers. astral calculates sunset and golden hour windows, osmnx downloads the walking street network and green space data, and networkx builds and scores loop candidates.

The full pipeline is a small LangGraph workflow: intake → sun → weather → map_fetch → route_builder → critic → card_writer → render. The critic node can send a route back for retry, relaxing constraints if the target distance isn't met. This design locks the LLM's uncertainty into a very small box.

A quick note on the tools mentioned here. osmnx is a Python library for downloading and analyzing street network data from OpenStreetMap (OSM), converting real-world road networks into computable graph structures. networkx is Python's most widely used graph computation library, used here for shortest-path and loop-finding algorithms. astral handles astronomical calculations — feed it coordinates and a date, and it returns precise times for sunrise, sunset, and the golden hour (roughly the hour before and after sunset, when the sun sits at an extreme low angle, casting orange-red directional light ideal for photography and outdoor walks). LangGraph is a directed-graph workflow framework from the LangChain ecosystem that lets developers chain multiple LLM and tool calls into branching, retryable pipelines — the critic node's ability to loop back and retry is exactly what LangGraph's stateful cycle support enables.

Reading User Requests with JSON Schema

The intake node requests structured output from a local Ollama model, ensuring that even a small model returns valid JSON:

resp = llm.chat_structured(
    model=state["model_name"],
    system="Extract walk parameters. Return JSON only.",
    user=state["user_input"],
    schema=IntakeParams.model_json_schema(),
    temperature=0.0,
    keep_alive="10m",   # keep model warm until card is written
)
intake = IntakeParams.model_validate_json(resp)

One key robustness design: if the model is unavailable or returns garbage, the node falls back to default values and logs a warning. The app always produces a card — this is classic experience-first thinking, not technical showmanship.

JSON Schema is a specification for describing JSON data structures, defining field names, types, and constraints to enforce a valid output format. When attached to an LLM prompt with temperature=0.0 (fully deterministic output), it significantly reduces the probability of malformed responses or missing fields. When running open-source models locally, Ollama supports a "structured output" mode that uses grammar-constrained decoding to force token sampling to conform to the specified schema at every step — rather than relying on the model to voluntarily follow instructions. This is the technical foundation that lets small models reliably return valid JSON at the intake stage, not just prompt engineering. keep_alive="10m" tells Ollama to keep the model weights in memory for ten minutes after the request completes, avoiding reload latency when the card_writer node makes its call later.

Writing the Card from Facts Only, with Every Place Name Verified

The card_writer prompt is deliberately tightened: the model may only use verified facts, and every place name must be wrapped in [[double brackets]] for post-hoc verification.

Use ONLY these facts. Do not invent places, landmarks, or plants.
Wrap every place name in [[double brackets]].
Start: ... | Distance: 3.4 km (~45 min) | Sunset: 2026-10-11 18:08
Golden light comes from the 271° . Weather: 29 C, 20% rain
Green spaces on the route: Allah Rakha Park, Bagh-e-Lyari

A validation step then extracts every [[name]] and checks it against the real OSM place name list:

unknown = unknown_names(card_text, state["place_names"])
if unknown:
    warnings.append(f"unknown place names: {', '.join(unknown)}")

In the author's words, this check is the dividing line between "the model wrote a beautiful card" and "the model wrote a beautiful card that sends you to a park that doesn't exist." It's a lightweight anti-hallucination guardrail — not a bet on the model's inherent reliability.

This "double-bracket validation" pattern is an example of what LLM application engineers call an "auditable output" approach. The core idea: rather than trying to prevent hallucinations at generation time (extremely hard), verify high-risk fields after generation. Place names are a classic high-risk field — models easily confuse similar real names, or synthesize a plausible-sounding name from patterns in training data. By requiring the model to mark all place names with a special token, a regex can automatically extract them and do exact comparisons against OSM data — no second LLM call needed, at near-zero cost. This "mark–extract–compare" pattern generalizes to any scenario requiring entity verification in model output: drug names, legal clause numbers, product model numbers, and so on.

Pitfalls Encountered

Honest engineering notes are often more valuable than success stories. The author candidly documents several issues:

  • 1B models do invent place names. In one run, llama3.2:1b produced a name that didn't exist in the OSM data. The double-bracket check caught it, logging a warning rather than silently delivering the card — it doesn't happen every time; the honest numbers are in the table below.
  • OSMnx loses lat/lon when saving a projected graph. GPX exports were producing empty coordinates until the loader back-projected node x/y attributes into WGS84.
  • weasyprint won't install on Windows. The author switched to headless Edge to print the HTML, treating PDF output as best-effort.

Model Comparison: Are Small Models Actually Good Enough?

Same request, same starting point, same map — only the model changes. Because the router contains no LLM, route quality is identical across runs. The interesting signal is hallucination count and card length.

ModelPace InterpretationWord CountHallucinated NamesLength ErrorPeak MemoryTime
qwen2.5:3bCorrect3600.3%259 MB106 s
qwen2.5:1.5bToo fast6904.4%260 MB16 s
llama3.2:1bToo slow5609.5%274 MB76 s
Template (no model)Correct5500.3%273 MB65 s

All three models stay well under 0.5 GB of model memory. The 3B model correctly interprets walking pace and produces the shortest, most accurate card; smaller models misread pace, which affects target distance and length error. This data makes the point: for language rewriting tasks, small models paired with strict factual constraints are entirely viable — the key is not deploying them where they don't belong.

The whole project can be summed up in one line from the author: the screen should lose this competition. The endpoint of the technology isn't more interaction — it's getting you out the door and into that perfectly timed evening light as fast as possible.

#LLM hallucination prevention#local small model#LangGraph pipeline#golden hour walk planner#osmnx route planning#structured output#1B model#GPX generation
Share:

Related articles