<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Warisha Danin Bilal</title>
    <description>The latest articles on DEV Community by Warisha Danin Bilal (@warishabilal7).</description>
    <link>https://dev.to/warishabilal7</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4171114%2F9ea2a934-6cb6-4a92-b468-46c3e903729a.jpg</url>
      <title>DEV Community: Warisha Danin Bilal</title>
      <link>https://dev.to/warishabilal7</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/warishabilal7"/>
    <language>en</language>
    <item>
      <title>A golden hour walk planner that runs on a 1B model.</title>
      <dc:creator>Warisha Danin Bilal</dc:creator>
      <pubDate>Sun, 11 Oct 2026 21:50:58 +0000</pubDate>
      <link>https://dev.to/warishabilal7/a-golden-hour-walk-planner-that-runs-on-a-1b-model-kfp</link>
      <guid>https://dev.to/warishabilal7/a-golden-hour-walk-planner-that-runs-on-a-1b-model-kfp</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxxeth0zgnm3ktqniz8qw.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxxeth0zgnm3ktqniz8qw.jpg" alt=" " width="800" height="1132"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Open outside
&lt;/h2&gt;

&lt;p&gt;At 17:27 I printed the one-page card and left the door. The sunset was at 18:07. The route sent me past nearby green spaces and the low sun came through the trees right as the card suggested.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Describe the light, the printed card in your hand, and one thing the route got&lt;br&gt;
right or wrong. Example shape: &lt;em&gt;"Sunset was at 18:09. I left the house at&lt;br&gt;
17:14 with one sheet of paper. The card sent me left toward the park instead of&lt;br&gt;
right along the main road, and the low sun came through the trees exactly where&lt;br&gt;
it said it would..."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No architecture in this part. Just the walk.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Most "where should I walk" tools want you to keep looking at a screen. You open a&lt;br&gt;
map app, drag around, check the sun, pick a loop, and by the time you leave the&lt;br&gt;
light has moved. I wanted the opposite: a few minutes at the keyboard that end in&lt;br&gt;
a single printed page, and then a phone in your pocket.&lt;/p&gt;

&lt;p&gt;So the output is a one-page route card (big type, a little map) plus a GPX file&lt;br&gt;
for Organic Maps or OsmAnd. The screen is the shortest part of the experience.&lt;/p&gt;
&lt;h2&gt;
  
  
  The rule that makes it work: the model never routes
&lt;/h2&gt;

&lt;p&gt;A 1B–3B model cannot plan a loop through a real street network. It will invent a&lt;br&gt;
road, miss a dead end, and hand you a route that crosses a river. So the split is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The model does language.&lt;/strong&gt; It reads your request ("45 minutes before dark,
trees and quiet") into structured parameters, and it writes the card.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic code does everything numeric.&lt;/strong&gt; &lt;code&gt;astral&lt;/code&gt; computes sunset and
the golden-hour window, &lt;code&gt;osmnx&lt;/code&gt; downloads the walk network and green space,
&lt;code&gt;networkx&lt;/code&gt; builds and scores the loop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pipeline is a small LangGraph: &lt;code&gt;intake → sun → weather → map_fetch →&lt;br&gt;
route_builder → critic → card_writer → render&lt;/code&gt;. The &lt;code&gt;critic&lt;/code&gt; can send the route&lt;br&gt;
back for another attempt, loosening the target if it cannot hit the length.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reading the request with a JSON schema
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;intake&lt;/code&gt; node asks the local model (Ollama) for structured output, so even a&lt;br&gt;
small model returns valid JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat_structured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Extract walk parameters. Return JSON only.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;IntakeParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_json_schema&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;keep_alive&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# stay warm until the card is written
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;intake&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;IntakeParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_validate_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the model is unreachable or returns junk, the node falls back to defaults and&lt;br&gt;
records a warning. The app always produces a card.&lt;/p&gt;
&lt;h2&gt;
  
  
  Writing the card from facts only
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;card_writer&lt;/code&gt; prompt is deliberately constrained: the model may only use the&lt;br&gt;
verified facts, and every place name must be wrapped in &lt;code&gt;[[double brackets]]&lt;/code&gt; so&lt;br&gt;
it can be checked afterwards.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a check pulls every &lt;code&gt;[[name]]&lt;/code&gt; back out and compares it to the OSM list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;unknown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;unknown_names&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;place_names&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;warnings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown place names: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That check is the difference between "the model wrote a nice card" and "the model&lt;br&gt;
wrote a nice card that sends you to a park that does not exist."&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The 1B model invented a place.&lt;/strong&gt; On one run &lt;code&gt;llama3.2:1b&lt;/code&gt; wrote a name that
was not in the OSM data. The bracketed-name check caught it and logged a
warning instead of shipping it. Not every run — the honest number is in the
table below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OSMnx drops latitude/longitude when it saves a projected graph.&lt;/strong&gt; The GPX
export came out with empty coordinates until the loader inverse-projected the
&lt;code&gt;x&lt;/code&gt;/&lt;code&gt;y&lt;/code&gt; node attributes back to WGS84.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;weasyprint&lt;/code&gt; would not install on Windows.&lt;/strong&gt; I print the HTML from Edge
headless (or a browser) instead, and treat the PDF as best-effort.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;[FIELD TEST — add anything the walk itself exposed: a dead end, a retraced&lt;br&gt;
segment, a muddy path, the light arriving earlier than the card said.]&lt;/p&gt;

&lt;h2&gt;
  
  
  The model comparison
&lt;/h2&gt;

&lt;p&gt;Same request, same start point, same map — only the model changes. The router has&lt;br&gt;
no LLM, so route quality is identical; the interesting columns are the&lt;br&gt;
hallucination count and the card length.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Pace read&lt;/th&gt;
&lt;th&gt;Words&lt;/th&gt;
&lt;th&gt;Hallucinated names&lt;/th&gt;
&lt;th&gt;Length error&lt;/th&gt;
&lt;th&gt;Peak RAM&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qwen2.5:3b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;normal&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.3%&lt;/td&gt;
&lt;td&gt;259 MB&lt;/td&gt;
&lt;td&gt;106 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qwen2.5:1.5b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;brisk&lt;/td&gt;
&lt;td&gt;69&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;4.4%&lt;/td&gt;
&lt;td&gt;260 MB&lt;/td&gt;
&lt;td&gt;16 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;llama3.2:1b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;slow&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;9.5%&lt;/td&gt;
&lt;td&gt;274 MB&lt;/td&gt;
&lt;td&gt;76 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;template (no model)&lt;/td&gt;
&lt;td&gt;normal&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.3%&lt;/td&gt;
&lt;td&gt;273 MB&lt;/td&gt;
&lt;td&gt;65 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All three fit under 0.5 GB of model memory. The 3B read the pace correctly and&lt;br&gt;
produced the shortest, most on-target card; the smaller models misread the pace,&lt;br&gt;
which moves the target length and the length error. Full columns live in&lt;br&gt;
&lt;code&gt;eval/results.csv&lt;/code&gt;; the harness is &lt;code&gt;eval/compare.py&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The whole point: the screen should lose.&lt;/p&gt;




&lt;h3&gt;
  
  
  References
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Ollama structured outputs — &lt;a href="https://docs.ollama.com/capabilities/structured-outputs" rel="noopener noreferrer"&gt;https://docs.ollama.com/capabilities/structured-outputs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;"Ollama Structured Outputs in Practice — Getting Type-Safe JSON from Local LLMs
with Pydantic" — &lt;a href="https://dev.to/jangwook_kim_e31e7291ad98/ollama-structured-outputs-in-practice-getting-type-safe-json-from-local-llms-with-pydantic-m38"&gt;https://dev.to/jangwook_kim_e31e7291ad98/ollama-structured-outputs-in-practice-getting-type-safe-json-from-local-llms-with-pydantic-m38&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;"Your LLM Returns JSON That Isn't JSON: A Robust Structured-Output Pipeline for
Local Models" — &lt;a href="https://dev.to/syed_anwar/your-llm-returns-json-that-isnt-json"&gt;https://dev.to/syed_anwar/your-llm-returns-json-that-isnt-json&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
