<?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: Mohd. Ibrahim Iqbal</title>
    <description>The latest articles on DEV Community by Mohd. Ibrahim Iqbal (@ibrahimiqbal).</description>
    <link>https://dev.to/ibrahimiqbal</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%2F473979%2F63a2bd1a-2d0d-425a-b6bc-af4a1dd2618b.jpeg</url>
      <title>DEV Community: Mohd. Ibrahim Iqbal</title>
      <link>https://dev.to/ibrahimiqbal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ibrahimiqbal"/>
    <language>en</language>
    <item>
      <title>cubby: I made my wifi password searchable by meaning, not memory</title>
      <dc:creator>Mohd. Ibrahim Iqbal</dc:creator>
      <pubDate>Wed, 21 Oct 2026 11:25:00 +0000</pubDate>
      <link>https://dev.to/ibrahimiqbal/cubby-i-made-my-wifi-password-searchable-by-meaning-not-memory-3o7d</link>
      <guid>https://dev.to/ibrahimiqbal/cubby-i-made-my-wifi-password-searchable-by-meaning-not-memory-3o7d</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;cubby&lt;/strong&gt; is a memory bank for the things I copy and then lose: a wifi password, a &lt;code&gt;docker run&lt;/code&gt; one-liner, a flight confirmation number, an address. You do two things with it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dump.&lt;/strong&gt; Paste anything and press enter. No title, no tag, no folder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask.&lt;/strong&gt; Later, type what you remember, in your own words. They don't have to be the words you saved it with.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An open-weight embedding model, EmbeddingGemma 300M, runs inside the browser tab and ranks what you saved by meaning. There is no server, no account and no API key.&lt;/p&gt;

&lt;p&gt;On my 20-item test set, semantic search found the right item &lt;strong&gt;20 out of 20 times&lt;/strong&gt;. Plain keyword search found it &lt;strong&gt;14 out of 20&lt;/strong&gt;. The six it missed are queries like &lt;em&gt;"how do I reach the property manager"&lt;/em&gt; against a note that says &lt;em&gt;"landlord's number: 555-0199, text don't call."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Who gets to be lazier: anyone who has ever said "I know I saved that somewhere" and then scrolled for ten minutes. The only job left for you is to paste.&lt;/p&gt;

&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%2Fxcr9swyc7xkkcmmrb7hc.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%2Fxcr9swyc7xkkcmmrb7hc.jpg" alt="cubby, the UI at a glance" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://cubby-6mn8.onrender.com" rel="noopener noreferrer"&gt;https://cubby-6mn8.onrender.com&lt;/a&gt; (first load downloads the model, about 300 MB, once)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpohyg72c71sgclybo31s.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpohyg72c71sgclybo31s.gif" alt="dump, ask, copy, in under 5 seconds" width="600" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A 10-second intro clip is &lt;a href="https://github.com/ibrahim-iqbal/cubby-hf26-wk3/blob/main/docs/design/post/00-intro.mp4" rel="noopener noreferrer"&gt;in the repo&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/ibrahim-iqbal/cubby-hf26-wk3" rel="noopener noreferrer"&gt;https://github.com/ibrahim-iqbal/cubby-hf26-wk3&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MIT licensed. React 19 + Vite, a static site with no backend.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  The model only measures meaning. It never writes a word.
&lt;/h3&gt;

&lt;p&gt;The week before this I shipped &lt;a href="https://dev.to/ibrahimiqbal/hushquote-quotes-that-stay-yours-3ldl"&gt;HushQuote&lt;/a&gt;, which asked a 270M model to &lt;em&gt;generate&lt;/em&gt; structured text. The write-up is mostly a list of ways it hallucinated. The lesson: a small open model is reliable at one thing, turning text into an embedding, and unreliable at writing prose. So cubby only asks it for the thing it is good at.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Who does it&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Items that share your exact words&lt;/td&gt;
&lt;td&gt;Plain lexical overlap (prefix / substring / exact)&lt;/td&gt;
&lt;td&gt;Must be exact. Keyword hits are the floor the model can't drop below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Items that mean the same thing in different words&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;EmbeddingGemma 300M, in the browser&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The one thing a small model earns its place for&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Which wins when they disagree&lt;/td&gt;
&lt;td&gt;A fixed &lt;code&gt;0.7·semantic + 0.3·lexical&lt;/code&gt; blend&lt;/td&gt;
&lt;td&gt;A tuned weight, not a per-query judgment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Writing anything in your results&lt;/td&gt;
&lt;td&gt;Nobody&lt;/td&gt;
&lt;td&gt;Small models hallucinate; retrieval doesn't&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&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%2Fi1durt8i0hhaesu26qqu.png" 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%2Fi1durt8i0hhaesu26qqu.png" alt="Architecture: everything inside the box runs in your browser tab" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Saving never waits on the model. The item is written first and its vector is filled in behind it:&lt;/p&gt;

&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%2Fzg7jlima8z96ygasgjp9.png" 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%2Fzg7jlima8z96ygasgjp9.png" alt="Dump flow: the item is saved first, the vector backfills" width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Asking blends two scores, so an exact keyword is never beaten by a weak semantic guess:&lt;/p&gt;

&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%2Ff4tccwk0nal4g8jw088o.png" 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%2Ff4tccwk0nal4g8jw088o.png" alt="Ask flow: semantic and lexical scores blended 70/30" width="560" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The test set, in full
&lt;/h3&gt;

&lt;p&gt;20 fixed (dump, query) pairs. Each query is ranked against the &lt;strong&gt;whole&lt;/strong&gt; 20-item corpus, so a hit means the right item rose above 19 distractors. Method and every row: &lt;a href="https://github.com/ibrahim-iqbal/cubby-hf26-wk3/blob/main/test/queries.md" rel="noopener noreferrer"&gt;&lt;code&gt;test/queries.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scorer&lt;/th&gt;
&lt;th&gt;Recall@3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;semantic only (EmbeddingGemma cosine)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20/20&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hybrid (shipped: &lt;code&gt;0.7·semantic + 0.3·lexical&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20/20&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lexical only (plain keyword search)&lt;/td&gt;
&lt;td&gt;14/20&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Twenty items is small, and I wrote the paraphrases myself. This is a measured result on the exact case the product exists for, not a claim about harder queries at scale. I'm committing to the same fixed set for future runs rather than moving the goalposts.&lt;/p&gt;

&lt;p&gt;The six keyword misses, for the record:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;query&lt;/th&gt;
&lt;th&gt;saved note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;refund deadline for the apartment booking&lt;/td&gt;
&lt;td&gt;Airbnb Lisbon — free cancellation until Jan 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sherlock holmes address&lt;/td&gt;
&lt;td&gt;123 Baker Street, London NW1 6XE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;what flowers should I get for mother&lt;/td&gt;
&lt;td&gt;mom's birthday is March 14, she likes tulips&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;how do I reach the property manager&lt;/td&gt;
&lt;td&gt;landlord's number: 555-0199, text don't call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;generate a self-signed TLS certificate&lt;/td&gt;
&lt;td&gt;openssl req -x509 -newkey rsa:4096 ...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;what time is my teeth cleaning&lt;/td&gt;
&lt;td&gt;dentist appointment moved to 2:30pm Thursday&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The parts that fought back
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A 300 MB first load.&lt;/strong&gt; A service worker (via &lt;code&gt;vite-plugin-pwa&lt;/code&gt;) caches the weights, so later visits are instant and work offline. On a slow connection the first load is a real wait, so the UI shows a live log of every shard as it lands instead of a fake progress bar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Matryoshka truncation to 256 dimensions.&lt;/strong&gt; EmbeddingGemma emits 768-dim vectors. I keep the first 256 and renormalize, which the model is trained to allow. Each item's vector is a third the size, and recall on the test set did not change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task prefixes.&lt;/strong&gt; The model is trained with task prefixes, so cubby always adds &lt;code&gt;task: search result | query:&lt;/code&gt; to an ask and &lt;code&gt;title: none | text:&lt;/code&gt; to a dump.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lexical matching had to work on characters, not whole words.&lt;/strong&gt; The first version needed a full-word match, so typing "doc" found nothing until you finished "docker". It now scores exact (1.0), prefix (0.9) and substring (0.6) per query token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallback.&lt;/strong&gt; If the model fails to load, cubby drops to keyword-only search and says so in the footer and the model card. You lose paraphrase matching, not the app.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Checking the privacy claim yourself
&lt;/h3&gt;

&lt;p&gt;"Nothing uploaded" is easy to say. Here is what I actually measured on the live site, and how you can repeat it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the &lt;a href="https://cubby-6mn8.onrender.com" rel="noopener noreferrer"&gt;live app&lt;/a&gt; with DevTools open on the Network tab.&lt;/li&gt;
&lt;li&gt;On the very first visit you'll see the ~300 MB model download from &lt;code&gt;huggingface.co&lt;/code&gt;. On later visits the weights come from the cache, but a small &lt;code&gt;generation_config.json&lt;/code&gt; request to &lt;code&gt;huggingface.co&lt;/code&gt; can still appear &lt;strong&gt;at page load&lt;/strong&gt;. Hugging Face can see your IP address and browser when that happens, the same as for any web request.&lt;/li&gt;
&lt;li&gt;Once the page has loaded, clear the log. Dump a note, ask a question, switch the theme. In my run, that produced &lt;strong&gt;zero&lt;/strong&gt; new requests.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I did not test the export button. It makes a local file and has no reason to touch the network, but I haven't confirmed it.&lt;/p&gt;

&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%2Fx43jsbcqauif6z3mfag0.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%2Fx43jsbcqauif6z3mfag0.jpg" alt="Illustration of the Network panel: one model fetch at load, nothing after. Drawn by me, not a screenshot, so repeat the steps above in your own DevTools" width="800" height="1386"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Four things in cubby only work because the weights are open:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The model is a file, not a service.&lt;/strong&gt; The app ships it as a static asset. There is no API key, no per-call bill, no vendor who can change the pricing, and no outage that isn't mine. A hosted embedding API would have required sending every note you save to someone's server, which is the problem I set out to avoid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I could see what was inside it.&lt;/strong&gt; Matryoshka truncation and the task prefixes are documented behaviour of the open model, and I could test both myself. Truncating a closed model's embeddings to save space is not something I could have checked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy you can inspect.&lt;/strong&gt; Because inference happens in your tab, the claim "your notes stay here" is something you can verify in DevTools, as above, instead of something you take on trust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It works with no signal.&lt;/strong&gt; The app shell and the weights are both cached. Closed APIs stop working on a plane or in a basement. This doesn't.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Saving costs nothing to think about.&lt;/strong&gt; One paste and one keypress. There are no fields: no title, no tag, no folder. Every notes app I've left asked me to organize first, and that is the work I always skipped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finding costs one sentence.&lt;/strong&gt; On my test set, keyword search sent me hunting in 6 queries out of 20. EmbeddingGemma sent me hunting in none (right item in the top 3).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's fast once warm.&lt;/strong&gt; In one run on the live site (headless browser, model already cached, one saved item), the right result was on screen about 170 ms after I entered the query. That is a single measurement, not a benchmark, and it will grow with a bigger collection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The slow part happens once.&lt;/strong&gt; The 300 MB download is the sloth's one big effort. After that, embedding happens quietly in the background while you carry on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm not going to claim hours saved. I haven't measured that, and a number I made up would not belong in a post about checking claims.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's honestly still missing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Text only. No images and no OCR.&lt;/li&gt;
&lt;li&gt;No sync between devices. Export and import of JSON is the only way to move data, on purpose, because there is no server to sync through.&lt;/li&gt;
&lt;li&gt;Hybrid ranking narrows the cases where semantic and lexical scores disagree, but it doesn't remove them.&lt;/li&gt;
&lt;li&gt;The test set is 20 items. It is the fixed set I'll keep re-running, not proof of anything at scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Android Share-to-cubby&lt;/strong&gt; is implemented through the PWA share target in the web manifest, but I have not tested it on a physical phone. iOS Safari's support for share targets is weak, so on iPhone you paste manually.&lt;/li&gt;
&lt;/ul&gt;

&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%2Fmjyhbi8mkcj6vvsa2n7r.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%2Fmjyhbi8mkcj6vvsa2n7r.jpg" alt="Illustration of the Android share sheet. Not captured on a device: I haven't tested this on a phone yet" width="800" height="1386"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma.&lt;/strong&gt; EmbeddingGemma 300M is the whole retrieval engine. Every ranked result exists because that model embedded the note on-device, with task-prefixed inputs and 256-dim Matryoshka truncation. It never generates text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Render.&lt;/strong&gt; A static site with no backend, deployed from the repo's &lt;code&gt;render.yaml&lt;/code&gt; blueprint and redeployed on every push.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Live: &lt;a href="https://cubby-6mn8.onrender.com" rel="noopener noreferrer"&gt;https://cubby-6mn8.onrender.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/ibrahim-iqbal/cubby-hf26-wk3" rel="noopener noreferrer"&gt;https://github.com/ibrahim-iqbal/cubby-hf26-wk3&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Five themes (terminal, paper, mono, cobalt, ember), each in light and dark, checked for WCAG AA contrast with &lt;code&gt;test/contrast.mjs&lt;/code&gt;:&lt;/li&gt;
&lt;/ul&gt;

&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%2Fo1el4c6kdvvo480h3qka.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%2Fo1el4c6kdvvo480h3qka.jpg" alt="The five themes in light and dark" width="800" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>gemma</category>
      <category>opensource</category>
    </item>
    <item>
      <title>HushQuote: quotes that stay yours</title>
      <dc:creator>Mohd. Ibrahim Iqbal</dc:creator>
      <pubDate>Wed, 14 Oct 2026 09:49:44 +0000</pubDate>
      <link>https://dev.to/ibrahimiqbal/hushquote-quotes-that-stay-yours-3ldl</link>
      <guid>https://dev.to/ibrahimiqbal/hushquote-quotes-that-stay-yours-3ldl</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/events/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;p&gt;&lt;strong&gt;What I Built:&lt;/strong&gt; HushQuote. Speak a job description, get an itemized, editable quote - then a PDF. A small open-weight model (Gemma 3 270M) runs entirely inside the browser tab, on WebGPU when it's there and WASM when it isn't. No account, no server, no install. The client's name and the day rate never leave the device they were typed on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2tcghr9ift2t2ihslh0h.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2tcghr9ift2t2ihslh0h.gif" alt="Real job typed in, the model extracts it, one line confirmed, client named - all on the live deploy" width="760" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Try your own phrasing, not just the demo's. If a line shows up that you never said, that's a known model quirk documented below, not a broken app - tap the × to drop it; totals recompute instantly either way.&lt;/p&gt;

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

&lt;p&gt;Every tradesperson I know who works for themselves does the same thing after a job: sits in the van, opens a notes app or a half-remembered Excel template, and tries to turn "forty square meters, two days, plus materials" into something a client will actually pay against. Then they either forget a line, round a number wrong, or just don't send the quote for three days because writing it feels like a second job.&lt;/p&gt;

&lt;p&gt;The invoicing SaaS market solved this with a $15/month subscription and an account you have to make, for software that's worse at the one job that actually matters: turning what you just said out loud into a document. That's the gap HushQuote is built for - it's cheap enough to be a real side-hustle tool, not a line item.&lt;/p&gt;

&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%2Frpb7odi23rrr7eh00srj.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%2Frpb7odi23rrr7eh00srj.jpg" alt="Cover page - model downloading, then the lock-open reveal" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;The whole app is one living document, not a wizard:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Margin&lt;/strong&gt; - a rail on the side holding your raw words in monospace, dictation-tape style.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Sheet&lt;/strong&gt; - an always-visible, real quote document. Line items typeset directly onto it as the model reads your words.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Command Bar&lt;/strong&gt; - mic or textarea, one submit button, nothing else.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Say or type the job. The model reads it into structured line items - description, quantity, unit, rate - and they land on the sheet looking exactly like what you'd hand a client, except every model-sourced line is visually provisional: a dashed border and an "Estimated. Yours to set." label, until you tap to confirm it. Math is never done by the model; totals are always computed in plain JavaScript from whatever's actually on the sheet.&lt;/p&gt;

&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%2Ffa9xcjmdkj8muem9osmb.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%2Ffa9xcjmdkj8muem9osmb.jpg" alt="The Sheet mid-extraction - confirmed and still-estimated lines side by side" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A Dashboard sits in front of the Sheet once you have more than one quote going: every saved quote in one list, with stat cards for quote count, total quoted value, and how many lines across all your drafts are still sitting on a model's estimate instead of your own number.&lt;/p&gt;

&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%2F4abs0nabgwcvu1nv2u0m.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%2F4abs0nabgwcvu1nv2u0m.jpg" alt="Dashboard - quote count, quoted value, and lines still needing a rate, with an illustrated empty state" width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I built it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stack, at a glance:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runtime:&lt;/strong&gt; React 19 + Vite, deployed as a static site on Render - no backend, no database, nothing running server-side at all&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; Gemma 3 270M-instruct (Q8_0 GGUF), via &lt;code&gt;wllama&lt;/code&gt; (llama.cpp compiled to WebAssembly/WebGPU), downloaded once into the browser tab&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation:&lt;/strong&gt; zod schemas (&lt;code&gt;src/lib/schema.js&lt;/code&gt;) checking every model response before it reaches the sheet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; &lt;code&gt;localStorage&lt;/code&gt; only - quotes, business profile, and rate library never leave the device&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF export:&lt;/strong&gt; the browser's native print dialog, no added dependency&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The model only extracts. The app decides what's true.
&lt;/h3&gt;

&lt;p&gt;That's the one sentence that governs every design choice here. A 270-million-parameter model is small enough to ship as a file instead of a service, but it is also small enough to be wrong in specific, repeatable ways - so nothing it says is trusted until code checks it.&lt;/p&gt;

&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%2Fhac37oudwnxc2618bccc.png" 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%2Fhac37oudwnxc2618bccc.png" alt="Flow: mic or text input through Gemma 3 270M, JSON-schema validation, one repair retry, and either a confirmed sheet line or a fallback to manual mode" width="800" height="720"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Concretely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Structured output, not a hopeful prompt.&lt;/strong&gt; &lt;code&gt;wllama&lt;/code&gt;'s &lt;code&gt;createChatCompletion&lt;/code&gt; supports OAI-compatible &lt;code&gt;response_format: {type:'json_schema'}&lt;/code&gt; natively - the model is sampling against a grammar that only lets valid tokens through, not just being asked nicely for JSON.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything still gets zod-validated.&lt;/strong&gt; A grammar guarantees the braces close; it says nothing about whether the content inside them is right. Every response goes through a real schema check before it touches the sheet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One repair generation, then honest surrender.&lt;/strong&gt; If the first response fails validation, the model gets one more try with the exact error attached. If that fails too, the app does &lt;em&gt;not&lt;/em&gt; guess - it drops back to manual mode and the user types the line themselves. A bad extraction costs a few seconds, never a wrong number going out silently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model never sees or touches the totals.&lt;/strong&gt; Subtotal, tax, and total are pure functions over whatever line items are actually on the sheet (&lt;code&gt;src/lib/totals.js&lt;/code&gt;) - they recompute instantly on every edit, independent of what the model ever said.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What I got wrong, and how I know
&lt;/h3&gt;

&lt;p&gt;I ran all 20 of my own test transcripts (&lt;code&gt;test/transcripts.md&lt;/code&gt;) through the real pipeline rather than trusting the two I'd eyeballed early on, and the results were worse, and more specific, than I expected.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;What happened&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1, 3&lt;/td&gt;
&lt;td&gt;Bathroom tiling (clean / with correction)&lt;/td&gt;
&lt;td&gt;Correct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6, 16&lt;/td&gt;
&lt;td&gt;Garden cleanup / fence staining with a price correction&lt;/td&gt;
&lt;td&gt;Mostly correct - 16 even picked up the corrected rate, not the original&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2, 7, 13&lt;/td&gt;
&lt;td&gt;Logo package / tap repair / photography&lt;/td&gt;
&lt;td&gt;Real job fragmented into 2-3 duplicate lines, flat fees landed in the wrong field&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4, 8, 10, 11, 12, 18, 19&lt;/td&gt;
&lt;td&gt;Fence, deck rebuild, painting, electrical, Chen deck, gutter cleaning, drywall&lt;/td&gt;
&lt;td&gt;Right-ish first item, &lt;strong&gt;plus a fabricated "Callout" line that doesn't exist anywhere in the transcript&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5, 17&lt;/td&gt;
&lt;td&gt;Lawn+hedges / dog walking&lt;/td&gt;
&lt;td&gt;Right item count, duplicated 2-5x, wrong units&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;9, 14&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Blog posts / callout+valve&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Model ignored the real transcript and echoed an unrelated internal example almost word for word&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;20&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;"Just an inspection, no numbers"&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Model invented a fully priced fake quote for a job that explicitly had no numbers in it&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That "Callout" line showing up in 10 of 18 fresh runs turned out to be my own few-shot example leaking into unrelated outputs - not a rate-library bug, confirmed by clearing the rate library mid-test and watching it persist. Transcript 20 is the one that actually matters: a model inventing prices for a job the user described as having none is exactly the failure "the LLM extracts, the app calculates" exists to make survivable, but a quote that reaches the pending-chip UI with a confidently wrong number on it is still a UX cost, even if it's a recoverable one.&lt;/p&gt;

&lt;p&gt;I tried the obvious fix - dropping the few-shot example entirely and relying on the JSON schema alone. It killed both of the worst cases, but introduced a new one (the model wrote the literal string &lt;code&gt;"${1}"&lt;/code&gt; as a description on every line). Not a clean win either way, so I shipped the known, documented behavior instead of an unproven swap days before the deadline. Full writeup, including a confirmed-separate truncation bug and its fix, is in &lt;code&gt;docs/RISKS.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update, after publishing:&lt;/strong&gt; shipped the narrower fix instead. &lt;code&gt;src/lib/filterHallucinations.js&lt;/code&gt; drops a model-output line when its description is a bare "callout" and the transcript never said any form of that word - the single leak that hit 10 of the 18 transcripts above. It's deliberately narrow: "Labor" leaks the same way sometimes, but "Labor" is correct far more often than it's a leak (transcript 1's "two days labor" needs that line to survive), so the same bare-word check there would trade a rare false positive for a common false negative. It does not touch the other two failure rows (9/14's verbatim echo, 20's fully invented quote) - those need the model to recognize it has no grounding at all, which a regex over one field can't do.&lt;/p&gt;

&lt;p&gt;Here's the safety net those two unfixed rows still rely on, in case you hit either one: tap the × on a wrong line, the total recomputes immediately.&lt;/p&gt;

&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%2Fjinvc791qudp1c7qko0v.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%2Fjinvc791qudp1c7qko0v.jpg" alt="Before: three pending lines, including one that's off, total $300" width="800" height="600"&gt;&lt;/a&gt;&lt;br&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%2Fl5o1flbqe18k89k3d8x6.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%2Fl5o1flbqe18k89k3d8x6.jpg" alt="After: the bad line removed with one tap, total recomputes to $0" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why open innovation matters here
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The model is a file the app ships, not a service you rent.&lt;/strong&gt; No API key, no per-request bill, no vendor who can change pricing under you. The entire cost of running this for a thousand quotes is the same as running it for one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client names and day rates never leave the device.&lt;/strong&gt; A plumber's client list is exactly the kind of data a side-hustle tool shouldn't be phoning home with, and "trust us" isn't verifiable the way "look, no network tab activity" is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It works with no signal.&lt;/strong&gt; A job site with no wifi and bad cell coverage is a normal Tuesday for a tradesperson. Closed APIs don't work there. This does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Running it myself, honestly, instead of hiding the rough edges&lt;/strong&gt;, is also the open-source story: the 20-transcript table above exists because I could actually inspect why the model did what it did, and the fixes I made (schema validation, the repair step, the token-budget bump, and now the Callout filter) are fixes to &lt;em&gt;my own code&lt;/em&gt;, not a support ticket to a vendor.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How this is different from the other Side Hustle freelance tools
&lt;/h2&gt;

&lt;p&gt;A few other Week 2 submissions land in the same space - invoice and receipt sorters for freelancers. The closest ones (InvoicePilot AI, VaultLedger AI, FreelanceAI) are all built the same way under the hood: Ollama or a Python backend the user has to install and run alongside the app. That's "local" in the sense that your data doesn't leave your machine, but it's not "no install" - you're still standing up a server process first.&lt;/p&gt;

&lt;p&gt;The one submission actually in the same product category - turning a messy brief into a priced estimate, not sorting receipts - is &lt;a href="https://dev.to/harshalranjhani/scopecraft-a-clearer-scope-a-fairer-quote-and-a-model-you-can-own-2cla"&gt;ScopeCraft&lt;/a&gt;, a genuinely good build around Qwen3-0.6B. Even it runs a small Node HTTP server locally to serve inference. HushQuote has no server step at all, local or otherwise.&lt;/p&gt;

&lt;p&gt;HushQuote has no backend at all, not even a local one. It's a static site. The model downloads into the browser tab itself and runs there via WebAssembly/WebGPU. Close the tab, and there's nothing left running on your machine to manage. That's a stronger version of the same privacy pitch, and it's the actual reason this is a PWA instead of a desktop app.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt; - Gemma 3 270M-instruct is the only model HushQuote ships, and it's the entire extraction pipeline: every line item on the sheet exists because this model read the transcript, not a cloud call standing in for it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Render&lt;/strong&gt; - the live deploy (&lt;code&gt;hushquote.onrender.com&lt;/code&gt;) is a Render static site, auto-deploying on every push to &lt;code&gt;main&lt;/code&gt; with no build server of my own to maintain.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The deterministic regex co-extraction pass flagged in &lt;code&gt;docs/RISKS.md&lt;/code&gt; as the next real lever on the two still-unfixed failure rows (verbatim echo, invented-quote), if there's time.&lt;/li&gt;
&lt;li&gt;Finding an actual tradesperson to hand this to and reporting back what they asked for next - the challenge's bonus round, not done yet at time of writing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://hushquote.onrender.com" rel="noopener noreferrer"&gt;https://hushquote.onrender.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/ibrahim-iqbal/hushquote" rel="noopener noreferrer"&gt;https://github.com/ibrahim-iqbal/hushquote&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full build log / decisions / bugs found:&lt;/strong&gt; &lt;code&gt;docs/PLAN.md&lt;/code&gt; and &lt;code&gt;docs/RISKS.md&lt;/code&gt; in the repo - the honest version of this post, day by day.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>hf26challenge</category>
      <category>gemma</category>
      <category>webassembly</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Touch Grass: voice dares that get you off the screen, running on-device with Gemma 3 270M</title>
      <dc:creator>Mohd. Ibrahim Iqbal</dc:creator>
      <pubDate>Sat, 10 Oct 2026 18:20:41 +0000</pubDate>
      <link>https://dev.to/ibrahimiqbal/touch-grass-voice-dares-that-get-you-off-the-screen-running-on-device-with-gemma-3-270m-fi5</link>
      <guid>https://dev.to/ibrahimiqbal/touch-grass-voice-dares-that-get-you-off-the-screen-running-on-device-with-gemma-3-270m-fi5</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%2F9vzt3xgqb1oyioa3663y.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%2F9vzt3xgqb1oyioa3663y.jpg" alt="Touch Grass banner" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&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;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Touch Grass&lt;/strong&gt; — say how much time and energy you've got, and it gives you one short, concrete dare to go do outside. Finish it, snap a photo as proof, and it counts toward a daily streak. No feed, no infinite scroll, no "just one more dare" loop — one dare, go outside, come back later.&lt;/p&gt;

&lt;p&gt;It's a PWA: install it, and after the first load it keeps working with no signal, because the shell and the model are both cached by a service worker.&lt;/p&gt;

&lt;p&gt;Who it's for: anyone who opens their phone meaning to do one quick thing and surfaces 40 minutes later having done none of them. Tell it you've got 10 minutes and low energy, it won't give you a half-marathon.&lt;/p&gt;

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

&lt;p&gt;🔗 &lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://touch-grass-bkii.onrender.com" rel="noopener noreferrer"&gt;https://touch-grass-bkii.onrender.com&lt;/a&gt;&lt;br&gt;
💻 &lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/ibrahim-iqbal/touch-grass" rel="noopener noreferrer"&gt;https://github.com/ibrahim-iqbal/touch-grass&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The whole app is a little outdoor world you stand in. The painted pixel-art scene fills the screen, your character stands in the grass, and the controls sit at the edges like a game HUD — nothing floats in a box over the middle.&lt;/p&gt;

&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%2Flj86p57lk8exrbvq2428.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%2Flj86p57lk8exrbvq2428.jpg" alt="Night scene — the HUD pins to the corners, the painted world fills the middle, and your character stands in the grass" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Flip the theme and the same world re-lights from a cozy night to a warm daytime — the backdrop art crossfades, the moon slides out and the sun comes up.&lt;/p&gt;

&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%2Fr2l7ixoi0bybsuimsjl1.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%2Fr2l7ixoi0bybsuimsjl1.jpg" alt="Day scene — same world, re-lit for daytime" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tap the mic, say your time and energy, and a dare slides up from the bottom in a dialogue ribbon — tagged with roughly how long it takes and how much it'll cost you — with a "prove it outside" button that opens the camera.&lt;/p&gt;

&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%2Fg1m7m3ffn1yn4ientbrp.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%2Fg1m7m3ffn1yn4ientbrp.jpg" alt="A generated dare in the quest ribbon, with time and energy tags and a prove-it-outside button" width="800" height="500"&gt;&lt;/a&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/ibrahim-iqbal" rel="noopener noreferrer"&gt;
        ibrahim-iqbal
      &lt;/a&gt; / &lt;a href="https://github.com/ibrahim-iqbal/touch-grass" rel="noopener noreferrer"&gt;
        touch-grass
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      voice dares that get you outside — local gemma, zero backend
    &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;touch grass&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;voice dares that get you outside. say how much time and energy you have
it gives you a short outdoor dare, you snap a photo as proof when you're done.&lt;/p&gt;
&lt;p&gt;built for the hacktoberfest open-source ai challenge, week 1: "touch grass."&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;why open&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;dare generation runs on-device with &lt;a href="https://huggingface.co/onnx-community/gemma-3-270m-it-ONNX" rel="nofollow noopener noreferrer"&gt;gemma 3 270m&lt;/a&gt; via transformers.js, not a hosted api&lt;/li&gt;
&lt;li&gt;works after the first load even with no signal — service worker caches the page and the model&lt;/li&gt;
&lt;li&gt;nothing you say or type leaves your browser; your hugging face token and elevenlabs key (both optional) are stored locally and used directly from your device&lt;/li&gt;
&lt;li&gt;swap the model, edit &lt;code&gt;dares.json&lt;/code&gt;, or rip out the generation step entirely — it's all plain js&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;running it&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;just a static site, no build step.&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;python3 -m http.server 8000&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;open &lt;code&gt;localhost:8000&lt;/code&gt;. voice input needs chrome or edge (web speech api support
is patchy elsewhere)…&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/ibrahim-iqbal/touch-grass" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Voice input needs Chrome or Edge (Web Speech API support is patchy elsewhere). Camera proof needs HTTPS or localhost — both are satisfied on the live link.&lt;/p&gt;

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

&lt;p&gt;Plain HTML/CSS/JS, no framework, no build step, no backend. A few deliberate choices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Default mode needs zero setup.&lt;/strong&gt; Say your time/energy/place, it matches you against a small curated dare bank (&lt;code&gt;dares.json&lt;/code&gt;) and speaks the result with the browser's own &lt;code&gt;speechSynthesis&lt;/code&gt;. This always works, offline, with nothing to configure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional smart layer: &lt;a href="https://huggingface.co/onnx-community/gemma-3-270m-it-ONNX" rel="noopener noreferrer"&gt;Gemma 3 270M&lt;/a&gt; via &lt;a href="https://huggingface.co/docs/transformers.js" rel="noopener noreferrer"&gt;transformers.js&lt;/a&gt;.&lt;/strong&gt; Paste a Hugging Face token into settings and dare generation switches to the model, running entirely in your browser via WASM — not a hosted API call. Nothing about your request ever leaves your device.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional voice layer: ElevenLabs&lt;/strong&gt;, same pattern — your key, used directly from your browser, swappable back to the built-in voice at any time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The UI is a game HUD, not a form.&lt;/strong&gt; The painted pixel-art world fills the screen and your character stands in it; the level, stats and controls pin to the corners like an RTS, and each dare appears in a slide-up dialogue ribbon like an RPG. The backdrop art is day/night and crossfades when you toggle the theme, with a sliding sun/moon, drifting clouds and foliage that fills in as your streak grows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A service worker caches the app shell&lt;/strong&gt; so the whole thing — including the model, once downloaded — works with no connection after the first load. That's the actual point of the "touch grass" theme: the app should survive exactly the situation where you're outside with no signal, which is where it's most useful.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-origin isolation (COOP/COEP)&lt;/strong&gt; configured on the Render deploy so the WASM runtime gets the full, fast execution path rather than a degraded fallback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Camera proof&lt;/strong&gt; uses &lt;code&gt;getUserMedia&lt;/code&gt; + canvas, with a defensive timeout — confirmed by hand that &lt;code&gt;getUserMedia&lt;/code&gt; can hang indefinitely on a device with no camera rather than reject, which otherwise traps the UI with no way out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployed on &lt;a href="https://render.com" rel="noopener noreferrer"&gt;Render&lt;/a&gt;&lt;/strong&gt; as a static site, auto-deploying from &lt;code&gt;main&lt;/code&gt; on every push.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Two reasons this specific shape only works because the model is open-weight:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It can genuinely run with nothing hosted by me.&lt;/strong&gt; There's no backend, no API key of mine in the loop, no bill that scales with usage. A user who wants the smarter dare generation brings their own free Hugging Face token and the model downloads and runs on &lt;em&gt;their&lt;/em&gt; device. I can't see their request, because it never reaches a server I control. A closed API would force a hosted proxy — which means a cost center, a privacy boundary I'd be asking users to trust, and a single point of failure the moment I stop paying the bill.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It made "fully optional" actually honest.&lt;/strong&gt; Because Gemma 3 270M is small enough to run client-side, the "smart" mode isn't a crippled free tier nudging you toward a paywall — it's a real optional upgrade with a real reliability floor underneath it (the curated dare bank) that works identically whether or not you ever touch the settings gear. That's a hard thing to build honestly on top of a closed, metered API; it's the default shape of an open-weight one.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Render&lt;/strong&gt; — static site, deployed and auto-deploying from this repo, with COOP/COEP headers configured for the WASM runtime&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt; — Gemma 3 270M running fully client-side via transformers.js for live dare generation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of ElevenLabs&lt;/strong&gt; — optional nicer voice output for the spoken dare, key used directly from the browser&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>OSS Buddy: a local Gemma that picks weekend-sized issues so my cousin can finally land his first PR</title>
      <dc:creator>Mohd. Ibrahim Iqbal</dc:creator>
      <pubDate>Fri, 02 Oct 2026 17:52:59 +0000</pubDate>
      <link>https://dev.to/ibrahimiqbal/oss-buddy-a-local-gemma-that-picks-weekend-sized-issues-so-my-cousin-can-finally-land-his-first-pr-277f</link>
      <guid>https://dev.to/ibrahimiqbal/oss-buddy-a-local-gemma-that-picks-weekend-sized-issues-so-my-cousin-can-finally-land-his-first-pr-277f</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;I have a cousin. He has been telling me he wants to contribute to open source for about a year. Every month or two he sends me a screenshot of a repo and asks "is this a good one to start with?" I say yes. He opens it, scrolls for twenty minutes, closes the tab. The screenshot never turns into a PR.&lt;/p&gt;

&lt;p&gt;I used to tell him "just pick a good first issue." That advice is useless. Good first issues live in repos with four hundred files and eighty open issues each. You still have to pick. He had already been doing that for a year.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;OSS Buddy&lt;/strong&gt;. You point it at any GitHub repo and a local Gemma 3 model tells you three things in about twenty-three seconds:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What the project actually does, in two plain sentences&lt;/li&gt;
&lt;li&gt;Up to three &lt;code&gt;good first issue&lt;/code&gt; tickets ranked easiest to hardest, with time estimates&lt;/li&gt;
&lt;li&gt;Which specific directory to start reading&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nothing leaves his laptop. No account, no API key, no credit card. He downloads Gemma once, forgets about it, the tool works forever.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fgithub.com%2Fibrahim-iqbal%2Foss-buddy%2Fraw%2Fmain%2Fdemo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fgithub.com%2Fibrahim-iqbal%2Foss-buddy%2Fraw%2Fmain%2Fdemo.gif" alt="demo" width="800" height="524"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One command, no cloud calls. Terminal recording against &lt;code&gt;kiwix/kiwix-android&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Benchmarks on five real repos
&lt;/h3&gt;

&lt;p&gt;Measured end-to-end on an M-series Mac with Gemma 3 4B via Ollama. Each run is one command, no warmup, no cache.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repo&lt;/th&gt;
&lt;th&gt;Stack&lt;/th&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;Good-first-issues found&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;kiwix/kiwix-android&lt;/td&gt;
&lt;td&gt;Kotlin&lt;/td&gt;
&lt;td&gt;27.9s&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;simonoppowa/OpenNutriTracker&lt;/td&gt;
&lt;td&gt;Flutter&lt;/td&gt;
&lt;td&gt;23.5s&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RetroMusicPlayer/RetroMusicPlayer&lt;/td&gt;
&lt;td&gt;Kotlin&lt;/td&gt;
&lt;td&gt;22.1s&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;neovim/neovim&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;22.0s&lt;/td&gt;
&lt;td&gt;0 (no &lt;code&gt;good first issue&lt;/code&gt; label)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fastapi/fastapi&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;22.0s&lt;/td&gt;
&lt;td&gt;0 (none open at test time)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Average: &lt;strong&gt;~23 seconds&lt;/strong&gt;, zero cloud bytes. The two zeros are honest limits — neovim doesn't use the &lt;code&gt;good first issue&lt;/code&gt; label at all, and fastapi had none open when I ran it. OSS Buddy says so plainly rather than hallucinating issues.&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/ibrahim-iqbal" rel="noopener noreferrer"&gt;
        ibrahim-iqbal
      &lt;/a&gt; / &lt;a href="https://github.com/ibrahim-iqbal/oss-buddy" rel="noopener noreferrer"&gt;
        oss-buddy
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A local Gemma 3 reads any GitHub repo and picks weekend-sized good-first-issues for your friend. Nothing leaves your machine.
    &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;OSS Buddy&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/ibrahim-iqbal/oss-buddy/demo.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fibrahim-iqbal%2Foss-buddy%2FHEAD%2Fdemo.gif" alt="demo"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Point it at a GitHub repo. A local Gemma 3 model reads the README, the
top-level file tree, and the open &lt;code&gt;good first issue&lt;/code&gt; tickets, then
tells you:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What the project does, in plain words&lt;/li&gt;
&lt;li&gt;Up to three issues that look weekend-sized, ranked easiest to hardest&lt;/li&gt;
&lt;li&gt;Where in the repo to start reading&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Nothing leaves your machine. Your GitHub token stays local. The model
runs on your laptop via &lt;a href="https://ollama.com" rel="nofollow noopener noreferrer"&gt;Ollama&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;A friend of mine kept opening huge OSS repos, scrolling for twenty
minutes, closing the tab. He wanted to contribute. He did not know
where to start. Every guide said "pick a good first issue" and every
repo had dozens.&lt;/p&gt;
&lt;p&gt;This reads the repo for him and says "here, pick one of these three
start reading here."&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Install&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;Needs Python 3.9+, the GitHub CLI, and Ollama.&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; ollama (macOS)&lt;/span&gt;
brew install ollama
brew services start ollama&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/ibrahim-iqbal/oss-buddy" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;~150 lines of stdlib Python. No &lt;code&gt;pip install&lt;/code&gt;. MIT.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Open-source AI used:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://ai.google.dev/gemma" rel="noopener noreferrer"&gt;Gemma 3 (4B)&lt;/a&gt; — Google's open-weight model, running on my laptop&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; — open-source runtime for the model&lt;/li&gt;
&lt;li&gt;GitHub CLI (&lt;code&gt;gh&lt;/code&gt;) for repo data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pipeline (lazy on purpose):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    gh api  (readme + top-level tree + good-first-issues)
                        │
                        ▼
                 one tuned prompt
                        │
                        ▼
         POST localhost:11434 (ollama)
                        │
                        ▼
                   gemma 3 4b
                        │
                        ▼
         stream the answer to the terminal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three HTTP calls to GitHub, one HTTP call to a server running on my own laptop. That is the whole pipeline. The Python file is small enough to read in one sitting.&lt;/p&gt;

&lt;p&gt;The prompt went through three real iterations in the git history. The first version produced great summaries but ended every answer with "Would you like me to delve deeper?" — chatty, off-brand. The second version started paraphrasing issue titles instead of quoting them, which was worse than useless because then you couldn't &lt;code&gt;grep&lt;/code&gt; for the issue on GitHub. The third version pins Gemma: "Quote the issue number and the EXACT title from the data below. Do not paraphrase or invent." That one shipped.&lt;/p&gt;

&lt;h3&gt;
  
  
  Labels aren't consistent across repos
&lt;/h3&gt;

&lt;p&gt;Early version only looked for the literal &lt;code&gt;good first issue&lt;/code&gt; label. Lost half the repos. The fix was a cheap one — try the three common spellings in order:&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;good first issue&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;good-first-issue&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;beginner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple, but it is the difference between the tool working on one repo in two and almost all of them.&lt;/p&gt;

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

&lt;p&gt;He does not need to sign up for anything before he can start contributing to open source. He already has GitHub, git, and a dozen tabs open. A new contributor does not need another sign-up flow before the one he actually came for. Open-source AI removes that friction entirely — he downloads the model once, forgets about it, and the tool just works.&lt;/p&gt;

&lt;p&gt;The second thing matters more. His GitHub token and the repos he explores never leave his laptop. Some of those repos are private — half-built side projects, work stuff. A closed API would have meant sending every README and every issue title to a third party's servers. With Gemma running locally that whole class of problem does not exist.&lt;/p&gt;

&lt;p&gt;The third thing is practical. It runs on a plane. It runs in a cafe with slow wifi. It runs inside offices that block third-party API calls. It costs zero rupees forever. For someone taking their first step into open source every friction point is a chance to quit — the open stack removes a few of them.&lt;/p&gt;

&lt;p&gt;And the whole chain is readable. 150 lines of Python. An open-weight model with public weights. A runtime with public source. If my cousin wants to see what the LLM did with his repo, he can trace it from his terminal all the way down to the weights. That is the open-source story — not just the model, the whole stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hand-over
&lt;/h2&gt;

&lt;p&gt;I sent him the install snippet on WhatsApp — three commands, no sign-up, no API key.&lt;/p&gt;

&lt;p&gt;He hasn't run it yet as of this edit. I'd rather say that plainly than leave a placeholder sitting in a published post. The honest version of "built for a friend" includes the part where the friend hasn't gotten to it yet — that's a real, common outcome of building something for someone else's spare time, not a reason to dress it up. I'll update this section the moment he does, with whatever he actually says, not a tidied-up version of it.&lt;/p&gt;

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

&lt;p&gt;Skipping DevRelay sessions on this one. The story is in the git log instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;31b5e08&lt;/code&gt; initial scaffold: fetch readme + good-first-issues via gh&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;4a8dbb5&lt;/code&gt; wire up ollama + first prompt that asks for 3 ranked issues&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ede3d7f&lt;/code&gt; tighten prompt (no outro, quote exact issue titles)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;b430d72&lt;/code&gt; add demo gif + vhs tape&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bf1babc&lt;/code&gt; readme: link to the dev.to writeup&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;0d26567&lt;/code&gt; readme: proper case + benchmarks table + cover image&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Six commits, a few hours of work spread across one evening and the next morning.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Short list, in the order I'd attack them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Deeper issue analysis.&lt;/strong&gt; Right now it reads the issue title and body. Next step: fetch linked files and the diff of the last PR to touch them, so "where to start" becomes line-level, not directory-level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remember what my cousin has already tried.&lt;/strong&gt; A tiny local SQLite log of attempted issues so OSS Buddy stops suggesting the same ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A thin web wrapper for people without Ollama.&lt;/strong&gt; Deployed somewhere small (likely a free Render tier) that calls a hosted Gemma via Groq. The CLI stays local-first; the web version is a convenience.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;--contributing&lt;/code&gt; flag&lt;/strong&gt; that reads the project's CONTRIBUTING.md and tells you what tests to run and how to open a PR, so the suggestion ends with "and here is exactly how this project wants it submitted."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of it changes the shape of the tool. It's all deeper reading of the same three sources.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Best Use of Gemma&lt;/strong&gt; — Gemma 3 (4B) via Ollama is the only model used. The entire pipeline depends on it. No fallback, no closed-model path, no "bring your own API key." Just Gemma, local, free.&lt;/p&gt;

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