<?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: Jarvis Wick</title>
    <description>The latest articles on DEV Community by Jarvis Wick (@jarviswick).</description>
    <link>https://dev.to/jarviswick</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%2F4161065%2Fbcce4f2c-7321-4ff4-a32d-0dd8ecd424d5.png</url>
      <title>DEV Community: Jarvis Wick</title>
      <link>https://dev.to/jarviswick</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jarviswick"/>
    <language>en</language>
    <item>
      <title>Slow Mode: the laziest way to go outside</title>
      <dc:creator>Jarvis Wick</dc:creator>
      <pubDate>Sat, 24 Oct 2026 05:30:49 +0000</pubDate>
      <link>https://dev.to/jarviswick/slow-mode-the-laziest-way-to-go-outside-5hg3</link>
      <guid>https://dev.to/jarviswick/slow-mode-the-laziest-way-to-go-outside-5hg3</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Trailhead&lt;/strong&gt; is a local-first Streamlit app that turns a few taps into a short outdoor outing. You pick an activity, how long you have, and where you are; it pulls live weather and remaining daylight from Open-Meteo, then asks a locally running open-weight model — &lt;strong&gt;Gemma 3 4B via Ollama&lt;/strong&gt; — for an "outing card": a plain-language route, what to look and listen for this week, three off-screen challenges, and a one-line safety note. The card exports as printable HTML or PDF.&lt;/p&gt;

&lt;p&gt;For Week 3, I asked the sloth question: &lt;em&gt;what if the app made every decision, and the correct answer was "less"?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So in this story Trailhead gets &lt;strong&gt;Slow Mode&lt;/strong&gt;: one button that refuses to let you plan anything.&lt;/p&gt;

&lt;p&gt;Meet Theo — invented, but you know him. Theo will spend forty minutes choosing a podcast for a twenty-minute walk. He has 6,412 unread emails and a browser with 31 tabs, three of which are "best walking routes near me." He does not need another planner. He needs a planner that &lt;strong&gt;quits early&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Slow Mode is that. One click and it picks the lowest-effort option for every field: a &lt;strong&gt;walk&lt;/strong&gt; (no gear), &lt;strong&gt;15 minutes&lt;/strong&gt; (the floor), the &lt;strong&gt;nearest green space&lt;/strong&gt;, and a card whose only instruction is &lt;em&gt;"walk to the end of your street and back. That's it."&lt;/em&gt; Then it drops him on the "Take me outside" screen — a single line, a return time, and no UI to get lost in.&lt;/p&gt;

&lt;p&gt;The whole product becomes a permission slip to do almost nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lazy Payoff
&lt;/h2&gt;

&lt;p&gt;The challenge asks how much effort the project actually saves. In the story, Theo timed his old routine against Slow Mode:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;With Slow Mode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Choosing an activity&lt;/td&gt;
&lt;td&gt;4 min&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Picking a route&lt;/td&gt;
&lt;td&gt;6 min&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checking the weather&lt;/td&gt;
&lt;td&gt;2 min&lt;/td&gt;
&lt;td&gt;0 (automatic)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deciding when to leave&lt;/td&gt;
&lt;td&gt;3 min&lt;/td&gt;
&lt;td&gt;0 (return time set)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~15 min&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1 click&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fifteen minutes of deliberation replaced by one click and a piece of paper. Theo's review, invented but plausible: &lt;em&gt;"I wanted to argue with it and there was nothing to argue with."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The real payoff isn't the minutes. It's that the app has no settings to fiddle with in Slow Mode — nothing to optimize, no feed to scroll, no "just one more tweak." It ends the planning by removing the choices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;In the story: Theo opens Trailhead, hits &lt;strong&gt;Slow Mode&lt;/strong&gt;, and gets a card for a 15-minute walk to the end of his street. The printer makes one page. He takes it, walks for fifteen minutes, and comes back. The app never asks him for an account, a rating, or a streak.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The engine (real).&lt;/strong&gt; Slow Mode isn't a separate model — it's the same engine with the decisions pre-filled. The model is constrained to a JSON schema and validated with Pydantic, so the output is structured rather than prose I have to parse:&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&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;OLLAMA_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                       &lt;span class="c1"&gt;# default: gemma3:4b
&lt;/span&gt;    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&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_PROMPT&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
              &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&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&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;OUTING_CARD_JSON_SCHEMA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;# Ollama constrains generation
&lt;/span&gt;    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OutingCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="c1"&gt;# Pydantic validates
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Slow Mode (the story's idea).&lt;/strong&gt; It's a preset plus a stricter prompt: pick the smallest outing, the nearest green space, and exactly one thing to notice.&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;slow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UserInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WALK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;time_available_minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;location_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;nearest_green_space&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lon&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_card&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt_variant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;smallest possible outing&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;&lt;strong&gt;Everything else (real).&lt;/strong&gt; Open-Meteo's geocoding and forecast APIs are free and keyless, so the only network call carries a city name or a coordinate. &lt;code&gt;fpdf2&lt;/code&gt; renders the PDF, &lt;code&gt;render_html&lt;/code&gt; gives a print-CSS fallback, and SQLite keeps a local journal so past cards stay readable offline.&lt;/p&gt;

&lt;p&gt;Stack: Python 3.15, Streamlit, httpx, the official &lt;code&gt;ollama&lt;/code&gt; client, Pydantic, fpdf2, SQLite. No Node, no build step, no custom JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;For a tool built around doing less, open matters in four very practical ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A small local model is enough.&lt;/strong&gt; Slow Mode generates a &lt;em&gt;deliberately tiny&lt;/em&gt; outing. A 4B model on a laptop handles that in seconds, with no cloud round trip and no account to create. The laziness extends to the setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It works at the trailhead.&lt;/strong&gt; Generating a card is local inference against a local &lt;code&gt;ollama serve&lt;/code&gt;. There's no signal required at the moment the app is supposed to get you out the door.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing to monetize means nothing to optimize for.&lt;/strong&gt; No telemetry, no usage stats (Streamlit's are disabled in config), no feed. Your walk isn't a data point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swappable by design.&lt;/strong&gt; &lt;code&gt;OLLAMA_MODEL&lt;/code&gt; changes the brain in one env var. If you want a faster, dumber model for a 15-minute walk, that's a legitimate choice — and it's yours to make.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The open part is why the app can be this boring. There's no growth team to satisfy.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;I'm entering &lt;strong&gt;Best Use of Gemma&lt;/strong&gt; — every card is generated by Gemma 3 4B running locally through Ollama, constrained by a JSON schema and validated with Pydantic.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>gemma</category>
      <category>python</category>
    </item>
    <item>
      <title>Trailhead's First $19: a side hustle from the trailhead (Week 2)</title>
      <dc:creator>Jarvis Wick</dc:creator>
      <pubDate>Sat, 17 Oct 2026 17:36:24 +0000</pubDate>
      <link>https://dev.to/jarviswick/trailheads-first-19-a-fictional-side-hustle-from-the-trailhead-week-2-1n7b</link>
      <guid>https://dev.to/jarviswick/trailheads-first-19-a-fictional-side-hustle-from-the-trailhead-week-2-1n7b</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Trailhead&lt;/strong&gt; is a local-first Streamlit app that turns a few taps into a short outdoor outing. You pick an activity, how long you have, and where you are; it pulls live weather and remaining daylight from Open-Meteo, then asks a locally running open-weight model — &lt;strong&gt;Gemma 3 4B via Ollama&lt;/strong&gt; — for an "outing card": a plain-language route, what to look and listen for this week at this latitude, three off-screen challenges, and a one-line safety note. The card exports as a printable HTML file or a PDF.&lt;/p&gt;

&lt;p&gt;Then I asked the Week 2 question: &lt;em&gt;who would actually pay for this?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Hikers won't. Hikers have phones. The person with money to spend and no signal is the person &lt;strong&gt;selling the walk&lt;/strong&gt; — the independent nature guide standing at the trailhead, handing paper to strangers.&lt;/p&gt;

&lt;p&gt;So in this story, Trailhead grows one button: &lt;strong&gt;"Export a guided-walk packet."&lt;/strong&gt; Same open-weight engine underneath. It just prints a stack of cards instead of one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Customer Who Isn't Real
&lt;/h2&gt;

&lt;p&gt;Mara Okonkwo is invented, but she's the person I had in mind. She runs &lt;em&gt;Foothills Slow Walks&lt;/em&gt;, a weekend side hustle: $19 per walker for a 90-minute "listen to the forest" loop in the Blue Ridge foothills. Eight walkers on a good Saturday. One of them, most weeks, comes back the next Saturday.&lt;/p&gt;

&lt;p&gt;Her problem wasn't the walk. It was the &lt;strong&gt;five minutes before&lt;/strong&gt; it. She wanted to hand each walker a card with what was happening &lt;em&gt;that week&lt;/em&gt; — the first of the spring peepers, the tulip poplars dropping, which birds had just arrived. She'd tried a phone app. It needed a cloud call and the trailhead has one bar of signal. It also wanted her route notes and her location on somebody else's server, which, for a guide whose whole product is a secret route she spent two years finding, was a non-starter.&lt;/p&gt;

&lt;p&gt;So: a product that runs where she works, keeps her route private, costs nothing per card, and spits out something &lt;strong&gt;she can hand to someone with no phone at all&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;In the story: Mara opens the app on a laptop in the passenger seat, types &lt;em&gt;Slow Walk — Great Falls Loop, 90 minutes, 8 walkers&lt;/em&gt;, hits &lt;strong&gt;Export packet&lt;/strong&gt;, and thirty seconds later the printer spits out a single letter-size page: six cards, each with the same route but a slightly different "challenge of the day," cut lines and all. She hands them out with a pen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The engine (real).&lt;/strong&gt; The model is constrained to a JSON schema and validated with Pydantic, so the output is structured instead of prose I have to parse:&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&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;OLLAMA_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                       &lt;span class="c1"&gt;# default: gemma3:4b
&lt;/span&gt;    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&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_PROMPT&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
              &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&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&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;OUTING_CARD_JSON_SCHEMA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;# Ollama constrains generation
&lt;/span&gt;    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OutingCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="c1"&gt;# Pydantic validates
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If validation fails it retries once; if Ollama is unreachable it falls back to a template card, so the app never dead-ends in front of a customer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The packet (the story's one new idea).&lt;/strong&gt; Instead of rendering one card, the export loops over &lt;code&gt;n&lt;/code&gt; walkers, generates a card per walker with a shared route but a varied challenge, then tiles them six-to-a-page for cutting:&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;cards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;generate_card&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;walkers&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="n"&gt;pdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TrailheadPacket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start_with_6_cards&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_cutout_card&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# 2 x 3 grid, letter size
&lt;/span&gt;&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;slow_walk_packet.pdf&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;&lt;strong&gt;Everything else (real).&lt;/strong&gt; Open-Meteo's geocoding and forecast APIs are free and keyless, so the only network call carries a city name or a coordinate. &lt;code&gt;fpdf2&lt;/code&gt; handles the PDF, &lt;code&gt;render_html&lt;/code&gt; gives a print-CSS fallback, and SQLite stores past packets so Mara can reprint last week's without regenerating it.&lt;/p&gt;

&lt;p&gt;Stack: Python 3.15, Streamlit, httpx, the official &lt;code&gt;ollama&lt;/code&gt; client, Pydantic, fpdf2, SQLite. No Node, no build step, no custom JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;For a side hustle, "why open" stops being philosophy and becomes the margin.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It runs where the work happens.&lt;/strong&gt; Generating a packet is local inference against a local &lt;code&gt;ollama serve&lt;/code&gt;. A closed API would strand Mara at the trailhead with one bar of signal — the exact place the product has to work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Her route stays hers.&lt;/strong&gt; The only thing that ever leaves the laptop is a city name to a keyless weather API. No account, no telemetry, no server holding the route she spent two years finding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The math only works because inference is free.&lt;/strong&gt; At $19 per walker, paying per-token for eight cards a session eats the side hustle. Local inference has no per-request cost, no quota, no pricing page that moves under her.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;She can tune it to her woods.&lt;/strong&gt; &lt;code&gt;OLLAMA_MODEL&lt;/code&gt; swaps the brain in one env var; a fine-tune on &lt;em&gt;her&lt;/em&gt; route's species would make the seasonal highlights better than any general-purpose API knows how to be.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The open part isn't a talking point here. It's the whole reason the product clears a price a real person would pay.&lt;/p&gt;

&lt;h2&gt;
  
  
  The $19
&lt;/h2&gt;

&lt;p&gt;In the story, Mara's first packet costs her nothing to produce and sells for $19 as part of a Saturday walk. She doesn't pay for the software. She pays — the second time — with a request.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Can it know my loop's birds better?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Which is, it turns out, a feature request an open-weight model can actually grant.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;I'm entering &lt;strong&gt;Best Use of Gemma&lt;/strong&gt; — the card generator is Gemma 3 4B running locally through Ollama, constrained by a JSON schema and validated with Pydantic.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>gemma</category>
      <category>python</category>
    </item>
    <item>
      <title>Trailhead: Gemma 3 plans your walk on your own laptop - then gets you off the screen</title>
      <dc:creator>Jarvis Wick</dc:creator>
      <pubDate>Sat, 10 Oct 2026 10:36:04 +0000</pubDate>
      <link>https://dev.to/jarviswick/trailhead-gemma-3-plans-your-walk-on-your-own-laptop-then-gets-you-off-the-screen-3i2a</link>
      <guid>https://dev.to/jarviswick/trailhead-gemma-3-plans-your-walk-on-your-own-laptop-then-gets-you-off-the-screen-3i2a</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Ja4V8s28Ck" rel="noopener noreferrer"&gt;
        Ja4V8s28Ck
      &lt;/a&gt; / &lt;a href="https://github.com/Ja4V8s28Ck/trailhead" rel="noopener noreferrer"&gt;
        trailhead
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Trailhead&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A local-first Streamlit app that uses an open-weight LLM to turn a few taps into a short outdoor outing, then gets you &lt;strong&gt;off the screen&lt;/strong&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What it does&lt;/h2&gt;
&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pick your outing&lt;/strong&gt; — Activity (walk, run, hike, birding, nature observation, photography), time (15 min – 3 hrs), and location (city name or lat/lon)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Get conditions&lt;/strong&gt; — Fetches live weather, sunrise/sunset, and daylight remaining from Open-Meteo (free, no API key)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate a card&lt;/strong&gt; — A locally running open-weight model (Gemma 3 4B via Ollama) creates an "outing card" with
&lt;ul&gt;
&lt;li&gt;A general route suggestion (no fake map data)&lt;/li&gt;
&lt;li&gt;What to look and listen for &lt;strong&gt;this week at your latitude and season&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;3 "off-screen challenges" (e.g., "Sit silently 3 minutes and count the sounds you hear")&lt;/li&gt;
&lt;li&gt;A one-line safety note based on actual weather and daylight&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go outside&lt;/strong&gt; — Download the card as HTML or PDF, or tap &lt;strong&gt;"Take me outside"&lt;/strong&gt; for…&lt;/li&gt;
&lt;/ol&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Ja4V8s28Ck/trailhead" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Trailhead&lt;/strong&gt; is a local-first Streamlit app that turns a few taps into a short outdoor outing — and then deliberately gets out of your way.&lt;/p&gt;

&lt;p&gt;You pick three things: an activity (walk, run, hike, birding, nature observation, photography), how much time you have (15 min to 3 hours), and where you are (a city name, or raw lat/lon). Trailhead pulls the live weather and remaining daylight from Open-Meteo, then asks a locally running open-weight model — &lt;strong&gt;Gemma 3 4B via Ollama&lt;/strong&gt; — for an "outing card":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A route suggestion&lt;/strong&gt; in plain language ("loop through the nearest green space, turn around at the halfway mark") — deliberately no invented trail names and no fake map data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seasonal highlights&lt;/strong&gt; for &lt;em&gt;this week at this latitude&lt;/em&gt;: what to look and listen for right now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three off-screen challenges&lt;/strong&gt; — concrete, phone-free things to do, like "sit silently for three minutes and count every distinct sound you hear."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A one-line safety note&lt;/strong&gt; grounded in the actual conditions ("dark in 40 min — bring a headlight or turn around by 6:20pm").&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then it tries to make the screen the shortest part of the experience. Download the card as a self-contained HTML file or a PDF and print it. Or hit &lt;strong&gt;"Take me outside"&lt;/strong&gt;, which replaces the whole UI with a single line — &lt;em&gt;"See you in 42 minutes"&lt;/em&gt; — and a return time. When you get back, you jot down 1–3 notes. Everything is stored locally in SQLite, and past cards stay readable offline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who it's for:&lt;/strong&gt; anyone who wants a reason to step outside but doesn't want to spend twenty minutes scrolling for one. The whole point is under 60 seconds on-screen, then out the door with a piece of paper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;The core is open-source AI running locally, so let me start there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemma 3 4B via Ollama, with structured output.&lt;/strong&gt; I didn't want to parse prose out of a model at runtime, so the model is constrained to a JSON schema and the result is validated with Pydantic:&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&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;OLLAMA_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                      &lt;span class="c1"&gt;# default: gemma3:4b
&lt;/span&gt;    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&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_PROMPT&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
              &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&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&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;OUTING_CARD_JSON_SCHEMA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# Ollama constrains generation
&lt;/span&gt;    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OutingCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                    &lt;span class="c1"&gt;# Pydantic validates
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ollama keeps generation inside the schema; Pydantic catches the rest. If either fails, we retry once, and if Ollama is unreachable the app falls back to a template card, so it's never a dead end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weather without an API key.&lt;/strong&gt; Open-Meteo's geocoding and forecast endpoints are free and keyless, so the only thing Trailhead ever sends over the network is a city name or a pair of coordinates. Everything else — the prompt, the model call, your notes — stays on the machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rendering.&lt;/strong&gt; &lt;code&gt;render_html&lt;/code&gt; produces a self-contained file with print CSS, so "download" really means "print it and take it with you." &lt;code&gt;fpdf2&lt;/code&gt; produces a matching PDF. Both are pure Python.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage.&lt;/strong&gt; A single SQLite file. Cards are serialized with &lt;code&gt;model_dump_json()&lt;/code&gt; and read back through the same Pydantic models, so a saved card and a fresh one are the same object.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; Python 3.15, Streamlit, httpx, the official &lt;code&gt;ollama&lt;/code&gt; client, Pydantic, fpdf2, SQLite. No Node, no build step, no custom JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;For Trailhead the answer is concrete:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It works where you actually need it.&lt;/strong&gt; Generating a card is a local inference call — no cloud API. Once the model is pulled, the generator runs entirely against a local &lt;code&gt;ollama serve&lt;/code&gt;. A closed API would make the app useless the moment you lose signal, which is exactly where an outdoor app should work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location data never leaves the device.&lt;/strong&gt; Trailhead knows where you walk. The only network call carries a city name or a coordinate to a keyless weather API — no account, no telemetry (Streamlit's usage stats are disabled in config), and no server holding a record of your habits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model is swappable.&lt;/strong&gt; &lt;code&gt;OLLAMA_MODEL=llama3.2:3b streamlit run app.py&lt;/code&gt; is the entire model-swap story. A 3B model is faster on a laptop; a larger one gives richer seasonal detail. You could fine-tune your own and point the same code at it — no provider, no pricing page, no rate limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing to run.&lt;/strong&gt; No keys, no billing, no quota. A hobby project that gets people outside shouldn't depend on anyone's credit card, least of all the runner's.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The open part isn't decoration. It's the reason the app can run offline, keep your data, and be changed by whoever runs it.&lt;/p&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/1234616489" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;I'm entering &lt;strong&gt;Best Use of Gemma&lt;/strong&gt; — the entire card generator is Gemma 3 4B running locally through Ollama, constrained by a JSON schema and validated with Pydantic.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>gemma</category>
      <category>python</category>
    </item>
    <item>
      <title>Groundtruth: a revision partner for my cousin that only answers from his notes</title>
      <dc:creator>Jarvis Wick</dc:creator>
      <pubDate>Sun, 04 Oct 2026 15:05:09 +0000</pubDate>
      <link>https://dev.to/jarviswick/groundtruth-a-revision-partner-for-my-cousin-that-only-answers-from-his-notes-1nfk</link>
      <guid>https://dev.to/jarviswick/groundtruth-a-revision-partner-for-my-cousin-that-only-answers-from-his-notes-1nfk</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;My cousin asked me to be his revision helper. He was working through college coursework and wanted someone to quiz him on it. I could say yes to the first session and not to the six after it, so I built the part that does not need me in the room.&lt;/p&gt;

&lt;p&gt;He had a specific problem before this one. He re-reads his notes until they feel familiar, then walks into an exam believing he knows the material. When he asked a chatbot to quiz him instead, it explained things that were never in his syllabus, produced citations that looked completely real and told him he was right when he was not. That is not a bug in that tool. It has every fact on the internet and no idea which lines of his notes are the ones that matter.&lt;/p&gt;

&lt;p&gt;Groundtruth reads his notes and does three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;writes questions that can be answered only from that material&lt;/li&gt;
&lt;li&gt;grades his free-text answers against it&lt;/li&gt;
&lt;li&gt;shows the exact line that settles every verdict&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When his notes do not cover something, it says so instead of guessing.&lt;/p&gt;

&lt;p&gt;He is using it now. I did not manage to get a quotable reaction out of him, which I choose to read as a good sign.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/1232766472" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The video is the bundled cell biology sample, so you can watch the refusal happen without supplying your own notes. Ask it about mitosis and it will tell you the notes never covered mitosis, because they do not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Ja4V8s28Ck" rel="noopener noreferrer"&gt;
        Ja4V8s28Ck
      &lt;/a&gt; / &lt;a href="https://github.com/Ja4V8s28Ck/groundtruth" rel="noopener noreferrer"&gt;
        groundtruth
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A revision partner
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Groundtruth&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A revision partner that only answers from a student's own notes and shows the exact line it used for every verdict. &lt;a href="https://vimeo.com/1232766472?share=copy&amp;amp;fl=sv&amp;amp;fe=ci" rel="nofollow noopener noreferrer"&gt;Demo link&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why I built it&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;My cousin asked me to be his revision helper. He was working through college coursework and wanted someone to quiz him on it. I could say yes to the first session and not to the six after it, so I built the part that does not need me in the room.&lt;/p&gt;
&lt;p&gt;He had a specific problem before this. He re-read his notes until they felt familiar, then walked into an exam believing he knew the material. When he asked a chatbot to quiz him instead, it explained things that were never in his syllabus, invented citations that looked real and told him he was right when he was not. That is not a bug in that tool. It has every fact…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Ja4V8s28Ck/groundtruth" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;a href="https://github.com/Ja4V8s28Ck/groundtruth" rel="noopener noreferrer"&gt;https://github.com/Ja4V8s28Ck/groundtruth&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Python and Streamlit, about 1,500 lines. You need &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; running and two models:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull qwen3:4b           &lt;span class="c"&gt;# writes and grades, 2.5 GB&lt;/span&gt;
ollama pull nomic-embed-text   &lt;span class="c"&gt;# finds passages, 274 MB&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;pip install -r requirements.txt&lt;/code&gt; and &lt;code&gt;streamlit run app.py&lt;/code&gt;. Click "Use the sample notes" and you have a working session in about 20 seconds. No API key, no account, nothing to sign up for.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;The model is not bolted onto a search box. Take it out and there is no product left, because there is no keyword path to writing a good revision question.&lt;/p&gt;

&lt;h3&gt;
  
  
  Citations you can check
&lt;/h3&gt;

&lt;p&gt;This is the part I care about most. Most AI study tools tell you whether you were right. This one shows why it thinks so, in the words of your own notes.&lt;/p&gt;

&lt;p&gt;Every chunk keeps the line range it came from. When the model grades an answer it has to return a verbatim quote and the chunk id. Back in Python that quote is checked against the real text with a sliding window match, which tolerates the model paraphrasing slightly and then returns the exact span from the file.&lt;/p&gt;

&lt;p&gt;If the model returns &lt;code&gt;correct&lt;/code&gt; and cannot produce a quote that exists in the notes, the verdict is downgraded rather than shown. You can open the file and check every claim the app made.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three guards, because a 4B model is generous
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;qwen3:4b&lt;/code&gt; is small enough to run on a laptop and loose enough to grade things correct that are not. Three checks in the store catch that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Quote verification.&lt;/strong&gt; A &lt;code&gt;correct&lt;/code&gt; verdict with no matching quote in the notes is never displayed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Term coverage.&lt;/strong&gt; If the retrieved passage covers almost none of the terms in the answer, the verdict is downgraded. This is what makes the refusal falsifiable. The app cannot be wrong by inventing and it cannot be wrong by being vague.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No spoilers.&lt;/strong&gt; The hint shown for a wrong answer is checked so it does not hand over the answer itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Term coverage came out of a failing test. A vague answer was being graded &lt;code&gt;incomplete&lt;/code&gt;, but that word implies the notes covered it and for a vague answer that is not knowable. So I made the claim falsifiable instead. If the notes barely mention what you wrote, the app is not permitted to assert that the notes covered it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The model thinks before it answers and you cannot stop it
&lt;/h3&gt;

&lt;p&gt;This was the surprise of the build. My machine has an RTX 4060 and Ollama reports the model running at 100% GPU, yet a call whose entire real answer was one word took 4.6 seconds and generated 166 tokens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wall 4.64s   generated 166 token done=stop
reply: 'Hmm, the user just asked me to reply with the single word "ready"...'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;qwen3&lt;/code&gt; is a reasoning model and Ollama ignores &lt;code&gt;think: false&lt;/code&gt; for it. So did &lt;code&gt;enable_thinking&lt;/code&gt; through the chat template kwargs and through the options bag. So did asking it in the system prompt not to reason.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attempt&lt;/th&gt;
&lt;th&gt;Tokens generated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;think: false&lt;/code&gt; (the default we send)&lt;/td&gt;
&lt;td&gt;166&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;chat_template_kwargs.enable_thinking=false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;133&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;options.enable_thinking=false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;162&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;System prompt "answer directly, do not reason"&lt;/td&gt;
&lt;td&gt;146&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Roughly 150 tokens of preamble on every single call and it is 80 to 90 percent of the latency. Nothing suppresses it on this model.&lt;/p&gt;

&lt;p&gt;What did help was noticing the calls were independent. Each passage is asked about separately and the calls share no state, so they now run in a thread pool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;build_session(count=8)   before   16.81s
build_session(count=8)   after    12.18s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is only 28 percent and the reason is worth being plain about. The GPU is saturated on generation, not on request overhead, so overlapping the calls hides the setup cost and nothing else. 35 tokens per second is simply what a 4B model generates on that chip. The real fix is a model that does not think and I did not have time to swap one and re-verify the guards before submitting.&lt;/p&gt;

&lt;h3&gt;
  
  
  A bug that only appeared once I added an embedding model
&lt;/h3&gt;

&lt;p&gt;Worth writing down, because it is the kind of failure that hides.&lt;/p&gt;

&lt;p&gt;My indexer looped over the chunks and embedded them one at a time, but it returned early as soon as the vector array existed. Chunk 0 got a real vector and every other row stayed zero. Semantic search filters zero scores out, so it cheerfully returned nothing but chunk 0 while the header still said the search was semantic. Grading was citing the first section of the notes and nothing else.&lt;/p&gt;

&lt;p&gt;It never fired while I had no embedding model installed, which is exactly the window in which I was testing everything else. The moment I pulled &lt;code&gt;nomic-embed-text&lt;/code&gt; it was live. The fix was one batched call for the whole document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rows with a real vector: 10 of 10
probe "sodium potassium pump"  -&amp;gt; chunks [3, 1, 6]
probe "glycolysis"             -&amp;gt; chunks [8, 3, 2]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;His notes are not mine to upload. Course material, a lecturer's slides and the record of what a student is weak at are not the builder's to hand to a third-party server. It runs over localhost, so the notes never leave the machine. There is no server to leak them to because there is no server.&lt;/p&gt;

&lt;p&gt;It has to work with no network. Revision happens at 7am in a quiet room the night before an exam and depending on a cloud API means depending on someone else's uptime and someone else's billing.&lt;/p&gt;

&lt;p&gt;There is a cost argument as well and it is the weakest of the four. A small model on your own hardware is free and renting an equivalent one costs a few cents a session. Free is not the interesting part. It's about keeping our data to ourselves.&lt;/p&gt;

</description>
      <category>hf26challenge</category>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hacktoberfest</category>
    </item>
  </channel>
</rss>
